TerraMours:Net7对接支付宝当面付
TerraMours:Net7对接支付宝当面付
使用场景:
TerraMours开源项目之一:基于GPT与stable diffusion webui的开源项目:希望能够加入充值入口,并使用tokens数来扣费。
后台源码地址:https://github.com/TerraMours/TerraMours_Gpt_Api
一:先想清楚自己系统支付的逻辑。
最开始是准备想着根据不同类别的会员来设置。后面感觉很难定价以及市场的波动,同时汇率的起伏。算了,还是类似于充值QB,然后基于金额来消费。这样类似于定好了单位一,也就是1 token多少钱,当前这个并没有解决汇率的问题。这个以7来计算的。
简单的画个图,不一定严谨,方便理解即可
根据这个逻辑,基本上需要用到的技术也定了,由于后台需要推送,那么选择SignalR.
二:查看文档
官方地址:https://opendocs.alipay.com/common/02kkv3
由于官方demo,net的很旧,同时我也希望如果我要对接微信支付,我不需要在引入一个新的库,我希望是一个通用的,但是由于微信支付需要有商家资质,我是开源项目,同时是个人开发。所以只支持支付宝。
选择的开源库是:https://github.com/essensoft/paylink
基本上很简单,同时,项目也有示例代码。
三:对接支付
1.当面付预下单的二维码code接口
请求参数:
out_trade_no这个是自己内部系统的交易号,需要保证唯一。
subject: 主题,与英语邮件类似,英文邮件的主题就是subject,正文是body。可以理解为概要等等
body:具体描述,订单详细说明等
total_amount:这个订单的总金额.支付宝规定:订单总金额,单位为元,精确到小数点后两位,取值范围为 [0.01,100000000],金额不能为 0。
参数说明官方地址:https://opendocs.alipay.com/open/f540afd8_alipay.trade.precreate?pathHash=d3c84596&ref=api&scene=19
[Required] [Display(Name = "out_trade_no")] public string OutTradeNo { get; set; } [Required] [Display(Name = "subject")] public string Subject { get; set; } [Display(Name = "body")] public string Body { get; set; } [Required] [Display(Name = "total_amount")] public string TotalAmount { get; set; } [Display(Name = "notify_url")] public string NotifyUrl { get; set; }
MinimalApi
/// <summary> /// 当面付-扫码支付 /// </summary> [HttpPost] public async Task<IResult> PreCreate(AlipayTradePreCreateReq viewModel) { if (viewModel.UserId == null) { viewModel.UserId = _httpContextAccessor.HttpContext.User.FindFirstValue(ClaimTypes.UserData); } var res = await _payService.PreCreate(viewModel); return Results.Ok(res); }
PayService
/// <summary> /// 当面付-扫码支付 /// </summary> /// <param name="req"></param> /// <returns></returns> public async Task<ApiResponse<AlipayTradePrecreateResponse>> PreCreate(AlipayTradePreCreateReq req) { //生成系统内唯一交易号 var tradeNo = $"TerraMours-{Guid.NewGuid()}"; // 支付宝规定:订单总金额,单位为元,精确到小数点后两位,取值范围为 [0.01,100000000],金额不能为 0。 //但是我们系统是decimal 保留六位小数,这里由于充值我们是整数,所以我们只需要传给支付宝的钱处理下,保留两位小数即可 var model = new AlipayTradePrecreateModel { //系统内部的唯一交易号 $"TerraMours-{Guid.NewGuid()}" OutTradeNo = tradeNo, //类似于邮件主题 Subject = req.Name, //金额总数: 将商品价格转换为保留 2 位小数的 decimal 再转换为字符串 TotalAmount = Math.Round(req.Price, 2).ToString(), Body = req.Description }; var request = new AlipayTradePrecreateRequest(); request.SetBizModel(model); request.SetNotifyUrl(req.NotifyUrl); //此时应该先在自己的order表里面创建一个待支付的订单 var order = new Order(req.ProductId, req.Name, req.Description, req.Price, req.UserId, tradeNo); await _dbContext.Orders.AddAsync(order); await _dbContext.SaveChangesAsync(); var response = await _client.ExecuteAsync(request, _optionsAccessor.Value); if (!response.IsError) { return ApiResponse<AlipayTradePrecreateResponse>.Success(response); } return ApiResponse<AlipayTradePrecreateResponse>.Fail("支付宝当面付二维码生成失败"); }
appsettings:支付宝
注意:密钥格式:请选择 PKCS1(非JAVA适用),切记 切记 切记!!!
记得密钥转化成PKCS1格式的
AppId、AlipayPublicKey、AppPrivateKey 三个参数数据如何获取,自行百度。
// 支付宝 // 更多配置,请查看AlipayOptions类 "Alipay": { // 注意: // 若涉及资金类支出接口(如转账、红包等)接入,必须使用“公钥证书”方式。不涉及到资金类接口,也可以使用“普通公钥”方式进行加签。 // 本示例默认的加签方式为“公钥证书”方式,并调用 CertificateExecuteAsync 方法 执行API。 // 若使用“普通公钥”方式,除了遵守下方注释的规则外,调用 CertificateExecuteAsync 也需改成 ExecuteAsync。 // 支付宝后台密钥/证书官方配置教程:https://opendocs.alipay.com/open/291/105971 // 密钥格式:请选择 PKCS1(非JAVA适用),切记 切记 切记 // 应用Id // 为支付宝开放平台-APPID "AppId": "", // 支付宝公钥 RSA公钥 // 为支付宝开放平台-支付宝公钥 // “公钥证书”方式时,留空 // “普通公钥”方式时,必填 "AlipayPublicKey": "", // 应用私钥 RSA私钥 // 为“支付宝开放平台开发助手”所生成的应用私钥 "AppPrivateKey": "", // 服务网关地址 // 默认为正式环境地址 "ServerUrl": "https://openapi.alipay.com/gateway.do", // 签名类型 // 支持:RSA2(SHA256WithRSA)、RSA1(SHA1WithRSA) // 默认为RSA2 "SignType": "RSA2", // 应用公钥证书 // 可为证书文件路径 / 证书文件的base64字符串 // “公钥证书”方式时,必填 // “普通公钥”方式时,留空 "AppPublicCert": "", // 支付宝公钥证书 // 可为证书文件路径 / 证书文件的base64字符串 // “公钥证书”方式时,必填 // “普通公钥”方式时,留空 "AlipayPublicCert": "", // 支付宝根证书 // 可为证书文件路径 / 证书文件的base64字符串 // “公钥证书”方式时,必填 // “普通公钥”方式时,留空 "AlipayRootCert": "" }
2.查询订单支付状态
一:单个只做查询,没有逻辑,这个可以自己后台或者提供给用户自己查询订单的信息的接口
MinimalApi
/// <summary> /// 支付宝交易查询 /// </summary> [HttpPost] public async Task<IResult> Query(AlipayTradeQueryReq viewMode) { var res = await _payService.Query(viewMode); return Results.Ok(res); }
PayService
OutTradeNo:是自己系统生成的唯一交易号。
TradeNo:是支付宝那边的交易号,如果查询这个交易详细信息,传这两个的任意一个都可以查询。但是一开始没支付成功是没有这个的,需要后续自己查询之后再自己保存这个交易号到自己数据库。
/// <summary> /// 交易查询 /// </summary> /// <param name="req"></param> /// <returns></returns> public async Task<ApiResponse<AlipayTradeQueryResponse>> Query(AlipayTradeQueryReq req) { var model = new AlipayTradeQueryModel { OutTradeNo = req.OutTradeNo, //可以不传 TradeNo = req.TradeNo }; var request = new AlipayTradeQueryRequest(); request.SetBizModel(model); var res = await _client.ExecuteAsync(request, _optionsAccessor.Value); return ApiResponse<AlipayTradeQueryResponse>.Success(res); }
二:后台查询三分钟,通过SignalR向前端推送订单状态。
PaymentHub
using Essensoft.Paylink.Alipay; using Essensoft.Paylink.Alipay.Domain; using Essensoft.Paylink.Alipay.Request; using Essensoft.Paylink.Alipay.Response; using Microsoft.AspNetCore.Cors; using Microsoft.AspNetCore.SignalR; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Options; using TerraMours.Framework.Infrastructure.EFCore; using TerraMours_Gpt.Domains.PayDomain.Contracts.Req; using ILogger = Serilog.ILogger; using TerraMours_Gpt.Framework.Infrastructure.Contracts.Commons.Enums; namespace TerraMours_Gpt.Domains.PayDomain.Hubs { /// <summary> /// 支付相关的长连接 /// </summary> [EnableCors("MyPolicy")] public class PaymentHub : Hub { private readonly IAlipayClient _client; private readonly IOptions<AlipayOptions> _optionsAccessor; private readonly FrameworkDbContext _dbContext; private readonly Serilog.ILogger _logger; public PaymentHub(IAlipayClient client, IOptions<AlipayOptions> optionsAccessor, FrameworkDbContext dbContext, ILogger logger) { _client = client; _optionsAccessor = optionsAccessor; _dbContext = dbContext; _logger = logger; } /// <summary> /// 即时查询状态 /// </summary> /// <param name="req"></param> /// <returns></returns> public async Task QueryPaymentStatus(AlipayTradeQueryReq req) { _logger.Warning($"即时查询状态,订单号:{req.OutTradeNo}"); //获取当前连接id var connectionId = this.Context.ConnectionId; //先查询系统里面是否有此账单 // todo 支付成功 则修改用户vip信息 var order = await _dbContext.Orders.FirstOrDefaultAsync(x => x.OrderId == req.OutTradeNo) ?? throw new Exception("此订单不存在"); var model = new AlipayTradeQueryModel { OutTradeNo = req.OutTradeNo, //可以不传 TradeNo = req.TradeNo }; var request = new AlipayTradeQueryRequest(); request.SetBizModel(model); //循环查询3分钟这个账单,超时或者状态位已支付则停止 using var httpClient = new HttpClient(); var startTime = DateTime.Now; //先查一次 以免使用未赋值的变量 AlipayTradeQueryResponse queryPayRes = await _client.ExecuteAsync(request, _optionsAccessor.Value); _logger.Warning($"第一次查询返回:{req.OutTradeNo},交易状态:{queryPayRes.TradeStatus}"); while ((DateTime.Now - startTime).TotalMinutes <= 3) { queryPayRes = await _client.ExecuteAsync(request, _optionsAccessor.Value); if (queryPayRes.TradeStatus != "WAIT_BUYER_PAY") { _logger.Warning($"订单号状态改变:{req.OutTradeNo},交易状态:{queryPayRes.TradeStatus}"); //交易状态:WAIT_BUYER_PAY(交易创建,等待买家付款)、TRADE_CLOSED(未付款交易超时关闭,或支付完成后全额退款)、TRADE_SUCCESS(交易支付成功)、TRADE_FINISHED(交易结束,不可退款) break; } // 间隔3秒进行下一次查询 await Task.Delay(3000); } // 如果超过3分钟仍未支付,发送订单状态信息 order.PayOrder(queryPayRes.TradeNo, queryPayRes.TradeStatus); _dbContext.Orders.Update(order); await _dbContext.SaveChangesAsync(); //如果支付成功,则把user的余额加上新充值的钱 var user = await _dbContext.SysUsers.FirstOrDefaultAsync(x => x.UserEmail == order.UserId) ?? throw new Exception("此用户不存在"); user.Balance = user.Balance + order.Price; _dbContext.SysUsers.Update(user); await _dbContext.SaveChangesAsync(); var res = false; //交易状态:WAIT_BUYER_PAY(交易创建,等待买家付款)、TRADE_CLOSED(未付款交易超时关闭,或支付完成后全额退款)、TRADE_SUCCESS(交易支付成功)、TRADE_FINISHED(交易结束,不可退款) if (queryPayRes.TradeStatus == AlipayTradeStatusEnum.TRADE_SUCCESS.ToString()) { res = true; } //queryPayRes,前端只需要传是否成功即可 await Clients.Client(connectionId).SendAsync("QueryPaymentStatus", res); } } }
跳出循环之后,建议再查询一次。这样保证Delay的时间如果在三分钟边界上,可能会有支付成功,但是返回的是没有支付这种情况
总结
整体上来说,采用这个开源库,由于demo示例代码都有,基本上不难,需要注意的是,非Java需要先把密钥格式转化为PKCS1的密钥。当然这个系统对金钱以及安全要求都不高,只是单纯记录TerraMours相关项目的开发过程而已。详细代码可以看GitHub源码。前端源码也是开源,都在组织项目里面。
后台源码地址:https://github.com/TerraMours/TerraMours_Gpt_Api
前端源码地址:https://github.com/TerraMours/TerraMours_Gpt_Web
后台管理前端源码地址:https://github.com/TerraMours/TerraMours_Admin_Web