diff --git a/db.sql b/db.sql
index 7141047..3931b2b 100644
--- a/db.sql
+++ b/db.sql
@@ -60,6 +60,10 @@ CREATE TABLE `t_order` (
`ChannelCode` VARCHAR(128) NULL COMMENT '渠道编码',
`ConsigneeCountry` VARCHAR(8) NULL COMMENT '国家二字码',
`CargoAttribute` VARCHAR(32) NULL COMMENT '货物属性:general/electronics/sensitive',
+ `ExportCustomsMode` VARCHAR(32) NULL COMMENT '出口报关方式',
+ `PredeclareFailReason` VARCHAR(512) NULL COMMENT '预报失败原因',
+ `SalesProductRulesVersion` VARCHAR(64) NULL COMMENT '销售产品规则快照版本',
+ `SalesProductPriceVersion` VARCHAR(64) NULL COMMENT '销售产品价格快照版本',
`OrderStatus` VARCHAR(32) NOT NULL COMMENT '订单状态',
`WarehouseId` INT NULL COMMENT '仓库ID',
`CustomerId` INT NULL COMMENT '客户ID',
@@ -280,6 +284,7 @@ CREATE TABLE `t_order_fee` (
`DownstreamProvider` VARCHAR(128) NULL COMMENT '下游服务商',
`PriceNo` VARCHAR(64) NULL COMMENT '价格编号',
`PriceEffectiveTime` DATETIME NULL COMMENT '价格编号生效时间',
+ `ChargingStage` VARCHAR(16) NULL COMMENT '计费阶段:forecast/inbound/outbound',
`IsDeleted` TINYINT NOT NULL DEFAULT 0 COMMENT '软删除:0-否,1-是',
`CreateUserId` INT NOT NULL DEFAULT 0 COMMENT '创建人',
`CreateTime` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
diff --git a/oms表结构与接口设计2026040802.md b/oms表结构与接口设计2026040802.md
deleted file mode 100644
index 180549b..0000000
--- a/oms表结构与接口设计2026040802.md
+++ /dev/null
@@ -1,3440 +0,0 @@
-# oms 表结构与接口设计文档
-
-----------------------------------------------------------------------------------------
-
-## OMS 系统说明
-
-OMS(Order Management System,订单管理系统)作为独立服务运行和admin权限管理服务合并,面向跨境物流/仓储业务场景,提供**订单**、**渠道**、**操作**、**客户**四大核心模块,支撑从客户注册、充值、下单、定价、支付到货物入库出库的全流程管理。
-
-### 核心模块
-
-| 模块 | 说明 |
-| --- | --- |
-| **订单** | 订单全生命周期管理,包括创建、预报、支付、发货、跟踪等 |
-| **渠道** | 销售产品与物流渠道的配置、定价及买价/卖价计算 |
-| **操作** | 入库、出库、打印等仓储操作 |
-| **客户** | 客户注册、认证、账户余额、额度及权限管理 |
-
-
-----------------------------------------------------------------------------------------
-
-### 1.2 表结构设计规范
-
-#### 表的字段命名规范
-
-| 规范 | 说明 | 示例 |
-| --- | --- | --- |
-| 驼峰命名 | 字段名开头大写+单词驼峰 | `ServiceProviderId` |
-| 单数名词 | 表名用单数,表示实体 | `customer` 而非 `customers` |
-| 业务前缀 | 表名按业务模块加前缀 | `t_order`、`t_customer` |
-
-
-#### 必备字段(所有表)
-
-| 字段名 | 字段类型 | 说明 |
-| --- | --- | --- |
-| Id | BIGINT | 主键,自增,步长 1 |
-| IsDeleted | TINYINT | 软删除:0-否,1-是,默认 0 |
-| CreateUserId | INT | 创建人 |
-| CreateTime | DATETIME | 创建时间 |
-| UpdateUserId | INT | 更新人 |
-| UpdateTime | DATETIME | 更新时间 |
-
-#### 字段命名规范
-
-| 规范 | 说明 | 示例 |
-| --- | --- | --- |
-| 布尔字段 | 用 `IsXxx` 或 `HasXxx` | `IsDeleted`、`IsActive` |
-| 外键字段 | `关联表名Id` 或 `XxxId` | `CustomerId`、`WarehouseId` |
-| 时间字段 | `XxxTime` 或 `XxxAt` | `CreateTime`、`PayTime` |
-| 禁用保留字 | 不用 desc、range、match 等 | 用 `Description` 替代 `Desc` |
-
-#### 数据类型规范
-
-| 类型 | 说明 |
-| --- | --- |
-| 主键 | BIGINT,单表自增 |
-| 金额/重量 | DECIMAL,禁止 float/double |
-| 字符串 | varchar 长度 ≤ 5000,超出用 TEXT |
-| 枚举 | 小数据量用 TINYINT,可读性要求高用 VARCHAR |
-| 状态标志 | TINYINT |
-
-
-
-----------------------------------------------------------------------------------------
-
-## 二、订单模块
-
-### 1.1 核心需求说明
-
-- **我的订单**:支持订单全流程管理,包括分页查询、详情查看、支付、预报、拦截/取消拦截、删除、打印、导出;订单状态覆盖草稿→已预报→已收货→待支付→待发货→运输中→派送成功/失败等全生命周期。
-- **我要查价**:根据目的国、运输方式、重量尺寸、货物类型等条件查价,返回可选渠道及费用明细。
-- **我要下单**:支持下载下单模板、上传 Excel 批量下单、查看任务进度与详情;模板分为标准模板与自定义模板。
-
-### 1.2 表结构设计
-
-> **命名约定**:表名按业务模块加 t_ 前缀;字段名驼峰命名(开头大写+单词驼峰)。
-
-#### 订单表 `t_order`
-
-| 字段名 | 字段类型 | 长度 | 主键/外键 | 非空 | 说明 |
-| --- | --- | --- | --- | --- | --- |
-| Id | BIGINT | - | 主键 | 是 | 自增唯一标识 |
-| ServiceNo | VARCHAR | 64 | - | 是 | 运单号 |
-| ClientNo | VARCHAR | 64 | - | 否 | 客户单号 |
-| TrackNo | VARCHAR | 64 | - | 否 | 跟踪号 |
-| ProductCode | VARCHAR | 128 | - | 否 | 产品code |
-| ChannelCode | VARCHAR | 128 | - | 否 | 渠道code |
-| ConsigneeCountry | VARCHAR | 8 | - | 否 | 国家二字码 |
-| CargoAttribute | VARCHAR | 32 | - | 否 | 货物属性:general-普货,electronics-3c&带电,sensitive-敏感货 |
-| OrderStatus | VARCHAR | 32 | - | 是 | 订单状态 |
-| WarehouseId | BIGINT | - | 外键 | 否 | 仓库ID |
-| CustomerId | BIGINT | - | 外键 | 否 | 客户ID |
-| SenderId | BIGINT | - | 外键 | 否 | 发件人ID → t_order_sender |
-| RecipientId | BIGINT | - | 外键 | 否 | 收件人ID → t_order_recipient |
-| DeclarationId | BIGINT | - | 外键 | 否 | 申报信息ID → t_order_declaration |
-| 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-是 |
-| IsDeleted | TINYINT | - | - | 是 | 是否删除:0-否,1-是,默认0 |
-| CreateUserId | INT | - | - | 是 | 创建人 |
-| CreateTime | DATETIME | - | - | 是 | 创建时间 |
-| UpdateUserId | INT | - | - | 否 | 更新人 |
-| UpdateTime | DATETIME | - | - | 否 | 更新时间 |
-
-**OrderStatus 枚举**:draft, predeclared, received, pendingPayment, pendingShip, pendingOnline, shipping, deliverySuccess, deliveryFailed, problematic, deleted, claimed
-
-**CargoAttribute 枚举**:general-普货,electronics-3c&带电,sensitive-敏感货
-
-#### 下单模板表 `t_order_template`
-
-| 字段名 | 字段类型 | 长度 | 主键/外键 | 非空 | 说明 |
-| --- | --- | --- | --- | --- | --- |
-| Id | BIGINT | - | 主键 | 是 | 自增唯一标识 |
-| Name | VARCHAR | 64 | - | 是 | 模板名称 |
-| Type | VARCHAR | 16 | - | 是 | standard/custom |
-| TemplateKey | VARCHAR | 64 | - | 否 | 模板标识 |
-| CustomerId | BIGINT | - | 外键 | 否 | 客户ID(自定义模板) |
-| IsDeleted | TINYINT | - | - | 是 | 是否删除:0-否,1-是,默认0 |
-| CreateUserId | INT | - | - | 是 | 创建人 |
-| CreateTime | DATETIME | - | - | 是 | 创建时间 |
-| UpdateUserId | INT | - | - | 否 | 更新人 |
-| UpdateTime | DATETIME | - | - | 否 | 更新时间 |
-
-#### 下单任务表 `t_order_task`
-
-| 字段名 | 字段类型 | 长度 | 主键/外键 | 非空 | 说明 |
-| --- | --- | --- | --- | --- | --- |
-| Id | BIGINT | - | 主键 | 是 | 自增唯一标识 |
-| TaskName | VARCHAR | 128 | - | 是 | 任务名称 |
-| Quantity | INT | - | - | 否 | 数量 |
-| Status | VARCHAR | 16 | - | 是 | 处理中/已完成/待处理 |
-| TemplateId | BIGINT | - | 外键 | 否 | 模板ID |
-| IsDeleted | TINYINT | - | - | 是 | 是否删除:0-否,1-是,默认0 |
-| CreateUserId | INT | - | - | 是 | 创建人 |
-| CreateTime | DATETIME | - | - | 是 | 创建时间 |
-| UpdateUserId | INT | - | - | 否 | 更新人 |
-| UpdateTime | DATETIME | - | - | 否 | 更新时间 |
-| CustomerId | BIGINT | - | 外键 | 否 | 客户ID |
-
-#### 订单发件人表 `t_order_sender`
-
-| 字段名 | 字段类型 | 长度 | 主键/外键 | 非空 | 说明 |
-| --- | --- | --- | --- | --- | --- |
-| Id | BIGINT | - | 主键 | 是 | 自增唯一标识 |
-| OrderId | BIGINT | - | 外键 | 是 | 订单ID |
-| SenderName | VARCHAR | 50 | - | 否 | 姓名 |
-| Company | VARCHAR | 255 | - | 否 | 公司 |
-| Country | INT | - | - | 否 | 国家 |
-| 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 | - | - | 否 | 更新时间 |
-
-#### 订单收件人表 `t_order_recipient`
-
-| 字段名 | 字段类型 | 长度 | 主键/外键 | 非空 | 说明 |
-| --- | --- | --- | --- | --- | --- |
-| Id | BIGINT | - | 主键 | 是 | 自增唯一标识 |
-| OrderId | BIGINT | - | 外键 | 是 | 订单ID |
-| RecipientsName | VARCHAR | 30 | - | 否 | 收件人名称 |
-| CompanyName | VARCHAR | 60 | - | 否 | 收件人公司名称 |
-| Country | INT | - | - | 否 | 国家 |
-| 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 | - | - | 否 | 更新时间 |
-
-#### 订单申报信息表 `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 | - | - | 否 | 更新时间 |
-
-#### 订单件重尺表 `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_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 | - | 否 | 价格编号 |
-| PriceEffectiveTime | DATETIME | - | - | 否 | 价格编号生效时间 |
-| IsDeleted | TINYINT | - | - | 是 | 是否删除:0-否,1-是,默认0 |
-| CreateUserId | INT | - | - | 是 | 创建人 |
-| CreateTime | DATETIME | - | - | 是 | 创建时间 |
-| UpdateUserId | INT | - | - | 否 | 更新人 |
-| UpdateTime | DATETIME | - | - | 否 | 更新时间 |
-
-**Type 枚举**:receivable(应收)、payable(应付)
-
-### 1.3 核心接口设计
-
-#### 我的订单
-
-| 接口名称 | 请求方式 | 请求路径 | 说明 |
-| --- | --- | --- | --- |
-| 分页查询订单列表 | POST | /order/list | 支持 status、keyword、createTimeStart/End 筛选 |
-| 订单详情 | GET | /order/detail | 根据订单ID获取详情 |
-| 支付 | POST | /order/pay | 订单支付 |
-| 预报 | POST | /order/pre-declare | 订单预报 |
-| 拦截 | POST | /order/intercept | 订单拦截 |
-| 取消拦截 | POST | /order/cancel-intercept | 取消订单拦截 |
-| 删除 | POST | /order/delete | 删除订单 |
-| 打印 | POST | /order/print | 打印订单 |
-| 导出 | POST | /order/export | 导出订单 |
-
-**请求 /order/list**(POST,分页参数放 Body)
-
-```json
-{
- "page": 1, // 页码,从1开始
- "pageSize": 20, // 每页条数
- "status": "all|draft|predeclared|received|...", // 订单状态筛选
- "keyword": "", // 关键词(运单号/客户单号/跟踪号)
- "createTimeStart": "", // 创建时间起
- "createTimeEnd": "" // 创建时间止
-}
-```
-
-**响应**(用于 order/myOrders 列表及订单详情抽屉,字段与 parcel/create 对应,创建时传啥返回啥 + weightList)
-
-```json
-{
- "items": [
- {
- "id": 1,
- "clientNo": "CUST...",
- "trackNo": "TRK...",
- "serviceNo": "LP...",
- "productCode": "US_AIR",
- "channelCode": "CH001",
- "cargoAttribute": "general",
- "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": 0,
- "province": "广东省",
- "city": "深圳市",
- "zipCode": "518000",
- "streetAddress1": "示例地址1",
- "streetAddress2": "",
- "streetAddress3": "",
- "houseNumber": ""
- },
- "receiver": {
- "recipientsName": "收件人1",
- "companyName": "",
- "phone": "13800138000",
- "telePhone": "",
- "email": "",
- "countryCode": 0,
- "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
-}
-```
-
-**请求 /order/detail**(GET,Query 参数)
-
-```
-?id=1
-```
-
-**响应**
-
-```json
-{
- "id": 1,
- "clientNo": "CUST...",
- "trackNo": "TRK...",
- "serviceNo": "LP...",
- "productCode": "US_AIR",
- "channelCode": "CH001",
- "cargoAttribute": "general",
- "warehouseId": 1,
- "warehouse": "深圳仓",
- "customerId": 1,
- "sender": {},
- "receiver": {},
- "items": [],
- "weightList": []
-}
-```
-
-> data 结构与 list 单条 item 一致,含 sender、receiver、items、weightList。
-
-**请求 /order/pay**(POST)
-
-```json
-{
- "id": 1,
- "requestId": ""
-}
-```
-
-> requestId 幂等,可选。
-
-**响应**
-
-```json
-null
-```
-
-**请求 /order/pre-declare**(POST)
-
-```json
-{
- "id": 1,
- "requestId": ""
-}
-```
-
-**响应**
-
-```json
-null
-```
-
-**请求 /order/intercept**(POST)
-
-```json
-{
- "id": 1,
- "requestId": ""
-}
-```
-
-**响应**
-
-```json
-null
-```
-
-**请求 /order/cancel-intercept**(POST)
-
-```json
-{
- "id": 1,
- "requestId": ""
-}
-```
-
-**响应**
-
-```json
-null
-```
-
-**请求 /order/delete**(POST)
-
-```json
-{
- "id": 1,
- "requestId": ""
-}
-```
-
-**响应**
-
-```json
-null
-```
-
-**请求 /order/print**(POST)
-
-```json
-{
- "id": 1,
- "requestId": ""
-}
-```
-
-**响应**
-
-```json
-{
- "printUrl": ""
-}
-```
-
-**请求 /order/export**(POST)
-
-```json
-{
- "status": "all|draft|...",
- "keyword": "",
- "createTimeStart": "",
- "createTimeEnd": ""
-}
-```
-
-**响应**
-
-```json
-{
- "downloadUrl": "",
- "expireAt": ""
-}
-```
-
-#### 我要查价
-
-| 接口名称 | 请求方式 | 请求路径 | 说明 |
-| --- | --- | --- | --- |
-| 查价 | POST | /order/price-check | 根据目的国、重量尺寸等查价 |
-
-**请求 /order/price-check**(POST)
-
-```json
-{
- "destinationCountry": "US", // 目的国二字码
- "shippingMethod": "", // 运输方式
- "cargoWeight": "2", // 货物重量(kg)
- "length": "20", // 长(cm)
- "width": "15", // 宽(cm)
- "height": "10", // 高(cm)
- "cargoType": "普货", // 货物类型
- "destinationProvince": "", // 目的省州
- "postalCode": "" // 邮编
-}
-```
-
-**响应**
-
-```json
-{
- "items": [
- {
- "id": 1,
- "shippingMethod": "美国空运专线(普货)",
- "warehouse": "一代深圳仓",
- "deliveryTime": "3-7",
- "volumetricWeight": "2",
- "chargeableWeight": "2",
- "costFormula": "(62*2) +5+7",
- "freight": "62",
- "otherFees": "5",
- "totalCost": "136",
- "maxDeliveryDays": 7,
- "totalCostNum": 136
- }
- ]
-}
-```
-
-#### 我要下单
-
-| 接口名称 | 请求方式 | 请求路径 | 说明 |
-| --- | --- | --- | --- |
-| 模板列表 | GET | /order/template/list | 获取下单模板列表 |
-| 下载模板 | GET | /order/template/download | 下载 Excel 模板 |
-| 上传下单 | POST | /order/upload | multipart/form-data,file + templateId |
-| 任务进度列表 | GET | /order/task/list | 批量下单任务列表 |
-| 任务详情 | GET | /order/task/detail | 任务详情 |
-| 创建小包 | POST | /order/parcel/create | 创建小包订单 |
-
-**请求 /order/parcel/create**(POST)
-
-> 字段与 t_order、t_order_sender、t_order_recipient、t_order_declaration、t_order_weight 表对应,采用 camelCase。
-
-```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-敏感货
- "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": 0, // 国家
- "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": 0, // 国家
- "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
-{
- "clientNo": "",
- "serviceNo": "",
- "trackNo": "",
- "labelUrl": ""
-}
-```
-
-> 幂等时可选传 requestId。
-
-**请求 /order/template/list**(GET,Query 参数)
-
-```
-?page=1&pageSize=20&type=standard|custom&keyword=
-```
-
-**响应**
-
-```json
-{
- "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
-}
-```
-
-**请求 /order/template/download**(GET,Query 参数)
-
-```
-?templateId=1
-```
-
-**响应**
-
-> 直接返回 Excel 文件流,Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet
-
-**请求 /order/upload**(POST,multipart/form-data)
-
-| 字段 | 类型 | 说明 |
-| --- | --- | --- |
-| file | File | Excel 文件 |
-| templateId | number | 模板ID |
-
-**响应**
-
-```json
-{
- "taskId": 1
-}
-```
-
-**请求 /order/task/list**(GET,Query 参数)
-
-```
-?page=1&pageSize=20&status=&keyword=
-```
-
-**响应**
-
-```json
-{
- "items": [
- {
- "id": 1,
- "taskName": "批量下单_20240101",
- "quantity": 100,
- "status": "处理中",
- "templateId": 1,
- "createTime": "2024-01-01 12:00:00",
- "updateTime": "2024-01-01 12:00:00"
- }
- ],
- "total": 10
-}
-```
-
-**请求 /order/task/detail**(GET,Query 参数)
-
-```
-?taskId=1
-```
-
-**响应**
-
-```json
-{
- "id": 1,
- "taskName": "批量下单_20240101",
- "quantity": 100,
- "successCount": 80,
- "failCount": 20,
- "status": "已完成",
- "templateId": 1,
- "createTime": "2024-01-01 12:00:00",
- "updateTime": "2024-01-01 12:00:00",
- "failList": []
-}
-```
-
-
-
-## 三、渠道模块
-
-### 2.1 核心需求说明
-
-- **销售产品**:管理对外销售的产品(专线/快递/邮政),配置时效、黑名单区、偏远分区、计重规则、计重进位等;支持价格配置。
- - **产品配置**:产品基础信息(编码、名称、分组、时效、面单、黑名单区、偏远分区、计重规则、计重进位等)。
- - **产品卖价**:为每个产品配置多条价格项(运费、保险费、燃油附加费等);每条价格项通过弹窗配置:
- - **基础信息**:费用项、费用单位、币种、收费时机、收费条件、计费因素(因素+因素值,如货物类型、分区、重量、仓库、客户等)。
- - **价格表**:根据计费因素动态生成价格表,支持分区/货物类型/重量等组合、计算公式(总价/单价/续单价)、计算值、客户组。
- - **排货规则**:选择排货方案(成本最低/时效最快/综合最优),配置服务商列表(服务商名称、服务渠道、权重、指定客户)。
- - **发布记录**:价格与规则的发布历史,支持查看版本内容、回滚到指定版本。
-- **服务渠道**:管理下游服务渠道(支持渠道组+渠道层级),配置预报单号类型、时效、买价等。
- - **渠道配置**:渠道基础信息(渠道代码、名称、时效、预报配置、描述、预报前脚本等)。
- - **渠道买价**:为每个渠道配置多条买价项(运费、保险费、燃油附加费等);每条买价项通过弹窗配置:
- - **基础信息**:费用项、费用单位、币种、收费时机、收费条件、计费因素(因素+因素值)。
- - **价格表**:根据计费因素动态生成价格表,支持分区/货物类型/重量等组合、计算公式、计算值、客户组。
- - **发布记录**:买价与规则的发布历史,支持查看版本内容、回滚到指定版本。
-- **黑名单区/偏远分区/预警分区**:共用 KpiArea 表,通过 appKey/areaType 区分;支持分区及明细(国家/省/市/邮编区间)的增删改查。
-- **计重规则/计重进位**:配置计费重量规则与进位方式,明细结构与分区类似。
-- **自动触发**:配置下单/入库/出库时的自动拦截、警告、问题件等触发规则。
-- **标签设计**:保存 vg-print 导出的标签模板 JSON,支持尾程/客户/入库/出库/sku 等类型。
-
-### 2.2 表结构设计
-
-#### 销售产品表 `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 |
-| 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 | - | - | 否 | 更新时间 |
-
-#### 产品卖价表 `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 |
-| ChargingTime | VARCHAR | 32 | - | 否 | 收费时机:预报/出库/签收 |
-| ChargingCondition | VARCHAR | 64 | - | 否 | 收费条件 |
-| IsDeleted | TINYINT | - | - | 是 | 是否删除:0-否,1-是,默认0 |
-| CreateUserId | INT | - | - | 是 | 创建人 |
-| CreateTime | DATETIME | - | - | 是 | 创建时间 |
-| UpdateUserId | INT | - | - | 否 | 更新人 |
-| UpdateTime | DATETIME | - | - | 否 | 更新时间 |
-
-#### 产品卖价-计费因素表 `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 | - | - | 否 | 更新时间 |
-
-#### 产品卖价-价格表明细表 `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 | - | - | 否 | 更新时间 |
-
-
-#### 排货规则-服务商表 `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数组,空表示全部 |
-| IsDeleted | TINYINT | - | - | 是 | 是否删除:0-否,1-是,默认0 |
-| CreateUserId | INT | - | - | 是 | 创建人 |
-| CreateTime | DATETIME | - | - | 是 | 创建时间 |
-| UpdateUserId | INT | - | - | 否 | 更新人 |
-| UpdateTime | DATETIME | - | - | 否 | 更新时间 |
-
-**Scheme 枚举**:costLowest(成本最低)、timeFastest(时效最快)、overallBest(综合最优)
-
-#### 发布记录表 `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 | - | - | 否 | 更新时间 |
-
-#### 服务渠道组表 `t_service_channel_group`(可选,用于渠道分组)
-
-| 字段名 | 字段类型 | 长度 | 主键/外键 | 非空 | 说明 |
-| --- | --- | --- | --- | --- | --- |
-| Id | BIGINT | - | 主键 | 是 | 自增唯一标识 |
-| Name | VARCHAR | 128 | - | 是 | 渠道组名称 |
-| IsDeleted | TINYINT | - | - | 是 | 是否删除:0-否,1-是,默认0 |
-| CreateUserId | INT | - | - | 是 | 创建人 |
-| CreateTime | DATETIME | - | - | 是 | 创建时间 |
-| UpdateUserId | INT | - | - | 否 | 更新人 |
-| UpdateTime | DATETIME | - | - | 否 | 更新时间 |
-
-#### 服务渠道表 `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 | - | - | 否 | 更新时间 |
-
-#### 渠道买价表 `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 |
-| ChargingTime | VARCHAR | 32 | - | 否 | 收费时机:预报/出库/签收 |
-| ChargingCondition | VARCHAR | 64 | - | 否 | 收费条件 |
-| IsDeleted | TINYINT | - | - | 是 | 是否删除:0-否,1-是,默认0 |
-| CreateUserId | INT | - | - | 是 | 创建人 |
-| CreateTime | DATETIME | - | - | 是 | 创建时间 |
-| UpdateUserId | INT | - | - | 否 | 更新人 |
-| UpdateTime | DATETIME | - | - | 否 | 更新时间 |
-
-#### 渠道买价-计费因素表 `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 | - | - | 否 | 更新时间 |
-
-#### 渠道买价-价格表明细表 `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 | - | - | 否 | 更新时间 |
-
-#### 服务渠道发布记录表 `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 | - | - | 否 | 更新时间 |
-
-#### 分区表(黑名单区/偏远分区/预警分区共用)`t_kpi_area`
-
-> 与轨迹下预警分区共用,通过 `AreaType` 区分。
-
-| 字段名 | 字段类型 | 长度 | 主键/外键 | 非空 | 说明 |
-| --- | --- | --- | --- | --- | --- |
-| Id | BIGINT | - | 主键 | 是 | 自增唯一标识 |
-| AppKey | VARCHAR | 64 | - | 是 | 应用标识 |
-| AreaType | VARCHAR | 32 | - | 是 | 类型: blacklist/remote/warning |
-| Name | VARCHAR | 128 | - | 是 | 分区名称 |
-| SystemName | VARCHAR | 128 | - | 否 | 系统名称 |
-| IsDeleted | TINYINT | - | - | 是 | 是否删除:0-否,1-是,默认0 |
-| CreateUserId | INT | - | - | 是 | 创建人 |
-| CreateTime | DATETIME | - | - | 是 | 创建时间 |
-| UpdateUserId | INT | - | - | 否 | 更新人 |
-| UpdateTime | DATETIME | - | - | 否 | 更新时间 |
-
-**AreaType 枚举**:blacklist(黑名单区)、remote(偏远分区)、warning(预警分区)
-
-#### 分区明细表 `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 | - | - | 否 | 更新时间 |
-
-#### 计重规则表 `t_weight_rule`
-
-| 字段名 | 字段类型 | 长度 | 主键/外键 | 非空 | 说明 |
-| --- | --- | --- | --- | --- | --- |
-| Id | BIGINT | - | 主键 | 是 | 自增唯一标识 |
-| AppKey | VARCHAR | 64 | - | 是 | 应用标识 |
-| Name | VARCHAR | 128 | - | 是 | 规则名称 |
-| IsDeleted | TINYINT | - | - | 是 | 是否删除:0-否,1-是,默认0 |
-| CreateUserId | INT | - | - | 是 | 创建人 |
-| CreateTime | DATETIME | - | - | 是 | 创建时间 |
-| UpdateUserId | INT | - | - | 否 | 更新人 |
-| UpdateTime | DATETIME | - | - | 否 | 更新时间 |
-
-#### 计重规则明细表 `t_weight_rule_info`(子表,关联 t_weight_rule)
-
-> 计重规则子页面,配置客户组对应的计费规则(条件表达式、计费重取值、材积系数)。
-
-| 字段名 | 字段类型 | 长度 | 主键/外键 | 非空 | 说明 |
-| --- | --- | --- | --- | --- | --- |
-| Id | BIGINT | - | 主键 | 是 | 自增唯一标识 |
-| RuleId | BIGINT | - | 外键 | 是 | 计重规则ID |
-| Client | VARCHAR | 128 | - | 是 | 客户/客户组(如:公共、客户组1、客户组2) |
-| 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 | - | - | 否 | 更新时间 |
-
-#### 计重进位表 `t_weight_rounding`
-
-| 字段名 | 字段类型 | 长度 | 主键/外键 | 非空 | 说明 |
-| --- | --- | --- | --- | --- | --- |
-| Id | BIGINT | - | 主键 | 是 | 自增唯一标识 |
-| AppKey | VARCHAR | 64 | - | 是 | 应用标识 |
-| Name | VARCHAR | 128 | - | 是 | 进位名称 |
-| IsDeleted | TINYINT | - | - | 是 | 是否删除:0-否,1-是,默认0 |
-| CreateUserId | INT | - | - | 是 | 创建人 |
-| CreateTime | DATETIME | - | - | 是 | 创建时间 |
-| UpdateUserId | INT | - | - | 否 | 更新人 |
-| UpdateTime | DATETIME | - | - | 否 | 更新时间 |
-
-#### 计重进位明细表 `t_weight_rounding_info`(子表,关联 t_weight_rounding)
-
-> 计重进位子页面,配置重量区间的进位值(起始值、结束值、公共/客户组进位值)。
-
-| 字段名 | 字段类型 | 长度 | 主键/外键 | 非空 | 说明 |
-| --- | --- | --- | --- | --- | --- |
-| Id | BIGINT | - | 主键 | 是 | 自增唯一标识 |
-| RoundingId | BIGINT | - | 外键 | 是 | 计重进位ID |
-| StartValue | DECIMAL | 18,4 | - | 是 | 起始值(kg) |
-| EndValue | DECIMAL | 18,4 | - | 是 | 结束值(kg) |
-| Public | VARCHAR | 32 | - | 否 | 公共进位值 |
-| CustomerGroup1 | VARCHAR | 32 | - | 否 | 客户组1进位值 |
-| CustomerGroup2 | VARCHAR | 32 | - | 否 | 客户组2进位值 |
-| IsDeleted | TINYINT | - | - | 是 | 是否删除:0-否,1-是,默认0 |
-| CreateUserId | INT | - | - | 是 | 创建人 |
-| CreateTime | DATETIME | - | - | 是 | 创建时间 |
-| UpdateUserId | INT | - | - | 否 | 更新人 |
-| UpdateTime | DATETIME | - | - | 否 | 更新时间 |
-
-#### 自动触发表 `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 | - | - | 否 | 更新时间 |
-
-#### 标签模板表 `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 | - | - | 否 | 更新时间 |
-
-### 2.3 核心接口设计
-
-#### 销售产品
-
-| 接口名称 | 请求方式 | 请求路径 | 说明 |
-| --- | --- | --- | --- |
-| 产品列表 | GET | /channel/sales-product/list | 产品列表 |
-| 产品详情 | GET | /channel/sales-product/detail | 产品详情 |
-| 新增产品 | POST | /channel/sales-product/create | 新增产品 |
-| 更新产品 | POST | /channel/sales-product/update | 更新产品 |
-| 删除产品 | POST | /channel/sales-product/delete | 删除产品 |
-| 保存产品配置 | POST | /channel/sales-product/config/save | 保存产品配置(产品配置 tab) |
-
-**请求 /channel/sales-product/list**(GET,Query 参数)
-
-```
-?page=1&pageSize=20&productGroup=专线&keyword=&isActive=1
-```
-
-**响应**
-
-```json
-{
- "items": [
- {
- "id": 1,
- "productCode": "US-AIR-P",
- "productName": "美国空运专线(普货)",
- "productGroup": "专线",
- "minTimeliness": 3,
- "maxTimeliness": 7,
- "customerWaybill": "系统面单",
- "blacklistAreaId": 1,
- "remoteAreaId": 2,
- "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
-}
-```
-
-**请求 /channel/sales-product/detail**(GET,Query 参数)
-
-```
-?id=1
-```
-
-**响应**
-
-```json
-{
- "id": 1,
- "productCode": "US-AIR-P",
- "productName": "美国空运专线(普货)",
- "productGroup": "专线",
- "minTimeliness": 3,
- "maxTimeliness": 7,
- "customerWaybill": "系统面单",
- "blacklistAreaId": 1,
- "remoteAreaId": 2,
- "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"
-}
-```
-
-**请求 /channel/sales-product/create**(POST)
-
-```json
-{
- "productCode": "US-AIR-P",
- "productName": "美国空运专线(普货)",
- "productGroup": "专线",
- "minTimeliness": 3,
- "maxTimeliness": 7,
- "customerWaybill": "系统面单",
- "blacklistAreaId": 1,
- "remoteAreaId": 2,
- "weightRuleId": 1,
- "weightRoundingId": 1,
- "productDescription": "美国空运普货专线",
- "preStorageScript": "",
- "isActive": 1,
- "scheme": "costLowest"
-}
-```
-
-**请求 /channel/sales-product/update**(POST)
-
-```json
-{
- "id": 1,
- "productCode": "US-AIR-P",
- "productName": "美国空运专线(普货)",
- "productGroup": "专线",
- "minTimeliness": 3,
- "maxTimeliness": 7,
- "customerWaybill": "系统面单",
- "blacklistAreaId": 1,
- "remoteAreaId": 2,
- "weightRuleId": 1,
- "weightRoundingId": 1,
- "productDescription": "美国空运普货专线",
- "preStorageScript": "",
- "isActive": 1,
- "scheme": "costLowest"
-}
-```
-
-**请求 /channel/sales-product/delete**(POST)
-
-```json
-{
- "id": 1
-}
-```
-
-**请求 /channel/sales-product/config/save**(POST)
-
-```json
-{
- "id": 1,
- "productCode": "US-AIR-P",
- "productName": "美国空运专线(普货)",
- "productGroup": "专线",
- "minTimeliness": 3,
- "maxTimeliness": 7,
- "customerWaybill": "系统面单",
- "blacklistAreaId": 1,
- "remoteAreaId": 2,
- "weightRuleId": 1,
- "weightRoundingId": 1,
- "productDescription": "美国空运普货专线",
- "preStorageScript": "",
- "isActive": 1,
- "scheme": "costLowest"
-}
-```
-
-**响应**(create/update/delete/config/save)
-
-```json
-null
-```
-
-#### 产品卖价(子表接口)
-
-| 接口名称 | 请求方式 | 请求路径 | 说明 |
-| --- | --- | --- | --- |
-| 价格项列表 | 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 | 批量保存产品卖价(保存并可选发布) |
-
-**请求 /channel/sales-product/price/list**(GET,Query 参数)
-
-```
-?salesProductId=1
-```
-
-**响应**
-
-```json
-{
- "items": [
- {
- "id": 1,
- "salesProductId": 1,
- "serialNumber": 1,
- "status": 1,
- "priceName": "运费",
- "costItem": "freight",
- "costUnit": "kg",
- "currency": "RMB",
- "chargingTime": "出库",
- "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
- }
- }
- ],
- "createTime": "2024-01-01 12:00:00",
- "updateTime": "2024-01-01 12:00:00"
- }
- ]
-}
-```
-
-**请求 /channel/sales-product/price/create**(POST)
-
-```json
-{
- "salesProductId": 1,
- "serialNumber": 1,
- "status": 1,
- "priceName": "运费",
- "costItem": "freight",
- "costUnit": "kg",
- "currency": "RMB",
- "chargingTime": "出库",
- "chargingCondition": "",
- "factors": [
- {
- "serialNumber": 1,
- "factor": "货物类型",
- "factorValues": ["普货", "敏感货"]
- },
- {
- "serialNumber": 2,
- "factor": "分区",
- "factorValues": ["欧洲", "美国"]
- }
- ],
- "details": [
- {
- "factorValuesJson": {"分区": "欧洲", "货物类型": "普货", "重量": "0-0.5"},
- "calculationFormula": "单价",
- "calculatedValue": 62,
- "customerGroupValues": {"客户组1": 60, "客户组2": 65}
- }
- ]
-}
-```
-
-**请求 /channel/sales-product/price/update**(POST)
-
-```json
-{
- "id": 1,
- "salesProductId": 1,
- "serialNumber": 1,
- "status": 1,
- "priceName": "运费",
- "costItem": "freight",
- "costUnit": "kg",
- "currency": "RMB",
- "chargingTime": "出库",
- "chargingCondition": "",
- "factors": [],
- "details": []
-}
-```
-
-**请求 /channel/sales-product/price/delete**(POST)
-
-```json
-{
- "id": 1
-}
-```
-
-**请求 /channel/sales-product/price/toggleStatus**(POST)
-
-```json
-{
- "id": 1,
- "status": 0
-}
-```
-
-**请求 /channel/sales-product/price/save**(POST)
-
-```json
-{
- "salesProductId": 1,
- "publish": false,
- "prices": [
- {
- "id": 1,
- "serialNumber": 1,
- "status": 1,
- "priceName": "运费",
- "costItem": "freight",
- "costUnit": "kg",
- "currency": "RMB",
- "chargingTime": "出库",
- "chargingCondition": "",
- "factors": [],
- "details": []
- }
- ]
-}
-```
-
-**响应**(create/update/delete/toggleStatus/save)
-
-```json
-null
-```
-
-#### 排货规则(子表接口)
-
-| 接口名称 | 请求方式 | 请求路径 | 说明 |
-| --- | --- | --- | --- |
-| 获取排货规则 | GET | /channel/sales-product/allocation/get | 按产品ID获取排货方案及服务商列表 |
-| 保存排货规则 | POST | /channel/sales-product/allocation/save | 保存排货方案及服务商(保存并可选发布) |
-
-**请求 /channel/sales-product/allocation/get**(GET,Query 参数)
-
-```
-?salesProductId=1
-```
-
-**响应**
-
-```json
-{
- "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"
- }
- ]
-}
-```
-
-**请求 /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
-null
-```
-
-#### 发布记录(子表接口)
-
-| 接口名称 | 请求方式 | 请求路径 | 说明 |
-| --- | --- | --- | --- |
-| 价格发布记录列表 | GET | /channel/sales-product/publish/price/list | 按产品ID获取价格发布历史 |
-| 规则发布记录列表 | GET | /channel/sales-product/publish/rules/list | 按产品ID获取规则发布历史 |
-| 发布 | POST | /channel/sales-product/publish | 发布当前配置(价格或规则) |
-| 回滚到版本 | POST | /channel/sales-product/publish/rollback | 回滚到指定版本 |
-
-**请求 /channel/sales-product/publish/price/list**(GET,Query 参数)
-
-```
-?salesProductId=1&page=1&pageSize=20
-```
-
-**响应**
-
-```json
-{
- "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
-}
-```
-
-**请求 /channel/sales-product/publish/rules/list**(GET,Query 参数)
-
-```
-?salesProductId=1&page=1&pageSize=20
-```
-
-**响应**(结构同 price/list,operatorType 为 rules)
-
-**请求 /channel/sales-product/publish**(POST)
-
-```json
-{
- "salesProductId": 1,
- "operatorType": "price",
- "description": "价格调整"
-}
-```
-
-**请求 /channel/sales-product/publish/rollback**(POST)
-
-```json
-{
- "salesProductId": 1,
- "recordId": 1,
- "operatorType": "price"
-}
-```
-
-**响应**(publish/rollback)
-
-```json
-null
-```
-
-#### 服务渠道
-
-| 接口名称 | 请求方式 | 请求路径 | 说明 |
-| --- | --- | --- | --- |
-| 渠道列表 | GET | /channel/service-channel/list | 渠道列表(含渠道组层级) |
-| 渠道详情 | GET | /channel/service-channel/detail | 渠道详情 |
-| 新增渠道 | POST | /channel/service-channel/create | 新增渠道 |
-| 更新渠道 | POST | /channel/service-channel/update | 更新渠道 |
-| 删除渠道 | POST | /channel/service-channel/delete | 删除渠道 |
-| 保存渠道配置 | POST | /channel/service-channel/config/save | 保存渠道配置(渠道配置 tab) |
-
-**请求 /channel/service-channel/list**(GET,Query 参数)
-
-```
-?page=1&pageSize=20&groupId=&keyword=&isActive=1
-```
-
-**响应**
-
-```json
-{
- "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
-}
-```
-
-**请求 /channel/service-channel/detail**(GET,Query 参数)
-
-```
-?id=1
-```
-
-**响应**
-
-```json
-{
- "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"
-}
-```
-
-**请求 /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
-}
-```
-
-**请求 /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
-}
-```
-
-**请求 /channel/service-channel/delete**(POST)
-
-```json
-{
- "id": 1
-}
-```
-
-**请求 /channel/service-channel/config/save**(POST)
-
-```json
-{
- "id": 1,
- "groupId": 1,
- "channelCode": "DHL-US",
- "channelName": "DHL美国专线",
- "minTime": 3,
- "maxTime": 7,
- "forecastType": "waybill",
- "forecastCode": "",
- "description": "DHL美国专线渠道",
- "preScript": "",
- "isActive": 1
-}
-```
-
-**响应**(create/update/delete/config/save)
-
-```json
-null
-```
-
-#### 渠道买价(子表接口)
-
-| 接口名称 | 请求方式 | 请求路径 | 说明 |
-| --- | --- | --- | --- |
-| 买价项列表 | 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 | 批量保存渠道买价(保存并可选发布) |
-
-**请求 /channel/service-channel/price/list**(GET,Query 参数)
-
-```
-?serviceChannelId=1
-```
-
-**响应**
-
-```json
-{
- "items": [
- {
- "id": 1,
- "serviceChannelId": 1,
- "serialNumber": 1,
- "status": 1,
- "priceName": "运费",
- "costItem": "freight",
- "costUnit": "kg",
- "currency": "RMB",
- "chargingTime": "出库",
- "chargingCondition": "",
- "factors": [
- {
- "id": 1,
- "priceId": 1,
- "serialNumber": 1,
- "factor": "货物类型",
- "factorValues": [
- "普货",
- "敏感货"
- ]
- }
- ],
- "details": [
- {
- "id": 1,
- "priceId": 1,
- "factorValuesJson": {
- "分区": "欧洲",
- "货物类型": "普货",
- "重量": "0-0.5"
- },
- "calculationFormula": "单价",
- "calculatedValue": 45,
- "customerGroupValues": {}
- }
- ],
- "createTime": "2024-01-01 12:00:00",
- "updateTime": "2024-01-01 12:00:00"
- }
- ]
-}
-```
-
-**请求 /channel/service-channel/price/create**(POST)
-
-```json
-{
- "serviceChannelId": 1,
- "serialNumber": 1,
- "status": 1,
- "priceName": "运费",
- "costItem": "freight",
- "costUnit": "kg",
- "currency": "RMB",
- "chargingTime": "出库",
- "chargingCondition": "",
- "factors": [
- {
- "serialNumber": 1,
- "factor": "货物类型",
- "factorValues": ["普货", "敏感货"]
- }
- ],
- "details": [
- {
- "factorValuesJson": {"分区": "欧洲", "货物类型": "普货", "重量": "0-0.5"},
- "calculationFormula": "单价",
- "calculatedValue": 45,
- "customerGroupValues": {}
- }
- ]
-}
-```
-
-**请求 /channel/service-channel/price/update**(POST)
-
-```json
-{
- "id": 1,
- "serviceChannelId": 1,
- "serialNumber": 1,
- "status": 1,
- "priceName": "运费",
- "costItem": "freight",
- "costUnit": "kg",
- "currency": "RMB",
- "chargingTime": "出库",
- "chargingCondition": "",
- "factors": [],
- "details": []
-}
-```
-
-**请求 /channel/service-channel/price/delete**(POST)
-
-```json
-{
- "id": 1
-}
-```
-
-**请求 /channel/service-channel/price/toggleStatus**(POST)
-
-```json
-{
- "id": 1,
- "status": 0
-}
-```
-
-**请求 /channel/service-channel/price/save**(POST)
-
-```json
-{
- "serviceChannelId": 1,
- "publish": false,
- "prices": [
- {
- "id": 1,
- "serialNumber": 1,
- "status": 1,
- "priceName": "运费",
- "costItem": "freight",
- "costUnit": "kg",
- "currency": "RMB",
- "chargingTime": "出库",
- "chargingCondition": "",
- "factors": [],
- "details": []
- }
- ]
-}
-```
-
-**响应**(create/update/delete/toggleStatus/save)
-
-```json
-null
-```
-
-#### 服务渠道发布记录(子表接口)
-
-| 接口名称 | 请求方式 | 请求路径 | 说明 |
-| --- | --- | --- | --- |
-| 价格发布记录列表 | GET | /channel/service-channel/publish/price/list | 按渠道ID获取买价发布历史 |
-| 规则发布记录列表 | GET | /channel/service-channel/publish/rules/list | 按渠道ID获取规则发布历史 |
-| 发布 | POST | /channel/service-channel/publish | 发布当前配置(买价或规则) |
-| 回滚到版本 | POST | /channel/service-channel/publish/rollback | 回滚到指定版本 |
-
-**请求 /channel/service-channel/publish/price/list**(GET,Query 参数)
-
-```
-?serviceChannelId=1&page=1&pageSize=20
-```
-
-**响应**
-
-```json
-{
- "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
-}
-```
-
-**请求 /channel/service-channel/publish/rules/list**(GET,Query 参数)
-
-```
-?serviceChannelId=1&page=1&pageSize=20
-```
-
-**响应**(结构同 price/list,operatorType 为 rules)
-
-**请求 /channel/service-channel/publish**(POST)
-
-```json
-{
- "serviceChannelId": 1,
- "operatorType": "price",
- "description": "买价调整"
-}
-```
-
-**请求 /channel/service-channel/publish/rollback**(POST)
-
-```json
-{
- "serviceChannelId": 1,
- "recordId": 1,
- "operatorType": "price"
-}
-```
-
-**响应**(publish/rollback)
-
-```json
-null
-```
-
-#### 黑名单区 / 偏远分区 / 预警分区(共用 KpiArea)
-
-> 共用 `Track/ForewarningService/KpiArea/*` 接口,通过 `appKey` 区分类型。建议网关层映射为 `/channel/kpi-area/*` 以符合 URL 规范。
-
-| 类型 | appKey 建议值 |
-| --- | --- |
-| 黑名单区 | blacklist |
-| 偏远分区 | remote |
-| 预警分区 | warning |
-
-| 接口名称 | 请求方式 | 请求路径 | 说明 |
-| --- | --- | --- | --- |
-| 创建分区 | POST | Track/ForewarningService/KpiArea/CreateKpiArea | 创建分区 |
-| 修改分区 | POST | Track/ForewarningService/KpiArea/ModifyKpiArea | 修改分区 |
-| 删除分区 | POST | Track/ForewarningService/KpiArea/DeleteKpiArea | 删除分区 |
-| 分区列表 | POST | Track/ForewarningService/KpiArea/GetKpiAreas | 分区列表 |
-| 新增分区明细 | POST | Track/ForewarningService/KpiArea/AddAreaInfo | 新增分区明细 |
-| 修改分区明细 | POST | Track/ForewarningService/KpiArea/ModifyKpiAreaInfo | 修改分区明细 |
-| 删除分区明细 | POST | Track/ForewarningService/KpiArea/DeleteKpiAreaInfo | 删除分区明细 |
-| 分区明细列表 | POST | Track/ForewarningService/KpiArea/GetKpiAreaInfos | 分区明细列表 |
-
-**请求 Track/ForewarningService/KpiArea/CreateKpiArea**(POST)
-
-```json
-{
- "appKey": "blacklist",
- "areaType": "blacklist",
- "name": "黑名单区1",
- "systemName": "系统黑名单"
-}
-```
-
-**请求 Track/ForewarningService/KpiArea/ModifyKpiArea**(POST)
-
-```json
-{
- "id": 1,
- "appKey": "blacklist",
- "areaType": "blacklist",
- "name": "黑名单区1",
- "systemName": "系统黑名单"
-}
-```
-
-**请求 Track/ForewarningService/KpiArea/DeleteKpiArea**(POST)
-
-```json
-{
- "id": 1
-}
-```
-
-**请求 Track/ForewarningService/KpiArea/GetKpiAreas**(POST)
-
-```json
-{
- "appKey": "blacklist",
- "areaType": "blacklist",
- "page": 1,
- "pageSize": 20
-}
-```
-
-**响应**(GetKpiAreas)
-
-```json
-{
- "items": [
- {
- "id": 1,
- "appKey": "blacklist",
- "areaType": "blacklist",
- "name": "黑名单区1",
- "systemName": "系统黑名单",
- "createTime": "2024-01-01 12:00:00",
- "updateTime": "2024-01-01 12:00:00"
- }
- ],
- "total": 5
-}
-```
-
-**请求 Track/ForewarningService/KpiArea/AddAreaInfo**(POST)
-
-```json
-{
- "areaId": 1,
- "countryCode": "US",
- "countryName": "美国",
- "province": "加州",
- "city": "洛杉矶",
- "leftPostcode": "90001",
- "rightPostcode": "90099"
-}
-```
-
-**请求 Track/ForewarningService/KpiArea/ModifyKpiAreaInfo**(POST)
-
-```json
-{
- "id": 1,
- "areaId": 1,
- "countryCode": "US",
- "countryName": "美国",
- "province": "加州",
- "city": "洛杉矶",
- "leftPostcode": "90001",
- "rightPostcode": "90099"
-}
-```
-
-**请求 Track/ForewarningService/KpiArea/DeleteKpiAreaInfo**(POST)
-
-```json
-{
- "id": 1
-}
-```
-
-**请求 Track/ForewarningService/KpiArea/GetKpiAreaInfos**(POST)
-
-```json
-{
- "areaId": 1,
- "page": 1,
- "pageSize": 20
-}
-```
-
-**响应**(GetKpiAreaInfos)
-
-```json
-{
- "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
-}
-```
-
-**响应**(Create/Modify/Delete/Add/Modify/Delete 明细)
-
-```json
-null
-```
-
-#### 计重规则 / 计重进位
-
-| 接口名称 | 请求方式 | 请求路径 | 说明 |
-| --- | --- | --- | --- |
-| 规则列表 | 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 | 删除明细 |
-
-计重进位接口同上,路径改为 `/channel/weight-rounding/*`,请求/响应结构与计重规则一致。
-
-**请求 /channel/weight-rounding/list**(POST)
-
-```json
-{
- "appKey": "itfx",
- "page": 1,
- "pageSize": 20
-}
-```
-
-**响应**
-
-```json
-{
- "items": [
- {
- "id": 1,
- "appKey": "itfx",
- "name": "标准进位",
- "createTime": "2024-01-01 12:00:00",
- "updateTime": "2024-01-01 12:00:00"
- }
- ],
- "total": 5
-}
-```
-
-**请求 /channel/weight-rounding/create**(POST)
-
-```json
-{
- "appKey": "itfx",
- "name": "标准进位"
-}
-```
-
-**请求 /channel/weight-rounding/update**(POST)
-
-```json
-{
- "id": 1,
- "appKey": "itfx",
- "name": "标准进位"
-}
-```
-
-**请求 /channel/weight-rounding/delete**(POST)
-
-```json
-{
- "id": 1
-}
-```
-
-**响应**(create/update/delete)
-
-```json
-null
-```
-
-**请求 /channel/weight-rule/list**(POST)
-
-```json
-{
- "appKey": "itfx",
- "page": 1,
- "pageSize": 20
-}
-```
-
-**响应**
-
-```json
-{
- "items": [
- {
- "id": 1,
- "appKey": "itfx",
- "name": "标准计重规则",
- "createTime": "2024-01-01 12:00:00",
- "updateTime": "2024-01-01 12:00:00"
- }
- ],
- "total": 5
-}
-```
-
-**请求 /channel/weight-rule/create**(POST)
-
-```json
-{
- "appKey": "itfx",
- "name": "标准计重规则"
-}
-```
-
-**请求 /channel/weight-rule/update**(POST)
-
-```json
-{
- "id": 1,
- "appKey": "itfx",
- "name": "标准计重规则"
-}
-```
-
-**请求 /channel/weight-rule/delete**(POST)
-
-```json
-{
- "id": 1
-}
-```
-
-**请求 /channel/weight-rule/info/list**(POST)
-
-```json
-{
- "ruleId": 1,
- "page": 1,
- "pageSize": 20
-}
-```
-
-**响应**
-
-```json
-{
- "items": [
- {
- "id": 1,
- "ruleId": 1,
- "client": "公共",
- "condition": "gw>vw",
- "billableWeight": "vw",
- "volumetricFactor": "6000",
- "createTime": "2024-01-01 12:00:00",
- "updateTime": "2024-01-01 12:00:00"
- }
- ],
- "total": 3
-}
-```
-
-**请求 /channel/weight-rule/info/add**(POST)
-
-```json
-{
- "ruleId": 1,
- "client": "公共",
- "condition": "gw>vw",
- "billableWeight": "vw",
- "volumetricFactor": "6000"
-}
-```
-
-**请求 /channel/weight-rule/info/update**(POST)
-
-```json
-{
- "id": 1,
- "ruleId": 1,
- "client": "公共",
- "condition": "gw>vw",
- "billableWeight": "vw",
- "volumetricFactor": "6000"
-}
-```
-
-**请求 /channel/weight-rule/info/delete**(POST)
-
-```json
-{
- "id": 1
-}
-```
-
-**响应**(create/update/delete/info/add/update/delete)
-
-```json
-null
-```
-
-**计重进位**(/channel/weight-rounding/*)请求/响应结构与计重规则类似,规则明细改为:
-
-**请求 /channel/weight-rounding/info/add**(POST)
-
-```json
-{
- "roundingId": 1,
- "startValue": 0,
- "endValue": 1,
- "public": "0.5",
- "customerGroup1": "0.5",
- "customerGroup2": "0.25"
-}
-```
-
-**响应**(info/list)
-
-```json
-{
- "items": [
- {
- "id": 1,
- "roundingId": 1,
- "startValue": 0,
- "endValue": 1,
- "public": "0.5",
- "customerGroup1": "0.5",
- "customerGroup2": "0.25",
- "createTime": "2024-01-01 12:00:00",
- "updateTime": "2024-01-01 12:00:00"
- }
- ],
- "total": 5
-}
-```
-
-#### 自动触发
-
-| 接口名称 | 请求方式 | 请求路径 | 说明 |
-| --- | --- | --- | --- |
-| 列表 | 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 | 切换启用状态 |
-
-**请求 /channel/auto-trigger/list**(GET,Query 参数)
-
-```
-?page=1&pageSize=20&operation=&triggerTiming=
-```
-
-**响应**
-
-```json
-{
- "items": [
- {
- "id": 1,
- "status": 1,
- "operation": "拦截",
- "triggerTiming": "下单",
- "triggerCondition": "weight>30",
- "promptMessage": "重量超过30kg,请确认",
- "notifier": "admin@example.com",
- "createTime": "2024-01-01 12:00:00",
- "updateTime": "2024-01-01 12:00:00"
- }
- ],
- "total": 5
-}
-```
-
-**请求 /channel/auto-trigger/create**(POST)
-
-```json
-{
- "status": 1,
- "operation": "拦截",
- "triggerTiming": "下单",
- "triggerCondition": "weight>30",
- "promptMessage": "重量超过30kg,请确认",
- "notifier": "admin@example.com"
-}
-```
-
-**请求 /channel/auto-trigger/update**(POST)
-
-```json
-{
- "id": 1,
- "status": 1,
- "operation": "拦截",
- "triggerTiming": "下单",
- "triggerCondition": "weight>30",
- "promptMessage": "重量超过30kg,请确认",
- "notifier": "admin@example.com"
-}
-```
-
-**请求 /channel/auto-trigger/delete**(POST)
-
-```json
-{
- "id": 1
-}
-```
-
-**请求 /channel/auto-trigger/toggleStatus**(POST)
-
-```json
-{
- "id": 1,
- "status": 0
-}
-```
-
-**响应**(create/update/delete/toggleStatus)
-
-```json
-null
-```
-
-#### 标签设计
-
-| 接口名称 | 请求方式 | 请求路径 | 说明 |
-| --- | --- | --- | --- |
-| 保存标签模板 | POST | /channel/label-template/save | 保存标签模板 |
-
-**请求 /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
-{
- "id": 1,
- "name": "尾程标签模板1",
- "templateType": "lastMile",
- "templateJson": {},
- "createTime": "2024-01-01 12:00:00",
- "updateTime": "2024-01-01 12:00:00"
-}
-```
-
-----------------------------------------------------------------------------------------
-
-----------------------------------------------------------------------------------------
-
-## 四、操作模块
-
-### 3.1 核心需求说明
-
-- **入库扫描**:扫描运单号完成入库,记录实重、尺寸、销售产品、目的国、仓库、操作人、操作时间;支持查看最近入库记录。
-- **出库扫描**:扫描运单号完成出库,记录仓库、操作人、操作时间;支持查看最近出库记录。
-- **标签打印**:按运单号/客户单号/跟踪号/其他单号选择标签模板打印;记录打印结果与操作人。
-
-### 3.2 表结构设计
-
-#### 入库记录表 `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 | - | - | 否 | 更新时间 |
-
-#### 出库记录表 `t_outbound_record`
-
-| 字段名 | 字段类型 | 长度 | 主键/外键 | 非空 | 说明 |
-| --- | --- | --- | --- | --- | --- |
-| Id | BIGINT | - | 主键 | 是 | 自增唯一标识 |
-| WaybillNo | VARCHAR | 64 | - | 是 | 运单号 |
-| WarehouseId | BIGINT | - | 外键 | 否 | 仓库ID |
-| OperateTime | DATETIME | - | - | 是 | 操作时间 |
-| OperatorId | BIGINT | - | 外键 | 否 | 操作人ID |
-| IsDeleted | TINYINT | - | - | 是 | 是否删除:0-否,1-是,默认0 |
-| CreateUserId | INT | - | - | 是 | 创建人 |
-| CreateTime | DATETIME | - | - | 是 | 创建时间 |
-| UpdateUserId | INT | - | - | 否 | 更新人 |
-| UpdateTime | DATETIME | - | - | 否 | 更新时间 |
-
-#### 标签打印记录表 `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 | - | - | 否 | 更新时间 |
-
-### 3.3 核心接口设计
-
-#### 入库扫描
-
-| 接口名称 | 请求方式 | 请求路径 | 说明 |
-| --- | --- | --- | --- |
-| 入库 | POST | /operation/inbound/create | 入库 |
-| 最近入库记录 | GET | /operation/inbound/recent-list | 最近入库记录 |
-
-**请求 /operation/inbound/create**(POST)
-
-```json
-{
- "waybillNo": "LP1234567890",
- "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
-{
- "id": 1,
- "waybillNo": "LP1234567890",
- "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"
-}
-```
-
-**请求 /operation/inbound/recent-list**(GET,Query 参数)
-
-```
-?limit=20&warehouseId=
-```
-
-**响应**
-
-```json
-{
- "items": [
- {
- "id": 1,
- "waybillNo": "LP1234567890",
- "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"
- }
- ]
-}
-```
-
-#### 出库扫描
-
-| 接口名称 | 请求方式 | 请求路径 | 说明 |
-| --- | --- | --- | --- |
-| 出库 | POST | /operation/outbound/create | 出库 |
-| 最近出库记录 | GET | /operation/outbound/recent-list | 最近出库记录 |
-
-**请求 /operation/outbound/create**(POST)
-
-```json
-{
- "waybillNo": "LP1234567890",
- "warehouseId": 1
-}
-```
-
-> 参数说明:`waybillNo` 运单号;`warehouseId` 仓库ID(可选,不传则从订单或默认仓库获取)。操作时间、操作人由服务端从当前会话获取。
-
-**响应**
-
-```json
-{
- "id": 1,
- "waybillNo": "LP1234567890",
- "warehouseId": 1,
- "operateTime": "2024-01-01 12:00:00",
- "operatorId": 1,
- "createTime": "2024-01-01 12:00:00"
-}
-```
-
-**请求 /operation/outbound/recent-list**(GET,Query 参数)
-
-```
-?limit=20&warehouseId=
-```
-
-**响应**
-
-```json
-{
- "items": [
- {
- "id": 1,
- "waybillNo": "LP1234567890",
- "warehouseId": 1,
- "warehouseName": "深圳仓",
- "operateTime": "2024-01-01 12:00:00",
- "operatorId": 1,
- "operatorName": "张三",
- "createTime": "2024-01-01 12:00:00"
- }
- ]
-}
-```
-
-#### 标签打印
-
-| 接口名称 | 请求方式 | 请求路径 | 说明 |
-| --- | --- | --- | --- |
-| 打印标签 | POST | /operation/print-label/print | 打印标签 |
-| 最近打印记录 | GET | /operation/print-label/recent-list | 最近打印记录 |
-| 标签模板列表 | GET | /operation/print-label/template-list | 标签模板列表 |
-
-**请求 /operation/print-label/print**(POST)
-
-```json
-{
- "number": "LP1234567890",
- "numberType": "waybill",
- "labelTemplateId": 1
-}
-```
-
-> 参数说明:`number` 运单号/客户单号/跟踪号/其他单号;`numberType` 单号类型:waybill(运单号)/customerOrder(客户单号)/tracking(跟踪号)/other(其他单号);`labelTemplateId` 标签模板ID,可选,不传则按 templateType 取默认模板。也可传 `templateType`:lastMile(尾程)/customer(客户)/inbound(入库)/outbound(出库)/sku。
-
-**响应**
-
-```json
-{
- "id": 1,
- "waybillNo": "LP1234567890",
- "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"
-}
-```
-
-**请求 /operation/print-label/recent-list**(GET,Query 参数)
-
-```
-?limit=20&labelTemplateId=
-```
-
-**响应**
-
-```json
-{
- "items": [
- {
- "id": 1,
- "waybillNo": "LP1234567890",
- "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"
- }
- ]
-}
-```
-
-**请求 /operation/print-label/template-list**(GET,Query 参数)
-
-```
-?templateType=lastMile
-```
-
-> 参数说明:`templateType` 可选,筛选模板类型:lastMile/customer/inbound/outbound/sku。不传则返回全部可用模板。
-
-**响应**
-
-```json
-{
- "items": [
- {
- "id": 1,
- "name": "尾程标签模板1",
- "templateType": "lastMile",
- "templateJson": {},
- "createTime": "2024-01-01 12:00:00",
- "updateTime": "2024-01-01 12:00:00"
- }
- ]
-}
-```
-
-----------------------------------------------------------------------------------------
-
-----------------------------------------------------------------------------------------
-
-## 五、客户模块
-
-### 4.1 核心需求说明
-
-- **客户管理**:客户信息的增删改查,支持客户编号、手机、邮箱、余额、额度、状态等管理;支持重置密码;已有 customer API。
-- **子账号**:主客户可创建子账号,子账号共享主客户余额与额度,独立登录与权限。
-
-### 4.2 表结构设计
-
-#### 客户表 `t_customer`
-
-> 已有 customer API,表结构参考 CustomerDto。
-
-| 字段名 | 字段类型 | 长度 | 主键/外键 | 非空 | 说明 |
-| --- | --- | --- | --- | --- | --- |
-| Id | BIGINT | - | 主键 | 是 | 自增唯一标识 |
-| CustomerCode | VARCHAR | 32 | - | 是 | 客户编号,规则:前缀w+年份+序号(如W2501) |
-| WechatAvatar | VARCHAR | 255 | - | 否 | 微信头像URL |
-| WechatNickname | VARCHAR | 64 | - | 否 | 微信昵称 |
-| Email | VARCHAR | 64 | - | 否 | 邮箱,唯一,用于登录/找回密码 |
-| Phone | VARCHAR | 16 | - | 是 | 手机号,唯一,用于登录/验证 |
-| Balance | DECIMAL | 18,2 | - | 是 | 账户余额,默认0 |
-| TotalQuota | DECIMAL | 18,2 | - | 是 | 总额度,默认0 |
-| UsedQuota | DECIMAL | 18,2 | - | 是 | 已用额度,默认0 |
-| Status | TINYINT | - | - | 是 | 状态:0-禁用,1-正常,2-冻结 |
-| Remark | VARCHAR | 512 | - | 否 | 备注信息 |
-| CreateUserId | INT | - | - | 是 | 创建人(系统/管理员账号) |
-| CreateTime | DATETIME | - | - | 是 | 创建时间,默认当前时间 |
-| UpdateUserId | INT | - | - | 否 | 更新人 |
-| UpdateTime | DATETIME | - | - | 否 | 更新时间 |
-| Salesman | VARCHAR | 32 | - | 否 | 所属销售员账号 |
-| CustomerService | VARCHAR | 32 | - | 否 | 所属客服账号 |
-| CustomerGroup | TINYINT | - | - | 否 | 客户分组:0-普通客户,1-VIP1,2-VIP2,3-VIP3,4-VIP4,5-VIP5 |
-| Role | VARCHAR | 64 | - | 否 | 角色,用于菜单权限 |
-| LastLoginIp | VARCHAR | 64 | - | 否 | 最后在线IP |
-| LastLoginTime | DATETIME | - | - | 否 | 最后在线时间 |
-| IsDeleted | TINYINT | - | - | 是 | 是否删除:0-否,1-是,默认0 |
-| WechatOpenid | VARCHAR | 64 | - | 否 | 微信OpenID,唯一,关联微信账号 |
-| Password | VARCHAR | 64 | - | 是 | 密码(MD5加密存储) |
-
-**CustomerGroup 枚举**:0-普通客户、1-VIP1、2-VIP2、3-VIP3、4-VIP4、5-VIP5
-
-#### 子账号表 `t_customer_sub_account`
-
-| 字段名 | 字段类型 | 长度 | 主键/外键 | 非空 | 说明 |
-| --- | --- | --- | --- | --- | --- |
-| Id | BIGINT | - | 主键 | 是 | 自增唯一标识 |
-| MainCustomerId | BIGINT | - | 外键(t_customer.Id) | 是 | 主客户ID |
-| SubAccountCode | VARCHAR | 32 | - | 是 | 子账号编号,规则:主客户编号+"_"+序号 |
-| SubWechatAvatar | VARCHAR | 255 | - | 否 | 子账号微信头像 |
-| SubWechatNickname | VARCHAR | 64 | - | 否 | 子账号微信昵称 |
-| SubPhone | VARCHAR | 16 | - | 是 | 子账号手机号,唯一 |
-| SubEmail | VARCHAR | 64 | - | 否 | 子账号邮箱 |
-| SubPassword | VARCHAR | 64 | - | 是 | 子账号密码(MD5加密) |
-| SubStatus | TINYINT | - | - | 是 | 子账号状态:0-禁用,1-正常 |
-| CreateUserId | INT | - | - | 是 | 创建人(主客户/管理员) |
-| CreateTime | DATETIME | - | - | 是 | 创建时间 |
-| UpdateUserId | INT | - | - | 否 | 更新人 |
-| UpdateTime | DATETIME | - | - | 否 | 更新时间 |
-| IsDeleted | TINYINT | - | - | 是 | 是否删除:0-否,1-是 |
-
-### 4.3 核心接口设计
-
-| 接口名称 | 请求方式 | 请求路径 | 说明 |
-| --- | --- | --- | --- |
-| 客户列表 | GET | /customer/list | 客户列表 |
-| 客户详情 | GET | /customer/get | 客户详情 |
-| 创建客户 | POST | /customer/create | 创建客户 |
-| 更新客户 | POST | /customer/update | 更新客户 |
-| 删除客户 | POST | /customer/delete | 删除客户 |
-| 重置密码 | POST | /customer/reset-password | 重置密码 |
-
-> 个人中心可复用 `/customer/get`,传入当前登录客户 ID。
-
-**请求 /customer/list**(GET,Query 参数)
-
-```
-?page=1&pageSize=20&keyword=&status=1&customerGroup=&salesman=&customerService=
-```
-
-**响应**
-
-```json
-{
- "items": [
- {
- "id": 1,
- "customerCode": "W2501",
- "wechatAvatar": "",
- "wechatNickname": "",
- "email": "customer@example.com",
- "phone": "13800138000",
- "balance": 1000.0,
- "totalQuota": 5000.0,
- "usedQuota": 500.0,
- "status": 1,
- "remark": "",
- "salesman": "sales1",
- "customerService": "cs1",
- "customerGroup": 1,
- "lastLoginIp": "192.168.1.1",
- "lastLoginTime": "2024-01-01 12:00:00",
- "createTime": "2024-01-01 12:00:00",
- "updateTime": "2024-01-01 12:00:00"
- }
- ],
- "total": 50
-}
-```
-
-**请求 /customer/get**(GET,Query 参数)
-
-```
-?id=1
-```
-
-**响应**
-
-```json
-{
- "id": 1,
- "customerCode": "W2501",
- "wechatAvatar": "",
- "wechatNickname": "",
- "email": "customer@example.com",
- "phone": "13800138000",
- "balance": 1000.0,
- "totalQuota": 5000.0,
- "usedQuota": 500.0,
- "status": 1,
- "remark": "",
- "salesman": "sales1",
- "customerService": "cs1",
- "customerGroup": 1,
- "lastLoginIp": "192.168.1.1",
- "lastLoginTime": "2024-01-01 12:00:00",
- "createTime": "2024-01-01 12:00:00",
- "createBy": "admin",
- "updateTime": "2024-01-01 12:00:00",
- "updateBy": "admin"
-}
-```
-
-**请求 /customer/create**(POST)
-
-```json
-{
- "phone": "13800138000",
- "email": "customer@example.com",
- "password": "123456",
- "totalQuota": 5000.00,
- "status": 1,
- "remark": "",
- "salesman": "sales1",
- "customerService": "cs1",
- "customerGroup": 1
-}
-```
-
-**请求 /customer/update**(POST)
-
-```json
-{
- "id": 1,
- "email": "customer@example.com",
- "totalQuota": 5000.00,
- "status": 1,
- "remark": "",
- "salesman": "sales1",
- "customerService": "cs1",
- "customerGroup": 1
-}
-```
-
-**请求 /customer/delete**(POST)
-
-```json
-{
- "id": 1
-}
-```
-
-**请求 /customer/reset-password**(POST)
-
-```json
-{
- "id": 1,
- "newPassword": "123456"
-}
-```
-
-**响应**(create/update/delete/reset-password)
-
-```json
-null
-```
-
-----------------------------------------------------------------------------------------
-
-----------------------------------------------------------------------------------------
-
-## 六、充值管理
-
-### 5.1 核心需求说明
-
-- **充值流程**:客户提交充值金额 → 选择支付方式(微信/支付宝)→ 上传充值截图 → 提交审核 → 管理员审核(通过/驳回)→ 审核通过后更新客户余额。
-- **充值记录**:完整留存充值全流程信息,支持查询、导出;充值状态包含:待审核、审核通过、审核驳回。
-
-### 5.2 表结构设计
-
-#### 充值记录表 `t_recharge_record`
-
-| 字段名 | 字段类型 | 长度 | 主键/外键 | 非空 | 说明 |
-| --- | --- | --- | --- | --- | --- |
-| Id | BIGINT | - | 主键 | 是 | 自增唯一标识 |
-| CustomerId | BIGINT | - | 外键(t_customer.Id) | 是 | 客户ID(主账号/子账号关联主账号ID) |
-| 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 |
-
-### 5.3 核心接口设计
-
-| 接口名称 | 请求方式 | 请求路径 | 请求参数 | 响应参数 | 接口说明 |
-| --- | --- | --- | --- | --- | --- |
-| 提交充值申请 | POST | /recharge/recharge/submit | customerId, payType, rechargeAmount, screenshotUrl, remark | serialNumber | 生成流水号,保存充值记录,状态为待审核 |
-| 审核充值申请 | POST | /recharge/recharge-record/approve 或 reject | id, auditStatus, auditBy, remark | null | 审核通过则更新客户余额,更新审核时间、状态、审核人 |
-| 查询充值记录 | GET | /recharge/recharge-record/list | customerId, serialNumber, startTime, endTime, status | list, total | 多条件分页查询充值记录 |
-| 导出充值记录 | GET | /recharge/export | customerId, startTime, endTime | 文件流 | 导出充值记录为Excel文件 |
-
-**请求 /recharge/recharge/submit**(POST)
-
-```json
-{
- "customerId": 1,
- "rechargeAmount": 500,
- "payType": 1,
- "rechargeScreenshot": "https://example.com/upload/screenshot.jpg",
- "remark": "充值500元"
-}
-```
-
-> 参数说明:`customerId` 客户ID(客户端提交时可不传,取当前登录客户);`rechargeAmount` 充值金额,≥0.01;`payType` 支付方式:1-微信,2-支付宝;`rechargeScreenshot` 充值截图URL;`remark` 备注。
-
-**响应**
-
-```json
-{
- "id": 1,
- "serialNumber": "RECHARGE2024010100123456",
- "customerId": 1,
- "rechargeAmount": 500.0,
- "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"
-}
-```
-
-**请求 /recharge/recharge-record/approve**(POST)
-
-```json
-{
- "id": 1,
- "remark": "审核通过"
-}
-```
-
-**请求 /recharge/recharge-record/reject**(POST)
-
-```json
-{
- "id": 1,
- "remark": "截图不清晰,请重新上传"
-}
-```
-
-**响应**(approve/reject)
-
-```json
-null
-```
-
-**请求 /recharge/recharge-record/list**(GET,Query 参数)
-
-```
-?page=1&pageSize=20&customerId=1&serialNumber=&rechargeStatus=0&startTime=2024-01-01&endTime=2024-01-31
-```
-
-**响应**
-
-```json
-{
- "items": [
- {
- "id": 1,
- "customerId": 1,
- "customerCode": "W2501",
- "customerPhone": "13800138000",
- "serialNumber": "RECHARGE2024010100123456",
- "rechargeTime": "2024-01-01 12:00:00",
- "payType": 1,
- "payTypeName": "微信",
- "rechargeAmount": 500.0,
- "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
-}
-```
-
-**请求 /recharge/export**(GET,Query 参数)
-
-```
-?customerId=1&startTime=2024-01-01&endTime=2024-01-31
-```
-
-**响应**
-
-> 返回 Excel 文件流,Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet
-
-----------------------------------------------------------------------------------------
-
-----------------------------------------------------------------------------------------
-
-## 七、系统模块
-
-### 6.1 核心需求说明
-
-- **仓库地址**:管理仓库收货地址信息,包括主仓/中转仓/退货仓/其他类型;支持收件人、联系电话、收货地址、客服电话、客服微信及二维码等配置。
-
-### 6.2 表结构设计
-
-#### 仓库地址表 `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 | - | - | 否 | 更新时间 |
-
-### 6.3 核心接口设计
-
-| 接口名称 | 请求方式 | 请求路径 | 说明 |
-| --- | --- | --- | --- |
-| 仓库列表 | GET | /system/warehouse/list | 仓库列表 |
-| 仓库详情 | GET | /system/warehouse/detail | 仓库详情 |
-| 新增仓库 | POST | /system/warehouse/create | 新增 |
-| 更新仓库 | POST | /system/warehouse/update | 更新 |
-| 删除仓库 | POST | /system/warehouse/delete | 删除 |
-
-**请求 /system/warehouse/list**(GET,Query 参数)
-
-```
-?page=1&pageSize=20&warehouseType=main&keyword=
-```
-
-**响应**
-
-```json
-{
- "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
-}
-```
-
-**请求 /system/warehouse/detail**(GET,Query 参数)
-
-```
-?id=1
-```
-
-**响应**
-
-```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"
- ],
- "createTime": "2024-01-01 12:00:00",
- "createBy": "admin",
- "updateTime": "2024-01-01 12:00:00",
- "updateBy": "admin"
-}
-```
-
-**请求 /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"]
-}
-```
-
-**请求 /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"]
-}
-```
-
-**请求 /system/warehouse/delete**(POST)
-
-```json
-{
- "id": 1
-}
-```
-
-**响应**(create/update/delete)
-
-```json
-null
-```
-
-----------------------------------------------------------------------------------------
-
-----------------------------------------------------------------------------------------
-
-## 附录
-
-### 附录一:接口路径规范(kebab-case 汇总)
-
-| 模块 | 路径前缀 | 示例 |
-| --- | --- | --- |
-| 订单 | /order/ | list, detail, pay, pre-declare, cancel-intercept, delete, price-check |
-| 渠道-销售产品 | /channel/sales-product/ | list, detail, create, update, delete, config/save, price/list, allocation/get |
-| 渠道-服务渠道 | /channel/service-channel/ | list, detail, create, update, delete, price/list |
-| 渠道-计重 | /channel/weight-rule/, weight-rounding/ | list, create, update, delete, info/list |
-| 渠道-自动触发 | /channel/auto-trigger/ | list, create, update, delete, toggle-status |
-| 渠道-标签 | /channel/label-template/save | |
-| 操作 | /operation/ | inbound/create, inbound/recent-list, outbound/create, print-label/print |
-| 客户 | /customer/ | list, get, create, update, delete, reset-password |
-| 充值 | /recharge/ | recharge/submit, recharge-record/list, approve, reject |
-| 系统 | /system/warehouse/ | list, detail, create, update, delete |
-
-### 附录二:分区类型说明(黑名单/偏远/预警)
-
-| 页面 | area_type / Key | 说明 |
-| --- | --- | --- |
-| 黑名单区 | blacklist | 黑名单分区 |
-| 偏远分区 | remote | 偏远分区 |
-| 预警分区 | warning | 轨迹下预警分区 |
-
-调用 KpiArea 接口时,传入对应 `Key` 即可区分不同业务分区,无需新建表或接口。
-
-### 附录三:状态码与枚举
-
-#### 订单状态
-
-| status | 中文 |
-| --- | --- |
-| draft | 草稿 |
-| predeclared | 已预报 |
-| received | 已收货 |
-| pendingPayment | 待支付 |
-| pendingShip | 待发货 |
-| pendingOnline | 待上网 |
-| shipping | 运输中 |
-| deliverySuccess | 派送成功 |
-| deliveryFailed | 派送失败 |
-| problematic | 问题件 |
-| deleted | 已删除 |
-| claimed | 已理赔 |
-
-#### 充值状态
-
-| payStatus | 中文 |
-| --- | --- |
-| 0 | 待审核 |
-| 1 | 成功 |
-| 2 | 失败 |
-
-#### 仓库类型
-
-| warehouseType | 中文 |
-| --- | --- |
-| main | 主仓 |
-| transit | 中转仓 |
-| return | 退货仓 |
-| other | 其他 |
-
-#### 客户分组(客户表)
-
-| customerGroup | 中文 |
-| --- | --- |
-| 0 | 普通客户 |
-| 1 | VIP1 |
-| 2 | VIP2 |
-| 3 | VIP3 |
-| 4 | VIP4 |
-| 5 | VIP5 |
-
-----------------------------------------------------------------------------------------
-
-*文档版本:v1.0 | 最后更新:2025-03-04*
-
-
-
-
diff --git a/oms表结构与接口设计20260427.md b/oms表结构与接口设计20260427.md
deleted file mode 100644
index 1d07366..0000000
--- a/oms表结构与接口设计20260427.md
+++ /dev/null
@@ -1,5933 +0,0 @@
-# oms 表结构与接口设计文档
-
-----------------------------------------------------------------------------------------
-
-## OMS 系统说明
-
-OMS(Order Management System,订单管理系统)作为独立服务运行和admin权限管理服务合并,面向跨境物流/仓储业务场景,提供**订单**、**渠道**、**操作**、**客户**四大核心模块,支撑从客户注册、充值、下单、定价、支付到货物入库出库的全流程管理。
-
-### 核心模块
-
-| 模块 | 说明 |
-| --- | --- |
-| **订单** | 订单全生命周期管理,包括创建、预报、支付、发货、跟踪等 |
-| **渠道** | 销售产品与物流渠道的配置、定价及买价/卖价计算 |
-| **操作** | 入库、出库、打印等仓储操作 |
-| **客户** | 客户注册、认证、账户余额、额度及权限管理 |
-
-
-----------------------------------------------------------------------------------------
-
-### 1.2 表结构设计公共规范
-
-#### 表的字段命名规范
-
-| 规范 | 说明 | 示例 |
-| --- | --- | --- |
-| 驼峰命名 | 字段名开头大写+单词驼峰 | `ServiceProviderId` |
-| 单数名词 | 表名用单数,表示实体 | `customer` 而非 `customers` |
-| 业务前缀 | 表名按业务模块加前缀 | `t_order`、`t_customer` |
-
-
-#### 必备字段(所有表)
-
-| 字段名 | 字段类型 | 说明 |
-| --- | --- | --- |
-| Id | BIGINT | 主键,自增,步长 1 |
-| IsDeleted | TINYINT | 软删除:0-否,1-是,默认 0 |
-| CreateUserId | INT | 创建人 |
-| CreateTime | DATETIME | 创建时间 |
-| UpdateUserId | INT | 更新人 |
-| UpdateTime | DATETIME | 更新时间 |
-
-#### 字段命名规范
-
-| 规范 | 说明 | 示例 |
-| --- | --- | --- |
-| 布尔字段 | 用 `IsXxx` 或 `HasXxx` | `IsDeleted`、`IsActive` |
-| 外键字段 | `关联表名Id` 或 `XxxId` | `CustomerId`、`WarehouseId` |
-| 时间字段 | `XxxTime` 或 `XxxAt` | `CreateTime`、`PayTime` |
-| 禁用保留字 | 不用 desc、range、match 等 | 用 `Description` 替代 `Desc` |
-
-#### 数据类型规范
-
-| 类型 | 说明 |
-| --- | --- |
-| 主键 | BIGINT,单表自增 |
-| 金额/重量 | DECIMAL,禁止 float/double |
-| 字符串 | varchar 长度 ≤ 5000,超出用 TEXT |
-| 枚举 | 小数据量用 TINYINT,可读性要求高用 VARCHAR |
-| 状态标志 | TINYINT |
-
-### 1.3 接口设计公共规范
-
-| 规范 | 说明 |
-| --- | --- |
-| 操作审计必填 | 任何新增、编辑、删除操作,必须记录操作者与操作时间 |
-| 新增接口 | 创建成功后需落库 `CreateUserId`、`CreateTime` |
-| 编辑接口 | 更新成功后需落库 `UpdateUserId`、`UpdateTime` |
-| 删除接口 | 逻辑删除时需落库 `UpdateUserId`、`UpdateTime`,并更新 `IsDeleted=1` |
-
-> 审计字段中的操作者信息优先从当前登录会话获取,不允许由前端直接信任覆盖。
-
-
-----------------------------------------------------------------------------------------
-
-## 需求迭代目录(按日期管理)
-
-> 迭代编号建议使用 `YYYYMMDD-序号`,按日期倒序维护;不同功能迭代可使用 `-01`、`-02` 递增。
-
-| 迭代日期 | 迭代编号 | 迭代功能 | 变更模块 | 备注 |
-| --- | --- | --- | --- | --- |
-| 2026-04-27 | 20260427-01 | 客户的账号密码接入登录接口/rbac/auth/login。入参增加一个类型,0(用户)/1(客户),客户的角色编码读取`t_customer`上的`Role`字段来划分菜单权限,默认使用编码`Customer` | 客户模块 | 已完成 |
-| 2026-04-27 | 20260427-02 | 任何新增、编辑、删除操作,必须通过authorization来记录操作者与操作时间 | 全局 | 已完成 |
-| 2026-04-27 | 20260427-03 | 文档版本差异汇总(20260427 vs 2026040802) | 全文档 | 已完成结构与内容差异归档,见下方“差异摘要” |
-
-### 20260427-03 差异明细(按表结构与接口)
-
-> 对比范围:`oms表结构与接口设计20260427.md` vs `oms表结构与接口设计2026040802.md`
-
-#### 一、表结构差异
-
-**新增表(4)**
-
-- `t_country`
-- `t_dict`
-- `t_notice`
-- `t_notice_read`
-
-**更新表(0)**
-
-- 无(按表名维度无同名结构替换;本次以新增为主)
-
-**删除表(0)**
-
-- 无
-
-#### 二、接口差异
-
-**新增接口(22)**
-
-- `GET /system/country/list`(国家列表)
-- `GET /system/dict/list`(字典列表)
-- `GET /system/notice/list`(通知列表)
-- `POST /channel/service-channel/group/create`(新增服务商)
-- `POST /channel/service-channel/group/delete`(删除服务商)
-- `POST /channel/service-channel/group/update`(编辑服务商)
-- `POST /order/parcel/batch-create`(批量创建小包)
-- `POST /system/dict/create`(新增字典)
-- `POST /system/dict/delete`(删除字典)
-- `POST /system/dict/update`(更新字典)
-- `POST /system/notice/create`(新增通知)
-- `POST /system/notice/delete`(删除通知)
-- `POST /system/notice/read-confirm`(确认已读)
-- `POST /system/notice/update`(编辑通知)
-
-**更新接口(8)**
-
-分区的接口命名更新了,可忽略,因为已经完成了变更了,知道这个事情就行。
----现在接口命名
-- `POST /channel/kpi-area/create`(创建分区)
-- `POST /channel/kpi-area/delete`(删除分区)
-- `POST /channel/kpi-area/info/add`(新增分区明细)
-- `POST /channel/kpi-area/info/delete`(删除分区明细)
-- `POST /channel/kpi-area/info/list`(分区明细列表)
-- `POST /channel/kpi-area/info/update`(修改分区明细)
-- `POST /channel/kpi-area/list`(分区列表)
-- `POST /channel/kpi-area/update`(修改分区)
----原来接口命名
-- `POST Track/ForewarningService/KpiArea/AddAreaInfo`(新增分区明细)
-- `POST Track/ForewarningService/KpiArea/CreateKpiArea`(创建分区)
-- `POST Track/ForewarningService/KpiArea/DeleteKpiArea`(删除分区)
-- `POST Track/ForewarningService/KpiArea/DeleteKpiAreaInfo`(删除分区明细)
-- `POST Track/ForewarningService/KpiArea/GetKpiAreaInfos`(分区明细列表)
-- `POST Track/ForewarningService/KpiArea/GetKpiAreas`(分区列表)
-- `POST Track/ForewarningService/KpiArea/ModifyKpiArea`(修改分区)
-- `POST Track/ForewarningService/KpiArea/ModifyKpiAreaInfo`(修改分区明细)
-
-**删除接口(10)**
-
-- `POST /channel/sales-product/config/save`(保存产品配置)
-- `POST /channel/service-channel/config/save`(保存渠道配置)
-
-
-
-
-----------------------------------------------------------------------------------------
-
-## 二、订单模块
-
-### 1.1 核心需求说明
-
-- **我的订单**:支持订单全流程管理,包括分页查询、详情查看、支付、预报、拦截/取消拦截、删除、打印、导出;订单状态覆盖草稿→已预报→已收货→待支付→待发货→运输中→派送成功/失败等全生命周期。
-- **我要查价**:根据目的国、运输方式、重量尺寸、货物类型等条件查价,返回可选渠道及费用明细。
-- **我要下单**:支持下载下单模板、上传 Excel 批量下单、查看任务进度与详情;模板分为标准模板与自定义模板。
-
-### 1.2 表结构设计
-
-> **命名约定**:表名按业务模块加 t_ 前缀;字段名驼峰命名(开头大写+单词驼峰)。
-
-#### 订单表 `t_order`
-
-| 字段名 | 字段类型 | 长度 | 主键/外键 | 非空 | 说明 |
-| --- | --- | --- | --- | --- | --- |
-| Id | BIGINT | - | 主键 | 是 | 自增唯一标识 |
-| ServiceNo | VARCHAR | 64 | - | 是 | 运单号 |
-| ClientNo | VARCHAR | 64 | - | 否 | 客户单号 |
-| TrackNo | VARCHAR | 64 | - | 否 | 跟踪号 |
-| ProductCode | VARCHAR | 128 | - | 否 | 产品code |
-| ChannelCode | VARCHAR | 128 | - | 否 | 渠道code |
-| ConsigneeCountry | VARCHAR | 8 | - | 否 | 国家二字码 |
-| CargoAttribute | VARCHAR | 32 | - | 否 | 货物属性:general-普货,electronics-3c&带电,sensitive-敏感货 |
-| OrderStatus | VARCHAR | 32 | - | 是 | 订单状态 |
-| WarehouseId | BIGINT | - | 外键 | 否 | 仓库ID |
-| CustomerId | BIGINT | - | 外键 | 否 | 客户ID |
-| SenderId | BIGINT | - | 外键 | 否 | 发件人ID → t_order_sender |
-| RecipientId | BIGINT | - | 外键 | 否 | 收件人ID → t_order_recipient |
-| DeclarationId | BIGINT | - | 外键 | 否 | 申报信息ID → t_order_declaration |
-| 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-是 |
-| IsDeleted | TINYINT | - | - | 是 | 是否删除:0-否,1-是,默认0 |
-| CreateUserId | INT | - | - | 是 | 创建人 |
-| CreateTime | DATETIME | - | - | 是 | 创建时间 |
-| UpdateUserId | INT | - | - | 否 | 更新人 |
-| UpdateTime | DATETIME | - | - | 否 | 更新时间 |
-
-**OrderStatus 枚举**:draft, predeclared, received, pendingPayment, pendingShip, pendingOnline, shipping, deliverySuccess, deliveryFailed, problematic, deleted, claimed
-
-**CargoAttribute 枚举**:general-普货,electronics-3c&带电,sensitive-敏感货
-
-#### 下单模板表 `t_order_template`
-
-| 字段名 | 字段类型 | 长度 | 主键/外键 | 非空 | 说明 |
-| --- | --- | --- | --- | --- | --- |
-| Id | BIGINT | - | 主键 | 是 | 自增唯一标识 |
-| Name | VARCHAR | 64 | - | 是 | 模板名称 |
-| Type | VARCHAR | 16 | - | 是 | standard/custom |
-| TemplateKey | VARCHAR | 64 | - | 否 | 模板标识 |
-| CustomerId | BIGINT | - | 外键 | 否 | 客户ID(自定义模板) |
-| IsDeleted | TINYINT | - | - | 是 | 是否删除:0-否,1-是,默认0 |
-| CreateUserId | INT | - | - | 是 | 创建人 |
-| CreateTime | DATETIME | - | - | 是 | 创建时间 |
-| UpdateUserId | INT | - | - | 否 | 更新人 |
-| UpdateTime | DATETIME | - | - | 否 | 更新时间 |
-
-#### 下单任务表 `t_order_task`
-
-| 字段名 | 字段类型 | 长度 | 主键/外键 | 非空 | 说明 |
-| --- | --- | --- | --- | --- | --- |
-| Id | BIGINT | - | 主键 | 是 | 自增唯一标识 |
-| TaskName | VARCHAR | 128 | - | 是 | 任务名称 |
-| Quantity | INT | - | - | 否 | 数量 |
-| Status | VARCHAR | 16 | - | 是 | 处理中/已完成/待处理 |
-| TemplateId | BIGINT | - | 外键 | 否 | 模板ID |
-| IsDeleted | TINYINT | - | - | 是 | 是否删除:0-否,1-是,默认0 |
-| CreateUserId | INT | - | - | 是 | 创建人 |
-| CreateTime | DATETIME | - | - | 是 | 创建时间 |
-| UpdateUserId | INT | - | - | 否 | 更新人 |
-| UpdateTime | DATETIME | - | - | 否 | 更新时间 |
-| CustomerId | BIGINT | - | 外键 | 否 | 客户ID |
-
-#### 订单发件人表 `t_order_sender`
-
-| 字段名 | 字段类型 | 长度 | 主键/外键 | 非空 | 说明 |
-| --- | --- | --- | --- | --- | --- |
-| Id | BIGINT | - | 主键 | 是 | 自增唯一标识 |
-| OrderId | BIGINT | - | 外键 | 是 | 订单ID |
-| SenderName | VARCHAR | 50 | - | 否 | 姓名 |
-| Company | VARCHAR | 255 | - | 否 | 公司 |
-| Country | INT | - | - | 否 | 国家 |
-| 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 | - | - | 否 | 更新时间 |
-
-#### 订单收件人表 `t_order_recipient`
-
-| 字段名 | 字段类型 | 长度 | 主键/外键 | 非空 | 说明 |
-| --- | --- | --- | --- | --- | --- |
-| Id | BIGINT | - | 主键 | 是 | 自增唯一标识 |
-| OrderId | BIGINT | - | 外键 | 是 | 订单ID |
-| RecipientsName | VARCHAR | 30 | - | 否 | 收件人名称 |
-| CompanyName | VARCHAR | 60 | - | 否 | 收件人公司名称 |
-| Country | INT | - | - | 否 | 国家 |
-| 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 | - | - | 否 | 更新时间 |
-
-#### 订单申报信息表 `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 | - | - | 否 | 更新时间 |
-
-#### 订单件重尺表 `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_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 | - | 否 | 价格编号 |
-| PriceEffectiveTime | DATETIME | - | - | 否 | 价格编号生效时间 |
-| IsDeleted | TINYINT | - | - | 是 | 是否删除:0-否,1-是,默认0 |
-| CreateUserId | INT | - | - | 是 | 创建人 |
-| CreateTime | DATETIME | - | - | 是 | 创建时间 |
-| UpdateUserId | INT | - | - | 否 | 更新人 |
-| UpdateTime | DATETIME | - | - | 否 | 更新时间 |
-
-**Type 枚举**:receivable(应收)、payable(应付)
-
-### 1.3 核心接口设计
-
-#### 我的订单
-
-| 接口名称 | 请求方式 | 请求路径 | 说明 |
-| --- | --- | --- | --- |
-| 分页查询订单列表 | POST | /order/list | 支持 status、keyword、createTimeStart/End 筛选 |
-| 订单详情 | GET | /order/detail | 根据订单ID获取详情 |
-| 支付 | POST | /order/pay | 订单支付 |
-| 预报 | POST | /order/pre-declare | 订单预报 |
-| 拦截 | POST | /order/intercept | 订单拦截 |
-| 取消拦截 | POST | /order/cancel-intercept | 取消订单拦截 |
-| 删除 | POST | /order/delete | 删除订单 |
-| 打印 | POST | /order/print | 打印订单 |
-| 导出 | POST | /order/export | 导出订单 |
-
-##### 分页查询订单列表(POST /order/list)
-
-###### 1 接口请求示例
-
-**请求 /order/list**(POST,分页参数放 Body)
-
-```json
-{
- "page": 1, // 页码,从1开始
- "pageSize": 20, // 每页条数
- "status": "all|draft|predeclared|received|...", // 订单状态筛选
- "keyword": "", // 关键词(运单号/客户单号/跟踪号)
- "createTimeStart": "", // 创建时间起
- "createTimeEnd": "" // 创建时间止
-}
-```
-
-**响应**(用于 order/myOrders 列表及订单详情抽屉,字段与 parcel/create 对应,创建时传啥返回啥 + weightList)
-
-```json
-{
- "code": 0,
- "msg": "success",
- "data": {
- "items": [
- {
- "id": 1,
- "clientNo": "CUST...",
- "trackNo": "TRK...",
- "serviceNo": "LP...",
- "productCode": "US_AIR",
- "channelCode": "CH001",
- "cargoAttribute": "general",
- "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": 0,
- "province": "广东省",
- "city": "深圳市",
- "zipCode": "518000",
- "streetAddress1": "示例地址1",
- "streetAddress2": "",
- "streetAddress3": "",
- "houseNumber": ""
- },
- "receiver": {
- "recipientsName": "收件人1",
- "companyName": "",
- "phone": "13800138000",
- "telePhone": "",
- "email": "",
- "countryCode": 0,
- "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 接口业务逻辑
-
-####### 核心流程
-
-- 按接口定义执行业务流程,完成参数解析、数据处理与结果返回。
-
-####### 校验与异常
-
-- 对必填参数、状态与数据存在性进行校验,异常场景返回统一错误码与错误信息。
-
-##### 订单详情(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",
- "warehouseId": 1,
- "warehouse": "深圳仓",
- "customerId": 1,
- "sender": {},
- "receiver": {},
- "items": [],
- "weightList": []
- }
-}
-```
-
-> data 结构与 list 单条 item 一致,含 sender、receiver、items、weightList。
-
-###### 2 接口业务逻辑
-
-####### 核心流程
-
-- 按接口定义执行业务流程,完成参数解析、数据处理与结果返回。
-
-####### 校验与异常
-
-- 对必填参数、状态与数据存在性进行校验,异常场景返回统一错误码与错误信息。
-
-##### 支付(POST /order/pay)
-
-###### 1 接口请求示例
-
-**请求 /order/pay**(POST)
-
-```json
-{
- "id": 1,
- "requestId": ""
-}
-```
-
-> requestId 幂等,可选。
-
-**响应**
-
-```json
-{
- "code": 0,
- "msg": "success",
- "data": null
-}
-```
-
-###### 2 接口业务逻辑
-
-####### 核心流程
-
-- 按接口定义执行业务流程,完成参数解析、数据处理与结果返回。
-
-####### 校验与异常
-
-- 对必填参数、状态与数据存在性进行校验,异常场景返回统一错误码与错误信息。
-
-##### 预报(POST /order/pre-declare)
-
-###### 1 接口请求示例
-
-**请求 /order/pre-declare**(POST)
-
-```json
-{
- "id": 1,
- "requestId": ""
-}
-```
-
-**响应**
-
-```json
-{
- "code": 0,
- "msg": "success",
- "data": null
-}
-```
-
-###### 2 接口业务逻辑
-
-####### 核心流程
-
-- 按接口定义执行业务流程,完成参数解析、数据处理与结果返回。
-
-####### 校验与异常
-
-- 对必填参数、状态与数据存在性进行校验,异常场景返回统一错误码与错误信息。
-
-##### 拦截(POST /order/intercept)
-
-###### 1 接口请求示例
-
-**请求 /order/intercept**(POST)
-
-```json
-{
- "id": 1,
- "requestId": ""
-}
-```
-
-**响应**
-
-```json
-{
- "code": 0,
- "msg": "success",
- "data": null
-}
-```
-
-###### 2 接口业务逻辑
-
-####### 核心流程
-
-- 按接口定义执行业务流程,完成参数解析、数据处理与结果返回。
-
-####### 校验与异常
-
-- 对必填参数、状态与数据存在性进行校验,异常场景返回统一错误码与错误信息。
-
-##### 取消拦截(POST /order/cancel-intercept)
-
-###### 1 接口请求示例
-
-**请求 /order/cancel-intercept**(POST)
-
-```json
-{
- "id": 1,
- "requestId": ""
-}
-```
-
-**响应**
-
-```json
-{
- "code": 0,
- "msg": "success",
- "data": null
-}
-```
-
-###### 2 接口业务逻辑
-
-####### 核心流程
-
-- 按接口定义执行业务流程,完成参数解析、数据处理与结果返回。
-
-####### 校验与异常
-
-- 对必填参数、状态与数据存在性进行校验,异常场景返回统一错误码与错误信息。
-
-##### 删除(POST /order/delete)
-
-###### 1 接口请求示例
-
-**请求 /order/delete**(POST)
-
-```json
-{
- "id": 1,
- "requestId": ""
-}
-```
-
-**响应**
-
-```json
-{
- "code": 0,
- "msg": "success",
- "data": null
-}
-```
-
-###### 2 接口业务逻辑
-
-####### 核心流程
-
-- 按接口定义执行业务流程,完成参数解析、数据处理与结果返回。
-
-####### 校验与异常
-
-- 对必填参数、状态与数据存在性进行校验,异常场景返回统一错误码与错误信息。
-
-##### 打印(POST /order/print)
-
-###### 1 接口请求示例
-
-**请求 /order/print**(POST)
-
-```json
-{
- "id": 1,
- "requestId": ""
-}
-```
-
-**响应**
-
-```json
-{
- "code": 0,
- "msg": "success",
- "data": {
- "printUrl": ""
- }
-}
-```
-
-###### 2 接口业务逻辑
-
-####### 核心流程
-
-- 按接口定义执行业务流程,完成参数解析、数据处理与结果返回。
-
-####### 校验与异常
-
-- 对必填参数、状态与数据存在性进行校验,异常场景返回统一错误码与错误信息。
-
-##### 导出(POST /order/export)
-
-###### 1 接口请求示例
-
-**请求 /order/export**(POST)
-
-```json
-{
- "status": "all|draft|...",
- "keyword": "",
- "createTimeStart": "",
- "createTimeEnd": ""
-}
-```
-
-**响应**
-
-```json
-{
- "code": 0,
- "msg": "success",
- "data": {
- "downloadUrl": "",
- "expireAt": ""
- }
-}
-```
-
-###### 2 接口业务逻辑
-
-####### 核心流程
-
-- 按接口定义执行业务流程,完成参数解析、数据处理与结果返回。
-
-####### 校验与异常
-
-- 对必填参数、状态与数据存在性进行校验,异常场景返回统一错误码与错误信息。
-#### 我要查价
-
-| 接口名称 | 请求方式 | 请求路径 | 说明 |
-| --- | --- | --- | --- |
-| 查价 | POST | /order/price-check | 根据目的国、重量尺寸等查价 |
-
-##### 查价(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": "(62*2) +5+7",
- "freight": "62",
- "otherFees": "5",
- "totalCost": "136",
- "maxDeliveryDays": 7,
- "totalCostNum": 136
- }
- ]
- }
-}
-```
-
-###### 2 接口业务逻辑
-
-####### 核心流程
-
-- 按接口定义执行业务流程,完成参数解析、数据处理与结果返回。
-
-####### 校验与异常
-
-- 对必填参数、状态与数据存在性进行校验,异常场景返回统一错误码与错误信息。
-#### 我要下单
-
-| 接口名称 | 请求方式 | 请求路径 | 说明 |
-| --- | --- | --- | --- |
-| 模板列表 | GET | /order/template/list | 获取下单模板列表 |
-| 下载模板 | GET | /order/template/download | 下载 Excel 模板 |
-| 上传下单 | POST | /order/upload | multipart/form-data,file + templateId |
-| 任务进度列表 | GET | /order/task/list | 批量下单任务列表 |
-| 任务详情 | GET | /order/task/detail | 任务详情 |
-| 创建小包 | POST | /order/parcel/create | 创建小包订单 |
-| 批量创建小包 | POST | /order/parcel/batch-create | 批量创建小包订单 |
-
-##### 创建小包(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
-{
- "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-敏感货
- "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": 0, // 国家
- "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": 0, // 国家
- "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 接口业务逻辑
-
-####### 核心流程
-
-- 按接口定义执行业务流程,完成参数解析、数据处理与结果返回。
-
-####### 校验与异常
-
-- 对必填参数、状态与数据存在性进行校验,异常场景返回统一错误码与错误信息。
-
-##### 批量创建小包(POST /order/parcel/batch-create)
-
-###### 1 接口请求示例
-
-**请求 /order/parcel/batch-create**(POST)
-
-> 批量请求体为数组,数组内每一项与 `/order/parcel/create` 的单票请求结构完全一致。
-
-```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-敏感货
- "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": 0, // 国家
- "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": 0, // 国家
- "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",
-}
-```
-
-###### 2 接口业务逻辑
-
-####### 核心流程
-
-- 接收批量订单数据并逐条按单票创建规则执行,下发统一批次任务号并返回逐条处理结果。
-
-####### 校验与异常
-
-- 校验请求数组非空、单次数量上限与单条必填字段;部分失败时返回明细错误信息。
-
-##### 模板列表(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 接口业务逻辑
-
-####### 核心流程
-
-- 按接口定义执行业务流程,完成参数解析、数据处理与结果返回。
-
-####### 校验与异常
-
-- 对必填参数、状态与数据存在性进行校验,异常场景返回统一错误码与错误信息。
-
-##### 下载模板(GET /order/template/download)
-
-###### 1 接口请求示例
-
-**请求 /order/template/download**(GET,Query 参数)
-
-```
-?templateId=1
-```
-
-**响应**
-
-> 直接返回 Excel 文件流,Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet
-
-###### 2 接口业务逻辑
-
-####### 核心流程
-
-- 按接口定义执行业务流程,完成参数解析、数据处理与结果返回。
-
-####### 校验与异常
-
-- 对必填参数、状态与数据存在性进行校验,异常场景返回统一错误码与错误信息。
-
-##### 上传下单(POST /order/upload)
-
-###### 1 接口请求示例
-
-**请求 /order/upload**(POST,multipart/form-data)
-
-| 字段 | 类型 | 说明 |
-| --- | --- | --- |
-| file | File | Excel 文件 |
-| templateId | number | 模板ID |
-
-**响应**
-
-```json
-{
- "code": 0,
- "msg": "success",
- "data": {
- "taskId": 1
- }
-}
-```
-
-###### 2 接口业务逻辑
-
-####### 核心流程
-
-- 按接口定义执行业务流程,完成参数解析、数据处理与结果返回。
-
-####### 校验与异常
-
-- 对必填参数、状态与数据存在性进行校验,异常场景返回统一错误码与错误信息。
-
-##### 任务进度列表(GET /order/task/list)
-
-###### 1 接口请求示例
-
-**请求 /order/task/list**(GET,Query 参数)
-
-```
-?page=1&pageSize=20&status=&keyword=
-```
-
-**响应**
-
-```json
-{
- "code": 0,
- "msg": "success",
- "data": {
- "items": [
- {
- "id": 1,
- "taskName": "批量下单_20240101",
- "quantity": 100,
- "status": "处理中",
- "templateId": 1,
- "createTime": "2024-01-01 12:00:00",
- "updateTime": "2024-01-01 12:00:00"
- }
- ],
- "total": 10
- }
-}
-```
-
-###### 2 接口业务逻辑
-
-####### 核心流程
-
-- 按接口定义执行业务流程,完成参数解析、数据处理与结果返回。
-
-####### 校验与异常
-
-- 对必填参数、状态与数据存在性进行校验,异常场景返回统一错误码与错误信息。
-
-##### 任务详情(GET /order/task/detail)
-
-###### 1 接口请求示例
-
-**请求 /order/task/detail**(GET,Query 参数)
-
-```
-?taskId=1
-```
-
-**响应**
-
-```json
-{
- "code": 0,
- "msg": "success",
- "data": {
- "id": 1,
- "taskName": "批量下单_20240101",
- "quantity": 100,
- "successCount": 80,
- "failCount": 20,
- "status": "已完成",
- "templateId": 1,
- "createTime": "2024-01-01 12:00:00",
- "updateTime": "2024-01-01 12:00:00",
- "failList": []
- }
-}
-```
-
-###### 2 接口业务逻辑
-
-####### 核心流程
-
-- 按接口定义执行业务流程,完成参数解析、数据处理与结果返回。
-
-####### 校验与异常
-
-- 对必填参数、状态与数据存在性进行校验,异常场景返回统一错误码与错误信息。
-## 三、渠道模块
-
-### 2.1 核心需求说明
-
-- **销售产品**:管理对外销售的产品(专线/快递/邮政),配置时效、黑名单区、偏远分区、价格分区、计重规则、计重进位等;支持价格配置。
- - **产品配置**:产品基础信息(编码、名称、分组、时效、面单、黑名单区、偏远分区、价格分区、计重规则、计重进位等)。
- - **产品卖价**:为每个产品配置多条价格项(运费、保险费、燃油附加费等);每条价格项通过弹窗配置:
- - **基础信息**:费用项、费用单位、币种、收费时机、收费条件、计费因素(因素+因素值,如货物类型、分区、重量、仓库、客户等)。
- - **价格表**:根据计费因素动态生成价格表,支持分区/货物类型/重量等组合、计算公式(总价/单价/续单价)、计算值、客户组。
- - **排货规则**:选择排货方案(成本最低/时效最快/综合最优),配置服务商列表(服务商名称、服务渠道、权重、指定客户)。
- - **发布记录**:价格与规则的发布历史,支持查看版本内容、回滚到指定版本。
-- **服务渠道**:管理下游服务渠道(支持渠道组+渠道层级),配置预报单号类型、时效、买价等。
- - **渠道配置**:渠道基础信息(渠道代码、名称、时效、预报配置、描述、预报前脚本等)。
- - **渠道买价**:为每个渠道配置多条买价项(运费、保险费、燃油附加费等);每条买价项通过弹窗配置:
- - **基础信息**:费用项、费用单位、币种、收费时机、收费条件、计费因素(因素+因素值)。
- - **价格表**:根据计费因素动态生成价格表,支持分区/货物类型/重量等组合、计算公式、计算值、客户组。
- - **发布记录**:买价与规则的发布历史,支持查看版本内容、回滚到指定版本。
-- **黑名单区/偏远分区/价格分区**:共用 KpiArea 表,仅通过 `AreaType` 区分;支持分区及明细(国家/省/市/邮编区间)的增删改查。
-- **计重规则/计重进位**:配置计费重量规则与进位方式,明细结构与分区类似。
-- **自动触发**:配置下单/入库/出库时的自动拦截、警告、问题件等触发规则。
-- **标签设计**:保存 vg-print 导出的标签模板 JSON,支持尾程/客户/入库/出库/sku 等类型。
-
-### 2.2 表结构设计
-
-#### 销售产品表 `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 | - | - | 否 | 更新时间 |
-
-#### 产品卖价表 `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 |
-| ChargingTime | VARCHAR | 32 | - | 否 | 收费时机:预报/出库/签收 |
-| ChargingCondition | VARCHAR | 64 | - | 否 | 收费条件 |
-| IsDeleted | TINYINT | - | - | 是 | 是否删除:0-否,1-是,默认0 |
-| CreateUserId | INT | - | - | 是 | 创建人 |
-| CreateTime | DATETIME | - | - | 是 | 创建时间 |
-| UpdateUserId | INT | - | - | 否 | 更新人 |
-| UpdateTime | DATETIME | - | - | 否 | 更新时间 |
-
-#### 产品卖价-计费因素表 `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 | - | - | 否 | 更新时间 |
-
-#### 产品卖价-价格表明细表 `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 | - | - | 否 | 更新时间 |
-
-
-#### 排货规则-服务商表 `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数组,空表示全部 |
-| IsDeleted | TINYINT | - | - | 是 | 是否删除:0-否,1-是,默认0 |
-| CreateUserId | INT | - | - | 是 | 创建人 |
-| CreateTime | DATETIME | - | - | 是 | 创建时间 |
-| UpdateUserId | INT | - | - | 否 | 更新人 |
-| UpdateTime | DATETIME | - | - | 否 | 更新时间 |
-
-**Scheme 枚举**:costLowest(成本最低)、timeFastest(时效最快)、overallBest(综合最优)
-
-#### 发布记录表 `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 | - | - | 否 | 更新时间 |
-
-#### 服务渠道组表 `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 | - | - | 否 | 更新时间 |
-
-#### 服务渠道表 `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 | - | - | 否 | 更新时间 |
-
-#### 渠道买价表 `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 |
-| ChargingTime | VARCHAR | 32 | - | 否 | 收费时机:预报/出库/签收 |
-| ChargingCondition | VARCHAR | 64 | - | 否 | 收费条件 |
-| IsDeleted | TINYINT | - | - | 是 | 是否删除:0-否,1-是,默认0 |
-| CreateUserId | INT | - | - | 是 | 创建人 |
-| CreateTime | DATETIME | - | - | 是 | 创建时间 |
-| UpdateUserId | INT | - | - | 否 | 更新人 |
-| UpdateTime | DATETIME | - | - | 否 | 更新时间 |
-
-#### 渠道买价-计费因素表 `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 | - | - | 否 | 更新时间 |
-
-#### 渠道买价-价格表明细表 `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 | - | - | 否 | 更新时间 |
-
-#### 服务渠道发布记录表 `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 | - | - | 否 | 更新时间 |
-
-#### 分区表(黑名单区/偏远分区/价格分区共用)`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(价格分区)
-
-#### 分区明细表 `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 | - | - | 否 | 更新时间 |
-
-#### 计重规则表 `t_weight_rule`
-
-| 字段名 | 字段类型 | 长度 | 主键/外键 | 非空 | 说明 |
-| --- | --- | --- | --- | --- | --- |
-| Id | BIGINT | - | 主键 | 是 | 自增唯一标识 |
-| Name | VARCHAR | 128 | - | 是 | 规则名称 |
-| IsDeleted | TINYINT | - | - | 是 | 是否删除:0-否,1-是,默认0 |
-| CreateUserId | INT | - | - | 是 | 创建人 |
-| CreateTime | DATETIME | - | - | 是 | 创建时间 |
-| UpdateUserId | INT | - | - | 否 | 更新人 |
-| UpdateTime | DATETIME | - | - | 否 | 更新时间 |
-
-#### 计重规则明细表 `t_weight_rule_info`(子表,关联 t_weight_rule)
-
-> 计重规则子页面,配置客户组对应的计费规则(条件表达式、计费重取值、材积系数)。
-
-| 字段名 | 字段类型 | 长度 | 主键/外键 | 非空 | 说明 |
-| --- | --- | --- | --- | --- | --- |
-| Id | BIGINT | - | 主键 | 是 | 自增唯一标识 |
-| RuleId | BIGINT | - | 外键 | 是 | 计重规则ID |
-| Client | VARCHAR | 128 | - | 是 | 客户/客户组(如:公共、客户组1、客户组2) |
-| 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 | - | - | 否 | 更新时间 |
-
-#### 计重进位表 `t_weight_rounding`
-
-| 字段名 | 字段类型 | 长度 | 主键/外键 | 非空 | 说明 |
-| --- | --- | --- | --- | --- | --- |
-| Id | BIGINT | - | 主键 | 是 | 自增唯一标识 |
-| Name | VARCHAR | 128 | - | 是 | 进位名称 |
-| IsDeleted | TINYINT | - | - | 是 | 是否删除:0-否,1-是,默认0 |
-| CreateUserId | INT | - | - | 是 | 创建人 |
-| CreateTime | DATETIME | - | - | 是 | 创建时间 |
-| UpdateUserId | INT | - | - | 否 | 更新人 |
-| UpdateTime | DATETIME | - | - | 否 | 更新时间 |
-
-#### 计重进位明细表 `t_weight_rounding_info`(子表,关联 t_weight_rounding)
-
-> 计重进位子页面,配置重量区间的进位值(起始值、结束值、公共/客户组进位值)。
-
-| 字段名 | 字段类型 | 长度 | 主键/外键 | 非空 | 说明 |
-| --- | --- | --- | --- | --- | --- |
-| Id | BIGINT | - | 主键 | 是 | 自增唯一标识 |
-| RoundingId | BIGINT | - | 外键 | 是 | 计重进位ID |
-| StartValue | DECIMAL | 18,4 | - | 是 | 起始值(kg) |
-| EndValue | DECIMAL | 18,4 | - | 是 | 结束值(kg) |
-| Public | VARCHAR | 32 | - | 否 | 公共进位值 |
-| CustomerGroup1 | VARCHAR | 32 | - | 否 | 客户组1进位值 |
-| CustomerGroup2 | VARCHAR | 32 | - | 否 | 客户组2进位值 |
-| IsDeleted | TINYINT | - | - | 是 | 是否删除:0-否,1-是,默认0 |
-| CreateUserId | INT | - | - | 是 | 创建人 |
-| CreateTime | DATETIME | - | - | 是 | 创建时间 |
-| UpdateUserId | INT | - | - | 否 | 更新人 |
-| UpdateTime | DATETIME | - | - | 否 | 更新时间 |
-
-#### 自动触发表 `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 | - | - | 否 | 更新时间 |
-
-#### 标签模板表 `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 | - | - | 否 | 更新时间 |
-
-### 2.3 核心接口设计
-
-#### 销售产品
-
-| 接口名称 | 请求方式 | 请求路径 | 说明 |
-| --- | --- | --- | --- |
-| 产品列表 | GET | /channel/sales-product/list | 产品列表 |
-| 产品详情 | GET | /channel/sales-product/detail | 产品详情 |
-| 新增产品 | POST | /channel/sales-product/create | 新增产品 |
-| 更新产品 | POST | /channel/sales-product/update | 更新产品(包含产品配置 tab 保存) |
-| 删除产品 | POST | /channel/sales-product/delete | 删除产品 |
-
-##### 产品列表(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`。
-
-##### 产品详情(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` 查询产品主数据及扩展配置,组装详情对象返回。
-####### 校验与异常
-- 若产品不存在或不可访问,返回业务错误码。
-
-##### 新增产品(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 接口业务逻辑
-
-####### 核心流程
-
-- 校验产品编码唯一性与必填字段后新增产品记录。
-####### 校验与异常
-- 初始化产品配置与默认状态,写入审计字段。
-
-##### 更新产品(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 的保存内容)。
-####### 校验与异常
-- 记录操作人和更新时间,并触发配置缓存刷新。
-
-##### 删除产品(POST /channel/sales-product/delete)
-
-###### 1 接口请求示例
-
-**请求 /channel/sales-product/delete**(POST)
-
-```json
-{
- "id": 1
-}
-```
-
-**响应**
-
-```json
-{
- "code": 0,
- "msg": "success",
- "data": null
-}
-```
-
-
-###### 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 | 批量保存产品卖价(保存并可选发布) |
-
-##### 价格项列表(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",
- "chargingTime": "出库",
- "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` 必填且产品存在;不存在时返回业务错误。
-- 仅返回未删除记录,查询异常返回统一错误码。
-
-##### 新增价格项(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",
- "chargingTime": "出库",
- "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` 不冲突;冲突时返回业务错误。
-
-##### 更新价格项(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",
- "chargingTime": "出库",
- "chargingCondition": "",
- "factors": [],
- "details": []
-}
-```
-
-**响应**
-
-```json
-{
- "code": 0,
- "msg": "success",
- "data": null
-}
-```
-
-###### 2 接口业务逻辑
-
-####### 核心流程
-
-- 按 `id` 更新价格项主数据,并按最新请求重建/同步因素与明细子数据。
-- 保留版本追溯能力,更新后可参与后续发布流程。
-
-####### 校验与异常
-
-- 校验 `id` 存在且归属 `salesProductId`。
-- 校验更新后配置合法(计算公式、阶梯区间、客户组取值等),不合法时返回业务错误。
-
-##### 删除价格项(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` 存在且允许删除(如未处于不可删状态)。
-- 删除失败返回统一业务错误码。
-
-##### 切换价格项状态(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` 存在,不存在时返回业务错误。
-
-##### 保存价格配置(POST /channel/sales-product/price/save)
-
-###### 1 接口请求示例
-
-**请求 /channel/sales-product/price/save**(POST)
-
-```json
-{
- "salesProductId": 1,
- "publish": false,
- "prices": [
- {
- "id": 1,
- "serialNumber": 1,
- "status": 1,
- "priceName": "运费",
- "costItem": "freight",
- "costUnit": "kg",
- "currency": "RMB",
- "chargingTime": "出库",
- "chargingCondition": "",
- "factors": [],
- "details": []
- }
- ]
-}
-```
-
-**响应**
-
-```json
-{
- "code": 0,
- "msg": "success",
- "data": null
-}
-```
-
-###### 2 接口业务逻辑
-
-####### 核心流程
-
-- 批量保存 `prices`,按是否存在 `id` 自动分流新增/更新,并同步子项数据。
-- 当 `publish=true` 时,保存完成后触发发布流程并生成版本记录。
-
-####### 校验与异常
-
-- 校验 `salesProductId`、`prices` 必填且数据结构合法。
-- 批量保存过程中任一子项失败则整体回滚,返回统一错误信息。
-#### 排货规则(子表接口)
-
-| 接口名称 | 请求方式 | 请求路径 | 说明 |
-| --- | --- | --- | --- |
-| 获取排货规则 | GET | /channel/sales-product/allocation/get | 按产品ID获取排货方案及服务商列表 |
-| 保存排货规则 | POST | /channel/sales-product/allocation/save | 保存排货方案及服务商(保存并可选发布) |
-
-##### 获取排货规则(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` 必填且产品存在;不存在时返回业务错误。
-
-##### 保存排货规则(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` 合法性。
-- 保存失败回滚事务并返回统一错误信息。
-
-#### 发布记录(子表接口)
-
-| 接口名称 | 请求方式 | 请求路径 | 说明 |
-| --- | --- | --- | --- |
-| 价格发布记录列表 | GET | /channel/sales-product/publish/price/list | 按产品ID获取价格发布历史 |
-| 规则发布记录列表 | GET | /channel/sales-product/publish/rules/list | 按产品ID获取规则发布历史 |
-| 发布 | POST | /channel/sales-product/publish | 发布当前配置(价格或规则) |
-| 回滚到版本 | POST | /channel/sales-product/publish/rollback | 回滚到指定版本 |
-
-##### 价格发布记录列表(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` 与分页参数合法性,异常时返回统一错误码。
-
-##### 规则发布记录列表(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` 与分页参数合法性,异常时返回统一错误码。
-
-##### 发布(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` 必填且可发布。
-
-##### 回滚到版本(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` 版本存在且与产品、类型一致,不一致时返回业务错误。
-
-#### 服务渠道
-
-| 接口名称 | 请求方式 | 请求路径 | 说明 |
-| --- | --- | --- | --- |
-| 渠道列表 | GET | /channel/service-channel/list | 渠道列表(含渠道组层级) |
-| 渠道详情 | GET | /channel/service-channel/detail | 渠道详情 |
-| 新增渠道 | POST | /channel/service-channel/create | 新增渠道 |
-| 更新渠道 | POST | /channel/service-channel/update | 更新渠道(包含渠道配置 tab 保存) |
-| 删除渠道 | POST | /channel/service-channel/delete | 删除渠道 |
-| 新增服务商 | 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 | 删除服务商分组(若分组下存在渠道需禁止删除) |
-
-##### 渠道列表(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 接口业务逻辑
-
-####### 核心流程
-
-- 按分页与筛选条件查询渠道列表,返回渠道及所属分组信息。
-
-####### 校验与异常
-
-- 校验分页参数合法性,异常时返回统一错误码。
-
-##### 渠道详情(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` 必填且存在,不存在时返回业务错误。
-
-##### 新增渠道(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` 等必填字段,编码冲突时返回业务错误。
-
-##### 新增服务商(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` 必填且有效。
-
-##### 编辑服务商(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` 存在且可编辑,不存在时返回业务错误。
-
-##### 删除服务商(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` 执行逻辑删除。
-
-####### 校验与异常
-
-- 若分组下存在渠道则禁止删除并返回业务错误。
-
-##### 更新渠道(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` 存在及字段合法性,更新失败返回业务错误。
-
-##### 删除渠道(POST /channel/service-channel/delete)
-
-###### 1 接口请求示例
-
-**请求 /channel/service-channel/delete**(POST)
-
-```json
-{
- "id": 1
-}
-```
-
-**响应**
-
-```json
-{
- "code": 0,
- "msg": "success",
- "data": null
-}
-```
-
-###### 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 | 批量保存渠道买价(保存并可选发布) |
-
-##### 买价项列表(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",
- "chargingTime": "出库",
- "chargingCondition": "",
- "factors": [],
- "details": [],
- "createTime": "2024-01-01 12:00:00",
- "updateTime": "2024-01-01 12:00:00"
- }
- ]
- }
-}
-```
-
-###### 2 接口业务逻辑
-
-####### 核心流程
-
-- 按 `serviceChannelId` 查询买价项及其因素、明细子项。
-
-####### 校验与异常
-
-- 校验 `serviceChannelId` 必填且渠道存在;异常返回统一错误码。
-
-##### 新增买价项(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",
- "chargingTime": "出库",
- "chargingCondition": "",
- "factors": [
- {
- "serialNumber": 1,
- "factor": "货物类型",
- "factorValues": ["普货", "敏感货"]
- }
- ],
- "details": [
- {
- "factorValuesJson": {"分区": "欧洲", "货物类型": "普货", "重量": "0-0.5"},
- "calculationFormula": "单价",
- "calculatedValue": 45,
- "customerGroupValues": {}
- }
- ]
-}
-```
-
-**响应**
-
-```json
-{
- "code": 0,
- "msg": "success",
- "data": null
-}
-```
-
-###### 2 接口业务逻辑
-
-####### 核心流程
-
-- 新增买价主记录并写入因素、明细子表。
-
-####### 校验与异常
-
-- 校验必填参数与序号冲突规则,校验失败返回业务错误。
-
-##### 更新买价项(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",
- "chargingTime": "出库",
- "chargingCondition": "",
- "factors": [],
- "details": []
-}
-```
-
-**响应**
-
-```json
-{
- "code": 0,
- "msg": "success",
- "data": null
-}
-```
-
-###### 2 接口业务逻辑
-
-####### 核心流程
-
-- 按 `id` 更新买价主数据并同步子项明细。
-
-####### 校验与异常
-
-- 校验 `id` 存在且归属渠道一致,校验失败返回业务错误。
-
-##### 删除买价项(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` 存在且允许删除。
-
-##### 切换买价项状态(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` 枚举与 `id` 存在性,不合法时返回业务错误。
-
-##### 保存买价配置(POST /channel/service-channel/price/save)
-
-###### 1 接口请求示例
-
-**请求 /channel/service-channel/price/save**(POST)
-
-```json
-{
- "serviceChannelId": 1,
- "publish": false,
- "prices": [
- {
- "id": 1,
- "serialNumber": 1,
- "status": 1,
- "priceName": "运费",
- "costItem": "freight",
- "costUnit": "kg",
- "currency": "RMB",
- "chargingTime": "出库",
- "chargingCondition": "",
- "factors": [],
- "details": []
- }
- ]
-}
-```
-
-**响应**
-
-```json
-{
- "code": 0,
- "msg": "success",
- "data": null
-}
-```
-
-###### 2 接口业务逻辑
-
-####### 核心流程
-
-- 批量保存买价配置,按 `id` 区分新增/更新;`publish=true` 时触发发布。
-
-####### 校验与异常
-
-- 校验 `serviceChannelId`、`prices` 合法;任一子项失败则事务回滚。
-#### 服务渠道发布记录(子表接口)
-
-| 接口名称 | 请求方式 | 请求路径 | 说明 |
-| --- | --- | --- | --- |
-| 价格发布记录列表 | GET | /channel/service-channel/publish/price/list | 按渠道ID获取买价发布历史 |
-| 规则发布记录列表 | GET | /channel/service-channel/publish/rules/list | 按渠道ID获取规则发布历史 |
-| 发布 | POST | /channel/service-channel/publish | 发布当前配置(买价或规则) |
-| 回滚到版本 | POST | /channel/service-channel/publish/rollback | 回滚到指定版本 |
-
-##### 价格发布记录列表(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` 与分页参数合法性。
-
-##### 规则发布记录列表(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` 与分页参数合法性。
-
-##### 发布(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` 必填且可发布。
-
-##### 回滚到版本(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` 与渠道、类型匹配关系,异常时返回业务错误。
-#### 黑名单区 / 偏远分区 / 价格分区(共用 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 | 分区明细列表 |
-
-##### 创建分区(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` 必填且合法。
-
-##### 修改分区(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` 匹配。
-
-##### 删除分区(POST /channel/kpi-area/delete)
-
-###### 1 接口请求示例
-
-**请求 /channel/kpi-area/delete**(POST)
-
-```json
-{
- "id": 1
-}
-```
-
-**响应**
-
-```json
-{
- "code": 0,
- "msg": "success",
- "data": null
-}
-```
-
-###### 2 接口业务逻辑
-
-####### 核心流程
-
-- 根据 `id` 执行分区逻辑删除。
-
-####### 校验与异常
-
-- 分区存在明细时按业务规则拦截或级联处理。
-
-##### 分区列表(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` 合法性。
-
-##### 新增分区明细(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` 存在且区间数据合法。
-
-##### 修改分区明细(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` 存在,更新内容合法。
-
-##### 删除分区明细(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` 存在,不存在时返回业务错误。
-
-##### 分区明细列表(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` 与分页参数合法性。
-#### 计重规则 / 计重进位
-
-| 接口名称 | 请求方式 | 请求路径 | 说明 |
-| --- | --- | --- | --- |
-| 规则列表 | 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 | 删除进位明细 |
-
-##### 规则列表(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 接口业务逻辑
-####### 核心流程
-- 分页查询计重规则主表。
-####### 校验与异常
-- 校验分页参数合法性。
-
-##### 创建规则(POST /channel/weight-rule/create)
-###### 1 接口请求示例
-**请求 /channel/weight-rule/create**(POST)
-
-```json
-{ "name": "标准计重规则" }
-```
-
-**响应**
-
-```json
-{ "code": 0, "msg": "success", "data": null }
-```
-
-###### 2 接口业务逻辑
-####### 核心流程
-- 新增计重规则并写入审计信息。
-####### 校验与异常
-- 校验 `name` 必填且不重复。
-
-##### 更新规则(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` 存在。
-
-##### 删除规则(POST /channel/weight-rule/delete)
-###### 1 接口请求示例
-**请求 /channel/weight-rule/delete**(POST)
-
-```json
-{ "id": 1 }
-```
-
-**响应**
-
-```json
-{ "code": 0, "msg": "success", "data": null }
-```
-
-###### 2 接口业务逻辑
-####### 核心流程
-- 对规则执行逻辑删除。
-####### 校验与异常
-- 校验是否存在关联明细。
-
-##### 规则明细列表(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, "client": "公共" }], "total": 3 }
-}
-```
-
-###### 2 接口业务逻辑
-####### 核心流程
-- 按 `ruleId` 分页查询规则明细。
-####### 校验与异常
-- 校验 `ruleId` 必填且有效。
-
-##### 新增明细(POST /channel/weight-rule/info/add)
-###### 1 接口请求示例
-**请求 /channel/weight-rule/info/add**(POST)
-
-```json
-{ "ruleId": 1, "client": "公共", "condition": "gw>vw", "billableWeight": "vw", "volumetricFactor": "6000" }
-```
-
-**响应**
-
-```json
-{ "code": 0, "msg": "success", "data": null }
-```
-
-###### 2 接口业务逻辑
-####### 核心流程
-- 新增规则明细。
-####### 校验与异常
-- 校验 `ruleId` 与字段合法性。
-
-##### 更新明细(POST /channel/weight-rule/info/update)
-###### 1 接口请求示例
-**请求 /channel/weight-rule/info/update**(POST)
-
-```json
-{ "id": 1, "ruleId": 1, "client": "公共", "condition": "gw>vw", "billableWeight": "vw", "volumetricFactor": "6000" }
-```
-
-**响应**
-
-```json
-{ "code": 0, "msg": "success", "data": null }
-```
-
-###### 2 接口业务逻辑
-####### 核心流程
-- 根据 `id` 更新规则明细。
-####### 校验与异常
-- 校验 `id` 存在且归属正确。
-
-##### 删除明细(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` 存在。
-
-##### 进位列表(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 接口业务逻辑
-####### 核心流程
-- 分页查询计重进位主表。
-####### 校验与异常
-- 校验分页参数。
-
-##### 创建进位(POST /channel/weight-rounding/create)
-###### 1 接口请求示例
-**请求 /channel/weight-rounding/create**(POST)
-
-```json
-{ "name": "标准进位" }
-```
-
-**响应**
-
-```json
-{ "code": 0, "msg": "success", "data": null }
-```
-
-###### 2 接口业务逻辑
-####### 核心流程
-- 新增进位规则。
-####### 校验与异常
-- 校验 `name` 必填。
-
-##### 更新进位(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` 存在。
-
-##### 删除进位(POST /channel/weight-rounding/delete)
-###### 1 接口请求示例
-**请求 /channel/weight-rounding/delete**(POST)
-
-```json
-{ "id": 1 }
-```
-
-**响应**
-
-```json
-{ "code": 0, "msg": "success", "data": null }
-```
-
-###### 2 接口业务逻辑
-####### 核心流程
-- 逻辑删除进位规则。
-####### 校验与异常
-- 校验是否可删除。
-
-##### 进位明细列表(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 }], "total": 5 }
-}
-```
-
-###### 2 接口业务逻辑
-####### 核心流程
-- 按 `roundingId` 分页查询进位明细。
-####### 校验与异常
-- 校验 `roundingId` 存在。
-
-##### 新增进位明细(POST /channel/weight-rounding/info/add)
-###### 1 接口请求示例
-**请求 /channel/weight-rounding/info/add**(POST)
-
-```json
-{ "roundingId": 1, "startValue": 0, "endValue": 1, "public": "0.5", "customerGroup1": "0.5", "customerGroup2": "0.25" }
-```
-
-**响应**
-
-```json
-{ "code": 0, "msg": "success", "data": null }
-```
-
-###### 2 接口业务逻辑
-####### 核心流程
-- 新增进位明细。
-####### 校验与异常
-- 校验区间与分组字段合法性。
-
-##### 更新进位明细(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", "customerGroup1": "0.5", "customerGroup2": "0.25" }
-```
-
-**响应**
-
-```json
-{ "code": 0, "msg": "success", "data": null }
-```
-
-###### 2 接口业务逻辑
-####### 核心流程
-- 根据 `id` 更新进位明细。
-####### 校验与异常
-- 校验 `id` 存在且归属正确。
-
-##### 删除进位明细(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` 存在。
-
-#### 自动触发
-
-| 接口名称 | 请求方式 | 请求路径 | 说明 |
-| --- | --- | --- | --- |
-| 列表 | 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 | 切换启用状态 |
-
-##### 列表(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 接口业务逻辑
-####### 核心流程
-- 分页查询自动触发规则列表。
-####### 校验与异常
-- 校验分页与筛选参数合法。
-
-##### 新增(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 接口业务逻辑
-####### 核心流程
-- 新增自动触发规则。
-####### 校验与异常
-- 校验关键字段必填。
-
-##### 更新(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` 存在且可编辑。
-
-##### 删除(POST /channel/auto-trigger/delete)
-###### 1 接口请求示例
-**请求 /channel/auto-trigger/delete**(POST)
-
-```json
-{ "id": 1 }
-```
-
-**响应**
-
-```json
-{ "code": 0, "msg": "success", "data": null }
-```
-
-###### 2 接口业务逻辑
-####### 核心流程
-- 逻辑删除自动触发规则。
-####### 校验与异常
-- 校验 `id` 存在且可删除。
-
-##### 切换启用状态(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` 枚举合法性。
-#### 标签设计
-
-| 接口名称 | 请求方式 | 请求路径 | 说明 |
-| --- | --- | --- | --- |
-| 保存标签模板 | 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.1 核心需求说明
-
-- **入库扫描**:扫描运单号完成入库,记录实重、尺寸、销售产品、目的国、仓库、操作人、操作时间;支持查看最近入库记录。
-- **出库扫描**:扫描运单号完成出库,记录仓库、操作人、操作时间;支持查看最近出库记录。
-- **标签打印**:按运单号/客户单号/跟踪号/其他单号选择标签模板打印;记录打印结果与操作人。
-
-### 3.2 表结构设计
-
-#### 入库记录表 `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 | - | - | 否 | 更新时间 |
-
-#### 出库记录表 `t_outbound_record`
-
-| 字段名 | 字段类型 | 长度 | 主键/外键 | 非空 | 说明 |
-| --- | --- | --- | --- | --- | --- |
-| Id | BIGINT | - | 主键 | 是 | 自增唯一标识 |
-| WaybillNo | VARCHAR | 64 | - | 是 | 运单号 |
-| WarehouseId | BIGINT | - | 外键 | 否 | 仓库ID |
-| OperateTime | DATETIME | - | - | 是 | 操作时间 |
-| OperatorId | BIGINT | - | 外键 | 否 | 操作人ID |
-| IsDeleted | TINYINT | - | - | 是 | 是否删除:0-否,1-是,默认0 |
-| CreateUserId | INT | - | - | 是 | 创建人 |
-| CreateTime | DATETIME | - | - | 是 | 创建时间 |
-| UpdateUserId | INT | - | - | 否 | 更新人 |
-| UpdateTime | DATETIME | - | - | 否 | 更新时间 |
-
-#### 标签打印记录表 `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 | - | - | 否 | 更新时间 |
-
-### 3.3 核心接口设计
-
-#### 入库扫描
-
-| 接口名称 | 请求方式 | 请求路径 | 说明 |
-| --- | --- | --- | --- |
-| 入库 | POST | /operation/inbound/create | 入库 |
-| 最近入库记录 | GET | /operation/inbound/recent-list | 最近入库记录 |
-
-##### 入库(POST /operation/inbound/create)
-
-###### 1 接口请求示例
-
-**请求 /operation/inbound/create**(POST)
-
-```json
-{
- "waybillNo": "LP1234567890",
- "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": "LP1234567890",
- "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 接口业务逻辑
-
-####### 核心流程
-
-- 按接口定义执行业务流程,完成参数解析、数据处理与结果返回。
-
-####### 校验与异常
-
-- 对必填参数、状态与数据存在性进行校验,异常场景返回统一错误码与错误信息。
-
-##### 最近入库记录(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": "LP1234567890",
- "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 接口业务逻辑
-
-####### 核心流程
-
-- 按接口定义执行业务流程,完成参数解析、数据处理与结果返回。
-
-####### 校验与异常
-
-- 对必填参数、状态与数据存在性进行校验,异常场景返回统一错误码与错误信息。
-#### 出库扫描
-
-| 接口名称 | 请求方式 | 请求路径 | 说明 |
-| --- | --- | --- | --- |
-| 出库 | POST | /operation/outbound/create | 出库 |
-| 最近出库记录 | GET | /operation/outbound/recent-list | 最近出库记录 |
-
-##### 出库(POST /operation/outbound/create)
-
-###### 1 接口请求示例
-
-**请求 /operation/outbound/create**(POST)
-
-```json
-{
- "waybillNo": "LP1234567890",
- "warehouseId": 1
-}
-```
-
-> 参数说明:`waybillNo` 运单号;`warehouseId` 仓库ID(可选,不传则从订单或默认仓库获取)。操作时间、操作人由服务端从当前会话获取。
-
-**响应**
-
-```json
-{
- "code": 0,
- "msg": "success",
- "data": {
- "id": 1,
- "waybillNo": "LP1234567890",
- "warehouseId": 1,
- "operateTime": "2024-01-01 12:00:00",
- "operatorId": 1,
- "createTime": "2024-01-01 12:00:00"
- }
-}
-```
-
-###### 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": "LP1234567890",
- "warehouseId": 1,
- "warehouseName": "深圳仓",
- "operateTime": "2024-01-01 12:00:00",
- "operatorId": 1,
- "operatorName": "张三",
- "createTime": "2024-01-01 12:00:00"
- }
- ]
- }
-}
-```
-
-###### 2 接口业务逻辑
-
-####### 核心流程
-
-- 按接口定义执行业务流程,完成参数解析、数据处理与结果返回。
-
-####### 校验与异常
-
-- 对必填参数、状态与数据存在性进行校验,异常场景返回统一错误码与错误信息。
-#### 标签打印
-
-| 接口名称 | 请求方式 | 请求路径 | 说明 |
-| --- | --- | --- | --- |
-| 打印标签 | POST | /operation/print-label/print | 打印标签 |
-| 最近打印记录 | GET | /operation/print-label/recent-list | 最近打印记录 |
-| 标签模板列表 | GET | /operation/print-label/template-list | 标签模板列表 |
-
-##### 打印标签(POST /operation/print-label/print)
-
-###### 1 接口请求示例
-
-**请求 /operation/print-label/print**(POST)
-
-```json
-{
- "number": "LP1234567890",
- "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": "LP1234567890",
- "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 接口业务逻辑
-
-####### 核心流程
-
-- 按接口定义执行业务流程,完成参数解析、数据处理与结果返回。
-
-####### 校验与异常
-
-- 对必填参数、状态与数据存在性进行校验,异常场景返回统一错误码与错误信息。
-
-##### 最近打印记录(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": "LP1234567890",
- "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 接口业务逻辑
-
-####### 核心流程
-
-- 按接口定义执行业务流程,完成参数解析、数据处理与结果返回。
-
-####### 校验与异常
-
-- 对必填参数、状态与数据存在性进行校验,异常场景返回统一错误码与错误信息。
-
-##### 标签模板列表(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 接口业务逻辑
-
-####### 核心流程
-
-- 按接口定义执行业务流程,完成参数解析、数据处理与结果返回。
-
-####### 校验与异常
-
-- 对必填参数、状态与数据存在性进行校验,异常场景返回统一错误码与错误信息。
-## 五、客户模块
-
-### 4.1 核心需求说明
-
-- **客户管理**:客户信息的增删改查,支持客户编号、手机、邮箱、余额、额度、状态等管理;支持重置密码;已有 customer API。
-- **子账号**:主客户可创建子账号,子账号共享主客户余额与额度,独立登录与权限。
-
-### 4.2 表结构设计
-
-#### 客户表 `t_customer`
-
-> 已有 customer API,表结构参考 CustomerDto。
-
-| 字段名 | 字段类型 | 长度 | 主键/外键 | 非空 | 说明 |
-| --- | --- | --- | --- | --- | --- |
-| Id | BIGINT | - | 主键 | 是 | 自增唯一标识 |
-| CustomerCode | VARCHAR | 32 | - | 是 | 客户编号,规则:前缀w+年份+序号(如W2501) |
-| CustomerName | VARCHAR | 128 | - | 是 | 客户名称 |
-| WechatAvatar | VARCHAR | 255 | - | 否 | 微信头像URL |
-| WechatNickname | VARCHAR | 64 | - | 否 | 微信昵称 |
-| Email | VARCHAR | 64 | - | 否 | 邮箱,唯一,用于登录/找回密码 |
-| Phone | VARCHAR | 16 | - | 是 | 手机号,唯一,用于登录/验证 |
-| Balance | DECIMAL | 18,2 | - | 是 | 账户余额,默认0 |
-| TotalQuota | DECIMAL | 18,2 | - | 是 | 总额度,默认0 |
-| UsedQuota | DECIMAL | 18,2 | - | 是 | 已用额度,默认0 |
-| Status | TINYINT | - | - | 是 | 状态:0-禁用,1-正常,2-冻结 |
-| Remark | VARCHAR | 512 | - | 否 | 备注信息 |
-| CreateUserId | INT | - | - | 是 | 创建人(系统/管理员账号) |
-| CreateTime | DATETIME | - | - | 是 | 创建时间,默认当前时间 |
-| UpdateUserId | INT | - | - | 否 | 更新人 |
-| UpdateTime | DATETIME | - | - | 否 | 更新时间 |
-| Salesman | VARCHAR | 32 | - | 否 | 所属销售员账号 |
-| CustomerService | VARCHAR | 32 | - | 否 | 所属客服账号 |
-| CustomerGroup | TINYINT | - | - | 否 | 客户分组:0-普通客户,1-VIP1,2-VIP2,3-VIP3,4-VIP4,5-VIP5 |
-| Role | VARCHAR | 64 | - | 否 | 角色,用于菜单权限 |
-| LastLoginIp | VARCHAR | 64 | - | 否 | 最后在线IP |
-| LastLoginTime | DATETIME | - | - | 否 | 最后在线时间 |
-| IsDeleted | TINYINT | - | - | 是 | 是否删除:0-否,1-是,默认0 |
-| WechatOpenid | VARCHAR | 64 | - | 否 | 微信OpenID,唯一,关联微信账号 |
-| Password | VARCHAR | 64 | - | 是 | 密码(MD5加密存储) |
-
-**CustomerGroup 枚举**:0-普通客户、1-VIP1、2-VIP2、3-VIP3、4-VIP4、5-VIP5
-
-#### 子账号表 `t_customer_sub_account`
-
-| 字段名 | 字段类型 | 长度 | 主键/外键 | 非空 | 说明 |
-| --- | --- | --- | --- | --- | --- |
-| Id | BIGINT | - | 主键 | 是 | 自增唯一标识 |
-| MainCustomerId | BIGINT | - | 外键(t_customer.Id) | 是 | 主客户ID |
-| SubAccountCode | VARCHAR | 32 | - | 是 | 子账号编号,规则:主客户编号+"_"+序号 |
-| SubWechatAvatar | VARCHAR | 255 | - | 否 | 子账号微信头像 |
-| SubWechatNickname | VARCHAR | 64 | - | 否 | 子账号微信昵称 |
-| SubPhone | VARCHAR | 16 | - | 是 | 子账号手机号,唯一 |
-| SubEmail | VARCHAR | 64 | - | 否 | 子账号邮箱 |
-| SubPassword | VARCHAR | 64 | - | 是 | 子账号密码(MD5加密) |
-| SubStatus | TINYINT | - | - | 是 | 子账号状态:0-禁用,1-正常 |
-| CreateUserId | INT | - | - | 是 | 创建人(主客户/管理员) |
-| CreateTime | DATETIME | - | - | 是 | 创建时间 |
-| UpdateUserId | INT | - | - | 否 | 更新人 |
-| UpdateTime | DATETIME | - | - | 否 | 更新时间 |
-| IsDeleted | TINYINT | - | - | 是 | 是否删除:0-否,1-是 |
-
-### 4.3 核心接口设计
-
-| 接口名称 | 请求方式 | 请求路径 | 说明 |
-| --- | --- | --- | --- |
-| 客户列表 | GET | /customer/list | 客户列表 |
-| 客户详情 | GET | /customer/get | 客户详情 |
-| 创建客户 | POST | /customer/create | 创建客户 |
-| 更新客户 | POST | /customer/update | 更新客户 |
-| 删除客户 | POST | /customer/delete | 删除客户 |
-| 重置密码 | POST | /customer/reset-password | 重置密码 |
-
-> 个人中心可复用 `/customer/get`,传入当前登录客户 ID。
-
-##### 客户列表(GET /customer/list)
-
-###### 1 接口请求示例
-**请求 /customer/list**(GET,Query 参数)
-
-```
-?page=1&pageSize=20&keyword=&status=1&customerGroup=&salesman=&customerService=
-```
-
-**响应**
-
-```json
-{
- "code": 0,
- "msg": "success",
- "data": {
- "items": [
- {
- "id": 1,
- "customerCode": "W2501",
- "wechatAvatar": "",
- "wechatNickname": "",
- "email": "customer@example.com",
- "phone": "13800138000",
- "balance": 1000.00,
- "totalQuota": 5000.00,
- "usedQuota": 500.00,
- "status": 1,
- "remark": "",
- "salesman": "sales1",
- "customerService": "cs1",
- "customerGroup": 1,
- "lastLoginIp": "192.168.1.1",
- "lastLoginTime": "2024-01-01 12:00:00",
- "createTime": "2024-01-01 12:00:00",
- "updateTime": "2024-01-01 12:00:00"
- }
- ],
- "total": 50
- }
-}
-```
-
-###### 2 接口业务逻辑
-
-####### 核心流程
-
-- 根据筛选参数分页查询客户列表,返回客户基础信息、状态与余额额度信息。
-
-####### 校验与异常
-
-- 校验分页参数合法性;当筛选条件无匹配数据时返回空列表与 total=0。
-
-##### 客户详情(GET /customer/get)
-
-###### 1 接口请求示例
-
-**请求 /customer/get**(GET,Query 参数)
-
-```
-?id=1
-```
-
-**响应**
-
-```json
-{
- "code": 0,
- "msg": "success",
- "data": {
- "id": 1,
- "customerCode": "W2501",
- "wechatAvatar": "",
- "wechatNickname": "",
- "email": "customer@example.com",
- "phone": "13800138000",
- "balance": 1000.00,
- "totalQuota": 5000.00,
- "usedQuota": 500.00,
- "status": 1,
- "remark": "",
- "salesman": "sales1",
- "customerService": "cs1",
- "customerGroup": 1,
- "lastLoginIp": "192.168.1.1",
- "lastLoginTime": "2024-01-01 12:00:00",
- "createTime": "2024-01-01 12:00:00",
- "createBy": "admin",
- "updateTime": "2024-01-01 12:00:00",
- "updateBy": "admin"
- }
-}
-```
-
-###### 2 接口业务逻辑
-
-####### 核心流程
-
-- 按客户 ID 查询客户详情,返回完整客户资料与审计字段。
-
-####### 校验与异常
-
-- 校验 `id` 必填且存在;不存在时返回业务错误码。
-
-##### 创建客户(POST /customer/create)
-
-###### 1 接口请求示例
-
-**请求 /customer/create**(POST)
-
-```json
-{
- "customerCode": "W2501",
- "customerName": "张三客户",
- "wechatAvatar": "https://cdn.example.com/avatar/w2501.png",
- "wechatNickname": "张三",
- "phone": "13800138000",
- "email": "customer@example.com",
- "password": "123456",
- "balance": 1000.00,
- "totalQuota": 5000.00,
- "usedQuota": 500.00,
- "status": 1,
- "role": "customer",
- "remark": "",
- "salesman": "sales1",
- "customerService": "cs1",
- "customerGroup": 1
-}
-```
-
-**响应**
-
-```json
-{
- "code": 0,
- "msg": "success",
- "data": null
-}
-```
-
-###### 2 接口业务逻辑
-
-####### 核心流程
-
-- 新增客户账号并写入客户基础资料(客户编号/名称、微信信息、联系方式)、余额额度、角色与业务归属信息。
-
-####### 校验与异常
-
-- 校验手机号/邮箱唯一性与必填字段,冲突时返回业务错误。
-
-##### 更新客户(POST /customer/update)
-
-###### 1 接口请求示例
-
-**请求 /customer/update**(POST)
-
-```json
-{
- "id": 1,
- "customerCode": "W2501",
- "customerName": "张三客户",
- "wechatAvatar": "https://cdn.example.com/avatar/w2501.png",
- "wechatNickname": "张三",
- "phone": "13800138000",
- "email": "customer@example.com",
- "balance": 1000.00,
- "totalQuota": 5000.00,
- "usedQuota": 500.00,
- "status": 1,
- "role": "customer",
- "remark": "",
- "salesman": "sales1",
- "customerService": "cs1",
- "customerGroup": 1
-}
-```
-
-**响应**
-
-```json
-{
- "code": 0,
- "msg": "success",
- "data": null
-}
-```
-
-###### 2 接口业务逻辑
-
-####### 核心流程
-
-- 根据客户 ID 更新客户资料(编号/名称、微信信息、联系方式)、余额额度、状态、角色与业务归属字段。
-
-####### 校验与异常
-
-- 校验 `id` 存在与可更新状态;非法状态流转时返回业务错误。
-
-##### 删除客户(POST /customer/delete)
-
-###### 1 接口请求示例
-
-**请求 /customer/delete**(POST)
-
-```json
-{
- "id": 1
-}
-```
-
-**响应**
-
-```json
-{
- "code": 0,
- "msg": "success",
- "data": null
-}
-```
-
-###### 2 接口业务逻辑
-
-####### 核心流程
-
-- 按客户 ID 执行逻辑删除,保留历史审计数据。
-
-####### 校验与异常
-
-- 校验 `id` 存在且可删除;存在关联数据限制时返回业务错误。
-
-##### 重置密码(POST /customer/reset-password)
-
-###### 1 接口请求示例
-
-**请求 /customer/reset-password**(POST)
-
-```json
-{
- "id": 1,
- "newPassword": "123456"
-}
-```
-
-**响应**
-
-```json
-{
- "code": 0,
- "msg": "success",
- "data": null
-}
-```
-
-###### 2 接口业务逻辑
-
-####### 核心流程
-
-- 根据客户 ID 重置登录密码并更新密码变更时间。
-
-####### 校验与异常
-
-- 校验 `id` 与 `newPassword` 合法性,不满足密码策略时返回业务错误。
-
-----------------------------------------------------------------------------------------
-
-----------------------------------------------------------------------------------------
-
-## 六、充值管理
-
-### 5.1 核心需求说明
-
-- **充值流程**:客户提交充值金额 → 选择支付方式(微信/支付宝)→ 上传充值截图 → 提交审核 → 管理员审核(通过/驳回)→ 审核通过后更新客户余额。
-- **充值记录**:完整留存充值全流程信息,支持查询、导出;充值状态包含:待审核、审核通过、审核驳回。
-
-### 5.2 表结构设计
-
-#### 充值记录表 `t_recharge_record`
-
-| 字段名 | 字段类型 | 长度 | 主键/外键 | 非空 | 说明 |
-| --- | --- | --- | --- | --- | --- |
-| Id | BIGINT | - | 主键 | 是 | 自增唯一标识 |
-| CustomerId | BIGINT | - | 外键(t_customer.Id) | 是 | 客户ID(主账号/子账号关联主账号ID) |
-| 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 |
-
-### 5.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 | 审核通过则更新客户余额,更新审核时间、状态、审核人 |
-| 查询充值记录 | GET | /recharge/recharge-record/list | customerId, serialNumber, startTime, endTime, status | list, total | 多条件分页查询充值记录 |
-| 导出充值记录 | GET | /recharge/export | customerId, startTime, endTime | 文件流 | 导出充值记录为Excel文件 |
-
-##### 提交充值申请(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` 客户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 接口业务逻辑
-
-####### 核心流程
-
-- 创建充值记录并生成流水号,初始状态为待审核。
-
-####### 校验与异常
-
-- 校验金额、支付方式与截图信息合法性,不合法时返回业务错误。
-
-##### 审核充值申请-通过(POST /recharge/recharge-record/approve)
-
-###### 1 接口请求示例
-
-**请求 /recharge/recharge-record/approve**(POST)
-
-```json
-{
- "id": 1,
- "remark": "审核通过"
-}
-```
-
-**响应**
-
-```json
-{
- "code": 0,
- "msg": "success",
- "data": null
-}
-```
-
-###### 2 接口业务逻辑
-
-####### 核心流程
-
-- 审核通过后更新充值状态与审核信息,并增加客户余额。
-
-####### 校验与异常
-
-- 校验记录状态必须为待审核;重复审核或状态冲突时返回业务错误。
-
-##### 审核充值申请-驳回(POST /recharge/recharge-record/reject)
-
-###### 1 接口请求示例
-
-**请求 /recharge/recharge-record/reject**(POST)
-
-```json
-{
- "id": 1,
- "remark": "截图不清晰,请重新上传"
-}
-```
-
-**响应**
-
-```json
-{
- "code": 0,
- "msg": "success",
- "data": null
-}
-```
-
-###### 2 接口业务逻辑
-
-####### 核心流程
-
-- 审核驳回后更新充值状态与审核备注,不变更客户余额。
-
-####### 校验与异常
-
-- 校验记录状态与审核权限,非法操作时返回业务错误。
-
-##### 查询充值记录(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 接口业务逻辑
-
-####### 核心流程
-
-- 按客户、流水号、状态和时间区间进行分页查询,返回记录列表与总数。
-
-####### 校验与异常
-
-- 校验时间区间与分页参数合法性,查询异常时返回统一错误码。
-
-##### 导出充值记录(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 文件流。
-
-####### 校验与异常
-
-- 校验导出条件与数据权限,导出失败时返回统一错误信息。
-
-----------------------------------------------------------------------------------------
-
-----------------------------------------------------------------------------------------
-
-## 七、系统模块
-
-### 6.1 核心需求说明
-
-- **仓库地址**:管理仓库收货地址信息,包括主仓/中转仓/退货仓/其他类型;支持收件人、联系电话、收货地址、客服电话、客服微信及二维码等配置。
-- **国家管理**:维护国家基础资料(中文名称、英文名称、二字代码),供渠道分区、下单地址等模块复用。
-- **字典管理**:维护系统字典项,支持按字典类型分组管理字典编码、名称和排序,供下拉选项、状态映射等场景统一复用。
-
-### 6.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 | - | - | 否 | 更新时间 |
-
-#### 字典表 `t_dict`
-
-| 字段名 | 字段类型 | 长度 | 主键/外键 | 非空 | 说明 |
-| --- | --- | --- | --- | --- | --- |
-| Id | BIGINT | - | 主键 | 是 | 自增唯一标识 |
-| DictType | VARCHAR | 64 | - | 是 | 字典类型(如 notice_type、warehouse_type) |
-| DictSort | INT | - | - | 是 | 字典排序,值越小越靠前 |
-| DictCode | VARCHAR | 64 | - | 是 | 字典编码(同类型下唯一) |
-| DictName | VARCHAR | 128 | - | 是 | 字典名称 |
-| IsDeleted | TINYINT | - | - | 是 | 是否删除:0-否,1-是,默认0 |
-| CreateUserId | INT | - | - | 是 | 创建人 |
-| CreateTime | DATETIME | - | - | 是 | 创建时间 |
-| UpdateUserId | INT | - | - | 否 | 更新人 |
-| UpdateTime | DATETIME | - | - | 否 | 更新时间 |
-
-#### 公告通知表 `t_notice`
-
-| 字段名 | 字段类型 | 长度 | 主键/外键 | 非空 | 说明 |
-| --- | --- | --- | --- | --- | --- |
-| Id | BIGINT | - | 主键 | 是 | 自增唯一标识 |
-| NoticeType | VARCHAR | 16 | - | 是 | 通知类型:notice-公告,message-站内信 |
-| Title | VARCHAR | 200 | - | 是 | 标题 |
-| Content | TEXT | - | - | 是 | 内容 |
-| IsPopup | TINYINT | - | - | 是 | 是否弹窗:0-否,1-是 |
-| ReceiverType | VARCHAR | 16 | - | 是 | 接收对象:user-用户,customer-客户 |
-| ReceiverIds | JSON | - | - | 是 | 接收人 ID 列表 |
-| IsDeleted | TINYINT | - | - | 是 | 是否删除:0-否,1-是,默认0 |
-| CreateUserId | INT | - | - | 是 | 创建人 |
-| CreateTime | DATETIME | - | - | 是 | 创建时间 |
-| UpdateUserId | INT | - | - | 否 | 更新人 |
-| UpdateTime | DATETIME | - | - | 否 | 更新时间 |
-
-#### 通知已读明细表 `t_notice_read`
-
-| 字段名 | 字段类型 | 长度 | 主键/外键 | 非空 | 说明 |
-| --- | --- | --- | --- | --- | --- |
-| Id | BIGINT | - | 主键 | 是 | 自增唯一标识 |
-| NoticeId | BIGINT | - | 外键(关联 `t_notice.Id`) | 是 | 通知 ID |
-| ReceiverType | VARCHAR | 16 | - | 是 | 接收对象:user-用户,customer-客户 |
-| ReceiverId | BIGINT | - | - | 是 | 已读主体 ID(用户 ID/客户 ID) |
-| ReadTime | DATETIME | - | - | 是 | 已读时间 |
-| CreateTime | DATETIME | - | - | 是 | 创建时间 |
-
-#### 仓库地址表 `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 | - | - | 否 | 更新时间 |
-
-### 6.3 核心接口设计
-
-| 接口名称 | 请求方式 | 请求路径 | 说明 |
-| --- | --- | --- | --- |
-| 国家列表 | 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/notice/list | 分页查询通知 |
-| 新增通知 | POST | /system/notice/create | 新增公告/站内信 |
-| 编辑通知 | POST | /system/notice/update | 编辑公告/站内信 |
-| 删除通知 | POST | /system/notice/delete | 删除公告/站内信 |
-| 确认已读 | POST | /system/notice/read-confirm | 当前用户/客户确认已读 |
-
-##### 国家列表(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` 排序返回。
-
-####### 校验与异常
-
-- 仅返回未删除记录;无数据时返回空数组。
-
-##### 字典列表(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` 降序返回。
-
-####### 校验与异常
-
-- 校验分页参数合法性;仅返回未删除记录,查询异常时返回统一错误码。
-
-##### 新增字典(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` 唯一,重复时返回业务错误。
-
-##### 更新字典(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` 与其他记录冲突,返回业务错误。
-
-##### 删除字典(POST /system/dict/delete)
-
-###### 1 接口请求示例
-
-**请求 /system/dict/delete**(POST)
-
-```json
-{
- "id": 1
-}
-```
-
-**响应**
-
-```json
-{
- "code": 0,
- "msg": "success",
- "data": null
-}
-```
-
-###### 2 接口业务逻辑
-
-####### 核心流程
-
-- 根据字典 ID 执行逻辑删除并保留历史审计信息。
-
-####### 校验与异常
-
-- 校验 `id` 存在与删除前置条件,不存在或已删除时返回业务错误。
-
-##### 仓库列表(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 接口业务逻辑
-
-####### 核心流程
-
-- 按仓库类型与关键词分页查询仓库地址信息列表。
-
-####### 校验与异常
-
-- 校验分页参数与筛选条件合法性,异常时返回统一错误码。
-
-##### 仓库详情(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` 必填且存在,不存在时返回业务错误。
-
-##### 新增仓库(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 接口业务逻辑
-
-####### 核心流程
-
-- 新增仓库地址配置并写入创建审计信息。
-
-####### 校验与异常
-
-- 校验仓库名称、类型及地址字段完整性,不合法时返回业务错误。
-
-##### 更新仓库(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` 存在与更新字段合法性,更新失败时返回业务错误。
-
-##### 删除仓库(POST /system/warehouse/delete)
-
-###### 1 接口请求示例
-
-**请求 /system/warehouse/delete**(POST)
-
-```json
-{
- "id": 1
-}
-```
-
-**响应**
-
-```json
-{
- "code": 0,
- "msg": "success",
- "data": null
-}
-```
-
-###### 2 接口业务逻辑
-
-####### 核心流程
-
-- 根据仓库 ID 执行逻辑删除并保留历史记录。
-
-####### 校验与异常
-
-- 校验 `id` 存在与删除前置条件,冲突时返回业务错误。
-
-##### 通知列表(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` 统计。
-
-####### 校验与异常
-
-- 校验分页参数合法性,仅返回未删除记录;异常时返回统一错误码。
-
-##### 新增通知(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` 为空或包含无效用户/客户时返回业务错误。
-
-##### 编辑通知(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 个接收人。
-
-##### 删除通知(POST /system/notice/delete)
-
-###### 1 接口请求示例
-
-**请求 /system/notice/delete**(POST)
-
-```json
-{
- "id": 101
-}
-```
-
-**响应**
-
-```json
-{
- "code": 0,
- "msg": "success",
- "data": null
-}
-```
-
-###### 2 接口业务逻辑
-
-####### 核心流程
-
-- 根据通知 ID 执行逻辑删除,保留历史审计信息。
-
-####### 校验与异常
-
-- 校验 `id` 必填且存在,不存在或已删除时返回业务错误。
-
-##### 确认已读(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` 幂等写入已读记录,并记录 `readTime`。
-
-####### 校验与异常
-
-- 校验通知存在且未删除。
-- 校验当前主体在通知接收范围内,不在范围内时返回无权限错误。
-
-----------------------------------------------------------------------------------------
-
-----------------------------------------------------------------------------------------
-
-## 附录
-
-### 附录一:接口路径规范(kebab-case 汇总)
-
-| 模块 | 路径前缀 | 示例 |
-| --- | --- | --- |
-| 订单 | /order/ | list, detail, pay, pre-declare, cancel-intercept, delete, price-check |
-| 渠道-销售产品 | /channel/sales-product/ | list, detail, create, update, delete, price/list, allocation/get |
-| 渠道-服务渠道 | /channel/service-channel/ | list, detail, create, update, delete, group/create, group/update, group/delete, price/list |
-| 渠道-计重 | /channel/weight-rule/, weight-rounding/ | list, create, update, delete, info/list |
-| 渠道-自动触发 | /channel/auto-trigger/ | list, create, update, delete, toggle-status |
-| 渠道-分区(KpiArea) | /channel/kpi-area/ | create, update, delete, list, info/add, info/update, info/delete, info/list |
-| 渠道-标签 | /channel/label-template/save | |
-| 操作 | /operation/ | inbound/create, inbound/recent-list, outbound/create, print-label/print |
-| 客户 | /customer/ | list, get, create, update, delete, reset-password |
-| 充值 | /recharge/ | recharge/submit, recharge-record/list, approve, reject |
-| 系统 | /system/ | country/list, warehouse/list, warehouse/detail, warehouse/create, warehouse/update, warehouse/delete, notice/list, notice/create, notice/update, notice/delete, notice/read-confirm |
-
-### 附录二:分区类型说明(黑名单/偏远/价格)
-
-| 页面 | areaType | 说明 |
-| --- | --- | --- |
-| 黑名单区 | blacklist | 黑名单分区 |
-| 偏远分区 | remote | 偏远分区 |
-| 价格分区 | price | 用于计价/价格表关联的分区 |
-
-调用 KpiArea 接口时,通过 `areaType` 区分不同业务分区,无需新建表或接口。
-
-### 附录三:状态码与枚举
-
-#### 订单状态
-
-| status | 中文 |
-| --- | --- |
-| draft | 草稿 |
-| predeclared | 已预报 |
-| received | 已收货 |
-| pendingPayment | 待支付 |
-| pendingShip | 待发货 |
-| pendingOnline | 待上网 |
-| shipping | 运输中 |
-| deliverySuccess | 派送成功 |
-| deliveryFailed | 派送失败 |
-| problematic | 问题件 |
-| deleted | 已删除 |
-| claimed | 已理赔 |
-
-#### 充值状态
-
-| payStatus | 中文 |
-| --- | --- |
-| 0 | 待审核 |
-| 1 | 成功 |
-| 2 | 失败 |
-
-#### 仓库类型
-
-| warehouseType | 中文 |
-| --- | --- |
-| main | 主仓 |
-| transit | 中转仓 |
-| return | 退货仓 |
-| other | 其他 |
-
-#### 通知类型
-
-| noticeType | 中文 |
-| --- | --- |
-| notice | 公告 |
-| message | 站内信 |
-
-#### 接收对象类型
-
-| receiverType | 中文 |
-| --- | --- |
-| user | 用户 |
-| customer | 客户 |
-
-#### 客户分组(客户表)
-
-| customerGroup | 中文 |
-| --- | --- |
-| 0 | 普通客户 |
-| 1 | VIP1 |
-| 2 | VIP2 |
-| 3 | VIP3 |
-| 4 | VIP4 |
-| 5 | VIP5 |
-
-----------------------------------------------------------------------------------------
-
-*文档版本:v1.0 | 最后更新:2025-03-04*
-
-
diff --git a/sql/parcel_billing.sql b/sql/parcel_billing.sql
new file mode 100644
index 0000000..d4a3717
--- /dev/null
+++ b/sql/parcel_billing.sql
@@ -0,0 +1,11 @@
+-- 小包预报计费:订单版本字段 + 费用计费阶段
+-- 若列已存在可跳过对应语句
+
+ALTER TABLE `t_order`
+ ADD COLUMN IF NOT EXISTS `ExportCustomsMode` VARCHAR(32) NULL COMMENT '出口报关方式' AFTER `CargoAttribute`,
+ ADD COLUMN IF NOT EXISTS `PredeclareFailReason` VARCHAR(512) NULL COMMENT '预报失败原因' AFTER `IsRemote`,
+ ADD COLUMN IF NOT EXISTS `SalesProductRulesVersion` VARCHAR(64) NULL COMMENT '销售产品规则快照版本' AFTER `PredeclareFailReason`,
+ ADD COLUMN IF NOT EXISTS `SalesProductPriceVersion` VARCHAR(64) NULL COMMENT '销售产品价格快照版本' AFTER `SalesProductRulesVersion`;
+
+ALTER TABLE `t_order_fee`
+ ADD COLUMN IF NOT EXISTS `ChargingStage` VARCHAR(16) NULL COMMENT '计费阶段:forecast/inbound/outbound' AFTER `PriceEffectiveTime`;
diff --git a/sql/wechat_login.sql b/sql/wechat_login.sql
new file mode 100644
index 0000000..6cc0067
--- /dev/null
+++ b/sql/wechat_login.sql
@@ -0,0 +1,5 @@
+-- 微信登录:客户 openId 唯一索引(允许多个 NULL)
+-- 执行前请确认索引不存在
+
+CREATE UNIQUE INDEX IF NOT EXISTS uk_customer_wechat_openid
+ ON t_customer (WechatOpenid);
diff --git a/src/Oms.Application/Dtos/Channels/ServiceChannelGroupListInput.cs b/src/Oms.Application/Dtos/Channels/ServiceChannelGroupListInput.cs
new file mode 100644
index 0000000..d394e76
--- /dev/null
+++ b/src/Oms.Application/Dtos/Channels/ServiceChannelGroupListInput.cs
@@ -0,0 +1,22 @@
+namespace Oms.Application.Dtos.Channels;
+
+///
+/// 服务渠道分组列表查询入参。
+///
+public class ServiceChannelGroupListInput
+{
+ ///
+ /// 关键词(分组名称模糊匹配)。
+ ///
+ public string? Keyword { get; set; }
+
+ ///
+ /// 应用 ID。
+ ///
+ public int? AppId { get; set; }
+
+ ///
+ /// API 服务商 ID。
+ ///
+ public int? ApiServiceProviderId { get; set; }
+}
diff --git a/src/Oms.Application/Dtos/Channels/ServiceChannelGroupListOutput.cs b/src/Oms.Application/Dtos/Channels/ServiceChannelGroupListOutput.cs
new file mode 100644
index 0000000..7bd3cf1
--- /dev/null
+++ b/src/Oms.Application/Dtos/Channels/ServiceChannelGroupListOutput.cs
@@ -0,0 +1,37 @@
+namespace Oms.Application.Dtos.Channels;
+
+///
+/// 服务渠道分组列表出参。
+///
+public class ServiceChannelGroupListOutput
+{
+ ///
+ /// 主键。
+ ///
+ public int Id { get; set; }
+
+ ///
+ /// 分组名称。
+ ///
+ public string Name { get; set; } = string.Empty;
+
+ ///
+ /// 应用 ID。
+ ///
+ public int AppId { get; set; }
+
+ ///
+ /// API 服务商 ID。
+ ///
+ public int ApiServiceProviderId { get; set; }
+
+ ///
+ /// 创建时间。
+ ///
+ public DateTime CreateTime { get; set; }
+
+ ///
+ /// 更新时间。
+ ///
+ public DateTime? UpdateTime { get; set; }
+}
diff --git a/src/Oms.Application/Dtos/Orders/OrderParcelCreateInput.cs b/src/Oms.Application/Dtos/Orders/OrderParcelCreateInput.cs
index f7cedbe..0ea130a 100644
--- a/src/Oms.Application/Dtos/Orders/OrderParcelCreateInput.cs
+++ b/src/Oms.Application/Dtos/Orders/OrderParcelCreateInput.cs
@@ -40,6 +40,11 @@ public class OrderParcelCreateInput
///
public string? CargoAttribute { get; set; }
+ ///
+ /// 出口报关方式。
+ ///
+ public string? ExportCustomsMode { get; set; }
+
///
/// 仓库主键。
///
diff --git a/src/Oms.Application/Dtos/Orders/OrderPriceCheckOutput.cs b/src/Oms.Application/Dtos/Orders/OrderPriceCheckOutput.cs
index 83d9324..a303919 100644
--- a/src/Oms.Application/Dtos/Orders/OrderPriceCheckOutput.cs
+++ b/src/Oms.Application/Dtos/Orders/OrderPriceCheckOutput.cs
@@ -64,4 +64,24 @@ public class OrderPriceCheckOutput
/// 总成本数值。
///
public decimal TotalCostNum { get; set; }
+
+ ///
+ /// 产品编码。
+ ///
+ public string? ProductCode { get; set; }
+
+ ///
+ /// 绑定的规则快照版本。
+ ///
+ public string? RulesVersion { get; set; }
+
+ ///
+ /// 绑定的价格快照版本。
+ ///
+ public string? PriceVersion { get; set; }
+
+ ///
+ /// 是否偏远。
+ ///
+ public bool IsRemote { get; set; }
}
diff --git a/src/Oms.Application/Dtos/Orders/ProductBillingSnapshotDtos.cs b/src/Oms.Application/Dtos/Orders/ProductBillingSnapshotDtos.cs
new file mode 100644
index 0000000..938dbf8
--- /dev/null
+++ b/src/Oms.Application/Dtos/Orders/ProductBillingSnapshotDtos.cs
@@ -0,0 +1,41 @@
+using Oms.Application.Dtos.Channels;
+
+namespace Oms.Application.Dtos.Orders;
+
+///
+/// 销售产品价格发布快照(与渠道发布 JSON 同构)。
+///
+public class SalesProductPricePublishSnapshotDto
+{
+ public int SalesProductId { get; set; }
+
+ public List Prices { get; set; } = [];
+}
+
+///
+/// 销售产品规则发布快照(与渠道发布 JSON 同构)。
+///
+public class SalesProductRulesPublishSnapshotDto
+{
+ public SalesProductSaveInput? Product { get; set; }
+
+ public List Providers { get; set; } = [];
+}
+
+///
+/// 绑定到订单的快照版本信息。
+///
+public class ProductSnapshotBinding
+{
+ public string RulesVersion { get; set; } = string.Empty;
+
+ public string PriceVersion { get; set; } = string.Empty;
+
+ public DateTime RulesEffectiveTime { get; set; }
+
+ public DateTime PriceEffectiveTime { get; set; }
+
+ public SalesProductRulesPublishSnapshotDto Rules { get; set; } = new();
+
+ public SalesProductPricePublishSnapshotDto Price { get; set; } = new();
+}
diff --git a/src/Oms.Application/Oms.Application.csproj b/src/Oms.Application/Oms.Application.csproj
index 3a58643..c010438 100644
--- a/src/Oms.Application/Oms.Application.csproj
+++ b/src/Oms.Application/Oms.Application.csproj
@@ -22,4 +22,8 @@
+
+
+
+
diff --git a/src/Oms.Application/Services/Channels/ServiceChannelService.Helpers.cs b/src/Oms.Application/Services/Channels/ServiceChannelService.Helpers.cs
index d5aa6e0..c97ff16 100644
--- a/src/Oms.Application/Services/Channels/ServiceChannelService.Helpers.cs
+++ b/src/Oms.Application/Services/Channels/ServiceChannelService.Helpers.cs
@@ -276,6 +276,19 @@ public partial class ServiceChannelService
entity.IsActive = input.IsActive;
}
+ private static ServiceChannelGroupListOutput MapGroupListOutput(ServiceChannelGroup entity)
+ {
+ return new ServiceChannelGroupListOutput
+ {
+ Id = entity.Id,
+ Name = entity.Name,
+ AppId = entity.AppId,
+ ApiServiceProviderId = entity.ApiServiceProviderId,
+ CreateTime = entity.CreateTime,
+ UpdateTime = OmsDocHelper.NullIfDefault(entity.UpdateTime)
+ };
+ }
+
private static ServiceChannelListOutput MapListOutput(ServiceChannel entity, IReadOnlyDictionary groupMap)
{
return new ServiceChannelListOutput
diff --git a/src/Oms.Application/Services/Channels/ServiceChannelService.cs b/src/Oms.Application/Services/Channels/ServiceChannelService.cs
index b70ced5..410ed8d 100644
--- a/src/Oms.Application/Services/Channels/ServiceChannelService.cs
+++ b/src/Oms.Application/Services/Channels/ServiceChannelService.cs
@@ -125,6 +125,27 @@ public partial class ServiceChannelService : ApplicationService
await ServiceChannelRepository.DeleteAsync(entity);
}
+ ///
+ /// 查询服务渠道分组列表。
+ ///
+ /// 入参:分组名称、应用 ID、API 服务商 ID 等筛选条件。
+ /// 服务渠道分组列表,供下拉与左侧树筛选使用。
+ ///
+ /// 核心业务逻辑:
+ /// 1. 按 keyword、appId、apiServiceProviderId 构造筛选条件。
+ /// 2. 查询未删除分组并按 id 倒序返回。
+ /// 3. 无数据时返回空数组。
+ ///
+ [HttpGet("/channel/service-channel/group/list")]
+ public async Task> GetGroupListAsync([FromQuery] ServiceChannelGroupListInput input)
+ {
+ var predicate = BuildGroupListPredicate(input);
+ var entities = await ServiceChannelGroupRepository.GetListAsync(
+ predicate,
+ new Dictionary { [nameof(ServiceChannelGroup.Id)] = false });
+ return entities.Select(MapGroupListOutput).ToList();
+ }
+
///
/// 新增服务渠道分组。
///
@@ -465,4 +486,17 @@ public partial class ServiceChannelService : ApplicationService
(string.IsNullOrWhiteSpace(keyword) || entity.ChannelCode.Contains(keyword) || entity.ChannelName.Contains(keyword)) &&
(!hasIsActiveFilter || entity.IsActive == isActiveValue);
}
+
+ private static Expression> BuildGroupListPredicate(ServiceChannelGroupListInput input)
+ {
+ var keyword = input.Keyword?.Trim();
+ var hasAppId = input.AppId.HasValue;
+ var appId = input.AppId.GetValueOrDefault();
+ var hasApiServiceProviderId = input.ApiServiceProviderId.HasValue;
+ var apiServiceProviderId = input.ApiServiceProviderId.GetValueOrDefault();
+ return entity =>
+ (string.IsNullOrWhiteSpace(keyword) || entity.Name.Contains(keyword)) &&
+ (!hasAppId || entity.AppId == appId) &&
+ (!hasApiServiceProviderId || entity.ApiServiceProviderId == apiServiceProviderId);
+ }
}
diff --git a/src/Oms.Application/Services/Orders/OrderService.Helpers.cs b/src/Oms.Application/Services/Orders/OrderService.Helpers.cs
index 4ca488e..325b7ad 100644
--- a/src/Oms.Application/Services/Orders/OrderService.Helpers.cs
+++ b/src/Oms.Application/Services/Orders/OrderService.Helpers.cs
@@ -111,41 +111,6 @@ public partial class OrderService
}).ToList();
}
- private async Task> BuildPriceCheckOutputsAsync(OrderPriceCheckInput input)
- {
- var products = await SalesProductRepository.GetListAsync(
- x => x.IsActive == Oms.Domain.Shared.Enums.EnableStatus.Enabled &&
- (string.IsNullOrWhiteSpace(input.ShippingMethod) || x.ProductName.Contains(input.ShippingMethod) || x.ProductCode.Contains(input.ShippingMethod)));
- var warehouses = await WarehouseAddressRepository.GetListAsync();
- var warehouse = warehouses.OrderBy(x => x.Id).FirstOrDefault();
- var volumetricWeight = input.Length.HasValue && input.Width.HasValue && input.Height.HasValue
- ? Math.Round((input.Length.Value * input.Width.Value * input.Height.Value) / 6000M, 3)
- : 0;
- var chargeableWeight = Math.Max(input.CargoWeight ?? 0, volumetricWeight);
-
- return products.Select(product =>
- {
- var freight = Math.Round(chargeableWeight * 62M, 2);
- var otherFees = 5M;
- var totalCost = freight + otherFees;
- return new OrderPriceCheckOutput
- {
- Id = product.Id,
- ShippingMethod = product.ProductName,
- Warehouse = warehouse?.WarehouseName,
- DeliveryTime = $"{product.MinTimeliness ?? 0}-{product.MaxTimeliness ?? 0}",
- VolumetricWeight = volumetricWeight,
- ChargeableWeight = chargeableWeight,
- CostFormula = $"(62*{chargeableWeight})+{otherFees}",
- Freight = freight,
- OtherFees = otherFees,
- TotalCost = totalCost,
- MaxDeliveryDays = product.MaxTimeliness,
- TotalCostNum = totalCost
- };
- }).ToList();
- }
-
private async Task> GetTemplateOutputsAsync(OrderTemplateListInput input)
{
var page = OmsDocHelper.NormalizePage(input.Page);
@@ -394,177 +359,6 @@ public partial class OrderService
};
}
- private async Task CreateParcelCoreAsync(OrderParcelCreateInput input)
- {
- if (!string.IsNullOrWhiteSpace(input.RequestId))
- {
- var existed = await OrderRepository.GetByRequestIdAsync(input.RequestId);
- if (existed != null)
- {
- return new OrderParcelCreateOutput
- {
- ClientNo = existed.ClientNo,
- ServiceNo = existed.ServiceNo,
- TrackNo = existed.TrackNo,
- LabelUrl = await CreateOrderPrintFileAsync((await MapOrderOutputsAsync([existed])).First())
- };
- }
- }
-
- var serviceNo = input.ServiceNo?.Trim();
- if (string.IsNullOrWhiteSpace(serviceNo))
- {
- throw new BusinessException("运单号不能为空");
- }
-
- if (await OrderRepository.HasServiceNoAsync(serviceNo))
- {
- throw new BusinessException("运单号已存在");
- }
-
- var order = await OrderRepository.InsertAsync(BuildOrderEntity(input, serviceNo));
- await SaveOrderChildrenAsync(order, input);
- return new OrderParcelCreateOutput
- {
- ClientNo = order.ClientNo,
- ServiceNo = order.ServiceNo,
- TrackNo = order.TrackNo,
- LabelUrl = await CreateOrderPrintFileAsync((await MapOrderOutputsAsync([order])).First())
- };
- }
-
- private Order BuildOrderEntity(OrderParcelCreateInput input, string serviceNo)
- {
- return new Order
- {
- RequestId = input.RequestId,
- ClientNo = input.ClientNo,
- TrackNo = input.TrackNo,
- ServiceNo = serviceNo,
- ProductCode = input.ProductCode,
- ChannelCode = input.ChannelCode,
- CargoAttribute = input.CargoAttribute ?? CargoAttributeCodes.General,
- WarehouseId = input.WarehouseId,
- CustomerId = input.CustomerId,
- IsCod = input.IsCod ? (byte)1 : (byte)0,
- CodCurrency = input.CodCurrency,
- CodSum = input.CodSum,
- DeclaredCurrency = input.DeclaredCurrency,
- DeclaredSum = input.DeclaredSum,
- Quantity = input.Quantity,
- IsIntercept = 0,
- IsRemote = 0,
- OrderStatus = OrderStatusCodes.PendingPayment
- };
- }
-
- private async Task SaveOrderChildrenAsync(Order order, OrderParcelCreateInput input)
- {
- var now = DateTime.Now;
- if (input.Sender != null)
- {
- var sender = await OrderRepository.InsertSenderAsync(new OrderSender
- {
- OrderId = order.Id,
- SenderName = input.Sender.SenderName,
- Company = input.Sender.Company,
- Country = input.Sender.CountryCode,
- Province = input.Sender.Province,
- City = input.Sender.City,
- ZipCode = input.Sender.ZipCode,
- StreetAddress1 = input.Sender.StreetAddress1,
- StreetAddress2 = input.Sender.StreetAddress2,
- StreetAddress3 = input.Sender.StreetAddress3,
- HouseNumber = input.Sender.HouseNumber,
- Phone = input.Sender.Phone,
- TelePhone = input.Sender.TelePhone,
- Email = input.Sender.Email,
- IsDefault = 0
- });
- order.SenderId = sender.Id;
- }
-
- if (input.Receiver != null)
- {
- var recipient = await OrderRepository.InsertRecipientAsync(new OrderRecipient
- {
- OrderId = order.Id,
- RecipientsName = input.Receiver.RecipientsName,
- CompanyName = input.Receiver.CompanyName,
- Country = input.Receiver.CountryCode,
- Phone = input.Receiver.Phone,
- TelePhone = input.Receiver.TelePhone,
- ZipCode = input.Receiver.ZipCode,
- Province = input.Receiver.Province,
- City = input.Receiver.City,
- StreetNumber = input.Receiver.StreetNumber,
- StreetAddress1 = input.Receiver.StreetAddress1,
- StreetAddress2 = input.Receiver.StreetAddress2,
- StreetAddress3 = input.Receiver.StreetAddress3,
- Email = input.Receiver.Email,
- TaxType = input.Receiver.TaxType,
- TaxNo = input.Receiver.TaxNo,
- CertificateType = input.Receiver.CertificateType,
- CertificateNo = input.Receiver.CertificateNo,
- Eori = input.Receiver.Eori,
- Vat = input.Receiver.Vat
- });
- order.RecipientId = recipient.Id;
- }
-
- if (input.Items.Count > 0)
- {
- await OrderRepository.InsertDeclarationsAsync(input.Items.Select(x => new OrderDeclaration
- {
- OrderId = order.Id,
- CnName = x.CnName,
- EnName = x.EnName,
- Sku = x.Sku,
- Unit = x.Unit,
- UnitPrice = x.UnitPrice,
- UnitWeight = x.UnitWeight,
- Quantity = x.Quantity,
- Amount = x.Amount,
- Weight = x.Weight,
- HsCode = x.ExHsCode ?? x.ImHsCode,
- ImHsCode = x.ImHsCode,
- ExHsCode = x.ExHsCode,
- SkuImage = x.SkuImage,
- Category = x.Category,
- Brand = x.Brand,
- BrandType = x.BrandType,
- IsMagnetic = x.IsMagnetic ? (byte)1 : (byte)0,
- Distribution = x.Distribution,
- Material = x.Material,
- EnMaterial = x.EnMaterial,
- Purpose = x.Purpose,
- Attributes = x.Attributes,
- OriginCountry = x.OriginCountry,
- SalesLink = x.SalesLink,
- Model = x.Model,
- Specifications = x.Specifications,
- Comment = x.Comment
- }));
- }
-
- await OrderRepository.InsertWeightsAsync([
- new OrderWeight
- {
- OrderId = order.Id,
- Type = OrderWeightTypeCodes.Forecast,
- Length = input.Length,
- Width = input.Width,
- Height = input.Height,
- ActualWeight = input.ActualWeight,
- VolumetricWeight = input.VolumetricWeight,
- ChargeableWeight = input.ChargeableWeight,
- Cubic = input.Cubic
- }
- ]);
-
- await OrderRepository.UpdateAsync(order);
- }
-
private async Task CreateOrderPrintFileAsync(OrderOutput order)
{
var directory = Path.Combine(GetWebRootDirectory(), "uploads", "prints", "orders");
diff --git a/src/Oms.Application/Services/Orders/OrderService.ParcelBilling.cs b/src/Oms.Application/Services/Orders/OrderService.ParcelBilling.cs
new file mode 100644
index 0000000..e74605a
--- /dev/null
+++ b/src/Oms.Application/Services/Orders/OrderService.ParcelBilling.cs
@@ -0,0 +1,693 @@
+using Fake.Application;
+using Oms.Application.Dtos.Channels;
+using Oms.Application.Dtos.Orders;
+using Oms.Application.Shared;
+using Oms.Domain.ChannelsAggregate.Entities;
+using Oms.Domain.ChannelsAggregate.Repositories;
+using Oms.Domain.CustomersAggregate.Entities;
+using Oms.Domain.CustomersAggregate.Enums;
+using Oms.Domain.CustomersAggregate.Repositories;
+using Oms.Domain.OrdersAggregate.Consts;
+using Oms.Domain.OrdersAggregate.Entities;
+using Oms.Domain.Shared.Enums;
+using Oms.Domain.SystemAggregate.Repositories;
+
+namespace Oms.Application.Services.Orders;
+
+public partial class OrderService
+{
+ private const string PriceOperatorType = "price";
+ private const string RulesOperatorType = "rules";
+ private const int ParcelBatchMaxCount = 2000;
+
+ private ICustomerRepository CustomerRepository => LazyServiceProvider.GetRequiredService();
+ private ICountryRepository CountryRepository => LazyServiceProvider.GetRequiredService();
+ private IWeightRuleRepository WeightRuleRepository => LazyServiceProvider.GetRequiredService();
+ private IWeightRoundingRepository WeightRoundingRepository => LazyServiceProvider.GetRequiredService();
+ private IKpiAreaRepository KpiAreaRepository => LazyServiceProvider.GetRequiredService();
+
+ private async Task CreateParcelCoreAsync(OrderParcelCreateInput input)
+ {
+ if (!string.IsNullOrWhiteSpace(input.RequestId))
+ {
+ var existed = await OrderRepository.GetByRequestIdAsync(input.RequestId);
+ if (existed != null)
+ {
+ return await BuildParcelCreateOutputAsync(existed);
+ }
+ }
+
+ var bindTime = DateTime.Now;
+ await ValidateParcelCreateInputAsync(input);
+
+ var product = await SalesProductRepository.GetByProductCodeAsync(input.ProductCode!.Trim());
+ if (product == null || product.IsActive != EnableStatus.Enabled)
+ {
+ throw new BusinessException("销售产品不存在或未启用");
+ }
+
+ var snapshotBinding = await ResolveSnapshotBindingAsync(product, bindTime);
+ var country = await CountryRepository.FirstOrDefaultAsync(x => x.Id == input.Receiver!.CountryCode);
+ if (country == null)
+ {
+ throw new BusinessException("收件国家不存在");
+ }
+
+ if (await IsBlacklistHitAsync(
+ product,
+ country.CountryCode,
+ input.Receiver?.Province,
+ input.Receiver?.City,
+ input.Receiver?.ZipCode))
+ {
+ throw new BusinessException("收件地址命中黑名单区域");
+ }
+
+ var serviceNo = await ResolveServiceNoAsync(input.ServiceNo);
+ var order = BuildDraftOrderEntity(input, product, country.CountryCode, snapshotBinding, serviceNo);
+ order = await OrderRepository.InsertAsync(order);
+
+ var weights = BuildInitialWeightRows(order.Id, input);
+ await SaveParcelChildrenAsync(order, input, weights);
+
+ try
+ {
+ order.IsRemote = await IsRemoteHitAsync(
+ product,
+ country.CountryCode,
+ input.Receiver?.Province,
+ input.Receiver?.City,
+ input.Receiver?.ZipCode) ? (byte)1 : (byte)0;
+
+ var forecast = weights.First(x => x.Type == OrderWeightTypeCodes.Forecast);
+ await ApplyChargeableWeightAsync(product, forecast);
+ SyncWeightRows(weights, forecast);
+ await OrderRepository.ReplaceWeightsAsync(order.Id, weights);
+
+ var zoneName = await MatchPriceZoneAsync(
+ product,
+ country.CountryCode,
+ input.Receiver?.Province,
+ input.Receiver?.City,
+ input.Receiver?.ZipCode);
+ if (string.IsNullOrWhiteSpace(zoneName))
+ {
+ throw new BusinessException("价格分区未匹配(规则快照)");
+ }
+
+ var customer = await CustomerRepository.FirstOrDefaultAsync(x => x.Id == order.CustomerId);
+ var fees = ParcelBillingCalculator.CalculateForecastReceivableFees(
+ snapshotBinding.Price,
+ snapshotBinding.PriceVersion,
+ snapshotBinding.PriceEffectiveTime,
+ forecast.ChargeableWeight ?? 0,
+ zoneName,
+ order.CargoAttribute ?? CargoAttributeCodes.General,
+ customer?.CustomerGroup);
+
+ if (fees.Count == 0 && ParcelBillingCalculator.HasEnabledForecastPriceItems(snapshotBinding.Price))
+ {
+ throw new BusinessException("产品卖价快照未配置或未匹配到有效价格");
+ }
+
+ if (fees.Count > 0)
+ {
+ await OrderRepository.UpsertFeesAsync(order.Id, fees);
+ }
+
+ order.OrderStatus = OrderStatusCodes.Predeclared;
+ order.PredeclareFailReason = string.Empty;
+ order.IsRemote = order.IsRemote;
+ order.UpdateTime = DateTime.Now;
+ await OrderRepository.UpdateAsync(order);
+
+ return await BuildParcelCreateOutputAsync(order);
+ }
+ catch (BusinessException ex)
+ {
+ await MarkParcelDraftFailedAsync(order, ex.Message);
+ throw;
+ }
+ catch (Exception ex)
+ {
+ await MarkParcelDraftFailedAsync(order, ex.Message);
+ throw new BusinessException(ex.Message);
+ }
+ }
+
+ private async Task ValidateParcelCreateInputAsync(OrderParcelCreateInput input)
+ {
+ if (string.IsNullOrWhiteSpace(input.ProductCode))
+ {
+ throw new BusinessException("产品代码不能为空");
+ }
+
+ if (!input.WarehouseId.HasValue || input.WarehouseId <= 0)
+ {
+ throw new BusinessException("仓库ID不能为空");
+ }
+
+ if (!input.CustomerId.HasValue || input.CustomerId <= 0)
+ {
+ throw new BusinessException("客户ID不能为空");
+ }
+
+ if (!input.Length.HasValue || !input.Width.HasValue || !input.Height.HasValue || !input.ActualWeight.HasValue
+ || input.Length <= 0 || input.Width <= 0 || input.Height <= 0 || input.ActualWeight <= 0)
+ {
+ throw new BusinessException("件重尺必须大于0");
+ }
+
+ if (string.IsNullOrWhiteSpace(input.CargoAttribute)
+ || input.CargoAttribute is not (CargoAttributeCodes.General or CargoAttributeCodes.Electronics or CargoAttributeCodes.Sensitive))
+ {
+ throw new BusinessException("货物属性不合法");
+ }
+
+ if (!string.IsNullOrWhiteSpace(input.ExportCustomsMode)
+ && !ExportCustomsModeCodes.All.Contains(input.ExportCustomsMode))
+ {
+ throw new BusinessException("出口报关方式不合法");
+ }
+
+ if (string.IsNullOrWhiteSpace(input.DeclaredCurrency))
+ {
+ throw new BusinessException("申报币种不能为空");
+ }
+
+ if (!input.Quantity.HasValue || input.Quantity <= 0)
+ {
+ throw new BusinessException("包裹数量不能为空");
+ }
+
+ if (input.Receiver == null
+ || string.IsNullOrWhiteSpace(input.Receiver.RecipientsName)
+ || !input.Receiver.CountryCode.HasValue
+ || string.IsNullOrWhiteSpace(input.Receiver.ZipCode)
+ || string.IsNullOrWhiteSpace(input.Receiver.StreetAddress1))
+ {
+ throw new BusinessException("收件人信息不完整");
+ }
+
+ if (input.Sender == null || string.IsNullOrWhiteSpace(input.Sender.SenderName))
+ {
+ throw new BusinessException("发件人姓名不能为空");
+ }
+
+ if (input.Items.Count == 0)
+ {
+ throw new BusinessException("申报明细不能为空");
+ }
+
+ foreach (var item in input.Items)
+ {
+ if (string.IsNullOrWhiteSpace(item.EnName)
+ || !item.UnitPrice.HasValue
+ || !item.UnitWeight.HasValue
+ || !item.Quantity.HasValue
+ || item.Quantity <= 0)
+ {
+ throw new BusinessException("申报明细必填字段不完整");
+ }
+ }
+
+ var warehouse = await WarehouseAddressRepository.FirstOrDefaultAsync(x => x.Id == input.WarehouseId.Value);
+ if (warehouse == null)
+ {
+ throw new BusinessException("仓库不存在");
+ }
+
+ var customer = await CustomerRepository.FirstOrDefaultAsync(x => x.Id == input.CustomerId.Value);
+ if (customer == null || customer.Status != CustomerStatus.Normal)
+ {
+ throw new BusinessException("客户不存在或状态不可用");
+ }
+ }
+
+ private async Task ResolveSnapshotBindingAsync(SalesProduct product, DateTime bindTime)
+ {
+ var rulesRecord = await SalesProductRepository.GetEffectivePublishRecordAsync(product.Id, RulesOperatorType, bindTime);
+ var priceRecord = await SalesProductRepository.GetEffectivePublishRecordAsync(product.Id, PriceOperatorType, bindTime);
+
+ SalesProductRulesPublishSnapshotDto rulesSnapshot;
+ if (rulesRecord == null)
+ {
+ rulesSnapshot = await BuildLiveRulesSnapshotAsync(product);
+ }
+ else
+ {
+ rulesSnapshot = OmsDocHelper.DeserializeJson(rulesRecord.Content)
+ ?? throw new BusinessException("未命中规则快照版本(内存/Redis/DB)");
+ }
+
+ SalesProductPricePublishSnapshotDto priceSnapshot;
+ if (priceRecord == null)
+ {
+ priceSnapshot = await BuildLivePriceSnapshotAsync(product);
+ if (priceSnapshot.Prices.Count == 0)
+ {
+ throw new BusinessException("未命中价格快照版本(内存/Redis/DB)");
+ }
+ }
+ else
+ {
+ priceSnapshot = OmsDocHelper.DeserializeJson(priceRecord.Content)
+ ?? throw new BusinessException("未命中价格快照版本(内存/Redis/DB)");
+ if (priceSnapshot.Prices.Count == 0)
+ {
+ throw new BusinessException("未命中价格快照版本(内存/Redis/DB)");
+ }
+ }
+
+ if (rulesRecord == null && !product.WeightRuleId.HasValue)
+ {
+ throw new BusinessException("未命中规则快照版本(内存/Redis/DB)");
+ }
+
+ return new ProductSnapshotBinding
+ {
+ RulesVersion = rulesRecord?.Version ?? $"LIVE-{product.Id}",
+ PriceVersion = priceRecord?.Version ?? $"LIVE-{product.Id}",
+ RulesEffectiveTime = rulesRecord?.PublishTime ?? bindTime,
+ PriceEffectiveTime = priceRecord?.PublishTime ?? bindTime,
+ Rules = rulesSnapshot,
+ Price = priceSnapshot
+ };
+ }
+
+ private async Task BuildLiveRulesSnapshotAsync(SalesProduct product)
+ {
+ return new SalesProductRulesPublishSnapshotDto
+ {
+ Product = new SalesProductSaveInput
+ {
+ Id = product.Id,
+ ProductCode = product.ProductCode,
+ ProductName = product.ProductName,
+ BlacklistAreaId = product.BlacklistAreaId,
+ RemoteAreaId = product.RemoteAreaId,
+ PriceAreaId = product.PriceAreaId,
+ WeightRuleId = product.WeightRuleId,
+ WeightRoundingId = product.WeightRoundingId,
+ IsActive = product.IsActive,
+ Scheme = product.Scheme
+ }
+ };
+ }
+
+ private async Task BuildLivePriceSnapshotAsync(SalesProduct product)
+ {
+ var prices = await SalesProductRepository.GetPricesAsync(product.Id);
+ var priceIds = prices.Select(x => x.Id).ToArray();
+ var factorMap = (await SalesProductRepository.GetPriceFactorsAsync(priceIds))
+ .GroupBy(x => x.PriceId)
+ .ToDictionary(x => x.Key, x => x.OrderBy(item => item.SerialNumber).ToList());
+ var detailMap = (await SalesProductRepository.GetPriceDetailsAsync(priceIds))
+ .GroupBy(x => x.PriceId)
+ .ToDictionary(x => x.Key, x => x.OrderBy(item => item.Id).ToList());
+
+ return new SalesProductPricePublishSnapshotDto
+ {
+ SalesProductId = product.Id,
+ Prices = prices.Select(price => new SalesProductPriceListOutput
+ {
+ Id = price.Id,
+ SalesProductId = price.SalesProductId,
+ SerialNumber = price.SerialNumber,
+ Status = (byte)price.Status,
+ PriceName = price.PriceName,
+ CostItem = price.CostItem,
+ CostUnit = price.CostUnit,
+ Currency = price.Currency,
+ ChargingTime = price.ChargingTime,
+ ChargingCondition = price.ChargingCondition,
+ Factors = factorMap.TryGetValue(price.Id, out var factors)
+ ? factors.Select(MapPriceFactorOutput).ToList()
+ : [],
+ Details = detailMap.TryGetValue(price.Id, out var details)
+ ? details.Select(MapPriceDetailOutput).ToList()
+ : []
+ }).ToList()
+ };
+ }
+
+ private static ChannelPriceFactorOutput MapPriceFactorOutput(SalesProductPriceFactor entity)
+ {
+ return new ChannelPriceFactorOutput
+ {
+ Id = entity.Id,
+ PriceId = entity.PriceId,
+ SerialNumber = entity.SerialNumber,
+ Factor = entity.Factor,
+ FactorValues = OmsDocHelper.DeserializeJson>(entity.FactorValues) ?? []
+ };
+ }
+
+ private static ChannelPriceDetailOutput MapPriceDetailOutput(SalesProductPriceDetail entity)
+ {
+ return new ChannelPriceDetailOutput
+ {
+ Id = entity.Id,
+ PriceId = entity.PriceId,
+ FactorValuesJson = OmsDocHelper.DeserializeJsonObject(entity.FactorValuesJson),
+ CalculationFormula = entity.CalculationFormula,
+ CalculatedValue = entity.CalculatedValue,
+ CustomerGroupValues = OmsDocHelper.DeserializeJsonObject(entity.CustomerGroupValues)
+ };
+ }
+
+ private async Task IsBlacklistHitAsync(
+ SalesProduct product,
+ string countryCode,
+ string? province,
+ string? city,
+ string? zipCode)
+ {
+ if (!product.BlacklistAreaId.HasValue || product.BlacklistAreaId <= 0)
+ {
+ return false;
+ }
+
+ var rules = await KpiAreaRepository.GetAllInfosByAreaIdAsync(product.BlacklistAreaId.Value);
+ return rules.Any(rule => ParcelBillingCalculator.IsAreaMatched(
+ rule,
+ countryCode,
+ province,
+ city,
+ zipCode));
+ }
+
+ private async Task IsRemoteHitAsync(
+ SalesProduct product,
+ string countryCode,
+ string? province,
+ string? city,
+ string? zipCode)
+ {
+ if (!product.RemoteAreaId.HasValue || product.RemoteAreaId <= 0)
+ {
+ return false;
+ }
+
+ var rules = await KpiAreaRepository.GetAllInfosByAreaIdAsync(product.RemoteAreaId.Value);
+ return rules.Any(rule => ParcelBillingCalculator.IsAreaMatched(
+ rule,
+ countryCode,
+ province,
+ city,
+ zipCode));
+ }
+
+ private async Task MatchPriceZoneAsync(
+ SalesProduct product,
+ string countryCode,
+ string? province,
+ string? city,
+ string? zipCode)
+ {
+ if (!product.PriceAreaId.HasValue || product.PriceAreaId <= 0)
+ {
+ return "默认分区";
+ }
+
+ var area = await KpiAreaRepository.FirstOrDefaultAsync(x => x.Id == product.PriceAreaId.Value);
+ var rules = await KpiAreaRepository.GetAllInfosByAreaIdAsync(product.PriceAreaId.Value);
+ var matched = rules.FirstOrDefault(rule => ParcelBillingCalculator.IsAreaMatched(
+ rule,
+ countryCode,
+ province,
+ city,
+ zipCode));
+ return ParcelBillingCalculator.ResolveZoneName(matched, area);
+ }
+
+ private async Task ApplyChargeableWeightAsync(SalesProduct product, OrderWeight forecast)
+ {
+ try
+ {
+ if (!product.WeightRuleId.HasValue || product.WeightRuleId <= 0)
+ {
+ throw new BusinessException("未配置计重规则");
+ }
+
+ var weightRule = await WeightRuleRepository.FirstOrDefaultAsync(x => x.Id == product.WeightRuleId.Value);
+ if (weightRule == null)
+ {
+ throw new BusinessException("未配置计重规则");
+ }
+
+ var ruleInfos = await WeightRuleRepository.GetAllInfosByRuleIdAsync(product.WeightRuleId.Value);
+ var (volumetricWeight, rawWeight) = ParcelBillingCalculator.CalculateRawBillableWeight(forecast, ruleInfos);
+ forecast.VolumetricWeight = volumetricWeight;
+
+ if (!product.WeightRoundingId.HasValue || product.WeightRoundingId <= 0)
+ {
+ throw new BusinessException("未配置计重进位");
+ }
+
+ var rounding = await WeightRoundingRepository.FirstOrDefaultAsync(x => x.Id == product.WeightRoundingId.Value);
+ if (rounding == null)
+ {
+ throw new BusinessException("未配置计重进位");
+ }
+
+ var ranges = await WeightRoundingRepository.GetAllInfosByRoundingIdAsync(product.WeightRoundingId.Value);
+ forecast.ChargeableWeight = ParcelBillingCalculator.ApplyRounding(rawWeight, ranges);
+ }
+ catch (BusinessException)
+ {
+ throw;
+ }
+ catch (InvalidOperationException ex)
+ {
+ throw new BusinessException(ex.Message);
+ }
+ }
+
+ private static List BuildInitialWeightRows(int orderId, OrderParcelCreateInput input)
+ {
+ var cubic = ParcelBillingCalculator.CalculateCubic(input.Length!.Value, input.Width!.Value, input.Height!.Value);
+ var forecast = new OrderWeight
+ {
+ OrderId = orderId,
+ Type = OrderWeightTypeCodes.Forecast,
+ Length = input.Length,
+ Width = input.Width,
+ Height = input.Height,
+ ActualWeight = input.ActualWeight,
+ VolumetricWeight = input.VolumetricWeight ?? 0,
+ ChargeableWeight = input.ChargeableWeight ?? input.ActualWeight,
+ Cubic = input.Cubic ?? cubic
+ };
+
+ return
+ [
+ forecast,
+ CloneWeightRow(orderId, OrderWeightTypeCodes.Receive, forecast),
+ CloneWeightRow(orderId, OrderWeightTypeCodes.Ship, forecast),
+ CloneWeightRow(orderId, OrderWeightTypeCodes.Downstream, forecast)
+ ];
+ }
+
+ private static OrderWeight CloneWeightRow(int orderId, string type, OrderWeight source) =>
+ new()
+ {
+ OrderId = orderId,
+ Type = type,
+ Length = source.Length,
+ Width = source.Width,
+ Height = source.Height,
+ ActualWeight = source.ActualWeight,
+ VolumetricWeight = source.VolumetricWeight,
+ ChargeableWeight = source.ChargeableWeight,
+ Cubic = source.Cubic
+ };
+
+ private static void SyncWeightRows(List weights, OrderWeight forecast)
+ {
+ foreach (var weight in weights)
+ {
+ weight.Length = forecast.Length;
+ weight.Width = forecast.Width;
+ weight.Height = forecast.Height;
+ weight.ActualWeight = forecast.ActualWeight;
+ weight.VolumetricWeight = forecast.VolumetricWeight;
+ weight.ChargeableWeight = forecast.ChargeableWeight;
+ weight.Cubic = forecast.Cubic;
+ }
+ }
+
+ private static Order BuildDraftOrderEntity(
+ OrderParcelCreateInput input,
+ SalesProduct product,
+ string consigneeCountry,
+ ProductSnapshotBinding snapshotBinding,
+ string serviceNo)
+ {
+ return new Order
+ {
+ RequestId = input.RequestId,
+ ClientNo = input.ClientNo,
+ TrackNo = input.TrackNo,
+ ServiceNo = serviceNo,
+ ProductCode = input.ProductCode,
+ ChannelCode = input.ChannelCode ?? product.ProductCode,
+ ConsigneeCountry = consigneeCountry,
+ CargoAttribute = input.CargoAttribute ?? CargoAttributeCodes.General,
+ ExportCustomsMode = input.ExportCustomsMode,
+ WarehouseId = input.WarehouseId,
+ CustomerId = input.CustomerId,
+ IsCod = input.IsCod ? (byte)1 : (byte)0,
+ CodCurrency = input.CodCurrency,
+ CodSum = input.CodSum,
+ DeclaredCurrency = input.DeclaredCurrency,
+ DeclaredSum = input.DeclaredSum,
+ Quantity = input.Quantity,
+ IsIntercept = 0,
+ IsRemote = 0,
+ PredeclareFailReason = string.Empty,
+ SalesProductRulesVersion = snapshotBinding.RulesVersion,
+ SalesProductPriceVersion = snapshotBinding.PriceVersion,
+ OrderStatus = OrderStatusCodes.Draft
+ };
+ }
+
+ private async Task SaveParcelChildrenAsync(Order order, OrderParcelCreateInput input, List weights)
+ {
+ if (input.Sender != null)
+ {
+ var sender = await OrderRepository.InsertSenderAsync(new OrderSender
+ {
+ OrderId = order.Id,
+ SenderName = input.Sender.SenderName,
+ Company = input.Sender.Company,
+ Country = input.Sender.CountryCode,
+ Province = input.Sender.Province,
+ City = input.Sender.City,
+ ZipCode = input.Sender.ZipCode,
+ StreetAddress1 = input.Sender.StreetAddress1,
+ StreetAddress2 = input.Sender.StreetAddress2,
+ StreetAddress3 = input.Sender.StreetAddress3,
+ HouseNumber = input.Sender.HouseNumber,
+ Phone = input.Sender.Phone,
+ TelePhone = input.Sender.TelePhone,
+ Email = input.Sender.Email,
+ IsDefault = 0
+ });
+ order.SenderId = sender.Id;
+ }
+
+ if (input.Receiver != null)
+ {
+ var recipient = await OrderRepository.InsertRecipientAsync(new OrderRecipient
+ {
+ OrderId = order.Id,
+ RecipientsName = input.Receiver.RecipientsName,
+ CompanyName = input.Receiver.CompanyName,
+ Country = input.Receiver.CountryCode,
+ Phone = input.Receiver.Phone,
+ TelePhone = input.Receiver.TelePhone,
+ ZipCode = input.Receiver.ZipCode,
+ Province = input.Receiver.Province,
+ City = input.Receiver.City,
+ StreetNumber = input.Receiver.StreetNumber,
+ StreetAddress1 = input.Receiver.StreetAddress1,
+ StreetAddress2 = input.Receiver.StreetAddress2,
+ StreetAddress3 = input.Receiver.StreetAddress3,
+ Email = input.Receiver.Email,
+ TaxType = input.Receiver.TaxType,
+ TaxNo = input.Receiver.TaxNo,
+ CertificateType = input.Receiver.CertificateType,
+ CertificateNo = input.Receiver.CertificateNo,
+ Eori = input.Receiver.Eori,
+ Vat = input.Receiver.Vat
+ });
+ order.RecipientId = recipient.Id;
+ }
+
+ if (input.Items.Count > 0)
+ {
+ await OrderRepository.InsertDeclarationsAsync(input.Items.Select(x => new OrderDeclaration
+ {
+ OrderId = order.Id,
+ CnName = x.CnName,
+ EnName = x.EnName,
+ Sku = x.Sku,
+ Unit = x.Unit,
+ UnitPrice = x.UnitPrice,
+ UnitWeight = x.UnitWeight,
+ Quantity = x.Quantity,
+ Amount = x.Amount,
+ Weight = x.Weight,
+ HsCode = x.ExHsCode ?? x.ImHsCode,
+ ImHsCode = x.ImHsCode,
+ ExHsCode = x.ExHsCode,
+ SkuImage = x.SkuImage,
+ Category = x.Category,
+ Brand = x.Brand,
+ BrandType = x.BrandType,
+ IsMagnetic = x.IsMagnetic ? (byte)1 : (byte)0,
+ Distribution = x.Distribution,
+ Material = x.Material,
+ EnMaterial = x.EnMaterial,
+ Purpose = x.Purpose,
+ Attributes = x.Attributes,
+ OriginCountry = x.OriginCountry,
+ SalesLink = x.SalesLink,
+ Model = x.Model,
+ Specifications = x.Specifications,
+ Comment = x.Comment
+ }));
+ }
+
+ await OrderRepository.InsertWeightsAsync(weights);
+ await OrderRepository.UpdateAsync(order);
+ }
+
+ private async Task ResolveServiceNoAsync(string? serviceNo)
+ {
+ var normalized = serviceNo?.Trim();
+ if (!string.IsNullOrWhiteSpace(normalized))
+ {
+ if (await OrderRepository.HasServiceNoAsync(normalized))
+ {
+ throw new BusinessException("运单号已存在");
+ }
+
+ return normalized;
+ }
+
+ for (var i = 0; i < 5; i++)
+ {
+ var generated = $"LP{DateTime.Now:yyyyMMddHHmmss}{Random.Shared.Next(1000, 9999)}";
+ if (!await OrderRepository.HasServiceNoAsync(generated))
+ {
+ return generated;
+ }
+ }
+
+ throw new BusinessException("运单号生成失败");
+ }
+
+ private async Task MarkParcelDraftFailedAsync(Order order, string reason)
+ {
+ order.OrderStatus = OrderStatusCodes.Draft;
+ order.PredeclareFailReason = reason;
+ order.UpdateTime = DateTime.Now;
+ await OrderRepository.UpdateAsync(order);
+ }
+
+ private async Task BuildParcelCreateOutputAsync(Order order)
+ {
+ var mapped = (await MapOrderOutputsAsync([order])).First();
+ return new OrderParcelCreateOutput
+ {
+ ClientNo = order.ClientNo,
+ ServiceNo = order.ServiceNo,
+ TrackNo = order.TrackNo,
+ LabelUrl = order.OrderStatus == OrderStatusCodes.Predeclared
+ ? await CreateOrderPrintFileAsync(mapped)
+ : string.Empty
+ };
+ }
+}
diff --git a/src/Oms.Application/Services/Orders/OrderService.ParcelQuote.cs b/src/Oms.Application/Services/Orders/OrderService.ParcelQuote.cs
new file mode 100644
index 0000000..11defb2
--- /dev/null
+++ b/src/Oms.Application/Services/Orders/OrderService.ParcelQuote.cs
@@ -0,0 +1,206 @@
+using Fake.Application;
+using Oms.Application.Dtos.Orders;
+using Oms.Domain.ChannelsAggregate.Entities;
+using Oms.Domain.CustomersAggregate.Enums;
+using Oms.Domain.OrdersAggregate.Consts;
+using Oms.Domain.Shared.Enums;
+
+namespace Oms.Application.Services.Orders;
+
+public partial class OrderService
+{
+ private async Task> BuildPriceCheckOutputsAsync(OrderPriceCheckInput input)
+ {
+ ValidatePriceCheckInput(input);
+ var bindTime = DateTime.Now;
+ var countryCode = await ResolveDestinationCountryCodeAsync(input.DestinationCountry);
+ var cargoAttribute = ParcelBillingCalculator.MapCargoTypeToAttribute(input.CargoType);
+ var quoteContext = new ParcelQuoteContext
+ {
+ CountryCode = countryCode,
+ Province = input.DestinationProvince,
+ PostalCode = input.PostalCode,
+ CargoAttribute = cargoAttribute,
+ Length = input.Length!.Value,
+ Width = input.Width!.Value,
+ Height = input.Height!.Value,
+ ActualWeight = input.CargoWeight!.Value
+ };
+
+ var customerGroup = await ResolveCurrentCustomerGroupAsync();
+ var shippingKeyword = input.ShippingMethod?.Trim();
+ var products = await SalesProductRepository.GetListAsync(x =>
+ x.IsActive == EnableStatus.Enabled &&
+ (string.IsNullOrWhiteSpace(shippingKeyword)
+ || x.ProductName.Contains(shippingKeyword)
+ || x.ProductCode.Contains(shippingKeyword)));
+
+ var warehouses = await WarehouseAddressRepository.GetListAsync();
+ var defaultWarehouse = warehouses.OrderBy(x => x.Id).FirstOrDefault();
+ var outputs = new List();
+
+ foreach (var product in products.OrderBy(x => x.Id))
+ {
+ var quote = await TryComputeProductQuoteAsync(product, quoteContext, customerGroup, bindTime);
+ if (quote == null)
+ {
+ continue;
+ }
+
+ var (freight, otherFees, totalCost, costFormula) = ParcelBillingCalculator.SummarizeFees(quote.Fees);
+ outputs.Add(new OrderPriceCheckOutput
+ {
+ Id = quote.ProductId,
+ ProductCode = quote.ProductCode,
+ ShippingMethod = quote.ProductName,
+ Warehouse = defaultWarehouse?.WarehouseName,
+ DeliveryTime = $"{product.MinTimeliness ?? 0}-{product.MaxTimeliness ?? 0}",
+ VolumetricWeight = quote.VolumetricWeight,
+ ChargeableWeight = quote.ChargeableWeight,
+ CostFormula = costFormula,
+ Freight = freight,
+ OtherFees = otherFees,
+ TotalCost = totalCost,
+ MaxDeliveryDays = product.MaxTimeliness,
+ TotalCostNum = totalCost,
+ RulesVersion = quote.RulesVersion,
+ PriceVersion = quote.PriceVersion,
+ IsRemote = quote.IsRemote
+ });
+ }
+
+ return outputs;
+ }
+
+ private static void ValidatePriceCheckInput(OrderPriceCheckInput input)
+ {
+ if (string.IsNullOrWhiteSpace(input.DestinationCountry))
+ {
+ throw new BusinessException("目的国不能为空");
+ }
+
+ if (!input.CargoWeight.HasValue || input.CargoWeight <= 0)
+ {
+ throw new BusinessException("货物重量必须大于0");
+ }
+
+ if (!input.Length.HasValue || !input.Width.HasValue || !input.Height.HasValue
+ || input.Length <= 0 || input.Width <= 0 || input.Height <= 0)
+ {
+ throw new BusinessException("长宽高必须大于0");
+ }
+ }
+
+ private async Task ResolveDestinationCountryCodeAsync(string destinationCountry)
+ {
+ var trimmed = destinationCountry.Trim();
+ var country = await CountryRepository.FirstOrDefaultAsync(x =>
+ x.CountryCode == trimmed || x.ChineseName == trimmed || x.EnglishName == trimmed);
+ return country?.CountryCode ?? trimmed.ToUpperInvariant();
+ }
+
+ private async Task ResolveCurrentCustomerGroupAsync()
+ {
+ try
+ {
+ var userId = CurrentUser.GetId();
+ if (userId <= 0)
+ {
+ return null;
+ }
+
+ var customer = await CustomerRepository.FirstOrDefaultAsync(x => x.UserId == userId);
+ return customer?.CustomerGroup;
+ }
+ catch
+ {
+ return null;
+ }
+ }
+
+ ///
+ /// 对单个销售产品执行预报试算(与 parcel/create 同源,不落库)。
+ ///
+ private async Task TryComputeProductQuoteAsync(
+ SalesProduct product,
+ ParcelQuoteContext context,
+ CustomerGroup? customerGroup,
+ DateTime bindTime)
+ {
+ try
+ {
+ if (product.IsActive != EnableStatus.Enabled)
+ {
+ return null;
+ }
+
+ var snapshotBinding = await ResolveSnapshotBindingAsync(product, bindTime);
+ if (await IsBlacklistHitAsync(
+ product,
+ context.CountryCode,
+ context.Province,
+ context.City,
+ context.PostalCode))
+ {
+ return null;
+ }
+
+ var forecast = ParcelBillingCalculator.BuildForecastWeight(context);
+ await ApplyChargeableWeightAsync(product, forecast);
+
+ var zoneName = await MatchPriceZoneAsync(
+ product,
+ context.CountryCode,
+ context.Province,
+ context.City,
+ context.PostalCode);
+ if (string.IsNullOrWhiteSpace(zoneName))
+ {
+ return null;
+ }
+
+ var fees = ParcelBillingCalculator.CalculateForecastReceivableFees(
+ snapshotBinding.Price,
+ snapshotBinding.PriceVersion,
+ snapshotBinding.PriceEffectiveTime,
+ forecast.ChargeableWeight ?? 0,
+ zoneName,
+ context.CargoAttribute,
+ customerGroup);
+
+ if (fees.Count == 0 && ParcelBillingCalculator.HasEnabledForecastPriceItems(snapshotBinding.Price))
+ {
+ return null;
+ }
+
+ var isRemote = await IsRemoteHitAsync(
+ product,
+ context.CountryCode,
+ context.Province,
+ context.City,
+ context.PostalCode);
+
+ return new ParcelQuoteResult
+ {
+ ProductId = product.Id,
+ ProductCode = product.ProductCode,
+ ProductName = product.ProductName,
+ RulesVersion = snapshotBinding.RulesVersion,
+ PriceVersion = snapshotBinding.PriceVersion,
+ VolumetricWeight = forecast.VolumetricWeight ?? 0,
+ ChargeableWeight = forecast.ChargeableWeight ?? 0,
+ ZoneName = zoneName,
+ IsRemote = isRemote,
+ Fees = fees
+ };
+ }
+ catch (BusinessException)
+ {
+ return null;
+ }
+ catch (InvalidOperationException)
+ {
+ return null;
+ }
+ }
+}
diff --git a/src/Oms.Application/Services/Orders/OrderService.cs b/src/Oms.Application/Services/Orders/OrderService.cs
index 41dddcc..ecc9276 100644
--- a/src/Oms.Application/Services/Orders/OrderService.cs
+++ b/src/Oms.Application/Services/Orders/OrderService.cs
@@ -354,6 +354,11 @@ public partial class OrderService : ApplicationService
throw new BusinessException("批量创建请求不能为空");
}
+ if (input.Count > ParcelBatchMaxCount)
+ {
+ throw new BusinessException($"单次批量创建不能超过{ParcelBatchMaxCount}条");
+ }
+
var batchNo = $"PCB{DateTime.Now:yyyyMMddHHmmssfff}";
var items = new List(input.Count);
var successCount = 0;
diff --git a/src/Oms.Application/Services/Orders/ParcelBillingCalculator.cs b/src/Oms.Application/Services/Orders/ParcelBillingCalculator.cs
new file mode 100644
index 0000000..d9881d6
--- /dev/null
+++ b/src/Oms.Application/Services/Orders/ParcelBillingCalculator.cs
@@ -0,0 +1,597 @@
+using System.Globalization;
+using System.Text.Json;
+using System.Text.RegularExpressions;
+using Oms.Application.Dtos.Channels;
+using Oms.Application.Shared;
+using Oms.Domain.ChannelsAggregate.Entities;
+using Oms.Domain.CustomersAggregate.Enums;
+using Oms.Domain.OrdersAggregate.Consts;
+using Oms.Domain.OrdersAggregate.Entities;
+
+namespace Oms.Application.Services.Orders;
+
+///
+/// 小包预报:计重、分区匹配、卖价算价。
+///
+internal static class ParcelBillingCalculator
+{
+ private static readonly JsonSerializerOptions JsonOptions = new()
+ {
+ PropertyNameCaseInsensitive = true
+ };
+
+ public static decimal CalculateCubic(decimal length, decimal width, decimal height) =>
+ Math.Round(length * width * height / 1_000_000M, 6, MidpointRounding.AwayFromZero);
+
+ public static (decimal VolumetricWeight, decimal RawBillableWeight) CalculateRawBillableWeight(
+ OrderWeight forecast,
+ IReadOnlyList rules)
+ {
+ var length = forecast.Length ?? 0;
+ var width = forecast.Width ?? 0;
+ var height = forecast.Height ?? 0;
+ var gw = forecast.ActualWeight ?? 0;
+ if (gw <= 0 || length <= 0 || width <= 0 || height <= 0)
+ {
+ throw new InvalidOperationException("件重尺数据不完整");
+ }
+
+ if (rules.Count == 0)
+ {
+ throw new InvalidOperationException("计重规则明细为空");
+ }
+
+ foreach (var rule in rules.OrderBy(x => x.Id))
+ {
+ if (!decimal.TryParse(rule.VolumetricFactor, NumberStyles.Any, CultureInfo.InvariantCulture, out var factor) || factor <= 0)
+ {
+ continue;
+ }
+
+ var vw = Math.Round(length * width * height / factor, 3, MidpointRounding.AwayFromZero);
+ if (!EvaluateCondition(rule.Condition, gw, vw))
+ {
+ continue;
+ }
+
+ var raw = string.Equals(rule.BillableWeight, "gw", StringComparison.OrdinalIgnoreCase) ? gw : vw;
+ return (vw, raw);
+ }
+
+ throw new InvalidOperationException("计重规则条件均未命中");
+ }
+
+ public static decimal ApplyRounding(decimal rawWeight, IReadOnlyList ranges)
+ {
+ var hit = ranges
+ .OrderBy(x => x.Id)
+ .FirstOrDefault(x => rawWeight >= x.StartValue && rawWeight <= x.EndValue);
+ if (hit == null)
+ {
+ throw new InvalidOperationException("计重进位区间未匹配");
+ }
+
+ if (!decimal.TryParse(hit.Public, NumberStyles.Any, CultureInfo.InvariantCulture, out var unit) || unit <= 0)
+ {
+ throw new InvalidOperationException("计重进位值无效");
+ }
+
+ var units = Math.Ceiling(rawWeight / unit);
+ return Math.Round(units * unit, 3, MidpointRounding.AwayFromZero);
+ }
+
+ public static bool IsAreaMatched(
+ KpiAreaInfo rule,
+ string countryCode,
+ string? province,
+ string? city,
+ string? zipCode)
+ {
+ if (!string.IsNullOrWhiteSpace(rule.CountryCode)
+ && !string.Equals(rule.CountryCode, countryCode, StringComparison.OrdinalIgnoreCase))
+ {
+ return false;
+ }
+
+ if (!string.IsNullOrWhiteSpace(rule.Province)
+ && !string.Equals(rule.Province, province, StringComparison.OrdinalIgnoreCase))
+ {
+ return false;
+ }
+
+ if (!string.IsNullOrWhiteSpace(rule.City)
+ && !string.Equals(rule.City, city, StringComparison.OrdinalIgnoreCase))
+ {
+ return false;
+ }
+
+ if (!string.IsNullOrWhiteSpace(rule.LeftPostcode) || !string.IsNullOrWhiteSpace(rule.RightPostcode))
+ {
+ if (string.IsNullOrWhiteSpace(zipCode))
+ {
+ return false;
+ }
+
+ if (!IsPostcodeInRange(zipCode, rule.LeftPostcode, rule.RightPostcode))
+ {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ public static string? ResolveZoneName(KpiAreaInfo? matched, KpiArea? area)
+ {
+ if (matched == null)
+ {
+ return null;
+ }
+
+ if (!string.IsNullOrWhiteSpace(matched.Province))
+ {
+ return matched.Province;
+ }
+
+ if (!string.IsNullOrWhiteSpace(matched.City))
+ {
+ return matched.City;
+ }
+
+ if (!string.IsNullOrWhiteSpace(matched.CountryName))
+ {
+ return matched.CountryName;
+ }
+
+ return area?.Name;
+ }
+
+ public static List CalculateForecastReceivableFees(
+ SalesProductPricePublishSnapshotDto priceSnapshot,
+ string priceVersion,
+ DateTime priceEffectiveTime,
+ decimal chargeableWeight,
+ string zoneName,
+ string cargoAttribute,
+ CustomerGroup? customerGroup)
+ {
+ var items = priceSnapshot.Prices.Select(p => new BillingPriceItem(
+ p.Status, p.ChargingTime, p.CostItem, p.Currency, p.Details));
+ return CalculateForecastFees(
+ items,
+ OrderFeeTypeCodes.Receivable,
+ priceVersion,
+ priceEffectiveTime,
+ chargeableWeight,
+ zoneName,
+ cargoAttribute,
+ customerGroup);
+ }
+
+ ///
+ /// 预报阶段应付费用(买价快照,计费重取 downstream 行,调用方传入对应重量)。
+ ///
+ public static List CalculateForecastPayableFees(
+ IReadOnlyList prices,
+ string priceVersion,
+ DateTime priceEffectiveTime,
+ decimal chargeableWeight,
+ string zoneName,
+ string cargoAttribute,
+ CustomerGroup? customerGroup)
+ {
+ var items = prices.Select(p => new BillingPriceItem(
+ p.Status, p.ChargingTime, p.CostItem, p.Currency, p.Details));
+ return CalculateForecastFees(
+ items,
+ OrderFeeTypeCodes.Payable,
+ priceVersion,
+ priceEffectiveTime,
+ chargeableWeight,
+ zoneName,
+ cargoAttribute,
+ customerGroup);
+ }
+
+ public static bool HasEnabledForecastPriceItems(SalesProductPricePublishSnapshotDto priceSnapshot) =>
+ priceSnapshot.Prices.Any(x => x.Status == 1 && IsForecastChargingTime(x.ChargingTime));
+
+ public static bool HasEnabledForecastPayablePriceItems(IReadOnlyList prices) =>
+ prices.Any(x => x.Status == 1 && IsForecastChargingTime(x.ChargingTime));
+
+ public static string NormalizeChargingTime(string? chargingTime)
+ {
+ var value = chargingTime?.Trim();
+ if (string.IsNullOrWhiteSpace(value))
+ {
+ return OrderFeeChargingStageCodes.Forecast;
+ }
+
+ return value switch
+ {
+ "预报" => OrderFeeChargingStageCodes.Forecast,
+ "入库" => OrderFeeChargingStageCodes.Inbound,
+ "出库" => OrderFeeChargingStageCodes.Outbound,
+ _ => value.ToLowerInvariant()
+ };
+ }
+
+ public static string GetCargoAttributeLabel(string? cargoAttribute) => cargoAttribute switch
+ {
+ CargoAttributeCodes.Electronics => "3c&带电",
+ CargoAttributeCodes.Sensitive => "敏感货",
+ _ => "普货"
+ };
+
+ public static string MapCargoTypeToAttribute(string? cargoType)
+ {
+ if (string.IsNullOrWhiteSpace(cargoType))
+ {
+ return CargoAttributeCodes.General;
+ }
+
+ var value = cargoType.Trim();
+ if (value.Contains("敏感", StringComparison.OrdinalIgnoreCase))
+ {
+ return CargoAttributeCodes.Sensitive;
+ }
+
+ if (value.Contains("带电", StringComparison.OrdinalIgnoreCase)
+ || value.Contains("3c", StringComparison.OrdinalIgnoreCase)
+ || value.Equals("electronics", StringComparison.OrdinalIgnoreCase))
+ {
+ return CargoAttributeCodes.Electronics;
+ }
+
+ if (value.Equals(CargoAttributeCodes.General, StringComparison.OrdinalIgnoreCase)
+ || value.Equals(CargoAttributeCodes.Electronics, StringComparison.OrdinalIgnoreCase)
+ || value.Equals(CargoAttributeCodes.Sensitive, StringComparison.OrdinalIgnoreCase))
+ {
+ return value.ToLowerInvariant();
+ }
+
+ return CargoAttributeCodes.General;
+ }
+
+ public static (decimal Freight, decimal OtherFees, decimal TotalCost, string CostFormula) SummarizeFees(
+ IReadOnlyList fees)
+ {
+ if (fees.Count == 0)
+ {
+ return (0, 0, 0, string.Empty);
+ }
+
+ var freight = fees
+ .Where(x => x.CostItem.Equals("freight", StringComparison.OrdinalIgnoreCase)
+ || x.CostItem.Contains("运费", StringComparison.OrdinalIgnoreCase))
+ .Sum(x => x.Amount);
+ if (freight <= 0)
+ {
+ freight = fees[0].Amount;
+ }
+
+ var total = fees.Sum(x => x.Amount);
+ var otherFees = Math.Max(0, total - freight);
+ var formula = string.Join(" + ", fees.Select(x => $"{x.CostItem}:{x.Amount:0.####}"));
+ return (freight, otherFees, total, formula);
+ }
+
+ public static OrderWeight BuildForecastWeight(ParcelQuoteContext context) =>
+ new()
+ {
+ Type = OrderWeightTypeCodes.Forecast,
+ Length = context.Length,
+ Width = context.Width,
+ Height = context.Height,
+ ActualWeight = context.ActualWeight,
+ Cubic = CalculateCubic(context.Length, context.Width, context.Height)
+ };
+
+ private static List CalculateForecastFees(
+ IEnumerable priceItems,
+ string feeType,
+ string priceVersion,
+ DateTime priceEffectiveTime,
+ decimal chargeableWeight,
+ string zoneName,
+ string cargoAttribute,
+ CustomerGroup? customerGroup)
+ {
+ var fees = new List();
+ foreach (var priceItem in priceItems.Where(x => x.Status == 1))
+ {
+ if (!IsForecastChargingTime(priceItem.ChargingTime))
+ {
+ continue;
+ }
+
+ var detail = MatchPriceDetail(priceItem.Details, zoneName, cargoAttribute, chargeableWeight);
+ if (detail == null)
+ {
+ continue;
+ }
+
+ var amount = CalculateDetailAmount(detail, chargeableWeight, customerGroup);
+ fees.Add(new OrderFee
+ {
+ Type = feeType,
+ CostItem = priceItem.CostItem,
+ Amount = amount,
+ Currency = priceItem.Currency,
+ PriceNo = priceVersion,
+ PriceEffectiveTime = priceEffectiveTime,
+ ChargingStage = OrderFeeChargingStageCodes.Forecast
+ });
+ }
+
+ return fees;
+ }
+
+ private readonly record struct BillingPriceItem(
+ byte Status,
+ string? ChargingTime,
+ string CostItem,
+ string Currency,
+ List Details);
+
+ private static bool IsForecastChargingTime(string? chargingTime)
+ {
+ var normalized = NormalizeChargingTime(chargingTime);
+ return normalized is OrderFeeChargingStageCodes.Forecast or "";
+ }
+
+ private static ChannelPriceDetailOutput? MatchPriceDetail(
+ IReadOnlyList details,
+ string zoneName,
+ string cargoAttribute,
+ decimal chargeableWeight)
+ {
+ var cargoLabel = GetCargoAttributeLabel(cargoAttribute);
+ foreach (var detail in details)
+ {
+ var factors = ParseFactorDictionary(detail.FactorValuesJson);
+ if (!MatchFactorValue(factors, "分区", zoneName)
+ && !MatchFactorValue(factors, "价格分区", zoneName))
+ {
+ continue;
+ }
+
+ if (!MatchFactorValue(factors, "货物类型", cargoLabel)
+ && factors.ContainsKey("货物类型"))
+ {
+ continue;
+ }
+
+ if (!MatchWeightRange(factors, chargeableWeight))
+ {
+ continue;
+ }
+
+ return detail;
+ }
+
+ return null;
+ }
+
+ private static Dictionary ParseFactorDictionary(object? factorValuesJson)
+ {
+ if (factorValuesJson == null)
+ {
+ return new Dictionary(StringComparer.OrdinalIgnoreCase);
+ }
+
+ if (factorValuesJson is JsonElement element)
+ {
+ return element.EnumerateObject()
+ .ToDictionary(x => x.Name, x => x.Value.ToString(), StringComparer.OrdinalIgnoreCase);
+ }
+
+ var json = JsonSerializer.Serialize(factorValuesJson, JsonOptions);
+ return JsonSerializer.Deserialize>(json, JsonOptions)
+ ?? new Dictionary(StringComparer.OrdinalIgnoreCase);
+ }
+
+ private static bool MatchFactorValue(Dictionary factors, string key, string expected)
+ {
+ if (!factors.TryGetValue(key, out var actual) || string.IsNullOrWhiteSpace(actual))
+ {
+ return true;
+ }
+
+ return string.Equals(actual.Trim(), expected, StringComparison.OrdinalIgnoreCase);
+ }
+
+ private static bool MatchWeightRange(Dictionary factors, decimal chargeableWeight)
+ {
+ if (!factors.TryGetValue("重量", out var rangeText) || string.IsNullOrWhiteSpace(rangeText))
+ {
+ return true;
+ }
+
+ var parts = rangeText.Split('-', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries);
+ if (parts.Length != 2)
+ {
+ return true;
+ }
+
+ if (!decimal.TryParse(parts[0], NumberStyles.Any, CultureInfo.InvariantCulture, out var start))
+ {
+ return true;
+ }
+
+ if (!decimal.TryParse(parts[1], NumberStyles.Any, CultureInfo.InvariantCulture, out var end))
+ {
+ return true;
+ }
+
+ return chargeableWeight >= start && chargeableWeight <= end;
+ }
+
+ private static decimal CalculateDetailAmount(
+ ChannelPriceDetailOutput detail,
+ decimal chargeableWeight,
+ CustomerGroup? customerGroup)
+ {
+ var formula = detail.CalculationFormula?.Trim() ?? string.Empty;
+ if (formula is "单价" or "固定价")
+ {
+ return ResolveCustomerGroupPrice(detail, customerGroup) ?? detail.CalculatedValue ?? 0;
+ }
+
+ if (formula is "续单价" or "续重价")
+ {
+ return CalculateContinuePrice(detail, chargeableWeight, customerGroup);
+ }
+
+ return detail.CalculatedValue ?? 0;
+ }
+
+ private static decimal CalculateContinuePrice(
+ ChannelPriceDetailOutput detail,
+ decimal chargeableWeight,
+ CustomerGroup? customerGroup)
+ {
+ var factors = ParseFactorDictionary(detail.FactorValuesJson);
+ var continuePricing = TryReadContinuePricing(factors, detail);
+ var firstWeight = continuePricing?.FirstWeight ?? 1M;
+ var firstPrice = continuePricing?.FirstPrice ?? detail.CalculatedValue ?? 0;
+ var continueUnit = continuePricing?.ContinueUnit ?? 0.5M;
+ var continuePrice = continuePricing?.ContinuePrice ?? firstPrice;
+
+ if (customerGroup.HasValue)
+ {
+ var groupPrice = ResolveCustomerGroupPrice(detail, customerGroup);
+ if (groupPrice.HasValue)
+ {
+ firstPrice = groupPrice.Value;
+ }
+ }
+
+ if (chargeableWeight <= firstWeight)
+ {
+ return Math.Round(firstPrice, 4, MidpointRounding.AwayFromZero);
+ }
+
+ var overflow = chargeableWeight - firstWeight;
+ var continueUnits = Math.Ceiling(overflow / continueUnit);
+ return Math.Round(firstPrice + continueUnits * continuePrice, 4, MidpointRounding.AwayFromZero);
+ }
+
+ private static ContinuePricingConfig? TryReadContinuePricing(
+ Dictionary factors,
+ ChannelPriceDetailOutput detail)
+ {
+ if (factors.TryGetValue("continuePricing", out var json) && !string.IsNullOrWhiteSpace(json))
+ {
+ return JsonSerializer.Deserialize(json, JsonOptions);
+ }
+
+ if (detail.FactorValuesJson is JsonElement element
+ && element.TryGetProperty("continuePricing", out var continueNode))
+ {
+ return continueNode.Deserialize(JsonOptions);
+ }
+
+ if (factors.TryGetValue("首重", out var firstWeightText)
+ && decimal.TryParse(firstWeightText, NumberStyles.Any, CultureInfo.InvariantCulture, out var firstWeight))
+ {
+ decimal.TryParse(factors.GetValueOrDefault("首重价"), NumberStyles.Any, CultureInfo.InvariantCulture, out var firstPrice);
+ decimal.TryParse(factors.GetValueOrDefault("续重单位"), NumberStyles.Any, CultureInfo.InvariantCulture, out var continueUnit);
+ decimal.TryParse(factors.GetValueOrDefault("续重价"), NumberStyles.Any, CultureInfo.InvariantCulture, out var continuePrice);
+ return new ContinuePricingConfig
+ {
+ FirstWeight = firstWeight,
+ FirstPrice = firstPrice > 0 ? firstPrice : detail.CalculatedValue ?? 0,
+ ContinueUnit = continueUnit > 0 ? continueUnit : 0.5M,
+ ContinuePrice = continuePrice > 0 ? continuePrice : detail.CalculatedValue ?? 0
+ };
+ }
+
+ return null;
+ }
+
+ private static decimal? ResolveCustomerGroupPrice(ChannelPriceDetailOutput detail, CustomerGroup? customerGroup)
+ {
+ if (!customerGroup.HasValue || detail.CustomerGroupValues == null)
+ {
+ return null;
+ }
+
+ var json = JsonSerializer.Serialize(detail.CustomerGroupValues, JsonOptions);
+ var map = JsonSerializer.Deserialize>(json, JsonOptions);
+ if (map == null)
+ {
+ return null;
+ }
+
+ var key = customerGroup.Value switch
+ {
+ CustomerGroup.Vip1 => "客户组1",
+ CustomerGroup.Vip2 => "客户组2",
+ CustomerGroup.Vip3 => "客户组3",
+ CustomerGroup.Vip4 => "客户组4",
+ CustomerGroup.Vip5 => "客户组5",
+ _ => null
+ };
+ if (key != null && map.TryGetValue(key, out var value))
+ {
+ return value;
+ }
+
+ return null;
+ }
+
+ private static bool EvaluateCondition(string condition, decimal gw, decimal vw)
+ {
+ if (string.IsNullOrWhiteSpace(condition))
+ {
+ return true;
+ }
+
+ var expr = Regex.Replace(condition.Trim(), @"\bgw\b", gw.ToString(CultureInfo.InvariantCulture), RegexOptions.IgnoreCase);
+ expr = Regex.Replace(expr, @"\bvw\b", vw.ToString(CultureInfo.InvariantCulture), RegexOptions.IgnoreCase);
+ expr = expr.Replace("==", "=");
+
+ try
+ {
+ var result = new System.Data.DataTable().Compute(expr, string.Empty);
+ return result is bool boolResult && boolResult
+ || result is int intResult && intResult != 0
+ || result is decimal decimalResult && decimalResult != 0
+ || result is double doubleResult && Math.Abs(doubleResult) > double.Epsilon;
+ }
+ catch
+ {
+ return false;
+ }
+ }
+
+ private static bool IsPostcodeInRange(string zipCode, string? left, string? right)
+ {
+ if (string.IsNullOrWhiteSpace(left) && string.IsNullOrWhiteSpace(right))
+ {
+ return true;
+ }
+
+ if (long.TryParse(zipCode, out var zip)
+ && long.TryParse(left, out var leftValue)
+ && long.TryParse(right, out var rightValue))
+ {
+ return zip >= leftValue && zip <= rightValue;
+ }
+
+ return string.Compare(zipCode, left, StringComparison.OrdinalIgnoreCase) >= 0
+ && string.Compare(zipCode, right, StringComparison.OrdinalIgnoreCase) <= 0;
+ }
+
+ private sealed class ContinuePricingConfig
+ {
+ public decimal FirstWeight { get; set; }
+
+ public decimal FirstPrice { get; set; }
+
+ public decimal ContinueUnit { get; set; }
+
+ public decimal ContinuePrice { get; set; }
+ }
+}
diff --git a/src/Oms.Application/Services/Orders/ParcelQuoteContext.cs b/src/Oms.Application/Services/Orders/ParcelQuoteContext.cs
new file mode 100644
index 0000000..6f9d410
--- /dev/null
+++ b/src/Oms.Application/Services/Orders/ParcelQuoteContext.cs
@@ -0,0 +1,25 @@
+namespace Oms.Application.Services.Orders;
+
+///
+/// 查价/试算用的地址与件重尺上下文(不落库)。
+///
+public sealed class ParcelQuoteContext
+{
+ public string CountryCode { get; init; } = string.Empty;
+
+ public string? Province { get; init; }
+
+ public string? City { get; init; }
+
+ public string? PostalCode { get; init; }
+
+ public string CargoAttribute { get; init; } = string.Empty;
+
+ public decimal Length { get; init; }
+
+ public decimal Width { get; init; }
+
+ public decimal Height { get; init; }
+
+ public decimal ActualWeight { get; init; }
+}
diff --git a/src/Oms.Application/Services/Orders/ParcelQuoteResult.cs b/src/Oms.Application/Services/Orders/ParcelQuoteResult.cs
new file mode 100644
index 0000000..1624c5c
--- /dev/null
+++ b/src/Oms.Application/Services/Orders/ParcelQuoteResult.cs
@@ -0,0 +1,29 @@
+using Oms.Domain.OrdersAggregate.Entities;
+
+namespace Oms.Application.Services.Orders;
+
+///
+/// 单产品销售产品试算结果(与 parcel/create 预报计费同源)。
+///
+public sealed class ParcelQuoteResult
+{
+ public int ProductId { get; init; }
+
+ public string ProductCode { get; init; } = string.Empty;
+
+ public string ProductName { get; init; } = string.Empty;
+
+ public string RulesVersion { get; init; } = string.Empty;
+
+ public string PriceVersion { get; init; } = string.Empty;
+
+ public decimal VolumetricWeight { get; init; }
+
+ public decimal ChargeableWeight { get; init; }
+
+ public string? ZoneName { get; init; }
+
+ public bool IsRemote { get; init; }
+
+ public IReadOnlyList Fees { get; init; } = [];
+}
diff --git a/src/Oms.Domain/ChannelsAggregate/Repositories/IKpiAreaRepository.cs b/src/Oms.Domain/ChannelsAggregate/Repositories/IKpiAreaRepository.cs
index 9238fcc..5b3c230 100644
--- a/src/Oms.Domain/ChannelsAggregate/Repositories/IKpiAreaRepository.cs
+++ b/src/Oms.Domain/ChannelsAggregate/Repositories/IKpiAreaRepository.cs
@@ -20,4 +20,6 @@ public interface IKpiAreaRepository : IRepository
Task UpdateInfoAsync(KpiAreaInfo info, CancellationToken cancellationToken = default);
Task DeleteInfoAsync(int id, CancellationToken cancellationToken = default);
+
+ Task> GetAllInfosByAreaIdAsync(int areaId, CancellationToken cancellationToken = default);
}
diff --git a/src/Oms.Domain/ChannelsAggregate/Repositories/ISalesProductRepository.cs b/src/Oms.Domain/ChannelsAggregate/Repositories/ISalesProductRepository.cs
index e37d729..b8906e5 100644
--- a/src/Oms.Domain/ChannelsAggregate/Repositories/ISalesProductRepository.cs
+++ b/src/Oms.Domain/ChannelsAggregate/Repositories/ISalesProductRepository.cs
@@ -51,4 +51,10 @@ public interface ISalesProductRepository : IRepository
Task InsertPublishRecordAsync(SalesProductPublishRecord record, CancellationToken cancellationToken = default);
Task GetPublishRecordByIdAsync(int id, CancellationToken cancellationToken = default);
+
+ Task GetEffectivePublishRecordAsync(
+ int salesProductId,
+ string operatorType,
+ DateTime bindTime,
+ CancellationToken cancellationToken = default);
}
diff --git a/src/Oms.Domain/ChannelsAggregate/Repositories/IWeightRoundingRepository.cs b/src/Oms.Domain/ChannelsAggregate/Repositories/IWeightRoundingRepository.cs
index de631ad..eb8f562 100644
--- a/src/Oms.Domain/ChannelsAggregate/Repositories/IWeightRoundingRepository.cs
+++ b/src/Oms.Domain/ChannelsAggregate/Repositories/IWeightRoundingRepository.cs
@@ -20,4 +20,6 @@ public interface IWeightRoundingRepository : IRepository
Task UpdateInfoAsync(WeightRoundingInfo info, CancellationToken cancellationToken = default);
Task DeleteInfoAsync(int id, CancellationToken cancellationToken = default);
+
+ Task> GetAllInfosByRoundingIdAsync(int roundingId, CancellationToken cancellationToken = default);
}
diff --git a/src/Oms.Domain/ChannelsAggregate/Repositories/IWeightRuleRepository.cs b/src/Oms.Domain/ChannelsAggregate/Repositories/IWeightRuleRepository.cs
index 99a8d65..ea446d5 100644
--- a/src/Oms.Domain/ChannelsAggregate/Repositories/IWeightRuleRepository.cs
+++ b/src/Oms.Domain/ChannelsAggregate/Repositories/IWeightRuleRepository.cs
@@ -20,4 +20,6 @@ public interface IWeightRuleRepository : IRepository
Task UpdateInfoAsync(WeightRuleInfo info, CancellationToken cancellationToken = default);
Task DeleteInfoAsync(int id, CancellationToken cancellationToken = default);
+
+ Task> GetAllInfosByRuleIdAsync(int ruleId, CancellationToken cancellationToken = default);
}
diff --git a/src/Oms.Domain/OrdersAggregate/Consts/OrderFeeTypeCodes.cs b/src/Oms.Domain/OrdersAggregate/Consts/OrderFeeTypeCodes.cs
new file mode 100644
index 0000000..0cb6040
--- /dev/null
+++ b/src/Oms.Domain/OrdersAggregate/Consts/OrderFeeTypeCodes.cs
@@ -0,0 +1,34 @@
+namespace Oms.Domain.OrdersAggregate.Consts;
+
+public static class OrderFeeTypeCodes
+{
+ public const string Receivable = "receivable";
+ public const string Payable = "payable";
+}
+
+public static class OrderFeeChargingStageCodes
+{
+ public const string Forecast = "forecast";
+ public const string Inbound = "inbound";
+ public const string Outbound = "outbound";
+}
+
+public static class ExportCustomsModeCodes
+{
+ public const string Mode9610Simple = "9610Simple";
+ public const string Mode9610Summary = "9610Summary";
+ public const string Mode9610Separate = "9610Separate";
+ public const string Mode9710 = "9710";
+ public const string Mode9810 = "9810";
+ public const string Mode0110 = "0110";
+
+ public static readonly HashSet All =
+ [
+ Mode9610Simple,
+ Mode9610Summary,
+ Mode9610Separate,
+ Mode9710,
+ Mode9810,
+ Mode0110
+ ];
+}
diff --git a/src/Oms.Domain/OrdersAggregate/Entities/Order.cs b/src/Oms.Domain/OrdersAggregate/Entities/Order.cs
index cd3db45..fa0c6cf 100644
--- a/src/Oms.Domain/OrdersAggregate/Entities/Order.cs
+++ b/src/Oms.Domain/OrdersAggregate/Entities/Order.cs
@@ -22,6 +22,14 @@ public class Order : OmsAuditedAggregateRoot
public string? CargoAttribute { get; set; }
+ public string? ExportCustomsMode { get; set; }
+
+ public string? PredeclareFailReason { get; set; }
+
+ public string? SalesProductRulesVersion { get; set; }
+
+ public string? SalesProductPriceVersion { get; set; }
+
public string OrderStatus { get; set; } = string.Empty;
public int? WarehouseId { get; set; }
diff --git a/src/Oms.Domain/OrdersAggregate/Entities/OrderFee.cs b/src/Oms.Domain/OrdersAggregate/Entities/OrderFee.cs
index 00ed1e9..374c1e6 100644
--- a/src/Oms.Domain/OrdersAggregate/Entities/OrderFee.cs
+++ b/src/Oms.Domain/OrdersAggregate/Entities/OrderFee.cs
@@ -21,4 +21,6 @@ public class OrderFee : OmsAuditedEntity
public string? PriceNo { get; set; }
public DateTime? PriceEffectiveTime { get; set; }
+
+ public string? ChargingStage { get; set; }
}
diff --git a/src/Oms.Domain/OrdersAggregate/Repositories/IOrderRepository.cs b/src/Oms.Domain/OrdersAggregate/Repositories/IOrderRepository.cs
index 5048071..7252a22 100644
--- a/src/Oms.Domain/OrdersAggregate/Repositories/IOrderRepository.cs
+++ b/src/Oms.Domain/OrdersAggregate/Repositories/IOrderRepository.cs
@@ -30,4 +30,10 @@ public interface IOrderRepository : IRepository
Task InsertDeclarationsAsync(IEnumerable declarations, CancellationToken cancellationToken = default);
Task InsertWeightsAsync(IEnumerable weights, CancellationToken cancellationToken = default);
+
+ Task ReplaceWeightsAsync(int orderId, IEnumerable weights, CancellationToken cancellationToken = default);
+
+ Task UpsertFeesAsync(int orderId, IEnumerable fees, CancellationToken cancellationToken = default);
+
+ Task> GetFeesByOrderIdsAsync(IEnumerable orderIds, CancellationToken cancellationToken = default);
}
diff --git a/src/Oms.SqlSugarCore/Channels/KpiAreaRepository.cs b/src/Oms.SqlSugarCore/Channels/KpiAreaRepository.cs
index 023c3e3..53ba77e 100644
--- a/src/Oms.SqlSugarCore/Channels/KpiAreaRepository.cs
+++ b/src/Oms.SqlSugarCore/Channels/KpiAreaRepository.cs
@@ -55,4 +55,13 @@ public class KpiAreaRepository : OmsSqlSugarRepository, IKpiAreaReposit
var db = await GetDbContextAsync(cancellationToken);
await db.Deleteable().Where(x => x.Id == id).ExecuteCommandAsync(cancellationToken);
}
+
+ public async Task> GetAllInfosByAreaIdAsync(int areaId, CancellationToken cancellationToken = default)
+ {
+ var db = await GetDbContextAsync(cancellationToken);
+ return await db.Queryable()
+ .Where(x => x.AreaId == areaId)
+ .OrderBy(x => x.Id)
+ .ToListAsync(cancellationToken);
+ }
}
diff --git a/src/Oms.SqlSugarCore/Channels/SalesProductRepository.cs b/src/Oms.SqlSugarCore/Channels/SalesProductRepository.cs
index 00aabee..7526450 100644
--- a/src/Oms.SqlSugarCore/Channels/SalesProductRepository.cs
+++ b/src/Oms.SqlSugarCore/Channels/SalesProductRepository.cs
@@ -195,6 +195,24 @@ public class SalesProductRepository : OmsSqlSugarRepository, ISale
return await db.Queryable().FirstAsync(x => x.Id == id, cancellationToken);
}
+ public async Task GetEffectivePublishRecordAsync(
+ int salesProductId,
+ string operatorType,
+ DateTime bindTime,
+ CancellationToken cancellationToken = default)
+ {
+ var db = await GetDbContextAsync(cancellationToken);
+ var records = await db.Queryable()
+ .Where(x => x.SalesProductId == salesProductId
+ && x.OperatorType == operatorType
+ && x.Status == "生效"
+ && x.PublishTime <= bindTime)
+ .OrderBy(x => x.PublishTime, SqlSugar.OrderByType.Desc)
+ .Take(1)
+ .ToListAsync(cancellationToken);
+ return records.FirstOrDefault();
+ }
+
private static async Task InsertPriceChildrenAsync(
SqlSugar.ISqlSugarClient db,
int priceId,
diff --git a/src/Oms.SqlSugarCore/Channels/WeightRoundingRepository.cs b/src/Oms.SqlSugarCore/Channels/WeightRoundingRepository.cs
index 91a1cf2..de02d4a 100644
--- a/src/Oms.SqlSugarCore/Channels/WeightRoundingRepository.cs
+++ b/src/Oms.SqlSugarCore/Channels/WeightRoundingRepository.cs
@@ -55,4 +55,13 @@ public class WeightRoundingRepository : OmsSqlSugarRepository, I
var db = await GetDbContextAsync(cancellationToken);
await db.Deleteable().Where(x => x.Id == id).ExecuteCommandAsync(cancellationToken);
}
+
+ public async Task> GetAllInfosByRoundingIdAsync(int roundingId, CancellationToken cancellationToken = default)
+ {
+ var db = await GetDbContextAsync(cancellationToken);
+ return await db.Queryable()
+ .Where(x => x.RoundingId == roundingId)
+ .OrderBy(x => x.Id)
+ .ToListAsync(cancellationToken);
+ }
}
diff --git a/src/Oms.SqlSugarCore/Channels/WeightRuleRepository.cs b/src/Oms.SqlSugarCore/Channels/WeightRuleRepository.cs
index 47e0d05..313ce39 100644
--- a/src/Oms.SqlSugarCore/Channels/WeightRuleRepository.cs
+++ b/src/Oms.SqlSugarCore/Channels/WeightRuleRepository.cs
@@ -55,4 +55,13 @@ public class WeightRuleRepository : OmsSqlSugarRepository, IWeightRu
var db = await GetDbContextAsync(cancellationToken);
await db.Deleteable().Where(x => x.Id == id).ExecuteCommandAsync(cancellationToken);
}
+
+ public async Task> GetAllInfosByRuleIdAsync(int ruleId, CancellationToken cancellationToken = default)
+ {
+ var db = await GetDbContextAsync(cancellationToken);
+ return await db.Queryable()
+ .Where(x => x.RuleId == ruleId)
+ .OrderBy(x => x.Id)
+ .ToListAsync(cancellationToken);
+ }
}
diff --git a/src/Oms.SqlSugarCore/Orders/OrderRepository.cs b/src/Oms.SqlSugarCore/Orders/OrderRepository.cs
index 6a535a8..b607128 100644
--- a/src/Oms.SqlSugarCore/Orders/OrderRepository.cs
+++ b/src/Oms.SqlSugarCore/Orders/OrderRepository.cs
@@ -123,4 +123,65 @@ public class OrderRepository : OmsSqlSugarRepository, IOrderRepository, I
var db = await GetDbContextAsync(cancellationToken);
await db.Insertable(items).ExecuteCommandAsync(cancellationToken);
}
+
+ public async Task ReplaceWeightsAsync(int orderId, IEnumerable weights, CancellationToken cancellationToken = default)
+ {
+ var items = weights.ToArray();
+ var db = await GetDbContextAsync(cancellationToken);
+ await db.Deleteable().Where(x => x.OrderId == orderId).ExecuteCommandAsync(cancellationToken);
+ if (items.Length > 0)
+ {
+ await db.Insertable(items).ExecuteCommandAsync(cancellationToken);
+ }
+ }
+
+ public async Task UpsertFeesAsync(int orderId, IEnumerable fees, CancellationToken cancellationToken = default)
+ {
+ var items = fees.ToArray();
+ if (items.Length == 0)
+ {
+ return;
+ }
+
+ var db = await GetDbContextAsync(cancellationToken);
+ foreach (var fee in items)
+ {
+ fee.OrderId = orderId;
+ var existingList = await db.Queryable()
+ .Where(x => x.OrderId == orderId
+ && x.Type == fee.Type
+ && x.CostItem == fee.CostItem
+ && x.ChargingStage == fee.ChargingStage)
+ .Take(1)
+ .ToListAsync(cancellationToken);
+ var existing = existingList.FirstOrDefault();
+ if (existing == null)
+ {
+ await db.Insertable(fee).ExecuteCommandAsync(cancellationToken);
+ continue;
+ }
+
+ existing.Amount = fee.Amount;
+ existing.Currency = fee.Currency;
+ existing.PriceNo = fee.PriceNo;
+ existing.PriceEffectiveTime = fee.PriceEffectiveTime;
+ existing.DownstreamProvider = fee.DownstreamProvider;
+ existing.UpdateTime = DateTime.Now;
+ await db.Updateable(existing).ExecuteCommandAsync(cancellationToken);
+ }
+ }
+
+ public async Task> GetFeesByOrderIdsAsync(IEnumerable orderIds, CancellationToken cancellationToken = default)
+ {
+ var ids = orderIds.Distinct().ToArray();
+ if (ids.Length == 0)
+ {
+ return [];
+ }
+
+ var db = await GetDbContextAsync(cancellationToken);
+ return await db.Queryable()
+ .Where(x => ids.Contains(x.OrderId))
+ .ToListAsync(cancellationToken);
+ }
}
diff --git a/src/Oms.Tests/ParcelBillingCalculatorTests.cs b/src/Oms.Tests/ParcelBillingCalculatorTests.cs
new file mode 100644
index 0000000..9a0b596
--- /dev/null
+++ b/src/Oms.Tests/ParcelBillingCalculatorTests.cs
@@ -0,0 +1,258 @@
+using FluentAssertions;
+using Oms.Application.Dtos.Orders;
+using Oms.Application.Services.Orders;
+using Oms.Domain.CustomersAggregate.Enums;
+using Oms.Domain.OrdersAggregate.Consts;
+using Oms.Domain.OrdersAggregate.Entities;
+using Oms.Tests.Support;
+
+namespace Oms.Tests;
+
+///
+/// 小包预报应收/应付计费单元测试(与设计文档 §4.4.3.1 续单价示例对齐)。
+///
+public class ParcelBillingCalculatorTests
+{
+ private const string Zone = "美西";
+ private readonly (string PriceVersion, DateTime EffectiveTime) _meta = ParcelBillingTestData.SnapshotMeta();
+
+ #region 计重
+
+ [Fact]
+ public void CalculateRawBillableWeight_ShouldUseGrossWeightWhenConditionHits()
+ {
+ var forecast = new OrderWeight
+ {
+ Length = 30,
+ Width = 20,
+ Height = 10,
+ ActualWeight = 1.5M
+ };
+ var rules = new[] { ParcelBillingTestData.GwRule() };
+
+ var (vw, raw) = ParcelBillingCalculator.CalculateRawBillableWeight(forecast, rules);
+
+ vw.Should().Be(1M);
+ raw.Should().Be(1.5M);
+ }
+
+ [Fact]
+ public void ApplyRounding_ShouldCeilToConfiguredUnit()
+ {
+ var ranges = new[] { ParcelBillingTestData.RoundingRange(0, 30, "0.5") };
+
+ var rounded = ParcelBillingCalculator.ApplyRounding(1.23M, ranges);
+
+ rounded.Should().Be(1.5M);
+ }
+
+ #endregion
+
+ #region 应收(卖价)
+
+ [Theory]
+ [InlineData(0.8, 18)]
+ [InlineData(1.2, 24)]
+ [InlineData(1.6, 30)]
+ public void CalculateForecastReceivableFees_ContinuePrice_ShouldMatchDesignExamples(
+ double chargeableWeight,
+ decimal expectedFreight)
+ {
+ var snapshot = ParcelBillingTestData.BuildReceivableSnapshot(
+ ParcelBillingTestData.ReceivablePriceItem(
+ "预报",
+ 1,
+ "freight",
+ ParcelBillingTestData.ContinueFreightDetail()));
+
+ var fees = ParcelBillingCalculator.CalculateForecastReceivableFees(
+ snapshot,
+ _meta.PriceVersion,
+ _meta.EffectiveTime,
+ (decimal)chargeableWeight,
+ Zone,
+ "general",
+ null);
+
+ fees.Should().ContainSingle();
+ fees[0].Type.Should().Be(OrderFeeTypeCodes.Receivable);
+ fees[0].CostItem.Should().Be("freight");
+ fees[0].Amount.Should().Be(expectedFreight);
+ fees[0].ChargingStage.Should().Be(OrderFeeChargingStageCodes.Forecast);
+ fees[0].PriceNo.Should().Be(_meta.PriceVersion);
+ }
+
+ [Theory]
+ [InlineData("入库")]
+ [InlineData("出库")]
+ [InlineData("inbound")]
+ [InlineData("outbound")]
+ public void CalculateForecastReceivableFees_ShouldSkipNonForecastChargingTime(string chargingTime)
+ {
+ var snapshot = ParcelBillingTestData.BuildReceivableSnapshot(
+ ParcelBillingTestData.ReceivablePriceItem(
+ chargingTime,
+ 1,
+ "freight",
+ ParcelBillingTestData.ContinueFreightDetail()));
+
+ var fees = ParcelBillingCalculator.CalculateForecastReceivableFees(
+ snapshot,
+ _meta.PriceVersion,
+ _meta.EffectiveTime,
+ 1.2M,
+ Zone,
+ "general",
+ null);
+
+ fees.Should().BeEmpty();
+ }
+
+ [Fact]
+ public void CalculateForecastReceivableFees_ShouldIncludeForecastAndFixedPriceItems()
+ {
+ var snapshot = ParcelBillingTestData.BuildReceivableSnapshot(
+ ParcelBillingTestData.ReceivablePriceItem(
+ "预报",
+ 1,
+ "freight",
+ ParcelBillingTestData.ContinueFreightDetail()),
+ ParcelBillingTestData.ReceivablePriceItem(
+ "预报",
+ 1,
+ "操作费",
+ ParcelBillingTestData.FixedPriceDetail(5M)));
+
+ var fees = ParcelBillingCalculator.CalculateForecastReceivableFees(
+ snapshot,
+ _meta.PriceVersion,
+ _meta.EffectiveTime,
+ 0.8M,
+ Zone,
+ "general",
+ null);
+
+ fees.Should().HaveCount(2);
+ fees.Should().Contain(x => x.CostItem == "freight" && x.Amount == 18M);
+ fees.Should().Contain(x => x.CostItem == "操作费" && x.Amount == 5M);
+
+ var summary = ParcelBillingCalculator.SummarizeFees(fees);
+ summary.Freight.Should().Be(18M);
+ summary.OtherFees.Should().Be(5M);
+ summary.TotalCost.Should().Be(23M);
+ }
+
+ [Fact]
+ public void CalculateForecastReceivableFees_ShouldApplyCustomerGroupPrice()
+ {
+ var detail = ParcelBillingTestData.UnitPriceDetail(20M);
+ detail.CustomerGroupValues = new Dictionary { ["客户组1"] = 15M };
+
+ var snapshot = ParcelBillingTestData.BuildReceivableSnapshot(
+ ParcelBillingTestData.ReceivablePriceItem("预报", 1, "freight", detail));
+
+ var fees = ParcelBillingCalculator.CalculateForecastReceivableFees(
+ snapshot,
+ _meta.PriceVersion,
+ _meta.EffectiveTime,
+ 2M,
+ Zone,
+ "general",
+ CustomerGroup.Vip1);
+
+ fees.Should().ContainSingle();
+ fees[0].Amount.Should().Be(15M);
+ }
+
+ [Fact]
+ public void HasEnabledForecastPriceItems_ShouldDetectForecastEnabledItems()
+ {
+ var snapshot = ParcelBillingTestData.BuildReceivableSnapshot(
+ ParcelBillingTestData.ReceivablePriceItem("入库", 1, "freight", ParcelBillingTestData.ContinueFreightDetail()),
+ ParcelBillingTestData.ReceivablePriceItem("预报", 1, "freight", ParcelBillingTestData.ContinueFreightDetail()));
+
+ ParcelBillingCalculator.HasEnabledForecastPriceItems(snapshot).Should().BeTrue();
+ }
+
+ #endregion
+
+ #region 应付(买价)
+
+ [Theory]
+ [InlineData(0.8, 12)]
+ [InlineData(1.2, 16)]
+ [InlineData(1.6, 20)]
+ public void CalculateForecastPayableFees_ContinuePrice_ShouldUseSameEngine(
+ double chargeableWeight,
+ decimal expectedFreight)
+ {
+ var prices = ParcelBillingTestData.BuildPayablePrices(
+ ParcelBillingTestData.PayablePriceItem(
+ "预报",
+ 1,
+ "freight",
+ ParcelBillingTestData.ContinueFreightDetail(
+ firstWeight: 1M,
+ firstPrice: 12M,
+ continueUnit: 0.5M,
+ continuePrice: 4M)));
+
+ var fees = ParcelBillingCalculator.CalculateForecastPayableFees(
+ prices,
+ _meta.PriceVersion,
+ _meta.EffectiveTime,
+ (decimal)chargeableWeight,
+ Zone,
+ "general",
+ null);
+
+ fees.Should().ContainSingle();
+ fees[0].Type.Should().Be(OrderFeeTypeCodes.Payable);
+ fees[0].Amount.Should().Be(expectedFreight);
+ fees[0].ChargingStage.Should().Be(OrderFeeChargingStageCodes.Forecast);
+ }
+
+ [Fact]
+ public void CalculateForecastPayableFees_ShouldSkipDisabledPriceItems()
+ {
+ var prices = ParcelBillingTestData.BuildPayablePrices(
+ ParcelBillingTestData.PayablePriceItem(
+ "预报",
+ 0,
+ "freight",
+ ParcelBillingTestData.ContinueFreightDetail()));
+
+ var fees = ParcelBillingCalculator.CalculateForecastPayableFees(
+ prices,
+ _meta.PriceVersion,
+ _meta.EffectiveTime,
+ 1.2M,
+ Zone,
+ "general",
+ null);
+
+ fees.Should().BeEmpty();
+ ParcelBillingCalculator.HasEnabledForecastPayablePriceItems(prices).Should().BeFalse();
+ }
+
+ [Fact]
+ public void ReceivableAndPayable_ShouldDifferByTypeButShareZoneMatching()
+ {
+ var detail = ParcelBillingTestData.ContinueFreightDetail();
+ var snapshot = ParcelBillingTestData.BuildReceivableSnapshot(
+ ParcelBillingTestData.ReceivablePriceItem("预报", 1, "freight", detail));
+ var payablePrices = ParcelBillingTestData.BuildPayablePrices(
+ ParcelBillingTestData.PayablePriceItem("预报", 1, "freight", detail));
+
+ var receivable = ParcelBillingCalculator.CalculateForecastReceivableFees(
+ snapshot, _meta.PriceVersion, _meta.EffectiveTime, 1.2M, Zone, "general", null);
+ var payable = ParcelBillingCalculator.CalculateForecastPayableFees(
+ payablePrices, _meta.PriceVersion, _meta.EffectiveTime, 1.2M, Zone, "general", null);
+
+ receivable[0].Type.Should().Be(OrderFeeTypeCodes.Receivable);
+ payable[0].Type.Should().Be(OrderFeeTypeCodes.Payable);
+ receivable[0].Amount.Should().Be(payable[0].Amount);
+ }
+
+ #endregion
+}
diff --git a/src/Oms.Tests/Support/ParcelBillingTestData.cs b/src/Oms.Tests/Support/ParcelBillingTestData.cs
new file mode 100644
index 0000000..4be347a
--- /dev/null
+++ b/src/Oms.Tests/Support/ParcelBillingTestData.cs
@@ -0,0 +1,114 @@
+using System.Globalization;
+using Oms.Application.Dtos.Channels;
+using Oms.Application.Dtos.Orders;
+using Oms.Domain.ChannelsAggregate.Entities;
+
+namespace Oms.Tests.Support;
+
+internal static class ParcelBillingTestData
+{
+ private static readonly DateTime EffectiveTime = new(2026, 4, 1, 0, 0, 0, DateTimeKind.Utc);
+ private const string PriceVersion = "P20260401001";
+ private const string Zone = "美西";
+
+ public static SalesProductPricePublishSnapshotDto BuildReceivableSnapshot(
+ params SalesProductPriceListOutput[] prices) =>
+ new() { SalesProductId = 1, Prices = prices.ToList() };
+
+ public static List BuildPayablePrices(
+ params ServiceChannelPriceListOutput[] prices) => prices.ToList();
+
+ public static SalesProductPriceListOutput ReceivablePriceItem(
+ string chargingTime,
+ byte status,
+ string costItem,
+ params ChannelPriceDetailOutput[] details) =>
+ new()
+ {
+ Status = status,
+ CostItem = costItem,
+ Currency = "CNY",
+ ChargingTime = chargingTime,
+ Details = details.ToList()
+ };
+
+ public static ServiceChannelPriceListOutput PayablePriceItem(
+ string chargingTime,
+ byte status,
+ string costItem,
+ params ChannelPriceDetailOutput[] details) =>
+ new()
+ {
+ Status = status,
+ CostItem = costItem,
+ Currency = "CNY",
+ ChargingTime = chargingTime,
+ Details = details.ToList()
+ };
+
+ public static ChannelPriceDetailOutput ContinueFreightDetail(
+ decimal firstWeight = 1M,
+ decimal firstPrice = 18M,
+ decimal continueUnit = 0.5M,
+ decimal continuePrice = 6M,
+ string zone = Zone,
+ string cargoType = "普货") =>
+ new()
+ {
+ CalculationFormula = "续单价",
+ FactorValuesJson = new Dictionary
+ {
+ ["分区"] = zone,
+ ["货物类型"] = cargoType,
+ ["首重"] = firstWeight.ToString(CultureInfo.InvariantCulture),
+ ["首重价"] = firstPrice.ToString(CultureInfo.InvariantCulture),
+ ["续重单位"] = continueUnit.ToString(CultureInfo.InvariantCulture),
+ ["续重价"] = continuePrice.ToString(CultureInfo.InvariantCulture)
+ }
+ };
+
+ public static ChannelPriceDetailOutput FixedPriceDetail(
+ decimal amount,
+ string zone = Zone,
+ string cargoType = "普货") =>
+ new()
+ {
+ CalculationFormula = "固定价",
+ CalculatedValue = amount,
+ FactorValuesJson = new Dictionary
+ {
+ ["分区"] = zone,
+ ["货物类型"] = cargoType
+ }
+ };
+
+ public static ChannelPriceDetailOutput UnitPriceDetail(
+ decimal unitPrice,
+ string zone = Zone) =>
+ new()
+ {
+ CalculationFormula = "单价",
+ CalculatedValue = unitPrice,
+ FactorValuesJson = new Dictionary { ["分区"] = zone }
+ };
+
+ public static WeightRuleInfo GwRule(string condition = "gw>=vw", string factor = "6000") =>
+ new()
+ {
+ Id = 1,
+ Condition = condition,
+ BillableWeight = "gw",
+ VolumetricFactor = factor
+ };
+
+ public static WeightRoundingInfo RoundingRange(decimal start, decimal end, string unit) =>
+ new()
+ {
+ Id = 1,
+ StartValue = start,
+ EndValue = end,
+ Public = unit
+ };
+
+ public static (string PriceVersion, DateTime EffectiveTime) SnapshotMeta() => (PriceVersion, EffectiveTime);
+}
diff --git a/src/SimpleAdmin.Api/Auth/AuthRequestDtos.cs b/src/SimpleAdmin.Api/Auth/AuthRequestDtos.cs
index c113ed9..fdc6ebd 100644
--- a/src/SimpleAdmin.Api/Auth/AuthRequestDtos.cs
+++ b/src/SimpleAdmin.Api/Auth/AuthRequestDtos.cs
@@ -25,6 +25,12 @@ public class OmsRefreshTokenInput
public string RefreshToken { get; set; } = string.Empty;
}
+public class OmsWxLoginInput
+{
+ [Required]
+ public string LoginUuid { get; set; } = string.Empty;
+}
+
public class OmsChangePasswordInput
{
[Required]
diff --git a/src/SimpleAdmin.Api/Auth/AuthService.cs b/src/SimpleAdmin.Api/Auth/AuthService.cs
index d86a777..48b08aa 100644
--- a/src/SimpleAdmin.Api/Auth/AuthService.cs
+++ b/src/SimpleAdmin.Api/Auth/AuthService.cs
@@ -18,6 +18,7 @@ using Oms.Application.Shared;
using Oms.Domain.CustomersAggregate.Entities;
using Oms.Domain.CustomersAggregate.Enums;
using Oms.Domain.CustomersAggregate.Repositories;
+using SimpleAdmin.Api.WeChat.Services;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.Processing;
@@ -37,7 +38,8 @@ public class AuthService(
IWebHostEnvironment webHostEnvironment,
IHttpContextAccessor httpContextAccessor,
FakeRbacDbContext rbacDbContext,
- ICustomerRepository customerRepository)
+ ICustomerRepository customerRepository,
+ IWeChatLoginSessionStore weChatLoginSessionStore)
: ApplicationService
{
private Guid CurrentUserGuid => CurrentUser.FindClaimValueOrNull(ClaimTypes.NameIdentifier)!.To();
@@ -60,6 +62,56 @@ public class AuthService(
};
}
+ ///
+ /// 微信扫码登录(轮询)。
+ ///
+ [AllowAnonymous]
+ [HttpPost("/rbac/auth/wx-login")]
+ public async Task WxLoginAsync(
+ [FromBody] OmsWxLoginInput input,
+ CancellationToken cancellationToken = default)
+ {
+ if (string.IsNullOrWhiteSpace(input.LoginUuid))
+ {
+ throw new BusinessException("缺少 loginUuid");
+ }
+
+ var loginUuid = input.LoginUuid.Trim();
+ var openId = await weChatLoginSessionStore.GetOpenIdAsync(loginUuid, cancellationToken);
+ if (string.IsNullOrWhiteSpace(openId))
+ {
+ throw new BusinessException("请扫码登录");
+ }
+
+ var customer = await customerRepository.GetByWechatOpenidAsync(openId, cancellationToken);
+ if (customer == null)
+ {
+ throw new BusinessException("未绑定微信,请先注册或绑定账号");
+ }
+
+ if (customer.ParentCustomerId.HasValue)
+ {
+ throw new BusinessException("子客户不支持微信扫码登录");
+ }
+
+ ValidateMainCustomerStatus(customer);
+
+ customer.LastLoginIp = httpContextAccessor.HttpContext?.Connection.RemoteIpAddress?.ToString();
+ customer.LastLoginTime = DateTime.Now;
+ await customerRepository.UpdateAsync(customer, cancellationToken);
+
+ var userGuid = await GetRbacUserGuidByMapIdAsync(customer.UserId, cancellationToken);
+ if (!userGuid.HasValue)
+ {
+ throw new BusinessException("客户未关联用户");
+ }
+
+ await weChatLoginSessionStore.RemoveAsync(loginUuid, cancellationToken);
+
+ var claims = await BuildClaimsAsync(userGuid.Value, OmsLoginType.Customer, cancellationToken);
+ return BuildLoginResult(userGuid.Value, claims);
+ }
+
///
/// 刷新登录令牌。
///
diff --git a/src/SimpleAdmin.Api/SimpleAdmin.Api.csproj b/src/SimpleAdmin.Api/SimpleAdmin.Api.csproj
index 61ff6fe..3ba3514 100644
--- a/src/SimpleAdmin.Api/SimpleAdmin.Api.csproj
+++ b/src/SimpleAdmin.Api/SimpleAdmin.Api.csproj
@@ -12,6 +12,7 @@
+
diff --git a/src/SimpleAdmin.Api/SimpleAdminApiModule.cs b/src/SimpleAdmin.Api/SimpleAdminApiModule.cs
index 064ac66..38716f4 100644
--- a/src/SimpleAdmin.Api/SimpleAdminApiModule.cs
+++ b/src/SimpleAdmin.Api/SimpleAdminApiModule.cs
@@ -15,6 +15,7 @@ using Microsoft.AspNetCore.HttpOverrides;
using Oms.Application;
using Oms.SqlSugarCore;
using SimpleAdmin.Api.Auth;
+using SimpleAdmin.Api.WeChat;
namespace SimpleAdmin.Api;
@@ -49,12 +50,18 @@ public class SimpleAdminApiModule : FakeModule
settings.RootPath = "rbac";
settings.TypePredicate = type => type == typeof(AuthService);
});
+ options.ApplicationServices2Controller(settings =>
+ {
+ settings.TypePredicate = type => type.Namespace == "SimpleAdmin.Api.WeChat";
+ });
options.ApplicationServices2Controller(settings =>
{
settings.RootPath = "rbac";
settings.TypePredicate = type => type != typeof(Fake.Rbac.Application.Services.AuthService);
});
});
+
+ services.AddOmsWeChat(configuration);
//Json格式化全局处理
context.Services.AddFakeNewtonsoft();
diff --git a/src/SimpleAdmin.Api/WeChat/Dtos/WeChatDtos.cs b/src/SimpleAdmin.Api/WeChat/Dtos/WeChatDtos.cs
new file mode 100644
index 0000000..9b8922d
--- /dev/null
+++ b/src/SimpleAdmin.Api/WeChat/Dtos/WeChatDtos.cs
@@ -0,0 +1,82 @@
+using System.ComponentModel.DataAnnotations;
+
+namespace SimpleAdmin.Api.WeChat.Dtos;
+
+public class WxLoginQrcodeOutput
+{
+ public string QrCodeUrl { get; set; } = string.Empty;
+
+ public int ExpiresIn { get; set; } = 600;
+}
+
+public class WeChatOAuthCodeInput
+{
+ [Required]
+ public string Code { get; set; } = string.Empty;
+}
+
+public class WeChatOAuthUserInfoOutput
+{
+ public string Openid { get; set; } = string.Empty;
+
+ public string? Nickname { get; set; }
+
+ public int Sex { get; set; }
+
+ public string? Province { get; set; }
+
+ public string? City { get; set; }
+
+ public string? Country { get; set; }
+
+ public string? Headimgurl { get; set; }
+
+ public string? Unionid { get; set; }
+
+ public bool AlreadyBound { get; set; }
+}
+
+public class WeChatProfileInput
+{
+ [Required]
+ public string Openid { get; set; } = string.Empty;
+
+ public string? Nickname { get; set; }
+
+ public string? Avatar { get; set; }
+
+ public string? Unionid { get; set; }
+}
+
+public class WeChatCustomerRegisterInput
+{
+ [Required]
+ public string CustomerCode { get; set; } = string.Empty;
+
+ [Required]
+ public string CustomerName { get; set; } = string.Empty;
+
+ [Required]
+ public string Phone { get; set; } = string.Empty;
+
+ public string? Email { get; set; }
+
+ [Required]
+ [MinLength(6)]
+ public string Password { get; set; } = string.Empty;
+
+ [Required]
+ public WeChatProfileInput Wechat { get; set; } = new();
+}
+
+public class WeChatCustomerBindInput
+{
+ [Required]
+ public string Account { get; set; } = string.Empty;
+
+ [Required]
+ public string Password { get; set; } = string.Empty;
+
+ [Required]
+ public WeChatProfileInput Wechat { get; set; } = new();
+}
diff --git a/src/SimpleAdmin.Api/WeChat/Options/WeChatMpOptions.cs b/src/SimpleAdmin.Api/WeChat/Options/WeChatMpOptions.cs
new file mode 100644
index 0000000..bb7f8e5
--- /dev/null
+++ b/src/SimpleAdmin.Api/WeChat/Options/WeChatMpOptions.cs
@@ -0,0 +1,22 @@
+namespace SimpleAdmin.Api.WeChat.Options;
+
+///
+/// 微信公众号配置。
+///
+public class WeChatMpOptions
+{
+ public const string SectionName = "WeChat:Mp";
+
+ public string AppId { get; set; } = string.Empty;
+
+ public string Secret { get; set; } = string.Empty;
+
+ public string Token { get; set; } = string.Empty;
+
+ public string? EncodingAesKey { get; set; }
+
+ ///
+ /// OAuth 回调基础域名,如 https://oms.nanafa.com
+ ///
+ public string OAuthRedirectBase { get; set; } = string.Empty;
+}
diff --git a/src/SimpleAdmin.Api/WeChat/Services/IWeChatLoginSessionStore.cs b/src/SimpleAdmin.Api/WeChat/Services/IWeChatLoginSessionStore.cs
new file mode 100644
index 0000000..9c43681
--- /dev/null
+++ b/src/SimpleAdmin.Api/WeChat/Services/IWeChatLoginSessionStore.cs
@@ -0,0 +1,13 @@
+namespace SimpleAdmin.Api.WeChat.Services;
+
+///
+/// 微信扫码登录会话:loginUuid → openId 桥接。
+///
+public interface IWeChatLoginSessionStore
+{
+ Task SetOpenIdAsync(string loginUuid, string openId, TimeSpan ttl, CancellationToken cancellationToken = default);
+
+ Task GetOpenIdAsync(string loginUuid, CancellationToken cancellationToken = default);
+
+ Task RemoveAsync(string loginUuid, CancellationToken cancellationToken = default);
+}
diff --git a/src/SimpleAdmin.Api/WeChat/Services/WeChatEventHandler.cs b/src/SimpleAdmin.Api/WeChat/Services/WeChatEventHandler.cs
new file mode 100644
index 0000000..2fc01b9
--- /dev/null
+++ b/src/SimpleAdmin.Api/WeChat/Services/WeChatEventHandler.cs
@@ -0,0 +1,90 @@
+using Oms.Domain.CustomersAggregate.Repositories;
+
+namespace SimpleAdmin.Api.WeChat.Services;
+
+public interface IWeChatEventHandler
+{
+ Task HandleAsync(WeChatIncomingMessage message, CancellationToken cancellationToken = default);
+}
+
+public class WeChatEventHandler(
+ ICustomerRepository customerRepository,
+ IWeChatLoginSessionStore sessionStore,
+ ILogger logger) : IWeChatEventHandler
+{
+ private const string QrScenePrefix = "qrscene_";
+
+ public async Task HandleAsync(WeChatIncomingMessage message, CancellationToken cancellationToken = default)
+ {
+ if (!string.Equals(message.MsgType, "event", StringComparison.OrdinalIgnoreCase))
+ {
+ return null;
+ }
+
+ var eventName = message.Event ?? string.Empty;
+ if (string.Equals(eventName, "SCAN", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(eventName, "subscribe", StringComparison.OrdinalIgnoreCase))
+ {
+ return await HandleScanLoginAsync(message, cancellationToken);
+ }
+
+ logger.LogDebug("未处理的微信事件: {Event}", eventName);
+ return null;
+ }
+
+ private async Task HandleScanLoginAsync(WeChatIncomingMessage message, CancellationToken cancellationToken)
+ {
+ var openId = message.FromUserName;
+ if (string.IsNullOrWhiteSpace(openId))
+ {
+ return null;
+ }
+
+ var existing = await customerRepository.GetByWechatOpenidAsync(openId, cancellationToken);
+ if (existing == null)
+ {
+ return BuildTextReply(message, "未绑定账号,请在公众号菜单注册账号或绑定已有账号。");
+ }
+
+ var scene = NormalizeScene(message.EventKey);
+ if (string.IsNullOrWhiteSpace(scene))
+ {
+ if (string.Equals(message.Event, "subscribe", StringComparison.OrdinalIgnoreCase))
+ {
+ return BuildTextReply(message, "感谢关注");
+ }
+
+ return null;
+ }
+
+ await sessionStore.SetOpenIdAsync(scene, openId, TimeSpan.FromMinutes(10), cancellationToken);
+ logger.LogInformation("微信扫码登录会话已写入: loginUuid={LoginUuid}, openId={OpenId}", scene, openId);
+
+ if (string.Equals(message.Event, "subscribe", StringComparison.OrdinalIgnoreCase))
+ {
+ return BuildTextReply(message, "感谢关注,请返回电脑端完成登录。");
+ }
+
+ return BuildTextReply(message, "扫码成功,请返回电脑端完成登录。");
+ }
+
+ private static string? NormalizeScene(string? eventKey)
+ {
+ if (string.IsNullOrWhiteSpace(eventKey))
+ {
+ return null;
+ }
+
+ return eventKey.StartsWith(QrScenePrefix, StringComparison.Ordinal)
+ ? eventKey[QrScenePrefix.Length..]
+ : eventKey;
+ }
+
+ private static string BuildTextReply(WeChatIncomingMessage message, string content)
+ {
+ return WeChatXmlMessageBuilder.BuildTextReply(
+ message.FromUserName ?? string.Empty,
+ message.ToUserName ?? string.Empty,
+ content);
+ }
+}
diff --git a/src/SimpleAdmin.Api/WeChat/Services/WeChatLoginSessionStore.cs b/src/SimpleAdmin.Api/WeChat/Services/WeChatLoginSessionStore.cs
new file mode 100644
index 0000000..20b09a3
--- /dev/null
+++ b/src/SimpleAdmin.Api/WeChat/Services/WeChatLoginSessionStore.cs
@@ -0,0 +1,29 @@
+using Microsoft.Extensions.Caching.Distributed;
+
+namespace SimpleAdmin.Api.WeChat.Services;
+
+public class WeChatLoginSessionStore(IDistributedCache cache) : IWeChatLoginSessionStore
+{
+ private const string KeyPrefix = "oms:wechat:login:";
+
+ public Task SetOpenIdAsync(string loginUuid, string openId, TimeSpan ttl, CancellationToken cancellationToken = default)
+ {
+ var options = new DistributedCacheEntryOptions
+ {
+ AbsoluteExpirationRelativeToNow = ttl
+ };
+ return cache.SetStringAsync(BuildKey(loginUuid), openId, options, cancellationToken);
+ }
+
+ public Task GetOpenIdAsync(string loginUuid, CancellationToken cancellationToken = default)
+ {
+ return cache.GetStringAsync(BuildKey(loginUuid), cancellationToken);
+ }
+
+ public Task RemoveAsync(string loginUuid, CancellationToken cancellationToken = default)
+ {
+ return cache.RemoveAsync(BuildKey(loginUuid), cancellationToken);
+ }
+
+ private static string BuildKey(string loginUuid) => $"{KeyPrefix}{loginUuid}";
+}
diff --git a/src/SimpleAdmin.Api/WeChat/Services/WeChatMpApiClient.cs b/src/SimpleAdmin.Api/WeChat/Services/WeChatMpApiClient.cs
new file mode 100644
index 0000000..85c2f38
--- /dev/null
+++ b/src/SimpleAdmin.Api/WeChat/Services/WeChatMpApiClient.cs
@@ -0,0 +1,293 @@
+using System.Text.Json.Serialization;
+using System.Xml.Linq;
+using Microsoft.Extensions.Caching.Memory;
+using Microsoft.Extensions.Options;
+using SimpleAdmin.Api.WeChat.Options;
+
+namespace SimpleAdmin.Api.WeChat.Services;
+
+public interface IWeChatMpApiClient
+{
+ Task CreateLoginQrCodeUrlAsync(string loginUuid, int expireSeconds, CancellationToken cancellationToken = default);
+
+ Task GetOAuthAccessTokenAsync(string code, CancellationToken cancellationToken = default);
+
+ Task GetOAuthUserInfoAsync(string accessToken, string openId, CancellationToken cancellationToken = default);
+}
+
+public sealed record WeChatOAuthTokenResult(string AccessToken, string OpenId);
+
+public sealed record WeChatOAuthUserResult(
+ string Openid,
+ string? Nickname,
+ int Sex,
+ string? Province,
+ string? City,
+ string? Country,
+ string? Headimgurl,
+ string? Unionid);
+
+internal sealed class WeChatMpApiClient(
+ IHttpClientFactory httpClientFactory,
+ IMemoryCache memoryCache,
+ IOptions options,
+ ILogger logger) : IWeChatMpApiClient
+{
+ private const string AccessTokenCacheKey = "oms:wechat:mp:access_token";
+
+ public async Task CreateLoginQrCodeUrlAsync(
+ string loginUuid,
+ int expireSeconds,
+ CancellationToken cancellationToken = default)
+ {
+ var accessToken = await GetAccessTokenAsync(cancellationToken);
+ var client = httpClientFactory.CreateClient(nameof(WeChatMpApiClient));
+
+ var payload = new
+ {
+ expire_seconds = expireSeconds,
+ action_name = "QR_STR_SCENE",
+ action_info = new
+ {
+ scene = new
+ {
+ scene_str = loginUuid
+ }
+ }
+ };
+
+ using var response = await client.PostAsJsonAsync(
+ $"https://api.weixin.qq.com/cgi-bin/qrcode/create?access_token={Uri.EscapeDataString(accessToken)}",
+ payload,
+ cancellationToken);
+
+ response.EnsureSuccessStatusCode();
+ var result = await response.Content.ReadFromJsonAsync(cancellationToken: cancellationToken);
+ if (result == null || string.IsNullOrWhiteSpace(result.Ticket))
+ {
+ throw new InvalidOperationException(result?.Errmsg ?? "创建微信二维码失败");
+ }
+
+ if (result.Errcode is > 0)
+ {
+ throw new InvalidOperationException(result.Errmsg ?? $"创建微信二维码失败({result.Errcode})");
+ }
+
+ return $"https://mp.weixin.qq.com/cgi-bin/showqrcode?ticket={Uri.EscapeDataString(result.Ticket)}";
+ }
+
+ public async Task GetOAuthAccessTokenAsync(string code, CancellationToken cancellationToken = default)
+ {
+ var mp = options.Value;
+ var client = httpClientFactory.CreateClient(nameof(WeChatMpApiClient));
+ var url =
+ $"https://api.weixin.qq.com/sns/oauth2/access_token?appid={Uri.EscapeDataString(mp.AppId)}&secret={Uri.EscapeDataString(mp.Secret)}&code={Uri.EscapeDataString(code)}&grant_type=authorization_code";
+
+ using var response = await client.GetAsync(url, cancellationToken);
+ response.EnsureSuccessStatusCode();
+ var body = await response.Content.ReadFromJsonAsync(cancellationToken: cancellationToken);
+ if (body == null || string.IsNullOrWhiteSpace(body.AccessToken) || string.IsNullOrWhiteSpace(body.Openid))
+ {
+ logger.LogWarning("微信 OAuth 换取 access_token 失败: {Message}", body?.Errmsg);
+ return null;
+ }
+
+ return new WeChatOAuthTokenResult(body.AccessToken, body.Openid);
+ }
+
+ public async Task GetOAuthUserInfoAsync(
+ string accessToken,
+ string openId,
+ CancellationToken cancellationToken = default)
+ {
+ var client = httpClientFactory.CreateClient(nameof(WeChatMpApiClient));
+ var url =
+ $"https://api.weixin.qq.com/sns/userinfo?access_token={Uri.EscapeDataString(accessToken)}&openid={Uri.EscapeDataString(openId)}&lang=zh_CN";
+
+ using var response = await client.GetAsync(url, cancellationToken);
+ response.EnsureSuccessStatusCode();
+ var body = await response.Content.ReadFromJsonAsync(cancellationToken: cancellationToken);
+ if (body == null || string.IsNullOrWhiteSpace(body.Openid))
+ {
+ logger.LogWarning("微信 OAuth 获取用户信息失败: {Message}", body?.Errmsg);
+ return null;
+ }
+
+ return new WeChatOAuthUserResult(
+ body.Openid,
+ body.Nickname,
+ body.Sex,
+ body.Province,
+ body.City,
+ body.Country,
+ body.Headimgurl,
+ body.Unionid);
+ }
+
+ private async Task GetAccessTokenAsync(CancellationToken cancellationToken)
+ {
+ if (memoryCache.TryGetValue(AccessTokenCacheKey, out string? cached) && !string.IsNullOrWhiteSpace(cached))
+ {
+ return cached;
+ }
+
+ var mp = options.Value;
+ var client = httpClientFactory.CreateClient(nameof(WeChatMpApiClient));
+ var url =
+ $"https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid={Uri.EscapeDataString(mp.AppId)}&secret={Uri.EscapeDataString(mp.Secret)}";
+
+ using var response = await client.GetAsync(url, cancellationToken);
+ response.EnsureSuccessStatusCode();
+ var body = await response.Content.ReadFromJsonAsync(cancellationToken: cancellationToken);
+ if (body == null || string.IsNullOrWhiteSpace(body.AccessToken))
+ {
+ throw new InvalidOperationException(body?.Errmsg ?? "获取微信 access_token 失败");
+ }
+
+ memoryCache.Set(AccessTokenCacheKey, body.AccessToken, TimeSpan.FromSeconds(Math.Max(300, body.ExpiresIn - 300)));
+ return body.AccessToken;
+ }
+
+ private sealed class WeChatAccessTokenResponse
+ {
+ [JsonPropertyName("access_token")]
+ public string? AccessToken { get; set; }
+
+ [JsonPropertyName("expires_in")]
+ public int ExpiresIn { get; set; }
+
+ [JsonPropertyName("errcode")]
+ public int Errcode { get; set; }
+
+ [JsonPropertyName("errmsg")]
+ public string? Errmsg { get; set; }
+ }
+
+ private sealed class WeChatQrTicketResponse
+ {
+ [JsonPropertyName("ticket")]
+ public string? Ticket { get; set; }
+
+ [JsonPropertyName("expire_seconds")]
+ public int ExpireSeconds { get; set; }
+
+ [JsonPropertyName("errcode")]
+ public int Errcode { get; set; }
+
+ [JsonPropertyName("errmsg")]
+ public string? Errmsg { get; set; }
+ }
+
+ private sealed class WeChatOAuthTokenResponse
+ {
+ [JsonPropertyName("access_token")]
+ public string? AccessToken { get; set; }
+
+ [JsonPropertyName("openid")]
+ public string? Openid { get; set; }
+
+ [JsonPropertyName("errcode")]
+ public int Errcode { get; set; }
+
+ [JsonPropertyName("errmsg")]
+ public string? Errmsg { get; set; }
+ }
+
+ private sealed class WeChatOAuthUserResponse
+ {
+ [JsonPropertyName("openid")]
+ public string? Openid { get; set; }
+
+ [JsonPropertyName("nickname")]
+ public string? Nickname { get; set; }
+
+ [JsonPropertyName("sex")]
+ public int Sex { get; set; }
+
+ [JsonPropertyName("province")]
+ public string? Province { get; set; }
+
+ [JsonPropertyName("city")]
+ public string? City { get; set; }
+
+ [JsonPropertyName("country")]
+ public string? Country { get; set; }
+
+ [JsonPropertyName("headimgurl")]
+ public string? Headimgurl { get; set; }
+
+ [JsonPropertyName("unionid")]
+ public string? Unionid { get; set; }
+
+ [JsonPropertyName("errcode")]
+ public int Errcode { get; set; }
+
+ [JsonPropertyName("errmsg")]
+ public string? Errmsg { get; set; }
+ }
+}
+
+internal static class WeChatXmlMessageParser
+{
+ public static WeChatIncomingMessage? Parse(string xml)
+ {
+ if (string.IsNullOrWhiteSpace(xml))
+ {
+ return null;
+ }
+
+ var doc = XDocument.Parse(xml);
+ var root = doc.Root;
+ if (root == null)
+ {
+ return null;
+ }
+
+ string? Value(string name) => root.Element(name)?.Value;
+
+ return new WeChatIncomingMessage
+ {
+ ToUserName = Value("ToUserName"),
+ FromUserName = Value("FromUserName"),
+ CreateTime = Value("CreateTime"),
+ MsgType = Value("MsgType"),
+ Event = Value("Event"),
+ EventKey = Value("EventKey"),
+ Content = Value("Content")
+ };
+ }
+}
+
+internal sealed class WeChatIncomingMessage
+{
+ public string? ToUserName { get; init; }
+
+ public string? FromUserName { get; init; }
+
+ public string? CreateTime { get; init; }
+
+ public string? MsgType { get; init; }
+
+ public string? Event { get; init; }
+
+ public string? EventKey { get; init; }
+
+ public string? Content { get; init; }
+}
+
+internal static class WeChatXmlMessageBuilder
+{
+ public static string BuildTextReply(string toUser, string fromUser, string content)
+ {
+ var now = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
+ return $"""
+
+
+
+ {now}
+
+
+
+ """;
+ }
+}
diff --git a/src/SimpleAdmin.Api/WeChat/Services/WeChatSignatureHelper.cs b/src/SimpleAdmin.Api/WeChat/Services/WeChatSignatureHelper.cs
new file mode 100644
index 0000000..e87e808
--- /dev/null
+++ b/src/SimpleAdmin.Api/WeChat/Services/WeChatSignatureHelper.cs
@@ -0,0 +1,24 @@
+using System.Security.Cryptography;
+using System.Text;
+
+namespace SimpleAdmin.Api.WeChat.Services;
+
+internal static class WeChatSignatureHelper
+{
+ public static bool Validate(string token, string timestamp, string nonce, string signature)
+ {
+ if (string.IsNullOrWhiteSpace(token)
+ || string.IsNullOrWhiteSpace(timestamp)
+ || string.IsNullOrWhiteSpace(nonce)
+ || string.IsNullOrWhiteSpace(signature))
+ {
+ return false;
+ }
+
+ var parts = new[] { token, timestamp, nonce }.OrderBy(x => x, StringComparer.Ordinal).ToArray();
+ var raw = string.Concat(parts);
+ var hash = SHA1.HashData(Encoding.UTF8.GetBytes(raw));
+ var hex = Convert.ToHexString(hash).ToLowerInvariant();
+ return string.Equals(hex, signature, StringComparison.OrdinalIgnoreCase);
+ }
+}
diff --git a/src/SimpleAdmin.Api/WeChat/WeChatCustomerAppService.cs b/src/SimpleAdmin.Api/WeChat/WeChatCustomerAppService.cs
new file mode 100644
index 0000000..b4d09ae
--- /dev/null
+++ b/src/SimpleAdmin.Api/WeChat/WeChatCustomerAppService.cs
@@ -0,0 +1,297 @@
+using Fake.Application;
+using Fake.Domain.Exceptions;
+using Fake.Rbac.Application.Dtos.Role;
+using Fake.Rbac.Application.Dtos.User;
+using Fake.Rbac.Application.Services;
+using Fake.Rbac.Infrastructure;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.EntityFrameworkCore;
+using Oms.Application.Shared;
+using Oms.Domain.CustomersAggregate.Entities;
+using Oms.Domain.CustomersAggregate.Enums;
+using Oms.Domain.CustomersAggregate.Repositories;
+using SimpleAdmin.Api.WeChat.Dtos;
+
+namespace SimpleAdmin.Api.WeChat;
+
+///
+/// 微信 OAuth 注册 / 绑定客户。
+///
+[AllowAnonymous]
+[ApiExplorerSettings(GroupName = "WeChat")]
+public class WeChatCustomerAppService(
+ IWeChatMpApiClient weChatMpApiClient,
+ ICustomerRepository customerRepository,
+ UserService userService,
+ RoleService roleService,
+ FakeRbacDbContext rbacDbContext) : ApplicationService
+{
+ private const string CustomerRoleCode = "Customer";
+
+ ///
+ /// OAuth code 换取微信用户信息。
+ ///
+ [HttpPost("/wechat/oauth/user-info")]
+ public async Task GetOAuthUserInfoAsync(
+ [FromBody] WeChatOAuthCodeInput input,
+ CancellationToken cancellationToken = default)
+ {
+ if (string.IsNullOrWhiteSpace(input.Code))
+ {
+ throw new BusinessException("缺少 code");
+ }
+
+ var token = await weChatMpApiClient.GetOAuthAccessTokenAsync(input.Code.Trim(), cancellationToken);
+ if (token == null)
+ {
+ throw new BusinessException("获取授权信息出现错误");
+ }
+
+ var userInfo = await weChatMpApiClient.GetOAuthUserInfoAsync(token.AccessToken, token.OpenId, cancellationToken);
+ if (userInfo == null)
+ {
+ throw new BusinessException("获取授权信息出现错误");
+ }
+
+ var existing = await customerRepository.GetByWechatOpenidAsync(userInfo.Openid, cancellationToken);
+ return new WeChatOAuthUserInfoOutput
+ {
+ Openid = userInfo.Openid,
+ Nickname = userInfo.Nickname,
+ Sex = userInfo.Sex,
+ Province = userInfo.Province,
+ City = userInfo.City,
+ Country = userInfo.Country,
+ Headimgurl = userInfo.Headimgurl,
+ Unionid = userInfo.Unionid,
+ AlreadyBound = existing != null
+ };
+ }
+
+ ///
+ /// 微信授权注册客户。
+ ///
+ [HttpPost("/wechat/customer/register")]
+ public async Task RegisterAsync(
+ [FromBody] WeChatCustomerRegisterInput input,
+ CancellationToken cancellationToken = default)
+ {
+ ValidateWechatProfile(input.Wechat);
+
+ var customerCode = NormalizeRequired(input.CustomerCode, "客户编号");
+ var customerName = NormalizeRequired(input.CustomerName, "客户名称");
+ var phone = NormalizeRequired(input.Phone, "手机号");
+ var email = NormalizeOptional(input.Email);
+ var password = NormalizeRequired(input.Password, "密码");
+
+ if (password.Length < 6)
+ {
+ throw new BusinessException("密码长度不能少于6位");
+ }
+
+ if (await customerRepository.GetByWechatOpenidAsync(input.Wechat.Openid, cancellationToken) != null)
+ {
+ throw new BusinessException("当前微信已绑定账号");
+ }
+
+ await EnsureCustomerIdentityUniqueAsync(customerCode, phone, email, cancellationToken);
+
+ var customerRoleId = await GetCustomerRoleIdOrThrowAsync(cancellationToken);
+ var user = await userService.CreateAsync(new UserCreateDto
+ {
+ Name = customerName,
+ Account = phone,
+ Password = password,
+ Email = email,
+ Avatar = NormalizeOptional(input.Wechat.Avatar),
+ RoleIds = []
+ });
+
+ try
+ {
+ await userService.AssignRolesAsync(user.Id, [customerRoleId]);
+ var userId = await GetRbacUserMapIdOrThrowAsync(user.Id, cancellationToken);
+ var entity = new Customer
+ {
+ UserId = userId,
+ CustomerCode = customerCode,
+ CustomerName = customerName,
+ Phone = phone,
+ Email = email,
+ Password = OmsPasswordHelper.ToMd5(password),
+ WechatOpenid = input.Wechat.Openid,
+ WechatNickname = NormalizeOptional(input.Wechat.Nickname),
+ WechatAvatar = NormalizeOptional(input.Wechat.Avatar),
+ Status = CustomerStatus.Normal
+ };
+ await customerRepository.InsertAsync(entity);
+ }
+ catch
+ {
+ await TryRollbackRbacUserAsync(user.Id);
+ throw;
+ }
+ }
+
+ ///
+ /// 微信授权绑定已有客户。
+ ///
+ [HttpPost("/wechat/customer/bind")]
+ public async Task BindAsync(
+ [FromBody] WeChatCustomerBindInput input,
+ CancellationToken cancellationToken = default)
+ {
+ ValidateWechatProfile(input.Wechat);
+
+ var account = NormalizeRequired(input.Account, "账号");
+ var password = NormalizeRequired(input.Password, "密码");
+
+ if (await customerRepository.GetByWechatOpenidAsync(input.Wechat.Openid, cancellationToken) != null)
+ {
+ throw new BusinessException("当前微信已绑定账号");
+ }
+
+ var customer = await FindCustomerByAccountAsync(account, cancellationToken);
+ if (customer == null)
+ {
+ throw new BusinessException("未查询到账号信息");
+ }
+
+ if (customer.ParentCustomerId.HasValue)
+ {
+ throw new BusinessException("子客户不支持绑定微信");
+ }
+
+ if (!string.IsNullOrWhiteSpace(customer.WechatOpenid))
+ {
+ throw new BusinessException("当前账号已绑定微信");
+ }
+
+ ValidateMainCustomerStatus(customer);
+
+ if (!string.Equals(customer.Password, OmsPasswordHelper.ToMd5(password), StringComparison.OrdinalIgnoreCase))
+ {
+ throw new BusinessException("账号或密码错误");
+ }
+
+ customer.WechatOpenid = input.Wechat.Openid;
+ customer.WechatNickname = NormalizeOptional(input.Wechat.Nickname) ?? customer.WechatNickname;
+ customer.WechatAvatar = NormalizeOptional(input.Wechat.Avatar) ?? customer.WechatAvatar;
+ await customerRepository.UpdateAsync(customer, cancellationToken);
+ }
+
+ private static void ValidateWechatProfile(WeChatProfileInput wechat)
+ {
+ if (string.IsNullOrWhiteSpace(wechat.Openid))
+ {
+ throw new BusinessException("缺少 Openid 授权信息");
+ }
+ }
+
+ private async Task EnsureCustomerIdentityUniqueAsync(
+ string customerCode,
+ string phone,
+ string? email,
+ CancellationToken cancellationToken)
+ {
+ if (await customerRepository.GetByCustomerCodeAsync(customerCode, cancellationToken) != null)
+ {
+ throw new BusinessException("客户编号已存在");
+ }
+
+ if (await customerRepository.GetByPhoneAsync(phone, cancellationToken) != null)
+ {
+ throw new BusinessException("手机号已存在");
+ }
+
+ if (email != null && await customerRepository.GetByEmailAsync(email, cancellationToken) != null)
+ {
+ throw new BusinessException("邮箱已存在");
+ }
+ }
+
+ private async Task FindCustomerByAccountAsync(string account, CancellationToken cancellationToken)
+ {
+ var customer = await customerRepository.GetByPhoneAsync(account, cancellationToken);
+ if (customer != null)
+ {
+ return customer;
+ }
+
+ customer = await customerRepository.GetByEmailAsync(account, cancellationToken);
+ if (customer != null)
+ {
+ return customer;
+ }
+
+ return await customerRepository.GetByCustomerCodeAsync(account, cancellationToken);
+ }
+
+ private async Task GetCustomerRoleIdOrThrowAsync(CancellationToken cancellationToken)
+ {
+ var roles = await roleService.GetAllRolesAsync();
+ var role = roles.FirstOrDefault(x => string.Equals(x.Code, CustomerRoleCode, StringComparison.OrdinalIgnoreCase));
+ if (role == null)
+ {
+ throw new BusinessException("客户角色未初始化");
+ }
+
+ return role.Id;
+ }
+
+ private async Task GetRbacUserMapIdOrThrowAsync(Guid id, CancellationToken cancellationToken)
+ {
+ var userId = await rbacDbContext.Database.SqlQuery(
+ $"select `userid` as `Value` from `user` where `Id` = {id}")
+ .FirstOrDefaultAsync(cancellationToken);
+ if (!userId.HasValue || userId.Value <= 0)
+ {
+ throw new BusinessException("用户编号创建失败");
+ }
+
+ return userId.Value;
+ }
+
+ private async Task TryRollbackRbacUserAsync(Guid userId)
+ {
+ try
+ {
+ await userService.DeleteAsync(userId);
+ }
+ catch
+ {
+ // ignore rollback failure
+ }
+ }
+
+ private static void ValidateMainCustomerStatus(Customer customer)
+ {
+ if (customer.Status == CustomerStatus.Disabled)
+ {
+ throw new BusinessException("客户已禁用");
+ }
+
+ if (customer.Status == CustomerStatus.Frozen)
+ {
+ throw new BusinessException("客户已冻结");
+ }
+ }
+
+ private static string NormalizeRequired(string? value, string fieldName)
+ {
+ var normalized = value?.Trim();
+ if (string.IsNullOrWhiteSpace(normalized))
+ {
+ throw new BusinessException($"{fieldName}不能为空");
+ }
+
+ return normalized;
+ }
+
+ private static string? NormalizeOptional(string? value)
+ {
+ var normalized = value?.Trim();
+ return string.IsNullOrWhiteSpace(normalized) ? null : normalized;
+ }
+}
diff --git a/src/SimpleAdmin.Api/WeChat/WeChatMpAppService.cs b/src/SimpleAdmin.Api/WeChat/WeChatMpAppService.cs
new file mode 100644
index 0000000..6332a9c
--- /dev/null
+++ b/src/SimpleAdmin.Api/WeChat/WeChatMpAppService.cs
@@ -0,0 +1,130 @@
+using Fake.Application;
+using Fake.Domain.Exceptions;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.Extensions.Options;
+using SimpleAdmin.Api.WeChat.Dtos;
+using SimpleAdmin.Api.WeChat.Options;
+using SimpleAdmin.Api.WeChat.Services;
+
+namespace SimpleAdmin.Api.WeChat;
+
+///
+/// 微信公众号服务器回调与登录二维码。
+///
+[AllowAnonymous]
+[ApiExplorerSettings(GroupName = "WeChat")]
+public class WeChatMpAppService(
+ IOptions options,
+ IWeChatMpApiClient weChatMpApiClient,
+ IWeChatEventHandler eventHandler,
+ ILogger logger) : ApplicationService
+{
+ ///
+ /// 微信服务器接入验签。
+ ///
+ [HttpGet("/wechat/mp/{appId}")]
+ [Produces("text/plain")]
+ public ContentResult ValidateAsync(
+ [FromRoute] string appId,
+ [FromQuery] string? signature,
+ [FromQuery] string? timestamp,
+ [FromQuery] string? nonce,
+ [FromQuery] string? echostr)
+ {
+ EnsureAppId(appId);
+ if (string.IsNullOrWhiteSpace(signature)
+ || string.IsNullOrWhiteSpace(timestamp)
+ || string.IsNullOrWhiteSpace(nonce)
+ || string.IsNullOrWhiteSpace(echostr))
+ {
+ return Content("error", "text/plain; charset=utf-8");
+ }
+
+ var valid = WeChatSignatureHelper.Validate(options.Value.Token, timestamp, nonce, signature);
+ return Content(valid ? echostr : "error", "text/plain; charset=utf-8");
+ }
+
+ ///
+ /// 微信消息/事件回调。
+ ///
+ [HttpPost("/wechat/mp/{appId}")]
+ [Consumes("text/xml", "application/xml", "text/plain")]
+ [Produces("application/xml")]
+ public async Task MessageAsync(
+ [FromRoute] string appId,
+ [FromQuery] string signature,
+ [FromQuery] string timestamp,
+ [FromQuery] string nonce,
+ [FromQuery] string? openid,
+ [FromQuery(Name = "encrypt_type")] string? encryptType,
+ [FromQuery(Name = "msg_signature")] string? msgSignature,
+ CancellationToken cancellationToken = default)
+ {
+ EnsureAppId(appId);
+ if (!WeChatSignatureHelper.Validate(options.Value.Token, timestamp, nonce, signature))
+ {
+ logger.LogWarning("微信消息验签失败");
+ return Content("error", "text/plain; charset=utf-8");
+ }
+
+ using var reader = new StreamReader(HttpContext.Request.Body);
+ var requestBody = await reader.ReadToEndAsync(cancellationToken);
+ if (string.IsNullOrWhiteSpace(requestBody))
+ {
+ return Content(string.Empty, "application/xml; charset=utf-8");
+ }
+
+ if (!string.IsNullOrWhiteSpace(encryptType)
+ && !string.Equals(encryptType, "raw", StringComparison.OrdinalIgnoreCase))
+ {
+ logger.LogWarning("收到加密微信消息({EncryptType}),当前仅支持明文模式,请在公众平台切换为明文或后续启用 AES。", encryptType);
+ return Content(string.Empty, "application/xml; charset=utf-8");
+ }
+
+ var message = WeChatXmlMessageParser.Parse(requestBody);
+ if (message == null)
+ {
+ return Content(string.Empty, "application/xml; charset=utf-8");
+ }
+
+ var reply = await eventHandler.HandleAsync(message, cancellationToken);
+ return Content(reply ?? string.Empty, "application/xml; charset=utf-8");
+ }
+
+ ///
+ /// 获取 PC 扫码登录二维码。
+ ///
+ [HttpGet("/wechat/mp/login-qrcode")]
+ public async Task GetLoginQrcodeAsync(
+ [FromQuery] string loginUuid,
+ CancellationToken cancellationToken = default)
+ {
+ if (string.IsNullOrWhiteSpace(loginUuid))
+ {
+ throw new BusinessException("缺少 loginUuid");
+ }
+
+ const int expireSeconds = 600;
+ var qrCodeUrl = await weChatMpApiClient.CreateLoginQrCodeUrlAsync(loginUuid.Trim(), expireSeconds, cancellationToken);
+ return new WxLoginQrcodeOutput
+ {
+ QrCodeUrl = qrCodeUrl,
+ ExpiresIn = expireSeconds
+ };
+ }
+
+ private void EnsureAppId(string appId)
+ {
+ var configured = options.Value.AppId;
+ if (string.IsNullOrWhiteSpace(configured))
+ {
+ throw new BusinessException("未配置微信公众号 AppId");
+ }
+
+ if (!string.Equals(configured, appId, StringComparison.OrdinalIgnoreCase))
+ {
+ throw new BusinessException("公众号 AppId 不匹配");
+ }
+ }
+}
diff --git a/src/SimpleAdmin.Api/WeChat/WeChatServiceExtensions.cs b/src/SimpleAdmin.Api/WeChat/WeChatServiceExtensions.cs
new file mode 100644
index 0000000..6213e1d
--- /dev/null
+++ b/src/SimpleAdmin.Api/WeChat/WeChatServiceExtensions.cs
@@ -0,0 +1,32 @@
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.DependencyInjection;
+using SimpleAdmin.Api.WeChat.Options;
+using SimpleAdmin.Api.WeChat.Services;
+
+namespace SimpleAdmin.Api.WeChat;
+
+public static class WeChatServiceExtensions
+{
+ public static IServiceCollection AddOmsWeChat(this IServiceCollection services, IConfiguration configuration)
+ {
+ services.Configure(configuration.GetSection(WeChatMpOptions.SectionName));
+ services.AddHttpClient(nameof(WeChatMpApiClient));
+ services.AddMemoryCache();
+
+ var redisConnection = configuration.GetConnectionString("Redis") ?? configuration["Redis:ConnectionString"];
+ if (!string.IsNullOrWhiteSpace(redisConnection))
+ {
+ services.AddStackExchangeRedisCache(options => options.Configuration = redisConnection);
+ }
+ else
+ {
+ services.AddDistributedMemoryCache();
+ }
+
+ services.AddSingleton();
+ services.AddSingleton();
+ services.AddTransient();
+
+ return services;
+ }
+}
diff --git a/src/SimpleAdmin.Api/appsettings.Development.json b/src/SimpleAdmin.Api/appsettings.Development.json
index 62539e8..f569101 100644
--- a/src/SimpleAdmin.Api/appsettings.Development.json
+++ b/src/SimpleAdmin.Api/appsettings.Development.json
@@ -1,9 +1,22 @@
{
+ "UseLocalConfigs": true,
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning",
"Microsoft.EntityFrameworkCore": "Information"
}
+ },
+ "Redis": {
+ "ConnectionString": ""
+ },
+ "WeChat": {
+ "Mp": {
+ "AppId": "",
+ "Secret": "",
+ "Token": "",
+ "EncodingAesKey": "",
+ "OAuthRedirectBase": "https://oms.nanafa.com"
+ }
}
}
\ No newline at end of file