# 商户通知管理接口文档

## 模块概述

商户通知管理模块提供通知统计、通知列表查询、通知已读标记等功能，支持系统通知、订单提醒等多种通知类型。

---

## 接口列表

1. [获取系统通知和订单提醒统计](#接口一获取系统通知和订单提醒统计)
2. [获取通知列表](#接口二获取通知列表)
3. [标记通知为已读](#接口三标记通知为已读)

---

## 接口一：获取系统通知和订单提醒统计

### 接口信息

- **接口名称**：获取系统通知和订单提醒统计
- **接口路径**：`GET /notice/getNoticeStatistics`
- **接口描述**：查询指定商户的系统通知和订单提醒统计信息，包括最新一条通知内容和未读数量

### 请求参数

#### Query 参数

| 参数名 | 类型 | 必填 | 说明 |
|--------|------|------|------|
| receiverId | String | 是 | 接收人ID（商户ID，格式如：store_18241052019） |

#### 请求示例

```http
GET /notice/getNoticeStatistics?receiverId=store_18241052019
```

### 响应参数

#### 有通知数据时

```json
{
    "code": 200,
    "success": true,
    "data": {
        "systemNotice": {
            "id": "12345",
            "senderId": "system",
            "receiverId": "store_18241052019",
            "businessId": 102,
            "title": "店铺审核通知",
            "context": "您的店铺已通过审核",
            "noticeType": 1,
            "isRead": 0,
            "createdTime": "2025-11-12 14:30:00",
            "systemNum": 5
        },
        "orderNotice": {
            "id": "12346",
            "senderId": "system",
            "receiverId": "store_18241052019",
            "businessId": 2001,
            "title": "新订单提醒",
            "context": "您有一笔新订单",
            "noticeType": 2,
            "isRead": 0,
            "createdTime": "2025-11-12 15:20:00",
            "orderNum": 3
        }
    },
    "msg": "查询成功"
}
```

#### 无通知数据时

```json
{
    "code": 200,
    "success": true,
    "data": {
        "systemNotice": "null",
        "orderNotice": "null"
    },
    "msg": "查询成功"
}
```

#### 响应字段说明

| 字段名 | 类型 | 说明 |
|--------|------|------|
| systemNotice | Object/String | 系统通知对象或字符串"null" |
| systemNotice.systemNum | Long | 系统通知未读数量 |
| orderNotice | Object/String | 订单提醒对象或字符串"null" |
| orderNotice.orderNum | Long | 订单提醒未读数量 |

---

## 接口二：获取通知列表

### 接口信息

- **接口名称**：获取通知列表
- **接口路径**：`GET /notice/getNoticeList`
- **接口描述**：分页查询指定商户的通知列表，支持按通知类型筛选

### 请求参数

#### Query 参数

| 参数名 | 类型 | 必填 | 默认值 | 说明 |
|--------|------|------|--------|------|
| pageNum | int | 是 | - | 页码（从1开始） |
| pageSize | int | 是 | - | 每页条数 |
| receiverId | String | 是 | - | 接收人ID（商户ID） |
| noticeType | int | 否 | 0 | 通知类型（0-系统通知和订单提醒之外的类型，1-系统通知，2-订单提醒） |

#### 请求示例

```http
GET /notice/getNoticeList?receiverId=store_18241052019&pageNum=1&pageSize=10&noticeType=0
```

### 响应参数

#### 响应数据结构

```json
{
    "code": 200,
    "success": true,
    "data": {
        "records": [
            {
                "id": "12347",
                "senderId": "user_13800138000",
                "receiverId": "store_18241052019",
                "businessId": 301,
                "title": "新评论通知",
                "context": "用户对您的店铺进行了评论",
                "noticeType": 0,
                "isRead": 0,
                "userName": "张三",
                "userImage": "https://example.com/avatar.jpg",
                "platformType": "1",
                "createdTime": "2025-11-12 16:00:00"
            }
        ],
        "total": 50,
        "size": 10,
        "current": 1,
        "pages": 5
    },
    "msg": null
}
```

#### 响应字段说明

| 字段名 | 类型 | 说明 |
|--------|------|------|
| records | Array | 当前页通知列表 |
| records[].id | String | 通知ID |
| records[].senderId | String | 发送人ID |
| records[].receiverId | String | 接收人ID |
| records[].businessId | Integer | 业务ID |
| records[].title | String | 通知标题 |
| records[].context | String | 通知内容 |
| records[].noticeType | Integer | 通知类型 |
| records[].isRead | Integer | 是否已读（0-未读，1-已读） |
| records[].userName | String | 发送用户名称 |
| records[].userImage | String | 发送用户头像 |
| records[].platformType | String | 平台类型（1-默认平台，2-其他） |
| records[].createdTime | String | 创建时间 |
| total | Long | 总记录数 |
| size | Integer | 每页条数 |
| current | Integer | 当前页码 |
| pages | Integer | 总页数 |

---

## 接口三：标记通知为已读

### 接口信息

- **接口名称**：标记通知为已读
- **接口路径**：`POST /notice/markNoticeAsRead`
- **接口描述**：将指定ID的通知标记为已读状态

### 请求参数

#### Query 参数

| 参数名 | 类型 | 必填 | 说明 |
|--------|------|------|------|
| id | Integer | 是 | 通知ID |

#### 请求示例

```http
POST /notice/markNoticeAsRead?id=2589
```

### 响应参数

#### 成功响应

```json
{
    "code": 200,
    "success": true,
    "data": null,
    "msg": "标记已读成功"
}
```

#### 失败响应

```json
{
    "code": 500,
    "success": false,
    "data": null,
    "msg": "标记已读失败"
}
```

#### 响应字段说明

| 字段名 | 类型 | 说明 |
|--------|------|------|
| code | Integer | 响应状态码，200表示成功 |
| success | Boolean | 是否成功 |
| data | Any | 数据（此接口为null） |
| msg | String | 响应消息 |

---

## 业务逻辑说明

### 通知类型

| noticeType | 说明 |
|------------|------|
| 0 | 系统通知和订单提醒之外的类型（如评论、关注、举报等） |
| 1 | 系统通知（店铺审核、违规处理等） |
| 2 | 订单提醒（新订单、订单核销等） |

### 已读状态

| isRead | 说明 |
|--------|------|
| 0 | 未读 |
| 1 | 已读 |

### 平台类型判断逻辑

**平台类型 `platformType`** 用于标识通知来源平台：
- `"1"`：默认平台（本平台）
- `"2"`：其他平台

**判断规则**：
1. `businessId` 为空 → 默认平台
2. 标题为 "店铺审核通知" → 默认平台
3. 举报内容分类 `reportContextType` 为 1、2、3（商户、用户、动态） → 默认平台

### 用户信息关联

通知中的 `senderId` 格式：
- **系统通知**：`"system"`
- **商户发送**：`"store_手机号"`（如：`store_18241052019`）
- **普通用户发送**：`"user_手机号"`（如：`user_13800138000`）

系统会自动查询并关联发送人的 `userName` 和 `userImage`。

---

## 技术实现

### Controller 层

**文件路径**：`alien-store-platform/src/main/java/shop/alien/storeplatform/controller/NoticeController.java`

```java
@Slf4j
@Api(tags = {"web端商户通知管理"})
@ApiSort(6)
@CrossOrigin
@RestController
@RequestMapping("/notice")
@RequiredArgsConstructor
public class NoticeController {

    private final NoticeService noticeService;

    @ApiOperation("获取系统通知和订单提醒统计")
    @GetMapping("/getNoticeStatistics")
    public R<JSONObject> getNoticeStatistics(@RequestParam("receiverId") String receiverId) {
        // 查询通知统计信息
    }

    @ApiOperation("获取通知列表")
    @GetMapping("/getNoticeList")
    public R<IPage<LifeNoticeVo>> getNoticeList(
            @RequestParam("pageNum") int pageNum,
            @RequestParam("pageSize") int pageSize,
            @RequestParam("receiverId") String receiverId,
            @RequestParam(value = "noticeType", defaultValue = "0") int noticeType) {
        // 查询通知列表
    }

    @ApiOperation("标记通知为已读")
    @PostMapping("/markNoticeAsRead")
    public R<Boolean> markNoticeAsRead(@RequestParam("id") Integer id) {
        // 标记通知为已读
    }
}
```

### Service 层

**文件路径**：`alien-store-platform/src/main/java/shop/alien/storeplatform/service/NoticeService.java`

```java
public interface NoticeService {

    /**
     * 获取系统通知和订单提醒统计
     */
    JSONObject getNoticeStatistics(String receiverId);

    /**
     * 获取通知列表
     */
    IPage<LifeNoticeVo> getNoticeList(int pageNum, int pageSize, 
                                      String receiverId, int noticeType);

    /**
     * 标记通知为已读
     */
    int markNoticeAsRead(Integer id);
}
```

### Service 实现层

**文件路径**：`alien-store-platform/src/main/java/shop/alien/storeplatform/service/impl/NoticeServiceImpl.java`

#### 核心实现

**1. getNoticeStatistics - 通知统计**

```java
@Override
public JSONObject getNoticeStatistics(String receiverId) {
    // 1. 查询系统通知和订单提醒（noticeType: 1, 2）
    LambdaQueryWrapper<LifeNotice> queryWrapper = new LambdaQueryWrapper<>();
    queryWrapper.eq(LifeNotice::getReceiverId, receiverId);
    queryWrapper.in(LifeNotice::getNoticeType, 1, 2);
    List<LifeNotice> lifeNoticeList = lifeNoticeMapper.selectList(queryWrapper);

    // 2. 获取最新一条系统通知和订单提醒
    LifeNotice systemNotice = lifeNoticeList.stream()
            .filter(item -> 1 == item.getNoticeType())
            .max(Comparator.comparing(LifeNotice::getCreatedTime))
            .orElse(null);

    // 3. 统计未读数量
    long systemUnreadCount = lifeNoticeList.stream()
            .filter(item -> 1 == item.getNoticeType() && item.getIsRead() == 0)
            .count();

    // 4. 构建返回结果
    // ...
}
```

**2. getNoticeList - 通知列表**

```java
@Override
public IPage<LifeNoticeVo> getNoticeList(int pageNum, int pageSize, 
                                          String receiverId, int noticeType) {
    // 1. 查询通知列表
    List<LifeNotice> lifeNoticeList = lifeNoticeMapper.selectList(queryWrapper);

    // 2. 解析 senderId，分组为 store 和 user
    Map<String, List<String>> senderIdMap = /* 分组逻辑 */;

    // 3. 查询违规举报信息（用于平台类型判断）
    List<LifeUserViolation> lifeUserViolationList = /* 查询逻辑 */;

    // 4. 查询用户信息（商户和普通用户）
    List<LifeMessageVo> userList = lifeMessageMapper
            .getLifeUserAndStoreUserByPhone(storePhones, userPhones);

    // 5. 组装 LifeNoticeVo，关联用户信息
    // 6. 设置平台类型标识
    // 7. 手动分页
    // 8. 构建分页结果
}
```

**3. markNoticeAsRead - 标记已读**

```java
@Override
public int markNoticeAsRead(Integer id) {
    // 使用 LambdaUpdateWrapper 更新 isRead 字段
    LambdaUpdateWrapper<LifeNotice> wrapper = new LambdaUpdateWrapper<>();
    wrapper.eq(LifeNotice::getId, id);
    wrapper.set(LifeNotice::getIsRead, 1);
    
    return lifeNoticeMapper.update(null, wrapper);
}
```

---

## 依赖注入

### Service 实现类

```java
private final LifeNoticeMapper lifeNoticeMapper;         // 通知Mapper
private final LifeMessageMapper lifeMessageMapper;       // 消息Mapper（用于查询用户信息）
private final LifeUserViolationMapper lifeUserViolationMapper;  // 违规举报Mapper
```

### 导入依赖

```java
import cn.hutool.core.collection.CollectionUtil;
import com.alibaba.fastjson.JSONObject;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.BeanUtils;
import shop.alien.entity.store.LifeNotice;
import shop.alien.entity.store.LifeUserViolation;
import shop.alien.entity.store.vo.LifeMessageVo;
import shop.alien.entity.store.vo.LifeNoticeVo;
import shop.alien.mapper.LifeMessageMapper;
import shop.alien.mapper.LifeNoticeMapper;
import shop.alien.mapper.LifeUserViolationMapper;

import java.util.*;
import java.util.stream.Collectors;
```

---

## 原接口对比

### 接口一：通知统计

| 项目 | 原接口 | 新接口 |
|------|--------|--------|
| 服务 | alien-store（app端） | alien-store-platform（web端） |
| 路径 | /alienStore/notice/getSystemAndOrderNoticeSum | /notice/getNoticeStatistics |
| 方法名 | getSystemAndOrderNoticeSum | getNoticeStatistics |

### 接口二：通知列表

| 项目 | 原接口 | 新接口 |
|------|--------|--------|
| 服务 | alien-store（app端） | alien-store-platform（web端） |
| 路径 | /alienStore/notice/getNoticeByPhoneId | /notice/getNoticeList |
| 方法名 | getNoticeList | getNoticeList |
| Controller | LifeNoticeController | NoticeController |

### 接口三：标记已读

| 项目 | 原接口 | 新接口 |
|------|--------|--------|
| 服务 | alien-store（app端） | alien-store-platform（web端） |
| 路径 | /alienStore/notice/readNoticeById | /notice/markNoticeAsRead |
| 方法名 | readNoticeById | markNoticeAsRead |
| 请求方式 | GET | POST |

---

## 数据表说明

### life_notice（通知表）

| 字段名 | 类型 | 说明 |
|--------|------|------|
| id | String | 主键ID |
| sender_id | String | 发送人ID |
| receiver_id | String | 接收人ID |
| business_id | Integer | 业务ID |
| title | String | 通知标题 |
| context | String | 通知内容 |
| notice_type | Integer | 通知类型（0/1/2） |
| is_read | Integer | 是否已读（0-未读，1-已读） |
| delete_flag | Integer | 删除标记（0-未删除，1-已删除） |
| created_time | Date | 创建时间 |

### life_user_violation（用户举报表）

| 字段名 | 类型 | 说明 |
|--------|------|------|
| id | Integer | 主键ID |
| report_context_type | String | 举报内容分类（0-商户，1-用户，2-动态，3-评论，4-二手商品，5-二手用户） |
| violation_type | String | 违规类型 |
| processing_status | String | 处理状态（0-未处理，1-违规，2-未违规） |

---

## 错误处理

### 异常情况

1. **参数错误**：receiverId、pageNum、pageSize、id 为空或无效
2. **数据库异常**：查询或更新失败
3. **分页参数错误**：pageNum < 1 或 pageSize < 1
4. **通知不存在**：标记已读时通知ID不存在

### 错误响应示例

```json
{
    "code": 500,
    "success": false,
    "data": null,
    "msg": "查询失败：数据库连接异常"
}
```

---

## 使用场景

### 场景一：通知中心首页

1. 调用 **getNoticeStatistics** 获取未读数量，显示通知角标
2. 调用 **getNoticeList** 获取最新通知列表

### 场景二：通知分类浏览

根据不同 `noticeType` 查询不同类型的通知：
- `noticeType=0`：其他通知（评论、关注等）
- `noticeType=1`：系统通知
- `noticeType=2`：订单提醒

### 场景三：通知已读管理

1. 用户点击某条通知
2. 调用 **markNoticeAsRead** 标记为已读
3. 重新调用 **getNoticeStatistics** 更新未读数量

### 场景四：实时更新

通过 WebSocket 或定时轮询：
1. 定期调用 **getNoticeStatistics** 更新未读数量
2. 有新通知时刷新 **getNoticeList**

---

## 测试建议

### 接口一（通知统计）测试场景

1. ✅ 有系统通知和订单提醒
2. ✅ 只有系统通知
3. ✅ 只有订单提醒
4. ✅ 没有任何通知
5. ✅ 未读数量统计准确性
6. ✅ 最新通知时间排序正确性

### 接口二（通知列表）测试场景

1. ✅ 正常分页查询
2. ✅ 空数据场景
3. ✅ 不同 noticeType 查询
4. ✅ 用户信息关联正确性
5. ✅ 平台类型标识正确性
6. ✅ 分页边界测试（第1页、最后1页、超出页数）
7. ✅ 大数据量性能测试

### 接口三（标记已读）测试场景

1. ✅ 标记未读通知为已读
2. ✅ 重复标记（应该仍然返回成功）
3. ✅ 不存在的ID（返回失败）
4. ✅ 已删除的通知（MyBatis-Plus 自动过滤）
5. ✅ 并发标记测试

### 测试SQL

```sql
-- 查询商户的通知统计
SELECT 
    notice_type,
    COUNT(*) AS total_count,
    SUM(CASE WHEN is_read = 0 THEN 1 ELSE 0 END) AS unread_count,
    MAX(created_time) AS latest_time
FROM life_notice
WHERE receiver_id = 'store_18241052019'
  AND notice_type IN (1, 2)
  AND delete_flag = 0
GROUP BY notice_type;

-- 查询通知列表
SELECT 
    id, sender_id, receiver_id, business_id, title, context,
    notice_type, is_read, created_time
FROM life_notice
WHERE receiver_id = 'store_18241052019'
  AND notice_type = 0
  AND delete_flag = 0
ORDER BY created_time DESC
LIMIT 10 OFFSET 0;

-- 标记通知为已读
UPDATE life_notice 
SET is_read = 1 
WHERE id = 2589 
  AND delete_flag = 0;
```

---

## 注意事项

1. **receiverId 格式**：通常为 `store_` + 手机号（如：`store_18241052019`）
2. **逻辑删除**：MyBatis-Plus 自动处理 `delete_flag` 字段
3. **手动分页**：由于需要关联多表和复杂逻辑，采用手动分页而非数据库分页
4. **性能优化**：
   - 一次性查询所有通知，避免N+1问题
   - 批量查询用户信息和违规举报信息
   - 使用 Stream API 进行数据转换和过滤
5. **空值处理**：通知统计接口返回字符串 `"null"` 而非 null 对象
6. **日志记录**：详细记录查询参数、结果数量、操作结果，便于问题追踪
7. **幂等性**：标记已读接口支持重复调用，不会产生副作用
8. **安全性**：需要验证 receiverId 和 id 的合法性，防止越权访问

---

## 接口调用流程示例

### 完整业务流程

```
用户进入通知中心
    ↓
① 调用 getNoticeStatistics
    ↓
显示：系统通知(5)、订单提醒(3)
    ↓
② 调用 getNoticeList(noticeType=1)
    ↓
显示系统通知列表（分页）
    ↓
用户点击某条通知
    ↓
③ 调用 markNoticeAsRead(id=2589)
    ↓
标记成功后，再次调用 ① 更新未读数量
    ↓
显示：系统通知(4)、订单提醒(3)
```

---

## 更新日志

### 2025-11-12

**接口一（通知统计）**：
- ✅ 完成接口迁移：从 alien-store 迁移到 alien-store-platform
- ✅ Controller 层：创建 `NoticeController`，添加 `getNoticeStatistics` 接口
- ✅ Service 层：创建 `NoticeService`，添加方法定义
- ✅ Service 实现层：创建 `NoticeServiceImpl`，实现统计逻辑
- ✅ 业务逻辑：完全复用原接口逻辑
- ✅ 命名规范：符合web端命名规范

**接口二（通知列表）**：
- ✅ 完成接口迁移：从 alien-store 迁移到 alien-store-platform
- ✅ Controller 层：添加 `getNoticeList` 接口
- ✅ Service 层：添加 `getNoticeList` 方法定义
- ✅ Service 实现层：实现复杂的通知列表查询逻辑
- ✅ 依赖注入：添加 `LifeMessageMapper` 和 `LifeUserViolationMapper`
- ✅ 用户信息关联：实现商户和普通用户信息查询
- ✅ 平台类型判断：实现复杂的平台类型标识逻辑
- ✅ 手动分页：实现内存分页功能
- ✅ 业务逻辑：完全复用原接口逻辑

**接口三（标记已读）**：
- ✅ 完成接口迁移：从 alien-store 迁移到 alien-store-platform
- ✅ Controller 层：添加 `markNoticeAsRead` 接口
- ✅ Service 层：添加 `markNoticeAsRead` 方法定义
- ✅ Service 实现层：使用 LambdaUpdateWrapper 实现更新逻辑
- ✅ 请求方式：改为 POST，更符合 REST 规范
- ✅ 命名优化：`readNoticeById` → `markNoticeAsRead`（更语义化）
- ✅ 业务逻辑：完全复用原接口逻辑
- ✅ Linter 检查：无错误

---

## 开发者信息

- **迁移时间**：2025-11-12
- **原服务**：alien-store（app端商户）
- **目标服务**：alien-store-platform（web端商户）
- **技术栈**：Spring Boot + MyBatis-Plus + FastJSON + Hutool + Java 8
- **开发人员**：ssk
- **文档版本**：v1.0

---

## 附录：常见问题

### Q1：为什么通知列表使用手动分页？

**A**：因为需要关联多个表（用户信息、违规举报信息）并进行复杂的数据处理（平台类型判断），如果使用数据库分页，SQL会非常复杂且难以维护。手动分页虽然会查询全部数据，但在通知数量不是特别大的情况下（通常每个商户的通知数量有限），性能是可以接受的。

### Q2：为什么通知统计返回字符串 "null" 而不是 null？

**A**：这是为了保持与原接口的兼容性。原接口就是这样设计的，可能是为了避免前端处理 null 值的麻烦。

### Q3：如何批量标记多个通知为已读？

**A**：当前接口只支持单个通知标记。如需批量标记，可以扩展接口，接收 ID 数组参数，循环调用更新逻辑。

### Q4：通知的删除和标记已读有什么区别？

**A**：
- **标记已读**：`is_read = 1`，通知仍然存在，只是标记为已读状态
- **删除通知**：`delete_flag = 1`，逻辑删除，通知不再显示在列表中

### Q5：如何保证通知的实时性？

**A**：建议结合以下方案：
1. 后端：使用 WebSocket 推送新通知
2. 前端：定时轮询通知统计接口（如每30秒）
3. 缓存：使用 Redis 缓存未读数量，减少数据库压力
