# 商户管理 — 技术方案 > **依据:** 《商户管理功能需求.md》v1.5 > **关联:** 《店铺管理》《商品分类》《商品管理》功能需求 v1.3.5 / v1.3.1 / v1.3.3(`doc/农资商城web/`) > **范围:** 本文以 **商户模块** 的数据库、接口为主;关联模块给出 **表关系、Facade 契约、状态联动**,不展开店铺/商品/分类/订单的完整表结构与接口。 > **原则:** 平台 **新增商户** 时通过 `IMerchantAccountBindService` 创建首条 **商户经营账号**;C 端入驻经 **入驻审核+公示** 后由 `MerchantEntryApplyService` 建档并绑会员。 > **v1.5:** 同步 `sql/biz_merchant.sql` 全字段 COMMENT;新增 `id_card_type`、`legal_id_card_type`、`reg_region_*`、`corp_bank_account`;平台企业新增仅校验法人姓名+企业名称。 --- ## 1. 技术架构 | 项 | 选型 | |----|------| | 基础框架 | RuoYi **v3.9.2**(`springboot2` 分支) | | 数据库 | **MySQL 5.7.39** | | ORM / 权限 / 响应 | MyBatis;Spring Security + `@PreAuthorize`;`AjaxResult` / `TableDataInfo` | | 文件 | 证照走 RuoYi `CommonController` 上传,库内存 URL | | 操作日志 | 增删改、改认证状态使用 `@Log`(RuoYi 惯例) | ### 1.1 农资商城模块划分(包建议 `com.ruoyi.agri`) ```text baqing-shop(Web 入口) ├── com.ruoyi.web.controller.agri.MerchantController ruoyi-system(或 ruoyi-agri) ├── domain / mapper / service ``` ### 1.2 业务链与数据归属 ```text biz_merchant(商户) └── biz_shop(店铺,merchant_id)── shop_count 回写商户 ├── biz_merchant_account(经营账号,同商户多店同登录名;店铺模块维护) ├── biz_goods_category(分类,shop_id) └── biz_goods(商品,shop_id + category_id) └── biz_order(订单,shop_id;删商户/店时校验) ``` | 模块 | 核心表(关联设计用) | 与商户关系 | |------|---------------------|------------| | 商户管理 | `biz_merchant` | — | | 店铺管理 | `biz_shop` | `merchant_id`;维护 `shop_count` | | 经营账号 | `biz_merchant_account` | `merchant_id`;**入驻时商户模块插入首条**;店铺模块 CRUD 其余 | | 商品分类 | `biz_goods_category` | 经 `shop_id` 间接 | | 商品管理 | `biz_goods` | 经 `shop_id` 间接;商户冻结 **不** 改商品状态 | | 订单 | `biz_order` | 删商户/店校验 **未完成订单**(《订单管理技术方案》v1.0 · O10) | ### 1.3 跨模块 Service 契约(Java Facade) | 接口 | 提供方 | 调用方 | 方法 | |------|--------|--------|------| | `IMerchantShopFacade` | 店铺 | 商户 | `incrementShopCount` / `decrementShopCount`(**与店铺写同事务**) | | `IOrderFacade` | 订单 | 商户、店铺 | `hasUnfinishedOrdersByMerchant(Long merchantId)` | | `IMerchantFacade` | 商户 | 店铺 | `checkOpenShop`、`listSelectableMerchants` | > 订单未实现前,`hasUnfinishedOrdersByMerchant` 可桩返回 `false`。 ### 1.4 状态联动(技术侧不落库自动任务) | 商户事件 | 店铺 | 商品 | C 端 | |----------|------|------|------| | 冻结 / 注销 | 不可新开店;已有店 **不自动** 停业 | 状态 **不变** | 不因商户冻结单独拦单 | | 恢复正常 | `openShopCheck` 通过后可开店 | — | — | | 逻辑删除 | 须 `shop_count=0` | — | — | --- ## 2. 数据库设计(商户模块) ### 2.1 本模块表 | 表名 | 说明 | |------|------| | `biz_merchant` | 主体资质 + 经营信息 + 认证状态 + `shop_count` | ### 2.2 需求字段与库字段对照 | 需求(功能/C 端) | 库字段 | 备注 | |------------------|--------|------| | 证件类型 | `id_card_type` / `legal_id_card_type` | 1大陆身份证 2来往内地通行证 | | 注册地址省市区 | `reg_region_code`、`reg_region_name` | 企业 | | 经营地址省市区 | `biz_region_code`、`biz_region_name` | 个人/企业 | | 对公银行账号 | `corp_bank_account` | 企业法人 | | 证件/营业「长期有效」 | `*_valid_type=2` | `*_valid_end` 可 NULL | | 经营账号 | `biz_merchant_account.*` | 见 `sql/biz_shop.sql` 账号表段 | ### 2.3 字段摘要 | 分组 | 关键字段 | |------|----------| | 主键/状态 | `merchant_id`, `merchant_type`, `cert_status`, `cert_time`, `biz_complete`, `shop_count` | | 个人主体 | `person_name`, `id_card_type`, `id_card_no`, `id_valid_*`, `id_card_front/back` … | | 企业法人 | `legal_*`, `corp_bank_account`, `account_permit` | | 企业信息 | `company_name`, `credit_code`, `reg_region_*`, `license_valid_*` … | | 经营/结算 | `merchant_name`, `service_phone`, `biz_region_*`, `contact_*`, `bank_*`, `business_license` | ※ `id_card_no`、`credit_code` 入驻后不可 UPDATE;`merchant_name` 非空时唯一(应用层)。 ### 2.4 计算与维护规则 **`biz_complete`:** 见 §9.2 字段清单,全部有值且校验通过 → `1`。 **`cert_time`:** 仅入驻成功、改 `cert_status` 时更新。 **`shop_count`:** 店铺增删时 Facade 维护;见 §9.7。 ### 2.5 DDL > **完整 DDL 及逐字段 COMMENT:** 见仓库 [`sql/biz_merchant.sql`](../../../../sql/biz_merchant.sql)。下文仅列结构要点,**不以本文为准覆盖 SQL 文件**。 ```sql -- 见 sql/biz_merchant.sql(含 id_card_type、reg_region_*、corp_bank_account 等 v1.5 字段) ``` ### 2.6 字典(sys_dict) | dict_type | 值 | |-----------|-----| | merchant_type | 1个人 2企业 | | merchant_cert_status | 0正常 1已冻结 2已注销 | | valid_period_type | 1区间 2长期 | --- ## 3. 接口设计(商户模块) **路径:** `/agri/merchant` **权限:** `agri:merchant:list|query|add|edit|remove|cert` ### 3.1 接口一览 | 方法 | 路径 | 说明 | |------|------|------| | GET | `/list` | 分页列表 | | GET | `/{merchantId}` | 详情 | | POST | `/` | 新增(主体资质 + 绑定账号,`MerchantCreateDTO`) | | GET | `/adminUserOptions` | 入驻可选平台管理员(role_key=merchant) | | GET | `/memberOptions` | 入驻可选会员 | | PUT | `/` | 编辑 | | PUT | `/{merchantId}/certStatus` | 改认证状态(支持两步 confirm) | | GET | `/{merchantId}/deleteCheck` | 删除预检 | | DELETE | `/{merchantIds}` | 逻辑删除(批量,**任一条失败则整批失败**) | | GET | `/selectList` | 开店选商户 | | GET | `/{merchantId}/openShopCheck` | 是否可开店 | | GET | `/{merchantId}/shops` | 商户下店铺简要列表 | ### 3.2 列表 `GET /list` | Query | 说明 | |-------|------| | merchantName | 模糊 `merchant_name` / `person_name` / `company_name` | | certStatus | 0/1/2 | | 响应 rows | 说明 | |-----------|------| | merchantId, merchantType | | | unitName, merchantName | 名称空→前端「待完善」 | | contactName, contactPhone | 未完善为 null,前端显示「—」 | | shopCount, certStatus, certTime | | | canEdit | cert_status∈{0,1} | | canDelete | 同 deleteCheck 全部通过 | ### 3.3 详情 `GET /{merchantId}` | 字段 | 说明 | |------|------| | 全表字段 | 主体 + 经营 | | bizComplete | 0/1 | | pendingBizFields | 未完整时的 **中文标签列表**(§9.2) | | allowedCertStatuses | `[{value,label,needConfirm,confirmMessage}]`(§9.1) | | canEdit, canDelete | | | shopCount, shops | `[{shopId,shopName,shopStatus}]` | | shopListUrl | `/agri/shop/list?merchantName={merchantName或unitName}` | ### 3.4 新增 `POST /` - Body:`MerchantCreateDTO` = 主体字段(§9.3)+ 绑定字段(§9.8)。拒绝经营字段入库。 - **平台主体校验**(`MerchantServiceImpl.validateSubjectOnInsert`):个人 **仅姓名** 必填;企业 **仅法人姓名 + 企业名称** 必填;信用代码有值才验唯一;经营信息 **非必填**。 - 成功:`cert_status=0`, `cert_time=now()`, `biz_complete=0`;同事务调用 `IMerchantAccountBindService.bindAccountOnMerchantCreate`。 - 过期:`data.warnExpired=true`, `data.expiredItems=[...]`,**仍 200 成功**(绑定已执行)。 - 响应 msg:「请尽快在编辑中完善商户经营信息后再开设店铺」。 ### 3.4.1 管理员选项 `GET /adminUserOptions` | Query | 说明 | |-------|------| | keyword | 可选;模糊 `user_name` / `phonenumber` | | 响应 data[] | 说明 | |-------------|------| | userId, userName, nickName, phonenumber | 仅含 `sys_user` 且拥有 **role_key=merchant** 的用户;最多 50 条 | 权限:`agri:merchant:add` ### 3.4.2 会员选项 `GET /memberOptions` | Query | 说明 | |-------|------| | keyword | 可选;模糊 `user_name`(会员名称)/ `phonenumber` | | 响应 data[] | 说明 | |-------------|------| | memberId, memberCode, nickName, mobile | `sys_user` 且拥有 **角色 member**;`memberId=userId`;mobile 脱敏;最多 50 条 | 权限:`agri:merchant:add` ### 3.5 编辑 `PUT /` - 前置:`cert_status`∈{0,1};拒收 `certStatus`。 - 不可改:`merchantType`, `idCardNo`, `creditCode`。 - 重算 `biz_complete`;`bizCompleteChanged:true` 时 msg 可带「已可开设店铺」。 ### 3.6 认证状态 `PUT /{merchantId}/certStatus` **两步确认协议(`confirm`):** | confirm | 行为 | |---------|------| | `false` 或未传 | 仅校验是否允许流转;**不持久化**;返回 `code=200`, `data.needConfirm=true`, `data.confirmMessage` | | `true` | 持久化并更新 `cert_time` | 状态机见 **§9.1**。 ### 3.7 删除 **预检 `GET /{merchantId}/deleteCheck`** ```json { "code": 200, "data": { "canDelete": false, "reasons": ["存在未完成订单", "已绑定店铺数量为 2"] } } ``` **执行 `DELETE /{merchantIds}`** | 规则 | 说明 | |------|------| | 前置 | cert_status∈{0,1};shop_count=0;无未完成订单 | | 批量 | **任一条不通过 → 整批失败**,`del_flag` 均不变 | | 前端 | 列表二次确认文案:「删除后不可恢复,是否继续?」 | ### 3.8 协作接口 **`GET /selectList`**:`del_flag=0 AND cert_status=0 AND biz_complete=1`(**不含** `shop_count` 上限条件) **`GET /{id}/openShopCheck`**:`allowed` + `reason`(经营信息未完善 / 商户已冻结 / 商户已注销 / 商户已删除) --- ## 4. 端到端关联流程 (同 v1.1:入驻 → 补全经营 → 开店 → 分类/商品;删店 → 删商户。) --- ## 5. 业务规则映射 | 编号 | 实现 | |------|------| | R1 | **不设** 店铺数量上限;`shop_count` 仅统计;selectList / openShopCheck **不校验** 店数 | | R2 | `MerchantAccountBindServiceImpl`;`MerchantBindMapper` | | R12 | `BIND_SYS_USER`:`login_name=user_name`,`admin_name=nick_name` | | R13 | `BIND_MEMBER`:追加 role_key=merchant + `biz_merchant_account`;见 §9.8 | | R3 | insert `cert_status=0` + `cert_time` | | R4 | update 忽略 `merchantType` | | R5 | `certStatus` 仅 `PUT .../certStatus` | | R6 | selectList 三条件过滤 | | R7 | 编辑校验 `merchant_name` 唯一;入驻校验 `credit_code` 唯一 | | R8 | deleteCheck + DELETE + OrderFacade | | R9 | 无级联改 shop/goods | | R10 | POST 主体白名单 | | R11 | SQL 不更新 `id_card_no`、`credit_code` | --- ## 6. 菜单权限 | 权限 | 标识 | |------|------| | 列表 | agri:merchant:list | | 查询 | agri:merchant:query | | 新增 | agri:merchant:add | | 修改 | agri:merchant:edit | | 删除 | agri:merchant:remove | | 改认证 | agri:merchant:cert | --- ## 7. 文档索引 | 文档 | 版本 | |------|------| | 商户管理功能需求.md | v1.5 | | 商户管理测试用例.md | v1.2 | | 店铺管理功能需求/技术方案 | v1.3.5 / v1.2.5 | | 商品分类功能需求/技术方案 | v1.3.1 / v1.1 | | 商品管理功能需求/技术方案 | v1.3.3 / v1.2 | --- ## 8. 非本期实现 会员注册/等级运营、OCR、三方实名、导出 Excel、商户冻结自动停业。 --- ## 9. 实现补充(联调契约) ### 9.1 认证状态机 | 当前 | 目标 | 允许 | needConfirm | confirmMessage(示例) | |:----:|:----:|:----:|:-----------:|------------------------| | 0 正常 | 1 冻结 | ✓ | 是 | 冻结后不可新开店铺,是否继续? | | 0 正常 | 2 注销 | ✓ | 是 | 注销后资料不可编辑,是否继续? | | 1 冻结 | 0 正常 | ✓ | 是 | 确认恢复为正常? | | 1 冻结 | 2 注销 | ✓ | 是 | 注销后资料不可编辑,是否继续? | | 2 注销 | 0 正常 | ✓ | 是 | 恢复后可编辑、可开店(须经营信息完整),是否继续? | | 2 注销 | 1 冻结 | ✗ | — | — | | 同态 | 同态 | ✗ | — | — | `allowedCertStatuses` 由上表按当前 `cert_status` 过滤生成。已注销 **仅** 可目标 `0`。 ### 9.2 经营信息完整度与 `pendingBizFields` | 字段 key | 中文标签 | 个人 | 企业 | |----------|----------|:----:|:----:| | merchantName | 商户名称 | ✓ | ✓ | | servicePhone | 客服电话 | ✓ | ✓ | | bizRegion | 经营地址 | ✓ | ✓ | | bizDetailAddress | 经营详细地址 | ✓ | ✓ | | contactName | 联系人姓名 | ✓ | ✓ | | contactPhone | 联系人手机 | ✓ | ✓ | | contactEmail | 联系人邮箱 | ✓ | ✓ | | bankName | 开户银行 | ✓ | ✓ | | bankBranch | 支行名称 | ✓ | ✓ | | bankAccount | 银行账号 | ✓ | ✓ | | businessLicense | 营业执照电子版 | — | ✓ | | accountPermit | 开户许可证 | — | ✓ | `bizRegion` 判定:`biz_region_code` 与 `biz_region_name` 均有值。 详情接口:`pendingBizFields` = 上表未填项的中文标签数组。 ### 9.3 请求 JSON 样例 **个人 — 新增 `POST /`(仅主体)** ```json { "merchantType": "1", "personName": "张三", "idCardNo": "110101199001011234", "birthDate": "1990-01-01", "idValidType": "1", "idValidStart": "2020-01-01", "idValidEnd": "2030-01-01", "residenceAddress": "北京市朝阳区xxx", "gender": "0", "idCardFront": "/profile/upload/2025/01/a.jpg", "idCardBack": "/profile/upload/2025/01/b.jpg" } ``` **企业 — 新增 `POST /`** ```json { "merchantType": "2", "legalName": "李四", "legalIdCardNo": "110101198001011234", "legalBirthDate": "1980-01-01", "legalIdValidType": "2", "legalIdValidStart": "2015-01-01", "legalIdValidEnd": null, "legalResidence": "北京市海淀区xxx", "legalGender": "0", "legalIdCardFront": "/profile/upload/legal_f.jpg", "legalIdCardBack": "/profile/upload/legal_b.jpg", "legalExtraPhoto": null, "companyName": "某某农资有限公司", "creditCode": "91110000MA01234567", "regAddress": "北京市西城区xxx", "companyDetailAddress": "xx大厦10层", "businessScope": "农资销售", "licenseValidType": "1", "licenseValidStart": "2020-01-01", "licenseValidEnd": "2030-12-31" } ``` **编辑 `PUT /`(个人,含经营信息片段)** ```json { "merchantId": 1, "personName": "张三", "idCardNo": "110101199001011234", "merchantName": "张三农资店", "servicePhone": "400-800-1234", "bizRegionCode": "110105", "bizRegionName": "北京市/朝阳区", "bizDetailAddress": "xx路1号", "contactName": "张三", "contactPhone": "13800138000", "contactEmail": "zhang@example.com", "bankName": "中国工商银行", "bankBranch": "朝阳支行", "bankAccount": "622202xxxxxxxxxxxx" } ``` ### 9.4 字段校验规则(Service / JSR303) | 字段 | 规则 | |------|------| | idCardNo / legalIdCardNo | 18 位身份证格式(含 X) | | creditCode | 18 位统一社会信用代码 | | contactPhone, servicePhone | 手机或固话(项目统一正则) | | contactEmail | 邮箱格式 | | bankAccount | 数字 8~30 位 | | idValidType=1 | start、end 必填,end≥start | | idValidType=2 | end 可空;start 建议必填 | | 图片 URL | 非空;后缀 jpg/png/jpeg;单张 ≤5MB(与 RuoYi 上传配置一致) | | 主体必填 | 按 merchantType 分组校验(入驻/编辑共用 Validator) | ### 9.5 业务错误文案(`AjaxResult.msg`) | 场景 | msg | |------|-----| | 信用代码重复 | 该统一社会信用代码已入驻 | | 证件号重复 | 该证件号码已入驻 | | 商户名称重复 | 商户名称已存在 | | 已注销不可编辑 | 商户已注销,不可编辑 | | 不可改认证 | 请在查看详情中修改认证状态 | | 删除-有店铺 | 请先删除该商户下全部店铺 | | 删除-有订单 | 存在未完成订单,无法删除 | | 删除-已注销 | 已注销商户不可删除 | | 批量删除失败 | 部分商户不满足删除条件,操作已取消 | | 状态不允许 | 不允许的认证状态变更 | | 未选绑定 | 请选择并绑定管理员账号或会员账号 | | 用户无 merchant 角色 | 所选用户未分配商户经营角色 | | 登录名重复 | 经营账号登录名已存在 | | 会员无昵称 | 会员昵称不能为空,无法作为经营账号管理员姓名 | | 会员无名称 | 会员名称不能为空,无法作为经营账号登录名 | ### 9.8 入驻绑定请求字段(`MerchantCreateDTO`) | 字段 | 类型 | 必填 | 说明 | |------|------|:----:|------| | bindType | String | ✓ | `SYS_USER` 或 `MEMBER` | | bindUserId | Long | SYS_USER 时 ✓ | `sys_user.user_id`(role_key=merchant) | | bindMemberId | Long | MEMBER 时 ✓ | `member_id = sys_user.user_id`(角色 member) | **`BIND_SYS_USER` 落库 `biz_merchant_account`** | 列 | 值 | |----|-----| | login_name | `sys_user.user_name` | | admin_name | `sys_user.nick_name`(空则 `user_name`) | | password | 复制 `sys_user.password` 或加密初始密码 | **`BIND_MEMBER`** 1. `grantRoleIfAbsent(userId, merchant)` → `sys_user_role` 追加 **role_key=merchant** 的角色 2. `login_name` = 会员名称(`user_name`,必填) 3. `admin_name` = 会员昵称(`nick_name`,必填) 4. `insert biz_merchant_account`;`password` 复制会员 `sys_user.password` 实现类:`com.ruoyi.web.modules.merchant.service.impl.MerchantAccountBindServiceImpl` SQL:`mapper/merchant/MerchantBindMapper.xml` **个人 — 新增 `POST /`(含绑定管理员)** ```json { "merchantType": "1", "personName": "张三", "idCardNo": "110101199001011234", "birthDate": "1990-01-01", "idValidType": "1", "idValidStart": "2020-01-01", "idValidEnd": "2030-01-01", "residenceAddress": "北京市朝阳区xxx", "gender": "0", "idCardFront": "/profile/upload/2025/01/a.jpg", "idCardBack": "/profile/upload/2025/01/b.jpg", "bindType": "SYS_USER", "bindUserId": 10 } ``` **个人 — 新增 `POST /`(含绑定会员)** ```json { "merchantType": "1", "personName": "李四", "idCardNo": "110101199002021234", "birthDate": "1990-02-02", "idValidType": "1", "idValidStart": "2020-01-01", "idValidEnd": "2030-01-01", "residenceAddress": "北京市海淀区xxx", "gender": "0", "idCardFront": "/profile/upload/f.jpg", "idCardBack": "/profile/upload/b.jpg", "bindType": "MEMBER", "bindMemberId": 5, "sysUserInitPassword": "123456" } ``` ### 9.6 `shop_count` 与批量删除 ```text 店铺 Service 创建/逻辑删除店铺(同一 @Transactional) → insert/update biz_shop → IMerchantShopFacade.increment/decrementShopCount(merchantId) ``` - 禁止在商户编辑接口修改 `shop_count`。 - 可选运维:`SELECT merchant_id, shop_count FROM biz_merchant` 与 `COUNT(*)` 对账任务(低频)。 - `DELETE /{merchantIds}`:循环预检,**任一失败立即返回**,不执行部分删除。 ### 9.7 展示脱敏(建议) | 字段 | 列表/详情展示 | |------|----------------| | idCardNo, legalIdCardNo | 前3后4掩码 | | bankAccount | 后4位 | | contactPhone | 中间4位掩码 | 存储仍明文;可使用 RuoYi `@Sensitive` 或 VO 转换。 --- *文档版本:v1.5 · MySQL 5.7.39 · RuoYi v3.9.2-springboot2 · 关联《商户管理功能需求.md》v1.5、《商户管理测试用例.md》v1.2*