diff --git a/.cursor/cursorrules-workflow.md b/.cursor/cursorrules-workflow.md index 2ab6a87d..d1ee573a 100644 --- a/.cursor/cursorrules-workflow.md +++ b/.cursor/cursorrules-workflow.md @@ -632,6 +632,12 @@ const handleStartWorkFlow = async (businessData: any) => { // - 退回 (backProcess) ``` +**🔴 退回残留任务清理(强制性,2026-10-15)**: + +- `backProcess` 在 `skip(REJECT)` 后必须清理同实例下非目标节点残留 `flow_task`/`flow_user`(`cleanResidualTasksAfterBack`) +- 草稿/撤销/退回重新提交时,`startWorkFlow` 需自愈多任务脏数据并按实例状态解析正确 taskId(`healResidualTasksOnRestart`) +- 背景:退回后若残留原审批节点 + draft 申请人任务,移动端再提交会报「启动工作流失败」 + ### 3. 流程撤销 ```typescript diff --git a/backend/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/service/impl/FlwTaskServiceImpl.java b/backend/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/service/impl/FlwTaskServiceImpl.java index 54f0ab28..181e6e87 100644 --- a/backend/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/service/impl/FlwTaskServiceImpl.java +++ b/backend/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/service/impl/FlwTaskServiceImpl.java @@ -112,12 +112,18 @@ public class FlwTaskServiceImpl implements IFlwTaskService { .eq(FlowInstance::getBusinessId, businessId)); if (ObjectUtil.isNotNull(flowInstance)) { BusinessStatusEnum.checkStartStatus(flowInstance.getFlowStatus()); + // 草稿/撤销/退回重新提交前,清理历史残留待办,避免取到错误 taskId + healResidualTasksOnRestart(flowInstance); List taskList = taskService.list(new FlowTask().setInstanceId(flowInstance.getId())); + if (CollUtil.isEmpty(taskList)) { + throw new ServiceException("当前流程无待办任务,无法重新提交,请联系管理员"); + } + Task currentTask = resolveRestartTask(taskList, flowInstance); taskService.mergeVariable(flowInstance, variables); insService.updateById(flowInstance); StartProcessReturnDTO dto = new StartProcessReturnDTO(); - dto.setProcessInstanceId(taskList.get(0).getInstanceId()); - dto.setTaskId(taskList.get(0).getId()); + dto.setProcessInstanceId(currentTask.getInstanceId()); + dto.setTaskId(currentTask.getId()); return dto; } FlowParams flowParams = FlowParams.build() @@ -421,6 +427,9 @@ public class FlwTaskServiceImpl implements IFlwTaskService { .hisTaskExt(bo.getFileId()); taskService.skip(task.getId(), flowParams); + // 退回后清理残留待办(原节点未删干净、历史 draft 申请人任务等) + cleanResidualTasksAfterBack(task.getInstanceId(), bo.getNodeCode()); + // 保存手写签名到历史任务表(驳回场景) if (StringUtils.isNotBlank(bo.getSignature())) { flwHisTaskExtMapper.updateSignatureByTaskId(taskId, bo.getSignature()); @@ -433,6 +442,135 @@ public class FlwTaskServiceImpl implements IFlwTaskService { } } + /** + * 退回后清理残留待办:仅保留目标节点上最新有效任务,删除其余 flow_task 及关联 flow_user。 + *

+ * warm-flow skip(REJECT) 后偶发残留原审批节点任务、以及启动时留下的 draft 申请人任务, + * 导致学生重新提交时 startWorkFlow 取到错误 taskId,移动端提示「启动工作流失败」。 + * + * @param instanceId 流程实例ID + * @param targetNodeCode 退回目标节点编码 + */ + private void cleanResidualTasksAfterBack(Long instanceId, String targetNodeCode) { + List tasks = selectByInstId(instanceId); + if (CollUtil.isEmpty(tasks) || tasks.size() == 1) { + return; + } + if (StringUtils.isBlank(targetNodeCode)) { + log.warn("退回清理跳过:目标节点编码为空, instanceId={}", instanceId); + return; + } + + Comparator newestFirst = Comparator + .comparing(FlowTask::getCreateTime, Comparator.nullsLast(Comparator.reverseOrder())) + .thenComparing(FlowTask::getId, Comparator.nullsLast(Comparator.reverseOrder())); + + List targetTasks = tasks.stream() + .filter(t -> StringUtils.equals(targetNodeCode, t.getNodeCode())) + .sorted(newestFirst) + .collect(Collectors.toList()); + if (CollUtil.isEmpty(targetTasks)) { + log.warn("退回清理未找到目标节点任务, instanceId={}, targetNodeCode={}, remain={}", + instanceId, targetNodeCode, StreamUtils.toList(tasks, FlowTask::getId)); + return; + } + + // 优先保留目标节点上状态为 back 的最新任务,否则取目标节点最新一条 + FlowTask keepTask = targetTasks.stream() + .filter(t -> TaskStatusEnum.BACK.getStatus().equals(t.getFlowStatus())) + .findFirst() + .orElse(targetTasks.get(0)); + + List deleteIds = tasks.stream() + .map(FlowTask::getId) + .filter(id -> !Objects.equals(id, keepTask.getId())) + .collect(Collectors.toList()); + if (CollUtil.isEmpty(deleteIds)) { + return; + } + + log.info("退回后清理残留任务 instanceId={}, keepTaskId={}, keepNode={}, deleteTaskIds={}", + instanceId, keepTask.getId(), keepTask.getNodeName(), deleteIds); + deleteTasksWithUsers(deleteIds); + } + + /** + * 草稿/撤销/退回重新提交前,清理同实例下多余残留待办,只保留与实例状态匹配的有效任务。 + */ + private void healResidualTasksOnRestart(FlowInstance flowInstance) { + if (!BusinessStatusEnum.isDraftOrCancelOrBack(flowInstance.getFlowStatus())) { + return; + } + List tasks = selectByInstId(flowInstance.getId()); + if (CollUtil.isEmpty(tasks) || tasks.size() <= 1) { + return; + } + + String status = flowInstance.getFlowStatus(); + String applyNodeCode = flwCommonService.applyNodeCode(flowInstance.getDefinitionId()); + Comparator newestFirst = Comparator + .comparing(FlowTask::getCreateTime, Comparator.nullsLast(Comparator.reverseOrder())) + .thenComparing(FlowTask::getId, Comparator.nullsLast(Comparator.reverseOrder())); + + FlowTask keepTask = tasks.stream() + .filter(t -> status.equals(t.getFlowStatus())) + .filter(t -> StringUtils.isBlank(applyNodeCode) || applyNodeCode.equals(t.getNodeCode())) + .sorted(newestFirst) + .findFirst() + .orElseGet(() -> tasks.stream() + .filter(t -> status.equals(t.getFlowStatus())) + .sorted(newestFirst) + .findFirst() + .orElse(null)); + if (keepTask == null) { + log.warn("重新提交自愈未找到匹配状态任务, instanceId={}, status={}, remain={}", + flowInstance.getId(), status, StreamUtils.toList(tasks, FlowTask::getId)); + return; + } + + List deleteIds = tasks.stream() + .map(FlowTask::getId) + .filter(id -> !Objects.equals(id, keepTask.getId())) + .collect(Collectors.toList()); + if (CollUtil.isEmpty(deleteIds)) { + return; + } + + log.info("重新提交前清理残留任务 instanceId={}, keepTaskId={}, keepNode={}, deleteTaskIds={}", + flowInstance.getId(), keepTask.getId(), keepTask.getNodeName(), deleteIds); + deleteTasksWithUsers(deleteIds); + } + + /** + * 从待办列表中解析重新提交应使用的任务(多任务时优先匹配实例状态)。 + */ + private Task resolveRestartTask(List taskList, FlowInstance flowInstance) { + if (taskList.size() == 1) { + return taskList.get(0); + } + String status = flowInstance.getFlowStatus(); + List matched = taskList.stream() + .filter(t -> status.equals(t.getFlowStatus())) + .collect(Collectors.toList()); + if (CollUtil.isNotEmpty(matched)) { + return matched.get(0); + } + log.warn("重新提交存在多个待办且无法按状态匹配, instanceId={}, status={}, taskIds={}", + flowInstance.getId(), status, StreamUtils.toList(taskList, Task::getId)); + return taskList.get(0); + } + + /** + * 删除待办任务及其审批人关联。 + */ + private void deleteTasksWithUsers(List taskIds) { + if (CollUtil.isEmpty(taskIds)) { + return; + } + FlowEngine.userService().deleteByTaskIds(taskIds); + flowTaskMapper.deleteByIds(taskIds); + } + /** * 获取可驳回的前置节点 * diff --git a/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/service/impl/XgHolidayStayServiceImpl.java b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/service/impl/XgHolidayStayServiceImpl.java index 2ed2a49a..985b0e75 100644 --- a/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/service/impl/XgHolidayStayServiceImpl.java +++ b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/service/impl/XgHolidayStayServiceImpl.java @@ -245,7 +245,16 @@ public class XgHolidayStayServiceImpl implements IXgHolidayStayService { * 保存前的数据校验 */ private void validEntityBeforeSave(XgHolidayStay entity) { - // TODO 做一些数据校验,如唯一约束 + // 库表 student_phone / emergency_* 为 NOT NULL;null 会被 MyBatis 跳过导致 SQLException + if (StringUtils.isBlank(entity.getStudentPhone())) { + throw new ServiceException("学生联系电话不能为空"); + } + if (StringUtils.isBlank(entity.getEmergencyContact())) { + throw new ServiceException("紧急联系人不能为空"); + } + if (StringUtils.isBlank(entity.getEmergencyPhone())) { + throw new ServiceException("紧急联系电话不能为空"); + } } /** @@ -787,6 +796,16 @@ public class XgHolidayStayServiceImpl implements IXgHolidayStayService { if (bo.getBedId() == null || bo.getRoomId() == null) { throw new ServiceException("未查询到当前在住宿舍/床位信息,请先完成床位分配后再申请留宿"); } + // student_phone / emergency_* 为库表 NOT NULL,空值会被 MyBatis 跳过导致 SQL 报错 + if (StringUtils.isBlank(bo.getStudentPhone())) { + throw new ServiceException("请填写联系电话"); + } + if (StringUtils.isBlank(bo.getEmergencyContact())) { + throw new ServiceException("请填写紧急联系人"); + } + if (StringUtils.isBlank(bo.getEmergencyPhone())) { + throw new ServiceException("请填写紧急联系电话"); + } bo.setApplyStatus("1"); bo.setApplyTime(new Date()); bo.setAuditorId(null); @@ -903,7 +922,8 @@ public class XgHolidayStayServiceImpl implements IXgHolidayStayService { } } bo.setCollegeName(student.getCollegeName()); - if (StringUtils.isBlank(bo.getStudentPhone())) { + // 仅在档案有电话时回填,避免把表单空串覆盖成 null 导致 INSERT 省略该字段 + if (StringUtils.isBlank(bo.getStudentPhone()) && StringUtils.isNotBlank(student.getPhone())) { bo.setStudentPhone(student.getPhone()); } } diff --git a/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/service/impl/XgStudentCancelLeaveServiceImpl.java b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/service/impl/XgStudentCancelLeaveServiceImpl.java index b9e907da..d4f76045 100644 --- a/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/service/impl/XgStudentCancelLeaveServiceImpl.java +++ b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/service/impl/XgStudentCancelLeaveServiceImpl.java @@ -1,5 +1,6 @@ package org.dromara.xg.service.impl; +import org.dromara.common.core.enums.BusinessStatusEnum; import org.dromara.common.core.utils.MapstructUtils; import org.dromara.common.core.utils.StringUtils; import org.dromara.common.mybatis.core.page.TableDataInfo; @@ -144,6 +145,7 @@ public class XgStudentCancelLeaveServiceImpl implements IXgStudentCancelLeaveSer @Override @Transactional(rollbackFor = Exception.class) public XgStudentCancelLeaveVo insertByBo(XgStudentCancelLeaveBo bo) { + bo.setCancelStatus(resolveCancelStatusForSave(null, bo.getCancelStatus())); XgStudentCancelLeave add = MapstructUtils.convert(bo, XgStudentCancelLeave.class); validEntityBeforeSave(add); boolean flag = baseMapper.insert(add) > 0; @@ -178,6 +180,12 @@ public class XgStudentCancelLeaveServiceImpl implements IXgStudentCancelLeaveSer @Override @Transactional(rollbackFor = Exception.class) public Boolean updateByBo(XgStudentCancelLeaveBo bo) { + if (bo.getId() != null) { + XgStudentCancelLeave existing = baseMapper.selectById(bo.getId()); + if (existing != null) { + bo.setCancelStatus(resolveCancelStatusForSave(existing.getCancelStatus(), bo.getCancelStatus())); + } + } XgStudentCancelLeave update = MapstructUtils.convert(bo, XgStudentCancelLeave.class); validEntityBeforeSave(update); boolean flag = baseMapper.updateById(update) > 0; @@ -200,6 +208,34 @@ public class XgStudentCancelLeaveServiceImpl implements IXgStudentCancelLeaveSer return flag; } + /** + * 销假保存时的状态。 + * 请假单上的 0/1/2/3 是「未销假、已销假、逾期、无需销假」,不能当作流程状态写入。 + * 流程已进入待审核、退回、完成等状态后,再次保存销假内容不得把状态盖回去。 + */ + private String resolveCancelStatusForSave(String existingStatus, String incomingStatus) { + if (isWorkflowOwnedStatus(existingStatus)) { + return existingStatus; + } + if (StringUtils.isBlank(incomingStatus) + || "0".equals(incomingStatus) + || "1".equals(incomingStatus) + || "2".equals(incomingStatus) + || "3".equals(incomingStatus)) { + return BusinessStatusEnum.DRAFT.getStatus(); + } + return incomingStatus; + } + + private boolean isWorkflowOwnedStatus(String status) { + return BusinessStatusEnum.WAITING.getStatus().equals(status) + || BusinessStatusEnum.FINISH.getStatus().equals(status) + || BusinessStatusEnum.INVALID.getStatus().equals(status) + || BusinessStatusEnum.TERMINATION.getStatus().equals(status) + || BusinessStatusEnum.BACK.getStatus().equals(status) + || BusinessStatusEnum.CANCEL.getStatus().equals(status); + } + /** * 保存前的数据校验 */ diff --git a/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/service/impl/XgStudentLeaveServiceImpl.java b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/service/impl/XgStudentLeaveServiceImpl.java index af306b4c..625b9cfb 100644 --- a/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/service/impl/XgStudentLeaveServiceImpl.java +++ b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/service/impl/XgStudentLeaveServiceImpl.java @@ -561,9 +561,47 @@ public class XgStudentLeaveServiceImpl implements IXgStudentLeaveService { * * @param processTaskEvent 参数 */ + @Transactional(rollbackFor = Exception.class) @EventListener(condition = "#processTaskEvent.flowCode.startsWith('hzdyjs-xj')") public void processTaskHandler_xj(ProcessTaskEvent processTaskEvent) { log.info("当前任务创建了{}", processTaskEvent.toString()); + if (StringUtils.isBlank(processTaskEvent.getBusinessId())) { + return; + } + if (!BusinessStatusEnum.WAITING.getStatus().equals(processTaskEvent.getStatus())) { + return; + } + XgStudentCancelLeave cancelLeave = xgStudentCancelLeaveMapper + .selectById(Long.valueOf(processTaskEvent.getBusinessId())); + if (ObjectUtil.isNull(cancelLeave) || !shouldAlignCancelStatusToWaiting(cancelLeave.getCancelStatus())) { + return; + } + String previousStatus = cancelLeave.getCancelStatus(); + cancelLeave.setCancelStatus(BusinessStatusEnum.WAITING.getStatus()); + xgStudentCancelLeaveMapper.updateById(cancelLeave); + if (cancelLeave.getLeaveId() != null) { + XgStudentLeave leave = baseMapper.selectById(cancelLeave.getLeaveId()); + if (ObjectUtil.isNotNull(leave)) { + leave.setCancelStatus(BusinessStatusEnum.WAITING.getStatus()); + baseMapper.updateById(leave); + } + } + log.info("销假业务状态已对齐为 waiting, businessId={}, taskId={}, nodeName={}, previousStatus={}", + processTaskEvent.getBusinessId(), processTaskEvent.getTaskId(), processTaskEvent.getNodeName(), + previousStatus); + } + + /** + * 销假流程状态尚未写成 waiting 时需要对齐。 + * 0/1/2/3 是请假单上的旧销假字典,draft 是提交标记丢失后停留的草稿。 + */ + private boolean shouldAlignCancelStatusToWaiting(String cancelStatus) { + return StringUtils.isBlank(cancelStatus) + || "0".equals(cancelStatus) + || "1".equals(cancelStatus) + || "2".equals(cancelStatus) + || "3".equals(cancelStatus) + || BusinessStatusEnum.DRAFT.getStatus().equals(cancelStatus); } /** diff --git a/docs/开发文档/API/假期留宿管理API.md b/docs/开发文档/API/假期留宿管理API.md new file mode 100644 index 00000000..65ab60da --- /dev/null +++ b/docs/开发文档/API/假期留宿管理API.md @@ -0,0 +1,27 @@ +# 假期留宿管理 API + +## 学生申请留宿 + +- **接口**: `POST /holidayStay/holidayStay/apply` +- **权限**: `holidayStay:holidayStay:apply` + +### 必填业务字段(库表 NOT NULL) + +| 字段 | 说明 | +|------|------| +| holidayType | 假期类型 | +| applyStartDate / applyEndDate | 留宿起止时间 | +| stayReason | 留宿原因 | +| studentPhone | 学生联系电话(档案无电话时须前端手动填写) | +| emergencyContact | 紧急联系人 | +| emergencyPhone | 紧急联系电话 | + +> **说明(2026-10-15)**: +> - 库表已支持 `student_phone` 可空(见 `docs/设计文档/database/假期留宿_student_phone允许为空.sql`),仅执行 SQL 即可先解除线上报错、无需发版。 +> - 代码侧仍建议保留校验/友好提示;档案 `phone` 为空时不会把表单空串覆盖为 `null`。 + +### 自动填充 + +- 学生信息快照:学号/姓名/班级/专业/学院(按当前登录学生) +- 宿舍快照:当前在住床位(无床位则拒绝申请) +- 申请状态:`1`(待审批) diff --git a/docs/开发文档/API/学生销假管理API.md b/docs/开发文档/API/学生销假管理API.md index f13a105a..f69e55d4 100644 --- a/docs/开发文档/API/学生销假管理API.md +++ b/docs/开发文档/API/学生销假管理API.md @@ -307,6 +307,8 @@ **事务保证**:使用 `@Transactional` 注解确保两个表的更新操作要么全部成功,要么全部回滚。 +**状态保护**(2026-10-19):`cancelStatus` 在销假流程中使用 `draft`、`waiting`、`finish` 等流程状态。请假单上的 `0/1/2/3`(未销假、已销假、逾期未销假、无需销假)传入本接口时按 `draft` 保存,不会写入销假单。销假单已是 `waiting`、`back`、`cancel`、`finish`、`invalid`、`termination` 时,再次保存销假内容不会覆盖该状态。审批任务创建且流程为待审核时,若业务状态仍是旧字典值或 `draft`,会自动对齐为 `waiting`。 + ### 5. 删除销假 - **接口路径**:`DELETE /studentCancelLeave/studentCancelLeave/{ids}` diff --git a/docs/开发文档/warm-flow工作流框架开发指南.md b/docs/开发文档/warm-flow工作流框架开发指南.md index 379610d0..77ca4ce0 100644 --- a/docs/开发文档/warm-flow工作流框架开发指南.md +++ b/docs/开发文档/warm-flow工作流框架开发指南.md @@ -1045,12 +1045,22 @@ public class TestLeaveController { 1. 流程定义未发布 2. 流程编码不存在 3. 业务数据状态不正确 +4. **退回后残留多个 `flow_task`**(原审批节点未清、历史 draft 申请人任务),重新提交时取到错误 taskId,移动端统一提示「启动工作流失败」 **解决**: 1. 检查流程定义是否已发布 2. 确认 `flowCode` 参数正确 -3. 确保业务数据状态为 `draft` +3. 确保业务数据状态为 `draft` / `back` / `cancel`(可重新提交) +4. **退回清理(已实现)**:`FlwTaskServiceImpl.backProcess` 在 `skip(REJECT)` 后调用 `cleanResidualTasksAfterBack`,只保留退回目标节点最新有效任务;重新提交时 `healResidualTasksOnRestart` 自愈历史脏数据 + +**排查 SQL 示例**(同一 `instance_id` 不应长期存在多条无关待办): + +```sql +SELECT id, node_name, flow_status, create_time +FROM flow_task +WHERE instance_id = ? AND IFNULL(del_flag, '0') = '0'; +``` ### 9.5 日期格式问题 diff --git a/docs/数据库脚本/2026-10-16-xg_student_msg-set-boarder-2026.sql b/docs/数据库脚本/2026-10-16-xg_student_msg-set-boarder-2026.sql new file mode 100644 index 00000000..ce26df83 --- /dev/null +++ b/docs/数据库脚本/2026-10-16-xg_student_msg-set-boarder-2026.sql @@ -0,0 +1,126 @@ +-- ============================================================================= +-- 将入学年份为 2026 或年级为 2026 的学生「是否住校」统一改为「是」 +-- +-- 目标表字段: +-- xg_student_msg.is_boarder 0-否,1-是 +-- xg_student_msg.enrollment_year / grade +-- 同时兼容班级表 xg_class_msg.enrollment_year / grade(学生字段为空时以班级为准) +-- +-- 用法: +-- mysql -h -u root -p hzdyjs_xg < docs/数据库脚本/2026-10-16-xg_student_msg-set-boarder-2026.sql +-- +-- 建议:先执行「一、预览」确认影响范围,再执行「二、更新」 +-- ============================================================================= + +SET NAMES utf8mb4; + +-- ----------------------------------------------------------------------------- +-- 一、预览:命中范围与当前住校分布 +-- ----------------------------------------------------------------------------- + +SELECT '========== 1. 命中学生数量(含已是住校)==========' AS section; + +SELECT COUNT(*) AS match_total +FROM xg_student_msg s +LEFT JOIN xg_class_msg c + ON c.deleted = 0 + AND NULLIF(TRIM(s.class_id), '') IS NOT NULL + AND c.id = CAST(NULLIF(TRIM(s.class_id), '') AS UNSIGNED) +WHERE s.deleted = 0 + AND ( + s.enrollment_year = 2026 + OR s.grade = '2026' + OR c.enrollment_year = 2026 + OR c.grade = '2026' + ); + +SELECT '========== 2. 命中且尚未住校(将被更新)==========' AS section; + +SELECT COUNT(*) AS need_update_count +FROM xg_student_msg s +LEFT JOIN xg_class_msg c + ON c.deleted = 0 + AND NULLIF(TRIM(s.class_id), '') IS NOT NULL + AND c.id = CAST(NULLIF(TRIM(s.class_id), '') AS UNSIGNED) +WHERE s.deleted = 0 + AND IFNULL(s.is_boarder, 0) <> 1 + AND ( + s.enrollment_year = 2026 + OR s.grade = '2026' + OR c.enrollment_year = 2026 + OR c.grade = '2026' + ); + +SELECT '========== 3. 命中明细(抽样核对)==========' AS section; + +SELECT + s.id, + s.student_no, + s.name, + s.enrollment_year AS student_enrollment_year, + s.grade AS student_grade, + c.enrollment_year AS class_enrollment_year, + c.grade AS class_grade, + s.is_boarder, + s.class_name, + s.college_name, + s.student_status +FROM xg_student_msg s +LEFT JOIN xg_class_msg c + ON c.deleted = 0 + AND NULLIF(TRIM(s.class_id), '') IS NOT NULL + AND c.id = CAST(NULLIF(TRIM(s.class_id), '') AS UNSIGNED) +WHERE s.deleted = 0 + AND ( + s.enrollment_year = 2026 + OR s.grade = '2026' + OR c.enrollment_year = 2026 + OR c.grade = '2026' + ) +ORDER BY s.enrollment_year, s.student_no +LIMIT 200; + +-- ----------------------------------------------------------------------------- +-- 二、更新:是否住校 -> 是(1) +-- ----------------------------------------------------------------------------- + +START TRANSACTION; + +UPDATE xg_student_msg s +LEFT JOIN xg_class_msg c + ON c.deleted = 0 + AND NULLIF(TRIM(s.class_id), '') IS NOT NULL + AND c.id = CAST(NULLIF(TRIM(s.class_id), '') AS UNSIGNED) +SET + s.is_boarder = 1, + s.update_time = NOW() +WHERE s.deleted = 0 + AND IFNULL(s.is_boarder, 0) <> 1 + AND ( + s.enrollment_year = 2026 + OR s.grade = '2026' + OR c.enrollment_year = 2026 + OR c.grade = '2026' + ); + +-- 核对更新结果(事务内) +SELECT ROW_COUNT() AS updated_rows; + +SELECT COUNT(*) AS still_not_boarder +FROM xg_student_msg s +LEFT JOIN xg_class_msg c + ON c.deleted = 0 + AND NULLIF(TRIM(s.class_id), '') IS NOT NULL + AND c.id = CAST(NULLIF(TRIM(s.class_id), '') AS UNSIGNED) +WHERE s.deleted = 0 + AND IFNULL(s.is_boarder, 0) <> 1 + AND ( + s.enrollment_year = 2026 + OR s.grade = '2026' + OR c.enrollment_year = 2026 + OR c.grade = '2026' + ); + +COMMIT; + +-- 可选:仅预览不更新时,把上面 TRANSACTION 整段注释掉,或改为 ROLLBACK diff --git a/docs/设计文档/database/fix_朱茏_学生管理员设置分配范围权限.sql b/docs/设计文档/database/fix_朱茏_学生管理员设置分配范围权限.sql new file mode 100644 index 00000000..c4a92b02 --- /dev/null +++ b/docs/设计文档/database/fix_朱茏_学生管理员设置分配范围权限.sql @@ -0,0 +1,342 @@ +-- ============================================================ +-- 临时权限修复:学生管理员设置「分配管理范围」 +-- 背景: +-- 页面:学生管理员设置 / 自定义管理员 / 为【xxx】分配管理范围 +-- 提交接口:POST /xgStudentManagerSource/studentManagerSource/grantDeptAuth +-- 后端权限:xgStudentManagerSource:studentManagerSource:grantDeptAuth +-- 前端无按钮权限控制,点确定后接口 403: +-- 黄条「无访问权限,可能原因:1. 用户权限不足…」 +-- 红字「分配失败,请重试」 +-- 该权限码是后加的自定义接口,菜单里往往没有对应按钮,角色也就授不了 +-- 适用: +-- 朱茏老师(user_id=223,角色含 3003-学生信息管理) +-- 以及已经能打开本模块(已有管理员列表/授权查询权限)的同类角色 +-- 执行后: +-- 相关用户需重新登录,使权限缓存生效 +-- ============================================================ + +-- ------------------------------------------------------------ +-- A) 先查(只读,可单独执行) +-- ------------------------------------------------------------ + +-- A1. 朱茏当前角色,以及本模块相关权限(无行=该权限没有) +SELECT + u.user_id, + u.user_name, + u.nick_name, + r.role_id, + r.role_name, + m.menu_id, + m.menu_name, + m.perms +FROM sys_user u +JOIN sys_user_role ur ON ur.user_id = u.user_id +JOIN sys_role r ON r.role_id = ur.role_id +JOIN sys_role_menu rm ON rm.role_id = r.role_id +JOIN sys_menu m ON m.menu_id = rm.menu_id +WHERE u.user_id = 223 + AND ( + m.perms LIKE 'xgStudentManager%' + OR m.menu_name LIKE '%学生管理员%' + ) +ORDER BY m.perms, r.role_id; + +-- A2. 关键权限是否已具备(1=有,0=没有) +SELECT + p.perms, + EXISTS ( + SELECT 1 + FROM sys_user_role ur + JOIN sys_role_menu rm ON rm.role_id = ur.role_id + JOIN sys_menu m ON m.menu_id = rm.menu_id + WHERE ur.user_id = 223 AND m.perms = p.perms + ) AS has_perm +FROM ( + SELECT 'xgStudentManager:studentManager:list' AS perms + UNION ALL SELECT 'xgStudentManagerSource:studentManagerSource:list' + UNION ALL SELECT 'xgStudentManagerSource:studentManagerSource:query' + UNION ALL SELECT 'xgStudentManagerSource:studentManagerSource:grantDeptAuth' +) p; + +-- A3. 库里是否已有这些菜单权限码(grantDeptAuth 无行=需要下面的修复段补菜单) +SELECT menu_id, menu_name, parent_id, menu_type, perms, status, visible +FROM sys_menu +WHERE perms IN ( + 'xgStudentManager:studentManager:list', + 'xgStudentManagerSource:studentManagerSource:list', + 'xgStudentManagerSource:studentManagerSource:query', + 'xgStudentManagerSource:studentManagerSource:edit', + 'xgStudentManagerSource:studentManagerSource:grantDeptAuth' +) + OR menu_name LIKE '%学生管理员%' +ORDER BY parent_id, order_num, menu_id; + +-- ============================================================ +-- 以下为修复段:确认缺 grantDeptAuth 后再执行;先备份再改 +-- ============================================================ + +-- ------------------------------------------------------------ +-- 0) 修改前备份(带时间戳后缀,避免覆盖) +-- ------------------------------------------------------------ +SET @bak_suffix = DATE_FORMAT(NOW(), '%Y%m%d_%H%i%s'); + +SET @sql_bak_menu = CONCAT( + 'CREATE TABLE IF NOT EXISTS sys_menu_bak_', @bak_suffix, + ' AS SELECT * FROM sys_menu' +); +PREPARE stmt_bak_menu FROM @sql_bak_menu; +EXECUTE stmt_bak_menu; +DEALLOCATE PREPARE stmt_bak_menu; + +SET @sql_bak_role_menu = CONCAT( + 'CREATE TABLE IF NOT EXISTS sys_role_menu_bak_', @bak_suffix, + ' AS SELECT * FROM sys_role_menu' +); +PREPARE stmt_bak_role_menu FROM @sql_bak_role_menu; +EXECUTE stmt_bak_role_menu; +DEALLOCATE PREPARE stmt_bak_role_menu; + +SELECT + CONCAT('sys_menu_bak_', @bak_suffix) AS menu_backup_table, + CONCAT('sys_role_menu_bak_', @bak_suffix) AS role_menu_backup_table; + +-- ------------------------------------------------------------ +-- 1) 解析父菜单(一条 SELECT,避免 Navicat 把 SET+子查询拆段) +-- 优先级:授权关系列表菜单 > 其父级 > 同模块按钮父级 > 学生管理员设置 > 管理员列表父级 +-- ------------------------------------------------------------ +SET @parent_id = NULL; + +SELECT picked.menu_id INTO @parent_id +FROM ( + SELECT menu_id, 1 AS ord + FROM sys_menu + WHERE perms = 'xgStudentManagerSource:studentManagerSource:list' + AND menu_type IN ('C', 'M') + UNION ALL + SELECT parent_id, 2 + FROM sys_menu + WHERE perms = 'xgStudentManagerSource:studentManagerSource:list' + UNION ALL + SELECT parent_id, 3 + FROM sys_menu + WHERE perms IN ( + 'xgStudentManagerSource:studentManagerSource:query', + 'xgStudentManagerSource:studentManagerSource:edit', + 'xgStudentManagerSource:studentManagerSource:add' + ) + UNION ALL + SELECT menu_id, 4 + FROM sys_menu + WHERE menu_name LIKE '%学生管理员设置%' + AND menu_type IN ('C', 'M') + UNION ALL + SELECT parent_id, 5 + FROM sys_menu + WHERE perms = 'xgStudentManager:studentManager:list' +) picked +WHERE picked.menu_id IS NOT NULL +ORDER BY picked.ord, picked.menu_id +LIMIT 1; + +SELECT @parent_id AS resolved_parent_id; + +-- ------------------------------------------------------------ +-- 2) 新增「分配管理范围」按钮权限(若不存在,且已解析到父菜单) +-- ------------------------------------------------------------ +INSERT INTO sys_menu ( + menu_id, menu_name, parent_id, order_num, path, component, query_param, + is_frame, is_cache, menu_type, visible, status, perms, icon, + create_dept, create_by, create_time, update_by, update_time, remark +) +SELECT + 2005461500000000003, + '分配管理范围', + @parent_id, + 6, + '', + NULL, + NULL, + 1, + 0, + 'F', + '0', + '0', + 'xgStudentManagerSource:studentManagerSource:grantDeptAuth', + '', + 100, + 1, + NOW(), + 1, + NOW(), + '补齐自定义管理员分配管理范围接口权限,解决提交403' +FROM DUAL +WHERE @parent_id IS NOT NULL + AND NOT EXISTS ( + SELECT 1 FROM sys_menu + WHERE perms = 'xgStudentManagerSource:studentManagerSource:grantDeptAuth' + ); + +-- 父菜单没解析到时,这条会提示,且上面的 INSERT 不会写入 +SELECT + CASE + WHEN @parent_id IS NULL THEN '未找到父菜单,未插入按钮。请先看 A3 的菜单结果,手工指定 parent_id 后再执行第 2 步' + WHEN EXISTS ( + SELECT 1 FROM sys_menu + WHERE perms = 'xgStudentManagerSource:studentManagerSource:grantDeptAuth' + ) THEN 'grantDeptAuth 菜单已存在(含本次新建)' + ELSE '未插入,请检查 sys_menu' + END AS menu_insert_result; + +-- ------------------------------------------------------------ +-- 3) 给「学生信息管理」角色(3003)授权(朱茏已拥有该角色) +-- ------------------------------------------------------------ +INSERT INTO sys_role_menu (role_id, menu_id) +SELECT 3003, m.menu_id +FROM sys_menu m +WHERE m.perms = 'xgStudentManagerSource:studentManagerSource:grantDeptAuth' + AND NOT EXISTS ( + SELECT 1 FROM sys_role_menu rm + WHERE rm.role_id = 3003 AND rm.menu_id = m.menu_id + ); + +-- ------------------------------------------------------------ +-- 4) 兼容:已能使用本模块的角色,一并补上分配范围权限 +-- 判定:已有管理员列表、授权关系查询/列表/修改,或菜单名含「学生管理员设置」 +-- ------------------------------------------------------------ +INSERT INTO sys_role_menu (role_id, menu_id) +SELECT DISTINCT rm.role_id, grant_menu.menu_id +FROM sys_role_menu rm +JOIN sys_menu owned ON owned.menu_id = rm.menu_id +JOIN sys_menu grant_menu + ON grant_menu.perms = 'xgStudentManagerSource:studentManagerSource:grantDeptAuth' +WHERE ( + owned.perms IN ( + 'xgStudentManager:studentManager:list', + 'xgStudentManagerSource:studentManagerSource:list', + 'xgStudentManagerSource:studentManagerSource:query', + 'xgStudentManagerSource:studentManagerSource:edit', + 'xgStudentManagerSource:studentManagerSource:add' + ) + OR owned.menu_name LIKE '%学生管理员设置%' + ) + AND NOT EXISTS ( + SELECT 1 FROM sys_role_menu x + WHERE x.role_id = rm.role_id AND x.menu_id = grant_menu.menu_id + ); + +-- ------------------------------------------------------------ +-- 5) 校验:朱茏是否已具备分配范围权限 +-- ------------------------------------------------------------ +SELECT + u.user_id, + u.user_name, + u.nick_name, + r.role_id, + r.role_name, + m.menu_id, + m.menu_name, + m.perms +FROM sys_user u +JOIN sys_user_role ur ON ur.user_id = u.user_id +JOIN sys_role r ON r.role_id = ur.role_id +JOIN sys_role_menu rm ON rm.role_id = r.role_id +JOIN sys_menu m ON m.menu_id = rm.menu_id +WHERE u.user_id = 223 + AND m.perms = 'xgStudentManagerSource:studentManagerSource:grantDeptAuth'; + +-- 有行=已具备。无行时看她的角色是否在第 3/4 步的授权范围内 +SELECT + 'xgStudentManagerSource:studentManagerSource:grantDeptAuth' AS perms, + EXISTS ( + SELECT 1 + FROM sys_user_role ur + JOIN sys_role_menu rm ON rm.role_id = ur.role_id + JOIN sys_menu m ON m.menu_id = rm.menu_id + WHERE ur.user_id = 223 + AND m.perms = 'xgStudentManagerSource:studentManagerSource:grantDeptAuth' + ) AS has_perm; + +-- ------------------------------------------------------------ +-- 6) 复查是否执行成功(只读,可单独反复执行) +-- 期望:menu_ready=1,role_3003_ready=1,zhulong_ready=1 +-- ------------------------------------------------------------ +SELECT + EXISTS ( + SELECT 1 FROM sys_menu + WHERE perms = 'xgStudentManagerSource:studentManagerSource:grantDeptAuth' + AND menu_type = 'F' + AND status = '0' + AND parent_id IS NOT NULL + ) AS menu_ready, + EXISTS ( + SELECT 1 + FROM sys_role_menu rm + JOIN sys_menu m ON m.menu_id = rm.menu_id + WHERE rm.role_id = 3003 + AND m.perms = 'xgStudentManagerSource:studentManagerSource:grantDeptAuth' + ) AS role_3003_ready, + EXISTS ( + SELECT 1 + FROM sys_user u + JOIN sys_user_role ur ON ur.user_id = u.user_id + JOIN sys_role_menu rm ON rm.role_id = ur.role_id + JOIN sys_menu m ON m.menu_id = rm.menu_id + WHERE u.user_id = 223 + AND u.nick_name = '朱茏' + AND m.perms = 'xgStudentManagerSource:studentManagerSource:grantDeptAuth' + AND m.status = '0' + ) AS zhulong_ready; + +-- 明细:按钮挂在哪个菜单下,哪些角色拿到了 +SELECT + m.menu_id, + m.menu_name, + m.parent_id, + p.menu_name AS parent_menu_name, + m.perms, + m.menu_type, + m.status, + r.role_id, + r.role_name +FROM sys_menu m +LEFT JOIN sys_menu p ON p.menu_id = m.parent_id +LEFT JOIN sys_role_menu rm ON rm.menu_id = m.menu_id +LEFT JOIN sys_role r ON r.role_id = rm.role_id +WHERE m.perms = 'xgStudentManagerSource:studentManagerSource:grantDeptAuth' +ORDER BY r.role_id; + +-- 明细:朱茏通过哪个角色拥有该权限(有行即成功) +SELECT + u.user_id, + u.user_name, + u.nick_name, + r.role_id, + r.role_name, + m.menu_id, + m.menu_name, + m.perms, + m.status AS menu_status +FROM sys_user u +JOIN sys_user_role ur ON ur.user_id = u.user_id +JOIN sys_role r ON r.role_id = ur.role_id +JOIN sys_role_menu rm ON rm.role_id = r.role_id +JOIN sys_menu m ON m.menu_id = rm.menu_id +WHERE u.user_id = 223 + AND m.perms = 'xgStudentManagerSource:studentManagerSource:grantDeptAuth'; + +-- ------------------------------------------------------------ +-- 回滚示例(按需执行,把后缀换成上面查出的备份表名) +-- ------------------------------------------------------------ +-- DELETE FROM sys_role_menu +-- WHERE menu_id IN ( +-- SELECT menu_id FROM sys_menu +-- WHERE perms = 'xgStudentManagerSource:studentManagerSource:grantDeptAuth' +-- ); +-- DELETE FROM sys_menu +-- WHERE perms = 'xgStudentManagerSource:studentManagerSource:grantDeptAuth' +-- AND menu_id = 2005461500000000003; +-- -- 或整表回滚: +-- -- TRUNCATE TABLE sys_role_menu; +-- -- INSERT INTO sys_role_menu SELECT * FROM sys_role_menu_bak_YYYYMMDD_HHMMSS; +-- -- TRUNCATE TABLE sys_menu; +-- -- INSERT INTO sys_menu SELECT * FROM sys_menu_bak_YYYYMMDD_HHMMSS; diff --git a/docs/设计文档/database/fix_朱茏_学籍异动新增权限.sql b/docs/设计文档/database/fix_朱茏_学籍异动新增权限.sql new file mode 100644 index 00000000..77df5933 --- /dev/null +++ b/docs/设计文档/database/fix_朱茏_学籍异动新增权限.sql @@ -0,0 +1,132 @@ +-- ============================================================ +-- 临时权限修复:学籍异动「新增」权限(退学/休学/复学/毕业提交接口) +-- 背景: +-- 前端退学按钮权限:studentMsg:status:tuixue +-- 后端提交接口校验:studentMsg:studentStatusChange:add +-- 系统菜单中原先缺少 add 权限码,导致有按钮但提交报 403 +-- 适用: +-- 朱茏老师(user_id=223,角色含 3003-学生信息管理)及同类角色 +-- 执行后: +-- 相关用户需重新登录,使权限缓存生效 +-- ============================================================ + +-- ------------------------------------------------------------ +-- 0) 修改前备份相关表(带时间戳后缀,避免覆盖) +-- ------------------------------------------------------------ +SET @bak_suffix = DATE_FORMAT(NOW(), '%Y%m%d_%H%i%s'); + +SET @sql_bak_menu = CONCAT( + 'CREATE TABLE IF NOT EXISTS sys_menu_bak_', @bak_suffix, + ' AS SELECT * FROM sys_menu' +); +PREPARE stmt_bak_menu FROM @sql_bak_menu; +EXECUTE stmt_bak_menu; +DEALLOCATE PREPARE stmt_bak_menu; + +SET @sql_bak_role_menu = CONCAT( + 'CREATE TABLE IF NOT EXISTS sys_role_menu_bak_', @bak_suffix, + ' AS SELECT * FROM sys_role_menu' +); +PREPARE stmt_bak_role_menu FROM @sql_bak_role_menu; +EXECUTE stmt_bak_role_menu; +DEALLOCATE PREPARE stmt_bak_role_menu; + +-- 查看本次备份表名 +SELECT + CONCAT('sys_menu_bak_', @bak_suffix) AS menu_backup_table, + CONCAT('sys_role_menu_bak_', @bak_suffix) AS role_menu_backup_table; + +-- ------------------------------------------------------------ +-- 1) 新增「学籍异动-新增」按钮权限(挂在「学籍异动信息」菜单下) +-- ------------------------------------------------------------ +INSERT INTO sys_menu ( + menu_id, menu_name, parent_id, order_num, path, component, query_param, + is_frame, is_cache, menu_type, visible, status, perms, icon, + create_dept, create_by, create_time, update_by, update_time, remark +) +SELECT + 2005461500000000001, + '新增', + 2004197349643042818, -- 学籍异动信息 + 0, + '', + NULL, + NULL, + 1, + 0, + 'F', + '0', + '0', + 'studentMsg:studentStatusChange:add', + '', + 100, + 1, + NOW(), + 1, + NOW(), + '补齐学籍异动提交接口权限,解决退学/休学等提交403' +FROM DUAL +WHERE NOT EXISTS ( + SELECT 1 FROM sys_menu WHERE perms = 'studentMsg:studentStatusChange:add' +); + +-- 若库中已用其他 menu_id 创建了同名 perms,下面授权统一按 perms 查找 menu_id + +-- ------------------------------------------------------------ +-- 2) 给「学生信息管理(保留)」角色(3003)授权(朱茏已拥有该角色) +-- ------------------------------------------------------------ +INSERT INTO sys_role_menu (role_id, menu_id) +SELECT 3003, m.menu_id +FROM sys_menu m +WHERE m.perms = 'studentMsg:studentStatusChange:add' + AND NOT EXISTS ( + SELECT 1 FROM sys_role_menu rm + WHERE rm.role_id = 3003 AND rm.menu_id = m.menu_id + ); + +-- ------------------------------------------------------------ +-- 3) 兼容:所有已拥有「退学」按钮权限的角色,一并补上 add +-- ------------------------------------------------------------ +INSERT INTO sys_role_menu (role_id, menu_id) +SELECT DISTINCT rm.role_id, add_menu.menu_id +FROM sys_role_menu rm +JOIN sys_menu tuixue ON tuixue.menu_id = rm.menu_id AND tuixue.perms = 'studentMsg:status:tuixue' +JOIN sys_menu add_menu ON add_menu.perms = 'studentMsg:studentStatusChange:add' +WHERE NOT EXISTS ( + SELECT 1 FROM sys_role_menu x + WHERE x.role_id = rm.role_id AND x.menu_id = add_menu.menu_id +); + +-- ------------------------------------------------------------ +-- 4) 校验:朱茏是否已具备 add 权限 +-- ------------------------------------------------------------ +SELECT + u.user_id, + u.user_name, + u.nick_name, + r.role_id, + r.role_name, + m.perms, + m.menu_name +FROM sys_user u +JOIN sys_user_role ur ON ur.user_id = u.user_id +JOIN sys_role r ON r.role_id = ur.role_id +JOIN sys_role_menu rm ON rm.role_id = r.role_id +JOIN sys_menu m ON m.menu_id = rm.menu_id +WHERE u.user_id = 223 + AND m.perms IN ( + 'studentMsg:status:tuixue', + 'studentMsg:studentStatusChange:add', + 'studentMsg:studentStatusChange:list' + ) +ORDER BY m.perms, r.role_id; + +-- ------------------------------------------------------------ +-- 回滚示例(按需执行,把后缀换成上面查出的备份表名) +-- ------------------------------------------------------------ +-- TRUNCATE TABLE sys_role_menu; +-- INSERT INTO sys_role_menu SELECT * FROM sys_role_menu_bak_YYYYMMDD_HHMMSS; +-- DELETE FROM sys_menu WHERE perms = 'studentMsg:studentStatusChange:add'; +-- -- 或整表回滚菜单: +-- -- TRUNCATE TABLE sys_menu; +-- -- INSERT INTO sys_menu SELECT * FROM sys_menu_bak_YYYYMMDD_HHMMSS; diff --git a/docs/设计文档/database/fix_朱茏_换班级新增权限.sql b/docs/设计文档/database/fix_朱茏_换班级新增权限.sql new file mode 100644 index 00000000..10b1d6b9 --- /dev/null +++ b/docs/设计文档/database/fix_朱茏_换班级新增权限.sql @@ -0,0 +1,246 @@ +-- ============================================================ +-- 临时权限修复:换班级「新增」权限(学生信息页提交换班级接口) +-- 背景: +-- 前端换班级按钮权限:studentMsg:class:change +-- 后端提交接口校验:studentMsg:studentClassChange:add +-- 有按钮但提交报 403 Forbidden(无访问权限) +-- 适用: +-- 朱茏老师(user_id=223,角色含 3003-学生信息管理)及同类角色 +-- 执行后: +-- 相关用户需重新登录,使权限缓存生效 +-- ============================================================ + +-- ------------------------------------------------------------ +-- A) 先查:朱茏是否已具备 add / 前端按钮权限(只读,可单独执行) +-- ------------------------------------------------------------ +SELECT + u.user_id, + u.user_name, + u.nick_name, + r.role_id, + r.role_name, + m.menu_id, + m.menu_name, + m.perms +FROM sys_user u +JOIN sys_user_role ur ON ur.user_id = u.user_id +JOIN sys_role r ON r.role_id = ur.role_id +JOIN sys_role_menu rm ON rm.role_id = r.role_id +JOIN sys_menu m ON m.menu_id = rm.menu_id +WHERE u.user_id = 223 + AND m.perms IN ( + 'studentMsg:class:change', + 'studentMsg:studentClassChange:add', + 'studentMsg:studentClassChange:list', + 'studentMsg:studentClassChange:query', + 'studentMsg:studentClassChange:edit', + 'studentMsg:studentClassChange:remove', + 'studentMsg:studentClassChange:export' + ) +ORDER BY m.perms, r.role_id; + +-- 汇总:是否具备关键权限(有行=有,无行=没有) +SELECT + 'studentMsg:class:change' AS perms, + EXISTS ( + SELECT 1 + FROM sys_user_role ur + JOIN sys_role_menu rm ON rm.role_id = ur.role_id + JOIN sys_menu m ON m.menu_id = rm.menu_id + WHERE ur.user_id = 223 AND m.perms = 'studentMsg:class:change' + ) AS has_perm +UNION ALL +SELECT + 'studentMsg:studentClassChange:add', + EXISTS ( + SELECT 1 + FROM sys_user_role ur + JOIN sys_role_menu rm ON rm.role_id = ur.role_id + JOIN sys_menu m ON m.menu_id = rm.menu_id + WHERE ur.user_id = 223 AND m.perms = 'studentMsg:studentClassChange:add' + ); + +-- 库中是否已存在 add 菜单权限码 +SELECT menu_id, menu_name, parent_id, perms, status, visible +FROM sys_menu +WHERE perms IN ( + 'studentMsg:class:change', + 'studentMsg:studentClassChange:add', + 'studentMsg:studentClassChange:list' +) +ORDER BY perms; + +-- ============================================================ +-- 以下为修复段:确认缺权限后再执行;先备份再改 +-- ============================================================ + +-- ------------------------------------------------------------ +-- 0) 修改前备份相关表(带时间戳后缀,避免覆盖) +-- ------------------------------------------------------------ +SET @bak_suffix = DATE_FORMAT(NOW(), '%Y%m%d_%H%i%s'); + +SET @sql_bak_menu = CONCAT( + 'CREATE TABLE IF NOT EXISTS sys_menu_bak_', @bak_suffix, + ' AS SELECT * FROM sys_menu' +); +PREPARE stmt_bak_menu FROM @sql_bak_menu; +EXECUTE stmt_bak_menu; +DEALLOCATE PREPARE stmt_bak_menu; + +SET @sql_bak_role_menu = CONCAT( + 'CREATE TABLE IF NOT EXISTS sys_role_menu_bak_', @bak_suffix, + ' AS SELECT * FROM sys_role_menu' +); +PREPARE stmt_bak_role_menu FROM @sql_bak_role_menu; +EXECUTE stmt_bak_role_menu; +DEALLOCATE PREPARE stmt_bak_role_menu; + +SELECT + CONCAT('sys_menu_bak_', @bak_suffix) AS menu_backup_table, + CONCAT('sys_role_menu_bak_', @bak_suffix) AS role_menu_backup_table; + +-- ------------------------------------------------------------ +-- 1) 解析父菜单:优先挂在「换班级」列表菜单下,否则挂在「换班级」按钮同级父菜单 +-- ------------------------------------------------------------ +SET @parent_id = ( + SELECT parent_id + FROM sys_menu + WHERE perms = 'studentMsg:studentClassChange:list' + AND menu_type IN ('C', 'M') + ORDER BY menu_id + LIMIT 1 +); + +-- list 本身若是目录/菜单,直接用其 menu_id 作父级 +SET @parent_id = IFNULL( + ( + SELECT menu_id + FROM sys_menu + WHERE perms = 'studentMsg:studentClassChange:list' + AND menu_type IN ('C', 'M') + ORDER BY menu_id + LIMIT 1 + ), + @parent_id +); + +-- 仍找不到时,退化为 class:change 按钮的父菜单 +SET @parent_id = IFNULL( + @parent_id, + ( + SELECT parent_id + FROM sys_menu + WHERE perms = 'studentMsg:class:change' + ORDER BY menu_id + LIMIT 1 + ) +); + +-- 最终兜底:学籍异动信息菜单(与学籍异动修复脚本同父级,仅当上面都找不到时) +SET @parent_id = IFNULL(@parent_id, 2004197349643042818); + +SELECT @parent_id AS resolved_parent_id; + +-- ------------------------------------------------------------ +-- 2) 新增「换班级-新增」按钮权限(若不存在) +-- ------------------------------------------------------------ +INSERT INTO sys_menu ( + menu_id, menu_name, parent_id, order_num, path, component, query_param, + is_frame, is_cache, menu_type, visible, status, perms, icon, + create_dept, create_by, create_time, update_by, update_time, remark +) +SELECT + 2005461500000000002, + '新增', + @parent_id, + 1, + '', + NULL, + NULL, + 1, + 0, + 'F', + '0', + '0', + 'studentMsg:studentClassChange:add', + '', + 100, + 1, + NOW(), + 1, + NOW(), + '补齐换班级提交接口权限,解决学生信息页换班级提交403' +FROM DUAL +WHERE NOT EXISTS ( + SELECT 1 FROM sys_menu WHERE perms = 'studentMsg:studentClassChange:add' +); + +-- ------------------------------------------------------------ +-- 3) 给「学生信息管理(保留)」角色(3003)授权 +-- ------------------------------------------------------------ +INSERT INTO sys_role_menu (role_id, menu_id) +SELECT 3003, m.menu_id +FROM sys_menu m +WHERE m.perms = 'studentMsg:studentClassChange:add' + AND NOT EXISTS ( + SELECT 1 FROM sys_role_menu rm + WHERE rm.role_id = 3003 AND rm.menu_id = m.menu_id + ); + +-- ------------------------------------------------------------ +-- 4) 兼容:所有已有「换班级」按钮权限的角色,一并补上 add +-- ------------------------------------------------------------ +INSERT INTO sys_role_menu (role_id, menu_id) +SELECT DISTINCT rm.role_id, add_menu.menu_id +FROM sys_role_menu rm +JOIN sys_menu btn ON btn.menu_id = rm.menu_id AND btn.perms = 'studentMsg:class:change' +JOIN sys_menu add_menu ON add_menu.perms = 'studentMsg:studentClassChange:add' +WHERE NOT EXISTS ( + SELECT 1 FROM sys_role_menu x + WHERE x.role_id = rm.role_id AND x.menu_id = add_menu.menu_id +); + +-- ------------------------------------------------------------ +-- 5) 校验:朱茏是否已具备 add 权限 +-- ------------------------------------------------------------ +SELECT + u.user_id, + u.user_name, + u.nick_name, + r.role_id, + r.role_name, + m.perms, + m.menu_name, + m.menu_id +FROM sys_user u +JOIN sys_user_role ur ON ur.user_id = u.user_id +JOIN sys_role r ON r.role_id = ur.role_id +JOIN sys_role_menu rm ON rm.role_id = r.role_id +JOIN sys_menu m ON m.menu_id = rm.menu_id +WHERE u.user_id = 223 + AND m.perms IN ( + 'studentMsg:class:change', + 'studentMsg:studentClassChange:add', + 'studentMsg:studentClassChange:list' + ) +ORDER BY m.perms, r.role_id; + +SELECT + 'studentMsg:studentClassChange:add' AS perms, + EXISTS ( + SELECT 1 + FROM sys_user_role ur + JOIN sys_role_menu rm ON rm.role_id = ur.role_id + JOIN sys_menu m ON m.menu_id = rm.menu_id + WHERE ur.user_id = 223 AND m.perms = 'studentMsg:studentClassChange:add' + ) AS has_perm; + +-- ------------------------------------------------------------ +-- 回滚示例(按需执行,把后缀换成上面查出的备份表名) +-- ------------------------------------------------------------ +-- TRUNCATE TABLE sys_role_menu; +-- INSERT INTO sys_role_menu SELECT * FROM sys_role_menu_bak_YYYYMMDD_HHMMSS; +-- DELETE FROM sys_menu WHERE perms = 'studentMsg:studentClassChange:add'; +-- -- 或整表回滚菜单: +-- -- TRUNCATE TABLE sys_menu; +-- -- INSERT INTO sys_menu SELECT * FROM sys_menu_bak_YYYYMMDD_HHMMSS; diff --git a/docs/设计文档/database/hzdyjs_xg0820.sql b/docs/设计文档/database/hzdyjs_xg0820.sql index 3f06cc0c..5acab57f 100644 --- a/docs/设计文档/database/hzdyjs_xg0820.sql +++ b/docs/设计文档/database/hzdyjs_xg0820.sql @@ -4622,7 +4622,7 @@ CREATE TABLE `xg_holiday_stay` ( `apply_end_date` datetime NOT NULL COMMENT '申请留宿结束日期', `stay_reason` text NOT NULL COMMENT '留宿原因(必填)', `stay_reason_type` varchar(20) DEFAULT NULL COMMENT '留宿原因类型(学习、实习、家庭困难、其他)', - `student_phone` varchar(20) NOT NULL COMMENT '学生联系电话', + `student_phone` varchar(20) DEFAULT NULL COMMENT '学生联系电话', `emergency_contact` varchar(50) NOT NULL COMMENT '紧急联系人姓名', `emergency_phone` varchar(20) NOT NULL COMMENT '紧急联系人电话', `parent_consent` char(1) NOT NULL DEFAULT '1' COMMENT '家长同意(0否 1是)', diff --git a/docs/设计文档/database/假期留宿_student_phone允许为空.sql b/docs/设计文档/database/假期留宿_student_phone允许为空.sql new file mode 100644 index 00000000..83eb5c6b --- /dev/null +++ b/docs/设计文档/database/假期留宿_student_phone允许为空.sql @@ -0,0 +1,23 @@ +-- ============================================================ +-- 假期留宿:student_phone 允许为空 +-- 背景:学生档案无手机号时,申请 INSERT 省略 student_phone, +-- 原 NOT NULL 无默认值会报 Field 'student_phone' doesn't have a default value +-- 目的:仅改库即可恢复申请,无需立即发版 +-- 日期:2026-10-15 +-- ============================================================ + +-- 主表:允许联系电话为空 +ALTER TABLE `xg_holiday_stay` + MODIFY COLUMN `student_phone` varchar(20) DEFAULT NULL COMMENT '学生联系电话'; + +-- 如存在历史表,一并放宽(没有可跳过) +-- ALTER TABLE `xg_holiday_stay_his` +-- MODIFY COLUMN `student_phone` varchar(20) DEFAULT NULL COMMENT '学生联系电话'; + +-- ------------------------------------------------------------ +-- 可选预防:紧急联系人/电话同样是 NOT NULL,表单空串一般能写入; +-- 若希望彻底避免同类 SQL 报错,可一并执行: +-- ALTER TABLE `xg_holiday_stay` +-- MODIFY COLUMN `emergency_contact` varchar(50) DEFAULT NULL COMMENT '紧急联系人姓名', +-- MODIFY COLUMN `emergency_phone` varchar(20) DEFAULT NULL COMMENT '紧急联系人电话'; +-- ------------------------------------------------------------ diff --git a/mobile/src/pages/xg/xsjcxx/xsqj/cancelLeaveEdit/index.vue b/mobile/src/pages/xg/xsjcxx/xsqj/cancelLeaveEdit/index.vue index 7a70f160..029faa86 100644 --- a/mobile/src/pages/xg/xsjcxx/xsqj/cancelLeaveEdit/index.vue +++ b/mobile/src/pages/xg/xsjcxx/xsqj/cancelLeaveEdit/index.vue @@ -1420,6 +1420,18 @@ export default { } catch (_) {} }, + // 请假单上的 0/1/2/3 不是流程状态,不能随销假保存回写 + resolveSubmitCancelStatus(action) { + if (action === 'draft') { + return 'draft'; + } + const current = this.cancelForm.status; + if (!current || ['0', '1', '2', '3'].includes(String(current))) { + return 'draft'; + } + return current; + }, + // ============== 审批与提交 ============== async handleApprovalVerifyOpen() { this.$refs.submitVerifyRef.openDialog(this.routeParams.taskId || ''); @@ -1548,7 +1560,7 @@ export default { cancelLocation: this.cancelForm.cancelLocation, cancelRemark: this.cancelForm.cancelRemark, cancelPhotoOssIds: this.cancelForm.cancelPhotoOssIds, - cancelStatus: status === 'draft' ? 'draft' : this.cancelForm.status || 'draft', + cancelStatus: this.resolveSubmitCancelStatus(status), }; try { diff --git a/mobile/src/pages/xgss/jqzs/apply.vue b/mobile/src/pages/xgss/jqzs/apply.vue index 116ac547..d6d71eec 100644 --- a/mobile/src/pages/xgss/jqzs/apply.vue +++ b/mobile/src/pages/xgss/jqzs/apply.vue @@ -79,13 +79,13 @@ count > - + - + - + @@ -308,6 +308,15 @@ export default { if (!this.form.stayReason || !this.form.stayReason.trim()) { return uni.showToast({ title: '请填写留宿原因', icon: 'none' }); } + if (!this.form.studentPhone || !String(this.form.studentPhone).trim()) { + return uni.showToast({ title: '请填写联系电话', icon: 'none' }); + } + if (!this.form.emergencyContact || !String(this.form.emergencyContact).trim()) { + return uni.showToast({ title: '请填写紧急联系人', icon: 'none' }); + } + if (!this.form.emergencyPhone || !String(this.form.emergencyPhone).trim()) { + return uni.showToast({ title: '请填写紧急联系电话', icon: 'none' }); + } this.submitting = true; try { await applyHolidayStay({ ...this.form }); diff --git a/org/dromara/warm/flow/core/listener/ListenerVariable.java b/org/dromara/warm/flow/core/listener/ListenerVariable.java new file mode 100644 index 00000000..8855d940 --- /dev/null +++ b/org/dromara/warm/flow/core/listener/ListenerVariable.java @@ -0,0 +1,213 @@ +/* + * Copyright 2024-2025, Warm-Flow (290631660@qq.com). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.dromara.warm.flow.core.listener; + +import org.dromara.warm.flow.core.dto.FlowParams; +import org.dromara.warm.flow.core.entity.Definition; +import org.dromara.warm.flow.core.entity.Instance; +import org.dromara.warm.flow.core.entity.Node; +import org.dromara.warm.flow.core.entity.Task; + +import java.util.List; +import java.util.Map; + +/** + * 监听器变量 + * + * @author warm + * @see 文档地址 + */ +public class ListenerVariable { + + /** + * 流程定义 + */ + private Definition definition; + + /** + * 流程实例 + */ + private Instance instance; + + /** + * 监听器对应的节点 + */ + private Node node; + + /** + * 当前任务 + */ + private Task task; + + /** + * 下一次执行的节点集合 + */ + private List nextNodes; + + /** + * 新创建任务集合 + */ + private List nextTasks; + + /** + * 流程变量 + */ + private Map variable; + + /** + * 工作流内置参数 + */ + private FlowParams flowParams; + + + public ListenerVariable() { + } + + public ListenerVariable(Definition definition, Instance instance, Map variable) { + this.definition = definition; + this.instance = instance; + this.variable = variable; + } + + public ListenerVariable(Definition definition, Instance instance, Node node, Map variable) { + this.definition = definition; + this.instance = instance; + this.node = node; + this.variable = variable; + } + + public ListenerVariable(Definition definition, Instance instance, Map variable, Task task) { + this.definition = definition; + this.instance = instance; + this.variable = variable; + this.task = task; + } + + public ListenerVariable(Definition definition, Instance instance, Node node, Map variable, Task task) { + this.definition = definition; + this.instance = instance; + this.node = node; + this.variable = variable; + this.task = task; + } + + public ListenerVariable(Definition definition, Instance instance, Node node, Map variable, Task task, List nextNodes) { + this.definition = definition; + this.instance = instance; + this.node = node; + this.variable = variable; + this.task = task; + this.nextNodes = nextNodes; + } + + public ListenerVariable(Definition definition, Instance instance, Node node , Map variable, Task task + , List nextNodes, List nextTasks) { + this.definition = definition; + this.instance = instance; + this.node = node; + this.variable = variable; + this.task = task; + this.nextNodes = nextNodes; + this.nextTasks = nextTasks; + } + + public Definition getDefinition() { + return definition; + } + + public ListenerVariable setDefinition(Definition definition) { + this.definition = definition; + return this; + } + + public Instance getInstance() { + return instance; + } + + public ListenerVariable setInstance(Instance instance) { + this.instance = instance; + return this; + } + + public Node getNode() { + return node; + } + + public ListenerVariable setNode(Node node) { + this.node = node; + return this; + } + + public Task getTask() { + return task; + } + + public ListenerVariable setTask(Task task) { + this.task = task; + return this; + } + + public List getNextNodes() { + return nextNodes; + } + + public ListenerVariable setNextNodes(List nextNodes) { + this.nextNodes = nextNodes; + return this; + } + + public List getNextTasks() { + return nextTasks; + } + + public ListenerVariable setNextTasks(List nextTasks) { + this.nextTasks = nextTasks; + return this; + } + + public Map getVariable() { + return variable; + } + + public ListenerVariable setVariable(Map variable) { + this.variable = variable; + return this; + } + + public FlowParams getFlowParams() { + return flowParams; + } + + public ListenerVariable setFlowParams(FlowParams flowParams) { + this.flowParams = flowParams; + return this; + } + + + @Override + public String toString() { + return "ListenerVariable{" + + "definition=" + definition + + ", instance=" + instance + + ", node=" + node + + ", task=" + task + + ", nextNodes=" + nextNodes + + ", nextTasks=" + nextTasks + + ", variable=" + variable + + ", flowParams=" + flowParams + + '}'; + } +} diff --git a/web/src/views/xg/xgss/holidayStay/holidayStay/index-sg.vue b/web/src/views/xg/xgss/holidayStay/holidayStay/index-sg.vue index 071ffed4..dc71e42b 100644 --- a/web/src/views/xg/xgss/holidayStay/holidayStay/index-sg.vue +++ b/web/src/views/xg/xgss/holidayStay/holidayStay/index-sg.vue @@ -939,6 +939,9 @@ const data = reactive>({ studentId: [{ required: true, message: '学生ID', trigger: 'blur' }], studentName: [{ required: true, message: '学生姓名不能为空', trigger: 'blur' }], studentNo: [{ required: true, message: '学号不能为空', trigger: 'blur' }], + studentPhone: [{ required: true, message: '学生联系电话不能为空', trigger: 'blur' }], + emergencyContact: [{ required: true, message: '紧急联系人不能为空', trigger: 'blur' }], + emergencyPhone: [{ required: true, message: '紧急联系电话不能为空', trigger: 'blur' }], applyStartDate: [{ required: true, message: '留宿开始日期不能为空', trigger: 'blur' }], applyEndDate: [{ required: true, message: '留宿结束日期不能为空', trigger: 'blur' }] } diff --git a/web/src/views/xg/xgss/holidayStay/holidayStay/index.vue b/web/src/views/xg/xgss/holidayStay/holidayStay/index.vue index 85d02efb..5fdafdbd 100644 --- a/web/src/views/xg/xgss/holidayStay/holidayStay/index.vue +++ b/web/src/views/xg/xgss/holidayStay/holidayStay/index.vue @@ -770,6 +770,9 @@ const data = reactive>({ studentId: [{ required: true, message: '学生ID', trigger: 'blur' }], studentName: [{ required: true, message: '学生姓名不能为空', trigger: 'blur' }], studentNo: [{ required: true, message: '学号不能为空', trigger: 'blur' }], + studentPhone: [{ required: true, message: '学生联系电话不能为空', trigger: 'blur' }], + emergencyContact: [{ required: true, message: '紧急联系人不能为空', trigger: 'blur' }], + emergencyPhone: [{ required: true, message: '紧急联系电话不能为空', trigger: 'blur' }], applyStartDate: [{ required: true, message: '留宿开始日期不能为空', trigger: 'blur' }], applyEndDate: [{ required: true, message: '留宿结束日期不能为空', trigger: 'blur' }] }