4. 后端实体类与DTO设计:使用JPA/Hibernate定义实体,DTO与VO分层设计

好,咱们直接进入正题。营销活动管理模块,说白了就是一堆活动规则、参与记录、奖励发放这些东西在数据库里怎么存、怎么传。我见过太多项目,实体类和DTO混在一起用,最后改一个字段要改七八个地方,那叫一个酸爽。

今天我就把我在几个电商促销项目里踩过的坑、总结的经验,一次性给你讲透。

4.1 实体类设计:JPA/Hibernate 的正确姿势

先说说实体类。你用 JPA 还是 MyBatis?我个人习惯用 JPA + Hibernate,因为对于营销活动这种关联关系复杂的场景,ORM 的懒加载和级联操作能省不少事。

但注意,别滥用!

我曾经在一个项目中,把活动、规则、奖品、用户参与记录全部用 @OneToMany 级联起来。结果查询一个活动,Hibernate 给我拉了 6 张表的数据,接口响应时间直接飙到 3 秒。后来全改成手动查询,才降到 200 毫秒。

来看一个典型的营销活动实体类设计:

@Entity
@Table(name = "marketing_activity")
public class MarketingActivity {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false, length = 100)
    private String activityName;        // 活动名称

    @Column(nullable = false)
    @Enumerated(EnumType.STRING)
    private ActivityType activityType;  // 活动类型:满减、折扣、赠品

    @Column(nullable = false)
    private LocalDateTime startTime;    // 开始时间

    @Column(nullable = false)
    private LocalDateTime endTime;      // 结束时间

    @Column(nullable = false)
    @Enumerated(EnumType.STRING)
    private ActivityStatus status;      // 状态:草稿、进行中、已结束

    @Column(columnDefinition = "TEXT")
    private String rules;               // JSON 格式的活动规则

    @Column(nullable = false)
    private Integer maxParticipantCount; // 最大参与人数

    @Column(nullable = false)
    private Integer currentParticipantCount = 0; // 当前参与人数

    @Version
    private Integer version;            // 乐观锁,防止并发超卖

    @CreatedDate
    private LocalDateTime createdTime;

    @LastModifiedDate
    private LocalDateTime updatedTime;

    // getter/setter 省略
}

这里有几个关键点我要强调一下:

  • 用 @Version 做乐观锁:营销活动经常有并发问题,比如 100 个人同时参与,库存只有 50 份。没有乐观锁,你就等着超卖吧。
  • 枚举用字符串存:@Enumerated(EnumType.STRING) 而不是 ORDINAL。为什么?因为枚举顺序一变,数据库里的数字就全乱了。我吃过这个亏。
  • 规则字段用 TEXT 存 JSON:活动规则千变万化,你不可能为每种规则建一张表。存 JSON 字符串,灵活又简单。

4.2 DTO 与 VO 分层设计:别再偷懒了

很多新手喜欢直接把实体类返回给前端。你想想看,实体类里可能包含密码、内部状态、版本号这些敏感信息,直接暴露出去不是找事吗?

我建议的分层是这样的:

层级 用途 典型场景
Entity 数据库映射,包含所有字段 ORM 操作、持久化
DTO 数据传输,按需裁剪字段 Service 层返回给 Controller
VO 视图对象,前端展示专用 Controller 返回给前端

说白了,DTO 是给后端内部用的,VO 是给前端看的。举个例子:

// DTO:后端内部传输用
public class ActivityDTO {
    private Long id;
    private String activityName;
    private LocalDateTime startTime;
    private LocalDateTime endTime;
    private String rules;           // 包含完整规则 JSON
    private Integer version;        // 乐观锁版本号,后端校验用
}

// VO:返回给前端
public class ActivityVO {
    private Long id;
    private String activityName;
    private String startTime;       // 格式化后的字符串
    private String endTime;
    private String statusDesc;      // 状态的中文描述
    private String ruleSummary;     // 规则摘要,前端展示用
}

你看,VO 里我把时间格式化了,把状态转成了中文,还加了个规则摘要。这些在 DTO 里都不需要。

我的一个小技巧:用 MapStruct 做对象转换,比手写 BeanUtils.copyProperties 快 10 倍,而且类型安全。我在项目中一直用这个。

4.3 避坑指南:我踩过的那些坑

嗯,这里我要多说几句。实体类和 DTO 设计看似简单,但坑不少。

  • 别在实体类里写业务逻辑:实体类只负责数据映射,业务逻辑放到 Service 层。我曾经见过有人在实体类里写满减计算,结果一改需求,所有地方都要改。
  • DTO 不要直接引用实体类:DTO 里不要出现 @Entity 注解的类作为字段。否则序列化时会出问题,而且破坏了分层。
  • VO 里不要有敏感字段:比如用户手机号、内部状态码。前端不需要这些。

核心原则:每一层只关心自己该关心的事。实体类关心数据库,DTO 关心传输,VO 关心展示。各司其职,才能好维护。

4.4 实战:一个完整的转换流程

最后,我给你看一个完整的 Service 方法,你就明白怎么用了:

@Service
public class ActivityService {

    @Autowired
    private ActivityRepository activityRepository;

    @Autowired
    private ActivityMapper activityMapper;  // MapStruct 生成的转换器

    public ActivityVO getActivityDetail(Long activityId) {
        // 1. 查询实体
        MarketingActivity entity = activityRepository.findById(activityId)
            .orElseThrow(() -> new BusinessException("活动不存在"));

        // 2. 转成 DTO(可能包含一些计算逻辑)
        ActivityDTO dto = activityMapper.toDTO(entity);
        dto.setRemainingParticipantCount(
            dto.getMaxParticipantCount() - dto.getCurrentParticipantCount()
        );

        // 3. 转成 VO(格式化展示数据)
        ActivityVO vo = activityMapper.toVO(dto);
        vo.setStatusDesc(ActivityStatusEnum.getDescByCode(dto.getStatus()));
        vo.setStartTime(dto.getStartTime().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm")));

        return vo;
    }
}

你看,每一步都很清晰。实体 -> DTO -> VO,各层职责明确。改前端展示?改 VO 就行。改数据库字段?改实体类就行。互不影响。

好了,这一章就到这里。下一章我会讲营销活动的规则引擎设计,那才是真正的硬核内容。到时候见。