网站搭建实操(二)后台管理实现
- 模块架构
- 后台代码架构
- 最小可用架构
- 模块依赖关系
- forum-dao(数据访问模块)
- forum-service (业务服务模块)
- forum-auth-server
- forum-user-service (用户服务)
- 前端简单页面
- 部署jar包
- 源码地址
模块架构
text
forum-backend/
├── forum-common/ # 公共工具模块
├── forum-core/ # 核心配置模块
├── forum-framework/ # 框架封装模块
├── forum-dao/ # 数据访问模块
├── forum-api/ # API接口模块
├── forum-service/ # 业务服务模块
├── forum-auth-server/ # 认证服务
├── forum-gateway/ # 网关服务
├── forum-user-service/ # 用户服务
├── forum-post-service/ # 帖子服务
├── forum-comment-service/ # 评论服务
├── forum-message-service/ # 消息服务
├── forum-search-service/ # 搜索服务
└── forum-admin-service/ # 管理服务
后台代码架构
text
forum-backend/
├── forum-common/ # 公共模块
│ ├── src/main/java/com/forum/common/
│ │ ├── base/
│ │ │ ├── BaseEntity.java # 基础实体类
│ │ │ ├── BaseController.java # 基础控制器
│ │ │ ├── BaseService.java # 基础服务接口
│ │ │ └── BaseServiceImpl.java # 基础服务实现
│ │ ├── constant/
│ │ │ ├── RedisKeyConstants.java
│ │ │ ├── SystemConstants.java
│ │ │ └── ApiConstants.java
│ │ ├── enums/
│ │ │ ├── ResultCodeEnum.java
│ │ │ ├── StatusEnum.java
│ │ │ └── DeleteEnum.java
│ │ ├── exception/
│ │ │ ├── BusinessException.java
│ │ │ ├── GlobalExceptionHandler.java
│ │ │ └── ServiceException.java
│ │ ├── utils/
│ │ │ ├── JwtUtils.java
│ │ │ ├── RedisUtils.java
│ │ │ ├── SnowflakeIdUtils.java
│ │ │ └── ValidateCodeUtils.java
│ │ └── annotation/
│ │ ├── RequiresPermission.java
│ │ ├── LogAnnotation.java
│ │ └── RateLimiter.java
│ └── pom.xml
│
├── forum-core/ # 核心模块
│ ├── src/main/java/com/forum/core/
│ │ ├── config/
│ │ │ ├── MybatisPlusConfig.java
│ │ │ ├── RedisConfig.java
│ │ │ ├── WebMvcConfig.java
│ │ │ ├── ThreadPoolConfig.java
│ │ │ └── SwaggerConfig.java
│ │ ├── interceptor/
│ │ │ ├── AuthenticationInterceptor.java
│ │ │ ├── RateLimitInterceptor.java
│ │ │ └── DataScopeInterceptor.java
│ │ ├── filter/
│ │ │ ├── RequestLogFilter.java
│ │ │ ├── XssFilter.java
│ │ │ └── CorsFilter.java
│ │ └── aop/
│ │ ├── LogAspect.java
│ │ └── PermissionAspect.java
│ └── pom.xml
│
├── forum-framework/ # 框架模块
│ ├── src/main/java/com/forum/framework/
│ │ ├── mybatis/
│ │ │ ├── MybatisPlusConfig.java
│ │ │ ├── MetaObjectHandler.java
│ │ │ ├── MybatisPlusSqlInjector.java
│ │ │ └── CustomSqlInjector.java
│ │ ├── redis/
│ │ │ ├── RedisConfig.java
│ │ │ ├── RedisTemplateConfig.java
│ │ │ └── RedisLock.java
│ │ ├── security/
│ │ │ ├── JwtAuthenticationFilter.java
│ │ │ ├── SecurityConfig.java
│ │ │ └── UserDetailsServiceImpl.java
│ │ └── task/
│ │ ├── ThreadPoolConfig.java
│ │ └── AsyncTask.java
│ └── pom.xml
│
├── forum-api/ # API接口模块
│ ├── src/main/java/com/forum/api/
│ │ ├── controller/
│ │ │ ├── auth/
│ │ │ │ └── AuthController.java
│ │ │ ├── user/
│ │ │ │ ├── UserController.java
│ │ │ │ └── UserProfileController.java
│ │ │ ├── forum/
│ │ │ │ ├── PostController.java
│ │ │ │ ├── CommentController.java
│ │ │ │ └── CategoryController.java
│ │ │ └── admin/
│ │ │ ├── AdminUserController.java
│ │ │ └── SystemConfigController.java
│ │ ├── vo/
│ │ │ ├── request/
│ │ │ │ ├── LoginRequest.java
│ │ │ │ ├── PostRequest.java
│ │ │ │ └── PageRequest.java
│ │ │ └── response/
│ │ │ ├── LoginResponse.java
│ │ │ ├── PostResponse.java
│ │ │ └── PageResponse.java
│ │ ├── dto/
│ │ │ ├── UserDTO.java
│ │ │ ├── PostDTO.java
│ │ │ └── CommentDTO.java
│ │ └── feign/
│ │ ├── UserFeignClient.java
│ │ └── MessageFeignClient.java
│ └── pom.xml
│
├── forum-dao/ # 数据访问模块
│ ├── src/main/java/com/forum/dao/
│ │ ├── entity/
│ │ │ ├── UserEntity.java
│ │ │ ├── PostEntity.java
│ │ │ ├── CommentEntity.java
│ │ │ └── CategoryEntity.java
│ │ ├── mapper/
│ │ │ ├── UserMapper.java
│ │ │ ├── PostMapper.java
│ │ │ ├── CommentMapper.java
│ │ │ └── CategoryMapper.java
│ │ └── repository/
│ │ ├── UserRepository.java
│ │ ├── PostRepository.java
│ │ └── impl/
│ │ ├── UserRepositoryImpl.java
│ │ └── PostRepositoryImpl.java
│ └── pom.xml
│
├── forum-service/ # 业务服务模块
│ ├── src/main/java/com/forum/service/
│ │ ├── auth/
│ │ │ ├── AuthService.java
│ │ │ └── impl/AuthServiceImpl.java
│ │ ├── user/
│ │ │ ├── UserService.java
│ │ │ └── impl/UserServiceImpl.java
│ │ ├── post/
│ │ │ ├── PostService.java
│ │ │ ├── impl/PostServiceImpl.java
│ │ │ └── strategy/
│ │ │ ├── PostTypeStrategy.java
│ │ │ └── NormalPostStrategy.java
│ │ └── admin/
│ │ ├── AdminService.java
│ │ └── impl/AdminServiceImpl.java
│ └── pom.xml
│
├── forum-auth-server/ # 认证服务
│ ├── src/main/java/com/forum/auth/
│ │ ├── OAuth2ServerConfig.java
│ │ ├── AuthorizationServerConfig.java
│ │ └── ResourceServerConfig.java
│ └── pom.xml
│
├── forum-gateway/ # 网关服务
│ ├── src/main/java/com/forum/gateway/
│ │ ├── GatewayConfig.java
│ │ ├── AuthFilter.java
│ │ └── GlobalExceptionHandler.java
│ └── pom.xml
│
├── sql/ # SQL脚本
│ ├── init.sql
│ └── data.sql
│
├── docker/ # Docker配置
│ ├── Dockerfile
│ └── docker-compose.yml
│
└── pom.xml # 父POM
创建父项目
pom文件
java
<?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>
<groupId>org.example</groupId>
<artifactId>forum-backend</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>pom</packaging>
<description>微服务论坛系统 - Java 8版本</description>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.5.RELEASE</version>
<relativePath/>
</parent>
<properties>
<!-- Java版本配置 -->
<java.version>1.8</java.version>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<!-- Spring Cloud 2021.x - 与Spring Boot 2.7.x完美配合 -->
<spring-cloud.version>2021.0.9</spring-cloud.version>
<!-- Spring Cloud Alibaba - 服务注册、配置、限流 -->
<spring-cloud-alibaba.version>2021.0.6.0</spring-cloud-alibaba.version>
<!-- MyBatis Plus 3.5.x - 增强版ORM,Lambda查询 -->
<mybatis-plus.version>3.5.3</mybatis-plus.version>
<!-- Redisson 3.17.x - 分布式锁、Redis集群 -->
<redisson.version>3.17.7</redisson.version>
<!-- JWT 0.11.x - 无状态认证 -->
<jjwt.version>0.11.5</jjwt.version>
<!-- Hutool 5.8.x - Java工具库,减少重复代码 -->
<hutool.version>5.8.11</hutool.version>
<!-- MapStruct 1.5.x - 对象转换,性能优于BeanUtils -->
<mapstruct.version>1.5.5.Final</mapstruct.version>
<lombok.version>1.18.30</lombok.version>
<!-- MySQL驱动 -->
<mysql.version>8.0.33</mysql.version>
<!-- API文档 -->
<knife4j.version>3.0.3</knife4j.version>
<!-- 测试框架 -->
<junit.version>5.9.3</junit.version>
</properties>
<dependencyManagement>
<dependencies>
<!-- Spring Cloud依赖管理 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!-- Spring Cloud Alibaba依赖管理 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>${spring-cloud-alibaba.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!-- MyBatis Plus -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>${mybatis-plus.version}</version>
</dependency>
<!-- Redisson - 分布式锁实现 -->
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
<version>${redisson.version}</version>
</dependency>
<!-- JWT -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>${jjwt.version}</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>${jjwt.version}</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>${jjwt.version}</version>
<scope>runtime</scope>
</dependency>
<!-- Hutool - 避免重复造轮子 -->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>${hutool.version}</version>
</dependency>
<!-- MapStruct - 高性能对象转换 -->
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
<version>${mapstruct.version}</version>
</dependency>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>${mapstruct.version}</version>
</dependency>
<!-- Knife4j - Swagger增强,更好用的API文档 -->
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>knife4j-spring-boot-starter</artifactId>
<version>${knife4j.version}</version>
</dependency>
<!-- MySQL驱动 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>${mysql.version}</version>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.11.0</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
<encoding>UTF-8</encoding>
<annotationProcessorPaths>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
</path>
<path>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>${mapstruct.version}</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
<!-- Spring Boot打包插件 -->
<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>
forum-common 公共模块
pom文件
java
<?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">
<parent>
<artifactId>forum-backend</artifactId>
<groupId>org.example</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>forum-common</artifactId>
<description>公共模块:基础类、工具类、常量、异常处理</description>
<dependencies>
<!-- Spring Boot Web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- MyBatis Plus注解,用于实体类 -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
</dependency>
<!-- Lombok - 减少样板代码 -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!-- Hutool - 丰富的工具类库 -->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
</dependency>
<!-- JWT -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<scope>runtime</scope>
</dependency>
<!-- Jackson JSON处理 -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-annotations</artifactId>
</dependency>
<!-- Bean Validation -->
<dependency>
<groupId>javax.validation</groupId>
<artifactId>validation-api</artifactId>
</dependency>
<!-- Servlet API -->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<scope>provided</scope>
</dependency>
<!-- Spring Security Core - 用于获取当前用户 -->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-core</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
</plugins>
</build>
</project>
实体基类
我要先做出效果,所以按照最小可用产品的思路,先实现最核心的功能:用户注册、登录,以及一个简单的首页展示。这样可以先在服务器上部署并看到效果。
最小可用架构

模块依赖关系
text
forum-common (已完成)
↑
forum-dao (依赖common)
↑
forum-service (依赖dao)
↑
forum-auth-server (依赖service)
↑
forum-user-service (依赖service)
forum-dao(数据访问模块)
pom文件
java
<?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">
<parent>
<artifactId>forum-backend</artifactId>
<groupId>org.example</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>forum-dao</artifactId>
<description>数据访问模块 - 实体类、Mapper接口</description>
<dependencies>
<!-- Common模块 -->
<dependency>
<groupId>org.example</groupId>
<artifactId>forum-common</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<!-- MyBatis Plus -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
</dependency>
<!-- MySQL -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<!-- Lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
</plugins>
</build>
</project>
实体类
user实体类
java
package com.forum.dao.entity;
import com.baomidou.mybatisplus.annotation.TableName;
import com.forum.common.base.BaseEntity;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* 用户实体类
*/
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("uc_user")
public class UserEntity extends BaseEntity {
/**
* 用户名
*/
private String username;
/**
* 密码(加密存储)
*/
private String password;
/**
* 邮箱
*/
private String email;
/**
* 手机号
*/
private String phone;
/**
* 头像URL
*/
private String avatar;
/**
* 昵称
*/
private String nickname;
/**
* 性别 0:未知 1:男 2:女
*/
private Integer gender;
/**
* 状态 0:禁用 1:启用 2:锁定
*/
private Integer status;
/**
* 最后登录时间
*/
private java.time.LocalDateTime lastLoginTime;
}
mapper
java
package com.forum.dao.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.forum.dao.entity.UserEntity;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
@Mapper
public interface UserMapper extends BaseMapper<UserEntity> {
/**
* 根据用户名查询用户
*/
@Select("SELECT * FROM uc_user WHERE username = #{username} AND is_deleted = 0")
UserEntity selectByUsername(@Param("username") String username);
/**
* 根据邮箱查询用户
*/
@Select("SELECT * FROM uc_user WHERE email = #{email} AND is_deleted = 0")
UserEntity selectByEmail(@Param("email") String email);
/**
* 根据手机号查询用户
*/
@Select("SELECT * FROM uc_user WHERE phone = #{phone} AND is_deleted = 0")
UserEntity selectByPhone(@Param("phone") String phone);
}
forum-service (业务服务模块)
业务实现类
java
package com.forum.service.user.service.impl;
import cn.hutool.core.bean.BeanUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.forum.common.base.BaseServiceImpl;
import com.forum.common.exception.BusinessException;
import com.forum.common.utils.JwtUtils;
import com.forum.dao.entity.UserEntity;
import com.forum.dao.mapper.UserMapper;
import com.forum.service.user.dto.UserLoginDTO;
import com.forum.service.user.dto.UserRegisterDTO;
import com.forum.service.user.service.UserService;
import com.forum.service.user.vo.UserVO;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.util.DigestUtils;
import java.time.LocalDateTime;
/**
* 用户服务实现类
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class UserServiceImpl extends BaseServiceImpl<UserMapper, UserEntity>
implements UserService {
private final JwtUtils jwtUtils;
@Override
public UserVO register(UserRegisterDTO dto) {
log.info("用户注册开始: username={}", dto.getUsername());
// 1. 校验两次密码是否一致
if (!dto.getPassword().equals(dto.getConfirmPassword())) {
throw new BusinessException("两次输入的密码不一致");
}
// 2. 检查用户名是否已存在
UserEntity existUser = findByUsername(dto.getUsername());
if (existUser != null) {
throw new BusinessException("用户名已存在");
}
// 3. 检查邮箱是否已存在
if (dto.getEmail() != null && !dto.getEmail().isEmpty()) {
UserEntity existEmail = findByEmail(dto.getEmail());
if (existEmail != null) {
throw new BusinessException("邮箱已被注册");
}
}
// 4. 创建用户
UserEntity user = new UserEntity();
BeanUtil.copyProperties(dto, user);
// 密码加密(MD5 + 盐值,生产环境建议使用BCrypt)
user.setPassword(DigestUtils.md5DigestAsHex(dto.getPassword().getBytes()));
// 设置默认昵称
if (user.getNickname() == null || user.getNickname().isEmpty()) {
user.setNickname(dto.getUsername());
}
// 设置默认状态:启用
user.setStatus(1);
// 5. 保存用户
this.save(user);
log.info("用户注册成功: id={}, username={}", user.getId(), user.getUsername());
// 6. 返回用户信息
return convertToVO(user);
}
@Override
public UserVO login(UserLoginDTO dto) {
log.info("用户登录: username={}", dto.getUsername());
// 1. 查询用户
UserEntity user = findByUsername(dto.getUsername());
if (user == null) {
throw new BusinessException("用户名或密码错误");
}
// 2. 校验密码
String encryptedPassword = DigestUtils.md5DigestAsHex(dto.getPassword().getBytes());
if (!encryptedPassword.equals(user.getPassword())) {
throw new BusinessException("用户名或密码错误");
}
// 3. 检查用户状态
if (user.getStatus() == 0) {
throw new BusinessException("账号已被禁用");
}
if (user.getStatus() == 2) {
throw new BusinessException("账号已被锁定");
}
// 4. 更新最后登录时间
user.setLastLoginTime(LocalDateTime.now());
this.updateById(user);
// 5. 生成JWT Token
String token = jwtUtils.generateToken(user.getId(), user.getUsername());
log.info("用户登录成功: id={}, username={}", user.getId(), user.getUsername());
// 6. 返回用户信息
UserVO userVO = convertToVO(user);
userVO.setToken(token);
return userVO;
}
@Override
public UserEntity findByUsername(String username) {
LambdaQueryWrapper<UserEntity> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(UserEntity::getUsername, username);
return this.getOne(wrapper);
}
@Override
public UserEntity findByEmail(String email) {
LambdaQueryWrapper<UserEntity> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(UserEntity::getEmail, email);
return this.getOne(wrapper);
}
@Override
public UserVO getUserById(Long userId) {
UserEntity user = getById(userId);
if (user == null) {
throw new BusinessException("用户不存在");
}
return convertToVO(user);
}
/**
* 转换为视图对象
*/
private UserVO convertToVO(UserEntity user) {
UserVO vo = new UserVO();
BeanUtil.copyProperties(user, vo);
return vo;
}
}
forum-auth-server
AuthController
java
package com.forum.auth.controller;
import com.forum.common.result.Result;
import com.forum.service.user.dto.UserLoginDTO;
import com.forum.service.user.dto.UserRegisterDTO;
import com.forum.service.user.service.UserService;
import com.forum.service.user.vo.UserVO;
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.validation.Valid;
import static com.forum.common.result.Result.error;
import static com.forum.common.result.Result.success;
import static com.forum.common.utils.SecurityUtils.getCurrentUserId;
/**
* 认证控制器
* 提供登录、注册、登出等功能
*/
@Slf4j
@Api(tags = "认证管理")
@RestController
@RequestMapping("/api/auth")
@RequiredArgsConstructor
public class AuthController {
private final UserService userService;
@ApiOperation("用户注册")
@PostMapping("/register")
public Result<UserVO> register(@Valid @RequestBody UserRegisterDTO dto) {
log.info("收到注册请求: username={}", dto.getUsername());
UserVO userVO = userService.register(dto);
return success(userVO);
}
@ApiOperation("用户登录")
@PostMapping("/login")
public Result<UserVO> login(@Valid @RequestBody UserLoginDTO dto) {
log.info("收到登录请求: username={}", dto.getUsername());
UserVO userVO = userService.login(dto);
return success(userVO);
}
@ApiOperation("获取当前用户信息")
@GetMapping("/current")
public Result<UserVO> getCurrentUser() {
Long userId = getCurrentUserId();
if (userId == null) {
return error("用户未登录");
}
UserVO userVO = userService.getUserById(userId);
return success(userVO);
}
@ApiOperation("健康检查")
@GetMapping("/health")
public Result<String> health() {
return success("服务正常运行");
}
}
forum-user-service (用户服务)
userController
java
package com.forum.user.controller;
import com.forum.common.base.BaseController;
import com.forum.common.result.Result;
import com.forum.service.user.service.UserService;
import com.forum.service.user.vo.UserVO;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* 用户控制器
*/
@Slf4j
@Api(tags = "用户管理")
@RestController
@RequestMapping("/api/users")
@RequiredArgsConstructor
public class UserController extends BaseController {
private final UserService userService;
@ApiOperation("获取当前用户信息")
@GetMapping("/current")
public Result<UserVO> getCurrentUser() {
Long userId = getCurrentUserId();
if (userId == null) {
return error("用户未登录");
}
UserVO userVO = userService.getUserById(userId);
return success(userVO);
}
@ApiOperation("根据ID获取用户信息")
@GetMapping("/{userId}")
public Result<UserVO> getUserById(@PathVariable Long userId) {
UserVO userVO = userService.getUserById(userId);
return success(userVO);
}
@ApiOperation("健康检查")
@GetMapping("/health")
public Result<String> health() {
return success("用户服务正常运行");
}
}
前端简单页面
先用来看效果,做一个html
前端HTML文件 - 放在Nginx静态目录
javascript
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>论坛系统 - 登录/注册</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
display: flex;
justify-content: center;
align-items: center;
}
.container {
width: 100%;
max-width: 450px;
padding: 20px;
}
/* 卡片样式 */
.card {
background: white;
border-radius: 16px;
padding: 40px;
box-shadow: 0 20px 60px rgba(0,0,0,0.3);
animation: fadeIn 0.5s ease;
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(-20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.logo {
text-align: center;
font-size: 48px;
margin-bottom: 20px;
}
.title {
text-align: center;
font-size: 28px;
font-weight: 600;
color: #333;
margin-bottom: 30px;
}
/* 表单样式 */
.form-group {
margin-bottom: 20px;
}
.form-group label {
display: block;
margin-bottom: 8px;
color: #666;
font-weight: 500;
font-size: 14px;
}
.form-group input {
width: 100%;
padding: 12px 15px;
border: 2px solid #e0e0e0;
border-radius: 8px;
font-size: 16px;
transition: all 0.3s;
outline: none;
}
.form-group input:focus {
border-color: #667eea;
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
}
.form-group input.error {
border-color: #f56565;
}
/* 按钮样式 */
.btn {
width: 100%;
padding: 14px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
border-radius: 8px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s;
margin-top: 10px;
}
.btn:hover {
transform: translateY(-2px);
box-shadow: 0 10px 20px rgba(102, 126, 234, 0.3);
}
.btn:active {
transform: translateY(0);
}
.btn:disabled {
opacity: 0.6;
cursor: not-allowed;
transform: none;
}
/* 切换链接 */
.switch-link {
text-align: center;
margin-top: 25px;
color: #666;
font-size: 14px;
}
.switch-link a {
color: #667eea;
text-decoration: none;
cursor: pointer;
font-weight: 500;
}
.switch-link a:hover {
text-decoration: underline;
}
/* 消息提示 */
.message {
padding: 12px;
border-radius: 8px;
margin-bottom: 20px;
font-size: 14px;
display: none;
animation: slideDown 0.3s ease;
}
@keyframes slideDown {
from {
opacity: 0;
transform: translateY(-10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.message.success {
background: #c6f6d5;
color: #22543d;
border-left: 4px solid #48bb78;
display: block;
}
.message.error {
background: #fed7d7;
color: #742a2a;
border-left: 4px solid #f56565;
display: block;
}
.message.info {
background: #bee3f8;
color: #2c5282;
border-left: 4px solid #4299e1;
display: block;
}
/* 隐藏类 */
.hidden {
display: none;
}
/* 加载动画 */
.loading {
display: inline-block;
width: 16px;
height: 16px;
border: 2px solid white;
border-radius: 50%;
border-top-color: transparent;
animation: spin 0.6s linear infinite;
margin-right: 8px;
vertical-align: middle;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
/* 密码强度提示 */
.password-strength {
margin-top: 5px;
font-size: 12px;
}
.strength-weak { color: #f56565; }
.strength-medium { color: #ed8936; }
.strength-strong { color: #48bb78; }
/* 记住我复选框 */
.checkbox-group {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 20px;
}
.checkbox-label {
display: flex;
align-items: center;
cursor: pointer;
}
.checkbox-label input {
width: auto;
margin-right: 8px;
}
.forgot-link {
color: #667eea;
text-decoration: none;
font-size: 14px;
}
/* 页脚 */
.footer {
text-align: center;
margin-top: 20px;
color: rgba(255,255,255,0.8);
font-size: 12px;
}
</style>
</head>
<body>
<div class="container">
<!-- 登录表单 -->
<div id="loginForm" class="card">
<div class="logo">📝</div>
<div class="title">欢迎回来</div>
<div id="loginMessage" class="message"></div>
<form id="loginFormElement">
<div class="form-group">
<label>用户名</label>
<input type="text" id="loginUsername" placeholder="请输入用户名" autocomplete="username">
</div>
<div class="form-group">
<label>密码</label>
<input type="password" id="loginPassword" placeholder="请输入密码" autocomplete="current-password">
</div>
<div class="checkbox-group">
<label class="checkbox-label">
<input type="checkbox" id="rememberMe"> 记住我
</label>
<a class="forgot-link" href="javascript:void(0)" onclick="showForgotPassword()">忘记密码?</a>
</div>
<button type="submit" class="btn" id="loginBtn">登 录</button>
</form>
<div class="switch-link">
还没有账号? <a onclick="showRegister()">立即注册</a>
</div>
</div>
<!-- 注册表单 -->
<div id="registerForm" class="card hidden">
<div class="logo">📝</div>
<div class="title">创建账号</div>
<div id="registerMessage" class="message"></div>
<form id="registerFormElement">
<div class="form-group">
<label>用户名</label>
<input type="text" id="regUsername" placeholder="3-20位字母、数字、下划线">
<small style="color: #999; font-size: 12px;">用户名长度3-20,只能包含字母、数字和下划线</small>
</div>
<div class="form-group">
<label>邮箱</label>
<input type="email" id="regEmail" placeholder="example@email.com">
</div>
<div class="form-group">
<label>手机号</label>
<input type="tel" id="regPhone" placeholder="手机号码">
</div>
<div class="form-group">
<label>密码</label>
<input type="password" id="regPassword" placeholder="请输入密码">
<div id="passwordStrength" class="password-strength"></div>
</div>
<div class="form-group">
<label>确认密码</label>
<input type="password" id="regConfirmPassword" placeholder="请再次输入密码">
</div>
<div class="form-group">
<label>昵称</label>
<input type="text" id="regNickname" placeholder="昵称(选填)">
</div>
<button type="submit" class="btn" id="registerBtn">注 册</button>
</form>
<div class="switch-link">
已有账号? <a onclick="showLogin()">立即登录</a>
</div>
</div>
<!-- 首页(登录后显示) -->
<div id="homePage" class="card hidden">
<div class="logo">🎉</div>
<div class="title" id="welcomeTitle">欢迎回来!</div>
<div id="homeMessage" class="message success" style="display: none;"></div>
<div id="userInfo" style="margin-bottom: 20px;">
<div style="background: #f7fafc; padding: 20px; border-radius: 12px; margin-bottom: 20px;">
<div style="display: flex; align-items: center; margin-bottom: 15px;">
<div style="width: 60px; height: 60px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 24px; color: white; margin-right: 15px;">
👤
</div>
<div>
<h3 id="userNickname" style="margin-bottom: 5px;">用户</h3>
<p id="userUsername" style="color: #666; font-size: 14px;">@username</p>
</div>
</div>
<div style="border-top: 1px solid #e2e8f0; padding-top: 15px;">
<p><strong>邮箱:</strong> <span id="userEmail">-</span></p>
<p><strong>手机:</strong> <span id="userPhone">-</span></p>
<p><strong>用户ID:</strong> <span id="userId">-</span></p>
<p><strong>状态:</strong> <span id="userStatus">-</span></p>
</div>
</div>
</div>
<div style="display: flex; gap: 15px;">
<button class="btn" onclick="getCurrentUser()" style="background: #48bb78;">刷新信息</button>
<button class="btn" onclick="logout()" style="background: #f56565;">退出登录</button>
</div>
<div class="switch-link" style="margin-top: 20px;">
<a onclick="showLogin()">返回登录页</a>
</div>
</div>
<div class="footer">
© 2024 论坛系统 | 连接思想,分享知识
</div>
</div>
<script>
// API配置
const API_BASE_URL = 'http://localhost:8081'; // 修改为你的服务器地址
// Token存储key
const TOKEN_KEY = 'forum_token';
const USER_KEY = 'forum_user';
// 页面加载时检查登录状态
document.addEventListener('DOMContentLoaded', function() {
checkLoginStatus();
initEventListeners();
});
// 初始化事件监听
function initEventListeners() {
// 登录表单提交
document.getElementById('loginFormElement').addEventListener('submit', function(e) {
e.preventDefault();
login();
});
// 注册表单提交
document.getElementById('registerFormElement').addEventListener('submit', function(e) {
e.preventDefault();
register();
});
// 密码强度检测
document.getElementById('regPassword').addEventListener('input', function() {
checkPasswordStrength(this.value);
});
}
// 检查登录状态
function checkLoginStatus() {
const token = localStorage.getItem(TOKEN_KEY);
if (token) {
// 验证token有效性
getCurrentUser();
} else {
showLogin();
}
}
// 显示登录表单
function showLogin() {
document.getElementById('loginForm').classList.remove('hidden');
document.getElementById('registerForm').classList.add('hidden');
document.getElementById('homePage').classList.add('hidden');
clearMessages();
}
// 显示注册表单
function showRegister() {
document.getElementById('loginForm').classList.add('hidden');
document.getElementById('registerForm').classList.remove('hidden');
document.getElementById('homePage').classList.add('hidden');
clearMessages();
}
// 显示首页
function showHome() {
document.getElementById('loginForm').classList.add('hidden');
document.getElementById('registerForm').classList.add('hidden');
document.getElementById('homePage').classList.remove('hidden');
}
// 清空消息
function clearMessages() {
document.querySelectorAll('.message').forEach(msg => {
msg.style.display = 'none';
msg.textContent = '';
});
}
// 显示消息
function showMessage(elementId, message, type) {
const element = document.getElementById(elementId);
element.textContent = message;
element.className = `message ${type}`;
element.style.display = 'block';
// 3秒后自动隐藏
setTimeout(() => {
if (element.style.display === 'block') {
element.style.display = 'none';
}
}, 5000);
}
// 设置按钮加载状态
function setButtonLoading(buttonId, loading) {
const btn = document.getElementById(buttonId);
if (loading) {
btn.disabled = true;
btn.innerHTML = '<span class="loading"></span>处理中...';
} else {
btn.disabled = false;
btn.innerHTML = btn.id === 'loginBtn' ? '登 录' : '注 册';
}
}
// 登录方法
async function login() {
const username = document.getElementById('loginUsername').value.trim();
const password = document.getElementById('loginPassword').value;
const rememberMe = document.getElementById('rememberMe').checked;
// 表单验证
if (!username) {
showMessage('loginMessage', '请输入用户名', 'error');
return;
}
if (!password) {
showMessage('loginMessage', '请输入密码', 'error');
return;
}
setButtonLoading('loginBtn', true);
try {
const response = await fetch(`${API_BASE_URL}/api/auth/login`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ username, password })
});
const result = await response.json();
if (result.code === 200) {
// 登录成功
if (rememberMe) {
localStorage.setItem(TOKEN_KEY, result.data.token);
localStorage.setItem(USER_KEY, JSON.stringify(result.data));
} else {
sessionStorage.setItem(TOKEN_KEY, result.data.token);
sessionStorage.setItem(USER_KEY, JSON.stringify(result.data));
}
showMessage('loginMessage', '登录成功!正在跳转...', 'success');
setTimeout(() => {
displayUserInfo(result.data);
showHome();
}, 1000);
} else {
showMessage('loginMessage', result.message || '登录失败', 'error');
}
} catch (error) {
console.error('登录错误:', error);
showMessage('loginMessage', '网络错误,请稍后重试', 'error');
} finally {
setButtonLoading('loginBtn', false);
}
}
// 注册方法
async function register() {
const username = document.getElementById('regUsername').value.trim();
const email = document.getElementById('regEmail').value.trim();
const phone = document.getElementById('regPhone').value.trim();
const password = document.getElementById('regPassword').value;
const confirmPassword = document.getElementById('regConfirmPassword').value;
const nickname = document.getElementById('regNickname').value.trim() || username;
// 表单验证
if (!username) {
showMessage('registerMessage', '请输入用户名', 'error');
return;
}
if (username.length < 3 || username.length > 20) {
showMessage('registerMessage', '用户名长度必须在3-20之间', 'error');
return;
}
if (!/^[a-zA-Z0-9_]+$/.test(username)) {
showMessage('registerMessage', '用户名只能包含字母、数字和下划线', 'error');
return;
}
if (!password) {
showMessage('registerMessage', '请输入密码', 'error');
return;
}
if (password.length < 6) {
showMessage('registerMessage', '密码长度不能少于6位', 'error');
return;
}
if (password !== confirmPassword) {
showMessage('registerMessage', '两次输入的密码不一致', 'error');
return;
}
if (email && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
showMessage('registerMessage', '邮箱格式不正确', 'error');
return;
}
if (phone && !/^1[3-9]\d{9}$/.test(phone)) {
showMessage('registerMessage', '手机号格式不正确', 'error');
return;
}
setButtonLoading('registerBtn', true);
try {
const response = await fetch(`${API_BASE_URL}/api/auth/register`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
username,
email: email || null,
phone: phone || null,
password,
confirmPassword,
nickname
})
});
const result = await response.json();
if (result.code === 200) {
showMessage('registerMessage', '注册成功!请登录', 'success');
setTimeout(() => {
showLogin();
// 清空注册表单
document.getElementById('registerFormElement').reset();
}, 1500);
} else {
showMessage('registerMessage', result.message || '注册失败', 'error');
}
} catch (error) {
console.error('注册错误:', error);
showMessage('registerMessage', '网络错误,请稍后重试', 'error');
} finally {
setButtonLoading('registerBtn', false);
}
}
// 获取当前用户信息
async function getCurrentUser() {
const token = localStorage.getItem(TOKEN_KEY) || sessionStorage.getItem(TOKEN_KEY);
if (!token) {
showLogin();
return;
}
try {
const response = await fetch(`${API_BASE_URL}/api/auth/current`, {
method: 'GET',
headers: {
'Authorization': `Bearer ${token}`
}
});
const result = await response.json();
if (result.code === 200) {
displayUserInfo(result.data);
showHome();
} else {
// token无效,清除本地存储
localStorage.removeItem(TOKEN_KEY);
sessionStorage.removeItem(TOKEN_KEY);
localStorage.removeItem(USER_KEY);
sessionStorage.removeItem(USER_KEY);
showLogin();
if (result.message) {
showMessage('loginMessage', result.message, 'error');
}
}
} catch (error) {
console.error('获取用户信息错误:', error);
showLogin();
}
}
// 显示用户信息
function displayUserInfo(user) {
document.getElementById('welcomeTitle').textContent = `欢迎回来,${user.nickname || user.username}!`;
document.getElementById('userNickname').textContent = user.nickname || user.username;
document.getElementById('userUsername').textContent = `@${user.username}`;
document.getElementById('userEmail').textContent = user.email || '未设置';
document.getElementById('userPhone').textContent = user.phone || '未设置';
document.getElementById('userId').textContent = user.id;
const statusMap = {0: '已禁用', 1: '正常', 2: '已锁定'};
document.getElementById('userStatus').textContent = statusMap[user.status] || '未知';
const homeMessage = document.getElementById('homeMessage');
homeMessage.textContent = `欢迎回来!上次登录时间: ${user.lastLoginTime || '首次登录'}`;
homeMessage.style.display = 'block';
setTimeout(() => {
homeMessage.style.display = 'none';
}, 5000);
}
// 退出登录
function logout() {
localStorage.removeItem(TOKEN_KEY);
sessionStorage.removeItem(TOKEN_KEY);
localStorage.removeItem(USER_KEY);
sessionStorage.removeItem(USER_KEY);
showMessage('loginMessage', '已安全退出登录', 'success');
setTimeout(() => {
showLogin();
// 清空登录表单
document.getElementById('loginFormElement').reset();
}, 1000);
}
// 密码强度检测
function checkPasswordStrength(password) {
const strengthDiv = document.getElementById('passwordStrength');
if (!password) {
strengthDiv.innerHTML = '';
return;
}
let strength = 0;
let message = '';
let className = '';
// 长度检查
if (password.length >= 8) strength++;
if (password.length >= 12) strength++;
// 包含数字
if (/\d/.test(password)) strength++;
// 包含小写字母
if (/[a-z]/.test(password)) strength++;
// 包含大写字母
if (/[A-Z]/.test(password)) strength++;
// 包含特殊字符
if (/[!@#$%^&*(),.?":{}|<>]/.test(password)) strength++;
if (strength <= 2) {
message = '弱密码';
className = 'strength-weak';
} else if (strength <= 4) {
message = '中等密码';
className = 'strength-medium';
} else {
message = '强密码';
className = 'strength-strong';
}
strengthDiv.innerHTML = `<span class="${className}">密码强度:${message}</span>`;
}
// 忘记密码(演示功能)
function showForgotPassword() {
alert('请联系管理员重置密码\n邮箱:admin@forum.com');
}
// 添加回车键支持
document.getElementById('loginPassword').addEventListener('keypress', function(e) {
if (e.key === 'Enter') {
login();
}
});
document.getElementById('regConfirmPassword').addEventListener('keypress', function(e) {
if (e.key === 'Enter') {
register();
}
});
</script>
</body>
</html>
部署jar包
将auth和user模块打包发到服务器
注意这两个模块的pom加打包插件
java
<build>
<!--设置打包名称-->
<finalName>forum-user</finalName>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
</plugins>
</build>
设置打包的jar包名称
打包方法
进入命令框

分步构建
bash
# 1. 进入项目根目录
cd /path/to/forum-backend
# 2. 先清理所有
mvn clean
# 3. 只编译不打包(安装到本地仓库)
# 注意:这里用 install,不是 package
mvn install -DskipTests
# 4. 如果第3步成功,再单独打包认证服务
cd forum-auth-server
mvn package -DskipTests
# 5. 打包用户服务
cd ../forum-user-service
mvn package -DskipTests
找到打包后的jar包

连接服务器,创建目录

上传到服务器需要第三方软件
下载链接:winSCP
如果链接不了,在阿里云使用密码链接,如果有以下提示

说明服务器没有开密码登录,
实例充值密码即可
连接后上传jar包

创建auth启动脚本
bash
vim auth.sh
输入命令
bash
nohup java -jar forum-auth.jar > auth.log 2>&1 &
创建auth启动脚本
bash
vim user.sh
输入命令
bash
nohup java -jar forum-user.jar > user.log 2>&1 &
给jar包添加执行权限
bash
chmod +x forum-auth.jar
chmod +x forum-user.jar
启动auth
bash
bash auth.sh
两个服务启动成功后准备页面
在宝塔页面添加html服务

完成后在/www/wwwroot/121.40.199.142目录已有宝塔默认文件
在浏览器输入服务器IP即可看到页面

然后把/www/wwwroot/121.40.199.142下的index.html文件换成我们的文件。刷新页面

此时登录会报错,有跨域问题
跨域问题在 forum-core模块进行配置,下一个博客进行 forum-core的编写。