Files
work-leave-service/docs/教师请假模块-改造计划-v1.md
wanghu 25147ec74e
build / build-push (push) Successful in 50s
commit
2026-10-10 15:42:52 +08:00

543 lines
43 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 教师请假(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 历史缺陷清单(重写时修正,不复制)
1. 半天换算 `JsAPm` 判成 `KsAPm`(C# 源码 bug)→ 新方案按天模式整体取消,不涉及。
2. 拒绝时返还时长传错类型 → 新方案拒绝不返还(未扣过),无此分支。
3. 附件绑定不落库 → 新方案仓储显式 save。
4. 时长池新建记录 `null + x = null` → 新方案 `default 0`。
5. 时间段校验用字符串拼 SQL → 新方案参数化 JPA + LocalDateTime。
6. 列表时间过滤口径不一(重叠 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;F1~F2 之间保持;F2~A1 之间→A1;晚于 A2→A2;A1~A2 保持);
end 同理(F2~A1 之间→F2);`minutes = end - start`;若 `start<=F2 && end>=A1` 再减午休 `(A1-F2)`。
- 跨天:首日 = `A2 - start`(start 落在 F1~F2 时再减午休);末日 = `end - F1`(end 落在 A1~A2 时再减午休);
中间整天按 `8h/天`(= `A2-A1 + F2-F1`);合计取小时。
- 校验:结束必须晚于开始;单条时长 > 0;跨天视为按天折算(不再区分上下午字段)。
### 6.2 申请与校验(Create/Edit)
1. 类型必须为启用的 leaveType 字典项;事由在 `reason_must=1` 时必填且 ≤50 字。
2. 时间冲突:同人、`status in (0,1)`、`(start_time < 已有.end_time) && (已有.start_time < end_time)` → 报"与已有请假时间冲突"。
3. 时长管控:类型 ∈ `time_managed_types` 时,`在途(0状态且管控类型) + 本次 <= usable`(无记录按 0),否则报"可申请 X 小时,时间不足"。
4. `total_hours` 后端按 §6.1 复核(与前端不一致以服务端为准落库)。
5. 提交成功 → 组装 `FlowApplyBody`:bizType=`leave`、bizId=`{id}-{attempt}`、bizTitle=`{姓名}的{类型}申请`、bizJump=page("detail").param("leaveId",id)、bizSnapshot=摘要(类型/起止/时长/事由)、submitterId/Name、formData(类型、时长、天数)。
6. 发起成功回写 `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_time` LEFT JOIN 教师名单(`base/GetSchoolAll` 同源,按组过滤)。
- 调整:`edit_hours` 可正可负,`now = old + edit`(不低于 0),写流水(describe=备注或"新加"),operator 取当前用户。
- 流水统计:`Sum` 返回 `新加总=Σ(±)`、`剩余总=Σ usable`(按过滤区间口径)。
---
## 7. 流程审核器对接要点(照手册执行)
1. **pom**:`yh-flow-callback-spring-boot-starter` + `spring-boot-starter-data-redis`(验签密钥链必需,optional 不传递)。
2. **配置**:
```yaml
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,无需放行改造。
3. **发起**:`YschFlowOpenApi.apply(schId, body, FlowApplyResult.class)`。
4. **回调**:实现 `FlowCallbackHandler#onEvent`(§6.3);`schId` 从事件取,不信任本地上下文。
5. **待办/审核**:`todoTasks(schId, assigneeId, "leave", page, size)`、`approveTask(schId, taskId, operatorId, operatorName, opinion[, edgeName])`、`rejectTask(...)`。
6. **进度/流程图**:`progress(null, "leave", bizId, schId, FlowProgress.class)`、`diagramUrl(instanceId, flowGuid, "track", schId, FlowDiagramUrl.class)`。
7. **自检**(手册 §5.1):本地单测(mock 回调事件 + apply 出参断言)→ 160 环境全链路 9 项验收。
---
## 8. 分阶段任务与验收
### P0 骨架重整
- [x] 后端:包 `com.yunhai.dormnotice` → `com.yunhai.workleave`(目录迁移 + 全量 import 替换);`DormNoticeApplication` → `WorkLeaveApplication`;pom `artifactId/name/finalName` → `workleave`;`bootstrap.yml spring.application.name` → `workleave`;日志/库名对齐
- [x] 后端:删除宿舍业务(ActivityController、Activity 系实体/模型/仓储/服务);保留 log/dict/file/base/contacts 底座
- [x] 前端:`package.json` name、`vite.config` 标题、`index.html`、README、App.vue appName → 教师请假
- **验收**:后端 `mvn -q compile` 通过;前端构建通过;全局搜索无 `dormnotice` 残留
### P1 实体与数据层
- [x] `BaseEntity` 审计人改存 userId(`create_by/update_by`)
- [x] 新实体 + Repository(LeaveApply/LeaveSetting/LeaveTimeSet/LeaveUserTime/LeaveUserTimeRecord/LeaveSection/LeaveCc/LeaveAuditTrace)
- [x] `sql/workleave_schema.sql`(建表 + leaveType 字典种子:事假/病假/调休/公假/婚假/产假)
- **验收**:编译通过;启动自动建表成功;`leaveType` 字典种子可查
### P2 设置域 + 时长域
- [x] `LeaveSettingController/Service`:作息时间、其他设置、节次
- [x] `LeaveTimeController/Service`:时长列表/调整/流水/合计;`LeaveTimeCalculator`
- **验收**:单测覆盖时长算法(同日/跨天/边界 clamp/午休)—— 16 例全绿
### P3 申请域 + 流程对接
- [x] `LeaveController/Service`:Create/Edit/Delete/Withdraw/Cancel/List/Detail/MyTime/Statis/TodoList/AuditedList/Audit
- [x] `flow/LeaveFlowService` + `flow/LeaveFlowCallbackHandler`(§6.3)
- [x] pom/application.yml 流程依赖与配置(§7)
- **验收**:编译通过;回调单测(mock FlowCallbackEvent 覆盖 5 类事件 + 终态不回退 + 乱序门 + 抄送幂等)—— 12 例全绿
### P4 前端壳层与路由
- [x] `api/module/leave.ts`、`types/leave.ts`、`router/routes.ts`(§4.3)、`menuRoute.ts` 别名更新
- [x] 删除 `views/dorm/*`、`api/module/dorm.ts`、`types/dorm.ts` 及依赖其的组件
- **验收**:`npm run build` 通过;菜单解析器对新节点 path 命中
### P5 前端页面
- [x] ApplyLeave:表单 + 时长实时试算 + 余额提示 + 附件 + 抄送人 + 提交/重新提交
- [x] MyLeave / LeaveDetail / LeaveAudit / LeaveCc / AllLeave
- [x] LeaveSetting(5 个 tab)/ TimeLog / LeaveStats
- **验收**:构建通过(联调需本地后端 + DB 环境,见 P6 说明)
### P6 联调与验收
- [x] 后端 `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 已交付功能不重做。
**审计发现**:
1. 流程发起返回 `null` 或缺少 `instanceId` 时,当前代码仍可能提交为“审批中”,形成无真实流程实例的孤儿单。
2. 拒绝/撤回后重新提交会复用同一请假主键;旧实例迟到事件若按主键兜底定位,可能误推进新 attempt。
3. `LeaveRequest.userId` 来自客户端请求体,普通教师可构造该字段替其他用户发起申请;申请人必须只信任登录态。
4. 手册 §5.1 要求 apply 请求契约断言;现有 12 例流程测试只覆盖回调状态机,没有覆盖
`bizType/bizId/bizJump/bizSnapshot/formData/submitter`。
**实施任务**:
- [x] P9.1 发起结果强校验:`FlowApplyResult` 及 `instanceId` 必须非空,否则抛业务异常并由事务回滚。
- [x] P9.2 回调实例隔离:事件 `bizId/instanceId` 必须与当前 attempt 一致;旧实例迟到事件正常忽略,不触发重推。
- [x] P9.3 回调终态幂等:重复终态事件不重复产生副作用,冲突终态事件不得覆盖已落终态。
- [x] P9.4 申请身份收口:Create 始终使用登录态 `currentUserId/userName`,删除前后端请求契约中的 `userId`。
- [x] P9.5 补齐流程发起请求构造测试、空实例回滚边界测试、旧 attempt/终态冲突回调测试。
- [x] P9.6 验证:后端 `mvn test` 34 例全绿;前端 `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.ts`
- `src/utils/leaveTime.ts`(与后端同规则的时长试算)
- `src/views/leave/*.vue`(9 个页面)
- `dist/`(构建产物,vite build 直出)
### 启动与联调说明
```bash
# 后端(本地库 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。
```bash
# 本地冒烟模式(免 CAS + 模拟用户 1001/冒烟测试教师/schId=1,见 P6.5)
java -jar target/workleave.jar --smoke.auth.enabled=true --yh.sso.url-pattern=/__smoke_off__/*
```