想对接支付,就需要申请对应的支付渠道。否则你只能模拟一个支付和支付成功,来走完自己的流程。
目前国内主要有微信支付和支付宝支付两种主流支付方式,但是微信支付不支持个体户,因此这里选择支付宝的沙箱支付。
其实支付流程基本如下图所示:
现在可以先去支付宝开发平台申请资质,https://open.alipay.com/develop/manage
然后直接下载手机版沙箱支付宝。
实现方案
以下是测试用例(java版):
public class PayTest { // 「沙箱环境」应用ID - 您的APPID,收款账号既是你的APPID对应支付宝账号。获取地址;https://open.alipay.com/develop/sandbox/app public static String app_id = "你的APPID"; // 「沙箱环境」商户私钥,你的PKCS8格式RSA2私钥 public static String app_private_key = "你的商户私钥" // 「沙箱环境」支付宝公钥 public static String alipay_public_key = "你的支付宝公钥" // 「沙箱环境」服务器异步通知页面路径。如果非公网地址建议使用 natapp 等内网穿透工具 public static String notify_url = "异步支付结果通知地址"; // 「沙箱环境」页面跳转同步通知页面路径 需http://格式的完整路径,必须外网可以正常访问,才会同步跳转 public static String return_url = "同步页面回调地址,可自动返回商户页面"; // 「沙箱环境」 public static String gatewayUrl = "支付宝网关地址"; // 签名方式 public static String sign_type = "RSA2"; // 字符编码格式 public static String charset = "utf-8"; @Test public void test_AliPay() throws AlipayApiException { AlipayClient alipayClient = new DefaultAlipayClient(gatewayUrl, app_id, app_private_key, "json", charset, alipay_public_key, sign_type); AlipayTradePagePayRequest request = new AlipayTradePagePayRequest(); // 发送请求的 Request类 request.setNotifyUrl(notify_url); // request.setReturnUrl(return_url); JSONObject bizContent = new JSONObject(); bizContent.put("out_trade_no", "daniel82AAAA000032333361X02"); // 我们自己生成的订单编号 bizContent.put("total_amount", "0.01"); // 订单的总金额 bizContent.put("subject", "测试商品"); // 支付的名称 bizContent.put("product_code", "FAST_INSTANT_TRADE_PAY"); // 固定配置 request.setBizContent(bizContent.toString()); String form = alipayClient.pageExecute(request).getBody(); log.info("测试结果:{}", form); } }
- 这是支付宝沙箱支付 SDK 支付宝扫码的调用案例,本身这个 SDK 支持了支付宝所有的支付方式,非常方便。
- 支付宝沙箱不影响实际环境,且容易申请,如果可能,可以申请微信支付、
- 案例运行成功后,会拿到一个html表单,这是一个动态表单,直接加载html就可以去往支付界面。
具体实现
创建订单
@RequestMapping(value = "create_pay_order", method = RequestMethod.POST) public Response<String> createParOrder(@RequestHeader("Authorization") String token, @RequestParam Integer productId) { try { // 1. 校验token boolean success = authService.checkToken(token); if (!success) { return Response.<String>builder() .code(Constants.ResponseCode.TOKEN_ERROR.getCode()) .info(Constants.ResponseCode.TOKEN_ERROR.getInfo()) .build(); } // 2. token解析 String openId = authService.getOpenId(token); assert openId != null; log.info("用户商品下单,根据商品ID创建支付单开始 openid:{} productId:{}", openId, productId); ShopCartEntity shopCartEntity = ShopCartEntity.builder() .openid(openId) .productId(productId) .build(); PayOrderEntity orderEntity = orderService.createOrder(shopCartEntity); log.info("用户商品下单,根据商品ID创建支付单完成 openid: {} productId: {} orderPay: {}", openId, productId, orderEntity.toString()); return Response.<String>builder() .code(Constants.ResponseCode.SUCCESS.getCode()) .info(Constants.ResponseCode.SUCCESS.getInfo()) .data(orderEntity.getPayUrl()) .build(); }catch (Exception e) { log.error("用户商品下单,根据商品ID创建支付单失败", e); return Response.<String>builder() .code(Constants.ResponseCode.UN_ERROR.getCode()) .info(Constants.ResponseCode.UN_ERROR.getInfo()) .build(); } }
public interface IOrderService { /** * 用户下单,通过购物车信息,返回下单后的支付单 * * @param shopCartEntity 简单购物车 * @return 支付单实体对象 */ PayOrderEntity createOrder(ShopCartEntity shopCartEntity); //... 省略其他方法 } @Override public PayOrderEntity createOrder(ShopCartEntity shopCartEntity) throws Exception { String openid = shopCartEntity.getOpenid(); // 1. 查询有效的未支付订单 UnpaidOrderEntity unpaidOrderEntity = orderRepository.queryUnpaidOrder(shopCartEntity); if (unpaidOrderEntity != null && unpaidOrderEntity.getPayStatus().equals(PayStatusVO.WAIT)) { log.info("创建订单-存在,已存在未支付订单,返回 openid: {} orderId: {} payUrl: {}", openid, unpaidOrderEntity.getOrderId(), unpaidOrderEntity.getPayUrl()); return PayOrderEntity.builder() .openid(openid) .payStatus(unpaidOrderEntity.getPayStatus()) .payUrl(unpaidOrderEntity.getPayUrl()) .orderId(unpaidOrderEntity.getOrderId()) .build(); }else if (unpaidOrderEntity != null && unpaidOrderEntity.getPayStatus().equals(PayStatusVO.CREATE)) { log.info("创建订单-存在,存在未创建支付单订单,返回 openid: {} orderId: {}", openid, unpaidOrderEntity.getOrderId()); PayOrderEntity payOrderEntity = this.doPrepayOrder(openid, unpaidOrderEntity.getOrderId(), unpaidOrderEntity.getProductName(), unpaidOrderEntity.getTotalAmount()); log.info("创建订单-完成,生成支付单。openid: {} orderId: {} payUrl: {}", openid, payOrderEntity.getOrderId(), payOrderEntity.getPayUrl()); return payOrderEntity; } // 2. 查询商品 ProductEntity productEntity = orderRepository.queryProduct(shopCartEntity.getProductId()); // 如果所购商品已下线 if (!productEntity.isAvailable()) { throw new ChatGPTException(Constants.ResponseCode.ORDER_PRODUCT_ERR.getCode(), Constants.ResponseCode.ORDER_PRODUCT_ERR.getInfo()); } // 3. 保存订单 OrderEntity orderEntity = this.doSaveOrder(openid, productEntity); // 4. 创建支付 PayOrderEntity payOrderEntity = this.doPrepayOrder(openid, orderEntity.getOrderId(), productEntity.getProductName(), orderEntity.getTotalAmount()); log.info("创建订单-完成,生成支付单。openid: {} orderId: {} payUrl: {}", openid, orderEntity.getOrderId(), payOrderEntity.getPayUrl()); return payOrderEntity; }
创建订单前,要查询有效的未支付订单,如果存在直接返回支付宝支付界面。避免创建一堆的订单。
此外我们做流程分析时候知道,还有可能是订单存在,但无支付单。那么这个时候需要主动创建一条支付单,再返回。
如果确实需要创建新订单,则需要根据购物车商品ID,查询出对应的商品信息,创建并保存订单,最后再创建支付单更新到订单上。
支付回调
@PostMapping("pay_notify") public void payNotify(HttpServletRequest request, HttpServletResponse response) throws IOException { try { log.info("支付回调,消息接收 {}", request.getParameter("trade_status")); if (request.getParameter("trade_status").equals("TRADE_SUCCESS")) { Map<String, String> params = new HashMap<>(); Map<String, String[]> requestParams = request.getParameterMap(); 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); } String tradeNo = params.get("out_trade_no"); String gmtPayment = params.get("gmt_payment"); String alipayTradeNo = params.get("trade_no"); String sign = params.get("sign"); Double total_amount = Double.valueOf(params.get("total_amount")); boolean checkSignature = AlipaySignature.rsaCheckV1(params, ALIPAY_PUBLIC_KEY, AlipayConfig.CHARSET, AlipayConfig.SIGNTYPE); // 支付宝验签通过 if (checkSignature) { // 验签通过 log.info("支付回调,交易名称: {}", params.get("subject")); log.info("支付回调,交易状态: {}", params.get("trade_status")); log.info("支付回调,支付宝交易凭证号: {}", params.get("trade_no")); log.info("支付回调,商户订单号: {}", params.get("out_trade_no")); log.info("支付回调,交易金额: {}", params.get("total_amount")); log.info("支付回调,买家在支付宝唯一id: {}", params.get("buyer_id")); log.info("支付回调,买家付款时间: {}", params.get("gmt_payment")); log.info("支付回调,买家付款金额: {}", params.get("buyer_pay_amount")); log.info("支付回调,支付回调,更新订单 {}", tradeNo); // 更新订单为已支付 boolean isSuccess = orderService.changeOrderPaySuccess(tradeNo, alipayTradeNo, BigDecimal.valueOf(total_amount), dateFormat.parse(gmtPayment)); if (isSuccess) { // 发布消息 // eventBus.post(tradeNo); rabbitTemplate.convertAndSend(PayNotifyConfig.PAYNOTIFY_EXCHANGE_FANOUT,"", tradeNo); } response.getWriter().write("<xml><return_code><![CDATA[SUCCESS]]></return_code></xml>"); }else { response.getWriter().write("<xml><return_code><![CDATA[FAIL]]></return_code></xml>"); } } }catch (Exception e) { log.error("支付失败", e); response.getWriter().write("<xml><return_code><![CDATA[FAIL]]></return_code></xml>"); } }
支付回调主要以支付宝支付的消息通知为主,更新订单并发送消息。这两步操作都是非常快的,不会让支付回调超时。
所以一般我们不是通过支付回调直接发货,因为发货流程更长,MQ消息解耦则是更好的方式。
注意 eventBus 是 Guava 的消息总线,类似于 MQ 消息。为了应对高并发已经改为了rabbitmq,如果业务简单可以继续用Guava
到这支付功能基本实现了,不过实际的商品下单支付场景肯定还会复杂,并且还得考虑异常,比如下单未支付,支付未发货,消息通知异常等等。小伙伴们可以思考哦。