diff --git a/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/controller/XgStudentStatusChangeController.java b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/controller/XgStudentStatusChangeController.java index ed3be00f..8aa204e2 100644 --- a/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/controller/XgStudentStatusChangeController.java +++ b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/controller/XgStudentStatusChangeController.java @@ -5,7 +5,6 @@ import java.util.List; import lombok.RequiredArgsConstructor; import jakarta.servlet.http.HttpServletResponse; import jakarta.validation.constraints.*; -import cn.dev33.satoken.annotation.SaCheckPermission; import org.springframework.web.bind.annotation.*; import org.springframework.validation.annotation.Validated; import org.dromara.common.idempotent.annotation.RepeatSubmit; @@ -18,6 +17,7 @@ import org.dromara.common.core.validate.EditGroup; import org.dromara.common.log.enums.BusinessType; import org.dromara.common.excel.utils.ExcelUtil; import org.dromara.xg.domain.vo.XgStudentStatusChangeVo; +import org.dromara.xg.domain.bo.XgStudentResumeWithClassChangeBo; import org.dromara.xg.domain.bo.XgStudentStatusChangeBo; import org.dromara.xg.service.IXgStudentStatusChangeService; import org.dromara.common.mybatis.core.page.TableDataInfo; @@ -93,6 +93,16 @@ public class XgStudentStatusChangeController extends BaseController { return toAjax(result); } + /** + * 复学并换班级(一次提交,事务化写入两条记录) + */ + @Log(title = "学生复学并换班级", businessType = BusinessType.INSERT) + @RepeatSubmit() + @PostMapping("/resume-with-class-change") + public R resumeWithClassChange(@Validated(AddGroup.class) @RequestBody XgStudentResumeWithClassChangeBo bo) { + return toAjax(xgStudentStatusChangeService.insertResumeWithClassChange(bo)); + } + /** * 修改学生学籍异动 */ diff --git a/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/domain/bo/XgStudentResumeWithClassChangeBo.java b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/domain/bo/XgStudentResumeWithClassChangeBo.java new file mode 100644 index 00000000..225e6fff --- /dev/null +++ b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/domain/bo/XgStudentResumeWithClassChangeBo.java @@ -0,0 +1,123 @@ +package org.dromara.xg.domain.bo; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.dromara.common.core.validate.AddGroup; +import org.dromara.common.mybatis.core.domain.BaseEntity; + +import java.util.Date; + +/** + * 复学并换班级组合业务对象 + * + * @author AI Assistant + * @date 2026-03-13 + */ +@Data +@EqualsAndHashCode(callSuper = true) +public class XgStudentResumeWithClassChangeBo extends BaseEntity { + + /** + * 学生ID + */ + @NotNull(message = "学生ID不能为空", groups = { AddGroup.class }) + private Long studentId; + + /** + * 学号 + */ + @NotBlank(message = "学号不能为空", groups = { AddGroup.class }) + private String studentNo; + + /** + * 学生姓名 + */ + private String studentName; + + /** + * 原学院ID + */ + private Long originalCollegeId; + + /** + * 原学院名称 + */ + private String originalCollegeName; + + /** + * 原专业ID + */ + private Long originalMajorId; + + /** + * 原专业名称 + */ + private String originalMajorName; + + /** + * 原班级ID + */ + private Long originalClassId; + + /** + * 原班级名称 + */ + private String originalClassName; + + /** + * 新学院ID + */ + @NotNull(message = "新学院不能为空", groups = { AddGroup.class }) + private Long newCollegeId; + + /** + * 新学院名称 + */ + private String newCollegeName; + + /** + * 新专业ID + */ + @NotNull(message = "新专业不能为空", groups = { AddGroup.class }) + private Long newMajorId; + + /** + * 新专业名称 + */ + private String newMajorName; + + /** + * 新班级ID + */ + @NotNull(message = "新班级不能为空", groups = { AddGroup.class }) + private Long newClassId; + + /** + * 新班级名称 + */ + private String newClassName; + + /** + * 生效日期(复学与换班级共用) + */ + @NotNull(message = "生效时间不能为空", groups = { AddGroup.class }) + private Date effectiveDate; + + /** + * 原因(复学原因,同时作为换班级原因) + */ + @NotBlank(message = "原因不能为空", groups = { AddGroup.class }) + private String reason; + + /** + * 经办人ID + */ + private Long operatorId; + + /** + * 经办人姓名 + */ + private String operatorName; +} diff --git a/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/service/IXgStudentStatusChangeService.java b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/service/IXgStudentStatusChangeService.java index 2c49df2f..53d16489 100644 --- a/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/service/IXgStudentStatusChangeService.java +++ b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/service/IXgStudentStatusChangeService.java @@ -1,6 +1,7 @@ package org.dromara.xg.service; import org.dromara.xg.domain.vo.XgStudentStatusChangeVo; +import org.dromara.xg.domain.bo.XgStudentResumeWithClassChangeBo; import org.dromara.xg.domain.bo.XgStudentStatusChangeBo; import org.dromara.common.mybatis.core.page.TableDataInfo; import org.dromara.common.mybatis.core.page.PageQuery; @@ -49,6 +50,14 @@ public interface IXgStudentStatusChangeService { */ Boolean insertByBo(XgStudentStatusChangeBo bo); + /** + * 复学并换班级(事务化组合提交) + * + * @param bo 组合业务参数 + * @return 是否新增成功 + */ + Boolean insertResumeWithClassChange(XgStudentResumeWithClassChangeBo bo); + /** * 修改学生学籍异动 * diff --git a/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/service/impl/XgStudentStatusChangeServiceImpl.java b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/service/impl/XgStudentStatusChangeServiceImpl.java index 94554cf7..22d716c7 100644 --- a/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/service/impl/XgStudentStatusChangeServiceImpl.java +++ b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/service/impl/XgStudentStatusChangeServiceImpl.java @@ -9,16 +9,19 @@ import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.toolkit.Wrappers; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.dromara.common.core.exception.ServiceException; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import org.dromara.xg.domain.bo.XgStudentClassChangeBo; +import org.dromara.xg.domain.bo.XgStudentResumeWithClassChangeBo; import org.dromara.xg.domain.bo.XgStudentStatusChangeBo; import org.dromara.xg.domain.vo.XgStudentStatusChangeVo; import org.dromara.xg.domain.XgStudentStatusChange; import org.dromara.xg.mapper.XgStudentStatusChangeMapper; +import org.dromara.xg.service.IXgStudentClassChangeService; import org.dromara.xg.service.IXgStudentStatusChangeService; import java.util.List; -import java.util.Map; import java.util.Collection; /** @@ -33,6 +36,7 @@ import java.util.Collection; public class XgStudentStatusChangeServiceImpl implements IXgStudentStatusChangeService { private final XgStudentStatusChangeMapper baseMapper; + private final IXgStudentClassChangeService xgStudentClassChangeService; /** * 查询学生学籍异动 @@ -73,7 +77,6 @@ public class XgStudentStatusChangeServiceImpl implements IXgStudentStatusChangeS } private LambdaQueryWrapper buildQueryWrapper(XgStudentStatusChangeBo bo) { - Map params = bo.getParams(); LambdaQueryWrapper lqw = Wrappers.lambdaQuery(); lqw.eq(bo.getId() != null, XgStudentStatusChange::getId, bo.getId()); lqw.eq(bo.getStudentId() != null, XgStudentStatusChange::getStudentId, bo.getStudentId()); @@ -106,6 +109,58 @@ public class XgStudentStatusChangeServiceImpl implements IXgStudentStatusChangeS return flag; } + /** + * 复学并换班级(事务化组合提交) + */ + @Override + @Transactional(rollbackFor = Exception.class) + public Boolean insertResumeWithClassChange(XgStudentResumeWithClassChangeBo bo) { + XgStudentStatusChangeBo statusChangeBo = new XgStudentStatusChangeBo(); + statusChangeBo.setStudentId(bo.getStudentId()); + statusChangeBo.setStudentNo(bo.getStudentNo()); + statusChangeBo.setChangeType("FU_XUE"); + statusChangeBo.setApplyDate(new java.util.Date()); + statusChangeBo.setEffectiveDate(bo.getEffectiveDate()); + statusChangeBo.setReason(bo.getReason()); + statusChangeBo.setOperatorId(bo.getOperatorId()); + statusChangeBo.setOperatorName(bo.getOperatorName()); + statusChangeBo.setSource("manual"); + statusChangeBo.setProcessStatus(0); + + boolean statusInserted = insertByBo(statusChangeBo); + if (!statusInserted) { + throw new ServiceException("复学记录保存失败"); + } + + XgStudentClassChangeBo classChangeBo = new XgStudentClassChangeBo(); + classChangeBo.setStudentId(bo.getStudentId()); + classChangeBo.setStudentNo(bo.getStudentNo()); + classChangeBo.setStudentName(bo.getStudentName()); + classChangeBo.setOriginalCollegeId(bo.getOriginalCollegeId()); + classChangeBo.setOriginalCollegeName(bo.getOriginalCollegeName()); + classChangeBo.setOriginalMajorId(bo.getOriginalMajorId()); + classChangeBo.setOriginalMajorName(bo.getOriginalMajorName()); + classChangeBo.setOriginalClassId(bo.getOriginalClassId()); + classChangeBo.setOriginalClassName(bo.getOriginalClassName()); + classChangeBo.setNewCollegeId(bo.getNewCollegeId()); + classChangeBo.setNewCollegeName(bo.getNewCollegeName()); + classChangeBo.setNewMajorId(bo.getNewMajorId()); + classChangeBo.setNewMajorName(bo.getNewMajorName()); + classChangeBo.setNewClassId(bo.getNewClassId()); + classChangeBo.setNewClassName(bo.getNewClassName()); + classChangeBo.setChangeReason(bo.getReason()); + classChangeBo.setChangeType("2"); + classChangeBo.setChangeTime(new java.util.Date()); + classChangeBo.setEffectiveTime(bo.getEffectiveDate()); + classChangeBo.setStatus("0"); + + boolean classInserted = xgStudentClassChangeService.insertByBo(classChangeBo); + if (!classInserted) { + throw new ServiceException("换班级记录保存失败"); + } + return true; + } + /** * 修改学生学籍异动 * diff --git a/docs/开发文档/API/API文档总览.md b/docs/开发文档/API/API文档总览.md index 09677c11..736e313a 100644 --- a/docs/开发文档/API/API文档总览.md +++ b/docs/开发文档/API/API文档总览.md @@ -100,6 +100,7 @@ | 日期 | 版本 | 更新内容 | 更新人 | |------|------|---------|-------| +| 2026-03-13 | 1.7 | 新增学生学籍异动与换班级API文档,补充复学+换班级事务化组合接口 | Cursor | | 2025-12-26 | 1.6 | 新增专业信息管理API,支持树形菜单+列表的专业管理 | Cursor | | 2025-12-26 | 1.5 | 新增二级学院管理API,支持二级学院的增删改查 | Cursor | | 2025-01-03 | 1.1 | 社团管理API更新:删除副社长信息卡片,role_type字段改为字符串类型 | 系统管理员 | @@ -168,6 +169,7 @@ |---------|------|------|----------| | [学生信息收集批次管理API](./学生信息收集批次管理API.md) | 学生信息收集批次班级权限管理 | ✅ 完成 | 2025-01-27 | | [学生信息批次收集API](./学生信息批次收集API.md) | 学生信息批次收集管理(新增) | ✅ 完成 | 2025-01-27 | +| [学生学籍异动与换班级API](./学生学籍异动与换班级API.md) | 学籍异动与换班级(含复学+换班级事务接口) | ✅ 完成 | 2026-03-13 | | [班级信息管理API](./班级信息管理API.md) | 班级基础信息增删改查 | ✅ 完成 | 2025-01-27 | | [二级学院管理API](./二级学院管理API.md) | 二级学院组织信息管理 | ✅ 完成 | 2025-12-26 | | [专业信息管理API](./专业信息管理API.md) | 专业信息管理(树形菜单+列表) | ✅ 完成 | 2025-12-26 | diff --git a/docs/开发文档/API/学生学籍异动与换班级API.md b/docs/开发文档/API/学生学籍异动与换班级API.md new file mode 100644 index 00000000..949e0ba6 --- /dev/null +++ b/docs/开发文档/API/学生学籍异动与换班级API.md @@ -0,0 +1,84 @@ +# 学生学籍异动与换班级API + +## 模块说明 + +本文档用于说明学生学籍异动(退学、休学、复学、毕业)与换班级相关接口,重点包含“复学并换班级”事务化组合接口。 + +- 基础路径:`/studentMsg/studentStatusChange` +- 关联模块:`/studentMsg/studentClassChange` + +## 1. 批量新增学籍异动 + +- 请求方式:`POST` +- 请求路径:`/studentMsg/studentStatusChange/batch` +- 功能描述:批量提交学籍异动记录(适用于退学、休学、毕业、普通复学) + +## 2. 复学并换班级(事务化组合接口) + +- 请求方式:`POST` +- 请求路径:`/studentMsg/studentStatusChange/resume-with-class-change` +- 功能描述:一次提交同时写入 + - 学籍异动表 1 条(`changeType=FU_XUE`) + - 换班级表 1 条(新学院/新专业/新班级) +- 事务说明:任一写入失败则整体回滚,避免“只复学未换班”或“只换班未复学”的数据不一致 + +### 请求参数 + +| 参数名 | 类型 | 必填 | 说明 | +|---|---|---|---| +| studentId | Long | 是 | 学生ID | +| studentNo | String | 是 | 学号 | +| studentName | String | 否 | 学生姓名 | +| originalCollegeId | Long | 否 | 原学院ID | +| originalCollegeName | String | 否 | 原学院名称 | +| originalMajorId | Long | 否 | 原专业ID | +| originalMajorName | String | 否 | 原专业名称 | +| originalClassId | Long | 否 | 原班级ID | +| originalClassName | String | 否 | 原班级名称 | +| newCollegeId | Long | 是 | 新学院ID | +| newCollegeName | String | 否 | 新学院名称 | +| newMajorId | Long | 是 | 新专业ID | +| newMajorName | String | 否 | 新专业名称 | +| newClassId | Long | 是 | 新班级ID | +| newClassName | String | 否 | 新班级名称 | +| effectiveDate | Date | 是 | 生效日期(复学与换班级共用) | +| reason | String | 是 | 原因(复学原因,同时写入换班级原因) | +| operatorId | Long | 否 | 经办人ID | +| operatorName | String | 否 | 经办人姓名 | + +### 请求示例 + +```json +{ + "studentId": 197001010001, + "studentNo": "20240001", + "studentName": "张三", + "originalCollegeId": 201, + "originalCollegeName": "信息工程学院", + "originalMajorId": 301, + "originalMajorName": "软件技术", + "originalClassId": 401, + "originalClassName": "软技2401", + "newCollegeId": 202, + "newCollegeName": "智能制造学院", + "newMajorId": 302, + "newMajorName": "机电一体化技术", + "newClassId": 402, + "newClassName": "机电2402", + "effectiveDate": "2026-03-13", + "reason": "学生复学并调整到新班级", + "operatorId": 100, + "operatorName": "管理员" +} +``` + +### 响应说明 + +- 成功响应:`R`,`code=200` +- 失败响应:`R`,`code!=200`,并返回错误信息 + +## 更新记录 + +| 日期 | 更新内容 | 更新人 | +|---|---|---| +| 2026-03-13 | 新增“复学并换班级”事务化组合接口文档 | Cursor | diff --git a/web/src/views/login.vue b/web/src/views/login.vue index 9fe330d4..182a35b9 100644 --- a/web/src/views/login.vue +++ b/web/src/views/login.vue @@ -58,11 +58,9 @@ {{ proxy.$t('login.logging') }} -
diff --git a/web/src/views/xg/xsjcxx/studentMsg/studentMsg/index.vue b/web/src/views/xg/xsjcxx/studentMsg/studentMsg/index.vue index 78c1b8ea..4fb86497 100644 --- a/web/src/views/xg/xsjcxx/studentMsg/studentMsg/index.vue +++ b/web/src/views/xg/xsjcxx/studentMsg/studentMsg/index.vue @@ -1676,6 +1676,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + @@ -1864,6 +1895,9 @@ const rawDeptOptions = ref([]); const majorOptions = ref([]); const classOptions = ref([]); const collegeOptions = ref([]); // 学院选项 +const resumeCollegeOptions = ref([]); // 复学弹窗学院选项 +const resumeMajorOptions = ref([]); // 复学弹窗专业选项 +const resumeClassOptions = ref([]); // 复学弹窗班级选项 const regionOptions = ref([]); // 行政区划选项 const dormBuildingOptions = ref([]); // 宿舍楼栋选项 const currentSelectedNode = ref(null); // 当前选中的部门节点 @@ -1945,6 +1979,9 @@ const statusChangeForm = reactive({ reason: '', operatorId: '', operatorName: '', + newCollegeId: '', + newMajorId: '', + newClassId: '', selectedStudents: [] as StudentVO[] }); @@ -2103,7 +2140,10 @@ const rules = reactive({ const statusChangeRules = reactive({ changeType: [{ required: true, message: '异动类型不能为空', trigger: 'change' }], effectiveDate: [{ required: true, message: '生效日期不能为空', trigger: 'blur' }], - reason: [{ required: true, message: '异动原因不能为空', trigger: 'blur' }] + reason: [{ required: true, message: '异动原因不能为空', trigger: 'blur' }], + newCollegeId: [{ required: true, message: '请选择新学院', trigger: 'change' }], + newMajorId: [{ required: true, message: '请选择新专业', trigger: 'change' }], + newClassId: [{ required: true, message: '请选择新班级', trigger: 'change' }] }); // 换班级表单验证规则 @@ -3090,8 +3130,13 @@ const openStatusChangeModal = (changeType?: string) => { statusChangeForm.reason = ''; statusChangeForm.operatorId = userStore.userId || ''; statusChangeForm.operatorName = userStore.nickname || ''; + resetResumeClassSelection(); statusChangeForm.selectedStudents = selectedStudents; + if (changeType === 'FU_XUE') { + loadResumeCollegeOptions(); + } + statusChangeDialog.visible = true; }; @@ -3112,6 +3157,99 @@ const disabledDate = (time: Date) => { return time.getTime() < Date.now() - 24 * 60 * 60 * 1000; // 不能选择昨天及以前的日期 }; +const isResumeChangeType = computed(() => statusChangeForm.changeType === 'FU_XUE'); + +const resetResumeClassSelection = () => { + statusChangeForm.newCollegeId = ''; + statusChangeForm.newMajorId = ''; + statusChangeForm.newClassId = ''; + resumeMajorOptions.value = []; + resumeClassOptions.value = []; +}; + +const loadResumeCollegeOptions = async () => { + try { + const res = await service.get('/system/dept/list', { + params: { + parentId: 101, // 二级学院节点 + deptCategory: 'xy-' // 学院标识 + } + }); + if (res && res.code === 200) { + resumeCollegeOptions.value = (res.data || []).map((dept: any) => ({ + id: dept.deptId, + label: dept.deptName + })); + } + } catch (error) { + console.error('获取复学学院列表失败:', error); + resumeCollegeOptions.value = []; + } +}; + +const handleStatusChangeCollegeChange = async (collegeId: string) => { + statusChangeForm.newMajorId = ''; + statusChangeForm.newClassId = ''; + resumeMajorOptions.value = []; + resumeClassOptions.value = []; + + if (!collegeId) return; + + try { + const res = await service.get('/system/dept/list', { + params: { + parentId: collegeId, + deptCategory: 'zy-' + } + }); + if (res && res.code === 200) { + resumeMajorOptions.value = (res.data || []).map((dept: any) => ({ + id: dept.deptId, + label: dept.deptName + })); + } + } catch (error) { + console.error('获取复学专业列表失败:', error); + resumeMajorOptions.value = []; + } +}; + +const handleStatusChangeMajorChange = async (majorId: string) => { + statusChangeForm.newClassId = ''; + resumeClassOptions.value = []; + + if (!majorId) return; + + try { + const res = await service.get('/system/dept/list', { + params: { + parentId: majorId, + deptCategory: 'bj-' + } + }); + if (res && res.code === 200) { + resumeClassOptions.value = (res.data || []).map((dept: any) => ({ + id: dept.deptId, + label: dept.deptName + })); + } + } catch (error) { + console.error('获取复学班级列表失败:', error); + resumeClassOptions.value = []; + } +}; + +watch( + () => statusChangeForm.changeType, + (value) => { + if (value === 'FU_XUE') { + loadResumeCollegeOptions(); + return; + } + resetResumeClassSelection(); + } +); + /** 提交学籍异动 */ const submitStatusChange = async () => { const formRef = statusChangeFormRef.value; @@ -3120,6 +3258,55 @@ const submitStatusChange = async () => { await formRef.validate(async (valid: boolean) => { if (valid) { try { + if (isResumeChangeType.value) { + if (statusChangeForm.selectedStudents.length !== 1) { + ElMessage.warning('复学并换班级仅支持单个学生操作'); + return; + } + if (!statusChangeForm.newCollegeId || !statusChangeForm.newMajorId || !statusChangeForm.newClassId) { + ElMessage.warning('请选择新学院、新专业和新班级'); + return; + } + + const selectedStudent = statusChangeForm.selectedStudents[0]; + if (String(statusChangeForm.newClassId) === String(selectedStudent.classId || '')) { + ElMessage.warning('请选择与原班级不同的新班级'); + return; + } + + const resumePayload = { + studentId: selectedStudent.id, + studentNo: selectedStudent.studentNo, + studentName: selectedStudent.name, + originalCollegeId: selectedStudent.collegeId || undefined, + originalCollegeName: (selectedStudent as any).collegeName || (selectedStudent as any).colegeName || '', + originalMajorId: selectedStudent.majorId || undefined, + originalMajorName: selectedStudent.majorName || '', + originalClassId: selectedStudent.classId || undefined, + originalClassName: selectedStudent.className || '', + newCollegeId: statusChangeForm.newCollegeId, + newCollegeName: resumeCollegeOptions.value.find((item: any) => String(item.id) === String(statusChangeForm.newCollegeId))?.label || '', + newMajorId: statusChangeForm.newMajorId, + newMajorName: resumeMajorOptions.value.find((item: any) => String(item.id) === String(statusChangeForm.newMajorId))?.label || '', + newClassId: statusChangeForm.newClassId, + newClassName: resumeClassOptions.value.find((item: any) => String(item.id) === String(statusChangeForm.newClassId))?.label || '', + effectiveDate: statusChangeForm.effectiveDate, + reason: statusChangeForm.reason, + operatorId: statusChangeForm.operatorId || undefined, + operatorName: statusChangeForm.operatorName + }; + + const res = await service.post('/studentMsg/studentStatusChange/resume-with-class-change', resumePayload); + if (res && res.code === 200) { + ElMessage.success('复学并换班级提交成功'); + statusChangeDialog.visible = false; + await getList(); + } else { + ElMessage.error(res?.msg || '提交失败'); + } + return; + } + const submitData = statusChangeForm.selectedStudents.map((student) => ({ studentId: student.id, studentNo: student.studentNo,