在线支付平台对资金安全与系统稳定性要求极高,使用C#构建此类系统时需要从架构分层、数据一致性和外部渠道对接三个维度综合考虑。下面以一个实际落地的ASP.NET Core项目为例,梳理关键设计点。

一、整体架构与网关抽象
在项目初期,我们直接将支付宝、微信支付的SDK调用散落在订单服务中,导致后续接入银联时改动了大量业务代码。后来引入了支付网关抽象层,把所有渠道统一为IPaymentGateway接口,业务侧只依赖抽象,不关心具体实现。
这种设计符合依赖倒置原则,也方便做单元测试。例如使用Mock网关即可在本地跑通下单、退款流程,而不用频繁调用真实渠道。同时,我们将签名、验签、报文组装等通用逻辑下沉到基类,各渠道只需实现独有的请求映射。
public interface IPaymentGateway
{
Task<PaymentResult> PayAsync(PaymentOrder order);
Task<RefundResult> RefundAsync(RefundRequest request);
bool VerifyNotify(string rawBody, string signature);
}
public abstract class BaseGateway : IPaymentGateway
{
protected string MerchantId { get; set; }
public abstract Task<PaymentResult> PayAsync(PaymentOrder order);
public abstract Task<RefundResult> RefundAsync(RefundRequest request);
public abstract bool VerifyNotify(string rawBody, string signature);
}
二、基于本地消息表的事务一致性
支付核心难题是“扣款成功但本地订单状态丢失”。我们采用本地消息表方案:在写入订单状态的同时,在同一数据库事务内插入一条待发送消息,由独立后台服务轮询并调用第三方API,成功后标记消息已发送。
借助EF Core的SaveChanges事务边界,可以保证业务数据与消息要么都提交、要么都回滚。相比引入重量级分布式事务框架,该方式在单一SQL Server实例上足够可靠,且排查问题只需看消息表状态。
using var tx = await dbContext.Database.BeginTransactionAsync();
var order = new Order { Status = OrderStatus.Paid };
dbContext.Orders.Add(order);
dbContext.OutboxMessages.Add(new OutboxMessage
{
Topic = "payment.success",
Body = JsonSerializer.Serialize(order),
Sent = false
});
await dbContext.SaveChangesAsync();
await tx.CommitAsync();
三、异步通知与验签处理
第三方支付通常通过异步回调通知结果,这里最容易被忽略的是验签和幂等。我们为每个网关实现VerifyNotify方法,用渠道公钥校验签名,并将通知号作为幂等键,避免重复更新。
在ASP.NET Core里,专门建立NotifyController接收不同渠道的回调,内部路由到对应Gateway。如果验签失败直接返回失败状态码,防止伪造请求篡改订单。对于处理成功的通知,立即返回渠道要求的success文本。
[HttpPost("notify/{channel}")]
public async Task<IActionResult> Notify(string channel, [FromBody] string body)
{
var gateway = gatewayFactory.Get(channel);
if (!gateway.VerifyNotify(body, Request.Headers["sign"]))
return BadRequest("invalid sign");
await mediator.Publish(new PaymentNotifyEvent(body));
return Ok("success");
}
四、用Polly增强调用韧性
调用外部支付接口时网络抖动难以避免。我们在网关HttpClient上集成Polly,配置重试与熔断策略:对于5xx或超时做有限次重试,连续失败则熔断一段时间,保护下游也保护自己。
具体做法是通过AddPolicyHandler注册策略,并将重试间隔设为指数退避。这样在大促期间某渠道不稳定时,系统能自动降级到备用渠道或排队,而不是雪崩。
var retry = Policy
.Handle<HttpRequestException>()
.OrResult<HttpResponseMessage>(r => !r.IsSuccessStatusCode)
.WaitAndRetryAsync(3, i => TimeSpan.FromSeconds(Math.Pow(2, i)));
services.AddHttpClient("pay")
.AddPolicyHandler(retry);
五、总结与避坑提醒
回顾整个项目,最大的收益来自清晰的网关抽象与本地消息表。切忌在业务代码里直接写死渠道SDK,也不要用单纯回调更新订单而不做幂等。资金类系统慢一点没关系,但必须准。
另外建议所有金额使用long型分单位存储,避免decimal浮点误差;日志中严禁打印卡号与密码,只记脱敏后的交易号。把这些基础规范固化到代码模板,团队扩展新支付渠道就能以天为单位交付。