天天看點

支付寶支付快速內建實作

之前寫過一篇微信支付的文章,相對來說,微信支付是比較麻煩的,申請商戶号需要真實資質。支付寶就友善很多,文檔和demo也相當齊全,尤其是友好的沙箱環境,可以實作支付功能的快速開發

概念

支付場景

支付寶支付支援多種支付場景包括當面付、app支付、手機網頁支付等。本文已當面付為例說明

支付寶支付快速內建實作

當面付-掃碼支付

場景簡介:買家通過使用支付寶 “掃一掃” 功能,掃描商家的二維碼等方式完成支付

支付流程:展示二維碼->掃碼支付->等待回調->修改訂單狀态->定期對帳

應用場景

使用者打開支付寶中的 掃一掃 功能,掃描商家展示的二維碼進行支付。該模式适用于線下實體店支付、面對面支付、自助售貨機等場景。流程如下圖所示:

支付寶支付快速內建實作

使用說明

  1. 收銀員在商家收銀系統操作生成支付寶訂單,并生成二維碼。
  2. 使用者登入支付寶,點選首頁 掃一掃 或點選 付錢-掃碼付,進入掃碼界面。
  3. 使用者掃收銀員提供的二維碼,核對金額,确認支付。
  4. 使用者付款支付寶提示成功或失敗,商家收銀系統會拿到支付成功或者失敗的結果。
更多說明檢視連結:支付寶支付開發文檔

開始開發

準備

  • 下載下傳sdk,sdk&demo提供了基礎的支付功能接口
支付寶支付快速內建實作
  • 引入項目,建立支付寶支付工程,拷貝一下項目即可
支付寶支付快速內建實作

(引入示例)

支付寶支付快速內建實作
  • 核心依賴包
<!-- 支付寶支付 -->
<dependency>
    <groupId>com.alipay.sdk</groupId>
    <artifactId>alipay-sdk-java</artifactId>
    <version>3.3.87.ALL</version>
    <exclusions>
        <exclusion>
            <artifactId>commons-logging</artifactId>
            <groupId>commons-logging</groupId>
        </exclusion>
    </exclusions>
</dependency>
<!--gson-->
<dependency>
    <groupId>com.google.code.gson</groupId>
    <artifactId>gson</artifactId>
    <version>2.8.5</version>
</dependency>
<!--zxing-->
<dependency>
    <groupId>com.google.zxing</groupId>
    <artifactId>core</artifactId>
    <version>3.2.1</version>
</dependency>
           

配置支付資訊

即配置zfbinfo.properties中的參數資訊。由于我沒有正式賬戶,以下已沙箱環境為例

配置過程

  • 進入沙箱環境
  • appid

    open_api_domain

    :appid拷貝資訊配置的有關内容,替換
支付寶支付快速內建實作
  • pid

    : 進入個人賬戶中心,主賬号資訊中的賬戶ID就是pid
支付寶支付快速內建實作
支付寶支付快速內建實作
  • 配置rsa公私鑰
  • 下載下傳指定的加密工具,生成工具下載下傳和使用
  • 打開應用,點選“生成密鑰”
    支付寶支付快速內建實作
  • private_key

    :将“應用私鑰”拷貝到 private_key
  • public_key

    : 将“應用公鑰”拷貝到 public_key
  • 将公私鑰拷貝到沙箱中
    支付寶支付快速內建實作
  • alipay_public_key

    : 支付寶公鑰拷貝到 alipay_public_key
    支付寶支付快速內建實作
  • 其他保持不變

代碼測試

  • 下載下傳sdk,配置zfbinfo.properties後,啟動main.java的main()方法,測試代碼
支付寶支付快速內建實作

自定義業務

代碼改寫

  • 參考main.java的中的支付方法,改寫當面付
public String f2fPayTradePay(@RequestBody TradePaySummary tradePaySummary) {
    // 訂單清單
    List<TradePaySummary.Item> items = tradePaySummary.getItems();
    //支付二維碼的通路路徑
    String qrCodePath = null;

    // (必填) 商戶網站訂單系統中唯一訂單号,64個字元以内,隻能包含字母、數字、下劃線,
    // 需保證商戶系統端不能重複,建議通過資料庫sequence生成,
    String outTradeNo = "flow" + System.currentTimeMillis() + (long) (Math.random() * 10000000L);

    // (必填) 訂單标題,粗略描述使用者的支付目的。如“xxx品牌xxx門店消費”
    String subject = tradePaySummary.getSubject();

    // (必填) 訂單總金額,機關為元,不能超過1億元
    // 如果同時傳入了【打折金額】,【不可打折金額】,【訂單總金額】三者,則必須滿足如下條件:【訂單總金額】=【打折金額】+【不可打折金額】
    String totalAmount = items.stream().map(item -> new BigDecimal(item.getPrice())).reduce(new BigDecimal("0"), BigDecimal::add).toString();

    // (可選) 訂單不可打折金額,可以配合商家平台配置折扣活動,如果酒水不參與打折,則将對應金額填寫至此字段
    // 如果該值未傳入,但傳入了【訂單總金額】,【打折金額】,則該值預設為【訂單總金額】-【打折金額】
    String undiscountableAmount = "0.0";

    // 賣家支付寶賬号ID,用于支援一個簽約賬号下支援打款到不同的收款賬号,(打款到sellerId對應的支付寶賬号)
    // 如果該字段為空,則預設為與支付寶簽約的商戶的PID,也就是appid對應的PID
    String sellerId = "";

    // 訂單描述,可以對交易或商品進行一個詳細地描述,比如填寫"購買商品3件共20.00元"
    String body = String.format("購買商品%s件共%s元", items.size(), totalAmount);

    // (必填) 商戶門店編号,通過門店号和商家背景可以配置精準到門店的折扣資訊,詳詢支付寶技術支援
    String storeId = tradePaySummary.getStoreId();

    // 業務擴充參數,目前可添加由支付寶配置設定的系統商編号(通過setSysServiceProviderId方法),詳情請咨詢支付寶技術支援
    String providerId = "2088100200300400500";
    ExtendParams extendParams = new ExtendParams();
    extendParams.setSysServiceProviderId(providerId);

    // 支付逾時,線下掃碼交易定義為5分鐘
    String timeoutExpress = "15m";

    // 商品明細清單,需填寫購買商品詳細資訊,
    List<GoodsDetail> goodsDetailList = new ArrayList<>();
    // 建立一個商品資訊,參數含義分别為商品id(使用國标)、名稱、單價(機關為分)、數量,如果需要添加商品類别,詳見GoodsDetail
    List<GoodsDetail> instances = items.stream().map(item -> {
        long price = new BigDecimal(item.getPrice()).multiply(new BigDecimal(100)).longValue();
        GoodsDetail instance = GoodsDetail.newInstance(item.getGoodsId(), item.getGoodsName(), price, item.getQuantity());
        return instance;
    }).collect(Collectors.toList());
    // 建立好一個商品後添加至商品明細清單
    goodsDetailList.addAll(instances);

    // 建立支付請求builder,設定請求參數
    AlipayTradePrecreateRequestBuilder builder = new AlipayTradePrecreateRequestBuilder()
            .setSubject(subject).setTotalAmount(totalAmount).setOutTradeNo(outTradeNo)
            .setUndiscountableAmount(undiscountableAmount).setSellerId(sellerId).setBody(body)
            .setOperatorId(tradePaySummary.getOperatorId()).setStoreId(storeId).setExtendParams(extendParams)
            .setTimeoutExpress(timeoutExpress)
            // 支付寶伺服器主動通知商戶伺服器裡指定的頁面http路徑,根據需要設定
            .setNotifyUrl(tradePayProperties.getPaySuccessCallBack())
            .setGoodsDetailList(goodsDetailList);

    // 調用tradePay方法擷取當面付應答
    AlipayF2FPrecreateResult result = alipayTradeService.tradePrecreate(builder);
    switch (result.getTradeStatus()) {
        case SUCCESS:
            log.info("支付寶支付成功: )");
            AlipayTradePrecreateResponse response = result.getResponse();
            dumpResponse(response);
            // 生成二維碼儲存路徑,建立二維碼
            String filePath = String.format(tradePayProperties.getAliPayPath() + "/qr-%s.png", response.getOutTradeNo());
            // 本地絕對路徑:/Users/yc/temp/qr-code/alipay/qr-123.png
            String storePath = tradePayProperties.getStorePath() + filePath;
            ZxingUtils.getQRCodeImge(response.getQrCode(), 256, storePath);
            // http請求相對路徑:http://localhost:9150/static/qrcode/alipay/qr-123.png
            qrCodePath = tradePayProperties.getHttpBasePath() + filePath;
            break;

        case FAILED:
            log.error("支付寶支付失敗!!!");
            break;

        case UNKNOWN:
            log.error("系統異常,訂單狀态未知!!!");
            break;

        default:
            log.error("不支援的交易狀态,交易傳回異常!!!");
            break;
    }
    return qrCodePath;
}
           

二維碼路徑

  • 路徑配置
trade:
  ali-pay:
    qrcode:
      store-path: /Users/yc/temp/qr-code/alipay # 本地檔案絕對路徑路徑
      http-base-path: /static/qrcode/alipay # http通路的url路徑,對應store-path
    callback-url: http://chetwhy.free.idcfengye.com/ali-pay/f2f/callback # 支付寶支付的回調位址
           
  • 讀取配置
@Component
@ConfigurationProperties(prefix = "trade.ali-pay")
@Data
public class AliPayTradeProperties {

    /**
     * 二維碼
     */
    private AliPayTradeProperties.Qrcode qrcode;
    /**
     * 支付成功回調頁面
     */
    private String callbackUrl;

    @Data
    public static class Qrcode{
        /**
         * qrcode存儲檔案路徑設定
         */
        private String storePath;
        /**
         * http通路路徑設定
         */
        private String httpBasePath;
    }
}
           
  • 資源路徑映射配置
@Configuration
public class ResourceConfig implements WebMvcConfigurer {

    @Autowired
    private TradePayProperties tradePayProperties;

    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        String os = System.getProperty("os.name");
        // 将http請求路徑映射到本地路徑
        registry.addResourceHandler(tradePayProperties.getHttpBasePath() + "/**")
                .addResourceLocations("file:" + tradePayProperties.getStorePath() + "/");
    }
}
           
  • 說明

store-path:二維碼存儲在伺服器本地中的絕對路徑

http-base-path:伺服器通路二維碼的網絡路徑

ResourceConfig: 配置後,http請求的路徑http-base-path可以直接映射到本地store-path,如通路http:localhost:8080/static/qrcode/alipay/qr-123.png,就是通路/Users/yc/temp/qr-code/alipay/qr-123.png,他們指向的都是同一資源

支付回調

當二維碼傳回後,使用者掃描二維碼并支付。這是使用者感覺行為,我們如何得知支付已完成。除了主動查詢外(參考main.java),更推薦使用支付回調的方式,讓支付寶背景主動通知商家背景
内網穿透

支付寶本身是一個在公網的服務應用,要想支付寶通知到我們的背景,需要我們背景有一個公網可以通路的域名。本地調試時,可以使用内網穿透工具,這樣通路指定公網域名,就可以直接通路到本地

回調通知是一個異步通知,不同于socket請求的後端與遠端伺服器保持長連結

Ngrok CC

之前在微信支付中,用的内網穿透工具的是natapp。這次使用ngrok cc示例,兩者都是不錯的工具

這裡不對ngrok的使用隻做簡單說明,有興趣的可以檢視官網教程

大體步驟:

  1. 注冊賬戶;
  2. 登入後,(沒有隧道)點選隧道管理-開通隧道,選擇免費的即可;
  3. 隧道管理-編輯,配置本地端口,這個端口就是支付應用啟動的端口
    支付寶支付快速內建實作
  4. 下載下傳用戶端
  5. 解壓本地,進入用戶端
  6. 拷貝隧道id,目前目錄打開終端,輸入啟動腳本
    ./sunny clientid 隧道id
               
支付寶支付快速內建實作
回調接口

參考文檔

@Slf4j
@RestController
@RequestMapping("/ali-pay/f2f")
public class AliPayF2FController {
    @Autowired
    private TradeService tradeService;

    /**
     * 支付寶支付回調,根據文檔:
     * 1.支付寶是用 POST 方式發送通知資訊
     * 2.在通知傳回參數清單中,除去sign、sign_type兩個參數外,凡是通知傳回回來的參數皆是待驗簽的參數。将剩下參數進行 url_decode, 然後進行字典排序,組成字元串,得到待簽名字元串
     * 3.程式執行完後必須通過 PrintWriter 類列印輸出"success"(不包含引号)。如果商戶回報給支付寶的字元不是 success 這7個字元,支付寶伺服器會不斷重發通知,直到超過 24 小時 22 分鐘。一般情況下,25 小時以内完成 8 次通知(通知的間隔頻率一般是:4m,10m,10m,1h,2h,6h,15h)
     *
     * @return
     */
    @PostMapping("/callback")
    public void aliPayCallback(HttpServletRequest request, HttpServletResponse response) {
        log.info("接受支付寶回調");
        Enumeration<String> names = request.getParameterNames();
        Map<String, String> map = new TreeMap<>();
        while (names.hasMoreElements()) {
            String name = names.nextElement();
            String value = request.getParameter(name);
            log.info("parameter name->{},value->{}", name, value);
            if (!"sign_type".equalsIgnoreCase(name)) {
                map.put(name, value);
            }
        }
        // 異步傳回結果驗簽
        try {
            boolean checkFlag = AlipaySignature.rsaCheckV2(map, Configs.getAlipayPublicKey(), "utf-8", Configs.getSignType());
            PrintWriter writer = response.getWriter();
            if (checkFlag) {
                writer.print("success");
            } else {
                writer.print("unSuccess");
            }
        } catch (AlipayApiException | IOException e) {
            throw new CustomException(ResultCodeEnum.ALIPAY_CALLBACK_CHECK_FAILED);
        }
    }
  
  // 其他業務接口...
}
           

目前mvc的通路路徑就是

/ali-pay/f2f/callback

,加上ngrok中主機域名後就是:http://chetwhy.free.idcfengye.com/ali-pay/f2f/callback,更新到配置檔案中

支付測試

支付接口

/**
 * 支付寶支付
 * @return
 */
@PostMapping("/ali-pay")
public ResponseEntity<R> doOrder() {
    log.info("測試支付寶支付...");
    TradePaySummary summary = new TradePaySummary();
    summary.setSubject("測試支付寶當面付");
    summary.setOperatorId("yc");
    summary.setStoreId("yc");
    TradePaySummary.Item item = new TradePaySummary.Item();
    item.setGoodsId("1001");
    item.setGoodsName("測試商品");
    item.setPrice("100000");
    item.setQuantity(1);
    summary.setItems(Arrays.asList(item));
    String qrcode = aliPayFeignApi.f2fPayTradePay(summary);
    if (StringUtils.isEmpty(qrcode)) {
        return R.error().message("網絡繁忙").buildResponseEntity();
    }
    // TODO 儲存訂單,更新狀态
    return R.ok().message("測試成功").data("qrcode",qrcode).buildResponseEntity();
}
           

測試流程

  • 下單沙箱版app
    支付寶支付快速內建實作
  • 請求二維碼接口
    支付寶支付快速內建實作
  • 點選二維碼路徑,擷取圖檔
    支付寶支付快速內建實作
  • 使用沙箱app掃碼支付,支付密碼預設是111111,詳細可檢視沙箱賬号(記住是沙箱app)
    支付寶支付快速內建實作

PS: 沙箱平台可為目前賬戶自由充值超大支付金額,體驗一把揮霍。。。

詳細源碼,請參考:https://github.com/chetwhy/cloud-flow

繼續閱讀