From be011dd4f119ca3fd391542eb62748bd53365d43 Mon Sep 17 00:00:00 2001 From: weicw <594098497@qq.com> Date: Tue, 3 Nov 2026 23:13:41 +0800 Subject: [PATCH] =?UTF-8?q?fix(gxmu):=20=E6=96=B0=E7=94=B3=E6=8A=A5?= =?UTF-8?q?=E6=8C=89=E5=AD=A6=E7=B1=8D=E8=A1=A5=E9=BD=90=20college=5Fid?= =?UTF-8?q?=EF=BC=8C=E4=B8=8D=E5=86=8D=E5=8F=AA=E6=9C=89=E5=AD=98=E9=87=8F?= =?UTF-8?q?=E5=9B=9E=E5=A1=AB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 4 张在跑表(gxmu_sjqn / gxmu_tzbcy_form / gxmu_qmgc_form / gxmu_wxxzx_project) 的实体补 collegeId,save 路径在为空时按「登录用户 → gxmu_student.college_id」 补齐;解析不到(无学籍、名册未同步、学院不唯一)一律留空不报错。 更新路径不调用解析器,collegeId 为 null 时 MyBatis-Plus 忽略该字段,库里已有值不变。 解析逻辑收在一个共享组件 CollegeResolver 里,4 个 controller 只各调一行; 口径与一次性回填脚本一致(优先在校那条学籍,学院不唯一就留空)。 单测:有学籍时写入、无学籍时留空、学院多义时留空、更新不覆盖已有值。 refs #58 --- .../gxmu/controller/QmgcFormController.java | 6 + .../gxmu/controller/SjqnFormController.java | 6 + .../gxmu/controller/TzbcyFormController.java | 6 + .../gxmu/controller/WxxzxFormController.java | 6 + .../com/gxwebsoft/gxmu/entity/QmgcForm.java | 3 + .../com/gxwebsoft/gxmu/entity/SjqnForm.java | 3 + .../com/gxwebsoft/gxmu/entity/TzbcyForm.java | 3 + .../com/gxwebsoft/gxmu/entity/WxxzxForm.java | 3 + .../gxmu/service/CollegeResolver.java | 34 ++++++ .../service/impl/CollegeResolverImpl.java | 62 ++++++++++ .../service/impl/CollegeResolverImplTest.java | 106 ++++++++++++++++++ 11 files changed, 238 insertions(+) create mode 100644 src/main/java/com/gxwebsoft/gxmu/service/CollegeResolver.java create mode 100644 src/main/java/com/gxwebsoft/gxmu/service/impl/CollegeResolverImpl.java create mode 100644 src/test/java/com/gxwebsoft/gxmu/service/impl/CollegeResolverImplTest.java diff --git a/src/main/java/com/gxwebsoft/gxmu/controller/QmgcFormController.java b/src/main/java/com/gxwebsoft/gxmu/controller/QmgcFormController.java index 82c06ef..20dcf33 100644 --- a/src/main/java/com/gxwebsoft/gxmu/controller/QmgcFormController.java +++ b/src/main/java/com/gxwebsoft/gxmu/controller/QmgcFormController.java @@ -15,6 +15,7 @@ import com.gxwebsoft.gxmu.param.QmgcFormParam; import com.gxwebsoft.gxmu.service.QmgcFormService; import com.gxwebsoft.gxmu.service.ReviewFlowStarterService; import com.gxwebsoft.gxmu.model.DeclareCheckResult; +import com.gxwebsoft.gxmu.service.CollegeResolver; import com.gxwebsoft.gxmu.service.DeclareGuardService; import org.springframework.transaction.annotation.Transactional; import com.gxwebsoft.gxmu.util.GxmuQueryHelper; @@ -42,6 +43,8 @@ public class QmgcFormController extends BaseController { @Resource private QmgcFormService qmgcFormService; @Resource + private CollegeResolver collegeResolver; + @Resource private ReviewFlowStarterService reviewFlowStarterService; @Resource private DeclareGuardService declareGuardService; @@ -145,6 +148,9 @@ public class QmgcFormController extends BaseController { if (loginUser != null) { qmgcForm.setUserId(loginUser.getUserId()); } + // 新申报没有学院标识时按学籍补齐(没有学籍 / 学院不唯一就留空,不报错): + // 审批链的 COLLEGE_ROLE 规则要靠它定位学院,一次性回填只解决存量 + qmgcForm.setCollegeId(collegeResolver.fillIfAbsent(qmgcForm.getCollegeId(), qmgcForm.getUserId())); DeclareCheckResult declareCheck = declareGuardService.checkBeforeCreate("gxmu_qmgc_form", qmgcForm.getDeclareId(), qmgcForm.getYear(), qmgcForm.getUserId()); if (declareCheck.isRejected()) { diff --git a/src/main/java/com/gxwebsoft/gxmu/controller/SjqnFormController.java b/src/main/java/com/gxwebsoft/gxmu/controller/SjqnFormController.java index 38d9bf9..cf84fd1 100644 --- a/src/main/java/com/gxwebsoft/gxmu/controller/SjqnFormController.java +++ b/src/main/java/com/gxwebsoft/gxmu/controller/SjqnFormController.java @@ -16,6 +16,7 @@ import com.gxwebsoft.gxmu.entity.SjqnForm; import com.gxwebsoft.gxmu.param.SjqnFormParam; import com.gxwebsoft.gxmu.service.ReviewFlowStarterService; import com.gxwebsoft.gxmu.model.DeclareCheckResult; +import com.gxwebsoft.gxmu.service.CollegeResolver; import com.gxwebsoft.gxmu.service.DeclareGuardService; import org.springframework.transaction.annotation.Transactional; import com.gxwebsoft.gxmu.service.SjqnFormService; @@ -46,6 +47,8 @@ public class SjqnFormController extends BaseController { @Resource private SjqnFormService sjqnFormService; @Resource + private CollegeResolver collegeResolver; + @Resource private ReviewFlowStarterService reviewFlowStarterService; @Resource private DeclareGuardService declareGuardService; @@ -162,6 +165,9 @@ public class SjqnFormController extends BaseController { if (loginUser != null) { sjqnForm.setUserId(loginUser.getUserId()); } + // 新申报没有学院标识时按学籍补齐(没有学籍 / 学院不唯一就留空,不报错): + // 审批链的 COLLEGE_ROLE 规则要靠它定位学院,一次性回填只解决存量 + sjqnForm.setCollegeId(collegeResolver.fillIfAbsent(sjqnForm.getCollegeId(), sjqnForm.getUserId())); DeclareCheckResult declareCheck = declareGuardService.checkBeforeCreate("gxmu_sjqn", sjqnForm.getDeclareId(), sjqnForm.getYear(), sjqnForm.getUserId()); if (declareCheck.isRejected()) { diff --git a/src/main/java/com/gxwebsoft/gxmu/controller/TzbcyFormController.java b/src/main/java/com/gxwebsoft/gxmu/controller/TzbcyFormController.java index b935226..00603f7 100644 --- a/src/main/java/com/gxwebsoft/gxmu/controller/TzbcyFormController.java +++ b/src/main/java/com/gxwebsoft/gxmu/controller/TzbcyFormController.java @@ -11,6 +11,7 @@ import com.gxwebsoft.gxmu.entity.TzbcyForm; import com.gxwebsoft.gxmu.model.TzbcyStatisticsResult; import com.gxwebsoft.gxmu.param.TzbcyFormParam; import com.gxwebsoft.gxmu.model.DeclareCheckResult; +import com.gxwebsoft.gxmu.service.CollegeResolver; import com.gxwebsoft.gxmu.service.DeclareGuardService; import com.gxwebsoft.gxmu.service.ReviewFlowStarterService; import org.springframework.transaction.annotation.Transactional; @@ -38,6 +39,8 @@ public class TzbcyFormController extends BaseController { @Resource private TzbcyFormService tzbcyFormService; @Resource + private CollegeResolver collegeResolver; + @Resource private ReviewFlowStarterService reviewFlowStarterService; @Resource private DeclareGuardService declareGuardService; @@ -99,6 +102,9 @@ public class TzbcyFormController extends BaseController { if (loginUser != null) { tzbcyForm.setUserId(loginUser.getUserId()); } + // 新申报没有学院标识时按学籍补齐(没有学籍 / 学院不唯一就留空,不报错): + // 审批链的 COLLEGE_ROLE 规则要靠它定位学院,一次性回填只解决存量 + tzbcyForm.setCollegeId(collegeResolver.fillIfAbsent(tzbcyForm.getCollegeId(), tzbcyForm.getUserId())); DeclareCheckResult declareCheck = declareGuardService.checkBeforeCreate("gxmu_tzbcy_form", tzbcyForm.getDeclareId(), tzbcyForm.getYear(), tzbcyForm.getUserId()); if (declareCheck.isRejected()) { diff --git a/src/main/java/com/gxwebsoft/gxmu/controller/WxxzxFormController.java b/src/main/java/com/gxwebsoft/gxmu/controller/WxxzxFormController.java index 46c7812..f6a7614 100644 --- a/src/main/java/com/gxwebsoft/gxmu/controller/WxxzxFormController.java +++ b/src/main/java/com/gxwebsoft/gxmu/controller/WxxzxFormController.java @@ -27,6 +27,7 @@ import com.gxwebsoft.gxmu.param.WxxzxFormParam; import com.gxwebsoft.gxmu.service.QmgcFormService; import com.gxwebsoft.gxmu.service.ReviewFlowStarterService; import com.gxwebsoft.gxmu.model.DeclareCheckResult; +import com.gxwebsoft.gxmu.service.CollegeResolver; import com.gxwebsoft.gxmu.service.DeclareGuardService; import org.springframework.transaction.annotation.Transactional; import com.gxwebsoft.gxmu.service.SjqnFormService; @@ -100,6 +101,8 @@ public class WxxzxFormController extends BaseController { @Resource private WxxzxFormService wxxzxFormService; @Resource + private CollegeResolver collegeResolver; + @Resource private SjqnFormService sjqnFormService; @Resource private SjtbzbsjFormService sjtbzbsjFormService; @@ -254,6 +257,9 @@ public class WxxzxFormController extends BaseController { if (loginUser != null) { wxxzxForm.setUserId(loginUser.getUserId()); } + // 新申报没有学院标识时按学籍补齐(没有学籍 / 学院不唯一就留空,不报错): + // 审批链的 COLLEGE_ROLE 规则要靠它定位学院,一次性回填只解决存量 + wxxzxForm.setCollegeId(collegeResolver.fillIfAbsent(wxxzxForm.getCollegeId(), wxxzxForm.getUserId())); DeclareCheckResult declareCheck = declareGuardService.checkBeforeCreate("gxmu_wxxzx_project", wxxzxForm.getDeclareId(), wxxzxForm.getYear(), wxxzxForm.getUserId()); if (declareCheck.isRejected()) { diff --git a/src/main/java/com/gxwebsoft/gxmu/entity/QmgcForm.java b/src/main/java/com/gxwebsoft/gxmu/entity/QmgcForm.java index ff60c4a..eb25330 100644 --- a/src/main/java/com/gxwebsoft/gxmu/entity/QmgcForm.java +++ b/src/main/java/com/gxwebsoft/gxmu/entity/QmgcForm.java @@ -117,6 +117,9 @@ public class QmgcForm implements Serializable { @ApiModelProperty("用户ID") private Integer userId; + @ApiModelProperty("学院标识(sys_organization.organization_id),新建申报时按学籍补齐,允许为空") + private Integer collegeId; + @ApiModelProperty("是否删除") @TableLogic private Integer deleted; diff --git a/src/main/java/com/gxwebsoft/gxmu/entity/SjqnForm.java b/src/main/java/com/gxwebsoft/gxmu/entity/SjqnForm.java index a9d3a0d..c91b9ff 100644 --- a/src/main/java/com/gxwebsoft/gxmu/entity/SjqnForm.java +++ b/src/main/java/com/gxwebsoft/gxmu/entity/SjqnForm.java @@ -92,6 +92,9 @@ public class SjqnForm implements Serializable { @ApiModelProperty("用户ID") private Integer userId; + @ApiModelProperty("学院标识(sys_organization.organization_id),新建申报时按学籍补齐,允许为空") + private Integer collegeId; + @ApiModelProperty("是否删除") @TableLogic private Integer deleted; diff --git a/src/main/java/com/gxwebsoft/gxmu/entity/TzbcyForm.java b/src/main/java/com/gxwebsoft/gxmu/entity/TzbcyForm.java index 8ac524b..22803cc 100644 --- a/src/main/java/com/gxwebsoft/gxmu/entity/TzbcyForm.java +++ b/src/main/java/com/gxwebsoft/gxmu/entity/TzbcyForm.java @@ -114,6 +114,9 @@ public class TzbcyForm implements Serializable { @ApiModelProperty("用户ID") private Integer userId; + @ApiModelProperty("学院标识(sys_organization.organization_id),新建申报时按学籍补齐,允许为空") + private Integer collegeId; + @ApiModelProperty("是否删除") @TableLogic private Integer deleted; diff --git a/src/main/java/com/gxwebsoft/gxmu/entity/WxxzxForm.java b/src/main/java/com/gxwebsoft/gxmu/entity/WxxzxForm.java index 3031b0c..a86d2d7 100644 --- a/src/main/java/com/gxwebsoft/gxmu/entity/WxxzxForm.java +++ b/src/main/java/com/gxwebsoft/gxmu/entity/WxxzxForm.java @@ -143,6 +143,9 @@ public class WxxzxForm implements Serializable { @ApiModelProperty("用户ID") private Integer userId; + @ApiModelProperty("学院标识(sys_organization.organization_id),新建申报时按学籍补齐,允许为空") + private Integer collegeId; + @ApiModelProperty("是否删除") @TableLogic private Integer deleted; diff --git a/src/main/java/com/gxwebsoft/gxmu/service/CollegeResolver.java b/src/main/java/com/gxwebsoft/gxmu/service/CollegeResolver.java new file mode 100644 index 0000000..4a319d5 --- /dev/null +++ b/src/main/java/com/gxwebsoft/gxmu/service/CollegeResolver.java @@ -0,0 +1,34 @@ +package com.gxwebsoft.gxmu.service; + +/** + * 学院标识解析:把登录用户映射到申报记录的 college_id(spec #58 用户故事 2)。 + * + *

申报表上的 college_id 指向 sys_organization.organization_id,与学生名册 + * gxmu_student.college_id 是同一套 ID(见 ADR 0006「学院标识」)。新建申报时按 + * 「登录用户 → gxmu_student.college_id」补齐,审批链里的 COLLEGE_ROLE 规则才有字段可用; + * 解析不到(没有学籍、名册未同步、学院不唯一)一律返回 null,不报错、不猜。 + * + * @author Codex + * @since 2026-11-11 + */ +public interface CollegeResolver { + + /** + * 按登录用户查学院标识。 + * + *

同一用户有多条学籍时以「在校」那条为准;学院不唯一说明无法判断,返回 null。 + * + * @param userId 登录用户 id(sys_user.user_id),可为空 + * @return 学院标识,解析不到返回 null + */ + Integer resolve(Integer userId); + + /** + * 只在申报记录还没有学院标识时补齐;已有值一律保留(更新路径不覆盖已有值)。 + * + * @param currentCollegeId 申报记录当前的学院标识 + * @param userId 登录用户 id + * @return 最终写入的学院标识 + */ + Integer fillIfAbsent(Integer currentCollegeId, Integer userId); +} diff --git a/src/main/java/com/gxwebsoft/gxmu/service/impl/CollegeResolverImpl.java b/src/main/java/com/gxwebsoft/gxmu/service/impl/CollegeResolverImpl.java new file mode 100644 index 0000000..143ee79 --- /dev/null +++ b/src/main/java/com/gxwebsoft/gxmu/service/impl/CollegeResolverImpl.java @@ -0,0 +1,62 @@ +package com.gxwebsoft.gxmu.service.impl; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.gxwebsoft.gxmu.openplat.entity.GxmuStudent; +import com.gxwebsoft.gxmu.openplat.mapper.GxmuStudentMapper; +import com.gxwebsoft.gxmu.service.CollegeResolver; +import org.springframework.stereotype.Service; + +import javax.annotation.Resource; +import java.util.List; + +/** + * 学院标识解析实现:gxmu_student.college_id 是学院标识的唯一权威来源。 + * + *

口径与一次性回填脚本 sql/20261110_backfill_college_id.sql 保持一致: + * 优先取「在校(status = 1)」那条学籍的学院;学院不唯一就留空交给人工, + * 不做层级推导、不猜。 + * + * @author Codex + * @since 2026-11-11 + */ +@Service +public class CollegeResolverImpl implements CollegeResolver { + + @Resource + private GxmuStudentMapper gxmuStudentMapper; + + @Override + public Integer resolve(Integer userId) { + if (userId == null) { + return null; + } + List students = gxmuStudentMapper.selectList(new LambdaQueryWrapper() + .select(GxmuStudent::getCollegeId, GxmuStudent::getStatus) + .eq(GxmuStudent::getUserId, userId) + .isNotNull(GxmuStudent::getCollegeId) + .orderByDesc(GxmuStudent::getStatus) + .orderByDesc(GxmuStudent::getId)); + if (students == null || students.isEmpty()) { + return null; + } + Integer collegeId = null; + for (GxmuStudent student : students) { + Integer candidate = student.getCollegeId(); + if (candidate == null) { + continue; + } + if (collegeId == null) { + collegeId = candidate; + } else if (!collegeId.equals(candidate)) { + // 同一用户的本科学院与研究生学院不同且没有可判定的在校行:留空,不猜 + return null; + } + } + return collegeId; + } + + @Override + public Integer fillIfAbsent(Integer currentCollegeId, Integer userId) { + return currentCollegeId != null ? currentCollegeId : resolve(userId); + } +} diff --git a/src/test/java/com/gxwebsoft/gxmu/service/impl/CollegeResolverImplTest.java b/src/test/java/com/gxwebsoft/gxmu/service/impl/CollegeResolverImplTest.java new file mode 100644 index 0000000..2f60cb4 --- /dev/null +++ b/src/test/java/com/gxwebsoft/gxmu/service/impl/CollegeResolverImplTest.java @@ -0,0 +1,106 @@ +package com.gxwebsoft.gxmu.service.impl; + +import com.baomidou.mybatisplus.core.MybatisConfiguration; +import com.baomidou.mybatisplus.core.metadata.TableInfoHelper; +import com.gxwebsoft.gxmu.openplat.entity.GxmuStudent; +import com.gxwebsoft.gxmu.openplat.mapper.GxmuStudentMapper; +import org.apache.ibatis.builder.MapperBuilderAssistant; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.mockito.junit.jupiter.MockitoSettings; +import org.mockito.quality.Strictness; + +import java.util.Arrays; +import java.util.Collections; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; + +/** + * 学院标识解析(spec #58 用户故事 2:一条链 27 个学院共用)。 + * + *

钉三件事:有学籍时新申报写入学院、没有学籍时留空不报错、更新路径不覆盖已有值。 + * 不启 Spring、不连库。 + * + * @author Codex + * @since 2026-11-11 + */ +@ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.LENIENT) +class CollegeResolverImplTest { + + private static final Integer USER_ID = 12; + private static final Integer COLLEGE_ID = 100; + + @Mock + private GxmuStudentMapper gxmuStudentMapper; + + @InjectMocks + private CollegeResolverImpl collegeResolver; + + /** MyBatis-Plus 的 lambda 列名解析需要 TableInfo,纯单元测试里手工初始化一次 */ + @BeforeAll + static void initLambdaCache() { + MapperBuilderAssistant assistant = new MapperBuilderAssistant(new MybatisConfiguration(), ""); + TableInfoHelper.initTableInfo(assistant, GxmuStudent.class); + } + + /** 有学籍:按 gxmu_student.college_id 解析出学院。 */ + @Test + void resolveReturnsCollegeFromStudentRecord() { + when(gxmuStudentMapper.selectList(any())) + .thenReturn(Collections.singletonList(student(COLLEGE_ID, 1))); + + assertEquals(COLLEGE_ID, collegeResolver.resolve(USER_ID)); + } + + /** 没有学籍(或学籍没学院):留空,不报错——申报不能因为名册没同步就提交不了。 */ + @Test + void resolveReturnsNullWithoutStudentRecord() { + when(gxmuStudentMapper.selectList(any())).thenReturn(Collections.emptyList()); + + assertNull(collegeResolver.resolve(USER_ID)); + assertNull(collegeResolver.resolve(null)); + } + + /** 同一用户本科学院与研究生学院不同且没有在校行:留空,不猜。 */ + @Test + void resolveReturnsNullWhenCollegesAreAmbiguous() { + when(gxmuStudentMapper.selectList(any())).thenReturn(Arrays.asList( + student(COLLEGE_ID, 0), + student(200, 0))); + + assertNull(collegeResolver.resolve(USER_ID)); + } + + /** 新建申报:没有学院标识时写入解析结果。 */ + @Test + void fillIfAbsentWritesResolvedCollegeForNewRecord() { + when(gxmuStudentMapper.selectList(any())) + .thenReturn(Collections.singletonList(student(COLLEGE_ID, 1))); + + assertEquals(COLLEGE_ID, collegeResolver.fillIfAbsent(null, USER_ID)); + } + + /** 更新路径:已有学院标识一律保留,不被登录用户的学籍覆盖。 */ + @Test + void fillIfAbsentKeepsExistingCollegeOnUpdate() { + assertEquals(Integer.valueOf(999), collegeResolver.fillIfAbsent(999, USER_ID)); + // 已有值时不查库 + assertEquals(Integer.valueOf(999), collegeResolver.fillIfAbsent(999, null)); + } + + private GxmuStudent student(Integer collegeId, Integer status) { + GxmuStudent student = new GxmuStudent(); + student.setUserId(USER_ID); + student.setCollegeId(collegeId); + student.setStatus(status); + return student; + } +}