This commit is contained in:
2026-08-14 12:20:52 +08:00
parent ce0812211c
commit afd7c9d704
17 changed files with 1641 additions and 0 deletions
+83
View File
@@ -0,0 +1,83 @@
# 2026-08-14
## 独立 Bug 修复:`OtherUtil.jsonParseList` 泛型擦除导致 ClassCastException
### 现象
- `POST /oapi/user/AddUser`(同步用户接口,非 `api/` 包,未被 UserContext 重构涉及)返回 `code:6 / 内部服务错误`。
- 服务器真实异常:`java.lang.ClassCastException: java.util.LinkedHashMap cannot be cast to com.yunhai.platform.utils.model.BoaUser`
位置 `SyncUserService.syncAddOrUpdate(SyncUserService.java:54)`(增强 for 循环 `for (BoaUser user : userList)`)。
- 连带次级错误(非致命、异步):审计日志 `sys_operation_log.request_params` 列存中文报 `Incorrect string value`(列字符集疑似 latin1/非 utf8mb4),被 `SysOperationLogServiceImpl` 捕获打印「异步落库失败」。
### 根因
- `SyncUserController.AddUser` 用 `OtherUtil.jsonParseList(jsonString, BoaUser.class)` 解析成 `List<BoaUser>`。
- 原 `jsonParseList` 实现:`objectMapper.readValue(json, new TypeReference<List<T>>() {})`。**Java 泛型擦除**使运行时 `List<T>` 退化为原始 `List`,Jackson 把元素反序列化为 `LinkedHashMap`;遍历时强转 `BoaUser` 即 `ClassCastException`。
- 同文件 `jsonParseArrayTree` 用 `objectMapper.getTypeFactory().constructCollectionType(List.class, elementClass)` 才正确——原 `jsonParseList` 注释写「TypeReference 保留泛型信息」是错的。
### 修复
- `utils/OtherUtil.java` 的 `jsonParseList`:改用 `constructCollectionType(List.class, targetClass)` 构建 `JavaType` 再 `readValue`。
- 影响面:顺带修好 `UpdateUser`、`DeleteUser`(同用 `jsonParseList`,之前同样会 ClassCast)。
### 待跟进(非本次阻塞)
- 审计日志表 `sys_operation_log.request_params` 列字符集问题:存中文报 `Incorrect string value`。需改 DDL 为 `utf8mb4`(或确认连接字符集)。属 DB schema,不影响主业务返回,已口头提示用户。
### 环境注意(延续)
- 本机 `mvn compile` 仍受 Lombok 命令行限制;改动在 IDE 验证。
## 深层根因 & 二次修复:`AddUser` 反序列化失败(JSON 与框架 BoaUser 结构不匹配)
### 现象(修复 ClassCast 后仍未解决)
- `jsonParseList` 改 `constructCollectionType` 后,`AddUser` 仍 "反序列化失败"(500)。
- 原因浮出:调用方下发的 JSON 是**自定义格式**,并非 yunhai 框架 `BoaUser`:
- `groups:[{"groupId":12,"no":null,"schoolId":3}]`、`roleList:[{"roleId":5,"schId":3}]`
- 原服务却按 `BoaUser` 调 `user.getSchool().getId()`、`group.getId()`、`group.getSort()`、`user.getRoleIds()`
- 用 `BoaUser.class` 反序列化时,Jackson(默认 `FAIL_ON_UNKNOWN_PROPERTIES=true`)对未知字段 `groups`/`roleList` 抛 `UnrecognizedPropertyException` → 仍 500。
### 修复(按用户指示:加 DTO 补字段)
- 新增 3 个 DTO(`model/tool` 包,与 `DeleteUser` 同风格、`@Data` + `@JsonIgnoreProperties(ignoreUnknown=true)`):
- `SyncUserDTO`:id, account, name, idcard, phone, email, gender(Integer), birthday, status(Integer), profilePicture, `List<SyncGroupDTO> groups`, `List<SyncRoleDTO> roleList`
- `SyncGroupDTO`:groupId, no(Integer), schoolId
- `SyncRoleDTO`:roleId, schId
- `SyncUserController`:`jsonParseList(jsonString, SyncUserDTO.class)`;`ISyncUserService.syncAddOrUpdate(List<SyncUserDTO>)`。
- 重写 `SyncUserService`:去掉 `BoaUser`/`BoaGroup` 依赖,按真实字段映射:
- 角色判定改用 `user.getRoleList().stream().map(SyncRoleDTO::getRoleId)` 比 `UserRole` 值;
- `group.getId()`→`group.getGroupId()`;`user.getSchool().getId()`→`group.getSchoolId()`;`group.getSort()`→`group.getNo()`;`stuClassNo` = `String.valueOf(group.getNo())`。
- `SchStudent.schId` 继承自 `BaseEntity`(存在);实体字段类型与 DTO 对齐(gender/status 为 Integer,idcard/account/name/phone 为 String)。
### 结论
- 该接口此前(含重构前)从未真正可用:先因 `LinkedHashMap` 强转炸,后为 `BoaUser` 字段不匹配炸。现在用匹配 JSON 的 DTO 彻底解耦。
- 注意 JSON 中 `roleId=5`(BUREAU_LEADER_USER),在当前样本数据下既不命中 isStudent 也不命中 isTeacher,故不会落库;这是调用方业务数据问题,非代码缺陷。
## 优化 `SyncUserService`
- 重写 `service/impl/sync/SyncUserService.java`,优化点:
1. 角色判定改为每个用户只遍历一次 `roleList`(`collectRoleIds` 提取去重 Set),`isStudent`/`isTeacher` 改判集合包含,去除重复流式遍历。
2. `deleteUser` 中 `studentDeletes`/`teacherDeletes` 同一过滤条件算了两次 → 合并为单次 `validDeletes`;删除死代码 `groupsBySchool`。
3. 修复教师分组删除 `toMap` 2 段键在「同教师同 groupId 跨多学校」时被静默丢键的 bug → 改用 `Collectors.groupingBy` 收集 `List<SchTeacherGroup>`,删除全部匹配分组。
4. 缓存清除去重:delete 时按 (schId,classId)/(schId) 去重后调用 `clearClassStudentCaches`/`clearTeacherCaches`(新增的批量方法)。
5. 补齐 add/update 后的缓存失效:此前只在 delete 清缓存,add/update 后班级学生缓存/学校教师缓存会陈旧 → 现同步保存后收集受影响键并 `clearClassStudentCaches`/`clearTeacherCaches`。
- 行为保持:事务注解、角色归类(学生优先于教师)、字段映射不变。
## 优化 `SchClassUpgradeService`(学年升级)
- `upgradeSingleSystem` 原本每个年级各调一次 `queryService.getSchClassCacheFilter` 反序列化全校班级列表、再逐年级 `saveAll`;改为**基于 SET 的批量 `@Modifying` 更新**:
- 新增 `SchClassRepository.batchUpgradeGrade(schId, 非最高年级列表, now)`:`grade_id = grade_id + 1` 单语句原子完成(仅基础教育)。
- 新增 `SchClassRepository.batchGraduate(schId, maxGrade, now)`:最高年级 `graduate = true`。
- 用 `?1 ?2 ?3` 位置参数避免受现有 lettuce `@Param` 误用影响;`clearAutomatically = true` 与 `SchStudentRepository` 风格一致。
- `upgradeAllClasses` 优化/修复:
- `schType` 提升到学制循环外只查一次(原在学制内重复查)。
- `getCurrentYearTerm()` 加 null 守卫,避免 NPE。
- 原 `schType.equals(...)` 在 `getSchType` 返回 null 时 NPE → 改为常量 `.equals(schType)` 判等,null 安全。
- 升级日志保存包 try/catch,避免日志写入失败掩盖升级结果。
- 升级后除 `clearClassCache` 外补 `clearGradeateClassCache`,消除毕业后毕业缓存脏数据(原漏清)。
- 行为保持:仅基础教育非最高年级升级、最高年级毕业;失败不阻断其他学制;失败记录可重试(existsBy...Status=true 不命中)。
## 全局修正 Repository 的 `@Param` 错误注解
- 发现 7 个 Repository 的 `@Query` 命名参数绑定误用了 `io.lettuce.core.dynamic.annotation.Param`(Lettuce Redis 注解),而非 Spring Data 的 `org.springframework.data.repository.query.Param`;后者不被 Spring Data 识别,运行时触发「Parameter binding not found」,属潜在崩溃隐患。
- 统一替换为 `org.springframework.data.repository.query.Param`:SchClassRepository、SchClassHistoryRepository、SchDicCourseRepository、SchTeacherRepository、SchDicMajorsRepository、SchStudentRepository、SchTeacherGroupRepository。
- 顺带在 SchClassRepository 补 `import java.util.Date;`(批量升级方法用到)并恢复 `Page`/`Pageable` 导入。
- 现状:本沙箱无 Maven 无法编译验证,需本机 `mvn compile` 确认。
## 修复 `SchClassUpgradeService` 中 `JdbcTemplate` 报红
- 现象:IDE 中标红 `private JdbcTemplate jdbcTemplate;`。
- 根因:`pom.xml` 仅引入 `spring-boot-starter-data-jpa`(其传递依赖 `spring-boot-starter-jdbc`/`spring-jdbc` 含 `JdbcTemplate`),但 IDE 未正确解析传递依赖时 `JdbcTemplate` 类找不到 → 报红。
- 修复:
1. 在 `pom.xml` 显式新增 `spring-boot-starter-jdbc` 依赖(置于 data-jpa 之后),保证 `JdbcTemplate` 类与自动配置 Bean 稳定可用。
2. `SchClassUpgradeService` 由字段注入改为 Lombok `@RequiredArgsConstructor` 构造器注入,消除 IDEA 「Could not autowire. No beans of 'JdbcTemplate' type found」的自动注入识别告警,同时符合 Spring 推荐写法。`backupAllSnapshot` 的批量 INSERT...SELECT 快照用法保留。