# 承销商管理 — 技术方案

> 依据：同目录 `承销商管理功能需求.md`。承销商主档 **CRUD** + **逻辑删除** + **编号自动生成（2 位）** + **销售去向**；列表编号/名称 **模糊**筛选。

---

## 1. 技术架构

| 项 | 说明 |
| --- | --- |
| **后端** | RuoYi **v3.9.2**（**springboot2** 分支）：JDK 8、Spring Boot 2.x、Spring MVC、MyBatis、Druid |
| **数据库** | MySQL **5.7.39**，InnoDB，`utf8mb4` |
| **前端** | 若依 Vue2；字段 **小驼峰**（本方案不展开页面） |

**分层**：`Controller` → `Service`（`DistributorValidation`、**编号生成**、**分配账号**）→ `Mapper`/XML → `domain`。

**代码包**：`com.ruoyi.web.modules.trading`

**外部依赖**：`ISysUserService`、`TradePartnerAccountAssigner`（与供应商共用）

**业务摘要**

| 场景 | 行为 |
| --- | --- |
| 新增 | 校验通过后 `DistributorCodeGenerator` 分配 `distributor_code`（`01`～`99`）；`del_flag=0` |
| 修改 | **不可改** `distributor_code` |
| 列表/详情 | 仅 `del_flag=0`；排序 `distributor_code ASC, id ASC` |
| 列表筛选 | `distributor_code`、`distributor_name` **模糊**匹配（`LIKE %keyword%`） |
| 删除 | 逻辑删除 `del_flag=2`；存在**未支付**订单时 `ServiceException`；已分配账号联动删用户 |
| 分配账号 | `account_assigned=0`；角色 **104**；登录名拼音首字母+`id` |

---

## 2. 数据库设计

### 2.1 表 `biz_distributor`（承销商）

| 字段 | 类型 | 非空 | 默认值 | 说明 |
| --- | --- | --- | --- | --- |
| `id` | `bigint(20)` | Y | 自增 | 主键 |
| `distributor_code` | `char(2)` | Y | — | 承销商编号 `01`～`99`，**全局唯一** |
| `distributor_name` | `varchar(64)` | Y | — | 承销商名称（**1～20** 字） |
| `distributor_type` | `tinyint(4)` | Y | — | `1` 个体户 `2` 企业 |
| `identity_no` | `varchar(32)` | Y | — | 身份证 / 统一社会信用代码 |
| `contact_phone` | `varchar(20)` | Y | — | 手机号 |
| `business_location` | `varchar(50)` | N | NULL | 经营地点（≤50 字） |
| `sales_destination` | `tinyint(4)` | Y | — | `1` 本市 `2` 本省 `3` 外省 |
| `account_assigned` | `tinyint(4)` | Y | `0` | `0` 未分配 `1` 已分配 |
| `sys_user_id` | `bigint(20)` | N | NULL | 关联用户 ID |
| `assigned_login_name` | `varchar(64)` | N | NULL | 已分配登录名 |
| `del_flag` | `char(1)` | Y | `'0'` | `0` 存在 `2` 删除 |
| `create_by` / `create_time` / `update_by` / `update_time` | 若依惯例 | — | — | 审计 |
| `remark` | `varchar(500)` | N | NULL | 备注 |

**索引**：`PRIMARY KEY (id)`；`UNIQUE KEY uk_distributor_code (distributor_code)`；`KEY idx_distributor_name (distributor_name)`；`KEY idx_identity_no (identity_no)`；`KEY idx_sales_destination (sales_destination)`；`KEY idx_del_flag (del_flag)`。

> **唯一性**：`distributor_name`、`identity_no` 在 `del_flag=0` 范围内由 Service 校验唯一。

### 2.2 DDL（MySQL 5.7）

```sql
CREATE TABLE `biz_distributor` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键',
  `distributor_code` char(2) NOT NULL COMMENT '承销商编号01-99',
  `distributor_name` varchar(64) NOT NULL COMMENT '承销商名称',
  `distributor_type` tinyint(4) NOT NULL COMMENT '1个体户 2企业',
  `identity_no` varchar(32) NOT NULL COMMENT '身份证号或统一社会信用代码',
  `contact_phone` varchar(20) NOT NULL COMMENT '联系方式',
  `business_location` varchar(50) DEFAULT NULL COMMENT '经营地点',
  `sales_destination` tinyint(4) NOT NULL COMMENT '1本市 2本省 3外省',
  `account_assigned` tinyint(4) NOT NULL DEFAULT '0' COMMENT '0未分配1已分配',
  `sys_user_id` bigint(20) DEFAULT NULL COMMENT '关联用户ID',
  `assigned_login_name` varchar(64) DEFAULT NULL COMMENT '已分配登录名',
  `del_flag` char(1) NOT NULL DEFAULT '0' COMMENT '0存在 2删除',
  `create_by` varchar(64) DEFAULT '' COMMENT '创建者',
  `create_time` datetime DEFAULT NULL COMMENT '创建时间',
  `update_by` varchar(64) DEFAULT '' COMMENT '更新者',
  `update_time` datetime DEFAULT NULL COMMENT '更新时间',
  `remark` varchar(500) DEFAULT NULL COMMENT '备注',
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_distributor_code` (`distributor_code`),
  KEY `idx_distributor_name` (`distributor_name`),
  KEY `idx_identity_no` (`identity_no`),
  KEY `idx_sales_destination` (`sales_destination`),
  KEY `idx_del_flag` (`del_flag`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='承销商';
```

### 2.3 枚举

**承销商类型 `distributor_type`**

| 值 | 标签 |
| --- | --- |
| 1 | 个体户 |
| 2 | 企业 |

**销售去向 `sales_destination`**

| 值 | 标签 |
| --- | --- |
| 1 | 本市 |
| 2 | 本省 |
| 3 | 外省 |

**删除标记 `del_flag`**

| 值 | 含义 |
| --- | --- |
| 0 | 正常 |
| 2 | 已逻辑删除 |

**账号分配**：`account_assigned` 同供应商；默认角色 **`role_id=104`**（承销商）。

### 2.4 编号生成（`DistributorCodeGenerator`）

与供应商模块一致：`MAX(distributor_code)` + 1，格式 `%02d`；上限 `99`；与插入同事务防并发重复。

### 2.5 分配账号

与供应商共用 `TradingPartnerLoginNameGenerator`、`TradePartnerAccountAssigner`；`distributor_name` + `id` 生成登录名；`DistributorRules.ROLE_ID_DISTRIBUTOR = 104`。

---

## 3. 接口设计

**统一响应**：`AjaxResult` / `TableDataInfo`。

**权限标识**：`tradeMarket:distributor:list|query|add|edit|remove|assignAccount`

**Base Path**：`/tradeMarket/distributor`

| # | 说明 | Method | URI | 权限 |
| --- | --- | --- | --- | --- |
| 3.1 | 分页列表 | GET | `/tradeMarket/distributor/list` | `list` |
| 3.2 | 详情 | GET | `/tradeMarket/distributor/{id}` | `query` |
| 3.3 | 新增 | POST | `/tradeMarket/distributor` | `add` |
| 3.4 | 修改 | PUT | `/tradeMarket/distributor` | `edit` |
| 3.5 | 删除 | DELETE | `/tradeMarket/distributor/{ids}` | `remove` |
| 3.6 | 分配账号 | POST | `/tradeMarket/distributor/assignAccount/{id}` | `assignAccount` |

#### 3.1.1 列表 Query

| 参数 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `pageNum` / `pageSize` | int | N | 默认 `1` / `20` |
| `distributorCode` | string | N | 承销商编号**模糊**：`distributor_code LIKE %value%` |
| `distributorName` | string | N | 承销商名称**模糊**：`distributor_name LIKE %value%` |

**列表行扩展（驼峰）**：`distributorTypeName`、`salesDestinationName` + `accountAssigned`、`assignedLoginName`。

**排序**：`distributor_code ASC, id ASC`。

#### 3.2.1 详情 `data`

全业务字段（驼峰）+ 类型/销售去向中文名 + 审计字段。

#### 3.3.1 新增 / 修改 Body（驼峰）

| 字段 | 新增 | 修改 | 说明 |
| --- | --- | --- | --- |
| `id` | — | Y | 主键 |
| `distributorCode` | — | — | **禁止传入**；仅响应带回 |
| `distributorName` | Y | Y | 1～20 字，trim |
| `distributorType` | Y | Y | `1` / `2` |
| `identityNo` | Y | Y | 身份证 / USCC |
| `contactPhone` | Y | Y | 手机号 |
| `businessLocation` | N | N | ≤50 字 |
| `salesDestination` | Y | Y | `1` / `2` / `3` |
| `remark` | N | N | ≤500 字 |

#### 3.4 服务端校验（`DistributorValidation`）

| 项 | 规则 |
| --- | --- |
| 名称 | trim 后 1～20 字；未删除记录内**名称唯一** |
| 类型 + 身份号 | `1` 身份证；`2` 18 位 USCC；**身份号唯一** |
| 手机 | 项目统一手机号规则 |
| 销售去向 | 必须为 `1`/`2`/`3` |
| 修改 | 禁止改 `distributor_code`；记录存在且未删 |
| 删除 | 未删；`DistributorReferenceChecker.hasUnpaidOrder(id)` 为 false（`biz_trade_order` 待支付且未删） |

#### 3.5 删除

- `DELETE /tradeMarket/distributor/{ids}`，前端单条传一个 id。  
- `del_flag='2'`；存在未支付订单时失败，msg「存在未支付订单，无法删除」。

#### 3.6 Mapper 约定

- 条件均带 `del_flag='0'`。  
- 模糊筛选示例：  
  `AND distributor_code LIKE CONCAT('%', #{distributorCode}, '%')`  
  `AND distributor_name LIKE CONCAT('%', #{distributorName}, '%')`  
- 逻辑删除：`UPDATE … SET del_flag='2' WHERE id=? AND del_flag='0'`。

---

## 4. 菜单与权限（示例）

| 类型 | 名称 | 权限标识 |
| --- | --- | --- |
| 菜单 | 承销商管理 | `tradeMarket:distributor:list` |
| 按钮 | 查询 | `tradeMarket:distributor:query` |
| 按钮 | 新增 | `tradeMarket:distributor:add` |
| 按钮 | 修改 | `tradeMarket:distributor:edit` |
| 按钮 | 删除 | `tradeMarket:distributor:remove` |
| 按钮 | 分配账号 | `tradeMarket:distributor:assignAccount` |

组件路径：`tradeMarket/distributor/index`（「交易市场平台」目录下）。

---

## 5. 与供应商模块差异（实施对照）

| 项 | 供应商 `biz_supplier` | 承销商 `biz_distributor` |
| --- | --- | --- |
| 列表筛选 | 编号/名称**精确** | **模糊** `LIKE` |
| 特色字段 | `fee_method` 及费率 | `sales_destination` |
| Base Path | `/tradeMarket/supplier` | `/tradeMarket/distributor` |
| 分配账号角色 | **103** | **104** |
| 分配账号接口 | `POST …/supplier/assignAccount/{id}` | `POST …/distributor/assignAccount/{id}` |

---

## 6. 交付清单

- [x] `sql/biz_distributor.sql`、`sql/alter_biz_supplier_distributor_assign_account.sql`  
- [x] `BizDistributor`、Mapper/XML、Service、Controller  
- [x] `DistributorValidation`、`DistributorCodeGenerator`、`DistributorRules`  
- [x] 共用 `TradePartnerAccountAssigner`、`TradingPartnerLoginNameGenerator`  
- [x] 单元测试 / MockMvc（含 `assignAccount`）

---

## 7. 修订记录

| 版本 | 说明 |
| --- | --- |
| 1.0 | 初稿：`biz_distributor`；CRUD+逻辑删除；2 位编号；销售去向；列表模糊；`tradeMarket/distributor` |
| 1.1 | 移除 `vehicle_plates` 字段及 API `vehiclePlates`（对齐修订草稿） |
| 1.2 | 分配账号：三字段、`POST assignAccount`、角色 104、拼音首字母+id、删除联动 |
| 1.3 | 删除约束：`DistributorReferenceChecker.hasUnpaidOrder`；`biz_trade_order` 待支付拦截 |
