feat(xg): 请假审批修复、点名今日汇总与寝室扣分条例脚本

同步请假/销假审批上下文与状态、宿舍点名今日汇总与假期留宿标识,并补充寝室检查扣分条例生产 SQL 与执行说明。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
“wuzhen”andCursor committed 2026-10-20 18:01:12 +08:00
1 parent 3e6349112c
commit 7cea0566ac
48 files changed
+1966 -116

No files matched your search

@@ -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<FlowApprovalContextVo> approvalContext(@PathVariable("businessId") String businessId) {
return R.ok(flwInstanceService.approvalContext(businessId));
}
/**
* 获取流程变量
*
@@ -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;
}
@@ -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<String, Object> flowHisTaskList(String businessId);
/**
* 办理页审批上下文(是否可审批、taskId、流程状态)
*
* @param businessId 业务id
* @return 审批上下文;无实例时 canApprove=false
*/
FlowApprovalContextVo approvalContext(String businessId);
/**
* 按照实例id更新状态
*
@@ -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<FlowTask> 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<UserDTO> 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<String, Object> flowHisTaskList(String businessId) {
FlowInstance flowInstance = this.selectInstByBusinessId(businessId);
@@ -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<DormRollCallTodaySummaryVo> todaySummary() {
return R.ok(xgDormRollCallService.queryTodaySummary());
}
@SaCheckPermission("dormRollCall:dormRollCall:query")
@GetMapping("/{id}")
public R<XgDormRollCallVo> getInfo(@NotNull(message = "主键不能为空") @PathVariable("id") Long id) {
@@ -54,6 +54,8 @@ public class XgDormRollCall extends TenantEntity {
private Integer leaveCount;
private Integer holidayStayCount;
private Integer absentCount;
private Integer emptyCount;
@@ -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;
}
@@ -54,6 +54,8 @@ public class XgDormRollCallBo extends BaseEntity {
private Integer leaveCount;
private Integer holidayStayCount;
private Integer absentCount;
private Integer emptyCount;
@@ -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;
}
@@ -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;
/**
* 销假时间
*/
@@ -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;
}
@@ -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<DormRollCallTodayDetailItemVo> leaveDetails = new ArrayList<>();
private List<DormRollCallTodayDetailItemVo> holidayStayDetails = new ArrayList<>();
private List<DormRollCallTodayDetailItemVo> absentDetails = new ArrayList<>();
}
@@ -23,6 +23,8 @@ public class RoomRollCallStatusVo implements Serializable {
private Integer leaveCount;
private Integer holidayStayCount;
private Integer absentCount;
private Integer emptyCount;
@@ -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;
}
@@ -50,6 +50,8 @@ public class XgDormRollCallVo implements Serializable {
private Integer leaveCount;
private Integer holidayStayCount;
private Integer absentCount;
private Integer emptyCount;
@@ -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;
/**
* 请假/销假业务状态与流程引擎对齐。
* <p>
* 规则:
* 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<String> 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<FlowTask> tasks = flwTaskService.selectByInstId(instanceId);
return tasks != null && !tasks.isEmpty();
} catch (Exception e) {
log.warn("查询运行中任务失败, businessId={}, err={}", businessId, e.getMessage());
return false;
}
}
}
@@ -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<XgDormRollCallVo> queryRoomHistory(Long roomId, PageQuery pageQuery);
/**
* 当前登录账号当天点名汇总(已/未点名寝室 + 请假/留宿/异常明细)
*/
DormRollCallTodaySummaryVo queryTodaySummary();
}
@@ -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<Long, XgHolidayStayVo> queryStudentHolidayStaysByCheckDate(List<Long> studentIds, LocalDateTime checkDate);
}
@@ -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<IXgHolidayStayService> xgHolidayStayServiceProvider;
private final FreshmanCollegeProxyHelper freshmanCollegeProxyHelper;
private final CurrentUserCollegeHelper currentUserCollegeHelper;
@@ -2489,7 +2494,7 @@ public class XgDormBedStudentServiceImpl implements IXgDormBedStudentService {
/**
* 根据房间ID获取住在当前房间的所有学生信息
* 包含学生姓名、学号、性别、学院名称、专业名称、班级名称、床位编码、床位ID
* 同时查询当前时间有效的请假信息
* 同时查询当前时间有效的请假信息、假期留宿信息
*/
@Override
public List<Object> getRoomStudents(Long roomId) {
@@ -2517,10 +2522,15 @@ public class XgDormBedStudentServiceImpl implements IXgDormBedStudentService {
.filter(Objects::nonNull)
.collect(Collectors.toList());
LocalDateTime now = LocalDateTime.now();
Map<Long, XgStudentLeaveVo> leaveMap = Map.of();
Map<Long, XgHolidayStayVo> 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<Object> 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<String, Object> 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<String, Object> 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<String, Object> 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);
}
}
@@ -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<XgDormRollCall> todayRecords = baseMapper.selectList(
Wrappers.<XgDormRollCall>lambdaQuery()
.eq(XgDormRollCall::getCheckerId, checkerId)
.eq(XgDormRollCall::getStatus, "0")
.ge(XgDormRollCall::getCheckDate, dayBegin)
.le(XgDormRollCall::getCheckDate, dayEnd)
.orderByDesc(XgDormRollCall::getCheckDate)
);
// 同一寝室多次点名时取最新一条,避免明细重复统计
Map<Long, XgDormRollCall> latestByRoom = new LinkedHashMap<>();
for (XgDormRollCall record : todayRecords) {
if (record.getRoomId() == null) {
continue;
}
latestByRoom.putIfAbsent(record.getRoomId(), record);
}
Set<Long> checkedRoomIds = latestByRoom.keySet();
summary.setCheckedRoomCount(checkedRoomIds.size());
Set<Long> scopeBuildingIds = resolveManagedBuildingIds(checkerId);
for (XgDormRollCall record : latestByRoom.values()) {
if (record.getBuildingId() != null) {
scopeBuildingIds.add(record.getBuildingId());
}
}
if (!scopeBuildingIds.isEmpty()) {
List<XgDormRoom> scopeRooms = roomMapper.selectList(
Wrappers.<XgDormRoom>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<Long> rollCallIds = latestByRoom.values().stream()
.map(XgDormRollCall::getId)
.filter(Objects::nonNull)
.collect(Collectors.toList());
if (rollCallIds.isEmpty()) {
return summary;
}
Map<Long, XgDormRollCall> rollCallMap = latestByRoom.values().stream()
.collect(Collectors.toMap(XgDormRollCall::getId, r -> r, (a, b) -> a));
List<XgDormRollCallDetail> details = detailMapper.selectList(
Wrappers.<XgDormRollCallDetail>lambdaQuery()
.in(XgDormRollCallDetail::getRollCallId, rollCallIds)
.in(XgDormRollCallDetail::getRollCallStatus, Arrays.asList("1", "2", "4"))
.isNotNull(XgDormRollCallDetail::getStudentId)
.orderByAsc(XgDormRollCallDetail::getBedCode)
);
List<DormRollCallTodayDetailItemVo> leaveDetails = new ArrayList<>();
List<DormRollCallTodayDetailItemVo> holidayStayDetails = new ArrayList<>();
List<DormRollCallTodayDetailItemVo> 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<Long> resolveManagedBuildingIds(Long userId) {
Set<Long> buildingIds = new HashSet<>();
if (userId == null) {
return buildingIds;
}
List<XgDormManager> managers = dormManagerMapper.selectList(
Wrappers.<XgDormManager>lambdaQuery()
.eq(XgDormManager::getUserId, userId)
.eq(XgDormManager::getStatus, "0")
);
if (managers == null || managers.isEmpty()) {
return buildingIds;
}
List<Long> managerIds = managers.stream().map(XgDormManager::getId).collect(Collectors.toList());
List<XgDormManagerBuilding> managerBuildings = dormManagerBuildingMapper.selectList(
Wrappers.<XgDormManagerBuilding>lambdaQuery()
.in(XgDormManagerBuilding::getManagerId, managerIds)
.eq(XgDormManagerBuilding::getStatus, "0")
);
for (XgDormManagerBuilding mb : managerBuildings) {
if (mb.getBuildingId() != null) {
buildingIds.add(mb.getBuildingId());
}
}
List<XgDormManagerSource> sources = dormManagerSourceMapper.selectList(
Wrappers.<XgDormManagerSource>lambdaQuery()
.in(XgDormManagerSource::getManagerId, managerIds)
.eq(XgDormManagerSource::getStatus, "0")
);
Set<Long> campusIds = new HashSet<>();
Set<Long> 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<XgDormBuilding> allBuildings = buildingMapper.selectList(
Wrappers.<XgDormBuilding>lambdaQuery().select(XgDormBuilding::getId)
);
for (XgDormBuilding b : allBuildings) {
if (b.getId() != null) {
buildingIds.add(b.getId());
}
}
return buildingIds;
}
if (!campusIds.isEmpty() || !areaIds.isEmpty()) {
LambdaQueryWrapper<XgDormBuilding> buildingLqw = Wrappers.<XgDormBuilding>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<XgDormBuilding> buildings = buildingMapper.selectList(buildingLqw);
for (XgDormBuilding b : buildings) {
if (b.getId() != null) {
buildingIds.add(b.getId());
}
}
}
return buildingIds;
}
private LambdaQueryWrapper<XgDormRollCall> buildQueryWrapper(XgDormRollCallBo bo) {
LambdaQueryWrapper<XgDormRollCall> lqw = Wrappers.<XgDormRollCall>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);
}
}
@@ -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<Long, XgHolidayStayVo> queryStudentHolidayStaysByCheckDate(List<Long> 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<XgHolidayStay> 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<XgHolidayStay> stayList = baseMapper.selectList(lqw);
return stayList.stream()
.collect(Collectors.toMap(
XgHolidayStay::getStudentId,
stay -> MapstructUtils.convert(stay, XgHolidayStayVo.class),
(existing, replacement) -> existing
));
}
}
@@ -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<XgStudentLeave> 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<String, Object> 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<String, Object> 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);
}
/**
@@ -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';
-- =============================================================================
@@ -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假期留宿)';
@@ -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';
@@ -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 列表。
@@ -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`,只是旧类型被停用,新检查不再可选。
@@ -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 全局监听器
+8
View File
@@ -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,
@@ -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';
},
},
+74 -15
View File
@@ -127,20 +127,41 @@
<!-- 操作按钮 -->
<view class="card-actions">
<u-button
@click="handleApproval(item)"
type="primary"
size="small"
shape="circle"
>
<u-icon
name="edit-pen"
size="16"
color="#fff"
style="margin-right: 8rpx"
></u-icon>
办理
</u-button>
<view class="action-btn">
<u-button
@click.stop="handleApproval(item)"
type="primary"
size="small"
shape="circle"
:custom-style="actionButtonStyle"
>
<u-icon
name="edit-pen"
size="16"
color="#fff"
style="margin-right: 8rpx"
></u-icon>
办理
</u-button>
</view>
<!-- 列表直接审批:仅本页弹窗,不跳转业务页 -->
<view class="action-btn">
<u-button
@click.stop="handleDirectApprove(item)"
type="warning"
size="small"
shape="circle"
:custom-style="actionButtonStyle"
>
<u-icon
name="checkmark"
size="16"
color="#fff"
style="margin-right: 8rpx"
></u-icon>
审批
</u-button>
</view>
</view>
</view>
</view>
@@ -231,15 +252,22 @@
</view>
</view>
</u-popup>
<!-- 待办直接审批:与业务表单里的审批弹窗是同一个组件 -->
<submit-verify ref="submitVerifyRef" @submit-callback="handleApproveCallback" />
</view>
</template>
<script>
import { request } from '@/common/request.js';
import { getDict, getDictTagType } from '@/common/dict.js';
import SubmitVerify from '@/components/Process/submitVerify.vue';
export default {
name: 'TaskWaiting',
components: {
SubmitVerify,
},
data() {
return {
loading: false,
@@ -271,6 +299,12 @@ export default {
userSearchKeyword: '', // 用户搜索关键词
userSearchTimer: null, // 搜索防抖定时器
actionButtonStyle: {
width: '100%',
height: '72rpx',
margin: '0',
},
// 用户列表分页参数
userListParams: {
pageNum: 1,
@@ -491,6 +525,25 @@ export default {
}
},
// 直接打开审批弹窗(复用 submitVerify),不进入业务办理页
handleDirectApprove(row) {
if (!row || row.id == null || row.id === '') {
uni.showToast({ title: '缺少任务信息,无法审批', icon: 'none' });
return;
}
const verifyRef = this.$refs.submitVerifyRef;
if (!verifyRef || typeof verifyRef.openDialog !== 'function') {
uni.showToast({ title: '审批组件未就绪', icon: 'none' });
return;
}
verifyRef.openDialog(row.id);
},
// 审批成功:刷新待办列表,停留在本页(不跳转请假列表等业务页)
handleApproveCallback() {
this.handleQuery();
},
// 办理任务
handleApproval(row) {
console.log('办理任务:', row);
@@ -863,11 +916,17 @@ export default {
.card-actions {
display: flex;
justify-content: flex-end;
align-items: center;
gap: 20rpx;
padding: 20rpx;
border-top: 1px solid #f0f0f0;
}
.action-btn {
flex: 1;
min-width: 0;
}
// 申请人选择弹窗样式
.user-select-popup {
background-color: #fff;
@@ -461,6 +461,7 @@
:page-type="routeParams.type"
:task-id="routeParams.taskId"
:button-loading="buttonLoading"
:can-approve="canApprove"
@submitForm="handleSubmitForm"
@approvalVerifyOpen="handleApprovalVerifyOpen"
@handleApprovalRecord="handleApprovalRecord"
@@ -547,6 +548,7 @@ export default {
return {
routeParams: {},
buttonLoading: false,
canApprove: false,
// 流程相关
showFlowSelectionPopup: false,
@@ -718,6 +720,7 @@ export default {
// 如已有销假记录,加载详情
if (this.cancelForm.id) {
await this.getCancelLeaveDetail(this.cancelForm.id);
await this.ensureApprovalCapability(this.cancelForm.id);
} else {
// 用请假记录中的已存在销假信息初始化
Object.assign(this.cancelForm, {
@@ -737,6 +740,37 @@ export default {
}
},
/**
* 以流程引擎待办为准刷新销假审批能力
*/
async ensureApprovalCapability(cancelLeaveId) {
try {
this.canApprove = false;
if (!cancelLeaveId) {
return;
}
const resp = await request({
url: `/workflow/instance/approvalContext/${cancelLeaveId}`,
method: 'GET',
});
const ctx = (resp && resp.data) || {};
if (ctx.suggestedBusinessStatus) {
this.cancelForm.status = ctx.suggestedBusinessStatus;
this.leaveInfo.cancelStatus = ctx.suggestedBusinessStatus;
}
if (ctx.canApprove) {
this.canApprove = true;
this.routeParams = {
...this.routeParams,
type: 'approval',
taskId: ctx.taskId != null ? String(ctx.taskId) : this.routeParams.taskId,
};
}
} catch (e) {
console.warn('销假审批上下文检查失败(不影响查看):', e);
}
},
async getLeaveDetail(id) {
try {
const response = await request({
@@ -145,14 +145,14 @@
<!-- 请假信息 -->
<view class="info-section">
<text class="section-title">请假信息</text>
<!-- 请假开始时间和结束时间分两行显示 -->
<view class="info-item">
<text class="label">开始时间:</text>
<text class="value">{{ item.startTime || '-' }}</text>
<!-- 请假开始时间和结束时间分两行显示,字号加大方便门岗查看 -->
<view class="info-item time-item">
<text class="label time-label">开始时间:</text>
<text class="value time-value">{{ item.startTime || '-' }}</text>
</view>
<view class="info-item">
<text class="label">结束时间:</text>
<text class="value">{{ item.endTime || '-' }}</text>
<view class="info-item time-item">
<text class="label time-label">结束时间:</text>
<text class="value time-value">{{ item.endTime || '-' }}</text>
</view>
<!-- 请假天数和是否离校放一行 -->
<view class="info-item-row">
@@ -359,6 +359,8 @@ export default {
startTime: '',
endTime: '',
approvalStatus: '', // 默认显示全部状态
// 排除销假审批已完成(finish / 旧值 1),仅本页使用
excludeFinishedCancel: true,
},
// 时间显示
@@ -1129,12 +1131,25 @@ export default {
}
}
.time-item {
align-items: center;
background-color: #fff7e6;
border-radius: 8rpx;
padding: 10rpx 12rpx;
}
.label {
font-size: 26rpx;
color: #606266;
min-width: 140rpx;
margin-right: 16rpx;
flex-shrink: 0;
&.time-label {
font-size: 28rpx;
font-weight: 600;
color: #303133;
}
}
.value {
@@ -1165,6 +1180,13 @@ export default {
&.reason {
line-height: 1.6;
}
&.time-value {
font-size: 34rpx;
font-weight: 700;
color: #d4380d;
line-height: 1.4;
}
}
// 操作按钮
@@ -459,6 +459,7 @@
:page-type="routeParams.type"
:task-id="routeParams.taskId"
:button-loading="buttonLoading"
:can-approve="canApprove"
@submitForm="handleSubmitForm"
@approvalVerifyOpen="handleApprovalVerifyOpen"
@handleApprovalRecord="handleApprovalRecord"
@@ -540,7 +541,6 @@ import SubmitVerify from '@/components/Process/submitVerify.vue';
import ApprovalRecord from '@/components/Process/approvalRecord.vue';
import weChatSDK from '@/utils/wechat.js';
import { downloadFile } from '@/common/request.js';
export default {
name: 'LeaveEdit',
components: {
@@ -552,6 +552,7 @@ export default {
return {
routeParams: {},
buttonLoading: false,
canApprove: false,
showLeaveTypePicker: false,
showStartTimePicker: false,
showEndTimePicker: false,
@@ -1071,6 +1072,7 @@ export default {
const leaveInfo = response.data;
Object.assign(this.form, leaveInfo);
this.form.status = leaveInfo.approvalStatus;
await this.ensureApprovalCapability(id);
// 同步时间值到选择器
this.startTimeValue = this.form.startTime
@@ -1099,6 +1101,34 @@ export default {
}
},
/**
* 以流程引擎待办为准刷新审批能力(覆盖 draft/back 残留导致无按钮)
*/
async ensureApprovalCapability(leaveId) {
try {
this.canApprove = false;
const resp = await request({
url: `/workflow/instance/approvalContext/${leaveId}`,
method: 'GET',
});
const ctx = (resp && resp.data) || {};
if (ctx.suggestedBusinessStatus) {
this.form.approvalStatus = ctx.suggestedBusinessStatus;
this.form.status = ctx.suggestedBusinessStatus;
}
if (ctx.canApprove) {
this.canApprove = true;
this.routeParams = {
...this.routeParams,
type: 'approval',
taskId: ctx.taskId != null ? String(ctx.taskId) : this.routeParams.taskId,
};
}
} catch (e) {
console.warn('请假审批上下文检查失败(不影响查看):', e);
}
},
// 获取附件文件
async getAttachmentFiles(ossIds) {
try {
@@ -21,6 +21,9 @@
<view v-if="bed.isLeaveAuto" class="leave-tag">
<u-tag text="请假" type="warning" size="mini"></u-tag>
</view>
<view v-else-if="bed.isHolidayStayAuto" class="leave-tag">
<u-tag text="假期留宿" type="primary" size="mini"></u-tag>
</view>
<view class="status-label" :class="getStatusColorClass(bed.rollCallStatus)">
{{ getStatusLabel(bed.rollCallStatus) }}
</view>
@@ -40,6 +43,10 @@
<text class="summary-label">请假:</text>
<text class="summary-value leave-count">{{ summary.leave }}</text>
</view>
<view class="summary-item">
<text class="summary-label">留宿:</text>
<text class="summary-value holiday-stay-count">{{ summary.holidayStay }}</text>
</view>
<view class="summary-item">
<text class="summary-label">不在:</text>
<text class="summary-value absent-count">{{ summary.absent }}</text>
@@ -53,7 +60,7 @@
<!-- 提示文字 -->
<view v-if="bedDetails.length > 0" class="hint-bar">
<u-icon name="info-circle" color="#909399" size="14"></u-icon>
<text class="hint-text">点击床位切换"在寝"和"不在"状态,请假不可修改</text>
<text class="hint-text">点击床位切换"在寝"和"不在"状态,请假/假期留宿不可修改</text>
</view>
<!-- 提交按钮 -->
@@ -102,15 +109,16 @@ export default {
},
computed: {
summary() {
let present = 0, leave = 0, absent = 0, empty = 0;
let present = 0, leave = 0, holidayStay = 0, absent = 0, empty = 0;
this.bedDetails.forEach((bed) => {
const status = bed.rollCallStatus;
if (status === '0') present++;
else if (status === '1') leave++;
else if (status === '4') holidayStay++;
else if (status === '2') absent++;
else if (status === '3') empty++;
});
return { present, leave, absent, empty };
return { present, leave, holidayStay, absent, empty };
},
},
watch: {
@@ -177,7 +185,7 @@ export default {
}
});
// 5. 映射为统一格式,并处理点名状态与请假信息
// 5. 映射为统一格式,并处理点名状态、请假、假期留宿
this.bedDetails = mergedList.map((item) => {
const bed = {
bedId: item.bedId || item.id,
@@ -194,21 +202,34 @@ export default {
collegeName: item.collegeName,
rollCallStatus: '0',
isLeaveAuto: false,
isHolidayStayAuto: false,
leaveId: null,
leaveTypeName: null,
leaveStartTime: null,
leaveEndTime: null,
holidayStayId: null,
holidayType: null,
holidayStartDate: null,
holidayEndDate: null,
};
if (!bed.studentId) {
bed.rollCallStatus = '3';
} else if (item.leaveInfo) {
// 请假优先
bed.rollCallStatus = '1';
bed.isLeaveAuto = true;
bed.leaveId = item.leaveInfo.leaveId;
bed.leaveTypeName = item.leaveInfo.leaveTypeName;
bed.leaveStartTime = item.leaveInfo.leaveStartTime;
bed.leaveEndTime = item.leaveInfo.leaveEndTime;
} else if (item.holidayStayInfo) {
bed.rollCallStatus = '4';
bed.isHolidayStayAuto = true;
bed.holidayStayId = item.holidayStayInfo.holidayStayId;
bed.holidayType = item.holidayStayInfo.holidayType;
bed.holidayStartDate = item.holidayStayInfo.applyStartDate;
bed.holidayEndDate = item.holidayStayInfo.applyEndDate;
}
return bed;
@@ -231,6 +252,7 @@ export default {
getBedCardClass(bed) {
if (bed.rollCallStatus === '0') return 'bed-present';
if (bed.rollCallStatus === '1') return 'bed-leave';
if (bed.rollCallStatus === '4') return 'bed-holiday-stay';
if (bed.rollCallStatus === '2') return 'bed-absent';
if (bed.rollCallStatus === '3') return 'bed-empty';
return '';
@@ -239,6 +261,7 @@ export default {
getStatusLabel(status) {
if (status === '0') return '在寝';
if (status === '1') return '请假';
if (status === '4') return '假期留宿';
if (status === '2') return '不在';
if (status === '3') return '空床位';
return '';
@@ -247,6 +270,7 @@ export default {
getStatusColorClass(status) {
if (status === '0') return 'color-present';
if (status === '1') return 'color-leave';
if (status === '4') return 'color-holiday-stay';
if (status === '2') return 'color-absent';
if (status === '3') return 'color-empty';
return '';
@@ -257,6 +281,10 @@ export default {
uni.showToast({ title: '该学生处于请假状态,不可修改', icon: 'none' });
return;
}
if (bed.isHolidayStayAuto) {
uni.showToast({ title: '该学生处于假期留宿状态,不可修改', icon: 'none' });
return;
}
if (!bed.studentId) {
return;
}
@@ -297,6 +325,10 @@ export default {
leaveTypeName: bed.leaveTypeName,
leaveStartTime: bed.leaveStartTime,
leaveEndTime: bed.leaveEndTime,
holidayStayId: bed.holidayStayId,
holidayType: bed.holidayType,
holidayStartDate: bed.holidayStartDate,
holidayEndDate: bed.holidayEndDate,
}));
const submitData = {
@@ -308,6 +340,7 @@ export default {
totalBeds: this.bedDetails.length,
presentCount: this.summary.present,
leaveCount: this.summary.leave,
holidayStayCount: this.summary.holidayStay,
absentCount: this.summary.absent,
emptyCount: this.summary.empty,
details,
@@ -364,6 +397,10 @@ export default {
background-color: #fff8e1;
border-color: #e6a23c;
}
.bed-holiday-stay {
background-color: #ecf5ff;
border-color: #409eff;
}
.bed-absent {
background-color: #ffebee;
border-color: #f56c6c;
@@ -419,6 +456,9 @@ export default {
.color-leave {
color: #e6a23c;
}
.color-holiday-stay {
color: #409eff;
}
.color-absent {
color: #f56c6c;
}
@@ -458,6 +498,9 @@ export default {
.leave-count {
color: #e6a23c;
}
.holiday-stay-count {
color: #409eff;
}
.absent-count {
color: #f56c6c;
}
+271 -1
View File
@@ -60,6 +60,39 @@
<u-loading-page :loading="pageLoading && list.length === 0" loading-text="加载中..."></u-loading-page>
<view v-if="!pageLoading || list.length > 0" class="content-container">
<!-- 当天点名汇总 -->
<view class="today-summary-card">
<view class="summary-header">
<text class="summary-title">今日点名汇总</text>
<text class="summary-date">{{ todayLabel }}</text>
</view>
<view class="summary-grid">
<view class="summary-item">
<text class="summary-num">{{ todaySummary.checkedRoomCount || 0 }}</text>
<text class="summary-label">已点名寝室</text>
</view>
<view class="summary-item">
<text class="summary-num">{{ todaySummary.uncheckedRoomCount || 0 }}</text>
<text class="summary-label">未点名寝室</text>
</view>
<view class="summary-item clickable" @click="openDetailPopup('leave')">
<text class="summary-num summary-num--leave">{{ todaySummary.leaveCount || 0 }}</text>
<text class="summary-label">请假</text>
<text class="summary-tip">点击明细</text>
</view>
<view class="summary-item clickable" @click="openDetailPopup('holidayStay')">
<text class="summary-num summary-num--holiday">{{ todaySummary.holidayStayCount || 0 }}</text>
<text class="summary-label">假期留宿</text>
<text class="summary-tip">点击明细</text>
</view>
<view class="summary-item clickable" @click="openDetailPopup('absent')">
<text class="summary-num summary-num--absent">{{ todaySummary.absentCount || 0 }}</text>
<text class="summary-label">异常</text>
<text class="summary-tip">点击明细</text>
</view>
</view>
</view>
<!-- 列表 -->
<view v-if="list.length > 0" class="list-section">
<view v-for="item in list" :key="item.id" class="check-card" @click="handleView(item)">
@@ -87,6 +120,10 @@
<text class="stat-num">{{ item.leaveCount || 0 }}</text>
<text class="stat-label">请假</text>
</view>
<view class="stat-item stat-holiday-stay">
<text class="stat-num">{{ item.holidayStayCount || 0 }}</text>
<text class="stat-label">留宿</text>
</view>
<view class="stat-item stat-absent">
<text class="stat-num">{{ item.absentCount || 0 }}</text>
<text class="stat-label">不在</text>
@@ -134,6 +171,42 @@
@close="showBuildingPicker = false"
/>
<!-- 汇总明细弹框 -->
<u-popup
:show="detailPopupVisible"
mode="bottom"
:round="16"
:closeable="true"
:safe-area-inset-bottom="true"
@close="detailPopupVisible = false"
>
<view class="detail-popup">
<view class="detail-popup-title">{{ detailPopupTitle }}</view>
<scroll-view scroll-y class="detail-popup-scroll">
<view v-if="detailPopupList.length === 0" class="detail-empty">
<u-empty text="暂无明细" mode="data"></u-empty>
</view>
<view v-for="(row, index) in detailPopupList" :key="row.studentId || index" class="detail-row">
<view class="detail-row-main">
<text class="detail-name">{{ row.studentName || '-' }}</text>
<text class="detail-no">{{ row.studentNo || '-' }}</text>
</view>
<view class="detail-row-sub">
<text>{{ row.buildingName || '-' }} · {{ row.roomName || '-' }}</text>
<text v-if="row.bedCode"> · {{ row.bedCode }}床</text>
</view>
<view class="detail-row-sub" v-if="row.className">班级:{{ row.className }}</view>
<view class="detail-row-sub" v-if="detailPopupType === 'leave' && row.leaveTypeName">
请假类型:{{ row.leaveTypeName }}
</view>
<view class="detail-row-sub" v-if="detailPopupType === 'holidayStay' && row.holidayType">
假期类型:{{ row.holidayType }}
</view>
</view>
</scroll-view>
</view>
</u-popup>
<!-- 悬浮点名按钮 -->
<view class="fab-button" @click="handleAdd">
<u-icon name="plus" size="24" color="#ffffff" />
@@ -142,7 +215,7 @@
</template>
<script>
import { listDormRollCall } from '@/api/dormRollCall.js';
import { listDormRollCall, getTodayRollCallSummary } from '@/api/dormRollCall.js';
import { request } from '@/common/request.js';
export default {
@@ -169,6 +242,19 @@ export default {
height: '32px',
boxSizing: 'border-box',
},
todaySummary: {
checkedRoomCount: 0,
uncheckedRoomCount: 0,
leaveCount: 0,
holidayStayCount: 0,
absentCount: 0,
leaveDetails: [],
holidayStayDetails: [],
absentDetails: [],
},
detailPopupVisible: false,
detailPopupType: '',
detailPopupList: [],
};
},
computed: {
@@ -178,12 +264,59 @@ export default {
totalPages() {
return Math.ceil(this.total / this.queryParams.pageSize);
},
todayLabel() {
const now = new Date();
const m = String(now.getMonth() + 1).padStart(2, '0');
const d = String(now.getDate()).padStart(2, '0');
return `${now.getFullYear()}-${m}-${d}`;
},
detailPopupTitle() {
const map = {
leave: '今日请假明细',
holidayStay: '今日假期留宿明细',
absent: '今日异常(不在)明细',
};
return map[this.detailPopupType] || '明细';
},
},
onShow() {
this.loadBuildingOptions();
this.getList();
this.loadTodaySummary();
},
methods: {
async loadTodaySummary() {
try {
const res = await getTodayRollCallSummary();
if (res && res.code === 200 && res.data) {
this.todaySummary = {
checkedRoomCount: res.data.checkedRoomCount || 0,
uncheckedRoomCount: res.data.uncheckedRoomCount || 0,
leaveCount: res.data.leaveCount || 0,
holidayStayCount: res.data.holidayStayCount || 0,
absentCount: res.data.absentCount || 0,
leaveDetails: res.data.leaveDetails || [],
holidayStayDetails: res.data.holidayStayDetails || [],
absentDetails: res.data.absentDetails || [],
};
}
} catch (error) {
console.error('加载今日点名汇总失败:', error);
}
},
openDetailPopup(type) {
this.detailPopupType = type;
if (type === 'leave') {
this.detailPopupList = this.todaySummary.leaveDetails || [];
} else if (type === 'holidayStay') {
this.detailPopupList = this.todaySummary.holidayStayDetails || [];
} else {
this.detailPopupList = this.todaySummary.absentDetails || [];
}
this.detailPopupVisible = true;
},
async getList() {
this.pageLoading = true;
try {
@@ -316,6 +449,134 @@ export default {
padding-bottom: 160rpx;
}
.today-summary-card {
background: #ffffff;
border-radius: 16rpx;
padding: 24rpx 20rpx 16rpx;
margin-bottom: 24rpx;
box-shadow: 0 4rpx 16rpx rgba(0, 81, 217, 0.08);
.summary-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20rpx;
.summary-title {
font-size: 30rpx;
font-weight: 600;
color: #303133;
}
.summary-date {
font-size: 24rpx;
color: #909399;
}
}
.summary-grid {
display: flex;
flex-wrap: wrap;
gap: 12rpx;
}
.summary-item {
width: calc(33.33% - 8rpx);
background: #f7f9fc;
border-radius: 12rpx;
padding: 16rpx 8rpx;
display: flex;
flex-direction: column;
align-items: center;
box-sizing: border-box;
&.clickable:active {
opacity: 0.85;
}
.summary-num {
font-size: 34rpx;
font-weight: 700;
color: #0051d9;
line-height: 1.2;
&--leave {
color: #e6a23c;
}
&--holiday {
color: #409eff;
}
&--absent {
color: #f56c6c;
}
}
.summary-label {
margin-top: 8rpx;
font-size: 22rpx;
color: #606266;
}
.summary-tip {
margin-top: 4rpx;
font-size: 20rpx;
color: #c0c4cc;
}
}
}
.detail-popup {
padding: 28rpx 24rpx 12rpx;
max-height: 70vh;
.detail-popup-title {
font-size: 32rpx;
font-weight: 600;
color: #303133;
margin-bottom: 16rpx;
text-align: center;
}
.detail-popup-scroll {
max-height: 58vh;
}
.detail-empty {
padding: 40rpx 0;
}
.detail-row {
padding: 20rpx 8rpx;
border-bottom: 1rpx solid #f0f0f0;
.detail-row-main {
display: flex;
align-items: baseline;
gap: 16rpx;
margin-bottom: 8rpx;
.detail-name {
font-size: 28rpx;
font-weight: 600;
color: #303133;
}
.detail-no {
font-size: 24rpx;
color: #909399;
}
}
.detail-row-sub {
font-size: 24rpx;
color: #606266;
line-height: 1.5;
}
}
}
.fab-button {
position: fixed;
right: 20px;
@@ -448,6 +709,15 @@ export default {
}
}
&.stat-holiday-stay {
background: #ecf5ff;
.stat-num,
.stat-label {
color: #409eff;
}
}
&.stat-absent {
background: #ffebee;
+5
View File
@@ -64,6 +64,7 @@
<view class="room-stats-mini" v-if="isRoomCompleted(room)">
<text class="stat-present">{{ room.presentCount || 0 }}在</text>
<text class="stat-leave">{{ room.leaveCount || 0 }}假</text>
<text class="stat-holiday-stay">{{ room.holidayStayCount || 0 }}留</text>
<text class="stat-absent">{{ room.absentCount || 0 }}不在</text>
</view>
<view class="room-status">
@@ -316,6 +317,7 @@ export default {
this.$set(this.rooms[idx], 'isCompleted', true);
this.$set(this.rooms[idx], 'presentCount', countOf(counts.presentCount));
this.$set(this.rooms[idx], 'leaveCount', countOf(counts.leaveCount));
this.$set(this.rooms[idx], 'holidayStayCount', countOf(counts.holidayStayCount));
this.$set(this.rooms[idx], 'absentCount', countOf(counts.absentCount));
this.$set(this.rooms[idx], 'emptyCount', countOf(counts.emptyCount));
}
@@ -468,6 +470,9 @@ export default {
.stat-leave {
color: #e6a23c;
}
.stat-holiday-stay {
color: #409eff;
}
.stat-absent {
color: #f56c6c;
}
+26 -4
View File
@@ -43,6 +43,10 @@
<view class="stat-number">{{ summary.leave }}</view>
<view class="stat-label">请假</view>
</view>
<view class="stat-item stat-holiday-stay">
<view class="stat-number">{{ summary.holidayStay }}</view>
<view class="stat-label">假期留宿</view>
</view>
<view class="stat-item stat-absent">
<view class="stat-number">{{ summary.absent }}</view>
<view class="stat-label">不在</view>
@@ -71,6 +75,10 @@
<u-tag text="请假" type="warning" size="mini"></u-tag>
<text class="leave-type">{{ student.leaveTypeName }}</text>
</view>
<view v-else-if="student.holidayStayId || student.rollCallStatus === '4'" class="leave-info">
<u-tag text="假期留宿" type="primary" size="mini"></u-tag>
<text v-if="student.holidayType" class="leave-type">{{ student.holidayType }}</text>
</view>
<view v-if="!student.studentId" class="empty-bed-tag">
<text class="empty-text">空床位</text>
</view>
@@ -100,15 +108,16 @@ export default {
},
computed: {
summary() {
let present = 0, leave = 0, absent = 0, empty = 0;
let present = 0, leave = 0, holidayStay = 0, absent = 0, empty = 0;
this.studentList.forEach((s) => {
const status = s.rollCallStatus;
if (status === '0') present++;
else if (status === '1') leave++;
else if (status === '4') holidayStay++;
else if (status === '2') absent++;
else if (status === '3') empty++;
});
return { present, leave, absent, empty };
return { present, leave, holidayStay, absent, empty };
},
},
onLoad(options) {
@@ -139,6 +148,7 @@ export default {
getStatusLabel(status) {
if (status === '0') return '在寝';
if (status === '1') return '请假';
if (status === '4') return '假期留宿';
if (status === '2') return '不在';
if (status === '3') return '空床位';
return '未知';
@@ -147,6 +157,7 @@ export default {
getStatusClass(status) {
if (status === '0') return 'status-present';
if (status === '1') return 'status-leave';
if (status === '4') return 'status-holiday-stay';
if (status === '2') return 'status-absent';
if (status === '3') return 'status-empty';
return '';
@@ -155,6 +166,7 @@ export default {
getStatusTagType(status) {
if (status === '0') return 'success';
if (status === '1') return 'warning';
if (status === '4') return 'primary';
if (status === '2') return 'error';
if (status === '3') return 'info';
return 'info';
@@ -197,8 +209,8 @@ export default {
.stats-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 20rpx;
grid-template-columns: repeat(5, 1fr);
gap: 12rpx;
padding: 20rpx 0;
}
@@ -233,6 +245,13 @@ export default {
color: #e6a23c;
}
.stat-holiday-stay {
background-color: #ecf5ff;
}
.stat-holiday-stay .stat-number {
color: #409eff;
}
.stat-absent {
background-color: #ffebee;
}
@@ -266,6 +285,9 @@ export default {
.student-card.status-leave {
border-left: 6rpx solid #e6a23c;
}
.student-card.status-holiday-stay {
border-left: 6rpx solid #409eff;
}
.student-card.status-absent {
border-left: 6rpx solid #f56c6c;
}
+8
View File
@@ -38,6 +38,14 @@ export const flowHisTaskList = (businessId: string | number) => {
});
};
/** 办理页审批上下文:是否可审批、taskId、建议业务状态 */
export const approvalContext = (businessId: string | number) => {
return request({
url: `/workflow/instance/approvalContext/${businessId}`,
method: 'get'
});
};
/**
* 分页查询当前登录人单据
* @param query
@@ -62,6 +62,8 @@ export interface DormRollCallVO {
presentCount: number;
/** 请假人数 */
leaveCount: number;
/** 假期留宿人数 */
holidayStayCount: number;
/** 不在人数 */
absentCount: number;
/** 空床位 */
@@ -106,7 +108,7 @@ export interface DormRollCallDetailVO {
collegeId?: string | number;
/** 学院名称 */
collegeName?: string;
/** 点名状态(0在寝/1请假/2不在/3空床位) */
/** 点名状态(0在寝/1请假/2不在/3空床位/4假期留宿) */
rollCallStatus: string;
/** 请假ID */
leaveId?: string | number;
@@ -116,6 +118,14 @@ export interface DormRollCallDetailVO {
leaveStartTime?: string;
/** 请假结束时间 */
leaveEndTime?: string;
/** 假期留宿ID */
holidayStayId?: string | number;
/** 假期类型 */
holidayType?: string;
/** 留宿开始日期 */
holidayStartDate?: string;
/** 留宿结束日期 */
holidayEndDate?: string;
}
/**
@@ -134,6 +144,8 @@ export interface RoomRollCallStatusVO {
presentCount: number;
/** 请假人数 */
leaveCount: number;
/** 假期留宿人数 */
holidayStayCount: number;
/** 不在人数 */
absentCount: number;
/** 空床位 */
@@ -182,6 +194,8 @@ export interface DormRollCallForm extends BaseEntity {
presentCount?: number;
/** 请假人数 */
leaveCount?: number;
/** 假期留宿人数 */
holidayStayCount?: number;
/** 不在人数 */
absentCount?: number;
/** 空床位 */
@@ -222,7 +236,7 @@ export interface DormRollCallDetailForm {
collegeId?: string | number;
/** 学院名称 */
collegeName?: string;
/** 点名状态(0在寝/1请假/2不在/3空床位) */
/** 点名状态(0在寝/1请假/2不在/3空床位/4假期留宿) */
rollCallStatus?: string;
/** 请假ID */
leaveId?: string | number;
@@ -232,4 +246,12 @@ export interface DormRollCallDetailForm {
leaveStartTime?: string;
/** 请假结束时间 */
leaveEndTime?: string;
/** 假期留宿ID */
holidayStayId?: string | number;
/** 假期类型 */
holidayType?: string;
/** 留宿开始日期 */
holidayStartDate?: string;
/** 留宿结束日期 */
holidayEndDate?: string;
}
@@ -19,7 +19,9 @@ const props = defineProps({
status: propTypes.string.def(''),
pageType: propTypes.string.def(''),
buttonLoading: propTypes.bool.def(false),
id: propTypes.string.def('') || propTypes.number.def()
id: propTypes.string.def('') || propTypes.number.def(),
/** 当前用户是否有可办审批待办(优先于 status===waiting) */
canApprove: propTypes.bool.def(false)
});
const emits = defineEmits(['submitForm', 'approvalVerifyOpen', 'handleApprovalRecord']);
//暂存,提交
@@ -43,8 +45,11 @@ const submitButtonShow = computed(() => {
);
});
//校验审批按钮是否显示
// 审批按钮:有待办可审,或传统 pageType=approval + waiting
const approvalButtonShow = computed(() => {
if (props.canApprove) {
return true;
}
return props.pageType === 'approval' && props.status && props.status === 'waiting';
});
@@ -13,6 +13,7 @@
<div class="summary-bar">
<span>在寝: <b style="color: #67c23a">{{ rollCallData.presentCount }}</b></span>
<span>请假: <b style="color: #e6a23c">{{ rollCallData.leaveCount }}</b></span>
<span>假期留宿: <b style="color: #409eff">{{ rollCallData.holidayStayCount || 0 }}</b></span>
<span>不在: <b style="color: #f56c6c">{{ rollCallData.absentCount }}</b></span>
<span>空床位: <b style="color: #909399">{{ rollCallData.emptyCount }}</b></span>
</div>
@@ -21,17 +22,19 @@
<el-table-column label="床位" prop="bedName" width="70" />
<el-table-column label="姓名" prop="studentName" width="80" />
<el-table-column label="学号" prop="studentNo" width="120" />
<el-table-column label="状态" prop="rollCallStatus" width="80">
<el-table-column label="状态" prop="rollCallStatus" width="90">
<template #default="scope">
<el-tag v-if="scope.row.rollCallStatus === '0'" type="success" size="small">在寝</el-tag>
<el-tag v-else-if="scope.row.rollCallStatus === '1'" type="warning" size="small">请假</el-tag>
<el-tag v-else-if="scope.row.rollCallStatus === '4'" type="primary" size="small">假期留宿</el-tag>
<el-tag v-else-if="scope.row.rollCallStatus === '2'" type="danger" size="small">不在</el-tag>
<el-tag v-else type="info" size="small">空床位</el-tag>
</template>
</el-table-column>
<el-table-column label="请假信息" min-width="120">
<el-table-column label="备注信息" min-width="120">
<template #default="scope">
<span v-if="scope.row.leaveTypeName">{{ scope.row.leaveTypeName }}</span>
<span v-else-if="scope.row.holidayType || scope.row.holidayStayId">假期留宿{{ scope.row.holidayType ? `(${scope.row.holidayType})` : '' }}</span>
<span v-else>-</span>
</template>
</el-table-column>
@@ -18,6 +18,11 @@
<span style="color: #e6a23c">{{ scope.row.leaveCount }}</span>
</template>
</el-table-column>
<el-table-column label="留宿" prop="holidayStayCount" width="60" align="center">
<template #default="scope">
<span style="color: #409eff">{{ scope.row.holidayStayCount || 0 }}</span>
</template>
</el-table-column>
<el-table-column label="不在" prop="absentCount" width="60" align="center">
<template #default="scope">
<span style="color: #f56c6c">{{ scope.row.absentCount }}</span>
@@ -18,6 +18,9 @@
<div v-if="bed.leaveId" class="leave-info">
<el-tag type="warning" size="small">{{ bed.leaveTypeName || '请假' }}</el-tag>
</div>
<div v-else-if="bed.holidayStayId" class="leave-info">
<el-tag type="primary" size="small">假期留宿</el-tag>
</div>
<div class="status-label">
{{ getStatusLabel(bed.rollCallStatus) }}
</div>
@@ -28,6 +31,7 @@
<div v-if="bedDetails.length > 0" class="summary-bar">
<span>在寝: <b style="color: #67c23a">{{ summary.present }}</b></span>
<span>请假: <b style="color: #e6a23c">{{ summary.leave }}</b></span>
<span>假期留宿: <b style="color: #409eff">{{ summary.holidayStay }}</b></span>
<span>不在: <b style="color: #f56c6c">{{ summary.absent }}</b></span>
<span>空床位: <b style="color: #909399">{{ summary.empty }}</b></span>
</div>
@@ -73,6 +77,7 @@ interface BedItem extends DormRollCallDetailForm {
bedId: string | number;
originalStatus?: string;
isLeaveAuto?: boolean;
isHolidayStayAuto?: boolean;
}
const loading = ref(false);
@@ -80,21 +85,23 @@ const submitting = ref(false);
const bedDetails = ref<BedItem[]>([]);
const summary = computed(() => {
let present = 0, leave = 0, absent = 0, empty = 0;
let present = 0, leave = 0, holidayStay = 0, absent = 0, empty = 0;
bedDetails.value.forEach(bed => {
const status = bed.rollCallStatus;
if (status === '0') present++;
else if (status === '1') leave++;
else if (status === '4') holidayStay++;
else if (status === '2') absent++;
else if (status === '3') empty++;
});
return { present, leave, absent, empty };
return { present, leave, holidayStay, absent, empty };
});
const getBedCardClass = (bed: BedItem) => {
const status = bed.rollCallStatus;
if (status === '0') return 'bed-present';
if (status === '1') return 'bed-leave';
if (status === '4') return 'bed-holiday-stay';
if (status === '2') return 'bed-absent';
if (status === '3') return 'bed-empty';
return '';
@@ -103,6 +110,7 @@ const getBedCardClass = (bed: BedItem) => {
const getStatusLabel = (status?: string) => {
if (status === '0') return '在寝';
if (status === '1') return '请假';
if (status === '4') return '假期留宿';
if (status === '2') return '不在';
if (status === '3') return '空床位';
return '';
@@ -113,6 +121,10 @@ const handleBedClick = (bed: BedItem) => {
ElMessage.info('该学生处于请假状态,不可修改');
return;
}
if (bed.isHolidayStayAuto) {
ElMessage.info('该学生处于假期留宿状态,不可修改');
return;
}
if (!bed.studentId) {
return;
}
@@ -201,6 +213,13 @@ const loadBedDetails = async () => {
bed.leaveTypeName = item.leaveInfo.leaveTypeName;
bed.leaveStartTime = item.leaveInfo.leaveStartTime;
bed.leaveEndTime = item.leaveInfo.leaveEndTime;
} else if (item.holidayStayInfo) {
bed.rollCallStatus = '4';
bed.isHolidayStayAuto = true;
bed.holidayStayId = item.holidayStayInfo.holidayStayId;
bed.holidayType = item.holidayStayInfo.holidayType;
bed.holidayStartDate = item.holidayStayInfo.applyStartDate;
bed.holidayEndDate = item.holidayStayInfo.applyEndDate;
}
bed.originalStatus = bed.rollCallStatus;
@@ -247,7 +266,11 @@ const handleSubmit = async () => {
leaveId: bed.leaveId,
leaveTypeName: bed.leaveTypeName,
leaveStartTime: bed.leaveStartTime,
leaveEndTime: bed.leaveEndTime
leaveEndTime: bed.leaveEndTime,
holidayStayId: bed.holidayStayId,
holidayType: bed.holidayType,
holidayStartDate: bed.holidayStartDate,
holidayEndDate: bed.holidayEndDate
}));
const submitData: DormRollCallForm = {
@@ -259,6 +282,7 @@ const handleSubmit = async () => {
totalBeds: bedDetails.value.length,
presentCount: summary.value.present,
leaveCount: summary.value.leave,
holidayStayCount: summary.value.holidayStay,
absentCount: summary.value.absent,
emptyCount: summary.value.empty,
details
@@ -325,6 +349,12 @@ watch(() => props.visible, (val) => {
cursor: not-allowed;
}
.bed-holiday-stay {
background: #ecf5ff;
border-color: #409eff;
cursor: not-allowed;
}
.bed-absent {
background: #ffebee;
border-color: #f56c6c;
@@ -76,6 +76,11 @@
<span style="color: #e6a23c">{{ scope.row.leaveCount }}</span>
</template>
</el-table-column>
<el-table-column label="假期留宿" align="center" prop="holidayStayCount" width="90">
<template #default="scope">
<span style="color: #409eff">{{ scope.row.holidayStayCount || 0 }}</span>
</template>
</el-table-column>
<el-table-column label="不在" align="center" prop="absentCount" width="70">
<template #default="scope">
<span style="color: #f56c6c">{{ scope.row.absentCount }}</span>
@@ -36,6 +36,7 @@
<div class="room-stats">
<span class="stat-item stat-present" title="在寝">{{ room.presentCount }}</span>
<span class="stat-item stat-leave" title="请假">{{ room.leaveCount }}</span>
<span class="stat-item stat-holiday-stay" title="假期留宿">{{ room.holidayStayCount || 0 }}</span>
<span class="stat-item stat-absent" title="不在">{{ room.absentCount }}</span>
<span class="stat-item stat-empty" title="空床位">{{ room.emptyCount }}</span>
</div>
@@ -248,6 +249,11 @@ onMounted(() => {
color: #e6a23c;
}
.stat-holiday-stay {
background: #ecf5ff;
color: #409eff;
}
.stat-absent {
background: #ffebee;
color: #f56c6c;
+31
View File
@@ -12,6 +12,7 @@
:id="form.id"
:status="form.status"
:pageType="routeParams.type"
:canApprove="canApprove"
/>
</div>
</div>
@@ -309,6 +310,7 @@ import { StartProcessBo } from '@/api/workflow/workflowCommon/types';
import { addStudentLeave, getStudentLeave, updateStudentLeave } from '@/api/workflow/studentLeave/studentLeave';
import { getStudentMsg, getClassTeacherId, getCollegeLeaveApprovers, getCollegeLeaders } from '@/api/xg/xsjcxx/studentMsg/studentMsg';
import { startWorkFlow } from '@/api/workflow/task';
import { approvalContext } from '@/api/workflow/instance';
import { StudentLeaveForm, StudentLeaveVO } from '@/api/workflow/studentLeave/studentLeave/types';
import { StudentMsgVO } from '@/api/xg/xsjcxx/studentMsg/studentMsg/types';
@@ -474,6 +476,8 @@ const submitVerifyRef = ref<InstanceType<typeof SubmitVerify>>();
const approvalRecordRef = ref<InstanceType<typeof ApprovalRecord>>();
//按钮组件
const approvalButtonRef = ref<InstanceType<typeof ApprovalButton>>();
/** 当前用户是否可审批(以流程待办为准) */
const canApprove = ref(false);
const leaveFormRef = ref<ElFormInstance>();
@@ -1177,6 +1181,31 @@ const openExternalMap = () => {
// }
// );
/**
* 以流程引擎待办为准刷新审批能力(覆盖 draft/back 残留导致无按钮)
*/
const ensureApprovalCapability = async (leaveId: string | number) => {
try {
canApprove.value = false;
const resp = await approvalContext(leaveId);
const ctx = resp?.data || {};
if (ctx.suggestedBusinessStatus) {
form.approvalStatus = ctx.suggestedBusinessStatus;
form.status = ctx.suggestedBusinessStatus;
}
if (ctx.canApprove) {
canApprove.value = true;
routeParams.value = {
...routeParams.value,
type: 'approval',
taskId: ctx.taskId != null ? String(ctx.taskId) : routeParams.value.taskId
};
}
} catch (e) {
console.warn('请假审批上下文检查失败(不影响查看):', e);
}
};
// 根据id获取学生请假信息
const getStudentLeaveInfo = async (id: string | number) => {
try {
@@ -1193,6 +1222,8 @@ const getStudentLeaveInfo = async (id: string | number) => {
// todo
form.status = leaveInfo.approvalStatus;
await ensureApprovalCapability(id);
// 如果有学生ID,则获取学生信息
if (leaveInfo.studentId) {
await getStudentInfo(leaveInfo.studentId);
+30
View File
@@ -12,6 +12,7 @@
:id="cancelForm.id"
:status="leaveInfo.status"
:pageType="routeParams.type"
:canApprove="canApprove"
/>
</div>
</div>
@@ -237,6 +238,7 @@ import ApprovalRecord from '@/components/Process/approvalRecord.vue';
import ApprovalButton from '@/components/Process/approvalButton.vue';
import { AxiosResponse } from 'axios';
import { StartProcessBo } from '@/api/workflow/workflowCommon/types';
import { approvalContext } from '@/api/workflow/instance';
import { getStudentLeave, updateStudentLeave } from '@/api/workflow/studentLeave/studentLeave';
import { getStudentMsg } from '@/api/xg/xsjcxx/studentMsg/studentMsg';
import { startWorkFlow } from '@/api/workflow/task';
@@ -369,6 +371,7 @@ const submitVerifyRef = ref<InstanceType<typeof SubmitVerify>>();
const approvalRecordRef = ref<InstanceType<typeof ApprovalRecord>>();
//按钮组件
const approvalButtonRef = ref<InstanceType<typeof ApprovalButton>>();
const canApprove = ref(false);
const submitFormData = ref<StartProcessBo>({
businessId: '',
@@ -786,6 +789,10 @@ const getStudentLeaveInfo = async (id: string | number) => {
console.log('销假表单初始化完成:', cancelForm);
if (cancelForm.id) {
await ensureApprovalCapability(cancelForm.id);
}
// 如果有学生ID,则获取学生信息
if (leaveData.studentId) {
await getStudentInfo(leaveData.studentId);
@@ -955,6 +962,29 @@ const handleCancelAttachmentData = async () => {
}
};
/** 以流程引擎待办为准刷新销假审批能力 */
const ensureApprovalCapability = async (cancelLeaveId: string | number) => {
try {
canApprove.value = false;
const resp = await approvalContext(cancelLeaveId);
const ctx = resp?.data || {};
if (ctx.suggestedBusinessStatus) {
leaveInfo.status = ctx.suggestedBusinessStatus;
cancelForm.cancelStatus = ctx.suggestedBusinessStatus;
}
if (ctx.canApprove) {
canApprove.value = true;
routeParams.value = {
...routeParams.value,
type: 'approval',
taskId: ctx.taskId != null ? String(ctx.taskId) : routeParams.value.taskId
};
}
} catch (e) {
console.warn('销假审批上下文检查失败(不影响查看):', e);
}
};
// 初始化页面数据
const initPageData = () => {
// 重置销假表单数据