# 站内消息 — 技术方案（C 端）

> **依据：** 《站内消息功能需求.md》v1.0  
> **关联：** 平台《商户入驻审核技术方案》v1.1、《商户入驻审核功能需求》v1.1、《会员管理技术方案》v1.3、《关联需求分析.md》v1.6；C 端《我的服务技术方案》v1.0  
> **范围：** C 端 **`/api/member/message/**`** 未读数、分页列表、详情（含 **打开即已读**）；**复用** 既有 `biz_member_message` 与入驻审核 **写入** 链路；**不含** 前端页面、平台消息管理、短信/Push。  
> **原则：** **不新建表**；写入 **仅** `entry` 模块 `MemberEntryMessageSupport`；本模块 **只读 + 已读更新**；按 `member_id` **严格隔离**。

---

## 1. 技术架构

| 项 | 选型 |
|----|------|
| 基础框架 | RuoYi **v3.9.2**（`springboot2` 分支） |
| 数据库 | **MySQL 5.7.39** |
| ORM / 响应 | MyBatis；`AjaxResult`（`code`、`msg`、`data`）；列表 **`TableDataInfo`**（`rows`、`total`） |
| 鉴权 | **`MemberAuthInterceptor`**（`MemberWebConfig` → `/api/member/**`）；**无** `@Anonymous` |
| 分页 | **`PageHelper`** + `startPage()`；Query `pageNum`、`pageSize` |
| 缓存 | **本期不做** Redis |

### 1.1 模块落位

```text
baqing-shop/src/main/java/com/ruoyi/web/modules/message/     # 新建 · C 端读
├── controller/MemberMessageAppController.java               # /api/member/message/**
├── service/IMemberMessageAppService.java
├── service/impl/MemberMessageAppServiceImpl.java
├── vo/
│   ├── MemberMessageListItemVO.java                         # 列表行
│   ├── MemberMessageDetailVO.java                           # 详情
│   └── MemberMessageUnreadVO.java                           # 未读数
└── constant/MemberMessageConstants.java                     # 文案/ bizType 常量（读侧）

entry/（已有 · 写入 · 协作）
├── domain/BizMemberMessage.java
├── mapper/BizMemberMessageMapper.java                       # 扩展 select/count/updateRead
├── support/MemberEntryMessageSupport.java                   # approve/reject/complete 写消息
└── constant/MerchantEntryApplyConstants.java                # BIZ_TYPE_ENTRY_AUDIT、标题常量

account/（已有 · 鉴权）
├── config/MemberWebConfig.java                              # /api/member/** 已覆盖 message
└── support/MemberContext.java、MemberAuthInterceptor.java

resources/mapper/entry/BizMemberMessageMapper.xml            # 扩展 SQL
sql/biz_member_message.sql                                   # 权威 DDL（已存在）
```

### 1.2 协作链

```text
【写入 · 平台入驻审核 · 已实现】
    PUT /agri/merchantEntryApply/approve/{applyId}
        → MemberEntryMessageSupport.sendPublicity(...)
    PUT /agri/merchantEntryApply/completePublicity/{applyId}
    或 MerchantEntryPublicityTask.processExpiredPublicity()
        → MemberEntryMessageSupport.sendPass(...)
    PUT /agri/merchantEntryApply/reject
        → MemberEntryMessageSupport.sendReject(...)

【本模块 · C 端读】
    GET  /api/member/message/unread-count     → 红点/角标
    GET  /api/member/message/list             → 分页列表
    GET  /api/member/message/{messageId}      → 详情 + 标记已读

【前端跳转 · 协作】
    biz_type=ENTRY_AUDIT & biz_id=applyId
        → 我的入驻 GET /api/merchant/entry/my（按 applyId 定位详情页）
```

| 关联模块 | 协作 |
|----------|------|
| **平台 · 商户入驻审核** | **唯一写入方**（EA14）；三类消息标题/正文与 `MemberEntryMessageSupport` **一致** |
| **平台 · 会员管理** | 禁用会员 **无法登录** → 本模块不可用 |
| **C 端 · 我的服务 / 入驻** | 消息详情 **跳转** 至 `/api/merchant/entry/my` 对应申请 |
| **C 端 · 订单/关注** | 订单/开业提醒类消息 **非本期**；`biz_type` 预留扩展 |

### 1.3 职责边界

| 层 | 职责 |
|----|------|
| `MemberEntryMessageSupport` | **insert** 消息；**不** 提供 C 端 API |
| `MemberMessageAppService` | 按 `member_id` **查询**、**未读计数**、**详情并 markRead** |
| C 端 Controller | **无** 创建/删除/编辑消息接口（MM8） |

---

## 2. 数据库设计

### 2.1 复用 · 会员站内消息 `biz_member_message`

> DDL 见 [`sql/biz_member_message.sql`](../../../../sql/biz_member_message.sql)；**本期不 ALTER**。

| 字段 | 类型 | 说明 |
|------|------|------|
| message_id | bigint PK | 自增 |
| member_id | bigint | 收件会员；**列表/详情 WHERE 必带** |
| biz_type | varchar(32) | 业务类型；本期 **`ENTRY_AUDIT`** |
| biz_id | bigint | 业务主键；入驻类 = **`apply_id`** |
| title | varchar(128) | 标题 |
| content | varchar(1000) | 正文（含驳回原因、公示时间等） |
| read_flag | char(1) | **`0`** 未读 / **`1`** 已读；默认 `0` |
| create_time | datetime | 投递时间；列表 **降序** |

**索引（已有）：**

| 索引 | 用途 |
|------|------|
| `idx_member_time` (member_id, create_time) | 分页列表 |
| PK (message_id) | 详情 |

**可选优化（非本期必做）：** `idx_member_read` (member_id, read_flag) — 未读数大时加速 `COUNT`。

### 2.2 业务类型枚举（本期）

| biz_type | 含义 | biz_id | 写入时机 |
|----------|------|--------|----------|
| `ENTRY_AUDIT` | 商户入驻审核 | `apply_id` | 公示中 / 入驻完成 / 审核驳回 |

**标题常量（与 `MerchantEntryApplyConstants` 对齐）：**

| 场景 | title |
|------|-------|
| 审核通过→公示 | 入驻审核通过，公示中 |
| 完成公示 | 入驻完成 |
| 审核驳回 | 入驻审核未通过 |

### 2.3 与关联表

| 表 | 关系 |
|----|------|
| `biz_merchant_entry_apply` | `biz_id` → `apply_id`；跳转前 **可选** 校验申请是否存在 |
| `biz_member` | `member_id` → 收件人；与登录态 **一致** |

---

## 3. C 端接口设计

**基路径：** `/api/member/message`  
**鉴权：** 全部须 **`Authorization: Bearer {token}`**；未登录 → **401**「请先登录」  
**MemberWebConfig：** `/api/member/**` **已拦截**，**无需追加** path。

### 3.1 接口一览

| 方法 | 路径 | 说明 |
|------|------|------|
| GET | `/api/member/message/unread-count` | 未读条数（红点） |
| GET | `/api/member/message/list` | 消息分页列表 |
| GET | `/api/member/message/{messageId}` | 消息详情 + **标记已读** |

**本期不提供：**

| 项 | 说明 |
|----|------|
| POST/PUT/DELETE 消息 | C 端 **不可** 创建/编辑/删除（MM8） |
| 分类 Tab / 搜索 / 批量已读 | 需求 **非本期** |
| 平台 B 端查会员信箱 | **无** |

### 3.2 未读数 `GET /api/member/message/unread-count`

| 项 | 说明 |
|----|------|
| 数据范围 | `member_id = MemberContext.getMemberId()` 且 `read_flag='0'` |
| 响应 | `AjaxResult` → `data` |

**`data` → `MemberMessageUnreadVO`：**

| 字段 | 类型 | 说明 |
|------|------|------|
| unreadCount | int | 未读条数；**0** 时前端 **隐藏** 红点 |

**SQL 要点：** `SELECT COUNT(1) FROM biz_member_message WHERE member_id=? AND read_flag='0'`

### 3.3 消息列表 `GET /api/member/message/list`

| 项 | 说明 |
|----|------|
| Query | `pageNum`、`pageSize`（RuoYi 默认） |
| 排序 | `create_time DESC` |
| 响应 | **`TableDataInfo`** |

**`rows[]` → `MemberMessageListItemVO`：**

| 字段 | 类型 | 说明 |
|------|------|------|
| messageId | long | |
| title | string | |
| summary | string | `content` **截断**（建议 ≤80 字，Service 层 `StringUtils.abbreviate`） |
| readFlag | string | `0` / `1` |
| createTime | string | `yyyy-MM-dd HH:mm:ss` |
| bizType | string | 如 `ENTRY_AUDIT` |
| bizId | long | 如 `applyId` |

**空列表：** `rows=[]`，`total=0`；**非异常**。

### 3.4 消息详情 `GET /api/member/message/{messageId}`

| 项 | 说明 |
|----|------|
| 数据范围 | `message_id=? AND member_id=当前会员` |
| 副作用 | 若 `read_flag='0'` → **更新为 `1`**（MM2）；已读 **幂等** |
| 事务 | `@Transactional`：查询 + 条件更新 **同一事务** |

**不存在或非本人：** `ServiceException`「消息不存在」（**404** 或业务码，与项目统一）。

**`data` → `MemberMessageDetailVO`：**

| 字段 | 类型 | 说明 |
|------|------|------|
| messageId | long | |
| title | string | 完整标题 |
| content | string | 完整正文 |
| readFlag | string | 返回 **`1`**（本次打开后） |
| createTime | string | |
| bizType | string | |
| bizId | long | |
| linkType | string | 跳转类型；`ENTRY_AUDIT` → **`MERCHANT_ENTRY`** |
| linkAvailable | boolean | 关联 `apply_id` **是否存在**；`false` 时前端 **仅展示正文** |

**跳转约定（前端）：**

| linkType | 行为 |
|----------|------|
| `MERCHANT_ENTRY` | 跳转 **我的入驻** 页，携带 `applyId=bizId`；数据来自 **`GET /api/merchant/entry/my`** |

### 3.5 错误与响应格式

| 场景 | code | msg（示例） |
|------|------|-------------|
| 未登录 | 401 | 请先登录 |
| 消息不存在 | 404 或 500 | 消息不存在 |
| 成功 | 200 | 操作成功 |

统一 **`AjaxResult` / `TableDataInfo`**，与 C 端其它模块一致。

---

## 4. Service 要点

### 4.1 `getUnreadCount(memberId)`

```text
return mapper.countUnreadByMemberId(memberId)
```

### 4.2 `listMessages(memberId)`（Controller 内 `startPage()`）

```text
return mapper.selectListByMemberId(memberId)   # ORDER BY create_time DESC
→ 每行 content → summary 截断
```

### 4.3 `getDetailAndMarkRead(memberId, messageId)`（@Transactional）

```text
msg = mapper.selectByIdAndMemberId(messageId, memberId)
if msg == null → throw「消息不存在」
if read_flag == '0' → mapper.updateReadFlag(messageId, memberId, '1')
if biz_type == ENTRY_AUDIT → linkAvailable = applyMapper.selectById(bizId) != null
assemble MemberMessageDetailVO
```

### 4.4 Mapper 扩展（`BizMemberMessageMapper`）

| 方法 | 说明 |
|------|------|
| `insert` | **已有** · 写入 |
| `countUnreadByMemberId(Long memberId)` | 未读数 |
| `selectListByMemberId(Long memberId)` | 列表（PageHelper 分页） |
| `selectByIdAndMemberId(messageId, memberId)` | 详情 + 隔离 |
| `updateReadFlag(messageId, memberId, readFlag)` | `WHERE message_id AND member_id` |

---

## 5. 与平台入驻审核写入对齐

| 平台操作 | 消息方法 | title | content 要点 |
|----------|----------|-------|--------------|
| approve | `sendPublicity` | 入驻审核通过，公示中 | 申请编号 + 公示结束时间 |
| completePublicity | `sendPass` | 入驻完成 | 申请编号 + 经营账号登录指引 |
| reject | `sendReject` | 入驻审核未通过 | 申请编号 + **驳回原因** |

> **本模块不修改** 上述写入逻辑；联调时核对 **标题/正文** 与《站内消息功能需求》§7 **一致** 即可。

---

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

| 编号 | 实现要点 |
|------|----------|
| MM0 | `MemberWebConfig` + `MemberContext` |
| MM1 | `unread-count` → `COUNT read_flag=0` |
| MM2 | 详情接口 **同事务** 更新 `read_flag=1` |
| MM3 | 写入在 **entry**；本模块 **只读** |
| MM4 | content 与 `MemberEntryMessageSupport` **同源** |
| MM5 | 所有 SQL **带 member_id** |
| MM6 | 列表 `create_time DESC` + PageHelper |
| MM7 | 详情返回 `linkType` + `bizId` |
| MM8 | **无** 写/删 API |
| MM9 | 代录入 **无** insert → 本模块 **无** 此类消息 |
| MM10 | 仅处理已写入的 `ENTRY_AUDIT` |

---

## 7. 测试要点

| 用例 | 预期 |
|------|------|
| 未登录调 list | 401 |
| 会员 A 读会员 B 的 messageId | 「消息不存在」 |
| 未读详情后再调 unread-count | 计数 **-1** |
| 已读详情再次打开 | `read_flag` 仍为 1 |
| approve 后 list | 新行 `read_flag=0`，title=公示中 |
| reject 后 detail | content **含** rejectReason |
| 空信箱 list | rows=[], total=0 |

**建议测试类：** `MemberMessageAppServiceTest`、`MemberMessageAppControllerTest`（MockMvc + Token）。

---

## 8. 非本期 / 扩展

| 项 | 说明 |
|----|------|
| 订单类 `biz_type=ORDER_*` | 订单模块建设时 **扩展** insert + linkType |
| 店铺开业 `SHOP_OPEN` | 关注模块 **另册** |
| `PUT .../read-all` 全部已读 | 可迭代 |
| Redis 缓存未读数 | 量大时再考虑 |
| 平台消息管理后台 | **无** |

---

## 9. 修订记录

| 版本 | 说明 |
|------|------|
| **v1.0** | 首版：复用 `biz_member_message`；`/api/member/message` 三接口；与入驻审核写入链协作 |

---

*文档版本：v1.0 · 关联《站内消息功能需求.md》v1.0、《商户入驻审核技术方案.md》v1.1、《会员管理技术方案》v1.3 · 草稿保持不变。*
