# 商户通知管理接口文档 ## 模块概述 商户通知管理模块提供通知统计、通知列表查询、通知已读标记等功能,支持系统通知、订单提醒等多种通知类型。 --- ## 接口列表 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¬iceType=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 getNoticeStatistics(@RequestParam("receiverId") String receiverId) { // 查询通知统计信息 } @ApiOperation("获取通知列表") @GetMapping("/getNoticeList") public R> 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 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 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 queryWrapper = new LambdaQueryWrapper<>(); queryWrapper.eq(LifeNotice::getReceiverId, receiverId); queryWrapper.in(LifeNotice::getNoticeType, 1, 2); List 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 getNoticeList(int pageNum, int pageSize, String receiverId, int noticeType) { // 1. 查询通知列表 List lifeNoticeList = lifeNoticeMapper.selectList(queryWrapper); // 2. 解析 senderId,分组为 store 和 user Map> senderIdMap = /* 分组逻辑 */; // 3. 查询违规举报信息(用于平台类型判断) List lifeUserViolationList = /* 查询逻辑 */; // 4. 查询用户信息(商户和普通用户) List userList = lifeMessageMapper .getLifeUserAndStoreUserByPhone(storePhones, userPhones); // 5. 组装 LifeNoticeVo,关联用户信息 // 6. 设置平台类型标识 // 7. 手动分页 // 8. 构建分页结果 } ``` **3. markNoticeAsRead - 标记已读** ```java @Override public int markNoticeAsRead(Integer id) { // 使用 LambdaUpdateWrapper 更新 isRead 字段 LambdaUpdateWrapper 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 缓存未读数量,减少数据库压力