fix(gxmu): 新申报按学籍补齐 college_id,不再只有存量回填

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
This commit is contained in:
2026-11-03 23:13:41 +08:00
parent 37cc8ee0d5
commit be011dd4f1
11 changed files with 238 additions and 0 deletions
@@ -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()) {
@@ -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()) {
@@ -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()) {
@@ -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()) {
@@ -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;
@@ -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;
@@ -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;
@@ -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;
@@ -0,0 +1,34 @@
package com.gxwebsoft.gxmu.service;
/**
* 学院标识解析:把登录用户映射到申报记录的 college_id(spec #58 用户故事 2)。
*
* <p>申报表上的 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 {
/**
* 按登录用户查学院标识。
*
* <p>同一用户有多条学籍时以「在校」那条为准;学院不唯一说明无法判断,返回 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);
}
@@ -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 是学院标识的唯一权威来源。
*
* <p>口径与一次性回填脚本 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<GxmuStudent> students = gxmuStudentMapper.selectList(new LambdaQueryWrapper<GxmuStudent>()
.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);
}
}
@@ -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 个学院共用)。
*
* <p>钉三件事:有学籍时新申报写入学院、没有学籍时留空不报错、更新路径不覆盖已有值。
* 不启 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;
}
}