chore: commit remaining workspace updates
include AGENTS guidance updates, plan doc replacements, and utility script changes left in working tree
This commit is contained in:
@@ -103,6 +103,25 @@ Use `schemas / repository / service` pattern:
|
||||
- Raw SQL (policies, triggers, functions) → `op.execute()`
|
||||
- Migrations must be reversible; no reliance on generated IDs
|
||||
|
||||
### Enum Storage Convention
|
||||
**Store enum names (strings), not integer values.**
|
||||
|
||||
- Use `VARCHAR(20)` + `CHECK` constraint in database
|
||||
- Use Python `Enum` class with `str` base in code
|
||||
- Benefits: debugging readability, easy to add new values without data migration, ORM-friendly
|
||||
|
||||
```python
|
||||
# Correct
|
||||
class AgentType(str, Enum):
|
||||
INTENT_RECOGNITION = "INTENT_RECOGNITION"
|
||||
TASK_EXECUTION = "TASK_EXECUTION"
|
||||
RESULT_REPORTING = "RESULT_REPORTING"
|
||||
|
||||
# Migration
|
||||
ALTER TABLE user_agents ADD CONSTRAINT chk_agent_type
|
||||
CHECK (agent_type IN ('INTENT_RECOGNITION', 'TASK_EXECUTION', 'RESULT_REPORTING'));
|
||||
```
|
||||
|
||||
### RLS Guidance
|
||||
- Backend does not rely on RLS for correctness (uses service_role), but RLS is mandatory as a defensive boundary for tables in PostgREST-exposed schemas.
|
||||
- **Mandatory default**: any new business table in `public` must enable RLS in the same Alembic migration.
|
||||
|
||||
@@ -0,0 +1,204 @@
|
||||
# Auth UX Enhancement Design
|
||||
|
||||
**日期**: 2026-02-26
|
||||
**状态**: 可实施(修订版)
|
||||
|
||||
## 目标
|
||||
|
||||
本次改动聚焦 4 个问题:
|
||||
1. 注册验证码页增加首次提示,降低用户困惑。
|
||||
2. 增加忘记密码流程(验证码模式)。
|
||||
3. 注册页增加邀请码输入(前端收集,后端暂不消费)。
|
||||
4. 修复用户名非唯一导致的用户查询问题,改为搜索接口。
|
||||
|
||||
## 非目标
|
||||
|
||||
- 不实现邀请码校验/入库。
|
||||
- 不改动 Supabase 邮件模板基础设施(当前 self-hosted 已配置 recovery 模板 URL)。
|
||||
- 不在本次引入新的认证机制(仅沿用 Supabase OTP + session)。
|
||||
|
||||
---
|
||||
|
||||
## 1. 忘记密码的可落地后端方案(对外两步)
|
||||
|
||||
### 1.1 约束说明(关键)
|
||||
|
||||
当前 Python SDK 的 `verify_otp` 参数模型不支持在验码时直接携带 `new_password`。因此不能走“单接口验码并改密”的实现。
|
||||
|
||||
### 1.2 可执行流程
|
||||
|
||||
对客户端暴露两步流程,第二步在后端内部执行两段动作:
|
||||
|
||||
1. `POST /auth/password-reset`:调用 Supabase `reset_password_email` 发送 recovery 验证码。
|
||||
2. `POST /auth/password-reset/confirm`:接收 `email + token + new_password`,后端内部先调用 `verify_otp(type="recovery")`,再基于该会话调用 `update_user(password=...)`。
|
||||
|
||||
这样既匹配 SDK 能力(`verify_otp` 不支持直接带 `new_password`),又保持前端体验为两步。
|
||||
|
||||
### 1.3 API 设计
|
||||
|
||||
#### POST /auth/password-reset
|
||||
|
||||
发送重置验证码。
|
||||
|
||||
Request
|
||||
```json
|
||||
{
|
||||
"email": "string(email)",
|
||||
"redirect_to": "string(optional)"
|
||||
}
|
||||
```
|
||||
|
||||
Response: `204 No Content`
|
||||
|
||||
Errors:
|
||||
- `422` 参数错误
|
||||
- `429` 频率受限
|
||||
|
||||
#### POST /auth/password-reset/confirm
|
||||
|
||||
验证 recovery 验证码并完成改密。
|
||||
|
||||
Request
|
||||
```json
|
||||
{
|
||||
"email": "string(email)",
|
||||
"token": "string(6 digits)",
|
||||
"new_password": "string(min 6)"
|
||||
}
|
||||
```
|
||||
|
||||
Response: `204 No Content`
|
||||
|
||||
Errors:
|
||||
- `401` 验证码无效或过期
|
||||
- `422` 参数错误
|
||||
- `429` 频率受限
|
||||
|
||||
### 1.4 安全边界
|
||||
|
||||
- 用户档案更新走 `users` 域(`/users/me` -> `UserService` -> `Profile`),仅允许公开资料字段。
|
||||
- 密码修改走 `auth` 域(Supabase Auth),不复用 `users` service/repository。
|
||||
- `POST /auth/password-reset/confirm` 必须在同一请求内完成“验码 + 改密”,禁止单独暴露“仅改密”接口。
|
||||
- 即使伪造 `/users/me` 请求,也无法触发密码修改路径。
|
||||
|
||||
---
|
||||
|
||||
## 2. 前端忘记密码流程
|
||||
|
||||
流程:
|
||||
|
||||
`登录页` -> `忘记密码页(输入邮箱)` -> `验证码+新密码页` -> `返回登录并用新密码登录`
|
||||
|
||||
关键点:
|
||||
- 第二步页面一次提交 `email + token + new_password` 到 `/auth/password-reset/confirm`。
|
||||
- 所有用户反馈统一使用 `Toast`(遵循 `apps/AGENTS.md`)。
|
||||
- 错误提示优先展示后端 `detail`。
|
||||
|
||||
---
|
||||
|
||||
## 3. 注册 UX 优化
|
||||
|
||||
### 3.1 验证码发送提示
|
||||
|
||||
在 `register_verification_screen.dart` 首次进入页面显示:
|
||||
|
||||
`验证码已发送,如未收到请检查垃圾邮件或确认邮箱已注册`
|
||||
|
||||
### 3.2 邀请码输入
|
||||
|
||||
在注册页新增可选字段:
|
||||
- Label: `邀请码(选填)`
|
||||
- Hint: `请输入邀请码`
|
||||
|
||||
前端请求体可携带 `invite_code`,后端忽略该字段,不返回错误。
|
||||
|
||||
---
|
||||
|
||||
## 4. 用户搜索 Bug 修复
|
||||
|
||||
### 4.1 问题
|
||||
|
||||
`GET /users/{username}` 隐含“用户名唯一”假设,实际不成立。
|
||||
|
||||
### 4.2 方案
|
||||
|
||||
后端删除 `GET /users/{username}`,改为 `POST /users/search`。
|
||||
|
||||
Request
|
||||
```json
|
||||
{
|
||||
"query": "string(1-100)"
|
||||
}
|
||||
```
|
||||
|
||||
Response
|
||||
```json
|
||||
[
|
||||
{
|
||||
"id": "string",
|
||||
"username": "string",
|
||||
"avatar_url": "string|null",
|
||||
"bio": "string|null"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
查询策略:
|
||||
- username: 模糊匹配(`ilike`)
|
||||
- email: 精确匹配
|
||||
- 最多返回 20 条
|
||||
- 返回公开字段,不返回 email
|
||||
|
||||
### 4.3 前端联动
|
||||
|
||||
必须同步迁移 `apps/lib/features/users/data/*`(`getByUsername` -> `searchUsers`),否则删除后端旧路由后前端会直接 404。
|
||||
|
||||
---
|
||||
|
||||
## 5. 主要改动文件
|
||||
|
||||
### 后端
|
||||
|
||||
- `backend/src/v1/auth/schemas.py`
|
||||
- `backend/src/v1/auth/service.py`
|
||||
- `backend/src/v1/auth/gateway.py`
|
||||
- `backend/src/v1/auth/router.py`
|
||||
- `backend/src/v1/users/schemas.py`
|
||||
- `backend/src/v1/users/repository.py`
|
||||
- `backend/src/v1/users/service.py`
|
||||
- `backend/src/v1/users/router.py`
|
||||
- `backend/tests/integration/test_auth_routes.py`
|
||||
- `backend/tests/integration/test_users_routes.py`
|
||||
|
||||
### 前端
|
||||
|
||||
- `apps/lib/features/auth/ui/screens/login_screen.dart`
|
||||
- `apps/lib/features/auth/ui/screens/register_screen.dart`
|
||||
- `apps/lib/features/auth/ui/screens/register_verification_screen.dart`
|
||||
- `apps/lib/features/auth/ui/screens/forgot_password_screen.dart`(新增)
|
||||
- `apps/lib/features/auth/ui/screens/reset_password_screen.dart`(新增)
|
||||
- `apps/lib/features/auth/presentation/cubits/forgot_password_cubit.dart`(新增)
|
||||
- `apps/lib/features/auth/presentation/cubits/reset_password_cubit.dart`(新增)
|
||||
- `apps/lib/features/auth/data/auth_api.dart`
|
||||
- `apps/lib/features/auth/data/auth_repository.dart`
|
||||
- `apps/lib/features/auth/data/auth_repository_impl.dart`
|
||||
- `apps/lib/features/users/data/users_api.dart`
|
||||
- `apps/lib/features/users/data/users_repository.dart`
|
||||
- `apps/lib/features/users/data/users_repository_impl.dart`
|
||||
- `apps/lib/core/router/app_router.dart`
|
||||
|
||||
### 文档
|
||||
|
||||
- `docs/runtime/runtime-route.md`(按 AGENTS 规则必须同步)
|
||||
|
||||
---
|
||||
|
||||
## 6. 验收标准
|
||||
|
||||
- [ ] 注册验证码页首次进入显示提示。
|
||||
- [ ] 登录页出现“忘记密码”入口。
|
||||
- [ ] 忘记密码流程可完整走通(发码、确认改密、重新登录)。
|
||||
- [ ] 注册页可输入邀请码且不影响注册。
|
||||
- [ ] `GET /users/{username}` 被移除。
|
||||
- [ ] `POST /users/search` 可用且返回不含 email。
|
||||
- [ ] 后端与前端相关测试通过,文档已同步。
|
||||
@@ -0,0 +1,402 @@
|
||||
# Auth UX Enhancement Implementation Plan
|
||||
|
||||
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
|
||||
|
||||
**Goal:** 以最小风险完成 Auth UX 优化(忘记密码、注册提示、邀请码、用户搜索修复),并确保与现有 Supabase SDK 能力完全一致。
|
||||
|
||||
**Architecture:** 对客户端暴露两步重置流程:`password-reset` 发码 + `password-reset/confirm` 确认改密。`confirm` 在后端内部串行执行 `verify_otp(type="recovery")` 与 `update_user(password=...)`,既符合 SDK 限制又保持交互简洁。用户查询从 `GET /users/{username}` 迁移到 `POST /users/search`,并同步前端数据层,避免断链。全流程按 TDD 执行并同步路由文档。
|
||||
|
||||
**Tech Stack:** FastAPI, Pydantic, SQLAlchemy, Supabase Auth, Flutter, Dio, Bloc/Cubit
|
||||
|
||||
---
|
||||
|
||||
## Phase 0: 基线与保护
|
||||
|
||||
### Task 1: 建立基线测试(RED 前准备)
|
||||
|
||||
**Files:**
|
||||
- Modify: `backend/tests/integration/test_auth_routes.py`
|
||||
- Modify: `backend/tests/integration/test_users_routes.py`
|
||||
|
||||
**Step 1: 新增失败测试占位(不改实现)**
|
||||
|
||||
新增并期望失败的测试:
|
||||
- `test_password_reset_request_returns_204`
|
||||
- `test_password_reset_confirm_returns_204`
|
||||
- `test_search_users_returns_list`
|
||||
|
||||
**Step 2: 运行后端测试确认 RED**
|
||||
|
||||
Run: `uv run pytest backend/tests/integration/test_auth_routes.py backend/tests/integration/test_users_routes.py -v`
|
||||
|
||||
Expected: 新增用例失败(404/AttributeError/未实现)。
|
||||
|
||||
**Step 3: Commit**
|
||||
|
||||
```bash
|
||||
git add backend/tests/integration/test_auth_routes.py backend/tests/integration/test_users_routes.py
|
||||
git commit -m "test: add failing tests for auth ux enhancement"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phase 1: 后端密码重置(对外两步)
|
||||
|
||||
### Task 2: 完成密码重置请求接口(发码)
|
||||
|
||||
**Files:**
|
||||
- Modify: `backend/src/v1/auth/schemas.py`
|
||||
- Modify: `backend/src/v1/auth/service.py`
|
||||
- Modify: `backend/src/v1/auth/gateway.py`
|
||||
- Modify: `backend/src/v1/auth/router.py`
|
||||
- Test: `backend/tests/integration/test_auth_routes.py`
|
||||
|
||||
**Step 1: 写失败测试(若 Task 1 未覆盖细节)**
|
||||
|
||||
确保 `POST /api/v1/auth/password-reset`:
|
||||
- 合法邮箱返回 `204`
|
||||
- 非法参数返回 `422`
|
||||
- 触发限流返回 `429`
|
||||
|
||||
**Step 2: 跑单测确认失败**
|
||||
|
||||
Run: `uv run pytest backend/tests/integration/test_auth_routes.py::test_password_reset_request_returns_204 -v`
|
||||
|
||||
Expected: FAIL。
|
||||
|
||||
**Step 3: 最小实现(GREEN)**
|
||||
|
||||
- `schemas.py`: 复用已有 `PasswordResetRequest`,不要重复定义同名模型。
|
||||
- `service.py`: 增加 `request_password_reset(...)`。
|
||||
- `gateway.py`: 调用 `self._client.auth.reset_password_email(email, options)`。
|
||||
- `router.py`: 新增 `POST /auth/password-reset`,返回 `204`,保留限流。
|
||||
|
||||
**Step 4: 跑测试确认通过**
|
||||
|
||||
Run: `uv run pytest backend/tests/integration/test_auth_routes.py::test_password_reset_request_returns_204 -v`
|
||||
|
||||
Expected: PASS。
|
||||
|
||||
**Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add backend/src/v1/auth/schemas.py backend/src/v1/auth/service.py backend/src/v1/auth/gateway.py backend/src/v1/auth/router.py backend/tests/integration/test_auth_routes.py
|
||||
git commit -m "feat(auth): add password reset request endpoint"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 3: 完成密码重置确认接口(验码 + 改密)
|
||||
|
||||
**Files:**
|
||||
- Modify: `backend/src/v1/auth/schemas.py`
|
||||
- Modify: `backend/src/v1/auth/service.py`
|
||||
- Modify: `backend/src/v1/auth/gateway.py`
|
||||
- Modify: `backend/src/v1/auth/router.py`
|
||||
- Test: `backend/tests/integration/test_auth_routes.py`
|
||||
|
||||
**Step 1: 写失败测试**
|
||||
|
||||
测试 `POST /api/v1/auth/password-reset/confirm`:
|
||||
- 正确 `email + token + new_password` 返回 `204`
|
||||
- 错误验证码返回 `401`
|
||||
- 弱密码/参数错误返回 `422`
|
||||
|
||||
**Step 2: 跑测试确认失败**
|
||||
|
||||
Run: `uv run pytest backend/tests/integration/test_auth_routes.py::test_password_reset_confirm_returns_204 -v`
|
||||
|
||||
Expected: FAIL。
|
||||
|
||||
**Step 3: 最小实现**
|
||||
|
||||
- `schemas.py`: 新增 `PasswordResetConfirmRequest(email, token, new_password)`。
|
||||
- `service.py`: 增加 `confirm_password_reset(...)`。
|
||||
- `gateway.py`: 在单个方法内先调用 `verify_otp({"type":"recovery", ...})`,再在该会话上下文调用 `update_user({"password": new_password})`。
|
||||
- `router.py`: 新增 `POST /auth/password-reset/confirm`,返回 `204`。
|
||||
|
||||
**Step 4: 跑测试确认通过**
|
||||
|
||||
Run: `uv run pytest backend/tests/integration/test_auth_routes.py::test_password_reset_confirm_returns_204 -v`
|
||||
|
||||
Expected: PASS。
|
||||
|
||||
**Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add backend/src/v1/auth/schemas.py backend/src/v1/auth/service.py backend/src/v1/auth/gateway.py backend/src/v1/auth/router.py backend/tests/integration/test_auth_routes.py
|
||||
git commit -m "feat(auth): add password reset confirm endpoint"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phase 2: 后端用户搜索替代用户名路由
|
||||
|
||||
### Task 4: 新增 POST /users/search 并移除旧路由
|
||||
|
||||
**Files:**
|
||||
- Modify: `backend/src/v1/users/schemas.py`
|
||||
- Modify: `backend/src/v1/users/repository.py`
|
||||
- Modify: `backend/src/v1/users/service.py`
|
||||
- Modify: `backend/src/v1/users/router.py`
|
||||
- Test: `backend/tests/integration/test_users_routes.py`
|
||||
|
||||
**Step 1: 写失败测试**
|
||||
|
||||
新增测试:
|
||||
- `POST /api/v1/users/search` 成功返回列表
|
||||
- `query` 为空返回 `422`
|
||||
- 删除后 `GET /api/v1/users/{username}` 返回 `404`
|
||||
|
||||
**Step 2: 跑测试确认失败**
|
||||
|
||||
Run: `uv run pytest backend/tests/integration/test_users_routes.py -v`
|
||||
|
||||
Expected: FAIL。
|
||||
|
||||
**Step 3: 最小实现**
|
||||
|
||||
- `schemas.py`: 增加 `UserSearchRequest`, `UserSearchResult`。
|
||||
- `repository.py`: 新增 `search_users(query)`;username `ilike` + email 精确匹配,`limit 20`。
|
||||
- `service.py`: 增加 `search_users(...)` 并映射公开字段。
|
||||
- `router.py`: 增加 `POST /users/search`;删除 `GET /users/{username}`。
|
||||
|
||||
**Step 4: 跑测试确认通过**
|
||||
|
||||
Run: `uv run pytest backend/tests/integration/test_users_routes.py -v`
|
||||
|
||||
Expected: PASS。
|
||||
|
||||
**Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add backend/src/v1/users/schemas.py backend/src/v1/users/repository.py backend/src/v1/users/service.py backend/src/v1/users/router.py backend/tests/integration/test_users_routes.py
|
||||
git commit -m "refactor(users): replace username endpoint with search"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phase 3: 前端认证 UX
|
||||
|
||||
### Task 5: 数据层支持密码重置两步接口
|
||||
|
||||
**Files:**
|
||||
- Modify: `apps/lib/features/auth/data/models/login_request.dart`
|
||||
- Modify: `apps/lib/features/auth/data/auth_api.dart`
|
||||
- Modify: `apps/lib/features/auth/data/auth_repository.dart`
|
||||
- Modify: `apps/lib/features/auth/data/auth_repository_impl.dart`
|
||||
- Test: `apps/test/features/auth/data/auth_repository_impl_test.dart`(如不存在则创建)
|
||||
|
||||
**Step 1: 写失败测试**
|
||||
|
||||
覆盖:
|
||||
- request -> confirm 的调用顺序
|
||||
- confirm 请求包含 `email + token + new_password`
|
||||
|
||||
**Step 2: 跑测试确认失败**
|
||||
|
||||
Run: `flutter test apps/test/features/auth/data/auth_repository_impl_test.dart`
|
||||
|
||||
Expected: FAIL。
|
||||
|
||||
**Step 3: 最小实现**
|
||||
|
||||
- 增加 `requestPasswordReset` / `confirmPasswordReset`。
|
||||
- 模型保持 snake_case JSON 键与后端一致。
|
||||
|
||||
**Step 4: 跑测试确认通过**
|
||||
|
||||
Run: `flutter test apps/test/features/auth/data/auth_repository_impl_test.dart`
|
||||
|
||||
Expected: PASS。
|
||||
|
||||
**Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add apps/lib/features/auth/data apps/test/features/auth/data/auth_repository_impl_test.dart
|
||||
git commit -m "feat(auth): add password reset data layer"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 6: 新增忘记密码页面与状态管理
|
||||
|
||||
**Files:**
|
||||
- Create: `apps/lib/features/auth/ui/screens/forgot_password_screen.dart`
|
||||
- Create: `apps/lib/features/auth/ui/screens/reset_password_screen.dart`
|
||||
- Create: `apps/lib/features/auth/presentation/cubits/forgot_password_cubit.dart`
|
||||
- Create: `apps/lib/features/auth/presentation/cubits/reset_password_cubit.dart`
|
||||
- Modify: `apps/lib/features/auth/ui/screens/login_screen.dart`
|
||||
- Modify: `apps/lib/core/router/app_router.dart`
|
||||
- Test: `apps/test/features/auth/ui/forgot_password_screen_test.dart`(可新建)
|
||||
|
||||
**Step 1: 写失败测试**
|
||||
|
||||
至少覆盖:
|
||||
- 登录页点击“忘记密码”可跳转
|
||||
- 忘记密码页提交后进入验证码改密页
|
||||
|
||||
**Step 2: 跑测试确认失败**
|
||||
|
||||
Run: `flutter test apps/test/features/auth/ui/forgot_password_screen_test.dart`
|
||||
|
||||
Expected: FAIL。
|
||||
|
||||
**Step 3: 最小实现**
|
||||
|
||||
- 使用 `Toast` 呈现提交成功/失败反馈。
|
||||
- reset 页面提交时调用单个 confirm 接口完成验码与改密。
|
||||
- 成功后跳回登录并提示“密码已重置”。
|
||||
|
||||
**Step 4: 跑测试确认通过**
|
||||
|
||||
Run: `flutter test apps/test/features/auth/ui/forgot_password_screen_test.dart`
|
||||
|
||||
Expected: PASS。
|
||||
|
||||
**Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add apps/lib/features/auth apps/lib/core/router/app_router.dart apps/test/features/auth/ui/forgot_password_screen_test.dart
|
||||
git commit -m "feat(auth): add forgot password ui flow"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 7: 注册体验优化(提示 + 邀请码)
|
||||
|
||||
**Files:**
|
||||
- Modify: `apps/lib/features/auth/ui/screens/register_verification_screen.dart`
|
||||
- Modify: `apps/lib/features/auth/ui/screens/register_screen.dart`
|
||||
- Modify: `apps/lib/features/auth/presentation/cubits/register_cubit.dart`
|
||||
- Modify: `apps/lib/features/auth/data/models/signup_request.dart`
|
||||
- Test: `apps/test/features/auth/ui/register_screen_test.dart`(如不存在则创建)
|
||||
|
||||
**Step 1: 写失败测试**
|
||||
|
||||
覆盖:
|
||||
- 验证码页首次进入显示提示 Toast
|
||||
- 注册页存在邀请码输入并为可选
|
||||
|
||||
**Step 2: 跑测试确认失败**
|
||||
|
||||
Run: `flutter test apps/test/features/auth/ui/register_screen_test.dart`
|
||||
|
||||
Expected: FAIL。
|
||||
|
||||
**Step 3: 最小实现**
|
||||
|
||||
- `signup_request` 可选字段 `invite_code`。
|
||||
- 页面展示邀请码输入框,不做必填校验。
|
||||
|
||||
**Step 4: 跑测试确认通过**
|
||||
|
||||
Run: `flutter test apps/test/features/auth/ui/register_screen_test.dart`
|
||||
|
||||
Expected: PASS。
|
||||
|
||||
**Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add apps/lib/features/auth/ui/screens/register_verification_screen.dart apps/lib/features/auth/ui/screens/register_screen.dart apps/lib/features/auth/presentation/cubits/register_cubit.dart apps/lib/features/auth/data/models/signup_request.dart apps/test/features/auth/ui/register_screen_test.dart
|
||||
git commit -m "feat(auth): improve verification hint and invite code input"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phase 4: 前端 users 数据层迁移
|
||||
|
||||
### Task 8: 将 getByUsername 迁移为 searchUsers
|
||||
|
||||
**Files:**
|
||||
- Modify: `apps/lib/features/users/data/users_api.dart`
|
||||
- Modify: `apps/lib/features/users/data/users_repository.dart`
|
||||
- Modify: `apps/lib/features/users/data/users_repository_impl.dart`
|
||||
- Test: `apps/test/features/users/data/users_repository_test.dart`(如不存在则创建)
|
||||
|
||||
**Step 1: 写失败测试**
|
||||
|
||||
覆盖:
|
||||
- `searchUsers(query)` 发送 `POST /api/v1/users/search`
|
||||
- 返回列表模型映射正确
|
||||
|
||||
**Step 2: 跑测试确认失败**
|
||||
|
||||
Run: `flutter test apps/test/features/users/data/users_repository_test.dart`
|
||||
|
||||
Expected: FAIL。
|
||||
|
||||
**Step 3: 最小实现**
|
||||
|
||||
- 删除 `getByUsername`。
|
||||
- 新增 `searchUsers(String query)`。
|
||||
|
||||
**Step 4: 跑测试确认通过**
|
||||
|
||||
Run: `flutter test apps/test/features/users/data/users_repository_test.dart`
|
||||
|
||||
Expected: PASS。
|
||||
|
||||
**Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add apps/lib/features/users/data apps/test/features/users/data/users_repository_test.dart
|
||||
git commit -m "refactor(users): migrate client to search endpoint"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phase 5: 文档与全量验证
|
||||
|
||||
### Task 9: 更新 API 文档并完成全量检查
|
||||
|
||||
**Files:**
|
||||
- Modify: `docs/runtime/runtime-route.md`
|
||||
|
||||
**Step 1: 更新路由文档**
|
||||
|
||||
- 新增:
|
||||
- `POST /auth/password-reset`
|
||||
- `POST /auth/password-reset/confirm`
|
||||
- `POST /users/search`
|
||||
- 删除:
|
||||
- `GET /users/{username}`
|
||||
|
||||
文档需包含:请求/响应 schema、状态码、错误格式(RFC 7807)。
|
||||
|
||||
**Step 2: 跑后端验证**
|
||||
|
||||
Run: `uv run pytest backend/tests -v && uv run basedpyright backend/src`
|
||||
|
||||
Expected: 全通过。
|
||||
|
||||
**Step 3: 跑前端验证**
|
||||
|
||||
Run: `flutter analyze apps/lib && flutter test`
|
||||
|
||||
Expected: 全通过。
|
||||
|
||||
**Step 4: 手动验收**
|
||||
|
||||
- 忘记密码完整链路(发码/确认改密/登录)
|
||||
- 注册页邀请码与验证码提示
|
||||
- `POST /users/search` 返回结果正确
|
||||
|
||||
**Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add docs/runtime/runtime-route.md
|
||||
git commit -m "docs: sync runtime routes for auth ux enhancement"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 验收清单
|
||||
|
||||
- [ ] 所有新增测试先失败后通过(有 RED/GREEN 记录)
|
||||
- [ ] 后端密码重置两步接口可用(confirm 内部完成验码 + 改密)
|
||||
- [ ] 前端忘记密码流程可用
|
||||
- [ ] 邀请码输入为选填且不破坏现有注册
|
||||
- [ ] `GET /users/{username}` 全链路移除
|
||||
- [ ] `POST /users/search` 前后端一致
|
||||
- [ ] `docs/runtime/runtime-route.md` 已同步
|
||||
@@ -1,81 +0,0 @@
|
||||
# 注册验证码流程 UX 优化设计
|
||||
|
||||
**日期**: 2026-02-26
|
||||
**状态**: 已确认
|
||||
|
||||
## 背景
|
||||
|
||||
当前注册流程存在问题:
|
||||
1. 点击"下一步"后界面卡住,等待邮件发送完成才跳转
|
||||
2. 重发验证码按钮没有倒计时
|
||||
3. 重发按钮样式不美观,文字挤在一起
|
||||
|
||||
## 目标
|
||||
|
||||
1. 优化界面响应速度,点击"下一步"立即跳转
|
||||
2. 添加 60 秒倒计时,倒计时期间禁用重发按钮
|
||||
3. 优化重发按钮样式
|
||||
|
||||
## 设计方案
|
||||
|
||||
### 1. 乐观跳转策略
|
||||
|
||||
**改动文件**: `apps/lib/features/auth/ui/screens/register_screen.dart`
|
||||
|
||||
- 点击"下一步"后,不等待 `submitStep1()` 完成,立即跳转到验证码界面
|
||||
- `submitStep1()` 在后台执行
|
||||
- 验证码界面监听发送结果,失败时用 Toast 提示
|
||||
|
||||
### 2. 倒计时状态管理
|
||||
|
||||
**改动文件**: `apps/lib/features/auth/ui/screens/register_verification_screen.dart`
|
||||
|
||||
- 添加 `Timer` 管理倒计时状态
|
||||
- 初始进入界面时启动 60 秒倒计时
|
||||
- 重发成功后重置 60 秒倒计时
|
||||
- 离开界面时取消 Timer
|
||||
|
||||
**状态**:
|
||||
- `countdown`: 当前剩余秒数 (60 → 0)
|
||||
- `canResend`: 是否可重发 (countdown == 0)
|
||||
|
||||
### 3. 重发按钮样式
|
||||
|
||||
**改动文件**: `apps/lib/features/auth/ui/screens/register_verification_screen.dart`
|
||||
|
||||
**视觉规格**:
|
||||
- 宽度: 90px
|
||||
- 高度: 40px
|
||||
- 圆角: 8px
|
||||
- 边框: 1px
|
||||
|
||||
**两种状态**:
|
||||
|
||||
| 状态 | 文字 | 边框颜色 | 文字颜色 | 可点击 |
|
||||
|------|------|----------|----------|--------|
|
||||
| 倒计时中 | `"60 s"` / `"59 s"` ... | `AppColors.slate300` | `AppColors.slate400` | 否 |
|
||||
| 可重发 | `"重新发送"` | `AppColors.primary` | `AppColors.primary` | 是 |
|
||||
|
||||
### 4. 错误提示
|
||||
|
||||
**改动文件**: `apps/lib/features/auth/ui/screens/register_verification_screen.dart`
|
||||
|
||||
- 发送/重发验证码失败时,使用 `Toast.show()` 显示错误提示
|
||||
- 不阻塞用户操作,用户可再次点击重发
|
||||
|
||||
## 实现范围
|
||||
|
||||
| 文件 | 改动内容 |
|
||||
|------|----------|
|
||||
| `register_screen.dart` | 乐观跳转,后台发送验证码 |
|
||||
| `register_verification_screen.dart` | Timer 倒计时、按钮样式、Toast 错误提示 |
|
||||
| `register_cubit.dart` | 添加 `isSending` 状态(可选) |
|
||||
|
||||
## 验收标准
|
||||
|
||||
1. 点击"下一步"后立即跳转到验证码界面,无卡顿
|
||||
2. 验证码界面显示 60 秒倒计时按钮
|
||||
3. 倒计时期间按钮禁用,显示秒数
|
||||
4. 倒计时结束后按钮可点击,显示"重新发送"
|
||||
5. 点击重发后重新开始 60 秒倒计时
|
||||
6. 发送失败时 Toast 提示错误信息
|
||||
@@ -1,404 +0,0 @@
|
||||
# 注册验证码流程 UX 优化实现计划
|
||||
|
||||
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
|
||||
|
||||
**Goal:** 优化注册流程的界面响应速度和重发验证码按钮的交互体验
|
||||
|
||||
**Architecture:** 乐观跳转策略 + Timer 倒计时状态管理,后台异步发送验证码
|
||||
|
||||
**Tech Stack:** Flutter, dart:async Timer, flutter_bloc
|
||||
|
||||
---
|
||||
|
||||
## Task 1: 扩展 RegisterCubit 状态
|
||||
|
||||
**Files:**
|
||||
- Modify: `apps/lib/features/auth/presentation/cubits/register_cubit.dart`
|
||||
- Modify: `apps/test/features/auth/presentation/cubits/register_cubit_test.dart`
|
||||
|
||||
**Step 1: 添加 isSending 状态字段**
|
||||
|
||||
在 `RegisterState` 中添加 `isSending` 字段:
|
||||
|
||||
```dart
|
||||
// RegisterState
|
||||
final bool isSending;
|
||||
|
||||
const RegisterState({
|
||||
// ... existing fields
|
||||
this.isSending = false,
|
||||
});
|
||||
|
||||
// copyWith
|
||||
bool? isSending,
|
||||
|
||||
// copyWith return
|
||||
isSending: isSending ?? this.isSending,
|
||||
|
||||
// props
|
||||
isSending,
|
||||
```
|
||||
|
||||
**Step 2: 添加 sendCodeSilently 方法**
|
||||
|
||||
在 `RegisterCubit` 中添加不阻塞的发送方法:
|
||||
|
||||
```dart
|
||||
Future<void> sendCodeSilently() async {
|
||||
if (!state.isStep1Valid) return;
|
||||
|
||||
emit(state.copyWith(isSending: true));
|
||||
|
||||
try {
|
||||
final response = await _repository.signupStart(
|
||||
SignupStartRequest(
|
||||
username: state.username.value,
|
||||
email: state.email.value,
|
||||
password: state.password.value,
|
||||
),
|
||||
);
|
||||
emit(
|
||||
state.copyWith(
|
||||
isSending: false,
|
||||
pendingEmail: response.email,
|
||||
codeSent: true,
|
||||
errorMessage: null,
|
||||
),
|
||||
);
|
||||
} catch (e) {
|
||||
final message = e is ApiException ? e.message : '验证码发送失败,请重试';
|
||||
emit(
|
||||
state.copyWith(
|
||||
isSending: false,
|
||||
errorMessage: message,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Step 3: 添加测试**
|
||||
|
||||
```dart
|
||||
// 在 register_cubit_test.dart 添加
|
||||
|
||||
group('sendCodeSilently', () {
|
||||
blocTest<RegisterCubit, RegisterState>(
|
||||
'sets isSending to true then false on success',
|
||||
build: () => cubit,
|
||||
seed: () => RegisterState(
|
||||
username: const Username.dirty('testuser'),
|
||||
email: const Email.dirty('test@example.com'),
|
||||
password: const Password.dirty('password123'),
|
||||
),
|
||||
setUp: () {
|
||||
when(() => mockRepository.signupStart(any()))
|
||||
.thenAnswer((_) async => SignupStartResponse(email: 'test@example.com'));
|
||||
},
|
||||
act: (c) => c.sendCodeSilently(),
|
||||
verify: (_) {
|
||||
verify(() => mockRepository.signupStart(any())).called(1);
|
||||
},
|
||||
);
|
||||
});
|
||||
```
|
||||
|
||||
**Step 4: 运行测试**
|
||||
|
||||
Run: `cd apps && flutter test test/features/auth/presentation/cubits/register_cubit_test.dart`
|
||||
Expected: PASS
|
||||
|
||||
**Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add apps/lib/features/auth/presentation/cubits/register_cubit.dart apps/test/features/auth/presentation/cubits/register_cubit_test.dart
|
||||
git commit -m "feat(auth): add sendCodeSilently with isSending state"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 2: 实现乐观跳转
|
||||
|
||||
**Files:**
|
||||
- Modify: `apps/lib/features/auth/ui/screens/register_screen.dart`
|
||||
|
||||
**Step 1: 修改 _handleNext 方法**
|
||||
|
||||
将同步等待改为乐观跳转:
|
||||
|
||||
```dart
|
||||
Future<void> _handleNext() async {
|
||||
final cubit = context.read<RegisterCubit>();
|
||||
cubit.usernameChanged(_nicknameController.text);
|
||||
cubit.emailChanged(_emailController.text);
|
||||
cubit.passwordChanged(_passwordController.text);
|
||||
|
||||
if (!cubit.state.isStep1Valid) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 乐观跳转:立即跳转到验证码界面
|
||||
if (mounted) {
|
||||
context.push('/register/verification', extra: cubit);
|
||||
}
|
||||
|
||||
// 后台发送验证码
|
||||
cubit.sendCodeSilently();
|
||||
}
|
||||
```
|
||||
|
||||
**Step 2: Commit**
|
||||
|
||||
```bash
|
||||
git add apps/lib/features/auth/ui/screens/register_screen.dart
|
||||
git commit -m "feat(auth): optimistic navigation to verification screen"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 3: 实现倒计时状态管理
|
||||
|
||||
**Files:**
|
||||
- Modify: `apps/lib/features/auth/ui/screens/register_verification_screen.dart`
|
||||
|
||||
**Step 1: 添加 Timer 状态**
|
||||
|
||||
在 `_RegisterVerificationViewState` 中添加:
|
||||
|
||||
```dart
|
||||
import 'dart:async';
|
||||
|
||||
// 状态变量
|
||||
Timer? _countdownTimer;
|
||||
int _countdown = 60;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_startCountdown();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_countdownTimer?.cancel();
|
||||
_codeController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _startCountdown() {
|
||||
setState(() {
|
||||
_countdown = 60;
|
||||
});
|
||||
_countdownTimer?.cancel();
|
||||
_countdownTimer = Timer.periodic(const Duration(seconds: 1), (timer) {
|
||||
if (_countdown > 0) {
|
||||
setState(() {
|
||||
_countdown--;
|
||||
});
|
||||
} else {
|
||||
timer.cancel();
|
||||
}
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
**Step 2: Commit**
|
||||
|
||||
```bash
|
||||
git add apps/lib/features/auth/ui/screens/register_verification_screen.dart
|
||||
git commit -m "feat(auth): add countdown timer for resend button"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 4: 优化重发按钮样式
|
||||
|
||||
**Files:**
|
||||
- Modify: `apps/lib/features/auth/ui/screens/register_verification_screen.dart`
|
||||
|
||||
**Step 1: 重构 _buildCodeInput 方法**
|
||||
|
||||
替换重发按钮部分:
|
||||
|
||||
```dart
|
||||
Widget _buildCodeInput(RegisterState state) {
|
||||
final canResend = _countdown == 0 && state.status != FormzSubmissionStatus.inProgress;
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'邮箱验证码',
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Color(0xFF475569),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: SizedBox(
|
||||
height: 40,
|
||||
child: TextField(
|
||||
controller: _codeController,
|
||||
keyboardType: TextInputType.number,
|
||||
decoration: const InputDecoration(
|
||||
hintText: '输入验证码',
|
||||
contentPadding: EdgeInsets.symmetric(
|
||||
horizontal: 12,
|
||||
vertical: 10,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
_buildResendButton(canResend),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildResendButton(bool canResend) {
|
||||
final borderColor = canResend ? AppColors.primary : AppColors.slate300;
|
||||
final textColor = canResend ? AppColors.primary : AppColors.slate400;
|
||||
final text = canResend ? '重新发送' : '$_countdown s';
|
||||
|
||||
return SizedBox(
|
||||
width: 90,
|
||||
height: 40,
|
||||
child: OutlinedButton(
|
||||
onPressed: canResend ? _handleResendCode : null,
|
||||
style: OutlinedButton.styleFrom(
|
||||
backgroundColor: AppColors.background,
|
||||
side: BorderSide(color: borderColor),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
text,
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: textColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
**Step 2: Commit**
|
||||
|
||||
```bash
|
||||
git add apps/lib/features/auth/ui/screens/register_verification_screen.dart
|
||||
git commit -m "feat(auth): improve resend button style with countdown"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 5: 添加 Toast 错误提示
|
||||
|
||||
**Files:**
|
||||
- Modify: `apps/lib/features/auth/ui/screens/register_verification_screen.dart`
|
||||
|
||||
**Step 1: 添加 BlocListener 监听错误**
|
||||
|
||||
用 `BlocConsumer` 替换 `BlocBuilder`,添加监听:
|
||||
|
||||
```dart
|
||||
import '../../../../shared/widgets/toast/toast.dart';
|
||||
|
||||
Widget _buildFormContainer() {
|
||||
return BlocConsumer<RegisterCubit, RegisterState>(
|
||||
listener: (context, state) {
|
||||
// 监听后台发送验证码的错误
|
||||
if (state.errorMessage != null && !state.isStep2Valid) {
|
||||
Toast.show(context, state.errorMessage!, type: ToastType.error);
|
||||
}
|
||||
},
|
||||
builder: (context, state) {
|
||||
return SizedBox(
|
||||
width: 327,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
_buildCodeInput(state),
|
||||
const SizedBox(height: 12),
|
||||
_buildStepIndicator(),
|
||||
const SizedBox(height: 12),
|
||||
AppButton(
|
||||
text: '完成注册',
|
||||
onPressed: state.status == FormzSubmissionStatus.inProgress
|
||||
? null
|
||||
: _handleComplete,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
**Step 2: 修改 _handleResendCode 重置倒计时**
|
||||
|
||||
```dart
|
||||
Future<void> _handleResendCode() async {
|
||||
final cubit = context.read<RegisterCubit>();
|
||||
await cubit.resendCode();
|
||||
|
||||
// 重发成功后重置倒计时
|
||||
if (cubit.state.codeSent && mounted) {
|
||||
_startCountdown();
|
||||
Toast.show(context, '验证码已发送', type: ToastType.success);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Step 3: Commit**
|
||||
|
||||
```bash
|
||||
git add apps/lib/features/auth/ui/screens/register_verification_screen.dart
|
||||
git commit -m "feat(auth): add toast feedback for code sending"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 6: 集成测试与验收
|
||||
|
||||
**Step 1: 运行全部测试**
|
||||
|
||||
Run: `cd apps && flutter test`
|
||||
Expected: All tests PASS
|
||||
|
||||
**Step 2: 手动验收清单**
|
||||
|
||||
- [ ] 点击"下一步"后立即跳转到验证码界面
|
||||
- [ ] 验证码界面显示倒计时按钮 "60 s" ... "1 s"
|
||||
- [ ] 倒计时期间按钮禁用
|
||||
- [ ] 倒计时结束后显示"重新发送",可点击
|
||||
- [ ] 点击重发后重新开始 60 秒倒计时
|
||||
- [ ] 发送失败时 Toast 提示错误
|
||||
|
||||
**Step 3: Final commit**
|
||||
|
||||
```bash
|
||||
git add -A
|
||||
git commit -m "feat(auth): complete register verification UX optimization"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 文件变更汇总
|
||||
|
||||
| 文件 | 变更 |
|
||||
|------|------|
|
||||
| `register_cubit.dart` | 添加 `isSending` 状态、`sendCodeSilently` 方法 |
|
||||
| `register_screen.dart` | 乐观跳转,后台发送 |
|
||||
| `register_verification_screen.dart` | Timer 倒计时、按钮样式、Toast 提示 |
|
||||
| `register_cubit_test.dart` | 新增 `sendCodeSilently` 测试 |
|
||||
@@ -1,190 +0,0 @@
|
||||
# RESTful API 全面重构设计
|
||||
|
||||
**日期**: 2026-02-26
|
||||
**状态**: 待实现
|
||||
|
||||
## 目标
|
||||
|
||||
将后端 HTTP API 改造为完全符合 RESTful 规范,并同步更新前端适配代码。
|
||||
|
||||
## 设计原则
|
||||
|
||||
1. **资源导向 URL**:使用名词而非动词
|
||||
2. **HTTP 语义化**:状态通过状态码表达,响应体只包含必要数据
|
||||
3. **响应格式统一**:成功响应无冗余字段,错误响应使用 RFC 7807
|
||||
4. **文档同步**:路由变更必须同步更新文档
|
||||
|
||||
## URL 重构
|
||||
|
||||
### Auth 相关
|
||||
|
||||
| 功能 | 当前 URL | 新 URL | HTTP 方法 |
|
||||
|------|----------|--------|-----------|
|
||||
| 注册发起 | `/signup/start` | `/verifications` | POST |
|
||||
| 重发验证码 | `/signup/resend` | `/verifications/resend` | POST |
|
||||
| 验证码校验 | `/signup/verify` | `/verifications/verify` | POST |
|
||||
| 登录 | `/login` | `/sessions` | POST |
|
||||
| 刷新 Token | `/refresh` | `/sessions/refresh` | POST |
|
||||
| 登出 | `/logout` | `/sessions` | DELETE |
|
||||
| 按邮箱查用户 | `/users/by-email` | `/users?email=xxx` | GET |
|
||||
|
||||
### Profile → Users 统一
|
||||
|
||||
| 功能 | 当前 URL | 新 URL | HTTP 方法 |
|
||||
|------|----------|--------|-----------|
|
||||
| 获取个人信息 | `/profile/me` | `/users/me` | GET |
|
||||
| 更新个人信息 | `/profile/me` | `/users/me` | PATCH |
|
||||
| 按用户名查用户 | `/profile/{username}` | `/users/{username}` | GET |
|
||||
|
||||
### Agent Chat
|
||||
|
||||
| 功能 | 当前 URL | 新 URL | HTTP 方法 |
|
||||
|------|----------|--------|-----------|
|
||||
| 运行对话 | `/agent-chat/run` | `/agent-chats` | POST |
|
||||
|
||||
## 响应格式
|
||||
|
||||
### 成功响应
|
||||
|
||||
| 端点 | 状态码 | 响应体 |
|
||||
|------|--------|--------|
|
||||
| `POST /verifications` | 202 | `{email: string}` |
|
||||
| `POST /verifications/resend` | 204 | 无 |
|
||||
| `POST /verifications/verify` | 200 | `{access_token, refresh_token, expires_in, token_type, user}` |
|
||||
| `POST /sessions` | 200 | 同上 |
|
||||
| `POST /sessions/refresh` | 200 | 同上 |
|
||||
| `DELETE /sessions` | 204 | 无 |
|
||||
| `GET /users?email=xxx` | 200 | `{id, email, created_at, email_confirmed_at?}` |
|
||||
| `GET /users/me` | 200 | `{id, username, avatar_url?, bio?}` |
|
||||
| `PATCH /users/me` | 200 | 同上 |
|
||||
| `GET /users/{username}` | 200 | 同上 |
|
||||
| `POST /agent-chats` | 200 | `{session_id, output, events[]}` |
|
||||
|
||||
### 错误响应
|
||||
|
||||
保持 RFC 7807 格式(已实现):
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "about:blank",
|
||||
"title": "Unauthorized",
|
||||
"status": 401,
|
||||
"detail": "Invalid verification code",
|
||||
"instance": "/api/v1/verifications/verify"
|
||||
}
|
||||
```
|
||||
|
||||
前端解析优先级:`detail` → `message` → `error` → "请求失败"
|
||||
|
||||
## Schema 定义
|
||||
|
||||
### 请求 Schema
|
||||
|
||||
```python
|
||||
# verifications
|
||||
class VerificationCreateRequest(BaseModel):
|
||||
username: str = Field(min_length=3, max_length=30)
|
||||
email: EmailStr
|
||||
password: str = Field(min_length=6)
|
||||
redirect_to: str | None = None
|
||||
|
||||
class VerificationResendRequest(BaseModel):
|
||||
email: EmailStr
|
||||
|
||||
class VerificationVerifyRequest(BaseModel):
|
||||
email: EmailStr
|
||||
token: str = Field(pattern=r"^\d{6}$")
|
||||
|
||||
# sessions
|
||||
class SessionCreateRequest(BaseModel):
|
||||
email: EmailStr
|
||||
password: str = Field(min_length=6)
|
||||
|
||||
class SessionRefreshRequest(BaseModel):
|
||||
refresh_token: str = Field(min_length=1)
|
||||
|
||||
class SessionDeleteRequest(BaseModel):
|
||||
refresh_token: str = Field(min_length=1)
|
||||
|
||||
# users
|
||||
class UserUpdateRequest(BaseModel):
|
||||
username: str | None = Field(default=None, min_length=3, max_length=30)
|
||||
avatar_url: str | None = None
|
||||
bio: str | None = Field(default=None, max_length=200)
|
||||
```
|
||||
|
||||
### 响应 Schema
|
||||
|
||||
```python
|
||||
class VerificationCreateResponse(BaseModel):
|
||||
email: EmailStr
|
||||
|
||||
class SessionResponse(BaseModel):
|
||||
access_token: str
|
||||
refresh_token: str
|
||||
expires_in: int
|
||||
token_type: str
|
||||
user: AuthUser
|
||||
|
||||
class UserResponse(BaseModel):
|
||||
id: str
|
||||
username: str
|
||||
avatar_url: str | None = None
|
||||
bio: str | None = None
|
||||
|
||||
class UserByEmailResponse(BaseModel):
|
||||
id: str
|
||||
email: EmailStr
|
||||
created_at: str
|
||||
email_confirmed_at: str | None = None
|
||||
```
|
||||
|
||||
## 文件改动清单
|
||||
|
||||
### 后端
|
||||
|
||||
| 文件 | 改动 |
|
||||
|------|------|
|
||||
| `v1/auth/router.py` | URL 重命名,简化响应 |
|
||||
| `v1/auth/schemas.py` | 重命名/简化模型 |
|
||||
| `v1/auth/gateway.py` | 适配新 schema |
|
||||
| `v1/auth/service.py` | 适配新 schema |
|
||||
| `v1/profile/` → `v1/users/` | 目录重命名 |
|
||||
| `v1/users/router.py` | URL 更新 |
|
||||
| `v1/users/schemas.py` | 重命名模型 |
|
||||
| `v1/agent_chat/router.py` | URL 更新 |
|
||||
| `v1/router.py` | 更新路由注册 |
|
||||
| 测试文件 | 同步更新 |
|
||||
|
||||
### 前端
|
||||
|
||||
| 文件 | 改动 |
|
||||
|------|------|
|
||||
| `auth/data/auth_api.dart` | URL 更新 |
|
||||
| `auth/data/models/auth_response.dart` | 简化 SignupStartResponse,删除 SignupResendResponse |
|
||||
| `profile/data/profile_api.dart` | 改为 `users/data/users_api.dart` |
|
||||
| `profile/data/models/profile_response.dart` | 重命名 |
|
||||
| 相关 cubit/screen | 适配新 API |
|
||||
|
||||
### 文档
|
||||
|
||||
| 文件 | 改动 |
|
||||
|------|------|
|
||||
| `docs/runtime/runtime-route.md` | 新增,记录所有路由 |
|
||||
| `AGENTS.md` | 添加路由文档同步规则 |
|
||||
|
||||
## 风险与缓解
|
||||
|
||||
| 风险 | 缓解措施 |
|
||||
|------|----------|
|
||||
| 前后端不同步导致功能失效 | 同时修改,一次性提交 |
|
||||
| 测试覆盖不足 | 先更新测试,确保通过后再改实现 |
|
||||
| 遗漏某个调用点 | 全局搜索旧 URL 字符串 |
|
||||
|
||||
## 验收标准
|
||||
|
||||
- [ ] 所有后端测试通过
|
||||
- [ ] 所有前端测试通过
|
||||
- [ ] 手动验证注册/登录/登出流程
|
||||
- [ ] `runtime-route.md` 包含所有端点文档
|
||||
- [ ] `AGENTS.md` 包含路由同步规则
|
||||
File diff suppressed because it is too large
Load Diff
@@ -4,6 +4,27 @@
|
||||
**Author:** AI Assistant
|
||||
**Status:** Draft
|
||||
|
||||
## 枚举存储约定
|
||||
|
||||
**统一使用枚举名称(字符串)存储,不使用整数值。**
|
||||
|
||||
- 数据库层:`VARCHAR(20)` + `CHECK` 约束
|
||||
- 代码层:Python `Enum` 类继承 `str`
|
||||
- 优势:调试可读、易扩展(新增枚举值无需迁移旧数据)、ORM 友好
|
||||
|
||||
```python
|
||||
class AgentType(str, Enum):
|
||||
INTENT_RECOGNITION = "INTENT_RECOGNITION"
|
||||
TASK_EXECUTION = "TASK_EXECUTION"
|
||||
RESULT_REPORTING = "RESULT_REPORTING"
|
||||
```
|
||||
|
||||
```sql
|
||||
-- Migration
|
||||
ALTER TABLE user_agents ADD CONSTRAINT chk_agent_type
|
||||
CHECK (agent_type IN ('INTENT_RECOGNITION', 'TASK_EXECUTION', 'RESULT_REPORTING'));
|
||||
```
|
||||
|
||||
## Overview
|
||||
|
||||
本方案面向 `social-app` 的下一阶段功能升级,重设计 PostgreSQL 数据模型,统一支持用户专属 agent、好友/群组协作、待处理消息、设置、可订阅且可授权编辑的日程事项、待办联动与自动化定时任务。目标是在 FastAPI + Flutter 协作场景下提供长期稳定的数据基础,降低后续 API 演进和跨端同步复杂度。
|
||||
@@ -23,7 +44,7 @@
|
||||
- [x] 性能:核心读路径(inbox 列表、待办列表、事项列表)P95 < 150ms(单用户典型数据量)
|
||||
- [x] 安全:权限以后端业务授权为准;数据库层保留 RLS 防御边界
|
||||
- [x] 一致性:关键写路径(好友状态、权限变更、任务触发)使用事务保障
|
||||
- [x] 可演进:支持旧表迁移、双写与灰度切换
|
||||
- [x] 可演进:当前阶段采用重建库快速迭代;后续稳定后切换为增量迁移与灰度
|
||||
|
||||
## Technical Approach
|
||||
|
||||
@@ -33,22 +54,23 @@
|
||||
|
||||
| Decision | Rationale |
|
||||
|----------|-----------|
|
||||
| 用户与 agent 采用 1:1 主约束 + 可扩展结构 | 当前满足“每用户专属 agent”,未来允许多 agent 形态演进 |
|
||||
| 用户与 agent 采用 1:1 主约束 + 可扩展结构 | 当前满足"每用户专属 agent",未来允许多 agent 形态演进 |
|
||||
| 记忆系统采用单表 + memory_type 区分 | user 类型可选 agent_id,work 类型必须绑定 agent_id |
|
||||
| 好友关系用单表双向规范化表示 | 避免 A-B / B-A 重复,降低去重成本 |
|
||||
| 事项权限采用 ACL 表而非仅 owner | 满足“仅特定人可修改”的协作场景 |
|
||||
| 待办采用单表 + JSONB 来源数组 | 一张表搞定待办,source_ids 存储关联日程事件 |
|
||||
| 自动化采用 Jobs + Runs 双表 | 只支持 daily/weekly 两种循环,active/disabled 两种状态 |
|
||||
| 待办采用主表 + 关联表 | `todos` + `todo_sources` 保证来源关系可校验 |
|
||||
| 自动化采用 Jobs 单表 + Sessions 关联 | `sessions` 通过 `session_type + job_id` 区分普通对话与自动化运行 |
|
||||
| inbox 采用单表接收者视角 | 发送者 + 消息类型 + 关联业务,一表搞定待处理消息 |
|
||||
|
||||
## A. 设计原则与边界
|
||||
|
||||
### 1) 核心实体与聚合边界
|
||||
- 用户聚合:`profiles`(含 settings JSONB), `user_agents`
|
||||
- 用户聚合:`profiles`(含 settings JSONB), `user_agents`, `memories`
|
||||
- 社交聚合:`friendships`, `groups`, `group_members`
|
||||
- 协作事项聚合:`schedule_items`, `schedule_subscriptions`
|
||||
- 协作事项聚合:`schedule_items`, `schedule_subscriptions`(当前仅用户主体)
|
||||
- 消息聚合:`inbox_messages`
|
||||
- 待办聚合:`todos`
|
||||
- 自动化聚合:`automation_jobs`, `automation_runs`
|
||||
- 自动化聚合:`automation_jobs`
|
||||
|
||||
### 2) 一致性分级
|
||||
- 强一致(同事务):好友关系状态迁移、群组成员角色变更、事项权限写入、定时任务抢占执行
|
||||
@@ -63,6 +85,8 @@
|
||||
### 实体与关系
|
||||
- `auth.users (1) - (1) profiles`(settings 作为 JSONB 内嵌)
|
||||
- `auth.users (1) - (1) user_agents`
|
||||
- `auth.users (1) - (N) memories`
|
||||
- `user_agents (1) - (N) memories`(work 类型)
|
||||
- `auth.users (N) - (N) auth.users` 通过 `friendships`
|
||||
- `auth.users (1) - (N) groups`(创建者)
|
||||
- `groups (1) - (N) group_members`,`auth.users (1) - (N) group_members`
|
||||
@@ -71,7 +95,7 @@
|
||||
- `auth.users (1) - (N) inbox_messages`
|
||||
- `auth.users (1) - (N) todos`
|
||||
- `auth.users (1) - (N) automation_jobs`
|
||||
- `automation_jobs (1) - (N) automation_runs`
|
||||
- `automation_jobs (1) - (N) sessions`(通过 `sessions.job_id` 关联)
|
||||
|
||||
### 关键约束
|
||||
- 唯一性:
|
||||
@@ -79,12 +103,25 @@
|
||||
- `friendships(user_low_id, user_high_id)` 唯一
|
||||
- `group_members(group_id, user_id)` 唯一
|
||||
- `schedule_subscriptions(item_id, subscriber_id)` 唯一
|
||||
- CHECK:
|
||||
- `friendships`: `user_low_id < user_high_id` 且 `user_low_id <> user_high_id`
|
||||
- `schedule_subscriptions`: `permission BETWEEN 0 AND 7`
|
||||
- `memories`: `work` 类型必须有 `agent_id`,`user` 类型必须无 `agent_id`
|
||||
- `sessions`: `session_type/job_id` 组合一致
|
||||
- 外键:统一显式 `ON DELETE` 策略(见下)
|
||||
- 可空性:权限关键字段、状态字段默认 `NOT NULL`
|
||||
- 删除策略:
|
||||
- 用户删除:大部分 `CASCADE`(用户私有数据);跨用户协作数据优先软删
|
||||
- 事项删除:对子表 `CASCADE`;待办保留历史,改 `status = 'archived'`
|
||||
|
||||
### 外键删除策略明细(必做)
|
||||
- `sessions.job_id -> automation_jobs.id`: `ON DELETE RESTRICT`
|
||||
- `todo_sources.todo_id -> todos.id`: `ON DELETE CASCADE`
|
||||
- `todo_sources.schedule_item_id -> schedule_items.id`: `ON DELETE CASCADE`
|
||||
- `inbox_messages.friendship_id -> friendships.id`: `ON DELETE CASCADE`
|
||||
- `inbox_messages.schedule_item_id -> schedule_items.id`: `ON DELETE CASCADE`
|
||||
- `inbox_messages.group_id -> groups.id`: `ON DELETE CASCADE`
|
||||
|
||||
## C. 数据库表设计(PostgreSQL)
|
||||
|
||||
以下为推荐主表(方案 1,规范化优先)。字段示例采用 `UUID + timestamptz + enum/text-check`。
|
||||
@@ -95,13 +132,12 @@
|
||||
- PK: `id UUID` (`auth.users.id`)
|
||||
- 关键字段: `username`, `avatar_url`, `bio`
|
||||
- **新增 JSONB 字段**:
|
||||
- `settings JSONB`(用户自定义设置,含 `preferences`, `privacy`, `notification` 三大块)
|
||||
- `settings_version INTEGER DEFAULT 1`(兼容旧数据的版本字段)
|
||||
- `settings JSONB`(用户自定义设置,含 `version`, `preferences`, `privacy`, `notification` 四大块)
|
||||
- 时间字段: `created_at`, `updated_at`, `deleted_at`
|
||||
- 索引:
|
||||
- `INDEX(username)`(允许重名,仅用于列表查询)
|
||||
- `GIN(settings)`(支持 JSONB 表达式查询)
|
||||
- 表达式索引:`(settings->>'notification_enabled')`(按需,对高频查询字段单独建)
|
||||
- 表达式索引:`(settings->'notification'->>'enabled')`(按需,对高频查询字段单独建)
|
||||
- 审计: `created_by`, `updated_by`(可等于 id)
|
||||
- 删除策略: 用户删除时 `CASCADE`
|
||||
|
||||
@@ -112,7 +148,6 @@
|
||||
- `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`)
|
||||
- 索引:
|
||||
@@ -122,6 +157,38 @@
|
||||
- `GIN(config)`(按需)
|
||||
- 审计: `created_by`, `updated_by`
|
||||
|
||||
#### `memories`
|
||||
- PK: `id UUID`
|
||||
- 关键字段:
|
||||
- `owner_id`(用户,NOT NULL)
|
||||
- `agent_id`(work 类型时必需)
|
||||
- `memory_type`(枚举:`user | work`)
|
||||
- `title`
|
||||
- `content`(JSONB,存储具体记忆结构)
|
||||
- `source`(`manual | agent | imported`)
|
||||
- 时间字段: `created_at`, `updated_at`
|
||||
- 状态字段: `status`(`active | disabled`)
|
||||
- 索引:
|
||||
- `INDEX(owner_id, memory_type, status)`
|
||||
- `INDEX(agent_id, memory_type, status)`
|
||||
- `GIN(content)`(支持 JSONB 内容查询)
|
||||
- 约束: `CHECK ((memory_type = 'work' AND agent_id IS NOT NULL) OR (memory_type = 'user' AND agent_id IS NULL))`
|
||||
|
||||
**memory_type 说明**:
|
||||
| 类型 | agent_id | 说明 |
|
||||
|------|----------|------|
|
||||
| `user` | 可空 | 用户记忆:偏好、背景信息、实体等 |
|
||||
| `work` | 必需 | 工作记忆:长期运行后对工作流程的经验整理,避免重复错误 |
|
||||
|
||||
**content JSONB 示例**:
|
||||
```json
|
||||
// 用户记忆
|
||||
{"type": "preference", "data": {"style": "concise", "language": "zh-CN"}}
|
||||
|
||||
// 工作记忆
|
||||
{"type": "workflow_summary", "data": {"task": "代码审查", "learnings": ["优先检查安全漏洞", "关注性能热点"], "improvements": []}}
|
||||
```
|
||||
|
||||
### 2) 社交关系
|
||||
|
||||
#### `friendships`
|
||||
@@ -148,17 +215,17 @@
|
||||
|
||||
#### `groups`
|
||||
- PK: `id UUID`
|
||||
- 关键字段: `name`, `description`, `visibility`, `owner_id`
|
||||
- 关键字段: `name`, `description`, `owner_id`
|
||||
- 时间字段: `created_at`, `updated_at`, `deleted_at`
|
||||
- 状态字段: `status`(`active|archived`)
|
||||
- 索引: `INDEX(owner_id, status)`, `INDEX(visibility)`
|
||||
- 索引: `INDEX(owner_id, status)`
|
||||
- 审计: `created_by`, `updated_by`
|
||||
|
||||
#### `group_members`
|
||||
- PK: `id UUID`
|
||||
- 关键字段:
|
||||
- `group_id`, `user_id`
|
||||
- `role`(枚举:`creator` | `admin` | `member`)
|
||||
- `role`(枚举:`owner` | `admin` | `member`)
|
||||
- `join_source`(`invited|joined`)
|
||||
- `invited_by`, `joined_at`
|
||||
- 时间字段: `created_at`, `updated_at`, `removed_at`
|
||||
@@ -170,14 +237,13 @@
|
||||
- 审计: `created_by`, `updated_by`
|
||||
|
||||
**role 说明**:
|
||||
| role | 含义 | 创建事项时默认给群组的权限 |
|
||||
|------|------|---------------------------|
|
||||
| `creator` | 群主/创建者 | `["view", "invite", "edit"]` |
|
||||
| `admin` | 管理员 | `["view", "invite"]` |
|
||||
| `member` | 普通成员 | `["view"]` |
|
||||
| role | 含义 |
|
||||
|------|------|
|
||||
| `owner` | 群主/创建者 |
|
||||
| `admin` | 管理员 |
|
||||
| `member` | 普通成员 |
|
||||
|
||||
- 角色可升降:服务层变更 role 字段即可
|
||||
- 角色决定了该用户在群里创建的日程事项默认授予群组的权限(见下方映射)
|
||||
|
||||
### 3) 用户设置(已合并至 profiles 表)
|
||||
|
||||
@@ -187,27 +253,15 @@
|
||||
{
|
||||
"version": 1,
|
||||
"preferences": {
|
||||
"theme": "dark",
|
||||
"language": "zh-CN",
|
||||
"interface_language": "zh-CN",
|
||||
"ai_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"
|
||||
}
|
||||
"privacy": {},
|
||||
"notification": {}
|
||||
}
|
||||
```
|
||||
|
||||
- 扩展方式:新增字段时递增 `settings_version`,应用层做 schema 兼容
|
||||
- 索引策略:对高频查询字段(如 `notification.enabled`)使用表达式索引
|
||||
- 索引策略:对高频查询字段使用表达式索引
|
||||
- 更新方式:服务层使用 JSONB merge 或字段级 UPDATE,避免读-改-写并发问题(建议用 `jsonb_set` 原子操作)
|
||||
|
||||
### 4) 事项与订阅/权限
|
||||
@@ -220,38 +274,82 @@
|
||||
- `description`
|
||||
- `start_at`
|
||||
- `end_at`
|
||||
- `timezone`
|
||||
- `timezone`(用于将日程时间转换为用户本地时间显示)
|
||||
- `metadata`(JSONB,扩展字段)
|
||||
- `recurrence_rule`(可选,支持循环日程)
|
||||
- `source_type`(`manual | imported | agent_generated`)
|
||||
- 时间字段: `created_at`, `updated_at`
|
||||
- 状态字段: `status`(`active | completed | canceled`)
|
||||
- 状态字段: `status`(`active | completed | canceled | archived`)
|
||||
- 索引:
|
||||
- `INDEX(owner_id, start_at)`
|
||||
- `INDEX(status, start_at)`
|
||||
- 审计: `created_by`
|
||||
|
||||
**metadata JSONB 示例**:
|
||||
```json
|
||||
{
|
||||
"color": "#FF6B6B",
|
||||
"location": "会议室A",
|
||||
"notes": "记得提前准备投影仪",
|
||||
"attachments": [
|
||||
{
|
||||
"name": "会议纪要.pdf",
|
||||
"url": "https://...",
|
||||
"visible_to": [],
|
||||
"type": "document"
|
||||
},
|
||||
{
|
||||
"name": "投影仪提醒",
|
||||
"visible_to": ["uuid1"],
|
||||
"type": "reminder",
|
||||
"content": "记得带投影仪"
|
||||
},
|
||||
{
|
||||
"name": "技术方案.docx",
|
||||
"url": "https://...",
|
||||
"visible_to": ["uuid2"],
|
||||
"type": "document",
|
||||
"note": "需要他确认预算"
|
||||
}
|
||||
],
|
||||
"version": 1
|
||||
}
|
||||
```
|
||||
|
||||
| type | 说明 | 特殊字段 |
|
||||
|------|------|----------|
|
||||
| document | 文档/文件 | url, note |
|
||||
| reminder | 提醒 | content |
|
||||
|
||||
#### `schedule_subscriptions`
|
||||
- PK: `id UUID`
|
||||
- 关键字段:
|
||||
- `item_id`
|
||||
- `subscriber_id`
|
||||
- `permission`(JSONB 数组,权柄组合:`["view"]` / `["view", "edit"]` / `["view", "invite", "edit"]`)
|
||||
- `notify_level`(`all | mentions | none`)
|
||||
- `permission`(INTEGER,用位运算存储权限组合,`NOT NULL DEFAULT 1`)
|
||||
- `notify_level`(`all | mentions | none`,`NOT NULL DEFAULT 'all'`)
|
||||
- 时间字段: `created_at`
|
||||
- 状态字段: `status`(`active | paused | unsubscribed`)
|
||||
- 状态字段: `status`(`active | paused | unsubscribed`,`NOT NULL DEFAULT 'active'`)
|
||||
- 约束: `UNIQUE(item_id, subscriber_id)`
|
||||
- 约束补充: `CHECK(permission BETWEEN 0 AND 7)`(`view=1, invite=2, edit=4`,`0` 表示无权限)
|
||||
- 索引: `INDEX(subscriber_id, status)`, `INDEX(item_id, status)`
|
||||
- 审计: `created_by`
|
||||
|
||||
**权柄说明**:
|
||||
| 权柄 | 含义 |
|
||||
|------|------|
|
||||
| `view` | 查看事项详情 |
|
||||
| `invite` | 邀请其他人订阅此事项 |
|
||||
| `edit` | 修改事项内容、管理订阅 |
|
||||
**权柄说明(位运算)**:
|
||||
| 权柄 | 值 | 二进制 | 说明 |
|
||||
|------|-----|--------|------|
|
||||
| view | 1 | 001 | 查看事项详情 |
|
||||
| invite | 2 | 010 | 邀请其他人订阅此事项 |
|
||||
| edit | 4 | 100 | 修改事项内容、管理订阅 |
|
||||
|
||||
- 事项 owner 默认拥有全部权柄:`["view", "invite", "edit"]`
|
||||
- 权限变更使用 JSONB 原子操作:`UPDATE ... SET permission = permission || '["edit"]'::jsonb`
|
||||
- 权限检查:`permission & 2 = 2` 检查是否有 invite 权限
|
||||
- 权限添加:`permission | 2` 添加 invite 权限
|
||||
- 事项 owner 默认拥有全部权柄:`7`(111)
|
||||
- owner 权柄由服务层恒等判定为 `7`,不依赖 owner 是否在 `schedule_subscriptions` 中存在记录
|
||||
|
||||
**当前版本边界**:
|
||||
- `schedule_subscriptions` 仅支持用户订阅(`subscriber_id -> auth.users.id`)
|
||||
- 事项协作暂不引入群主体授权
|
||||
|
||||
### 5) 待处理消息(Inbox)
|
||||
|
||||
@@ -259,19 +357,35 @@
|
||||
- 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`)
|
||||
- `sender_id`(发送者,系统消息可为 NULL)
|
||||
- `message_type`(枚举:`friend_request | calendar | system | group`)
|
||||
- `friendship_id`(可空,`friend_request` 时必填)
|
||||
- `schedule_item_id`(可空,`calendar` 时必填)
|
||||
- `group_id`(可空,`group` 时必填)
|
||||
- `content`(TEXT,消息内容,系统消息用)
|
||||
- 时间字段: `created_at`
|
||||
- 状态字段:
|
||||
- `is_read`(BOOLEAN,是否已读)
|
||||
- `status`(`pending | accepted | rejected | dismissed`)
|
||||
- 索引:
|
||||
- `INDEX(recipient_id, status, created_at DESC)`
|
||||
- 部分索引 `INDEX(recipient_id, created_at DESC) WHERE status='pending'`
|
||||
- 审计: `created_by`
|
||||
|
||||
**说明**:一张表搞定,接收者视角,只关心谁发的、什么类型、关联什么业务对象。
|
||||
**message_type 与业务字段对应关系**:
|
||||
| message_type | 对应业务字段 |
|
||||
|--------------|-----------------|
|
||||
| friend_request | friendship_id -> friendships.id |
|
||||
| calendar | schedule_item_id -> schedule_items.id |
|
||||
| system | 三个业务字段均为 NULL(内容直接在 content) |
|
||||
| group | group_id -> groups.id |
|
||||
|
||||
**说明**:一张表搞定,接收者视角,通过 `message_type + 对应业务字段` 直接定位要处理的业务,避免单列多态外键带来的引用不一致问题。
|
||||
|
||||
**一致性约束(必做)**:
|
||||
- 使用 `CHECK` 保证不同 `message_type` 下仅允许对应业务字段非空(`system` 时业务字段全空)
|
||||
- 使用 `CHECK` 保证 `message_type='system'` 时 `sender_id IS NULL`,否则 `sender_id IS NOT NULL`
|
||||
- `friendship_id`、`schedule_item_id`、`group_id` 分别建立 FK,并显式声明 `ON DELETE` 策略
|
||||
|
||||
### 6) 待办
|
||||
|
||||
@@ -282,8 +396,7 @@
|
||||
- `title`
|
||||
- `description`
|
||||
- `due_at`
|
||||
- `priority`(枚举:`low | medium | high | urgent`)
|
||||
- `source_ids`(JSONB 数组,关联的日程事件 ID,`[]` 表示手动创建)
|
||||
- `priority`(INTEGER,用于四象限:1=重要且紧急, 2=重要不紧急, 3=紧急不重要, 4=不重要不紧急)
|
||||
- 时间字段: `created_at`, `completed_at`
|
||||
- 状态字段: `status`(`pending | done | canceled`)
|
||||
- 索引:
|
||||
@@ -292,241 +405,247 @@
|
||||
- 部分索引 `INDEX(owner_id, due_at) WHERE status='pending'`
|
||||
- 审计: `created_by`
|
||||
|
||||
#### `todo_sources`
|
||||
- PK: `id UUID`
|
||||
- 关键字段:
|
||||
- `todo_id`(FK -> todos.id)
|
||||
- `schedule_item_id`(FK -> schedule_items.id)
|
||||
- 时间字段: `created_at`
|
||||
- 约束: `UNIQUE(todo_id, schedule_item_id)`
|
||||
- 索引: `INDEX(todo_id)`, `INDEX(schedule_item_id)`
|
||||
|
||||
**说明**:
|
||||
- 手动创建待办:不写 `todo_sources`
|
||||
- 从事项提取待办:写入 `todo_sources`,替代 JSONB 数组,保证来源关系可校验
|
||||
|
||||
### 7) 自动化定时任务
|
||||
|
||||
#### `automation_jobs`
|
||||
- PK: `id UUID`
|
||||
- 关键字段:
|
||||
- `owner_id`
|
||||
- `name`
|
||||
- `job_type`
|
||||
- `target_type`
|
||||
- `target_id`
|
||||
- `params`(JSONB)
|
||||
- `title`(任务标题)
|
||||
- `prompt`(AI 执行的 prompt)
|
||||
- `schedule_type`(枚举:`daily | weekly`)
|
||||
- `schedule_time`(时间,如 `09:00` 表示每天/每周几点执行)
|
||||
- `run_at`(首次运行时间)
|
||||
- `next_run_at`(下次运行时间,调度器扫描主字段)
|
||||
- `timezone`(时区,如 `Asia/Shanghai`)
|
||||
- `last_run_at`(最近运行时间,可空)
|
||||
- 时间字段: `created_at`, `updated_at`
|
||||
- 状态字段: `status`(`active | disabled`)
|
||||
- 索引: `INDEX(owner_id, status)`
|
||||
- 索引: `INDEX(owner_id, status)`, `INDEX(status, next_run_at)`
|
||||
- 约束补充: `UNIQUE(id, owner_id)`(用于 `sessions(job_id, user_id)` 归属一致性外键)
|
||||
- 审计: `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)`
|
||||
**说明**:定时任务执行时,在 sessions 表创建记录存储 AI 对话内容。
|
||||
|
||||
### 8) 会话表扩展(已有 sessions)
|
||||
|
||||
#### `sessions`(更新)
|
||||
- 新增字段:
|
||||
- `session_type`(`chat | automation`)
|
||||
- `job_id`(可空,FK -> automation_jobs.id)
|
||||
- 一致性约束:
|
||||
- `CHECK((session_type = 'chat' AND job_id IS NULL) OR (session_type = 'automation' AND job_id IS NOT NULL))`
|
||||
- 通过复合 FK 约束归属一致性:`FOREIGN KEY(job_id, user_id) -> automation_jobs(id, owner_id)`
|
||||
- 索引:
|
||||
- `INDEX(user_id, session_type, last_activity_at DESC)`
|
||||
- `INDEX(job_id)`
|
||||
|
||||
## D. 权限与协作模型
|
||||
|
||||
### 1) 事项权限落表
|
||||
- 权限直接存储在 `schedule_subscriptions.permission` JSONB 数组中
|
||||
- 权限直接存储在 `schedule_subscriptions.permission` 整数中(位运算)
|
||||
- owner 不写入 `schedule_subscriptions`,owner 权限仅由 `schedule_items.owner_id` 推导
|
||||
- 权限决策顺序:
|
||||
1. `schedule_items.owner_id` → 全部权柄 `["view", "invite", "edit"]`
|
||||
2. `schedule_subscriptions` 中该用户的 `permission` 数组
|
||||
3. 默认只有 `["view"]`
|
||||
1. `schedule_items.owner_id` → 服务层恒等全部权柄 `["view", "invite", "edit"]`(7)
|
||||
2. `schedule_subscriptions` 中该用户的 `permission` 位图
|
||||
3. 非 owner 且非 subscriber 默认无权限(0)
|
||||
|
||||
### 2) 群组与事项权限
|
||||
- 群成员在事项中的权限通过 `schedule_subscriptions.subject_type = 'group'` 关联
|
||||
- 群角色决定默认权限:
|
||||
- `creator` → `["view", "invite", "edit"]`
|
||||
- `admin` → `["view", "invite"]`
|
||||
- `member` → `["view"]`
|
||||
### 2) 当前版本边界
|
||||
- 事项权限仅处理用户主体(owner + subscriber)
|
||||
- 群组与事项权限继承关系不在本期范围
|
||||
|
||||
## E. 消息与待办联动
|
||||
|
||||
### 1) inbox 关联业务对象
|
||||
- `inbox_messages.message_type` 枚举:
|
||||
- `friend_request`(好友请求)
|
||||
- `group_invitation`(群组邀请)
|
||||
- `schedule_item_shared`(日程事项分享)
|
||||
- 通过 `related_type` / `related_id` 关联业务对象
|
||||
- `friend_request`(好友请求)→ `friendship_id` 指向 friendships
|
||||
- `calendar`(日程邀请)→ `schedule_item_id` 指向 schedule_items
|
||||
- `system`(系统消息)→ 业务字段均为 NULL
|
||||
- `group`(群组邀请)→ `group_id` 指向 groups
|
||||
- 通过 `message_type + 对应业务字段` 直接定位业务对象,并用 `CHECK` 约束保证字段一致性
|
||||
|
||||
### 2) 待办来源提取
|
||||
- 从事项提取待办时,将日程事件 ID 存入 `todos.source_ids` JSONB 数组
|
||||
- 手动创建的待办 `source_ids = []`
|
||||
- 支持多来源:同一待办可关联多个日程事项 `source_ids = [<id1>, <id2>]`
|
||||
- 从事项提取待办时,写入 `todo_sources(todo_id, schedule_item_id)`
|
||||
- 手动创建的待办不写 `todo_sources`
|
||||
- 支持多来源:同一待办可关联多个日程事项(多行 `todo_sources`)
|
||||
- 待办完成时无需反向更新来源事项状态(简化设计)
|
||||
|
||||
## F. 定时任务模型
|
||||
|
||||
### 1) 调度规则
|
||||
- `schedule_type` 枚举:`daily`(每日) | `weekly`(每周)
|
||||
- `schedule_time` 格式:`HH:MM`(如 `09:00` 表示每天/每周几点执行)
|
||||
- 调度器扫描 `status='active'` 的任务,按 `schedule_type + schedule_time` 计算下次执行时间
|
||||
- `run_at` 用于首次执行时间,`next_run_at` 用于后续调度
|
||||
- 调度器扫描 `status='active' AND next_run_at <= now()` 的任务,执行后回写下一次 `next_run_at`
|
||||
- `timezone` 参与下一次执行时间计算,避免时区偏差
|
||||
|
||||
### 2) 执行记录
|
||||
- 每次执行生成 `automation_runs` 记录
|
||||
- 状态流转:`queued` → `running` → `succeeded` | `failed`
|
||||
- 失败重试:`attempt` 字段记录当前重试次数(需业务确认最大重试次数)
|
||||
- 每次执行在 sessions 表创建记录,通过 `sessions.job_id` 关联 job
|
||||
- `sessions` 通过 `session_type` 区分 `chat` 与 `automation`
|
||||
- 执行失败时记录在 `automation_jobs`(如 `last_error`,可后续细化)
|
||||
|
||||
## G. 演进与迁移计划(从旧表到新模型)
|
||||
## G. 数据库迁移思路
|
||||
|
||||
### Phase 1: 基础并行建模(8-12 小时)
|
||||
1. 新建核心表(不删旧表):`user_agents`, `friendships`, `groups`, `group_members`
|
||||
2. 对 `profiles` 表新增 `settings JSONB`, `settings_version` 字段
|
||||
3. 建立最小外键和索引,启用 RLS deny-all 策略
|
||||
4. 提供只写新表的影子接口(内部开关)
|
||||
### 策略:重建数据库 + Alembic ORM 迁移
|
||||
|
||||
### 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. 指标稳定后停止旧表写入,保留只读回滚窗口
|
||||
**执行门禁(强制)**:
|
||||
- 仅允许在本地开发环境执行
|
||||
- 禁止在生产/共享环境执行 `rm backend/alembic/versions/*.py`
|
||||
- 执行前必须备份数据库或创建 git tag
|
||||
|
||||
### Phase 4: 收尾与清理(4-8 小时)
|
||||
1. 下线旧读路径和旧双写逻辑
|
||||
2. 保留旧表冷备份后归档/删除
|
||||
3. 固化运行手册与告警阈值
|
||||
1. **删除所有旧 migration 脚本**(保留 `env.py`)
|
||||
2. **创建 ORM 模型文件**
|
||||
3. **生成 Alembic migration**
|
||||
4. **重建数据库并执行迁移**
|
||||
|
||||
### 回滚策略
|
||||
- 任意阶段回滚:读切回旧表 + 关闭新表写开关
|
||||
- 双写阶段故障:保留操作日志,可按时间窗重放补偿
|
||||
- 最终切换前必须满足:新旧关键查询结果偏差 < 0.1%
|
||||
### 执行步骤
|
||||
|
||||
## H. 两套方案对比
|
||||
1. 删除旧 migration 文件
|
||||
```bash
|
||||
rm backend/alembic/versions/*.py
|
||||
```
|
||||
|
||||
### 方案 1(推荐):规范化、可维护性优先
|
||||
- 特点:按领域拆表,ACL 与来源映射独立,严格约束
|
||||
- 优点:一致性好、权限边界清晰、长期演进成本低
|
||||
- 缺点:联表较多,初期 API 复杂度较高
|
||||
2. 重建空数据库(确保以空库基线生成 initial migration)
|
||||
```bash
|
||||
docker compose --env-file .env -f infra/docker/docker-compose.yml down -v
|
||||
docker compose --env-file .env -f infra/docker/docker-compose.yml up -d db
|
||||
```
|
||||
|
||||
### 方案 2:开发效率优先、适度反规范化
|
||||
- 特点:将部分结构合并为 JSONB(如 `permissions_snapshot`, `todo_origin`)
|
||||
- 优点:开发快、迁移初期改动小
|
||||
- 缺点:约束弱、查询和审计困难、后续重构成本高
|
||||
- 注:`user_settings` 已按此思路内嵌至 profiles,其他模块仍建议保持规范化
|
||||
3. 创建 ORM 模型文件(参考 `models/` 目录结构)
|
||||
- 新增:`user_agents.py`, `memories.py`, `friendships.py`, `groups.py`, `group_members.py`, `schedule_items.py`, `schedule_subscriptions.py`, `inbox_messages.py`, `todos.py`, `todo_sources.py`, `automation_jobs.py`
|
||||
- 更新:`profile.py` - 添加 `settings` 字段
|
||||
- 更新:`agent_chat_session.py` - 添加 `session_type`、`job_id` 字段
|
||||
- 重写:`create_profile_for_new_user` 触发器,确保 `profiles.settings` 有默认值
|
||||
|
||||
### 对比矩阵
|
||||
4. 更新 `models/__init__.py` 导出所有模型
|
||||
|
||||
| 维度 | 方案 1(规范化) | 方案 2(反规范化) |
|
||||
|------|------------------|--------------------|
|
||||
| 复杂度 | 中高 | 中 |
|
||||
| 查询性能 | 读热点需索引与缓存优化 | 单行读取快,复杂筛选慢 |
|
||||
| 写入成本 | 中(多表事务) | 低到中 |
|
||||
| 扩展性 | 高 | 中低 |
|
||||
| 风险 | 中(实施复杂) | 中高(数据质量和权限风险) |
|
||||
5. 更新 `alembic/env.py` 添加模型导入
|
||||
|
||||
### 推荐结论
|
||||
- 推荐方案 1:当前业务已包含社交关系、协作权限、跨域联动和自动化调度,若选择反规范化将把复杂性转移到应用层并放大后续维护风险。方案 1 在 FastAPI + PostgreSQL 下更符合长期可维护与可审计目标。
|
||||
6. 生成 initial migration(以空库为对比基线)
|
||||
```bash
|
||||
cd backend && uv run alembic revision --autogenerate -m "initial schema"
|
||||
```
|
||||
|
||||
## I. 交付物
|
||||
7. 为所有新建 `public` 业务表补齐 RLS(`SELECT/INSERT/UPDATE/DELETE` policy)
|
||||
- 每张表都执行 `ENABLE ROW LEVEL SECURITY`
|
||||
- 每张表都显式创建 `SELECT/INSERT/UPDATE/DELETE` policy
|
||||
- downgrade 必须可逆,不得弱化既定安全边界
|
||||
- `anon/authenticated` 默认全部 deny
|
||||
|
||||
### 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. 通过幂等键、部分索引和事务边界保障高并发稳定性。
|
||||
RLS 最小策略矩阵(本期统一模板):
|
||||
- `anon`:`SELECT/INSERT/UPDATE/DELETE` 全部 deny
|
||||
- `authenticated`:`SELECT/INSERT/UPDATE/DELETE` 全部 deny
|
||||
- `service_role`:由后端服务连接使用,不依赖 RLS 放行
|
||||
|
||||
### I-2. 后续 API 设计所需数据契约清单
|
||||
8. 执行迁移
|
||||
```bash
|
||||
cd backend && uv run alembic upgrade head
|
||||
```
|
||||
|
||||
- 用户/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`
|
||||
9. 验证表结构
|
||||
|
||||
### I-3. 最小可行迁移 DDL 清单(按优先级)
|
||||
## H. 交付物
|
||||
|
||||
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 规范化约束)
|
||||
### ORM 模型文件清单
|
||||
|
||||
P1(协作事项)
|
||||
7. `CREATE TABLE schedule_items (...)`
|
||||
8. `CREATE TABLE schedule_subscriptions (...)`
|
||||
9. `CREATE INDEX`(owner/status/time + subscription 查询)
|
||||
| 文件 | 说明 |
|
||||
|------|------|
|
||||
| `models/user_agents.py` | 用户专属 agent |
|
||||
| `models/memories.py` | 用户/工作记忆 |
|
||||
| `models/friendships.py` | 好友关系 |
|
||||
| `models/groups.py` | 群组 |
|
||||
| `models/group_members.py` | 群组成员 |
|
||||
| `models/schedule_items.py` | 日程事项 |
|
||||
| `models/schedule_subscriptions.py` | 日程订阅与权限 |
|
||||
| `models/inbox_messages.py` | 待处理消息 |
|
||||
| `models/todos.py` | 待办 |
|
||||
| `models/todo_sources.py` | 待办与事项来源关联 |
|
||||
| `models/automation_jobs.py` | 定时任务 |
|
||||
| `models/profile.py` | 更新:添加 `settings` 字段 |
|
||||
| `models/agent_chat_session.py` | 更新:添加 `session_type`、`job_id` 字段 |
|
||||
|
||||
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 (...)`
|
||||
1. 删除旧 migration 文件
|
||||
```bash
|
||||
rm backend/alembic/versions/*.py
|
||||
```
|
||||
|
||||
P4(安全与治理)
|
||||
21. 对新增 `public` 表执行 `ALTER TABLE ... ENABLE ROW LEVEL SECURITY`
|
||||
22. 为 `anon, authenticated` 创建默认 deny-all `SELECT/INSERT/UPDATE/DELETE` policy
|
||||
23. 审计字段回填脚本与触发器(如需)
|
||||
2. 重建空数据库(确保以空库基线生成 initial migration)
|
||||
```bash
|
||||
docker compose --env-file .env -f infra/docker/docker-compose.yml down -v
|
||||
docker compose --env-file .env -f infra/docker/docker-compose.yml up -d db
|
||||
```
|
||||
|
||||
## Dependencies
|
||||
3. 创建/更新 ORM 模型文件
|
||||
|
||||
- [ ] PostgreSQL 扩展:`pgcrypto`(UUID/哈希,若已启用可跳过)
|
||||
- [ ] Alembic 迁移体系(现有)
|
||||
- [ ] 后端任务执行器(Celery)用于自动化触发与补偿
|
||||
4. 更新 `models/__init__.py` 导出所有模型
|
||||
|
||||
## Testing Strategy
|
||||
5. 更新 `alembic/env.py` 添加模型导入
|
||||
|
||||
- **Unit Tests:** 状态流转、权限决策、去重哈希、幂等键生成
|
||||
- **Integration Tests:** 迁移升级/回滚、双写一致性、RLS policy 基线、并发抢占执行
|
||||
- **E2E Tests:** 好友请求到 inbox、群组邀请处理、事项订阅变更到待办、自动化任务触发到结果回显
|
||||
6. 生成 initial migration(以空库为对比基线)
|
||||
```bash
|
||||
cd backend && uv run alembic revision --autogenerate -m "initial schema"
|
||||
```
|
||||
|
||||
## Risks & Mitigations
|
||||
7. 为所有新建 `public` 业务表补齐 RLS(`SELECT/INSERT/UPDATE/DELETE` policy)
|
||||
- 每张表都执行 `ENABLE ROW LEVEL SECURITY`
|
||||
- 每张表都显式创建 `SELECT/INSERT/UPDATE/DELETE` policy
|
||||
- downgrade 必须可逆,不得弱化既定安全边界
|
||||
|
||||
| Risk | Impact | Likelihood | Mitigation |
|
||||
|------|--------|------------|------------|
|
||||
| 旧表结构未知导致回填失败 | High | Medium | 先做 schema 探测脚本,按表存在性分支回填 |
|
||||
| 双写期间新旧数据不一致 | High | Medium | 引入操作日志 + 对账任务 + 幂等补偿 |
|
||||
| 权限模型过复杂影响上线进度 | Medium | Medium | 先上线最小权限集(owner/user/group),后续扩展 |
|
||||
| 自动化任务并发冲突 | Medium | Medium | `SKIP LOCKED` + 幂等键 + 乐观锁版本号 |
|
||||
| 索引不足导致移动端列表慢 | Medium | Medium | 先覆盖 P0/P1 热路径索引,监控后增量优化 |
|
||||
8. 执行迁移
|
||||
```bash
|
||||
cd backend && uv run alembic upgrade head
|
||||
```
|
||||
|
||||
## Estimated Effort
|
||||
9. 更新测试文件适配新表结构
|
||||
|
||||
| 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** |
|
||||
## I. 数据库表名规范与审计
|
||||
|
||||
## 需业务确认(关键不确定项)
|
||||
### 1) 命名规范(统一执行)
|
||||
- 使用 `snake_case`
|
||||
- 业务表统一使用复数名词(如 `profiles`, `friendships`, `automation_jobs`)
|
||||
- 关联表使用 `<主实体复数>_<从实体复数>` 或约定俗成复数短语(如 `group_members`, `todo_sources`)
|
||||
- 禁止过于泛化的表名(如 `messages`, `sessions`),必须带业务前缀
|
||||
- 存量历史表允许短期例外,但必须在审计表中登记并给出迁移计划
|
||||
- 缩写保持一致:LLM 统一使用 `llm` 前缀,不混用 `model`/`llm` 两套命名
|
||||
|
||||
1. ~~`profiles.username` 是否允许重名~~(已确认:允许重名,仅建普通索引)。
|
||||
2. ~~group_members.role 权柄组合~~(已确认:JSONB 数组 `["view", "invite", "edit"]`)。
|
||||
3. 是否近期需要"组织/团队"多租户(决定 `tenant_id` 是否立即强制)。
|
||||
4. 事项是否必须绑定群组上下文(`context_group_id` 是否必需)。
|
||||
5. 自动化任务失败重试上限与退避策略(固定/指数)。
|
||||
### 2) 表名审计结果
|
||||
|
||||
| 当前表名 | 审计结论 | 建议表名 | 说明 |
|
||||
|----------|----------|----------|------|
|
||||
| `profiles` | 通过 | - | 符合复数名词规范 |
|
||||
| `user_agents` | 通过 | - | 语义清晰 |
|
||||
| `memories` | 通过 | - | 语义清晰 |
|
||||
| `friendships` | 通过 | - | 关系表命名清晰 |
|
||||
| `groups` | 通过 | - | 符合规范 |
|
||||
| `group_members` | 通过 | - | 关联表命名清晰 |
|
||||
| `schedule_items` | 通过 | - | 语义清晰 |
|
||||
| `schedule_subscriptions` | 通过 | - | 语义清晰 |
|
||||
| `inbox_messages` | 通过 | - | 带业务前缀,避免歧义 |
|
||||
| `todos` | 通过 | - | 简洁且清晰 |
|
||||
| `todo_sources` | 通过 | - | 关联关系明确 |
|
||||
| `automation_jobs` | 通过 | - | 语义清晰 |
|
||||
| `llms` | 通过 | - | 与 LLM 语义一致 |
|
||||
| `llm_factory` | 建议调整 | `llm_factories` | 当前为单数,建议改复数以统一规范 |
|
||||
| `sessions` | 建议调整 | `agent_chat_sessions` | 过于泛化,建议加业务前缀 |
|
||||
| `messages` | 建议调整 | `agent_chat_messages` | 过于泛化,建议加业务前缀 |
|
||||
|
||||
### 3) 落地建议
|
||||
- 本期命名边界:不重命名 `llm_factory/sessions/messages`,仅在新表严格执行命名规范
|
||||
- 本期最小可行:先保持现有表名可运行,新增表全部遵循规范
|
||||
- 下期统一治理:通过一次性迁移将 `llm_factory/sessions/messages` 重命名到规范名
|
||||
- 若本期直接重命名,需同步 ORM 模型、外键、索引、RLS policy 名称与运行文档
|
||||
|
||||
Executable
+17
@@ -0,0 +1,17 @@
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
SESSION_NAME="${SESSION_NAME:-social-dev}"
|
||||
|
||||
echo "=== App Down ==="
|
||||
|
||||
if ! tmux has-session -t "$SESSION_NAME" 2>/dev/null; then
|
||||
echo "No tmux session '$SESSION_NAME' found."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "Stopping tmux session '$SESSION_NAME'..."
|
||||
|
||||
tmux kill-session -t "$SESSION_NAME"
|
||||
|
||||
echo "Session stopped and cleaned up."
|
||||
Reference in New Issue
Block a user