2026-07-05-pl-hazard-cert-by-type-design.md 15 KB

危化品证书按产品类型区分 — 设计文档

日期: 2026-07-05
状态: 已实现(2026-07-05)
前置: docs/superpowers/specs/2026-07-04-pl-hazard-cert-ready-design.md(计划级「证书齐备」人工确认,已实现)
界面模式: P06 复杂业务列表 + 弹窗表单(延续 /#/pl/plan-cert 危化品证上传页)

背景与问题

现状(2026-07-04 齐备功能上线后)

环节 行为
危化品识别 以 CRM 产品主数据 isToxic / isExplosive 为准(计划产品行冗余快照)
证书存储 pl_plan_product.purchase_cert_url(购买证)、transport_cert_url(界面称「合法使用证明」)
上传页 UI 所有危化品产品统一展示「购买证 + 合法使用证明」两个上传位
安排运输 仅校验 pl_plan.hazard_cert_ready 人工确认标志,不强制 URL 非空

业务诉求(2026-07-05 确认)

证书类型应 按产品危化属性区分,而非所有危化品统一双证:

产品属性 所需证书
仅易制毒 购买证 + 运输证
仅易制爆 合法使用证明
易制毒 + 易制爆(双属性) 购买证 + 运输证 + 合法使用证明(三种)

问题

  1. 现有两字段无法同时表达「运输证」与「合法使用证明」——双属性产品需要 三个独立 URL 槽位。
  2. transport_cert_url 在库表 comment 与 UI 中均标注为「合法使用证明」,与易制毒所需的「运输证」语义冲突。
  3. 列表/查看/上传组件均写死「购买证 + 合法使用证明」,无法按类型展示 1~3 种证。

目标

  1. 新增第三字段 legal_use_cert_url,三字段语义固定、互不混用。
  2. 上传、查看、列表展示 按产品类型动态渲染 所需证书位(1~3 个)。
  3. 提供历史数据迁移脚本,按属性拆分旧 transport_cert_url。
  4. 维持 2026-07-04 设计:安排运输仍只校验 hazard_cert_ready,不因 URL 缺失阻断(D4/D10);列表展示缺失状态供操作员自查。

已确认设计

# 决策 说明
D1 三字段独立存储 purchase_cert_url(购买证)、transport_cert_url(运输证)、legal_use_cert_url(合法使用证明)
D2 按属性决定所需证种 仅毒 → 购买证+运输证;仅爆 → 合法使用证明;毒+爆 → 三种都要
D3 上传不强制齐全 延续 D3/D10:保存时允许 0~N 个附件;不恢复 URL 硬性 gate 安排运输
D4 安排运输仍查齐备标志 不新增 URL 非空校验;hazard_cert_ready 逻辑不变
D5 仅易制毒历史 transport 当运输证 旧 UI 虽标「合法使用证明」,仅易制毒产品行的 transport_cert_url 迁移时 原样保留,语义改为运输证
D6 仅易制爆历史 transport → legal_use 仅易制爆产品行:transport_cert_url 迁入 legal_use_cert_url 后清空 transport_cert_url
D7 双属性历史 transport → legal_use 毒+爆双属性产品行:旧第二槽位在 UI 中为「合法使用证明」→ transport_cert_url 迁入 legal_use_cert_url;transport_cert_url 置空(运输证需补传)
D8 上传弹窗与查看结构一致 采用「产品卡片 + 动态表单项」布局,避免固定列表格无法容纳 1~3 证
D9 CRM 标识为准 所需证种以计划产品行 is_toxic / is_explosive(及 CRM enrichment)判定;实现时上传页继续 enrichPlanProductsHazardFromCrm

一、数据模型

1.1 pl_plan_product 字段语义(变更后)

字段 类型 说明 适用属性
purchase_cert_url varchar(500), nullable 购买证 URL 易制毒(含双属性)
transport_cert_url varchar(500), nullable 运输证 URL(comment 从「合法使用证明」更正) 易制毒(含双属性)
legal_use_cert_url varchar(500), nullable 合法使用证明 URL(新增) 易制爆(含双属性)

DDL 示例:

ALTER TABLE `pl_plan_product`
  ADD COLUMN `legal_use_cert_url` varchar(500) DEFAULT NULL COMMENT '合法使用证明URL' AFTER `transport_cert_url`;

ALTER TABLE `pl_plan_product`
  MODIFY COLUMN `transport_cert_url` varchar(500) DEFAULT NULL COMMENT '运输证URL';

同步写入 docs/sql/pl_tables.sql、docs/sql/shanxia2.0.sql,增量脚本置于 backend/scripts/migrate_pl_plan_product_legal_use_cert_url.sql。

1.2 所需证种判定(逻辑)

# 伪代码
def required_cert_keys(is_toxic, is_explosive):
    keys = []
    if is_toxic:
        keys += ["purchase_cert_url", "transport_cert_url"]
    if is_explosive:
        keys += ["legal_use_cert_url"]
    return keys
is_toxic is_explosive 所需字段
1 0 purchase + transport
0 1 legal_use
1 1 purchase + transport + legal_use
0 0 (非危化品,不涉及)

1.3 pl_plan.hazard_cert_ready 保留

2026-07-04 三字段与齐备标志 正交:齐备仍为计划级人工确认;产品变更(D7)仍自动撤销齐备。本变更 不修改 齐备 API 与安排运输 gate 逻辑。


二、历史数据迁移

脚本:backend/scripts/migrate_pl_plan_product_legal_use_cert_url.sql(DDL + DML 同文件或拆分)。

2.1 迁移规则

以计划产品行快照 is_toxic / is_explosive 为准(与线上一致):

场景 purchase_cert_url transport_cert_url legal_use_cert_url
仅易制毒 (toxic=1, explosive=0) 不变 不变(D5:视为运输证) NULL
仅易制爆 (toxic=0, explosive=1) 不变(通常为空) 清空 = 原 transport_cert_url
双属性 (toxic=1, explosive=1) 不变 清空 = 原 transport_cert_url(D7:旧第二槽为合法使用证明)
非危化品 不涉及 不涉及 不涉及

2.2 DML 伪代码

-- 仅易制爆:搬迁合法使用证明
UPDATE pl_plan_product
SET legal_use_cert_url = transport_cert_url,
    transport_cert_url = NULL
WHERE is_toxic = 0 AND is_explosive = 1
  AND transport_cert_url IS NOT NULL AND transport_cert_url != '';

-- 双属性:旧 transport 作合法使用证明,运输证待补传
UPDATE pl_plan_product
SET legal_use_cert_url = transport_cert_url,
    transport_cert_url = NULL
WHERE is_toxic = 1 AND is_explosive = 1
  AND transport_cert_url IS NOT NULL AND transport_cert_url != '';

-- 仅易制毒:无 DML(transport 原样保留为运输证)

2.3 迁移后业务影响

  • 双属性计划:迁移后可能缺「运输证」,操作员需在危化品证上传页补传;「证书齐备」若已标记,建议产品确认是否批量撤销齐备(默认:不自动撤销,由操作员按需处理)。
  • 仅易制毒:附件位置不变,仅 UI 文案由「合法使用证明」改为「运输证」。

三、后端 API

3.1 Schema / Model

文件 改动
backend/app/models/pl_models.py 新增 legal_use_cert_url;更新 transport_cert_url comment
backend/app/schemas/pl/plan.py PlanProductRead/Create/Update、ProductCertUpdateItem 增加 legalUseCertUrl;更新字段 description

3.2 更新证书接口

现有:

PATCH /api/transport/plan/{id}/product-certs
Body: { products: [{ id, purchaseCertUrl?, transportCertUrl? }] }

扩展为:

{
  "products": [
    {
      "id": "…",
      "purchaseCertUrl": "…",
      "transportCertUrl": "…",
      "legalUseCertUrl": "…"
    }
  ]
}
  • 保存时:三个 URL 均可空字符串(清空附件)。
  • 不按属性校验必填(延续 D3)。
  • _row_to_dict / 计划详情 / 列表 products 须回显 legalUseCertUrl。

3.3 计划 CRUD 透传

pl_plan_service.py 创建/更新计划产品时:

  • 读写 legal_use_cert_url;
  • 编辑计划时合并旧证书 URL 逻辑需覆盖第三字段(与现有 purchase/transport 合并模式一致)。

3.4 安排运输

无变更:仍调用 validate_hazard_cert_ready_for_transport,不检查 URL。


四、前端

4.1 类型与 API

web-admin/src/api/pl/index.ts:

  • PlanProduct 增加 legalUseCertUrl?: string;
  • updateProductCerts 请求体增加该字段。

4.2 Composable(单一事实来源)

web-admin/src/composables/usePlToxicProductCertCheck.ts 扩展:

export type HazardCertSlotKey = 'purchaseCertUrl' | 'transportCertUrl' | 'legalUseCertUrl'

export interface HazardCertSlot {
  key: HazardCertSlotKey
  label: string
}

export function getRequiredCertSlots(product: HazardFlagProduct): HazardCertSlot[]

export function isProductCertSlotFilled(product: PlanProduct, key: HazardCertSlotKey): boolean

export function isProductCertComplete(product: PlanProduct): boolean

export function findMissingHazardCerts(products: PlanProduct[]): PlanProduct[]

export function buildMissingHazardCertMessage(products: PlanProduct[]): string

findMissingHazardCerts 重写:按 getRequiredCertSlots 检查对应 key 是否有 URL(仅用于列表提示/标红,不阻断运输)。

标签映射:

key label
purchaseCertUrl 购买证
transportCertUrl 运输证
legalUseCertUrl 合法使用证明

4.3 上传弹窗(P0)

web-admin/src/components/business/pl-plan-cert-upload-dialog/index.vue

项 改动
标题 「上传易制毒/易制爆证书」→ 可保留或改为「上传危化品证书」
提示 按类型说明所需证种;保留「可先证书齐备再补传」
表单项 v-for 产品 × getRequiredCertSlots(product) 动态渲染 AttachmentUploadField
布局 毒 2 项、爆 1 项、毒+爆 3 项;仍用 form-grid-2,三证时第三项可 grid-column: 1 / -1 或三列自适应
保存 提交三字段;非所需槽位传空或不传(后端忽略)

4.4 查看表格(P0)

web-admin/src/components/business/pl-plan-hazard-cert-view-table/index.vue

方案(D8):改为与上传弹窗一致的 产品卡片 + 只读附件,每产品展示其 getRequiredCertSlots 对应项;弃用固定「购买证 | 合法使用证明」两列表格。

4.5 列表证书单元格(P0)

组件 使用页面 改动
pl-plan-list-cert-cell/index.vue 危化品证上传列表「计划附件」列 按槽位动态展示「有/无」
pl-plan-list-product-cell/index.vue 运输计划列表产品列(show-cert-status) 同上;缺失 --missing 按类型判定

4.6 间接页面(P1,文案微调)

页面 改动
views/pl/plan-cert/index.vue 证书齐备确认框文案:说明分类型所需证种
pl-plan-view-tabs/index.vue 「危化品证书信息」区块经子组件生效
pl-plan-arrange-dialog/index.vue 内嵌查看经子组件生效
pl-plan-archive-dialog/index.vue 同上

4.7 透传(P2)

文件 改动
pl-plan-form-dialog/index.vue 编辑/复制计划时透传 legalUseCertUrl
usePlContractToPlan.ts 类型透传

4.8 非目标(本变更不改)

  • 司机 hazmatCertNo、车辆/人员证书、证件到期提醒 document-reminder;
  • 小程序 / 公开运输页(若无计划产品证书展示);
  • hazard_cert_ready API 与安排运输 gate。

五、UI 结构示意

5.1 上传弹窗 — 仅易制毒

┌─ 产品 A ─────────────── [易制毒] ─┐
│  购买证          [上传]            │
│  运输证          [上传]            │
└───────────────────────────────────┘

5.2 上传弹窗 — 仅易制爆

┌─ 产品 B ─────────────── [易制爆] ─┐
│  合法使用证明    [上传]            │
└───────────────────────────────────┘

5.3 上传弹窗 — 双属性

┌─ 产品 C ──────── [易制毒] [易制爆] ─┐
│  购买证          [上传]              │
│  运输证          [上传]              │
│  合法使用证明    [上传]              │
└─────────────────────────────────────┘

六、与 2026-07-04 齐备设计的关系

2026-07-04 决策 本变更
D3 上传 0~2 任意组合 演进为 0~3,按属性上限
D4 安排运输只查齐备 不变
D10 零附件可齐备 不变
D11 齐备后可补传 不变;补传支持第三字段
列表展示有/无 增强:按类型展示 1~3 种证的有无

七、影响范围(实现清单)

层 文件/模块
SQL docs/sql/pl_tables.sql、docs/sql/shanxia2.0.sql
迁移 backend/scripts/migrate_pl_plan_product_legal_use_cert_url.sql
Model backend/app/models/pl_models.py
Schema backend/app/schemas/pl/plan.py
Service backend/app/services/pl_plan_service.py
API backend/app/api/pl/plan.py(product-certs、_row_to_dict)
FE API web-admin/src/api/pl/index.ts
FE Composable usePlToxicProductCertCheck.ts
FE 组件 pl-plan-cert-upload-dialog、pl-plan-hazard-cert-view-table、pl-plan-list-cert-cell、pl-plan-list-product-cell
FE 页面 plan-cert/index.vue(文案)
文档 docs/运输计划管理-需求说明文档.md;openspec/changes/transport-plan-management/specs/transport-plan/spec.md

八、风险与缓解

风险 缓解
双属性历史数据缺运输证 D7 迁移说明 + 列表标红缺失;操作员补传
仅易制毒历史 transport 实为误传合法使用证明 产品已确认按运输证保留(D5);若有个案由操作员重传
字段 comment 与旧文档不一致 同步更新需求文档与 OpenSpec
前端多处硬编码双证文案 统一走 getRequiredCertSlots,禁止组件内重复规则

九、验收标准

  • pl_plan_product 存在 legal_use_cert_url;transport_cert_url comment 为「运输证」
  • 迁移脚本:仅爆、双属性 transport → legal_use;仅毒 transport 不变
  • 上传弹窗:仅毒 2 项、仅爆 1 项、双属性 3 项;保存后 API 回显正确
  • 查看弹窗 / 列表:证书有/无与上传一致,标签名称正确
  • findMissingHazardCerts 按类型判定缺失,列表标红正确
  • 安排运输:仍仅校验 hazardCertReady;未齐备时提示不变
  • 证书齐备 / 取消齐备 / 补传附件:行为与 2026-07-04 一致
  • 需求文档与 OpenSpec 已更新证种规则

十、后续

实现前须按 superpowers 流程编写 Implementation Plan:

docs/superpowers/plans/2026-07-05-pl-hazard-cert-by-type.md

建议任务顺序:DDL + 迁移 → 后端 Schema/API → composable → 上传弹窗 → 查看/列表组件 → 文案与文档。