43 KiB
教师请假(WorkLeave)模块改造计划 v1
目标:把当前「宿舍通知(DormNotice)」模板工程整体改造为教师请假子应用。 业务闭环:教师提交请假申请 → 平台流程审核器(流程中心)流转审批 → 回调推进单据状态 → 扣减教师时长。 本文件是执行依据:按 P0~P7 逐阶段落地,每阶段附验收标准。
- 当前后端:
E:\YHXT\java\WorkLeave(现为 com.yunhai.dormnotice,Spring Boot 2.7 + JPA + 基础平台 yh-spring-boot-starter) - 当前前端:
E:\YHXT\view\WorkLeave(现为 dorm-notice,Vue3 + Vite + view-ui-plus + alova) - 历史前端(Vue2,只参考页面/菜单节点逻辑):
H:\工作区\MYFile\备份\请假前端 - 历史后端(C#/EF,参考实体字段与业务规则):
H:\工作区\MYFile\备份\请假申请2.2 - 流程对接手册:
E:\xwechat_files\wxid_psk2hommdhtq22_310d\msg\file\2026-09\子应用流程与消息对接手册-20260929.md - 平台参考实现:
E:\YHXT\java\UserclassService(UserContext / SysOperationLogAspect)、E:\YHXT\java\HomeLeave(同平台请假姊妹工程,风格参照)
1. 参考资产盘点(已勘察结论)
1.1 历史左侧菜单节点(请假前端 router,modId 100~190)
| modId | 页面 | 历史路径 | 新方案 |
|---|---|---|---|
| 100 | 我要请假(申请表单) | /leave/ |
保留 → /apply |
| 110 | 我的请假 | /leave/myleave |
保留 → /myleave |
| 111 | 请假编辑(子页) | /leave/edit |
合并进 /apply?serid=(隐藏路由) |
| 120 | 审核列表(待我审核) | /leave/shenhe |
保留 → /audit(改用流程中心待办) |
| 130 | 抄送列表 | /leave/leave_send |
保留 → /cc |
| 140 | 全部请假(管理端) | /leave/totallist |
保留 → /all |
| 150 | 指派代课 | /leave/assignment |
P7 可选(依赖排课/课务数据,v1 暂缓) |
| 160 | 流程管理(固定流程配置) | /leave/liucheng |
删除:新平台由「流程中心-流程编排」承担(管理员配置) |
| 170 | 请假设置(6 个 tab) | /leave/leave_timeSetting |
保留 → /setting(作息/类型/教师时长/节次/其他) |
| 180 | 时长日志 | /leave/leave_amendant |
保留 → /timelog |
| 190 | 统计分析 | /leave/tongji |
保留 → /stats(本地实现,旧版跳外部站点不沿用) |
1.2 历史状态机(C# 口径,新方案做了流程化收敛)
历史:0 待审核 / 1 通过 / 2 未通过 / 3 撤销(通过) / 4 撤销待审 / 5 通过(撤销被拒) / 6 修改待审,
并配套自建流程引擎(Common_flowinfo/flowstep/freeflowinfo/flowtranctInfo/Groupuser)实现固定流程与自由流程。
新方案:审核流转全部交给平台流程审核器,本地只保留单据状态;4/5/6 三个依赖自建引擎的状态取消:
| 新状态 | 枚举 | 进入事件 |
|---|---|---|
| 0 | APPROVING 审批中 | 提交(flow.submitted / flow.task.created / flow.task.handled) |
| 1 | PASSED 已通过 | flow.completed(扣减时长) |
| 2 | REJECTED 已拒绝 | flow.rejected |
| 3 | CANCELED 已销假 | 已通过后教师发起销假(本模块动作,返还时长) |
| 4 | WITHDRAWN 已撤回 | flow.withdrawn(提交人撤回,需无任务被办理) |
1.3 历史核心业务规则(重写要点,详见 §6)
- 请假类型:字典(历史
Common_Dictionary typeName='请假类别'),学校开关 + 排序 → 新方案sys_dict(dict_type='leaveType')。 - 时长(调休余额)池:类型进入「时长管控清单」时校验余额;在途占用 = 审批中单据时长之和;通过扣减、销假返还;加减流水留痕。
- 时间/时长计算:按学校作息(上午 F1-F2、下午 A1-A2)折算小时,同日去午休、跨天按整天 8h;小时制(
IsCalculateWay=true)为唯一口径,按天模式取消。 - 冲突校验:同一申请人、状态 ∈ {0,1} 的时间区间不得重叠(历史含 5,新方案无该状态)。
- 编辑:仅「审批中」「已拒绝」可编辑并重新提交(重新发起流程,bizId 追加尝试序号);已通过只能销假,不能改。
- 详情快照:终态时把回调事件 data(含审批意见)序列化落库,详情页免二次查询。
- 抄送:流程中心 NOTIFY 节点自动发消息;本模块落
leave_cc留痕表供「抄送列表」渲染。 - 删除:物理删除(历史口径),仅限审批中/已拒绝/已撤回,且仅本人。
1.4 历史缺陷清单(重写时修正,不复制)
- 半天换算
JsAPm判成KsAPm(C# 源码 bug)→ 新方案按天模式整体取消,不涉及。 - 拒绝时返还时长传错类型 → 新方案拒绝不返还(未扣过),无此分支。
- 附件绑定不落库 → 新方案仓储显式 save。
- 时长池新建记录
null + x = null→ 新方案default 0。 - 时间段校验用字符串拼 SQL → 新方案参数化 JPA + LocalDateTime。
- 列表时间过滤口径不一(重叠 vs 包含)→ 统一为重叠语义。
2. 关键决策
| # | 决策 | 理由 |
|---|---|---|
| D1 | 后端包名 com.yunhai.workleave,artifactId/finalName=workleave,应用名WorkLeave |
需求 1;与旧模板彻底切割 |
| D2 | 删除宿舍业务(Activity/ActivityRead/ActivitySignup 及全部 Model/Repository/Service/Controller),保留 sys_operation_log(含切面)、sys_dict、up_file、contacts(自定义组)、base/dict/file/log 平台代理控制器 |
需求 2(删实体保留日志表);保留件是请假的通用底座 |
| D3 | 业务人员字段只存平台数字 ID(userId),姓名/头像/类型名做冗余展示字段;BaseEntity.create_by/update_by 改存操作人 userId,不存账号 |
需求 3(不保存师生账号) |
| D4 | 审核流转对接平台流程审核器(yh-flow-callback-starter + YschFlowOpenApi),bizType=leave 全局唯一 |
需求 7 对接手册;替代历史自建流程引擎 |
| D5 | 申请发起 bizId = {leaveId}-{attempt}(attempt 为第几次提交,从 1 起);回调按 - 前缀解析 leaveId |
流程中心按 bizType+bizId 幂等,拒绝后重新提交必须换 bizId |
| D6 | 消息跳转 JumpTarget.page("detail").param("leaveId", id),前端提供 /detail?leaveId= 落地页 |
手册 §3.1 红线(query 形态、页面名两端一致) |
| D7 | 时长计算做成工具类前后端同规则(前端实时算、后端复核),小时制保留 2 位小数 | 需求 3 业务逻辑;历史算法在前端算、后端只校验,重写为双端一致 |
| D8 | 前端保留现有技术栈与壳层(Vue3+Vite+view-ui-plus+alova),仅替换业务域与菜单契约 | 与当前前端工程一致,降低风险;"只参考页面逻辑"不搬 Vue2 代码 |
| D9 | 节点 160 流程管理删除;审批意见/流程图/进度时间线用流程中心能力(progress/diagramUrl) | 平台已提供,避免重复建设 |
3. 前置依赖(需平台管理员配合,先登记后联调)
| # | 事项 | 说明 | 状态 |
|---|---|---|---|
| A1 | projman 建应用 | 部署名称 leave(= app_code,逐字一致;公共组件勾 Redis) |
☐ 待确认 |
| A2 | UserBase 注册应用 | app_code=leave、app_key/app_secret、pc_url=http://<域名>/leave/front/index.html#/ |
☐ 待确认 |
| A3 | 流程中心编排 | 注册流程记 flowGuid(建议 leave-apply-v1;如需销假审批再加 leave-cancel-v1) |
☐ 待确认 |
| A4 | 白名单登记 | flow-center trusted-apps 加 appKey:secret;UserMsg/UserBase 密钥链 |
☐ 待确认 |
| A5 | 菜单树配置 | 基础平台按 §4.3 的 modId/path 配置 9 个节点(对齐 GetUserMod) | ☐ 待确认 |
| A6 | Nacos 配置 | workleave.yaml(app-key/secret/flow-guid/open.base-host),本地用 application.yml 兜底 |
☐ 待确认 |
本地开发不需要 A4;
open-host指向http://open.yunhaixingtong.com(hosts 加192.168.10.160 open.yunhaixingtong.com)可直连真实服务(手册 §5.0)。
4. 目标架构
4.1 后端目录结构(com.yunhai.workleave)
src/main/java/com/yunhai/workleave/
├── WorkLeaveApplication.java
├── aspect/ ControllerUserContextAspect(Order1)· SysOperationLogAspect(Order3)
├── common/ listener/BaseAuditListener · validation/ValidationGroups
├── config/ Config(API_PATH) · JacksonConfig · SwaggerConfig
├── entity/ BaseEntity · SysDict · UpFile · SysOperationLog
│ LeaveApply · LeaveSetting · LeaveTimeSet · LeaveUserTime · LeaveUserTimeRecord
│ LeaveSection · LeaveCc · (P7) LeaveCourse · LeaveCourseExchange · LeaveApplyOld
├── enums/ LeaveStatusEnum · LeaveActionEnum · UserType
├── flow/ LeaveFlowService(apply/withdraw/todo/approve/reject/progress/diagram)
│ LeaveFlowCallbackHandler(事件状态机 + 终态守卫 + 快照 + 扣减/返还 + 抄送留痕)
├── model/ BasePageQuery/BasePageResult · bsch(SchTeaList/ClassTreeNodeVo) · file(UpFileVo/Result)
│ └── leave/ LeaveVo · LeaveDetailVo · LeaveQuery · LeaveRequest · LeaveCancelRequest
│ LeaveAuditRequest · LeaveProgressVo · LeaveStatsVo · TimeSetVo/TimeSetRequest
│ LeaveSettingVo · UserTimeVo · UserTimeRecordVo · UserTimeAdjustRequest · SectionVo ...
├── repository/ LeaveApplyRepository · LeaveSettingRepository · LeaveTimeSetRepository
│ LeaveUserTimeRepository · LeaveUserTimeRecordRepository · LeaveSectionRepository
│ LeaveCcRepository · SysDictRepository · UpFileRepository · SysOperationLogRepository
├── service/ I/DictService · I/FileService · I/ContactsService · I/UserCenterService · I/ClassService
│ I/LeaveApplyService · I/LeaveSettingService · I/LeaveUserTimeService
│ I/LeaveStatsService · I/SysOperationLogService
│ └── impl/ 同名实现 + LeaveTimeCalculator(时长算法)
├── controller/api/ BaseApiController · DictController · FileController · LogController · ContactsController
│ LeaveController(申请域)· LeaveAuditController(待办/审核/进度)
│ LeaveSettingController(设置域)· LeaveTimeController(时长域)· LeaveStatsController
└── utils/ UserContext · PageUtils · GzipUtil · IpUtils · LeaveTimeText
4.2 数据表(sql/workleave_schema.sql;ddl-auto=update 兜底建表,脚本供人工核对)
| 表 | 说明 | 关键字段 |
|---|---|---|
leave_apply |
请假申请主表 | id, sch_id, user_id, user_name, user_photo, leave_type(字典key), leave_type_name, start_time, end_time, total_hours, reason, file_ids, cc_user_ids, status, biz_id, instance_id, instance_version, flow_guid, flow_attempt, snapshot(终态JSON), apply_time + BaseEntity 审计列 |
leave_setting |
学校设置(一校一行) | sch_id, reason_must, show_explain, enable_course, allow_exchange, time_managed_types(CSV 字典key), time_limit_enabled |
leave_time_set |
作息时间(一校一行) | sch_id, forenoon1, forenoon2, afternoon1, afternoon2, interval_minutes(默认 08:00/12:00/13:00/17:00/10) |
leave_user_time |
教师时长池 | sch_id, user_id, user_name, usable_hours, remark;唯一键 (sch_id,user_id) |
leave_user_time_record |
时长流水 | sch_id, user_id, user_name, leave_id, leave_type, edit_hours(±), old_hours, new_hours, describe, operator_id, create_time |
leave_section |
节次表(P7 课务用,设置页可维护) | sch_id, name, rank, begin_time, end_time |
leave_cc |
抄送留痕 | sch_id, leave_id, cc_user_id, cc_user_name, instance_id, create_time |
sys_dict |
字典(保留):新增 leaveType |
(sch_id, dict_type='leaveType', dict_key, dict_value, sort_order, enable_flag) |
up_file / sys_operation_log |
附件 / 审计日志(保留) | 现状不动 |
flow_callback_event / flow_callback_reg |
回调 starter 自动建表 | 不手工维护 |
4.3 前端路由与 modId 契约(对齐平台菜单树)
| modId | path | 页面 | 说明 |
|---|---|---|---|
| 100 | /apply |
我要请假 | 新建/编辑(?serid=),含时长试算、附件、抄送人 |
| 110 | /myleave |
我的请假 | 本人列表:查看/编辑/销假/撤回/删除 |
| 111 | /detail |
请假详情 | ?leaveId=,流程消息跳转落地页;meta.parentId=110(菜单不单列) |
| 120 | /audit |
审核列表 | tab:待我审核(流程待办)/ 我已审核;同意/拒绝/流程图 |
| 130 | /cc |
抄送列表 | leave_cc 渲染 |
| 140 | /all |
全部请假 | 管理端全量 + 导出 CSV |
| 170 | /setting |
请假设置 | tab:作息时间/请假类型/教师时长/节次/其他设置 |
| 180 | /timelog |
时长日志 | 时长流水 + 合计(新加/剩余),支持加减时长 |
| 190 | /stats |
统计分析 | 数量/时长按类型与月份、明细 |
| (150) | /assignment |
指派代课 | P7 可选 |
| — | / → /apply 重定向 |
— |
平台菜单若分配了不同 modId:优先按
path解析(menuRoute.ts 已具备),仅需把上表 modId 改为实际值即可。
4.4 前端文件结构(E:\YHXT\view\WorkLeave)
src/
├── api/module/leave.ts # 全部接口封装(沿用 get/postForm/postJSON 约定)
├── types/leave.ts # 类型契约(枚举、VO/DTO、请求入参)
├── router/routes.ts # 上表路由 + meta.modId/parentId
├── utils/leaveTime.ts # 时长计算(与后端同规则)
├── views/leave/
│ ├── ApplyLeave.vue # 我要请假(新建/编辑)
│ ├── MyLeave.vue # 我的请假
│ ├── LeaveDetail.vue # 详情(信息+进度时间线+附件+抄送人)
│ ├── LeaveAudit.vue # 审核列表
│ ├── LeaveCc.vue # 抄送列表
│ ├── AllLeave.vue # 全部请假
│ ├── LeaveSetting.vue # 请假设置(多 tab)
│ ├── TimeLog.vue # 时长日志
│ └── LeaveStats.vue # 统计分析
└── (保留)components/、utils/、assets/ 等基础设施;删除 views/dorm/*
5. 接口契约(前后端对齐)
统一前缀:/{platform}/api(前端 serviceHost + /api/...;平台值 api/manage/mobiapi)。响应由平台 ApiResponseAdvice 包装 {code,msg,data},alova 解包 data。
5.1 申请域 leave/
| 方法 | 接口 | 入参 | 出参 |
|---|---|---|---|
| GET | /leave/List |
scope(user=我的/all=全部), status, keyword, startTime, endTime, pageIndex, pageSize | {content:[LeaveVo], total, ...} |
| GET | /leave/Detail |
leaveId | LeaveDetailVo(含进度、抄送、附件、快照、当前用户能否审核) |
| POST | /leave/Create |
LeaveRequest(JSON) | leaveId(内部:落库→发起流程→回写 bizId/instanceId) |
| POST | /leave/Edit |
LeaveRequest + id(JSON) | void(仅 0/2/4 状态;重新发起流程 attempt+1) |
| POST | /leave/Delete |
ids(JSON 数组) | void |
| POST | /leave/Withdraw |
id | void(调流程中心 withdraw) |
| POST | /leave/Cancel |
id + reason(JSON) | void(销假:状态 1 → 3,返还时长,落流水) |
| GET | /leave/Progress |
leaveId | {steps:[...], tasks:[...]}(流程中心 progress 包装) |
| GET | /leave/DiagramUrl |
leaveId | {url}(流程图 10 分钟 token) |
| GET | /leave/MyTime |
leaveType | {usable, inTransit, remaining}(表单页余额提示) |
5.2 审核域 leave/
| 方法 | 接口 | 入参 | 出参 |
|---|---|---|---|
| GET | /leave/TodoList |
pageIndex, pageSize, keyword | {content:[TodoVo], total}(todoTasks(bizType=leave) + 本地单号补全) |
| GET | /leave/AuditedList |
pageIndex, pageSize, keyword | 已办:本地按 instance 与审核记录反查 |
| POST | /leave/Audit |
taskId, action(approve/reject), opinion(JSON) | void(approveTask/rejectTask;状态由回调推进) |
5.3 抄送 / 统计 / 设置 / 时长
| 方法 | 接口 | 说明 |
|---|---|---|
| GET | /leave/CcList |
抄送给我的列表(leave_cc 关联 leave_apply) |
| GET | /leave/Statis |
dateStart/dateEnd → 总数量、总时长、按类型分布、按月趋势、明细 Top |
| GET | /leave/setting/GetTimeSet / SetTimeSet |
学校作息 |
| GET | /leave/setting/GetOption / SetOption |
其他设置(含时长管控类型) |
| GET | /leave/setting/SectionList |
节次列表(P7 预览,设置页可维护) |
| POST | /leave/setting/SectionSave / SectionDelete / SectionSort |
节次维护 |
| GET | /leave/time/UserTimeList |
教师时长列表(按组/关键字;可用 base/GetSchoolAll 组树过滤) |
| POST | /leave/time/Adjust |
加减时长(单人或批量,JSON;写流水) |
| GET | /leave/time/Records |
时长流水(rqType 0今日/1本周/2本月/3累计 + 关键字 + 分页) |
| GET | /leave/time/Sum |
合计:新加总时长、剩余总时长 |
| GET/POST | /api/dict/*(复用) |
type=leaveType 的字典增删改查排序(请假类型 tab) |
5.4 平台代理接口(保留,改包名)
base/GetUserInfo、base/GetUserMod、base/GetSchoolAll(选人树)、base/SetRole、base/FileUpload、base/Logout、
dict/*、file/*、contacts/*(自定义组)、log/*(审计日志查询)。
6. 业务规则明细
6.1 时长计算(前后端同规则,工具类 LeaveTimeCalculator / leaveTime.ts)
输入:起止时间(yyyy-MM-dd HH:mm)、作息(F1/F2/A1/A2)。输出:小时(四舍五入 2 位)。
- 同日:start
clamp到工作时段(早于 F1→F1;F1F2 之间保持;F2A1 之间→A1;晚于 A2→A2;A1A2 保持); end 同理(F2A1 之间→F2);minutes = end - start;若start<=F2 && end>=A1再减午休(A1-F2)。 - 跨天:首日 =
A2 - start(start 落在 F1F2 时再减午休);末日 =A2 时再减午休); 中间整天按end - F1(end 落在 A18h/天(=A2-A1 + F2-F1);合计取小时。 - 校验:结束必须晚于开始;单条时长 > 0;跨天视为按天折算(不再区分上下午字段)。
6.2 申请与校验(Create/Edit)
- 类型必须为启用的 leaveType 字典项;事由在
reason_must=1时必填且 ≤50 字。 - 时间冲突:同人、
status in (0,1)、(start_time < 已有.end_time) && (已有.start_time < end_time)→ 报"与已有请假时间冲突"。 - 时长管控:类型 ∈
time_managed_types时,在途(0状态且管控类型) + 本次 <= usable(无记录按 0),否则报"可申请 X 小时,时间不足"。 total_hours后端按 §6.1 复核(与前端不一致以服务端为准落库)。- 提交成功 → 组装
FlowApplyBody:bizType=leave、bizId={id}-{attempt}、bizTitle={姓名}的{类型}申请、bizJump=page("detail").param("leaveId",id)、bizSnapshot=摘要(类型/起止/时长/事由)、submitterId/Name、formData(类型、时长、天数)。 - 发起成功回写
biz_id/instance_id/flow_guid/status=0;duplicated=true时视为幂等成功(不新建实例)。
6.3 回调状态机(LeaveFlowCallbackHandler)
| 事件 | 处理 |
|---|---|
flow.submitted / flow.task.created / flow.task.handled |
status→0(审批中);终态不回退:已 1/2/3/4 则忽略 |
flow.completed |
status→1;扣时长(管控类型才执行)+ 流水"扣除";落终态快照 |
flow.rejected |
status→2;不扣不返还;落终态快照 |
flow.withdrawn |
status→4(仅当时为 0 时) |
flow.cc.sent |
写 leave_cc 留痕(幂等:同 leaveId+ccUserId+instanceId 不重复落) |
- 乱序守卫:事件带
instanceVersion,小于单据已记录版本的事件忽略(终态快照之外)。 - 事务:回调方法内异常 → 整体回滚 + 流程中心重推;确定性脏消息(bizId 解析失败等)记录 warn 后正常 return,不抛异常。
- 注册:首次发起前
callbackRegistrar.ensureRegistered(schId, flowGuid)(结果落 flow_callback_reg 镜像表)。
6.4 销假(Cancel)与撤回(Withdraw)
- 销假:仅
status=1;置 3;管控类型返还时长 + 流水"销假返还";记cancel_time/reason(快照字段)。已通过 50 天时效沿用(可配置为 50 天常量)。 - 撤回:仅
status=0;调YschFlowOpenApi.withdraw(schId, instanceId, submitterId);等回调置 4(不本地直改,避免与流程中心不一致)。
6.5 时长池管理
- 列表:
leave_user_timeLEFT JOIN 教师名单(base/GetSchoolAll同源,按组过滤)。 - 调整:
edit_hours可正可负,now = old + edit(不低于 0),写流水(describe=备注或"新加"),operator 取当前用户。 - 流水统计:
Sum返回新加总=Σ(±)、剩余总=Σ usable(按过滤区间口径)。
7. 流程审核器对接要点(照手册执行)
- pom:
yh-flow-callback-spring-boot-starter+spring-boot-starter-data-redis(验签密钥链必需,optional 不传递)。 - 配置:
回调地址默认
yh: app-auth: { enabled: true, app-key: "1000xx", app-secret: "******" } flow-callback: enabled: true biz-types: [leave] open-host: ${yh.open.base-host} # 未配 callback-url 时用于拼回调地址 management.health.redis.enabled: false leave.flow.guid: leave-apply-v1 # 编排所得/oapi/flow/callback(starter 自证 context-path 生成);yh.sso.url-pattern=/api/*不含 oapi,无需放行改造。 - 发起:
YschFlowOpenApi.apply(schId, body, FlowApplyResult.class)。 - 回调:实现
FlowCallbackHandler#onEvent(§6.3);schId从事件取,不信任本地上下文。 - 待办/审核:
todoTasks(schId, assigneeId, "leave", page, size)、approveTask(schId, taskId, operatorId, operatorName, opinion[, edgeName])、rejectTask(...)。 - 进度/流程图:
progress(null, "leave", bizId, schId, FlowProgress.class)、diagramUrl(instanceId, flowGuid, "track", schId, FlowDiagramUrl.class)。 - 自检(手册 §5.1):本地单测(mock 回调事件 + apply 出参断言)→ 160 环境全链路 9 项验收。
8. 分阶段任务与验收
P0 骨架重整
- 后端:包
com.yunhai.dormnotice→com.yunhai.workleave(目录迁移 + 全量 import 替换);DormNoticeApplication→WorkLeaveApplication;pomartifactId/name/finalName→workleave;bootstrap.yml spring.application.name→workleave;日志/库名对齐 - 后端:删除宿舍业务(ActivityController、Activity 系实体/模型/仓储/服务);保留 log/dict/file/base/contacts 底座
- 前端:
package.jsonname、vite.config标题、index.html、README、App.vue appName → 教师请假 - 验收:后端
mvn -q compile通过;前端构建通过;全局搜索无dormnotice残留
P1 实体与数据层
BaseEntity审计人改存 userId(create_by/update_by)- 新实体 + Repository(LeaveApply/LeaveSetting/LeaveTimeSet/LeaveUserTime/LeaveUserTimeRecord/LeaveSection/LeaveCc/LeaveAuditTrace)
sql/workleave_schema.sql(建表 + leaveType 字典种子:事假/病假/调休/公假/婚假/产假)- 验收:编译通过;启动自动建表成功;
leaveType字典种子可查
P2 设置域 + 时长域
LeaveSettingController/Service:作息时间、其他设置、节次LeaveTimeController/Service:时长列表/调整/流水/合计;LeaveTimeCalculator- 验收:单测覆盖时长算法(同日/跨天/边界 clamp/午休)—— 16 例全绿
P3 申请域 + 流程对接
LeaveController/Service:Create/Edit/Delete/Withdraw/Cancel/List/Detail/MyTime/Statis/TodoList/AuditedList/Auditflow/LeaveFlowService+flow/LeaveFlowCallbackHandler(§6.3)- pom/application.yml 流程依赖与配置(§7)
- 验收:编译通过;回调单测(mock FlowCallbackEvent 覆盖 5 类事件 + 终态不回退 + 乱序门 + 抄送幂等)—— 12 例全绿
P4 前端壳层与路由
api/module/leave.ts、types/leave.ts、router/routes.ts(§4.3)、menuRoute.ts别名更新- 删除
views/dorm/*、api/module/dorm.ts、types/dorm.ts及依赖其的组件 - 验收:
npm run build通过;菜单解析器对新节点 path 命中
P5 前端页面
- ApplyLeave:表单 + 时长实时试算 + 余额提示 + 附件 + 抄送人 + 提交/重新提交
- MyLeave / LeaveDetail / LeaveAudit / LeaveCc / AllLeave
- LeaveSetting(5 个 tab)/ TimeLog / LeaveStats
- 验收:构建通过(联调需本地后端 + DB 环境,见 P6 说明)
P6 联调与验收
- 后端
mvn -o compile/ 单测全绿;前端npm run build - 160 环境按手册 §5.1 九项清单验收(含消息卡片跳转
/detail?leaveId=)—— 需管理员提供 A1~A6 前置项 - 验收:清单全过;
proj34.sys_message最新行jump_path=/detail?leaveId=xxx、jump_app=leave
P6.5 本地冒烟测试(2026-10-09 新增)
目标:在不依赖平台管理员前置项(A1~A6)的前提下,对已交付代码做本地全链路冒烟, 验证后端服务、数据库、核心业务接口、前端构建与页面渲染的可运行性。 160 全链路(真实 SSO 登录 + 流程审批闭环)仍归 P6,等管理员补齐前置项。
冒烟辅助件(开发专用,生产零影响):
- 新增
config/DevMockUserFilter:仅当smoke.auth.enabled=true时注册,向BoaUserContext注入可配置的模拟用户(smoke.auth.user-id/user-name/sch-id/user-type),默认关闭。 - 本地启动时用
--yh.sso.url-pattern=/__smoke_off__/*关闭 CAS 拦截,使/api/*可直连 (模拟用户由上述过滤器提供,业务代码无需感知)。
冒烟清单(S1~S6):
| # | 项 | 内容 | 通过标准 | 结果 |
|---|---|---|---|---|
| S1 | 构建 | mvn -o test(含 28 例单测) |
BUILD SUCCESS,0 失败 | ✅ 28 例全绿(修复后复跑仍全绿) |
| S2 | 启动 | smoke 模式 java -jar 起 8105 |
日志无 ERROR,/actuator/health 200 |
✅ |
| S3 | 数据 | DB work_leave 13 张表 + leaveType 种子 |
表齐全,LT001~LT006 可查 | ✅ 13 表 + 6 种子 |
| S4 | 设置域 API | GetTimeSet/SetTimeSet、GetOption/SetOption、dict 全链路 | 读默认值→写入→回读一致 | ✅(修复 2 个 bug 后通过,见 §10) |
| S5 | 申请/时长域 API | List 空态、time/Adjust+Sum+Records、MyTime、Statis、Create(流程中心链路) | 只读接口正常;Adjust 后 Sum/Records 联动;Create 失败时事务回滚无孤儿单(flowGuid 占位期预期),若流程中心可用则全链路通过 | ✅ Adjust +8h→Sum/Records/MyTime 联动一致;Create 报"流程失败 404"且回滚(leave_apply=0);操作日志记录失败尝试 |
| S6 | 前端 | npm run build + dev server 浏览器访问核心页面 |
构建通过;页面壳层渲染、控制台无阻断性报错 | ✅ 9 页面全渲染;申请表单可交互(类型下拉 6 项/时长试算 4h/事由 50 字计数/提交按钮联动);提交失败 toast 正确透传后端错误;"我的请假"空态表格/统计卡正常;设置页 5 tab 正常 |
执行结论随做随记入 §10。
P7 可选扩展(v1 后)
- 指派代课/调课(课务安排表 + 教务安排人 + 通知);节次/班级/科目数据源接入
- 修改待审流程(快照表
leave_apply_old+ 二次流程) - 发起销假审批(第二 flowGuid
leave-cancel-v1) - 教师时长 Excel 导入导出;统计图表增强
P8 对齐旧版生产行为(2026-10-09,参考 oa.bestsch.com/leave 实地勘察)
背景:登录旧版生产系统(lwj 账号)实地核对,发现原计划 v1 裁剪掉的两个行为在生产实际在用, 且旧版"其他设置"里有请假计算(按小时/按天)开关(该学校当前为按天)。本阶段把这两块补齐。 旧版算法结论:后端永远存小时(leave_totaltime),"按天"只是输入/显示模式(显示=小时/8), 与新架构小时制天然兼容,改造聚焦在输入侧与配置面。
后端:
leave_setting.calc_way(1 按小时[默认] / 0 按天,历史 IsCalculateWay 同义)leave_time_set.min_minutes(最少请假时间,分钟,0 不限制;历史"学校时间设置"项)leave_apply.course_arrange(0 无课务安排 / 1 自行安排 / 2 教务安排;不传默认 0)- Create/Edit 校验:时长分钟 < min_minutes → 拒绝"请假时长不能少于 X 分钟"(边界=值本身放行)
- VO:TimeSetVo+minMinutes、LeaveSettingVo+calcWay、LeaveRequest/LeaveVo/LeaveDetailVo+courseArrange(+Name)
前端:
- ApplyLeave:calcWay=0 时表单切换为「半天/半天以上 + 日期 + 上午/下午」输入(按作息组合起止
datetime 后复用既有小时试算),时长主显天(=小时/8,同旧版);calcWay=1 保持 datetime 输入。
新增「时间说明」行(showExplain=1 时):
上午(F1-F2)-下午(A1-A2),固定X小时工作制。 新增「课务安排」三选一(enableCourse=1 时,默认无课务安排)。 - LeaveSetting:作息 tab + 「最少请假时间(分钟)」;其他设置 tab 置顶 + 「请假计算:按小时/按天」 radio;课务安排/允许换课去掉 P7 标注(课务字段已落地输入侧,代课执行仍属 P7)。
- LeaveDetail:显示课务安排。
验证(2026-10-09):28 例单测全绿;smoke API 实测 calcWay/minMinutes 读写回显一致、 20 分钟申请被拒(30 下限)、4h 半天口径(08:00-12:00)正确、courseArrange 落库字段就绪(流程 404 回滚期无孤儿单);浏览器(真实账号李晶 + 8108 IDEA 实例 + devtools 热载):时间说明行、按小时表单、 左侧 9 菜单节点渲染正常。
端口变更:application.yml server.port 8105 → 8108(IDEA debug 实例),vite 代理同步改 8108。
用户另起 dev server 于 8079(与 8078 同源,均为 vite --mode dev)。
学校 3(李晶,对齐旧版生产)已切按天:calc_way=0、enable_course=1(2026-10-09 经 DB 设置)。
8079 实测:半天=0.5 天(合 4 小时)、半天以上 10-12 上午~10-14 下午=3 天(合 24 小时),与旧版算法一致。
按小时模式对齐旧版(2026-10-09 二次勘察旧系统按小时形态后):
起止时间改为旧版同款四控件一行(开始日期+开始时间 至 结束日期+结束时间),TimePicker 分钟按
最短请假间隔 步进(:steps=[1,intervalMinutes,1],对应旧版分钟过滤);时长标签「时长(小时)」
纯小时显示(去掉天副注)。8079 实测 10-12 08:00~12:00 = 4 小时。验证后学校 3 已切回按天。
附件 → 图片上传(2026-10-09,对齐旧版):AttachmentUpload 组件加 imageOnly 模式
(仅 jpg/jpeg/bmp/png、单张 ≤20MB、accept 过滤、错误文案"只能上传 jpg/bmp/png 格式的图片!");
申请页 FormItem「附件」→「图片」,按钮「上传图片」+ 提示
「(上传图片格式:.jpg、.bmp、*.png 图片大小不要超过20MB)」逐字对齐旧版;
详情页「附件」→「图片」,图片文件渲染 60×60 缩略图(点击预览直链打开)。
后端 FileUpload 保持通用(格式限制在前端,P6 联调时可评估是否服务端加固)。
申请页样式对齐旧版(2026-10-09 四轮,末轮按旧版真实 CSS 值精调):① label 统一加全角「:」
(CSS ::after,不逐个改);② label-width 100px、标签色 #272829(旧版实测值);
③ 表单铺满内容区(取消窄卡片约束,旧版控件实测宽 ~946px);④ 控件宽度按旧版实测:
类型下拉/事由文本框满宽、日期 190px(旧版 193px)、时间 130px、时长 180px(旧版 180px);
⑤ 行距 20px(旧版实测);⑥ 时长由蓝色文字改为禁用 Input(占位「自动获取时长」);
⑦ 按天模式「起止时间」合并为一行:半天/半天以上 + 开始日期 + 上午/下午(半天以上再接
「至」+ 结束日期 + 上午/下午);按小时模式同为四控件一行;⑧ 事由占位「请输入请假事由……」(旧版原文);
⑨ 图片上传按钮改旧版同款方形图标按钮;⑩ 抄送人未选时显示灰色提示
「请选择抄送人,审批通过后通知抄送人」;⑪ 时间说明文字色 #515a6e。
8079 浏览器复核:满宽下拉/事由、100px 冒号标签、180px 时长框、一行起止时间、方形图片按钮均与旧版一致。
P9 流程一致性与安全加固(2026-10-09 现状审计新增)
审计范围:以《子应用流程与消息对接手册-20260929》§2.3、§2.4、§5.1 为准,复核
LeaveFlowService、LeaveFlowCallbackHandler、申请身份边界及现有单测。P0~P8 已交付功能不重做。
审计发现:
- 流程发起返回
null或缺少instanceId时,当前代码仍可能提交为“审批中”,形成无真实流程实例的孤儿单。 - 拒绝/撤回后重新提交会复用同一请假主键;旧实例迟到事件若按主键兜底定位,可能误推进新 attempt。
LeaveRequest.userId来自客户端请求体,普通教师可构造该字段替其他用户发起申请;申请人必须只信任登录态。- 手册 §5.1 要求 apply 请求契约断言;现有 12 例流程测试只覆盖回调状态机,没有覆盖
bizType/bizId/bizJump/bizSnapshot/formData/submitter。
实施任务:
- P9.1 发起结果强校验:
FlowApplyResult及instanceId必须非空,否则抛业务异常并由事务回滚。 - P9.2 回调实例隔离:事件
bizId/instanceId必须与当前 attempt 一致;旧实例迟到事件正常忽略,不触发重推。 - P9.3 回调终态幂等:重复终态事件不重复产生副作用,冲突终态事件不得覆盖已落终态。
- P9.4 申请身份收口:Create 始终使用登录态
currentUserId/userName,删除前后端请求契约中的userId。 - P9.5 补齐流程发起请求构造测试、空实例回滚边界测试、旧 attempt/终态冲突回调测试。
- P9.6 验证:后端
mvn test34 例全绿;前端npm run typecheck:leave通过;npm run build通过。 全工程vue-tsc --noEmit已执行,另发现 74 个模板历史错误(知识树/年级班级/Vant 等未接入请假路由的遗留件), 与本次请假业务文件无关,登记为 R7,不混入本阶段改造。
验收标准:
- 流程发起无实例 ID 时接口失败且事务不留下申请记录;正常结果完整回写
bizId/instanceId。 100-1的迟到回调不能修改当前bizId=100-2的单据;重复completed只扣减一次。- 请求体即使伪造
userId也不进入后端 DTO,申请人只取登录态。 - 单测显式断言跳转为
/detail?leaveId=xxx、bizType=leave,以及快照/表单/提交人字段。
9. 风险与未决项
| # | 风险/未决 | 处置 |
|---|---|---|
| R1 | app_key/app_secret、flowGuid、菜单 modId 实际值未定(依赖管理员) | 全部走配置项;本地先以占位值 + mock 单测开发;P6 前与管理员确认 |
| R2 | 回调在本地收不到(flow-center 不能推回本机) | 回调逻辑以单测 mock 覆盖;真链路 160 验收 |
| R3 | Redis 不可达导致启动/健康检查异常 | management.health.redis.enabled=false;starter 只在回调验签用 Redis,本地联调可用 docker 起一个 |
| R4 | 历史「按天/半天」模式与「课务安排」不影响 v1 核心闭环 | 已在决策中裁剪,P7 按需恢复 |
| R5 | 教师名单来源(bschclass GetSchteaList)不可达时选人/时长列表降级 |
沿用现有 ClassService 缓存 + 空列表降级策略 |
| R6 | 时长算法双端实现漂移 | 同一份规则单测用例(P2 后端 + P4 前端各一份对照) |
| R7 | 前端模板遗留 74 个全量类型错误(知识树/年级班级/移动端 Vant 等未使用模块) | 新增 tsconfig.leave.json + npm run typecheck:leave 作为请假模块严格门禁;遗留模板另立清理任务,不影响当前构建 |
10. 执行记录(随实施更新)
| 日期 | 阶段 | 说明 |
|---|---|---|
| 2026-10-08 | 计划 | 本文档创建,勘察完成 |
| 2026-10-08 | P0 完成 | 后端包名 com.yunhai.workleave、宿舍业务全删、工程标识更名 workleave;前端标识更名教师请假 |
| 2026-10-08 | P1 完成 | 8 个业务实体 + 仓储 + 建库脚本(含 leaveType 字典种子);审计人改存 userId |
| 2026-10-08 | P2 完成 | 设置域(作息/其他/节次)+ 时长域(列表/调整/流水/合计)+ LeaveTimeCalculator;16 例单测通过 |
| 2026-10-08 | P3 完成 | 申请域(申请/编辑/删除/撤回/销假/待办/审核/统计)+ 流程门面 + 回调状态机;12 例单测通过(共 28 例) |
| 2026-10-08 | P4/P5 完成 | 前端 types/api/routes/壳层重建;9 个业务页面落地;宿舍遗留全删 |
| 2026-10-08 | P6 完成 | 后端 mvn package 出包 target/workleave.jar(105MB fat jar);28 例单测 + 前端 npm run build + vue-tsc 新增文件全绿;启动验证:java -jar 起 8105,Hibernate 自动建 13 张表(leave_apply/leave_setting/leave_time_set/leave_user_time/leave_user_time_record/leave_section/leave_cc/leave_audit_trace + 底座表),/actuator/health 返回 200,leaveType 字典种子可查(LT001~LT006) |
| 2026-10-08 | 待办 | 160 环境全链路验收依赖 §3 前置项 A1~A6(app_key/flowGuid/菜单 modId 由管理员分配);/api/* 请求按平台 CAS 约定 302 跳 SSO 登录(浏览器带票据后正常,与改造前行为一致) |
| 2026-10-09 | P6.5 冒烟 | S1~S6 全部执行完毕(结果见 §8 P6.5 表)。新增冒烟辅助件 config/DevMockUserFilter(smoke.auth.enabled=true + --yh.sso.url-pattern=/__smoke_off__/* 启动即可本地免 CAS 冒烟)。修复 3 个 bug:① DictService.normalizeType 大小写比对错误("leaveType".equals("leavetype") 恒 false)导致字典写接口全挂 → 改为忽略大小写比对;② LeaveSettingService.saveOption 显式 null 无法清空 timeManagedTypes → 改为无条件写入(null 视为清空);③ LeaveRequest.userId 多余的 @NotNull 与前端契约冲突(前端从不传 userId、后端本就从 UserContext 取)导致前端 Create/Edit 100% 被 Bean Validation 拦截报"不能为null" → 删除该注解。前端修复:ApplyLeave/LeaveSetting 共 6 处裸 required FormItem 补 :model/prop/rules,消除常驻 "undefined is required" 错误文案。修复后 28 例单测复跑全绿、前端重建通过、浏览器复验通过(提交失败 toast 正确透传"发起审批流程失败:OpenAPI 404",DB 无孤儿单)。流程中心 404 为 flowGuid 占位预期(待 A3) |
| 2026-10-09 | P8 完成 | 对照旧版生产系统(oa.bestsch.com/leave,lwj 登录勘察)补齐:按天模式(calc_way + 半天/半天以上表单 + 天显示)、最少请假时间(min_minutes + 校验)、课务安排三选一(course_arrange)、申请页时间说明行;详见 §8 P8。真实账号(李晶)浏览器全链路验证通过;application.yml 端口改 8108(IDEA 实例),vite 代理同步 |
| 2026-10-09 | P8 微调 | ① 移除申请页顶部「我要请假/编辑请假申请」标题(页面更贴近旧版留白);② 时长开放编辑:申请页时长输入框由禁用改为可编辑(默认自动试算值、后缀带单位,起止时间变化后恢复自动值),提交携带手工值;后端 resolveHours:请求 totalHours>0 时以手工值为准(原逻辑一律服务端重算覆盖),最少请假时间/余额校验按最终落库值执行;后端编译 + 前端构建通过(后端需重启生效) |
| 2026-10-09 | P8 教师时长设置对齐旧版 | 按旧版 oa.bestsch.com/leave/leave_timeSetting「教师时长设置」tab 实地截图 + 历史源码(请假前端 TeaTimeImport.vue、C# leave_System.cs ImportQJSchUserTime/ExportQJSchUserTime)重做:① 管控类型勾选迁入教师时长 tab(「请选择需要使用时长的请假类型」勾选即存,timeLimitEnabled 随勾选联动;其他设置中原两项移除);② 左侧教师组树过滤(base/GetSchoolAll 组节点);③ 表格逐行「新加时长(小时)InputNumber + 情况备注」,底部组内合计剩余时长 + 统一提交(复用 Adjust 批量接口),替代原逐人弹窗;④ 新增 Excel 导入:GET leave/time/ImportTemplate(easyexcel 出模板:部门/教师名称/新加时长(小时)/备注,教师按组预填)+ POST leave/time/Import(表头校验、按部门+姓名定位教师、任一行错整体不入账并返回行级错误明细,语义同旧版 C#);前端 LeaveSetting.vue 重构该 tab + 导入弹窗;mvn test 28 例全绿 + 前端 build 通过(后端需重启) |
| 2026-10-09 | P9 完成 | 流程发起空实例强制失败回滚;回调增加当前 attempt/instance 隔离、重复终态幂等与冲突终态保护;Create 申请人只取登录态并移除前后端 LeaveRequest.userId;新增 6 例流程契约/边界测试(总计 34 例全绿),npm run typecheck:leave 与生产构建通过。全工程类型检查另检出 74 个未使用模板历史错误,见 R7。 |
交付物清单
后端(E:\YHXT\java\WorkLeave)
docs/教师请假模块-改造计划-v1.md(本文件)src/main/resources/sql/workleave_schema.sql(建表 + 字典种子)src/main/java/com/yunhai/workleave/(8 实体 + 8 仓储 + 4 控制器 + 5 服务 + 2 门面/回调)src/test/java/com/yunhai/workleave/(28 例单测)target/workleave.jar(可执行 fat jar)
前端(E:\YHXT\view\WorkLeave)
src/types/leave.ts、src/api/module/leave.ts、src/router/routes.tssrc/utils/leaveTime.ts(与后端同规则的时长试算)src/views/leave/*.vue(9 个页面)dist/(构建产物,vite build 直出)
启动与联调说明
# 后端(本地库 work_leave,root/123123,见 application.yml)
cd E:\YHXT\java\WorkLeave
mvn -o -DskipTests package
java -jar target/workleave.jar # 首启自动建表;8108 端口
# 前端
cd E:\YHXT\view\WorkLeave
npm install && npm run dev # 8078,/api 代理到 8108
npm run typecheck:leave # 请假模块严格类型检查
本地未起 Redis / 未配 flow-callback 时
yh.flow-callback.enabled=false, 申请提交会因流程中心不可达而整体回滚(设计如此:不留"不回流"的孤儿单)。 需要本地跑通提交链路时,按手册 §5.0 配yh.open.base-host指向 open 子域名并联通 160。
# 本地冒烟模式(免 CAS + 模拟用户 1001/冒烟测试教师/schId=1,见 P6.5)
java -jar target/workleave.jar --smoke.auth.enabled=true --yh.sso.url-pattern=/__smoke_off__/*