
1. 项目概述一个看起来很简单做起来全是细节的“实体抽取”先解释下这个标题里藏的东西。Day02-08说明这是一次系统性学习/开发计划里的某一天第二节课11:34是时间戳这种命名方式特别像我们团队内部做视频课程或者写技术笔记时用的标题规则。但真正核心的是后面半句“分析产品原型抽取QUERY、DTO、VO实体”。这句话对很多后端开发来说看似不起眼甚至有人觉得这有什么好讲的拿到原型对着页面字段建类不就完了我当年在第一个项目组也是这么想的直到后来被一个几千行代码的巨型实体类坑得体无完肤才明白“实体抽取”这四个字本质上是整个后端架构设计中非常容易被低估、却决定了后续开发效率的关键动作。这篇文章面向的读者是初级/中级后端开发者尤其是那些正在做电商、管理系统、企业级SaaS项目的朋友。你们一定见过这种场景产品经理扔过来一版Axure原型上面画了列表页、搜索区、弹窗表单、详情页然后说“后端照着做就行”。看起来没什么技术含量但真正动手时你才会发现是全是Query类还是DTO还是VO哪些字段放哪个类字段类型用什么校验规则放哪里要不要嵌对象这些决策如果没有提前想清楚等开始写Controller、Service、DAO的时候就会一边写一边返工改到怀疑人生。我希望通过这篇文章把我在多个项目里“分析原型→抽取三类实体”的完整思路、踩坑记录和最终沉淀下来的方法尽量完整地分享出来。你说一篇博客能不能帮你完全解决架构问题那肯定不可能但至少下次你拿到原型图时心里会有一条清晰的拆解路径先看什么、再分什么、最后怎么落代码。2. 为什么要把实体拆成QUERY、DTO、VO三套而不是一套走天下其实这是所有新人最先问的问题。原型上就是一堆字段订单号、用户姓名、金额、状态、日期……为什么不能直接定义一个Order实体前后端都用它我也曾经这么干过结果就是前端需要展示“用户昵称用户手机号”数据库订单表里只有userId前端要传“开始时间结束时间”做筛选订单实体里根本没有这俩字段状态字段数据库存的是Integer前端要的是“已支付/待发货”这种中文文本于是你只能在实体里额外加一个statusDesc还得写上忽略序列化的注解来防止它被存进数据库。这种“一个实体到处用”的做法用两周就会让你后悔。因为三层模型里的三个角色各自服务的场景完全不同硬融在一起必然导致职责混乱。先看QUERY对象。它永远只服务于“查询”这个场景具体一点就是接收前端传过来的查询条件参数。一个典型的管理后台列表页往往有搜索框、下拉框、时间范围选择器、分页控件这些东西对应的就是一整个查询条件集合。QUERY对象存在的意义就是把这一坨散乱的参数变成一个结构化的请求对象。它可以不带业务逻辑但必须包含所有可能的筛选条件和分页参数。注意QUERY对象是要直接暴露给前端接口的所以它的字段设计要考虑前端传参习惯比如时间范围通常是一个startTime和一个endTime分开传而不是一个数组。再看DTO。DTO的全称叫Data Transfer Object中文是数据传输对象。它核心的价值在于“跨层传输”尤其是后端内部各层之间或者服务与服务之间传递的完整数据。我通常将DTO拆成两类入参DTO比如创建订单时的表单数据和出参DTO比如Service层组装完成、准备交给Controller的结果数据。入参DTO对应的是“前端提交到后端”的数据结构它要负责承载参数校验逻辑出参DTO对应的是“后端内部真正处理完”的数据快照它可以包含各种计算后的字段它不关心前端怎么展示。最后是VO。VO的全称是View Object视图对象它只服务于“前端展示”。同一个订单数据在列表页显示一行截断的信息在详情页显示全字段在导出Excel时又可能是另一种形态——这三种形态如果都用一个类那么这个类的字段会越来越多最终导致改一个字段影响三个页面。VO的价值就是让后端返回的数据长成前端最希望看到的样子字段名都替前端想好嵌套层级也直接对应页面需要。一次接口返回一个VO前端拿来即用这就是理想状态。用一句话说清楚这三者的关系前端传什么参数用QUERY接收业务处理过程碰什么数据用DTO承载前端最终看什么内容用VO返回。如果一个字段同时出现在多个对象里那没有任何问题但每个对象的字段增减理由必须是独立的——QUERY因为查询条件变化而变DTO因为业务逻辑调整而变VO因为页面展示需求而变。当这三个对象各管各的事时你改列表页只动VO改搜索条件只动QUERY它们互不干扰才不会牵一发动全身。3. 拿到产品原型第一步不是写代码而是“读页面”很多新手拿到原型后第一件事就是打开IDE新建Java类这是一个非常危险的信号。原型不只是给你看有哪些字段的它更像是一份“需求说明书”只不过这份说明书是图形化的。你需要先花十几分钟把原型完整读一遍搞清楚它到底包含了几类页面、每类页面的数据流向是什么然后才谈得上抽取实体。我个人的习惯是把产品原型里涉及的页面分成三类列表/查询页、表单提交页、详情展示页。这个分类看起来简单但它直接决定了你会抽出哪些类型的实体。列表页必然产生QUERY和列表VO表单页必然产生入参DTO详情页必然产生详情VO。如果原型里还有导入导出功能那还要考虑出参DTO或者专门的Excel模型对象——这部分很多教程不会提但实际项目里非常常见。举个例子有一次我们做的是一个生鲜电商的订单管理后台产品原型大概有六个页面订单列表、订单详情、退款审核、发货管理、用户管理、数据看板。如果我直接开始抽取很可能对着“订单列表”一顿操作抽出几十个字段扔进一个类里然后写接口时才发现订单详情页需要展示用户地址但列表页不需要退款审核页需要展示退款原因和凭证图但订单详情页根本不放这些。正确做法是先把这六个页面的数据流画出来哪些接口属于查询类接口哪些属于提交类接口哪些属于批量操作类接口然后针对每一类接口去定义对应的入参和出参。在这个阶段我强烈建议你拿一张纸或者用思维导图把每个页面上的字段抄下来然后逐一标注它的归属“出现在搜索区→QUERY候选出现在表单→DTO候选出现在表格列或详情文字→VO候选”。这个过程看似原始但特别有效它能让你在写代码之前就发现字段冲突。比如同样叫“状态”搜索区里前端传的是Integer的status列表展示里返回的是String的statusDesc那么你就要在QUERY里定义一个Integer字段在VO里定义一个String字段而不是试图用同一个字段兼容两种类型。还有一点容易被忽略原型里写着“客户姓名”的地方不一定就是直接存储的字段。如果系统里用户信息是另一个服务负责的那么订单表里可能只有userId这时候你必须在VO里预留一个userName字段由后端在组装VO时去调用用户服务填充。这种“展示字段”和“存储字段”的区分恰恰是原型分析里最有价值的部分。原型只告诉你页面长什么样但它不会告诉你这些数据从哪来而你需要通过“读页面、追数据源、判断字段归属”来把这个问题想清楚。4. 三张清单法把原型字段拆进QUERY、DTO、VO的实操路径读懂了原型之后实体抽取就到了最关键的执行环节。我平时在团队里给大家分享过一个方法叫“三张清单”本质上是把模糊的字段枚举变成一个有秩序的拆解流程很适合作为日常操作标准。第一张清单叫入参清单里面放所有从浏览器/客户端传到后端的数据。入参数据主要来源于两个地方搜索条件和表单提交。搜索条件对应QUERY对象表单提交对应入参DTO对象。这张清单里的字段重点关注的是三个问题字段名是否和前端约定一致类型是否准确校验规则是什么前端传一个“page1size20”你后端接收就要有一个pageNum、pageSize前端传一个日期字符串“2025-03-01 00:00:00”你后端接收就要用LocalDateTime而不是String否则后续转换全是麻烦。第二张清单叫业务数据清单放所有后端处理和存储过程中涉及的数据。比如创建订单时前端表单只传了商品ID和数量但后端需要根据商品ID去查单价、算总价、算优惠、算运费最终生成的Order对象就是一个完整的业务DTO。这张清单里的字段不一定对应原型上的任何字它们是在业务逻辑处理过程中产生的。很多人在这个环节会愣住原型上没有的东西也需要抽取吗答案是当然需要。因为你不可能把整个Order实体直接暴露给前端让它们看到成本价、供应商ID、内部备注这些不该被展示的字段所以需要DTO把业务数据和展示数据隔离开。第三张清单叫展示清单放所有最终要落到页面上的数据。展示清单几乎逐字对应原型上的字段它的核心任务是定义VO。我在做这张清单时有个习惯把原型截个图放在旁边对着图例的每一列在清单里画勾一个字段都不漏。漏字段这个问题在初学者中非常常见往往接口写完了前端联调时发现少了一个订单备注然后就得补VO字段、补转换代码、重新发布浪费大量时间。这三张清单做完之后你就能清楚地看到哪几个对象需要被创建。然后花五分钟把对象的字段填上去实体抽取这个环节基本就完成了。剩下的事情是写代码但在写代码之前有一个动作不能省略逐对象做一些字段级别的审查清单比如主键字段用Long还是String、金额用BigDecimal还是Double、状态用Integer还是枚举、时间用LocalDateTime还是Date、关联用户是用对象嵌套还是只存ID。这一套做完实体类的骨架基本就稳了。4.1 字段类型怎么定一张对照表说清楚做完字段归类和命名之后接下来就是定类型。这一步非常考验经验因为类型选错了后面所有的转换代码都会变得非常痛苦。我基于实际项目经验整理了一个常用对照表建议新手直接收藏。数据含义推荐类型不推荐类型理由主键IDLong / StringInteger数据量大了之后Integer很容易溢出分布式ID是字符串金额BigDecimalDouble/FloatDouble存在精度丢失算钱必须用BigDecimal数量/件数Integer / LongString统计需求一上来字符串完全没法用状态字段Integer/枚举含codeString方便存储和判断但注意VO中要返回文本说明时间字段LocalDateTime / LocalDateDate / String新项目JDK8以上建议直接用LocalDateTime手机号/电话StringLong手机号可能带区号、不能参与计算用String最稳邮箱/地址String无本身是文本没有其他选择布尔开关BooleanInteger后端用Boolean更清晰前端true/false对得上这个表里最值得展开说的是金额和时间的处理。金额是无数项目的重灾区你用Double存金额第一次可能是99.99但经历过几次乘法运算、除法分摊、累计汇总之后精度问题就出来了。做电商的同学应该对0.10.2不等于0.3这种问题有切肤之痛。所以只要字段语义是“钱”一律BigDecimal没有商量余地。时间的处理也同样重要。很多老项目用Date然后各种格式化工具类满天飞前端拿到的是一串时间戳或者带T的字符串还要二次处理。现在JDK8我们都直接用LocalDateTime配合Spring Boot自带的时间序列化配置输出格式统一且好用。有跨时区需求的话再考虑OffsetDateTime但大部分电商项目LocalDateTime足够。4.2 命名规范为什么QUERY、DTO、VO要和数据库实体区分得清清楚楚类型定完之后命名也是实体抽取环节必须统一的问题。我见过太多项目里出现OrderInfo和OrderInfoDTO混用的情况到处都是Info、Model、Entity后缀看代码头痛不说git提交时连自己要改哪个类都要找半天。我目前的命名习惯是数据库表对应的实体类单纯用表名比如Order、User、OrderItemQUERY类则统一加Query后缀比如OrderQuery、UserQuery入参DTO加DTO后缀比如CreateOrderDTO、UpdateUserDTO出参数据如果最终是给前端页面的加VO后缀比如OrderListVO、OrderDetailVO。这样看代码的时候只看后缀就能判断这个类的职责边界根本不需要翻阅每个字段。命名这块还有两条更小但很重要的原则。第一不要用单词缩写能写全一定要写全。比如页面原型上写着“创建时间”那就定义createTime不要写crtTm开发协作中缩写是沟通成本最大的来源。第二接口的语义要跟着对象名走比如创建一个订单的接口入参类型就叫CreateOrderDTO不要叫OrderDTO更不要叫O。字段可以短一点类名一定要完整、清晰、见名知义。5. 从一个“订单列表订单详情”原型完整推导三类实体讲完方法论下面我把之前做生鲜电商项目时的一个实际案例完整走一遍。这个案例我用简化版描述但抽取的思路和步骤是真实的直接可以套到你自己手里的原型上。产品原型的样子是这样的订单列表页顶部是筛选区包含订单号输入框、订单状态下拉框、下单时间范围选择器、渠道来源下拉框表格列包含订单号、客户姓名、商品名称、实付金额、订单状态、下单时间、操作按钮操作按钮包括“查看详情”和“发货”。点击“查看详情”跳转到订单详情页包含订单基础信息订单号、下单时间、支付时间、订单来源、收货人信息姓名、手机号、地址、商品明细列表商品名称、规格、数量、单价、小计、金额汇总商品总额、运费、优惠金额、实付金额、操作记录时间、动作、操作人。按照三张清单法我先做入参清单。搜索区的字段都要进QUERY对象所以OrderQuery里至少要有orderNo、status、channelSource、startTime、endTime同时因为列表页有分页还要加pageNum、pageSize。这里有个细节产品原型上下单时间是一个范围控件但后端接口参数通常拆成两个字段否则一个对象没法表达一段区间。前端传参也是分开的所以在OrderQuery里定义成startTime、endTime是最稳妥的。然后做业务数据清单。创建一个订单DTO时前端会提交商品ID列表和数量、收货地址ID、用户ID、支付方式但后端真正处理订单时还要去查询商品最新价格、计算总金额、计算运费这些计算后的结果就是订单业务DTO的字段来源。这里我不展开建表细节但你需要明白DTO里的字段数量通常远大于表单提交的字段数量因为Service层会往里面塞大量处理过程中产生或查出来的数据。最后做展示清单。订单列表VO需要显示的是orderNo、userName、productName商品概要一个订单可能有多个商品这里通常是“商品A等3件”这种摘要、actualAmount、statusDesc、createTime。订单详情VO则需要更完整的结构订单基础信息、收货人信息对象、商品明细对象列表、金额汇总对象、操作记录列表。注意详情VO里往往要嵌套好几个子对象而子对象可以单独定义成一个内部类或者独立VO类取决于其他页面是否也会复用。从这个案例可以看到同一个订单在QUERY、DTO、VO里出现的字段不完全相同类型也有差异这正是三层模型的意义所在。列表页和详情页的VO基本不同时不可能用一个Order类同时去兼容两种展示形式的强行兼容的唯一结果就是类膨胀、判断满天飞。5.1 代码落地OrderQuery、CreateOrderDTO、OrderListVO示例为了方便大家直接参考我把上面这个案例的关键代码写出来使用Java Spring Boot常见的Lombok风格。说话算话直接上代码类名和字段名就是上面分析的结果。/** * 订单列表查询参数 */ Data public class OrderQuery { /** 订单号模糊查询 */ private String orderNo; /** 订单状态1-待付款 2-待发货 3-已发货 4-已完成 5-已取消 */ private Integer status; /** 渠道来源1-App 2-小程序 3-网页 */ private Integer channelSource; /** 下单时间范围-开始 */ private LocalDateTime startTime; /** 下单时间范围-结束 */ private LocalDateTime endTime; /** 页码从1开始 */ private Integer pageNum 1; /** 每页条数 */ private Integer pageSize 10; }注意两个细节第一pageNum和pageSize都有默认值这样前端漏传参数时不会直接报错第二状态字段用Integer接收直接对应数据库存储值搜索条件不需要接收文本。/** * 创建订单入参DTO */ Data public class CreateOrderDTO { /** 用户ID登录态中获取也可以前端传入 */ NotNull(message 用户ID不能为空) private Long userId; /** 收货地址ID */ NotNull(message 收货地址不能为空) private Long addressId; /** 商品明细 */ NotEmpty(message 商品明细不能为空) Valid private ListOrderItemDTO items; /** 支付方式1-微信 2-支付宝 */ NotNull(message 支付方式不能为空) private Integer payType; /** 买家备注 */ private String buyerRemark; /** * 订单内商品明细 */ Data public static class OrderItemDTO { /** 商品ID */ NotNull(message 商品ID不能为空) private Long productId; /** 购买数量 */ NotNull(message 购买数量不能为空) Min(value 1, message 购买数量至少为1) private Integer count; } }入参DTO和QUERY的差别在这里就体现出来了CreateOrderDTO里全是业务提交字段而且校验规则直接打到注解上Controller入口只需要加一个Validated就能完成参数校验不需要在Controller里写一串if-else判断。这里还嵌套了一个OrderItemDTO使用的是静态内部类这种写法适合子结构只被当前类使用的情况简单清晰。/** * 订单列表展示VO */ Data public class OrderListVO { /** 订单号 */ private String orderNo; /** 客户姓名 */ private String userName; /** 商品概要描述 */ private String productSummary; /** 实付金额元 */ private BigDecimal actualAmount; /** 订单状态文本描述 */ private String statusDesc; /** 下单时间 */ private LocalDateTime createTime; }VO类和入参DTO最大的区别是什么VO里的字段几乎全是“展示语义”比如statusDesc是一个计算出来的文本它不对应数据库字段只为了前端页面显示“已发货”而不是在页面上写if语句去翻译。productSummary是在列表查询时通过SQL或Java代码拼接出来的摘要。如果你想让前端直接展示这些内容就在VO里定义它们然后把组装动作交给Service层。/** * 订单详情展示VO */ Data public class OrderDetailVO { /** 订单基础信息 */ private OrderBaseInfoVO baseInfo; /** 收货人信息 */ private ReceiverInfoVO receiverInfo; /** 商品明细 */ private ListOrderItemVO items; /** 金额汇总 */ private AmountSummaryVO amountSummary; /** 操作记录 */ private ListOrderOperationVO operations; }详情VO一定是一个嵌套结构。因为详情页本身就是一个区块化的页面基础信息一块、收货人一块、商品明细一个表格、金额汇总一块、操作记录一个列表。用扁平的字段排列当然也能实现但前端取数据时会非常痛苦一级一级从对象里抠字段的体验有多差跟后端联调过的前端都会深有体会。所以详情VO在设计时尽量让对象结构和页面区块一一对应一个区块一个子对象后续维护和调试都会非常舒服。抽出这一整套之后后面的Controller和Service怎么写就很顺了Controller接收OrderQuery或CreateOrderDTOService接收之后处理最后返回OrderListVO或OrderDetailVO。5.2 从原型页面到实体清单的对照检查表每次做完实体抽取我还会做一个检查动作拿着原型走一遍确认没有遗漏。直接对着原型页面检查太容易被“字段太多”干扰所以我把常用的检查维度做成了一张简单的表帮助你自查。检查项检查要点命中哪类实体搜索区所有筛选项是否都在QUERY里输入框、下拉、时间范围、关键词QUERY分页、排序参数是否定义页码、每页条数、排序关键字QUERY表单提交项是否都在入参DTO里必填项、选填项、上传项DTO表单校验规则是否完整非空、长度、最小值、枚举值校验DTO列表页所有展示列是否都在列表VO里列名、状态文本、格式化后的金额VO详情页所有展示区块是否有对应VO基础信息、明细、金额汇总、操作记录VO是否有前端需要但后端没有的数据如组合名称、状态描述、脱敏手机号VO是否有数据库有但前端不能看的数据如成本价、内部备注、供应商信息只放DTO/排除金额字段是否为BigDecimal实付金额、优惠金额、运费DTO/VO时间字段类型是否为LocalDateTime创建时间、支付时间、发货时间所有实体这张表不是拿来摆设的我会把它直接作为代码评审时的一个提纲。每次有人提交实体类设计我都会先拿他的类和这张表过一遍效果比空口提意见靠谱得多。你也完全可以按自己的项目情况去调整这张表但核心思路相同把经验变成可执行的自查清单而不是靠灵感。6. 实操记录我的实体类设计评审单以及当年踩过的坑写到这里我发现不少读者可能已经迫不及待想把这些方法论用起来了。但在这之前我再补几个真实项目里高频踩坑的场景这些坑我在不同团队里都反复见过提前打个预防针很值得。第一个坑是QUERY对象继承了数据库实体。很多老项目会有一个BaseEntity里面放ID、创建时间、更新时间新手一看诶查询条件也需要创建时间范围那我直接继承BaseEntity不就好了这个想法很危险。继承数据库实体意味着你的QUERY对象里全是你用不到的字段前端传参时如果真的传了一个id到后端你的Mapper里很可能就会误带上这个条件产生诡异的数据问题。QUERY对象应该是独立的哪怕和实体有重名字段也不要去继承保持极简和纯粹。第二个坑是用Map代替DTO/VO。有一些追求快速的开发者觉得既然前端字段老变我直接用Map接收参数/返回结果不就好了改字段就不用改类了。这是典型的短期省事、长期还债。Map类型让代码完全丢掉编译期检查字段名拼错只有运行时才暴露出了问题排查困难可读性极差。尤其是多人协作的项目里一个Map参数的接口根本没人知道它到底接受哪些字段前端问起来只能去翻代码看字符串开发效率断崖式下跌。实体类的重量感不是负担而是保护。第三个坑是所有字段都用String接收。前端传“2025-03-01”你用了String接然后自己在代码里转LocalDateTime前端传金额“99.90”你用String接收再转BigDecimal前端传状态2你用String接收再转Integer。看起来每个环节都“没事”但每一层都在做无意义的转换纯属给自己加工作量。字段类型在最开始设计时一定严格不要图省事否则后续所有代码都在为这个“省事”买单。第四个坑是过度拆分。QUERY、DTO、VO三件套是方法论但并不意味着每个接口都必须三件套齐全。一个最简单的改动状态接口可能只需要一个入参DTO里面放订单号和状态根本不需要VO一个导出Excel的接口出参可能就是一个文件流也不需要VO。如果机械地每个接口都建三个类代码量会翻倍但收益微乎其微。判断标准就一条这个对象有没有自己的独立职责有就建没有就并不要让方法论变成过度设计的借口。7. 关于校验、转换和Bean拷贝的实操建议实体类建好了接下来有几个“周边”工作虽然是小节但特别重要。第一个是参数校验我强烈建议在入参DTO上直接使用JSR-303校验注解配合Spring Boot的Validated在Controller入口做校验不要让无效请求进入Service层。校验规则要写得准确、可读比如字符串长度限制、数值范围、枚举取值、嵌套对象校验都可以通过注解搞定。我见过很多项目把校验逻辑写在Service方法的前十行每行一个if-else维护起来简直就是灾难用注解一次性声明清楚要优雅得多。第二个是实体转换。QUERY→DTO→VO→Entity之间必然要做字段拷贝手工getter/setter写起来让人崩溃我见过有人在转换方法里写了十几行的setter只为拷贝十个字段。主流方案有两个Apache Commons BeanUtils或Spring的BeanUtils.copyProperties。但我更建议直接用MapStruct因为它在编译期生成转换代码性能好类型错误能提前发现而且支持字段不一致时的显式映射。不过不管用哪个工具都要注意一个问题字段名不一致时自动拷贝会静默失败导致出现null值这个问题在联调阶段才暴露时排查成本极高。所以转换代码写完后务必用单元测试覆盖关键转换路径。第三个是状态字段的文本映射。前端如果需要的不是Integer状态码而是“已发货”这种文本不要在Controller里通过if判断去设置更不要在VO里存一个无意义的JSON序列化字段。推荐的做法是使用一个枚举类通过code获取描述在Service组装VO时直接写入statusDesc字段。这样一来状态码和状态的映射关系收敛在枚举里任何一处要用都是同一个来源后期新增状态也只改一个文件。8. 用一张图抓住本质QUERY/DTO/VO的职责边界以及最终的总结整个QUERY、DTO、VO实体抽取的流程其实可以压缩成一句经验之谈先读原型再分页面然后顺着数据流画出三张清单最后严格定义每个对象的字段和类型。说起来很短但每一步背后都对应着大量的权衡和判断。在这个环节图省事后面就是无休止的修改和前端扯皮而且你会发现每次小改动都像在拆炸弹因为你根本不知道改这个字段会不会影响另一个页面。我自己经历过一次很深刻的教训。有一个订单模块当初为了赶版本直接拿数据库实体去接收前端参数又直接把数据库实体返回给前端。前两周开发确实快后来需求开始加“订单支持拆单发货”“展示订单包含多个包裹的物流信息”的时候这个实体被撑得面目全非。最后不得不花了一个周的时间把几万行代码里的实体引用一点一点拆回QUERY、DTO、VO三件套期间还因为某个字段改漏引发了一次生产事故。如果一开始就按正确的方式抽取实体这个周的时间完全可以省下来。所以现在我再拿到任何原型不管工期多紧都坚持走完“读页面→三张清单→定义实体”这三步。它不会让你变慢反而会让你在后端工作里省掉最多的返工时间。希望这篇文章也能成为你团队里的一份参考如果你在实操中遇到过更奇葩的实体设计坑欢迎带着具体场景来交流和补充。