企业微信审批回调事件的幂等接收与业务解耦设计

企业微信审批回调事件的幂等接收与业务解耦设计

企业微信审批流程支持配置回调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; // 确保事务回滚(若使用)
}

通过幂等控制、异步解耦与策略路由,系统可安全接收企业微信审批回调,即使面对重复推送或突发流量,也能保证业务一致性与高可用性。

相关推荐
wechatbot88815 小时前
企业微信API开发:登录-联系人查询-消息发送完整开发流程分享
汇编·微信·自动化·企业微信·ai编程·rpa
肥胖小羊2 天前
企业微信应用的 Token 缓存策略与分布式锁高可用设计
分布式·缓存·企业微信
噗噗122 天前
企业微信通讯录增量同步与异步任务队列的最佳实践
企业微信
肥胖小羊2 天前
基于企业微信 OAuth2.0 的单点登录(SSO)系统架构设计
企业微信
肥胖小羊2 天前
企业微信审批流状态变更监听与业务系统自动化对接
运维·自动化·企业微信
噗噗122 天前
企业微信自建应用如何优雅实现高性能消息推送
企业微信
qige0013 天前
企业微信通讯录全量与增量同步架构设计
企业微信
噗噗124 天前
企业微信 API 异常监控、全局错误码与限流处理最佳实践
企业微信
wechatbot8884 天前
解决企业微信官方 API 短板:iPad 协议全功能对接方案
java·汇编·windows·http·微信·企业微信
悟空码字4 天前
企业微信变了
微信·企业微信·ai助手