消息通知 API
消息通知系统提供完整的消息管理功能,包括系统通知、公告、提醒和审批消息的发送、接收、查看和管理。支持个人消息和全局消息,具备消息状态管理和统计分析功能。
功能概述
- 消息发送:支持发送给指定用户或全局广播
- 消息类型:支持通知、公告、提醒、审批四种消息类型
- 优先级管理:支持低、普通、高、紧急四个优先级
- 状态管理:支持已读/未读、归档状态管理
- 权限控制:管理员管理所有消息,用户只能查看自己的消息
- 统计分析:提供消息统计和阅读率分析
消息类型说明
| 类型 | 英文标识 | 描述 | 用途 |
|---|---|---|---|
| 通知 | notification | 系统通知 | 系统功能更新、维护通知等 |
| 公告 | announcement | 公告消息 | 公司公告、政策发布等 |
| 提醒 | reminder | 提醒消息 | 会议提醒、任务提醒等 |
| 审批 | approval | 审批消息 | 请假审批、报销审批等 |
优先级说明
| 优先级 | 英文标识 | 描述 | 显示效果 |
|---|---|---|---|
| 低 | low | 低优先级 | 普通显示 |
| 普通 | normal | 普通优先级 | 默认显示 |
| 高 | high | 高优先级 | 突出显示 |
| 紧急 | urgent | 紧急 | 红色高亮显示 |
管理员接口
创建消息
管理员创建新消息,可发送给指定用户或设为全局消息。
- URL:
/api/v1/admin/messages - 方法:
POST - 内容类型:
application/json - 认证: 需要管理员权限
请求参数
| 字段 | 类型 | 必填 | 描述 | 示例 |
|---|---|---|---|---|
| title | string | 是 | 消息标题 | "部门会议提醒" |
| content | string | 否 | 消息内容 | "明天上午10点部门会议,请准时参加" |
| type | string | 是 | 消息类型 | "reminder" |
| priority | string | 是 | 消息优先级 | "normal" |
| recipient_ids | uint[] | 否 | 接收者ID列表 | [1, 2, 3] |
| is_global | bool | 否 | 是否全局消息 | false |
| expires_at | string | 否 | 过期时间 | "2024-12-31" |
| published_at | string | 否 | 发布时间 | "2024-01-01T00:00:00Z" |
请求示例
json
{
"title": "部门会议提醒",
"content": "明天上午10点部门会议,请准时参加。会议地点:3楼会议室。",
"type": "reminder",
"priority": "normal",
"recipient_ids": [1, 2, 3],
"is_global": false,
"expires_at": "2024-12-31",
"published_at": "2024-01-01T09:00:00Z"
}1
2
3
4
5
6
7
8
9
10
2
3
4
5
6
7
8
9
10
响应示例
成功响应 (201)
json
{
"code": 200,
"message": "创建成功",
"data": {
"id": 1,
"title": "部门会议提醒",
"content": "明天上午10点部门会议,请准时参加。会议地点:3楼会议室。",
"type": "reminder",
"priority": "normal",
"sender_id": 1,
"sender": {
"id": 1,
"username": "admin",
"real_name": "管理员"
},
"is_global": false,
"expires_at": "2024-12-31T23:59:59Z",
"published_at": "2024-01-01T09:00:00Z",
"is_active": true,
"is_expired": false,
"is_published": true,
"created_at": "2024-01-01T08:30:00Z",
"updated_at": "2024-01-01T08:30:00Z"
},
"timestamp": "2024-01-01T12:00:00Z"
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
更新消息
更新指定消息的信息(只有发送者可以更新)。
- URL:
/api/v1/admin/messages/{id} - 方法:
PUT - 内容类型:
application/json - 认证: 需要管理员权限
请求参数
路径参数
| 参数 | 类型 | 必填 | 描述 | 示例 |
|---|---|---|---|---|
| id | uint | 是 | 消息ID | 1 |
请求体参数
| 字段 | 类型 | 必填 | 描述 | 示例 |
|---|---|---|---|---|
| title | string | 否 | 消息标题 | "更新后的会议提醒" |
| content | string | 否 | 消息内容 | "会议时间改为下午2点" |
| priority | string | 否 | 消息优先级 | "high" |
| is_active | bool | 否 | 是否激活 | true |
请求示例
json
{
"title": "更新后的会议提醒",
"content": "会议时间改为下午2点,地点不变。",
"priority": "high",
"is_active": true
}1
2
3
4
5
6
2
3
4
5
6
响应示例
成功响应 (200)
json
{
"code": 200,
"message": "更新消息成功",
"data": {
"id": 1,
"title": "更新后的会议提醒",
"content": "会议时间改为下午2点,地点不变。",
"type": "reminder",
"priority": "high",
"sender_id": 1,
"sender": {
"id": 1,
"username": "admin",
"real_name": "管理员"
},
"is_global": false,
"expires_at": "2024-12-31T23:59:59Z",
"published_at": "2024-01-01T09:00:00Z",
"is_active": true,
"is_expired": false,
"is_published": true,
"created_at": "2024-01-01T08:30:00Z",
"updated_at": "2024-01-01T09:15:00Z"
},
"timestamp": "2024-01-01T12:00:00Z"
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
删除消息
删除指定消息(只有发送者可以删除)。
- URL:
/api/v1/admin/messages/{id} - 方法:
DELETE - 认证: 需要管理员权限
请求参数
| 参数 | 类型 | 必填 | 描述 | 示例 |
|---|---|---|---|---|
| id | uint | 是 | 消息ID | 1 |
响应示例
成功响应 (200)
json
{
"code": 200,
"message": "删除消息成功",
"data": null,
"timestamp": "2024-01-01T12:00:00Z"
}1
2
3
4
5
6
2
3
4
5
6
获取管理员消息列表
获取系统中所有消息列表(管理员权限)。
- URL:
/api/v1/admin/messages - 方法:
GET - 认证: 需要管理员权限
请求参数
| 参数 | 类型 | 必填 | 描述 | 示例 |
|---|---|---|---|---|
| page | int | 否 | 页码,默认1 | 1 |
| page_size | int | 否 | 每页数量,默认10 | 20 |
| type | string | 否 | 消息类型 | "reminder" |
| priority | string | 否 | 消息优先级 | "high" |
| sender_id | uint | 否 | 发送者ID | 1 |
| is_active | bool | 否 | 是否激活 | true |
| is_global | bool | 否 | 是否全局消息 | false |
| search | string | 否 | 搜索关键词 | "会议" |
| date_from | string | 否 | 开始日期 | "2024-01-01" |
| date_to | string | 否 | 结束日期 | "2024-12-31" |
请求示例
GET /api/v1/admin/messages?page=1&page_size=20&type=reminder&priority=high&search=会议1
响应示例
成功响应 (200)
json
{
"code": 200,
"message": "获取消息列表成功",
"data": {
"messages": [
{
"id": 1,
"title": "更新后的会议提醒",
"content": "会议时间改为下午2点,地点不变。",
"type": "reminder",
"priority": "high",
"sender_id": 1,
"sender": {
"id": 1,
"username": "admin",
"real_name": "管理员"
},
"is_global": false,
"expires_at": "2024-12-31T23:59:59Z",
"published_at": "2024-01-01T09:00:00Z",
"is_active": true,
"is_expired": false,
"is_published": true,
"created_at": "2024-01-01T08:30:00Z",
"updated_at": "2024-01-01T09:15:00Z"
}
],
"pagination": {
"current_page": 1,
"page_size": 20,
"total_items": 1,
"total_pages": 1,
"has_next": false,
"has_prev": false
}
},
"timestamp": "2024-01-01T12:00:00Z"
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
获取消息详情
获取消息的详细信息,包括接收者信息。
- URL:
/api/v1/admin/messages/{id}/detail - 方法:
GET - 认证: 需要管理员权限
请求参数
| 参数 | 类型 | 必填 | 描述 | 示例 |
|---|---|---|---|---|
| id | uint | 是 | 消息ID | 1 |
响应示例
成功响应 (200)
json
{
"code": 200,
"message": "获取消息详情成功",
"data": {
"id": 1,
"title": "更新后的会议提醒",
"content": "会议时间改为下午2点,地点不变。",
"type": "reminder",
"priority": "high",
"sender_id": 1,
"sender": {
"id": 1,
"username": "admin",
"real_name": "管理员"
},
"is_global": false,
"expires_at": "2024-12-31T23:59:59Z",
"published_at": "2024-01-01T09:00:00Z",
"is_active": true,
"is_expired": false,
"is_published": true,
"created_at": "2024-01-01T08:30:00Z",
"updated_at": "2024-01-01T09:15:00Z",
"recipients": [
{
"id": 1,
"message_id": 1,
"recipient_id": 1,
"recipient": {
"id": 1,
"username": "user1",
"real_name": "张三"
},
"is_read": true,
"read_at": "2024-01-01T10:00:00Z",
"is_archived": false,
"archived_at": null,
"created_at": "2024-01-01T08:30:00Z",
"updated_at": "2024-01-01T10:00:00Z"
}
]
},
"timestamp": "2024-01-01T12:00:00Z"
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
添加消息接收者
为指定消息添加接收者(只有发送者可操作)。
- URL:
/api/v1/admin/messages/{id}/recipients - 方法:
POST - 内容类型:
application/json - 认证: 需要管理员权限
请求参数
路径参数
| 参数 | 类型 | 必填 | 描述 | 示例 |
|---|---|---|---|---|
| id | uint | 是 | 消息ID | 1 |
请求体参数
| 字段 | 类型 | 必填 | 描述 | 示例 |
|---|---|---|---|---|
| recipient_ids | uint[] | 是 | 接收者ID列表 | [4, 5, 6] |
请求示例
json
{
"recipient_ids": [4, 5, 6]
}1
2
3
2
3
响应示例
成功响应 (200)
json
{
"code": 200,
"message": "添加接收者成功",
"data": null,
"timestamp": "2024-01-01T12:00:00Z"
}1
2
3
4
5
6
2
3
4
5
6
发送全局消息
发送全局消息给所有用户。
- URL:
/api/v1/admin/messages/global - 方法:
POST - 内容类型:
application/json - 认证: 需要管理员权限
请求参数
| 字段 | 类型 | 必填 | 描述 | 示例 |
|---|---|---|---|---|
| title | string | 是 | 消息标题 | "系统维护通知" |
| content | string | 否 | 消息内容 | "系统将于今晚22:00进行维护,预计2小时" |
| type | string | 是 | 消息类型 | "announcement" |
| priority | string | 是 | 消息优先级 | "high" |
| expires_at | string | 否 | 过期时间 | "2024-12-31" |
| published_at | string | 否 | 发布时间 | "2024-01-01T20:00:00Z" |
请求示例
json
{
"title": "系统维护通知",
"content": "系统将于今晚22:00进行维护,预计2小时,请提前做好相关工作安排。",
"type": "announcement",
"priority": "high",
"expires_at": "2024-12-31",
"published_at": "2024-01-01T20:00:00Z"
}1
2
3
4
5
6
7
8
2
3
4
5
6
7
8
响应示例
成功响应 (201)
json
{
"code": 200,
"message": "发送全局消息成功",
"data": {
"id": 2,
"title": "系统维护通知",
"content": "系统将于今晚22:00进行维护,预计2小时,请提前做好相关工作安排。",
"type": "announcement",
"priority": "high",
"sender_id": 1,
"sender": {
"id": 1,
"username": "admin",
"real_name": "管理员"
},
"is_global": true,
"expires_at": "2024-12-31T23:59:59Z",
"published_at": "2024-01-01T20:00:00Z",
"is_active": true,
"is_expired": false,
"is_published": true,
"created_at": "2024-01-01T19:30:00Z",
"updated_at": "2024-01-01T19:30:00Z"
},
"timestamp": "2024-01-01T12:00:00Z"
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
用户接口
获取用户消息列表
获取当前用户的消息列表。
- URL:
/api/v1/messages - 方法:
GET - 认证: 需要用户权限
请求参数
| 参数 | 类型 | 必填 | 描述 | 示例 |
|---|---|---|---|---|
| page | int | 否 | 页码,默认1 | 1 |
| page_size | int | 否 | 每页数量,默认10 | 10 |
| type | string | 否 | 消息类型 | "reminder" |
| priority | string | 否 | 消息优先级 | "high" |
| is_read | bool | 否 | 是否已读 | false |
| is_archived | bool | 否 | 是否归档 | false |
| sender_id | uint | 否 | 发送者ID | 1 |
| search | string | 否 | 搜索关键词 | "会议" |
请求示例
GET /api/v1/messages?page=1&page_size=10&is_read=false&search=会议1
响应示例
成功响应 (200)
json
{
"code": 200,
"message": "获取消息列表成功",
"data": {
"messages": [
{
"id": 1,
"title": "更新后的会议提醒",
"content": "会议时间改为下午2点,地点不变。",
"type": "reminder",
"priority": "high",
"sender_id": 1,
"sender": {
"id": 1,
"username": "admin",
"real_name": "管理员"
},
"is_global": false,
"expires_at": "2024-12-31T23:59:59Z",
"published_at": "2024-01-01T09:00:00Z",
"is_active": true,
"is_expired": false,
"is_published": true,
"is_read": false,
"read_at": null,
"is_archived": false,
"archived_at": null,
"created_at": "2024-01-01T08:30:00Z",
"updated_at": "2024-01-01T09:15:00Z"
}
],
"pagination": {
"current_page": 1,
"page_size": 10,
"total_items": 1,
"total_pages": 1,
"has_next": false,
"has_prev": false
}
},
"timestamp": "2024-01-01T12:00:00Z"
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
获取消息详情
获取单个消息的详细信息。
- URL:
/api/v1/messages/{id} - 方法:
GET - 认证: 需要用户权限
请求参数
| 参数 | 类型 | 必填 | 描述 | 示例 |
|---|---|---|---|---|
| id | uint | 是 | 消息ID | 1 |
响应示例
成功响应 (200)
json
{
"code": 200,
"message": "获取消息成功",
"data": {
"id": 1,
"title": "更新后的会议提醒",
"content": "会议时间改为下午2点,地点不变。",
"type": "reminder",
"priority": "high",
"sender_id": 1,
"sender": {
"id": 1,
"username": "admin",
"real_name": "管理员"
},
"is_global": false,
"expires_at": "2024-12-31T23:59:59Z",
"published_at": "2024-01-01T09:00:00Z",
"is_active": true,
"is_expired": false,
"is_published": true,
"is_read": false,
"read_at": null,
"is_archived": false,
"archived_at": null,
"created_at": "2024-01-01T08:30:00Z",
"updated_at": "2024-01-01T09:15:00Z"
},
"timestamp": "2024-01-01T12:00:00Z"
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
标记消息为已读
标记一个或多个消息为已读状态。
- URL:
/api/v1/messages/read - 方法:
POST - 内容类型:
application/json - 认证: 需要用户权限
请求参数
| 字段 | 类型 | 必填 | 描述 | 示例 |
|---|---|---|---|---|
| message_ids | uint[] | 是 | 消息ID列表 | [1, 2, 3] |
请求示例
json
{
"message_ids": [1, 2, 3]
}1
2
3
2
3
响应示例
成功响应 (200)
json
{
"code": 200,
"message": "标记消息已读成功",
"data": null,
"timestamp": "2024-01-01T12:00:00Z"
}1
2
3
4
5
6
2
3
4
5
6
归档消息
标记一个或多个消息为归档状态。
- URL:
/api/v1/messages/archive - 方法:
POST - 内容类型:
application/json - 认证: 需要用户权限
请求参数
| 字段 | 类型 | 必填 | 描述 | 示例 |
|---|---|---|---|---|
| message_ids | uint[] | 是 | 消息ID列表 | [1, 2, 3] |
请求示例
json
{
"message_ids": [1, 2, 3]
}1
2
3
2
3
响应示例
成功响应 (200)
json
{
"code": 200,
"message": "归档消息成功",
"data": null,
"timestamp": "2024-01-01T12:00:00Z"
}1
2
3
4
5
6
2
3
4
5
6
获取未读消息数量
获取当前用户的未读消息数量。
- URL:
/api/v1/messages/unread-count - 方法:
GET - 认证: 需要用户权限
响应示例
成功响应 (200)
json
{
"code": 200,
"message": "获取未读消息数量成功",
"data": {
"unread_count": 5
},
"timestamp": "2024-01-01T12:00:00Z"
}1
2
3
4
5
6
7
8
2
3
4
5
6
7
8
获取用户消息统计
获取当前用户的消息统计信息。
- URL:
/api/v1/messages/stats - 方法:
GET - 认证: 需要用户权限
响应示例
成功响应 (200)
json
{
"code": 200,
"message": "获取消息统计成功",
"data": {
"total_count": 100,
"unread_count": 5,
"read_count": 95,
"archived_count": 10,
"notification_count": 80,
"announcement_count": 10,
"reminder_count": 8,
"approval_count": 2
},
"timestamp": "2024-01-01T12:00:00Z"
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
2
3
4
5
6
7
8
9
10
11
12
13
14
15
数据模型
MessageResponse
typescript
interface MessageResponse {
id: number; // 消息ID
title: string; // 消息标题
content: string; // 消息内容
type: MessageType; // 消息类型
priority: MessagePriority; // 消息优先级
sender_id: number; // 发送者ID
sender?: UserResponse; // 发送者信息
is_global: boolean; // 是否全局消息
expires_at?: string; // 过期时间
published_at?: string; // 发布时间
is_active: boolean; // 是否激活
is_expired: boolean; // 是否过期
is_published: boolean; // 是否已发布
created_at: string; // 创建时间
updated_at: string; // 更新时间
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
UserMessageResponse
typescript
interface UserMessageResponse extends MessageResponse {
is_read: boolean; // 是否已读
read_at?: string; // 阅读时间
is_archived: boolean; // 是否归档
archived_at?: string; // 归档时间
}1
2
3
4
5
6
2
3
4
5
6
MessageDetailResponse
typescript
interface MessageDetailResponse extends MessageResponse {
recipients: MessageRecipientResponse[]; // 接收者列表
}1
2
3
2
3
MessageStatsResponse
typescript
interface MessageStatsResponse {
total_count: number; // 总消息数
unread_count: number; // 未读消息数
read_count: number; // 已读消息数
archived_count: number; // 归档消息数
notification_count: number; // 通知消息数
announcement_count: number; // 公告消息数
reminder_count: number; // 提醒消息数
approval_count: number; // 审批消息数
}1
2
3
4
5
6
7
8
9
10
2
3
4
5
6
7
8
9
10
MessageType
typescript
type MessageType = 'notification' | 'announcement' | 'reminder' | 'approval';1
MessagePriority
typescript
type MessagePriority = 'low' | 'normal' | 'high' | 'urgent';1
错误代码
| 错误代码 | 描述 | 解决方案 |
|---|---|---|
| 400 | 请求参数错误 | 检查请求参数格式和必填字段 |
| 401 | 未认证 | 检查 JWT token 是否有效 |
| 403 | 无权限 | 确认用户具有相应权限 |
| 404 | 消息不存在 | 检查消息ID是否正确 |
| 500 | 服务器内部错误 | 联系系统管理员 |
消息状态管理
读取状态
- 未读:用户尚未查看的消息
- 已读:用户已查看的消息,自动记录阅读时间
归档状态
- 未归档:正常显示在消息列表中
- 已归档:已归档的消息,不在常规列表中显示
发布状态
- 未发布:消息创建但未到发布时间
- 已发布:消息已到发布时间,用户可见
- 已过期:消息已过过期时间,不再显示
权限说明
管理员权限
- 创建、更新、删除所有消息
- 查看所有消息和接收者信息
- 发送全局消息
- 管理消息接收者
用户权限
- 查看发送给自己的消息
- 标记消息为已读/归档
- 查看自己的消息统计
- 不能修改或删除消息
集成示例
JavaScript 消息管理组件
javascript
import React, { useState, useEffect } from 'react';
const MessageCenter = () => {
const [messages, setMessages] = useState([]);
const [unreadCount, setUnreadCount] = useState(0);
const [loading, setLoading] = useState(false);
const [selectedType, setSelectedType] = useState('all');
const [selectedPriority, setSelectedPriority] = useState('all');
const fetchMessages = async (type = 'all', priority = 'all') => {
setLoading(true);
try {
const params = new URLSearchParams();
if (type !== 'all') params.append('type', type);
if (priority !== 'all') params.append('priority', priority);
const response = await fetch(`/api/v1/messages?${params}`, {
headers: {
'Authorization': `Bearer ${localStorage.getItem('token')}`
}
});
const data = await response.json();
if (data.code === 200) {
setMessages(data.data.messages);
}
} catch (error) {
console.error('获取消息列表失败:', error);
} finally {
setLoading(false);
}
};
const fetchUnreadCount = async () => {
try {
const response = await fetch('/api/v1/messages/unread-count', {
headers: {
'Authorization': `Bearer ${localStorage.getItem('token')}`
}
});
const data = await response.json();
if (data.code === 200) {
setUnreadCount(data.data.unread_count);
}
} catch (error) {
console.error('获取未读消息数量失败:', error);
}
};
const markAsRead = async (messageIds) => {
try {
const response = await fetch('/api/v1/messages/read', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${localStorage.getItem('token')}`
},
body: JSON.stringify({ message_ids: messageIds })
});
if (response.ok) {
// 更新本地状态
setMessages(prev => prev.map(msg =>
messageIds.includes(msg.id)
? { ...msg, is_read: true, read_at: new Date().toISOString() }
: msg
));
setUnreadCount(prev => Math.max(0, prev - messageIds.length));
}
} catch (error) {
console.error('标记已读失败:', error);
}
};
const archiveMessages = async (messageIds) => {
try {
const response = await fetch('/api/v1/messages/archive', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${localStorage.getItem('token')}`
},
body: JSON.stringify({ message_ids: messageIds })
});
if (response.ok) {
// 从列表中移除已归档的消息
setMessages(prev => prev.filter(msg => !messageIds.includes(msg.id)));
}
} catch (error) {
console.error('归档消息失败:', error);
}
};
const getPriorityColor = (priority) => {
const colors = {
low: '#6c757d',
normal: '#28a745',
high: '#ffc107',
urgent: '#dc3545'
};
return colors[priority] || '#6c757d';
};
const getTypeIcon = (type) => {
const icons = {
notification: '🔔',
announcement: '📢',
reminder: '⏰',
approval: '✅'
};
return icons[type] || '📧';
};
const formatDate = (dateString) => {
return new Date(dateString).toLocaleString('zh-CN');
};
useEffect(() => {
fetchMessages();
fetchUnreadCount();
}, []);
useEffect(() => {
fetchMessages(selectedType, selectedPriority);
}, [selectedType, selectedPriority]);
const handleMarkAsRead = (messageId) => {
markAsRead([messageId]);
};
const handleArchiveMessage = (messageId) => {
archiveMessages([messageId]);
};
const handleMarkAllAsRead = () => {
const unreadIds = messages.filter(msg => !msg.is_read).map(msg => msg.id);
if (unreadIds.length > 0) {
markAsRead(unreadIds);
}
};
return (
<div className="message-center">
<div className="message-header">
<h2>消息中心</h2>
<div className="header-actions">
<span className="unread-badge">
未读消息: {unreadCount}
</span>
<button
className="mark-all-read-btn"
onClick={handleMarkAllAsRead}
disabled={unreadCount === 0}
>
全部标记为已读
</button>
</div>
</div>
<div className="filters">
<div className="filter-group">
<label>消息类型:</label>
<select
value={selectedType}
onChange={(e) => setSelectedType(e.target.value)}
>
<option value="all">全部</option>
<option value="notification">通知</option>
<option value="announcement">公告</option>
<option value="reminder">提醒</option>
<option value="approval">审批</option>
</select>
</div>
<div className="filter-group">
<label>优先级:</label>
<select
value={selectedPriority}
onChange={(e) => setSelectedPriority(e.target.value)}
>
<option value="all">全部</option>
<option value="low">低</option>
<option value="normal">普通</option>
<option value="high">高</option>
<option value="urgent">紧急</option>
</select>
</div>
</div>
{loading ? (
<div className="loading">加载中...</div>
) : (
<div className="message-list">
{messages.length === 0 ? (
<div className="empty-state">
<p>暂无消息</p>
</div>
) : (
messages.map(message => (
<div
key={message.id}
className={`message-item ${!message.is_read ? 'unread' : ''}`}
>
<div className="message-main">
<div className="message-header-info">
<div className="message-meta">
<span className="message-icon">
{getTypeIcon(message.type)}
</span>
<h3 className="message-title">{message.title}</h3>
<span
className="priority-badge"
style={{ backgroundColor: getPriorityColor(message.priority) }}
>
{message.priority}
</span>
{!message.is_read && <span className="unread-indicator">未读</span>}
</div>
<div className="message-info">
<span className="sender">
发送者: {message.sender?.real_name || '系统'}
</span>
<span className="time">
{formatDate(message.created_at)}
</span>
</div>
</div>
<div className="message-content">
<p>{message.content}</p>
</div>
{message.expires_at && (
<div className="message-expiry">
过期时间: {formatDate(message.expires_at)}
</div>
)}
</div>
<div className="message-actions">
{!message.is_read && (
<button
className="action-btn mark-read-btn"
onClick={() => handleMarkAsRead(message.id)}
>
标记为已读
</button>
)}
<button
className="action-btn archive-btn"
onClick={() => handleArchiveMessage(message.id)}
>
归档
</button>
</div>
</div>
))
)}
</div>
)}
<style jsx>{`
.message-center {
max-width: 900px;
margin: 0 auto;
padding: 20px;
}
.message-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
padding-bottom: 15px;
border-bottom: 1px solid #e0e0e0;
}
.message-header h2 {
margin: 0;
color: #333;
}
.header-actions {
display: flex;
align-items: center;
gap: 15px;
}
.unread-badge {
background: #dc3545;
color: white;
padding: 4px 8px;
border-radius: 12px;
font-size: 12px;
font-weight: bold;
}
.mark-all-read-btn {
padding: 6px 12px;
background: #28a745;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 12px;
}
.mark-all-read-btn:disabled {
background: #6c757d;
cursor: not-allowed;
}
.filters {
display: flex;
gap: 20px;
margin-bottom: 20px;
padding: 15px;
background: #f8f9fa;
border-radius: 8px;
}
.filter-group {
display: flex;
align-items: center;
gap: 8px;
}
.filter-group label {
font-weight: bold;
color: #495057;
}
.filter-group select {
padding: 6px 12px;
border: 1px solid #ced4da;
border-radius: 4px;
background: white;
}
.message-list {
display: flex;
flex-direction: column;
gap: 15px;
}
.message-item {
border: 1px solid #e0e0e0;
border-radius: 8px;
padding: 20px;
background: white;
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
transition: all 0.2s ease;
}
.message-item.unread {
border-left: 4px solid #007bff;
background: #f8f9ff;
}
.message-item:hover {
box-shadow: 0 2px 8px rgba(0,0,0,0.15);
}
.message-header-info {
margin-bottom: 12px;
}
.message-meta {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 8px;
}
.message-icon {
font-size: 18px;
}
.message-title {
margin: 0;
color: #333;
font-size: 16px;
flex: 1;
}
.priority-badge {
padding: 2px 8px;
border-radius: 12px;
font-size: 11px;
font-weight: bold;
color: white;
}
.unread-indicator {
background: #dc3545;
color: white;
padding: 2px 8px;
border-radius: 12px;
font-size: 11px;
font-weight: bold;
}
.message-info {
display: flex;
gap: 15px;
font-size: 12px;
color: #6c757d;
}
.message-content {
margin-bottom: 12px;
line-height: 1.5;
color: #495057;
}
.message-content p {
margin: 0;
}
.message-expiry {
font-size: 11px;
color: #6c757d;
margin-bottom: 12px;
}
.message-actions {
display: flex;
gap: 10px;
justify-content: flex-end;
}
.action-btn {
padding: 6px 12px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 12px;
transition: background-color 0.2s;
}
.mark-read-btn {
background: #28a745;
color: white;
}
.mark-read-btn:hover {
background: #218838;
}
.archive-btn {
background: #6c757d;
color: white;
}
.archive-btn:hover {
background: #5a6268;
}
.empty-state {
text-align: center;
padding: 40px;
color: #6c757d;
}
.loading {
text-align: center;
padding: 40px;
color: #6c757d;
}
`}</style>
</div>
);
};
export default MessageCenter;1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
Python 消息管理脚本
python
import requests
import json
from datetime import datetime
from typing import List, Optional, Dict, Any
class MessageManager:
def __init__(self, base_url: str, admin_token: str, user_token: str):
self.base_url = base_url
self.admin_token = admin_token
self.user_token = user_token
self.admin_headers = {
'Authorization': f'Bearer {admin_token}',
'Content-Type': 'application/json'
}
self.user_headers = {
'Authorization': f'Bearer {user_token}',
'Content-Type': 'application/json'
}
def create_message(self, title: str, content: str, message_type: str,
priority: str, recipient_ids: Optional[List[int]] = None,
is_global: bool = False, expires_at: Optional[str] = None,
published_at: Optional[str] = None) -> Dict[str, Any]:
"""创建消息"""
data = {
'title': title,
'content': content,
'type': message_type,
'priority': priority,
'is_global': is_global
}
if recipient_ids:
data['recipient_ids'] = recipient_ids
if expires_at:
data['expires_at'] = expires_at
if published_at:
data['published_at'] = published_at
response = requests.post(
f'{self.base_url}/api/v1/admin/messages',
json=data,
headers=self.admin_headers
)
response.raise_for_status()
return response.json()
def send_global_message(self, title: str, content: str, message_type: str,
priority: str, expires_at: Optional[str] = None,
published_at: Optional[str] = None) -> Dict[str, Any]:
"""发送全局消息"""
data = {
'title': title,
'content': content,
'type': message_type,
'priority': priority
}
if expires_at:
data['expires_at'] = expires_at
if published_at:
data['published_at'] = published_at
response = requests.post(
f'{self.base_url}/api/v1/admin/messages/global',
json=data,
headers=self.admin_headers
)
response.raise_for_status()
return response.json()
def get_user_messages(self, message_type: Optional[str] = None,
priority: Optional[str] = None,
is_read: Optional[bool] = None,
search: Optional[str] = None,
page: int = 1, page_size: int = 20) -> Dict[str, Any]:
"""获取用户消息列表"""
params = {
'page': page,
'page_size': page_size
}
if message_type:
params['type'] = message_type
if priority:
params['priority'] = priority
if is_read is not None:
params['is_read'] = is_read
if search:
params['search'] = search
response = requests.get(
f'{self.base_url}/api/v1/messages',
params=params,
headers=self.user_headers
)
response.raise_for_status()
return response.json()
def get_admin_messages(self, message_type: Optional[str] = None,
priority: Optional[str] = None,
sender_id: Optional[int] = None,
is_active: Optional[bool] = None,
is_global: Optional[bool] = None,
search: Optional[str] = None,
date_from: Optional[str] = None,
date_to: Optional[str] = None,
page: int = 1, page_size: int = 20) -> Dict[str, Any]:
"""获取管理员消息列表"""
params = {
'page': page,
'page_size': page_size
}
if message_type:
params['type'] = message_type
if priority:
params['priority'] = priority
if sender_id:
params['sender_id'] = sender_id
if is_active is not None:
params['is_active'] = is_active
if is_global is not None:
params['is_global'] = is_global
if search:
params['search'] = search
if date_from:
params['date_from'] = date_from
if date_to:
params['date_to'] = date_to
response = requests.get(
f'{self.base_url}/api/v1/admin/messages',
params=params,
headers=self.admin_headers
)
response.raise_for_status()
return response.json()
def mark_as_read(self, message_ids: List[int]) -> Dict[str, Any]:
"""标记消息为已读"""
response = requests.post(
f'{self.base_url}/api/v1/messages/read',
json={'message_ids': message_ids},
headers=self.user_headers
)
response.raise_for_status()
return response.json()
def archive_messages(self, message_ids: List[int]) -> Dict[str, Any]:
"""归档消息"""
response = requests.post(
f'{self.base_url}/api/v1/messages/archive',
json={'message_ids': message_ids},
headers=self.user_headers
)
response.raise_for_status()
return response.json()
def get_unread_count(self) -> Dict[str, Any]:
"""获取未读消息数量"""
response = requests.get(
f'{self.base_url}/api/v1/messages/unread-count',
headers=self.user_headers
)
response.raise_for_status()
return response.json()
def get_user_stats(self) -> Dict[str, Any]:
"""获取用户消息统计"""
response = requests.get(
f'{self.base_url}/api/v1/messages/stats',
headers=self.user_headers
)
response.raise_for_status()
return response.json()
def add_recipients(self, message_id: int, recipient_ids: List[int]) -> Dict[str, Any]:
"""添加消息接收者"""
response = requests.post(
f'{self.base_url}/api/v1/admin/messages/{message_id}/recipients',
json={'recipient_ids': recipient_ids},
headers=self.admin_headers
)
response.raise_for_status()
return response.json()
def update_message(self, message_id: int, **kwargs) -> Dict[str, Any]:
"""更新消息"""
response = requests.put(
f'{self.base_url}/api/v1/admin/messages/{message_id}',
json=kwargs,
headers=self.admin_headers
)
response.raise_for_status()
return response.json()
def delete_message(self, message_id: int) -> Dict[str, Any]:
"""删除消息"""
response = requests.delete(
f'{self.base_url}/api/v1/admin/messages/{message_id}',
headers=self.admin_headers
)
response.raise_for_status()
return response.json()
def generate_message_report(self, date_from: Optional[str] = None,
date_to: Optional[str] = None) -> str:
"""生成消息报告"""
messages_data = self.get_admin_messages(
date_from=date_from,
date_to=date_to,
page_size=1000
)
stats_data = self.get_admin_messages(page_size=1)
total_messages = len(messages_data['data']['messages'])
# 统计各类消息数量
type_counts = {}
priority_counts = {}
for message in messages_data['data']['messages']:
msg_type = message['type']
priority = message['priority']
type_counts[msg_type] = type_counts.get(msg_type, 0) + 1
priority_counts[priority] = priority_counts.get(priority, 0) + 1
report = f"""
# 消息系统统计报告
## 基本统计
- 总消息数: {total_messages}
- 统计时间范围: {date_from or '开始'} 至 {date_to or '现在'}
## 消息类型分布
"""
for msg_type, count in type_counts.items():
report += f"- {msg_type}: {count} 条 ({count/total_messages*100:.1f}%)\n"
report += "\n## 优先级分布\n"
for priority, count in priority_counts.items():
report += f"- {priority}: {count} 条 ({count/total_messages*100:.1f}%)\n"
return report
# 使用示例
def main():
# 初始化消息管理器
message_manager = MessageManager(
base_url='http://localhost:8080',
admin_token='your_admin_token',
user_token='your_user_token'
)
print("=== 消息管理示例 ===")
# 管理员操作
print("\n--- 管理员操作 ---")
# 创建消息
try:
result = message_manager.create_message(
title="系统维护通知",
content="系统将于今晚22:00进行维护,预计2小时。",
message_type="announcement",
priority="high",
is_global=True,
expires_at="2024-12-31"
)
print(f"创建消息成功: {result['message']}")
message_id = result['data']['id']
except Exception as e:
print(f"创建消息失败: {e}")
message_id = 1
# 发送全局消息
try:
result = message_manager.send_global_message(
title="春节放假通知",
content="根据国家规定,春节放假时间为2月10日至2月17日。",
message_type="announcement",
priority="high"
)
print(f"发送全局消息成功: {result['message']}")
except Exception as e:
print(f"发送全局消息失败: {e}")
# 获取管理员消息列表
try:
admin_messages = message_manager.get_admin_messages(
message_type="announcement",
priority="high"
)
print(f"高优先级公告消息数: {len(admin_messages['data']['messages'])}")
except Exception as e:
print(f"获取管理员消息列表失败: {e}")
# 用户操作
print("\n--- 用户操作 ---")
# 获取用户消息列表
try:
user_messages = message_manager.get_user_messages(is_read=False)
print(f"未读消息数: {len(user_messages['data']['messages'])}")
except Exception as e:
print(f"获取用户消息列表失败: {e}")
# 获取未读消息数量
try:
unread_data = message_manager.get_unread_count()
print(f"未读消息数量: {unread_data['data']['unread_count']}")
except Exception as e:
print(f"获取未读消息数量失败: {e}")
# 获取用户消息统计
try:
stats = message_manager.get_user_stats()
print("消息统计:")
print(f" 总消息数: {stats['data']['total_count']}")
print(f" 已读消息数: {stats['data']['read_count']}")
print(f" 未读消息数: {stats['data']['unread_count']}")
print(f" 归档消息数: {stats['data']['archived_count']}")
except Exception as e:
print(f"获取消息统计失败: {e}")
# 标记消息为已读
if user_messages['data']['messages']:
unread_ids = [msg['id'] for msg in user_messages['data']['messages']]
try:
result = message_manager.mark_as_read(unread_ids[:1]) # 标记第一条为已读
print(f"标记已读成功: {result['message']}")
except Exception as e:
print(f"标记已读失败: {e}")
# 生成消息报告
try:
report = message_manager.generate_message_report(
date_from="2024-01-01",
date_to="2024-12-31"
)
print(report)
except Exception as e:
print(f"生成报告失败: {e}")
if __name__ == '__main__':
main()1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352