# 供应商管理 — 技术方案

> 依据：同目录 `供应商管理功能需求.md`。供应商主档 **CRUD** + **逻辑删除** + **编号自动生成（2 位）** + **收费方式联动校验**；车辆号牌 JSON 存储。

---

## 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`（`SupplierValidation`、**编号生成**、收费字段清洗、**分配账号**）→ `Mapper`/XML → `domain`。

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

**外部依赖**：平台 **用户/角色**（`ISysUserService`、`TradePartnerAccountAssigner`）

**业务摘要**

| 场景 | 行为 |
| --- | --- |
| 新增 | 校验通过后 `SupplierCodeGenerator` 分配 `supplier_code`（`01`～`99`）；`del_flag=0` |
| 修改 | **不可改** `supplier_code`；按 `fee_method` 清空不适用费率字段 |
| 列表/详情 | 仅 `del_flag=0`；排序 `supplier_code ASC, id ASC` |
| 列表筛选 | `supplier_code`、`supplier_name` **精确**匹配 |
| 删除 | 逻辑删除 `del_flag=2`；存在**未结算**结算单时 `ServiceException`；**已分配账号**时联动 `deleteUserById` |
| 分配账号 | `account_assigned=0`；创建 `sys_user` 绑定角色 **103**；登录名=名称拼音首字母+`id`；成功一次 |

---

## 2. 数据库设计

### 2.1 表 `biz_supplier`（供应商）

| 字段 | 类型 | 非空 | 默认值 | 说明 |
| --- | --- | --- | --- | --- |
| `id` | `bigint(20)` | Y | 自增 | 主键 |
| `supplier_code` | `char(2)` | Y | — | 供应商编号，`01`～`99`，**全局唯一** |
| `supplier_name` | `varchar(64)` | Y | — | 供应商名称（**1～20** 字） |
| `supplier_type` | `tinyint(4)` | Y | — | `1` 个体户 `2` 企业 |
| `identity_no` | `varchar(32)` | Y | — | 身份证号 / 统一社会信用代码（18 位） |
| `contact_phone` | `varchar(20)` | Y | — | 手机号 |
| `business_location` | `varchar(50)` | N | NULL | 经营地点（≤50 字） |
| `vehicle_plates` | `varchar(512)` | N | NULL | 车辆号牌 JSON 数组，最多 **10** 条 |
| `fee_method` | `tinyint(4)` | Y | — | `1` 按户 `2` 按头 `3` 按额 |
| `fee_standard` | `decimal(12,2)` | N | NULL | 收费标准（元/头）；`fee_method=2` 时必填 |
| `transaction_fee_rate` | `decimal(5,2)` | N | NULL | 交易费率（**%**）；`fee_method=3` 时必填 |
| `management_fee_rate` | `decimal(5,2)` | N | NULL | 管理费率（**%**）；`fee_method=3` 时必填 |
| `account_assigned` | `tinyint(4)` | Y | `0` | `0` 未分配 `1` 已分配 |
| `sys_user_id` | `bigint(20)` | N | NULL | 关联 `sys_user.user_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_supplier_code (supplier_code)`；`KEY idx_supplier_name (supplier_name)`；`KEY idx_identity_no (identity_no)`；`KEY idx_del_flag (del_flag)`。

> **唯一性**：`supplier_name`、`identity_no` 在 **未删除**（`del_flag=0`）范围内唯一，由 Service 插入/更新前查询保证（MySQL 5.7 不做部分唯一索引）。

### 2.2 DDL（MySQL 5.7）

```sql
CREATE TABLE `biz_supplier` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键',
  `supplier_code` char(2) NOT NULL COMMENT '供应商编号01-99',
  `supplier_name` varchar(64) NOT NULL COMMENT '供应商名称',
  `supplier_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 '经营地点',
  `vehicle_plates` varchar(512) DEFAULT NULL COMMENT '车辆号牌JSON数组',
  `fee_method` tinyint(4) NOT NULL COMMENT '1按户 2按头 3按额',
  `fee_standard` decimal(12,2) DEFAULT NULL COMMENT '收费标准元/头',
  `transaction_fee_rate` decimal(5,2) DEFAULT NULL COMMENT '交易费率%',
  `management_fee_rate` decimal(5,2) DEFAULT NULL COMMENT '管理费率%',
  `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_supplier_code` (`supplier_code`),
  KEY `idx_supplier_name` (`supplier_name`),
  KEY `idx_identity_no` (`identity_no`),
  KEY `idx_del_flag` (`del_flag`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='供应商';
```

### 2.3 枚举

**供应商类型 `supplier_type`**

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

**收费方式 `fee_method`**

| 值 | 标签 | 有效字段 |
| --- | --- | --- |
| 1 | 按户计费 | 三项费率/标准均为 NULL |
| 2 | 按头计费 | `fee_standard` &gt; 0 |
| 3 | 按额计费 | `transaction_fee_rate`、`management_fee_rate` ∈ [0, 100] |

**删除标记 `del_flag`**

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

**账号分配 `account_assigned`**

| 值 | 含义 |
| --- | --- |
| 0 | 未分配 |
| 1 | 已分配 |

**分配账号默认角色（`sys_role.role_id`）**

| 主体 | `role_id` |
| --- | --- |
| 供应商 | **103** |

**已有库升级**：执行 `sql/alter_biz_supplier_distributor_assign_account.sql`（供应商、承销商字段一并追加）。

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

1. 查询 `del_flag` 任意状态下 `MAX(supplier_code)`（或维护 `sys_seq` 表，本期可用 `MAX` + 事务）。  
2. 无记录 → `01`；有记录 → 数值 +1，格式 `%02d`。  
3. 下一号 &gt; `99` → 抛 `ServiceException`「供应商编号已达上限，请联系管理员」。  
4. 与插入同事务，避免并发重复（`SELECT … FOR UPDATE` 或表级锁）。

### 2.5 车辆号牌存储

- 库内：`vehicle_plates` 存 JSON，如 `["藏A12345","藏B67890"]`。  
- API：`vehiclePlates` 为 `string[]`；空数组或 null 表示未登记。  
- 单条长度建议 ≤16；数组长度 ≤10；非法车牌格式由 `SupplierValidation` 拒绝。

### 2.6 分配账号登录名（`TradingPartnerLoginNameGenerator`）

- 对 `supplier_name` 逐字取汉语拼音**首字母**（小写）；ASCII 字母数字原样小写拼接。  
- 结果 + `id` 作为 `user_name`（如 `扎西供应商`、id=12 → `zxgys12`）。  
- 无有效字符时回退 `user` + `id`。  
- 由 `TradePartnerAccountAssigner.createAccount` 调 `ISysUserService.insertUser`，初始密码 `sys.user.initPassword`。

---

## 3. 接口设计

**统一响应**：`AjaxResult` / `TableDataInfo`（`code`、`msg`、`data` / `rows`、`total`）。

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

**Base Path**：`/tradeMarket/supplier`

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

#### 3.1.1 列表 Query

| 参数 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `pageNum` / `pageSize` | int | N | 默认 `1` / `20` |
| `supplierCode` | string | N | 供应商编号**精确**匹配 |
| `supplierName` | string | N | 供应商名称**精确**匹配 |

**列表行（驼峰）**：表字段 + `supplierTypeName`、`feeMethodName` + `accountAssigned`、`sysUserId`、`assignedLoginName`。

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

#### 3.2.1 详情 `data`

全业务字段（驼峰）+ `supplierTypeName`、`feeMethodName` + `vehiclePlates`（数组）+ 审计字段。

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

| 字段 | 新增 | 修改 | 说明 |
| --- | --- | --- | --- |
| `id` | — | Y | 主键 |
| `supplierCode` | — | — | **禁止传入**；仅响应带回 |
| `supplierName` | Y | Y | 1～20 字，trim |
| `supplierType` | Y | Y | `1` / `2` |
| `identityNo` | Y | Y | 个体户身份证 / 企业 USCC |
| `contactPhone` | Y | Y | 手机号 |
| `businessLocation` | N | N | ≤50 字 |
| `vehiclePlates` | N | N | 字符串数组，≤10 |
| `feeMethod` | Y | Y | `1` / `2` / `3` |
| `feeStandard` | 条件 | 条件 | `feeMethod=2` 必填，&gt;0 |
| `transactionFeeRate` | 条件 | 条件 | `feeMethod=3` 必填，0～100 |
| `managementFeeRate` | 条件 | 条件 | `feeMethod=3` 必填，0～100 |
| `remark` | N | N | ≤500 字 |

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

| 项 | 规则 |
| --- | --- |
| 名称 | trim 后 1～20 字；`del_flag=0` 下 **名称唯一** |
| 类型 + 身份号 | `supplier_type=1` → 身份证校验；`=2` → 18 位 USCC；**身份号唯一**（未删除） |
| 手机 | 项目统一手机号规则 |
| 收费联动 | `fee_method=1` → 三费率字段置 NULL；`=2` → 仅 `fee_standard`；`=3` → 两费率 0～100 |
| 车辆 | 每条格式合法；数量 ≤10 |
| 修改 | 禁止改 `supplier_code`；记录存在且未删 |
| 删除 | 未删；`SupplierReferenceChecker.hasUnsettledSettlement(id)` 为 false（`biz_supplier_settlement` 待结算且未删） |
| 分配账号 | `SupplierValidation.validateForAssignAccount`：`account_assigned≠1` |

#### 3.5 删除

- `DELETE /tradeMarket/supplier/{ids}`，本期前端**单条**传一个 id。  
- `del_flag='2'`；存在未结算结算单时失败，msg「存在未结算的结算单，无法删除」。  
- 若 `account_assigned=1` 且 `sys_user_id` 非空：`TradePartnerAccountAssigner.deleteLinkedAccount` → `deleteUserById`。

#### 3.6 分配账号

| 项 | 说明 |
| --- | --- |
| **Method / URI** | `POST /tradeMarket/supplier/assignAccount/{id}` |
| **权限** | `tradeMarket:supplier:assignAccount` |
| **前置** | `account_assigned=0`；`del_flag=0` |
| **结果** | 创建 `sys_user`，绑定角色 **103**；写 `sys_user_id`、`assigned_login_name`、`account_assigned=1` |
| **失败** | 登录名冲突等返回明确 `msg`，**不**置 `account_assigned=1` |

#### 3.7 Mapper 约定

- 查询/更新条件带 `del_flag='0'`。  
- 列表筛选：`supplier_code = #{supplierCode}`、`supplier_name = #{supplierName}`（非空时）。  
- 逻辑删除：按 id 更新 `del_flag='2'`。

---

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

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

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

---

## 5. 交付清单

- [x] `sql/biz_supplier.sql`、`sql/alter_biz_supplier_distributor_assign_account.sql`、菜单权限 SQL  
- [x] `BizSupplier`、Mapper/XML、Service、Controller  
- [x] `SupplierValidation`、`SupplierCodeGenerator`、`SupplierRules`  
- [x] `TradingPartnerLoginNameGenerator`、`TradePartnerAccountAssigner`、`TradePartnerAccountRules`  
- [x] 单元测试（编号、收费联动、唯一性、分配账号、删除联动）  
- [x] MockMvc（list、CRUD、`assignAccount`）

---

## 6. 修订记录

| 版本 | 说明 |
| --- | --- |
| 1.0 | 初稿：`biz_supplier`；CRUD+逻辑删除；2 位编号；收费方式三档；车辆 JSON；`tradeMarket/supplier` |
| 1.1 | 分配账号：`account_assigned`/`sys_user_id`/`assigned_login_name`；`POST assignAccount`；角色 103；拼音首字母+id |
| 1.3 | 删除约束：`SupplierReferenceChecker.hasUnsettledSettlement`；`biz_supplier_settlement` 待结算拦截 |
