docs: 按评审意见修正品牌设计文档

- 迁移编号按仓库现状调整为 factory/0004、material/0007、material/0008
- 说明本次是反向 rename(factory_short_name → brand → short_name)
- 统一 API 权限表述:读接口所有已认证用户,写接口 admin
- 数据迁移脚本改为按品牌批量 update
- 风险章节补充 CSS 变量 --brand-* 在 grep 时需排除

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
caoqianming 2026-04-23 14:44:26 +08:00
parent 08b855794f
commit d96140795e
1 changed files with 28 additions and 24 deletions

View File

@ -39,7 +39,7 @@
| 数据迁移 | 自动迁移(从 Factory.short_name 唯一值自动建 Brand 并回填 Material.brand | 历史数据一步到位 |
| 迁移颗粒度 | 单次大迁移schema + data 合并发布) | 数据量小,一次到位更省事 |
| 菜单位置 | 顶层一级菜单"品牌库"(供应商库与材料管理之间) | 作为核心业务数据,位置显眼 |
| 权限 | 仅 admin 可见可编辑 | 品牌是管理员维护的主数据 |
| 权限 | 管理界面(品牌库菜单+CRUD 页面)仅 admin读接口对所有已认证用户开放供材料表单下拉读取 | 品牌是管理员维护的主数据,但非 admin 需要在录入材料时选择品牌 |
| 品牌删除行为 | PROTECT前端 catch 错误并提示 | 防止误删丢关联 |
| 品牌库搜索 | 仅按 name 模糊搜索 | description 语义不适合搜索 |
| Material 列表 | 增加"品牌"列 + 品牌筛选器 | 与现有 factory 筛选保持一致 |
@ -117,15 +117,17 @@ brand = models.ForeignKey(
### 迁移顺序
仓库现有 migration 最新状态:`factory/0003_rename_factory_short_name_to_brand.py`(此前字段原名 `factory_short_name`,在 0003 被 rename 为 `brand`;本次是反向 rename恢复为 `short_name` 以消除歧义),`material/0006_alter_material_options_and_more.py`。因此本次新增 migration 编号如下:
1. `brand/migrations/0001_initial.py` — 建 Brand 表
2. `factory/migrations/0002_rename_brand_short_name.py` — 重命名字段(手写 `migrations.RenameField`,避免依赖 makemigrations 的交互式识别)
3. `material/migrations/0002_add_brand_fk.py` — 新增 Material.brand 外键(依赖 brand.0001
4. `material/migrations/0003_populate_brand.py` — 数据迁移(`RunPython`,依赖 factory.0002 和 material.0002
2. `factory/migrations/0004_rename_brand_to_short_name.py` — 重命名字段(手写 `migrations.RenameField`,避免依赖 makemigrations 的交互式识别dependencies 指向 `('factory', '0003_rename_factory_short_name_to_brand')`
3. `material/migrations/0007_add_brand_fk.py` — 新增 Material.brand 外键(依赖 `brand.0001` 与 `material.0006_alter_material_options_and_more`
4. `material/migrations/0008_populate_brand.py` — 数据迁移(`RunPython`,依赖 `factory.0004`、`material.0007`、`brand.0001`
### 数据迁移脚本
```python
# material/migrations/0003_populate_brand.py
# material/migrations/0008_populate_brand.py
from django.db import migrations
def forwards(apps, schema_editor):
@ -134,17 +136,19 @@ def forwards(apps, schema_editor):
Material = apps.get_model('material', 'Material')
# 1) 从 Factory.short_name 唯一值创建 Brand
for sn in Factory.objects.values_list('short_name', flat=True).distinct():
if sn:
Brand.objects.get_or_create(name=sn)
short_names = (
Factory.objects
.exclude(short_name__isnull=True)
.exclude(short_name='')
.values_list('short_name', flat=True)
.distinct()
)
for sn in short_names:
Brand.objects.get_or_create(name=sn)
# 2) 回填 Material.brand
for material in Material.objects.select_related('factory').all():
if material.factory and material.factory.short_name:
brand = Brand.objects.filter(name=material.factory.short_name).first()
if brand:
material.brand = brand
material.save(update_fields=['brand'])
# 2) 回填 Material.brand按品牌批量 update避免逐条 save
for brand in Brand.objects.all():
Material.objects.filter(factory__short_name=brand.name).update(brand=brand)
def backwards(apps, schema_editor):
# schema migration 回滚时会删掉 Material.brand 字段,这里无需操作
@ -152,8 +156,8 @@ def backwards(apps, schema_editor):
class Migration(migrations.Migration):
dependencies = [
('material', '0002_add_brand_fk'),
('factory', '0002_rename_brand_short_name'),
('material', '0007_add_brand_fk'),
('factory', '0004_rename_brand_to_short_name'),
('brand', '0001_initial'),
]
operations = [migrations.RunPython(forwards, backwards)]
@ -161,13 +165,13 @@ class Migration(migrations.Migration):
### API 端点
`/api/brand/`(仅 admin 写,读可对所有登录用户开放,但菜单只 admin 可见):
`/api/brand/` 权限约定:**所有已认证用户可读,仅 admin 可写**(非 admin 用户在材料表单的品牌下拉需要读这个接口,所以不能整体 admin-only。"决策总表"里的"仅 admin 可见可编辑"特指前端**菜单与管理界面**的可见/编辑权限,不影响读接口。
- `GET /api/brand/` — 列表,支持 `search=<name>` 模糊搜索与分页
- `POST /api/brand/` — 创建
- `GET /api/brand/{id}/` — 详情
- `PUT /api/brand/{id}/` — 更新
- `DELETE /api/brand/{id}/` — 删除;若被 Material 引用则 PROTECT 报错viewset 需捕获 `ProtectedError` 返回 400 + 中文提示
- `GET /api/brand/` — 列表,支持 `search=<name>` 模糊搜索与分页(已认证用户)
- `POST /api/brand/` — 创建admin
- `GET /api/brand/{id}/` — 详情(已认证用户)
- `PUT /api/brand/{id}/` — 更新admin
- `DELETE /api/brand/{id}/` — 删除admin;若被 Material 引用则 PROTECT 报错viewset 需捕获 `ProtectedError` 返回 400 + 中文提示
Material 现有接口调整:
@ -259,7 +263,7 @@ Factory 接口调整:
- **Django rename migration 的交互坑**`makemigrations` 会交互询问是否重命名,自动化环境不友好。**应对**:手写 `migrations.RenameField`
- **数据迁移边界**`Factory.short_name` 为空 / None 时跳过;多个 Factory 相同 short_name 时 `get_or_create` 正确Material 无 factory 时 brand 留空。
- **前端 FactoryManage 对 brand 的遗留引用**rename 时前端所有 `brand` 引用都要同步改grep 检查避免漏网。
- **前端 FactoryManage 对 brand 的遗留引用**rename 时前端所有 `brand` 引用都要同步改grep 检查避免漏网。注意 `frontend/src/styles/base.css``MainLayout.vue``--brand-900` / `--brand-950` CSS 变量,是设计 token 命名与业务字段无关grep 时需排除。
- **品牌删除 PROTECT 错误**DRF 默认 `ProtectedError` 抛 500。**应对**viewset 的 `destroy` 捕获并返回 400 + 中文提示。
- **回滚路径**:顺序反向 `migrate` 即可material → factory → brand zero。`backwards` 保持 no-op。