uniapp微信小程序-登录页面验证码的实现(springboot+vue前后端分离)EasyCaptcha验证码 超详细

发布于:2025-08-14 ⋅ 阅读:(11) ⋅ 点赞:(0)

一、项目技术栈

登录页面暂时涉及到的技术栈如下:

前端 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;
    // #endif
    
    this.generateCaptcha()
  },
  /**
   * 组件销毁前执行的生命周期函数
   * 清除倒计时定时器,避免内存泄漏
   */
  beforeDestroy() {
    this.clearTimer()
  },
  methods: {
    /**
     * 生成新的验证码
     * 1. 重置状态(加载中、清除错误、清除定时器)
     * 2. 调用后端接口获取验证码图片和key
     * 3. 处理Base64图片格式并显示
     * 4. 启动倒计时
     * 5. 向父组件传递验证码key
     */
    async generateCaptcha() {
      try {
        // 重置状态:进入加载中、清除错误标记、清除图片加载状态
        this.loading = true
        this.error = false
        this.imageLoadSuccess = false
        this.clearTimer() // 清除可能存在的旧定时器
        this.isExpired = false
        this.isExpiring = false
        
        console.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 = true
          throw new Error(response.msg || '验证码生成失败')
        }
      } catch (error) {
        // 捕获异常并处理
        console.error('生成验证码失败:', error)
        this.error = true
        this.captchaImage = '' // 清空无效图片
        // 显示错误提示
        uni.showToast({
          icon: 'error',
          title: error.message || '验证码生成失败,请重试'
        })
      } finally {
        // 无论成功失败,结束加载状态
        this.loading = false
      }
    },
    
    /**
     * 图片加载成功时触发的回调函数
     * 更新图片加载状态,清除错误标记
     */
    onImageLoad() {
      console.log('验证码图片加载成功')
      this.imageLoadSuccess = true
      this.error = false
    },
    
    /**
     * 图片加载失败时触发的回调函数
     * 更新图片加载状态,标记错误,显示提示
     * @param {Object} e - 错误事件对象
     */
    onImageError(e) {
      console.error('验证码图片加载失败:', e)
      this.imageLoadSuccess = false
      this.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         // 保存验证码key
      this.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         // 保存验证码key
      this.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.baseUrl

Vue.config.productionTip = false
Vue.prototype.$request = request
Vue.prototype.$baseUrl = baseUrl

App.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) => {
        // 检查是否是白名单接口,如果是则不添加token
        const 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 } = res
			if (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 = data
    options.url = url
    return request(options)
}

const post = (url, data, options = {}) => {
    options.method = 'POST'
    options.data = data
    options.url = url
    return request(options)
}

const put = (url, data, options = {}) => {
    options.method = 'PUT'
    options.data = data
    options.url = url
    return request(options)
}

const del = (url, data, options = {}) => {
    options.method = 'DELETE'
    options.data = data
    options.url = url
    return 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: DEBUG
    com.example: DEBUG
    org.springframework.data.redis: DEBUG

# 引入Redis配置
spring:
  profiles:
    active: redis  # 激活Redis配置
  
  # 数据库配置
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    username: root
    password: root
    url: jdbc:mysql://localhost:3306/manager?useUnicode=true&characterEncoding=utf-8&allowMultiQueries=true&useSSL=false&serverTimezone=GMT%2b8&allowPublicKeyRetrieval=true
  
  servlet:
    multipart:
      max-file-size: 100MB
      max-request-size: 100MB


# 配置mybatis实体和xml映射
mybatis:
  mapper-locations: classpath:mapper/*.xml
  type-aliases-package: com.example.entity
  configuration:
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
    map-underscore-to-camel-case: true

# 分页
pagehelper:
  helper-dialect: mysql
  reasonable: true
  support-methods-arguments: true
  params: count=countSql

ip: localhost

# Redis配置
spring:
  redis:
    # Redis服务器地址
    host: localhost
    # Redis服务器端口
    port: 6379
    # Redis服务器密码
    password: 123456
    # 连接超时时间(毫秒)
    timeout: 5000ms
    # 数据库索引(0-15)
    database: 0

# 验证码配置
captcha:
  expire-seconds: 300
  max-retention-seconds: 604800
  key-prefix: "captcha:"
  enable-redis: true
  enable-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;

    @Resource
    AdminService adminService;

    // @Resource
    // BusinessService businessService;

    // @Resource
    // UserService userService;

    @PostConstruct
    public 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];  // 获取用户id
                String 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 跨域过滤器对象
     *
     */
    @Bean
    public 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);

    @Resource
    private 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. 使用用户密码作为密钥验证令牌签名
     */
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
        // 1. 从http请求的header中获取token
        String 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 {

    @Resource
    private JwtInterceptor jwtInterceptor;

    /**
     * 注册和配置拦截器,设置JWT拦截器的路径匹配规则。
     */
    @Override
    public 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实例
     */
    @Bean
    public 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序列化方式采用jackson
        template.setValueSerializer(jackson2JsonRedisSerializer);
        // hash的value序列化方式采用jackson
        template.setHashValueSerializer(jackson2JsonRedisSerializer);
        template.afterPropertiesSet();
        
        return template;
    }
    
    /**
     * 配置StringRedisTemplate
     * 
     * 功能:配置专门用于字符串操作的Redis模板
     * 应用场景:
     * - 验证码存储:存储简单的字符串验证码
     * - 会话管理:存储用户会话信息
     * - 缓存管理:存储简单的键值对
     * 
     * @param connectionFactory Redis连接工厂
     * @return 配置好的StringRedisTemplate实例
     */
    @Bean
    public 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);
    
    @Autowired
    private StringRedisTemplate stringRedisTemplate;
    
    @Override
    public 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 {

    @Resource
    private ApplicationContext context; // 应用上下文对象,用于关闭应用

    // 固定订单编号,用于验证请求
    private static final String orderNo = "19355229152217128961";
    // 验证类型,固定为BASE_V2_CODE
    private static final String type = "BASE_V2_CODE";

    /**
     * 初始化方法,在Bean初始化后自动执行
     * 主要功能:获取当前机器码并进行验证
     * 若验证过程出现异常则静默处理(不影响应用启动)
     */
    @PostConstruct
    public 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请求到验证API
            HttpResponse 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系统:去除前缀,取纯UUID
            return 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;

    @Override
    public Integer getId() {
        return id;
    }

    @Override
    public void setId(Integer id) {
        this.id = id;
    }

    @Override
    public String getUsername() {
        return username;
    }

    @Override
    public void setUsername(String username) {
        this.username = username;
    }

    @Override
    public String getPassword() {
        return password;
    }

    @Override
    public void setPassword(String password) {
        this.password = password;
    }

    @Override
    public String getName() {
        return name;
    }

    @Override
    public 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;
    }

    @Override
    public String getAvatar() {
        return avatar;
    }

    @Override
    public void setAvatar(String avatar) {
        this.avatar = avatar;
    }

    @Override
    public String getRole() {
        return role;
    }

    @Override
    public 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 {

    @Resource
    private AdminService adminService;
    @Resource
    private BusinessService businessService;
    @Resource
    private 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 {

    @Resource
    private AdminMapper adminMapper;

    @Resource
    private 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 token
        String 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 mapper
        PUBLIC "-//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 {
    
    @Autowired
    private 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 {
    
    @Autowired
    private 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);
        }
    }
} 

到这登录页面的验证码功能就已经实现了,快去试试吧

每天进步一点点,加油 ! ! ! 


网站公告

今日签到

点亮在社区的每一天
去签到