商城項目服務端實踐SSM(十二)-------前臺_支付接口(2)(當面付的掃碼支付)

當面付的文檔

  • 驗籤流程: 

  • 支付流程: 

  • 公共常量類
package com.mmall.common;

import com.google.common.collect.Sets;

import java.util.Set;

//設置公共量
public class Const {
    public static final String CURRENT_USER = "currentUser";
    public static final String EMAIL="email";
    public static final String USERNAME="username";
    public interface Role{
        int ROLE_CUSTOMER=0;//普通用戶
        int ROLE_ADMIN=1;//管理員
    }

    public enum ProductStatusEnum{
        ON_SALE(1,"在線");
        private String value;
        private int code;
        ProductStatusEnum(int code,String value){
            this.code=code;
            this.value=value;
        }
        public String getValue(){
            return value;
        }
        public int getCode(){
            return code;
        }
    }
    public enum OrderStatusEnum{
        CANCELED(0,"已取消"),
        NO_PAY(10,"未支付"),
        PAID(20,"已付款"),
        SHIPPED(40,"已發貨"),
        ORDER_SUCCESS(50,"訂單完成"),
        ORDER_CLOSE(60,"訂單關閉");

        private String value;
        private int code;
        OrderStatusEnum(int code,String value){
            this.code=code;
            this.value=value;

        }
        public String getValue(){
            return value;
        }
        public int getCode(){
            return code;
        }

        public static OrderStatusEnum codeOf(int code){
            for(OrderStatusEnum orderStatusEnum : values()){
                if(orderStatusEnum.getCode() == code){
                    return orderStatusEnum;
                }
            }
            throw new RuntimeException("沒有找到對應的枚舉");
        }
    }
        public interface AlipayCallback

    {
        String TRADE_STATUS_WAIT_BUYER_PAY = "WAIT_BUYER_PAY";
        String TRADE_STATUS_TRADE_SUCCESS = "TRADE_SUCCESS";

        String RESPONSE_SUCCESS = "success";
        String RESPONSE_FAILED = "failed";

    }

    public enum PayPlatformEnum{
        ALIPAY(1,"支付寶");

        PayPlatformEnum(int code,String value){
            this.code = code;
            this.value = value;
        }
        private String value;
        private int code;

        public String getValue() {
            return value;
        }

        public int getCode() {
            return code;
        }
    }

    public enum PaymentTypeEnum{
        ONLINE_PAY(1,"在線支付");

        PaymentTypeEnum(int code,String value){
            this.code = code;
            this.value = value;
        }
        private String value;
        private int code;

        public String getValue() {
            return value;
        }

        public int getCode() {
            return code;
        }

        public static PaymentTypeEnum codeOf(int code){
            for(PaymentTypeEnum paymentTypeEnum : values()){
                if(paymentTypeEnum.getCode() == code){
                    return paymentTypeEnum;
                }
            }
            throw new RuntimeException("沒有找到對應的枚舉");
        }

    }

}
  • 創建一個properties配置文件,配置支付寶的回調地址,必須是線上的,互聯網可以訪問的地址。

(沒有服務器的話,可以通過配置一個隧道進行訪問)

 

一、支付(當面付的掃碼支付)

  • 思路:

1、當用戶生成訂單,發起支付的時候,商戶將用戶生成的訂單數據和異步通知回調地址作爲請求參數向支付寶發起預下單的請求。預下單成功後,支付寶返回一個支付二維碼給商戶,商戶把這個支付二維碼儲存到FTP服務器上,然後在展示給用戶支付。

2、用戶掃碼進行支付,支付寶會將該筆訂單的變更信息,沿着商戶調用預下單請求時所傳入的回調地址主動回調給商戶。

3、商戶對支付寶回調的交易結果使用RSA2對支付寶公鑰驗證,確認該結果是支付寶返回的。除此之外還要驗證支付寶返回的數據訂單號是否爲商戶創建的,交易總金額是否正確等等。

4、如果驗證通過,商戶則向支付寶返回“success”,即爲支付成功。(把訂單的支付金額和支付狀態寫進訂單表中,也要把支付的信息存入支付的數據庫表中)

  • controller
    //支付寶掃碼當面付付款
    @RequestMapping("pay.do")
    @ResponseBody
    public ServerResponse pay(HttpSession session, Long orderNo, HttpServletRequest request){
        User user= (User) session.getAttribute(Const.CURRENT_USER);
        if (user == null){
            return ServerResponse.createByErrorCodeMessage(ResponseCode.NEED_LOGIN.getCode(),ResponseCode.NEED_LOGIN.getDesc());
        }
        String path=request.getSession().getServletContext().getRealPath("upload");
        return iOrderService.pay(orderNo,user.getId(),path);
    }
  • impl
 //日誌
    private static final Logger logger = LoggerFactory.getLogger(OrderServiceImpl.class);
    //支付
    public ServerResponse pay(Long orderNo, Integer userId, String path) {

        Map<String, String> resultMap = Maps.newHashMap();
        //查詢用戶是否有該訂單
        Order order = orderMapper.selectByUserIdAndOrderNo(userId, orderNo);
        if (order == null) {
            return ServerResponse.createByErrorMessage("用戶沒有該訂單");
        }
        resultMap.put("orderNo", String.valueOf(order.getOrderNo()));

        // (必填) 商戶網站訂單系統中唯一訂單號,64個字符以內,只能包含字母、數字、下劃線,
        // 需保證商戶系統端不能重複,建議通過數據庫sequence生成,
        String outTradeNo = order.getOrderNo().toString();

        // (必填) 訂單標題,粗略描述用戶的支付目的。如“xxx品牌xxx門店當面付掃碼消費”
        String subject = new StringBuilder().append("mmall在線商城掃碼支付,訂單號").append(outTradeNo).toString();

        // (必填) 訂單總金額,單位爲元,不能超過1億元
        // 如果同時傳入了【打折金額】,【不可打折金額】,【訂單總金額】三者,則必須滿足如下條件:【訂單總金額】=【打折金額】+【不可打折金額】
        String totalAmount = order.getPayment().toString();

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

        // 賣家支付寶賬號ID,用於支持一個簽約賬號下支持打款到不同的收款賬號,(打款到sellerId對應的支付寶賬號)
        // 如果該字段爲空,則默認爲與支付寶簽約的商戶的PID,也就是appid對應的PID
        String sellerId = "";

        // 訂單描述,可以對交易或商品進行一個詳細地描述,比如填寫"購買商品2件共15.00元"
        String body = new StringBuilder().append("訂單").append(outTradeNo).append("購買商品共").append(totalAmount).append("元").toString();

        // 商戶操作員編號,添加此參數可以爲商戶操作員做銷售統計
        String operatorId = "test_operator_id";

        // (必填) 商戶門店編號,通過門店號和商家後臺可以配置精準到門店的折扣信息,詳詢支付寶技術支持
        String storeId = "test_store_id";

        // 業務擴展參數,目前可添加由支付寶分配的系統商編號(通過setSysServiceProviderId方法),詳情請諮詢支付寶技術支持
        ExtendParams extendParams = new ExtendParams();
        extendParams.setSysServiceProviderId("2088100200300400500");

        // 支付超時,定義爲120分鐘
        String timeoutExpress = "120m";

        // 支付寶裏聲明的商品明細列表,需填寫購買商品詳細信息,
        List<GoodsDetail> goodsDetailList = new ArrayList<GoodsDetail>();
        //OrderItem爲自己商城裏的訂單明細表
        List<OrderItem> orderItemList = orderItemMapper.getByOderNoAndUserId(orderNo, userId);

        //foreach循環,每個商品的信息都添加到GoodsDetail中
        for (OrderItem orderItem : orderItemList) {
            // 創建一個商品信息,參數含義分別爲商品id(使用國標)、名稱、單價(單位爲分)、數量,如果需要添加商品類別,詳見GoodsDetail
            GoodsDetail goods = GoodsDetail.newInstance(orderItem.getProductId().toString(), orderItem.getProductName(), BigDecimalUtil.mul(orderItem.getCurrentUnitPrice().doubleValue(), new Double(100).doubleValue()).longValue(), orderItem.getQuantity());
            // 創建好一個商品後添加至商品明細列表
            goodsDetailList.add(goods);
        }

        // 創建掃碼支付請求builder,設置請求參數
        AlipayTradePrecreateRequestBuilder builder = new AlipayTradePrecreateRequestBuilder()
                .setSubject(subject).setTotalAmount(totalAmount).setOutTradeNo(outTradeNo)
                .setUndiscountableAmount(undiscountableAmount).setSellerId(sellerId).setBody(body)
                .setOperatorId(operatorId).setStoreId(storeId).setExtendParams(extendParams)
                .setTimeoutExpress(timeoutExpress)
                .setNotifyUrl(PropertiesUtil.getProperty("alipay.callback.url"))//支付寶服務器主動通知商戶服務器裏指定的頁面http路徑,根據需要設置
                .setGoodsDetailList(goodsDetailList);

        /** 一定要在創建AlipayTradeService之前調用Configs.init()設置默認參數
         *  Configs會讀取classpath下的zfbinfo.properties文件配置信息,如果找不到該文件則確認該文件是否在classpath目錄
         */
        Configs.init("zfbinfo.properties");

        /** 使用Configs提供的默認參數
         *  AlipayTradeService可以使用單例或者爲靜態成員對象,不需要反覆new
         */
        AlipayTradeService tradeService = new AlipayTradeServiceImpl.ClientBuilder().build();

        AlipayF2FPrecreateResult result = tradeService.tradePrecreate(builder);

        switch (result.getTradeStatus()) {
            case SUCCESS:
                logger.info("支付寶預下單成功: )");

                AlipayTradePrecreateResponse response = result.getResponse();
                dumpResponse(response);

                // 支付寶預下單成功生成二維碼,把二維碼圖片上傳到FTP服務器上
                //聲明上傳的文件保存的文件夾
                File folder = new File(path);
                //如果文件夾不存在則要創建它
                if (!folder.exists()) {
                    //文件夾的權限爲可寫
                    folder.setWritable(true);
                    //創建文件夾
                    folder.mkdirs();
                }
                // 需要修改爲運行機器上的路徑
                //二維碼路徑
                String qrPath = String.format(path + "/qr-%s.png", response.getOutTradeNo());
                //生成的二維碼文件名,用訂單號命名,會自動替換到%s上,例如qr-11111111.png
                String qrFileName = String.format("qr-%s.png", response.getOutTradeNo());
                //獲得二維碼生成的圖片
                ZxingUtils.getQRCodeImge(response.getQrCode(), 256, qrPath);

                //創建上傳的文件
                File targetFile = new File(path, qrFileName);
                try {
                    //上傳至FTP服務器
                    FTPUtil.uploadFile(Lists.<File>newArrayList(targetFile));
                } catch (IOException e) {
                    logger.error("上傳二維碼異常", e);
                }
                logger.info("qrPath:" + qrPath);
                //二維碼的URL
                String qrUrl = PropertiesUtil.getProperty("ftp.server.http.prefix") + targetFile.getName();
                resultMap.put("quUrl", qrUrl);
                return ServerResponse.createBySuccess(resultMap);


            case FAILED:
                logger.error("支付寶預下單失敗!!!");
                return ServerResponse.createByErrorMessage("支付寶預下單失敗!!!");

            case UNKNOWN:
                logger.error("系統異常,預下單狀態未知!!!");
                return ServerResponse.createByErrorMessage("系統異常,預下單狀態未知!!!");

            default:
                logger.error("不支持的交易狀態,交易返回異常!!!");
                return ServerResponse.createByErrorMessage("不支持的交易狀態,交易返回異常!!!");

        }

    }

    // 簡單打印應答
    private void dumpResponse(AlipayResponse response) {
        if (response != null) {
            logger.info(String.format("code:%s, msg:%s", response.getCode(), response.getMsg()));
            if (StringUtils.isNotEmpty(response.getSubCode())) {
                logger.info(String.format("subCode:%s, subMsg:%s", response.getSubCode(),
                        response.getSubMsg()));
            }
            logger.info("body:" + response.getBody());
        }
    }

二、支付寶回調驗證

  • controller
//支付寶回調函數
    //使用sdk驗籤方法 https://openclub.alipay.com/club/history/read/2214
    @RequestMapping("alipay_callback.do")
    @ResponseBody
    public Object alipayCallback(HttpServletRequest request){
        Map<String,String> params= Maps.newHashMap();
        //記錄着支付寶裏的參數和參數的值
        Map requestParams=request.getParameterMap();
        //傳來的參數的value取出來
        for(Iterator iter=requestParams.keySet().iterator();iter.hasNext();){
            String name=(String)iter.next();
            String[] values=(String[]) requestParams.get(name);
            String valueStr="";
            for(int i=0;i<values.length;i++){
                valueStr=(i == values.length-1)?valueStr+values[i]:valueStr+values[i]+",";
            }

            params.put(name,valueStr);
        }
        logger.info("支付寶回調,sign:{},trade_statux;{},參數:{}",params.get("sign"),params.get("trade_status"),params.toString());

        //驗證回調的正確性,是不是支付寶發的
        //在通知返回參數列表中,除去sign、sign_type兩個參數外,凡是通知返回回來的參數皆是待驗籤的參數。
        //由於支付寶中已經自動除去sign,所以我們還得自己除去sign_type
        params.remove("sign_type");
        try {
            //調用SDK驗證簽名,驗證支付寶公鑰
            boolean alipayRSACheckedV2= AlipaySignature.rsaCheckV2(params, Configs.getAlipayPublicKey(),"utf-8",Configs.getSignType());
            //驗證不通過
            if(!alipayRSACheckedV2){
                return ServerResponse.createByErrorMessage("非法請求,驗證不通過");
            }

        } catch (AlipayApiException e) {
            logger.error("支付寶驗證回調異常",e);
        }
	/* 實際驗證過程建議商戶務必添加以下校驗:
	1、需要驗證該通知數據中的out_trade_no是否爲商戶系統中創建的訂單號,
	2、判斷total_amount是否確實爲該訂單的實際金額(即商戶訂單創建時的金額),
	3、校驗通知中的seller_id(或者seller_email) 是否爲out_trade_no這筆單據的對應的操作方(有的時候,一個商戶可能有多個seller_id/seller_email)
	4、驗證app_id是否爲該商戶本身。
	*/
        //驗證各種數據
        ServerResponse serverResponse=iOrderService.aliCallback(params);

        //驗證成功,則給支付寶返回success
        if (serverResponse.isSuccess()){
           return Const.AlipayCallback.RESPONSE_SUCCESS;
       }
        return Const.AlipayCallback.RESPONSE_FAILED;
    }
  • impl
//此鏈接可以查看支付寶異步通知參數https://docs.open.alipay.com/194/103296#s5
    public ServerResponse aliCallback(Map<String, String> params) {
        //訂單號
        Long orderNo = Long.parseLong(params.get("out_trade_no"));
        //支付寶交易號
        String tradeNo = params.get("trade_no");
        //交易狀態
        String tradeStatus = params.get("trade_status");
        //查詢是否有該訂單
        Order order = orderMapper.selectByOrderNo(orderNo);
        if (order == null) {
            return ServerResponse.createByErrorMessage("非該商場的訂單");
        }
        //如果有該訂單,並且該訂單已付款
        if (order.getStatus() >= Const.OrderStatusEnum.PAID.getCode()) {
            return ServerResponse.createBySuccess("訂單重複使用");
        }
        //在支付寶的業務通知中,只有交易通知狀態爲 TRADE_SUCCESS 或 TRADE_FINISHED 時,支付寶纔會認定爲買家付款成功。
        //如果交易狀態tradeStatus的值等於TRADE_SUCCESS
        if (Const.AlipayCallback.TRADE_STATUS_TRADE_SUCCESS.equals(tradeStatus)) {
            //把該訂單信息添加至訂單表中
            order.setPaymentTime(DateTimeUtil.strToDate(params.get("gmt_payment")));
            order.setStatus(Const.OrderStatusEnum.PAID.getCode());
            orderMapper.updateByPrimaryKeySelective(order);
        }
        //在支付信息表中設置該訂單的支付信息
        PayInfo payInfo = new PayInfo();
        payInfo.setUserId(order.getUserId());
        payInfo.setOrderNo(order.getOrderNo());
        payInfo.setPayPlatform(Const.PayPlatformEnum.ALIPAY.getCode());
        payInfo.setPlatformNumber(tradeNo);
        payInfo.setPlatformStatus(tradeStatus);

        payInfoMapper.insert(payInfo);
        return ServerResponse.createBySuccess();
    }

三、查詢訂單支付狀態

  • controller
        //付款後,跳到訂單頁面,查詢訂單支付狀態
        @RequestMapping("query_order_pay_status.do")
        @ResponseBody
        public ServerResponse<Boolean> queryOrderPayStatus(HttpSession session, Long orderNo){
            User user= (User) session.getAttribute(Const.CURRENT_USER);
            if (user == null){
                return ServerResponse.createByErrorCodeMessage(ResponseCode.NEED_LOGIN.getCode(),ResponseCode.NEED_LOGIN.getDesc());
            }
                    ServerResponse serverResponse=iOrderService.queryOrderPayStatus(user.getId(),orderNo);
            if (serverResponse.isSuccess()){
                return ServerResponse.createBySuccess(true);
            }
            return ServerResponse.createBySuccess(false);
        }
  • impl
//付款後,跳到訂單,查詢訂單支付狀態
    public ServerResponse queryOrderPayStatus(Integer userId, Long orderNo) {
        Order order = orderMapper.selectByUserIdAndOrderNo(userId, orderNo);
        if (order == null) {
            return ServerResponse.createByErrorMessage("非該商場的訂單");
        }
        if (order.getStatus() >= Const.OrderStatusEnum.PAID.getCode()) {
            return ServerResponse.createBySuccess();
        }
        return ServerResponse.createByError();
    }

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章