接入服务商模式的支付宝支付

前言

笔者之前写过一篇《接入合作伙伴模式的微信支付》(传送门:mp.weixin.qq.com/s/9XFh_TWzu...),今天正好有机会完整操作了一遍基于合作伙伴模式(也叫服务商模式)的支付宝接入流程,顺手完整的记录一下。

支付宝的服务商模式和普通商户模式区别和微信那边类似,整个收款的流程也是服务商这边的应用发起支付请求,代替商家收款,最后资金也是直接进入到商家账户。

选择服务商模式的好处相比普通商户来说也是全方面的,除了费率更低,或者说可以申请成更低的费率,对商家和服务商来说都是一个减负的操作。

与服务商来说,可以在自己的服务商开放平台直接创建第三方应用,公钥私钥,网关地址什么的都是你自己管理,配置完成后,只需要邀请指定的商家授权就可以了,免去了反复和商家沟通的麻烦;

对商家来说,只需要按照实际需求提前和服务商沟通好需要开通哪些服务,如哪种支付方式,需不需要接入支付宝登录,要不要做一些红包之类的业务,然后服务商按商家需求,在发起授权时,勾选对应的权限,商家根据实际情况开通即可。

然后本篇介绍的所有相关的操作都是依据支付宝官方提供的文档,虽然现在是AI时代,且接入支付类产品的流程也相对成熟,但对还没有操作过这类产品的小伙伴来说门槛还是有的,因为开发的过程实际上很简单,现成的方案和思路相当多,而如果你没有一个对接入支付类产品的全景认识,那真开始做的时候,还是会一脸懵。

本篇就以开通一个"订单码"支付权限的第三方应用为例,从准备阶段一直到开发上线展开聊聊。

准备阶段

注意,这里的准备阶段是指正式开始接入支付宝,配置公钥私钥等流程,如果是开发阶段,则完全不需要先等这一步做完,可以利用支付宝提供的沙箱环境先把功能跑通,最后在配置也是ok的。篇幅限制,本篇不会过多介绍支付宝沙箱相关的内容

服务商方面

注意,虽然本节标题是"服务商方面",但多数操作都是在开放平台完成的,但从用户角度来说,我是作为软件提供商,所以是以服务商的角色开通开发平台并操作的。这里相信小伙伴们操作一般就理解了。

注册服务商

首先是先根据服务商平台入驻规则,将你所在的开发团队入驻成为服务商:p.alipay.com/page/settle...。这一步就是在官方网站操作,按引导完成即可,不再赘述。

注意:即便你的产品是自研的,使用服务商模式也是完全ok的,费率还可能更低,更省成本。应用集中在服务商控制台管理,商家在商家平台管理,后续如果你们的支付产品成熟到可以对外输出服务,也都是在服务商控制台进行管理,然后对外引导商家完成授权操作即可。

创建应用

登录开放平台后,创建第三方应用,填入一个合适的名字(注意按命名规范命名,不然审核不通过),绑定服务商账号。

开发设置

创建完应用后,平台会要求创建开发信息,这里面接口加签方式,不论应用是哪种方式创建的,都需要配置,而授权回调地址,是服务商模式需要额外配置,其他都是选填的。

早期开发阶段,建议先做最小化配置,等跑通开发流程后再来完善,比如服务器ip白名单,接口内容加密方式等,按需配置即可。注意接口内容加密之后,你在代码层面接受支付宝回调给你的信息时,也要做对应的操作,这一步不用有太多心智负担,多一层处理即可,对业务代码没有影响(文档:opendocs.alipay.com/common/02ms...

加签方式

接口加签是需要下载支付宝的密钥工具,加签方式有2种,如果你要用后续一些红包之类的服务,那就需要选证书的方式,我这里选择的是密钥方式。按软件指引,生成密钥对以后,把公钥上传上去即可,私钥自己保留。

这里需要说明的是,虽然我选择的是密钥方式,但现在支付宝主推的加签方式是证书,这个新项目的话建议优先考虑证书方式的加签方式。

注意

非Java开发语言,私钥要额外做一层转换,将默认的PCKS8转成PCKS1格式。

授权回调地址

服务商模式下,授权回调地址是需要创建的,这个是在商家完成应用授权后跳转到的地址,跳转时会携带应用授权码(app_auth_code)和appid等关键信息到你的回调地址,也就是说,你这个回调地址要能公开访问,不能做授权验证,然后要能接受这些参数。接收到这些参数后,需要请求支付宝接口,获取到应用授权令牌(app_auth_token),然后后续在请求支付宝相关接口的时候,都需要带着这个token才可以。这也是服务商模式比商家自研模式唯一多出来的一个开发步骤。(文档地址:opendocs.alipay.com/common/02qj...

其他非必填参数配置,这里不再赘述,按需配置即可,操作完成后就可以提交审核了

邀请商家授权

应用审核通过之后,就可以去邀请商家授权了,邀请的时候注意要把使用的产品服务勾选好,然后就可以发起授权了,授权完成后都会跳转到我们前面配置的那个授权回调地址,获取到应用令牌。

现在支付宝简化了这个授权流程,就是不论用的哪种授权方式,即便是最后没有完成跳转,在开放平台后端也能看到授权完成后的令牌。额怎么说呢,我觉得支付宝这样是对现实的妥协吧,算是给开发者兜底了,进一步降低了这个门槛,但即便如此我们最好还是按照文档指引把授权回调的相关流程做好,不能都指着平台兜底。

至此,服务商方面的操作就完成了,后续如果再有其他应用也要开通支付宝支付,就用同样的流程,在创建一个第三方应用即可,关键的应用配置都在服务商这边管理,但是要使用服务是需要商家授权的。这种模式的好处就是,双方管理上都是减负的,假如某一天,合作终止,商家既可以终止授权,服务商也可以主动解除,有需要就在开,毫无社交压力~~

商家方面

商家方面的操作就简单许多,前面我们邀请商家授权有3种方式,如果是直接输入商家id发起授权,商家登录商家平台后,会看到一个待办事项,点进去按指引完成操作即可,还可以用商家的支付宝账号扫描二维码,也可以直接点击我们根据授权文档生成的授权链接;

这里我以第一种方式为例,进入商家平台后点击待办事项

点击同意授权即可

这里有一点需要注意,如果我们发起的授权勾选的接口权限,商家这边没开通,就需要根据指引开通一下。

点击进入该链接👉:b.alipay.com/page/produc...,进入后,按下图所示,开通需要的产品;需要按指引上传相关材料,选择好经营类目,注意,每个经营类目对应的证照要保持一致,否则会被驳回:cshall.alipay.com/enterprise/...

这个是官方固定流程,没什么可说的,证照齐全即可。

开发阶段

应用授权接口

前面我们提到要设置的那个应用授权接口,这个是需要我们本地开发的,目的是自动获取授权完成后的应用授权令牌,还是那句话,虽然支付宝平台有兜底策略,但这个还是实际动手做一下比较好,开发文档在这里:opendocs.alipay.com/isv/04h3ue?...

开发文档里,只是给出的获取令牌的最小开发代码,是业务无关的,那这里我们就需要思考这个授权代码怎么更好的融入到业务系统里,按照官方的指引做一些本地化的改造和扩展即可。

这个代码官方案例已经足够清晰,我这里因为结合了业务所以也不太具备典型性,大概说一下我这里的逻辑,然后直接给出核心代码。

  1. 接受到支付宝授权回调后的参数,请求支付宝接口,换取app_auto_token
  2. 更新业务系统里的配置信息,把app_auth_token记录上,最关键
  3. 返回一个友好的提示页面给商家,让他知道他的操作结果

核心代码如下

csharp 复制代码
[HttpGet("Callback")]
[AllowAnonymous]
public async Task<IActionResult> Callback([FromQuery] string app_id, [FromQuery] string app_auth_code)
{
    var query = new AlipayAuthCallbackQuery
    {
        AppId = app_id,
        AppAuthCode = app_auth_code
    };
    if (string.IsNullOrEmpty(query.AppAuthCode))
    {
        return BadRequest("未获取到授权码 app_auth_code");
    }

    var paymentConfigResult = await _decPaymentConfigRepo.GetOneAsync(u => u.AlipayAppId == query.AppId);

    if (paymentConfigResult == null)
    {
        return BadRequest("未获取到支付配置");
    }

    // 拿到app_auth_code后,立即调用支付宝接口换取app_auth_token
    var result = await GetAppAuthTokenAsync(query.AppAuthCode, paymentConfigResult.Id);

    if (!result.IsSuccess)
    {
        // 生成一次性token并缓存失败结果
        var errorToken = Guid.NewGuid().ToString("N");
        var errorResult = new AuthResultCache
        {
            IsSuccess = false,
            ErrorMessage = result.ErrorMessage
        };
        await _easyCachingProvider.SetAsync($"AlipayAuthResult_{errorToken}", JsonHelper.Serialize(errorResult), TimeSpan.FromMinutes(5));

        return Redirect($"/web/alipay-auth-result.html?token={errorToken}");
    }
    // 生成一次性token并缓存成功结果
     var successToken = Guid.NewGuid().ToString("N");
     var successResult = new AuthResultCache
     {
         IsSuccess = true,
         MerchantAppId = result.Value.MerchantAppId,
         AppAuthToken = Encypt(result.Value.AppAuthToken), //注意这里要做一下加密处理,用户最后看到的也是密文,提供给我们的时候,再做解秘的处理
         AuthTime = result.Value.AuthTime.ToString("yyyy-MM-dd HH:mm:ss")
     };
     await _easyCachingProvider.SetAsync($"AlipayAuthResult_{successToken}", JsonHelper.Serialize(successResult), TimeSpan.FromMinutes(5));

    return Redirect($"/web/alipay-auth-result.html?token={successToken}");
}
/// <summary>
/// 验证一次性token并返回授权结果(供前端页面调用)
/// </summary>
[HttpGet("VerifyToken")]
public async Task<IActionResult> VerifyToken([FromQuery] string token)
{
    if (string.IsNullOrEmpty(token))
    {
        return Ok(new { success = false, message = "无效的访问" });
    }

    var cacheKey = $"AlipayAuthResult_{token}";
    var cachedResult = await _easyCachingProvider.GetAsync<string>(cacheKey);

    if (!cachedResult.HasValue || string.IsNullOrEmpty(cachedResult.Value))
    {
        return Ok(new { success = false, message = "链接已过期或无效" });
    }

    // 验证后立即删除,防止重复使用
    await _easyCachingProvider.RemoveAsync(cacheKey);

    var authResult = JsonHelper.Deserialize<AuthResultCache>(cachedResult.Value);
    return Ok(new { success = true, data = authResult });
}

上面GetAppAuthTokenAsync方法里包含了换取令牌的方法,代码就是参考的官方文档,此外还有一些业务代码在里面,这里不再赘述,最后授权完成后,用户会看到一个友好的结果页面,演示效果如下。

好了,至此,应用授权阶段的流程基本完成。

订单码支付

订单码支付相关的开发文档在这里:opendocs.alipay.com/open/05osux...

这个就类似微信支付里的Native支付方式,可以在自己的系统上生成收款二维码,而不需要跳转到第三方平台,融合性和操作体验,我认为是最好的。我这里接入支付宝的方式使用了paylinks这个插件,因为支付宝和微信的接口还是有区别的,如果我们用微信的服务商模式,是不能直接使用paylinks(v5.0.4版本)这个插件的,而支付宝不同我们只需要在所有的请求方法签名里,增加上那个授权令牌即可,其他都不需要变;

预下单接口

这里我把构造生成订单码的私有方法给出了

csharp 复制代码
private async Task<AlipayTradePreCreateResponse> GetAlipayPreCreateResponse(DecOrderResponseDto order)
{
    var alipayOption = JsonHelper.Deserialize<AlipayClientOptions>(order.DecPaymentConfig!.AlipayCfg);
    AlipayTradePreCreateBodyModel Input = new AlipayTradePreCreateBodyModel()
    {
        OutTradeNo = order.OrderInfo!.OutTradeNo,
        TotalAmount = order.OrderInfo!.TotalAmount.ToString(),
        Subject = order.OrderInfo!.Title,
        NotifyUrl = $"{ConfigurationHelper.GetSection("BaseHost").Value}/api/DecOrderNotify/AlipayResult/{order.DecPaymentConfig.Id}/{order.DecOrderId}"
    };

    var additionalCfg = JsonHelper.Deserialize<PaymentAdditionalDto>(order.DecPaymentConfig.AdditionalCfg);

    var request = new AlipayTradePreCreateRequest();
    request.SetBodyModel(Input);
    if (additionalCfg != null && !string.IsNullOrEmpty(additionalCfg.AlipayAuthToken))
    {
        return await _alipayClient.ExecuteAsync(request, alipayOption, additionalCfg.AlipayAuthToken);
    }
    return await _alipayClient.ExecuteAsync(request, alipayOption);
}

通知回调

支付完成后,要等待支付宝发送的回调通知,然后再更新本地订单的相关状态,和微信支付的流程一样,要保证外部状态变化大于内部,也就是确保支付宝平台订单已经完成支付,才能更新本地订单,并处理相关业务,比如给用户发送通知。关键代码如下

csharp 复制代码
 [AllowAnonymous]
 [HttpPost("AlipayResult/{paymentId:long}/{orderId:long}")]
 public async Task<IActionResult> AlipayResult(long paymentId, long orderId)
 {
     // 前置请求签名验证部分省略
     try
     {
         var parameters = await Request.GetAlipayParametersAsync();
         var option = await GetAlipayConfig(paymentId);
         if (option == null)
         {
             _logger.LogError("支付宝支付配置无效");
             await EmailKitHelper.SendEMailToManagerMsgAsync($"支付宝支付配置无效,paymentId:{paymentId};\r\n支付宝返回内容,parameters:{parameters}");
             return AlipayNotifyResult.Fail;
         }
         var notify = await _alipayNotifyClient.ExecuteAsync<AlipayTradeStatusSyncNotify>(parameters, option);
         // 更新订单状态,操作日志等
         var result = await RecordAliPay(notify, parameters, orderId);

         if (result.IsSuccess)
         {
             await _capPublisher.PublishAsync(CapConsts.ApiPrefix + "SendOrderNotice", new NotceOrderModel()
             {                        
                 orderId = orderId,
                 content = $"您的订单(编号:【{notify.OutTradeNo}】)已支付完成,实收金额为【{notify.ReceiptAmount}元】,关联的申报信息如下:\r\n"
             });
             await _easyCachingProvider.TrySetAsync($"orderNotify_{orderId}",notify.TradeStatus,TimeSpan.FromMinutes(2));
         }
         return AlipayNotifyResult.Success;
     }
     catch (AlipayException ex)
     {
         _logger.LogError(ex.Message);               
         return AlipayNotifyResult.Fail;
     }
 }

前两步有一个之前录制的gif,依赖的是支付宝沙箱配置,和正式配置流程是大体相同的。包括生成预下单二维码,扫码支付后完成回调,以及退费,效果如下

这里要说明的是,沙箱环境仅能用在支付场景下,前面的授权场景是不可以用沙箱来测试的,

手动对账

手动对账就是避免前面自动回调因为一些不可预测的原因没有及时完成甚至失败,或者用户很快就想确认订单是否完成,那也可以手动发起对账请求,逻辑上还是先确定外部状态后,再更新内部,私有代码如下。

csharp 复制代码
private async Task<AlipayTradeQueryResponse> GetAlipayTradeQueryResponse(DecOrderResponseDto order)
{
    var alipayOption = JsonHelper.Deserialize<AlipayClientOptions>(order.DecPaymentConfig!.AlipayCfg);

    AlipayTradeQueryBodyModel model = new AlipayTradeQueryBodyModel()
    {
        OutTradeNo = order.OrderInfo!.OutTradeNo
        };

    var additionalCfg = JsonHelper.Deserialize<PaymentAdditionalDto>(order.DecPaymentConfig.AdditionalCfg);

    var request = new AlipayTradeQueryRequest();
    request.SetBodyModel(model);
    return await _alipayClient.ExecuteAsync(request, alipayOption, additionalCfg.AlipayAuthToken);
}

取消订单

这里我再额外多说一个关于取消订单的情况,实际场景中,会出现大量下单之后,没有支付的情况,这种情况,不能完全依赖用户按流程操作自动取消,而是要有一个兜底的定时任务,将这些过期的订单及时的清理掉,这里篇幅限制不再给出定期任务相关的代码,dotnet技术栈下,有多种选择,我是用的是coravel的方案;

csharp 复制代码
 private async Task<bool> CloseAlipayOrder(DecOrderSimpleResponseDto order)
 {
     var alipayOption = JsonHelper.Deserialize<AlipayClientOptions>(order.AliPaymentConfig);

     AlipayTradeCloseBodyModel model = new AlipayTradeCloseBodyModel()
     {
         OutTradeNo = order.TradeNo
     };
     var request = new AlipayTradeCloseRequest();
     request.SetBodyModel(model);
     PaymentAdditionalDto paymentAdditionalDto = new PaymentAdditionalDto() { AlipayAuthToken = string.Empty };

     if (!string.IsNullOrWhiteSpace(order.AdditionalCfg))
     {
         paymentAdditionalDto = JsonHelper.Deserialize<PaymentAdditionalDto>(order.AdditionalCfg);
     }
     var response = await _alipayClient.ExecuteAsync(request, alipayOption, paymentAdditionalDto.AlipayAuthToken);
     return response.IsSuccessful;
 }

退款

csharp 复制代码
private async Task<bool> RefundForAlipay(long configId, ViewDecOrder item)
{
    var allOptions = await GetAlipayConfig(configId);
    // 退款单号规则,R+原单号
    string refundNumber = $"R{item.TradeNo}";
    var Input = new AlipayTradeRefundBodyModel
    {
        RefundAmount = item.Amount.ToString(),
        OutTradeNo = item.TradeNo,
        RefundReason = $"{item.Title}退款",
        OutRequestNo = refundNumber,
    };

    var request = new AlipayTradeRefundRequest();
    request.SetBodyModel(Input);

    var alipayOptions = JsonHelper.Deserialize<AlipayClientOptions>(allOptions.Cfg);
    var additionalCfg = new Application.Dtos.Custom.PaymentAdditionalDto() { AlipayAuthToken = string.Empty };

    if (string.IsNullOrWhiteSpace(allOptions.AdditionalCfg))
    {
        additionalCfg = JsonHelper.Deserialize<Application.Dtos.Custom.PaymentAdditionalDto>(allOptions.AdditionalCfg);
    }
    var response = await _AlipayClient.ExecuteAsync(request, alipayOptions!, additionalCfg.AlipayAuthToken);
    if (response != null && response.FundChange == "Y")
    {
        return await ConfirmAlipayRefundStatus(item.Id, refundNumber, alipayOptions, additionalCfg.AlipayAuthToken);
    }
    return false;
}

确认退款状态

确认退款状态和前面确认支付状态的逻辑一样,也是要确认外部平台状态先完成变更,内部业务在进行修改;

csharp 复制代码
private async Task<bool> ConfirmAlipayRefundStatus(long orderId, string refundNumber, AlipayClientOptions? alipayOptions, string? authToken)
{
    const int maxRetries = 5;
    const int retryIntervalMs = 3000;

    Snackbar.Add("退款已提交,正在等待平台确认...", Severity.Info);

    for (int i = 0; i < maxRetries; i++)
    {
        await Task.Delay(retryIntervalMs);

        var order = await _decOrderProvider.GetOneAsync(u => u.Id == orderId);
        if (order == null) return false;

        // 回调可能已抢先完成
        if (order.OrderStatus == OrderStatus.Closed)
        {
            Snackbar.Add("退款已完成", Severity.Success);
            return true;
        }

        var Input = new AlipayTradeFastPayRefundQueryBodyModel()
        {
            OutRequestNo = refundNumber,
            OutTradeNo = order.TradeNo
        };
        List<String> queryOptions = new List<String>();
        queryOptions.Add("refund_detail_item_list");
        Input.QueryOptions = queryOptions;

        var request = new AlipayTradeFastPayRefundQueryRequest();
        request.SetBodyModel(Input);

        var response = await _AlipayClient.ExecuteAsync(request, alipayOptions!, authToken);
        if (response != null && !string.IsNullOrEmpty(response.RefundStatus) && response.RefundStatus.Equals("REFUND_SUCCESS"))
        {
            var refundResult = await _decOrderProvider.NotifyRefundOrderPassive(orderId, refundNumber);
            if (refundResult.IsSuccess)
            {
                Snackbar.Add("退款操作完成,订单状态已修改", Severity.Success);
            }
            else
            {
                Snackbar.Add(refundResult.ErrorMessage, Severity.Error);
            }
            return true;
        }
    }

    // 重试耗尽但退款API已受理,回调会完成后续逻辑
    Snackbar.Add("退款已提交,平台处理中,请稍后刷新页面查看", Severity.Info);
    return true;
}

总结

好了,基本就是这些,开发方面的东西其实很少,主要还是前面的准备工作要做好,技术上没有难点。最后给出一些引用材料的地址

相关推荐
八苦1 小时前
MCP 企业托管授权(Enterprise-Managed Authorization)
后端
用户938515635071 小时前
Node.js 路径与文件系统:从基础到异步进化之路
javascript·后端·面试
一tiao咸鱼2 小时前
前端转 agent # 03 - Pydantic v2 数据校验深入
前端·后端
一路向北North2 小时前
Spring Security OAuth2.0(18):资源服务测试
java·后端·spring
用户99045017780092 小时前
若依工作流主流方案对接
后端
触底反弹2 小时前
🚀 Node.js path 和 fs 模块,从回调地狱到 async/await 的进化之路!
javascript·后端·node.js
Csvn3 小时前
Python 开发技巧 · 生成器(Generator)进阶 —— 从 yield 到 yield from,写出省内存的生产者-消费者模式
后端·python
geovindu3 小时前
CSharp: Dijkstra Algorithms
开发语言·后端·算法·c#
掘金者阿豪4 小时前
听书别再被会员和广告打断:把NAS变成自己的私人有声图书馆
后端