Java商城如何接入支付宝支付:配置流程与回调处理示例

科技资讯3小时前发布 jdkjadmin
0 00
Java商城如何接入支付宝支付:配置流程与回调处理示例

Java商城如何接入支付宝支付:配置流程与回调处理示例

本文以 Java 商城接入支付宝电脑网站支付为例,介绍从开放平台配置、创建支付请求,到处理异步通知的完整流程。示例使用支付宝 Java SDK、RSA2 和 Spring MVC。手机网站支付、App 支付、小程序支付的产品配置和调用接口并不相同,不应直接照搬电脑网站支付接口。

支付接入的关键不只是“生成一个收银台链接”,还包括订单金额由谁决定、通知如何验真、重复通知如何处理,以及用户浏览器返回时怎样避免误判支付成功。下面先说明边界,再给出代码。

一、适用场景与处理边界

电脑网站支付适用于用户在电脑浏览器的商城页面发起付款,由后端请求支付宝电脑网站支付接口,前端跳转到支付宝收银台。支付宝完成支付后,可以向商户配置的异步通知地址发送结果;用户也可能被浏览器带回商户的同步返回地址。

  • 异步通知(notify_url):服务器到服务器的通知,用于可靠地更新订单状态。应验签并核对订单、金额和商户信息。
  • 同步返回(return_url):浏览器跳转回商户页面,适合展示结果或引导用户查询订单,不适合作为付款成功的唯一依据。
  • 订单状态查询:遇到通知延迟、网络异常或状态不确定时,商户后端可以使用支付宝查询接口核实交易状态。不要根据前端页面参数自行认定支付成功。

下例仅处理支付,不包含退款、关单、分账等流程。这些业务有各自的接口和状态约束。接入前应在支付宝开放平台开通对应支付产品,并以当前产品文档为准核对接口权限、字段规则和通知行为。

二、准备应用和密钥

  1. 在支付宝开放平台创建应用,按平台要求完成应用配置和上线流程,开通电脑网站支付产品。
  2. 为应用配置商户侧 RSA2 应用私钥所对应的应用公钥。商户保存应用私钥,向平台提交应用公钥;平台提供支付宝公钥,商户用它验证支付宝签名。
  3. 准备应用 App ID、应用私钥、支付宝公钥和网关地址。正式环境网关通常为 https://openapi.alipay.com/gateway.do;沙箱应使用沙箱环境对应的网关、应用和密钥,不能混用正式与沙箱配置。
  4. 配置公网可访问的 HTTPS 异步通知地址。它需要由商户服务器接收;开发机本地地址通常无法被支付宝直接访问。

应用私钥是敏感凭据,只能保存在后端安全配置中,例如密钥管理服务、受控的环境变量或受保护的配置文件。不要提交到代码仓库、放进前端代码或返回给浏览器。支付宝公钥用于验签,不是应用私钥,也不能用应用公钥替代。生产密钥应限制访问权限,并制定轮换和泄露处置流程。

RSA2 指基于 RSA 和 SHA-256 的签名方式。签名验真用于确认数据签名来自对应密钥且内容未被篡改;它本身不等于订单金额、订单归属和业务状态校验。验签后仍需逐项核对业务数据。

三、添加 Java SDK 并配置客户端

以下 Maven 依赖示例使用支付宝 Java SDK。项目应固定经过验证的 SDK 版本,并按组织的依赖管理策略定期检查更新;如果所用版本、JDK 或依赖仓库不兼容,请以官方 SDK 仓库和发布记录为准。

<dependency>
    <groupId>com.alipay.sdk</groupId>
    <artifactId>alipay-sdk-java</artifactId>
    <version>4.38.157.ALL</version>
</dependency>

以下配置类使用环境变量读取密钥。示例保留了字符串格式的私钥、公钥配置方式;不要把尖括号包围的占位文字当成真实密钥。真实密钥应按 SDK 支持的格式提供,常见做法是使用 PEM 内容去掉头尾标记和换行后配置。不要随意改写密钥内容。

import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import com.alipay.api.AlipayClient;
import com.alipay.api.DefaultAlipayClient;

@Configuration
public class AlipayConfig {

    @Bean
    public AlipayClient alipayClient(
            @Value("${alipay.gateway}") String gateway,
            @Value("${alipay.app-id}") String appId,
            @Value("${alipay.private-key}") String privateKey,
            @Value("${alipay.public-key}") String alipayPublicKey) {

        return new DefaultAlipayClient(
                gateway,
                appId,
                privateKey,
                "json",
                "UTF-8",
                alipayPublicKey,
                "RSA2");
    }
}
alipay:
  gateway: ${ALIPAY_GATEWAY}
  app-id: ${ALIPAY_APP_ID}
  private-key: ${ALIPAY_PRIVATE_KEY}
  public-key: ${ALIPAY_PUBLIC_KEY}
  notify-url: https://shop.example.com/pay/alipay/notify
  return-url: https://shop.example.com/pay/alipay/return

密钥经环境变量注入只是示例,并不意味着环境变量在所有部署环境中都是最安全的存储方式。生产系统可结合密钥管理服务,并避免在日志、异常信息和诊断接口中打印密钥或完整敏感请求。

四、创建订单并发起支付

创建支付请求前,先在商户数据库中创建待支付订单。订单号由商户生成且应保持唯一;支付金额必须从商户已保存的订单读取,不能信任浏览器提交的金额、商品单价或折扣结果。金额建议使用 BigDecimal,不要用浮点数计算。

下面的示例省略了具体数据库实现。orderService 应从数据库读取当前用户有权支付的订单,并返回服务端计算、已持久化的订单金额。生成支付请求不应改变订单为“已支付”。

import java.math.BigDecimal;
import java.util.HashMap;
import java.util.Map;

import com.alipay.api.AlipayClient;
import com.alipay.api.request.AlipayTradePagePayRequest;
import com.alipay.api.response.AlipayTradePagePayResponse;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/pay/alipay")
public class AlipayController {

    private final AlipayClient alipayClient;
    private final OrderService orderService;
    private final ObjectMapper objectMapper;

    @Value("${alipay.notify-url}")
    private String notifyUrl;

    @Value("${alipay.return-url}")
    private String returnUrl;

    public AlipayController(
            AlipayClient alipayClient,
            OrderService orderService,
            ObjectMapper objectMapper) {
        this.alipayClient = alipayClient;
        this.orderService = orderService;
        this.objectMapper = objectMapper;
    }

    @PostMapping(
        value = "/page",
        produces = MediaType.TEXT_HTML_VALUE)
    public ResponseEntity<String> createPagePay(
            @RequestParam String orderNo,
            @SessionAttribute("userId") Long userId) throws Exception {

        // 必须校验订单属于当前用户,且处于允许支付的状态。
        Order order = orderService.getPayableOrder(orderNo, userId);
        BigDecimal amount = order.getPayAmount();

        AlipayTradePagePayRequest request =
                new AlipayTradePagePayRequest();
        request.setNotifyUrl(notifyUrl);
        request.setReturnUrl(returnUrl);

        Map<String, Object> biz = new HashMap<>();
        biz.put("out_trade_no", order.getOrderNo());
        biz.put("total_amount", amount.toPlainString());
        biz.put("subject", order.getPaySubject());
        biz.put("product_code", "FAST_INSTANT_TRADE_PAY");
        request.setBizContent(objectMapper.writeValueAsString(biz));

        AlipayTradePagePayResponse response =
                alipayClient.pageExecute(request);

        if (!response.isSuccess()) {
            // 生产环境记录必要的错误码和请求上下文;
            // 不要将密钥或敏感信息写入日志。
            return ResponseEntity.status(HttpStatus.BAD_GATEWAY)
                    .contentType(MediaType.TEXT_PLAIN)
                    .body("暂时无法发起支付,请稍后重试");
        }

        // SDK 返回的是可供浏览器提交的收银台表单 HTML。
        return ResponseEntity.ok()
                .contentType(MediaType.TEXT_HTML)
                .body(response.getBody());
    }
}

FAST_INSTANT_TRADE_PAY 是电脑网站支付示例使用的产品代码。商品名称、金额、订单号等字段须符合对应接口文档和商户产品要求。不同接口的字段集合、格式、长度和业务限制可能不同;不要把其他支付产品的参数要求套用到本接口。接入时应逐项查阅当前接口文档,而不是假设某个长度或金额上限对所有场景都适用。

示例中返回 SDK 生成的表单 HTML,由浏览器提交到支付宝。只允许返回可信 SDK 生成的内容,不要把用户可控字符串拼接成表单或跳转地址。接口应按商城的登录、CSRF 防护和支付发起规则设置;如果支付接口通过 AJAX 调用,应按实际页面集成方式处理响应,而不是把 HTML 当 JSON 解析。

五、处理异步通知:验签、核对、幂等

支付宝向 notify_url 发起通知时,商户需要读取表单参数、验签,再核对应用和交易信息。对电脑网站支付,常见成功状态包括 TRADE_SUCCESS 和 TRADE_FINISHED;具体应根据接口文档和商城业务决定哪些状态触发履约。不能只判断请求到达,也不能只看参数中的状态字符串。

下面的回调示例展示验签和关键核对逻辑。orderService.applyPaidOnce 必须在数据库事务中实现幂等:锁定或以条件更新方式检查订单状态、订单号和金额,只允许一次从待支付状态变成已支付,并记录支付宝交易号。通知可能重复送达,也可能与主动查询、其他通知并发执行。

import java.math.BigDecimal;
import java.util.LinkedHashMap;
import java.util.Map;
import javax.servlet.http.HttpServletRequest;

import com.alipay.api.internal.util.AlipaySignature;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/pay/alipay")
public class AlipayNotifyController {

    private final OrderService orderService;

    @Value("${alipay.public-key}")
    private String alipayPublicKey;

    @Value("${alipay.app-id}")
    private String appId;

    // 填写商户实际的支付宝卖家 ID,并与当前应用/收款账户配置对应。
    @Value("${alipay.seller-id}")
    private String sellerId;

    public AlipayNotifyController(OrderService orderService) {
        this.orderService = orderService;
    }

    @PostMapping(
        value = "/notify",
        produces = MediaType.TEXT_PLAIN_VALUE)
    public ResponseEntity<String> notify(HttpServletRequest request) {
        try {
            Map<String, String> params = new LinkedHashMap<>();
            request.getParameterMap().forEach((key, values) -> {
                if (values != null && values.length > 0) {
                    params.put(key, values[0]);
                }
            });

            boolean valid = AlipaySignature.rsaCheckV1(
                    params, alipayPublicKey, "UTF-8", "RSA2");
            if (!valid) {
                return ResponseEntity.badRequest().body("failure");
            }

            String notifiedAppId = params.get("app_id");
            String notifiedSellerId = params.get("seller_id");
            String orderNo = params.get("out_trade_no");
            String tradeNo = params.get("trade_no");
            String tradeStatus = params.get("trade_status");
            String totalAmount = params.get("total_amount");

            if (!appId.equals(notifiedAppId)
                    || !sellerId.equals(notifiedSellerId)
                    || orderNo == null
                    || tradeNo == null
                    || totalAmount == null) {
                return ResponseEntity.badRequest().body("failure");
            }

            if (!"TRADE_SUCCESS".equals(tradeStatus)
                    && !"TRADE_FINISHED".equals(tradeStatus)) {
                // 非成功状态按业务处理;不要因此把订单标成已支付。
                return ResponseEntity.ok("success");
            }

            Order order = orderService.findByOrderNo(orderNo);
            if (order == null) {
                return ResponseEntity.badRequest().body("failure");
            }

            BigDecimal notifiedAmount = new BigDecimal(totalAmount);
            if (order.getPayAmount().compareTo(notifiedAmount) != 0) {
                // 金额不一致时拒绝自动履约,转入告警与人工核查流程。
                return ResponseEntity.badRequest().body("failure");
            }

            // 此方法应事务化并幂等处理重复通知及并发状态更新。
            orderService.applyPaidOnce(orderNo, tradeNo, notifiedAmount);

            return ResponseEntity.ok("success");
        } catch (Exception e) {
            // 记录可用于排查的错误信息,但避免记录密钥等敏感内容。
            return ResponseEntity.internalServerError().body("failure");
        }
    }
}

示例使用 javax.servlet 命名空间,适用于采用相应 Spring Boot / Servlet 依赖的项目。若项目使用 Spring Boot 3,应将 Servlet 导入替换为 jakarta.servlet,并按项目所用 SDK 版本适配依赖。不要在同一工程里混用不兼容的 Servlet API。

幂等更新的基本原则

回调处理不应只是“查订单,再更新状态”。两个相同通知可能并发到达,或者通知与主动查询同时更新。较稳妥的做法是使用数据库事务,并通过行锁或带状态条件的更新保证只有一个请求完成状态迁移。例如,更新语句可以限定订单仍为待支付,受影响行数为零时再检查是否已由同一笔交易处理。支付宝交易号也应保存并避免被另一笔交易覆盖。

只有数据库状态更新成功后,才向支付宝响应成功。若数据库暂时不可用,返回失败以便后续重试,通常比“先回复成功、再异步更新”更安全。重复通知若确认订单已由同一交易成功处理,则可以幂等地返回成功,避免无意义重试。具体响应字符串应符合支付宝当前通知协议要求;电脑网站支付通知通常以响应正文 success 表示商户已成功处理。

在真实业务中,发货、发券等履约操作可能涉及其他服务。不要在持有数据库事务时执行耗时的外部调用。可先在本地事务中完成订单状态变更并写入可靠消息或待处理任务,再由消费者幂等执行履约。

六、同步返回页应该做什么

return_url 上可能带有支付宝返回的业务参数,但用户可以直接访问页面、刷新页面或修改请求内容。返回页不应仅凭浏览器参数将订单改为已支付,也不应因为用户没有返回而认为支付失败。

合理做法是从已登录用户的订单列表或支付结果页读取商户订单号,展示“正在确认”或当前本地订单状态;如果尚未收到通知,可以由后端调用支付查询接口核实,或提示用户稍后刷新。查询接口的请求也必须由后端使用受保护的商户凭据发起,不能把私钥交给浏览器。

七、金额、参数和安全注意事项

  • 金额由服务端计算。从已保存的订单读取最终应付金额,使用 BigDecimal 和明确的金额规则。比较金额时用 compareTo,不要用 double 的 ==。
  • 订单号须可唯一识别。使用商户生成的订单号,并按对应接口文档限制字符、长度和重复使用规则。不要假定所有支付宝接口共享同一套参数限制。
  • 验签不替代业务校验。验签后还要核对 App ID、卖家标识、商户订单号、金额和交易状态。签名验证所用支付宝公钥必须与当前应用和环境匹配。
  • 通知地址必须可达。检查 DNS、HTTPS 证书、防火墙、负载均衡、网关路由和请求方法。确认应用没有只允许登录用户访问该回调地址。
  • 日志要克制。记录订单号、交易号、验签结果、处理结果等必要信息;避免记录私钥、完整敏感数据和不必要的个人信息。
  • 请求入口也要保护。校验用户对订单的访问权限,限制支付发起频率,防止通过修改订单号为他人订单重复发起支付。支付接口只在后端创建订单金额和业务描述。

通知参数具体有哪些、哪些字段在某种交易场景下必有,以及字段格式和长度限制,应以当前接口文档和实际通知样本为准。不要从历史代码或其他支付产品的示例推断所有字段都必然存在。解析金额失败、关键字段缺失或交易归属不符时,应拒绝自动履约并告警排查。

八、常见错误与排查顺序

  1. 签名失败:检查是否混用了应用公钥与支付宝公钥、私钥和公钥是否属于同一应用、是否误用沙箱密钥、签名类型是否为 RSA2,以及密钥复制时是否丢失字符。验签前不要自行改变参数编码或过滤字段,应使用 SDK 的验签方法。
  2. 网关或应用不匹配:确认 App ID、网关、密钥和已开通产品属于同一环境。正式应用参数不能与沙箱网关随意搭配。
  3. 接口返回权限或参数错误:检查是否开通对应支付产品、产品代码是否匹配、必填字段是否缺失,并将 SDK 返回的错误码与当前接口文档对照。不要将金额或描述从客户端原样透传。
  4. 收银台无法跳转:确认后端返回的是 SDK 生成的表单 HTML,浏览器按预期提交该表单;排查响应是否被 JSON 序列化、模板转义或前端错误地当成普通文本显示。
  5. 回调没有到达:确认通知地址可从公网访问,POST 路由没有被登录拦截,反向代理允许请求,服务端日志能记录到请求。开发环境需要使用安全、可控的公网测试入口,不能假设本地地址可被访问。
  6. 用户已付款但本地仍待支付:检查通知日志和验签配置;在订单详情页提供后端查询与状态刷新机制。不要让用户反复付款,也不要仅根据同步返回强行改状态。
  7. 重复发货或重复发券:为订单状态迁移、支付宝交易号记录和下游履约增加幂等保护;不要依赖“通知通常只来一次”的假设。

九、上线前核对清单

  • 确认电脑网站支付产品已开通,正式与沙箱参数没有混用。
  • 确认应用私钥仅在后端安全保存,支付宝公钥与当前应用匹配。
  • 确认支付金额来自商户订单,订单归属和可支付状态已校验。
  • 确认异步通知完成验签、字段核对、金额核对和幂等更新。
  • 确认同步返回页不直接认定支付成功,必要时由后端查询交易。
  • 确认通知处理失败时能够排查,重复通知不会重复履约。
  • 确认 SDK 版本、接口参数和字段限制已经依据当前官方文档核实。

接入支付宝支付的核心,是把“发起支付”和“确认收款”分开:商户后端根据已保存订单创建支付请求,异步通知经过验签和业务核对后再幂等更新状态;浏览器返回只负责展示体验。做好这几条边界,才能降低错账、重复履约和凭据泄露的风险。

© 版权声明

相关文章