实现业务附件受控入库 V1(05-28-business-attachment-governance-v1)
这个提交包含在:
@@ -17,3 +17,93 @@
|
||||
- `.trellis/tasks/` may contain PRD and acceptance context, but only in
|
||||
redacted form.
|
||||
- `scripts/qa/check-trellis-governance.mjs` must pass before commit.
|
||||
|
||||
## Scenario: Business Attachment Governance
|
||||
|
||||
### 1. Scope / Trigger
|
||||
|
||||
- Trigger: adding or changing OA business attachments for reimbursement,
|
||||
purchase, payment, contract, project or company-document workflows.
|
||||
- Applies when an attachment is stored by the OA and can later be previewed,
|
||||
downloaded, reviewed or archived.
|
||||
- Do not introduce a generic file-center or free directory upload unless a
|
||||
Trellis task explicitly reopens that architecture.
|
||||
|
||||
### 2. Signatures
|
||||
|
||||
- DB fields on business attachment models:
|
||||
`governance_status`, `sensitivity_level`, `attachment_type`,
|
||||
`reviewed_by`, `reviewed_at`, `review_note`, `archived_by`,
|
||||
`archived_at`, `archive_note`, `download_policy`.
|
||||
- Upload multipart fields may include:
|
||||
`governanceStatus`, `sensitivityLevel`, `attachmentType`, `downloadPolicy`.
|
||||
- API response fields must use camelCase:
|
||||
`governanceStatus`, `sensitivityLevel`, `attachmentType`, `downloadPolicy`,
|
||||
`reviewedBy`, `reviewedAt`, `reviewNote`, `archivedBy`, `archivedAt`,
|
||||
`archiveNote`.
|
||||
- Download APIs may accept `reason`; sensitive or reason-required attachments
|
||||
must reject downloads without a non-empty reason.
|
||||
|
||||
### 3. Contracts
|
||||
|
||||
- Default status is `approved` unless the business workflow has a stricter
|
||||
state such as `pending_review`.
|
||||
- Default sensitivity is `internal`; payment receipts and similarly sensitive
|
||||
financial proof should use `confidential`.
|
||||
- `index_only` means OA stores the index/metadata, not the original file.
|
||||
- Frontend labels are informational only; backend permission and download
|
||||
reason checks remain authoritative.
|
||||
- Audit logs for downloads must include actor, business target, attachment ID,
|
||||
attachment type, sensitivity level, governance status and sanitized reason
|
||||
when provided.
|
||||
|
||||
### 4. Validation & Error Matrix
|
||||
|
||||
- Unknown governance enum in upload -> normalize to the module default, do not
|
||||
persist arbitrary strings.
|
||||
- Sensitive or `reason_required` attachment download without reason ->
|
||||
`400 VALID_001` with `{ reason: "required" }`.
|
||||
- Missing attachment or missing stored object -> `404`.
|
||||
- User lacks business read permission -> `403 PERM_001`.
|
||||
- Download reason longer than the server limit -> trim before writing audit
|
||||
metadata.
|
||||
|
||||
### 5. Good/Base/Bad Cases
|
||||
|
||||
- Good: a bank receipt upload returns `confidential`, `receipt`,
|
||||
`reason_required`; download first asks for用途 and writes an audit log.
|
||||
- Base: a reimbursement supporting material upload defaults to `internal`,
|
||||
`supporting_material`, `standard` and remains downloadable by authorized
|
||||
participants.
|
||||
- Bad: a page hides the download button but the backend route still streams the
|
||||
file without permission or reason checks.
|
||||
- Bad: Enterprise WeChat messages or reports include original attachments
|
||||
instead of OA links and desensitized summaries.
|
||||
|
||||
### 6. Tests Required
|
||||
|
||||
- Prisma validate after schema or migration changes.
|
||||
- Backend route tests for default governance fields, custom multipart fields
|
||||
when exposed, sensitive-download reason rejection and audit `extra` metadata.
|
||||
- Frontend typecheck/build after API type changes.
|
||||
- Focused UI verification for pages that add attachment governance labels or
|
||||
download-reason interactions.
|
||||
|
||||
### 7. Wrong vs Correct
|
||||
|
||||
#### Wrong
|
||||
|
||||
```js
|
||||
return reply.send(stream);
|
||||
```
|
||||
|
||||
#### Correct
|
||||
|
||||
```js
|
||||
const reason = sanitizeDownloadReason(request.query?.reason);
|
||||
if (requiresDownloadReason(attachment) && !reason) {
|
||||
throw buildValidationError({ reason: "required" });
|
||||
}
|
||||
await prisma.auditLog.create({ data: buildDownloadAudit({ attachment, reason }) });
|
||||
return reply.send(stream);
|
||||
```
|
||||
|
||||
在新工单中引用
屏蔽一个用户