启动项目测试流程
开启内网穿透 映射你启动项目的端口 自己访问一下是否通
启动程序 请求下单接口 /api/wx-pay/native/native/{productId}
{productId} 查看商品表数据的ID
复制返回的微信二维码地址
进入 https://cli.im/url 生成扫描二维码 使用微信扫描
等待微信回调
ok我们可以正常的接收到微信的回调我们需要根据回调的数据来处理自己系统的业务
修改回调方法 新增 processOrder 业务传递报文
log.info("通知验签成功:{}", bodyMap); // 通知回调 -> 更新订单状态逻辑 wxPayService.processOrder(bodyMap); log.info("回调业务处理完毕");
修改 WxPayService 服务类
可以搞redsi分布式锁根据实际业务来我们只是个demo就不要那么严谨
/** * 一个可重入互斥 锁 */ private final ReentrantLock lock = new ReentrantLock(); /** * 通知回调-> 更新订单状态逻辑 */ @Transactional(rollbackFor = Exception.class) @Override public void processOrder(Map<String, Object> bodyMap) throws GeneralSecurityException, InterruptedException { log.info("处理订单"); //解密报文 String plainText = decryptFromResource(bodyMap); // 将明文转换成map Map<String, Object> plainTextMap = JSONUtil.toBean(plainText, Map.class); String orderNo = (String) plainTextMap.get("out_trade_no"); // 微信特别提醒: // 在对业务数据进行状态检查和处理之前, // 要采用数据锁进行并发控制,以避免函数重入造成的数据混乱. // 尝试获取锁: // 成功获取则立即返回true,获取失败则立即返回false。不必一直等待锁的释放. if (lock.tryLock()) { try { // 处理重复的通知 // 接口调用的幂等性:无论接口被调用多少次,产生的结果是一致的。 OrderInfo orderInfo = orderInfoService.lambdaQuery().eq(OrderInfo::getOrderNo, (orderNo)).one(); if (null != orderInfo && !OrderStatus.NOTPAY.getType().equals(orderInfo.getOrderStatus())) { log.info("重复的通知,已经支付成功啦"); return; } // 模拟通知并发 //TimeUnit.SECONDS.sleep(5); // 更新订单状态 orderInfoService.lambdaUpdate().eq(OrderInfo::getOrderNo, orderNo).set(OrderInfo::getOrderStatus, OrderStatus.SUCCESS.getType()).update(); log.info("更新订单状态,订单号:{},订单状态:{}", orderNo, OrderStatus.SUCCESS); // 记录支付日志 paymentInfoService.createPaymentInfo(plainText); } finally { // 要主动释放锁 lock.unlock(); } } }
扩展本次不运用到项目当中
### ⚠️ 以上基本就ok了但是在订单服务是不是还缺少点什么? #### 方式一 🔒 植入分布式锁 LOCK4J ``` <!-- lock4j mybatiplus 扩展--> <dependency> <groupId>com.baomidou</groupId> <artifactId>lock4j-redisson-spring-boot-starter</artifactId> <version>2.2.5</version> </dependency> <!--redisson--> <dependency> <groupId>org.redisson</groupId> <artifactId>redisson-spring-boot-starter</artifactId> <version>3.21.1</version> </dependency> ``` 修改application.yml ```yml # redis配置 --- # 分布式锁配置 #acquire-timeout 可以理解为排队时长,超过这个时才就退出排队,抛出获取锁超时异常。 #为什么必须要有这个参数?现实你会一直排队等下去吗?所有人都一直排队有没有问题 ? #expire 锁过期时间 。 主要是防止死锁。 建议估计好你锁方法运行时常,正常没有复杂业务的增删改查最多几秒, #留有一定冗余,10秒足够。 我们默认30秒是为了兼容绝大部分场景。 lock4j: acquire-timeout: 3000 #默认值3s,可不设置 expire: 30000 #默认值30s,可不设置 primary-executor: com.baomidou.lock.executor.RedissonLockExecutor lock-key-prefix: lock4j #锁key前缀, 默认值lock4j,可不设置 spring: data: redis: database: 6 url: 127.0.0.1 port: 6391 password: '123456' # 分布式redis配置 redisson: # redis key前缀 keyPrefix: yby6 # 线程池数量 threads: 4 # Netty线程池数量 nettyThreads: 8 # 单节点配置 singleServerConfig: # 客户端名称 clientName: yangbuyiya # 最小空闲连接数 connectionMinimumIdleSize: 8 # 连接池大小 connectionPoolSize: 32 # 连接空闲超时,单位:毫秒 idleConnectionTimeout: 10000 # 命令等待超时,单位:毫秒 timeout: 3000 # 发布和订阅连接池大小 subscriptionConnectionPoolSize: 50 ``` 使用方式参考 https://gitee.com/baomidou/lock4j > ⚠️ 上面分布式锁作为扩展知识点分布式锁要求唯一ID 比如当前登陆用户唯一ID 目前我们没有登陆功能则使用其他代替锁
参数解密
/** * 对称解密 */ private String decryptFromResource(Map<String, Object> bodyMap) throws GeneralSecurityException { log.info("密文解密"); //通知数据拿到 resource 节点 Map<String, String> resourceMap = (Map) bodyMap.get("resource"); //数据密文 String ciphertext = resourceMap.get("ciphertext"); //随机串 String nonce = resourceMap.get("nonce"); //附加数据 String associatedData = resourceMap.get("associated_data"); log.info("密文 ===> {}", ciphertext); AesUtil aesUtil = new AesUtil(wxPayConfig.getApiV3Key().getBytes(StandardCharsets.UTF_8)); // 使用key、nonce和associated_data,对数据密文resource.ciphertext进行解密,得到JSON形式的资源对象 String plainText = aesUtil.decryptToString(associatedData.getBytes(StandardCharsets.UTF_8), nonce.getBytes(StandardCharsets.UTF_8), ciphertext); log.info("明文 ===> {}", plainText); return plainText; }
记录支付日志
引入
/** * 支付日志 */ private final PaymentInfoService paymentInfoService;
修改 PaymentInfoService
package com.yby6.service; import cn.hutool.core.map.MapUtil; import cn.hutool.json.JSONUtil; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.yby6.domain.PaymentInfo; import com.yby6.enums.PayType; import com.yby6.mapper.PaymentInfoMapper; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.util.Map; @Slf4j @Service public class PaymentInfoService extends ServiceImpl<PaymentInfoMapper, PaymentInfo> { /** * 创建付款信息 * * @param plainText 纯文本 */ @Transactional public void createPaymentInfo(String plainText) { log.info("记录支付日志: {}", plainText); Map<String, Object> plainTextMap = JSONUtil.toBean(plainText, Map.class); //订单号 String orderNo = (String) plainTextMap.get("out_trade_no"); //业务编号 String transactionId = (String) plainTextMap.get("transaction_id"); //支付类型 String tradeType = (String) plainTextMap.get("trade_type"); //交易状态 String tradeState = (String) plainTextMap.get("trade_state"); //用户实际支付金额 Map<String, Object> amount = (Map<String, Object>) plainTextMap.get("amount"); Integer payerTotal = MapUtil.getInt(amount, "payer_total"); PaymentInfo paymentInfo = new PaymentInfo(); paymentInfo.setOrderNo(orderNo); paymentInfo.setPaymentType(PayType.WXPAY.getType()); paymentInfo.setTransactionId(transactionId); paymentInfo.setTradeType(tradeType); paymentInfo.setTradeState(tradeState); paymentInfo.setPayerTotal(payerTotal); paymentInfo.setContent(plainText); baseMapper.insert(paymentInfo); } }
启动项目测试流程
开启内网穿透 映射你启动项目的端口 自己访问一下是否通
启动程序 请求下单接口 /api/wx-pay/native/native/{productId}
{productId} 查看商品表数据的ID
复制返回的微信二维码地址
进入 https://cli.im/url 生成扫描二维码 使用微信扫描
等待微信回调处理系统业务订单状态更改
最后
本期结束咱们下次再见👋~
,关注我不迷路,如果本篇文章对你有所帮助,或者你有什么疑问,欢迎在评论区留言,我一般看到都会回复的。大家点赞支持一下哟~ 💗
【选题思路】
“技术源于生活” 为什么写微信支付这种项目的文章呢? 因为我看到市面上的文章都不全面不细节不小白话更加没有配套Demo!!!
从而我的从零玩转微信支付诞生啦~ 搭配PC端、Uniapp端的不同实现.
【写作提纲】
一、前言
通过前言表达我每次的文章内容是什么东西和注意事项
二、Native模式回调
介绍回调的思路、通知规则、通知报文、通知签名、签名验证、参数加解密、证书和回调包稳解密、支付通知、最后进行测试功能的集成!