敲 my-project 时候写的一点东西

本文最后更新于:2024年5月1日 晚上

反正就是写点杂七杂八的东西所以无所谓标题了

项目来源:SpringBoot3+Vue3 前后端分离项目

后端部分

最重要的话写在最前面:一定一定一定不要忘记加注解 很多时候出错了先看看是不是自己忘记加注解了

如何和数据库进行联动(MyBatis-Plus)

首先,需要创建好对应的 DTO (数据传输对象)类:

1
2
3
4
5
6
7
8
9
10
11
12
@Data
@TableName("db_account")
@AllArgsConstructor
public class AccountDTO {
@TableId(type = IdType.AUTO)
Integer id;
String username;
String password;
String email;
String role;
Date registerTime;
}

然后,创建对应的 Mapper (映射)类:

1
2
@Mapper
public interface IAccountMapper extends BaseMapper<AccountDTO> { }

到这里其实就完成了,接下来简单说下如何在 Service 层使用:
首先,定义好对应的实体类的 Service 接口,注意一定要extends IService<T>

1
2
3
4
public interface IAccountService extends IService<AccountDTO>, UserDetailsService {
// methods
AccountDTO findAccountByNameOrEmail(String text);
}

然后,创建对应的 Service 实现类,注意一定要extends ServiceImpl<M, T>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Service
public class AccountServiceImpl extends ServiceImpl<IAccountMapper, AccountDTO> implements IAccountService {

/**
* 从数据库中查询用户信息
* @param text
* @return
*/
@Override
public AccountDTO findAccountByNameOrEmail(String text) {
return this.query()
.eq("username", text).or()
.eq("eamil", text)
.one();
}
}

解决 Invalid value type for attribute ‘factoryBeanObjectType’ 报错

其实这个问题是因为mybatis-plus的包更新得没这么快导致的,里边包含得mybatis-spring的版本落后了,需要手动导入更高版本的mybatis-spring

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.4.1</version>
<exclusions>
<exclusion>
<groupId>org.mybatis</groupId>
<artifactId>mybatis-spring</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- 解决 Invalid value type for attribute 'factoryBeanObjectType' -->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis-spring</artifactId>
<version>3.0.3</version>
</dependency>

解决 SpringSecurity 没有加密器报错

要在配置文件中创建一个加密器:

1
2
3
4
5
6
7
@Configuration
public class WebConfiguration {
@Bean
BCryptPasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
}

对象转换小工具(DTO -> VO)

第一种办法,用 Spring 自带的 BeanUtils.copyProperties(source, target);,即可快速完成对应属性复制。
第二种方法,自己实现一个转换工具,哪个类要用直接继承就行了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
public interface BaseData {

default <V> V asViewObject(Class<V> vClass, Consumer<V> consumer){
V v = this.asViewObject(vClass);
consumer.accept(v);
return v;
}

default <V> V asViewObject(Class<V> vClass){
try{
Field[] declaredFields = vClass.getDeclaredFields();
Constructor<V> constructor = vClass.getConstructor();
V v = constructor.newInstance();
for (Field declaredField : declaredFields){
convert(declaredField, v);
}
return v;
} catch (ReflectiveOperationException exception){
throw new RuntimeException(exception.getMessage());
}
}

private void convert(Field field, Object vo){
try{
Field sourceField = this.getClass().getDeclaredField(field.getName());
field.setAccessible(true);
sourceField.setAccessible(true);
field.set(vo, sourceField.get(this));
} catch (IllegalAccessException | NoSuchFieldException ignored){

}
}
}

解决跨域问题

SpringSecurity 的优先级默认是 -100
Spring 貌似自带处理跨域请求的写法,全部放行。不过我们也可以手动写一个 Filter 去处理跨域请求。(写了不行,自己写的 CorsFilter 和 SpringSecurity 的起冲突了(这里是后期,我怀疑是我当时没写@Component注解),直接配置 SpringSecurity 自带的吧…)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Component
@Order(Ordered.HIGHEST_PRECEDENCE)
public class CorsFilter extends HttpFilter {
@Override
protected void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
this.addCorsHeader(request, response);
chain.doFilter(request, response);
}

private void addCorsHeader(HttpServletRequest request, HttpServletResponse response){
response.addHeader("Access-Control-Allow-Origin", request.getHeader("Origin")); // 放行所有地址
// response.addHeader("Access-Control-Allow-Origin", request.getHeader("http://localhost:5173")); // 仅放行前端
response.addHeader("Access-Control-Allow-Methods", request.getHeader("GET, POST, PUT, DELETE, PATCH")); // 需要处理哪个REST请求就加哪个
response.addHeader("Access-Control-Allow-Headers", request.getHeader("Authorization, Content-Type"));
}
}

建议还是直接配置 SpringSecurity 自带的吧…

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Configuration
public class CorsConfig {
@Bean
public CorsFilter corsFilter() {
final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
final CorsConfiguration conf = new CorsConfiguration();
// conf.setAllowCredentials(true);
conf.addAllowedOrigin("*"); // 接受全部地址的跨域请求
// conf.addAllowedOrigin("http://localhost:5173"); // 只接受前端的跨域请求
conf.addAllowedMethod("*");
conf.addAllowedHeader("*");
source.registerCorsConfiguration("/**", conf);
return new CorsFilter(source);
}
}

RabbitMQ 的配置

作为消费者,记得在配置类里边加上反序列化代码,不然后台会反复获取但获取不到信息,报错(下面报错只截取了部分关键信息):

1
2
Caused by: java.lang.SecurityException: Attempt to deserialize unauthorized class java.util.CollSer; 
add allowed class name patterns to the message converter or, if you trust the message orginiator, set environment variable 'SPRING_AMQP_DESERIALIZATION_TRUST_ALL' or system property 'spring.amqp.deserialization.trust.all' to true

RabbitMQConfiguration中设置反序列化:

1
2
3
4
@Bean
public MessageConverter jsonMessageConverter(ObjectMapper objectMapper) {
return new Jackson2JsonMessageConverter(objectMapper);
}

Controller 同一传参

GET 请求就用 @RequestParam,POST 请求就用 @RequestBody

使用 @Resource 和 @Autowired 注解要注意的地方

建议使用@Resource注入到接口。以及请在 SpringBoot 启动完成后再调用(即在方法中调用),否则会抛异常。

一些炫酷写法

写进 Controller 里边的一些难以理解的写法

1
2
3
4
5
6
7
8
9
10
11
12
13
@PostMapping("/reset/confirm")
public RestBean<Void> resetConfirm(@RequestBody @Valid ResetConfirmVo vo){
return this.msgHandler(vo, service::resetCodeConfirm);
}

private RestBean<Void> msgHandler(Supplier<String> action) {
String msg = action.get();
return msg == null ? RestBean.success() : RestBean.failure(400, msg);
}

private <T> RestBean<Void> masHandler(T vo, Function<T, String> function){
return msgHandler(() -> function.apply(vo));
}

How to use MinIO in SpringBoot?

导入MAVEN依赖

pom.xml中添加以下依赖:

1
2
3
4
5
6
<!-- MinIO对象存储 -->
<dependency>
<groupId>io.minio</groupId>
<artifactId>minio</artifactId>
<version>8.3.9</version>
</dependency>

配置文件

以下是MinioConfiguration.java中的内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Slf4j
@Configuration
public class MinioConfiguration {
@Value("${spring.minio.endpoint}")
String endpoint;
@Value("${spring.minio.username}")
String username;
@Value("${spring.minio.password}")
String password;

@Bean
public MinioClient minioClient() {
log.info("MinIO 客户端启动中...");
return MinioClient.builder().endpoint(endpoint).credentials(username, password).build();
}
}

其中的endpoint的值注意一下,配置的是S3-API的值,不要配置成Console的值了,一般就是90909000两个端口,不知道是哪个就尝试一下,因为配置错了会报Non XML的错。

更改数据库表的结构,同步更新后端代码

这里直接把头像的url放在数据库的db_account表中去了,所以新增了一个avatar字段。既然数据库更新了,那后端的部分代码也需要更新,例如包含了Account相关的一些DTOVO或是Service等。

如何上传图片到MinIO?如何从MinIO中取出图片

说简单一点就是使用PutObjectArgsGetObjectArgs两个类,上传头像记得同步更新数据库。要详细一点就看代码,毕竟涉及到requestresponsestream

存储用户头像的url为什么要使用UUID

如果简单地使用用户ID加入到用户头像的url中,即使用户换了头像,url也不会更改,此时,若用户的浏览器中存在缓存,则无法正确更新头像,所以需要生成唯一的UUID。(还可以防爬虫)

记得放行相关接口

访问静态资源的话如果不想太麻烦就直接放行相关接口吧…配置SpringSecurity.requestMatchers("/api/image/avatar/**").permitAll()
正常来说的话是需要前端携带验证头的。

如何正确地从 Redis 中删除缓存

如果是指定的一条缓存,即一个key,那么直接template.delete就行。如果是带*的,其实也就是代指keys,那就需要用如下方法:

1
2
3
4
public void deleteCachePattern(String key) {
Set<String> keys = Optional.ofNullable(template.keys(key)).orElse(Collections.emptySet());
template.delete(keys);
}

如何使用 MyBatis-Plus 进行分页查询

配置文件

WebConfiguration.java中进行如下配置(因为这个项目用的是 Mysql 所以DbType选择MYSQL):

1
2
3
4
5
6
@Bean
public PaginationInnerInterceptor paginationInnerInterceptor() {
PaginationInnerInterceptor paginationInnerInterceptor = new PaginationInnerInterceptor(DbType.MYSQL);
paginationInnerInterceptor.setMaxLimit(100L);
return paginationInnerInterceptor;
}

上面其实我用着没问题,但是说下面才是正确配置:

1
2
3
4
5
6
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor(){
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new PaginationInnerInterceptor());
return interceptor;
}

使用示例及参考代码

1
2
3
4
5
6
7
Page<PostDTO> postPage = Page.of(page, 10);
if (tagId == 0) {
baseMapper.selectPage(postPage, Wrappers.<PostDTO>query().orderByDesc("post_time"));
} else {
baseMapper.selectPage(postPage, Wrappers.<PostDTO>query().eq("tag_id", tagId).orderByDesc("post_time"));
}
List<PostDTO> posts = postPage.getRecords();

前端部分

我是真觉得前端设计页面没什么好写的…加之我也不是很理解前端这些东西该怎么写…

vue-router 快速使用

终端输入命令安装:npm install vue-router
\router文件夹下创建index.js来配置vue-router:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import {createRouter, createWebHistory} from "vue-router";

const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [
{
path: '/',
name: 'welcome',
component: () => import('@/views/WelcomeView.vue'),
children: [
{
path: '',
name: 'welcome-login',
component: () => import('@/views/welcome/LoginPage.vue')
}
]
}
]
})

export default router // 将 router 暴露出去

main.js下使用路由:

1
2
3
4
5
6
7
8
9
import { createApp } from 'vue'
import App from './App.vue'
import router from "@/router";

const app = createApp(App)

app.use(router) // 使用路由

app.mount('#app')

App.vue下边使用router-view,此时在相应的 HTML 元素下边展示的就是WelcomeView.vue组件(页面)了:

1
2
3
4
5
<template>
<div>
<router-view/>
</div>
</template>

如果需要点击按钮跳转到某些界面或者刷新组件,就可以通过路由来改变:router.push(targetRoute)

如何发出异步请求

需要借助其他工具,一般就是用 axios,直接npm install axios。但是直接用不太方便,建议按需要封装。
记得在main.js设置 axios 发出请求的默认 URL:axios.defaults.baseURL = 'http://localhost:8080'
axios 默认发出请求格式是 json

自带的 JSON 转换工具

JSON 转 string,使用JSON.stringify函数:

1
2
const authObj = {token: token, expire: expire}
const str = JSON.stringify(authObj)

string 转 JSON,使用JSON.parse函数:

1
2
const str = localStorage.getItem(authKey) || sessionStorage.getItem(authKey)
const authObj = JSON.parse(str)

页面有一点白边,填充不完整,怎么会事?

直接来到最外层的index.html找到<style>标签并在其内部加入如下代码:

1
2
3
body {
margin: 0
}

按照Element PLus的文档的快速开始安装了按需导入的Element Plus,但是在某些情况下不生效?

找到index.html,插入:<link rel="stylesheet" href="//unpkg.com/element-plus/dist/index.css" />

暗黑模式适配

main.js中加入:import 'element-plus/theme-chalk/dark/css-vars.css'
安装包:npm install @vueuse/core
App.vue中加入代码:

1
2
3
4
5
6
7
8
9
10
useDark({
selector: 'html',
attribute: 'class',
valueDark: 'dark',
valueLight: 'light'
})

useDark({
onChanged(dark) { useToggle(dark) }
})

对应元素的style中的background-color属性的值设置为:var(--el-bg-color)

配置路由守卫

如果不配置的话,不需要登录,直接写对应的路由就能跳转到需要登录后才能使用的界面,又或者说,用户登录了,但是又可以通过路由回到登录界面:

1
2
3
4
5
6
7
8
9
10
11
12
router.beforeEach((to, from, next) => {
const login = isLogin()
if (to.name.startsWith('welcome-') && login){
// 已经登录,丢去主页
next('/index')
} else if (to.fullPath.startsWith('/index') && !login){
// 还未登录,送去登录
next('/')
} else {
next()
}
})

如何给路由加上渐入渐出动画?

<template>中加入以下代码(mode有两种,in-outout-in):

1
2
3
4
5
<router-view v-slot="{ Component }">
<transition name="fade" mode="out-in">
<component :is="Component" />
</transition>
</router-view>

<style>中加入以下代码

1
2
3
4
5
6
7
8
9
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.7s ease;
}

.fade-enter-from,
.fade-leave-to {
opacity: 0;
}

注:因为<transition>标签中给了一个name属性,所以 css 里边本来是.v-enter-active的,v变成了fade
注:element-plus 里边也带了些动画,可以看看

缩小浏览器窗口有些东西不见了怎么办

对应的标签里面的style里设置min-heightmin-width属性:<div style="width: 100vw;height: 100vh;overflow: hidden;display: flex;min-width: 480px;min-height: 640px">

ref 和 reactive 的区别?

ref适用于创建简单的基本类型数据的响应式引用,而reactive适用于创建复杂的对象或数组的响应式引用

前端布局怎么设计?

直接去看 Element-Plus 官方文档的“Container 布局容器”部分。

前端如何把从后端获取到的一些信息(如用户基本信息)存起来?

用 pinia 来存。先安装 pinia:npm install pinia
安装好后,配置 pinia。建议是创建一个store文件夹,在其下面创建index.js来配置。示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import {defineStore} from "pinia";

export const useStore = defineStore('general', {
state: () => {
return {
user: {
username: '',
email: '',
role: '',
registerTime: null
}
}
}
})

最外层跟App.vue在一起的main.js也需要配置:

1
2
3
import {createPinia} from "pinia";

app.use(createPinia())

最后,在需要使用store的地方通过useStore()使用就好了:

1
2
3
import {useStore} from "@/store";

const store = useStore()

卡片

可以在components里边放一些通用的组件,比如做一个通用的展示资料用的小卡片Cards.vue

菜单开启路由模式

<el-menu router :default-active="$route.path">即启用菜单的路由模式,菜单选项中对应的index需要改为对应的路由路径,如:index="/index/setting"

检查拼写

solid不是soild
记得区分routerouter

prop 的使用

有一些东西需要从外部传入到当前组件(父组件向子组件传值),就可以使用prop,如下面的例子:
子组件设置需要传的值,以及用到的地方

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<script setup>
defineProps({
icon: Object,
title: String,
description: String
})
</script>

<template>
<div class="card">
<div class="card-header" v-if="title">
<div>
<el-icon style="margin-right: 5px; translate: 0 2px">
<component :is="icon"/>
</el-icon>
{{title}}
</div>
<div>{{description}}</div>
</div>
<slot/>
</div>
</template>

父组件中传入需要传入的值:

1
<card :icon="User" title="账户信息设置" description="在这里编辑自己的个人信息" v-loading="loading">

如何把一部分内容固定在视窗上,不随其他内容的滚动而消失

直接用这个:<div style="position: sticky;top: 20px">

关于头像

要处理的有三个问题:

  1. 如何上传头像
  2. 如何获取自己的头像
  3. 如何获取他人的头像
    (其实这里更多是后端的处理而不是前端,前端只需要负责调用正确的接口就能拿到数据了其实)

1.如何上传头像

关于第一个问题,可以采用<el-upload>配合<el-button>来调用后端接口:

1
2
3
4
<el-upload :action="axios.defaults.baseURL + '/api/image/upload_avatar'" :show-file-list="false"
:before-upload="checkAvatar" :on-success="uploadAvatarSuccess" :headers="getAccessHeader()">
<el-button size="small" round>修改头像</el-button>
</el-upload>

不要忘记发送上传头像的请求还需要附带上验证信息,以及更新存在piniastore里边的对应的头像url数据

2.如何获取自己的头像

<el-avatar>src属性设置成动态属性,取出store里边的计算属性avatarUrl<el-avatar :size="100" :src="store.avatarUrl"/>
store/index.js中加入以下计算属性(顺便user里边多存一个avatar):

1
2
3
4
5
6
7
8
9
10
11
getters: {
// 头像Url
avatarUrl() {
if(this.user.avatar){
return `${axios.defaults.baseURL}/api/image${this.user.avatar}`
}
else {
return "https://s11.ax1x.com/2023/12/23/pi7Rd0g.jpg"
}
}
}

3.如何获取他人的头像

1
<el-avatar :size="40" :src="`${axios.defaults.baseURL}/api/image${item.avatar == null ? '/avatar/d498a8bbe1f64f659a58b5afd55c4587' : item.avatar}`"/>

如何增加边框或是弄一个圆角?

使用 css 的borderborder-radius属性,例如:border-radius: 5px; border: solid 1px var(--el-border-color);

引入富文本编辑器

适配 Vue3 的 Quill 富文本编辑器:npm install @vueup/vue-quill

如何强制覆盖css?

1
2
3
4
5
:deep(.el-drawer) {
width: 800px;
margin: auto;
border-radius: 10px;
}

如何防止反复刷新一个界面(组件)

比如论坛的帖子列表,点进去看一个帖子,再返回,就会刷新一次,如何防止重复刷新调用太多次接口导致资源浪费?
可以在<router-view>标签下使用<keep-alive>标签,并配置include属性,如:<keep-alive include="PostView">,其中PostView为组件名称。

如何实现“回到顶部”功能

有许多界面都需要实现“回到顶部”的功能,要清楚是哪里在控制滚动条,然后用选择器选到外部元素:
首先找到外部元素,即滚动条所在的元素,如下所示,可以看到滚动条在class="index-main"

1
2
3
4
5
6
7
8
9
<el-main class="index-main">
<el-scrollbar style="height: calc(100vh - 60px)">
<router-view v-slot="{ Component }">
<transition name="el-fade-in-linear" mode="out-in">
<component :is="Component" style="height: 100%"/>
</transition>
</router-view>
</el-scrollbar>
</el-main>

然后在需要的地方添加如下代码,target即为滚动条所在的目标元素:

1
<el-backtop target=".index-main .el-scrollbar__wrap" :right="20" :bottom="70"/>

数据库部分

不仅后端可以加参数验证或通过一些代码逻辑保证一些数据的唯一性,同时也可以在数据库上保险,给相应的表设置索引,比如:
给 db_account 表的 username 和 email 字段设定索引,索引类型为 UNIQUE,来保证用户名和邮箱地址的唯一性。

思考

记录一下自己想的一些东西,在这个系统上再增加一些东西

如何限制 ip 段登录?

这样就能够限制使用区域为校园网,其实实现也很简单,毕竟要拿到发来请求的 ip 也就request.getRemoteAddr()就可以了,主要解决的是如何判断这个 ip 是否在我们允许请求的 ip 范围内。
那判断的话其实也很简单,毕竟我们拿到手的是一个合法 ip 地址,直接用String.Split('.')就能得到每段的地址,直接数字比大小判断就行了。要是看过之前的“IP和字符串的相互转换”的文章,甚至都不需要用这个方法,直接位运算。

如何限制邮箱注册?(已实现!)

比如使用某些邮箱的话不能进行注册,这样就能够将用户限制为本校学生

可以设置访客角色吗?

是否可以做一个访客登录,仅能浏览帖子。
想法:感觉就是创建一个账号,然后角色设置为visitor即访客,点访客登录就是自动用这个账号登录进去。

可以根据角色显示不同界面吗?(已实现!)

尝试一下使用<el-container><el-header><el-aside>来设计。因为在本地的authObj中已经保存了role的信息(更新提示:现在已经用 pinia 来保存role的信息了),所以可以尝试取出来并使用v-if标签进行内容选择性渲染:v-if="takeRole() === 'user'"
要记住就算前端一般用户无法看到管理员的界面,但是后端仍需要对管理员的每次操作都进行身份校验,保险。

打算弄个禁言功能

Redis 弄个黑名单表?

深色模式用按钮来操控

这个目前不知道怎么弄

各环境默认端口

mysql

localhost:3306

rabbitMQ

管理工具:http://127.0.0.1:15672/
默認賬戶:admin/admin

Vue

http://localhost:5173

SpringBoot

http://localhost:8080

Redis

http://localhost:6379

MinIO

Console: http://localhost:9090
S3-API: http://localhost:9000


这里有一只爱丽丝

希望本文章能够帮到您~


敲 my-project 时候写的一点东西
https://map1e-g.github.io/2023/12/03/my-project-essay-1/
作者
MaP1e-G
发布于
2023年12月3日
许可协议