跳到主要内容

待办消息发送与管理指南

什么是待办消息?

待办消息(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_userstring接收消息的用户 ID(OpenID)
messageobject消息内容对象
message.titlestring待办标题(建议不超过 50 字)
message.bodystring待办内容正文(支持换行,建议不超过 500 字)
message.urlstring点击跳转的链接地址(必须是 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_usersarray接收消息的用户 ID 列表(字符串数组)
messageobject消息内容对象(结构同单发)
message.titlestring待办标题
message.bodystring待办内容正文
message.urlstring点击跳转的链接地址

请求示例:

{
"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_idstring需要更新状态的消息 ID
open_idstring接收该消息的用户 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: 建议:

  1. 实现本地持久化,发送前保存到数据库
  2. 发送失败时自动重试
  3. 建立消息补偿机制,定期扫描未发送成功的记录

Q: 性能如何优化?
A: 建议:

  1. 批量发送时使用 mass 接口而非循环调用 single
  2. 实现连接池和请求队列
  3. 异步处理非关键逻辑
  4. 设置合理的超时和重试策略

附录

错误码参考

错误码说明解决方案
0成功-
40001消息不存在检查 msg_id 是否正确
40002参数错误检查请求参数格式
40003无权操作验证发送者权限
42403token 过期刷新 access_token
42404用户不存在检查用户 open_id
50001服务器内部错误稍后重试
50002服务暂时不可用稍后重试

相关文档

技术支持

如有问题,请联系: