diff --git a/backend/apps/factory/migrations/0005_add_business_info.py b/backend/apps/factory/migrations/0005_add_business_info.py new file mode 100644 index 0000000..abf50e5 --- /dev/null +++ b/backend/apps/factory/migrations/0005_add_business_info.py @@ -0,0 +1,34 @@ +# Generated migration for factory app + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('factory', '0004_rename_brand_to_short_name'), + ] + + operations = [ + migrations.AddField( + model_name='factory', + name='unified_social_credit_code', + field=models.CharField(blank=True, max_length=18, null=True, verbose_name='统一社会信用代码'), + ), + migrations.AddField( + model_name='factory', + name='cooperation_mode', + field=models.CharField( + blank=True, + choices=[('direct', '厂家直供'), ('authorized', '授权代理商')], + max_length=20, + null=True, + verbose_name='合作模式', + ), + ), + migrations.AddField( + model_name='factory', + name='interaction_capability', + field=models.TextField(blank=True, null=True, verbose_name='交互能力'), + ), + ] diff --git a/backend/apps/factory/models.py b/backend/apps/factory/models.py index 8671dff..a8297eb 100644 --- a/backend/apps/factory/models.py +++ b/backend/apps/factory/models.py @@ -1,5 +1,12 @@ from django.db import models + +COOPERATION_MODE_CHOICES = [ + ('direct', '厂家直供'), + ('authorized', '授权代理商'), +] + + class Factory(models.Model): """ 工厂模型 @@ -8,6 +15,9 @@ class Factory(models.Model): product_category = models.CharField(max_length=255, blank=True, null=True, verbose_name='产品分类') factory_name = models.CharField(max_length=255, verbose_name='生产工厂全称') short_name = models.CharField(max_length=100, unique=True, verbose_name='供应商简称') + unified_social_credit_code = models.CharField(max_length=18, blank=True, null=True, verbose_name='统一社会信用代码') + cooperation_mode = models.CharField(max_length=20, choices=COOPERATION_MODE_CHOICES, blank=True, null=True, verbose_name='合作模式') + interaction_capability = models.TextField(blank=True, null=True, verbose_name='交互能力') province = models.CharField(max_length=50, verbose_name='省') city = models.CharField(max_length=50, verbose_name='市') district = models.CharField(max_length=50, blank=True, null=True, verbose_name='区') diff --git a/backend/apps/factory/serializers.py b/backend/apps/factory/serializers.py index e869290..1cb9b0b 100644 --- a/backend/apps/factory/serializers.py +++ b/backend/apps/factory/serializers.py @@ -1,5 +1,10 @@ +import re + from rest_framework import serializers -from .models import Factory +from .models import Factory, COOPERATION_MODE_CHOICES + + +USCC_PATTERN = re.compile(r'^[0-9A-Z]{18}$') class FactorySerializer(serializers.ModelSerializer): @@ -8,14 +13,25 @@ class FactorySerializer(serializers.ModelSerializer): """ material_count = serializers.SerializerMethodField() usernames = serializers.SerializerMethodField() + cooperation_mode_display = serializers.CharField(source='get_cooperation_mode_display', read_only=True) + unified_social_credit_code = serializers.CharField( + required=True, allow_null=False, allow_blank=False, max_length=18 + ) + cooperation_mode = serializers.ChoiceField( + choices=COOPERATION_MODE_CHOICES, required=True, allow_null=False, allow_blank=False + ) class Meta: model = Factory fields = ['id', 'dealer_name', 'product_category', 'factory_name', - 'short_name', 'province', 'city', 'district', + 'short_name', 'unified_social_credit_code', + 'cooperation_mode', 'cooperation_mode_display', + 'interaction_capability', + 'province', 'city', 'district', 'address', 'website', 'created_at', 'updated_at', 'material_count', 'usernames'] - read_only_fields = ['id', 'created_at', 'updated_at', 'material_count', 'usernames'] + read_only_fields = ['id', 'created_at', 'updated_at', 'material_count', + 'usernames', 'cooperation_mode_display'] def get_material_count(self, obj): return obj.materials.count() @@ -23,16 +39,24 @@ class FactorySerializer(serializers.ModelSerializer): def get_usernames(self, obj): return list(obj.users.values_list('username', flat=True)) + def validate_unified_social_credit_code(self, value): + if not USCC_PATTERN.match(value or ''): + raise serializers.ValidationError('统一社会信用代码必须为 18 位数字或大写字母') + return value + class FactoryListSerializer(serializers.ModelSerializer): """ 工厂列表序列化器(简化版) """ usernames = serializers.SerializerMethodField() + cooperation_mode_display = serializers.CharField(source='get_cooperation_mode_display', read_only=True) class Meta: model = Factory - fields = ['id', 'factory_name', 'short_name', 'province', 'city', 'dealer_name', 'usernames'] + fields = ['id', 'factory_name', 'short_name', 'province', 'city', + 'dealer_name', 'usernames', + 'cooperation_mode', 'cooperation_mode_display'] def get_usernames(self, obj): return list(obj.users.values_list('username', flat=True)) diff --git a/docs/superpowers/specs/2026-04-24-supplier-business-info-design.md b/docs/superpowers/specs/2026-04-24-supplier-business-info-design.md new file mode 100644 index 0000000..0721211 --- /dev/null +++ b/docs/superpowers/specs/2026-04-24-supplier-business-info-design.md @@ -0,0 +1,81 @@ +# 供应商新增工商与合作信息字段 — 设计文档 + +**日期**:2026-04-24 +**范围**:在 `Factory`(供应商)模型上新增 3 个业务字段,并在前端列表、表单、详情页同步展示/编辑。 + +## 目标 + +满足供应商资料登记的新业务要求: +1. 工商注册信息 — 本期仅记录**统一社会信用代码**(其余工商字段本期不做) +2. **交互能力** — 包含最大月供量、常规库存量、生产周期、运输方式、应急供货响应时间等信息(本期先用单个大文本框承载) +3. **合作模式** — 厂家直供 / 授权代理商 二选一 + +## 新增字段 + +| 字段 | DB 类型 | DB 约束 | 表单必填 | 校验规则 | +|---|---|---|---|---| +| `unified_social_credit_code` 统一社会信用代码 | `CharField(max_length=18)` | `null=True, blank=True` | ✅ 必填 | 正则 `^[0-9A-Z]{18}$` — 长度 18,仅数字 + 大写字母 | +| `interaction_capability` 交互能力 | `TextField` | `null=True, blank=True` | 选填 | 无;前端 textarea 的 placeholder 提示:"最大月供量 / 常规库存量 / 生产周期 / 运输方式 / 应急供货响应时间" | +| `cooperation_mode` 合作模式 | `CharField(max_length=20, choices=COOPERATION_MODE_CHOICES)` | `null=True, blank=True` | ✅ 必填 | choices: `direct='厂家直供'`, `authorized='授权代理商'` | + +### 存量数据策略 + +三个字段在数据库层均允许为空(`null=True, blank=True`),用以兼容已有的供应商记录。前端表单和后端 serializer 在新增/编辑时强制校验"必填"字段。 + +对用户行为的影响: +- 历史供应商记录在列表/详情页展示为 `-` +- 用户第一次编辑历史供应商时,两个"必填"字段必须补齐才能保存,否则前/后端都会拦截 + +### 校验位置 + +| 校验点 | 统一社会信用代码 | 合作模式 | 交互能力 | +|---|---|---|---| +| 前端 `el-form rules` | 必填 + 正则 | 必填(在 choices 内) | 无 | +| 后端 `serializer.validate_*` | 必填 + 正则 | 必填(在 choices 内) | 无 | + +后端 `null=True, blank=True` 的字段在序列化器层面仍然通过 `required=True` 覆盖为必填。 + +## 改动清单 + +### 后端 `backend/apps/factory/` + +- **`models.py`**: + - 在 `Factory` 模型上新增 3 个字段 + - 新增常量 `COOPERATION_MODE_CHOICES = [('direct', '厂家直供'), ('authorized', '授权代理商')]` +- **`migrations/0005_add_business_info.py`**:新增 3 字段的 Django migration +- **`serializers.py`**: + - `FactorySerializer.Meta.fields` 追加 3 个字段 + - `FactorySerializer` 新增 `cooperation_mode_display` 只读字段(`source='get_cooperation_mode_display'`) + - `FactorySerializer` 新增 `validate_unified_social_credit_code` 方法做正则校验 + 必填 + - `FactorySerializer` 对 `unified_social_credit_code` 和 `cooperation_mode` 显式 `required=True, allow_null=False, allow_blank=False` 覆盖模型的 null/blank + - `FactoryListSerializer.Meta.fields` 追加 `cooperation_mode` 和 `cooperation_mode_display` + +### 前端 `frontend/src/views/` + +- **`FactoryManage.vue`**: + - 列表在"经销商"列之后新增「合作模式」列(`min-width: 120`,显示 `cooperation_mode_display`) + - dialog 表单新增 3 个表单项: + - 统一社会信用代码 — ``,含 form rules(必填 + 正则) + - 合作模式 — ``(厂家直供 / 授权代理商),含 form rules(必填) + - 交互能力 — ``,提示语 placeholder,3~6 行高度 + - `form` reactive 对象新增 3 个字段;`resetForm()` 同步新增 + - `onSubmit` 前走 `formRef.validate()`;新增 form rules 定义 +- **`FactoryDetail.vue`**: + - `` 新增 3 行:统一社会信用代码、合作模式(使用 `cooperation_mode_display`)、交互能力 + - 交互能力内容可能较长,需保持换行(`white-space: pre-wrap`) + +## 非目标(YAGNI) + +- 工商注册信息中的注册资本/注册地址/法人代表/营业期限/实缴资本/认缴资本等字段本期不做 +- 资质证书(生产许可证、安全生产许可证、绿色建材认证、ISO 三体系等)本期不做 +- 交互能力本期不拆分为结构化子字段(如独立的月供量/库存量/生产周期字段)。后续如有按字段检索/对比的需求再演进 +- 统一社会信用代码本期不做末位校验码算法验证 +- 文件上传、附件管理本期不涉及 + +## 验收 + +- [ ] 新建供应商:填完所有必填字段(含新增的 2 个必填)能成功保存;任一必填字段留空或格式错时,前/后端都拦截 +- [ ] 编辑历史供应商(历史字段为空):必须补齐两个必填字段后才能保存 +- [ ] 列表页展示「合作模式」列,值为中文标签("厂家直供"/"授权代理商"),历史数据显示 `-` +- [ ] 详情页展示三个新字段;历史数据显示 `-` +- [ ] 统一社会信用代码校验:长度非 18 或含小写字母/非字母数字字符被拦截 diff --git a/frontend/src/views/FactoryDetail.vue b/frontend/src/views/FactoryDetail.vue index 089c96d..0a8cd55 100644 --- a/frontend/src/views/FactoryDetail.vue +++ b/frontend/src/views/FactoryDetail.vue @@ -8,10 +8,15 @@ {{ displayText(factory.factory_name) }} {{ displayText(factory.short_name) }} + {{ displayText(factory.unified_social_credit_code) }} + {{ displayText(factory.cooperation_mode_display) }} {{ displayText(factory.dealer_name) }} {{ displayText(factory.product_category) }} {{ displayRegion(factory) }} {{ displayText(factory.address) }} + + {{ displayText(factory.interaction_capability) }} + formatRegion(item.province, item.city, item.dist color: var(--brand-500); word-break: break-all; } + +.multiline { + white-space: pre-wrap; +} diff --git a/frontend/src/views/FactoryManage.vue b/frontend/src/views/FactoryManage.vue index d70077d..ed9946a 100644 --- a/frontend/src/views/FactoryManage.vue +++ b/frontend/src/views/FactoryManage.vue @@ -9,6 +9,11 @@ + + +