For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: 为商品新增可空文本字段 spec(规格),贯通 DB → 后端 API → web-admin 商品新增/编辑/列表/导出,并在所有展示「计量+单位」的业务页按规范展示(列表独立列、行内 计量单位(规格))。
Architecture: 在 bas_product / bas_product_his 增加 spec VARCHAR(100);Admin 商品 CRUD 与关联 join 接口统一返回 spec;前端新增 product-measure-display.ts 统一格式化;商品管理页负责录入,业务页只读展示。
Tech Stack: MySQL · FastAPI · SQLAlchemy · Vue3 · TypeScript · Element Plus
设计依据: docs/superpowers/specs/2026-06-26-web-admin-product-spec-field-design.md
| 文件 | 职责 |
|---|---|
backend/scripts/migration_20260626_bas_product_spec.sql |
DDL 迁移 |
backend/app/utils/product_spec.py |
spec 空串规范化 |
backend/app/models/models.py |
ORM 字段 |
backend/app/crud/product.py |
历史快照写入 spec |
backend/app/api/admin.py |
商品 CRUD + 订单/结算/询价报价 join |
backend/app/api/delivery.py |
配送任务商品行 |
web-admin/src/utils/product-measure-display.ts |
formatMeasureCell / formatMeasureUnitSpec |
web-admin/src/api/admin.ts |
创建/更新商品类型 |
web-admin/src/views/system/product/index.vue |
表单、列表、导出 |
web-admin/src/components/business/product-view-dialog/index.vue |
查看弹窗 |
| 6 个业务页 + 1 个结算组件 | 列表列 + 行内合并展示 |
Files:
Create: backend/scripts/migration_20260626_bas_product_spec.sql
[ ] Step 1: 编写迁移 SQL
-- 商品规格字段:自由文本,非必填
ALTER TABLE bas_product
ADD COLUMN spec VARCHAR(100) NULL COMMENT '商品规格(自由文本,非必填)' AFTER measure_value;
ALTER TABLE bas_product_his
ADD COLUMN spec VARCHAR(100) NULL COMMENT '商品规格(自由文本,非必填)' AFTER measure_value;
Run:
mysql -u<user> -p<pass> sanqiang < backend/scripts/migration_20260626_bas_product_spec.sql
Expected: Query OK × 2,无重复列错误。
Run:
mysql -u<user> -p<pass> sanqiang -e "SHOW COLUMNS FROM bas_product LIKE 'spec'; SHOW COLUMNS FROM bas_product_his LIKE 'spec';"
Expected: 两表均返回 spec | varchar(100) | YES。
Files:
backend/app/models/models.py(BasProduct ~348 行、BasProductHis ~386 行)Modify: backend/app/crud/product.py(update_with_history ~76 行、soft_delete_with_history ~102 行)
[ ] Step 1: Model 增加 spec 列
在 BasProduct.measure_value 后增加:
spec = Column(String(100), nullable=True, comment="商品规格(自由文本,非必填)")
BasProductHis 同步增加同名字段(measure_value 后)。
specproduct.py 两处 his_data 均在 measure_value 后增加:
"spec": getattr(original, "spec", None),
Run:
python -m py_compile backend/app/models/models.py backend/app/crud/product.py
Expected: 无输出(成功)。
Files:
backend/app/utils/product_spec.pyModify: backend/app/api/admin.py(list_products ~1639、get_product ~1684、create_product ~1740、update_product ~1852)
[ ] Step 1: 新建规范化函数
backend/app/utils/product_spec.py:
from typing import Any, Optional
def normalize_product_spec(value: Any) -> Optional[str]:
"""空串/纯空白 -> None;否则 strip 后返回,最长 100。"""
if value is None:
return None
s = str(value).strip()
if not s:
return None
return s[:100]
Run:
python -c "
from backend.app.utils.product_spec import normalize_product_spec
assert normalize_product_spec(None) is None
assert normalize_product_spec('') is None
assert normalize_product_spec(' ') is None
assert normalize_product_spec('礼盒装') == '礼盒装'
assert normalize_product_spec(' 礼盒装 ') == '礼盒装'
print('ok')
"
Expected: 输出 ok。
spec在 admin.py 顶部 import:
from app.utils.product_spec import normalize_product_spec
列表项 dict(~1639 行 measureValue 旁)增加:
"spec": getattr(p, "spec", None) or None,
详情 dict(~1684 行)同样增加 "spec": product.spec or None。
spec在 create_product 调用 product_crud.create 前:
if "spec" in request:
request["spec"] = normalize_product_spec(request.get("spec"))
update_product 同样在 product_crud.update_with_history 前处理。
specupdate_product 返回体(~1874 行)增加 "spec": product.spec or None。
Run: 启动 uvicorn,GET /api/web/admin/products?page=1&pageSize=1(带 Token)。
Expected: list[0] 含 spec 字段(可为 null)。
specFiles:
backend/app/api/admin.py在下列响应 dict 中,凡已有 "measureValue" 的行,紧邻增加 "spec": getattr(product, "spec", None) if product else None(或从已加载的 product 变量读取):
| 约略行号 | 场景 |
|---|---|
| ~2410 | 询价详情 items |
| ~2588 | 报价 items |
| ~2833 | 询价商品池 items |
| ~2976 | 报价对比 items |
| ~4161 | 订单明细 items |
| ~5448 | 客户结算 line children |
| ~6175 | 供应商结算 line children |
| ~7846 | 订单调整可选商品 |
| ~8370 | 配送预警/关联订单 items |
[ ] Step 1: 订单明细 item_details.append(~4154 行)补 spec
[ ] Step 2: 客户结算 _build_client_pending_settlement_tree_rows children(~5439 行)补 spec
[ ] Step 3: 供应商结算对应 children(~6175 行)补 spec
[ ] Step 4: 询价/报价相关 4 处 items 补 spec
[ ] Step 5: 订单调整、配送预警 items 补 spec
[ ] Step 6: 语法检查
Run:
python -m py_compile backend/app/api/admin.py
Expected: 成功。
specFiles:
Modify: backend/app/api/delivery.py(~403 行)
[ ] Step 1: 配送任务商品行 dict 增加 spec
在 "measureValue": measure_value 后:
"spec": getattr(product, "spec", None) if product else None,
Run:
python -m py_compile backend/app/api/delivery.py
Expected: 成功。
Files:
Create: web-admin/src/utils/product-measure-display.ts
[ ] Step 1: 实现格式化函数
/** 列表「计量」列:与商品管理页现有逻辑一致 */
export function formatMeasureCell(v: unknown): string {
if (v == null || v === '') return '—'
const n = Number(v)
if (Number.isNaN(n)) return '—'
return n.toFixed(3).replace(/\.?0+$/, '')
}
/** 列表「规格」列 */
export function formatSpecCell(spec?: string | null): string {
const s = spec != null ? String(spec).trim() : ''
return s || '—'
}
/**
* 行内合并:计量 + 单位 +(规格)
* 例:500 克(礼盒装);无规格时不显示括号
*/
export function formatMeasureUnitSpec(
measureValue: unknown,
unitName?: string | null,
spec?: string | null
): string {
const m = formatMeasureCell(measureValue)
const hasMeasure = m !== '—'
const u = unitName != null ? String(unitName).trim() : ''
const s = spec != null ? String(spec).trim() : ''
let base = ''
if (hasMeasure && u) base = `${m} ${u}`
else if (hasMeasure) base = m
else if (u) base = u
else if (s) base = ''
else return '—'
if (s) {
return base ? `${base}(${s})` : `(${s})`
}
return base || '—'
}
/** 数量 × 计量单位(规格) */
export function formatQtyMeasureUnitSpec(
qty: number | string,
measureValue: unknown,
unitName?: string | null,
spec?: string | null
): string {
const inner = formatMeasureUnitSpec(measureValue, unitName, spec)
if (inner === '—') return String(qty)
return `${qty}×${inner}`
}
Run:
pnpm --dir web-admin exec vue-tsc --noEmit 2>&1 | head -20
Expected: 新文件无类型错误(允许项目既有其它告警)。
Files:
Modify: web-admin/src/api/admin.ts(fetchCreateProduct ~467、fetchUpdateProduct ~489)
[ ] Step 1: 类型增加 spec
spec?: string | null
分别加入 fetchCreateProduct data 与 fetchUpdateProduct Partial data。
Files:
Modify: web-admin/src/views/system/product/index.vue
[ ] Step 1: formData 增加 spec: ''(~983 行)
[ ] Step 2: 基本信息区增加规格输入框
在「计量/单位」el-row 之后新增一行:
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="规格">
<el-input
v-model="formData.spec"
placeholder="如:礼盒装、约 2 斤/袋"
maxlength="100"
show-word-limit
clearable
/>
</el-form-item>
</el-col>
</el-row>
不加 prop / required 规则。
<el-table-column prop="spec" label="规格" min-width="100" align="center" header-align="center">
<template #default="{ row }">
{{ formatSpecCell(row.spec) }}
</template>
</el-table-column>
从 @/utils/product-measure-display import formatSpecCell;本地 formatMeasureCell 可改为从工具 import 或保留(二选一,推荐统一 import)。
productExportColumns(~871 行)增加spec: { title: '规格' },
mapProductExportRow 增加 spec: row.spec || '—'。
handleSubmit 的 basicData(~1658 行)增加spec: formData.spec?.trim() ? formData.spec.trim() : undefined,
resetForm / loadProductForEdit / handleCopy 读写 specresetForm:spec: ''。
编辑回填:spec: detail.spec || ''。
复制:formData.spec = detail.spec || ''。
打开 http://localhost:3006/#/base/product:
—Files:
Modify: web-admin/src/components/business/product-view-dialog/index.vue
[ ] Step 1: formData / resetForm / loadProduct 增加 spec
[ ] Step 2: Hero 文案(~34 行)
标签改为「计量/单位(规格)」;内容:
{{ formatMeasureUnitSpec(formData.measureValue, selectedUnitName, formData.spec) }}
dt 改为「计量/单位(规格)」;dd 使用 formatMeasureUnitSpec。
<span class="muted-text"> / {{ formatMeasureUnitSpec(formData.measureValue, row.unitName, formData.spec) }}</span>
formatMeasureUnitSpec,可删除本地 formatMeasureView 若已不再使用Files:
Modify: web-admin/src/views/system/order/index.vue
[ ] Step 1: 所有含「计量」「单位」列的 el-table 在「单位」列后增加「规格」列
<el-table-column prop="spec" label="规格" min-width="90" align="center">
<template #default="{ row }">{{ formatSpecCell(row.spec) }}</template>
</el-table-column>
涉及约 5 处表格(行 ~151、~481、~1016、~1314、~1466)。
formatDeliveredQtyMeasureUnit(~1662 行)改用工具函数import { formatQtyMeasureUnitSpec } from '@/utils/product-measure-display'
const formatDeliveredQtyMeasureUnit = (row: any, q: number) =>
formatQtyMeasureUnitSpec(q, row?.measureValue, row?.unitName, row?.spec)
订单详情弹窗:明细表有规格列;已配送文案为 10×500 克(礼盒装) 形式。
Files:
web-admin/src/views/system/delivery/index.vueModify: web-admin/src/views/system/delivery-warning/index.vue
[ ] Step 1: 各页含计量+单位列的表格增加「规格」列(模式同 Task 10)
[ ] Step 2: 行内若存在 formatMeasureCell 拼接单位展示,改为 formatMeasureUnitSpec
[ ] Step 3: 两页分别打开列表/详情验证
Files:
Modify: web-admin/src/views/system/inquiry-quotation/index.vue
[ ] Step 1: 所有同时展示计量、单位的表格增加「规格」列
该文件表格较多(~308、~895、~1271 等),统一在 unitName 列后插入规格列。
measureValue + unitName)改为 formatMeasureUnitSpec示例(~1432 行附近):
formatMeasureUnitSpec(row.measureValue, row.unitName, row.spec)
fetchProductList 回填处确保 spec 透传到行数据(~2640、~3193 等 map 处加 spec: it?.spec ?? null)Files:
web-admin/src/views/system/client-settlement/index.vueweb-admin/src/views/system/supplier-settlement/index.vueModify: web-admin/src/components/settlement/SettlementSheetDetailPanel.vue
[ ] Step 1: 三处表格增加「规格」列(unitName 列后)
[ ] Step 2: 行内计量单位展示改用 formatMeasureUnitSpec
SettlementSheetDetailPanel.vue 中 formatMeasureValue 相关拼接(~271 行)评估是否改为 formatMeasureUnitSpec(line.measureValue, line.unitName, line.spec)。
Files:
Modify(可选): docs/superpowers/specs/2026-06-26-web-admin-product-spec-field-design.md 末尾追加「实施完成」勾选
[ ] Step 1: 商品管理全链路
| 步骤 | 预期 |
|---|---|
| 新增,无规格 | 保存成功,规格列 — |
| 新增,规格「礼盒装」 | 列表/查看均为「礼盒装」 |
| 编辑清空规格 | 存 NULL,列表 — |
| 复制商品 | 规格随复制 |
| 导出 | Excel 含规格列 |
| 场景 | 预期 |
|---|---|
| 查看弹窗 Hero | 500 克(礼盒装) 或 500 克 |
| 订单已配送 | 10×500 克(礼盒装) |
| 仅规格无计量单位 | (礼盒装) |
[ ] Step 3: 边界
规格 100 字前端限制生效
规格含括号原样显示,不再套第二层括号
[ ] Step 4: 确认未改页面仍正常
价格清单、报价、询价、订单调整页无回归。
| 设计章节 | 对应 Task |
|---|---|
数据模型 spec 字段 |
Task 1–2 |
| 历史表快照 | Task 2 |
| 商品 CRUD API | Task 3 |
| 关联业务 API | Task 4–5 |
formatMeasureUnitSpec |
Task 6 |
| 商品表单/列表/导出 | Task 8 |
| 查看弹窗 | Task 9 |
| 业务列表 P1 | Task 10–13 |
| 非目标(小程序/订单快照) | 无 Task(符合预期) |
| 手工测试 | Task 14 |
Task 1 → 2 → 3 → 4 → 5 → 6 → 7 → 8 → 9 → 10 → 11 → 12 → 13 → 14
Task 6–7 可与 Task 3–5 并行(前后端分工时)。