1. 为什么SaaS平台离不开一个能持续跑的CRUD底座
1.1 交付节奏被重复劳动拖慢的真实场景
做过SaaS项目的人大概都有这样的体验:一个中等规模的业务系统,真正决定交付周期的往往不是那些花哨的核心算法,而是一堆结构相似、字段不同、接口几乎一样的基础管理模块。用户管理、角色管理、字典管理、配置管理、公告管理、日志管理,这些模块的代码骨架高度雷同,写起来枯燥,改起来更烦。我在某团队做交付支持时统计过一版数据:一个标准后台系统里,与CRUD直接相关的代码量占到总代码量的六成以上,而其中真正包含差异化业务逻辑的部分不到三成。
问题不在于CRUD本身简单,而在于它必须"批量地、规范地、可维护地"存在。一个新人接手项目,打开代码仓库看到二十个风格迥异的Controller,命名一会儿是getUserList,一会儿是queryAllUsers,返回结构一会儿是裸数组,一会儿是包了三层的Result对象,这种项目后续的维护成本会成倍上升。更麻烦的是,每次加一个新表,就要重新复制一遍分页逻辑、参数校验、异常处理、审计字段填充,复制过程中稍有疏忽就漏掉一处,等到线上出问题再回头补,付出的代价远高于一开始就统一。
这也是为什么很多团队在做SaaS平台时会认真考虑引入代码生成工具。不是因为懒,而是因为重复劳动的边际收益递减得太快,而规范漂移的边际成本递增得太快。谷雨代码生成工具在这个背景下进入视野,它的定位很明确:把标准CRUD这类"结构确定、规范统一、变更频繁"的部分交给生成器,把人的精力留给真正需要判断的业务逻辑。
1.2 规范漂移的隐性成本
规范这件事,写在文档里是一回事,落到代码里是另一回事。我见过不少团队,开发规范文档写得非常完整,命名约定、分层结构、异常码、返回体格式都有明确规定,但实际代码里依然会出现各种变体。原因很简单:规范靠人自觉执行,而人在赶工期的时候最容易妥协。
规范漂移带来的成本是隐性的,短期看不出来,长期会集中爆发。举几个具体的例子:返回体格式不统一,前端要针对每个接口写不同的解析逻辑,联调时间被拉长;分页参数命名不一致,有的用page和size,有的用pageNum和pageSize,封装通用表格组件时就得做兼容层;审计字段有的表有create_time,有的叫created_at,做数据归档和审计查询时就得写一堆特判。这些零碎的问题单看都不大,叠加起来就是持续交付路上的减速带。
代码生成工具的价值在这里体现得很直接:规范不再依赖人的记性,而是固化在模板里。生成出来的代码天然遵循同一套约定,新人不需要背规范文档,看两份生成物就能明白项目长什么样。这一点对SaaS平台尤其重要,因为SaaS的特点是多租户、多模块、长期迭代,代码风格的一致性直接决定了后续扩展的难易程度。
1.3 代码生成工具在交付链路里的位置
需要说清楚一个认知:代码生成工具不是银弹,它解决的是"结构性重复"问题,不解决"业务复杂度"问题。把它放在交付链路里看,它的位置大概在需求拆解和数据建模之后、业务逻辑开发之前。也就是说,先用它把骨架搭起来,让项目能跑通、能联调、能演示,然后再在骨架上填业务的血肉。
这个定位决定了工具的设计取向。如果生成器试图把业务逻辑也一起生成,那它必然要理解业务语义,复杂度会失控;如果生成器只生成最基础的骨架,把扩展点留清楚,那它就能保持轻量和稳定。谷雨工具走的是后一条路,后面几节会详细展开。我先给一个结论:一个能长期用的代码生成工具,衡量标准不是生成得多,而是生成得稳、改得动、退得回。这三点分别对应模板稳定性、扩展机制和覆盖防护,缺一个都会让工具从助力变成负担。
2. 谷雨工具的设计取向:薄生成、厚扩展
2.1 模板驱动和元数据驱动的取舍
代码生成工具有两条常见路线。一条是模板驱动,开发者写好模板文件,生成器把变量填进去渲染出代码;另一条是元数据驱动,开发者描述数据模型和约束,生成器根据内置规则推导出完整产物。两条路线各有适用场景,实际项目里往往是混合使用。
模板驱动的优势是灵活,想生成什么就写什么模板,缺点是模板质量完全取决于写模板的人,模板一多维护成本就上来了,而且模板里的逻辑分支容易写得越来越复杂。元数据驱动的优势是统一,字段类型、约束、索引这些信息描述一次,生成器自动推导出实体类、Mapper、Service、Controller、DTO、查询参数对象,缺点是灵活性受限,遇到特殊需求要额外开扩展口。
谷雨工具的做法是元数据为主、模板为辅。数据模型定义是唯一的事实来源,字段名、类型、长度、是否可空、是否索引、注释这些信息集中描述;模板只负责把推导结果渲染成目标语言的结构,模板里不写业务判断。这么设计的理由很实际:元数据是业务语义的自然表达,改一个字段的可空性,所有相关产物应该同步变化;而模板里如果掺杂业务判断,改一处逻辑就得改多个模板,很快会失控。
我用生活化的方式打个比方:元数据像是菜谱上写的食材和用量,模板像是固定的烹饪流程。食材换了,流程不变,出来的菜结构还是一样的。如果流程里写死了"如果放牛肉就多炒两分钟",那流程本身就成了业务逻辑,后面加新食材就得改流程,越改越乱。
2.2 生成物与手写代码的边界
这是代码生成工具最容易踩坑的地方。很多工具一开始很受欢迎,用了半年就被团队弃用,原因往往是生成物和手写代码混在一起,再生成一次就把手写的东西覆盖了,或者手写代码依赖了生成物的内部结构,生成物一升级就全线报错。
谷雨工具在边界划分上做了三件事。第一,生成目录和手写目录物理隔离,生成物统一落在generated包下,手写代码放在business包下,构建时两者一起编译,但目录结构上泾渭分明。第二,生成物里的扩展点用继承或组合的方式预留,比如生成的Service是一个基础实现类,业务侧继承它并覆写需要定制的方法,而不是直接改生成文件。第三,生成器带覆盖检测,执行前会对比目标文件与上次生成记录的哈希,发现被手工修改过就中止并提示,避免误覆盖。
这套边界设计带来的直接好处是:生成器可以放心升级模板,因为手写代码不依赖生成物的内部实现细节;反过来,手写代码里发现的问题也可以反哺模板,把通用改进沉淀下来,下次生成自动生效。这是一个正向循环,也是"可持续"三个字的具体落地。
2.3 为什么不做"一键生成整个系统"
有些工具宣传的时候喜欢说"一键生成整个后台",听起来很吸引人,实际用起来往往是灾难。原因在于,一个完整系统里包含大量业务判断,这些判断没法从数据模型里推导出来。强行生成的结果是一堆看起来能用、实际处处要改的代码,改的成本比从零写还高,因为你还得先读懂生成器的心智模型。
谷雨工具明确不做这件事,它的目标范围就是单表的标准化CRUD加上必要的查询能力。具体来说,生成的内容包括:实体类、数据访问层、服务层基础实现、控制器、请求参数对象、响应视图对象、基础校验注解、分页查询封装。这些内容的共同特征是结构确定、规范统一、变更多为字段级调整。至于复杂的业务编排、跨表事务、状态机流转,一律不碰,留给开发者。
这个取舍看起来保守,实际上是负责任的做法。工具的边界清晰,使用者对它的预期就清晰,不会出现"以为它能干结果干不了"的落差。我在实际项目里见过反面案例:某团队引入了一个功能很全的生成器,结果生成出来的代码有七成需要手工调整,调整过程中又引入了新的不一致,最后整个项目变成生成物和手写代码的混合体,谁也不敢动。工具的克制,恰恰是它可长期使用的前提。
3. 3分钟生成一套规范CRUD的实操记录
3.1 环境准备与目录约定
先说我实际跑通的环境。JDK 17、Maven 3.9、Spring Boot 3.x、MyBatis-Plus 作为数据访问层,数据库用MySQL 8。谷雨工具以Maven插件的形式引入,好处是不依赖额外的运行时环境,跟着构建走,团队成员拉下代码就能用。
目录约定这块建议提前定好,不要等生成了一堆文件再回头整理。我的习惯是这样:
project-root ├── src/main/java │ ├── com.example.demo │ │ ├── generated # 生成物,禁止手工修改 │ │ │ ├── entity │ │ │ ├── mapper │ │ │ ├── service │ │ │ ├── controller │ │ │ └── model │ │ ├── business # 手写业务代码 │ │ └── common # 公共组件 ├── src/main/resources │ ├── generator # 生成器配置与模板 │ └── mapper # XML映射文件插件配置大概长这样,核心是数据源、包名、输出路径、作者信息几项:
<plugin> <groupId>com.example</groupId> <artifactId>guyu-generator-maven-plugin</artifactId> <version>1.4.0</version> <configuration> <jdbcUrl>jdbc:mysql://localhost:3306/demo</jdbcUrl> <username>dev</username> <password>dev_pass</password> <basePackage>com.example.demo</basePackage> <outputDir>src/main/java</outputDir> <generatedPackage>generated</generatedPackage> <author>team</author> <enableSwagger>true</enableSwagger> <enableValidation>true</enableValidation> </configuration> </plugin>注意:数据库账号建议单独建一个只读账号给生成器用,不要图省事用业务主账号。生成器只需要读表结构,不需要写权限,权限收窄是基本的安全习惯。
3.2 数据模型定义的写法
谷雨工具的元数据来源有两块:一是数据库里已有的表结构,二是配置文件里补充的业务语义。表结构能提供字段名、类型、长度、可空性、索引,但提供不了"这个字段在列表页展示、那个字段只在详情页展示"这类信息,所以需要补充配置。
补充配置我一般写在generator/model.yaml里,格式如下:
tables: - name: t_order comment: 订单表 fields: - name: order_no comment: 订单编号 queryType: eq # 精确查询 listVisible: true - name: customer_name comment: 客户名称 queryType: like # 模糊查询 listVisible: true - name: amount comment: 订单金额 queryType: range # 区间查询 listVisible: true - name: status comment: 订单状态 queryType: in listVisible: true dictCode: order_status # 关联字典 - name: remark comment: 备注 listVisible: false这里有几个我踩过坑的点。queryType这个字段很关键,它决定了查询参数对象里生成什么样的字段和什么样的查询条件构造逻辑。如果不配,生成器默认按精确匹配处理,模糊搜索就得手工加,加的时候又容易破坏统一的查询封装。listVisible控制列表页返回哪些字段,不配的话默认全返回,遇到大文本字段(比如详情、富文本)会把列表接口的响应体撑得很大,实测下来一个列表接口返回两百条记录、每条带一个几KB的备注字段,响应时间能翻好几倍。
字典码dictCode是我很推荐的一个配置项。SaaS平台里状态类字段特别多,订单状态、支付状态、审核状态,如果每个模块自己维护一套枚举,前端就得维护多套映射。配置了字典码之后,生成器会自动在响应视图对象里加上状态描述字段,前端直接展示,不需要再写一遍映射逻辑。这个细节看起来小,实际项目里能省掉大量重复沟通。
3.3 执行生成与产物解读
配置完成后执行一条命令:
mvn guyu:generate -Dtable=t_order三分钟之内,generated目录下会多出七个文件。我逐个说明它们的作用和设计意图。
实体类OrderEntity对应表结构,字段类型自动映射,create_time和update_time这类审计字段会自动标注填充策略,避免每次插入都要手工赋值。数据访问层OrderMapper继承通用的基础Mapper,自带单表增删改查和条件构造能力,只有复杂查询才需要写XML。服务层OrderService和实现类OrderServiceImpl提供基础CRUD方法,方法签名统一为create、update、remove、getById、page。控制器OrderController暴露标准REST接口,路径按/api/order约定,方法上带参数校验注解和接口文档注解。
请求参数对象OrderQuery是查询条件的载体,包含了配置里定义的查询字段,内部封装了区间查询、集合查询的逻辑,调用方只需要赋值。响应视图对象OrderVO是列表和详情返回的结构,包含了字典描述字段。还有一个OrderConvert负责实体和视图对象之间的转换,把映射逻辑集中在一处。
实操心得:生成完先别急着写业务,花十分钟把生成物从头到尾读一遍。一方面确认字段映射是否符合预期,另一方面熟悉这套代码的风格,后面手写业务代码时保持同样的写法,整个项目的一致性会好很多。
3.4 生成之后的扩展入口
生成物是骨架,业务逻辑要从扩展入口进去。谷雨工具预留的扩展方式是继承加覆写。比如订单创建时需要校验库存、计算优惠、生成流水号,这些逻辑写在业务侧的服务类里:
@Service public class OrderBizService extends OrderServiceImpl { @Override public Long create(OrderCreateDTO dto) { checkStock(dto); String orderNo = generateOrderNo(); dto.setOrderNo(orderNo); Long id = super.create(dto); writeOrderLog(id, dto); return id; } }控制器侧同理,如果某个接口需要额外的权限校验或者特殊的参数处理,继承生成的控制器覆写对应方法即可。这种继承关系的好处是,生成物升级时新增的方法会自动被业务类继承,不需要手工同步;而业务类覆写的方法保持稳定,不受影响。
这里要提醒一个设计上的细节:生成的基础方法签名要尽量稳定,不要频繁变动。我在早期版本里犯过一个错,把create方法的返回值从void改成了主键ID,结果所有覆写了这个方法的下游业务类全部编译报错。后来定了个规矩:生成物的公开方法签名视为契约,变更需要走版本号,破坏性变更只在大版本里做,小版本只加不改。这个规矩看起来是流程问题,实际是工具能否被信任的关键。
4. 让生成物像人写的:规范落地的关键细节
4.1 命名与分层规范
生成物最容易被人诟病的一点是"一眼看出是机器生成的",代码能跑但读起来别扭。这个问题多半出在命名和分层上。谷雨工具在这块做了几层处理,我觉得值得借鉴。
命名方面,表名到类名的转换有一套固定规则:去掉表前缀(比如t_、sys_),下划线转驼峰,单复数统一。t_order_item生成OrderItem而不是TOrderItem或者OrderItems。字段名到属性名的转换同理,customer_name转customerName。这些规则写在配置里,团队可以按自己的习惯调整,但一旦定下来就全局统一。
分层方面,生成物严格遵循四层结构,每层的职责边界清晰:控制器只做参数接收和结果返回,不写业务判断;服务层承载业务编排;数据访问层只管数据读写;模型层负责数据结构定义。这个分层不新鲜,但关键在于生成器强制执行,不给"图省事在控制器里直接调Mapper"留口子。我在实际项目里见过太多控制器里写几十行业务逻辑的代码,后期要加事务、加缓存、加权限,全得重构。分层规范靠人守不住,靠工具守才守得住。
还有一个细节是注释。生成物的类注释、方法注释、字段注释全部从元数据里带出来,表注释变成类注释,字段注释变成属性注释。这一点对SaaS平台特别有价值,因为SaaS的字段经常随客户需求调整,注释跟着元数据走,就不会出现代码改了注释没改的情况。数据库里的字段注释写清楚,生成的代码注释就清楚,前后端对接的时候少扯很多皮。
4.2 查询接口的通用参数设计
列表查询是CRUD里使用频率最高的接口,也是最容易写乱的接口。不同开发者写出来的查询参数五花八门,前端对接成本高。谷雨工具生成的查询参数对象采用统一的设计,核心参数如下表所示:
| 参数名 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| pageNum | Integer | 1 | 页码,从1开始 |
| pageSize | Integer | 20 | 每页条数,上限500 |
| orderBy | String | 空 | 排序字段,白名单校验 |
| orderDirection | String | asc | 排序方向,只允许asc/desc |
| keyword | String | 空 | 全局模糊搜索关键词 |
这套参数的设计有几个考量。分页参数用pageNum和pageSize而不是page和size,是因为前者语义更明确,不容易和业务字段混淆。pageSize设上限500,是防止有人传个一万进来把数据库拖垮,这个上限在参数校验里强制执行。排序字段走白名单,只允许配置里声明过的字段参与排序,避免SQL注入风险,这个点在安全审计时经常被问到。
keyword这个全局搜索参数是我个人很推荐的设计。它对应多个字段的模糊匹配,生成器会根据listVisible为true的文本字段自动拼出OR条件。比如订单列表的全局搜索会同时匹配订单编号和客户名称。这个参数的存在让前端只需要一个搜索框就能覆盖大部分场景,交互简洁,后端实现也统一。当然,涉及大表的全局模糊搜索要注意索引问题,我的做法是只对建立了索引的字段开放全局搜索,其他字段走独立的条件查询。
4.3 审计字段、权限与软删除的自动注入
SaaS平台有几个通用能力,每个模块都需要,但每个模块单独实现又很啰嗦。谷雨工具把这些能力做成了生成时的自动注入。
审计字段方面,create_by、create_time、update_by、update_time四个字段由框架统一填充,业务代码里不需要出现任何赋值语句。实现方式是在数据访问层加拦截器,插入时填充创建信息,更新时填充更新信息。这么做的好处是审计信息的准确性有保障,不会因为某个开发者忘了赋值而出现空值。
权限方面,生成的控制器方法上会带上权限注解,权限码按模块:操作的格式自动生成,比如order:create、order:delete。权限码统一之后,权限配置界面可以直接从注解里扫描出来,不需要手工录入。这个设计在SaaS场景下价值很大,因为SaaS的权限往往是按租户配置的,权限码如果散落在各处,配置和维护都会很混乱。
软删除方面,生成的删除方法默认是逻辑删除,把deleted字段置为1,查询时自动过滤。这里有个坑要提醒:逻辑删除和唯一索引会冲突。比如订单编号设了唯一索引,删掉一条记录后再用同样的编号新建,会违反唯一约束。我的处理方式是把唯一索引改成(order_no, deleted)的联合唯一索引,删除时deleted置为记录ID而不是1,这样多次删除也不会冲突。这个细节在生成器里做了默认处理,但团队最好知道背后的原因,遇到问题时能自己判断。
4.4 多租户隔离的处理
多租户是SaaS平台绕不开的话题,也是代码生成工具必须考虑的横切关注点。谷雨工具的处理方式是在元数据里标记租户字段(默认tenant_id),生成时自动在数据访问层加上租户过滤条件,业务代码里完全不需要感知。
具体实现是在查询构造和数据操作时统一追加tenant_id = 当前租户的条件,租户ID从上下文里取。插入时自动填充租户ID,更新和删除时校验租户归属,防止越权操作。这套机制对业务开发者是透明的,写查询的时候不需要考虑租户,框架兜底。
注意:多租户隔离一定要在数据访问层做,不要在控制器或者服务层做。放在上层意味着每个方法都要记得加条件,迟早会漏。放在数据访问层是最后一道防线,漏不掉。另外,涉及跨租户的统计查询要单独开白名单,走专门的接口,不要在主查询里开口子。
还有一个容易被忽略的点:租户隔离要和缓存、消息、文件存储一起考虑。生成器管的是数据库层,其他层的隔离需要配套的组件支持。我的建议是在项目初期就把租户上下文做成一个统一的ThreadLocal,所有需要租户信息的地方都从这里取,生成器生成的代码也依赖这个上下文,保持一致。
5. AI时代的合规考量:可审计的代码生产
5.1 AI辅助编程带来的新变量
这两年AI辅助编程工具普及得很快,写代码的效率确实提升了,但同时也带来了新的问题。AI生成的代码风格不统一,同一个项目里能看到好几种写法;AI有时会引入不存在的依赖或者过时的API,代码能跑但存在隐患;更麻烦的是,AI生成的代码往往缺少上下文,开发者自己也说不清这段逻辑为什么这么写,后期维护时无从下手。
对于SaaS平台来说,这些问题会被放大。SaaS的特点是长期迭代、多人协作、多租户共用一套代码,任何一处不一致都可能在后续扩展时变成障碍。我在某项目里就遇到过这样的情况:团队成员用AI辅助生成了几个模块,短期内功能都正常,但半年后要做统一的权限改造时发现,这几个模块的鉴权逻辑各写各的,改造工作量比重新写还大。
这不是说AI辅助编程不好,而是说AI生成的代码需要有一个统一的收敛机制。代码生成工具在这里扮演的角色,就是把"结构性的部分"从AI手里接管过来,让AI专注于那些真正需要判断的逻辑。基础CRUD这种结构确定的部分,用模板生成比用AI生成更可控,因为模板是确定的、可审计的、可版本管理的。
5.2 模板统一带来的可审计收益
合规这个词听起来很宏大,落到代码层面其实很具体:能不能说清楚每一行代码是怎么来的,能不能在出问题时快速定位,能不能在审计时提供完整的证据链。模板生成的代码在这三点上有天然优势。
先说来源清晰。生成物的每一行都来自模板加元数据,模板是版本管理的,元数据是配置管理的,任何一个生成物的内容都可以通过"模板版本加元数据快照"完整复现。这意味着如果哪天发现某个模块有安全问题,可以快速判断影响范围:是模板的问题还是元数据的问题,模板的哪个版本引入的,哪些表受影响了。这个追溯能力在AI生成代码的场景下是很难做到的,因为AI的输出具有不确定性,同样的输入不一定得到同样的输出。
再说定位效率。规范统一的代码在排查问题时效率高很多。分页逻辑只有一份实现,出问题改一处;权限校验只有一种写法,漏校验的情况一眼能看出来;审计字段的填充机制是统一的,数据异常时排查路径是确定的。这些在规范漂移的项目里都是奢望,每个模块都要单独看一遍。
最后说审计证据。现在很多企业对软件交付有合规要求,需要提供代码来源、变更记录、评审记录。模板生成的代码在变更记录上是清晰的,元数据改了什么、模板改了什么、生成了哪些文件,都有记录可查。这比"某个开发者用AI生成了一段代码然后手工调整"要容易审计得多。
5.3 生成日志与变更追溯
谷雨工具在执行生成时会产出一份生成日志,记录本次生成的时间、操作人、涉及的表、生成的文件列表、每个文件的哈希值。这份日志我建议纳入版本管理,和代码一起提交。它的作用在几个场景下会体现出来。
场景一,怀疑某个文件被手工改过。对比当前文件哈希和日志里的哈希,不一致就说明被改过。这个检测在再生成之前自动执行,发现不一致会中止并提示,避免覆盖。
场景二,排查某个字段是什么时候加的。翻生成日志,找到对应表的生成记录,看元数据的变更历史,一目了然。这比翻Git提交记录要直观,因为Git记录的是文件变更,生成日志记录的是模型变更。
场景三,新人接手项目。让他看一遍生成日志,就能明白哪些模块是生成的、生成时用了什么配置、哪些是手写的。这个信息在项目交接时价值很高,能省掉大量沟通。
# 生成日志示例(简化) [2024-06-12 10:23:41] table=t_order operator=dev generated: OrderEntity.java (sha256: a1b2...) generated: OrderMapper.java (sha256: c3d4...) generated: OrderService.java (sha256: e5f6...) generated: OrderController.java (sha256: g7h8...) meta: model.yaml@v3 fields=[order_no,customer_name,amount,status,remark]实操心得:生成日志不要只放在本地,纳入版本管理。我见过团队因为日志丢失,再生成时无法判断文件是否被改过,只能全部覆盖,结果把手工调整的逻辑冲掉了。这个坑踩一次就够记一辈子。
6. 常见问题与排查实录
6.1 表结构变更后的再生成
表结构变更是常态,加字段、改类型、加索引,每次变更都要考虑再生成的影响。谷雨工具的处理逻辑是增量更新:新增的字段会生成对应的属性和映射,删除的字段会从生成物里移除,修改的字段会更新类型和注释。听起来很顺,但实际用起来有几个点要注意。
第一,字段删除要谨慎。生成物里删掉一个字段,手写业务代码里如果还引用了这个字段,会编译报错。我的做法是先改元数据把字段标记为废弃,生成时保留属性但标注@Deprecated,观察一个迭代周期确认没有引用后再真正删除。这个流程看起来慢,但比线上出问题快。
第二,字段类型变更要检查数据兼容性。比如amount从int改成decimal,生成物里的类型会跟着变,但数据库里的历史数据如果精度不够,转换时可能丢精度。我的做法是类型变更前先跑一遍数据检查脚本,确认现有数据能安全转换,再执行变更。
第三,索引变更不影响生成物,但影响查询性能。加索引之后要确认生成的查询条件能用上索引,特别是复合索引的字段顺序要匹配查询条件的顺序。这个点生成器管不了,得靠人看执行计划。
6.2 手写代码被覆盖的防护
这个问题在团队协作场景下最容易出现。某个开发者不知道生成物的规矩,直接改了generated目录下的文件,下次别人再生成时就被覆盖了,改的东西没了,排查半天才发现原因。
防护措施有三层。第一层是目录约定加注释警告,生成物的每个文件头部都有"此文件由生成器生成,请勿手工修改"的注释,醒目位置标明扩展方式。第二层是生成前哈希校验,发现文件被改过就中止,并列出被改的文件,让人确认是回滚还是跳过。第三层是CI检查,构建时校验生成物与元数据是否一致,不一致就阻断合并。
三层防护下来,基本不会出现误覆盖。但也要留一个口子:确实需要在生成物里加东西的时候,走元数据或者模板的扩展机制,而不是直接改文件。我在团队里定的规矩是,改生成物必须同时改元数据或模板,否则代码评审不通过。规矩执行一段时间后,大家都形成了习惯。
6.3 类型映射与分页失效的排查表
实际使用中遇到的问题,我整理了一张速查表,覆盖最常见的几类:
| 现象 | 可能原因 | 排查方法 | 解决方式 |
|---|---|---|---|
| 生成的属性类型不对 | 数据库类型与映射规则不匹配 | 查生成器类型映射配置 | 补充自定义映射规则 |
| 分页查询返回全量数据 | 分页插件未生效或参数未传递 | 看SQL日志是否带limit | 检查插件配置和参数名 |
| 模糊查询查不到数据 | 字符集不一致或未加通配符 | 直接执行SQL验证 | 统一字符集,检查拼接逻辑 |
| 逻辑删除后数据仍可查到 | 查询未经过拦截器 | 检查是否走了自定义SQL | 自定义SQL手工加过滤条件 |
| 租户隔离失效 | 上下文未设置或异步线程丢失 | 打日志看租户ID | 异步场景手工传递上下文 |
| 字典描述为空 | 字典码未配置或字典表无数据 | 查字典表对应记录 | 补字典数据或修正字典码 |
这张表里的每一条都是我在实际项目里遇到过的。分页失效那条特别常见,原因往往是参数名不一致,前端传page,后端接pageNum,插件拿不到分页参数就走了全量查询。这种问题在联调阶段发现还好,上线后发现就是性能事故。租户隔离失效那条更隐蔽,同步调用正常,异步线程里上下文丢了,查出来的数据跨租户,这种问题不打个日志根本发现不了。
7. 我在实际项目里的几点体会
这套工具用下来,最大的感受是:代码生成的价值不在于省了多少行代码,而在于把"确定性"沉淀下来了。哪些东西是确定的,就交给工具;哪些东西需要判断,就留给人。这个边界划清楚之后,团队的协作效率提升是实实在在的,新人上手快,代码评审的焦点从"格式对不对"转移到"逻辑对不对",这才是评审该有的样子。
还有一个体会是关于"克制"。工具的功能越加越多,维护成本就越高,使用者理解成本也越高。谷雨工具目前的能力范围控制得比较窄,就是标准CRUD加必要的查询能力,复杂的东西一概不碰。这个克制让它在项目里活了很久,没有变成那种"引入时很热闹、半年后没人用"的工具。我见过太多大而全的框架最后被团队抛弃,原因往往不是能力不够,而是太重了,重到大家不愿意学。
最后分享一个实操中的小技巧:每次生成之后,花五分钟把生成物和上一次的生成物做个diff,看看模板或者元数据的变更带来了什么影响。这个习惯帮我提前发现过好几次问题,比如某次改模板不小心影响了一个不该影响的字段,diff里一眼就看出来了。如果等到运行时才发现,排查成本会高很多。工具是死的,用工具的人是活的,多看一眼总没错。