简介:面向C#开发者的一份微信支付接口实战代码包,聚焦微信支付、企业付款到个人及企业付款到零钱等常见资金流转场景,适合需要快速接入微信支付能力的中初级后端开发人员参考。压缩包共93个文件、约2.13MB,以47个cs源码文件为核心,配合13个aspx页面、8个dll类库及若干config配置、js脚本与项目解决方案文件,构成可直接编译运行的示例工程。内容围绕微信支付API展开,涵盖JSAPI支付、刷卡支付、扫码支付,以及企业转账、企业付款到零钱、订单退款、订单与退款查询、账单下载等接口的调用流程,并附带CS_sdk_v3.0.9版本SDK封装。读者可据此了解预支付会话获取、签名验证、请求发送与结果处理等关键环节,对照示例代码完成接口调试与功能移植。目前已有1578人学习下载,适合作为微信支付接入的起步参考。
1. 微信企业付款到零钱:一份能直接跑通的 C# Demo 到底解决了什么
做支付对接的同行大概都遇到过这种场景:运营在群里甩来一句“给这批用户退 5 块钱到零钱”,你打开文档一看,企业付款到零钱、商家转账到零钱、企业付款到银行卡,接口名字换了好几茬,参数从openid到transfer_scene_id一路加,最后卡在“签名错误”或者“产品权限未开通”上,日志里只有一串看不懂的返回码。这份 C# Demo 就是冲着这个痛点来的——它把微信支付和企业付款到个人(含企业付款到零钱)的请求组装、签名、证书加载、回调验签、结果查询串成一条可运行的链路,用 C# 写成,适合正在做 .NET 系后台、需要给用户打款或做退款到零钱的开发者。你拿到的不只是一段能编译的代码,而是一套把“证书怎么放、签名怎么算、金额单位怎么换算、失败怎么查”讲清楚的落地骨架,照着改参数就能接进自己的系统。
2. 拆开 Demo 看结构:请求、签名、证书三件套怎么摆
2.1 目录与职责划分:先认清哪块该改、哪块别动
拿到一个支付 Demo,最忌讳上来就全局替换商户号然后编译报错。我一般先看目录,把“配置层、网络层、业务层、工具层”分开。这份 Demo 的典型结构大致是这样:配置文件里放商户号、AppID、证书路径、APIv3 密钥;工具类里放签名、验签、证书加载、金额转换;业务类里按“下单、付款到零钱、查询、回调”分方法;入口是一个控制台或 WebAPI 的示例调用。你要改的只有配置和业务参数,签名与证书加载逻辑尽量别动,那是血泪经验堆出来的。
| 模块 | 典型文件/类 | 你要不要改 | 说明 |
|---|---|---|---|
| 配置 | appsettings / 常量类 | 必须改 | 商户号、AppID、证书路径、密钥 |
| 签名工具 | 签名/验签类 | 不改 | RSA 与 HMAC 两套,改了就翻车 |
| 证书加载 | 证书工具类 | 不改 | 商户证书与平台证书分开 |
| 业务方法 | 付款/查询/回调 | 按需改 | 金额、openid、单号 |
| 示例入口 | Program / Controller | 参考 | 照着调一遍验证链路 |
这里有个容易忽略的点:企业付款到零钱用的是商户 API 证书做签名,而回调验签用的是平台证书,两套证书别混。Demo 里如果只给了一个证书路径,你要确认它对应的是哪一套,常见做法是商户证书放本地文件、平台证书通过下载接口拉取后缓存。
2.2 签名与证书加载:报“签名错误”八成栽在这
微信支付的签名不是玄学,但细节多。APIv3 的请求签名流程是:构造签名串(方法、URL、时间戳、随机串、请求体),用商户私钥做 SHA256withRSA 签名,再拼成 Authorization 头。Demo 里这段通常封装成一个方法,你重点看三处:签名串的换行符是不是\n、请求体是不是原始 JSON 字符串(不能先反序列化再序列化,字段顺序会变)、时间戳是不是秒级。下面这段是签名核心逻辑的示意,参数含义写在注释里。
// 构造待签名串:HTTP方法\nURL\n时间戳\n随机串\n请求体\n string message = $"{method}\n{urlPath}\n{timestamp}\n{nonce}\n{body}\n"; // 用商户私钥做 SHA256withRSA,输出 Base64 using var rsa = RSA.Create(); rsa.ImportPkcs8PrivateKey(Convert.FromBase64String(privateKey), out _); byte[] signBytes = rsa.SignData( Encoding.UTF8.GetBytes(message), HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); string signature = Convert.ToBase64String(signBytes); // Authorization: WECHATPAY2-SHA256-RSA2048 mchid="...",nonce_str="...",signature="...",timestamp="...",serial_no="..."逻辑说明:message的五段顺序不能乱,最后那个换行很多人漏掉;urlPath是带 query 的路径,不含域名;serial_no是商户证书序列号,不是平台证书的。参数上,timestamp用DateTimeOffset.UtcNow.ToUnixTimeSeconds(),别用本地时间;nonce用 32 位随机串即可。证书加载常见做法是把.p12转成 PEM 后读私钥,或者直接用X509Certificate2加载再导出私钥,注意 .NET 版本对ExportPkcs8PrivateKey的支持差异,老框架上会直接抛异常。
2.3 付款到零钱的请求组装:金额单位是最容易翻车的地方
企业付款到零钱的请求体里,amount单位是分,不是元。Demo 里如果有个MoneyUtil.ToFen()之类的转换,别嫌它多余,我见过把 5 元写成 5 分结果用户收到 5 分钱来投诉的。请求字段大致包括appid、mchid、out_batch_no(批次单号)、batch_name、batch_remark、total_amount、total_num,以及transfer_detail_list里的out_detail_no、openid、transfer_amount、transfer_remark。新版接口还要求transfer_scene_id之类的场景标识,具体以你开通的产品权限为准。
var payload = new { appid = appId, mchid = mchId, out_batch_no = $"B{DateTime.Now:yyyyMMddHHmmss}{Random.Shared.Next(1000, 9999)}", batch_name = "用户奖励发放", batch_remark = "活动奖励", total_amount = 500, // 单位:分,这里是 5 元 total_num = 1, transfer_detail_list = new[] { new { out_detail_no = $"D{DateTime.Now:yyyyMMddHHmmssfff}", openid = userOpenId, transfer_amount = 500, transfer_remark = "奖励到零钱" } } };逻辑说明:批次单号out_batch_no全局唯一,重复提交会返回“单号已存在”,所以别用固定值;total_amount必须等于明细金额之和,否则校验不过;openid必须是该 AppID 下的用户,跨公众号和小程序的 openid 不通用,这是踩坑重灾区。参数改法上,金额从元转分用(int)(yuan * 100)前先做四舍五入,浮点直接乘会出499.9999的幺蛾子。
3. 从调用到回调:把一条打款链路跑通的四个动作
3.1 发起付款:先小额验证再批量
链路跑通的第一步不是直接上生产打款,而是用 1 分钱验证。Demo 的入口方法一般接收 openid 和金额,内部完成签名、发请求、解析返回。你第一次跑,建议把金额写死 1 分,单号写死一个测试值,确认返回batch_id和out_batch_no对得上。请求发出去后,HTTP 状态码 200 不代表成功,要看返回体里的code字段,成功时通常没有code或为SUCCESS。
// 发起批量转账,返回体里拿 batch_id 用于后续查询 var result = await transferClient.PostAsync("/v3/transfer/batches", payload); string respBody = await result.Content.ReadAsStringAsync(); // 成功:{"out_batch_no":"...","batch_id":"...","create_time":"..."} // 失败:{"code":"PARAM_ERROR","message":"...","detail":{...}}逻辑说明:PostAsync内部要带上 Authorization 头,别漏;返回体先打日志再反序列化,出问题时原始报文比异常堆栈有用。参数上,batch_id是微信侧生成的,后续查询和回调都用它或out_batch_no关联,建议入库时两个都存。
3.2 查询与对账:别只信回调
回调可能丢、可能重复,所以查询接口是后悔药。Demo 里通常有按out_batch_no查批次、按out_detail_no查明细两个方法。对账时我一般会做一张本地流水表,记录每次请求的单号、金额、状态,定时拉查询接口比对,状态不一致的单独捞出来人工看。查询接口的签名和付款接口一样,URL 里带out_batch_no参数即可,注意 query 参数要参与签名串的 URL 部分。
// 按商户批次单号查询,GET 请求,query 参与签名 string path = $"/v3/transfer/batches/out-batch-no/{outBatchNo}?need_query_detail=true"; var resp = await transferClient.GetAsync(path); // 明细状态:SUCCESS / FAIL / PROCESSING,FAIL 时看 fail_reason逻辑说明:need_query_detail=true才会返回明细列表,否则只有批次汇总;fail_reason常见有“余额不足”“openid 不匹配”“超出限额”,对应处理方式不同。参数上,查询频率别太高,微信侧有频控,批量对账建议错峰跑。
3.3 回调验签与幂等:重复通知是常态
付款结果回调进来,第一件事是验签,第二件事是幂等。验签用平台证书公钥,对Wechatpay-Timestamp、Wechatpay-Nonce、请求体拼串后验Wechatpay-Signature。Demo 里这段如果写成了“跳过验签先跑通”,上线前必须补回来,否则任何人都能伪造通知。幂等靠out_batch_no或batch_id做唯一键,收到重复通知直接返回成功,别重复改状态。
// 验签串:时间戳\n随机串\n请求体\n string verifyMsg = $"{timestamp}\n{nonce}\n{body}\n"; bool ok = platformRsa.VerifyData( Encoding.UTF8.GetBytes(verifyMsg), Convert.FromBase64String(signature), HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); if (!ok) return Unauthorized(); // 幂等:按 out_batch_no 查本地流水,已处理则直接返回成功逻辑说明:body必须是原始请求体字节,不能反序列化后再拼;平台证书会轮换,验签失败先看证书序列号是不是过期了。参数上,回调响应要快,业务处理放队列,别在回调里做耗时操作导致微信重试。
3.4 异常码对照:把返回码翻译成人话
Demo 跑不通时,返回码是黑匣子。下面这张表是我整理的高频码,遇到先对号入座,比盲改代码快。
| 返回码/现象 | 常见原因 | 处理方向 |
|---|---|---|
| 签名错误 | 签名串顺序/换行/请求体被改写 | 打印待签名串逐字节比对 |
| 产品权限未开通 | 商户号没开企业付款产品 | 商户平台确认产品权限 |
| openid 不匹配 | AppID 与 openid 来源不一致 | 核对公众号/小程序 AppID |
| 余额不足 | 商户号可用余额不够 | 充值或改用其他出资账户 |
| 单号已存在 | out_batch_no 重复 | 换单号或先查询原单 |
| 证书序列号错误 | 用了平台证书序列号 | 换商户证书序列号 |
4. 避坑排查:这五条我踩过,你别再踩
4.1 金额单位混用导致打款金额错位
现象:用户收到 0.05 元而不是 5 元。原因:请求里transfer_amount单位是分,代码里从元直接赋值没乘 100,或者乘了两次。解决:统一在入口做一次元转分,工具方法里加断言,金额小于 1 分直接抛异常,日志里同时打印元和分两个值。
4.2 证书路径在开发机正常、服务器报找不到
现象:本地跑通,部署后抛“证书文件不存在”或“私钥读取失败”。原因:证书用了绝对路径,服务器目录结构不同;或者 .p12 密码没配、权限不足。解决:证书放相对路径并随发布输出,密码走配置中心,部署后用一段最小代码先验证证书能加载再跑业务。
4.3 回调验签失败但报文看着没问题
现象:验签一直 false,报文和文档示例一致。原因:框架中间件把请求体读了一次,业务里再读时流已到末尾,拿到空串;或者平台证书过期未更新。解决:在中间件里缓存原始请求体字节,验签用缓存;平台证书做定时刷新,验签失败时先查证书有效期。
4.4 重复提交产生多笔打款
现象:同一用户收到两笔钱。原因:网络超时后代码重试,但第一次请求其实已成功,out_batch_no没做唯一约束。解决:单号由业务侧生成并入库唯一索引,重试前先查询原单状态,查询到成功就不再发。
4.5 限额与场景标识没配导致大额失败
现象:小额能过,超过某个金额就报错。原因:企业付款到零钱有单笔和单日限额,新版接口还要求场景标识与报备场景一致。解决:先查商户号当前限额,场景标识按实际业务选并确认已报备,批量打款拆单前算好总额。
5. 进阶:把 Demo 改造成可对账的生产骨架
Demo 能跑通只是起点,真正上线要补的是“可观测”和“可对账”。我的习惯是给每次请求加一个本地流水号,和微信的batch_id、out_batch_no三列一起入库,状态机走“已提交→处理中→成功/失败”,任何一步异常都留原始报文。下面是一个简化的流水表结构和状态流转,照着建表比事后翻日志省事。
CREATE TABLE wx_transfer_flow ( id BIGINT PRIMARY KEY AUTO_INCREMENT, out_batch_no VARCHAR(64) NOT NULL UNIQUE, -- 商户批次单号,唯一约束防重 batch_id VARCHAR(64), -- 微信侧批次号 total_amount INT NOT NULL, -- 单位分 status VARCHAR(16) NOT NULL, -- SUBMITTED/PROCESSING/SUCCESS/FAIL fail_reason VARCHAR(255), raw_resp TEXT, -- 原始返回报文,排错用 created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP );逻辑说明:out_batch_no唯一索引是防重的最后一道闸;raw_resp存原始报文,出问题时不用去翻应用日志;status只由查询结果和回调驱动更新,别在发起请求时就写成功。参数上,total_amount用 INT 存分,避免浮点;fail_reason留够长度,微信的失败描述有时挺长。
验证方法上,我一般做三步:第一步用 1 分钱跑通全链路并核对流水;第二步模拟回调重复推送,确认幂等生效;第三步断网重试,确认不会产生第二笔。进阶技巧是写一个对账脚本,每天定时拉前一天的批次和明细,和本地流水逐条比对,差异项输出成表人工处理。这套骨架搭好之后,再换其他打款产品,改的只是请求字段,流水和对账逻辑能复用。
从那以后我每次接新的支付打款接口,都强制先跑一遍“1 分钱 + 重复回调 + 断网重试”三件套,再谈批量。希望帮到你。
本文还有配套的精品资源,点击获取