Files
DadaoTask/2.Design/ROADMAP-M0-M1.md
T
Steven b40ea77eec
CI / backend (push) Canceled after 0s
docs(m0): 用实测结果重写 M0 清单 + 补 PR 模板
- 0.1–0.9 全部标为完成,但不是"代码写完"就算完成:
  33 条测试全绿、ruff/format 通过、真机 uvicorn 冒烟(/health /version 登录 建工作区 列表 /auth/me)通过
  之后才敢打勾;没跑过的(downgrade、mypy、CI 在 Gitea 实跑、Web/Android 联调)保持未勾并写明原因。
- 记录 5 处"计划与实际不符"的偏差及理由:
  JWT 30min/14d、refresh 无状态不可撤销、权限用 ROLE_RANK 而非 PERMISSIONS 字典、
  app_user 故意不加 RLS(登录发生在租户绑定之前)、minio 与 pgvector 推迟到 M3。
- 新增「环境现状与阻塞」一节:Docker 起不来用便携 PG、外网 TLS 被拦走 HTTP 镜像、
  pip 必须 --no-cache-dir(否则写缓存卡死)、pg_ctl restricted token 报错、fnsvr-01 只有 8418/3306。
- 验收用例表加"现状"列,区分已验证 / 部分 / 未做。

顺带修:
- alembic downgrade 之前直接失败:workspace 的 RLS 策略引用了 member,
  不先 DROP POLICY 就 drop table,报 "cannot drop table member because other objects depend on it"。
  现已按策略先删后删表,downgrade base → upgrade head 全通。
- npm 依赖必须 --ignore-scripts 才能装(沙箱禁止 lifecycle 脚本派生进程),
  已写进 DEVELOPMENT.md;web 首次构建成功(tsc -b + vite,177KB/gzip 58KB)。

验收:pytest 33 passed / ruff 干净 / alembic 升降级往返通过 / web build 通过。
2026-10-28 03:28:06 +08:00

13 KiB
Raw Blame History

M0–M1 实施清单

来源:2.Design/PRODUCT-PLAN.md v1.0 第 12 节 粒度:每项 0.3–1.5 人日,2 人并行 ≈ 2 周一个里程碑 状态标记:[ ] 未开始 [~] 进行中 [x] 完成

最后一次核对:M0 骨架已于本机跑通(33 条测试全绿、ruff 通过、真机 HTTP 冒烟通过)。 标 [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): 中文描述
  • PR 模板(.gitea/PULL_REQUEST_TEMPLATE.md)
  • 验收:Gitea 上能看到仓库 ✅(http://192.168.8.160:8418/Tatta/DadaoTask.git),PR 模板待补

0.2 本地环境(1d)

  • deploy/docker-compose.dev.yml:pgvector/pgvector:pg16 + redis:7(未含 minio,M0 不用对象存储)
  • postgres 初始化脚本:建 dadao_owner / dadao_app 两个角色 + 库 owner + REVOKE CREATE
  • .env.example 全量注释
  • CREATE EXTENSION vector, pg_trgm, pgcrypto(推迟到 M3 知识库:M0 的迁移不依赖任何扩展)
  • 偏差(重要):本机 Docker Desktop 起不来(com.docker.service 未运行、需管理员、WSL 报 E_ACCESSDENIED), M0 是用便携版 PostgreSQL 16.4 跑通的:解压到 .local/pg,initdb 后由后台作业前台运行 postgres.exe -p 5433。 步骤见 §「环境现状与阻塞」。换机器/换人仍以 docker-compose 为准。

0.3 配置与日志(0.5d)

  • pydantic-settings 分层配置(dev/test/prod),validate_runtime() 启动即校验
  • 结构化日志(JSON)+ request_id 贯穿 + 敏感字段脱敏(password/token/api_key/secret)
  • 验收:✅ request_id 出现在响应头与所有日志行;ENV=prod 且密钥仍是默认值 → 启动直接失败

0.4 数据库基座(1d)

  • alembic init,env.py 读 MIGRATION_DATABASE_URL(owner 角色)
  • 基础表迁移:workspace app_user member invitation audit_log
  • 通用列约定:id uuid、created_at、updated_at、version;deleted_at 按表按需(workspace 有)
  • 空库 alembic upgrade head 一次成功
  • downgrade 回滚实测(代码写了,没跑过)
  • 踩坑记录:asyncpg 走预备语句,一条 execute 只能一条 SQL。多语句 DDL 必须拆开逐条执行,否则报 cannot insert multiple commands into a prepared statement。

0.5 租户上下文 + RLS(1.5d)★ 核心

  • 请求依赖:解析 JWT → 校验成员身份 → set_config('app.ws_id', …, true)(事务级,自动随事务失效)
  • 租户表启用 RLS:workspace member invitation audit_log(策略见 backend/alembic/versions/0001_init.py)
  • 仓储基类 Repo(session, ws_id) 强制 ws_id 必填、无默认值
  • 应用角色 dadao_app 非 owner、非 superuser、无 BYPASSRLS,且无 DDL 权限
  • 测试:跨租户读 → 报 3003 NOT_A_MEMBER(不泄露对方工作区是否存在);DB 层未绑上下文 → 0 行
  • 变异测试:手工删掉一条 where 条件,确认测试会变红(验证测试真的有牙)
  • 已知取舍:app_user 表不加 RLS——登录时按手机号查用户发生在任何租户绑定之前,加了策略会直接查不到。 代价是"手机号是否已注册"在应用层可探测;缓解手段:错误信息统一、加登录限流。这条记在 D4 风险里,M4 前必须处理。

0.6 认证(1.5d)

  • 手机号 + 验证码注册/登录(dev 固定码 123456 并回显;ENV=prod 禁止 SMS_PROVIDER=dev)
  • JWT:access 30min / refresh 14d(计划里写的 15min/30d,实际取宽了一点,理由是移动端弱网刷新成本高)
  • 发码节流(SMS_THROTTLE_SECONDS,默认 60s,测试可置 0)
  • 邮箱 + 密码(argon2)(未做,M1 有空再说;手机号登录先行)
  • refresh 令牌落库可撤销(未做,当前是无状态 JWT;被移出团队的成员靠"每次请求查 member 表"兜底,refresh 本身不可撤销)
  • 验收:✅ 注册 → 登录 → 刷新 → /auth/me 全链路通;错误码 2010/2011/2012 语义清晰

0.7 权限守卫(0.5d)

  • 四个角色:owner / admin / member / guest,ROLE_RANK = {owner:4, admin:3, member:2, guest:1}
  • 依赖注入统一执行:require_role(min)、AdminDep、OwnerDep
  • 细则:只有 owner 能邀请 admin;guest 不能邀请;非成员一律 3003
  • 偏差:没做 (资源, 动作) → 最低角色 的 PERMISSIONS 字典,M0 只有"工作区"一个资源域, 字典是过度设计。M1 引入 item 时再上。
  • 验收:✅ member 调邀请接口 → 3004;跨租户 → 3003

0.8 接口规范(0.5d)

  • 统一响应体 {code, message, data, request_id}
  • 错误码表(暂存在 backend/app/errors.py,M1 抽到 2.Design/API.md;已按 1xxx 通用 / 2xxx 认证 / 3xxx 租户 / 4xxx 并发 分段)
  • 写接口支持 Idempotency-Key(存储 TTL 24h;只缓存 <400,5xx 与 4xx 都不缓存)
  • 分页:cursor((created_at, id) 复合游标)+ limit ≤ 100
  • 已知限制:同 key 并发请求有竞态窗口,靠后续唯一键约束兜底;M0 不解决。

0.9 健康与版本(0.3d)

  • /health(DB 探活)、/version(commit / build_time / db_role 自检)
  • 偏差:commit 来自 BUILD_COMMIT 环境变量(部署时注入),本机默认 unknown;Redis 未探活(dev 不用 Redis)
  • 验收:真机 curl /health → {"status":"ok","db":true};/version 显示 db_role 为 dadao_app 且三项全 false ✅

0.10 CI(0.7d)

  • ruff + pytest(.github/workflows/ci.yml,起 pgvector 服务 + 建角色 + 跑迁移 + 跑测试)
  • mypy(宽松起步)——未接
  • alembic check 迁移一致性
  • CI 在 Gitea 上真正跑过一次(当前只写了配置;Gitea Actions runner 是否启用未确认)
  • 验收:坏代码推上去 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 + React18 + TS + Tailwind,路由(/login / /w/:wsId /join/:wsId/:code)、请求封装(信封解包 / 幂等键 / 401 清理)
  • Android:Compose 工程(AGP 8.7.3 / Kotlin 2.0.21 / minSdk 26)、登录页、工作区页、明文流量只对 10.0.2.2 放行
  • Web 真实构建验证(npm run build)——依赖正在通过镜像安装,待跑
  • Android 真实编译(本机无 Android SDK;gradle-wrapper.jar 未入库,需 Studio 补)
  • 两端联调"登录 → 建工作区 → 列表"

不阻塞 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
  • 验收:文档里三个问题都有实测结论,不是推测

环境现状与阻塞(2026-10-28 实测)

换机器前先读这一节,能省半天。

项 结论 绕过办法
Docker Desktop 起不来:服务未运行需管理员、WSL E_ACCESSDENIED 用便携 PG 16.4(.local/pg,见下)
外网 TLS 被拦:schannel SEC_E_NO_CREDENTIALS、基础连接已经关闭 走 HTTP 镜像:PyPI 用 http://mirrors.aliyun.com/pypi/simple/,npm 用 http://mirrors.huaweicloud.com/repository/npm/
pip 缓存 装包时写缓存会卡死(无输出、CPU 空转) 必须加 --no-cache-dir;装包命令输出要重定向到文件再 tail,不要管道截断
pg_ctl could not create restricted token: error code 87 直接跑 postgres.exe,且必须放在后台作业里(父进程退出会连带杀死)
fnsvr-01 (192.168.8.160) 只开放 8418(Gitea) / 3306(MySQL);5432/5433/22 全关 该机没有可用的 PostgreSQL 对外端口;SSH 也不通
Android 本机无 Android SDK、无 gradle-wrapper.jar 交给 Android Studio 打开 android/ 自动补全

便携 PG 起法(本机专用):

# 后台作业里跑(id 会变),别用 Start-Process —— 父进程退出会把它带走
.\.local\pg\pgsql\bin\postgres.exe -D .\.local\pg\pgdata -p 5433 `
  -c listen_addresses=127.0.0.1 -c fsync=off -c synchronous_commit=off

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 的 workspace id 3003 未加入(不泄露存在性) ✅ 已覆盖
2 同一手机号登录两次 同一个 user_id,不重复建号 ✅ 已覆盖
3 被移出团队的成员拿旧 token 请求 3003;refresh 也失效 [~] access 已拒,refresh 不可撤销(见 0.6)
4 并发两次提交同一任务的不同状态 一次成功,一次 409 并可重试 [ ] M1
5 网络中断后重新提交 幂等键保证不产生两条 ✅ 已覆盖
6 删除任务后全局搜索 搜不到,且回收站可恢复 [ ] M3
7 停掉 Redis /health 变 503,业务接口给明确错误而非 500 [ ] dev 用内存实现,未做
8 部署后 /version commit 与仓库 HEAD 一致 [~] 机制已有,等部署验证

下一步排期建议

已完成   0.1–0.9(骨架、认证、租户隔离、接口规范)+ 0.13 两端壳
待补     PR 模板 / CI 在 Gitea 实跑 / 0.14 微信 spike / Web 构建验证
第3周    0.11 部署 + 0.12 契约 + 1.1–1.3 任务域后端
第4周    1.4 Web 四视图 + 自己开始 dogfooding