基于Java的仓库管理系统设计与实现(五)
基于Java的仓库管理系统设计与实现(五)
目前正在编写前端界面,后端接口写了七七八八,在编写前端与后端交互中再进行改动。
前端gitee地址
1. 创建Vue项目
使用vite创建vue项目,并引入pinia持久化插件,还有Element-Plus,按需导入插件等等。具体配置不再记录,前几天Vue3基础篇已经基本都引入了
2. 编写登录界面、首页等,请求后端接口生成菜单。
写的不好,但是够用,要求不高,实在是对这玩意不敏感,不会写就搜搜查查,总有合适的。
登陆界面 login.vue
<!-- 登陆界面 login.vue -->
<template>
<el-row>
<el-col :md="12" :lg="16">
<div>
<h1>欢迎使用</h1>
<h2>欢迎使用 基于Java的仓库后台管理系统</h2>
<h2>作者:十一月</h2>
</div>
</el-col>
<el-col :md="12" :lg="8">
<div v-if="isLoginForm">
<h2>欢迎回来</h2>
<span class="line-text"> 账号密码登录 </span>
<el-form ref="formRef" style="max-width: 35vh" :model="ruleForm" status-icon :rules="rules" label-width="20%">
<el-form-item label="账号" prop="account">
<el-input v-model="ruleForm.account" type="text" placeholder="请输入账号" />
</el-form-item>
<el-form-item label="密码" prop="password">
<el-input v-model="ruleForm.password" type="password" placeholder="请输入密码" show-password />
</el-form-item>
<el-form-item>
<el-checkbox v-model="rememberPassword">记住密码</el-checkbox>
<span style="cursor: pointer" @click="switchToRegister">还没有账号,去注册</span>
</el-form-item>
<el-form-item>
<el-button round style="width: 35vh; font-size: 1.5vh;" type="primary" @click="submitForm">
登录
</el-button>
</el-form-item>
</el-form>
</div>
<div v-else=>
<h2>请先注册</h2>
<el-form ref="registerFormRef" style="max-width: 35vh" :model="registerForm" status-icon :rules="registerRules"
label-width="30%">
<el-form-item label="注册账号" prop="registerAccount">
<el-input v-model="registerForm.registerAccount" type="text" placeholder="请输入注册账号" />
</el-form-item>
<el-form-item label="注册密码" prop="registerPassword">
<el-input v-model="registerForm.registerPassword" type="password" placeholder="请输入注册密码" show-password />
</el-form-item>
<el-form-item label="确认密码" prop="registerReconfirmPassword">
<el-input v-model="registerForm.registerReconfirmPassword" @blur="checkRegisterPassword" type="password"
placeholder="请再次输入注册密码" show-password />
</el-form-item>
<el-form-item label="姓名" prop="name">
<el-input v-model="registerForm.name" type="text" placeholder="请输入真实姓名" />
</el-form-item>
<el-form-item label="手机号" prop="phone">
<el-input v-model="registerForm.phone" type="text" placeholder="请输入注册手机号" maxlength="11" />
</el-form-item>
<el-form-item label="邮箱" prop="email">
<el-input v-model="registerForm.email" type="text" placeholder="请输入注册邮箱" />
</el-form-item>
<el-form-item label="验证码" prop="verificationCode">
<el-input style="width: 13vh;margin-right: 2vh;" v-model="registerForm.verificationCode" type="text"
placeholder="邮箱验证码" />
<el-button type="primary" @click="sendVerificationCode" :disabled="isSendingCode">{{ sendText }}</el-button>
</el-form-item>
<el-form-item>
<span style="cursor: pointer" @click="switchToLogin">已有账号,去登录</span>
</el-form-item>
<el-form-item>
<el-button round style="width: 35vh; font-size: 1.5vh; " type="primary" @click="submitRegisterForm">
注册
</el-button>
</el-form-item>
</el-form>
</div>
</el-col>
</el-row>
</template>
<script setup>
import { useUserStore } from '@/store/user.js';
import { useRouter } from 'vue-router';
import request from '@/utils/request.js';
// 获取 router 实例 编程式导航使用
const router = useRouter();
// 获取 userStore 实例
const userStore = useUserStore();
// 表单组件
const formRef = ref(null);
// 注册表单组件
const registerFormRef = ref(null);
// 记住密码
const rememberPassword = ref(false);
// 是否是登录表单
const isLoginForm = ref(true);
// 是否正在发送验证码
const isSendingCode = ref(false);
// 发送验证码倒计时
const countdown = ref(0);
// 发送验证码按钮文字
const sendText = ref('发送');
// 登录表单字段
const ruleForm = reactive({
account: '',
password: '',
});
// 注册表单字段
const registerForm = reactive({
registerAccount: '',
registerPassword: '',
registerReconfirmPassword: '',
name: '',
phone: '',
email: '',
verificationCode: ''
});
//返回注册
const switchToRegister = () => {
isLoginForm.value = false;
};
//返回登陆
const switchToLogin = () => {
isLoginForm.value = true;
};
// 自定义密码匹配校验函数
const validatePasswordMatch = (rule, value, callback) => {
if (value !== registerForm.registerPassword) {
callback(new Error('两次输入的密码不一致,请重新输入'));
} else {
callback();
}
};
// 手机号校验规则
const validatePhone = (rule, value, callback) => {
const phoneReg = /^1[3-9]\d{9}$/;
if (!phoneReg.test(value)) {
callback(new Error('请输入有效的手机号'));
} else {
callback();
}
};
// 邮箱校验规则
const validateEmail = async (rule, value, callback) => {
const emailReg = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
if (!emailReg.test(value)) {
callback(new Error('请输入有效的邮箱地址'));
} else {
//通过校验,判断邮箱是否存在
try {
const response = await request.post('auth/checkUser', {
email: value
});
if (response.code === 200) {
callback();
} else {
callback(new Error(response.message));
}
} catch (error) {
callback(new Error(response.message));
}
}
};
// 注册账号是否存在校验函数
const checkUserExists = async (rule, value, callback) => {
try {
const response = await request.post('auth/checkUser', {
username: value
});
if (response.code === 200) {
callback();
} else {
callback(new Error(response.message));
}
} catch (error) {
callback(new Error(response.message));
}
};
// 登录表单校验规则
const rules = {
account: [
{ required: true, message: '请输入账号', trigger: 'blur' },
{ min: 4, max: 20, message: '长度在 4 到 20 个字符', trigger: 'blur' },
],
password: [
{ required: true, message: '请输入密码', trigger: 'blur' },
{ min: 6, max: 20, message: '长度在 6 到 20 个字符', trigger: 'blur' },
],
};
// 注册表单校验规则
const registerRules = {
registerAccount: [
{ required: true, message: '请输入注册账号', trigger: 'blur' },
{ min: 4, max: 20, message: '长度在 4 到 20 个字符', trigger: 'blur' },
// 关联校验函数
{ validator: checkUserExists, trigger: 'blur' },
],
registerPassword: [
{ required: true, message: '请输入注册密码', trigger: 'blur' },
{ min: 6, max: 20, message: '长度在 6 到 20 个字符', trigger: 'blur' },
],
registerReconfirmPassword: [
{ required: true, message: '请再次输入注册密码', trigger: 'blur' },
{ min: 6, max: 20, message: '长度在 6 到 20 个字符,和注册密码保持一致', trigger: 'blur' },
// 关联校验函数
{ validator: validatePasswordMatch, trigger: 'blur' },
],
name: [
{ required: true, message: '请输入真实姓名', trigger: 'blur' }
],
phone: [
{ required: true, message: '请输入注册手机号', trigger: 'blur' },
// 关联校验函数
{ validator: validatePhone, trigger: 'blur' }
],
email: [
{ required: true, message: '请输入注册邮箱', trigger: 'blur' },
// 关联校验函数
{ validator: validateEmail, trigger: 'blur' }
],
verificationCode: [
{ required: true, message: '请输入邮箱收到的验证码', trigger: 'blur' }
]
};
// 登陆表单提交
const submitForm = () => {
formRef.value.validate((valid) => {
if (valid) {
handleLogin();
// 这里可以添加登录成功后的逻辑
}
});
};
// 注册表单提交
const submitRegisterForm = () => {
registerFormRef.value.validate((valid) => {
if (valid) {
handleRegister();
// 这里可以添加注册成功后的逻辑
}
});
};
// 检测二次密码是否一致 失去焦点触发校验规则
const checkRegisterPassword = () => {
registerFormRef.value.validateField('registerReconfirmPassword');
};
//发送邮箱验证码
const sendVerificationCode = () => {
if (isSendingCode.value) return;
// 校验邮箱是否有效
registerFormRef.value.validateField('email', (valid) => {
if (valid) {
handleEmailCode()
// 这里需要添加实际发送验证码的逻辑,比如调用 API
console.log(`发送验证码到邮箱: ${registerForm.email}`);
}
});
};
// 发送邮箱验证码处理
const handleEmailCode = async () => {
//验证通过 请求api发送验证码
try {
const req = await request.post('auth/sendEmailCode', {
email: registerForm.email
});
console.log(req);
if (req.code !== 200) {
showMessage('error', req.message);
} else {
//等于200 开始验证码处理
isSendingCode.value = true;
countdown.value = 60;
const timer = setInterval(() => {
countdown.value--;
sendText.value = `(${countdown.value})`;
if (countdown.value === 0) {
clearInterval(timer);
isSendingCode.value = false;
sendText.value = '发送';
}
}, 1000);
showMessage('success', req.message);
}
} catch (error) {
showMessage('error', req.message);
}
}
//登录处理
const handleLogin = async () => {
try {
const response = await request.post('auth/login', {
username: ruleForm.account,
password: ruleForm.password
});
if (response.code === 200) {
const token = response.access_token;
ElMessage({
type: 'success',
message: '登录成功',
duration: 2000,
showClose: true,
onClose: () => {
//登录成功消失后再跳转到首页
if (rememberPassword.value) {
// 记住密码了 就存到 pinia 的 store 里
userStore.setUsers({
account: ruleForm.account,
password: ruleForm.password,
token: token
});
} else {
localStorage.removeItem('user');
}
router.push('/index');
}
});
} else {
showMessage('error', '登录失败,' + response.message);
}
} catch (error) {
showMessage('error', '登录失败' + error.message);
}
}
//注册处理
const handleRegister = async () => {
try {
const response = await request.post('auth/register', {
username: registerForm.registerAccount,
password: registerForm.registerPassword,
realName: registerForm.name,
phone: registerForm.phone,
email: registerForm.email,
code: registerForm.verificationCode
});
if (response.code === 200) {
ElMessage({
type: 'success',
message: '注册成功',
duration: 2000,
showClose: true
});
} else {
showMessage('error', response.message);
}
} catch (error) {
showMessage('error', response.message);
}
}
// 封装消息提示函数,提高代码复用性
const showMessage = (type, message) => {
ElMessage({
type,
message,
duration: 2000,
showClose: true
});
};
// 挂载后触发记住密码输入账号密码
onMounted(() => {
const storedAccount = userStore.users.account;
const storedPassword = userStore.users.password;
if (storedAccount && storedPassword) {
ruleForm.account = storedAccount;
ruleForm.password = storedPassword;
rememberPassword.value = true;
}
});
</script>
<style scoped>
.el-row {
min-height: 100vh;
min-width: 100vh;
background-color: rgb(113, 125, 228);
}
.el-col {
display: flex;
justify-content: center;
align-items: center;
}
.el-col:nth-child(2) {
background-color: #fff;
text-align: center;
}
h1 {
font-size: 5vh;
color: white;
}
.line-text {
position: relative;
display: inline-block;
font-size: 2vh;
color: gray;
padding: 0 10px;
}
.line-text::before,
.line-text::after {
content: '';
position: absolute;
top: 50%;
width: 50px;
height: 1px;
background-color: gray;
}
.line-text::before {
right: 100%;
}
.line-text::after {
left: 100%;
}
h2 {
font-size: 3vh;
}
.el-form {
margin-top: 3vh;
}
.el-checkbox {
margin-right: 1vh;
}
</style>
首页 index.vue
<template>
<el-container class="layout-container-demo" style="height: 100vh">
<el-aside width="200px">
<el-scrollbar>
<AsideMenu :menus="menus" @select="handleMenuSelect" @home-click="switchToHome"
@item-click="handleMenuItemClick" />
</el-scrollbar>
</el-aside>
<el-container>
<el-header style="text-align: right; font-size: 18px">
<div class="toolbar">
<el-dropdown @command="handleUserDropdownCommand">
<div class="user-account" @mouseenter="showUserMenu = true" @mouseleave="showUserMenu = false">
<p>{{ userStore.users.account }}</p>
</div>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item command="profile">个人资料</el-dropdown-item>
<el-dropdown-item command="settings">设置</el-dropdown-item>
<el-dropdown-item command="logout">退出登录</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
</el-header>
<el-main>
<el-tabs v-model="activeTabName" type="card" class="demo-tabs" @edit="handleTabsEdit">
<el-tab-pane v-for="tab in tabs" :key="tab.name" :label="tab.title" :name="tab.name" :closable="tab.closable">
<div class="tab-content">
<p>{{ tab.title }} 的内容</p>
<!-- 你的页面内容 -->
</div>
</el-tab-pane>
</el-tabs>
</el-main>
<el-footer>
<div class="footer-content">
© 2025 十一月 Copyright
</div>
</el-footer>
</el-container>
</el-container>
</template>
<script setup>
import { useUserStore } from '@/store/user';
import { useRouter } from 'vue-router';
import request from '@/utils/request.js';
import AsideMenu from '@/views/asideMenu.vue';
const router = useRouter();
const userStore = useUserStore();
const item = {
date: '2016-05-02',
name: 'Tom',
address: 'No. 189, Grove St, Los Angeles',
};
const tableData = ref(Array.from({ length: 20 }).fill(item));
// 菜单配置
const menus = ref([]);
// 标签页数据
const tabs = ref([
{
title: '首页',
name: '0',
closable: false,
},
]);
let tabIndex = 0
const activeTabName = ref('0');
const showUserMenu = ref(false);
// 菜单交互处理
const handleMenuSelect = (index) => {
if (index === 'close') {
handleSelect("close")
}
};
const handleMenuItemClick = (item) => {
addTab(item.menuName, item.id);
};
// 添加首页跳转方法
const switchToHome = () => {
activeTabName.value = '0';
};
// 添加标签页的方法
const addTab = (title, name) => {
const existingTab = tabs.value.find(tab => tab.name === name);
if (!existingTab) {
tabs.value.push({
title,
name,
closable: true,
})
}
activeTabName.value = name
};
// 标签页编辑事件(修改后)
const handleTabsEdit = (targetName, action) => {
if (action === 'remove') {
// 防止删除首页
if (targetName === '0') return;
const index = tabs.value.findIndex(tab => tab.name === targetName);
if (index > -1) {
tabs.value.splice(index, 1);
}
if (activeTabName.value === targetName) {
const lastTab = tabs.value[tabs.value.length - 1];
if (lastTab) {
activeTabName.value = lastTab.name;
}
}
}
};
// 用户下拉菜单命令处理
const handleUserDropdownCommand = (command) => {
switch (command) {
case 'profile':
console.log('查看个人资料');
break;
case 'settings':
console.log('打开设置');
break;
case 'logout':
console.log('退出登录');
handleSelect("close")
break;
default:
break;
}
};
// 处理菜单选中回调
const handleSelect = (index) => {
console.log(index)
if (index == "close") {
ElMessageBox.confirm('确认退出系统吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
ElMessage({
type: 'success',
message: '退出成功',
duration: 1000,
showClose: true,
onClose: () => {
//清除token
userStore.resetToken();
//登录成功消失后再跳转到首页
router.push('/login');
}
});
}).catch(() => {
showMessage('info', '取消退出')
});
}
}
const getMenuList = async () => {
try {
const response = await request.post('system/menu/list', {
roleId: 1
});
if (response.code === 200) {
const data = response.data;
const menuList = data;
menus.value = menuList;
showMessage('success', response.message);
} else {
showMessage('error', response.message);
}
} catch (error) {
showMessage('error', '获取菜单失败');
}
}
// 封装消息提示函数,提高代码复用性
const showMessage = (type, message) => {
ElMessage({
type,
message,
duration: 2000,
showClose: true
});
};
onMounted(() => {
getMenuList();
})
</script>
<style>
/* 定义全局颜色变量 */
:root {
--global-bg-color: #FFFFFF;
/* 深蓝色背景 */
--aside-bg-color: #F9FAFC;
/* 侧边栏背景色 */
--aside-active-bg-color: #3169a1;
/* 侧边栏展开背景色 */
--header-bg-color: #605CA8;
/* 头部背景色 */
--footer-bg-color: #FFFFFF;
/* 底部背景色 */
--main-bg-color: #FFFFFF;
/* 浅灰色主内容区背景色 */
--text-color: #000;
/* 文本颜色 */
}
/* 整体布局容器 */
.layout-container-demo {
display: flex;
height: 100vh;
background-color: var(--global-bg-color);
}
.layout-container-demo .el-aside {
color: var(--text-color);
background-color: var(--aside-bg-color);
}
.layout-container-demo .custom-menu {
background-color: var(--aside-bg-color);
border-right: none;
}
.layout-container-demo .custom-menu .el-sub-menu__title,
.el-menu-item-group__title,
.el-menu-item {
background-color: var(--aside-bg-color);
color: var(--text-color);
}
.layout-container-demo .custom-menu .el-sub-menu__title .el-menu-item-group__title .el-menu-item {
background-color: var(--aside-active-bg-color);
color: var(--text-color);
}
.layout-container-demo .custom-menu .el-sub-menu__popper {
background-color: var(--aside-bg-color);
}
.layout-container-demo .el-header {
position: relative;
background-color: var(--header-bg-color);
color: var(--text-color);
height: 8%;
}
.layout-container-demo .el-main {
padding: 0;
flex: 1;
display: flex;
flex-direction: column;
background-color: var(--main-bg-color);
}
.layout-container-demo .el-scrollbar {
flex: 1;
}
.layout-container-demo .toolbar {
display: inline-flex;
align-items: center;
justify-content: center;
height: 100%;
}
.layout-container-demo .el-footer {
background-color: var(--footer-bg-color);
display: flex;
justify-content: center;
align-items: center;
height: 4%;
color: var(--text-color);
}
.layout-container-demo .custom-tabs {
flex: 1;
background-color: var(--main-bg-color);
color: #6b778c;
font-size: 32px;
font-weight: 600;
}
.custom-tabs span {
vertical-align: middle;
margin-left: 4px;
}
.layout-container-demo .el-tabs__header {
background-color: var(--main-bg-color);
}
.layout-container-demo .el-tabs__item.is-active {
background-color: var(--active-tab-bg-color);
}
.layout-container-demo .user-account {
display: inline-flex;
align-items: center;
padding: 0 10px;
cursor: pointer;
color: #fff;
}
</style>
菜单页 asideMenu.vue
<template>
<el-menu :default-openeds="['1']" class="custom-menu" @select="handleMenuSelect">
<el-menu-item index="dashboard"
style="justify-content: center;background-color: #555299; min-height: 8vh; font-size: 24px;"
@click="handleHomeClick">仓库管理系统</el-menu-item>
<template v-for="menu in menus" :key="menu.id">
<el-sub-menu v-if="menu.children && menu.children.length > 0" :index="menu.id.toString()">
<template #title>
<el-icon v-if="menu.icon">
<component :is="$icon[menu.icon]" />
</el-icon>
{{ menu.menuName }}
</template>
<el-menu-item-group>
<el-menu-item v-for="child in menu.children" :key="child.id" :index="child.id.toString()"
@click="() => handleMenuItemClick(child)">
<el-icon v-if="child.icon">
<component :is="$icon[child.icon]" />
</el-icon>
{{ child.menuName }}
</el-menu-item>
</el-menu-item-group>
</el-sub-menu>
<el-menu-item v-else :index="menu.id.toString()" @click="() => handleMenuItemClick(menu)">
<template #title>
<el-icon v-if="menu.icon">
<component :is="$icons[menu.icon]" />
</el-icon>
{{ menu.menuName }}
</template>
</el-menu-item>
</template>
<el-menu-item index="close">
<template #title>
<el-icon>
<iEpSwitchButton />
</el-icon>退出系统
</template>
</el-menu-item>
</el-menu>
</template>
<script setup>
import { defineProps, defineEmits } from 'vue';
const props = defineProps({
menus: {
type: Array,
required: true
}
});
const emits = defineEmits(['select', 'home-click', 'item-click']);
const handleMenuSelect = (index) => {
emits('select', index);
};
const handleHomeClick = () => {
emits('home-click');
};
const handleMenuItemClick = (item) => {
emits('item-click', item);
};
</script>
3. 封装Axios请求
src下新建utils文件夹,再创建request.js
import axios from 'axios';
// 创建 axios 实例
const service = axios.create({
baseURL: import.meta.env.VUE_APP_BASE_API, // 确保此处正确
timeout: 5000 // 请求超时时间
});
const token = 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJuYmYiOjE3NDIyODQ1NjMsImV4cCI6MTc0MjM3MDk2MywidXNlcklkIjo1LCJpYXQiOjE3NDIyODQ1NjMsInVzZXJuYW1lIjoiZGFoYWkifQ.bTTJq1jod6gvFYElcyswNf6XkKflUqFW8YzTLJWJ738';
// 请求拦截器
service.interceptors.request.use(
config => {
// 在发送请求之前做些什么
// 例如,添加请求头
// 添加token 先固定 后续从userStore中获取
config.headers['token'] = token;
return config;
},
error => {
// 处理请求错误
console.log(error); // for debug
Promise.reject(error);
}
);
// 响应拦截器
service.interceptors.response.use(
response => {
const res = response.data;
// // 这里可以根据业务需求处理响应数据
// if (res.code !== 200) {
// // 处理错误情况
// console.error(res.message);
// return Promise.reject(new Error(res.message || 'Error'));
// } else {
// return res;
// }
return res;
},
error => {
// 处理响应错误
console.log('err' + error); // for debug
return Promise.reject(error);
}
);
// 封装请求方法
const request = {
get(url, params = {}) {
return service.get(url, { params });
},
post(url, data = {}) {
return service.post(url, data);
},
put(url, data = {}) {
return service.put(url, data);
},
delete(url, params = {}) {
return service.delete(url, { params });
}
};
export default request;
这里用到了
import.meta.env.VUE_APP_BASE_API,确保项目根目录有.env文件,我这里是开发环境,所以文件名为.env.development,里面写API地址
# 注意一定要以VUE_APP 开头
VUE_APP_BASE_API=http://localhost:8080` # 开发环境的 API 地址
配置这里,axios调用的时候,发现请求不对,打印一看没有获取到这个值,百度一顿搜,试了一堆都没有,最终看到了一个需要在
vite.config.js进行一个配置,因为vite不识别,默认识别的是VITE_开头的
export default defineConfig({
//添加这一行就可以了
envPrefix: ['VITE_', 'VUE_APP_'],
//....
}
4. 后端邮件发送配置
- 引入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-mail</artifactId>
</dependency>
- yml文件配置,我这里使用的网易
spring:
mail:
host: smtp.163.com
port: 25
username: 邮箱账号
password: 获取的授权码
properties:
mail.smtp.auth: true
mail.smtp.starttls.enable: true
mail.smtp.starttls.required: true
- 创建EmailVo类
package cn.xy21lin.wms_lin.vo;
import lombok.Data;
@Data
public class EmailVo {
private String from;
private String to;
private String subject;
private String content;
}
- 编写EmailService,对应实现类EmailServiceImpl
package cn.xy21lin.wms_lin.service;
import cn.xy21lin.wms_lin.vo.EmailVo;
public interface EmailService {
void sendMail(EmailVo email);
}
// 实现类
package cn.xy21lin.wms_lin.service.impl;
import cn.xy21lin.wms_lin.service.EmailService;
import cn.xy21lin.wms_lin.vo.EmailVo;
import jakarta.annotation.Resource;
import org.springframework.mail.SimpleMailMessage;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.stereotype.Service;
@Service
public class EmailServiceImpl implements EmailService {
@Resource
private JavaMailSender javamail;
@Override
public void sendMail(EmailVo email) {
// 创建一个简单邮件消息对象。
SimpleMailMessage message = new SimpleMailMessage();
// 设置发件人地址。
message.setFrom(email.getFrom());
// 设置收件人地址。
message.setTo(email.getTo());
// 设置邮件主题。
message.setSubject(email.getSubject());
// 设置邮件内容。
message.setText(email.getContent());
// 设置抄送地址为发件人地址。
message.setCc(email.getFrom());
// 使用注入的 JavaMailSender 发送邮件。
javamail.send(message);
}
}
- 创建MailUtil,添加验证码有效期5分钟、验证验证码、发送邮件逻辑
package cn.xy21lin.wms_lin.util;
import cn.xy21lin.wms_lin.service.EmailService;
import cn.xy21lin.wms_lin.vo.EmailVo;
import jakarta.annotation.Resource;
import lombok.Getter;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Map;
import java.util.Random;
import java.util.concurrent.ConcurrentHashMap;
@Component
public class MailUtil {
@Resource
private EmailService emailService;
private final Map<String, VerificationCodeInfo> verificationCodes = new ConcurrentHashMap<>();
@Value("${spring.mail.username}")
private String from;
public void sendMail(String email) {
String code = generateVerificationCode();
// 记录验证码和过期时间
verificationCodes.put(email, new VerificationCodeInfo(code, LocalDateTime.now().plusMinutes(5)));
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.append("仓库管理系统:")
.append("\n欢迎注册,您的验证码为:")
.append(code)
.append("\n验证码 5 分钟有效,请尽快填写");
EmailVo emailVo = new EmailVo();
emailVo.setFrom(from);
emailVo.setTo(email);
String format = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd"));
emailVo.setSubject(format + "-仓库管理系统注册验证码");
emailVo.setContent(stringBuilder.toString());
emailService.sendMail(emailVo);
}
public boolean verifyCode(String email, String code) {
VerificationCodeInfo info = verificationCodes.get(email);
if (info != null) {
if (LocalDateTime.now().isBefore(info.getExpirationTime()) && info.getCode().equals(code)) {
verificationCodes.remove(email);
return true;
} else {
// 验证码已过期或不正确,移除记录
verificationCodes.remove(email);
}
}
return false;
}
private String generateVerificationCode() {
Random random = new Random();
int code = 100000 + random.nextInt(900000);
return String.valueOf(code);
}
@Getter
// 内部类,用于存储验证码和过期时间
private class VerificationCodeInfo {
private final String code;
private final LocalDateTime expirationTime;
public VerificationCodeInfo(String code, LocalDateTime expirationTime) {
this.code = code;
this.expirationTime = expirationTime;
}
}
}
- Controller编写请求
前端发送邮件验证码请求时,总是超时,但是过了十几秒邮件又收到了,所以我用hutool的线程工具类去发,避免请求阻塞等待发送结果,直接返回发送成功
@Resource
private MailUtil mailUtil;
private static final Map<String, Long> emailSendTimeMap = new ConcurrentHashMap<>();
private static final String EMAIL_REGEX = "^[a-zA-Z0-9_+&*-]+(?:\\.[a-zA-Z0-9_+&*-]+)*@(?:[a-zA-Z0-9-]+\\.)+[a-zA-Z]{2,7}$";
private static final Pattern EMAIL_PATTERN = Pattern.compile(EMAIL_REGEX);
@PostMapping("/sendEmailCode")
@Operation(summary = "发送邮箱验证码", description = "根据邮箱账号发送邮箱验证码")
@ApiResponse(responseCode = "200", description = "发送成功", content = @Content(schema = @Schema(example = "{\"code\": 200, \"message\": \"发送成功\", \"data\": null, \"success\": \"true\"}")))
public Result<?> sendEmailCode(
@RequestBody @Schema(example = "{\"email\": \"571497983@qq.com\"}") String email) {
// 将收到的字符串转JSON 然后获取email属性
// {"email":"571497983@qq.com"}
try{
email = JSONUtil.parseObj(email).getStr("email");
}catch (JSONException exception){
return Result.fail().setMessage("JSON解析失败");
}
if (!StringUtils.isNullOrEmpty(email) && !EMAIL_PATTERN.matcher(email).matches()) {
return Result.fail().setMessage("请输入正确的邮箱账号");
}
// 检查是否在一分钟内发送过
long currentTime = System.currentTimeMillis();
if (emailSendTimeMap.containsKey(email) && currentTime - emailSendTimeMap.get(email) < 60 * 1000) {
return Result.fail().setMessage("发送频繁,1分钟后重试");
}
// 这里发送邮件 前端发送请求,总是超时,但是过了十几秒邮件有发送了,所以我用hutool的线程工具类去发,直接返回发送成功
String finalEmail = email;
ThreadUtil.execAsync(()->{
mailUtil.sendMail(finalEmail);
});
// 更新发送时间
emailSendTimeMap.put(email, currentTime);
return Result.success().setMessage("发送成功");
}
5. git推送
add commit push 记得保存
本文是原创文章,采用 CC BY-NC-ND 4.0 协议,完整转载请注明来自 枫月Blog
评论
匿名评论
隐私政策
你无需删除空行,直接评论以获取最佳展示效果