feat(m0): 后端骨架 + Web/安卓前端骨架 + 设计文档

- backend: FastAPI 应用工厂、统一错误信封、JWT、短信登录、工作区/成员/邀请、幂等中间件
- alembic 0001: workspace/member/invitation/audit_log/app_user + RLS 策略 + 双角色 GRANT
- tests: 认证/工作区/租户隔离/幂等 四组用例(尚未在本机执行)
- web: React18+Vite 骨架(登录、工作区、邀请落地页)
- android: Compose 骨架(登录、工作区列表)
- 2.Design: PRODUCT-PLAN / ROADMAP-M0-M1 / NAMING / DEVELOPMENT
This commit is contained in:
Steven
2026-10-28 02:38:53 +08:00
commit 0c53aa7062
72 changed files with 5320 additions and 0 deletions
+166
View File
@@ -0,0 +1,166 @@
# M0–M1 实施清单
> 来源:`2.Design/PRODUCT-PLAN.md` v1.0 第 12 节
> 粒度:每项 0.3–1.5 人日,2 人并行 ≈ 2 周一个里程碑
> 状态标记:`[ ]` 未开始 `[~]` 进行中 `[x]` 完成
---
## M0 · 骨架(第 1–2 周,10 人日)
**里程碑目标**:一个能注册、能建团队、能证明"两个租户互相看不见"的空壳,部署在服务器上,CI 全绿。
### 0.1 仓库与约定(0.5d)
- [ ] 目录结构:`backend/ web/ android/ deploy/ 2.Design/`
- [ ] `AGENTS.md`:提交纪律(改完必 push)、前端产物只能由 HEAD 构建、文档落点
- [ ] `.gitignore` / `.editorconfig` / `README.md`
- [ ] 分支策略:`main` 受保护,功能走 `feat/*`;提交信息 `feat(module): 中文描述`
- **验收**:Gitea 上能看到仓库,PR 模板可用
### 0.2 本地环境(1d)
- [ ] `deploy/docker-compose.dev.yml`:postgres:16(含 pgvector)+ redis:7 + minio
- [ ] `postgres` 初始化脚本:`CREATE EXTENSION vector, pg_trgm, pgcrypto`
- [ ] `.env.example` 全量注释
- **验收**:`docker compose up -d` 后 `psql` 能连、扩展已装
### 0.3 配置与日志(0.5d)
- [ ] `pydantic-settings` 分层配置(dev/prod),启动即校验必填项
- [ ] 结构化日志(JSON)+ `request_id` 贯穿 + 敏感字段脱敏中间件(`api_key`/`token`/`password`)
- **验收**:一个请求在日志里能用 request_id 串起来,密钥不会出现在日志里
### 0.4 数据库基座(1d)
- [ ] `alembic init`,autogenerate 接入,`env.py` 读应用配置
- [ ] 基础表迁移:`workspace` `user` `member` `invitation` `audit_log`
- [ ] 通用列约定:`id uuid`、`created_at`、`updated_at`、`version`、`deleted_at`
- **验收**:空库 `alembic upgrade head` 一次成功;`downgrade` 可回滚
### 0.5 租户上下文 + RLS(1.5d)★ 核心
- [ ] 请求依赖:解析 JWT → 校验成员身份 → `SET LOCAL app.ws_id`
- [ ] 全部租户表启用 RLS,策略 `workspace_id = current_setting('app.ws_id')::uuid`
- [ ] 仓储基类强制 `ws_id` 为必填构造参数(不给默认值)
- [ ] 测试:租户 A 的 token 读租户 B 的资源 → 0 行 / 403
- **验收**:`pytest -k tenant_isolation` 全绿;故意删掉一条 where 条件时测试必须变红(用变异测试验证一次)
### 0.6 认证(1.5d)
- [ ] 手机号 + 验证码注册/登录(开发环境固定码 `123456`,短信通道留接口)
- [ ] 邮箱 + 密码(argon2)作为备选
- [ ] JWT:access 15min / refresh 30d 轮换,refresh 落库可撤销
- [ ] 登录限流(IP + 账号维度)
- **验收**:注册 → 登录 → 刷新 → 登出 全链路可用;错误码清晰(不是 500)
### 0.7 权限守卫(0.5d)
- [ ] `PERMISSIONS` 字典:`(资源, 动作) → 最低角色`,FastAPI 依赖注入统一执行
- [ ] 四个角色:owner / admin / member / guest
- **验收**:guest 调管理接口 403;member 调 admin 接口 403
### 0.8 接口规范(0.5d)
- [ ] 统一响应体 `{code, message, data, request_id}`
- [ ] 错误码表(`2.Design/API.md` 里维护)
- [ ] 写接口支持 `Idempotency-Key`(Redis 落 24h)
- [ ] 分页规范:cursor 优先,`limit` 上限 100
### 0.9 健康与版本(0.3d)
- [ ] `/health`(DB/Redis 探活)、`/version`(返回 commit + 构建时间)
- **验收**:部署后 `curl /version` 与 `git rev-parse --short HEAD` 一致
### 0.10 CI(0.7d)
- [ ] `ruff` + `mypy`(宽松起步)+ `pytest`
- [ ] 迁移检查:`alembic upgrade head` 后再 `alembic check` 无差异
- [ ] 镜像构建 + 推送到私有 registry(或直接在服务器构建)
- **验收**:坏代码推上去 CI 变红,前端产物不入库
### 0.11 部署(1d)
- [ ] `deploy/docker-compose.prod.yml` + nginx 反代 + TLS
- [ ] 端口分配登记到 `2.Design/DEPLOY.md`(避开 PosSync 的 5888 / 5176)
- [ ] 备份脚本接入现有每日备份(pg_dump + 保留 14 天)
- **验收**:从零一台机器 30 分钟内起完;重启后数据在;备份可恢复到临时库
### 0.12 三端契约(0.5d)
- [ ] 导出 `openapi.json`,生成 TS 客户端与 Kotlin 客户端
- [ ] `make sdk` 一键重生成
- **验收**:改一个后端字段,两端编译报错(说明契约真的在生效)
### 0.13 客户端壳(1d)
- [ ] Web:Vite 起项目、路由、登录页、空白任务页、请求封装(含 refresh 重试)
- [ ] Android:Compose 工程、登录页、空列表页、Retrofit+OkHttp 封装
- **验收**:两端都能真实登录并拿到空列表
### 0.14 微信 iLink 通道 spike(0.5d)★ 决策前置
> 不阻塞 M0 交付,但必须在写 `integration` 表之前做完,否则接法选错要返工。详见 `PRODUCT-PLAN.md` §10.3。
- [ ] 用 DSH 插件(`@michengai/dsh-im-connect`)实际扫码绑定一个微信号,观察 bot 身份归属
- [ ] 用第二个微信号私聊该 bot,确认多用户共用一号时消息是否互不串
- [ ] 压一下:连续 20 条消息的延迟与是否触发风控;断网 5 分钟后 `syncBuf` 能否续上
- [ ] 记录 `bot_token` 失效表现(多久失效、失效后是报错还是静默)
- **产出**:`2.Design/SPIKE-WECHAT-ILINK.md`,明确回答"用户自带号"还是"平台统一号"
- **验收**:文档里三个问题都有实测结论,不是推测
---
## M1 · 任务域(第 3–4 周,10 人日)
**里程碑目标**:自己开始每天用它记待办,连续 3 天不别扭。
### 1.1 数据(1d)
- [ ] `item` `item_assignee` `tag` `item_tag` `comment` `activity` 迁移
- [ ] 索引:`(ws_id, status, due_at)`、`(ws_id, owner_id, status)`、`deleted_at` 部分索引
- [ ] 全文索引:`zhparser` 或 `pg_bigm` 建在 `title + body_md`
### 1.2 领域(1.5d)
- [ ] 状态机 `inbox→todo→doing→done`(+ blocked/canceled),非法流转拒绝并给提示
- [ ] 所有变更写 `activity`(含 `changed_fields` 增量)
- [ ] `version` 乐观锁,冲突返回 409 带当前版本
### 1.3 API(1.5d)
- [ ] 增删改查、批量状态变更、排序(手动 sort_order)
- [ ] 视图查询:inbox / today / week / all,支持标签与关键词过滤
- [ ] 搜索接口(标题 + 正文,返回高亮片段)
### 1.4 Web(3d)
- [ ] 四视图 + 看板拖拽(列 = 状态)
- [ ] 详情侧栏:优先级/截止/标签/备注/子任务(一层)/评论
- [ ] 全局快速录入(`Ctrl+K` 或顶部输入框,支持 `#标签` `!优先级` `@人` 语法)
- [ ] 键盘流:`j/k` 上下、`Enter` 完成、`e` 编辑
### 1.5 安卓(1.5d)
- [ ] 列表 + 详情 + 勾选完成
- [ ] 下拉刷新、只读缓存(Room,仅离线展示,改动必须联网)
### 1.6 通知与提醒(0.8d)
- [ ] 站内通知表与未读角标
- [ ] 每日摘要邮件(可关)
### 1.7 测试与验收(0.7d)
- [ ] 每个接口一条 happy path + 一条越权用例
- [ ] 前端关键交互 e2e(Playwright 起 3 条:登录、建任务、拖动状态)
### M1 完成定义(DOD)
- [ ] 连续 3 天所有待办都记在里面,没有回到微信/备忘录
- [ ] 冷启动到"建完第一个任务"< 90 秒(新账号实测)
- [ ] 1000 条任务下四视图首屏 < 300ms
- [ ] 跨租户测试、越权测试全绿
---
## 关键验收用例(M0–M1 必须通过)
| # | 用例 | 期望 |
|---|---|---|
| 1 | 租户 A 用户请求租户 B 的 item id | 404(不是 403,避免泄露存在性) |
| 2 | 同一手机号注册两次 | 第二次提示已存在并引导登录 |
| 3 | 被移出团队的成员拿旧 token 请求 | 401,且 refresh 失效 |
| 4 | 并发两次提交同一任务的不同状态 | 一次成功,一次 409 并可重试 |
| 5 | 网络中断后重新提交 | 幂等键保证不产生两条 |
| 6 | 删除任务后全局搜索 | 搜不到,且回收站可恢复 |
| 7 | 停掉 Redis | `/health` 变 503,业务接口给明确错误而非 500 |
| 8 | 部署后 `/version` | commit 与仓库 HEAD 一致 |
---
## 下一步排期建议
```
第1周 0.1–0.7(另一个人并行做 0.13 客户端壳)
第2周 0.8–0.12(另一个人并行做 Web 登录与请求封装)
第3周 1.1–1.3 + 1.4 开工
第4周 1.4–1.7 + 自己开始 dogfooding
```