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