注意:因为是学习,所以在没有发现问题前,可能会存在错误,请仔细甄别。
- 前端:Vue3+Vite+Pinia+ElementPlus+axios
- 后端:SpringBoot+MyBatis
- 参考信息如下
- 【黑马程序员SpringBoot3+Vue3全套视频教程,springboot+vue企业级全栈开发从基础、实战到面试一套通关】
- https://www.bilibili.com/video/BV1m84y1w7Tb/?p=159
- 鱼皮的编程宝典 | 鱼皮的编程宝典 (codefather.cn)
待补充
使用到的库
lombok库
Lombok Java类库,使用注解的形式自动生成构造器、getter、setter、equals、hashcode、toString等方法
https://mvnrepository.com/artifact/org.projectlombok/lombok
https://www.bilibili.com/video/BV1m84y1w7Tb?p=121
项目文档
请求参数
- 请求参数格式:x-www-form-urlencoded
- 请求参数格式:queryString
- 请求参数格式(file):multipart/form-data
- SpringBoot后端接口请求参数映射方式详解
响应参数
- 响应数据类型:application/json
JavaWeb(Spring Boot)
controller
@RequestParam(required = false) 表示参数不是必须的
@PathVariable
@RequestBody
@RequestHeader
service
serviceImpl
mapper
登录认证
会话技术:
会话:用户打开浏览器,访问web服务器的资源,会话建立,知道有一方断开连接,会话结束。在一次会话中可以包含多次请求和响应。
会话跟踪:一种维护浏览状态的方法,服务器需要识别多次请求是否来自于同一浏览器,以便在同一次会话的多次请求间共享数据。
会话跟踪方案:客户端会话跟踪技术:Cookie,服务端会话跟踪技术:Session,令牌技术
拦截器(Filter)
使用implements声明多个接口,包含初始化(一次),放行(多次),销毁(一次)。
过滤器(Interceptor)
Spring Boot项目部署
属性配置方式
改变运行端口:
1. 修改 application.properties 或 application.yml 中的 server.port.端口号 (打包后麻烦,且交给客户后更加麻烦)
2. 使用 --键=值 的方式 如:java -jar 文件名.jar --server.port =9999 这样就可以将运行端口改为 9999
3. 修改环境变量: 在用户变量下,新建一个变量名为 server.port ,变量值为 9999 (务必注意,修改完环境变量后,记得重新打开窗口)
4. 外部文件配置方式:在jar包所在目录下,新建一个 application.yml ,将需要改变的配置写入即可。
配置优先级:项目中 resources 目录下的 application.yml < Jar包所在目录的 application.yml < 操作系统环境变量 < 命令行参数
多环境开发-Pofiles
# 如何分隔不同环境配置?
---
# 如何指定哪个环境的配置文件生效?
spring:
profiles:
active: 环境名称
# 如何指定哪些配置属于哪个环境?
spring:
config:
activate:
on-profile: 环境名称
---
# 通用配置,指定激活的配置文件
spring:
profiles:
active: dev # 指定激活 dev 环境
# 项目访问路径(虚拟路径)
server:
servlet:
context-path: /aaa
---
# 开发环境配置
spring:
config:
activate:
on-profile: dev
server:
port: 8081
---
# 测试环境配置
spring:
config:
activate:
on-profile: test
server:
port: 8082
---
# 生产环境配置
spring:
config:
activate:
on-profile: pro
server:
port: 8083
很显然,文件内容太多,看的眼花缭乱。因此还有一种方法。
我们可以分别为环境设置对应的yml文件如:application-dev.yml 、 application-test.yml 、 application-pro.yml ,只需要在 application.yml 中使用 spring.profiles.active.dev 即可激活
根据上面的方法,我们可以类推出,把配置信息配置到不同的配置文件中如:application-devServer.yml 、 application-devDB.yml 等等。
如何激活:spring.profiles.active:dev.group."dev":devServer,devDB
吐槽:非大型项目已选第一个就行,这配置方法多种多样,去学Vue了。。
Vue
跨域(CORS):指由于浏览器同源策略限制,向不同源(不同协议、不同域名、不同端口)发送ajax请求会失败
更改配置文件
# request.js
import axios from 'axios'
const baseURL = '/api'
const instance = axios.create({baseURL})
//以下略
# vite.config.js
server: {
proxy: {
'/api': {
target: 'http://localhost:8080',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, '')
}
}
}
如果访问 http://localhost:5173/api 则会改为 http://localhost:8080/
Vue Router
npm install vue-router@4
导航守卫
Pinia
*请在此处访问开始 | Pinia (vuejs.org)和快速开始 | pinia-plugin-persistedstate (prazdevs.github.io)来进行安装,这里不再赘述。
在stores文件夹下新建index.js文件和modules文件夹。将pinia进行独立维护。
// index.js
import { createPinia } from 'pinia'
// 引入pinia插件 用于数据持久化
import { createPersistedState } from 'pinia-plugin-persistedstate'
const pinia = createPinia();
const persist = createPersistedState();
pinia.use(persist)
export default pinia
// 导出所有的store
export * from './modules/(文件名)'
使用
// 其他地方直接调用 例:import {useUserStore} from '@/stores'
挑重点,可恶,看的头大,以下根据黑马程序员的大事件案例进行梳理
前端使用vue、vite、element-plus、unplugin-vue-components、unplugin-auto-import、pinia、pinia-plugin-persistedstate、vue-router、axios、eslint、prettier、sass。这里默认你已经安装好。
// 这里我们在src目录下创建一个stores/modules文件夹,里面再创建一个token.js文件,让pinia来存储
// token.js
import { ref } from 'vue'
import { defineStore } from 'pinia'
/*
第一个参数:名字,唯一性
第二个参数:函数,函数的内部可以定义状态的所有内容
返回值: 函数
*/
export const useTokenStore = defineStore('token', () => {
// 定义状态
// 1.响应式变量
const token = ref('')
// 2.定义一个函数,修改token的值
const setToken = (newToken) => {
token.value = newToken
}
// 3.定义一个函数,清除token的值
const removeToken = () => {
token.value = ''
}
return { token, setToken, removeToken }
},{
persist: true, // 是否持久化
})
其次是请求,我们src目录下创建一个utils文件夹,再创建一个request.js文件
import axios from 'axios'
import { ElMessage } from 'element-plus'
// 公共地址
const baseURL = 'http://localhost:8080'
const instance = axios.create({baseURL})
// 导入用户token,便于存储
import {useTokenStore} from '@/stores/token.js'
// 请求拦截器
instance.interceptors.request.use(
(config)=>{
// 请求前回调
const tokenStore = useTokenStore();
// 尝试将token添加到请求头
// 如果token存在,则添加到请求头
if(tokenStore.token){
config.headers.Authorization = tokenStore.token
}
return config
},
(err)=>{
// 将异步的状态改为失败的状态
return Promise.reject(err)
}
)
// 导入路由便于跳转
import router from '@/router'
// 响应拦截器
instance.interceptors.response.use(
(result)=>{
// 响应成功回调
if(result.data.code === 0){
// 将异步的状态改为成功的状态
return result.data
}
// 其他状态码
ElMessage.error(result.data.msg ? result.data.msg : '服务器错误,请通知管理员')
// 将异步的状态改为失败的状态
return Promise.reject(result.data)
},
(err)=>{
// 响应错误回调
// 1.获取错误信息
const {response} = err
// 2.判断是否有响应
if(response){
// 3.判断状态码
switch(response.status){
case 401:
// 4.提示用户
ElMessage.error('请先登录')
// 5.跳转到登录页
router.push('/login')
break
case 403:
// 6.提示用户
ElMessage.error('没有权限')
break
case 404:
// 7.提示用户
ElMessage.error('请求地址错误')
break
default:
// 8.提示用户
ElMessage.error('服务器错误,请通知管理员')
break
}
}else{
// 9.提示用户
ElMessage.error('网络错误')
}
// 将异步的状态改为失败的状态
return Promise.reject(err)
}
)
export default instance;
其次是api,我们src目录下创建一个api文件夹,再创建一个category.js文件
/**
* 分类模块接口列表
*/
import request from '@/utils/request'
// 获取分类列表
export const categoryListService = () =>{
return request.get('/category')
}
// 新增分类
export const categoryAddService = (categoryData) => {
return request.post('/category',categoryData)
}
// 删除分类
export const categoryDeleteService = (id) => {
return request.delete('/category?id='+id)
}
// 更新分类
export const categoryUpdateService = (categoryData) => {
return request.put('/category',categoryData)
}
// 获取分类详情
export const categoryDetailService = (id) => {
return request.get('/category/detail?id='+id)
}
封装组件
把对应的组件放到单独的vue文件里,暴露数据以供调试。
打包后的问题
nginx配置
// 基本用法,其他用法暂时省略
server{
listen 8081;
server_name localhost;
location / {
root html/dist2;
index index.html index.htm;
try_files $uri $uri/ /index.html;
}
}
数据库设计
用户(user)
- id(用户ID)
- user_name(用户名)
- nick_name(别名)
- sex(性别)
- password(密码)
- email(邮箱)
- user_pic(用户头像)
- status(状态)
- create_time(创建时间)
- update_time(最近更新时间)
用户权限(user_role)
- user_id(用户ID)
- role_id(权限ID)
游戏角色(game_user)
- id(角色ID)
- frist_name(性)
- last_name(名)
- nick_name(别名)
- sex(性别)
- age(年龄)
- biography(兴趣爱好)
- update_id(更新用户的id)
- create_time(创建时间)
- update_time(最近更新时间)
文章(article)
- id(文章ID)
- title(标题)
- title_name(副标题)
- content(内容)
- cover(封面)
- banner(头图)
- status(状态 发布|草稿)
- is_top(是否置顶)
- view_count(阅读量)
- category_id(类型ID)
- create_user_id(创建该分类的用户ID)
- create_time(创建时间)
- update_time(最近更新时间)
文章分类(article_tag)
- article_id
- tag_id
分类(tag)
文章类型(category)
- id(类型ID)
- name(名称)
- description(简述)
- status(状态)
- create_user_id(创建该分类的用户ID)
- create_time(创建时间)
- update_time(最近更新时间)
制作商(publisher)
- id
- publisher
发行商(franchise)
- id
- franchise
权限明细(role)
- id
- name(权限名)
- role_key(角色权限字符串)
- status(角色状态 正常0|停用1)
- create_time(创建时间)
- update_time(最近更新时间)
使用以下代码时,请务必已经观看完此前集数的所有视频
接口文档
将会使用Apifox编写,TODO
第15集、注册接口
路径、文件、整体如下:
......
java
|--com.example.galhand_spring
|--|--common
|--|--|--Result.java
|--|--controller
|--|--|--ShopsGameAdminController.java
|--|--dao
|--|--entity
|--|--|--ShopsGameAdmin.java
|--|--|--ShopsGameArticle.java
|--|--|--ShopsGameCategory.java
|--|--exception
|--|--mapper
|--|--|--ShopsGameAdminMapper.java
|--|--service
|--|--|--impl
|--|--|--|--ShopsGameAdminServiceImpl.java
|--|--|--ShopsGameAdminService.java
|--|--utils
|--|--|--Md5Utiles.java
|--GalHandSpringApplication
resources
|--static
|--|--......
|--application.yml
Result.java
package com.example.galhand_spring.common;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
// 无参数的构造方法
// 全参的构造方法
@NoArgsConstructor
@AllArgsConstructor
@Data
public class Result<T> {
private Integer code; // 业务状态码 0-成功 1-失败
private String message; // 提示信息
private T data; // 响应数据
// 快速返回操作成功响应结果(带响应数据)
public static <E> Result<E> success(E data) {return new Result<>(0, "操作成功", data);}
// 快速返回操作成功响应结果
public static Result success() {return new Result(0, "操作成功", null);}
public static Result error(String message) {return new Result(1, message, null);}
}
ShopsGameAdminController.java
package com.example.galhand_spring.controller;
import com.example.galhand_spring.common.Result;
import com.example.galhand_spring.entity.ShopsGameAdmin;
import com.example.galhand_spring.service.ShopsGameAdminService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/user")
public class ShopsGameAdminController {
@Autowired
private ShopsGameAdminService shopsGameAdminService;
@PostMapping("/register")
public Result register(String username,String password){
// 查询用户
ShopsGameAdmin u = shopsGameAdminService.findByUserName(username);
if (u==null){
// 没有占用
shopsGameAdminService.register(username,password);
return Result.success();
}else {
// 占用
return Result.error("用户名已被占用");
}
}
}
ShopsGameAdmin.java
package com.example.galhand_spring.entity;
import java.time.LocalDateTime;
import lombok.Data;
// 使用lombok 代替get set方法
// pom.xml中引入lombok依赖
@Data
public class ShopsGameAdmin {
private Integer id; // 后台用户ID
private String username; // 后台用户名
private String password; // 后台用户密码
private String nickname; // 后台用户昵称/别名
private String email; // 后台用户邮箱
private String userPic; // 后台用户头像
private LocalDateTime createTime; // 创建时间
private LocalDateTime updateTime; // 更新时间
// private String role; // 角色权限
}
ShopsGameArticle.java
package com.example.galhand_spring.entity;
import lombok.Data;
import java.time.LocalDateTime;
// 使用lombok 代替get set方法
// pom.xml中引入lombok依赖
@Data
public class ShopsGameArticle {
private Integer id; // 文章ID
private String title; // 文章标题
private String titleName; //文章别名
private String content; // 文那日容
private String coverImg; // 封面图像
private String state; // 状态
private Integer categoryId; // 文章分类
private Integer createUser; // 创建人ID
private LocalDateTime createTime; // 创建时间
private LocalDateTime updateTime; // 更新时间
}
ShopsGameCategory.java
package com.example.galhand_spring.entity;
import lombok.Data;
import java.time.LocalDateTime;
// 使用lombok 代替get set方法
// pom.xml中引入lombok依赖
@Data
public class ShopsGameCategory {
private Integer id; // 分类主键ID
private String categoryName; // 分类名称
private String categoryAlias; // 分类别名
private Integer createUser; // 创建人ID
private LocalDateTime createTime; // 创建时间
private LocalDateTime updateTime; // 更新时间
}
ShopsGameAdminMapper.java
package com.example.galhand_spring.mapper;
import com.example.galhand_spring.entity.ShopsGameAdmin;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;
@Mapper
public interface ShopsGameAdminMapper {
//根据用户名查询用户
@Select("select * from shops_game_admin where username=#{username}")
ShopsGameAdmin findByAdminName(String username);
//添加
// 注意这里时对应数据字段的
@Insert("insert into shops_game_admin(username,password,createTime,updateTime)" +
"values(#{username},#{password},now(),now())")
// 敬请注意!!!自动生成的代码一定要再看一遍,防止他生成错了
void add(String username, String password);
}
ShopsGameAdminService.java
package com.example.galhand_spring.service;
import com.example.galhand_spring.entity.ShopsGameAdmin;
public interface ShopsGameAdminService {
// 根据用户名查询用户
ShopsGameAdmin findByUserName(String username);
// 注册
void register(String username, String password);
}
ShopsGameAdminServiceImpl.java
package com.example.galhand_spring.service.impl;
import com.example.galhand_spring.entity.ShopsGameAdmin;
import com.example.galhand_spring.mapper.ShopsGameAdminMapper;
import com.example.galhand_spring.service.ShopsGameAdminService;
import com.example.galhand_spring.utils.Md5Util;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class ShopsGameAdminServiceImpl implements ShopsGameAdminService {
@Autowired
private ShopsGameAdminMapper shopsGameAdminMapper;
@Override
public ShopsGameAdmin findByUserName(String username) {
ShopsGameAdmin u = shopsGameAdminMapper.findByAdminName(username);
return u;
}
@Override
public void register(String username, String password) {
// 加密
String md5String = Md5Util.getMD5String(password);
// 调用 Mapper 添加
shopsGameAdminMapper.add(username,md5String);
}
}
Md5Util.java
package com.example.galhand_spring.utils;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
public class Md5Util {
/**
* 默认的密码字符串组合,用来将字节转换成 16 进制表示的字符,apache校验下载的文件的正确性用的就是默认的这个组合
*/
protected static char hexDigits[] = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'};
protected static MessageDigest messagedigest = null;
static {
try {
messagedigest = MessageDigest.getInstance("MD5");
} catch (NoSuchAlgorithmException nsaex) {
System.err.println(Md5Util.class.getName() + "初始化失败,MessageDigest不支持MD5Util。");
nsaex.printStackTrace();
}
}
/**
* 生成字符串的md5校验值
*
* @param s
* @return
*/
public static String getMD5String(String s) {
return getMD5String(s.getBytes());
}
/**
* 判断字符串的md5校验码是否与一个已知的md5码相匹配
*
* @param password 要校验的字符串
* @param md5PwdStr 已知的md5校验码
* @return
*/
public static boolean checkPassword(String password, String md5PwdStr) {
String s = getMD5String(password);
return s.equals(md5PwdStr);
}
public static String getMD5String(byte[] bytes) {
messagedigest.update(bytes);
return bufferToHex(messagedigest.digest());
}
private static String bufferToHex(byte bytes[]) {
return bufferToHex(bytes, 0, bytes.length);
}
private static String bufferToHex(byte bytes[], int m, int n) {
StringBuffer stringbuffer = new StringBuffer(2 * n);
int k = m + n;
for (int l = m; l < k; l++) {
appendHexPair(bytes[l], stringbuffer);
}
return stringbuffer.toString();
}
private static void appendHexPair(byte bt, StringBuffer stringbuffer) {
char c0 = hexDigits[(bt & 0xf0) >> 4];// 取字节中高 4 位的数字转换, >>>
// 为逻辑右移,将符号位一起右移,此处未发现两种符号有何不同
char c1 = hexDigits[bt & 0xf];// 取字节中低 4 位的数字转换
stringbuffer.append(c0);
stringbuffer.append(c1);
}
}
关于这部分更加详细的内容请观看第15集