Skip to content

阶段七:项目实战(持续进行) ​

纸上得来终觉浅,绝知此事要躬行。本阶段通过五个由易到难的实战项目,将前面所学知识融会贯通,构建完整的Java后端开发能力。


7.1 推荐实战项目(由易到难) ​

项目总览 ​

序号项目名称技术栈难度预计周期核心能力
1个人博客系统Spring Boot + MyBatis + MySQL + Redis⭐⭐2周基础CRUD、缓存
2电商系统Spring Cloud微服务架构⭐⭐⭐⭐4周微服务、分布式
3即时通讯系统WebSocket + Netty⭐⭐⭐2周长连接、高性能
4短链接服务Redis + 分布式ID⭐⭐⭐1周高并发、高性能
5权限管理系统RBAC + Spring Security + JWT⭐⭐⭐⭐3周安全、权限设计

7.1.1 个人博客系统 ​

项目概述 ​

一个功能完整的个人博客系统,支持文章发布、分类管理、标签管理、评论互动、用户管理等核心功能。

技术选型 ​

层次技术说明
框架Spring Boot 3.x核心框架
持久层MyBatis-PlusORM框架
数据库MySQL 8.0关系型数据库
缓存Redis热点数据缓存
认证JWT用户认证
文档Swagger/Knife4jAPI文档
构建Maven项目构建

项目目录结构 ​

blog-system/
├── pom.xml
├── src/
│   ├── main/
│   │   ├── java/com/example/blog/
│   │   │   ├── BlogApplication.java              # 启动类
│   │   │   ├── config/                            # 配置类
│   │   │   │   ├── RedisConfig.java
│   │   │   │   ├── SwaggerConfig.java
│   │   │   │   ├── WebMvcConfig.java
│   │   │   │   └── MyBatisPlusConfig.java
│   │   │   ├── common/                            # 公共模块
│   │   │   │   ├── Result.java                    # 统一响应
│   │   │   │   ├── PageResult.java                # 分页响应
│   │   │   │   ├── BusinessException.java         # 业务异常
│   │   │   │   ├── GlobalExceptionHandler.java    # 全局异常处理
│   │   │   │   └── Constants.java                 # 常量定义
│   │   │   ├── entity/                            # 实体类
│   │   │   │   ├── User.java
│   │   │   │   ├── Article.java
│   │   │   │   ├── Category.java
│   │   │   │   ├── Tag.java
│   │   │   │   ├── Comment.java
│   │   │   │   └── ArticleTag.java
│   │   │   ├── dto/                               # 数据传输对象
│   │   │   │   ├── LoginDTO.java
│   │   │   │   ├── RegisterDTO.java
│   │   │   │   ├── ArticleDTO.java
│   │   │   │   ├── ArticleQueryDTO.java
│   │   │   │   └── CommentDTO.java
│   │   │   ├── vo/                                # 视图对象
│   │   │   │   ├── UserVO.java
│   │   │   │   ├── ArticleVO.java
│   │   │   │   ├── ArticleDetailVO.java
│   │   │   │   └── CommentVO.java
│   │   │   ├── mapper/                            # Mapper接口
│   │   │   │   ├── UserMapper.java
│   │   │   │   ├── ArticleMapper.java
│   │   │   │   ├── CategoryMapper.java
│   │   │   │   ├── TagMapper.java
│   │   │   │   └── CommentMapper.java
│   │   │   ├── service/                           # 服务接口
│   │   │   │   ├── UserService.java
│   │   │   │   ├── ArticleService.java
│   │   │   │   ├── CategoryService.java
│   │   │   │   ├── TagService.java
│   │   │   │   └── CommentService.java
│   │   │   ├── service/impl/                      # 服务实现
│   │   │   │   ├── UserServiceImpl.java
│   │   │   │   ├── ArticleServiceImpl.java
│   │   │   │   ├── CategoryServiceImpl.java
│   │   │   │   ├── TagServiceImpl.java
│   │   │   │   └── CommentServiceImpl.java
│   │   │   ├── controller/                        # 控制器
│   │   │   │   ├── UserController.java
│   │   │   │   ├── ArticleController.java
│   │   │   │   ├── CategoryController.java
│   │   │   │   ├── TagController.java
│   │   │   │   └── CommentController.java
│   │   │   ├── interceptor/                       # 拦截器
│   │   │   │   └── JwtInterceptor.java
│   │   │   └── util/                              # 工具类
│   │   │       ├── JwtUtil.java
│   │   │       └── RedisUtil.java
│   │   └── resources/
│   │       ├── application.yml                    # 主配置
│   │       ├── application-dev.yml                # 开发环境
│   │       ├── application-prod.yml               # 生产环境
│   │       └── mapper/                            # MyBatis XML
│   │           ├── ArticleMapper.xml
│   │           └── CommentMapper.xml
│   └── test/
│       └── java/com/example/blog/
│           ├── service/
│           │   └── ArticleServiceTest.java
│           └── controller/
│               └── ArticleControllerTest.java
└── sql/
    └── blog_init.sql                              # 初始化SQL

数据库设计 ​

sql
-- 用户表
CREATE TABLE `user` (
    `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '用户ID',
    `username` VARCHAR(50) NOT NULL COMMENT '用户名',
    `password` VARCHAR(100) NOT NULL COMMENT '密码(BCrypt加密)',
    `nickname` VARCHAR(50) DEFAULT NULL COMMENT '昵称',
    `email` VARCHAR(100) DEFAULT NULL COMMENT '邮箱',
    `avatar` VARCHAR(255) DEFAULT NULL COMMENT '头像URL',
    `role` TINYINT NOT NULL DEFAULT 0 COMMENT '角色:0-普通用户,1-管理员',
    `status` TINYINT NOT NULL DEFAULT 1 COMMENT '状态:0-禁用,1-启用',
    `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
    `updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
    PRIMARY KEY (`id`),
    UNIQUE KEY `uk_username` (`username`),
    UNIQUE KEY `uk_email` (`email`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表';

-- 分类表
CREATE TABLE `category` (
    `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '分类ID',
    `name` VARCHAR(50) NOT NULL COMMENT '分类名称',
    `description` VARCHAR(200) DEFAULT NULL COMMENT '分类描述',
    `sort_order` INT NOT NULL DEFAULT 0 COMMENT '排序序号',
    `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
    PRIMARY KEY (`id`),
    UNIQUE KEY `uk_name` (`name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='分类表';

-- 标签表
CREATE TABLE `tag` (
    `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '标签ID',
    `name` VARCHAR(50) NOT NULL COMMENT '标签名称',
    `color` VARCHAR(20) DEFAULT '#409EFF' COMMENT '标签颜色',
    `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
    PRIMARY KEY (`id`),
    UNIQUE KEY `uk_name` (`name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='标签表';

-- 文章表
CREATE TABLE `article` (
    `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '文章ID',
    `title` VARCHAR(200) NOT NULL COMMENT '文章标题',
    `summary` VARCHAR(500) DEFAULT NULL COMMENT '文章摘要',
    `content` LONGTEXT NOT NULL COMMENT '文章内容(Markdown)',
    `cover_image` VARCHAR(255) DEFAULT NULL COMMENT '封面图片URL',
    `author_id` BIGINT NOT NULL COMMENT '作者ID',
    `category_id` BIGINT DEFAULT NULL COMMENT '分类ID',
    `status` TINYINT NOT NULL DEFAULT 0 COMMENT '状态:0-草稿,1-已发布',
    `view_count` INT NOT NULL DEFAULT 0 COMMENT '浏览次数',
    `like_count` INT NOT NULL DEFAULT 0 COMMENT '点赞数',
    `comment_count` INT NOT NULL DEFAULT 0 COMMENT '评论数',
    `is_top` TINYINT NOT NULL DEFAULT 0 COMMENT '是否置顶:0-否,1-是',
    `is_deleted` TINYINT NOT NULL DEFAULT 0 COMMENT '逻辑删除:0-未删除,1-已删除',
    `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
    `updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
    PRIMARY KEY (`id`),
    KEY `idx_author_id` (`author_id`),
    KEY `idx_category_id` (`category_id`),
    KEY `idx_status` (`status`),
    KEY `idx_created_at` (`created_at`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='文章表';

-- 文章标签关联表
CREATE TABLE `article_tag` (
    `article_id` BIGINT NOT NULL COMMENT '文章ID',
    `tag_id` BIGINT NOT NULL COMMENT '标签ID',
    PRIMARY KEY (`article_id`, `tag_id`),
    KEY `idx_tag_id` (`tag_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='文章标签关联表';

-- 评论表
CREATE TABLE `comment` (
    `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '评论ID',
    `content` VARCHAR(1000) NOT NULL COMMENT '评论内容',
    `article_id` BIGINT NOT NULL COMMENT '文章ID',
    `user_id` BIGINT NOT NULL COMMENT '评论用户ID',
    `parent_id` BIGINT DEFAULT NULL COMMENT '父评论ID(用于回复)',
    `reply_to_user_id` BIGINT DEFAULT NULL COMMENT '回复目标用户ID',
    `status` TINYINT NOT NULL DEFAULT 1 COMMENT '状态:0-待审核,1-已通过',
    `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
    PRIMARY KEY (`id`),
    KEY `idx_article_id` (`article_id`),
    KEY `idx_user_id` (`user_id`),
    KEY `idx_parent_id` (`parent_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='评论表';

核心代码示例 ​

pom.xml 依赖配置:

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>3.2.0</version>
    </parent>

    <groupId>com.example</groupId>
    <artifactId>blog-system</artifactId>
    <version>1.0.0</version>

    <properties>
        <java.version>17</java.version>
        <mybatis-plus.version>3.5.5</mybatis-plus.version>
        <jjwt.version>0.12.3</jjwt.version>
        <knife4j.version>4.3.0</knife4j.version>
    </properties>

    <dependencies>
        <!-- Spring Boot Web -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <!-- Spring Boot Validation -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-validation</artifactId>
        </dependency>

        <!-- Spring Boot Redis -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>

        <!-- MyBatis-Plus -->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-spring-boot3-starter</artifactId>
            <version>${mybatis-plus.version}</version>
        </dependency>

        <!-- MySQL -->
        <dependency>
            <groupId>com.mysql</groupId>
            <artifactId>mysql-connector-j</artifactId>
            <scope>runtime</scope>
        </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>

        <!-- Knife4j(Swagger增强) -->
        <dependency>
            <groupId>com.github.xiaoymin</groupId>
            <artifactId>knife4j-openapi3-jakarta-spring-boot-starter</artifactId>
            <version>${knife4j.version}</version>
        </dependency>

        <!-- Lombok -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>

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

application.yml 配置:

yaml
server:
  port: 8080

spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/blog_db?useSSL=false&serverTimezone=Asia/Shanghai&characterEncoding=utf8mb4
    username: root
    password: ${DB_PASSWORD:123456}

  data:
    redis:
      host: localhost
      port: 6379
      password: ${REDIS_PASSWORD:}
      lettuce:
        pool:
          max-active: 8
          max-idle: 8
          min-idle: 0

  jackson:
    date-format: yyyy-MM-dd HH:mm:ss
    time-zone: Asia/Shanghai

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

jwt:
  secret: ${JWT_SECRET:mySecretKeyForBlogSystemMustBeLongEnough2024}
  expiration: 86400000  # 24小时

logging:
  level:
    com.example.blog: debug

统一响应类 Result.java:

java
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Result<T> {
    private int code;
    private String message;
    private T data;

    public static <T> Result<T> success(T data) {
        return new Result<>(200, "success", data);
    }

    public static <T> Result<T> success() {
        return new Result<>(200, "success", null);
    }

    public static <T> Result<T> error(int code, String message) {
        return new Result<>(code, message, null);
    }

    public static <T> Result<T> error(String message) {
        return new Result<>(500, message, null);
    }
}

全局异常处理 GlobalExceptionHandler.java:

java
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {

    /**
     * 业务异常
     */
    @ExceptionHandler(BusinessException.class)
    public Result<Void> handleBusinessException(BusinessException e) {
        log.warn("业务异常:{}", e.getMessage());
        return Result.error(e.getCode(), e.getMessage());
    }

    /**
     * 参数校验异常
     */
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public Result<Void> handleValidException(MethodArgumentNotValidException e) {
        String message = e.getBindingResult().getAllErrors().get(0).getDefaultMessage();
        log.warn("参数校验失败:{}", message);
        return Result.error(400, message);
    }

    /**
     * 系统异常
     */
    @ExceptionHandler(Exception.class)
    public Result<Void> handleException(Exception e) {
        log.error("系统异常:", e);
        return Result.error("系统繁忙,请稍后重试");
    }
}

文章实体 Article.java:

java
@Data
@TableName("article")
public class Article {
    @TableId(type = IdType.AUTO)
    private Long id;

    private String title;
    private String summary;
    private String content;
    private String coverImage;
    private Long authorId;
    private Long categoryId;
    private Integer status;

    @TableField(fill = FieldFill.INSERT)
    private Integer viewCount;

    @TableField(fill = FieldFill.INSERT)
    private Integer likeCount;

    @TableField(fill = FieldFill.INSERT)
    private Integer commentCount;

    private Integer isTop;

    @TableLogic
    private Integer isDeleted;

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

    @TableField(fill = FieldFill.INSERT_UPDATE)
    private LocalDateTime updatedAt;
}

文章服务层 ArticleServiceImpl.java:

java
@Service
@Slf4j
public class ArticleServiceImpl extends ServiceImpl<ArticleMapper, Article>
        implements ArticleService {

    @Autowired
    private ArticleTagMapper articleTagMapper;
    @Autowired
    private CategoryMapper categoryMapper;
    @Autowired
    private UserMapper userMapper;
    @Autowired
    private StringRedisTemplate redisTemplate;

    private static final String ARTICLE_CACHE_KEY = "blog:article:";
    private static final String ARTICLE_LIST_CACHE_KEY = "blog:article:list:";

    /**
     * 发布文章
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public Long publishArticle(ArticleDTO dto) {
        // 1. 构建文章实体
        Article article = new Article();
        BeanUtils.copyProperties(dto, article);
        article.setStatus(1); // 已发布

        // 2. 保存文章
        this.save(article);

        // 3. 保存文章标签关联
        if (dto.getTagIds() != null && !dto.getTagIds().isEmpty()) {
            for (Long tagId : dto.getTagIds()) {
                ArticleTag articleTag = new ArticleTag(article.getId(), tagId);
                articleTagMapper.insert(articleTag);
            }
        }

        // 4. 清除缓存
        redisTemplate.delete(ARTICLE_LIST_CACHE_KEY + "*");

        log.info("文章发布成功,ID:{}", article.getId());
        return article.getId();
    }

    /**
     * 分页查询文章列表(带缓存)
     */
    @Override
    public PageResult<ArticleVO> getArticleList(ArticleQueryDTO query) {
        String cacheKey = ARTICLE_LIST_CACHE_KEY + query.getPage() + ":" + query.getSize();

        // 尝试从缓存获取
        String cached = redisTemplate.opsForValue().get(cacheKey);
        if (cached != null) {
            try {
                return new ObjectMapper().readValue(cached,
                    new TypeReference<PageResult<ArticleVO>>() {});
            } catch (JsonProcessingException e) {
                log.warn("缓存反序列化失败,重新查询");
            }
        }

        // 构建查询条件
        Page<Article> page = new Page<>(query.getPage(), query.getSize());
        LambdaQueryWrapper<Article> wrapper = new LambdaQueryWrapper<>();

        wrapper.eq(Article::getStatus, 1)
               .eq(query.getCategoryId() != null, Article::getCategoryId, query.getCategoryId())
               .like(StringUtils.hasText(query.getKeyword()), Article::getTitle, query.getKeyword())
               .orderByDesc(Article::getIsTop)
               .orderByDesc(Article::getCreatedAt);

        Page<Article> result = this.page(page, wrapper);

        // 转换为VO
        List<ArticleVO> voList = result.getRecords().stream()
                .map(this::convertToVO)
                .collect(Collectors.toList());

        PageResult<ArticleVO> pageResult = new PageResult<>(
                voList, result.getTotal(), query.getPage(), query.getSize());

        // 写入缓存(30分钟过期)
        try {
            redisTemplate.opsForValue().set(cacheKey,
                new ObjectMapper().writeValueAsString(pageResult),
                30, TimeUnit.MINUTES);
        } catch (JsonProcessingException e) {
            log.warn("缓存序列化失败");
        }

        return pageResult;
    }

    /**
     * 获取文章详情
     */
    @Override
    public ArticleDetailVO getArticleDetail(Long id) {
        // 尝试从缓存获取
        String cacheKey = ARTICLE_CACHE_KEY + id;
        String cached = redisTemplate.opsForValue().get(cacheKey);
        if (cached != null) {
            try {
                return new ObjectMapper().readValue(cached, ArticleDetailVO.class);
            } catch (JsonProcessingException e) {
                log.warn("缓存反序列化失败");
            }
        }

        // 查询文章
        Article article = this.getById(id);
        if (article == null || article.getStatus() != 1) {
            throw new BusinessException(404, "文章不存在");
        }

        // 增加浏览次数
        this.update(new UpdateWrapper<Article>()
                .eq("id", id)
                .setSql("view_count = view_count + 1"));

        // 转换为详情VO
        ArticleDetailVO vo = new ArticleDetailVO();
        BeanUtils.copyProperties(article, vo);
        vo.setViewCount(article.getViewCount() + 1);

        // 查询作者信息
        User author = userMapper.selectById(article.getAuthorId());
        vo.setAuthorName(author.getNickname());
        vo.setAuthorAvatar(author.getAvatar());

        // 查询分类信息
        if (article.getCategoryId() != null) {
            Category category = categoryMapper.selectById(article.getCategoryId());
            vo.setCategoryName(category.getName());
        }

        // 查询标签列表
        List<Tag> tags = articleTagMapper.selectTagsByArticleId(id);
        vo.setTags(tags);

        // 写入缓存
        try {
            redisTemplate.opsForValue().set(cacheKey,
                new ObjectMapper().writeValueAsString(vo),
                1, TimeUnit.HOURS);
        } catch (JsonProcessingException e) {
            log.warn("缓存序列化失败");
        }

        return vo;
    }

    /**
     * 转换为VO
     */
    private ArticleVO convertToVO(Article article) {
        ArticleVO vo = new ArticleVO();
        BeanUtils.copyProperties(article, vo);

        // 查询作者
        User author = userMapper.selectById(article.getAuthorId());
        vo.setAuthorName(author.getNickname());

        // 查询分类
        if (article.getCategoryId() != null) {
            Category category = categoryMapper.selectById(article.getCategoryId());
            vo.setCategoryName(category.getName());
        }

        // 查询标签
        List<Tag> tags = articleTagMapper.selectTagsByArticleId(article.getId());
        vo.setTags(tags);

        return vo;
    }
}

文章控制器 ArticleController.java:

java
@RestController
@RequestMapping("/api/articles")
@Tag(name = "文章管理")
@Slf4j
public class ArticleController {

    @Autowired
    private ArticleService articleService;

    @GetMapping
    @Operation(summary = "分页查询文章列表")
    public Result<PageResult<ArticleVO>> list(ArticleQueryDTO query) {
        PageResult<ArticleVO> result = articleService.getArticleList(query);
        return Result.success(result);
    }

    @GetMapping("/{id}")
    @Operation(summary = "获取文章详情")
    public Result<ArticleDetailVO> detail(@PathVariable Long id) {
        ArticleDetailVO vo = articleService.getArticleDetail(id);
        return Result.success(vo);
    }

    @PostMapping
    @Operation(summary = "发布文章")
    public Result<Long> publish(@RequestBody @Valid ArticleDTO dto) {
        Long id = articleService.publishArticle(dto);
        return Result.success(id);
    }

    @PutMapping("/{id}")
    @Operation(summary = "更新文章")
    public Result<Void> update(@PathVariable Long id, @RequestBody @Valid ArticleDTO dto) {
        articleService.updateArticle(id, dto);
        return Result.success();
    }

    @DeleteMapping("/{id}")
    @Operation(summary = "删除文章")
    public Result<Void> delete(@PathVariable Long id) {
        articleService.deleteArticle(id);
        return Result.success();
    }

    @PostMapping("/{id}/like")
    @Operation(summary = "点赞文章")
    public Result<Void> like(@PathVariable Long id) {
        articleService.likeArticle(id);
        return Result.success();
    }
}

JWT工具类 JwtUtil.java:

java
@Component
public class JwtUtil {

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

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

    /**
     * 生成Token
     */
    public String generateToken(Long userId, String username) {
        return Jwts.builder()
                .subject(username)
                .claim("userId", userId)
                .issuedAt(new Date())
                .expiration(new Date(System.currentTimeMillis() + expiration))
                .signWith(getSigningKey())
                .compact();
    }

    /**
     * 解析Token
     */
    public Claims parseToken(String token) {
        return Jwts.parser()
                .setSigningKey(getSigningKey())
                .build()
                .parseClaimsJws(token)
                .getBody();
    }

    /**
     * 验证Token
     */
    public boolean validateToken(String token) {
        try {
            parseToken(token);
            return true;
        } catch (Exception e) {
            return false;
        }
    }

    /**
     * 获取用户ID
     */
    public Long getUserId(String token) {
        Claims claims = parseToken(token);
        return claims.get("userId", Long.class);
    }

    private SecretKey getSigningKey() {
        byte[] keyBytes = Decoders.BASE64.decode(
            Base64.getEncoder().encodeToString(secret.getBytes()));
        return Keys.hmacShaKeyFor(keyBytes);
    }
}

JWT拦截器 JwtInterceptor.java:

java
@Component
public class JwtInterceptor implements HandlerInterceptor {

    @Autowired
    private JwtUtil jwtUtil;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response,
                             Object handler) {
        // 放行OPTIONS预检请求
        if ("OPTIONS".equalsIgnoreCase(request.getMethod())) {
            return true;
        }

        String token = request.getHeader("Authorization");
        if (StringUtils.hasText(token) && token.startsWith("Bearer ")) {
            token = token.substring(7);
        }

        if (!StringUtils.hasText(token) || !jwtUtil.validateToken(token)) {
            response.setStatus(401);
            response.setContentType("application/json;charset=UTF-8");
            try {
                response.getWriter().write("{\"code\":401,\"message\":\"未登录或Token已过期\"}");
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
            return false;
        }

        // 将用户信息存入request属性
        Long userId = jwtUtil.getUserId(token);
        request.setAttribute("userId", userId);
        return true;
    }
}

接口设计 ​

方法路径说明认证
POST/api/users/register用户注册否
POST/api/users/login用户登录否
GET/api/users/info获取用户信息是
GET/api/articles文章列表(分页)否
GET/api/articles/文章详情否
POST/api/articles发布文章是
PUT/api/articles/更新文章是
DELETE/api/articles/删除文章是
POST/api/articles/{id}/like点赞文章是
GET/api/categories分类列表否
POST/api/categories创建分类是
GET/api/tags标签列表否
POST/api/tags创建标签是
GET/api/articles/{id}/comments评论列表否
POST/api/articles/{id}/comments发表评论是

开发步骤 ​

  1. 环境搭建(第1-2天)

    • 创建Spring Boot项目
    • 配置MySQL、Redis
    • 集成MyBatis-Plus
    • 配置Swagger文档
  2. 用户模块(第3-4天)

    • 用户注册/登录
    • JWT认证
    • 用户信息管理
  3. 文章模块(第5-8天)

    • 文章CRUD
    • 分页查询
    • Redis缓存
    • 文章详情(含作者、分类、标签)
  4. 分类与标签(第9-10天)

    • 分类管理
    • 标签管理
    • 文章-标签多对多关联
  5. 评论模块(第11-12天)

    • 发表评论
    • 评论列表(树形结构)
    • 评论数统计
  6. 优化与测试(第13-14天)

    • 接口测试
    • 性能优化
    • 部署上线

7.1.2 电商系统(微服务架构) ​

项目概述 ​

基于Spring Cloud的微服务电商系统,包含用户服务、商品服务、订单服务、支付服务四大核心模块。

技术选型 ​

层次技术说明
微服务框架Spring Cloud微服务治理
注册中心Nacos服务注册与发现
配置中心Nacos Config动态配置管理
网关Spring Cloud GatewayAPI网关
服务调用OpenFeign声明式HTTP调用
负载均衡Spring Cloud LoadBalancer客户端负载均衡
熔断降级Sentinel流量控制与熔断
数据库MySQL 8.0关系型数据库
缓存Redis分布式缓存
消息队列RabbitMQ异步消息
分布式事务Seata分布式事务
分布式IDSnowflake雪花算法

项目目录结构 ​

ecommerce-system/
├── pom.xml                                        # 父POM
├── ecommerce-gateway/                             # API网关
│   ├── pom.xml
│   └── src/main/java/com/example/gateway/
│       ├── GatewayApplication.java
│       ├── config/
│       │   └── GatewayConfig.java
│       └── filter/
│           └── AuthGlobalFilter.java
├── ecommerce-common/                              # 公共模块
│   ├── pom.xml
│   └── src/main/java/com/example/common/
│       ├── result/Result.java
│       ├── exception/BusinessException.java
│       ├── util/SnowflakeIdGenerator.java
│       └── constant/OrderConstants.java
├── ecommerce-user/                                # 用户服务
│   ├── pom.xml
│   └── src/main/java/com/example/user/
│       ├── UserApplication.java
│       ├── entity/User.java
│       ├── entity/UserAddress.java
│       ├── mapper/UserMapper.java
│       ├── mapper/UserAddressMapper.java
│       ├── service/UserService.java
│       ├── service/impl/UserServiceImpl.java
│       ├── controller/UserController.java
│       └── dto/LoginDTO.java
├── ecommerce-product/                             # 商品服务
│   ├── pom.xml
│   └── src/main/java/com/example/product/
│       ├── ProductApplication.java
│       ├── entity/Product.java
│       ├── entity/ProductCategory.java
│       ├── entity/ProductSku.java
│       ├── mapper/ProductMapper.java
│       ├── service/ProductService.java
│       ├── service/impl/ProductServiceImpl.java
│       ├── controller/ProductController.java
│       └── dto/StockDeductionDTO.java
├── ecommerce-order/                               # 订单服务
│   ├── pom.xml
│   └── src/main/java/com/example/order/
│       ├── OrderApplication.java
│       ├── entity/Order.java
│       ├── entity/OrderItem.java
│       ├── mapper/OrderMapper.java
│       ├── mapper/OrderItemMapper.java
│       ├── service/OrderService.java
│       ├── service/impl/OrderServiceImpl.java
│       ├── controller/OrderController.java
│       ├── feign/ProductFeignClient.java
│       ├── feign/UserFeignClient.java
│       ├── listener/OrderMessageListener.java
│       └── dto/CreateOrderDTO.java
├── ecommerce-payment/                             # 支付服务
│   ├── pom.xml
│   └── src/main/java/com/example/payment/
│       ├── PaymentApplication.java
│       ├── entity/PaymentRecord.java
│       ├── mapper/PaymentRecordMapper.java
│       ├── service/PaymentService.java
│       ├── service/impl/PaymentServiceImpl.java
│       ├── controller/PaymentController.java
│       └── strategy/
│           ├── PaymentStrategy.java
│           ├── AlipayStrategy.java
│           └── WechatPayStrategy.java
└── sql/
    ├── ecommerce_user.sql
    ├── ecommerce_product.sql
    ├── ecommerce_order.sql
    └── ecommerce_payment.sql

数据库设计 ​

用户服务数据库:

sql
-- 用户表
CREATE TABLE `user` (
    `id` BIGINT NOT NULL COMMENT '用户ID(雪花算法)',
    `username` VARCHAR(50) NOT NULL COMMENT '用户名',
    `password` VARCHAR(100) NOT NULL COMMENT '密码',
    `phone` VARCHAR(20) DEFAULT NULL COMMENT '手机号',
    `email` VARCHAR(100) DEFAULT NULL COMMENT '邮箱',
    `nickname` VARCHAR(50) DEFAULT NULL COMMENT '昵称',
    `avatar` VARCHAR(255) DEFAULT NULL COMMENT '头像',
    `gender` TINYINT DEFAULT 0 COMMENT '性别:0-未知,1-男,2-女',
    `status` TINYINT NOT NULL DEFAULT 1 COMMENT '状态:0-禁用,1-启用',
    `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
    `updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    PRIMARY KEY (`id`),
    UNIQUE KEY `uk_username` (`username`),
    UNIQUE KEY `uk_phone` (`phone`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表';

-- 收货地址表
CREATE TABLE `user_address` (
    `id` BIGINT NOT NULL AUTO_INCREMENT,
    `user_id` BIGINT NOT NULL COMMENT '用户ID',
    `receiver_name` VARCHAR(50) NOT NULL COMMENT '收货人',
    `receiver_phone` VARCHAR(20) NOT NULL COMMENT '收货电话',
    `province` VARCHAR(50) NOT NULL COMMENT '省',
    `city` VARCHAR(50) NOT NULL COMMENT '市',
    `district` VARCHAR(50) NOT NULL COMMENT '区',
    `detail_address` VARCHAR(200) NOT NULL COMMENT '详细地址',
    `is_default` TINYINT NOT NULL DEFAULT 0 COMMENT '是否默认地址',
    `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
    `updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    PRIMARY KEY (`id`),
    KEY `idx_user_id` (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='收货地址表';

商品服务数据库:

sql
-- 商品分类表
CREATE TABLE `product_category` (
    `id` BIGINT NOT NULL AUTO_INCREMENT,
    `name` VARCHAR(50) NOT NULL COMMENT '分类名称',
    `parent_id` BIGINT NOT NULL DEFAULT 0 COMMENT '父分类ID',
    `level` TINYINT NOT NULL COMMENT '层级:1-一级,2-二级,3-三级',
    `sort_order` INT NOT NULL DEFAULT 0 COMMENT '排序',
    `icon` VARCHAR(255) DEFAULT NULL COMMENT '图标',
    `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
    PRIMARY KEY (`id`),
    KEY `idx_parent_id` (`parent_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='商品分类表';

-- 商品表(SPU)
CREATE TABLE `product` (
    `id` BIGINT NOT NULL COMMENT '商品ID',
    `name` VARCHAR(200) NOT NULL COMMENT '商品名称',
    `category_id` BIGINT NOT NULL COMMENT '分类ID',
    `brand` VARCHAR(100) DEFAULT NULL COMMENT '品牌',
    `description` TEXT COMMENT '商品描述',
    `main_image` VARCHAR(255) NOT NULL COMMENT '主图',
    `images` VARCHAR(1000) DEFAULT NULL COMMENT '图片列表(JSON)',
    `status` TINYINT NOT NULL DEFAULT 0 COMMENT '状态:0-下架,1-上架',
    `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
    `updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    PRIMARY KEY (`id`),
    KEY `idx_category_id` (`category_id`),
    KEY `idx_status` (`status`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='商品表';

-- 商品SKU表
CREATE TABLE `product_sku` (
    `id` BIGINT NOT NULL COMMENT 'SKU ID',
    `product_id` BIGINT NOT NULL COMMENT '商品ID',
    `sku_name` VARCHAR(200) NOT NULL COMMENT 'SKU名称',
    `sku_attrs` VARCHAR(500) DEFAULT NULL COMMENT 'SKU属性(JSON)',
    `price` DECIMAL(10,2) NOT NULL COMMENT '价格',
    `original_price` DECIMAL(10,2) DEFAULT NULL COMMENT '原价',
    `stock` INT NOT NULL DEFAULT 0 COMMENT '库存',
    `locked_stock` INT NOT NULL DEFAULT 0 COMMENT '锁定库存',
    `image` VARCHAR(255) DEFAULT NULL COMMENT 'SKU图片',
    `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
    `updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    PRIMARY KEY (`id`),
    KEY `idx_product_id` (`product_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='商品SKU表';

订单服务数据库:

sql
-- 订单表
CREATE TABLE `order` (
    `id` BIGINT NOT NULL COMMENT '订单ID',
    `order_no` VARCHAR(32) NOT NULL COMMENT '订单编号',
    `user_id` BIGINT NOT NULL COMMENT '用户ID',
    `total_amount` DECIMAL(10,2) NOT NULL COMMENT '订单总金额',
    `pay_amount` DECIMAL(10,2) DEFAULT NULL COMMENT '实付金额',
    `freight_amount` DECIMAL(10,2) NOT NULL DEFAULT 0.00 COMMENT '运费',
    `discount_amount` DECIMAL(10,2) NOT NULL DEFAULT 0.00 COMMENT '优惠金额',
    `status` TINYINT NOT NULL DEFAULT 0 COMMENT '状态:0-待支付,1-已支付,2-已发货,3-已完成,4-已取消,5-已退款',
    `receiver_name` VARCHAR(50) NOT NULL COMMENT '收货人',
    `receiver_phone` VARCHAR(20) NOT NULL COMMENT '收货电话',
    `receiver_address` VARCHAR(300) NOT NULL COMMENT '收货地址',
    `payment_time` DATETIME DEFAULT NULL COMMENT '支付时间',
    `delivery_time` DATETIME DEFAULT NULL COMMENT '发货时间',
    `receive_time` DATETIME DEFAULT NULL COMMENT '收货时间',
    `expire_time` DATETIME NOT NULL COMMENT '过期时间(未支付自动取消)',
    `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
    `updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    PRIMARY KEY (`id`),
    UNIQUE KEY `uk_order_no` (`order_no`),
    KEY `idx_user_id` (`user_id`),
    KEY `idx_status` (`status`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订单表';

-- 订单明细表
CREATE TABLE `order_item` (
    `id` BIGINT NOT NULL AUTO_INCREMENT,
    `order_id` BIGINT NOT NULL COMMENT '订单ID',
    `order_no` VARCHAR(32) NOT NULL COMMENT '订单编号',
    `product_id` BIGINT NOT NULL COMMENT '商品ID',
    `sku_id` BIGINT NOT NULL COMMENT 'SKU ID',
    `product_name` VARCHAR(200) NOT NULL COMMENT '商品名称',
    `sku_name` VARCHAR(200) NOT NULL COMMENT 'SKU名称',
    `product_image` VARCHAR(255) DEFAULT NULL COMMENT '商品图片',
    `price` DECIMAL(10,2) NOT NULL COMMENT '单价',
    `quantity` INT NOT NULL COMMENT '数量',
    `total_amount` DECIMAL(10,2) NOT NULL COMMENT '小计金额',
    `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
    PRIMARY KEY (`id`),
    KEY `idx_order_id` (`order_id`),
    KEY `idx_order_no` (`order_no`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订单明细表';

支付服务数据库:

sql
-- 支付记录表
CREATE TABLE `payment_record` (
    `id` BIGINT NOT NULL AUTO_INCREMENT,
    `payment_no` VARCHAR(32) NOT NULL COMMENT '支付流水号',
    `order_no` VARCHAR(32) NOT NULL COMMENT '订单编号',
    `user_id` BIGINT NOT NULL COMMENT '用户ID',
    `amount` DECIMAL(10,2) NOT NULL COMMENT '支付金额',
    `pay_channel` VARCHAR(20) NOT NULL COMMENT '支付渠道:ALIPAY/WECHAT',
    `status` TINYINT NOT NULL DEFAULT 0 COMMENT '状态:0-待支付,1-支付成功,2-支付失败',
    `transaction_id` VARCHAR(64) DEFAULT NULL COMMENT '第三方交易号',
    `pay_time` DATETIME DEFAULT NULL COMMENT '支付时间',
    `callback_content` TEXT DEFAULT NULL COMMENT '回调内容',
    `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
    `updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    PRIMARY KEY (`id`),
    UNIQUE KEY `uk_payment_no` (`payment_no`),
    KEY `idx_order_no` (`order_no`),
    KEY `idx_user_id` (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='支付记录表';

核心代码示例 ​

父POM pom.xml:

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>

    <groupId>com.example</groupId>
    <artifactId>ecommerce-system</artifactId>
    <version>1.0.0</version>
    <packaging>pom</packaging>

    <modules>
        <module>ecommerce-common</module>
        <module>ecommerce-gateway</module>
        <module>ecommerce-user</module>
        <module>ecommerce-product</module>
        <module>ecommerce-order</module>
        <module>ecommerce-payment</module>
    </modules>

    <properties>
        <java.version>17</java.version>
        <spring-boot.version>3.2.0</spring-boot.version>
        <spring-cloud.version>2023.0.0</spring-cloud.version>
        <spring-cloud-alibaba.version>2023.0.0.0</spring-cloud-alibaba.version>
        <mybatis-plus.version>3.5.5</mybatis-plus.version>
        <seata.version>2.0.0</seata.version>
    </properties>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-dependencies</artifactId>
                <version>${spring-boot.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>${spring-cloud.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
            <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>
        </dependencies>
    </dependencyManagement>
</project>

雪花算法ID生成器 SnowflakeIdGenerator.java:

java
public class SnowflakeIdGenerator {

    private final long epoch = 1704067200000L; // 2024-01-01 00:00:00
    private final long workerIdBits = 5L;
    private final long datacenterIdBits = 5L;
    private final long sequenceBits = 12L;

    private final long maxWorkerId = ~(-1L << workerIdBits);
    private final long maxDatacenterId = ~(-1L << datacenterIdBits);
    private final long sequenceMask = ~(-1L << sequenceBits);

    private final long workerIdShift = sequenceBits;
    private final long datacenterIdShift = sequenceBits + workerIdBits;
    private final long timestampShift = sequenceBits + workerIdBits + datacenterIdBits;

    private long workerId;
    private long datacenterId;
    private long sequence = 0L;
    private long lastTimestamp = -1L;

    public SnowflakeIdGenerator(long workerId, long datacenterId) {
        if (workerId > maxWorkerId || workerId < 0) {
            throw new IllegalArgumentException("workerId不合法");
        }
        if (datacenterId > maxDatacenterId || datacenterId < 0) {
            throw new IllegalArgumentException("datacenterId不合法");
        }
        this.workerId = workerId;
        this.datacenterId = datacenterId;
    }

    public synchronized long nextId() {
        long timestamp = System.currentTimeMillis();

        if (timestamp < lastTimestamp) {
            throw new RuntimeException("时钟回拨,拒绝生成ID");
        }

        if (lastTimestamp == timestamp) {
            sequence = (sequence + 1) & sequenceMask;
            if (sequence == 0) {
                timestamp = tilNextMillis(lastTimestamp);
            }
        } else {
            sequence = 0L;
        }

        lastTimestamp = timestamp;

        return ((timestamp - epoch) << timestampShift)
                | (datacenterId << datacenterIdShift)
                | (workerId << workerIdShift)
                | sequence;
    }

    private long tilNextMillis(long lastTimestamp) {
        long timestamp = System.currentTimeMillis();
        while (timestamp <= lastTimestamp) {
            timestamp = System.currentTimeMillis();
        }
        return timestamp;
    }
}

订单服务核心逻辑 OrderServiceImpl.java:

java
@Service
@Slf4j
public class OrderServiceImpl extends ServiceImpl<OrderMapper, Order>
        implements OrderService {

    @Autowired
    private OrderItemMapper orderItemMapper;
    @Autowired
    private ProductFeignClient productFeignClient;
    @Autowired
    private UserFeignClient userFeignClient;
    @Autowired
    private RabbitTemplate rabbitTemplate;
    @Autowired
    private StringRedisTemplate redisTemplate;

    private final SnowflakeIdGenerator idGenerator = new SnowflakeIdGenerator(1, 1);

    /**
     * 创建订单(核心流程)
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public String createOrder(CreateOrderDTO dto, Long userId) {
        log.info("开始创建订单,用户ID:{}", userId);

        // 1. 获取收货地址
        UserAddressDTO address = userFeignClient.getAddress(dto.getAddressId());
        if (address == null) {
            throw new BusinessException("收货地址不存在");
        }

        // 2. 获取商品信息并计算总价
        BigDecimal totalAmount = BigDecimal.ZERO;
        List<OrderItem> orderItems = new ArrayList<>();

        for (OrderItemDTO itemDTO : dto.getItems()) {
            // 查询SKU信息
            SkuDTO sku = productFeignClient.getSku(itemDTO.getSkuId());
            if (sku == null) {
                throw new BusinessException("商品SKU不存在:" + itemDTO.getSkuId());
            }

            // 检查库存
            if (sku.getStock() - sku.getLockedStock() < itemDTO.getQuantity()) {
                throw new BusinessException("商品库存不足:" + sku.getSkuName());
            }

            // 构建订单明细
            OrderItem item = new OrderItem();
            item.setProductId(sku.getProductId());
            item.setSkuId(sku.getId());
            item.setProductName(sku.getProductName());
            item.setSkuName(sku.getSkuName());
            item.setProductImage(sku.getImage());
            item.setPrice(sku.getPrice());
            item.setQuantity(itemDTO.getQuantity());
            item.setTotalAmount(sku.getPrice().multiply(
                new BigDecimal(itemDTO.getQuantity())));
            orderItems.add(item);

            totalAmount = totalAmount.add(item.getTotalAmount());
        }

        // 3. 生成订单编号
        String orderNo = generateOrderNo(userId);

        // 4. 创建订单
        Order order = new Order();
        order.setId(idGenerator.nextId());
        order.setOrderNo(orderNo);
        order.setUserId(userId);
        order.setTotalAmount(totalAmount);
        order.setPayAmount(totalAmount); // 暂不计算优惠
        order.setFreightAmount(BigDecimal.ZERO);
        order.setDiscountAmount(BigDecimal.ZERO);
        order.setStatus(OrderConstants.STATUS_PENDING);
        order.setReceiverName(address.getReceiverName());
        order.setReceiverPhone(address.getReceiverPhone());
        order.setReceiverAddress(
            address.getProvince() + address.getCity() +
            address.getDistrict() + address.getDetailAddress());
        order.setExpireTime(LocalDateTime.now().plusMinutes(30)); // 30分钟过期

        this.save(order);

        // 5. 保存订单明细
        for (OrderItem item : orderItems) {
            item.setOrderId(order.getId());
            item.setOrderNo(orderNo);
            orderItemMapper.insert(item);
        }

        // 6. 锁定库存(调用商品服务)
        List<StockDeductionDTO> stockList = dto.getItems().stream()
                .map(item -> new StockDeductionDTO(item.getSkuId(), item.getQuantity()))
                .collect(Collectors.toList());
        productFeignClient.lockStock(stockList);

        // 7. 发送延迟消息(30分钟后自动取消未支付订单)
        rabbitTemplate.convertAndSend(
            "order.exchange", "order.delay",
            orderNo,
            message -> {
                message.getMessageProperties().setDelay(30 * 60 * 1000);
                return message;
            }
        );

        // 8. 清除购物车(异步)
        List<Long> skuIds = dto.getItems().stream()
                .map(OrderItemDTO::getSkuId)
                .collect(Collectors.toList());
        rabbitTemplate.convertAndSend("cart.exchange", "cart.clear",
            new CartClearMessage(userId, skuIds));

        log.info("订单创建成功,订单号:{}", orderNo);
        return orderNo;
    }

    /**
     * 支付订单
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void payOrder(String orderNo, String payChannel) {
        Order order = this.getOne(
            new LambdaQueryWrapper<Order>().eq(Order::getOrderNo, orderNo));

        if (order == null) {
            throw new BusinessException("订单不存在");
        }
        if (order.getStatus() != OrderConstants.STATUS_PENDING) {
            throw new BusinessException("订单状态异常");
        }

        // 调用支付服务(通过Feign)
        // paymentFeignClient.pay(orderNo, payChannel, order.getPayAmount());

        // 更新订单状态
        order.setStatus(OrderConstants.STATUS_PAID);
        order.setPaymentTime(LocalDateTime.now());
        this.updateById(order);

        // 扣减库存(确认)
        productFeignClient.confirmStock(orderNo);

        // 发送订单已支付消息
        rabbitTemplate.convertAndSend("order.exchange", "order.paid", orderNo);
    }

    /**
     * 取消订单
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void cancelOrder(String orderNo) {
        Order order = this.getOne(
            new LambdaQueryWrapper<Order>().eq(Order::getOrderNo, orderNo));

        if (order == null) {
            throw new BusinessException("订单不存在");
        }
        if (order.getStatus() != OrderConstants.STATUS_PENDING) {
            throw new BusinessException("只能取消待支付订单");
        }

        // 更新订单状态
        order.setStatus(OrderConstants.STATUS_CANCELLED);
        this.updateById(order);

        // 释放库存
        productFeignClient.unlockStock(orderNo);

        log.info("订单已取消,订单号:{}", orderNo);
    }

    /**
     * 生成订单编号:时间戳 + 用户ID后4位 + 随机数
     */
    private String generateOrderNo(Long userId) {
        String timestamp = String.valueOf(System.currentTimeMillis());
        String userSuffix = String.format("%04d", userId % 10000);
        String random = String.format("%04d", new Random().nextInt(10000));
        return timestamp + userSuffix + random;
    }
}

Feign客户端 ProductFeignClient.java:

java
@FeignClient(name = "ecommerce-product", fallbackFactory = ProductFeignFallback.class)
public interface ProductFeignClient {

    @GetMapping("/api/skus/{id}")
    SkuDTO getSku(@PathVariable("id") Long id);

    @PostMapping("/api/stocks/lock")
    Result<Void> lockStock(@RequestBody List<StockDeductionDTO> stockList);

    @PostMapping("/api/stocks/confirm/{orderNo}")
    Result<Void> confirmStock(@PathVariable("orderNo") String orderNo);

    @PostMapping("/api/stocks/unlock/{orderNo}")
    Result<Void> unlockStock(@PathVariable("orderNo") String orderNo);
}

Sentinel熔断降级配置:

java
@Component
public class ProductFeignFallback implements FallbackFactory<ProductFeignClient> {

    @Override
    public ProductFeignClient create(Throwable cause) {
        return new ProductFeignClient() {
            @Override
            public SkuDTO getSku(Long id) {
                log.error("商品服务调用失败,降级处理:{}", cause.getMessage());
                return null;
            }

            @Override
            public Result<Void> lockStock(List<StockDeductionDTO> stockList) {
                log.error("锁定库存失败,降级处理");
                return Result.error("系统繁忙,请稍后重试");
            }

            @Override
            public Result<Void> confirmStock(String orderNo) {
                log.error("确认库存失败,降级处理");
                return Result.error("系统繁忙,请稍后重试");
            }

            @Override
            public Result<Void> unlockStock(String orderNo) {
                log.error("释放库存失败,降级处理");
                return Result.error("系统繁忙,请稍后重试");
            }
        };
    }
}

网关全局过滤器 AuthGlobalFilter.java:

java
@Component
public class AuthGlobalFilter implements GlobalFilter, Ordered {

    private static final List<String> WHITE_LIST = List.of(
        "/api/user/login",
        "/api/user/register",
        "/api/product/list",
        "/api/product/detail"
    );

    @Autowired
    private JwtUtil jwtUtil;

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        ServerHttpRequest request = exchange.getRequest();
        String path = request.getURI().getPath();

        // 白名单直接放行
        if (WHITE_LIST.stream().anyMatch(path::startsWith)) {
            return chain.filter(exchange);
        }

        // 获取Token
        String token = request.getHeaders().getFirst("Authorization");
        if (!StringUtils.hasText(token) || !token.startsWith("Bearer ")) {
            return unauthorized(exchange);
        }

        token = token.substring(7);

        // 验证Token
        if (!jwtUtil.validateToken(token)) {
            return unauthorized(exchange);
        }

        // 将用户信息传递到下游服务
        Long userId = jwtUtil.getUserId(token);
        ServerHttpRequest mutatedRequest = request.mutate()
                .header("X-User-Id", userId.toString())
                .build();

        return chain.filter(exchange.mutate().request(mutatedRequest).build());
    }

    @Override
    public int getOrder() {
        return -1;
    }

    private Mono<Void> unauthorized(ServerWebExchange exchange) {
        ServerHttpResponse response = exchange.getResponse();
        response.setStatusCode(HttpStatus.UNAUTHORIZED);
        response.getHeaders().setContentType(MediaType.APPLICATION_JSON);
        String body = "{\"code\":401,\"message\":\"未登录或Token已过期\"}";
        DataBuffer buffer = response.bufferFactory()
                .wrap(body.getBytes(StandardCharsets.UTF_8));
        return response.writeWith(Mono.just(buffer));
    }
}

支付策略模式:

java
// 支付策略接口
public interface PaymentStrategy {
    String getChannel();
    PaymentResult pay(String orderNo, BigDecimal amount);
    boolean handleCallback(String callbackData);
}

// 支付宝支付策略
@Component
public class AlipayStrategy implements PaymentStrategy {

    @Override
    public String getChannel() {
        return "ALIPAY";
    }

    @Override
    public PaymentResult pay(String orderNo, BigDecimal amount) {
        // 调用支付宝SDK
        // AlipayClient alipayClient = ...
        // AlipayTradePagePayRequest request = new AlipayTradePagePayRequest();
        // ...
        return new PaymentResult(true, "ALIPAY_" + orderNo);
    }

    @Override
    public boolean handleCallback(String callbackData) {
        // 验证支付宝回调签名
        // 更新支付状态
        return true;
    }
}

// 微信支付策略
@Component
public class WechatPayStrategy implements PaymentStrategy {

    @Override
    public String getChannel() {
        return "WECHAT";
    }

    @Override
    public PaymentResult pay(String orderNo, BigDecimal amount) {
        // 调用微信支付SDK
        return new PaymentResult(true, "WECHAT_" + orderNo);
    }

    @Override
    public boolean handleCallback(String callbackData) {
        // 验证微信回调签名
        return true;
    }
}

// 支付上下文(策略工厂)
@Component
public class PaymentContext {

    @Autowired
    private List<PaymentStrategy> strategies;

    private Map<String, PaymentStrategy> strategyMap;

    @PostConstruct
    public void init() {
        strategyMap = strategies.stream()
                .collect(Collectors.toMap(PaymentStrategy::getChannel, s -> s));
    }

    public PaymentStrategy getStrategy(String channel) {
        PaymentStrategy strategy = strategyMap.get(channel);
        if (strategy == null) {
            throw new BusinessException("不支持的支付渠道:" + channel);
        }
        return strategy;
    }
}

网关配置 ​

yaml
server:
  port: 8080

spring:
  application:
    name: ecommerce-gateway
  cloud:
    nacos:
      discovery:
        server-addr: localhost:8848
    gateway:
      routes:
        - id: user-service
          uri: lb://ecommerce-user
          predicates:
            - Path=/api/user/**
        - id: product-service
          uri: lb://ecommerce-product
          predicates:
            - Path=/api/product/**
        - id: order-service
          uri: lb://ecommerce-order
          predicates:
            - Path=/api/order/**
        - id: payment-service
          uri: lb://ecommerce-payment
          predicates:
            - Path=/api/payment/**
      default-filters:
        - DedupeResponseHeader=Access-Control-Allow-Origin

接口设计 ​

服务方法路径说明
用户POST/api/user/register注册
用户POST/api/user/login登录
用户GET/api/user/info用户信息
用户GET/api/user/addresses地址列表
商品GET/api/product/list商品列表
商品GET/api/product/detail/商品详情
商品GET/api/product/categories分类列表
商品POST/api/stocks/lock锁定库存
订单POST/api/order/create创建订单
订单GET/api/order/list订单列表
订单GET/api/order/detail/订单详情
订单POST/api/order/pay/支付订单
订单POST/api/order/cancel/取消订单
支付POST/api/payment/pay发起支付
支付POST/api/payment/callback/支付回调

开发步骤 ​

  1. 基础设施搭建(第1-3天)

    • 搭建Nacos注册中心与配置中心
    • 创建父工程与公共模块
    • 搭建Gateway网关
    • 配置各微服务基础框架
  2. 用户服务(第4-6天)

    • 用户注册/登录
    • JWT认证
    • 收货地址管理
  3. 商品服务(第7-10天)

    • 商品分类管理
    • 商品SPU/SKU管理
    • 库存管理(预扣/确认/释放)
    • 商品搜索
  4. 订单服务(第11-16天)

    • 创建订单流程
    • Feign远程调用
    • RabbitMQ延迟队列(订单超时取消)
    • 分布式事务处理
  5. 支付服务(第17-20天)

    • 支付策略模式
    • 支付回调处理
    • 退款流程
  6. 联调与优化(第21-28天)

    • 服务间联调
    • Sentinel限流熔断
    • 性能测试
    • Docker容器化部署

7.1.3 即时通讯系统 ​

项目概述 ​

基于WebSocket和Netty的高性能即时通讯系统,支持单聊、群聊、在线状态管理、消息历史记录、离线消息推送等功能。

技术选型 ​

层次技术说明
网络框架Netty高性能NIO框架
协议WebSocket全双工通信
序列化Protobuf / JSON消息序列化
数据库MySQL消息持久化
缓存Redis在线状态、未读计数
消息队列RabbitMQ消息异步处理

项目目录结构 ​

im-system/
├── pom.xml
├── src/main/java/com/example/im/
│   ├── ImApplication.java
│   ├── config/
│   │   ├── NettyConfig.java
│   │   └── WebSocketConfig.java
│   ├── netty/
│   │   ├── NettyServer.java                    # Netty服务端
│   │   ├── NettyServerInitializer.java
│   │   ├── WebSocketChannelHandler.java
│   │   ├── HeartbeatHandler.java
│   │   └── MessageHandler.java
│   ├── protocol/
│   │   ├── MessageProtocol.java                # 消息协议
│   │   ├── MessageType.java
│   │   └── MessageCodec.java                   # 编解码器
│   ├── entity/
│   │   ├── User.java
│   │   ├── Message.java
│   │   ├── Group.java
│   │   └── GroupMember.java
│   ├── mapper/
│   │   ├── UserMapper.java
│   │   ├── MessageMapper.java
│   │   ├── GroupMapper.java
│   │   └── GroupMemberMapper.java
│   ├── service/
│   │   ├── ChatService.java
│   │   ├── GroupService.java
│   │   ├── MessageService.java
│   │   └── OnlineService.java
│   ├── service/impl/
│   │   ├── ChatServiceImpl.java
│   │   ├── GroupServiceImpl.java
│   │   ├── MessageServiceImpl.java
│   │   └── OnlineServiceImpl.java
│   ├── controller/
│   │   ├── ChatController.java
│   │   ├── GroupController.java
│   │   └── MessageController.java
│   ├── session/
│   │   ├── UserSession.java                    # 用户会话管理
│   │   └── SessionManager.java
│   └── handler/
│       ├── SingleChatHandler.java              # 单聊处理
│       ├── GroupChatHandler.java               # 群聊处理
│       └── SystemHandler.java                  # 系统消息处理
└── src/main/resources/
    └── application.yml

数据库设计 ​

sql
-- 用户表
CREATE TABLE `im_user` (
    `id` BIGINT NOT NULL AUTO_INCREMENT,
    `username` VARCHAR(50) NOT NULL COMMENT '用户名',
    `password` VARCHAR(100) NOT NULL COMMENT '密码',
    `nickname` VARCHAR(50) NOT NULL COMMENT '昵称',
    `avatar` VARCHAR(255) DEFAULT NULL COMMENT '头像',
    `online_status` TINYINT NOT NULL DEFAULT 0 COMMENT '在线状态:0-离线,1-在线',
    `last_login_time` DATETIME DEFAULT NULL COMMENT '最后登录时间',
    `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
    PRIMARY KEY (`id`),
    UNIQUE KEY `uk_username` (`username`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表';

-- 消息表
CREATE TABLE `im_message` (
    `id` BIGINT NOT NULL COMMENT '消息ID',
    `from_user_id` BIGINT NOT NULL COMMENT '发送者ID',
    `to_id` BIGINT NOT NULL COMMENT '接收者(用户ID或群ID)',
    `msg_type` TINYINT NOT NULL COMMENT '消息类型:1-单聊,2-群聊,3-系统',
    `content_type` VARCHAR(20) NOT NULL COMMENT '内容类型:TEXT/IMAGE/FILE/VOICE',
    `content` TEXT NOT NULL COMMENT '消息内容',
    `status` TINYINT NOT NULL DEFAULT 0 COMMENT '状态:0-发送中,1-已送达,2-已读',
    `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
    PRIMARY KEY (`id`),
    KEY `idx_from_user` (`from_user_id`),
    KEY `idx_to_id` (`to_id`, `msg_type`),
    KEY `idx_created_at` (`created_at`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='消息表';

-- 群组表
CREATE TABLE `im_group` (
    `id` BIGINT NOT NULL AUTO_INCREMENT,
    `name` VARCHAR(50) NOT NULL COMMENT '群名称',
    `avatar` VARCHAR(255) DEFAULT NULL COMMENT '群头像',
    `owner_id` BIGINT NOT NULL COMMENT '群主ID',
    `description` VARCHAR(200) DEFAULT NULL COMMENT '群描述',
    `max_members` INT NOT NULL DEFAULT 200 COMMENT '最大成员数',
    `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
    PRIMARY KEY (`id`),
    KEY `idx_owner_id` (`owner_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='群组表';

-- 群成员表
CREATE TABLE `im_group_member` (
    `id` BIGINT NOT NULL AUTO_INCREMENT,
    `group_id` BIGINT NOT NULL COMMENT '群ID',
    `user_id` BIGINT NOT NULL COMMENT '用户ID',
    `nickname` VARCHAR(50) DEFAULT NULL COMMENT '群内昵称',
    `role` TINYINT NOT NULL DEFAULT 0 COMMENT '角色:0-成员,1-管理员,2-群主',
    `muted` TINYINT NOT NULL DEFAULT 0 COMMENT '是否禁言',
    `joined_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
    PRIMARY KEY (`id`),
    UNIQUE KEY `uk_group_user` (`group_id`, `user_id`),
    KEY `idx_user_id` (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='群成员表';

-- 用户关系表(好友)
CREATE TABLE `im_friendship` (
    `id` BIGINT NOT NULL AUTO_INCREMENT,
    `user_id` BIGINT NOT NULL COMMENT '用户ID',
    `friend_id` BIGINT NOT NULL COMMENT '好友ID',
    `remark` VARCHAR(50) DEFAULT NULL COMMENT '备注名',
    `status` TINYINT NOT NULL DEFAULT 0 COMMENT '状态:0-待确认,1-已同意',
    `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
    PRIMARY KEY (`id`),
    UNIQUE KEY `uk_user_friend` (`user_id`, `friend_id`),
    KEY `idx_friend_id` (`friend_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='好友关系表';

核心代码示例 ​

Netty服务端 NettyServer.java:

java
@Component
@Slf4j
public class NettyServer {

    @Value("${netty.port:8888}")
    private int port;

    @Autowired
    private WebSocketChannelHandler channelHandler;

    private EventLoopGroup bossGroup;
    private EventLoopGroup workerGroup;

    @PostConstruct
    public void start() {
        new Thread(this::init).start();
    }

    private void init() {
        bossGroup = new NioEventLoopGroup(1);
        workerGroup = new NioEventLoopGroup();

        try {
            ServerBootstrap bootstrap = new ServerBootstrap();
            bootstrap.group(bossGroup, workerGroup)
                    .channel(NioServerSocketChannel.class)
                    .option(ChannelOption.SO_BACKLOG, 1024)
                    .option(ChannelOption.SO_KEEPALIVE, true)
                    .childOption(ChannelOption.TCP_NODELAY, true)
                    .childHandler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        protected void initChannel(SocketChannel ch) {
                            ChannelPipeline pipeline = ch.pipeline();
                            // HTTP编解码器(WebSocket基于HTTP升级)
                            pipeline.addLast(new HttpServerCodec());
                            pipeline.addLast(new ChunkedWriteHandler());
                            // HTTP聚合器(WebSocket握手需要)
                            pipeline.addLast(new HttpObjectAggregator(65536));
                            // WebSocket协议处理器
                            pipeline.addLast(new WebSocketServerProtocolHandler("/ws"));
                            // 心跳检测
                            pipeline.addLast(new IdleStateHandler(60, 30, 0));
                            pipeline.addLast(new HeartbeatHandler());
                            // 消息处理
                            pipeline.addLast(channelHandler);
                        }
                    });

            ChannelFuture future = bootstrap.bind(port).sync();
            log.info("Netty WebSocket服务器启动,端口:{}", port);
            future.channel().closeFuture().sync();

        } catch (InterruptedException e) {
            log.error("Netty服务器异常", e);
            Thread.currentThread().interrupt();
        } finally {
            shutdown();
        }
    }

    @PreDestroy
    public void shutdown() {
        if (bossGroup != null) bossGroup.shutdownGracefully();
        if (workerGroup != null) workerGroup.shutdownGracefully();
        log.info("Netty服务器已关闭");
    }
}

消息协议定义 MessageProtocol.java:

java
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class MessageProtocol {
    /** 消息类型 */
    private Integer type;
    /** 消息ID */
    private String msgId;
    /** 发送者ID */
    private Long fromUserId;
    /** 接收者ID */
    private Long toId;
    /** 内容类型 */
    private String contentType;
    /** 消息内容 */
    private String content;
    /** 时间戳 */
    private Long timestamp;
    /** 扩展字段(JSON) */
    private String extra;
}

public class MessageType {
    public static final int HEARTBEAT = 0;          // 心跳
    public static final int SINGLE_CHAT = 1;        // 单聊
    public static final int GROUP_CHAT = 2;         // 群聊
    public static final int SYSTEM = 3;             // 系统消息
    public static final int ONLINE_STATUS = 4;      // 在线状态变更
    public static final int MESSAGE_ACK = 5;        // 消息确认
    public static final int MESSAGE_READ = 6;       // 消息已读
    public static final int TYPING = 7;             // 正在输入
}

WebSocket消息处理器 WebSocketChannelHandler.java:

java
@Component
@ChannelHandler.Sharable
@Slf4j
public class WebSocketChannelHandler extends SimpleChannelInboundHandler<TextWebSocketFrame> {

    @Autowired
    private SessionManager sessionManager;
    @Autowired
    private SingleChatHandler singleChatHandler;
    @Autowired
    private GroupChatHandler groupChatHandler;
    @Autowired
    private ObjectMapper objectMapper;

    @Override
    public void channelActive(ChannelHandlerContext ctx) {
        log.info("客户端连接:{}", ctx.channel().remoteAddress());
    }

    @Override
    protected void channelRead0(ChannelHandlerContext ctx, TextWebSocketFrame msg)
            throws Exception {
        String text = msg.text();
        log.debug("收到消息:{}", text);

        MessageProtocol protocol = objectMapper.readValue(text, MessageProtocol.class);

        switch (protocol.getType()) {
            case MessageType.HEARTBEAT:
                handleHeartbeat(ctx);
                break;
            case MessageType.SINGLE_CHAT:
                singleChatHandler.handle(ctx, protocol);
                break;
            case MessageType.GROUP_CHAT:
                groupChatHandler.handle(ctx, protocol);
                break;
            case MessageType.MESSAGE_ACK:
                handleMessageAck(ctx, protocol);
                break;
            case MessageType.MESSAGE_READ:
                handleMessageRead(ctx, protocol);
                break;
            default:
                log.warn("未知消息类型:{}", protocol.getType());
        }
    }

    @Override
    public void channelInactive(ChannelHandlerContext ctx) {
        // 用户断开连接
        Long userId = sessionManager.getUserId(ctx.channel());
        if (userId != null) {
            sessionManager.removeSession(userId);
            log.info("用户断开连接,用户ID:{}", userId);
        }
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
        log.error("Channel异常:{}", cause.getMessage());
        ctx.close();
    }

    private void handleHeartbeat(ChannelHandlerContext ctx) {
        MessageProtocol pong = MessageProtocol.builder()
                .type(MessageType.HEARTBEAT)
                .timestamp(System.currentTimeMillis())
                .build();
        sendMessage(ctx, pong);
    }

    private void handleMessageAck(ChannelHandlerContext ctx, MessageProtocol protocol) {
        // 更新消息状态为已送达
    }

    private void handleMessageRead(ChannelHandlerContext ctx, MessageProtocol protocol) {
        // 更新消息状态为已读
    }

    public void sendMessage(ChannelHandlerContext ctx, MessageProtocol protocol) {
        try {
            String json = objectMapper.writeValueAsString(protocol);
            ctx.writeAndFlush(new TextWebSocketFrame(json));
        } catch (JsonProcessingException e) {
            log.error("消息序列化失败", e);
        }
    }
}

会话管理 SessionManager.java:

java
@Component
@Slf4j
public class SessionManager {

    /** 用户ID -> Channel映射 */
    private final ConcurrentHashMap<Long, Channel> userChannelMap = new ConcurrentHashMap<>();

    /** Channel -> 用户ID映射 */
    private final ConcurrentHashMap<Channel, Long> channelUserMap = new ConcurrentHashMap<>();

    /**
     * 注册会话
     */
    public void addSession(Long userId, Channel channel) {
        // 如果用户已在其他设备登录,关闭旧连接
        Channel oldChannel = userChannelMap.get(userId);
        if (oldChannel != null && oldChannel.isActive()) {
            oldChannel.close();
        }

        userChannelMap.put(userId, channel);
        channelUserMap.put(channel, userId);
        log.info("用户上线,用户ID:{}", userId);
    }

    /**
     * 移除会话
     */
    public void removeSession(Long userId) {
        Channel channel = userChannelMap.remove(userId);
        if (channel != null) {
            channelUserMap.remove(channel);
        }
    }

    /**
     * 获取用户Channel
     */
    public Channel getChannel(Long userId) {
        return userChannelMap.get(userId);
    }

    /**
     * 获取Channel对应的用户ID
     */
    public Long getUserId(Channel channel) {
        return channelUserMap.get(channel);
    }

    /**
     * 判断用户是否在线
     */
    public boolean isOnline(Long userId) {
        Channel channel = userChannelMap.get(userId);
        return channel != null && channel.isActive();
    }

    /**
     * 获取在线用户数
     */
    public int getOnlineCount() {
        return userChannelMap.size();
    }

    /**
     * 获取所有在线用户ID
     */
    public Set<Long> getOnlineUsers() {
        return userChannelMap.keySet();
    }
}

单聊处理器 SingleChatHandler.java:

java
@Component
@Slf4j
public class SingleChatHandler {

    @Autowired
    private SessionManager sessionManager;
    @Autowired
    private MessageService messageService;
    @Autowired
    private OnlineService onlineService;
    @Autowired
    private ObjectMapper objectMapper;

    public void handle(ChannelHandlerContext ctx, MessageProtocol protocol) {
        Long fromUserId = protocol.getFromUserId();
        Long toUserId = protocol.getToId();

        // 1. 保存消息到数据库
        Message message = new Message();
        message.setId(Long.parseLong(protocol.getMsgId()));
        message.setFromUserId(fromUserId);
        message.setToId(toUserId);
        message.setMsgType(MessageType.SINGLE_CHAT);
        message.setContentType(protocol.getContentType());
        message.setContent(protocol.getContent());
        message.setStatus(0); // 发送中
        messageService.save(message);

        // 2. 构建推送消息
        MessageProtocol pushMsg = MessageProtocol.builder()
                .type(MessageType.SINGLE_CHAT)
                .msgId(protocol.getMsgId())
                .fromUserId(fromUserId)
                .toId(toUserId)
                .contentType(protocol.getContentType())
                .content(protocol.getContent())
                .timestamp(System.currentTimeMillis())
                .build();

        // 3. 判断接收者是否在线
        if (sessionManager.isOnline(toUserId)) {
            // 在线:直接推送
            Channel toChannel = sessionManager.getChannel(toUserId);
            sendToChannel(toChannel, pushMsg);

            // 更新消息状态为已送达
            message.setStatus(1);
            messageService.updateById(message);
        } else {
            // 离线:存入离线消息队列
            onlineService.saveOfflineMessage(toUserId, pushMsg);
        }

        // 4. 给发送者回复ACK
        MessageProtocol ackMsg = MessageProtocol.builder()
                .type(MessageType.MESSAGE_ACK)
                .msgId(protocol.getMsgId())
                .timestamp(System.currentTimeMillis())
                .build();
        sendToChannel(ctx.channel(), ackMsg);
    }

    private void sendToChannel(Channel channel, MessageProtocol protocol) {
        try {
            String json = objectMapper.writeValueAsString(protocol);
            channel.writeAndFlush(new TextWebSocketFrame(json));
        } catch (JsonProcessingException e) {
            log.error("消息序列化失败", e);
        }
    }
}

群聊处理器 GroupChatHandler.java:

java
@Component
@Slf4j
public class GroupChatHandler {

    @Autowired
    private SessionManager sessionManager;
    @Autowired
    private GroupService groupService;
    @Autowired
    private MessageService messageService;
    @Autowired
    private ObjectMapper objectMapper;

    public void handle(ChannelHandlerContext ctx, MessageProtocol protocol) {
        Long fromUserId = protocol.getFromUserId();
        Long groupId = protocol.getToId();

        // 1. 验证用户是否是群成员
        if (!groupService.isMember(groupId, fromUserId)) {
            log.warn("用户不是群成员,用户ID:{},群ID:{}", fromUserId, groupId);
            return;
        }

        // 2. 保存消息
        Message message = new Message();
        message.setId(Long.parseLong(protocol.getMsgId()));
        message.setFromUserId(fromUserId);
        message.setToId(groupId);
        message.setMsgType(MessageType.GROUP_CHAT);
        message.setContentType(protocol.getContentType());
        message.setContent(protocol.getContent());
        messageService.save(message);

        // 3. 获取群成员列表
        List<Long> memberIds = groupService.getMemberIds(groupId);

        // 4. 构建推送消息
        MessageProtocol pushMsg = MessageProtocol.builder()
                .type(MessageType.GROUP_CHAT)
                .msgId(protocol.getMsgId())
                .fromUserId(fromUserId)
                .toId(groupId)
                .contentType(protocol.getContentType())
                .content(protocol.getContent())
                .timestamp(System.currentTimeMillis())
                .build();

        // 5. 遍历群成员推送消息
        for (Long memberId : memberIds) {
            if (memberId.equals(fromUserId)) {
                continue; // 不给自己发
            }

            if (sessionManager.isOnline(memberId)) {
                Channel channel = sessionManager.getChannel(memberId);
                sendToChannel(channel, pushMsg);
            } else {
                // 离线消息
                // 存入离线消息队列
            }
        }
    }

    private void sendToChannel(Channel channel, MessageProtocol protocol) {
        try {
            String json = objectMapper.writeValueAsString(protocol);
            channel.writeAndFlush(new TextWebSocketFrame(json));
        } catch (JsonProcessingException e) {
            log.error("消息序列化失败", e);
        }
    }
}

心跳处理器 HeartbeatHandler.java:

java
public class HeartbeatHandler extends ChannelInboundHandlerAdapter {

    @Override
    public void userEventTriggered(ChannelHandlerContext ctx, Object evt) {
        if (evt instanceof IdleStateEvent) {
            IdleStateEvent event = (IdleStateEvent) evt;
            if (event.state() == IdleState.READER_IDLE) {
                // 读超时:客户端未发送心跳
                log.warn("客户端心跳超时,关闭连接:{}", ctx.channel().remoteAddress());
                ctx.close();
            } else if (event.state() == IdleState.WRITER_IDLE) {
                // 写超时:主动发送心跳
                MessageProtocol heartbeat = MessageProtocol.builder()
                        .type(MessageType.HEARTBEAT)
                        .timestamp(System.currentTimeMillis())
                        .build();
                try {
                    String json = new ObjectMapper().writeValueAsString(heartbeat);
                    ctx.writeAndFlush(new TextWebSocketFrame(json));
                } catch (JsonProcessingException e) {
                    log.error("心跳消息序列化失败");
                }
            }
        }
    }
}

接口设计 ​

方法路径说明
WebSocketws://host:port/wsWebSocket连接
POST/api/auth/login登录获取Token
GET/api/friends好友列表
POST/api/friends/request添加好友
POST/api/groups创建群
GET/api/groups/{id}/members群成员列表
POST/api/groups/{id}/join加入群
GET/api/messages/history历史消息
GET/api/messages/unread/count未读消息数

开发步骤 ​

  1. 基础搭建(第1-3天)

    • Netty服务器搭建
    • WebSocket协议处理
    • 心跳机制
  2. 单聊功能(第4-6天)

    • 消息收发
    • 消息持久化
    • 在线/离线消息处理
  3. 群聊功能(第7-9天)

    • 群组管理
    • 群消息广播
    • 群成员管理
  4. 高级功能(第10-14天)

    • 消息已读回执
    • 离线消息推送
    • 消息搜索
    • 文件/图片传输

7.1.4 短链接服务 ​

项目概述 ​

高性能短链接生成与解析服务,类似bit.ly。支持短链接生成、跳转、统计分析、自定义短码等功能。

技术选型 ​

层次技术说明
框架Spring Boot核心框架
数据库MySQL持久化存储
缓存Redis热点数据缓存
分布式IDSnowflake / 号段模式ID生成
消息队列RabbitMQ异步统计

项目目录结构 ​

short-url-service/
├── pom.xml
├── src/main/java/com/example/shorturl/
│   ├── ShortUrlApplication.java
│   ├── config/
│   │   └── RedisConfig.java
│   ├── entity/
│   │   ├── ShortUrl.java
│   │   └── UrlAccessLog.java
│   ├── mapper/
│   │   ├── ShortUrlMapper.java
│   │   └── UrlAccessLogMapper.java
│   ├── service/
│   │   ├── ShortUrlService.java
│   │   └── StatisticsService.java
│   ├── service/impl/
│   │   ├── ShortUrlServiceImpl.java
│   │   └── StatisticsServiceImpl.java
│   ├── controller/
│   │   ├── ShortUrlController.java
│   │   └── RedirectController.java
│   ├── generator/
│   │   ├── ShortCodeGenerator.java           # 短码生成策略
│   │   ├── Base62Generator.java
│   │   └── PreGeneratedPoolGenerator.java    # 预生成池
│   └── filter/
│       └── RateLimitFilter.java              # 限流过滤器
└── src/main/resources/
    └── application.yml

数据库设计 ​

sql
-- 短链接映射表
CREATE TABLE `short_url` (
    `id` BIGINT NOT NULL AUTO_INCREMENT,
    `short_code` VARCHAR(10) NOT NULL COMMENT '短码',
    `original_url` VARCHAR(2000) NOT NULL COMMENT '原始URL',
    `user_id` BIGINT DEFAULT NULL COMMENT '创建用户ID',
    `expire_time` DATETIME DEFAULT NULL COMMENT '过期时间(NULL表示永不过期)',
    `access_count` BIGINT NOT NULL DEFAULT 0 COMMENT '访问次数',
    `status` TINYINT NOT NULL DEFAULT 1 COMMENT '状态:0-禁用,1-启用',
    `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
    `updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    PRIMARY KEY (`id`),
    UNIQUE KEY `uk_short_code` (`short_code`),
    KEY `idx_user_id` (`user_id`),
    KEY `idx_original_url` (`original_url`(100))
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='短链接映射表';

-- 访问日志表
CREATE TABLE `url_access_log` (
    `id` BIGINT NOT NULL AUTO_INCREMENT,
    `short_code` VARCHAR(10) NOT NULL COMMENT '短码',
    `access_ip` VARCHAR(50) DEFAULT NULL COMMENT '访问IP',
    `user_agent` VARCHAR(500) DEFAULT NULL COMMENT '浏览器UA',
    `referer` VARCHAR(500) DEFAULT NULL COMMENT '来源',
    `device_type` VARCHAR(20) DEFAULT NULL COMMENT '设备类型',
    `os_type` VARCHAR(20) DEFAULT NULL COMMENT '操作系统',
    `browser_type` VARCHAR(20) DEFAULT NULL COMMENT '浏览器类型',
    `country` VARCHAR(50) DEFAULT NULL COMMENT '国家',
    `province` VARCHAR(50) DEFAULT NULL COMMENT '省份',
    `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
    PRIMARY KEY (`id`),
    KEY `idx_short_code` (`short_code`),
    KEY `idx_created_at` (`created_at`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='访问日志表';

核心代码示例 ​

Base62短码生成器 Base62Generator.java:

java
@Component
public class Base62Generator implements ShortCodeGenerator {

    private static final String BASE62_CHARS =
        "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
    private static final int BASE = 62;
    private static final int CODE_LENGTH = 6; // 6位短码

    @Autowired
    private IdGenerator idGenerator;

    @Override
    public String generate() {
        long id = idGenerator.nextId();
        return encode(id);
    }

    /**
     * 将数字编码为Base62字符串
     */
    private String encode(long num) {
        StringBuilder sb = new StringBuilder();
        while (num > 0) {
            sb.insert(0, BASE62_CHARS.charAt((int) (num % BASE)));
            num /= BASE;
        }
        // 补齐到固定长度
        while (sb.length() < CODE_LENGTH) {
            sb.insert(0, '0');
        }
        return sb.toString();
    }

    /**
     * 将Base62字符串解码为数字
     */
    public long decode(String code) {
        long num = 0;
        for (int i = 0; i < code.length(); i++) {
            num = num * BASE + BASE62_CHARS.indexOf(code.charAt(i));
        }
        return num;
    }
}

预生成池方案 PreGeneratedPoolGenerator.java:

java
@Component
public class PreGeneratedPoolGenerator implements ShortCodeGenerator {

    @Autowired
    private StringRedisTemplate redisTemplate;

    private static final String POOL_KEY = "shorturl:code:pool";
    private static final int POOL_THRESHOLD = 1000; // 低于此值触发补充

    @PostConstruct
    public void init() {
        // 初始化时预生成一批短码
        if (redisTemplate.opsForList().size(POOL_KEY) == 0) {
            fillPool(10000);
        }
        // 定时检查并补充
        ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();
        scheduler.scheduleAtFixedRate(this::checkAndFill, 1, 1, TimeUnit.MINUTES);
    }

    @Override
    public String generate() {
        String code = redisTemplate.opsForList().rightPop(POOL_KEY);
        if (code == null) {
            // 池为空,同步生成
            fillPool(1000);
            code = redisTemplate.opsForList().rightPop(POOL_KEY);
        }
        return code;
    }

    private void checkAndFill() {
        Long size = redisTemplate.opsForList().size(POOL_KEY);
        if (size != null && size < POOL_THRESHOLD) {
            fillPool(5000);
        }
    }

    private synchronized void fillPool(int count) {
        List<String> codes = new ArrayList<>();
        for (int i = 0; i < count; i++) {
            codes.add(randomCode());
        }
        redisTemplate.opsForList().leftPushAll(POOL_KEY, codes);
    }

    private String randomCode() {
        String chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
        StringBuilder sb = new StringBuilder(6);
        Random random = new Random();
        for (int i = 0; i < 6; i++) {
            sb.append(chars.charAt(random.nextInt(chars.length())));
        }
        return sb.toString();
    }
}

短链接服务核心 ShortUrlServiceImpl.java:

java
@Service
@Slf4j
public class ShortUrlServiceImpl implements ShortUrlService {

    @Autowired
    private ShortUrlMapper shortUrlMapper;
    @Autowired
    private ShortCodeGenerator shortCodeGenerator;
    @Autowired
    private StringRedisTemplate redisTemplate;
    @Autowired
    private RabbitTemplate rabbitTemplate;

    private static final String CACHE_PREFIX = "shorturl:";
    private static final long CACHE_EXPIRE_HOURS = 24;

    /**
     * 创建短链接
     */
    @Override
    public ShortUrl createShortUrl(String originalUrl, Long userId, LocalDateTime expireTime) {
        // 1. 检查是否已存在相同URL的短链接
        String existCode = redisTemplate.opsForValue().get("shorturl:url:" + originalUrl);
        if (existCode != null) {
            return shortUrlMapper.selectByShortCode(existCode);
        }

        ShortUrl exist = shortUrlMapper.selectByOriginalUrl(originalUrl);
        if (exist != null) {
            return exist;
        }

        // 2. 生成短码(保证唯一)
        String shortCode;
        do {
            shortCode = shortCodeGenerator.generate();
        } while (shortUrlMapper.selectByShortCode(shortCode) != null);

        // 3. 保存短链接
        ShortUrl shortUrl = new ShortUrl();
        shortUrl.setShortCode(shortCode);
        shortUrl.setOriginalUrl(originalUrl);
        shortUrl.setUserId(userId);
        shortUrl.setExpireTime(expireTime);
        shortUrlMapper.insert(shortUrl);

        // 4. 写入缓存
        redisTemplate.opsForValue().set(
            CACHE_PREFIX + shortCode, originalUrl,
            CACHE_EXPIRE_HOURS, TimeUnit.HOURS);
        redisTemplate.opsForValue().set(
            "shorturl:url:" + originalUrl, shortCode,
            CACHE_EXPIRE_HOURS, TimeUnit.HOURS);

        log.info("短链接创建成功:{} -> {}", shortCode, originalUrl);
        return shortUrl;
    }

    /**
     * 解析短链接(重定向核心逻辑)
     */
    @Override
    public String resolveShortUrl(String shortCode, String clientIp, String userAgent) {
        // 1. 先查缓存
        String originalUrl = redisTemplate.opsForValue().get(CACHE_PREFIX + shortCode);

        if (originalUrl == null) {
            // 2. 缓存未命中,查数据库
            ShortUrl shortUrl = shortUrlMapper.selectByShortCode(shortCode);
            if (shortUrl == null) {
                return null;
            }

            // 检查是否过期
            if (shortUrl.getExpireTime() != null &&
                shortUrl.getExpireTime().isBefore(LocalDateTime.now())) {
                return null;
            }

            // 检查是否禁用
            if (shortUrl.getStatus() != 1) {
                return null;
            }

            originalUrl = shortUrl.getOriginalUrl();

            // 3. 写入缓存
            redisTemplate.opsForValue().set(
                CACHE_PREFIX + shortCode, originalUrl,
                CACHE_EXPIRE_HOURS, TimeUnit.HOURS);
        }

        // 4. 异步记录访问日志
        UrlAccessLog accessLog = new UrlAccessLog();
        accessLog.setShortCode(shortCode);
        accessLog.setAccessIp(clientIp);
        accessLog.setUserAgent(userAgent);
        accessLog.setCreatedAt(LocalDateTime.now());
        rabbitTemplate.convertAndSend("url.access", "access.log", accessLog);

        // 5. 异步更新访问次数
        redisTemplate.opsForValue().increment("shorturl:count:" + shortCode);

        return originalUrl;
    }

    /**
     * 获取短链接统计信息
     */
    @Override
    public UrlStatistics getStatistics(String shortCode) {
        UrlStatistics stats = new UrlStatistics();

        // 总访问次数
        String countStr = redisTemplate.opsForValue().get("shorturl:count:" + shortCode);
        long redisCount = countStr != null ? Long.parseLong(countStr) : 0;

        ShortUrl shortUrl = shortUrlMapper.selectByShortCode(shortCode);
        stats.setTotalAccess(shortUrl.getAccessCount() + redisCount);

        // 今日访问次数
        long todayCount = shortUrlMapper.countTodayAccess(shortCode);
        stats.setTodayAccess(todayCount);

        // 最近7天访问趋势
        List<DailyAccess> dailyAccesses = shortUrlMapper.countDailyAccess(shortCode, 7);
        stats.setDailyAccesses(dailyAccesses);

        return stats;
    }
}

重定向控制器 RedirectController.java:

java
@RestController
@Slf4j
public class RedirectController {

    @Autowired
    private ShortUrlService shortUrlService;

    /**
     * 短链接重定向(核心接口,需要高性能)
     */
    @GetMapping("/{shortCode}")
    public void redirect(@PathVariable String shortCode,
                         HttpServletRequest request,
                         HttpServletResponse response) throws IOException {
        String clientIp = getClientIp(request);
        String userAgent = request.getHeader("User-Agent");

        String originalUrl = shortUrlService.resolveShortUrl(shortCode, clientIp, userAgent);

        if (originalUrl == null) {
            response.sendError(404, "短链接不存在或已过期");
            return;
        }

        // 302临时重定向(每次都会访问短链接服务)
        // 如果需要减少短链接服务压力,可使用301永久重定向
        response.setStatus(302);
        response.setHeader("Location", originalUrl);
        response.setHeader("Cache-Control", "no-cache");
    }

    private String getClientIp(HttpServletRequest request) {
        String ip = request.getHeader("X-Forwarded-For");
        if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("X-Real-IP");
        }
        if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getRemoteAddr();
        }
        // 多个代理时取第一个
        if (ip != null && ip.contains(",")) {
            ip = ip.split(",")[0].trim();
        }
        return ip;
    }
}

限流过滤器 RateLimitFilter.java:

java
@Component
public class RateLimitFilter implements Filter {

    @Autowired
    private StringRedisTemplate redisTemplate;

    private static final int MAX_REQUESTS = 100; // 每分钟最大请求数
    private static final int WINDOW_SECONDS = 60;

    @Override
    public void doFilter(ServletRequest request, ServletResponse response,
                         FilterChain chain) throws IOException, ServletException {
        HttpServletRequest httpRequest = (HttpServletRequest) request;
        String clientIp = httpRequest.getRemoteAddr();
        String key = "ratelimit:" + clientIp;

        Long count = redisTemplate.opsForValue().increment(key);
        if (count == 1) {
            redisTemplate.expire(key, WINDOW_SECONDS, TimeUnit.SECONDS);
        }

        if (count > MAX_REQUESTS) {
            HttpServletResponse httpResponse = (HttpServletResponse) response;
            httpResponse.setStatus(429);
            httpResponse.setContentType("application/json;charset=UTF-8");
            httpResponse.getWriter().write(
                "{\"code\":429,\"message\":\"请求过于频繁,请稍后再试\"}");
            return;
        }

        chain.doFilter(request, response);
    }
}

接口设计 ​

方法路径说明认证
POST/api/shorturl创建短链接可选
GET/api/shorturl/获取短链接信息否
DELETE/api/shorturl/删除短链接是
PUT/api/shorturl/更新短链接是
GET/api/shorturl/{code}/stats获取统计信息是
GET/短链接重定向否

开发步骤 ​

  1. 基础功能(第1-2天)

    • 短码生成算法实现
    • 短链接CRUD
    • 重定向逻辑
  2. 性能优化(第3-4天)

    • Redis缓存层
    • 预生成短码池
    • 异步访问日志
  3. 高级功能(第5-7天)

    • 访问统计分析
    • 限流保护
    • 自定义短码
    • 批量生成

7.1.5 权限管理系统 ​

项目概述 ​

基于RBAC(基于角色的访问控制)模型的权限管理系统,支持用户管理、角色管理、菜单管理、部门管理、日志审计等功能。

技术选型 ​

层次技术说明
安全框架Spring Security安全认证与授权
认证方式JWT无状态认证
权限模型RBAC基于角色的访问控制
数据权限MyBatis拦截器SQL自动拼接

RBAC权限模型 ​

用户(User) ──N:M──> 角色(Role) ──N:M──> 权限/菜单(Permission)
     │                                    │
     └──────────── 部门(Dept) ────────────┘
  • 一个用户可以拥有多个角色
  • 一个角色可以分配给多个用户
  • 一个角色可以拥有多个权限
  • 一个权限可以属于多个角色

项目目录结构 ​

permission-system/
├── pom.xml
├── src/main/java/com/example/permission/
│   ├── PermissionApplication.java
│   ├── security/
│   │   ├── SecurityConfig.java              # Security配置
│   │   ├── JwtAuthenticationFilter.java     # JWT认证过滤器
│   │   ├── JwtAuthenticationEntryPoint.java # 未认证处理
│   │   ├── AccessDeniedHandlerImpl.java     # 无权限处理
│   │   ├── UserDetailsServiceImpl.java      # 用户详情服务
│   │   ├── LoginUser.java                   # 登录用户信息
│   │   └── annotation/
│   │       ├── RequiresPermission.java      # 权限注解
│   │       └── DataScope.java               # 数据权限注解
│   ├── entity/
│   │   ├── SysUser.java
│   │   ├── SysRole.java
│   │   ├── SysMenu.java
│   │   ├── SysDept.java
│   │   ├── SysUserRole.java
│   │   ├── SysRoleMenu.java
│   │   └── SysOperLog.java
│   ├── mapper/
│   │   ├── SysUserMapper.java
│   │   ├── SysRoleMapper.java
│   │   ├── SysMenuMapper.java
│   │   ├── SysDeptMapper.java
│   │   └── SysOperLogMapper.java
│   ├── service/
│   │   ├── SysUserService.java
│   │   ├── SysRoleService.java
│   │   ├── SysMenuService.java
│   │   ├── SysDeptService.java
│   │   └── SysOperLogService.java
│   ├── service/impl/
│   │   ├── SysUserServiceImpl.java
│   │   ├── SysRoleServiceImpl.java
│   │   ├── SysMenuServiceImpl.java
│   │   ├── SysDeptServiceImpl.java
│   │   └── SysOperLogServiceImpl.java
│   ├── controller/
│   │   ├── AuthController.java
│   │   ├── SysUserController.java
│   │   ├── SysRoleController.java
│   │   ├── SysMenuController.java
│   │   └── SysDeptController.java
│   ├── interceptor/
│   │   └── DataScopeInterceptor.java        # 数据权限拦截器
│   └── aspect/
│       └── OperLogAspect.java               # 操作日志切面
└── src/main/resources/
    └── application.yml

数据库设计 ​

sql
-- 用户表
CREATE TABLE `sys_user` (
    `id` BIGINT NOT NULL AUTO_INCREMENT,
    `username` VARCHAR(50) NOT NULL COMMENT '用户名',
    `password` VARCHAR(100) NOT NULL COMMENT '密码(BCrypt)',
    `nickname` VARCHAR(50) DEFAULT NULL COMMENT '昵称',
    `email` VARCHAR(100) DEFAULT NULL COMMENT '邮箱',
    `phone` VARCHAR(20) DEFAULT NULL COMMENT '手机号',
    `avatar` VARCHAR(255) DEFAULT NULL COMMENT '头像',
    `dept_id` BIGINT DEFAULT NULL COMMENT '部门ID',
    `status` TINYINT NOT NULL DEFAULT 1 COMMENT '状态:0-禁用,1-启用',
    `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
    `updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    PRIMARY KEY (`id`),
    UNIQUE KEY `uk_username` (`username`),
    KEY `idx_dept_id` (`dept_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表';

-- 角色表
CREATE TABLE `sys_role` (
    `id` BIGINT NOT NULL AUTO_INCREMENT,
    `role_name` VARCHAR(50) NOT NULL COMMENT '角色名称',
    `role_key` VARCHAR(50) NOT NULL COMMENT '角色标识',
    `sort_order` INT NOT NULL DEFAULT 0 COMMENT '排序',
    `data_scope` TINYINT NOT NULL DEFAULT 1 COMMENT '数据范围:1-全部,2-本部门,3-本部门及以下,4-本人',
    `status` TINYINT NOT NULL DEFAULT 1 COMMENT '状态:0-禁用,1-启用',
    `remark` VARCHAR(200) DEFAULT NULL COMMENT '备注',
    `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
    PRIMARY KEY (`id`),
    UNIQUE KEY `uk_role_key` (`role_key`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='角色表';

-- 菜单权限表
CREATE TABLE `sys_menu` (
    `id` BIGINT NOT NULL AUTO_INCREMENT,
    `menu_name` VARCHAR(50) NOT NULL COMMENT '菜单名称',
    `parent_id` BIGINT NOT NULL DEFAULT 0 COMMENT '父菜单ID',
    `sort_order` INT NOT NULL DEFAULT 0 COMMENT '排序',
    `path` VARCHAR(200) DEFAULT NULL COMMENT '路由路径',
    `component` VARCHAR(200) DEFAULT NULL COMMENT '组件路径',
    `menu_type` TINYINT NOT NULL DEFAULT 0 COMMENT '类型:0-目录,1-菜单,2-按钮',
    `permission` VARCHAR(100) DEFAULT NULL COMMENT '权限标识(如:system:user:list)',
    `icon` VARCHAR(100) DEFAULT NULL COMMENT '图标',
    `visible` TINYINT NOT NULL DEFAULT 1 COMMENT '是否可见:0-隐藏,1-显示',
    `status` TINYINT NOT NULL DEFAULT 1 COMMENT '状态:0-禁用,1-启用',
    `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
    PRIMARY KEY (`id`),
    KEY `idx_parent_id` (`parent_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='菜单权限表';

-- 部门表
CREATE TABLE `sys_dept` (
    `id` BIGINT NOT NULL AUTO_INCREMENT,
    `dept_name` VARCHAR(50) NOT NULL COMMENT '部门名称',
    `parent_id` BIGINT NOT NULL DEFAULT 0 COMMENT '父部门ID',
    `ancestors` VARCHAR(500) DEFAULT NULL COMMENT '祖级列表(如:0,1,2)',
    `sort_order` INT NOT NULL DEFAULT 0 COMMENT '排序',
    `leader` VARCHAR(50) DEFAULT NULL COMMENT '负责人',
    `status` TINYINT NOT NULL DEFAULT 1 COMMENT '状态:0-禁用,1-启用',
    `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
    PRIMARY KEY (`id`),
    KEY `idx_parent_id` (`parent_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='部门表';

-- 用户角色关联表
CREATE TABLE `sys_user_role` (
    `user_id` BIGINT NOT NULL COMMENT '用户ID',
    `role_id` BIGINT NOT NULL COMMENT '角色ID',
    PRIMARY KEY (`user_id`, `role_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户角色关联表';

-- 角色菜单关联表
CREATE TABLE `sys_role_menu` (
    `role_id` BIGINT NOT NULL COMMENT '角色ID',
    `menu_id` BIGINT NOT NULL COMMENT '菜单ID',
    PRIMARY KEY (`role_id`, `menu_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='角色菜单关联表';

-- 操作日志表
CREATE TABLE `sys_oper_log` (
    `id` BIGINT NOT NULL AUTO_INCREMENT,
    `user_id` BIGINT DEFAULT NULL COMMENT '操作人ID',
    `username` VARCHAR(50) DEFAULT NULL COMMENT '操作人',
    `operation` VARCHAR(200) DEFAULT NULL COMMENT '操作描述',
    `method` VARCHAR(200) DEFAULT NULL COMMENT '请求方法',
    `params` TEXT DEFAULT NULL COMMENT '请求参数',
    `ip` VARCHAR(50) DEFAULT NULL COMMENT 'IP地址',
    `duration` BIGINT DEFAULT NULL COMMENT '耗时(毫秒)',
    `status` TINYINT DEFAULT NULL COMMENT '状态:0-失败,1-成功',
    `error_msg` TEXT DEFAULT NULL COMMENT '错误信息',
    `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
    PRIMARY KEY (`id`),
    KEY `idx_user_id` (`user_id`),
    KEY `idx_created_at` (`created_at`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='操作日志表';

核心代码示例 ​

Spring Security配置 SecurityConfig.java:

java
@Configuration
@EnableWebSecurity
@EnableMethodSecurity
public class SecurityConfig {

    @Autowired
    private JwtAuthenticationFilter jwtAuthFilter;
    @Autowired
    private JwtAuthenticationEntryPoint entryPoint;
    @Autowired
    private AccessDeniedHandlerImpl accessDeniedHandler;

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .csrf(csrf -> csrf.disable())
            .sessionManagement(session ->
                session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/api/auth/**").permitAll()
                .requestMatchers("/doc.html", "/webjars/**").permitAll()
                .anyRequest().authenticated()
            )
            .exceptionHandling(ex -> ex
                .authenticationEntryPoint(entryPoint)
                .accessDeniedHandler(accessDeniedHandler)
            )
            .addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class);

        return http.build();
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Bean
    public AuthenticationManager authenticationManager(
            AuthenticationConfiguration config) throws Exception {
        return config.getAuthenticationManager();
    }
}

JWT认证过滤器 JwtAuthenticationFilter.java:

java
@Component
@RequiredArgsConstructor
public class JwtAuthenticationFilter extends OncePerRequestFilter {

    private final JwtUtil jwtUtil;
    private final UserDetailsService userDetailsService;

    @Override
    protected void doFilterInternal(HttpServletRequest request,
                                    HttpServletResponse response,
                                    FilterChain filterChain)
            throws ServletException, IOException {

        String authHeader = request.getHeader("Authorization");

        if (authHeader == null || !authHeader.startsWith("Bearer ")) {
            filterChain.doFilter(request, response);
            return;
        }

        String token = authHeader.substring(7);

        if (!jwtUtil.validateToken(token)) {
            filterChain.doFilter(request, response);
            return;
        }

        String username = jwtUtil.getUsername(token);

        // 如果尚未设置认证信息
        if (SecurityContextHolder.getContext().getAuthentication() == null) {
            UserDetails userDetails = userDetailsService.loadUserByUsername(username);

            UsernamePasswordAuthenticationToken authToken =
                new UsernamePasswordAuthenticationToken(
                    userDetails, null, userDetails.getAuthorities());

            authToken.setDetails(
                new WebAuthenticationDetailsSource().buildDetails(request));

            SecurityContextHolder.getContext().setAuthentication(authToken);
        }

        filterChain.doFilter(request, response);
    }
}

用户详情服务 UserDetailsServiceImpl.java:

java
@Service
public class UserDetailsServiceImpl implements UserDetailsService {

    @Autowired
    private SysUserMapper userMapper;
    @Autowired
    private SysRoleMapper roleMapper;
    @Autowired
    private SysMenuMapper menuMapper;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        // 1. 查询用户
        SysUser user = userMapper.selectByUsername(username);
        if (user == null) {
            throw new UsernameNotFoundException("用户不存在:" + username);
        }
        if (user.getStatus() != 1) {
            throw new DisabledException("用户已被禁用");
        }

        // 2. 查询用户角色
        List<SysRole> roles = roleMapper.selectRolesByUserId(user.getId());
        Set<String> roleKeys = roles.stream()
                .map(SysRole::getRoleKey)
                .collect(Collectors.toSet());

        // 3. 查询用户权限标识
        Set<String> permissions;
        if (roleKeys.contains("admin")) {
            // 超级管理员拥有所有权限
            permissions = Set.of("*:*:*");
        } else {
            List<SysMenu> menus = menuMapper.selectMenusByUserId(user.getId());
            permissions = menus.stream()
                    .map(SysMenu::getPermission)
                    .filter(StringUtils::hasText)
                    .collect(Collectors.toSet());
        }

        // 4. 查询用户数据权限范围
        Integer dataScope = roles.stream()
                .map(SysRole::getDataScope)
                .max(Integer::compareTo)
                .orElse(1);

        // 5. 构建登录用户对象
        LoginUser loginUser = new LoginUser();
        loginUser.setUserId(user.getId());
        loginUser.setUsername(user.getUsername());
        loginUser.setDeptId(user.getDeptId());
        loginUser.setRoles(roleKeys);
        loginUser.setPermissions(permissions);
        loginUser.setDataScope(dataScope);

        return loginUser;
    }
}

登录用户 LoginUser.java:

java
@Data
public class LoginUser implements UserDetails {

    private Long userId;
    private String username;
    private String password;
    private Long deptId;
    private Set<String> roles;
    private Set<String> permissions;
    private Integer dataScope;

    @JsonIgnore
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        List<SimpleGrantedAuthority> authorities = new ArrayList<>();

        // 角色权限
        for (String role : roles) {
            authorities.add(new SimpleGrantedAuthority("ROLE_" + role));
        }

        // 菜单权限
        for (String permission : permissions) {
            authorities.add(new SimpleGrantedAuthority(permission));
        }

        return authorities;
    }

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

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

    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return true;
    }
}

权限注解 @RequiresPermission:

java
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RequiresPermission {
    String[] value();           // 权限标识
    Logical logical() default Logical.AND;  // 逻辑关系
}

public enum Logical {
    AND, OR
}

权限校验AOP:

java
@Aspect
@Component
public class PermissionAspect {

    @Before("@annotation(requiresPermission)")
    public void checkPermission(RequiresPermission requiresPermission) {
        LoginUser loginUser = getLoginUser();
        if (loginUser == null) {
            throw new BusinessException(401, "未登录");
        }

        Set<String> permissions = loginUser.getPermissions();

        // 超级管理员
        if (permissions.contains("*:*:*")) {
            return;
        }

        String[] requiredPerms = requiresPermission.value();
        Logical logical = requiresPermission.logical();

        if (logical == Logical.AND) {
            for (String perm : requiredPerms) {
                if (!permissions.contains(perm)) {
                    throw new BusinessException(403, "没有权限:" + perm);
                }
            }
        } else {
            boolean hasAny = false;
            for (String perm : requiredPerms) {
                if (permissions.contains(perm)) {
                    hasAny = true;
                    break;
                }
            }
            if (!hasAny) {
                throw new BusinessException(403, "没有所需权限");
            }
        }
    }

    private LoginUser getLoginUser() {
        Authentication authentication = SecurityContextHolder.getContext()
                .getAuthentication();
        if (authentication != null && authentication.getPrincipal() instanceof LoginUser) {
            return (LoginUser) authentication.getPrincipal();
        }
        return null;
    }
}

数据权限拦截器 DataScopeInterceptor.java:

java
@Intercepts({
    @Signature(type = Executor.class, method = "query",
               args = {MappedStatement.class, Object.class, RowBounds.class,
                       ResultHandler.class})
})
@Component
public class DataScopeInterceptor implements Interceptor {

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        MappedStatement ms = (MappedStatement) invocation.getArgs()[0];

        // 获取方法上的@DataScope注解
        DataScope dataScope = getDataScopeAnnotation(ms);
        if (dataScope == null) {
            return invocation.proceed();
        }

        LoginUser loginUser = getLoginUser();
        if (loginUser == null) {
            return invocation.proceed();
        }

        // 超级管理员不做限制
        if (loginUser.getRoles().contains("admin")) {
            return invocation.proceed();
        }

        // 根据数据范围拼接SQL条件
        String sqlCondition = buildSqlCondition(dataScope, loginUser);

        // 修改SQL(通过BoundSql包装)
        // ...

        return invocation.proceed();
    }

    private String buildSqlCondition(DataScope dataScope, LoginUser loginUser) {
        String deptAlias = dataScope.deptAlias();
        String userAlias = dataScope.userAlias();

        switch (loginUser.getDataScope()) {
            case 1: // 全部数据
                return "";
            case 2: // 本部门数据
                return String.format(" AND %s.dept_id = %d",
                    deptAlias, loginUser.getDeptId());
            case 3: // 本部门及以下数据
                return String.format(
                    " AND %s.dept_id IN (SELECT id FROM sys_dept WHERE FIND_IN_SET(%s.dept_id, ancestors))",
                    deptAlias, deptAlias);
            case 4: // 仅本人数据
                return String.format(" AND %s.id = %d",
                    userAlias, loginUser.getUserId());
            default:
                return "";
        }
    }
}

操作日志切面 OperLogAspect.java:

java
@Aspect
@Component
@Slf4j
public class OperLogAspect {

    @Autowired
    private SysOperLogService operLogService;

    @Around("@annotation(operLog)")
    public Object around(ProceedingJoinPoint point, OperLog operLog) throws Throwable {
        long startTime = System.currentTimeMillis();

        SysOperLog logEntity = new SysOperLog();
        logEntity.setOperation(operLog.value());
        logEntity.setMethod(point.getTarget().getClass().getName() + "." +
                           point.getSignature().getName());

        // 获取请求参数
        Object[] args = point.getArgs();
        try {
            logEntity.setParams(new ObjectMapper().writeValueAsString(args));
        } catch (Exception e) {
            logEntity.setParams("参数序列化失败");
        }

        // 获取当前用户
        LoginUser loginUser = getLoginUser();
        if (loginUser != null) {
            logEntity.setUserId(loginUser.getUserId());
            logEntity.setUsername(loginUser.getUsername());
        }

        try {
            Object result = point.proceed();
            logEntity.setStatus(1); // 成功
            logEntity.setDuration(System.currentTimeMillis() - startTime);
            return result;
        } catch (Exception e) {
            logEntity.setStatus(0); // 失败
            logEntity.setErrorMsg(e.getMessage());
            logEntity.setDuration(System.currentTimeMillis() - startTime);
            throw e;
        } finally {
            // 异步保存日志
            operLogService.saveAsync(logEntity);
        }
    }
}

认证控制器 AuthController.java:

java
@RestController
@RequestMapping("/api/auth")
@Tag(name = "认证管理")
@Slf4j
public class AuthController {

    @Autowired
    private AuthenticationManager authenticationManager;
    @Autowired
    private JwtUtil jwtUtil;
    @Autowired
    private StringRedisTemplate redisTemplate;

    @PostMapping("/login")
    @Operation(summary = "用户登录")
    public Result<LoginVO> login(@RequestBody @Valid LoginDTO dto) {
        // 1. 认证
        Authentication authentication = authenticationManager.authenticate(
            new UsernamePasswordAuthenticationToken(dto.getUsername(), dto.getPassword()));

        LoginUser loginUser = (LoginUser) authentication.getPrincipal();

        // 2. 生成Token
        String token = jwtUtil.generateToken(loginUser.getUsername());

        // 3. 将用户信息存入Redis(用于Token校验和权限缓存)
        String redisKey = "login:token:" + token;
        redisTemplate.opsForValue().set(redisKey,
            JSON.toJSONString(loginUser), 24, TimeUnit.HOURS);

        // 4. 构建返回
        LoginVO loginVO = new LoginVO();
        loginVO.setToken(token);
        loginVO.setUserId(loginUser.getUserId());
        loginVO.setUsername(loginUser.getUsername());
        loginVO.setRoles(loginUser.getRoles());
        loginVO.setPermissions(loginUser.getPermissions());

        log.info("用户登录成功:{}", dto.getUsername());
        return Result.success(loginVO);
    }

    @PostMapping("/logout")
    @Operation(summary = "退出登录")
    public Result<Void> logout(HttpServletRequest request) {
        String token = request.getHeader("Authorization");
        if (token != null && token.startsWith("Bearer ")) {
            token = token.substring(7);
            redisTemplate.delete("login:token:" + token);
        }
        return Result.success();
    }

    @GetMapping("/info")
    @Operation(summary = "获取当前用户信息")
    public Result<UserInfoVO> getUserInfo() {
        LoginUser loginUser = getLoginUser();
        UserInfoVO vo = new UserInfoVO();
        vo.setUserId(loginUser.getUserId());
        vo.setUsername(loginUser.getUsername());
        vo.setRoles(loginUser.getRoles());
        vo.setPermissions(loginUser.getPermissions());
        return Result.success(vo);
    }

    private LoginUser getLoginUser() {
        return (LoginUser) SecurityContextHolder.getContext()
                .getAuthentication().getPrincipal();
    }
}

用户管理控制器 SysUserController.java:

java
@RestController
@RequestMapping("/api/system/users")
@Tag(name = "用户管理")
public class SysUserController {

    @Autowired
    private SysUserService userService;

    @GetMapping
    @RequiresPermission("system:user:list")
    @Operation(summary = "用户列表")
    public Result<PageResult<SysUserVO>> list(UserQueryDTO query) {
        return Result.success(userService.getUserList(query));
    }

    @GetMapping("/{id}")
    @RequiresPermission("system:user:query")
    @Operation(summary = "用户详情")
    public Result<SysUserVO> detail(@PathVariable Long id) {
        return Result.success(userService.getUserDetail(id));
    }

    @PostMapping
    @RequiresPermission("system:user:add")
    @Operation(summary = "新增用户")
    public Result<Void> add(@RequestBody @Valid UserDTO dto) {
        userService.addUser(dto);
        return Result.success();
    }

    @PutMapping("/{id}")
    @RequiresPermission("system:user:edit")
    @Operation(summary = "修改用户")
    public Result<Void> update(@PathVariable Long id, @RequestBody @Valid UserDTO dto) {
        userService.updateUser(id, dto);
        return Result.success();
    }

    @DeleteMapping("/{id}")
    @RequiresPermission("system:user:remove")
    @Operation(summary = "删除用户")
    public Result<Void> delete(@PathVariable Long id) {
        userService.deleteUser(id);
        return Result.success();
    }

    @PutMapping("/{id}/status")
    @RequiresPermission("system:user:edit")
    @Operation(summary = "修改用户状态")
    public Result<Void> updateStatus(@PathVariable Long id, @RequestParam Integer status) {
        userService.updateStatus(id, status);
        return Result.success();
    }

    @PutMapping("/{id}/password")
    @RequiresPermission("system:user:resetPwd")
    @Operation(summary = "重置密码")
    public Result<Void> resetPassword(@PathVariable Long id, @RequestParam String password) {
        userService.resetPassword(id, password);
        return Result.success();
    }

    @PostMapping("/{id}/roles")
    @RequiresPermission("system:user:edit")
    @Operation(summary = "分配角色")
    public Result<Void> assignRoles(@PathVariable Long id, @RequestBody List<Long> roleIds) {
        userService.assignRoles(id, roleIds);
        return Result.success();
    }
}

接口设计 ​

模块方法路径权限标识
认证POST/api/auth/login无需认证
认证POST/api/auth/logout需要认证
认证GET/api/auth/info需要认证
用户GET/api/system/userssystem:user:list
用户POST/api/system/userssystem:user:add
用户PUT/api/system/users/system:user:edit
用户DELETE/api/system/users/system:user:remove
角色GET/api/system/rolessystem:role:list
角色POST/api/system/rolessystem:role:add
角色PUT/api/system/roles/system:role:edit
角色DELETE/api/system/roles/system:role:remove
菜单GET/api/system/menussystem:menu:list
菜单POST/api/system/menussystem:menu:add
部门GET/api/system/deptssystem:dept:list
部门POST/api/system/deptssystem:dept:add
日志GET/api/system/logssystem:log:list

开发步骤 ​

  1. 安全框架集成(第1-3天)

    • Spring Security配置
    • JWT认证流程
    • 登录/登出
  2. RBAC权限模型(第4-7天)

    • 用户管理CRUD
    • 角色管理CRUD
    • 菜单权限管理
    • 权限分配
  3. 接口权限控制(第8-10天)

    • @PreAuthorize注解
    • 自定义权限注解
    • 权限校验AOP
  4. 数据权限(第11-13天)

    • MyBatis拦截器
    • 数据范围SQL拼接
    • 部门层级数据
  5. 辅助功能(第14-18天)

    • 部门管理
    • 操作日志
    • 登录日志
    • 在线用户管理
  6. 测试与优化(第19-21天)

    • 单元测试
    • 接口测试
    • 安全审计

7.2 开发规范 ​

7.2.1 代码规范:阿里巴巴Java开发手册 ​

命名规范 ​

java
// ✅ 正确示例

// 类名:UpperCamelCase
public class UserService {}
public class OrderController {}
public class ProductServiceImpl {}

// 方法名/变量名:lowerCamelCase
public void getUserById() {}
private String userName;
private Long orderId;

// 常量:全大写下划线分隔
public static final int MAX_RETRY_COUNT = 3;
public static final String DEFAULT_CHARSET = "UTF-8";

// 包名:全小写
package com.example.user.service;

// 抽象类以Abstract或Base开头
public abstract class AbstractService {}

// 异常类以Exception结尾
public class BusinessException extends RuntimeException {}

// 测试类以Test结尾
public class UserServiceTest {}

// 枚举类以Enum结尾
public enum OrderStatusEnum {
    PENDING, PAID, SHIPPED, COMPLETED, CANCELLED
}

// ❌ 错误示例
public class userservice {}          // 不符合UpperCamelCase
public void GetUserInfo() {}         // 方法名不应大写开头
private String user_name;            // 变量名不应用下划线
public static final int max_count = 3; // 常量应全大写

POJO类规范 ​

java
// ✅ 正确的POJO类
@Data
public class UserDTO {

    /** 用户ID */
    @NotNull(message = "用户ID不能为空")
    private Long userId;

    /** 用户名 */
    @NotBlank(message = "用户名不能为空")
    @Size(min = 3, max = 20, message = "用户名长度3-20位")
    private String username;

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

    /** 年龄 */
    @Min(value = 0, message = "年龄不能小于0")
    @Max(value = 150, message = "年龄不能大于150")
    private Integer age;

    // 规则:
    // 1. POJO类属性必须使用包装数据类型(Integer而非int)
    // 2. 布尔类型属性不加is前缀(避免序列化问题)
    // 3. 必须重写toString方法(Lombok @Data自动处理)
    // 4. 构造方法里有业务逻辑,禁止使用@DefaultConstructor
}

集合处理规范 ​

java
// ✅ 正确
// 判断集合是否为空
if (CollectionUtils.isEmpty(list)) { ... }
if (CollectionUtils.isNotEmpty(list)) { ... }

// 判断Map是否为空
if (MapUtils.isEmpty(map)) { ... }

// 使用entrySet遍历Map
for (Map.Entry<String, Object> entry : map.entrySet()) {
    String key = entry.getKey();
    Object value = entry.getValue();
}

// 集合转数组
String[] array = list.toArray(new String[0]);

// Arrays.asList返回的是固定大小列表,不能add/remove
List<String> fixedList = Arrays.asList("A", "B", "C");
// fixedList.add("D"); // 抛出UnsupportedOperationException

// 使用new ArrayList<>(Arrays.asList(...))创建可变列表
List<String> mutableList = new ArrayList<>(Arrays.asList("A", "B", "C"));

// ❌ 错误
if (list == null || list.size() == 0) { ... } // 应使用CollectionUtils

并发处理规范 ​

java
// ✅ 线程池创建(禁止使用Executors快捷方法)
ThreadPoolExecutor executor = new ThreadPoolExecutor(
    Runtime.getRuntime().availableProcessors(),     // 核心线程数
    Runtime.getRuntime().availableProcessors() * 2, // 最大线程数
    60L, TimeUnit.SECONDS,                          // 空闲超时
    new LinkedBlockingQueue<>(1000),                // 工作队列
    new ThreadFactoryBuilder().setNameFormat("biz-pool-%d").build(),
    new ThreadPoolExecutor.CallerRunsPolicy()       // 拒绝策略
);

// ✅ SimpleDateFormat线程不安全
// 方式1:使用DateTimeFormatter(推荐)
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
String formatted = LocalDateTime.now().format(formatter);

// 方式2:使用ThreadLocal
private static final ThreadLocal<SimpleDateFormat> SDF =
    ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));

// ✅ 对多个资源加锁时,注意锁顺序,避免死锁
// 始终按固定顺序获取锁

注释规范 ​

java
/**
 * 用户服务类
 *
 * @author zhangsan
 * @date 2024-01-01
 */
@Service
public class UserService {

    /**
     * 根据用户名查询用户
     *
     * @param username 用户名,不能为空
     * @return 用户信息,不存在返回null
     * @throws BusinessException 当参数非法时抛出
     */
    public User findByUsername(String username) {
        // 先查缓存
        User user = redisTemplate.get("user:" + username);
        if (user != null) {
            return user;
        }

        // 缓存未命中,查数据库
        user = userMapper.selectByUsername(username);

        // 写入缓存
        if (user != null) {
            redisTemplate.set("user:" + username, user, 30, TimeUnit.MINUTES);
        }

        return user;
    }
}

// 规则:
// 1. 所有类必须有创建者和创建日期
// 2. 所有公共方法必须有Javadoc注释
// 3. 方法内部单行注释用 //
// 4. 边写代码边注释,修改代码同时修改注释
// 5. 特殊注释标记:
//    TODO(标记人,标记时间,预计处理时间)
//    FIXME(标记问题,预计处理时间)

7.2.2 Git工作流(Git Flow) ​

分支模型 ​

main (master)          ─────────────────────────────── 生产分支
  │                                         ↑
  │                               merge     │
  ├── develop ─────────────────────────────── 开发分支
  │     │                    ↑
  │     │    feature/login   │ merge
  │     ├───●───●───●────────┤
  │     │                    │
  │     │    feature/order   │ merge
  │     ├───●───●────────────┤
  │     │                    │
  │     ├── release/1.0 ─────┤
  │     │                    │
  │     └── hotfix/bug ──────┘

分支说明:

  • main/master:生产环境分支,始终保持稳定
  • develop:开发主分支,包含最新开发功能
  • feature/*:功能分支,从 develop 创建,合并回 develop
  • release/*:发布分支,从 develop 创建,测试通过后合并到 main 和 develop
  • hotfix/*:紧急修复分支,从 main 创建,修复后合并到 main 和 develop

Git 提交规范 ​

bash
# 提交信息格式
<type>(<scope>): <subject>

# type 类型:
feat:     新功能
fix:      修复 bug
docs:     文档更新
style:    代码格式调整(不影响功能)
refactor: 代码重构
test:     测试相关
chore:    构建/工具更新

# 示例:
feat(user): 添加用户登录功能
fix(order): 修复订单金额计算错误
docs(readme): 更新项目说明文档
style(code): 统一代码格式
refactor(service): 重构用户服务层
test(user): 添加用户模块单元测试
chore(deps): 升级依赖版本

分支命名规范 ​

bash
# 功能分支
feature/user-login
feature/order-payment
feature/product-search

# 修复分支
fix/login-error
fix/order-calculation

# 发布分支
release/v1.0.0
release/v1.1.0

# 热修复分支
hotfix/login-crash
hotfix/payment-timeout

7.2.3 接口设计规范(RESTful) ​

RESTful 设计原则 ​

java
// ✅ 正确的 RESTful 设计

// 资源命名:使用名词复数
GET    /api/users          // 获取用户列表
GET    /api/users/{id}     // 获取单个用户
POST   /api/users          // 创建用户
PUT    /api/users/{id}     // 更新用户(全量)
PATCH  /api/users/{id}     // 更新用户(部分)
DELETE /api/users/{id}     // 删除用户

// ❌ 错误的 RESTful 设计
GET    /api/getUsers       // 不要使用动词
POST   /api/createUser     // 不要使用动词
GET    /api/user/{id}      // 应该用复数 users

// 嵌套资源
GET    /api/users/{userId}/orders          // 获取用户的订单列表
GET    /api/users/{userId}/orders/{orderId} // 获取用户的单个订单

// 查询参数
GET    /api/users?page=1&size=10           // 分页
GET    /api/users?status=1&role=admin      // 过滤
GET    /api/users?sort=createdAt,desc      // 排序
GET    /api/users?keyword=zhang            // 搜索

统一响应格式 ​

java
// 成功响应
{
    "code": 200,
    "message": "success",
    "data": {
        "userId": 1,
        "username": "zhangsan"
    },
    "timestamp": 1704067200000
}

// 分页响应
{
    "code": 200,
    "message": "success",
    "data": {
        "list": [...],
        "total": 100,
        "page": 1,
        "size": 10,
        "pages": 10
    }
}

// 错误响应
{
    "code": 400,
    "message": "参数错误:用户名不能为空",
    "data": null,
    "timestamp": 1704067200000
}

// 未认证
{
    "code": 401,
    "message": "未登录或 Token 已过期",
    "data": null
}

// 无权限
{
    "code": 403,
    "message": "没有操作权限",
    "data": null
}

HTTP 状态码使用 ​

java
// 2xx 成功
200 OK              // 请求成功(GET、PUT、PATCH)
201 Created         // 创建成功(POST)
204 No Content      // 删除成功(DELETE)

// 4xx 客户端错误
400 Bad Request     // 请求参数错误
401 Unauthorized    // 未认证
403 Forbidden       // 无权限
404 Not Found       // 资源不存在
405 Method Not Allowed  // 请求方法不允许
429 Too Many Requests   // 请求过于频繁

// 5xx 服务器错误
500 Internal Server Error   // 服务器内部错误
502 Bad Gateway             // 网关错误
503 Service Unavailable     // 服务不可用
504 Gateway Timeout         // 网关超时

7.2.4 日志规范 ​

日志级别使用 ​

java
@Slf4j
public class OrderService {
    
    public Order createOrder(OrderDTO dto) {
        // DEBUG:调试信息,开发环境使用
        log.debug("创建订单,参数:{}", dto);
        
        try {
            // INFO:重要业务流程
            log.info("开始创建订单,用户ID:{}", dto.getUserId());
            
            Order order = orderMapper.insert(dto);
            
            log.info("订单创建成功,订单ID:{}", order.getId());
            return order;
            
        } catch (Exception e) {
            // ERROR:错误异常,需要立即处理
            log.error("创建订单失败,参数:{}", dto, e);
            throw new BusinessException("创建订单失败");
        }
    }
    
    public void processPayment(Order order) {
        // WARN:警告信息,可能有问题但不影响主流程
        if (order.getAmount() > 10000) {
            log.warn("大额订单,订单ID:{},金额:{}", order.getId(), order.getAmount());
        }
        
        // TRACE:最详细的追踪信息,一般不启用
        log.trace("处理支付,订单详情:{}", order);
    }
}

// 日志级别使用原则:
// ERROR:系统错误,需要立即处理(如:数据库连接失败、第三方服务调用失败)
// WARN:潜在问题,需要关注(如:参数异常但可降级处理、性能警告)
// INFO:重要业务流程(如:用户登录、订单创建、支付成功)
// DEBUG:调试信息(如:方法入参、中间结果)
// TRACE:详细追踪(如:循环内部变量)

日志格式规范 ​

java
// ✅ 正确的日志格式
log.info("用户登录成功,用户ID:{},登录IP:{}", userId, ip);
log.error("调用支付接口失败,订单ID:{},错误信息:{}", orderId, e.getMessage(), e);

// ❌ 错误的日志格式
log.info("用户登录成功");  // 缺少关键信息
log.error("错误:" + e.getMessage());  // 使用字符串拼接
log.info("用户ID:" + userId);  // 使用占位符而非拼接

// 日志内容规范:
// 1. 使用占位符 {} 而非字符串拼接
// 2. 包含关键业务信息(ID、订单号等)
// 3. 异常日志必须包含堆栈信息
// 4. 避免记录敏感信息(密码、身份证号等)
// 5. 避免在循环中打印大量日志

Logback 配置示例 ​

xml
<!-- logback-spring.xml -->
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
    <property name="LOG_PATH" value="./logs"/>
    <property name="APP_NAME" value="blog-system"/>
    
    <!-- 控制台输出 -->
    <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
        </encoder>
    </appender>
    
    <!-- 文件输出(按天滚动) -->
    <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <file>${LOG_PATH}/${APP_NAME}.log</file>
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <fileNamePattern>${LOG_PATH}/${APP_NAME}.%d{yyyy-MM-dd}.log</fileNamePattern>
            <maxHistory>30</maxHistory>
        </rollingPolicy>
        <encoder>
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
        </encoder>
    </appender>
    
    <!-- 错误日志单独文件 -->
    <appender name="ERROR_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <file>${LOG_PATH}/${APP_NAME}-error.log</file>
        <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
            <level>ERROR</level>
        </filter>
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <fileNamePattern>${LOG_PATH}/${APP_NAME}-error.%d{yyyy-MM-dd}.log</fileNamePattern>
            <maxHistory>30</maxHistory>
        </rollingPolicy>
        <encoder>
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
        </encoder>
    </appender>
    
    <!-- 开发环境 -->
    <springProfile name="dev">
        <root level="INFO">
            <appender-ref ref="CONSOLE"/>
            <appender-ref ref="FILE"/>
        </root>
        <logger name="com.example.blog" level="DEBUG"/>
    </springProfile>
    
    <!-- 生产环境 -->
    <springProfile name="prod">
        <root level="INFO">
            <appender-ref ref="FILE"/>
            <appender-ref ref="ERROR_FILE"/>
        </root>
    </springProfile>
</configuration>

7.2.5 单元测试规范 ​

测试类命名规范 ​

java
// 测试类命名:被测试类名 + Test
public class UserServiceTest { }
public class OrderServiceTest { }

// 测试方法命名:test + 方法名 + 场景
@Test
public void testFindById_success() { }

@Test
public void testFindById_notFound() { }

@Test
public void testCreateOrder_success() { }

@Test
public void testCreateOrder_insufficientStock() { }

JUnit 5 + Mockito 示例 ​

java
@ExtendWith(MockitoExtension.class)
class OrderServiceTest {

    @Mock
    private OrderMapper orderMapper;
    
    @Mock
    private ProductMapper productMapper;
    
    @Mock
    private StringRedisTemplate redisTemplate;
    
    @InjectMocks
    private OrderService orderService;
    
    @Test
    @DisplayName("创建订单成功")
    void testCreateOrder_success() {
        // Given(准备数据)
        OrderDTO dto = new OrderDTO();
        dto.setUserId(1L);
        dto.setProductId(100L);
        dto.setQuantity(2);
        
        Product product = new Product();
        product.setId(100L);
        product.setName("测试商品");
        product.setPrice(new BigDecimal("99.99"));
        product.setStock(10);
        
        // Mock 行为
        when(productMapper.selectById(100L)).thenReturn(product);
        when(orderMapper.insert(any(Order.class))).thenReturn(1);
        
        // When(执行测试)
        Order result = orderService.createOrder(dto);
        
        // Then(验证结果)
        assertNotNull(result);
        assertEquals(1L, result.getUserId());
        assertEquals(new BigDecimal("199.98"), result.getTotalAmount());
        
        // 验证方法调用
        verify(productMapper, times(1)).selectById(100L);
        verify(orderMapper, times(1)).insert(any(Order.class));
    }
    
    @Test
    @DisplayName("创建订单失败 - 库存不足")
    void testCreateOrder_insufficientStock() {
        // Given
        OrderDTO dto = new OrderDTO();
        dto.setUserId(1L);
        dto.setProductId(100L);
        dto.setQuantity(10);
        
        Product product = new Product();
        product.setId(100L);
        product.setStock(5); // 库存不足
        
        when(productMapper.selectById(100L)).thenReturn(product);
        
        // When & Then
        BusinessException exception = assertThrows(BusinessException.class, () -> {
            orderService.createOrder(dto);
        });
        
        assertEquals("库存不足", exception.getMessage());
    }
    
    @Test
    @DisplayName("查询订单列表 - 空结果")
    void testFindOrderList_empty() {
        // Given
        when(orderMapper.selectList(any())).thenReturn(Collections.emptyList());
        
        // When
        List<Order> result = orderService.findOrderList(1L);
        
        // Then
        assertTrue(result.isEmpty());
    }
}

测试覆盖率要求 ​

java
// 测试覆盖原则:
// 1. 核心业务逻辑必须测试(Service 层)
// 2. 边界条件必须测试(空值、极值、异常)
// 3. 分支覆盖(if-else、switch 的所有分支)
// 4. 异常场景必须测试

// 测试分层:
// 单元测试:测试单个类或方法(使用 Mock)
// 集成测试:测试多个组件协作(使用真实数据库)
// 接口测试:测试 HTTP 接口(使用 MockMvc)

@SpringBootTest
class UserControllerIntegrationTest {

    @Autowired
    private MockMvc mockMvc;
    
    @Autowired
    private ObjectMapper objectMapper;
    
    @Test
    void testLogin_success() throws Exception {
        LoginDTO dto = new LoginDTO();
        dto.setUsername("admin");
        dto.setPassword("123456");
        
        mockMvc.perform(post("/api/auth/login")
                .contentType(MediaType.APPLICATION_JSON)
                .content(objectMapper.writeValueAsString(dto)))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.code").value(200))
                .andExpect(jsonPath("$.data.token").isNotEmpty());
    }
}

7.3 练习任务 ​

综合练习项目 ​

练习1:个人博客系统(入门级) ​

目标: 独立完成一个完整的博客系统

功能要求:

  1. 用户注册/登录(JWT 认证)
  2. 文章 CRUD(支持 Markdown)
  3. 分类管理
  4. 标签管理(多对多)
  5. 评论功能
  6. 文章搜索
  7. Redis 缓存热点文章

技术要点:

  • Spring Boot + MyBatis-Plus
  • MySQL 数据库设计
  • Redis 缓存策略
  • JWT 认证
  • Swagger API 文档

预计时间: 2周


练习2:电商订单系统(进阶级) ​

目标: 实现电商核心订单流程

功能要求:

  1. 商品管理(SPU/SKU)
  2. 购物车功能
  3. 订单创建(库存扣减)
  4. 订单支付(模拟)
  5. 订单超时取消(延迟队列)
  6. 订单查询(分页、搜索)

技术要点:

  • 分布式 ID 生成
  • Redis 库存预扣减
  • RabbitMQ 延迟队列
  • 分布式事务(Seata 或最终一致性)
  • 接口幂等性设计

预计时间: 3周


练习3:权限管理系统(高级) ​

目标: 实现企业级权限管理系统

功能要求:

  1. 用户管理
  2. 角色管理
  3. 菜单权限管理
  4. 部门管理
  5. 数据权限(部门级别)
  6. 操作日志审计
  7. 在线用户管理

技术要点:

  • Spring Security 深度集成
  • RBAC 权限模型
  • 动态权限加载
  • AOP 操作日志
  • MyBatis 拦截器(数据权限)

预计时间: 3周


7.4 学习检查清单 ​

项目实战能力 ​

  • [ ] 能够独立完成从需求分析到部署上线的完整流程
  • [ ] 掌握至少一个完整的项目架构设计
  • [ ] 能够设计合理的数据库表结构
  • [ ] 能够编写规范的 RESTful API
  • [ ] 能够使用 Redis 进行缓存优化
  • [ ] 能够使用消息队列处理异步任务
  • [ ] 能够使用 Spring Security 实现认证授权

代码质量能力 ​

  • [ ] 遵循阿里巴巴 Java 开发规范
  • [ ] 能够编写清晰的注释和文档
  • [ ] 能够编写单元测试(覆盖率 > 60%)
  • [ ] 能够使用 Git 进行版本控制和团队协作
  • [ ] 能够进行代码审查和重构

问题解决能力 ​

  • [ ] 能够独立排查和解决常见 Bug
  • [ ] 能够使用日志定位问题
  • [ ] 能够进行基本的性能优化
  • [ ] 能够阅读源码理解框架原理

工程化能力 ​

  • [ ] 能够使用 Maven/Gradle 管理项目依赖
  • [ ] 能够使用 Docker 容器化部署
  • [ ] 能够配置 CI/CD 自动化流程
  • [ ] 能够编写项目文档(README、API 文档)

7.5 学习建议 ​

项目选择建议 ​

  1. 初学者:从个人博客系统开始,熟悉基础开发流程
  2. 有一定经验:挑战电商系统,学习分布式架构
  3. 进阶开发者:实现权限管理系统,深入理解安全框架

学习方法建议 ​

  1. 先模仿后创新:参考开源项目,理解设计思路后再加入自己的改进
  2. 小步快跑:将大项目拆分为小模块,逐个实现
  3. 持续重构:完成功能后回顾代码,不断优化
  4. 写文档:记录设计思路、遇到的问题及解决方案
  5. 代码审查:找同学或社区帮忙 Review 代码

常见问题解答 ​

Q1:项目做到什么程度才算完成?

A:满足以下条件即可:

  • 核心功能完整可用
  • 代码规范,有适当注释
  • 有基本的单元测试
  • 有完整的 API 文档
  • 能够部署运行

Q2:遇到不会的技术怎么办?

A:

  1. 先查阅官方文档
  2. 搜索相关教程和博客
  3. 参考开源项目的实现
  4. 在社区提问(StackOverflow、掘金等)
  5. 适当降低要求,先用简单方案实现

Q3:如何避免项目烂尾?

A:

  1. 控制项目规模,不要贪大求全
  2. 制定明确的里程碑和截止时间
  3. 优先完成核心功能
  4. 记录进度,保持成就感
  5. 找伙伴一起学习和监督

完成本阶段后,你应该能够独立完成中等复杂度的 Java 后端项目,具备初级 Java 工程师的实战能力。继续深入学习和实践,你将逐步成长为中高级开发者。

评论

正在加载评论…