订单支付
1.1 微信支付介绍
微信支付产品
- 付款码支付,用户打开付款码,让商家扫
- JSAPI支付,网页端调取微信支付
- 小程序支付,小程序中调取微信支付
- Native支付,我们的微信生成一个码,别人扫我们支付
- APP支付,在APP内调取微信支付
- 刷脸支付

1.2 微信支付准备工作
微信支付接入流程
- 提交资料
- 签署协议
- 绑定场景

微信小程序支付

用户请求下单: 用户在小程序选好商品,点击“支付”。
商家预下单(关键): 商家系统接收请求,先在自己后台生成订单,然后调用微信支付的“统一下单接口”。

mchid为商户id;out_trade_no为商户业务系统的订单号;appid为小程序appid,需要和商户id绑定;notify_url为用户支付成功后,微信通知商户的回调地址;amount有两个字段,total为支付金额,currency为支付币种;payer中的openid为微信用户id标识;
微信预支付响应: 微信后台返回一个预支付交易会话标识(prepay_id)。
二次签名(安全核心): 商家系统拿到
prepay_id后,不能直接给用户,必须进行再次签名(图中的步骤7),生成前端调起支付所需的参数。前端唤起支付: 用户收到商家传回的参数,小程序自动弹出微信支付密码输入框。

双向通知(闭环):
- 同步: 用户支付完,微信直接在界面显示结果给用户。
- 异步(最重要): 微信后台会给商家系统发送一个回调通知(步骤13),商家根据这个通知更新订单状态(步骤14),这才算真正完成交易。
微信支付过程需要使用到微信支付平台证书、商户私钥文件。
工具 存放位置 核心作用 比喻 商户私钥 你的服务器 对请求进行签名 你的个人私章 商户公钥 微信服务器 验证你的请求签名 微信手里留存的你的印鉴卡 微信平台证书 你的服务器 验证微信的回调签名 微信给你的官方授权信 微信支付接口的回调url,需要公网ip。课程中推荐了cpolar工具,其可以给主机的某个端口映射一个临时域名,从而实现微信支付的回调通知功能,实现内网穿透。但是我已经租用了一台阿里云服务器,Gemini给我推荐了frp工具,以下是两者对比。
维度 cpolar (免费/基础版) frp (基于你的阿里云) 带宽限制 通常限制在 1-2Mbps(较慢) 无限制(取决于你阿里云的带宽,通常 5Mbps+) 域名/IP 稳定性 免费版域名每 24 小时随机更换 固定公网 IP(你的阿里云 IP 永远不变) 连接数限制 有并发连接限制(容易断开) 无限制(取决于服务器内存,2G 足够撑起整个开发组) 端口选择 随机分配端口 自定义(你可以直接映射到公网的 80 或 443 端口) 安全性 闭源软件,数据经过 cpolar 转发 开源透明,数据只在你的阿里云和本地之间传输 成本 固定域名和高速带宽需按月付费 完全免费(因为你已经付过云服务器的钱了) frp使用:
frp分为frps 和 frpc,frps运行在服务端,frpc在客户端,请求frps,frps会按照规则将请求转发到frpc。
服务器 frps.toml 文件编写:

本地 frpc.toml 文件编写:

之后在服务器上开启frps服务,在本机开启frpc服务即可。可以进一步将服务器上的frps注册为系统服务
可以访问服务器的7500端口可视化管理转发
==之后微信支付回调我们服务器的80端口,服务器即可将流量转发给我们的Java程序==
1.3 代码开发
导入微信支付相关配置

用户(小程序)请求支付,商家先预下单,调用微信支付的“统一下单接口”,微信会返回一个预支付交易会话标识(prepay_id),进行再次签名,生成前端调起支付所需的参数。
代码:
java/** * 订单支付 * * @param ordersPaymentDTO * @return */ @PutMapping("/payment") @ApiOperation("订单支付") public Result<OrderPaymentVO> payment(@RequestBody OrdersPaymentDTO ordersPaymentDTO) throws Exception { log.info("订单支付:{}", ordersPaymentDTO); OrderPaymentVO orderPaymentVO = ordersService.payment(ordersPaymentDTO); log.info("生成预支付交易单:{}", orderPaymentVO); return Result.success(orderPaymentVO); }java/** * 订单支付 * * @param ordersPaymentDTO * @return */ public OrderPaymentVO payment(OrdersPaymentDTO ordersPaymentDTO) throws Exception { // 当前登录用户id Long userId = BaseContext.getCurrentId(); User user = userMapper.getById(userId); //调用微信支付接口,生成预支付交易单 // 这里由工具类封装 JSONObject jsonObject = weChatPayUtil.pay( ordersPaymentDTO.getOrderNumber(), //商户订单号 new BigDecimal(0.01), //支付金额,单位 元 "苍穹外卖订单", //商品描述 user.getOpenid() //微信用户的openid ); if (jsonObject.getString("code") != null && jsonObject.getString("code").equals("ORDERPAID")) { throw new OrderBusinessException("该订单已支付"); } OrderPaymentVO vo = jsonObject.toJavaObject(OrderPaymentVO.class); vo.setPackageStr(jsonObject.getString("package")); return vo; }用户确认支付,微信支付完成后,会回调我们的Java后端服务,我们解析微信响应,确认无误后,最后更新订单状态
代码:
java/** * 支付成功回调 * * @param request */ @RequestMapping("/paySuccess") public void paySuccessNotify(HttpServletRequest request, HttpServletResponse response) throws Exception { //读取数据 String body = readData(request); log.info("支付成功回调:{}", body); //数据解密 String plainText = decryptData(body); log.info("解密后的文本:{}", plainText); JSONObject jsonObject = JSON.parseObject(plainText); String outTradeNo = jsonObject.getString("out_trade_no");//商户平台订单号 String transactionId = jsonObject.getString("transaction_id");//微信支付交易号 log.info("商户平台订单号:{}", outTradeNo); log.info("微信支付交易号:{}", transactionId); //业务处理,修改订单状态、来单提醒 ordersService.paySuccess(outTradeNo); //给微信响应 responseToWeixin(response); } /** * 读取数据 * * @param request * @return * @throws Exception */ private String readData(HttpServletRequest request) throws Exception { BufferedReader reader = request.getReader(); StringBuilder result = new StringBuilder(); String line = null; while ((line = reader.readLine()) != null) { if (result.length() > 0) { result.append("\n"); } result.append(line); } return result.toString(); } /** * 数据解密 * * @param body * @return * @throws Exception */ private String decryptData(String body) throws Exception { JSONObject resultObject = JSON.parseObject(body); JSONObject resource = resultObject.getJSONObject("resource"); String ciphertext = resource.getString("ciphertext"); String nonce = resource.getString("nonce"); String associatedData = resource.getString("associated_data"); AesUtil aesUtil = new AesUtil(weChatProperties.getApiV3Key().getBytes(StandardCharsets.UTF_8)); //密文解密 String plainText = aesUtil.decryptToString(associatedData.getBytes(StandardCharsets.UTF_8), nonce.getBytes(StandardCharsets.UTF_8), ciphertext); return plainText; } /** * 给微信响应 * @param response */ private void responseToWeixin(HttpServletResponse response) throws Exception{ response.setStatus(200); HashMap<Object, Object> map = new HashMap<>(); map.put("code", "SUCCESS"); map.put("message", "SUCCESS"); response.setHeader("Content-type", ContentType.APPLICATION_JSON.toString()); response.getOutputStream().write(JSONUtils.toJSONString(map).getBytes(StandardCharsets.UTF_8)); response.flushBuffer(); }java/** * 支付成功,修改订单状态 * * @param outTradeNo */ public void paySuccess(String outTradeNo) { // 根据订单号查询订单 Orders ordersDB = ordersMapper.getByNumber(outTradeNo); // 根据订单id更新订单的状态、支付方式、支付状态、结账时间 Orders orders = Orders.builder() .id(ordersDB.getId()) .status(Orders.TO_BE_CONFIRMED) .payStatus(Orders.PAID) .checkoutTime(LocalDateTime.now()) .build(); ordersMapper.update(orders); }