# 运费模版 — 技术方案

> **依据：** 《运费模版功能需求.md》v1.0  
> **关联：** 《关联需求分析.md》v1.6；平台《店铺管理功能需求》v1.3、《订单管理技术方案》v1.0.2、《商品管理功能需求》v1.3.3；商家《店铺商品列表技术方案》v1.5、《属性模版技术方案》v1.3、《商家端店铺上下文接口说明》  
> **范围：** 商家端 **当前店铺** 运费模版 CRUD、设为默认、下拉选项；为 **商品列表 · 快递运费** 与 **确认订单运费计算** 提供规则数据源；**平台不参与** 模版维护。  
> **原则：** `shop_id` 店铺隔离；逻辑删除；同店 **唯一默认**；批量删除 **整批失败**；编辑模版 **不 UPDATE** 历史订单运费快照。

---

## 1. 技术架构

| 项 | 选型 |
|----|------|
| 基础框架 | RuoYi **v3.9.2**（`springboot2` 分支） |
| 数据库 | **MySQL 5.7.39** |
| ORM / 权限 / 响应 | MyBatis；`@PreAuthorize`；`AjaxResult` / `TableDataInfo` |
| 店铺上下文 | `SellerShopContextInterceptor` + **`X-Shop-Id`**；`SellerShopContext.getShopId()` |
| 删店拦截 | `IShopFacade.assertShopMaintable(shopId)` |
| 地区数据 | 复用 **`biz_region`**（省级 `type=1`）；指定地区存 **省 ID 列表** |
| 日志 | 增删改 / 设默认 `@Log` |

### 1.1 与平台侧边界

| 维度 | 平台 · 商品/订单 | 本模块 · 运费模版 |
|------|------------------|-------------------|
| 维护端 | **无** 模版 CRUD | **仅** 商家端 |
| 数据范围 | — | `shop_id = 当前店铺` |
| 订单运费 | 读 **订单快照** `freight_amount` | **不** 追溯改历史订单 |
| 路径 | `/agri/goods`、`/agri/order` | **`/agri/seller/freightTemplate`** |

### 1.2 业务链

```text
GET /agri/seller/context          → 前端设置 X-Shop-Id
        ↓
biz_shop_freight_template（shop_id = 当前店铺）
        └── biz_shop_freight_template_region（指定地区规则，可选）
        ↓
【商品列表】固定运费 / 运费模版 → biz_goods_freight（引用 freight_template_id）
        ↓
【C 端详情】freightDesc ← Facade 摘要
        ↓
【确认订单】CheckoutSupport.calcFreight ← 读模版 + 地址省 + 件数/重量/商品金额
        ↓
【订单】submit 写入 freight_amount 快照
```

| 关联模块 | 协作 |
|----------|------|
| **店铺管理** | 上游 `shop_id`；`assertShopMaintable` |
| **属性模版** | **并列**；无表关联 |
| **商品列表** | 下拉 + 默认预填；写 `biz_goods_freight` |
| **确认订单（C 端）** | `IFreightTemplateFacade.calcFreight`（**规划**） |
| **订单管理** | 只读 `freight_amount`；**不** 读本模块表 |

### 1.3 模块落位

**本模块包：** `com.ruoyi.web.modules.freight`

```text
baqing-shop/src/main/java/com/ruoyi/web/modules/freight/
├── controller/SellerFreightTemplateController.java   # /agri/seller/freightTemplate
├── service/
│   ├── IFreightTemplateService.java
│   └── impl/FreightTemplateServiceImpl.java
├── domain/
│   ├── BizShopFreightTemplate.java
│   └── BizShopFreightTemplateRegion.java
├── dto/
│   ├── FreightTemplateSaveDTO.java
│   └── FreightTemplateRegionRuleDTO.java
├── vo/
│   ├── FreightTemplateListVO.java
│   ├── FreightTemplateDetailVO.java
│   └── FreightTemplateOptionVO.java
├── mapper/
│   ├── BizShopFreightTemplateMapper.java
│   └── BizShopFreightTemplateRegionMapper.java
├── facade/
│   ├── IFreightTemplateFacade.java
│   └── impl/FreightTemplateFacadeImpl.java
├── support/FreightCalcSupport.java                   # 计费纯函数
├── exception/FreightTemplateBatchDeleteException.java
└── constant/FreightTemplateConstants.java

resources/mapper/freight/
├── BizShopFreightTemplateMapper.xml
└── BizShopFreightTemplateRegionMapper.xml

sql/biz_shop_freight_template.sql                     # 模版主表 + 地区规则表
sql/biz_goods_freight.sql                             # 商品运费扩展（协作）
sql/agri_seller_freight_template_menu.sql             # 菜单（待补）
```

**跨模块协作（商品侧 · 规划）：**

| 场景 | 商品模块 |
|------|----------|
| 发品下拉 | `SellerGoodsController` → `IFreightTemplateFacade.listOptionsByShopId` |
| 默认预填 | `getDefaultTemplate(shopId)` |
| 保存校验 | `assertFreightTemplateIfPresent(shopId, templateId)` |
| 删模版校验 | `IGoodsFacade.existsByFreightTemplateId` |

### 1.4 跨模块 Facade

| 接口 | 提供方 | 调用方 | 方法 |
|------|--------|--------|------|
| **`IFreightTemplateFacade`** | **本模块** | 商品、确认订单 | `listOptionsByShopId`、`getDefaultTemplate`、`getDetailForGoods` |
| | | | `buildFreightDesc(templateId, shopId)` — C 端展示摘要 |
| | | | `calcFreight(...)` — 下单运费（**规划**） |
| **`IGoodsFacade`** | 商品 | **本模块** | `existsByFreightTemplateId(templateId)` |
| **`IShopFacade`** | 店铺 | 本模块 | `assertShopMaintable` |

---

## 2. 数据库设计

### 2.1 本模块表

| 表名 | 说明 |
|------|------|
| `biz_shop_freight_template` | 运费模版主表（含 **全国默认** 计价字段） |
| `biz_shop_freight_template_region` | **指定地区** 覆盖规则（0..N 行） |

### 2.2 `biz_shop_freight_template`

| 字段 | 类型 | 说明 |
|------|------|------|
| template_id | bigint PK | 模版 ID |
| shop_id | bigint NOT NULL | **当前店铺 ID** |
| template_name | varchar(64) NOT NULL | 模版名称；本店 `del_flag=0` 唯一 |
| ship_time | varchar(64) NOT NULL | 发货承诺，如「24 小时内发货」 |
| free_flag | char(1) NOT NULL | **0** 不包邮 **1** 包邮 |
| pricing_type | char(1) NULL | **1** 按件数 **2** 按重量；`free_flag=1` 时 NULL |
| is_default | char(1) NOT NULL DEFAULT '0' | **0** 否 **1** 是；同店至多一条为 1 |
| first_unit | decimal(10,2) NULL | 首件数 / 首重(kg) |
| first_fee | decimal(10,2) NULL | 首费（元） |
| add_unit | decimal(10,2) NULL | 续件数 / 续重(kg) |
| add_fee | decimal(10,2) NULL | 续费（元） |
| free_threshold | decimal(12,2) NULL | 满免阈值（**商品金额**）；NULL=不启用 |
| del_flag | char(1) | **0** 存在 **2** 删除 |
| create_by / create_time / update_by / update_time | | RuoYi 审计 |

> **全国默认规则** 落在主表；**不包邮** 时 `first_* / add_*` **必填**（应用层校验）。

### 2.3 `biz_shop_freight_template_region`

| 字段 | 类型 | 说明 |
|------|------|------|
| region_rule_id | bigint PK | |
| template_id | bigint NOT NULL | FK → 模版 |
| province_ids | varchar(512) NOT NULL | 省级 `biz_region.id` 逗号分隔，如 `3,10,11` |
| province_names | varchar(512) NOT NULL | 省名摘要，如「河北省,江苏省,浙江省」 |
| first_unit | decimal(10,2) NOT NULL | 同主表语义 |
| first_fee | decimal(10,2) NOT NULL | |
| add_unit | decimal(10,2) NOT NULL | |
| add_fee | decimal(10,2) NOT NULL | |
| free_threshold | decimal(12,2) NULL | 该地区满免阈值 |
| sort_no | int NOT NULL DEFAULT 0 | 展示顺序 |

**约束（应用层）：**

- 同一 `template_id` 下，任意两个 `region_rule` 的 `province_ids` **集合不交**；
- `province_ids` 须为 **`biz_region.type=1`** 的省级 ID；
- **包邮模版** **不插入** 地区规则行。

### 2.4 商品侧扩展（商品列表协作 · 规划）

| 表 | 字段 | 说明 |
|----|------|------|
| `biz_goods_freight` | goods_id PK/FK | 1:1 商品 |
| | freight_type | **1** 固定运费 **2** 运费模版 |
| | fixed_amount | `freight_type=1` 时金额 |
| | freight_template_id | `freight_type=2` 时模版 ID |
| | del_flag | 0 正常 |

**删模版校验 SQL：**

```sql
SELECT 1 FROM biz_goods_freight f
INNER JOIN biz_goods g ON g.goods_id = f.goods_id AND g.del_flag = '0'
WHERE f.freight_type = '2'
  AND f.freight_template_id = #{templateId}
  AND f.del_flag = '0'
LIMIT 1
```

> v1.0 商品侧 **`biz_goods_freight` 已实现**（发品保存/回显/默认预填）；C 端详情 `freightDesc` 与确认订单真实计费仍 **规划**。

### 2.5 索引

| 表 | 索引 | 字段 |
|----|------|------|
| template | PRIMARY | template_id |
| template | idx_shop_default | shop_id, del_flag, is_default, create_time |
| region | idx_template | template_id, sort_no |
| goods_freight | idx_template | freight_template_id, del_flag |

`template_name` 唯一：**应用层** `(shop_id, template_name)` WHERE `del_flag='0'`。

### 2.6 DDL 概要

```sql
-- sql/biz_shop_freight_template.sql
CREATE TABLE IF NOT EXISTS `biz_shop_freight_template` (
  `template_id`     bigint(20)    NOT NULL AUTO_INCREMENT COMMENT '模版ID',
  `shop_id`         bigint(20)    NOT NULL COMMENT '店铺ID',
  `template_name`   varchar(64)   NOT NULL COMMENT '模版名称',
  `ship_time`       varchar(64)   NOT NULL COMMENT '发货时间承诺',
  `free_flag`       char(1)       NOT NULL COMMENT '0不包邮1包邮',
  `pricing_type`    char(1)       DEFAULT NULL COMMENT '1按件数2按重量',
  `is_default`      char(1)       NOT NULL DEFAULT '0' COMMENT '0否1默认',
  `first_unit`      decimal(10,2) DEFAULT NULL COMMENT '首件数/首重kg',
  `first_fee`       decimal(10,2) DEFAULT NULL COMMENT '首费',
  `add_unit`        decimal(10,2) DEFAULT NULL COMMENT '续件数/续重kg',
  `add_fee`         decimal(10,2) DEFAULT NULL COMMENT '续费',
  `free_threshold`  decimal(12,2) DEFAULT NULL COMMENT '满免阈值(商品金额)',
  `del_flag`        char(1)       NOT NULL DEFAULT '0',
  `create_by`       varchar(64)   DEFAULT '',
  `create_time`     datetime      DEFAULT NULL,
  `update_by`       varchar(64)   DEFAULT '',
  `update_time`     datetime      DEFAULT NULL,
  PRIMARY KEY (`template_id`),
  KEY `idx_shop_default` (`shop_id`,`del_flag`,`is_default`,`create_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='店铺运费模版';

CREATE TABLE IF NOT EXISTS `biz_shop_freight_template_region` (
  `region_rule_id`  bigint(20)    NOT NULL AUTO_INCREMENT,
  `template_id`     bigint(20)    NOT NULL,
  `province_ids`    varchar(512)  NOT NULL COMMENT '省级region.id,逗号分隔',
  `province_names`  varchar(512)  NOT NULL COMMENT '省名摘要',
  `first_unit`      decimal(10,2) NOT NULL,
  `first_fee`       decimal(10,2) NOT NULL,
  `add_unit`        decimal(10,2) NOT NULL,
  `add_fee`         decimal(10,2) NOT NULL,
  `free_threshold`  decimal(12,2) DEFAULT NULL,
  `sort_no`         int(11)       NOT NULL DEFAULT '0',
  PRIMARY KEY (`region_rule_id`),
  KEY `idx_template` (`template_id`,`sort_no`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='运费模版指定地区规则';
```

```sql
-- sql/biz_goods_freight.sql（商品协作 · 规划）
CREATE TABLE IF NOT EXISTS `biz_goods_freight` (
  `goods_id`            bigint(20)    NOT NULL COMMENT '商品ID',
  `freight_type`        char(1)       NOT NULL COMMENT '1固定运费2运费模版',
  `fixed_amount`        decimal(10,2) DEFAULT NULL COMMENT '固定运费金额',
  `freight_template_id` bigint(20)    DEFAULT NULL COMMENT '运费模版ID',
  `del_flag`            char(1)       NOT NULL DEFAULT '0',
  `create_time`         datetime      DEFAULT NULL,
  `update_time`         datetime      DEFAULT NULL,
  PRIMARY KEY (`goods_id`),
  KEY `idx_freight_template` (`freight_template_id`,`del_flag`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='商品快递运费配置';
```

### 2.7 字典

| dict_type | 说明 |
|-----------|------|
| freight_pricing_type | 1 按件数 / 2 按重量 |
| freight_free_flag | 0 不包邮 / 1 包邮 |

---

## 3. 接口设计

**基路径：** `/agri/seller/freightTemplate`  
**公共请求头：** `X-Shop-Id`（必填）  
**数据范围：** `shop_id = 当前店铺` AND `del_flag='0'`。

### 3.1 接口一览

| 方法 | 路径 | 权限 | 说明 |
|------|------|------|------|
| GET | `/list` | list | **分页**列表 |
| GET | `/{templateId}` | query | 详情（编辑回显） |
| POST | `/` | add | 新增 |
| PUT | `/` | edit | 编辑（**全量替换** 地区规则） |
| PUT | `/default/{templateId}` | edit | **设为默认** |
| DELETE | `/{templateIds}` | remove | 删除（**支持批量**；整批失败） |
| GET | `/options` | list | 发品下拉（不分页） |

**列表 Query：** `pageNum`、`pageSize`、`keyword`（模版名称模糊，可选）。

**列表排序：** `is_default DESC, create_time DESC`（默认置顶）。

### 3.2 列表 `GET /list`

**`rows[]` → `FreightTemplateListVO`：**

| 字段 | 类型 | 说明 |
|------|------|------|
| templateId | long | |
| templateName | string | |
| freeFlag | string | 0/1 |
| freeFlagLabel | string | 是/否 或 包邮/不包邮 |
| pricingType | string | 1/2；包邮时 null |
| pricingTypeLabel | string | 按件数/按重量/— |
| shipTime | string | |
| isDefault | string | 0/1 |

```json
{
  "code": 200,
  "total": 2,
  "rows": [
    {
      "templateId": 1,
      "templateName": "全国包邮",
      "freeFlag": "1",
      "pricingType": null,
      "shipTime": "24小时内发货",
      "isDefault": "1"
    },
    {
      "templateId": 2,
      "templateName": "江浙沪特惠",
      "freeFlag": "0",
      "pricingType": "1",
      "shipTime": "48小时内发货",
      "isDefault": "0"
    }
  ]
}
```

### 3.3 详情 `GET /{templateId}`

```json
{
  "code": 200,
  "data": {
    "templateId": 2,
    "templateName": "江浙沪特惠",
    "shipTime": "48小时内发货",
    "isDefault": "0",
    "freeFlag": "0",
    "pricingType": "1",
    "defaultRule": {
      "firstUnit": 1,
      "firstFee": 8.00,
      "addUnit": 1,
      "addFee": 2.00,
      "freeThreshold": 99.00
    },
    "regionRules": [
      {
        "provinceIds": [3, 10, 11],
        "provinceNames": "河北省,江苏省,浙江省",
        "firstUnit": 1,
        "firstFee": 12.00,
        "addUnit": 1,
        "addFee": 5.00,
        "freeThreshold": null
      }
    ]
  }
}
```

> **包邮模版：** `pricingType`、`defaultRule`、`regionRules` 均为 **空**。

### 3.4 新增 / 编辑 `POST /`、`PUT /`

**请求体 `FreightTemplateSaveDTO`：**

```json
{
  "templateId": 2,
  "templateName": "江浙沪特惠",
  "shipTime": "48小时内发货",
  "isDefault": "0",
  "freeFlag": "0",
  "pricingType": "1",
  "defaultRule": {
    "firstUnit": 1,
    "firstFee": 8,
    "addUnit": 1,
    "addFee": 2,
    "freeThreshold": 99
  },
  "regionRules": [
    {
      "provinceIds": [19, 20],
      "firstUnit": 1,
      "firstFee": 15,
      "addUnit": 1,
      "addFee": 8,
      "freeThreshold": null
    }
  ]
}
```

| 校验 | 说明 |
|------|------|
| templateName | 非空 ≤64；本店唯一（编辑排除自身） |
| shipTime | 非空 ≤64 |
| freeFlag | 0/1 |
| freeFlag=1 | **忽略** pricingType、defaultRule、regionRules |
| freeFlag=0 | pricingType 必填；defaultRule 首/续 **>0**、fee **≥0** |
| addUnit | **>0**（FT6） |
| freeThreshold | 可选；若填须 **>0** |
| regionRules | 可选；`provinceIds` 省级且不 **跨规则重叠** |
| templateId | PUT 必填且属于当前店 |

**持久化（PUT · 事务）：**

```text
UPDATE biz_shop_freight_template
clearDefaultIfNeeded(shopId) when isDefault=1
DELETE biz_shop_freight_template_region WHERE template_id=?
INSERT region rules（sort_no 按数组顺序）
```

### 3.5 设为默认 `PUT /default/{templateId}`

```text
校验 template 属于当前 shop
    → UPDATE 同 shop 全部 is_default='0'
    → UPDATE 当前 template is_default='1'
```

**响应：** `{ "templateId": 1, "isDefault": "1" }`

### 3.6 删除 `DELETE /{templateIds}`

```text
校验均属于当前 shop
    → 对每个 ID：IGoodsFacade.existsByFreightTemplateId
    ├── 任一有引用 → FreightTemplateBatchDeleteException；整批取消
    └── 全无 → logicDelete template；DELETE region 行（物理删）
```

### 3.7 下拉 `GET /options`

```json
{
  "code": 200,
  "data": [
    { "templateId": 1, "templateName": "全国包邮", "isDefault": "1", "freeFlag": "1" },
    { "templateId": 2, "templateName": "江浙沪特惠", "isDefault": "0", "freeFlag": "0" }
  ]
}
```

**商品侧挂载（已实现）：**

| 方法 | 路径 | 说明 |
|------|------|------|
| GET | `/agri/seller/goods/freightTemplateOptions` | 委托 `IFreightTemplateFacade.listOptionsByShopId` |
| GET | `/agri/seller/goods/freightDefault` | 默认模版预填（`freightType=2` + templateId） |
| GET | `/agri/seller/goods/freightTemplate/{templateId}` | 发品回显模版详情 |

---

## 4. 运费计算（`FreightCalcSupport`）

> **v1.0 确认订单** 仍可能 **freight=0**；本节定义 **统一算法**，供后续 `CheckoutSupport` 与 C 端详情摘要 **共用**。

### 4.1 规则选取

```text
输入：templateId, shopId, provinceId（收货地址省级 region.id）, pieceCount, totalWeightKg, goodsAmount
    ↓
load template + regions
    ↓
free_flag=1 → 返回 0
    ↓
在 regionRules 中查找 provinceId ∈ province_ids
    ├── 命中 → 用该地区 rule
    └── 未命中 → 用主表 defaultRule
    ↓
若 rule.free_threshold != null 且 goodsAmount >= threshold → 返回 0
    ↓
按 pricing_type 计算（见需求 §7.2）
```

### 4.2 按件数

```java
if (pieceCount <= firstUnit) return firstFee;
BigDecimal extra = pieceCount.subtract(firstUnit);
int steps = extra.divide(addUnit, 0, RoundingMode.CEILING).intValue();
return firstFee.add(addFee.multiply(BigDecimal.valueOf(steps)));
```

### 4.3 按重量

```java
if (totalWeightKg <= firstUnit) return firstFee;
BigDecimal extra = totalWeightKg.subtract(firstUnit);
int steps = extra.divide(addUnit, 0, RoundingMode.CEILING).intValue();
return firstFee.add(addFee.multiply(BigDecimal.valueOf(steps)));
```

### 4.4 展示摘要 `buildFreightDesc`

| 场景 | 示例 |
|------|------|
| 包邮 | 「包邮」 |
| 按件 + 有首费 | 「运费 ¥8.00 起」或「首件 ¥8.00，续件 ¥2.00/件」 |
| 满免 | 追加「满 ¥99 包邮」 |

---

## 5. 对外协作

### 5.1 商品模块（下游 · 已实现）

| 场景 | 实现 |
|------|------|
| 发品下拉 | `GET /agri/seller/goods/freightTemplateOptions` |
| 默认预填 | `GET /agri/seller/goods/freightDefault` |
| 保存 | `GoodsSaveDTO.freightType` **必填**；upsert `biz_goods_freight` |
| 删模版校验 | `IGoodsFacade.existsByFreightTemplateId` |

### 5.2 确认订单（C 端 · 规划）

| 场景 | 实现 |
|------|------|
| preview/submit | `CheckoutSupport.calcFreight` 聚合 **同店** 各行；多商品 **合并件数/重量** 后 **按店一次运费**（产品口径：同店一单 **一个运费**，与确认订单 v1 一致） |
| 地址变更 | 重算时传入 **省 ID**（由 `region_code` 解析省级） |

### 5.3 平台 / C 端

- 平台 **无** 本模块接口；订单 **只读** `freight_amount`。  
- C 端详情 **不直读** 模版表；经 **商品 freight 配置 + Facade 摘要**。

---

## 6. 核心流程

### 6.1 Service 伪代码

```text
requireShopId()
assertShopMaintable(shopId)
validateSaveDto()                    // FT4～FT8
assertNameUnique(shopId, name)
insertTemplate + insertRegions       // @Transactional
updateTemplate + replaceRegions
setDefault(templateId)               // 清其它 default
existsByFreightTemplateId()          // 删前
logicDeleteByIds()                   // 批量整批失败
```

### 6.2 端到端

```text
【商家】X-Shop-Id → POST /freightTemplate
            ↓
【商家】商品列表 → 选模版 / 默认预填 → biz_goods_freight
            ↓
【C 端】确认订单 → calcFreight → biz_order.freight_amount
```

---

## 7. 业务规则映射

| 编号 | 实现 |
|------|------|
| FT0 | 拦截器 + SQL `shop_id` |
| FT1～FT2 | `setDefault` / 保存时 `clearDefaultExcept` |
| FT3 | 创建时写死 `shop_id`；更新 **不改** shop_id |
| FT4 | `countByName` |
| FT5 | `free_flag=1` 跳过计价校验 |
| FT6 | 不包邮校验 defaultRule + pricing_type |
| FT7 | `validateRegionOverlap` |
| FT8 | `free_threshold` 可选 |
| FT9 | `existsByFreightTemplateId` + 批量整批失败 |
| FT10 | 同属性模版批量删 |
| FT11 | **无** UPDATE biz_order |
| FT12 | 商品侧 `freight_type` 互斥 |
| FT13 | list `keyword` + shop 过滤 |

---

## 8. 菜单权限

| 权限 | 标识 |
|------|------|
| 列表 / 下拉 | `agri:seller:freightTemplate:list` |
| 详情 | `agri:seller:freightTemplate:query` |
| 新增 | `agri:seller:freightTemplate:add` |
| 修改 / 设默认 | `agri:seller:freightTemplate:edit` |
| 删除 | `agri:seller:freightTemplate:remove` |

**菜单 SQL：** `sql/agri_seller_freight_template_menu.sql`  
**组件路径：** `agri/seller/freightTemplate/index`  
**上级菜单：** 店铺后台 · 物流 / 商品管理（与属性模版并列，以前端菜单为准）。

---

## 9. 校验与错误 msg

| 场景 | msg |
|------|-----|
| 未选店铺 | 请先选择当前店铺 |
| 模版名称为空 | 请输入运费模版名称 |
| 模版名称重复 | 运费模版名称已存在 |
| 发货时间为空 | 请输入发货时间 |
| 不包邮缺计价方式 | 请选择计价方式 |
| 首/续计量无效 | 首件数/续件数须大于0（或首重/续重） |
| 金额为负 | 运费金额不能为负数 |
| 地区重叠 | 指定地区存在重叠，请调整 |
| 删除有引用 | 该运费模版已被商品使用，无法删除 |
| 批量删失败 | 部分运费模版不满足删除条件，整批操作已取消 |
| 跨店 ID | 运费模版不存在 |
| 店铺已关闭 | 店铺已关闭，无法维护运费模版 |

---

## 10. 测试要点

| 类型 | 覆盖 |
|------|------|
| 单元 | `FreightTemplateServiceImpl`：shop scope；名称唯一；包邮/不包邮校验；地区重叠；设默认互斥 |
| 单元 | `FreightCalcSupport`：按件/按重量/满免/地区命中 |
| 接口 | `/list`、`/{id}`、POST/PUT、`/default/{id}`、DELETE 批量、`/options` |
| 协作 | `existsByFreightTemplateId`；商品 `/freightTemplateOptions`、`/freightDefault` |

---

## 11. 实现分期

| 阶段 | 内容 | 状态 |
|------|------|------|
| **v1.0** | 模版两表 CRUD；设默认；`/options`；`FreightCalcSupport`；`existsByFreightTemplateId` | **已实现** |
| **v1.0·商品协作** | `biz_goods_freight`；发品保存/回显；默认预填 | **已实现** |
| **v1.1·确认订单** | `CheckoutSupport.calcFreight` 真实计费 | **规划** |
| **非本期** | 模版导入导出、版本历史、平台代维、多物流公司 | — |

---

## 12. 修订记录

| 版本 | 说明 |
|------|------|
| **v1.0** | 首版：两表模型；`/agri/seller/freightTemplate`；计费 Support；与商品列表/确认订单/订单管理协作；对齐《运费模版功能需求》v1.0 |
| **v1.1** | 同步代码：v1.0 模版 CRUD + Facade + 测试 **已实现**；商品侧 `biz_goods_freight` 保存/回显/默认预填 **已实现** |

---

*文档版本：v1.1 · 关联《运费模版功能需求.md》v1.0、《属性模版技术方案.md》v1.3、《店铺商品列表技术方案.md》v1.6、《确认订单页（多商品）技术方案.md》v1.0*
