CI / backend (push) Canceled after 0s
- ROADMAP:勾选与实际证据一一对应,未做的写明原因与去向(通知推迟到 M2、子任务与 @人 合并进 item_link) - DEVELOPMENT:新增向量库、e2e、性能实测三节 - API:补批量状态与看板接口,重排小节编号 - deploy:docker-compose.vector.yml + vector-init(独立 PG+pgvector,5434,不碰 fnsvr-01) - .gitignore:注释乱码修正,补 *.tsbuildinfo / e2e 产物
17 KiB
17 KiB
M0–M1 实施清单
来源:
2.Design/PRODUCT-PLAN.mdv1.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 角色)- 基础表迁移:
workspaceapp_usermemberinvitationaudit_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:
workspacememberinvitationaudit_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 补) - 两端联调"登录 → 建工作区 → 列表"
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 - 验收:文档里三个问题都有实测结论,不是推测
环境现状与阻塞(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) / 8432(PG 17.6,dbadmin/superuser) | 用途定死:只有 dadaotask 与 dadaotask_test 两个库,不再往上装扩展;能力:无 pgvector,只有 pg_trgm/pgcrypto |
| Android | 无 JDK、无 Gradle(java -version/gradle -v 都不存在),SDK 平台目录在但缺构建链 |
装 JDK 17 + Gradle 8.x 后 ./gradlew assembleDebug;当前安卓代码未被编译器验证过 |
| 向量库 | Docker 起不来 → deploy/docker-compose.vector.yml 写了但没跑过 |
等 Docker 可用(或换一台机器)再 up -d,端口 5434 |
便携 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 天不别扭。
进度口径:
[x]= 已实现且验证过(测试/构建/冒烟),[ ]= 未做。每项后面写证据或偏差原因。
1.1 数据(1d)
itemtagitem_tagcommentactivity迁移(backend/alembic/versions/0002_tasks.py,revision 0002 ← 0001)- 索引:
ix_item_ws_status_due (ws,status,due_at)、ix_item_ws_assignee、ix_item_ws_updated、ix_item_ws_sort - [~] 中文全文:没用 zhparser/pg_bigm,改用
pg_trgm+ILIKE。理由:服务器是 Alpine 上的 PG 17,pg_available_extensions只有pg_trgm/pgcrypto,装 zhparser 要编译 + 改镜像;trigram 对中文按 3 字符切,子串匹配够用,且gin_trgm_ops索引能救性能。真正需要"相关度排序"时再上 zhparser(M3 知识库阶段重评)。 - 偏差:没有
item_assignee关联表,item.assignee_id单列。理由:M1 只有"一个任务一个负责人",多人协作由工单阶段(M2)的item_link承担;提前上关联表会让每条查询都多一次 join。 - RLS:五张新表全部
ENABLE ROW LEVEL SECURITY+workspace_id = app_ws_id(),并 GRANT 给dadao_app
1.2 领域(1.5d)
- 状态机
backend/app/domain/task.py:inbox→todo→doing→done(+ blocked/canceled,done→todo可重开);非法流转抛5002并回allowed("你可以去 canceled/todo"),不用前端猜 - 所有变更写
activity(created/updated/status_changed{from,to}/commented,payload 是 JSONB) version乐观锁:UPDATE ... WHERE version = ?看rowcount,冲突回4009+current_version(实测 stale 请求拿到current=4)done写completed_at,重开清空;deleted_at软删,view=deleted才看得到
1.3 API(1.5d)
- 增删改查(
backend/app/api/items.py)、POST /{id}/status、软删(带 version) - 视图:
inbox|active|today|week|done|canceled|all|deleted;today/week按Asia/Shanghai切日界(settings.default_timezone),不按 UTC - 过滤:
status(可重复)、assignee=me|<uuid>、tag、q(标题+正文 ILIKE,实测中文周报命中) - 批量状态变更(
POST /items/batch-status,逐条返回成败,不做全回滚)+ 看板数据GET /items/board - [~] 搜索返回高亮片段:未做,只返回命中行。理由:高亮要在客户端按 query 二次切词,纯展示逻辑,等 Web 有真实使用反馈再补
- 分页:
(sort_key DESC, id DESC)keyset 游标;sort=due不分页(NULLS LAST 会把 NULL 行切碎,只有今日/逾期短列表用)
1.4 Web(3d)
- 视图 tab(今天/进行中/收件箱/已完成/全部)+ 搜索框 + 回车快速新建(
web/src/pages/Tasks.tsx) - 勾选完成/重开、
收下(inbox→todo)、开工(todo→doing)、阻塞 - 看板拖拽(
web/src/pages/Board.tsx):HTML5 拖放,四列,非法流转当场拦下并说明原因;勾选后可批量移动 - 详情侧栏可编辑(
web/src/components/TaskDetail.tsx):标题/优先级/截止时间/标签/备注/评论,全部落库;409 时自动刷新为最新内容 - 快速录入语法:
#标签!高/!中/!低/!1..!3(web/src/lib/parse.ts,语法从标题里剥离) - 键盘流:
j/k选择、Enter完成、e编辑、Esc关闭 npm run build通过:41 modules,195.20 kB │ gzip 63.73 kB- [~]
@人指派:未做。理由:重名的人比标签多,悄悄指错负责人比不支持更糟;等 M2 有成员选择器再做 - [~] 子任务(一层):未做,排到 M2 与工单的
item_link一起做——两者本质是同一种"事项之间的边"
1.5 安卓(1.5d)
- 任务列表 + 详情弹窗 + 勾选完成 + 快速新建(
android/.../ui/TaskListScreen.kt) - 视图切换(今天/进行中/收件箱/已完成)、乐观锁冲突提示与自动刷新
- 未编译:本机没有 JDK、没有 Gradle(
java -version/gradle -v均不存在), Android SDK 平台目录存在但缺构建工具链。代码按 Compose 常规写法写,尚未经过编译器验证。 补 JDK 17 + Gradle 后第一件事就是./gradlew assembleDebug,那时的报错才算数。 - 下拉刷新、只读缓存(Room)未做
1.6 通知与提醒(0.8d)
- 站内通知表与未读角标 —— 主动推迟到 M2:通知的绝大多数触发源是工单(被指派、被催、验收), M1 只有自己的任务,做出来也没人收到东西。表随 M2 的工单一起建,避免建两次。
- 每日摘要邮件(同上推迟)
1.7 测试与验收(0.7d)
backend/tests/test_items.py13 个用例:默认收件箱、状态机 happy path 与非法流转、版本冲突、 跨租户 3003 且他人列表为空、搜索+标签、today时区边界、软删、评论与事件流、游标翻页、 批量改状态的部分成功、批量非法流转、看板分列- 全量
pytest -q→ 46 passed;ruff check+ruff format --check全 0 - Playwright 真浏览器 e2e(
web/e2e/smoke.mjs,7 项断言全绿):登录 → 建工作区 → 快速录入解析#标签/!优先级→ 详情抽屉保存并回写列表 → 看板拖拽改状态 → 非法拖拽被拦 → 控制台无报错 - 性能:1000 条任务下 P50 —— 今天 43.1ms / 进行中 42.4ms / 收件箱 42.9ms / 全部 38.4ms / 搜索 45.0ms / 看板 55.4ms(含本机网络与鉴权中间件,是下限)
M1 完成定义(DOD)
- 连续 3 天所有待办都记在里面,没有回到微信/备忘录 —— 只有真实使用能证伪,代码侧无法替代
- 冷启动到"建完第一个任务"< 90 秒(新账号实测)
- 1000 条任务下四视图首屏 < 300ms —— 实测 P50 38–55ms,最慢的看板 55.4ms
- 跨租户测试、越权测试全绿(
test_tenant_isolation.py+test_items.py的跨租户与批量用例)
关键验收用例(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