数据库里每张表都有 create_time、update_time你敢保证线上每一条记录的这两个字段都是对的值吗大部分项目靠 insert 时手写几个字段update 时漏一个就静默吃灰。qkl-boot 里我把这层用 MyBatis-Plus 的字段自动填充 逻辑删除收编了审计字段不再靠自觉。表结构先统一脚手架的公共字段是固定的创建时间、更新时间、创建人、更新人、删除标记、版本号。建表的时候统一带上别一张表一个样CREATETABLEjx_order(idbigintNOTNULLAUTO_INCREMENTCOMMENT主键,order_novarchar(32)NOTNULLCOMMENT订单号,worker_idbigintDEFAULTNULLCOMMENT接单工人ID,create_timedatetimeDEFAULTNULLCOMMENT创建时间,update_timedatetimeDEFAULTNULLCOMMENT更新时间,create_byvarchar(32)DEFAULTNULLCOMMENT创建人,update_byvarchar(32)DEFAULTNULLCOMMENT更新人,deletedtinyintNOTNULLDEFAULT0COMMENT逻辑删除0未删 1已删,versionintNOTNULLDEFAULT0COMMENT乐观锁版本,PRIMARYKEY(id),KEYidx_order_no(order_no))ENGINEInnoDBDEFAULTCHARSETutf8mb4COMMENT零工订单表;deleted和version一个管逻辑删除一个给乐观锁用都是脚手架全局统一的命名谁也别想自己起个别名。自动填充一个处理器管全部表以前业务代码里写order.setCreateTime(new Date())的地方全删掉。MyBatis-Plus 的MetaObjectHandler拦截 insert/update统一往里塞值ComponentpublicclassMyMetaObjectHandlerimplementsMetaObjectHandler{OverridepublicvoidinsertFill(MetaObjectmetaObject){this.strictInsertFill(metaObject,createTime,LocalDateTime.class,LocalDateTime.now());this.strictInsertFill(metaObject,updateTime,LocalDateTime.class,LocalDateTime.now());this.strictInsertFill(metaObject,deleted,Integer.class,0);// createBy 从当前登录上下文取没有登录态就填 systemLonguserIdSecurityUtils.getUserId();this.strictInsertFill(metaObject,createBy,String.class,userId!null?userId.toString():system);this.strictInsertFill(metaObject,updateBy,String.class,userId!null?userId.toString():system);}OverridepublicvoidupdateFill(MetaObjectmetaObject){this.strictUpdateFill(metaObject,updateTime,LocalDateTime.class,LocalDateTime.now());LonguserIdSecurityUtils.getUserId();this.strictUpdateFill(metaObject,updateBy,String.class,userId!null?userId.toString():system);}}实体上标注解告诉 MP 哪些字段走自动填充DataTableName(jx_order)publicclassOrder{TableId(typeIdType.ASSIGN_ID)privateLongid;privateStringorderNo;privateLongworkerId;TableField(fillFieldFill.INSERT)privateLocalDateTimecreateTime;TableField(fillFieldFill.INSERT_UPDATE)privateLocalDateTimeupdateTime;TableField(fillFieldFill.INSERT)privateStringcreateBy;TableField(fillFieldFill.INSERT_UPDATE)privateStringupdateBy;TableLogicTableField(fillFieldFill.INSERT)privateIntegerdeleted;VersionprivateIntegerversion;}这么一套下来业务代码里 insert/update 就只写业务字段审计字段全是系统自动补的。踩坑实录批量更新把 create_time 覆盖成了当前时间现象运营后台有个批量修改订单备注的功能改完一看一批订单的 create_time 全变成了刚才的时间。用户投诉订单创建时间怎么是昨天下午。排查过程先看 SQLMP 生成的 update 语句里带了create_time ?说明 create_time 进入了更新字段集合。再看实体createTime上标的是FieldFill.INSERT理论上 update 不该带上它——但批量更新走的不是单条 update。定位思路翻代码发现批量改备注用的是updateBatchById而实体里 create_time 标了TableField(fill FieldFill.INSERT)按 MP 语义 update 时不该填充——问题出在实体上还有个隐藏点createTime 同时被insertFill和updateFill都写了值。排查到MyMetaObjectHandler.updateFill里为了省事把 createBy/updateBy 一起塞的时候顺手写了strictUpdateFill(metaObject, createTime, ...)——多了这一行update 时 createTime 也被填充了。最终解决把 updateFill 里的 createTime 那行删掉只保留 updateTime 和 updateBy。回归测试单条 update、updateBatchById、wrapper.update 三种路径都验一遍确认 create_time 不再出现在 update set 里。逻辑删除deleted 字段上的 TableLogic物理删除一时爽出了事想恢复只能找备份。qkl-boot 全局用逻辑删除MP 的TableLogic会自动把 delete 变成 update查询自动带deleted0// 业务代码照常写orderService.removeById(orderId);// 实际执行的 SQL// UPDATE jx_order SET deleted1, update_time? WHERE id? AND deleted0查询侧不用管MP 生成的 select 都自动追加AND deleted0。但注意唯一索引的坑比如订单号 order_no 建了唯一索引逻辑删除后同一订单号再发一单插入会撞唯一索引。解法两个把唯一索引改成(order_no, deleted)联合唯一或者删除时把 order_no 加后缀改写比如order_no #del id。零工场景我选的联合唯一索引简单直接数据也不变形。可以直接抄的清单公共字段统一create_time/update_time/create_by/update_by/deleted/version建表 DDL 直接复制MetaObjectHandler只负责 insert/update 填充updateFill 里千万别写 createTime实体注解三件套TableField(fill...)、TableLogic、Version缺一个功能就歪逻辑删除 唯一索引 联合唯一索引(业务唯一键, deleted)别单独建批量更新updateBatchById路径也要回归验证它和单条 update 的填充行为不完全一样项目源码https://gitee.com/gzqkl/qkl-boot