实现微信支付(Native支付),使用WebSocket进行推送
——4.配置SpringBoot支持WebSocket,推送结果
依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
一、配置WebSocket
@Configuration
public class WebSocketConfig {
@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
}
二、创建WebSocketService
2.1 session缓存问题
向客户端发送消息,需要使用Session对象。但是这些生命周期函数都由于客户端某种操作,而触发执行的。如果客户端不触发操作,那么后端是无法主动给客户端发送消息的。所以我们要把Session对象缓存起来。需要的时候,我们提取缓存的Session,主动向客户端发送消息。
因为后端的WebSocket服务类是多例的,所以我们想要全局共享缓存,要么用Redis,要么声明静态的HashMap对象。如果选用Redis,那么保存Session对象要用到序列化,会消耗一定的时间,所以不建议使用。如果全局共享使用HashMap,又会存在并发读写的问题,最终我们选择ConcurrentHashMap类。
2.2 token问题
WebSocket不支持Cookie,所以我们要自己把Token字符串上传给服务端,我们要自己从Token字符串中提取UserId出来。
2.3 数据格式
在WebSocket中,我们要约定跟客户端传递数据的格式。为了能让数据看起来格式规整,我采用传递JSON字符串的方式。
参数 | 含义 | 例子 |
---|---|---|
opt | 操作行为 | ping |
token | 令牌字符串 | eyJzb2Z0d2FyZV9pZCI6IjROUkIxLTBYWkFCWkk5RTYtNVNNM1IiLCJjbGll |
其他参数 | 其他参数 | (略) |
@Slf4j
@ServerEndpoint(value = "/socket")
@Component
public class WebSocketService {
//用于保存WebSocket连接对象
public static ConcurrentHashMap<String, Session> sessionMap = new ConcurrentHashMap<>();
/**
* 连接建立成功调用的方法
*/
@OnOpen
public void onOpen(Session session) {
}
/**
* 连接关闭调用的方法
*/
@OnClose
public void onClose(Session session) {
Map map = session.getUserProperties();
if (map.containsKey("userId")) {
String userId = MapUtil.getStr(map, "userId");
sessionMap.remove(userId);
}
}
/**
* 接收消息
*
* @param message
* @param session
*/
@OnMessage
public void onMessage(String message, Session session) {
//把字符串转换成JSON
JSONObject json = JSONUtil.parseObj(message);
String opt = json.getStr("opt");
if("ping".equals(opt)){
return;
}
//从JSON中取出Token
String token = json.getStr("token");
//从Token取出userId
String userId = StpUtil.stpLogic.getLoginIdByToken(token).toString();
//取出Session绑定的属性
Map map = session.getUserProperties();
//如果没有userId属性,就给Session绑定userId属性,关闭连接的时候会用到
if (!map.containsKey(userId)) {
map.put("userId", userId);
}
//把Session缓存起来
if (sessionMap.containsKey(userId)) {
//替换缓存中的Session
sessionMap.replace(userId, session);
} else {
//向缓存添加Session
sessionMap.put(userId, session);
}
sendInfo("ok",userId);
}
@OnError
public void onError(Session session, Throwable error) {
log.error("发生错误", error);
}
/**
* 发送消息给客户端
*/
public static void sendInfo(String message, String userId) {
if (StrUtil.isNotBlank(userId) && sessionMap.containsKey(userId)) {
//从缓存中查找到Session对象
Session session = sessionMap.get(userId);
//发送消息
sendMessage(message, session);
}
}
/**
* 封装发送消息给客户端
*/
private static void sendMessage(String message, Session session) {
try {
session.getBasicRemote().sendText(message);
} catch (Exception e) {
log.error("执行异常", e);
}
}
}
三、推送付款结果
之前我们已经实现了:后端接收到微信平台发送的付款成功通知,接下来只需要用WebSocket技术把结果推送给前端页面。
3.1 持久层
因为微信平台发送过来的XML数据中,没有userId,所以我们想根据userId查找缓存的Session,就有问题。所以要先查出userId才能发起推送。
<select id="searchUserIdByUUID" parameterType="String" resultType="Integer">
SELECT user_id AS userId
FROM tb_amect
WHERE uuid = #{uuid}
</select>
public interface TbAmectDao {
……
public int searchUserIdByUUID(String uuid);
}
3.2 业务层代码
public interface AmectService {
……
public int searchUserIdByUUID(String uuid);
}
public class AmectServiceImpl implements AmectService {
……
@Override
public int searchUserIdByUUID(String uuid) {
int userId = amectDao.searchUserIdByUUID(uuid);
return userId;
}
}
3.3 Web层
修改之前的recieveMessage,在 //TODO 向前端页面推送付款结果 处添加代码
public class AmectController {
……
@Operation(summary = "接收消息通知")
@RequestMapping("/recieveMessage")
public void recieveMessage(HttpServletRequest request, HttpServletResponse response) throws Exception {
……
int rows = amectService.updateStatus(param);
if (rows == 1) {
//根据罚款单ID查询用户ID
int userId = amectService.searchUserIdByUUID(outTradeNo);
//向用户推送结果
WebSocketService.sendInfo("收款成功", userId + "");
……
}
……
}
}
四、前端接收数据
1.在前端项目启动是建立WebSocket连接
//使用WebSocket,后端项目给前端页面推送通知更
import VueNativeSock from "vue-native-websocket-vue3";
app.use(VueNativeSock,"ws://192.168.99.216:8090/emos-api/socket",{
"format": "json"
});
2.发送轮询请求
连接创建之后,真正开始使用是在用户登陆系统之后,也就是进入到首页。在该页面的created()函数中,添加了轮询的ping请求,防止WebSocket连接因为超时被切断。
重建连接不如轮询维持连接,这样资源浪费会小一点,当然你也可以按自己的想法来
created() { let that = this; that.routeHandle(that.$route); //当WebSocket连接创建成功之后,会触发这个回调函数的运行 that.$options.sockets.onopen = function(resp) { //发送心跳检测,避免超时后服务端切断连接 setInterval(function() { that.$socket.sendObj({ opt: 'ping' }); }, 60 * 1000); };},
3.接收付款结果
在之前创建订单的前端代码之前,调用webSocket接收回调,使用$nextTick接收异步回调
需要特别注意,在接收消息之前,我们要先想后端发送一条消息。因为后端onMessage()方法,遇到ping请求,是不会把Session缓存起来的。所以我们要随便发一个不是ping的请求给后端,这样它才能缓存Session,将来才可以给我推送消息。
下面为核心代码
that.$nextTick(()=>{
// 利用WebSocket接受后端推送的付款结果
//从浏览器localStorage中获取Token令牌
let token = localStorage.getItem("token");
//向WebSocket服务类发送消息,让服务类缓存Session对象,可以推送消息给当前页面
that.$socket.sendObj({opt:'pay_amect',token:token});
//接收服务端推送的消息
that.$options.sockets.onmessage = function(resp){
//console.log("reps",resp);
let data = resp.data;
if(data == '收款成功'){
that.result = true;
}
}
that.$http('amect/createNativeAmectPayOrder', 'POST', { amectId: id }, true, function(resp) {
that.qrCode = resp.qrCodeBase64;
});
})
},
到此,微信支付功能已基本实现,但这还不够完善,如果后端或用户没有收到微信服务器发来的付款成功的消息的话,可能存在隐患,所以我们还要添加一个主动查询付款是否成功的功能。
五、主动查询付款结果
5.1 微信官方API
微信官方的API接口( https://pay.weixin.qq.com/wiki/doc/api/native.php?chapter=9_2) 给我们提供了查询付款结果的功能
参数 | 含义 | 类型 | 示例 |
---|---|---|---|
appid | 开发者账号的APPID | String | wxd678efh567hg6787 |
mch_id | 商户号ID | String | 1230000109 |
transaction_id | 支付订单ID | String | 1009660380201506130 |
out_trade_no | 商品订单ID | String | 20150806125346 |
nonce_str | 随机字符串 | String | C380BEC2BFD727A4B6845133519F3AD6 |
sign | 数字签名 | String | 5K8264ILTKCH16CQ2502SI8ZNMTM67VS |
返回的响应,我们只需要看4个参数就可以了。
参数 | 含义 | 类型 | 示例 |
---|---|---|---|
return_code | 通信状态码 | String | SUCCESS |
result_code | 业务状态码 | String | SUCCESS |
trade_state | 交易状态码 | String | SUCCESS |
sign | 数字签名 | String | 5K8264ILTKCH16CQ2502SI8ZNMTM67VS |
5.2 业务层代码
public interface AmectService {
……
public void searchNativeAmectPayResult(HashMap param);
}
注意:修改一处代码
把wxPay.isPayResultNotifySignatureValid(result))
改为WXPayUtil.isSignatureValid(result,myWXPayConfig.getKey(), WXPayConstants.SignType.HMACSHA256)
原因是isPayResultNotifySignatureValid中是根据微信服务器
返回的sign_type,来判断签名类型的,如果没有返回sign_type参数(即为null),
则默认为MD5,最重要的是我发现吧,查询支付是否成功微信服务器返回的结果里
还真就没有sign_type,你可以自己尝试一下或者查看微信官网的API接口,
所以调用这个方法验证签名就是MD5,但微信支付使用的是HMACSHA256,
可想而知怎么验证都不正确。
之前我没考虑到这个,以为是只能使用MD5,所以我直接修改了微信支付工具类,
直接改为用MD5,后来觉得使用MD5不需要密钥很不安全,重新debug后发现原来
是这个原因。现在改回用HMACSHA256,需要修改三处代码,这是其中一处
源代码如下:
另外两处:
实现微信支付(Native支付),使用WebSocket进行推送——2.微信支付工具类:中的二、官网给的工具类 的 2.1 WXPay类 核心类
以及:实现微信支付(Native支付),使用WebSocket进行推送——3.创建支付订单,接收付款结果:中的二、接收付款结果 的 3.编写Web层代码
这是微信支付的官方文档:微信支付官网API查询支付结果返回参数
public boolean isPayResultNotifySignatureValid(Map<String, String> reqData) throws Exception {
String signTypeInData = reqData.get(WXPayConstants.FIELD_SIGN_TYPE);
SignType signType;
if (signTypeInData == null) {
signType = SignType.MD5;
} else {
signTypeInData = signTypeInData.trim();
if (signTypeInData.length() == 0) {
signType = SignType.MD5;
} else if (WXPayConstants.MD5.equals(signTypeInData)) {
signType = SignType.MD5;
} else if (WXPayConstants.HMACSHA256.equals(signTypeInData)) {
signType = SignType.HMACSHA256;
} else {
throw new Exception(String.format("Unsupported sign_type: %s", signTypeInData));
}
}
return WXPayUtil.isSignatureValid(reqData, this.config.getKey(), signType);
}
public class AmectServiceImpl implements AmectService {
……
@Override
public void searchNativeAmectPayResult(HashMap param) {
HashMap map = amectDao.searchAmectByCondition(param);
if(MapUtil.isNotEmpty(map)){
String uuid = MapUtil.getStr(map,"uuid");
param.clear();
param.put("appid", myWXPayConfig.getAppID());
param.put("mch_id", myWXPayConfig.getMchID());
param.put("out_trade_no", uuid);
param.put("nonce_str", WXPayUtil.generateNonceStr());
try {
String sign = WXPayUtil.generateSignature(param, myWXPayConfig.getKey());
param.put("sign",sign);
WXPay wxPay = new WXPay(myWXPayConfig);
Map<String,String> result = wxPay.orderQuery(param);
//修改这段代码,把wxPay.isPayResultNotifySignatureValid(result))改为WXPayUtil.isSignatureValid(result,myWXPayConfig.getKey(), WXPayConstants.SignType.HMACSHA256)
if(WXPayUtil.isSignatureValid(result,myWXPayConfig.getKey(), WXPayConstants.SignType.HMACSHA256)){
String resultCode = result.get("result_code");
String returnCode = result.get("return_code");
if("SUCCESS".equals(returnCode) && "SUCCESS".equals(resultCode)){
String tradeState = result.get("trade_state");
//查询到订单支付成功
if("SUCCESS".equals(tradeState)){
//更新订单状态
amectDao.updateStatus(new HashMap(){{
put("uuid",uuid);
put("status",2);
}});
}
}
}else {
log.error("数字签名异常");
throw new EmosException("数字签名异常");
}
} catch (Exception e) {
log.error("执行异常", e);
throw new EmosException("执行异常");
}
}
}
}
5.3 WEB层
@Data
@Schema(description = "查询Navtive支付罚款单支付结果表单")
public class SearchNativeAmectPayResultForm {
@NotNull(message = "amectId不能为空")
@Min(value = 1, message = "amectId不能小于1")
@Schema(description = "罚款单ID")
private Integer amectId;
}
public class AmectController {
……
@PostMapping("/searchNativeAmectPayResult")
@Operation(summary = "查询Native支付罚款订单结果")
@SaCheckLogin
public R searchNativeAmectPayResult(@Valid @RequestBody SearchNativeAmectPayResultForm form) {
int userId = StpUtil.getLoginIdAsInt();
int amectId = form.getAmectId();
HashMap param = new HashMap() {{
put("amectId", amectId);
put("userId", userId);
put("status", 1);
}};
amectService.searchNativeAmectPayResult(param);
return R.ok();
}
}
5.4 前端
前端只要提供方法,发送ajax访问该API即可