项目开发流程及技术选型

流程

需求分析 => 设计(概要设计、详细设计) => 技术选型 =>
初始化 / 引入需要的技术 => 写Demo => 写代码(实现业务逻辑) =>
测试(单元测试) => 代码提交 / 代码评审 => 部署 => 发布

需求分析与技术选型

  1. 登录 / 注册
  2. 用户管理(仅管理员可见)对用户的查询或者修改
  3. 用户校验

前端:三件套
后端:Java + MySQL
部署:服务器 / 容器(平台)

前端初始化

Vue 初始化

  1. 创建项目
    Vue官网
    npm create vue@latest
    选择功能:Typescript + Router + Pinia + Eslint + Prettier
    npm install 安装依赖

格式规范
设置中搜索eslint,选择自动配置;搜索prettier,选择自动配置;
eslint 9.39.1 因为版本太新,不兼容,所以暂时禁用不管。

  1. 使用组件库
    Ant Design Vue版本:4.2.6
    在文档中的快速上手
    $ npm i --save ant-design-vue@4.x

main.ts中引入文档的全局完整注册

1
2
3
4
5
6
7
8
import { createApp } from 'vue';
import Antd from 'ant-design-vue';
import App from './App';
import 'ant-design-vue/dist/reset.css';

const app = createApp(App);

app.use(Antd).mount('#app');
  1. 全局布局管理
    TS与组合式API
    在src目录创建layouts/BasicLayout.vue
    引入组件库布局样式
1
2
3
4
5
<a-layout>
<a-layout-header :style="headerStyle">Header</a-layout-header>
<a-layout-content :style="contentStyle">Content</a-layout-content>
<a-layout-footer :style="footerStyle">Footer</a-layout-footer>
</a-layout>

在App.vue中添加内容

1
2
3
4
5
6
7
8
9
<template>
<div id="app">
<BasicLayout/> <!-- 全局布局 -->
</div>
</template>
<style scoped></style>
<script setup lang="ts"> <!-- 自动生成 -->
import BasicLayout from "@/layouts/BasicLayout.vue";
</script>

继续在BasicLayout.vue中编写代码

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
<template>
<div id="basicLayout">
<!-- 组件库布局样式 -->
<a-layout>
<a-layout-header>Header</a-layout-header>
<a-layout-content>Content</a-layout-content>
<a-layout-footer class="footer">
<a href="https://wzcwzc10.github.io" target="_blank">微光zc的网络小屋</a>
</a-layout-footer>
</a-layout>
</div>
</template>
<script setup lang="ts">

</script>
<style scoped>
#basicLayout .footer{
background: #222222;
text-align: center;
position: fixed;
bottom: 0;
left: 0;
right: 0;
padding: 16px;
}
</style>
  1. 创建路由
    路由
    动态内容替换
    在BasicLayout.vue中将Content替换为
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
34
35
36
37
38
39
40
<template>
<div id="basicLayout">
<a-layout>
<a-layout-header class="header">
<GlobalHeader/>
</a-layout-header>
<a-layout-content class="content">
<router-view/>
</a-layout-content>
<a-layout-footer class="footer">
<a href="https://wzcwzc10.github.io" target="_blank">微光zc的网络小屋</a>
</a-layout-footer>
</a-layout>
</div>
</template>
<script setup lang="ts">
import GlobalHeader from "@/components/GlobalHeader.vue";
</script>
<style scoped>
#basicLayout .footer{
background: #222222;
text-align: center;
position: fixed;
bottom: 0;
left: 0;
right: 0;
padding: 16px;
}
#basicLayout .content{
padding: 20px;
margin-bottom: 20px;
background: linear-gradient(to right, #FCFCFCD8, #ffffff);
}
#basicLayout .header{
background: #ffffff;
margin-bottom: 16px;
color: #ffffff;
padding-inline: 20px;
}
</style>

在router的index.ts添加路由

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
{
path: "/",
name: "home",
component: HomePage,
},
{
path: "/user/login",
name: "userLogin",
component: UserLoginPage,
},
{
path: "/user/register",
name: "userRegister",
component: UserRegisterPage,
},
{
path: "/admin/userManage",
name: "adminUserManage",
component: UserManagePage,
},
  1. 顶部栏
    在components创建GlobalHeader.vue
    直接复制组件库中的顶部导航栏
    同样在BasicLayout.vue中将Header替换为
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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
<template>
<div id="globalHeader">
<a-row :wrap="false"><!--warp自动换行-->
<a-col flex="200px">
<div class="title-bar">
<img class="logo" src="../../src/assets/logo.png" alt="logo" />
<div class="title">微光管理中心</div>
</div>
</a-col>
<!--组件库栅格-->
<a-col flex="auto">
<a-menu
v-model:selectedKeys="current"
mode="horizontal"
:items="items"
@click="doMenuClick"
/>
</a-col>
<!--登录按钮-->
<a-col flex="80px">
<div class="user-login-status">
<!-- <div v-if="loginUserStore.loginUser.id">-->
<!-- {{ loginUserStore.loginUser.username ?? "无名" }}-->
<!-- </div>-->
<!-- <div v-else>-->
<!-- <a-button type="primary" href="/user/login">登录</a-button>-->
<!-- </div>-->
</div>
</a-col>
</a-row>
</div>
</template>

<script lang="ts" setup>
import { h, ref } from "vue";
import { CrownOutlined, HomeOutlined } from "@ant-design/icons-vue";
import { MenuProps } from "ant-design-vue";
import { useRouter } from "vue-router";
//import { useLoginUserStore } from "../../../user-center-frontend-2026/src/stores/useLoginUserStore.ts";

//const loginUserStore = useLoginUserStore();

const router = useRouter();
// 点击菜单后的路由跳转事件
const doMenuClick = ({ key }: { key: string }) => {
router.push({
path: key,
});
};

const current = ref<string[]>(["mail"]);
// 监听路由变化,更新当前菜单选中状态
router.afterEach((to, from, failure) => {
current.value = [to.path];
});

const items = ref<MenuProps["items"]>([
{
key: "/",
icon: () => h(HomeOutlined),
label: "主页",
title: "主页",
},
{
key: "/user/login",
label: "用户登录",
title: "用户登录",
},
{
key: "/user/register",
label: "用户注册",
title: "用户注册",
},
{
key: "/admin/userManage",
icon: () => h(CrownOutlined),
label: "用户管理",
title: "用户管理",
},
{
key: "others",
label: h(
"a",
{ href: "https://wzcwzc10.github.io", target: "_blank" },
"微光zc的网络小窝"
),
title: "微光zc的网络小窝",
},
]);
</script>

<style scoped>
.title-bar {
display: flex;
align-items: center;
}

.title {
color: black;
font-size: 18px;
margin-left: 16px;
}

.logo {
height: 48px;
}
</style>
  1. Axios
    npm install axios
    通过Axios文档CV过来
    创建请求配置文件/src/request.ts
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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
import axios from "axios";

alert(process.env.NODE_ENV);
// 创建axios实例(文档中有)
const myAxios = axios.create({
// 区分开发和线上环境
baseURL:
process.env.NODE_ENV === "development"
? "http://localhost:8080"
: "https://codefather.cn",
timeout: 10000,
withCredentials: true, // 允许携带cookie
});

// Add a request interceptor
// 请求拦截器
myAxios.interceptors.request.use(
function (config) {
// Do something before request is sent
return config;
},
function (error) {
// Do something with request error
return Promise.reject(error);
}
);

// Add a response interceptor
// 响应拦截器
myAxios.interceptors.response.use(
function (response) {
// Any status code that lie within the range of 2xx cause this function to trigger
// Do something with response data
console.log(response);

const { data } = response;
console.log(data);
// 未登录
if (data.code === 40100) {
// 不是获取用户信息接口,且不是登录页面,则跳转到登录页面
if (
!response.request.responseURL.includes("user/current") &&
!window.location.pathname.includes("/user/login")
) {
window.location.href = `/user/login?redirect=${window.location.href}`;
}
}
return response;
},
function (error) {
// Any status codes that falls outside the range of 2xx cause this function to trigger
// Do something with response error
return Promise.reject(error);
}
);
// 导出自定义的axios实例
export default myAxios;

前后端联动

遇到端口冲突:–port 3000
新建api文件夹并创建user.ts

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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
// 引入自定义的axios实例
import myAxios from "@/request";

/**
* 用户注册
* @param params
*/
// 导出用户注册接口,async 是异步函数
export const userRegister = async (params: any) => {
return myAxios.request({
url: "/api/user/register",
method: "POST",
data: params,
});
};

/**
* 用户登录
* @param params
*/
export const userLogin = async (params: any) => {
return myAxios.request({
url: "/api/user/login",
method: "POST",
data: params,
});
};

/**
* 用户注销
* @param params
*/
export const userLogout = async (params: any) => {
return myAxios.request({
url: "/api/user/logout",
method: "POST",
data: params,
});
};

/**
* 获取当前用户
*/
export const getCurrentUser = async () => {
return myAxios.request({
url: "/api/user/current",
method: "GET",
});
};

/**
* 获取用户列表
* @param username
*/
export const searchUsers = async (username: any) => {
return myAxios.request({
url: "/api/user/search",
method: "GET",
params: {
username,
},
});
};

/**
* 删除用户
* @param id
*/
export const deleteUser = async (id: string) => {
return myAxios.request({
url: "/api/user/delete",
method: "POST",
data: id,
// 关键点:要传递 JSON 格式的值
headers: {
"Content-Type": "application/json",
},
});
};

全局状态管理

pinia
在src/stores文件夹下创建 userLoginUserStore.ts
与Java的get set方法类似

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
import { defineStore } from "pinia";
import { ref } from "vue";
import { getCurrentUser } from "@/api/user";

// 定义全局状态管理
export const useLoginUserStore = defineStore("loginUser", () => {
const loginUser = ref<any>({
username: "未登录",
});

// 远程获取登录用户信息
async function fetchLoginUser() {
const res = await getCurrentUser();
if (res.data.code === 0 && res.data.data) {
loginUser.value = res.data.data;
}
// else {
// setTimeout(() => {
// loginUser.value = { username: "小黑子", id: 1 };
// }, 3000);
// }
}

// 单独设置信息
function setLoginUser(newLoginUser: any) {
loginUser.value = newLoginUser;
}

return { loginUser, fetchLoginUser, setLoginUser };
});

页面开发

页面

  1. 欢迎页src/pages/HomePages.vue
1
2
3
4
5
6
7
8
9
10
11
12
13
14
<template>
<div id="homePage">
<h1>{{ msg }}</h1>
</div>
</template>

<script setup lang="ts">
const msg = "欢迎~";
</script>

<style scoped>
#homePage {
}
</style>
  1. 登录页面src/pages/user/UserLoginPages.vue
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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
<template>
<div id="userLoginPage">
<h2 class="title">用户登录</h2>
<a-form
style="max-width: 480px; margin: 0 auto"
:model="formState"
name="basic"
label-align="left"
:label-col="{ span: 4 }"
:wrapper-col="{ span: 20 }"
autocomplete="off"
@finish="handleSubmit"
>
<a-form-item
label="账号"
name="userAccount"
:rules="[{ required: true, message: '请输入账号' }]"
>
<a-input
v-model:value="formState.userAccount"
placeholder="请输入账号"
/>
</a-form-item>
<a-form-item
label="密码"
name="userPassword"
:rules="[
{ required: true, message: '请输入密码' },
{ min: 8, message: '密码不能小于 8 位' },
]"
>
<a-input-password
v-model:value="formState.userPassword"
placeholder="请输入密码"
/>
</a-form-item>
<a-form-item :wrapper-col="{ offset: 4, span: 20 }">
<a-button type="primary" html-type="submit">登录</a-button>
</a-form-item>
</a-form>
</div>
</template>
<script lang="ts" setup>
import { reactive } from "vue";
import { userLogin } from "@/api/user";
import { userLoginUserStore } from "@/stores/userLoginUserStore";
import { message } from "ant-design-vue";
import { useRouter } from "vue-router";

interface FormState {
userAccount: string;
userPassword: string;
}

const formState = reactive<FormState>({
userAccount: "",
userPassword: "",
});

const router = useRouter();
const loginUserStore = userLoginUserStore();

/**
* 提交表单
* @param values
*/
const handleSubmit = async (values: any) => {
const res = await userLogin(values);
// 登录成功,把登录态保存到全局状态中
if (res.data.code === 0 && res.data.data) {
await loginUserStore.fetchLoginUser();
message.success("登录成功");
router.push({
path: "/",
replace: true,
});
} else {
message.error("登录失败");
}
};
</script>

<style scoped>
#userLoginPage .title {
text-align: center;
margin-bottom: 16px;
}
</style>

  1. 注册页面src/pages/user/UserRegisterPages.vue
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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
<template>
<div id="userRegisterPage">
<h2 class="title">用户注册</h2>
<a-form
style="max-width: 480px; margin: 0 auto"
:model="formState"
name="basic"
label-align="left"
:label-col="{ span: 4 }"
:wrapper-col="{ span: 20 }"
autocomplete="off"
@finish="handleSubmit"
>
<a-form-item
label="账号"
name="userAccount"
:rules="[{ required: true, message: '请输入账号' }]"
>
<a-input
v-model:value="formState.userAccount"
placeholder="请输入账号"
/>
</a-form-item>
<a-form-item
label="密码"
name="userPassword"
:rules="[
{ required: true, message: '请输入密码' },
{ min: 8, message: '密码不能小于 8 位' },
]"
>
<a-input-password
v-model:value="formState.userPassword"
placeholder="请输入密码"
/>
</a-form-item>
<a-form-item
label="确认密码"
name="checkPassword"
:rules="[
{ required: true, message: '请输入确认密码' },
{ min: 8, message: '确认密码不能小于 8 位' },
]"
>
<a-input-password
v-model:value="formState.checkPassword"
placeholder="请输入确认密码"
/>
</a-form-item>
<a-form-item
label="编号"
name="planetCode"
:rules="[{ required: true, message: '请输入编号' }]"
>
<a-input
v-model:value="formState.planetCode"
placeholder="请输入编号"
/>
</a-form-item>
<a-form-item :wrapper-col="{ offset: 4, span: 20 }">
<a-button type="primary" html-type="submit">注册</a-button>
</a-form-item>
</a-form>
</div>
</template>
<script lang="ts" setup>
import { reactive } from "vue";
import { userRegister } from "@/api/user";
import { message } from "ant-design-vue";
import { useRouter } from "vue-router";

interface FormState {
userAccount: string;
userPassword: string;
checkPassword: string;
planetCode: string;
}

const formState = reactive<FormState>({
userAccount: "",
userPassword: "",
checkPassword: "",
planetCode: "",
});

const router = useRouter();

/**
* 提交表单
* @param values
*/
const handleSubmit = async (values: any) => {
// 判断两次输入的密码是否一致
if (formState.userPassword !== formState.checkPassword) {
message.error("二次输入的密码不一致");
return;
}
const res = await userRegister(values);
// 注册成功,跳转到登录页面
if (res.data.code === 0 && res.data.data) {
message.success("注册成功");
router.push({
path: "/user/login",
replace: true,
});
} else {
message.error("注册失败," + res.data.description);
}
};
</script>

<style scoped>
#userRegisterPage .title {
text-align: center;
margin-bottom: 16px;
}
</style>

  1. 管理页面src/pages/admin/UserManagePage.vue
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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
<template>
<div id="userManagePage">
<a-input-search
style="max-width: 320px; margin-bottom: 20px"
v-model:value="searchValue"
placeholder="输入用户名搜索"
enter-button="搜索"
size="large"
@search="onSearch"
/>
<a-table :columns="columns" :data-source="data">
<template #bodyCell="{ column, record }">
<template v-if="column.dataIndex === 'avatarUrl'">
<a-image :src="record.avatarUrl" :width="120" />
</template>
<template v-else-if="column.dataIndex === 'userRole'">
<div v-if="record.userRole === 1">
<a-tag color="green">管理员</a-tag>
</div>
<div v-else>
<a-tag color="blue">普通用户</a-tag>
</div>
</template>
<template v-else-if="column.dataIndex === 'createTime'">
{{ dayjs(record.createTime).format("YYYY-MM-DD HH:mm:ss") }}
</template>
<template v-else-if="column.key === 'action'">
<a-button danger @click="doDelete(record.id)">删除</a-button>
</template>
</template>
</a-table>
</div>
</template>
<script lang="ts" setup>
import { deleteUser, searchUsers } from "@/api/user";
import { ref } from "vue";
import { message } from "ant-design-vue";
import dayjs from "dayjs";

const searchValue = ref("");
// 获取数据
const onSearch = () => {
fetchData(searchValue.value);
};

// 删除数据
const doDelete = async (id: string) => {
if (!id) {
return;
}
const res = await deleteUser(id);
if (res.data.code === 0) {
message.success("删除成功");
} else {
message.error("删除失败");
}
};

const columns = [
{
title: "id",
dataIndex: "id",
},
{
title: "用户名",
dataIndex: "username",
},
{
title: "账号",
dataIndex: "userAccount",
},
{
title: "头像",
dataIndex: "avatarUrl",
},
{
title: "性别",
dataIndex: "gender",
},
{
title: "创建时间",
dataIndex: "createTime",
},
{
title: "用户角色",
dataIndex: "userRole",
},
{
title: "操作",
key: "action",
},
];

// 数据
const data = ref([]);

// 获取数据
const fetchData = async (username = "") => {
const res = await searchUsers(username);
if (res.data.data) {
data.value = res.data.data;
} else {
message.error("获取数据失败");
}
};

fetchData();
</script>

全局校验

创建src/access.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import router from "@/router";
import { useLoginUserStore } from "@/store/userLoginUserStore";
import { message } from "ant-design-vue";

/**
* 全局权限校验
*/
router.beforeEach(async (to, from, next) => {
const loginUserStore = useLoginUserStore();
const loginUser = loginUserStore.loginUser;
const toUrl = to.fullPath;
if (toUrl.startsWith("/admin")) {
if (!loginUser || loginUser.userRole !== 1) {
message.error("没有权限");
next(`/user/login?redirect=${to.fullPath}`);
return;
}
}
next();
});

在src/main.ts 中引入import './access.ts';

多环境

报出了 Cannot find name ‘process’ 的错误。
npm install --save-dev @types/node
在 tsconfig.app.json 中添加以下内容:

1
2
3
4
5
6
7
8
9
{
"compilerOptions": {
// ... 其他选项
"types": [
"vue",
"node" // <-- 确保添加了这一行
]
}
}
1
2
3
4
5
6
7
8
9
10
11
alert(process.env.NODE_ENV);

const myAxios = axios.create({
// 区分开发和线上环境
baseURL:
process.env.NODE_ENV === "development"
? "http://localhost:8080"
: "https://codefather.cn",
timeout: 10000,
withCredentials: true,
});

后端初始化

  1. spring boot项目初始化
    在IDEA中创建SpringBoot项目,选择Spring Initializr。
    Spring boot版本选择2.7.6
    添加依赖:
    Lombok + Spring Web(spring mvc提供接口访问、restfulf接口等能力) +
    Spring Boot DevTools(热更新) +
    Spring Configuration Processor(读取属性文件) +
    MySQL Driver + MyBatis Framework(操作数据库)

MyBatis相关介绍
mybatis(Java操作数据库的框架,持久层框架,对jdbc的封装)
mybatis-plus(对mybatis的增强,不用写sql也能实现增删改查)

  1. 数据库初始化
    连接 MySQL 数据库 userNew
    按照 mybatis-plus 官方教程 操作
    删除mybatis的依赖,mybatis-plus依赖已经集成了mybatis,否则会报错
  • 引入依赖
1
2
3
4
5
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.14</version>
</dependency>

JUnit 单元测试框架,在 Java 上编写和运行可重复的自动化测试。

1
2
3
4
5
6
7
<!-- https://mvnrepository.com/artifact/junit/junit -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.2</version>
<scope>test</scope>
</dependency>
  1. 配置文件
    application.properties 改成 application.yml(格式更美观)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
spring:
application:
name: user-center

# 数据源
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
# 如果安装了多版本的 MySQL ,此处需要修改端口号
url: jdbc:mysql://localhost:3306/wzc
username: root
password: 123456

server:
port: 8080

# MyBatisX自动命名类似映射的冲突解决
mybatis-plus:
configuration:
map-underscore-to-camel-case: false
  1. 创建数据库表
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
DROP TABLE IF EXISTS 'user';
CREATE TABLE 'user' (

`username` varchar(256) NULL COMMENT '用户昵称',
`id` bigint NOT NULL AUTO_INCREMENT COMMENT 'id',
`userAccount` varchar(256) NULL COMMENT '账号',
`avatarUrl` varchar(1024) NULL COMMENT '用户头像',
`gender` tinyint NULL COMMENT '性别',
`userPassword` varchar(512) NOT NULL COMMENT '密码',
`phone` varchar(128) NULL COMMENT '电话',
`email` varchar(512) NULL COMMENT '邮箱',
`userStatus` int DEFAULT '0' NULL COMMENT '状态 0-正常',
`createTime` datetime DEFAULT CURRENT_TIMESTAMP NULL COMMENT '创建时间',
`updateTime` datetime DEFAULT CURRENT_TIMESTAMP NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`isDelete` tinyint DEFAULT '0' NOT NULL COMMENT '是否删除',
`userRole` tinyint DEFAULT '0' NULL COMMENT '用户角色 0-普通用户 1-管理员',
PRIMARY KEY (`id`)

) COMMENT='用户表';
  1. 测试 Mybatis-plus 是否引入成功
    在 Spring Boot 启动类中添加 @MapperScan 注解,扫描 mapper 文件夹
    @MapperScan("fun.wgzc.usercenterbackend2026.mapper")

编写实体类 model/User.java:

1
2
3
4
5
6
7
@Data
public class User {
private Long id;
private String name;
private Integer age;
private String email;
}

编写BaseMapper<User>接口类 mapper/UserMapper.java:

1
2
3
public interface UserMapper extends BaseMapper<User> {

}

添加测试类,进行功能测试(两种方式)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@SpringBootTest
class UserCenterApplicationTests {

@Resource
private UserMapper userMapper;

@Test
void contextLoads() {
System.out.println(("----- selectAll method test ------"));
List userList = userMapper.selectList(null);
Assert.isTrue(5 == userList.size(), "");
userList.forEach(System.out::println);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
@SpringBootTest
public class SampleTest {

@Resource
private UserMapper userMapper;

@Test
public void testSelect() {
System.out.println(("----- selectAll method test ------"));
List userList = userMapper.selectList(null);
Assert.isTrue(5 == userList.size(), "");
userList.forEach(System.out::println);
}
}
  1. Spring Boot 五层结构
    文件 说明 补充说明
    controller 请求层/控制层
    (只接收请求)
    应用程序的入口点,负责接收用户的请求并返回响应。
    调用Service层的方法来处理业务逻辑,并将结果返回给前端用户
    service 业务逻辑层
    (专门编写业务逻辑,例如登录注册)
    程序核心,负责处理业务逻辑。
    接收Controller层的请求,调用Mapper层执行数据库操作,并将处理结果返回给Controller层。
    还可以包含一些复杂的业务逻辑处理,如事务管理、数据校验等。
    mapper / dao 数据库访问层
    (专门从数据库中对数据进行增删改查操作)
    负责与数据库交互,执行数据的增删改查(CRUD)操作。
    通常包含了一系列的接口,定义了数据库操作的方法,而具体的SQL实现则位于Mapper XML文件中
    model / entity / pojo 实体层
    (定义了一些和数据库相关的数据模型、封装类)
    (可能需要分层 entity、dto、vo……)
    定义数据模型,即数据库表的映射实体类。
    实体类中包含了与数据库表相对应的属性和方法(如get和set方法,toString 方法,有参无参构造函数)
    utils 存放一些工具类
    (加密、格式转换、日期转换……
    与业务关系不太大的类)
    static 静态文件(html)

后端注册登录功能

代码自动生成器 Mybatis-X 插件(根据数据库生成)

•domain(实体对象)
•mapper(操作数据库的对象)
•mapper.xml(定义了mapper对象和数据库的关联,可以在里面自己写SQL)
•service(包含常用的增删改查)
◦serviceImpl(具体实现service)

右键数据库表,点击第一行路径默认

选择 camel 格式类名
选择 Mybatis-Plus3
勾选 Lombok
勾选 Comment 可以在生成的实体类添加注释
勾选 Actual Column 可以让生成实体类的字段名和数据库保持一致
勾选 Model 创建实体类
生成后根据项目结构自己处理

测试
鼠标放在 userService.java 文件的 userService 上,按住 alt + enter ,选择创建测试类
编写测试代码【安装插件 CenerateAllSetter ,可快速生成代码】
编写代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@SpringBootTest
class UserServiceTest {

@Resource
private UserService userService;

@Test
void testAddUser() {
User user = new User();
user.setUsername("wgzc");
user.setUserAccount("123");
user.setAvatarUrl("https://wzcwzc10.github.io/img/jufufu-ht.gif");
user.setGender(0);
user.setUserPassword("xxx");
user.setPhone("123");
user.setEmail("456");
boolean result = userService.save(user);
System.out.println(user.getId());
Assertions.assertTrue(result); // 断言result为true
}

详细设计

注册接口

  1. 前端输入账户和密码、以及校验码(todo)
  2. 校验用户的账户、密码、校验密码是否符合要求
    ◦账户不小于 4 位(自己扩展校验)
    ◦密码不小于 8 位
    ◦账户不能重复
    ◦账户不包含特殊字符
    ◦密码和校验密码相同
  3. 对密码进行加密(千万不能明文存储到数据库中)
  4. 向数据库插入用户数据

登录接口

  1. 接收参数:用户账户、密码
  2. 请求类型:POST
    请求参数很长时不建议用 GET
  3. 请求体:JSON格式的数据
  4. 返回值:用户信息(脱敏)

逻辑

  1. 校验用户账户和密码是否合法
    ◦非空
    ◦账户不小于 4 位
    ◦密码不小于 8 位
    ◦账户不包含特殊字符
  2. 校验密码是否输入正确,要和数据库中的密文密码对比
  3. 用户脱敏,隐藏敏感信息,防止数据库中的字段泄露
  4. 我们要记录用户的登录态(session),将其存到服务器上(用后端SpringBoot框架封装的服务器tomcat去记录)
    ◦cookie
  5. 返回安全的脱敏后的用户信息

注册功能

去maven仓库引入 commons-lang3 依赖(类似hutool)

1
2
3
4
5
6
<!-- https://mvnrepository.com/artifact/org.apache.commons/commons-lang3 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.18.0</version>
</dependency>

在 UserService 类中添加
long userRegister(String userAccount, String userPassword, String checkPassword);
User userLogin(String userAccount, String userPassword, HttpServletRequest request);

Tips: 快速生成注释
在方法上打出 /** 回车即可
alt + enter 快速实现方法

commons-lang3 方法 StringUtils.isAnyBlank:判断字符串是否为空
if (StringUtils.isAnyBlank(userAccount, userPassword, checkPassword))

MyBatis-Plus 条件构造器查询 QueryWrapper

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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
// 注入 UserMapper
@Resource
private UserMapper userMapper;

@Override
public long userRegister(String userAccount, String userPassword, String checkPassword) {
/* 1. 校验 */
if (StringUtils.isAnyBlank(userAccount, userPassword, checkPassword)) {
return -1;
}
if (userAccount.length() < 4) {
return -1;
}
if (userPassword.length() < 8 || checkPassword.length() < 8) {
return -1;
}

//账户不能包含特殊字符
String validateRegExp = "\\pP|\\pS|\\s+";
Matcher matcher = Pattern.compile(validateRegExp).matcher(userAccount);
if (!matcher.find()) {
return -1;
}

//校验密码是否相同
if (!userPassword.equals(checkPassword)) {
return -1;
}

//账户不能重复
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("userAccount", userAccount);
Long count = userMapper.selectCount(queryWrapper);
if (count > 0) {
return -1;
}
/* 2.加密 */
final String SALT = "wgzc";
String encryptPassword = DigestUtils.md5DigestAsHex((SALT + userPassword).getBytes());

/* 3.插入数据 */
User user = new User();
user.setUserAccount(userAccount);
user.setUserPassword(encryptPassword);
boolean saveResult = this.save(user);
if (!saveResult) {
return -1;
}
return user.getId();
}

单元测试

ServiceTest 测试代码生成与编写

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
34
35
@Test
void userRegister() {
String userAccount = "yupi";
String userPassword = "";
String checkPassword = "123456";
long result = userService.userRegister(userAccount, userPassword, checkPassword);
Assertions.assertEquals(-1, result);

userAccount = "yu";
result = userService.userRegister(userAccount, userPassword, checkPassword);
Assertions.assertEquals(-1, result);

userAccount = "yupi";
userPassword = "123456";
result = userService.userRegister(userAccount, userPassword, checkPassword);
Assertions.assertEquals(-1, result);

userAccount = "yu pi";
userPassword = "12345678";
result = userService.userRegister(userAccount, userPassword, checkPassword);
Assertions.assertEquals(-1, result);

checkPassword = "123456789";
result = userService.userRegister(userAccount, userPassword, checkPassword);
Assertions.assertEquals(-1, result);

userAccount = "dogYupi";
checkPassword = "12345678";
result = userService.userRegister(userAccount, userPassword, checkPassword);
Assertions.assertEquals(-1, result);

userAccount = "yupi";
result = userService.userRegister(userAccount, userPassword, checkPassword);
Assertions.assertTrue(result > 0);
}

执行报错
过滤特殊字符的正则表达式换成搜索出的正则表达式[A-Za-z0-9_\-\u4e00-\u9fa5]+
再次执行,报错了(是因为数据库中没有dogYupi 这个账户,设置与其返回值为 -1,所以报错)

登录态管理(Cookie 和 Session)

如何知道是哪个用户登陆了?(JavaWeb)

  1. 连接服务器端后,得到一个 session 状态(匿名会话),返回给前端
  2. 登陆成功得到登陆成功的 session 并设置一些值(比如用户信息),返回给前端一个设置 cookie 的“命令” session ⇒ cookie
  3. 前端接收到后端的命令后,设置 cookie ,保存到浏览器内
  4. 前端再次请求后端的时候(相同的域名),在请求头中带上cookie去请求
  5. 后端拿到前端传来的 cookie ,找到对应的 session
  6. 后端从 session 中可以取出基于该 session 存储的变量(用户的登陆信息、登录名)
    Cookie & Session 相关知识了解
Cookie Session
定义 Cookie 是存储在浏览器上的小数据。由服务器发送,浏览器保存,并在后续请求中自动发送回服务器。用于识别用户身份、存储用户偏好等 Session 是服务器端技术,存储用户会话信息。服务器会为每个用户创建一个唯一的 Session ID,并通过 Cookie(或其他方式,如URL重写)将 Session ID 发送给客户端。客户端在后续请求中会携带这个 Session ID,服务器通过解析 Session ID 来获取对应的 Session 信息
特点 •存储在客户端 •容量有限(大多数浏览器限制每个 Cookie 的大小不超过 4KB,且每个站点最多可以创建 20 个 Cookie) •安全性较低,因为 Cookie 存储在用户的计算机上,可以被用户看到并修改(尽管可以通过设置 HttpOnly 和 Secure 属性来提高安全性) •发送 Cookie 会增加每次 HTTP 请求的数据量,可能影响页面加载速度 •存储在服务器端 •安全性较高,因为敏感信息不会直接存储在客户端。 •容量相对较大,因为受限于服务器的资源而非客户端的浏览器限制。 •依赖于客户端的 Session ID 传递
用途 •用户身份识别。 •存储用户偏好设置。 •跟踪用户行为(例如,记录用户的访问次数) •存储用户会话信息,如登录状态、购物车内容等。 •管理用户登录状态,实现身份验证和授权
联系 •Session 通常依赖于 Cookie 来传递 Session ID,以维持用户会话的连续性。 •都可用于跟踪用户状态
区别 •存储位置:Cookie 存储在客户端,Session 存储在服务器端。 •安全性:Session 比 Cookie 安全性高,因为敏感数据不直接存储在客户端。 •容量:Cookie 的容量有限,Session 的容量相对较大。 •使用场景:Cookie 更适合存储非敏感信息,如用户偏好;Session 更适合存储敏感信息,如用户登录状态

登录功能

将 Register 的逻辑复制修改:

  • 将盐值 final String SALT = “wgzc”; 提到前面
  • 添加用户登录态键 USER_LOGIN_STATE = “userloginstate”;
  • 添加 @Slf4j 注解使用 log 【方便后续系统出现问题,到日志中查找问题】
  • 修改部分逻辑 【删除插入数据;账户不能重复修改成查询用户是否存在,放在加密之后】
  • 添加用户脱敏逻辑
  • 最后返回脱敏后的用户信息 safetyUser
1
2
3
4
/**
* 盐值,混淆密码
*/
private static final String SALT = "wgzc";
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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
@Override
public User userLogin(String userAccount, String userPassword, HttpServletRequest request) {
/* 1. 校验 */
if (StringUtils.isAnyBlank(userAccount,userPassword)) {
return null;
}
if(userAccount.length() < 4){
return null;
}
if(userPassword.length() < 8){
return null;
}

//账户不能包含特殊字符
String validPattern = "[^A-Za-z0-9_\\\\-\\\\u4e00-\\\\u9fa5]";
Matcher matcher = Pattern.compile(validPattern).matcher(userAccount);
if (matcher.find()) {
return null;
}

/* 2.加密 */
String encryptPassword = DigestUtils.md5DigestAsHex((SALT + userPassword).getBytes());
//查询用户是否存在
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("userAccount",userAccount);
queryWrapper.eq("userPassword",encryptPassword);
User user = userMapper.selectOne(queryWrapper);
if (user == null) {
log.error("user login failed,userAccount cannot match userPassword");
return null;
}
/* 3.用户脱敏 */
User safetyUser = new User();
safetyUser.setId(user.getId());
safetyUser.setUsername(user.getUsername());
safetyUser.setUserAccount(user.getUserAccount());
safetyUser.setAvatarUrl(user.getAvatarUrl());
safetyUser.setGender(user.getGender());
safetyUser.setPhone(user.getPhone());
safetyUser.setEmail(user.getEmail());
safetyUser.setUserStatus(user.getUserStatus());
safetyUser.setCreateTime(user.getCreateTime());
safetyUser.setUserRole(user.getUserRole());

/*4.记录用户的登录态 */
request.getSession().setAttribute(USER_LOGIN_STATE,safetyUser);

return safetyUser;
}

逻辑删除

上面查询用户是否存在的代码逻辑是存在问题的
如果用户的 isDelete 字段是删除状态,能否查出来呢?

@TableLogic 标记实体类中的逻辑删除字段
使用此注解,MyBatis-Plus 可以在查询、更新和删除操作中自动处理逻辑删除字段的值。

要删除一条数据时,不是真正的删除,而是将数据库中的某个字段从 0 置为 1 表示数据失效,无法查询。

使用方法:

  1. 在 application.yml 中配置 MyBatis-Plus 的全局逻辑删除属性:
1
2
3
4
5
6
7
8
9
mybatis-plus:
configuration:
map-underscore-to-camel-case: false
# 全局属性
global-config:
db-config:
logic-delete-field: isDelete # 全局逻辑删除字段名
logic-delete-value: 1 # 逻辑已删除值
logic-not-delete-value: 0 # 逻辑未删除值
  1. User类中为 isDelete 字段添加 @TableLogic

后端接口开发及测试

控制层Controller封装请求

  1. application.yml 指定接口全局 api【也可以可以等代理做】
1
2
3
4
5
server:
port: 8080
# 配置接口全局 api
servlet:
context-path: /api

@RestController 适用于编写restful风格的api,返回值默认为json类型
controller 层倾向于对请求参数本身的校验,不涉及业务逻辑本身(越少越好)
service 层是对业务逻辑的校验(有可能被 controller 之外的类调用)

  1. 下载插件Auto filling Java call arguments
    【自动填充 java 参数】(安装完成记得重启)
    alt + 回车 ,Auto filling Java call arguments

  2. 新建 model/request 包
    创建 UserRegisterRequest.java 继承 Serializable(序列化),打上@Data注解

1
2
3
4
private static final long serialVersionUID = 3191241716373120793L;
private String userAccount;
private String userPassword;
private String checkPassword;
  1. 新建 UserController.java
    添加 @RestController 注解(这个类中所有的请求的接口返回值,相应的数据类型都是 application json)
    添加 @RequestMapping 注解(定义请求的路径)
    添加 @CrossOrigin 注解(解决跨域问题)
1
2
3
@RestController
@RequestMapping("/user")
@CrossOrigin(origins = "http://localhost:5173/" , allowCredentials = "true")
  1. 引入 User Service
1
2
@Resource
private UserService userService;
  1. UserController 中编写 register 请求
    @PostMapping 注解(定义请求的路径)
    @RequestBody 注解(将请求体中的数据映射为 UserRegisterRequest 对象)
1
2
3
4
5
6
7
8
9
10
11
12
13
@PostMapping("/register")
public Long userRegister(@RequestBody UserRegisterRequest userRegisterRequest){
if (userRegisterRequest == null) {
return null;
}
String userAccount = userRegisterRequest.getUserAccount();
String userPassword = userRegisterRequest.getUserPassword();
String checkPassword = userRegisterRequest.getCheckPassword();
if(StringUtils.isAnyBlank(userAccount, userPassword, checkPassword)){
return null;
}
return userService.userRegister(userAccount, userPassword, checkPassword);
}
  1. UserController 中编写 login 请求
    复制 register 请求,进行修改
1
2
3
4
5
6
7
8
9
10
11
12
@PostMapping("/login")
public User userLogin(@RequestBody UserLoginRequest userLoginRequest, HttpServletRequest request){
if (userLoginRequest == null) {
return null;
}
String userAccount = userLoginRequest.getUserAccount();
String userPassword = userLoginRequest.getUserPassword();
if(StringUtils.isAnyBlank(userAccount, userPassword)){
return null;
}
return userService.userLogin(userAccount, userPassword, request);
}
  1. 新建 UserLoginRequest.java
    (复制粘贴 UserRegisterRequest.java 删除其中的 checkPassword 即可)

用户管理功能

!!!必须鉴权 !!!

  • 查询用户(允许根据用户名查询)
  • 删除用户
  1. UserController 里编写查询用户请求,删除用户请求
1
2
3
4
5
6
7
8
@GetMapping("/search")
public List<User> searchUsers(String username, HttpServletRequest request) {
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
if (StringUtils.isNotBlank(username)) {
queryWrapper.like("username", username);
}
return userService.list(queryWrapper);
}
1
2
3
4
5
6
7
@PostMapping("/delete")
public boolean deleteUser(@RequestBody long id) {
if (id <= 0) {
return false;
}
return userService.removeById(id);
}
  1. 校验是否是管理员
    创建 contant/UserConstant.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* 用户常量
*/
public interface UserConstant {

/**
* 用户登录态键
*/
String USER_LOGIN_STATE = "userLoginState";

// ------- 权限 --------
int DEFAULT_ROLE = 0; // 普通用户
int ADMIN_ROLE = 1; // 管理员
}

  1. 修改 yml 配置文件
    设置 session 失效时间
1
2
3
4
spring:
# session超时时间
session:
timeout: 86400
  1. UserServiceImpl 下新建 getSafetyUser 方法,将用户脱敏的代码剪切过去
  • 将 user 重构为 originUser

  • 原来 用户脱敏 的位置调用 getSafetyUser 方法
    User safetyUser = getSafetyUser(user);

  • 添加注解 @Override 将方法引入 UserService 接口,加上注释
    User getSafetyUser(User originUser);

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
/**
* 用户脱敏
*
* @param originUser 原始用户信息
* @return 脱敏后的用户信息
*/
@Override
public User getSafetyUser(User originUser) {
if (originUser == null) {
return null;
}
User safetyUser = new User();
safetyUser.setId(originUser.getId());
safetyUser.setUsername(originUser.getUsername());
safetyUser.setUserAccount(originUser.getUserAccount());
safetyUser.setAvatarUrl(originUser.getAvatarUrl());
safetyUser.setGender(originUser.getGender());
safetyUser.setPhone(originUser.getPhone());
safetyUser.setEmail(originUser.getEmail());
safetyUser.setUserStatus(originUser.getUserStatus());
safetyUser.setCreateTime(originUser.getCreateTime());
safetyUser.setUserRole(originUser.getUserRole());
return safetyUser;
}

用户注销功能

  1. UserService 中添加 userLogout 方法
    int userLogout(HttpServletRequest request);

  2. UserServiceImpl 中实现方法
    Alt + Enter 选择 Implement methods
    修改一下代码,获取 session 中的数据,鼠标指示在 removeAttribute 上,发现返回值是 void
    修改一下返回值【后续注销失败抛出异常即可,不需要定义一个返回值】,UserService中也修改一下

1
2
3
4
5
6
7
8
9
10
  /**
* 用户注销
* @param request
*/
@Override
public int userLogout(HttpServletRequest request) {
//移除用户登录态
request.getSession().removeAttribute(USER_LOGIN_STATE);
return 1;
}
  1. UserController 中编写注销请求
1
2
3
4
5
6
7
@PostMapping("/logout")
public Integer userLogout(HttpServletRequest request){
if (request == null) {
return null;
}
return userService.userLogout(request);
}

后端代码优化

  • 通用返回对象
  • 封装全局异常处理
  1. 新建 common 包 下新建 BaseResponse.java
    Alt + Insert 生成 Constructor 方法进行修改
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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
/**
* 通用返回类
*/
@Data
public class BaseResponse<T> implements Serializable {

/**
* 状态码
*/
private int code;

/**
* 数据
*/
private T data;

/**
* 消息
*/
private String message;

/**
* 描述
*/
private String description;

// https://t.zsxq.com/0emozsIJh
public BaseResponse(int code, T data, String message, String description) {
this.code = code;
this.data = data;
this.message = message;
this.description = description;
}

public BaseResponse(int code, T data, String message) {
this(code, data, message, "");
}

public BaseResponse(int code, T data) {
this(code, data, "", "");
}

public BaseResponse(ErrorCode errorCode) {
this(errorCode.getCode(), null, errorCode.getMessage(), errorCode.getDescription());
}
}

  1. 新建 common/ResultUtils.java
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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
/**
* 返回工具类
*/
public class ResultUtils {

/**
* 成功
* @param data 数据
* @param <T> 泛型
* @return BaseResponse
*/
public static <T> BaseResponse<T> success(T data) {
return new BaseResponse<>(0, data, "ok");
}

/**
* 失败
* @param errorCode
* @return
*/
public static BaseResponse error(ErrorCode errorCode) {
return new BaseResponse<>(errorCode);
}

/**
* 失败
* @param code 错误码
* @param message 错误信息
* @param description 错误描述
* @return BaseResponse
*/
public static BaseResponse error(int code, String message, String description) {
return new BaseResponse(code, null, message, description);
}

/**
* 失败
* @param errorCode 错误码
* @return BaseResponse
*/
public static BaseResponse error(ErrorCode errorCode, String message, String description) {
return new BaseResponse(errorCode.getCode(), null, message, description);
}

/**
* 失败
* @param errorCode 错误码
* @return BaseResponse
*/
public static BaseResponse error(ErrorCode errorCode, String description) {
return new BaseResponse(errorCode.getCode(), errorCode.getMessage(), description);
}
}

  1. 修改 UserController.java
    BaseResponse<Long>
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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
@RestController
@RequestMapping("/user")
public class UserController {

@Resource
private UserService userService;

/**
* 用户注册
* @param userRegisterRequest 注册信息
* @return 用户id
*/
@PostMapping("/register")
public BaseResponse<Long> userRegister(@RequestBody UserRegisterRequest userRegisterRequest) {
// 校验
if (userRegisterRequest == null) {
throw new BusinessException(ErrorCode.PARAMS_ERROR);
}
String userAccount = userRegisterRequest.getUserAccount();
String userPassword = userRegisterRequest.getUserPassword();
String checkPassword = userRegisterRequest.getCheckPassword();
if (StringUtils.isAnyBlank(userAccount, userPassword, checkPassword)) {
return null;
}
long result = userService.userRegister(userAccount, userPassword, checkPassword);
return ResultUtils.success(result);
}

/**
* 用户登录
*
* @param userLoginRequest
* @param request
* @return
*/
@PostMapping("/login")
public BaseResponse<User> userLogin(@RequestBody UserLoginRequest userLoginRequest, HttpServletRequest request) {
if (userLoginRequest == null) {
return ResultUtils.error(ErrorCode.PARAMS_ERROR);
}
String userAccount = userLoginRequest.getUserAccount();
String userPassword = userLoginRequest.getUserPassword();
if (StringUtils.isAnyBlank(userAccount, userPassword)) {
return ResultUtils.error(ErrorCode.PARAMS_ERROR);
}
User user = userService.userLogin(userAccount, userPassword, request);
return ResultUtils.success(user);
}

/**
* 用户注销
*
* @param request
* @return
*/
@PostMapping("/logout")
public BaseResponse<Integer> userLogout(HttpServletRequest request) {
if (request == null) {
throw new BusinessException(ErrorCode.PARAMS_ERROR);
}
int result = userService.userLogout(request);
return ResultUtils.success(result);
}

/**
* 获取当前用户
*
* @param request
* @return
*/
@GetMapping("/current")
public BaseResponse<User> getCurrentUser(HttpServletRequest request) {
Object userObj = request.getSession().getAttribute(USER_LOGIN_STATE);
User currentUser = (User) userObj;
if (currentUser == null) {
throw new BusinessException(ErrorCode.NOT_LOGIN);
}
long userId = currentUser.getId();
// TODO 校验用户是否合法
User user = userService.getById(userId);
User safetyUser = userService.getSafetyUser(user);
return ResultUtils.success(safetyUser);
}


@GetMapping("/search")
public BaseResponse<List<User>> searchUsers(String username, HttpServletRequest request) {
if (!isAdmin(request)) {
throw new BusinessException(ErrorCode.NO_AUTH, "缺少管理员权限");
}
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
if (StringUtils.isNotBlank(username)) {
queryWrapper.like("username", username);
}
List<User> userList = userService.list(queryWrapper);
List<User> list = userList.stream().map(user -> userService.getSafetyUser(user)).collect(Collectors.toList());
return ResultUtils.success(list);
}

@PostMapping("/delete")
public BaseResponse<Boolean> deleteUser(@RequestBody long id, HttpServletRequest request) {
if (!isAdmin(request)) {
throw new BusinessException(ErrorCode.NO_AUTH);
}
if (id <= 0) {
throw new BusinessException(ErrorCode.PARAMS_ERROR);
}
boolean b = userService.removeById(id);
return ResultUtils.success(b);
}

/**
* 是否为管理员
*/
private boolean isAdmin(HttpServletRequest request) {
// 仅管理员可查询
Object userObj = request.getSession().getAttribute(USER_LOGIN_STATE);
User user = (User) userObj;
return user != null && user.getUserRole() == ADMIN_ROLE;
}
}

  1. 自定义异常及错误代码
    新建 common/ErrorCode.java
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
34
35
36
37
38
39
40
41
42
43
/**
* 错误码
*/
public enum ErrorCode {

SUCCESS(0, "ok", ""),
PARAMS_ERROR(40000, "请求参数错误", ""),
NULL_ERROR(40001, "请求数据为空", ""),
NOT_LOGIN(40100, "未登录", ""),
NO_AUTH(40101, "无权限", ""),
SYSTEM_ERROR(50000, "系统内部异常", "");

private final int code;

/**
* 状态码信息
*/
private final String message;

/**
* 状态码描述(详情)
*/
private final String description;

ErrorCode(int code, String message, String description) {
this.code = code;
this.message = message;
this.description = description;
}

public int getCode() {
return code;
}

public String getMessage() {
return message;
}


public String getDescription() {
return description;
}
}
  1. 新建 exception 包下新建 BusinessException.java
    生成 constructor
    复制生成的 constructor 方法修改
    生成 Getter
    生成的 Get 方法
    增加 final
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
34
35
36
37
38
39
40
41
42
43
/**
* 自定义异常类
*/
public class BusinessException extends RuntimeException {

/**
* 异常码
*/
private final int code;

/**
* 描述
*/
private final String description;

public BusinessException(String message, int code, String description) {
super(message);
this.code = code;
this.description = description;
}

public BusinessException(ErrorCode errorCode) {
super(errorCode.getMessage());
this.code = errorCode.getCode();
this.description = errorCode.getDescription();
}

public BusinessException(ErrorCode errorCode, String description) {
super(errorCode.getMessage());
this.code = errorCode.getCode();
this.description = description;
}

public int getCode() {
return code;
}


public String getDescription() {
return description;
}
}

新建 exception/GlobalExceptionHandler.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
* 全局异常处理器
*/
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {

@ExceptionHandler(BusinessException.class)
public BaseResponse<?> businessExceptionHandler(BusinessException e) {
log.error("businessException: " + e.getMessage(), e);
return ResultUtils.error(e.getCode(), e.getMessage(), e.getDescription());
}

@ExceptionHandler(RuntimeException.class)
public BaseResponse<?> runtimeExceptionHandler(RuntimeException e) {
log.error("runtimeException", e);
return ResultUtils.error(ErrorCode.SYSTEM_ERROR, e.getMessage(), "");
}
}

  1. 修改 UserServiceImpl
    注册异常抛出
1
2
3
4
5
6
7
8
9
if (StringUtils.isAnyBlank(userAccount, userPassword, checkPassword, planetCode)) {
throw new BusinessException(ErrorCode.PARAMS_ERROR, "参数为空");
}
if (userAccount.length() < 4) {
throw new BusinessException(ErrorCode.PARAMS_ERROR, "用户账号过短");
}
if (userPassword.length() < 8 || checkPassword.length() < 8) {
throw new BusinessException(ErrorCode.PARAMS_ERROR, "用户密码过短");
}

账号不能重复,修改return -1;

1
throw new BusinessException(ErrorCode.PARAMS_ERROR, "用户账号重复");

后端多环境理论及实战

主要是改

  • 依赖的环境地址
  • 数据库地址
  • 缓存地址
  • 消息队列地址
  • 项目端口号
  • 服务器配置
    Spring Boot 项目通过 application.yml 添加不同的后缀区分配置文件(项目启动时传入环境变量)
    application.yml 公共配置

导出数据表结构
找到数据表,右键 => Navigation => Go to DDL,新建 sql/create_table.sql
将 DDL 中的建表语句粘贴【如果这个数据表不能开源给其他人看,记得不要给出这个 sql 文件】

线上数据库搭建

Idea 测试连接
远程服务器公网 IP 工作台 => 实例与镜像 => 实例 => 实例详情 => 配置信息 => 公网 IP

线上数据库连接成功后配置
◦进入线上数据库控制台,创建数据库、数据表
◦修改 application-prod.yml 数据库配置信息
◦测试本地运行生产环境的项目

原始 Nginx + SpringBoot

参考文章:https://www.bilibili.com/read/cv16179200 -by 鱼皮
需要 Linux 服务器(建议用 CentOS 8+/7.6 以上)
原始部署:什么都自己装

前端

阿里云在线可视化工具:https://ecs-workbench.aliyun.com/
需要 web 服务器:nginx、apache、tomcat

  1. 安装 nginx 服务器
    •用系统自带带软件包管理器快速安装,比如 centos 的 yum
    •自己到官网安装【参考文章 nginx 下载】

依次执行下列命令

1
2
3
4
5
6
7
8
9
10
11
12
# 查看当前所在目录
pwd
/root

# 创建 services 目录,存放项目的依赖和安装包
mkdir services

# 进入 services 目录
cd services

# 列出当前目录所含文件
ls
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# nginx 1.26.2 安装链接
https://nginx.org/download/nginx-1.26.2.tar.gz

# 安装 nginx 命令
curl -o nginx-1.26.2.tar.gz https://nginx.org/download/nginx-1.26.2.tar.gz
ls

# 解压 nginx 安装包
tar -zxvf nginx-1.26.2.tar.gz

# 进入 nginx 目录
cd nginx-1.26.2
ls

# 删除 nginx 安装包——附加不执行
rm -rf nginx-1.26.2
rm -rf nginx-1.26.2.tar.gz nginx-1.26.2.tar.gz
1
2
3
4
5
6
7
8
9
10
11
12
# 检查 nginx 依赖环境是否正常
./configure

# 安装依赖
yum install pcre pcre-devel -y
yum install openssl openssl-devel -y

# 设置系统配置参数
./configure --with-http_ssl_module --with-http_v2_module --with-stream

# 再次检查 nginx 依赖环境是否正常
./configure
1
2
3
4
5
6
7
8
9
# 开始编译
make

# 安装
make install
ls
ls /usr/local/nginx/sbin/nginx

# history 命令可以得到所有执行过的命令
1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 配置环境变量
vim /etc/profile
# shift + g 跳至最后一行 按 o,最后一行插入
export PATH=$PATH:/usr/local/nginx/sbin
# 按下 esc ,输入 :wq 回车,保存并退出

# 使文件生效
source /etc/profile

# 执行
nginx

# 查看启动情况,可以看到 80 端口已被 nginx 占用
netstat -ntlp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 进入 conf 文件
cd conf
ls

# 复制 nginx.conf 文件为 nginx.default.conf
cp nginx.conf nginx.default.conf
ls

# 查看原始配置文件
cat nginx.conf

# 回到 services 目录
cd ..
cd ..
# 或者 cd /root/services

build 前端项目,将 dist 文件夹直接拖过来,失败了
将 dist 文件夹下的所有文件移动到 user-center-front 文件夹下,删除 dist 文件夹
再次访问,403 了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 查看 nignx 进程启动人
ps -ef | grep nginx

# 修改 nginx 启动人为 root
vim nginx.conf
# 按 i 进入编辑模式,修改如下,修改完成按 esc ,输入 :wq ,回车
user root;
worker_processes 1;

#error_log logs/error.log;
#error_log logs/error.log notice;
#error_log logs/error.log info;

#pid logs/nginx.pid;

# 更新配置
nginx -s reload