feat: PLANNING体系首版(00路线图/02任务总表/03执行协议/I-03/I-04/registry42口径)+M1止血交付

This commit is contained in:
2026-09-12 14:25:25 +08:00
commit 8242e730b4
482 changed files with 37029 additions and 0 deletions
+357
View File
@@ -0,0 +1,357 @@
# Handoff Bundle — C-01 路由/代理前缀冲突修复
生成时间:2026-09-11 23:18 +0800
项目:`chunyu` 项目根:`C:\Users\12914\Desktop\vscode\chunyu_project_react`
优先级:P0 · 里程碑 M1 · 预估 1 天
建议模型:deepseek-v4.1-flash 复核:glm-5.3-flash
依赖:无
进度文件:`PROGRESS_C-01.md`(写到项目根)
> 这是一份**自包含**交接包:接手模型读完本节即可开工,不需要再问路径、命令、上下文。
> 卡内所有硬约束以第 1 节为准;本包的其余部分是上下文,**不得覆盖卡内约束**。
---
## 0. 接手须知(先读)
1. 本包是「C-01」的完整上下文。若你是接手上游的部分成果,**不要重做已验证部分**——
先跑第 5 节的基线命令确认当前状态,再从断点继续。
2. 密钥零接触:`.env`、`(服务器数据)`、`~/.dsh/secrets` —— 只读都不行。
3. 交付 = 代码 + 项目根 `PROGRESS_C-01.md`(格式见第 2 节)+ 关键命令的**原始输出**。
4. 验收标准全绿才可声明 done;跑不绿就写 partial/blocked 并附原始输出。
5. 完成后逐条对照第 1 节「验收标准」自检,未验证项必须写明——**虚报验收直接降级**。
---
## 1. 任务卡全文
# C-01 · 路由/代理前缀冲突修复
| 字段 | 值 |
|---|---|
| 项目 | 可乐工具 · `vscode/chunyu_project_react`(前端)+ `vscode/chunyu_project`(后端) |
| 优先级 | P0 · M1 |
| 建议模型 | deepseek-v4.1-flash(主)/ glm-5.3-flash(全量回归复核) |
| 依赖 | 无 |
| 预估 | 1 天 |
## 一、背景(为什么做)
2026-09-10 页面健康检查实测:**开发环境 22 条路由、生产环境 7 条路由被 API 代理劫持**——用户刷新页面、收藏链接、分享链接时直接打开 Django 404 或原始 JSON,而不是前端页面。SPA 内部点击导航不受影响,所以**藏得深、咬得狠**。
证据:`vscode/docs/页面健康检查报告.md`(问题清单 1.1 / 1.2)。
根因:前端路由与后端 API 共用了一组顶级路径段(`/api`、`/article`、`/learn`、`/user`、`/bug`、`/chat`、`/history`、`/message`、`/s`、`/search`、`/tool`),vite 代理用**前缀匹配**、生产 nginx 用**带尾斜杠的正则**,两边都会误伤前端路由。
## 二、目标(交付物)
1. **前端冲突路由改名**(推荐方案 A,报告结论"一劳永逸,趁早改便宜"):
- `/api` 及其子路由(`/api-directory`、`/api-docs`、`/api-detail/:id`、`/api/ip-location` 等 5 个数据 API 页)→ `/open-api/*`
- `/article/:id` → `/post/:id`(或 `/article-detail/:id`)
- `/user/:id` → `/user-home/:id`(项目已有 `/user-home` 路由,可复用)
2. **代理收窄**:vite 代理不再是宽前缀——改为**精确路径清单 / 正则白名单**(先枚举后端真实 URL:`chunyu_project/config/urls.py` + 各 app `urls.py`,以枚举结果为准)。
3. **生产 nginx 同步**:`nginx-docker.conf` 同样收窄,并覆盖 `/article/:id`、`/user/:id`、`/api/*` 五条数据页的冲突段。
4. **回归脚本**:提交一个可重复运行的 73 路由检查脚本(如 `scripts/check_routes.mjs`),检测逻辑三件套:① 是否返回 Django 404/JSON(劫持)② `#root` 是否空(白屏)③ 正文长度。
5. 全量更新 `navigation(...)` / 跳转引用 / 语言包中的路径引用。
## 三、执行步骤
```text
1. 枚举后端真实 API 路径(config/urls.py + 全部 app urls.py),产出一张「API 路径清单」
2. 对照前端 73 条路由,标记全部冲突项(不只报告里那 7 条)
3. 执行改名 + 同步全部引用(grep 旧路径确认零残留)
4. 收窄 vite 代理 → 启动 dev 双端,跑回归脚本至 73/73
5. 收窄 nginx-docker.conf,静态审查冲突模式段
6. 提交:代码 + 回归脚本 + 检查报告(贴进 PROGRESS)
```
## 四、验收标准
- [ ] dev 环境:73 条路由直访全部返回 SPA(无 Django 404/JSON 劫持、无白屏)
- [ ] 生产 nginx 配置:审查确认 7 条冲突段全部让路(含 `/article/:id`、`/user/:id`——核心分享场景)
- [ ] `grep -rn "旧路径"` 零残留(含 App.tsx、页面组件、i18n 词条)
- [ ] API 调用回归正常(登录、文章列表、工具使用等核心接口抽查 via 代理)
- [ ] SPA 内部导航抽查正常(首页→文章→详情)
- [ ] 回归脚本已入库,可被 CI 复用
## 五、验收命令(参考)
```bash
# 后端
cd chunyu_project && python manage.py runserver 8002
# 前端
cd chunyu_project_react && npm run dev # 5173
# 路由回归(脚本由本任务交付)
node scripts/check_routes.mjs --base http://127.0.0.1:5173
# 期望输出:PASS 73/73,hijacked=0,blank=0
# 引用残留检查
grep -rn "'/api-docs'\|'/api-directory'\|/article/\|/user/" src/ --include="*.tsx" --include="*.ts" | grep -v open-api
```
## 六、边界(不许做)
- 不动后端 API 的响应结构(纯路由层工作)
- 不重构页面组件内部逻辑
- 不修改 `docs/` 下既有报告
- 新增路由名后必须保留旧路径的**客户端重定向**(302 内的 SPA `Navigate`)——已分享出去的链接不能全死
## 七、交接
交付 = 代码 + `scripts/check_routes.mjs` + **PROGRESS_C-01.md**(含 73/73 输出与占位截图说明)。
格式见 `PLANNING/03-执行协议.md`。
---
## 2. 作业规范(协议要点)
## 一、接单流程(强制顺序)
```text
1. 通读任务卡 → 确认「边界」段(哪些文件不许动)
2. 读项目根 README + 最近 2 份 PROGRESS/迭代报告(了解项目惯例与雷区)
3. 跑「基线测试」——确认开工前测试是绿的;有红的先记录,不背锅
4. 实施(最小变更原则)
5. 跑「验收命令」——全绿
6. 写 PROGRESS_<任务ID>.md 到项目根
7. 交付:代码 + PROGRESS + 关键命令输出摘要
```
**禁止跳步**:不读文档就动手、不跑基线就改代码、不跑验收就宣称完成——三者任一发生,交付直接打回。
---
## 二、PROGRESS 文件格式(交付凭证)
写到**项目根目录**,文件名 `PROGRESS_<任务ID>.md`:
```markdown
# PROGRESS — <任务ID> <任务名>
- 状态:done | partial | blocked
- 执行模型:<模型名>
- 日期:YYYY-MM-DD
- 分支/提交:<commit hash 或"未提交">
## 三、质量红线(硬性)
1. **不许伪造**:测试跑不绿就写 partial/blocked,附原始输出。伪造一次 = 该模型在该项目永久降级为 C 级。
2. **不许删测试**:既有测试只许更绿。删测试用例、跳过断言、注释掉失败用例 = 交付无效。
3. **不许夹带**:任务卡外的一律不做。看到顺手能修的 bug,写进 PROGRESS 的「遗留问题」,不顺手改。
4. **不许碰密钥**:`.env`、`~/.dsh/secrets`、`(服务器数据)` 目录——只读都不许,物理隔离。
5. **不许破坏契约**:改 API 响应结构/字段名必须保持向后兼容或双写过渡,并在 PROGRESS 说明。
6. **不许强推**:不执行 `git push --force`、不 rebase 他人提交、不 git reset 别人的改动。
7. **不许超时硬干**:预计超预算时,先交「阶段报告 + 断点说明」,让调度器续派,不许烂尾。
---
## 四、各项目雷区(前人踩过的坑,直接记)
### 可乐工具(chunyu)
- **vite 代理前缀匹配过宽**:`/api` 会连 `/api-docs` 一起劫持——改代理时必须用精确规则(见 C-01)。
- **生产 nginx 正则带尾斜杠**:`^/(api|user|...)/` 依然劫持子路径,改完前端要同步审查 `nginx-docker.conf`。
- 前端 67 页组件,路由引用散落各处——改路由名必须 grep 全量 `navigation(` 调用。
- 后端全异步(adrf):新视图必须 async,不要写同步视图混挂。
### EnglishDrill
- **granian 孤儿 worker**:只杀父进程会留 worker 占端口,表现为"改了代码不生效"。用 `tools/devserver.ps1 restart`。
- **E2E 双模式**:`tests/e2e/run_e2e.py`(默认 ASGI 进程内 / `--http` 真实 granian)。隔离库 `backend/.tmp/e2e-db.sqlite3`,不碰开发库。
- **值收口层**:外部 id 必须走 `core/helpers.py` 的 `as_int` 等收口函数,禁止裸 `int()` 进 ORM(曾造成 44 处 5xx)。
- **stats 口径**:分类统计必须带 `is_deleted=False`,否则和 `/api/categories` 打架(BUG-2 教训)。
- 前端登录守卫在 `src/App.tsx:29`。
### dealerhub
- 460 passed / 4 skipped 是基线;4 个跳过是 PG 并发用例(D-02 处理)。
- 测试命令:`pytest`(在 `backend/`);前端 `npm test` + `npm run build`。
- `makemigrations --check --dry-run` 必须干净。
- 演示数据重建:`seed_demo --reset`;改数据模型要同步改 seed。
### DSP
- 双端:Web(Vue3)+ Android;后端 Django + Channels。
- 转码依赖本地 ffmpeg;nginx 托管静态视频。
- 无 E2E 套件——S-01 会建;在此之前以 `backend/tests` 为准。
### DSH / 基础设施
- `~/.dsh/settings.yaml` 是**热配置**:改前备份(`.bak_<日期>`),改后 YAML 解析校验。插件曾覆盖式写入破坏能力声明(2026-09-08 事故)。
- 测试:`server/` 下 `pytest -q`,用独立测试库 `postgres_test`,conftest 拒绝在生产库跑。
- 探活:`GET /healthz`。
---
---
## 3. 项目上下文
### 3.1 项目 README:`chunyu_project_react/README.md`
```markdown
# React + TypeScript + Vite
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
Currently, two official plugins are available:
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Oxc](https://oxc.rs)
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/)
## React Compiler
The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation).
## Expanding the ESLint configuration
If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules:
```js
export default defineConfig([
globalIgnores(['dist']),
{
files: ['**/*.{ts,tsx}'],
extends: [
// Other configs...
// Remove tseslint.configs.recommended and replace with this
tseslint.configs.recommendedTypeChecked,
// Alternatively, use this for stricter rules
tseslint.configs.strictTypeChecked,
// Optionally, add this for stylistic rules
tseslint.configs.stylisticTypeChecked,
// Other configs...
],
languageOptions: {
parserOptions: {
project: ['./tsconfig.node.json', './tsconfig.app.json'],
tsconfigRootDir: import.meta.dirname,
},
// other options...
},
},
])
```
You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules:
```js
// eslint.config.js
import reactX from 'eslint-plugin-react-x'
import reactDom from 'eslint-plugin-react-dom'
export default defineConfig([
globalIgnores(['dist']),
{
files: ['**/*.{ts,tsx}'],
extends: [
// Other configs...
// Enable lint rules for React
reactX.configs['recommended-typescript'],
// Enable lint rules for React DOM
reactDom.configs.recommended,
],
languageOptions: {
parserOptions: {
project: ['./tsconfig.node.json', './tsconfig.app.json'],
tsconfigRootDir: import.meta.dirname,
},
// other options...
},
},
])
```
…(已截断:原文 73 行 / 2425 字符)
```
### 3.2 最近 PROGRESS(0 份)
(该项目尚无 PROGRESS —— 本卡可能是首张卡)
---
## 4. 相关源码清单
卡内点名过的路径,逐个核实:
- `vscode/docs/页面健康检查报告.md` → 文件 docs/页面健康检查报告.md(vscode,5039 B,改于 2026-09-10 18:56)
- `nginx-docker.conf` → 文件 nginx-docker.conf(vscode,3149 B,改于 2026-09-05 11:49)
- `docs/` → 目录 chunyu_project_react/docs(chunyu)
- `PLANNING/03-执行协议.md` → 文件 PLANNING/03-执行协议.md(vscode,5486 B,改于 2026-09-11 22:37)
**未解析的线索**(卡里提到但当前树中找不到 —— 可能是路径漂移,接手时按候选或自行搜索确认):
- `chunyu_project/config/urls.py` —— 近似候选:chunyu_project/air_quality/urls.py、chunyu_project/api/urls.py、chunyu_project/apidirectory/urls.py
- `urls.py` —— 近似候选:chunyu_project/air_quality/urls.py、chunyu_project/api/urls.py、chunyu_project/apidirectory/urls.py
- `scripts/check_routes.mjs`
**项目根一级结构**(先看哪儿):
- `chunyu_project_react` → docs/,public/,src/,test-results/,tests/,Dockerfile,README.md,_add_timestamp.cjs,_patch_date_calc.cjs,_verify.cjs,add_login_record.cjs,article-editor-dark-final.png,article-editor-dark-rich.png,article-editor-dark-verified.png…
---
## 5. 基线测试命令与输出
> 以下为打包时实跑捕获(超时上限 90s/条,重型套件已跳过)。开工前请自行复跑确认,不要直接抄这里的结论。
### 前端类型/构建检查(重)
```bash
cd chunyu_project_react && npm run build
```
[SKIP] 标记为重型(全量套件可能跑很久)——打包时未执行,接手模型必须自己跑一遍并记录真实输出
### 后端 Django 系统检查
```bash
$ cd chunyu_project && python manage.py check
```
```text
System check identified some issues:
WARNINGS:
?: (urls.W005) URL namespace 'shorturl' isn't unique. You may not be able to reverse all URLs in this namespace
api.TranslateUsage: (models.W042) Auto-created primary key used when not defining a primary key type, by default 'django.db.models.AutoField'.
HINT: Configure the DEFAULT_AUTO_FIELD setting or the ApiConfig.default_auto_field attribute to point to a subclass of AutoField, e.g. 'django.db.models.BigAutoField'.
apidirectory.ApiCategory: (models.W042) Auto-created primary key used when not defining a primary key type, by default 'django.db.models.AutoField'.
HINT: Configure the DEFAULT_AUTO_FIELD setting or the ApidirectoryConfig.default_auto_field attribute to point to a subclass of AutoField, e.g. 'django.db.models.BigAutoField'.
apidirectory.ApiFavorite: (models.W042) Auto-created primary key used when not defining a primary key type, by default 'django.db.models.AutoField'.
HINT: Configure the DEFAULT_AUTO_FIELD setting or the ApidirectoryConfig.default_auto_field attribute to point to a subclass of AutoField, e.g. 'django.db.models.BigAutoField'.
apidirectory.ApiItem: (models.W042) Auto-created primary key used when not defining a primary key type, by default 'django.db.models.AutoField'.
HINT: Configure the DEFAULT_AUTO_FIELD setting or the ApidirectoryConfig.default_auto_field attribute to point to a subclass of AutoField, e.g. 'django.db.models.BigAutoField'.
app.Changelog: (models.W042) Auto-created primary key used when not defining a primary key type, by default 'django.db.models.AutoField'.
HINT: Configure the DEFAULT_AUTO_FIELD setting or the AppConfig.default_auto_field attribute to point to a subclass of AutoField, e.g. 'django.db.models.BigAutoField'.
tool.ColorHistory: (models.W042) Auto-created primary key used when not defining a primary key type, by default 'django.db.models.AutoField'.
HINT: Configure the DEFAULT_AUTO_FIELD setting or the ToolConfig.default_auto_field attribute to point to a subclass of AutoField, e.g. 'django.db.models.BigAutoField'.
tool.CompressionHistory: (models.W042) Auto-created primary key used when not defining a primary key type, by default 'django.db.models.AutoField'.
HINT: Configure the DEFAULT_AUTO_FIELD setting or the ToolConfig.default_auto_field attribute to point to a subclass of AutoField, e.g. 'django.db.models.BigAutoField'.
tool.Tool: (models.W042) Auto-created primary key used when not defining a primary key type, by default 'django.db.models.AutoField'.
HINT: Configure the DEFAULT_AUTO_FIELD setting or the ToolConfig.default_auto_field attribute to point to a subclass of AutoField, e.g. 'django.db.models.BigAutoField'.
tool.ToolCategory: (models.W042) Auto-created primary key used when not defining a primary key type, by default 'django.db.models.AutoField'.
HINT: Configure the DEFAULT_AUTO_FIELD setting or the ToolConfig.default_auto_field attribute to point to a subclass of AutoField, e.g. 'django.db.models.BigAutoField'.
tool.ToolFavorite: (models.W042) Auto-created primary key used when not defining a primary key type, by default 'django.db.models.AutoField'.
HINT: Configure the DEFAULT_AUTO_FIELD setting or the ToolConfig.default_auto_field attribute to point to a subclass of AutoField, e.g. 'django.db.models.BigAutoField'.
tool.ToolUsageRecord: (models.W042) Auto-created primary key used when not defining a primary key type, by default 'django.db.models.AutoField'.
HINT: Configure the DEFAULT_AUTO_FIELD setting or the ToolConfig.default_auto_field attribute to point to a subclass of AutoField, e.g. 'django.db.models.BigAutoField'.
System check identified 12 issues (0 silenced).
```
→ 退出码 0,耗时 2.4s,末行判读:System check identified 12 issues (0 silenced).(期望:System check identified no issues)
---
## 6. 收工检查(提交前逐条打勾)
```text
[ ] 第 1 节「验收标准」逐条已满足,且每条都有原始命令输出支撑
[ ] 「边界」段列出的文件一个都没动
[ ] 既有测试没被删/没被跳过(只许更绿)
[ ] 卡外的一律没夹带(顺手发现的 bug 写进 PROGRESS「遗留问题」)
[ ] PROGRESS_C-01.md 已写到项目根,格式符合第 2 节
[ ] 基线对照写了「开工前 → 完工后」两段真实数字
[ ] 未验证项已明确标注(不许虚报)
```
---
*本包由 `PLANNING/tools/handoff_bundle.py` 生成 · 规范见 `PLANNING/03-执行协议.md` §七*
+326
View File
@@ -0,0 +1,326 @@
# Handoff Bundle — C-01 路由/代理前缀冲突修复
生成时间:2026-09-11 23:17 +0800
项目:`chunyu` 项目根:`C:\Users\12914\Desktop\vscode\chunyu_project_react`
优先级:P0 · 里程碑 M1 · 预估 1 天
建议模型:deepseek-v4.1-flash 复核:glm-5.3-flash
依赖:无
进度文件:`PROGRESS_C-01.md`(写到项目根)
> 这是一份**自包含**交接包:接手模型读完本节即可开工,不需要再问路径、命令、上下文。
> 卡内所有硬约束以第 1 节为准;本包的其余部分是上下文,**不得覆盖卡内约束**。
---
## 0. 接手须知(先读)
1. 本包是「C-01」的完整上下文。若你是接手上游的部分成果,**不要重做已验证部分**——
先跑第 5 节的基线命令确认当前状态,再从断点继续。
2. 密钥零接触:`.env`、`(服务器数据)`、`~/.dsh/secrets` —— 只读都不行。
3. 交付 = 代码 + 项目根 `PROGRESS_C-01.md`(格式见第 2 节)+ 关键命令的**原始输出**。
4. 验收标准全绿才可声明 done;跑不绿就写 partial/blocked 并附原始输出。
5. 完成后逐条对照第 1 节「验收标准」自检,未验证项必须写明——**虚报验收直接降级**。
---
## 1. 任务卡全文
# C-01 · 路由/代理前缀冲突修复
| 字段 | 值 |
|---|---|
| 项目 | 可乐工具 · `vscode/chunyu_project_react`(前端)+ `vscode/chunyu_project`(后端) |
| 优先级 | P0 · M1 |
| 建议模型 | deepseek-v4.1-flash(主)/ glm-5.3-flash(全量回归复核) |
| 依赖 | 无 |
| 预估 | 1 天 |
## 一、背景(为什么做)
2026-09-10 页面健康检查实测:**开发环境 22 条路由、生产环境 7 条路由被 API 代理劫持**——用户刷新页面、收藏链接、分享链接时直接打开 Django 404 或原始 JSON,而不是前端页面。SPA 内部点击导航不受影响,所以**藏得深、咬得狠**。
证据:`vscode/docs/页面健康检查报告.md`(问题清单 1.1 / 1.2)。
根因:前端路由与后端 API 共用了一组顶级路径段(`/api`、`/article`、`/learn`、`/user`、`/bug`、`/chat`、`/history`、`/message`、`/s`、`/search`、`/tool`),vite 代理用**前缀匹配**、生产 nginx 用**带尾斜杠的正则**,两边都会误伤前端路由。
## 二、目标(交付物)
1. **前端冲突路由改名**(推荐方案 A,报告结论"一劳永逸,趁早改便宜"):
- `/api` 及其子路由(`/api-directory`、`/api-docs`、`/api-detail/:id`、`/api/ip-location` 等 5 个数据 API 页)→ `/open-api/*`
- `/article/:id` → `/post/:id`(或 `/article-detail/:id`)
- `/user/:id` → `/user-home/:id`(项目已有 `/user-home` 路由,可复用)
2. **代理收窄**:vite 代理不再是宽前缀——改为**精确路径清单 / 正则白名单**(先枚举后端真实 URL:`chunyu_project/config/urls.py` + 各 app `urls.py`,以枚举结果为准)。
3. **生产 nginx 同步**:`nginx-docker.conf` 同样收窄,并覆盖 `/article/:id`、`/user/:id`、`/api/*` 五条数据页的冲突段。
4. **回归脚本**:提交一个可重复运行的 73 路由检查脚本(如 `scripts/check_routes.mjs`),检测逻辑三件套:① 是否返回 Django 404/JSON(劫持)② `#root` 是否空(白屏)③ 正文长度。
5. 全量更新 `navigation(...)` / 跳转引用 / 语言包中的路径引用。
## 三、执行步骤
```text
1. 枚举后端真实 API 路径(config/urls.py + 全部 app urls.py),产出一张「API 路径清单」
2. 对照前端 73 条路由,标记全部冲突项(不只报告里那 7 条)
3. 执行改名 + 同步全部引用(grep 旧路径确认零残留)
4. 收窄 vite 代理 → 启动 dev 双端,跑回归脚本至 73/73
5. 收窄 nginx-docker.conf,静态审查冲突模式段
6. 提交:代码 + 回归脚本 + 检查报告(贴进 PROGRESS)
```
## 四、验收标准
- [ ] dev 环境:73 条路由直访全部返回 SPA(无 Django 404/JSON 劫持、无白屏)
- [ ] 生产 nginx 配置:审查确认 7 条冲突段全部让路(含 `/article/:id`、`/user/:id`——核心分享场景)
- [ ] `grep -rn "旧路径"` 零残留(含 App.tsx、页面组件、i18n 词条)
- [ ] API 调用回归正常(登录、文章列表、工具使用等核心接口抽查 via 代理)
- [ ] SPA 内部导航抽查正常(首页→文章→详情)
- [ ] 回归脚本已入库,可被 CI 复用
## 五、验收命令(参考)
```bash
# 后端
cd chunyu_project && python manage.py runserver 8002
# 前端
cd chunyu_project_react && npm run dev # 5173
# 路由回归(脚本由本任务交付)
node scripts/check_routes.mjs --base http://127.0.0.1:5173
# 期望输出:PASS 73/73,hijacked=0,blank=0
# 引用残留检查
grep -rn "'/api-docs'\|'/api-directory'\|/article/\|/user/" src/ --include="*.tsx" --include="*.ts" | grep -v open-api
```
## 六、边界(不许做)
- 不动后端 API 的响应结构(纯路由层工作)
- 不重构页面组件内部逻辑
- 不修改 `docs/` 下既有报告
- 新增路由名后必须保留旧路径的**客户端重定向**(302 内的 SPA `Navigate`)——已分享出去的链接不能全死
## 七、交接
交付 = 代码 + `scripts/check_routes.mjs` + **PROGRESS_C-01.md**(含 73/73 输出与占位截图说明)。
格式见 `PLANNING/03-执行协议.md`。
---
## 2. 作业规范(协议要点)
## 一、接单流程(强制顺序)
```text
1. 通读任务卡 → 确认「边界」段(哪些文件不许动)
2. 读项目根 README + 最近 2 份 PROGRESS/迭代报告(了解项目惯例与雷区)
3. 跑「基线测试」——确认开工前测试是绿的;有红的先记录,不背锅
4. 实施(最小变更原则)
5. 跑「验收命令」——全绿
6. 写 PROGRESS_<任务ID>.md 到项目根
7. 交付:代码 + PROGRESS + 关键命令输出摘要
```
**禁止跳步**:不读文档就动手、不跑基线就改代码、不跑验收就宣称完成——三者任一发生,交付直接打回。
---
## 二、PROGRESS 文件格式(交付凭证)
写到**项目根目录**,文件名 `PROGRESS_<任务ID>.md`:
```markdown
# PROGRESS — <任务ID> <任务名>
- 状态:done | partial | blocked
- 执行模型:<模型名>
- 日期:YYYY-MM-DD
- 分支/提交:<commit hash 或"未提交">
## 三、质量红线(硬性)
1. **不许伪造**:测试跑不绿就写 partial/blocked,附原始输出。伪造一次 = 该模型在该项目永久降级为 C 级。
2. **不许删测试**:既有测试只许更绿。删测试用例、跳过断言、注释掉失败用例 = 交付无效。
3. **不许夹带**:任务卡外的一律不做。看到顺手能修的 bug,写进 PROGRESS 的「遗留问题」,不顺手改。
4. **不许碰密钥**:`.env`、`~/.dsh/secrets`、`(服务器数据)` 目录——只读都不许,物理隔离。
5. **不许破坏契约**:改 API 响应结构/字段名必须保持向后兼容或双写过渡,并在 PROGRESS 说明。
6. **不许强推**:不执行 `git push --force`、不 rebase 他人提交、不 git reset 别人的改动。
7. **不许超时硬干**:预计超预算时,先交「阶段报告 + 断点说明」,让调度器续派,不许烂尾。
---
## 四、各项目雷区(前人踩过的坑,直接记)
### 可乐工具(chunyu)
- **vite 代理前缀匹配过宽**:`/api` 会连 `/api-docs` 一起劫持——改代理时必须用精确规则(见 C-01)。
- **生产 nginx 正则带尾斜杠**:`^/(api|user|...)/` 依然劫持子路径,改完前端要同步审查 `nginx-docker.conf`。
- 前端 67 页组件,路由引用散落各处——改路由名必须 grep 全量 `navigation(` 调用。
- 后端全异步(adrf):新视图必须 async,不要写同步视图混挂。
### EnglishDrill
- **granian 孤儿 worker**:只杀父进程会留 worker 占端口,表现为"改了代码不生效"。用 `tools/devserver.ps1 restart`。
- **E2E 双模式**:`tests/e2e/run_e2e.py`(默认 ASGI 进程内 / `--http` 真实 granian)。隔离库 `backend/.tmp/e2e-db.sqlite3`,不碰开发库。
- **值收口层**:外部 id 必须走 `core/helpers.py` 的 `as_int` 等收口函数,禁止裸 `int()` 进 ORM(曾造成 44 处 5xx)。
- **stats 口径**:分类统计必须带 `is_deleted=False`,否则和 `/api/categories` 打架(BUG-2 教训)。
- 前端登录守卫在 `src/App.tsx:29`。
### dealerhub
- 460 passed / 4 skipped 是基线;4 个跳过是 PG 并发用例(D-02 处理)。
- 测试命令:`pytest`(在 `backend/`);前端 `npm test` + `npm run build`。
- `makemigrations --check --dry-run` 必须干净。
- 演示数据重建:`seed_demo --reset`;改数据模型要同步改 seed。
### DSP
- 双端:Web(Vue3)+ Android;后端 Django + Channels。
- 转码依赖本地 ffmpeg;nginx 托管静态视频。
- 无 E2E 套件——S-01 会建;在此之前以 `backend/tests` 为准。
### DSH / 基础设施
- `~/.dsh/settings.yaml` 是**热配置**:改前备份(`.bak_<日期>`),改后 YAML 解析校验。插件曾覆盖式写入破坏能力声明(2026-09-08 事故)。
- 测试:`server/` 下 `pytest -q`,用独立测试库 `postgres_test`,conftest 拒绝在生产库跑。
- 探活:`GET /healthz`。
---
---
## 3. 项目上下文
### 3.1 项目 README:`chunyu_project_react/README.md`
```markdown
# React + TypeScript + Vite
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
Currently, two official plugins are available:
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Oxc](https://oxc.rs)
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/)
## React Compiler
The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation).
## Expanding the ESLint configuration
If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules:
```js
export default defineConfig([
globalIgnores(['dist']),
{
files: ['**/*.{ts,tsx}'],
extends: [
// Other configs...
// Remove tseslint.configs.recommended and replace with this
tseslint.configs.recommendedTypeChecked,
// Alternatively, use this for stricter rules
tseslint.configs.strictTypeChecked,
// Optionally, add this for stylistic rules
tseslint.configs.stylisticTypeChecked,
// Other configs...
],
languageOptions: {
parserOptions: {
project: ['./tsconfig.node.json', './tsconfig.app.json'],
tsconfigRootDir: import.meta.dirname,
},
// other options...
},
},
])
```
You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules:
```js
// eslint.config.js
import reactX from 'eslint-plugin-react-x'
import reactDom from 'eslint-plugin-react-dom'
export default defineConfig([
globalIgnores(['dist']),
{
files: ['**/*.{ts,tsx}'],
extends: [
// Other configs...
// Enable lint rules for React
reactX.configs['recommended-typescript'],
// Enable lint rules for React DOM
reactDom.configs.recommended,
],
languageOptions: {
parserOptions: {
project: ['./tsconfig.node.json', './tsconfig.app.json'],
tsconfigRootDir: import.meta.dirname,
},
// other options...
},
},
])
```
…(已截断:原文 73 行 / 2425 字符)
```
### 3.2 最近 PROGRESS(0 份)
(该项目尚无 PROGRESS —— 本卡可能是首张卡)
---
## 4. 相关源码清单
卡内点名过的路径,逐个核实:
- `vscode/docs/页面健康检查报告.md` → 文件 docs/页面健康检查报告.md(vscode,5039 B,改于 2026-09-10 18:56)
- `nginx-docker.conf` → 文件 nginx-docker.conf(vscode,3149 B,改于 2026-09-05 11:49)
- `docs/` → 目录 chunyu_project_react/docs(chunyu)
- `PLANNING/03-执行协议.md` → 文件 PLANNING/03-执行协议.md(vscode,5486 B,改于 2026-09-11 22:37)
**未解析的线索**(卡里提到但当前树中找不到 —— 可能是路径漂移,接手时按候选或自行搜索确认):
- `chunyu_project/config/urls.py` —— 近似候选:chunyu_project/air_quality/urls.py、chunyu_project/api/urls.py、chunyu_project/apidirectory/urls.py
- `urls.py` —— 近似候选:chunyu_project/air_quality/urls.py、chunyu_project/api/urls.py、chunyu_project/apidirectory/urls.py
- `scripts/check_routes.mjs`
**项目根一级结构**(先看哪儿):
- `chunyu_project_react` → docs/,public/,src/,test-results/,tests/,Dockerfile,README.md,_add_timestamp.cjs,_patch_date_calc.cjs,_verify.cjs,add_login_record.cjs,article-editor-dark-final.png,article-editor-dark-rich.png,article-editor-dark-verified.png…
---
## 5. 基线测试命令与输出
打包时未执行(默认不跑,避免拖慢打包)。接手模型**必须先自己跑一遍**,把开工前的真实状态记进 PROGRESS 的「基线对照」:
- **前端类型/构建检查**(重)
```bash
cd chunyu_project_react && npm run build
```
期望:构建成功,无类型错误
- **后端 Django 系统检查**
```bash
cd chunyu_project && python manage.py check
```
期望:System check identified no issues
---
## 6. 收工检查(提交前逐条打勾)
```text
[ ] 第 1 节「验收标准」逐条已满足,且每条都有原始命令输出支撑
[ ] 「边界」段列出的文件一个都没动
[ ] 既有测试没被删/没被跳过(只许更绿)
[ ] 卡外的一律没夹带(顺手发现的 bug 写进 PROGRESS「遗留问题」)
[ ] PROGRESS_C-01.md 已写到项目根,格式符合第 2 节
[ ] 基线对照写了「开工前 → 完工后」两段真实数字
[ ] 未验证项已明确标注(不许虚报)
```
---
*本包由 `PLANNING/tools/handoff_bundle.py` 生成 · 规范见 `PLANNING/03-执行协议.md` §七*
+463
View File
@@ -0,0 +1,463 @@
# Handoff Bundle — D-02 PG 并发测试环境(激活 4 个 skipped)
生成时间:2026-09-12 01:08 +0800
项目:`dealerhub` 项目根:`C:\Users\12914\Desktop\gj\dealerhub`
优先级:P1 · 里程碑 M3 · 预估 0.5 天
建议模型:deepseek-v4-flash 复核:glm-5.3-flash
依赖:无
进度文件:`PROGRESS_D-02.md`(写到项目根)
> 这是一份**自包含**交接包:接手模型读完本节即可开工,不需要再问路径、命令、上下文。
> 卡内所有硬约束以第 1 节为准;本包的其余部分是上下文,**不得覆盖卡内约束**。
---
## 0. 接手须知(先读)
1. 本包是「D-02」的完整上下文。若你是接手上游的部分成果,**不要重做已验证部分**——
先跑第 5 节的基线命令确认当前状态,再从断点继续。
2. 密钥零接触:`.env`、`(服务器数据)`、`~/.dsh/secrets` —— 只读都不行。
3. 交付 = 代码 + 项目根 `PROGRESS_D-02.md`(格式见第 2 节)+ 关键命令的**原始输出**。
4. 验收标准全绿才可声明 done;跑不绿就写 partial/blocked 并附原始输出。
5. 完成后逐条对照第 1 节「验收标准」自检,未验证项必须写明——**虚报验收直接降级**。
---
## 1. 任务卡全文
# D-02 · PG 并发测试环境(激活 4 个 skipped)
| 字段 | 值 |
|---|---|
| 项目 | dealerhub · `Desktop/gj/dealerhub/backend` |
| 优先级 | P1 · M3 |
| 建议模型 | deepseek-v4-flash(主)/ glm-5.3-flash(复核) |
| 依赖 | 无 |
| 预估 | 0.5 天 |
## 一、背景(为什么做)
基线 `460 passed, 4 skipped`——4 个跳过用例全是 PostgreSQL 并发测试,需要 `config.settings.pgtest` 与可用 PG 实例。迭代 5 明确写了"本轮未伪造该环境结果"。**补齐测试矩阵最后一块,把"没验证"变成"已验证"。**
## 二、目标(交付物)
1. `config/settings/pgtest.py`:从环境变量读 PG 连接(或提供默认容器 DSN),并发测试专用。
2. 一条命令可建测试库并跑 4 个用例(附 Docker 命令或已有 PG 说明)。
3. 文档:`backend/docs/pgtest.md`(怎么起 PG、怎么跑、为什么这类测试必须真 PG)。
4. 4 个用例真实跑通(不再是 skip)。
## 三、执行步骤
```text
1. 找到 4 个 skip 用例(grep skip / pytest.mark.skipif)看它们的真实依赖
2. pgtest settings + 测试库隔离策略(绝不在开发/生产库上跑)
3. 本地起 PG(Docker 或复用现有实例)→ 跑通
4. 把命令固化进 README/docs
```
## 四、验收标准
- [ ] `pytest -q --settings=config.settings.pgtest` 下 **4 个原跳过用例 passed**(贴原始输出)
- [ ] 测试库与开发/生产严格隔离(DSN 不同、conftest 防护或文档明示)
- [ ] 文档包含:环境准备一条命令、运行一条命令、常见坑
- [ ] 默认 `pytest -q` 行为不变(不强制依赖 PG)
- [ ] 无生产配置变更
## 五、验收命令(参考)
```bash
# 起 PG(示例,按项目已有约定调整)
docker run -d --rm --name dh-pgtest -p 15433:5432 -e POSTGRES_USER=dh -e POSTGRES_PASSWORD=dh -e POSTGRES_DB=dh_test postgres:16-alpine
cd backend && pytest -q -k "concurren" --settings=config.settings.pgtest
```
## 六、边界(不许做)
- 不为了过测试放宽用例本身
- 不改生产 settings 默认值
- 不把测试库指向任何已有数据库
## 七、交接
写 `PROGRESS_D-02.md`(含 4 用例 passed 原始输出 + PG 启动命令)。
---
## 2. 作业规范(协议要点)
## 一、接单流程(强制顺序)
```text
1. 通读任务卡 → 确认「边界」段(哪些文件不许动)
2. 读项目根 README + 最近 2 份 PROGRESS/迭代报告(了解项目惯例与雷区)
3. 跑「基线测试」——确认开工前测试是绿的;有红的先记录,不背锅
4. 实施(最小变更原则)
5. 跑「验收命令」——全绿
6. 写 PROGRESS_<任务ID>.md 到项目根
7. 交付:代码 + PROGRESS + 关键命令输出摘要
```
**禁止跳步**:不读文档就动手、不跑基线就改代码、不跑验收就宣称完成——三者任一发生,交付直接打回。
---
## 二、PROGRESS 文件格式(交付凭证)
写到**项目根目录**,文件名 `PROGRESS_<任务ID>.md`:
```markdown
# PROGRESS — <任务ID> <任务名>
- 状态:done | partial | blocked
- 执行模型:<模型名>
- 日期:YYYY-MM-DD
- 分支/提交:<commit hash 或"未提交">
## 三、质量红线(硬性)
1. **不许伪造**:测试跑不绿就写 partial/blocked,附原始输出。伪造一次 = 该模型在该项目永久降级为 C 级。
2. **不许删测试**:既有测试只许更绿。删测试用例、跳过断言、注释掉失败用例 = 交付无效。
3. **不许夹带**:任务卡外的一律不做。看到顺手能修的 bug,写进 PROGRESS 的「遗留问题」,不顺手改。
4. **不许碰密钥**:`.env`、`~/.dsh/secrets`、`(服务器数据)` 目录——只读都不许,物理隔离。
5. **不许破坏契约**:改 API 响应结构/字段名必须保持向后兼容或双写过渡,并在 PROGRESS 说明。
6. **不许强推**:不执行 `git push --force`、不 rebase 他人提交、不 git reset 别人的改动。
7. **不许超时硬干**:预计超预算时,先交「阶段报告 + 断点说明」,让调度器续派,不许烂尾。
---
## 四、各项目雷区(前人踩过的坑,直接记)
### 可乐工具(chunyu)
- **vite 代理前缀匹配过宽**:`/api` 会连 `/api-docs` 一起劫持——改代理时必须用精确规则(见 C-01)。
- **生产 nginx 正则带尾斜杠**:`^/(api|user|...)/` 依然劫持子路径,改完前端要同步审查 `nginx-docker.conf`。
- 前端 67 页组件,路由引用散落各处——改路由名必须 grep 全量 `navigation(` 调用。
- 后端全异步(adrf):新视图必须 async,不要写同步视图混挂。
### EnglishDrill
- **granian 孤儿 worker**:只杀父进程会留 worker 占端口,表现为"改了代码不生效"。用 `tools/devserver.ps1 restart`。
- **E2E 双模式**:`tests/e2e/run_e2e.py`(默认 ASGI 进程内 / `--http` 真实 granian)。隔离库 `backend/.tmp/e2e-db.sqlite3`,不碰开发库。
- **值收口层**:外部 id 必须走 `core/helpers.py` 的 `as_int` 等收口函数,禁止裸 `int()` 进 ORM(曾造成 44 处 5xx)。
- **stats 口径**:分类统计必须带 `is_deleted=False`,否则和 `/api/categories` 打架(BUG-2 教训)。
- 前端登录守卫在 `src/App.tsx:29`。
### dealerhub
- 460 passed / 4 skipped 是基线;4 个跳过是 PG 并发用例(D-02 处理)。
- 测试命令:`pytest`(在 `backend/`);前端 `npm test` + `npm run build`。
- `makemigrations --check --dry-run` 必须干净。
- 演示数据重建:`seed_demo --reset`;改数据模型要同步改 seed。
### DSP
- 双端:Web(Vue3)+ Android;后端 Django + Channels。
- 转码依赖本地 ffmpeg;nginx 托管静态视频。
- 无 E2E 套件——S-01 会建;在此之前以 `backend/tests` 为准。
### DSH / 基础设施
- `~/.dsh/settings.yaml` 是**热配置**:改前备份(`.bak_<日期>`),改后 YAML 解析校验。插件曾覆盖式写入破坏能力声明(2026-09-08 事故)。
- 测试:`server/` 下 `pytest -q`,用独立测试库 `postgres_test`,conftest 拒绝在生产库跑。
- 探活:`GET /healthz`。
---
---
## 3. 项目上下文
### 3.1 项目 README:`C:/Users/12914/Desktop/gj/dealerhub/README.md`
```markdown
# dealerhub · 经销商 SaaS
> 类似管家婆的进销存+财务+分销管理 SaaS。技术栈:**Django 5.2 + DRF + adrf + Granian + PostgreSQL**。
## 目录结构
```
dealerhub/
├── PLAN.md # 总体实施计划(5 个阶段)
├── README.md # 本文件
└── backend/ # Django 后端
├── manage.py
├── requirements.in
├── pytest.ini
├── conftest.py
├── config/ # 项目配置(settings/{base,dev,prod,test}.py + asgi.py)
├── apps/ # 业务应用
│ ├── core/ # 多租户/多组织/审计日志
│ └── catalog/ # 商品中心
├── tests/ # 集成测试
└── .env
```
## 快速开始
### 1. 安装依赖(本机已有 Django 5.2.16 + DRF + adrf + Granian 2.8)
```bash
pip install -r backend/requirements.txt
```
### 2. 初始化数据库(SQLite)
```bash
cd backend
python manage.py migrate
python manage.py seed_initial_data
python manage.py createsuperuser
```
### 3. 启动 Granian(ASGI)
```bash
cd backend
sh infra/scripts/run_dev.sh
# 或直接:
DJANGO_SETTINGS_MODULE=config.settings.dev granian config.asgi:application \
--interface asgi --host 0.0.0.0 --port 8000 --reload
```
### 4. 验证
```bash
curl http://127.0.0.1:8000/api/v1/ping/
```
返回示例:
```json
{
"ok": true,
"service": "dealerhub",
"ts": "2026-09-07T00:30:00+00:00",
"versions": {
"python": "3.12.x",
"django": "5.2.x",
"granian": "2.8.x"
}
}
```
### 5. 运行测试
```bash
cd backend
pytest
```
### 6. 构建生产镜像(自包含前端)
生产镜像使用仓库根目录作为构建上下文,Docker 多阶段构建会自动编译 Vue 前端并把产物交给 Django:
```bash
# 在仓库根目录执行
DJANGO_SECRET_KEY='replace-me' POSTGRES_PASSWORD='replace-me' \\
docker compose -f backend/infra/docker/docker-compose.yml config
DJANGO_SECRET_KEY='replace-me' POSTGRES_PASSWORD='replace-me' \\
docker compose -f backend/infra/docker/docker-compose.yml build backend
```
本地演示账套:
…(已截断:原文 144 行 / 3061 字符)
```
### 3.2 最近 PROGRESS(2 份)
#### `C:/Users/12914/Desktop/gj/dealerhub/PROGRESS_AGI_ITERATION_5.md`
```markdown
# AGI 迭代第 5 轮 · 可运行闭环与回归收口
日期:2026-09-11
## 本轮完成
### 构建与部署基线
- 修复 `backend/infra/scripts/run_dev.sh` 工作目录解析,脚本从任意调用目录都能定位 `backend/manage.py`。
- `backend/infra/docker/Dockerfile.backend` 改为 Node + Python 多阶段构建,镜像内自动执行 `npm ci` / `npm run build`,不再依赖宿主机预生成 `backend/frontend_dist`。
- 修正 `backend/infra/docker/docker-compose.yml` 的构建上下文与 Dockerfile 路径。
- 新增根目录 `.dockerignore`,排除 SQLite、日志、缓存、node_modules 和本地构建产物。
- 对齐 `requirements.in` / `requirements.txt`,补齐 `python-dateutil`,统一 psycopg 安装声明。
- 修复 PostgreSQL 准备脚本把探测到的密码传入迁移阶段的问题。
- 清理 `apps/finance/management/__init__.py` 中重复的 management command 实现。
### 演示数据
- `seed_demo --reset` 现在会清理商城订单、订单行、商城账号和商品授权,避免重建残留。
- 演示账龄数据补齐 `0-30`、`31-60`、`61-90`、`91-180`、`181-365`、`365+` 六个桶。
- 回归测试覆盖完整 storefront reset 和六桶账龄。
### 交易与库存
- 销售页保留用户手工改价,不因数量/客户刷新而覆盖;重新取价改为显式操作。
- 最低售价统一按基本单位价格比较,修复 source unit 重复换算。
- 采购页使用成本价而非销售价,并按录入单位显示金额,提交 `source_quantity`。
- 商品单位接口为换算单位返回对应 `cost_price`,同时保留销售价。
- 批次页近效期筛选改为后端过滤并重置分页;新增 `near_expiry` / `expiry_days` 参数。
- 新增前端 `src/utils/transaction.js` 和 Node 原生单测,固定单位换算、金额和边界校验口径。
### 通知与会话
- 通知列表只返回当前租户广播和当前用户通知。
- `mark-read` 不能修改其他用户的私有通知。
- `mark-all-read` 只处理广播和当前用户通知,不再批量改动其他用户状态。
- 普通登录和退出时清理演示只读、商城会话状态;主布局监听 storage 变化,租户/只读标签不再陈旧。
### 开放平台与表单
- 商品新增表单增加必填与正价格校验。
- API Key 管理页显示过期时间并支持确认吊销。
- 前端增加 `npm test`,覆盖交易金额、换算和边界校验。
## 验证结果
```text
backend: 460 passed, 4 skipped
frontend: npm run build ✅
frontend: npm test ✅ (3 tests)
Django check ✅
makemigrations --check --dry-run ✅
bash -n backend/infra/scripts/run_dev.sh ✅
```
4 个跳过用例是 PostgreSQL 并发测试,需要 `config.settings.pgtest` 与可用的 PostgreSQL 实例;本轮未伪造该环境结果。Dockerfile/Compose 配置已通过静态路径与插值检查,完整镜像构建需启动 Docker Desktop。
## 下一轮最高价值项
1. 为用户—租户增加服务端 membership/授权关系,补跨租户切换回归。
2. 把收款/付款分配 service 接入 REST action,并补 API 集成测试。
3. 在 PostgreSQL 上执行并发过账、库存和核销测试,审查 `EXPLAIN ANALYZE`。
4. 补齐前端 Voucher、Channel、StorefrontAdmin 的完整表单校验与分页。
5. 实现审计日志保留/归档策略和正式部署 readiness gate。
…(已截断:原文 64 行 / 1929 字符)
```
#### `C:/Users/12914/Desktop/gj/dealerhub/PROGRESS_AGI_ITERATION_4.md`
```markdown
# dealerhub · AGI 自主迭代报告(第 4 轮 · 并发序列 + N+1 优化)
> **承接**:第 3 轮报告末尾提出的"`_generate_bill_no` 用 count()+1 取序号,并发下有竞态"。
> 本轮坐实并修复,同时扫描出**另外 4 处同类问题**;顺带做性能基线,
> 发现并修复列表接口的 **N+1**(5.3 倍提速)。
> **回归**:SQLite **454 passed** / PostgreSQL **458 passed**,`check` 0 issues。
---
## 一、单号生成竞态(PG 实测坐实)
### 症状
9 个并发建单 → **多个拿到同一单号**:
```
IntegrityError: 重复键违反唯一约束 "sales_bill_tenant_id_bill_no_uniq"
DETAIL: 键值"(tenant_id, bill_no)=(1, XS202609110001)" 已经存在
```
### 根因:`count() + 1` 有两个缺陷
```python
seq = Model.objects.filter(tenant=tenant, bill_no__startswith=head).count() + 1
```
1. **并发撞号**:两个事务同时 `count()` 拿到同一个数 → 生成相同单号
2. **删除后复用**(更隐蔽):建了 001/002 后删掉 001 → `count()=1` → 下一张又是 002
→ 撞唯一约束(因为 002 已存在)
### 修复:`MAX(序号)+1` + 唯一约束重试
新增共享工具(`apps/core/services.py`):
```python
next_bill_no(tenant, prefix, model, *, date_str=None, field="bill_no")
# 扫已有单号取 MAX(序号),而非 count() —— 删除后不回退
create_with_unique_bill_no(model, *, tenant, prefix, defaults, field="bill_no")
# 取号 → 尝试创建(保存点包裹)→ 撞唯一约束则换号重试,最多 20 次
```
**为什么用重试而不是锁**:`select_for_update()` **锁不住不存在的行**
(这是第 3 轮修库存竞态时学到的教训)。乐观重试更适合"创建时取名"场景。
**保存点必不可少**:`IntegrityError` 会把外层事务标记为 aborted,
后续任何查询都报 `TransactionManagementError`。
### 修复范围:5 处同类序列
| 位置 | 用途 | 修复方式 |
|---|---|---|
| `sales.services._generate_bill_no` | 销售单 XS | `create_with_unique_bill_no` |
| `purchase.services._generate_bill_no` | 进货单 PB | 同上 |
| `storefront.services._next_order_no` | 商城单 HD | 同上(`field="order_no"`) |
| `channel.services` | 电商转单 SO | `create_with_unique_bill_no` |
| `finance.services._generate_ar_ap_no` | 应收/应付 RC/PY | `next_bill_no` + 凭证号重试 |
| `finance.services` 凭证号 V | 记账凭证 | 保存点 + 换号重试 |
### 验证(3 个场景)
```python
# 1. 并发
test_concurrent_bill_no_generation_unique # 9 线程并发建单,单号必须全唯一
# 2. 删除后不复用
test_bill_no_not_reused_after_deletion # 建 001/002/003 → 删 001 → 新单必须是 004
# 3. 空洞不回退
test_bill_no_survives_gap # 人工造 0099 → 新单必须是 0100(不填空洞)
```
…(已截断:原文 303 行 / 7825 字符)
```
---
## 4. 相关源码清单
卡内点名过的路径,逐个核实:
(卡内未点名具体文件)
**未解析的线索**(卡里提到但当前树中找不到 —— 可能是路径漂移,接手时按候选或自行搜索确认):
- `config/settings/pgtest.py` —— 近似候选:C:/Users/12914/Desktop/gj/dealerhub/backend/config/settings/pgtest.py
- `backend/docs/pgtest.md`
- `PROGRESS_D-02.md`
**项目根一级结构**(先看哪儿):
- `C:/Users/12914/Desktop/gj/dealerhub` → backend/,docs/,frontend/,NEXT_PLAN.md,PLAN.md,PROGRESS.md,PROGRESS_AGI_ITERATION_1.md,PROGRESS_AGI_ITERATION_2.md,PROGRESS_AGI_ITERATION_3.md,PROGRESS_AGI_ITERATION_4.md,PROGRESS_AGI_ITERATION_5.md,PROGRESS_BATCH_A.md,PROGRESS_BATCH_BC.md,PROGRESS_BATCH_D.md…
---
## 5. 基线测试命令与输出
打包时未执行(默认不跑,避免拖慢打包)。接手模型**必须先自己跑一遍**,把开工前的真实状态记进 PROGRESS 的「基线对照」:
- **后端测试**(重)
```bash
cd C:/Users/12914/Desktop/gj/dealerhub/backend && python -m pytest -q
```
期望:460 passed, 4 skipped(4 个 skipped 是 PG 并发用例,D-02 处理)
- **迁移检查**
```bash
cd C:/Users/12914/Desktop/gj/dealerhub/backend && python manage.py makemigrations --check --dry-run
```
期望:No changes detected
---
## 6. 收工检查(提交前逐条打勾)
```text
[ ] 第 1 节「验收标准」逐条已满足,且每条都有原始命令输出支撑
[ ] 「边界」段列出的文件一个都没动
[ ] 既有测试没被删/没被跳过(只许更绿)
[ ] 卡外的一律没夹带(顺手发现的 bug 写进 PROGRESS「遗留问题」)
[ ] PROGRESS_D-02.md 已写到项目根,格式符合第 2 节
[ ] 基线对照写了「开工前 → 完工后」两段真实数字
[ ] 未验证项已明确标注(不许虚报)
```
---
*本包由 `PLANNING/tools/handoff_bundle.py` 生成 · 规范见 `PLANNING/03-执行协议.md` §七*