微信公众号扫码授权登录思路
引言
上学期研究了一下微信登录相关内容,也写了两三篇笔记,但是最后实际登录流程没有写,主要因为感觉功能完成有所欠缺,一直也没有好的思路;这两天我又看了看官方文档,重新构思了一下微信公众号登录相关的内容,可能还是有不足之处,但是基本架子已经搭起来了,在这里我就简单说明一下我的想法;
关于登录方式
登录方式在我看来是必须要设计好的一个关键点,我设计的系统涉及到了手机号验证码登录,每个用户绑定的手机号是唯一的;所以对于初次使用微信登录的用户,必须绑定一个手机号才可以;(如果你只是单纯的微信登录,就会少很多步骤,下面我会细说)
微信中有一个叫openId的参数,这个参数可以说对每个关注公众号的用户都是不一样的,也就是每个用户的唯一性标识;
那么我们可以分析出来,一个用户除了id主键唯一以外,他的手机号是唯一的,微信中对应的openId也是唯一的;
所以数据库中就需要有一个用来存放openId的字段,后期微信登录就要通过该字段来检索用户;
准备工作
想要实现微信公众号扫码登录,首先要知道以下三点的实现:
1,获取微信公众号二维码
2,微信网页授权的实现
3,微信公众号消息接收和回复的实现
这三点我之前已经写过对应文章了,可以结合微信官方文档学习;
- 微信公众号扫码登录(一)—— 获取微信公众号二维码
- 微信登录——授权登录获取用户信息
- 微信公众号被动消息回复实现
下面代码可能会和上面文章有所重复,但也有修改之处,看不懂可以对比来看;
登录流程
我的思路如下:
可以分为以下步骤:
1,判断fromUserName即用户openId在数据库中是否存在
2, 如果存在则通过该openId查询到该用户信息,生成token进行登录操作
3, 如果不存在用户信息则跳转到手机号绑定页面
4, 如果该手机号已经注册有用户,则绑定该openId
5, 如果没有注册,则获取用户微信信息,将openId和手机号绑定
大致思路就是这样,其中当然还有很多小细节,下面用代码来大致演示一下;
代码实现
这个接口其实是实现微信消息接收和推送的接口,上面文章中也有,这里只是提取了service层
/*** 接收微信公众号消息(微信登录也经由该接口)*/
@PostMapping("/callback")
@ResponseBody
public String responseMsg(HttpServletRequest req, HttpServletResponse resp) throws IOException {req.setCharacterEncoding("UTF-8");String respContent = wxService.responseMsg(req);return respContent;
}
service
// 这个方法就是微信的登录方法
@Override
public String responseMsg(HttpServletRequest req) {if (req == null) {throw new BusinessException(StatusCode.SYSTEM_ERROR);}String message = "success";try {// 把微信返回的xml信息转义成mapMap<String, String> xmlMessage = WxMessageUtil.xmlToMap(req); // 解析微信发来的请求信息String fromUserName = xmlMessage.get("FromUserName"); // 这个就该事件的用户openIdString toUserName = xmlMessage.get("ToUserName"); // 这个开发者微信号String msgType = xmlMessage.get("MsgType"); // 消息类型(event或者text)String createTime = xmlMessage.get("CreateTime"); // 消息创建时间 (整型)log.info("发送方帐号(用户的openId)=>" + fromUserName);log.info("开发者微信号=>" + toUserName);log.info("消息类型为=>" + msgType);log.info("消息创建时间 (整型)=>" + createTime);if ("event".equals(msgType)) { // 如果是事件推送String eventType = xmlMessage.get("Event"); // 事件类型String eventKey = xmlMessage.get("EventKey"); // 获取事件KEY值if ("subscribe".equals(eventType)) { // 如果是扫描二维码后订阅消息String subscribeContent = "感谢关注";// 如果是扫码登录二维码后订阅公众号,则获取该用户信息进行登录操作if (!StringUtils.isAnyBlank(eventKey)&& WxConstant.LOGIN_QR_ID.toString().equals(eventKey.split("_")[1])) {subscribeContent = dealWithWxLoginUser(fromUserName, subscribeContent);}String subscribeReturnXml = WxMessageUtil.getWxReturnMsg(xmlMessage, subscribeContent);return subscribeReturnXml;}if ("SCAN".equals(eventType)) { // 如果是扫码消息String scanContent = "扫码成功";// 如果是扫描登录二维码,则获取该用户信息进行登录操作if (!StringUtils.isAnyBlank(eventKey)&& WxConstant.LOGIN_QR_ID.toString().equals(eventKey)) {scanContent = dealWithWxLoginUser(fromUserName, scanContent);}String scanReturnXml = WxMessageUtil.getWxReturnMsg(xmlMessage, scanContent);return scanReturnXml;}}if ("text".equals(msgType)) { // 如果是文本消息推送String content = xmlMessage.get("Content"); // 接收到的消息内容String textReturnXml = WxMessageUtil.getWxReturnMsg(xmlMessage, content);return textReturnXml; // 将接收到的文本消息变成xml格式再返回}} catch (IOException | DocumentException e) {throw new RuntimeException(e);}return message;
}/*** 处理微信登录的用户* @param openId 扫码登录用户的openId* @param content 处理结果* @return 处理信息*/
private String dealWithWxLoginUser(String openId, String content) {if (StringUtils.isAnyBlank(openId, content)) {throw new BusinessException(StatusCode.PARAMS_ERROR, "dealWithWxLoginUser方法参数为空");}// 1,判断fromUserName即用户openId在数据库中是否存在User loginUser = userService.getOne(new LambdaQueryWrapper<User>().eq(User::getWechatNum, openId));if (loginUser == null) {// 如果不存在用户信息则跳转到手机号绑定页面// (此时微信登录就和这里的方法没有关系了,登录工作由下面跳转的绑定页面完成,这里链接目的之一是引导用户授权信息)// 能访问到该绑定页面只有两种情况,不符合这两种情况不能访问该页面:// 1,该openId对应用户未绑定手机号 2,该openId对应用户为空content = "[POLAR]该账号未绑定手机号,请绑定手机号后登录\n" +"<a href =\"http://内网穿透域名/api/user/wx/redirect\">[绑定手机号]</a>";} else {// 如果存在则通过该openId查询到该用户信息,生成token(存入redis)进行登录操作// 封装用户信息UserVo userVo = userUtil.setUserVo(loginUser);// 生成tokenString token = JwtUtil.createJWT(loginUser.getId().toString());// 将用户信息存入redisredisTemplate.opsForValue().set(RedisKey.LOGIN_USER + loginUser.getId(), userVo, 14, TimeUnit.DAYS);content = "用户" + userVo.getNickname() + "登录成功\n\n" +"登录日期:" + new Date();}return content;
}
这里实现的就是流程图中的第一步判断openId是否存在,主要判断方法就是dealWithWxLoginUser这个方法,该方法触发的条件就是用户扫码订阅公众号事件或者扫码登录事件(SCAN和subscribe)。
可以看到dealWithWxLoginUser方法中当用户不存在时,则需要引导用户进入手机号绑定界面,这里就是通过公众号向用户发送了手机号绑定超链接;实际效果如图:
然后用户点击下面超链接进行手机号绑定;
如果已经绑定openId,则执行登录操作,效果如图:
接下来就是绑定手机号步骤,绑定手机号链接到的是微信的网页授权接口,这一块在微信授权文章里说过,通过内网穿透映射到对应链接;
代码如下:
// 调用微信授权接口重定向
@GetMapping("/redirect")
@ResponseBody
public String toRedirectUrl(HttpServletResponse response) {String redirectUrl = "https://open.weixin.qq.com/connect/oauth2/authorize" +"?appid=" + WxConfigurationConstant.APP_ID +"&redirect_uri=" + WxConfigurationConstant.REDIRECT_URL +"&response_type=code" + "&scope=snsapi_userinfo" + // 只有关注公众号才能获取用户全部信息"&state=STATE" + "&connect_redirect=1#wechat_redirect";try {response.sendRedirect(redirectUrl); // 重定向url} catch (IOException e) {log.error("获取微信code失败: " + e.getMessage());}return "重定向成功";
}// 授权接口重定向回调方法
@GetMapping("/redirect/info")
public String redirectInfo(@RequestParam(value = "code") String code,@RequestParam(value = "state", required = false) String state,HttpServletResponse response,Model model) {// 获取登录用户信息WxUserInfo loginUserInfo = wxService.getWxLoginUserInfo(code);// 判断该用户是否已经绑定手机号(只有用户绑定手机号后恶意访问绑定链接才会触发),已绑定则跳转错误界面User user = userService.getOne(new LambdaQueryWrapper<User>().eq(User::getWechatNum, loginUserInfo.getOpenid()));if (user != null) {return "BindPhoneErrorPage";}model.addAttribute("loginUserInfo", loginUserInfo);// 不响应json数据,返回一个手机号绑定界面return "BindPhonePage";
}
第一个/redirect接口就是上面超链接的url,通过该接口进行用户信息授权获取的:
然后该接口重定向到下面的/redirect/info接口,这个接口的作用才是实际获取用户信息的接口,通过getWxLoginUserInfo方法获取扫码用户的微信信息;这里可能会有疑惑,既然/redirect/info接口是获取用户信息的接口,那么要/redirect接口干嘛,这里上面微信授权文章也讲过,目的就是一个:获取code参数,只有重定向方式才能活到到code,有了code参数才能获取用户信息;微信官方文档有写;
/redirect/info接口返回的是一个html页面,即手机号绑定页面,并把授权获取的用户信息loginUserInfo也传到了该页面,这里用的是springboot的thymleaf模板,因为我前端实在是烂,就简单写了个条条框框实现这个功能,后期再完善页面:
BindPhonePage:
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org"><head><meta charset="UTF-8"><title>绑定手机号</title></head><body><div><br/>
<!-- <span id="hideWxUserInfo" th:text="${loginUserInfo}" hidden></span>--><input id="phoneNum" placeholder="请输入手机号" /> <br/><br/><input id="code" placeholder="请输入验证码" /><button id="sendCode">发送验证码</button><br/><br/><button id="bindClick">绑定</button></div></body><script src="https://apps.bdimg.com/libs/jquery/2.1.4/jquery.min.js"></script><script type="text/javascript" th:inline="javascript">$("#sendCode").click(() => {// 发送ajax请求获取验证码var phone = $("#phoneNum").val();$.ajax({url:'http://内网穿透域名/api/user/sms/aliyun/code/' + phone,// data:{// },type:'post',// dataType:'json',success:function (data) {if (data.code == 20000) {alert('验证码发送成功')} else {// 提示信息alert(data.message);}}});})$("#bindClick").click(() => {// 发送手机号绑定请求var wxUserInfo = [[${loginUserInfo}]];var phone = $("#phoneNum").val();var code = $("#code").val();var allInfo = {"phone": phone,"code": code,"wxUserInfo": wxUserInfo}$.ajax({url:'http://内网穿透域名/api/user/bind/phone',data: JSON.stringify(allInfo),contentType: "application/json;charset=UTF-8",type:'post',dataType:'json',success:function (data) {if (data.code == 20000) {alert('绑定成功');// TODO 跳转失败不知道为什么window.location.href = 'BindPhoneErrorPage.html';} else {// 提示信息alert(data.description);}}});})</script>
</html>
可以看到绑定按钮点击后会发送一个绑定请求:http://内网穿透域名/api/user/bind/phone,这个接口主要完成流程图的注册或添加openId功能,代码如下:
@PostMapping("/bind/phone")
public BaseResponse<String> bindPhoneAndOpenId(@RequestBody BindPhoneAndOpenIdRequest bindRequest) {if (bindRequest == null|| StringUtils.isAnyBlank(bindRequest.getPhone(), bindRequest.getCode())|| bindRequest.getWxUserInfo() == null) {throw new BusinessException(StatusCode.PARAMS_ERROR);}String phone = bindRequest.getPhone();String code = bindRequest.getCode();WxUserInfo wxUserInfo = bindRequest.getWxUserInfo();userService.bindPhoneAndOpenId(phone, code, wxUserInfo);return ResultUtils.success("绑定成功");
}
service
@Transactional
@Override
public void bindPhoneAndOpenId(String phone, String code, WxUserInfo wxUserInfo) {// 参数校验if (StringUtils.isAnyBlank(phone, code) || wxUserInfo == null) {throw new BusinessException(StatusCode.PARAMS_ERROR, "参数为空");}RegExpUtil.regExpVerify(RegExpUtil.phoneRegExp, phone, "手机号格式错误");// 从redis中获取验证码进行校验String phoneCode = (String) redisTemplate.opsForValue().get(RedisKey.SMS_LOGIN_CODE + phone);if (StringUtils.isAnyBlank(phoneCode)) {throw new BusinessException(StatusCode.OPERATION_ERROR, "验证码不存在或已超时");}phoneCode = phoneCode.split("_")[0]; // 获取真正的验证码if (!code.equals(phoneCode)) {throw new BusinessException(StatusCode.OPERATION_ERROR, "验证码错误");}User user = userMapper.selectOne(new LambdaQueryWrapper<User>().eq(User::getPhone, phone));if (user != null) { // 如果该手机号已经注册有用户,则绑定该openIduser.setWechatNum(wxUserInfo.getOpenid());user.setUpdateTime(null);userMapper.updateById(user);// 再次查询处理好的user数据user = userMapper.selectOne(new LambdaQueryWrapper<User>().eq(User::getPhone, phone));} else { // 如果没有注册,则获取用户微信信息,将openId和手机号绑定(用户注册流程)user = new User();user.setPhone(phone);user.setNickname(wxUserInfo.getNickname());user.setAvatar(wxUserInfo.getHeadimgurl());user.setGender(wxUserInfo.getSex());user.setWechatNum(wxUserInfo.getOpenid());user.setProfile("简单介绍一下自己吧!");userMapper.insert(user); // 新增用户// 重新获取新增用户信息user = userMapper.selectOne(new LambdaQueryWrapper<User>().eq(User::getPhone, phone));// 设置用户为普通用户Role normalRole = roleService.getOne(new LambdaQueryWrapper<Role>().eq(Role::getRoleName, "普通用户"));UserRoleRelation userRoleRelation = new UserRoleRelation();userRoleRelation.setUserId(user.getId());userRoleRelation.setRoleId(normalRole.getId());userRoleRelationService.save(userRoleRelation);}// 封装用户信息UserVo userVo = userUtil.setUserVo(user);// 生成tokenString token = JwtUtil.createJWT(user.getId().toString());// 将用户信息存入redisredisTemplate.opsForValue().set(RedisKey.LOGIN_USER + user.getId(), userVo, 14, TimeUnit.DAYS);// 登录成功后将验证码清除redisTemplate.delete(RedisKey.SMS_LOGIN_CODE + phone);
}
这里就是对应流程图的如下步骤:
前面一大堆就是参数校验和验证码校验,后面if~else才是核心;
其中跳转BindPhoneErrorPage.html失败,没有找到原因,后期完善了补充;
BindPhoneErrorPage页面
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head><meta charset="UTF-8"><title>ERROR</title>
</head><body><h1>绑定手机号成功</h1></body>
</html>
效果如下:
用户点击绑定手机号会有如下页面进行操作:
获取验证码后点击绑定即可:
绑定成功后查看数据库可以看到完整用户数据,即phone和openId字段都有(我这里openId字段是wechat_num):
第一条openId为空的数据就是没有使用过微信登录只通过手机号验证码登录的用户;第二条就是绑定好的数据;
查看redis:
至此微信登录大致就完成了;
总结
其实我这里实现的仅仅是后端部分,前端如何判断用户是否扫码,如何获取用户登录的token都是待解决的问题,这里我可以提供两个思路,前端可以通过和后端长连接websocket通信进行判断用户是否扫码从而进一行下一步操作;或者是很多网站都是用的前端通过轮询的方式定时向后端发送请求查看后端是否登录完成;
以上思路和代码仅仅是我个人想法,稳定性和效率上没有经过测试,希望能給你提供一个思路,如果有问题请指点一二;
相关文章:
微信公众号扫码授权登录思路
引言 上学期研究了一下微信登录相关内容,也写了两三篇笔记,但是最后实际登录流程没有写,主要因为感觉功能完成有所欠缺,一直也没有好的思路;这两天我又看了看官方文档,重新构思了一下微信公众号登录相关的…...
数据结构与算法基础-学习-10-线性表之顺序栈的清理、销毁、压栈、弹栈
一、函数实现顺序栈的其他函数实现,请看之前的博客链接《数据结构与算法基础-学习-09-线性表之栈的理解、初始化顺序栈、判断顺序栈空、获取顺序栈长度的实现》。1、ClearSqStack(1)用途清理栈的空间。只需要栈顶指针和栈底指针相等ÿ…...
Hazel游戏引擎(005)
本人菜鸟,文中若有代码、术语等错误,欢迎指正 我写的项目地址:https://github.com/liujianjie/GameEngineLightWeight(中文的注释适合中国人的你) 文章目录前言关键操作代码文件关键代码代码流程代码文件关键代码exter…...
牛客网Python篇数据分析习题(四)
1.现有一个Nowcoder.csv文件,它记录了牛客网的部分用户数据,包含如下字段(字段与字段之间以逗号间隔): Nowcoder_ID:用户ID Level:等级 Achievement_value:成就值 Num_of_exercise&a…...
盲盒如何创业?
所谓的“盲盒”,受众群体大部分是那些爱碰运气的人,顾客买的是那种在打开盲盒时一刹那的惊喜感和神秘感,在打开盲盒之前,谁也不知道自己会得到什么,这也是为什么消费者更愿意购买的原因。网上的盲盒,主要是…...
第1集丨Java中面向对象相关概念汇总
目录一、基本概念1.1 类1.2 属性1.3 方法1.4 静态1.5 包1.6 import二、高级概念2.1 构造方法2.2 继承2.3 super & this2.4 多态2.5 方法重载2.6 方法重写2.7 访问权限2.8 内部类2.9 final2.10 抽象2.11 接口2.12 匿名类面向对象的编程思想力图使计算机语言中对事物的描述与…...
高性能(二)
三、读写分离和分库分表 1.读写分离 1.1 概述 将数据库的读写操作分散到不同的数据库节点上 通常一主多从一台主数据库负责写,多台从数据库负责读。 主库和从库之间会进行数据同步,以保证从库中数据的准确性。 1.2 问题及解决 1.2.1 问题 主从同…...
Allegro如何实现同一个屏幕界面分屏显示操作指导
Allegro如何实现同一个屏幕界面分屏显示操作指导 在做PCB设计的时候,会需要分屏显示,比如一边是放大的视图,另外一边是缩小的视图,Allegro支持同一个屏幕界面下进行分屏显示,如下图 而且会实时同步起来 如何分屏,具体操作如下 点击View...
前后端一些下载与配置(第二篇 第10天过后)nuxt banner redis 短信服务
NUXT 应该是不用怎么装? 有现成的 axios 还需要在npm吗 好像已经有现成的了 banner banner 笔记汇总P396 Redis Linux安装redis tar -xzvf redis-6.2.6.tar.gz cd redis-6.2.6 照着他做 然后 cd /usr/local/redis/bin ./redis-server /usr/local/redis…...
OSG三维渲染引擎编程学习之四十八:“第五章:OSG场景渲染” 之 “5.6 多重纹理映射”
目录 第五章 OSG场景渲染 5.6 多重纹理映射 5.6.1 多重纹理映射介绍 5.6.2 多重纹理映射示例...
对Node.js 的理解?优缺点?应用场景?
一、是什么 Node.js 是一个开源与跨平台的 JavaScript 运行时环境 在浏览器外运行 V8 JavaScript 引擎(Google Chrome 的内核),利用事件驱动、非阻塞和异步输入输出模型等技术提高性能 可以理解为 Node.js 就是一个服务器端的、非阻塞式I/…...
Bean的生命周期
所谓的生命周期指的是一个对象从诞生到销毁的整个生命过程,我们把这个过程就叫做一个对象的生命周期~~ Bean的生命周期分为以下五大部分: 实例化(为 Bean 分配内存空间) 设置属性(Bean对象注入/装配) 初…...
Python学习-----函数2.0(函数对象,名称空间,作用域-->全局变量与局部变量)
目录 前言: 1.函数对象 (1)函数对象的引用 (2)函数可以放到序列里面 (3)函数可以作为参数 , 传递给另一个函数 2.名称空间 3.作用域 (1)作用域的理解 …...
Java中Json字符串和Java对象的互转
JSON(JavaScript Object Notation)是一种轻量级的数据交换格式。诞生于 2002 年。易于人阅读和编写。同时也易于机器解析和生成。JSON 是目前主流的前后端数据传输方式。 JSON 采用完全独立于语言的文本格式,但是也使用了类似于 C 语言家族的…...
代码随想录NO42 | 动态规划_Leetcode70. 爬楼梯 (进阶) 322. 零钱兑换 279.完全平方数
动态规划_Leetcode70. 爬楼梯 (进阶) 322. 零钱兑换 279.完全平方数70. 爬楼梯 (进阶) 在原题基础上,改为:一步一个台阶,两个台阶,三个台阶,…,直到 m个台阶…...
【C++从入门到放弃】初识C++(基础知识入门详解)
🧑💻作者: 情话0.0 📝专栏:《C从入门到放弃》 👦个人简介:一名双非编程菜鸟,在这里分享自己的编程学习笔记,欢迎大家的指正与点赞,谢谢! C基础…...
企业工程项目管理系统源码+spring cloud 系统管理+java 系统设置+二次开发
工程项目各模块及其功能点清单 一、系统管理 1、数据字典:实现对数据字典标签的增删改查操作 2、编码管理:实现对系统编码的增删改查操作 3、用户管理:管理和查看用户角色 4、菜单管理:实现对系统菜单的增删改查操…...
【GPLT 三阶题目集】L3-016 二叉搜索树的结构
二叉搜索树或者是一棵空树,或者是具有下列性质的二叉树: 若它的左子树不空,则左子树上所有结点的值均小于它的根结点的值;若它的右子树不空,则右子树上所有结点的值均大于它的根结点的值;它的左、右子树也分…...
核心交换机安全多业务高性能万兆交换机
RG-S5750-24SFP/12GT交换机是锐捷网络推出的融合了高性能、高安全、多业务的新一代三层交换机。RG-S5750-24SFP/12GT 交换机能够提供灵活的介质接口,满足网络建设中不同介质的连接需要。全千兆的端口形态,加上可扩展的高密度万兆端口,提供1&a…...
Android APK 签名打包原理分析(三)【静默安装的实现方案】
背景 小编目前从事的系统定制类工作,有客户提出了,需要后台“静默安装”他们的app,也就是悄无声息的安装,而且特别强调,不可以跳出任何安装引导页面,他们的app下载完成之后,后台调用公开的android install代码,系统就后台完成安装,安装完成之后,重新打开应用就可以。…...
mulesoft MCIA 破釜沉舟备考 2023.02.14.05
mulesoft MCIA 破釜沉舟备考 2023.02.14.05 1. Refer to the exhibit.2. A Kubernetes controller automatically adds another pod replica to the resource pool in response to increased application load.3. An XA transaction Is being configured that involves a JMS c…...
结构体的三种定义方法、结构体类型名(可选标志符)什么时候可以省略
结构体的三种定义方法 一、单独定义: 先定义结构体类型,再定义变量 定义结构体的格式如下: struct 结构体名 { 若干数据项; } ; 其中,struct为关键字; 结构体名是用户定…...
cgo静态编译不能用glibc,用musl
Golang 的一个动态链接依赖问题 upx 是一个压缩二进制的工具,如上图,经过压缩之后,这些 binary 的体积都减少了 46%。 静态链接 CGO 的依赖 如果使用 glibc 的是,是不能静态链接的: rootf88271a666f9:/workspace# g…...
力扣解法汇总1124. 表现良好的最长时间段
目录链接: 力扣编程题-解法汇总_分享记录-CSDN博客 GitHub同步刷题项目: https://github.com/September26/java-algorithms 原题链接:力扣 描述: 给你一份工作时间表 hours,上面记录着某一位员工每天的工作小时数。…...
12- 降维算法 (PCA降维/LDA分类/NMF) (数据处理)
数据降维就是一种对高维度特征数据预处理方法。降维是将高维度的数据保留下最重要的一些特征,去除噪声和不重要的特征,从而实现提升数据处理速度的目的。PCA算法有两种实现方法: 基于特征值分解协方差矩阵实现PCA算法基于SVD分解协方差矩阵实…...
QT+ OpenGL学习
文章目录QT OpenGLQOpenGLWidget:不需要GLFWQOpenGLFunction_X_X_Core:不需要GLAD你好,三角形顶点输入顶点着色器片段着色器链接着色器本节代码元素缓冲对象EBOQT交互GLSLGLSL支持的类型输入输出Uniform纹理纹理单元纹理环绕纹理过滤多级渐远纹理QT OpenGL 本篇完整…...
C语言(字符串输入)
目录 一.gets和puts组合 二.fgets()和fputs() 三.fgets()函数返回 四.fgets读取满问题 五.修改fgets函数,自动用\0替换\n 一.gets和puts组合 Gets()读取整行输入,知道遇到换行符,然后丢弃换行符,存储其余字符,并在这些字符的…...
背包问题求方案数(AcWing)(JAVA)
有 N件物品和一个容量是 V 的背包。每件物品只能使用一次。 第 i 件物品的体积是 vi,价值是 wi。 求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。 输出 最优选法的方案数。注意答案可能很大,请输出答…...
一篇文章带你读懂HashMap
HashMap是面试中经常问到的一个知识点,也是判断一个候选人基础是否扎实的标准之一。可见HashMap的掌握是多重要。 一、HashMap源码分析 1、构造函数 让我们先从构造函数说起,HashMap有四个构造方法,别慌 1.1 HashMap() // 1.无参构造方法、// 构造一…...
Java如何进行优雅的判空——Optional类的灵活应用
0 引言 在Java Web项目开发中,经常令人头疼的NPE问题(NullPointerException)——空指针,例如我们在调用equal()方法时,就经常会出现NPE问题: String str null; str.equals("fsfs");…...
购物网站php源代码/北京网站推广助理
1,实现效果 实现下来刷新 2,实现效果 【1】添加依赖,导入V7包SwipeRefreshLayout是V4包下的,V7保护我 compile com.android.support:appcompat-v7:23.4.0 添加网络权限 【2】把需要展示的内容用SwipeRefreshLayout包裹 <?…...
美食网站建设规划书/百度一下官方网页
Oracle Grant详解GRANT 名称 GRANT — 赋予一个用户,一个组或所有用户访问权限 GRANT privilege [, ...] ON object [, ...] TO { PUBLIC | GROUP group | username } 输入 privilege 可能的权限有: SELECT 访问声明的表/视图的所有列/字段࿰…...
企业创新平台建设/seo排名点击报价
IntelliJ换行CRLF, LF, CR的解释和默认设置 在window下开发有一个大坑,就是换行默认是CRLF,也就是回车换行,但是Linux下只有换行LF,这样代码提交后,会出现编译问题,所以最好的办法是在IntelliJ下设置默认为…...
兰州网站建设哪家专业/专业seo推广
数组指针字符串C语言程序的设计-第4节,3,4,5; aarray3; array36; void main() int array5,i ; /使用循环语句对数组的5个元素分别赋值 for (i0; i5; i) arrayii*i; /分别输出数组元素的值 for (i0; i5; i) cout下标为i的元素的值是: arrayiendl; C语言系统对数组的下…...
织梦网站程序下载/seo流量排行榜神器
(产品代码)Product Code:ljkfuhjpccxt8xq2re37n97595ldmv9kch (序列号)Serial Number:302967 (口令)Password:xs374ca License Number:999...
云商网站建设/seo还有哪些方面的优化
Win10系统想要卸载某些软件,通过“控制面板”---“程序和功能”,卸载可能会报错,大概率是因为权限不够。 于是基于cmd进行卸载,步骤如下: 1. 在“开始”处键入cmd; 2. 以管理员身份运行“cmd.exe” 3. 键入“Wmic”…...