TerraMours:Net7对接支付宝当面付

使用场景:

TerraMours开源项目之一:基于GPT与stable diffusion webui的开源项目:希望能够加入充值入口,并使用tokens数来扣费。

后台源码地址:https://github.com/TerraMours/TerraMours_Gpt_Api

一:先想清楚自己系统支付的逻辑。

​ 最开始是准备想着根据不同类别的会员来设置。后面感觉很难定价以及市场的波动,同时汇率的起伏。算了,还是类似于充值QB,然后基于金额来消费。这样类似于订好了单位一,也就是1 token多少钱,当前这个并没有解决汇率的问题。这个以7来计算的。

​ 简单的画个图,不一定严谨,方便理解即可

image-1692518128457

根据这个逻辑,基本上需要用到的技术也定了,由于后台需要推送,那么选择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