fix: 留言板区分业务序列号与系统追溯码

语义修正:
  Product.serial_number  = 16位HEX系统追溯码(如000000000000000D)
  Product.external_serial = 业务产品序列号(如25022)

后端:
  - ProductMessageItem 新增 external_serial 字段
  - search_product_messages 关联查询 Product.external_serial
  - 关键词搜索也支持按 external_serial 匹配

前端显示:
  样品升降台V1J · 序列号: 25022          ← 醒目
  追溯码: 000000000000000D               ← 10px浅灰小字
This commit is contained in:
2026-08-12 14:20:38 +08:00
parent c14bbeb891
commit ff7c791d49
3 changed files with 18 additions and 10 deletions

View File

@ -38,7 +38,8 @@ class ProductMessageItem(BaseModel):
id: str
content: str
operator_name: str # 留言人中文姓名
product_sn: str # 16位SN
product_sn: str # 16位HEX系统追溯码
external_serial: str | None # 业务产品序列号(如 25022
material_name: str # 物料名称
created_at: str # ISO时间字符串
@ -194,9 +195,9 @@ async def search_product_messages(
from app.models.product import Product
from app.core.time_utils import BEIJING_TZ
# 基础查询
# 基础查询 — 同时取出 16位追溯码 + 业务序列号
stmt = (
select(ProductMessage, Product.serial_number, Product.material_name)
select(ProductMessage, Product.serial_number, Product.material_name, Product.external_serial)
.join(Product, ProductMessage.product_id == Product.id)
)
@ -205,6 +206,7 @@ async def search_product_messages(
kw = f"%{keyword.strip()}%"
stmt = stmt.where(or_(
Product.serial_number.ilike(kw),
Product.external_serial.ilike(kw),
Product.material_name.ilike(kw),
ProductMessage.operator_id.ilike(kw),
ProductMessage.content.ilike(kw),
@ -227,7 +229,7 @@ async def search_product_messages(
name_map = get_display_names(raw_ids)
items: list[ProductMessageItem] = []
for msg, sn, mat_name in rows:
for msg, sn, mat_name, ext_sn in rows:
t = msg.created_at
if t:
if t.tzinfo is None:
@ -243,6 +245,7 @@ async def search_product_messages(
content=msg.content,
operator_name=name_map.get(msg.operator_id, msg.operator_id),
product_sn=sn or "",
external_serial=ext_sn or None,
material_name=mat_name or "",
created_at=time_str,
))

View File

@ -127,11 +127,15 @@ function MsgRow({ m }: { m: ProductMessageItem }) {
<span className="shrink-0 text-xs text-gray-400">{t}</span>
</div>
<p className="mb-2 text-sm leading-relaxed text-gray-600">{m.content}</p>
<div className="flex items-center gap-1 text-xs text-gray-500">
<div className="flex flex-col gap-0.5">
{m.material_name && (
<span className="font-medium text-gray-700">{m.material_name}</span>
<span className="text-xs">
<span className="font-semibold text-gray-700">{m.material_name}</span>
<span className="text-gray-400"> · : </span>
<span className="font-medium text-gray-600">{m.external_serial || "未录入"}</span>
</span>
)}
<span className="font-mono text-gray-400">(SN: {m.product_sn})</span>
<span className="text-[10px] text-gray-300 font-mono">: {m.product_sn}</span>
</div>
</div>
</div>

View File

@ -30,6 +30,7 @@ export interface ProductMessageItem {
content: string;
operator_name: string;
product_sn: string;
external_serial: string | null;
material_name: string;
created_at: string;
}