# 教师请假(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__/* ```