## Context 合同与调价函已强制作废原因。报价单仅有可选 `remark` 拼写;运输/车辆/结算/地磅的 cancel/void 无 body、无专用字段。用户确认采用**独立字段三元组**(方案 A),覆盖所列全部作废入口(含小程序);并明确:**作废原因非必填**,有则落库展示,无则可直接作废。 ## Goals / Non-Goals **Goals:** - 作废支持填写原因并持久化独立字段,详情可展示(有则显示)。 - 原因**可选**:留空不阻塞作废。 - CRM 报价单对齐合同/调价函命名(`invalid_*`)。 - PL 业务对齐 `/cancel` 语义(`cancel_*`)。 - 地磅对齐 `is_voided` / `/void`(`void_*`)。 - Web + 小程序相关入口行为一致。 **Non-Goals:** - 不改造合同、调价函既有「必填原因」流程。 - 不改造公开页出入库单状态改「已作废」(本变更范围外)。 - 不作废原因字典(自由文本即可)。 - 不回填历史已作废单据的原因。 ## Decisions ### D1 — 字段命名分域 | 域 | 字段 | 理由 | |----|------|------| | CRM 报价单 | `invalid_reason` / `invalid_time` / `invalid_by` | 与合同、调价函一致 | | PL(计划、加油、充值、维保、尿素、结算) | `cancel_reason` / `cancel_time` / `cancel_by` | 与 `/cancel`、`cancelled` 一致 | | 地磅 `db_weighing_slip` | `void_reason` / `void_time` / `void_by` | 与 `is_voided`、`POST .../void` 一致 | `*_reason`:`varchar(500)`,nullable。 `*_time` / `*_by`:作废成功时写入(即使原因为空,仍记录何时、何人作废)。 ### D2 — 原因可选(产品确认) - 前端弹窗提供「作废原因」输入框,**不设 required**;可直接确认作废。 - API body 中原因字段 Optional;trim 后为空则存 NULL,不返回 400。 - 非 BREAKING(相对「仅确认」的旧客户端):无 body / 无原因字段仍可作废;建议新客户端传 body 以便扩展。 ### D3 — API 契约 - 报价单 invalid:可选 `invalidReason` → 写 `invalid_*`(原因可空;时间/人必写)。 - PL cancel:可选 body `{ "cancelReason": "..." }`。 - 地磅 void:可选 body `{ "voidReason": "..." }`。 ### D4 — 报价单迁移行为 - 新作废写入 `invalid_*`,**不再**向 `remark` 追加 `【作废原因】`。 - 历史 remark 拼接不自动迁移。 ### D5 — 加油作废恢复 `restore` 时清空 `cancel_reason` / `cancel_time` / `cancel_by`。 ### D6 — UI - Web:作废对话框含可选 textarea(placeholder 提示「选填」);列表与详情共用。 - 小程序:可输入原因的弹层;允许空提交。 - 查看态:有原因则展示;时间、操作人有则展示。 ### D7 — SQL 交付 - 增量迁移 + 更新 `docs/sql/shanxia2.0.sql` 与 models。 ## Risks / Trade-offs - [原因常空] → 接受;至少有 `*_time`/`*_by` 可追溯操作。 - [三套字段前缀] → 文档与 DTO 写清;UI 文案统一「作废原因」。 - [公开出入库未纳入] → 另开变更。 ## Migration Plan 1. ALTER 加列(均可 NULL)。 2. 部署后端(接受可选原因)。 3. 部署 web-admin + miniprogram(弹窗选填)。 4. 回滚:列可保留;UI 可退回仅确认。 ## Open Questions - (无)地磅命名 `void_*`;原因非必填已确认。