企业微信审批回调事件的幂等接收与业务解耦设计
企业微信审批流程支持配置回调URL,当审批状态变更(如提交、通过、驳回)时,会向该地址推送事件。由于网络不稳定或重试机制,同一事件可能被多次发送。若直接在Controller中处理业务逻辑,易导致重复执行(如重复打款、重复通知)。本文基于消息去重、异步消费与策略路由,实现高可靠、低耦合的审批事件处理架构。
1. 审批事件结构与唯一标识提取
企业微信审批回调体包含SpNo(审批单号)作为全局唯一ID:
java
package wlkankan.cn.wecom.approval.event;
import com.fasterxml.jackson.annotation.JsonProperty;
public class ApprovalEvent {
@JsonProperty("SpNo")
private String spNo; // 审批单号,唯一
@JsonProperty("OpenSpName")
private String openSpName; // 模板名称
@JsonProperty("ApprovalNodes")
private ApprovalNode[] approvalNodes;
@JsonProperty("Statue") // 注意:官方拼写为 Statue 而非 Status
private Integer statue; // 1-审批中,2-已通过,3-已驳回,4-已撤销
// getters...
}

2. 幂等拦截器:基于Redis的去重
使用Redis原子操作确保事件仅处理一次:
java
package wlkankan.cn.wecom.approval.idempotent;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import java.util.concurrent.TimeUnit;
@Component
public class IdempotentChecker {
private final StringRedisTemplate redisTemplate;
private static final String KEY_PREFIX = "wecom:approval:event:";
private static final long EXPIRE_HOURS = 24;
public IdempotentChecker(StringRedisTemplate redisTemplate) {
this.redisTemplate = redisTemplate;
}
public boolean isDuplicate(String spNo) {
String key = KEY_PREFIX + spNo;
Boolean isNew = redisTemplate.opsForValue().setIfAbsent(key, "1", EXPIRE_HOURS, TimeUnit.HOURS);
return Boolean.FALSE.equals(isNew); // false 表示已存在,是重复
}
}
3. 事件接收Controller:轻量入口
仅做验签、解析与投递,不包含业务逻辑:
java
package wlkankan.cn.wecom.approval.web;
import wlkankan.cn.wecom.approval.event.ApprovalEvent;
import wlkankan.cn.wecom.approval.service.ApprovalEventDispatcher;
import wlkankan.cn.wecom.approval.idempotent.IdempotentChecker;
import org.springframework.web.bind.annotation.*;
import javax.servlet.http.HttpServletRequest;
import java.io.BufferedReader;
@RestController
@RequestMapping("/callback/wecom")
public class ApprovalCallbackController {
private final IdempotentChecker idempotentChecker;
private final ApprovalEventDispatcher dispatcher;
public ApprovalCallbackController(IdempotentChecker idempotentChecker,
ApprovalEventDispatcher dispatcher) {
this.idempotentChecker = idempotentChecker;
this.dispatcher = dispatcher;
}
@PostMapping("/approval")
public String handleApprovalEvent(HttpServletRequest request) throws Exception {
String body = readBody(request);
// 1. 验证企业微信签名(略)
verifySignature(request, body);
// 2. 反序列化
ApprovalEvent event = parseEvent(body);
// 3. 幂等检查
if (idempotentChecker.isDuplicate(event.getSpNo())) {
return "success"; // 已处理,直接返回成功
}
// 4. 异步分发
dispatcher.dispatchAsync(event);
return "success";
}
private String readBody(HttpServletRequest request) throws Exception {
StringBuilder sb = new StringBuilder();
try (BufferedReader reader = request.getReader()) {
String line;
while ((line = reader.readLine()) != null) {
sb.append(line);
}
}
return sb.toString();
}
// 验签与解析方法略
}
4. 事件分发器:策略模式路由
根据审批模板名路由到不同处理器:
java
package wlkankan.cn.wecom.approval.service;
import wlkankan.cn.wecom.approval.event.ApprovalEvent;
import wlkankan.cn.wecom.approval.handler.ApprovalHandler;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.stereotype.Service;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
@Service
public class ApprovalEventDispatcher implements InitializingBean {
private final Map<String, ApprovalHandler> handlerMap; // 模板名 -> 处理器
private final ApprovalEventExecutor executor;
public ApprovalEventDispatcher(Map<String, ApprovalHandler> handlerMap,
ApprovalEventExecutor executor) {
this.handlerMap = handlerMap;
this.executor = executor;
}
public void dispatchAsync(ApprovalEvent event) {
CompletableFuture.runAsync(() -> {
String templateName = event.getOpenSpName();
ApprovalHandler handler = handlerMap.get(templateName);
if (handler == null) {
throw new IllegalArgumentException("No handler for template: " + templateName);
}
handler.handle(event);
}, executor.getExecutor());
}
@Override
public void afterPropertiesSet() {
// 启动时校验所有模板是否有对应处理器
// 可结合配置中心动态加载
}
}
5. 业务处理器实现示例
以"差旅报销"模板为例:
java
package wlkankan.cn.wecom.approval.handler.impl;
import wlkankan.cn.wecom.approval.event.ApprovalEvent;
import wlkankan.cn.wecom.approval.handler.ApprovalHandler;
import wlkankan.cn.wecom.finance.service.ReimbursementService;
import org.springframework.stereotype.Component;
@Component("差旅报销申请")
public class TravelReimbursementHandler implements ApprovalHandler {
private final ReimbursementService reimbursementService;
public TravelReimbursementHandler(ReimbursementService reimbursementService) {
this.reimbursementService = reimbursementService;
}
@Override
public void handle(ApprovalEvent event) {
switch (event.getStatue()) {
case 2: // 通过
reimbursementService.approve(event.getSpNo());
break;
case 3: // 驳回
reimbursementService.reject(event.getSpNo());
break;
default:
// 忽略其他状态
}
}
}
6. 异步执行器与异常隔离
避免一个失败影响整体:
java
package wlkankan.cn.wecom.approval.service;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.stereotype.Component;
@Component
public class ApprovalEventExecutor {
private final ThreadPoolTaskExecutor executor;
public ApprovalEventExecutor() {
ThreadPoolTaskExecutor exec = new ThreadPoolTaskExecutor();
exec.setCorePoolSize(4);
exec.setMaxPoolSize(16);
exec.setQueueCapacity(100);
exec.setThreadNamePrefix("approval-event-");
exec.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
exec.initialize();
this.executor = exec;
}
public ThreadPoolTaskExecutor getExecutor() {
return executor;
}
}
7. 监控与重试机制(可选)
记录处理日志并支持人工重放:
java
// 在 handler 中
try {
// 业务逻辑
} catch (Exception e) {
// 记录失败事件到 DB
failedEventLogRepository.save(new FailedEventLog(event.getSpNo(), e.getMessage()));
// 触发告警
alertService.send("Approval handler failed: " + event.getSpNo());
throw e; // 确保事务回滚(若使用)
}
通过幂等控制、异步解耦与策略路由,系统可安全接收企业微信审批回调,即使面对重复推送或突发流量,也能保证业务一致性与高可用性。