天天看點

二維碼收款——後端API

後端使用springboot

API流程見下圖,手機畫圖,見諒

二維碼收款——後端API

  1. 使用者通過微信公衆号授權擷取其openid,微信支付需要使用openid,授權的流程等同于登入,授權完成後将openid寫入session表示登入成功,後面接口通信基于session驗證是否登入
  2. 使用者端輸入金額後發起支付請求,接口 /create/pay/order
  3. 服務端收到支付請求後建立本系統訂單并持久化,然後向微信支付統一下單接口發起請求擷取微信支付必要參數,擷取參數後傳回給用戶端(/create/pay/order接口的傳回)
  4. 使用者端(微信中)收到/create/pay/order接口的傳回,本地拉起微信支付,使用者支付成功
  5. 使用者支付成功後,微信支付伺服器會通知我的服務端,服務端将本系統訂單狀态修改為已支付,同時将訂單支付成功的資訊發送給語音播報程式(語音播報裝在電腦上通過socket與服務端連接配接)

注:服務端提供websocket服務,用于和語音播報程式長連接配接

按流程順序,核心代碼如下:

登入驗證,通過springboot攔截器實作,攔截器類代碼

@Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
            throws Exception {
        logger.info("請求dump:{}", WebUtil.dumpRequest(request));
        String servletPath = WebUtil.getServletPathWithParam(request);
        if (!(handler instanceof HandlerMethod)) {
            return true;
        }
        //進入攔截處理邏輯
        final HandlerMethod handlerMethod = (HandlerMethod) handler;
        final Class<?> clazz = handlerMethod.getBeanType();
        final Method method = handlerMethod.getMethod();
        //a.終端使用者登入
        if (clazz.isAnnotationPresent(LoginUserRequired.class) || method.isAnnotationPresent(LoginUserRequired.class)) {
            LoginUserBean userBean = AuthHelper.getUserBean(request);
            logger.info("微信網頁端登入:request中的使用者=>{}", userBean);
            //如果request中已經有使用者對象
            if (userBean != null) {
                return true;
            }
            logger.info("微信網頁登入失效,請重新授權");
            //ajax請求傳回json
            if (WebUtil.isAjaxRequest(request)) {
                String s = APIUtil.getReturn(401, "請重新登入");
                response.setContentType("application/json;charset=UTF-8");
                response.getWriter().print(s);
            } else {
                //跳轉到微信授權的頁面
                String referer = Base64.encodeBase64String(servletPath.getBytes(StandardCharsets.UTF_8));
                response.sendRedirect("/c/auth/wechat?referer=" + referer);
            }
            return false;
        }
    return false;
}      

微信授權代碼(/c/auth/wechat接口),Controller中

/**
     * 終端使用者微信網頁登入
     * @param code
     * @param state
     * @param referer base64編碼的原始請求連結,帶有全部get參數
     * @return
     */
    @RequestMapping(value = "/c/auth/wechat")
    public Object wxLogin(String code, String state, String referer, HttpServletRequest request,
            HttpServletResponse response) throws UnsupportedEncodingException {
        Object result;
        logger.info("微信登入 code=>{}, state=>{}, referer=>{}", code, state, referer);
        LoginUserBean userBean = AuthHelper.getUserBean(request);
        if (userBean != null) {
            logger.info("已經登入成功,無需再次微信授權,{}", userBean);
            if (StringUtils.isBlank(referer)) {
                result = new MsgTipsView(MsgTipsView.Level.INFO, "提示", "已經登入");
            } else {
                referer = new String(Base64.decodeBase64(referer));
                result = "redirect:" + referer;
            }
            return result;
        }
        //微信授權第一次請求
        if (StringUtils.isAnyBlank(code, state)) {
            //authBackURL的host必須與微信背景設定的相同
            String basePath = appConf.getApiHost();
            String authBackURL = basePath + "/c/auth/wechat?referer=" + referer;
            try {
                authBackURL = URLEncoder.encode(authBackURL, "utf-8");
            } catch (Exception e) {
                logger.error("URLEncode錯誤, URL: {}, error: {}", authBackURL, e.getMessage());
            }
            logger.debug("authBackURL: " + authBackURL);
            String rand = RandomStringUtils.randomAlphanumeric(5);
            String wxAuthURL = gzhApiService.getWxGZHApiModule().gzhWebAuthURL(authBackURL, rand, "snsapi_base");
            result = "redirect:" + wxAuthURL;
            return result;
        }
        //微信授權完成,通過code擷取使用者資訊
        WeixinUserInfo userInfo = gzhApiService.getWxGZHApiModule().getWeixinIDByAuth2Code(code, "snsapi_base");
        logger.info("擷取的微信使用者資訊: {}", userInfo);
        if (userInfo == null) {
            result = new MsgTipsView(MsgTipsView.Level.ERROR, "微信登入", "擷取微信資訊失敗");
            return result;
        }
        //擷取openid成功,寫入session以表示登入
        userBean = new LoginUserBean(userInfo.getOpenid());
        AuthHelper.writeUserBean(request, userBean);
        if (StringUtils.isBlank(referer)) {
            return new MsgTipsView(MsgTipsView.Level.SUCCESS, "登入成功", "登入成功");
        }
        //恢複最開始請求的頁面
        return "redirect:" + new String(Base64.decodeBase64(referer));
    }      

本系統建立訂單(/create/pay/order接口)

/**
     * 建立支付訂單
     * @param orderParam   支付訂單所需的參數 {'mercNo': 'xxx','amount': '訂單金額', 'memo': '訂單備注', 'channel': '支付通道'}
     * @return 用戶端實際支付所需的參數
     */
    @PostMapping(value = "/a/create/pay/order")
    @ResponseBody
    @LoginUserRequired
    public String createPayOrder(@RequestBody Map<String, String> orderParam, LoginUserBean userBean,
            HttpServletRequest request) {
        String openid = userBean.getWxOpenid();
        String mercNo = orderParam.getOrDefault("mercNo", "");
        String payChannel = orderParam.getOrDefault("channel", "");
        BigDecimal orderAmount = new BigDecimal(orderParam.getOrDefault("amount", "0.00"));
        String orderMemo = orderParam.getOrDefault("memo", "");
        //驗證參數
        RMerc merc;
        if ((merc = mercService.selectMerc(mercNo)) == null) {
            return APIUtil.getReturn(1, "商戶号不存在");
        }
        List<String> availableChannels = ImmutableList.of("wxpay");
        if (!availableChannels.contains(payChannel)) {
            return APIUtil.getReturn(1, "不支援的支付通道");
        }
        if (orderAmount.compareTo(BigDecimal.ZERO) <= 0 || orderAmount.scale() > 2) {
            return APIUtil.getReturn(1, "金額格式錯誤");
        }
        //微信支付
        if ("wxpay".equals(payChannel)) {
            RPayOrder order = payOrderService.createOrderAndSave(openid, mercNo, orderAmount, "wxpay", orderMemo);
            logger.info("建立支付訂單:{}", JSON.toJSONString(order));
            //構造微信統一下單API所需的參數
            String orderId = order.getOrderId();
            int fee = orderAmount.multiply(new BigDecimal(100)).intValue();
            String ip = WebUtil.getRealIp(request);
            String notifyURL = getPayNotifyURL();
            GZHWxpayModule module = gzhApiService.getGzhWxpayModule();
            WxpayModule wxpayModule = module.getWxpayModule();
            String body = String.format("支付訂單%s%s", orderId, StringUtils.isNotBlank(orderMemo) ? ":" + orderMemo : "");
            SortedMap<String, String> ret = wxpayModule.getJsApiPayParam(module.getAppid(), openid, orderId, body, fee,
                    ip, notifyURL);
            if (CollectionUtils.isEmpty(ret)) {
                return APIUtil.getReturn(1, "系統錯誤");
            }
            return APIUtil.getReturn(APIConst.OK, ret);
        }
        return APIUtil.getReturn(1, "不支援的支付通道");
    }      

微信端拉起微信支付代碼,見前一篇文章pay.js中代碼

微信支付成功後,微信支付系統回調我們的服務端

/**
     * 微信支付回調通知
     * @param content   post的内容
     * @return 響應給微信支付的文本
     */
    @PostMapping(value = "/c/wxpay/notify")
    @ResponseBody
    public String wxpayNotifyBack(@RequestBody String content) {
        String resp = PayCommonUtil.setXML("FAIL", "exception");
        try {
            Map<String, String> resMap = XMLUtil.doXMLParse(content);
            logger.info("微信支付回調通知:{}", resMap);
            if (resMap == null) {
                resp = "請求内容非法";
                throw new RuntimeException("請求内容格式錯誤");
            }
            WxpayModule module = gzhApiService.getGzhWxpayModule().getWxpayModule();
            //驗證簽名
            if (!module.checkSignValid(resMap)) {
                resp = PayCommonUtil.setXML("FAIL", "invalid sign");
                throw new RuntimeException("簽名錯誤");
            }
            String resCode = resMap.get("return_code");
            String bizCode = resMap.get("result_code");
            if ("FAIL".equals(resCode) || "FAIL".equals(bizCode)) {
                logger.error("微信回調失敗:{}", content);
                resp = PayCommonUtil.setXML("FAIL", "weixin pay fail");
                throw new RuntimeException("微信回調系統錯誤");
            }
            if (!"SUCCESS".equals(bizCode)) {
                logger.error("微信支付系統業務錯誤:{}", content);
                throw new RuntimeException("微信支付系統業務錯誤");
            }
            //------------支付成功----------------
            // 業務系統的訂單号
            String outTradeNo = resMap.get("out_trade_no");
            // 微信支付的訂單号
            String transactionId = resMap.get("transaction_id");
            // 支付金額(分)
            int totalFee = Integer.parseInt(resMap.get("total_fee"));
            //調用内部處理
            doActionWithWhenPaySuccess(outTradeNo, transactionId, totalFee);
            //給微信支付接口傳回成功
            resp = PayCommonUtil.setXML("SUCCESS", "OK");
        } catch (Exception e) {
            logger.error("微信支付回調異常:{},{}", content, e.getMessage());
        }
        return resp;
    }

    /**
     * 支付成功後處理業務邏輯
     * @param outTradeNo        業務系統訂單号
     * @param transactionId     支付方單号
     * @param totalFee          實際支付金額,分
     */
    private void doActionWithWhenPaySuccess(String outTradeNo, String transactionId, int totalFee) {
        RPayOrder order = payOrderService.queryById(outTradeNo);
        if (order == null || order.getOrderStatus() > 0) {
            return;
        }
        //驗證明際支付的金額
        BigDecimal payAmount = new BigDecimal(totalFee).divide(new BigDecimal(100), 2, RoundingMode.UNNECESSARY);
        if (order.getAmount().compareTo(payAmount) != 0) {
            logger.info("實際支付金額與訂單金額不符,訂單:{},實際支付金額:{}元,訂單金額:{}元", order.getOrderId(), payAmount, order.getAmount());
            return;
        }
        //填入支付資訊,讓回調隊列去處理
        order.setPayOrderId(transactionId);
        order.setOrderStatus(1);
        orderCallbackTaskService.putTask(order);
    }      
OrderCallbackTaskService類專門用于訂單支付成功後處理
      
/**
 * 訂單支付完成後回調任務服務
 */
@Service
public class OrderCallbackTaskService implements CommandLineRunner {
    private final Logger logger = LoggerFactory.getLogger(OrderCallbackTaskService.class);

    private LinkedBlockingDeque<RPayOrder> callbackOrderQueue = new LinkedBlockingDeque<>();
    @Resource private ThreadPoolTaskExecutor taskExecutor;
    @Resource private RPayOrderService payOrderService;

    @Override
    public void run(String... args) throws Exception {
        taskExecutor.execute(() -> {
            boolean exit = false;
            while (!exit) {
                try {
                    RPayOrder order = callbackOrderQueue.take();
                    //1. 支付成功,更新訂單狀态
                    updateOrderStatus(order);
                } catch (InterruptedException e) {
                    logger.error("訂單回調隊列中斷");
                    exit = true;
                }
            }
        });
    }

    /**
     * 向隊列加入一個任務
     * @param order 訂單
     */
    @SneakyThrows
    public void putTask(RPayOrder order) {
        callbackOrderQueue.put(order);
    }

    /**
     * 更新訂單的狀态
     * @param order 訂單
     */
    private void updateOrderStatus(RPayOrder order) {
        boolean s = payOrderService.updatePayInfo(order.getOrderId(), order.getPayOrderId(), order.getOrderStatus());
        if (s) {
            //發送通知
            pushNotice(order);
        }
    }

    /**
     * 推送通知
     * @param order
     */
    private void pushNotice(RPayOrder order) {
        //給語音播報裝置發送通知
        WebSocketService.sendMessage(order.getMercNo(), new RefuWsMessage<>("order", order));
    }
}      

Java端websocket服務比較簡單,參考文章很多

相關文檔資料

微信公衆号授權:https://developers.weixin.qq.com/doc/offiaccount/OA_Web_Apps/Wechat_webpage_authorization.html

微信支付:https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_1_1.shtml

Websocket:https://www.cnblogs.com/onlymate/p/9521327.html

下一篇,通知程式(語音播報)