# 商户入驻审核 — 技术方案

> **依据：** 《商户入驻审核功能需求.md》v1.1  
> **关联：** 《商户管理技术方案.md》v1.5、《店铺管理技术方案.md》v1.2.5、《商城入驻协议技术方案.md》v1.0、《会员管理技术方案.md》v1.0.1、《关联需求分析.md》v1.2  
> **范围：** C 端 **完整字段** 申请提交；平台 **审核→公示→完成入驻**；公示完成后落库商户+首店+经营账号；站内消息。  
> **原则：** 申请状态与商户 `cert_status` **分离**；平台 `POST /agri/merchant` **不写** 申请单。  
> **v1.1：** `apply_status` 增加 **3公示中**；`approve` 仅进公示；`completePublicity`/`processExpiredPublicity` 建档；校验类 `EntryApplyFormValidator`；DDL 见 `sql/` 目录。

---

## 1. 技术架构

| 项 | 选型 |
|----|------|
| 基础框架 | RuoYi **v3.9.2**（`springboot2` 分支） |
| 数据库 | **MySQL 5.7.39** |
| ORM / 权限 | MyBatis；`@PreAuthorize`；`AjaxResult` / `TableDataInfo` |
| 申请状态 | `apply_status` char(1)：**0** 待审 **1** 已完成入驻 **2** 未通过 **3** 公示中 |
| 校验 | `EntryApplyFormValidator.validateMobileSubmit`（C 端完整字段） |
| 表单快照 | `form_json` LONGTEXT（详情展示 + 审核落库映射） |
| 日志 | 审核 `@Log` |

### 1.1 模块落位

```text
baqing-shop
├── com.ruoyi.web.modules.entry
│   ├── controller.MerchantEntryApplyController        # 平台审核
│   ├── controller.MerchantEntryApplyAppController     # C 端提交/我的申请
│   ├── domain.BizMerchantEntryApply
│   ├── mapper.BizMerchantEntryApplyMapper
│   ├── service.IMerchantEntryApplyService
│   ├── dto.EntryApplySubmitDTO / EntryApplyRejectDTO
│   ├── vo.EntryApplyListVO / EntryApplyDetailVO
│   ├── constant.MerchantEntryApplyConstants
│   ├── support.EntryApplyFormMapper                   # JSON ↔ 商户/店铺 DTO
│   ├── support.EntryApplyFormValidator                # C 端完整字段校验
│   ├── task.MerchantEntryPublicityTask                # 公示期满自动建档（每 30 分钟）
│   └── support.MemberEntryMessageSupport              # 站内消息
sql/
├── biz_merchant_entry_apply.sql
├── biz_merchant_entry_form_json说明.sql               # form_json 字段对照
└── biz_member_message.sql                             # 可选：会员站内消息
```

> 包名 `entry` 与 `content` 下「入驻协议」区分：协议=配置；本模块=**申请单**。

### 1.2 业务链

```text
biz_member
    └── biz_merchant_entry_apply（apply_status）
            ├── C 端 POST /api/merchant/entry/apply
            │       └── IMerchantEntryAgreementFacade.assertAccepted
            │       └── EntryApplyFormValidator.validateMobileSubmit
            │
            ├── 平台 GET list/detail、PUT approve/reject/completePublicity
            │
            ├── approve（事务）
            │       └── apply_status=3；写 publicity_*；发「公示中」消息
            │
            └── completePublicity / processExpiredPublicity（事务）
                    ├── insert biz_merchant（cert_status=0, biz_complete 按表单）
                    ├── IMerchantAccountBindService.bindAccountOnMerchantCreate
                    ├── IShopService.createShop（首家店）
                    ├── apply_status=1；回填 merchant_id/shop_id
                    └── biz_member_message（入驻完成通知）
```

| 模块 | 关系 |
|------|------|
| 商城入驻协议 | C 端提交前 `assertAccepted` |
| 商户管理 | 公示完成后 `insert` 商户；**不** 经 `insertMerchant` 平台表单接口 |
| 店铺管理 | 公示完成后 `createShop` |
| 会员管理 | `member_id`；消息收件人 |
| 商品/订单 | 无直接依赖 |

### 1.3 跨模块 Facade

| 接口 | 提供方 | 方法 | 用途 |
|------|--------|------|------|
| `IMerchantEntryAgreementFacade` | content | `assertAccepted(boolean)` | C 端提交 |
| `IMerchantAccountBindService` | merchant | `bindAccountOnMerchantCreate(...)` | 会员→经营账号 |
| `IShopService` | store | `createShop(ShopCreateDTO)` | 首家店 |
| `IMerchantFacade` | merchant | 唯一性校验可复用 Mapper | 证件/信用代码 |

### 1.4 状态联动

| 事件 | `biz_merchant_entry_apply` | `biz_merchant` |
|------|---------------------------|----------------|
| approve | status=3（公示中） | **不** 新增 |
| completePublicity | status=1；回填 merchant_id/shop_id | **新增** |
| reject | status=2 | **不** 新增 |
| 改入驻协议 | **不变** | **不变** |
| 平台代录入商户 | **无申请行** | 直接新增 |

---

## 2. 数据库设计

### 2.1 本模块表 `biz_merchant_entry_apply`

> 权威 DDL 与 COMMENT 见 [`sql/biz_merchant_entry_apply.sql`](../../../../sql/biz_merchant_entry_apply.sql)。

| 字段 | 类型 | 说明 |
|------|------|------|
| apply_id | bigint PK | 申请 ID |
| apply_no | varchar(32) UK | 申请编号，如 `EA`+日期+序号 |
| apply_status | char(1) | **0** 待审 **1** 已完成入驻 **2** 未通过 **3** 公示中 |
| merchant_type | char(1) | 1 个人 2 企业 |
| member_id | bigint | 申请人会员 ID |
| member_code | varchar(64) | 冗余检索：会员名称 |
| subject_label | varchar(128) | 列表「申请信息」：姓名/企业名 |
| contact_name | varchar(64) | 联系人 |
| contact_phone | varchar(20) | 联系人手机 |
| id_card_no | varchar(32) | 个人证件号（唯一校验） |
| credit_code | varchar(32) | 企业信用代码（唯一校验） |
| merchant_name | varchar(128) | 申请中商户名称（唯一校验） |
| shop_name | varchar(128) | 申请中店铺名称（唯一校验） |
| form_json | longtext | 完整表单 JSON 快照 |
| reject_reason | varchar(500) | 驳回原因 |
| merchant_id | bigint | **公示完成** 后回填 |
| shop_id | bigint | **公示完成** 后回填 |
| apply_time | datetime | 提交时间 |
| audit_by | varchar(64) | 审核人（进入公示或驳回时） |
| audit_time | datetime | 审核时间 |
| publicity_start_time | datetime | 公示开始（approve 时） |
| publicity_end_time | datetime | 公示结束（默认 start+7 天） |
| del_flag | char(1) | 0 存在 2 删 |
| create_by/time, update_by/time | | 审计 |

**索引：**

| 索引 | 用途 |
|------|------|
| `uk_apply_no` | 编号 |
| `idx_status_time` (apply_status, apply_time) | 待审列表 |
| `idx_member_status` (member_id, apply_status) | 同会员待审校验 |
| `idx_member_code` (member_code) | 关键词检索 |
| `idx_id_card` (id_card_no) | 个人唯一 |
| `idx_credit_code` (credit_code) | 企业唯一 |

### 2.2 会员站内消息 `biz_member_message`（建议）

| 字段 | 类型 | 说明 |
|------|------|------|
| message_id | bigint PK | |
| member_id | bigint | 收件会员 |
| biz_type | varchar(32) | `ENTRY_AUDIT` |
| biz_id | bigint | apply_id |
| title | varchar(128) | 如「入驻审核通过」 |
| content | varchar(1000) | 含驳回原因摘要 |
| read_flag | char(1) | 0 未读 1 已读 |
| create_time | datetime | |

> 若已有统一消息中心，可替换本表；接口契约不变。

### 2.3 `form_json` 结构（约定）

```json
{
  "subject": { },
  "biz": { },
  "shop": { "shopName", "shopAvatar", "shopPhone", "shopDesc" },
  "agreementAccepted": true
}
```

- `subject` / `biz` 字段名与 `biz_merchant`、商户管理 DTO **一致**，便于 `EntryApplyFormMapper.toMerchantFromJson()` / `toShopCreateDTO()`。
- C 端必填字段清单见 [`sql/biz_merchant_entry_form_json说明.sql`](../../../../sql/biz_merchant_entry_form_json说明.sql)；提交时由 `EntryApplyFormValidator` 校验。
- 详情接口解析 JSON 填 `EntryApplyDetailVO` 各分区。

### 2.4 与既有表关系

| 表 | 关系 |
|----|------|
| `biz_merchant` | completePublicity 后 insert；`merchant_id` 回写 apply |
| `biz_shop` | completePublicity 后 insert；`shop_id` 回写 |
| `biz_merchant_account` | 绑定会员时 insert |
| `biz_member` | `member_id` FK 逻辑 |

**平台 `biz_merchant` 无 `apply_id` 字段**：代录入商户 `source_type` 可选扩展（`1` 平台 / `2` C 端），非本期必做。

---

## 3. 状态机（Service）

| 操作 | 前置 | 结果 | 副作用 |
|------|------|------|--------|
| submit | 无 blocking（0/3）；协议已勾选；完整字段+唯一性通过 | apply_status=0 | — |
| approve | status=0 | status=3 | 写 publicity_*；「公示中」消息 |
| completePublicity | status=3 且 `now>=publicity_end_time` | status=1 | 商户+店+账号+完成消息 |
| processExpiredPublicity | 定时扫描 status=3 且已到期 | 同上 | 批量 completePublicity |
| reject | status=0；rejectReason 非空 | status=2 | 驳回消息 |

---

## 4. 接口设计

### 4.1 C 端 — 提交申请 `POST /api/merchant/entry/apply`

| 项 | 说明 |
|----|------|
| 鉴权 | 会员 Token（`MemberAuthInterceptor`） |
| Body | `EntryApplySubmitDTO`：subject + biz + shop + `agreementAccepted` |

| 校验 | 说明 |
|------|------|
| 协议 | `IMerchantEntryAgreementFacade.assertAccepted` |
| 待审/公示中 | 同 `member_id` 无 status in (0,3)（`countBlockingByMemberId`） |
| 完整字段 | `EntryApplyFormValidator.validateMobileSubmit` |
| 唯一性 | 证件/信用代码/merchant_name/shop_name |
| 会员昵称 | 绑定路径须 `nick_name` 非空 |

**成功：** `{ applyId, applyNo, applyStatus: "0" }`

### 4.2 C 端 — 我的申请 `GET /api/merchant/entry/my`（可选）

| 项 | 说明 |
|----|------|
| 鉴权 | 会员 Token |
| 响应 | 当前会员最近 N 条申请摘要（状态、时间、驳回原因） |

### 4.3 平台 — 列表 `GET /agri/merchantEntryApply/list`

| 项 | 说明 |
|----|------|
| 权限 | `agri:merchantEntryApply:list` |
| Query | `keyword`（member_code）、`applyStatus`、`pageNum/pageSize` |
| 排序 | status=0 → `apply_time ASC`；否则 `apply_time DESC` |

### 4.4 平台 — 待审数量 `GET /agri/merchantEntryApply/pendingCount`

| 响应 | `{ "pendingCount": n }` |

### 4.5 平台 — 详情 `GET /agri/merchantEntryApply/{applyId}`

| 权限 | `agri:merchantEntryApply:query` |
| 响应 | `EntryApplyDetailVO`：基础信息 + 解析 form_json 各分区 + 公示时间 + `canApprove`/`canReject`/`canCompletePublicity` |

### 4.6 平台 — 审核通过（进入公示） `PUT /agri/merchantEntryApply/approve/{applyId}`

| 权限 | `agri:merchantEntryApply:audit` |
| 日志 | title=商户入驻审核通过 |
| Body | 可选 `{ "confirmed": true }` 二次确认 |

**成功：** `{ applyStatus: "3", publicityEndTime }`；**不** 返回 merchantId/shopId。

### 4.7 平台 — 完成公示 `PUT /agri/merchantEntryApply/completePublicity/{applyId}`

| 权限 | `agri:merchantEntryApply:audit` |
| 前置 | status=3 且公示期已满（`publicity_end_time <= now`） |
| 日志 | title=商户入驻公示完成 |

**成功：** `{ merchantId, shopId }`；申请 status=1。

> 定时任务 `MerchantEntryPublicityTask`（默认每 30 分钟）调用 `processExpiredPublicity()`，对到期公示单自动执行与 4.7 相同的建档逻辑（操作人 `system`）。

### 4.8 平台 — 审核驳回 `PUT /agri/merchantEntryApply/reject`

| 权限 | `agri:merchantEntryApply:audit` |
| Body | `{ "applyId", "rejectReason" }` |

| 校验 | rejectReason 必填 |

### 4.9 C 端 — 消息 `GET /api/member/message/list`（可选独立模块）

| 说明 | 读取 `biz_member_message`；本方案可由 `MemberEntryMessageSupport` 写入 |

---

## 5. Service 要点

### 5.1 `submitApply(memberId, dto)`

```text
assertAgreement → assertNoBlockingApply(memberId)   # countBlockingByMemberId
→ EntryApplyFormValidator.validateMobileSubmit
→ assertUnique(idCard/credit/merchantName/shopName)
→ build BizMerchantEntryApply + form_json
→ insert
```

### 5.2 `approve(applyId, dto, operator)`（@Transactional）

```text
load apply(status=0) → re-validate unique
→ update apply: status=3, publicity_start/end, audit_*
→ memberMessageSupport.sendPublicity(...)
→ return { applyStatus, publicityEndTime }
```

### 5.3 `completePublicity(applyId, operator)` / `finalizeEntry`（@Transactional）

```text
load apply(status=3) → assert publicity_end <= now
→ re-validate unique → require member nick_name
→ EntryApplyFormMapper → BizMerchant
→ merchantMapper.insert
→ accountBindService.bindAccountOnMerchantCreate(merchantId, ...)
→ shopService.createShop(shopDto)
→ update apply: status=1, merchant_id, shop_id
→ memberMessageSupport.sendPass(...)
```

### 5.4 `reject(dto, operator)`

```text
load apply(status=0) → update status=2, reject_reason
→ memberMessageSupport.sendReject(memberId, reason)
```

### 5.5 公示配置

| 配置键 | 说明 | 默认 |
|--------|------|------|
| `agri.merchant.entry.publicityDays` | 公示天数 | 7 |

### 5.6 经营账号（会员路径）

| 项 | 规则 |
|----|------|
| 关联行 | `account_id=member_id`（= `sys_user.user_id`），`merchant_id`，`shop_id=NULL` |
| 登录凭证 | 沿用会员既有 `sys_user`（`user_name`=会员名称，`nick_name`=会员昵称） |
| 角色 | `grantRoleIfAbsent(memberId, merchant)` |

---

## 6. 业务规则映射（EA）

| 编号 | 实现 |
|------|------|
| EA1 | 仅 C 端 `submitApply` 写 apply 表 |
| EA2 | `apply_status` 独立字段 |
| EA3 | approve/reject 校验 status=0 |
| EA4 | reject 校验 `rejectReason` |
| EA5 | completePublicity 事务内商户+店+消息 |
| EA5a | approve **仅** 进公示，不 insert merchant/shop |
| EA6 | reject 不 insert merchant/shop |
| EA7 | `EntryApplyFormMapper` 字段对齐商户/店铺 |
| EA8 | `IMerchantEntryAgreementFacade` |
| EA9 | list 支持 applyStatus 筛选 |
| EA10 | list keyword → `member_code` like |
| EA11 | `countBlockingByMemberId`（status 0 或 3）> 0 阻断 submit |
| EA12 | 唯一：merchant 表 + apply 表 status in (0,1,3) |
| EA13 | 驳回后可新 apply_id |
| EA14 | `biz_member_message` |
| EA15 | 不调用商品/订单 Service |

---

## 7. 菜单权限（建议）

| 菜单 | 权限 |
|------|------|
| 商户入驻审核 | `agri:merchantEntryApply:list` |
| 查看详情 | `agri:merchantEntryApply:query` |
| 审核 | `agri:merchantEntryApply:audit` |

---

## 8. 非本期实现

申请批量审核、审核 SLA、申请单修改、短信通知、游客免会员提交、申请单导出。

---

## 9. 文档索引

| 文档 | 版本 |
|------|------|
| 商户入驻审核功能需求.md | v1.1 |
| 商户管理技术方案.md | v1.5 |
| 店铺管理技术方案.md | v1.2.5 |
| 商城入驻协议技术方案.md | v1.0 |

---

## 10. 修订记录

| 版本 | 说明 |
|------|------|
| **v1.0** | 首版：申请单表、平台/C 端接口、审核落库 |
| **v1.0.1** | 曾用 `shopEntryApply` / `biz_shop_entry_apply` 命名 |
| **v1.0.2** | 定名 **商户入驻审核**；`/agri/merchantEntryApply`、`biz_merchant_entry_apply`；规则 **EA1～EA15** |
| **v1.1** | 审核→公示→完成入驻；`apply_status=3`；`completePublicity`/定时任务；`EntryApplyFormValidator`；公示字段 DDL |

---

*文档版本：v1.1 · MySQL 5.7.39 · RuoYi v3.9.2-springboot2 · 关联《商户入驻审核功能需求.md》v1.1*
