简介:本资源面向.NET开发者与支付系统集成人员,聚焦在C#.NET环境下统一接入微信、支付宝与银联三大支付渠道,解决多平台支付接口分散、回调处理复杂、安全合规难把控等实际问题,适合具备一定C#基础、需要落地线上支付功能的中高级开发者参考。压缩包为zip格式,整体约52.91MB,文件总数与类型明细上游未提供,从内容看核心围绕Payment.Api支付服务接口层展开,封装了三种支付方式的交互逻辑,对外提供统一调用入口。资源重点覆盖微信JSAPI、Native、H5、APP等多场景接口,支付宝网页、APP与扫码支付,以及银联网上支付、快捷支付、查询退款撤销等操作,并涉及SDK引入、预支付订单生成、异步回调验证、敏感信息加密与沙箱测试等关键环节。目前已有1495人学习下载,可帮助读者理解统一支付API层的设计思路,掌握回调通知处理、错误机制与合规注意事项,为构建稳定可靠的支付模块提供可借鉴的工程结构。
1. 一个支付整合项目,为什么值得花时间拆
做过电商或 SaaS 的朋友大概都有过这种体验:微信支付刚调通,产品说支付宝也要上;支付宝上线没两周,财务又要求接银联企业网银。三套 SDK、三套签名规则、三套回调格式,代码里到处是if (channel == "wxpay")的分支,改一处崩三处。我手上这份 C#.NET 支付整合项目,就是冲着这个痛点来的——它把微信支付、支付宝、银联三条通道收敛到一套统一接口下,用工厂加策略的方式做路由,新增通道时业务层几乎不用动。项目基于 .NET 6 的 ASP.NET Core Web API 搭建,适合正在做聚合支付、多商户收银台,或者想系统学习支付网关设计的开发者。下面我按「能干什么 → 怎么跑 → 怎么改」的顺序,把这份源码拆开讲。
2. 架构与目录:三条支付通道怎么收敛成一套接口
2.1 整体分层与项目结构
这个项目最值得先看的是分层。它没有把三个支付的逻辑堆在一个 Controller 里,而是按「接口层 → 应用层 → 通道实现层 → 基础设施层」切得很干净。核心思路是:业务代码只依赖抽象,具体走哪条通道由运行时决定。
目录大致长这样:
PaymentGateway/ ├── PaymentGateway.Api/ # Web API 入口,控制器与中间件 │ ├── Controllers/ │ │ └── PaymentController.cs # 统一支付入口 │ └── Program.cs # 依赖注入与管道配置 ├── PaymentGateway.Core/ # 领域层:接口、模型、枚举 │ ├── Interfaces/ │ │ ├── IPaymentService.cs # 统一支付抽象 │ │ └── IPaymentChannel.cs # 通道抽象 │ ├── Models/ │ │ ├── PaymentRequest.cs │ │ ├── PaymentResult.cs │ │ └── RefundRequest.cs │ └── Enums/ │ └── PaymentChannel.cs # WeChat / Alipay / UnionPay ├── PaymentGateway.Channels/ # 各通道具体实现 │ ├── WeChat/ │ │ ├── WeChatPayChannel.cs │ │ └── WeChatSignHelper.cs │ ├── Alipay/ │ │ ├── AlipayChannel.cs │ │ └── AlipaySignHelper.cs │ └── UnionPay/ │ ├── UnionPayChannel.cs │ └── UnionPaySignHelper.cs ├── PaymentGateway.Infrastructure/ # 配置、日志、HTTP 客户端 │ ├── Config/ │ └── Http/ └── PaymentGateway.Tests/ # 单元测试这个结构的好处是:Core层不引用任何第三方支付 SDK,通道实现层才去引微信、支付宝、银联的官方库。将来某个通道要升级 SDK 版本,改动被隔离在Channels目录里,不会污染业务逻辑。常见做法是把IPaymentChannel定义成异步接口,返回统一的PaymentResult,这样上层调用时完全感知不到底层差异。
2.2 统一支付抽象接口设计
先看最关键的抽象。整个项目的「契约」就在IPaymentService和IPaymentChannel这两个接口上:
// Core/Interfaces/IPaymentChannel.cs public interface IPaymentChannel { // 该通道支持的支付方式标识 PaymentChannel Channel { get; } // 统一下单:返回给前端的支付参数(二维码/跳转链接/表单) Task<PaymentResult> CreateOrderAsync(PaymentRequest request, CancellationToken ct = default); // 查询订单状态 Task<PaymentResult> QueryOrderAsync(string outTradeNo, CancellationToken ct = default); // 退款 Task<RefundResult> RefundAsync(RefundRequest request, CancellationToken ct = default); // 处理异步回调,返回给第三方的应答内容 Task<string> HandleNotifyAsync(HttpContext context); }PaymentChannel是个枚举,只有WeChat、Alipay、UnionPay三个值。PaymentRequest里放的是通道无关的字段:商户订单号OutTradeNo、金额Amount(统一用「分」为单位,避免浮点误差)、商品描述Subject、回调地址NotifyUrl。各通道实现类在内部把这些通用字段翻译成自己 SDK 需要的格式。
这里有个设计细节值得说:金额统一用long存「分」,而不是decimal存「元」。微信和银联的接口本来就以分为单位,支付宝虽然用元,但在通道实现里做一次转换即可。这样能彻底避开0.1 + 0.2 != 0.3这类玄学问题,血泪经验。
2.3 通道工厂与依赖注入
有了抽象,接下来要解决「怎么根据请求选通道」。项目用的是工厂 + DI 的组合:
// Channels/PaymentChannelFactory.cs public class PaymentChannelFactory : IPaymentChannelFactory { private readonly IEnumerable<IPaymentChannel> _channels; // 构造函数注入所有已注册的通道实现 public PaymentChannelFactory(IEnumerable<IPaymentChannel> channels) { _channels = channels; } public IPaymentChannel Resolve(PaymentChannel channel) { var impl = _channels.FirstOrDefault(c => c.Channel == channel); if (impl is null) throw new NotSupportedException($"未注册的支付通道: {channel}"); return impl; } }然后在Program.cs里把三个通道都注册进去:
// Program.cs 片段 builder.Services.AddScoped<IPaymentChannel, WeChatPayChannel>(); builder.Services.AddScoped<IPaymentChannel, AlipayChannel>(); builder.Services.AddScoped<IPaymentChannel, UnionPayChannel>(); builder.Services.AddScoped<IPaymentChannelFactory, PaymentChannelFactory>(); builder.Services.AddScoped<IPaymentService, PaymentService>();IEnumerable<IPaymentChannel>是 .NET DI 的一个实用特性:注册多个同接口实现后,注入IEnumerable<T>就能拿到全部。工厂在构造时一次性收集,Resolve时按枚举匹配。这样新增通道只要写一个实现类、加一行注册,工厂和业务层都不用改,符合开闭原则。控制器里最终就变成这样一句:
var channel = _factory.Resolve(request.Channel); var result = await channel.CreateOrderAsync(request);业务代码干净得不像话,这就是抽象到位的样子。
3. 跑起来:配置、下单、回调的完整链路
3.1 环境准备与配置文件
项目基于 .NET 6,跑之前先确认本机装了对应 SDK,dotnet --version能输出 6.x 即可。数据库不是必须的,示例里订单状态用内存字典模拟,方便你先把链路跑通再换持久化。
配置集中在appsettings.json,三个通道各占一段:
{ "Payment": { "WeChat": { "AppId": "your_appid", "MchId": "your_mchid", "ApiKey": "your_apiv3_key", "CertPath": "certs/wechat/apiclient_cert.p12", "NotifyUrl": "https://your-domain/api/payment/notify/wechat" }, "Alipay": { "AppId": "your_appid", "PrivateKey": "certs/alipay/private_key.pem", "AlipayPublicKey": "certs/alipay/alipay_public_key.pem", "NotifyUrl": "https://your-domain/api/payment/notify/alipay" }, "UnionPay": { "MerId": "your_merid", "CertPath": "certs/unionpay/", "NotifyUrl": "https://your-domain/api/payment/notify/unionpay" } } }参数说明:NotifyUrl必须是公网可访问的 HTTPS 地址,本地调试时用内网穿透工具映射出去(这是支付调试绕不开的一步)。证书路径建议放在项目外的绝对路径,别提交进 Git。微信的ApiKey是 APIv3 密钥,不是早期的 MD5 密钥,两者签名算法完全不同,配错会一直报签名失败。
3.2 统一下单接口调用
控制器暴露一个统一入口,前端传通道类型和订单信息:
// Controllers/PaymentController.cs [ApiController] [Route("api/[controller]")] public class PaymentController : ControllerBase { private readonly IPaymentChannelFactory _factory; public PaymentController(IPaymentChannelFactory factory) => _factory = factory; [HttpPost("create")] public async Task<IActionResult> Create([FromBody] PaymentRequest request) { // 参数校验:金额必须大于 0,订单号不能为空 if (request.Amount <= 0 || string.IsNullOrWhiteSpace(request.OutTradeNo)) return BadRequest("参数不合法"); var channel = _factory.Resolve(request.Channel); var result = await channel.CreateOrderAsync(request); // result.PayParams 直接返回给前端调起支付 return Ok(result); } }PaymentResult里最关键的是PayParams字段,它是个字典,装的是各通道调起支付所需的参数。微信返回的是prepay_id加签名,支付宝返回的是一段表单 HTML 或跳转 URL,银联返回的是表单。前端拿到后按通道类型分别处理即可。这里要注意:下单接口本身不落库,订单持久化建议放在调用CreateOrderAsync之前,否则回调来了找不到订单,这是新手最容易翻车的地方。
3.3 异步回调的统一处理
支付回调是整条链路里最容易被忽视、又最容易出问题的部分。三个通道的回调格式完全不同:微信是 XML 或 JSON 加密串,支付宝是表单 POST,银联是表单加签名。项目在通道实现里各自解析,但对外暴露统一的HandleNotifyAsync:
// WeChat/WeChatPayChannel.cs 回调处理片段 public async Task<string> HandleNotifyAsync(HttpContext context) { using var reader = new StreamReader(context.Request.Body); var body = await reader.ReadToEndAsync(); // 1. 验签:用平台证书验证回调来源 if (!WeChatSignHelper.VerifyNotify(body, _options.ApiV3Key)) return WeChatNotifyResponse.Fail("验签失败"); // 2. 解密 resource 字段拿到订单信息 var notify = WeChatSignHelper.DecryptResource(body, _options.ApiV3Key); // 3. 幂等处理:同一笔订单重复回调只处理一次 if (!await _orderService.TryMarkPaidAsync(notify.OutTradeNo)) return WeChatNotifyResponse.Success(); // 已处理过,直接应答成功 // 4. 更新订单状态、发货等业务逻辑 await _orderService.CompleteOrderAsync(notify.OutTradeNo, notify.TransactionId); // 5. 必须返回通道要求的成功应答,否则会一直重推 return WeChatNotifyResponse.Success(); }逻辑说明:第一步验签是安全底线,绝不能省;第二步解密拿到真实订单号;第三步幂等是重点——微信和支付宝都会重复推送回调,如果没做幂等,用户可能被重复发货。TryMarkPaidAsync用数据库唯一约束或 Redis 分布式锁实现,保证同一订单只处理一次。第五步的应答格式每个通道不一样,微信要返回特定 JSON,支付宝要返回纯文本success,银联要返回特定 XML,返回错了对方会认为你没收到,持续重推。
提示:本地调试回调时,先把
NotifyUrl指向一个能打印请求体的临时接口,确认参数结构后再写解析逻辑,比对着文档猜格式高效得多。
4. 避坑与排查:支付联调最常见的五个坑
4.1 签名失败但参数看着都对
现象:调微信下单接口,返回「签名错误」,但对照文档逐字段检查,参数一个不差。
原因:八成是签名串拼接时字段顺序或空值处理不对。微信 APIv3 的签名要求把参与签名的字段按字典序排列,用\n拼接,末尾再拼一次\n,任何一处多了空格或漏了换行都会失败。另外Authorization头里的timestamp和nonce_str必须和签名时用的一致。
解决:把待签名字符串先Console.WriteLine出来,和官方文档的示例逐字符比对。常见做法是封装一个BuildSignString方法,所有通道共用,避免每个地方手拼。
4.2 回调一直重推,订单状态却没变
现象:日志里看到回调请求反复进来,但订单状态始终是「待支付」。
原因:要么是应答格式不对(对方认为你没成功接收),要么是业务处理抛异常被吞掉了。还有一种情况是回调地址在内网,第三方根本访问不到,请求压根没进来。
解决:先在回调入口打日志确认请求是否到达;到达了就看应答内容是否符合通道要求;业务异常要捕获并记录,别让异常冒泡导致返回 500。幂等逻辑要放在业务处理之前。
4.3 金额对不上,差几分钱
现象:订单金额 99.99 元,回调回来变成 9999 分,或者反过来差 1 分。
原因:元与分的转换用了浮点数。(long)(99.99 * 100)在某些情况下会得到 9998,因为浮点精度问题。
解决:全程用整数分存储和传递,只在展示层做除法。转换时用decimal而非double,或者干脆约定接口层就传分。这个坑我在两个项目里都踩过,现在一律强制走整数。
4.4 证书路径在开发机能用,部署后报错
现象:本地跑得好好的,发布到服务器后微信支付报「证书加载失败」。
原因:证书路径写成了相对路径,或者用了开发机的绝对路径。另外 .p12 证书在 Linux 上加载需要额外的权限设置。
解决:证书路径从配置读取,部署时通过环境变量覆盖;Linux 下确认文件权限是600且属主是运行进程的用户。常见做法是把证书放在容器挂载卷里,不打包进镜像。
4.5 并发下单导致订单号重复
现象:压测时偶发「订单号已存在」或两笔订单互相覆盖。
原因:订单号生成用了时间戳,高并发下同一毫秒生成相同单号。
解决:订单号用「时间戳 + 商户号 + 随机数 + 序列号」组合,或者直接用雪花算法。数据库层面给OutTradeNo加唯一索引兜底,插入冲突时重试。
5. 二次开发:新增通道与改造的实操思路
拿到这份源码后,最常见的二次开发需求是「再加一个支付通道」,比如某银行直连或者数字人民币。按这个项目的设计,新增通道其实很轻,我一般按下面四步走。
第一步,在PaymentChannel枚举里加一个值,比如DigitalRmb。第二步,在Channels目录下新建对应文件夹,实现IPaymentChannel接口的四个方法。第三步,在Program.cs里加一行AddScoped<IPaymentChannel, DigitalRmbChannel>()。第四步,在appsettings.json里补上该通道的配置节。工厂和控制器完全不用动,这就是抽象带来的红利。
不过有几个改造点值得提前想清楚。一是签名逻辑的复用:三个通道的签名虽然算法不同,但「拼接待签串 → 计算 → 编码」的骨架是一样的,可以抽一个SignHelperBase出来,各通道只实现BuildSignContent和ComputeHash两个方法。二是回调路由:现在每个通道一个回调地址,如果通道多了,可以统一成一个入口,按请求特征或路径参数分发。三是订单状态机:示例里状态比较简单,真实业务里「待支付 → 支付中 → 已支付 → 已退款」的流转建议用状态机管理,避免非法流转。
验证改造是否成功,我习惯写一个最小闭环测试:下单 → 拿到支付参数 → 模拟回调 → 查订单状态变成已支付 → 发起退款 → 状态变成已退款。这五步跑通,通道基本就没问题了。单元测试里可以用WebApplicationFactory起一个内存服务,把通道实现替换成 Mock,专门测业务逻辑,不依赖真实支付接口。
还有个技巧:把每个通道的请求和响应都记一份结构化日志,字段包括通道、订单号、耗时、结果码。支付问题排查时,这份日志就是后悔药。我一般用ILogger加一个PaymentLogScope,在通道实现里统一埋点,出问题时按订单号一搜,整条链路清清楚楚。
从那以后我每次接新支付通道,都强制先跑一遍「下单 → 回调 → 退款」的最小闭环,再往业务里集成,省得在复杂业务里定位问题。希望这份拆解帮到你,源码拿过去先跑通链路,再按自己的业务改,比一上来就大改结构稳妥得多。
本文还有配套的精品资源,点击获取