实现制度签收轻量回执(05-28-oa-policy-acknowledgement-v1)

这个提交包含在:
2026-05-28 11:48:37 +08:00
父节点 7c5b6d9578
当前提交 c80d9adbcd
共修改 16 个文件,包含 1806 行新增和 78 行删除
@@ -22,3 +22,73 @@ QA script patterns over new abstractions.
- `cd server && node --check src/routes/workflow.js`
- `cd server && ./scripts/regression-workflow-actions.sh`
- `cd server && ./scripts/regression-all.sh`
## Scenario: Portal Announcement Acknowledgement
### 1. Scope / Trigger
- Trigger: extending `announcements_v1` from read-only announcements into formal employee acknowledgement.
- Applies to `server/prisma/schema.prisma`, `server/src/routes/announcements.js`, `client/src/api/announcements.ts`, employee `/announcements`, and admin `/system/announcements`.
- Use this only for lightweight policy acknowledgement; do not treat it as electronic signature, CA signing, contract signing, or full policy CMS.
### 2. Signatures
- DB: `PortalAnnouncement` may carry `category`, `requires_acknowledgement`, `acknowledgement_statement`, `acknowledgement_due_at`.
- DB: `PortalAnnouncementAcknowledgement` stores one receipt per `(announcement_id, employee_id)` with statement/title/summary snapshots and `actor_id`.
- API: `GET /api/announcements?manage=0` returns visible announcements and the current employee's acknowledgement status.
- API: `GET /api/announcements?manage=1` requires `system.settings` and returns acknowledgement summary plus minimal pending employees.
- API: `POST /api/announcements/:id/acknowledge` accepts `{ "confirmed": true }`.
### 3. Contracts
- Required acknowledgement status values: `not_required`, `pending`, `acknowledged`, `overdue`.
- Employee response must not include other employees' acknowledgement rows or pending lists.
- Admin pending list may include only `id`, `employeeNo`, `name`, and `status`; do not include phone, email, WeCom userid, account identifiers, or sensitive profile fields.
- Acknowledgement is explicit only: opening an announcement or clicking its link must never create a receipt.
- Policy content changes after any acknowledgement require a new announcement version; do not mutate title, summary, category, required flag, or statement in place.
### 4. Validation & Error Matrix
- `announcements_v1` disabled -> `409 VALID_001`.
- policy acknowledgement without statement -> `400 VALID_001`.
- `manage=1` without `system.settings` -> `403 PERM_001`.
- acknowledge invisible/missing announcement -> `404 ANN_404`.
- acknowledge normal announcement -> `409 VALID_001`.
- acknowledge without `{ confirmed: true }` -> `400 VALID_001`.
- edit protected policy fields after acknowledgement exists -> `409 VALID_001`.
### 5. Good/Base/Bad Cases
- Good: admin creates a policy announcement, employee checks the confirmation box, server creates one receipt and one audit log.
- Base: normal announcement remains compatible and returns `not_required`.
- Bad: frontend displays another employee's receipt status on `/announcements`.
- Bad: admin updates an acknowledged policy title instead of creating a new version.
### 6. Tests Required
- Prisma validate after schema/migration changes.
- Route focused tests for required statement, permission, idempotency, normal/invisible announcement rejection, admin summary, and protected edit rejection.
- Frontend typecheck and build.
- Playwright flow for admin create -> employee acknowledge -> admin summary update when UI changes.
### 7. Wrong vs Correct
#### Wrong
```js
await prisma.portalAnnouncement.update({
where: { id },
data: { title: "制度 V2" },
});
```
#### Correct
```js
const acknowledgementCount = await prisma.portalAnnouncementAcknowledgement.count({
where: { announcement_id: id },
});
if (acknowledgementCount > 0) {
throw new AppError({ code: "VALID_001", message: "参数校验失败", statusCode: 409 });
}
```
@@ -0,0 +1,4 @@
{"file": ".trellis/spec/hyb-oa/project-governance.md", "reason": "Verify Trellis task contract and migration approval boundaries."}
{"file": ".trellis/spec/hyb-oa/backend-fastify-prisma.md", "reason": "Verify Prisma, route and backend test requirements."}
{"file": ".trellis/spec/hyb-oa/frontend-react-vite-design.md", "reason": "Verify frontend page changes follow existing OA design conventions."}
{"file": ".trellis/spec/hyb-oa/security-privacy.md", "reason": "Verify acknowledgement APIs and UI do not leak sensitive employee or policy data."}
@@ -0,0 +1,176 @@
# 制度签收轻量回执设计
## Summary
采用“公告中心扩展 + 独立回执表”的方案。制度签收公告仍是 `PortalAnnouncement`,新增必签元数据;员工签收写入新回执表。这样复用既有公告入口、权限、feature flag 和 Portal 展示,同时让正式签收回执独立留痕,不把公告阅读误认为签收。
## Data Model
建议扩展 `PortalAnnouncement`:
- `category`: `String`,默认 `general`,V1 值为 `general` / `policy`
- `requires_acknowledgement`: `Boolean`,默认 `false`
- `acknowledgement_statement`: `String?`,必签时必填
- `acknowledgement_due_at`: `DateTime?`
新增 `PortalAnnouncementAcknowledgement`:
- `id`: uuid
- `announcement_id`: uuid,关联 `PortalAnnouncement`
- `employee_id`: uuid,关联 `Employee`
- `acknowledged_at`: DateTime
- `statement_snapshot`: String
- `title_snapshot`: String
- `summary_snapshot`: String
- `actor_id`: uuid,可与 employee_id 相同,V1 不支持代签
- `created_at`: DateTime
索引/约束:
- `@@unique([announcement_id, employee_id])`
- `@@index([employee_id, acknowledged_at])`
- `@@index([announcement_id, acknowledged_at])`
## API Contract
### `GET /api/announcements?manage=0`
普通员工返回可见公告,并附带本人签收状态:
```json
{
"id": "...",
"title": "费用报销制度签收",
"summary": "请阅读并确认小团队 V1 报销规则。",
"category": "policy",
"requiresAcknowledgement": true,
"acknowledgementStatement": "我已阅读并理解该制度。",
"acknowledgementDueAt": "2026-06-01T00:00:00.000Z",
"acknowledgement": {
"status": "pending",
"acknowledgedAt": null,
"overdue": false
}
}
```
普通员工只看到自己的签收状态,不看到他人列表。
### `GET /api/announcements?manage=1`
仅 `system.settings` 可用。返回管理视图,包含统计:
```json
{
"acknowledgementSummary": {
"required": true,
"totalActiveEmployees": 6,
"acknowledgedCount": 4,
"pendingCount": 2,
"overdueCount": 1
},
"pendingEmployees": [
{ "id": "...", "employeeNo": "HYD***", "name": "员工A" }
]
}
```
未签员工列表只返回员工号、姓名和状态,不返回手机号、邮箱、身份证、企业微信 userid 或账号凭据。
### `POST /api/announcements/:id/acknowledge`
普通员工签收本人回执。
请求:
```json
{
"confirmed": true
}
```
校验:
- `announcements_v1` 必须启用。
- 公告必须 active 且在可见时间窗口内。
- 公告 `requires_acknowledgement=true`。
- `confirmed=true`。
- 员工必须 active。
- 重复签收返回已有回执,不新建第二条。
响应:
```json
{
"item": {
"announcementId": "...",
"status": "acknowledged",
"acknowledgedAt": "..."
}
}
```
审计:
- `PORTAL_ANNOUNCEMENT_ACKNOWLEDGED`
## Frontend Design
使用 `frontend-design` 裁决后的方向:**安静、正式、清晰的 OA 合规面板**。
员工公告页:
- 保持公告列表,不做大营销布局。
- 必签公告使用更明确的状态列/徽标:待签收、已签收、已逾期。
- 卡片内展示签收声明、截止日期和主按钮。
- 签收按钮前必须有 checkbox:“我已阅读并理解该制度内容和资料留存边界”。
- 已签后展示签收时间,不再显示可重复操作按钮。
管理员公告页:
- 左侧新建公告表单增加:
- 公告类型:普通 / 制度签收
- 签收声明
- 截止日期
- 右侧列表显示签收统计,不把未签名单做成大表格;V1 用紧凑列表即可。
- 页面继续使用现有 `card-std` / PC 操作页模式,不新建独立视觉系统。
## Permissions
- 创建/编辑/归档/查看统计:`system.settings`
- 查看公告:authenticated user
- 签收:authenticated active employee,仅本人
- 不允许管理员代签,不允许普通员工查看他人签收。
## Migration Notes
- 新增字段全部带默认或 nullable,避免影响既有公告数据。
- 新增回执表只记录未来签收,不导入历史纸质/电子签收。
- 生产 migration 只在用户明确批准部署时应用;本任务本地验证不代表公网已启用。
## Tests
后端 focused:
- 创建普通公告仍兼容。
- 创建必签公告缺声明 -> 400。
- 员工获取公告时看到本人 pending 状态。
- 员工签收必签公告 -> 201/200,审计写入。
- 重复签收 -> 幂等。
- 普通公告签收 -> 409。
- 管理员 manage 查询看到统计和未签列表。
前端 E2E:
- 管理员创建必签公告。
- 员工进入公告页,勾选确认,点击签收。
- 员工刷新后状态为已签收。
- 管理员看到已签/待签统计变化。
## Reviewer Need
本任务涉及 Prisma migration、权限和审计。实施后需要做只读 review,重点看:
- 普通用户是否可能看到他人签收。
- 管理接口是否泄露手机号、邮箱、账号、企业微信 userid。
- 已签回执是否会被公告编辑破坏语义。
@@ -0,0 +1,5 @@
{"file": ".trellis/spec/hyb-oa/project-governance.md", "reason": "Trellis task, approvals, commit and migration governance for a non-trivial feature."}
{"file": ".trellis/spec/hyb-oa/backend-fastify-prisma.md", "reason": "Backend route, Prisma migration and validation requirements."}
{"file": ".trellis/spec/hyb-oa/frontend-react-vite-design.md", "reason": "Frontend design authority and PC operation page conventions."}
{"file": ".trellis/spec/hyb-oa/security-privacy.md", "reason": "Sensitive data exclusions for policy materials and employee acknowledgement reports."}
{"file": ".trellis/spec/hyb-oa/docs-and-evidence.md", "reason": "Evidence and report boundaries for task artifacts."}
@@ -0,0 +1,95 @@
# 制度签收轻量回执
## Goal
在不引入完整电子签章、复杂制度库或合同级法务系统的前提下,为 OA 公告中心增加“正式制度签收回执”能力。管理员可发布需要员工确认的制度公告,员工必须通过明确勾选和点击完成签收,系统保存签收时间、签收人、声明快照和审计记录。目标是补齐 OALAUNCH1 数据治理门禁中的“公告阅读不等于正式签收”缺口。
## Scope
- 基于现有 `announcements_v1` 和公告中心扩展,不新增独立制度 CMS。
- 支持管理员创建/编辑公告时标记“需要签收”,填写签收声明和签收截止日期。
- 支持员工在公告页查看待签收状态,并通过明确确认动作完成本人签收。
- 支持管理员在公告管理页查看签收总数、已签、未签、逾期统计和未签员工脱敏/最小必要列表。
- 后端保存签收回执、签收声明快照和审计日志。
- 覆盖后端 focused 测试、前端 typecheck/build 和至少一个公告签收 E2E/组件级流程验证。
## What I Already Know
- 当前 `PortalAnnouncement` 只有公告标题、摘要、链接、启停、优先级和时间窗口,没有签收模型。
- `EmployeeChecklistTask.task_type = policy_ack` 已存在,但它只服务入职 checklist,不适合作为公司级制度签收台账。
- `docs/管理员手册.md` 和 `docs/员工快速上手.md` 已明确:公告阅读不等于正式签收,制度签收是正式启用前的数据治理缺口。
- 公告模块已有 feature flag `announcements_v1`、后台路由 `/api/announcements`、员工页 `/announcements` 和管理页 `/system/announcements`。
- 前端视觉裁决以 `frontend-design` 为准;本任务应保持 OA 操作界面简洁、密集、可扫读,不做营销页或重视觉重构。
## Requirements
- 管理员创建公告时可选择:
- 普通公告:无需签收,行为保持兼容。
- 制度签收公告:需要签收,必须有签收声明,建议可填截止日期。
- 员工看到制度签收公告时必须有明确状态:
- 待签收
- 已签收
- 已逾期未签
- 员工签收必须是显式动作:勾选确认声明后点击签收按钮;不能仅因打开页面或点击链接视为签收。
- 同一员工对同一公告只能形成一条有效回执;重复点击应幂等返回已签收状态。
- 签收回执至少记录:公告、员工、签收时间、签收声明快照、公告标题/摘要快照、操作者、创建时间。
- 管理员可以查看签收概览和未签员工列表;普通员工不能查看他人签收状态。
- 公告正文和链接仍按现有敏感资料边界处理:不上传涉密合同正文、银行资料、完整路径、账号凭据或敏感报价。
- 更新已签收的制度公告不自动重置签收;如制度内容变化,应新建一条新版本公告,避免历史回执语义混乱。
## Acceptance Criteria
- [x] 后端模型和迁移支持公告签收回执,Prisma validate 通过。
- [x] `GET /api/announcements` 对普通员工返回本人签收状态,不泄露他人信息。
- [x] `POST /api/announcements/:id/acknowledge` 支持本人签收,校验 feature flag、公告可见、需要签收和确认声明。
- [x] 管理员管理接口能看到签收统计和未签员工最小必要信息。
- [x] 前端员工公告页能完成待签收 -> 已签收流程,按钮状态、防重复和逾期提示清楚。
- [x] 前端公告管理页能创建必签公告并查看签收概览。
- [x] 后端 focused 测试覆盖签收权限、幂等、不可签普通公告、不可签不可见公告。
- [x] 前端 typecheck/build 通过,签收流程 E2E 或等价自动化通过。
- [x] 不部署、不改公网配置、不批量写真实签收数据;公网启用需另行批准。
## Out Of Scope
- 不做电子签章、CA、手写签名或法律级电子合同。
- 不做复杂制度库、制度版本全文管理、在线红线审阅或知识库问答。
- 不做按部门/角色定向签收;V1 默认面向所有 active 员工。
- 不做代签、补签证明上传、批量导入历史纸质签收。
- 不通过企业微信原生审批承载制度签收;企业微信只可作为后续提醒入口。
- 不把公司制度原文、合同正文、银行资料、工资资料、完整本地路径或敏感附件导入仓库或 Trellis。
## Test Plan
- Backend:
- `cd server && npm run prisma:validate`
- `cd server && node --check src/routes/announcements.js`
- 增加并运行公告签收 focused 单测或现有路由测试。
- Frontend:
- `cd client && npm run typecheck`
- `cd client && npm run build`
- 增加/运行公告签收 E2E:管理员创建必签公告,员工签收,管理员看到统计。
- Governance:
- `node scripts/qa/check-trellis-governance.mjs`
- `git diff --check`
## Rollout / Rollback
- V1 受现有 `announcements_v1` 控制;不新增部署 env。
- 本地/测试通过后再决定是否部署公网。
- 回滚方式:关闭 `announcements_v1` 或隐藏公告入口;已保存回执作为审计记录保留,不自动删除。
## Open Questions
- 建议 V1 默认“所有 active 员工都需要签收”,暂不做定向范围。是否批准按这个范围实现?
## Technical Notes
- 参考文件:
- `server/prisma/schema.prisma`
- `server/src/routes/announcements.js`
- `client/src/api/announcements.ts`
- `client/src/pages/AnnouncementsPage.tsx`
- `client/src/pages/PortalAnnouncementsPage.tsx`
- `.trellis/spec/hyb-oa/frontend-react-vite-design.md`
- `.trellis/spec/hyb-oa/backend-fastify-prisma.md`
- `.trellis/spec/hyb-oa/security-privacy.md`
@@ -0,0 +1,34 @@
{
"id": "oa-policy-acknowledgement-v1",
"name": "oa-policy-acknowledgement-v1",
"title": "制度签收轻量回执",
"description": "在现有公告中心上增加轻量制度签收回执,支持管理员发布必签制度公告、员工显式签收、管理员查看签收统计和审计留痕。",
"status": "in_progress",
"dev_type": "feature",
"scope": "server-client-prisma-announcements-policy-acknowledgement",
"package": "hyb-oa",
"priority": "P2",
"creator": "shunli",
"assignee": "shunli",
"createdAt": "2026-05-28",
"completedAt": null,
"branch": null,
"base_branch": "main",
"worktree_path": null,
"commit": null,
"pr_url": null,
"subtasks": [],
"children": [],
"parent": null,
"relatedFiles": [
"server/prisma/schema.prisma",
"server/src/routes/announcements.js",
"client/src/api/announcements.ts",
"client/src/pages/AnnouncementsPage.tsx",
"client/src/pages/PortalAnnouncementsPage.tsx",
"docs/管理员手册.md",
"docs/员工快速上手.md"
],
"notes": "",
"meta": {}
}