From 7cea0566accab7aaa48e976a213af04993c8af8b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9Cwuzhen=E2=80=9D?= Date: Tue, 20 Oct 2026 18:01:12 +0800 Subject: [PATCH] =?UTF-8?q?feat(xg):=20=E8=AF=B7=E5=81=87=E5=AE=A1?= =?UTF-8?q?=E6=89=B9=E4=BF=AE=E5=A4=8D=E3=80=81=E7=82=B9=E5=90=8D=E4=BB=8A?= =?UTF-8?q?=E6=97=A5=E6=B1=87=E6=80=BB=E4=B8=8E=E5=AF=9D=E5=AE=A4=E6=89=A3?= =?UTF-8?q?=E5=88=86=E6=9D=A1=E4=BE=8B=E8=84=9A=E6=9C=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 同步请假/销假审批上下文与状态、宿舍点名今日汇总与假期留宿标识,并补充寝室检查扣分条例生产 SQL 与执行说明。 Co-authored-by: Cursor --- .../controller/FlwInstanceController.java | 9 + .../domain/vo/FlowApprovalContextVo.java | 31 ++ .../workflow/service/IFlwInstanceService.java | 9 + .../service/impl/FlwInstanceServiceImpl.java | 53 ++++ .../controller/XgDormRollCallController.java | 11 + .../org/dromara/xg/domain/XgDormRollCall.java | 2 + .../xg/domain/XgDormRollCallDetail.java | 12 + .../xg/domain/bo/XgDormRollCallBo.java | 2 + .../xg/domain/bo/XgDormRollCallDetailBo.java | 8 + .../xg/domain/bo/XgStudentLeaveBo.java | 5 + .../vo/DormRollCallTodayDetailItemVo.java | 50 ++++ .../domain/vo/DormRollCallTodaySummaryVo.java | 39 +++ .../xg/domain/vo/RoomRollCallStatusVo.java | 2 + .../xg/domain/vo/XgDormRollCallDetailVo.java | 8 + .../xg/domain/vo/XgDormRollCallVo.java | 2 + .../xg/helper/LeaveFlowStatusSyncHelper.java | 136 +++++++++ .../xg/service/IXgDormRollCallService.java | 6 + .../xg/service/IXgHolidayStayService.java | 11 + .../impl/XgDormBedStudentServiceImpl.java | 50 ++-- .../impl/XgDormRollCallServiceImpl.java | 234 +++++++++++++++ .../impl/XgHolidayStayServiceImpl.java | 39 +++ .../impl/XgStudentLeaveServiceImpl.java | 140 +++++---- ..._20261020_dorm_hygiene_deduction_types.sql | 164 +++++++++++ ...e_20261020_dorm_roll_call_holiday_stay.sql | 32 +++ ..._20261020_fix_leave_cancel_status_sync.sql | 83 ++++++ docs/开发文档/API/宿舍点名假期留宿标识说明.md | 57 ++++ .../寝室检查扣分条例生产执行说明-20261020.md | 137 +++++++++ docs/开发文档/请假销假审批按钮修复说明.md | 20 ++ mobile/src/api/dormRollCall.js | 8 + .../src/components/Process/approvalButton.vue | 10 +- mobile/src/pages/task/taskWaiting.vue | 89 +++++- .../xg/xsjcxx/xsqj/cancelLeaveEdit/index.vue | 34 +++ .../src/pages/xg/xsjcxx/xsqj/leave/index.vue | 36 ++- .../pages/xg/xsjcxx/xsqj/leaveEdit/index.vue | 32 ++- .../ssdm/components/RoomRollCallPopup.vue | 51 +++- mobile/src/pages/xgss/ssdm/ssdm.vue | 272 +++++++++++++++++- mobile/src/pages/xgss/ssdm/ssdmCheck.vue | 5 + mobile/src/pages/xgss/ssdm/ssdmDetail.vue | 30 +- web/src/api/workflow/instance/index.ts | 8 + .../xgss/dormRollCall/dormRollCall/types.ts | 26 +- web/src/components/Process/approvalButton.vue | 9 +- .../dormRollCall/RollCallDetailDrawer.vue | 7 +- .../dormRollCall/RollCallHistoryDrawer.vue | 5 + .../dormRollCall/RoomRollCallPopup.vue | 36 ++- .../xgss/dormRollCall/dormRollCall/index.vue | 5 + .../dormRollCall/rollCallCheck.vue | 6 + web/src/views/xg/xsjcxx/xsqj/dialog.vue | 31 ++ web/src/views/xg/xsjcxx/xsqj/xjdialog.vue | 30 ++ 48 files changed, 1966 insertions(+), 116 deletions(-) create mode 100644 backend/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/domain/vo/FlowApprovalContextVo.java create mode 100644 backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/domain/vo/DormRollCallTodayDetailItemVo.java create mode 100644 backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/domain/vo/DormRollCallTodaySummaryVo.java create mode 100644 backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/helper/LeaveFlowStatusSyncHelper.java create mode 100644 backend/script/sql/update/update_20261020_dorm_hygiene_deduction_types.sql create mode 100644 backend/script/sql/update/update_20261020_dorm_roll_call_holiday_stay.sql create mode 100644 backend/script/sql/update/update_20261020_fix_leave_cancel_status_sync.sql create mode 100644 docs/开发文档/API/宿舍点名假期留宿标识说明.md create mode 100644 docs/开发文档/寝室检查扣分条例生产执行说明-20261020.md create mode 100644 docs/开发文档/请假销假审批按钮修复说明.md diff --git a/backend/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/controller/FlwInstanceController.java b/backend/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/controller/FlwInstanceController.java index 5817c8db..e64ebdbb 100644 --- a/backend/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/controller/FlwInstanceController.java +++ b/backend/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/controller/FlwInstanceController.java @@ -13,6 +13,7 @@ import org.dromara.workflow.common.ConditionalOnEnable; import org.dromara.workflow.domain.bo.FlowCancelBo; import org.dromara.workflow.domain.bo.FlowInstanceBo; import org.dromara.workflow.domain.bo.FlowInvalidBo; +import org.dromara.workflow.domain.vo.FlowApprovalContextVo; import org.dromara.workflow.domain.vo.FlowInstanceVo; import org.dromara.workflow.service.IFlwInstanceService; import org.springframework.validation.annotation.Validated; @@ -132,6 +133,14 @@ public class FlwInstanceController extends BaseController { return R.ok(flwInstanceService.flowHisTaskList(businessId)); } + /** + * 办理页审批上下文:当前用户是否可审批、taskId、流程状态建议 + */ + @GetMapping("/approvalContext/{businessId}") + public R approvalContext(@PathVariable("businessId") String businessId) { + return R.ok(flwInstanceService.approvalContext(businessId)); + } + /** * 获取流程变量 * diff --git a/backend/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/domain/vo/FlowApprovalContextVo.java b/backend/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/domain/vo/FlowApprovalContextVo.java new file mode 100644 index 00000000..f718d8f1 --- /dev/null +++ b/backend/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/domain/vo/FlowApprovalContextVo.java @@ -0,0 +1,31 @@ +package org.dromara.workflow.domain.vo; + +import lombok.Data; + +import java.io.Serial; +import java.io.Serializable; + +/** + * 业务办理页审批上下文:以流程引擎为准,判断当前用户是否可审批。 + */ +@Data +public class FlowApprovalContextVo implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + /** 流程实例状态 */ + private String flowStatus; + + /** 建议同步到业务表的状态(存在可办审批任务时为 waiting) */ + private String suggestedBusinessStatus; + + /** 当前用户是否可审批(存在本人 waiting 待办) */ + private Boolean canApprove; + + /** 当前用户待办任务ID */ + private Long taskId; + + /** 是否存在运行中待办 */ + private Boolean hasWaitingTask; +} diff --git a/backend/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/service/IFlwInstanceService.java b/backend/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/service/IFlwInstanceService.java index 01e5124b..02fa1427 100644 --- a/backend/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/service/IFlwInstanceService.java +++ b/backend/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/service/IFlwInstanceService.java @@ -6,6 +6,7 @@ import org.dromara.warm.flow.orm.entity.FlowInstance; import org.dromara.workflow.domain.bo.FlowCancelBo; import org.dromara.workflow.domain.bo.FlowInstanceBo; import org.dromara.workflow.domain.bo.FlowInvalidBo; +import org.dromara.workflow.domain.vo.FlowApprovalContextVo; import org.dromara.workflow.domain.vo.FlowInstanceVo; import java.util.List; @@ -109,6 +110,14 @@ public interface IFlwInstanceService { */ Map flowHisTaskList(String businessId); + /** + * 办理页审批上下文(是否可审批、taskId、流程状态) + * + * @param businessId 业务id + * @return 审批上下文;无实例时 canApprove=false + */ + FlowApprovalContextVo approvalContext(String businessId); + /** * 按照实例id更新状态 * diff --git a/backend/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/service/impl/FlwInstanceServiceImpl.java b/backend/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/service/impl/FlwInstanceServiceImpl.java index 86f6c25e..191d9b8b 100644 --- a/backend/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/service/impl/FlwInstanceServiceImpl.java +++ b/backend/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/service/impl/FlwInstanceServiceImpl.java @@ -38,6 +38,7 @@ import org.dromara.workflow.common.enums.TaskStatusEnum; import org.dromara.workflow.domain.bo.FlowCancelBo; import org.dromara.workflow.domain.bo.FlowInstanceBo; import org.dromara.workflow.domain.bo.FlowInvalidBo; +import org.dromara.workflow.domain.vo.FlowApprovalContextVo; import org.dromara.workflow.domain.vo.FlowHisTaskVo; import org.dromara.workflow.domain.vo.FlowInstanceVo; import org.dromara.workflow.handler.FlowProcessEventHandler; @@ -286,6 +287,58 @@ public class FlwInstanceServiceImpl implements IFlwInstanceService { * * @param businessId 业务id */ + @Override + public FlowApprovalContextVo approvalContext(String businessId) { + FlowApprovalContextVo vo = new FlowApprovalContextVo(); + vo.setCanApprove(false); + vo.setHasWaitingTask(false); + if (StringUtils.isBlank(businessId)) { + return vo; + } + FlowInstance flowInstance = this.selectInstByBusinessId(businessId); + if (ObjectUtil.isNull(flowInstance)) { + return vo; + } + vo.setFlowStatus(flowInstance.getFlowStatus()); + vo.setSuggestedBusinessStatus(flowInstance.getFlowStatus()); + + List flowTaskList = flwTaskService.selectByInstId(flowInstance.getId()); + if (CollUtil.isEmpty(flowTaskList)) { + return vo; + } + vo.setHasWaitingTask(true); + + // 实例处于 waiting 且有待办时,业务展示状态应对齐 waiting(覆盖 back/draft 残留) + if (BusinessStatusEnum.WAITING.getStatus().equals(flowInstance.getFlowStatus())) { + vo.setSuggestedBusinessStatus(BusinessStatusEnum.WAITING.getStatus()); + } + + String currentUserId = LoginHelper.getUserIdStr(); + if (StringUtils.isBlank(currentUserId)) { + return vo; + } + // 退回至申请人节点时,待办人也是申请人:此时不应显示「审批」,仅可重新提交 + if (BusinessStatusEnum.isDraftOrCancelOrBack(flowInstance.getFlowStatus())) { + return vo; + } + + for (FlowTask flowTask : flowTaskList) { + List users = flwTaskService.currentTaskAllUser(flowTask.getId()); + if (CollUtil.isEmpty(users)) { + continue; + } + boolean matched = users.stream() + .anyMatch(u -> u.getUserId() != null && currentUserId.equals(String.valueOf(u.getUserId()))); + if (matched) { + vo.setCanApprove(true); + vo.setTaskId(flowTask.getId()); + vo.setSuggestedBusinessStatus(BusinessStatusEnum.WAITING.getStatus()); + break; + } + } + return vo; + } + @Override public Map flowHisTaskList(String businessId) { FlowInstance flowInstance = this.selectInstByBusinessId(businessId); diff --git a/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/controller/XgDormRollCallController.java b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/controller/XgDormRollCallController.java index af51401f..36f27e11 100644 --- a/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/controller/XgDormRollCallController.java +++ b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/controller/XgDormRollCallController.java @@ -19,6 +19,7 @@ import org.dromara.common.log.enums.BusinessType; import org.dromara.common.excel.utils.ExcelUtil; import org.dromara.xg.domain.vo.XgDormRollCallVo; import org.dromara.xg.domain.vo.RoomRollCallStatusVo; +import org.dromara.xg.domain.vo.DormRollCallTodaySummaryVo; import org.dromara.xg.domain.bo.XgDormRollCallBo; import org.dromara.xg.service.IXgDormRollCallService; import org.dromara.common.mybatis.core.page.TableDataInfo; @@ -37,6 +38,16 @@ public class XgDormRollCallController extends BaseController { return xgDormRollCallService.queryPageList(bo, pageQuery); } + /** + * 当前账号当天点名汇总(已/未点名寝室 + 请假/留宿/异常明细) + * 须声明在 /{id} 之前,避免被路径变量吞掉 + */ + @SaCheckPermission("dormRollCall:dormRollCall:list1") + @GetMapping("/todaySummary") + public R todaySummary() { + return R.ok(xgDormRollCallService.queryTodaySummary()); + } + @SaCheckPermission("dormRollCall:dormRollCall:query") @GetMapping("/{id}") public R getInfo(@NotNull(message = "主键不能为空") @PathVariable("id") Long id) { diff --git a/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/domain/XgDormRollCall.java b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/domain/XgDormRollCall.java index 75ad6f11..239f2579 100644 --- a/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/domain/XgDormRollCall.java +++ b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/domain/XgDormRollCall.java @@ -54,6 +54,8 @@ public class XgDormRollCall extends TenantEntity { private Integer leaveCount; + private Integer holidayStayCount; + private Integer absentCount; private Integer emptyCount; diff --git a/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/domain/XgDormRollCallDetail.java b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/domain/XgDormRollCallDetail.java index be1157bd..5cc60d0d 100644 --- a/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/domain/XgDormRollCallDetail.java +++ b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/domain/XgDormRollCallDetail.java @@ -57,4 +57,16 @@ public class XgDormRollCallDetail extends TenantEntity { private Date leaveStartTime; private Date leaveEndTime; + + /** 关联假期留宿记录ID */ + private Long holidayStayId; + + /** 假期类型 */ + private String holidayType; + + /** 留宿开始日期 */ + private Date holidayStartDate; + + /** 留宿结束日期 */ + private Date holidayEndDate; } diff --git a/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/domain/bo/XgDormRollCallBo.java b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/domain/bo/XgDormRollCallBo.java index bad0a86f..1343aa88 100644 --- a/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/domain/bo/XgDormRollCallBo.java +++ b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/domain/bo/XgDormRollCallBo.java @@ -54,6 +54,8 @@ public class XgDormRollCallBo extends BaseEntity { private Integer leaveCount; + private Integer holidayStayCount; + private Integer absentCount; private Integer emptyCount; diff --git a/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/domain/bo/XgDormRollCallDetailBo.java b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/domain/bo/XgDormRollCallDetailBo.java index 1364e86d..e096cec0 100644 --- a/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/domain/bo/XgDormRollCallDetailBo.java +++ b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/domain/bo/XgDormRollCallDetailBo.java @@ -39,4 +39,12 @@ public class XgDormRollCallDetailBo { private Date leaveStartTime; private Date leaveEndTime; + + private Long holidayStayId; + + private String holidayType; + + private Date holidayStartDate; + + private Date holidayEndDate; } diff --git a/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/domain/bo/XgStudentLeaveBo.java b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/domain/bo/XgStudentLeaveBo.java index a301f476..f61b01c7 100644 --- a/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/domain/bo/XgStudentLeaveBo.java +++ b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/domain/bo/XgStudentLeaveBo.java @@ -181,6 +181,11 @@ public class XgStudentLeaveBo extends BaseEntity { @NotBlank(message = "销假状态:0-未销假,1-已销假,2-逾期未销假 3-无需销假 不能为空", groups = { AddGroup.class, EditGroup.class }) private String cancelStatus; + /** + * 查询专用:是否排除已完成销假审批的记录(finish / 旧值 1)。非表字段,仅列表查询使用。 + */ + private Boolean excludeFinishedCancel; + /** * 销假时间 */ diff --git a/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/domain/vo/DormRollCallTodayDetailItemVo.java b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/domain/vo/DormRollCallTodayDetailItemVo.java new file mode 100644 index 00000000..930a6276 --- /dev/null +++ b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/domain/vo/DormRollCallTodayDetailItemVo.java @@ -0,0 +1,50 @@ +package org.dromara.xg.domain.vo; + +import lombok.Data; + +import java.io.Serial; +import java.io.Serializable; +import java.util.Date; + +/** + * 当天点名汇总明细项(请假 / 假期留宿 / 异常) + */ +@Data +public class DormRollCallTodayDetailItemVo implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + private Long studentId; + + private String studentName; + + private String studentNo; + + private String className; + + private Long roomId; + + private String roomName; + + private Long buildingId; + + private String buildingName; + + private String bedCode; + + /** 点名状态:1请假 2不在 4假期留宿 */ + private String rollCallStatus; + + private String leaveTypeName; + + private Date leaveStartTime; + + private Date leaveEndTime; + + private String holidayType; + + private Date holidayStartDate; + + private Date holidayEndDate; +} diff --git a/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/domain/vo/DormRollCallTodaySummaryVo.java b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/domain/vo/DormRollCallTodaySummaryVo.java new file mode 100644 index 00000000..3e89473e --- /dev/null +++ b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/domain/vo/DormRollCallTodaySummaryVo.java @@ -0,0 +1,39 @@ +package org.dromara.xg.domain.vo; + +import lombok.Data; + +import java.io.Serial; +import java.io.Serializable; +import java.util.ArrayList; +import java.util.List; + +/** + * 当前账号当天宿舍点名汇总 + */ +@Data +public class DormRollCallTodaySummaryVo implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + /** 已点名寝室数(去重) */ + private Integer checkedRoomCount = 0; + + /** 未点名寝室数 */ + private Integer uncheckedRoomCount = 0; + + /** 请假学生数 */ + private Integer leaveCount = 0; + + /** 假期留宿学生数 */ + private Integer holidayStayCount = 0; + + /** 异常(不在)学生数 */ + private Integer absentCount = 0; + + private List leaveDetails = new ArrayList<>(); + + private List holidayStayDetails = new ArrayList<>(); + + private List absentDetails = new ArrayList<>(); +} diff --git a/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/domain/vo/RoomRollCallStatusVo.java b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/domain/vo/RoomRollCallStatusVo.java index 7a3efb17..ef055b9f 100644 --- a/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/domain/vo/RoomRollCallStatusVo.java +++ b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/domain/vo/RoomRollCallStatusVo.java @@ -23,6 +23,8 @@ public class RoomRollCallStatusVo implements Serializable { private Integer leaveCount; + private Integer holidayStayCount; + private Integer absentCount; private Integer emptyCount; diff --git a/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/domain/vo/XgDormRollCallDetailVo.java b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/domain/vo/XgDormRollCallDetailVo.java index adf8678e..96473aa7 100644 --- a/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/domain/vo/XgDormRollCallDetailVo.java +++ b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/domain/vo/XgDormRollCallDetailVo.java @@ -52,4 +52,12 @@ public class XgDormRollCallDetailVo implements Serializable { private Date leaveStartTime; private Date leaveEndTime; + + private Long holidayStayId; + + private String holidayType; + + private Date holidayStartDate; + + private Date holidayEndDate; } diff --git a/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/domain/vo/XgDormRollCallVo.java b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/domain/vo/XgDormRollCallVo.java index f8f03c2b..84c03d10 100644 --- a/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/domain/vo/XgDormRollCallVo.java +++ b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/domain/vo/XgDormRollCallVo.java @@ -50,6 +50,8 @@ public class XgDormRollCallVo implements Serializable { private Integer leaveCount; + private Integer holidayStayCount; + private Integer absentCount; private Integer emptyCount; diff --git a/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/helper/LeaveFlowStatusSyncHelper.java b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/helper/LeaveFlowStatusSyncHelper.java new file mode 100644 index 00000000..9abedf3c --- /dev/null +++ b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/helper/LeaveFlowStatusSyncHelper.java @@ -0,0 +1,136 @@ +package org.dromara.xg.helper; + +import cn.hutool.core.util.ObjectUtil; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.dromara.common.core.enums.BusinessStatusEnum; +import org.dromara.common.core.service.WorkflowService; +import org.dromara.common.core.utils.StringUtils; +import org.dromara.warm.flow.orm.entity.FlowTask; +import org.dromara.workflow.common.ConditionalOnEnable; +import org.dromara.workflow.service.IFlwTaskService; +import org.springframework.stereotype.Component; + +import java.util.List; +import java.util.function.Consumer; + +/** + * 请假/销假业务状态与流程引擎对齐。 + *

+ * 规则: + * 1) submit=true → waiting + * 2) 流程实例为 waiting → 业务强制 waiting(覆盖 draft/back 残留) + * 3) 禁止 waiting → draft 降级 + * 4) 其它情况按事件/实例状态同步 + */ +@ConditionalOnEnable +@Slf4j +@RequiredArgsConstructor +@Component +public class LeaveFlowStatusSyncHelper { + + private final WorkflowService workflowService; + private final IFlwTaskService flwTaskService; + + /** + * 计算应对齐的业务状态。 + * + * @param businessId 业务ID + * @param submit 是否申请人提交 + * @param eventStatus 事件带来的状态(可空) + * @param currentStatus 业务表当前状态 + * @return 应对齐的状态;null 表示不改 + */ + public String resolveStatus(String businessId, Boolean submit, String eventStatus, String currentStatus) { + if (Boolean.TRUE.equals(submit)) { + return BusinessStatusEnum.WAITING.getStatus(); + } + + String flowStatus = safeFlowStatus(businessId); + boolean instanceWaiting = BusinessStatusEnum.WAITING.getStatus().equals(flowStatus); + boolean hasOpenTask = hasOpenTask(businessId); + + // 引擎已是 waiting:业务必须 waiting(打回重提后最常见脏数据) + if (instanceWaiting) { + return BusinessStatusEnum.WAITING.getStatus(); + } + + // 任务创建事件明确 waiting + if (BusinessStatusEnum.WAITING.getStatus().equals(eventStatus)) { + return BusinessStatusEnum.WAITING.getStatus(); + } + + String candidate = StringUtils.isNotBlank(eventStatus) ? eventStatus : flowStatus; + if (StringUtils.isBlank(candidate)) { + return null; + } + + // 禁止 waiting → draft + if (BusinessStatusEnum.WAITING.getStatus().equals(currentStatus) + && BusinessStatusEnum.DRAFT.getStatus().equals(candidate) + && hasOpenTask) { + log.warn("忽略将业务状态从 waiting 降为 draft, businessId={}", businessId); + return null; + } + + // 有运行中待办且候选为 draft:优先保持/切到 waiting(避免提交标记丢失) + if (hasOpenTask + && BusinessStatusEnum.DRAFT.getStatus().equals(candidate) + && !BusinessStatusEnum.isDraftOrCancelOrBack(flowStatus)) { + return BusinessStatusEnum.WAITING.getStatus(); + } + + return candidate; + } + + /** + * 应用状态到业务实体(仅当有变更时回调更新)。 + */ + public void applyIfChanged(String businessId, Boolean submit, String eventStatus, + String currentStatus, Consumer setter) { + String resolved = resolveStatus(businessId, submit, eventStatus, currentStatus); + if (StringUtils.isBlank(resolved) || resolved.equals(currentStatus)) { + return; + } + setter.accept(resolved); + log.info("业务状态已对齐流程: businessId={}, {} -> {}, submit={}, eventStatus={}, flowStatus={}", + businessId, currentStatus, resolved, submit, eventStatus, safeFlowStatus(businessId)); + } + + /** + * 保存时保护:已有 waiting 不允许被前端写成 draft。 + */ + public String protectFromDraftDowngrade(String existingStatus, String incomingStatus) { + if (BusinessStatusEnum.WAITING.getStatus().equals(existingStatus) + && BusinessStatusEnum.DRAFT.getStatus().equals(incomingStatus)) { + return existingStatus; + } + return incomingStatus; + } + + private String safeFlowStatus(String businessId) { + if (StringUtils.isBlank(businessId) || ObjectUtil.isNull(workflowService)) { + return null; + } + try { + return workflowService.getBusinessStatus(businessId); + } catch (Exception e) { + log.warn("读取流程状态失败, businessId={}, err={}", businessId, e.getMessage()); + return null; + } + } + + private boolean hasOpenTask(String businessId) { + try { + Long instanceId = workflowService.getInstanceIdByBusinessId(businessId); + if (instanceId == null) { + return false; + } + List tasks = flwTaskService.selectByInstId(instanceId); + return tasks != null && !tasks.isEmpty(); + } catch (Exception e) { + log.warn("查询运行中任务失败, businessId={}, err={}", businessId, e.getMessage()); + return false; + } + } +} diff --git a/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/service/IXgDormRollCallService.java b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/service/IXgDormRollCallService.java index 686e85bd..41fe61a5 100644 --- a/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/service/IXgDormRollCallService.java +++ b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/service/IXgDormRollCallService.java @@ -2,6 +2,7 @@ package org.dromara.xg.service; import org.dromara.xg.domain.vo.XgDormRollCallVo; import org.dromara.xg.domain.vo.RoomRollCallStatusVo; +import org.dromara.xg.domain.vo.DormRollCallTodaySummaryVo; import org.dromara.xg.domain.bo.XgDormRollCallBo; import org.dromara.common.mybatis.core.page.TableDataInfo; import org.dromara.common.mybatis.core.page.PageQuery; @@ -30,4 +31,9 @@ public interface IXgDormRollCallService { XgDormRollCallVo submitRollCall(XgDormRollCallBo bo); TableDataInfo queryRoomHistory(Long roomId, PageQuery pageQuery); + + /** + * 当前登录账号当天点名汇总(已/未点名寝室 + 请假/留宿/异常明细) + */ + DormRollCallTodaySummaryVo queryTodaySummary(); } diff --git a/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/service/IXgHolidayStayService.java b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/service/IXgHolidayStayService.java index 877a61e1..3180b40c 100644 --- a/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/service/IXgHolidayStayService.java +++ b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/service/IXgHolidayStayService.java @@ -8,8 +8,10 @@ import org.dromara.xg.domain.bo.StudentOptionBo; import org.dromara.common.mybatis.core.page.TableDataInfo; import org.dromara.common.mybatis.core.page.PageQuery; +import java.time.LocalDateTime; import java.util.Collection; import java.util.List; +import java.util.Map; /** * 假期留宿管理Service接口 @@ -116,4 +118,13 @@ public interface IXgHolidayStayService { */ Boolean revoke(XgHolidayStayAuditBo auditBo); + /** + * 按检查时刻查询学生当前有效的假期留宿(已通过,且日期落在留宿起止日内,结束日当天整天有效) + * + * @param studentIds 学生ID列表 + * @param checkDate 检查时刻 + * @return 学生ID → 留宿记录(每人取最新一条) + */ + Map queryStudentHolidayStaysByCheckDate(List studentIds, LocalDateTime checkDate); + } diff --git a/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/service/impl/XgDormBedStudentServiceImpl.java b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/service/impl/XgDormBedStudentServiceImpl.java index f699fdd8..d4ad946a 100644 --- a/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/service/impl/XgDormBedStudentServiceImpl.java +++ b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/service/impl/XgDormBedStudentServiceImpl.java @@ -41,7 +41,9 @@ import org.dromara.xg.domain.bo.XgDormSelectiveAllocationBo; import org.dromara.xg.mapper.XgStudentMsgMapper; import org.dromara.xg.domain.XgStudentMsg; import org.dromara.xg.service.IXgStudentLeaveService; +import org.dromara.xg.service.IXgHolidayStayService; import org.dromara.xg.domain.vo.XgStudentLeaveVo; +import org.dromara.xg.domain.vo.XgHolidayStayVo; import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; import org.dromara.common.satoken.utils.LoginHelper; import org.dromara.xg.domain.vo.DormBuildingTreeVo; @@ -58,6 +60,7 @@ import org.dromara.xg.domain.vo.XgDormBedDetailVo; import org.dromara.xg.domain.bo.XgDormSelectiveAllocationBo; import org.dromara.xg.helper.FreshmanCollegeProxyHelper; import org.dromara.xg.helper.CurrentUserCollegeHelper; +import org.springframework.beans.factory.ObjectProvider; /** * 床位学生分配关系Service业务层处理 @@ -79,6 +82,8 @@ public class XgDormBedStudentServiceImpl implements IXgDormBedStudentService { private final XgDormAreaMapper xgDormAreaMapper; private final XgClassMsgMapper xgClassMsgMapper; private final IXgStudentLeaveService xgStudentLeaveService; + /** 延迟获取,避免与假期留宿服务互相依赖形成循环 */ + private final ObjectProvider xgHolidayStayServiceProvider; private final FreshmanCollegeProxyHelper freshmanCollegeProxyHelper; private final CurrentUserCollegeHelper currentUserCollegeHelper; @@ -2489,7 +2494,7 @@ public class XgDormBedStudentServiceImpl implements IXgDormBedStudentService { /** * 根据房间ID获取住在当前房间的所有学生信息 * 包含学生姓名、学号、性别、学院名称、专业名称、班级名称、床位编码、床位ID - * 同时查询当前时间有效的请假信息 + * 同时查询当前时间有效的请假信息、假期留宿信息 */ @Override public List getRoomStudents(Long roomId) { @@ -2517,10 +2522,15 @@ public class XgDormBedStudentServiceImpl implements IXgDormBedStudentService { .filter(Objects::nonNull) .collect(Collectors.toList()); + LocalDateTime now = LocalDateTime.now(); Map leaveMap = Map.of(); + Map holidayStayMap = Map.of(); if (!studentIds.isEmpty()) { - leaveMap = xgStudentLeaveService.queryStudentLeavesByCheckDate( - studentIds, LocalDateTime.now()); + leaveMap = xgStudentLeaveService.queryStudentLeavesByCheckDate(studentIds, now); + IXgHolidayStayService holidayStayService = xgHolidayStayServiceProvider.getIfAvailable(); + if (holidayStayService != null) { + holidayStayMap = holidayStayService.queryStudentHolidayStaysByCheckDate(studentIds, now); + } } List result = new ArrayList<>(); @@ -2549,8 +2559,17 @@ public class XgDormBedStudentServiceImpl implements IXgDormBedStudentService { studentInfo.put("bedCode", mapVal(detailMap, "bedCode", "bed_code")); studentInfo.put("bedId", toIdString(mapVal(detailMap, "bedId", "bed_id"))); + Long studentIdValue = null; if (studentIdObj instanceof Number) { - Long studentIdValue = ((Number) studentIdObj).longValue(); + studentIdValue = ((Number) studentIdObj).longValue(); + } else if (studentIdObj != null) { + try { + studentIdValue = Long.valueOf(studentIdObj.toString()); + } catch (Exception ignored) { + // ignore invalid student id + } + } + if (studentIdValue != null) { XgStudentLeaveVo leaveInfo = leaveMap.get(studentIdValue); if (leaveInfo != null) { Map leaveInfoMap = new LinkedHashMap<>(); @@ -2560,20 +2579,15 @@ public class XgDormBedStudentServiceImpl implements IXgDormBedStudentService { leaveInfoMap.put("leaveEndTime", leaveInfo.getEndTime()); studentInfo.put("leaveInfo", leaveInfoMap); } - } else if (studentIdObj != null) { - try { - Long studentIdValue = Long.valueOf(studentIdObj.toString()); - XgStudentLeaveVo leaveInfo = leaveMap.get(studentIdValue); - if (leaveInfo != null) { - Map leaveInfoMap = new LinkedHashMap<>(); - leaveInfoMap.put("leaveId", leaveInfo.getId()); - leaveInfoMap.put("leaveTypeName", leaveInfo.getLeaveTypeName()); - leaveInfoMap.put("leaveStartTime", leaveInfo.getStartTime()); - leaveInfoMap.put("leaveEndTime", leaveInfo.getEndTime()); - studentInfo.put("leaveInfo", leaveInfoMap); - } - } catch (Exception ignored) { - // ignore invalid student id + // 请假优先;无请假时再挂假期留宿 + XgHolidayStayVo holidayStay = holidayStayMap.get(studentIdValue); + if (leaveInfo == null && holidayStay != null) { + Map holidayStayInfoMap = new LinkedHashMap<>(); + holidayStayInfoMap.put("holidayStayId", holidayStay.getId()); + holidayStayInfoMap.put("holidayType", holidayStay.getHolidayType()); + holidayStayInfoMap.put("applyStartDate", holidayStay.getApplyStartDate()); + holidayStayInfoMap.put("applyEndDate", holidayStay.getApplyEndDate()); + studentInfo.put("holidayStayInfo", holidayStayInfoMap); } } diff --git a/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/service/impl/XgDormRollCallServiceImpl.java b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/service/impl/XgDormRollCallServiceImpl.java index 669dbdd4..c7448bc6 100644 --- a/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/service/impl/XgDormRollCallServiceImpl.java +++ b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/service/impl/XgDormRollCallServiceImpl.java @@ -16,20 +16,29 @@ import org.dromara.xg.domain.bo.XgDormRollCallDetailBo; import org.dromara.xg.domain.vo.XgDormRollCallVo; import org.dromara.xg.domain.vo.XgDormRollCallDetailVo; import org.dromara.xg.domain.vo.RoomRollCallStatusVo; +import org.dromara.xg.domain.vo.DormRollCallTodaySummaryVo; +import org.dromara.xg.domain.vo.DormRollCallTodayDetailItemVo; import org.dromara.xg.domain.XgDormRollCall; import org.dromara.xg.domain.XgDormRollCallDetail; +import org.dromara.xg.domain.XgDormManager; +import org.dromara.xg.domain.XgDormManagerBuilding; +import org.dromara.xg.domain.XgDormManagerSource; import org.dromara.xg.mapper.XgDormRollCallMapper; import org.dromara.xg.mapper.XgDormRollCallDetailMapper; import org.dromara.xg.mapper.XgDormRoomMapper; import org.dromara.xg.mapper.XgDormBedMapper; import org.dromara.xg.mapper.XgDormBuildingMapper; import org.dromara.xg.mapper.XgStudentMsgMapper; +import org.dromara.xg.mapper.XgDormManagerMapper; +import org.dromara.xg.mapper.XgDormManagerBuildingMapper; +import org.dromara.xg.mapper.XgDormManagerSourceMapper; import org.dromara.xg.domain.XgStudentMsg; import org.dromara.xg.domain.XgDormRoom; import org.dromara.xg.domain.XgDormBuilding; import org.dromara.xg.domain.XgDormBed; import org.dromara.xg.service.IXgDormRollCallService; import org.dromara.common.satoken.utils.LoginHelper; +import cn.hutool.core.date.DateUtil; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; @@ -48,6 +57,9 @@ public class XgDormRollCallServiceImpl implements IXgDormRollCallService { private final XgDormBedMapper bedMapper; private final XgDormBuildingMapper buildingMapper; private final XgStudentMsgMapper studentMsgMapper; + private final XgDormManagerMapper dormManagerMapper; + private final XgDormManagerBuildingMapper dormManagerBuildingMapper; + private final XgDormManagerSourceMapper dormManagerSourceMapper; @Override public XgDormRollCallVo queryById(Long id) { @@ -176,12 +188,14 @@ public class XgDormRollCallServiceImpl implements IXgDormRollCallService { status.setRollCallId(completed.getId()); status.setPresentCount(completed.getPresentCount()); status.setLeaveCount(completed.getLeaveCount()); + status.setHolidayStayCount(completed.getHolidayStayCount() != null ? completed.getHolidayStayCount() : 0); status.setAbsentCount(completed.getAbsentCount()); status.setEmptyCount(completed.getEmptyCount()); } else { status.setIsCompleted(false); status.setPresentCount(0); status.setLeaveCount(0); + status.setHolidayStayCount(0); status.setAbsentCount(0); status.setEmptyCount(0); } @@ -267,6 +281,222 @@ public class XgDormRollCallServiceImpl implements IXgDormRollCallService { return TableDataInfo.build(page); } + @Override + public DormRollCallTodaySummaryVo queryTodaySummary() { + DormRollCallTodaySummaryVo summary = new DormRollCallTodaySummaryVo(); + Long checkerId; + try { + checkerId = LoginHelper.getUserId(); + } catch (Exception e) { + return summary; + } + if (checkerId == null) { + return summary; + } + + Date dayBegin = DateUtil.beginOfDay(new Date()); + Date dayEnd = DateUtil.endOfDay(new Date()); + + List todayRecords = baseMapper.selectList( + Wrappers.lambdaQuery() + .eq(XgDormRollCall::getCheckerId, checkerId) + .eq(XgDormRollCall::getStatus, "0") + .ge(XgDormRollCall::getCheckDate, dayBegin) + .le(XgDormRollCall::getCheckDate, dayEnd) + .orderByDesc(XgDormRollCall::getCheckDate) + ); + + // 同一寝室多次点名时取最新一条,避免明细重复统计 + Map latestByRoom = new LinkedHashMap<>(); + for (XgDormRollCall record : todayRecords) { + if (record.getRoomId() == null) { + continue; + } + latestByRoom.putIfAbsent(record.getRoomId(), record); + } + Set checkedRoomIds = latestByRoom.keySet(); + summary.setCheckedRoomCount(checkedRoomIds.size()); + + Set scopeBuildingIds = resolveManagedBuildingIds(checkerId); + for (XgDormRollCall record : latestByRoom.values()) { + if (record.getBuildingId() != null) { + scopeBuildingIds.add(record.getBuildingId()); + } + } + + if (!scopeBuildingIds.isEmpty()) { + List scopeRooms = roomMapper.selectList( + Wrappers.lambdaQuery() + .in(XgDormRoom::getBuildingId, scopeBuildingIds) + .eq(XgDormRoom::getRoomStatus, "0") + ); + long unchecked = scopeRooms.stream() + .map(XgDormRoom::getId) + .filter(id -> !checkedRoomIds.contains(id)) + .count(); + summary.setUncheckedRoomCount((int) unchecked); + } else { + summary.setUncheckedRoomCount(0); + } + + if (latestByRoom.isEmpty()) { + return summary; + } + + List rollCallIds = latestByRoom.values().stream() + .map(XgDormRollCall::getId) + .filter(Objects::nonNull) + .collect(Collectors.toList()); + if (rollCallIds.isEmpty()) { + return summary; + } + + Map rollCallMap = latestByRoom.values().stream() + .collect(Collectors.toMap(XgDormRollCall::getId, r -> r, (a, b) -> a)); + + List details = detailMapper.selectList( + Wrappers.lambdaQuery() + .in(XgDormRollCallDetail::getRollCallId, rollCallIds) + .in(XgDormRollCallDetail::getRollCallStatus, Arrays.asList("1", "2", "4")) + .isNotNull(XgDormRollCallDetail::getStudentId) + .orderByAsc(XgDormRollCallDetail::getBedCode) + ); + + List leaveDetails = new ArrayList<>(); + List holidayStayDetails = new ArrayList<>(); + List absentDetails = new ArrayList<>(); + + for (XgDormRollCallDetail detail : details) { + DormRollCallTodayDetailItemVo item = toTodayDetailItem(detail, rollCallMap.get(detail.getRollCallId())); + String status = detail.getRollCallStatus(); + if ("1".equals(status)) { + leaveDetails.add(item); + } else if ("4".equals(status)) { + holidayStayDetails.add(item); + } else if ("2".equals(status)) { + absentDetails.add(item); + } + } + + summary.setLeaveDetails(leaveDetails); + summary.setHolidayStayDetails(holidayStayDetails); + summary.setAbsentDetails(absentDetails); + summary.setLeaveCount(leaveDetails.size()); + summary.setHolidayStayCount(holidayStayDetails.size()); + summary.setAbsentCount(absentDetails.size()); + return summary; + } + + private DormRollCallTodayDetailItemVo toTodayDetailItem(XgDormRollCallDetail detail, XgDormRollCall rollCall) { + DormRollCallTodayDetailItemVo item = new DormRollCallTodayDetailItemVo(); + item.setStudentId(detail.getStudentId()); + item.setStudentName(detail.getStudentName()); + item.setStudentNo(detail.getStudentNo()); + item.setClassName(detail.getClassName()); + item.setBedCode(detail.getBedCode()); + item.setRollCallStatus(detail.getRollCallStatus()); + item.setLeaveTypeName(detail.getLeaveTypeName()); + item.setLeaveStartTime(detail.getLeaveStartTime()); + item.setLeaveEndTime(detail.getLeaveEndTime()); + item.setHolidayType(detail.getHolidayType()); + item.setHolidayStartDate(detail.getHolidayStartDate()); + item.setHolidayEndDate(detail.getHolidayEndDate()); + if (rollCall != null) { + item.setRoomId(rollCall.getRoomId()); + item.setRoomName(rollCall.getRoomName()); + item.setBuildingId(rollCall.getBuildingId()); + item.setBuildingName(rollCall.getBuildingName()); + } + return item; + } + + /** + * 解析当前账号宿舍管理员管辖的楼栋(楼栋权限表 + 授权来源表)。 + */ + private Set resolveManagedBuildingIds(Long userId) { + Set buildingIds = new HashSet<>(); + if (userId == null) { + return buildingIds; + } + List managers = dormManagerMapper.selectList( + Wrappers.lambdaQuery() + .eq(XgDormManager::getUserId, userId) + .eq(XgDormManager::getStatus, "0") + ); + if (managers == null || managers.isEmpty()) { + return buildingIds; + } + List managerIds = managers.stream().map(XgDormManager::getId).collect(Collectors.toList()); + + List managerBuildings = dormManagerBuildingMapper.selectList( + Wrappers.lambdaQuery() + .in(XgDormManagerBuilding::getManagerId, managerIds) + .eq(XgDormManagerBuilding::getStatus, "0") + ); + for (XgDormManagerBuilding mb : managerBuildings) { + if (mb.getBuildingId() != null) { + buildingIds.add(mb.getBuildingId()); + } + } + + List sources = dormManagerSourceMapper.selectList( + Wrappers.lambdaQuery() + .in(XgDormManagerSource::getManagerId, managerIds) + .eq(XgDormManagerSource::getStatus, "0") + ); + Set campusIds = new HashSet<>(); + Set areaIds = new HashSet<>(); + boolean allCampus = false; + for (XgDormManagerSource source : sources) { + String authType = source.getAuthType(); + Long authSource = source.getAuthSource(); + if ("1".equals(authType)) { + allCampus = true; + } else if ("2".equals(authType) && authSource != null) { + campusIds.add(authSource); + } else if ("3".equals(authType) && authSource != null) { + areaIds.add(authSource); + } else if ("4".equals(authType) && authSource != null) { + buildingIds.add(authSource); + } + } + if (allCampus) { + List allBuildings = buildingMapper.selectList( + Wrappers.lambdaQuery().select(XgDormBuilding::getId) + ); + for (XgDormBuilding b : allBuildings) { + if (b.getId() != null) { + buildingIds.add(b.getId()); + } + } + return buildingIds; + } + if (!campusIds.isEmpty() || !areaIds.isEmpty()) { + LambdaQueryWrapper buildingLqw = Wrappers.lambdaQuery() + .select(XgDormBuilding::getId); + buildingLqw.and(w -> { + boolean first = true; + if (!campusIds.isEmpty()) { + w.in(XgDormBuilding::getCampusId, campusIds); + first = false; + } + if (!areaIds.isEmpty()) { + if (!first) { + w.or(); + } + w.in(XgDormBuilding::getAreaId, areaIds); + } + }); + List buildings = buildingMapper.selectList(buildingLqw); + for (XgDormBuilding b : buildings) { + if (b.getId() != null) { + buildingIds.add(b.getId()); + } + } + } + return buildingIds; + } + private LambdaQueryWrapper buildQueryWrapper(XgDormRollCallBo bo) { LambdaQueryWrapper lqw = Wrappers.lambdaQuery(); lqw.like(StringUtils.isNotBlank(bo.getBatchNo()), XgDormRollCall::getBatchNo, bo.getBatchNo()); @@ -300,6 +530,10 @@ public class XgDormRollCallServiceImpl implements IXgDormRollCallService { detail.setLeaveTypeName(detailBo.getLeaveTypeName()); detail.setLeaveStartTime(detailBo.getLeaveStartTime()); detail.setLeaveEndTime(detailBo.getLeaveEndTime()); + detail.setHolidayStayId(detailBo.getHolidayStayId()); + detail.setHolidayType(detailBo.getHolidayType()); + detail.setHolidayStartDate(detailBo.getHolidayStartDate()); + detail.setHolidayEndDate(detailBo.getHolidayEndDate()); detailMapper.insert(detail); } } diff --git a/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/service/impl/XgHolidayStayServiceImpl.java b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/service/impl/XgHolidayStayServiceImpl.java index 985b0e75..4721c4c9 100644 --- a/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/service/impl/XgHolidayStayServiceImpl.java +++ b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/service/impl/XgHolidayStayServiceImpl.java @@ -39,6 +39,10 @@ import java.util.HashMap; import java.util.Calendar; import java.text.SimpleDateFormat; import java.util.Date; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.ZoneId; import org.springframework.beans.BeanUtils; /** @@ -999,4 +1003,39 @@ public class XgHolidayStayServiceImpl implements IXgHolidayStayService { private String toStr(Object val) { return val == null ? null : val.toString(); } + + /** + * 按检查时刻查询学生当前有效的假期留宿。 + * 条件:已通过;检查日落在 [applyStartDate, applyEndDate](按日期比较,结束日当天整天有效)。 + */ + @Override + public Map queryStudentHolidayStaysByCheckDate(List studentIds, LocalDateTime checkDate) { + if (studentIds == null || studentIds.isEmpty() || checkDate == null) { + return Map.of(); + } + + ZoneId zone = ZoneId.systemDefault(); + LocalDate checkDay = checkDate.toLocalDate(); + Date dayStart = Date.from(checkDay.atStartOfDay(zone).toInstant()); + Date dayEnd = Date.from(LocalDateTime.of(checkDay, LocalTime.MAX).atZone(zone).toInstant()); + + LambdaQueryWrapper lqw = Wrappers.lambdaQuery(); + lqw.in(XgHolidayStay::getStudentId, studentIds); + // 仅已通过 + lqw.eq(XgHolidayStay::getApplyStatus, "2"); + // 开始日 <= 检查日末,结束日 >= 检查日初(结束日当天整天有效) + lqw.le(XgHolidayStay::getApplyStartDate, dayEnd); + lqw.ge(XgHolidayStay::getApplyEndDate, dayStart); + lqw.isNotNull(XgHolidayStay::getApplyStartDate); + lqw.isNotNull(XgHolidayStay::getApplyEndDate); + lqw.orderByDesc(XgHolidayStay::getCreateTime); + + List stayList = baseMapper.selectList(lqw); + return stayList.stream() + .collect(Collectors.toMap( + XgHolidayStay::getStudentId, + stay -> MapstructUtils.convert(stay, XgHolidayStayVo.class), + (existing, replacement) -> existing + )); + } } diff --git a/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/service/impl/XgStudentLeaveServiceImpl.java b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/service/impl/XgStudentLeaveServiceImpl.java index 625b9cfb..0805eef5 100644 --- a/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/service/impl/XgStudentLeaveServiceImpl.java +++ b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/service/impl/XgStudentLeaveServiceImpl.java @@ -15,6 +15,7 @@ import org.dromara.xg.domain.vo.XgStudentLeaveVo; import org.dromara.xg.domain.XgStudentLeave; import org.dromara.xg.mapper.XgStudentLeaveMapper; import org.dromara.xg.service.IXgStudentLeaveService; +import org.dromara.xg.helper.LeaveFlowStatusSyncHelper; import org.dromara.workflow.common.ConditionalOnEnable; import org.dromara.workflow.domain.TestLeave; import org.dromara.xg.domain.XgStudentCancelLeave; @@ -52,10 +53,12 @@ import org.dromara.xg.domain.XgStudentManagerSource; import org.dromara.xg.domain.XgClassMsg; import java.time.LocalDateTime; +import java.util.Arrays; import java.util.List; import java.util.Map; import java.util.Collection; import java.util.ArrayList; +import java.util.Objects; import java.util.Set; import java.util.HashSet; import java.util.stream.Collectors; @@ -75,6 +78,7 @@ public class XgStudentLeaveServiceImpl implements IXgStudentLeaveService { private final XgStudentLeaveMapper baseMapper; private final WorkflowService workflowService; private final XgStudentCancelLeaveMapper xgStudentCancelLeaveMapper; + private final LeaveFlowStatusSyncHelper leaveFlowStatusSyncHelper; // 添加权限过滤需要的依赖 private final SysDeptMapper sysDeptMapper; private final XgStudentManagerMapper xgStudentManagerMapper; @@ -327,6 +331,7 @@ public class XgStudentLeaveServiceImpl implements IXgStudentLeaveService { lqw.like(StringUtils.isNotBlank(bo.getApprovalByName()), XgStudentLeave::getApprovalByName, bo.getApprovalByName()); lqw.eq(StringUtils.isNotBlank(bo.getCancelStatus()), XgStudentLeave::getCancelStatus, bo.getCancelStatus()); + applyExcludeFinishedCancel(lqw, bo); lqw.eq(bo.getCancelTime() != null, XgStudentLeave::getCancelTime, bo.getCancelTime()); lqw.eq(StringUtils.isNotBlank(bo.getCancelLocation()), XgStudentLeave::getCancelLocation, bo.getCancelLocation()); @@ -376,6 +381,7 @@ public class XgStudentLeaveServiceImpl implements IXgStudentLeaveService { lqw.like(StringUtils.isNotBlank(bo.getApprovalByName()), XgStudentLeave::getApprovalByName, bo.getApprovalByName()); lqw.eq(StringUtils.isNotBlank(bo.getCancelStatus()), XgStudentLeave::getCancelStatus, bo.getCancelStatus()); + applyExcludeFinishedCancel(lqw, bo); lqw.eq(bo.getCancelTime() != null, XgStudentLeave::getCancelTime, bo.getCancelTime()); lqw.eq(StringUtils.isNotBlank(bo.getCancelLocation()), XgStudentLeave::getCancelLocation, bo.getCancelLocation()); @@ -385,6 +391,19 @@ public class XgStudentLeaveServiceImpl implements IXgStudentLeaveService { return lqw; } + /** + * 移动端学生请假列表:排除销假审批已完成记录。 + * finish=流程完成;1=旧字典「已销假」。未传开关时不影响 Web 等其它调用方。 + */ + private void applyExcludeFinishedCancel(LambdaQueryWrapper lqw, XgStudentLeaveBo bo) { + if (!Boolean.TRUE.equals(bo.getExcludeFinishedCancel())) { + return; + } + lqw.and(w -> w.isNull(XgStudentLeave::getCancelStatus) + .or() + .notIn(XgStudentLeave::getCancelStatus, Arrays.asList("finish", "1"))); + } + /** * 新增学生请假 - 记录学生的请假申请、审批和销假信息 * @@ -417,11 +436,13 @@ public class XgStudentLeaveServiceImpl implements IXgStudentLeaveService { // 流程已在待审核时,禁止被前端再次保存成 draft(否则办理页审批按钮会消失) if (update.getId() != null && StringUtils.isNotBlank(update.getApprovalStatus())) { XgStudentLeave existing = baseMapper.selectById(update.getId()); - if (ObjectUtil.isNotNull(existing) - && BusinessStatusEnum.WAITING.getStatus().equals(existing.getApprovalStatus()) - && BusinessStatusEnum.DRAFT.getStatus().equals(update.getApprovalStatus())) { - log.warn("忽略将请假审批状态从 waiting 回写为 draft, leaveId={}", update.getId()); - update.setApprovalStatus(null); + if (ObjectUtil.isNotNull(existing)) { + String protectedStatus = leaveFlowStatusSyncHelper.protectFromDraftDowngrade( + existing.getApprovalStatus(), update.getApprovalStatus()); + if (!protectedStatus.equals(update.getApprovalStatus())) { + log.warn("忽略将请假审批状态从 waiting 回写为 draft, leaveId={}", update.getId()); + update.setApprovalStatus(null); + } } } return baseMapper.updateById(update) > 0; @@ -464,12 +485,12 @@ public class XgStudentLeaveServiceImpl implements IXgStudentLeaveService { log.error("请假流程事件未找到业务数据, businessId={}", processEvent.getBusinessId()); return; } - // 申请人提交:强制对齐为 waiting;其它事件按流程状态同步(空 status 不覆盖) - if (processEvent.getSubmit()) { - xgStudentLeave.setApprovalStatus(BusinessStatusEnum.WAITING.getStatus()); - } else if (StringUtils.isNotBlank(processEvent.getStatus())) { - xgStudentLeave.setApprovalStatus(processEvent.getStatus()); - } + leaveFlowStatusSyncHelper.applyIfChanged( + processEvent.getBusinessId(), + processEvent.getSubmit(), + processEvent.getStatus(), + xgStudentLeave.getApprovalStatus(), + xgStudentLeave::setApprovalStatus); // 用于例如审批附件 审批意见等 存储到业务表内 自行根据业务实现存储流程 Map params = processEvent.getParams(); if (MapUtil.isNotEmpty(params)) { @@ -494,30 +515,34 @@ public class XgStudentLeaveServiceImpl implements IXgStudentLeaveService { @Transactional(rollbackFor = Exception.class) @EventListener(condition = "#processEvent.flowCode.startsWith('hzdyjs-xj')") public void processHandl_xj(ProcessEvent processEvent) { - // 更新了销假表 log.info("当前任务执行了{}", processEvent.toString()); XgStudentCancelLeave xgStudentCancelLeave = xgStudentCancelLeaveMapper .selectById(Long.valueOf(processEvent.getBusinessId())); - // 更新了请假表 - XgStudentLeave xgStudentLeave = baseMapper.selectById(xgStudentCancelLeave.getLeaveId()); - xgStudentCancelLeave.setCancelStatus(processEvent.getStatus()); - xgStudentLeave.setCancelStatus(processEvent.getStatus()); + if (ObjectUtil.isNull(xgStudentCancelLeave)) { + log.error("销假流程事件未找到业务数据, businessId={}", processEvent.getBusinessId()); + return; + } + leaveFlowStatusSyncHelper.applyIfChanged( + processEvent.getBusinessId(), + processEvent.getSubmit(), + processEvent.getStatus(), + xgStudentCancelLeave.getCancelStatus(), + xgStudentCancelLeave::setCancelStatus); // 用于例如审批附件 审批意见等 存储到业务表内 自行根据业务实现存储流程 Map params = processEvent.getParams(); if (MapUtil.isNotEmpty(params)) { - // 历史任务扩展(通常为附件) String hisTaskExt = Convert.toStr(params.get("hisTaskExt")); - // 办理人 String handler = Convert.toStr(params.get("handler")); - // 办理意见 String message = Convert.toStr(params.get("message")); } - if (processEvent.getSubmit()) { - xgStudentCancelLeave.setCancelStatus(BusinessStatusEnum.WAITING.getStatus()); - xgStudentLeave.setCancelStatus(BusinessStatusEnum.WAITING.getStatus()); - } xgStudentCancelLeaveMapper.updateById(xgStudentCancelLeave); - baseMapper.updateById(xgStudentLeave); + if (xgStudentCancelLeave.getLeaveId() != null) { + XgStudentLeave xgStudentLeave = baseMapper.selectById(xgStudentCancelLeave.getLeaveId()); + if (ObjectUtil.isNotNull(xgStudentLeave)) { + xgStudentLeave.setCancelStatus(xgStudentCancelLeave.getCancelStatus()); + baseMapper.updateById(xgStudentLeave); + } + } } /** @@ -540,12 +565,18 @@ public class XgStudentLeaveServiceImpl implements IXgStudentLeaveService { log.error("请假任务创建事件未找到业务数据, businessId={}", processTaskEvent.getBusinessId()); return; } - if (BusinessStatusEnum.DRAFT.getStatus().equals(leave.getApprovalStatus()) - && BusinessStatusEnum.WAITING.getStatus().equals(processTaskEvent.getStatus())) { - leave.setApprovalStatus(BusinessStatusEnum.WAITING.getStatus()); + String before = leave.getApprovalStatus(); + leaveFlowStatusSyncHelper.applyIfChanged( + processTaskEvent.getBusinessId(), + false, + processTaskEvent.getStatus(), + leave.getApprovalStatus(), + leave::setApprovalStatus); + if (!Objects.equals(before, leave.getApprovalStatus())) { baseMapper.updateById(leave); - log.info("请假业务状态已从 draft 对齐为 waiting, businessId={}, taskId={}, nodeName={}", - processTaskEvent.getBusinessId(), processTaskEvent.getTaskId(), processTaskEvent.getNodeName()); + log.info("请假任务创建后业务状态已对齐, businessId={}, taskId={}, nodeName={}, {} -> {}", + processTaskEvent.getBusinessId(), processTaskEvent.getTaskId(), processTaskEvent.getNodeName(), + before, leave.getApprovalStatus()); } } @@ -568,40 +599,37 @@ public class XgStudentLeaveServiceImpl implements IXgStudentLeaveService { 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())) { + if (ObjectUtil.isNull(cancelLeave)) { 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); + // 旧字典 0/1/2/3 视为未进入流程态,按 draft 参与对齐 + String currentForSync = previousStatus; + if ("0".equals(previousStatus) || "1".equals(previousStatus) + || "2".equals(previousStatus) || "3".equals(previousStatus)) { + currentForSync = BusinessStatusEnum.DRAFT.getStatus(); + } + leaveFlowStatusSyncHelper.applyIfChanged( + processTaskEvent.getBusinessId(), + false, + processTaskEvent.getStatus(), + currentForSync, + cancelLeave::setCancelStatus); + if (!Objects.equals(previousStatus, cancelLeave.getCancelStatus())) { + xgStudentCancelLeaveMapper.updateById(cancelLeave); + if (cancelLeave.getLeaveId() != null) { + XgStudentLeave leave = baseMapper.selectById(cancelLeave.getLeaveId()); + if (ObjectUtil.isNotNull(leave)) { + leave.setCancelStatus(cancelLeave.getCancelStatus()); + baseMapper.updateById(leave); + } } + log.info("销假业务状态已对齐, businessId={}, taskId={}, nodeName={}, {} -> {}", + processTaskEvent.getBusinessId(), processTaskEvent.getTaskId(), processTaskEvent.getNodeName(), + previousStatus, cancelLeave.getCancelStatus()); } - log.info("销假业务状态已对齐为 waiting, businessId={}, taskId={}, nodeName={}, previousStatus={}", - processTaskEvent.getBusinessId(), processTaskEvent.getTaskId(), processTaskEvent.getNodeName(), - previousStatus); - } - - /** - * 销假流程状态尚未写成 waiting 时需要对齐。 - * 0/1/2/3 是请假单上的旧销假字典,draft 是提交标记丢失后停留的草稿。 - */ - private boolean shouldAlignCancelStatusToWaiting(String cancelStatus) { - return StringUtils.isBlank(cancelStatus) - || "0".equals(cancelStatus) - || "1".equals(cancelStatus) - || "2".equals(cancelStatus) - || "3".equals(cancelStatus) - || BusinessStatusEnum.DRAFT.getStatus().equals(cancelStatus); } /** diff --git a/backend/script/sql/update/update_20261020_dorm_hygiene_deduction_types.sql b/backend/script/sql/update/update_20261020_dorm_hygiene_deduction_types.sql new file mode 100644 index 00000000..4a97c080 --- /dev/null +++ b/backend/script/sql/update/update_20261020_dorm_hygiene_deduction_types.sql @@ -0,0 +1,164 @@ +-- ============================================================================= +-- 寝室检查扣分项同步:按「扣分条例10月15日」重建宿舍区违纪类型 +-- 文件:update_20261020_dorm_hygiene_deduction_types.sql +-- 依据:宿管《扣分条例10月15日》 +-- 影响表:xskh_discipline_type(仅 area_type='1' 宿舍区) +-- 不影响:教学区违纪(area_type=2)、历史检查明细中的旧 type_id(仅停用不删除) +-- ============================================================================= +-- 【重要】执行前请完整阅读配套说明: +-- docs/开发文档/寝室检查扣分条例生产执行说明-20261020.md +-- ============================================================================= + +-- USE your_prod_database; -- 如有需要请取消注释并改成生产库名 + +SET NAMES utf8mb4; + +-- ----------------------------------------------------------------------------- +-- 步骤0:备份(强烈建议先做) +-- ----------------------------------------------------------------------------- +CREATE TABLE IF NOT EXISTS xskh_discipline_type_bak_20261020 AS +SELECT * FROM xskh_discipline_type WHERE 1 = 0; + +INSERT INTO xskh_discipline_type_bak_20261020 +SELECT t.* +FROM xskh_discipline_type t +LEFT JOIN xskh_discipline_type_bak_20261020 b ON b.id = t.id +WHERE t.area_type = '1' + AND t.deleted = 0 + AND b.id IS NULL; + +-- 核对备份行数 +SELECT COUNT(*) AS bak_dorm_type_count FROM xskh_discipline_type_bak_20261020; + +-- ----------------------------------------------------------------------------- +-- 步骤1:执行前核对(只读) +-- ----------------------------------------------------------------------------- +SELECT id, type_code, type_name, min_score, max_score, status, sort_order +FROM xskh_discipline_type +WHERE area_type = '1' AND deleted = 0 +ORDER BY status, sort_order, id; + +-- ----------------------------------------------------------------------------- +-- 步骤2:停用旧的宿舍区违纪类型(不物理删除,保留历史引用) +-- 仅停用非本次批次编码(sskf202610_ 开头的视为本脚本数据,可重复执行) +-- ----------------------------------------------------------------------------- +UPDATE xskh_discipline_type +SET status = '1', + update_time = NOW(), + update_by = 1, + remark = CONCAT(IFNULL(remark, ''), ' [2026-10-20停用:切换至10月15日扣分条例]') +WHERE area_type = '1' + AND deleted = 0 + AND status = '0' + AND type_code NOT LIKE 'sskf202610_%'; + +SELECT ROW_COUNT() AS disabled_old_count; + +-- ----------------------------------------------------------------------------- +-- 步骤3:清理本脚本同批次数据(支持幂等重跑) +-- 注意:若生产已用新类型做过检查,请勿再执行 DELETE,改为手工调整 +-- ----------------------------------------------------------------------------- +DELETE t FROM xskh_discipline_type t +WHERE t.type_code LIKE 'sskf202610_%' + AND NOT EXISTS ( + SELECT 1 + FROM xg_dorm_hygiene_check_deduction d + WHERE d.discipline_type_id = t.id + AND IFNULL(d.deleted, 0) = 0 + ); + +-- ----------------------------------------------------------------------------- +-- 步骤4:插入新条例(区域=宿舍区1,分类=违纪类型1,状态=正常0) +-- ID 区间:2210610150000000001 ~ ...(专用号段,避免与雪花ID冲突概率极低) +-- ----------------------------------------------------------------------------- +INSERT INTO xskh_discipline_type ( + id, type_code, type_name, type_category, area_type, remark, + min_score, max_score, is_equalized, equalized_threshold, status, sort_order, + create_by, create_time, update_by, update_time, create_dept, deleted, tenant_id +) VALUES +(2210610150000000001, 'sskf202610_ws_01', '地面未拖扫', '1', '1', '卫生方面', 0.50, 0.50, '0', NULL, '0', 1, 1, NOW(), 1, NOW(), 103, 0, '000000'), +(2210610150000000002, 'sskf202610_ws_02', '床沿、床边、床内乱挂', '1', '1', '卫生方面', 0.50, 3.00, '0', NULL, '0', 2, 1, NOW(), 1, NOW(), 103, 0, '000000'), +(2210610150000000003, 'sskf202610_ws_03', '被子质量、摆放问题', '1', '1', '卫生方面', 0.50, 3.00, '0', NULL, '0', 3, 1, NOW(), 1, NOW(), 103, 0, '000000'), +(2210610150000000004, 'sskf202610_ws_04', '床铺未整齐', '1', '1', '卫生方面', 0.50, 3.00, '0', NULL, '0', 4, 1, NOW(), 1, NOW(), 103, 0, '000000'), +(2210610150000000005, 'sskf202610_ws_05', '鞋子未摆放整齐', '1', '1', '卫生方面', 0.50, 1.00, '0', NULL, '0', 5, 1, NOW(), 1, NOW(), 103, 0, '000000'), +(2210610150000000006, 'sskf202610_ws_06', '椅子未摆放到位', '1', '1', '卫生方面', 0.50, 3.00, '0', NULL, '0', 6, 1, NOW(), 1, NOW(), 103, 0, '000000'), +(2210610150000000007, 'sskf202610_ws_07', '桌面有污渍、灰尘', '1', '1', '卫生方面', 0.50, 3.00, '0', NULL, '0', 7, 1, NOW(), 1, NOW(), 103, 0, '000000'), +(2210610150000000008, 'sskf202610_ws_08', '桌面摆放不整齐', '1', '1', '卫生方面', 0.50, 3.00, '0', NULL, '0', 8, 1, NOW(), 1, NOW(), 103, 0, '000000'), +(2210610150000000009, 'sskf202610_ws_09', '公区未打扫', '1', '1', '卫生方面', 0.50, 0.50, '0', NULL, '0', 9, 1, NOW(), 1, NOW(), 103, 0, '000000'), +(2210610150000000010, 'sskf202610_ws_10', '劳动工具未摆放好', '1', '1', '卫生方面', 0.50, 0.50, '0', NULL, '0', 10, 1, NOW(), 1, NOW(), 103, 0, '000000'), +(2210610150000000011, 'sskf202610_ws_11', '毛巾未挂齐', '1', '1', '卫生方面', 0.50, 0.50, '0', NULL, '0', 11, 1, NOW(), 1, NOW(), 103, 0, '000000'), +(2210610150000000012, 'sskf202610_ws_12', '椅地床鞋架脸盆架阳台有杂物', '1', '1', '卫生方面;椅子/地面/床铺/鞋架/脸盆架/阳台有杂物', 0.50, 3.00, '0', NULL, '0', 12, 1, NOW(), 1, NOW(), 103, 0, '000000'), +(2210610150000000013, 'sskf202610_ws_13', '垃圾桶未倒', '1', '1', '卫生方面', 0.50, 0.50, '0', NULL, '0', 13, 1, NOW(), 1, NOW(), 103, 0, '000000'), +(2210610150000000014, 'sskf202610_ws_14', '便池有污垢', '1', '1', '卫生方面', 0.50, 0.50, '0', NULL, '0', 14, 1, NOW(), 1, NOW(), 103, 0, '000000'), +(2210610150000000015, 'sskf202610_ws_15', '箱包未摆放整齐或未在指定位置', '1', '1', '卫生方面', 0.50, 0.50, '0', NULL, '0', 15, 1, NOW(), 1, NOW(), 103, 0, '000000'), +(2210610150000000016, 'sskf202610_ws_16', '柜子底部收纳不整齐', '1', '1', '卫生方面', 0.50, 3.00, '0', NULL, '0', 16, 1, NOW(), 1, NOW(), 103, 0, '000000'), +(2210610150000000017, 'sskf202610_ws_17', '蚊帐未拉起', '1', '1', '卫生方面', 0.50, 3.00, '0', NULL, '0', 17, 1, NOW(), 1, NOW(), 103, 0, '000000'), +(2210610150000000018, 'sskf202610_ws_18', '洗脸台摆放', '1', '1', '卫生方面', 0.50, 0.50, '0', NULL, '0', 18, 1, NOW(), 1, NOW(), 103, 0, '000000'), +(2210610150000000019, 'sskf202610_ws_19', '洗脸台有水渍、垃圾', '1', '1', '卫生方面', 0.50, 0.50, '0', NULL, '0', 19, 1, NOW(), 1, NOW(), 103, 0, '000000'), +(2210610150000000020, 'sskf202610_ws_20', '镜子有明显水渍', '1', '1', '卫生方面', 0.50, 0.50, '0', NULL, '0', 20, 1, NOW(), 1, NOW(), 103, 0, '000000'), +(2210610150000000021, 'sskf202610_ws_21', '墙壁柜子乱贴乱画', '1', '1', '卫生方面', 0.50, 0.50, '0', NULL, '0', 21, 1, NOW(), 1, NOW(), 103, 0, '000000'), +(2210610150000000022, 'sskf202610_jl_01', '晚出寝', '1', '1', '纪律方面', 0.50, 3.00, '0', NULL, '0', 22, 1, NOW(), 1, NOW(), 103, 0, '000000'), +(2210610150000000023, 'sskf202610_jl_02', '就寝未熄灯', '1', '1', '纪律方面', 0.50, 0.50, '0', NULL, '0', 23, 1, NOW(), 1, NOW(), 103, 0, '000000'), +(2210610150000000024, 'sskf202610_jl_03', '就寝纪律差', '1', '1', '纪律方面', 0.50, 0.50, '0', NULL, '0', 24, 1, NOW(), 1, NOW(), 103, 0, '000000'), +(2210610150000000025, 'sskf202610_jl_04', '卫生间电灯未关', '1', '1', '纪律方面', 0.50, 0.50, '0', NULL, '0', 25, 1, NOW(), 1, NOW(), 103, 0, '000000'), +(2210610150000000026, 'sskf202610_jl_05', '走道上方电灯未关', '1', '1', '纪律方面', 0.50, 0.50, '0', NULL, '0', 26, 1, NOW(), 1, NOW(), 103, 0, '000000'), +(2210610150000000027, 'sskf202610_jl_06', '书桌电灯未关', '1', '1', '纪律方面', 0.50, 3.00, '0', NULL, '0', 27, 1, NOW(), 1, NOW(), 103, 0, '000000'), +(2210610150000000028, 'sskf202610_jl_07', '空调未关', '1', '1', '纪律方面', 2.00, 2.00, '0', NULL, '0', 28, 1, NOW(), 1, NOW(), 103, 0, '000000'), +(2210610150000000029, 'sskf202610_jl_08', '上周五备忘录晚交', '1', '1', '纪律方面', 0.50, 0.50, '0', NULL, '0', 29, 1, NOW(), 1, NOW(), 103, 0, '000000'), +(2210610150000000030, 'sskf202610_jl_09', '没有电脑进寝单', '1', '1', '纪律方面', 2.00, 12.00, '0', NULL, '0', 30, 1, NOW(), 1, NOW(), 103, 0, '000000'), +(2210610150000000031, 'sskf202610_jl_10', '阳台电灯未关', '1', '1', '纪律方面', 0.50, 0.50, '0', NULL, '0', 31, 1, NOW(), 1, NOW(), 103, 0, '000000'), +(2210610150000000032, 'sskf202610_jl_11', '熄灯串寝', '1', '1', '纪律方面', 0.50, 3.00, '0', NULL, '0', 32, 1, NOW(), 1, NOW(), 103, 0, '000000'), +(2210610150000000033, 'sskf202610_jl_12', '晚归', '1', '1', '纪律方面', 0.50, 3.00, '0', NULL, '0', 33, 1, NOW(), 1, NOW(), 103, 0, '000000'), +(2210610150000000034, 'sskf202610_jl_13', '住其他寝室(未报备)', '1', '1', '纪律方面', 1.00, 6.00, '0', NULL, '0', 34, 1, NOW(), 1, NOW(), 103, 0, '000000'), +(2210610150000000035, 'sskf202610_jl_14', '带外卖进寝室', '1', '1', '纪律方面', 1.00, 6.00, '0', NULL, '0', 35, 1, NOW(), 1, NOW(), 103, 0, '000000'), +(2210610150000000036, 'sskf202610_jl_15', '往窗外扔垃圾', '1', '1', '纪律方面', 1.00, 6.00, '0', NULL, '0', 36, 1, NOW(), 1, NOW(), 103, 0, '000000'), +(2210610150000000037, 'sskf202610_jl_16', '猫眼堵住', '1', '1', '纪律方面', 0.50, 0.50, '0', NULL, '0', 37, 1, NOW(), 1, NOW(), 103, 0, '000000'), +(2210610150000000038, 'sskf202610_zd_01', '烟头、香烟、烟盒', '1', '1', '重大违纪', 0.50, 30.00, '0', NULL, '0', 38, 1, NOW(), 1, NOW(), 103, 0, '000000'), +(2210610150000000039, 'sskf202610_zd_02', '抽烟', '1', '1', '重大违纪', 2.00, 12.00, '0', NULL, '0', 39, 1, NOW(), 1, NOW(), 103, 0, '000000'), +(2210610150000000040, 'sskf202610_zd_03', '插座未拔、私拉电线、私装插座', '1', '1', '重大违纪', 0.50, 12.00, '0', NULL, '0', 40, 1, NOW(), 1, NOW(), 103, 0, '000000'), +(2210610150000000041, 'sskf202610_zd_04', '对消防产生隐患行为', '1', '1', '重大违纪', 2.00, 12.00, '0', NULL, '0', 41, 1, NOW(), 1, NOW(), 103, 0, '000000'), +(2210610150000000042, 'sskf202610_zd_05', '喝酒、酗酒', '1', '1', '重大违纪', 2.00, 12.00, '0', NULL, '0', 42, 1, NOW(), 1, NOW(), 103, 0, '000000'), +(2210610150000000043, 'sskf202610_zd_06', '酒瓶', '1', '1', '重大违纪', 0.50, 30.00, '0', NULL, '0', 43, 1, NOW(), 1, NOW(), 103, 0, '000000'), +(2210610150000000044, 'sskf202610_zd_07', '扑克牌', '1', '1', '重大违纪', 0.50, 0.50, '0', NULL, '0', 44, 1, NOW(), 1, NOW(), 103, 0, '000000'), +(2210610150000000045, 'sskf202610_zd_08', '违禁电器、管制刀具等违禁物品', '1', '1', '重大违纪', 2.00, 12.00, '0', NULL, '0', 45, 1, NOW(), 1, NOW(), 103, 0, '000000'), +(2210610150000000046, 'sskf202610_zd_09', '不服从管理、有顶撞、对抗行为', '1', '1', '重大违纪', 2.00, 12.00, '0', NULL, '0', 46, 1, NOW(), 1, NOW(), 103, 0, '000000'), +(2210610150000000047, 'sskf202610_zd_10', '存放易燃易爆物品', '1', '1', '重大违纪', 2.00, 12.00, '0', NULL, '0', 47, 1, NOW(), 1, NOW(), 103, 0, '000000'), +(2210610150000000048, 'sskf202610_zd_11', '影响公共安全稳定', '1', '1', '重大违纪', 2.00, 2.00, '0', NULL, '0', 48, 1, NOW(), 1, NOW(), 103, 0, '000000'), +(2210610150000000049, 'sskf202610_zd_12', '留宿外人', '1', '1', '重大违纪', 2.00, 2.00, '0', NULL, '0', 49, 1, NOW(), 1, NOW(), 103, 0, '000000'), +(2210610150000000050, 'sskf202610_zd_13', '破坏寝室公物', '1', '1', '重大违纪', 2.00, 2.00, '0', NULL, '0', 50, 1, NOW(), 1, NOW(), 103, 0, '000000'), +(2210610150000000051, 'sskf202610_zd_14', '赌博', '1', '1', '重大违纪', 2.00, 2.00, '0', NULL, '0', 51, 1, NOW(), 1, NOW(), 103, 0, '000000'), +(2210610150000000052, 'sskf202610_zd_15', '进入异性寝室', '1', '1', '重大违纪', 2.00, 2.00, '0', NULL, '0', 52, 1, NOW(), 1, NOW(), 103, 0, '000000'), +(2210610150000000053, 'sskf202610_zd_16', '一夜未归', '1', '1', '重大违纪', 2.00, 12.00, '0', NULL, '0', 53, 1, NOW(), 1, NOW(), 103, 0, '000000'), +(2210610150000000054, 'sskf202610_zd_17', '爬围墙、爬围栏', '1', '1', '重大违纪', 2.00, 12.00, '0', NULL, '0', 54, 1, NOW(), 1, NOW(), 103, 0, '000000'), +(2210610150000000055, 'sskf202610_zd_18', '打架斗殴', '1', '1', '重大违纪', 2.00, 12.00, '0', NULL, '0', 55, 1, NOW(), 1, NOW(), 103, 0, '000000'), +(2210610150000000056, 'sskf202610_zd_19', '养宠物', '1', '1', '重大违纪', 2.00, 2.00, '0', NULL, '0', 56, 1, NOW(), 1, NOW(), 103, 0, '000000'), +(2210610150000000057, 'sskf202610_zd_20', '偷窃', '1', '1', '重大违纪', 2.00, 2.00, '0', NULL, '0', 57, 1, NOW(), 1, NOW(), 103, 0, '000000'), +(2210610150000000058, 'sskf202610_zd_21', '吸毒、贩毒', '1', '1', '重大违纪;条例原文未写分值,暂按2-12录入,请宿管确认后可改', 2.00, 12.00, '0', NULL, '0', 58, 1, NOW(), 1, NOW(), 103, 0, '000000'), +(2210610150000000059, 'sskf202610_zd_22', '敲诈勒索', '1', '1', '重大违纪', 2.00, 2.00, '0', NULL, '0', 59, 1, NOW(), 1, NOW(), 103, 0, '000000'), +(2210610150000000060, 'sskf202610_zd_23', '焚烧废纸杂物', '1', '1', '重大违纪', 2.00, 2.00, '0', NULL, '0', 60, 1, NOW(), 1, NOW(), 103, 0, '000000'); + +-- ----------------------------------------------------------------------------- +-- 步骤5:执行后核对 +-- ----------------------------------------------------------------------------- +SELECT COUNT(*) AS new_active_dorm_types +FROM xskh_discipline_type +WHERE area_type = '1' AND deleted = 0 AND status = '0' AND type_code LIKE 'sskf202610_%'; + +SELECT type_code, type_name, min_score, max_score, sort_order, remark +FROM xskh_discipline_type +WHERE area_type = '1' AND deleted = 0 AND status = '0' AND type_code LIKE 'sskf202610_%' +ORDER BY sort_order; + +-- 仍启用的旧宿舍区类型(应为0) +SELECT id, type_code, type_name, status +FROM xskh_discipline_type +WHERE area_type = '1' AND deleted = 0 AND status = '0' AND type_code NOT LIKE 'sskf202610_%'; + +-- ============================================================================= +-- 回滚参考(慎用): +-- 1) DELETE FROM xskh_discipline_type WHERE type_code LIKE 'sskf202610_%'; +-- 2) UPDATE xskh_discipline_type t +-- INNER JOIN xskh_discipline_type_bak_20261020 b ON b.id = t.id +-- SET t.status = b.status, t.remark = b.remark, t.update_time = NOW() +-- WHERE t.area_type = '1'; +-- ============================================================================= diff --git a/backend/script/sql/update/update_20261020_dorm_roll_call_holiday_stay.sql b/backend/script/sql/update/update_20261020_dorm_roll_call_holiday_stay.sql new file mode 100644 index 00000000..aa075814 --- /dev/null +++ b/backend/script/sql/update/update_20261020_dorm_roll_call_holiday_stay.sql @@ -0,0 +1,32 @@ +-- 宿舍点名:假期留宿状态与统计 +-- 执行库:hzdyjs_xg + +-- 1. 主表增加假期留宿人数 +ALTER TABLE `xg_dorm_roll_call` + ADD COLUMN `holiday_stay_count` int(11) DEFAULT '0' COMMENT '假期留宿人数' AFTER `leave_count`; + +-- 2. 明细表增加假期留宿关联字段 +ALTER TABLE `xg_dorm_roll_call_detail` + ADD COLUMN `holiday_stay_id` bigint(20) DEFAULT NULL COMMENT '关联假期留宿记录ID' AFTER `leave_end_time`, + ADD COLUMN `holiday_type` varchar(50) DEFAULT NULL COMMENT '假期类型' AFTER `holiday_stay_id`, + ADD COLUMN `holiday_start_date` datetime DEFAULT NULL COMMENT '留宿开始日期' AFTER `holiday_type`, + ADD COLUMN `holiday_end_date` datetime DEFAULT NULL COMMENT '留宿结束日期' AFTER `holiday_start_date`; + +-- 3. 点名状态字典补充「假期留宿」 +INSERT INTO `sys_dict_data` ( + `dict_code`, `tenant_id`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, + `css_class`, `list_class`, `is_default`, `create_dept`, `create_by`, `create_time`, + `update_by`, `update_time`, `remark` +) +SELECT 7463495598327466504, '000000', 5, '假期留宿', '4', 'xg_dorm_roll_call_status', + '', 'primary', 'N', 103, 1, NOW(), 1, NOW(), '假期留宿中' +FROM DUAL +WHERE NOT EXISTS ( + SELECT 1 FROM `sys_dict_data` + WHERE `dict_type` = 'xg_dorm_roll_call_status' AND `dict_value` = '4' +); + +-- 4. 明细状态注释同步(可选,不影响运行) +-- ALTER TABLE `xg_dorm_roll_call_detail` +-- MODIFY COLUMN `roll_call_status` char(1) NOT NULL DEFAULT '0' +-- COMMENT '点名状态(0在寝 1请假 2不在 3空床位 4假期留宿)'; diff --git a/backend/script/sql/update/update_20261020_fix_leave_cancel_status_sync.sql b/backend/script/sql/update/update_20261020_fix_leave_cancel_status_sync.sql new file mode 100644 index 00000000..91bdcd48 --- /dev/null +++ b/backend/script/sql/update/update_20261020_fix_leave_cancel_status_sync.sql @@ -0,0 +1,83 @@ +-- ============================================================================= +-- 修复:请假/销假业务状态与流程实例不同步(含打回后重提残留 back/draft) +-- 日期:2026-10-20 +-- 现象:流程已 waiting、业务表仍为 draft/back,办理页无「审批」按钮 +-- 范围:xg_student_leave.approval_status、xg_student_cancel_leave.cancel_status +-- 及请假表上同步的 cancel_status +-- ============================================================================= +-- 执行顺序:先 SELECT 核对 → 再 UPDATE +-- ============================================================================= + +-- ----------------------------------------------------------------------------- +-- 一、请假:实例 waiting,业务非 waiting(draft/back 等) +-- ----------------------------------------------------------------------------- +SELECT + l.id, + l.student_name, + l.approval_status AS leave_approval_status, + i.id AS instance_id, + i.flow_status, + i.node_name, + l.update_time +FROM xg_student_leave l +INNER JOIN flow_instance i + ON i.business_id = CAST(l.id AS CHAR) + AND i.del_flag = '0' +WHERE l.deleted = 0 + AND i.flow_status = 'waiting' + AND IFNULL(l.approval_status, '') <> 'waiting' +ORDER BY l.update_time DESC; + +UPDATE xg_student_leave l +INNER JOIN flow_instance i + ON i.business_id = CAST(l.id AS CHAR) + AND i.del_flag = '0' +SET l.approval_status = 'waiting', + l.update_time = NOW() +WHERE l.deleted = 0 + AND i.flow_status = 'waiting' + AND IFNULL(l.approval_status, '') <> 'waiting'; + +-- ----------------------------------------------------------------------------- +-- 二、销假:实例 waiting,销假表状态非 waiting +-- ----------------------------------------------------------------------------- +SELECT + c.id AS cancel_leave_id, + c.leave_id, + c.cancel_status AS cancel_leave_status, + i.id AS instance_id, + i.flow_status, + i.node_name, + c.update_time +FROM xg_student_cancel_leave c +INNER JOIN flow_instance i + ON i.business_id = CAST(c.id AS CHAR) + AND i.del_flag = '0' +WHERE c.deleted = 0 + AND i.flow_status = 'waiting' + AND IFNULL(c.cancel_status, '') <> 'waiting' +ORDER BY c.update_time DESC; + +UPDATE xg_student_cancel_leave c +INNER JOIN flow_instance i + ON i.business_id = CAST(c.id AS CHAR) + AND i.del_flag = '0' +SET c.cancel_status = 'waiting', + c.update_time = NOW() +WHERE c.deleted = 0 + AND i.flow_status = 'waiting' + AND IFNULL(c.cancel_status, '') <> 'waiting'; + +-- 同步请假表上的销假状态 +UPDATE xg_student_leave l +INNER JOIN xg_student_cancel_leave c + ON c.leave_id = l.id + AND c.deleted = 0 +INNER JOIN flow_instance i + ON i.business_id = CAST(c.id AS CHAR) + AND i.del_flag = '0' +SET l.cancel_status = 'waiting', + l.update_time = NOW() +WHERE l.deleted = 0 + AND i.flow_status = 'waiting' + AND IFNULL(l.cancel_status, '') <> 'waiting'; diff --git a/docs/开发文档/API/宿舍点名假期留宿标识说明.md b/docs/开发文档/API/宿舍点名假期留宿标识说明.md new file mode 100644 index 00000000..65cd99f7 --- /dev/null +++ b/docs/开发文档/API/宿舍点名假期留宿标识说明.md @@ -0,0 +1,57 @@ +# 宿舍点名 · 假期留宿标识说明 + +## 背景 + +宿舍点名原先仅对「当前有效请假」自动标识为请假(状态 `1`,不可修改)。 +已通过审批且仍在留宿日期内的学生,点名界面会误显示为「在寝」。现已补充「假期留宿」标识与独立统计。 + +## 有效留宿判定(后端统一) + +接口:`getRoomStudents` 在返回学生信息时附加 `holidayStayInfo`(有有效请假时不返回,请假优先)。 + +条件: + +1. `apply_status = '2'`(已通过) +2. 检查日落在 `[apply_start_date, apply_end_date]`(按日期比较,**结束日当天整天有效**) +3. 同一学生多条记录时取最新一条 + +假期结束后,接口不再返回 `holidayStayInfo`,前端恢复默认「在寝」可切换。 + +## 点名状态字典 `xg_dorm_roll_call_status` + +| 值 | 含义 | +|----|------| +| 0 | 在寝 | +| 1 | 请假 | +| 2 | 不在 | +| 3 | 空床位 | +| 4 | 假期留宿 | + +## 统计字段 + +主表 `xg_dorm_roll_call.holiday_stay_count`:本房间假期留宿人数。 +明细可存 `holiday_stay_id` / `holiday_type` / `holiday_start_date` / `holiday_end_date`。 + +## 交互 + +与请假一致:自动标为假期留宿后**不可修改**。 + +## 库表脚本 + +`backend/script/sql/update/update_20261020_dorm_roll_call_holiday_stay.sql` + +部署时需先执行该脚本,再发布后端与前端。 + +## 当天点名汇总(移动端首页) + +接口:`GET /dormRollCall/dormRollCall/todaySummary` +权限:`dormRollCall:dormRollCall:list1` + +口径: + +- **已点名寝室**:当前账号当天有效点名记录的去重房间数(同房多次取最新) +- **未点名寝室**:管辖楼栋(宿舍管理员权限)∪ 当天已点过的楼栋 下可用房间 − 已点名 +- **请假 / 假期留宿 / 异常**:最新点名明细中状态 `1` / `4` / `2`,可点开看明细 + +仅供移动端首页展示,不影响点名提交与 Web 列表。 + diff --git a/docs/开发文档/寝室检查扣分条例生产执行说明-20261020.md b/docs/开发文档/寝室检查扣分条例生产执行说明-20261020.md new file mode 100644 index 00000000..d7008fbb --- /dev/null +++ b/docs/开发文档/寝室检查扣分条例生产执行说明-20261020.md @@ -0,0 +1,137 @@ +# 寝室检查扣分条例生产执行说明(2026-10-20) + +## 目标 + +按宿管《扣分条例10月15日》,把寝室卫生检查可选扣分项切换为新条例(共 **60** 项)。 + +系统加载规则:寝室检查调用 `listByAreaType?areaType=1`,只显示 +`xskh_discipline_type` 中 **区域类型=宿舍区(1)** 且 **状态=正常(0)** 的违纪类型。 + +**不需要发版**,只改生产库数据。 + +--- + +## 交付文件 + +| 文件 | 说明 | +|------|------| +| `backend/script/sql/update/update_20261020_dorm_hygiene_deduction_types.sql` | 备份 → 停用旧宿舍区类型 → 插入新条例 | +| 本文档 | 生产执行步骤与验收 | + +--- + +## 执行前确认 + +1. 已与宿管确认《扣分条例10月15日》为现行标准。 +2. **「吸毒、贩毒」** 原文无分值,脚本暂按 **2–12** 录入(见该条 `remark`),确认后可改。 +3. 有生产库备份权限;建议低峰执行。 +4. 准备好可回滚窗口(约 10–15 分钟)。 + +--- + +## 推荐执行步骤 + +### 第 1 步:全库备份(运维) + +按现有规范做一次生产库逻辑备份 / 快照(至少包含 `xskh_discipline_type`)。 + +### 第 2 步:在测试库先跑一遍(强烈建议) + +```bash +# 示例:在测试库执行整份脚本 +mysql -h<测试主机> -u<用户> -p <库名> < backend/script/sql/update/update_20261020_dorm_hygiene_deduction_types.sql +``` + +核对输出: + +- `bak_dorm_type_count` > 0(若原先没有宿舍区类型,可为 0) +- `new_active_dorm_types` = **60** +- 「仍启用的旧宿舍区类型」查询结果为 **空** + +测试环境打开:寝室卫生检查 → 新建检查 → 扣分项列表应为新 60 项。 + +### 第 3 步:生产执行 + +1. 登录生产库(只读账号先跑「步骤1核对」SELECT,确认现状)。 +2. 用有写权限账号**整文件执行**: + +```bash +mysql -h<生产主机> -u<用户> -p <生产库名> < backend/script/sql/update/update_20261020_dorm_hygiene_deduction_types.sql +``` + +或在 Navicat / DBeaver 中打开该 SQL,**按步骤分段执行**(0→1→2→3→4→5),每步看结果再继续。 + +3. 确认: + +```sql +SELECT COUNT(*) AS new_active_dorm_types +FROM xskh_discipline_type +WHERE area_type = '1' AND deleted = 0 AND status = '0' + AND type_code LIKE 'sskf202610_%'; +-- 期望:60 +``` + +### 第 4 步:业务验收 + +1. Web:**学生考核 → 违纪类型**,筛「宿舍区」,应看到 60 条新类型(编码 `sskf202610_*`)。 +2. Web / 移动端:**寝室卫生检查** 新建一条,扣分项下拉为新条例;选一项默认分为最低分,可在区间内改。 +3. 旧历史检查记录仍可打开(旧类型已停用但未删)。 + +### 第 5 步:通知宿管 + +告知:从即日起检查按新条例扣分;若某项分值需调整,在「违纪类型」里改最低/最高分即可,不必再跑脚本。 + +--- + +## 脚本做了什么(摘要) + +1. **备份**当前宿舍区类型 → `xskh_discipline_type_bak_20261020` +2. **停用**旧宿舍区启用类型(`status=1`,不删) +3. **删除**未被子检查引用的本批次旧插入(便于重跑) +4. **插入** 60 条新类型(`area_type=1`,`type_category=1`,`status=0`) +5. **核对**查询 + +教学区违纪(`area_type=2`)**不动**。 + +--- + +## 回滚(出问题才用) + +```sql +-- 1. 去掉本次新数据(若尚未被检查引用) +DELETE FROM xskh_discipline_type WHERE type_code LIKE 'sskf202610_%'; + +-- 2. 按备份恢复旧宿舍区类型状态 +UPDATE xskh_discipline_type t +INNER JOIN xskh_discipline_type_bak_20261020 b ON b.id = t.id +SET t.status = b.status, + t.remark = b.remark, + t.update_time = NOW() +WHERE t.area_type = '1'; +``` + +若新类型已被检查使用,不要 DELETE,改为把新类型 `status` 置停用,再恢复旧类型。 + +--- + +## 分类与条数 + +| 分类 | 编码前缀 | 条数 | +|------|----------|------| +| 卫生方面 | `sskf202610_ws_*` | 21 | +| 纪律方面 | `sskf202610_jl_*` | 16 | +| 重大违纪 | `sskf202610_zd_*` | 23 | +| **合计** | | **60** | + +--- + +## 常见问题 + +**Q:执行后检查页还是旧项?** +刷新页面或重新进入检查;确认接口 `listByAreaType?areaType=1` 返回新数据。 + +**Q:能否只改几条、不全量切换?** +可以,改在后台「违纪类型」手工增删改;本脚本适合一次性对齐条例。 + +**Q:历史扣分记录会变吗?** +不会。历史明细仍引用旧 `discipline_type_id`,只是旧类型被停用,新检查不再可选。 diff --git a/docs/开发文档/请假销假审批按钮修复说明.md b/docs/开发文档/请假销假审批按钮修复说明.md new file mode 100644 index 00000000..d9dadb1e --- /dev/null +++ b/docs/开发文档/请假销假审批按钮修复说明.md @@ -0,0 +1,20 @@ +# 请假/销假审批按钮修复说明(方案 B) + +## 问题 + +办理页「审批」按钮依赖 `pageType=approval && status=waiting`。 +打回重提后,流程实例已是 `waiting`,业务表常残留 `draft`/`back`,老师看不到审批按钮。 + +## 方案 + +1. **后端权威同步**:`LeaveFlowStatusSyncHelper` 在流程事件/任务创建时,按流程实例状态对齐请假 `approval_status`、销假 `cancel_status`(实例 waiting 则强制 waiting;禁止 waiting→draft)。 +2. **审批上下文接口**:`GET /workflow/instance/approvalContext/{businessId}` + 返回 `canApprove`、`taskId`、`suggestedBusinessStatus`。 +3. **按钮判断**:`canApprove=true` 即可显示审批(不再只绑死业务 status)。 +4. **存量修复**:`backend/script/sql/update/update_20261020_fix_leave_cancel_status_sync.sql` + +## 影响范围 + +- 请假/销假办理页(Web + 移动端) +- 通用 `approvalButton`(新增可选 `canApprove`,默认 false,其它模块不受影响) +- 不改 Warm-Flow 全局监听器 diff --git a/mobile/src/api/dormRollCall.js b/mobile/src/api/dormRollCall.js index 129ba7a4..2d617fe2 100644 --- a/mobile/src/api/dormRollCall.js +++ b/mobile/src/api/dormRollCall.js @@ -8,6 +8,14 @@ export const listDormRollCall = (query) => { }); }; +/** 当前账号当天点名汇总 */ +export const getTodayRollCallSummary = () => { + return request({ + url: '/dormRollCall/dormRollCall/todaySummary', + method: 'GET' + }); +}; + export const getDormRollCall = (id) => { return request({ url: '/dormRollCall/dormRollCall/' + id, diff --git a/mobile/src/components/Process/approvalButton.vue b/mobile/src/components/Process/approvalButton.vue index f6a73bf9..556479ea 100644 --- a/mobile/src/components/Process/approvalButton.vue +++ b/mobile/src/components/Process/approvalButton.vue @@ -69,6 +69,11 @@ export default { type: [String, Number], default: '', }, + /** 当前用户是否有可办审批待办(优先于 status===waiting) */ + canApprove: { + type: Boolean, + default: false, + }, }, computed: { // 校验提交按钮是否显示(与Web端逻辑完全一致) @@ -81,8 +86,11 @@ export default { ); }, - // 校验审批按钮是否显示(与Web端逻辑完全一致) + // 审批按钮:有待办可审,或传统 pageType=approval + waiting approvalButtonShow() { + if (this.canApprove) { + return true; + } return this.pageType === 'approval' && this.status && this.status === 'waiting'; }, }, diff --git a/mobile/src/pages/task/taskWaiting.vue b/mobile/src/pages/task/taskWaiting.vue index 941138d1..e8b6faa9 100644 --- a/mobile/src/pages/task/taskWaiting.vue +++ b/mobile/src/pages/task/taskWaiting.vue @@ -127,20 +127,41 @@ - - - 办理 - + + + + 办理 + + + + + + + 审批 + + @@ -231,15 +252,22 @@ + + +