Files
vscode-workbench/PLANNING/bundles/bundle-D-02.md
T

464 lines
18 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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` §七*