青青陪玩
一、项目框架

二、搭建项目
1、新建项目


2、新建Model模块


3、引入依赖
<!--数据库驱动-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.33</version>
<scope>runtime</scope>
</dependency>
<!--Druid 连接池-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.2.20</version>
</dependency>
<!--MyBatisPlus-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-spring-boot3-starter</artifactId>
<version>3.5.14</version>
</dependency>
<!--FastJson2-->
<dependency>
<groupId>com.alibaba.fastjson2</groupId>
<artifactId>fastjson2</artifactId>
<version>2.0.40</version>
</dependency>
<!--redis-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!--分页插件-->
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper-spring-boot-autoconfigure</artifactId>
<version>1.4.6</version>
</dependency>
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper-spring-boot-starter</artifactId>
<version>1.4.6</version>
</dependency>
<!--AOP-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
4、复制上一个项目可以使用的工具类等
5、修改mobile-api的依赖 pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.javasm</groupId>
<artifactId>qingqing</artifactId>
<version>0.0.1-SNAPSHOT</version>
</parent>
<artifactId>mobile-api</artifactId>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>com.javasm</groupId>
<artifactId>common</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
</dependencies>
</project>
三、介绍项目模板
1、找到代码

2、配置文件yml
server:
tomcat:
max-swallow-size: 10MB
max-http-form-post-size: 10MB
port: 8080
spring:
servlet:
multipart:
max-file-size: 10MB
max-request-size: 10MB
application:
name: mobile_api
datasource:
url: jdbc:mysql://127.0.0.1:3306/qingqing?useUnicode=true&characterEncoding=UTF-8&allowMultiQueries=true&serverTimezone=GMT%2B8&useSSL=false&allowPublicKeyRetrieval=true
driver-class-name: com.mysql.cj.jdbc.Driver
username: root
password: root
type: com.alibaba.druid.pool.DruidDataSource
druid:
initial-size: 5
min-idle: 10
max-active: 100
data:
redis:
host: localhost
port: 6379
password: javasm
database: 2
mybatis-plus:
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
3、vite.config.js
这个文件,仅仅限于在本地使用vite启动项目的时候生效。
当前项目,如果发布到Linux服务器上,使用Nginx发布的话,配置文件失效,要重新配置
import { fileURLToPath, URL } from 'node:url'
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
export default defineConfig({
plugins: [vue()],
//配置根路径,这个不配置会导致部署之后访问不到
base: './',
// 构建
build: {
outDir: 'dist', //指定打包输出路径
assetsDir: 'assets', //指定静态资源存放路径
cssCodeSplit: true, //css代码拆分,禁用则所有样式保存在一个css里面
sourcemap: false, //是否构建source map 文件
// 生产环境取消 console
minify: 'terser',
terserOptions: {
compress: {
drop_console: true,
drop_debugger: true
}
},
//会打包出 css js 等文件夹 目录显得清晰
rollupOptions: {
output: {
chunkFileNames: 'js/[name]-[hash].js',
entryFileNames: 'js/[name]-[hash].js',
assetFileNames: '[ext]/[name]-[hash].[ext]'
}
}
},
resolve: {
alias: {
//别名,给./src目录,起个别名@,在文件中就可以使用@替换src了
'@': fileURLToPath(new URL('./src', import.meta.url))
}
},
// 本地服务
server: {
host: "0.0.0.0", // ip
port: 5173, // 端口号
open: false, // 是否自动在浏览器打开
https: false, // 是否开启 https
cors: true, // 允许跨域
proxy: {
//之前访问服务端接口:http://localhost:8080/login/doLogin
//这样配置之后;http://localhost:5173/web-dev-api/login/doLogin
//内部看见了/web-dev-api转发,到http://localhost:8080
//http://localhost:8080/login/doLogin
'/web-dev-api': {
//本地IP
target: 'http://localhost:8080',
changeOrigin: true,
rewrite: (p) => p.replace(/^\/web-dev-api/, '')
},
'/web-api': {
//线上服务器地址
target: 'http://121.36.72.48:8080',
changeOrigin: true,
rewrite: (p) => p.replace(/^\/web-api/, '')
},
}
}
})
3、环境配置

vite项目,会根据环境,来自动选择哪个配置文件。
开发阶段,会自动读取.evn.development配置文件
4、安装

5、启动

四、登录模块
已经有登录页面了,只需要写接口即可。
1、接口
登录模块:对方需要什么返回值,我需要什么参数,才能给他这个返回值。
页面能提供:用户名和密码
页面需要:用户基本信息,包括uid头像昵称等
先写接口、测试接口可用
写页面
页面对接接口
1.1 接口思路
-
创建一个Controller
- LoginController 调用 LoginService ,LoginService去调用WebUserService,查询用户信息
-
新建用户名密码登录的方法
-
完善登录接口,doUsernameLogin
-
添加Redis缓存,改变请求的流程
-
添加,记录登录日志的功能
-
每天,每个用户,只能记录1次
-
记录每天第一次登录的时间,其余不记录
-
1.2 代码示例
从接口最开始的Controller开始写,后续缺什么补什么。
- Controller
@RestController
@RequestMapping("/login")
public class LoginController {
@Resource
LoginService loginService;
@PostMapping("/doUsernameLogin")
public R doUnameLogin(String username,String password){
//检查参数合法性
ParameterUtils.checkParameter(username,password);
WebUser loginUser = loginService.doUnameLogin(username,password);
return R.ok(loginUser);
}
}
- LoginService
第一形态
@Service
public class LoginServiceImpl implements LoginService {
@Resource
WebUserService webUserService;
@Override
public WebUser doUnameLogin(String username, String password) {
//不加Redis的情况下,纯mysql查询的业务完成之后,要暂停编码,开始测试
//心里要确认一下,这一段的代码是没有问题的
//根据用户名 查询用户信息
WebUser loginUser = webUserService.getByUname(username);
//如果用户信息是null ,说明数据库没有
if (loginUser == null){
throw new JavasmException(ExceptionEnum.User_Not_Found);
}
//如果有用户信息,校验密码是否正确
//如果密码不正确,抛出异常
if (!password.equals(loginUser.getPassword())){
throw new JavasmException(ExceptionEnum.Password_Error);
}
//返回用户信息
return loginUser;
}
}
第二形态
@Service
public class LoginServiceImpl implements LoginService {
@Resource
WebUserService webUserService;
@Resource
RedisTemplate<String,WebUser> redisTemplate;
@Override
public WebUser doUnameLogin(String username, String password) {
//先从Redis中获取用户信息 weuser:username:%s
//考虑到Key会反复使用,新建一个常量类,统一管理Redis的Key
String key = String.format(RedisKeys.WebUserName,username);
//从Redis中获取数据
WebUser webUser = redisTemplate.opsForValue().get(key);
if (webUser != null){
if (webUser.getUid() == null){
throw new JavasmException(ExceptionEnum.User_Not_Found);
}
//说明已经查询到了数据
//检查密码
//如果密码不正确,抛出异常
if (!password.equals(webUser.getPassword())){
throw new JavasmException(ExceptionEnum.Password_Error);
}
//刷新一下Redis的过期时间
redisTemplate.expire(key,15, TimeUnit.DAYS);
return webUser;
}
//Redis中获取不到数据了
//根据用户名 查询用户信息
WebUser loginUser = webUserService.getByUname(username);
//如果用户信息是null ,说明数据库没有
if (loginUser == null){
redisTemplate.opsForValue().set(key,new WebUser(),10,TimeUnit.MINUTES);
throw new JavasmException(ExceptionEnum.User_Not_Found);
}
//如果有用户信息,校验密码是否正确
if (!password.equals(loginUser.getPassword())){
throw new JavasmException(ExceptionEnum.Password_Error);
}
//登录成功了
redisTemplate.opsForValue().set(key,loginUser,15,TimeUnit.DAYS);
//返回用户信息
return loginUser;
}
}
第三形态
考虑到登录之后,需要头像和昵称
@Data
@NoArgsConstructor
@AllArgsConstructor
public class WebUser extends Model<WebUser> {
@TableId(type = IdType.AUTO)
private Integer uid;
//用户名
private String username;
//密码
private String password;
//0正常1删除2封号
private Integer status;
//邮箱
private String email;
//手机号
private String phone;
//注册时间
private Date regTime;
@TableField(exist = false)
private WebUserInfo userInfo;
}
@Data
@NoArgsConstructor
@AllArgsConstructor
public class WebUserInfo extends Model<WebUserInfo> {
//用户编号
@TableId(type = IdType.INPUT)
private Integer uid;
//用户名
private String nickname;
//用户vip等级
private Integer vipGrade;
//头像
private String headPic;
//贵族级别
private Integer loadGrade;
//是否大神用户 0普通用户 1大神用户
private Integer godStatus;
//粉丝数量
private Integer fansNum;
//IP归属地
private String ipAddress;
//个人说明
private String remark;
//喜欢的游戏说明
private String hobbyGame;
//喜欢的音乐说明
private String hobbyMusic;
//封停状态 0封停 1正常
private String lockoutStatus;
//解封时间
private Date lockoutTime;
//生日
private String birthday;
//城市
private String city;
//性别 0保密1男2女
private Integer gender;
//心情
private String mood;
//简介
private String intro;
//0自然人1ai
private Integer isai;
}
@Service
public class LoginServiceImpl implements LoginService {
@Resource
WebUserService webUserService;
@Resource
WebUserInfoService webUserInfoService;
@Resource
RedisTemplate<String,WebUser> redisTemplate;
@Override
public WebUser doUnameLogin(String username, String password) {
//先从Redis中获取用户信息 weuser:username:%s
//考虑到Key会反复使用,新建一个常量类,统一管理Redis的Key
String key = String.format(RedisKeys.WebUserName,username);
//从Redis中获取数据
WebUser webUser = redisTemplate.opsForValue().get(key);
if (webUser != null){
if (webUser.getUid() == null){
throw new JavasmException(ExceptionEnum.User_Not_Found);
}
//说明已经查询到了数据
//检查密码
//如果密码不正确,抛出异常
if (!password.equals(webUser.getPassword())){
throw new JavasmException(ExceptionEnum.Password_Error);
}
//刷新一下Redis的过期时间
redisTemplate.expire(key,15, TimeUnit.DAYS);
return webUser;
}
//Redis中获取不到数据了
//根据用户名 查询用户信息
WebUser loginUser = webUserService.getByUname(username);
//如果用户信息是null ,说明数据库没有
if (loginUser == null){
redisTemplate.opsForValue().set(key,new WebUser(),10,TimeUnit.MINUTES);
throw new JavasmException(ExceptionEnum.User_Not_Found);
}
//如果有用户信息,校验密码是否正确
if (!password.equals(loginUser.getPassword())){
throw new JavasmException(ExceptionEnum.Password_Error);
}
//登录成功之后,存入Redis之前,要查询出WebUserInfo的信息
WebUserInfo userInfo = webUserInfoService.getById(loginUser.getUid());
if (userInfo != null){
loginUser.setUserInfo(userInfo);
}
//登录成功了
redisTemplate.opsForValue().set(key,loginUser,15,TimeUnit.DAYS);
//返回用户信息
return loginUser;
}
}
1.3 记录登录日志
- 自定义注解
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface WebLoginLog {
}
- AOP
@Component
@Aspect
public class WebLoginLogAspect {
@Resource
RedisTemplate<String,Integer> redisTemplate;
@AfterReturning(
value = "@annotation(com.javasm.qingqing.common.annotation.WebLoginLog)",
returning = "webUser"
)
public void saveLog(JoinPoint joinPoint,Object webUser){
if (webUser != null && webUser instanceof WebUser){
//记录日志
//记录日志的行为,不在主要流程里,
// 日志是否记录以及是否记录成功,是否抛出异常,都不影响登录成功了
new Thread(()->{
WebUser user = (WebUser) webUser;
Integer uid = user.getUid();
//如何判断 今天是否已经记录过日志了
//因为 登录接口访问的比较频繁,如果通过mysql判断数据是否已经存储了,会给数据库带来很大压力
String today = new SimpleDateFormat("yyyy-MM-dd").format(new Date());
//key webuser:login:log:1006:2025-11-29
String key = String.format(RedisKeys.WebLoginLog,uid,today);
Integer redisUid = redisTemplate.opsForValue().get(key);
if (redisUid == null){
//今天还没有存储过
WebUserLoginLog webUserLoginLog = new WebUserLoginLog();
webUserLoginLog.setUid(uid);
webUserLoginLog.setLoginTime(new Date());
webUserLoginLog.insert();
//redis中 记录今天登录信息
redisTemplate.opsForValue().set(key,uid,1, TimeUnit.DAYS);
}
}).start();
}
}
}
- 使用注解
@WebLoginLog
public WebUser doUnameLogin(String username, String password)
- RedisKeys
public interface RedisKeys {
//用户名查询用户信息:webuser:username:用户名
String WebUserName = "webuser:username:%s";
//每日登录记录:webuser:login:log:uid:2025-11-11
String WebLoginLog = "webuser:login:log:%s:%s";
}
2、页面对接
<template>
<div class="login-main">
<div class="login-head">
<div class="row video-main">
<div class="col-lg-12 col-md-12">
<video class="head-video" src="http://cd.ray-live.cn/video/xiangxiang.mp4" poster="" autoplay="" loop="" muted=""></video>
</div>
</div>
</div>
<div class="container">
<div class="row">
<div class="col-lg-6 col-md-6 login-form-main">
<div class="login-form">
<h2>登录</h2>
<form ref="formValid" class="needs-validation" novalidate>
<div class="tab-content" id="nav-tabContent">
<div class="tab-pane fade show active" id="nav-home" role="tabpanel" aria-labelledby="nav-home-tab" tabindex="0">
<div class="form-group">
<input type="text" class="form-control" placeholder="请输入用户名" v-model="form.uname" >
</div>
<div class="form-group input-group">
<input type="password" class="form-control" placeholder="请输入密码" v-model="form.pwd" @keyup.enter="doLogin">
</div>
</div>
<div class="tab-pane fade" id="nav-profile" role="tabpanel" aria-labelledby="nav-profile-tab" tabindex="0">
<div class="form-group">
<input type="text" class="form-control" placeholder="请输入手机号" v-model="form.uname" required >
</div>
<div class="form-group input-group">
<input type="password" class="form-control" placeholder="请输入验证码" v-model="form.pwd" required>
<input type="button" :value="countDown > 0 ? countDown+` s后重新发送` : '发送验证码'" class="btn btn-info" :disabled="countDown > 0">
</div>
</div>
</div>
<nav>
<div class="nav nav-tabs" id="nav-tab" role="tablist">
<button class="nav-link active" id="nav-home-tab"
data-bs-toggle="tab" data-bs-target="#nav-home" type="button"
role="tab" aria-controls="nav-home" aria-selected="true" @click="form.type='uname'">用户名密码登录</button>
<button class="nav-link" id="nav-profile-tab"
data-bs-toggle="tab" data-bs-target="#nav-profile" type="button"
role="tab" aria-controls="nav-profile" aria-selected="false" @click="form.type='phone'">手机号验证码登录</button>
</div>
</nav>
<button type="button" class="default-btn" @click="doLogin">登录</button>
<div class="or-text"><span></span></div>
<button type="button" class="google-btn" @click="jumpReg">注册</button>
</form>
</div>
</div>
<div class="col-lg-3 col-md-3 offset-3 login-logo">
<img src="@/assets/imgs/logo_reg.png" alt="image">
</div>
</div>
</div>
</div>
</template>
<script setup>
import {ref, onMounted} from "vue";
import router from '@/router/index.js'
import api from '@/utils/request.js'
import userStore from '@/stores/userStore.js';
let form = ref({
uname: '',
pwd: '',
});
let countDown=ref(0);
let doLogin = () => {
let param = {
"username": form.value.uname,
"password": form.value.pwd
}
api.post("/login/doUsernameLogin",param).then(result=>{
//把登录返回的用户信息,存储到pina中,sessionStorage中
userStore().userModel = result.data;
router.push("/")
})
}
let jumpReg=()=>{
router.push("/reg")
}
onMounted(() => {
})
</script>
<style scoped>
.login-form input {
margin: 20px 0px;
}
.vector-image img {
margin-top: 145px;
}
.login-head {
height: 490px;
display: flex;
justify-content: center;
position: absolute;
width: 100%;
top: 0px;
left: 0px;
z-index: -1;
}
.login-head video {
position: absolute;
top: 0;
left: 0;
min-width: 100%; /* 保证视频宽度至少为 100% */
min-height: 100%; /* 保证视频高度至少为 100% */
width: auto;
height: auto;
z-index: -100;
object-fit: cover;
}
.login-main {
height: 100%;
width: 99.9%;
max-width: 1920px;
min-height: 99vh;
min-width: 1320px;
max-height: 100%;
position: absolute;
z-index: -2;
scrollbar-width: none;
overflow: hidden;
}
.login-main .container {
position: absolute;
float: left;
left: 25%;
top: 10px;
margin-top: 0px;
z-index: 2;
height: 690px;
}
.login-form-main{
margin-top: 100px;
}
.login-logo{
}
.login-form{
background: rgba(255,255,255,0.5);
}
/* 针对 WebKit 浏览器 */
::-webkit-scrollbar {
display: none;
}
</style>
五、注册模块
1、接口
- Controller
@PostMapping("/doReg")
public R doRegister(@RequestBody WebUser webUser) {
//参加合法校验
ParameterUtils.checkParameter(
webUser,
webUser.getUserInfo(),
webUser.getUsername(),
webUser.getUserInfo().getNickname()
);
WebUser regUser = loginService.doRegister(webUser);
return R.ok(regUser);
}
- Service
package com.javasm.qingqing.login.service.impl;
import com.javasm.qingqing.common.annotation.WebLoginLog;
import com.javasm.qingqing.common.container.RedisKeys;
import com.javasm.qingqing.common.exception.ExceptionEnum;
import com.javasm.qingqing.common.exception.JavasmException;
import com.javasm.qingqing.common.utils.IpAddressUtil;
import com.javasm.qingqing.common.utils.MD5Util;
import com.javasm.qingqing.login.service.LoginService;
import com.javasm.qingqing.webuser.entity.WebUser;
import com.javasm.qingqing.webuser.entity.WebUserInfo;
import com.javasm.qingqing.webuser.entity.WebUserPossession;
import com.javasm.qingqing.webuser.service.WebUserInfoService;
import com.javasm.qingqing.webuser.service.WebUserService;
import io.micrometer.common.util.StringUtils;
import jakarta.annotation.Resource;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.Date;
import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.TimeUnit;
/**
* @className: LoginServiceImpl
* @author: gfs
* @date: 2025/11/29 11:08
* @version: 0.1
* @since: jdk17
* @description:
*/
@Service
public class LoginServiceImpl implements LoginService {
@Resource
WebUserService webUserService;
@Resource
WebUserInfoService webUserInfoService;
@Resource
RedisTemplate<String,WebUser> redisTemplate;
@Resource
IpAddressUtil ipAddressUtil;
@Override
@WebLoginLog
public WebUser doUnameLogin(String username, String password) {
//先从Redis中获取用户信息 weuser:username:%s
//考虑到Key会反复使用,新建一个常量类,统一管理Redis的Key
String key = String.format(RedisKeys.WebUserName,username);
//从Redis中获取数据
WebUser webUser = redisTemplate.opsForValue().get(key);
if (webUser != null){
if (webUser.getUid() == null){
throw new JavasmException(ExceptionEnum.User_Not_Found);
}
//说明已经查询到了数据
//检查密码
//如果密码不正确,抛出异常
webUser.checkPassword(password);
//刷新一下Redis的过期时间
redisTemplate.expire(key,15, TimeUnit.DAYS);
return webUser;
}
//Redis中获取不到数据了
//根据用户名 查询用户信息
WebUser loginUser = webUserService.getByUname(username);
//如果用户信息是null ,说明数据库没有
if (loginUser == null){
redisTemplate.opsForValue().set(key,new WebUser(),10,TimeUnit.MINUTES);
throw new JavasmException(ExceptionEnum.User_Not_Found);
}
//如果有用户信息,校验密码是否正确
loginUser.checkPassword(password);
//登录成功之后,存入Redis之前,要查询出WebUserInfo的信息
WebUserInfo userInfo = webUserInfoService.getById(loginUser.getUid());
if (userInfo != null){
loginUser.setUserInfo(userInfo);
}
//登录成功了
redisTemplate.opsForValue().set(key,loginUser,15,TimeUnit.DAYS);
//返回用户信息
return loginUser;
}
@Override
@Transactional
@WebLoginLog//注册完成,自动登录,记录登录日志
public WebUser doRegister(WebUser webUser) {
//用户数据 单独拿出来
String username = webUser.getUsername();
String password = webUser.getPassword();
String phone = webUser.getPhone();
String nickname = webUser.getUserInfo().getNickname();
//校验 用户名是否重复
WebUser byUname = webUserService.getByUname(username);
if (byUname != null){
throw new JavasmException(ExceptionEnum.User_Exist);
}
//密码,如果密码加密,需要把用户传入的密码加密之后,重新放到对象中
String md5password = MD5Util.encrypt(password);
webUser.setPassword(md5password);
//注册时间
webUser.setRegTime(new Date());
//状态--数据库中已经配置了默认值,但是等会要把数据存入Redis,不想二次查询数据库,所以配一个默认值
webUser.setStatus(0);
//保存webUser数据
webUserService.save(webUser);
//数据存储成功,就会有uid
Integer uid = webUser.getUid();
if (uid == null){
throw new JavasmException(ExceptionEnum.SYSTEM_ERROR);
}
WebUserInfo userInfo = webUser.getUserInfo();
userInfo.setUid(uid);
//如果昵称是空的,生成一个
if (StringUtils.isEmpty(nickname)){
//用户没有传昵称,分配一个默认昵称
userInfo.setNickname("虾仁"+uid);
}
//分配头像
String path = "http://cd.ray-live.cn/imgs/headpic/pic_%s.jpg";
//顾前不顾后
int num = ThreadLocalRandom.current().nextInt(1, 580);
userInfo.setHeadPic(String.format(path,num));
//ip
userInfo.setIpAddress(ipAddressUtil.getClientIp());
//其他的属性
userInfo.setVipGrade(1);
userInfo.setFansNum(0);
userInfo.setLoadGrade(1);
userInfo.setGodStatus(0);
userInfo.setLockoutStatus("1");
//...
userInfo.insert();
//添加用户财富信息
WebUserPossession possession = new WebUserPossession();
possession.setUid(uid);
possession.setRichNum(0L);
possession.setCharmNum(0L);
possession.insert();
webUser.setPossession(possession);
//数据保存到redis
new Thread(()->{
String uname_key = String.format(RedisKeys.WebUserName,username);
redisTemplate.opsForValue().set(uname_key,webUser,15,TimeUnit.DAYS);
String uid_key = String.format(RedisKeys.WebUserId,uid);
redisTemplate.opsForValue().set(uid_key,webUser,15,TimeUnit.DAYS);
}).start();
return webUser;
}
}
- 修改了密码加密策略,所以登录模块的密码校验更改了
@Data
@NoArgsConstructor
@AllArgsConstructor
public class WebUser extends Model<WebUser> {
@TableId(type = IdType.AUTO)
private Integer uid;
//用户名
private String username;
//密码
private String password;
//0正常1删除2封号
private Integer status;
//邮箱
private String email;
//手机号
private String phone;
//注册时间
private Date regTime;
@TableField(exist = false)
private WebUserInfo userInfo;
@TableField(exist = false)
private WebUserPossession possession;
public void checkPassword(String pwd){
if (!MD5Util.encrypt(pwd).equals(password)){
throw new JavasmException(ExceptionEnum.Password_Error);
}
}
}
- 注册的时候,有多表添加,增加了事务
@Configuration
@EnableTransactionManagement
public class ServerConfig {
}
2、页面
<template>
<div class="reg-main">
<div class="reg-head">
<div class="row video-main">
<div class="col-lg-12 col-md-12">
<video width="100%"
class="head-video"
src="http://cd.ray-live.cn/video/goddess-moon.mp4"
poster="http://cd.ray-live.cn/video/goddess-moon.jpg"
autoplay
loop
muted></video>
</div>
</div>
</div>
<div class="container">
<div class="row">
<div class="col-lg-12 col-md-12">
<div class="register-form">
<h2>注册</h2>
<form id="reg-form" class="needs-validation" novalidate ref="form">
<div class="form-group">
<label>用户名</label>
<input type="text" class="form-control" placeholder="请输入用户名"
v-model="user.username" required minlength="4">
<div class="invalid-feedback" >用户名长度不足</div>
<div class="valid-feedback" >用户名不能为空</div>
</div>
<div class="form-group">
<label>密码</label>
<input type="password" class="form-control" placeholder="请输入密码"
v-model="user.password" required>
<div class="invalid-feedback">密码不能为空</div>
</div>
<div class="form-group">
<label>手机号</label>
<input type="number" class="form-control" placeholder="请输入手机号"
v-model="user.phone" required minlength="11">
<div class="invalid-feedback">请输入11位手机号</div>
</div>
<div class="form-group">
<label>昵称</label>
<input type="text" class="form-control" placeholder="请输入昵称"
v-model="user.userInfo.nickname" required>
<div class="invalid-feedback">昵称不能为空</div>
</div>
<button type="button" class="default-btn" @click="doRegister">注册</button>
<div class="or-text"><span></span></div>
<button type="button" class="google-btn" @click="jumpLogin">返回登录</button>
</form>
</div>
</div>
</div>
</div>
<div class="row reg-logo">
<div class="col-lg-12 col-md-12">
<img src="@/assets/imgs/logo_reg.png" alt="image">
</div>
</div>
</div>
</template>
<script setup>
import {ref, reactive, getCurrentInstance, onMounted} from "vue";
import router from '@/router/index'
import api from '@/utils/request.js'
import userStore from "@/stores/UserStore.js";
import {ElMessage} from "element-plus";
let user = ref({
"username": "",
"password": "",
"userInfo": {
"nickname": "",
"email": ""
}
})
let form = ref()
let jumpLogin = () => {
router.push("/login")
}
//登录
let doRegister = () => {
//$('#already').modal('show')
if (form.value.checkValidity()){
//校验通过
api.postJson("/login/doReg",user.value).then(result=>{
if (result.code === 200){
userStore().userModel = result.data;
ElMessage.success("注册成功");
router.push("/")
}else {
ElMessage.error(result.msg)
}
})
}
form.value.classList.add('was-validated');
}
onMounted(() => {
})
</script>
<style scoped>
.reg-head {
height: 480px;
display: flex;
justify-content: center;
position: absolute;
width: 99.3%;
top: 0px;
left: 0px;
z-index: -1;
}
.login-head video {
position: absolute;
top: 0;
left: 0;
min-width: 100%; /* 保证视频宽度至少为 100% */
min-height: 100%; /* 保证视频高度至少为 100% */
width: auto;
height: auto;
z-index: -100;
object-fit: cover;
}
.reg-main{
height: 100%;
width: 99.9%;
max-width: 1920px;
min-height: 99vh;
min-width: 1320px;
max-height: 100%;
position: absolute;
z-index: -2;
scrollbar-width: none;
overflow: hidden;
}
.register-form {
background: rgba(255, 255, 255, 0.5);
}
.container {
right: 50px;
top: 50px;
position: absolute;
width: 660px;
}
.reg-logo {
position: absolute;
left: 100px;
top: 0px
}
</style>
六、首页推荐
1、游戏列表
找到游戏列表,game表,生成代码,写游戏列表接口,获取热门list
- Controller
@RestController
@RequestMapping("/home")
public class HomeController {
@Resource
HomeService homeService;
@GetMapping("/hot/game/list")
public R queryHotGameList(){
List<Game> gameList = homeService.queryHotGameList();
return R.ok(gameList);
}
}
- Service
@Service
public class HomeServiceImpl implements HomeService {
@Resource
GameService gameService;
@Override
public List<Game> queryHotGameList() {
List<Game> gameList = gameService.getHotGameList();
return gameList;
}
}
- gameService
@Service("gameService")
public class GameServiceImpl extends ServiceImpl<GameDao, Game> implements GameService {
@Override
public List<Game> getHotGameList() {
LambdaQueryWrapper<Game> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(Game::getIshot,1);
queryWrapper.orderByAsc(Game::getSort);
List<Game> list = list(queryWrapper);
return list;
}
}
2、热门游戏人物列表
- Controller
@GetMapping("/hot/game/user/list/{id}")
public R queryGameUserListByGid(@PathVariable Integer id){
ParameterUtils.checkParameter(id);
List<HomeGameVo> list = homeService.queryHotGameUserList(id);
return R.ok(list);
}
- HomeService
@Override
public List<HomeGameVo> queryHotGameUserList(Integer id) {
List<HomeGameVo> list;
if (id == -1){
list = gameService.queryHomeGameUserHot();
}else {
//根据游戏id 查询信息
list = gameService.queryHomeGameUserList(id);
}
return list;
}
- GameService
@Override
public List<HomeGameVo> queryHomeGameUserList(Integer id) {
return gameDao.selectGameUserListByGid(id);
}
@Override
public List<HomeGameVo> queryHomeGameUserHot() {
return gameDao.selectGameUserHotList();
}
- GameDao
public interface GameDao extends BaseMapper<Game> {
@Select("SELECT skill.skill_price as price," +
"skill.uid,web.head_pic," +
"web.nickname,game.`name` as game_name " +
"FROM skill,game,web_user_info web " +
"WHERE skill.game_id = game.id " +
"AND skill.uid = web.uid " +
"AND skill.game_id = #{id} limit 12")
List<HomeGameVo> selectGameUserListByGid(Integer id);
@Select("SELECT skill.skill_price as price," +
"skill.uid,web.head_pic," +
"web.nickname,game.`name` as game_name " +
"FROM skill,game,web_user_info web " +
"WHERE skill.game_id = game.id " +
"AND skill.uid = web.uid " +
"AND skill.skill_promotion > 0 " +
"ORDER BY skill.order_times DESC," +
"skill.skill_price ASC " +
"LIMIT 12")
List<HomeGameVo> selectGameUserHotList();
}
3、优化
减少查询的次数,减少数据库访问的次数,减少Redis的访问次数,减少对业务服务器的访问次数
思路
不能让用户每次访问首页的时候,都去查询数据库
让用户去查询Redis缓存
客户端和服务端通信了几次
理想状态
用户第一次访问首页,通过1个接口,把所有的数据,都得到。
所有的数据:推荐的游戏列表,把每个游戏推荐的列表,也一起发送过去
前端:把这些数据,存储到SessionStorage中
用户第二次访问首页的时候,直接访问本地的数据,不再请求Java服务器
- HomeController
@GetMapping("/game")
public R queryHomeGame(){
List<Game> list = homeService.getAllGameList();
return R.ok(list);
}
- HomeService
@Resource
RedisTemplate<String,Object> redisTemplate;
@Override
public List<Game> getAllGameList() {
//去Redis中查询数据
String key = RedisKeys.HomeHotList;
//数据是list ,但是不想存储成redislist类型,整存整取
Object o = redisTemplate.opsForValue().get(key);
if(o != null){
return (List<Game>) o;
}
//说明缓存没有数据,数据库查询
//查询游戏以列表
List<Game> list = gameService.getHotGameList();
//根据游戏列表,把所有的游戏 用户信息 查询到
List<HomeGameVo> homeGameVoList = gameService.getHotGameUserList(list);
//手工创建一个 id = -1的推荐游戏
Game gameHot = new Game();
gameHot.setId(-1);
gameHot.setName("人气推荐");
gameHot.setIcon("http://cd.ray-live.cn/imgs/game/jx.png");
List<HomeGameVo> gameVoList = gameService.queryHomeGameUserHot();
gameHot.setGameSkillList(gameVoList);
list.add(0,gameHot);
//循环游戏列表,寻找对应的陪玩人员信息
list.forEach(game->{
if (game.getId() != -1){
//空的集合,等会存储当前循环的游戏,都有哪些人员
List<HomeGameVo> gameSkillList = new ArrayList<>();
homeGameVoList.forEach(homeGameVo -> {
if (game.getId().equals(homeGameVo.getGameId())){
//在大的集合中,找到当前游戏的陪玩人员信息,收集起来
gameSkillList.add(homeGameVo);
}
});
//陪玩列表,存入游戏对象
game.setGameSkillList(gameSkillList);
}
});
//数据组装完成,存入Redis
redisTemplate.opsForValue().set(key,list,2, TimeUnit.HOURS);
return list;
}
- GameService
@Override
public List<HomeGameVo> getHotGameUserList(List<Game> list) {
//"1,2,3,4"
String ids = list.stream().map(game -> String.valueOf(game.getId())).collect(Collectors.joining(","));
//减少数据库访问次数,不能使用for循环访问
List<HomeGameVo> gameVoList = gameDao.selectGameUserListByGids(ids);
return gameVoList;
}
- GameDao
@Select("SELECT skill.skill_price as price," +
"skill.uid,web.head_pic," +
"web.nickname,game.`name` as game_name, game.id as game_id " +
"FROM skill,game,web_user_info web " +
"WHERE skill.game_id = game.id " +
"AND skill.uid = web.uid " +
"AND skill.game_id in (${ids}) limit 10")
List<HomeGameVo> selectGameUserListByGids(String ids);
- HomeGameVo
@Data
public class HomeGameVo {
private Integer uid;
private String headPic;
private String nickname;
private String gameName;
private String price;
private Integer gameId;
}
4 页面
4.1 Pina
import {ref, reactive, getCurrentInstance, onMounted} from "vue";
import {defineStore} from "pinia";
export default defineStore('userHomeGameStore', () => {
const gameList = ref([])
return {
gameList
}
}, {
persist: {
storage: sessionStorage,
paths: ['gameList']
}
}
)
4.2 vue
<template>
<div class="hot-room">
<div class="row">
<h1>人气推荐</h1>
</div>
<div class="row">
<div v-for="game in game_list"
class="col-md-2 col-lg-1 card a game-card"
:id="'game-card-'+game.id" @click="viewUser(game.id)">
<img :src="game.icon">
<div class="card-body">
<h6 class="card-title">{{game.name}}</h6>
</div>
</div>
</div>
<div class="row skill-list">
<div v-for="user in game_user_list" class="col-lg-2 col-md-4 card skill-card" >
<img :src="user.headPic">
<div class="card-body">
<h5 class="card-title">{{user.nickname}}</h5>
</div>
<div class="card-body row">
<div class="col">
<span class="bg-success text-white p-1">{{user.gameName}}</span>
</div>
<div class="col">
<span class="text-danger a">{{user.price}}/小时</span>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import {ref, onMounted, nextTick} from "vue";
import router from '@/router/index'
import api from '@/utils/request.js'
import UserHomeGameStore from "@/stores/UserHomeGameStore.js";
let game_list = ref([])
let game_user_list=ref([])
let viewUser = (gameId) => {
//移除game-card的class属性 active
document.querySelectorAll(".game-card").forEach(item => {
item.classList.remove("active")
})
//添加当前game-card的class属性 active
document.querySelector("#game-card-" + gameId).classList.add("active")
game_user_list.value = game_map.value.get(gameId);
}
let query=()=>{
//先去SessionStorage中查询数据
if (UserHomeGameStore().gameList.length > 0){
//本地有数据,已经缓存了
game_list.value = UserHomeGameStore().gameList;
createMap(UserHomeGameStore().gameList)
nextTick(()=>{
viewUser(-1)
})
}else {
api.get("/home/game").then(result=>{
//本页循环需要使用的数据
game_list.value = result.data;
//本地SessionStorage缓存数据
UserHomeGameStore().gameList = result.data;
createMap(result.data)
nextTick(()=>{
viewUser(-1)
})
})
}
}
let game_map = ref({})
function createMap(list){
let map = new Map();
list.forEach(game=>{
map.set(game.id,game.gameSkillList);
})
game_map.value = map;
}
onMounted(() => {
query();
});
</script>
<style scoped>
</style>