# 供应商管理 — 技术方案 > 依据:同目录 `供应商管理功能需求.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` > 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. 下一号 > `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` 必填,>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` 待结算拦截 |