From bd601a698776a2acb1d17bad172777a99be6ceaa Mon Sep 17 00:00:00 2001 From: shunli Date: Sun, 17 May 2026 23:07:34 +0800 Subject: [PATCH] =?UTF-8?q?WCFB=20=E5=A4=8D=E7=94=A8=E5=B7=A5=E4=BD=9C?= =?UTF-8?q?=E5=8F=B0=E4=BC=81=E5=BE=AE=E6=89=93=E5=8D=A1=E5=90=8C=E6=AD=A5?= =?UTF-8?q?=E5=AF=86=E9=92=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .agents/skills/hyb-oa-delivery/SKILL.md | 2 +- deploy/trialrun/.env.example | 1 + docs/trialrun/人工试运行报告.md | 4 +- docs/文档事实源状态矩阵.md | 3 +- docs/文档索引.md | 4 +- docs/海音达OA-MVP需求规格说明书.md | 4 +- docs/管理员手册.md | 7 +- docs/需求-企业微信基础设施规约.md | 4 +- docs/需求-请假考勤修正规约.md | 2 +- ...39-wecom-checkin-agent-secret-fallback.csv | 5 + .../src/services/integrations/wecomCheckin.js | 15 ++- server/tests/unit/routes/attendance.test.js | 122 ++++++++++++++++++ 12 files changed, 158 insertions(+), 15 deletions(-) create mode 100644 issues/2026-05-17_22-57-39-wecom-checkin-agent-secret-fallback.csv diff --git a/.agents/skills/hyb-oa-delivery/SKILL.md b/.agents/skills/hyb-oa-delivery/SKILL.md index 332eaf9b..288cc8f4 100644 --- a/.agents/skills/hyb-oa-delivery/SKILL.md +++ b/.agents/skills/hyb-oa-delivery/SKILL.md @@ -104,7 +104,7 @@ Final responses should include: - R6 scope defaults to reimbursement, leave, procurement, approval, Enterprise WeChat entry/notification, finance payment/export. - Do not expand to budget, ERP, AI, full travel, or complex BPM during R6; record those as follow-up Issues. - Enterprise WeChat personal app message is the formal approval notification channel after R5; mock and group bots are not acceptance evidence for R6 business notification. -- After HRWD/HCLT, ERP is not a future OA integration target, Enterprise WeChat native approval is not the formal workflow, and OA-driven Enterprise WeChat directory create/update/disable is live verified. For later HR/WeCom directory issues, first check configuration, permission, department, and data-specific causes before treating the capability as unfinished. +- After HRWD/HCLT, ERP is not a future OA integration target, Enterprise WeChat native approval is not the formal workflow, and OA-driven Enterprise WeChat directory create/update/disable is live verified. For later HR/WeCom directory issues, first check configuration, permission, department, and data-specific causes before treating the capability as unfinished. After WCFB, Enterprise WeChat check-in sync uses an authorized custom app Secret: `WECOM_CHECKIN_SECRET` is an explicit override for a dedicated check-in app, and when it is empty OA can reuse the authorized workbench app `WECOM_AGENT_SECRET`. - Approval-chain correctness can be proven by PC or automation, while Enterprise WeChat validates entry, binding,免登录, notification, deeplink, and mobile layout. - If real-device mobile validation is paused after automation passes, record the gate as `PAUSED`; do not close RS6/UI6/R6 mobile blockers until representative real-device confirmation is recorded. - Role-specific pages should use one shared OA/mobile framework and tailor summaries, entries, actions, and sensitive fields by role. Do not create unrelated UI systems for employee, approver, finance, admin, or CEO views. diff --git a/deploy/trialrun/.env.example b/deploy/trialrun/.env.example index 3e045bf2..cd118d4f 100644 --- a/deploy/trialrun/.env.example +++ b/deploy/trialrun/.env.example @@ -67,6 +67,7 @@ WECOM_OAUTH_ENABLED=false WECOM_REDIRECT_URI=https://oa.example.com/api/auth/wecom/callback WECOM_ALLOWED_RETURN_HOSTS=oa.example.com WECOM_CHECKIN_SYNC_ENABLED=false +# 企业微信打卡同步 Secret:显式配置优先;留空时复用已授权调用打卡 API 的 WECOM_AGENT_SECRET WECOM_CHECKIN_SECRET= WECOM_CONTACT_SYNC_ENABLED=false WECOM_CONTACT_SECRET= diff --git a/docs/trialrun/人工试运行报告.md b/docs/trialrun/人工试运行报告.md index b75c1bc1..5e057c3f 100644 --- a/docs/trialrun/人工试运行报告.md +++ b/docs/trialrun/人工试运行报告.md @@ -34,11 +34,12 @@ DCL7 已删除本文件中 R70-R91 的长流水历史,避免旧环境、旧开 - 2026-05-17 SYSTEM-COMPLETE-GATE 已完成系统开发完成门禁:公网 `/ready`、public bundle、live smoke `3 passed`、PDF live OCR `1 passed`,只读 flag 核验显示 `payment_batch_v1=force_on`、`erp_sync=force_off`;公网移动/企业微信 UA 矩阵 `14 passed`;结论 `GO_TO_USE`。当前首批正式能力为员工报销、PDF OCR、审批、请假、采购、通知、移动/企业微信入口、财务台账、付款批次、银行回单附件和代账交接。 - 2026-05-17 LAUNCH-OPS-V1 已固定首周正式使用运营口径:`wangshunli` 为上线拍板、管理员复核和回滚决策人,`wangzhiling` 为出纳/付款执行和代账资料准备人;首周问题登记、每日巡检、暂停条件、代账交接节奏和 UX8/可选功能冻结口径已回写现有手册。后续新增功能只按真实反馈和业务频次另建 Issue CSV。 - 2026-05-17 REQ-COMPLETE-AUDIT 已完成需求完成度复核和考勤边界校正:首批正式能力保持 `GO_TO_USE` 结论不变;企业微信打卡作为原始采集和风控底座,OA 不重造定位、人脸、Wi-Fi、排班、设备和防作弊。 -- 2026-05-17 ATTV1 已完成考勤统一入口实现并部署到公网:历史 OA 自建打卡 `attendance_v1` 关闭;OA 新增企微打卡摘要同步、补卡/外出/加班修正审批和月度考勤解释;所有审批继续统一在 OA,企业微信只负责打卡原始采集和风控。公网复检显示旧打卡 API 返回 409、统一考勤月度汇总和修正列表可读、移动/企业微信矩阵 14 passed;企微 live 同步依赖 `WECOM_CHECKIN_SECRET` 和企微打卡 API 权限,缺配置时记 `SKIP`。 +- 2026-05-17 ATTV1 已完成考勤统一入口实现并部署到公网:历史 OA 自建打卡 `attendance_v1` 关闭;OA 新增企微打卡摘要同步、补卡/外出/加班修正审批和月度考勤解释;所有审批继续统一在 OA,企业微信只负责打卡原始采集和风控。公网复检显示旧打卡 API 返回 409、统一考勤月度汇总和修正列表可读、移动/企业微信矩阵 14 passed;企微 live 同步依赖已授权调用打卡 API 的自建应用 Secret 和企微权限,`WECOM_CHECKIN_SECRET` 可显式覆盖,未配置时复用已授权工作台应用的 `WECOM_AGENT_SECRET`,缺配置时记 `SKIP`。 - 2026-05-17 LAUNCH-AUTO-OPS-V1 已完成正式启用后自动化收口脚本和证据刷新:本地 Prisma/node check/focused 单测、前端 typecheck/build、公网 readiness、PDF live OCR、统一考勤 live 检查、公网移动矩阵和企业微信 UA/渲染矩阵均通过;显式企微打卡同步检查返回 `SKIP wecom_checkin_sync_disabled`;但只读 flag 检查发现当前公网 `payment_batch_v1=force_off`,与 FIN-PROD-ENABLE 记录的正式常开口径不一致,结论为 `PAUSE_FOR_FIX`。runner 已跳过付款批次写入,未切换 flag、未部署、不接真实支付、不接 ERP。 - 2026-05-17 AUTO-REMAINING-CLOSEOUT 已完成剩余无需人工操作收口:受控恢复公网 `payment_batch_v1=force_on`,保持 `erp_sync=force_off`;复跑 `run-launch-auto-ops.sh` 通过,公网测试报销 `BX-20260517-0027` 完成审批、付款批次创建/复核、回单上传/下载、付款执行、AP 导出和审计核验;公网移动矩阵 6 passed、企业微信渲染矩阵 8 passed。显式企微打卡同步仍为 `SKIP wecom_checkin_sync_disabled`,属于外部后台配置缺口,不阻断 OA 统一考勤入口和正式运营门禁;结论 `GO_TO_OPERATE`。 - 2026-05-17 HR-WECOM-DIRECTORY-V1 已完成并部署公网:产品口径已调整为 ERP 未来不接入 OA;人事生命周期和考勤同属 HR 大域但分模块实现。OA 继续作为员工、权限、审批和审计主事实源;企业微信作为通讯录、入口、通知和打卡采集基础设施。HRWD 新增企业微信通讯录同步 flag、配置、同步记录、入职创建/资料更新/离职禁用钩子和员工管理重试入口。本地后端/前端/Playwright 通过,部署后公网 readiness、PDF live OCR、HRWD 端点 SKIP 检查和公网移动/企微矩阵通过;部署过程中发现 `seed-config` 会覆盖正式 feature flag,已修正为只创建缺失开关并复部署验证,最终 `payment_batch_v1=force_on`、`erp_sync=force_off`。 - 2026-05-17 HRWD-CONTACT-LIVE-TEST 已完成公网企业微信通讯录真实写入闭环:使用 `HRWDTEST-20260517214548` 测试员工,验证 OA 创建员工后由 onboarding 触发企微 create、资料更新触发企微 update、离职 checklist 完成后禁用 OA 账号并触发企微 disable;三次企微同步均为 `wecom_contact_synced` 且 `errcode=0`。普通员工查看/重试他人同步状态被拒绝,审计日志包含 `WECOM_DIRECTORY_CREATE/UPDATE/DISABLE`;测试成员按约定保留离职/禁用状态,不删除。 +- 2026-05-17 WCFB 已完成企微打卡同步 Secret 兼容修正的本地验证:用户确认「工作台」就是 OA 企业微信入口应用,且该应用已在企业微信打卡 API「可调用接口的应用」中授权。OA 后端调整为 `WECOM_CHECKIN_SECRET` 显式优先,未配置时复用已授权工作台应用的 `WECOM_AGENT_SECRET`,避免重复维护同一个 Secret;本地语法检查、focused 单测、文档和项目 skill 校验通过。该修正尚未部署到公网,部署后需复检企微打卡同步。 ## 关键证据索引 @@ -120,6 +121,7 @@ DCL7 已删除本文件中 R70-R91 的长流水历史,避免旧环境、旧开 | LAUNCH-AUTO-OPS-V1 正式启用后自动化收口 | 暂停,结论 `PAUSE_FOR_FIX` | `run-launch-auto-ops.sh` 报告 `artifacts/qa-flow/launch-auto-ops-20260517-175548/`;本地门禁、live readiness、PDF live OCR、考勤统一入口、公网移动矩阵 6 passed、企业微信渲染矩阵 8 passed;只读 flag 检查失败:`payment_batch_v1=force_off`,期望为正式常开 `force_on`,因此付款批次写入回归被安全跳过。 | | AUTO-REMAINING-CLOSEOUT 剩余自动化收口 | 通过,结论 `GO_TO_OPERATE` | 新增 QA-only flag 恢复脚本并受控恢复 `payment_batch_v1=force_on`;`run-launch-auto-ops.sh` 报告 `artifacts/qa-flow/launch-auto-ops-20260517-193317/`,本地门禁、live readiness、PDF live OCR、正式 flag、统一考勤、付款批次常开闭环、公网移动矩阵 6 passed、企业微信渲染矩阵 8 passed;测试报销 `BX-20260517-0027` paid,票据 archived,AP 台账 paid;企微打卡同步缺后台配置按 `SKIP wecom_checkin_sync_disabled` 记录。 | | HRWD-CONTACT-LIVE-TEST 企微通讯录公网闭环 | 通过,结论 `PASS` | 新增 QA-only runner `scripts/qa/run-hrwd-contact-live-test.mjs`;公网创建 `HRWDTEST-20260517214548` 测试员工,onboarding 触发企微 create、资料变更触发 update、offboarding checklist 触发 disable,三次同步均 `errcode=0`;OA 账号最终 archived/inactive,离职后登录 403 `AUTH_003`,普通员工同步状态权限负例和审计动作均通过;报告 `artifacts/qa-flow/hrwd-contact-live-test-20260517T134548Z/summary.json`。 | +| WCFB 企微打卡 Secret 复用 | 本地通过,待部署复检 | Issue `issues/2026-05-17_22-57-39-wecom-checkin-agent-secret-fallback.csv`;`WECOM_CHECKIN_SECRET` 显式优先,未配置时复用已授权工作台应用的 `WECOM_AGENT_SECRET`;本地 `node --check`、`attendance.test.js`、Issue CSV 校验、文档审计和 skill 校验通过。 | ## R7EP / OPS8 / QAF8 / UX8 / P2T 预检推进 diff --git a/docs/文档事实源状态矩阵.md b/docs/文档事实源状态矩阵.md index 22994d3b..0215bc76 100644 --- a/docs/文档事实源状态矩阵.md +++ b/docs/文档事实源状态矩阵.md @@ -60,6 +60,7 @@ | `AUTO-REMAINING-CLOSEOUT` | `issues/2026-05-17_19-30-44-auto-remaining-closeout.csv` | `EVIDENCE` | 剩余无需人工操作事项已收口,结论 `GO_TO_OPERATE`:公网 `payment_batch_v1` 已恢复 `force_on`,`erp_sync=force_off`;正式自动巡检报告 `artifacts/qa-flow/launch-auto-ops-20260517-193317/` 为 PASS,付款批次常开闭环、移动矩阵和企业微信渲染矩阵通过;企微打卡同步仍因外部配置 `SKIP wecom_checkin_sync_disabled` | | `HR-WECOM-DIRECTORY-V1` | `issues/2026-05-17_20-00-40-hr-wecom-directory-v1.csv` | `EVIDENCE` | 人事生命周期与企业微信通讯录同步已部署并收口:ERP 未来不接入 OA;HR 大域拆分为人事生命周期和考勤两个模块;OA 驱动企微通讯录创建/更新/禁用,失败不回滚 OA 人事动作;公网端点检查显示缺通讯录配置时重试记录 `SKIP wecom_contact_sync_disabled`,不阻断 OA 人事闭环;seed-config 已改为不覆盖已有正式 feature flag,复部署后 `payment_batch_v1=force_on`、`erp_sync=force_off` | | `HRWD-CONTACT-LIVE-TEST` | `issues/2026-05-17_21-41-43-hrwd-contact-live-test.csv` | `EVIDENCE` | 公网企微通讯录真实写入闭环已收口:`HRWDTEST-20260517214548` 测试员工完成 create/update/disable,企微 `errcode=0`,OA 员工最终 archived/inactive,登录负例、普通员工权限负例和审计动作均通过;报告 `artifacts/qa-flow/hrwd-contact-live-test-20260517T134548Z/summary.json` | +| `WCFB` | `issues/2026-05-17_22-57-39-wecom-checkin-agent-secret-fallback.csv` | `LOCAL_VERIFIED` | 企微打卡同步 Secret 兼容修正:`WECOM_CHECKIN_SECRET` 显式优先,未配置时复用已授权工作台应用的 `WECOM_AGENT_SECRET`;本地语法和 focused 单测通过,待部署后公网复检 | 旧 Issue CSV 保留为历史审计证据:不删除、不批量回写、不移动到 `docs/archive/` 或其他归档目录。新连续合同如替代旧合同,只在新合同 `Notes` 或文档索引中说明 supersedes。 @@ -173,4 +174,4 @@ - `ME6-3` 代表性企业微信真机门禁已关闭。 - MIF7-1..7 与 MIF7T 已完成本地整改、公网部署、公网匿名复检和带凭据 live smoke/OCR 复检。 -- R7E 真实扩大试运行阶段验收、P2 排序、R7E 收口、QAOPS8 上线硬化阶段收口、QAOPS9 上线硬化连续合同、PRELAUNCH 正式启用前公网预回归、CASHIER-VOUCHER-V1 财务闭环与银行回单附件、LAUNCH-DEPLOY-FINANCE 部署复检、FIN-PROD-ENABLE 正式常开、ACCOUNTING-HANDOFF-V1 代账交接 SOP、SYSTEM-COMPLETE-GATE 系统完成门禁、LAUNCH-OPS-V1 首周运营保障、REQ-COMPLETE-AUDIT 需求完成度复核、ATTV1 考勤统一入口、AUTO-REMAINING-CLOSEOUT 剩余自动化收口和 HRWD-CONTACT-LIVE-TEST 公网企微通讯录真实写入闭环均已完成,当前 P0/P1 阻断为 0;P2D 已完成决策分流,P2-ORG 只读巡检已完成且无需写修复合同。LAUNCH-AUTO-OPS-V1 发现的公网 `payment_batch_v1=force_off` 漂移已由 AUTOREM 恢复为 `force_on`,正式自动巡检结论为 `GO_TO_OPERATE`;`erp_sync=force_off`,ERP 未来不接入 OA,正式支付接口、图片 OCR 和银行流水导入继续不进入当前路线。ATTV1 已部署:历史 OA 自建打卡 `attendance_v1` 关闭,OA 补卡/外出/加班审批和月度考勤解释已纳入统一考勤主线;企微打卡 live 同步能力依赖 `WECOM_CHECKIN_SECRET` 和企微 API 权限,缺配置时记录 `SKIP`。HRWD 已完成 OA 驱动企业微信通讯录同步,并通过 `HRWDTEST-20260517214548` 真实公网 create/update/disable 验证;测试成员按审计要求保留离职/禁用状态。 +- R7E 真实扩大试运行阶段验收、P2 排序、R7E 收口、QAOPS8 上线硬化阶段收口、QAOPS9 上线硬化连续合同、PRELAUNCH 正式启用前公网预回归、CASHIER-VOUCHER-V1 财务闭环与银行回单附件、LAUNCH-DEPLOY-FINANCE 部署复检、FIN-PROD-ENABLE 正式常开、ACCOUNTING-HANDOFF-V1 代账交接 SOP、SYSTEM-COMPLETE-GATE 系统完成门禁、LAUNCH-OPS-V1 首周运营保障、REQ-COMPLETE-AUDIT 需求完成度复核、ATTV1 考勤统一入口、AUTO-REMAINING-CLOSEOUT 剩余自动化收口和 HRWD-CONTACT-LIVE-TEST 公网企微通讯录真实写入闭环均已完成,当前 P0/P1 阻断为 0;P2D 已完成决策分流,P2-ORG 只读巡检已完成且无需写修复合同。LAUNCH-AUTO-OPS-V1 发现的公网 `payment_batch_v1=force_off` 漂移已由 AUTOREM 恢复为 `force_on`,正式自动巡检结论为 `GO_TO_OPERATE`;`erp_sync=force_off`,ERP 未来不接入 OA,正式支付接口、图片 OCR 和银行流水导入继续不进入当前路线。ATTV1 已部署:历史 OA 自建打卡 `attendance_v1` 关闭,OA 补卡/外出/加班审批和月度考勤解释已纳入统一考勤主线;企微打卡 live 同步能力使用已授权调用打卡 API 的自建应用 Secret,`WECOM_CHECKIN_SECRET` 显式优先,未配置时可复用已授权工作台应用的 `WECOM_AGENT_SECRET`,缺配置时记录 `SKIP`。HRWD 已完成 OA 驱动企业微信通讯录同步,并通过 `HRWDTEST-20260517214548` 真实公网 create/update/disable 验证;测试成员按审计要求保留离职/禁用状态。 diff --git a/docs/文档索引.md b/docs/文档索引.md index 81c3b9fc..146ba2b8 100644 --- a/docs/文档索引.md +++ b/docs/文档索引.md @@ -20,7 +20,7 @@ ## 最近事实源链 -R6 -> PC6 -> PA6 -> FL6 -> AP6 -> RI6 -> LP6 -> ME6/QG6 -> DG7/MRF7 -> MIA7 -> MIF7/MIF7T(公网部署与带凭据公网复检已收口) -> DG8 -> DG9 -> R7E -> FBA -> QAOPS9/P2D -> P2-ORG -> P2-FIN-DECISION -> P2-FIN-V1 -> P2-OCR-SAMPLE -> P2Q -> P2AUTO -> P2AF -> P2-FIN-LIVE-DECISION -> P2-FIN-LIVE -> PRELAUNCH -> CASHIER-VOUCHER-V1 -> LAUNCH-DEPLOY-FINANCE -> FIN-PROD-ENABLE -> ACCOUNTING-HANDOFF-V1 -> SYSTEM-COMPLETE-GATE -> LAUNCH-OPS-V1 -> REQ-COMPLETE-AUDIT -> ATTV1 -> LAUNCH-AUTO-OPS-V1 -> AUTO-REMAINING-CLOSEOUT -> HR-WECOM-DIRECTORY-V1 -> HRWD-CONTACT-LIVE-TEST。 +R6 -> PC6 -> PA6 -> FL6 -> AP6 -> RI6 -> LP6 -> ME6/QG6 -> DG7/MRF7 -> MIA7 -> MIF7/MIF7T(公网部署与带凭据公网复检已收口) -> DG8 -> DG9 -> R7E -> FBA -> QAOPS9/P2D -> P2-ORG -> P2-FIN-DECISION -> P2-FIN-V1 -> P2-OCR-SAMPLE -> P2Q -> P2AUTO -> P2AF -> P2-FIN-LIVE-DECISION -> P2-FIN-LIVE -> PRELAUNCH -> CASHIER-VOUCHER-V1 -> LAUNCH-DEPLOY-FINANCE -> FIN-PROD-ENABLE -> ACCOUNTING-HANDOFF-V1 -> SYSTEM-COMPLETE-GATE -> LAUNCH-OPS-V1 -> REQ-COMPLETE-AUDIT -> ATTV1 -> LAUNCH-AUTO-OPS-V1 -> AUTO-REMAINING-CLOSEOUT -> HR-WECOM-DIRECTORY-V1 -> HRWD-CONTACT-LIVE-TEST -> WCFB。 - R6、PC6、PA6、FL6、AP6、RI6、LP6 已完成并部署。 - ME6 自动化、公网视觉矩阵和 `ME6-3` 代表性企业微信真机门禁已完成。 @@ -54,6 +54,7 @@ R6 -> PC6 -> PA6 -> FL6 -> AP6 -> RI6 -> LP6 -> ME6/QG6 -> DG7/MRF7 -> MIA7 -> M - AUTO-REMAINING-CLOSEOUT 是 LAUNCH-AUTO-OPS-V1 后的剩余自动化收口合同:已受控恢复公网 `payment_batch_v1=force_on` 并保持 `erp_sync=force_off`,复跑正式启用后自动巡检通过;`LAUNCHAUTO` marker 测试报销 `BX-20260517-0027` 完成付款批次、回单、AP 导出和审计闭环;公网移动矩阵 6 passed,企业微信渲染矩阵 8 passed。企微打卡 live 同步仍因外部配置记录 `SKIP wecom_checkin_sync_disabled`,不影响 OA 统一考勤入口完成态。 - HR-WECOM-DIRECTORY-V1 是 HRWD 当前合同:ERP 从冻结待办改为未来不接入 OA;人事生命周期和考勤同属 HR 大域但分模块实现;OA 为员工、权限、审批和审计主事实源,企业微信作为通讯录、入口、通知和打卡采集基础设施。新增 `hr_wecom_directory_sync_v1`、企业微信通讯录同步记录、入职创建/调岗更新/离职禁用和管理员重试;缺通讯录 Secret 或 API 权限时 OA 人事闭环照常完成,同步记录为可解释 SKIP/失败。 - HRWD-CONTACT-LIVE-TEST 已完成公网企微通讯录真实写入闭环:使用 `HRWDTEST-20260517214548` 测试员工验证 OA 创建员工后由 onboarding 触发企微成员创建、资料更新触发企微 update、离职 checklist 完成后禁用 OA 账号并触发企微 disable;同步均返回 `wecom_contact_synced`、`errcode=0`,普通员工同步记录权限负例和审计动作核验通过。测试成员按约定保留离职/禁用状态,不删除。 +- WCFB 是企微打卡同步 Secret 兼容修正合同:用户确认工作台应用已作为企微入口应用配置且已授权调用打卡 API;OA 后端调整为 `WECOM_CHECKIN_SECRET` 显式优先,未配置时复用已授权工作台应用的 `WECOM_AGENT_SECRET`,避免重复维护同一个 Secret。本地语法和考勤 focused 单测通过;公网需部署后复检企微打卡同步。 ## 当前活跃治理流 @@ -94,6 +95,7 @@ R6 -> PC6 -> PA6 -> FL6 -> AP6 -> RI6 -> LP6 -> ME6/QG6 -> DG7/MRF7 -> MIA7 -> M | `AUTO-REMAINING-CLOSEOUT` | `issues/2026-05-17_19-30-44-auto-remaining-closeout.csv` | 已收口:剩余自动化运营门禁通过,结论 `GO_TO_OPERATE` | 恢复正式 `payment_batch_v1=force_on`、复跑 `run-launch-auto-ops.sh`、付款批次常开闭环、移动/企微矩阵、企微同步 SKIP 证据;报告 `artifacts/qa-flow/launch-auto-ops-20260517-193317/` | 不部署、不改业务代码、不接真实支付、不接 ERP、不开放图片 OCR、不导入银行流水;企微打卡同步外部配置仍需后台配置 | | `HR-WECOM-DIRECTORY-V1` | `issues/2026-05-17_20-00-40-hr-wecom-directory-v1.csv` | 已收口:人事生命周期与企业微信通讯录同步闭环已部署公网 | ERP 永不接入 OA;入职/调岗/资料变更/离职驱动企微通讯录创建、更新、禁用;员工管理和详情展示同步状态与重试;部署后 live smoke、HRWD 端点 SKIP 检查、公网移动/企微矩阵通过;seed 已改为不覆盖正式 flag | 不使用企业微信原生审批、不接真实支付、不开放图片 OCR、不导入银行流水;企微通讯录真实写入依赖后台 Secret、默认部门和 API 权限 | | `HRWD-CONTACT-LIVE-TEST` | `issues/2026-05-17_21-41-43-hrwd-contact-live-test.csv` | 已收口:公网企微通讯录真实写入闭环通过 | `HRWDTEST-20260517214548` 测试员工完成 OA 创建、onboarding 企微 create、资料 update、offboarding 企微 disable、OA 禁用、登录负例、权限负例和审计核验;报告 `artifacts/qa-flow/hrwd-contact-live-test-20260517T134548Z/summary.json` | 不删除企业微信测试成员、不重置数据库、不改业务 API/schema/UI、不部署;测试成员最终保留离职/禁用状态 | +| `WCFB` | `issues/2026-05-17_22-57-39-wecom-checkin-agent-secret-fallback.csv` | 本地通过:企微打卡同步 Secret 兼容修正 | `WECOM_CHECKIN_SECRET` 显式优先;未配置时复用已授权工作台应用的 `WECOM_AGENT_SECRET`,避免重复维护同一个 Secret;本地语法、focused 单测和文档/skill 校验通过 | 不新增业务 API/schema/UI;不改变企微权限边界;部署后需公网复检打卡同步 | 旧 Issue CSV 不删除、不批量回写、不移动到归档目录。若新治理流替代旧合同,只在新 CSV 的 `Notes` 或本索引说明 supersedes,旧文件保留历史审计语义。 diff --git a/docs/海音达OA-MVP需求规格说明书.md b/docs/海音达OA-MVP需求规格说明书.md index a2555faa..eb39f1fd 100644 --- a/docs/海音达OA-MVP需求规格说明书.md +++ b/docs/海音达OA-MVP需求规格说明书.md @@ -240,7 +240,7 @@ REQAUD 复核后,ATTV1 已按“企业微信打卡、OA 审批解释”口径 | 请假影响团队状态/日历 | `DONE` | 已批准请假可进入团队状态和日历展示。 | | 企业微信入口、免登/绑定、通知 deeplink 回 OA | `DONE` | 企业微信只作为入口、身份和提醒,审批动作仍回 OA。 | | 企业微信打卡运营使用 | `DONE` | 员工继续按公司要求在企业微信打卡;这是企业微信侧能力,不代表 OA 已同步打卡 API。 | -| 企业微信打卡 API 同步到 OA | `PARTIAL` | 已新增 `attendance_unified_v1` 同步批次和打卡摘要表;有企微打卡 Secret/API 权限时可同步,缺配置时按 `SKIP` 记录,不伪造通过。 | +| 企业微信打卡 API 同步到 OA | `PARTIAL` | 已新增 `attendance_unified_v1` 同步批次和打卡摘要表;有已授权调用打卡 API 的自建应用 Secret/API 权限时可同步,缺配置时按 `SKIP` 记录,不伪造通过。`WECOM_CHECKIN_SECRET` 显式优先,未配置时复用已授权工作台应用的 `WECOM_AGENT_SECRET`。 | | 员工补卡申请审批 | `DONE` | 已新增 OA 考勤修正申请,补卡只解释企微缺卡,不向企业微信写回。 | | 外出申请审批 | `DONE` | 已新增外出修正申请和 OA 审批,用于月度考勤解释;不重造企微定位风控。 | | 加班申请审批 | `DONE` | 已新增加班修正申请和 OA 审批,记录加班事实和补偿口径。 | @@ -404,7 +404,7 @@ REQAUD 复核后,ATTV1 已按“企业微信打卡、OA 审批解释”口径 后续事项必须另立 Issue CSV,不直接进入当前 MVP: -- 企业微信打卡 live 同步配置和权限稳定化。 +- 企业微信打卡 live 同步验证和真实考勤数据口径稳定化。 - 补卡、外出、加班的真实使用反馈体验优化。 - 合同台账、合同编号、合同审批和离线档案号的细化。 - 预算控制和付款/AP 运营真实使用反馈优化;ERP 不再规划出站凭证或实时集成。 diff --git a/docs/管理员手册.md b/docs/管理员手册.md index 9be4751d..55462b67 100644 --- a/docs/管理员手册.md +++ b/docs/管理员手册.md @@ -309,7 +309,7 @@ ORDER BY e.employee_no; - 普通员工主导航不出现预算、AP、AI、完整差旅等当前首批不开放的备用模块;ERP 入口保持关闭。 ## 11. 4 周观测操作法 -1. 每周一进入 `Portal`、`审批待办` 和 `考勤` 查看近 7 天登录覆盖率、审批积压、报销/OCR 异常、请假/考勤修正审批和企业微信入口错误;企微打卡同步缺配置时记录为 `SKIP`。 +1. 每周一进入 `Portal`、`审批待办` 和 `考勤` 查看近 7 天登录覆盖率、审批积压、报销/OCR 异常、请假/考勤修正审批和企业微信入口错误;企微打卡同步缺已授权应用 Secret 或 API 权限时记录为 `SKIP`。 2. 每周五记录本周结论、关键阻塞、是否需要调整审批规则、报销规则、企业微信入口或导航。 3. 若某周出现员工未登录、报销长期停留、发票重复处理不清、审批积压或企业微信绑定异常,优先修正规则和入口,不额外维护表格作为正式记录。 4. 连续 4 周都达到预期后,再扩大到 P1 采购/费用、合同归档和项目轻管理试点。 @@ -336,7 +336,8 @@ ORDER BY e.employee_no; - `WECOM_REDIRECT_URI=https://oa.hyinda.com/api/auth/wecom/callback` - `WECOM_ALLOWED_RETURN_HOSTS=oa.hyinda.com` 5. 先让管理员用账号密码登录,在 `个人设置` 完成一次绑定,再测试企业微信工作台直达;已绑定用户从手机企业微信进入时应直接看到移动工作台。 -6. 不配置企业微信原生审批模板作为正式流程;审批提醒使用企业微信消息,审批动作回到 OA 页面完成。 +6. 若启用 OA 同步企业微信打卡数据,在企业微信打卡 API「可调用接口的应用」中授权 `海音达工作台`。OA 默认可复用 `WECOM_AGENT_SECRET`;只有单独创建考勤同步应用时才额外配置 `WECOM_CHECKIN_SECRET`。 +7. 不配置企业微信原生审批模板作为正式流程;审批提醒使用企业微信消息,审批动作回到 OA 页面完成。 ### 员工会看到的典型状态 - 未绑定:登录页会提示“当前企业微信账号尚未绑定”,回退到账号密码登录。 @@ -386,7 +387,7 @@ ORDER BY e.employee_no; - `APP_ORIGIN=https://oa.example.com` - `COOKIE_SECURE=true` - `PUBLIC_SECURITY_HARDENING_ENABLED=true` -3. 若启用企业微信入口,补齐:`WECOM_*` 与 `WECOM_ALLOWED_RETURN_HOSTS`。若启用 OA 驱动企业微信通讯录同步,还需补齐 `WECOM_CONTACT_SYNC_ENABLED=true`、`WECOM_CONTACT_SECRET`、`WECOM_CONTACT_DEFAULT_DEPARTMENT_ID`,并确认该 Secret 具备通讯录写权限。 +3. 若启用企业微信入口,补齐:`WECOM_*` 与 `WECOM_ALLOWED_RETURN_HOSTS`。若启用 OA 驱动企业微信通讯录同步,还需补齐 `WECOM_CONTACT_SYNC_ENABLED=true`、`WECOM_CONTACT_SECRET`、`WECOM_CONTACT_DEFAULT_DEPARTMENT_ID`,并确认该 Secret 具备通讯录写权限。若启用企微打卡同步,确认 `海音达工作台` 已被授权调用打卡 API;`WECOM_CHECKIN_SECRET` 留空时复用 `WECOM_AGENT_SECRET`,单独考勤同步应用才需要填写独立 Secret。 4. 将 `OPENAPI_DOCS_ENABLED` 保持为 `false`,不要在公网生产暴露文档入口。 5. 定期检查审计日志中的登录失败、Refresh 来源不匹配与涉密资料查看记录。 6. 准备独立 smoke 账号,不使用老板、管理员或真实员工日常账号跑自动化;smoke 账号的角色和直属上级应按正式审批链配置。 diff --git a/docs/需求-企业微信基础设施规约.md b/docs/需求-企业微信基础设施规约.md index ea432eac..3bebd7c9 100644 --- a/docs/需求-企业微信基础设施规约.md +++ b/docs/需求-企业微信基础设施规约.md @@ -164,7 +164,7 @@ deeplink 必须校验目标地址白名单,避免跳转到非 OA 域名或未 - 企业微信打卡用于原始采集,保留其定位、人脸识别、Wi-Fi、排班和设备能力。 - OA 首批不重造打卡定位、人脸识别、Wi-Fi、排班、设备绑定、防作弊和异常判断引擎。 -- ATTV1 新增企微打卡摘要同步、同步批次和 raw hash 留痕;缺 Secret 或 API 权限时同步批次记录 `SKIP`。 +- ATTV1 新增企微打卡摘要同步、同步批次和 raw hash 留痕;缺已授权自建应用 Secret 或 API 权限时同步批次记录 `SKIP`。若工作台应用已在企业微信打卡 API「可调用接口的应用」中授权,OA 可复用现有 `WECOM_AGENT_SECRET`;`WECOM_CHECKIN_SECRET` 仅作为单独考勤同步应用的显式覆盖。 - OA 把企业微信打卡摘要与 OA 请假、补卡、外出、加班审批结果合并,形成正式考勤解释。 - 企业微信打卡异常不直接等于最终考勤异常;最终解释必须结合 OA 审批记录。 @@ -173,7 +173,7 @@ REQAUD 复核后,ATTV1 已完成统一入口实现,企业微信打卡边界 | 项目 | 状态 | 当前口径 | | --- | --- | --- | | 企业微信打卡运营使用 | `DONE` | 员工继续按公司要求使用企业微信打卡,定位、人脸、Wi-Fi、设备和防作弊由企业微信侧承担。 | -| OA 同步企业微信打卡 API | `PARTIAL` | 同步表、批次、摘要和脱敏错误码已实现;公网真实同步取决于 `WECOM_CHECKIN_SECRET` 和企微打卡 API 权限。 | +| OA 同步企业微信打卡 API | `PARTIAL` | 同步表、批次、摘要和脱敏错误码已实现;公网真实同步取决于已授权调用打卡 API 的自建应用 Secret 和企微打卡 API 权限。默认显式 `WECOM_CHECKIN_SECRET` 优先;未配置时复用已授权工作台应用的 `WECOM_AGENT_SECRET`。 | | OA 历史 `attendance_v1` 极简打卡 | `FROZEN_CLOSED` | 该模块已从正式入口关闭;旧表保留,旧写接口返回 409,不作为企微打卡能力。 | | 正式考勤解释 | `DONE` | OA 已支持请假、补卡、外出、加班审批和月度解释;不向企业微信原生审批或补卡接口写回。 | diff --git a/docs/需求-请假考勤修正规约.md b/docs/需求-请假考勤修正规约.md index 317d129c..27f89ade 100644 --- a/docs/需求-请假考勤修正规约.md +++ b/docs/需求-请假考勤修正规约.md @@ -45,7 +45,7 @@ REQAUD 复核后,ATTV1 已完成统一入口实现,当前运行时完成度 | 请假申请、审批、撤回、编辑、移动查看 | `DONE` | 已接入统一审批中心和 PC/移动页面。 | | 请假对团队状态/日历的影响 | `DONE` | 已批准请假可作为团队状态和日历依据。 | | 企业微信打卡作为原始采集 | `DONE` | 由企业微信侧承载定位、人脸、Wi-Fi、设备和防作弊能力;OA 不重造。 | -| 企业微信打卡 API 同步 | `PARTIAL` | 已新增同步批次和企微打卡摘要表;有 `WECOM_CHECKIN_SECRET` 和企微权限时同步,缺配置时记录 `SKIP`。 | +| 企业微信打卡 API 同步 | `PARTIAL` | 已新增同步批次和企微打卡摘要表;有已授权调用打卡 API 的自建应用 Secret 和企微权限时同步,缺配置时记录 `SKIP`。`WECOM_CHECKIN_SECRET` 可作为单独考勤同步应用的显式覆盖;未配置时复用已授权工作台应用的 `WECOM_AGENT_SECRET`。 | | 员工补卡申请审批 | `DONE` | 员工可在 OA 提交补卡修正,进入统一审批;通过后用于月度考勤解释,不写回企业微信。 | | 外出申请审批 | `DONE` | 员工可在 OA 提交外出修正,记录地点摘要和事由,审批后用于月度解释。 | | 加班申请审批 | `DONE` | 员工可在 OA 提交加班修正,记录时间、原因和补偿口径,审批后用于月度解释。 | diff --git a/issues/2026-05-17_22-57-39-wecom-checkin-agent-secret-fallback.csv b/issues/2026-05-17_22-57-39-wecom-checkin-agent-secret-fallback.csv new file mode 100644 index 00000000..f6acdcec --- /dev/null +++ b/issues/2026-05-17_22-57-39-wecom-checkin-agent-secret-fallback.csv @@ -0,0 +1,5 @@ +ID,Title,Description,Acceptance,Test_Method,Tools,Dev_Status,Review1_Status,Regression_Status,Files,Dependencies,Notes +WCFB-0,合同建档,记录用户确认工作台应用 Secret 已作为企业微信入口环境变量配置且工作台已被授权调用打卡 API,Issue CSV 通过校验且范围限定为企微打卡 Secret 复用兼容,python3 /home/shunli/.codex/skills/plan/scripts/validate_issues_csv.py issues/2026-05-17_22-57-39-wecom-checkin-agent-secret-fallback.csv,plan,DONE,DONE,DONE,issues/2026-05-17_22-57-39-wecom-checkin-agent-secret-fallback.csv,[],不新增业务功能不改数据库不部署 +WCFB-1,后端 Secret 复用,企微打卡同步在 WECOM_CHECKIN_SECRET 为空时复用已配置的 WECOM_AGENT_SECRET 作为已授权工作台应用 Secret,check-live-attendance-unified 不再因仅缺 WECOM_CHECKIN_SECRET 直接 SKIP 且仍在无任何 Secret 时返回缺配置,cd server && node --check src/config.js src/services/integrations/wecomCheckin.js && npm test -- tests/unit/routes/attendance.test.js,node,DONE,DONE,DONE,server/src/config.js|server/src/services/integrations/wecomCheckin.js|server/tests/unit/routes/attendance.test.js,["WCFB-0"],回滚路径为恢复强制 WECOM_CHECKIN_SECRET 的检查 +WCFB-2,文档口径同步,更新企业微信和考勤事实源说明显式打卡 Secret 优先且已授权工作台应用可复用 WECOM_AGENT_SECRET,文档和项目 skill 不再误导为必须寻找内置打卡应用 Secret,node scripts/qa/audit-docs.mjs --check-registry && python3 /home/shunli/.codex/skills/.system/skill-creator/scripts/quick_validate.py .agents/skills/hyb-oa-delivery && rg -n 'WECOM_CHECKIN_SECRET|WECOM_AGENT_SECRET|工作台应用' docs .agents/skills/hyb-oa-delivery/SKILL.md,rg|node|skill-creator,DONE,DONE,DONE,docs/需求-企业微信基础设施规约.md|docs/需求-请假考勤修正规约.md|docs/trialrun/人工试运行报告.md|docs/海音达OA-MVP需求规格说明书.md|docs/管理员手册.md|docs/文档索引.md|docs/文档事实源状态矩阵.md|deploy/trialrun/.env.example|.agents/skills/hyb-oa-delivery/SKILL.md,["WCFB-1"],不新增综合规划文档 +WCFB-3,收口验证,完成静态检查单测和 diff 检查并记录是否需要服务器重启或公网复检,本地验证通过且给出公网下一步命令,git diff --check && git status --short,git,DONE,DONE,DONE,N/A,"[""WCFB-1"",""WCFB-2""]",公网只需重启 server 后跑企微打卡同步检查 diff --git a/server/src/services/integrations/wecomCheckin.js b/server/src/services/integrations/wecomCheckin.js index 71177425..00b689ea 100644 --- a/server/src/services/integrations/wecomCheckin.js +++ b/server/src/services/integrations/wecomCheckin.js @@ -40,6 +40,13 @@ function sanitizeCheckinApiResponse(payload) { return Object.keys(result).length ? result : null; } +function resolveCheckinSecret(config) { + return ( + toSafeString(config?.wecomCheckinSecret, 500) || + toSafeString(config?.wecomAgentSecret, 500) + ); +} + function normalizeCheckinRecord(raw, { employeeByExternalUserId, timeZone }) { const externalUserId = toSafeString(raw?.userid ?? raw?.UserId, 128); const employee = employeeByExternalUserId.get(externalUserId); @@ -82,13 +89,14 @@ async function fetchWecomCheckinData({ response: null, }; } - if (!toSafeString(config?.wecomCheckinSecret, 500)) { + const checkinSecret = resolveCheckinSecret(config); + if (!checkinSecret) { return { ok: false, skipped: true, retryable: false, code: "wecom_checkin_secret_missing", - message: "Missing WECOM_CHECKIN_SECRET", + message: "Missing WECOM_CHECKIN_SECRET or WECOM_AGENT_SECRET", records: [], response: null, }; @@ -108,7 +116,7 @@ async function fetchWecomCheckinData({ const tokenResult = await getWecomAppAccessToken({ config: { ...config, - wecomAgentSecret: config.wecomCheckinSecret, + wecomAgentSecret: checkinSecret, }, timeoutMs: config.wecomRequestTimeoutMs ?? 5000, fetchImpl, @@ -177,5 +185,6 @@ module.exports = { fetchWecomCheckinData, formatDateInTimezone, normalizeCheckinRecord, + resolveCheckinSecret, sanitizeCheckinApiResponse, }; diff --git a/server/tests/unit/routes/attendance.test.js b/server/tests/unit/routes/attendance.test.js index 583c0ecc..93234f11 100644 --- a/server/tests/unit/routes/attendance.test.js +++ b/server/tests/unit/routes/attendance.test.js @@ -6,6 +6,7 @@ const { AppError } = require("../../../src/utils/AppError"); const EMPLOYEE_ID = "11111111-1111-4111-8111-111111111111"; const MANAGER_ID = "22222222-2222-4222-8222-222222222222"; const DEPARTMENT_ID = "bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"; +const originalFetch = global.fetch; function buildEmployee(overrides = {}) { return { @@ -36,6 +37,14 @@ function buildFlag(flagKey, overrideMode = "force_on") { }; } +function buildJsonResponse(payload, overrides = {}) { + return { + ok: overrides.ok ?? true, + status: overrides.status ?? 200, + json: async () => payload, + }; +} + function buildApp(options = {}) { const app = Fastify(); app.setErrorHandler((error, request, reply) => { @@ -91,6 +100,7 @@ function buildApp(options = {}) { wecomCheckinSyncEnabled: false, wecomCheckinSecret: "", leaveHolidayCalendarEnabled: false, + ...options.config, }); app.decorate("prisma", { featureFlag: { findUnique: featureFlagFindUnique }, @@ -179,6 +189,7 @@ describe("attendance unified routes", () => { let currentApp = null; afterEach(async () => { + global.fetch = originalFetch; if (currentApp) { await currentApp.close(); currentApp = null; @@ -320,4 +331,115 @@ describe("attendance unified routes", () => { }), ); }); + + it("records WeCom sync as skipped when no authorized app secret is configured", async () => { + const { app, prisma } = buildApp({ + config: { + wecomCheckinSyncEnabled: true, + wecomCorpId: "corp-id", + wecomAgentId: "1000001", + wecomCheckinSecret: "", + wecomAgentSecret: "", + }, + }); + currentApp = app; + + const response = await app.inject({ + method: "POST", + url: "/api/attendance/wecom/sync", + payload: { + startDate: "2026-05-11", + endDate: "2026-05-12", + }, + }); + + expect(response.statusCode).toBe(200); + expect(response.json().data).toMatchObject({ + skipped: true, + code: "wecom_checkin_secret_missing", + employeeCount: 1, + boundUserCount: 1, + recordCount: 0, + }); + expect(prisma.wecomCheckinSyncRun.update).toHaveBeenCalledWith( + expect.objectContaining({ + data: expect.objectContaining({ + status: "skipped", + error_code: "wecom_checkin_secret_missing", + }), + }), + ); + }); + + it("uses the authorized workbench app secret fallback for WeCom checkin sync", async () => { + global.fetch = vi + .fn() + .mockResolvedValueOnce(buildJsonResponse({ errcode: 0, access_token: "token-from-workbench" })) + .mockResolvedValueOnce( + buildJsonResponse({ + errcode: 0, + checkindata: [ + { + userid: "wecom-user-1", + checkin_time: 1778461200, + checkin_type: "上班打卡", + exception_type: "", + source_type: "app", + result: "正常", + }, + ], + }), + ); + const { app, prisma } = buildApp({ + config: { + wecomCheckinSyncEnabled: true, + wecomCorpId: "corp-id", + wecomAgentId: "1000001", + wecomCheckinSecret: "", + wecomAgentSecret: "workbench-secret", + }, + }); + currentApp = app; + + const response = await app.inject({ + method: "POST", + url: "/api/attendance/wecom/sync", + payload: { + startDate: "2026-05-11", + endDate: "2026-05-12", + }, + }); + + expect(response.statusCode).toBe(200); + expect(response.json().data).toMatchObject({ + skipped: false, + code: "wecom_checkin_fetched", + employeeCount: 1, + boundUserCount: 1, + recordCount: 1, + }); + expect(String(global.fetch.mock.calls[0][0])).toContain("corpsecret=workbench-secret"); + expect(String(global.fetch.mock.calls[1][0])).toContain("/cgi-bin/checkin/getcheckindata"); + expect(prisma.wecomCheckinRecord.createMany).toHaveBeenCalledWith( + expect.objectContaining({ + data: [ + expect.objectContaining({ + employee_id: EMPLOYEE_ID, + external_user_id: "wecom-user-1", + checkin_type: "上班打卡", + result: "正常", + }), + ], + }), + ); + expect(prisma.wecomCheckinSyncRun.update).toHaveBeenCalledWith( + expect.objectContaining({ + data: expect.objectContaining({ + status: "success", + record_count: 1, + error_code: null, + }), + }), + ); + }); });