# uni-app微信小程序----“起名” ## 第一章 uni-app入门 ### 1-1 uni-app简介 ### 1-2 小程序项目介绍 宝宝在线起名一直是中国人的刚需,与其路边寻找大师,不如求教线上业务 目前只在淘宝搜索:发现营业额极高,那就让我们把大师的价格打下去 ![image-20200305151834887](C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20200305151834887.png) ![image-20200305151850634](C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20200305151850634.png) ### 1-3 uni-app安装及目录介绍 #### 下载hbuilderx 要讲UNI-APP,不得不介绍dcloud旗下一款轻量级ide编辑器hbuilderx 下载地址dcloud.io官网下载 hx mac和win之间分为标准和APP版 标准版需要自己安装插件,app开发版已经把app开发常用的插件预先集成,开箱即可用 #### UNI-APP有两种创建方式 使用 vue-cli 脚手架 创建 uni-app | cli创建UNI-APP目录名称 | 解析 | | ---------------------- | ------------------------------------------------------------ | | dist ->build | 存放通过build编译的各个平台的代码,如mp-weixin | | node_modules | 项目依赖包模块 | | public | 放置的为公共文件,比如index.html文件,为项目的生成模板,我们写的vue的代码,在webpack打包项目的时候,最后都会基于该模板转换为浏览器可读的三大件:html+javascript+css | | src | 存放通过HBuilderX可视化界面创建的的所有目录,为源码目录 | | .gitignore | git上传需要忽略的文件格式 | | babel.config.js | ES6语法编译配置 | | package.json | 项目基本信息 | | package-lock.json | 锁定安装时的包的版本号,并且需要上传到git,以保证其他人在npm install时大家的依赖能保证一致 | | postcss.config.js | postcss-loader 的配置文件名,通过js对 CSS 进行处理 | | README.md | 项目说明 | | **pages** | 业务页面文件存放的目录 | | ------------- | ------------------------------------------------------------ | | **static** | 存放应用引用静态资源(如图片、视频等)的地方,注意:静态资源只能存放于此 | | app.vue | 应用配置,用来配置App全局样式以及监听生命周期 | | main.js | Vue初始化入口文件 | | manifest.json | 配置应用名称、appid、logo、版本等打包信息 | | pages.json | 配置页面路由、导航条、选项卡等页面类信息 | | uni.scss | 全局样式 | ### 1-4 uni-app配置 uni-app配置项目目录和微信小程序的目录基本相同 mainfest.json 主要作用配置应用名称、appid、logo、版本等打包信息 appid是微信小程序身份唯一标识 appid,以微信小程序为例子, 进入公众平台 pages.json 配置页面路由、导航条、tabbar选项卡等页面类信息 当我们在微信小程序 json 中设置 backgroundColor 时,实际在电脑的模拟器中根本看不到效果, 窗体下拉刷新或上拉加载时露出的背景 ## 第二章 整体设计 ### 2.1api接口设计 #### 首先我们了解下 什么是前后端分离? 前后端分离,就是在对前端开发人员和后端开发人员的工作进行解耦,尽量减少他她们之间的交流成本,帮助他她们更能专注于自己擅长的工作。 前端web 框架 vue \react \angular 安卓端 ios端 小程序端 pwa 本项目是一款基于 SpringBoot 的 Api 服务器脚手架。服务端基础通用框架提取,配以详细的说明文档,针对 Restful 风格 API 服务器,降低学习成本,提高开发效率 #### API设计规范 设计接口是一件容易的事,也是件困难的事。设计接口每个人都会,每个人都能设计,也由此产生了各种各样的理念的接口。工作这么多年,我也很有感悟。很多人会说,设计接口多么简单,只要命名好,然后联调通了,上线可以调用就行了。特别是非互联网行业的人,这里没有歧视的意思。因为互联网行业和传统行业太多不一致性决定了这种思想的产生。 **通过不同的method(get\post\put\delete)来进行crud** **格式{ "data": 返回数据, "code": 状态码, "msg": "返回描述"}** ### 2.2 mysql数据库搭建 本机vmware搭建centos7环境 使用docker安装mysql8 ### 2.3 mysql数据库设计 客户端form表单 服务端算法推荐 ## 第二章 前端页面 ### 1.1 简介 前端使用uni-app 后端springboot2.X+mybatis plus 持久化数据库mysql8.0.16 效果展示: ![image](https://github.com/calvin008/weixin-login/blob/master/image/show.gif?raw=true) ### 1.2 微信小程序登录步骤 ![image](https://github.com/calvin008/weixin-login/blob/master/image/login-api.jpg?raw=true) *第一步:小程序通过uni.login()获取code。* *第二步:小程序通过uni.request()发送code到开发者服务器。* *第三步:开发者服务器接收小程序发送的code,并携带appid、appsecret(这两个需要到微信小程序后台查看)、code发送到微信服务器。* *第四步:微信服务器接收开发者服务器发送的appid、appsecret、code进行校验。校验通过后向开发者服务器发送session_key、openid。* *第五步:开发者服务器自己生成一个key(自定义登录状态)与openid、session_key进行关联,并存到数据库中(mysql、redis等)。* *第六步:开发者服务器返回生成key(自定义登录状态)到小程序。* *第七步:小程序存储key(自定义登录状态)到本地。* ### 1.3首页index ``` ``` ### 1.4 登录页面login ``` ``` ## 第二章 uni-app业务逻辑开发 ### 2.1 vuex在uni-app中的使用 ``` import Vue from 'vue' import Vuex from 'vuex' Vue.use(Vuex) const store = new Vuex.Store({ state: { hasLogin: false, userInfo: {}, }, mutations: { login(state, provider) { state.hasLogin = true; state.userInfo = provider; uni.setStorage({//缓存用户登陆状态 key: 'userInfo', data: provider }) console.log(state.userInfo); }, logout(state) { state.hasLogin = false; state.userInfo = {}; uni.removeStorage({ key: 'userInfo' }) } }, actions: { } }) export default store ``` ### 2.2 登录功能实现 小程序已经抛弃getUserInfo,使用open-type绑定即可; 调用 wx.login 获取 code。 使用 wx.getSetting 获取用户的授权情况 - 如果用户已经授权,直接调用 API wx.getUserInfo 获取用户最新的信息; - 用户未授权,在界面中显示一个按钮提示用户登入,当用户点击并授权后就获取到用户的最新信息。 将获取到的用户数据连同wx.login返回的code一同传给后端 ## 第三章 数据库设计与运行 ### 3.1 vmware使用docker安装mysql **docker安装mysql,远程访问** ```text //搜索mysql docker search mysql //选定版本,抓取镜像 docker pull mysql:8.0 //创建同步mysql的文件夹 mkdir -p /data/mysql01 //创建容器 docker run --name mysql01 -p 3307:3306 -v /data/mysql01:/var/lib/mysql -e MYSQL_ROOT_PASSWORD=zan123456 -d mysql:8.0 --restart 标志会检查容器的退出代码,并据此来决定是否要重启容器,默认是不会重启。 --restart的参数说明 always:无论容器的退出代码是什么,Docker都会自动重启该容器。 on-failure:只有当容器的退出代码为非0值的时候才会自动重启。另外,该参数还接受一个可选的重启次数参数,`--restart=on-fialure:5`表示当容器退出代码为非0时,Docker会尝试自动重启该容器,最多5次。 -v 容器内的 /var/lib/mysql 在宿主机上 /data/mysql01 做映射 -e MYSQL_ROOT_PASSWORD 初始密码 -p 将宿主机3306的端口映射到容器3306端口 ``` **error:如果启动失败,查看日志docker logs mysql01提示** chown: cannot read directory '/var/lib/mysql/': Permission denied 容器中没有执行权限 //挂载外部数据卷时,无法启动容器, 报 chown: cannot read directory '/var/lib/mysql/': Permission denied 由$ docker logs [name] 查看得知 该原因为centOs7默认开启selinux安全模块,需要临时关闭该安全模块,或者添加目录到白名单 临时关闭selinux:su -c "setenforce 0" 重新开启selinux:su -c "setenforce 1" 添加selinux规则,将要挂载的目录添加到白名单: 示例:chcon -Rt svirt_sandbox_file_t /data/mysql01(我启动挂载的路径) **error:用navicat连接如果报错** ![img](https://pic2.zhimg.com/80/v2-8bc25b3599b809c5787eebee4d70067d_hd.jpg)报错是因为加密算法变了 我们在docker里面改变加密算法 ```text mysql> grant all PRIVILEGES on *.* to root@'%' WITH GRANT OPTION; Query OK, 0 rows affected (0.01 sec) mysql> ALTER user 'root'@'%' IDENTIFIED BY '123456' PASSWORD EXPIRE NEVER; Query OK, 0 rows affected (0.11 sec) mysql> ALTER user 'root'@'%' IDENTIFIED WITH mysql_native_password BY '123456'; Query OK, 0 rows affected (0.11 sec) mysql> FLUSH PRIVILEGES; Query OK, 0 rows affected (0.01 sec) ``` 算法换成mysql_native_password即可 ### 3.2 user数据表建立 配置application.yml ``` server: port: 8080 spring: datasource: url: jdbc:mysql://192.168.253.133:3307/test?useUnicode=true&useSSL=false&characterEndcoding=utf8&useTimezone=true&serverTimezone=Asia/Shanghai username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver ``` 创建表格 ``` CREATE TABLE `user` ( `open_id` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT 'open_id', `skey` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT 'skey', `create_time` timestamp(0) NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `last_visit_time` timestamp(0) NULL DEFAULT CURRENT_TIMESTAMP COMMENT '最后登录时间', `session_key` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT 'session_key', `avatar_url` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '头像', `nick_name` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '网名', PRIMARY KEY (`open_id`) USING BTREE ) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '微信用户信息' ; ``` ## 第四章 springboot开发后端接口 ### 4.1 entity ``` package top.weimumu.loginapi.entity; import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; import com.fasterxml.jackson.annotation.JsonFormat; import lombok.Data; import org.springframework.format.annotation.DateTimeFormat; import java.util.Date; /** * @author: create by calvin wong * @date:2019/12/10 **/ @Data @TableName("user") public class User { /** * openid */ @TableId(value = "open_id",type = IdType.INPUT) private String openId; /** * 用户头像 */ private String avatarUrl; /** * 用户网名 */ private String nickName; /** * session_key */ private String session_key; /** * skey */ private String skey; /** * 创建时间 */ @TableField("create_time") @JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8") private Date createTime; /** * 最后登录时间 */ @TableField("last_visit_time") @JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8") private Date lastVisitTime; } ``` ### 4.2 mapper ``` package top.weimumu.loginapi.dao; import com.baomidou.mybatisplus.core.mapper.BaseMapper; import top.weimumu.loginapi.entity.User; /** * @author: create by calvin wong * @date:2019/12/4 **/ public interface UserMapper extends BaseMapper { } ``` ### 4.3 common封装工具类 GolbalResult ``` package top.weimumu.loginapi.common; /** * @author: create by calvin wong * @date:2019/12/10 **/ public class GlobalResult { // 响应业务状态 private Integer status; // 响应消息 private String msg; // 响应中的数据 private Object data; private String ok; // 不使用 public static GlobalResult build(Integer status, String msg, Object data) { return new GlobalResult(status, msg, data); } public static GlobalResult ok(Object data) { return new GlobalResult(data); } public static GlobalResult ok() { return new GlobalResult(null); } public static GlobalResult errorMsg(String msg) { return new GlobalResult(500, msg, null); } public static GlobalResult errorMap(Object data) { return new GlobalResult(501, "error", data); } public static GlobalResult errorTokenMsg(String msg) { return new GlobalResult(502, msg, null); } public static GlobalResult errorException(String msg) { return new GlobalResult(555, msg, null); } public GlobalResult() { } public GlobalResult(Integer status, String msg, Object data) { this.status = status; this.msg = msg; this.data = data; } public GlobalResult(Object data) { this.status = 200; this.msg = "OK"; this.data = data; } public Boolean isOK() { return this.status == 200; } public Integer getStatus() { return status; } public void setStatus(Integer status) { this.status = status; } public String getMsg() { return msg; } public void setMsg(String msg) { this.msg = msg; } public Object getData() { return data; } public void setData(Object data) { this.data = data; } public String getOk() { return ok; } public void setOk(String ok) { this.ok = ok; } } ``` HttpClientUtils ``` package top.weimumu.loginapi.common; import org.apache.http.NameValuePair; import org.apache.http.client.entity.UrlEncodedFormEntity; import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.HttpGet; import org.apache.http.client.methods.HttpPost; import org.apache.http.client.utils.URIBuilder; import org.apache.http.entity.ContentType; import org.apache.http.entity.StringEntity; import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.impl.client.HttpClients; import org.apache.http.message.BasicNameValuePair; import org.apache.http.util.EntityUtils; import java.io.IOException; import java.net.URI; import java.util.ArrayList; import java.util.List; import java.util.Map; /** * @author: create by calvin wong * @date:2019/12/10 **/ public class HttpClientUtil { public static String doGet(String url, Map param) { // 创建Httpclient对象 CloseableHttpClient httpclient = HttpClients.createDefault(); String resultString = ""; CloseableHttpResponse response = null; try { // 创建uri URIBuilder builder = new URIBuilder(url); if (param != null) { for (String key : param.keySet()) { builder.addParameter(key, param.get(key)); } } URI uri = builder.build(); // 创建http GET请求 HttpGet httpGet = new HttpGet(uri); // 执行请求 response = httpclient.execute(httpGet); // 判断返回状态是否为200 if (response.getStatusLine().getStatusCode() == 200) { resultString = EntityUtils.toString(response.getEntity(), "UTF-8"); } } catch (Exception e) { e.printStackTrace(); } finally { try { if (response != null) { response.close(); } httpclient.close(); } catch (IOException e) { e.printStackTrace(); } } return resultString; } public static String doGet(String url) { return doGet(url, null); } public static String doPost(String url, Map param) { // 创建Httpclient对象 CloseableHttpClient httpClient = HttpClients.createDefault(); CloseableHttpResponse response = null; String resultString = ""; try { // 创建Http Post请求 HttpPost httpPost = new HttpPost(url); // 创建参数列表 if (param != null) { List paramList = new ArrayList<>(); for (String key : param.keySet()) { paramList.add(new BasicNameValuePair(key, param.get(key))); } // 模拟表单 UrlEncodedFormEntity entity = new UrlEncodedFormEntity(paramList); httpPost.setEntity(entity); } // 执行http请求 response = httpClient.execute(httpPost); resultString = EntityUtils.toString(response.getEntity(), "utf-8"); } catch (Exception e) { e.printStackTrace(); } finally { try { response.close(); } catch (IOException e) { e.printStackTrace(); } } return resultString; } public static String doPost(String url) { return doPost(url, null); } public static String doPostJson(String url, String json) { // 创建Httpclient对象 CloseableHttpClient httpClient = HttpClients.createDefault(); CloseableHttpResponse response = null; String resultString = ""; try { // 创建Http Post请求 HttpPost httpPost = new HttpPost(url); // 创建请求内容 StringEntity entity = new StringEntity(json, ContentType.APPLICATION_JSON); httpPost.setEntity(entity); // 执行http请求 response = httpClient.execute(httpPost); resultString = EntityUtils.toString(response.getEntity(), "utf-8"); } catch (Exception e) { e.printStackTrace(); } finally { try { response.close(); } catch (IOException e) { e.printStackTrace(); } } return resultString; } } ``` WechatUtil ``` package top.weimumu.loginapi.common; import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.JSONObject; import org.apache.shiro.codec.Base64; import org.bouncycastle.jce.provider.BouncyCastleProvider; import javax.crypto.Cipher; import javax.crypto.spec.IvParameterSpec; import javax.crypto.spec.SecretKeySpec; import java.security.AlgorithmParameters; import java.security.Security; import java.util.Arrays; import java.util.HashMap; import java.util.Map; /** * @author: create by calvin wong * @date:2019/12/10 **/ public class WechatUtil { public static JSONObject getSessionKeyOrOpenId(String code) { String requestUrl = "https://api.weixin.qq.com/sns/jscode2session"; Map requestUrlParam = new HashMap<>(); // https://mp.weixin.qq.com/wxopen/devprofile?action=get_profile&token=164113089&lang=zh_CN //小程序appId requestUrlParam.put("appid", "wx62e0151eaf62eff7"); //小程序secret requestUrlParam.put("secret", "c45170b3aa8dbc5cd45ab12c319298f8"); //小程序端返回的code requestUrlParam.put("js_code", code); //默认参数 requestUrlParam.put("grant_type", "authorization_code"); //发送post请求读取调用微信接口获取openid用户唯一标识 JSONObject jsonObject = JSON.parseObject(HttpClientUtil.doPost(requestUrl, requestUrlParam)); return jsonObject; } public static JSONObject getUserInfo(String encryptedData, String sessionKey, String iv) { // 被加密的数据 byte[] dataByte = Base64.decode(encryptedData); // 加密秘钥 byte[] keyByte = Base64.decode(sessionKey); // 偏移量 byte[] ivByte = Base64.decode(iv); try { // 如果密钥不足16位,那么就补足. 这个if 中的内容很重要 int base = 16; if (keyByte.length % base != 0) { int groups = keyByte.length / base + (keyByte.length % base != 0 ? 1 : 0); byte[] temp = new byte[groups * base]; Arrays.fill(temp, (byte) 0); System.arraycopy(keyByte, 0, temp, 0, keyByte.length); keyByte = temp; } // 初始化 Security.addProvider(new BouncyCastleProvider()); Cipher cipher = Cipher.getInstance("AES/CBC/PKCS7Padding", "BC"); SecretKeySpec spec = new SecretKeySpec(keyByte, "AES"); AlgorithmParameters parameters = AlgorithmParameters.getInstance("AES"); parameters.init(new IvParameterSpec(ivByte)); cipher.init(Cipher.DECRYPT_MODE, spec, parameters);// 初始化 byte[] resultByte = cipher.doFinal(dataByte); if (null != resultByte && resultByte.length > 0) { String result = new String(resultByte, "UTF-8"); return JSON.parseObject(result); } } catch (Exception e) { } return null; } } ``` ### 4.3 controller ``` package top.weimumu.loginapi.controller; import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.JSONObject; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import top.weimumu.loginapi.common.GlobalResult; import top.weimumu.loginapi.common.WechatUtil; import top.weimumu.loginapi.dao.UserMapper; import top.weimumu.loginapi.entity.User; import java.util.Date; import java.util.UUID; /** * @author: create by calvin wong * @date:2019/12/4 **/ @RestController public class UserController { @Autowired private UserMapper userMapper; @PostMapping("/wxlogin") public GlobalResult wxLogin( @RequestParam(value = "code", required = false) String code, @RequestParam(value = "rawData", required = false) String rawData ) { JSONObject rawDataJson = JSON.parseObject(rawData); JSONObject SessionKeyOpenId = WechatUtil.getSessionKeyOrOpenId(code); String openid = SessionKeyOpenId.getString("openid"); String sessionKey = SessionKeyOpenId.getString("session_key"); User user = this.userMapper.selectById(openid); String skey = UUID.randomUUID().toString(); if (user == null) { String nickName = rawDataJson.getString("nickName"); String avatarUrl = rawDataJson.getString("avatarUrl"); user = new User(); user.setOpenId(openid); user.setSkey(skey); user.setCreateTime(new Date()); user.setLastVisitTime(new Date()); user.setSession_key(sessionKey); user.setAvatarUrl(avatarUrl); user.setNickName(nickName); this.userMapper.insert(user); }else { // 已存在,更新用户登录时间 user.setLastVisitTime(new Date()); // // 重新设置会话skey user.setSkey(skey); this.userMapper.updateById(user); } GlobalResult result = GlobalResult.build(200, null, skey); return result; } } ```