feat(xg): 请假审批修复、点名今日汇总与寝室扣分条例脚本
同步请假/销假审批上下文与状态、宿舍点名今日汇总与假期留宿标识,并补充寝室检查扣分条例生产 SQL 与执行说明。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
1 parent
3e6349112c
commit
7cea0566ac
48 files changed
+1966
-116
No files matched your search
+9
@@ -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));
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取流程变量
|
||||
*
|
||||
|
||||
+31
@@ -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;
|
||||
}
|
||||
+9
@@ -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更新状态
|
||||
*
|
||||
|
||||
+53
@@ -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);
|
||||
|
||||
+11
@@ -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;
|
||||
|
||||
+12
@@ -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;
|
||||
}
|
||||
+2
@@ -54,6 +54,8 @@ public class XgDormRollCallBo extends BaseEntity {
|
||||
|
||||
private Integer leaveCount;
|
||||
|
||||
private Integer holidayStayCount;
|
||||
|
||||
private Integer absentCount;
|
||||
|
||||
private Integer emptyCount;
|
||||
|
||||
+8
@@ -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;
|
||||
}
|
||||
+5
@@ -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;
|
||||
|
||||
/**
|
||||
* 销假时间
|
||||
*/
|
||||
|
||||
+50
@@ -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;
|
||||
}
|
||||
+39
@@ -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<>();
|
||||
}
|
||||
+2
@@ -23,6 +23,8 @@ public class RoomRollCallStatusVo implements Serializable {
|
||||
|
||||
private Integer leaveCount;
|
||||
|
||||
private Integer holidayStayCount;
|
||||
|
||||
private Integer absentCount;
|
||||
|
||||
private Integer emptyCount;
|
||||
|
||||
+8
@@ -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;
|
||||
}
|
||||
+2
@@ -50,6 +50,8 @@ public class XgDormRollCallVo implements Serializable {
|
||||
|
||||
private Integer leaveCount;
|
||||
|
||||
private Integer holidayStayCount;
|
||||
|
||||
private Integer absentCount;
|
||||
|
||||
private Integer emptyCount;
|
||||
|
||||
+136
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
+6
@@ -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();
|
||||
}
|
||||
+11
@@ -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);
|
||||
|
||||
}
|
||||
+32
-18
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+234
@@ -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
@@ -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
|
||||
));
|
||||
}
|
||||
}
|
||||
+84
-56
@@ -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,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';
|
||||
},
|
||||
},
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 = () => {
|
||||
// 重置销假表单数据
|
||||
|
||||
Reference in new issue
Block a user