前端技術堆疊:Vue-Cli
前端軟體:WebStorm 2020.3
#前端樣式: Bootstrap
後端技術堆疊:SpringBoot
後端軟體:IntelliJ IEDA2019
JavaJDK:1.8
伺服器:阿里雲Centos 7
其他:MyBatis,Redis,MySql,Docker, Shiro
專案原始碼:shoppingProject01_pub : version6.0
#項目參考:Project05;不良人_Vue-Cli;不良人_Redis;不良人_Axios;尚矽谷_Redis
1)信箱註冊登入:
用戶應用郵箱註冊點擊提交後,網站會給用戶發送郵件附帶激活碼鏈接,用戶點擊鏈接實現帳號激活傳送門。
2)簡訊註冊登入:
用戶應用手機號碼註冊時,點擊「取得驗證碼」按鈕,手機會接收到網站發送的簡訊附帶驗證碼。基於Redis實現了驗證碼有效期5分鐘,每個手機號只能取得三次簡訊驗證碼傳送門。
3)alipay付款:
透過下載Android版支付寶沙箱app使用者可透過alipay掃碼購買網站上的商品,後台MySql會記錄此訂單行為傳送門,網頁展示如圖1所示。
當用戶掃碼購買年度VIP會員後,購買網站上的商品一律半價,後台MySql記錄使用者角色的變更。
5)用戶積分排行榜:
用戶購買商品會增加自己的積分,網頁展示如圖2所示。
1)郵件發送功能本地測試通過,伺服器端測試Bug頻出,解決方法。
2)專案在伺服器部署後,無法連接伺服器上的Redis。解決方法:(1)將Redis在伺服器部署而非Docker;(2)將Redis連接埠改為7000;(3)防火牆active狀態下放行伺服器和阿里雲的7000埠;(4)修改Redis.conf檔。
3)git上傳本地原始碼到gitee,誤操作導致本地原始碼被gitee上的舊程式碼覆蓋,第二天才發現。解決方法:因為在伺服器上留有原始碼的jar包,透過反編譯工具jd_gui救回半條命。另外git上傳檔案參考傳送門。
Vue-Cli模組說明:
1.1 Vue-Cli的概述:1)以前後端分離、單頁web應用(SPA)為特點,Vue-Cli可以建立一個Vue項目,而這個項目存在腳手架規格。 Vue-Cli的優點如下:(1) 基於鷹架規範的開發會變得非常靈活。
(2) Vue-Cli基於webpack建構並帶有合理的預設配置,打包工具webpack能夠聚合單頁和各種開發元件。
(3) Vue-Cli是一個豐富的官方插件集合,繼承了前端生態中最好的工具。
(1) 安裝WebStorm(用於開發),安裝node.js,安裝vue-cli,安裝axios(用於發起跨域請求),引入bootstrap樣式。
npm run build # 在WebStorm终端执行,生成dist文件夹 docker pull nginx:1.19.10 # 不建议Vue-cli项目部署到tomcat,因为tomcat属于动态服务器,启动需要java环境,是为了解析动态语言jsp的;像纯静态的就部署到静态服务器nginx上。 mkdir html # 为了做docker容器内外的数据卷映射 mv dist/ html/ docker run -p 80:80 --name nginx01 -d -v /root/html/dist/:/usr/share/nginx/html nginx:1.19.10 # 数据卷映射 # 此时可访问 http://120.79.133.235:80/index.html4)Vue-Cli開發要點:
(1)在WebStorm中,開發過程主要面向src文件,如圖3所示:
[1] 首先掌握路由(router)與元件(components[公有元件],views[私有元件]),元件就是一個個“頁面”,元件建立後要向路由進行註冊;[2] asserts封裝了bootstrap樣式,並在main.js中被導入;[3] 為了發送跨域請求,在utils中封裝了axios實例,程式碼如下:
import axios from 'axios' // 创建默认实例 const instance = axios.create({ baseURL: 'http://120.79.133.235:8989/eb', // timeout: 10000, }); // 请求拦截器 instance.interceptors.request.use(config=>{ console.log("请求拦截器"); return config; }) // 响应拦截器 instance.interceptors.response.use(response=>{ console.log("响应拦截器"); return response; }, err=>{ console.log("响应出现错误时进入的拦截器"); }); // 暴露instance实例对象 export default instance;在各元件中,對於後端的get和post請求方法如下:
// Get请求 // 向后端接口发当前页码,获取当前页面的商品List instance.get("/item/findAllItem?page="+this.page).then(res=>{ that.items = res.data.items; that.totalPage = res.data.totalPage; that.page = res.data.page; }); // Post请求 // 向后端接口发送当前商品id和用户id,获取商品购买状态 instance.post("/order/alipay/callback",{itemId:this.itemid,userId:this.user.id}).then(res=>{ if ( res.data.code == 20000 ) { alert("提示:您已购买该商品"); } else { alert("提示:您未购买该商品"); } }); }[4] 元件之間的跳轉和傳值方法如下:
// 跳转到MailReg组件 this.$router.push({name:"MailReg"}); // 跳转到item组件,并传递当前商品的id this.$router.push({path:"/item",query:{ItemId:myid}}); // item组件接收方法: this.itemid = this.$route.query.ItemId; // 另外不同组件可以依据token获取登录用户信息,需要用到redis,详见下文
2 用户积分排行榜模块说明:
1.1 Reids概述:
1) Redis是一种基于内存的数据存储NoSql;
2) Redis支持丰富的数据类型(String, List, Set, ZSet, Hash);
3) Redis有两种持久化方法: (1)快照(snapshot)存储,也叫rdb持久化,保存当前时刻的数据状态;(2) AOF(append only file)存储,将所有redis的写命令记录到日志文件中,Redis支持持久化间隔最快也是一秒,所以它是事务不安全的,即是可能丢失数据的。
4)Redis的应用场景:
(1) 利用Redis字符串完成项目中手机验证码存储的实现。------本项目采用
(2) 利用Redis字符串类型完成具有时效性的业务功能,如订单还有40分钟即将关闭。
(3) 利用Redis实现分布式集群系统中的Session共享。
(4) 利用Redis的ZSet数据类型(可排序set类型:元素+分数)实现排行榜功能。 ------本项目采用
(5) 利用Redis完成分布式缓存。 ------本项目实现MySql中数据的缓存
(6) 利用Redis存储认证之后的token信息。 ------非常方便,本项目采用。
(7) 利用Redis解决分布式集群系统中分布式锁问题。
1.2 基于Redis实现前端组件从后端获取用户信息:
Step1:前端Login.vue组件中用户输入登录信息提交的接口如下:
// 这里调用了后端/user/login接口,获取当前登录用户的token,存入Session的localStorage中,在后续网页浏览过程中可随时调取这个token instance.post("/user/login",this.user).then(res=>{ if ( res.data.state ) { alert(res.data.msg+",点击确定进入主页"); // 前端存储token信息 localStorage.setItem("token",res.data.token); that.$router.push({path:"/itemList"}); } else { alert(res.data.msg); that.user = {}; } });
Step2:后端/user/login接口实现如下:
// Controller层 @PostMapping("login") public Map<String, Object> loginAccount(@RequestBody User user, HttpSession session) { return userService.loginAccount(user, session); } // Service层 // 情况3:查询到一个用户时 // 获取主体对象 try { Subject subject = SecurityUtils.getSubject(); subject.login(new UsernamePasswordToken(user.getName(), user.getPassword())); User userDB = userListDB.get(0); String token = session.getId(); if (userDB.getScore() == null) { userDB.setScore(0.0); userDAO.updateUserScore(userDB); } redisTemplate.opsForValue().set("TOKEN_" + token, userDB, 30, TimeUnit.MINUTES); redisTemplate.opsForZSet().add("userRank", userDB, userDB.getScore()); map.put("token", token); map.put("state",true); map.put("msg","登录成功"); return map; ...
Redis整合SpringBoot有两种Template,即RedisTemplate和StringRedisTemplate。其中StringRedisTemplate是RedisTemplate的子类,两个方法基本一致,不同之处在于操作的数据类型不同,RedisTemplate中的两个泛型都是Object,意味着存储的key和value都可以是一个对象,而StringRedisTemplate的两个泛型都是String,意味着StringRedisTemplate的key和value都只能是字符串。
在Step2中,我将token和数据库中的用户信息userDB绑定在一起存入了Redis中,后续前端组件获取登录用户信息的代码如下:
// 从localStorage获取token let token = localStorage.getItem("token"); let that = this; // 发送axios请求,根据token获取用户信息 instance.get("/user/token?token="+token).then(res=>{ that.user = res.data; console.log(that.user); })
后端/user/token的接口如下:
@GetMapping({"token"}) public User findUser(String token) { System.out.println("接收的token信息:" + token); return (User)redisTemplate.opsForValue().get("TOKEN_" + token); }
Step3:用户退出登录时,应消除浏览器中对应的token,后端接口代码如下:
// 退出登录 @DeleteMapping({"logout"}) public Map<String, Object> logout(String token) { Map<String, Object> map = new HashMap<>(); try { redisTemplate.delete("TOKEN_" + token); Subject subject = SecurityUtils.getSubject(); subject.logout(); map.put("state", true); map.put("msg", "提示:退出账户成功"); return map; } catch (Exception e) { e.printStackTrace(); map.put("state", false); map.put("msg", "提示:退出账户失败"); return map; } }
1.3 基于Redis的用户积分排行榜实现:
MySql中的用户信息如图4所示:
Redis中的UserRank如图5所示:
Step1:当用户登录时,他的首要任务是接入UserRank对应的信息,后端代码如下:
if (userDB.getScore() == null) { userDB.setScore(0.0); userDAO.updateUserScore(userDB); } redisTemplate.opsForValue().set("TOKEN_" + token, userDB, 30, TimeUnit.MINUTES); redisTemplate.opsForZSet().add("userRank", userDB, userDB.getScore());
userDB是数据库中当前登录用户的信息(一定是有的,你注册了,对吧?),若用户首次登录我将他的分数在数据库设置为0.0,之后我在Redis的ZSet中加入这个用户,你知道,Set集合不会存储重复key值的元素,因此不会同一个用户出现在UserRank中两次。两个template完成了token绑定User,User绑定UserRank中Score的过程,之后的分数更新过程会反复使用这两个template实现。
Step2:当用户信息更新时,相应的与用户信息有关的两个template都要发生变化,代码如下:
// key值序列化 redisTemplate.setKeySerializer(new StringRedisSerializer()); // 由当前用户的token获取当前用户的信息 User firstUser = (User)redisTemplate.opsForValue().get("TOKEN_" + token); // 删除zSet中的当前用户 redisTemplate.opsForZSet().remove("userRank", firstUser); // 产生新的当前用户(昵称改变) List<User> userListDB = this.userDAO.selectUserByName(user.getName()); User secondUser = userListDB.get(0); // 更新token中当前用户的信息 redisTemplate.opsForValue().set("TOKEN_" + token, secondUser, 30, TimeUnit.MINUTES); // 产生zSet中的当前用户 redisTemplate.opsForZSet().add("userRank", secondUser, secondUser.getScore());
Step3:当用户扫码支付时,首次进入的后端controller如下:
// 支付单件商品 @GetMapping("payForItem") public byte[] alipay(String itemid,String userid, String token) { this.token = token; log.info("itemid=====>"+itemid); log.info("userid=====>"+userid); PayVo payVo = new PayVo(); payVo.setItemId(itemid); payVo.setUserId(userid); System.out.println(payVo.getUserId()); return alipayService.alipay(payVo); }
在alipayService有一个小型用户分级,即vip用户购物价格减半:
// 1:支付的用户 String userId = payVo.getUserId(); // my 1: 依据用户id查询用户 User user = userService.selectUserById(Integer.valueOf(userId)); // 依据商品id查询商品 Item item = itemService.getItemById(payVo.getItemId()); // my 1: 依据用户id查询用户 if ( item == null ) return null; // 2: 支付金额 String tempMoney = item.getPrice().toString(); String money = ""; if ( user.getRole().equals("normal") ) { money = tempMoney; } if ( user.getRole().equals("vip") ) { Double tempMoney2 = Double.valueOf(tempMoney)*0.5; money = String.valueOf(tempMoney2); }
在payForItem相同文件下,调用了payCommonService,在这里会实现用户积分更新和用户等级更新:
payCommonService.payUserPublic(bodyJsonObject, userId, user.getName(), orderNumber, tradeno, "alipay", this.token);
将"VIP"这件商品的id设置为“666”,当用户购买该商品时,当前用户更新过程如下:
if ( itemId.equals("666") ) { int myuserId = Integer.valueOf(userId); User userDB = userService.selectUserById(myuserId); // key值序列化 this.redisTemplate.setKeySerializer(new StringRedisSerializer()); // 由当前token获取当前用户信息 User firstUser = (User)redisTemplate.opsForValue().get("TOKEN_" + token); // 由当前用户信息删除当前用户zSet redisTemplate.opsForZSet().remove("userRank", firstUser); // 更新当前用户信息身份 userDB.setRole("vip"); // 更新当前用户新身份的分数 userService.updateUserRole(userDB); List<User> userListDB = this.userDAO.selectUserByName(userDB.getName()); // 获取当前新身份用户的完整信息 User secondUser = userListDB.get(0); // 更新当前token对应的当前用户 redisTemplate.opsForValue().set("TOKEN_" + token, secondUser, 30, TimeUnit.MINUTES); // 设置当前用户的zSet redisTemplate.opsForZSet().add("userRank", secondUser, secondUser.getScore().doubleValue()); }
当前用户积分更新过程如下:
// 更新当前用户的积分 double tempScore = Double.valueOf(orderDetail.getPrice()) * 0.3; String key1 = "TOKEN_" + token; // 由当前token获取当前用户 User user = (User)redisTemplate.opsForValue().get(key1); // 更新当前用户的zSet分数 redisTemplate.opsForZSet().incrementScore("userRank", user, tempScore); // 获取当前用户的zSet分数 double newScore = redisTemplate.opsForZSet().score("userRank", user); // 删除当前用户的zSet(因为要更新当前用户的信息,将当前用户在数据库中的分数进行同步) redisTemplate.opsForZSet().remove("userRank", new Object[] { user }); user.setScore(newScore); userDAO.updateUserScore(user); // 更新token对应的当前用户的信息 redisTemplate.opsForValue().set(key1, user); // 新增当前用户的zSet redisTemplate.opsForZSet().add("userRank", user, newScore);
以上是Redis+SpringBoot案例分析的詳細內容。更多資訊請關注PHP中文網其他相關文章!