📚 系列文章导航:

  • 【YiFeiWebApi】易飞ERP全能WebAPI应用场景:点击阅读
  • 【YiFeiWebApi】易飞ERP集成平台 综合评估报告:点击阅读
  • 【YiFeiWebApi】易飞ERP与WMS系统接口对接实战:从0到1全流程指南:点击阅读
  • 【YiFeiWebApi】全模块WebAPI接口清单及功能介绍:点击阅读
  • 【YiFeiWebApi】易飞ERP全能WebAPI发布:全单据CRUD与审核流操作,RESTful风格覆盖所有版本:点击阅读
  • 【YiFeiWebApi】易飞ERP WebAPI 接口授权机制详解:从设计到实战:点击阅读
  • 【YiFeiWebApi】易飞ERP接口开发踩坑实录(避坑指南):点击阅读
  • 【YiFeiWebApi】YiFeiWebApi接口安装说明:点击阅读
  • 【YiFeiWebApi】YiFeiWebApi接口公测说明文档:点击阅读
  • 【YiFeiWebApi】YiFeIWebApi更新日志:点击阅读
  • 【YiFeiWebApi】企业微信审批直通易飞ERP:从回调验签到自动审核的完整实战(.NET Minimal API):点击阅读
  • 【YiFeiWebApi】.NET 实战:易飞 ERP 未审核销售订单自动推送企业微信审批流(applyevent 踩坑全记录):点击阅读
  • 【YiFeiWebApi】给鼎捷易飞 ERP 接一个大模型:我用 ASP.NET Core + DeepSeek 做了个"易飞小智",自然语言直接查业务数据:点击阅读

企业微信审批直通易飞ERP:从回调验签到自动审核的完整实战(.NET Minimal API)

场景:领导在企业微信手机上审批通过一张销售订单,几秒后,易飞 ERP 里这张单据的"审核"自动完成——全程没有人再登录 ERP 点一次按钮。

本文记录我用 .NET(Minimal API)从零实现这条链路的完整过程:企微回调验签、AES 消息解密、事件路由、审批表单解析、易飞 WebAPI 调用,以及 8 个真实踩坑记录。全部代码可落地,配置驱动,新增审批类型零代码。

一、需求背景

公司的进销存跑在鼎捷易飞 ERP 上,单据审核一直是"两头跑"的模式:

  1. 业务员在 ERP 里开单;
  2. 打印/截图找领导签字,或者领导登录 ERP 客户端点审核。

领导经常在外,审批滞后;审批完忘记在 ERP 里点"审核",下游流程卡住,又要电话催。

而公司内部协作已经全面跑在企业微信上,审批功能天然支持手机端。于是目标明确:

企微审批通过 → ERP 单据自动审核,审批即生效。

易飞本身提供了 WebAPI(/api/copi06/approve 这类审核接口),企微提供了审批事件回调,中间缺的就是一个"翻译官"——这就是本文的同步服务。

二、整体架构

┌──────────┐   ①提交审批    ┌──────────────┐
│ 业务员企微 │ ────────────→ │ 企微审批中心   │
└──────────┘                └──────┬───────┘
                                   ②审批通过,推送事件
                                   (POST,密文+签名)
                                   ▼
                        ┌─────────────────────┐
                        │  同步服务(本文主角)  │
                        │  验签 → 解密 → 路由   │
                        │  拉表单 → 组装报文    │
                        └──────────┬──────────┘
                                   ③datakeys 审核请求
                                   ▼
                        ┌─────────────────────┐
                        │  易飞 ERP WebAPI     │
                        │  /api/copi06/approve │
                        └─────────────────────┘

同步服务的职责边界很清晰:

  • :企微回调(URL 验证 GET + 事件推送 POST),加解密;
  • 翻译:根据"审批模板 ID + 审批状态"查规则表,决定调易飞哪个接口、怎么组参;
  • :调用易飞 WebAPI,解析 std_data 响应;
  • 兜底:失败让企微重试,成功后去重。

整个服务就是一个 ASP.NET Core Minimal API 单项目,几百行核心代码。

在这里插入图片描述

三、企微回调:验签与解密

3.1 两种回调

企微自建应用的"接收消息"配置里有两种请求:

  • GET:后台保存回调 URL 时的一次性验证,带 msg_signature/timestamp/nonce/echostr 四个参数,要求解密 echostr 后原样返回明文;
  • POST:事件推送,签名三参数在 URL Query 上,密文在请求体的 <Encrypt> 节点里。

这一点很多人第一次会踩坑——以为密文也在 Query 里,或者以为 POST 是明文 JSON。企微自建应用回调只有"安全模式"(加密模式),不存在明文模式。

3.2 验签算法

签名 = 把 token, timestamp, nonce, encrypt 四个字符串字典序排序后拼接,取 SHA1

public static string ComputeSignature(string token, string timestamp, string nonce, string encrypt)
{
    var items = new[] { token, timestamp, nonce, encrypt };
    Array.Sort(items, StringComparer.Ordinal);   // 注意是 Ordinal
    using var sha1 = SHA1.Create();
    var hash = sha1.ComputeHash(Encoding.UTF8.GetBytes(string.Concat(items)));
    var sb = new StringBuilder(40);
    foreach (var b in hash) sb.Append(b.ToString("x2"));
    return sb.ToString();
}

3.3 解密算法

  • Key = Base64Decode(EncodingAESKey + "="),43 位补一个 = 凑成 44 位 Base64,解出 32 字节 AES-256 密钥;
  • IV = Key 前 16 字节;
  • AES-256-CBC,手动去 PKCS#7 填充(填充块按 32 字节算,不是 16);
  • 解密后的明文结构:16字节随机串 + 4字节大端消息长度 + 消息体 + ReceiveId(CorpId),最后要校验 ReceiveId 是否等于自己的 CorpId。
private string AesDecrypt(string encryptedBase64)
{
    var cipherBytes = Convert.FromBase64String(encryptedBase64);
    using var aes = Aes.Create();
    aes.Key = _aesKey;
    aes.IV = _aesKey.Take(16).ToArray();
    aes.Mode = CipherMode.CBC;
    aes.Padding = PaddingMode.None;          // 手动去填充

    using var decryptor = aes.CreateDecryptor();
    var plain = decryptor.TransformFinalBlock(cipherBytes, 0, cipherBytes.Length);

    var pad = plain[^1];
    if (pad is < 1 or > 32) throw new CryptographicException("解密填充非法");
    var content = plain[..^pad];

    var msgLen = (content[16] << 24) | (content[17] << 16) | (content[18] << 8) | content[19];
    var message  = Encoding.UTF8.GetString(content, 20, msgLen);
    var receiveId = Encoding.UTF8.GetString(content, 20 + msgLen, content.Length - 20 - msgLen);

    if (receiveId != _corpId) throw new CryptographicException("消息 ReceiveId 不匹配");
    return message;
}

官方 C# 版 WXBizMsgCrypt 也可以直接用,自己实现一遍的好处是心里有数——排查签名不匹配时知道每一环在哪。

3.4 回调端点(Minimal API)

// GET:URL 验证
app.MapGet(callbackPath, (HttpContext ctx, WXBizMsgCrypt crypt) =>
{
    var q = ctx.Request.Query;
    var echo = crypt.VerifyUrl(q["msg_signature"], q["timestamp"], q["nonce"], q["echostr"]);
    return Results.Text(echo);
});

// POST:事件推送
app.MapPost(callbackPath, async (HttpContext ctx, WXBizMsgCrypt crypt,
                                 IApprovalSyncService sync, ILogger<Program> logger,
                                 CancellationToken ct) =>
{
    var q = ctx.Request.Query;
    string plainXml;
    using (var reader = new StreamReader(ctx.Request.Body))
    {
        var body = await reader.ReadToEndAsync(ct);
        try { plainXml = crypt.DecryptPostXml(body, q["msg_signature"], q["timestamp"], q["nonce"]); }
        catch (Exception ex)
        {
            logger.LogWarning(ex, "回调消息验签/解密失败");
            return Results.Text("invalid signature", statusCode: 401);
        }
    }
    try { await sync.HandleEventAsync(plainXml, ct); }
    catch (Exception ex)
    {
        // 业务失败返回 500,企微会重试推送(最多 3 次)
        logger.LogError(ex, "处理企业微信事件失败,等待重试");
        return Results.Text("retry", statusCode: 500);
    }
    return Results.Text("success");   // 必须在 5 秒内返回
});

两个关键约束

  1. 必须在 5 秒内响应,否则企微判定推送失败并重试——这直接决定了下游易飞调用的超时预算(见踩坑 7);
  2. 业务失败返回 500 而不是吞掉异常返回 200——把重试机制交给企微,比自己做队列简单可靠得多。

四、事件解析与规则路由

审批状态变化事件的明文 XML 长这样(节选):

<xml>
  <ToUserName><![CDATA[wwxxxxxxxx]]></ToUserName>
  <MsgType><![CDATA[event]]></MsgType>
  <Event><![CDATA[sys_approval_change]]></Event>
  <ApprovalInfo>
    <SpNo>202609180001</SpNo>
    <TemplateId><![CDATA[C4en9STxxxxxxxxxxxxx]]></TemplateId>
    <SpStatus>2</SpStatus>
  </ApprovalInfo>
</xml>

三个关键字段:SpNo(审批单号)、TemplateId(审批模板)、SpStatus(状态:2=通过,3=驳回,4=撤销,6=通过后撤销,7=删除)。

路由的核心设计:TemplateId + SpStatus → 易飞接口,全部放在配置里:

"YiFei": {
  "BaseUrl": "http://erp.example.com:8080",
  "License": "<X-API-License>",
  "CompanyId": "001",
  "SecurityCode": "******",
  "TimeoutSeconds": 3,
  "Rules": [
    {
      "TemplateId": "C4en9STxxxxxxxxxxxxx",
      "SpStatus": 2,
      "PayloadType": "DataKey",
      "ApiPath": "/api/copi06/approve",
      "DocTypeField": "单别",
      "DocNoField": "单号"
    }
  ]
}

PayloadType 支持两种入参形态:

类型适用动作入参来源
DataKeyapprove / disapprove / invalid / delete从审批表单里按控件标题取"单别"“单号”
Documentcreate / updateJSON 模板 + 表单值渲染完整单据体

这样接入新的审批类型 = 企微建一个模板 + 配置文件加一条规则,不改代码。

五、拉取审批详情,取单别单号

事件推送里只有 SpNo,没有表单内容。需要再调企微 oa/getapprovaldetail 接口(用自建应用的 CorpSecret 换 access_token 后调用)拉回完整表单。

表单是控件数组,每个控件有 titlevalue,不同控件类型(Text/Number/Date/Selector/明细表格)的 value 结构完全不同,解析时按类型分流:

  • Text/Number/Money → 直接取值;
  • Date → timestamp 转 yyyyMMdd
  • Selector → 取选中项 key;
  • 明细表格 → 按行展开,供 Document 模板渲染。

然后按规则里配置的控件标题(DocTypeField: "单别"DocNoField: "单号")取出两个值,取不到直接抛异常,绝不发脏数据给 ERP:

private async Task<(bool Ok, string Message)> InvokeByDataKeyAsync(
    TemplateRule rule, ApprovalForm form, string spNo, CancellationToken ct)
{
    var docType = ReadRequiredField(form.Scalars, rule.DocTypeField, spNo, "单别");
    var docNo   = ReadRequiredField(form.Scalars, rule.DocNoField, spNo, "单号");
    return await _yiFei.InvokeByDataKeyAsync(rule.ApiPath, docType, docNo, ct);
}

六、调用易飞 WebAPI

易飞 WebAPI 的契约是标准 std_data 包裹,审核类接口入参为 datakeys

{
  "std_data": {
    "parameter": {
      "datakeys": [
        { "doc_type_no": "2213", "doc_no": "2609015" }
      ]
    }
  }
}

请求头三个认证字段:X-API-LicenseX-API-CompanyIdX-API-SecurityCode

响应:

{
  "std_data": {
    "execution": { "code": 0, "description": "审核成功" },
    "parameter": {
      "result": {
        "success": [{ "doc_type_no": "2213", "doc_no": "2609015" }],
        "error": []
      }
    }
  }
}

注意 HTTP 200 不代表业务成功,必须判断 execution.code == 0,否则会漏掉"单据状态不允许审核"这类业务错误。

成功后以 SpNo + ApiPath 为键做 24 小时内存去重——企微的重试机制意味着同一事件可能收到多次,而"同一审批单挂多个动作"的场景也要求去重键带上接口路径。

七、踩坑记录(本文精华)

这部分是拿真实调试时间换来的,按排查顺序列。

坑 1:服务只监听 localhost,公网推不进来

Kestrel 默认绑定 localhost:5000。本机测试一切正常,企微推送石沉大海,控制台连一行日志都没有。启动日志里一行小字暴露了问题:

Now listening on: http://localhost:5000   ← 只有回环地址

修复,一行:

builder.WebHost.UseUrls("http://0.0.0.0:5000");

坑 2:Windows 防火墙没有 5000 入站规则

改成 0.0.0.0 后依然收不到。netsh advfirewall firewall show rule 里查不到 5000,公网请求全被拦。加规则:

netsh advfirewall firewall add rule name="WeComSync 5000" dir=in action=allow protocol=TCP localport=5000

判别方法:从外部 curl http://服务器IP:5000/health,通不了就是网络层问题,别先怀疑代码。

坑 3:签名不匹配 → Token/AESKey 必须逐字符一致

CryptographicException: 事件消息签名不匹配。程序里加一行启动日志,把加载到的 Token/AESKey 打出来和企微后台逐字符比对(改配置后必须重启进程才生效)。这个错误基本只有两个来源:值不一致、或改了配置没重启。

坑 4:TemplateId 抄错两个字母,规则永远不匹配

最隐蔽的一个。把 TemplateId 从后台 URL 抄进配置时看错了字符(Ctn 看成 Cnt),于是事件到了、解密成功了,但规则匹配不上,日志一直打印"忽略未配置规则的审批事件"。

排查技巧:在"忽略"日志里同时打印企微推送值和所有已配置值,一次对比:

忽略未配置规则的审批事件:TemplateId=C4en...Ctnpy9PE(推送值)
                        已配置的 TemplateId=[C4en...Cntpy9PE]
                                                              ^^ 就差这里

结论:以企微实际推送的值为准,不要相信手工转录。教训是这类 ID 永远用文本复制,不要经过截图。

坑 5:企微审批 API 权限与可信 IP

两个后台开关漏一个,getapprovaldetail 就调不通:

  • 审批 → API → 可调用应用:必须勾选自建应用,否则报 60011 no permission;
  • 自建应用 → 可信 IP:必须加服务器公网 IP,否则报 ip not allowed。

坑 6:易飞 License 绑定机器码

易飞 WebAPI 的 License 按机器码(Machine key)授权,换一台服务器部署,License 立刻 401。响应里会给出当前机器的 Machine key,拿去给易飞重新授权即可。本地测试和正式部署的机器码不同,都要授权

坑 7:易飞超时 8 秒 > 企微 5 秒上限

最初易飞超时设 8 秒。设想这个时序:易飞 6 秒处理完 → 企微 5 秒已断开判定失败 → 企微重试 → 同一张单被审核两次。虽然易飞对已审核单会返回"状态为 Y,不可审核"兜底,但正确做法是把易飞超时压到 3 秒以内,宁可让企微重试,也不让调用悬在半空。

坑 8:单据字段不能"看着像数字就当数字"

渲染 create 类单据体时,单别 2213、日期 20260918 如果被序列化成 JSON 数字,易飞字符串字段直接出错。模板占位符统一输出字符串,数量/单价等真正需要数字的字段用显式过滤器声明(如 {{订单数量|number}})。

八、可靠性设计小结

环节机制
企微推送失败返回 500,企微自动重试 3 次
处理超时易飞超时 3s < 企微 5s 硬上限
重复推送SpNo + ApiPath 内存去重 24h
脏数据表单取不到单别/单号直接失败,不发 ERP
安全回调 SHA1 验签 + AES 解密 + ReceiveId 校验
可观测规则加载日志、命中日志、单据映射日志、失败详情

九、总结

这条链路技术上没有单点难点,难在把两套系统的契约精确对齐:企微的加解密、事件结构、5 秒时限,加上易飞的 std_data、机器码授权、字符串字段。坑几乎全是"差一个字符""差一条规则"级别的。

架构上最值得复用的是配置驱动的规则表TemplateId + SpStatus → ApiPath + 入参映射。审批/驳回/作废/删除这类 datakeys 动作,新增一个审批类型只需要在企微后台建模板、在配置里加一条规则,程序零改动。

如果你也在做企微与 ERP 的集成,欢迎评论区交流。

步骤一:企业微信新增ERP客户订单(可从ERP同步)

在这里插入图片描述
步骤二:企业微信审批流程
步骤三:审批结束后自动触发,易飞-API接口,执行成功。
在这里插入图片描述
前置条件:(1)YiFeiWebApi–易飞API接口 (2) WeComYiFeiSync-企微易飞同步服务


Logo

一站式 AI 云服务平台

更多推荐