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:
parent
08b855794f
commit
d96140795e
|
|
@ -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。
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue