实现微信支付(Native支付),使用WebSocket进行推送 ——4.配置SpringBoot支持WebSocket,推送结果

实现微信支付(Native支付),使用WebSocket进行推送

——4.配置SpringBoot支持WebSocket,推送结果

依赖

        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-websocketartifactId>
        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即可

微信支付系列其他部分

你可能感兴趣的:(笔记,微信支付,websocket,微信,spring,boot)