☰
C#.NET支付网关源码拆解:一套代码整合微信、支付宝与银联支付
2026/10/16 4:34:31 网站建设 项目流程

简介:面向C#/.NET开发者的支付整合资源,专门解决微信、支付宝、银联三类支付渠道的统一对接问题,适用于电商平台、App、H5商城等需要在线收款的业务场景,也适合从零搭建支付模块的新手参考。资源以zip压缩包提供,整体约52.91MB,目前已吸引1493人学习下载。内容覆盖微信JSAPI、Native、H5、App支付,支付宝网页支付、扫码支付与App支付,以及银联网上支付、快捷支付等常用接入方式,并给出统一支付API层的设计思路,让上层业务无需关心渠道差异。针对支付安全与稳定性,还整理了商户密钥加密、异步回调验签、订单状态更新、退款/撤销、异常处理与沙箱联调等关键要点。下载后既能对照示例理清从下单、支付到回调处理的全链路流程,也可将其中的接口封装与配置规范复用到真实支付项目中。

1. C#.NET 整合微信、支付宝、银联支付:一套代码顶三套 SDK

凡是接过多渠道支付的后端,多少都有过这种体验:微信要拼 XML、验 MD5 签名、加载 p12 客户端证书;支付宝要配 RSA2 密钥、拼待签名字符串、解析 form 表单回调;银联又单独是一套证书体系和报文格式。三个渠道各来一遍,业务代码里到处是 if (channel == "wechat") 的分支。最近拆的这份 C#.NET 支付网关源码,把这些杂乱统一到了一个抽象层:业务侧只面临一个下单方法、一个回调入口,微信、支付宝、银联各自以适配器的方式插在底层。适合正在做电商、缴费、会员充值系统的 .NET 开发者,新项目可以直接裁剪,老项目也可以拿它当剥离支付逻辑的模板。下面先从解决方案的目录和分层开始拆。

2. 技术架构:从统一支付入口到渠道适配层的分层思路

整体架构一句话可以说完:业务层不感知任何渠道细节,所有支付能力都从统一入口派发。源码解压后是一个典型的四项目解决方案,API 层只负责接收 HTTP 请求,核心业务模型放在中间层,渠道实现独立成项目,测试项目用来做本地联调。这个分层的价值在于,每次新增渠道不需要动业务代码,也不需要把 SDK 依赖散落到各个 Service 里。

2.1 为什么先用接口定义而不是直接引 SDK

很多项目最初的支付代码都是这么写出来的:Controller 里先判断支付方式,if 微信一大堆逻辑,else if 支付宝又是一大堆逻辑。订单状态一多,这套代码就变成没人敢动的黑匣子。这个项目的第一步是定义渠道接口,把支付渠道的共同动作收敛成四个方法:创建订单、查询订单、申请退款、解析回调。

public interface IPaymentChannel { string ChannelCode { get; } PaymentResult CreateOrder(PaymentOrder order); PaymentQueryResult QueryOrder(string outTradeNo); RefundResult Refund(RefundRequest request); PaymentNotifyResult ParseNotify(string channelRawData, IDictionary<string, string> headers); }

逻辑说明:接口的四个方法覆盖支付生命周期里最核心的四个动作。ChannelCode 用来标识当前渠道,比如 wechat、alipay、unionpay。CreateOrder 入参统一用 PaymentOrder 模型,出参 PaymentResult 包含前端需要的数据——微信是 code_url,支付宝是跳转表单,银联是拼好的 HTML。ParseNotify 接收各渠道回传的原始数据,在内部完成验签后输出统一的订单结果。

参数说明:PaymentOrder 里比较关键的字段有 out_trade_no、total_fee、channelCode、notifyUrl、openId。其中 total_fee 在所有渠道里统一以“分”为单位,浮点转整数的动作在上层完成,避免在渠道层出现精度问题。notifyUrl 不是每个渠道都必填,但保留这个字段方便内网穿透和本地联调。

从这个接口能看出,真正的复杂度被挡在了接口实现内部。业务层只需要拿到 PaymentResult,根据返回类型做展示即可。后来接新渠道,本质是新增一个实现类,而不是改动业务流程。

2.2 目录结构与核心文件分布

解决方案内部按职责拆成四个项目,边界非常干净。

PayGateway.sln ├─ PayGateway.Api // Web API 入口,接收下单与回调请求 │ ├─ Controllers │ │ ├─ PaymentController.cs │ │ └─ PaymentNotifyController.cs │ └─ Program.cs ├─ PayGateway.Core // 核心领域模型与业务逻辑 │ ├─ Models │ │ ├─ PaymentOrder.cs │ │ ├─ PaymentResult.cs │ │ └─ RefundRequest.cs │ ├─ Services │ │ ├─ PaymentDispatcher.cs │ │ └─ OrderStateMachine.cs │ └─ IPaymentChannel.cs ├─ PayGateway.Channels // 渠道实现 │ ├─ Wechat │ │ ├─ WechatPaymentChannel.cs │ │ ├─ WechatSignature.cs │ │ └─ WechatClient.cs │ ├─ Alipay │ │ ├─ AlipayPaymentChannel.cs │ │ ├─ AlipaySignature.cs │ │ └─ AlipayClient.cs │ └─ Unionpay │ ├─ UnionpayPaymentChannel.cs │ ├─ UnionpaySignature.cs │ └─ UnionpayClient.cs └─ PayGateway.Tests // 渠道调试与 mock 测试

从文件分布能看出来,每个渠道都遵循同一套范式:PaymentChannel 负责流程编排,Client 负责 HTTP 请求与证书加载,Signature 只做签名和验签。把“通信”和“业务”分开,好处在切换 UAT 环境或者替换 HTTP 工具时特别明显,改 Client 不影响下单单据组装,改签名类也不影响外部的账户体系。

2.3 配置模型与依赖注入的绑定方式

配置没有写死在代码里,而是统一放在 appsettings.json 中,每个渠道一个独立 section。微信部分主要放 app_id、mch_id、api_key、证书路径;支付宝部分放 app_id、private_key、public_key、sign_type;银联部分放 merId、证书路径、网关地址。

{ "PaymentChannels": { "Wechat": { "Enabled": true, "AppId": "", "MchId": "", "ApiKey": "", "NotifyUrl": "https://api.example.com/pay/notify/wechat" }, "Alipay": { "Enabled": true, "AppId": "", "PrivateKey": "", "AlipayPublicKey": "", "NotifyUrl": "https://api.example.com/pay/notify/alipay" }, "Unionpay": { "Enabled": true, "MerId": "", "SignCertPath": "", "EncryptCertPath": "", "RootCertPath": "", "NotifyUrl": "https://api.example.com/pay/notify/unionpay" } } }

逻辑说明:每个渠道都有自己的证书和密钥,拆成独立 section 后,发布到不同环境只替换配置文件,不需要改代码。银联的签名证书、加密证书、根证书路径是有明确分工的:签名证书给请求报文签名,加密证书加密敏感字段,根证书用来验证银联返回响应的签名,三者不能混用。

配置读取之后,项目在 Program.cs 里做了程序集扫描,把实现了 IPaymentChannel 的类注册进 DI 容器,同时注册 PaymentDispatcher 作为统一入口。业务代码注入时拿到的是渠道管理器,通过传入的渠道编码获取对应实例,而不是手动 new 一个实现类。这种注册方式让渠道实现完全对业务透明,也是后面扩展新渠道的基础。

2.4 统一入口 PaymentDispatcher 的分发逻辑

渠道管理器是整个网关的中枢,所有下单请求都从它这里走到具体渠道。分发逻辑相当简单,本质是根据 channelCode 从注册表里取实例。

public PaymentResult DispatchCreateOrder(PaymentOrder order) { var channel = _channels.GetValueOrDefault(order.ChannelCode); if (channel == null) { throw new PaymentException($"不支持的支付渠道: {order.ChannelCode}"); } return channel.CreateOrder(order); }

逻辑说明:DispatchCreateOrder 的职责只有一个——按渠道编码把请求派给正确的实现类。如果配置里没启用该渠道或者实现没注册,直接抛异常而不是返回空结果,这样调用方可以快速感知配置问题。同理,查询、退款、回调分发都走同一个模式。分发层不做任何业务校验,校验订单金额、商户号合法性等逻辑放在 Core 层的 OrderStateMachine 中。

提示:实际项目里我会在分发层加一个简单的重试开关,只有查询和退款允许重试,下单和回调不允许。因为下单重复提交会生成多个未支付订单,回调重复处理则可能重复入账。

理解了这个分发机制,再看各渠道实现,就会清晰很多。每一种渠道都只关心自己的协议细节,不用管订单从哪来、到哪去。

3. 核心模块拆解:一个订单从接口到三方网关的完整链路

这一章是源码中最值得读的部分。三种渠道的处理风格差异非常大,但都嵌在同一个框架里。下面按照微信、支付宝、银联依次过一遍下单主链路。

3.1 微信支付:NATIVE 扫码下单与 XML 参数组装

微信渠道支持 JSAPI 和 NATIVE 两种模式,其中 NATIVE 扫码支付路径最简单,最适合讲参数组装和签名规则。

public PaymentResult CreateOrder(PaymentOrder order) { var dict = new SortedDictionary<string, string> { ["appid"] = _options.AppId, ["mch_id"] = _options.MchId, ["out_trade_no"] = order.OutTradeNo, ["total_fee"] = order.TotalFee.ToString(), ["body"] = order.Subject, ["notify_url"] = order.NotifyUrl ?? _options.NotifyUrl, ["trade_type"] = "NATIVE", ["nonce_str"] = Guid.NewGuid().ToString("N").ToUpper() }; var sign = WechatSignature.Sign(dict, _options.ApiKey); dict["sign"] = sign; var xml = BuildXml(dict); var responseXml = _client.PostXml("https://api.mch.weixin.qq.com/pay/unifiedorder", xml); var result = ParseXml(responseXml); if (result["return_code"] == "SUCCESS" && result["result_code"] == "SUCCESS") { return PaymentResult.FromCodeUrl(result["code_url"]); } throw new PaymentException($"微信下单失败: {result["err_code_des"]}"); }

逻辑说明:微信支付的签名规则是先把参数放进一个按字典序排列的 SortedDictionary,拼成键值对字符串,再拼接 ApiKey 做 MD5。nonce_str 每次请求都要重新生成,防止重放攻击。签名完成后把所有参数转成 XML 提交到统一下单接口,返回结果里 return_code 表示通信是否成功,result_code 表示业务是否成功,两个都成功才有 code_url。

参数说明:total_fee 单位是分,金额 1 元就传 100;body 字段有长度限制,中文按 UTF-8 编码,超长会被微信返回参数错误。如果 return_code 为 SUCCESS 但 result_code 为 FAIL,优先看 err_code_des,实际遇到最多的是 appid 和 mch_id 不匹配。

拿到 code_url 后,后端把它返回给前端生成二维码,前端轮询查单接口,支付成功再跳转结果页。整个链路里后端不需要额外动作,回调进来时再更新订单状态。

3.2 支付宝电脑网站支付:RSA2 签名与收银台表单构造

支付宝处理方式和微信完全不同,它不是直接调 API,而是后端生成一段自动提交的 HTML 表单,浏览器提交到支付宝收银台。

public PaymentResult CreateOrder(PaymentOrder order) { var sortedParams = new SortedDictionary<string, string> { ["app_id"] = _options.AppId, ["method"] = "alipay.trade.page.pay", ["charset"] = "utf-8", ["sign_type"] = "RSA2", ["timestamp"] = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"), ["version"] = "1.0", ["notify_url"] = _options.NotifyUrl, ["biz_content"] = JsonSerializer.Serialize(new { out_trade_no = order.OutTradeNo, total_amount = order.TotalFee / 100m, subject = order.Subject, product_code = "FAST_INSTANT_TRADE_PAY" }) }; var sign = AlipaySignature.Rsa2Sign(sortedParams, _options.PrivateKey); return PaymentResult.FromForm(BuildAutoSubmitForm(sortedParams, sign)); }

逻辑说明:支付宝的签名范围比微信复杂一些,需要对除 sign、sign_type 之外的参数按 key 做字典序拼接,用 key=value&key2=value2 拼出待签名字符串,最后用 RSA2 算法以商户私钥做 SHA256withRSA 签名。拼接时注意 biz_content 的值本身是一个 JSON 字符串,原样参与签名,不能重新序列化,否则签名对不上。

参数说明:total_amount 单位是元,所以这里做了除以 100 的转换,和微信用分正好相反。product_code 是固定值,不同产品线要换成对应的 code。alipay.trade.page.pay 是电脑网站支付的方法名,换成手机网站支付就要用 alipay.trade.wap.pay,核心流程不变,只是方法名与网关地址不同。

我一般会在生成表单前把请求参数打印到日志里,沙箱验签不通过时对照日志去改,比盯着文档猜快得多。支付宝沙箱环境的公钥和正式环境不同,切换环境时最容易漏改 AlipayPublicKey。

3.3 银联网关支付:证书签名与报文组装

银联在三者中接入方式最传统,保留了完整的证书体系:请求参数用签名证书签名,敏感字段用加密证书加密,响应结果用验签证书验证。源码里这个渠道文件明显比前两个长,就是因为前后处理步骤多。

public PaymentResult CreateOrder(PaymentOrder order) { var requestMap = new Dictionary<string, string> { ["version"] = "5.1.0", ["encoding"] = "UTF-8", ["signMethod"] = "01", ["txnType"] = "01", ["txnSubType"] = "01", ["bizType"] = "000201", ["channelType"] = "07", ["merId"] = _options.MerId, ["orderId"] = order.OutTradeNo, ["txnTime"] = DateTime.Now.ToString("yyyyMMddHHmmss"), ["txnAmt"] = order.TotalFee.ToString(), ["currencyCode"] = "156", ["frontUrl"] = _options.FrontUrl, ["backUrl"] = _options.NotifyUrl }; var sign = UnionpaySignature.Sign(requestMap, _options.SignCertPath, _options.SignCertPassword); requestMap["signature"] = sign; var html = UnionpayForm.Build(requestMap, _options.GatewayUrl); return PaymentResult.FromForm(html); }

逻辑说明:银联对参数名非常敏感,字段顺序无所谓,但字段名一个都不能错。txnType=01 表示消费,txnSubType=01 表示消费交易,bizType=000201 表示 B2C 网关支付,channelType=07 表示互联网渠道。这些属于枚举写死的值,基本没有按业务调整的空间。

参数说明:txnTime 的格式是 yyyyMMddHHmmss,第一次接的人经常按 yyyy-MM-dd HH:mm:ss 传,银联会返回签名错误或交易超时。txnAmt 单位是分。银联要求同一个商户下订单号唯一,且短时间内不能重复,测试环境建议订单号加上时间戳再拼随机数。

银联支付结果有两条返回通道:前台返回 frontUrl 和后台通知 backUrl。项目里两个地址都参与验签,但处理逻辑不同:前台返回直接渲染支付结果页,后台通知才真正修改订单状态。

4. 回调与订单状态同步:三套异步通知的收敛策略

支付下单只是上半场,真正的难点在回调解析。微信回调是 XML,支付宝回调是 form 表单键值对,银联回调是 JSON 串,各自验签方式也完全不一样。但验签之后,它们最终都必须汇合到同一个订单状态更新入口,否则状态会越走越乱。

4.1 统一回调入口与渠道分发

回调请求集中在 PaymentNotifyController 里,路由中带渠道标识,方便日志查询和问题排查。

[Route("api/pay/notify/{channel}")] public async Task<IActionResult> Notify(string channel) { string rawBody = await new StreamReader(Request.Body).ReadToEndAsync(); var channelImpl = _channelManager.GetByCode(channel); var result = channelImpl.ParseNotify(rawBody, Request.Headers.ToDictionary(x => x.Key, x => x.Value.ToString())); if (!result.Success) { return Content("failed", "text/plain"); } await _orderService.MarkPaidAsync(result.OrderId, result.ChannelTradeNo, result.PaidAt); return ChannelReply.Success(channel); }

逻辑说明:三个渠道的回调最终打到同一个 Controller 方法。先把请求体原样读出来,交给对应渠道实现去验签,验签失败返回 failed 文本,这是大多数渠道约定好的失败应答格式。验签通过后调 MarkPaidAsync 更新本地订单,最后按渠道要求返回成功应答。

参数说明:微信回调要求成功时返回包含 return_code 和 return_msg 的 XML;支付宝只需要返回 success 纯文本;银联要返回特定成功报文。所以 ChannelReply.Success 内部是按渠道分派的,不能统一都返回 success。另外读取 Request.Body 时必须显式声明 UTF-8,这一点在支付宝渠道上尤其重要,否则中文参数会在验签前就被破坏。

4.2 签名验证差异:三个渠道三种验签方式

验签部分的代码在源码包里分成三个独立文件,因为三个渠道的算法和验签素材完全不同。

渠道回调载体验签算法关键验签素材
微信XML 字符串MD5 + 商户 API Key参数拼接后跟 sign 字段比对
支付宝表单键值对RSA2(SHA256withRSA)支付宝公钥
银联JSON 字符串SHA256withRSA + 证书验签银联平台验签证书

以支付宝为例,回调验签代码大致是这样:

public PaymentNotifyResult ParseNotify(string rawBody, IDictionary<string, string> headers) { var form = ParseForm(rawBody); var sortedParams = form .Where(x => x.Key != "sign" && x.Key != "sign_type") .ToDictionary(x => x.Key, x => x.Value); var content = string.Join("&", sortedParams.OrderBy(x => x.Key) .Select(x => $"{x.Key}={x.Value}")); bool valid = AlipaySignature.VerifyRsa2(content, form["sign"], _options.AlipayPublicKey); ... }

逻辑说明:支付宝回调参数中,sign 字段存放签名,sign_type 只标识算法类型,这两个字段不能参与验签。其余字段按字典序排列后拼接待验签字符串,用支付宝公钥验证。最容易出错的点是 URL 解码,回调参数部分带编码,解码后再拼接,顺序错了就会验签失败。

微信回调验签思路类似,但细节不同:微信回调的参数包在 XML 里,解析之后所有字段除 sign 自身外都参与 MD5 签名,拼接结果加上 ApiKey 再做一次 MD5,与回调中的 sign 大写比较。银联回调验签依赖证书链:根据证书 ID 找到验签证书,用证书公钥验签,同时还需要对加密字段先解密再处理。三种验签逻辑建议各自封装,不要试图写一个通用方法兼容,否则代码会面目全非。

4.3 幂等处理与订单状态流转

回调渠道只保证尽力投递,不保证不重复。这个项目在 MarkPaidAsync 里做了两道防护:第一,按 out_trade_no 查出订单;第二,只有待支付状态才允许改成已支付。

public async Task MarkPaidAsync(string orderId, string channelTradeNo, DateTime paidAt) { var order = await _orderRepo.GetByOutTradeNo(orderId); if (order == null) return; if (order.Status != OrderStatus.WaitingPayment) { _logger.LogWarning("订单 {OrderId} 重复回调,当前状态 {Status}", orderId, order.Status); return; } order.Status = OrderStatus.Paid; order.ChannelTradeNo = channelTradeNo; order.PaidAt = paidAt; await _orderRepo.UpdateAsync(order); }

逻辑说明:这段代码的核心是状态机判断,只有等待支付状态下回调才真正改动订单。其他状态,比如已关闭、已退款,重复回调直接忽略。这样即使渠道重复通知三次,订单也只会被更新一次,后续的通知推送和账务记录不会重复执行。

为避免两个并发回调同时通过状态检查,项目里给订单表加了一个 version 字段做乐观锁,更新语句带 where version = 旧值,更新成功再把 version 加一。这种方案在单机场景足够,不需要引入分布式锁。等到退款场景,状态机里的待退款、退款中、已退款三个状态也是同样思路,保证每一笔退款只被处理一次。

4.4 主动查单与退款结果同步

回调并不是唯一的状态来源,掉单是常态。用户扫码后没支付、支付了但回调没送到、回调报文解析失败,这些场景都需要主动查单来兜底。项目里提供一个定时补偿任务,定时扫描超过五分钟还在待支付的订单,调渠道 QueryOrder 方法反查真实状态。

public async Task CompensatePendingOrdersAsync() { var pendingOrders = await _orderRepo.GetPendingOrders(TimeSpan.FromMinutes(5)); foreach (var order in pendingOrders) { var channel = _channelManager.GetByCode(order.ChannelCode); var queryResult = channel.QueryOrder(order.OutTradeNo); if (queryResult.TradeState == TradeState.Success) { await MarkPaidAsync(order.OutTradeNo, queryResult.ChannelTradeNo, queryResult.PaidAt); } } }

逻辑说明:补偿任务本质是主动查单,逻辑很简单但很关键。它真正解决的是回调丢失场景,也是我每接到一个支付需求就必然提醒自己补上的模块。QueryOrder 在渠道实现内部负责把各渠道的查单接口差异消化掉,比如微信的 orderquery、支付宝的 trade.query、银联的查询接口,最后统一返回 TradeState。

退款逻辑的处理类似。退款请求发起后,退款的异步结果也通过回调或主动查询拿回。项目里把退款结果绑定到原支付单,不单独建退款单表,退款状态存放在支付单的 RefundStatus 字段上。这样做的好处是对账简单,一笔支付单永远只有一条主记录,坏处是多次退款时字段不够用,如果业务明确支持拆单退款,建议还是单独建退款单表。

5. 避坑指南:证书、签名、编码与重复回调的踩坑记录

把项目跑起来不难,难在跨过第一次接入时绕不开的坑。这些是拆这个项目时实际遇到并且整理过的典型问题,按“现象、原因、解决”三个环节描述,适合拿来当排查手册。

5.1 微信退款抛出证书加载异常

现象:微信支付下单正常,退款接口一调就报客户端证书加载失败,本地开发环境尤其频繁。

原因:微信退款接口要求双向证书,需要加载 apiclient_cert.p12 客户端证书。很多项目只配置了 API 密钥,没有处理证书文件的读取路径和数据格式。IIS 部署时应用池账号没有证书文件读取权限,也会报同样错误。

解决:把证书文件放在项目外的固定目录,配置里用绝对路径指向,渠道初始化时手动构造 HttpClientHandler 绑定证书,不依赖系统证书库。另一个隐蔽坑是部分工具导出的 p12 私钥会多一个换行符,导致签名一直不对,建议用微信官方证书工具重新导出一次。

5.2 支付宝回调验签乱码

现象:支付宝回调里中文商品名变成乱码,验签一直通不过,订单状态卡在待支付。

原因:支付宝回调编码是 UTF-8,但 ASP.NET Core 读取请求体的默认编码或自定义中间件把它按其他编码解析了,字符串在验签前已经损坏。

解决:读取 Request.Body 时显式指定 Encoding.UTF8,解析 form 数据不要依赖 Request.Form 的自动解码,手动按 UTF8 解析原始字符串。源码里支付宝验签前还加了一步强制编码修正,用于兜底。

5.3 银联下单报错签名错误

现象:银联下单返回签名错误,证书、参数都和官方文档核对过,仍然失败。

原因:最常见是 txnTime 格式不对,或订单号短时间重复。另一个隐蔽原因是拼接待签名字符串时,value 里包含 & 和 = 没有转义,最终字符串和银联服务端不一致。

解决:拼接完成后统一对参数值做一次 URLEncode,先对值编码再拼接,不要拼完再整体编码。调试时把待签名字符串打印出来,跟银联官方 Demo 生成的结果逐字符比对,很快能定位差异。

5.4 回调重复通知导致订单被重复处理

现象:同一笔订单收到多次回调,系统里出现两条支付成功记录,对账不平。

原因:渠道为了确保送达会重试多次回调,业务侧没有做幂等控制,每收到一次就完整处理一遍。

解决:订单状态机加乐观锁,状态从待支付变成已支付后,后续回调直接返回成功应答但不再改状态。数据库给 out_trade_no 加唯一索引,就算状态判断漏了,唯一索引也能挡住重复插入。

5.5 前端轮询查单太频繁拖垮接口

现象:NATIVE 扫码支付时,前端页面每 1 秒轮询查单接口,支付高峰期数据库被打满。

原因:轮询口径太粗,查单直接查业务订单表,大量重复查询把数据库 IO 占满。

解决:查单接口改成先查缓存,缓存没有才回源数据库或调渠道接口。轮询间隔从 1 秒拉到 3 秒,超过 5 分钟未支付自动停止轮询,等用户刷新二维码页面再重新开启。这个改动对资源消耗的改善非常明显。

6. 二次开发:如何不改业务代码接进一个新支付渠道

真正有价值的框架一定允许低成本接入新渠道。下面以接一个模拟银行扫码渠道为例,看这个项目框架下扩展一个渠道的全过程和验证方法。

6.1 新增渠道实现并注册

新渠道只需要做三件事:实现 IPaymentChannel、增加配置 section、在渠道注册处补一行代码。核心在实体 MockBankChannel 的创建上。

public class MockBankChannel : IPaymentChannel { private readonly MockBankOptions _options; public string ChannelCode => "mockbank"; public MockBankChannel(IOptions<MockBankOptions> options) { _options = options.Value; } public PaymentResult CreateOrder(PaymentOrder order) { var payUrl = _options.GatewayUrl + "?orderId=" + order.OutTradeNo + "&amount=" + order.TotalFee; return PaymentResult.FromRedirectUrl(payUrl); } public PaymentNotifyResult ParseNotify(string channelRawData, IDictionary<string, string> headers) { // 模拟银行回调使用简单 token 验签,真实渠道需要替换成正式签名算法 return PaymentNotifyResult.Success(orderId, channelTradeNo, DateTime.Now); } }

逻辑说明:新增渠道后唯一强制要求是实现接口方法。CreateOrder 返回支付链接,通过 PaymentResult.FromRedirectUrl 区分于微信的 codeUrl 和支付宝的表单。注册渠道后配置加一个 MockBank section 并设为 Enabled,统一入口就能派发到新渠道。

我建议在 PaymentResult 里增加一个 PayMode 枚举标识展示方式——扫码、跳转链接、表单。这样前端不需要判断渠道,直接根据 PayMode 渲染载体。渠道越多,这个枚举带来的收益越明显。

6.2 用 mock 渠道跑通本地联调

没有真实商户号时,项目预留了 Mock 渠道,可以在本地不走三方网关模拟支付回调,联调不再依赖外网环境和真实账号。

[HttpPost("callback")] public async Task<IActionResult> Callback([FromBody] MockCallbackRequest request) { var raw = JsonSerializer.Serialize(request); // 直接调用渠道解析和状态更新 var channel = _channelManager.GetByCode("mockbank"); var parsed = channel.ParseNotify(raw, new Dictionary<string, string>()); await _orderService.MarkPaidAsync(parsed.OrderId, parsed.ChannelTradeNo, parsed.PaidAt); return Ok(); }

逻辑说明:mock 渠道的支付链接带上 out_trade_no,测试时浏览器访问支付链接后再调用这个 callback 接口,订单状态就会被推进到已支付。前后端、数据库、状态机在本地全链路打通,不需要真实商户号。这比每改一个字段都要跑到沙箱验证高效得多。

验证方法:本地起服务,用内网穿透工具把回调地址暴露成外网地址,渠道的 NotifyUrl 配置成该地址,就能完整走通“下单 → 回调 → 验签 → 状态更新”整条链路。

6.3 接真实渠道的验证清单

接真实渠道时,按这个顺序走一遍能避免上线后才发现问题:

  1. 下单请求参数先对照官方文档逐项核对,重点看单位、时间格式、编码三处。
  2. 沙箱环境验证签名、验签是否通过,生产密钥禁止写进测试环境代码。
  3. 回调接口在沙箱里用渠道后台主动发一次测试通知,确认验签、幂等、状态流转都符合预期。
  4. 用一笔小额真实支付走完整流程:下单、扫码或跳转、用户支付、回调、查单。
  5. 做一笔退款,确认退款回调和原支付单状态能对上。
  6. 检查线下日志,下单和回调两处必须记录原始报文,排查问题才不用猜。

这套流程里最容易被忽略的是退款回调。多数渠道退款结果也是异步通知,跟支付回调走同一个 Controller,如果状态机里没有把待退款、已退款两个状态卡好,退款通知漏掉一次账就对不平。从那以后我每次接新渠道,都强制走一遍下单、回调、退款、查单四步验证,并且把原始报文的日志级别全打开,确保扣款、入账、退款任何一步异常都能回答“钱去哪了”。希望这次的拆解过程能让你在接支付渠道时少走一轮弯路,把源码拉起来直接改。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询