简介:面向Spring Boot开发者的Trae自动生成后端程序示例,围绕前后端分离架构中studentmanager学生管理系统的后端实现展开,定位清晰,适合希望借助自动化工具减少重复编码的Java开发者。资源以真实案例展示Trae如何依据数据模型自动生成实体类、Repository数据访问层、Service服务层、Controller控制器与配置类,配合pom.xml和接口测试文档,可直观理解自动化代码生成流程及Spring Boot分层工程的搭建逻辑。压缩包共20个文件、约10KB,包含11个xml工程与IDE配置、4个java核心源码、2个properties应用配置,以及gitignore和接口测试md文档,文件虽少但覆盖了后端项目的主要骨架。当前已有939人学习下载,适合作为学习Spring Boot自动生成工具、梳理RESTful后端基础结构的轻量参考,也可借其模板化思路迁移到自身项目中。
1. 用Trae自动生成前后端分离的web应用程序:后端程序这条线到底怎么走
用Trae自动生成前后端分离的web应用程序,很多人只关注前端页面怎么聊,真正容易卡住的其实是后端程序:Spring Boot工程骨架结构乱、依赖版本对不上、接口返回格式不统一、一联调就翻车。这篇笔记把我复现一个订单管理模块后端的过程完整拆开,从提示词怎么写、工程结构怎么约束,到参数怎么调、踩坑怎么排,全部走一遍。适合正在用AI辅助开发、但被生成代码的兼容性坑过的人,也适合前后端分离刚入门、想快速搭出Spring Boot后端骨架的新手。我尽量少说玄学,多给能直接抄的写法。
我复现时用的思路是:不让AI从零生成整个工程,而是先生成一个干净的Maven骨架,再让Trae在指定包路径下分层生成Controller、Service、Mapper。这样做的原因是Trae这类对话式AI工具对“单点生成”的代码质量有保障,但对整个工程结构的感知很弱——你直接让它“写一个完整后端项目”,它会给你一个混合了Thymeleaf视图、旧版本依赖、甚至自带前端页面的怪东西,反而不如交给它写具体类来得干净。
2. 生成前的技术准备:Spring Boot工程结构与Trae上下文约束
2.1 前后端分离下的后端职责边界:为什么不能生成Thymeleaf模板
用Trae生成Spring Boot后端程序时,最容易被带偏的就是“视图层依赖”。AI训练数据里包含大量传统单体Web应用示例,所以当你用“做一个XXX管理系统”这类宽泛描述时,它很可能在pom.xml里引入spring-boot-starter-thymeleaf,Controller方法返回字符串视图名,写一堆th:each替换标签。这在前后端分离项目里没有任何用处,需要识别出来后顺手删掉。
我判断是不是前后端分离正确写法的三个标志:
| 检查点 | 单体写法(错误) | 分离写法(正确) |
|---|---|---|
| Controller | @Controller + 返回字符串 | @RestController + 返回JSON对象 |
| 模板引擎 | thymeleaf / freemarker / jsp | 不需要 |
| 静态资源 | 放在src/main/resources/templates | 放在前端CDN或Vue/React工程里 |
你在REPL(对话窗口)里看到pom.xml出现模板引擎相关坐标,直接让它删掉就行,命令是“删除所有模板引擎相关依赖,工程只保留Spring Web与数据访问组件”。也可以用一句更稳妥的话兜底:“本工程为前后端分离后端接口程序,不渲染任何视图,Controller全部返回JSON”。
这一步算是一个既定前提:后端只做接口,不输出HTML。如果不管这个,后面生成的代码会陷入“页面和数据混在一起”的混乱状态,前端联调时根本没法用。
2.2 工程结构约束:用Maven父子模块把生成代码固定在三层内
我一般会先手动创建Maven骨架,不依赖Trae生成工程目录。骨架建好之后,Trae的角色是在指定目录下写类,而不是决定工程长什么样。
常用做法是在本地创建一个空的Spring Boot工程,目录结构大致如下:
demo-backend/ ├─ pom.xml └─ src/ ├─ main/ │ ├─ java/com/example/demo/ │ │ ├─ controller/ │ │ ├─ service/ │ │ ├─ mapper/ │ │ ├─ entity/ │ │ └─ config/ │ └─ resources/ │ ├─ application.yml │ └─ mapper/ # XML文件放这里 └─ test/java/创建好骨架后,对Trae下达的结构约束指令我通常这样写:“只修改src/main/java/com/example/demo/controller、service、mapper、entity这四个包,不要新增包、不要修改pom.xml、不要创建前端文件”。这一句话能显著提升生成结果的可控性。
很多翻车场景源于AI自作主张新增包路径,比如它在service下按业务模块建一堆子包,导致服务和实体散落在十几个包位置,排查问题时找半天。所以锁定包路径这个动作,在让Trae大批量生成代码前必须做。这种做法本质上是把AI当成一个熟悉框架的工程师,但它不太懂你工程的约定,必须用上下文告诉它边界在哪里。
2.3 依赖版本选择:Spring Boot 2.7与3.x的取舍
Trae生成的代码偏向哪套Spring Boot版本,取决于训练数据。目前市面上的示例代码既有旧版写法又有新写法,如果你不指定版本,它生成的代码可能混合新旧API——比如同时出现javax.servlet与jakarta.servlet、同时支持MyBatis-Plus旧版拦截器与新版Interceptor。我最常踩的混合问题就是Spring Boot 3.x工程里出现了javax.servlet.Filter实现。
所以开工前必须钦定版本。我的经验是:本地JDK是8或11,就用Spring Boot 2.7系列;本地JDK 17及以上,用Spring Boot 3.x系列。这里不追新,选你本地Maven仓库里最熟的稳定版即可。然后让Trae把父POM改为Spring Boot(版本号我每次直接写在上下文约束里),同时告诉它不要自己引入版本管理。
下面是一段pom.xml的核心结构,在让Trae生成代码前最好先自己建好:
<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>你本地的稳定版本</version> <relativePath/> </parent> <properties> <java.version>8</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> </dependencies>代码里mybatis-plus版本号统一由依赖管理收口,不要写死版本数字。原因很简单:你写死一个AI推荐的版本,它在本地的存储库里不一定有,Maven解析阶段就会报错,Transport错误弹出来之后排查成本反而高。让父POM统一管理的好处是,AI生成的代码只关心代码逻辑,不接触版本决策,版本文案问题从源头消失。
3. 复现记录:用Trae生成订单模块的完整代码路径
3.1 第一轮对话:把需求描述拆成Trae能理解的结构化指令
我让Trae生成代码时发现一个规律:它不喜欢空洞需求。你写“帮我写一个订单接口”,它给你的通常是一个贫血Controller,没有统一返回体,没有参数校验,MyBatis-Plus依赖也没有。这是因为订单接口这四个字在训练数据里对应太多可能性,它只能按统计概率挑选一条最常见的路线,而这个路线通常很简陋。
我调整后的做法是让需求描述带四个维度:实体定义、接口列表、校验规则、返回体规范。下面是低效与高效两种问法的对比。
低效的问题描述是这样的:
帮我写一个订单模块高效的问题描述则需要落在具体结构上,像这样:
为订单表(字段:主键id、用户id、订单号、商品名称、数量、金额、状态、创建时间、更新时间)生成一个后端模块,包含:分页查询订单接口、新增订单接口、根据订单号查询接口、修改订单状态接口。实体类使用Lombok,Mapper使用MyBatis-Plus的BaseMapper,Service接口与实现分离,Controller使用RestController并返回统一Result对象。不要生成前端页面和模板文件。上面这段描述在提示词层面做了四件事:给出表字段(让实体贴合实际)、给出接口清单(让Controller不超范围)、指定技术选型(框架固定为MyBatis-Plus)、指定返回体(统一Result)。Trae收到这种结构化上下文后生成的代码明显更能落得下来——所谓可落地,指的是一套代码逻辑自洽,不会出现一个接口调用另一个没生成的Service方法。
这就是对话式AI生成后端程序的关键:把它当作一个需要精确上下文才能写对代码的专家,需求里没说清楚的部分它只能猜,猜多了就容易翻车。我试过几次不加约束直接让它生成,结果就是工程里无缘无故多出一个User模块,代码还引用了不存在的工具类。
3.2 三个能落地的提示词模板
下面给出我实际使用过的三组提示词,分别对应实体层、Mapper层、Controller/Service层。
第一组:实体层生成提示词。
生成一个订单实体类OrderInfo,对应数据库表order_info:字段id(Long)、userId(Long)、orderNo(String)、productName(String)、quantity(Integer)、amount(BigDecimal)、status(Integer)、createTime(LocalDateTime)、updateTime(LocalDateTime)。使用Lombok的@Data注解,用MyBatis-Plus的@TableName关联表名,id主键策略为@TableId(type = IdType.AUTO),createTime和updateTime分别使用@TableField(fill = FieldFill.INSERT)和@TableField(fill = FieldFill.INSERT_UPDATE)配合自动填充。这段提示词把每个字段的类型都显式给出,不让AI自己从字段名推断类型。原因是我踩过坑:单写“金额”两个字,AI可能推断成Double而不是BigDecimal,后续接财务计算时出现精度问题。参数说明上,status字段使用Integer而不是String,是为了让接口参数校验时能统一做数值判断;createTime和updateTime交由自动填充,让插入和更新动作带时间效率更高。
生成的实体类大致长这样:
@Data @TableName("order_info") public class OrderInfo { @TableId(type = IdType.AUTO) private Long id; private Long userId; private String orderNo; private String productName; private Integer quantity; private BigDecimal amount; private Integer status; @TableField(fill = FieldFill.INSERT) private LocalDateTime createTime; @TableField(fill = FieldFill.INSERT_UPDATE) private LocalDateTime updateTime; }此处代码块上的逻辑说明:@Data让字段不手写getter/setter,省去大量样板代码;@TableField(fill = ...) 只是声明了自动填充时机,还需要在配置类里实现MetaObjectHandler才会真正生效,这个坑在第4章会细说。主键策略用AUTO,此时MySQL表字段必须是自增主键,如果表的主键是业务号(比如分布式订单号),则需要改成IdType.ASSIGN_ID,这属于提示词阶段就要定好的参数——AI没法替你判断主键的产生策略。
第二组:Mapper层生成提示词。
生成OrderInfoMapper接口,继承MyBatis-Plus的BaseMapper<OrderInfo>,并使用@Mapper注解。增加一个通过订单号查询的方法,SQL写在对应XML文件里,方法名为selectByOrderNo,返回类型为OrderInfo,查询条件为order_no = #{orderNo}。生成的代码结构比较直接:
@Mapper public interface OrderInfoMapper extends BaseMapper<OrderInfo> { OrderInfo selectByOrderNo(@Param("orderNo") String orderNo); }对应XML文件放在src/main/resources/mapper/OrderInfoMapper.xml里:
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="com.example.demo.mapper.OrderInfoMapper"> <select id="selectByOrderNo" resultType="com.example.demo.entity.OrderInfo"> select * from order_info where order_no = #{orderNo} </select> </mapper>逻辑说明:BaseMapper已经提供了insert/updateById/selectById等常用方法,无需重新声明;selectByOrderNo属于自定义查询,放XML的好处是后期调整SQL不用重新编译。参数说明里的@Param("orderNo")必须和XML里的#{orderNo}一致,否则MyBatis会报传参错误。另外,namespace必须写Mapper接口的完整全限定名,差一个字母都匹配不上。
第三组:Controller/Service层生成提示词。
生成订单模块的Controller和Service。Service接口OrderInfoService继承IService<OrderInfo>,实现OrderInfoServiceImpl继承ServiceImpl<OrderInfoMapper, OrderInfo>。Controller提供四个接口:POST /order/page分页查询(参数为当前页和每页条数)、POST /order/add新增、GET /order/detail/{orderNo}按订单号查询、PUT /order/status修改状态。Controller返回统一Result对象,表示例如:code=200为成功,500为业务失败。参数校验使用@Validated。Service实现类的代码大概如下:
@Service public class OrderInfoServiceImpl extends ServiceImpl<OrderInfoMapper, OrderInfo> implements OrderInfoService { @Override public OrderInfo getByOrderNo(String orderNo) { return this.baseMapper.selectByOrderNo(orderNo); } }Controller部分我用过一种更省事的写法,让AI同时生成统一Result类和Controller:
@RestController @RequestMapping("/order") public class OrderInfoController { @Autowired private OrderInfoService orderInfoService; @PostMapping("/page") public Result<Page<OrderInfo>> page(@RequestParam(defaultValue = "1") Integer current, @RequestParam(defaultValue = "10") Integer size) { Page<OrderInfo> page = new Page<>(current, size); return Result.success(orderInfoService.page(page)); } @PostMapping("/add") public Result<Void> add(@RequestBody @Validated OrderInfo orderInfo) { orderInfoService.save(orderInfo); return Result.success(); } @GetMapping("/detail/{orderNo}") public Result<OrderInfo> detail(@PathVariable String orderNo) { return Result.success(orderInfoService.getByOrderNo(orderNo)); } @PutMapping("/status") public Result<Void> updateStatus(@RequestBody OrderInfo orderInfo) { orderInfoService.updateById(orderInfo); return Result.success(); } }逻辑说明:Page分页需要配合MyBatis-Plus的分页插件才能生效,常见做法是在配置类里注册MybatisPlusInterceptor,否则page方法返回的数据不精确;@Validated注解只是在Controller层兜底校验,更细的业务规则建议放在Service层,因为多个Controller可能复用同一个Service方法。参数说明里defaultValue="1"和"10"是为了让前端不传分页参数时也能正常返回,避免接口返回NullPointerException。
3.3 生成后的必改清单
Trae生成代码基本能跑通,但有几处是每次必改的,不改就存在隐患。我整理成一张常见问题清单:
| 位置 | 默认生成结果 | 需要改成 | 原因 |
|---|---|---|---|
| 实体主键 | @TableId(type = IdType.ASSIGN_ID) | IdType.AUTO | 自增表使用雪花ID会导致插入失败 |
| 逻辑删除 | 无 | @TableLogic | 关联数据被错误物理删除 |
| 时间填充 | 无 | MetaObjectHandler | 插入时间字段自动留空 |
| CORS跨域 | @CrossOrigin注解零散 | WebMvcConfigurer统一配置 | 注解方式在拦截器链里可能被过滤掉 |
| 统一返回 | Map返回 | Result泛型类 | 前端难以解析错误状态 |
必改清单里最容易被忽视的是MyBatis-Plus新版拦截器。它已经不再采用旧版PaginationInnerInterceptor在配置里手动new的方式,而是统一通过MybatisPlusInterceptor进行链式注册。Trae生成的代码里,分页插件可能缺失,也可能用一个已经过时的类,导致分页查询返回total为0。
这个清单算是给机器生成的代码兜底,把它当成固定动作嵌入每次AI生成后的处理流程,可以避免大多数运行期异常。
4. 避坑实录:Trae生成Spring Boot代码的五个高频问题
4.1 时间字段少8小时,接口返回UTC格式
现象:前端联调时发现订单创建时间返回的是凌晨4点,不是当天下午,查看原始JSON字符串末尾多一个Z。
原因:Jackson默认按UTC序列化LocalDateTime,本地MySQL的datetime类型存储的是北京时区时间,生成时没被正确转换,读取后直接序列化输出。这个问题出在application.yml里缺少时区设置,AI生成的配置默认带的是serverTimezone=UTC的JDBC参数,两地时差叠加导致结果错位。
解决:在application.yml里显式声明统一时区为GMT+8,并在连接串里去掉UTC时区参数。
spring: jackson: time-zone: GMT+8 date-format: yyyy-MM-dd HH:mm:ss datasource: url: jdbc:mysql://localhost:3306/demo_db?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai这段配置说明:spring.jackson.time-zone负责序列化与反序列化的时区;date-format控制接口返回的日期字符串样式,避免前端拿到带T分隔符的ISO格式。连接串里的serverTimezone改用Asia/Shanghai后,数据库驱动才会按东八区读取time类型字段。如果你生成的代码里没有这两项,直接补齐即可。
4.2 CORS跨域配置失效,前端OPTIONS预检请求直接403
现象:本地Vue工程启动后,访问后端接口时浏览器CORS报错,请求失败。
原因:控制器方法上零散用了@CrossOrigin注解,而且允许来源配置成allowedOrigins("*")配合allowCredentials(true),Spring Security层面不允许这种组合;即使没有Security,多个@CrossOrigin在跨域过滤顺序上也可能出现拦截器先于CORS处理的情况。
解决:利用WebMvcConfigurer统一配置CORS,接管整个工程的跨域策略:
@Configuration public class CorsConfig implements WebMvcConfigurer { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") .allowedOriginPatterns("*") .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") .allowedHeaders("*") .allowCredentials(true) .maxAge(3600); } }逻辑说明:allowedOriginPatterns("")与allowedHeaders("")是配对的;allowCredentials(true)表示允许前端携带Cookie等凭证信息,此时不能用allowedOrigins(""),用allowedOriginPatterns("")是一个被广泛接受的做法,兼顾了通配与凭证带宽。maxAge缓存了预检结果有效时长,前端请求频率高时,减少OPTIONS请求次数是比较直观的性能优化。
这种统一配置方式避开了注解分散管理的问题,因为拦截器如果先执行,有可能把OPTIONS请求直接拦掉,CORS过滤器根本没有机会加分响应头。
4.3 实体类字段与数据库列不匹配,启动即抛异常
现象:项目启动时MyBatis报错,提示无法找到列userId或列与之对应,错误码是Invalid bound statement。
原因:Trae生成实体字段用的是驼峰命名,而数据库表字段是下划线命名(如user_id、product_name)。MyBatis默认不会自动把both形式转成驼峰。
解决:在application.yml里开启驼峰映射,同时用@TableField显式标注列名,双保险。
mybatis: configuration: map-underscore-to-camel-case: true如果还是有问题,直接在实体字段上补一个@TableField("user_id")。多数情况下只需要开启驼峰映射即可,但遇到特殊字段(比如数据库里字段名有关键字)时,用@TableField显式指定列名更稳妥。MyBatis-Plus本身自带全局映射配置,参数上建议写true而不是默认值false。
4.4 MyBatis-Plus自动填充不生效,createTime一直为null
现象:调用新增接口后,数据库里create_time字段为空,程序不报错。
原因:实体类虽然写了@TableField(fill = FieldFill.INSERT),但工程里没有实现MetaObjectHandler这个处理器,MyBatis-Plus不知道插入时该填充什么值。
解决:创建一个自动填充处理器:
@Component public class MyMetaObjectHandler implements MetaObjectHandler { @Override public void insertFill(MetaObject metaObject) { this.strictInsertFill(metaObject, "createTime", LocalDateTime.class, LocalDateTime.now()); this.strictInsertFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now()); } @Override public void updateFill(MetaObject metaObject) { this.strictUpdateFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now()); } }代码里strictInsertFill带了四个参数——metaObject是MyBatis传入的元数据对象;字段名createTime与实体属性名保持一致;LocalDateTime.class是字段类型;LocalDateTime.now()是插入时的时间值。这里有个隐藏细节:如果实体字段没有标fill属性,严格模式会跳过填充,所以这两段配置必须配合使用,缺一不可。
4.5 逻辑删除失效,删除接口直接删了真实数据
现象:调用删除接口,表里的记录直接消失了,而不是置为删除标记状态。
原因:实体上没有@TableLogic注解,MyBatis-Plus的delete方法和updateById都按物理删除处理。AI生成代码时默认不带业务上的删除保护机制,尤其是在表结构里没设计deleted字段的前提下。
解决:先给表加一个deleted字段(默认0),实体上增加逻辑删除字段并注解,然后让删除接口调用removeById或deleteById。
@TableLogic @TableField("deleted") private Integer deleted;逻辑说明:@TableLogic生效后,MyBatis-Plus生成的delete语句会自动转成UPDATE SET deleted = 1 WHERE id = ? AND deleted = 0;select语句也会自动追加deleted = 0条件,这个细节很容易刚踩过,却是数据表设计的底线。要让它的作用完全发挥,还得在application.yml里确认全局逻辑删除配置,如果所有表都用同一个deleted字段,可以用全局配置方式统一处理,不然每张表实体都要注解一遍,后续迭代容易漏掉。
5. 验证与进阶:把AI生成的后端代码收编进自己的规范里
5.1 做一个最小的回归验证脚本,不用打开浏览器就能测
Trae生成的代码是否跑通,不要等着前端联调。等你点完Vue页面才发现接口404,那至少已经过去十分钟。我会在接口写完以后做一个bash脚本覆盖核心链路,用curl直接打接口验证返回数据结构。
下面这个脚本算是我的习惯动作,换了机器跑也只是改一下端口和IP:
#!/bin/bash BASE_URL="http://localhost:8080" echo "== 1. 分页查询订单 ==" curl -s -X POST "$BASE_URL/order/page" \ -H "Content-Type: application/json" \ -d '{"current":1,"size":10}' | head -c 500 echo "" echo "== 2. 新增订单 ==" curl -s -X POST "$BASE_URL/order/add" \ -H "Content-Type: application/json" \ -d '{"userId":1001,"orderNo":"NO20240001","productName":"测试商品","quantity":2,"amount":199.00,"status":1}' | head -c 500 echo "" echo "== 3. 按订单号查详情 ==" curl -s -X GET "$BASE_URL/order/detail/NO20240001" | head -c 500 echo "" echo "== 4. 修改订单状态 ==" curl -s -X PUT "$BASE_URL/order/status" \ -H "Content-Type: application/json" \ -d '{"id":1,"status":2}' | head -c 500这段脚本里每个curl都对应一个接口契约:分页查询传current和size;新增订单传创建所需全部字段;详情查询走路径参数;状态修改只传id和status。脚本能确认的边界是:接口能通、参数解析正确、返回体里有code=200。它验证不了业务规则的完备性,但作为回归用例已经足够日常开发自测了。脚本里的身份证号、手机号都是虚拟化的占位数据,运行完也不会对数据库产生无害的重复影响——你可以配合事务回滚机制再跑一次。
5.2 进阶动作:让AI生成过程与工程规范绑定
我在跑通一个模块之后,会再花十几分钟做一件容易被忽略的事:把刚生成的代码当作“新规范”喂回对话上下文,让后续模块照葫芦画瓢。具体做法是把Result类的代码、统一异常处理类、分页插件的配置类放进对话上下文,并追加一句明确的约束话术:
后续所有生成的Controller都遵循Result返回,所有Service实现类都继承ServiceImpl,所有实体类都带@TableLogic逻辑删除字段和自动填充注解。不要重复生成配置类。这套约束话术的价值在于:让AI的输出风格与当前工程对齐,规避它记住的上一套模板。很多开发者的切肤之痛是让AI先写了用户模块,再让它写订单模块,生成的风格完全对不上。有了规范词,生成第二个模块时风格保持一致,代码整体看起来像同一个人的手笔,维护成本随之降下来。
另一方面,Tea的上下文长度支持足够承载这些约束信息,你每次新开对话都要主动粘贴规范约束,绝不是让它凭记忆来。如果对话内容岔得太远,不如新开一个窗口,把规范重新贴一遍,效果比在一段长对话里反复纠正要好得多。
最后补一个自己养成的习惯:任何AI生成的代码,在提交到仓库之前,我会强制本地跑一次“回归5连”脚本,并从Controller到Mapper逐层看一遍字段类型和注解。有一次我图省事,让AI在上一个模块基础上追加一个导出接口,结果它把Excel工具类放进Controller层,做了个强依赖耦合,后面改起来相当难受。从那以后我给自己定了一条死规矩:AI只负责产出初始版本,工程规范由人把控,每个生成模块都要经过回归验证才算完成。希望这篇笔记能帮你把从自然语言到接口的路径走得稳一点,至少让踩坑变成有预期的事,而不是每次都被生成代码黑匣子卡住。
本文还有配套的精品资源,点击获取