待办消息发送与管理指南
什么是待办消息?
待办消息(Process Messages) 是一种特殊的消息类型,专门用于向用户发送需要处理的任务、审批事项或工作提醒。与普通消息不同,待办消息具有状态管理能力,可以跟踪任务的完成情况。
适用场景
- ✅ 审批流程:请假审批、报销审批、采购审批等
- ✅ 任务分配:工作安排、项目任务、待办事项等
- ✅ 工作提醒:会议通知、截止日期提醒、待处理工单等
- ✅ 协作流程:文件审阅、代码审查、合同审核等
待办消息的特点
| 特性 | 普通消息 | 待办消息 |
|---|---|---|
| 状态管理 | ❌ 无状态 | ✅ 有待办/已处理状态 |
| 操作追踪 | ❌ 无法追踪 | ✅ 可追踪处理情况 |
| 跳转链接 | ⚠️ 可选 | ✅ 必选,直达处理页面 |
| 批量发送 | ⚠️ 支持 | ✅ 原生支持群发 |
| 消息 ID | ⚠️ 仅返回 | ✅ 必需,用于状态更新 |
效果展示

快速开始
5 分钟上手示例
发送单个待办消息
// 发送待办消息给个人
async function sendProcessMessage() {
const response = await fetch(
"https://open.qingtui.cn/message/send/process/single",
{
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: "Bearer YOUR_ACCESS_TOKEN",
},
body: JSON.stringify({
to_user: "user_openid_123",
message: {
title: "请假审批申请",
body: "张三申请年假 3 天\n时间:2024-01-15 至 2024-01-17\n事由:家庭事务",
url: "https://yourapp.com/approve/12345",
},
}),
},
);
const result = await response.json();
if (result.code === 0) {
console.log("发送成功,消息 ID:", result.data.msg_id);
// 保存 msg_id 用于后续状态更新
return result.data.msg_id;
} else {
console.error("发送失败:", result.message);
throw new Error(result.message);
}
}
// 标记为已处理
async function markAsComplete(msgId, userId) {
const response = await fetch(
"https://open.qingtui.cn/message/update/process/complete",
{
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: "Bearer YOUR_ACCESS_TOKEN",
},
body: JSON.stringify({
msg_id: msgId,
open_id: userId,
}),
},
);
const result = await response.json();
if (result.code === 0) {
console.log("已标记为已处理");
} else {
console.error("更新失败:", result.message);
}
}
// 使用示例
const msgId = await sendProcessMessage();
// ... 用户处理完成后
await markAsComplete(msgId, "user_openid_123");
批量发送待办消息
// 发送给多个用户
async function sendToMultipleUsers() {
const users = ["user1_openid", "user2_openid", "user3_openid"];
const response = await fetch(
"https://open.qingtui.cn/message/send/process/mass",
{
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: "Bearer YOUR_ACCESS_TOKEN",
},
body: JSON.stringify({
to_users: users,
message: {
title: "紧急会议通知",
body: "时间:今天下午 3 点\n地点:第一会议室\n议题:Q1 项目评审",
url: "https://yourapp.com/meeting/detail/789",
},
}),
},
);
const result = await response.json();
if (result.code === 0) {
console.log("批量发送成功");
console.log("发送结果:", result.data.send_result);
// 保存每个用户的 msg_id
result.data.send_result.forEach((item) => {
console.log(`用户 ${item.open_id} 的消息 ID: ${item.msg_id}`);
});
}
return result.data.send_result;
}
接口详细说明
1. 发送待办消息给个人(单发)
向单个用户发送待办消息,适用于一对一的任务分配或审批通知。
接口信息
请求地址: POST https://open.qingtui.cn/message/send/process/single
请求参数:
| 参数名 | 类型 | 必须 | 说明 |
|---|---|---|---|
| to_user | string | 是 | 接收消息的用户 ID(OpenID) |
| message | object | 是 | 消息内容对象 |
| message.title | string | 是 | 待办标题(建议不超过 50 字) |
| message.body | string | 是 | 待办内容正文(支持换行,建议不超过 500 字) |
| message.url | string | 是 | 点击跳转的链接地址(必须是 HTTPS) |
请求示例:
{
"to_user": "user_openid_abc123",
"message": {
"title": "请假审批申请",
"body": "申请人:张三\n部门:技术部\n请假类型:年假\n请假时间:2024-01-15 至 2024-01-17(共 3 天)\n请假事由:家庭事务需要处理",
"url": "https://yourapp.com/approval/detail/12345"
}
}
响应示例(成功):
{
"code": 0,
"message": "success",
"data": {
"msg_id": "process_msg_xyz789"
}
}
响应示例(失败):
{
"code": 42403,
"message": "expired token",
"data": null
}
响应参数说明:
| 参数名 | 说明 | 用途 |
|---|---|---|
| code | 返回码 | 0 表示成功,其他表示失败 |
| message | 返回信息 | 成功时为 "success",失败时为错误描述 |
| data.msg_id | 消息 ID | 重要:用于后续标记为已处理 |
注意事项:
- ⚠️ 必须保存 msg_id:后续更新状态时需要使用
- ⚠️ URL 格式要求:必须是有效的 HTTPS 链接
- ⚠️ 用户权限:接收消息的用户必须已关注该轻应用
- ✅ 标题简洁:建议控制在 30 字以内,突出重点
- ✅ 正文格式化:使用换行符
\n提升可读性
2. 发送待办消息给多用户(群发)
一次性向多个用户发送相同的待办消息,适用于会议通知、公告等场景。
接口信息
请求地址: POST https://open.qingtui.cn/message/send/process/mass
请求参数:
| 参数名 | 类型 | 必须 | 说明 |
|---|---|---|---|
| to_users | array | 是 | 接收消息的用户 ID 列表(字符串数组) |
| message | object | 是 | 消息内容对象(结构同单发) |
| message.title | string | 是 | 待办标题 |
| message.body | string | 是 | 待办内容正文 |
| message.url | string | 是 | 点击跳转的链接地址 |
请求示例:
{
"to_users": [
"user_openid_001",
"user_openid_002",
"user_openid_003"
],
"message": {
"title": "Q1 项目评审会议",
"body": "时间:2024-01-20 14:00\n地点:第一会议室\n参会人员:产品部、技术部、设计部\n议程:\n1. Q1 工作总结\n2. Q2 计划讨论\n3. 资源分配方案",
"url": "https://yourapp.com/meeting/q1-review"
}
}
响应示例:
{
"code": 0,
"message": "success",
"data": {
"send_result": [
{
"open_id": "user_openid_001",
"msg_id": "msg_001_abc"
},
{
"open_id": "user_openid_002",
"msg_id": "msg_002_def"
},
{
"open_id": "user_openid_003",
"msg_id": "msg_003_gHi"
}
]
}
}
响应参数说明:
| 参数名 | 说明 |
|---|---|
| code | 返回码 |
| message | 返回信息 |
| data.send_result | 发送结果列表 |
| data.send_result[].open_id | 用户 ID |
| data.send_result[].msg_id | 该用户对应的消息 ID |
限制说明:
- ⚠️ 人数限制:单次最多发送给 100 个用户
- ⚠️ 部分失败处理:如果部分用户发送失败,会在返回结果中体现
- ✅ 建议:超过 100 人时分批发送
发送结果处理示例:
async function handleMassSendResult(result) {
if (result.code !== 0) {
console.error("群发失败:", result.message);
return;
}
const successList = [];
const failedList = [];
result.data.send_result.forEach((item) => {
if (item.msg_id) {
successList.push({
userId: item.open_id,
msgId: item.msg_id,
});
} else {
failedList.push(item.open_id);
}
});
console.log(`发送成功:${successList.length}人`);
console.log(`发送失败:${failedList.length}人`);
// 保存成功的记录到数据库
await saveToSendRecords(successList);
// 记录失败的用戶,稍后重试或通知管理员
if (failedList.length > 0) {
await logFailedUsers(failedList);
}
}
3. 修改待办消息状态为已处理
当用户完成待办事项后,调用此接口将消息标记为"已处理"状态。
接口信息
请求地址: POST https://open.qingtui.cn/message/update/process/complete
请求参数:
| 参数名 | 类型 | 必须 | 说明 |
|---|---|---|---|
| msg_id | string | 是 | 需要更新状态的消息 ID |
| open_id | string | 是 | 接收该消息的用户 ID |
请求示例:
{
"msg_id": "process_msg_xyz789",
"open_id": "user_openid_abc123"
}
响应示例(成功):
{
"code": 0,
"message": "success"
}
响应示例(失败):
{
"code": 40001,
"message": "message not found"
}
使用场景:
- ✅ 用户点击"同意"或"拒绝"按钮后
- ✅ 用户完成任务填写后
- ✅ 用户确认收到通知后
- ✅ 审批流程结束后
注意事项:
- ⚠️ 状态不可逆:一旦标记为已处理,无法恢复
- ⚠️ 权限验证:只能更新自己发送的消息
- ⚠️ 消息存在性:消息必须存在且有效
- ✅ 幂等性:重复调用不会产生副作用
- ✅ 及时更新:用户处理后应立即调用
完整工作流程
标准流程图
时序图
数据流图
实战案例
案例 1:审批流程系统
class ApprovalSystem {
constructor(accessToken) {
this.accessToken = accessToken;
this.baseUrl = "https://open.qingtui.cn";
}
/**
* 发送审批待办
*/
async sendApprovalTask(approverId, approvalData) {
const response = await fetch(
`${this.baseUrl}/message/send/process/single`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${this.accessToken}`,
},
body: JSON.stringify({
to_user: approverId,
message: {
title: `${approvalData.type}审批申请`,
body: this.formatApprovalBody(approvalData),
url: `${this.baseUrl}/approval/${approvalData.id}`,
},
}),
},
);
const result = await response.json();
if (result.code === 0) {
// 保存到数据库
await this.saveApprovalRecord({
approvalId: approvalData.id,
approverId: approverId,
msgId: result.data.msg_id,
status: "pending",
});
return result.data.msg_id;
}
throw new Error(result.message);
}
/**
* 格式化审批内容
*/
formatApprovalBody(data) {
const lines = [
`申请人:${data.applicantName}`,
`部门:${data.department}`,
`申请类型:${data.type}`,
`申请时间:${data.startDate} 至 ${data.endDate}`,
`共计 ${data.days} 天`,
`申请事由:${data.reason}`,
];
if (data.remarks) {
lines.push(`备注:${data.remarks}`);
}
return lines.join("\n");
}
/**
* 审批完成后更新状态
*/
async completeApproval(msgId, userId) {
const response = await fetch(
`${this.baseUrl}/message/update/process/complete`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${this.accessToken}`,
},
body: JSON.stringify({
msg_id: msgId,
open_id: userId,
}),
},
);
const result = await response.json();
if (result.code === 0) {
// 更新数据库状态
await this.updateApprovalStatus(msgId, "completed");
}
return result.code === 0;
}
/**
* 批量发送会议通知
*/
async sendMeetingNotification(participants, meetingData) {
const response = await fetch(`${this.baseUrl}/message/send/process/mass`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${this.accessToken}`,
},
body: JSON.stringify({
to_users: participants.map((p) => p.openId),
message: {
title: `会议通知:${meetingData.title}`,
body: this.formatMeetingBody(meetingData),
url: `${this.baseUrl}/meeting/${meetingData.id}`,
},
}),
});
const result = await response.json();
if (result.code === 0) {
// 保存参会人员的 msg_id
await this.saveMeetingRecords(result.data.send_result, meetingData.id);
}
return result;
}
formatMeetingBody(data) {
const lines = [
`会议主题:${data.title}`,
`会议时间:${data.time}`,
`会议地点:${data.location}`,
`主持人:${data.host}`,
`议程:`,
data.agenda.map((item, i) => `${i + 1}. ${item}`).join("\n"),
];
return lines.join("\n");
}
}
// 使用示例
const approvalSystem = new ApprovalSystem("access_token");
// 发送审批待办
const msgId = await approvalSystem.sendApprovalTask("manager_openid", {
id: "APP20240115001",
type: "请假",
applicantName: "张三",
department: "技术部",
startDate: "2024-01-15",
endDate: "2024-01-17",
days: 3,
reason: "家庭事务需要处理",
remarks: "已安排工作交接",
});
// 审批完成后
await approvalSystem.completeApproval(msgId, "manager_openid");
案例 2:任务管理系统
class TaskManager {
constructor(accessToken) {
this.accessToken = accessToken;
}
/**
* 分配任务
*/
async assignTask(assigneeId, taskData) {
const urgencyLevel = {
1: "【低优先级】",
2: "【中优先级】",
3: "【高优先级】",
4: "【紧急】",
};
const response = await fetch(
"https://open.qingtui.cn/message/send/process/single",
{
method: "POST",
headers: {
Authorization: `Bearer ${this.accessToken}`,
},
body: JSON.stringify({
to_user: assigneeId,
message: {
title: `${urgencyLevel[taskData.priority] || ""}新任务分配`,
body: [
`任务名称:${taskData.name}`,
`任务描述:${taskData.description}`,
`截止时间:${taskData.deadline}`,
`项目负责人:${taskData.owner}`,
`附件:${taskData.attachments?.length || 0}个文件`,
].join("\n"),
url: `https://task.yourapp.com/detail/${taskData.id}`,
},
}),
},
);
return await response.json();
}
/**
* 项目上线通知(群发)
*/
async notifyProjectLaunch(teamMembers, projectData) {
const response = await fetch(
"https://open.qingtui.cn/message/send/process/mass",
{
method: "POST",
headers: {
Authorization: `Bearer ${this.accessToken}`,
},
body: JSON.stringify({
to_users: teamMembers,
message: {
title: "🎉 项目上线通知",
body: [
`项目名称:${projectData.name}`,
`上线时间:${projectData.launchTime}`,
`版本号:v${projectData.version}`,
"",
"主要更新:",
...projectData.changelog.map((item) => `• ${item}`),
].join("\n"),
url: `https://project.yourapp.com/${projectData.id}`,
},
}),
},
);
return await response.json();
}
/**
* 任务完成确认
*/
async confirmTaskCompletion(msgId, userId, completionData) {
// 1. 标记待办为已完成
await fetch("https://open.qingtui.cn/message/update/process/complete", {
method: "POST",
headers: {
Authorization: `Bearer ${this.accessToken}`,
},
body: JSON.stringify({
msg_id: msgId,
open_id: userId,
}),
});
// 2. 记录完成日志
await this.logTaskCompletion(completionData);
// 3. 通知项目负责人
await this.notifyProjectOwner(completionData);
}
}
案例 3:工单系统
class TicketSystem {
/**
* 创建工单并发送待办
*/
async createTicket(ticketData, assigneeId) {
// 创建工单
const ticket = await this.createTicketInDB(ticketData);
// 发送待办消息
const result = await fetch(
"https://open.qingtui.cn/message/send/process/single",
{
method: "POST",
headers: {
Authorization: `Bearer ${this.accessToken}`,
},
body: JSON.stringify({
to_user: assigneeId,
message: {
title: `【${ticketData.priority}】${ticketData.subject}`,
body: [
`工单编号:${ticket.id}`,
`提交人:${ticketData.submitter}`,
`提交时间:${new Date().toLocaleString()}`,
`问题类型:${ticketData.category}`,
"",
"问题描述:",
ticketData.description,
].join("\n"),
url: `https://support.yourapp.com/ticket/${ticket.id}`,
},
}),
},
);
// 关联 msg_id
await this.associateMsgId(ticket.id, result.data.msg_id);
return ticket;
}
/**
* 工单处理完成
*/
async resolveTicket(ticketId, resolverId) {
const record = await this.getMsgIdByTicket(ticketId);
// 标记待办完成
await fetch("https://open.qingtui.cn/message/update/process/complete", {
method: "POST",
headers: {
Authorization: `Bearer ${this.accessToken}`,
},
body: JSON.stringify({
msg_id: record.msg_id,
open_id: resolverId,
}),
});
// 更新工单状态
await this.updateTicketStatus(ticketId, "resolved");
// 通知提交人
await this.notifySubmitter(ticketId);
}
}
最佳实践
1. 消息内容规范
// ✅ 推荐:结构清晰,重点突出
{
title: "【高优先级】采购审批申请",
body: `申请人:李四
部门:采购部
采购物品:办公设备一批
预算金额:¥50,000
申请日期:2024-01-15
期望到货日期:2024-01-25
采购清单:
1. 笔记本电脑 x 5
2. 显示器 x 5
3. 办公桌椅 x 10
申请事由:新员工入职设备需求`
}
// ❌ 不推荐:内容冗长,重点不清晰
{
title: "审批",
body: "有个采购需要审批一下,东西挺多的,麻烦尽快处理..."
}
2. 错误处理与重试
class MessageSender {
async sendWithRetry(params, maxRetries = 3) {
for (let i = 0; i < maxRetries; i++) {
try {
const result = await this.sendMessage(params);
if (result.code === 0) {
return result;
}
// 根据错误码决定是否重试
if (!this.isRetryableError(result.code)) {
throw new Error(result.message);
}
// 指数退避
const delay = Math.pow(2, i) * 1000;
await this.sleep(delay);
} catch (error) {
if (i === maxRetries - 1) {
// 最后一次重试失败
await this.logError(params, error);
throw error;
}
}
}
}
isRetryableError(code) {
const retryableCodes = [50001, 50002, 408, 429]; // 服务器错误、超时、限流
return retryableCodes.includes(code);
}
sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
}
3. 数据库设计建议
-- 待办消息记录表
CREATE TABLE process_messages (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
msg_id VARCHAR(64) NOT NULL UNIQUE COMMENT '轻推消息 ID',
business_id VARCHAR(64) NOT NULL COMMENT '业务 ID(审批 ID/任务 ID 等)',
business_type VARCHAR(32) NOT NULL COMMENT '业务类型',
receiver_open_id VARCHAR(64) NOT NULL COMMENT '接收者 ID',
sender_account_id VARCHAR(64) NOT NULL COMMENT '发送者 ID',
title VARCHAR(200) NOT NULL COMMENT '消息标题',
content TEXT NOT NULL COMMENT '消息内容',
target_url VARCHAR(500) NOT NULL COMMENT '跳转链接',
status TINYINT NOT NULL DEFAULT 0 COMMENT '状态:0-待处理 1-已处理 2-已取消',
sent_at DATETIME NOT NULL COMMENT '发送时间',
completed_at DATETIME NULL COMMENT '完成时间',
completed_by VARCHAR(64) NULL COMMENT '完成操作人',
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_receiver (receiver_open_id),
INDEX idx_business (business_type, business_id),
INDEX idx_status (status),
INDEX idx_sent_at (sent_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='待办消息记录表';
4. 监控与统计
class MessageMonitor {
/**
* 记录发送指标
*/
async recordMetrics(event, data) {
await metricsClient.increment(`process_message.${event}`, {
business_type: data.businessType,
success: data.success,
error_code: data.errorCode,
});
}
/**
* 生成日报
*/
async generateDailyReport(date) {
const stats = await db.query(
`
SELECT
business_type,
COUNT(*) as total_count,
SUM(CASE WHEN status = 1 THEN 1 ELSE 0 END) as completed_count,
AVG(TIMESTAMPDIFF(SECOND, sent_at, completed_at)) as avg_complete_time
FROM process_messages
WHERE DATE(sent_at) = ?
GROUP BY business_type
`,
[date],
);
return stats;
}
/**
* 告警检查
*/
async checkAlerts() {
// 检查长时间未处理的待办
const overdueTasks = await db.query(`
SELECT COUNT(*) as count
FROM process_messages
WHERE status = 0
AND sent_at < DATE_SUB(NOW(), INTERVAL 24 HOUR)
`);
if (overdueTasks[0].count > 100) {
await alertService.send("大量待办任务未处理", {
count: overdueTasks[0].count,
level: "warning",
});
}
// 检查发送失败率
const failureRate = await this.calculateFailureRate();
if (failureRate > 0.05) {
// 失败率超过 5%
await alertService.send("待办消息发送失败率过高", {
rate: failureRate,
level: "critical",
});
}
}
}
常见问题 FAQ
Q: msg_id 有什么作用?
A: msg_id 是待办消息的唯一标识,用于后续将消息标记为"已处理"状态。必须在发送成功后妥善保存。
Q: 群发时部分用户发送失败怎么办?
A: 检查返回的 send_result 数组,每个用户的发送结果独立。对失败的用户可以记录日志,稍后重试或采用其他通知方式。
Q: 已处理的状态还能修改吗?
A: 不能。一旦标记为已处理,状态不可逆转。如果需要重新发起待办,应该创建新的消息。
Q: 待办消息有数量限制吗?
A: 单个用户接收待办没有明确限制,但建议合理控制发送频率,避免骚扰用户。群发单次最多 100 人。
Q: URL 有什么要求?
A: 必须使用 HTTPS 协议,确保链接有效可访问。建议使用短链接或固定格式的业务 URL。
Q: 如何处理用户拒收的情况?
A: 如果用户取消关注轻应用,会发送失败。应该检查返回的错误码,更新用户状态,停止向该用户发送消息。
Q: 待办消息会过期吗?
A: 消息本身不会自动过期,但建议在业务层面设置合理的处理时限,并在消息内容中明确标注截止时间。
Q: 能否撤回已发送的待办?
A: 当前不支持撤回功能。如果需要取消待办,可以在业务系统中标记为"已取消"状态,并通知用户。
Q: 如何保证消息不丢失?
A: 建议:
- 实现本地持久化,发送前保存到数据库
- 发送失败时自动重试
- 建立消息补偿机制,定期扫描未发送成功的记录
Q: 性能如何优化?
A: 建议:
- 批量发送时使用 mass 接口而非循环调用 single
- 实现连接池和请求队列
- 异步处理非关键逻辑
- 设置合理的超时和重试策略
附录
错误码参考
| 错误码 | 说明 | 解决方案 |
|---|---|---|
| 0 | 成功 | - |
| 40001 | 消息不存在 | 检查 msg_id 是否正确 |
| 40002 | 参数错误 | 检查请求参数格式 |
| 40003 | 无权操作 | 验证发送者权限 |
| 42403 | token 过期 | 刷新 access_token |
| 42404 | 用户不存在 | 检查用户 open_id |
| 50001 | 服务器内部错误 | 稍后重试 |
| 50002 | 服务暂时不可用 | 稍后重试 |
相关文档
技术支持
如有问题,请联系:
- 开发者社区:https://open.qingtui.cn/forum
- 技术邮箱:dev@qingtui.cn