# OMS 业务系统 — 表结构与接口设计 ## 1 导读 ### 1.1 这份文档解决什么问题? 1. **订单**:创建、预报、支付、状态机、费用与版本对齐、模板/批量任务、**客户发件地址簿**、**揽收订单**等。 2. **渠道**:销售产品、服务渠道、计价与发布、计重、分区、自动触发、标签等。 3. **操作**:入库、出库、面单打印等仓储作业。 4. **充值**:充值申请、审核及入账(**客户余额权威在 `t_customer.Balance`**;**余额变动统一写入 `t_customer_account_flow` 资金流水**,审核通过后由 OMS 客户服务更新余额)。 5. **系统主数据**:国家、字典、仓库地址、**其他配置(含小包轨迹服务)**、通知等 **`/system/*`** 能力。 ### 1.2 文档定位 **OMS** 为 **业务系统**;认证、RBAC、内部用户、客户主数据等由 **`docs/rbac系统设计-060801.md`** 描述(与线上一致)。业务侧以 **`CustomerId`(`t_customer.Id`)** 表达订单/充值等归属;与 JWT 会话(**`mapId`**、**`loginType`**)的对齐见 **1.4**。**本篇不展开 RBAC 表与 `/rbac/*`、`/customer/*` 接口明细**。 ### 1.3 业务模块一览 | 模块 | 章 | 说明 | | --- | --- | --- | | **订单** | **4** | 全生命周期、查价、模板与批量任务、发件地址、揽收订单 | | **渠道** | **5** | 销售产品、服务渠道、计价、计重、分区、标签等 | | **操作** | **6** | 入库、出库、打印 | | **充值** | **7** | 充值申请、审核、入账;**客户账户资金流水** | | **系统主数据** | **8** | 国家、字典、仓库、通知(`/system/*`) | ### 1.4 与客户 / 用户数据的边界 **客户主数据、子账号、内部用户、认证与 RBAC** 均在 **`docs/rbac系统设计-060801.md`**;**本篇不写**上述领域的表结构与 `/rbac/*`、`/customer/*` 接口明细。 #### 双 ID 与业务归属(与 RBAC 文档一致) | 标识 | 来源 | OMS 用途 | | --- | --- | --- | | `user.Id` | RBAC GUID | JWT 主体;`/rbac/*` 管理接口 | | `user.userid` | RBAC 自增 INT | JWT Claim **`mapId`**;`CurrentUser.GetId()` | | `t_customer.UserId` | OMS 客户表 | **等于** `user.userid`,桥接 RBAC 与 OMS | | `t_customer.Id` | OMS 客户主键 | 业务表 **`CustomerId`** 外键 | OMS 业务表中的 **`CustomerId`** 指向 **`t_customer.Id`**(客户业务主键),**不是** `user.userid`。 #### 登录主体与业务 CustomerId 解析 | `loginType`(JWT Claim) | 值 | 登录校验 | OMS 业务上下文 | | --- | --- | --- | --- | | 内部用户 | 0 | RBAC `user` 账号密码 | `mapId` 可作审计 `CreateUserId`;一般不直接作为 `CustomerId` | | 客户(含主/子) | 1 | `t_customer` 手机/邮箱/编号 + MD5 密码 | 先用 **`mapId` 查 `t_customer.UserId`**,再取该行 **`t_customer.Id`** 作为业务 `CustomerId` | **子客户**:`t_customer.ParentCustomerId` 非空表示子客户;登录 Token 的 `mapId` 仍是**该子客户自己的** `user.userid`。订单/充值等若按「主客户归属」落库,须将子客户解析为其 **`ParentCustomerId` 对应的主客户 `t_customer.Id`**(具体接口在实现时二选一:按登录客户自身 Id,或归并到主客户 Id,须在接口章节写死;**禁止**把 `mapId` 直接当作 `t_customer.Id`)。 **存在性、状态、余额、子客户权限** 以 **`/customer/*`** 与 RBAC 服务为准;**不得**仅凭请求体 Id 绕过鉴权。**订单模块**(`/admin/order/*`)另须遵守 **§4.4.0** 客户数据隔离。 **不在本篇设计的内容**:统一认证、RBAC 管理、内部用户、客户 CRUD、角色权限分配等 —— 见 **`docs/rbac系统设计-060801.md`**。 ### 1.5 后端实现对照(oms0414-back) > 对照仓库:`C:\Users\wilso\Desktop\oms0414-back`(`SimpleAdmin.sln`)。HTTP 由 **Fake 框架** 将 `ApplicationService` 上的 `[HttpGet]` / `[HttpPost]` 自动暴露为 API,**无手写 Controller**。结构说明见仓库内 **`系统结构说明.md`**。 #### 1.5.1 技术栈与路由约定 | 项 | 说明 | | --- | --- | | 运行时 | .NET 8、SqlSugar + MySQL、JWT(Claim **`mapId`** = `user.userid`)、Swagger | | OMS 业务路由 | `/order/*`、`/channel/*`、`/operation/*`、`/recharge/*`、`/system/*`、`/customer/*` | | 认证 / RBAC | `/rbac/auth/*`(宿主 `AuthService` 覆盖)、`/rbac/*`(NuGet `Fake.Rbac.Application`) | | 微信 | `/wechat/mp/*`、`/wechat/customer/*`、`/wechat/oauth/*` | | 分页响应 | `OmsPageResult` → `{ items, total }`;无分页列表 → `OmsItemsResult` → `{ items }` | | 分页默认值 | `OmsDocHelper.NormalizePage` / `NormalizePageSize`(默认 pageSize **30**) | #### 1.5.2 ApplicationService 与模块映射 | 业务域 | Service 类(`Oms.Application/Services/`) | 路由前缀 | | --- | --- | --- | | 订单 | `OrderService`(+ `ParcelBilling.cs`、`ParcelQuote.cs`、`Helpers.cs`) | `/order/*` | | 算价引擎 | `ParcelBillingCalculator`(静态,无 HTTP) | — | | 销售产品 | `SalesProductService` | `/channel/sales-product/*` | | 服务渠道 | `ServiceChannelService` | `/channel/service-channel/*` | | KPI 分区 | `KpiAreaService` | `/channel/kpi-area/*` | | 计重 / 进位 | `WeightRuleService`、`WeightRoundingService` | `/channel/weight-rule/*`、`/channel/weight-rounding/*` | | 自动触发 / 标签 | `AutoTriggerService`、`LabelTemplateService` | `/channel/auto-trigger/*`、`/channel/label-template/save` | | 问题模板 | `IssueTemplateService` | `/channel/issue-template/*` | | 仓内作业 | `InboundRecordService`、`OutboundRecordService`、`LabelPrintService` | `/operation/*` | | 充值 | `RechargeService` | `/recharge/*` | | 客户账户流水 | `CustomerAccountFlowService`、`CustomerBalanceMutator` | `/customer/account-flow/*`(列表);余额变动由充值审核、订单支付触发 | | 系统主数据 | `CountryService`、`DictService`、`NoticeService`、`WarehouseService` | `/system/*` | | 客户 | `CustomerService` | `/customer/*`(契约见 RBAC 文档) | #### 1.5.3 会话与客户隔离(`OmsSessionHelper`) | 方法 | 行为 | | --- | --- | | `ResolveSessionCustomerAsync` | **`loginType=1`**:按 `mapId` 查 `t_customer.UserId`,得到 **`t_customer.Id`**;请求体 `customerId` 若传则须一致,否则 **「客户ID非法」**。**管理员**:须传 `customerId`(或 fallback `mapId` 作 Id 查客户),否则 **「客户ID不能为空」** | | `ResolveListCustomerIdAsync` | **客户登录**:强制返回 `t_customer.Id` 用于列表过滤。**管理员**:返回 `null`,**不过滤** `CustomerId`(可查看全部客户订单/任务/充值) | | `GetOrderOrThrowAsync` | 存在性校验后,客户登录时 **`order.CustomerId` 须等于会话 Id**,否则 **「订单不存在」** | > **与 §4.4.0 的差异**:设计文档要求客户门户专用;**当前实现允许管理员(`loginType=0`)跨客户查询**,列表接口不注入 `CustomerId` 条件。 #### 1.5.4 设计 vs 实现:关键差异速查 | 能力 | 设计目标 | 当前实现(oms0414-back) | | --- | --- | --- | | 快照读取 | Redis + 进程内存为主,DB 降级 | **直接读** `t_sales_product_publish_record`;无发布记录时 **Live 组装**(`BuildLiveRulesSnapshotAsync` / `BuildLivePriceSnapshotAsync`),版本号写 **`LIVE-{salesProductId}`** | | 预报应付费用 | §4.3.4.2 排货确认渠道 + 渠道买价 | **创建时不排货**;**入库**时 `TryConfirmServiceChannelAndPayableAsync`(失败 → **`pendingAllocation`** + `AllocationPendingReason`,不阻断收货/inbound 应收) | | 入库触发应收/应付 | §6.3.1.1 收货计重后重算 | **已实现**:`/operation/inbound/create` → `ApplyReceiveBillingByWaybillAsync`(inbound 应收 + **可选排货**;成功 → `pendingPayment`,失败 → `pendingAllocation`) | | 出库更新状态与发货件重尺 | §6.3.2.1 覆盖 `Type=ship` 并重算;**`OrderStatus=pendingOnline`** | **已实现**:`/operation/outbound/create` → `ApplyShipBillingByWaybillAsync` + 写 `t_outbound_record` | | 批量 JSON 下单 | 异步任务 + `taskId` | **同步逐条**调用 `CreateParcelCoreAsync`,返回 **`batchNo` + `items[]`** | | Excel 导入 | 解析后等同批量创建 / 异步 | **统一走** `t_order_batch_job`(`action=import-row`);MQ 开启异步入队,关闭则同步逐条处理;返回 **`jobId`** | | 批量重算费用 | `POST /order/fee/batch-recalculate` | **未实现**(§4.4.1.10 为设计稿) | | 订单支付 | 扣减 `t_customer.Balance` 并写 `t_customer_account_flow` | **已实现**:仅 `pendingPayment` 可支付;应收合计扣款;流水 `BizType=orderPay` | | 系统面单 / 订单附件 | 独立表;系统面单仅 1 份;客户只读系统面单 | **已实现**:`t_order_file`;创单成功后异步/同步生成 PDF 写入 **`systemLabel`**;打印优先读文件表,缺失则补生成;**已废弃** `t_order.LabelUrl` / `LabelGenerateStatus` / `LabelGenerateError` | | 规则快照下游元数据 | 排货 Provider 含渠道/itfx 对接字段 | **已实现**:规则发布时 **`AllocationProviderSnapshotEnricher`** enrich `Providers`;排货确认写入 **`t_order`** 四字段;**AppSecret 不落快照/订单** | | 轨迹 / 下游预报 | 预报后异步轨迹;下游 createOrderOne | **部分**:预报成功异步 UpsertSmallBag;「我的订单」可手动 **`POST /order/track-upsert-small-bag`**;下游预报 **`POST /order/downstream-forward-create-one`**(及批量任务);成功回写 **`DownstreamServiceNo` / `TrackNo`**,下游面单写入 **`t_order_file.downstreamLabel`** | | 创建问题件 / 理赔件 | §4.4.1.13~§4.4.1.15 批量标记 + 问题模板消费 | **已落地**(`OrderService.Exception` + `OrderExceptionWorkflowService`) | | 问题件通知 | 按模板 `NotifyMethod` 投递 | **部分**:`inApp`(站内信)、`email`(操作人 SMTP)已接入;`sms/feishu/wechat` 待通道对接 | | 自动触发运行时 | 下单/入库/出库消费 `t_auto_trigger` | **部分**:`AutoTriggerRuntimeService` 已挂钩;**问题件**联动问题模板,**拦截/警告**已支持 | 完整接口清单与实现状态见 **§10.6**;订单各接口「当前实现」见 **§4.4**;**渠道各接口总览见 §5.3.0**。 --- ## 2 业务设计思路与主流程 ### 2.1 职责边界:OMS 与 RBAC / 客户域 | 侧 | 职责 | | --- | --- | | **OMS(本文)** | 订单全生命周期、渠道配置与算价支撑、仓内作业、充值业务流水、**客户账户资金流水(`t_customer_account_flow`)**、`/system/*` 业务主数据(国家、字典、仓库地址、**其他配置**、通知等)。 | | **RBAC / 客户域(另文)** | JWT 认证(`/rbac/auth/*`)、RBAC 用户/角色/菜单/组织(`/rbac/*`)、客户资料与 RBAC 联动(`/customer/*`)、权限码目录与分配、子客户专属角色 `CustomerSub_{userId}`。 | | **协作约定** | OMS 用 **`t_customer.Id`** 作业务 `CustomerId`;会话侧用 **`mapId`(`user.userid`)** 定位客户行;**客户是否存在、状态、子客户权限、余额** 以 **`t_customer` + RBAC 服务** 为准,**禁止**把 `mapId` 直接当 `t_customer.Id`,**禁止**仅凭请求体 Id 当作已鉴权事实。 | ### 2.2 主流程 A:配置发布 → 运行时算价(渠道支撑订单) **设计意图**:高并发查价/下单不走「每次打 DB 拉全量规则」;以 **版本化快照 + Redis + 计费实例内存** 为主路径,DB 发布表为审计与降级。 1. 运营维护销售产品、卖价/买价、分区(KpiArea)、计重、排货、服务渠道等。 2. **发布** 产生 **不可变版本**;结构化快照写入 **Redis**,计费服务加载 **本地内存**。 3. **查价与正式下单共用**:按绑定时刻(如 `bindTime`)选取应对生效的 **rules/price 版本** → 规则匹配(分区、计重、黑名单等)→ 出费用明细;查价响应宜带 **`rulesVersion` / `priceVersion`** 便于与订单对齐。 > **当前实现(oms0414-back)**:步骤 2~3 的 Redis/内存路径 **尚未接入**;运行时直接读 **`t_sales_product_publish_record`**,无发布时 **Live 组装**(见 **§4.3.5**)。 ### 2.3 主流程 B:订单(客户侧业务链) **设计意图**:**先锁「计费契约版本」、再按履约阶段分批落费**;历史订单不因后台新发版被静默改价。 1. **查价**(可选)→ **创建 / 预报** → 校验 → 草稿与包裹 → **预报计重完成后**将全部启用卖价项写入 **`t_order_fee`**(`ChargingStage=forecast`)。 2. **契约固化**:预报等关键节点写入 **`SalesProductRulesVersion`、`SalesProductPriceVersion`**(及渠道侧应付相关版本,若有);非显式「改单重算」流程 **不得** 因后台新发布而改写已存版本号。 3. **系统面单**:创单进入 **`predeclared`** 后触发生成(队列开启则异步 `generate-label-pdf`,否则同步);PDF 落盘并写入 **`t_order_file`(`FileType=systemLabel`,仅 1 份可替换)**;失败**不阻断创单**,打印时补生成。模板解析顺序:订单 **`LabelTemplateId`** → 销售产品绑定模板 → 全局默认模板。 4. **状态机**(概念顺序;枚举见 **`## 10 附录` → `### 10.3`**):如 草稿 → 已预报 → **待排货(可选)** → 待支付 → 待发货 → **待上网** → 运输中 → 派送成功/失败;另有拦截、**问题件**(`problematic`)、**理赔件**(`claimed`)、删除等分支。入库排货失败时进入 **`pendingAllocation`**,写入 **`AllocationPendingReason`**,仍保留收货与 inbound 应收。 5. **支付**:OMS 侧订单状态与支付动作对接;**扣减 `t_customer.Balance`** 并 **写入 `t_customer_account_flow`(`BizType=orderPay`,金额为负)**;支付金额取订单 **`t_order_fee` 应收合计**。 6. **分阶段计费(按件重尺类型)**:卖价/买价 **不再配置「收费时机」**;应收在 **`t_order_weight.Type=forecast` 或 `receive` 计重完成** 后触发,全部启用价格项参与;`t_order_fee.ChargingStage` 标记本次算价所用重量阶段(`forecast` / `inbound`)。出库阶段(`ship` 行)待 §6.3.2.1 接入。 7. **批量下单**:Excel/模板 或 JSON 批量接口 → 异步任务;**单次最多受理 2000 条**。 8. **揽收订单**(§4.4.4):客户维护 **发件地址簿**(§4.2.10 / §4.4.5)→ 创建时选 **发件地址 + 仓库 + 运输方式(销售产品)+ 揽收时间 + 货物名称/重量** → 服务端组装为小包创建入参 → **复用 `CreateParcelCoreAsync` / §4.4.3.1** 落 `t_order`(`OrderSource=pickup`)。**不是**仓储模块 `C_parcel` / 到货登记另开一套订单主表。 9. **店铺订单创建物流**(§4.4.7):平台店铺单勾选 → 校对收件人/明细 → 选运输方式 → **批量创建** `t_order`(`OrderSource=platform`)并回写 `LinkedOrderId`;若「物流&代发」则同弹窗内按单绑定包裹/库存(复用 §4.4.6 语义)。与「匹配库存」分流:后者依赖平台 SKU 映射并创建时扣减库存。 ### 2.4 主流程 C:操作(仓库) 1. **入库扫描**:实重、尺寸等落库;**仅更新 `Type=receive` 件重尺**并重算;可触发 **`inbound` 阶段**应收计费。 2. **出库扫描**:**仅更新 `Type=ship` 发货件重尺**并重算计费重;订单 **待发货 → 待上网**,写入 **`ShipTime`**;可触发 **`outbound` 阶段**应收计费。 3. **标签打印**:按单号与模板输出,记录打印结果。 ### 2.5 主流程 D:充值 1. 客户提交 → **`t_recharge_record` 待审核**。 2. 审核通过/驳回;**通过时** 经 **`CustomerBalanceMutator`** 增加 **`t_customer.Balance`** 并写入 **`t_customer_account_flow`(`BizType=recharge`,金额为正)**;**`t_recharge_record`** 保留充值申请与审核状态(**非**统一资金流水表)。 ### 2.6 主流程 E:系统主数据 国家、字典、仓库地址、**其他配置(第三方集成参数,如小包轨迹 `track_service`)**、通知供全业务引用;**与客户/内部员工相关的 DictType** 定义以 **`docs/rbac系统设计-060801.md`** 为准。 ### 2.7 设计不变量(推断与实现时须一致) | 不变量 | 说明 | | --- | --- | | 版本锁定 | 订单一旦写入规则/卖价版本字段,后续后台发版 **不自动** 推翻该票货的计费契约。 | | Live 快照 | 无发布记录时版本号为 **`LIVE-{salesProductId}`**,仍落库为订单契约(**§4.3.5**)。 | | 版本与落费时机解耦 | **版本**在预报等节点锁定;**何时写入 `t_order_fee`** 由 **件重尺类型**(forecast/receive/ship)触发的算价流程决定,与卖价/买价配置项无关。 | | 后阶段仍用旧版快照 | 入库/出库触发的算价,加载的是 **该订单已绑定 version** 的快照,不是「当前最新 version」。 | | 客户与余额 | 校验客户、余额、子客户关系 → **`t_customer` + `/customer/*`**;OMS 持 `CustomerId`(`t_customer.Id`)与业务流程;**任意余额变动须同步写 `t_customer_account_flow`**,流水含变动前后余额。 | | **订单客户隔离** | **`/admin/order/*`(服务内 `/order/*`)** 下全部增删改查须以会话解析的 **`effectiveCustomerId`** 过滤与鉴权;**禁止**跨客户读写订单及相关任务/模板。见 **§4.4.0**。 | | 审计字段 | 写操作落 `Create*`/`Update*`;操作者来自 **JWT `mapId`**(`CurrentUser.GetId()`),见 **`### 3.2`、`### 3.3`**。 | ### 2.8 本文档章节索引(从流程跳转到细节) | 上述块 | 正文位置 | | --- | --- | | 文档定位、模块一览、权限边界 | **`## 1 导读` → `### 1.3`~`### 1.5`** | | 订单、查价、费用、版本策略 | **`## 4 订单模块`**(**`### 4.3.5` 当前实现**、**`### 4.4.0` 客户隔离**、**`### 4.3` 下单计费与版本策略**、**`§4.4.6` 绑定**、**`§4.4.7` 店铺创建物流/代发**) | | 产品/渠道/发布/分区/计重/标签 | **`## 5 渠道模块`**(**`### 5.3.0` 当前实现总览**) | | 入库/出库/打印 | **`## 6 操作模块`** | | 充值与账户流水 | **`## 7 充值管理`**(含 **`§7.4` 资金流水**) | | 国家/字典/仓库/通知 | **`## 8 系统主数据`** | | 需求迭代与历史差异 | **`## 9 需求迭代与变更记录`** | | 枚举与路径速查 | **`## 10 附录`** | --- ## 3 公共规范 ### 3.1 表结构设计公共规范 #### 3.1.1 表的字段命名规范 | 规范 | 说明 | 示例 | | --- | --- | --- | | 驼峰命名 | 字段名开头大写+单词驼峰 | `ServiceProviderId` | | 单数名词 | 表名用单数,表示实体 | `order` 而非 `orders` | | 业务前缀 | 表名按业务模块加前缀 | `t_order`、`t_channel` | #### 3.1.2 必备字段(所有表) > **强制**:凡继承 `OmsAuditedAggregateRoot` / Fake `AggregateRoot` 的 OMS 实体表,建表与增量迁移 **必须**包含下表全部字段。漏列会导致 SqlSugar 生成 SQL 时报错,例如:`Unknown column 'ConcurrencyStamp' in 'field list'`。 | 字段名 | 字段类型 | 说明 | | --- | --- | --- | | Id | INT / BIGINT | 主键,自增(实现侧多为 `INT` IDENTITY) | | IsDeleted | TINYINT | 软删除:0-否,1-是,默认 0 | | **ConcurrencyStamp** | **CHAR(36)** | **并发戳(乐观锁版本)**;来自 Fake `AggregateRoot`,ORM 读写自动带上;更新时校验/轮换,防止并发覆盖写。业务代码一般无需手写。库表默认值用**固定 GUID**(见下),**禁止**迁移脚本写 `DEFAULT (UUID())` | | CreateUserId | INT | 创建人 | | CreateTime | DATETIME | 创建时间 | | UpdateUserId | INT | 更新人(可空) | | UpdateTime | DATETIME | 更新时间(可空) | **建表 / 迁移检查清单** 1. 新建表:`CREATE TABLE` 必须含 `ConcurrencyStamp CHAR(36) NOT NULL DEFAULT '00000000-0000-0000-0000-000000000000'`(建议放在 `IsDeleted` 之后)。 2. 增量脚本:若实体已有该属性而库表没有,须 `ALTER TABLE ... ADD COLUMN ConcurrencyStamp ...`(可参考 `sql/fix_concurrency_stamp_missing.sql`)。 3. **复制安全(Error/Note 1674)**:`DEFAULT (UUID())` / `UPDATE ... SET x = UUID()` 在 STATEMENT/MIXED binlog 下会被判为 unsafe(主从可能得到不同值)。迁移 DDL **一律用常量默认值**;真实 UUID 由应用插入/更新时写入。若必须用 `UUID()` 回填历史行,会话临时 `SET SESSION binlog_format='ROW'`(需权限)或接受仅在 ROW 复制拓扑执行。 4. 评审:新迁移 PR 对照本节必备字段,禁止再出现「实体有列、库表无列」。 5. 典型报错定位:接口返回 `Unknown column 'ConcurrencyStamp'` → 查 SQL 中的表名 → 对该表补列后重试。 **DDL 示例片段** ```sql `IsDeleted` TINYINT NOT NULL DEFAULT 0 COMMENT '软删除:0-否,1-是', -- 禁止:DEFAULT (UUID()) → 复制环境 1674 `ConcurrencyStamp` CHAR(36) NOT NULL DEFAULT '00000000-0000-0000-0000-000000000000' COMMENT '并发戳', `CreateUserId` INT NOT NULL DEFAULT 0 COMMENT '创建人', `CreateTime` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `UpdateUserId` INT NULL COMMENT '更新人', `UpdateTime` DATETIME NULL COMMENT '更新时间', ``` #### 3.1.3 字段命名规范 | 规范 | 说明 | 示例 | | --- | --- | --- | | 布尔字段 | 用 `IsXxx` 或 `HasXxx` | `IsDeleted`、`IsActive` | | 外键字段 | `关联表名Id` 或 `XxxId` | `CustomerId`、`WarehouseId` | | 时间字段 | `XxxTime` 或 `XxxAt` | `CreateTime`、`PayTime` | | 禁用保留字 | 不用 desc、range、match 等 | 用 `Description` 替代 `Desc` | #### 3.1.4 数据类型规范 | 类型 | 说明 | | --- | --- | | 主键 | BIGINT,单表自增 | | 金额/重量 | DECIMAL,禁止 float/double | | 字符串 | varchar 长度 ≤ 5000,超出用 TEXT | | 枚举 | 小数据量用 TINYINT,可读性要求高用 VARCHAR | | 状态标志 | TINYINT | ### 3.2 接口设计公共规范 | 规范 | 说明 | | --- | --- | | 操作审计必填 | 任何新增、编辑、删除操作,必须记录操作者与操作时间 | | 新增接口 | 创建成功后需落库 `CreateUserId`、`CreateTime` | | 编辑接口 | 更新成功后需落库 `UpdateUserId`、`UpdateTime` | | 删除接口 | 逻辑删除时需落库 `UpdateUserId`、`UpdateTime`,并更新 `IsDeleted=1` | > 审计字段中的操作者信息优先从当前登录会话的 **`mapId`**(`CurrentUser.GetId()`,即 `user.userid`)获取,不允许由前端直接信任覆盖。 ### 3.3 OMS 与 RBAC 会话协作 与 **`docs/rbac系统设计-060801.md`** 对齐的 OMS 侧约定如下。 #### 3.3.1 认证接口(不在本篇逐条列出) | 能力 | 路径 | 说明 | | --- | --- | --- | | 登录 | `POST /rbac/auth/login` | `type=0` 内部用户,`type=1` 客户 | | 刷新令牌 | `POST /rbac/auth/refresh-token` | | | 当前用户 | `GET /rbac/auth/get-current-user` | 返回 `roles`、`permissions`、`menus` | | 改密 | `POST /rbac/auth/change-password` | 客户改 `t_customer` MD5,内部用户改 RBAC 哈希 | 请求头:`Authorization: Bearer {accessToken}`。 #### 3.3.2 从会话解析 OMS 业务主体 ``` JWT → mapId(user.userid INT) → 查 t_customer WHERE UserId = mapId AND IsDeleted = 0 → 得到 t_customer.Id 作为业务 CustomerId → 若 ParentCustomerId 非空则为子客户,按接口约定决定是否归并到主客户 Id ``` #### 3.3.3 权限码与接口鉴权 - 权限码格式:`Rbac.Users.Query`、`Api.Interfaces.Query` 等(PascalCase 点分隔),定义见 RBAC 文档 **§5**。 - 当前后端对 OMS 业务接口主要校验 **已登录**(`[Authorize]`);细粒度 Permission 拦截待 RBAC 包完善。 - 前端路由 `meta.permission` 与 `userInfo.permissions` 对齐;实现细节见 RBAC 文档 **§9**。 #### 3.3.4 客户域 HTTP(OMS 宿主内) 客户资料 CRUD、子客户创建(含权限子集校验)见 **`/customer/*`**,契约见 **`docs/rbac系统设计-060801.md` §8.7**,不在本篇展开。 #### 3.3.5 订单模块客户数据隔离(`/admin/order/*`) 线上网关将 OMS 订单接口挂载为 **`https://api.nanafa.com/admin/order/*`**;服务内路由为 **`/order/*`**(网关前缀 `/admin` 由反向代理剥离或统一转发,二者指同一组接口)。 | 项 | 约定 | | --- | --- | | 隔离键 | 会话解析得到的 **`effectiveCustomerId`**(**`t_customer.Id`**,非 `mapId`) | | 解析方式 | `loginType=1`:`mapId` → `t_customer.UserId` → `t_customer.Id`;子客户是否归并主客户 Id 与 **§1.4** 一致,**全订单模块统一** | | 查询(R) | 列表/导出/任务列表等:**强制** `WHERE CustomerId = effectiveCustomerId`(及 `IsDeleted=0` 等既有条件);**忽略**请求体中用于越权的其它 `customerId` | | 单条读 | 按 `id` 加载后校验 `t_order.CustomerId == effectiveCustomerId`,否则按 **「订单不存在」** 返回(**禁止**泄露他客订单存在性) | | 新增(C) | 创建订单/批量任务时 **`CustomerId` 仅取自会话**,写入 `t_order.CustomerId` / `t_order_batch_job.CustomerId`;请求体若带 `customerId` 须与 `effectiveCustomerId` **一致**,否则 **403** | | 更新/删除(U/D) | 支付、预报、拦截、删除、打印、费用重算等:先 **归属校验** 再执行业务;批量接口逐单校验,任一不属于当前客户则该单失败或整批拒绝(实现二选一须在 **§4.4.0** 写死) | | 关联表 | `t_order_sender`、`t_order_recipient`、`t_order_declaration`、`t_order_weight`、`t_order_fee`、**`t_order_file`** 等通过 **`OrderId`** 间接隔离,**禁止**仅凭子表 Id 绕过主表 `CustomerId` 校验;**`t_customer_sender_address`** 以 **`CustomerId`** 直接隔离(§4.2.10) | | 内部用户 | `loginType=0` **默认禁止**调用 `/admin/order/*` 客户订单接口;若运营后台需跨客户查单,须 **另设** 内部管理路径与 PermissionCode,**不得**复用客户侧 `/admin/order` 且不带客户隔离 | > **安全原则**:`CustomerId` **不得**由前端单独作为可信事实;**不得**仅按订单 `Id` 操作而不校验归属。 ## 4 订单模块 ### 4.1 核心需求说明 - **我的订单**:支持订单全流程管理,包括分页查询、详情查看、支付、预报、拦截/取消拦截、删除、打印、导出、**订单附件**(§4.2.12 / §4.4.1.17);**创建问题件**、**创建理赔件**、处理子状态筛选;可按 **`OrderSource`**(含揽收)筛选;订单状态覆盖草稿→已预报→已收货→待支付→待发货→运输中→派送成功/失败→问题件→理赔件等全生命周期。**客户门户**附件仅可查看 **系统面单**,不可上传/删除。 - **我要查价**:根据目的国、运输方式、重量尺寸、货物类型等条件查价,返回可选渠道及费用明细;与正式下单共用「选版本 + 内存匹配」计费逻辑,响应宜携带 `rulesVersion` / `priceVersion` 便于与订单对齐。 - **我要下单**:支持下载下单模板、上传 Excel 批量下单、在 **批量任务** 页查看导入进度与失败明细;模板分为标准模板与自定义模板。**单次导入订单上限 2000 条**(Excel 有效数据行或 `/order/parcel/batch-create` 请求数组长度,超出拒绝)。 - **发件地址**(菜单挂 **客户**,接口属订单域):客户维护可复用的发件地址簿,供揽收下单选择;**不**写入订单快照表 `t_order_sender`(见 §4.2.10 / §4.4.5)。 - **揽收订单**(菜单挂 **订单**):简化创建——选发件地址 + 仓库地址、填揽收时间与货物名称/重量、选运输方式(销售产品)即可成单;后端组装收/发件与申报后复用小包创建全链路(§4.4.4)。 - **店铺订单**:拉单/同步后,客户可对勾选单据做 **创建物流订单**(仅下物流单)或 **创建物流&代发订单**(下物流单并绑定仓库包裹/库存);交互以「校对防错」为第一优先级(§4.4.7)。另有既有 **匹配库存**(平台 SKU 映射后扣减建单),与上述两入口分流。 ### 4.2 表结构设计 > **命名约定**:表名按业务模块加 t_ 前缀;字段名驼峰命名(开头大写+单词驼峰)。 #### 4.2.1 订单表 `t_order` | 字段名 | 字段类型 | 长度 | 主键/外键 | 非空 | 说明 | | --- | --- | --- | --- | --- | --- | | Id | BIGINT | - | 主键 | 是 | 自增唯一标识 | | ServiceNo | VARCHAR | 64 | - | 是 | 运单号 | | ClientNo | VARCHAR | 64 | - | 否 | 客户单号 | | TrackNo | VARCHAR | 64 | - | 否 | 跟踪号 | | DownstreamServiceNo | VARCHAR | 64 | - | 否 | 服务商单号(下游 createOrderOne 成功响应 **`Data.serviceNo`**);OMS 自有运单号仍为 **`ServiceNo`** | | ProductCode | VARCHAR | 128 | - | 否 | 产品code | | ChannelCode | VARCHAR | 128 | - | 否 | 渠道code | | DownstreamSupplierCode | VARCHAR | 64 | - | 否 | 下游 API 服务商编码(itfx **`supplierCode`**);排货确认渠道时自规则快照 **`Providers`** 写入 | | DownstreamAppKey | VARCHAR | 128 | - | 否 | 下游 itfx 应用 **AppKey**;与 **`DownstreamAppId`** 配套 | | DownstreamAppId | INT | - | - | 否 | 下游 itfx 应用 Id;**AppSecret 不落库**,运行时按 Id 调 itfx **`/App/GetInfo`** | | ChannelForecastCode | VARCHAR | 128 | - | 否 | 渠道预报编码(**ForecastCode** / 轨迹 **ApiCode**);排货确认时固化 | | ConsigneeCountry | VARCHAR | 8 | - | 否 | 国家二字码 | | CargoAttribute | VARCHAR | 32 | - | 否 | 货物属性:general-普货,electronics-3c&带电,sensitive-敏感货 | | OrderStatus | VARCHAR | 32 | - | 是 | 订单状态 | | HandleStatus | VARCHAR | 16 | - | 否 | 处理子状态:`pending` 待处理 / `processed` 已处理;仅 **问题件、理赔件** 有效 | | IssueType | VARCHAR | 32 | - | 否 | 问题类型 code,见 **§10.3.6** / `IssueTypeCodes` | | IssueTitle | VARCHAR | 128 | - | 否 | 问题标题(可来自问题模板) | | IssueContent | TEXT | - | - | 否 | 问题内容 | | IssueTemplateId | BIGINT | - | 外键 → `t_issue_template.Id` | 否 | 引用的问题模板 | | IssueRemark | VARCHAR | 512 | - | 否 | 问题备注(人工补充) | | IssueTime | DATETIME | - | - | 否 | 标记问题件时间 | | ClaimType | VARCHAR | 32 | - | 否 | 理赔类型 code,见 **§10.3.7** | | ClaimAmount | DECIMAL | 18,4 | - | 否 | 索赔金额 | | ClaimCurrency | VARCHAR | 16 | - | 否 | 索赔币种 | | ClaimReason | VARCHAR | 512 | - | 否 | 理赔原因说明 | | ClaimTime | DATETIME | - | - | 否 | 创建理赔件时间 | | PreviousOrderStatus | VARCHAR | 32 | - | 否 | 进入异常状态前的订单状态(便于追溯/回滚扩展) | | WarehouseId | BIGINT | - | 外键 | 否 | 仓库ID | | CustomerId | BIGINT | - | 外键 → `t_customer.Id` | 否 | 订单归属客户业务主键 | | OrderSource | VARCHAR | 32 | - | 否 | 订单来源:`manual` 普通/我要下单,`pickup` 揽收订单,`upload` Excel 上传,`batch` JSON 批量,`platform` 平台店;**默认 `manual`**;见 **§10.3.9** | | PickupTime | DATETIME | - | - | 否 | 预约揽收时间;仅 **`OrderSource=pickup`** 有业务含义 | | CustomerSenderAddressId | BIGINT | - | 外键 → `t_customer_sender_address.Id` | 否 | 创建时选用的发件地址簿 Id(溯源;地址变更不影响订单快照) | | SenderId | BIGINT | - | 外键 | 否 | 发件人ID → t_order_sender | | RecipientId | BIGINT | - | 外键 | 否 | 收件人ID → t_order_recipient | | DeclarationId | BIGINT | - | 外键 | 否 | 申报信息ID → t_order_declaration | | ExportCustomsMode | VARCHAR | 16 | - | 否 | 出口报关方式,见 ExportCustomsMode 枚举 | | ReceiveTime | DATETIME | - | - | 否 | 收货时间 | | ShipTime | DATETIME | - | - | 否 | 发货时间 | | PayTime | DATETIME | - | - | 否 | 支付时间 | | SignTime | DATETIME | - | - | 否 | 签收时间 | | IsCod | TINYINT | - | - | 否 | 是否COD:0-否,1-是 | | CodCurrency | VARCHAR | 16 | - | 否 | COD币种 | | CodSum | DECIMAL | 18,4 | - | 否 | COD金额 | | IsIntercept | TINYINT | - | - | 否 | 是否拦截:0-否,1-是 | | IsRemote | TINYINT | - | - | 否 | 是否偏远:0-否,1-是 | | PredeclareFailReason | VARCHAR | 512 | - | 否 | 预报失败原因:草稿已落库后环节失败时记录;黑名单命中、快照未命中等不落库场景不写此字段 | | AllocationPendingReason | VARCHAR | 512 | - | 否 | 待排货原因:入库排货或渠道买价未匹配时记录(`OrderStatus=pendingAllocation`) | | SalesProductRulesVersion | VARCHAR | 32 | - | 否 | 下单绑定的规则版本号(与发布记录 `Version` 一致,`OperatorType=rules`) | | SalesProductPriceVersion | VARCHAR | 32 | - | 否 | 下单绑定的价格版本号(与发布记录 `Version` 一致,`OperatorType=price`) | | LabelTemplateId | INT | - | 外键 → `t_label_template.Id` | 否 | 出单实际使用的标签模板 Id(模板配置,**非**面单文件地址;文件见 **`t_order_file.systemLabel`**) | | IsDeleted | TINYINT | - | - | 是 | 是否删除:0-否,1-是,默认0 | | CreateUserId | INT | - | - | 是 | 创建人 | | CreateTime | DATETIME | - | - | 是 | 创建时间 | | UpdateUserId | INT | - | - | 否 | 更新人 | | UpdateTime | DATETIME | - | - | 否 | 更新时间 | > **已废弃字段(勿再写入)**:原 `LabelUrl` / `LabelGenerateStatus` / `LabelGenerateError` 已迁至 **`t_order_file`**(见 **§4.2.12**),迁移脚本删除订单侧列。 **OrderStatus 枚举**:draft, predeclared, pendingAllocation, received, pendingPayment, pendingShip, pendingOnline, shipping, deliverySuccess, deliveryFailed, problematic, deleted, claimed **OrderSource 枚举**:见 **§10.3.9**(`manual` / `pickup` / `upload` / `batch` / `platform`) **CargoAttribute 枚举**:general-普货,electronics-3c&带电,sensitive-敏感货 **ExportCustomsMode 枚举**(存库与下列 code 一致):`9610Simple`(9610简易申报),`9610Summary`(9610汇总申报),`9610Separate`(9610单独申报),`9710`,`9810`,`0110` > **请求约定**:`exportCustomsMode` **可选**;若传入须为上表 **code 本身**。非法示例:`9610`、`1`(字典序号)、`export_customs_mode` 字典项编码等;非法时返回 **「出口报关方式不合法」**。 #### 4.2.2 下单模板表 `t_order_template` | 字段名 | 字段类型 | 长度 | 主键/外键 | 非空 | 说明 | | --- | --- | --- | --- | --- | --- | | Id | BIGINT | - | 主键 | 是 | 自增唯一标识 | | Name | VARCHAR | 64 | - | 是 | 模板名称 | | Type | VARCHAR | 16 | - | 是 | standard/custom | | TemplateKey | VARCHAR | 64 | - | 否 | 模板标识 | | CustomerId | BIGINT | - | 外键 → `t_customer.Id` | 否 | 客户ID(自定义模板) | | IsDeleted | TINYINT | - | - | 是 | 是否删除:0-否,1-是,默认0 | | CreateUserId | INT | - | - | 是 | 创建人 | | CreateTime | DATETIME | - | - | 是 | 创建时间 | | UpdateUserId | INT | - | - | 否 | 更新人 | | UpdateTime | DATETIME | - | - | 否 | 更新时间 | #### 4.2.3 订单批量任务表 `t_order_batch_job` / `t_order_batch_job_item` > **说明**:原 `t_order_task` / `t_order_task_fail` 已合并至本组表;Excel 导入、批量支付/预报/打印等异步操作均统一落库于此。 **任务头表 `t_order_batch_job`(摘要)** | 字段名 | 说明 | | --- | --- | | Id | 主键 | | Action | 操作类型:`import-row` / `pay` / `pre-declare` / `print` 等 | | Status | `pending` / `running` / `completed` / `failed` / `cancelled` | | Total / SuccessCount / FailCount | 进度统计 | | CustomerId | 客户隔离键 | | PayloadJson | 任务级扩展(导入时含 `taskName`、`templateId`、`customerId`) | | RequestId | 幂等键 | | StartedAt / FinishedAt | 执行时间 | **任务明细表 `t_order_batch_job_item`(摘要)** | 字段名 | 说明 | | --- | --- | | JobId | 关联任务头 | | OrderId | 目标订单(导入行前为 0) | | ClientNo / ServiceNo | 客户单号 / 运单号(执行后回填) | | Status | `pending` / `running` / `success` / `failed` / `cancelled` | | PayloadJson | 子项入参(导入行含 `rowNumber`、`rowDataJson`) | | ResultJson / FailReason | 执行结果 / 失败原因 | | RetryCount | 系统重试次数 | 完整 DDL 见 `oms0414-back/db.sql` 或 `scripts/order_batch_job_tables.sql`。 #### 4.2.4 订单发件人表 `t_order_sender` | 字段名 | 字段类型 | 长度 | 主键/外键 | 非空 | 说明 | | --- | --- | --- | --- | --- | --- | | Id | BIGINT | - | 主键 | 是 | 自增唯一标识 | | OrderId | BIGINT | - | 外键 | 是 | 订单ID | | SenderName | VARCHAR | 50 | - | 否 | 姓名 | | Company | VARCHAR | 255 | - | 否 | 公司 | | Country | INT | - | - | 否 | 国家(存 **`t_country.Id`**;创建小包请求体见 §4.4.3.1,传 ISO 二字码) | | Province | VARCHAR | 50 | - | 否 | 州省 | | City | VARCHAR | 50 | - | 否 | 城市 | | ZipCode | VARCHAR | 50 | - | 否 | 邮编 | | StreetAddress1 | VARCHAR | 500 | - | 否 | 地址1 | | StreetAddress2 | VARCHAR | 500 | - | 否 | 地址2 | | StreetAddress3 | VARCHAR | 500 | - | 否 | 地址3 | | HouseNumber | VARCHAR | 50 | - | 否 | 门牌号 | | Phone | VARCHAR | 50 | - | 否 | 手机 | | TelePhone | VARCHAR | 50 | - | 否 | 电话 | | Email | VARCHAR | 60 | - | 否 | 邮箱 | | IsDefault | TINYINT | - | - | 否 | 是否默认:0-否,1-是 | | IsDeleted | TINYINT | - | - | 是 | 是否删除:0-否,1-是,默认0 | | CreateUserId | INT | - | - | 是 | 创建人 | | CreateTime | DATETIME | - | - | 是 | 创建时间 | | UpdateUserId | INT | - | - | 否 | 更新人 | | UpdateTime | DATETIME | - | - | 否 | 更新时间 | #### 4.2.5 订单收件人表 `t_order_recipient` | 字段名 | 字段类型 | 长度 | 主键/外键 | 非空 | 说明 | | --- | --- | --- | --- | --- | --- | | Id | BIGINT | - | 主键 | 是 | 自增唯一标识 | | OrderId | BIGINT | - | 外键 | 是 | 订单ID | | RecipientsName | VARCHAR | 30 | - | 否 | 收件人名称 | | CompanyName | VARCHAR | 60 | - | 否 | 收件人公司名称 | | Country | INT | - | - | 否 | 国家(存 **`t_country.Id`**;创建小包请求体见 §4.4.3.1,传 ISO 二字码) | | Phone | VARCHAR | 20 | - | 否 | 收件人手机 | | TelePhone | VARCHAR | 20 | - | 否 | 收件人电话 | | ZipCode | VARCHAR | 15 | - | 否 | 收件人邮编 | | Province | VARCHAR | 25 | - | 否 | 收件人省/州 | | City | VARCHAR | 25 | - | 否 | 收件人城市 | | StreetNumber | VARCHAR | 20 | - | 否 | 收件人门牌号 | | StreetAddress1 | VARCHAR | 255 | - | 否 | 收件人地址1 | | StreetAddress2 | VARCHAR | 255 | - | 否 | 收件人地址2 | | StreetAddress3 | VARCHAR | 255 | - | 否 | 收件人地址3 | | Email | VARCHAR | 60 | - | 否 | 收件人邮箱 | | TaxType | INT | - | - | 否 | 税号类型 | | TaxNo | VARCHAR | 35 | - | 否 | 税号 | | CertificateType | INT | - | - | 否 | 证件类型 | | CertificateNo | VARCHAR | 30 | - | 否 | 证件号码 | | Eori | VARCHAR | 255 | - | 否 | EORI | | Vat | VARCHAR | 255 | - | 否 | Vat | | IsDeleted | TINYINT | - | - | 是 | 是否删除:0-否,1-是,默认0 | | CreateUserId | INT | - | - | 是 | 创建人 | | CreateTime | DATETIME | - | - | 是 | 创建时间 | | UpdateUserId | INT | - | - | 否 | 更新人 | | UpdateTime | DATETIME | - | - | 否 | 更新时间 | #### 4.2.6 订单申报信息表 `t_order_declaration` | 字段名 | 字段类型 | 长度 | 主键/外键 | 非空 | 说明 | | --- | --- | --- | --- | --- | --- | | Id | BIGINT | - | 主键 | 是 | 自增唯一标识 | | OrderId | BIGINT | - | 外键 | 是 | 订单ID | | CnName | VARCHAR | 500 | - | 否 | 品名--中 | | EnName | VARCHAR | 500 | - | 否 | 品名--英 | | Sku | VARCHAR | 255 | - | 否 | SKU | | Unit | VARCHAR | 5 | - | 否 | 单位 | | UnitPrice | DECIMAL | 10,2 | - | 否 | 单价 | | UnitWeight | DECIMAL | 11,3 | - | 否 | 单重 | | Quantity | INT | - | - | 否 | 数量 | | Amount | DECIMAL | 11,2 | - | 否 | 总金额 | | Weight | DECIMAL | 11,3 | - | 否 | 总重量 | | HsCode | VARCHAR | 50 | - | 否 | HsCode | | SkuImage | VARCHAR | 255 | - | 否 | 图片 | | Category | VARCHAR | 20 | - | 否 | 类别 | | Brand | VARCHAR | 50 | - | 否 | 品牌 | | BrandType | VARCHAR | 50 | - | 否 | 品牌类型 | | IsMagnetic | BIT | - | - | 否 | 是否带磁带电 | | Distribution | VARCHAR | 255 | - | 否 | 配货信息 | | Material | VARCHAR | 50 | - | 否 | 材质 | | EnMaterial | VARCHAR | 64 | - | 否 | 英文材质 | | Purpose | VARCHAR | 50 | - | 否 | 用途 | | Attributes | VARCHAR | 50 | - | 否 | 属性 | | OriginCountry | VARCHAR | 50 | - | 否 | 原产国 | | SalesLink | VARCHAR | 255 | - | 否 | 销售链接 | | Model | VARCHAR | 64 | - | 否 | 型号 | | Specifications | VARCHAR | 50 | - | 否 | 规格 | | Comment | VARCHAR | 500 | - | 否 | 备注 | | IsDeleted | TINYINT | - | - | 是 | 是否删除:0-否,1-是,默认0 | | CreateUserId | INT | - | - | 是 | 创建人 | | CreateTime | DATETIME | - | - | 是 | 创建时间 | | UpdateUserId | INT | - | - | 否 | 更新人 | | UpdateTime | DATETIME | - | - | 否 | 更新时间 | #### 4.2.7 订单件重尺表 `t_order_weight` | 字段名 | 字段类型 | 长度 | 主键/外键 | 非空 | 说明 | | --- | --- | --- | --- | --- | --- | | Id | BIGINT | - | 主键 | 是 | 自增唯一标识 | | OrderId | BIGINT | - | 外键 | 是 | 订单ID | | Type | VARCHAR | 16 | - | 是 | 类型:forecast/receive/ship/downstream | | Length | DECIMAL | 10,2 | - | 否 | 长(cm) | | Width | DECIMAL | 10,2 | - | 否 | 宽(cm) | | Height | DECIMAL | 10,2 | - | 否 | 高(cm) | | ActualWeight | DECIMAL | 10,3 | - | 否 | 实重(kg) | | VolumetricWeight | DECIMAL | 10,3 | - | 否 | 体积重(kg) | | ChargeableWeight | DECIMAL | 10,3 | - | 否 | 计费重(kg) | | Cubic | DECIMAL | 12,6 | - | 否 | 立方(m³) | | IsDeleted | TINYINT | - | - | 是 | 是否删除:0-否,1-是,默认0 | | CreateUserId | INT | - | - | 是 | 创建人 | | CreateTime | DATETIME | - | - | 是 | 创建时间 | | UpdateUserId | INT | - | - | 否 | 更新人 | | UpdateTime | DATETIME | - | - | 否 | 更新时间 | **Type 枚举**:forecast(预报)、receive(收货)、ship(发货)、downstream(下游) **履约更新**:四行 **`t_order_weight`** 各维护独立件重尺口径,**入库/出库仅覆盖并重算本环节对应 `Type` 行**,**不得**向其它 `Type` 拷贝或同步。**入库** → 仅 **`receive` 行**(§6.3.1.1);**出库** → 仅 **`ship` 行**(§6.3.2.1)。**`forecast`** 保留下单预报口径;**`downstream`** 由下游回传或独立流程更新,**不由**入库/出库流程写入。 #### 4.2.8 订单费用表 `t_order_fee`(子表,关联 t_order) | 字段名 | 字段类型 | 长度 | 主键/外键 | 非空 | 说明 | | --- | --- | --- | --- | --- | --- | | Id | BIGINT | - | 主键 | 是 | 自增唯一标识 | | OrderId | BIGINT | - | 外键 | 是 | 订单ID | | Type | VARCHAR | 16 | - | 是 | 类型:receivable(应收)/payable(应付) | | CostItem | VARCHAR | 64 | - | 是 | 费用项 | | Amount | DECIMAL | 18,4 | - | 是 | 金额 | | Currency | VARCHAR | 16 | - | 是 | 币种:RMB/USD/EUR | | DownstreamProvider | VARCHAR | 128 | - | 否 | 下游服务商 | | PriceNo | VARCHAR | 64 | - | 否 | 价格编号(建议写入订单绑定的 `SalesProductPriceVersion` 或渠道侧价格版本) | | PriceEffectiveTime | DATETIME | - | - | 否 | 价格版本生效时间(对应该次计费所用快照的生效时刻) | | ChargingStage | VARCHAR | 16 | - | 否 | 本行费用是在哪一重量阶段计入:`forecast`(预报)/`inbound`(入库)/`outbound`(出库),由触发算价时的件重尺类型决定,便于对账 | | IsDeleted | TINYINT | - | - | 是 | 是否删除:0-否,1-是,默认0 | | CreateUserId | INT | - | - | 是 | 创建人 | | CreateTime | DATETIME | - | - | 是 | 创建时间 | | UpdateUserId | INT | - | - | 否 | 更新人 | | UpdateTime | DATETIME | - | - | 否 | 更新时间 | **Type 枚举**:receivable(应收)、payable(应付) #### 4.2.9 订单异常事件表 `t_order_exception_event` > **用途**:问题件/理赔件操作的 **审计流水**(一单可多次「更新处理子状态」;问题件与理赔件各记一条创建事件)。主表 `t_order` 存 **当前快照**,本表存 **历史**。 | 字段名 | 字段类型 | 长度 | 主键/外键 | 非空 | 说明 | | --- | --- | --- | --- | --- | --- | | Id | BIGINT | - | 主键 | 是 | 自增 | | OrderId | BIGINT | - | 外键 → `t_order.Id` | 是 | 订单 Id | | EventType | VARCHAR | 32 | - | 是 | `mark_problematic` / `mark_claimed` / `handle_processed` | | FromStatus | VARCHAR | 32 | - | 否 | 变更前 `OrderStatus` | | ToStatus | VARCHAR | 32 | - | 否 | 变更后 `OrderStatus` | | FromHandleStatus | VARCHAR | 16 | - | 否 | 变更前 `HandleStatus` | | ToHandleStatus | VARCHAR | 16 | - | 否 | 变更后 `HandleStatus` | | IssueType | VARCHAR | 32 | - | 否 | 问题类型(创建问题件时) | | Title | VARCHAR | 128 | - | 否 | 标题快照 | | Content | TEXT | - | - | 否 | 内容快照 | | IssueTemplateId | BIGINT | - | - | 否 | 模板 Id | | NotifyMethod | VARCHAR | 64 | - | 否 | 通知方式快照(逗号分隔) | | ClaimType | VARCHAR | 32 | - | 否 | 理赔类型 | | ClaimAmount | DECIMAL | 18,4 | - | 否 | 索赔金额 | | ClaimCurrency | VARCHAR | 16 | - | 否 | 索赔币种 | | ClaimReason | VARCHAR | 512 | - | 否 | 理赔原因 | | Remark | VARCHAR | 512 | - | 否 | 操作备注 | | CreateUserId | INT | - | - | 是 | 操作人 | | CreateTime | DATETIME | - | - | 是 | 操作时间 | **索引**:`idx_oee_order_time` (`OrderId`, `CreateTime`)。 #### 4.2.10 客户发件地址表 `t_customer_sender_address` > **用途**:客户侧 **可复用发件地址簿**(菜单 **客户 → 发件地址**)。创建揽收订单时按 Id 读取并 **快照** 到 `t_order_sender`;此后地址簿修改 **不影响** 已下订单。 > **与 `t_order_sender` 关系**:`t_order_sender` 仍是 **订单瞬时快照**(每单一行);本表是 **主数据模板**,按 `CustomerId` 隔离。 > **迁移脚本(待编写)**:`sql/migrate_t_customer_sender_address.sql`;订单扩展字段见 `sql/migrate_t_order_pickup_fields.sql`(`OrderSource` / `PickupTime` / `CustomerSenderAddressId`)。 | 字段名 | 字段类型 | 长度 | 主键/外键 | 非空 | 说明 | | --- | --- | --- | --- | --- | --- | | Id | BIGINT | - | 主键 | 是 | 自增 | | CustomerId | BIGINT | - | 外键 → `t_customer.Id` | 是 | 归属客户(隔离键) | | AddressName | VARCHAR | 64 | - | 否 | 地址别名(如「公司仓」「张三宅」);列表展示用 | | SenderName | VARCHAR | 50 | - | 是 | 姓名 | | Company | VARCHAR | 255 | - | 否 | 公司 | | Country | INT | - | - | 否 | 国家(存 **`t_country.Id`**;接口传 ISO 二字码) | | Province | VARCHAR | 50 | - | 否 | 州省 | | City | VARCHAR | 50 | - | 否 | 城市 | | ZipCode | VARCHAR | 50 | - | 否 | 邮编 | | StreetAddress1 | VARCHAR | 500 | - | 是 | 地址1 | | StreetAddress2 | VARCHAR | 500 | - | 否 | 地址2 | | StreetAddress3 | VARCHAR | 500 | - | 否 | 地址3 | | HouseNumber | VARCHAR | 50 | - | 否 | 门牌号 | | Phone | VARCHAR | 50 | - | 否 | 手机 | | TelePhone | VARCHAR | 50 | - | 否 | 电话 | | Email | VARCHAR | 60 | - | 否 | 邮箱 | | IsDefault | TINYINT | - | - | 是 | 是否默认:0-否,1-是;**同一客户仅允许一条默认**(设默认时清空其它) | | IsDeleted | TINYINT | - | - | 是 | 软删:0-否,1-是,默认 0 | | CreateUserId | INT | - | - | 是 | 创建人 | | CreateTime | DATETIME | - | - | 是 | 创建时间 | | UpdateUserId | INT | - | - | 否 | 更新人 | | UpdateTime | DATETIME | - | - | 否 | 更新时间 | **索引** | 索引名 | 类型 | 字段 | 说明 | | --- | --- | --- | --- | | idx_csa_customer | KEY | CustomerId, IsDeleted, Id | 客户列表 | | idx_csa_customer_default | KEY | CustomerId, IsDefault | 默认地址查询 | #### 4.2.11 物流单仓储绑定表 `t_order_stock_bind` > **用途**:客户在「我的订单」对**已创建成功**的物流单(`t_order`)绑定仓储 **包裹行** 或 **库存 SKU**,用于代发出库占用。 > **与店铺链路区别**: > - **匹配库存** `match-inventory`:店铺单 → **扣库存** → **创建**物流单; > - **创建物流&代发** §4.4.7:店铺单 → **创建**物流单 → 本表 **冻结绑定**(不即时扣实物); > - 本表在「我的订单」也可对已有物流单单独挂载包裹/库存。 > **数量**:绑定数量**不要求**与 `t_order_declaration` 申报件数对齐。 > **冻结**:绑定保存时冻结(`C_inventory` / `C_parcel_item` 的 `FrozenQty`↑、`AvailableQty`↓);确认出库/预报钩子再扣实物 `Qty`/`Quantity` 并释放冻结。 > **迁移脚本**:`sql/migrate_t_order_stock_bind.sql`。 | 字段名 | 字段类型 | 长度 | 主键/外键 | 非空 | 说明 | | --- | --- | --- | --- | --- | --- | | Id | INT | - | 主键 | 是 | 自增 | | OrderId | INT | - | 外键 → `t_order.Id` | 是 | 物流单 | | CustomerId | INT | - | 外键 → `t_customer.Id` | 是 | 归属客户(隔离键) | | WarehouseId | INT | - | - | 是 | 仓库;须与订单 `WarehouseId` 一致(订单无仓库时以本字段写入为准) | | BindType | VARCHAR | 20 | - | 是 | `inventory` / `parcelItem` | | SkuId | INT | - | - | 否 | 库存必填;包裹行可空(未上架) | | InventoryId | INT | - | → `C_inventory.Id` | 否 | 指定库位行;空则按 SKU 自动分摊冻结 | | BinLocationId | INT | - | - | 否 | 库位快照 | | ParcelId | INT | - | → `C_parcel.Id` | 否 | `BindType=parcelItem` 时必填 | | ParcelItemId | INT | - | → `C_parcel_item.Id` | 否 | `BindType=parcelItem` 时必填 | | BindQty | INT | - | - | 是 | 绑定数量,>0 | | Status | VARCHAR | 20 | - | 是 | `bound` 已冻结 / `deducted` 已扣实物 / `released` 已释放 | | SkuCode | VARCHAR | 64 | - | 否 | 展示快照 | | SkuName | VARCHAR | 128 | - | 否 | 展示快照 | | TrackingNo | VARCHAR | 64 | - | 否 | 包裹运单号快照 | | ProductName | VARCHAR | 256 | - | 否 | 包裹品名快照 | | ImageUrl | VARCHAR | 512 | - | 否 | 图片快照 | | AttributeText | VARCHAR | 256 | - | 否 | 规格快照 | | Remark | VARCHAR | 512 | - | 否 | 备注 | | IsDeleted | TINYINT | - | - | 是 | 软删,默认 0 | | ConcurrencyStamp | CHAR | 36 | - | 是 | 并发戳;见 **§3.1.2**,默认 UUID | | CreateUserId | INT | - | - | 是 | 创建人 | | CreateTime | DATETIME | - | - | 是 | 创建时间 | | UpdateUserId | INT | - | - | 否 | 更新人 | | UpdateTime | DATETIME | - | - | 否 | 更新时间 | **索引** | 索引名 | 类型 | 字段 | 说明 | | --- | --- | --- | --- | | idx_osb_order | KEY | OrderId, IsDeleted, Status | 订单绑定明细 | | idx_osb_parcel_item | KEY | ParcelItemId, Status, IsDeleted | 包裹行占用汇总 | | idx_osb_sku | KEY | CustomerId, WarehouseId, SkuId, Status | 库存占用查询 | **仓储表字段增量(同迁移脚本)** | 表 | 新增字段 | 说明 | | --- | --- | --- | | `C_inventory` | `FrozenQty` INT NOT NULL DEFAULT 0 | 已绑定未出库冻结量 | | `C_inventory` | `AvailableQty` INT NOT NULL DEFAULT 0 | 可用 = `Qty - FrozenQty`(应用层维护) | | `C_parcel_item` | `FrozenQty` INT NOT NULL DEFAULT 0 | 已被物流单占用数量 | | `C_parcel_item` | `AvailableQty` INT NOT NULL DEFAULT 0 | 可用 = `Quantity - FrozenQty`(应用层维护) | **不变量** 1. `AvailableQty = Qty(或 Quantity) - FrozenQty`,且三者均 ≥ 0。 2. 绑定保存:校验 `BindQty ≤ AvailableQty`(对本单原占用先释放再算)。 3. 包裹 Tab **允许全部生命周期**(`inTransit` / `arrived` / `stored`)。 4. 已上架包裹行(有 `SkuId`)绑定包裹时**同时**冻结对应库存,避免重复售卖。 5. 保存采用**全量覆盖**:提交 `items[]` 为最终态。 #### 4.2.12 订单附件表 `t_order_file` > **用途**:订单级附件(交易证明、系统面单、下游面单、末端面单、红外图片)统一落库;**不再**在 `t_order` 上存面单 URL / 生成状态字段。 > **系统面单**:`FileType=systemLabel` **仅允许 1 份**;生成或上传时先软删同类型旧记录再插入(替换)。其它类型每类最多 **20** 份。 > **客户 ACL**:客户登录仅可 **查询** `systemLabel`;**禁止**上传/删除任意附件类型。内部账号可管理全部类型。 > **迁移脚本**:`sql/20260728_order_file.sql`(建表 + 迁移旧 `LabelUrl` → `systemLabel` + 删除订单旧列);注释补丁 `sql/20260728_order_file_comments.sql`。 | 字段名 | 字段类型 | 长度 | 主键/外键 | 非空 | 说明 | | --- | --- | --- | --- | --- | --- | | Id | INT | - | 主键 | 是 | 自增 | | OrderId | INT | - | 外键 → `t_order.Id` | 是 | 订单 Id | | FileType | VARCHAR | 32 | - | 是 | 附件类型:`tradeProof` 交易证明 / `systemLabel` 系统面单 / `downstreamLabel` 下游面单 / `lastMileLabel` 末端面单 / `infraredImage` 红外图片 | | FileName | VARCHAR | 255 | - | 否 | 原始文件名 | | FileUrl | VARCHAR | 512 | - | 是 | 文件地址(相对路径如 `/uploads/...` 或外链) | | ContentType | VARCHAR | 128 | - | 否 | 文件 MIME 类型 | | FileSize | BIGINT | - | - | 否 | 文件大小(字节) | | Sort | INT | - | - | 是 | 同类型内排序,默认 0 | | Source | VARCHAR | 32 | - | 是 | 来源:`upload` 上传 / `systemGenerate` 系统生成 / `downstream` 下游回传 / `migrate` 迁移;默认 `upload` | | Remark | VARCHAR | 512 | - | 否 | 备注 | | IsDeleted | TINYINT | - | - | 是 | 软删除:0-否,1-是,默认 0 | | ConcurrencyStamp | CHAR | 36 | - | 是 | 并发戳;见 **§3.1.2**,默认 UUID | | CreateUserId | INT | - | - | 是 | 创建人 | | CreateTime | DATETIME | - | - | 是 | 创建时间 | | UpdateUserId | INT | - | - | 否 | 更新人 | | UpdateTime | DATETIME | - | - | 否 | 更新时间 | **索引** | 索引名 | 类型 | 字段 | 说明 | | --- | --- | --- | --- | | idx_ord_file_order_type | KEY | OrderId, FileType, IsDeleted | 按订单+类型查询 | **写入约定** | 场景 | FileType | Source | 说明 | | --- | --- | --- | --- | | 创单成功 / 打印补生成 / 批量 `generate-label-pdf` | `systemLabel` | `systemGenerate` | `LabelPdfGenerationService`;替换旧系统面单 | | 内部上传 | 任一类型 | `upload` | `POST /order/files/upload`;`systemLabel` 仍仅 1 份 | | 下游 createOrderOne 成功返回 `labelUrl` | `downstreamLabel` | `downstream` | 不写 `t_order`;与 `DownstreamServiceNo`/`TrackNo` 一并回写 | | 历史 `t_order.LabelUrl` 迁移 | `systemLabel` | `migrate` | 一次性 SQL | ### 4.3 下单计费与版本策略(配置 → 运行时 → 下单) > 本节约定**销售产品**维度的规则/卖价如何进入线上算价链路,以及与 `t_order` 版本字段的对应关系;与渠道模块中「运营发布 → 生成版本」接口衔接。 #### 4.3.1 总体流程 ```text 运营新建版本 → 录入/维护规则与卖价 → 发布并设为生效 ↓ 系统组装结构化 JSON(规则快照 + 价格快照,均绑定 salesProductId 与 version) ↓ 写入 Redis(按产品/版本可寻址)→ 计费服务实例订阅或定时拉取 → 载入本地内存(热数据) ↓ 用户下单 / 预估(高 QPS) ↓ 内存匹配:按 bindTime 选定生效版本 → 规则(黑名单/偏远/计重/进位/价格分区等)→ 价格明细 → 输出费用 ↓ 订单落库:固化 SalesProductRulesVersion、SalesProductPriceVersion(必要时可另存发布记录 Id 便于追溯,非本表字段) ↓ 后台上新发布、调价 → 仅刷新 Redis 与各实例内存中的「当前生效」视图 ↓ 新订单:bindTime 落在 new version 生效之后 → 绑定新版本算价 老订单:已写入的版本号不变 → 复核/对账/理赔仍以当时快照为准,不因后台最新发布而改历史费用 ``` #### 4.3.2 设计要点 | 要点 | 说明 | | --- | --- | | 版本不可变 | 订单创建/预报成功时写入 `SalesProductRulesVersion`、`SalesProductPriceVersion`,作为该票货的**计费契约**;非「改单重算」类流程不得静默改写。 | | 生效与历史 | 同一 `salesProductId` 在同一时刻仅一条「对外生效」规则+价格组合;历史 version 保留在 **Redis(多版本 key)** 与 **`t_sales_product_publish_record`**,便于审计与回放。 | | Redis | 建议按 `salesProductId` + `version` 存完整结构化快照 JSON;可选维护 `current` 指针 key 指向当前生效 version,便于实例 O(1) 发现最新配置。 | | 本地内存 | 实例启动或收到变更通知后,将当前需服务的 JSON **反序列化并预热**为内存对象(或编译为匹配结构),下单/预估路径**禁止**主链路同步穿透远程 DB。 | | 下单与预估 | `/order/parcel/create` 与「我要查价」类预估接口**共用**同一套选版本与匹配逻辑;预估不落库,但响应中建议返回 `rulesVersion` / `priceVersion` 便于与正式单对齐展示。 | | 降级 | Redis/内存未命中时,允许回退查询 **`t_sales_product_publish_record`**(按 `bindTime` 取当时生效的 rules/price 各一条,读 `Content`),命中后**异步回写 Redis**;并打点告警。 | #### 4.3.3 与运营侧发布的衔接 - 运营在 Admin 完成卖价、排货规则等配置后,通过 **发布** 生成新版本(参见渠道模块 `/channel/sales-product/publish` 等);发布成功后由**配置同步任务**将结构化结果写入 Redis 并通知计费服务刷新内存。 - 「设为生效」与版本号 `version`、生效时间 `effectiveStartAt`(或等价字段)应对齐,以便 bindTime 维度解析「当前应绑哪一版」。 #### 4.3.4 版本确认时机 vs 费用计算时机(件重尺阶段) > **版本(契约)**:在**下单/预报**流程中(如 `/order/parcel/create` 绑定快照时)即确定 `SalesProductRulesVersion`、`SalesProductPriceVersion`(及渠道侧与应付相关的版本,若有)。后续只要未发生「改单重算」等显式流程,**不得**因后台发布新版本而改写订单已绑定版本。 > **金额(执行)**:卖价/买价主表 **已移除 `ChargingTime`(收费时机)**;应收在 **`t_order_weight` 对应行计重完成** 后触发,**全部启用价格项**参与;`t_order_fee.ChargingStage` 记录本次重量阶段。 | 件重尺 `Type` | 典型触发节点 | 应收(`Type=receivable`) | 应付(`Type=payable`,设计) | | --- | --- | --- | --- | | `forecast` | 下单预报成功 | **§4.3.4.1**:全部启用卖价项;`ChargingStage=forecast`;**不排货、不写应付** | | `receive` | 仓库收货 `/operation/inbound/create` | **§4.3.4.1**:全部启用卖价项;`ChargingStage=inbound` | **§4.3.4.2**:**TryConfirm** 排货 + 渠道买价(失败 → `pendingAllocation`,不抛错);`ChargingStage=inbound`;重量取 **`receive`** | | `ship` | 仓库出库 §6.3.2.1 | **§4.3.4.1**(**未实现**) | **§4.3.4.2**(**未实现**) | **规则小结**:先锁版本;按件重尺阶段触发算价;每次对 **全部启用** 价格项算价;后阶段仍读订单绑定快照。 ##### 4.3.4.1 应收费用重算(`Type=receivable`) 1. **筛选**:`status=1` 的全部卖价项。 2. **重量**:按触发节点取 `forecast` / `receive` / `ship` 行计费重。 3. **输出**:upsert `t_order_fee`,幂等键 `OrderId + CostItem + ChargingStage`。 4. **已实现**:`/order/parcel/create`、`ApplyReceiveBillingByWaybillAsync`。 ##### 4.3.4.2 应付费用重算(`Type=payable`) 1. **排货选渠**:读取订单绑定 **规则快照** 内 `Providers` 与 `Scheme`;按 **客户范围**(`CustomerIds` 空=全部)过滤候选服务商;对每个候选 **服务渠道** 试算买价总额,再按方案选型: - **`costLowest`**:应付总额最低; - **`timeFastest`**:`MinTime` 最小; - **`overallBest`**:`Weight` 最大。 选中后回写 **`t_order.ChannelCode`**,并自选中 **`Providers`** 项固化下游对接字段:**`DownstreamSupplierCode`**、**`DownstreamAppKey`**、**`DownstreamAppId`**、**`ChannelForecastCode`**(实现:`ConfirmServiceChannelAndPayableAsync` → **`AllocationProviderSnapshotEnricher.ApplyDownstreamFieldsToOrder`**)。 2. **筛选**:`status=1` 的全部买价项(优先读渠道买价发布快照,无则 Live 组装)。 3. **重量**:预报阶段取 **`downstream`** 行计费重;入库阶段取 **`receive`** 行计费重(收货复量后重新排货)。 4. **输出**:upsert `t_order_fee`,幂等键 `OrderId + CostItem + ChargingStage`;`PriceNo` 写渠道买价版本。 5. **调用方**:`/order/parcel/create`(预报)、`ApplyReceiveBillingByWaybillAsync`(入库)、`/order/fee/recalculate-payable`。 6. **下游运行时读取**:轨迹 **UpsertSmallBag** 的 **`ApiCode`** 优先 **`t_order.ChannelForecastCode`**,缺失时回退 live 查 **`t_service_channel.ForecastCode`**;itfx 下游预报(前端 **`createOrderOne`**)优先读订单 **`DownstreamSupplierCode` / `DownstreamAppKey` / `DownstreamAppId`**,**AppSecret** 仍按 **`DownstreamAppId`** 调 itfx App API。 #### 4.3.5 当前实现:快照解析与算价(oms0414-back) > 实现位置:`OrderService.ParcelBilling.cs` → `ResolveSnapshotBindingAsync`;`ParcelBillingCalculator.cs`。 **快照绑定(`ResolveSnapshotBindingAsync`)** 1. `bindTime = DateTime.Now`。 2. 分别查询 **`t_sales_product_publish_record`**:`OperatorType = 'rules' | 'price'`,`PublishTime <= bindTime`,按发布时间降序取生效记录。 3. **有发布记录**:反序列化 `Content` 为规则/价格快照 DTO;价格快照 **`Prices` 为空** → 失败 **「未命中价格快照版本(内存/Redis/DB)」**。 4. **无发布记录(Live 降级)**: - **规则**:从 `t_sales_product` + `t_kpi_area_info` / `t_weight_rule_info` / `t_weight_rounding_info` 实时组装;若产品 **`WeightRuleId` 为空** → 失败 **「未命中规则快照版本(内存/Redis/DB)」**。 - **价格**:从 `t_sales_product_price`(及 factor、detail)实时组装;**无任何价格行** → 同上价格失败。 - 版本字段落库:**`LIVE-{salesProductId}`**;生效时间取 `bindTime`。 5. **Redis / 进程内存**:当前代码路径 **未接入**;错误文案保留「内存/Redis/DB」字样以兼容后续接入。 **分区 / 黑名单 / 偏远** - 优先读快照内嵌的 **`BlacklistAreaInfos` / `RemoteAreaInfos` / `PriceAreaInfos`**;缺失时回退 **`t_kpi_area_info`**(按产品或快照上的 `*AreaId`)。 - 黑名单命中 → **不落库**,抛 **「收件地址命中黑名单区域」**。 - 偏远命中 → 更新 **`t_order.IsRemote = 1`**,不拦截。 **计重与进位(`ApplyChargeableWeightAsync`)** - `effectiveWeightRuleId` / `effectiveWeightRoundingId`:优先 **`t_sales_product`**,其次规则快照上的 ID 字段。 - 明细:优先快照 **`WeightRuleInfos` / `WeightRoundingInfos`**,否则查 **`t_weight_rule_info` / `t_weight_rounding_info`**。 - 算法:`ParcelBillingCalculator.CalculateRawBillableWeight` → `ApplyRounding`(`ceil(raw/unit)×unit`,3 位小数)。 - 预报成功后 **四行** `t_order_weight`(`forecast/receive/ship/downstream`)**同步相同**件重尺与计费重。 **卖价算价** | 场景 | 方法 | 说明 | | --- | --- | --- | | 小包创建 / 查价 | `CalculateReceivableFees` / `CalculateQuoteReceivableFees` | 全部启用卖价项;创建写 `ChargingStage=forecast`(**仅应收**) | | 入库收货 | `ApplyReceiveBillingByWaybillAsync` | 仅覆盖 **`Type=receive`** 并重算应收;**TryConfirm** 排货/应付(`ChargingStage=inbound`);成功 → **`pendingPayment`**,失败 → **`pendingAllocation`** + `AllocationPendingReason`;写入 **`ReceiveTime`** | | 出库发货 | `ApplyShipBillingByWaybillAsync` | 仅覆盖 **`Type=ship`** 并重算;写 `ChargingStage=outbound`;**`OrderStatus=pendingOnline`**,写入 **`ShipTime`** | | 应付 | `TryConfirmServiceChannelAndPayableAsync` / 手动重算 | **入库**或运营重算;成功清 `AllocationPendingReason` 并 → `pendingPayment` | **查价额外逻辑**(`OrderService.ParcelQuote.cs`):遍历 **全部已启用** 销售产品(`shippingMethod` 模糊匹配 `ProductCode` / `ProductName`);失败写入 **`failures[]`**(含 `productId`、`step`、`reason`);成功项带 **`rulesVersion`、`priceVersion`、`isRemote`**。 ### 4.4 核心接口设计 > **网关路径**:生产环境对外为 **`/admin/order/*`**(例:`POST https://api.nanafa.com/admin/order/list`);下文契约路径以 **`/order/*`** 表示服务内路由,与前端 `orderUrl('/order/...')` 拼接规则一致。 #### 4.4.0 客户数据隔离(强制) **适用范围**:**§4.4.1 我的订单**、**§4.4.2 我要查价**、**§4.4.3 我要下单** 中列出的全部接口,以及同模块后续增补的订单相关接口。 **会话客户上下文** 1. 从 JWT 解析 **`mapId`**、**`loginType`**(见 **§3.3.2**)。 2. 仅 **`loginType=1`(客户登录)** 允许访问本组接口;否则返回 **401/403**。 3. 查询 `t_customer`:`UserId = mapId AND IsDeleted = 0`,得到 **`effectiveCustomerId = t_customer.Id`**。 4. 若 **`ParentCustomerId` 非空**(子客户):按 **§1.4** 约定,订单归属使用 **主客户 `t_customer.Id`** 或 **子客户自身 `Id`** 之一;**全模块、全接口统一**,实现与测试须与选定策略一致(推荐:**子客户订单落在主客户 `CustomerId` 下**,子账号仅能通过 RBAC 子客户权限访问主客户数据)。 **隔离规则(增删改查)** | 操作类型 | 接口示例 | 强制规则 | | --- | --- | --- | | **查 · 列表** | `POST /order/list`、`GET /order/batch/jobs`、`GET /order/template/list` | 查询条件 **必须包含** `CustomerId = effectiveCustomerId`;**不得**返回其它客户数据 | | **查 · 详情** | `GET /order/detail`、`GET /order/batch/jobs/{id}` | 加载实体后校验 `CustomerId`;不匹配返回业务错误「订单不存在」/「批量任务不存在」或 **403**(**禁止**返回他客数据) | | **增** | `POST /order/parcel/create`、`POST /order/parcel/batch-create`、`POST /order/upload` | 落库 `CustomerId` **仅**取 `effectiveCustomerId`;请求体 `customerId` 若存在须 **相等**,否则拒绝 | | **改** | `POST /order/pay`、`POST /order/pre-declare`、`POST /order/intercept`、`POST /order/cancel-intercept`、`POST /order/fee/batch-recalculate` | 按 `id` / `orderIds` 加载订单后 **逐单**校验 `CustomerId`;不属于当前客户 **不得**变更状态或费用 | | **删** | `POST /order/delete` | 同上,仅允许删除 **`CustomerId = effectiveCustomerId`** 的订单 | | **其它写** | `POST /order/print`、`POST /order/export`、`GET/POST /order/files*` | 打印/导出/附件前校验归属;导出筛选范围 **限定**当前客户;客户附件仅可读 systemLabel | **批量与关联资源** - **`POST /order/fee/batch-recalculate`**:`orderIds` 中每一单均须归属 `effectiveCustomerId`;建议 **部分失败** 策略(`failures` 记录越权单,错误码如 `ORDER_FORBIDDEN`),与 **§4.4.1.10** 一致。 - **`t_order_template`**(自定义模板):`CustomerId` 隔离;标准模板(`Type=standard`)全员可见,**不可**通过模板 Id 间接操作他客订单。 - **`t_order_batch_job`**:列表/详情 **仅**返回 `CustomerId = effectiveCustomerId` 的任务;子项创建订单时 **继承**同一 `CustomerId`。 **查价(§4.4.2)** - `POST /order/price-check` 不读写 `t_order`,但仍须 **已登录客户**;算价所用产品/仓库权限须与当前客户可用范围一致(若产品按客户组/客户 Id 限制,按配置过滤);**禁止**借查价接口探测他客专属价目。 **错误码建议** | 场景 | 建议 | | --- | --- | | 未登录 / Token 无效 | 401 | | 非客户身份调用 | 403 | | 订单 Id 存在但 `CustomerId` 不匹配 | 404 或 403(文案统一「订单不存在」,避免信息泄露) | | 请求体 `customerId` 与会话不一致 | 业务错误 **「客户ID非法」**(HTTP 403 亦可) | **实现检查清单(后端,2026-06-24 已落地)** 1. 封装 **`ResolveSessionCustomerAsync` / `ResolveListCustomerIdAsync`**(`OmsSessionHelper`),创建类接口 **先**解析 `effectiveCustomerId` 再校验其它字段;列表/导出/任务列表 **强制** `WHERE CustomerId = effectiveCustomerId`(**禁止**仅用 `CreateUserId` 过滤)。 2. 列表类 Repository 查询 **统一注入** `CustomerId` 条件,**禁止**仅靠前端传参过滤。 3. **`GetOrderOrThrowAsync(id)`** 在存在性校验后增加 **`order.CustomerId == effectiveCustomerId`**,不匹配返回 **「订单不存在」**。 4. 创建类接口(`parcel/create`、`batch-create`、`upload`):**未传** `customerId` 时由会话补全;**已传**则须与会话一致;落库 **仅**写入 `effectiveCustomerId`。 5. 单测/集成测覆盖:**客户 A 的 Token 无法 list/detail/改/删 客户 B 的订单**。 #### 4.4.1 我的订单 | 接口名称 | 请求方式 | 请求路径 | 说明 | | --- | --- | --- | --- | | 分页查询订单列表 | POST | /order/list | 支持 status、keyword、createTimeStart/End、**orderSource** 筛选;**强制按会话 `CustomerId` 隔离**(§4.4.0) | | 订单详情 | GET | /order/detail | 根据订单ID获取详情;**须校验订单归属当前客户** | | 支付 | POST | /order/pay | 订单支付;**须校验订单归属** | | 预报 | POST | /order/pre-declare | 订单预报;**须校验订单归属** | | 拦截 | POST | /order/intercept | 订单拦截;**须校验订单归属** | | 取消拦截 | POST | /order/cancel-intercept | 取消订单拦截;**须校验订单归属** | | 删除 | POST | /order/delete | 删除订单;**须校验订单归属** | | 打印 | POST | /order/print | 打印订单;优先返回 **`t_order_file.systemLabel`**,缺失则补生成 PDF;**须校验订单归属** | | 订单附件列表 | GET | /order/files | 按类型分组;客户仅返回 **systemLabel**;**须校验订单归属** | | 上传订单附件 | POST | /order/files/upload | multipart;客户账号 **禁止**;**须校验订单归属** | | 删除订单附件 | POST | /order/files/delete | 客户账号 **禁止**;**须校验订单归属** | | 导出 | POST | /order/export | 导出订单;**仅导出当前客户订单** | | 批量重算费用 | POST | /order/fee/batch-recalculate | 入参订单 Id 列表 + `scopes`;**逐单校验 `CustomerId`**(§4.4.0) | | 创建轨迹订单 | POST | /order/track-upsert-small-bag | 手动同步调用轨迹 **UpsertSmallBag**(联调/补推);**须校验订单归属** | | 创建问题件 | POST | /order/mark-problematic | 批量将订单标记为 **问题件**;消费问题模板(§5.2.21);**须校验订单归属** | | 创建理赔件 | POST | /order/mark-claimed | 批量将订单标记为 **理赔件**;**须校验订单归属** | | 更新处理子状态 | POST | /order/handle-status/update | 问题件/理赔件 **待处理→已处理**;**须校验订单归属** | | 预报下游订单 | POST | /order/downstream-forward-create-one | OMS 组装 createOrderOne 并转发 itfx;成功回写服务商单号/跟踪号,下游面单入 **`t_order_file`**(§4.4.1.12) | ##### 4.4.1.1 分页查询订单列表(POST /order/list) ###### 1 接口请求示例 **请求 /order/list**(POST,分页参数放 Body) ```json { "page": 1, // 页码,从1开始 "pageSize": 20, // 每页条数 "status": "all|draft|predeclared|received|...", // 订单状态筛选 "handleStatus": "pending|processed", // 处理子状态;仅 status=problematic|claimed 时生效 "orderSource": "all|manual|pickup|upload|batch|platform", // 订单来源;默认 all 不按来源过滤;揽收菜单可固定传 pickup "keyword": "", // 关键词(运单号/客户单号/跟踪号) "createTimeStart": "", // 创建时间起 "createTimeEnd": "" // 创建时间止 } ``` **响应**(用于 order/myOrders 列表及订单详情抽屉,字段与 parcel/create 对应;**注意**:`sender.countryCode` / `receiver.countryCode` 响应中为 **`t_country.Id`(int)**,与创建请求体的 ISO 二字码不同;列表/详情宜回显 **`orderSource`、`pickupTime`、`customerSenderAddressId`**) ```json { "code": 0, "msg": "success", "data": { "items": [ { "id": 1, "clientNo": "CUST...", "trackNo": "TRK...", "serviceNo": "LP...", "productCode": "US_AIR", "channelCode": "CH001", "downstreamSupplierCode": "SP001", "downstreamAppKey": "AKxxxx", "downstreamAppId": 12, "channelForecastCode": "FORECAST_01", "cargoAttribute": "general", "exportCustomsMode": "9610Simple", "warehouseId": 1, "warehouse": "深圳仓", "customerId": 1, "isCod": false, "codCurrency": "", "codSum": "", "orderStatus": "待支付", "status": "pendingPayment", "receiveTime": "", "shipTime": "", "payTime": "", "signTime": "", "isIntercept": false, "isRemote": false, "createTime": "2024-01-01 12:00:00", "createBy": "", "updateTime": "", "updateBy": "", "sender": { "senderName": "发件人", "company": "示例公司", "phone": "13800138000", "telePhone": "", "email": "sender@example.com", "countryCode": 1, "province": "广东省", "city": "深圳市", "zipCode": "518000", "streetAddress1": "示例地址1", "streetAddress2": "", "streetAddress3": "", "houseNumber": "" }, "receiver": { "recipientsName": "收件人1", "companyName": "", "phone": "13800138000", "telePhone": "", "email": "", "countryCode": 1, "province": "California", "city": "Los Angeles", "streetNumber": "", "streetAddress1": "123 Main St", "streetAddress2": "", "streetAddress3": "", "zipCode": "90001", "taxType": 0, "taxNo": "", "certificateType": 0, "certificateNo": "", "eori": "", "vat": "" }, "items": [ { "cnName": "示例商品", "enName": "Sample Product", "sku": "SKU001", "unit": "件", "unitPrice": 10, "unitWeight": 0.5, "quantity": 1, "amount": 10, "weight": 0.5, "hsCode": "1234567890", "skuImage": "", "category": "普货", "brand": "示例品牌", "brandType": "自有", "isMagnetic": false, "distribution": "", "material": "塑料", "enMaterial": "Plastic", "purpose": "日用", "attributes": "", "originCountry": "中国", "salesLink": "", "model": "", "specifications": "", "comment": "" } ], "weightList": [ { "type": "forecast", "length": "20", "width": "15", "height": "10", "actualWeight": "2.50", "volumetricWeight": "2.00", "chargeableWeight": "2.50", "cubic": "0.003" } ] } ], "total": 100 } } ``` ###### 2 接口业务逻辑 ###### 核心流程 1. 解析会话 **`effectiveCustomerId`**(§4.4.0)。 2. 构造分页查询条件时 **固定追加** `t_order.CustomerId = effectiveCustomerId`(及 `IsDeleted = 0`、状态/关键词/时间等前端筛选)。 3. **忽略**请求体中任意用于切换客户的 `customerId` 字段(若存在)。 ###### 状态与失败处理 - 未登录或非客户身份:401/403。 ###### 校验与异常 - 不得返回其它客户订单;不得通过 `keyword` 等条件绕过 `CustomerId` 过滤。 ###### 3 当前实现(oms0414-back) - `OrderService.GetPaginatedListAsync`:`ResolveListCustomerIdAsync` — **客户**强制 `CustomerId` 过滤;**管理员** `customerId=null` **不过滤**。 - 关键词匹配 **`ServiceNo`、`ClientNo`、`TrackNo`**;`status=all` 或不传表示不过滤状态。 - 排序:`UpdateTime desc, Id desc`;分页默认见 §1.5.1。 ##### 4.4.1.2 订单详情(GET /order/detail) ###### 1 接口请求示例 **请求 /order/detail**(GET,Query 参数) ``` ?id=1 ``` **响应** ```json { "code": 0, "msg": "success", "data": { "id": 1, "clientNo": "CUST...", "trackNo": "TRK...", "serviceNo": "LP...", "productCode": "US_AIR", "channelCode": "CH001", "cargoAttribute": "general", "exportCustomsMode": "9610Simple", "warehouseId": 1, "warehouse": "深圳仓", "customerId": 1, "sender": {}, "receiver": {}, "items": [], "weightList": [] } } ``` ###### 2 接口业务逻辑 ###### 核心流程 1. 按 `id` 查询订单;不存在或 `IsDeleted=1` 时返回「订单不存在」。 2. 校验 `t_order.CustomerId == effectiveCustomerId`(§4.4.0);不匹配时 **同样**返回「订单不存在」(禁止泄露他客订单)。 3. 映射详情并返回。 ###### 校验与异常 - 禁止仅凭 `id` 跨客户读取。 ##### 4.4.1.3 支付(POST /order/pay) ###### 1 接口请求示例 **请求 /order/pay**(POST) ```json { "id": 1, "requestId": "" } ``` > requestId 幂等,可选。 **响应** ```json { "code": 0, "msg": "success", "data": null } ``` ###### 2 接口业务逻辑 ###### 核心流程 1. `GetOrderOrThrowAsync(id)`:订单存在且客户隔离通过。 2. **状态校验**:若已 **`pendingShip` 且已有 `PayTime`** → **「订单已支付」**;若 **`OrderStatus` 非 `pendingPayment` 且非 `received`**(`received` 为入库后历史数据兼容)→ **「当前订单状态不可支付」**。 3. **应付金额**:汇总该订单 **`t_order_fee`** 中 **`Type=receivable`** 的 **`Amount`**;**≤ 0** → **「订单应收金额无效,无法支付」**。 4. **扣款**:加载 **`order.CustomerId`** 对应客户;调用 **`CustomerBalanceMutator.DebitOrderPayAsync`**: - 校验 **`Balance ≥ payAmount`**,不足 → **「账户余额不足」**; - **`t_customer.Balance -= payAmount`**; - 插入 **`t_customer_account_flow`**:`BizType=orderPay`,`BizId=order.Id`,`BizNo=ServiceNo`,**`Amount` 为负数**,记录 **`BalanceBefore` / `BalanceAfter`**,流水号 **`FLOW{yyyyMMddHHmmss}{6位随机}`**。 5. 更新订单:**`PayTime = DateTime.Now`**,**`OrderStatus = pendingShip`**。 > 步骤 4~5 在同一 **`ApplicationService` 工作单元**内提交;任一步失败整体回滚。 ###### 校验与异常 | 场景 | 提示 | | --- | --- | | 订单不存在 / 越权 | **「订单不存在」** | | 已支付 | **「订单已支付」** | | 状态非待支付 | **「当前订单状态不可支付」** | | 无客户 | **「订单未关联客户,无法支付」** | | 应收无效 | **「订单应收金额无效,无法支付」** | | 客户不存在 | **「客户不存在」** | | 余额不足 | **「账户余额不足」** | ###### 3 当前实现(oms0414-back) - 实现:`OrderService.PayAsync`(`OrderService.cs`)+ **`CustomerBalanceMutator`**(`CustomerBalanceMutator.cs`)。 - 与 §2.3 主流程 B「支付扣减余额」**已对齐**;**不修改** `t_order_fee`(费用在预报/入库阶段已写入)。 ##### 4.4.1.4 预报(POST /order/pre-declare) ###### 1 接口请求示例 **请求 /order/pre-declare**(POST) ```json { "id": 1, "requestId": "" } ``` **响应** ```json { "code": 0, "msg": "success", "data": null } ``` ###### 2 接口业务逻辑 ###### 核心流程 1. `GetOrderOrThrowAsync(id)`。 2. 直接将 **`OrderStatus` 置为 `predeclared`** 并保存。 3. **不**重跑计重/算价/黑名单;**不**校验是否已有预报费用。 ###### 3 当前实现(oms0414-back) - 实现:`OrderService.PreDeclareAsync` — 纯状态更新,供运营手工补状态。 ##### 4.4.1.5 拦截(POST /order/intercept) ###### 1 接口请求示例 **请求 /order/intercept**(POST) ```json { "id": 1, "requestId": "" } ``` **响应** ```json { "code": 0, "msg": "success", "data": null } ``` ###### 2 接口业务逻辑 ###### 核心流程 1. `GetOrderOrThrowAsync(id)` → 设置 **`IsIntercept = 1`** 并保存。 ###### 3 当前实现(oms0414-back) - `OrderService.InterceptAsync`。 ##### 4.4.1.6 取消拦截(POST /order/cancel-intercept) ###### 1 接口请求示例 **请求 /order/cancel-intercept**(POST) ```json { "id": 1, "requestId": "" } ``` **响应** ```json { "code": 0, "msg": "success", "data": null } ``` ###### 2 接口业务逻辑 ###### 核心流程 1. `GetOrderOrThrowAsync(id)` → 设置 **`IsIntercept = 0`** 并保存。 ###### 3 当前实现(oms0414-back) - `OrderService.CancelInterceptAsync`。 ##### 4.4.1.7 删除(POST /order/delete) ###### 1 接口请求示例 **请求 /order/delete**(POST) ```json { "id": 1, "requestId": "" } ``` **响应** ```json { "code": 0, "msg": "success", "data": null } ``` ###### 2 接口业务逻辑 ###### 核心流程 1. `GetOrderOrThrowAsync(id)`。 2. **`OrderStatus = deleted`**,调用仓储 **软删除**(`IsDeleted=1`)。 ###### 3 当前实现(oms0414-back) - `OrderService.DeleteAsync` — 先更新状态再 `DeleteAsync` 软删。 ##### 4.4.1.8 打印(POST /order/print) ###### 1 接口请求示例 **请求 /order/print**(POST) ```json { "id": 1, "requestId": "" } ``` **响应** ```json { "code": 0, "msg": "success", "data": { "printUrl": "/uploads/prints/orders/....pdf", "labelGenerateStatus": "Ready", "labelGenerateError": null } } ``` > `labelGenerateStatus` / `labelGenerateError` 为 **本次响应瞬时字段**(Ready / Failed),**不落库**。 ###### 2 接口业务逻辑 ###### 核心流程 1. `GetOrderOrThrowAsync(id)` → 映射订单详情。 2. 若已存在 **`t_order_file`(`FileType=systemLabel`)** 有效记录,直接返回其 **`FileUrl`** 作为 `printUrl`。 3. 否则调用 **`LabelPdfGenerationService.GenerateAndPersistAsync`**:按模板渲染 PDF → 落盘 `/uploads/prints/orders/` → **替换写入** `systemLabel`;成功返回 PDF 相对路径。 4. PDF 仍失败时:回落 `CreateOrderPrintFileAsync` 生成简易 **HTML** stub,避免彻底不可打;响应带失败原因。 ###### 3 当前实现(oms0414-back) - `OrderService.PrintAsync` + `OrderFileManager` + `LabelPdfGenerationService`;批量打印走 `PrintForBatchAsync` / 批量任务 `action=print`。 ##### 4.4.1.9 导出(POST /order/export) ###### 1 接口请求示例 **请求 /order/export**(POST) ```json { "status": "all|draft|...", "keyword": "", "createTimeStart": "", "createTimeEnd": "" } ``` **响应** ```json { "code": 0, "msg": "success", "data": { "downloadUrl": "", "expireAt": "" } } ``` ###### 2 接口业务逻辑 ###### 核心流程 1. 客户登录时 **`ResolveListCustomerIdAsync`** 限定导出范围。 2. 按 `status`、`keyword`、`createTimeStart/End` 查询订单(与 list 同 predicate)。 3. MiniExcel 写入 `wwwroot/uploads/exports/orders/orders-{timestamp}.xlsx`。 4. 清理 **24 小时**前旧文件;返回 **`downloadUrl`**、`expireAt`(当前时间 + 24h)。 ###### 3 当前实现(oms0414-back) - `OrderService.ExportAsync`。 ##### 4.4.1.10 批量重算费用(POST /order/fee/batch-recalculate) > **实现状态:未落地**(oms0414-back 无对应 `ApplicationService` 方法)。以下为设计目标,供后续迭代实现;**§10.6** 标记为「设计中」。 用于「我的订单」运营/对账场景:按订单 Id 列表 **批量** 重算并回写 **`t_order_fee`**,算法拆分为 **应收** 与 **应付** 两段(**§4.3.4.1**、**§4.3.4.2**),与预报、入库、出库、下游链路 **共用同一套计算单元**。 ###### 1 接口请求示例 **请求 /order/fee/batch-recalculate**(POST) ```json { "orderIds": [1001, 1002, 1003], "scopes": ["receivable", "payable"] } ``` > 参数说明:`orderIds` **必填**,非空数组,元素为 **`t_order.Id`**。`scopes` **必填**,取值为 `receivable`、`payable` 的子集(可只传其一或两者都传)。**`receivable`**:对列表内订单执行 **§4.3.4.1**。**`payable`**:对列表内订单执行 **§4.3.4.2**(依赖 **`downstream`** 行已存在且有效)。 **响应** ```json { "code": 0, "msg": "success", "data": { "total": 3, "successCount": 2, "failCount": 1, "failures": [{ "orderId": 1003, "reason": "缺少 t_order_weight.Type=downstream" }] } } ``` ###### 2 接口业务逻辑 ###### 核心流程 1. **校验**:`orderIds` 去重、长度上限(如 ≤500,可配置)、订单存在且 **`IsDeleted=0`**;**逐单校验 `t_order.CustomerId = effectiveCustomerId`(§4.4.0)**,越权单记入 `failures` 或整批拒绝;调用方须已登录且为 **`loginType=1` 客户**。 2. **当 `scopes` 含 `receivable`(重算应收)** - 逐单加载:绑定 **`SalesProductPriceVersion`** 卖价快照、`t_order_weight`、`t_order_fee` 既有应收行(若策略为全量重算可先逻辑清理再写)。 - 按 **§4.3.4.1**:全部启用卖价项;重量取对应 **`t_order_weight` 行** `ChargeableWeight`,**upsert** 应收。 3. **当 `scopes` 含 `payable`(重算应付)** - 逐单校验 **`t_order_weight` 存在 `Type=downstream`** 且可用于买价匹配;加载绑定 **买价** 快照。 - 按 **§4.3.4.2**:**仅 `downstream`** 行参与重量/件重尺因子,重算并 **upsert** `Type=payable`。 4. **事务**:建议 **按订单** 单事务提交,避免一批中一单失败影响其他单;`failures` 逐单记录原因。 5. **与履约链路的关系**:**预报成功、入库完成、出库完成** 应在各自流程内调用 **§4.3.4.1**(重量分别取 **`forecast` / `receive` / `ship` 行**);**`downstream` 行更新后**(下游回传或独立流程)再调用 **§4.3.4.2**。本接口为 **显式批量补算**,不改变订单已绑定版本契约。 ###### 校验与异常 - `orderIds` 为空、`scopes` 为空或不合法、订单无绑定卖价/买价版本、缺 `downstream`(在请求 `payable` 时)等返回业务错误;部分失败时仍返回 `200` + `data.failures`(或与全局错误码策略一致)。 ##### 4.4.1.11 创建轨迹订单(POST /order/track-upsert-small-bag) > **实现状态:已落地**(oms0414-back `OrderService.TrackUpsertSmallBagAsync`;前端「我的订单」批量按钮)。 手动触发小包轨迹 **`POST {TrackBaseUrl}/OrderV2/UpsertSmallBag`**(同步调用,返回完整请求/响应便于联调)。与预报成功后的 **异步** 轨迹推送(`SmallBagTrackOrderNotifier`)共用 **`BuildSmallBagTrackUpsertRequestAsync`** 组装逻辑。 ###### 1 接口请求示例 **请求 /order/track-upsert-small-bag**(POST) ```json { "id": 1001 } ``` **响应**(`SmallBagTrackUpsertResult`) ```json { "code": 0, "msg": "success", "data": { "skipped": false, "skipReason": null, "requestUrl": "http://api.nanafa.com/track/OrderV2/UpsertSmallBag", "requestBody": { "appKey": "...", "orderNo": "LP...", "apiCode": "FORECAST_01" }, "responseStatusCode": 200, "responseBody": "..." } } ``` ###### 2 接口业务逻辑 1. 校验订单存在、未删除、**归属当前客户**(§4.4.0);**`ServiceNo`** 非空。 2. 组装 **`SmallBagTrackUpsertRequest`**:客户编码、运单号、跟踪号、渠道、产品、收件人国家/省/邮编等。 3. **`ApiCode`**:优先 **`t_order.ChannelForecastCode`**;为空且 **`ChannelCode`** 有值时,回退查 **`t_service_channel.ForecastCode`**。 4. 轨迹凭证读 **`t_system_other_config`**(`ConfigCode=track_service`),缺省回退 **`Oms:TrackService`**(§8.2.7);未启用或参数不全时返回 **`skipped=true`** 及原因。 5. 联调入口:**`POST /order/debug/track-upsert-small-bag`**(Swagger,可自定义测试字段)。 ##### 4.4.1.12 预报下游订单(POST /order/downstream-forward-create-one) > **实现状态:已落地**(OMS 后端转发 itfx;亦可批量任务 `downstream-forward-create-one`)。 运营在「我的订单」勾选订单后,由 **OMS** 组装 createOrderOne Body 并调用 itfx 下游;凭证优先读订单绑定字段(§4.3.4.2)。 ###### 1 凭证解析优先级 | 顺序 | 来源 | 字段 | | --- | --- | --- | | 1(优先) | **`t_order` 绑定字段**(排货确认时写入) | `downstreamSupplierCode`、`downstreamAppKey`、`downstreamAppId` | | 2(回退) | 渠道树 live 查询 | `ChannelCode` → `t_service_channel` → 分组 → itfx App / ServiceProvider | **AppSecret** 不落订单、不落规则快照;无论哪条路径,均按 **`downstreamAppId`(或分组 `AppId`)** 调 itfx **`GET /App/GetInfo`** 读取。 ###### 2 成功回写 | 下游 Data 字段 | 落库位置 | | --- | --- | | `serviceNo` | **`t_order.DownstreamServiceNo`**(OMS 自有运单号 `ServiceNo` 不变) | | `trackNo` | **`t_order.TrackNo`**(空值不覆盖已有) | | `labelUrl` | **`t_order_file`**,`FileType=downstreamLabel`,`Source=downstream`(**不写**订单主表) | ###### 3 与规则快照的关系 - 规则发布时 **`Providers[]`** 已 enrich 渠道/itfx 元数据(§5.3.0.2);排货确认渠道时固化至 **`t_order`**。 - **历史订单**(确认渠道时尚未写入四字段)仍走凭证解析 **回退路径**;重新发布销售产品规则后,**新排货确认** 的单据将携带完整绑定字段。 ##### 4.4.1.13 创建问题件(POST /order/mark-problematic) > **实现状态:设计中**(oms0414-back `OrderService` 待新增;前端 `order/myOrders` 批量弹窗)。 ###### 1 业务目标 运营在「我的订单」勾选一条或多条订单,填写问题类型与话术(可引用 **问题模板** §5.2.21),将订单 **`OrderStatus` 置为 `problematic`**,**`HandleStatus` 置为 `pending`**,并写入问题快照字段;供 **问题件** Tab 与 **待处理/已处理** 子 Tab 筛选展示。 ###### 2 状态准入规则 | 规则 | 说明 | | --- | --- | | **禁止源状态** | `draft`、`deleted`、`problematic`、`claimed` | | **允许源状态** | `predeclared`、`received`、`pendingPayment`、`pendingShip`、`pendingOnline`、`shipping`、`deliverySuccess`、`deliveryFailed` | | **互斥** | 已为问题件/理赔件/已删除的订单 **不可重复创建**;一期 **不支持** 问题件↔理赔件互转 | | **拦截** | `IsIntercept=1` **不阻止** 标记问题件(与拦截标记独立) | 变更时写入 **`PreviousOrderStatus`** = 变更前 `OrderStatus`,便于二期回滚或报表。 ###### 3 请求示例 ```json { "orderIds": [1001, 1002], "issueTemplateId": 12, "issueType": "address", "title": "收件地址无法派送", "content": "运单 {serviceNo} 收件地址不完整,请补充门牌号与邮编。", "remark": "客户已电话确认" } ``` | 字段 | 必填 | 说明 | | --- | --- | --- | | `orderIds` | 是 | 订单 Id 列表;**单次最多 200 条** | | `issueTemplateId` | 否 | 引用 `t_issue_template.Id`;传入时 **覆盖** `title`/`content`/`issueType`,并快照 `NotifyMethod` 至事件表 | | `issueType` | 条件 | 未传 `issueTemplateId` 时 **必填**;枚举见 **§10.3.6** | | `title` | 条件 | 未传模板时 **必填**;传模板时可覆盖模板标题 | | `content` | 条件 | 未传模板时 **必填** | | `remark` | 否 | 人工备注,写入 `IssueRemark` 与事件表 | **模板解析**:按订单归属客户 **`OrganizationId`**(经 `t_customer` 关联)调用 **`GET /channel/issue-template/options?organizationId=&issueType=`** 同源逻辑;`issueTemplateId` 须为 **启用** 且未删除的模板。 **占位符(一期)**:`content` **原样落库**;二期在发通知前渲染 `{serviceNo}`、`{clientNo}`、`{trackNo}` 等。 ###### 4 响应示例(部分失败) ```json { "code": 0, "msg": "success", "data": { "successCount": 1, "failures": [ { "orderId": 1002, "serviceNo": "LP002", "reason": "仅草稿或已删除订单不可标记问题件" } ] } } ``` ###### 5 落库与副作用 1. **更新 `t_order`**:`OrderStatus=problematic`,`HandleStatus=pending`,问题字段、`IssueTime=now`,`PreviousOrderStatus`。 2. **插入 `t_order_exception_event`**:`EventType=mark_problematic`,快照标题/内容/模板/通知方式。 3. **通知(二期)**:引用问题模板时读取 `NotifyMethod` 并投递(**`inApp` 站内信、`email` 操作人 SMTP 已接入**;`sms/feishu/wechat` 待通道对接);通知失败 **不回滚** 问题件标记。 4. **客户隔离**:逐单 `GetOrderOrThrowAsync`;管理员须具备数据权限(§4.4.0、`ApplyInternalOrderDataScopeAsync`)。 ###### 6 权限 | 权限码 | 说明 | | --- | --- | | `Order.Problematic.Create` | 工具栏「创建问题件」及接口调用 | ##### 4.4.1.14 创建理赔件(POST /order/mark-claimed) > **实现状态:设计中**。 ###### 1 业务目标 运营勾选订单,填写理赔类型、索赔金额与原因,将订单 **`OrderStatus` 置为 `claimed`(理赔件)**,**`HandleStatus` 置为 `pending`**。 ###### 2 状态准入规则 与 **§4.4.1.13** 相同:**禁止** `draft`/`deleted`/`problematic`/`claimed`;**允许**其余在途/完结状态(见上表)。 > 业务上理赔多发生在运输/派送阶段;技术上与问题件共用同一准入集合,由运营自行判断。 ###### 3 请求示例 ```json { "orderIds": [2001], "claimType": "lost", "claimAmount": 128.5, "claimCurrency": "USD", "claimReason": "末端确认丢件,申请运费+货值赔偿", "remark": "" } ``` | 字段 | 必填 | 说明 | | --- | --- | --- | | `orderIds` | 是 | 同上,最多 200 条 | | `claimType` | 是 | 理赔类型,见 **§10.3.7** | | `claimAmount` | 是 | `> 0` | | `claimCurrency` | 是 | `RMB` / `USD` / `EUR` 等 | | `claimReason` | 是 | 最长 512 | | `remark` | 否 | 写入事件表 | ###### 4 响应 与 **§4.4.1.13** 同构:`successCount` + `failures[]`。 ###### 5 落库 1. **`t_order`**:`OrderStatus=claimed`,`HandleStatus=pending`,理赔字段、`ClaimTime=now`,`PreviousOrderStatus`;**清空**问题件专用字段(若曾有问题字段残留)。 2. **`t_order_exception_event`**:`EventType=mark_claimed`。 ###### 6 权限 | 权限码 | 说明 | | --- | --- | | `Order.Claim.Create` | 工具栏「创建理赔件」 | ##### 4.4.1.15 更新处理子状态(POST /order/handle-status/update) > **实现状态:设计中**(`HandleStatus` 列与列表筛选 **已落地**,见 `migrate_t_order_handle_status.sql`)。 ###### 1 业务目标 在 **问题件 / 理赔件** Tab 下,将选中订单由 **待处理** 标记为 **已处理**(或一期仅支持 `pending → processed`)。 ###### 2 准入 - 当前 `OrderStatus` 必须为 `problematic` 或 `claimed`。 - 当前 `HandleStatus` 为 `pending` 或 `NULL`(视为待处理)。 ###### 3 请求示例 ```json { "orderIds": [1001], "handleStatus": "processed", "remark": "已联系客户并结案" } ``` ###### 4 落库 - 更新 `t_order.HandleStatus`。 - 插入 `t_order_exception_event`:`EventType=handle_processed`,记录 `FromHandleStatus` / `ToHandleStatus`。 ###### 5 权限 | 权限码 | 说明 | | --- | --- | | `Order.HandleStatus.Update` | 子 Tab「标记已处理」 | ##### 4.4.1.16 前端交互(`order/myOrders`) | 场景 | UI | | --- | --- | | 订单详情抽屉 | Tab **附件**(§4.4.1.17):内部可上传各类型;客户门户仅展示系统面单只读 | | 非问题件/理赔件 Tab | 工具栏 **创建问题件**、**创建理赔件**(须勾选行;`hasPermission` 控制) | | 问题件/理赔件 Tab | 子 Tab **待处理 / 已处理**(已落地);工具栏 **标记已处理**(`Order.HandleStatus.Update`) | | 创建问题件弹窗 | `ModalPro` + `FormPro`:问题类型 → 联动 **`issue-template/options`** → 可选模板;标题、内容可编辑;备注 | | 创建理赔件弹窗 | 理赔类型、金额、币种、原因 | | 批量策略 | 优先调用 **批量接口**;失败行 `Message` 汇总展示 `failures` | 列表/详情响应补充字段(实现时):`handleStatus`、`handleStatusName`、`issueType`、`issueTitle`、`claimType`、`claimAmount` 等(与 **§4.2.1** 对齐)。 ##### 4.4.1.17 订单附件(GET/POST /order/files*) > **实现状态:已落地**(`OrderService.OrderFiles` + `OrderFileManager`;前端订单详情抽屉「附件」Tab)。 ###### 1 查询(GET /order/files?orderId=) - 校验订单归属。 - 返回按 `FileType` 分组的附件列表;**客户登录仅含 `systemLabel`**,其它类型键可不返回或为空数组。 ###### 2 上传(POST /order/files/upload,multipart) - 表单:`orderId`、`fileType`、`files`。 - **客户账号禁止**;内部账号校验订单归属后写入。 - `systemLabel` 仅允许 1 个文件(替换旧记录);其它类型合计不超过 20。 ###### 3 删除(POST /order/files/delete) ```json { "id": 1 } ``` - **客户账号禁止**;软删 `t_order_file` 行。 #### 4.4.2 我要查价 | 接口名称 | 请求方式 | 请求路径 | 说明 | | --- | --- | --- | --- | | 查价 | POST | /order/price-check | 按销售产品**最新生效卖价版本**试算运费并返回;**须客户登录**(§4.4.0) | ##### 4.4.2.1 查价(POST /order/price-check) ###### 1 接口请求示例 **请求 /order/price-check**(POST) ```json { "destinationCountry": "US", // 目的国二字码 "shippingMethod": "", // 运输方式 "cargoWeight": "2", // 货物重量(kg) "length": "20", // 长(cm) "width": "15", // 宽(cm) "height": "10", // 高(cm) "cargoType": "普货", // 货物类型 "destinationProvince": "", // 目的省州 "postalCode": "" // 邮编 } ``` **响应** ```json { "code": 0, "msg": "success", "data": { "items": [ { "id": 1, "shippingMethod": "美国空运专线(普货)", "warehouse": "一代深圳仓", "deliveryTime": "3-7", "volumetricWeight": "2", "chargeableWeight": "2", "costFormula": "freight:11 + handling:20", "freight": "11", "otherFees": "20", "totalCost": "31", "maxDeliveryDays": 7, "totalCostNum": 31, "productCode": "test01", "rulesVersion": "RULES-20260601-001", "priceVersion": "PRICE-20260601-001", "isRemote": false } ], "failures": [ { "productId": 2, "productCode": "OTHER", "productName": "其他产品", "step": "zone", "reason": "目的国 US 邮编 00000 未匹配产品价格分区(请检查 PriceAreaId 及分区规则)" } ] } } ``` ###### 2 接口业务逻辑 ###### 核心流程 1. **校验**:`destinationCountry` 必填(ISO 二字码);`cargoWeight` 须 **> 0**;**尺寸可选** — 全空则按实重估算,或长/宽/高三者齐全(部分填写 → 报错)。 2. **绑定时刻**:`bindTime = DateTime.Now`;解析卖价最新生效快照(`ResolveSnapshotBindingAsync`)。 3. **产品范围**:未传 `shippingMethod` 时遍历 **全部已启用** 销售产品;有值则按代码/名称过滤;未找到 → `failures[0].step=product`。 4. **逐产品试算**(按快照明细匹配;**无邮编时跳过黑名单**;分区无匹配时可 **zone 兜底** 取最低价分区,标记 `quoteLevel=estimated`): - **分区**:用 **目的国 + 邮编**(及省州/城市若有)匹配;未命中且无兜底 → `step=zone`。 - **计重**:无尺寸时按实重;有尺寸则体积重参与;失败 → `step=weight`。 - **货物/仓库**:未筛选时展开 **general/electronics/sensitive × 全部仓库** 组合试算。 - **算价**:`CalculateQuoteReceivableFees`;无明细命中 → `step=price`。 5. **结果**:成功写入 `items`(含 **`quoteLevel`、`disclaimers`**);顶层 **`queryHint`** 汇总缺失字段与免责声明;失败写入 `failures`(含 `step` + `reason`),**禁止静默丢弃**。 ###### failures.step 说明 | step | 含义 | | --- | --- | | `product` | 未找到启用产品 | | `snapshot` | 卖价/规则快照解析失败 | | `blacklist` | 命中黑名单 | | `weight` | 计重规则/进位失败 | | `zone` | 国家+邮编未匹配价格分区 | | `price` | 分区已匹配但卖价明细未命中(重量区间/货物属性/无启用项) | ###### 校验与异常 | 场景 | 错误信息 | | --- | --- | | 目的国为空 | 目的国不能为空 | | 重量 ≤ 0 | 货物重量必须大于0 | | 尺寸部分填写 | 尺寸请填写完整的长、宽、高,或全部留空 | | 目的国不存在 | 目的国不存在 | | 全部产品未命中 | `200` + `items: []` + `failures` 逐条原因 | **响应扩展(对客提示)** | 字段 | 说明 | | --- | --- | | `queryHint.quoteLevel` | `exact` / `estimated`(有缺失字段时为估算) | | `queryHint.disclaimers` | 免责声明列表(如未填尺寸、未填邮编) | | `items[].quoteLevel` | 单行报价精度 | | `items[].disclaimers` | 单行补充说明 | ###### 3 当前实现(oms0414-back) - `OrderService.PriceCheckAsync` → `BuildPriceCheckResultAsync`(`OrderService.ParcelQuote.cs`)。 - **`cargoType`** 经 `ParcelBillingCalculator.MapCargoTypeToAttribute` 映射为 `cargoAttribute`(支持中文「普货」等)。 - 客户登录时 **`ResolveCurrentCustomerGroupAsync`** 参与卖价 **客户组** 因子;管理员查价时客户组为 `null`。 - 详见 **§4.3.5**。 #### 4.4.3 我要下单 | 接口名称 | 请求方式 | 请求路径 | 说明 | | --- | --- | --- | --- | | 模板列表 | GET | /order/template/list | 获取下单模板列表;自定义模板按 **`CustomerId` 隔离** | | 下载模板 | GET | /order/template/download | 下载 Excel 模板 | | 上传下单 | POST | /order/upload | multipart/form-data;创建 **`import-row`** 批量任务,返回 **`jobId`** | | 批量任务列表 | GET | /order/batch/jobs | 批量任务分页列表;**仅当前客户** | | 批量任务详情 | GET | /order/batch/jobs/{id} | 任务进度(含 success/fail/pending 计数) | | 批量任务子项 | GET | /order/batch/jobs/{id}/items | 子项明细(含 Excel 行号、客户单号、失败原因) | | 重试失败子项 | POST | /order/batch/jobs/{id}/retry-failed | 重新投递失败子项 | | 取消待处理子项 | POST | /order/batch/jobs/{id}/cancel | 取消 pending 子项 | | 创建小包 | POST | /order/parcel/create | 创建小包订单;**`CustomerId` 与会话一致**(§4.4.0) | | 批量创建小包 | POST | /order/parcel/batch-create | 批量创建;**同上客户隔离** | ##### 4.4.3.1 创建小包(POST /order/parcel/create) ###### 1 接口请求示例 **请求 /order/parcel/create**(POST) > 字段与 t_order、t_order_sender、t_order_recipient、t_order_declaration、t_order_weight 表对应,采用 camelCase。 ```json { "requestId": "string", // 幂等键 t_order.RequestId//可选;重复则直接返回已有订单结果 "clientNo": "string", // 客户单号 t_order.ClientNo//可选 "trackNo": "string", // 跟踪号 t_order.TrackNo//可选 "serviceNo": "string", // 运单号 t_order.ServiceNo//可选;未传时服务端自动生成(IDE 前缀) "productCode": "string", // 产品代码 t_order.ProductCode//必填 "channelCode": "string", // 渠道代码 t_order.ChannelCode "cargoAttribute": "string", // 货物属性 t_order.CargoAttribute:general-普货,electronics-3c&带电,sensitive-敏感货//必填 "exportCustomsMode": "9610Simple", // 出口报关方式 t_order.ExportCustomsMode,见 §4.2.1 ExportCustomsMode 枚举//可选 "warehouseId": 0, // 仓库ID t_order.WarehouseId//必填 "customerId": 0, // 客户ID t_order.CustomerId//可选:客户登录由会话补全;管理员代下单必填 "length": 3, // 长(cm) t_order_weight.Length "width": 3, // 宽(cm) t_order_weight.Width "height": 3, // 高(cm) t_order_weight.Height "actualWeight": 3, // 实重(kg) t_order_weight.ActualWeight//必填 "isCod": false, // 是否COD "codSum": "string", // COD金额 "codCurrency": "string", // COD币种 "declaredCurrency": "string", // 申报币种//必填 "declaredSum": "string", // 申报总金额 "quantity": "string", // 包裹数量//必填 "receiver": { // 收件人 t_order_recipient "recipientsName": "string", // 收件人名称//必填 "companyName": "string", // 收件人公司名称 "phone": "string", // 收件人手机(接口层非必填,前端建议填写) "telePhone": "string", // 收件人电话 "email": "string", // 收件人邮箱 "countryCode": "US", // 收件国家 ISO 3166-1 alpha-2 二字码(对应 t_country.CountryCode)//必填 "province": "string", // 收件人省/州 "city": "string", // 收件人城市 "streetNumber": "string", // 收件人门牌号 "streetAddress1": "string", // 收件人地址1//必填 "streetAddress2": "string", // 收件人地址2 "streetAddress3": "string", // 收件人地址3 "zipCode": "string", // 收件人邮编//必填 "taxType": 0, // 税号类型 "taxNo": "string", // 税号 "certificateType": 0, // 证件类型 "certificateNo": "string", // 证件号码 "eori": "string", // EORI "vat": "string" // Vat }, "sender": { // 发件人 t_order_sender "senderName": "string", // 发件人姓名//必填 "company": "string", // 发件人公司 "phone": "string", // 发件人手机 "telePhone": "string", // 发件人电话 "email": "string", // 发件人邮箱 "countryCode": "CN", // 发件国家 ISO 二字码//可选 "province": "string", // 州省 "city": "string", // 城市 "zipCode": "string", // 邮编 "streetAddress1": "string", // 地址1 "streetAddress2": "string", // 地址2 "streetAddress3": "string", // 地址3 "houseNumber": "string" // 门牌号 }, "items": [ // 申报明细 t_order_declaration { "cnName": "string", // 品名-中 "enName": "string", // 品名-英//必填 "sku": "string", // SKU "unit": "string", // 单位 "unitPrice": 0, // 单价//必填 "unitWeight": 0, // 单重//必填 "quantity": 0, // 数量//必填 "amount": 0, // 总金额 "weight": 0, // 总重量 "imHsCode": "string", // 进口HsCode "exHsCode": "string", // 出口HsCode "skuImage": "string", // 图片 "category": "string", // 类别 "brand": "string", // 品牌 "brandType": "string", // 品牌类型 "isMagnetic": false, // 是否带磁带电 "distribution": "string", // 配货信息 "material": "string", // 材质 "enMaterial": "string", // 英文材质 "purpose": "string", // 用途 "attributes": "string", // 属性 "originCountry": "string", // 原产国 "salesLink": "string", // 销售链接 "model": "string", // 型号 "specifications": "string", // 规格 "comment": "string" // 备注 } ] } ``` **响应** ```json { "code": 0, "msg": "success", "data": { "clientNo": "", "serviceNo": "", "trackNo": "", "labelUrl": "" } } ``` > 幂等时可选传 requestId。 ###### 2 接口业务逻辑 ###### 核心流程 - 本接口用于创建小包订单预报数据,执行顺序固定为:参数校验 -> 查询销售产品与规则/价格快照绑定 -> **黑名单校验(命中则中止,不落库)** -> 创建订单草稿 -> 偏远校验 -> 计费重计算 -> **仅「预报」时机费用计算与落库** -> 更新订单状态 -> 返回结果。(入库/出库时机费用见 §4.3.4,由仓储收货、出库流程触发。) - 参数校验失败,或销售产品/快照绑定失败、黑名单校验命中时,均不创建任何订单数据,直接返回业务错误。 - 进入草稿创建后,先将订单主数据、发件人、收件人、申报明细、预报件重尺数据落库,订单状态固定写入 `draft`。 - 后续偏远、计费重、费用等流程全部基于已创建的草稿订单继续处理;如任一业务环节失败,订单状态不扩展新枚举,统一保留 `draft`,并在 `t_order.PredeclareFailReason` 记录失败原因。 ###### 详细业务逻辑 1. 参数解析与基础数据校验 - **会话客户(优先)**:调用 `ResolveSessionCustomerAsync`,将 **`effectiveCustomerId`** 写入待落库 `customerId`;客户登录 **可不传** `customerId`;管理员代下单 **须传** `customerId`,否则返回 **「客户ID不能为空」**。 - 校验请求体必填字段:`productCode`、`warehouseId`、`length`、`width`、`height`、`actualWeight`、`receiver.recipientsName`、`receiver.countryCode`、`receiver.zipCode`、`receiver.streetAddress1`、`sender.senderName`、`items`(**不含** `customerId`、`trackNo`、`serviceNo`)。 - 校验数值合法性:`length/width/height/actualWeight` 必须大于 0。 - 校验枚举合法性:`cargoAttribute` 仅允许 `general`、`electronics`、`sensitive`。 - 校验枚举合法性:`exportCustomsMode` **可选**;若传入则须为 §4.2.1 **ExportCustomsMode** 枚举 code,否则返回 **「出口报关方式不合法」**。 - **客户校验**:按 **`effectiveCustomerId`**(**`t_customer.Id`**)确认客户存在且 `Status` 可用;不可用则返回错误。请求体若传 `customerId` 须与 **`effectiveCustomerId` 一致**,否则 **「客户ID非法」**。 - **客户隔离(§4.4.0)**:落库 `t_order.CustomerId` **仅**写入 `effectiveCustomerId`。 - 查询 `t_warehouse_address`,条件:`Id = warehouseId and IsDeleted = 0`,返回字段:`Id`、`WarehouseName`;仓库不存在时返回错误。 - 查询 `t_country` 校验收件人国家:`receiver.countryCode` 为 **string**,优先按 **`CountryCode`(ISO 二字码,大小写不敏感)** 匹配;亦兼容 **数字字符串** 形式的 `t_country.Id`;亦可按中/英文名匹配。不存在时返回 **「收件国家不存在」**。下列字段任一缺失返回 **「收件人信息不完整」**:`receiver`、`recipientsName`、`countryCode`、`zipCode`、`streetAddress1`。 2. 查询销售产品配置(用于定位发布记录) - 查询 `t_sales_product`,条件:`ProductCode = productCode and IsDeleted = 0 and IsActive = 1` - 返回字段:`Id`、`ProductCode`、`ProductName` - 如不存在,**不创建订单**,直接返回失败(错误信息需明确「销售产品不存在或未启用」)。 3. 绑定销售产品计费快照(强关联:以创建订单当刻绑定的价格与规则为准;**运行时优先 Redis + 本地内存**) - 绑定时点:以本次请求处理时间作为 `bindTime`(草稿落库后,`t_order.CreateTime` 应与该时点一致或可由该时点推导)。 - **主路径(推荐)**:按 `salesProductId = t_sales_product.Id` 与 `bindTime`,从 **Redis / 计费服务本地内存** 解析「当前应对外生效」的 **规则快照** 与 **价格快照**(结构化 JSON,与运营发布组装的版本一致),得到: - `rulesSnapshot`:含 `version`、`effectiveStartAt`(或等价字段)、`content` - `priceSnapshot`:含 `version`、`effectiveStartAt`、`content` - **降级路径**:当 Redis/内存未命中或版本指针异常时,从 **`t_sales_product_publish_record`** 读取: - 规则:条件 `SalesProductId = t_sales_product.Id and OperatorType = 'rules' and Status = '生效' and PublishTime <= bindTime`,按 `PublishTime desc` 取 1 条,`Content` 即规则快照正文(与下文 `rulesSnapshot.content` 同构)。 - 价格:条件 `OperatorType = 'price'`,其余同上,`Content` 即价格快照正文。 - 命中后应**异步回写 Redis**,并打点监控。 - 绑定失败处理(**尚未落库订单**): - 若未找到 rulesSnapshot:**不创建订单**,直接返回失败(错误信息需明确「未命中规则快照版本(内存/Redis/DB)」)。 - 若未找到 priceSnapshot:**不创建订单**,直接返回失败(错误信息需明确「未命中价格快照版本(内存/Redis/DB)」)。 - 版本字段落库:在下一步「创建订单草稿」插入 `t_order` 时写入(强关联字段,**新单新价、老单不改**): - `SalesProductRulesVersion = rulesSnapshot.version`(与发布记录 `Version` 一致) - `SalesProductPriceVersion = priceSnapshot.version` - 性能要求: - 下单/预估主链路应在**进程内内存**完成快照读取与匹配;降级读 DB 仅作兜底,应限流并尽快恢复 Redis。 4. 黑名单校验(拦截型,来自已绑定的 rules 快照 `content`) - 读取 `rulesSnapshot.content.areas.blacklist.rules`(国家/省市/邮编区间规则集合)。 - 匹配规则: - 国家必须匹配参数校验阶段已解析的收件国家编码(与后续写入 `t_order.ConsigneeCountry` 的值一致,来源于 `t_country.CountryCode`) - 若规则配置了省/市,则请求体收件人 `receiver` 的省/市也必须匹配 - 邮编落在任一 `postcodeRanges.left ~ postcodeRanges.right` 区间内即视为命中 - 命中处理:**不创建订单**,直接返回失败(错误信息需明确「收件地址命中黑名单区域」),不进入草稿落库及后续流程。 5. 创建订单草稿 - **`ServiceNo`**:请求体 `serviceNo` 为空时,服务端自动生成唯一运单号(**`IDE` + `yyMMddHHmmss` + 4 位随机数**,年份取后两位如 2026→26);`trackNo` 为空则落库 `NULL`。 - 插入 `t_order`: - 写入字段:`ServiceNo`、`ClientNo`、`TrackNo`、`ProductCode`、`ChannelCode`、`ConsigneeCountry`、`CargoAttribute`、`ExportCustomsMode`、`OrderStatus`、`WarehouseId`、`CustomerId`、`IsCod`、`CodCurrency`、`CodSum`、`IsIntercept`、`IsRemote`、`PredeclareFailReason`、`SalesProductRulesVersion`、`SalesProductPriceVersion`、审计字段。 - 处理规则: - `OrderStatus` 固定写 `draft` - `ConsigneeCountry` 取收件国家对应的 `t_country.CountryCode` - `IsIntercept` 初始写 `0` - `IsRemote` 初始写 `0` - `PredeclareFailReason` 初始写空 - `SalesProductRulesVersion`、`SalesProductPriceVersion` 取步骤 3 已解析的快照版本 - `ExportCustomsMode` 取请求体 `exportCustomsMode`(须为 §4.2.1 枚举 code),未传则 `NULL` - 插入 `t_order_sender` / `t_order_recipient`: - 请求体 `sender.countryCode` / `receiver.countryCode` 为 **ISO 二字码**(见步骤 1);落库 **`Country` 字段为 `t_country.Id`**(int)。 - 其它字段见下表列名映射。 - 插入 `t_order_sender`(明细): - 条件:使用新生成的 `OrderId` - 写入字段:`OrderId`、`SenderName`、`Company`、`Country`、`Province`、`City`、`ZipCode`、`StreetAddress1`、`StreetAddress2`、`StreetAddress3`、`HouseNumber`、`Phone`、`TelePhone`、`Email` - 插入 `t_order_recipient`: - 写入字段:`OrderId`、`RecipientsName`、`CompanyName`、`Country`、`Phone`、`TelePhone`、`ZipCode`、`Province`、`City`、`StreetNumber`、`StreetAddress1`、`StreetAddress2`、`StreetAddress3`、`Email`、`TaxType`、`TaxNo`、`CertificateType`、`CertificateNo`、`Eori`、`Vat` - 插入 `t_order_declaration`: - 对 `items` 数组逐条入库 - 写入字段:`OrderId`、`CnName`、`EnName`、`Sku`、`Unit`、`UnitPrice`、`UnitWeight`、`Quantity`、`Amount`、`Weight`、`HsCode`、`SkuImage`、`Category`、`Brand`、`BrandType`、`IsMagnetic`、`Distribution`、`Material`、`EnMaterial`、`Purpose`、`Attributes`、`OriginCountry`、`SalesLink`、`Model`、`Specifications`、`Comment` - 插入 `t_order_weight`: - 一次性写入 **4** 条记录,`Type` 依次为 **`forecast`**、**`receive`**、**`ship`**、**`downstream`**(与 §4.2.7 `Type` 枚举一致) - 写入字段:`OrderId`、`Type`、`Length`、`Width`、`Height`、`ActualWeight`、`VolumetricWeight`、`ChargeableWeight`、`Cubic` - **先**构造 **`Type = forecast`** 行的初始值: - `ActualWeight` 取请求值 - `VolumetricWeight`、`ChargeableWeight` 可先写请求体值或 `0`,后续步骤 7、8 按规则重算并覆盖 - `Cubic = Length × Width × Height / 1000000` - **`Type = receive` / `ship` / `downstream` 三行**:`Length`、`Width`、`Height`、`ActualWeight`、`VolumetricWeight`、`ChargeableWeight`、`Cubic` **均与 `forecast` 行相同**(创建时从 `forecast` 拷贝;后续收货、发货、下游等环节再按各自 `Type` 更新差异化测量) 6. 偏远区域校验(标记型,来自已绑定的 rules 快照) - 读取 `rulesSnapshot.content.areas.remote.rules`(国家/省市/邮编区间规则集合)。 - 匹配规则与黑名单一致。 - 命中则更新 `t_order.IsRemote = 1`;未命中则更新 `t_order.IsRemote = 0` - 偏远校验不拦截订单,只影响后续费用匹配。 7. 计费重计算(**有效规则 ID:仅销售产品**) - **输入**:仅使用 `t_order_weight` 中 **`Type = forecast`** 的件重尺:`Length`、`Width`、`Height`、`ActualWeight`(单位与库表约定一致,一般为 cm / kg)。 - **解析 `effectiveWeightRuleId`**:取本单绑定 **`t_sales_product.WeightRuleId`**;须**非空**且对应 **`t_weight_rule.IsDeleted=0`**。否则更新 `PredeclareFailReason`(如「未配置计重规则」),保持 `draft`,失败返回。 - **加载明细集合 `rules`**:`t_weight_rule_info` 中 **`RuleId = effectiveWeightRuleId and IsDeleted=0`**,按 **`Id` 升序**排序;或从 **`rulesSnapshot.content.weightRule`** 读取与 **`effectiveWeightRuleId`** 一致的 **`clientRules`**(结构无 `client` 字段,与表 §5.2.16 对齐)。集合为空则同上失败。 - **算法(未进位计费重 `rawW`)**: 1. `gw = ActualWeight`(实重,kg)。 2. 依次遍历 `rules` 中每一行(已按 `Id` 升序):对该行解析 **`volumetricFactor`** 为正数,计算 **`vw = (Length × Width × Height) / volumetricFactor`**(与产品历史口径一致:体积 cm³ ÷ 系数 → 体积重 kg;若实现使用 m³,须在服务内统一换算)。 3. 将该行的 **`gw`、`vw`** 代入 **`condition`**(支持 `>`、`>=`、`<`、`<=`、`==` 等,运算对象可为 `gw`、`vw` 及常量;**不支持**跨行交叉引用)。 4. **第一条**使 `condition` 为**真**的行:**命中行**;若无一命中,失败(`PredeclareFailReason` 建议「计重规则条件均未命中」)。 5. 命中行的 **`billableWeight`** 取值仅为 **`gw` 或 `vw`**(大小写不敏感):`rawW = (billableWeight == 'gw') ? gw : vw`。 6. 写回 **`t_order_weight.VolumetricWeight`**:建议取**命中行**对应的 **`vw`**。**预报草稿**将同一 `vw` 写入四行(创建时初始拷贝策略)。**入库**仅写 **`receive` 行**;**出库**仅写 **`ship` 行**;均**不**向其它 `Type` 同步。 8. 计费重进位(**有效进位 ID:仅销售产品**) - **解析 `effectiveWeightRoundingId`**:取本单绑定 **`t_sales_product.WeightRoundingId`**;须**非空**且 **`t_weight_rounding.IsDeleted=0`**。无效则失败(`PredeclareFailReason` 如「未配置计重进位」)。 - **加载区间集合 `ranges`**:`t_weight_rounding_info` 中 **`RoundingId = effectiveWeightRoundingId and IsDeleted=0`**;或快照 **`rulesSnapshot.content.weightRounding.ranges`**(仅含 **`start`/`end`/`public`**,与 §5.2.18 对齐)。 - **匹配区间**:在 `ranges` 中找到满足 **`start <= rawW <= end`** 的区间(多区间重叠时按 **`Id` 升序**取第一条,或产品约定唯一覆盖);**无匹配**则失败。 - **算法(最终计费重)**:设命中区间 **`public = u`**(正数,进位值 kg)。推荐统一为**按进位值向上取整**: - `ChargeableWeight = ceil(rawW / u) × u` - `ceil` 为数学向上取整;实现须固定小数位数(如 3 位)避免浮点误差。 - 更新 **`t_order_weight.ChargeableWeight`**:**预报阶段**四行同步为同一计费重。**入库**仅写 **`receive` 行`**;**出库**仅写 **`ship` 行`**;均**不**向其它 `Type` 同步。若 **`public` 为空或非法**,失败。 9. 价格分区匹配(来自已绑定的 rules 快照) - 读取 `rulesSnapshot.content.areas.price.rules`,按收件国家/省市/邮编区间匹配得到 `zoneName`(如“美国西部”)。 - 若未匹配到价格分区,更新 `t_order.PredeclareFailReason = '价格分区未匹配(规则快照)'`,订单状态保持 `draft`,返回失败。 10. 费用计算与费用落库(**预报节点仅应收**) - **重算应收(§4.3.4.1)**:全部启用卖价项;**重量**取 **`Type=forecast`**;**upsert** `ChargingStage=forecast`。 - **不在创建阶段排货/写应付**;渠道与应付在 **入库** `ApplyReceiveBillingByWaybillAsync` 中 `TryConfirmServiceChannelAndPayableAsync` 处理(见 **§6.3.1.1**)。 - **强校验**:存在启用卖价项但未匹配到任何明细时,预报失败。 11. 固定报价示例规则(某产品,来自 price 快照的 continuePricing) - 示例规则: - 首重:1kg 以内,首重价 18 元 - 续重:超出 1kg 部分,每 0.5kg 为一个续重单位,续重单价 6 元 / 0.5kg - 进位规则:不足 0.5kg 按 0.5kg 进位 - 适用前提: - 该产品已匹配到对应价格分区与卖价配置 - `t_order_weight.ChargeableWeight` 已完成最终进位 - price 快照明细为 `calculationFormula = 续单价` 且包含 `continuePricing` - 计算口径: - 当 `ChargeableWeight <= 1` 时,总费用 = 18 - 当 `ChargeableWeight > 1` 时: - 先收首重价 18 - 超出部分 = `ChargeableWeight - 1` - 超出部分按 0.5kg 向上取整到续重单位数 - 续重费用 = 续重单位数 * 6 - 总费用 = 18 + 续重费用 - 示例: - `ChargeableWeight = 0.8kg`,进位到 `1.0kg`,总费用 = 18 - `ChargeableWeight = 1.2kg`,超出 `0.2kg`,按 0.5kg 进 1 个续重单位,总费用 = 18 + 6 = 24 - `ChargeableWeight = 1.6kg`,超出 `0.6kg`,按 0.5kg 进 2 个续重单位,总费用 = 18 + 12 = 30 - 该示例说明:计费重来源仅为 `t_order_weight`,不参与 `t_order_declaration` 的商品重量汇总逻辑。 12. 成功完成预报 - 当偏远标记完成、计费重计算完成、**本阶段(预报)应计费用**写入完成后(黑名单已在草稿落库前完成校验): - 更新 `t_order.OrderStatus = 'predeclared'` - 更新 `t_order.PredeclareFailReason = ''` - 更新 `t_order.UpdateUserId`、`t_order.UpdateTime` - **系统面单**:调用 `EnqueueGenerateLabelPdfAsync`(MQ 开启则入队 `generate-label-pdf`,否则同步生成);写入 **`t_order_file.systemLabel`**;失败不阻断创单。 - 返回字段: - `clientNo`:取 `t_order.ClientNo` - `serviceNo`:取 `t_order.ServiceNo` - `trackNo`:取 `t_order.TrackNo` - `labelUrl`:取已生成的 **系统面单** `FileUrl`;若尚未就绪则为空字符串(异步队列场景常见) ###### 状态与失败处理 - 参数校验失败:不创建订单,直接返回错误。 - 销售产品不存在或未启用、规则/价格快照未命中、黑名单命中:均**不创建订单**,直接返回错误(与参数校验一致,不落库,不写 `PredeclareFailReason`)。 - 草稿创建成功但后续业务失败:订单状态统一保留 `draft`。 - 偏远命中:仅更新 `IsRemote = 1`,不拦截流程。 - 计重规则、进位规则、价格分区、卖价配置缺失或未匹配:统一写入 `PredeclareFailReason`,订单状态保持 `draft`。 ###### 校验与异常 - 对必填参数、枚举值、数值范围、客户/仓库/国家/销售产品存在性进行校验,异常场景返回统一错误码与错误信息。 - 草稿创建后的异常场景需要保留订单数据,并将失败原因写入 `t_order.PredeclareFailReason`,供订单列表和详情页展示。 ###### 3 当前实现(oms0414-back) 与上文 **§4.3.5**、**§1.5.4** 对齐;补充要点: | 项 | 实现细节 | | --- | --- | | 幂等 | `requestId` 非空时先 `GetByRequestIdAsync`,命中则 **`BuildParcelCreateOutputAsync`** 直接返回 | | 会话客户 | `ResolveEffectiveCustomerAsync` → 写 `input.CustomerId = sessionCustomer.Id` | | 必填校验 | 另含 **`declaredCurrency`、`quantity`(>0)**、申报明细 **`enName/unitPrice/unitWeight/quantity`**;**`clientNo` 非必填** | | `channelCode` | 未传时默认 **`product.ProductCode`** | | `serviceNo` | 自定义时查重,冲突 → **「运单号已存在」**;否则 **`IDE{yyMMddHHmmss}{4位随机}`** | | 快照 | 见 **§4.3.5**(DB 发布 + Live 降级,**无 Redis**) | | 应付/排货 | **创建时不执行**;入库时 **TryConfirm**(失败 → `pendingAllocation`) | | 成功 | `OrderStatus=predeclared`;清空 `AllocationPendingReason`;触发系统面单生成写入 **`t_order_file.systemLabel`**;响应 **`labelUrl`** 读文件表(未就绪可为空) | | 失败 | 草稿已建则 **`MarkParcelDraftFailedAsync`**(保持 `draft` + `PredeclareFailReason`)后 **仍抛业务异常** | ##### 4.4.3.2 批量创建小包(POST /order/parcel/batch-create) 与 **§4.4.3.1 创建小包** 使用同一套单票业务规则(`CreateParcelCoreAsync`)。数组元素字段与单票请求体一致。**差异**:本接口在 **同一 HTTP 请求内同步逐条** 调用单票逻辑,**不创建** 批量任务、**不入队**;HTTP 响应返回 **批次号与逐条明细**(含每票 `serviceNo` / `labelUrl` / 失败原因)。 ###### 1 接口请求示例 **请求 /order/parcel/batch-create**(POST) > 请求体为 **`OrderParcelBatchCreateInput`**:与单票结构相同的对象 **数组**(非裸数组根)。单次 **≤ 2000** 条;超限整批拒绝。每元素可选 **`requestId`** 幂等。 ```json [ { "clientNo": "string", // 客户单号 t_order.ClientNo "trackNo": "string", // 跟踪号 t_order.TrackNo//可选 "serviceNo": "string", // 运单号 t_order.ServiceNo//可选;未传时自动生成 "productCode": "string", // 产品代码 t_order.ProductCode "channelCode": "string", // 渠道代码 t_order.ChannelCode "cargoAttribute": "string", // 货物属性 t_order.CargoAttribute:general-普货,electronics-3c&带电,sensitive-敏感货 "exportCustomsMode": "9610Simple", // 出口报关方式,见 §4.2.1//可选 "warehouseId": 0, // 仓库ID t_order.WarehouseId "customerId": 0, // 客户ID t_order.CustomerId//可选:客户登录由会话补全 "length": 3, // 长(cm) t_order_weight.Length "width": 3, // 宽(cm) t_order_weight.Width "height": 3, // 高(cm) t_order_weight.Height "actualWeight": 3, // 实重(kg) t_order_weight.ActualWeight "volumetricWeight": 3, // 体积重(kg) t_order_weight.VolumetricWeight "chargeableWeight": 3, // 计费重(kg) t_order_weight.ChargeableWeight "cubic": 0, // 立方(m³) t_order_weight.Cubic "isCod": false, // 是否COD "codCurrency": "string", // COD币种 "codSum": "string", // COD金额 "declaredCurrency": "string", // 申报币种 "declaredSum": "string", // 申报金额 "quantity": "string", // 包裹数量 "receiver": { // 收件人 t_order_recipient "recipientsName": "string", // 收件人名称 "companyName": "string", // 收件人公司名称 "phone": "string", // 收件人手机 "telePhone": "string", // 收件人电话 "email": "string", // 收件人邮箱 "countryCode": "US", // 收件国家 ISO 二字码//必填 "province": "string", // 收件人省/州 "city": "string", // 收件人城市 "streetNumber": "string", // 收件人门牌号 "streetAddress1": "string", // 收件人地址1 "streetAddress2": "string", // 收件人地址2 "streetAddress3": "string", // 收件人地址3 "zipCode": "string", // 收件人邮编 "taxType": 0, // 税号类型 "taxNo": "string", // 税号 "certificateType": 0, // 证件类型 "certificateNo": "string", // 证件号码 "eori": "string", // EORI "vat": "string" // Vat }, "sender": { // 发件人 t_order_sender "senderName": "string", // 发件人姓名 "company": "string", // 发件人公司 "phone": "string", // 发件人手机 "telePhone": "string", // 发件人电话 "email": "string", // 发件人邮箱 "countryCode": "CN", // 发件国家 ISO 二字码//可选 "province": "string", // 州省 "city": "string", // 城市 "zipCode": "string", // 邮编 "streetAddress1": "string", // 地址1 "streetAddress2": "string", // 地址2 "streetAddress3": "string", // 地址3 "houseNumber": "string" // 门牌号 }, "items": [ // 申报明细 t_order_declaration { "cnName": "string", // 品名-中 "enName": "string", // 品名-英 "sku": "string", // SKU "unit": "string", // 单位 "unitPrice": 0, // 单价 "unitWeight": 0, // 单重 "quantity": 0, // 数量 "amount": 0, // 总金额 "weight": 0, // 总重量 "imHsCode": "string", // 进口HsCode "exHsCode": "string", // 出口HsCode "skuImage": "string", // 图片 "category": "string", // 类别 "brand": "string", // 品牌 "brandType": "string", // 品牌类型 "isMagnetic": false, // 是否带磁带电 "distribution": "string", // 配货信息 "material": "string", // 材质 "enMaterial": "string", // 英文材质 "purpose": "string", // 用途 "attributes": "string", // 属性 "originCountry": "string", // 原产国 "salesLink": "string", // 销售链接 "model": "string", // 型号 "specifications": "string", // 规格 "comment": "string" // 备注 } ] } ] ``` **响应** ```json { "code": 0, "msg": "success", "data": { "batchNo": "PCB20240509120000123", "successCount": 98, "failCount": 2, "items": [ { "index": 1, "requestId": "optional", "clientNo": "C001", "serviceNo": "LP202405091200011234", "trackNo": null, "labelUrl": "/uploads/prints/orders/...", "success": true, "message": "success" }, { "index": 2, "requestId": null, "clientNo": "C002", "serviceNo": null, "trackNo": null, "labelUrl": null, "success": false, "message": "销售产品不存在或未启用" } ] } } ``` - **`batchNo`**:服务端生成,`PCB` + 时间戳毫秒。 - **`successCount` / `failCount`**:与 `items.length` 一致;单条失败 **不中断** 后续条目。 - 失败条目 **`message`** 为业务异常文案;若草稿已创建且后续失败,单票仍可能落库为 **`draft`** + `PredeclareFailReason`(与 §4.4.3.1 一致)。 ###### 2 接口业务逻辑 ###### 核心流程 1. 校验数组非空、`Count ≤ 2000`(常量 `ParcelBatchMaxCount`)。 2. 生成 **`batchNo`**,按序 **`for` 循环** 调用 **`CreateParcelCoreAsync`**。 3. 每条 **`try/catch`**:成功累加 `successCount` 并填充 `items`(含 `labelUrl`);失败写入 `items` 且 `success=false`。 4. 返回 **`OrderParcelBatchCreateOutput`**(**无 `taskId`**)。 ###### 校验与异常 - 空数组 → **「批量创建请求不能为空」**。 - 超过 2000 条 → **「单次批量创建不能超过2000条」**。 ###### 3 当前实现(oms0414-back) - `OrderService.BatchCreateParcelAsync`(`OrderService.cs`)。 - **设计演进说明**:批量 JSON 创建仍为 **同步** `batchNo` + `items[]`;若需异步可改为创建 `parcel-create` 批量任务并入队。 ##### 4.4.3.3 模板列表(GET /order/template/list) ###### 1 接口请求示例 **请求 /order/template/list**(GET,Query 参数) ``` ?page=1&pageSize=20&type=standard|custom&keyword= ``` **响应** ```json { "code": 0, "msg": "success", "data": { "items": [ { "id": 1, "name": "标准模板", "type": "standard", "templateKey": "standard_v1", "customerId": 0, "createTime": "2024-01-01 12:00:00", "updateTime": "2024-01-01 12:00:00" } ], "total": 10 } } ``` ###### 2 接口业务逻辑 ###### 核心流程 - 客户登录:仅返回 **`Type=standard`** 的模板,以及 **`Type=custom` 且 `CustomerId = effectiveCustomerId`** 的自定义模板。 - 管理员:按 `type`、`keyword` 分页查询,不做客户隔离。 - 按接口定义完成参数解析与分页返回。 ###### 校验与异常 - 客户门户 **不得**通过 `templateId` 下载或上传他客 `custom` 模板(无权限时返回 **「模板不存在」**)。 ##### 4.4.3.4 下载模板(GET /order/template/download) ###### 1 接口请求示例 **请求 /order/template/download**(GET,Query 参数) ``` ?templateId=1 ``` **响应** > 直接返回 Excel 文件流,Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet ###### 2 接口业务逻辑 ###### 核心流程 - 校验模板存在;客户登录时须通过 **§4.4.3.3** 可见性规则(`standard` 或归属当前客户的 `custom`)。 - 标准模板 Excel 表头建议包含列 **「出口报关方式」**(与 `exportCustomsMode` / `t_order.ExportCustomsMode` 对应),单元格填写 §4.2.1 **ExportCustomsMode** 枚举 code;列缺失或空单元格按 **未传** 处理,落库 `NULL`。 - 示例行中 **`CustomerId` 列**对客户门户导出为 **`effectiveCustomerId`**(非模板表上的 `CustomerId` 字段)。 ###### 校验与异常 - 对必填参数、状态与数据存在性进行校验,异常场景返回统一错误码与错误信息。 - 若模板中提供「出口报关方式」列,非空值须为合法枚举 code,否则该行在解析或异步处理阶段失败(错误信息需可定位行号)。 - 客户访问他客 `custom` 模板时返回 **「模板不存在」**。 ##### 4.4.3.5 上传下单(POST /order/upload) ###### 1 接口请求示例 **请求 /order/upload**(POST,multipart/form-data) | 字段 | 类型 | 说明 | | --- | --- | --- | | file | File | Excel 文件 | | templateId | number | 模板ID | > **导入条数限制**:按模板解析得到的有效订单数据行(不含表头)**最多 2000 行**;超出则拒绝本次上传并返回错误,不创建任务。 **响应** ```json { "code": 0, "msg": "success", "data": { "jobId": 1001, "async": true } } ``` - **`jobId`**:批量任务 ID,前端跳转 **批量任务详情页**(`/order/batchJobs?jobId=`)。 - **`async`**:`true` 表示 RabbitMQ 异步入队处理;`false` 表示 MQ 未启用、已在当前请求内同步处理完毕。 ###### 2 接口业务逻辑 ###### 核心流程 1. **`ResolveEffectiveCustomerAsync`** → `effectiveCustomerId`;校验模板可见性(`standard` 或归属客户的 `custom`)。 2. 解析 Excel 有效数据行(**最多 2000 行**),每行映射为 `OrderParcelCreateInput` 字段集。 3. 创建 **`t_order_batch_job`**(`Action=import-row`,`PayloadJson` 含 `taskName`、`templateId`)及 **`t_order_batch_job_item`**(每行一条,`PayloadJson` 含 `rowNumber`、`rowDataJson`)。 4. **MQ 开启**:逐子项发布 RabbitMQ,异步调用 **`CreateParcelCoreAsync`**(§4.4.3.1 全链路)。 5. **MQ 关闭**:同步逐条 `ProcessAsync`,当场更新子项状态与任务汇总。 6. 返回 **`{ jobId, async }`**;失败明细见 **`GET /order/batch/jobs/{id}/items`**(筛选 `status=failed`)。 ###### 校验与异常 - 文件为空 → **「上传文件不能为空」**。 - 模板不存在 / 无权限 → **「模板不存在」**。 - 超过 2000 行 → **「单次最多导入 2000 行」**。 ###### 3 当前实现(oms0414-back) - `OrderService.ImportOrdersAsync` → `BuildImportBatchJobInputAsync` + `OrderBatchJobCoordinator.CreatePayloadJobAsync` / `CreatePayloadJobAndProcessSyncAsync`。 - 原 **`t_order_task` / `t_order_task_fail` 及 `/order/task/*` 接口已移除**。 ##### 4.4.3.6 批量任务(GET /order/batch/jobs、/items 等) > Excel 导入及订单列表批量操作(支付、预报、打印等)均复用本组接口;详见 **§4.2.3**。 ###### 1 接口请求示例 **任务列表** `GET /order/batch/jobs?page=1&pageSize=30&status=&action=&jobId=` **任务详情** `GET /order/batch/jobs/{id}` **子项列表** `GET /order/batch/jobs/{id}/items` **响应(任务详情摘要)** ```json { "code": 0, "msg": "success", "data": { "jobId": 1001, "action": "import-row", "status": "completed", "total": 100, "successCount": 95, "failCount": 5, "pendingCount": 0, "runningCount": 0, "cancelledCount": 0, "templateId": 1, "taskName": "批量下单_20260722120000", "startedAt": "2026-07-22 12:00:00", "finishedAt": "2026-07-22 12:01:30" } } ``` **响应(子项摘要)** ```json { "code": 0, "msg": "success", "data": [ { "id": 1, "orderId": 9001, "clientNo": "CUST-001", "rowNumber": 2, "status": "failed", "failReason": "客户单号重复", "resultMessage": "客户单号重复" } ] } ``` ###### 2 接口业务逻辑 ###### 核心流程 - 客户登录:查询条件 **强制** `t_order_batch_job.CustomerId = effectiveCustomerId`。 - 导入类任务(`import-row`):子项 **`clientNo`**、**`rowNumber`** 用于定位 Excel 行;原始行数据在 `PayloadJson.rowDataJson`。 - 支持 **重试失败子项**、**取消待处理子项**(须相应 PermissionCode)。 ###### 校验与异常 - 禁止跨客户查看批量任务详情;不匹配时返回 **「批量任务不存在」**。 #### 4.4.4 揽收订单 > **定位**:客户预约「从发件地址揽收货件,送往指定仓库」并绑定运输方式(销售产品)的简化下单入口。 > **落库结果**:仍是一张 **`t_order`**,走既有预报 / 计费 / 支付 / 入库链路;**`OrderSource = pickup`**。 > **不要混淆**: > - **揽收订单**(本节):跨境/仓配 OMS 订单,入口菜单 **订单 → 揽收订单**。 > - **仓储小包 / 到货**(`C_parcel`、`C_parcel_arrival` 等,前端 `/warehouse/parcel`):国内快递入仓库存管理,**不是**本节对象。 ##### 4.4.4.0 与现有能力结合(总览) ```text 客户维护发件地址簿(§4.2.10 / §4.4.5) ↓ 选择 senderAddressId POST /order/pickup/create ↓ 读取仓库 t_warehouse_address → 组装 receiver ↓ 货物名称/重量 → 组装 items + actualWeight ↓ 运输方式 productCode + warehouseId + pickupTime ↓ 组装 OrderParcelCreateInput(缺省填齐 L/W/H、申报、货物属性等) ↓ 复用 CreateParcelCoreAsync(§4.4.3.1 全链路:快照绑定/黑名单/计费/预报…) ↓ t_order(OrderSource=pickup, PickupTime, CustomerSenderAddressId) + t_order_sender(发件快照)+ t_order_recipient(仓库映射快照) + t_order_declaration / t_order_weight / t_order_fee … ``` | 能力 | 复用方式 | | --- | --- | | 小包创建 / 预报 / 计费 | **必须**复用 `CreateParcelCoreAsync` / `POST /order/parcel/create` 逻辑;揽收接口只做 **入参适配**,禁止另写一套落单 | | 仓库列表 | `GET /system/warehouse/list`(已有);创建时校验 `warehouseId` 同 §4.4.3.1 | | 运输方式 | 销售产品启用列表(与「我要下单 / 查价」同源:`ProductCode` + 启用态) | | 发件信息 | 自 `t_customer_sender_address` 复制到请求 `sender`,并写 `CustomerSenderAddressId` | | 收件信息 | **由仓库地址映射为 `receiver`**(客户表单不填海外收件人) | | 我的订单 | 支付/预报/拦截/删除等 **全部沿用** `/order/*`;列表可筛 `orderSource=pickup` | | 入库/出库 | 订单进仓后仍走操作模块既有扫描(§6),与来源无关 | ##### 4.4.4.1 仓库 → 收件人映射规则 `t_warehouse_address` 当前为 **非结构化**收货信息(`Recipient` / `ContactPhone` / `ShippingAddress` TEXT),揽收一期映射约定: | `t_order_recipient` | 取值规则 | | --- | --- | | `RecipientsName` | `t_warehouse_address.Recipient`;空则用 `WarehouseName` | | `Phone` | `ContactPhone` | | `StreetAddress1` | `ShippingAddress`(整段写入地址1;省市区邮编可空) | | `Country` / `countryCode` | **默认 `CN`**(仓库业务默认中国);若后续仓库表扩展国家字段则优先用仓库国家 | | `ZipCode` | 无则写占位 **`000000`**(满足 §4.4.3.1 必填校验;黑名单/分区按产品配置评估) | | 其余字段 | 空 | > **风险提示**:目的国为仓所在国(默认 CN)时,销售产品须能匹配该目的国;运营应配置/启用适合「送仓」场景的产品,或二期将仓库地址拆为结构化国家/省/市/邮编后再严格计费分区。 ##### 4.4.4.2 简化字段 → 小包入参缺省 | 揽收表单 | 映射到 `OrderParcelCreateInput` | | --- | --- | | `productCode` | 同左(运输方式) | | `warehouseId` | 同左 | | `senderAddressId` | 读地址簿 → `sender.*`;并落库 `CustomerSenderAddressId` | | `pickupTime` | 落库 `t_order.PickupTime`(创建核心在写 `t_order` 时写入) | | `cargoName` | `items[0].cnName` + `items[0].enName`(英文缺省可同中文或拼音拷贝) | | `actualWeight` | `actualWeight`;`items[0].unitWeight` / `weight`;`quantity=1` | | (无尺寸) | `length/width/height` **缺省各 `1`**(cm),满足现网必填;入库后以实测量覆盖 | | (无申报币种/单价) | `declaredCurrency=CNY`,`declaredSum=1`,`items[0].unitPrice=1`,`cargoAttribute=general`,`quantity`(包裹数)=`1` | | `clientNo` | 可选 | ##### 4.4.4.3 接口清单 | 接口名称 | 请求方式 | 请求路径 | 说明 | | --- | --- | --- | --- | | 创建揽收订单 | POST | /order/pickup/create | 组装后走小包创建;**`CustomerId` 会话隔离** | | 揽收订单列表 | POST | /order/list | **复用**;固定或默认 `orderSource=pickup`(前端揽收菜单) | ##### 4.4.4.4 创建揽收订单(POST /order/pickup/create) ###### 1 请求示例 ```json { "requestId": "string", // 可选幂等键,透传小包创建 "clientNo": "string", // 可选客户单号 "productCode": "US_AIR", // 运输方式=销售产品编码//必填 "warehouseId": 1, // 仓库地址 Id//必填 "senderAddressId": 10, // t_customer_sender_address.Id//必填 "pickupTime": "2026-07-16 14:00:00", // 预约揽收时间//必填;须为未来时间(实现可放宽到 ≥ 当日 00:00) "cargoName": "服装", // 货物名称//必填 "actualWeight": 2.5, // 重量 kg//必填,> 0 "cargoAttribute": "general" // 可选;默认 general } ``` ###### 2 响应 与 **`POST /order/parcel/create`** 成功响应同构(至少含 `serviceNo` / `clientNo` / `orderId` 等现有字段);另可回显 `orderSource: "pickup"`、`pickupTime`。 ###### 3 业务处理 1. 解析 **`effectiveCustomerId`**(§4.4.0)。 2. 校验必填:`productCode`、`warehouseId`、`senderAddressId`、`pickupTime`、`cargoName`、`actualWeight`。 3. 查询 `t_customer_sender_address`:`Id = senderAddressId AND CustomerId = effectiveCustomerId AND IsDeleted = 0`;否则 **「发件地址不存在」**。 4. 查询 `t_warehouse_address`:同 §4.4.3.1;不存在则失败;按 §4.4.4.1 组装 `receiver`。 5. 组装 `OrderParcelCreateInput`(§4.4.4.2)→ 调用 **`CreateParcelCoreAsync`**。 6. 在写入 `t_order` 时(扩展点):`OrderSource = pickup`,`PickupTime = pickupTime`,`CustomerSenderAddressId = senderAddressId`。 - 实现建议:给 `CreateParcelCoreAsync` / 草稿插入增加可选「来源上下文」参数,避免复制整段创建代码。 7. 后续偏远/黑名单/计费/预报与 §4.4.3.1 **完全一致**。 ###### 4 校验与异常 - 禁止使用他客 `senderAddressId`。 - `actualWeight <= 0` 拒绝。 - 小包创建既有错误(产品未启用、未命中快照、黑名单等)原样返回。 ##### 4.4.4.5 前端(itfx-front) | 项 | 说明 | | --- | --- | | 路由 | `/order/pickupOrder`,菜单 **订单 → 揽收订单** | | 页面 | `src/views/order/pickupOrder/index.vue`(建议:上区创建表单 + 下区揽收订单列表,或独立创建页 + Tab 列表) | | 组件 | **FormPro**(创建)+ **TablePro** / HandsontablePro(列表);遵守 `docs/前端页面设计规范.md` | | 权限 | `meta.permission`: `Order.Pickup.Query`;创建按钮 `Order.Pickup.Create` | | 下拉数据 | 发件地址:`/order/sender-address/list`;仓库:`/system/warehouse/list`;运输方式:销售产品下拉(与下单页同源) | | 列表 | `POST /order/list` 且 **`orderSource=pickup`**;详情/支付/预报跳转或复用 **我的订单** 能力 | #### 4.4.5 客户发件地址 > 菜单挂在 **客户**,HTTP 挂在 **`/order/sender-address/*`**(享受 §4.4.0 客户隔离;**不**放在 RBAC `/customer/*` 主数据文档内展开)。 ##### 4.4.5.1 接口清单 | 接口名称 | 请求方式 | 请求路径 | 说明 | | --- | --- | --- | --- | | 分页列表 | POST | /order/sender-address/list | 仅当前客户;支持 keyword、仅默认 | | 详情 | GET | /order/sender-address/detail | `id`;校验归属 | | 新增 | POST | /order/sender-address/create | 写入 `CustomerId=effectiveCustomerId` | | 更新 | POST | /order/sender-address/update | 校验归属后更新 | | 删除 | POST | /order/sender-address/delete | 软删;**不级联**已下订单 | | 设为默认 | POST | /order/sender-address/set-default | 同客户仅一条 `IsDefault=1` | ##### 4.4.5.2 新增(POST /order/sender-address/create) ```json { "addressName": "公司仓", "senderName": "张三", // 必填 "company": "", "phone": "13800000000", "telePhone": "", "email": "", "countryCode": "CN", // ISO 二字码;默认 CN "province": "广东省", "city": "深圳市", "zipCode": "518000", "streetAddress1": "南山区…", // 必填 "streetAddress2": "", "streetAddress3": "", "houseNumber": "", "isDefault": true } ``` **响应**:`{ "id": 10 }`(或含完整对象)。国家落库规则同小包:`countryCode` → `t_country.Id`。 ##### 4.4.5.3 列表(POST /order/sender-address/list) ```json { "page": 1, "pageSize": 30, "keyword": "", // 匹配 AddressName / SenderName / Phone / StreetAddress1 "onlyDefault": false } ``` 响应:`totalCount` + `items[]`(字段 camelCase,含 `countryCode` 建议回显 ISO 或与详情约定一致)。 ##### 4.4.5.4 前端(itfx-front) | 项 | 说明 | | --- | --- | | 路由 | `/user/senderAddress`,菜单 **客户 → 发件地址** | | 页面 | `src/views/user/senderAddress/index.vue` | | 组件 | **TablePro** 列表 + **ModalPro** / 抽屉表单;按钮高 32px、`var(--app-primary-color)` | | 权限 | `Order.SenderAddress.Query` / `.Create` / `.Update` / `.Delete` | | 联动 | 揽收创建页下拉刷新本列表;删除已占用地址 **允许**(订单已快照) | #### 4.4.6 物流单绑定包裹/库存(我的订单) > **入口**:我的订单 → 选中一单 →「绑定包裹/库存」弹窗(左:包裹/库存 Tab;右:已选明细,可调数量)。 > **权限**:`Order.MyOrders.StockBind`。 > **实现状态**:设计落地中(oms0414-back `OrderService.StockBind` + 前端 `StockBindModal`)。 ##### 4.4.6.1 接口清单 | 接口名称 | 请求方式 | 请求路径 | 说明 | | --- | --- | --- | --- | | 当前绑定 | GET | /order/stock-bind/detail | `orderId`;右侧已选 | | 库存候选 | POST | /order/stock-bind/inventory-candidates | 左侧库存 Tab | | 包裹候选 | POST | /order/stock-bind/parcel-candidates | 左侧包裹 Tab(**全部生命周期**) | | 保存绑定 | POST | /order/stock-bind/save | 全量覆盖;冻结 | | 清空绑定 | POST | /order/stock-bind/clear | 释放全部 `bound` | | 确认扣减 | POST | /order/stock-bind/confirm-deduct | 扣实物并 `deducted`;可由预报/出库钩子调用 | ##### 4.4.6.2 当前绑定(GET /order/stock-bind/detail) **请求**:`orderId`(必填)。 **响应** ```json { "orderId": 1001, "warehouseId": 3, "warehouseName": "华南仓", "bindStatus": "bound", "items": [ { "id": 11, "bindType": "inventory", "skuId": 88, "skuCode": "SKU-A", "skuName": "T恤", "inventoryId": null, "binLocationId": 5, "binCode": "A-01", "bindQty": 2, "availableQty": 10, "imageUrl": null, "status": "bound" }, { "id": 12, "bindType": "parcelItem", "parcelId": 50, "parcelItemId": 501, "trackingNo": "YT123", "productName": "配件", "attributeText": "红/L", "skuId": 88, "skuCode": "SKU-A", "bindQty": 1, "availableQty": 3, "imageUrl": "", "status": "bound" } ] } ``` `bindStatus`:`none` / `bound` / `deducted`(存在任意 `deducted` 优先;否则有 `bound` 为 `bound`)。 ##### 4.4.6.3 库存候选(POST /order/stock-bind/inventory-candidates) ```json { "orderId": 1001, "warehouseId": 3, "keyword": "", "page": 1, "pageSize": 30 } ``` - 默认仓库:入参 `warehouseId` 优先,否则取订单 `WarehouseId`。 - 仅返回 `AvailableQty > 0` 的 SKU 汇总。 - 响应:`total` + `items[]`:`skuId`, `skuCode`, `skuName`, `skuImageUrl`, `warehouseId`, `totalQty`, `frozenQty`, `availableQty`, `binCount`。 ##### 4.4.6.4 包裹候选(POST /order/stock-bind/parcel-candidates) ```json { "orderId": 1001, "warehouseId": 3, "lifecycleStatus": "", "keyword": "", "page": 1, "pageSize": 20 } ``` - `lifecycleStatus` 空或 `all`:全部生命周期均可。 - 包裹下行:`parcelItemId`, `productName`, `attributeText`, `imageUrl`, `skuId`, `skuCode`, `quantity`, `frozenQty`, `availableQty`。 - 仅下行 `availableQty > 0` 的包裹进入列表(包裹头仍可展示)。 ##### 4.4.6.5 保存绑定(POST /order/stock-bind/save) ```json { "orderId": 1001, "items": [ { "bindType": "inventory", "skuId": 88, "bindQty": 2, "inventoryId": null }, { "bindType": "parcelItem", "parcelId": 50, "parcelItemId": 501, "bindQty": 1 } ] } ``` **服务端步骤** 1. 校验订单归属;`OrderStatus` 为已删除/已取消则拒绝;若已有 `deducted` 明细则拒绝改绑(须先业务回滚,本期不做回滚接口)。 2. 解析仓库;无仓库且候选依赖仓库时拒绝。 3. 读取现有 `Status=bound` 明细 → 全部释放冻结 → 软删/标 `released`。 4. 按 `items[]` 重新冻结并插入 `bound` 明细(空数组等价清空)。 5. `inventory`:按 `inventoryId` 或 SKU 多库位分摊冻结 `C_inventory`。 6. `parcelItem`:冻结 `C_parcel_item`;若行上已有 `SkuId`,同步按 SKU 冻结库存。 7. **不**校验申报明细数量。 8. 事务提交;响应同 detail。 ##### 4.4.6.6 清空 / 确认扣减 - `POST /order/stock-bind/clear`:`{ "orderId" }`,仅释放 `bound`。 - `POST /order/stock-bind/confirm-deduct`:对 `bound` 行:`Qty`/`Quantity` 与 `FrozenQty` 同减 `BindQty`,`AvailableQty` 重算,明细改 `deducted`。 ##### 4.4.6.7 前端(itfx-front) | 项 | 说明 | | --- | --- | | 入口 | `/order/myOrders` 工具栏/更多:「绑定包裹/库存」 | | 组件 | `StockBindModal.vue`:左右两卡片;左 Tab 包裹/库存;右已选 + 数量步进 | | 权限 | `Order.MyOrders.StockBind` | | 规范 | `docs/前端页面设计规范.md`;主色 `var(--app-primary-color)` | #### 4.4.7 店铺订单:创建物流订单 / 创建物流&代发订单 > **页面**:`/order/platformStoreOrder`(店铺订单)。 > **设计原则(客户视角)**:弹窗第一目标是 **校对防错**——看清「哪几张平台单、寄给谁、寄什么、用哪条运输方式、(代发时)绑哪件库存/包裹」,再一键提交;宁可多一步确认,也不要静默下错单或绑错仓。 > **实现状态**:设计稿(2026-07-19);待前后端落地。 ##### 4.4.7.1 能力分流(与「匹配库存」并列) | 能力 | 入口 | 是否依赖平台 SKU→库内 SKU 映射 | 库存动作 | 产出 | | --- | --- | --- | --- | --- | | **匹配库存**(既有) | 行内 / 批量匹配 | **是** | 创建时 **扣减** `C_inventory` | 创建 `t_order` + 回写 `LinkedOrderId`,履约→`toShip` | | **创建物流订单**(本期) | 工具栏「创建物流订单」 | **否** | 无 | 仅批量创建物流单并关联店铺单 | | **创建物流&代发订单**(本期) | 工具栏「创建物流&代发订单」 | **否** | 创建后按 §4.4.6 **冻结绑定**(不即时扣实物) | 创建物流单 + `t_order_stock_bind` | - 三入口互斥前提相同:店铺单 `FulfillStatus=pending`、未搁置、无有效 `LinkedOrderId`。 - **匹配库存**适合「平台 SKU 已映射、要从仓内 SKU 扣数出货」;本期两入口适合「先按店铺收件人/明细下物流单」,代发场景用绑定占用库存/包裹,出库/预报再扣减。 ##### 4.4.7.2 用户故事与校对目标 作为客户,我勾选若干待履约店铺单后: 1. **创建物流订单**:只关心「运输方式是否选对、收件人/国家/地址/电话是否与平台单一致、申报品名数量是否对」。确认后批量生成 OMS 物流单。 2. **创建物流&代发订单**:在上述校对之外,还要确认「每一张店铺单各自绑了哪些库存 SKU / 包裹行、数量是否够、仓库是否同一仓」。确认后批量建单并冻结绑定。 ##### 4.4.7.3 前端交互总览 **列表工具栏(`toolbar-left`)** | 按钮 | 权限码 | 前置校验(点击时) | | --- | --- | --- | | 创建物流订单 | `Order.PlatformStoreOrder.CreateLogistics` | 至少勾选 1 单;全部可创建;上限 **50** 单/次(与批量匹配同量级,避免一次校对失控) | | 创建物流&代发订单 | `Order.PlatformStoreOrder.CreateFulfill` | 同上;另需后续选仓 | 不可创建时:`Message.warning` 列出原因摘要(已关联物流单 / 已搁置 / 非 pending),不打开弹窗。 **弹窗组件** | 模式 | 组件(建议) | 宽度 | | --- | --- | --- | | 仅物流 | `CreateLogisticsModal.vue` | `960px` | | 物流&代发 | `CreateLogisticsFulfillModal.vue`(可内嵌复用绑定面板) | `1100px` | 共用视觉骨架(`platform-store-create-modal`): ``` ┌─────────────────────────────────────────────────────────────┐ │ 标题 + 模式标签(仅物流 / 物流&代发) │ │ 一句提示:请逐单核对收件人与明细后再提交 │ ├─────────────────────────────────────────────────────────────┤ │ 【共享配置条】运输方式* | 发货仓库(代发必填;仅物流建议填) │ │ 勾选 N 单 · 已就绪 M 单(代发) │ ├──────────────────────┬──────────────────────────────────────┤ │ 【左:待创建清单】 │ 【右:当前单详情 / 绑定区】 │ │ 可点选切换当前单 │ · 收件人校对卡 │ │ 每行:平台单号、国家、 │ · 商品明细表(品名/SKU/数量) │ │ 收件人姓名、行数摘要 │ · (代发)绑定交互 = StockBind 左右栏 │ │ 代发:绑定状态徽标 │ │ ├──────────────────────┴──────────────────────────────────────┤ │ 【底部确认条】摘要:运输方式名 · 仓库名 · N 单 │ │ 主按钮「确认创建」/「确认创建并绑定」 │ └─────────────────────────────────────────────────────────────┘ ``` **校对优先的交互细则** 1. **打开即选中第一单**;右侧默认展示该单收件人 + 明细,强迫用户「看见」再提交。 2. **收件人校对卡**(只读为主,允许本弹窗内临时改写收件人字段,仅影响本次创建入参,不写回店铺主数据除非另开编辑): - 姓名、电话、国家二字码、省/市、邮编、详细地址分行展示;缺关键字段标红「缺项」。 - 与列表列 `recipientName` / `orderCountry` 同源(店铺单结构化收件字段,见拉单落库)。 3. **商品明细表**:平台 SKU、品名、数量、单价;排除 `IsShipExcluded` 行;空行则该单不可提交。 4. **共享运输方式**:顶部单一 `a-select`(销售产品,同匹配库存下拉数据源);**所有勾选单共用**;变更后底部摘要同步刷新。 5. **仅物流**:仓库可选(有则写入订单 `WarehouseId`;无则创建链路按小包规则处理)。右侧无绑定区,仅校对。 6. **物流&代发**: - 仓库 **必填**;变更仓库时清空各单已选绑定草稿并提示。 - 右侧绑定区复用「我的订单」`StockBindModal` 交互语义(左候选库存/包裹 Tab + 搜索;右已选 + 数量步进);数据先落在 **前端草稿**(按 `storeOrderId` 分桶),提交时一并传给后端。 - 左清单每行徽标:`未绑定`(灰)/ `已绑 n 件`(主色)。 - **提交门槛**:每一单至少 1 条绑定且数量 >0;未就绪单在清单上抖动/标红,点击跳转到该单。 7. **二次确认**(`Modal.confirm`):展示一行摘要「将创建 N 张物流单,运输方式 XXX;(代发)仓库 YYY,共绑定 Z 件」。确认后才调接口。 8. **结果反馈**:成功/失败按单展示(对齐 `batch-create` / `batch-match-inventory` 的 `items[]`);部分成功时刷新列表并保留失败单说明,可继续对失败单重试。 **样式约定**(对齐 `docs/前端页面设计规范.md`) - 容器 padding、按钮高 32px、圆角 4px、间距 8px;主色 `var(--app-primary-color)`。 - 左清单选中态:左边框主色 + 浅底,避免「卡片堆」感;校对卡用分区标题(基本信息 / 收件人 / 商品 / 绑定),不用多层阴影。 - 缺项、未绑定用 `danger` 文字色;已就绪用主色 Tag。 ##### 4.4.7.4 后端接口 > **推荐**:店铺域包装接口(内部逐单 `CreateParcelForBatchAsync` + 回写关联;代发再调 StockBind 保存)。前端 **不** 直接拼 `/order/parcel/batch-create` 后手动回写 `LinkedOrderId`(易漏关联、难做幂等)。 > 包装接口语义上仍是「批量创建订单」;实现可复用 `PlatformStoreOrderParcelBuilder` 的扩展版(**无映射 SKU 时用平台行申报**)。 | 接口名称 | 请求方式 | 请求路径 | 说明 | | --- | --- | --- | --- | | 批量创建物流订单 | POST | `/order/platform-store-order/batch-create-logistics` | 仅建物流单并关联 | | 批量创建物流&代发 | POST | `/order/platform-store-order/batch-create-fulfill` | 建单 + 按单保存绑定(冻结) | **共同请求字段** ```json { "customerId": null, "productCode": "SP-US-EXPRESS", "warehouseId": 3, "items": [ { "storeOrderId": 101, "receiver": null, "bindItems": [ { "bindType": "inventory", "skuId": 88, "bindQty": 2, "inventoryId": null }, { "bindType": "parcelItem", "parcelId": 50, "parcelItemId": 501, "bindQty": 1 } ] } ] } ``` | 字段 | 仅物流 | 物流&代发 | | --- | --- | --- | | `productCode` | 必填 | 必填 | | `warehouseId` | 可选 | **必填** | | `items[].storeOrderId` | 必填 | 必填 | | `items[].receiver` | 可选覆盖 | 可选覆盖 | | `items[].bindItems` | 忽略 | **必填且非空** | **共同响应**(对齐批量匹配) ```json { "batchNo": "PSL20260719153000", "successCount": 2, "failCount": 1, "items": [ { "storeOrderId": 101, "success": true, "orderId": 9001, "serviceNo": "SN9001", "message": null }, { "storeOrderId": 102, "success": false, "orderId": null, "serviceNo": null, "message": "收件人国家缺失" } ] } ``` ##### 4.4.7.5 服务端处理步骤 **A. `batch-create-logistics`(单条)** 1. 客户隔离 + `EnsureCreatableAsync`(同匹配:未搁置、pending、无有效 LinkedOrder)。 2. 加载店铺单行(排除 `IsShipExcluded`);用平台行组装申报(`PlatformSku` / `ProductName` / 数量 / 单价;重量缺省最小值)。**不要求** `MappedSkuId`。 3. 收件人:`receiver` 覆盖 > 店铺结构化收件字段 > `PlatformExtraJson` 解析(与现网 `ParseReceiver` 一致)。 4. `RequestId = platform-store-order:{storeOrderId}`(与匹配库存共用幂等键,避免重复建单)。 5. `OrderSource=platform`,`ClientNo=PlatformOrderNo`,`ProductCode`、可选 `WarehouseId`。 6. `CreateParcelForBatchAsync` → 回写 `LinkedOrderId`、`FulfillStatus=toShip`、`LastSyncTime`;有 TrackNo 可写 `ShippingLabelNo`。 7. **不**扣减库存。 **B. `batch-create-fulfill`(单条)** 1. 执行 A;成功后立刻按 `bindItems` 调用与 §4.4.6.5 相同的冻结保存逻辑(`orderId` = 新建物流单 Id,`warehouseId` = 请求仓库)。 2. 若绑定失败:**回滚本条**(软删/取消新建物流单或标记删除 + 清空 `LinkedOrderId`,并释放已冻冻结),该条记入 `failCount`;**不影响**批次内其他已成功单。 3. 绑定数量与申报件数 **不强制对齐**(同 §4.4.6)。 **批量**:同步逐条;单次 `items.length` ≤ 50;返回 `batchNo` + 逐条结果。 ##### 4.4.7.6 申报组装说明(相对匹配库存) | 项 | 匹配库存 | 创建物流 / 物流&代发 | | --- | --- | --- | | 申报 SKU | 库内 `CSku.SkuCode` | 平台 `PlatformSku`(可空则用品名截断) | | 品名 | 行品名 / SKU 名 | 行 `ProductName` | | 重量尺寸 | 优先 SKU 档案 | 缺省最小件重/默认长宽高(可后续增强) | | 库存 | 创建前扣减 | 仅物流:无;代发:绑定冻结 | 可在 `PlatformStoreOrderParcelBuilder` 增加 `BuildFromPlatformLines(...)` 重载,避免污染映射扣减路径。 ##### 4.4.7.7 权限与前端落点 | 项 | 说明 | | --- | --- | | 权限 | `Order.PlatformStoreOrder.CreateLogistics`、`Order.PlatformStoreOrder.CreateFulfill`(代发另需具备 StockBind 等价能力,或本权限内含绑定) | | API | `src/api/order/platformStore.ts` 增加两方法 | | 页面 | `platformStoreOrder/index.vue` 工具栏两按钮;勾选态驱动 | | 组件 | `CreateLogisticsModal.vue`、`CreateLogisticsFulfillModal.vue`;绑定子区可抽 `StockBindPanel.vue` 供我的订单与店铺共用 | | i18n | `order.platformStoreOrder.createLogistics.*` / `createFulfill.*` | | 规范 | `docs/前端页面设计规范.md`;主色 `var(--app-primary-color)` | ##### 4.4.7.8 验收要点(客户校对场景) 1. 缺收件人国家/地址时,右侧标红且无法提交该单(或整批拦截并定位到问题单)。 2. 运输方式未选,主按钮禁用。 3. 代发:未绑任何资源时主按钮禁用;换仓后绑定草稿清空。 4. 提交后列表出现 `linkedServiceNo`,履约状态变为待发货;代发单在「我的订单」打开绑定详情可见冻结行。 5. 对已关联物流单再次点击两入口,前置校验拒绝。 6. 与「匹配库存」并行存在,文案不混淆(工具栏分组或次要按钮区分)。 ## 5 渠道模块 ### 5.1 核心需求说明 - **销售产品**:管理对外销售的产品(专线/快递/邮政),配置时效、黑名单区、偏远分区、价格分区、计重规则、计重进位等;支持价格配置。 - **产品配置**:产品基础信息(编码、名称、分组、时效、面单、黑名单区、偏远分区、价格分区、计重规则、计重进位等)。 - **产品卖价**:为每个产品配置多条价格项(运费、保险费、燃油附加费等);每条价格项通过弹窗配置: - **基础信息**:费用项、费用单位、币种、收费条件、计费因素(因素+因素值,如货物类型、分区、重量、仓库、客户等)。 - **价格表**:根据计费因素动态生成价格表,支持分区/货物类型/重量等组合、计算公式(总价/单价/续单价)、计算值、客户组。 - **排货规则**:选择排货方案(成本最低/时效最快/综合最优),配置服务商列表(服务商名称、服务渠道、权重、指定客户)。 - **发布记录**:价格与规则的发布历史,支持查看版本内容、回滚到指定版本。 - **服务渠道**:管理下游服务渠道(支持渠道组+渠道层级),配置预报单号类型、时效、买价等。 - **渠道分组**:`GET /channel/service-channel/group/list` 查询服务商分组列表(下拉/左侧树筛选);`group/create`、`group/update`、`group/delete` 维护 `t_service_channel_group`。 - **渠道配置**:渠道基础信息(渠道代码、名称、时效、预报配置、描述、预报前脚本等)。 - **渠道买价**:为每个渠道配置多条买价项(运费、保险费、燃油附加费等);每条买价项通过弹窗配置: - **基础信息**:费用项、费用单位、币种、收费条件、计费因素(因素+因素值)。 - **价格表**:根据计费因素动态生成价格表,支持分区/货物类型/重量等组合、计算公式、计算值、客户组。 - **发布记录**:买价与规则的发布历史,支持查看版本内容、回滚到指定版本。 - **黑名单区/偏远分区/价格分区**:共用 KpiArea 表,仅通过 `AreaType` 区分;支持分区及明细(国家/省/市/邮编区间)的增删改查。 - **计重规则/计重进位**:配置计费重量规则与进位方式,明细结构与分区类似。 - **自动触发**:配置下单/入库/出库时的自动拦截、警告、问题件等触发规则;**V2 目标**为订单校验规则引擎(结构化条件 + 全字段 + 预报同步拦截),见 **§5.3.0.6.1**。 - **问题模板**:按组织维护问题件话术与通知方式,供标记问题件与自动触发消费(一期 CRUD)。 - **标签设计**:保存 vg-print 导出的标签模板 JSON,支持尾程/客户/入库/出库/sku 等类型。 ### 5.2 表结构设计 #### 5.2.1 销售产品表 `t_sales_product` | 字段名 | 字段类型 | 长度 | 主键/外键 | 非空 | 说明 | | --- | --- | --- | --- | --- | --- | | Id | BIGINT | - | 主键 | 是 | 自增唯一标识 | | ProductCode | VARCHAR | 64 | - | 是 | 产品编码 | | ProductName | VARCHAR | 128 | - | 是 | 产品名称 | | ProductGroup | VARCHAR | 32 | - | 否 | 分组(专线/快递/邮政) | | MinTimeliness | INT | - | - | 否 | 最小时效(天) | | MaxTimeliness | INT | - | - | 否 | 最大时效(天) | | CustomerWaybill | VARCHAR | 32 | - | 否 | 系统面单/客户面单 | | BlacklistAreaId | BIGINT | - | 外键 | 否 | 黑名单区ID | | RemoteAreaId | BIGINT | - | 外键 | 否 | 偏远分区ID | | PriceAreaId | BIGINT | - | 外键 | 否 | 价格分区ID(`t_kpi_area`,`AreaType=price`) | | WeightRuleId | BIGINT | - | 外键 | 否 | 计重规则ID | | WeightRoundingId | BIGINT | - | 外键 | 否 | 计重进位ID | | ProductDescription | TEXT | - | - | 否 | 产品描述 | | PreStorageScript | TEXT | - | - | 否 | 入库前脚本 | | IsActive | TINYINT | - | - | 是 | 是否启用 | | Scheme | VARCHAR | 32 | - | 是 | 排货方案:costLowest/timeFastest/overallBest | | IsDeleted | TINYINT | - | - | 是 | 是否删除:0-否,1-是,默认0 | | CreateUserId | INT | - | - | 是 | 创建人 | | CreateTime | DATETIME | - | - | 是 | 创建时间 | | UpdateUserId | INT | - | - | 否 | 更新人 | | UpdateTime | DATETIME | - | - | 否 | 更新时间 | #### 5.2.2 产品卖价表 `t_sales_product_price`(子表,关联 t_sales_product) > 产品卖价主表,每个产品可配置多条价格项(运费、保险费、燃油附加费等)。 | 字段名 | 字段类型 | 长度 | 主键/外键 | 非空 | 说明 | | --- | --- | --- | --- | --- | --- | | Id | BIGINT | - | 主键 | 是 | 自增唯一标识 | | SalesProductId | BIGINT | - | 外键 | 是 | 销售产品ID | | SerialNumber | INT | - | - | 是 | 序号 | | Status | TINYINT | - | - | 是 | 状态:0-禁用,1-启用 | | PriceName | VARCHAR | 64 | - | 是 | 价格名称(如运费、保险费) | | CostItem | VARCHAR | 64 | - | 是 | 费用项 | | CostUnit | VARCHAR | 16 | - | 是 | 费用单位:kg/票/件 | | Currency | VARCHAR | 16 | - | 是 | 币种:RMB/USD/EUR | | ChargingCondition | VARCHAR | 64 | - | 否 | 收费条件 | | IsDeleted | TINYINT | - | - | 是 | 是否删除:0-否,1-是,默认0 | | CreateUserId | INT | - | - | 是 | 创建人 | | CreateTime | DATETIME | - | - | 是 | 创建时间 | | UpdateUserId | INT | - | - | 否 | 更新人 | | UpdateTime | DATETIME | - | - | 否 | 更新时间 | #### 5.2.3 产品卖价-计费因素表 `t_sales_product_price_factor`(子表,关联 t_sales_product_price) > 弹窗「基础信息」中的计费因素配置,支持货物类型、分区、重量、仓库、客户等。 | 字段名 | 字段类型 | 长度 | 主键/外键 | 非空 | 说明 | | --- | --- | --- | --- | --- | --- | | Id | BIGINT | - | 主键 | 是 | 自增唯一标识 | | PriceId | BIGINT | - | 外键 | 是 | 产品卖价ID | | SerialNumber | INT | - | - | 是 | 序号(决定因素排序) | | Factor | VARCHAR | 64 | - | 是 | 因素:货物类型/分区/重量/仓库/客户 | | FactorValues | JSON | - | - | 否 | 因素值数组,如 ["普货","敏感货"] | | IsDeleted | TINYINT | - | - | 是 | 是否删除:0-否,1-是,默认0 | | CreateUserId | INT | - | - | 是 | 创建人 | | CreateTime | DATETIME | - | - | 是 | 创建时间 | | UpdateUserId | INT | - | - | 否 | 更新人 | | UpdateTime | DATETIME | - | - | 否 | 更新时间 | #### 5.2.4 产品卖价-价格表明细表 `t_sales_product_price_detail`(子表,关联 t_sales_product_price) > 弹窗「价格表」中的价格明细,表头由计费因素动态生成,存储各组合的价格值。 | 字段名 | 字段类型 | 长度 | 主键/外键 | 非空 | 说明 | | --- | --- | --- | --- | --- | --- | | Id | BIGINT | - | 主键 | 是 | 自增唯一标识 | | PriceId | BIGINT | - | 外键 | 是 | 产品卖价ID | | FactorValuesJson | JSON | - | - | 否 | 非客户因素组合,如 {"分区":"欧洲","货物类型":"普货","重量":"0-0.5"} | | CalculationFormula | VARCHAR | 32 | - | 是 | 计算公式:总价/单价/续单价 | | CalculatedValue | DECIMAL | 18,4 | - | 否 | 计算值 | | CustomerGroupValues | JSON | - | - | 否 | 客户组对应价格,如 {"客户组1":10,"客户组2":12} | | IsDeleted | TINYINT | - | - | 是 | 是否删除:0-否,1-是,默认0 | | CreateUserId | INT | - | - | 是 | 创建人 | | CreateTime | DATETIME | - | - | 是 | 创建时间 | | UpdateUserId | INT | - | - | 否 | 更新人 | | UpdateTime | DATETIME | - | - | 否 | 更新时间 | #### 5.2.5 排货规则-服务商表 `t_sales_product_allocation_provider`(子表,关联 t_sales_product_allocation_rule) > 服务商列表:服务商名称、服务渠道、权重、指定客户。 | 字段名 | 字段类型 | 长度 | 主键/外键 | 非空 | 说明 | | --- | --- | --- | --- | --- | --- | | Id | BIGINT | - | 主键 | 是 | 自增唯一标识 | | SalesProductId | BIGINT | - | 外键 | 是 | 销售产品ID | | SerialNumber | INT | - | - | 是 | 序号 | | ProviderName | VARCHAR | 128 | - | 是 | 服务商名称 | | ServiceChannelId | BIGINT | - | 外键 | 否 | 服务渠道ID | | Weight | INT | - | - | 是 | 权重(评分用) | | CustomerIds | JSON | - | - | 否 | 指定客户 ID 数组(**`t_customer.Id`**),空表示全部 | | IsDeleted | TINYINT | - | - | 是 | 是否删除:0-否,1-是,默认0 | | CreateUserId | INT | - | - | 是 | 创建人 | | CreateTime | DATETIME | - | - | 是 | 创建时间 | | UpdateUserId | INT | - | - | 否 | 更新人 | | UpdateTime | DATETIME | - | - | 否 | 更新时间 | **Scheme 枚举**:costLowest(成本最低)、timeFastest(时效最快)、overallBest(综合最优) #### 5.2.6 发布记录表 `t_sales_product_publish_record`(子表,关联 t_sales_product) > 价格与规则的发布历史,支持查看版本内容、回滚。 | 字段名 | 字段类型 | 长度 | 主键/外键 | 非空 | 说明 | | --- | --- | --- | --- | --- | --- | | Id | BIGINT | - | 主键 | 是 | 自增唯一标识 | | SalesProductId | BIGINT | - | 外键 | 是 | 销售产品ID | | Version | VARCHAR | 32 | - | 是 | 版本号,如 20231227112339 | | OperatorType | VARCHAR | 16 | - | 是 | 操作类型:price(价格)/rules(规则) | | Status | VARCHAR | 16 | - | 是 | 状态:生效/回滚/无效 | | PublishUser | VARCHAR | 64 | - | 是 | 发布人 | | PublishTime | DATETIME | - | - | 是 | 发布时间 | | Description | VARCHAR | 512 | - | 否 | 版本备注 | | Content | JSON | - | - | 否 | 发布内容(价格或规则配置的JSON快照) | | IsDeleted | TINYINT | - | - | 是 | 是否删除:0-否,1-是,默认0 | | CreateUserId | INT | - | - | 是 | 创建人 | | CreateTime | DATETIME | - | - | 是 | 创建时间 | | UpdateUserId | INT | - | - | 否 | 更新人 | | UpdateTime | DATETIME | - | - | 否 | 更新时间 | ##### 5.2.6.1 发布内容 `Content`(JSON快照)结构规范(用于限制校验与计费) > 设计目标: > 1) **强绑定有效期**:订单创建时按当时的生效版本(PublishTime)绑定发布记录;后续计算以绑定版本为准。 > 2) **计算快**:下单/计费/校验不再联查 `t_kpi_area*`、`t_weight_rule*`、`t_weight_rounding*`、`t_sales_product_price*` 等多表;只读取一次发布记录 `Content`(可缓存)。 > 3) **可回溯**:任何时候都能根据订单绑定的发布记录ID拿到当时的规则/价格快照。 > > **性能实现要求(强制)**: > - 发布后的规则/价格快照以 **`t_sales_product_publish_record` 为权威落库**;同步任务将标准化后的 `Content` 写入 **Redis**(按 `salesProductId` + `OperatorType` + `Version` 或 `current` 指针),并通知计费服务刷新**本地内存**。 > - **在线下单/预估只读 Redis + 本地内存**;仅在 Redis/内存异常时**降级**直读发布记录表(见订单模块 `/order/parcel/create` 步骤 4),并应异步回填 Redis。 ##### 5.2.6.2 Redis 与发布同步(在线计费) > **当前实现(oms0414-back)**:发布接口 **仅** 向 `t_sales_product_publish_record` / `t_service_channel_publish_record` 插入记录(`Content` 为完整快照 JSON)。**未**写入 Redis、**未**推送计费实例刷新内存。运行时算价见订单 **§4.3.5**(读发布表或 Live 组装)。以下 Redis 流程为 **目标架构**,待渠道发布与订单模块联调时落地。 - **Key 示例**(可按团队规范调整前缀): - `oms:pricing:sp:{salesProductId}:rules:{version}` / `...:price:{version}`:存放与 `Content` 同构的 JSON(或已序列化的二进制)。 - `oms:pricing:sp:{salesProductId}:rules:current` / `...:price:current`:指向当前对外生效的 `version` 字符串,便于 O(1) 定位。 - **在线匹配数据来源**:计费服务进程内已解析的 `rulesSnapshot` / `priceSnapshot`(来自 Redis 加载或启动预热)。 **发布同步流程(必须)**: 1. 业务侧完成一次规则或价格发布,写入 `t_sales_product_publish_record`(含 `Version`、`Status`、`PublishTime`、`Content`)。 2. 将 `Content` 标准化后写入 Redis(对应 `OperatorType` 的 key);更新 `current` 指针指向新版本(与「生效」语义一致)。 3. 通知各计费实例刷新内存;实例冷启动可从 Redis 全量加载,必要时可从发布表补齐缺失版本。 4. 若 Redis 写入失败:应**阻断或重试发布**,直至线上可见一致快照,避免「库里有版本但线上算价仍用旧缓存」。 ###### 1) 通用字段(rules / price 共用) - `schemaVersion`:快照结构版本(从 1 开始) - `operatorType`:`rules` 或 `price`(与发布记录 `OperatorType` 保持一致) - `salesProduct`:发布时的产品基础信息快照(用于展示与校验,不参与联查) - `effective`: - `startAt`:生效时间(一般等于 `PublishTime`) - `endAt`:失效时间(可为空,表示至今有效;当有新版本发布时可由系统补齐旧版本 `endAt`) - `checksum`:可选,用于校验内容是否被篡改(如 sha256) ###### 2) `rules` 快照内容(限制 + 计重 + 进位) `Content` 示例(OperatorType=rules): ```json { "schemaVersion": 1, "operatorType": "rules", "salesProduct": { "salesProductId": 1, "productCode": "US_AIR", "productName": "美国空运专线(普货)", "cargoAttributes": ["general", "electronics", "sensitive"], "scheme": "overallBest" }, "effective": { "startAt": "2026-04-25 14:30:00", "endAt": null }, "areas": { "blacklist": { "areaName": "黑名单区1", "rules": [ { "countryCode": "US", "province": null, "city": null, "postcodeRanges": [ { "left": "90000", "right": "90010" }, { "left": "91000", "right": "91099" } ] } ] }, "remote": { "areaName": "偏远分区1", "rules": [ { "countryCode": "US", "province": "California", "city": null, "postcodeRanges": [{ "left": "90001", "right": "90099" }] } ] }, "price": { "areaName": "价格分区(美国)", "rules": [ { "zoneCode": "US_WEST", "zoneName": "美国西部", "countryCode": "US", "province": null, "city": null, "postcodeRanges": [{ "left": "90000", "right": "96999" }] } ] } }, "weightRule": { "weightRuleId": 1, "name": "标准计重规则", "clientRules": [ { "volumetricFactor": 6000, "condition": "gw>vw", "billableWeight": "gw" }, { "volumetricFactor": 6000, "condition": "gw<=vw", "billableWeight": "vw" } ] }, "weightRounding": { "weightRoundingId": 1, "name": "标准进位", "ranges": [{ "start": 0, "end": 30, "public": "0.5" }] }, "checksum": "" } ``` 说明: - `areas.blacklist/remote/price` 将 `t_kpi_area_info` 的明细**扁平化内嵌**到快照里,避免下单时联查分区表。 - `areas.price.rules[*].zoneCode/zoneName` 用于后续计价因子“分区”匹配(对应价格快照的 `factorValuesJson.分区`)。 - `weightRule.clientRules` 将 `t_weight_rule_info` 直接内嵌(**无 `client` 列**);运行时以 §4.4.3.1 步骤 7 解析的 **`effectiveWeightRuleId`** 为准,与快照中 **`weightRuleId`** 对齐或回表查询。 - `weightRounding.ranges` 将 `t_weight_rounding_info` 内嵌(**仅 `public` 进位值**);运行时以步骤 8 的 **`effectiveWeightRoundingId`** 为准。 ###### 3) `price` 快照内容(卖价配置 + 价格表) `Content` 示例(OperatorType=price): ```json { "schemaVersion": 1, "operatorType": "price", "salesProduct": { "salesProductId": 1, "productCode": "US_AIR", "productName": "美国空运专线(普货)" }, "effective": { "startAt": "2026-04-25 14:30:00", "endAt": null }, "priceItems": [ { "serialNumber": 1, "status": 1, "priceName": "运费", "costItem": "freight", "costUnit": "kg", "currency": "RMB", "chargingCondition": "", "factors": [ { "serialNumber": 1, "factor": "分区" }, { "serialNumber": 2, "factor": "重量" } ], "details": [ { "factorValuesJson": { "分区": "美国西部", "重量": "0-9999" }, "calculationFormula": "续单价", "calculatedValue": 0, "customerGroupValues": null, "continuePricing": { "firstWeightKg": 1, "firstPrice": 18, "continueUnitKg": 0.5, "continueUnitPrice": 6, "roundingUnitKg": 0.5 } } ] } ], "checksum": "" } ``` 说明: - 为了支持“**首重/续重/进位**”的可落地配置,当 `calculationFormula = 续单价` 时,使用 `continuePricing` 描述完整计费参数(不再依赖额外表字段)。 - `roundingUnitKg` 表示计价侧进位值(如 0.5kg)。若同时存在规则侧进位(`rules` 快照里的 `weightRounding`),**计费重进位以规则侧为准**,`roundingUnitKg` 用于价格项自身的续重单位计算兜底(两者一般配置一致)。 #### 5.2.7 服务渠道组表 `t_service_channel_group` | 字段名 | 字段类型 | 长度 | 主键/外键 | 非空 | 说明 | | --- | --- | --- | --- | --- | --- | | Id | BIGINT | - | 主键 | 是 | 自增唯一标识 | | Name | VARCHAR | 128 | - | 是 | 渠道组名称 | | AppId | BIGINT | - | - | 否 | 应用ID(页面新增/编辑服务商时保存) | | ApiServiceProviderId | BIGINT | - | - | 否 | API服务商ID(页面新增/编辑服务商时保存) | | IsDeleted | TINYINT | - | - | 是 | 是否删除:0-否,1-是,默认0 | | CreateUserId | INT | - | - | 是 | 创建人 | | CreateTime | DATETIME | - | - | 是 | 创建时间 | | UpdateUserId | INT | - | - | 否 | 更新人 | | UpdateTime | DATETIME | - | - | 否 | 更新时间 | #### 5.2.8 服务渠道表 `t_service_channel` | 字段名 | 字段类型 | 长度 | 主键/外键 | 非空 | 说明 | | --- | --- | --- | --- | --- | --- | | Id | BIGINT | - | 主键 | 是 | 自增唯一标识 | | GroupId | BIGINT | - | 外键 | 否 | 渠道组ID(可选) | | ChannelCode | VARCHAR | 64 | - | 是 | 渠道编码 | | ChannelName | VARCHAR | 128 | - | 是 | 渠道名称 | | MinTime | INT | - | - | 否 | 最小时效(天) | | MaxTime | INT | - | - | 否 | 最大时效(天) | | ForecastType | VARCHAR | 32 | - | 否 | 预报单号类型(customer/waybill/tracking) | | ForecastCode | VARCHAR | 128 | - | 否 | 预报单号 | | Description | TEXT | - | - | 否 | 描述 | | PreScript | TEXT | - | - | 否 | 入库前脚本 | | IsActive | TINYINT | - | - | 是 | 是否启用 | | IsDeleted | TINYINT | - | - | 是 | 是否删除:0-否,1-是,默认0 | | CreateUserId | INT | - | - | 是 | 创建人 | | CreateTime | DATETIME | - | - | 是 | 创建时间 | | UpdateUserId | INT | - | - | 否 | 更新人 | | UpdateTime | DATETIME | - | - | 否 | 更新时间 | #### 5.2.9 渠道买价表 `t_service_channel_price`(子表,关联 t_service_channel) > 渠道买价主表,每个渠道可配置多条买价项(运费、保险费、燃油附加费等)。 | 字段名 | 字段类型 | 长度 | 主键/外键 | 非空 | 说明 | | --- | --- | --- | --- | --- | --- | | Id | BIGINT | - | 主键 | 是 | 自增唯一标识 | | ServiceChannelId | BIGINT | - | 外键 | 是 | 服务渠道ID | | SerialNumber | INT | - | - | 是 | 序号 | | Status | TINYINT | - | - | 是 | 状态:0-禁用,1-启用 | | PriceName | VARCHAR | 64 | - | 是 | 价格名称 | | CostItem | VARCHAR | 64 | - | 是 | 费用项 | | CostUnit | VARCHAR | 16 | - | 是 | 费用单位:kg/票/件 | | Currency | VARCHAR | 16 | - | 是 | 币种:RMB/USD/EUR | | ChargingCondition | VARCHAR | 64 | - | 否 | 收费条件 | | IsDeleted | TINYINT | - | - | 是 | 是否删除:0-否,1-是,默认0 | | CreateUserId | INT | - | - | 是 | 创建人 | | CreateTime | DATETIME | - | - | 是 | 创建时间 | | UpdateUserId | INT | - | - | 否 | 更新人 | | UpdateTime | DATETIME | - | - | 否 | 更新时间 | #### 5.2.10 渠道买价-计费因素表 `t_service_channel_price_factor`(子表,关联 t_service_channel_price) > 弹窗「基础信息」中的计费因素配置。 | 字段名 | 字段类型 | 长度 | 主键/外键 | 非空 | 说明 | | --- | --- | --- | --- | --- | --- | | Id | BIGINT | - | 主键 | 是 | 自增唯一标识 | | PriceId | BIGINT | - | 外键 | 是 | 渠道买价ID | | SerialNumber | INT | - | - | 是 | 序号 | | Factor | VARCHAR | 64 | - | 是 | 因素:货物类型/分区/重量/仓库/客户 | | FactorValues | JSON | - | - | 否 | 因素值数组 | | IsDeleted | TINYINT | - | - | 是 | 是否删除:0-否,1-是,默认0 | | CreateUserId | INT | - | - | 是 | 创建人 | | CreateTime | DATETIME | - | - | 是 | 创建时间 | | UpdateUserId | INT | - | - | 否 | 更新人 | | UpdateTime | DATETIME | - | - | 否 | 更新时间 | #### 5.2.11 渠道买价-价格表明细表 `t_service_channel_price_detail`(子表,关联 t_service_channel_price) > 弹窗「价格表」中的价格明细。 | 字段名 | 字段类型 | 长度 | 主键/外键 | 非空 | 说明 | | --- | --- | --- | --- | --- | --- | | Id | BIGINT | - | 主键 | 是 | 自增唯一标识 | | PriceId | BIGINT | - | 外键 | 是 | 渠道买价ID | | FactorValuesJson | JSON | - | - | 否 | 非客户因素组合 | | CalculationFormula | VARCHAR | 32 | - | 是 | 计算公式:总价/单价/续单价 | | CalculatedValue | DECIMAL | 18,4 | - | 否 | 计算值 | | CustomerGroupValues | JSON | - | - | 否 | 客户组对应价格 | | IsDeleted | TINYINT | - | - | 是 | 是否删除:0-否,1-是,默认0 | | CreateUserId | INT | - | - | 是 | 创建人 | | CreateTime | DATETIME | - | - | 是 | 创建时间 | | UpdateUserId | INT | - | - | 否 | 更新人 | | UpdateTime | DATETIME | - | - | 否 | 更新时间 | #### 5.2.12 服务渠道发布记录表 `t_service_channel_publish_record`(子表,关联 t_service_channel) > 买价与规则的发布历史。 | 字段名 | 字段类型 | 长度 | 主键/外键 | 非空 | 说明 | | --- | --- | --- | --- | --- | --- | | Id | BIGINT | - | 主键 | 是 | 自增唯一标识 | | ServiceChannelId | BIGINT | - | 外键 | 是 | 服务渠道ID | | Version | VARCHAR | 32 | - | 是 | 版本号 | | OperatorType | VARCHAR | 16 | - | 是 | 操作类型:price(价格)/rules(规则) | | Status | VARCHAR | 16 | - | 是 | 状态:生效/回滚/无效 | | PublishUser | VARCHAR | 64 | - | 是 | 发布人 | | PublishTime | DATETIME | - | - | 是 | 发布时间 | | Description | VARCHAR | 512 | - | 否 | 版本备注 | | Content | JSON | - | - | 否 | 发布内容JSON快照 | | IsDeleted | TINYINT | - | - | 是 | 是否删除:0-否,1-是,默认0 | | CreateUserId | INT | - | - | 是 | 创建人 | | CreateTime | DATETIME | - | - | 是 | 创建时间 | | UpdateUserId | INT | - | - | 否 | 更新人 | | UpdateTime | DATETIME | - | - | 否 | 更新时间 | #### 5.2.13 分区表(黑名单区/偏远分区/价格分区共用)`t_kpi_area` > 三类分区共用本表,仅通过 `AreaType` 区分业务含义。 | 字段名 | 字段类型 | 长度 | 主键/外键 | 非空 | 说明 | | --- | --- | --- | --- | --- | --- | | Id | BIGINT | - | 主键 | 是 | 自增唯一标识 | | AreaType | VARCHAR | 32 | - | 是 | 类型: blacklist/remote/price | | Name | VARCHAR | 128 | - | 是 | 分区名称 | | SystemName | VARCHAR | 128 | - | 否 | 系统名称 | | IsDeleted | TINYINT | - | - | 是 | 是否删除:0-否,1-是,默认0 | | CreateUserId | INT | - | - | 是 | 创建人 | | CreateTime | DATETIME | - | - | 是 | 创建时间 | | UpdateUserId | INT | - | - | 否 | 更新人 | | UpdateTime | DATETIME | - | - | 否 | 更新时间 | **AreaType 枚举**:blacklist(黑名单区)、remote(偏远分区)、price(价格分区) #### 5.2.14 分区明细表 `t_kpi_area_info` | 字段名 | 字段类型 | 长度 | 主键/外键 | 非空 | 说明 | | --- | --- | --- | --- | --- | --- | | Id | BIGINT | - | 主键 | 是 | 自增唯一标识 | | AreaId | BIGINT | - | 外键 | 是 | 分区ID | | CountryCode | VARCHAR | 8 | - | 否 | 国家二字码 | | CountryName | VARCHAR | 64 | - | 否 | 国家名称 | | Province | VARCHAR | 128 | - | 否 | 省份 | | City | VARCHAR | 128 | - | 否 | 城市 | | LeftPostcode | VARCHAR | 32 | - | 否 | 左侧邮编 | | RightPostcode | VARCHAR | 32 | - | 否 | 右侧邮编 | | IsDeleted | TINYINT | - | - | 是 | 是否删除:0-否,1-是,默认0 | | CreateUserId | INT | - | - | 是 | 创建人 | | CreateTime | DATETIME | - | - | 是 | 创建时间 | | UpdateUserId | INT | - | - | 否 | 更新人 | | UpdateTime | DATETIME | - | - | 否 | 更新时间 | #### 5.2.15 计重规则表 `t_weight_rule` | 字段名 | 字段类型 | 长度 | 主键/外键 | 非空 | 说明 | | --- | --- | --- | --- | --- | --- | | Id | BIGINT | - | 主键 | 是 | 自增唯一标识 | | Name | VARCHAR | 128 | - | 是 | 规则名称 | | IsDeleted | TINYINT | - | - | 是 | 是否删除:0-否,1-是,默认0 | | CreateUserId | INT | - | - | 是 | 创建人 | | CreateTime | DATETIME | - | - | 是 | 创建时间 | | UpdateUserId | INT | - | - | 否 | 更新人 | | UpdateTime | DATETIME | - | - | 否 | 更新时间 | #### 5.2.16 计重规则明细表 `t_weight_rule_info`(子表,关联 t_weight_rule) > 计重规则子页面,仅配置**条件表达式**、**计费重取值**、**材积系数**;不再使用「客户/客户组」列。同一 `RuleId` 下多行按 **`Id` 升序**逐条尝试,**第一条**使条件为真的明细生效(算法见 §4.4.3.1 步骤 7)。运行时通过本单绑定 **`t_sales_product.WeightRuleId`** 选用对应 `t_weight_rule`(不同销售产品可绑定不同规则)。 | 字段名 | 字段类型 | 长度 | 主键/外键 | 非空 | 说明 | | --- | --- | --- | --- | --- | --- | | Id | BIGINT | - | 主键 | 是 | 自增唯一标识 | | RuleId | BIGINT | - | 外键 | 是 | 计重规则ID | | Condition | VARCHAR | 256 | - | 是 | 条件表达式(如:gw>vw) | | BillableWeight | VARCHAR | 64 | - | 是 | 计费重取值(如:vw、gw) | | VolumetricFactor | VARCHAR | 32 | - | 是 | 材积系数(如:6000) | | IsDeleted | TINYINT | - | - | 是 | 是否删除:0-否,1-是,默认0 | | CreateUserId | INT | - | - | 是 | 创建人 | | CreateTime | DATETIME | - | - | 是 | 创建时间 | | UpdateUserId | INT | - | - | 否 | 更新人 | | UpdateTime | DATETIME | - | - | 否 | 更新时间 | #### 5.2.17 计重进位表 `t_weight_rounding` | 字段名 | 字段类型 | 长度 | 主键/外键 | 非空 | 说明 | | --- | --- | --- | --- | --- | --- | | Id | BIGINT | - | 主键 | 是 | 自增唯一标识 | | Name | VARCHAR | 128 | - | 是 | 进位名称 | | IsDeleted | TINYINT | - | - | 是 | 是否删除:0-否,1-是,默认0 | | CreateUserId | INT | - | - | 是 | 创建人 | | CreateTime | DATETIME | - | - | 是 | 创建时间 | | UpdateUserId | INT | - | - | 否 | 更新人 | | UpdateTime | DATETIME | - | - | 否 | 更新时间 | #### 5.2.18 计重进位明细表 `t_weight_rounding_info`(子表,关联 t_weight_rounding) > 计重进位子页面,按重量区间配置**单一进位值** `Public`(不再按客户组分列)。运行时通过本单绑定 **`t_sales_product.WeightRoundingId`** 选用对应 `t_weight_rounding`(算法见 §4.4.3.1 步骤 8)。 | 字段名 | 字段类型 | 长度 | 主键/外键 | 非空 | 说明 | | --- | --- | --- | --- | --- | --- | | Id | BIGINT | - | 主键 | 是 | 自增唯一标识 | | RoundingId | BIGINT | - | 外键 | 是 | 计重进位ID | | StartValue | DECIMAL | 18,4 | - | 是 | 起始值(kg) | | EndValue | DECIMAL | 18,4 | - | 是 | 结束值(kg) | | Public | VARCHAR | 32 | - | 否 | 进位值(kg,如 0.5 表示按 0.5kg 向上取整) | | IsDeleted | TINYINT | - | - | 是 | 是否删除:0-否,1-是,默认0 | | CreateUserId | INT | - | - | 是 | 创建人 | | CreateTime | DATETIME | - | - | 是 | 创建时间 | | UpdateUserId | INT | - | - | 否 | 更新人 | | UpdateTime | DATETIME | - | - | 否 | 更新时间 | #### 5.2.19 自动触发表 `t_auto_trigger` | 字段名 | 字段类型 | 长度 | 主键/外键 | 非空 | 说明 | | --- | --- | --- | --- | --- | --- | | Id | BIGINT | - | 主键 | 是 | 自增唯一标识 | | Status | TINYINT | - | - | 是 | 是否启用 | | Operation | VARCHAR | 32 | - | 否 | 操作: 拦截/警告/问题件 | | TriggerTiming | VARCHAR | 32 | - | 否 | 触发时机: 下单/入库/出库 | | TriggerCondition | TEXT | - | - | 否 | 触发条件 | | PromptMessage | VARCHAR | 512 | - | 否 | 提示信息 | | Notifier | VARCHAR | 128 | - | 否 | 通知人 | | IsDeleted | TINYINT | - | - | 是 | 是否删除:0-否,1-是,默认0 | | CreateUserId | INT | - | - | 是 | 创建人 | | CreateTime | DATETIME | - | - | 是 | 创建时间 | | UpdateUserId | INT | - | - | 否 | 更新人 | | UpdateTime | DATETIME | - | - | 否 | 更新时间 | #### 5.2.20 标签模板表 `t_label_template` | 字段名 | 字段类型 | 长度 | 主键/外键 | 非空 | 说明 | | --- | --- | --- | --- | --- | --- | | Id | BIGINT | - | 主键 | 是 | 自增唯一标识 | | Name | VARCHAR | 128 | - | 是 | 模板名称 | | TemplateJson | JSON | - | - | 否 | vg-print 模板 JSON | | TemplateType | VARCHAR | 32 | - | 否 | 尾程/客户/入库/出库/sku | | IsDeleted | TINYINT | - | - | 是 | 是否删除:0-否,1-是,默认0 | | CreateUserId | INT | - | - | 是 | 创建人 | | CreateTime | DATETIME | - | - | 是 | 创建时间 | | UpdateUserId | INT | - | - | 否 | 更新人 | | UpdateTime | DATETIME | - | - | 否 | 更新时间 | #### 5.2.21 问题模板表 `t_issue_template` | 字段名 | 字段类型 | 长度 | 主键/外键 | 非空 | 说明 | | --- | --- | --- | --- | --- | --- | | Id | BIGINT | - | 主键 | 是 | 自增唯一标识 | | OrganizationId | CHAR | 36 | 外键 → `organization.Id` | 否 | 归属组织 GUID;**NULL = 平台通用** | | IssueType | VARCHAR | 32 | - | 是 | 问题类型 code,见 §10.3 | | Title | VARCHAR | 128 | - | 是 | 问题标题 | | Content | TEXT | - | - | 是 | 问题内容(支持占位符 `{serviceNo}` 等,二期渲染) | | NotifyMethod | VARCHAR | 64 | - | 是 | 通知方式,逗号分隔:`inApp,email,sms,feishu,wechat` | | Status | TINYINT | - | - | 是 | 是否启用:0-否,1-是 | | IsDeleted | TINYINT | - | - | 是 | 是否删除:0-否,1-是,默认0 | | ConcurrencyStamp | CHAR | 36 | - | 是 | 并发戳(AggregateRoot 约定) | | CreateUserId | INT | - | - | 是 | 创建人 | | CreateTime | DATETIME | - | - | 是 | 创建时间 | | UpdateUserId | INT | - | - | 否 | 更新人 | | UpdateTime | DATETIME | - | - | 否 | 更新时间 | **唯一性(业务层)**:同一 `OrganizationId`(含 NULL)+ `IssueType` 仅允许一条未删除记录。 ### 5.3 核心接口设计 > **网关前缀**:生产环境渠道接口挂载为 **`https://api.nanafa.com/admin/channel/...`**;下文路径以 **`/channel/*`** 表示服务内路由。 #### 5.3.0 当前实现总览(oms0414-back) > 实现位置:`Oms.Application/Services/Channels/`。Swagger 分组 **`OMS-Channel`**。渠道接口 **无客户数据隔离**(运营/管理员使用);须 **已登录**(`[Authorize]`)。 ##### 5.3.0.1 公共约定 | 项 | 说明 | | --- | --- | | 分页 | `OmsPageResult` → `{ items, total }`;列表无分页 → `OmsItemsResult` → `{ items }` | | 软删除 | 各 `DeleteAsync` 走仓储软删(`IsDeleted=1`) | | 唯一性 | 销售产品 **`ProductCode`**、服务渠道 **`ChannelCode`**、KPI 分区 **`AppKey+AreaType+Name`**、渠道分组 **`Name`**、标签模板 **`Name+TemplateType`** | | 发布类型 | `operatorType` 仅 **`price`** \| **`rules`**(大小写不敏感,内部归一化小写) | | 版本号 | 发布时 **`Version = yyyyMMddHHmmss`**(字符串);`Status` 固定写 **「生效」** | | Redis | 发布 **仅写 DB** `t_*_publish_record.Content`;**未**同步 Redis / 未通知计费实例(见 **§5.2.6.2** 实现说明) | ##### 5.3.0.2 销售产品(`SalesProductService`) | 接口 | 核心逻辑 | | --- | --- | | `GET .../list` | 筛选:`productGroup`、`keyword`(匹配 Code/Name)、`isActive`;排序 `UpdateTime desc, Id desc` | | `GET .../detail` | 单条;不存在 → **「销售产品不存在」** | | `POST .../create` | 校验 Code 唯一 → `InsertAsync`;`Scheme` 空则默认 **`costLowest`** | | `POST .../update` | 同 create,带 `Id` | | `POST .../delete` | 软删产品 | | `POST .../config/save` | 与 update 相同(`[IgnoreApi]`,供前端配置 Tab 专用路径) | | `GET .../price/list` | 校验产品存在 → 一次查齐价格项 + factors + details(避免 N+1) | | `POST .../price/create` | 插入价格主表 + factor + detail 子表 | | `POST .../price/update` | 校验价格项归属 `SalesProductId` → 全量替换 factor/detail | | `POST .../price/delete` | 删价格项 | | `POST .../price/toggleStatus` | `ToggleStatusInput` 更新启用状态 | | `POST .../price/save` | **`ReplacePricesAsync`** 整树替换(临时负数 Id 关联子表);可选 **`publish=true`** 触发价格发布 | | `GET .../allocation/get` | 返回 `Scheme` + 排货服务商列表(补 **`ServiceChannelName`**) | | `POST .../allocation/save` | 更新产品 `Scheme` + **`ReplaceAllocationProvidersAsync`**;`CustomerIds` 存 JSON;可选 **`publish=true`** 触发规则发布 | | `GET .../publish/price\|rules/list` | 分页发布记录;`Content` 反序列化为 JSON 对象返回 | | `GET .../publish/preview` | **不落库**预览快照:`BuildPricePublishSnapshotAsync` / `BuildRulesPublishSnapshotAsync` | | `POST .../publish` | `CreatePublishRecordAsync`:组装快照 JSON 写入发布表 | | `POST .../publish/rollback` | 读指定 `recordId` 的 `Content`:**price** → `SavePriceAsync` 恢复价格树;**rules** → 更新产品配置 + 恢复排货 providers | **规则快照(`operatorType=rules`)组装内容**(`BuildRulesPublishSnapshotAsync`): - 产品基础字段(含 `BlacklistAreaId`、`RemoteAreaId`、`PriceAreaId`、`WeightRuleId`、`WeightRoundingId` 等) - 排货 **`Providers`**(**不含** `CreateTime`/`UpdateTime`,与卖价 Slim 策略一致) - 通过 `RulesSnapshotHelper` 嵌入:**黑名单/偏远/价格分区明细**、**计重规则明细**、**进位规则明细** - 发布前由 **`AllocationProviderSnapshotEnricher`** 对每个 Provider(有 **`ServiceChannelId`**)补充 **下游对接元数据**(见下表) **`Providers[]` 下游 enrich 字段**(写入 `t_sales_product_publish_record.Content`,供排货/轨迹/下游预报读取): | 字段 | 来源 | 说明 | | --- | --- | --- | | `channelCode` | `t_service_channel.ChannelCode` | 服务渠道编码 | | `forecastCode` | `t_service_channel.ForecastCode` | 渠道预报编码(轨迹 **ApiCode**) | | `groupId` | `t_service_channel.GroupId` | 服务商分组 Id | | `appId` | `t_service_channel_group.AppId` | itfx 应用 Id | | `apiServiceProviderId` | `t_service_channel_group.ApiServiceProviderId` | itfx API 服务商 Id | | `appKey` | itfx **`GET /App/GetInfo?id={appId}`** | 应用 AppKey | | `supplierCode` | itfx **`GET /ServiceProvider/GetServiceProvider?serviceProviderId=`** | API 服务商编码 | **安全与兼容约定**: - **`AppSecret` 不落快照、不落 `t_order`**;运行时按 **`appId` / `downstreamAppId`** 调 itfx App API 获取。 - itfx 元数据客户端配置:**`Oms:Itfx:BaseUrl`**(默认 `http://api.nanafa.com/itfx`),转发当前请求 **JWT Authorization**。 - **旧版 rules 快照**(无上述字段):排货时 **`ResolveAllocationProvidersAsync`** 按需 live enrich;确认渠道后仍写入 **`t_order`** 四字段(**`sql/migrate_t_order_downstream_integration.sql`**)。 **价格快照(`operatorType=price`)组装内容**(`BuildPricePublishSnapshotAsync`): - 全部卖价项(Slim 后)+ factors + details - 通过 `PriceAreaSnapshotHelper` 嵌入 **`PriceAreaInfos`**(价格分区明细,供运行时匹配 zone key) ##### 5.3.0.3 服务渠道(`ServiceChannelService`) 与销售产品 **对称**,差异要点: | 项 | 说明 | | --- | --- | | 列表筛选 | `groupId`、`keyword`(Code/Name)、`isActive`;列表/详情补 **`groupName`** | | 分组 | `group/list` 返回 **数组**(非分页);筛选 `keyword`、`appId`、`apiServiceProviderId` | | 分组删除 | 分组下仍有渠道 → **「当前分组下仍有关联渠道,无法删除」** | | 分组校验 | `Name`、`AppId`、`ApiServiceProviderId` 必填;名称唯一 | | `config/save` | 同 update(`[IgnoreApi]`) | | 买价 | 与卖价相同 CRUD + **`price/save`** 整树替换 + 可选发布 | | 发布/回滚 | 写 **`t_service_channel_publish_record`**;**price** 回滚恢复买价树;**rules** 回滚恢复渠道主配置 | | 发布预览 | **`GET .../publish/preview`**(与卖价对称,2026-06 已落地) | ##### 5.3.0.4 KPI 分区(`KpiAreaService`) | 接口 | 核心逻辑 | | --- | --- | | `POST .../list` | 筛选 `appKey`、`areaType`(**blacklist / remote / price**,见 §10.2) | | `POST .../create\|update` | 同 **`AppKey+AreaType+Name`** 唯一;update 校验存在 | | `POST .../delete` | 软删主表 | | `POST .../info/list` | 按 `areaId` 分页明细 | | `POST .../info/add\|update\|delete` | 明细 CRUD:国家码、省市区、邮编区间 `LeftPostcode`~`RightPostcode` | | `GET .../info/import-template` | 下载 Excel 模板(`KpiAreaInfoImportHelper`) | | `POST .../info/import` | multipart:`areaId` + file;逐行解析 → 校验国家码(对照 `t_country`)→ 插入;部分失败返回 **`successCount/failCount/failures[]`** | ##### 5.3.0.5 计重规则 / 进位(`WeightRuleService`、`WeightRoundingService`) 标准 **主表 + info 子表** CRUD(均为 **POST** 路由): - 主表:list(分页,可按 keyword 筛)、create、update、delete - 子表:info/list(按 `ruleId` 或 `roundingId` 分页)、info/add、info/update、info/delete - 计重明细字段:`VolumetricFactor`、`Condition`、`BillableWeight`(gw/vw) - 进位明细字段:`StartValue`、`EndValue`、`Public`(进位单位 kg) 运行时算价见订单 **§4.3.5**(按 `Id` 升序匹配首条计重条件;进位 `start <= raw <= end`)。 ##### 5.3.0.6 自动触发(`AutoTriggerService`) | 接口 | 核心逻辑 | | --- | --- | | `GET .../list` | 分页;筛选 `operation`、`triggerTiming` | | `POST .../create\|update` | 字段:`Status`、`Operation`、`TriggerTiming`、`TriggerCondition`、`PromptMessage`、`Notifier` | | `POST .../delete` | 软删 | | `POST .../toggleStatus` | 更新 `Status` | > **运行时(现状)**:`AutoTriggerRuntimeService` **已部分落地**(见 **§9.4.5**):预报成功 / 入库 / 出库后异步执行;条件匹配器仅支持 **关键字子串** 与 **少量 JSON 等值**,**不支持** 表达式、重量尺寸、客户名称等字段;**拦截** 为预报成功后写 `IsIntercept=1`,**不能阻止下单**。 > > **目标(V2 设计稿)**:见 **§5.3.0.6.1**,将自动触发升级为 **订单校验与限制规则引擎**——结构化条件、全量订单字段、比较/集合/逻辑运算符;**预报时机 + 拦截** 在提交前 **同步拒绝** 并返回 `promptMessage`。 ##### 5.3.0.6.1 自动触发规则引擎 V2(设计稿 · 订单校验与限制) > **状态**:设计稿(2026-07-09),**尚未开发**。本节为 `/channel/autoTrigger` 页面与 `AutoTriggerRuntimeService` 的下一版目标设计,服务于 **订单校验、限制下单、自动预警、自动问题件**。 > > **业务目标**:运营可在渠道模块配置规则,在 **预报 / 入库 / 出库** 等节点,按 **订单全量相关字段**(主表、件重尺、收发件人、申报内件、客户、产品等)组合条件;动作为 **拦截(禁止继续)**、**警告(通知)**、**问题件(走问题模板)**。 ###### 1)现状与差距 | 维度 | 现状(代码) | V2 目标 | | --- | --- | --- | | 配置页 | 触发时机/条件为 **纯文本**;操作三选一硬编码 | 时机/操作走字典;**可视化条件构建器**(字段 + 运算符 + 值) | | 条件语法 | 关键字子串;JSON 仅 `orderStatus/productCode/channelCode/keyword` 等值 | **结构化 AST**(AND/OR/NOT 分组 + 比较表达式) | | 可用字段 | 运单号、客户单号、跟踪号、产品/渠道编码 | **订单域全字段**(见 §5.3.0.6.1.4) | | 运算符 | 无(仅 contains 式关键字) | `>`、`>=`、`<`、`<=`、`=`、`!=`、`between`、`in`、`notIn`、`contains`、`notContains`、`isEmpty`… | | 预报 + 拦截 | 预报 **成功后** 写 `IsIntercept=1` | **预报提交前同步校验**;命中 **拦截** → **拒绝下单/预报**,返回 `promptMessage` | | 尺寸/重量 | 未读取 `t_order_weight`、内件 | 支持 **按件重尺类型**(forecast/receive/ship/downstream)及 **内件聚合** | | 客户 | 未参与匹配 | 支持 `customer.name` / `customer.code` / `customer.group` 等 | | 规则优先级 | 全部启用规则依次执行 | 增加 `Priority`(数字越小越先);**拦截** 命中即短路 | ###### 2)核心概念 **触发时机(TriggerTiming)** — 字典 `auto_trigger_timing`(扩展): | dictCode | 名称 | 挂钩点 | 执行方式 | | --- | --- | --- | --- | | `forecast` | 预报 | `POST /order/parcel/create`、批量下单、Excel 导入(均走 `CreateParcelCoreAsync`) | **同步**(提交前) | | `inbound` | 入库 | `POST /operation/inbound/create` | 异步(扫描成功后) | | `outbound` | 出库 | `POST /operation/outbound/create` | 异步(扫描成功后) | > 兼容:旧数据 `下单`/`order_create`/`预报` 等文本,运行时归一化为 `forecast`。 **操作(Operation)** — 字典 `auto_trigger_operation`: | dictCode | 名称 | 预报时机行为 | 入库/出库时机行为 | | --- | --- | --- | --- | | `intercept` | 拦截 | **拒绝**本次下单/预报,`HTTP` 业务错误文案 = `PromptMessage` | 订单已存在 → 设 `IsIntercept=1` | | `warn` | 警告 | 允许成功,发站内信(+ 模板占位符) | 同左 | | `problem` | 问题件 | 允许成功,按 `issueType` 匹配问题模板并标记 | 同左 | **两类执行阶段**: ```mermaid flowchart LR A[请求进入] --> B{时机=forecast?} B -->|是| C[PreCommit 同步评估] C -->|命中 intercept| D[抛 BusinessException\nPromptMessage] C -->|未命中或 warn/problem| E[继续预报主流程] E --> F[预报成功] F --> G[PostCommit 异步评估\nwarn / problem / 入库出库 intercept] ``` - **PreCommit(同步校验)**:仅 `forecast`;用于 **拦截下单**;必须在 `OrderRepository.InsertAsync` **之前**(或至少在写 `predeclared` 之前)调用。 - **PostCommit(异步处置)**:现有 `AutoTriggerRuntimeService.FireAsync` 演进;用于警告、问题件、以及入库/出库拦截标记。 ###### 3)示例规则(超尺寸 · 英国 · 指定客户 · 排除产品) **业务描述**(运营可读): - **触发时机**:预报 - **触发条件**: - (单件最大长度 > 60 **或** 单件最大宽度 > 46 **或** 单件长+宽+高 > 150) - **且** 收件国家包含 `GB` - **且** 客户名称包含 `阿里国际站/深圳一达通` - **且** 销售产品 **不包含** `EX_ASP_HKDHL_YD` - **操作**:拦截 - **提示语**:`{serviceNo}超尺寸,禁止下单`(预报前尚无运单号时可用固定文案:`超尺寸,禁止下单`) **结构化条件(`TriggerCondition` JSON · version=2)**: ```json { "version": 2, "logic": "and", "children": [ { "logic": "or", "children": [ { "field": "piece.maxLength", "op": "gt", "value": 60 }, { "field": "piece.maxWidth", "op": "gt", "value": 46 }, { "field": "piece.sumLwh", "op": "gt", "value": 150 } ] }, { "field": "receiver.countryCode", "op": "in", "value": ["GB"] }, { "field": "customer.name", "op": "contains", "value": "阿里国际站/深圳一达通" }, { "field": "productCode", "op": "notIn", "value": ["EX_ASP_HKDHL_YD"] } ] } ``` **字段取值说明(本例)**: | 字段 token | 含义 | 数据来源(预报 PreCommit) | | --- | --- | --- | | `piece.maxLength` | 单件最大长度(cm) | `max(包裹预报长宽高, 各内件对应尺寸)`;单包裹场景 = `input.Length` | | `piece.maxWidth` | 单件最大宽度(cm) | 同上 → `input.Width` | | `piece.sumLwh` | 单件长+宽+高(cm) | `Length + Width + Height`(按件取 max 后再比) | | `receiver.countryCode` | 收件国家二字码 | `input.Receiver.CountryCode` 归一化后 | | `customer.name` | 客户名称 | `t_customer.CustomerName`(`input.CustomerId` 解析) | | `productCode` | 销售产品编码 | `input.ProductCode` | ###### 4)条件表达式模型 **叶子节点(比较)**: ```json { "field": "forecast.chargeableWeight", "op": "gt", "value": 30 } ``` | 属性 | 说明 | | --- | --- | | `field` | 字段 token,见 §5.3.0.6.1.5 | | `op` | 运算符 code,见 §5.3.0.6.1.6 | | `value` | 标量 / 数组 / 区间 `{ "min": 1, "max": 10 }`(`between`) | **分组节点**: ```json { "logic": "and|or", "children": [ /* 叶子或分组 */ ] } ``` **否定(可选)**: ```json { "logic": "not", "children": [ { "field": "...", "op": "...", "value": "..." } ] } ``` **存储**:继续复用 `t_auto_trigger.TriggerCondition`(TEXT),内容为 **JSON**;`version: 2` 表示新引擎。`version` 缺失或 `version: 1`/纯文本 → 走 **兼容适配器**(映射到现有关键字匹配器,逐步迁移)。 ###### 5)运算符一览 | op | 名称 | 适用类型 | 说明 | | --- | --- | --- | --- | | `eq` | 等于 | 字符串/数字/布尔 | | | `ne` | 不等于 | 同上 | | | `gt` | 大于 | 数字 | | | `gte` | 大于等于 | 数字 | | | `lt` | 小于 | 数字 | | | `lte` | 小于等于 | 数字 | | | `between` | 区间内 | 数字 | `value: { min, max }` 闭区间 | | `in` | 在集合中 | 字符串/数字 | `value: []` | | `notIn` | 不在集合中 | 字符串/数字 | | | `contains` | 包含 | 字符串 | 子串,忽略大小写 | | `notContains` | 不包含 | 字符串 | | | `startsWith` | 前缀 | 字符串 | | | `endsWith` | 后缀 | 字符串 | | | `isEmpty` | 为空 | 任意 | 无需 value | | `isNotEmpty` | 不为空 | 任意 | | > 页面按字段 **值类型** 过滤可选运算符(如数字字段不展示 `contains`)。 ###### 6)订单相关字段目录(规则引擎可用 token) > 下列为 **V2 一期** 拟支持字段;页面左侧按分组展示,右侧选运算符与值。带 `[]` 表示内件聚合。 **A. 订单主表 `order.*`** | token | 说明 | | --- | --- | | `order.serviceNo` | 运单号 | | `order.clientNo` | 客户单号 | | `order.trackNo` | 跟踪号 | | `order.productCode` | 销售产品编码 | | `order.channelCode` | 服务渠道编码 | | `order.orderStatus` | 订单状态 | | `order.cargoAttribute` | 货物属性 | | `order.exportCustomsMode` | 出口报关方式 | | `order.warehouseId` | 仓库 ID | | `order.quantity` | 件数 | | `order.declaredCurrency` / `order.declaredSum` | 申报币种/总金额 | | `order.isCod` / `order.codCurrency` / `order.codSum` | COD | | `order.isIntercept` / `order.isRemote` / `order.isPaid` | 标志位 | | `order.consigneeCountry` / `order.consigneeCountryName` | 收件国家码/名 | | `order.salesProductRulesVersion` / `order.salesProductPriceVersion` | 绑定版本 | | `order.predeclareFailReason` | 预报失败原因 | **B. 件重尺 `forecast.*` / `receive.*` / `ship.*` / `downstream.*`** | token | 说明 | | --- | --- | | `{type}.length` / `width` / `height` | 长/宽/高(cm) | | `{type}.actualWeight` | 实重(kg) | | `{type}.volumetricWeight` | 体积重(kg) | | `{type}.chargeableWeight` | 计费重(kg) | | `{type}.cubic` | 体积 | | `{type}.sumLwh` | 长+宽+高 | `{type}` ∈ `forecast`(预报,**PreCommit 默认**)、`receive`(收货)、`ship`(发货)、`downstream`(下游)。 **C. 包裹/单件聚合 `piece.*`(预报常用)** | token | 说明 | | --- | --- | | `piece.maxLength` / `piece.maxWidth` / `piece.maxHeight` | 单件最大长宽高 | | `piece.sumLwh` | 单件长+宽+高(对当前包裹) | | `piece.maxChargeableWeight` | 单件最大计费重 | **D. 收件人 `receiver.*` → `t_order_recipient` / 下单入参** `recipientsName`, `companyName`, `phone`, `telePhone`, `email`, `countryCode`, `province`, `city`, `zipCode`, `streetNumber`, `streetAddress1~3`, `taxType`, `taxNo`, `certificateType`, `certificateNo`, `eori`, `vat` **E. 发件人 `sender.*` → `t_order_sender` / 下单入参** `senderName`, `company`, `phone`, `telePhone`, `email`, `countryCode`, `province`, `city`, `zipCode`, `streetAddress1~3`, `houseNumber` **F. 客户 `customer.*` → `t_customer`** | token | 说明 | | --- | --- | | `customer.id` | 客户 ID | | `customer.code` | 客户编码 | | `customer.name` | 客户名称(**本例「客户包含」**) | | `customer.group` | 客户组 | | `customer.phone` / `customer.email` | 联系方式 | | `customer.salesman` / `customer.customerService` | 销售/客服 | **G. 销售产品 `product.*` → `t_sales_product`(按 `productCode` 关联)** `product.code`, `product.name`, `product.group`, `product.minTimeliness`, `product.maxTimeliness` **H. 申报内件聚合 `items.*`** | token | 说明 | | --- | --- | | `items.count` | 内件行数 | | `items.totalQuantity` | 数量合计 | | `items.totalWeight` / `items.totalAmount` | 重量/金额合计 | | `items.maxUnitWeight` | 最大单重 | | `items.anyCnName` / `items.anyEnName` | 任一品名包含(需配合 `contains`) | | `items.anySku` / `items.anyHsCode` | SKU/税号 | | `items.anyIsMagnetic` | 是否存在带磁内件 | **I. 费用快照(二期,可选)** `fee.receivable.total` / `fee.payable.total` — 当前阶段应收/应付合计。 ###### 7)提示语占位符 `PromptMessage` 支持 `IssuePlaceholderRenderer` 同款占位符,另增尺寸类: | 占位符 | 说明 | | --- | --- | | `{serviceNo}` / `{运单号}` | 运单号(PreCommit 未生成时可空) | | `{clientNo}` / `{客户单号}` | 客户单号 | | `{productCode}` / `{产品代码}` | 销售产品 | | `{receiverCountry}` / `{收件国家}` | 国家码 | | `{pieceMaxLength}` 等 | 触发时计算的聚合值 | 本例落地文案:**`超尺寸,禁止下单`** 或 **`{clientNo}超尺寸,禁止下单`**。 ###### 8)配置页 `/channel/autoTrigger` UI 设计 **列表页(保持 TablePro/Handsontable)**,列增加:`规则名称`、`优先级`、条件摘要(人类可读)、启用状态。 **新增/编辑弹窗**: ``` ┌─────────────────────────────────────────────────────────┐ │ 规则名称 * [ 英国阿里超尺寸拦截 ] │ │ 状态 (•) 启用 ( ) 禁用 │ │ 触发时机 * [ 预报 ▼ ] (字典 auto_trigger_timing)│ │ 操作 * [ 拦截 ▼ ] (字典 auto_trigger_operation)│ │ 优先级 [ 100 ] 数字越小越先执行 │ ├─────────────────────────────────────────────────────────┤ │ 触发条件 * [+ 添加条件组] │ │ ┌ 满足 [全部 AND ▼] ─────────────────────────────────┐ │ │ │ ┌ 满足 [任一 OR ▼] ───────────────────────────────┐ │ │ │ │ │ [单件最大长度 ▼] [大于 ▼] [ 60 ] [删除] │ │ │ │ │ │ [单件最大宽度 ▼] [大于 ▼] [ 46 ] [删除] │ │ │ │ │ │ [单件长+宽+高 ▼] [大于 ▼] [ 150 ] [删除] │ │ │ │ │ └ [+ 添加条件] │ │ │ │ │ [收件国家 ▼] [在列表中 ▼] [ GB (+) ] [删除]│ │ │ │ [客户名称 ▼] [包含 ▼] [ 阿里国际站/深圳一达通 ] │ │ │ │ [销售产品 ▼] [不在列表 ▼] [ EX_ASP_HKDHL_YD ] │ │ │ └ [+ 添加条件组] │ │ ├─────────────────────────────────────────────────────────┤ │ 提示语 * [ 超尺寸,禁止下单 ] │ │ 通知人 [ 用户多选,仅警告/问题件 ] │ └─────────────────────────────────────────────────────────┘ ``` **交互要点**: - 字段下拉分组:订单 / 件重尺 / 收件人 / 发件人 / 客户 / 产品 / 内件聚合。 - 运算符随字段类型变化;`in`/`notIn` 用多选或标签输入。 - 底部 **「预览 JSON」** 只读(给研发调试);运营默认不展开。 - 保存时前端 `condition` 序列化为 §4 JSON,`TriggerTiming`/`Operation` 存 **dictCode**(`forecast`/`intercept`)。 ###### 9)表结构与接口变更(建议) **`t_auto_trigger` 增字段(迁移脚本)**: | 字段 | 类型 | 说明 | | --- | --- | --- | | `RuleName` | VARCHAR(128) | 规则名称 | | `Priority` | INT | 优先级,默认 100 | | `ConditionVersion` | TINYINT | 条件版本,默认 2 | **新增接口(可选)**: | 方法 | 路径 | 说明 | | --- | --- | --- | | GET | `/channel/auto-trigger/meta` | 返回字段目录、运算符、时机/操作字典(供构建器) | | POST | `/channel/auto-trigger/test` | 传入样例订单 JSON + 规则 ID,返回是否命中(运营调试) | CRUD 入参/出参增加 `ruleName`、`priority`;`triggerCondition` 存 JSON。 ###### 10)后端评估器设计 **新组件**: | 类 | 职责 | | --- | --- | | `OrderRuleEvaluationContext` | 聚合 `OrderParcelCreateInput`(PreCommit)或 `Order`+子表(PostCommit) | | `OrderRuleFieldResolver` | token → 强类型值 | | `OrderRuleConditionEvaluator` | 解析 JSON AST,递归求值 | | `OrderAutoTriggerService` | 加载启用规则 → 排序 → PreCommit/PostCommit 分流 | **PreCommit 调用点**(`OrderService.ParcelBilling.CreateParcelCoreAsync`): ``` ValidateParcelCreateInputAsync → 销售产品/黑名单等硬编码校验 → OrderAutoTriggerService.EvaluatePreCommitAsync(input, timing=forecast) // 新增 → InsertAsync(order) ... → ... 预报成功 ... → OrderAutoTriggerService.EvaluatePostCommitAsync(order, timing=forecast) // 替代原 FireAsync 或内部转调 ``` **拦截短路**:PreCommit 命中 `intercept` → `throw new BusinessException(promptMessage)`;批量下单逐行失败,错误写入任务明细。 **PostCommit**:保留警告/问题件/入库出库拦截标记;失败 **仅记日志**,不影响主流程(与现网一致)。 ###### 11)与硬编码校验的关系 | 机制 | 定位 | | --- | --- | | 代码内校验(产品存在、黑名单、分区匹配等) | **系统不变量**,不可由运营关闭 | | 自动触发 V2 | **可配置业务规则**,运营可启停;优先级低于安全类硬编码 | 黑名单(KPI 分区)与自动触发 **并存**;均命中时 **先执行的** 抛出错误(PreCommit 顺序:硬编码 → 自动触发按 `Priority`)。 ###### 12)实施分期 | 阶段 | 范围 | | --- | --- | | **P0** | 表字段 + 条件 JSON 存储 + `OrderRuleConditionEvaluator` + 预报 PreCommit 拦截 + 字段目录 API | | **P1** | 配置页可视化构建器 + 字典化时机/操作 + 本例超尺寸规则可配置 | | **P2** | 入库/出库字段(`receive.*`/`ship.*`)+ 测试接口 + 条件人类可读摘要 | | **P3** | 费用字段、正则、规则命中审计日志 `t_auto_trigger_hit_log` | ###### 13)验收标准(本例) 1. 在 `/channel/autoTrigger` 配置 §3 示例规则并启用。 2. 预报一单:GB、客户名含「阿里国际站/深圳一达通」、尺寸 61×40×40、产品非 `EX_ASP_HKDHL_YD` → **接口失败**,文案「超尺寸,禁止下单」,**不产生** `predeclared` 订单。 3. 将产品改为 `EX_ASP_HKDHL_YD` → **放行**。 4. 尺寸改为 50×40×40 → **放行**。 --- ##### 5.3.0.7 问题模板(`IssueTemplateService`) | 接口 | 核心逻辑 | | --- | --- | | `GET .../list` | 分页;筛选 `organizationId`、`issueType`、`keyword`、`status` | | `GET .../detail` | 单条详情 | | `GET .../options` | 启用模板下拉;组织专属优先于平台通用 | | `POST .../create\|update` | 字段:`OrganizationId`、`IssueType`、`Title`、`Content`、`NotifyMethod`、`Status`;**OrganizationId+IssueType 唯一** | | `POST .../delete` | 软删 | | `POST .../toggleStatus` | 更新 `Status` | > **运行时消费**(**§4.4.1.13** 标记问题件发通知、自动触发关联模板)**二期**;当前为配置 CRUD;**`GET .../options`** 供创建问题件弹窗联动。 ##### 5.3.0.8 标签模板(`LabelTemplateService`) | 接口 | 核心逻辑 | | --- | --- | | `POST .../label-template/save` | `id>0` 更新,否则新增;**`Name+TemplateType` 唯一**;`templateJson` 序列化存库;返回带 `id` 的完整对象 | 下拉供仓内打印见 **`GET /operation/print-label/template-list`**(操作模块)。 ##### 5.3.0.9 文档有而需对照的实现差异 | 设计描述 | 当前实现 | | --- | --- | | 发布后同步 Redis + 刷新计费内存 | **未实现**;下单读 DB 发布表或 Live 组装(§4.3.5) | | 发布时旧版本 `Status` 置失效 | **未显式**更新历史记录状态;算价按 **`PublishTime <= bindTime` 取最新一条** | | 服务渠道 `publish/preview` | 与卖价对称 | **已落地**:`GET /channel/service-channel/publish/preview` | | 自动触发执行 | **部分落地** | `AutoTriggerRuntimeService` 已挂钩;**V2 规则引擎见 §5.3.0.6.1** | #### 5.3.1 销售产品 | 接口名称 | 请求方式 | 请求路径 | 说明 | | --- | --- | --- | --- | | 产品列表 | GET | /channel/sales-product/list | 产品列表 | | 产品详情 | GET | /channel/sales-product/detail | 产品详情 | | 新增产品 | POST | /channel/sales-product/create | 新增产品 | | 更新产品 | POST | /channel/sales-product/update | 更新产品主表 | | 保存产品配置 | POST | /channel/sales-product/config/save | 同 update(前端配置 Tab 专用,Swagger 隐藏) | | 删除产品 | POST | /channel/sales-product/delete | 删除产品 | ##### 5.3.1.1 产品列表(GET /channel/sales-product/list) ###### 1 接口请求示例 **请求 /channel/sales-product/list**(GET,Query 参数) ``` ?page=1&pageSize=20&productGroup=专线&keyword=&isActive=1 ``` **响应** ```json { "code": 0, "msg": "success", "data": { "items": [ { "id": 1, "productCode": "US-AIR-P", "productName": "美国空运专线(普货)", "productGroup": "专线", "minTimeliness": 3, "maxTimeliness": 7, "customerWaybill": "系统面单", "blacklistAreaId": 1, "remoteAreaId": 2, "priceAreaId": 3, "weightRuleId": 1, "weightRoundingId": 1, "productDescription": "美国空运普货专线", "isActive": 1, "scheme": "costLowest", "createTime": "2024-01-01 12:00:00", "updateTime": "2024-01-01 12:00:00" } ], "total": 10 } } ``` ###### 2 接口业务逻辑 ###### 核心流程 - 接收分页与筛选参数后构建查询条件(productGroup、keyword、isActive)。 ###### 校验与异常 - 按分页参数查询销售产品列表,返回 `items` 与 `total`。 ###### 3 当前实现(oms0414-back) - 见 **§5.3.0.2** `GET .../list`。 ##### 5.3.1.2 产品详情(GET /channel/sales-product/detail) ###### 1 接口请求示例 **请求 /channel/sales-product/detail**(GET,Query 参数) ``` ?id=1 ``` **响应** ```json { "code": 0, "msg": "success", "data": { "id": 1, "productCode": "US-AIR-P", "productName": "美国空运专线(普货)", "productGroup": "专线", "minTimeliness": 3, "maxTimeliness": 7, "customerWaybill": "系统面单", "blacklistAreaId": 1, "remoteAreaId": 2, "priceAreaId": 3, "weightRuleId": 1, "weightRoundingId": 1, "productDescription": "美国空运普货专线", "preStorageScript": "", "isActive": 1, "scheme": "costLowest", "createTime": "2024-01-01 12:00:00", "createBy": "admin", "updateTime": "2024-01-01 12:00:00", "updateBy": "admin" } } ``` ###### 2 接口业务逻辑 ###### 核心流程 - 根据 `id` 查询产品主数据及扩展配置,组装详情对象返回。 ###### 校验与异常 - 若产品不存在或不可访问,返回业务错误码。 ##### 5.3.1.3 新增产品(POST /channel/sales-product/create) ###### 1 接口请求示例 **请求 /channel/sales-product/create**(POST) ```json { "productCode": "US-AIR-P", "productName": "美国空运专线(普货)", "productGroup": "专线", "minTimeliness": 3, "maxTimeliness": 7, "customerWaybill": "系统面单", "blacklistAreaId": 1, "remoteAreaId": 2, "priceAreaId": 3, "weightRuleId": 1, "weightRoundingId": 1, "productDescription": "美国空运普货专线", "preStorageScript": "", "isActive": 1, "scheme": "costLowest" } ``` **响应** ```json { "code": 0, "msg": "success", "data": null } ``` ###### 2 接口业务逻辑 ###### 核心流程 - 校验产品编码唯一性与必填字段后新增产品记录。 ###### 校验与异常 - 初始化产品配置与默认状态,写入审计字段。 ##### 5.3.1.4 更新产品(POST /channel/sales-product/update) ###### 1 接口请求示例 **请求 /channel/sales-product/update**(POST) ```json { "id": 1, "productCode": "US-AIR-P", "productName": "美国空运专线(普货)", "productGroup": "专线", "minTimeliness": 3, "maxTimeliness": 7, "customerWaybill": "系统面单", "blacklistAreaId": 1, "remoteAreaId": 2, "priceAreaId": 3, "weightRuleId": 1, "weightRoundingId": 1, "productDescription": "美国空运普货专线", "preStorageScript": "", "isActive": 1, "scheme": "costLowest" } ``` **响应** ```json { "code": 0, "msg": "success", "data": null } ``` ###### 2 接口业务逻辑 ###### 核心流程 - 根据 `id` 校验产品存在性后更新可变字段(包含产品配置 tab 的保存内容)。 ###### 校验与异常 - 记录操作人和更新时间,并触发配置缓存刷新。 ##### 5.3.1.5 删除产品(POST /channel/sales-product/delete) ###### 1 接口请求示例 **请求 /channel/sales-product/delete**(POST) ```json { "id": 1 } ``` **响应** ```json { "code": 0, "msg": "success", "data": null } ``` ###### 2 接口业务逻辑 ###### 核心流程 - 校验产品是否可删除(无有效关联配置/任务)后执行逻辑删除。 ###### 校验与异常 - 更新删除标识及审计字段,避免物理删除带来的历史数据丢失。 #### 5.3.2 产品卖价(子表接口) | 接口名称 | 请求方式 | 请求路径 | 说明 | | --- | --- | --- | --- | | 价格项列表 | GET | /channel/sales-product/price/list | 按产品ID获取价格项列表 | | 新增价格项 | POST | /channel/sales-product/price/create | 新增价格项(含基础信息+计费因素+价格表) | | 更新价格项 | POST | /channel/sales-product/price/update | 更新价格项 | | 删除价格项 | POST | /channel/sales-product/price/delete | 删除价格项 | | 切换价格项状态 | POST | /channel/sales-product/price/toggleStatus | 启用/禁用价格项 | | 批量保存价格项 | POST | /channel/sales-product/price/save | 整树替换卖价;可选 `publish=true` 同步发布 | ##### 5.3.2.1 价格项列表(GET /channel/sales-product/price/list) ###### 1 接口请求示例 **请求 /channel/sales-product/price/list**(GET,Query 参数) ``` ?salesProductId=1 ``` **响应** ```json { "code": 0, "msg": "success", "data": { "items": [ { "id": 1, "salesProductId": 1, "serialNumber": 1, "status": 1, "priceName": "运费", "costItem": "freight", "costUnit": "kg", "currency": "RMB", "chargingCondition": "", "factors": [ { "id": 1, "priceId": 1, "serialNumber": 1, "factor": "货物类型", "factorValues": ["普货", "敏感货"] } ], "details": [ { "id": 1, "priceId": 1, "factorValuesJson": {"分区": "欧洲", "货物类型": "普货", "重量": "0-0.5"}, "calculationFormula": "单价", "calculatedValue": 62, "customerGroupValues": {"客户组1": 60, "客户组2": 65} } ], "version": "20260425143000", "publishTime": "2026-04-25 14:30:00", "createTime": "2024-01-01 12:00:00", "updateTime": "2024-01-01 12:00:00" } ] } } ``` ###### 2 接口业务逻辑 ###### 核心流程 - 按 `salesProductId` 查询价格项主表,并聚合返回计费因素与价格明细子数据。 - 结果按 `serialNumber` 升序组织,便于前端按配置顺序渲染。 ###### 校验与异常 - 校验 `salesProductId` 必填且产品存在;不存在时返回业务错误。 - 仅返回未删除记录,查询异常返回统一错误码。 ##### 5.3.2.2 新增价格项(POST /channel/sales-product/price/create) ###### 1 接口请求示例 **请求 /channel/sales-product/price/create**(POST) ```json { "salesProductId": 1, "serialNumber": 1, "status": 1, "priceName": "运费", "costItem": "freight", "costUnit": "kg", "currency": "RMB", "chargingCondition": "", "factors": [ { "serialNumber": 1, "factor": "货物类型", "factorValues": ["普货", "敏感货"] }, { "serialNumber": 2, "factor": "分区", "factorValues": ["欧洲", "美国"] } ], "details": [ { "factorValuesJson": {"分区": "欧洲", "货物类型": "普货", "重量": "0-0.5"}, "calculationFormula": "单价", "calculatedValue": 62, "customerGroupValues": {"客户组1": 60, "客户组2": 65} } ] } ``` **响应** ```json { "code": 0, "msg": "success", "data": null } ``` ###### 2 接口业务逻辑 ###### 核心流程 - 新增价格项主记录,并写入对应 `factors` 与 `details` 子记录。 - 同步写入审计字段,保持主从数据一致性。 ###### 校验与异常 - 校验 `salesProductId`、`priceName`、`costItem`、`costUnit` 等必填字段。 - 校验同产品下 `serialNumber` 不冲突;冲突时返回业务错误。 ##### 5.3.2.3 更新价格项(POST /channel/sales-product/price/update) ###### 1 接口请求示例 **请求 /channel/sales-product/price/update**(POST) ```json { "id": 1, "salesProductId": 1, "serialNumber": 1, "status": 1, "priceName": "运费", "costItem": "freight", "costUnit": "kg", "currency": "RMB", "chargingCondition": "", "factors": [], "details": [] } ``` **响应** ```json { "code": 0, "msg": "success", "data": null } ``` ###### 2 接口业务逻辑 ###### 核心流程 - 按 `id` 更新价格项主数据,并按最新请求重建/同步因素与明细子数据。 - 保留版本追溯能力,更新后可参与后续发布流程。 ###### 校验与异常 - 校验 `id` 存在且归属 `salesProductId`。 - 校验更新后配置合法(计算公式、阶梯区间、客户组取值等),不合法时返回业务错误。 ##### 5.3.2.4 删除价格项(POST /channel/sales-product/price/delete) ###### 1 接口请求示例 **请求 /channel/sales-product/price/delete**(POST) ```json { "id": 1 } ``` **响应** ```json { "code": 0, "msg": "success", "data": null } ``` ###### 2 接口业务逻辑 ###### 核心流程 - 按价格项 `id` 执行逻辑删除,并处理关联子表数据的可见性。 ###### 校验与异常 - 校验 `id` 存在且允许删除(如未处于不可删状态)。 - 删除失败返回统一业务错误码。 ##### 5.3.2.5 切换价格项状态(POST /channel/sales-product/price/toggleStatus) ###### 1 接口请求示例 **请求 /channel/sales-product/price/toggleStatus**(POST) ```json { "id": 1, "status": 0 } ``` **响应** ```json { "code": 0, "msg": "success", "data": null } ``` ###### 2 接口业务逻辑 ###### 核心流程 - 按 `id` 更新价格项启用状态(`status`),并刷新相关缓存。 ###### 校验与异常 - 校验 `status` 仅允许合法枚举值(启用/禁用)。 - 校验 `id` 存在,不存在时返回业务错误。 #### 5.3.3 排货规则(子表接口) | 接口名称 | 请求方式 | 请求路径 | 说明 | | --- | --- | --- | --- | | 获取排货规则 | GET | /channel/sales-product/allocation/get | 按产品ID获取排货方案及服务商列表 | | 保存排货规则 | POST | /channel/sales-product/allocation/save | 保存排货方案及服务商(保存并可选发布) | ##### 5.3.3.1 获取排货规则(GET /channel/sales-product/allocation/get) ###### 1 接口请求示例 **请求 /channel/sales-product/allocation/get**(GET,Query 参数) ``` ?salesProductId=1 ``` **响应** ```json { "code": 0, "msg": "success", "data": { "salesProductId": 1, "scheme": "costLowest", "providers": [ { "id": 1, "salesProductId": 1, "serialNumber": 1, "providerName": "一代物流", "serviceChannelId": 10, "serviceChannelName": "DHL美国专线", "weight": 100, "customerIds": [1, 2], "createTime": "2024-01-01 12:00:00", "updateTime": "2024-01-01 12:00:00" } ] } } ``` ###### 2 接口业务逻辑 ###### 核心流程 - 根据 `salesProductId` 查询排货方案与服务商列表并按顺序返回。 ###### 校验与异常 - 校验 `salesProductId` 必填且产品存在;不存在时返回业务错误。 ##### 5.3.3.2 保存排货规则(POST /channel/sales-product/allocation/save) ###### 1 接口请求示例 **请求 /channel/sales-product/allocation/save**(POST) ```json { "salesProductId": 1, "publish": false, "scheme": "costLowest", "providers": [ { "id": 0, "serialNumber": 1, "providerName": "一代物流", "serviceChannelId": 10, "weight": 100, "customerIds": [1, 2] } ] } ``` **响应** ```json { "code": 0, "msg": "success", "data": null } ``` ###### 2 接口业务逻辑 ###### 核心流程 - 保存排货策略与服务商配置,按 `id` 判定新增或更新。 - 当 `publish=true` 时触发规则发布并生成版本记录。 ###### 校验与异常 - 校验 `salesProductId`、`scheme` 与 `providers` 合法性。 - 保存失败回滚事务并返回统一错误信息。 #### 5.3.4 发布记录(子表接口) | 接口名称 | 请求方式 | 请求路径 | 说明 | | --- | --- | --- | --- | | 价格发布记录列表 | GET | /channel/sales-product/publish/price/list | 按产品ID获取价格发布历史 | | 规则发布记录列表 | GET | /channel/sales-product/publish/rules/list | 按产品ID获取规则发布历史 | | 发布预览 | GET | /channel/sales-product/publish/preview | 预览即将发布的快照 JSON(不落库) | | 发布 | POST | /channel/sales-product/publish | 发布当前配置(价格或规则) | | 回滚到版本 | POST | /channel/sales-product/publish/rollback | 回滚到指定版本 | ##### 5.3.4.1 价格发布记录列表(GET /channel/sales-product/publish/price/list) ###### 1 接口请求示例 **请求 /channel/sales-product/publish/price/list**(GET,Query 参数) ``` ?salesProductId=1&page=1&pageSize=20 ``` **响应** ```json { "code": 0, "msg": "success", "data": { "items": [ { "id": 1, "salesProductId": 1, "version": "20231227112339", "operatorType": "price", "status": "生效", "publishUser": "admin", "publishTime": "2023-12-27 11:23:39", "description": "价格调整", "content": {}, "createTime": "2023-12-27 11:23:39" } ], "total": 5 } } ``` ###### 2 接口业务逻辑 ###### 核心流程 - 按产品ID分页查询价格类发布历史,返回版本与发布人信息。 ###### 校验与异常 - 校验 `salesProductId` 与分页参数合法性,异常时返回统一错误码。 ##### 5.3.4.2 规则发布记录列表(GET /channel/sales-product/publish/rules/list) ###### 1 接口请求示例 **请求 /channel/sales-product/publish/rules/list**(GET,Query 参数) ``` ?salesProductId=1&page=1&pageSize=20 ``` **响应** ```json { "code": 0, "msg": "success", "data": { "items": [ { "id": 2, "salesProductId": 1, "version": "20231228103000", "operatorType": "rules", "status": "生效", "publishUser": "admin", "publishTime": "2023-12-28 10:30:00", "description": "规则调整", "content": {}, "createTime": "2023-12-28 10:30:00" } ], "total": 3 } } ``` ###### 2 接口业务逻辑 ###### 核心流程 - 按产品ID分页查询规则类发布历史,`operatorType` 固定为 `rules`。 ###### 校验与异常 - 校验 `salesProductId` 与分页参数合法性,异常时返回统一错误码。 ##### 5.3.4.3 发布(POST /channel/sales-product/publish) ###### 1 接口请求示例 **请求 /channel/sales-product/publish**(POST) ```json { "salesProductId": 1, "operatorType": "price", "description": "价格调整" } ``` **响应** ```json { "code": 0, "msg": "success", "data": null } ``` ###### 2 接口业务逻辑 ###### 核心流程 - 将当前配置发布为新版本并写入发布记录。 ###### 校验与异常 - 校验 `salesProductId`、`operatorType` 必填且可发布。 ##### 5.3.4.4 回滚到版本(POST /channel/sales-product/publish/rollback) ###### 1 接口请求示例 **请求 /channel/sales-product/publish/rollback**(POST) ```json { "salesProductId": 1, "recordId": 1, "operatorType": "price" } ``` **响应** ```json { "code": 0, "msg": "success", "data": null } ``` ###### 2 接口业务逻辑 ###### 核心流程 - 将配置回滚到指定发布版本并生成回滚记录。 ###### 校验与异常 - 校验 `recordId` 版本存在且与产品、类型一致,不一致时返回业务错误。 #### 5.3.5 服务渠道 | 接口名称 | 请求方式 | 请求路径 | 说明 | | --- | --- | --- | --- | | 渠道列表 | GET | /channel/service-channel/list | 渠道列表(含渠道组层级) | | 渠道详情 | GET | /channel/service-channel/detail | 渠道详情 | | 新增渠道 | POST | /channel/service-channel/create | 新增渠道 | | 更新渠道 | POST | /channel/service-channel/update | 更新渠道主表 | | 保存渠道配置 | POST | /channel/service-channel/config/save | 同 update(Swagger 隐藏) | | 删除渠道 | POST | /channel/service-channel/delete | 删除渠道 | | 服务商分组列表 | GET | /channel/service-channel/group/list | 获取服务商分组列表(下拉/筛选) | | 新增服务商 | POST | /channel/service-channel/group/create | 新增服务商分组(写入 `t_service_channel_group`,保存应用ID与API服务商ID) | | 编辑服务商 | POST | /channel/service-channel/group/update | 编辑服务商分组(更新名称/应用ID/API服务商ID) | | 删除服务商 | POST | /channel/service-channel/group/delete | 删除服务商分组(若分组下存在渠道需禁止删除) | ##### 5.3.5.1 渠道列表(GET /channel/service-channel/list) ###### 1 接口请求示例 **请求 /channel/service-channel/list**(GET,Query 参数) ``` ?page=1&pageSize=20&groupId=&keyword=&isActive=1 ``` **响应** ```json { "code": 0, "msg": "success", "data": { "items": [ { "id": 1, "groupId": 1, "groupName": "DHL渠道组", "channelCode": "DHL-US", "channelName": "DHL美国专线", "minTime": 3, "maxTime": 7, "forecastType": "waybill", "forecastCode": "", "description": "DHL美国专线渠道", "isActive": 1, "createTime": "2024-01-01 12:00:00", "updateTime": "2024-01-01 12:00:00" } ], "total": 10 } } ``` ###### 2 接口业务逻辑 ###### 核心流程 - 按分页与筛选条件查询渠道列表,返回渠道及所属分组信息。 ###### 校验与异常 - 校验分页参数合法性,异常时返回统一错误码。 ##### 5.3.5.2 渠道详情(GET /channel/service-channel/detail) ###### 1 接口请求示例 **请求 /channel/service-channel/detail**(GET,Query 参数) ``` ?id=1 ``` **响应** ```json { "code": 0, "msg": "success", "data": { "id": 1, "groupId": 1, "groupName": "DHL渠道组", "channelCode": "DHL-US", "channelName": "DHL美国专线", "minTime": 3, "maxTime": 7, "forecastType": "waybill", "forecastCode": "", "description": "DHL美国专线渠道", "preScript": "", "isActive": 1, "createTime": "2024-01-01 12:00:00", "createBy": "admin", "updateTime": "2024-01-01 12:00:00", "updateBy": "admin" } } ``` ###### 2 接口业务逻辑 ###### 核心流程 - 根据渠道 `id` 查询渠道详情与配置信息。 ###### 校验与异常 - 校验 `id` 必填且存在,不存在时返回业务错误。 ##### 5.3.5.3 新增渠道(POST /channel/service-channel/create) ###### 1 接口请求示例 **请求 /channel/service-channel/create**(POST) ```json { "groupId": 1, "channelCode": "DHL-US", "channelName": "DHL美国专线", "minTime": 3, "maxTime": 7, "forecastType": "waybill", "forecastCode": "", "description": "DHL美国专线渠道", "preScript": "", "isActive": 1 } ``` **响应** ```json { "code": 0, "msg": "success", "data": null } ``` ###### 2 接口业务逻辑 ###### 核心流程 - 新增渠道基础配置并写入审计字段。 ###### 校验与异常 - 校验 `groupId`、`channelCode`、`channelName` 等必填字段,编码冲突时返回业务错误。 ##### 5.3.5.4 服务商分组列表(GET /channel/service-channel/group/list) ###### 1 接口请求示例 **请求 /channel/service-channel/group/list**(GET,Query 参数) ``` ?keyword=&appId=&apiServiceProviderId= ``` **响应** ```json { "code": 0, "msg": "success", "data": [ { "id": 1, "name": "壹号", "appId": 8, "apiServiceProviderId": 41, "createTime": "2026-05-04 14:15:45", "updateTime": "1900-01-01 00:00:00" } ] } ``` ###### 2 接口业务逻辑 ###### 核心流程 - 查询 `t_service_channel_group` 未删除分组,可按 `keyword`、`appId`、`apiServiceProviderId` 过滤,按 `id` 倒序返回。 ###### 校验与异常 - 过滤参数格式非法时返回业务错误;无数据时返回空数组。 ##### 5.3.5.5 新增服务商(POST /channel/service-channel/group/create) ###### 1 接口请求示例 **请求 /channel/service-channel/group/create**(POST) ```json { "name": "DHL服务商", "appId": 1, "apiServiceProviderId": 12 } ``` **响应** ```json { "code": 0, "msg": "success", "data": null } ``` ###### 2 接口业务逻辑 ###### 核心流程 - 在 `t_service_channel_group` 新增服务商分组并写入应用ID与API服务商ID。 ###### 校验与异常 - 校验 `name`、`appId`、`apiServiceProviderId` 必填且有效。 ##### 5.3.5.6 编辑服务商(POST /channel/service-channel/group/update) ###### 1 接口请求示例 **请求 /channel/service-channel/group/update**(POST) ```json { "id": 1, "name": "DHL服务商(更新)", "appId": 1, "apiServiceProviderId": 12 } ``` **响应** ```json { "code": 0, "msg": "success", "data": null } ``` ###### 2 接口业务逻辑 ###### 核心流程 - 根据分组 `id` 更新服务商名称、应用ID与API服务商ID。 ###### 校验与异常 - 校验 `id` 存在且可编辑,不存在时返回业务错误。 ##### 5.3.5.7 删除服务商(POST /channel/service-channel/group/delete) ###### 1 接口请求示例 **请求 /channel/service-channel/group/delete**(POST) ```json { "id": 1 } ``` **响应** ```json { "code": 0, "msg": "success", "data": null } ``` ###### 2 接口业务逻辑 ###### 核心流程 - 根据分组 `id` 执行逻辑删除。 ###### 校验与异常 - 若分组下存在渠道则禁止删除并返回业务错误。 ##### 5.3.5.8 更新渠道(POST /channel/service-channel/update) ###### 1 接口请求示例 **请求 /channel/service-channel/update**(POST) ```json { "id": 1, "groupId": 1, "channelCode": "DHL-US", "channelName": "DHL美国专线", "minTime": 3, "maxTime": 7, "forecastType": "waybill", "forecastCode": "", "description": "DHL美国专线渠道", "preScript": "", "isActive": 1 } ``` **响应** ```json { "code": 0, "msg": "success", "data": null } ``` ###### 2 接口业务逻辑 ###### 核心流程 - 根据 `id` 更新渠道基础信息与渠道配置 tab 数据。 ###### 校验与异常 - 校验 `id` 存在及字段合法性,更新失败返回业务错误。 ##### 5.3.5.9 删除渠道(POST /channel/service-channel/delete) ###### 1 接口请求示例 **请求 /channel/service-channel/delete**(POST) ```json { "id": 1 } ``` **响应** ```json { "code": 0, "msg": "success", "data": null } ``` ###### 2 接口业务逻辑 ###### 核心流程 - 校验渠道是否可删除后执行逻辑删除并保留历史审计信息。 ###### 校验与异常 - 存在关联配置或不可删除状态时返回业务错误。 #### 5.3.6 渠道买价(子表接口) > **与 §5.3.2 产品卖价对称**:主从表结构、弹窗交互(基础信息 + 计费因素 + 价格表)、CRUD、`price/save` 整树替换与发布快照机制一致;差异为关联 **`serviceChannelId`**(非 `salesProductId`),运行时应付算价读取渠道买价快照(§4.3.4.2)。 | 接口名称 | 请求方式 | 请求路径 | 说明 | | --- | --- | --- | --- | | 买价项列表 | GET | /channel/service-channel/price/list | 按渠道ID获取买价项列表 | | 新增买价项 | POST | /channel/service-channel/price/create | 新增买价项(含基础信息+计费因素+价格表) | | 更新买价项 | POST | /channel/service-channel/price/update | 更新买价项 | | 删除买价项 | POST | /channel/service-channel/price/delete | 删除买价项 | | 切换买价项状态 | POST | /channel/service-channel/price/toggleStatus | 启用/禁用买价项 | | 批量保存买价项 | POST | /channel/service-channel/price/save | 整树替换买价;可选 `publish=true` 同步发布 | ##### 5.3.6.1 买价项列表(GET /channel/service-channel/price/list) ###### 1 接口请求示例 **请求 /channel/service-channel/price/list**(GET,Query 参数) ``` ?serviceChannelId=1 ``` **响应** ```json { "code": 0, "msg": "success", "data": { "items": [ { "id": 1, "serviceChannelId": 1, "serialNumber": 1, "status": 1, "priceName": "运费", "costItem": "freight", "costUnit": "kg", "currency": "RMB", "chargingCondition": "", "factors": [ { "id": 1, "priceId": 1, "serialNumber": 1, "factor": "货物类型", "factorValues": ["普货", "敏感货"] }, { "id": 2, "priceId": 1, "serialNumber": 2, "factor": "分区", "factorValues": ["欧洲", "美国"] } ], "details": [ { "id": 1, "priceId": 1, "factorValuesJson": {"分区": "欧洲", "货物类型": "普货", "重量": "0-0.5"}, "calculationFormula": "单价", "calculatedValue": 45, "customerGroupValues": {"客户组1": 43, "客户组2": 47} } ], "createTime": "2024-01-01 12:00:00", "updateTime": "2024-01-01 12:00:00" } ] } } ``` ###### 2 接口业务逻辑 ###### 核心流程 - 按 `serviceChannelId` 查询买价项主表,并**一次聚合**返回 `factors`、`details` 子数据(避免 N+1)。 - 结果按 `serialNumber` 升序组织,便于前端按配置顺序渲染。 ###### 校验与异常 - 校验 `serviceChannelId` 必填且渠道存在;不存在时返回业务错误。 - 仅返回未删除记录,查询异常返回统一错误码。 ##### 5.3.6.2 新增买价项(POST /channel/service-channel/price/create) ###### 1 接口请求示例 **请求 /channel/service-channel/price/create**(POST) ```json { "serviceChannelId": 1, "serialNumber": 1, "status": 1, "priceName": "运费", "costItem": "freight", "costUnit": "kg", "currency": "RMB", "chargingCondition": "", "factors": [ { "serialNumber": 1, "factor": "货物类型", "factorValues": ["普货", "敏感货"] } ], "details": [ { "factorValuesJson": {"分区": "欧洲", "货物类型": "普货", "重量": "0-0.5"}, "calculationFormula": "单价", "calculatedValue": 45, "customerGroupValues": {"客户组1": 43, "客户组2": 47} } ] } ``` **响应** ```json { "code": 0, "msg": "success", "data": null } ``` ###### 2 接口业务逻辑 ###### 核心流程 - 新增买价主记录,并写入对应 `factors` 与 `details` 子记录(与 §5.3.2.2 卖价新增一致)。 - 同步写入审计字段,保持主从数据一致性。 ###### 校验与异常 - 校验 `serviceChannelId`、`priceName`、`costItem`、`costUnit` 等必填字段。 - 校验同渠道下 `serialNumber` 不冲突;冲突时返回业务错误。 ##### 5.3.6.3 更新买价项(POST /channel/service-channel/price/update) ###### 1 接口请求示例 **请求 /channel/service-channel/price/update**(POST) ```json { "id": 1, "serviceChannelId": 1, "serialNumber": 1, "status": 1, "priceName": "运费", "costItem": "freight", "costUnit": "kg", "currency": "RMB", "chargingCondition": "", "factors": [], "details": [] } ``` **响应** ```json { "code": 0, "msg": "success", "data": null } ``` ###### 2 接口业务逻辑 ###### 核心流程 - 按 `id` 更新买价主数据,并按最新请求**重建/同步**因素与明细子数据。 - 保留版本追溯能力,更新后可参与后续发布流程(§5.3.7)。 ###### 校验与异常 - 校验 `id` 存在且归属 `serviceChannelId`。 - 校验更新后配置合法(计算公式、阶梯区间、客户组取值等),不合法时返回业务错误。 ##### 5.3.6.4 删除买价项(POST /channel/service-channel/price/delete) ###### 1 接口请求示例 **请求 /channel/service-channel/price/delete**(POST) ```json { "id": 1 } ``` **响应** ```json { "code": 0, "msg": "success", "data": null } ``` ###### 2 接口业务逻辑 ###### 核心流程 - 按买价项 `id` 执行逻辑删除,并处理关联子表数据的可见性。 ###### 校验与异常 - 校验 `id` 存在且允许删除。 - 删除失败返回统一业务错误码。 ##### 5.3.6.5 切换买价项状态(POST /channel/service-channel/price/toggleStatus) ###### 1 接口请求示例 **请求 /channel/service-channel/price/toggleStatus**(POST) ```json { "id": 1, "status": 0 } ``` **响应** ```json { "code": 0, "msg": "success", "data": null } ``` ###### 2 接口业务逻辑 ###### 核心流程 - 按 `id` 更新买价项启用状态(`status`)。 ###### 校验与异常 - 校验 `status` 仅允许合法枚举值(0/1)。 - 校验 `id` 存在,不存在时返回业务错误。 ##### 5.3.6.6 批量保存买价项(POST /channel/service-channel/price/save) > 与 **§5.3.2** 卖价 `price/save` 一致:前端也可逐条 `create/update`;本接口用于**整树替换**当前渠道下全部买价项(含 factors、details),可选连带发布。 ###### 1 接口请求示例 **请求 /channel/service-channel/price/save**(POST) ```json { "serviceChannelId": 1, "publish": false, "prices": [ { "serialNumber": 1, "status": 1, "priceName": "运费", "costItem": "freight", "costUnit": "kg", "currency": "RMB", "chargingCondition": "", "factors": [], "details": [] } ] } ``` **响应** ```json { "code": 0, "msg": "success", "data": null } ``` ###### 2 接口业务逻辑 ###### 核心流程 1. 校验 `serviceChannelId` 存在。 2. 使用临时负数 `Id` 关联子表,调用 **`ReplacePricesAsync`** 整树替换当前渠道买价。 3. 若 `publish=true`,写入 **`t_service_channel_publish_record`**(`operatorType=price`),快照内容为瘦身后的全部买价项(见 **§5.3.0.3**)。 ###### 校验与异常 - `prices` 可为空数组(表示清空买价);单条校验同 create/update。 - 发布失败时买价替换已成功则不回滚(与卖价策略一致),返回明确错误信息。 #### 5.3.7 服务渠道发布记录(子表接口) | 接口名称 | 请求方式 | 请求路径 | 说明 | | --- | --- | --- | --- | | 价格发布记录列表 | GET | /channel/service-channel/publish/price/list | 按渠道ID获取买价发布历史 | | 规则发布记录列表 | GET | /channel/service-channel/publish/rules/list | 按渠道ID获取规则发布历史 | | 发布预览 | GET | /channel/service-channel/publish/preview | 预览即将发布的快照 JSON(不落库) | | 发布 | POST | /channel/service-channel/publish | 发布当前配置(买价或规则) | | 回滚到版本 | POST | /channel/service-channel/publish/rollback | 回滚到指定版本 | ##### 5.3.7.1 价格发布记录列表(GET /channel/service-channel/publish/price/list) ###### 1 接口请求示例 **请求 /channel/service-channel/publish/price/list**(GET,Query 参数) ``` ?serviceChannelId=1&page=1&pageSize=20 ``` **响应** ```json { "code": 0, "msg": "success", "data": { "items": [ { "id": 1, "serviceChannelId": 1, "version": "20231227112339", "operatorType": "price", "status": "生效", "publishUser": "admin", "publishTime": "2023-12-27 11:23:39", "description": "买价调整", "content": {}, "createTime": "2023-12-27 11:23:39" } ], "total": 5 } } ``` ###### 2 接口业务逻辑 ###### 核心流程 - 按渠道ID分页查询买价发布记录并返回版本信息。 ###### 校验与异常 - 校验 `serviceChannelId` 与分页参数合法性。 ##### 5.3.7.2 规则发布记录列表(GET /channel/service-channel/publish/rules/list) ###### 1 接口请求示例 **请求 /channel/service-channel/publish/rules/list**(GET,Query 参数) ``` ?serviceChannelId=1&page=1&pageSize=20 ``` **响应** ```json { "code": 0, "msg": "success", "data": { "items": [ { "id": 2, "serviceChannelId": 1, "version": "20231228103000", "operatorType": "rules", "status": "生效", "publishUser": "admin", "publishTime": "2023-12-28 10:30:00", "description": "规则调整", "content": {}, "createTime": "2023-12-28 10:30:00" } ], "total": 3 } } ``` ###### 2 接口业务逻辑 ###### 核心流程 - 按渠道ID分页查询规则发布记录,`operatorType` 固定为 `rules`。 ###### 校验与异常 - 校验 `serviceChannelId` 与分页参数合法性。 ##### 5.3.7.3 发布(POST /channel/service-channel/publish) ###### 1 接口请求示例 **请求 /channel/service-channel/publish**(POST) ```json { "serviceChannelId": 1, "operatorType": "price", "description": "买价调整" } ``` **响应** ```json { "code": 0, "msg": "success", "data": null } ``` ###### 2 接口业务逻辑 ###### 核心流程 - 发布当前渠道配置并生成新的发布版本记录。 ###### 校验与异常 - 校验 `serviceChannelId`、`operatorType` 必填且可发布。 ##### 5.3.7.4 回滚到版本(POST /channel/service-channel/publish/rollback) ###### 1 接口请求示例 **请求 /channel/service-channel/publish/rollback**(POST) ```json { "serviceChannelId": 1, "recordId": 1, "operatorType": "price" } ``` **响应** ```json { "code": 0, "msg": "success", "data": null } ``` ###### 2 接口业务逻辑 ###### 核心流程 - 回滚到指定版本并生成回滚操作记录。 ###### 校验与异常 - 校验 `recordId` 与渠道、类型匹配关系,异常时返回业务错误。 #### 5.3.8 黑名单区 / 偏远分区 / 价格分区(共用 KpiArea) > 共用 `/channel/kpi-area/*` 接口,仅通过请求/响应中的 `areaType`(对应表字段 `AreaType`)区分分区类型。 | 类型 | areaType | | --- | --- | | 黑名单区 | blacklist | | 偏远分区 | remote | | 价格分区 | price | | 接口名称 | 请求方式 | 请求路径 | 说明 | | --- | --- | --- | --- | | 创建分区 | POST | /channel/kpi-area/create | 创建分区 | | 修改分区 | POST | /channel/kpi-area/update | 修改分区 | | 删除分区 | POST | /channel/kpi-area/delete | 删除分区 | | 分区列表 | POST | /channel/kpi-area/list | 分区列表 | | 新增分区明细 | POST | /channel/kpi-area/info/add | 新增分区明细 | | 修改分区明细 | POST | /channel/kpi-area/info/update | 修改分区明细 | | 删除分区明细 | POST | /channel/kpi-area/info/delete | 删除分区明细 | | 分区明细列表 | POST | /channel/kpi-area/info/list | 分区明细列表 | | 下载明细导入模板 | GET | /channel/kpi-area/info/import-template | Excel 导入模板 | | 导入分区明细 | POST | /channel/kpi-area/info/import | multipart 批量导入明细 | ##### 5.3.8.1 创建分区(POST /channel/kpi-area/create) ###### 1 接口请求示例 **请求 /channel/kpi-area/create**(POST) ```json { "areaType": "blacklist", "name": "黑名单区1", "systemName": "系统黑名单" } ``` **响应** ```json { "code": 0, "msg": "success", "data": null } ``` ###### 2 接口业务逻辑 ###### 核心流程 - 根据 `areaType` 创建分区主记录。 ###### 校验与异常 - 校验 `areaType`、`name` 必填且合法。 ##### 5.3.8.2 修改分区(POST /channel/kpi-area/update) ###### 1 接口请求示例 **请求 /channel/kpi-area/update**(POST) ```json { "id": 1, "areaType": "blacklist", "name": "黑名单区1", "systemName": "系统黑名单" } ``` **响应** ```json { "code": 0, "msg": "success", "data": null } ``` ###### 2 接口业务逻辑 ###### 核心流程 - 根据 `id` 更新分区名称与系统名称。 ###### 校验与异常 - 校验 `id` 存在且与 `areaType` 匹配。 ##### 5.3.8.3 删除分区(POST /channel/kpi-area/delete) ###### 1 接口请求示例 **请求 /channel/kpi-area/delete**(POST) ```json { "id": 1 } ``` **响应** ```json { "code": 0, "msg": "success", "data": null } ``` ###### 2 接口业务逻辑 ###### 核心流程 - 根据 `id` 执行分区逻辑删除。 ###### 校验与异常 - 分区存在明细时按业务规则拦截或级联处理。 ##### 5.3.8.4 分区列表(POST /channel/kpi-area/list) ###### 1 接口请求示例 **请求 /channel/kpi-area/list**(POST) ```json { "areaType": "blacklist", "page": 1, "pageSize": 20 } ``` **响应** ```json { "code": 0, "msg": "success", "data": { "items": [ { "id": 1, "areaType": "blacklist", "name": "黑名单区1", "systemName": "系统黑名单", "createTime": "2024-01-01 12:00:00", "updateTime": "2024-01-01 12:00:00" } ], "total": 5 } } ``` ###### 2 接口业务逻辑 ###### 核心流程 - 按 `areaType` 分页查询分区列表。 ###### 校验与异常 - 校验分页参数与 `areaType` 合法性。 ##### 5.3.8.5 新增分区明细(POST /channel/kpi-area/info/add) ###### 1 接口请求示例 **请求 /channel/kpi-area/info/add**(POST) ```json { "areaId": 1, "countryCode": "US", "countryName": "美国", "province": "加州", "city": "洛杉矶", "leftPostcode": "90001", "rightPostcode": "90099" } ``` **响应** ```json { "code": 0, "msg": "success", "data": null } ``` ###### 2 接口业务逻辑 ###### 核心流程 - 向指定分区新增明细记录(国家/省市/邮编区间)。 ###### 校验与异常 - 校验 `areaId` 存在且区间数据合法。 ##### 5.3.8.6 修改分区明细(POST /channel/kpi-area/info/update) ###### 1 接口请求示例 **请求 /channel/kpi-area/info/update**(POST) ```json { "id": 1, "areaId": 1, "countryCode": "US", "countryName": "美国", "province": "加州", "city": "洛杉矶", "leftPostcode": "90001", "rightPostcode": "90099" } ``` **响应** ```json { "code": 0, "msg": "success", "data": null } ``` ###### 2 接口业务逻辑 ###### 核心流程 - 根据明细 `id` 更新分区明细字段。 ###### 校验与异常 - 校验 `id` 存在,更新内容合法。 ##### 5.3.8.7 删除分区明细(POST /channel/kpi-area/info/delete) ###### 1 接口请求示例 **请求 /channel/kpi-area/info/delete**(POST) ```json { "id": 1 } ``` **响应** ```json { "code": 0, "msg": "success", "data": null } ``` ###### 2 接口业务逻辑 ###### 核心流程 - 根据明细 `id` 执行逻辑删除。 ###### 校验与异常 - 校验 `id` 存在,不存在时返回业务错误。 ##### 5.3.8.8 分区明细列表(POST /channel/kpi-area/info/list) ###### 1 接口请求示例 **请求 /channel/kpi-area/info/list**(POST) ```json { "areaId": 1, "page": 1, "pageSize": 20 } ``` **响应** ```json { "code": 0, "msg": "success", "data": { "items": [ { "id": 1, "areaId": 1, "countryCode": "US", "countryName": "美国", "province": "加州", "city": "洛杉矶", "leftPostcode": "90001", "rightPostcode": "90099", "createTime": "2024-01-01 12:00:00", "updateTime": "2024-01-01 12:00:00" } ], "total": 10 } } ``` ###### 2 接口业务逻辑 ###### 核心流程 - 按 `areaId` 分页查询分区明细列表。 ###### 校验与异常 - 校验 `areaId` 与分页参数合法性。 #### 5.3.9 计重规则 / 计重进位 | 接口名称 | 请求方式 | 请求路径 | 说明 | | --- | --- | --- | --- | | 规则列表 | POST | /channel/weight-rule/list | 规则列表 | | 创建规则 | POST | /channel/weight-rule/create | 创建规则 | | 更新规则 | POST | /channel/weight-rule/update | 更新规则 | | 删除规则 | POST | /channel/weight-rule/delete | 删除规则 | | 规则明细列表 | POST | /channel/weight-rule/info/list | 规则明细列表 | | 新增明细 | POST | /channel/weight-rule/info/add | 新增明细 | | 更新明细 | POST | /channel/weight-rule/info/update | 更新明细 | | 删除明细 | POST | /channel/weight-rule/info/delete | 删除明细 | | 进位列表 | POST | /channel/weight-rounding/list | 进位列表 | | 创建进位 | POST | /channel/weight-rounding/create | 创建进位 | | 更新进位 | POST | /channel/weight-rounding/update | 更新进位 | | 删除进位 | POST | /channel/weight-rounding/delete | 删除进位 | | 进位明细列表 | POST | /channel/weight-rounding/info/list | 进位明细列表 | | 新增进位明细 | POST | /channel/weight-rounding/info/add | 新增进位明细 | | 更新进位明细 | POST | /channel/weight-rounding/info/update | 更新进位明细 | | 删除进位明细 | POST | /channel/weight-rounding/info/delete | 删除进位明细 | ##### 5.3.9.1 规则列表(POST /channel/weight-rule/list) ###### 1 接口请求示例 **请求 /channel/weight-rule/list**(POST) ```json { "page": 1, "pageSize": 20 } ``` **响应** ```json { "code": 0, "msg": "success", "data": { "items": [{ "id": 1, "name": "标准计重规则" }], "total": 5 } } ``` ###### 2 接口业务逻辑 ###### 核心流程 - 分页查询计重规则主表。 ###### 校验与异常 - 校验分页参数合法性。 ##### 5.3.9.2 创建规则(POST /channel/weight-rule/create) ###### 1 接口请求示例 **请求 /channel/weight-rule/create**(POST) ```json { "name": "标准计重规则" } ``` **响应** ```json { "code": 0, "msg": "success", "data": null } ``` ###### 2 接口业务逻辑 ###### 核心流程 - 新增计重规则并写入审计信息。 ###### 校验与异常 - 校验 `name` 必填且不重复。 ##### 5.3.9.3 更新规则(POST /channel/weight-rule/update) ###### 1 接口请求示例 **请求 /channel/weight-rule/update**(POST) ```json { "id": 1, "name": "标准计重规则" } ``` **响应** ```json { "code": 0, "msg": "success", "data": null } ``` ###### 2 接口业务逻辑 ###### 核心流程 - 根据 `id` 更新规则信息。 ###### 校验与异常 - 校验 `id` 存在。 ##### 5.3.9.4 删除规则(POST /channel/weight-rule/delete) ###### 1 接口请求示例 **请求 /channel/weight-rule/delete**(POST) ```json { "id": 1 } ``` **响应** ```json { "code": 0, "msg": "success", "data": null } ``` ###### 2 接口业务逻辑 ###### 核心流程 - 对规则执行逻辑删除。 ###### 校验与异常 - 校验是否存在关联明细。 ##### 5.3.9.5 规则明细列表(POST /channel/weight-rule/info/list) ###### 1 接口请求示例 **请求 /channel/weight-rule/info/list**(POST) ```json { "ruleId": 1, "page": 1, "pageSize": 20 } ``` **响应** ```json { "code": 0, "msg": "success", "data": { "items": [{ "id": 1, "ruleId": 1, "condition": "gw>vw", "billableWeight": "gw", "volumetricFactor": "6000" }], "total": 3 } } ``` ###### 2 接口业务逻辑 ###### 核心流程 - 按 `ruleId` 分页查询规则明细。 ###### 校验与异常 - 校验 `ruleId` 必填且有效。 ##### 5.3.9.6 新增明细(POST /channel/weight-rule/info/add) ###### 1 接口请求示例 **请求 /channel/weight-rule/info/add**(POST) ```json { "ruleId": 1, "condition": "gw>vw", "billableWeight": "vw", "volumetricFactor": "6000" } ``` **响应** ```json { "code": 0, "msg": "success", "data": null } ``` ###### 2 接口业务逻辑 ###### 核心流程 - 新增规则明细。 ###### 校验与异常 - 校验 `ruleId` 与字段合法性。 ##### 5.3.9.7 更新明细(POST /channel/weight-rule/info/update) ###### 1 接口请求示例 **请求 /channel/weight-rule/info/update**(POST) ```json { "id": 1, "ruleId": 1, "condition": "gw>vw", "billableWeight": "vw", "volumetricFactor": "6000" } ``` **响应** ```json { "code": 0, "msg": "success", "data": null } ``` ###### 2 接口业务逻辑 ###### 核心流程 - 根据 `id` 更新规则明细。 ###### 校验与异常 - 校验 `id` 存在且归属正确。 ##### 5.3.9.8 删除明细(POST /channel/weight-rule/info/delete) ###### 1 接口请求示例 **请求 /channel/weight-rule/info/delete**(POST) ```json { "id": 1 } ``` **响应** ```json { "code": 0, "msg": "success", "data": null } ``` ###### 2 接口业务逻辑 ###### 核心流程 - 删除规则明细(逻辑删除)。 ###### 校验与异常 - 校验 `id` 存在。 ##### 5.3.9.9 进位列表(POST /channel/weight-rounding/list) ###### 1 接口请求示例 **请求 /channel/weight-rounding/list**(POST) ```json { "page": 1, "pageSize": 20 } ``` **响应** ```json { "code": 0, "msg": "success", "data": { "items": [{ "id": 1, "name": "标准进位" }], "total": 5 } } ``` ###### 2 接口业务逻辑 ###### 核心流程 - 分页查询计重进位主表。 ###### 校验与异常 - 校验分页参数。 ##### 5.3.9.10 创建进位(POST /channel/weight-rounding/create) ###### 1 接口请求示例 **请求 /channel/weight-rounding/create**(POST) ```json { "name": "标准进位" } ``` **响应** ```json { "code": 0, "msg": "success", "data": null } ``` ###### 2 接口业务逻辑 ###### 核心流程 - 新增进位规则。 ###### 校验与异常 - 校验 `name` 必填。 ##### 5.3.9.11 更新进位(POST /channel/weight-rounding/update) ###### 1 接口请求示例 **请求 /channel/weight-rounding/update**(POST) ```json { "id": 1, "name": "标准进位" } ``` **响应** ```json { "code": 0, "msg": "success", "data": null } ``` ###### 2 接口业务逻辑 ###### 核心流程 - 根据 `id` 更新进位规则。 ###### 校验与异常 - 校验 `id` 存在。 ##### 5.3.9.12 删除进位(POST /channel/weight-rounding/delete) ###### 1 接口请求示例 **请求 /channel/weight-rounding/delete**(POST) ```json { "id": 1 } ``` **响应** ```json { "code": 0, "msg": "success", "data": null } ``` ###### 2 接口业务逻辑 ###### 核心流程 - 逻辑删除进位规则。 ###### 校验与异常 - 校验是否可删除。 ##### 5.3.9.13 进位明细列表(POST /channel/weight-rounding/info/list) ###### 1 接口请求示例 **请求 /channel/weight-rounding/info/list**(POST) ```json { "roundingId": 1, "page": 1, "pageSize": 20 } ``` **响应** ```json { "code": 0, "msg": "success", "data": { "items": [{ "id": 1, "roundingId": 1, "startValue": 0, "endValue": 1, "public": "0.5" }], "total": 5 } } ``` ###### 2 接口业务逻辑 ###### 核心流程 - 按 `roundingId` 分页查询进位明细。 ###### 校验与异常 - 校验 `roundingId` 存在。 ##### 5.3.9.14 新增进位明细(POST /channel/weight-rounding/info/add) ###### 1 接口请求示例 **请求 /channel/weight-rounding/info/add**(POST) ```json { "roundingId": 1, "startValue": 0, "endValue": 1, "public": "0.5" } ``` **响应** ```json { "code": 0, "msg": "success", "data": null } ``` ###### 2 接口业务逻辑 ###### 核心流程 - 新增进位明细。 ###### 校验与异常 - 校验区间与 `public` 进位值合法性。 ##### 5.3.9.15 更新进位明细(POST /channel/weight-rounding/info/update) ###### 1 接口请求示例 **请求 /channel/weight-rounding/info/update**(POST) ```json { "id": 1, "roundingId": 1, "startValue": 0, "endValue": 1, "public": "0.5" } ``` **响应** ```json { "code": 0, "msg": "success", "data": null } ``` ###### 2 接口业务逻辑 ###### 核心流程 - 根据 `id` 更新进位明细。 ###### 校验与异常 - 校验 `id` 存在且归属正确。 ##### 5.3.9.16 删除进位明细(POST /channel/weight-rounding/info/delete) ###### 1 接口请求示例 **请求 /channel/weight-rounding/info/delete**(POST) ```json { "id": 1 } ``` **响应** ```json { "code": 0, "msg": "success", "data": null } ``` ###### 2 接口业务逻辑 ###### 核心流程 - 删除进位明细(逻辑删除)。 ###### 校验与异常 - 校验 `id` 存在。 #### 5.3.10 自动触发 | 接口名称 | 请求方式 | 请求路径 | 说明 | | --- | --- | --- | --- | | 列表 | GET | /channel/auto-trigger/list | 列表 | | 新增 | POST | /channel/auto-trigger/create | 新增 | | 更新 | POST | /channel/auto-trigger/update | 更新 | | 删除 | POST | /channel/auto-trigger/delete | 删除 | | 切换启用状态 | POST | /channel/auto-trigger/toggleStatus | 切换启用状态 | ##### 5.3.10.1 列表(GET /channel/auto-trigger/list) ###### 1 接口请求示例 **请求 /channel/auto-trigger/list**(GET,Query 参数) ``` ?page=1&pageSize=20&operation=&triggerTiming= ``` **响应** ```json { "code": 0, "msg": "success", "data": { "items": [{ "id": 1, "operation": "拦截", "triggerTiming": "下单" }], "total": 5 } } ``` ###### 2 接口业务逻辑 ###### 核心流程 - 分页查询自动触发规则列表。 ###### 校验与异常 - 校验分页与筛选参数合法。 ##### 5.3.10.2 新增(POST /channel/auto-trigger/create) ###### 1 接口请求示例 **请求 /channel/auto-trigger/create**(POST) ```json { "status": 1, "operation": "拦截", "triggerTiming": "下单", "triggerCondition": "weight>30", "promptMessage": "重量超过30kg,请确认", "notifier": "admin@example.com" } ``` **响应** ```json { "code": 0, "msg": "success", "data": null } ``` ###### 2 接口业务逻辑 ###### 核心流程 - 新增自动触发规则。 ###### 校验与异常 - 校验关键字段必填。 ##### 5.3.10.3 更新(POST /channel/auto-trigger/update) ###### 1 接口请求示例 **请求 /channel/auto-trigger/update**(POST) ```json { "id": 1, "status": 1, "operation": "拦截", "triggerTiming": "下单", "triggerCondition": "weight>30", "promptMessage": "重量超过30kg,请确认", "notifier": "admin@example.com" } ``` **响应** ```json { "code": 0, "msg": "success", "data": null } ``` ###### 2 接口业务逻辑 ###### 核心流程 - 根据 `id` 更新自动触发规则。 ###### 校验与异常 - 校验 `id` 存在且可编辑。 ##### 5.3.10.4 删除(POST /channel/auto-trigger/delete) ###### 1 接口请求示例 **请求 /channel/auto-trigger/delete**(POST) ```json { "id": 1 } ``` **响应** ```json { "code": 0, "msg": "success", "data": null } ``` ###### 2 接口业务逻辑 ###### 核心流程 - 逻辑删除自动触发规则。 ###### 校验与异常 - 校验 `id` 存在且可删除。 ##### 5.3.10.5 切换启用状态(POST /channel/auto-trigger/toggleStatus) ###### 1 接口请求示例 **请求 /channel/auto-trigger/toggleStatus**(POST) ```json { "id": 1, "status": 0 } ``` **响应** ```json { "code": 0, "msg": "success", "data": null } ``` ###### 2 接口业务逻辑 ###### 核心流程 - 按 `id` 切换规则启用状态。 ###### 校验与异常 - 校验 `status` 枚举合法性。 #### 5.3.11 问题模板 | 接口名称 | 请求方式 | 请求路径 | 说明 | | --- | --- | --- | --- | | 列表 | GET | /channel/issue-template/list | 分页列表 | | 详情 | GET | /channel/issue-template/detail | 单条详情 | | 下拉选项 | GET | /channel/issue-template/options | 启用模板选项 | | 新增 | POST | /channel/issue-template/create | 新增 | | 更新 | POST | /channel/issue-template/update | 更新 | | 删除 | POST | /channel/issue-template/delete | 软删 | | 切换启用状态 | POST | /channel/issue-template/toggleStatus | 切换状态 | ##### 5.3.11.1 新增(POST /channel/issue-template/create) ```json { "organizationId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "issueType": "address", "title": "收件地址无法派送", "content": "运单 {serviceNo} 收件地址不完整,请补充门牌号与邮编。", "notifyMethod": "inApp,email", "status": 1 } ``` **校验**:`issueType` 合法;`title`/`content` 非空;`notifyMethod` 至少一种;同组织同类型不重复。 #### 5.3.12 标签设计 | 接口名称 | 请求方式 | 请求路径 | 说明 | | --- | --- | --- | --- | | 保存标签模板 | POST | /channel/label-template/save | 保存标签模板 | ###### 1 接口请求示例 **请求 /channel/label-template/save**(POST) ```json { "id": 0, "name": "尾程标签模板1", "templateType": "lastMile", "templateJson": {} } ``` > 参数说明:`id` 模板ID,0为新增;`templateType` 类型:lastMile(尾程)/customer(客户)/inbound(入库)/outbound(出库)/sku;`templateJson` vg-print 导出的模板 JSON。 **响应** ```json { "code": 0, "msg": "success", "data": { "id": 1, "name": "尾程标签模板1", "templateType": "lastMile", "templateJson": {}, "createTime": "2024-01-01 12:00:00", "updateTime": "2024-01-01 12:00:00" } } ``` ---------------------------------------------------------------------------------------- ---------------------------------------------------------------------------------------- ###### 2 接口业务逻辑 ###### 核心流程 - 按本节接口定义执行业务处理流程,完成参数解析、数据查询/写入与结果返回。 ###### 校验与异常 - 对必填参数、状态流转与数据存在性进行校验;异常场景返回统一错误码与错误信息。 ###### 3 当前实现(oms0414-back) - `LabelTemplateService.SaveAsync`:见 **§5.3.0.8**;`id=0` 新增,否则更新;**Name+TemplateType** 唯一。 ## 6 操作模块 ### 6.1 核心需求说明 - **入库扫描**:扫描运单号完成入库,记录实重、尺寸、销售产品、目的国、仓库、操作人、操作时间;**仅覆盖 `Type=receive` 行并重算计费重**(规则见 §4.4.3.1),再调用 **§4.3.4.1** 重算应收(`receive` 计费重,`ChargingStage=inbound`);**不修改** `forecast` / `ship` / `downstream` 行;支持查看最近入库记录。 - **出库扫描**:扫描运单号完成出库,**记录实重、尺寸、销售产品、目的国、服务渠道ID**、仓库、操作人、操作时间(**`t_outbound_record` 在入库记录字段基础上另存 `ServiceChannelId`**,见 §6.2.2);**仅覆盖 `Type=ship`(发货)行尺寸与重量并重算计费重**(实测尺重见请求体,见 §6.3.2.1);**出库成功后更新 `t_order.OrderStatus = pendingOnline`(待上网)并写入 `ShipTime`**;再调用 **§4.3.4.1** 重算应收(`ship` 计费重,`ChargingStage=outbound`);**不修改** `forecast` / `receive` / `downstream` 行;支持查看最近出库记录。 - **标签打印**:按运单号/客户单号/跟踪号/其他单号选择标签模板打印;记录打印结果与操作人。 ### 6.2 表结构设计 #### 6.2.1 入库记录表 `t_inbound_record` | 字段名 | 字段类型 | 长度 | 主键/外键 | 非空 | 说明 | | --- | --- | --- | --- | --- | --- | | Id | BIGINT | - | 主键 | 是 | 自增唯一标识 | | WaybillNo | VARCHAR | 64 | - | 是 | 运单号 | | Weight | DECIMAL | 10,2 | - | 否 | 实重(kg) | | Length | DECIMAL | 10,2 | - | 否 | 长(cm) | | Width | DECIMAL | 10,2 | - | 否 | 宽(cm) | | Height | DECIMAL | 10,2 | - | 否 | 高(cm) | | SalesProductId | BIGINT | - | 外键 | 否 | 销售产品ID | | DestinationCountry | VARCHAR | 8 | - | 否 | 目的国 | | WarehouseId | BIGINT | - | 外键 | 否 | 仓库ID | | OperateTime | DATETIME | - | - | 是 | 操作时间 | | OperatorId | BIGINT | - | 外键 | 否 | 操作人ID | | IsDeleted | TINYINT | - | - | 是 | 是否删除:0-否,1-是,默认0 | | CreateUserId | INT | - | - | 是 | 创建人 | | CreateTime | DATETIME | - | - | 是 | 创建时间 | | UpdateUserId | INT | - | - | 否 | 更新人 | | UpdateTime | DATETIME | - | - | 否 | 更新时间 | #### 6.2.2 出库记录表 `t_outbound_record` > **与 `t_inbound_record` 对齐**:除业务含义为「出库」外,件重尺与业务维度字段与入库记录表一致;**另存 `ServiceChannelId`**,关联 **`t_service_channel.Id`**,记录本单出库时所选/所绑定的服务渠道,便于对账与列表筛选。 | 字段名 | 字段类型 | 长度 | 主键/外键 | 非空 | 说明 | | --- | --- | --- | --- | --- | --- | | Id | BIGINT | - | 主键 | 是 | 自增唯一标识 | | WaybillNo | VARCHAR | 64 | - | 是 | 运单号 | | Weight | DECIMAL | 10,2 | - | 否 | 实重(kg),与本次出库写入 `t_order_weight.Type=ship` 的 `ActualWeight` 一致(发货复量实测) | | Length | DECIMAL | 10,2 | - | 否 | 长(cm) | | Width | DECIMAL | 10,2 | - | 否 | 宽(cm) | | Height | DECIMAL | 10,2 | - | 否 | 高(cm) | | SalesProductId | BIGINT | - | 外键 | 否 | 销售产品ID | | DestinationCountry | VARCHAR | 8 | - | 否 | 目的国 | | ServiceChannelId | BIGINT | - | 外键(**`t_service_channel.Id`**) | 否 | **服务渠道ID**;请求可选传入,未传时建议服务端按订单已绑定服务渠道或排货结果兜底 | | WarehouseId | BIGINT | - | 外键 | 否 | 仓库ID | | OperateTime | DATETIME | - | - | 是 | 操作时间 | | OperatorId | BIGINT | - | 外键 | 否 | 操作人ID | | IsDeleted | TINYINT | - | - | 是 | 是否删除:0-否,1-是,默认0 | | CreateUserId | INT | - | - | 是 | 创建人 | | CreateTime | DATETIME | - | - | 是 | 创建时间 | | UpdateUserId | INT | - | - | 否 | 更新人 | | UpdateTime | DATETIME | - | - | 否 | 更新时间 | #### 6.2.3 标签打印记录表 `t_label_print_record` | 字段名 | 字段类型 | 长度 | 主键/外键 | 非空 | 说明 | | --- | --- | --- | --- | --- | --- | | Id | BIGINT | - | 主键 | 是 | 自增唯一标识 | | WaybillNo | VARCHAR | 64 | - | 是 | 运单号 | | CustomerOrderNo | VARCHAR | 64 | - | 否 | 客户单号 | | TrackingNo | VARCHAR | 64 | - | 否 | 跟踪号 | | OtherNumber | VARCHAR | 64 | - | 否 | 其他单号 | | LabelTemplateId | BIGINT | - | 外键 | 否 | 标签模板ID | | PrintSuccess | TINYINT | - | - | 是 | 是否打印成功 | | PrintTime | DATETIME | - | - | 是 | 打印时间 | | OperatorId | BIGINT | - | 外键 | 否 | 操作人ID | | IsDeleted | TINYINT | - | - | 是 | 是否删除:0-否,1-是,默认0 | | CreateUserId | INT | - | - | 是 | 创建人 | | CreateTime | DATETIME | - | - | 是 | 创建时间 | | UpdateUserId | INT | - | - | 否 | 更新人 | | UpdateTime | DATETIME | - | - | 否 | 更新时间 | ### 6.3 核心接口设计 #### 6.3.1 入库扫描 | 接口名称 | 请求方式 | 请求路径 | 说明 | | --- | --- | --- | --- | | 入库 | POST | /operation/inbound/create | 入库 | | 最近入库记录 | GET | /operation/inbound/recent-list | 最近入库记录 | ##### 6.3.1.1 入库(POST /operation/inbound/create) ###### 1 接口请求示例 **请求 /operation/inbound/create**(POST) ```json { "waybillNo": "IDE2607221859123456", "weight": 2.5, "length": 20, "width": 15, "height": 10, "salesProductId": 1, "destinationCountry": "US", "warehouseId": 1 } ``` > 参数说明:`waybillNo` 运单号;`weight` 实重(kg);`length/width/height` 长宽高(cm);`salesProductId` 销售产品ID;`destinationCountry` 目的国二字码;`warehouseId` 仓库ID。操作时间、操作人由服务端从当前会话获取。 **响应** ```json { "code": 0, "msg": "success", "data": { "id": 1, "waybillNo": "IDE2607221859123456", "weight": 2.5, "length": 20, "width": 15, "height": 10, "salesProductId": 1, "destinationCountry": "US", "warehouseId": 1, "operateTime": "2024-01-01 12:00:00", "operatorId": 1, "createTime": "2024-01-01 12:00:00" } } ``` ###### 2 接口业务逻辑 ###### 核心流程 - 解析 `waybillNo` 等参数,定位订单(及绑定销售产品、已落库的 `t_order_weight` 四行)。 - 写入 **`t_inbound_record`**(实重、尺寸、销售产品、目的国、仓库、操作人、操作时间等)。 - **仅覆盖 `t_order_weight` 中本单 `Type=receive` 行**:实测件重尺重算计费重(**不修改 forecast/ship/downstream**)。 - **§4.3.4.1** 重算应收(`receive` 计费重,`ChargingStage=inbound`)。 - **TryConfirmServiceChannelAndPayableAsync** 尝试排货与应付: - 成功 → **`OrderStatus=pendingPayment`**,清空 **`AllocationPendingReason`**,写入 **`ReceiveTime`**。 - 失败 → **`OrderStatus=pendingAllocation`**,写入 **`AllocationPendingReason`**(仍完成收货与 inbound 应收)。 - **允许入库的源状态**:`predeclared`、`pendingAllocation`;已入库状态拒绝重复收货。 ###### 校验与异常 - 对必填参数、订单状态、单号与订单关联、仓库权限等进行校验;件重尺非法、计重规则未命中、进位无匹配等按业务错误返回。 ###### 3 当前实现(oms0414-back) - `InboundRecordService.CreateAsync` → `ApplyReceiveBillingByWaybillAsync`:联动 **`receive` 行**计重、inbound 应收、**可选排货**;状态 **`pendingPayment` / `pendingAllocation`**;`ParcelBillingValidators.EnsureInboundOrderStatus`。 ##### 6.3.1.2 最近入库记录(GET /operation/inbound/recent-list) ###### 1 接口请求示例 **请求 /operation/inbound/recent-list**(GET,Query 参数) ``` ?limit=20&warehouseId= ``` **响应** ```json { "code": 0, "msg": "success", "data": { "items": [ { "id": 1, "waybillNo": "IDE2607221859123456", "weight": 2.5, "length": 20, "width": 15, "height": 10, "salesProductId": 1, "salesProductName": "美国空运专线(普货)", "destinationCountry": "US", "warehouseId": 1, "warehouseName": "深圳仓", "operateTime": "2024-01-01 12:00:00", "operatorId": 1, "operatorName": "张三", "createTime": "2024-01-01 12:00:00" } ] } } ``` ###### 2 接口业务逻辑 ###### 核心流程 - 按接口定义执行业务流程,完成参数解析、数据处理与结果返回。 ###### 校验与异常 - 对必填参数、状态与数据存在性进行校验,异常场景返回统一错误码与错误信息。 #### 6.3.2 出库扫描 | 接口名称 | 请求方式 | 请求路径 | 说明 | | --- | --- | --- | --- | | 出库 | POST | /operation/outbound/create | 出库 | | 最近出库记录 | GET | /operation/outbound/recent-list | 最近出库记录 | ##### 6.3.2.1 出库(POST /operation/outbound/create) ###### 1 接口请求示例 **请求 /operation/outbound/create**(POST) ```json { "waybillNo": "IDE2607221859123456", "weight": 2.5, "length": 20, "width": 15, "height": 10, "salesProductId": 1, "destinationCountry": "US", "serviceChannelId": 100, "warehouseId": 1 } ``` > 参数说明:`waybillNo` 运单号;**`weight` 实重(kg)、`length`/`width`/`height` 长宽高(cm) 必填**(发货复量实测,写入 **`Type=ship` 行**);`salesProductId` 销售产品ID;`destinationCountry` 目的国二字码;**`serviceChannelId` 服务渠道ID**(对应 **`t_service_channel.Id`**,可选,未传时服务端按订单绑定/排货兜底并写入 `t_outbound_record`);`warehouseId` 仓库ID(可选,不传则从订单或默认仓库获取)。**`salesProductId`、`destinationCountry` 可选**,未传时建议服务端用订单绑定/收件国兜底。操作时间、操作人由服务端从当前会话获取。 **响应** ```json { "code": 0, "msg": "success", "data": { "id": 1, "waybillNo": "IDE2607221859123456", "weight": 2.5, "length": 20, "width": 15, "height": 10, "salesProductId": 1, "destinationCountry": "US", "serviceChannelId": 100, "warehouseId": 1, "operateTime": "2024-01-01 12:00:00", "operatorId": 1, "createTime": "2024-01-01 12:00:00" } } ``` ###### 2 接口业务逻辑 ###### 核心流程 1. **定位订单**:解析 `waybillNo`,按运单号关联 **`t_order`** 及本单 **`t_order_weight` 四行**(`forecast` / `receive` / `ship` / `downstream`)。 2. **前置校验**: - 订单须存在且 **`OrderStatus = pendingShip`(待发货)**;**`pendingPayment` / `received`** → **「订单未付款,禁止出库」**;其它状态 → **「当前订单状态不允许出库」**。 - 须已存在 **`Type=ship`(发货)** 行(预报成功时随主单创建)。 3. **写入出库记录**:落库 **`t_outbound_record`**(实重、尺寸、销售产品、目的国、**`ServiceChannelId`**、仓库、操作人、操作时间;件重尺与业务维度与 **`t_inbound_record`** 对齐,**并含 `ServiceChannelId`**;`Weight/Length/Width/Height` 取值以请求、订单绑定及下文 **`ship` 行最终件重尺** 为准)。 4. **更新发货件重尺(`t_order_weight.Type = ship`)**:以请求 **`weight` / `length` / `width` / `height`** 写入 **`ship` 行**的 **`Length`、`Width`、`Height`、`ActualWeight`**(`weight` → `ActualWeight`),并重算 **`Cubic = Length × Width × Height / 1_000_000`**(**不读取、不覆盖其它 `Type` 行**)。 5. **计重**:对 **`ship` 行**按订单绑定 **`WeightRuleId` / `WeightRoundingId`**(及 rules 快照或子表明细)执行 **§4.4.3.1 步骤 7、8 相同算法**,重算 **`VolumetricWeight`、`ChargeableWeight`**(**不修改 `forecast` / `receive` / `downstream` 行**)。 6. **更新订单主表**:**`OrderStatus = pendingOnline`(待上网)**,**`ShipTime = 操作时间`**,刷新 **`UpdateTime` / `UpdateUserId`**;持久化 **`ship` 行**与主表。 7. **阶段计费**(待实现):按 **§4.3.4.1** 以 **`ship` 行** `ChargeableWeight` 重算应收(`ChargingStage=outbound`)。 > **与入库对称**:入库 **仅**更新 **`receive` 行** → **`OrderStatus=pendingPayment`** + **`ReceiveTime`**;出库 **仅**更新 **`ship` 行** → **`OrderStatus=pendingOnline`** + **`ShipTime`**。四行件重尺各自独立维护。 ###### 校验与异常 - **`waybillNo`** 必填;**`weight` / `length` / `width` / `height` 必填且均 > 0**。 - 订单须 **`pendingShip`**;**`pendingPayment` / `received`** 禁止出库;**`pendingOnline` 及以后**(已出库)→ **「订单已出库」**。 - 缺少 **`Type=ship`** 行 → 业务错误。 - 计重规则未命中、进位无匹配、价格分区未匹配(若触发 §4.3.4 算价)→ 明确错误信息;**事务回滚**,不写入出库记录与状态变更。 ###### 3 当前实现(oms0414-back) - `OutboundRecordService.CreateAsync` → **`ApplyShipBillingByWaybillAsync`** + 插入 **`t_outbound_record`**;已覆盖 **`ship` 行**、更新 **`OrderStatus=pendingOnline`** / **`ShipTime`**、写入 outbound 应收。 ##### 6.3.2.2 最近出库记录(GET /operation/outbound/recent-list) ###### 1 接口请求示例 **请求 /operation/outbound/recent-list**(GET,Query 参数) ``` ?limit=20&warehouseId= ``` **响应** ```json { "code": 0, "msg": "success", "data": { "items": [ { "id": 1, "waybillNo": "IDE2607221859123456", "weight": 2.5, "length": 20, "width": 15, "height": 10, "salesProductId": 1, "salesProductName": "美国空运专线(普货)", "destinationCountry": "US", "serviceChannelId": 100, "serviceChannelName": "XX组-USPS", "warehouseId": 1, "warehouseName": "深圳仓", "operateTime": "2024-01-01 12:00:00", "operatorId": 1, "operatorName": "张三", "createTime": "2024-01-01 12:00:00" } ] } } ``` > 响应字段在 **§6.3.1.2 最近入库记录** 列表基础上增加 **`serviceChannelId` / `serviceChannelName`**(联查 **`t_service_channel`** 及展示名,命名可与实现一致),仅业务来源为出库。 ###### 2 接口业务逻辑 ###### 核心流程 - 分页/条数限制与 **`GET /operation/inbound/recent-list`** 一致;返回 **`t_outbound_record`** 行及联查名称字段。 ###### 校验与异常 - 对 `limit`、`warehouseId` 等查询参数及数据权限校验;异常返回统一错误码与错误信息。 #### 6.3.3 标签打印 | 接口名称 | 请求方式 | 请求路径 | 说明 | | --- | --- | --- | --- | | 打印标签 | POST | /operation/print-label/print | 打印标签 | | 最近打印记录 | GET | /operation/print-label/recent-list | 最近打印记录 | | 标签模板列表 | GET | /operation/print-label/template-list | 标签模板列表 | ##### 6.3.3.1 打印标签(POST /operation/print-label/print) ###### 1 接口请求示例 **请求 /operation/print-label/print**(POST) ```json { "number": "IDE2607221859123456", "numberType": "waybill", "labelTemplateId": 1 } ``` > 参数说明:`number` 运单号/客户单号/跟踪号/其他单号;`numberType` 单号类型:waybill(运单号)/customerOrder(客户单号)/tracking(跟踪号)/other(其他单号);`labelTemplateId` 标签模板ID,可选,不传则按 templateType 取默认模板。也可传 `templateType`:lastMile(尾程)/customer(客户)/inbound(入库)/outbound(出库)/sku。 **响应** ```json { "code": 0, "msg": "success", "data": { "id": 1, "waybillNo": "IDE2607221859123456", "customerOrderNo": "", "trackingNo": "", "otherNumber": "", "labelTemplateId": 1, "labelTemplateName": "尾程标签模板1", "printSuccess": 1, "printTime": "2024-01-01 12:00:00", "operatorId": 1, "createTime": "2024-01-01 12:00:00" } } ``` ###### 2 接口业务逻辑 ###### 核心流程 - 按接口定义执行业务流程,完成参数解析、数据处理与结果返回。 ###### 校验与异常 - 对必填参数、状态与数据存在性进行校验,异常场景返回统一错误码与错误信息。 ##### 6.3.3.2 最近打印记录(GET /operation/print-label/recent-list) ###### 1 接口请求示例 **请求 /operation/print-label/recent-list**(GET,Query 参数) ``` ?limit=20&labelTemplateId= ``` **响应** ```json { "code": 0, "msg": "success", "data": { "items": [ { "id": 1, "waybillNo": "IDE2607221859123456", "customerOrderNo": "CUST001", "trackingNo": "TRK001", "otherNumber": "", "labelTemplateId": 1, "labelTemplateName": "尾程标签模板1", "printSuccess": 1, "printTime": "2024-01-01 12:00:00", "operatorId": 1, "operatorName": "张三", "createTime": "2024-01-01 12:00:00" } ] } } ``` ###### 2 接口业务逻辑 ###### 核心流程 - 按接口定义执行业务流程,完成参数解析、数据处理与结果返回。 ###### 校验与异常 - 对必填参数、状态与数据存在性进行校验,异常场景返回统一错误码与错误信息。 ##### 6.3.3.3 标签模板列表(GET /operation/print-label/template-list) ###### 1 接口请求示例 **请求 /operation/print-label/template-list**(GET,Query 参数) ``` ?templateType=lastMile ``` > 参数说明:`templateType` 可选,筛选模板类型:lastMile/customer/inbound/outbound/sku。不传则返回全部可用模板。 **响应** ```json { "code": 0, "msg": "success", "data": { "items": [ { "id": 1, "name": "尾程标签模板1", "templateType": "lastMile", "templateJson": {}, "createTime": "2024-01-01 12:00:00", "updateTime": "2024-01-01 12:00:00" } ] } } ``` ---------------------------------------------------------------------------------------- ---------------------------------------------------------------------------------------- ###### 2 接口业务逻辑 ###### 核心流程 - 按接口定义执行业务流程,完成参数解析、数据处理与结果返回。 ###### 校验与异常 - 对必填参数、状态与数据存在性进行校验,异常场景返回统一错误码与错误信息。 ---------------------------------------------------------------------------------------- ---------------------------------------------------------------------------------------- ## 7 充值管理 ### 7.1 核心需求说明 - **充值流程**:客户提交充值金额 → 选择支付方式(微信/支付宝)→ 上传充值截图 → 提交审核 → 管理员审核(通过/驳回)→ 审核通过后更新客户余额并 **写入账户资金流水**。 - **充值记录**:完整留存充值全流程信息(`t_recharge_record`),支持查询、导出;充值状态包含:待审核、审核通过、审核驳回。 - **资金流水**:所有 **余额变动**(充值入账、订单支付等)统一记入 **`t_customer_account_flow`**,前端「资金流水」页查询 **`GET /customer/account-flow/list`**。 ### 7.2 表结构设计 #### 7.2.1 充值记录表 `t_recharge_record` | 字段名 | 字段类型 | 长度 | 主键/外键 | 非空 | 说明 | | --- | --- | --- | --- | --- | --- | | Id | BIGINT | - | 主键 | 是 | 自增唯一标识 | | CustomerId | BIGINT | - | 外键 → `t_customer.Id` | 是 | 充值归属客户业务主键;子客户登录时是否归并主客户 Id 见 **§1.4** | | SerialNumber | VARCHAR | 64 | - | 是 | 流水号,规则:RECHARGE+日期+随机6位数字 | | RechargeTime | DATETIME | - | - | 是 | 充值提交时间,默认当前时间 | | PayType | TINYINT | - | - | 是 | 支付方式:1-微信,2-支付宝 | | RechargeAmount | DECIMAL | 18,2 | - | 是 | 充值金额,≥0.01 | | RechargeScreenshot | VARCHAR | 255 | - | 否 | 充值截图URL(存储文件服务器地址) | | RechargeStatus | TINYINT | - | - | 是 | 充值状态:0-待审核,1-审核通过,2-审核驳回 | | Remark | VARCHAR | 512 | - | 否 | 备注(客户/审核人填写) | | CreateUserId | INT | - | - | 是 | 创建人 | | CreateTime | DATETIME | - | - | 是 | 创建时间 | | UpdateUserId | INT | - | - | 否 | 更新人 | | UpdateTime | DATETIME | - | - | 否 | 更新时间 | | AuditTime | DATETIME | - | - | 否 | 审核时间 | | AuditBy | VARCHAR | 32 | - | 否 | 审核人(管理员账号) | | IsDeleted | TINYINT | - | - | 是 | 是否删除:0-否,1-是,默认0 | #### 7.2.2 客户账户资金流水表 `t_customer_account_flow` > **定位**:客户 **`t_customer.Balance` 的唯一变动账本**;与 **`t_recharge_record`**(充值申请/审核)分离——后者管「充值业务单据」,本表管「账户余额每一次增减」。 | 字段名 | 字段类型 | 长度 | 主键/外键 | 非空 | 说明 | | --- | --- | --- | --- | --- | --- | | Id | INT | - | 主键 | 是 | 自增唯一标识 | | CustomerId | INT | - | 外键 → `t_customer.Id` | 是 | 流水归属客户 | | SerialNumber | VARCHAR | 64 | 唯一索引 | 是 | 流水号,规则:**`FLOW` + yyyyMMddHHmmss + 6 位随机数** | | BizType | VARCHAR | 32 | - | 是 | 业务类型:`recharge` 充值入账;`orderPay` 订单支付(可扩展) | | BizId | INT | - | - | 否 | 关联业务主键(如 `t_recharge_record.Id`、`t_order.Id`) | | BizNo | VARCHAR | 64 | - | 否 | 关联业务单号(如充值流水号、运单号 `ServiceNo`) | | Amount | DECIMAL | 18,2 | - | 是 | **变动金额(带符号)**:正数=入账,负数=出账 | | BalanceBefore | DECIMAL | 18,2 | - | 是 | 变动前账户余额 | | BalanceAfter | DECIMAL | 18,2 | - | 是 | 变动后账户余额(= BalanceBefore + Amount) | | Currency | VARCHAR | 8 | - | 是 | 币种,默认 **CNY** | | Remark | VARCHAR | 512 | - | 否 | 备注 | | IsDeleted | TINYINT | - | - | 是 | 软删,默认 0 | | ConcurrencyStamp | CHAR | 36 | - | 是 | 并发戳(AggregateRoot 约定);新建表可用 DEFAULT(UUID()),已有表补列见 `migrate_t_customer_account_flow_concurrency.sql`(ALTER 勿用 UUID 默认值,避免复制 Error 1674) | | CreateUserId | INT | - | - | 是 | 创建人 | | CreateTime | DATETIME | - | - | 是 | 创建时间(流水发生时间) | | UpdateUserId | INT | - | - | 否 | 更新人 | | UpdateTime | DATETIME | - | - | 否 | 更新时间 | **索引**: | 索引名 | 字段 | 说明 | | --- | --- | --- | | `uk_customer_account_flow_serial` | SerialNumber | 流水号唯一 | | `idx_customer_account_flow_customer_time` | CustomerId, CreateTime | 客户流水列表 | | `idx_customer_account_flow_biz` | BizType, BizId | 按业务反查 | **迁移脚本**:`sql/migrate_t_customer_account_flow.sql` ### 7.3 核心接口设计 | 接口名称 | 请求方式 | 请求路径 | 请求参数 | 响应参数 | 接口说明 | | --- | --- | --- | --- | --- | --- | | 提交充值申请 | POST | /recharge/recharge/submit | customerId, payType, rechargeAmount, screenshotUrl, remark | code, msg, data.serialNumber | 生成流水号,保存充值记录,状态为待审核 | | 审核充值申请 | POST | /recharge/recharge-record/approve 或 reject | id, auditStatus, auditBy, remark | code, msg | 审核通过则 **`CustomerBalanceMutator` 增余额 + 写 `t_customer_account_flow`**,更新审核时间、状态、审核人 | | 查询充值记录 | GET | /recharge/recharge-record/list | customerId, serialNumber, startTime, endTime, status | list, total | 多条件分页查询充值记录 | | 导出充值记录 | GET | /recharge/export | customerId, startTime, endTime | 文件流 | 导出充值记录为Excel文件 | | 查询账户资金流水 | GET | /customer/account-flow/list | page, pageSize, keyword, bizType, customerId, startTime, endTime | items, total | 分页查询余额变动流水,见 **§7.4** | ##### 6.3.1.1 提交充值申请(POST /recharge/recharge/submit) ###### 1 接口请求示例 **请求 /recharge/recharge/submit**(POST) ```json { "customerId": 1, "rechargeAmount": 500, "payType": 1, "rechargeScreenshot": "https://example.com/upload/screenshot.jpg", "remark": "充值500元" } ``` > 参数说明:`customerId` 为 **`t_customer.Id`**(客户端可不传:用 JWT **`mapId`** 查 `t_customer.UserId` 得到对应 `Id`);`rechargeAmount` 充值金额,≥0.01;`payType` 支付方式:1-微信,2-支付宝;`rechargeScreenshot` 充值截图URL;`remark` 备注。 **响应** ```json { "code": 0, "msg": "success", "data": { "id": 1, "serialNumber": "RECHARGE2024010100123456", "customerId": 1, "rechargeAmount": 500.00, "payType": 1, "rechargeScreenshot": "https://example.com/upload/screenshot.jpg", "rechargeStatus": 0, "remark": "充值500元", "rechargeTime": "2024-01-01 12:00:00", "createTime": "2024-01-01 12:00:00" } } ``` ###### 2 接口业务逻辑 ###### 核心流程 - 创建充值记录并生成流水号,初始状态为待审核。 ###### 校验与异常 - 校验金额、支付方式与截图信息合法性,不合法时返回业务错误。 ##### 6.3.1.2 审核充值申请-通过(POST /recharge/recharge-record/approve) ###### 1 接口请求示例 **请求 /recharge/recharge-record/approve**(POST) ```json { "id": 1, "remark": "审核通过" } ``` **响应** ```json { "code": 0, "msg": "success", "data": null } ``` ###### 2 接口业务逻辑 ###### 核心流程 1. 校验记录 **`RechargeStatus = Pending`**,否则 **「只有待审核记录才能审核通过」**。 2. 更新 **`t_recharge_record`**:状态=审核通过,写入审核人、审核时间、备注。 3. 调用 **`CustomerBalanceMutator.CreditRechargeAsync`**: - **`t_customer.Balance += RechargeAmount`**; - 插入 **`t_customer_account_flow`**:`BizType=recharge`,`BizId=record.Id`,`BizNo=record.SerialNumber`,**`Amount` 为正数**,记录变动前后余额。 > 步骤 2~3 在同一工作单元内提交;**不再**单独 `Balance +=` 而不写流水。 ###### 校验与异常 - 校验记录状态必须为待审核;重复审核或状态冲突时返回业务错误。 - 客户不存在 → **「客户不存在」**。 ##### 6.3.1.3 审核充值申请-驳回(POST /recharge/recharge-record/reject) ###### 1 接口请求示例 **请求 /recharge/recharge-record/reject**(POST) ```json { "id": 1, "remark": "截图不清晰,请重新上传" } ``` **响应** ```json { "code": 0, "msg": "success", "data": null } ``` ###### 2 接口业务逻辑 ###### 核心流程 - 审核驳回后更新充值状态与审核备注,不变更客户余额。 ###### 校验与异常 - 校验记录状态与审核权限,非法操作时返回业务错误。 ##### 6.3.1.4 查询充值记录(GET /recharge/recharge-record/list) ###### 1 接口请求示例 **请求 /recharge/recharge-record/list**(GET,Query 参数) ``` ?page=1&pageSize=20&customerId=1&serialNumber=&rechargeStatus=0&startTime=2024-01-01&endTime=2024-01-31 ``` **响应** ```json { "code": 0, "msg": "success", "data": { "items": [ { "id": 1, "customerId": 1, "customerCode": "W2501", "customerPhone": "13800138000", "serialNumber": "RECHARGE2024010100123456", "rechargeTime": "2024-01-01 12:00:00", "payType": 1, "payTypeName": "微信", "rechargeAmount": 500.00, "rechargeScreenshot": "https://example.com/upload/screenshot.jpg", "rechargeStatus": 0, "rechargeStatusName": "待审核", "remark": "充值500元", "auditTime": null, "auditBy": null, "createTime": "2024-01-01 12:00:00", "updateTime": "2024-01-01 12:00:00" } ], "total": 20 } } ``` ###### 2 接口业务逻辑 ###### 核心流程 - 按客户、流水号、状态和时间区间进行分页查询,返回记录列表与总数。 ###### 校验与异常 - 校验时间区间与分页参数合法性,查询异常时返回统一错误码。 ##### 6.3.1.5 导出充值记录(GET /recharge/export) ###### 1 接口请求示例 **请求 /recharge/export**(GET,Query 参数) ``` ?customerId=1&startTime=2024-01-01&endTime=2024-01-31 ``` **响应** > 返回 Excel 文件流,Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet ###### 2 接口业务逻辑 ###### 核心流程 - 根据筛选条件导出充值记录为 Excel 文件流。 ###### 校验与异常 - 校验导出条件与数据权限,导出失败时返回统一错误信息。 ### 7.4 客户账户资金流水 #### 7.4.1 设计说明 | 概念 | 表 / 组件 | 说明 | | --- | --- | --- | | 账户余额(权威) | `t_customer.Balance` | 客户当前可用余额;**仅通过 `CustomerBalanceMutator` 变更** | | 充值业务单据 | `t_recharge_record` | 充值申请、截图、审核状态;**不等同于**资金流水 | | 资金流水(账本) | `t_customer_account_flow` | 每一次余额增减的 **不可变记录**,含变动前后余额 | | 余额变动服务 | `CustomerBalanceMutator` | 统一 **改 Balance + 插流水**;充值审核、订单支付均调用 | **BizType 枚举(当前)**: | BizType | 中文 | 触发场景 | Amount 符号 | | --- | --- | --- | --- | | `recharge` | 充值入账 | `POST /recharge/recharge-record/approve` 审核通过 | **正** | | `orderPay` | 订单支付 | `POST /order/pay` 待支付订单扣款 | **负** | #### 7.4.2 余额变动业务逻辑(`CustomerBalanceMutator`) **公共步骤 `ApplyAsync`**: 1. 读取 **`balanceBefore = customer.Balance`**,计算 **`balanceAfter = balanceBefore + signedAmount`**。 2. 若 **`signedAmount == 0`** → **「流水金额不能为 0」**;若 **`balanceAfter < 0`** → **「账户余额不足」**。 3. 更新 **`customer.Balance = balanceAfter`** 并 **`UpdateAsync`**。 4. 生成唯一 **`SerialNumber`**(`FLOW{yyyyMMddHHmmss}{random6}`,冲突重试最多 5 次)。 5. 插入 **`t_customer_account_flow`**,写入 `BizType`、`BizId`、`BizNo`、`Amount`(带符号)、`BalanceBefore`、`BalanceAfter`、`Currency=CNY`、`Remark`。 **封装方法**: | 方法 | 调用方 | 说明 | | --- | --- | --- | | `CreditRechargeAsync` | `RechargeService.ApproveAsync` | 入账金额为正 | | `DebitOrderPayAsync` | `OrderService.PayAsync` | 出账金额为负;**payAmount 须 > 0** | > 实现类:`Oms.Application.Shared.CustomerBalanceMutator`;仓储:`ICustomerAccountFlowRepository` / `CustomerAccountFlowRepository`。 #### 7.4.3 查询账户流水(GET /customer/account-flow/list) **服务**:`CustomerAccountFlowService.GetPaginatedListAsync` **分组**:OpenAPI **`OMS-Customer`** **Query 参数**: | 参数 | 类型 | 必填 | 说明 | | --- | --- | --- | --- | | page | int | 否 | 页码,默认 1 | | pageSize | int | 否 | 每页条数,默认 20 | | keyword | string | 否 | 模糊匹配 **流水号 SerialNumber** 或 **关联单号 BizNo** | | bizType | string | 否 | `recharge` / `orderPay`;空或 `all` 表示全部 | | customerId | int | 否 | 管理员可按客户筛选;**客户登录时由会话强制过滤为本客户 `t_customer.Id`** | | startTime | datetime | 否 | 创建时间起(与 endTime 成对使用) | | endTime | datetime | 否 | 创建时间止 | **响应 `items[]` 字段**: | 字段 | 说明 | | --- | --- | | id | 流水主键 | | customerId | 客户 Id | | customerCode | 客户编码(列表补齐) | | serialNumber | 流水号 | | bizType | 业务类型码 | | bizTypeName | 业务类型中文(充值入账 / 订单支付) | | bizId | 关联业务 Id | | bizNo | 关联单号 | | amount | 变动金额(正/负) | | balanceBefore | 变动前余额 | | balanceAfter | 变动后余额 | | currency | 币种 | | remark | 备注 | | createTime | 流水时间 | **请求示例** ``` GET /customer/account-flow/list?page=1&pageSize=50&bizType=orderPay&startTime=2026-06-01%2000:00:00&endTime=2026-06-30%2023:59:59 ``` **响应示例** ```json { "code": 0, "msg": "success", "data": { "items": [ { "id": 1, "customerId": 10, "customerCode": "C001", "serialNumber": "FLOW20260626164200123456", "bizType": "orderPay", "bizTypeName": "订单支付", "bizId": 24, "bizNo": "LP202606261642156945", "amount": -128.50, "balanceBefore": 500.00, "balanceAfter": 371.50, "currency": "CNY", "remark": "订单支付 LP202606261642156945", "createTime": "2026-06-26 16:42:15" } ], "total": 1 } } ``` ###### 校验与隔离 - 客户门户:**`ResolveListCustomerIdAsync`** 强制 `CustomerId` 过滤,**不可**查看他人流水。 - 管理员:`customerId` 为空时查全部客户流水;传 `customerId` 时按指定客户过滤。 #### 7.4.4 前端(itfx-front) | 项 | 说明 | | --- | --- | | 路由 | `/user/accountFlow`,菜单 **客户 → 资金流水** | | 页面 | `src/views/user/accountFlow/index.vue` | | 表格 | **HandsontablePro** + **PaginationPro** | | 入口 | 顶部导航余额下拉「资金流水」;支付成功后触发 **`oms:customer-balance-changed`** 刷新余额展示 | ---------------------------------------------------------------------------------------- ---------------------------------------------------------------------------------------- ## 8 系统主数据 ### 8.1 核心需求说明 - **仓库地址**:管理仓库收货地址信息,包括主仓/中转仓/退货仓/其他类型;支持收件人、联系电话、收货地址、客服电话、客服微信及二维码等配置。 - **国家管理**:维护国家基础资料(中文名称、英文名称、二字代码),供渠道分区、下单地址等模块复用。 - **字典管理**:维护 **`t_dict`**,供全平台下拉与状态映射复用。**与客户、内部员工相关的 `DictType` 及编码含义**在 **`docs/rbac系统设计-060801.md`** 定义与迭代;OMS 仅保证业务侧按 **`DictType` + `DictCode`** 引用时与实现一致。本篇表结构中 **`DictType` 示例**以 **仓库、通知等纯业务枚举** 为主。 - **其他配置**:维护 **`t_system_other_config`**,以 **卡片式** 页面(路由 **`/system/otherConfig`**)管理第三方集成参数;当前首条业务为 **小包轨迹服务**(`ConfigCode = track_service`),供订单预报成功后异步调用外部 **`POST {BaseUrl}/OrderV2/UpsertSmallBag`**。运行时优先读库,缺省字段回退 **`appsettings` → `Oms:TrackService`**。 ### 8.2 表结构设计 #### 8.2.1 与 RBAC 文档的职责划分 **权限系统**(认证、内部用户、客户主数据、角色权限、菜单、组织等)**全部由 `docs/rbac系统设计-060801.md` 承担**;OMS **「8 系统主数据」** 只描述 **业务侧主数据**:**国家、字典、通知、仓库地址、其他配置** 等(见下)。 #### 8.2.2 国家表 `t_country` | 字段名 | 字段类型 | 长度 | 主键/外键 | 非空 | 说明 | | --- | --- | --- | --- | --- | --- | | Id | BIGINT | - | 主键 | 是 | 自增唯一标识 | | ChineseName | VARCHAR | 128 | - | 是 | 中文名称 | | EnglishName | VARCHAR | 128 | - | 是 | 英文名称 | | CountryCode | VARCHAR | 2 | - | 是 | 国家二字代码(ISO 3166-1 alpha-2) | | IsDeleted | TINYINT | - | - | 是 | 是否删除:0-否,1-是,默认0 | | CreateUserId | INT | - | - | 是 | 创建人 | | CreateTime | DATETIME | - | - | 是 | 创建时间 | | UpdateUserId | INT | - | - | 否 | 更新人 | | UpdateTime | DATETIME | - | - | 否 | 更新时间 | #### 8.2.3 字典表 `t_dict` | 字段名 | 字段类型 | 长度 | 主键/外键 | 非空 | 说明 | | --- | --- | --- | --- | --- | --- | | Id | BIGINT | - | 主键 | 是 | 自增唯一标识 | | DictType | VARCHAR | 64 | - | 是 | 字典类型(如 **`notice_type`**、**`warehouse_type`** 等 OMS 业务枚举;与客户/员工相关的 `DictType` 由 **RBAC 文档** 统一约定) | | DictSort | INT | - | - | 是 | 字典排序,值越小越靠前 | | DictCode | VARCHAR | 64 | - | 是 | 字典编码(同类型下唯一) | | DictName | VARCHAR | 128 | - | 是 | 字典名称 | | IsDeleted | TINYINT | - | - | 是 | 是否删除:0-否,1-是,默认0 | | CreateUserId | INT | - | - | 是 | 创建人 | | CreateTime | DATETIME | - | - | 是 | 创建时间 | | UpdateUserId | INT | - | - | 否 | 更新人 | | UpdateTime | DATETIME | - | - | 否 | 更新时间 | #### 8.2.4 公告通知表 `t_notice` | 字段名 | 字段类型 | 长度 | 主键/外键 | 非空 | 说明 | | --- | --- | --- | --- | --- | --- | | Id | BIGINT | - | 主键 | 是 | 自增唯一标识 | | NoticeType | VARCHAR | 16 | - | 是 | 通知类型:notice-公告,message-站内信 | | Title | VARCHAR | 200 | - | 是 | 标题 | | Content | TEXT | - | - | 是 | 内容 | | IsPopup | TINYINT | - | - | 是 | 是否弹窗:0-否,1-是 | | ReceiverType | VARCHAR | 16 | - | 是 | 接收对象;**当前实现仅支持 `user`**(见 **§10.3.5**) | | ReceiverIds | JSON | - | - | 是 | 接收人 ID 列表;存 **`user.userid`**(INT)数组,非 `t_customer.Id` | | IsDeleted | TINYINT | - | - | 是 | 是否删除:0-否,1-是,默认0 | | CreateUserId | INT | - | - | 是 | 创建人 | | CreateTime | DATETIME | - | - | 是 | 创建时间 | | UpdateUserId | INT | - | - | 否 | 更新人 | | UpdateTime | DATETIME | - | - | 否 | 更新时间 | #### 8.2.5 通知已读明细表 `t_notice_read` | 字段名 | 字段类型 | 长度 | 主键/外键 | 非空 | 说明 | | --- | --- | --- | --- | --- | --- | | Id | BIGINT | - | 主键 | 是 | 自增唯一标识 | | NoticeId | BIGINT | - | 外键(关联 `t_notice.Id`) | 是 | 通知 ID | | ReceiverType | VARCHAR | 16 | - | 是 | 接收对象;**当前实现固定为 `user`** | | ReceiverId | BIGINT | - | - | 是 | 已读主体 ID = **`user.userid`**(JWT `mapId`) | | ReadTime | DATETIME | - | - | 是 | 已读时间 | | CreateTime | DATETIME | - | - | 是 | 创建时间 | #### 8.2.6 仓库地址表 `t_warehouse_address` | 字段名 | 字段类型 | 长度 | 主键/外键 | 非空 | 说明 | | --- | --- | --- | --- | --- | --- | | Id | BIGINT | - | 主键 | 是 | 自增唯一标识 | | WarehouseName | VARCHAR | 128 | - | 是 | 仓库名称 | | WarehouseType | VARCHAR | 32 | - | 是 | main/transit/return/other | | Recipient | VARCHAR | 64 | - | 否 | 收件人 | | ContactPhone | VARCHAR | 32 | - | 否 | 联系电话 | | ShippingAddress | TEXT | - | - | 否 | 收货地址 | | ServicePhone | VARCHAR | 32 | - | 否 | 客服电话 | | ServiceWechat | VARCHAR | 64 | - | 否 | 客服微信 | | ServiceWechatFiles | JSON | - | - | 否 | 微信二维码图片 | | IsDeleted | TINYINT | - | - | 是 | 是否删除:0-否,1-是,默认0 | | CreateUserId | INT | - | - | 是 | 创建人 | | CreateTime | DATETIME | - | - | 是 | 创建时间 | | UpdateUserId | INT | - | - | 否 | 更新人 | | UpdateTime | DATETIME | - | - | 否 | 更新时间 | #### 8.2.7 系统其他配置表 `t_system_other_config` > **迁移脚本**:`sql/migrate_t_system_other_config.sql`(增量);全量建表见根目录 **`db.sql`**。 | 字段名 | 字段类型 | 长度 | 主键/外键 | 非空 | 说明 | | --- | --- | --- | --- | --- | --- | | Id | INT | - | 主键 | 是 | 自增唯一标识 | | ConfigCode | VARCHAR | 64 | - | 是 | 配置编码,**全局唯一**(如 **`track_service`**) | | ConfigName | VARCHAR | 128 | - | 是 | 配置名称(展示用,如「小包轨迹服务」) | | Enabled | TINYINT | - | - | 是 | 是否启用:0-否,1-是,默认 0 | | BaseUrl | VARCHAR | 512 | - | 否 | 第三方服务根地址(轨迹示例:`http://api.nanafa.com/track`) | | AppKey | VARCHAR | 128 | - | 否 | 接入 AppKey | | AppSecret | VARCHAR | 256 | - | 否 | 接入 AppSecret(**敏感**,列表接口脱敏) | | TimeoutSeconds | INT | - | - | 是 | HTTP 超时秒数,默认 30 | | Remark | VARCHAR | 512 | - | 否 | 备注 | | IsDeleted | TINYINT | - | - | 是 | 是否删除:0-否,1-是,默认 0 | | CreateUserId | INT | - | - | 是 | 创建人 | | CreateTime | DATETIME | - | - | 是 | 创建时间 | | UpdateUserId | INT | - | - | 否 | 更新人 | | UpdateTime | DATETIME | - | - | 否 | 更新时间 | **索引** | 索引名 | 类型 | 字段 | 说明 | | --- | --- | --- | --- | | uk_system_other_config_code | UNIQUE | ConfigCode | 配置编码唯一 | | idx_system_other_config_enabled | KEY | Enabled, Id | 按启用状态筛选 | **预置配置编码(`ConfigCode`)** | ConfigCode | ConfigName(建议) | 消费方 | 说明 | | --- | --- | --- | --- | | `track_service` | 小包轨迹服务 | 订单预报成功后异步轨迹同步 | 调用 **`POST {BaseUrl}/OrderV2/UpsertSmallBag`**;请求公共参数含 `appKey`、`timestamp`、`sign`;**签名**:`MD5("appKey={appKey}×tamp={timestamp}&appSecret={appSecret}")` 转 **大写十六进制** | **运行时读取优先级(以 `track_service` 为例)** 1. 查询 **`t_system_other_config`**:`ConfigCode = track_service` 且未删除;各字段非空则覆盖对应运行时参数。 2. 若库中无记录或某字段为空,回退 **`Oms:TrackService`**(`appsettings` / Consul):`Enabled`、`BaseUrl`、`AppKey`、`AppSecret`、`TimeoutSeconds`。 3. `Enabled = false` 或 `BaseUrl` / `AppKey` / `AppSecret` 不完整时,**跳过** UpsertSmallBag(仅记 Debug 日志,不影响订单主流程)。 ### 8.3 核心接口设计 > **登录、刷新、当前用户**:`POST /rbac/auth/login`、`POST /rbac/auth/refresh-token`、`GET /rbac/auth/get-current-user` 等路径与契约见 **`docs/rbac系统设计-060801.md`** 与 OpenAPI;**不在本篇逐条列出**。 | 接口名称 | 请求方式 | 请求路径 | 说明 | | --- | --- | --- | --- | | 国家列表 | GET | /system/country/list | 查询国家列表 | | 字典列表 | GET | /system/dict/list | 按类型分页查询字典 | | 新增字典 | POST | /system/dict/create | 新增字典项 | | 更新字典 | POST | /system/dict/update | 更新字典项 | | 删除字典 | POST | /system/dict/delete | 删除字典项 | | 仓库列表 | GET | /system/warehouse/list | 仓库列表 | | 仓库详情 | GET | /system/warehouse/detail | 仓库详情 | | 新增仓库 | POST | /system/warehouse/create | 新增 | | 更新仓库 | POST | /system/warehouse/update | 更新 | | 删除仓库 | POST | /system/warehouse/delete | 删除 | | 其他配置列表 | GET | /system/other-config/list | 分页查询其他配置(AppSecret 脱敏) | | 其他配置详情 | GET | /system/other-config/detail | 按 Id 查询详情(含完整 AppSecret,供编辑) | | 新增其他配置 | POST | /system/other-config/create | 新增 | | 更新其他配置 | POST | /system/other-config/update | 更新(AppSecret 留空或 `******` 表示不修改) | | 删除其他配置 | POST | /system/other-config/delete | 逻辑删除 | | 通知列表 | GET | /system/notice/list | 分页查询通知 | | 新增通知 | POST | /system/notice/create | 新增公告/站内信 | | 编辑通知 | POST | /system/notice/update | 编辑公告/站内信 | | 删除通知 | POST | /system/notice/delete | 删除公告/站内信 | | 确认已读 | POST | /system/notice/read-confirm | 当前用户/客户确认已读 | ### 8.4 系统主数据接口详细设计(`/system/*` 等) > **范围**:本章 **`/system/*`** 及 **`t_country` / `t_dict` / `t_notice` / `t_warehouse_address` / `t_system_other_config`**;**认证、令牌、权限码、RBAC/客户 HTTP** 见 **`docs/rbac系统设计-060801.md`** 与 **OpenAPI/Swagger**(`/rbac/*`、`/customer/*`)。 ##### 8.4.1.1 国家列表(GET /system/country/list) ###### 1 接口请求示例 **请求 /system/country/list**(GET) > 无请求参数,返回全部国家字段。 **响应** ```json { "code": 0, "msg": "success", "data": [ { "id": 1, "chineseName": "中国", "englishName": "China", "countryCode": "CN" }, { "id": 2, "chineseName": "美国", "englishName": "United States", "countryCode": "US" } ] } ``` ###### 2 接口业务逻辑 ###### 核心流程 - 查询 `t_country` 全量有效数据,按 `countryCode` 或 `englishName` 排序返回。 ###### 校验与异常 - 仅返回未删除记录;无数据时返回空数组。 ##### 8.4.1.2 字典列表(GET /system/dict/list) ###### 1 接口请求示例 **请求 /system/dict/list**(GET,Query 参数) ``` ?page=1&pageSize=20&dictType=warehouse_type&keyword=主仓 ``` **响应** ```json { "code": 0, "msg": "success", "data": { "items": [ { "id": 1, "dictType": "warehouse_type", "dictSort": 1, "dictCode": "main", "dictName": "主仓", "createTime": "2026-04-27 10:00:00", "updateTime": "2026-04-27 10:00:00" } ], "total": 1 } } ``` ###### 2 接口业务逻辑 ###### 核心流程 - 按 `dictType`、关键词分页查询 `t_dict`,按 `dictSort` 升序、`id` 降序返回。 ###### 校验与异常 - 校验分页参数合法性;仅返回未删除记录,查询异常时返回统一错误码。 ##### 8.4.1.3 新增字典(POST /system/dict/create) ###### 1 接口请求示例 **请求 /system/dict/create**(POST) ```json { "dictType": "warehouse_type", "dictSort": 1, "dictCode": "main", "dictName": "主仓" } ``` **响应** ```json { "code": 0, "msg": "success", "data": null } ``` ###### 2 接口业务逻辑 ###### 核心流程 - 新增字典项并写入创建审计信息。 ###### 校验与异常 - 校验 `dictType`、`dictSort`、`dictCode`、`dictName` 必填及格式合法性。 - 校验同一 `dictType` 下 `dictCode` 唯一,重复时返回业务错误。 ##### 8.4.1.4 更新字典(POST /system/dict/update) ###### 1 接口请求示例 **请求 /system/dict/update**(POST) ```json { "id": 1, "dictType": "warehouse_type", "dictSort": 2, "dictCode": "transit", "dictName": "中转仓" } ``` **响应** ```json { "code": 0, "msg": "success", "data": null } ``` ###### 2 接口业务逻辑 ###### 核心流程 - 根据字典 ID 更新字典类型、排序、编码、名称及审计信息。 ###### 校验与异常 - 校验 `id` 存在且未删除,更新字段合法。 - 若更新后 `dictType + dictCode` 与其他记录冲突,返回业务错误。 ##### 8.4.1.5 删除字典(POST /system/dict/delete) ###### 1 接口请求示例 **请求 /system/dict/delete**(POST) ```json { "id": 1 } ``` **响应** ```json { "code": 0, "msg": "success", "data": null } ``` ###### 2 接口业务逻辑 ###### 核心流程 - 根据字典 ID 执行逻辑删除并保留历史审计信息。 ###### 校验与异常 - 校验 `id` 存在与删除前置条件,不存在或已删除时返回业务错误。 ##### 8.4.1.6 仓库列表(GET /system/warehouse/list) ###### 1 接口请求示例 **请求 /system/warehouse/list**(GET,Query 参数) ``` ?page=1&pageSize=20&warehouseType=main&keyword= ``` **响应** ```json { "code": 0, "msg": "success", "data": { "items": [ { "id": 1, "warehouseName": "深圳主仓", "warehouseType": "main", "warehouseTypeName": "主仓", "recipient": "张三", "contactPhone": "0755-12345678", "shippingAddress": "广东省深圳市南山区xxx路xxx号", "servicePhone": "400-xxx-xxxx", "serviceWechat": "itfx_service", "serviceWechatFiles": ["https://example.com/qrcode1.jpg"], "createTime": "2024-01-01 12:00:00", "updateTime": "2024-01-01 12:00:00" } ], "total": 5 } } ``` ###### 2 接口业务逻辑 ###### 核心流程 - 按仓库类型与关键词分页查询仓库地址信息列表。 ###### 校验与异常 - 校验分页参数与筛选条件合法性,异常时返回统一错误码。 ##### 8.4.1.7 仓库详情(GET /system/warehouse/detail) ###### 1 接口请求示例 **请求 /system/warehouse/detail**(GET,Query 参数) ``` ?id=1 ``` **响应** ```json { "code": 0, "msg": "success", "data": { "id": 1, "warehouseName": "深圳主仓", "warehouseType": "main", "recipient": "张三", "contactPhone": "0755-12345678", "shippingAddress": "广东省深圳市南山区xxx路xxx号", "servicePhone": "400-xxx-xxxx", "serviceWechat": "itfx_service", "serviceWechatFiles": ["https://example.com/qrcode1.jpg"], "createTime": "2024-01-01 12:00:00", "createBy": "admin", "updateTime": "2024-01-01 12:00:00", "updateBy": "admin" } } ``` ###### 2 接口业务逻辑 ###### 核心流程 - 根据仓库 ID 查询仓库详情,返回完整地址与客服信息。 ###### 校验与异常 - 校验 `id` 必填且存在,不存在时返回业务错误。 ##### 8.4.1.8 新增仓库(POST /system/warehouse/create) ###### 1 接口请求示例 **请求 /system/warehouse/create**(POST) ```json { "warehouseName": "深圳主仓", "warehouseType": "main", "recipient": "张三", "contactPhone": "0755-12345678", "shippingAddress": "广东省深圳市南山区xxx路xxx号", "servicePhone": "400-xxx-xxxx", "serviceWechat": "itfx_service", "serviceWechatFiles": ["https://example.com/qrcode1.jpg"] } ``` **响应** ```json { "code": 0, "msg": "success", "data": null } ``` ###### 2 接口业务逻辑 ###### 核心流程 - 新增仓库地址配置并写入创建审计信息。 ###### 校验与异常 - 校验仓库名称、类型及地址字段完整性,不合法时返回业务错误。 ##### 8.4.1.9 更新仓库(POST /system/warehouse/update) ###### 1 接口请求示例 **请求 /system/warehouse/update**(POST) ```json { "id": 1, "warehouseName": "深圳主仓", "warehouseType": "main", "recipient": "张三", "contactPhone": "0755-12345678", "shippingAddress": "广东省深圳市南山区xxx路xxx号", "servicePhone": "400-xxx-xxxx", "serviceWechat": "itfx_service", "serviceWechatFiles": ["https://example.com/qrcode1.jpg"] } ``` **响应** ```json { "code": 0, "msg": "success", "data": null } ``` ###### 2 接口业务逻辑 ###### 核心流程 - 根据仓库 ID 更新仓库基础信息与联系方式。 ###### 校验与异常 - 校验 `id` 存在与更新字段合法性,更新失败时返回业务错误。 ##### 8.4.1.10 删除仓库(POST /system/warehouse/delete) ###### 1 接口请求示例 **请求 /system/warehouse/delete**(POST) ```json { "id": 1 } ``` **响应** ```json { "code": 0, "msg": "success", "data": null } ``` ###### 2 接口业务逻辑 ###### 核心流程 - 根据仓库 ID 执行逻辑删除并保留历史记录。 ###### 校验与异常 - 校验 `id` 存在与删除前置条件,冲突时返回业务错误。 ##### 8.4.1.11 通知列表(GET /system/notice/list) ###### 1 接口请求示例 **请求 /system/notice/list**(GET,Query 参数) ``` ?page=1&pageSize=20¬iceType=notice&receiverType=user&keyword=活动 ``` **响应** ```json { "code": 0, "msg": "success", "data": { "items": [ { "id": 101, "noticeType": "notice", "noticeTypeName": "公告", "title": "五一放假通知", "content": "仓库 5 月 1 日-5 月 3 日暂停收货。", "isPopup": 1, "receiverType": "user", "receiverTypeName": "用户", "receiverIds": [1001, 1002, 1003], "readCount": 2, "receiverCount": 3, "createTime": "2026-04-25 12:00:00", "updateTime": "2026-04-25 12:00:00" } ], "total": 1 } } ``` ###### 2 接口业务逻辑 ###### 核心流程 - 按通知类型、接收对象、关键词分页查询 `t_notice`。 - 已读数通过聚合 `t_notice_read`(按 `NoticeId` + `ReceiverId` 去重)计算,接收人总数按 `receiverIds` 统计。 ###### 校验与异常 - 校验分页参数合法性,仅返回未删除记录;异常时返回统一错误码。 ##### 8.4.1.12 新增通知(POST /system/notice/create) ###### 1 接口请求示例 **请求 /system/notice/create**(POST) ```json { "noticeType": "notice", "title": "五一放假通知", "content": "仓库 5 月 1 日-5 月 3 日暂停收货。", "isPopup": 1, "receiverType": "user", "receiverIds": [1001, 1002, 1003] } ``` **响应** ```json { "code": 0, "msg": "success", "data": null } ``` ###### 2 接口业务逻辑 ###### 核心流程 - 新增公告/站内信并写入接收对象与接收人列表。 ###### 校验与异常 - 校验 `noticeType`、`title`、`content`、`isPopup`、`receiverType`、`receiverIds` 必填及枚举合法性。 - `receiverIds` 为空或包含无效用户/客户时返回业务错误。 ##### 8.4.1.13 编辑通知(POST /system/notice/update) ###### 1 接口请求示例 **请求 /system/notice/update**(POST) ```json { "id": 101, "noticeType": "message", "title": "账单补充说明", "content": "请于本周内完成 4 月账单核对。", "isPopup": 0, "receiverType": "customer", "receiverIds": [2001, 2002] } ``` **响应** ```json { "code": 0, "msg": "success", "data": null } ``` ###### 2 接口业务逻辑 ###### 核心流程 - 根据通知 ID 更新通知主体内容与接收范围。 - 如接收人发生变化,清理 `t_notice_read` 中不在新接收范围内的已读明细。 ###### 校验与异常 - 校验 `id` 存在且未删除,更新字段合法。 - 已发布通知可编辑但必须保留至少 1 个接收人。 ##### 8.4.1.14 删除通知(POST /system/notice/delete) ###### 1 接口请求示例 **请求 /system/notice/delete**(POST) ```json { "id": 101 } ``` **响应** ```json { "code": 0, "msg": "success", "data": null } ``` ###### 2 接口业务逻辑 ###### 核心流程 - 根据通知 ID 执行逻辑删除,保留历史审计信息。 ###### 校验与异常 - 校验 `id` 必填且存在,不存在或已删除时返回业务错误。 ##### 8.4.1.15 确认已读(POST /system/notice/read-confirm) ###### 1 接口请求示例 **请求 /system/notice/read-confirm**(POST) ```json { "id": 101 } ``` **响应** ```json { "code": 0, "msg": "success", "data": null } ``` ###### 2 接口业务逻辑 ###### 核心流程 - 当前登录主体调用后,在 `t_notice_read` 中按 `NoticeId + ReceiverType + ReceiverId` 幂等写入已读记录(**`ReceiverId` = 当前 JWT `mapId`**,即 `user.userid`)。 ###### 校验与异常 - 校验通知存在且未删除。 - 校验当前主体在通知接收范围内,不在范围内时返回无权限错误。 ##### 8.4.1.16 其他配置列表(GET /system/other-config/list) ###### 1 接口请求示例 **请求 /system/other-config/list**(GET,Query 参数) ``` ?page=1&pageSize=30&keyword=track ``` **响应** ```json { "code": 0, "msg": "success", "data": { "items": [ { "id": 1, "configCode": "track_service", "configName": "小包轨迹服务", "enabled": true, "baseUrl": "http://api.nanafa.com/track", "appKey": "YOUR_APP_KEY", "appSecret": "******", "timeoutSeconds": 30, "remark": "", "createTime": "2026-06-27 10:00:00", "updateTime": "2026-06-27 10:00:00" } ], "total": 1 } } ``` ###### 2 接口业务逻辑 ###### 核心流程 - 按关键词(匹配 `ConfigCode`、`ConfigName`)分页查询 `t_system_other_config`,按 `Id` 降序返回。 - 列表项 **`appSecret`** 固定脱敏为 **`******`**(未配置时返回空)。 ###### 校验与异常 - 校验分页参数合法性;仅返回未删除记录。 ##### 8.4.1.17 其他配置详情(GET /system/other-config/detail) ###### 1 接口请求示例 **请求 /system/other-config/detail**(GET,Query 参数) ``` ?id=1 ``` **响应** ```json { "code": 0, "msg": "success", "data": { "id": 1, "configCode": "track_service", "configName": "小包轨迹服务", "enabled": true, "baseUrl": "http://api.nanafa.com/track", "appKey": "YOUR_APP_KEY", "appSecret": "YOUR_APP_SECRET", "timeoutSeconds": 30, "remark": "", "createTime": "2026-06-27 10:00:00", "updateTime": "2026-06-27 10:00:00" } } ``` ###### 2 接口业务逻辑 ###### 核心流程 - 根据主键查询单条配置,返回完整字段(含 **未脱敏** `appSecret`),供管理端编辑表单回填。 ###### 校验与异常 - 校验 `id` 必填且存在,不存在时返回业务错误 **「配置不存在」**。 ##### 8.4.1.18 新增其他配置(POST /system/other-config/create) ###### 1 接口请求示例 **请求 /system/other-config/create**(POST) ```json { "configCode": "track_service", "configName": "小包轨迹服务", "enabled": true, "baseUrl": "http://api.nanafa.com/track", "appKey": "YOUR_APP_KEY", "appSecret": "YOUR_APP_SECRET", "timeoutSeconds": 30, "remark": "" } ``` **响应** ```json { "code": 0, "msg": "success", "data": null } ``` ###### 2 接口业务逻辑 ###### 核心流程 - 校验入参后写入 `t_system_other_config` 并记录创建审计信息。 - 当 `ConfigCode = track_service` 时,后续订单 **预报成功** 将按本配置异步调用轨迹 UpsertSmallBag。 ###### 校验与异常 - `configCode`、`configName` 必填;`configCode` **全局唯一**,重复时返回 **「配置编码已存在」**。 - 当 `configCode = track_service` 时:`baseUrl`、`appKey`、`appSecret` 均必填,否则分别返回 **「轨迹服务地址不能为空」**、**「轨迹服务 AppKey 不能为空」**、**「轨迹服务 AppSecret 不能为空」**。 - `timeoutSeconds` 小于等于 0 时落库按 **30** 处理。 ##### 8.4.1.19 更新其他配置(POST /system/other-config/update) ###### 1 接口请求示例 **请求 /system/other-config/update**(POST) ```json { "id": 1, "configCode": "track_service", "configName": "小包轨迹服务", "enabled": true, "baseUrl": "http://api.nanafa.com/track", "appKey": "YOUR_APP_KEY", "appSecret": "", "timeoutSeconds": 30, "remark": "生产环境" } ``` **响应** ```json { "code": 0, "msg": "success", "data": null } ``` ###### 2 接口业务逻辑 ###### 核心流程 - 根据 `id` 更新配置;**`appSecret` 为空或为 `******` 时不修改库中已有密钥**(其余字段正常更新)。 ###### 校验与异常 - 校验 `id` 存在;`configCode` 唯一性(排除自身)。 - `configCode = track_service` 时:`baseUrl`、`appKey` 仍必填;`appSecret` 仅在传入新值时更新。 ##### 8.4.1.20 删除其他配置(POST /system/other-config/delete) ###### 1 接口请求示例 **请求 /system/other-config/delete**(POST) ```json { "id": 1 } ``` **响应** ```json { "code": 0, "msg": "success", "data": null } ``` ###### 2 接口业务逻辑 ###### 核心流程 - 根据主键执行逻辑删除;删除后轨迹同步回退 **`Oms:TrackService`** 或跳过(视 appsettings 与 `Enabled` 而定)。 ###### 校验与异常 - 校验 `id` 必填且存在,不存在时返回 **「配置不存在」**。 ---------------------------------------------------------------------------------------- ---------------------------------------------------------------------------------------- ## 9 需求迭代与变更记录 ### 9.1 需求迭代目录(按日期管理) > 迭代编号建议使用 `YYYYMMDD-序号`,按日期倒序维护;不同功能迭代可使用 `-01`、`-02` 递增。 | 迭代日期 | 迭代编号 | 迭代功能 | 变更模块 | 备注 | | --- | --- | --- | --- | --- | | 2026-07-28 | 20260728-01 | 订单附件表 + 系统面单落文件表 | 订单 §1.5.4、§2.3、§4.1、§4.2.1、**§4.2.12**、§4.4.1、§4.4.1.8、§4.4.1.12、§4.4.1.17、§4.4.3.1、§10.6.1 | `t_order_file`;废弃订单 `LabelUrl`/`LabelGenerate*`;客户只读 systemLabel;下游 labelUrl→downstreamLabel | | 2026-07-22 | 20260722-01 | 查价/下单/入库/出库链路加固;待排货;运单号 IDE | 订单 §2.3、§4.3.5、§4.4.2.1、§4.4.3.1、§6.3;附录 §10.3.1;新手指引 `/order/guide` | 查价可选尺寸+参考价;创建仅 forecast 应收;入库可选排货→`pendingAllocation`;运单号 `IDE{yyMMddHHmmss}{4位随机}` | | 2026-07-19 | 20260719-01 | 店铺订单创建物流 / 物流&代发 | 订单 §4.1、§2.3、§4.2.11、**§4.4.7**;附录权限 | 校对弹窗;`batch-create-logistics` / `batch-create-fulfill`;与匹配库存分流 | | 2026-07-15 | 20260715-01 | 客户发件地址 + 揽收订单 | 订单 §4.1、§4.2.1、§4.2.10、§4.4.1.1、§4.4.4、§4.4.5;附录 §10.3.9 | 地址簿 `t_customer_sender_address`;`OrderSource=pickup` 复用小包创建;菜单客户/订单 | | 2026-07-04 | 20260704-01 | 创建问题件 / 创建理赔件 | 订单 §4.2.1、§4.2.9、§4.4.1.13~§4.4.1.16;渠道 §5.3.0.7;附录 §10.3.6~§10.3.8 | 问题模板消费;`t_order_exception_event`;权限 `Order.Problematic.Create` 等 | | 2026-06-27 | 20260627-02 | 规则快照下游元数据 + 订单绑定 | 订单 §4.2.1、§4.3.4.2、§4.4.1.11~§4.4.1.12;渠道 §5.3.0.2 | `Providers` enrich;`t_order` 四字段;轨迹/下游预报优先读订单绑定 | | 2026-06-27 | 20260627-01 | 系统其他配置(小包轨迹) | 系统主数据 §8.1、§8.2.7、§8.3、§8.4.1.16~§8.4.1.20 | `t_system_other_config` + `/system/other-config/*`;卡片页 `/system/otherConfig` | | 2026-06-26 | 20260626-02 | 渠道模块反向对齐 oms0414-back | 渠道模块 §5.3.0、§5.2.6.2、§10.6.2 | 发布/价格 save/导入等 | | 2026-06-26 | 20260626-01 | 反向对齐 oms0414-back 接口实现 | §1.5、§4.3.5、§4.4、§10.6、§9.9 | 快照 Live 降级、同步 batch-create、简化 upload 等 | | 2026-06-24 | 20260624-03 | 查价规则与失败原因 | 订单模块 §4.4.2.1 | unit 公式、重量左开右闭、failures | | 2026-06-26 | 20260626-01 | 移除收费时机 | 订单 §4.3.4、渠道卖价/买价 | 删除 ChargingTime;应收按 forecast/receive 计重触发 | | 2026-06-24 | 20260624-02 | 小包创建与会话/字段约定落地 | 订单模块 §4.2.1、§4.4.3.1~§4.4.3.7 | 客户 Id 会话补全、国家二字码、报关方式枚举、可选运单号等 | | 2026-06-24 | 20260624-01 | 订单接口客户数据隔离 | 订单模块 §4.4.0、§3.3.5 | `/admin/order/*` 增删改查强制 `CustomerId` 隔离 | | 2026-04-30 | 20260430-03 | 获取渠道分组 | 渠道模块 | 之前漏写该接口 | | — | — | **与客户、内部用户、认证、RBAC 相关的迭代** | — | **一律在 `docs/rbac系统设计-060801.md` 维护**;OMS 本篇仅记录业务域迭代 | ### 9.2 20260719-01 差异明细(店铺订单创建物流 / 物流&代发) #### 9.2.1 能力 | 能力 | 说明 | | --- | --- | | 创建物流订单 | 勾选店铺单 → 校对收件人/明细 → 选运输方式 → 批量创建 `t_order`(`OrderSource=platform`)并回写 `LinkedOrderId`;**不**扣库存、**不**要求 SKU 映射 | | 创建物流&代发订单 | 同上 + 按单绑定库存/包裹(§4.4.6 冻结语义);建单失败或绑定失败按条回滚 | | 与匹配库存 | 匹配库存仍走映射扣减建单;三入口前置条件一致但库存语义不同 | #### 9.2.2 接口(待实现) | 方法 | 路径 | | --- | --- | | POST | `/order/platform-store-order/batch-create-logistics` | | POST | `/order/platform-store-order/batch-create-fulfill` | #### 9.2.3 前端(待实现) - `platformStoreOrder` 工具栏两按钮 + `CreateLogisticsModal` / `CreateLogisticsFulfillModal` - 代发绑定交互复用 `StockBindModal` 语义;权限 `CreateLogistics` / `CreateFulfill` - 菜单三级按钮目录:`src/constants/pageButtonCatalog.ts` → `Order.PlatformStoreOrder.CreateLogistics` / `.CreateFulfill`(RBAC「同步三级按钮」拉取) #### 9.2.4 正文锚点 - **§4.4.7**(完整交互与接口设计) --- ### 9.3 20260715-01 差异明细(客户发件地址 + 揽收订单) #### 9.3.1 表结构(迁移脚本,待编写) | 脚本 | 说明 | | --- | --- | | `sql/migrate_t_customer_sender_address.sql` | 新建 **`t_customer_sender_address`**(§4.2.10) | | `sql/migrate_t_order_pickup_fields.sql` | **`t_order`** 增加 **`OrderSource`**、**`PickupTime`**、**`CustomerSenderAddressId`**;存量行 `OrderSource` 置 `manual` | #### 9.3.2 新增 / 扩展接口 | 方法 | 路径 | 说明 | | --- | --- | --- | | POST | `/order/sender-address/list` | §4.4.5 | | GET | `/order/sender-address/detail` | §4.4.5 | | POST | `/order/sender-address/create` | §4.4.5 | | POST | `/order/sender-address/update` | §4.4.5 | | POST | `/order/sender-address/delete` | §4.4.5 | | POST | `/order/sender-address/set-default` | §4.4.5 | | POST | `/order/pickup/create` | §4.4.4;内部调用小包创建核心 | | POST | `/order/list` | **扩展**筛选 `orderSource`;响应回显来源/揽收时间 | #### 9.3.3 权限码(RBAC 菜单待配置) | 权限码 | 说明 | | --- | --- | | `Order.SenderAddress.Query` | 发件地址查询 / 菜单 **客户 → 发件地址** | | `Order.SenderAddress.Create` | 新增发件地址 | | `Order.SenderAddress.Update` | 更新 / 设默认 | | `Order.SenderAddress.Delete` | 删除发件地址 | | `Order.Pickup.Query` | 揽收订单菜单 / 列表 | | `Order.Pickup.Create` | 创建揽收订单 | #### 9.3.4 前端路由 | 菜单 | path | 视图 | | --- | --- | --- | | 客户 → 发件地址 | `/user/senderAddress` | `views/user/senderAddress/index.vue` | | 订单 → 揽收订单 | `/order/pickupOrder` | `views/order/pickupOrder/index.vue` | #### 9.3.5 实现分期 | 期次 | 范围 | 状态 | | --- | --- | --- | | **一期** | 地址簿 CRUD + 揽收创建(仓库→收件映射 + 缺省件重尺/申报)+ 列表 `orderSource` 筛选 + 两菜单页 | **已落地**(待执行 SQL 迁移与 RBAC 权限码配置) | | **二期** | 仓库地址结构化国家/省市区邮编;揽收预约与承运商对接;我的订单来源角标/筛选体验 | 待定 | #### 9.3.6 与仓储小包边界(实现时勿混表) | 域 | 主表 | 入口 | | --- | --- | --- | | 揽收订单 | `t_order`(`OrderSource=pickup`) | `/order/pickupOrder` | | 仓储小包/到货 | `C_parcel` 等 | `/warehouse/parcel` | ### 9.4 20260704-01 差异明细(创建问题件 / 创建理赔件) #### 9.4.1 表结构(迁移脚本,待执行) | 脚本 | 说明 | | --- | --- | | `sql/migrate_t_order_handle_status.sql` | **`HandleStatus`** 列(**已编写**;列表子 Tab 已依赖) | | `sql/migrate_t_order_exception_fields.sql` | **`t_order`** 扩展:问题件/理赔件快照字段 + **`PreviousOrderStatus`** | | `sql/migrate_t_order_exception_event.sql` | 新建 **`t_order_exception_event`**(§4.2.9) | #### 9.4.2 新增接口 | 方法 | 路径 | 说明 | | --- | --- | --- | | POST | `/order/mark-problematic` | §4.4.1.13 | | POST | `/order/mark-claimed` | §4.4.1.14 | | POST | `/order/handle-status/update` | §4.4.1.15 | #### 9.4.3 权限码(RBAC 菜单待配置) | 权限码 | 说明 | | --- | --- | | `Order.Problematic.Create` | 创建问题件 | | `Order.Claim.Create` | 创建理赔件 | | `Order.HandleStatus.Update` | 标记已处理 | #### 9.4.4 实现分期 | 期次 | 范围 | 状态 | | --- | --- | --- | | **一期** | 表结构 + 三个 HTTP 接口 + `myOrders` 弹窗/按钮 + 列表字段回显 | **已落地** | | **二期** | 占位符渲染、`NotifyMethod` 投递、自动触发消费问题模板 | **部分已落地**(见 §9.4.5) | #### 9.4.5 二期实现说明(2026-07-06) **占位符渲染**(`IssuePlaceholderRenderer`) - 支持 `{serviceNo}`、`{clientNo}`、`{trackNo}`、`{productCode}`、`{channelCode}`、`{issueType}`、`{issueTypeName}`、`{issueTitle}` 及中文别名(如 `{运单号}`、`{客户单号}`)。 - 标记问题件时按 **单订单** 渲染标题/内容后写入 `t_order` 与 `t_order_exception_event`。 **通知投递**(`OrderIssueNotificationService`) | `NotifyMethod` | 行为 | | --- | --- | | `inApp` | 写入 `t_notice`(`noticeType=message`),接收人为订单客户 `t_customer.UserId` + 自动触发 `notifier` 中的用户 Id | | `email` | 使用 **当前操作人** 的 `t_system_batch_email_account` SMTP 配置,发送至客户 `Email` | | `sms` / `feishu` / `wechat` | 记录日志并跳过(通道待对接) | **自动触发运行时**(`AutoTriggerRuntimeService`) | 挂钩点 | `TriggerTiming` 关键字示例 | | --- | --- | | 小包预报成功 | 下单、order、create、parcel、预报 | | `/operation/inbound/create` | 入库、inbound、receive、收货 | | `/operation/outbound/create` | 出库、outbound、ship、发货 | - **问题件**:按 `triggerCondition` 解析 `issueType`(支持 JSON `{"issueType":"address",...}` 或纯 code),匹配 **问题模板** 后自动标记并通知。 - **拦截**:`IsIntercept=1`(**事后标记**;**不能阻止预报**,预报同步拦截见 **§5.3.0.6.1**)。 - **警告**:仅发站内信(不改编订单状态)。 ### 9.5 20260627-02 差异明细(规则快照下游元数据 + 订单绑定) #### 9.2.1 表结构差异 **`t_order`**(迁移 **`sql/migrate_t_order_downstream_integration.sql`**) | 字段 | 说明 | | --- | --- | | `DownstreamSupplierCode` | itfx **`supplierCode`** | | `DownstreamAppKey` | itfx AppKey | | `DownstreamAppId` | itfx 应用 Id(Secret 不落库) | | `ChannelForecastCode` | 渠道 **ForecastCode** / 轨迹 ApiCode | #### 9.2.2 接口与契约差异 **规则发布(`POST /channel/sales-product/publish`,`operatorType=rules`)** - `Content.Providers[]` 增加 **`channelCode`、`forecastCode`、`groupId`、`appId`、`apiServiceProviderId`、`appKey`、`supplierCode`**(**不含 `appSecret`**)。 **排货确认(`ConfirmServiceChannelAndPayableAsync`)** - 除 **`ChannelCode`** 外,写入上述四字段至 **`t_order`**。 **`POST /order/track-upsert-small-bag`** - 新增;手动同步轨迹 UpsertSmallBag;**ApiCode** 优先 **`ChannelForecastCode`**。 **「预报下游订单」(前端 itfx)** - 非 OMS 后端路由;凭证 **`resolveOrderDownstreamCredentials`** 优先订单 **`downstream*`** 字段。 **列表/详情 `OrderOutput`** - 响应增加 **`downstreamSupplierCode`、`downstreamAppKey`、`downstreamAppId`、`channelForecastCode`**。 --- ### 9.6 20260624-03 差异明细(查价对接卖价最新版本算价) #### 9.2.1 表结构差异 - 无 #### 9.2.2 接口与契约差异 **`POST /order/price-check`** | 项 | 说明 | | --- | --- | | 匹配 | 仅按国家+邮编、运输方式、重量尺寸、货物属性匹配快照 | | 重量区间 | 左开右闭 `(start,end]`;单值 `0.5` → `(0,0.5]` | | 公式 | 字典编码 `unit` / `total` / `continueUnit` 等 | | 响应 | `items` + `failures`(`step` + `reason`) | --- ### 9.7 20260624-02 差异明细(小包创建与会话/字段约定) #### 9.3.1 表结构差异 - 无 #### 9.3.2 接口与契约差异 **`POST /order/parcel/create`、`POST /order/parcel/batch-create`** | 项 | 原约定/易错点 | 现约定(与实现对齐) | | --- | --- | --- | | `customerId` | 请求体必填 | **可选**;客户登录由 `mapId → t_customer.Id` **会话补全**;不一致 → **「客户ID非法」** | | `trackNo` / `serviceNo` | 文档未明确可选 | **可选**;`serviceNo` 空则服务端自动生成 | | `receiver.countryCode` | 传 `t_country.Id`(数字) | 传 **ISO 二字码 string**(如 `CN`、`US`);兼容数字字符串 Id | | `sender.countryCode` | 数字 Id | 同收件人,**可选** | | `exportCustomsMode` | 易传 `9610`、字典 `1` | 须为 §4.2.1 六个 **code**;非法 → **「出口报关方式不合法」** | | 收件人校验失败 | — | 缺 `recipientsName` / `countryCode` / `zipCode` / `streetAddress1` → **「收件人信息不完整」** | **`POST /order/upload`** - 创建 **`t_order_batch_job`**(`import-row`),`CustomerId` 与导入行 `CustomerId` **均取会话** `effectiveCustomerId`。 **`POST /order/list`、`POST /order/export`、`GET /order/batch/jobs`、`GET /order/batch/jobs/{id}`** - 客户门户 **按 `t_order.CustomerId` / `t_order_batch_job.CustomerId` 隔离**(非 `CreateUserId`);跨客户单条访问返回 **「订单不存在」** / **「批量任务不存在」**。 **`GET /order/template/list`、`GET /order/template/download`** - 客户仅见 `standard` + 自有 `custom` 模板;他客模板返回 **「模板不存在」**。 **响应字段说明** - 列表/详情中 `sender.countryCode` / `receiver.countryCode` 仍为 **`t_country.Id`(int)**;与 **创建请求体 ISO 二字码** 区分使用。 --- ### 9.8 20260624-01 差异明细(订单客户数据隔离) #### 9.4.1 表结构差异 - 无(沿用 `t_order.CustomerId`、`t_order_batch_job.CustomerId`、`t_order_template.CustomerId`;**已删除** `t_order_task`) #### 9.4.2 接口差异 **更新接口(订单模块全部 `/order/*`)** - 统一增加 **§4.4.0** 约束:会话解析 `effectiveCustomerId`,列表强制过滤、单条/批量写操作归属校验;跨客户访问返回「订单不存在」或 403。 **新增文档章节** - **§3.3.5** 订单模块客户数据隔离(公共规范) - **§4.4.0** 客户数据隔离(强制,含实现检查清单) --- ### 9.9 20260427-03 差异明细(按表结构与接口) > 对比范围:`oms表结构与接口设计20260427.md` vs `oms表结构与接口设计20260430.md`。**与认证、客户、内部用户、RBAC 相关的历史差异**不再在此归档,见 **`docs/rbac系统设计-060801.md`**。 #### 9.5.1 表结构差异 **新增表(0)** - 无 **更新表(0)** - 无 **删除表(0)** - 无 #### 9.5.2 接口差异 **新增接口(1)** - `GET | /channel/service-channel/group/list`(获取渠道分组列表) **更新接口(0)** - 无 **删除接口(0)** - 无 --- ### 9.10 20260626-01 差异明细(反向对齐 oms0414-back) > 对照仓库:`C:\Users\wilso\Desktop\oms0414-back`。本文在保留原设计目标的前提下,用 **「当前实现」** 小节标注代码真实行为。 #### 9.6.1 新增文档章节 - **§1.5** 后端实现对照(技术栈、Service 映射、会话隔离、设计 vs 实现速查) - **§4.3.5** 快照解析与算价(`ResolveSnapshotBindingAsync`、`ParcelBillingCalculator`) - **§5.3.0** 渠道模块当前实现总览(销售产品、服务渠道、KPI、计重、自动触发、标签) - **§10.6** 接口实现状态总表 #### 9.6.2 接口与行为差异(设计 → 代码) | 接口 / 能力 | 原设计 / 文档描述 | 当前实现 | | --- | --- | --- | | 快照读取 | Redis + 内存为主 | **DB 发布记录** + **Live 实时组装**,版本 `LIVE-{id}` | | `POST /order/parcel/create` | 应收 + 应付预报 | **仅应收**(`CalculateForecastReceivableFees`) | | `POST /order/parcel/batch-create` | 异步 `taskId` | **同步** `batchNo` + `items[]`,逐条 `CreateParcelCoreAsync` | | `POST /order/upload` | 等同批量创建全链路 | **批量任务** `import-row`,MQ 异步或同步处理,返回 **`jobId`** | | `POST /order/pay` | 扣余额 + 写流水 | **已落地**:`pendingPayment` → 扣应收合计 → `orderPay` 流水 → `pendingShip` | | `POST /order/fee/batch-recalculate` | 批量重算 | **无 HTTP 接口** | | 入库 / 出库 | 触发 §4.3.4 分阶段算价 | **仅**作业记录表 | | 管理员订单列表 | §4.4.0 客户门户 | **管理员不过滤** `CustomerId` | | 渠道发布后 Redis 同步 | 写 Redis + 刷新计费内存 | **仅写 DB** 发布记录 | | 自动触发规则 | 订单/仓储事件消费 | **部分落地**(关键字匹配 + 事后拦截);**V2 结构化条件 + 预报同步拦截见 §5.3.0.6.1** | | 服务渠道 publish/preview | 同销售产品 | **已落地** | #### 9.6.4 渠道模块已落地要点(详见 §5.3.0) - 销售产品 **`price/save`**、排货 **`allocation/save`** 支持 **`publish=true`** 连带发布 - 销售产品 **`publish/preview`** 预览快照;**`publish/rollback`** 从 `Content` 恢复价格树或规则+排货 - 服务渠道 **`group/delete`** 有关联渠道时拒绝删除 - KPI **`info/import`** 逐行导入,失败行记入 **`failures`** - 发布版本号 **`yyyyMMddHHmmss`**,`operatorType` 仅 **`price` \| `rules`** #### 9.6.3 已确认与实现对齐(补充到正文) - **`requestId`** 幂等(`t_order.RequestId`) - 查价 **`failures[].step`**:`product` / `snapshot` / `blacklist` / `weight` / `zone` / `price` - 充值审核 **`CustomerBalanceMutator.CreditRechargeAsync`**(`Pending` 可审,**Balance 与 `t_customer_account_flow` 同事务**) - 订单支付 **`CustomerBalanceMutator.DebitOrderPayAsync`**(见 **§4.4.1.3、§7.4**) - 面单 **`labelUrl` / `printUrl`**:优先 **`t_order_file.systemLabel`**(PDF);缺失时打印可补生成或回落 HTML stub `/uploads/prints/orders/` --- ## 10 附录 ### 10.1 接口路径规范(kebab-case 汇总) > **网关前缀**:订单模块生产 URL 为 **`https://api.nanafa.com/admin/order/...`**;下表「服务内路径」为 **`/order/...`**,与网关拼接规则:`{VITE_API_ADMIN_URL}` + `/order/{action}`。 | 模块 | 网关前缀(生产) | 服务内路径 | 示例 | | --- | --- | --- | --- | | 订单 | `/admin/order/` | `/order/` | list, detail, pay, pre-declare, cancel-intercept, delete, print, files, files/upload, files/delete, price-check, parcel/create, parcel/batch-create, downstream-forward-create-one;**设计中**:fee/batch-recalculate, mark-problematic, mark-claimed, handle-status/update | | 渠道-销售产品 | `/admin/channel/`(同订单网关) | `/channel/sales-product/` | list, detail, create, update, config/save, delete, price/list, price/save, allocation/get, publish/preview | | 渠道-服务渠道 | `/admin/channel/` | `/channel/service-channel/` | list, detail, create, update, config/save, delete, group/list, price/save, publish/preview, publish/rollback | | 渠道-计重 | `/admin/channel/` | `/channel/weight-rule/`、`/channel/weight-rounding/` | list, create, update, delete, info/list, info/add | | 渠道-自动触发 | `/admin/channel/` | `/channel/auto-trigger/` | list, create, update, delete, toggleStatus | | 渠道-问题模板 | `/admin/channel/` | `/channel/issue-template/` | list, detail, options, create, update, delete, toggleStatus | | 渠道-分区(KpiArea) | `/admin/channel/` | `/channel/kpi-area/` | create, update, delete, list, info/*, info/import, info/import-template | | 渠道-标签 | `/admin/channel/` | `/channel/label-template/save` | save | | 操作 | `/admin/operation/` | `/operation/` | inbound/create, inbound/recent-list, outbound/create, print-label/print | | 权限与客户域(非 OMS) | — | — | **`docs/rbac系统设计-060801.md`**(`/customer/*`、`/rbac/*` 等) | | 充值 | `/admin/recharge/` | `/recharge/` | recharge/submit, recharge-record/list, approve, reject | | 客户账户流水 | `/admin/customer/`(同 admin 网关) | `/customer/account-flow/` | **list**(分页查询资金流水,见 **§7.4**) | | 系统 | `/admin/system/` | `/system/` | country/list, warehouse/list, warehouse/detail, warehouse/create, warehouse/update, warehouse/delete, notice/list, notice/create, notice/update, notice/delete, notice/read-confirm | ### 10.2 分区类型说明(黑名单/偏远/价格) | 页面 | areaType | 说明 | | --- | --- | --- | | 黑名单区 | blacklist | 黑名单分区 | | 偏远分区 | remote | 偏远分区 | | 价格分区 | price | 用于计价/价格表关联的分区 | 调用 KpiArea 接口时,通过 `areaType` 区分不同业务分区,无需新建表或接口。 ### 10.3 状态码与枚举 #### 10.3.1 订单状态 | status | 中文 | | --- | --- | | draft | 草稿 | | predeclared | 已预报 | | pendingAllocation | 待排货 | | received | 已收货 | | pendingPayment | 待支付 | | pendingShip | 待发货 | | pendingOnline | 待上网 | | shipping | 运输中 | | deliverySuccess | 派送成功 | | deliveryFailed | 派送失败 | | problematic | 问题件 | | deleted | 已删除 | | claimed | 理赔件 | #### 10.3.2 充值状态 | payStatus | 中文 | | --- | --- | | 0 | 待审核 | | 1 | 成功 | | 2 | 失败 | #### 10.3.3 仓库类型 | warehouseType | 中文 | | --- | --- | | main | 主仓 | | transit | 中转仓 | | return | 退货仓 | | other | 其他 | #### 10.3.4 通知类型 | noticeType | 中文 | | --- | --- | | notice | 公告 | | message | 站内信 | #### 10.3.5 接收对象类型(通知) | receiverType | 中文 | 说明 | | --- | --- | --- | | user | RBAC 用户(按 `user.userid`) | **`ReceiverIds` / `ReceiverId` 均存 `user.userid`(INT)**,与 JWT Claim **`mapId`** 一致 | **与 RBAC 登录主体的关系**(见 **`docs/rbac系统设计-060801.md`**): - 内部用户、客户主账号、子客户登录后 JWT 均带 **`mapId` = 对应行的 `user.userid`**。 - 通知模块**当前实现**(`NoticeService`)**仅接受 `receiverType=user`**,创建/更新时校验 `ReceiverIds` 均在 RBAC `user` 表存在;**不支持** `receiverType=customer`。 - 若需按 `t_customer.Id` 广播给客户,须在实现层扩展 `receiverType` 与校验逻辑,并同步改 OpenAPI 与本文 **§8.2.4 / §8.2.5**。 **说明**:**客户分组、客户状态** 等枚举见 RBAC 文档 **`CustomerStatus` / `CustomerGroup`**(`docs/rbac系统设计-060801.md` §6)。 #### 10.3.6 问题类型(`IssueTypeCodes`) | code | 中文 | | --- | --- | | address | 地址异常 | | customs | 清关问题 | | weight | 重量异常 | | document | 单证不全 | | cargo | 货物异常 | | channel | 渠道异常 | | payment | 费用异常 | | other | 其他 | #### 10.3.7 理赔类型(`ClaimTypeCodes`,待实现) | code | 中文 | | --- | --- | | lost | 丢件 | | damaged | 破损 | | delay | 延误 | | customs | 清关理赔 | | fee | 费用争议 | | other | 其他 | #### 10.3.8 订单处理子状态(`OrderHandleStatusCodes`) | code | 中文 | 适用 `OrderStatus` | | --- | --- | --- | | pending | 待处理 | `problematic`、`claimed` | | processed | 已处理 | `problematic`、`claimed` | **说明**:非问题件/理赔件订单 `HandleStatus` 应为 **NULL**;列表筛选 `handleStatus` 仅在 Tab 为 `problematic` / `claimed` 时传入。 #### 10.3.9 订单来源(`OrderSourceCodes`) | Code | 说明 | | --- | --- | | `manual` | 普通下单(我要下单 / 单笔小包创建);存量订单迁移默认值 | | `pickup` | 揽收订单(§4.4.4) | | `upload` | Excel 上传下单 | | `batch` | JSON 批量创建 | | `platform` | 平台店铺订单转 OMS(预留) | ### 10.6 接口实现状态总表(oms0414-back,2026-07-04) > 状态:**已落地** = 有 `ApplicationService` 方法且与下表逻辑一致;**部分** = 已暴露 HTTP 但业务未达设计;**设计中** = 本文有契约、代码无接口。 #### 10.6.1 订单 `/order/*` | 方法 | 路径 | 状态 | 实现类 / 备注 | | --- | --- | --- | --- | | POST | `/order/list` | 已落地 | `OrderService.GetPaginatedListAsync` — 客户过滤 `CustomerId`;支持 `orderSource` 筛选 | | POST | `/order/pickup/create` | 已落地 | §4.4.4;`PickupOrderParcelBuilder` + `CreateParcelCoreAsync` | | POST | `/order/sender-address/list` | 已落地 | §4.4.5 | | GET | `/order/sender-address/detail` | 已落地 | §4.4.5 | | POST | `/order/sender-address/create` | 已落地 | §4.4.5 | | POST | `/order/sender-address/update` | 已落地 | §4.4.5 | | POST | `/order/sender-address/delete` | 已落地 | §4.4.5 | | POST | `/order/sender-address/set-default` | 已落地 | §4.4.5 | | GET | `/order/detail` | 已落地 | 含收发件人、申报、四行重量 | | POST | `/order/pay` | 已落地 | 扣余额 + `t_customer_account_flow`;见 **§4.4.1.3** | | POST | `/order/pre-declare` | 部分 | 纯状态置 `predeclared` | | POST | `/order/intercept` | 已落地 | `IsIntercept=1` | | POST | `/order/cancel-intercept` | 已落地 | `IsIntercept=0` | | POST | `/order/delete` | 已落地 | 软删 + `deleted` | | POST | `/order/print` | 已落地 | 优先 `t_order_file.systemLabel`;缺失补生成 PDF | | GET | `/order/files` | 已落地 | 按类型分组;客户仅 systemLabel | | POST | `/order/files/upload` | 已落地 | 内部上传;客户禁止 | | POST | `/order/files/delete` | 已落地 | 软删;客户禁止 | | POST | `/order/export` | 已落地 | Excel,24h 过期 | | POST | `/order/price-check` | 已落地 | 见 §4.3.5、§4.4.2.1 | | GET | `/order/template/list` | 已落地 | 客户见 standard + 自有 custom | | GET | `/order/template/download` | 已落地 | 动态生成 Excel 表头 | | POST | `/order/upload` | 已落地 | Excel 导入 → 批量任务 `import-row`,见 §4.4.3.5 | | GET | `/order/batch/jobs` | 已落地 | 批量任务列表,客户隔离 | | GET | `/order/batch/jobs/{id}` | 已落地 | 任务进度与汇总 | | GET | `/order/batch/jobs/{id}/items` | 已落地 | 子项明细(含失败行、客户单号) | | POST | `/order/batch/jobs/{id}/retry-failed` | 已落地 | 重试失败子项 | | POST | `/order/batch/jobs/{id}/cancel` | 已落地 | 取消待处理子项 | | POST | `/order/parcel/create` | 部分 | 卖价预报应收;无应付;Live 快照;成功后生成 systemLabel | | POST | `/order/parcel/batch-create` | 部分 | 同步批处理,见 §4.4.3.2 | | POST | `/order/downstream-forward-create-one` | 已落地 | itfx createOrderOne;回写 DownstreamServiceNo/TrackNo + downstreamLabel | | GET | `/order/downstream-create-one-body` | 已落地 | 预览下游请求体 | | POST | `/order/fee/batch-recalculate` | 设计中 | §4.4.1.10 | | POST | `/order/track-upsert-small-bag` | 已落地 | 手动轨迹 UpsertSmallBag;见 **§4.4.1.11** | | POST | `/order/mark-problematic` | 设计中 | §4.4.1.13;批量标记问题件 + 问题模板 | | POST | `/order/mark-claimed` | 设计中 | §4.4.1.14;批量创建理赔件 | | POST | `/order/handle-status/update` | 设计中 | §4.4.1.15;待处理→已处理 | | POST | `/order/debug/track-upsert-small-bag` | 已落地 | Swagger 联调轨迹 | #### 10.6.2 渠道 `/channel/*` > 明细见 **§5.3.0**。 | 前缀 | 状态 | Service / 备注 | | --- | --- | --- | | `/channel/sales-product/*` | 已落地 | `SalesProductService` — 18 个端点:CRUD、`config/save`、价格 CRUD + **`price/save`**、排货、发布列表/预览/发布/回滚 | | `/channel/service-channel/*` | 已落地 | `ServiceChannelService` — CRUD、分组 CRUD、买价 CRUD + **`price/save`**、**`publish/preview`**、发布/回滚 | | `/channel/kpi-area/*` | 已落地 | 10 个端点:主表 CRUD + 明细分页 CRUD + **Excel 导入/模板** | | `/channel/weight-rule/*` | 已落地 | 8 个端点:主表 + info 子表 CRUD(POST 路由) | | `/channel/weight-rounding/*` | 已落地 | 8 个端点:同上 | | `/channel/auto-trigger/*` | 部分 | CRUD + toggleStatus + **部分运行时**(事后拦截/警告/问题件);**V2 规则引擎 §5.3.0.6.1 未实现** | | `/channel/label-template/save` | 已落地 | upsert,`Name+TemplateType` 唯一 | **渠道发布与订单算价衔接**:发布快照写入 DB 后,订单 **`ResolveSnapshotBindingAsync`** 按 `PublishTime <= bindTime` 读取;**Redis 同步未实现**(§5.2.6.2)。 #### 10.6.3 操作 `/operation/*` | 方法 | 路径 | 状态 | 备注 | | --- | --- | --- | --- | | POST | `/operation/inbound/create` | 部分 | 仅写 `t_inbound_record`,不联动订单算价 | | GET | `/operation/inbound/recent-list` | 已落地 | | | POST | `/operation/outbound/create` | 部分 | 仅写 `t_outbound_record` + 状态校验;不更新 `ship` 件重尺 / `pendingOnline` | | GET | `/operation/outbound/recent-list` | 已落地 | | | POST | `/operation/print-label/print` | 已落地 | 标签打印记录 | | GET | `/operation/print-label/recent-list` | 已落地 | | | GET | `/operation/print-label/template-list` | 已落地 | | #### 10.6.4 充值 `/recharge/*` | 方法 | 路径 | 状态 | 备注 | | --- | --- | --- | --- | | POST | `/recharge/recharge/submit` | 已落地 | multipart,最小 ¥0.01,流水 `RECHARGE{ts}{random}` | | POST | `/recharge/recharge-record/approve` | 已落地 | 待审核 → 通过;**`CustomerBalanceMutator` + 流水 `recharge`** | | POST | `/recharge/recharge-record/reject` | 已落地 | | | GET | `/recharge/recharge-record/list` | 已落地 | 客户过滤;补齐 customerCode/phone | | GET | `/recharge/export` | 已落地 | MiniExcel | #### 10.6.4.1 客户账户流水 `/customer/account-flow/*` | 方法 | 路径 | 状态 | 备注 | | --- | --- | --- | --- | | GET | `/customer/account-flow/list` | 已落地 | `CustomerAccountFlowService` — keyword/bizType/时间/customerId 分页;客户门户强制隔离 | #### 10.6.5 系统 `/system/*` | 方法 | 路径 | 状态 | | --- | --- | --- | | GET | `/system/country/list` | 已落地 | | GET/POST | `/system/dict/*` | 已落地 — list/create/update/delete | | GET/POST | `/system/warehouse/*` | 已落地 | | GET/POST | `/system/notice/*` | 已落地 — 含 read-confirm | #### 10.6.6 宿主 SimpleAdmin.Api(非 OMS.Application) | 前缀 | 说明 | | --- | --- | | `/rbac/auth/*` | `AuthService` — 管理员/客户登录、微信登录、刷新、改密等 | | `/wechat/*` | 公众号验签、扫码登录、OAuth 注册/绑定 | 客户 **`/customer/*`**、RBAC 管理 **`/rbac/user|role|menu|...`** 见 **`docs/rbac系统设计-060801.md`**。 --- *文档版本:v1.9 | 最后更新:2026-07-19 | 已补充 **§4.4.7** 店铺订单创建物流 / 物流&代发(校对弹窗设计);迭代 **20260719-01***