Files
social-app/docs/plans/2026-02-26-social-data-model-redesign.md
T

533 lines
22 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Plan: social-app 数据库数据模型重设计(支持社交/事项/自动化)
**Date:** 2026-02-26
**Author:** AI Assistant
**Status:** Draft
## Overview
本方案面向 `social-app` 的下一阶段功能升级,重设计 PostgreSQL 数据模型,统一支持用户专属 agent、好友/群组协作、待处理消息、设置、可订阅且可授权编辑的日程事项、待办联动与自动化定时任务。目标是在 FastAPI + Flutter 协作场景下提供长期稳定的数据基础,降低后续 API 演进和跨端同步复杂度。
## Requirements
### Functional
- [x] 每个用户有专属 agent,且模型可扩展到未来多 agent 能力
- [x] 用户支持好友关系、群组创建与成员管理
- [x] 用户支持 inbox/pending 待处理消息
- [x] 用户支持个性化设置(偏好/隐私/通知)
- [x] 用户支持“绑定日程的事项”,可多人订阅,且仅特定人可修改
- [x] 用户支持待办事项(可由日程事项提取,也可手动创建)
- [x] 用户支持自动化定时任务(循环触发)
### Non-Functional
- [x] 性能:核心读路径(inbox 列表、待办列表、事项列表)P95 < 150ms(单用户典型数据量)
- [x] 安全:权限以后端业务授权为准;数据库层保留 RLS 防御边界
- [x] 一致性:关键写路径(好友状态、权限变更、任务触发)使用事务保障
- [x] 可演进:支持旧表迁移、双写与灰度切换
## Technical Approach
采用“认证域(`auth.users`+ 业务域(`public.*`)”分层建模。保持 `auth.users` 作为身份主键来源,业务表统一引用 `user_id UUID -> auth.users.id`。领域边界拆分为:Identity/Profile、Social Graph、Collaboration(事项/订阅/权限)、Inbox、Todo、Automation。通过“规范化主模型 + 局部物化/冗余快照”平衡一致性与查询性能。
### Key Decisions
| Decision | Rationale |
|----------|-----------|
| 用户与 agent 采用 1:1 主约束 + 可扩展结构 | 当前满足“每用户专属 agent”,未来允许多 agent 形态演进 |
| 好友关系用单表双向规范化表示 | 避免 A-B / B-A 重复,降低去重成本 |
| 事项权限采用 ACL 表而非仅 owner | 满足“仅特定人可修改”的协作场景 |
| 待办采用单表 + JSONB 来源数组 | 一张表搞定待办,source_ids 存储关联日程事件 |
| 自动化采用 Jobs + Runs 双表 | 只支持 daily/weekly 两种循环,active/disabled 两种状态 |
| inbox 采用单表接收者视角 | 发送者 + 消息类型 + 关联业务,一表搞定待处理消息 |
## A. 设计原则与边界
### 1) 核心实体与聚合边界
- 用户聚合:`profiles`(含 settings JSONB, `user_agents`
- 社交聚合:`friendships`, `groups`, `group_members`
- 协作事项聚合:`schedule_items`, `schedule_subscriptions`
- 消息聚合:`inbox_messages`
- 待办聚合:`todos`
- 自动化聚合:`automation_jobs`, `automation_runs`
### 2) 一致性分级
- 强一致(同事务):好友关系状态迁移、群组成员角色变更、事项权限写入、定时任务抢占执行
- 最终一致:inbox 衍生、待办同步、提醒派发(允许异步补偿)
### 3) 多租户假设
- 默认假设:单租户产品(同一业务库服务所有用户),以 `user_id` 做数据隔离
- 扩展预留:各核心表可预留 `tenant_id UUID NULL`(需业务确认是否近期引入组织空间)
## B. 领域模型与关系图(文字化)
### 实体与关系
- `auth.users (1) - (1) profiles`settings 作为 JSONB 内嵌)
- `auth.users (1) - (1) user_agents`
- `auth.users (N) - (N) auth.users` 通过 `friendships`
- `auth.users (1) - (N) groups`(创建者)
- `groups (1) - (N) group_members``auth.users (1) - (N) group_members`
- `auth.users (1) - (N) schedule_items`(创建者)
- `schedule_items (1) - (N) schedule_subscriptions``auth.users (1) - (N) schedule_subscriptions`
- `auth.users (1) - (N) inbox_messages`
- `auth.users (1) - (N) todos`
- `auth.users (1) - (N) automation_jobs`
- `automation_jobs (1) - (N) automation_runs`
### 关键约束
- 唯一性:
- `user_agents.user_id` 唯一
- `friendships(user_low_id, user_high_id)` 唯一
- `group_members(group_id, user_id)` 唯一
- `schedule_subscriptions(item_id, subscriber_id)` 唯一
- 外键:统一显式 `ON DELETE` 策略(见下)
- 可空性:权限关键字段、状态字段默认 `NOT NULL`
- 删除策略:
- 用户删除:大部分 `CASCADE`(用户私有数据);跨用户协作数据优先软删
- 事项删除:对子表 `CASCADE`;待办保留历史,改 `status = 'archived'`
## C. 数据库表设计(PostgreSQL
以下为推荐主表(方案 1,规范化优先)。字段示例采用 `UUID + timestamptz + enum/text-check`
### 1) 用户与 agent
#### `profiles`(已有,建议补齐)
- PK: `id UUID` (`auth.users.id`)
- 关键字段: `username`, `avatar_url`, `bio`
- **新增 JSONB 字段**:
- `settings JSONB`(用户自定义设置,含 `preferences`, `privacy`, `notification` 三大块)
- `settings_version INTEGER DEFAULT 1`(兼容旧数据的版本字段)
- 时间字段: `created_at`, `updated_at`, `deleted_at`
- 索引:
- `INDEX(username)`(允许重名,仅用于列表查询)
- `GIN(settings)`(支持 JSONB 表达式查询)
- 表达式索引:`(settings->>'notification_enabled')`(按需,对高频查询字段单独建)
- 审计: `created_by`, `updated_by`(可等于 id
- 删除策略: 用户删除时 `CASCADE`
#### `user_agents`
- PK: `id UUID`
- 关键字段:
- `user_id UNIQUE`(每用户专属 agent
- `llm_id UUID NOT NULL`(关联绑定的 LLM 模型)
- `agent_type VARCHAR(20) NOT NULL`(枚举限制:`INTENT_RECOGNITION` | `TASK_EXECUTION` | `RESULT_REPORTING`
- `config JSONB`agent 配置参数)
- `capability_version INTEGER DEFAULT 1`
- 时间字段: `created_at`, `updated_at`, `deleted_at`
- 状态字段: `status``active|paused|migrating`
- 索引:
- `UNIQUE(user_id) WHERE deleted_at IS NULL`
- `INDEX(status)`
- `INDEX(agent_type)`
- `GIN(config)`(按需)
- 审计: `created_by`, `updated_by`
### 2) 社交关系
#### `friendships`
- PK: `id UUID`
- 关键字段:
- `user_low_id`(两者中较小的 UUID
- `user_high_id`(两者中较大的 UUID
- `initiator_id`(发起请求方的 user_id,用于追溯谁主动)
- `status`, `requested_at`, `accepted_at`, `blocked_by`
- 时间字段: `created_at`, `updated_at`
- 状态字段: `status``pending|accepted|blocked|declined|canceled`
- 约束:
- `CHECK(user_low_id < user_high_id)`(强制小值放 low,大值放 high,确保 A→B 和 B→A 是同一行)
- `CHECK(initiator_id IN (user_low_id, user_high_id))`
- `UNIQUE(user_low_id, user_high_id)`
- 索引:
- `INDEX(user_low_id, status)`
- `INDEX(user_high_id, status)`
- 部分索引 `INDEX(status) WHERE status='pending'`
- 审计: `created_by`, `updated_by`
**查询示例**
- 查询用户 A 的所有好友:`SELECT * FROM friendships WHERE user_low_id = A OR user_high_id = A`
#### `groups`
- PK: `id UUID`
- 关键字段: `name`, `description`, `visibility`, `owner_id`
- 时间字段: `created_at`, `updated_at`, `deleted_at`
- 状态字段: `status``active|archived`
- 索引: `INDEX(owner_id, status)`, `INDEX(visibility)`
- 审计: `created_by`, `updated_by`
#### `group_members`
- PK: `id UUID`
- 关键字段:
- `group_id`, `user_id`
- `role`(枚举:`creator` | `admin` | `member`
- `join_source``invited|joined`
- `invited_by`, `joined_at`
- 时间字段: `created_at`, `updated_at`, `removed_at`
- 状态字段: `status``active|muted|removed`
- 约束: `UNIQUE(group_id, user_id)`
- 索引:
- `INDEX(group_id, role, status)`
- `INDEX(user_id, status)`
- 审计: `created_by`, `updated_by`
**role 说明**
| role | 含义 | 创建事项时默认给群组的权限 |
|------|------|---------------------------|
| `creator` | 群主/创建者 | `["view", "invite", "edit"]` |
| `admin` | 管理员 | `["view", "invite"]` |
| `member` | 普通成员 | `["view"]` |
- 角色可升降:服务层变更 role 字段即可
- 角色决定了该用户在群里创建的日程事项默认授予群组的权限(见下方映射)
### 3) 用户设置(已合并至 profiles 表)
用户设置采用 JSONB 内嵌方式,渐进式扩展无需改表结构:
```json
{
"version": 1,
"preferences": {
"theme": "dark",
"language": "zh-CN",
"timezone": "Asia/Shanghai"
},
"privacy": {
"profile_visible_to": "friends",
"activity_visible_to": "friends",
"allow_friend_requests": true
},
"notification": {
"enabled": true,
"push_enabled": true,
"email_enabled": false,
"quiet_hours_start": "22:00",
"quiet_hours_end": "08:00"
}
}
```
- 扩展方式:新增字段时递增 `settings_version`,应用层做 schema 兼容
- 索引策略:对高频查询字段(如 `notification.enabled`)使用表达式索引
- 更新方式:服务层使用 JSONB merge 或字段级 UPDATE,避免读-改-写并发问题(建议用 `jsonb_set` 原子操作)
### 4) 事项与订阅/权限
#### `schedule_items`
- PK: `id UUID`
- 关键字段:
- `owner_id`
- `title`
- `description`
- `start_at`
- `end_at`
- `timezone`
- `recurrence_rule`(可选,支持循环日程)
- `source_type``manual | imported | agent_generated`
- 时间字段: `created_at`, `updated_at`
- 状态字段: `status``active | completed | canceled`
- 索引:
- `INDEX(owner_id, start_at)`
- `INDEX(status, start_at)`
- 审计: `created_by`
#### `schedule_subscriptions`
- PK: `id UUID`
- 关键字段:
- `item_id`
- `subscriber_id`
- `permission`JSONB 数组,权柄组合:`["view"]` / `["view", "edit"]` / `["view", "invite", "edit"]`
- `notify_level``all | mentions | none`
- 时间字段: `created_at`
- 状态字段: `status``active | paused | unsubscribed`
- 约束: `UNIQUE(item_id, subscriber_id)`
- 索引: `INDEX(subscriber_id, status)`, `INDEX(item_id, status)`
- 审计: `created_by`
**权柄说明**
| 权柄 | 含义 |
|------|------|
| `view` | 查看事项详情 |
| `invite` | 邀请其他人订阅此事项 |
| `edit` | 修改事项内容、管理订阅 |
- 事项 owner 默认拥有全部权柄:`["view", "invite", "edit"]`
- 权限变更使用 JSONB 原子操作:`UPDATE ... SET permission = permission || '["edit"]'::jsonb`
### 5) 待处理消息(Inbox
#### `inbox_messages`
- PK: `id UUID`
- 关键字段:
- `recipient_id`(接收者)
- `sender_id`(发送者)
- `message_type``friend_request` / `group_invitation` / `schedule_item_shared`
- `content`TEXT 或 JSONB,消息内容)
- `related_type`(关联业务类型:`friendship` / `group` / `schedule_item`
- `related_id`(关联业务 ID
- 时间字段: `created_at`, `read_at`, `acted_at`
- 状态字段: `status``pending|read|accepted|rejected|dismissed`
- 索引:
- `INDEX(recipient_id, status, created_at DESC)`
- 部分索引 `INDEX(recipient_id, created_at DESC) WHERE status='pending'`
- 审计: `created_by`
**说明**:一张表搞定,接收者视角,只关心谁发的、什么类型、关联什么业务对象。
### 6) 待办
#### `todos`
- PK: `id UUID`
- 关键字段:
- `owner_id`
- `title`
- `description`
- `due_at`
- `priority`(枚举:`low | medium | high | urgent`
- `source_ids`(JSONB 数组,关联的日程事件 ID,`[]` 表示手动创建)
- 时间字段: `created_at`, `completed_at`
- 状态字段: `status``pending | done | canceled`
- 索引:
- `INDEX(owner_id, status, due_at)`
- `INDEX(owner_id, created_at DESC)`
- 部分索引 `INDEX(owner_id, due_at) WHERE status='pending'`
- 审计: `created_by`
### 7) 自动化定时任务
#### `automation_jobs`
- PK: `id UUID`
- 关键字段:
- `owner_id`
- `name`
- `job_type`
- `target_type`
- `target_id`
- `params`JSONB
- `schedule_type`(枚举:`daily | weekly`
- `schedule_time`(时间,如 `09:00` 表示每天/每周几点执行)
- 时间字段: `created_at`, `updated_at`
- 状态字段: `status``active | disabled`
- 索引: `INDEX(owner_id, status)`
- 审计: `created_by`
#### `automation_runs`
- PK: `id UUID`
- 关键字段:
- `job_id`
- `scheduled_at`(计划执行时间)
- `started_at`
- `finished_at`
- `status``queued | running | succeeded | failed`
- `attempt`
- `error_message`
- `result`JSONB
- 时间字段: `created_at`
- 索引: `INDEX(job_id, scheduled_at DESC)`, `INDEX(status, scheduled_at)`
## D. 权限与协作模型
### 1) 事项权限落表
- 权限直接存储在 `schedule_subscriptions.permission` JSONB 数组中
- 权限决策顺序:
1. `schedule_items.owner_id` → 全部权柄 `["view", "invite", "edit"]`
2. `schedule_subscriptions` 中该用户的 `permission` 数组
3. 默认只有 `["view"]`
### 2) 群组与事项权限
- 群成员在事项中的权限通过 `schedule_subscriptions.subject_type = 'group'` 关联
- 群角色决定默认权限:
- `creator``["view", "invite", "edit"]`
- `admin``["view", "invite"]`
- `member``["view"]`
## E. 消息与待办联动
### 1) inbox 关联业务对象
- `inbox_messages.message_type` 枚举:
- `friend_request`(好友请求)
- `group_invitation`(群组邀请)
- `schedule_item_shared`(日程事项分享)
- 通过 `related_type` / `related_id` 关联业务对象
### 2) 待办来源提取
- 从事项提取待办时,将日程事件 ID 存入 `todos.source_ids` JSONB 数组
- 手动创建的待办 `source_ids = []`
- 支持多来源:同一待办可关联多个日程事项 `source_ids = [<id1>, <id2>]`
- 待办完成时无需反向更新来源事项状态(简化设计)
## F. 定时任务模型
### 1) 调度规则
- `schedule_type` 枚举:`daily`(每日) | `weekly`(每周)
- `schedule_time` 格式:`HH:MM`(如 `09:00` 表示每天/每周几点执行)
- 调度器扫描 `status='active'` 的任务,按 `schedule_type + schedule_time` 计算下次执行时间
### 2) 执行记录
- 每次执行生成 `automation_runs` 记录
- 状态流转:`queued``running``succeeded` | `failed`
- 失败重试:`attempt` 字段记录当前重试次数(需业务确认最大重试次数)
## G. 演进与迁移计划(从旧表到新模型)
### Phase 1: 基础并行建模(8-12 小时)
1. 新建核心表(不删旧表):`user_agents`, `friendships`, `groups`, `group_members`
2.`profiles` 表新增 `settings JSONB`, `settings_version` 字段
3. 建立最小外键和索引,启用 RLS deny-all 策略
4. 提供只写新表的影子接口(内部开关)
### Phase 2: 协作与联动接入(12-16 小时)
1. 新建 `schedule_items`, `schedule_subscriptions`, `inbox_messages`, `todos`, `automation_jobs`, `automation_runs`
2. 编写回填脚本(从旧事项/旧消息结构回填,若不存在则跳过)
3. 开启双写:旧接口写旧表同时写新表,记录双写差异日志
### Phase 3: 读切换与一致性校验(8-12 小时)
1. API 读路径灰度切换到新表(按用户百分比)
2. 每日对账:记录数、状态分布、关键字段哈希比对
3. 指标稳定后停止旧表写入,保留只读回滚窗口
### Phase 4: 收尾与清理(4-8 小时)
1. 下线旧读路径和旧双写逻辑
2. 保留旧表冷备份后归档/删除
3. 固化运行手册与告警阈值
### 回滚策略
- 任意阶段回滚:读切回旧表 + 关闭新表写开关
- 双写阶段故障:保留操作日志,可按时间窗重放补偿
- 最终切换前必须满足:新旧关键查询结果偏差 < 0.1%
## H. 两套方案对比
### 方案 1(推荐):规范化、可维护性优先
- 特点:按领域拆表,ACL 与来源映射独立,严格约束
- 优点:一致性好、权限边界清晰、长期演进成本低
- 缺点:联表较多,初期 API 复杂度较高
### 方案 2:开发效率优先、适度反规范化
- 特点:将部分结构合并为 JSONB(如 `permissions_snapshot`, `todo_origin`
- 优点:开发快、迁移初期改动小
- 缺点:约束弱、查询和审计困难、后续重构成本高
- 注:`user_settings` 已按此思路内嵌至 profiles,其他模块仍建议保持规范化
### 对比矩阵
| 维度 | 方案 1(规范化) | 方案 2(反规范化) |
|------|------------------|--------------------|
| 复杂度 | 中高 | 中 |
| 查询性能 | 读热点需索引与缓存优化 | 单行读取快,复杂筛选慢 |
| 写入成本 | 中(多表事务) | 低到中 |
| 扩展性 | 高 | 中低 |
| 风险 | 中(实施复杂) | 中高(数据质量和权限风险) |
### 推荐结论
- 推荐方案 1:当前业务已包含社交关系、协作权限、跨域联动和自动化调度,若选择反规范化将把复杂性转移到应用层并放大后续维护风险。方案 1 在 FastAPI + PostgreSQL 下更符合长期可维护与可审计目标。
## I. 交付物
### I-1. 可直接进入实现计划的结论性摘要(12 条)
1.`auth.users` 为身份主键,业务表统一 `user_id` 外键。
2. 引入 `user_agents`,通过 `UNIQUE(user_id)` 满足每用户专属 agent。
3. 用户设置内嵌至 `profiles.settings JSONB`,支持渐进式扩展。
4. 好友关系采用标准化双向一行模型,避免重复边。
5. 群组采用 `groups + group_members`,角色内建 creator/admin/member。
6. 事项、订阅、权限三表解耦,支持多人订阅与精细编辑授权。
7. inbox 采用单表 `inbox_messages`,接收者视角简洁设计。
8. 待办采用单表 `todos`,通过 `source_ids` JSONB 数组存储来源日程事件。
9. 自动化采用 `jobs + runs` 双表,只支持 daily/weekly 两种循环。
10. 所有关键表补齐状态机字段与审计字段,支持可观测与追责。
11. 索引以"用户维度 + 状态 + 时间"为主,兼顾移动端列表查询。
12. 迁移走"四阶段":并行建模 -> 双写回填 -> 读切换 -> 清理。
13. 通过幂等键、部分索引和事务边界保障高并发稳定性。
### I-2. 后续 API 设计所需数据契约清单
- 用户/agent
- `UserAgentDTO`: `id,user_id,llm_id,agent_type,status,capability_version,config,updated_at`
- `UserSettingsDTO`(内嵌于 Profile: `settings JSONB, settings_version`
- 好友
- `FriendshipDTO`: `id,user_a,user_b,status,initiator_id,requested_at,accepted_at`
- 状态流转:`pending -> accepted|declined|canceled|blocked`
- 群组
- `GroupDTO`: `id,name,owner_id,visibility,status`
- `GroupMemberDTO`: `group_id,user_id,role,status,joined_at`
- 事项
- `ScheduleItemDTO`: `id,owner_id,title,description,start_at,end_at,timezone,recurrence_rule,source_type,status,created_at`
- `ScheduleSubscriptionDTO`: `id,item_id,subscriber_id,permission (JSONB数组),notify_level,status,created_at`
- Inbox
- `InboxMessageDTO`: `id,recipient_id,sender_id,message_type,content,related_type,related_id,status,created_at,read_at,acted_at`
- Todo
- `TodoDTO`: `id,owner_id,title,description,due_at,priority,status,source_ids (JSONB数组),created_at,completed_at`
- 自动化
- `AutomationJobDTO`: `id,owner_id,name,job_type,target_type,target_id,params,schedule_type,schedule_time,status,created_at`
- `AutomationRunDTO`: `id,job_id,scheduled_at,started_at,finished_at,status,attempt,error_message,result`
### I-3. 最小可行迁移 DDL 清单(按优先级)
P0(身份与社交基础)
1. `ALTER TABLE profiles ADD COLUMN settings JSONB DEFAULT '{}'`
2. `ALTER TABLE profiles ADD COLUMN settings_version INTEGER DEFAULT 1`
3. `CREATE INDEX idx_profiles_settings_notification ON profiles ((settings->>'notification_enabled'))`
4. `CREATE TABLE user_agents (...)`
5. `CREATE TABLE friendships (...)`
6. `CREATE TABLE groups (...)`
7. `CREATE TABLE group_members (...)`
8. `CREATE INDEX/UNIQUE/CHECK`friendships 规范化约束)
P1(协作事项)
7. `CREATE TABLE schedule_items (...)`
8. `CREATE TABLE schedule_subscriptions (...)`
9. `CREATE INDEX`owner/status/time + subscription 查询)
P2(消息与待办)
11. `CREATE TABLE inbox_messages (...)`
12. `CREATE TABLE todos (...)`
P3(自动化)
13. `CREATE TABLE automation_jobs (...)`
14. `CREATE TABLE automation_runs (...)`
15. `CREATE INDEX idx_automation_runs_job_scheduled (...)`
P4(安全与治理)
21. 对新增 `public` 表执行 `ALTER TABLE ... ENABLE ROW LEVEL SECURITY`
22.`anon, authenticated` 创建默认 deny-all `SELECT/INSERT/UPDATE/DELETE` policy
23. 审计字段回填脚本与触发器(如需)
## Dependencies
- [ ] PostgreSQL 扩展:`pgcrypto`UUID/哈希,若已启用可跳过)
- [ ] Alembic 迁移体系(现有)
- [ ] 后端任务执行器(Celery)用于自动化触发与补偿
## Testing Strategy
- **Unit Tests:** 状态流转、权限决策、去重哈希、幂等键生成
- **Integration Tests:** 迁移升级/回滚、双写一致性、RLS policy 基线、并发抢占执行
- **E2E Tests:** 好友请求到 inbox、群组邀请处理、事项订阅变更到待办、自动化任务触发到结果回显
## Risks & Mitigations
| Risk | Impact | Likelihood | Mitigation |
|------|--------|------------|------------|
| 旧表结构未知导致回填失败 | High | Medium | 先做 schema 探测脚本,按表存在性分支回填 |
| 双写期间新旧数据不一致 | High | Medium | 引入操作日志 + 对账任务 + 幂等补偿 |
| 权限模型过复杂影响上线进度 | Medium | Medium | 先上线最小权限集(owner/user/group),后续扩展 |
| 自动化任务并发冲突 | Medium | Medium | `SKIP LOCKED` + 幂等键 + 乐观锁版本号 |
| 索引不足导致移动端列表慢 | Medium | Medium | 先覆盖 P0/P1 热路径索引,监控后增量优化 |
## Estimated Effort
| Phase | Effort |
|-------|--------|
| Phase 1 | 8-12 hours |
| Phase 2 | 12-16 hours |
| Phase 3 | 8-12 hours |
| Phase 4 | 4-8 hours |
| **Total** | **32-48 hours** |
## 需业务确认(关键不确定项)
1. ~~`profiles.username` 是否允许重名~~(已确认:允许重名,仅建普通索引)。
2. ~~group_members.role 权柄组合~~(已确认:JSONB 数组 `["view", "invite", "edit"]`)。
3. 是否近期需要"组织/团队"多租户(决定 `tenant_id` 是否立即强制)。
4. 事项是否必须绑定群组上下文(`context_group_id` 是否必需)。
5. 自动化任务失败重试上限与退避策略(固定/指数)。