fix(leave/dorm): 打回重提、审批摘要与待办标题优化
修复请假/销假打回后空 flowCode 导致启动失败;审批弹窗展示请假摘要并去掉附件;待办标题短显请假/销假;点名 todaySummary 与楼栋关联脚本一并同步。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
1 parent
de223a1f5a
commit
15ed3ec9fb
20 files changed
+1136
-320
No files matched your search
+1
-2
@@ -29,9 +29,8 @@ public class StartProcessBo implements Serializable {
|
||||
private String businessId;
|
||||
|
||||
/**
|
||||
* 流程定义编码
|
||||
* 流程定义编码(首次启动必填;草稿/退回/撤销后重新提交时实例已存在,可为空)
|
||||
*/
|
||||
@NotBlank(message = "流程定义编码不能为空", groups = {AddGroup.class})
|
||||
private String flowCode;
|
||||
|
||||
/**
|
||||
|
||||
+4
@@ -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())
|
||||
|
||||
+32
-27
@@ -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);
|
||||
}
|
||||
}
|
||||
+5
-5
@@ -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;
|
||||
|
||||
+5
-2
@@ -33,7 +33,10 @@ public interface IXgDormRollCallService {
|
||||
TableDataInfo<XgDormRollCallVo> queryRoomHistory(Long roomId, PageQuery pageQuery);
|
||||
|
||||
/**
|
||||
* 当前登录账号当天点名汇总(已/未点名寝室 + 请假/留宿/异常明细)
|
||||
* 当天点名人数汇总(总人数 / 已点名人数 + 请假/留宿/异常明细)。
|
||||
* <p>
|
||||
* 总人数 = 有床位且在住学生数;已点名人数 = 本日点名明细有记录的去重学生数。
|
||||
* buildingId 为空时统计全校;有值时仅统计该楼。
|
||||
*/
|
||||
DormRollCallTodaySummaryVo queryTodaySummary();
|
||||
DormRollCallTodaySummaryVo queryTodaySummary(Long buildingId);
|
||||
}
|
||||
+36
-145
@@ -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原班主任,请按业务另行改回
|
||||
```
|
||||
@@ -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`/筛选;选房页加载房间、单间点名提交后刷新本楼汇总。
|
||||
|
||||
@@ -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
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -17,47 +17,31 @@
|
||||
</view>
|
||||
|
||||
<scroll-view scroll-y class="content" :show-scrollbar="false">
|
||||
<!-- 消息提醒 -->
|
||||
<!-- <view class="form-item">
|
||||
<text class="label">消息提醒</text>
|
||||
<u-checkbox-group
|
||||
v-model="form.messageType"
|
||||
@change="handleMessageTypeChange"
|
||||
>
|
||||
<view class="checkbox-row">
|
||||
<u-checkbox
|
||||
name="1"
|
||||
:checked="true"
|
||||
:disabled="true"
|
||||
activeColor="#2979ff"
|
||||
>
|
||||
站内信
|
||||
</u-checkbox>
|
||||
<u-checkbox name="2" activeColor="#2979ff">邮件</u-checkbox>
|
||||
<u-checkbox name="3" activeColor="#2979ff">短信</u-checkbox>
|
||||
<!-- 请假业务摘要(待办直接审批时展示) -->
|
||||
<view v-if="showLeaveSummary" class="form-item leave-summary">
|
||||
<text class="label">请假信息</text>
|
||||
<view class="leave-summary-card">
|
||||
<view class="leave-summary-row">
|
||||
<text class="leave-summary-label">请假类型</text>
|
||||
<text class="leave-summary-value">{{ leaveInfo.leaveTypeName || '-' }}</text>
|
||||
</view>
|
||||
</u-checkbox-group>
|
||||
</view> -->
|
||||
|
||||
<!-- 附件上传 -->
|
||||
<view class="form-item">
|
||||
<text class="label">附件</text>
|
||||
<FileUpload
|
||||
v-model="form.fileId"
|
||||
:file-type="[
|
||||
'png',
|
||||
'jpg',
|
||||
'jpeg',
|
||||
'doc',
|
||||
'docx',
|
||||
'xlsx',
|
||||
'xls',
|
||||
'ppt',
|
||||
'txt',
|
||||
'pdf',
|
||||
]"
|
||||
:file-size="20"
|
||||
/>
|
||||
<view class="leave-summary-row">
|
||||
<text class="leave-summary-label">开始时间</text>
|
||||
<text class="leave-summary-value">{{ formatLeaveDateTime(leaveInfo.startTime) }}</text>
|
||||
</view>
|
||||
<view class="leave-summary-row">
|
||||
<text class="leave-summary-label">结束时间</text>
|
||||
<text class="leave-summary-value">{{ formatLeaveDateTime(leaveInfo.endTime) }}</text>
|
||||
</view>
|
||||
<view class="leave-summary-row">
|
||||
<text class="leave-summary-label">是否离校</text>
|
||||
<text class="leave-summary-value">{{ formatLeaveSchool(leaveInfo.isLeaveSchool) }}</text>
|
||||
</view>
|
||||
<view class="leave-summary-row leave-summary-row--reason">
|
||||
<text class="leave-summary-label">请假原因</text>
|
||||
<text class="leave-summary-value">{{ leaveInfo.leaveReason || '-' }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 抄送 -->
|
||||
@@ -248,14 +232,14 @@
|
||||
/>
|
||||
</u-popup>
|
||||
|
||||
<!-- 退回弹窗 -->
|
||||
<!-- 退回弹窗:必须高于审批弹窗(20090),否则打开后被盖住表现为“点了没反应” -->
|
||||
<u-popup
|
||||
:show="backVisible"
|
||||
mode="center"
|
||||
width="80%"
|
||||
height="70%"
|
||||
border-radius="12"
|
||||
z-index="20080"
|
||||
:z-index="20100"
|
||||
@close="closeBackDialog"
|
||||
:custom-style="{ width: '90vw' }"
|
||||
>
|
||||
@@ -365,14 +349,14 @@
|
||||
</view>
|
||||
</u-popup>
|
||||
|
||||
<!-- 减签弹窗 -->
|
||||
<!-- 减签弹窗:高于审批弹窗 -->
|
||||
<u-popup
|
||||
:show="deleteSignatureVisible"
|
||||
mode="center"
|
||||
width="90%"
|
||||
height="60%"
|
||||
border-radius="12"
|
||||
z-index="20080"
|
||||
:z-index="20100"
|
||||
@close="deleteSignatureVisible = false"
|
||||
>
|
||||
<view class="delete-signature-container">
|
||||
@@ -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;
|
||||
|
||||
@@ -74,7 +74,10 @@
|
||||
<view class="card-header">
|
||||
<view class="task-title">
|
||||
<text class="title-index">{{ index + 1 }}.</text>
|
||||
{{ item.flowName || '-' }}
|
||||
<text
|
||||
class="title-short"
|
||||
:class="getTaskTitleClass(item)"
|
||||
>{{ getTaskShortTitle(item) }}</text>
|
||||
</view>
|
||||
<view class="task-status">
|
||||
<u-tag
|
||||
@@ -341,6 +344,48 @@ export default {
|
||||
},
|
||||
|
||||
// 获取流程状态显示文本
|
||||
/**
|
||||
* 卡片标题短文案:请假 / 销假(避免「学生请假流程-杭州第一技师」过长)
|
||||
*/
|
||||
getTaskShortTitle(item) {
|
||||
const kind = this.resolveLeaveTaskKind(item);
|
||||
if (kind === 'leave') return '请假';
|
||||
if (kind === 'cancel') return '销假';
|
||||
return (item && item.flowName) || '-';
|
||||
},
|
||||
|
||||
getTaskTitleClass(item) {
|
||||
const kind = this.resolveLeaveTaskKind(item);
|
||||
if (kind === 'leave') return 'title-leave';
|
||||
if (kind === 'cancel') return 'title-cancel';
|
||||
return '';
|
||||
},
|
||||
|
||||
resolveLeaveTaskKind(item) {
|
||||
if (!item) return '';
|
||||
const code = String(item.flowCode || '').toLowerCase();
|
||||
const name = String(item.flowName || '');
|
||||
const category = String(item.categoryName || '');
|
||||
// 销假优先(名称/编码里同时含「请假」字样时避免误判)
|
||||
if (
|
||||
code.includes('xj') ||
|
||||
code.includes('cancel') ||
|
||||
name.includes('销假') ||
|
||||
category.includes('销假')
|
||||
) {
|
||||
return 'cancel';
|
||||
}
|
||||
if (
|
||||
code.includes('qj') ||
|
||||
code.includes('leave') ||
|
||||
name.includes('请假') ||
|
||||
category.includes('请假')
|
||||
) {
|
||||
return 'leave';
|
||||
}
|
||||
return '';
|
||||
},
|
||||
|
||||
getFlowStatusText(status) {
|
||||
if (!this.flowStatusDict || this.flowStatusDict.length === 0) {
|
||||
// 字典未加载时的降级处理
|
||||
@@ -870,6 +915,7 @@ export default {
|
||||
color: #000000;
|
||||
font-weight: bold;
|
||||
margin-right: 8rpx;
|
||||
font-size: 36rpx;
|
||||
}
|
||||
|
||||
.task-title {
|
||||
@@ -877,6 +923,22 @@ export default {
|
||||
font-weight: bold;
|
||||
color: #303133;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.title-short {
|
||||
font-size: 40rpx;
|
||||
font-weight: 700;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.title-leave {
|
||||
color: #2979ff;
|
||||
}
|
||||
|
||||
.title-cancel {
|
||||
color: #e6a23c;
|
||||
}
|
||||
|
||||
.task-status {
|
||||
|
||||
@@ -1624,18 +1624,15 @@ export default {
|
||||
return;
|
||||
}
|
||||
|
||||
// 正式提交:选流程或直接发起
|
||||
if (!this.selectedFlowCode) {
|
||||
// 首次触发,弹出流程选择
|
||||
this.selectedFlowCode = this.flowCodeOptions[0].value;
|
||||
this.showFlowSelectionPopup = true;
|
||||
return;
|
||||
}
|
||||
|
||||
// 正式提交:确保有 flowCode(打回/撤销重提不再卡在空编码)
|
||||
this.ensureSelectedFlowCode();
|
||||
await this.startWorkflow();
|
||||
} catch (e) {
|
||||
console.error('提交失败:', e);
|
||||
uni.showToast({ title: '提交失败', icon: 'none' });
|
||||
uni.showToast({
|
||||
title: (e && (e.msg || e.message)) || '提交失败',
|
||||
icon: 'none',
|
||||
});
|
||||
} finally {
|
||||
this.buttonLoading = false;
|
||||
}
|
||||
@@ -1653,8 +1650,19 @@ export default {
|
||||
await this.startWorkflow();
|
||||
},
|
||||
|
||||
ensureSelectedFlowCode() {
|
||||
if (this.selectedFlowCode) {
|
||||
return this.selectedFlowCode;
|
||||
}
|
||||
this.selectedFlowCode =
|
||||
(this.flowCodeOptions && this.flowCodeOptions[0] && this.flowCodeOptions[0].value) ||
|
||||
'hzdyjs-xj';
|
||||
return this.selectedFlowCode;
|
||||
},
|
||||
|
||||
async startWorkflow() {
|
||||
try {
|
||||
this.ensureSelectedFlowCode();
|
||||
// 获取班主任ID
|
||||
const classTeacherResp = await request({
|
||||
url: `/studentMsg/studentMsg/getClassTeacherId/${this.leaveInfo.studentId}`,
|
||||
@@ -1678,10 +1686,20 @@ export default {
|
||||
|
||||
if (workflowResponse.code === 200) {
|
||||
this.$refs.submitVerifyRef.openDialog(workflowResponse.data.taskId);
|
||||
} else {
|
||||
uni.showToast({
|
||||
title: (workflowResponse && workflowResponse.msg) || '启动工作流失败',
|
||||
icon: 'none',
|
||||
duration: 2500,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('启动工作流失败:', error);
|
||||
uni.showToast({ title: '启动工作流失败', icon: 'none' });
|
||||
uni.showToast({
|
||||
title: (error && (error.msg || error.message)) || '启动工作流失败',
|
||||
icon: 'none',
|
||||
duration: 2500,
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
@@ -1072,6 +1072,13 @@ export default {
|
||||
const leaveInfo = response.data;
|
||||
Object.assign(this.form, leaveInfo);
|
||||
this.form.status = leaveInfo.approvalStatus;
|
||||
// 打回/撤销/草稿打开编辑时预填流程编码,避免重提时空 flowCode
|
||||
if (
|
||||
!this.selectedFlowCode &&
|
||||
['draft', 'back', 'cancel'].includes(leaveInfo.approvalStatus)
|
||||
) {
|
||||
this.ensureSelectedFlowCode(leaveInfo);
|
||||
}
|
||||
await this.ensureApprovalCapability(id);
|
||||
|
||||
// 同步时间值到选择器
|
||||
@@ -3106,11 +3113,8 @@ export default {
|
||||
'this.needShowFlowSelection(response.data) =',
|
||||
this.needShowFlowSelection(response.data),
|
||||
);
|
||||
// 直接使用第一个流程代码,无需显示选择弹窗
|
||||
if (this.needShowFlowSelection(response.data)) {
|
||||
this.selectedFlowCode = this.flowCodeOptions[0].value; // 默认选择第一个
|
||||
console.log('selectedFlowCode', this.selectedFlowCode);
|
||||
}
|
||||
// 草稿/新建/打回/撤销重提:都必须带上有效 flowCode(后端入参曾因此校验失败)
|
||||
this.ensureSelectedFlowCode(response.data);
|
||||
|
||||
// 启动工作流
|
||||
await this.startWorkflow(response.data);
|
||||
@@ -3220,6 +3224,8 @@ export default {
|
||||
// 启动工作流
|
||||
async startWorkflow(data) {
|
||||
try {
|
||||
this.ensureSelectedFlowCode(data);
|
||||
|
||||
// 获取班主任ID、学院审批人、二级学院分管领导
|
||||
const [classTeacherResponse, collegeApproversResponse, collegeLeadersResponse] = await Promise.all([
|
||||
request({
|
||||
@@ -3284,13 +3290,23 @@ export default {
|
||||
|
||||
if (workflowResponse.code === 200) {
|
||||
this.$refs.submitVerifyRef.openDialog(workflowResponse.data.taskId);
|
||||
} else {
|
||||
uni.showToast({
|
||||
title: (workflowResponse && workflowResponse.msg) || '启动工作流失败',
|
||||
icon: 'none',
|
||||
duration: 2500,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('启动工作流失败:', error);
|
||||
// request 拦截器可能已 toast 过;这里优先展示后端真实原因,避免盖成笼统文案
|
||||
const msg =
|
||||
(error && (error.msg || error.message)) ||
|
||||
'启动工作流失败';
|
||||
uni.showToast({
|
||||
title: '启动工作流失败',
|
||||
title: msg,
|
||||
icon: 'none',
|
||||
duration: 2000,
|
||||
duration: 2500,
|
||||
});
|
||||
}
|
||||
},
|
||||
@@ -3356,6 +3372,26 @@ export default {
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
* 确保提交前一定有 flowCode。
|
||||
* 打回(back)/撤销(cancel)重提时原先不会走流程选择,selectedFlowCode 为空,
|
||||
* 导致 /workflow/task/startWorkFlow 入参校验失败并提示「启动工作流失败」。
|
||||
*/
|
||||
ensureSelectedFlowCode(data) {
|
||||
if (this.selectedFlowCode) {
|
||||
return this.selectedFlowCode;
|
||||
}
|
||||
const defaultCode =
|
||||
(this.flowCodeOptions && this.flowCodeOptions[0] && this.flowCodeOptions[0].value) ||
|
||||
'hzdyjs-qj';
|
||||
this.selectedFlowCode = defaultCode;
|
||||
console.log('ensureSelectedFlowCode', {
|
||||
approvalStatus: data && data.approvalStatus,
|
||||
selectedFlowCode: this.selectedFlowCode,
|
||||
});
|
||||
return this.selectedFlowCode;
|
||||
},
|
||||
|
||||
// 提交回调
|
||||
handleSubmitCallback() {
|
||||
console.log('handleSubmitCallback 回调成功');
|
||||
|
||||
@@ -63,17 +63,20 @@
|
||||
<!-- 当天点名汇总 -->
|
||||
<view class="today-summary-card">
|
||||
<view class="summary-header">
|
||||
<text class="summary-title">今日点名汇总</text>
|
||||
<view class="summary-title-wrap">
|
||||
<text class="summary-title">今日点名汇总</text>
|
||||
<text class="summary-scope">全校</text>
|
||||
</view>
|
||||
<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>
|
||||
<text class="summary-num">{{ todaySummary.totalStudentCount || 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>
|
||||
<text class="summary-num">{{ todaySummary.checkedStudentCount || 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>
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -40,8 +40,17 @@
|
||||
<view class="rooms-header">
|
||||
<text class="rooms-title">宿舍房间列表</text>
|
||||
<view class="rooms-stats">
|
||||
<text>总: {{ rooms.length }}</text>
|
||||
<text style="margin-left: 16rpx">已点名: {{ completedRooms.length }}</text>
|
||||
<text>总人数: {{ buildingSummary.totalStudentCount || 0 }}</text>
|
||||
<text>已点: {{ buildingSummary.checkedStudentCount || 0 }}</text>
|
||||
<text class="stat-clickable" @click.stop="openBuildingDetail('leave')">
|
||||
请假: {{ buildingSummary.leaveCount || 0 }}
|
||||
</text>
|
||||
<text class="stat-clickable" @click.stop="openBuildingDetail('holidayStay')">
|
||||
留宿: {{ buildingSummary.holidayStayCount || 0 }}
|
||||
</text>
|
||||
<text class="stat-clickable" @click.stop="openBuildingDetail('absent')">
|
||||
异常: {{ buildingSummary.absentCount || 0 }}
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
@@ -110,13 +119,49 @@
|
||||
</scroll-view>
|
||||
</view>
|
||||
</u-popup>
|
||||
|
||||
<!-- 本楼请假/留宿/异常名单 -->
|
||||
<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.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>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapState } from 'vuex';
|
||||
import { request } from '@/common/request.js';
|
||||
import { generateBatchNo, getBuildingRoomsStatus } from '@/api/dormRollCall.js';
|
||||
import { generateBatchNo, getBuildingRoomsStatus, getTodayRollCallSummary } from '@/api/dormRollCall.js';
|
||||
import dayjs from 'dayjs';
|
||||
import RoomRollCallPopup from './components/RoomRollCallPopup.vue';
|
||||
|
||||
@@ -149,6 +194,19 @@ export default {
|
||||
buildingOptions: [],
|
||||
rooms: [],
|
||||
completedRooms: [], // 已完成点名的房间ID列表
|
||||
buildingSummary: {
|
||||
totalStudentCount: 0,
|
||||
checkedStudentCount: 0,
|
||||
leaveCount: 0,
|
||||
holidayStayCount: 0,
|
||||
absentCount: 0,
|
||||
leaveDetails: [],
|
||||
holidayStayDetails: [],
|
||||
absentDetails: [],
|
||||
},
|
||||
detailPopupVisible: false,
|
||||
detailPopupType: '',
|
||||
detailPopupList: [],
|
||||
|
||||
currentRoom: null,
|
||||
popupHeight: 600,
|
||||
@@ -163,6 +221,15 @@ export default {
|
||||
buildingColumns() {
|
||||
return [this.buildingOptions.map((item) => ({ text: item.buildingName, value: item.id }))];
|
||||
},
|
||||
detailPopupTitle() {
|
||||
const prefix = this.formData.buildingName || '本楼';
|
||||
const map = {
|
||||
leave: `${prefix} · 请假名单`,
|
||||
holidayStay: `${prefix} · 假期留宿名单`,
|
||||
absent: `${prefix} · 异常名单`,
|
||||
};
|
||||
return map[this.detailPopupType] || '明细';
|
||||
},
|
||||
},
|
||||
onLoad() {
|
||||
this.initPage();
|
||||
@@ -245,6 +312,7 @@ export default {
|
||||
// 标记已点名房间
|
||||
this.completedRooms = res.data.filter((r) => r.isCompleted).map((r) => r.roomId);
|
||||
}
|
||||
await this.loadBuildingSummary();
|
||||
} catch (error) {
|
||||
console.error('加载房间失败:', error);
|
||||
uni.showToast({ title: '加载房间失败', icon: 'none' });
|
||||
@@ -253,6 +321,39 @@ export default {
|
||||
}
|
||||
},
|
||||
|
||||
async loadBuildingSummary() {
|
||||
if (!this.formData.buildingId) return;
|
||||
try {
|
||||
const res = await getTodayRollCallSummary(this.formData.buildingId);
|
||||
if (res && res.code === 200 && res.data) {
|
||||
this.buildingSummary = {
|
||||
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,
|
||||
leaveDetails: res.data.leaveDetails || [],
|
||||
holidayStayDetails: res.data.holidayStayDetails || [],
|
||||
absentDetails: res.data.absentDetails || [],
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载本楼点名汇总失败:', error);
|
||||
}
|
||||
},
|
||||
|
||||
openBuildingDetail(type) {
|
||||
this.detailPopupType = type;
|
||||
if (type === 'leave') {
|
||||
this.detailPopupList = this.buildingSummary.leaveDetails || [];
|
||||
} else if (type === 'holidayStay') {
|
||||
this.detailPopupList = this.buildingSummary.holidayStayDetails || [];
|
||||
} else {
|
||||
this.detailPopupList = this.buildingSummary.absentDetails || [];
|
||||
}
|
||||
this.detailPopupVisible = true;
|
||||
},
|
||||
|
||||
isRoomCompleted(room) {
|
||||
return this.completedRooms.includes(room.roomId);
|
||||
},
|
||||
@@ -270,6 +371,7 @@ export default {
|
||||
this.rooms = [];
|
||||
this.completedRooms = [];
|
||||
this.hasLoadedRooms = false;
|
||||
this.resetBuildingSummary();
|
||||
this.loadBuildingOptions(campus.id);
|
||||
}
|
||||
}
|
||||
@@ -294,11 +396,25 @@ export default {
|
||||
this.rooms = [];
|
||||
this.completedRooms = [];
|
||||
this.hasLoadedRooms = false;
|
||||
this.resetBuildingSummary();
|
||||
}
|
||||
}
|
||||
this.showBuildingPicker = false;
|
||||
},
|
||||
|
||||
resetBuildingSummary() {
|
||||
this.buildingSummary = {
|
||||
totalStudentCount: 0,
|
||||
checkedStudentCount: 0,
|
||||
leaveCount: 0,
|
||||
holidayStayCount: 0,
|
||||
absentCount: 0,
|
||||
leaveDetails: [],
|
||||
holidayStayDetails: [],
|
||||
absentDetails: [],
|
||||
};
|
||||
},
|
||||
|
||||
handleRoomClick(room) {
|
||||
this.currentRoom = room;
|
||||
this.showRoomPopup = true;
|
||||
@@ -325,6 +441,7 @@ export default {
|
||||
uni.showToast({ title: `${room?.roomName || ''} 点名完成`, icon: 'success' });
|
||||
this.showRoomPopup = false;
|
||||
this.currentRoom = null;
|
||||
this.loadBuildingSummary();
|
||||
},
|
||||
|
||||
handlePopupClose() {
|
||||
@@ -387,8 +504,9 @@ export default {
|
||||
|
||||
.rooms-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 12rpx;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
@@ -399,11 +517,65 @@ export default {
|
||||
}
|
||||
|
||||
.rooms-stats {
|
||||
width: 100%;
|
||||
font-size: 22rpx;
|
||||
color: #606266;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8rpx 16rpx;
|
||||
|
||||
.stat-clickable {
|
||||
color: #0051d9;
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
.detail-popup {
|
||||
padding: 32rpx 24rpx 24rpx;
|
||||
max-height: 70vh;
|
||||
}
|
||||
|
||||
.detail-popup-title {
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.detail-popup-scroll {
|
||||
max-height: 58vh;
|
||||
}
|
||||
|
||||
.detail-empty {
|
||||
padding: 40rpx 0;
|
||||
}
|
||||
|
||||
.detail-row {
|
||||
padding: 20rpx 0;
|
||||
border-bottom: 1rpx solid #ebeef5;
|
||||
}
|
||||
|
||||
.detail-row-main {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.detail-name {
|
||||
font-size: 28rpx;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.detail-no {
|
||||
font-size: 24rpx;
|
||||
color: #909399;
|
||||
display: flex;
|
||||
flex-wrap: nowrap;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.detail-row-sub {
|
||||
font-size: 24rpx;
|
||||
color: #606266;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.hint-text {
|
||||
|
||||
@@ -1101,9 +1101,9 @@ const submitForm = (status: string) => {
|
||||
dialogVisible.visible = true;
|
||||
return;
|
||||
}
|
||||
// 说明启动过先随意穿个参数
|
||||
// 打回/撤销等已有实例重提:补齐真实流程编码(勿用无效占位 'xx')
|
||||
if (flowCode.value === '' || flowCode.value === null) {
|
||||
flowCode.value = 'xx';
|
||||
flowCode.value = flowCodeOptions[0].value;
|
||||
}
|
||||
await handleStartWorkFlow(res.data);
|
||||
}
|
||||
|
||||
@@ -729,9 +729,9 @@ const submitForm = (status: string) => {
|
||||
dialogVisible.visible = true;
|
||||
return;
|
||||
}
|
||||
// 说明启动过先随意穿个参数
|
||||
// 打回/撤销等已有实例重提:补齐真实流程编码(勿用无效占位 'xx')
|
||||
if (flowCode.value === '' || flowCode.value === null) {
|
||||
flowCode.value = 'xx';
|
||||
flowCode.value = flowCodeOptions[0].value;
|
||||
}
|
||||
await handleStartWorkFlow(cancelInfo);
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user