二维码收款——后端API
后端使用springboot
API流程见下图,手机画图,见谅
- 用户通过微信公众号授权获取其openid,微信支付需要使用openid,授权的流程等同于登录,授权完成后将openid写入session表示登录成功,后面接口通信基于session验证是否登录
- 用户端输入金额后发起支付请求,接口 /create/pay/order
- 服务端收到支付请求后创建本系统订单并持久化,然后向微信支付统一下单接口发起请求获取微信支付必要参数,获取参数后返回给客户端(/create/pay/order接口的返回)
- 用户端(微信中)收到/create/pay/order接口的返回,本地拉起微信支付,用户支付成功
- 用户支付成功后,微信支付服务器会通知我的服务端,服务端将本系统订单状态修改为已支付,同时将订单支付成功的信息发送给语音播报程序(语音播报装在电脑上通过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
下一篇,通知程序(语音播报)