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 的实现!🚀