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

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

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

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

相关推荐
梦想的旅途28 小时前
企业微信外部群消息主动推送开发指南
企业微信
梦想的旅途210 小时前
基于企业微信 API 的外部群消息异步推送机制实现
企业微信
vx-bot55566611 小时前
企业微信协议接口的安全调用与性能优化规范
安全·性能优化·企业微信
天空属于哈夫克311 小时前
通过企业微信二次开发构建外部群主动推送体系
企业微信
企微自动化1 天前
企业微信二次开发实战:突破限制,实现外部群消息主动推送
自动化·企业微信
天空属于哈夫克31 天前
企业微信二次开发:如何实现外部群自动化消息推送?
运维·自动化·企业微信
2501_941982051 天前
突破企业微信 API 限制:利用 RPA 架构实现外部群自动化调用的深度实践
自动化·企业微信·rpa
天空属于哈夫克31 天前
企业微信实现外部群消息的主动推送?
前端·chrome·企业微信
企微自动化1 天前
企业微信外部群:主动推送消息的技术方案
企业微信