# 商户通知管理接口文档

## 模块概述

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

---

## 接口列表

1. [获取系统通知和订单提醒统计](#接口一获取系统通知和订单提醒统计)
2. [获取通知列表](#接口二获取通知列表)

---

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

### 接口信息

- **接口名称**：获取系统通知和订单提醒统计
- **接口路径**：`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": "查询成功"
}
```

#### 响应字段说明

| 字段名 | 类型 | 说明 |
|--------|------|------|
| 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 | 总页数 |

---

## 业务逻辑说明

### 通知类型

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

### 平台类型判断逻辑

**平台类型 `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
@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) {
    // ...
}
```

### Service 层

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

```java
JSONObject getNoticeStatistics(String receiverId);

IPage<LifeNoticeVo> getNoticeList(int pageNum, int pageSize, String receiverId, int noticeType);
```

### Service 实现层

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

#### getNoticeList 核心逻辑

```java
@Override
public IPage<LifeNoticeVo> getNoticeList(int pageNum, int pageSize, String receiverId, int noticeType) {
    // 1. 查询通知列表
    LambdaQueryWrapper<LifeNotice> queryWrapper = new LambdaQueryWrapper<>();
    queryWrapper.eq(LifeNotice::getReceiverId, receiverId);
    queryWrapper.eq(LifeNotice::getNoticeType, noticeType);
    queryWrapper.eq(LifeNotice::getDeleteFlag, 0);
    queryWrapper.orderByDesc(LifeNotice::getCreatedTime);
    List<LifeNotice> lifeNoticeList = lifeNoticeMapper.selectList(queryWrapper);

    // 2. 解析 senderId，分组为 store 和 user
    Map<String, List<String>> senderIdMap = lifeNoticeList.stream()
            .map(LifeNotice::getSenderId)
            .filter(item -> item != null && !"system".equals(item) && item.contains("_"))
            .collect(Collectors.groupingBy(
                    item -> item.split("_")[0],  // 按 store 或 user 分组
                    Collectors.mapping(
                            item -> item.split("_")[1],  // 提取手机号
                            Collectors.toList()
                    )));

    // 3. 查询违规举报信息（用于判断平台类型）
    List<Integer> businessIdList = lifeNoticeList.stream()
            .map(LifeNotice::getBusinessId)
            .filter(Objects::nonNull)
            .collect(Collectors.toList());
    
    List<LifeUserViolation> lifeUserViolationList = 
            lifeUserViolationMapper.selectBatchIds(businessIdList);
    
    Map<Integer, String> lifeUserViolationMap = lifeUserViolationList.stream()
            .filter(item -> item.getReportContextType() != null)
            .collect(Collectors.toMap(
                    LifeUserViolation::getId,
                    LifeUserViolation::getReportContextType,
                    (existing, replacement) -> existing
            ));

    // 4. 查询用户信息（商户和普通用户）
    String storePhones = senderIdMap.containsKey("store") ? 
            "'" + String.join("','", senderIdMap.get("store")) + "'" : "''";
    String userPhones = senderIdMap.containsKey("user") ? 
            "'" + String.join("','", senderIdMap.get("user")) + "'" : "''";
    
    List<LifeMessageVo> userList = 
            lifeMessageMapper.getLifeUserAndStoreUserByPhone(storePhones, userPhones);

    // 5. 组装 LifeNoticeVo，关联用户信息
    List<LifeNoticeVo> noticeVoList = new ArrayList<>();
    lifeNoticeList.forEach(item -> {
        LifeNoticeVo noticeVo = new LifeNoticeVo();
        BeanUtils.copyProperties(item, noticeVo);
        
        // 设置用户信息（如果不是系统通知）
        if (!"system".equals(noticeVo.getSenderId())) {
            LifeMessageVo userinfo = userList.stream()
                    .filter(user -> user.getPhoneId().equals(item.getSenderId()))
                    .findFirst()
                    .orElse(null);
            if (userinfo != null) {
                noticeVo.setUserName(userinfo.getUserName());
                noticeVo.setUserImage(userinfo.getUserImage());
            }
        }
        
        noticeVoList.add(noticeVo);
    });

    // 6. 设置平台类型标识
    for (LifeNoticeVo lifeNoticeVo : noticeVoList) {
        // businessId 为空时，设置为默认平台
        if (lifeNoticeVo.getBusinessId() == null) {
            lifeNoticeVo.setPlatformType("1");
        }
        
        // 店铺审核通知设置为默认平台
        if ("店铺审核通知".equals(lifeNoticeVo.getTitle())) {
            lifeNoticeVo.setPlatformType("1");
        }
        
        // 根据举报类型判断平台类型
        if (lifeNoticeVo.getBusinessId() != null && 
            lifeUserViolationMap.containsKey(lifeNoticeVo.getBusinessId())) {
            String reportContextType = lifeUserViolationMap.get(lifeNoticeVo.getBusinessId());
            if ("1,2,3".contains(reportContextType)) {
                lifeNoticeVo.setPlatformType("1");
            }
        }
    }

    // 7. 手动分页
    List<LifeNoticeVo> pageList = noticeVoList.stream()
            .skip((long) (pageNum - 1) * pageSize)
            .limit(pageSize)
            .collect(Collectors.toList());

    // 8. 构建分页结果
    IPage<LifeNoticeVo> result = new Page<>();
    result.setRecords(pageList);
    result.setTotal(noticeVoList.size());
    result.setPages((int) Math.ceil(noticeVoList.size() / (double) pageSize));
    result.setSize(pageSize);
    result.setCurrent(pageNum);

    return result;
}
```

---

## 依赖注入

### 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.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 |
| Service | LifeNoticeService | NoticeService |

---

## 数据表说明

### 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 为空或无效
2. **数据库异常**：查询失败
3. **分页参数错误**：pageNum < 1 或 pageSize < 1

### 错误响应示例

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

---

## 使用场景

### 1. 首页通知角标

调用 `getNoticeStatistics` 获取未读数量，在首页显示通知角标。

### 2. 通知中心列表

调用 `getNoticeList` 获取通知列表，展示不同类型的通知：
- `noticeType=0`：其他通知（评论、关注等）
- `noticeType=1`：系统通知
- `noticeType=2`：订单提醒

### 3. 实时更新

通过 WebSocket 或定时轮询更新通知数量和列表。

---

## 测试建议

### 测试场景

**接口一（通知统计）**：
1. 有系统通知和订单提醒
2. 只有系统通知
3. 只有订单提醒
4. 没有任何通知
5. 未读数量统计准确性

**接口二（通知列表）**：
1. 正常分页查询
2. 空数据场景
3. 不同 noticeType 查询
4. 用户信息关联正确性
5. 平台类型标识正确性
6. 分页边界测试（第1页、最后1页、超出页数）

### 测试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;
```

---

## 注意事项

1. **receiverId 格式**：通常为 `store_` + 手机号（如：`store_18241052019`）
2. **逻辑删除**：MyBatis-Plus 自动处理 `delete_flag` 字段
3. **手动分页**：由于需要关联多表和复杂逻辑，采用手动分页而非数据库分页
4. **性能优化**：
   - 一次性查询所有通知，避免N+1问题
   - 批量查询用户信息和违规举报信息
   - 使用 Stream API 进行数据转换和过滤
5. **空值处理**：通知统计接口返回字符串 `"null"` 而非 null 对象
6. **日志记录**：详细记录查询参数、结果数量，便于问题追踪

---

## 更新日志

### 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`
- ✅ 用户信息关联：实现商户和普通用户信息查询
- ✅ 平台类型判断：实现复杂的平台类型标识逻辑
- ✅ 手动分页：实现内存分页功能
- ✅ Linter 检查：修复所有警告
- ✅ 业务逻辑：完全复用原接口逻辑
- ✅ 命名规范：符合web端命名规范

---

## 开发者信息

- **迁移时间**：2025-11-12
- **原服务**：alien-store（app端商户）
- **目标服务**：alien-store-platform（web端商户）
- **技术栈**：Spring Boot + MyBatis-Plus + FastJSON + Hutool + Java 8

