fix(leave/dorm): 打回重提、审批摘要与待办标题优化

修复请假/销假打回后空 flowCode 导致启动失败;审批弹窗展示请假摘要并去掉附件;待办标题短显请假/销假;点名 todaySummary 与楼栋关联脚本一并同步。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
“wuzhen”andCursor committed 2026-10-21 11:24:48 +08:00
1 parent de223a1f5a
commit 15ed3ec9fb
20 files changed
+1136 -320

No files matched your search

@@ -29,9 +29,8 @@ public class StartProcessBo implements Serializable {
private String businessId;
/**
* 流程定义编码
* 流程定义编码(首次启动必填;草稿/退回/撤销后重新提交时实例已存在,可为空)
*/
@NotBlank(message = "流程定义编码不能为空", groups = {AddGroup.class})
private String flowCode;
/**
@@ -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())
@@ -39,17 +39,44 @@ public class XgDormRollCallController extends BaseController {
}
/**
* 当前账号当天点名汇总(已/未点名寝室 + 请假/留宿/异常明细)
* 须声明在 /{id} 之前,避免被路径变量吞掉
* 当天点名人数汇总(总人数/已点名人数 + 请假/留宿/异常明细)
*
* @param buildingId 可选;空=全校,有值=指定楼栋
*/
@SaCheckPermission("dormRollCall:dormRollCall:list1")
@GetMapping("/todaySummary")
public R<DormRollCallTodaySummaryVo> todaySummary() {
return R.ok(xgDormRollCallService.queryTodaySummary());
public R<DormRollCallTodaySummaryVo> todaySummary(
@RequestParam(value = "buildingId", required = false) Long buildingId) {
return R.ok(xgDormRollCallService.queryTodaySummary(buildingId));
}
@SaCheckPermission("dormRollCall:dormRollCall:add")
@GetMapping("/generateBatchNo")
public R<String> generateBatchNo() {
return R.ok(xgDormRollCallService.generateBatchNo());
}
@SaCheckPermission("dormRollCall:dormRollCall:list1")
@GetMapping("/buildingRoomsStatus")
public R<List<RoomRollCallStatusVo>> 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<XgDormRollCallVo> 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<XgDormRollCallVo> 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<String> generateBatchNo() {
return R.ok(xgDormRollCallService.generateBatchNo());
}
@SaCheckPermission("dormRollCall:dormRollCall:list1")
@GetMapping("/buildingRoomsStatus")
public R<List<RoomRollCallStatusVo>> 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<XgDormRollCallVo> roomHistory(
@NotNull(message = "房间ID不能为空") @PathVariable("roomId") Long roomId,
PageQuery pageQuery) {
return xgDormRollCallService.queryRoomHistory(roomId, pageQuery);
}
}
@@ -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;
@@ -33,7 +33,10 @@ public interface IXgDormRollCallService {
TableDataInfo<XgDormRollCallVo> queryRoomHistory(Long roomId, PageQuery pageQuery);
/**
* 当前登录账号当天点名汇总(已/未点名寝室 + 请假/留宿/异常明细)
* 当天点名人数汇总(总人数 / 已点名人数 + 请假/留宿/异常明细)。
* <p>
* 总人数 = 有床位且在住学生数;已点名人数 = 本日点名明细有记录的去重学生数。
* buildingId 为空时统计全校;有值时仅统计该楼。
*/
DormRollCallTodaySummaryVo queryTodaySummary();
DormRollCallTodaySummaryVo queryTodaySummary(Long buildingId);
}
@@ -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<XgDormBedStudent> totalLqw = Wrappers.<XgDormBedStudent>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<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)
);
// 全校或指定楼栋的当天点名主表
LambdaQueryWrapper<XgDormRollCall> rollLqw = Wrappers.<XgDormRollCall>lambdaQuery()
.eq(XgDormRollCall::getStatus, "0")
.ge(XgDormRollCall::getCheckDate, dayBegin)
.le(XgDormRollCall::getCheckDate, dayEnd)
.eq(buildingId != null, XgDormRollCall::getBuildingId, buildingId)
.orderByDesc(XgDormRollCall::getCheckDate);
List<XgDormRollCall> todayRecords = baseMapper.selectList(rollLqw);
// 同一寝室多次点名时取最新一条,避免明细重复统计
// 同一寝室多次点名时取最新一条
Map<Long, XgDormRollCall> 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<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;
}
@@ -354,26 +324,34 @@ public class XgDormRollCallServiceImpl implements IXgDormRollCallService {
Map<Long, XgDormRollCall> rollCallMap = latestByRoom.values().stream()
.collect(Collectors.toMap(XgDormRollCall::getId, r -> r, (a, b) -> a));
List<XgDormRollCallDetail> details = detailMapper.selectList(
// 已点名人数:明细中有学生记录的去重(含在寝/请假/留宿/异常,不含空床)
List<XgDormRollCallDetail> allStudentDetails = detailMapper.selectList(
Wrappers.<XgDormRollCallDetail>lambdaQuery()
.in(XgDormRollCallDetail::getRollCallId, rollCallIds)
.in(XgDormRollCallDetail::getRollCallStatus, Arrays.asList("1", "2", "4"))
.isNotNull(XgDormRollCallDetail::getStudentId)
.orderByAsc(XgDormRollCallDetail::getBedCode)
);
Set<Long> checkedStudentIds = allStudentDetails.stream()
.map(XgDormRollCallDetail::getStudentId)
.filter(Objects::nonNull)
.collect(Collectors.toSet());
summary.setCheckedStudentCount(checkedStudentIds.size());
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()));
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<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());
@@ -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;
@@ -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,老师待办可审
@@ -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;
@@ -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原班主任,请按业务另行改回
```