欢迎光临
我们一直在努力

博客基础模块开发(登录注册)

博客基础模块开发(登录注册)

目录

  • 一、方案编写
  • 二、基础环境搭建
  • 三、快速体验框架使用
  • 四、数据库表构建
  • 五、后端API接口实现与测试
  • 六、前端代码环境搭建和编写
  • 七、前后端联调

一、方案编写

1.1 功能概述

实现博客系统的用户登录和注册功能,包含基础的用户认证、会话管理和安全验证。

1.2 功能需求明细

1.2.1 用户注册功能
功能点描述验证规则
用户名注册 用户设置唯一用户名 4-20位字母/数字/下划线
邮箱注册 绑定邮箱用于找回密码 有效邮箱格式
手机号注册 可选,绑定手机号 11位手机号
密码设置 用户设置登录密码 8-20位,字母+数字+特殊字符
确认密码 二次输入确认 与密码一致
验证码 邮箱/手机验证码 6位数字,有效期5分钟
图形验证码 防止机器注册 4位字母数字,忽略大小写
用户协议 同意协议才能注册 必须勾选
1.2.2 用户登录功能
功能点描述说明
账号登录 用户名/邮箱/手机号 + 密码 支持多种登录方式
验证码登录 手机号 + 验证码 免密码登录
记住我 保持登录状态 Token有效期7天/30天
图形验证码 登录失败3次后出现 防止暴力破解
错误提示 友好的错误提示 不暴露具体信息
1.2.3 系统优化需求
验证码忽略大小写

// 验证码校验工具类
public class CaptchaValidator {

/**
* 验证验证码(忽略大小写)
* @param inputCode 用户输入的验证码
* @param storeCode 实际存储的验证码
* @return 是否通过验证
*/

public static boolean validateIgnoreCase(String inputCode, String storeCode) {
if (inputCode == null || storeCode == null) {
return false;
}
return inputCode.equalsIgnoreCase(storeCode);
}

/**
* 生成验证码(统一转为大写存储)
*/

public static String generateCode(int length) {
String chars = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789";
Random random = new Random();
StringBuilder sb = new StringBuilder();
for (int i = 0; i < length; i++) {
sb.append(chars.charAt(random.nextInt(chars.length())));
}
return sb.toString();
}
}

字体配置更新

# application.yml
blog:
font:
path: /usr/share/fonts/blog/ # Linux服务器路径
# path: C:/Windows/Fonts/blog/ # Windows开发环境
default: Microsoft YaHei.ttf
title-font: FZZJ-HBJW.ttf # 标题字体
content-font: SimSun.ttf # 内容字体

网站基础配置

# application.yml
blog:
site:
title: 我的技术博客
subtitle: 分享编程经验和技术干货
keywords: Java,Spring Boot,Redis,博客
description: 一个专注于后端技术的个人博客
icp: 京ICP备xxxxxx号
author: Your Name
email: your@email.com

seo:
baidu-verify: xxxxxx
google-verify: xxxxxx

个性化主题-追悼模式

/* 追悼模式样式 theme/mourning.css */
.mourning-mode {
filter: grayscale(100%);
-webkit-filter: grayscale(100%);
-moz-filter: grayscale(100%);
-ms-filter: grayscale(100%);
-o-filter: grayscale(100%);
}

.mourning-mode .navbar {
background-color: #333 !important;
}

.mourning-mode .footer {
background-color: #222 !important;
}

.mourning-mode a:hover {
color: #999 !important;
}

// 主题切换逻辑
const ThemeManager = {
// 启用追悼模式
enableMourningMode() {
document.body.classList.add('mourning-mode');
localStorage.setItem('theme', 'mourning');

// 通知后端记录设置
axios.post('/api/user/theme', { theme: 'mourning' });
},

// 禁用追悼模式
disableMourningMode() {
document.body.classList.remove('mourning-mode');
localStorage.setItem('theme', 'normal');
axios.post('/api/user/theme', { theme: 'normal' });
},

// 切换追悼模式
toggleMourningMode() {
if (document.body.classList.contains('mourning-mode')) {
this.disableMourningMode();
} else {
this.enableMourningMode();
}
},

// 初始化主题
initTheme() {
const savedTheme = localStorage.getItem('theme');
if (savedTheme === 'mourning') {
document.body.classList.add('mourning-mode');
}

// 特殊日期自动开启(清明节等)
const today = new Date();
if (this.isMourningDate(today)) {
this.enableMourningMode();
}
},

// 判断是否追悼日期
isMourningDate(date) {
const month = date.getMonth() + 1;
const day = date.getDate();
// 清明节(4月4日或5日)
return (month === 4 && (day === 4 || day === 5)) ||
// 512汶川地震纪念日
(month === 5 && day === 12) ||
// 南京大屠杀纪念日
(month === 12 && day === 13);
}
};

// 页面加载时初始化
document.addEventListener('DOMContentLoaded', () => {
ThemeManager.initTheme();
});

1.3 技术选型

技术栈用途版本
Spring Boot 后端框架 2.7.x
Spring Security 安全认证 2.7.x
MyBatis Plus ORM框架 3.5.x
MySQL 数据库 8.0+
Redis 缓存/会话 7.x
JWT Token认证 0.11.x
Vue.js 前端框架 3.x
Element Plus UI组件库 2.x
Axios HTTP客户端 1.x

二、基础环境搭建

2.1 Ubuntu 环境搭建

2.1.1 安装 JDK 17

# 更新软件源
sudo apt update

# 安装 OpenJDK 17
sudo apt install openjdk-17-jdk -y

# 验证安装
java -version

# 设置 JAVA_HOME
echo 'export JAVA_HOME=/usr/lib/jvm/java-17-openjdk-amd64' >> ~/.bashrc
echo 'export PATH=$JAVA_HOME/bin:$PATH' >> ~/.bashrc
source ~/.bashrc

2.1.2 安装 MySQL 8.0

# 安装 MySQL
sudo apt install mysql-server-8.0 -y

# 启动 MySQL
sudo systemctl start mysql
sudo systemctl enable mysql

# 安全配置
sudo mysql_secure_installation

# 登录 MySQL
sudo mysql -u root -p

# 创建数据库和用户
CREATE DATABASE blog_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE USER 'blog_user'@'localhost' IDENTIFIED BY 'your_password';
GRANT ALL PRIVILEGES ON blog_db.* TO 'blog_user'@'localhost';
FLUSH PRIVILEGES;

2.1.3 安装 Redis

# 安装 Redis
sudo apt install redis-server -y

# 修改配置
sudo nano /etc/redis/redis.conf

# 设置密码(找到 requirepass 取消注释)
requirepass your_redis_password

# 重启 Redis
sudo systemctl restart redis
sudo systemctl enable redis

# 验证安装
redis-cli –version
redis-cli -a your_redis_password ping

2.1.4 安装 Node.js 和 npm

# 使用 nvm 安装(推荐)
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.0/install.sh | bash
source ~/.bashrc

# 安装 Node.js 18
nvm install 18
nvm use 18
nvm alias default 18

# 验证安装
node –version
npm –version

2.1.5 安装 Maven

# 安装 Maven
sudo apt install maven -y

# 验证安装
mvn –version

# 配置阿里云镜像(加速)
sudo nano /etc/maven/settings.xml

# 在 mirrors 标签内添加:
<mirror>
<id>aliyunmaven</id>
<mirrorOf>*</mirrorOf>
<name>阿里云公共仓库</name>
<url>https://maven.aliyun.com/repository/public</url>
</mirror>

2.1.6 安装 Git

# 安装 Git
sudo apt install git -y

# 配置 Git
git config –global user.name "Your Name"
git config –global user.email "your@email.com"

# 生成 SSH 密钥
ssh-keygen -t rsa -b 4096 -C "your@email.com"
cat ~/.ssh/id_rsa.pub
# 复制公钥添加到 GitHub/Gitee

2.2 Windows 环境搭建

2.2.1 安装 JDK 17
  • 下载 JDK 17:https://www.oracle.com/java/technologies/javase/jdk17-archive-downloads.html
  • 双击安装,记住安装路径(如 C:\\Program Files\\Java\\jdk-17)
  • 配置环境变量:
    • 右键"此电脑" → 属性 → 高级系统设置 → 环境变量
    • 新建系统变量 JAVA_HOME = C:\\Program Files\\Java\\jdk-17
    • 编辑 Path 变量,添加 %JAVA_HOME%\\bin
  • 验证:打开 CMD,运行 java -version
  • 2.2.2 安装 MySQL 8.0
  • 下载 MySQL Installer:https://dev.mysql.com/downloads/installer/
  • 运行安装程序,选择"Developer Default"
  • 设置 root 密码
  • 安装完成后,打开 MySQL Command Line Client 验证
  • 创建数据库:
  • CREATE DATABASE blog_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
    CREATE USER 'blog_user'@'localhost' IDENTIFIED BY 'your_password';
    GRANT ALL PRIVILEGES ON blog_db.* TO 'blog_user'@'localhost';
    FLUSH PRIVILEGES;

    2.2.3 安装 Redis for Windows
  • 下载 Redis Windows 版本:https://github.com/microsoftarchive/redis/releases
  • 下载 .msi 或 .zip 文件
  • 安装或解压到 C:\\Redis
  • 添加到系统 Path:C:\\Redis
  • 修改配置文件 redis.windows.conf:
  • requirepass your_redis_password

  • 启动 Redis:
  • redis-server –service-install redis.windows.conf
    redis-server –service-start

    2.2.4 安装 Node.js
  • 下载 Node.js 18 LTS:https://nodejs.org/
  • 双击安装,勾选"Add to PATH"
  • 验证:
  • node –version
    npm –version

    2.2.5 安装 Maven
  • 下载 Maven:https://maven.apache.org/download.cgi
  • 解压到 C:\\Maven
  • 配置环境变量:
    • 新建 MAVEN_HOME = C:\\Maven
    • 编辑 Path 添加 %MAVEN_HOME%\\bin
  • 配置阿里云镜像:编辑 C:\\Maven\\conf\\settings.xml,添加镜像配置
  • 2.2.6 安装 Git
  • 下载 Git:https://git-scm.com/download/win
  • 双击安装,使用默认选项
  • 配置 Git Bash:
  • git config –global user.name "Your Name"
    git config –global user.email "your@email.com"
    ssh-keygen -t rsa -b 4096 -C "your@email.com"
    cat ~/.ssh/id_rsa.pub

    2.2.7 安装开发工具

    # IntelliJ IDEA(推荐)
    # 下载地址:https://www.jetbrains.com/idea/download/

    # VS Code
    # 下载地址:https://code.visualstudio.com/

    # Navicat/DBeaver(数据库工具)
    # DBeaver 免费版:https://dbeaver.io/download/


    三、快速体验框架使用

    3.1 框架官网示例

    3.1.1 克隆示例项目

    # 创建项目目录
    mkdir -p ~/projects/blog
    cd ~/projects/blog

    # 克隆示例项目(以若依为例,可根据实际框架调整)
    git clone https://gitee.com/y_project/RuoYi-Vue.git ruoyi-demo
    cd ruoyi-demo

    3.1.2 导入数据库

    # 创建数据库
    mysql -u root -p
    CREATE DATABASE ry-vue CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
    USE ry-vue;
    source /path/to/ruoyi-demo/sql/ry_2024xxxx.sql;

    3.1.3 修改配置文件

    # 编辑配置文件
    nano ruoyi-admin/src/main/resources/application-druid.yml

    # 修改数据库连接
    spring:
    datasource:
    url: jdbc:mysql://localhost:3306/ry-vue?useUnicode=true&characterEncoding=utf8&serverTimezone=GMT%2B8
    username: root
    password: your_password

    # 修改Redis配置
    nano ruoyi-admin/src/main/resources/application.yml
    redis:
    host: localhost
    port: 6379
    password: your_redis_password

    3.1.4 启动后端

    # 编译打包
    cd ruoyi-demo
    mvn clean package

    # 启动服务
    cd ruoyi-admin/target
    java -jar ruoyi-admin.jar

    # 或使用 IDEA 启动
    # 打开项目,运行 RuoYiApplication.java

    3.1.5 启动前端

    # 进入前端目录
    cd ruoyi-ui

    # 安装依赖(使用淘宝镜像)
    npm install –registry=https://registry.npmmirror.com

    # 启动开发服务器
    npm run dev

    # 访问 http://localhost:80
    # 默认账号: admin 密码: admin123

    3.2 快速理解框架结构

    3.2.1 后端结构

    ruoyi-demo/
    ├── ruoyi-admin # 启动模块
    ├── ruoyi-common # 通用工具类
    ├── ruoyi-framework # 框架核心
    ├── ruoyi-generator # 代码生成
    ├── ruoyi-quartz # 定时任务
    ├── ruoyi-system # 系统模块
    └── ruoyi-ui # 前端模块

    3.2.2 前端结构

    ruoyi-ui/
    ├── src/
    │ ├── api/ # API接口
    │ ├── assets/ # 静态资源
    │ ├── components/ # 通用组件
    │ ├── layout/ # 布局组件
    │ ├── router/ # 路由配置
    │ ├── store/ # 状态管理
    │ ├── utils/ # 工具类
    │ └── views/ # 页面视图
    ├── public/ # 公共资源
    └── package.json # 依赖配置

    3.3 生成第一个CRUD

    3.3.1 使用代码生成器
  • 登录后台系统
  • 进入"系统工具" → “代码生成”
  • 导入需要生成的数据表
  • 配置生成选项:
    • 包名:com.blog.modules
    • 模块名:article
    • 功能名:文章管理
    • 作者:Your Name
  • 生成代码并下载
  • 3.3.2 导入生成的代码

    # 解压下载的代码
    unzip ruoyi.zip -d generated-code

    # 复制后端代码
    cp -r generated-code/main/* src/main/

    # 复制前端代码
    cp -r generated-code/vue/* ruoyi-ui/src/

    # 执行SQL
    mysql -u root -p blog_db < generated-code/sql/xxx_menu.sql

    3.3.3 测试生成的CRUD
  • 重启后端服务
  • 刷新前端页面
  • 在菜单中找到新生成的功能
  • 测试增删改查功能

  • 四、数据库表构建

    4.1 用户表设计

    4.1.1 用户主表 (blog_user)

    CREATE TABLE `blog_user` (
    `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '用户ID',
    `username` varchar(50) NOT NULL COMMENT '用户名',
    `password` varchar(100) NOT NULL COMMENT '密码',
    `salt` varchar(50) DEFAULT NULL COMMENT '密码盐',
    `email` varchar(100) DEFAULT NULL COMMENT '邮箱',
    `phone` varchar(20) DEFAULT NULL COMMENT '手机号',
    `avatar` varchar(500) DEFAULT NULL COMMENT '头像地址',
    `nickname` varchar(50) DEFAULT NULL COMMENT '昵称',
    `real_name` varchar(50) DEFAULT NULL COMMENT '真实姓名',
    `gender` tinyint(1) DEFAULT '0' COMMENT '性别(0:未知 1:男 2:女)',
    `birthday` date DEFAULT NULL COMMENT '生日',
    `signature` varchar(500) DEFAULT NULL COMMENT '个性签名',
    `status` tinyint(1) DEFAULT '1' COMMENT '状态(0:禁用 1:正常)',
    `login_ip` varchar(50) DEFAULT NULL COMMENT '最后登录IP',
    `login_time` datetime DEFAULT NULL COMMENT '最后登录时间',
    `create_by` varchar(50) DEFAULT NULL COMMENT '创建者',
    `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
    `update_by` varchar(50) DEFAULT NULL COMMENT '更新者',
    `update_time` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
    `deleted` tinyint(1) DEFAULT '0' COMMENT '逻辑删除(0:正常 1:删除)',
    PRIMARY KEY (`id`),
    UNIQUE KEY `idx_username` (`username`),
    UNIQUE KEY `idx_email` (`email`),
    UNIQUE KEY `idx_phone` (`phone`),
    KEY `idx_status` (`status`),
    KEY `idx_create_time` (`create_time`)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='博客用户表';

    4.1.2 用户登录记录表 (blog_user_login_log)

    CREATE TABLE `blog_user_login_log` (
    `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '日志ID',
    `user_id` bigint(20) NOT NULL COMMENT '用户ID',
    `username` varchar(50) NOT NULL COMMENT '用户名',
    `login_type` tinyint(1) DEFAULT '1' COMMENT '登录类型(1:密码登录 2:验证码登录)',
    `ip` varchar(50) DEFAULT NULL COMMENT '登录IP',
    `location` varchar(100) DEFAULT NULL COMMENT '登录地点',
    `device` varchar(100) DEFAULT NULL COMMENT '登录设备',
    `browser` varchar(50) DEFAULT NULL COMMENT '浏览器',
    `os` varchar(50) DEFAULT NULL COMMENT '操作系统',
    `status` tinyint(1) DEFAULT '1' COMMENT '状态(1:成功 0:失败)',
    `message` varchar(255) DEFAULT NULL COMMENT '消息',
    `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
    PRIMARY KEY (`id`),
    KEY `idx_user_id` (`user_id`),
    KEY `idx_create_time` (`create_time`)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户登录日志表';

    4.1.3 验证码记录表 (blog_captcha)

    CREATE TABLE `blog_captcha` (
    `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'ID',
    `uuid` varchar(100) NOT NULL COMMENT '唯一标识',
    `code` varchar(10) NOT NULL COMMENT '验证码',
    `type` tinyint(1) DEFAULT '1' COMMENT '类型(1:图形验证码 2:短信验证码 3:邮箱验证码)',
    `target` varchar(100) DEFAULT NULL COMMENT '目标(手机号/邮箱)',
    `expire_time` datetime NOT NULL COMMENT '过期时间',
    `status` tinyint(1) DEFAULT '1' COMMENT '状态(1:未使用 0:已使用)',
    `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
    PRIMARY KEY (`id`),
    KEY `idx_uuid` (`uuid`),
    KEY `idx_target` (`target`),
    KEY `idx_expire_time` (`expire_time`)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='验证码记录表';

    4.1.4 用户配置表 (blog_user_config)

    CREATE TABLE `blog_user_config` (
    `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'ID',
    `user_id` bigint(20) NOT NULL COMMENT '用户ID',
    `theme` varchar(50) DEFAULT 'default' COMMENT '主题(default:默认 dark:暗黑 mourning:追悼)',
    `font_size` tinyint(2) DEFAULT '14' COMMENT '字体大小',
    `font_family` varchar(50) DEFAULT 'Microsoft YaHei' COMMENT '字体',
    `language` varchar(10) DEFAULT 'zh-CN' COMMENT '语言',
    `notification` tinyint(1) DEFAULT '1' COMMENT '通知(1:开启 0:关闭)',
    `privacy` tinyint(1) DEFAULT '0' COMMENT '隐私模式(0:公开 1:私密)',
    `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
    `update_time` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
    PRIMARY KEY (`id`),
    UNIQUE KEY `idx_user_id` (`user_id`)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户配置表';

    4.1.5 用户角色表 (blog_role)

    CREATE TABLE `blog_role` (
    `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '角色ID',
    `role_code` varchar(50) NOT NULL COMMENT '角色编码',
    `role_name` varchar(50) NOT NULL COMMENT '角色名称',
    `description` varchar(255) DEFAULT NULL COMMENT '描述',
    `sort` int(5) DEFAULT '0' COMMENT '排序',
    `status` tinyint(1) DEFAULT '1' COMMENT '状态(0:禁用 1:正常)',
    `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
    `update_time` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
    PRIMARY KEY (`id`),
    UNIQUE KEY `idx_role_code` (`role_code`)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='角色表';

    — 初始化角色数据
    INSERT INTO `blog_role` (`role_code`, `role_name`, `description`, `sort`) VALUES
    ('admin', '管理员', '系统管理员', 1),
    ('editor', '编辑', '内容编辑', 2),
    ('user', '普通用户', '普通注册用户', 3),
    ('vip', 'VIP用户', '付费用户', 4);

    4.1.6 用户角色关联表 (blog_user_role)

    CREATE TABLE `blog_user_role` (
    `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'ID',
    `user_id` bigint(20) NOT NULL COMMENT '用户ID',
    `role_id` bigint(20) NOT NULL COMMENT '角色ID',
    `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
    PRIMARY KEY (`id`),
    UNIQUE KEY `idx_user_role` (`user_id`, `role_id`)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户角色关联表';

    4.2 数据库构建方式

    4.2.1 方式一:手动执行SQL脚本

    # 创建数据库
    mysql -u root -p -e "CREATE DATABASE IF NOT EXISTS blog_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;"

    # 导入SQL文件
    mysql -u root -p blog_db < ~/projects/blog/sql/blog_schema.sql
    mysql -u root -p blog_db < ~/projects/blog/sql/blog_data.sql

    4.2.2 方式二:使用Flyway自动迁移

    <!– pom.xml 添加依赖 –>
    <dependency>
    <groupId>org.flywaydb</groupId>
    <artifactId>flyway-core</artifactId>
    <version>9.16.0</version>
    </dependency>
    <dependency>
    <groupId>org.flywaydb</groupId>
    <artifactId>flyway-mysql</artifactId>
    <version>9.16.0</version>
    </dependency>

    # application.yml 配置
    spring:
    flyway:
    enabled: true
    baseline-on-migrate: true
    locations: classpath:db/migration
    table: flyway_schema_history

    创建迁移文件:

    # 创建目录
    mkdir -p src/main/resources/db/migration

    # 创建初始版本 V1__init_schema.sql
    nano src/main/resources/db/migration/V1__init_schema.sql

    — V1__init_schema.sql
    — 用户表
    CREATE TABLE IF NOT EXISTS `blog_user` (
    `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '用户ID',
    `username` varchar(50) NOT NULL COMMENT '用户名',
    — … 其他字段
    PRIMARY KEY (`id`)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='博客用户表';

    — 创建索引
    CREATE INDEX idx_username ON blog_user(username);
    CREATE INDEX idx_email ON blog_user(email);

    # 创建V2__init_data.sql
    nano src/main/resources/db/migration/V2__init_data.sql

    — V2__init_data.sql
    — 初始化角色数据
    INSERT INTO `blog_role` (`role_code`, `role_name`, `description`) VALUES
    ('admin', '管理员', '系统管理员'),
    ('user', '普通用户', '普通注册用户');

    — 初始化管理员账号(密码: admin123)
    INSERT INTO `blog_user` (`username`, `password`, `salt`, `email`, `status`) VALUES
    ('admin', '加密后的密码', '随机salt', 'admin@blog.com', 1);

    4.2.3 使用MyBatis-Plus代码生成器

    // CodeGenerator.java
    package com.blog.generator;

    import com.baomidou.mybatisplus.generator.FastAutoGenerator;
    import com.baomidou.mybatisplus.generator.config.OutputFile;
    import com.baomidou.mybatisplus.generator.config.rules.DateType;
    import com.baomidou.mybatisplus.generator.engine.FreemarkerTemplateEngine;

    import java.util.Collections;

    public class CodeGenerator {
    public static void main(String[] args) {
    String projectPath = System.getProperty("user.dir");

    FastAutoGenerator.create("jdbc:mysql://localhost:3306/blog_db?useUnicode=true&characterEncoding=utf8&serverTimezone=GMT%2B8",
    "root", "your_password")
    .globalConfig(builder -> {
    builder.author("Your Name") // 作者
    .outputDir(projectPath + "/src/main/java") // 输出路径
    .dateType(DateType.ONLY_DATE) // 日期类型
    .commentDate("yyyy-MM-dd") // 注释日期
    .disableOpenDir(); // 禁止打开输出目录
    })
    .packageConfig(builder -> {
    builder.parent("com.blog") // 父包名
    .moduleName("modules") // 模块名
    .entity("entity") // 实体类包名
    .service("service") // service包名
    .serviceImpl("service.impl") // serviceImpl包名
    .mapper("mapper") // mapper包名
    .controller("controller") // controller包名
    .xml("mapper.xml") // mapper.xml包名
    .pathInfo(Collections.singletonMap(OutputFile.xml, projectPath + "/src/main/resources/mapper")); // mapper.xml路径
    })
    .strategyConfig(builder -> {
    builder.addInclude("blog_user", "blog_role", "blog_user_role") // 表名
    .addTablePrefix("blog_") // 表前缀
    .entityBuilder()
    .enableLombok() // 启用Lombok
    .enableChainModel() // 链式模型
    .enableTableFieldAnnotation() // 启用字段注解
    .controllerBuilder()
    .enableRestStyle() // Rest风格
    .enableHyphenStyle() // 连字符风格
    .mapperBuilder()
    .enableBaseResultMap() // 启用BaseResultMap
    .enableBaseColumnList(); // 启用BaseColumnList
    })
    .templateEngine(new FreemarkerTemplateEngine()) // 模板引擎
    .execute();
    }
    }


    五、后端API接口实现与测试

    5.1 项目结构

    blog-backend/
    ├── src/main/java/com/blog/
    │ ├── BlogApplication.java # 启动类
    │ ├── common/ # 公共模块
    │ │ ├── constant/ # 常量定义
    │ │ ├── exception/ # 异常处理
    │ │ ├── result/ # 统一返回结果
    │ │ ├── util/ # 工具类
    │ │ └── validator/ # 验证器
    │ ├── config/ # 配置类
    │ │ ├── SecurityConfig.java # 安全配置
    │ │ ├── RedisConfig.java # Redis配置
    │ │ ├── SwaggerConfig.java # API文档配置
    │ │ └── WebConfig.java # Web配置
    │ ├── modules/ # 功能模块
    │ │ └── auth/ # 认证模块
    │ │ ├── controller/ # 控制器
    │ │ ├── service/ # 服务层
    │ │ ├── mapper/ # 数据访问
    │ │ ├── entity/ # 实体类
    │ │ ├── dto/ # 数据传输对象
    │ │ └── vo/ # 视图对象
    │ └── security/ # 安全相关
    │ ├── filter/ # 过滤器
    │ ├── handler/ # 处理器
    │ └── utils/ # 安全工具类
    └── src/main/resources/
    ├── application.yml # 主配置文件
    ├── application-dev.yml # 开发环境配置
    └── mapper/ # MyBatis XML文件

    5.2 依赖配置

    <!– pom.xml –>
    <?xml version="1.0" encoding="UTF-8"?>
    <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"
    >

    <modelVersion>4.0.0</modelVersion>

    <parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.7.14</version>
    </parent>

    <groupId>com.blog</groupId>
    <artifactId>blog-backend</artifactId>
    <version>1.0.0</version>
    <packaging>jar</packaging>

    <properties>
    <java.version>17</java.version>
    <mybatis-plus.version>3.5.3</mybatis-plus.version>
    <jwt.version>0.11.5</jwt.version>
    <fastjson.version>2.0.32</fastjson.version>
    </properties>

    <dependencies>
    <!– Spring Boot Starters –>
    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
    </dependency>
    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>
    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-validation</artifactId>
    </dependency>
    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-mail</artifactId>
    </dependency>

    <!– Database –>
    <dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-boot-starter</artifactId>
    <version>${mybatis-plus.version}</version>
    </dependency>
    <dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>8.0.33</version>
    </dependency>

    <!– JWT –>
    <dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-api</artifactId>
    <version>${jwt.version}</version>
    </dependency>
    <dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-impl</artifactId>
    <version>${jwt.version}</version>
    <scope>runtime</scope>
    </dependency>
    <dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-jackson</artifactId>
    <version>${jwt.version}</version>
    <scope>runtime</scope>
    </dependency>

    <!– Tools –>
    <dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <optional>true</optional>
    </dependency>
    <dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>fastjson</artifactId>
    <version>${fastjson.version}</version>
    </dependency>
    <dependency>
    <groupId>cn.hutool</groupId>
    <artifactId>hutool-all</artifactId>
    <version>5.8.20</version>
    </dependency>
    <dependency>
    <groupId>com.github.whvcse</groupId>
    <artifactId>easy-captcha</artifactId>
    <version>1.6.2</version>
    </dependency>

    <!– API文档 –>
    <dependency>
    <groupId>io.springfox</groupId>
    <artifactId>springfox-boot-starter</artifactId>
    <version>3.0.0</version>
    </dependency>

    <!– Test –>
    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
    </dependency>
    <dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-test</artifactId>
    <scope>test</scope>
    </dependency>
    </dependencies>

    <build>
    <plugins>
    <plugin>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-maven-plugin</artifactId>
    <configuration>
    <excludes>
    <exclude>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    </exclude>
    </excludes>
    </configuration>
    </plugin>
    </plugins>
    </build>
    </project>

    5.3 配置文件

    # application.yml
    spring:
    profiles:
    active: dev
    application:
    name: blogbackend


    # application-dev.yml
    spring:
    datasource:
    url: jdbc:mysql://localhost:3306/blog_db?useUnicode=true&characterEncoding=utf8&serverTimezone=GMT%2B8&useSSL=false
    username: root
    password: your_password
    driver-class-name: com.mysql.cj.jdbc.Driver
    hikari:
    minimum-idle: 5
    maximum-pool-size: 20
    idle-timeout: 30000
    connection-timeout: 30000

    redis:
    host: localhost
    port: 6379
    password: your_redis_password
    database: 0
    timeout: 5000ms
    lettuce:
    pool:
    max-active: 8
    max-idle: 8
    min-idle: 0

    mail:
    host: smtp.163.com
    port: 465
    username: your_email@163.com
    password: your_authorization_code
    protocol: smtp
    properties:
    mail.smtp.auth: true
    mail.smtp.ssl.enable: true
    mail.smtp.starttls.enable: true

    servlet:
    multipart:
    max-file-size: 10MB
    max-request-size: 100MB

    mybatis-plus:
    mapper-locations: classpath*:/mapper/**/*.xml
    type-aliases-package: com.blog.modules.*.entity
    global-config:
    db-config:
    id-type: auto
    logic-delete-field: deleted
    logic-delete-value: 1
    logic-not-delete-value: 0
    configuration:
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
    map-underscore-to-camel-case: true

    jwt:
    secret: your_jwt_secret_key_here_minimum_32_bytes_long
    expiration: 86400000 # 24小时
    refresh-expiration: 604800000 # 7天
    header: Authorization
    token-prefix: Bearer

    blog:
    site:
    title: 我的技术博客
    icp: 京ICP备xxxxxx号
    font:
    path: /usr/share/fonts/blog/
    default: Microsoft YaHei.ttf
    captcha:
    width: 120
    height: 40
    length: 4
    expire: 300 # 5分钟

    5.4 统一返回结果

    // common/result/Result.java
    package com.blog.common.result;

    import lombok.Data;
    import java.io.Serializable;

    @Data
    public class Result<T> implements Serializable {
    private static final long serialVersionUID = 1L;

    private int code;
    private String msg;
    private T data;
    private long timestamp;

    private Result() {
    this.timestamp = System.currentTimeMillis();
    }

    public static <T> Result<T> success() {
    Result<T> result = new Result<>();
    result.setCode(200);
    result.setMsg("操作成功");
    return result;
    }

    public static <T> Result<T> success(T data) {
    Result<T> result = new Result<>();
    result.setCode(200);
    result.setMsg("操作成功");
    result.setData(data);
    return result;
    }

    public static <T> Result<T> success(String msg, T data) {
    Result<T> result = new Result<>();
    result.setCode(200);
    result.setMsg(msg);
    result.setData(data);
    return result;
    }

    public static <T> Result<T> error() {
    Result<T> result = new Result<>();
    result.setCode(500);
    result.setMsg("操作失败");
    return result;
    }

    public static <T> Result<T> error(String msg) {
    Result<T> result = new Result<>();
    result.setCode(500);
    result.setMsg(msg);
    return result;
    }

    public static <T> Result<T> error(int code, String msg) {
    Result<T> result = new Result<>();
    result.setCode(code);
    result.setMsg(msg);
    return result;
    }

    public static <T> Result<T> unauthorized() {
    Result<T> result = new Result<>();
    result.setCode(401);
    result.setMsg("未授权");
    return result;
    }

    public static <T> Result<T> forbidden() {
    Result<T> result = new Result<>();
    result.setCode(403);
    result.setMsg("禁止访问");
    return result;
    }

    public boolean isSuccess() {
    return this.code == 200;
    }
    }

    // common/result/ResultCode.java
    package com.blog.common.result;

    public interface ResultCode {
    int SUCCESS = 200;
    int FAIL = 500;
    int UNAUTHORIZED = 401;
    int FORBIDDEN = 403;
    int NOT_FOUND = 404;
    int VALIDATE_FAILED = 400;

    String SUCCESS_MSG = "操作成功";
    String FAIL_MSG = "操作失败";
    String UNAUTHORIZED_MSG = "未授权,请先登录";
    String FORBIDDEN_MSG = "没有权限访问";
    String NOT_FOUND_MSG = "资源不存在";
    String VALIDATE_FAILED_MSG = "参数校验失败";
    }

    5.5 实体类

    // modules/auth/entity/User.java
    package com.blog.modules.auth.entity;

    import com.baomidou.mybatisplus.annotation.*;
    import lombok.Data;
    import lombok.experimental.Accessors;
    import java.time.LocalDateTime;

    @Data
    @Accessors(chain = true)
    @TableName("blog_user")
    public class User {

    @TableId(type = IdType.AUTO)
    private Long id;

    private String username;

    private String password;

    private String salt;

    private String email;

    private String phone;

    private String avatar;

    private String nickname;

    private String realName;

    private Integer gender;

    private LocalDate birthday;

    private String signature;

    private Integer status;

    private String loginIp;

    private LocalDateTime loginTime;

    private String createBy;

    @TableField(fill = FieldFill.INSERT)
    private LocalDateTime createTime;

    private String updateBy;

    @TableField(fill = FieldFill.UPDATE)
    private LocalDateTime updateTime;

    @TableLogic
    private Integer deleted;

    // 非数据库字段
    @TableField(exist = false)
    private List<Role> roles;

    @TableField(exist = false)
    private String token;
    }

    // modules/auth/dto/LoginRequest.java
    package com.blog.modules.auth.dto;

    import lombok.Data;
    import javax.validation.constraints.NotBlank;

    @Data
    public class LoginRequest {

    @NotBlank(message = "账号不能为空")
    private String account; // 可以是用户名/邮箱/手机号

    @NotBlank(message = "密码不能为空")
    private String password;

    private String captcha;

    private String captchaKey;

    private Boolean rememberMe;
    }

    // modules/auth/dto/RegisterRequest.java
    package com.blog.modules.auth.dto;

    import lombok.Data;
    import javax.validation.constraints.*;

    @Data
    public class RegisterRequest {

    @NotBlank(message = "用户名不能为空")
    @Pattern(regexp = "^[a-zA-Z0-9_]{4,20}$", message = "用户名必须是4-20位字母、数字或下划线")
    private String username;

    @NotBlank(message = "密码不能为空")
    @Pattern(regexp = "^(?=.*[0-9])(?=.*[a-zA-Z])(?=.*[@#$%^&+=])(?=\\\\S+$).{8,20}$",
    message = "密码必须包含字母、数字和特殊字符,长度8-20位")
    private String password;

    @NotBlank(message = "确认密码不能为空")
    private String confirmPassword;

    @Email(message = "邮箱格式不正确")
    private String email;

    @Pattern(regexp = "^1[3-9]\\\\d{9}$", message = "手机号格式不正确")
    private String phone;

    @NotBlank(message = "验证码不能为空")
    private String captcha;

    @NotBlank(message = "验证码Key不能为空")
    private String captchaKey;

    @AssertTrue(message = "必须同意用户协议")
    private Boolean agreement;
    }

    5.6 工具类

    // security/utils/JwtUtil.java
    package com.blog.security.utils;

    import io.jsonwebtoken.*;
    import io.jsonwebtoken.security.Keys;
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.beans.factory.annotation.Value;
    import org.springframework.stereotype.Component;
    import java.security.Key;
    import java.util.Date;
    import java.util.HashMap;
    import java.util.Map;
    import java.util.function.Function;

    @Slf4j
    @Component
    public class JwtUtil {

    @Value("${jwt.secret}")
    private String secret;

    @Value("${jwt.expiration}")
    private Long expiration;

    @Value("${jwt.refresh-expiration}")
    private Long refreshExpiration;

    private Key getSigningKey() {
    return Keys.hmacShaKeyFor(secret.getBytes());
    }

    public String generateToken(String username) {
    Map<String, Object> claims = new HashMap<>();
    return createToken(claims, username, expiration);
    }

    public String generateRefreshToken(String username) {
    Map<String, Object> claims = new HashMap<>();
    return createToken(claims, username, refreshExpiration);
    }

    private String createToken(Map<String, Object> claims, String subject, Long expiration) {
    return Jwts.builder()
    .setClaims(claims)
    .setSubject(subject)
    .setIssuedAt(new Date(System.currentTimeMillis()))
    .setExpiration(new Date(System.currentTimeMillis() + expiration))
    .signWith(getSigningKey(), SignatureAlgorithm.HS256)
    .compact();
    }

    public String extractUsername(String token) {
    return extractClaim(token, Claims::getSubject);
    }

    public Date extractExpiration(String token) {
    return extractClaim(token, Claims::getExpiration);
    }

    public <T> T extractClaim(String token, Function<Claims, T> claimsResolver) {
    final Claims claims = extractAllClaims(token);
    return claimsResolver.apply(claims);
    }

    private Claims extractAllClaims(String token) {
    try {
    return Jwts.parserBuilder()
    .setSigningKey(getSigningKey())
    .build()
    .parseClaimsJws(token)
    .getBody();
    } catch (ExpiredJwtException e) {
    log.error("Token已过期: {}", e.getMessage());
    throw e;
    } catch (JwtException e) {
    log.error("Token解析失败: {}", e.getMessage());
    throw e;
    }
    }

    private Boolean isTokenExpired(String token) {
    try {
    Date expiration = extractExpiration(token);
    return expiration.before(new Date());
    } catch (ExpiredJwtException e) {
    return true;
    }
    }

    public Boolean validateToken(String token, String username) {
    try {
    String extractedUsername = extractUsername(token);
    return (extractedUsername.equals(username) && !isTokenExpired(token));
    } catch (Exception e) {
    log.error("Token验证失败: {}", e.getMessage());
    return false;
    }
    }

    public Boolean validateToken(String token) {
    try {
    return !isTokenExpired(token);
    } catch (Exception e) {
    return false;
    }
    }

    public Long getRemainingTime(String token) {
    Date expiration = extractExpiration(token);
    long remaining = expiration.getTime() System.currentTimeMillis();
    return remaining > 0 ? remaining : 0;
    }
    }

    // common/util/CaptchaUtil.java
    package com.blog.common.util;

    import com.wf.captcha.ArithmeticCaptcha;
    import com.wf.captcha.GifCaptcha;
    import com.wf.captcha.SpecCaptcha;
    import com.wf.captcha.base.Captcha;
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.beans.factory.annotation.Value;
    import org.springframework.data.redis.core.StringRedisTemplate;
    import org.springframework.stereotype.Component;
    import java.util.UUID;
    import java.util.concurrent.TimeUnit;

    @Slf4j
    @Component
    public class CaptchaUtil {

    @Value("${blog.captcha.width:120}")
    private int width;

    @Value("${blog.captcha.height:40}")
    private int height;

    @Value("${blog.captcha.length:4}")
    private int length;

    @Value("${blog.captcha.expire:300}")
    private int expire;

    private final StringRedisTemplate redisTemplate;

    public CaptchaUtil(StringRedisTemplate redisTemplate) {
    this.redisTemplate = redisTemplate;
    }

    /**
    * 生成图形验证码(标准类型)
    */

    public CaptchaInfo generateSpecCaptcha() {
    SpecCaptcha captcha = new SpecCaptcha(width, height, length);
    return saveCaptcha(captcha);
    }

    /**
    * 生成图形验证码(GIF类型)
    */

    public CaptchaInfo generateGifCaptcha() {
    GifCaptcha captcha = new GifCaptcha(width, height, length);
    return saveCaptcha(captcha);
    }

    /**
    * 生成算术验证码
    */

    public CaptchaInfo generateArithmeticCaptcha() {
    ArithmeticCaptcha captcha = new ArithmeticCaptcha(width, height);
    captcha.setLen(2); // 运算位数
    return saveCaptcha(captcha);
    }

    /**
    * 保存验证码到Redis
    */

    private CaptchaInfo saveCaptcha(Captcha captcha) {
    String key = UUID.randomUUID().toString().replace("-", "");
    String code = captcha.text();

    // 统一转为大写存储,实现忽略大小写验证
    code = code.toUpperCase();

    // 存储到Redis
    redisTemplate.opsForValue().set(
    "captcha:" + key,
    code,
    expire,
    TimeUnit.SECONDS
    );

    CaptchaInfo info = new CaptchaInfo();
    info.setKey(key);
    info.setImage(captcha.toBase64());
    info.setExpire(expire);

    log.debug("验证码生成成功, key: {}, code: {}", key, code);
    return info;
    }

    /**
    * 验证验证码(忽略大小写)
    */

    public boolean validate(String key, String code) {
    if (key == null || code == null) {
    return false;
    }

    String cacheCode = redisTemplate.opsForValue().get("captcha:" + key);
    if (cacheCode == null) {
    log.debug("验证码已过期或不存在, key: {}", key);
    return false;
    }

    // 删除已使用的验证码
    redisTemplate.delete("captcha:" + key);

    // 忽略大小写验证
    boolean result = cacheCode.equalsIgnoreCase(code);
    log.debug("验证码验证, key: {}, input: {}, cache: {}, result: {}",
    key, code, cacheCode, result);

    return result;
    }

    /**
    * 验证码信息类
    */

    @Data
    public static class CaptchaInfo {
    private String key;
    private String image;
    private int expire;
    }
    }

    // common/util/PasswordEncoder.java
    package com.blog.common.util;

    import cn.hutool.crypto.SecureUtil;
    import cn.hutool.crypto.digest.BCrypt;
    import org.springframework.stereotype.Component;
    import java.util.UUID;

    @Component
    public class PasswordEncoder {

    /**
    * 生成BCrypt加密密码
    */

    public String encode(String rawPassword) {
    return BCrypt.hashpw(rawPassword, BCrypt.gensalt());
    }

    /**
    * 验证BCrypt密码
    */

    public boolean matches(String rawPassword, String encodedPassword) {
    if (encodedPassword == null || encodedPassword.isEmpty()) {
    return false;
    }
    return BCrypt.checkpw(rawPassword, encodedPassword);
    }

    /**
    * 生成MD5加密密码(带盐)
    */

    public String encodeMd5(String rawPassword, String salt) {
    return SecureUtil.md5(rawPassword + salt);
    }

    /**
    * 生成随机盐
    */

    public String generateSalt() {
    return UUID.randomUUID().toString().replace("-", "").substring(0, 16);
    }

    /**
    * 验证MD5密码
    */

    public boolean matchesMd5(String rawPassword, String salt, String encodedPassword) {
    String newEncoded = encodeMd5(rawPassword, salt);
    return newEncoded.equals(encodedPassword);
    }
    }

    5.7 Service层实现

    // modules/auth/service/UserService.java
    package com.blog.modules.auth.service;

    import com.baomidou.mybatisplus.extension.service.IService;
    import com.blog.modules.auth.dto.LoginRequest;
    import com.blog.modules.auth.dto.RegisterRequest;
    import com.blog.modules.auth.entity.User;
    import com.blog.modules.auth.vo.LoginVO;

    public interface UserService extends IService<User> {

    /**
    * 用户注册
    */

    User register(RegisterRequest request);

    /**
    * 用户登录
    */

    LoginVO login(LoginRequest request);

    /**
    * 刷新Token
    */

    String refreshToken(String refreshToken);

    /**
    * 退出登录
    */

    void logout(String token);

    /**
    * 根据用户名查询用户
    */

    User getByUsername(String username);

    /**
    * 根据邮箱查询用户
    */

    User getByEmail(String email);

    /**
    * 根据手机号查询用户
    */

    User getByPhone(String phone);

    /**
    * 检查用户名是否存在
    */

    boolean checkUsernameExists(String username);

    /**
    * 检查邮箱是否存在
    */

    boolean checkEmailExists(String email);

    /**
    * 检查手机号是否存在
    */

    boolean checkPhoneExists(String phone);

    /**
    * 更新最后登录信息
    */

    void updateLoginInfo(Long userId, String ip);

    /**
    * 修改密码
    */

    boolean changePassword(Long userId, String oldPassword, String newPassword);

    /**
    * 重置密码
    */

    boolean resetPassword(String email, String newPassword);
    }

    // modules/auth/service/impl/UserServiceImpl.java
    package com.blog.modules.auth.service.impl;

    import cn.hutool.core.util.IdUtil;
    import cn.hutool.core.util.StrUtil;
    import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
    import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
    import com.blog.common.exception.BusinessException;
    import com.blog.common.util.CaptchaUtil;
    import com.blog.common.util.PasswordEncoder;
    import com.blog.modules.auth.dto.LoginRequest;
    import com.blog.modules.auth.dto.RegisterRequest;
    import com.blog.modules.auth.entity.User;
    import com.blog.modules.auth.entity.UserLoginLog;
    import com.blog.modules.auth.mapper.UserMapper;
    import com.blog.modules.auth.service.UserLoginLogService;
    import com.blog.modules.auth.service.UserService;
    import com.blog.modules.auth.vo.LoginVO;
    import com.blog.security.utils.JwtUtil;
    import lombok.RequiredArgsConstructor;
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.data.redis.core.StringRedisTemplate;
    import org.springframework.stereotype.Service;
    import org.springframework.transaction.annotation.Transactional;
    import javax.servlet.http.HttpServletRequest;
    import java.time.LocalDateTime;
    import java.util.concurrent.TimeUnit;

    @Slf4j
    @Service
    @RequiredArgsConstructor
    public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {

    private final PasswordEncoder passwordEncoder;
    private final JwtUtil jwtUtil;
    private final CaptchaUtil captchaUtil;
    private final StringRedisTemplate redisTemplate;
    private final UserLoginLogService loginLogService;
    private final HttpServletRequest request;

    @Override
    @Transactional(rollbackFor = Exception.class)
    public User register(RegisterRequest req) {
    log.info("用户注册: {}", req.getUsername());

    // 验证码校验
    if (!captchaUtil.validate(req.getCaptchaKey(), req.getCaptcha())) {
    throw new BusinessException("验证码错误");
    }

    // 密码一致性校验
    if (!req.getPassword().equals(req.getConfirmPassword())) {
    throw new BusinessException("两次输入的密码不一致");
    }

    // 检查用户名是否存在
    if (checkUsernameExists(req.getUsername())) {
    throw new BusinessException("用户名已存在");
    }

    // 检查邮箱是否存在
    if (StrUtil.isNotBlank(req.getEmail()) && checkEmailExists(req.getEmail())) {
    throw new BusinessException("邮箱已被注册");
    }

    // 检查手机号是否存在
    if (StrUtil.isNotBlank(req.getPhone()) && checkPhoneExists(req.getPhone())) {
    throw new BusinessException("手机号已被注册");
    }

    // 创建新用户
    User user = new User();
    user.setUsername(req.getUsername());
    user.setPassword(passwordEncoder.encode(req.getPassword()));
    user.setEmail(req.getEmail());
    user.setPhone(req.getPhone());
    user.setNickname(req.getUsername());
    user.setStatus(1); // 正常状态
    user.setCreateTime(LocalDateTime.now());

    // 保存用户
    save(user);

    // 分配默认角色(普通用户)
    // userRoleService.assignDefaultRole(user.getId());

    log.info("用户注册成功: {}, id: {}", req.getUsername(), user.getId());
    return user;
    }

    @Override
    public LoginVO login(LoginRequest req) {
    log.info("用户登录: {}", req.getAccount());

    // 登录失败次数检查
    String loginFailKey = "login:fail:" + req.getAccount();
    String failCountStr = redisTemplate.opsForValue().get(loginFailKey);
    int failCount = failCountStr == null ? 0 : Integer.parseInt(failCountStr);

    // 失败超过3次需要验证码
    if (failCount >= 3) {
    if (StrUtil.isBlank(req.getCaptcha()) || StrUtil.isBlank(req.getCaptchaKey())) {
    throw new BusinessException("请输入验证码");
    }
    if (!captchaUtil.validate(req.getCaptchaKey(), req.getCaptcha())) {
    throw new BusinessException("验证码错误");
    }
    }

    // 查询用户
    User user = null;
    if (req.getAccount().contains("@")) {
    user = getByEmail(req.getAccount());
    } else if (req.getAccount().matches("^1[3-9]\\\\d{9}$")) {
    user = getByPhone(req.getAccount());
    } else {
    user = getByUsername(req.getAccount());
    }

    // 用户不存在或密码错误
    if (user == null || !passwordEncoder.matches(req.getPassword(), user.getPassword())) {
    // 记录失败次数
    redisTemplate.opsForValue().set(loginFailKey, String.valueOf(failCount + 1), 30, TimeUnit.MINUTES);
    throw new BusinessException("用户名或密码错误");
    }

    // 检查用户状态
    if (user.getStatus() != 1) {
    throw new BusinessException("账号已被禁用");
    }

    // 登录成功,清除失败记录
    redisTemplate.delete(loginFailKey);

    // 生成Token
    String token = jwtUtil.generateToken(user.getUsername());
    String refreshToken = jwtUtil.generateRefreshToken(user.getUsername());

    // 更新登录信息
    String ip = getClientIp();
    updateLoginInfo(user.getId(), ip);

    // 记录登录日志
    UserLoginLog loginLog = new UserLoginLog();
    loginLog.setUserId(user.getId());
    loginLog.setUsername(user.getUsername());
    loginLog.setIp(ip);
    loginLog.setStatus(1);
    loginLog.setCreateTime(LocalDateTime.now());
    loginLogService.save(loginLog);

    // 返回登录信息
    LoginVO vo = new LoginVO();
    vo.setToken(token);
    vo.setRefreshToken(refreshToken);
    vo.setExpiresIn(jwtUtil.getRemainingTime(token));
    vo.setUser(user);

    log.info("用户登录成功: {}, id: {}", req.getAccount(), user.getId());
    return vo;
    }

    @Override
    public String refreshToken(String refreshToken) {
    if (!jwtUtil.validateToken(refreshToken)) {
    throw new BusinessException("刷新Token无效或已过期");
    }

    String username = jwtUtil.extractUsername(refreshToken);
    User user = getByUsername(username);

    if (user == null || user.getStatus() != 1) {
    throw new BusinessException("用户不存在或已被禁用");
    }

    return jwtUtil.generateToken(username);
    }

    @Override
    public void logout(String token) {
    // 将Token加入黑名单
    if (StrUtil.isNotBlank(token)) {
    long remaining = jwtUtil.getRemainingTime(token);
    if (remaining > 0) {
    redisTemplate.opsForValue().set("blacklist:token:" + token, "1", remaining, TimeUnit.MILLISECONDS);
    }
    }
    log.info("用户退出登录");
    }

    @Override
    public User getByUsername(String username) {
    return getOne(new LambdaQueryWrapper<User>()
    .eq(User::getUsername, username)
    .eq(User::getDeleted, 0));
    }

    @Override
    public User getByEmail(String email) {
    return getOne(new LambdaQueryWrapper<User>()
    .eq(User::getEmail, email)
    .eq(User::getDeleted, 0));
    }

    @Override
    public User getByPhone(String phone) {
    return getOne(new LambdaQueryWrapper<User>()
    .eq(User::getPhone, phone)
    .eq(User::getDeleted, 0));
    }

    @Override
    public boolean checkUsernameExists(String username) {
    return count(new LambdaQueryWrapper<User>()
    .eq(User::getUsername, username)) > 0;
    }

    @Override
    public boolean checkEmailExists(String email) {
    return count(new LambdaQueryWrapper<User>()
    .eq(User::getEmail, email)) > 0;
    }

    @Override
    public boolean checkPhoneExists(String phone) {
    return count(new LambdaQueryWrapper<User>()
    .eq(User::getPhone, phone)) > 0;
    }

    @Override
    public void updateLoginInfo(Long userId, String ip) {
    User user = new User();
    user.setId(userId);
    user.setLoginIp(ip);
    user.setLoginTime(LocalDateTime.now());
    updateById(user);
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public boolean changePassword(Long userId, String oldPassword, String newPassword) {
    User user = getById(userId);
    if (user == null) {
    throw new BusinessException("用户不存在");
    }

    if (!passwordEncoder.matches(oldPassword, user.getPassword())) {
    throw new BusinessException("原密码错误");
    }

    user.setPassword(passwordEncoder.encode(newPassword));
    return updateById(user);
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public boolean resetPassword(String email, String newPassword) {
    User user = getByEmail(email);
    if (user == null) {
    throw new BusinessException("邮箱不存在");
    }

    user.setPassword(passwordEncoder.encode(newPassword));
    return updateById(user);
    }

    /**
    * 获取客户端IP
    */

    private String getClientIp() {
    String ip = request.getHeader("X-Forwarded-For");
    if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
    ip = request.getHeader("Proxy-Client-IP");
    }
    if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
    ip = request.getHeader("WL-Proxy-Client-IP");
    }
    if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
    ip = request.getRemoteAddr();
    }
    return ip;
    }
    }

    5.8 Controller层实现

    // modules/auth/controller/AuthController.java
    package com.blog.modules.auth.controller;

    import com.blog.common.result.Result;
    import com.blog.common.util.CaptchaUtil;
    import com.blog.modules.auth.dto.LoginRequest;
    import com.blog.modules.auth.dto.RegisterRequest;
    import com.blog.modules.auth.service.UserService;
    import com.blog.modules.auth.vo.LoginVO;
    import io.swagger.annotations.Api;
    import io.swagger.annotations.ApiOperation;
    import lombok.RequiredArgsConstructor;
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.web.bind.annotation.*;
    import javax.servlet.http.HttpServletRequest;
    import javax.validation.Valid;

    @Slf4j
    @Api(tags = "认证管理")
    @RestController
    @RequestMapping("/api/auth")
    @RequiredArgsConstructor
    public class AuthController {

    private final UserService userService;
    private final CaptchaUtil captchaUtil;

    @ApiOperation("获取验证码")
    @GetMapping("/captcha")
    public Result<CaptchaUtil.CaptchaInfo> getCaptcha() {
    CaptchaUtil.CaptchaInfo captcha = captchaUtil.generateSpecCaptcha();
    return Result.success(captcha);
    }

    @ApiOperation("用户注册")
    @PostMapping("/register")
    public Result<?> register(@Valid @RequestBody RegisterRequest request) {
    userService.register(request);
    return Result.success("注册成功");
    }

    @ApiOperation("用户登录")
    @PostMapping("/login")
    public Result<LoginVO> login(@Valid @RequestBody LoginRequest request) {
    LoginVO vo = userService.login(request);
    return Result.success("登录成功", vo);
    }

    @ApiOperation("刷新Token")
    @PostMapping("/refresh")
    public Result<String> refreshToken(@RequestParam String refreshToken) {
    String newToken = userService.refreshToken(refreshToken);
    return Result.success(newToken);
    }

    @ApiOperation("退出登录")
    @PostMapping("/logout")
    public Result<?> logout(HttpServletRequest request) {
    String token = request.getHeader("Authorization");
    if (token != null && token.startsWith("Bearer ")) {
    token = token.substring(7);
    }
    userService.logout(token);
    return Result.success("退出成功");
    }

    @ApiOperation("检查用户名是否存在")
    @GetMapping("/check/username")
    public Result<Boolean> checkUsername(@RequestParam String username) {
    boolean exists = userService.checkUsernameExists(username);
    return Result.success(exists);
    }

    @ApiOperation("检查邮箱是否存在")
    @GetMapping("/check/email")
    public Result<Boolean> checkEmail(@RequestParam String email) {
    boolean exists = userService.checkEmailExists(email);
    return Result.success(exists);
    }

    @ApiOperation("检查手机号是否存在")
    @GetMapping("/check/phone")
    public Result<Boolean> checkPhone(@RequestParam String phone) {
    boolean exists = userService.checkPhoneExists(phone);
    return Result.success(exists);
    }
    }

    5.9 测试用例

    // src/test/java/com/blog/modules/auth/AuthControllerTest.java
    package com.blog.modules.auth;

    import com.alibaba.fastjson.JSON;
    import com.blog.modules.auth.dto.LoginRequest;
    import com.blog.modules.auth.dto.RegisterRequest;
    import lombok.extern.slf4j.Slf4j;
    import org.junit.jupiter.api.Test;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
    import org.springframework.boot.test.context.SpringBootTest;
    import org.springframework.http.MediaType;
    import org.springframework.test.web.servlet.MockMvc;
    import org.springframework.test.web.servlet.MvcResult;
    import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
    import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;

    @Slf4j
    @SpringBootTest
    @AutoConfigureMockMvc
    public class AuthControllerTest {

    @Autowired
    private MockMvc mockMvc;

    @Test
    public void testGetCaptcha() throws Exception {
    MvcResult result = mockMvc.perform(get("/api/auth/captcha"))
    .andExpect(status().isOk())
    .andExpect(jsonPath("$.code").value(200))
    .andExpect(jsonPath("$.data.key").exists())
    .andExpect(jsonPath("$.data.image").exists())
    .andReturn();

    log.info("验证码响应: {}", result.getResponse().getContentAsString());
    }

    @Test
    public void testRegister() throws Exception {
    // 先获取验证码
    MvcResult captchaResult = mockMvc.perform(get("/api/auth/captcha"))
    .andReturn();
    String captchaResponse = captchaResult.getResponse().getContentAsString();
    // 解析验证码key和图片(实际测试时需要从响应中提取)
    String captchaKey = "test-key"; // 这里应该是实际获取的key
    String captchaCode = "1234"; // 这里应该是实际验证码

    RegisterRequest request = new RegisterRequest();
    request.setUsername("testuser");
    request.setPassword("Test@123456");
    request.setConfirmPassword("Test@123456");
    request.setEmail("test@example.com");
    request.setCaptchaKey(captchaKey);
    request.setCaptcha(captchaCode);
    request.setAgreement(true);

    MvcResult result = mockMvc.perform(post("/api/auth/register")
    .contentType(MediaType.APPLICATION_JSON)
    .content(JSON.toJSONString(request)))
    .andExpect(status().isOk())
    .andExpect(jsonPath("$.code").value(200))
    .andReturn();

    log.info("注册响应: {}", result.getResponse().getContentAsString());
    }

    @Test
    public void testLogin() throws Exception {
    LoginRequest request = new LoginRequest();
    request.setAccount("testuser");
    request.setPassword("Test@123456");
    request.setRememberMe(true);

    MvcResult result = mockMvc.perform(post("/api/auth/login")
    .contentType(MediaType.APPLICATION_JSON)
    .content(JSON.toJSONString(request)))
    .andExpect(status().isOk())
    .andExpect(jsonPath("$.code").value(200))
    .andExpect(jsonPath("$.data.token").exists())
    .andReturn();

    log.info("登录响应: {}", result.getResponse().getContentAsString());
    }

    @Test
    public void testCheckUsername() throws Exception {
    MvcResult result = mockMvc.perform(get("/api/auth/check/username")
    .param("username", "testuser"))
    .andExpect(status().isOk())
    .andExpect(jsonPath("$.code").value(200))
    .andReturn();

    log.info("检查用户名响应: {}", result.getResponse().getContentAsString());
    }
    }

    5.10 使用Postman测试

    // 1. 获取验证码
    GET http://localhost:8080/api/auth/captcha

    // 响应示例
    {
    "code": 200,
    "msg": "操作成功",
    "data": {
    "key": "a1b2c3d4e5f6",
    "image": "data:image/png;base64,iVBORw0KGgo…",
    "expire": 300
    },
    "timestamp": 1701234567890
    }

    // 2. 用户注册
    POST http://localhost:8080/api/auth/register
    ContentType: application/json

    {
    "username": "john123",
    "password": "Pass@123456",
    "confirmPassword": "Pass@123456",
    "email": "john@example.com",
    "phone": "13800138000",
    "captchaKey": "a1b2c3d4e5f6",
    "captcha": "ABCD",
    "agreement": true
    }

    // 3. 用户登录
    POST http://localhost:8080/api/auth/login
    ContentType: application/json

    {
    "account": "john123",
    "password": "Pass@123456",
    "rememberMe": true
    }

    // 响应示例
    {
    "code": 200,
    "msg": "登录成功",
    "data": {
    "token": "eyJhbGciOiJIUzI1NiJ9…",
    "refreshToken": "eyJhbGciOiJIUzI1NiJ9…",
    "expiresIn": 86400000,
    "user": {
    "id": 1,
    "username": "john123",
    "email": "john@example.com",
    "phone": "13800138000",
    "nickname": "john123",
    "avatar": null,
    "status": 1
    }
    },
    "timestamp": 1701234567890
    }

    // 4. 刷新Token
    POST http://localhost:8080/api/auth/refresh?refreshToken=eyJhbGciOiJIUzI1NiJ9

    // 5. 退出登录
    POST http://localhost:8080/api/auth/logout
    Authorization: Bearer eyJhbGciOiJIUzI1NiJ9


    六、前端代码环境搭建和编写

    6.1 前端项目初始化

    # 创建项目目录
    mkdir -p ~/projects/blog-frontend
    cd ~/projects/blog-frontend

    # 使用Vite创建Vue3项目
    npm create vite@latest .–template vue

    # 安装依赖
    npm install

    # 安装Element Plus
    npm install element-plus

    # 安装路由
    npm install vue-router@4

    # 安装状态管理
    npm install pinia

    # 安装HTTP客户端
    npm install axios

    # 安装工具库
    npm install dayjs
    npm install lodash-es
    npm install @vueuse/core

    # 安装验证码组件
    npm install vue3-verify-code

    # 安装开发依赖
    npm install -D sass
    npm install -D @types/node

    6.2 项目结构

    blog-frontend/
    ├── public/ # 静态资源
    │ ├── favicon.ico # 网站图标
    │ └── index.html # 入口HTML
    ├── src/
    │ ├── api/ # API接口
    │ │ ├── auth.js # 认证相关
    │ │ ├── user.js # 用户相关
    │ │ └── request.js # axios配置
    │ ├── assets/ # 资源文件
    │ │ ├── fonts/ # 字体文件
    │ │ ├── images/ # 图片
    │ │ └── styles/ # 全局样式
    │ │ ├── index.scss # 入口样式
    │ │ ├── theme.scss # 主题样式
    │ │ └── mourning.scss # 追悼模式样式
    │ ├── components/ # 公共组件
    │ │ ├── common/ # 通用组件
    │ │ │ ├── Captcha.vue # 验证码组件
    │ │ │ └── ThemeSwitch.vue # 主题切换
    │ │ └── layout/ # 布局组件
    │ │ ├── Header.vue # 头部
    │ │ ├── Footer.vue # 底部
    │ │ └── Sidebar.vue # 侧边栏
    │ ├── composables/ # 组合式函数
    │ │ ├── useTheme.js # 主题管理
    │ │ ├── useAuth.js # 认证管理
    │ │ └── useCaptcha.js # 验证码管理
    │ ├── router/ # 路由配置
    │ │ └── index.js
    │ ├── stores/ # Pinia状态
    │ │ ├── user.js # 用户状态
    │ │ └── theme.js # 主题状态
    │ ├── utils/ # 工具函数
    │ │ ├── auth.js # 认证工具
    │ │ ├── storage.js # 存储工具
    │ │ └── validate.js # 验证工具
    │ ├── views/ # 页面视图
    │ │ ├── auth/ # 认证相关
    │ │ │ ├── Login.vue # 登录页
    │ │ │ └── Register.vue # 注册页
    │ │ ├── home/ # 首页
    │ │ │ └── Index.vue
    │ │ └── user/ # 用户中心
    │ │ ├── Profile.vue
    │ │ └── Settings.vue
    │ ├── App.vue # 根组件
    │ └── main.js # 入口文件
    ├── .env.development # 开发环境变量
    ├── .env.production # 生产环境变量
    ├── index.html # 模板文件
    ├── vite.config.js # Vite配置
    └── package.json # 依赖配置

    6.3 环境变量配置

    # .env.development
    VITE_APP_TITLE=博客系统-开发版
    VITE_APP_BASE_API=/api
    VITE_APP_BASE_URL=http://localhost:8080
    VITE_APP_CAPTCHA_IGNORE_CASE=true

    # .env.production
    VITE_APP_TITLE=我的技术博客
    VITE_APP_BASE_API=/api
    VITE_APP_BASE_URL=https://api.yourblog.com
    VITE_APP_CAPTCHA_IGNORE_CASE=true

    6.4 入口文件配置

    // src/main.js
    import { createApp } from 'vue'
    import { createPinia } from 'pinia'
    import ElementPlus from 'element-plus'
    import 'element-plus/dist/index.css'
    import * as ElementPlusIconsVue from '@element-plus/icons-vue'
    import App from './App.vue'
    import router from './router'
    import './assets/styles/index.scss'

    // 创建应用
    const app = createApp(App)

    // 注册Element Plus图标
    for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
    app.component(key, component)
    }

    // 使用插件
    app.use(createPinia())
    app.use(router)
    app.use(ElementPlus, {
    size: 'default',
    zIndex: 3000
    })

    // 挂载应用
    app.mount('#app')

    6.5 路由配置

    // src/router/index.js
    import { createRouter, createWebHistory } from 'vue-router'
    import { useUserStore } from '@/stores/user'

    const routes = [
    {
    path: '/',
    name: 'home',
    component: () => import('@/views/home/Index.vue')
    },
    {
    path: '/auth',
    name: 'auth',
    redirect: '/auth/login',
    children: [
    {
    path: 'login',
    name: 'login',
    component: () => import('@/views/auth/Login.vue'),
    meta: { requiresGuest: true }
    },
    {
    path: 'register',
    name: 'register',
    component: () => import('@/views/auth/Register.vue'),
    meta: { requiresGuest: true }
    }
    ]
    },
    {
    path: '/user',
    name: 'user',
    component: () => import('@/views/user/Profile.vue'),
    meta: { requiresAuth: true },
    children: [
    {
    path: 'profile',
    name: 'profile',
    component: () => import('@/views/user/Profile.vue')
    },
    {
    path: 'settings',
    name: 'settings',
    component: () => import('@/views/user/Settings.vue')
    }
    ]
    }
    ]

    const router = createRouter({
    history: createWebHistory(import.meta.env.BASE_URL),
    routes
    })

    // 路由守卫
    router.beforeEach(async (to, from, next) => {
    const userStore = useUserStore()
    const token = userStore.token

    // 设置页面标题
    if (to.meta.title) {
    document.title = `${to.meta.title}${import.meta.env.VITE_APP_TITLE}`
    } else {
    document.title = import.meta.env.VITE_APP_TITLE
    }

    // 需要认证的页面
    if (to.meta.requiresAuth && !token) {
    next({ name: 'login', query: { redirect: to.fullPath } })
    }
    // 需要游客的页面(已登录用户不能访问)
    else if (to.meta.requiresGuest && token) {
    next({ name: 'home' })
    }
    else {
    next()
    }
    })

    export default router

    6.6 状态管理

    // src/stores/user.js
    import { defineStore } from 'pinia'
    import { ref, computed } from 'vue'
    import { login, register, logout, getInfo } from '@/api/auth'
    import { removeToken, setToken, getToken } from '@/utils/auth'

    export const useUserStore = defineStore('user', () => {
    // state
    const token = ref(getToken())
    const userInfo = ref(null)
    const roles = ref([])
    const permissions = ref([])

    // getters
    const isLoggedIn = computed(() => !!token.value)
    const username = computed(() => userInfo.value?.username || '')
    const avatar = computed(() => userInfo.value?.avatar || '/default-avatar.png')

    // actions
    function setUserToken(userToken) {
    token.value = userToken
    setToken(userToken)
    }

    async function loginAction(loginData) {
    try {
    const res = await login(loginData)
    const { token, refreshToken, user } = res.data
    setUserToken(token)
    userInfo.value = user
    return res
    } catch (error) {
    return Promise.reject(error)
    }
    }

    async function registerAction(registerData) {
    return await register(registerData)
    }

    async function logoutAction() {
    await logout()
    token.value = null
    userInfo.value = null
    roles.value = []
    permissions.value = []
    removeToken()
    }

    async function getUserInfo() {
    try {
    const res = await getInfo()
    userInfo.value = res.data.user
    roles.value = res.data.roles || []
    permissions.value = res.data.permissions || []
    return res
    } catch (error) {
    return Promise.reject(error)
    }
    }

    return {
    token,
    userInfo,
    roles,
    permissions,
    isLoggedIn,
    username,
    avatar,
    loginAction,
    registerAction,
    logoutAction,
    getUserInfo,
    setUserToken
    }
    })

    // src/stores/theme.js
    import { defineStore } from 'pinia'
    import { ref, watch } from 'vue'

    export const useThemeStore = defineStore('theme', () => {
    // state
    const theme = ref(localStorage.getItem('theme') || 'light')
    const mourningMode = ref(localStorage.getItem('mourningMode') === 'true')

    // 监听主题变化
    watch(theme, (newTheme) => {
    localStorage.setItem('theme', newTheme)
    applyTheme(newTheme)
    })

    watch(mourningMode, (newValue) => {
    localStorage.setItem('mourningMode', newValue)
    applyMourningMode(newValue)
    })

    // actions
    function setTheme(newTheme) {
    theme.value = newTheme
    }

    function toggleMourningMode() {
    mourningMode.value = !mourningMode.value
    }

    function enableMourningMode() {
    mourningMode.value = true
    }

    function disableMourningMode() {
    mourningMode.value = false
    }

    // 应用主题
    function applyTheme(themeName) {
    document.documentElement.setAttribute('data-theme', themeName)
    }

    // 应用追悼模式
    function applyMourningMode(enabled) {
    if (enabled) {
    document.body.classList.add('mourning-mode')
    } else {
    document.body.classList.remove('mourning-mode')
    }
    }

    // 初始化主题
    function initTheme() {
    applyTheme(theme.value)
    applyMourningMode(mourningMode.value)

    // 检查是否追悼日期
    checkMourningDate()
    }

    // 检查追悼日期
    function checkMourningDate() {
    const today = new Date()
    const month = today.getMonth() + 1
    const day = today.getDate()

    // 清明节(4月4日或5日)
    const isQingming = month === 4 && (day === 4 || day === 5)
    // 512汶川地震纪念日
    const isWenchuan = month === 5 && day === 12
    // 南京大屠杀纪念日
    const isNanjing = month === 12 && day === 13

    if (isQingming || isWenchuan || isNanjing) {
    enableMourningMode()
    }
    }

    return {
    theme,
    mourningMode,
    setTheme,
    toggleMourningMode,
    enableMourningMode,
    disableMourningMode,
    initTheme
    }
    })

    6.7 API配置

    // src/api/request.js
    import axios from 'axios'
    import { ElMessage, ElMessageBox } from 'element-plus'
    import { useUserStore } from '@/stores/user'
    import router from '@/router'

    // 创建axios实例
    const request = axios.create({
    baseURL: import.meta.env.VITE_APP_BASE_URL + import.meta.env.VITE_APP_BASE_API,
    timeout: 30000,
    withCredentials: true
    })

    // 请求拦截器
    request.interceptors.request.use(
    config => {
    const userStore = useUserStore()
    const token = userStore.token
    if (token) {
    config.headers['Authorization'] = 'Bearer ' + token
    }

    // 添加时间戳防止缓存
    if (config.method === 'get') {
    config.params = { config.params, _t: Date.now() }
    }

    return config
    },
    error => {
    return Promise.reject(error)
    }
    )

    // 响应拦截器
    request.interceptors.response.use(
    response => {
    const res = response.data

    // 如果响应不是200,说明出错了
    if (res.code !== 200) {
    ElMessage({
    message: res.msg || '系统繁忙,请稍后重试',
    type: 'error',
    duration: 5 * 1000
    })

    // 401: 未登录或Token过期
    if (res.code === 401) {
    ElMessageBox.confirm('登录状态已过期,请重新登录', '系统提示', {
    confirmButtonText: '重新登录',
    cancelButtonText: '取消',
    type: 'warning'
    }).then(() => {
    const userStore = useUserStore()
    userStore.logoutAction()
    router.push('/auth/login')
    })
    }
    return Promise.reject(new Error(res.msg || 'Error'))
    } else {
    return res
    }
    },
    error => {
    console.log('err' + error)
    let message = error.message || '请求失败'

    if (error.response) {
    switch (error.response.status) {
    case 400:
    message = '请求错误'
    break
    case 401:
    message = '未授权,请登录'
    break
    case 403:
    message = '拒绝访问'
    break
    case 404:
    message = '请求地址不存在'
    break
    case 408:
    message = '请求超时'
    break
    case 500:
    message = '服务器内部错误'
    break
    default:
    message = `连接错误${error.response.status}`
    }
    }

    ElMessage({
    message: message,
    type: 'error',
    duration: 5 * 1000
    })

    return Promise.reject(error)
    }
    )

    export default request

    // src/api/auth.js
    import request from './request'

    // 登录
    export function login(data) {
    return request({
    url: '/auth/login',
    method: 'post',
    data
    })
    }

    // 注册
    export function register(data) {
    return request({
    url: '/auth/register',
    method: 'post',
    data
    })
    }

    // 退出登录
    export function logout() {
    return request({
    url: '/auth/logout',
    method: 'post'
    })
    }

    // 获取验证码
    export function getCaptcha() {
    return request({
    url: '/auth/captcha',
    method: 'get'
    })
    }

    // 刷新Token
    export function refreshToken(refreshToken) {
    return request({
    url: '/auth/refresh',
    method: 'post',
    params: { refreshToken }
    })
    }

    // 检查用户名是否存在
    export function checkUsername(username) {
    return request({
    url: '/auth/check/username',
    method: 'get',
    params: { username }
    })
    }

    // 检查邮箱是否存在
    export function checkEmail(email) {
    return request({
    url: '/auth/check/email',
    method: 'get',
    params: { email }
    })
    }

    // 检查手机号是否存在
    export function checkPhone(phone) {
    return request({
    url: '/auth/check/phone',
    method: 'get',
    params: { phone }
    })
    }

    6.8 工具函数

    // src/utils/auth.js
    const TokenKey = 'Admin-Token'

    export function getToken() {
    return localStorage.getItem(TokenKey)
    }

    export function setToken(token) {
    return localStorage.setItem(TokenKey, token)
    }

    export function removeToken() {
    return localStorage.removeItem(TokenKey)
    }

    // src/utils/validate.js
    // 验证用户名
    export function validateUsername(rule, value, callback) {
    const reg = /^[a-zA-Z0-9_]{4,20}$/
    if (value && !reg.test(value)) {
    callback(new Error('用户名必须是4-20位字母、数字或下划线'))
    } else {
    callback()
    }
    }

    // 验证密码
    export function validatePassword(rule, value, callback) {
    const reg = /^(?=.*[0-9])(?=.*[a-zA-Z])(?=.*[@#$%^&+=])(?=\\S+$).{8,20}$/
    if (value && !reg.test(value)) {
    callback(new Error('密码必须包含字母、数字和特殊字符,长度8-20位'))
    } else {
    callback()
    }
    }

    // 验证确认密码
    export function validateConfirmPassword(password) {
    return (rule, value, callback) => {
    if (value !== password) {
    callback(new Error('两次输入的密码不一致'))
    } else {
    callback()
    }
    }
    }

    // 验证邮箱
    export function validateEmail(rule, value, callback) {
    const reg = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$/
    if (value && !reg.test(value)) {
    callback(new Error('邮箱格式不正确'))
    } else {
    callback()
    }
    }

    // 验证手机号
    export function validatePhone(rule, value, callback) {
    const reg = /^1[3-9]\\d{9}$/
    if (value && !reg.test(value)) {
    callback(new Error('手机号格式不正确'))
    } else {
    callback()
    }
    }

    6.9 组合式函数

    // src/composables/useAuth.js
    import { ref } from 'vue'
    import { useRouter } from 'vue-router'
    import { ElMessage } from 'element-plus'
    import { useUserStore } from '@/stores/user'
    import { login, register, getCaptcha } from '@/api/auth'

    export function useAuth() {
    const router = useRouter()
    const userStore = useUserStore()

    const loading = ref(false)
    const captchaKey = ref('')
    const captchaImage = ref('')

    // 获取验证码
    const fetchCaptcha = async () => {
    try {
    const res = await getCaptcha()
    captchaKey.value = res.data.key
    captchaImage.value = res.data.image
    } catch (error) {
    console.error('获取验证码失败:', error)
    }
    }

    // 登录
    const handleLogin = async (loginForm) => {
    loading.value = true
    try {
    await userStore.loginAction(loginForm)
    ElMessage.success('登录成功')
    router.push('/')
    return true
    } catch (error) {
    // 刷新验证码
    await fetchCaptcha()
    return false
    } finally {
    loading.value = false
    }
    }

    // 注册
    const handleRegister = async (registerForm) => {
    loading.value = true
    try {
    await userStore.registerAction(registerForm)
    ElMessage.success('注册成功,请登录')
    router.push('/auth/login')
    return true
    } catch (error) {
    // 刷新验证码
    await fetchCaptcha()
    return false
    } finally {
    loading.value = false
    }
    }

    return {
    loading,
    captchaKey,
    captchaImage,
    fetchCaptcha,
    handleLogin,
    handleRegister
    }
    }

    // src/composables/useCaptcha.js
    import { ref } from 'vue'
    import { getCaptcha, verifyCaptcha } from '@/api/auth'

    export function useCaptcha() {
    const captchaKey = ref('')
    const captchaImage = ref('')
    const captchaCode = ref('')
    const loading = ref(false)

    // 刷新验证码
    const refreshCaptcha = async () => {
    loading.value = true
    try {
    const res = await getCaptcha()
    captchaKey.value = res.data.key
    captchaImage.value = res.data.image
    captchaCode.value = ''
    } catch (error) {
    console.error('刷新验证码失败:', error)
    } finally {
    loading.value = false
    }
    }

    // 验证验证码
    const validateCaptcha = async () => {
    if (!captchaCode.value) {
    return false
    }
    try {
    const res = await verifyCaptcha({
    key: captchaKey.value,
    code: captchaCode.value
    })
    return res.data
    } catch (error) {
    return false
    }
    }

    return {
    captchaKey,
    captchaImage,
    captchaCode,
    loading,
    refreshCaptcha,
    validateCaptcha
    }
    }

    6.10 登录页面

    <!– src/views/auth/Login.vue –>
    <template>
    <div class="login-container">
    <div class="login-card">
    <div class="login-header">
    <h2>{{ siteTitle }}</h2>
    <p>欢迎回来,请登录您的账号</p>
    </div>

    <el-form
    ref="loginFormRef"
    :model="loginForm"
    :rules="loginRules"
    label-position="top"
    @submit.prevent
    >
    <el-form-item label="账号" prop="account">
    <el-input
    v-model="loginForm.account"
    placeholder="请输入用户名/邮箱/手机号"
    :prefix-icon="User"
    size="large"
    clearable
    />
    </el-form-item>

    <el-form-item label="密码" prop="password">
    <el-input
    v-model="loginForm.password"
    type="password"
    placeholder="请输入密码"
    :prefix-icon="Lock"
    size="large"
    show-password
    @keyup.enter="handleLogin"
    />
    </el-form-item>

    <!– 验证码(登录失败3次后显示) –>
    <el-form-item v-if="showCaptcha" label="验证码" prop="captcha">
    <div class="captcha-container">
    <el-input
    v-model="loginForm.captcha"
    placeholder="请输入验证码"
    :prefix-icon="Key"
    size="large"
    />
    <div class="captcha-image" @click="refreshCaptcha">
    <img v-if="captchaImage" :src="captchaImage" alt="验证码">
    <span v-else>点击刷新</span>
    </div>
    </div>
    </el-form-item>

    <el-form-item>
    <div class="form-options">
    <el-checkbox v-model="loginForm.rememberMe">记住我</el-checkbox>
    <el-link type="primary" :underline="false">忘记密码?</el-link>
    </div>
    </el-form-item>

    <el-form-item>
    <el-button
    type="primary"
    size="large"
    :loading="loading"
    @click="handleLogin"
    >
    {{ loading ? '登录中…' : '登录' }}
    </el-button>
    </el-form-item>
    </el-form>

    <div class="login-footer">
    <span>还没有账号?</span>
    <el-link type="primary" @click="goToRegister">立即注册</el-link>
    </div>

    <!– 追悼模式开关 –>
    <div class="theme-switch">
    <el-switch
    v-model="mourningMode"
    active-text="追悼模式"
    inactive-text="正常模式"
    @change="toggleMourningMode"
    />
    </div>
    </div>
    </div>
    </template>

    <script setup>
    import { ref, reactive, computed, onMounted } from 'vue'
    import { useRouter } from 'vue-router'
    import { ElMessage } from 'element-plus'
    import { User, Lock, Key } from '@element-plus/icons-vue'
    import { useUserStore } from '@/stores/user'
    import { useThemeStore } from '@/stores/theme'
    import { useAuth } from '@/composables/useAuth'
    import { validatePassword } from '@/utils/validate'

    const router = useRouter()
    const userStore = useUserStore()
    const themeStore = useThemeStore()
    const { loading, captchaKey, captchaImage, fetchCaptcha, handleLogin: authLogin } = useAuth()

    // 站点标题
    const siteTitle = import.meta.env.VITE_APP_TITLE

    // 追悼模式
    const mourningMode = computed({
    get: () => themeStore.mourningMode,
    set: (val) => {
    if (val) {
    themeStore.enableMourningMode()
    } else {
    themeStore.disableMourningMode()
    }
    }
    })

    // 是否显示验证码
    const showCaptcha = ref(false)
    const loginFailCount = ref(0)

    // 登录表单
    const loginFormRef = ref()
    const loginForm = reactive({
    account: '',
    password: '',
    captcha: '',
    captchaKey: '',
    rememberMe: true
    })

    // 登录规则
    const loginRules = {
    account: [
    { required: true, message: '请输入账号', trigger: 'blur' },
    { min: 4, max: 50, message: '长度在 4 到 50 个字符', trigger: 'blur' }
    ],
    password: [
    { required: true, message: '请输入密码', trigger: 'blur' },
    { validator: validatePassword, trigger: 'blur' }
    ],
    captcha: [
    { required: true, message: '请输入验证码', trigger: 'blur' },
    { min: 4, max: 4, message: '验证码长度为4位', trigger: 'blur' }
    ]
    }

    // 刷新验证码
    const refreshCaptcha = async () => {
    await fetchCaptcha()
    loginForm.captchaKey = captchaKey.value
    }

    // 登录处理
    const handleLogin = async () => {
    await loginFormRef.value.validate()

    // 如果显示验证码,验证是否已填写
    if (showCaptcha.value && !loginForm.captcha) {
    ElMessage.warning('请输入验证码')
    return
    }

    const success = await authLogin(loginForm)
    if (!success) {
    loginFailCount.value++
    if (loginFailCount.value >= 3) {
    showCaptcha.value = true
    await refreshCaptcha()
    }
    }
    }

    // 跳转到注册
    const goToRegister = () => {
    router.push('/auth/register')
    }

    // 切换追悼模式
    const toggleMourningMode = (val) => {
    if (val) {
    themeStore.enableMourningMode()
    } else {
    themeStore.disableMourningMode()
    }
    }

    // 页面加载时检查登录状态
    onMounted(() => {
    if (userStore.isLoggedIn) {
    router.push('/')
    }
    // 初始化验证码(如果需要)
    // refreshCaptcha()
    })
    </script>

    <style scoped lang="scss">
    .login-container {
    min-height: 100vh;
    display: flex;
    align-items: center;
    justify-content: center;
    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
    padding: 20px;
    }

    .login-card {
    width: 100%;
    max-width: 450px;
    background: rgba(255, 255, 255, 0.95);
    border-radius: 12px;
    padding: 40px;
    box-shadow: 0 10px 40px rgba(0, 0, 0, 0.1);
    position: relative;

    .login-header {
    text-align: center;
    margin-bottom: 30px;

    h2 {
    font-size: 28px;
    color: #333;
    margin-bottom: 8px;
    }

    p {
    color: #666;
    font-size: 14px;
    }
    }

    .captcha-container {
    display: flex;
    gap: 10px;

    .el-input {
    flex: 1;
    }

    .captcha-image {
    width: 120px;
    height: 40px;
    background: #f0f2f5;
    border-radius: 4px;
    cursor: pointer;
    display: flex;
    align-items: center;
    justify-content: center;
    overflow: hidden;

    img {
    width: 100%;
    height: 100%;
    object-fit: cover;
    }

    span {
    color: #999;
    font-size: 12px;
    }
    }
    }

    .form-options {
    display: flex;
    align-items: center;
    justify-content: space-between;
    width: 100%;
    }

    .el-button {
    width: 100%;
    }

    .login-footer {
    text-align: center;
    margin-top: 20px;
    font-size: 14px;
    color: #666;

    .el-link {
    font-size: 14px;
    }
    }

    .theme-switch {
    position: absolute;
    bottom: 20px;
    right: 20px;
    font-size: 12px;
    }
    }

    // 追悼模式样式
    :global(.mourning-mode) {
    .login-card {
    background: rgba(51, 51, 51, 0.95);

    .login-header {
    h2, p {
    color: #999;
    }
    }

    .el-form-item__label {
    color: #999;
    }
    }
    }
    </style>

    6.11 注册页面

    <!– src/views/auth/Register.vue –>
    <template>
    <div class="register-container">
    <div class="register-card">
    <div class="register-header">
    <h2>{{ siteTitle }}</h2>
    <p>注册新账号,开启技术之旅</p>
    </div>

    <el-form
    ref="registerFormRef"
    :model="registerForm"
    :rules="registerRules"
    label-position="top"
    @submit.prevent
    >
    <el-form-item label="用户名" prop="username">
    <el-input
    v-model="registerForm.username"
    placeholder="4-20位字母、数字或下划线"
    :prefix-icon="User"
    size="large"
    clearable
    @blur="checkUsername"
    />
    </el-form-item>

    <el-form-item label="邮箱" prop="email">
    <el-input
    v-model="registerForm.email"
    placeholder="请输入邮箱"
    :prefix-icon="Message"
    size="large"
    clearable
    @blur="checkEmail"
    />
    </el-form-item>

    <el-form-item label="手机号" prop="phone">
    <el-input
    v-model="registerForm.phone"
    placeholder="请输入手机号(可选)"
    :prefix-icon="Iphone"
    size="large"
    clearable
    @blur="checkPhone"
    />
    </el-form-item>

    <el-form-item label="密码" prop="password">
    <el-input
    v-model="registerForm.password"
    type="password"
    placeholder="8-20位,包含字母、数字和特殊字符"
    :prefix-icon="Lock"
    size="large"
    show-password
    />
    </el-form-item>

    <el-form-item label="确认密码" prop="confirmPassword">
    <el-input
    v-model="registerForm.confirmPassword"
    type="password"
    placeholder="请再次输入密码"
    :prefix-icon="Lock"
    size="large"
    show-password
    />
    </el-form-item>

    <el-form-item label="验证码" prop="captcha" required>
    <div class="captcha-container">
    <el-input
    v-model="registerForm.captcha"
    placeholder="请输入验证码"
    :prefix-icon="Key"
    size="large"
    />
    <div class="captcha-image" @click="refreshCaptcha">
    <img v-if="captchaImage" :src="captchaImage" alt="验证码">
    <span v-else>点击刷新</span>
    </div>
    </div>
    </el-form-item>

    <el-form-item prop="agreement">
    <el-checkbox v-model="registerForm.agreement">
    我已阅读并同意
    <el-link type="primary" :underline="false">《用户协议》</el-link>

    <el-link type="primary" :underline="false">《隐私政策》</el-link>
    </el-checkbox>
    </el-form-item>

    <el-form-item>
    <el-button
    type="primary"
    size="large"
    :loading="loading"
    @click="handleRegister"
    >
    {{ loading ? '注册中…' : '注册' }}
    </el-button>
    </el-form-item>
    </el-form>

    <div class="register-footer">
    <span>已有账号?</span>
    <el-link type="primary" @click="goToLogin">立即登录</el-link>
    </div>
    </div>
    </div>
    </template>

    <script setup>
    import { ref, reactive, onMounted } from 'vue'
    import { useRouter } from 'vue-router'
    import { ElMessage } from 'element-plus'
    import { User, Lock, Message, Iphone, Key } from '@element-plus/icons-vue'
    import { useUserStore } from '@/stores/user'
    import { useAuth } from '@/composables/useAuth'
    import {
    validateUsername,
    validatePassword,
    validateConfirmPassword,
    validateEmail,
    validatePhone
    } from '@/utils/validate'
    import { checkUsername, checkEmail, checkPhone } from '@/api/auth'

    const router = useRouter()
    const userStore = useUserStore()
    const { loading, captchaKey, captchaImage, fetchCaptcha, handleRegister: authRegister } = useAuth()

    // 站点标题
    const siteTitle = import.meta.env.VITE_APP_TITLE

    // 注册表单
    const registerFormRef = ref()
    const registerForm = reactive({
    username: '',
    email: '',
    phone: '',
    password: '',
    confirmPassword: '',
    captcha: '',
    captchaKey: '',
    agreement: false
    })

    // 注册规则
    const registerRules = {
    username: [
    { required: true, message: '请输入用户名', trigger: 'blur' },
    { validator: validateUsername, trigger: 'blur' },
    { validator: async (rule, value, callback) => {
    if (value) {
    try {
    const res = await checkUsername(value)
    if (res.data) {
    callback(new Error('用户名已存在'))
    } else {
    callback()
    }
    } catch (error) {
    callback()
    }
    } else {
    callback()
    }
    }, trigger: 'blur' }
    ],
    email: [
    { required: true, message: '请输入邮箱', trigger: 'blur' },
    { validator: validateEmail, trigger: 'blur' },
    { validator: async (rule, value, callback) => {
    if (value) {
    try {
    const res = await checkEmail(value)
    if (res.data) {
    callback(new Error('邮箱已被注册'))
    } else {
    callback()
    }
    } catch (error) {
    callback()
    }
    } else {
    callback()
    }
    }, trigger: 'blur' }
    ],
    phone: [
    { validator: validatePhone, trigger: 'blur' },
    { validator: async (rule, value, callback) => {
    if (value) {
    try {
    const res = await checkPhone(value)
    if (res.data) {
    callback(new Error('手机号已被注册'))
    } else {
    callback()
    }
    } catch (error) {
    callback()
    }
    } else {
    callback()
    }
    }, trigger: 'blur' }
    ],
    password: [
    { required: true, message: '请输入密码', trigger: 'blur' },
    { validator: validatePassword, trigger: 'blur' }
    ],
    confirmPassword: [
    { required: true, message: '请再次输入密码', trigger: 'blur' },
    { validator: validateConfirmPassword(registerForm.password), trigger: 'blur' }
    ],
    captcha: [
    { required: true, message: '请输入验证码', trigger: 'blur' },
    { min: 4, max: 4, message: '验证码长度为4位', trigger: 'blur' }
    ],
    agreement: [
    { required: true, message: '请同意用户协议', trigger: 'change' }
    ]
    }

    // 刷新验证码
    const refreshCaptcha = async () => {
    await fetchCaptcha()
    registerForm.captchaKey = captchaKey.value
    }

    // 检查用户名
    const checkUsername = async () => {
    if (registerForm.username) {
    try {
    const res = await checkUsername(registerForm.username)
    if (res.data) {
    ElMessage.warning('用户名已存在')
    }
    } catch (error) {
    console.error('检查用户名失败:', error)
    }
    }
    }

    // 检查邮箱
    const checkEmail = async () => {
    if (registerForm.email) {
    try {
    const res = await checkEmail(registerForm.email)
    if (res.data) {
    ElMessage.warning('邮箱已被注册')
    }
    } catch (error) {
    console.error('检查邮箱失败:', error)
    }
    }
    }

    // 检查手机号
    const checkPhone = async () => {
    if (registerForm.phone) {
    try {
    const res = await checkPhone(registerForm.phone)
    if (res.data) {
    ElMessage.warning('手机号已被注册')
    }
    } catch (error) {
    console.error('检查手机号失败:', error)
    }
    }
    }

    // 注册处理
    const handleRegister = async () => {
    await registerFormRef.value.validate()

    const success = await authRegister(registerForm)
    if (success) {
    // 注册成功,跳转到登录页
    router.push('/auth/login')
    }
    }

    // 跳转到登录
    const goToLogin = () => {
    router.push('/auth/login')
    }

    // 页面加载时获取验证码
    onMounted(() => {
    if (userStore.isLoggedIn) {
    router.push('/')
    }
    refreshCaptcha()
    })
    </script>

    <style scoped lang="scss">
    .register-container {
    min-height: 100vh;
    display: flex;
    align-items: center;
    justify-content: center;
    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
    padding: 20px;
    }

    .register-card {
    width: 100%;
    max-width: 500px;
    background: rgba(255, 255, 255, 0.95);
    border-radius: 12px;
    padding: 40px;
    box-shadow: 0 10px 40px rgba(0, 0, 0, 0.1);

    .register-header {
    text-align: center;
    margin-bottom: 30px;

    h2 {
    font-size: 28px;
    color: #333;
    margin-bottom: 8px;
    }

    p {
    color: #666;
    font-size: 14px;
    }
    }

    .captcha-container {
    display: flex;
    gap: 10px;

    .el-input {
    flex: 1;
    }

    .captcha-image {
    width: 120px;
    height: 40px;
    background: #f0f2f5;
    border-radius: 4px;
    cursor: pointer;
    display: flex;
    align-items: center;
    justify-content: center;
    overflow: hidden;

    img {
    width: 100%;
    height: 100%;
    object-fit: cover;
    }

    span {
    color: #999;
    font-size: 12px;
    }
    }
    }

    .el-button {
    width: 100%;
    }

    .register-footer {
    text-align: center;
    margin-top: 20px;
    font-size: 14px;
    color: #666;

    .el-link {
    font-size: 14px;
    }
    }
    }

    :deep(.el-form-item__label) {
    font-weight: 500;
    }

    :deep(.el-checkbox__label) {
    font-size: 14px;
    color: #666;
    }
    </style>


    七、前后端联调

    7.1 启动后端服务

    # 进入后端项目目录
    cd ~/projects/blog-backend

    # 使用Maven打包
    mvn clean package

    # 启动服务
    java -jar target/blog-backend-1.0.0.jar

    # 或者使用IDEA启动
    # 运行 BlogApplication.java

    7.2 启动前端服务

    # 进入前端项目目录
    cd ~/projects/blog-frontend

    # 安装依赖
    npm install

    # 启动开发服务器
    npm run dev

    # 访问 http://localhost:5173

    7.3 联调测试流程

    7.3.1 测试注册流程
  • 访问注册页面:http://localhost:5173/auth/register
  • 验证验证码是否正确显示
  • 填写注册信息
  • 提交注册表单
  • 检查控制台请求和响应
  • 验证数据库是否有新用户
  • 7.3.2 测试登录流程
  • 访问登录页面:http://localhost:5173/auth/login
  • 输入刚注册的账号密码
  • 提交登录表单
  • 检查是否返回Token
  • 验证是否跳转到首页
  • 检查localStorage中是否有Token
  • 7.3.3 测试验证码功能
  • 连续输入错误密码3次
  • 验证是否出现验证码输入框
  • 测试验证码刷新功能
  • 验证验证码忽略大小写功能
  • 7.3.4 测试追悼模式
  • 点击主题开关
  • 检查页面是否变为灰度
  • 刷新页面,检查状态是否保持
  • 切换回正常模式
  • 7.4 常见问题排查

    7.4.1 跨域问题

    // 后端添加跨域配置
    @Configuration
    public class CorsConfig {

    @Bean
    public WebMvcConfigurer corsConfigurer() {
    return new WebMvcConfigurer() {
    @Override
    public void addCorsMappings(CorsRegistry registry) {
    registry.addMapping("/**")
    .allowedOrigins("http://localhost:5173")
    .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
    .allowedHeaders("*")
    .allowCredentials(true)
    .maxAge(3600);
    }
    };
    }
    }

    7.4.2 Token失效问题

    // 前端添加Token自动刷新
    let isRefreshing = false
    let requests = []

    request.interceptors.response.use(
    response => response,
    async error => {
    const { config, response } = error
    const originalRequest = config

    if (response?.status === 401 && !originalRequest._retry) {
    if (isRefreshing) {
    return new Promise(resolve => {
    requests.push(token => {
    originalRequest.headers['Authorization'] = 'Bearer ' + token
    resolve(request(originalRequest))
    })
    })
    }

    originalRequest._retry = true
    isRefreshing = true

    try {
    const refreshToken = localStorage.getItem('Refresh-Token')
    const res = await refreshToken(refreshToken)
    const newToken = res.data

    setToken(newToken)
    originalRequest.headers['Authorization'] = 'Bearer ' + newToken

    requests.forEach(cb => cb(newToken))
    requests = []

    return request(originalRequest)
    } catch (err) {
    // 刷新失败,跳转到登录页
    const userStore = useUserStore()
    await userStore.logoutAction()
    router.push('/auth/login')
    return Promise.reject(err)
    } finally {
    isRefreshing = false
    }
    }

    return Promise.reject(error)
    }
    )

    7.4.3 验证码忽略大小写验证

    // 前端验证码输入自动转为大写
    const captcha = ref('')

    watch(captcha, (newVal) => {
    if (newVal) {
    captcha.value = newVal.toUpperCase()
    }
    })

    7.5 部署准备

    7.5.1 后端打包

    # 打包成jar
    mvn clean package -Pprod

    # 查看打包结果
    ls -lh target/*.jar

    7.5.2 前端打包

    # 构建生产版本
    npm run build

    # 查看构建结果
    ls -lh dist/

    7.5.3 部署脚本

    #!/bin/bash
    # deploy.sh

    # 后端部署
    scp target/blog-backend-1.0.0.jar user@your-server:/app/blog/
    ssh user@your-server "cd /app/blog && ./restart.sh"

    # 前端部署
    scp -r dist/* user@your-server:/var/www/blog/


    文档版本记录

    版本日期修改内容作者
    v1.0 2024-01-01 初始版本,完成登录注册模块 Your Name
    v1.1 2024-01-02 添加追悼模式功能 Your Name
    v1.2 2024-01-03 完善验证码忽略大小写功能 Your Name

    赞(0)
    未经允许不得转载:171主机测评 » 博客基础模块开发(登录注册)
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址