From 15ed3ec9fb9c1921da030e7ce9b8314f4262fabd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9Cwuzhen=E2=80=9D?= Date: Wed, 21 Oct 2026 11:24:48 +0800 Subject: [PATCH] =?UTF-8?q?fix(leave/dorm):=20=E6=89=93=E5=9B=9E=E9=87=8D?= =?UTF-8?q?=E6=8F=90=E3=80=81=E5=AE=A1=E6=89=B9=E6=91=98=E8=A6=81=E4=B8=8E?= =?UTF-8?q?=E5=BE=85=E5=8A=9E=E6=A0=87=E9=A2=98=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 修复请假/销假打回后空 flowCode 导致启动失败;审批弹窗展示请假摘要并去掉附件;待办标题短显请假/销假;点名 todaySummary 与楼栋关联脚本一并同步。 Co-authored-by: Cursor --- .../workflow/domain/bo/StartProcessBo.java | 3 +- .../service/impl/FlwTaskServiceImpl.java | 4 + .../controller/XgDormRollCallController.java | 59 +-- .../domain/vo/DormRollCallTodaySummaryVo.java | 10 +- .../xg/service/IXgDormRollCallService.java | 7 +- .../impl/XgDormRollCallServiceImpl.java | 181 ++-------- ..._20261020_create_dorm_manager_building.sql | 64 ++++ ..._leave_back_resubmit_diag_202513063048.sql | 336 ++++++++++++++++++ ...61021_rollcall_today_summary_demo_data.sql | 25 ++ .../sql/update/请假一年级代理测试账号说明.md | 44 +++ docs/开发文档/API/宿舍点名假期留宿标识说明.md | 21 +- mobile/src/api/dormRollCall.js | 7 +- .../src/components/Process/submitVerify.vue | 296 ++++++++++----- mobile/src/pages/task/taskWaiting.vue | 64 +++- .../xg/xsjcxx/xsqj/cancelLeaveEdit/index.vue | 38 +- .../pages/xg/xsjcxx/xsqj/leaveEdit/index.vue | 50 ++- mobile/src/pages/xgss/ssdm/ssdm.vue | 51 ++- mobile/src/pages/xgss/ssdm/ssdmCheck.vue | 188 +++++++++- web/src/views/xg/xsjcxx/xsqj/dialog.vue | 4 +- web/src/views/xg/xsjcxx/xsqj/xjdialog.vue | 4 +- 20 files changed, 1136 insertions(+), 320 deletions(-) create mode 100644 backend/script/sql/update/update_20261020_create_dorm_manager_building.sql create mode 100644 backend/script/sql/update/update_20261021_leave_back_resubmit_diag_202513063048.sql create mode 100644 backend/script/sql/update/update_20261021_rollcall_today_summary_demo_data.sql create mode 100644 backend/script/sql/update/请假一年级代理测试账号说明.md diff --git a/backend/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/domain/bo/StartProcessBo.java b/backend/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/domain/bo/StartProcessBo.java index ea21a81e..4f35387e 100644 --- a/backend/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/domain/bo/StartProcessBo.java +++ b/backend/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/domain/bo/StartProcessBo.java @@ -29,9 +29,8 @@ public class StartProcessBo implements Serializable { private String businessId; /** - * 流程定义编码 + * 流程定义编码(首次启动必填;草稿/退回/撤销后重新提交时实例已存在,可为空) */ - @NotBlank(message = "流程定义编码不能为空", groups = {AddGroup.class}) private String flowCode; /** diff --git a/backend/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/service/impl/FlwTaskServiceImpl.java b/backend/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/service/impl/FlwTaskServiceImpl.java index 181e6e87..3d0676af 100644 --- a/backend/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/service/impl/FlwTaskServiceImpl.java +++ b/backend/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/service/impl/FlwTaskServiceImpl.java @@ -126,6 +126,10 @@ public class FlwTaskServiceImpl implements IFlwTaskService { dto.setTaskId(currentTask.getId()); return dto; } + // 首次启动必须带流程编码;打回/撤销重提走上面已有实例分支,不依赖 flowCode + if (StringUtils.isBlank(startProcessBo.getFlowCode())) { + throw new ServiceException("流程定义编码不能为空"); + } FlowParams flowParams = FlowParams.build() .flowCode(startProcessBo.getFlowCode()) .variable(startProcessBo.getVariables()) diff --git a/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/controller/XgDormRollCallController.java b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/controller/XgDormRollCallController.java index 36f27e11..19b9a16d 100644 --- a/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/controller/XgDormRollCallController.java +++ b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/controller/XgDormRollCallController.java @@ -39,17 +39,44 @@ public class XgDormRollCallController extends BaseController { } /** - * 当前账号当天点名汇总(已/未点名寝室 + 请假/留宿/异常明细) - * 须声明在 /{id} 之前,避免被路径变量吞掉 + * 当天点名人数汇总(总人数/已点名人数 + 请假/留宿/异常明细) + * + * @param buildingId 可选;空=全校,有值=指定楼栋 */ @SaCheckPermission("dormRollCall:dormRollCall:list1") @GetMapping("/todaySummary") - public R todaySummary() { - return R.ok(xgDormRollCallService.queryTodaySummary()); + public R todaySummary( + @RequestParam(value = "buildingId", required = false) Long buildingId) { + return R.ok(xgDormRollCallService.queryTodaySummary(buildingId)); + } + + @SaCheckPermission("dormRollCall:dormRollCall:add") + @GetMapping("/generateBatchNo") + public R generateBatchNo() { + return R.ok(xgDormRollCallService.generateBatchNo()); + } + + @SaCheckPermission("dormRollCall:dormRollCall:list1") + @GetMapping("/buildingRoomsStatus") + public R> getBuildingRoomsStatus( + @RequestParam("buildingId") Long buildingId, + @RequestParam(value = "batchNo", required = false) String batchNo) { + return R.ok(xgDormRollCallService.getBuildingRoomsStatus(buildingId, batchNo)); } @SaCheckPermission("dormRollCall:dormRollCall:query") - @GetMapping("/{id}") + @GetMapping("/roomHistory/{roomId}") + public TableDataInfo roomHistory( + @NotNull(message = "房间ID不能为空") @PathVariable("roomId") Long roomId, + PageQuery pageQuery) { + return xgDormRollCallService.queryRoomHistory(roomId, pageQuery); + } + + /** + * 详情:id 限制为数字,避免 /todaySummary、/generateBatchNo 等固定路径被当成 id + */ + @SaCheckPermission("dormRollCall:dormRollCall:query") + @GetMapping("/{id:\\d+}") public R getInfo(@NotNull(message = "主键不能为空") @PathVariable("id") Long id) { return R.ok(xgDormRollCallService.queryById(id)); } @@ -82,20 +109,6 @@ public class XgDormRollCallController extends BaseController { ExcelUtil.exportExcel(list, "宿舍点名记录", XgDormRollCallVo.class, response); } - @SaCheckPermission("dormRollCall:dormRollCall:add") - @GetMapping("/generateBatchNo") - public R generateBatchNo() { - return R.ok(xgDormRollCallService.generateBatchNo()); - } - - @SaCheckPermission("dormRollCall:dormRollCall:list1") - @GetMapping("/buildingRoomsStatus") - public R> getBuildingRoomsStatus( - @RequestParam("buildingId") Long buildingId, - @RequestParam(value = "batchNo", required = false) String batchNo) { - return R.ok(xgDormRollCallService.getBuildingRoomsStatus(buildingId, batchNo)); - } - @SaCheckPermission("dormRollCall:dormRollCall:add") @PostMapping("/submit") @RepeatSubmit() @@ -106,12 +119,4 @@ public class XgDormRollCallController extends BaseController { } return R.fail("提交点名记录失败"); } - - @SaCheckPermission("dormRollCall:dormRollCall:query") - @GetMapping("/roomHistory/{roomId}") - public TableDataInfo roomHistory( - @NotNull(message = "房间ID不能为空") @PathVariable("roomId") Long roomId, - PageQuery pageQuery) { - return xgDormRollCallService.queryRoomHistory(roomId, pageQuery); - } } diff --git a/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/domain/vo/DormRollCallTodaySummaryVo.java b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/domain/vo/DormRollCallTodaySummaryVo.java index 3e89473e..4082f983 100644 --- a/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/domain/vo/DormRollCallTodaySummaryVo.java +++ b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/domain/vo/DormRollCallTodaySummaryVo.java @@ -8,7 +8,7 @@ import java.util.ArrayList; import java.util.List; /** - * 当前账号当天宿舍点名汇总 + * 当天宿舍点名人数汇总(全校或指定楼栋) */ @Data public class DormRollCallTodaySummaryVo implements Serializable { @@ -16,11 +16,11 @@ public class DormRollCallTodaySummaryVo implements Serializable { @Serial private static final long serialVersionUID = 1L; - /** 已点名寝室数(去重) */ - private Integer checkedRoomCount = 0; + /** 总人数:有床位且在住(status=0)学生数 */ + private Integer totalStudentCount = 0; - /** 未点名寝室数 */ - private Integer uncheckedRoomCount = 0; + /** 已点名人数:本日点名明细中有记录的去重学生数 */ + private Integer checkedStudentCount = 0; /** 请假学生数 */ private Integer leaveCount = 0; diff --git a/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/service/IXgDormRollCallService.java b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/service/IXgDormRollCallService.java index 41fe61a5..8c6dd955 100644 --- a/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/service/IXgDormRollCallService.java +++ b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/service/IXgDormRollCallService.java @@ -33,7 +33,10 @@ public interface IXgDormRollCallService { TableDataInfo queryRoomHistory(Long roomId, PageQuery pageQuery); /** - * 当前登录账号当天点名汇总(已/未点名寝室 + 请假/留宿/异常明细) + * 当天点名人数汇总(总人数 / 已点名人数 + 请假/留宿/异常明细)。 + *

+ * 总人数 = 有床位且在住学生数;已点名人数 = 本日点名明细有记录的去重学生数。 + * buildingId 为空时统计全校;有值时仅统计该楼。 */ - DormRollCallTodaySummaryVo queryTodaySummary(); + DormRollCallTodaySummaryVo queryTodaySummary(Long buildingId); } diff --git a/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/service/impl/XgDormRollCallServiceImpl.java b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/service/impl/XgDormRollCallServiceImpl.java index c7448bc6..f15e2545 100644 --- a/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/service/impl/XgDormRollCallServiceImpl.java +++ b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/service/impl/XgDormRollCallServiceImpl.java @@ -20,18 +20,14 @@ 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.domain.XgDormBedStudent; 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.mapper.XgDormBedStudentMapper; import org.dromara.xg.domain.XgStudentMsg; import org.dromara.xg.domain.XgDormRoom; import org.dromara.xg.domain.XgDormBuilding; @@ -57,9 +53,7 @@ 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; + private final XgDormBedStudentMapper bedStudentMapper; @Override public XgDormRollCallVo queryById(Long id) { @@ -282,31 +276,32 @@ public class XgDormRollCallServiceImpl implements IXgDormRollCallService { } @Override - public DormRollCallTodaySummaryVo queryTodaySummary() { + public DormRollCallTodaySummaryVo queryTodaySummary(Long buildingId) { DormRollCallTodaySummaryVo summary = new DormRollCallTodaySummaryVo(); - Long checkerId; - try { - checkerId = LoginHelper.getUserId(); - } catch (Exception e) { - return summary; - } - if (checkerId == null) { - return summary; - } + + // 总人数:有床位且在住(status=0) + LambdaQueryWrapper totalLqw = Wrappers.lambdaQuery() + .eq(XgDormBedStudent::getStatus, "0") + .eq(XgDormBedStudent::getDeleted, "0") + .isNotNull(XgDormBedStudent::getStudentId) + .isNotNull(XgDormBedStudent::getBedId) + .eq(buildingId != null, XgDormBedStudent::getBuildingId, buildingId); + Long totalStudents = bedStudentMapper.selectCount(totalLqw); + summary.setTotalStudentCount(totalStudents == null ? 0 : totalStudents.intValue()); Date dayBegin = DateUtil.beginOfDay(new Date()); Date dayEnd = DateUtil.endOfDay(new Date()); - List todayRecords = baseMapper.selectList( - Wrappers.lambdaQuery() - .eq(XgDormRollCall::getCheckerId, checkerId) - .eq(XgDormRollCall::getStatus, "0") - .ge(XgDormRollCall::getCheckDate, dayBegin) - .le(XgDormRollCall::getCheckDate, dayEnd) - .orderByDesc(XgDormRollCall::getCheckDate) - ); + // 全校或指定楼栋的当天点名主表 + LambdaQueryWrapper rollLqw = Wrappers.lambdaQuery() + .eq(XgDormRollCall::getStatus, "0") + .ge(XgDormRollCall::getCheckDate, dayBegin) + .le(XgDormRollCall::getCheckDate, dayEnd) + .eq(buildingId != null, XgDormRollCall::getBuildingId, buildingId) + .orderByDesc(XgDormRollCall::getCheckDate); + List todayRecords = baseMapper.selectList(rollLqw); - // 同一寝室多次点名时取最新一条,避免明细重复统计 + // 同一寝室多次点名时取最新一条 Map latestByRoom = new LinkedHashMap<>(); for (XgDormRollCall record : todayRecords) { if (record.getRoomId() == null) { @@ -314,31 +309,6 @@ public class XgDormRollCallServiceImpl implements IXgDormRollCallService { } latestByRoom.putIfAbsent(record.getRoomId(), record); } - Set checkedRoomIds = latestByRoom.keySet(); - summary.setCheckedRoomCount(checkedRoomIds.size()); - - Set scopeBuildingIds = resolveManagedBuildingIds(checkerId); - for (XgDormRollCall record : latestByRoom.values()) { - if (record.getBuildingId() != null) { - scopeBuildingIds.add(record.getBuildingId()); - } - } - - if (!scopeBuildingIds.isEmpty()) { - List scopeRooms = roomMapper.selectList( - Wrappers.lambdaQuery() - .in(XgDormRoom::getBuildingId, scopeBuildingIds) - .eq(XgDormRoom::getRoomStatus, "0") - ); - long unchecked = scopeRooms.stream() - .map(XgDormRoom::getId) - .filter(id -> !checkedRoomIds.contains(id)) - .count(); - summary.setUncheckedRoomCount((int) unchecked); - } else { - summary.setUncheckedRoomCount(0); - } - if (latestByRoom.isEmpty()) { return summary; } @@ -354,26 +324,34 @@ public class XgDormRollCallServiceImpl implements IXgDormRollCallService { Map rollCallMap = latestByRoom.values().stream() .collect(Collectors.toMap(XgDormRollCall::getId, r -> r, (a, b) -> a)); - List details = detailMapper.selectList( + // 已点名人数:明细中有学生记录的去重(含在寝/请假/留宿/异常,不含空床) + List allStudentDetails = detailMapper.selectList( Wrappers.lambdaQuery() .in(XgDormRollCallDetail::getRollCallId, rollCallIds) - .in(XgDormRollCallDetail::getRollCallStatus, Arrays.asList("1", "2", "4")) .isNotNull(XgDormRollCallDetail::getStudentId) .orderByAsc(XgDormRollCallDetail::getBedCode) ); + Set checkedStudentIds = allStudentDetails.stream() + .map(XgDormRollCallDetail::getStudentId) + .filter(Objects::nonNull) + .collect(Collectors.toSet()); + summary.setCheckedStudentCount(checkedStudentIds.size()); List leaveDetails = new ArrayList<>(); List holidayStayDetails = new ArrayList<>(); List absentDetails = new ArrayList<>(); - for (XgDormRollCallDetail detail : details) { - DormRollCallTodayDetailItemVo item = toTodayDetailItem(detail, rollCallMap.get(detail.getRollCallId())); + for (XgDormRollCallDetail detail : allStudentDetails) { String status = detail.getRollCallStatus(); + if (!"1".equals(status) && !"2".equals(status) && !"4".equals(status)) { + continue; + } + DormRollCallTodayDetailItemVo item = toTodayDetailItem(detail, rollCallMap.get(detail.getRollCallId())); if ("1".equals(status)) { leaveDetails.add(item); } else if ("4".equals(status)) { holidayStayDetails.add(item); - } else if ("2".equals(status)) { + } else { absentDetails.add(item); } } @@ -410,93 +388,6 @@ public class XgDormRollCallServiceImpl implements IXgDormRollCallService { return item; } - /** - * 解析当前账号宿舍管理员管辖的楼栋(楼栋权限表 + 授权来源表)。 - */ - private Set resolveManagedBuildingIds(Long userId) { - Set buildingIds = new HashSet<>(); - if (userId == null) { - return buildingIds; - } - List managers = dormManagerMapper.selectList( - Wrappers.lambdaQuery() - .eq(XgDormManager::getUserId, userId) - .eq(XgDormManager::getStatus, "0") - ); - if (managers == null || managers.isEmpty()) { - return buildingIds; - } - List managerIds = managers.stream().map(XgDormManager::getId).collect(Collectors.toList()); - - List managerBuildings = dormManagerBuildingMapper.selectList( - Wrappers.lambdaQuery() - .in(XgDormManagerBuilding::getManagerId, managerIds) - .eq(XgDormManagerBuilding::getStatus, "0") - ); - for (XgDormManagerBuilding mb : managerBuildings) { - if (mb.getBuildingId() != null) { - buildingIds.add(mb.getBuildingId()); - } - } - - List sources = dormManagerSourceMapper.selectList( - Wrappers.lambdaQuery() - .in(XgDormManagerSource::getManagerId, managerIds) - .eq(XgDormManagerSource::getStatus, "0") - ); - Set campusIds = new HashSet<>(); - Set areaIds = new HashSet<>(); - boolean allCampus = false; - for (XgDormManagerSource source : sources) { - String authType = source.getAuthType(); - Long authSource = source.getAuthSource(); - if ("1".equals(authType)) { - allCampus = true; - } else if ("2".equals(authType) && authSource != null) { - campusIds.add(authSource); - } else if ("3".equals(authType) && authSource != null) { - areaIds.add(authSource); - } else if ("4".equals(authType) && authSource != null) { - buildingIds.add(authSource); - } - } - if (allCampus) { - List allBuildings = buildingMapper.selectList( - Wrappers.lambdaQuery().select(XgDormBuilding::getId) - ); - for (XgDormBuilding b : allBuildings) { - if (b.getId() != null) { - buildingIds.add(b.getId()); - } - } - return buildingIds; - } - if (!campusIds.isEmpty() || !areaIds.isEmpty()) { - LambdaQueryWrapper buildingLqw = Wrappers.lambdaQuery() - .select(XgDormBuilding::getId); - buildingLqw.and(w -> { - boolean first = true; - if (!campusIds.isEmpty()) { - w.in(XgDormBuilding::getCampusId, campusIds); - first = false; - } - if (!areaIds.isEmpty()) { - if (!first) { - w.or(); - } - w.in(XgDormBuilding::getAreaId, areaIds); - } - }); - List buildings = buildingMapper.selectList(buildingLqw); - for (XgDormBuilding b : buildings) { - if (b.getId() != null) { - buildingIds.add(b.getId()); - } - } - } - return buildingIds; - } - private LambdaQueryWrapper buildQueryWrapper(XgDormRollCallBo bo) { LambdaQueryWrapper lqw = Wrappers.lambdaQuery(); lqw.like(StringUtils.isNotBlank(bo.getBatchNo()), XgDormRollCall::getBatchNo, bo.getBatchNo()); diff --git a/backend/script/sql/update/update_20261020_create_dorm_manager_building.sql b/backend/script/sql/update/update_20261020_create_dorm_manager_building.sql new file mode 100644 index 00000000..95bd04fd --- /dev/null +++ b/backend/script/sql/update/update_20261020_create_dorm_manager_building.sql @@ -0,0 +1,64 @@ +-- ============================================================ +-- 补建:管理员宿舍楼权限关系表 xg_dorm_manager_building +-- 背景:宿舍点名今日汇总(todaySummary)查询未点名寝室时依赖本表, +-- 开发/部分环境库缺失导致 SQLSyntaxErrorException。 +-- 执行库:hzdyjs_xg(先开发后生产) +-- 日期:2026-10-20 +-- ============================================================ + +-- 1. 建表(幂等) +CREATE TABLE IF NOT EXISTS `xg_dorm_manager_building` ( + `id` bigint(20) NOT NULL COMMENT '主键ID', + `manager_id` bigint(20) NOT NULL COMMENT '管理员ID(xg_dorm_manager.id)', + `building_id` bigint(20) NOT NULL COMMENT '宿舍楼ID(xg_dorm_building.id)', + `status` char(1) DEFAULT '0' COMMENT '状态(0有效 1已撤销)', + `deleted` char(1) DEFAULT '0' COMMENT '删除标志(0代表存在 1代表删除)', + `tenant_id` bigint(20) DEFAULT '0' COMMENT '租户编号', + `create_by` bigint(20) DEFAULT NULL COMMENT '创建者', + `create_dept` bigint(20) DEFAULT NULL COMMENT '创建部门', + `create_time` datetime DEFAULT NULL COMMENT '创建时间', + `update_by` bigint(20) DEFAULT NULL COMMENT '更新者', + `update_time` datetime DEFAULT NULL COMMENT '更新时间', + `remark` varchar(500) DEFAULT NULL COMMENT '备注', + PRIMARY KEY (`id`) USING BTREE, + KEY `idx_manager_id` (`manager_id`) USING BTREE, + KEY `idx_building_id` (`building_id`) USING BTREE, + KEY `idx_manager_building` (`manager_id`, `building_id`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC COMMENT='管理员宿舍楼权限关系表'; + +-- 2. 从授权来源表回填「楼栋级」权限(auth_type=4) +-- 使用 source.id 作为主键,避免重复执行产生重复数据 +INSERT INTO `xg_dorm_manager_building` ( + `id`, `manager_id`, `building_id`, `status`, `deleted`, + `tenant_id`, `create_by`, `create_dept`, `create_time`, + `update_by`, `update_time`, `remark` +) +SELECT + s.`id`, + s.`manager_id`, + s.`auth_source` AS `building_id`, + IFNULL(s.`status`, '0'), + IFNULL(s.`deleted`, '0'), + IFNULL(s.`tenant_id`, 0), + s.`create_by`, + s.`create_dept`, + IFNULL(s.`create_time`, NOW()), + s.`update_by`, + s.`update_time`, + IFNULL(s.`remark`, '从 xg_dorm_manager_source(auth_type=4) 回填') +FROM `xg_dorm_manager_source` s +WHERE IFNULL(s.`deleted`, '0') = '0' + AND s.`auth_type` = '4' + AND s.`auth_source` IS NOT NULL + AND NOT EXISTS ( + SELECT 1 + FROM `xg_dorm_manager_building` b + WHERE b.`id` = s.`id` + OR (b.`manager_id` = s.`manager_id` AND b.`building_id` = s.`auth_source` AND IFNULL(b.`deleted`, '0') = '0') + ); + +-- 3. 验收 +SELECT + (SELECT COUNT(*) FROM `xg_dorm_manager_building` WHERE IFNULL(deleted, '0') = '0') AS building_perm_count, + (SELECT COUNT(*) FROM `xg_dorm_manager_source` + WHERE IFNULL(deleted, '0') = '0' AND auth_type = '4' AND auth_source IS NOT NULL) AS source_building_count; diff --git a/backend/script/sql/update/update_20261021_leave_back_resubmit_diag_202513063048.sql b/backend/script/sql/update/update_20261021_leave_back_resubmit_diag_202513063048.sql new file mode 100644 index 00000000..2cec2086 --- /dev/null +++ b/backend/script/sql/update/update_20261021_leave_back_resubmit_diag_202513063048.sql @@ -0,0 +1,336 @@ +-- ============================================================================= +-- 请假打回后无法再审批:排查 + 场景化修复 +-- 学号:202513063048(朱晟睿,今早请假;班主任吴旭萍打回) +-- 日期:2026-10-21 +-- 现象:打回修改后再提交审不了;审批记录「申请人」待办办理人显示成班主任 +-- +-- 判断要点(先只跑 SELECT,确认场景后再解开对应 UPDATE): +-- 场景 A:实例 waiting + 业务仍 draft/back → 同步业务状态(昨天上线同类问题) +-- 场景 B:实例 back + 待办停在「申请人」 → 正常等学生重提;核对办理人应为学生 +-- 场景 C:同一实例多条残留 flow_task → 清理残留,只留目标节点最新一条 +-- 场景 D:实例 waiting + 待办在「班主任审批」但办理人不是班主任 → 改 flow_user +-- +-- 注意: +-- 1) 审批记录把「申请人」显示成班主任,多为展示层用当前登录人覆盖,不代表库内办错人 +-- 2) 禁止用 SQL 强行把「申请人」节点 skip 到「班主任审批」(易造成历史断层) +-- 3) 所有 UPDATE 默认注释,核对 leave_id / task_id / user_id 后再执行 +-- ============================================================================= + +SET @student_no = '202513063048'; + +-- ----------------------------------------------------------------------------- +-- 一、请假单 + 流程实例(核心对照:业务状态 vs 引擎状态) +-- ----------------------------------------------------------------------------- +SELECT + l.id AS leave_id, + l.student_name, + l.student_no, + l.class_id, + l.class_name, + l.leave_type_name, + l.leave_reason, + l.approval_status AS leave_approval_status, + l.start_time, + l.end_time, + l.create_by AS leave_create_by, + l.create_time, + l.update_time, + i.id AS instance_id, + i.flow_status AS instance_flow_status, + i.node_code, + i.node_name, + i.create_by AS instance_create_by, + fd.flow_code, + fd.flow_name, + CASE + WHEN i.id IS NULL THEN '无流程实例' + WHEN i.flow_status = 'waiting' AND IFNULL(l.approval_status, '') <> 'waiting' + THEN '场景A:引擎waiting、业务未waiting → 可修业务状态' + WHEN i.flow_status = 'back' AND i.node_name LIKE '%申请%' + THEN '场景B:已退回申请人 → 应学生重提,老师暂不能审' + WHEN i.flow_status = 'waiting' AND i.node_name LIKE '%班主任%' + THEN '待办应在班主任 → 再查办理人是否正确' + ELSE CONCAT('其它:instance=', IFNULL(i.flow_status, 'null'), + ', leave=', IFNULL(l.approval_status, 'null'), + ', node=', IFNULL(i.node_name, 'null')) + END AS diagnose_hint +FROM xg_student_leave l +LEFT JOIN flow_instance i + ON i.business_id = CAST(l.id AS CHAR) + AND IFNULL(i.del_flag, '0') = '0' +LEFT JOIN flow_definition fd + ON fd.id = i.definition_id + AND IFNULL(fd.del_flag, '0') = '0' +WHERE l.deleted = 0 + AND l.student_no = @student_no +ORDER BY l.create_time DESC; + +-- ----------------------------------------------------------------------------- +-- 二、当前运行待办 + 真实办理人(以 flow_user 为准,不是审批记录 UI) +-- ----------------------------------------------------------------------------- +SELECT + l.id AS leave_id, + i.id AS instance_id, + i.flow_status AS instance_flow_status, + i.node_name AS instance_node_name, + i.create_by AS instance_create_by, + t.id AS task_id, + t.node_code, + t.node_name, + t.flow_status AS task_flow_status, + t.create_time AS task_create_time, + u.id AS flow_user_id, + u.type AS flow_user_type, + u.processed_by AS assignee_user_id, + su.user_name AS assignee_user_name, + su.nick_name AS assignee_nick_name, + su.phonenumber AS assignee_phone, + CASE + WHEN t.node_name LIKE '%申请%' + AND CAST(u.processed_by AS CHAR) = CAST(i.create_by AS CHAR) + THEN 'OK:申请人待办挂在发起人' + WHEN t.node_name LIKE '%申请%' + AND CAST(u.processed_by AS CHAR) <> CAST(i.create_by AS CHAR) + THEN '异常:申请人待办办理人不是发起人 → 场景B修复' + WHEN t.node_name LIKE '%班主任%' + THEN '班主任待办:对照第三节 class_teacher_id' + ELSE '请人工判断' + END AS assignee_hint +FROM xg_student_leave l +INNER JOIN flow_instance i + ON i.business_id = CAST(l.id AS CHAR) + AND IFNULL(i.del_flag, '0') = '0' +INNER JOIN flow_task t + ON t.instance_id = i.id + AND IFNULL(t.del_flag, '0') = '0' +LEFT JOIN flow_user u + ON u.associated = t.id + AND IFNULL(u.del_flag, '0') = '0' +LEFT JOIN sys_user su + ON su.user_id = CAST(u.processed_by AS UNSIGNED) +WHERE l.deleted = 0 + AND l.student_no = @student_no +ORDER BY l.create_time DESC, t.create_time DESC; + +-- ----------------------------------------------------------------------------- +-- 三、班级班主任(对照 bzrspr / 待办办理人) +-- ----------------------------------------------------------------------------- +SELECT + l.id AS leave_id, + l.student_name, + l.class_id, + c.class_name, + c.class_teacher_id, + c.class_teacher_name, + su.user_name, + su.nick_name, + su.phonenumber +FROM xg_student_leave l +LEFT JOIN xg_class_msg c + ON c.id = l.class_id + AND c.deleted = 0 +LEFT JOIN sys_user su + ON su.user_id = c.class_teacher_id +WHERE l.deleted = 0 + AND l.student_no = @student_no +ORDER BY l.create_time DESC; + +-- ----------------------------------------------------------------------------- +-- 四、历史任务时间线(对照截图:申请人通过 → 班主任退回 → 当前申请人) +-- ----------------------------------------------------------------------------- +SELECT + h.id, + h.task_id, + h.node_name, + h.target_node_name, + h.skip_type, + h.flow_status, + h.approver, + su.nick_name AS approver_name, + su.phonenumber AS approver_phone, + h.message, + h.create_time AS start_time, + h.update_time AS end_time +FROM xg_student_leave l +INNER JOIN flow_instance i + ON i.business_id = CAST(l.id AS CHAR) + AND IFNULL(i.del_flag, '0') = '0' +INNER JOIN flow_his_task h + ON h.instance_id = i.id + AND IFNULL(h.del_flag, '0') = '0' +LEFT JOIN sys_user su + ON su.user_id = CAST(h.approver AS UNSIGNED) +WHERE l.deleted = 0 + AND l.student_no = @student_no +ORDER BY h.create_time ASC, h.id ASC; + +-- ----------------------------------------------------------------------------- +-- 五、残留待办检测(同一 instance 多条 flow_task 会导致重提取错 taskId) +-- ----------------------------------------------------------------------------- +SELECT + i.id AS instance_id, + COUNT(*) AS open_task_cnt, + GROUP_CONCAT(CONCAT(t.id, ':', t.node_name, ':', t.flow_status) + ORDER BY t.create_time DESC SEPARATOR ' | ') AS task_summary +FROM xg_student_leave l +INNER JOIN flow_instance i + ON i.business_id = CAST(l.id AS CHAR) + AND IFNULL(i.del_flag, '0') = '0' +INNER JOIN flow_task t + ON t.instance_id = i.id + AND IFNULL(t.del_flag, '0') = '0' +WHERE l.deleted = 0 + AND l.student_no = @student_no +GROUP BY i.id +HAVING COUNT(*) > 1; + +-- ----------------------------------------------------------------------------- +-- 六、学生账号(发起人 user_id,用于核对/修复申请人办理人) +-- ----------------------------------------------------------------------------- +SELECT + l.id AS leave_id, + l.student_id, + l.student_no, + l.student_name, + i.create_by AS instance_create_by, + su.user_id, + su.user_name, + su.nick_name, + su.phonenumber +FROM xg_student_leave l +LEFT JOIN flow_instance i + ON i.business_id = CAST(l.id AS CHAR) + AND IFNULL(i.del_flag, '0') = '0' +LEFT JOIN sys_user su + ON su.user_id = CAST(i.create_by AS UNSIGNED) + OR su.user_name = l.student_no +WHERE l.deleted = 0 + AND l.student_no = @student_no +ORDER BY l.create_time DESC; + +-- ============================================================================= +-- 以下为修复(全部注释)。先把上面 SELECT 结果贴出来对照,再按场景解开执行。 +-- 请把 @leave_id / @instance_id / @keep_task_id 换成第一步查出的真实值。 +-- ============================================================================= + +-- SET @leave_id = 0; -- 第一步 leave_id +-- SET @instance_id = 0; -- 第一步 instance_id +-- SET @keep_task_id = 0; -- 要保留的 task_id(通常最新申请人或班主任任务) + +-- ----------------------------------------------------------------------------- +-- 修复 A:引擎 waiting,业务仍 draft/back(老师办理页无审批按钮) +-- 条件:第一节 diagnose_hint = 场景A +-- ----------------------------------------------------------------------------- +-- UPDATE xg_student_leave l +-- INNER JOIN flow_instance i +-- ON i.business_id = CAST(l.id AS CHAR) +-- AND IFNULL(i.del_flag, '0') = '0' +-- SET l.approval_status = 'waiting', +-- l.update_time = NOW() +-- WHERE l.deleted = 0 +-- AND l.id = @leave_id +-- AND i.flow_status = 'waiting' +-- AND IFNULL(l.approval_status, '') <> 'waiting'; + +-- 核对: +-- SELECT id, approval_status, update_time FROM xg_student_leave WHERE id = @leave_id; + +-- ----------------------------------------------------------------------------- +-- 修复 B1:实例 back,业务不是 back(展示混乱)→ 业务对齐 back +-- 条件:实例 flow_status=back,待办在申请人 +-- ----------------------------------------------------------------------------- +-- UPDATE xg_student_leave l +-- INNER JOIN flow_instance i +-- ON i.business_id = CAST(l.id AS CHAR) +-- AND IFNULL(i.del_flag, '0') = '0' +-- SET l.approval_status = 'back', +-- l.update_time = NOW() +-- WHERE l.deleted = 0 +-- AND l.id = @leave_id +-- AND i.flow_status = 'back' +-- AND IFNULL(l.approval_status, '') <> 'back'; + +-- ----------------------------------------------------------------------------- +-- 修复 B2:申请人待办办理人不是发起人 → 改回 instance.create_by(学生) +-- 条件:第二节 assignee_hint = 异常 +-- 先查再改: +-- ----------------------------------------------------------------------------- +-- SELECT u.id, u.associated, u.processed_by, i.create_by +-- FROM flow_user u +-- INNER JOIN flow_task t ON t.id = u.associated AND IFNULL(t.del_flag,'0')='0' +-- INNER JOIN flow_instance i ON i.id = t.instance_id +-- WHERE i.id = @instance_id +-- AND t.node_name LIKE '%申请%' +-- AND IFNULL(u.del_flag,'0') = '0'; + +-- UPDATE flow_user u +-- INNER JOIN flow_task t +-- ON t.id = u.associated +-- AND IFNULL(t.del_flag, '0') = '0' +-- INNER JOIN flow_instance i +-- ON i.id = t.instance_id +-- AND IFNULL(i.del_flag, '0') = '0' +-- SET u.processed_by = CAST(i.create_by AS CHAR), +-- u.update_time = NOW() +-- WHERE i.id = @instance_id +-- AND t.node_name LIKE '%申请%' +-- AND IFNULL(u.del_flag, '0') = '0' +-- AND CAST(u.processed_by AS CHAR) <> CAST(i.create_by AS CHAR); + +-- ----------------------------------------------------------------------------- +-- 修复 C:清理残留待办(只保留 @keep_task_id,其余逻辑删除) +-- 条件:第五节 open_task_cnt > 1;keep 选「目标节点最新一条」 +-- ----------------------------------------------------------------------------- +-- 预览将删除的任务: +-- SELECT id, node_name, flow_status, create_time +-- FROM flow_task +-- WHERE instance_id = @instance_id +-- AND IFNULL(del_flag, '0') = '0' +-- AND id <> @keep_task_id; + +-- UPDATE flow_user +-- SET del_flag = '1', update_time = NOW() +-- WHERE associated IN ( +-- SELECT id FROM ( +-- SELECT id FROM flow_task +-- WHERE instance_id = @instance_id +-- AND IFNULL(del_flag, '0') = '0' +-- AND id <> @keep_task_id +-- ) x +-- ) +-- AND IFNULL(del_flag, '0') = '0'; + +-- UPDATE flow_task +-- SET del_flag = '1', update_time = NOW() +-- WHERE instance_id = @instance_id +-- AND IFNULL(del_flag, '0') = '0' +-- AND id <> @keep_task_id; + +-- ----------------------------------------------------------------------------- +-- 修复 D:待办已在「班主任审批」且实例 waiting,但办理人不是班主任 +-- 条件:第二节 node=班主任审批,processed_by ≠ 第三节 class_teacher_id +-- ----------------------------------------------------------------------------- +-- SET @teacher_user_id = 0; -- 第三节 class_teacher_id +-- SET @bzr_task_id = 0; -- 第二节班主任任务 task_id + +-- UPDATE flow_user +-- SET processed_by = CAST(@teacher_user_id AS CHAR), +-- update_time = NOW() +-- WHERE associated = @bzr_task_id +-- AND IFNULL(del_flag, '0') = '0' +-- AND type = '1'; + +-- 同步业务 waiting(若仍不是): +-- UPDATE xg_student_leave +-- SET approval_status = 'waiting', update_time = NOW() +-- WHERE id = @leave_id +-- AND deleted = 0 +-- AND IFNULL(approval_status, '') <> 'waiting'; + +-- ----------------------------------------------------------------------------- +-- 修复后复查(建议整段再跑一遍) +-- ----------------------------------------------------------------------------- +-- 重跑:一、二、四、五 +-- 期望: +-- - 若仍 back+申请人:学生账号打开请假单应有「提交」,提交后 instance→waiting、node→班主任审批 +-- - 若 waiting+班主任:flow_user.processed_by = 班主任 user_id,老师待办可审 diff --git a/backend/script/sql/update/update_20261021_rollcall_today_summary_demo_data.sql b/backend/script/sql/update/update_20261021_rollcall_today_summary_demo_data.sql new file mode 100644 index 00000000..1a06cb1d --- /dev/null +++ b/backend/script/sql/update/update_20261021_rollcall_today_summary_demo_data.sql @@ -0,0 +1,25 @@ +-- ============================================================================= +-- 今日点名人数汇总 · 开发库演示数据 +-- 日期:2026-10-21 +-- 批次:RC_TEST_20261021_DEMO +-- 范围:临安6号楼 6102/6103/6104(各 6 人:3在寝+1请假+1异常+1假期留宿) +-- +-- 期望(插入后、当日): +-- 列表页全校:总人数≈558,已点名=18,请假=3,假期留宿=3,异常=3 +-- 选房页临安6号楼:总人数≈333,已点=18,请假/留宿/异常=3/3/3(可点名单) +-- +-- 清理: +-- DELETE FROM xg_dorm_roll_call_detail +-- WHERE roll_call_id IN (SELECT id FROM xg_dorm_roll_call WHERE batch_no='RC_TEST_20261021_DEMO'); +-- DELETE FROM xg_dorm_roll_call WHERE batch_no='RC_TEST_20261021_DEMO'; +-- ============================================================================= + +-- 本文件仅作说明与清理;造数已由脚本写入开发库。 +-- 若需重造:联系开发用 Python 造数脚本重跑,或手工点名覆盖。 + +SELECT + r.id, r.batch_no, r.building_name, r.room_name, + r.present_count, r.leave_count, r.holiday_stay_count, r.absent_count, r.check_date +FROM xg_dorm_roll_call r +WHERE r.batch_no = 'RC_TEST_20261021_DEMO' +ORDER BY r.room_name; diff --git a/backend/script/sql/update/请假一年级代理测试账号说明.md b/backend/script/sql/update/请假一年级代理测试账号说明.md new file mode 100644 index 00000000..6dc1c3c1 --- /dev/null +++ b/backend/script/sql/update/请假一年级代理测试账号说明.md @@ -0,0 +1,44 @@ +# 请假一年级代理学院 - 开发环境测试账号 + +> 已写入开发库 `47.99.164.66 / hzdyjs_xg` +> 统一密码:`Hzdyjs123456!@#` +> 系统参数:`xg.studentLeave.freshmanProxyCollegeId = 202512231307170003`(人文学院) +> 学年规则:每年 9 月 1 日切届;当前(2026-09)入学年 **2026=一年级**,**2025=二年级及以上** + +## 学生账号 + +| 用户名 | 姓名 | 说明 | 期望 | +|--------|------|------|------| +| `qjtest_stu_y1` | 请假测试一年级 | 数字信息学院 / 测试班级11 / 入学年 2026 | 学院审批、分管领导 → **人文学院**测试账号 | +| `qjtest_stu_y2` | 请假测试二年级 | 数字信息学院 / 电子商务251 / 入学年 2025 | 学院审批、分管领导 → **数字信息学院**测试账号 | +| `qjtest_stu_noey` | 请假测试无入学年 | 无班级、无入学年 | 提交请假应被拒绝(入学年不完整) | + +## 审批账号 + +| 用户名 | 姓名 | 角色 | 用途 | +|--------|------|------|------| +| `qjtest_bzr` | 请假测试班主任 | 班主任 | 一年级班班主任(测试班级11已挂此班主任) | +| `qjtest_rw_ejxy` | 请假测试人文院审 | 请假二级学院审批人 | 一年级应走到此人 | +| `qjtest_rw_fgld` | 请假测试人文分管 | 请假分管院领导 + xg_college_leader | 一年级长假分管领导应走到此人 | +| `qjtest_sz_ejxy` | 请假测试数信院审 | 请假二级学院审批人 | **对照**:二年级应走此人,一年级不应走 | +| `qjtest_sz_fgld` | 请假测试数信分管 | 分管领导 + xg_college_leader | **对照**:二年级长假应走此人 | +| `qjtest_xsc` | 请假测试学生处 | 请假学生处审批人 | 请假天数 >3 时学生处节点(角色办理) | + +## 建议测试步骤 + +1. 重启后端(或刷新参数缓存),确保读到 `sys_config`。 +2. 用 `qjtest_stu_y1` 登录,申请 **>1 天** 请假并提交;待办应出现在 `qjtest_rw_ejxy`,而不是 `qjtest_sz_ejxy`。 +3. 用 `qjtest_stu_y2` 同样操作;待办应在 `qjtest_sz_ejxy`。 +4. 用 `qjtest_stu_noey` 提交;应提示入学年不完整、无法提交。 +5. 可选:一年级申请 **>7 天**,分管领导待办应在 `qjtest_rw_fgld`。 + +## 清理(如需) + +```sql +DELETE FROM sys_user_role WHERE user_id BETWEEN 2090161608000000001 AND 2090161608000000013; +DELETE FROM sys_user WHERE user_name LIKE 'qjtest%'; +DELETE FROM xg_student_msg WHERE student_no LIKE 'qjtest%'; +DELETE FROM xg_college_leader WHERE remark = '请假一年级代理测试数据'; +DELETE FROM sys_config WHERE config_key = 'xg.studentLeave.freshmanProxyCollegeId'; +-- 如需恢复测试班级11原班主任,请按业务另行改回 +``` diff --git a/docs/开发文档/API/宿舍点名假期留宿标识说明.md b/docs/开发文档/API/宿舍点名假期留宿标识说明.md index 65cd99f7..3be7df30 100644 --- a/docs/开发文档/API/宿舍点名假期留宿标识说明.md +++ b/docs/开发文档/API/宿舍点名假期留宿标识说明.md @@ -42,16 +42,23 @@ 部署时需先执行该脚本,再发布后端与前端。 -## 当天点名汇总(移动端首页) +## 当天点名人数汇总(移动端) -接口:`GET /dormRollCall/dormRollCall/todaySummary` +接口:`GET /dormRollCall/dormRollCall/todaySummary?buildingId=`(`buildingId` 可选) 权限:`dormRollCall:dormRollCall:list1` -口径: +口径(2026-10-21): -- **已点名寝室**:当前账号当天有效点名记录的去重房间数(同房多次取最新) -- **未点名寝室**:管辖楼栋(宿舍管理员权限)∪ 当天已点过的楼栋 下可用房间 − 已点名 -- **请假 / 假期留宿 / 异常**:最新点名明细中状态 `1` / `4` / `2`,可点开看明细 +| 字段 | 含义 | +|------|------| +| `totalStudentCount` | 总人数:`xg_dorm_bed_student` 中 status=0 且有床位、有学生的在住人数 | +| `checkedStudentCount` | 已点名人数:本日各寝室最新点名明细中有学生记录的去重人数 | +| `leaveCount` / `holidayStayCount` / `absentCount` | 最新明细状态 `1` / `4` / `2`,并返回对应 `*Details` 名单 | -仅供移动端首页展示,不影响点名提交与 Web 列表。 +范围: + +- **不传 buildingId**:全校(列表页「今日点名汇总」) +- **传 buildingId**:指定楼栋(点名选房页红圈统计;请假/留宿/异常可点开本楼名单) + +移动端刷新:列表页 `onShow`/筛选;选房页加载房间、单间点名提交后刷新本楼汇总。 diff --git a/mobile/src/api/dormRollCall.js b/mobile/src/api/dormRollCall.js index 2d617fe2..7b5d19cc 100644 --- a/mobile/src/api/dormRollCall.js +++ b/mobile/src/api/dormRollCall.js @@ -8,11 +8,12 @@ export const listDormRollCall = (query) => { }); }; -/** 当前账号当天点名汇总 */ -export const getTodayRollCallSummary = () => { +/** 当天点名人数汇总:不传=全校,传 buildingId=本楼 */ +export const getTodayRollCallSummary = (buildingId) => { return request({ url: '/dormRollCall/dormRollCall/todaySummary', - method: 'GET' + method: 'GET', + params: buildingId ? { buildingId } : undefined }); }; diff --git a/mobile/src/components/Process/submitVerify.vue b/mobile/src/components/Process/submitVerify.vue index 9910f17b..7e751732 100644 --- a/mobile/src/components/Process/submitVerify.vue +++ b/mobile/src/components/Process/submitVerify.vue @@ -17,47 +17,31 @@ - - + + 请假信息 + + + 请假类型 + {{ leaveInfo.leaveTypeName || '-' }} - - --> - - - - 附件 - + + 开始时间 + {{ formatLeaveDateTime(leaveInfo.startTime) }} + + + 结束时间 + {{ formatLeaveDateTime(leaveInfo.endTime) }} + + + 是否离校 + {{ formatLeaveSchool(leaveInfo.isLeaveSchool) }} + + + 请假原因 + {{ leaveInfo.leaveReason || '-' }} + + @@ -248,14 +232,14 @@ /> - + @@ -365,14 +349,14 @@ - + @@ -480,6 +464,7 @@ export default { title: '提示', }, deleteSignatureVisible: false, + leaveInfo: null, form: { taskId: undefined, message: undefined, @@ -499,6 +484,11 @@ export default { }, }; }, + computed: { + showLeaveSummary() { + return !!(this.leaveInfo && (this.leaveInfo.leaveTypeName || this.leaveInfo.leaveReason)); + }, + }, methods: { // 打开弹窗 async openDialog(id) { @@ -507,6 +497,7 @@ export default { this.form.fileId = undefined; this.taskId = id; this.form.message = undefined; + this.leaveInfo = null; this.visible = true; this.loading = true; this.buttonDisabled = true; @@ -539,6 +530,8 @@ export default { if (nextData.code === 200) { this.nestNodeList = nextData.data; } + + await this.loadLeaveSummaryIfNeeded(); } } catch (error) { console.error('获取任务信息失败:', error); @@ -551,12 +544,56 @@ export default { } }, + isLeaveFlowTask() { + const flowCode = (this.task && this.task.flowCode) || ''; + const formPath = (this.task && this.task.formPath) || ''; + return flowCode === 'hzdyjs-qj' || formPath.indexOf('xsqj') !== -1 || formPath.indexOf('studentLeave') !== -1; + }, + + async loadLeaveSummaryIfNeeded() { + if (!this.isLeaveFlowTask() || !this.task.businessId) { + this.leaveInfo = null; + return; + } + try { + const response = await request({ + url: `/studentLeave/studentLeave/${this.task.businessId}`, + method: 'GET', + }); + if (response.code === 200 && response.data) { + this.leaveInfo = response.data; + } else { + this.leaveInfo = null; + } + } catch (error) { + console.warn('加载请假摘要失败(不影响审批):', error); + this.leaveInfo = null; + } + }, + + formatLeaveDateTime(value) { + if (!value) return '-'; + const date = value instanceof Date ? value : new Date(value); + if (Number.isNaN(date.getTime())) { + return String(value); + } + const pad = (n) => (n < 10 ? `0${n}` : `${n}`); + return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}`; + }, + + formatLeaveSchool(value) { + if (value === '1' || value === 1) return '离校'; + if (value === '0' || value === 0) return '不离校'; + return value || '-'; + }, + // 关闭弹窗 cancel() { this.visible = false; this.buttonDisabled = false; this.nickName = {}; this.form.assigneeMap = {}; + this.leaveInfo = null; this.$emit('cancelCallback'); }, @@ -648,29 +685,34 @@ export default { } }, - // 退回弹窗打开 + // 点击退回:仅一个可退回节点时直接确认提交;多个节点时再打开选择弹窗 async handleBackProcessOpen() { - this.backForm = {}; - this.backForm.messageType = ['1']; - this.backVisible = true; - this.backLoading = true; - this.backButtonDisabled = true; + if (!this.task || !this.task.definitionId || !this.task.nodeCode) { + uni.showToast({ title: '任务信息不完整,无法退回', icon: 'none' }); + return; + } + uni.showLoading({ title: '加载中', mask: true }); + let nodes = []; try { const response = await request({ url: `/workflow/task/getBackTaskNode/${this.task.definitionId}/${this.task.nodeCode}`, method: 'GET', }); - if (response.code === 200) { - this.taskNodeList = response.data; - this.backLoading = false; - this.backButtonDisabled = false; - if (this.taskNodeList.length > 0) { - this.selectedBackNodeIndex = 0; - this.backForm.nodeCode = this.taskNodeList[0].nodeCode; - this.selectedBackNodeName = this.taskNodeList[0].nodeName; - } + if (response.code !== 200) { + uni.showToast({ + title: response.msg || '获取退回节点失败', + icon: 'none', + }); + return; + } + + nodes = response.data || []; + this.taskNodeList = nodes; + if (nodes.length === 0) { + uni.showToast({ title: '暂无可退回节点', icon: 'none' }); + return; } } catch (error) { console.error('获取退回节点失败:', error); @@ -678,7 +720,44 @@ export default { title: '获取退回节点失败', icon: 'none', }); + return; + } finally { + // 必须先关掉 loading,再弹确认框;否则 mask 会挡住「确定/取消」 + uni.hideLoading(); } + + // 常见请假场景:只有「申请人」一个节点,直接确认,不再进二级驳回页 + if (nodes.length === 1) { + const node = nodes[0]; + const opinion = (this.form && this.form.message) || ''; + const result = await this.showConfirm( + `确认退回到【${node.nodeName || '申请人'}】?`, + ); + if (!result.confirm) { + return; + } + await this.doBackProcess({ + taskId: this.taskId, + nodeCode: node.nodeCode, + message: opinion, + messageType: ['1'], + fileId: this.form && this.form.fileId, + }); + return; + } + + // 多个可退回节点:打开选择弹窗 + this.backForm = { + messageType: ['1'], + message: (this.form && this.form.message) || '', + fileId: this.form && this.form.fileId, + }; + this.selectedBackNodeIndex = 0; + this.backForm.nodeCode = nodes[0].nodeCode; + this.selectedBackNodeName = nodes[0].nodeName; + this.backButtonDisabled = false; + this.backLoading = false; + this.backVisible = true; }, // 确认选择驳回节点 @@ -703,43 +782,50 @@ export default { this.selectedBackNodeName = ''; }, - // 退回流程 + // 退回弹窗内点「确认」 async handleBackProcess() { + if (!this.backForm || !this.backForm.nodeCode) { + uni.showToast({ title: '请选择驳回节点', icon: 'none' }); + return; + } this.backForm.taskId = this.taskId; + const result = await this.showConfirm('是否确认驳回?'); + if (!result.confirm) { + return; + } + await this.doBackProcess(this.backForm); + }, + // 实际调用退回接口并关闭弹窗、刷新列表 + async doBackProcess(payload) { + this.loading = true; + this.backLoading = true; + this.backButtonDisabled = true; try { - const result = await this.showConfirm('是否确认驳回?'); - if (result.confirm) { - this.loading = true; - this.backLoading = true; - this.backButtonDisabled = true; + const response = await request({ + url: '/workflow/task/backProcess', + method: 'POST', + data: payload, + }); - const response = await request({ - url: '/workflow/task/backProcess', - method: 'POST', - data: this.backForm, + if (response.code === 200) { + this.visible = false; + this.closeBackDialog(); + this.$emit('submit-callback'); + uni.showToast({ + title: '退回成功', + icon: 'success', + }); + } else { + uni.showToast({ + title: response.msg || '退回失败', + icon: 'none', }); - - if (response.code === 200) { - this.visible = false; - this.closeBackDialog(); - console.log('驳回成功'); - this.$emit('submit-callback'); - uni.showToast({ - title: '操作成功', - icon: 'success', - }); - } else { - uni.showToast({ - title: response.msg || '操作失败', - icon: 'none', - }); - } } } catch (error) { console.error('退回失败:', error); uni.showToast({ - title: '操作失败', + title: '退回失败', icon: 'none', }); } finally { @@ -1168,6 +1254,44 @@ export default { margin-bottom: 16rpx; } +.leave-summary-card { + background: #f7f9fc; + border: 1rpx solid #e8eef7; + border-radius: 12rpx; + padding: 8rpx 24rpx; +} + +.leave-summary-row { + display: flex; + align-items: flex-start; + padding: 16rpx 0; + border-bottom: 1rpx solid #edf1f7; +} + +.leave-summary-row:last-child { + border-bottom: none; +} + +.leave-summary-label { + width: 160rpx; + flex-shrink: 0; + font-size: 26rpx; + color: #667085; + line-height: 1.5; +} + +.leave-summary-value { + flex: 1; + font-size: 26rpx; + color: #1f2937; + line-height: 1.5; + word-break: break-all; +} + +.leave-summary-row--reason .leave-summary-value { + white-space: pre-wrap; +} + .checkbox-row { display: flex; gap: 40rpx; diff --git a/mobile/src/pages/task/taskWaiting.vue b/mobile/src/pages/task/taskWaiting.vue index 2c007f23..abcda617 100644 --- a/mobile/src/pages/task/taskWaiting.vue +++ b/mobile/src/pages/task/taskWaiting.vue @@ -74,7 +74,10 @@ {{ index + 1 }}. - {{ item.flowName || '-' }} + {{ getTaskShortTitle(item) }} - 今日点名汇总 + + 今日点名汇总 + 全校 + {{ todayLabel }} - {{ todaySummary.checkedRoomCount || 0 }} - 已点名寝室 + {{ todaySummary.totalStudentCount || 0 }} + 总人数 - {{ todaySummary.uncheckedRoomCount || 0 }} - 未点名寝室 + {{ todaySummary.checkedStudentCount || 0 }} + 已点名人数 {{ todaySummary.leaveCount || 0 }} @@ -243,8 +246,8 @@ export default { boxSizing: 'border-box', }, todaySummary: { - checkedRoomCount: 0, - uncheckedRoomCount: 0, + totalStudentCount: 0, + checkedStudentCount: 0, leaveCount: 0, holidayStayCount: 0, absentCount: 0, @@ -281,17 +284,23 @@ export default { }, onShow() { this.loadBuildingOptions(); - this.getList(); - this.loadTodaySummary(); + this.refreshPageData(); }, methods: { + /** 列表 + 全校汇总一起刷新 */ + refreshPageData() { + this.getList(); + this.loadTodaySummary(); + }, + async loadTodaySummary() { try { + // 图1:始终全校汇总,不按筛选楼栋收窄 const res = await getTodayRollCallSummary(); if (res && res.code === 200 && res.data) { this.todaySummary = { - checkedRoomCount: res.data.checkedRoomCount || 0, - uncheckedRoomCount: res.data.uncheckedRoomCount || 0, + totalStudentCount: res.data.totalStudentCount || 0, + checkedStudentCount: res.data.checkedStudentCount || 0, leaveCount: res.data.leaveCount || 0, holidayStayCount: res.data.holidayStayCount || 0, absentCount: res.data.absentCount || 0, @@ -344,13 +353,13 @@ export default { handleQuery() { this.queryParams.pageNum = 1; - this.getList(); + this.refreshPageData(); }, resetQuery() { this.queryParams = { pageNum: 1, pageSize: 20, batchNo: '', buildingId: '' }; this.selectedBuildingName = ''; - this.getList(); + this.refreshPageData(); }, handleBuildingConfirm(e) { @@ -359,6 +368,9 @@ export default { if (selected) { this.queryParams.buildingId = selected.value; this.selectedBuildingName = selected.text; + // 选楼后立即刷新该楼今日汇总 + this.queryParams.pageNum = 1; + this.refreshPageData(); } this.showBuildingPicker = false; }, @@ -462,15 +474,28 @@ export default { align-items: center; margin-bottom: 20rpx; + .summary-title-wrap { + display: flex; + flex-direction: column; + gap: 4rpx; + min-width: 0; + } + .summary-title { font-size: 30rpx; font-weight: 600; color: #303133; } + .summary-scope { + font-size: 22rpx; + color: #909399; + } + .summary-date { font-size: 24rpx; color: #909399; + flex-shrink: 0; } } diff --git a/mobile/src/pages/xgss/ssdm/ssdmCheck.vue b/mobile/src/pages/xgss/ssdm/ssdmCheck.vue index 94e0b121..34ba9ee3 100644 --- a/mobile/src/pages/xgss/ssdm/ssdmCheck.vue +++ b/mobile/src/pages/xgss/ssdm/ssdmCheck.vue @@ -40,8 +40,17 @@ 宿舍房间列表 - 总: {{ rooms.length }} - 已点名: {{ completedRooms.length }} + 总人数: {{ buildingSummary.totalStudentCount || 0 }} + 已点: {{ buildingSummary.checkedStudentCount || 0 }} + + 请假: {{ buildingSummary.leaveCount || 0 }} + + + 留宿: {{ buildingSummary.holidayStayCount || 0 }} + + + 异常: {{ buildingSummary.absentCount || 0 }} + @@ -110,13 +119,49 @@ + + + + + {{ detailPopupTitle }} + + + + + + + {{ row.studentName || '-' }} + {{ row.studentNo || '-' }} + + + {{ row.roomName || '-' }} + · {{ row.bedCode }}床 + + 班级:{{ row.className }} + + 请假类型:{{ row.leaveTypeName }} + + + 假期类型:{{ row.holidayType }} + + + + +