Files
railwaysystem/build_ui/docs/portrait-render-diagnosis-20261108.md
T

13 KiB
Raw Blame History

学员个人画像页面「新模块不渲染」诊断报告

  • 诊断日期:2026-11-08
  • 涉及页面:src/views/train/portrait/index.vue(个人画像 / 学员 360)
  • 涉及接口:GET /train/analysis/portrait/student-360/{studentId}
  • 涉及后端:ruoyi-train 模块(StudentPortraitServiceImpl、TrainPortraitAnalyticsMapper)
  • 诊断方式:前端源码与编译产物核查、后端运行实例与编译产物时间比对、本地数据库(只读)表结构核对与 SQL 实测

一、结论摘要

  1. 直接原因(P0):画像聚合第 9 条 SQL selectDormContext 使用了 V1 旧表结构字段(train_dorm_auth.student_id / plan_id / dorm_id),而当前库为 V2 结构,实测报错 Unknown column 'da.dorm_id' in 'field list'。
  2. 该异常在 TrainAnalysisServiceImpl.getStudent360() 中被 catch 吞掉,仅记录 log.warn,导致返回体里 没有 portrait 节点。
  3. 前端所有新增模块包在 <div v-if="portrait"> 中,portrait 为 null 时整块不渲染,页面只剩老 360 内容 —— 即“旧信息仍在、新图表不出现”。
  4. 次要原因(P1):build_ui/dist 为 2026-11-03 构建,产物中不存在任何新画像代码;若访问的是该静态站点,即使后端修好也只会看到旧页面。
  5. 非缺陷项(P2):即使 portrait 正常返回,各图表自身还有数据门槛(雷达需 ≥3 个可用维度等),数据不足时会显示空态文案,属预期降级。

二、从组件引入到实际渲染的完整链路

源码 build_ui/src/views/train/portrait/*            ✅ 新代码存在(render 树校验通过)
   ↓ 构建
dev server :1026(vue-cli-service serve)           ✅ 从源码编译,随 HMR 更新
dist/index.html(2026-11-03 构建)                  ❌ 早于本次改动,产物中搜不到
                                                      "个人画像结论 / 个人能力雷达 / portrait-radar"
   ↓ 路由(sys_menu → train/portrait/index)         ✅ 页面可打开(旧内容可见)
   ↓ 接口 getStudent360
后端 :8096(2026-11-08 22:39:52 启动,class 22:39)  ✅ 新代码已加载
   ↓ StudentPortraitServiceImpl.getPortrait()
第 9 条 SQL selectDormContext                       ❌ Unknown column 'da.dorm_id'
   ↓ catch (Exception) 仅 log.warn                   ❌ 静默降级,portrait = null
   ↓ JSON 响应无 portrait 字段
前端 <div v-if="portrait">                          ❌ 结论 / 三图 / 趋势 / 风险全部不渲染
   ↓ 各图表自身数据门槛                              ⚠️ 数据不足时只显示空态
   ↓ echarts 容器宽高                                ⚠️ 隐藏容器中初始化为 0×0

链路断点唯一且确定:后端画像聚合 SQL 抛错 → 异常被吞 → portrait 缺失 → 前端整块 v-if 不成立。


三、断点定位与证据

3.1 前端:所有新模块挂在 v-if="portrait" 之下

src/views/train/portrait/index.vue:99

<!-- A2. 个人画像看板:全部数据仅属于当前学员本人,不展示任何其他学员信息 -->
<div v-if="portrait" class="portrait-extras">

该容器内依次包含:个人画像结论(综合分环、四项结论、标签、口径与数据截止)、三张图表卡(雷达 / 活跃度 / 目标达成)、七维画像明细、周趋势与跨班期、风险预警。portrait 为 null 时全部不产出 DOM。

3.2 后端:异常被吞,画像节点直接缺失

RuoYi-Vue/ruoyi-train/.../service/impl/TrainAnalysisServiceImpl.java:510-520

if (portraitService != null)
{
    try
    {
        vo.setPortrait(portraitService.getPortrait(studentId));
    }
    catch (Exception e)
    {
        log.warn("学员画像扩展数据生成失败, studentId={}, reason={}", studentId, e.getMessage());
    }
}

StudentPortraitServiceImpl.getPortrait() 内部按顺序执行:

fillAttendance(raw); fillStudy(raw); fillScores(raw, studentId);
fillExamCompletion(raw); fillDorm(raw, studentId);
fillClassBaseline(raw); fillTrend(raw);
StudentPortraitVo portrait = PortraitCalculator.build(raw);

任一 fillXxx 抛错都会使整个画像构建失败,当前只有 saveSnapshot 做了单条容错。

3.3 触发异常的具体 SQL

RuoYi-Vue/ruoyi-train/src/main/resources/mapper/train/TrainPortraitAnalyticsMapper.xml:137-150

<select id="selectDormContext" resultType="java.util.HashMap">
    select da.face_sync_status as face_sync_status,
           da.dorm_id          as dorm_id,
           (select count(1)
              from train_accommodation a2
             where a2.dorm_id = da.dorm_id
               and a2.enrollment_id &lt;&gt; #{enrollmentId}
               and a2.accommodation_status &lt;&gt; 2) as dorm_mates,
           (select max(a3.accommodation_status)
              from train_accommodation a3
             where a3.enrollment_id = #{enrollmentId}) as accommodation_status
    from train_dorm_auth da
    where da.student_id = #{studentId}
      and da.plan_id = #{planId}
    limit 1
</select>

这是 V1 旧结构写法。V2 中门禁授权通过 accommodation_id 关联(可参考旧 mapper 的既有正确写法 TrainAnalysisMapper.xml:395-413):

FROM train_accommodation a
JOIN train_enrollment e ON e.enrollment_id = a.enrollment_id
LEFT JOIN train_dormitory_info d ON d.dorm_id = a.dorm_id
LEFT JOIN train_dorm_auth auth ON auth.accommodation_id = a.accommodation_id

3.4 数据库层实证(本地库,只读)

逐表逐列核对画像 SQL 依赖,结论如下:

检查项 结果
train_theory_exam_score(is_final/plan_course_id/enrollment_id 等) 存在
train_student.extract_status、card_no、source_type、id_card_last4 存在
train_study_duration.resource_id、sync_time 存在
train_student_checkin.checkin_way、manual_flag 存在
train_course.c_duration、train_course_resource.c_id 存在
train_student_portrait(快照表) 存在
train_dorm_auth.student_id / plan_id / dorm_id 不存在

train_dorm_auth 实际列为: auth_id, accommodation_id, credential_type, credential_no, auth_status, retry_count, last_sync_time, last_error, face_sync_status, create_time, update_time

直接执行该 SQL 的结果:

DORM_SQL_FAIL: Unknown column 'da.dorm_id' in 'field list'

即:每打开一次画像页必然失败一次,与学员数据多少无关。

3.5 前端产物陈旧(次要原因)

检查项 结果
build_ui/dist/index.html 修改时间 2026-11-03(早于本次改动)
dist 中是否包含 个人画像结论 / 个人能力雷达 / portrait-radar 均未命中
前端 dev server 存在,端口 1026,vue-cli-service serve
端口 80 为 WAMP/Apache 默认站点,非本项目前端

若浏览器访问的是 dist 打包站点,则新组件根本不在产物中,必须重新 npm run build。

3.6 旧信息为何仍在

  • 基本信息、考勤记录/明细、培训履历、成绩、住宿门禁由另一条链路(analysisMapper 老查询)生成,与 portrait 无关,因此不受影响、始终有内容。
  • 老代码中“自主学习学时”本就是硬编码:

TrainAnalysisServiceImpl.java:463-465

// 5. 自主学习状态 (严格不造假)
vo.setHasStudyData(false);
vo.setStudyStatusDesc("暂无自主学习记录 / 待外部平台数据同步");

新卡片读取的是 portrait.dimensions[study];portrait 为 null 时新逻辑不执行,于是“旧文案仍在、新指标未出现”。


四、各组件显示条件

组件 / 区块 渲染条件 不满足时的表现
整个画像看板 profile.portrait != null 整块消失(当前状态)
综合分环形 + 四项结论 同上 同上
标签行 visibleTags.length > 0(已剔除 advantage 组) 不显示标签行
个人能力雷达 PortraitRadar 维度中 available === true 且 score != null 的 ≥3 个 “暂无可用的维度评分”
活跃度环形 PortraitActivity activity.parts 中 included === true 且 score != null 的 ≥1 个 “暂无活跃度分项数据”
个人目标达成 goalItems.length > 0(学时/课次/考核完成度/出勤率任一有值) “暂无可对照的计划目标”
周趋势 PortraitWeekly trend.weekly.length > 0 “暂无周维度学习与出勤记录”
跨班期对比 PortraitPeriods trend.periods.length > 0 “暂无跨班期记录”
风险预警 riskRules.length > 0 卡片整体不渲染

雷达最易凑不够 3 轴:无课次→考勤不可用;无学习记录→学习投入不可用;无成绩→考核能力不可用;无课次→进度不可用。此时仅剩“身份与数据质量”“综合画像”,雷达退化为空态。属正常降级,不是渲染故障。


五、让新图表正常渲染的前提条件

P0 必须满足(否则一定空白)

  1. 修复门禁 SQL:selectDormContext 改为 V2 关联方式,并按 enrollmentId 查询。
  2. 后端生效:修改 XML 后需重启 / 触发热加载(当前实例 22:39 启动)。
  3. 前端最新:
    • 走 :1026 dev server 时硬刷新(Ctrl+F5);
    • 走 dist 静态站点时必须重新 npm run build。

P1 强烈建议

  1. 逐条容错:在 StudentPortraitServiceImpl 的每个 fillXxx 外层加 try/catch,失败时置空并写入 meta.warnings,避免“一条 SQL 坏掉 → 整块画像消失”。

P2 数据层面(决定“有图”还是“空态”)

  1. 至少有 1 条参训记录(train_enrollment),否则返回空画像。
  2. 有课次(train_lesson)→ 考勤/进度维度可用,雷达才可能凑够 3 轴。
  3. 有学习时长(train_study_duration)→ 学习投入维度、周趋势学时柱有值。
  4. 有成绩(train_theory_exam_score,is_final = 1 且 del_flag = '0')→ 考核维度、考核均分虚线有值。
  5. 有多期参训 → 跨班期对比有 ≥2 根柱(单期也可画,仅一根)。

P3 渲染层面

  1. echarts 容器需在可见状态下初始化;若被隐藏 tab / v-show 包裹,需重新触发 resize(组件已用 ResizeObserver 兜底)。

六、修复建议(未实施,待确认)

6.1 门禁 SQL 改为 V2 结构

<select id="selectDormContext" resultType="java.util.HashMap">
    select da.face_sync_status as face_sync_status,
           a.dorm_id           as dorm_id,
           (select count(1)
              from train_accommodation a2
             where a2.dorm_id = a.dorm_id
               and a2.enrollment_id &lt;&gt; a.enrollment_id
               and a2.accommodation_status &lt;&gt; 2) as dorm_mates,
           a.accommodation_status as accommodation_status
    from train_accommodation a
    left join train_dorm_auth da on da.accommodation_id = a.accommodation_id
    where a.enrollment_id = #{enrollmentId}
    limit 1
</select>

同步调整:

  • TrainPortraitAnalyticsMapper.selectDormContext(...) 入参改为 @Param("enrollmentId") Long enrollmentId;
  • StudentPortraitServiceImpl.fillDorm(...) 改为传入 raw.enrollmentId。

6.2 画像构建逐条容错(建议)

private void safeFill(String name, PortraitRawData raw, Runnable action) {
    try { action.run(); }
    catch (Exception e) { raw.warnings.add(name + " 数据获取失败,已按数据不足处理"); log.warn(...); }
}

调用侧:safeFill("住宿门禁", raw, () -> fillDorm(raw, raw.enrollmentId)); —— 保证单个维度失败只降级,不隐藏整块画像。


七、快速自检步骤

  1. F12 → Network → student-360 响应中搜索 "portrait":
    • 无该字段 → 后端断点(本文根因);
    • 有但维度全空 → 数据问题,见第五节 P2。
  2. 后端日志搜索 学员画像扩展数据生成失败,若包含 Unknown column 'da.dorm_id',即本文根因。
  3. 观察页面检索框:候选列表版 = 新前端;否则看的是旧产物(dist)。

八、附:本次核查使用的关键路径

用途 路径
画像页面 build_ui/src/views/train/portrait/index.vue
图表组件 build_ui/src/views/train/portrait/components/Portrait*.vue、chartMixin.js
画像接口 build_ui/src/api/train/analysis.js(getStudent360)
画像组装 RuoYi-Vue/ruoyi-train/.../service/impl/TrainAnalysisServiceImpl.java
画像计算 RuoYi-Vue/ruoyi-train/.../service/impl/StudentPortraitServiceImpl.java、PortraitCalculator.java
画像 SQL RuoYi-Vue/ruoyi-train/src/main/resources/mapper/train/TrainPortraitAnalyticsMapper.xml
正确住宿/门禁关联参考 RuoYi-Vue/ruoyi-train/src/main/resources/mapper/train/TrainAnalysisMapper.xml
前端产物 build_ui/dist(2026-11-03,陈旧)