提交
This commit is contained in:
1 parent
a0bb39d069
commit
6034b215e5
19 files changed
+1396
-10
No files matched your search
@@ -632,6 +632,12 @@ const handleStartWorkFlow = async (businessData: any) => {
|
|||||||
// - 退回 (backProcess)
|
// - 退回 (backProcess)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**🔴 退回残留任务清理(强制性,2026-10-15)**:
|
||||||
|
|
||||||
|
- `backProcess` 在 `skip(REJECT)` 后必须清理同实例下非目标节点残留 `flow_task`/`flow_user`(`cleanResidualTasksAfterBack`)
|
||||||
|
- 草稿/撤销/退回重新提交时,`startWorkFlow` 需自愈多任务脏数据并按实例状态解析正确 taskId(`healResidualTasksOnRestart`)
|
||||||
|
- 背景:退回后若残留原审批节点 + draft 申请人任务,移动端再提交会报「启动工作流失败」
|
||||||
|
|
||||||
### 3. 流程撤销
|
### 3. 流程撤销
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
|
|||||||
+140
-2
@@ -112,12 +112,18 @@ public class FlwTaskServiceImpl implements IFlwTaskService {
|
|||||||
.eq(FlowInstance::getBusinessId, businessId));
|
.eq(FlowInstance::getBusinessId, businessId));
|
||||||
if (ObjectUtil.isNotNull(flowInstance)) {
|
if (ObjectUtil.isNotNull(flowInstance)) {
|
||||||
BusinessStatusEnum.checkStartStatus(flowInstance.getFlowStatus());
|
BusinessStatusEnum.checkStartStatus(flowInstance.getFlowStatus());
|
||||||
|
// 草稿/撤销/退回重新提交前,清理历史残留待办,避免取到错误 taskId
|
||||||
|
healResidualTasksOnRestart(flowInstance);
|
||||||
List<Task> taskList = taskService.list(new FlowTask().setInstanceId(flowInstance.getId()));
|
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);
|
taskService.mergeVariable(flowInstance, variables);
|
||||||
insService.updateById(flowInstance);
|
insService.updateById(flowInstance);
|
||||||
StartProcessReturnDTO dto = new StartProcessReturnDTO();
|
StartProcessReturnDTO dto = new StartProcessReturnDTO();
|
||||||
dto.setProcessInstanceId(taskList.get(0).getInstanceId());
|
dto.setProcessInstanceId(currentTask.getInstanceId());
|
||||||
dto.setTaskId(taskList.get(0).getId());
|
dto.setTaskId(currentTask.getId());
|
||||||
return dto;
|
return dto;
|
||||||
}
|
}
|
||||||
FlowParams flowParams = FlowParams.build()
|
FlowParams flowParams = FlowParams.build()
|
||||||
@@ -421,6 +427,9 @@ public class FlwTaskServiceImpl implements IFlwTaskService {
|
|||||||
.hisTaskExt(bo.getFileId());
|
.hisTaskExt(bo.getFileId());
|
||||||
taskService.skip(task.getId(), flowParams);
|
taskService.skip(task.getId(), flowParams);
|
||||||
|
|
||||||
|
// 退回后清理残留待办(原节点未删干净、历史 draft 申请人任务等)
|
||||||
|
cleanResidualTasksAfterBack(task.getInstanceId(), bo.getNodeCode());
|
||||||
|
|
||||||
// 保存手写签名到历史任务表(驳回场景)
|
// 保存手写签名到历史任务表(驳回场景)
|
||||||
if (StringUtils.isNotBlank(bo.getSignature())) {
|
if (StringUtils.isNotBlank(bo.getSignature())) {
|
||||||
flwHisTaskExtMapper.updateSignatureByTaskId(taskId, 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);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取可驳回的前置节点
|
* 获取可驳回的前置节点
|
||||||
*
|
*
|
||||||
|
|||||||
+22
-2
@@ -245,7 +245,16 @@ public class XgHolidayStayServiceImpl implements IXgHolidayStayService {
|
|||||||
* 保存前的数据校验
|
* 保存前的数据校验
|
||||||
*/
|
*/
|
||||||
private void validEntityBeforeSave(XgHolidayStay entity) {
|
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) {
|
if (bo.getBedId() == null || bo.getRoomId() == null) {
|
||||||
throw new ServiceException("未查询到当前在住宿舍/床位信息,请先完成床位分配后再申请留宿");
|
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.setApplyStatus("1");
|
||||||
bo.setApplyTime(new Date());
|
bo.setApplyTime(new Date());
|
||||||
bo.setAuditorId(null);
|
bo.setAuditorId(null);
|
||||||
@@ -903,7 +922,8 @@ public class XgHolidayStayServiceImpl implements IXgHolidayStayService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
bo.setCollegeName(student.getCollegeName());
|
bo.setCollegeName(student.getCollegeName());
|
||||||
if (StringUtils.isBlank(bo.getStudentPhone())) {
|
// 仅在档案有电话时回填,避免把表单空串覆盖成 null 导致 INSERT 省略该字段
|
||||||
|
if (StringUtils.isBlank(bo.getStudentPhone()) && StringUtils.isNotBlank(student.getPhone())) {
|
||||||
bo.setStudentPhone(student.getPhone());
|
bo.setStudentPhone(student.getPhone());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+36
@@ -1,5 +1,6 @@
|
|||||||
package org.dromara.xg.service.impl;
|
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.MapstructUtils;
|
||||||
import org.dromara.common.core.utils.StringUtils;
|
import org.dromara.common.core.utils.StringUtils;
|
||||||
import org.dromara.common.mybatis.core.page.TableDataInfo;
|
import org.dromara.common.mybatis.core.page.TableDataInfo;
|
||||||
@@ -144,6 +145,7 @@ public class XgStudentCancelLeaveServiceImpl implements IXgStudentCancelLeaveSer
|
|||||||
@Override
|
@Override
|
||||||
@Transactional(rollbackFor = Exception.class)
|
@Transactional(rollbackFor = Exception.class)
|
||||||
public XgStudentCancelLeaveVo insertByBo(XgStudentCancelLeaveBo bo) {
|
public XgStudentCancelLeaveVo insertByBo(XgStudentCancelLeaveBo bo) {
|
||||||
|
bo.setCancelStatus(resolveCancelStatusForSave(null, bo.getCancelStatus()));
|
||||||
XgStudentCancelLeave add = MapstructUtils.convert(bo, XgStudentCancelLeave.class);
|
XgStudentCancelLeave add = MapstructUtils.convert(bo, XgStudentCancelLeave.class);
|
||||||
validEntityBeforeSave(add);
|
validEntityBeforeSave(add);
|
||||||
boolean flag = baseMapper.insert(add) > 0;
|
boolean flag = baseMapper.insert(add) > 0;
|
||||||
@@ -178,6 +180,12 @@ public class XgStudentCancelLeaveServiceImpl implements IXgStudentCancelLeaveSer
|
|||||||
@Override
|
@Override
|
||||||
@Transactional(rollbackFor = Exception.class)
|
@Transactional(rollbackFor = Exception.class)
|
||||||
public Boolean updateByBo(XgStudentCancelLeaveBo bo) {
|
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);
|
XgStudentCancelLeave update = MapstructUtils.convert(bo, XgStudentCancelLeave.class);
|
||||||
validEntityBeforeSave(update);
|
validEntityBeforeSave(update);
|
||||||
boolean flag = baseMapper.updateById(update) > 0;
|
boolean flag = baseMapper.updateById(update) > 0;
|
||||||
@@ -200,6 +208,34 @@ public class XgStudentCancelLeaveServiceImpl implements IXgStudentCancelLeaveSer
|
|||||||
return flag;
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 保存前的数据校验
|
* 保存前的数据校验
|
||||||
*/
|
*/
|
||||||
|
|||||||
+38
@@ -561,9 +561,47 @@ public class XgStudentLeaveServiceImpl implements IXgStudentLeaveService {
|
|||||||
*
|
*
|
||||||
* @param processTaskEvent 参数
|
* @param processTaskEvent 参数
|
||||||
*/
|
*/
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
@EventListener(condition = "#processTaskEvent.flowCode.startsWith('hzdyjs-xj')")
|
@EventListener(condition = "#processTaskEvent.flowCode.startsWith('hzdyjs-xj')")
|
||||||
public void processTaskHandler_xj(ProcessTaskEvent processTaskEvent) {
|
public void processTaskHandler_xj(ProcessTaskEvent processTaskEvent) {
|
||||||
log.info("当前任务创建了{}", processTaskEvent.toString());
|
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` 注解确保两个表的更新操作要么全部成功,要么全部回滚。
|
**事务保证**:使用 `@Transactional` 注解确保两个表的更新操作要么全部成功,要么全部回滚。
|
||||||
|
|
||||||
|
**状态保护**(2026-10-19):`cancelStatus` 在销假流程中使用 `draft`、`waiting`、`finish` 等流程状态。请假单上的 `0/1/2/3`(未销假、已销假、逾期未销假、无需销假)传入本接口时按 `draft` 保存,不会写入销假单。销假单已是 `waiting`、`back`、`cancel`、`finish`、`invalid`、`termination` 时,再次保存销假内容不会覆盖该状态。审批任务创建且流程为待审核时,若业务状态仍是旧字典值或 `draft`,会自动对齐为 `waiting`。
|
||||||
|
|
||||||
### 5. 删除销假
|
### 5. 删除销假
|
||||||
|
|
||||||
- **接口路径**:`DELETE /studentCancelLeave/studentCancelLeave/{ids}`
|
- **接口路径**:`DELETE /studentCancelLeave/studentCancelLeave/{ids}`
|
||||||
|
|||||||
@@ -1045,12 +1045,22 @@ public class TestLeaveController {
|
|||||||
1. 流程定义未发布
|
1. 流程定义未发布
|
||||||
2. 流程编码不存在
|
2. 流程编码不存在
|
||||||
3. 业务数据状态不正确
|
3. 业务数据状态不正确
|
||||||
|
4. **退回后残留多个 `flow_task`**(原审批节点未清、历史 draft 申请人任务),重新提交时取到错误 taskId,移动端统一提示「启动工作流失败」
|
||||||
|
|
||||||
**解决**:
|
**解决**:
|
||||||
|
|
||||||
1. 检查流程定义是否已发布
|
1. 检查流程定义是否已发布
|
||||||
2. 确认 `flowCode` 参数正确
|
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 日期格式问题
|
### 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;
|
||||||
@@ -4622,7 +4622,7 @@ CREATE TABLE `xg_holiday_stay` (
|
|||||||
`apply_end_date` datetime NOT NULL COMMENT '申请留宿结束日期',
|
`apply_end_date` datetime NOT NULL COMMENT '申请留宿结束日期',
|
||||||
`stay_reason` text NOT NULL COMMENT '留宿原因(必填)',
|
`stay_reason` text NOT NULL COMMENT '留宿原因(必填)',
|
||||||
`stay_reason_type` varchar(20) DEFAULT 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_contact` varchar(50) NOT NULL COMMENT '紧急联系人姓名',
|
||||||
`emergency_phone` varchar(20) NOT NULL COMMENT '紧急联系人电话',
|
`emergency_phone` varchar(20) NOT NULL COMMENT '紧急联系人电话',
|
||||||
`parent_consent` char(1) NOT NULL DEFAULT '1' COMMENT '家长同意(0否 1是)',
|
`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 (_) {}
|
} 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() {
|
async handleApprovalVerifyOpen() {
|
||||||
this.$refs.submitVerifyRef.openDialog(this.routeParams.taskId || '');
|
this.$refs.submitVerifyRef.openDialog(this.routeParams.taskId || '');
|
||||||
@@ -1548,7 +1560,7 @@ export default {
|
|||||||
cancelLocation: this.cancelForm.cancelLocation,
|
cancelLocation: this.cancelForm.cancelLocation,
|
||||||
cancelRemark: this.cancelForm.cancelRemark,
|
cancelRemark: this.cancelForm.cancelRemark,
|
||||||
cancelPhotoOssIds: this.cancelForm.cancelPhotoOssIds,
|
cancelPhotoOssIds: this.cancelForm.cancelPhotoOssIds,
|
||||||
cancelStatus: status === 'draft' ? 'draft' : this.cancelForm.status || 'draft',
|
cancelStatus: this.resolveSubmitCancelStatus(status),
|
||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -79,13 +79,13 @@
|
|||||||
count
|
count
|
||||||
></u-textarea>
|
></u-textarea>
|
||||||
</u-form-item>
|
</u-form-item>
|
||||||
<u-form-item label="联系电话" prop="studentPhone">
|
<u-form-item label="联系电话" prop="studentPhone" required>
|
||||||
<u-input v-model="form.studentPhone" placeholder="可自动带出,可修改" />
|
<u-input v-model="form.studentPhone" placeholder="可自动带出,可修改" />
|
||||||
</u-form-item>
|
</u-form-item>
|
||||||
<u-form-item label="紧急联系人" prop="emergencyContact">
|
<u-form-item label="紧急联系人" prop="emergencyContact" required>
|
||||||
<u-input v-model="form.emergencyContact" placeholder="请输入紧急联系人" />
|
<u-input v-model="form.emergencyContact" placeholder="请输入紧急联系人" />
|
||||||
</u-form-item>
|
</u-form-item>
|
||||||
<u-form-item label="紧急电话" prop="emergencyPhone">
|
<u-form-item label="紧急电话" prop="emergencyPhone" required>
|
||||||
<u-input v-model="form.emergencyPhone" placeholder="请输入紧急联系电话" />
|
<u-input v-model="form.emergencyPhone" placeholder="请输入紧急联系电话" />
|
||||||
</u-form-item>
|
</u-form-item>
|
||||||
<u-form-item label="家长同意" prop="parentConsent">
|
<u-form-item label="家长同意" prop="parentConsent">
|
||||||
@@ -308,6 +308,15 @@ export default {
|
|||||||
if (!this.form.stayReason || !this.form.stayReason.trim()) {
|
if (!this.form.stayReason || !this.form.stayReason.trim()) {
|
||||||
return uni.showToast({ title: '请填写留宿原因', icon: 'none' });
|
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;
|
this.submitting = true;
|
||||||
try {
|
try {
|
||||||
await applyHolidayStay({ ...this.form });
|
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' }],
|
studentId: [{ required: true, message: '学生ID', trigger: 'blur' }],
|
||||||
studentName: [{ required: true, message: '学生姓名不能为空', trigger: 'blur' }],
|
studentName: [{ required: true, message: '学生姓名不能为空', trigger: 'blur' }],
|
||||||
studentNo: [{ 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' }],
|
applyStartDate: [{ required: true, message: '留宿开始日期不能为空', trigger: 'blur' }],
|
||||||
applyEndDate: [{ 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' }],
|
studentId: [{ required: true, message: '学生ID', trigger: 'blur' }],
|
||||||
studentName: [{ required: true, message: '学生姓名不能为空', trigger: 'blur' }],
|
studentName: [{ required: true, message: '学生姓名不能为空', trigger: 'blur' }],
|
||||||
studentNo: [{ 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' }],
|
applyStartDate: [{ required: true, message: '留宿开始日期不能为空', trigger: 'blur' }],
|
||||||
applyEndDate: [{ required: true, message: '留宿结束日期不能为空', trigger: 'blur' }]
|
applyEndDate: [{ required: true, message: '留宿结束日期不能为空', trigger: 'blur' }]
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user