A1-A7 收口OA统一审批中心与企微基础设施路线

这个提交包含在:
2026-05-09 15:28:39 +08:00
父节点 37c717110b
当前提交 8110e7df9c
共修改 10 个文件,包含 103 行新增和 32 行删除
+4 -4
查看文件
@@ -1,6 +1,6 @@
export const MAIN_NAV_ORDER = ["/portal", "/team", "/leave-requests", "/reimbursements", "/assets", "/weekly-reports", "/todos", "/announcements", "/projects"]; export const MAIN_NAV_ORDER = ["/portal", "/workflow/tasks", "/team", "/leave-requests", "/reimbursements", "/assets", "/weekly-reports", "/todos", "/announcements", "/projects"];
export const ADMIN_NAV_ORDER = ["/system/workspace", "/people", "/organization", "/system/workflow-definitions", "/system/settings", "/system/feature-flags", "/audit-logs"]; export const ADMIN_NAV_ORDER = ["/system/workspace", "/people", "/organization", "/system/workflow-definitions", "/system/settings", "/system/feature-flags", "/audit-logs"];
export const BACKUP_NAV_ORDER = ["/attendance", "/workflow/tasks", "/contracts-registry", "/purchase-requests", "/travel-requests", "/budgets", "/payment-batches", "/ap-operations", "/invoice-pool", "/inventory-checks", "/system/cutover", "/system/integrations/erp", "/dashboard", "/settings/profile"]; export const BACKUP_NAV_ORDER = ["/attendance", "/contracts-registry", "/purchase-requests", "/travel-requests", "/budgets", "/payment-batches", "/ap-operations", "/invoice-pool", "/inventory-checks", "/system/cutover", "/system/integrations/erp", "/dashboard", "/settings/profile"];
const PAGE_TITLES: Array<{ path: string; title: string; group: string }> = [ const PAGE_TITLES: Array<{ path: string; title: string; group: string }> = [
{ path: "/portal", title: "工作台", group: "工作台" }, { path: "/portal", title: "工作台", group: "工作台" },
@@ -22,8 +22,8 @@ const PAGE_TITLES: Array<{ path: string; title: string; group: string }> = [
{ path: "/reimbursements", title: "报销", group: "主导航" }, { path: "/reimbursements", title: "报销", group: "主导航" },
{ path: "/leave-requests", title: "请假", group: "主导航" }, { path: "/leave-requests", title: "请假", group: "主导航" },
{ path: "/assets", title: "资产", group: "主导航" }, { path: "/assets", title: "资产", group: "主导航" },
{ path: "/workflow/tasks/", title: "审批详情", group: "备用模块" }, { path: "/workflow/tasks/", title: "审批详情", group: "主导航" },
{ path: "/workflow/tasks", title: "审批待办", group: "备用模块" }, { path: "/workflow/tasks", title: "审批待办", group: "主导航" },
{ path: "/mobile/assets/scan", title: "移动资产扫描", group: "移动工作台" }, { path: "/mobile/assets/scan", title: "移动资产扫描", group: "移动工作台" },
{ path: "/mobile/approvals", title: "移动审批", group: "移动工作台" }, { path: "/mobile/approvals", title: "移动审批", group: "移动工作台" },
{ path: "/mobile/capture", title: "移动拍照上传", group: "移动工作台" }, { path: "/mobile/capture", title: "移动拍照上传", group: "移动工作台" },
@@ -19,10 +19,24 @@ async function login(page: Page, user: E2EUser) {
await page.goto("/login"); await page.goto("/login");
const credentialForm = page.locator("#login-identifier, input[autocomplete='username'], input[placeholder*='邮箱']").first(); const credentialForm = page.locator("#login-identifier, input[autocomplete='username'], input[placeholder*='邮箱']").first();
const accountLoginButton = page.getByRole("button", { name: "使用账号密码登录" }); const accountLoginButton = page.getByRole("button", { name: "使用账号密码登录" });
for (let attempt = 0; attempt < 3; attempt += 1) {
const credentialFormVisible = await credentialForm.isVisible({ timeout: 1_000 }).catch(() => false); const credentialFormVisible = await credentialForm.isVisible({ timeout: 1_000 }).catch(() => false);
const accountLoginVisible = await accountLoginButton.isVisible().catch(() => false); if (credentialFormVisible) {
if (!credentialFormVisible && accountLoginVisible) { break;
}
const accountLoginVisible = await accountLoginButton.isVisible({ timeout: 3_000 }).catch(() => false);
if (!accountLoginVisible) {
break;
}
try {
await accountLoginButton.click({ timeout: 5_000 }); await accountLoginButton.click({ timeout: 5_000 });
} catch (error) {
if (attempt >= 2) {
throw error;
}
}
} }
const identifierByTestId = page.getByTestId("login-identifier"); const identifierByTestId = page.getByTestId("login-identifier");
+15 -8
查看文件
@@ -3,7 +3,7 @@
## 1. 适用范围 ## 1. 适用范围
- 适用于 `small_team_internal` 工作区模式。 - 适用于 `small_team_internal` 工作区模式。
- 适用于单公司、少部门、全员账号密码登录的小团队。 - 适用于单公司、少部门、全员账号密码登录的小团队。
- 目标不是替代飞书/钉钉等 SaaS,而是把内部高频管理动作收口到一个系统。 - 目标是把内部高频管理动作收口到 `hyb-oa`;企业微信只作为入口、身份、通知和打卡底座。
## 2. 管理员每周固定动作 ## 2. 管理员每周固定动作
- 周一:查看 `Portal` 的团队状态、上周周报汇总、入离职待办、资产待归还。 - 周一:查看 `Portal` 的团队状态、上周周报汇总、入离职待办、资产待归还。
@@ -12,8 +12,8 @@
## 3. 首次上线最小配置 ## 3. 首次上线最小配置
1. 进入 `工作区设置`,确认产品模式为 `small_team_internal`。 1. 进入 `工作区设置`,确认产品模式为 `small_team_internal`。
2. 仅保留主导航:`Portal`、`团队`、`请假`、`报销`、`资产`、`周报`、`待办`、`公告`、`项目`。 2. 仅保留主导航:`Portal`、`审批待办`、`团队`、`请假`、`报销`、`资产`、`周报`、`待办`、`公告`、`项目`。
3. 将 `考勤`、`采购`、`差旅`、`预算`、`支付批次`、`AP 运营`、`ERP`、`AI` 等模块降级为隐藏或管理员备用。 3. 将 `考勤`、`采购`、`差旅`、`预算`、`支付批次`、`AP 运营`、`ERP`、`AI` 等模块按真实启用情况保留为主路径或管理员备用;审批动作统一留在 OA。
4. 在 `用户管理` 中按推荐预设配置:老板 / 管理员 / 主管 / 员工。 4. 在 `用户管理` 中按推荐预设配置:老板 / 管理员 / 主管 / 员工。
5. 检查功能开关: 5. 检查功能开关:
- 建议开启:`portal_home_v1`、`announcements_v1`、`presence_board_v1`、`weekly_report_v1`、`employee_lifecycle_v1`、`todo_board_v1`、`global_search_v1` - 建议开启:`portal_home_v1`、`announcements_v1`、`presence_board_v1`、`weekly_report_v1`、`employee_lifecycle_v1`、`todo_board_v1`、`global_search_v1`
@@ -36,10 +36,12 @@
2. 若员工仍有名下资产或待处理审批任务,系统不会自动完成离职。 2. 若员工仍有名下资产或待处理审批任务,系统不会自动完成离职。
3. checklist 完成且阻塞清零后,账号会被停用,主业务入口对该账号不可再用。 3. checklist 完成且阻塞清零后,账号会被停用,主业务入口对该账号不可再用。
## 5. 团队状态管理 ## 5. 团队状态与考勤口径
- 员工每天可在 `Portal` 或 `团队` 页更新状态:在岗 / 居家 / 外出 / 出差。 - 员工每天可在 `Portal` 或 `团队` 页更新状态:在岗 / 居家 / 外出 / 出差。
- 已批准请假会自动覆盖当天状态为 `请假`。 - 已批准请假会自动覆盖当天状态为 `请假`。
- 管理员只需要看当天分布,不做打卡、迟到、排班和月度统计。 - 企业微信打卡只作为原始采集工具;定位、人脸、Wi-Fi 和排班规则不在 OA 内重造。
- OA 后续同步企业微信打卡数据后,应结合 OA 请假、补卡、外出、加班审批形成正式考勤口径。
- 请假、补卡、外出、加班等审批仍在 OA 内完成,不使用企业微信原生审批。
## 6. 周报与待办 ## 6. 周报与待办
- 周报固定三段:本周完成 / 当前阻塞 / 下周计划。 - 周报固定三段:本周完成 / 当前阻塞 / 下周计划。
@@ -84,13 +86,15 @@
4. 连续 4 周都达到预期后,再将 R66 试点验收关单并形成最终验收结论。 4. 连续 4 周都达到预期后,再将 R66 试点验收关单并形成最终验收结论。
## 12. 企业微信入口与登录策略 ## 12. 企业微信入口与登录策略
- 建议把企业微信定位为 `入口 + 提醒 + 可选免登`,不要在企业微信里再维护第二套审批或项目录入路径。 - 企业微信定位为 `入口 + 免登 + 通知 + 打卡原始采集`,不承载公司正式审批流。
- 公司只维护一个企业微信自建应用,建议命名为 `海音达工作台`。
- 默认工作台入口在企业微信手机端落到 `/mobile`;桌面浏览器和账号密码登录仍进入 `/portal`。员工真正操作仍在 `hyb-oa` 内完成。 - 默认工作台入口在企业微信手机端落到 `/mobile`;桌面浏览器和账号密码登录仍进入 `/portal`。员工真正操作仍在 `hyb-oa` 内完成。
- 所有报销、采购、费用、合同、请假、补卡、外出、加班、出差审批都在 OA 内提交和审批;企业微信消息只负责提醒并 deeplink 回 OA。
- 企微免登建议先完成账号绑定,再逐步在试点部门开启;任何时候都保留账号密码回退。 - 企微免登建议先完成账号绑定,再逐步在试点部门开启;任何时候都保留账号密码回退。
- 若 OA 部署到公网,生产建议同时满足:`HTTPS`、`COOKIE_SECURE=true`、登录失败锁定、限流、审计开启。 - 若 OA 部署到公网,生产建议同时满足:`HTTPS`、`COOKIE_SECURE=true`、登录失败锁定、限流、审计开启。
### 正式接入最小步骤 ### 正式接入最小步骤
1. 在企业微信管理后台创建或编辑自建应用。 1. 在企业微信管理后台创建或编辑唯一自建应用:`海音达工作台`。
2. 将工作台入口配置为:`https://oa.hyinda.com/api/auth/wecom/login?returnTo=%2Fmobile`。 2. 将工作台入口配置为:`https://oa.hyinda.com/api/auth/wecom/login?returnTo=%2Fmobile`。
3. 将 OAuth 回调地址配置为:`https://oa.hyinda.com/api/auth/wecom/callback`。 3. 将 OAuth 回调地址配置为:`https://oa.hyinda.com/api/auth/wecom/callback`。
4. 在 `deploy/trialrun/.env` 中补齐并开启: 4. 在 `deploy/trialrun/.env` 中补齐并开启:
@@ -103,12 +107,14 @@
- `WECOM_REDIRECT_URI=https://oa.hyinda.com/api/auth/wecom/callback` - `WECOM_REDIRECT_URI=https://oa.hyinda.com/api/auth/wecom/callback`
- `WECOM_ALLOWED_RETURN_HOSTS=oa.hyinda.com` - `WECOM_ALLOWED_RETURN_HOSTS=oa.hyinda.com`
5. 先让管理员用账号密码登录,在 `个人设置` 完成一次绑定,再测试企业微信工作台直达;已绑定用户从手机企业微信进入时应直接看到移动工作台。 5. 先让管理员用账号密码登录,在 `个人设置` 完成一次绑定,再测试企业微信工作台直达;已绑定用户从手机企业微信进入时应直接看到移动工作台。
6. 不配置企业微信原生审批模板作为正式流程;审批提醒使用企业微信消息,审批动作回到 OA 页面完成。
### 员工会看到的典型状态 ### 员工会看到的典型状态
- 未绑定:登录页会提示“当前企业微信账号尚未绑定”,回退到账号密码登录。 - 未绑定:登录页会提示“当前企业微信账号尚未绑定”,回退到账号密码登录。
- 已绑定:从企业微信工作台进入后直接落到 `/mobile`,桌面或普通浏览器场景仍可进入 `Portal`。 - 已绑定:从企业微信工作台进入后直接落到 `/mobile`,桌面或普通浏览器场景仍可进入 `Portal`。
- 入口未开放:个人设置仍可绑定,但登录页不会把企业微信作为正式入口展示。 - 入口未开放:个人设置仍可绑定,但登录页不会把企业微信作为正式入口展示。
- 配置不完整:个人设置会明确提示缺少 `CorpID / AgentID / 回调地址` 等原因。 - 配置不完整:个人设置会明确提示缺少 `CorpID / AgentID / 回调地址` 等原因。
- 收到审批提醒:点击后进入 OA 审批页,在 OA 中同意、驳回、退回或转审。
## 13. 轻项目管理使用建议 ## 13. 轻项目管理使用建议
- 项目主档只做轻量管理:成员、里程碑、任务、风险、交付记录、合同台账与资料引用。 - 项目主档只做轻量管理:成员、里程碑、任务、风险、交付记录、合同台账与资料引用。
@@ -124,6 +130,7 @@
- `hyb-oa` 只记录涉密合同或资料的元数据:编号、责任人、密级、归档位置、离线档案号。 - `hyb-oa` 只记录涉密合同或资料的元数据:编号、责任人、密级、归档位置、离线档案号。
- 涉密正文、涉密附件、详细报价/成本/毛利、敏感客户资料不进入公网 OA。 - 涉密正文、涉密附件、详细报价/成本/毛利、敏感客户资料不进入公网 OA。
- 企业微信通知不得发送涉密正文,只保留最小必要信息和 deeplink。 - 企业微信通知不得发送涉密正文,只保留最小必要信息和 deeplink。
- 企业微信审批不作为涉密或财务审批载体,正式审批记录以 OA 审批历史和审计日志为准。
- 如项目被标记为涉密,管理员应要求正文继续保存在内网文件盘、NAS 或离线档案中。 - 如项目被标记为涉密,管理员应要求正文继续保存在内网文件盘、NAS 或离线档案中。
## 15. 初始化与导入建议 ## 15. 初始化与导入建议
@@ -176,7 +183,7 @@
## 19. R29 设计与工作台治理口径 ## 19. R29 设计与工作台治理口径
- 首页不再做“模块超市”,而是先告诉员工今天该做什么。 - 首页不再做“模块超市”,而是先告诉员工今天该做什么。
- 管理员不要把备用模块重新平铺到主导航,避免系统再次变成模块拼盘。 - 管理员不要把备用模块重新平铺到主导航,避免系统再次变成模块拼盘。
- 员工入口默认认知应保持为:`企业微信进入 -> Portal 查看今日动作 -> 进入具体模块处理`。 - 员工入口默认认知应保持为:`企业微信进入 -> OA 移动工作台 -> 进入审批/报销/请假等模块处理`;PC 用户可直接登录 OA。
- 如果未来新增页面,请优先复用统一骨架: - 如果未来新增页面,请优先复用统一骨架:
- 首页:`WorkbenchHero + MetricCard + SectionPanel` - 首页:`WorkbenchHero + MetricCard + SectionPanel`
- 列表页:`PageHeader + FilterBar + 主列表` - 列表页:`PageHeader + FilterBar + 主列表`
+8 -4
查看文件
@@ -2,6 +2,7 @@
## 1. 你每周主要会用到什么 ## 1. 你每周主要会用到什么
- `Portal`:看公告、我的待办、我的资产、本周周报入口。 - `Portal`:看公告、我的待办、我的资产、本周周报入口。
- `审批`:处理分配给你的报销、请假、采购、差旅等审批。
- `团队`:更新今天是在岗、居家、外出还是出差。 - `团队`:更新今天是在岗、居家、外出还是出差。
- `请假`:提交并查看请假记录。 - `请假`:提交并查看请假记录。
- `报销`:继续提交和跟踪报销。 - `报销`:继续提交和跟踪报销。
@@ -32,8 +33,9 @@
- 可填写优先级和截止日期。 - 可填写优先级和截止日期。
- 完成后及时点完成,避免 Portal 一直堆积。 - 完成后及时点完成,避免 Portal 一直堆积。
## 6. 请假和报销 ## 6. 审批、请假和报销
- 继续按现有流程提交。 - 公司正式审批都在海音达工作台内完成,不在企业微信审批里处理。
- 请假、补卡、外出、加班、报销、采购、差旅等申请都按 OA 页面提交。
- 批准后的请假会自动反映到团队状态。 - 批准后的请假会自动反映到团队状态。
- 报销状态仍可在系统内查看,不需要额外问行政。 - 报销状态仍可在系统内查看,不需要额外问行政。
@@ -46,14 +48,16 @@
- 每周五提交周报 - 每周五提交周报
- 状态有变化时及时更新 `团队` - 状态有变化时及时更新 `团队`
- 有通知先看站内消息,IM 只是补充提醒 - 有通知先看站内消息,IM 只是补充提醒
- 收到企业微信审批提醒时,点开后回到 OA 页面处理。
## 9. 从企业微信进入 ## 9. 从企业微信进入
- 如果公司启用了企业微信工作台入口,优先从企业微信打开海音达工作台。 - 如果公司启用了企业微信工作台入口,优先从企业微信打开海音达工作台。
- 企业微信只是入口和提醒,真正填写和修改仍在海音达工作台页面内完成。 - 企业微信只是入口、免登和提醒,真正填写、审批和修改仍在海音达工作台页面内完成。
- 企业微信打卡仍按公司要求使用;请假、补卡、外出、加班等修正申请在 OA 内提交。
- 如果企业微信免登失败,直接回到账号密码登录即可。 - 如果企业微信免登失败,直接回到账号密码登录即可。
- 如果你第一次从企业微信进入但看到“尚未绑定”,请不要反复重试,先用账号密码登录,再到 `个人设置 -> 企业微信绑定` 完成绑定。 - 如果你第一次从企业微信进入但看到“尚未绑定”,请不要反复重试,先用账号密码登录,再到 `个人设置 -> 企业微信绑定` 完成绑定。
- 如果看到“登录已失效 / 已超时”,重新从企业微信工作台进入即可。 - 如果看到“登录已失效 / 已超时”,重新从企业微信工作台进入即可。
- 日常建议:在企业微信里打开海音达工作台,看 `Portal` 和通知;复杂填写、项目更新、报销与周报仍在工作台页面里完成。 - 日常建议:手机从企业微信打开海音达工作台,PC 直接登录 OA;审批、项目更新、报销与周报都在工作台页面里完成。
## 10. 项目怎么用 ## 10. 项目怎么用
- 如果你是项目成员,优先在 `项目` 页面查看自己的里程碑、任务、风险和交付记录。 - 如果你是项目成员,优先在 `项目` 页面查看自己的里程碑、任务、风险和交付记录。
+16
查看文件
@@ -5,6 +5,22 @@
--- ---
## 平台边界
### P0-1 OA 唯一审批中心
- `hyb-oa` 是公司唯一业务主系统和唯一审批中心。
- 报销、采购、费用、合同、请假、补卡、外出、加班、出差等正式审批,都必须在 OA 内提交、审批、留痕、归档。
- 企业微信原生审批不作为公司正式审批入口;不得出现一部分审批在企业微信、一部分审批在 OA 的割裂口径。
- 企业微信只作为基础设施:手机工作台入口、免登/绑定、通讯录身份、消息通知、打卡原始采集。
### P0-2 企业微信打卡与 OA 考勤口径
- 企业微信打卡用于原始采集,保留其定位、人脸、Wi-Fi、规则与设备能力。
- OA 后续可同步企业微信打卡原始记录,并结合 OA 内的请假、补卡、外出、加班审批形成正式考勤口径。
- 请假、补卡、外出、加班等考勤修正审批仍在 OA 完成,不走企业微信审批。
- 第一阶段不新增打卡 API 同步实现,先冻结边界,避免在审批中心收口时扩大战线。
---
## 请假模块 ## 请假模块
### L-1 团队日历可见性 ### L-1 团队日历可见性
+1
查看文件
@@ -9,6 +9,7 @@
- 发票是报销凭证,附件是补充材料,二者不能混用。 - 发票是报销凭证,附件是补充材料,二者不能混用。
- 已经报销、他人已报销、重复上传的发票不能加入新的报销单。 - 已经报销、他人已报销、重复上传的发票不能加入新的报销单。
- 服务端必须强约束,前端只负责友好提示。 - 服务端必须强约束,前端只负责友好提示。
- 报销审批必须在 OA 内完成,企业微信只发送提醒和 deeplink,不作为报销审批流或财务附件主库。
## 发票状态 ## 发票状态
@@ -0,0 +1,8 @@
ID,Title,Description,Acceptance,Test_Method,Tools,Dev_Status,Review1_Status,Regression_Status,Files,Dependencies,Notes
A1,废弃飞书方案与清理工作区,"废弃飞书转向基线,删除未跟踪飞书文件并回退本轮飞书相关 tracked 改动,不使用 git reset --hard。","1) git status 不再包含 feishu/lark 相关新增文件;2) 不保留 FEISHU_* 配置、飞书路由、飞书 Prisma 表、飞书前端入口和飞书测试;3) 企业微信现有入口、绑定、通知能力不被删除。","命令:git status --short && rg -n ""FEISHU_|feishu|Feishu|飞书|lark|Lark"" server/src client/src server/prisma server/tests client/tests;业务流程:管理员确认飞书不再作为主平台或接入路线,系统仍保留企业微信工作台入口。",git|ripgrep,DONE,DONE,DONE,server|client|server/prisma|issues,"[]","回滚路径:如后续重新启用飞书,重新创建独立 Issue CSV 和迁移,不复用本轮废弃实现。"
A2,OA 唯一审批中心事实源,"固定 hyb-oa 是唯一业务主系统和唯一审批中心,企业微信原生审批不作为正式审批入口。","1) 产品/管理员/员工文档明确所有审批在 OA 内提交、审批、留痕、归档;2) 报销、采购、费用、合同、请假、补卡、外出、加班、出差均以 OA 审批为准;3) 企业微信只做入口、身份、通知和打卡原始采集。","业务流程:老板/管理员阅读事实源后能确认正式审批只认 OA;步骤:人工检查 docs/product-decisions.md、docs/admin-handbook.md、docs/employee-quickstart.md。",manual,DONE,DONE,DONE,docs/product-decisions.md|docs/admin-handbook.md|docs/employee-quickstart.md,"[""A1""]","不引入企业微信审批 API;不开发复杂 BPM。"
A3,企业微信入口免登通知边界固化,"保留一个企业微信自建应用作为海音达工作台入口,继续使用企业微信免登和消息通知,但审批动作必须回到 OA。","1) 管理员文档明确只建一个企业微信自建应用;2) 工作台入口默认 /mobile,PC 直接访问 OA;3) 企业微信消息只发送脱敏提醒和 deeplink;4) 不新增企业微信审批模板或审批回调依赖。","命令:cd client && npx playwright test tests/e2e/wecom-entry.spec.ts --workers=1 --reporter=list;业务流程:员工从企业微信工作台进入 OA 手机端,审批人从企微提醒点击进入 OA 审批页。",playwright|manual,DONE,DONE,DONE,docs/admin-handbook.md|docs/employee-quickstart.md|client/src/lib/runtimeContext.ts|client/src/pages/LoginPage.tsx,"[""A2""]","真实企业微信后台配置需人工复核;本轮不修改生产后台。"
A4,审批待办导航与移动入口收口,"把审批待办定位为 OA 核心入口而非边缘备用能力,PC 和移动端均应清晰可达。","1) PC 导航或工作台中审批待办入口清晰可达;2) 移动底部导航保留审批入口;3) 企业微信 UA 不误入 PC 页面;4) 不改变既有审批 API 契约。","命令:cd client && npm run typecheck && npm run build;cd client && npx playwright test tests/e2e/wecom-entry.spec.ts tests/e2e/workflow-quick-approve.spec.ts --workers=1 --reporter=list;业务流程:审批人从 PC 或手机进入待办并完成 OA 审批。",typescript|playwright,DONE,DONE,DONE,client/src/components/navigation|client/src/features/navigation.ts|client/src/features/mobile|client/tests/e2e,"[""A2"",""A3""]","若现有入口已满足,仅更新文案/事实源并用测试证明。"
A5,打卡与考勤边界文档化,"明确企业微信负责打卡原始采集,OA 后续同步并结合 OA 审批形成正式考勤口径。","1) 文档明确企业微信打卡不是审批中心;2) OA 请假、补卡、外出、加班审批是考勤修正依据;3) 本轮不新增打卡 API 同步实现;4) 形成后续 backlog 边界。","业务流程:管理员能据文档理解员工在企业微信打卡、在 OA 提交请假/补卡/外出/加班审批,最终考勤口径由 OA 汇总;步骤:人工检查 docs/admin-handbook.md 和 docs/product-decisions.md。",manual,DONE,DONE,DONE,docs/product-decisions.md|docs/admin-handbook.md,"[""A2""]","后续如接入企业微信打卡 API,另立 Issue CSV 和只读同步/补偿策略。"
A6,审批主流程回归,"验证报销、采购、请假等现有 OA 审批主流程不因飞书清理和入口收口被破坏。","1) workflow 语法检查通过;2) workflow actions 回归通过;3) 报销提交/审批/状态更新仍通过;4) 采购和请假审批相关单测或回归通过。","命令:cd server && npm run prisma:validate && node --check src/routes/workflow.js && node --check src/routes/reimbursements.js && ./scripts/regression-workflow-actions.sh;业务流程:报销提交->OA 审批->状态更新;采购提交->OA 审批;请假提交->OA 审批。",node|prisma|bash,DONE,DONE,DONE,server/src/routes/workflow.js|server/src/routes/reimbursements.js|server/scripts/regression-workflow-actions.sh,"[""A1"",""A4""]","涉及 workflow 状态机,完成后需 reviewer/回归重点关注状态跳转和权限。"
A7,部署公网与企业微信真机复核说明,"收口本轮验证结果并明确是否已部署、是否可在公网/企业微信真机测试。","1) 本地 server/client 检查通过或明确阻塞;2) git diff --check 通过;3) 最终回复明确未部署或已部署;4) 若未部署,不提示用户去企业微信真机测试。","命令:git diff --check;如部署则 curl /ready 和企业微信入口 smoke;业务流程:管理员根据最终报告判断是否可进入公网或企业微信工作台验收。",git|curl|manual,DONE,DONE,DONE,server|client|docs|issues|deploy,"[""A1"",""A2"",""A3"",""A4"",""A5"",""A6""]","本轮默认不主动改生产配置;部署需单独确认。"
1 ID Title Description Acceptance Test_Method Tools Dev_Status Review1_Status Regression_Status Files Dependencies Notes
2 A1 废弃飞书方案与清理工作区 废弃飞书转向基线,删除未跟踪飞书文件并回退本轮飞书相关 tracked 改动,不使用 git reset --hard。 1) git status 不再包含 feishu/lark 相关新增文件;2) 不保留 FEISHU_* 配置、飞书路由、飞书 Prisma 表、飞书前端入口和飞书测试;3) 企业微信现有入口、绑定、通知能力不被删除。 命令:git status --short && rg -n "FEISHU_|feishu|Feishu|飞书|lark|Lark" server/src client/src server/prisma server/tests client/tests;业务流程:管理员确认飞书不再作为主平台或接入路线,系统仍保留企业微信工作台入口。 git|ripgrep DONE DONE DONE server|client|server/prisma|issues [] 回滚路径:如后续重新启用飞书,重新创建独立 Issue CSV 和迁移,不复用本轮废弃实现。
3 A2 OA 唯一审批中心事实源 固定 hyb-oa 是唯一业务主系统和唯一审批中心,企业微信原生审批不作为正式审批入口。 1) 产品/管理员/员工文档明确所有审批在 OA 内提交、审批、留痕、归档;2) 报销、采购、费用、合同、请假、补卡、外出、加班、出差均以 OA 审批为准;3) 企业微信只做入口、身份、通知和打卡原始采集。 业务流程:老板/管理员阅读事实源后能确认正式审批只认 OA;步骤:人工检查 docs/product-decisions.md、docs/admin-handbook.md、docs/employee-quickstart.md。 manual DONE DONE DONE docs/product-decisions.md|docs/admin-handbook.md|docs/employee-quickstart.md ["A1"] 不引入企业微信审批 API;不开发复杂 BPM。
4 A3 企业微信入口免登通知边界固化 保留一个企业微信自建应用作为海音达工作台入口,继续使用企业微信免登和消息通知,但审批动作必须回到 OA。 1) 管理员文档明确只建一个企业微信自建应用;2) 工作台入口默认 /mobile,PC 直接访问 OA;3) 企业微信消息只发送脱敏提醒和 deeplink;4) 不新增企业微信审批模板或审批回调依赖。 命令:cd client && npx playwright test tests/e2e/wecom-entry.spec.ts --workers=1 --reporter=list;业务流程:员工从企业微信工作台进入 OA 手机端,审批人从企微提醒点击进入 OA 审批页。 playwright|manual DONE DONE DONE docs/admin-handbook.md|docs/employee-quickstart.md|client/src/lib/runtimeContext.ts|client/src/pages/LoginPage.tsx ["A2"] 真实企业微信后台配置需人工复核;本轮不修改生产后台。
5 A4 审批待办导航与移动入口收口 把审批待办定位为 OA 核心入口而非边缘备用能力,PC 和移动端均应清晰可达。 1) PC 导航或工作台中审批待办入口清晰可达;2) 移动底部导航保留审批入口;3) 企业微信 UA 不误入 PC 页面;4) 不改变既有审批 API 契约。 命令:cd client && npm run typecheck && npm run build;cd client && npx playwright test tests/e2e/wecom-entry.spec.ts tests/e2e/workflow-quick-approve.spec.ts --workers=1 --reporter=list;业务流程:审批人从 PC 或手机进入待办并完成 OA 审批。 typescript|playwright DONE DONE DONE client/src/components/navigation|client/src/features/navigation.ts|client/src/features/mobile|client/tests/e2e ["A2","A3"] 若现有入口已满足,仅更新文案/事实源并用测试证明。
6 A5 打卡与考勤边界文档化 明确企业微信负责打卡原始采集,OA 后续同步并结合 OA 审批形成正式考勤口径。 1) 文档明确企业微信打卡不是审批中心;2) OA 请假、补卡、外出、加班审批是考勤修正依据;3) 本轮不新增打卡 API 同步实现;4) 形成后续 backlog 边界。 业务流程:管理员能据文档理解员工在企业微信打卡、在 OA 提交请假/补卡/外出/加班审批,最终考勤口径由 OA 汇总;步骤:人工检查 docs/admin-handbook.md 和 docs/product-decisions.md。 manual DONE DONE DONE docs/product-decisions.md|docs/admin-handbook.md ["A2"] 后续如接入企业微信打卡 API,另立 Issue CSV 和只读同步/补偿策略。
7 A6 审批主流程回归 验证报销、采购、请假等现有 OA 审批主流程不因飞书清理和入口收口被破坏。 1) workflow 语法检查通过;2) workflow actions 回归通过;3) 报销提交/审批/状态更新仍通过;4) 采购和请假审批相关单测或回归通过。 命令:cd server && npm run prisma:validate && node --check src/routes/workflow.js && node --check src/routes/reimbursements.js && ./scripts/regression-workflow-actions.sh;业务流程:报销提交->OA 审批->状态更新;采购提交->OA 审批;请假提交->OA 审批。 node|prisma|bash DONE DONE DONE server/src/routes/workflow.js|server/src/routes/reimbursements.js|server/scripts/regression-workflow-actions.sh ["A1","A4"] 涉及 workflow 状态机,完成后需 reviewer/回归重点关注状态跳转和权限。
8 A7 部署公网与企业微信真机复核说明 收口本轮验证结果并明确是否已部署、是否可在公网/企业微信真机测试。 1) 本地 server/client 检查通过或明确阻塞;2) git diff --check 通过;3) 最终回复明确未部署或已部署;4) 若未部署,不提示用户去企业微信真机测试。 命令:git diff --check;如部署则 curl /ready 和企业微信入口 smoke;业务流程:管理员根据最终报告判断是否可进入公网或企业微信工作台验收。 git|curl|manual DONE DONE DONE server|client|docs|issues|deploy ["A1","A2","A3","A4","A5","A6"] 本轮默认不主动改生产配置;部署需单独确认。
+13 -7
查看文件
@@ -417,17 +417,23 @@ echo "[8/10] duplicate override audit"
RID_OVERRIDE_A="$(create_reimbursement "$EMPLOYEE_TOKEN" "$TMP_DIR/override-reim-a.json")" RID_OVERRIDE_A="$(create_reimbursement "$EMPLOYEE_TOKEN" "$TMP_DIR/override-reim-a.json")"
RID_OVERRIDE_B="$(create_reimbursement "$EMPLOYEE_TOKEN" "$TMP_DIR/override-reim-b.json")" RID_OVERRIDE_B="$(create_reimbursement "$EMPLOYEE_TOKEN" "$TMP_DIR/override-reim-b.json")"
OV_FILE="$TMP_DIR/override-$RUN_SUFFIX.pdf" OV_CODE="AUDDUP$RUN_SUFFIX"
printf '%%PDF-1.4\nMOCK_DUP:%s\n' "$RUN_SUFFIX" > "$OV_FILE" OV_NUMBER="AUDNO$RUN_SUFFIX"
OV_FILE_A="$TMP_DIR/override-a-$RUN_SUFFIX.pdf"
OV_FILE_B="$TMP_DIR/override-b-$RUN_SUFFIX.pdf"
printf '%%PDF-1.4\nMOCK_DUP_A:%s\n' "$RUN_SUFFIX" > "$OV_FILE_A"
printf '%%PDF-1.4\nMOCK_DUP_B:%s\n' "$RUN_SUFFIX" > "$OV_FILE_B"
upload_status="$(upload_invoice "$EMPLOYEE_TOKEN" "$OV_FILE" "$TMP_DIR/override-upload-a.json")" upload_status="$(upload_invoice "$EMPLOYEE_TOKEN" "$OV_FILE_A" "$TMP_DIR/override-upload-a.json")"
assert_status "$upload_status" "200" "upload override invoice A" "$TMP_DIR/override-upload-a.json" assert_status "$upload_status" "200" "upload override invoice A" "$TMP_DIR/override-upload-a.json"
IID_OVERRIDE_A="$(jq -er '.data.invoicePoolId' "$TMP_DIR/override-upload-a.json")" IID_OVERRIDE_A="$(jq -er '.data.invoicePoolId' "$TMP_DIR/override-upload-a.json")"
patch_invoice "$EMPLOYEE_TOKEN" "$IID_OVERRIDE_A" "{\"totalAmount\":\"188.80\",\"invoiceCode\":\"$OV_CODE\",\"invoiceNumber\":\"$OV_NUMBER\"}" "$TMP_DIR/override-patch-a.json"
bind_invoice "$EMPLOYEE_TOKEN" "$IID_OVERRIDE_A" "$RID_OVERRIDE_A" "$TMP_DIR/override-bind-a.json" bind_invoice "$EMPLOYEE_TOKEN" "$IID_OVERRIDE_A" "$RID_OVERRIDE_A" "$TMP_DIR/override-bind-a.json"
upload_status="$(upload_invoice "$EMPLOYEE_TOKEN" "$OV_FILE" "$TMP_DIR/override-upload-b.json")" upload_status="$(upload_invoice "$EMPLOYEE_TOKEN" "$OV_FILE_B" "$TMP_DIR/override-upload-b.json")"
assert_status "$upload_status" "200" "upload override invoice B" "$TMP_DIR/override-upload-b.json" assert_status "$upload_status" "200" "upload override invoice B" "$TMP_DIR/override-upload-b.json"
IID_OVERRIDE_B="$(jq -er '.data.invoicePoolId' "$TMP_DIR/override-upload-b.json")" IID_OVERRIDE_B="$(jq -er '.data.invoicePoolId' "$TMP_DIR/override-upload-b.json")"
patch_invoice "$EMPLOYEE_TOKEN" "$IID_OVERRIDE_B" "{\"totalAmount\":\"188.80\",\"invoiceCode\":\"$OV_CODE\",\"invoiceNumber\":\"$OV_NUMBER\"}" "$TMP_DIR/override-patch-b.json"
bind_status="$(api_json_auth POST "$BASE_URL/api/invoice-pool/$IID_OVERRIDE_B/bind" "$EMPLOYEE_TOKEN" "$TMP_DIR/override-bind-b.json" "{\"reimbursementId\":\"$RID_OVERRIDE_B\"}")" bind_status="$(api_json_auth POST "$BASE_URL/api/invoice-pool/$IID_OVERRIDE_B/bind" "$EMPLOYEE_TOKEN" "$TMP_DIR/override-bind-b.json" "{\"reimbursementId\":\"$RID_OVERRIDE_B\"}")"
assert_status "$bind_status" "409" "bind duplicate before override" "$TMP_DIR/override-bind-b.json" assert_status "$bind_status" "409" "bind duplicate before override" "$TMP_DIR/override-bind-b.json"
@@ -446,17 +452,17 @@ echo "[9/10] OCR retry audit + VALID_006/VALID_007"
NOW_ISO="$(date -u +"%Y-%m-%dT%H:%M:%S.000Z")" NOW_ISO="$(date -u +"%Y-%m-%dT%H:%M:%S.000Z")"
OLD_ISO="$(date -u -d "-2 minutes" +"%Y-%m-%dT%H:%M:%S.000Z")" OLD_ISO="$(date -u -d "-2 minutes" +"%Y-%m-%dT%H:%M:%S.000Z")"
INVOICE_FREQ="$(create_invoice_record "$EMPLOYEE_IDENTIFIER" "MOCK_OK:$RUN_SUFFIX" "ocr_failed" "0" "$NOW_ISO")" INVOICE_FREQ="$(create_invoice_record "$EMPLOYEE_IDENTIFIER" "MOCK_OK:FREQ:$RUN_SUFFIX" "ocr_failed" "0" "$NOW_ISO")"
retry_freq_status="$(retry_ocr "$EMPLOYEE_TOKEN" "$INVOICE_FREQ" "$TMP_DIR/retry-freq.json")" retry_freq_status="$(retry_ocr "$EMPLOYEE_TOKEN" "$INVOICE_FREQ" "$TMP_DIR/retry-freq.json")"
assert_status "$retry_freq_status" "409" "retry too frequent" "$TMP_DIR/retry-freq.json" assert_status "$retry_freq_status" "409" "retry too frequent" "$TMP_DIR/retry-freq.json"
assert_error_code "$TMP_DIR/retry-freq.json" "VALID_007" "retry too frequent" assert_error_code "$TMP_DIR/retry-freq.json" "VALID_007" "retry too frequent"
INVOICE_EXCEEDED="$(create_invoice_record "$EMPLOYEE_IDENTIFIER" "MOCK_OK:$RUN_SUFFIX" "ocr_failed" "3" "$OLD_ISO")" INVOICE_EXCEEDED="$(create_invoice_record "$EMPLOYEE_IDENTIFIER" "MOCK_OK:EXCEEDED:$RUN_SUFFIX" "ocr_failed" "3" "$OLD_ISO")"
retry_exceeded_status="$(retry_ocr "$EMPLOYEE_TOKEN" "$INVOICE_EXCEEDED" "$TMP_DIR/retry-exceeded.json")" retry_exceeded_status="$(retry_ocr "$EMPLOYEE_TOKEN" "$INVOICE_EXCEEDED" "$TMP_DIR/retry-exceeded.json")"
assert_status "$retry_exceeded_status" "409" "retry attempts exceeded" "$TMP_DIR/retry-exceeded.json" assert_status "$retry_exceeded_status" "409" "retry attempts exceeded" "$TMP_DIR/retry-exceeded.json"
assert_error_code "$TMP_DIR/retry-exceeded.json" "VALID_006" "retry attempts exceeded" assert_error_code "$TMP_DIR/retry-exceeded.json" "VALID_006" "retry attempts exceeded"
INVOICE_RETRY_OK="$(create_invoice_record "$EMPLOYEE_IDENTIFIER" "MOCK_OK:$RUN_SUFFIX" "ocr_failed" "1" "$OLD_ISO")" INVOICE_RETRY_OK="$(create_invoice_record "$EMPLOYEE_IDENTIFIER" "MOCK_OK:RETRY:$RUN_SUFFIX" "ocr_failed" "1" "$OLD_ISO")"
retry_ok_status="$(retry_ocr "$EMPLOYEE_TOKEN" "$INVOICE_RETRY_OK" "$TMP_DIR/retry-ok.json")" retry_ok_status="$(retry_ocr "$EMPLOYEE_TOKEN" "$INVOICE_RETRY_OK" "$TMP_DIR/retry-ok.json")"
assert_status "$retry_ok_status" "200" "retry ok" "$TMP_DIR/retry-ok.json" assert_status "$retry_ok_status" "200" "retry ok" "$TMP_DIR/retry-ok.json"
assert_jq "$TMP_DIR/retry-ok.json" '.data.status == "pending_ocr"' "retry ok status" assert_jq "$TMP_DIR/retry-ok.json" '.data.status == "pending_ocr"' "retry ok status"
@@ -242,6 +242,7 @@ create_and_bind_invoice() {
local file_path="$TMP_DIR/${label}-${RUN_SUFFIX}-${RANDOM}.pdf" local file_path="$TMP_DIR/${label}-${RUN_SUFFIX}-${RANDOM}.pdf"
local upload_body="$TMP_DIR/${label}-upload.json" local upload_body="$TMP_DIR/${label}-upload.json"
local patch_body="$TMP_DIR/${label}-patch.json"
local bind_body="$TMP_DIR/${label}-bind.json" local bind_body="$TMP_DIR/${label}-bind.json"
printf '%%PDF-1.4\n%s\n' "$label-$RUN_SUFFIX-$(date +%s%N)-$RANDOM" > "$file_path" printf '%%PDF-1.4\n%s\n' "$label-$RUN_SUFFIX-$(date +%s%N)-$RANDOM" > "$file_path"
@@ -253,6 +254,9 @@ create_and_bind_invoice() {
local invoice_id local invoice_id
invoice_id="$(jq -er '.data.invoicePoolId' "$upload_body")" invoice_id="$(jq -er '.data.invoicePoolId' "$upload_body")"
status="$(api_json_auth PATCH "$BASE_URL/api/invoice-pool/$invoice_id" "$token" "$patch_body" '{"totalAmount":"188.80"}')"
assert_status "$status" "200" "patch invoice total amount $label" "$patch_body"
status="$(api_json_auth POST "$BASE_URL/api/invoice-pool/$invoice_id/bind" "$token" "$bind_body" "{\"reimbursementId\":\"$reimbursement_id\"}")" status="$(api_json_auth POST "$BASE_URL/api/invoice-pool/$invoice_id/bind" "$token" "$bind_body" "{\"reimbursementId\":\"$reimbursement_id\"}")"
assert_status "$status" "200" "bind invoice $label" "$bind_body" assert_status "$status" "200" "bind invoice $label" "$bind_body"
+16 -5
查看文件
@@ -21,17 +21,19 @@ if ! command -v jq >/dev/null 2>&1; then
exit 1 exit 1
fi fi
if [[ ! -f "$SAMPLE_FILE" ]]; then
echo "[ERROR] Sample file not found: $SAMPLE_FILE" >&2
exit 1
fi
TMP_DIR="$(mktemp -d)" TMP_DIR="$(mktemp -d)"
cleanup() { cleanup() {
rm -rf "$TMP_DIR" rm -rf "$TMP_DIR"
} }
trap cleanup EXIT trap cleanup EXIT
if [[ ! -f "$SAMPLE_FILE" ]]; then
REQUESTED_SAMPLE_FILE="$SAMPLE_FILE"
SAMPLE_FILE="$TMP_DIR/hyb-oa-sample-invoice.pdf"
printf '%%PDF-1.4\nhyb-oa generated smoke invoice\n' > "$SAMPLE_FILE"
echo "[WARN] Sample file not found: $REQUESTED_SAMPLE_FILE; using generated smoke fixture" >&2
fi
RUN_UPLOAD_FILE="$TMP_DIR/upload-$(basename "$SAMPLE_FILE")" RUN_UPLOAD_FILE="$TMP_DIR/upload-$(basename "$SAMPLE_FILE")"
cp "$SAMPLE_FILE" "$RUN_UPLOAD_FILE" cp "$SAMPLE_FILE" "$RUN_UPLOAD_FILE"
printf '\n%%RUN-%s\n' "$(date +%s%N)" >> "$RUN_UPLOAD_FILE" printf '\n%%RUN-%s\n' "$(date +%s%N)" >> "$RUN_UPLOAD_FILE"
@@ -104,6 +106,15 @@ upload_status="$(curl -sS -o "$upload_body" -w "%{http_code}" \
assert_status "$upload_status" "200" "upload invoice" assert_status "$upload_status" "200" "upload invoice"
INVOICE_ID="$(extract_json_field "$upload_body" '.data.invoicePoolId')" INVOICE_ID="$(extract_json_field "$upload_body" '.data.invoicePoolId')"
echo "[5.1/13] Patch invoice total amount"
patch_invoice_body="$TMP_DIR/patch-invoice.json"
patch_invoice_status="$(curl -sS -o "$patch_invoice_body" -w "%{http_code}" \
-X PATCH "$BASE_URL/api/invoice-pool/$INVOICE_ID" \
-H "authorization: Bearer $EMPLOYEE_TOKEN" \
-H "content-type: application/json" \
-d '{"totalAmount":"188.80"}')"
assert_status "$patch_invoice_status" "200" "patch invoice total amount"
echo "[6/13] Bind invoice" echo "[6/13] Bind invoice"
bind_body="$TMP_DIR/bind.json" bind_body="$TMP_DIR/bind.json"
bind_status="$(curl -sS -o "$bind_body" -w "%{http_code}" \ bind_status="$(curl -sS -o "$bind_body" -w "%{http_code}" \