企业微信审批回调事件的幂等消费与状态一致性保障
回调事件重复投递问题
企业微信在审批状态变更(如"同意"、"驳回")时,会向配置的回调 URL 发送 HTTP POST 通知。由于网络超时或响应延迟,同一事件可能被多次投递 。若业务系统未做幂等处理,将导致重复审批、重复打款等严重后果。必须通过唯一事件 ID + 状态机校验确保一次且仅一次处理。
事件结构与唯一标识提取
企业微信审批回调体包含 SpNo(审批单号)和 ApprovalNodes,但关键幂等依据是整个事件内容的摘要 或企业微信提供的 MsgId(如有) 。若无 MsgId,则使用 SpNo + timestamp + 审批节点 hash 构造唯一键:
java
package wlkankan.cn.event;
import com.fasterxml.jackson.databind.JsonNode;
import wlkankan.cn.crypto.Sha256Util;
public class ApprovalEvent {
private final String spNo; // 审批单号
private final long eventTime; // 事件时间戳(来自审批记录)
private final JsonNode rawPayload; // 原始 JSON
private final String eventId; // 幂等ID
public ApprovalEvent(JsonNode payload) {
this.rawPayload = payload;
this.spNo = payload.get("SpNo").asText();
this.eventTime = payload.get("StatuChangeEvent").get("TimeStamp").asLong();
// 使用完整 payload 计算哈希,避免 SpNo 相同但状态不同的事件被误判
this.eventId = Sha256Util.sha256Base64(payload.toString());
}
// getters...
}

幂等性检查与状态机校验
使用数据库唯一索引防止重复插入,并结合审批单当前状态判断是否可执行:
java
package wlkankan.cn.service;
import wlkankan.cn.event.ApprovalEvent;
import wlkankan.cn.repo.ApprovalRecordRepository;
import wlkankan.cn.repo.IdempotentEventRepository;
public class ApprovalEventHandler {
private final IdempotentEventRepository eventRepo;
private final ApprovalRecordRepository recordRepo;
public void handle(ApprovalEvent event) {
// 1. 幂等检查:尝试插入事件ID
if (!eventRepo.insertIfAbsent(event.getEventId(), event.getSpNo())) {
return; // 已处理过
}
// 2. 获取当前审批单状态
ApprovalRecord record = recordRepo.findBySpNo(event.getSpNo());
if (record == null) {
throw new IllegalArgumentException("Unknown approval spNo: " + event.getSpNo());
}
// 3. 状态机校验:仅允许合法状态转移
String newStatus = extractNewStatus(event);
if (!isValidTransition(record.getStatus(), newStatus)) {
// 例如:已通过的单子再次收到"同意"事件,直接忽略
return;
}
// 4. 执行业务逻辑(如更新财务状态、发送通知)
executeBusinessLogic(record, newStatus);
// 5. 更新审批单状态(含版本号乐观锁)
recordRepo.updateStatusWithVersion(record.getId(), newStatus, record.getVersion());
}
private boolean isValidTransition(String current, String target) {
if ("PENDING".equals(current) && ("APPROVED".equals(target) || "REJECTED".equals(target))) {
return true;
}
if ("APPROVED".equals(current) && "COMPLETED".equals(target)) {
return true;
}
return false; // 非法转移,如 REJECTED → APPROVED
}
private String extractNewStatus(ApprovalEvent event) {
// 解析 event.getRawPayload() 中的审批结果
return event.getRawPayload().get("StatuChangeEvent").get("Status").asText();
}
}
数据库表设计
sql
-- 幂等事件表(唯一约束)
CREATE TABLE idempotent_approval_event (
event_id VARCHAR(256) PRIMARY KEY,
sp_no VARCHAR(64) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- 审批单主表(含版本号)
CREATE TABLE approval_record (
id BIGINT PRIMARY KEY,
sp_no VARCHAR(64) UNIQUE NOT NULL,
status VARCHAR(32) NOT NULL,
version INT NOT NULL DEFAULT 0
);
IdempotentEventRepository.insertIfAbsent() 实现:
java
package wlkankan.cn.repo;
import org.springframework.dao.DuplicateKeyException;
public class IdempotentEventRepository {
public boolean insertIfAbsent(String eventId, String spNo) {
try {
// 使用 INSERT IGNORE 或 ON CONFLICT DO NOTHING
jdbcTemplate.update(
"INSERT INTO idempotent_approval_event (event_id, sp_no) VALUES (?, ?)",
eventId, spNo
);
return true;
} catch (DuplicateKeyException e) {
return false;
}
}
}
异常重试与补偿机制
若 executeBusinessLogic() 失败(如调用下游支付接口超时),需支持重试。但重试时仍受幂等表保护,不会重复执行业务:
java
// 在消息队列消费者中
public void onMessage(String jsonPayload) {
ApprovalEvent event = new ApprovalEvent(objectMapper.readTree(jsonPayload));
try {
approvalEventHandler.handle(event);
} catch (Exception e) {
// 记录失败,由定时任务或 DLQ 重试
failedEventQueue.enqueue(event, e.getMessage());
}
}
重试时,因 event_id 已存在,handle() 直接返回,避免副作用。
企业微信回调验证与安全
接收回调前需验证签名,防止伪造请求:
java
@PostMapping("/wechat/approval/callback")
public ResponseEntity<String> handleCallback(
@RequestHeader("Msg-Signature") String signature,
@RequestBody String body) {
if (!wlkankan.cn.security.WechatSignature.verify(signature, body, nonce, timestamp)) {
return ResponseEntity.status(403).build();
}
ApprovalEvent event = parseEvent(body);
approvalEventHandler.handle(event);
return ResponseEntity.ok("success");
}
通过唯一事件 ID + 数据库幂等表 + 状态机校验 + 乐观锁更新四重保障,企业微信审批回调可实现强一致性消费,杜绝重复处理与状态错乱。