• 企业微信PC版应用跳转到默认浏览器,避坑指南,欢迎补充。。。


    引子

    我们公司内部用企业微信沟通,最近有个需求,一个应用在企业微信PC版打开时,要自动跳转到PC的默认浏览器。在开发过程中,我经历了几个坑,在这里记录一下,希望对你有所帮助。

    我在网上查了下资料,打开系统默认浏览器需要用到企业微信JS-SDK,官方文档的介绍如下:

    企业微信JS-SDK是企业微信面向网页开发者提供的基于企业微信内的网页开发工具包。
    通过使用企业微信JS-SDK,网页开发者可借助企业微信高效地使用拍照、选图、语音、位置等手机系统的能力,同时可以直接使用企业微信分享、扫一扫等企业微信特有的能力,为企业微信用户提供更优质的网页体验。

    查了下文档,企业微信支持打开系统默认浏览器,需要调用openDefaultBrowser方法:
    在这里插入图片描述
    但是为了调用openDefaultBrowser方法之前,必须先注入配置信息,否则将无法调用。

    JS-SDK配置信息使用说明

    坑一

    我在网上找了下,有的说是要通过agentConfig注入应用的权限,我用agentConfig试了下没有成功(有可能是我的原因,agentConfig也许也可以),我碰到了第一坑。

    要调用openDefaultBrowser方法,其实用config接口注入权限验证配置就行了,不需要用agentConfig。

    写代码

    接下来就要写具体的代码来实现了,我在网上找了一篇写的很棒的文章:vue项目中企业微信使用js-sdk时config和agentConfig配置

    看了下,代码写的不错,我大部分代码就直接拷贝自这篇文章,由于我这个项目用的是jsp,前端页面有点不一样,后端代码差不多。

    前端页面

    <%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
    <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
    <c:set var="path" value="${pageContext.request.contextPath}">c:set>
    
    DOCTYPE html>
    <html>
    <head>
        
        <script src="//res.wx.qq.com/open/js/jweixin-1.2.0.js">script>
        <script src="${path }/static/js/jquery-3.1.1.js">script>
        <script type="text/javascript">
            $(function(){
                // 调用接口请求需要的参数回来
                $.ajax({
                    url: "/wechat/getWeiXinPermissionsValidationConfig",
                    data: {
                    	// 当前网页的URL,不包含#及其后面部分,签名算法的时候会用到
                        url: window.location.href.split("#")[0]
                    },
                    type: "get",
                    success: function (res) {
                        console.log('res------------->', res.data)
                        wx.config({
                            beta: true,// 必须这么写,否则wx.invoke调用形式的jsapi会有问题
                            debug: false, // 开启调试模式,调用的所有api的返回值会在客户端alert出来,若要查看传入的参数,可以在pc端打开,参数信息会通过log打出,仅在pc端时才会打印。
                            appId: res.data.corpid, // 必填,企业微信的corpid,必须与当前登录的企业一致
                            timestamp: res.data.timestamp, // 必填,生成签名的时间戳
                            nonceStr: res.data.nonceStr, // 必填,生成签名的随机串
                            signature: res.data.signature,// 必填,签名,见附录-JS-SDK使用权限签名算法
                            jsApiList: ['openDefaultBrowser'] //必填,传入需要使用的接口名称
                        })
    
                        wx.ready(function(){
                            openDefaultBrowser()
                        })
    
                        wx.error(function(res){
                            // config信息验证失败会执行error函数,如签名过期导致验证失败,具体错误信息可以打开config的debug模式查看,也可以在返回的res参数中查看,对于SPA可以在这里更新签名。
                            console.log(res);
                        })
                    }
                })
    
                function openDefaultBrowser() {
                    wx.invoke('openDefaultBrowser', {
                        // 在默认浏览器打开redirect_uri,并附加code参数;也可以直接指定要打开的url,此时不会附带上code参数。
                        'url': "https://open.weixin.qq.com/connect/oauth2/authorize?appid=******&redirect_uri=http%3A%2F%2Fabc.com%3A6868%2Fwechat%2Fpc&response_type=code&scope=snsapi_userinfo&agentid=**&state=STATE#wechat_redirect"
                    }, function(res){
                        console.log('res------------->', res)
                        if(res.err_msg != "openDefaultBrowser:ok"){
                            //错误处理
                        }
                    })
                }
            })
        script>
        <title>跳转页面title>
    head>
    <body>
        <p style="margin-left: 40%;margin-top: 10%">自动跳转到电脑端默认浏览器p>
    body>
    html>
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63

    由于调用wx.config接口需要appId、timestamp、nonceStr、signature这些参数,而这些参数的值必须和后台生成签名时的值一样,所以这些参数必须从后台获取。

    这里调用的接口是“/wechat/getWeiXinPermissionsValidationConfig”,这个是自定义的接口,也就是我们要接下来写的后端代码。

    后端代码

    首先是跳转到上面前端页面的代码。

    /**
     * 企业微信PC端跳转到默认浏览器页面
     * @return
     */
    @RequestMapping(value = "/wechat/openDefaultBrowser")
    public ModelAndView openDefaultBrowser(){
    	ModelAndView model = new ModelAndView("/userInfo/openDefaultBrowser");
    	return model;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    下面是获取JS-SDK使用权限签名,具体步骤参考:JS-SDK使用权限签名算法

    签名生成规则如下:
    参与签名的参数有四个: noncestr(随机字符串), jsapi_ticket(如何获取参考“获取企业jsapi_ticket”以及“获取应用的jsapi_ticket接口”), timestamp(时间戳), url(当前网页的URL, 不包含#及其后面部分)

    简单地说,我们需要一个加密的签名,生成这个签名又需要 jsapi_ticket,所以生成签名之前要先获取 jsapi_ticket。

    public class WeixinHelper {
    
    	private static final Logger LOGGER = LoggerFactory.getLogger(WeixinHelper.class);
    
    	// 企业id
    	public static final String APP_ID = "********";
    	// CRM电脑端
    	public static final String CRM_PC_AGENT_ID = "********";
    	public static final String CRM_PC_CORPSECRET = "********";
    
    	/**
    	 * 存放ticket的容器
    	 */
    	private static Map<String, Ticket> ticketMap = new HashMap<>();
    
    	/**
    	 * 获取token
    	 * @return
    	 */
    	public static String getAccessToken(String secret) {
    		//获取token
    		String getTokenUrl = "https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid=" + WeixinHelper.APP_ID + "&corpsecret=" + secret;
    		String tokenContent = HttpService.get(getTokenUrl);
    		LOGGER.info("tokenContent = " + tokenContent);
    		JSONObject jsonObject = JSONObject.parseObject(tokenContent);
    		String accessToken = jsonObject.getString("access_token");
    		LOGGER.info("accessToken = " + accessToken);
    		return accessToken;
    	}
    
    	/**
    	 * 获取jsapi_ticket
    	 * @param secret
    	 * @param type
    	 * @return
    	 */
    	public static String getJsApiTicket(String secret, String type) {
    		String accessToken = getAccessToken(secret);
    		String key = accessToken;
    		if (!StringUtils.isEmpty(accessToken)) {
    			if ("agent_config".equals(type)){
    				key = type + "_" + accessToken;
    			}
    			Ticket ticket = ticketMap.get(key);
    			if (!ObjectUtils.isEmpty(ticket)) {
    				long now = Calendar.getInstance().getTime().getTime();
    				Long expiresIn = ticket.getExpiresIn();
    				//有效期内的ticket 直接返回
    				if (expiresIn - now > 0) {
    					return ticket.getTicket();
    				}
    			}
    			ticket = getJsApiTicketFromWeChatPlatform(accessToken, type);
    			if (ticket != null) {
    				ticketMap.put(key, ticket);
    				return ticket.getTicket();
    			}
    		}
    		return null;
    	}
    
    
    	/**
    	 * 获取企业的jsapi_ticket或应用的jsapi_ticket
    	 * @param accessToken
    	 * @param type 为agent_config时获取应用的jsapi_ticket,否则获取企业的jsapi_ticket
    	 * @return
    	 */
    	public static Ticket getJsApiTicketFromWeChatPlatform(String accessToken, String type) {
    		String url;
    		if ("agent_config".equals(type)) {
    			url = "https://qyapi.weixin.qq.com/cgi-bin/ticket/get?access_token=" + accessToken+ "&type=" + type;
    		} else {
    			url = "https://qyapi.weixin.qq.com/cgi-bin/get_jsapi_ticket?access_token=" + accessToken;
    		}
    		Long now = System.currentTimeMillis();
    		if (!StringUtils.isEmpty(accessToken)) {
    			String body = HttpService.get(url);
    			LOGGER.info("ticketContent = " + body);
    			if (!StringUtils.isEmpty(body)) {
    				JSONObject object = JSON.parseObject(body);
    				if (object.getIntValue("errcode") == 0) {
    					Ticket ticket = new Ticket();
    					ticket.setTicket(object.getString("ticket"));
    					ticket.setExpiresIn(now + object.getLongValue("expires_in") * 1000);
    					return ticket;
    				}
    			}
    		}
    		return null;
    	}
    
    
    	/**
    	 * 获取JS-SDK使用权限签名
    	 * @param ticket
    	 * @param nonceStr
    	 * @param timestamp
    	 * @param url
    	 * @return
    	 * @throws NoSuchAlgorithmException
    	 */
    	public static String getJSSDKSignature(String ticket, String nonceStr, long timestamp, String url) throws NoSuchAlgorithmException{
    		String unEncryptStr = "jsapi_ticket=" + ticket + "&noncestr=" + nonceStr + "×tamp=" + timestamp + "&url=" + url;
    		MessageDigest sha = MessageDigest.getInstance("SHA");
    		// 调用digest方法,进行加密操作
    		byte[] cipherBytes = sha.digest(unEncryptStr.getBytes());
    		String encryptStr = Hex.encodeHexString(cipherBytes);
    		return encryptStr;
    	}
    
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113

    Ticket 类:

    public class Ticket {
    
        private String ticket;
        private Long expiresIn;
    
        public Ticket() {
    
        }
    
        public Ticket(String ticket, Long expiresIn) {
            this.ticket = ticket;
            this.expiresIn = expiresIn;
        }
    
        public String getTicket() {
            return ticket;
        }
    
        public void setTicket(String ticket) {
            this.ticket = ticket;
        }
    
        public Long getExpiresIn() {
            return expiresIn;
        }
    
        public void setExpiresIn(Long expiresIn) {
            this.expiresIn = expiresIn;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30

    调用post、get方法的工具类:

    public class HttpService {
    
    	private static int readTimeout=25000;
    
    	private static int connectTimeout=25000;
    
    	private static final Logger LOGGER = LoggerFactory.getLogger(HttpService.class);
    	/**
    	 * 

    * POST方法 *

    * * @param sendUrl 访问URL * @param sendParam 参数串 * @return */
    public static String post(String sendUrl, String sendParam) { StringBuffer receive = new StringBuffer(); DataOutputStream dos = null; BufferedReader rd = null; HttpURLConnection URLConn = null; LOGGER.info("sendUrl = " + sendUrl + " sendParam = " + sendParam); try { URL url = new URL(sendUrl); URLConn = (HttpURLConnection)url.openConnection(); URLConn.setReadTimeout(readTimeout); URLConn.setConnectTimeout(connectTimeout); URLConn.setDoOutput(true); URLConn.setDoInput(true); URLConn.setRequestMethod("POST"); URLConn.setUseCaches(false); URLConn.setAllowUserInteraction(true); URLConn.setInstanceFollowRedirects(true); URLConn.setRequestProperty("Content-Type", "application/json; charset=UTF-8"); if (sendParam != null && sendParam.length() > 0) { URLConn.setRequestProperty("Content-Length", String.valueOf(sendParam.getBytes("UTF-8").length)); dos = new DataOutputStream(URLConn.getOutputStream()); dos.write(sendParam.getBytes("UTF-8")); dos.flush(); } rd = new BufferedReader(new InputStreamReader(URLConn.getInputStream(), "UTF-8")); String line; while ((line = rd.readLine()) != null) { receive.append(line); } } catch (java.io.IOException e) { receive.append("访问产生了异常-->").append(e.getMessage()); e.printStackTrace(); } finally { if (dos != null) { try { dos.close(); } catch (IOException ex) { ex.printStackTrace(); } } if (rd != null) { try { rd.close(); } catch (IOException ex) { ex.printStackTrace(); } } URLConn.disconnect(); } String content = receive.toString(); LOGGER.info("content = "+content); return content; } public static String get(String sendUrl) { StringBuffer receive = new StringBuffer(); HttpURLConnection URLConn = null; BufferedReader in = null; try { System.out.println("sendUrl:" + sendUrl); URL url = new URL(sendUrl); URLConn = (HttpURLConnection) url.openConnection(); URLConn.setDoInput(true); URLConn.connect(); in = new BufferedReader(new InputStreamReader(URLConn.getInputStream(), "UTF-8")); String line; while ((line = in.readLine()) != null) { receive.append(line); } } catch (IOException e) { receive.append("访问产生了异常-->").append(e.getMessage()); e.printStackTrace(); } finally { if (in != null) { try { in.close(); } catch (java.io.IOException ex) { ex.printStackTrace(); } in = null; } URLConn.disconnect(); } return receive.toString(); } public static String post(String sendUrl) { return post(sendUrl, null); } }
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122

    前端页面调用的Controller方法,返回前端需要的参数:

         /**
    	 * 获取config接口注入权限验证配置
    	 * @param url
    	 * @return
    	 * @throws NoSuchAlgorithmException
    	 */
    	@ResponseBody
    	@RequestMapping(value = "/wechat/getWeiXinPermissionsValidationConfig")
    	public ApiResult<Map<String, Object>> getWeiXinPermissionsValidationConfig(String url) throws NoSuchAlgorithmException {
    		ApiResult<Map<String, Object>> apiResult = new ApiResult<Map<String, Object>>();
    		Map<String, Object> resultMap = new HashMap<>(16);
    		// 获取jsapi_ticket
    		String ticket = WeixinHelper.getJsApiTicket(WeixinHelper.CRM_PC_CORPSECRET, "");
    		//当前时间戳转成秒
    		long timestamp = System.currentTimeMillis() / 1000;
    		//随机字符串
    		String nonceStr = "Wm3WZYTPz0wzccnW";
    		// 获取JS-SDK使用权限签名
    		String signature = WeixinHelper.getJSSDKSignature(ticket, nonceStr, timestamp, url);
    		resultMap.put("corpid", WeixinHelper.APP_ID);
    		resultMap.put("agentid", WeixinHelper.CRM_PC_AGENT_ID);
    		resultMap.put("timestamp", timestamp);
    		resultMap.put("nonceStr", nonceStr);
    		resultMap.put("signature", signature);
    		return apiResult.success(resultMap);
    	}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26

    ApiResult类:

    public class ApiResult<T> {
    
    	private int code;
    	private String message;
    	private T data;
    	
    	public ApiResult() {
    		
    	}
    	
    	public ApiResult(int code, String message, T data) {
    		this.code = code;
    		this.message = message;
    		this.data = data;
    	}
    	
    	public ApiResult(ResultEnum resultEnum) {
    		this.code = resultEnum.getCode();
    		this.message = resultEnum.getMessage();
    	}
    
    	public ApiResult(ResultEnum resultEnum, T data) {
    		this.code = resultEnum.getCode();
    		this.message = resultEnum.getMessage();
    		this.data = data;
    	}
    	
    	
    	public ApiResult<T> success(T data) {
    		this.code = ResultEnum.SUCCESS.getCode();
    		this.message = ResultEnum.SUCCESS.getMessage();
    		this.data = data;
    		return this;
    	}
    
    	public ApiResult<T> success(String message, T data) {
    		this.code = ResultEnum.SUCCESS.getCode();
    		this.message = message;
    		this.data = data;
    		return this;
    	}
    	
    	public ApiResult<T> fail(T data) {
    		this.code = ResultEnum.SERVER_ERROR.getCode();
    		this.message = ResultEnum.SERVER_ERROR.getMessage();
    		this.data = data;
    		return this;
    	}
    
    	public ApiResult<T> fail(String message, T data) {
    		this.code = ResultEnum.SERVER_ERROR.getCode();
    		this.message = message;
    		this.data = data;
    		return this;
    	}
    
    	public int getCode() {
    		return code;
    	}
    
    	public void setCode(int code) {
    		this.code = code;
    	}
    
    	public String getMessage() {
    		return message;
    	}
    
    	public void setMessage(String message) {
    		this.message = message;
    	}
    
    	public T getData() {
    		return data;
    	}
    
    	public void setData(T data) {
    		this.data = data;
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80

    企业微信设置

    代码写好了之后,我们需要配置企业微信,在应用管理里新增一个应用,在应用主页中把**/wechat/openDefaultBrowser的调跳转路径填进去,打开这个应用之后就会跳到我们的前端页面。
    在这里插入图片描述

    坑二 网页授权及JS-SDK

    我碰到的第二个坑就是没有设置可信域名,导致报错。

    在设置可信域名弹出框中,需要把两个空都填上。
    在这里插入图片描述
    填第二个可信域名的时候,需要域名校验(如应用页面需使用微信JS-SDK、跳转小程序等, 需完成域名归属验证)。也就是说如果的你的域名是abc.com,那么在浏览器中输入地址:abc.com/****.txt必须要能访问成功。

    刚开始我正愁要把这个txt文件放置项目的哪个位置,才能直接访问呢?

    我放了好几个地方都不行,后来在网上搜了下,原来这个txt文件里面就是一个字符串,我们只要把这个字符串的内容返回就行了,可以直接在Controller写个方法直接返回txt文件里的字符串就行了。

         /**
    	 * 企业微信域名校验
    	 * @return
    	 */
    	@ResponseBody
    	@RequestMapping(value = "/***ITyAe***.txt")
    	public String wxPrivateKey(){
    		return "*******";
    	}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    填好域名之后,显示已验证就成功了。
    在这里插入图片描述

    坑三 配置企业可信IP

    发布了代码之后测试,结果一直返回错误:

    2022-07-20 09:51:08,278-[TS] INFO http-nio-6868-exec-5 com.abc.weixin.WeixinHelper - ticketContent = {"errcode":60020,"errmsg":"not allow to access from your ip, hint: [1658281868365200070724015], 
    from ip: ***.***.***.***, more info at https://open.work.weixin.qq.com/devtool/query?e=60020"}
    
    • 1
    • 2

    原来是ip地址不允许访问,需要在企业可信IP中把服务器的ip地址填上:

    在这里插入图片描述

    最后

    最后,终于能够实现调整到默认浏览器啦!

    注意:我在打开默认浏览器的时候,需要实现系统自动登录,所以调用了企业微信的OAuth2接口:

    “https://open.weixin.qq.com/connect/oauth2/authorize?appid=****&redirect_uri=http%3A%2F%2Fabc.com%3A6868%2Fwechat%2Fpc&response_type=code&scope=snsapi_userinfo&agentid=&state=STATE#wechat_redirect”

    如果不用自动登录,直接打开一般的链接就行了。

    关于企业微信,我以前还写过一篇文章企业微信小程序避坑指南,欢迎补充。。。

    你在企业微信开发过程中还碰到了什么坑?欢迎在留言中补充交流,谢谢。

  • 相关阅读:
    QGridLayout::addWidget 的使用详解
    Zephyr调度算法
    C/C++自动 21 级(含卓越 211)《软件技术基础》期末大作业
    12. 虚拟机与类加载机制
    CTF靶场搭建及Web赛题制作与终端docker环境部署
    【数据结构教程】线性表及其逻辑结构
    神经网络硕士就业前景,图神经网络前景如何
    vue属性data的处理规则
    Spring整合MyBatis导致一级缓存失效问题
    软件测试面试怎样介绍自己的测试项目?会问到什么程度?
  • 原文地址:https://blog.csdn.net/zhanyd/article/details/125886516