This commit is contained in:
zlwu committed 2026-10-20 11:50:53 +08:00
1 parent a0bb39d069
commit 6034b215e5
19 files changed
+1396 -10

No files matched your search

+6
View File
@@ -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
@@ -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<Task> 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。
* <p>
* warm-flow skip(REJECT) 后偶发残留原审批节点任务、以及启动时留下的 draft 申请人任务,
* 导致学生重新提交时 startWorkFlow 取到错误 taskId,移动端提示「启动工作流失败」。
*
* @param instanceId 流程实例ID
* @param targetNodeCode 退回目标节点编码
*/
private void cleanResidualTasksAfterBack(Long instanceId, String targetNodeCode) {
List<FlowTask> tasks = selectByInstId(instanceId);
if (CollUtil.isEmpty(tasks) || tasks.size() == 1) {
return;
}
if (StringUtils.isBlank(targetNodeCode)) {
log.warn("退回清理跳过:目标节点编码为空, instanceId={}", instanceId);
return;
}
Comparator<FlowTask> newestFirst = Comparator
.comparing(FlowTask::getCreateTime, Comparator.nullsLast(Comparator.reverseOrder()))
.thenComparing(FlowTask::getId, Comparator.nullsLast(Comparator.reverseOrder()));
List<FlowTask> 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<Long> 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<FlowTask> tasks = selectByInstId(flowInstance.getId());
if (CollUtil.isEmpty(tasks) || tasks.size() <= 1) {
return;
}
String status = flowInstance.getFlowStatus();
String applyNodeCode = flwCommonService.applyNodeCode(flowInstance.getDefinitionId());
Comparator<FlowTask> 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<Long> 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<Task> taskList, FlowInstance flowInstance) {
if (taskList.size() == 1) {
return taskList.get(0);
}
String status = flowInstance.getFlowStatus();
List<Task> 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<Long> taskIds) {
if (CollUtil.isEmpty(taskIds)) {
return;
}
FlowEngine.userService().deleteByTaskIds(taskIds);
flowTaskMapper.deleteByIds(taskIds);
}
/**
* 获取可驳回的前置节点
*
@@ -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());
}
}
@@ -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);
}
/**
* 保存前的数据校验
*/
@@ -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);
}
/**
@@ -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`(待审批)
@@ -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}`
@@ -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 日期格式问题
@@ -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 <host> -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
@@ -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;
@@ -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;
@@ -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;
+1 -1
View File
@@ -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是)',
@@ -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 '紧急联系人电话';
-- ------------------------------------------------------------
@@ -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 {
+12 -3
View File
@@ -79,13 +79,13 @@
count
></u-textarea>
</u-form-item>
<u-form-item label="联系电话" prop="studentPhone">
<u-form-item label="联系电话" prop="studentPhone" required>
<u-input v-model="form.studentPhone" placeholder="可自动带出,可修改" />
</u-form-item>
<u-form-item label="紧急联系人" prop="emergencyContact">
<u-form-item label="紧急联系人" prop="emergencyContact" required>
<u-input v-model="form.emergencyContact" placeholder="请输入紧急联系人" />
</u-form-item>
<u-form-item label="紧急电话" prop="emergencyPhone">
<u-form-item label="紧急电话" prop="emergencyPhone" required>
<u-input v-model="form.emergencyPhone" placeholder="请输入紧急联系电话" />
</u-form-item>
<u-form-item label="家长同意" prop="parentConsent">
@@ -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 });
@@ -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 <a href="https://warm-flow.dromara.org/master/advanced/listener.html">文档地址</a>
*/
public class ListenerVariable {
/**
* 流程定义
*/
private Definition definition;
/**
* 流程实例
*/
private Instance instance;
/**
* 监听器对应的节点
*/
private Node node;
/**
* 当前任务
*/
private Task task;
/**
* 下一次执行的节点集合
*/
private List<Node> nextNodes;
/**
* 新创建任务集合
*/
private List<Task> nextTasks;
/**
* 流程变量
*/
private Map<String, Object> variable;
/**
* 工作流内置参数
*/
private FlowParams flowParams;
public ListenerVariable() {
}
public ListenerVariable(Definition definition, Instance instance, Map<String, Object> variable) {
this.definition = definition;
this.instance = instance;
this.variable = variable;
}
public ListenerVariable(Definition definition, Instance instance, Node node, Map<String, Object> variable) {
this.definition = definition;
this.instance = instance;
this.node = node;
this.variable = variable;
}
public ListenerVariable(Definition definition, Instance instance, Map<String, Object> variable, Task task) {
this.definition = definition;
this.instance = instance;
this.variable = variable;
this.task = task;
}
public ListenerVariable(Definition definition, Instance instance, Node node, Map<String, Object> 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<String, Object> variable, Task task, List<Node> 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<String, Object> variable, Task task
, List<Node> nextNodes, List<Task> 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<Node> getNextNodes() {
return nextNodes;
}
public ListenerVariable setNextNodes(List<Node> nextNodes) {
this.nextNodes = nextNodes;
return this;
}
public List<Task> getNextTasks() {
return nextTasks;
}
public ListenerVariable setNextTasks(List<Task> nextTasks) {
this.nextTasks = nextTasks;
return this;
}
public Map<String, Object> getVariable() {
return variable;
}
public ListenerVariable setVariable(Map<String, Object> 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 +
'}';
}
}
@@ -939,6 +939,9 @@ const data = reactive<PageData<HolidayStayForm, HolidayStayQuery>>({
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' }]
}
@@ -770,6 +770,9 @@ const data = reactive<PageData<HolidayStayForm, HolidayStayQuery>>({
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' }]
}