# 商城设置 — 技术方案

> **依据：** 《商城设置功能需求.md》v1.1  
> **关联：** 《店铺设置技术方案.md》v1.1.1、《订单管理技术方案.md》v1.0.2、《商品管理技术方案.md》v1.2、《商城服务协议技术方案.md》v1.0、《首页 Banner 设置技术方案.md》v1.0.1；商家《商品列表功能需求》；C 端《商品详情内页功能需求》v1.0、《我的订单功能需求》v1.1、《我的服务功能需求》v1.1；《关联需求分析.md》v1.6  
> **范围：** 全平台 **单例** 商城配置 **`biz_mall_config`**；平台 **查看/保存**；C 端 **只读拉取**；**Facade** 供编号前缀、订单自动确认、售后时限、会员默认头像、C 端销量展示消费。  
> **原则：** 与 **店铺设置**（`biz_shop_global_config`）**分表分模块**；改配置 **不批量改商品/订单**；自动确认与手动确认 **共用完结逻辑**。

---

## 1. 技术架构

| 项 | 选型 |
|----|------|
| 基础框架 | RuoYi **v3.9.2**（`springboot2` 分支） |
| 数据库 | **MySQL 5.7.39** |
| ORM / 权限 / 响应 | MyBatis；`@PreAuthorize`；`AjaxResult` |
| 单例配置 | `biz_mall_config.config_id = 1`（同店铺设置 / 服务协议模式） |
| 定时任务 | RuoYi **Quartz** Bean 方法（对齐 `OrderPayTimeoutJob`） |
| 商品编号 | Redis 自增；前缀读 **`IMallConfigFacade.getGoodsSnPrefix()`** |
| 图片 | 默认头像走 **`/common/upload`**，库存 URL |
| 日志 | 保存配置 `@Log` |

### 1.1 与兄弟模块边界

| 维度 | **本模块 · 商城设置** | 店铺设置 | 订单 O8 关单 | 商城服务协议 |
|------|----------------------|----------|--------------|--------------|
| 表 | `biz_mall_config` | `biz_shop_global_config` | `sys_config` | `biz_mall_service_agreement` |
| 路径 | `/agri/mallSetting` | `/agri/shopSetting` | Quartz `orderPayTimeoutJob` | `/agri/mallServiceAgreement` |
| 职责 | 备案、商品销量展示、自动确认、售后窗口、默认头像 | 免审、子管理员上限 | 待支付超时 | 买家协议 |

### 1.2 模块落位

**文档目录：** `doc/平台后台/商城设置/`  
**菜单：** 内容管理 → **商城设置**（建议 `menu_id=7055`，`parent_id` 与 Banner/协议同级，以实现 SQL 为准）  
**后端包：** `com.ruoyi.web.modules.mall`

```text
mall/
├── controller/
│   ├── MallSettingController.java           # 平台 GET/PUT /agri/mallSetting
│   ├── MallSettingEntryController.java      # 商家只读 GET /agri/mallSetting/entryHints
│   └── MallConfigAppController.java         # C 端 GET /api/mall/config（@Anonymous）
├── domain/BizMallConfig.java
├── mapper/BizMallConfigMapper.java
├── service/IMallConfigService.java
├── service/impl/MallConfigServiceImpl.java    # 同时实现 Facade
├── facade/IMallConfigFacade.java
├── dto/MallSettingSaveDTO.java
├── vo/MallSettingVO.java
├── vo/MallSettingEntryHintsVO.java            # 商家发品页
├── vo/MallConfigAppVO.java                    # C 端
├── constant/MallConfigConstants.java
├── support/
│   └── MallConfigSupport.java                 # URL、天数、开关校验
└── job/OrderAutoConfirmReceiveJob.java        # Quartz Bean

order/（扩展 · 非本包新建表）
├── service/IOrderCompletionService.java       # 抽取确认收货完结
├── service/impl/OrderCompletionServiceImpl.java
├── job/OrderAutoConfirmReceiveJob 引用 completion
└── mapper/BizOrderMapper.java                   # selectShippedForAutoConfirm / 已有 updateForConfirmReceive

goods/（扩展）
└── support/GoodsSnGenerator.java                # 前缀改读 Facade（SN）

home/
└── service/impl/GoodsAppServiceImpl.java        # 按开关隐藏销量

account/
└── service/impl/MemberAppServiceImpl.java       # 注册/资料读默认头像
    support/MemberAvatarSupport.java             # 空 avatar → 默认

sql/
├── biz_mall_config.sql
├── biz_mall_config_v1_1_alter.sql               # 已上线 v1.0 环境删列
├── biz_goods_drop_market_price.sql              # 已加 market_price 时删列
└── biz_mall_setting_menu.sql
```

**前端（建议）：**

| 项 | 值 |
|----|-----|
| 平台页 | `ruoyi-ui/src/views/agri/mall/setting/index.vue` |
| API | `ruoyi-ui/src/api/agri/mall/setting.js` |
| 商家发品 | 可选调 `entryHints` 展示货币符、编号前缀 |

### 1.3 业务链

```text
biz_mall_config（config_id=1）
    │
    ├── 平台 GET/PUT /agri/mallSetting
    │
    ├── 商家 GET /agri/mallSetting/entryHints（prefix、currency）
    │
    ├── C 端 GET /api/mall/config（备案、展示开关、默认头像 URL）
    │
    ├── IMallConfigFacade
    │       ├── GoodsSnGenerator
    │       ├── GoodsAppServiceImpl（C 端详情 · 销量开关）
    │       ├── OrderAppSupport.resolveActions（售后按钮）
    │       ├── OrderAftersaleAppServiceImpl.submit（售后时限）
    │       ├── OrderAutoConfirmReceiveJob → IOrderCompletionService
    │       └── MemberAppServiceImpl / MemberAvatarSupport
    │
    └── 只读展示项（paidCancelable 等）硬编码进 MallSettingVO，不入库
```

---

## 2. 数据库设计

### 2.1 本模块表 `biz_mall_config`（单例）

| 字段 | 类型 | 默认 | 说明 |
|------|------|------|------|
| config_id | bigint PK | **1** | 固定单例 |
| icp_no | varchar(128) | NULL | ICP 备案号 |
| icp_link | varchar(512) | NULL | ICP 链接 |
| psb_no | varchar(128) | NULL | 公安备案号 |
| psb_link | varchar(512) | NULL | 公安备案链接 |
| currency_symbol | varchar(8) | **¥** | 货币符（库内固定，PUT **不可改**） |
| currency_name_cn | varchar(16) | **元** | 货币中文（PUT **不可改**） |
| goods_sn_prefix | varchar(8) | **SN** | 编号前缀（PUT **不可改**） |
| show_sales | char(1) | **1** | 0否 1是 · C 端是否展示销量 |
| auto_confirm_days | int | **7** | 已发货自动确认收货（自然日） |
| aftersale_limit_days | int | **7** | 确认收货后可申请售后（自然日） |
| default_avatar | varchar(512) | **''** | 会员默认头像 URL；**保存时必填** |
| create_by/time, update_by/time | | | 审计 |

**初始化：** `sql/biz_mall_config.sql` — `INSERT ... WHERE NOT EXISTS`；`default_avatar` 初始可为内置占位图 URL（部署后运营替换）。

> **不入库：**「已支付可取消」「已发货可取消发货」— `MallSettingVO` 固定返回 `false`，对齐功能需求 §7.3。

### 2.2 订单表（无 DDL 变更）

使用已有字段：

| 字段 | 用途 |
|------|------|
| `order_status` | `2` 已发货参与自动确认；`3` 已完成参与售后时限 |
| `ship_time` | 自动确认 **起算** |
| `finish_time` | 售后时限 **起算**（手动/自动确认写入） |

**扫描索引：** 已有 `idx_shop_status`；自动确认 SQL 条件 `order_status='2' AND ship_time IS NOT NULL`，数据量可控；量大可加 `idx_auto_confirm (order_status, ship_time)`（可选）。

---

## 3. 接口设计

### 3.1 平台 · 商城设置

**基路径：** `/agri/mallSetting`  
**权限：** `agri:mall:setting:query`（GET）、`agri:mall:setting:edit`（PUT）

#### `GET /agri/mallSetting`

**响应 `MallSettingVO`：**

```json
{
  "icpNo": "",
  "icpLink": "",
  "psbNo": "",
  "psbLink": "",
  "currencySymbol": "¥",
  "currencyNameCn": "元",
  "goodsSnPrefix": "SN",
  "showSales": "1",
  "autoConfirmDays": 7,
  "aftersaleLimitDays": 7,
  "paidOrderCancelable": false,
  "shippedOrderCancelable": false,
  "defaultAvatar": "https://..."
}
```

#### `PUT /agri/mallSetting`

**Body `MallSettingSaveDTO`：** 仅 **可编辑项**（备案四字段、销量开关、两个天数、defaultAvatar）。

| 校验 | 失败 msg（示例） |
|------|------------------|
| autoConfirmDays ∈ [1, 90] | 自动确认收货天数须在 1～90 之间 |
| aftersaleLimitDays ∈ [1, 365] | 售后申请限制天数须在 1～365 之间 |
| icp/psb link 非空时须 http(s) URL | 备案链接格式不正确 |
| defaultAvatar 非空 | 请上传会员默认头像 |

**成功 msg：** `保存成功`（或 `MallConfigConstants.MSG_SAVED`）

**不做：** PUT 不接受 `currencySymbol` / `goodsSnPrefix` / 只读取消开关。

### 3.2 商家 · 发品提示（只读）

**路径：** `GET /agri/mallSetting/entryHints`  
**权限：** `agri:seller:goods:list`（或 add/edit 任一）

```json
{
  "currencySymbol": "¥",
  "currencyNameCn": "元",
  "goodsSnPrefix": "SN"
}
```

供商家端表单 **只读展示** 货币与编号前缀；**落库仍以服务端为准**。

### 3.3 C 端 · 公共配置

**路径：** `GET /api/mall/config`  
**权限：** `@Anonymous`

```json
{
  "icpNo": "京ICP备xxx",
  "icpLink": "https://...",
  "psbNo": "京公网安备xxx",
  "psbLink": "https://...",
  "showSales": true,
  "defaultAvatar": "https://...",
  "currencySymbol": "¥"
}
```

| 规则 |
|------|
| 备案项：**号、链接均非空** 才输出；否则该组字段 **省略或 null** |
| **不输出** 订单天数（C 端无配置 UI，减少暴露） |
| 可加 **短期 HTTP 缓存**（可选）；保存后 **立即生效** |

---

## 4. Facade 契约

**接口：** `com.ruoyi.web.modules.mall.facade.IMallConfigFacade`  
**实现：** `MallConfigServiceImpl`（与 `IShopGlobalConfigFacade` 同构）

| 方法 | 调用方 | 说明 |
|------|--------|------|
| `getGoodsSnPrefix()` | `GoodsSnGenerator` | 默认 `SN` |
| `isShowSales()` | `GoodsAppServiceImpl`、列表 Mapper 组装 | |
| `getAutoConfirmDays()` | `OrderAutoConfirmReceiveJob` | |
| `getAftersaleLimitDays()` | `OrderAftersaleAppServiceImpl`、`OrderAppSupport` | |
| `getDefaultAvatarUrl()` | `MemberAvatarSupport` | 非空 |
| `getFilingIcp()` / `getFilingPsb()` | `MallConfigAppController` | 可选 DTO |

**加载策略：** `select config_id=1` → 无则 **lazy insert 默认值**（对齐 `ShopGlobalConfigServiceImpl.requireConfig()`）。

---

## 5. 核心业务实现

### 5.1 商品编号前缀（MC-G3）

**现状：** `GoodsSnGenerator` 硬编码 **`G` + yyyyMMdd + 6 位**。

**改造：**

```java
return facade.getGoodsSnPrefix() + date + String.format("%06d", seq);
// Redis key 建议 agri:goods:sn:{prefix}:{yyyyMMdd}
```

| 说明 |
|------|
| 新商品 **SN** 前缀；存量 **`G...` 编号保留**，不迁移 |
| `MallSettingVO.goodsSnPrefix` 只读展示 **SN** |

### 5.2 C 端销量展示（MC-G5）

**`GoodsAppServiceImpl.getDetail`：**

```text
查 base → 组装 VO
    → 若 !facade.isShowSales() → 不 set salesCount
```

**列表（首页/分类/搜索）：** 若 SQL/VO 含 `salesCount`，在 Service 层同样 **按开关置 null**；**排序仍用 DB `sales_count`**（HM10）。

### 5.4 订单完结 · 抽取共用（MC-O1～O3）

**新建 `IOrderCompletionService`：**

```java
void completeOrder(BizOrder order, Date finishTime, String operator, String traceContent);
```

**逻辑（自 `OrderAppServiceImpl.confirmReceive` 迁入）：**

```text
updateForConfirmReceive(orderId, memberId, finishTime)  // 乐观锁 status=2
insertTrace(TRACE_CONFIRM, traceContent)
shopFundService.settleOrder(order)
memberFacade.refreshConsumption(memberId)
```

| 调用方 | traceContent |
|--------|--------------|
| C 端手动确认 | 买家确认收货 |
| 自动确认 Job | 系统自动确认收货 |

**`OrderAppServiceImpl.confirmReceive`：** 校验会员 + 已发货 → 调 `completeOrder`。

### 5.5 自动确认收货 Job（MC-O1～O5）

**Bean：** `@Component("orderAutoConfirmReceiveJob")`  
**方法：** `autoConfirmReceive()`  
**Quartz：** cron `0 0 1 * * ?`（每日 1:00）；初始化 SQL 见 **`sql/sys_job_order_auto_confirm_receive.sql`**（幂等，status=0 启用）

**扫描 SQL（自然日 · 发货日起算）：**

```sql
SELECT order_id, member_id, shop_id, ...
FROM biz_order
WHERE order_status = '2'
  AND ship_time IS NOT NULL
  AND DATEDIFF(CURDATE(), DATE(ship_time)) >= #{autoConfirmDays}
  AND del_flag = '0'
LIMIT 500
```

| 项 | 说明 |
|----|------|
| 起算 | **`DATE(ship_time)`**，对齐功能需求「发货后 N 自然日」 |
| 示例 | 1 月 1 日发货、N=7 → 1 月 8 日（含）首次扫描命中 |
| 批量 | 分页循环直至无数据；**单笔事务** completeOrder |
| 失败 | 单笔失败 **记日志继续**；不阻断批次 |
| 动态 N | 每次 Job 读 **最新** `getAutoConfirmDays()` |

**Mapper：** `BizOrderMapper.selectShippedForAutoConfirm(@Param("days") int days, @Param("limit") int limit)`

### 5.6 售后申请时限（MC-O6～O8）

**`OrderAftersaleAppServiceImpl.submit` 追加：**

```text
若 order.orderStatus == COMPLETED && order.finishTime != null
    若 daysBetween(now, finishTime) > facade.getAftersaleLimitDays()
        throw ServiceException("已超过售后申请期限")
```

**待发货 / 已发货（无 finishTime）：** **不校验** M（MC-O8 · 仅退款-未发货等仍走原 validateApplyType）。

**`OrderAppSupport.resolveActions`：**

```text
原逻辑：COMPLETED → AFTERSALE
改为：COMPLETED 且 !aftersaleExpired(order) → AFTERSALE
aftersaleExpired = finishTime != null && daysSince(finishTime) > limit
```

**提示文案：** `OrderAppConstants.MSG_AFTERSALE_EXPIRED = "已超过售后申请期限"`

### 5.7 会员默认头像（MC-A1～A5）

**`MemberAvatarSupport.resolve(String avatar)`：**

```text
StringUtils.isNotEmpty(avatar) ? avatar : facade.getDefaultAvatarUrl()
```

| 挂点 | 行为 |
|------|------|
| `MemberAppServiceImpl.register` → `MemberCreateDTO.avatar` | 未传则写默认 URL 到 `sys_user.avatar` |
| `getProfile` / `toProfileVo` | 返回 **解析后** URL |
| 平台会员详情 | 读库 avatar；空则 **展示层** 解析默认（与 C 端一致） |

更换默认头像 **不 UPDATE** 已有自定义 avatar 的会员。

---

## 6. Service 要点

### 6.1 `getSettings()` / `updateSettings(dto, operator)`

```text
requireConfig() → MallSettingVO（含只读 cancel 字段）
validate(dto) → UPDATE biz_mall_config WHERE config_id=1
（不 UPDATE biz_goods / biz_order / biz_member 批量）
```

### 6.2 首次无配置

```text
select config_id=1 → null
    → insert 默认值（§2.1）
    → default_avatar 使用 MallConfigConstants.PLACEHOLDER_AVATAR
```

平台 **首次保存** 须上传正式头像（MC-A1）。

---

## 7. 业务规则映射

| 编号 | 实现要点 |
|------|----------|
| MC-R1～R3 | C 端 config 接口；链接 `MallConfigValidator` |
| MC-G1～G5 | VO 只读字段；PUT 忽略；`GoodsAppServiceImpl` 销量开关 |
| MC-O1～O5 | Job + `DATEDIFF` + `IOrderCompletionService` |
| MC-O6～O8 | `OrderAftersaleAppServiceImpl` + `OrderAppSupport` |
| MC-O9～O10 | VO 硬编码 false |
| MC-A1～A5 | `MemberAvatarSupport` + register/profile |

---

## 8. 菜单与权限

**脚本：** `sql/biz_mall_setting_menu.sql`

| 菜单 | path | perms |
|------|------|-------|
| 商城设置 | `agri/mall/setting/index` | `agri:mall:setting:query` |
| 保存按钮 | — | `agri:mall:setting:edit` |

> 与 **店铺设置** `agri:shop:setting` **独立**。

---

## 9. 测试要点

| 编号 | 类型 | 场景 |
|------|------|------|
| MC-UT-1 | 单元 | `MallConfigServiceImpl` lazy init、PUT 校验 |
| MC-UT-2 | 单元 | `OrderAppSupport` 完成后超 M 天无 AFTERSALE action |
| MC-UT-3 | 单元 | `MemberAvatarSupport` 空 avatar 回退 |
| MC-IT-1 | 集成 | 已发货单 mock 日期 → Job → status=3 + settle |
| MC-IT-3 | 集成 | 完成后 M+1 天 submit 售后 → 400 |
| MC-API-1 | 接口 | `GET /api/mall/config` Anonymous 备案成对过滤 |

---

## 10. 实施分期

| 阶段 | 内容 | 依赖 |
|------|------|------|
| **P1** | DDL + Domain/Mapper/Service/Controller + 菜单 + 单测 | — |
| **P2** | Facade + GoodsSn + entryHints | P1 |
| **P3** | C 端 config + GoodsApp 销量开关 | P1 |
| **P4** | `IOrderCompletionService` + AutoConfirm Job | 订单模块已上线 |
| **P5** | 售后时限 + resolveActions | P1、售后模块 |
| **P6** | 会员默认头像 register/profile | P1、会员模块 |
| **P7** | ruoyi-ui 平台设置页 | P1 |

---

## 11. 风险与说明

| 项 | 说明 |
|----|------|
| 编号前缀 **G→SN** | **仅新生成** 使用 SN；与功能需求一致，运营知悉存量编号 |
| 自动确认与 O8 | O8 仍用 `OrderPayTimeoutJob` + `sys_config`；**与本模块无关** |
| 缩短 autoConfirmDays | 在途单下次 Job **可能立即完结**；功能需求 MC-O4，**无技术拦截** |
| 备案 C 端落点 | 由 **shop-app** 读 `/api/mall/config` 渲染页脚；**无后端额外接口** |
| 多规格 SKU | **非本模块**；若后续建 `biz_goods_sku` 可含市场价，与商城设置无关 |

---

## 12. 修订记录

| 版本 | 说明 |
|------|------|
| **v1.0** | 首版：`biz_mall_config`、平台/C 端/商家接口、Facade、商品/订单/会员挂点、自动确认 Job |
| **v1.1** | 对齐功能需求 v1.1：移除 `market_price_ratio`、`show_market_price`；删除 `biz_goods.market_price` 及关联代码 |

---

*文档版本：v1.1 · 关联《商城设置功能需求.md》v1.1 · DDL：`sql/biz_mall_config.sql`、`sql/biz_mall_config_v1_1_alter.sql`、`sql/biz_goods_drop_market_price.sql`*
