实现制度签收轻量回执(05-28-oa-policy-acknowledgement-v1)
这个提交包含在:
@@ -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": {}
|
||||
}
|
||||
在新工单中引用
屏蔽一个用户