概述
在项目中,我们使用不同的对象模型 来处理不同场景的数据,这是分层架构的重要体现。
为什么需要多种对象?
- 🔐 安全性:防止敏感数据泄露
- 🎯 职责分离:每个对象只关注自己的职责
- 🔄 灵活性:不同层可以独立演化
- 🛡️ 解耦:数据库变动不影响前端
1️⃣ Entity(实体类)- 数据库映射
定义
Entity 是与数据库表 一一对应的 Java 对象,也叫持久化对象。
特点:
- 🗄️ 对应数据库表结构
- 📦 包含所有字段(包括敏感字段)
- 🔗 包含数据库注解(如 @Table、@Column)
- 💾 只在 DAO/Mapper 层使用
实战案例 - User 实体
java
package com.MiniBlog.entity;
import lombok.Data;
import javax.persistence.*;
import java.util.Date;
/**
* 用户实体类 - 对应数据库表 tb_user
*
* 【注解说明】
* @Entity:JPA注解,表示这是一个实体类
* @Table:指定对应的数据库表名
* @Data:Lombok注解,自动生成getter/setter/toString等
*/
@Data
@Entity
@Table(name = "tb_user")
public class User {
// ========== 【主键】 ==========
/**
* @Id:主键标识
* @GeneratedValue:主键生成策略(自增)
*/
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
// ========== 【基本信息】 ==========
/**
* 用户编号(业务主键)
*/
@Column(name = "no", length = 50)
private String no;
/**
* 真实姓名
*/
@Column(name = "realname", length = 100)
private String realname;
/**
* 手机号
*/
@Column(name = "mobile", length = 20)
private String mobile;
/**
* 邮箱
*/
@Column(name = "email", length = 100)
private String email;
// ========== 【敏感信息】⚠️ ==========
/**
* 密码(加密后的)
* 【注意】这个字段不应该返回给前端!
*/
@Column(name = "password", length = 200)
private String password;
/**
* 密码盐值
* 【注意】这个字段不应该返回给前端!
*/
@Column(name = "salt", length = 50)
private String salt;
/**
* 身份证号
* 【注意】需要脱敏后才能返回给前端!
*/
@Column(name = "cardno", length = 50)
private String cardno;
// ========== 【状态字段】 ==========
/**
* 账号状态:1-正常,2-冻结,3-注销
*/
@Column(name = "status")
private Integer status;
/**
* 是否删除:0-否,1-是
*/
@Column(name = "deleted")
private Integer deleted;
// ========== 【微信相关】 ==========
@Column(name = "openid", length = 100)
private String openid;
@Column(name = "unionid", length = 100)
private String unionid;
@Column(name = "mp_openid", length = 100)
private String mpOpenid;
// ========== 【人脸识别】 ==========
@Column(name = "faceid", length = 100)
private String faceid;
// ========== 【时间戳】 ==========
/**
* 创建时间
*/
@Column(name = "create_time")
@Temporal(TemporalType.TIMESTAMP)
private Date createTime;
/**
* 更新时间
*/
@Column(name = "update_time")
@Temporal(TemporalType.TIMESTAMP)
private Date updateTime;
/**
* 创建人
*/
@Column(name = "create_user")
private Integer createUser;
/**
* 更新人
*/
@Column(name = "update_user")
private Integer updateUser;
}
数据库表结构(对应):
sql
CREATE TABLE `tb_user` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`no` varchar(50) DEFAULT NULL COMMENT '用户编号',
`realname` varchar(100) DEFAULT NULL COMMENT '真实姓名',
`mobile` varchar(20) DEFAULT NULL COMMENT '手机号',
`email` varchar(100) DEFAULT NULL COMMENT '邮箱',
`password` varchar(200) DEFAULT NULL COMMENT '密码',
`salt` varchar(50) DEFAULT NULL COMMENT '盐值',
`cardno` varchar(50) DEFAULT NULL COMMENT '身份证号',
`status` int(11) DEFAULT '1' COMMENT '状态',
`deleted` int(11) DEFAULT '0' COMMENT '是否删除',
`openid` varchar(100) DEFAULT NULL COMMENT '微信openid',
`unionid` varchar(100) DEFAULT NULL COMMENT '微信unionid',
`mp_openid` varchar(100) DEFAULT NULL COMMENT '公众号openid',
`faceid` varchar(100) DEFAULT NULL COMMENT '人脸ID',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
`create_user` int(11) DEFAULT NULL COMMENT '创建人',
`update_user` int(11) DEFAULT NULL COMMENT '更新人',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_mobile` (`mobile`),
KEY `idx_no` (`no`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表';
2️⃣ VO(View Object)- 视图对象
定义
VO 是返回给前端的视图对象,只包含前端需要展示的数据。
特点:
- 👁️ 只包含前端需要的字段
- 🔒 不包含敏感字段(密码、盐值等)
- 🎨 可能包含计算字段(如年龄、格式化日期)
- 📤 只在 Controller 层返回给前端
实战案例 - UserVo
java
package com.MiniBlog.vo.user;
import lombok.Data;
import com.payslip.entity.UserCard;
import java.util.List;
/**
* 用户视图对象 - 返回给前端
*
* 【设计原则】
* 1. 只包含前端需要的字段
* 2. 敏感字段不包含(如password、salt)
* 3. 需要脱敏的字段已处理(如cardno)
* 4. 可以包含关联对象(如银行卡列表)
*/
@Data
public class UserVo {
// ========== 【基本信息】 ==========
private Integer id;
private String no;
private String realname;
/**
* 手机号(脱敏)
* 例如:138****5678
*/
private String mobile;
/**
* 邮箱(可能脱敏)
* 例如:abc***@qq.com
*/
private String email;
/**
* 身份证号(脱敏)
* 例如:320***********1234
*/
private String cardno;
// ========== 【注意】❌ 不包含这些字段 ==========
// private String password; // 密码不返回
// private String salt; // 盐值不返回
// ========== 【状态】 ==========
private Integer status;
/**
* 状态文本(前端显示用)
* 计算属性,根据 status 值生成
*/
private String statusText;
// ========== 【头像】 ==========
private String avatar;
// ========== 【微信信息】 ==========
/**
* 是否绑定微信
* 计算属性:openid 不为空则已绑定
*/
private Boolean hasWechat;
/**
* 是否绑定公众号
* 计算属性:mpOpenid 不为空则已绑定
*/
private Boolean hasMpWechat;
// ========== 【关联信息】 ==========
/**
* 用户的银行卡列表
* 这是关联查询的结果,Entity 中没有这个字段
*/
private List<UserCard> cards;
// ========== 【时间】 ==========
/**
* 创建时间(格式化后)
* 例如:2024-01-15 10:30:00
*/
private String createTime;
// ========== 【计算字段】 ==========
/**
* 账号年龄(天数)
* 根据创建时间计算,Entity 中没有这个字段
*/
private Integer accountAge;
/**
* 是否实名认证
* 根据 cardno 是否为空判断
*/
private Boolean isRealAuth;
}
3️⃣ DTO(Data Transfer Object)- 数据传输对象
定义
DTO 是服务间传输数据的对象,用于跨层或跨服务传递数据。
特点:
- 📡 用于 Service 层之间传递数据
- 🔄 用于微服务之间传递数据
- 📦 可能包含多个实体的数据
- 🎯 职责单一,只负责数据传输
实战案例 - UserTokenDTO
java
package com.MiniBlog.dto.user;
import lombok.Data;
import lombok.Builder;
/**
* 用户登录成功后返回的数据传输对象
*
* 【使用场景】
* - 用户注册成功
* - 用户登录成功
* - Token刷新成功
*/
@Data
@Builder
public class UserTokenDTO {
/**
* 用户ID
*/
private Integer userId;
/**
* 用户编号
*/
private String userNo;
/**
* 用户姓名
*/
private String userName;
/**
* 访问令牌(JWT Token)
* 前端需要保存,每次请求携带
*/
private String token;
/**
* Token过期时间(秒)
*/
private Long expiresIn;
/**
* 刷新令牌
* 用于Token过期后刷新
*/
private String refreshToken;
/**
* 是否首次登录
* 首次登录需要引导用户完善信息
*/
private Boolean firstLogin;
/**
* 用户头像
*/
private String avatar;
}
4️⃣ Form(表单对象)- 接收前端数据
定义
Form 是接收前端提交数据的对象,包含参数校验规则。
特点:
- 📥 只在 Controller 层接收前端数据
- ✅ 包含参数校验注解(@NotNull、@Size等)
- 🎯 一个接口一个Form,职责明确
- 🔒 校验规则集中管理
实战案例 - UserMobileRegisterDTO
java
package com.MiniBlog.form.user;
import lombok.Data;
import javax.validation.constraints.*;
/**
* 用户手机号注册表单
*
* 【使用场景】
* POST /user/mobile-register
*
* 【校验规则】
* - 手机号必填,格式正确
* - 验证码必填,6位数字
* - 密码必填,6-20位
*/
@Data
public class UserMobileRegisterDTO {
/**
* 手机号
*
* @NotBlank:不能为空(会自动trim)
* @Pattern:正则校验
*/
@NotBlank(message = "手机号不能为空")
@Pattern(
regexp = "^1[3-9]\\d{9}$",
message = "手机号格式不正确"
)
private String mobile;
/**
* 短信验证码
*/
@NotBlank(message = "验证码不能为空")
@Size(min = 4, max = 6, message = "验证码长度为4-6位")
private String code;
/**
* 密码
*/
@NotBlank(message = "密码不能为空")
@Size(min = 6, max = 20, message = "密码长度为6-20位")
private String password;
/**
* 邀请码(可选)
*/
private String inviteCode;
}
5️⃣ 对象之间的转换流程
完整数据流转图
sql
┌─────────────────────────────────────────────────────────────────┐
│ 前端(浏览器/APP) │
└────────────┬────────────────────────────────────────┬────────────┘
│ │
│ ① 发送请求 │ ⑥ 接收响应
│ JSON: {mobile, code, password} │ JSON: {userId, token, userName}
▼ ▲
┌─────────────────────────────────────────────────────────────────┐
│ Controller 层(控制器) │
│ @PostMapping("/register") │
│ public ApiResponse<UserTokenDTO> register( │
│ @Valid @RequestBody UserMobileRegisterDTO form) { │
│ │
│ ② Form 接收参数 ⑤ VO 返回给前端 │
│ └─> 自动校验(@Valid) └─> Entity 转 VO │
│ │
│ UserTokenDTO dto = userService.register(form); │
│ return ApiResponse.ok(dto); │
│ } │
└────────────┬────────────────────────────────────────┬────────────┘
│ │
│ ③ Form 传给 Service │ ④ DTO 返回
▼ ▲
┌─────────────────────────────────────────────────────────────────┐
│ Service 层(业务逻辑) │
│ public UserTokenDTO register(UserMobileRegisterDTO form) { │
│ │
│ // 1. 校验验证码 │
│ validateCode(form.getMobile(), form.getCode()); │
│ │
│ // 2. 创建 Entity 对象 │
│ User user = new User(); │
│ user.setMobile(form.getMobile()); │
│ user.setPassword(encryptPassword(form.getPassword())); │
│ user.setCreateTime(new Date()); │
│ │
│ // 3. 保存到数据库 │
│ userDao.save(user); ────┐ │
│ │ │
│ // 4. 生成 Token │
│ String token = generateToken(user.getId()); │
│ │
│ // 5. 构建 DTO 返回 │
│ return UserTokenDTO.builder() │
│ .userId(user.getId()) │
│ .token(token) │
│ .userName(user.getRealname()) │
│ .build(); │
│ } │ │
└───────────────────────────────┼─────────────────────────────────┘
│
│ Entity 保存/查询
▼
┌─────────────────────────────────────────────────────────────────┐
│ DAO/Mapper 层(数据访问) │
│ │
│ public interface UserRepository { │
│ User save(User user); // 保存 │
│ User findById(Integer id); // 查询 │
│ } │
└────────────┬────────────────────────────────────────┬────────────┘
│ │
│ SQL 语句 │ 查询结果
▼ ▲
┌─────────────────────────────────────────────────────────────────┐
│ 数据库(MySQL) │
│ │
│ tb_user 表 │
│ ┌────┬──────┬──────────┬──────────┬──────────┐ │
│ │ id │ no │ mobile │ password │ salt │ │
│ ├────┼──────┼──────────┼──────────┼──────────┤ │
│ │ 1 │ U001 │ 13800000 │ ******* │ ******* │ │
│ └────┴──────┴──────────┴──────────┴──────────┘ │
└─────────────────────────────────────────────────────────────────┘
6️⃣ 对象转换代码示例
实际项目中的转换
java
@RestController
@RequestMapping("/user")
@Slf4j
public class UserController extends BaseController {
@Autowired
private UserService userService;
@Autowired
private UserCardService userCardService;
/**
* 根据Token获取用户信息
*
* 【数据流转】
* 1. 从数据库查询 Entity(包含所有字段)
* 2. Entity 转换为 VO(只包含安全字段)
* 3. 关联查询银行卡(补充数据)
* 4. 返回 VO 给前端
*/
@GetMapping("/findByToken")
public ApiResponse<UserVo> findByToken() {
// ========== ① 查询 Entity ==========
Integer userId = LoginContext.getUserId();
User user = userService.findById(userId); // Entity 对象
// 校验用户存在
Asserts.notNull(user, -10001, "登录失效");
// ========== ② Entity 转 VO ==========
// 使用 BeanUtil 复制属性(只复制同名字段)
UserVo userVo = BeanUtil.copyProperties(user, UserVo.class);
// ========== ③ 补充数据 ==========
// 查询关联的银行卡列表
List<UserCard> cards = userCardService.findByUserId(user.getId());
userVo.setCards(cards);
// 设置计算字段
userVo.setHasWechat(user.getOpenid() != null);
userVo.setIsRealAuth(user.getCardno() != null);
// 脱敏处理
if (user.getMobile() != null) {
userVo.setMobile(desensitizeMobile(user.getMobile()));
}
if (user.getCardno() != null) {
userVo.setCardno(desensitizeCardNo(user.getCardno()));
}
// ========== ④ 返回 VO ==========
return ApiResponse.ok(userVo);
}
/**
* 手机号脱敏
* 13800138000 -> 138****8000
*/
private String desensitizeMobile(String mobile) {
if (mobile == null || mobile.length() != 11) {
return mobile;
}
return mobile.substring(0, 3) + "****" + mobile.substring(7);
}
/**
* 身份证号脱敏
* 320102199001011234 -> 320***********1234
*/
private String desensitizeCardNo(String cardNo) {
if (cardNo == null || cardNo.length() < 8) {
return cardNo;
}
return cardNo.substring(0, 3) + "***********" + cardNo.substring(cardNo.length() - 4);
}
}
使用工具类转换(推荐)
java
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.bean.copier.CopyOptions;
/**
* 对象转换工具类
*/
public class BeanConverter {
/**
* Entity 转 VO
* 自动忽略 null 值
*/
public static <T> T toVo(Object source, Class<T> targetClass) {
if (source == null) {
return null;
}
return BeanUtil.copyProperties(source, targetClass,
CopyOptions.create().ignoreNullValue());
}
/**
* Entity List 转 VO List
*/
public static <S, T> List<T> toVoList(List<S> sourceList, Class<T> targetClass) {
if (sourceList == null || sourceList.isEmpty()) {
return Collections.emptyList();
}
return sourceList.stream()
.map(source -> toVo(source, targetClass))
.collect(Collectors.toList());
}
/**
* Form 转 Entity
*/
public static <T> T toEntity(Object form, Class<T> entityClass) {
if (form == null) {
return null;
}
T entity = BeanUtil.copyProperties(form, entityClass);
// 设置创建时间等默认字段
if (entity instanceof BaseEntity) {
((BaseEntity) entity).setCreateTime(new Date());
}
return entity;
}
}
// 使用示例
@Service
public class UserServiceImpl implements UserService {
@Override
public UserVo getUserInfo(Integer userId) {
// 1. 查询 Entity
User user = userDao.findById(userId);
// 2. 转换为 VO
UserVo vo = BeanConverter.toVo(user, UserVo.class);
// 3. 补充额外数据
vo.setCards(userCardService.findByUserId(userId));
return vo;
}
@Override
public List<UserVo> getUserList(List<Integer> userIds) {
// 1. 批量查询 Entity
List<User> users = userDao.findByIds(userIds);
// 2. 批量转换为 VO
return BeanConverter.toVoList(users, UserVo.class);
}
}
7️⃣ 为什么要分这么多对象?
核心原因
1. 安全性
java
// ❌ 错误:直接返回 Entity
@GetMapping("/user/{id}")
public ApiResponse<User> getUser(@PathVariable Integer id) {
User user = userService.findById(id);
return ApiResponse.success(user); // 密码、盐值都返回了!
}
// 前端收到的数据(危险!)
{
"id": 123,
"mobile": "13800138000",
"password": "e10adc3949ba59abbe56e057f20f883e", // MD5密码
"salt": "a1b2c3d4", // 盐值暴露
"cardno": "320102199001011234" // 身份证明文
}
// ✅ 正确:返回 VO
@GetMapping("/user/{id}")
public ApiResponse<UserVo> getUser(@PathVariable Integer id) {
User user = userService.findById(id);
UserVo vo = BeanUtil.copyProperties(user, UserVo.class);
// VO 中没有 password、salt 字段
return ApiResponse.success(vo);
}
// 前端收到的数据(安全)
{
"id": 123,
"mobile": "138****8000", // 脱敏
"cardno": "320***********1234" // 脱敏
}
2. 解耦
java
// 数据库表结构变更,不影响前端
// 数据库改了字段名:user_name -> real_name
@Entity
public class User {
@Column(name = "real_name") // 数据库字段改了
private String realname; // Java字段不变
}
// VO 不变,前端不受影响
public class UserVo {
private String realname; // 前端继续用原来的字段
}
3. 灵活性
java
// VO 可以包含 Entity 没有的计算字段
public class UserVo {
private Integer id;
private String realname;
// ========== 【计算字段】 ==========
// Entity 中没有这些字段
private Integer age; // 根据生日计算
private String statusText; // 根据status转换(1->正常,2->冻结)
private Boolean isVip; // 根据会员等级判断
private String createTimeText; // 格式化日期(2024-01-15)
// ========== 【关联数据】 ==========
// Entity 中没有这些字段
private List<UserCard> cards; // 关联的银行卡
private Integer orderCount; // 订单数量(统计)
private BigDecimal totalAmount; // 总消费金额(统计)
}
4. 职责分离
java
// 每个对象都有明确的职责
Entity: 负责与数据库交互
↓
DTO: 负责在Service层传递数据
↓
VO: 负责返回给前端展示
Form: 负责接收前端提交的数据
↓
DTO: 负责在Service层传递数据
↓
Entity: 负责保存到数据库
8️⃣ 实战完整流程示例
用户注册完整流程
java
// ========== ① 前端提交表单 ==========
// POST /user/register
// Body: { "mobile": "13800138000", "code": "1234", "password": "abc123" }
// ========== ② Controller 接收 Form ==========
@RestController
@RequestMapping("/user")
public class UserController {
@Autowired
private UserService userService;
@PostMapping("/register")
public ApiResponse<UserTokenDTO> register(
@Valid @RequestBody UserMobileRegisterDTO form) { // Form 对象
log.info("用户注册: mobile={}", form.getMobile());
// 调用 Service,传递 Form,接收 DTO
UserTokenDTO dto = userService.register(form);
// 返回 DTO 给前端
return ApiResponse.ok(dto);
}
}
// ========== ③ Service 处理业务逻辑 ==========
@Service
@Slf4j
public class UserServiceImpl implements UserService {
@Autowired
private UserRepository userDao;
@Autowired
private SmsService smsService;
@Override
@Transactional(rollbackFor = Exception.class)
public UserTokenDTO register(UserMobileRegisterDTO form) {
// 1. 校验验证码
boolean valid = smsService.validateCode(form.getMobile(), form.getCode());
Asserts.isTrue(valid, "验证码错误");
// 2. 检查手机号是否已注册
User existUser = userDao.findByMobile(form.getMobile());
Asserts.isNull(existUser, "手机号已注册");
// 3. Form 转 Entity
User user = new User();
user.setNo(generateUserNo()); // 生成用户编号
user.setMobile(form.getMobile()); // 手机号
user.setRealname("用户" + user.getNo()); // 默认昵称
// 4. 密码加密
String salt = UUID.randomUUID().toString();
String encryptedPwd = Secure.encryptPassword(form.getPassword(), salt);
user.setSalt(salt);
user.setPassword(encryptedPwd);
// 5. 设置默认值
user.setStatus(1); // 正常
user.setDeleted(0); // 未删除
user.setCreateTime(new Date());
user.setUpdateTime(new Date());
// 6. 保存到数据库(Entity)
userDao.save(user);
log.info("用户注册成功: userId={}", user.getId());
// 7. 生成 Token
String token = JwtUtil.generateToken(user.getId(), user.getNo());
// 8. 构建 DTO 返回
return UserTokenDTO.builder()
.userId(user.getId())
.userNo(user.getNo())
.userName(user.getRealname())
.token(token)
.expiresIn(7200L) // 2小时
.firstLogin(true)
.build();
}
private String generateUserNo() {
return "U" + System.currentTimeMillis();
}
}
// ========== ④ DAO 保存 Entity ==========
@Repository
public interface UserRepository extends JpaRepository<User, Integer> {
/**
* 根据手机号查询用户
*/
User findByMobile(String mobile);
/**
* 根据用户编号查询
*/
User findByNo(String no);
}
// ========== ⑤ 数据库存储 ==========
// INSERT INTO tb_user
// (no, mobile, realname, password, salt, status, deleted, create_time, update_time)
// VALUES
// ('U1704528000123', '13800138000', '用户U1704528000123',
// 'e10adc3949ba59abbe56e057f20f883e', 'a1b2c3d4', 1, 0, NOW(), NOW());
// ========== ⑥ 返回给前端 ==========
// Response:
{
"code": 0,
"message": "成功",
"data": {
"userId": 123,
"userNo": "U1704528000123",
"userName": "用户U1704528000123",
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"expiresIn": 7200,
"firstLogin": true
}
}
9️⃣ 最佳实践与规范
推荐做法
1. Entity 使用规范
java
✅ DO:
- 只在 DAO/Mapper 层使用
- 字段名与数据库列名对应
- 包含完整的数据库注解
- 不要有业务逻辑代码
❌ DON'T:
- 不要在 Controller 中直接返回 Entity
- 不要在 Entity 中写复杂的方法
- 不要让前端知道 Entity 的结构
2. VO 使用规范
java
✅ DO:
- 只包含前端需要的字段
- 敏感字段要脱敏
- 可以包含计算字段
- 可以包含关联数据
- 只在 Controller 返回时使用
❌ DON'T:
- 不要包含密码、盐值等敏感字段
- 不要包含数据库注解
- 不要在 Service 层使用
3. DTO 使用规范
java
✅ DO:
- 在 Service 层之间传递数据
- 在微服务之间传递数据
- 可以包含多个 Entity 的数据
- 可以包含业务状态信息
❌ DON'T:
- 不要直接暴露给前端
- 不要包含数据库操作
4. Form 使用规范
java
✅ DO:
- 只在 Controller 接收前端数据
- 包含完整的校验注解
- 一个接口一个 Form
- 字段命名清晰
❌ DON'T:
- 不要在 Service 层定义 Form
- 不要在 Form 中写业务逻辑
🔟 对比总结表
|------------|--------|------------|----------------|----------|
| 对象类型 | 作用 | 使用层级 | 主要特点 | 是否包含敏感字段 |
| Entity | 数据库映射 | DAO/Mapper | 与表一一对应,包含数据库注解 | ✅ 是 |
| VO | 返回给前端 | Controller | 只包含展示字段,脱敏处理 | ❌ 否 |
| DTO | 服务间传输 | Service | 跨层传输数据,业务对象 | 🔸 视情况而定 |
| Form | 接收前端数据 | Controller | 包含校验注解,参数验证 | 🔸 可能包含 |