feat: 供应商新增统一社会信用代码、合作模式、交互能力字段

- 模型加 unified_social_credit_code / cooperation_mode / interaction_capability,DB 均可空以兼容存量数据
- 序列化器显式 required=True 并做 18 位大写字母数字正则校验
- 列表页加「合作模式」列;新增/编辑表单加对应控件及校验规则
- 详情页展示三项新字段,交互能力保留换行
- 存量数据策略:DB 允许空、表单必填;旧数据首次编辑需补齐
This commit is contained in:
caoqianming 2026-04-24 10:10:56 +08:00
parent 3a305b4a7e
commit faff711915
6 changed files with 243 additions and 12 deletions

View File

@ -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='交互能力'),
),
]

View File

@ -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='')

View File

@ -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))

View File

@ -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 个表单项:
- 统一社会信用代码 — `<el-input>`,含 form rules必填 + 正则)
- 合作模式 — `<el-select>`(厂家直供 / 授权代理商),含 form rules必填
- 交互能力 — `<el-input type="textarea">`,提示语 placeholder3~6 行高度
- `form` reactive 对象新增 3 个字段;`resetForm()` 同步新增
- `onSubmit` 前走 `formRef.validate()`;新增 form rules 定义
- **`FactoryDetail.vue`**
- `<el-descriptions>` 新增 3 行:统一社会信用代码、合作模式(使用 `cooperation_mode_display`)、交互能力
- 交互能力内容可能较长,需保持换行(`white-space: pre-wrap`
## 非目标YAGNI
- 工商注册信息中的注册资本/注册地址/法人代表/营业期限/实缴资本/认缴资本等字段本期不做
- 资质证书生产许可证、安全生产许可证、绿色建材认证、ISO 三体系等)本期不做
- 交互能力本期不拆分为结构化子字段(如独立的月供量/库存量/生产周期字段)。后续如有按字段检索/对比的需求再演进
- 统一社会信用代码本期不做末位校验码算法验证
- 文件上传、附件管理本期不涉及
## 验收
- [ ] 新建供应商:填完所有必填字段(含新增的 2 个必填)能成功保存;任一必填字段留空或格式错时,前/后端都拦截
- [ ] 编辑历史供应商(历史字段为空):必须补齐两个必填字段后才能保存
- [ ] 列表页展示「合作模式」列,值为中文标签("厂家直供"/"授权代理商"),历史数据显示 `-`
- [ ] 详情页展示三个新字段;历史数据显示 `-`
- [ ] 统一社会信用代码校验:长度非 18 或含小写字母/非字母数字字符被拦截

View File

@ -8,10 +8,15 @@
<el-descriptions :column="1" border class="detail-descriptions">
<el-descriptions-item label="供应商全称">{{ displayText(factory.factory_name) }}</el-descriptions-item>
<el-descriptions-item label="供应商简称">{{ displayText(factory.short_name) }}</el-descriptions-item>
<el-descriptions-item label="统一社会信用代码">{{ displayText(factory.unified_social_credit_code) }}</el-descriptions-item>
<el-descriptions-item label="合作模式">{{ displayText(factory.cooperation_mode_display) }}</el-descriptions-item>
<el-descriptions-item label="经销商">{{ displayText(factory.dealer_name) }}</el-descriptions-item>
<el-descriptions-item label="产品分类">{{ displayText(factory.product_category) }}</el-descriptions-item>
<el-descriptions-item label="地区">{{ displayRegion(factory) }}</el-descriptions-item>
<el-descriptions-item label="地址">{{ displayText(factory.address) }}</el-descriptions-item>
<el-descriptions-item label="交互能力">
<span class="multiline">{{ displayText(factory.interaction_capability) }}</span>
</el-descriptions-item>
<el-descriptions-item label="官网">
<a
v-if="factory.website"
@ -69,4 +74,8 @@ const displayRegion = (item) => formatRegion(item.province, item.city, item.dist
color: var(--brand-500);
word-break: break-all;
}
.multiline {
white-space: pre-wrap;
}
</style>

View File

@ -9,6 +9,11 @@
<el-table-column prop="factory_name" label="供应商全称" min-width="220" show-overflow-tooltip />
<el-table-column prop="short_name" label="供应商简称" min-width="160" show-overflow-tooltip />
<el-table-column prop="dealer_name" label="经销商" min-width="160" show-overflow-tooltip />
<el-table-column label="合作模式" min-width="120" show-overflow-tooltip>
<template #default="scope">
{{ scope.row.cooperation_mode_display || '-' }}
</template>
</el-table-column>
<el-table-column label="用户账号" min-width="160" show-overflow-tooltip>
<template #default="scope">
{{ (scope.row.usernames || []).join('、') || '-' }}
@ -44,19 +49,36 @@
</div>
<el-dialog v-model="dialogVisible" :title="dialogTitle" width="640px" class="dialog-scroll">
<el-form :model="form" label-width="100px">
<el-form-item label="经销商">
<el-form ref="formRef" :model="form" :rules="formRules" label-width="140px">
<el-form-item label="经销商" prop="dealer_name">
<el-input v-model="form.dealer_name" />
</el-form-item>
<el-form-item label="产品分类">
<el-form-item label="产品分类" prop="product_category">
<el-input v-model="form.product_category" />
</el-form-item>
<el-form-item label="供应商全称" required>
<el-form-item label="供应商全称" prop="factory_name" required>
<el-input v-model="form.factory_name" />
</el-form-item>
<el-form-item label="供应商简称" required>
<el-form-item label="供应商简称" prop="short_name" required>
<el-input v-model="form.short_name" />
</el-form-item>
<el-form-item label="统一社会信用代码" prop="unified_social_credit_code" required>
<el-input
v-model="form.unified_social_credit_code"
maxlength="18"
placeholder="18 位数字或大写字母"
/>
</el-form-item>
<el-form-item label="合作模式" prop="cooperation_mode" required>
<el-select v-model="form.cooperation_mode" placeholder="请选择" style="width: 100%">
<el-option
v-for="item in cooperationModeOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
<el-form-item label="省市区" required>
<el-cascader
v-model="regionValue"
@ -66,12 +88,20 @@
@change="onRegionChange"
/>
</el-form-item>
<el-form-item label="地址">
<el-form-item label="地址" prop="address">
<el-input v-model="form.address" type="textarea" />
</el-form-item>
<el-form-item label="官网">
<el-form-item label="官网" prop="website">
<el-input v-model="form.website" />
</el-form-item>
<el-form-item label="交互能力" prop="interaction_capability">
<el-input
v-model="form.interaction_capability"
type="textarea"
:rows="4"
placeholder="最大月供量 / 常规库存量 / 生产周期 / 运输方式 / 应急供货响应时间"
/>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="dialogVisible = false">取消</el-button>
@ -103,15 +133,45 @@ const dialogVisible = ref(false)
const dialogTitle = ref('')
const isEdit = ref(false)
const currentId = ref(null)
const formRef = ref(null)
const regionOptions = regionData
const regionValue = ref([])
const cooperationModeOptions = [
{ value: 'direct', label: '厂家直供' },
{ value: 'authorized', label: '授权代理商' }
]
const USCC_REGEX = /^[0-9A-Z]{18}$/
const formRules = {
factory_name: [{ required: true, message: '请输入供应商全称', trigger: 'blur' }],
short_name: [{ required: true, message: '请输入供应商简称', trigger: 'blur' }],
unified_social_credit_code: [
{ required: true, message: '请输入统一社会信用代码', trigger: 'blur' },
{
validator: (_rule, value, cb) => {
if (!value) return cb()
if (!USCC_REGEX.test(value)) {
return cb(new Error('必须为 18 位数字或大写字母'))
}
cb()
},
trigger: 'blur'
}
],
cooperation_mode: [{ required: true, message: '请选择合作模式', trigger: 'change' }]
}
const form = reactive({
dealer_name: '',
product_category: '',
factory_name: '',
short_name: '',
unified_social_credit_code: '',
cooperation_mode: '',
interaction_capability: '',
province: '',
city: '',
district: '',
@ -135,12 +195,16 @@ const resetForm = () => {
form.product_category = ''
form.factory_name = ''
form.short_name = ''
form.unified_social_credit_code = ''
form.cooperation_mode = ''
form.interaction_capability = ''
form.province = ''
form.city = ''
form.district = ''
form.address = ''
form.website = ''
regionValue.value = []
formRef.value?.clearValidate()
}
const onRegionChange = (val) => {
@ -168,6 +232,11 @@ const openEdit = async (row) => {
}
const onSubmit = async () => {
try {
await formRef.value?.validate()
} catch (_) {
return
}
try {
if (isEdit.value) {
await updateFactory(currentId.value, { ...form })
@ -178,7 +247,11 @@ const onSubmit = async () => {
dialogVisible.value = false
loadFactories()
} catch (error) {
ElMessage.error(error.response?.data?.detail || '保存失败')
const data = error.response?.data
const msg = data?.detail
|| (data && typeof data === 'object' ? Object.values(data).flat().join('') : null)
|| '保存失败'
ElMessage.error(msg)
}
}