diff --git a/apps/lib/core/api/api_exception.dart b/apps/lib/core/api/api_exception.dart index 01a8ea0..a31fbeb 100644 --- a/apps/lib/core/api/api_exception.dart +++ b/apps/lib/core/api/api_exception.dart @@ -40,9 +40,6 @@ abstract class ApiException implements Exception { } static String _localizeError(String detail, int? statusCode) { - if (statusCode == 401) { - return '邮箱或密码错误'; - } if (statusCode == 403) { return '没有权限执行此操作'; } @@ -55,9 +52,6 @@ abstract class ApiException implements Exception { if (statusCode != null && statusCode >= 500) { return '服务器错误,请稍后再试'; } - if (detail.contains('credentials') || detail.contains('password')) { - return '邮箱或密码错误'; - } return detail; } } diff --git a/apps/lib/features/auth/data/auth_api.dart b/apps/lib/features/auth/data/auth_api.dart index e0fe99f..7d15d03 100644 --- a/apps/lib/features/auth/data/auth_api.dart +++ b/apps/lib/features/auth/data/auth_api.dart @@ -25,12 +25,12 @@ class AuthApi { return AuthResponse.fromJson(response.data); } - Future signupResend(SignupResendRequest request) async { + Future signupResend(SignupResendRequest request) async { final response = await _client.post( '$_prefix/signup/resend', data: request.toJson(), ); - return SignupStartResponse.fromJson(response.data); + return SignupResendResponse.fromJson(response.data); } Future login(LoginRequest request) async { diff --git a/apps/lib/features/auth/data/auth_repository.dart b/apps/lib/features/auth/data/auth_repository.dart index 20f1cda..2bf372e 100644 --- a/apps/lib/features/auth/data/auth_repository.dart +++ b/apps/lib/features/auth/data/auth_repository.dart @@ -5,7 +5,7 @@ import 'package:social_app/features/auth/data/models/auth_response.dart'; abstract class AuthRepository { Future signupStart(SignupStartRequest request); Future signupVerify(SignupVerifyRequest request); - Future signupResend(SignupResendRequest request); + Future signupResend(SignupResendRequest request); Future login(LoginRequest request); Future refresh(String refreshToken); Future logout(); diff --git a/apps/lib/features/auth/data/auth_repository_impl.dart b/apps/lib/features/auth/data/auth_repository_impl.dart index 3860ab7..23bf352 100644 --- a/apps/lib/features/auth/data/auth_repository_impl.dart +++ b/apps/lib/features/auth/data/auth_repository_impl.dart @@ -29,7 +29,7 @@ class AuthRepositoryImpl implements AuthRepository { } @override - Future signupResend(SignupResendRequest request) { + Future signupResend(SignupResendRequest request) { return _api.signupResend(request); } diff --git a/apps/lib/features/auth/data/models/auth_response.dart b/apps/lib/features/auth/data/models/auth_response.dart index b2dd3c5..71e9a76 100644 --- a/apps/lib/features/auth/data/models/auth_response.dart +++ b/apps/lib/features/auth/data/models/auth_response.dart @@ -54,3 +54,13 @@ class SignupStartResponse { ); } } + +class SignupResendResponse { + final String message; + + const SignupResendResponse({required this.message}); + + factory SignupResendResponse.fromJson(Map json) { + return SignupResendResponse(message: json['message'] as String); + } +} diff --git a/apps/test/features/auth/presentation/cubits/register_cubit_test.dart b/apps/test/features/auth/presentation/cubits/register_cubit_test.dart index 5647b18..c7a73db 100644 --- a/apps/test/features/auth/presentation/cubits/register_cubit_test.dart +++ b/apps/test/features/auth/presentation/cubits/register_cubit_test.dart @@ -170,13 +170,9 @@ void main() { build: () => cubit, seed: () => RegisterState(pendingEmail: 'test@example.com'), setUp: () { - when(() => mockRepository.signupResend(any())).thenAnswer( - (_) async => SignupStartResponse( - status: 'ok', - email: 'test@example.com', - message: 'Code sent', - ), - ); + when( + () => mockRepository.signupResend(any()), + ).thenAnswer((_) async => SignupResendResponse(message: 'Code sent')); }, act: (c) => c.resendCode(), expect: () => [ diff --git a/docs/plans/2026-02-26-social-data-model-redesign.md b/docs/plans/2026-02-26-social-data-model-redesign.md index 84e8fbd..624b854 100644 --- a/docs/plans/2026-02-26-social-data-model-redesign.md +++ b/docs/plans/2026-02-26-social-data-model-redesign.md @@ -36,19 +36,19 @@ | 用户与 agent 采用 1:1 主约束 + 可扩展结构 | 当前满足“每用户专属 agent”,未来允许多 agent 形态演进 | | 好友关系用单表双向规范化表示 | 避免 A-B / B-A 重复,降低去重成本 | | 事项权限采用 ACL 表而非仅 owner | 满足“仅特定人可修改”的协作场景 | -| 待办采用主表 + 源映射表 | 支持从事项提取、手动创建、去重与追踪来源 | -| 自动化调度采用 `rrule + cron + interval` 三选一 | 同时覆盖日历型循环和工程型间隔任务 | -| inbox 采用事件聚合模型 | 将好友请求/群组邀请/事项变更统一进入待处理中心 | +| 待办采用单表 + JSONB 来源数组 | 一张表搞定待办,source_ids 存储关联日程事件 | +| 自动化采用 Jobs + Runs 双表 | 只支持 daily/weekly 两种循环,active/disabled 两种状态 | +| inbox 采用单表接收者视角 | 发送者 + 消息类型 + 关联业务,一表搞定待处理消息 | ## A. 设计原则与边界 ### 1) 核心实体与聚合边界 - 用户聚合:`profiles`(含 settings JSONB), `user_agents` - 社交聚合:`friendships`, `groups`, `group_members` -- 协作事项聚合:`schedule_items`, `schedule_item_subscriptions`, `schedule_item_permissions` -- 消息聚合:`inbox_events`, `inbox_receipts` -- 待办聚合:`todos`, `todo_sources` -- 自动化聚合:`automation_jobs`, `automation_schedules`, `automation_runs` +- 协作事项聚合:`schedule_items`, `schedule_subscriptions` +- 消息聚合:`inbox_messages` +- 待办聚合:`todos` +- 自动化聚合:`automation_jobs`, `automation_runs` ### 2) 一致性分级 - 强一致(同事务):好友关系状态迁移、群组成员角色变更、事项权限写入、定时任务抢占执行 @@ -67,13 +67,10 @@ - `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_item_subscriptions`,`auth.users (1) - (N) schedule_item_subscriptions` -- `schedule_items (1) - (N) schedule_item_permissions`,`auth.users (1) - (N) schedule_item_permissions` -- `inbox_events (1) - (N) inbox_receipts`,`auth.users (1) - (N) inbox_receipts` +- `schedule_items (1) - (N) schedule_subscriptions`,`auth.users (1) - (N) schedule_subscriptions` +- `auth.users (1) - (N) inbox_messages` - `auth.users (1) - (N) todos` -- `todos (1) - (N) todo_sources`(一条待办可有多个来源记录,默认 1 条) - `auth.users (1) - (N) automation_jobs` -- `automation_jobs (1) - (N) automation_schedules` - `automation_jobs (1) - (N) automation_runs` ### 关键约束 @@ -81,15 +78,12 @@ - `user_agents.user_id` 唯一 - `friendships(user_low_id, user_high_id)` 唯一 - `group_members(group_id, user_id)` 唯一 - - `schedule_item_subscriptions(item_id, subscriber_id)` 唯一 - - `schedule_item_permissions(item_id, subject_type, subject_id, permission)` 唯一 - - `todo_sources(source_type, source_id, owner_id)` 唯一(防止重复抽取) - - `automation_runs(job_id, idempotency_key)` 唯一 + - `schedule_subscriptions(item_id, subscriber_id)` 唯一 - 外键:统一显式 `ON DELETE` 策略(见下) -- 可空性:权限关键字段、状态字段、外部幂等键默认 `NOT NULL` +- 可空性:权限关键字段、状态字段默认 `NOT NULL` - 删除策略: - 用户删除:大部分 `CASCADE`(用户私有数据);跨用户协作数据优先软删 - - 事项删除:对子表 `CASCADE`;待办来源保留历史可改 `SET NULL` + 软删 + - 事项删除:对子表 `CASCADE`;待办保留历史,改 `status = 'archived'` ## C. 数据库表设计(PostgreSQL) @@ -164,27 +158,26 @@ - PK: `id UUID` - 关键字段: - `group_id`, `user_id` - - `role`(JSONB 数组,权柄组合,如 `["view"]` / `["view", "invite"]` / `["view", "invite", "edit"]`) + - `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, status)` + - `INDEX(group_id, role, status)` - `INDEX(user_id, status)` - - GIN 索引支持权柄查询:`INDEX group_members_role USING GIN(role)` - 审计: `created_by`, `updated_by` -**权柄说明**: -| 权柄 | 含义 | -|------|------| -| `view` | 查看群组信息、成员列表、聊天记录 | -| `invite` | 邀请新成员入群 | -| `edit` | 修改群组信息、管理成员(禁言/移除) | +**role 说明**: +| role | 含义 | 创建事项时默认给群组的权限 | +|------|------|---------------------------| +| `creator` | 群主/创建者 | `["view", "invite", "edit"]` | +| `admin` | 管理员 | `["view", "invite"]` | +| `member` | 普通成员 | `["view"]` | -- 群主(创建者)默认拥有全部权柄:`["view", "invite", "edit"]` -- 权柄可动态变更:服务层使用 `jsonb ||` 或 `jsonb -` 原子操作增减权柄 +- 角色可升降:服务层变更 role 字段即可 +- 角色决定了该用户在群里创建的日程事项默认授予群组的权限(见下方映射) ### 3) 用户设置(已合并至 profiles 表) @@ -222,184 +215,157 @@ #### `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`, `deleted_at` -- 状态字段: `status`(`running|completed|archived`) + - `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)` - - `INDEX(updated_at DESC)` -- 审计: `created_by`, `updated_by` +- 审计: `created_by` -#### `schedule_item_subscriptions` +#### `schedule_subscriptions` - PK: `id UUID` -- 关键字段: `item_id`, `subscriber_id`, `notify_level`, `subscription_source` -- 时间字段: `created_at`, `updated_at`, `unsubscribed_at` -- 状态字段: `status`(`active|paused|unsubscribed`) +- 关键字段: + - `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`, `updated_by` +- 审计: `created_by` -#### `schedule_item_permissions` -- PK: `id UUID` -- 关键字段: `item_id`, `subject_type`(`user|group|friend_circle`), `subject_id`, `permission` -- 时间字段: `created_at`, `updated_at`, `expires_at` -- 状态字段: `status`(`active|revoked|expired`) -- 约束: - - `UNIQUE(item_id, subject_type, subject_id, permission)` - - `permission` 建议枚举:`view|comment|edit|manage` -- 索引: - - `INDEX(item_id, permission, status)` - - `INDEX(subject_type, subject_id, status)` -- 审计: `granted_by`, `updated_by` +**权柄说明**: +| 权柄 | 含义 | +|------|------| +| `view` | 查看事项详情 | +| `invite` | 邀请其他人订阅此事项 | +| `edit` | 修改事项内容、管理订阅 | + +- 事项 owner 默认拥有全部权柄:`["view", "invite", "edit"]` +- 权限变更使用 JSONB 原子操作:`UPDATE ... SET permission = permission || '["edit"]'::jsonb` ### 5) 待处理消息(Inbox) -#### `inbox_events` +#### `inbox_messages` - PK: `id UUID` -- 关键字段: `event_type`, `actor_id`, `object_type`, `object_id`, `payload_jsonb`, `dedupe_key` -- 时间字段: `created_at`, `updated_at` -- 状态字段: `status`(`open|resolved|canceled`) -- 约束: `UNIQUE(dedupe_key)` -- 索引: `INDEX(event_type, created_at DESC)`, `GIN(payload_jsonb)` -- 审计: `created_by`, `updated_by` - -#### `inbox_receipts` -- PK: `id UUID` -- 关键字段: `event_id`, `recipient_id`, `inbox_state`, `action_required`, `acted_at` -- 时间字段: `created_at`, `updated_at`, `read_at` -- 状态字段: `inbox_state`(`pending|read|accepted|rejected|dismissed|expired`) -- 约束: `UNIQUE(event_id, recipient_id)` +- 关键字段: + - `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, inbox_state, created_at DESC)` - - 部分索引 `INDEX(recipient_id, created_at DESC) WHERE inbox_state='pending'` -- 审计: `created_by`, `updated_by` + - `INDEX(recipient_id, status, created_at DESC)` + - 部分索引 `INDEX(recipient_id, created_at DESC) WHERE status='pending'` +- 审计: `created_by` -### 6) 待办与来源映射 +**说明**:一张表搞定,接收者视角,只关心谁发的、什么类型、关联什么业务对象。 + +### 6) 待办 #### `todos` - PK: `id UUID` -- 关键字段: `owner_id`, `title`, `description`, `due_at`, `priority`, `origin_type`, `source_hash` -- 时间字段: `created_at`, `updated_at`, `completed_at`, `deleted_at` -- 状态字段: `status`(`pending|in_progress|done|canceled|archived`) +- 关键字段: + - `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, updated_at DESC)` - - 部分索引 `INDEX(owner_id, due_at) WHERE status IN ('pending','in_progress')` -- 审计: `created_by`, `updated_by` - -#### `todo_sources` -- PK: `id UUID` -- 关键字段: `todo_id`, `owner_id`, `source_type`(`schedule_item|manual|inbox_event|automation`), `source_id`, `extracted_at`, `sync_mode` -- 时间字段: `created_at`, `updated_at` -- 状态字段: `status`(`linked|unlinked|stale`) -- 约束: `UNIQUE(owner_id, source_type, source_id)`(去重关键) -- 索引: - - `INDEX(todo_id)` - - `INDEX(owner_id, source_type, status)` -- 审计: `created_by`, `updated_by` + - `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` -- 时间字段: `created_at`, `updated_at`, `deleted_at` -- 状态字段: `status`(`active|paused|disabled`) -- 索引: `INDEX(owner_id, status)`, `INDEX(target_type, target_id)` -- 审计: `created_by`, `updated_by` - -#### `automation_schedules` -- PK: `id UUID` -- 关键字段: - - `job_id UNIQUE` - - `schedule_type`(`cron|rrule|interval`) - - `cron_expr` / `rrule_text` / `interval_seconds`(三选一) - - `timezone`, `start_at`, `end_at`, `next_run_at` +- 关键字段: + - `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|paused|expired|invalid`) -- 约束: - - `CHECK` 保证三种表达互斥且至少一项有效 -- 索引: - - 部分索引 `INDEX(next_run_at) WHERE status='active'` - - `INDEX(job_id, status)` -- 审计: `created_by`, `updated_by` +- 状态字段: `status`(`active | disabled`) +- 索引: `INDEX(owner_id, status)` +- 审计: `created_by` #### `automation_runs` - PK: `id UUID` -- 关键字段: `job_id`, `scheduled_for`, `started_at`, `finished_at`, `attempt`, `max_retries`, `idempotency_key`, `worker_id`, `result_jsonb`, `error_code` -- 时间字段: `created_at`, `updated_at` -- 状态字段: `status`(`queued|running|succeeded|failed|canceled|dead_letter`) -- 约束: - - `UNIQUE(job_id, idempotency_key)` - - `CHECK(attempt <= max_retries + 1)` -- 索引: - - `INDEX(job_id, scheduled_for DESC)` - - `INDEX(status, scheduled_for)` - - 部分索引 `INDEX(status, updated_at) WHERE status IN ('queued','running')` -- 审计: `created_by`, `updated_by` +- 关键字段: + - `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) 事项编辑权限落表 +### 1) 事项权限落表 +- 权限直接存储在 `schedule_subscriptions.permission` JSONB 数组中 - 权限决策顺序: - 1. `schedule_items.owner_id`(天然 `manage`) - 2. `schedule_item_permissions` 针对 `subject_type=user` 的显式授权 - 3. 用户所在群组在 `subject_type=group` 的授权 - 4. 默认无编辑权限 -- 建议在服务层计算“有效权限”,可落缓存字段 `effective_permission`(可选) + 1. `schedule_items.owner_id` → 全部权柄 `["view", "invite", "edit"]` + 2. `schedule_subscriptions` 中该用户的 `permission` 数组 + 3. 默认只有 `["view"]` -### 2) 群组角色与事项关系 -- `group_members.role`:JSONB 数组,权柄组合 `["view"]` / `["view", "invite"]` / `["view", "invite", "edit"]` -- 群主(创建者)默认拥有全部权柄 -- 权柄与事项权限映射: - - 群成员 `view` → 事项 `view` - - 群成员 `invite` → 隐含 `view` - - 群成员 `edit` → 事项 `edit|manage` -- 若事项绑定群组上下文,可增 `schedule_items.context_group_id NULL` +### 2) 群组与事项权限 +- 群成员在事项中的权限通过 `schedule_subscriptions.subject_type = 'group'` 关联 +- 群角色决定默认权限: + - `creator` → `["view", "invite", "edit"]` + - `admin` → `["view", "invite"]` + - `member` → `["view"]` ## E. 消息与待办联动 -### 1) inbox 关联来源 -- `inbox_events.event_type` 建议枚举: - - `friend_request` - - `group_invitation` - - `group_role_changed` - - `schedule_item_changed` - - `schedule_item_permission_granted` - - `automation_run_failed` -- 通过 `object_type/object_id` 直接关联业务对象 +### 1) inbox 关联业务对象 +- `inbox_messages.message_type` 枚举: + - `friend_request`(好友请求) + - `group_invitation`(群组邀请) + - `schedule_item_shared`(日程事项分享) +- 通过 `related_type` / `related_id` 关联业务对象 -### 2) 待办提取与防循环 -- 从事项提取待办: - - 先计算 `source_hash = sha256(owner_id + source_type + source_id + title + due_at_bucket)` - - 查 `todo_sources(owner_id, source_type, source_id)` 唯一约束防重复 -- 防循环同步: - - `todo_sources.sync_mode`:`one_way|two_way` - - 当来源为 `schedule_item` 且 `two_way` 时,写回需携带 `sync_trace_id` - - 同一 `sync_trace_id` 在同一对象链路只消费一次(应用层幂等) +### 2) 待办来源提取 +- 从事项提取待办时,将日程事件 ID 存入 `todos.source_ids` JSONB 数组 +- 手动创建的待办 `source_ids = []` +- 支持多来源:同一待办可关联多个日程事项 `source_ids = [, ]` +- 待办完成时无需反向更新来源事项状态(简化设计) ## F. 定时任务模型 -### 1) 循环表达建议 -- 推荐优先级: - - 用户型日历任务:`rrule` - - 运维/工程任务:`cron` - - 简单轮询:`interval_seconds` -- 数据层统一在 `automation_schedules`,并通过 `schedule_type` 区分 +### 1) 调度规则 +- `schedule_type` 枚举:`daily`(每日) | `weekly`(每周) +- `schedule_time` 格式:`HH:MM`(如 `09:00` 表示每天/每周几点执行) +- 调度器扫描 `status='active'` 的任务,按 `schedule_type + schedule_time` 计算下次执行时间 -### 2) 触发记录与重试 -- 任务调度器按 `next_run_at` 扫描活跃计划,插入 `automation_runs(status='queued')` -- 执行器抢占:`SELECT ... FOR UPDATE SKIP LOCKED` -- 重试策略:`attempt` 递增,失败后按退避策略更新下一次 `scheduled_for` - -### 3) 幂等与并发冲突 -- 幂等键:`idempotency_key = sha256(job_id + scheduled_for + logical_partition)` -- 冲突处理:唯一约束冲突时视为重复投递,直接忽略或合并结果 -- 并发安全:`automation_jobs.version`(乐观锁)可选 +### 2) 执行记录 +- 每次执行生成 `automation_runs` 记录 +- 状态流转:`queued` → `running` → `succeeded` | `failed` +- 失败重试:`attempt` 字段记录当前重试次数(需业务确认最大重试次数) ## G. 演进与迁移计划(从旧表到新模型) @@ -410,7 +376,7 @@ 4. 提供只写新表的影子接口(内部开关) ### Phase 2: 协作与联动接入(12-16 小时) -1. 新建 `schedule_items*`, `inbox_*`, `todos*`, `automation_*` +1. 新建 `schedule_items`, `schedule_subscriptions`, `inbox_messages`, `todos`, `automation_jobs`, `automation_runs` 2. 编写回填脚本(从旧事项/旧消息结构回填,若不存在则跳过) 3. 开启双写:旧接口写旧表同时写新表,记录双写差异日志 @@ -462,15 +428,15 @@ 2. 引入 `user_agents`,通过 `UNIQUE(user_id)` 满足每用户专属 agent。 3. 用户设置内嵌至 `profiles.settings JSONB`,支持渐进式扩展。 4. 好友关系采用标准化双向一行模型,避免重复边。 -4. 群组采用 `groups + group_members`,角色内建 owner/admin/member。 -5. 事项、订阅、权限三表解耦,支持多人订阅与精细编辑授权。 -6. inbox 采用 `events + receipts`,统一承载待处理动作。 -7. 待办采用 `todos + todo_sources`,实现来源追踪与去重。 -8. 自动化采用 `jobs + schedules + runs`,支持 cron/rrule/interval。 -9. 所有关键表补齐状态机字段与审计字段,支持可观测与追责。 -10. 索引以“用户维度 + 状态 + 时间”为主,兼顾移动端列表查询。 -11. 迁移走“四阶段”:并行建模 -> 双写回填 -> 读切换 -> 清理。 -12. 通过幂等键、部分索引和事务边界保障高并发稳定性。 +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 设计所需数据契约清单 @@ -484,19 +450,15 @@ - `GroupDTO`: `id,name,owner_id,visibility,status` - `GroupMemberDTO`: `group_id,user_id,role,status,joined_at` - 事项 - - `ScheduleItemDTO`: `id,owner_id,title,start_at,end_at,status,timezone,recurrence_rule` - - `ScheduleSubscriptionDTO`: `item_id,subscriber_id,status,notify_level` - - `SchedulePermissionDTO`: `item_id,subject_type,subject_id,permission,status` + - `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 - - `InboxEventDTO`: `id,event_type,object_type,object_id,payload,status,created_at` - - `InboxReceiptDTO`: `event_id,recipient_id,inbox_state,action_required,read_at,acted_at` + - `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,due_at,status,priority,origin_type` - - `TodoSourceDTO`: `todo_id,source_type,source_id,sync_mode,status` + - `TodoDTO`: `id,owner_id,title,description,due_at,priority,status,source_ids (JSONB数组),created_at,completed_at` - 自动化 - - `AutomationJobDTO`: `id,owner_id,job_type,target_type,target_id,status` - - `AutomationScheduleDTO`: `job_id,schedule_type,cron_expr,rrule_text,interval_seconds,next_run_at,status` - - `AutomationRunDTO`: `id,job_id,scheduled_for,status,attempt,idempotency_key,error_code` + - `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 清单(按优先级) @@ -512,23 +474,17 @@ P0(身份与社交基础) P1(协作事项) 7. `CREATE TABLE schedule_items (...)` -8. `CREATE TABLE schedule_item_subscriptions (...)` -9. `CREATE TABLE schedule_item_permissions (...)` -10. `CREATE INDEX`(owner/status/time + permission 查询) +8. `CREATE TABLE schedule_subscriptions (...)` +9. `CREATE INDEX`(owner/status/time + subscription 查询) P2(消息与待办) -11. `CREATE TABLE inbox_events (...)` -12. `CREATE TABLE inbox_receipts (...)` -13. `CREATE TABLE todos (...)` -14. `CREATE TABLE todo_sources (...)` -15. `CREATE UNIQUE INDEX uq_todo_sources_owner_source (...)` +11. `CREATE TABLE inbox_messages (...)` +12. `CREATE TABLE todos (...)` P3(自动化) -16. `CREATE TABLE automation_jobs (...)` -17. `CREATE TABLE automation_schedules (...)` -18. `CREATE TABLE automation_runs (...)` -19. `CREATE UNIQUE INDEX uq_automation_runs_job_idempotency (...)` -20. `CREATE INDEX idx_automation_schedules_next_run_active (...)` +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` @@ -571,7 +527,6 @@ P4(安全与治理) 1. ~~`profiles.username` 是否允许重名~~(已确认:允许重名,仅建普通索引)。 2. ~~group_members.role 权柄组合~~(已确认:JSONB 数组 `["view", "invite", "edit"]`)。 -3. 是否近期需要“组织/团队”多租户(决定 `tenant_id` 是否立即强制)。 +3. 是否近期需要"组织/团队"多租户(决定 `tenant_id` 是否立即强制)。 4. 事项是否必须绑定群组上下文(`context_group_id` 是否必需)。 -5. 待办与事项同步是否默认双向(推荐默认单向,降低循环风险)。 -6. 自动化任务失败重试上限与退避策略(固定/指数)。 +5. 自动化任务失败重试上限与退避策略(固定/指数)。