chore(sync): 同步当前工作区全部变更(学员学号可编辑并留空自动生成等)
This commit is contained in:
1 parent
01e949093a
commit
f5e8d7f564
531 files changed
+72978
-2720
No files matched your search
@@ -0,0 +1,278 @@
|
||||
# 学员个人画像页面「新模块不渲染」诊断报告
|
||||
|
||||
- 诊断日期: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`
|
||||
|
||||
```html
|
||||
<!-- A2. 个人画像看板:全部数据仅属于当前学员本人,不展示任何其他学员信息 -->
|
||||
<div v-if="portrait" class="portrait-extras">
|
||||
```
|
||||
|
||||
该容器内依次包含:个人画像结论(综合分环、四项结论、标签、口径与数据截止)、三张图表卡(雷达 / 活跃度 / 目标达成)、七维画像明细、周趋势与跨班期、风险预警。`portrait` 为 null 时全部不产出 DOM。
|
||||
|
||||
### 3.2 后端:异常被吞,画像节点直接缺失
|
||||
|
||||
`RuoYi-Vue/ruoyi-train/.../service/impl/TrainAnalysisServiceImpl.java:510-520`
|
||||
|
||||
```java
|
||||
if (portraitService != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
vo.setPortrait(portraitService.getPortrait(studentId));
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
log.warn("学员画像扩展数据生成失败, studentId={}, reason={}", studentId, e.getMessage());
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`StudentPortraitServiceImpl.getPortrait()` 内部按顺序执行:
|
||||
|
||||
```java
|
||||
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`
|
||||
|
||||
```xml
|
||||
<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 <> #{enrollmentId}
|
||||
and a2.accommodation_status <> 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`):
|
||||
|
||||
```xml
|
||||
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`
|
||||
|
||||
```java
|
||||
// 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 强烈建议
|
||||
|
||||
4. **逐条容错**:在 `StudentPortraitServiceImpl` 的每个 `fillXxx` 外层加 try/catch,失败时置空并写入 `meta.warnings`,避免“一条 SQL 坏掉 → 整块画像消失”。
|
||||
|
||||
### P2 数据层面(决定“有图”还是“空态”)
|
||||
|
||||
5. 至少有 1 条参训记录(`train_enrollment`),否则返回空画像。
|
||||
6. 有课次(`train_lesson`)→ 考勤/进度维度可用,雷达才可能凑够 3 轴。
|
||||
7. 有学习时长(`train_study_duration`)→ 学习投入维度、周趋势学时柱有值。
|
||||
8. 有成绩(`train_theory_exam_score`,`is_final = 1` 且 `del_flag = '0'`)→ 考核维度、考核均分虚线有值。
|
||||
9. 有多期参训 → 跨班期对比有 ≥2 根柱(单期也可画,仅一根)。
|
||||
|
||||
### P3 渲染层面
|
||||
|
||||
10. echarts 容器需在可见状态下初始化;若被隐藏 tab / `v-show` 包裹,需重新触发 `resize`(组件已用 `ResizeObserver` 兜底)。
|
||||
|
||||
---
|
||||
|
||||
## 六、修复建议(未实施,待确认)
|
||||
|
||||
### 6.1 门禁 SQL 改为 V2 结构
|
||||
|
||||
```xml
|
||||
<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 <> a.enrollment_id
|
||||
and a2.accommodation_status <> 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 画像构建逐条容错(建议)
|
||||
|
||||
```java
|
||||
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,陈旧) |
|
||||
Reference in new issue
Block a user