# 商城服务协议 — 技术方案

> **依据：** 《商城服务协议功能需求.md》v1.0  
> **关联：** 《会员管理技术方案.md》v1.0.1、《商城入驻协议技术方案.md》v1.0、《首页banner设置技术方案.md》v1.0、《关联需求分析.md》v1.2  
> **范围：** 全平台 **单条** 买家服务协议配置；平台查看/保存；C 端只读拉取（注册/登录场景）；**无** 会员/订单表 FK。  
> **原则：** 单表单行（`config_id=1`）；启用时正文必填；C 端 `@Anonymous`；与入驻协议 **同构分表**；**SP4** 区分注册开放与登录是否须勾选。

---

## 1. 技术架构

| 项 | 选型 |
|----|------|
| 基础框架 | RuoYi **v3.9.2**（`springboot2` 分支） |
| 数据库 | **MySQL 5.7.39** |
| ORM / 权限 / 响应 | MyBatis；`@PreAuthorize`；`AjaxResult` |
| 富文本 | 库字段 `LONGTEXT`；`AgreementContentSupport` 判空 |
| 日志 | 保存配置 `@Log` |

### 1.1 模块落位

```text
baqing-shop
├── com.ruoyi.web.modules.content
│   ├── controller.MallServiceAgreementController           # 平台单页配置
│   ├── controller.MallServiceAgreementAppController        # C 端只读
│   ├── domain.BizMallServiceAgreement
│   ├── mapper.BizMallServiceAgreementMapper
│   ├── service.IMallServiceAgreementService
│   ├── constant.MallServiceAgreementConstants
│   ├── facade.IMallServiceAgreementFacade
│   ├── facade.impl.MallServiceAgreementFacadeImpl
│   └── support.AgreementContentSupport                     # 与入驻协议共用
sql/
└── biz_mall_service_agreement.sql
```

> 与 **入驻协议**、**Banner** 同属 `content` 包；**独立表** `biz_mall_service_agreement`。

### 1.2 业务链

```text
biz_mall_service_agreement（config_id=1 · 单例）
        │
        ├── 平台 GET/PUT /agri/mallServiceAgreement
        │
        ├── C 端 GET /api/member/serviceAgreement（Anonymous）
        │       ├── registrationOpen=false → 不可完成新注册
        │       └── requireAgreementOnLogin=false → 登录页不展示勾选（SP4）
        │
        └── 会员模块 POST /api/member/register | /login
                └── Facade 校验 agreementAccepted（见 §4.4、§5）
```

| 模块 | 关系 |
|------|------|
| 会员管理 | `register` / `login` **注入** `IMallServiceAgreementFacade`；**不** 改 `biz_member` 结构 |
| 入驻协议 | 表、接口路径 **独立**；用户须 **分别勾选**（SP7） |
| 商户/店铺/订单 | 无依赖；改协议不级联（SP10） |

### 1.3 跨模块契约

| 接口 | 提供方 | 调用方 | 方法 |
|------|--------|--------|------|
| **`IMallServiceAgreementFacade`** | **本模块** | `MemberAppController` / `MemberServiceImpl` | `isRegistrationOpen()` — 同 `isEnabled()` |
| | | | `isAgreementRequiredOnLogin()` — 同 `isEnabled()` |
| | | | `assertAcceptedForRegister(boolean agreementAccepted)` |
| | | | `assertAcceptedForLogin(boolean agreementAccepted)` |
| | | | `getCheckboxLabel()` |

| 场景 | Facade 行为 |
|------|-------------|
| **注册** | `!isRegistrationOpen()` →「会员注册暂未开放」；未勾选 →「请先阅读并同意商城服务协议」 |
| **登录** | `!isAgreementRequiredOnLogin()` → **跳过** 协议校验；已启用且未勾选 → 同上协议文案 |

### 1.4 状态联动

| 事件 | `biz_mall_service_agreement` | `biz_member` 等 |
|------|------------------------------|-----------------|
| 保存协议/改启用 | 更新本表 | **不变** |
| 会员禁用/启用 | — | **不变** 协议配置 |

---

## 2. 数据库设计

### 2.1 本模块表

| 表名 | 说明 |
|------|------|
| `biz_mall_service_agreement` | 商城服务协议 **单例配置**（买家会员注册/登录） |

### 2.2 字段

| 字段 | 类型 | 说明 |
|------|------|------|
| config_id | bigint PK | 固定 **1**（`CONFIG_ID=1`） |
| agreement_title | varchar(128) | 协议标题；非空 |
| version_label | varchar(32) | 版本号；可空 |
| content | longtext | 富文本 HTML；启用时非空 |
| enable_flag | char(1) | `0` 否 `1` 是；默认 **0** |
| create_by, create_time, update_by, update_time, remark | | RuoYi 审计 |

> **无** `del_flag`、**无** 多版本行。

### 2.3 单例约定

| 规则 | 实现 |
|------|------|
| 仅一行 | `WHERE config_id = 1` |
| 首次保存 | 无记录 `INSERT`；有则 `UPDATE` |
| 禁止 DELETE | 不提供删除接口 |

### 2.4 DDL

脚本：`sql/biz_mall_service_agreement.sql`

```sql
CREATE TABLE `biz_mall_service_agreement` (
  `config_id` bigint(20) NOT NULL COMMENT '固定为1',
  `agreement_title` varchar(128) NOT NULL DEFAULT '' COMMENT '协议标题',
  `version_label` varchar(32) DEFAULT NULL COMMENT '版本号',
  `content` longtext COMMENT '富文本正文',
  `enable_flag` char(1) NOT NULL DEFAULT '0' COMMENT '0否1是',
  `create_by` varchar(64) DEFAULT '',
  `create_time` datetime DEFAULT NULL,
  `update_by` varchar(64) DEFAULT '',
  `update_time` datetime DEFAULT NULL,
  `remark` varchar(500) DEFAULT NULL,
  PRIMARY KEY (`config_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='商城服务协议配置';

INSERT INTO `biz_mall_service_agreement` (
  `config_id`, `agreement_title`, `version_label`, `content`, `enable_flag`, `create_time`
)
SELECT 1, '农资商城用户服务协议', 'v1.0', '', '0', NOW()
FROM DUAL
WHERE NOT EXISTS (SELECT 1 FROM `biz_mall_service_agreement` WHERE `config_id` = 1);
```

### 2.5 字典

| dict_type | 用途 |
|-----------|------|
| sys_yes_no | `enable_flag` 平台页展示 |

---

## 3. 平台端接口设计

**基路径：** `/agri/mallServiceAgreement`  
**权限：** `agri:mallServiceAgreement:query` | `edit`

### 3.1 接口一览

| 方法 | 路径 | 权限 | 说明 |
|------|------|------|------|
| GET | `/` | query | 获取当前配置 |
| PUT | `/` | edit | 保存配置 |

### 3.2 获取配置 `GET /`

**响应 `data`：**

```json
{
  "configId": 1,
  "agreementTitle": "农资商城用户服务协议",
  "versionLabel": "v1.0",
  "content": "<p>协议正文...</p>",
  "enableFlag": "1",
  "enableFlagLabel": "是",
  "updateBy": "admin",
  "updateTime": "2026-05-26 10:00:00"
}
```

| 场景 | 行为 |
|------|------|
| 表无记录 | `configId=1`；标题/内容空；`enableFlag=0` |

### 3.3 保存配置 `PUT /`

```json
{
  "agreementTitle": "农资商城用户服务协议",
  "versionLabel": "v1.0",
  "content": "<p>正文 HTML</p>",
  "enableFlag": "1",
  "remark": ""
}
```

| 字段 | 校验 |
|------|------|
| agreementTitle | 非空，≤128 |
| versionLabel | 可空，≤32 |
| enableFlag | `0` / `1` |
| content | `enableFlag=1` 时非空且 `isBlankHtml=false`；上限 **512KB**（应用层） |
| enableFlag=0 | `content` 可空 |

### 3.4 错误文案（Service）

| 场景 | msg |
|------|-----|
| 标题空 | 请输入协议标题 |
| 启用但内容空 | 启用状态下请填写协议内容 |
| 内容仅空白标签 | 协议内容不能为空 |
| 启用参数非法 | 是否启用参数无效 |

---

## 4. C 端接口设计

**基路径：** `/api/member/serviceAgreement`  
**鉴权：** `@Anonymous`

### 4.1 获取服务协议 `GET /api/member/serviceAgreement`

| 项 | 说明 |
|----|------|
| 数据源 | `config_id=1` |
| 缓存 | 本期 **不做** Redis |

**协议已启用且正文有效（`isEnabled()=true`）：**

```json
{
  "code": 200,
  "data": {
    "enabled": true,
    "registrationOpen": true,
    "requireAgreementOnLogin": true,
    "agreementTitle": "农资商城用户服务协议",
    "versionLabel": "v1.0",
    "content": "<p>HTML...</p>",
    "checkboxLabel": "我已阅读并同意《农资商城用户服务协议》（v1.0）"
  }
}
```

**协议未启用或正文无效（SP4）：**

```json
{
  "code": 200,
  "data": {
    "enabled": false,
    "registrationOpen": false,
    "requireAgreementOnLogin": false,
    "message": "会员注册暂未开放"
  }
}
```

| 字段 | 含义 |
|------|------|
| `enabled` | 是否对外展示 **完整协议正文+勾选**（与 `isEnabled()` 一致） |
| `registrationOpen` | 是否允许 **新会员注册**（本期 = `isEnabled()`） |
| `requireAgreementOnLogin` | 登录提交前是否 **必须勾选**（本期 = `isEnabled()`；禁用时 `false`） |
| `message` | 未开放时提示；**不返回** `content` |

**`checkboxLabel` 规则**（与入驻协议一致）：

```text
我已阅读并同意《{agreementTitle}》
若 versionLabel 非空 → 我已阅读并同意《{agreementTitle}》（{versionLabel}）
```

### 4.2 开放状态 `GET /api/member/serviceAgreement/status`

```json
{
  "code": 200,
  "data": {
    "registrationOpen": true,
    "requireAgreementOnLogin": true
  }
}
```

供 C 端注册/登录页 **单独拉取开关**（可与 §4.1 合并，由前端只调 §4.1）。

### 4.3 与入驻协议路径区分

| 接口 | 用途 |
|------|------|
| `GET /api/member/serviceAgreement` | **买家** 服务协议（本模块） |
| `GET /api/merchant/entry/agreement` | **商家入驻** 协议 |

### 4.4 会员注册/登录对接（会员模块扩展）

在《会员管理技术方案》C 端接口上增加 Body 字段（实现期）：

| 接口 | 新增字段 | 校验 |
|------|----------|------|
| `POST /api/member/register` | `agreementAccepted` | `assertAcceptedForRegister(true)` |
| `POST /api/member/login` | `agreementAccepted` | `assertAcceptedForLogin(...)` — 协议未启用时 **忽略** 该字段 |

> 会员 **status**、密码、验证码等校验 **先于或并行于** 协议校验，顺序以实现为准；协议失败 **不写入** `biz_member`。

---

## 5. Service 要点

```text
getConfig()                    → 平台 VO
saveConfig(row)                → validate → insert/update(1)
getForApp()                    → 拼 enabled / registrationOpen / requireAgreementOnLogin / checkboxLabel
isEnabled()                    → enable_flag=1 && !isBlankHtml(content)
isRegistrationOpen()           → isEnabled()
isAgreementRequiredOnLogin()   → isEnabled()
assertAcceptedForRegister(b)   → !open → 会员注册暂未开放；!b → 请先阅读并同意商城服务协议
assertAcceptedForLogin(b)      → !require → return；!b → 请先阅读并同意商城服务协议
```

### 5.1 富文本与安全

| 项 | 实现 |
|----|------|
| 存储 | 原样存 `content` |
| 空内容 | `AgreementContentSupport.isBlankHtml` |
| 共用 | 与 `biz_merchant_entry_agreement` 同一 Support 类 |

---

## 6. 业务规则映射

| 编号 | 实现 |
|------|------|
| SP1 | `config_id=1` 单表一行 |
| SP2 | `MallServiceAgreementController` + 菜单权限 |
| SP3 | `assertAcceptedForRegister` / `assertAcceptedForLogin` |
| SP4 | `requireAgreementOnLogin=false` 且 `assertAcceptedForLogin` 直接 return |
| SP5 | `saveConfig` 启用时校验 content |
| SP6 | 无历史表 |
| SP7 | 表 `/api/member/serviceAgreement` vs `/api/merchant/entry/agreement` |
| SP8 | 商户/平台后台 **不引用** Facade |
| SP9 | 无发布步骤 |
| SP10 | 无级联 UPDATE 会员/订单等 |

---

## 7. 菜单权限

| 菜单（建议） | 权限标识 |
|--------------|----------|
| 商城服务协议 | `agri:mallServiceAgreement:query` |
| 保存 | `agri:mallServiceAgreement:edit` |

上级：**农资管理 → 内容管理**（与入驻协议、Banner 并列）。

---

## 8. 关联方案摘要

```text
【平台】PUT /agri/mallServiceAgreement
    ↓
【C 端】GET /api/member/serviceAgreement
    ↓
【C 端】POST /api/member/register  + agreementAccepted
【C 端】POST /api/member/login     + agreementAccepted（协议禁用时可不传/忽略）
    ↓
【并行】已是会员 → GET /api/merchant/entry/agreement → 入驻勾选（独立）
```

| 对比 | 入驻协议 | 本模块 |
|------|----------|--------|
| 表 | `biz_merchant_entry_agreement` | `biz_mall_service_agreement` |
| 平台路径 | `/agri/merchantEntryAgreement` | `/agri/mallServiceAgreement` |
| C 端路径 | `/api/merchant/entry/agreement` | `/api/member/serviceAgreement` |
| 禁用后老用户 | 仅拦 **新入驻** | 拦 **新注册**；**登录不要求勾选**（SP4） |
| Facade | `assertAccepted` 单方法 | 注册/登录 **分拆** 两个 assert |

---

## 9. 非本期实现

协议历史表、勾选存证、强制阅读时长、PDF、多语言、下单页二次协议、Redis 缓存、`sys_config` 存正文。

---

## 10. 文档索引

| 文档 | 版本 |
|------|------|
| 商城服务协议功能需求.md | v1.0 |
| 商城入驻协议技术方案.md | v1.0 |
| 会员管理技术方案.md | v1.0.1 |
| 关联需求分析.md | v1.2 |

---

*文档版本：v1.0 · MySQL 5.7.39 · RuoYi v3.9.2-springboot2 · 关联《商城服务协议功能需求.md》v1.0*
