fix(sync): 班级人数与辅导员不再被空值覆盖,人数按名册兜底

开放平台对 2025 级起的班级不返回 BJRS,对 2026 级连 FDYH 都不返回,
而 batchUpsert 用 VALUES() 直写这两列,每晚同步都会把人工补录的辅导员和
学生人数刷成 NULL;新生班级的人数因此一直空着,班级管理页首屏(2026 级)
两列全空。

- ClassInfoMapper.xml: counselor_name / student_count 改为
  COALESCE(VALUES(x), x),平台有值仍以平台为准,给空时保留存量
- 新增 refreshStudentCountFromRoster:学生同步结束时按 gxmu_student
  全量在册统计(不过滤 status,与平台 BJRS 口径一致)回填人数,
  只补「为空或 0」的班级,名册里没学生的班级不动
- StudentSyncHandler: 学生段写完后调用兜底

开发库已用同一语句回填 728 个班级。
This commit is contained in:
2026-10-24 20:41:35 +08:00
parent 4c2a787d81
commit 0a2801850d
4 changed files with 65 additions and 2 deletions
@@ -26,4 +26,15 @@ public interface ClassInfoMapper extends BaseMapper<ClassInfo> {
*/ */
int batchUpsert(@Param("list") List<ClassInfo> list); int batchUpsert(@Param("list") List<ClassInfo> list);
/**
* 按学生名册回填班级人数(学生同步末尾调用)。
*
* <p>平台 BJRS 对新生班级返回空,只能本地按 {@code gxmu_student.class_id} 统计兜底。
* 只补为空或 0 的班级,平台有值时不覆盖。
*
* @param tenantId 租户ID
* @return 更新行数
*/
int refreshStudentCountFromRoster(@Param("tenantId") int tenantId);
} }
@@ -7,6 +7,12 @@
ON DUPLICATE KEY UPDATE 里只列「平台权威字段」(docs/adr/0005): ON DUPLICATE KEY UPDATE 里只列「平台权威字段」(docs/adr/0005):
sort_number / counselor_phone 属人工维护,同步不触碰;create_time 不重复刷新。 sort_number / counselor_phone 属人工维护,同步不触碰;create_time 不重复刷新。
幂等键是 uk_gxmu_class_code(tenant_id, class_code)。 幂等键是 uk_gxmu_class_code(tenant_id, class_code)。
counselor_name / student_count 用 COALESCE 保住存量:平台对 2025 级起的班级
不返回 BJRS、对 2026 级连 FDYH 都不返回(实测 2026-09-15 那批 1584 条里
2025 级 148 条全无人数、2026 级 152 条两项全空)。若照旧写 VALUES(),
每晚同步都会把这些列刷成 NULL,人工补录的辅导员和学生人数也就一并清掉了。
平台有值时仍以平台为准,只有平台给空才保留原值。
--> -->
<insert id="batchUpsert"> <insert id="batchUpsert">
INSERT INTO gxmu_class ( INSERT INTO gxmu_class (
@@ -26,12 +32,40 @@
class_name = VALUES(class_name), class_name = VALUES(class_name),
college_id = VALUES(college_id), college_id = VALUES(college_id),
grade_year = VALUES(grade_year), grade_year = VALUES(grade_year),
counselor_name = VALUES(counselor_name), counselor_name = COALESCE(VALUES(counselor_name), counselor_name),
student_count = VALUES(student_count), student_count = COALESCE(VALUES(student_count), student_count),
status = VALUES(status), status = VALUES(status),
remark = VALUES(remark), remark = VALUES(remark),
update_time = VALUES(update_time), update_time = VALUES(update_time),
deleted = VALUES(deleted) deleted = VALUES(deleted)
</insert> </insert>
<!--
按学生名册回填班级人数。
平台的 BJRS(班级人数) 是教务口径,实测与「本班在册学生数」基本吻合
(2020-2024 级逐班比对,平均误差 0.5~2.7 人),但新生班级一律返回空。
学生同步写完后用它兜底:只补「为空或 0」的班级,平台有值时不覆盖,
名册里查不到学生(s.n 为空)的班级也不动,避免每次同步都刷 update_time。
-->
<update id="refreshStudentCountFromRoster">
UPDATE gxmu_class c
LEFT JOIN (
SELECT class_id, COUNT(*) AS n
FROM gxmu_student
WHERE tenant_id = #{tenantId}
AND deleted = 0
AND class_id IS NOT NULL
GROUP BY class_id
) s ON s.class_id = c.id
SET c.student_count = s.n,
c.update_time = NOW()
WHERE c.tenant_id = #{tenantId}
AND c.deleted = 0
AND c.class_code IS NOT NULL
AND (c.student_count IS NULL OR c.student_count = 0)
AND s.n IS NOT NULL
AND s.n &gt; 0
</update>
</mapper> </mapper>
@@ -165,6 +165,22 @@ public class SyncSupport {
} }
} }
/**
* 按学生名册回填班级人数。
*
* <p>平台的 BJRS 对 2025 级起的班级一律返回空(实测 2025 级 148 个班全空),
* 只能拿刚写完的名册按 class_id 统计兜底,否则班级管理页的学生人数永远为空。
*
* @return 更新行数
*/
public int refreshClassStudentCounts() {
int updated = classInfoMapper.refreshStudentCountFromRoster(OpenplatSyncConstants.TENANT_ID);
if (updated > 0) {
logger.warn("班级人数回填: 平台未给出人数的 {} 个班级已按学生名册补上", updated);
}
return updated;
}
/** /**
* 分批写入部门 * 分批写入部门
*/ */
@@ -111,6 +111,8 @@ public class StudentSyncHandler {
int deactivated = support.deactivateMissingStudents(existingHash.keySet(), incoming); int deactivated = support.deactivateMissingStudents(existingHash.keySet(), incoming);
result.setSkipCount(deactivated); result.setSkipCount(deactivated);
// 班级同步(本批更早执行)拿到的 BJRS 对新生班级一律为空,这里按刚写完的名册补上
support.refreshClassStudentCounts();
logger.warn("学生同步完成: 平台 {} 人, 实际写入 {} 行, 未变更跳过 {} 行, 停用 {} 行, 失败 {} 条", logger.warn("学生同步完成: 平台 {} 人, 实际写入 {} 行, 未变更跳过 {} 行, 停用 {} 行, 失败 {} 条",
merged.size(), changed.size(), merged.size() - changed.size(), merged.size(), changed.size(), merged.size() - changed.size(),
deactivated, result.getFailCount()); deactivated, result.getFailCount());