# 批量标记通知已读接口（一键已读）- DTO版本

## 更新说明

本文档为 `markAllNoticesAsRead` 接口的最新版本，该接口已从使用Query参数改为使用DTO接收JSON请求体。

---

## 接口信息

### 批量标记通知为已读（一键已读）

#### 接口详情

- **接口名称**: 批量标记通知为已读（一键已读）
- **接口路径**: `POST /notice/markAllNoticesAsRead`
- **请求方式**: POST
- **Content-Type**: application/json
- **接口描述**: 批量将指定用户的未读通知标记为已读，支持按通知类型筛选
- **登录验证**: ❌ 不需要（公开接口）

---

## 请求参数

### 请求体（JSON格式）

使用 `MarkAllNoticesReadDTO` 作为请求体：

| 参数名 | 类型 | 必填 | 说明 | 示例值 |
|--------|------|------|------|--------|
| receiverId | String | 是 | 接收人ID（商户ID） | "store_18241052019" |
| noticeType | Integer | 否 | 通知类型 | 1 |

**noticeType 说明**：
- `0`: 系统通知和订单提醒之外的类型
- `1`: 系统通知
- `2`: 订单提醒
- 不传或传 `null`: 标记所有类型的通知

---

## DTO定义

```java
@Data
@ApiModel(description = "批量标记通知已读请求参数")
public class MarkAllNoticesReadDTO implements Serializable {
    
    @ApiModelProperty(value = "接收人ID（商户ID）", required = true, example = "store_18241052019")
    private String receiverId;
    
    @ApiModelProperty(value = "通知类型（0-系统通知和订单提醒之外的类型，1-系统通知，2-订单提醒）。不传则标记所有类型", required = false, example = "1")
    private Integer noticeType;
}
```

---

## 请求示例

### 示例1: 标记所有类型的通知为已读

```http
POST /notice/markAllNoticesAsRead
Content-Type: application/json

{
    "receiverId": "store_18241052019"
}
```

```bash
curl -X POST "http://localhost:8080/notice/markAllNoticesAsRead" \
  -H "Content-Type: application/json" \
  -d '{
    "receiverId": "store_18241052019"
  }'
```

---

### 示例2: 只标记系统通知为已读

```http
POST /notice/markAllNoticesAsRead
Content-Type: application/json

{
    "receiverId": "store_18241052019",
    "noticeType": 1
}
```

```bash
curl -X POST "http://localhost:8080/notice/markAllNoticesAsRead" \
  -H "Content-Type: application/json" \
  -d '{
    "receiverId": "store_18241052019",
    "noticeType": 1
  }'
```

---

### 示例3: 只标记订单提醒为已读

```http
POST /notice/markAllNoticesAsRead
Content-Type: application/json

{
    "receiverId": "store_18241052019",
    "noticeType": 2
}
```

```bash
curl -X POST "http://localhost:8080/notice/markAllNoticesAsRead" \
  -H "Content-Type: application/json" \
  -d '{
    "receiverId": "store_18241052019",
    "noticeType": 2
  }'
```

---

## 响应参数

### 成功响应

```json
{
    "code": 200,
    "success": true,
    "data": 15,
    "msg": "批量标记已读成功，共标记 15 条通知"
}
```

### 响应字段说明

| 字段名 | 类型 | 说明 |
|--------|------|------|
| code | Integer | 状态码，200表示成功 |
| success | Boolean | 是否成功 |
| data | Integer | 被标记的通知数量 |
| msg | String | 提示信息 |

---

## 响应示例

### 场景1: 成功标记多条通知

**请求**:
```json
{
    "receiverId": "store_18241052019",
    "noticeType": 1
}
```

**响应**:
```json
{
    "code": 200,
    "success": true,
    "data": 15,
    "msg": "批量标记已读成功，共标记 15 条通知"
}
```

---

### 场景2: 没有需要标记的通知

**请求**:
```json
{
    "receiverId": "store_18241052019",
    "noticeType": 1
}
```

**响应**:
```json
{
    "code": 200,
    "success": true,
    "data": 0,
    "msg": "没有需要标记的通知"
}
```

**说明**: 当前用户该类型的通知已全部已读，或不存在该类型的通知。

---

### 场景3: 参数错误

**请求**:
```json
{
    "noticeType": 1
}
```

**响应**:
```json
{
    "code": 500,
    "success": false,
    "data": null,
    "msg": "receiverId不能为空"
}
```

---

### 场景4: 系统异常

**响应**:
```json
{
    "code": 500,
    "success": false,
    "data": null,
    "msg": "标记失败：{异常信息}"
}
```

---

## 业务逻辑说明

### 处理流程

```
┌─────────────────────────────────────────────┐
│  1. 接收DTO参数                               │
│  - receiverId (必填)                         │
│  - noticeType (可选)                         │
└─────────────────┬───────────────────────────┘
                  ↓
┌─────────────────────────────────────────────┐
│  2. 参数验证                                  │
│  IF receiverId == null OR receiverId.isEmpty()│
│    RETURN 错误：receiverId不能为空           │
└─────────────────┬───────────────────────────┘
                  ↓
┌─────────────────────────────────────────────┐
│  3. 构建查询条件                              │
│  wrapper.eq("receiver_id", receiverId)       │
│  wrapper.eq("is_read", 0)  // 未读           │
│  IF noticeType != null                       │
│    wrapper.eq("notice_type", noticeType)     │
└─────────────────┬───────────────────────────┘
                  ↓
┌─────────────────────────────────────────────┐
│  4. 查询符合条件的未读通知                    │
│  List<LifeNotice> notices = selectList(wrapper)│
└─────────────────┬───────────────────────────┘
                  ↓
┌─────────────────────────────────────────────┐
│  5. 批量更新为已读状态                        │
│  updateWrapper.set("is_read", 1)             │
│  updateWrapper.eq("receiver_id", receiverId)  │
│  updateWrapper.eq("is_read", 0)              │
│  IF noticeType != null                       │
│    updateWrapper.eq("notice_type", noticeType)│
│  int count = update(null, updateWrapper)     │
└─────────────────┬───────────────────────────┘
                  ↓
┌─────────────────────────────────────────────┐
│  6. 返回结果                                  │
│  IF count > 0                                │
│    RETURN "批量标记已读成功，共标记 {count} 条"│
│  ELSE                                        │
│    RETURN "没有需要标记的通知"               │
└─────────────────────────────────────────────┘
```

### 更新SQL

```sql
UPDATE life_notice
SET is_read = 1
WHERE receiver_id = ?
  AND is_read = 0
  AND notice_type = ?  -- 如果指定了noticeType
```

---

## 前端集成示例

### Vue.js 示例

```javascript
export default {
    methods: {
        // 一键已读（所有类型）
        async markAllAsRead() {
            try {
                const response = await this.$axios.post('/notice/markAllNoticesAsRead', {
                    receiverId: this.currentUserId
                });
                
                if (response.data.success) {
                    this.$message.success(response.data.msg);
                    // 刷新通知列表
                    this.fetchNoticeList();
                } else {
                    this.$message.error(response.data.msg);
                }
            } catch (error) {
                console.error('标记失败:', error);
                this.$message.error('操作失败，请稍后重试');
            }
        },
        
        // 标记指定类型的通知为已读
        async markTypeAsRead(noticeType) {
            try {
                const response = await this.$axios.post('/notice/markAllNoticesAsRead', {
                    receiverId: this.currentUserId,
                    noticeType: noticeType
                });
                
                if (response.data.success) {
                    const count = response.data.data;
                    this.$message.success(`成功标记 ${count} 条通知为已读`);
                    this.fetchNoticeList();
                }
            } catch (error) {
                console.error('标记失败:', error);
            }
        }
    }
}
```

---

### React 示例

```javascript
import { useState } from 'react';
import axios from 'axios';
import { message } from 'antd';

function NoticeComponent() {
    const [userId, setUserId] = useState('store_18241052019');
    
    // 一键已读（所有类型）
    const markAllAsRead = async () => {
        try {
            const response = await axios.post('/notice/markAllNoticesAsRead', {
                receiverId: userId
            });
            
            if (response.data.success) {
                message.success(response.data.msg);
                // 刷新列表
                fetchNoticeList();
            }
        } catch (error) {
            message.error('操作失败');
        }
    };
    
    // 标记指定类型
    const markTypeAsRead = async (noticeType) => {
        try {
            const response = await axios.post('/notice/markAllNoticesAsRead', {
                receiverId: userId,
                noticeType: noticeType
            });
            
            if (response.data.success) {
                const count = response.data.data;
                message.success(`成功标记 ${count} 条通知为已读`);
            }
        } catch (error) {
            message.error('操作失败');
        }
    };
    
    return (
        <div>
            <button onClick={markAllAsRead}>一键已读（全部）</button>
            <button onClick={() => markTypeAsRead(1)}>标记系统通知已读</button>
            <button onClick={() => markTypeAsRead(2)}>标记订单提醒已读</button>
        </div>
    );
}
```

---

### jQuery/原生JS示例

```javascript
// 使用jQuery
function markAllNoticesAsRead(receiverId, noticeType) {
    $.ajax({
        url: '/notice/markAllNoticesAsRead',
        type: 'POST',
        contentType: 'application/json',
        data: JSON.stringify({
            receiverId: receiverId,
            noticeType: noticeType
        }),
        success: function(response) {
            if (response.success) {
                alert(response.msg);
                // 刷新通知列表
                loadNoticeList();
            } else {
                alert('操作失败：' + response.msg);
            }
        },
        error: function(xhr, status, error) {
            alert('网络错误，请稍后重试');
        }
    });
}

// 使用原生Fetch API
async function markAllNoticesAsRead(receiverId, noticeType = null) {
    try {
        const response = await fetch('/notice/markAllNoticesAsRead', {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json'
            },
            body: JSON.stringify({
                receiverId: receiverId,
                noticeType: noticeType
            })
        });
        
        const data = await response.json();
        
        if (data.success) {
            console.log('标记成功:', data.msg);
            return data.data; // 返回标记的数量
        } else {
            console.error('标记失败:', data.msg);
            return 0;
        }
    } catch (error) {
        console.error('请求失败:', error);
        return 0;
    }
}

// 使用示例
markAllNoticesAsRead('store_18241052019'); // 标记所有类型
markAllNoticesAsRead('store_18241052019', 1); // 只标记系统通知
```

---

## 与旧版本对比

### 旧版本（Query参数）

```java
@PostMapping("/markAllNoticesAsRead")
public R<Integer> markAllNoticesAsRead(
    @RequestParam("receiverId") String receiverId,
    @RequestParam(value = "noticeType", required = false) Integer noticeType
)
```

**请求方式**:
```
POST /notice/markAllNoticesAsRead?receiverId=store_18241052019&noticeType=1
```

---

### 新版本（JSON Body + DTO）

```java
@PostMapping("/markAllNoticesAsRead")
public R<Integer> markAllNoticesAsRead(@RequestBody MarkAllNoticesReadDTO dto)
```

**请求方式**:
```http
POST /notice/markAllNoticesAsRead
Content-Type: application/json

{
    "receiverId": "store_18241052019",
    "noticeType": 1
}
```

---

### 差异对比

| 项目 | 旧版本 | 新版本 | 说明 |
|------|--------|--------|------|
| 参数传递方式 | Query参数 | JSON Body | 更符合RESTful规范 |
| 参数定义 | 直接在方法参数中 | 使用DTO | 更易维护和扩展 |
| Content-Type | application/x-www-form-urlencoded | application/json | 标准JSON格式 |
| Swagger文档 | @ApiImplicitParams | @ApiModel/@ApiModelProperty | 自动生成完整文档 |
| 参数验证 | 手动验证 | 可使用@Valid + 验证注解 | 更规范 |
| 可扩展性 | 低 | 高 | 新增参数只需修改DTO |

---

## 参数验证增强（可选）

如果需要更严格的参数验证，可以在DTO中添加验证注解：

```java
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.Min;
import javax.validation.constraints.Max;

@Data
@ApiModel(description = "批量标记通知已读请求参数")
public class MarkAllNoticesReadDTO implements Serializable {
    
    @NotBlank(message = "接收人ID不能为空")
    @ApiModelProperty(value = "接收人ID（商户ID）", required = true)
    private String receiverId;
    
    @Min(value = 0, message = "通知类型必须大于等于0")
    @Max(value = 2, message = "通知类型必须小于等于2")
    @ApiModelProperty(value = "通知类型", required = false)
    private Integer noticeType;
}
```

然后在Controller中启用验证：

```java
@PostMapping("/markAllNoticesAsRead")
public R<Integer> markAllNoticesAsRead(@RequestBody @Valid MarkAllNoticesReadDTO dto) {
    // ...
}
```

---

## 常见问题

### Q1: 为什么要改用DTO？

**答案**: 
1. **更符合RESTful规范**: POST请求应使用JSON Body传递数据
2. **易于扩展**: 新增参数只需修改DTO，不影响方法签名
3. **类型安全**: 强类型DTO，编译时检查
4. **易于验证**: 可以使用Bean Validation注解
5. **文档更清晰**: Swagger自动生成完整的请求体文档

---

### Q2: 旧版本的调用方式还能用吗？

**答案**: 不能。接口已改为使用JSON Body，必须按新的方式调用。

---

### Q3: 如何迁移现有代码？

**答案**: 

**旧代码**:
```javascript
$.post('/notice/markAllNoticesAsRead?receiverId=' + userId + '&noticeType=1');
```

**新代码**:
```javascript
$.ajax({
    url: '/notice/markAllNoticesAsRead',
    type: 'POST',
    contentType: 'application/json',
    data: JSON.stringify({
        receiverId: userId,
        noticeType: 1
    })
});
```

---

### Q4: noticeType 不传和传null有区别吗？

**答案**: 没有区别。不传或传null都表示标记所有类型的通知。

---

### Q5: 返回的data是什么？

**答案**: data是被标记为已读的通知数量（Integer类型）。如果为0表示没有需要标记的通知。

---

## 测试用例

### 测试场景1: 标记所有类型的通知

**请求**:
```json
{
    "receiverId": "store_18241052019"
}
```

**预期结果**:
- 返回标记的数量 > 0
- 所有未读通知被标记为已读

---

### 测试场景2: 只标记系统通知

**请求**:
```json
{
    "receiverId": "store_18241052019",
    "noticeType": 1
}
```

**预期结果**:
- 只有系统通知（noticeType=1）被标记
- 其他类型的通知保持不变

---

### 测试场景3: receiverId为空

**请求**:
```json
{
    "noticeType": 1
}
```

**预期结果**:
- 返回错误："receiverId不能为空"

---

### 测试场景4: 无未读通知

**前置条件**: 
- 用户的所有通知都已读

**请求**:
```json
{
    "receiverId": "store_18241052019"
}
```

**预期结果**:
```json
{
    "code": 200,
    "data": 0,
    "msg": "没有需要标记的通知"
}
```

---

## 性能优化

### 1. 批量更新

使用MyBatis-Plus的批量更新功能，一次SQL更新所有符合条件的记录：

```sql
UPDATE life_notice 
SET is_read = 1 
WHERE receiver_id = ? 
  AND is_read = 0 
  AND notice_type = ?
```

### 2. 索引优化

确保以下字段有索引：

```sql
-- 复合索引
ALTER TABLE life_notice 
ADD INDEX idx_receiver_read_type (receiver_id, is_read, notice_type);
```

### 3. 返回更新数量

使用 `update()` 方法的返回值直接获取更新的记录数，避免额外查询。

---

## 注意事项

### 1. Content-Type必须正确

⚠️ 必须设置 `Content-Type: application/json`，否则服务器无法解析JSON请求体。

### 2. JSON格式

⚠️ 请求体必须是有效的JSON格式，字段名需要与DTO中的字段名完全匹配。

### 3. noticeType可选

⚠️ `noticeType` 是可选参数，不传或传null表示标记所有类型的通知。

### 4. 返回值类型

⚠️ `data` 字段是Integer类型，表示被标记的数量，不是布尔值。

---

## 更新日志

### 2025-11-17

**接口重构**:
- ✅ 从Query参数改为JSON Body + DTO
- ✅ 创建 `MarkAllNoticesReadDTO` 类
- ✅ 修改Controller方法签名
- ✅ 移除 @ApiImplicitParams 注解
- ✅ 使用 @RequestBody 接收参数

**优势**:
- ✅ 更符合RESTful规范
- ✅ 更易扩展和维护
- ✅ Swagger文档更清晰
- ✅ 支持参数验证注解

**涉及文件**:
- `MarkAllNoticesReadDTO.java` - 新增
- `NoticeController.java` - 修改

**代码质量**:
- ✅ Linter检查：无错误
- ✅ 代码规范：符合Java规范
- ✅ 注释完整：完整的注释和文档

**开发人员**: ssk

---

**文档版本**: v2.0  
**最后更新**: 2025-11-17  
**维护人员**: ssk






