473 lines
11 KiB
Markdown
473 lines
11 KiB
Markdown
# 04 数据模型
|
||||
|
|
|
|||
|
|
## 1. 通用字段规范
|
|||
|
|
|
|||
|
|
核心表统一包含:
|
|||
|
|
|
|||
|
|
- id: bigint/uuid(二选一后固定;建议 bigint 内部主键 + public_id 随机ID)
|
|||
|
|
- public_id: varchar,外部暴露,禁止连续可枚举
|
|||
|
|
- created_at
|
|||
|
|
- updated_at
|
|||
|
|
- created_by(后台业务表适用)
|
|||
|
|
- updated_by
|
|||
|
|
- deleted_at(需要软删除的表)
|
|||
|
|
|
|||
|
|
时间统一数据库 UTC 存储,展示按 Asia/Shanghai。
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 2. 核心实体
|
|||
|
|
|
|||
|
|
### users
|
|||
|
|
|
|||
|
|
微信/后台统一用户主体,避免重复身份系统。
|
|||
|
|
|
|||
|
|
- id
|
|||
|
|
- public_id
|
|||
|
|
- status
|
|||
|
|
- user_type
|
|||
|
|
- mobile(加密/脱敏策略)
|
|||
|
|
- nickname
|
|||
|
|
- avatar_url
|
|||
|
|
- last_login_at
|
|||
|
|
|
|||
|
|
### wechat_identities
|
|||
|
|
|
|||
|
|
- user_id
|
|||
|
|
- openid_ciphertext(AES-256-GCM)
|
|||
|
|
- openid_hash(HMAC-SHA-256,仅用于等值查询)
|
|||
|
|
- unionid_ciphertext nullable
|
|||
|
|
- unionid_hash nullable
|
|||
|
|
- appid
|
|||
|
|
- created_at
|
|||
|
|
唯一索引 `(appid, openid_hash)`;日志和 API 均不得返回 openid/unionid。
|
|||
|
|
|
|||
|
|
### staff_profiles
|
|||
|
|
|
|||
|
|
- user_id
|
|||
|
|
- employee_no nullable
|
|||
|
|
- display_name
|
|||
|
|
- department
|
|||
|
|
- status
|
|||
|
|
|
|||
|
|
工作人员显示名不是服务对象敏感档案字段;严禁把其扩展为手机号、证件号或详细住址。服务对象称呼仍按下文要求加密。
|
|||
|
|
|
|||
|
|
### roles / permissions / user_roles / role_permissions
|
|||
|
|
|
|||
|
|
标准 RBAC。Phase 1 预置 `VOLUNTEER`、`STAFF`、`PROJECT_MANAGER`、`ADMIN`、
|
|||
|
|
`SUPER_ADMIN`,权限码由 shared contracts 统一定义。角色替换必须在事务中写审计,并禁止移除最后一名有效超级管理员。
|
|||
|
|
|
|||
|
|
### admin_credentials
|
|||
|
|
|
|||
|
|
- user_id(唯一)
|
|||
|
|
- username(唯一)
|
|||
|
|
- password_hash(Argon2id)
|
|||
|
|
- failed_login_count
|
|||
|
|
- locked_until nullable
|
|||
|
|
- password_changed_at
|
|||
|
|
|
|||
|
|
### auth_sessions
|
|||
|
|
|
|||
|
|
- public_id(随机 UUID)
|
|||
|
|
- user_id
|
|||
|
|
- channel(WECHAT/ADMIN)
|
|||
|
|
- token_hash(仅保存带 pepper 的 HMAC,不保存明文 token)
|
|||
|
|
- expires_at
|
|||
|
|
- last_used_at nullable
|
|||
|
|
- revoked_at nullable
|
|||
|
|
|
|||
|
|
同一用户、同一渠道重新登录时撤销旧会话;过期、撤销或非 ACTIVE 用户的会话均无效。
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 3. beneficiary_profiles
|
|||
|
|
|
|||
|
|
服务对象档案:
|
|||
|
|
|
|||
|
|
- id
|
|||
|
|
- public_id
|
|||
|
|
- display_name_encrypted / display_name_hash
|
|||
|
|
- gender nullable
|
|||
|
|
- birth_year nullable
|
|||
|
|
- mobile_encrypted / mobile_hash nullable
|
|||
|
|
- region_code
|
|||
|
|
- address_detail_encrypted nullable
|
|||
|
|
- primary_category
|
|||
|
|
- key_care_flag
|
|||
|
|
- is_minor
|
|||
|
|
- status
|
|||
|
|
- notes_encrypted nullable
|
|||
|
|
- created_by
|
|||
|
|
- timestamps
|
|||
|
|
|
|||
|
|
不要默认收集完整身份证。
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 4. beneficiary_contacts
|
|||
|
|
|
|||
|
|
- beneficiary_id
|
|||
|
|
- public_id
|
|||
|
|
- name_encrypted
|
|||
|
|
- relation
|
|||
|
|
- mobile_encrypted / mobile_hash
|
|||
|
|
- is_guardian
|
|||
|
|
- is_emergency_contact
|
|||
|
|
- consent_record_id nullable
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 5. beneficiary_tags / tags
|
|||
|
|
|
|||
|
|
标签:
|
|||
|
|
|
|||
|
|
- 独居
|
|||
|
|
- 高龄
|
|||
|
|
- 行动不便
|
|||
|
|
- 定期探访
|
|||
|
|
- 困境儿童
|
|||
|
|
等。
|
|||
|
|
|
|||
|
|
标签只能帮助服务,不应成为歧视性画像。
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 6. service_applications
|
|||
|
|
|
|||
|
|
- id
|
|||
|
|
- public_id(随机 UUID,仅此字段对外暴露)
|
|||
|
|
- application_no(随机业务编号)
|
|||
|
|
- applicant_user_id
|
|||
|
|
- beneficiary_id nullable
|
|||
|
|
- consent_id
|
|||
|
|
- source_follow_up_task_id nullable(继续帮扶来源,唯一)
|
|||
|
|
- parent_application_id nullable(原申请)
|
|||
|
|
- client_request_id
|
|||
|
|
- request_fingerprint(域隔离 HMAC-SHA-256)
|
|||
|
|
- apply_mode
|
|||
|
|
- service_category
|
|||
|
|
- beneficiary_name_encrypted(AES-256-GCM)
|
|||
|
|
- contact_mobile_encrypted(AES-256-GCM)
|
|||
|
|
- contact_mobile_hash(域隔离 HMAC-SHA-256,仅用于等值查询)
|
|||
|
|
- region_code
|
|||
|
|
- description_encrypted(AES-256-GCM)
|
|||
|
|
- preferred_contact_time nullable
|
|||
|
|
- needs_home_visit nullable
|
|||
|
|
- mobility_difficulty nullable
|
|||
|
|
- status
|
|||
|
|
- source
|
|||
|
|
- submitted_at
|
|||
|
|
- accepted_at nullable
|
|||
|
|
- closed_at nullable
|
|||
|
|
- timestamps
|
|||
|
|
|
|||
|
|
首次申请在 Phase 2 创建时不自动建立 beneficiary 档案;Phase 3 经人工核验后关联。Phase 4 的继续帮扶申请保留 `parent_application_id` 和 `source_follow_up_task_id`,并创建新的独立 consent,不修改或复用原申请授权。
|
|||
|
|
|
|||
|
|
索引:
|
|||
|
|
|
|||
|
|
- application_no unique
|
|||
|
|
- `(applicant_user_id, client_request_id)` unique,保证数据库级幂等
|
|||
|
|
- applicant_user_id + submitted_at
|
|||
|
|
- status + submitted_at
|
|||
|
|
- service_category + submitted_at
|
|||
|
|
- contact_mobile_hash
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 7. service_application_status_history
|
|||
|
|
|
|||
|
|
- application_id
|
|||
|
|
- from_status
|
|||
|
|
- to_status
|
|||
|
|
- operator_user_id
|
|||
|
|
- reason_code
|
|||
|
|
- created_at
|
|||
|
|
|
|||
|
|
不可更新,只追加。
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 8. service_cases
|
|||
|
|
|
|||
|
|
- id
|
|||
|
|
- public_id
|
|||
|
|
- case_no
|
|||
|
|
- application_id
|
|||
|
|
- accept_request_id / accept_request_fingerprint(接单幂等)
|
|||
|
|
- beneficiary_id
|
|||
|
|
- owner_staff_id
|
|||
|
|
- category
|
|||
|
|
- priority
|
|||
|
|
- status
|
|||
|
|
- planned_start_at nullable
|
|||
|
|
- planned_end_at nullable
|
|||
|
|
- service_location_encrypted nullable
|
|||
|
|
- summary_encrypted
|
|||
|
|
- result_summary_encrypted nullable
|
|||
|
|
- follow_up_plan_encrypted nullable
|
|||
|
|
- completed_at nullable
|
|||
|
|
- complete_request_id / complete_request_fingerprint nullable(完成服务幂等)
|
|||
|
|
- timestamps
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 9. case_assignees
|
|||
|
|
|
|||
|
|
支持多人协作:
|
|||
|
|
|
|||
|
|
- case_id
|
|||
|
|
- user_id
|
|||
|
|
- assignee_type STAFF/VOLUNTEER
|
|||
|
|
- role
|
|||
|
|
- assigned_at
|
|||
|
|
- removed_at nullable
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 10. service_records
|
|||
|
|
|
|||
|
|
- id
|
|||
|
|
- public_id
|
|||
|
|
- case_id
|
|||
|
|
- beneficiary_id
|
|||
|
|
- service_start_at
|
|||
|
|
- service_end_at nullable
|
|||
|
|
- service_type
|
|||
|
|
- client_request_id / request_fingerprint(同一创建者范围内幂等)
|
|||
|
|
- content_encrypted
|
|||
|
|
- result_encrypted
|
|||
|
|
- follow_up_suggestion_encrypted nullable
|
|||
|
|
- beneficiary_confirm_status nullable
|
|||
|
|
- created_by
|
|||
|
|
- timestamps
|
|||
|
|
|
|||
|
|
`service_record_revisions` 在修改前保存加密快照、原 revision 和修改人;更新使用 `expectedRevision` 乐观锁,防止覆盖并支持审计追溯。
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 11. follow_up_tasks
|
|||
|
|
|
|||
|
|
- id
|
|||
|
|
- public_id / follow_up_no(均不可预测)
|
|||
|
|
- beneficiary_id
|
|||
|
|
- case_id nullable
|
|||
|
|
- owner_staff_id
|
|||
|
|
- due_at
|
|||
|
|
- follow_up_type(SERVICE_COMPLETION / KEY_CARE / MANUAL)
|
|||
|
|
- status(PENDING / COMPLETED / CANCELLED)
|
|||
|
|
- source(AUTO_SERVICE_COMPLETION / STAFF_MANUAL / KEY_CARE_PLAN)
|
|||
|
|
- reason_encrypted
|
|||
|
|
- result_code nullable
|
|||
|
|
- result_note_encrypted nullable
|
|||
|
|
- create_request_id / create_request_fingerprint
|
|||
|
|
- complete_request_id / complete_request_fingerprint nullable
|
|||
|
|
- completed_at nullable
|
|||
|
|
- created_by / updated_by / timestamps
|
|||
|
|
|
|||
|
|
自动回访与服务完成在同一事务创建。人工回访以 `(created_by, create_request_id)` 唯一;完成请求保存指纹并安全重放。
|
|||
|
|
|
|||
|
|
## 12. follow_up_status_history
|
|||
|
|
|
|||
|
|
- follow_up_task_id
|
|||
|
|
- from_status nullable
|
|||
|
|
- to_status
|
|||
|
|
- operator_user_id
|
|||
|
|
- reason_code
|
|||
|
|
- created_at
|
|||
|
|
|
|||
|
|
只追加,不更新。继续帮扶申请通过 `service_applications.source_follow_up_task_id` 回指来源,原申请历史不覆盖。
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 13. volunteers / volunteer_skills
|
|||
|
|
|
|||
|
|
volunteers:
|
|||
|
|
|
|||
|
|
- user_id
|
|||
|
|
- real_name
|
|||
|
|
- mobile
|
|||
|
|
- region_code
|
|||
|
|
- availability_note
|
|||
|
|
- status
|
|||
|
|
- approved_by nullable
|
|||
|
|
- approved_at nullable
|
|||
|
|
|
|||
|
|
skills:
|
|||
|
|
|
|||
|
|
- volunteer_id
|
|||
|
|
- skill_code
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 14. projects
|
|||
|
|
|
|||
|
|
- id
|
|||
|
|
- public_id
|
|||
|
|
- project_no
|
|||
|
|
- client_request_id / request_fingerprint
|
|||
|
|
- name
|
|||
|
|
- category
|
|||
|
|
- summary
|
|||
|
|
- content
|
|||
|
|
- cover_file_id
|
|||
|
|
- manager_user_id
|
|||
|
|
- start_date
|
|||
|
|
- end_date
|
|||
|
|
- target_people nullable
|
|||
|
|
- status
|
|||
|
|
- is_public
|
|||
|
|
- sort_order
|
|||
|
|
- timestamps
|
|||
|
|
|
|||
|
|
`(created_by_id, client_request_id)` 唯一;`project_status_history` 只追加记录创建、发布和完成。`service_cases.project_id` 可空,用于把既有工单归集到项目但不改变工单事实。
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 15. activities
|
|||
|
|
|
|||
|
|
- id
|
|||
|
|
- public_id
|
|||
|
|
- activity_no
|
|||
|
|
- client_request_id / request_fingerprint
|
|||
|
|
- project_id nullable
|
|||
|
|
- title
|
|||
|
|
- summary
|
|||
|
|
- content
|
|||
|
|
- cover_file_id
|
|||
|
|
- start_at
|
|||
|
|
- end_at
|
|||
|
|
- registration_deadline
|
|||
|
|
- location_text
|
|||
|
|
- capacity nullable
|
|||
|
|
- status
|
|||
|
|
- published_at nullable
|
|||
|
|
- timestamps
|
|||
|
|
|
|||
|
|
`(created_by_id, client_request_id)` 唯一;`activity_status_history` 只追加记录活动状态变化。
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 16. activity_registrations
|
|||
|
|
|
|||
|
|
- activity_id
|
|||
|
|
- user_id
|
|||
|
|
- volunteer_id
|
|||
|
|
- public_id / client_request_id / request_fingerprint
|
|||
|
|
- status
|
|||
|
|
- check_in_status
|
|||
|
|
- registered_at
|
|||
|
|
- reviewed_by / reviewed_at / review_note_encrypted nullable
|
|||
|
|
- checked_in_at nullable
|
|||
|
|
- checked_in_by nullable
|
|||
|
|
- service_minutes nullable
|
|||
|
|
- service_minutes_recorded_at / service_minutes_recorded_by nullable
|
|||
|
|
|
|||
|
|
唯一 `(activity_id,user_id)` 防重复报名。
|
|||
|
|
`activity_registration_history` 只追加保存报名审核、取消、签到和服务时长变更前后事实,并以全局请求 UUID 唯一约束支持安全重放。
|
|||
|
|
|
|||
|
|
志愿者资料的姓名、手机号、可参与时间和审核备注加密;手机号与姓名仅保存域隔离 HMAC 查询值。`volunteer_status_history` 只追加,并与志愿者角色授予/撤销事务一致。
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 17. content_posts
|
|||
|
|
|
|||
|
|
- id
|
|||
|
|
- public_id
|
|||
|
|
- type
|
|||
|
|
- title
|
|||
|
|
- summary
|
|||
|
|
- content
|
|||
|
|
- cover_file_id nullable
|
|||
|
|
- status DRAFT/REVIEW/PUBLISHED/ARCHIVED
|
|||
|
|
- author_user_id
|
|||
|
|
- reviewer_user_id nullable
|
|||
|
|
- published_at nullable
|
|||
|
|
- timestamps
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 18. files
|
|||
|
|
|
|||
|
|
只保存元数据:
|
|||
|
|
|
|||
|
|
- id
|
|||
|
|
- public_id
|
|||
|
|
- storage_provider
|
|||
|
|
- bucket
|
|||
|
|
- object_key
|
|||
|
|
- original_filename_encrypted
|
|||
|
|
- mime_type
|
|||
|
|
- size
|
|||
|
|
- sha256
|
|||
|
|
- business_type
|
|||
|
|
- uploader_user_id
|
|||
|
|
- status
|
|||
|
|
- created_at
|
|||
|
|
|
|||
|
|
`service_record_files` 关联服务记录与已确认文件。对象 key 使用随机值,上传确认时复核大小、MIME 和可选 SHA-256;不匹配文件进入隔离状态。
|
|||
|
|
|
|||
|
|
Phase 5 允许 `files.case_id` 为空,并通过 `business_type=PROJECT_COVER/ACTIVITY_COVER` 关联公开封面;封面仍经同一私有对象存储 Adapter 生成短时读取地址,不保存永久公网 URL。
|
|||
|
|
|
|||
|
|
前端不得直接拼接永久公网 URL。
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 19. consents
|
|||
|
|
|
|||
|
|
- id
|
|||
|
|
- public_id
|
|||
|
|
- user_id nullable
|
|||
|
|
- beneficiary_id nullable
|
|||
|
|
- consent_type
|
|||
|
|
- version
|
|||
|
|
- granted_by
|
|||
|
|
- granted_at
|
|||
|
|
- revoked_at nullable
|
|||
|
|
- evidence_json(仅保存必要证明)
|
|||
|
|
- ip_hash nullable
|
|||
|
|
- created_at
|
|||
|
|
|
|||
|
|
Phase 2 每次申请显式要求 `privacyConsent=true`,记录隐私同意版本、授权主体、授权时间、最小证据和 IP HMAC;不得在证据中重复保存申请敏感明文。
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 20. audit_logs
|
|||
|
|
|
|||
|
|
只追加:
|
|||
|
|
|
|||
|
|
- id
|
|||
|
|
- request_id
|
|||
|
|
- actor_user_id
|
|||
|
|
- actor_role
|
|||
|
|
- action
|
|||
|
|
- resource_type
|
|||
|
|
- resource_public_id
|
|||
|
|
- result
|
|||
|
|
- metadata_json(不得含敏感明文)
|
|||
|
|
- ip_hash
|
|||
|
|
- user_agent_summary
|
|||
|
|
- created_at
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 21. notifications / notification_deliveries
|
|||
|
|
|
|||
|
|
通知实体与发送记录分开,支持重试和追踪。
|
|||
|
|
|
|||
|
|
Phase 6 已落地 `content_posts`、只追加的 `content_post_status_history`、`notifications`、`notification_preferences` 和 `notification_deliveries`。消息以 `(user_id,event_key)` 唯一防重,微信投递以 `(notification_id,channel)` 唯一,失败最多重试五次后进入 `DEAD`。
|
|||
|
|
|
|||
|
|
Phase 7 不建立可人工修改的统计快照;九项 V1 指标直接从申请、工单、服务记录、服务对象、志愿者、活动和报名服务分钟事实计算。
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 22. 数据库规则
|
|||
|
|
|
|||
|
|
1. 必须使用 FK 或由 ORM/服务保证等价完整性;禁止无约束“凭感觉关联”。
|
|||
|
|
2. 唯一业务编号必须 unique。
|
|||
|
|
3. 常查字段建组合索引。
|
|||
|
|
4. 禁止对大文本字段滥建索引。
|
|||
|
|
5. 金额如未来出现用 decimal,禁止 float。
|
|||
|
|
6. 所有枚举在 contracts 中有单一事实来源。
|
|||
|
|
7. migration 不允许 destructive change 直接上线。
|
|||
|
|
8. 删除列采用“停用 → 迁移 → 验证 → 后续版本删除”。
|