# 配送设置 — 技术方案

> **依据：** 《配送设置功能需求.md》v1.2  
> **关联：** 平台《关联需求分析.md》v1.6、《店铺管理功能需求.md》v1.3.7、《订单管理功能需求.md》v1.0.1、《订单管理技术方案.md》v1.0.2；商家《运费模版技术方案.md》v1.1、《店铺设置技术方案.md》v1.1、《发货管理功能需求.md》v1.0、《商家端店铺上下文接口说明》；C 端《商品详情内页技术方案.md》§2.2  
> **范围：** 商家端 **当前店铺** 发货信息 **查询 + 保存（Upsert）**；C 端商品详情 **只读发货城市**；**不含** 平台维护、订单发货预填、运费模版。  
> **原则：** **每店一份** `uk_shop_id`；`shop_id = X-Shop-Id` 隔离；保存 **覆盖**；修改 **不追溯** 历史订单。

---

## 1. 技术架构

| 项 | 选型 |
|----|------|
| 基础框架 | RuoYi **v3.9.2**（`springboot2` 分支） |
| 数据库 | **MySQL 5.7.39** |
| ORM / 权限 / 响应 | MyBatis；`@PreAuthorize`；`AjaxResult` |
| 店铺上下文 | `SellerShopContextInterceptor` + **`X-Shop-Id`** |
| 行政区划 | 复用 **`biz_region`** + 已有 **`GET /agri/region/tree`** |
| 日志 | 保存 `@Log` |
| C 端读 | `IShopDeliveryFacade.getShipCity(shopId)` |

### 1.1 与平台 / 关联模块边界

| 维度 | 平台 · 店铺/订单 | **商家 · 配送设置** | C 端 · 商品详情 |
|------|------------------|---------------------|-----------------|
| 维护端 | **无** 本模块 | **仅** 商家端 | **只读** |
| 路径 | `/agri/shop`、`/agri/order` | **`/agri/seller/delivery`** | `/api/goods/{id}` |
| 数据 | `biz_shop` 店资料 | **`biz_shop_delivery_setting`** | 读 **`ship_city`** |
| 订单发货 | 写 `biz_order` 履约 + **发货人/地址快照** | **快照来源**（只读） | — |

### 1.2 协作链

```text
【平台】开店 → 本表 **无预置行**
            ↓
【登录】GET /agri/seller/context → X-Shop-Id
            ↓
【本模块】GET  /agri/seller/delivery        → 回显 / null
           PUT  /agri/seller/delivery        → Upsert（每店一份）
            ↓
【C 端】GoodsAppServiceImpl.buildLogistics
        → IShopDeliveryFacade.getShipCity(goods.shopId)
        → logistics.shipCity（未配置则 null，前端隐藏）
            ↓
【并行 · 不耦合】
  运费模版 /agri/seller/freightTemplate  → 运费规则
  订单发货 /agri/seller/order/{id}/ship  → 读本表快照至 biz_order（DS6 不预填弹窗）
```

| 关联模块 | 协作 |
|----------|------|
| **运费模版** | **并列**；无表关联 |
| **店铺设置** | **并列**；`biz_shop.shop_phone` vs 本表 `shipper_mobile` |
| **发货管理 / 全部订单** | 发货时 **读** 本表快照至订单；**不 UPDATE** 本表 |
| **平台 · 订单管理** | 只读订单快照；**不** 维护本表 |
| **商品详情（C 端）** | **只读** `ship_city` |
| **行政区划** | 保存时校验 `biz_region` 三级链 |

### 1.3 模块落位

**包路径：** `com.ruoyi.web.modules.delivery`（与 `freight`、`store` **并列**）

```text
baqing-shop/src/main/java/com/ruoyi/web/modules/delivery/
├── controller/SellerDeliveryController.java     # /agri/seller/delivery
├── service/
│   ├── IShopDeliveryService.java
│   └── impl/ShopDeliveryServiceImpl.java
├── facade/
│   ├── IShopDeliveryFacade.java               # C 端 / 内部读 shipCity
│   └── impl/ShopDeliveryFacadeImpl.java
├── domain/BizShopDeliverySetting.java
├── dto/ShopDeliverySaveDTO.java
│   └── DeliveryRegionDTO.java                 # 发货地区（省市区）
├── vo/ShopDeliverySettingVO.java
│   └── DeliveryRegionVO.java
├── mapper/BizShopDeliverySettingMapper.java
└── constant/DeliveryConstants.java

baqing-shop/src/main/java/com/ruoyi/web/modules/home/
└── service/impl/GoodsAppServiceImpl.java        # 接 Facade 填 shipCity

sql/biz_shop_delivery_setting.sql              # 权威 DDL
```

**前端（建议）：**

| 项 | 值 |
|----|-----|
| 页面 | `ruoyi-ui/src/views/agri/seller/delivery/index.vue` |
| API | `ruoyi-ui/src/api/agri/seller/delivery.js` |
| 省市区 | 复用 `GET /agri/region/tree` |

---

## 2. 数据库设计

**新建表 1 张。** 权威 DDL：[`sql/biz_shop_delivery_setting.sql`](../../../../sql/biz_shop_delivery_setting.sql)。

> **说明：** 表单「发货地区（省市区）」在接口层为 **`region` 对象**；落库为 **`region_code` + `region_name` + `city_name`** 三列，与会员地址、商户经营地址口径一致（区县级 `biz_region.code`）。

**已有 `district_id` 列** 请执行迁移：[`sql/biz_shop_delivery_setting_migrate_region_code.sql`](../../../../sql/biz_shop_delivery_setting_migrate_region_code.sql)

### 2.1 表 `biz_shop_delivery_setting`

| 字段 | 类型 | 必填 | 说明 |
|------|------|:----:|------|
| setting_id | bigint PK AI | ✓ | 主键 |
| shop_id | bigint | ✓ | **UK**；= 当前店铺 |
| shipper_name | varchar(64) | ✓ | 发货人 |
| shipper_mobile | varchar(20) | ✓ | 发货人联系方式 |
| region_code | varchar(64) | ✓ | 发货地区 **区县级** `biz_region.code` |
| region_name | varchar(128) | ✓ | 省/市/区名称快照，`/` 拼接 |
| city_name | varchar(64) | ✓ | 市名称快照；**C 端 shipCity 来源** |
| detail_address | varchar(256) | ✓ | 详细地址 |
| create_by / create_time | | ✓ | 首次保存 |
| update_by / update_time | | | 覆盖保存 |

**约束：**

| 项 | 说明 |
|----|------|
| `UNIQUE KEY uk_shop_delivery (shop_id)` | **每店一份**（DS2） |
| 无 del_flag | **不提供删除**；仅 Upsert 覆盖 |
| 与 `biz_shop` | `shop_id` 逻辑外键；删店后 **不可再维护** |

**DDL 摘要：**

```sql
CREATE TABLE IF NOT EXISTS `biz_shop_delivery_setting` (
  `setting_id`     bigint(20)   NOT NULL AUTO_INCREMENT,
  `shop_id`        bigint(20)   NOT NULL COMMENT '店铺ID',
  `shipper_name`   varchar(64)  NOT NULL COMMENT '发货人',
  `shipper_mobile` varchar(20)  NOT NULL COMMENT '发货人联系方式',
  `region_code`    varchar(64)  NOT NULL COMMENT '区县级 biz_region.code',
  `region_name`    varchar(128) NOT NULL COMMENT '省/市/区名称快照',
  `city_name`      varchar(64)  NOT NULL COMMENT '市名称快照; C端发货地',
  `detail_address` varchar(256) NOT NULL COMMENT '详细地址',
  `create_by`      varchar(64)  DEFAULT NULL,
  `create_time`    datetime     NOT NULL,
  `update_by`      varchar(64)  DEFAULT NULL,
  `update_time`    datetime     DEFAULT NULL,
  PRIMARY KEY (`setting_id`),
  UNIQUE KEY `uk_shop_delivery` (`shop_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='店铺配送设置(发货信息,每店一份)';
```

### 2.2 索引

| 索引 | 用途 |
|------|------|
| `uk_shop_delivery (shop_id)` | 商家读写、C 端按店查 city |

### 2.3 与 `biz_region` 校验（保存时）

**入参：** `region.regionCode`（区县级 `code`，字符串）

```text
selectByCode(regionCode) → type=3
  → 向上查 city(type=2) → province(type=1)
```

校验通过后写入：
- `region_code` ← `region.regionCode`
- `region_name` ← `省/市/区` 名称 `/` 拼接
- `city_name` ← 市级名称（C 端直读）

**回显：** `region_code` 查 `biz_region` 反推 `provinceId/cityId/districtId`（级联回显）；名称读 `region_name` 快照。

---

## 3. 接口设计

**基路径：** `/agri/seller/delivery`  
**上下文：** **`X-Shop-Id`** 必填

### 3.1 接口一览

| 方法 | 路径 | 权限 | 说明 |
|------|------|------|------|
| GET | `/` | `agri:seller:delivery:query` | 查询当前店设置 |
| PUT | `/` | `agri:seller:delivery:edit` | 保存（Upsert） |

> **说明：** 无 POST/DELETE；**PUT 即新增或覆盖**。

### 3.2 查询 `GET /`

**固定条件：** `shop_id = requireShopId()`。

**响应：**

| 场景 | data |
|------|------|
| 已配置 | `ShopDeliverySettingVO`（§3.4） |
| 未配置 | `null` 或 `{}`（前端 **空表单**） |

### 3.3 保存 `PUT /`

**Body（ShopDeliverySaveDTO）：**

```json
{
  "shipperName": "张三",
  "shipperMobile": "13800138000",
  "region": {
    "regionCode": "540102"
  },
  "detailAddress": "某某街道 1 号"
}
```

| 字段 | 校验 |
|------|------|
| shipperName | 非空；≤64 |
| shipperMobile | 非空；11 位手机号（与 `ShopConstants` / 全项目手机规则一致） |
| region | 非空；`regionCode` 非空；须为 **区县级** 合法 `biz_region.code`（§2.3） |
| detailAddress | 非空；≤256 |

**逻辑：**

```text
SELECT BY shop_id
  ├── 无行 → INSERT（写 create_by/time + 名称快照）
  └── 有行 → UPDATE 全字段（写 update_by/time）
```

**名称快照：** Service 据 `regionCode` 查 `biz_region` 并向上取省/市/区名称写入；**不依赖** 前端传 `regionName`。

### 3.4 VO `ShopDeliverySettingVO`

| 字段 | 说明 |
|------|------|
| settingId | |
| shopId | |
| shipperName | |
| shipperMobile | |
| region | 发货地区；含 `regionCode`、级联回显 `provinceId/cityId/districtId`、名称 `provinceName/cityName/districtName` |
| detailAddress | |
| updateTime | 可选 |

### 3.5 业务错误 msg

| 场景 | msg |
|------|-----|
| 未选店铺 | 请先选择当前店铺 |
| 发货人为空 | 请填写发货人 |
| 手机无效 | 请填写正确的发货人联系方式 |
| 区划不完整 | 请选择完整的发货地区 |
| 详细地址为空 | 请填写发货详细地址 |
| 店无效/越权 | 当前店铺无效或无权操作 |

---

## 4. Service 分层

```text
SellerDeliveryController
    → IShopDeliveryService
        → getByShopId(shopId)
        → save(shopId, dto, operator)
        → BizShopDeliverySettingMapper
        → BizRegionMapper（校验 + 取名称快照）
        → SellerShopContextSupport.assertShopAccessible（可选）

GoodsAppController（C 端 · 已有）
    → GoodsAppServiceImpl.buildLogistics
        → IShopDeliveryFacade.getShipCity(shopId)
            → SELECT city_name FROM biz_shop_delivery_setting WHERE shop_id=?
```

**Upsert 伪代码：**

```java
void save(Long shopId, ShopDeliverySaveDTO dto, String operator) {
    validate(dto);
    assertRegionChain(dto.getProvinceId(), dto.getCityId(), dto.getDistrictId());
    BizShopDeliverySetting row = toEntity(dto, shopId, loadRegionNames(...));
    if (mapper.selectByShopId(shopId) == null) {
        mapper.insert(row);
    } else {
        mapper.updateByShopId(row);
    }
}
```

---

## 5. C 端协作

| 方法 | 路径 | 说明 |
|------|------|------|
| GET | `/api/goods/{goodsId}` | `logistics.shipCity` ← Facade |

**规则（DS4）：**

```java
String city = deliveryFacade.getShipCity(goods.getShopId());
logistics.setShipCity(city); // null → 前端隐藏或兜底
```

> **运费 / 承诺发货** 仍走 **`biz_goods_freight` / 服务快照**；**不** 读本表其它字段。

---

## 6. 菜单与权限

**上级菜单：** **物流相关**（与 **运费模版** 并列）

| 菜单 | 路由 | 组件 | 权限前缀 |
|------|------|------|----------|
| 配送设置 | `/agri/seller/delivery` | `agri/seller/delivery/index` | `agri:seller:delivery` |

| 权限 | 说明 |
|------|------|
| `agri:seller:delivery:query` | 查询 |
| `agri:seller:delivery:edit` | 保存 |

**菜单 SQL（待配置）：** `sql/agri_seller_delivery_menu.sql`

---

## 7. Mapper 方法

| 方法 | 说明 |
|------|------|
| `selectByShopId(shopId)` | GET / Upsert 判断 |
| `insert(setting)` | 首次保存 |
| `updateByShopId(setting)` | 覆盖保存 |
| `selectCityNameByShopId(shopId)` | Facade 只读 city |

---

## 8. 与平台方案对照

| 平台模块 | 与本模块 |
|----------|----------|
| **店铺管理** | 开店 **不写入** 本表；删店后商家 **不可维护** |
| **订单管理** | 订单 `consignee_*` = 买家地址；**无关** 本表 `shipper_*` |
| **关联需求分析 §5.1** | 发品 **不依赖** 本表；C 端展示 **可选增强** |
| **会员管理** | 买家地址 `biz_member_address`；**独立** |

---

## 9. 测试要点

| 类型 | 要点 |
|------|------|
| 单测 | Upsert；区划链校验；手机校验；shop 隔离 |
| 接口 | `SellerDeliveryControllerTest`：无 X-Shop-Id、GET null、PUT 回显 |
| 协作 | 保存后 Facade 返回 city；Goods 详情 logistics 有值 |

---

## 10. 实现状态

| 项 | 状态 |
|----|------|
| DDL `sql/biz_shop_delivery_setting.sql` | **已建** |
| `SellerDeliveryController` / Service / Mapper | **已实现** |
| `IShopDeliveryFacade` | **已实现** |
| `GoodsAppServiceImpl.shipCity` | **已接入 Facade** |
| 商家菜单 SQL | **待配置** |
| 前端页 | **待实现** |

---

## 11. 需求规则映射

| 规则 | 实现 |
|------|------|
| DS1 | `shop_id` 全链路 |
| DS2 | `uk_shop_delivery` + PUT Upsert |
| DS3 | 无预置行；GET 可 null |
| DS4 | Facade → `city_name` → C 端 |
| DS5 | 与 freight 分表分接口 |
| DS6 | 订单 ship **不读** 本表 |
| DS7 | 仅 UPDATE 本表；订单无联动 |
| DS8 | `X-Shop-Id` |
| DS9 | 无平台 Controller |
| DS10 | Service 校验 |
| DS11 | `@PreAuthorize` |

---

## 12. 非本期

| 项 | 说明 |
|----|------|
| 多发货点 | 单表单行 |
| 订单发货预填 | DS6 |
| 平台查询接口 | DS9 |
| 邮编 / 地图选点 | — |
| DELETE 清空 | 仅 Upsert |

---

## 13. 修订记录

| 版本 | 说明 |
|------|------|
| **v1.4** | 发货协作：`shipOrder` **读** 本表快照至 `biz_order`（[`sql/biz_order_ship_snapshot.sql`](../../../../sql/biz_order_ship_snapshot.sql)）；弹窗仍不预填 |
| **v1.3** | `district_id` 改为 `region_code`（区县级 code）；保存入参 `region.regionCode`；迁移脚本 `biz_shop_delivery_setting_migrate_region_code.sql` |
| **v1.2** | 表结构精简：`region_code` + `region_name` + `city_name`（自 v1.3 起 `region_code` 替代 `district_id`） |
| **v1.1** | 对齐功能需求 v1.1：接口 `region` 对象对应「发货地区（省市区）」 |
| **v1.0** | 首版 |

---

*文档版本：v1.4 · 关联《配送设置功能需求.md》v1.2 · 发货快照见《发货管理技术方案.md》v1.3*
