一、项目介绍
首先声明,本项目静态资源和前端部分样式,后端部分接口代码由黑马程序员提供,我根据黑马程序员的基础更改了一部分功能,本项目学习视频:https://www.bilibili.com/video/BV14z4y1N7pg/?spm_id_from=333.337.search-card.all.click&vd_source=3c7831e560a6c8660ad9e4ed74fc8980
该项目是经典的后台管理系统
前后端分别使用vue和springboot
首先我先进行演示一下
二、业务实现逻辑
注册模块:用户通过输入用户名,邮箱,邮箱验证码,密码进行注册,后端接收到用户传入的数据,首先对数据进行校验,验证用户输入的邮箱验证码是否与数据库中的匹配,如果匹配就通过验证,将用户的信息存储到数据库中,其中密码通过md5加密算法进行算法加密,前端返回用户登录成功。
登录模块:用户输入邮箱和密码后进行登录,后端开始对数据进行验证,首先验证邮箱输入是否正确,如果错误返回给用户提示信息,否则登录成功,当用户登录成功后,后端会生成相应的token,并储存在redis中一份,用户在访问其他页面时,必须携带token,否则拦截器进行拦截。
忘记密码模块:用户通过输入邮箱,获取验证码后进行校验比对,如果用户输入的验证码正确,就通过下一步,用户输入新密码,确认新密码后,密码重置成功。
文章管理模块:用户登录成功后,会跳转后文章管理的页面,如果用户在数据库中有数据,则前端向后端发送请求获取文章数据,同时获取用户数据的数量,对其进行分页,用户可以进行条件查询,后端使用动态sql语句进行查询并返回结果,用户点击添加文章,前端通过监听点击事件,弹出抽屉,用户可以输入文章标题,文章分类通过调用后台获取用户的分类数量并显示,用户输入文章封面时,获取图片的url地址,图片会上传到阿里云服务器,数据库存储的仅仅只是数据库的路径,当用户点击编辑按钮时,也会通过监听事件弹出抽屉,并且获取点击按钮的数据并展示,当用户修改了数据,会调用后台的接口进行修改,当用户删除时会进行提示,当用户点击确定后,后台会将该文章从该数据库中删除。
文章分类模块:用户点击分类模块时,前端监听到事件后会调用后台接口进行数据查询,并且返回给前端,前端进行渲染,用户点击添加分类时会弹出表单框,用户输入数据后,前端会把数据返回到后端,后端进行添加,用户点击编辑分类时,通过会弹出表单框,表单框中存有数据,用户可以修改数据然后提交,后端接收到用户修改的数据进行修改,当用户点击删除时,前端监听到事件后会提示用户是否删除,如果用户点击确定,前端调用后端接口进行删除。
基本资料模块:用户点击基本资料,调用后台接口查询数据库中的用户信息,渲染在前端页面上,当用户点击操作按钮时,会弹出一个表单框,用户数据渲染在表单上,用户可以修改用户信息,但是用户名和邮箱是不可修改的,用户无权限修改。
上传头像模块:用户点击选择图片,选择本地图片,然后调用后端接口显示在页面上,此时图片储存到了阿里云服务器上,数据库仅仅存放图片在阿里云服务器url地址,当用户点击上传,后端接口接收到指令后,向阿里云服务器发送请求,获取图片,并且将图片返给前端,前端将图片渲染在页面上。
重置密码模块:用户在表单中输入旧密码,新密码和确认新密码后,点击提交,前端将用户数据发送给后端,后端接收到请求后首先判断用户输入的旧密码是否正确,如果错误,返回给前端错误信息,前端将错误信息渲染后返给用户,如果正确,则验证输入的新密码和确认后的新密码是否一致,如果一致,则调用接口修改数据库中的密码,并将md5加密后的密码存储在数据库中。
三、代码实现
由于项目过大,本次我仅仅演示 登录注册模块的前后端交互模块
后端
在后端接口中,我们创建业务的模块分为四部分,controller层,mapper层,service层和pojo层
controller层
@PostMapping("/register")
public Result register(@RequestBody UserRegistrationDTO userRegistrationDTO) {
//获取前端传过来的参数
String username = userRegistrationDTO.getUsername();
String password = userRegistrationDTO.getPassword();
String email = userRegistrationDTO.getEmail();
String captcha = userRegistrationDTO.getCaptcha();
if (username == null && password == null && email == null && captcha == null){
return Result.error("请求参数不能为空!");
}
// 查找用户名是否已被占用
User user = userService.findByUserName(username);
if (user != null) {
return Result.error("用户名已被占用!");
}
//查找邮箱是否被占用
User find_email = userService.findByEmail(email);
if (find_email != null){
captchaMapper.deleteByEmail(email);
return Result.error("邮箱已经被占用!");
}
// 验证邮箱和验证码是否匹配
try {
capthcaService.vaildCaptcha(email, captcha);
} catch (IllegalArgumentException e) {
return Result.error("验证码错误!");
}
// 注册用户
try {
userService.register(username, password, email, captcha);
return Result.success();
} catch (Exception e) {
return Result.error("注册失败:" + e.getMessage());
}
}
@PostMapping("/login")
public Result<String> login(@RequestBody User user){
String email = user.getEmail();
String password = user.getPassword();
// System.out.println(email);
// System.out.println(password);
//查找是否存在用户邮箱
User find_email = userService.findByEmail(email);
if (find_email == null){
return Result.error("邮箱不存在!");
}
//加密后的密码
String passwordByEmail = userService.findPasswordByEmail(email);
// System.out.println("数据库中加密后的密码:"+passwordByEmail);
String inputUserPassword = Md5Util.getMD5String(password);
// System.out.println("用户输入的加密后的密码:"+inputUserPassword);
if (passwordByEmail.equals(inputUserPassword)){
//登录成功
userService.login(email,password);
//获取数据库中的用户名和id
String usernameByEmail = userService.findUsernameByEmail(email);
Integer idByEmail = userService.findIdByEmail(email);
//jwt令牌验证
Map<String,Object> claims = new HashMap<>();
claims.put("id",idByEmail);
claims.put("username",usernameByEmail);
String token = JwtUtil.genToken(claims);
// System.out.println(token);
//把token存储到redis中
ValueOperations<String, String> operations = stringRedisTemplate.opsForValue();
//将token设置为键值对,token即为键,又为值,并且设置redis中token过期时间为12天
operations.set(token,token,12, TimeUnit.DAYS);
return Result.success(token);
}else {
return Result.error("密码错误!");
}
}
mapper层
@Mapper
public interface UserMapper {
//根据用户名查询
@Select("select * from user where username = #{username}")
User findByUserName(String username);
@Insert("insert into user(username,password,email,create_time,update_time) " +
"values(#{username},#{encryptedPassword},#{email},now(),now())")
void register(User user);
//查询数据库中的邮箱
@Select("select * from user where email = #{email}")
User findByEmail(String email);
//查询数据库中加密后的密码
@Select("SELECT password FROM user WHERE email =#{email}")
String findPasswordByEmail(String email);
//根据邮箱查询数据库中的用户名
@Select("SELECT username FROM user WHERE email =#{email}")
String findUsernameByEmail(String email);
//根据邮箱查询数据库中的id
@Select("SELECT id FROM user WHERE email =#{email}")
Integer findIdByEmail(String email);
}
service层
@Override
public void register(String username, String password, String email, String captcha) {
// 创建用户对象
User user = new User();
user.setUsername(username);
String encryptedPassword = Md5Util.getMD5String(password);
user.setEncryptedPassword(encryptedPassword);
user.setEmail(email);
// 将用户信息添加到数据库
userMapper.register(user);
}
@Override
public void login(String email, String password) {
// 根据用户名查询用户信息
User email_login = userMapper.findByEmail(email);
if (email_login == null) {
throw new IllegalArgumentException("邮箱不存在!");
}
// 验证密码是否匹配
String encryptedPassword = Md5Util.getMD5String(password);
if (!email_login.getPassword().equals(encryptedPassword)) {
throw new IllegalArgumentException("密码已经错误!");
}
}
pojo层
//lombok 在编译阶段自动生成getter setter toString等方法
@Data
public class User {
private Integer id;//主键ID
private String username;//用户名
// @JsonIgnore //让springmvc把当前对象转换成json格式的时候忽略
private String password;//密码
private String encryptedPassword;//加密后密码
private String nickname;//昵称
private String email;//邮箱
private String userPic;//用户头像地址
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime createTime;//创建时间
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime updateTime;//更新时间
private String age; //年龄
private String phoneNumber; //手机号
private String gender; //性别
private String work; //职业
private String hobby; //兴趣爱好
private String area; //地区
private String personSignature; //个性签名
}
前端
登录注册在一个vue文件中,两个表单通过数据模型的变化而变化
<script setup>
import {User, Lock, CreditCard, Right} from '@element-plus/icons-vue'
import {ref} from 'vue'
// 导入 axios
import axios from 'axios';
import {getCaptcha,startCountdown} from './api/register.js'
import {ElMessage} from "element-plus";
//控制注册与登录表单得显示, 默认显示注册
const isRegister = ref(false)
//定义用于注册的数据模型
const registerData = ref({
username:'',
email:'',
captcha:'',
password:'',
repassword:''
});
const loginData = ref({
email:'',
password:''
})
//自定义确认密码的校验函数
const rePasswordValid = (rule, value, callback) => {
if (value == null || value === '') {
return callback(new Error('请再次确认密码'))
}else if(value !== registerData.value.password) {
return callback(new Error('两次输入密码不一致'))
}else {
return callback()
}
}
//定义表单校验规则
const rules = {
username:[
{required:true,message:"请输入用户名",trigger:"blur"},
{max:16,min:5,message: "长度为5到16位非空字符",trigger: "blur"}
],
email:[
{required:true,message:"请输入邮箱",trigger:"blur"}
],
captcha:[
{required:true,message:"请输入验证码",trigger:"blur"}
],
password:[
{required:true,message:"请输入密码",trigger:"blur"},
{max:16,min:5,message: "长度为5到16位非空字符",trigger: "blur"}
],
repassword:[
{validator:rePasswordValid,trigger:"blur"}
]
}
const email = ref('');
const disabled = ref(false);
const countdown = ref(60);
const buttonText = ref('获取验证码');
function handleGetVerificationCode() {
// 检查邮箱是否为空
if (!registerData.value.email) {
console.error("邮箱不能为空!");
return;
}
// 发送请求给后端获取验证码
getCaptcha(registerData.value.email, axios)
.then(response => {
ElMessage.success("验证码发送成功!")
// 验证码发送成功,开始倒计时
disabled.value = true;
startCountdown({ disabled, countdown, buttonText });
})
.catch(error => {
console.error(error);
// 处理错误情况
});
}
// 发送注册请求给后端
async function register(){
// 从 registerData 中获取用户输入的数据
const { username, email, captcha, password } = registerData.value;
// 发送注册请求给后端
try {
const response = await axios.post('/api/user/register', {
username,
password,
email,
captcha
}, {
headers: {
'Content-Type': 'application/json'
}
});
// 根据后端返回的响应进行处理
if (response.data.code === 0) {
ElMessage.success("注册成功!")
} else {
console.error('注册失败:', response.data.message);
// 在界面上显示错误提示信息或者执行其他必要的操作
ElMessage.error(response.data.message?response.data.message:"注册失败!")
}
} catch (error) {
// 处理注册失败的情况
console.error('验证注册失败:', error.message);
}
}
import {useTokenStore} from "@/stores/token.js";
import {useRouter} from 'vue-router'
const router = useRouter()
const tokenStore = useTokenStore()
//发送登录请求给后端
async function login(){
// 从 loginData 中获取用户输入的数据
const {email,password} =loginData.value;
//给后端发送登录请求
try {
const response = await axios.post('/api/user/login', {
email,
password
}, {
headers: {
'Content-Type': 'application/json',
}
});
if (response.data.code === 0){
console.log('登录成功:', response.data);
ElMessage.success("登录成功!")
//将得到的token放入pinia中
tokenStore.setToken(response.data.data,12 * 24 * 60 * 60)//设置12天有效期
//登录成功后跳转到主页面
router.push('/layoutMain')
}else {
// 在界面上显示错误提示信息或者执行其他必要的操作
ElMessage.error(response.data.message?response.data.message:"登录失败!")
}
}catch (error){
console.log("登录失败:"+error.message)
}
}
//忘记密码跳转页面
const forgetPwd = ()=>{
router.push('/forgetPwd')
}
</script>
<template>
<el-row class="login-page">
<el-col :span="12" class="bg"></el-col>
<el-col :span="6" :offset="3" class="form">
<!-- 注册表单 -->
<template v-if="isRegister">
<el-form ref="form" size="large" autocomplete="off" :rules="rules" :model="registerData">
<el-form-item>
<h1>注册</h1>
</el-form-item>
<el-form-item prop="username">
<el-input :prefix-icon="User" placeholder="请输入用户名" v-model="registerData.username"></el-input>
</el-form-item>
<el-form-item prop="email">
<el-input :prefix-icon="CreditCard" placeholder="请输入邮箱" v-model="registerData.email"></el-input>
<el-button type="primary" :disabled="disabled" @click="handleGetVerificationCode">{{buttonText}}</el-button>
</el-form-item>
<el-form-item prop="captcha">
<el-input :prefix-icon="Right" placeholder="请输入验证码" v-model="registerData.captcha"></el-input>
</el-form-item>
<el-form-item prop="password">
<el-input :prefix-icon="Lock" type="password" placeholder="请输入密码" v-model="registerData.password"></el-input>
</el-form-item>
<el-form-item prop="repassword">
<el-input :prefix-icon="Lock" type="password" placeholder="请输入再次密码" v-model="registerData.repassword"></el-input>
</el-form-item>
<!-- 注册按钮 -->
<el-form-item>
<el-button class="button" type="primary" auto-insert-space @click="register">
注册
</el-button>
</el-form-item>
<el-form-item class="flex">
<el-link type="info" :underline="false" @click="isRegister = false">
← 返回
</el-link>
</el-form-item>
</el-form>
</template>
<!-- 登录表单 -->
<template v-else>
<el-form ref="form" size="large" autocomplete="off" :rules="rules" :model="loginData">
<el-form-item>
<h1>登录</h1>
</el-form-item>
<el-form-item prop="email">
<el-input :prefix-icon="User" placeholder="请输入邮箱" v-model="loginData.email"></el-input>
</el-form-item>
<el-form-item prop="password">
<el-input name="password" :prefix-icon="Lock" type="password" placeholder="请输入密码" v-model="loginData.password"></el-input>
</el-form-item>
<el-form-item class="flex">
<div class="flex">
<el-checkbox>记住我</el-checkbox>
<el-link type="primary" :underline="false" @click="forgetPwd">忘记密码?</el-link>
</div>
</el-form-item>
<!-- 登录按钮 -->
<el-form-item>
<el-button class="button" type="primary" auto-insert-space @click="login">登录</el-button>
</el-form-item>
<el-form-item class="flex">
<el-link type="info" :underline="false" @click="isRegister = true">
注册 →
</el-link>
</el-form-item>
</el-form>
</template>
</el-col>
</el-row>
</template>
<style lang="scss" scoped>
/* 样式 */
.login-page {
height: 100vh;
background-color: #fff;
.bg {
background: url('@/assets/logo.png') no-repeat 60% center / 240px auto,
url('@/assets/login_bg.jpg') no-repeat center / cover;
border-radius: 0 20px 20px 0;
}
.form {
display: flex;
flex-direction: column;
justify-content: center;
user-select: none;
.title {
margin: 0 auto;
}
.button {
width: 100%;
}
.flex {
width: 100%;
display: flex;
justify-content: space-between;
}
/* 将输入框和按钮的宽度设为自适应 */
.el-input {
flex: 2; /* 较长 */
}
.el-button {
flex: 1; /* 较短 */
}
/* 调整输入框和按钮之间的间距 */
.el-input + .el-button {
margin-left: 10px;
}
}
}
</style>
浏览器跨域问题
在前后端分离的项目中还存在浏览器跨域问题,我们通过配置代理的方法,让前端向后端发送请求,不直接通过浏览器发送
import { fileURLToPath, URL } from 'node:url'
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [
vue(),
],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url))
}
},
//为后端服务器配置代理
server:{
proxy:{
'/api':{
target:'http://localhost:8080', //后端服务器地址
changeOrigin:true,
rewrite:(path)=>path.replace(/^\/api/,'') //api替换为控制符串
}
}
}
})
本项目的源码后续更新......尽请期待.......