跳到主要内容

Phase 3: 状态模式 - 审核流转 🎭

学习目标: 掌握状态模式、Spring Event、权限控制、消除 if-else


📚 核心知识点

什么是状态模式?

状态模式(State Pattern) 是一种行为设计模式,允许对象在内部状态改变时改变其行为,对象看起来好像修改了它的类。

传统做法(大量 if-else)

public void approveComment(Long id) {
Comment comment = getById(id);

if (comment.getStatus() == PENDING) {
comment.setStatus(APPROVED);
// 发送通知
// 更新缓存
// ...
} else if (comment.getStatus() == SPAM) {
throw new Exception("垃圾评论无法通过审核");
} else if (comment.getStatus() == DELETED) {
throw new Exception("已删除评论无法通过审核");
}
// ... 更多判断
}

状态模式做法

public void approveComment(Long id) {
Comment comment = getById(id);
comment.getState().approve(comment); // 委托给状态对象
}

优势

  • ✅ 消除大量 if-else/switch 判断
  • ✅ 符合开闭原则(新增状态无需修改现有代码)
  • ✅ 每个状态的行为独立封装,易于维护
  • ✅ 状态转换规则清晰

🗄️ Step 1: 扩展评论状态枚举

1.1 更新 CommentStatus 枚举

文件路径: blog-comment-api/src/main/java/com/blog/comment/api/enums/CommentStatus.java

当前版本

public enum CommentStatus {
APPROVED, // 已通过
PENDING, // 待审核
REJECTED // 已拒绝
}

新增状态

package com.blog.comment.api.enums;

import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Getter;

/**
* 评论状态枚举(扩展版)
*
* @author liusxml
* @since 1.3.0
*/
@Getter
@AllArgsConstructor
@Schema(description = "评论状态")
public enum CommentStatus {

/**
* 待审核 - 新评论的初始状态
*/
PENDING("待审核", true),

/**
* 已通过 - 审核通过,前端可见
*/
APPROVED("已通过", true),

/**
* 已拒绝 - 审核不通过,前端不可见
*/
REJECTED("已拒绝", false),

/**
* 用户删除 - 用户主动删除(软删除)
*/
USER_DELETED("用户删除", false),

/**
* 管理员删除 - 管理员删除(违规内容)
*/
ADMIN_DELETED("管理员删除", false);

private final String description;
private final boolean visible; // 前端是否可见

/**
* 判断是否为已删除状态
*/
public boolean isDeleted() {
return this == USER_DELETED || this == ADMIN_DELETED;
}

/**
* 判断是否可审核
*/
public boolean canAudit() {
return this == PENDING;
}
}

知识点

  • visible 字段:控制前端查询时是否返回该状态的评论
  • isDeleted() 方法:业务逻辑判断
  • canAudit() 方法:状态流转前置检查

🎭 Step 2: 实现状态模式

2.1 创建状态接口

文件路径: blog-comment-service/src/main/java/com/blog/comment/domain/state/CommentState.java

package com.blog.comment.domain.state;

import com.blog.comment.domain.entity.CommentEntity;

/**
* 评论状态接口
*
* <p>定义评论在不同状态下的行为</p>
*
* @author liusxml
* @since 1.3.0
*/
public interface CommentState {

/**
* 审核通过
*
* @param comment 评论实体
* @throws com.blog.common.exception.BusinessException 状态转换不合法时抛出
*/
void approve(CommentEntity comment);

/**
* 审核拒绝
*
* @param comment 评论实体
* @param reason 拒绝原因
* @throws com.blog.common.exception.BusinessException 状态转换不合法时抛出
*/
void reject(CommentEntity comment, String reason);

/**
* 用户删除
*
* @param comment 评论实体
* @throws com.blog.common.exception.BusinessException 状态转换不合法时抛出
*/
void deleteByUser(CommentEntity comment);

/**
* 管理员删除
*
* @param comment 评论实体
* @param reason 删除原因
* @throws com.blog.common.exception.BusinessException 状态转换不合法时抛出
*/
void deleteByAdmin(CommentEntity comment, String reason);

/**
* 获取当前状态名称
*/
String getStateName();
}

2.2 创建抽象状态类

文件路径: blog-comment-service/src/main/java/com/blog/comment/domain/state/AbstractCommentState.java

package com.blog.comment.domain.state;

import com.blog.comment.api.enums.CommentStatus;
import com.blog.comment.domain.entity.CommentEntity;
import com.blog.common.exception.BusinessException;
import com.blog.common.exception.SystemErrorCode;
import lombok.extern.slf4j.Slf4j;

/**
* 抽象评论状态类
*
* <p>提供通用的状态转换异常处理</p>
*
* @author liusxml
* @since 1.3.0
*/
@Slf4j
public abstract class AbstractCommentState implements CommentState {

/**
* 抛出非法状态转换异常
*/
protected void throwIllegalStateException(String operation) {
String message = String.format(
"评论当前状态为 [%s],无法执行 [%s] 操作",
getStateName(),
operation
);
log.warn(message);
throw new BusinessException(SystemErrorCode.PARAM_ERROR, message);
}

/**
* 记录状态转换日志
*/
protected void logStateTransition(CommentEntity comment, CommentStatus from, CommentStatus to) {
log.info("评论状态转换: id={}, {} -> {}", comment.getId(), from, to);
}

@Override
public void approve(CommentEntity comment) {
throwIllegalStateException("审核通过");
}

@Override
public void reject(CommentEntity comment, String reason) {
throwIllegalStateException("审核拒绝");
}

@Override
public void deleteByUser(CommentEntity comment) {
throwIllegalStateException("用户删除");
}

@Override
public void deleteByAdmin(CommentEntity comment, String reason) {
throwIllegalStateException("管理员删除");
}
}

2.3 实现具体状态类

PendingState(待审核状态)

文件路径: blog-comment-service/src/main/java/com/blog/comment/domain/state/PendingState.java

package com.blog.comment.domain.state;

import com.blog.comment.api.enums.CommentStatus;
import com.blog.comment.domain.entity.CommentEntity;
import org.springframework.stereotype.Component;

/**
* 待审核状态
*
* @author liusxml
* @since 1.3.0
*/
@Component
public class PendingState extends AbstractCommentState {

@Override
public void approve(CommentEntity comment) {
logStateTransition(comment, comment.getStatus(), CommentStatus.APPROVED);
comment.setStatus(CommentStatus.APPROVED);
// TODO: 发布评论通过事件(Phase 3 后续实现)
}

@Override
public void reject(CommentEntity comment, String reason) {
logStateTransition(comment, comment.getStatus(), CommentStatus.REJECTED);
comment.setStatus(CommentStatus.REJECTED);
// TODO: 记录拒绝原因(可存入 metadata JSON 字段)
}

@Override
public void deleteByAdmin(CommentEntity comment, String reason) {
logStateTransition(comment, comment.getStatus(), CommentStatus.ADMIN_DELETED);
comment.setStatus(CommentStatus.ADMIN_DELETED);
// TODO: 记录删除原因
}

@Override
public String getStateName() {
return "待审核";
}
}

ApprovedState(已通过状态)

package com.blog.comment.domain.state;

import com.blog.comment.api.enums.CommentStatus;
import com.blog.comment.domain.entity.CommentEntity;
import org.springframework.stereotype.Component;

/**
* 已通过状态
*
* @author liusxml
* @since 1.3.0
*/
@Component
public class ApprovedState extends AbstractCommentState {

@Override
public void deleteByUser(CommentEntity comment) {
logStateTransition(comment, comment.getStatus(), CommentStatus.USER_DELETED);
comment.setStatus(CommentStatus.USER_DELETED);
}

@Override
public void deleteByAdmin(CommentEntity comment, String reason) {
logStateTransition(comment, comment.getStatus(), CommentStatus.ADMIN_DELETED);
comment.setStatus(CommentStatus.ADMIN_DELETED);
}

@Override
public String getStateName() {
return "已通过";
}
}

RejectedState(已拒绝状态)

package com.blog.comment.domain.state;

import org.springframework.stereotype.Component;

/**
* 已拒绝状态(终态,不允许任何转换)
*
* @author liusxml
* @since 1.3.0
*/
@Component
public class RejectedState extends AbstractCommentState {

@Override
public String getStateName() {
return "已拒绝";
}
}

DeletedState(已删除状态)

package com.blog.comment.domain.state;

import org.springframework.stereotype.Component;

/**
* 已删除状态(终态,不允许任何转换)
*
* @author liusxml
* @since 1.3.0
*/
@Component
public class DeletedState extends AbstractCommentState {

@Override
public String getStateName() {
return "已删除";
}
}

2.4 创建状态工厂

文件路径: blog-comment-service/src/main/java/com/blog/comment/domain/state/CommentStateFactory.java

package com.blog.comment.domain.state;

import com.blog.comment.api.enums.CommentStatus;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;

import java.util.Map;

/**
* 评论状态工厂
*
* <p>根据评论状态返回对应的状态处理器</p>
*
* @author liusxml
* @since 1.3.0
*/
@Component
@RequiredArgsConstructor
public class CommentStateFactory {

private final PendingState pendingState;
private final ApprovedState approvedState;
private final RejectedState rejectedState;
private final DeletedState deletedState;

/**
* 状态映射表
*/
private Map<CommentStatus, CommentState> getStateMap() {
return Map.of(
CommentStatus.PENDING, pendingState,
CommentStatus.APPROVED, approvedState,
CommentStatus.REJECTED, rejectedState,
CommentStatus.USER_DELETED, deletedState,
CommentStatus.ADMIN_DELETED, deletedState
);
}

/**
* 根据状态获取对应的处理器
*
* @param status 评论状态
* @return 状态处理器
*/
public CommentState getState(CommentStatus status) {
return getStateMap().get(status);
}
}

知识点

  • 使用 Map 映射状态和处理器,避免 switch
  • Spring 自动注入所有状态类实例
  • 工厂模式 + 状态模式组合使用

🏛️ Step 3: Service 层集成

3.1 扩展 Entity

文件路径: blog-comment-service/src/main/java/com/blog/comment/domain/entity/CommentEntity.java

// 在 CommentEntity 中添加:

/**
* 获取当前状态处理器
*/
@TableField(exist = false)
@JsonIgnore
private transient CommentState state;

3.2 扩展 Service 接口

文件路径: blog-comment-service/src/main/java/com/blog/comment/service/ICommentService.java

/**
* 审核通过评论
*
* @param id 评论ID
*/
void approveComment(Long id);

/**
* 审核拒绝评论
*
* @param id 评论ID
* @param reason 拒绝原因
*/
void rejectComment(Long id, String reason);

/**
* 用户删除评论
*
* @param id 评论ID
*/
void deleteCommentByUser(Long id);

/**
* 管理员删除评论
*
* @param id 评论ID
* @param reason 删除原因
*/
void deleteCommentByAdmin(Long id, String reason);

3.3 实现 Service

文件路径: blog-comment-service/src/main/java/com/blog/comment/service/impl/CommentServiceImpl.java

private final CommentStateFactory stateFactory;

// 在构造函数中注入
public CommentServiceImpl(CommentConverter converter, CommentStateFactory stateFactory) {
super(converter);
this.stateFactory = stateFactory;
this.treeBuilder = new TreeBuilder<>(
CommentTreeVO::getId,
CommentTreeVO::getParentId,
CommentTreeVO::setChildren
);
}

@Override
@Transactional(rollbackFor = Exception.class)
public void approveComment(Long id) {
CommentEntity comment = getById(id);
if (comment == null) {
throw new BusinessException(SystemErrorCode.NOT_FOUND, "评论不存在");
}

// 获取状态处理器并执行审核通过操作
CommentState state = stateFactory.getState(comment.getStatus());
state.approve(comment);

// 更新数据库
updateById(comment);
}

@Override
@Transactional(rollbackFor = Exception.class)
public void rejectComment(Long id, String reason) {
CommentEntity comment = getById(id);
if (comment == null) {
throw new BusinessException(SystemErrorCode.NOT_FOUND, "评论不存在");
}

CommentState state = stateFactory.getState(comment.getStatus());
state.reject(comment, reason);

updateById(comment);
}

@Override
@Transactional(rollbackFor = Exception.class)
public void deleteCommentByUser(Long id) {
CommentEntity comment = getById(id);
if (comment == null) {
throw new BusinessException(SystemErrorCode.NOT_FOUND, "评论不存在");
}

// 验证是否为当前用户的评论
Long currentUserId = SecurityUtils.getCurrentUserId();
if (!comment.getCreateBy().equals(currentUserId)) {
throw new BusinessException(SystemErrorCode.ACCESS_DENIED, "无权删除他人评论");
}

CommentState state = stateFactory.getState(comment.getStatus());
state.deleteByUser(comment);

updateById(comment);
}

@Override
@Transactional(rollbackFor = Exception.class)
public void deleteCommentByAdmin(Long id, String reason) {
CommentEntity comment = getById(id);
if (comment == null) {
throw new BusinessException(SystemErrorCode.NOT_FOUND, "评论不存在");
}

CommentState state = stateFactory.getState(comment.getStatus());
state.deleteByAdmin(comment, reason);

updateById(comment);
}

🎮 Step 4: Controller 层实现

4.1 创建审核 DTO

文件路径: blog-comment-api/src/main/java/com/blog/comment/api/dto/CommentAuditDTO.java

package com.blog.comment.api.dto;

import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;

import java.io.Serializable;

/**
* 评论审核 DTO
*
* @author liusxml
* @since 1.3.0
*/
@Data
@Schema(description = "评论审核对象")
public class CommentAuditDTO implements Serializable {

@Schema(description = "拒绝/删除原因", example = "内容违规")
@NotBlank(message = "原因不能为空")
private String reason;
}

4.2 创建审核 Controller

文件路径: blog-comment-service/src/main/java/com/blog/comment/controller/CommentAuditController.java

package com.blog.comment.controller;

import com.blog.comment.api.dto.CommentAuditDTO;
import com.blog.comment.service.ICommentService;
import com.blog.common.model.Result;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;

/**
* 评论审核 Controller(管理员专用)
*
* @author liusxml
* @since 1.3.0
*/
@Slf4j
@RestController
@RequestMapping("/api/v1/comments/audit")
@RequiredArgsConstructor
@Tag(name = "评论审核管理", description = "管理员审核评论接口")
public class CommentAuditController {

private final ICommentService commentService;

/**
* 审核通过评论
*/
@PostMapping("/{id}/approve")
@Operation(summary = "审核通过评论")
@PreAuthorize("hasRole('ADMIN')")
public Result<Void> approve(@PathVariable Long id) {
commentService.approveComment(id);
return Result.success();
}

/**
* 审核拒绝评论
*/
@PostMapping("/{id}/reject")
@Operation(summary = "审核拒绝评论")
@PreAuthorize("hasRole('ADMIN')")
public Result<Void> reject(
@PathVariable Long id,
@Valid @RequestBody CommentAuditDTO dto) {
commentService.rejectComment(id, dto.getReason());
return Result.success();
}

/**
* 管理员删除评论
*/
@DeleteMapping("/{id}")
@Operation(summary = "管理员删除评论")
@PreAuthorize("hasRole('ADMIN')")
public Result<Void> deleteByAdmin(
@PathVariable Long id,
@Valid @RequestBody CommentAuditDTO dto) {
commentService.deleteCommentByAdmin(id, dto.getReason());
return Result.success();
}
}

4.3 扩展用户 Controller

文件路径: blog-comment-service/src/main/java/com/blog/comment/controller/CommentController.java

/**
* 用户删除自己的评论
*/
@DeleteMapping("/{id}")
@Operation(summary = "删除评论")
public Result<Void> delete(@PathVariable Long id) {
commentService.deleteCommentByUser(id);
return Result.success();
}

✅ Phase 3 Step 1-4 完成检查清单

  • 扩展 CommentStatus 枚举(新增 USER_DELETED、ADMIN_DELETED)
  • 创建 CommentState 接口
  • 创建 AbstractCommentState 抽象类
  • 实现 PendingState、ApprovedState、RejectedState、DeletedState
  • 创建 CommentStateFactory 工厂类
  • Service 层集成状态模式
  • 创建 CommentAuditDTO
  • 创建 CommentAuditController(管理员专用)
  • 测试状态流转(PENDING → APPROVED)
  • 测试非法状态转换抛出异常
  • 测试权限控制(@PreAuthorize)

💡 关键知识点总结

知识点核心概念实际作用
状态模式对象行为随状态改变消除 if-else,符合开闭原则
抽象类提供通用实现避免重复代码
工厂模式创建状态对象解耦状态获取逻辑
@PreAuthorize方法级权限控制限制管理员专用接口
@Transactional事务控制确保状态更新原子性

🎯 下一步

完成 Phase 3 Step 1-4 后,继续学习:

  • Spring Event 事件驱动(评论审核通知)
  • 审核历史记录(记录每次状态变更)
  • 缓存失效策略(状态变更后清理缓存)

准备好后继续 Phase 3 Step 5-7 的实现!🚀