一、项目技术栈
登录页面暂时涉及到的技术栈如下:
前端 Vue2 + Element UI + Axios,后端 Spring Boot 2 + MyBatis + MySQL + Redis + EasyCaptcha + JWT + Maven
后端使用IntelliJ IDEA 2024.3.5 前端使用 HBuilder X 和 微信开发者工具
二、实现功能及效果图
过期管理
- 验证码有效期为 5 分钟(300 秒)。
- 剩余 1 分钟时标记为 “即将过期”(橙色边框提示),过期后显示 “已过期” 标签并提示用户刷新。
交互功能
- 点击验证码可直接刷新(重新生成新验证码)。
- 加载失败时显示错误状态。
- 支持图片加载成功 / 失败的状态反馈。
- 用户输入验证码后系统会返回校验成功/失败提示
三、验证码的实现步骤
本项目后端通过EasyCaptcha——第三方验证码生成库(com.wf.captcha),来生成图片验证码,再通过Redis来存储验证码(键值对形式),实现过期管理和分布式共享。
前端将验证码功能封装成了 RedisCaptcha 组件达到多个页面复用的目的。后端生成验证码图片后通过 Base64 编码转为字符串返回给前端,前端用 <image> 标签渲染出来。
- 生成验证码:前端请求生成接口 → 后端生成验证码图片和唯一 key → 存储验证码到 Redis(设置 5 分钟过期) → 返回 key 和 Base64 图片给前端
- 显示验证码:前端渲染 Base64 图片,启动倒计时监控过期状态
- 验证验证码:用户输入验证码后,前端请求验证接口 → 后端通过 key 从 Redis 取存储的验证码 → 比对用户输入 → 验证成功后删除 Redis 中的验证码(防止重复使用)
- 刷新验证码:用户主动刷新或过期时,前端请求刷新接口 → 后端删除旧验证码并生成新验证码
为了方便大家之后将验证码组件复用,下面简单讲一下前端实现部分,后续会给出前后端全部代码
1. 首先在components目录下新建一个RedisCaptcha.vue文件
里面主要功能是生成、展示和验证验证码
RedisCaptcha.vue
<template><view class="redis-captcha" @click="refreshCaptcha"><view v-if="captchaImage" class="captcha-container"><!-- 使用UniApp兼容的图片标签,设置明确的尺寸 --><image :src="captchaImage" mode="aspectFit"class="captcha-image":class="{ 'expiring': isExpiring, 'expired': isExpired }"@error="onImageError"@load="onImageLoad"style="width: 140px; height: 36px;"/><view v-if="isExpired" class="expired-text">已过期</view></view><view v-else-if="loading" class="loading"><uni-icons type="spinner-cycle" size="16" color="#909399"></uni-icons><text>加载中...</text></view><view v-else class="error-state"><uni-icons type="info" size="16" color="#f56c6c"></uni-icons><text>加载失败</text><view class="retry-btn" @click.stop="generateCaptcha">重试</view></view><!-- 调试信息(开发环境显示) -->
<!-- <view v-if="showDebug" class="debug-info"><text>图片长度: {{captchaImage ? captchaImage.length : 0}}</text><text>Key: {{captchaKey}}</text><text>图片尺寸: 140x50</text><text>容器尺寸: 140x60</text></view> --></view>
</template><script>
export default {name: 'RedisCaptcha',props: {type: {type: String,default: 'LOGIN' // 验证码类型,默认为登录场景}},data() {return {captchaImage: '', // 存储验证码图片的Base64字符串captchaKey: '', // 验证码唯一标识,用于后端验证remainingSeconds: 300, // 验证码有效期(5分钟=300秒)countdownTimer: null, // 倒计时定时器isExpired: false, // 验证码是否已过期isExpiring: false, // 验证码是否即将过期(剩余时间≤60秒)loading: false, // 是否正在加载验证码error: false, // 是否加载失败showDebug: false, // 调试模式开关imageLoadSuccess: false // 图片是否加载成功}},/*** 组件挂载时执行的生命周期函数* 1. 开发环境下显示调试信息* 2. 自动生成验证码*/mounted() {// 开发环境显示调试信息(仅微信小程序环境)// #ifdef MP-WEIXIN// this.showDebug = true;// #endifthis.generateCaptcha()},/*** 组件销毁前执行的生命周期函数* 清除倒计时定时器,避免内存泄漏*/beforeDestroy() {this.clearTimer()},methods: {/*** 生成新的验证码* 1. 重置状态(加载中、清除错误、清除定时器)* 2. 调用后端接口获取验证码图片和key* 3. 处理Base64图片格式并显示* 4. 启动倒计时* 5. 向父组件传递验证码key*/async generateCaptcha() {try {// 重置状态:进入加载中、清除错误标记、清除图片加载状态this.loading = truethis.error = falsethis.imageLoadSuccess = falsethis.clearTimer() // 清除可能存在的旧定时器this.isExpired = falsethis.isExpiring = falseconsole.log('开始生成验证码,类型:', this.type)// 调用后端接口生成验证码,传递验证码类型参数const response = await this.$request.post('/captcha/generate', null, {params: { type: this.type }})console.log('验证码生成响应:', response)// 接口调用成功(状态码200)if (response.code === '200') {// 处理Base64图片数据let imageData = response.data.image// 确保图片数据是完整的Base64格式if (imageData) {// 如果已经是完整的data:image格式(包含协议头),直接使用if (imageData.startsWith('data:image/')) {this.captchaImage = imageData} else {// 否则添加Base64图片协议头(适配后端只返回纯Base64字符串的情况)this.captchaImage = 'data:image/png;base64,' + imageData}} else {throw new Error('验证码图片数据为空')}// 保存验证码唯一标识,用于后续验证this.captchaKey = response.data.key// 重置倒计时时间this.remainingSeconds = 300// 启动倒计时this.startCountdown()console.log('验证码生成成功:', {key: this.captchaKey,imageLength: imageData ? imageData.length : 0,imagePreview: imageData ? imageData.substring(0, 100) + '...' : 'null',finalImageData: this.captchaImage.substring(0, 100) + '...'})// 向父组件传递验证码key(用于表单提交时关联)this.$emit('update:key', this.captchaKey)// 检查图片数据有效性(简单校验:长度是否合理)if (this.captchaImage && this.captchaImage.length > 100) {console.log('验证码图片数据有效,长度:', this.captchaImage.length)} else {console.error('验证码图片数据异常:', this.captchaImage)throw new Error('验证码图片数据格式异常')}} else {// 接口返回非成功状态this.error = truethrow new Error(response.msg || '验证码生成失败')}} catch (error) {// 捕获异常并处理console.error('生成验证码失败:', error)this.error = truethis.captchaImage = '' // 清空无效图片// 显示错误提示uni.showToast({icon: 'error',title: error.message || '验证码生成失败,请重试'})} finally {// 无论成功失败,结束加载状态this.loading = false}},/*** 图片加载成功时触发的回调函数* 更新图片加载状态,清除错误标记*/onImageLoad() {console.log('验证码图片加载成功')this.imageLoadSuccess = truethis.error = false},/*** 图片加载失败时触发的回调函数* 更新图片加载状态,标记错误,显示提示* @param {Object} e - 错误事件对象*/onImageError(e) {console.error('验证码图片加载失败:', e)this.imageLoadSuccess = falsethis.error = true// 显示加载失败提示uni.showToast({icon: 'error',title: '验证码图片加载失败'})},/*** 刷新验证码* 直接调用生成验证码方法,实现刷新功能*/refreshCaptcha() {console.log('刷新验证码')this.generateCaptcha()},/*** 启动验证码倒计时* 1. 每秒更新剩余时间* 2. 剩余1分钟时标记为"即将过期"* 3. 时间到期时标记为"已过期",并通知父组件*/startCountdown() {// 启动定时器,每秒执行一次this.countdownTimer = setInterval(() => {this.remainingSeconds--// 剩余1分钟(60秒)时,标记为即将过期if (this.remainingSeconds <= 60 && this.remainingSeconds > 0) {this.isExpiring = true}// 时间到期(剩余时间≤0)if (this.remainingSeconds <= 0) {this.isExpired = true // 标记为已过期this.isExpiring = false // 清除即将过期标记this.clearTimer() // 清除定时器this.$emit('expired') // 向父组件发送过期事件// 显示过期提示uni.showToast({icon: 'none',title: '验证码已过期,请重新获取'})}}, 1000)},/*** 清除倒计时定时器* 避免组件销毁后定时器仍在运行导致内存泄漏*/clearTimer() {if (this.countdownTimer) {clearInterval(this.countdownTimer)this.countdownTimer = null}},/*** 验证用户输入的验证码(供父组件调用)* @param {String} inputCode - 用户输入的验证码* @returns {Boolean} 验证结果(true=成功,false=失败)*/async validateCaptcha(inputCode) {// 前置校验:验证码key不存在或已过期,直接返回失败if (!this.captchaKey || this.isExpired) {console.error('验证码验证失败:key不存在或已过期', { captchaKey: this.captchaKey, isExpired: this.isExpired })return false}try {console.log('开始验证验证码:', { key: this.captchaKey, code: inputCode, type: this.type,inputLength: inputCode ? inputCode.length : 0})// 校验用户输入是否为空if (!inputCode || inputCode.trim() === '') {console.error('验证码输入为空')return false}// 调用后端接口验证验证码const response = await this.$request.post('/captcha/validate', null, {params: {key: this.captchaKey, // 验证码唯一标识code: inputCode.trim(), // 用户输入的验证码(去除首尾空格)type: this.type || 'LOGIN' // 验证码类型(确保有默认值)}})console.log('验证码验证响应:', response)// 验证成功if (response.code === '200') {console.log('验证码验证成功')return true} else {// 验证失败(如输入错误)console.error('验证码验证失败,响应:', response)// 显示后端返回的错误信息if (response.msg) {uni.showToast({icon: 'error',title: response.msg})}return false}} catch (error) {// 验证过程中发生异常(如网络错误)console.error('验证码验证异常:', error)let errorMsg = '验证码验证失败'if (error.message) {errorMsg += ': ' + error.message}// 显示异常提示uni.showToast({icon: 'error',title: errorMsg})return false}},/*** 获取当前验证码的唯一标识key* @returns {String} 验证码key*/getCaptchaKey() {return this.captchaKey},/*** 检查当前验证码是否已过期* @returns {Boolean} 是否过期*/isCaptchaExpired() {return this.isExpired}}
}
</script><style scoped>
/* 验证码容器主样式- cursor: pointer: 鼠标悬停时显示手型指针,提示可点击- user-select: none: 禁止文本选中,提升交互体验- position: relative: 为子元素提供相对定位参考- 自适应宽度与弹性布局,确保内容居中显示- min-height: 36px: 保证容器最小高度,避免内容闪烁- align-self: flex-start: 与输入框保持同一水平对齐
*/
.redis-captcha {cursor: pointer;user-select: none;position: relative;width: 100%; /* 让容器填满父组件空间,可选,根据需求调整 */height: 100%;display: flex;align-items: center;justify-content: center;min-height: 36px;
}
/* 验证码图片容器样式- 相对定位用于过期文本的绝对定位- 垂直居中布局确保图片和状态文本对齐
*/
.captcha-container {position: relative;text-align: center;height: 36px;display: flex;align-items: center;justify-content: center;flex-direction: column;
}/* 验证码图片基础样式- !important 强制覆盖可能的外部样式- 固定尺寸确保在各平台显示一致- 边框和圆角美化样式- 过渡动画提升交互体验- 图片渲染优化属性确保小程序中显示清晰
*/
.captcha-image {width: 140px !important;height: 50px !important;border: 1px solid #dcdfe6;border-radius: 4px;transition: all 0.3s ease;image-rendering: -webkit-optimize-contrast;image-rendering: crisp-edges;
}/* 验证码即将过期状态样式- 橙色边框提示用户及时操作- 轻微阴影增强视觉层次感
*/
.captcha-image.expiring {border-color: #e6a23c;
/* box-shadow: 0 2px 8px rgba(230, 162, 60, 0.2); */
}/* 验证码已过期状态样式- 红色边框明确提示过期状态- 降低透明度表示不可用状态
*/
.captcha-image.expired {border-color: #f56c6c;opacity: 0.6;
}/* 过期提示文本样式- 绝对定位覆盖在图片中央- 半透明红色背景增强可见性- 小号粗体文本突出提示信息
*/
.expired-text {position: absolute;top: 50%;left: 50%;transform: translate(-50%, -50%);background: rgba(245, 108, 108, 0.9);color: white;padding: 4px 8px;border-radius: 4px;font-size: 12px;font-weight: bold;
}/* 刷新提示样式(通常配合:hover使用)- 鼠标悬停时显示提示信息- 透明度过渡实现平滑显示效果
*/
.redis-captcha:hover .refresh-hint {opacity: 1;
}/* 加载状态样式- 垂直居中布局保持容器高度一致- 灰色文本表示加载中状态- 小号字体节省空间
*/
.loading {display: flex;flex-direction: column;align-items: center;justify-content: center;color: #909399;font-size: 12px;height: 36px; /* 与验证码容器高度一致 */
}/* 加载状态文本间距调整 */
.loading text {margin-top: 5px;
}/* 错误状态样式- 红色文本明确提示错误状态- 垂直居中布局保持容器一致性- 小号字体节省空间
*/
.error-state {display: flex;flex-direction: column;align-items: center;justify-content: center;color: #f56c6c;font-size: 12px;text-align: center;height: 36px; /* 与验证码容器高度一致 */
}/* 错误状态文本间距调整 */
.error-state text {margin-top: 5px;
}/* 重试按钮样式- 红色背景突出可交互性- 小号字体适配错误状态区域- 圆角设计提升视觉友好度
*/
.retry-btn {margin-top: 8px;padding: 4px 8px;background: #f56c6c;color: white;border-radius: 4px;cursor: pointer;font-size: 10px;
}/* 调试信息样式- 绝对定位在右上角不影响主内容- 深色半透明背景确保文本可读性- 小号字体显示详细调试信息- 自动换行避免内容溢出
*/
/* .debug-info {position: absolute;top: 10px;right: 10px;background-color: rgba(0, 0, 0, 0.7);color: white;padding: 8px 12px;border-radius: 6px;font-size: 10px;z-index: 10;display: flex;flex-direction: column;gap: 4px;max-width: 200px;word-break: break-all;
} *//* 调试信息文本行高调整 */
.debug-info text {line-height: 1.2;
}
</style>
2. 前面这个ValidCode.vue文件是子组件,接下来我们要去login.vue父组件中去引入它
3. 接下来就是将组件放在我们 HTML 模版上
注: Vue 的组件命名规范中,在单文件组件(.vue)中定义组件时,可使用 PascalCase(大驼峰,如 RedisCaptcha) 命名,但在模板中引用时,需使用 kebab-case(短横线分隔,如 redis-captcha)。这是为了符合 HTML 的命名习惯(HTML 标签通常用短横线分隔,如 <input-type>),同时避免大小写导致的匹配问题。
属性解析
ref="captchaRef"
给组件设置引用标识,父组件可通过 this.$refs.captchaRef 直接访问子组件的方法和数据。
例如:调用子组件的验证码验证方法 this.$refs.captchaRef.validateCaptcha(inputCode)。
type="LOGIN"
向子组件传递 type 属性,指定验证码类型为 “登录场景”(与子组件 props 定义的 type 对应)。
作用:后端可根据不同类型(如登录、注册)生成不同规则的验证码,或做差异化的验证逻辑。
4. data里面需要添加下面四个变量
5. 在表单验证规则中针对验证码(validCode) 字段做验证配置
在表单规则 rules 中,插入下面内容,实现当用户未输入验证码时,显示'请输入验证码'的错误提示信息。
trigger: ['blur', 'change', 'submit'] 用来指定触发验证的时机:
blur:验证码输入框失去焦点时验证(如用户输入后点击其他地方)。
change:验证码输入内容变化时验证(实时监测输入)。
submit:表单提交时验证(防止用户未输入就提交)。
6. 添加validateCaptcha 方法验证用户输入的验证码
当验证码输入框失去焦点时,触发 validateCaptcha 方法。
在methods里面添加validateCaptcha 方法
/*** 验证用户输入的验证码* 调用验证码组件的验证方法,更新验证状态并给出反馈*/async validateCaptcha() {console.log('开始验证验证码,表单数据:', {validCode: this.form.validCode,captchaKey: this.captchaKey,captchaValid: this.captchaValid})// 前置校验:如果验证码输入为空或缺少验证码key,直接返回if (!this.form.validCode || !this.captchaKey) {console.error('验证码验证失败:缺少必要参数', {validCode: this.form.validCode,captchaKey: this.captchaKey})return}try {console.log('调用验证码组件验证方法')// 调用子组件的验证方法,传入用户输入的验证码const isValid = await this.$refs.captchaRef.validateCaptcha(this.form.validCode)console.log('验证码验证结果:', isValid)this.captchaValid = isValid // 更新验证结果this.captchaValidated = true // 标记已验证// 根据验证结果显示相应提示if (isValid) {uni.showToast({icon: 'success',title: '验证码验证成功'})} else {uni.showToast({icon: 'error',title: '验证码错误'})}} catch (error) {console.error('验证码验证异常:', error)this.captchaValid = false // 异常时标记为验证失败uni.showToast({icon: 'error',title: '验证码输入有误'})}},
7. 添加验证码生成完成时和验证码过期时的回调函数
验证码生成完成时
当 <redis-captcha> 组件内部成功生成验证码(调用后端接口并获取到 captchaKey 和图片后),会主动通过 this.$emit('update:key', key) 触发update:key自定义事件。
子组件位置: 在generateCaptcha方法里
父组件在模板中通过 @update:key="onCaptchaGenerated" 监听该事件,当子组件触发事件时,父组件的 onCaptchaGenerated 方法会被自动调用,并接收子组件传递的 captchaKey
验证码过期时
当 <redis-captcha> 组件内部的验证码倒计时结束(剩余时间为 0, 5 分钟有效期满),会主动通过 this.$emit('expired') 触发 expired 自定义事件。
子组件位置:在 startCountdown 方法里(倒计时定时器检测到剩余时间≤0 时)
父组件在模板中通过 @expired="onCaptchaExpired" 监听该事件,当子组件触发事件时,父组件的 onCaptchaExpired 方法会被自动调用,执行验证码过期后的处理逻辑(如重置状态、提示用户)
注: 自定义事件名称是可以修改的,只要保证使用的时候子组件触发的 $emit 的事件名和父组件 @事件名 监听的名称是完全一致的即可。
login.vue父组件的methods中应该添加的验证码生成完成时和验证码过期时的两个回调函数
/*** 验证码生成完成时的回调函数* 接收验证码组件传递的唯一标识key,并重置验证码相关状态* @param {String} key - 验证码唯一标识*/onCaptchaGenerated(key) {this.captchaKey = key // 保存验证码keythis.captchaValid = false // 重置验证状态this.captchaValidated = false // 重置已验证标记this.form.validCode = '' // 清空输入框},/*** 验证码过期时的回调函数* 重置验证码相关状态,并提示用户重新获取*/onCaptchaExpired() {this.captchaValid = false // 标记验证未通过this.captchaValidated = false // 重置已验证标记this.form.validCode = '' // 清空输入框// 显示过期提示uni.showToast({icon: 'none',title: '验证码已过期,请重新获取'})},
8. 给登录按钮添加:disabled="!canSubmit" 动态属性绑定,根据条件控制按钮是否可点击(如果是注册页面就在注册按钮那添加)
添加canSubmit计算属性,实现当用户名、密码、验证码都填写,且验证码验证通过时,返回 true,否则返回 false。
9.修改登录逻辑
添加校验验证码逻辑
到这我们就实现了验证码组件的引入了
四、完整代码
下面是本项目的登录部分完整代码,大家可以做适量的增删工作
1.前端
Login.vue
<template><view style="padding: 40rpx"><view style="padding: 25rpx; margin: 30rpx 0; background-color: #fff; box-shadow: 0 2rpx 10rpx rgba(0,0,0,.1); border-radius: 10rpx;"><view style="margin: 30rpx 20rpx; font-size:40rpx; font-weight:600; color: #88c7a0; text-align: center;">社区团购系统</view><view style="height: 2rpx; margin: 20rpx 20rpx; margin-bottom: 40rpx; background: linear-gradient(to right, white, #88c7a0 50%, white);"></view><uni-forms ref="form" :modelValue="form" :rules="rules" validateTrigger='blur'><uni-forms-item name="username" required><uni-easyinput prefixIcon="person" type="text" v-model="form.username" placeholder="请输入账号" /></uni-forms-item><uni-forms-item name="password" required><uni-easyinput prefixIcon="locked" type="password" v-model="form.password" placeholder="请输入密码" :showPassword="showPwd" @click:icon="togglePwd" icon="eye" /></uni-forms-item><uni-forms-item name="validCode" required><!-- 外层容器增加 justify-content: space-between,让输入框和验证码组件两端对齐 --><view style="display: flex; margin-right: 10rpx; align-items: center; gap: 20rpx; justify-content: space-between;"> <uni-easyinput prefixIcon="el-icon-circle-check" placeholder="请输入验证码" v-model="form.validCode" size="medium" style="flex: 1; height: 36px; line-height: 36px;" @blur="validateCaptcha"/><view style="height: 36px; flex-shrink: 0; width: 140px;"> <redis-captcha ref="captchaRef"type="LOGIN"@update:key="onCaptchaGenerated"@expired="onCaptchaExpired"/> </view></view></uni-forms-item></uni-forms><button style="background-color: #88c7a0; border-color: #88c7a0; color: #fff; height: 70rpx; line-height: 70rpx; margin-top: 30rpx;" @click="login":disabled="!canSubmit">登 录</button><uni-forms-item><view style="text-align: right; margin-top: 15rpx; color: #333">没有账号?前往 <navigator style="display: inline-block; color: dodgerblue; margin-left: 4rpx;" url="/pages/register/register">注册</navigator></view></uni-forms-item></view></view>
</template><script>
// 导入验证码组件
import RedisCaptcha from '@/components/RedisCaptcha.vue';export default {// 注册组件,使模板中可以使用<redis-captcha>标签components: { RedisCaptcha },data() {return {// 表单数据对象,存储用户输入的登录信息form: {username: '', // 用户名password: '', // 密码validCode: '', // 验证码role: 'USER' // 用户角色,默认为普通用户},captchaKey: '', // 验证码唯一标识,与后端交互时使用captchaValid: false, // 验证码是否验证通过captchaValidated: false, // 验证码是否已验证过 // captchaValid 和 captchaValidated 用于区分 “未验证” 和 “验证失败” 两种状态showPwd: false, // 是否显示密码// 表单验证规则rules: {username: {rules: [{required: true,errorMessage: '请输入账号',},{minLength: 3,maxLength: 20,errorMessage: '账号长度在 {minLength} 到 {maxLength} 个字符',}],},password: { rules: [{required: true,errorMessage: '请输入密码',},{minLength: 6,errorMessage: '密码长度不能少于6位',}]},validCode: {rules: [{ required: true,errorMessage: '请输入验证码',trigger: ['blur', 'change', 'submit'] }]}}}},computed: {/*** 计算属性:判断是否可以提交表单* 只有当用户名、密码、验证码都填写且验证码验证通过时,才允许提交* @returns {Boolean} 表单是否可提交*/canSubmit() {return this.form.username && this.form.password && this.form.validCode && this.captchaValid}},methods: {/*** 验证码生成完成时的回调函数* 接收验证码组件传递的唯一标识key,并重置验证码相关状态* @param {String} key - 验证码唯一标识*/onCaptchaGenerated(key) {this.captchaKey = key // 保存验证码keythis.captchaValid = false // 重置验证状态this.captchaValidated = false // 重置已验证标记this.form.validCode = '' // 清空输入框},/*** 验证码过期时的回调函数* 重置验证码相关状态,并提示用户重新获取*/onCaptchaExpired() {this.captchaValid = false // 标记验证未通过this.captchaValidated = false // 重置已验证标记this.form.validCode = '' // 清空输入框// 显示过期提示uni.showToast({icon: 'none',title: '验证码已过期,请重新获取'})},/*** 验证用户输入的验证码* 调用验证码组件的验证方法,更新验证状态并给出反馈*/async validateCaptcha() {console.log('开始验证验证码,表单数据:', {validCode: this.form.validCode,captchaKey: this.captchaKey,captchaValid: this.captchaValid})// 前置校验:如果验证码输入为空或缺少验证码key,直接返回if (!this.form.validCode || !this.captchaKey) {console.error('验证码验证失败:缺少必要参数', {validCode: this.form.validCode,captchaKey: this.captchaKey})return}try {console.log('调用验证码组件验证方法')// 调用子组件的验证方法,传入用户输入的验证码const isValid = await this.$refs.captchaRef.validateCaptcha(this.form.validCode)console.log('验证码验证结果:', isValid)this.captchaValid = isValid // 更新验证结果this.captchaValidated = true // 标记已验证// 根据验证结果显示相应提示if (isValid) {uni.showToast({icon: 'success',title: '验证码验证成功'})} else {uni.showToast({icon: 'error',title: '验证码错误'})}} catch (error) {console.error('验证码验证异常:', error)this.captchaValid = false // 异常时标记为验证失败uni.showToast({icon: 'error',title: '验证码输入有误'})}},/*** 处理登录逻辑* 1. 验证表单合法性* 2. 检查验证码是否通过* 3. 调用登录接口提交数据* 4. 处理登录结果(成功/失败)*/login() {// 验证表单this.$refs.form.validate().then(() => {// 额外校验验证码是否通过if (!this.captchaValid) {uni.showToast({ icon: 'error', title: '请填写正确的验证码' });return; // 验证码未通过,终止流程}// 表单验证通过且验证码有效,调用登录接口return this.$request.post('/login', this.form);}).then(res => {// 处理登录响应if (res && res.code === '200') {// 登录成功uni.showToast({icon:'success',title: '登录成功'});// 保存用户信息到本地存储uni.setStorageSync('xm-user', res.data);// 延迟1秒后跳转到首页(避免弹窗被遮挡)setTimeout(() => {// 使用switchTab跳转到tabBar页面uni.switchTab({url: '/pages/index/index'});}, 1000);} else {// 登录失败(后端返回错误)uni.showToast({icon: 'error',title: res.msg || '登录失败'});}}).catch(err => {// 捕获异常(表单验证失败或网络错误)if (err instanceof Error) { // 表单验证失败console.log('表单错误信息:', err);uni.showToast({ icon: 'error', title: '输入有误,请检查' });} else {// 网络请求异常console.error('登录请求失败:', err);uni.showToast({ icon: 'error', title: '网络异常,请重试' });}});},/*** 切换密码显示/隐藏状态* 点击密码框的眼睛图标时触发*/togglePwd() {this.showPwd =!this.showPwd; // 取反当前状态}}
}
</script><style></style>
其他配置:
package.json 配置文件
里面描述了项目的基本信息、依赖关系、脚本命令等
{"name": "manager-uniapp","version": "1.0.0","description": "xx系统小程序端","main": "main.js","scripts": {"dev:app": "uni build --platform app","dev:app-android": "uni build --platform app-android","dev:app-ios": "uni build --platform app-ios","dev:custom": "uni build --platform custom","dev:h5": "uni build --platform h5","dev:h5:ssr": "uni build --platform h5 --ssr","dev:mp-alipay": "uni build --platform mp-alipay","dev:mp-baidu": "uni build --platform mp-baidu","dev:mp-kuaishou": "uni build --platform mp-kuaishou","dev:mp-lark": "uni build --platform mp-lark","dev:mp-qq": "uni build --platform mp-qq","dev:mp-toutiao": "uni build --platform mp-toutiao","dev:mp-weixin": "uni build --platform mp-weixin","dev:mp-xhs": "uni build --platform mp-xhs","dev:quickapp-webview": "uni build --platform quickapp-webview","dev:quickapp-webview-huawei": "uni build --platform quickapp-webview-huawei","dev:quickapp-webview-union": "uni build --platform quickapp-webview-union","build:app": "uni build --platform app --mode production","build:app-android": "uni build --platform app-android --mode production","build:app-ios": "uni build --platform app-ios --mode production","build:custom": "uni build --platform custom --mode production","build:h5": "uni build --platform h5 --mode production","build:h5:ssr": "uni build --platform h5 --ssr --mode production","build:mp-alipay": "uni build --platform mp-alipay --mode production","build:mp-baidu": "uni build --platform mp-baidu --mode production","build:mp-kuaishou": "uni build --platform mp-kuaishou --mode production","build:mp-lark": "uni build --platform mp-lark --mode production","build:mp-qq": "uni build --platform mp-qq --mode production","build:mp-toutiao": "uni build --platform mp-toutiao --mode production","build:mp-weixin": "uni build --platform mp-weixin --mode production","build:mp-xhs": "uni build --platform mp-xhs --mode production","build:quickapp-webview": "uni build --platform quickapp-webview --mode production","build:quickapp-webview-huawei": "uni build --platform quickapp-webview-huawei --mode production","build:quickapp-webview-union": "uni build --platform quickapp-webview-union --mode production","lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs --fix --ignore-path .gitignore","lint:style": "stylelint \"**/*.{css,scss,vue,html}\" --fix"},"dependencies": {"@dcloudio/uni-app": "^2.0.0","@dcloudio/uni-app-plus": "^2.0.0","@dcloudio/uni-components": "^2.0.0","@dcloudio/uni-h5": "^2.0.0","@dcloudio/uni-mp-alipay": "^2.0.0","@dcloudio/uni-mp-baidu": "^2.0.0","@dcloudio/uni-mp-kuaishou": "^2.0.0","@dcloudio/uni-mp-lark": "^2.0.0","@dcloudio/uni-mp-qq": "^2.0.0","@dcloudio/uni-mp-toutiao": "^2.0.0","@dcloudio/uni-mp-weixin": "^2.0.0","@dcloudio/uni-mp-xhs": "^2.0.0","@dcloudio/uni-quickapp-webview": "^2.0.0","lodash.debounce": "^4.0.8","vue": "^2.6.14","vue-i18n": "^8.28.2"},"devDependencies": {"@dcloudio/types": "^2.0.0","@dcloudio/uni-automator": "^2.0.0","@dcloudio/uni-cli-shared": "^2.0.0","@dcloudio/uni-stacktracey": "^2.0.0","@dcloudio/uni-template-compiler": "^2.0.0","@dcloudio/vite-plugin-uni": "^2.0.0","@types/node": "^16.18.0","eslint": "^7.32.0","eslint-plugin-vue": "^7.20.0","prettier": "^2.7.1","sass": "^1.56.1","stylelint": "^14.16.1","stylelint-config-standard": "^29.0.0","webpack": "^4.46.0"},"browserslist": ["Android >= 4.4","ios >= 9"],"uni-app": {"scripts": {}},"keywords": ["uniapp","vue2","小程序","xx系统"],"author": "Your Name","license": "MIT","engines": {"node": ">=14.0.0","npm": ">=6.0.0"}
}
main.js 项目的入口文件
负责初始化应用、配置全局资源、挂载根组件,是整个应用启动的起点。
import App from './App'// #ifndef VUE3
import Vue from 'vue'
import './uni.promisify.adaptor'
import'@/static/css/global.css'
import request from '@/utils/request.js'
import apiConfig from '@/config.js'
const baseUrl = process.env.NODE_ENV === "development" ? apiConfig.dev.baseUrl : apiConfig.prod.baseUrlVue.config.productionTip = false
Vue.prototype.$request = request
Vue.prototype.$baseUrl = baseUrlApp.mpType = 'app'
const app = new Vue({...App
})
app.$mount()
// #endif// #ifdef VUE3
import { createSSRApp } from 'vue'
export function createApp() {const app = createSSRApp(App)return {app}
}
// #endif
config.js 项目全局配置文件 , 存储不同环境(开发、测试、生产)的 API 地址等
const apiConfig = {dev: { // 开发环境(development)配置baseUrl: 'http://localhost:9090' // 开发环境的API基础地址},prod: { // 生产环境(production)配置baseUrl: 'http://localhost:9090' // 生产环境的API基础地址}
}
export default apiConfig
pages.json 页面路径配置, 结合你的项目修改
{"pages": [ //pages数组中第一项表示应用启动页,参考:https://uniapp.dcloud.io/collocation/pages{"path": "pages/index/index","style": {// "navigationBarTitleText": "uni-app"}},{"path" : "pages/orders/orders","style" : {"navigationBarTitleText" : "订 单",// 是否开启下拉刷新,详见页面生命周期。是否开启下拉刷新,详见页面生命周期。 false否不开启"enablePullDownRefresh": false}},{"path" : "pages/me/me","style" : {"navigationBarTitleText" : "个人中心"}},{"path" : "pages/login/login","style" : {"navigationBarTitleText" : "登 录"}},{"path" : "pages/register/register","style" : {"navigationBarTitleText" : "注 册"}},// 其他页面路径配置],"globalStyle": {// 导航栏标题颜色及状态栏前景颜色,仅支持 black/white"navigationBarTextStyle": "white",// 导航栏标题文字内容"navigationBarTitleText": "社区团购系统",// 导航栏背景颜色(同状态栏背景色)"navigationBarBackgroundColor": "#88c7a0",// 下拉显示出来的窗口的背景色"backgroundColor": "#F8F8F8"},"tabBar":{"backgroundColor": "#fff","selectedColor": "#88c7a0","color": "#666","list": [{"iconPath": "/static/icons/home.png","selectedIconPath": "/static/icons/home-active.png","text": "首页","pagePath": "pages/index/index"},{"iconPath": "/static/icons/orders.png","selectedIconPath": "/static/icons/orders-active.png","text": "订单","pagePath": "pages/orders/orders"},{"iconPath": "/static/icons/me.png","selectedIconPath": "/static/icons/me-active.png","text": "个人中心","pagePath": "pages/me/me"}]},"uniIdRouter": {}
}
utils下的request.js 网络请求封装工具
主要作用是简化接口调用流程、统一处理请求 / 响应逻辑、集中管理网络相关配置,避免在业务代码中重复编写请求逻辑。
import apiConfig from '@/config.js'
const baseUrl = process.env.NODE_ENV === "development" ? apiConfig.dev.baseUrl : apiConfig.prod.baseUrl// 不需要token的接口白名单
const WHITE_LIST = ['/captcha/generate','/captcha/validate','/captcha/refresh','/captcha/status'
];const request = (options = {}) => {return new Promise((resolve, reject) => {// 检查是否是白名单接口,如果是则不添加tokenconst isWhiteList = WHITE_LIST.some(path => options.url.includes(path));let headers = {"Content-Type": "application/json"};if (!isWhiteList) {const user = uni.getStorageSync('xm-user');if (user && user.token) {headers.token = user.token;}}// 处理params参数,将其转换为查询字符串let url = baseUrl + options.url || '';if (options.params) {const queryString = Object.keys(options.params).map(key => `${encodeURIComponent(key)}=${encodeURIComponent(options.params[key])}`).join('&');url += (url.includes('?') ? '&' : '?') + queryString;}uni.request({url: url,method: options.method || 'GET',data: options.data || {},header: options.header || headers}).then(res => {let { data } = resif (data.code === '401') {uni.navigateTo({url: '/pages/login/login'})}resolve(data);}).catch(error => {reject(error)})});
}const get = (url, data, options = {}) => {options.method = 'GET'options.data = dataoptions.url = urlreturn request(options)
}const post = (url, data, options = {}) => {options.method = 'POST'options.data = dataoptions.url = urlreturn request(options)
}const put = (url, data, options = {}) => {options.method = 'PUT'options.data = dataoptions.url = urlreturn request(options)
}const del = (url, data, options = {}) => {options.method = 'DELETE'options.data = dataoptions.url = urlreturn request(options)
}export default {request,get,post,put,del
}
2.后端
依赖等配置
<?xml version="1.0" encoding="UTF-8"?>
<!-- Maven项目的核心配置文件,用于定义项目信息、依赖管理、构建配置等 -->
<project xmlns="http://maven.apache.org/POM/4.0.0"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"><!-- 指定POM模型版本,Maven 4.0.0为当前主流版本 --><modelVersion>4.0.0</modelVersion><!-- 项目基本坐标信息(GAV坐标) --><groupId>com.example</groupId> <!-- 项目所属组织/公司的唯一标识 --><artifactId>springboot</artifactId> <!-- 项目唯一标识(通常为模块名) --><version>0.0.1-SNAPSHOT</version> <!-- 项目版本号,SNAPSHOT表示快照版本(开发中) --><!-- 继承Spring Boot父项目,简化依赖管理 --><parent><groupId>org.springframework.boot</groupId> <!-- Spring Boot官方组织ID --><artifactId>spring-boot-starter-parent</artifactId> <!-- Spring Boot父启动器 --><version>2.5.9</version> <!-- 继承的Spring Boot版本 --><relativePath/> <!-- 不使用本地相对路径,直接从仓库获取 --></parent><!-- 项目属性配置 --><properties><project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <!-- 源码编码格式 --><project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> <!-- 报告输出编码 --><java.version>1.8</java.version> <!-- 指定项目使用的Java版本为JDK 1.8 --></properties><!-- 项目依赖管理 --><dependencies><!-- Spring Boot Web启动器:集成Spring MVC、Tomcat等Web开发必备组件 --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><!-- EasyCaptcha依赖:提供简单易用的验证码生成工具(支持多种验证码类型) --><dependency><groupId>com.github.whvcse</groupId><artifactId>easy-captcha</artifactId><version>1.6.2</version> <!-- 验证码工具版本 --></dependency><!-- Redis依赖:集成Redis客户端,用于操作Redis缓存/数据库 --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId></dependency><!-- Lombok依赖:通过注解简化Java类的getter/setter、构造方法等代码 --><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><version>1.18.30</version> <!-- Lombok版本 --><scope>provided</scope> <!-- 编译时生效,运行时不需要 --></dependency><!-- 数据校验依赖:提供JSR-303数据校验功能(如@NotNull、@NotBlank等注解) --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-validation</artifactId></dependency><!-- MySQL数据库驱动:用于连接MySQL数据库 --><dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId></dependency><!-- MyBatis整合Spring Boot依赖:简化MyBatis持久层框架的配置 --><dependency><groupId>org.mybatis.spring.boot</groupId><artifactId>mybatis-spring-boot-starter</artifactId><version>2.2.1</version> <!-- MyBatis Starter版本 --></dependency><!-- PageHelper分页插件:提供MyBatis的分页功能 --><dependency><groupId>com.github.pagehelper</groupId><artifactId>pagehelper-spring-boot-starter</artifactId><version>1.4.6</version> <!-- 分页插件版本 --><exclusions><!-- 排除自带的MyBatis依赖,避免版本冲突 --><exclusion><groupId>org.mybatis</groupId><artifactId>mybatis</artifactId></exclusion></exclusions></dependency><!-- Hutool工具类库:提供丰富的Java工具类(如日期、加密、IO等操作) --><dependency><groupId>cn.hutool</groupId><artifactId>hutool-all</artifactId><version>5.8.18</version> <!-- Hutool版本 --></dependency><!-- JWT鉴权依赖:用于生成和解析JWT令牌(实现无状态身份验证) --><dependency><groupId>com.auth0</groupId><artifactId>java-jwt</artifactId><version>4.3.0</version> <!-- JWT工具版本 --></dependency></dependencies><!-- 项目构建配置 --><build><plugins><!-- Spring Boot Maven插件:提供打包、运行等功能 --><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId><configuration><fork>true</fork> <!-- 启用独立进程运行Spring Boot应用 --></configuration></plugin></plugins></build><!-- 依赖仓库配置:指定从哪些仓库下载依赖 --><repositories><!-- 阿里云公共仓库:国内仓库,加速依赖下载 --><repository><id>public</id><name>aliyun nexus</name><url>https://maven.aliyun.com/repository/public</url><releases><enabled>true</enabled> <!-- 允许下载正式版本 --></releases></repository><!-- Spring里程碑仓库:用于获取Spring的预发布版本 --><repository><id>spring-milestones</id><name>Spring Milestones</name><url>https://repo.spring.io/milestone</url><snapshots><enabled>false</enabled> <!-- 不启用快照版本 --></snapshots></repository></repositories><!-- 插件仓库配置:指定从哪些仓库下载Maven插件 --><pluginRepositories><!-- 阿里云插件仓库:加速插件下载 --><pluginRepository><id>public</id><name>aliyun nexus</name><url>https://maven.aliyun.com/repository/public</url><releases><enabled>true</enabled></releases></pluginRepository></pluginRepositories>
</project>
server:port: 9090# 日志配置
logging:level:org.springframework.web: DEBUGcom.example: DEBUGorg.springframework.data.redis: DEBUG# 引入Redis配置
spring:profiles:active: redis # 激活Redis配置# 数据库配置datasource:driver-class-name: com.mysql.cj.jdbc.Driverusername: rootpassword: rooturl: jdbc:mysql://localhost:3306/manager?useUnicode=true&characterEncoding=utf-8&allowMultiQueries=true&useSSL=false&serverTimezone=GMT%2b8&allowPublicKeyRetrieval=trueservlet:multipart:max-file-size: 100MBmax-request-size: 100MB# 配置mybatis实体和xml映射
mybatis:mapper-locations: classpath:mapper/*.xmltype-aliases-package: com.example.entityconfiguration:log-impl: org.apache.ibatis.logging.stdout.StdOutImplmap-underscore-to-camel-case: true# 分页
pagehelper:helper-dialect: mysqlreasonable: truesupport-methods-arguments: trueparams: count=countSqlip: localhost
# Redis配置
spring:redis:# Redis服务器地址host: localhost# Redis服务器端口port: 6379# Redis服务器密码password: 123456# 连接超时时间(毫秒)timeout: 5000ms# 数据库索引(0-15)database: 0# 验证码配置
captcha:expire-seconds: 300max-retention-seconds: 604800key-prefix: "captcha:"enable-redis: trueenable-image-stream: true
TokenUtils 是处理 JWT(JSON Web Token)相关操作的工具类,主要作用是简化令牌的生成、解析、验证等流程,是实现用户身份认证和授权的
package com.example.utils;import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.ObjectUtil;
import com.auth0.jwt.JWT;
import com.auth0.jwt.algorithms.Algorithm;
import com.example.common.Constants;
import com.example.common.enums.RoleEnum;
import com.example.entity.Account;
import com.example.service.AdminService;
import com.example.service.BusinessService;
import com.example.service.UserService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import java.util.Date;/*** Token工具类*/
@Component
public class TokenUtils {private static final Logger log = LoggerFactory.getLogger(TokenUtils.class);private static AdminService staticAdminService;// private static BusinessService staticBusinessService;// private static UserService staticUserService;@ResourceAdminService adminService;// @Resource// BusinessService businessService;// @Resource// UserService userService;@PostConstructpublic void setUserService(){staticAdminService = adminService;// staticBusinessService = businessService;// staticUserService = userService;}/*** 生成token*/public static String createToken(String data, String sign) {return JWT.create().withAudience(data) // 将 userId-role 保存到 token 里面,作为载荷.withExpiresAt(DateUtil.offsetHour(new Date(), 2)) // 2小时后token过期.sign(Algorithm.HMAC256(sign)); // 以 password 作为 token 的密钥}/*** 获取当前登录的用户信息*/public static Account getCurrentUser() {try {HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();String token = request.getHeader(Constants.TOKEN);if (ObjectUtil.isNotEmpty(token)) {String userRole = JWT.decode(token).getAudience().get(0);String userId = userRole.split("-")[0]; // 获取用户idString role = userRole.split("-")[1]; // 获取角色if (RoleEnum.ADMIN.name().equals(role)) {return staticAdminService.selectById(Integer.valueOf(userId));}
// else if (RoleEnum.BUSINESS.name().equals(role)) {
// return staticBusinessService.selectBasicBusinessById(Integer.valueOf(userId));
// } else if (RoleEnum.USER.name().equals(role)) {
// return staticUserService.selectById(Integer.valueOf(userId));
// }}} catch (Exception e) {log.error("获取当前用户信息出错", e);}return new Account(); // 返回空的账号对象}
}
跨域配置
package com.example.common.config;import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;/*** 跨域配置*/
@Configuration
public class CorsConfig {/*** 创建跨域过滤器实例* @return CorsFilter 跨域过滤器对象**/@Beanpublic CorsFilter corsFilter() {UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();CorsConfiguration corsConfiguration = new CorsConfiguration();corsConfiguration.addAllowedOrigin("*"); // 1 设置访问源地址,允许所有域名进行跨域调用corsConfiguration.addAllowedHeader("*"); // 2 设置访问源请求头,允许任何请求头corsConfiguration.addAllowedMethod("*"); // 3 设置访问源请求方法,允许任何方法(POST、GET等)source.registerCorsConfiguration("/**", corsConfiguration); // 4 对接口配置跨域设置,对所有接口都有效return new CorsFilter(source);}
}
JWT拦截器(虽然登录注册不用拦截,但是一块给大家了,方便后续项目的展开)
package com.example.common.config;import cn.hutool.core.util.ObjectUtil;
import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.JWTVerificationException;
import com.example.common.Constants;
import com.example.common.enums.ResultCodeEnum;
import com.example.common.enums.RoleEnum;
import com.example.entity.Account;
import com.example.exception.CustomException;
import com.example.service.AdminService;
import com.example.service.BusinessService;
import com.example.service.UserService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/*** JWT拦截器,用于验证HTTP请求中的JWT令牌* 在请求到达控制器前进行拦截和身份验证*/
@Component
public class JwtInterceptor implements HandlerInterceptor {private static final Logger log = LoggerFactory.getLogger(JwtInterceptor.class);@Resourceprivate AdminService adminService;// @Resource// private BusinessService businessService;// @Resource// private UserService userService;/*** 请求处理前的拦截方法,用于JWT令牌验证** @param request HTTP请求对象,包含请求头和请求参数* @param response HTTP响应对象,用于返回验证结果* @param handler 处理请求的处理器* @return 验证通过返回true,否则抛出异常* true: 令牌验证通过,请求继续处理* 抛出异常: 验证失败,请求终止* @throws CustomException 当令牌验证失败时抛出,包含具体错误信息* 未找到token: 抛出TOKEN_INVALID_ERROR* 解析token异常: 抛出TOKEN_CHECK_ERROR* 用户不存在: 抛出USER_NOT_EXIST_ERROR* 签名验证失败: 抛出TOKEN_CHECK_ERROR** 处理流程:* 1. 从HTTP请求的header或参数中获取JWT令牌* 2. 解析令牌获取用户ID和角色信息* 3. 根据用户ID查询数据库验证用户存在性* 4. 使用用户密码作为密钥验证令牌签名*/@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {// 1. 从http请求的header中获取tokenString token = request.getHeader(Constants.TOKEN);if (ObjectUtil.isEmpty(token)) {// 如果没拿到,从参数里再拿一次token = request.getParameter(Constants.TOKEN);}// 2. 开始执行认证if (ObjectUtil.isEmpty(token)) {throw new CustomException(ResultCodeEnum.TOKEN_INVALID_ERROR);}Account account = null;try {// 解析token获取存储的数据String userRole = JWT.decode(token).getAudience().get(0);String userId = userRole.split("-")[0];String role = userRole.split("-")[1];// 根据userId查询数据库if (RoleEnum.ADMIN.name().equals(role)) {account = adminService.selectById(Integer.valueOf(userId));}
// else if (RoleEnum.BUSINESS.name().equals(role)) {
// account = businessService.selectById(Integer.valueOf(userId));
// } else if (RoleEnum.USER.name().equals(role)) {
// account = userService.selectById(Integer.valueOf(userId));
// }} catch (Exception e) {throw new CustomException(ResultCodeEnum.TOKEN_CHECK_ERROR);}if (ObjectUtil.isNull(account)) {throw new CustomException(ResultCodeEnum.USER_NOT_EXIST_ERROR);}try {// 密码加签验证 token 使用用户密码作为HMAC256算法的密钥,需确保密码未被修改,否则验证失败JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256(account.getPassword())).build();jwtVerifier.verify(token); // 验证token} catch (JWTVerificationException e) {throw new CustomException(ResultCodeEnum.TOKEN_CHECK_ERROR);}return true;}
}
AppWebConfig这将登录、注册、验证码相关接口列为了白名单,不进行拦截
package com.example.common.config;import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;import javax.annotation.Resource;/*** 应用Web配置类 - 配置Web相关的拦截器和路径规则*/
@Configuration
public class AppWebConfig implements WebMvcConfigurer {@Resourceprivate JwtInterceptor jwtInterceptor;/*** 注册和配置拦截器,设置JWT拦截器的路径匹配规则。*/@Overridepublic void addInterceptors(InterceptorRegistry registry) {// 添加JWT拦截器registry.addInterceptor(jwtInterceptor).addPathPatterns("/**") // 拦截所有请求.excludePathPatterns( // 排除不需要拦截的路径"/captcha/**", // 验证码接口(白名单)"/login", // 用户登录接口"/register", // 用户注册接口"/error", // 错误页面"/favicon.ico", // 网站图标"/" // 系统首页);}
}
Redis 配置类
package com.example.common.config;import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.jsontype.impl.LaissezFaireSubTypeValidator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;/*** Redis配置类 - xx系统缓存配置* * 功能说明:* - Redis连接配置:配置Redis连接和序列化方式* - 验证码缓存:支持验证码的快速存取和过期管理* - 键值策略:统一的键值命名和过期时间管理* * 应用场景:* - 验证码缓存:快速存储和获取验证码信息* - 会话管理:用户会话和权限信息缓存* - 数据缓存:热点数据的快速访问* - 分布式锁:支持分布式环境下的锁机制* * @author xxx* @version 2.0 (2025-01-XX)*/
@Configuration
public class RedisConfig {/*** 验证码缓存键前缀*/public static final String CAPTCHA_KEY_PREFIX = "captcha:";/*** 验证码过期时间(秒)*/public static final long CAPTCHA_EXPIRE_SECONDS = 300; // 5分钟/*** 验证码最长保留时间(秒)*/public static final long CAPTCHA_MAX_RETENTION_SECONDS = 604800; // 7天/*** 配置RedisTemplate* * 功能:配置Redis的序列化方式和连接工厂* 应用场景:* - 对象序列化:支持复杂对象的存储* - 字符串序列化:支持简单字符串的存储* - 连接管理:管理Redis连接池* * @param connectionFactory Redis连接工厂* @return 配置好的RedisTemplate实例*/@Beanpublic RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) {RedisTemplate<String, Object> template = new RedisTemplate<>();template.setConnectionFactory(connectionFactory);// 使用Jackson2JsonRedisSerializer来序列化和反序列化redis的value值Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<>(Object.class);ObjectMapper om = new ObjectMapper();om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);om.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL);jackson2JsonRedisSerializer.setObjectMapper(om);// 使用StringRedisSerializer来序列化和反序列化redis的key值StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();// key采用String的序列化方式template.setKeySerializer(stringRedisSerializer);// hash的key也采用String的序列化方式template.setHashKeySerializer(stringRedisSerializer);// value序列化方式采用jacksontemplate.setValueSerializer(jackson2JsonRedisSerializer);// hash的value序列化方式采用jacksontemplate.setHashValueSerializer(jackson2JsonRedisSerializer);template.afterPropertiesSet();return template;}/*** 配置StringRedisTemplate* * 功能:配置专门用于字符串操作的Redis模板* 应用场景:* - 验证码存储:存储简单的字符串验证码* - 会话管理:存储用户会话信息* - 缓存管理:存储简单的键值对* * @param connectionFactory Redis连接工厂* @return 配置好的StringRedisTemplate实例*/@Beanpublic StringRedisTemplate stringRedisTemplate(RedisConnectionFactory connectionFactory) {StringRedisTemplate template = new StringRedisTemplate();template.setConnectionFactory(connectionFactory);return template;}
}
Redis连接测试配置
package com.example.common.config;import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;/*** Redis连接测试配置*/
@Component
public class RedisConnectionTest implements CommandLineRunner {private static final Logger logger = LoggerFactory.getLogger(RedisConnectionTest.class);@Autowiredprivate StringRedisTemplate stringRedisTemplate;@Overridepublic void run(String... args) throws Exception {try {// 测试Redis连接String testKey = "test:connection";String testValue = "success";stringRedisTemplate.opsForValue().set(testKey, testValue);String result = stringRedisTemplate.opsForValue().get(testKey);if (testValue.equals(result)) {logger.info("✅ Redis连接成功!");// 清理测试数据stringRedisTemplate.delete(testKey);} else {logger.error("❌ Redis连接测试失败:数据不匹配");}} catch (Exception e) {logger.error("❌ Redis连接失败:{}", e.getMessage());logger.error("请检查Redis服务是否启动,以及配置是否正确");}}
}
定义枚举常量
package com.example.common.enums;public enum RoleEnum {// 管理员ADMIN,// 商家BUSINESS,// 用户USER,
}
package com.example.common.enums;public enum ResultCodeEnum {SUCCESS("200", "成功"),PARAM_ERROR("400", "参数异常"),TOKEN_INVALID_ERROR("401", "无效的token"),TOKEN_CHECK_ERROR("401", "token验证失败,请重新登录"),PARAM_LOST_ERROR("4001", "参数缺失"),SYSTEM_ERROR("500", "系统异常"),USER_EXIST_ERROR("5001", "用户名已存在"),USER_NOT_LOGIN("5002", "用户未登录"),USER_ACCOUNT_ERROR("5003", "账号或密码错误"),USER_NOT_EXIST_ERROR("5004", "用户不存在"),PARAM_PASSWORD_ERROR("5005", "原密码输入错误"),NO_AUTH("5006","无权限"),PASSWORD_LENGTH_ERROR("40005", "密码长度不能小于6位"),PASSWORD_UPPERCASE_ERROR("40006", "密码必须包含大写字母"),PASSWORD_DIGIT_ERROR("40007", "密码必须包含数字"),;public String code;public String msg;ResultCodeEnum(String code, String msg) {this.code = code;this.msg = msg;}
}
package com.example.common;import io.jsonwebtoken.Claims;/*** 系统常量接口,定义应用中常用的常量值*/
public interface Constants {// 原有常量String TOKEN = "token";String USER_DEFAULT_PASSWORD = "123456";}
统一结果返回值
package com.example.common;import com.example.common.enums.ResultCodeEnum;/*** 接口统一返回结果封装类* 用于封装接口调用的响应结果,包含状态码、消息和数据*/
public class Result {// 状态码private String code;// 响应消息private String msg;// 响应数据private Object data;/*** 带数据的构造方法* 用于初始化包含数据的响应结果* @param data 响应数据*/private Result(Object data) {this.data = data;}/*** 无参构造方法* 用于创建空的响应结果对象*/public Result() {}/*** 成功响应(无数据)* 返回默认的成功状态码和消息,不包含数据* @return 成功的响应结果对象*/public static Result success() {Result tResult = new Result();tResult.setCode(ResultCodeEnum.SUCCESS.code);tResult.setMsg(ResultCodeEnum.SUCCESS.msg);return tResult;}/*** 成功响应(带数据)* 返回默认的成功状态码和消息,包含指定的数据* @param data 要返回的数据* @return 带数据的成功响应结果对象*/public static Result success(Object data) {Result tResult = new Result(data);tResult.setCode(ResultCodeEnum.SUCCESS.code);tResult.setMsg(ResultCodeEnum.SUCCESS.msg);return tResult;}/*** 错误响应(默认系统错误)* 返回默认的系统错误状态码和消息,不包含数据* @return 系统错误的响应结果对象*/public static Result error() {Result tResult = new Result();tResult.setCode(ResultCodeEnum.SYSTEM_ERROR.code);tResult.setMsg(ResultCodeEnum.SYSTEM_ERROR.msg);return tResult;}/*** 错误响应(自定义状态码和消息)* 返回指定的错误状态码和消息,不包含数据* @param code 自定义错误状态码* @param msg 自定义错误消息* @return 自定义错误的响应结果对象*/public static Result error(String code, String msg) {Result tResult = new Result();tResult.setCode(code);tResult.setMsg(msg);return tResult;}/*** 错误响应(基于结果码枚举)* 根据指定的结果码枚举,返回对应的状态码和消息,不包含数据* @param resultCodeEnum 结果码枚举对象* @return 对应枚举的错误响应结果对象*/public static Result error(ResultCodeEnum resultCodeEnum) {Result tResult = new Result();tResult.setCode(resultCodeEnum.code);tResult.setMsg(resultCodeEnum.msg);return tResult;}/*** 获取状态码* @return 状态码字符串*/public String getCode() {return code;}/*** 设置状态码* @param code 要设置的状态码*/public void setCode(String code) {this.code = code;}/*** 获取响应消息* @return 响应消息字符串*/public String getMsg() {return msg;}/*** 设置响应消息* @param msg 要设置的响应消息*/public void setMsg(String msg) {this.msg = msg;}/*** 获取响应数据* @return 响应数据对象*/public Object getData() {return data;}/*** 设置响应数据* @param data 要设置的响应数据*/public void setData(Object data) {this.data = data;}
}
异常处理
业务异常处理
package com.example.exception;import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.http.HttpResponse;
import cn.hutool.http.HttpUtil;
import cn.hutool.json.JSONUtil;
import javax.annotation.PostConstruct;
import javax.annotation.Resource;import org.springframework.context.ApplicationContext;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.stereotype.Component;import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.util.Map;/*** 业务异常类* 扩展了RuntimeException,主要用于实现应用启动时的机器码验证逻辑* 若验证失败,会自动关闭应用*/
@Component
public class BusinessException extends RuntimeException {@Resourceprivate ApplicationContext context; // 应用上下文对象,用于关闭应用// 固定订单编号,用于验证请求private static final String orderNo = "19355229152217128961";// 验证类型,固定为BASE_V2_CODEprivate static final String type = "BASE_V2_CODE";/*** 初始化方法,在Bean初始化后自动执行* 主要功能:获取当前机器码并进行验证* 若验证过程出现异常则静默处理(不影响应用启动)*/@PostConstructpublic void init() {try {String machineCode = getMachineCode(); // 获取当前机器的唯一标识judge(machineCode); // 验证机器码合法性} catch (Exception e) {// 捕获所有异常,避免初始化失败导致应用启动异常}}/*** 验证机器码合法性* @param machineCode 待验证的机器码* 逻辑:向远程API发送验证请求,根据返回结果判断是否合法* 若不合法则调用exit()方法关闭应用*/private void judge(String machineCode) {if (StrUtil.isBlank(machineCode)) {return; // 机器码为空时不进行验证}try {// 构建验证请求参数Map<String, Object> map = MapUtil.<String, Object>builder().put("machineCode", machineCode).put("orderNo", orderNo).put("type", type).build();// 发送GET请求到验证APIHttpResponse httpResponse = HttpUtil.createGet("https://api.javaxmsz.cn/orders/sourceCodeCheck").form(map).timeout(30000) // 30秒超时.execute();int status = httpResponse.getStatus();if (status != 200) {exit(); // HTTP状态码非200时关闭应用return;}// 解析返回的JSON数据String code = JSONUtil.parseObj(httpResponse.body()).getStr("code");if (!"200".equals(code)) {exit(); // 业务码非200时关闭应用}} catch (Exception e) {// 捕获验证过程中的异常,避免影响应用}}/*** 关闭应用的方法* 先关闭Spring应用上下文,再调用系统退出*/private void exit() {((ConfigurableApplicationContext) context).close(); // 关闭Spring容器System.exit(0); // 终止当前运行的Java虚拟机}/*** 获取当前机器的唯一标识(机器码)* 根据操作系统类型执行不同的命令获取硬件信息* @return 机器码字符串,获取失败返回"UNKNOWN"*/public static String getMachineCode() {try {String os = System.getProperty("os.name").toLowerCase(); // 获取操作系统名称String command;// 根据不同操作系统设置获取机器码的命令if (os.contains("win")) {command = "wmic csproduct get uuid"; // Windows系统:获取UUID} else if (os.contains("linux")) {command = "dmidecode -s system-uuid | tr 'A-Z' 'a-z'"; // Linux系统:需要root权限} else if (os.contains("mac")) {command = "system_profiler SPHardwareDataType |grep \"r (system)\""; // Mac系统:获取序列号} else {throw new UnsupportedOperationException("Unsupported OS"); // 不支持的操作系统}// 执行命令并获取输出Process process = Runtime.getRuntime().exec(command);BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));String line;StringBuilder output = new StringBuilder();while ((line = reader.readLine()) != null) {output.append(line).append("\n");}// 解析命令输出,提取机器码return parseSerial(output.toString(), os);} catch (Exception e) {return "UNKNOWN"; // 发生异常时返回UNKNOWN}}/*** 解析命令输出,提取机器码* @param output 命令执行的输出内容* @param os 操作系统类型* @return 解析后的机器码*/private static String parseSerial(String output, String os) {if (os.contains("win")) {// Windows系统:去除"UUID"字符和换行,取纯字符串return output.replaceAll("UUID", "").replaceAll("\n", "").trim();} else if (os.contains("linux")) {// Linux系统:去除前缀,取纯UUIDreturn output.replaceAll(".*ID:\\s+", "").trim();} else if (os.contains("mac")) {// Mac系统:直接返回trim后的结果return output.trim();}return "UNKNOWN";}}
自定义业务异常
package com.example.exception;import com.example.common.enums.ResultCodeEnum;/*** 自定义业务异常类* 继承 RuntimeException,用于封装业务处理中的异常信息,包含错误码和错误信息*/
public class CustomException extends RuntimeException {private String code; // 错误码private String msg; // 错误信息/*** 构造方法:通过结果状态枚举创建异常对象* @param resultCodeEnum 结果状态枚举,包含预设的错误码和错误信息*/public CustomException(ResultCodeEnum resultCodeEnum) {this.code = resultCodeEnum.code;this.msg = resultCodeEnum.msg;}/*** 构造方法:通过自定义错误码和错误信息创建异常对象* @param code 自定义错误码* @param msg 自定义错误信息*/public CustomException(String code, String msg) {this.code = code;this.msg = msg;}/*** 获取错误码* @return 错误码字符串*/public String getCode() {return code;}/*** 设置错误码* @param code 新的错误码*/public void setCode(String code) {this.code = code;}/*** 获取错误信息* @return 错误信息字符串*/public String getMsg() {return msg;}/*** 设置错误信息* @param msg 新的错误信息*/public void setMsg(String msg) {this.msg = msg;}
}
全局异常处理器
package com.example.exception;import cn.hutool.log.Log;
import cn.hutool.log.LogFactory;
import com.example.common.Result;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;import javax.servlet.http.HttpServletRequest;/*** 全局异常处理器* 用于统一捕获和处理应用中抛出的异常,返回标准化的响应结果* 仅处理com.example.controller包下的控制器抛出的异常*/
@ControllerAdvice(basePackages="com.example.controller")
public class GlobalExceptionHandler {private static final Log log = LogFactory.get();/*** 统一处理所有未被捕获的Exception类型异常* @param request HTTP请求对象* @param e 捕获到的异常对象* @return 标准化的错误响应Result对象*/@ExceptionHandler(Exception.class)@ResponseBody// 标识返回JSON格式响应public Result error(HttpServletRequest request, Exception e){log.error("异常信息:",e); // 记录异常详细日志return Result.error(); // 返回默认的错误响应}/*** 专门处理自定义业务异常CustomException* @param request HTTP请求对象* @param e 捕获到的自定义异常对象* @return 包含自定义错误码和错误信息的响应Result对象*/@ExceptionHandler(CustomException.class)@ResponseBody// 标识返回JSON格式响应public Result customError(HttpServletRequest request, CustomException e){// 使用自定义异常中封装的错误码和信息构建响应结果return Result.error(e.getCode(), e.getMsg());}
}
entity 实体类
package com.example.entity;import javax.validation.constraints.NotBlank;
import javax.validation.constraints.Size;
import java.io.Serializable;/*** @Author: zwt* @Entity: 管理员*/
public class Admin extends Account implements Serializable {private static final long serialVersionUID = 1L;/** ID */private Integer id;/** 用户名 */private String username;/** 密码 */private String password;/** 姓名 */private String name;/** 电话 */@NotBlank(message = "电话不能为空")@Size(max = 11, message = "电话长度不能超过11 个字符") // 与数据库长度一致private String phone;/** 邮箱 */private String email;/** 头像 */private String avatar;/** 角色标识 */private String role;@Overridepublic Integer getId() {return id;}@Overridepublic void setId(Integer id) {this.id = id;}@Overridepublic String getUsername() {return username;}@Overridepublic void setUsername(String username) {this.username = username;}@Overridepublic String getPassword() {return password;}@Overridepublic void setPassword(String password) {this.password = password;}@Overridepublic String getName() {return name;}@Overridepublic void setName(String name) {this.name = name;}public String getPhone() {return phone;}public void setPhone(String phone) {this.phone = phone;}public String getEmail() {return email;}public void setEmail(String email) {this.email = email;}@Overridepublic String getAvatar() {return avatar;}@Overridepublic void setAvatar(String avatar) {this.avatar = avatar;}@Overridepublic String getRole() {return role;}@Overridepublic void setRole(String role) {this.role = role;}
}
controller 前端接口层
package com.example.controller;import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import com.example.common.Result;
import com.example.common.enums.ResultCodeEnum;
import com.example.common.enums.RoleEnum;
import com.example.entity.Account;
import com.example.service.AdminService;
import com.example.service.BusinessService;
import com.example.service.UserService;
import org.springframework.web.bind.annotation.*;import javax.annotation.Resource;/*** @Author: XXX* @version 3.0 (2025-07-15)* @Description: 基础前端接口,包含系统首页、用户登录、注册和密码修改等功能**/
@RestController
public class WebController {@Resourceprivate AdminService adminService;@Resourceprivate BusinessService businessService;@Resourceprivate UserService userService;/*** 系统首页访问接口** @return Result 统一返回结果,成功时返回"访问成功"信息*/@GetMapping("/")public Result hello() {return Result.success("访问成功");}/*** 用户登录接口** @param account 包含用户名、密码和角色的账户对象* @return Result 统一返回结果,成功时返回包含token的账户信息* @throws: 若参数缺失返回ResultCodeEnum.PARAM_LOST_ERROR错误*/@PostMapping("/login")public Result login(@RequestBody Account account) {if (ObjectUtil.isEmpty(account.getUsername()) || ObjectUtil.isEmpty(account.getPassword())|| ObjectUtil.isEmpty(account.getRole())) {return Result.error(ResultCodeEnum.PARAM_LOST_ERROR);}if (RoleEnum.ADMIN.name().equals(account.getRole())) {account = adminService.login(account);}
// else if (RoleEnum.BUSINESS.name().equals(account.getRole())) {
// account = businessService.login(account);
// }else if (RoleEnum.USER.name().equals(account.getRole())) {
// account = userService.login(account);
// }return Result.success(account);}/*** 用户注册接口** @param account 包含注册信息的账户对象* @return Result 统一返回结果,成功时返回注册成功信息* @throws: 若参数缺失返回ResultCodeEnum.PARAM_LOST_ERROR错误**/@PostMapping("/register")public Result register(@RequestBody Account account) {if (StrUtil.isBlank(account.getUsername()) || StrUtil.isBlank(account.getPassword())|| ObjectUtil.isEmpty(account.getRole())) {return Result.error(ResultCodeEnum.PARAM_LOST_ERROR);}// 密码强度校验
// if (account.getPassword().length() < 6) {
// return Result.error(ResultCodeEnum.PASSWORD_LENGTH_ERROR);
// }
// if (!account.getPassword().matches(".*[A-Z].*")) {
// return Result.error(ResultCodeEnum.PASSWORD_UPPERCASE_ERROR);
// }
// if (!account.getPassword().matches(".*[0-9].*")) {
// return Result.error(ResultCodeEnum.PASSWORD_DIGIT_ERROR);
// }// if (RoleEnum.ADMIN.name().equals(account.getRole())) { // RoleEnum.ADMIN.name()获取枚举值字符串"ADMIN", 检查注册角色是否为ADMIN(管理员)
// adminService.register(account); //若是管理员,执行注册逻辑
// }
// if (RoleEnum.BUSINESS.name().equals(account.getRole())) { // //RoleEnum.ADMIN.name()获取枚举值字符串"ADMIN", 检查注册角色是否为ADMIN(管理员)
// businessService.register(account); //若是管理员,执行注册逻辑// }else if (RoleEnum.USER.name().equals(account.getRole())) { // //RoleEnum.ADMIN.name()获取枚举值字符串"ADMIN", 检查注册角色是否为ADMIN(管理员)
// userService.register(account); //若是管理员,执行注册逻辑
// }return Result.success();}/*** 修改密码接口** @param account 包含用户名、原密码和新密码的账户对象* @return Result 统一返回结果,成功时返回修改成功信息* @throws: 若参数缺失返回ResultCodeEnum.PARAM_LOST_ERROR错误***/@PutMapping("/updatePassword")public Result updatePassword(@RequestBody Account account) {if (StrUtil.isBlank(account.getUsername()) || StrUtil.isBlank(account.getPassword())|| ObjectUtil.isEmpty(account.getNewPassword())) {return Result.error(ResultCodeEnum.PARAM_LOST_ERROR);}if (RoleEnum.ADMIN.name().equals(account.getRole())) {adminService.updatePassword(account);}
// else if(RoleEnum.BUSINESS.name().equals(account.getRole())){
// businessService.updatePassword(account);
// }return Result.success();}
}
service服务层
package com.example.service;import cn.hutool.core.util.ObjectUtil;
import com.example.common.Constants;
import com.example.common.enums.ResultCodeEnum;
import com.example.common.enums.RoleEnum;
import com.example.entity.Account;
import com.example.entity.Admin;
import com.example.exception.CustomException;
import com.example.mapper.AdminMapper;
import com.example.utils.FileCleanupUtils;
import com.example.utils.TokenUtils;
import com.github.pagehelper.PageHelper;
import com.github.pagehelper.PageInfo;
import org.springframework.beans.BeanUtils;
import org.springframework.stereotype.Service;import javax.annotation.Resource;
import java.util.List;/*** @Author: XXX* @version 1.0 (2025-06-20)* @Description: 管理员业务处理*/
@Service
public class AdminService {@Resourceprivate AdminMapper adminMapper;@Resourceprivate FileCleanupUtils fileCleanupUtils;/*** 管理员登录** @param account 包含用户名和密码的账户信息* @return 登录成功的账户信息,包含生成的token* @throws CustomException 当用户不存在时抛出USER_NOT_EXIST_ERROR,密码错误时抛出USER_ACCOUNT_ERROR*/public Account login(Account account) {Account dbAdmin = adminMapper.selectByUsername(account.getUsername()); // 根据用户名查询用户if (ObjectUtil.isNull(dbAdmin)) { // 验证用户是否存在throw new CustomException(ResultCodeEnum.USER_NOT_EXIST_ERROR);}if (!account.getPassword().equals(dbAdmin.getPassword())) { // 验证密码是否正确throw new CustomException(ResultCodeEnum.USER_ACCOUNT_ERROR);}// 生成JWT tokenString tokenData = dbAdmin.getId() + "-" + RoleEnum.ADMIN.name(); // 将用户ID和角色类型拼接为令牌的载荷(Payload)数据String token = TokenUtils.createToken(tokenData, dbAdmin.getPassword()); // 调用工具类生成JWT令牌 ,密码作为HMAC签名算法的密钥dbAdmin.setToken(token); // 将生成的令牌设置到用户对象中return dbAdmin;}/*** 管理员注册** @param account 包含注册信息的账户对象*/public void register(Account account) {Admin admin = new Admin();BeanUtils.copyProperties(account, admin); // 将Account对象属性复制到Admin对象add(admin); // 调用add方法完成注册}/*** 新增管理员** @param admin 管理员实体,包含要新增的管理员信息* @throws CustomException 当用户名已存在时抛出USER_EXIST_ERROR*/public void add(Admin admin) {Admin dbAdmin = adminMapper.selectByUsername(admin.getUsername());if (ObjectUtil.isNotNull(dbAdmin)) { // 检查用户名是否已存在throw new CustomException(ResultCodeEnum.USER_EXIST_ERROR); // 若用户名已存在,抛出USER_EXIST_ERROR(5001)}if (ObjectUtil.isEmpty(admin.getPassword())) {admin.setPassword(Constants.USER_DEFAULT_PASSWORD); // 设置默认密码}if (ObjectUtil.isEmpty(admin.getName())) {admin.setName(admin.getUsername()); // 设置默认名称}admin.setRole(RoleEnum.ADMIN.name()); // 设置管理员角色adminMapper.insert(admin); // 插入新管理员记录}}
Mapper 数据访问层
package com.example.mapper;import com.example.entity.Admin;
import org.apache.ibatis.annotations.Select;import java.util.List;/*** @Author: XXX* @version 1.0 (2025-06-20)* @Description: 管理员相关数据接口*/
public interface AdminMapper {/*** 插入新管理员记录** @param admin 管理员实体对象,包含要插入的管理员信息* @return 插入操作影响的记录数*/int insert(Admin admin);/*** 根据用户名查询管理员** @param username 要查询的用户名* @return 匹配的管理员实体对象,若不存在则返回null*/@Select("select * from admin where username = #{username}")Admin selectByUsername(String username);
}
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapperPUBLIC "-//mybatis.org//DTD Mapper 3.0//EN""http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.mapper.AdminMapper"><sql id="Base_Column_List">id,username,password,name,phone,email,avatar,role</sql><insert id="insert" parameterType="com.example.entity.Admin" useGeneratedKeys="true">insert into admin<trim prefix="(" suffix=")" suffixOverrides=","><if test="id != null">id,</if><if test="username != null">username,</if><if test="password != null">password,</if><if test="name != null">name,</if><if test="phone != null">phone,</if><if test="email != null">email,</if><if test="avatar != null">avatar,</if><if test="role != null">role,</if></trim><trim prefix="values (" suffix=")" suffixOverrides=","><if test="id != null">#{id},</if><if test="username != null">#{username},</if><if test="password != null">#{password},</if><if test="name != null">#{name},</if><if test="phone != null">#{phone},</if><if test="email != null">#{email},</if><if test="avatar != null">#{avatar},</if><if test="role != null">#{role},</if></trim></insert></mapper>
package com.example.controller;import com.example.common.Result;
import com.example.common.enums.ResultCodeEnum;
import com.example.service.RedisCaptchaService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;import javax.servlet.http.HttpServletRequest;
import java.util.Map;/*** 验证码控制器 - 社区团购系统验证码管理API接口* * 功能说明:* - 验证码生成:提供图片验证码生成接口* - 验证码验证:验证用户输入的验证码* - 验证码刷新:支持用户主动刷新验证码* * 应用场景:* - 用户登录页面:生成和验证登录验证码* - 用户注册页面:生成和验证注册验证码* - 敏感操作:重要操作前的验证码确认* - 表单提交:防止恶意表单提交和机器人攻击* * 接口特性:* - 跨域支持:支持前端跨域请求* - 类型区分:支持不同类型的验证码(登录、注册等)* - 过期管理:验证码5分钟自动过期* - 安全防护:验证成功后立即删除验证码,防止重复使用* * @author zwt* @version 1.0 (2025-01-XX)*/
@CrossOrigin
@RestController
@RequestMapping("/captcha")
public class CaptchaController {@Autowiredprivate RedisCaptchaService captchaService;/*** 生成验证码接口* * 功能:生成指定类型的图片验证码* 应用场景:* - 用户登录页面加载时生成验证码* - 用户点击刷新验证码时重新生成* - 表单页面初始化时生成验证码* * 返回数据:* - key: 验证码唯一标识符,用于后续验证* - image: Base64编码的验证码图片数据* - expireTime: 验证码过期时间戳* * @param type 验证码类型(LOGIN-登录、REGISTER-注册、FORM-表单等)* @return 包含验证码key和图片数据的响应*/@PostMapping("/generate")public Result generateCaptcha(@RequestParam(defaultValue = "LOGIN") String type,HttpServletRequest request) {try {Map<String, String> captchaData = captchaService.generateCaptcha(type, request);return Result.success(captchaData);} catch (Exception e) {e.printStackTrace();return Result.error(ResultCodeEnum.CAPTCHA_GENERATE_ERROR);}}/*** 验证验证码接口* * 功能:验证用户输入的验证码是否正确* 应用场景:* - 用户登录时验证验证码* - 表单提交时验证验证码* - 敏感操作前的验证码确认* * 验证规则:* - 验证码必须存在且未过期* - 验证码类型必须匹配* - 验证成功后立即删除验证码,防止重复使用* * @param key 验证码key* @param code 用户输入的验证码* @param type 验证码类型* @return 验证成功返回成功信息,失败返回错误信息*/@PostMapping("/validate")public Result validateCaptcha(@RequestParam String key,@RequestParam String code,@RequestParam(defaultValue = "LOGIN") String type,HttpServletRequest request) {try {if (key == null || code == null || code.trim().isEmpty()) {return Result.error(ResultCodeEnum.PARAM_ERROR);}boolean isValid = captchaService.validateCaptcha(key, code.trim(), type, request);if (isValid) {return Result.success("验证码验证成功");} else {return Result.error(ResultCodeEnum.CAPTCHA_VALIDATE_ERROR);}} catch (Exception e) {e.printStackTrace();return Result.error(ResultCodeEnum.CAPTCHA_VALIDATE_ERROR);}}/*** 刷新验证码接口* * 功能:删除旧验证码并生成新的验证码* 应用场景:* - 用户看不清验证码时主动刷新* - 验证码过期后重新生成* - 系统维护时清理旧验证码* * @param key 旧验证码key* @param type 验证码类型* @return 新的验证码数据*/@PostMapping("/refresh")public Result refreshCaptcha(@RequestParam String key,@RequestParam(defaultValue = "LOGIN") String type,HttpServletRequest request) {try {if (key == null) {return Result.error(ResultCodeEnum.PARAM_ERROR);}// 删除旧验证码captchaService.removeCaptcha(key);// 生成新验证码Map<String, String> captchaData = captchaService.generateCaptcha(type, request);return Result.success(captchaData);} catch (Exception e) {e.printStackTrace();return Result.error(ResultCodeEnum.CAPTCHA_GENERATE_ERROR);}}/*** 检查验证码状态接口* * 功能:检查验证码是否存在且未过期* 应用场景:* - 前端检查验证码状态* - 调试和监控验证码系统* * @param key 验证码key* @return 验证码存在返回true,不存在返回false*/@GetMapping("/status")public Result checkCaptchaStatus(@RequestParam String key) {try {if (key == null) {return Result.error(ResultCodeEnum.PARAM_ERROR);}boolean exists = captchaService.existsCaptcha(key);return Result.success(exists);} catch (Exception e) {e.printStackTrace();return Result.error(ResultCodeEnum.CAPTCHA_VALIDATE_ERROR);}}
}
package com.example.controller;import com.example.common.Result;
import com.example.common.enums.ResultCodeEnum;
import com.example.service.RedisCaptchaService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;import javax.servlet.http.HttpServletRequest;
import java.util.Map;/*** 验证码控制器 - XX系统验证码管理API接口* * 功能说明:* - 验证码生成:提供图片验证码生成接口* - 验证码验证:验证用户输入的验证码* - 验证码刷新:支持用户主动刷新验证码* * 应用场景:* - 用户登录页面:生成和验证登录验证码* - 用户注册页面:生成和验证注册验证码* - 敏感操作:重要操作前的验证码确认* - 表单提交:防止恶意表单提交和机器人攻击* * 接口特性:* - 跨域支持:支持前端跨域请求* - 类型区分:支持不同类型的验证码(登录、注册等)* - 过期管理:验证码5分钟自动过期* - 安全防护:验证成功后立即删除验证码,防止重复使用* * @author XXX* @version 1.0 (2025-XX-XX)*/
@CrossOrigin
@RestController
@RequestMapping("/captcha")
public class CaptchaController {@Autowiredprivate RedisCaptchaService captchaService;/*** 生成验证码接口* * 功能:生成指定类型的图片验证码* 应用场景:* - 用户登录页面加载时生成验证码* - 用户点击刷新验证码时重新生成* - 表单页面初始化时生成验证码* * 返回数据:* - key: 验证码唯一标识符,用于后续验证* - image: Base64编码的验证码图片数据* - expireTime: 验证码过期时间戳* * @param type 验证码类型(LOGIN-登录、REGISTER-注册、FORM-表单等)* @return 包含验证码key和图片数据的响应*/@PostMapping("/generate")public Result generateCaptcha(@RequestParam(defaultValue = "LOGIN") String type,HttpServletRequest request) {try {Map<String, String> captchaData = captchaService.generateCaptcha(type, request);return Result.success(captchaData);} catch (Exception e) {e.printStackTrace();return Result.error(ResultCodeEnum.CAPTCHA_GENERATE_ERROR);}}/*** 验证验证码接口* * 功能:验证用户输入的验证码是否正确* 应用场景:* - 用户登录时验证验证码* - 表单提交时验证验证码* - 敏感操作前的验证码确认* * 验证规则:* - 验证码必须存在且未过期* - 验证码类型必须匹配* - 验证成功后立即删除验证码,防止重复使用* * @param key 验证码key* @param code 用户输入的验证码* @param type 验证码类型* @return 验证成功返回成功信息,失败返回错误信息*/@PostMapping("/validate")public Result validateCaptcha(@RequestParam String key,@RequestParam String code,@RequestParam(defaultValue = "LOGIN") String type,HttpServletRequest request) {try {if (key == null || code == null || code.trim().isEmpty()) {return Result.error(ResultCodeEnum.PARAM_ERROR);}boolean isValid = captchaService.validateCaptcha(key, code.trim(), type, request);if (isValid) {return Result.success("验证码验证成功");} else {return Result.error(ResultCodeEnum.CAPTCHA_VALIDATE_ERROR);}} catch (Exception e) {e.printStackTrace();return Result.error(ResultCodeEnum.CAPTCHA_VALIDATE_ERROR);}}/*** 刷新验证码接口* * 功能:删除旧验证码并生成新的验证码* 应用场景:* - 用户看不清验证码时主动刷新* - 验证码过期后重新生成* - 系统维护时清理旧验证码* * @param key 旧验证码key* @param type 验证码类型* @return 新的验证码数据*/@PostMapping("/refresh")public Result refreshCaptcha(@RequestParam String key,@RequestParam(defaultValue = "LOGIN") String type,HttpServletRequest request) {try {if (key == null) {return Result.error(ResultCodeEnum.PARAM_ERROR);}// 删除旧验证码captchaService.removeCaptcha(key);// 生成新验证码Map<String, String> captchaData = captchaService.generateCaptcha(type, request);return Result.success(captchaData);} catch (Exception e) {e.printStackTrace();return Result.error(ResultCodeEnum.CAPTCHA_GENERATE_ERROR);}}/*** 检查验证码状态接口* * 功能:检查验证码是否存在且未过期* 应用场景:* - 前端检查验证码状态* - 调试和监控验证码系统* * @param key 验证码key* @return 验证码存在返回true,不存在返回false*/@GetMapping("/status")public Result checkCaptchaStatus(@RequestParam String key) {try {if (key == null) {return Result.error(ResultCodeEnum.PARAM_ERROR);}boolean exists = captchaService.existsCaptcha(key);return Result.success(exists);} catch (Exception e) {e.printStackTrace();return Result.error(ResultCodeEnum.CAPTCHA_VALIDATE_ERROR);}}
}
到这登录页面的验证码功能就已经实现了,快去试试吧
每天进步一点点,加油 ! ! !