资讯动态

SpringBoot整合MyBatis-Plus全解析:分页插件与代码生成器

发布时间:2026/10/9 6:46:04 来源:尧图企业网站定制
1. 为什么大家都在用SpringBoot整合Mybatisplus先聊点实在的。我最早写Java后端的时候用的还是SSH那套Struts2、Spring、Hibernate配置全堆在XML里改个数据源都要翻半天配置文件。后来SpringBoot出来了约定大于配置内嵌Tomcatjava -jar一跑就完事确实省心不少。但真正做到数据访问这一层Spring Data JPA虽然方便可复杂的联表查询、动态SQL写起来是真的憋屈。这个时候MyBatis-Plus出现了简单说它就是MyBatis的增强版只做增强不做改变CRUD方法现成的分页插件、条件构造器、代码生成器全都给你打包好了。我推荐SpringBoot整合Mybatisplus核心原因有这几点首先是开发效率单表CRUD基本零SQLBaseMapper里全都有了你只需要写业务逻辑其次是灵活度需要自定义SQL的时候XML和注解都能写不会被框架绑死再就是生态成熟国内公司用得非常多招聘要求里恨不得每条都写着熟悉MyBatis-Plus你跑不掉的。这篇文章不是光讲怎么把依赖加进去跑个Hello World而是把我这几年的落地经验、踩过的坑、分页配置、代码生成这些实操细节都拿出来适合正在做毕业设计的学生、刚转Java后端的新人以及想把项目数据访问层写得干净利落的开发者。我见过太多人引入了MyBatis-Plus之后用起来跟原生MyBatis没什么两样Equals方法不用、分页插件不配、代码生成器不碰最后只用了个LambdaQueryWrapper然后跑来问“为什么我的分页不生效”。说真的这套组合拳打得好不好差距就在这里接下来我按实战顺序把它拆开讲清楚。2. 版本选型与核心依赖配置2.1 版本坑太多先看这张对照表很多人第一步就死在版本上。SpringBoot版本高了之后MyBatis-Plus的老版本启动会直接报错或者明明配了分页插件就是不生效。我把这几年验证过稳定的组合整理成了一张表照着选基本不会翻车。SpringBoot版本MyBatis-Plus版本JDK建议备注2.3.x ~ 2.6.x3.4.x ~ 3.5.2JDK 8老项目经典组合稳定2.7.x3.5.3JDK 8/11推荐兼容性好3.0.x ~ 3.2.x3.5.3 必须JDK 17低版本starter会直接启动失败SpringBoot 3.x这个版本是个分水岭因为它把javax命名空间改成了jakarta大量老库直接废掉。你要是用的3.x配了MyBatis-Plus 3.5.2启动的时候会报ClassNotFoundException或者各种Bean创建异常就是因为底层包名不兼容。我把这行代码贴出来直接放进pom.xml就行dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.7/version /dependencyGradle项目则是这样implementation com.baomidou:mybatis-plus-boot-starter:3.5.72.2 数据源和连接池别漏MyBatis-Plus本身不管数据库连接它只是把MyBatis包了一层所以必须配合数据源来用。SpringBoot 2.x默认用的是HikariCP性能极好不用额外引入。但数据源依赖不能漏很多人只加了MyBatis-Plus的starter结果一启动就报DataSource错误。dependency groupIdcom.mysql/groupId artifactIdmysql-connector-j/artifactId scoperuntime/scope /dependency这里有个细节新版的MySQL驱动groupId已经改成了com.mysql不再是以前的mysql:mysql-connector-javaSpringBoot 2.7和3.x管理的就是这个新版坐标。我自己刚升3.x那会儿就卡在这网上老教程全是老坐标折腾了小半天。配置文件也很关键我习惯用YAML配置项清晰spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/your_db?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalseallowPublicKeyRetrievaltrue username: root password: your_password mybatis-plus: configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl global-config: db-config: id-type: assign_id logic-delete-field: deleted logic-delete-value: 1 logic-not-delete-value: 0 mapper-locations: classpath*:mapper/**/*.xmlurl里那个serverTimezone必须写尤其是国内服务器不写会报时区错误或者时间差8小时。allowPublicKeyRetrievaltrue是MySQL 8.0之后才需要的因为默认缓存SHA2密码认证连接工具和驱动行为不太一样。2.3 实体类扫描与Mapper扫描Service层和Mapper层的写法有个约定Mapper接口要继承BaseMapper然后在启动类上加上MapperScan扫描。你也可以在Mapper接口上直接加Mapper注解但我更推荐统一用MapperScan因为一个包下去全扫了不用每个文件都点一下。SpringBootApplication MapperScan(com.example.demo.mapper) public class DemoApplication { public static void main(String[] args) { SpringApplication.run(DemoApplication.class, args); } }实体类的写法也有讲究对应表名和字段名用驼峰映射。表字段user_name映射实体属性userName前提是map-underscore-to-camel-case必须开着我上面的配置已经开了。每个实体类我一般固定写这几个注解Data TableName(sys_user) public class User { TableId(type IdType.ASSIGN_ID) private Long id; private String userName; private String email; TableLogic private Integer deleted; Version private Integer version; TableField(fill FieldFill.INSERT) private LocalDateTime createTime; TableField(fill FieldFill.INSERT_UPDATE) private LocalDateTime updateTime; }TableName指定表名防止实体名和表名对不上TableId的ASSIGN_ID用的是雪花算法免得自己写ID生成器了TableLogic是逻辑删除我后面细讲Version是乐观锁版本号createTime和updateTime配合自动填充使用这些我认为是MyBatis-Plus用得最舒服的几组注解。3. 核心功能拆解最常用的三板斧3.1 条件构造器其实你用错了条件构造器是MyBatis-Plus的灵魂但我发现很多人的用法停留在最浅层只会new QueryWrapper然后eq。实际上LambdaQueryWrapper才是更推荐的写法因为它是用Lambda表达式引用实体属性编译期就能验证字段名重构的时候不会出现字符串查不出错的问题。// 不推荐字段名写字符串重构时容易漏 QueryWrapperUser wrapper new QueryWrapper(); wrapper.eq(user_name, admin); // 推荐类型安全IDE可以跳转 LambdaQueryWrapperUser lambdaWrapper new LambdaQueryWrapper(); lambdaWrapper.eq(User::getUserName, admin) .like(User::getEmail, qq.com) .orderByDesc(User::getCreateTime);从这里延伸到高阶场景条件构造器还有几个很妙的方法值得说说。apply方法可以拼接原生SQL片段比如按日期范围查询时直接lambdaWrapper.apply(DATE_FORMAT(create_time, %Y-%m-%d) DATE_FORMAT(NOW(), %Y-%m-%d))省去自己拼接字符串的麻烦。还有last方法可以直接拼LIMIT语句比如做随机抽一条记录lambdaWrapper.last(LIMIT 1)但一定注意last是直接拼在SQL末尾的不能用于用户输入的数据否则SQL注入风险极大。分组的场景呢用QueryWrapper的groupBy配合select方法可以返回聚合结果。假设每个用户有多条订单要统计每个用户的订单总额QueryWrapperOrder wrapper new QueryWrapper(); wrapper.select(user_id, SUM(amount) AS total_amount) .groupBy(user_id); ListMapString, Object maps orderMapper.selectMaps(wrapper);这种聚合查询返回的是ListMapString, Object比实体类灵活得多。还有or方法和and方法嵌套的问题普通人最容易踩坑。比如“名字是admin或者邮箱以qq.com结尾同时状态为正常”如果写成下面这样语义就跑偏了LambdaQueryWrapperUser wrapper new LambdaQueryWrapper(); wrapper.eq(User::getUserName, admin) .or() .like(User::getEmail, qq.com) .eq(User::getStatus, 1);这样SQL变成user_name admin OR email LIKE %qq.com% AND status 1由于AND优先级高于OR状态条件会被绑定在邮箱那半边。正确写法是嵌套wrapper.eq(User::getStatus, 1) .and(inner - inner.eq(User::getUserName, admin) .or() .like(User::getEmail, qq.com));3.2 Service层IService的取舍Mapper层操作已经够用那Service层为什么要用IService我用下来觉得最核心的是批量操作和链式查询。继承ServiceImpl之后可以直接调用saveBatch(List entityList)这个做批量插入时性能提升非常明显。我实测过一次性插入5000条数据用saveBatch比循环单条insert快了好几倍因为它内部帮你拼接了多值INSERT语句减少了SQL执行次数和网络往返。IService还提供了lambdaQuery和lambdaUpdate的链式入口写起来非常顺滑ListUser users userService.lambdaQuery() .eq(User::getUserName, admin) .list(); boolean updated userService.lambdaUpdate() .eq(User::getId, 1) .set(User::getEmail, newqq.com) .update();这里要注意lambdaUpdate().set(User::getEmail, newqq.com)这个方法的语义它是把这一条更新SQL的SET子句拆出来写跟updateById传实体有本质区别。updateById整个实体的话null字段默认不会更新但当你想把某个字段值改成NULLupdateById反而做不到得用lambdaUpdate的set手动指定。还有个更大的坑就是字段自动填充比如updateTimeupdateById会触发自动填充而lambdaUpdate().update()不会触发字段填充因为它是直接写SQL的。如果你用了TableField(fill FieldFill.INSERT_UPDATE)这种注解又通过lambdaUpdate更新数据那updateTime就不会被刷新了这个问题我查了很久才意识到。什么时候不用IService也有讲究。主要是复杂的多表关联查询和复杂的批量业务操作IService帮不上忙还是得在Mapper里写自定义SQL。我的做法是Service保留IService但复杂查询直接在Service里注入对应Mapper然后调自定义方法两边不冲突。3.3 分页插件一步忘了全盘皆输分页是MyBatis-Plus最经典的功能也是翻车率最高的地方。很多人配置完依赖写完selectPage一跑发现返回了全表数据完全没有分页效果。原因有且只有一个没有注册分页插件MyBatis-Plus默认不开启分页它不是靠拦截器自动完成的。在SpringBoot 3.x时代配置类写法要注意MyBatisPlusInterceptor这个Bean必须定义。3.5.3之后分页插件类名有了新写法但老写法PaginationInnerInterceptor仍然可用只是要小心依赖冲突。Configuration public class MybatisPlusConfig { Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); PaginationInnerInterceptor pagination new PaginationInnerInterceptor(DbType.MYSQL); pagination.setMaxLimit(500L); pagination.setOverflow(false); interceptor.addInnerInterceptor(pagination); return interceptor; } }这个setMaxLimit(500L)是我自己踩出来的经验。热搜词里那个“单页500条限制”就是这个参数的作用。假设某天你在代码里写了page(1, 1000)如果不设置限制SQL就会查出1000条。但如果你设置maxLimit为500分页插件会自动把每页大小压到500防止有人误写超大分页直接打垮数据库。这个参数的作用是在生产环境保护数据库。selectPage的具体用法倒是很简单PageUser page new Page(1, 10); LambdaQueryWrapperUser wrapper new LambdaQueryWrapper(); wrapper.eq(User::getStatus, 1); PageUser result userMapper.selectPage(page, wrapper); ListUser records result.getRecords(); long total result.getTotal();这里有个容易忽略的点Page对象入参之后查询完它自己也会被填充total等数据所以你既可以用返回值也可以直接看传入的page对象。分页插件只对物理分页生效是LIMIT 10这种千万别相信什么假分页。还有自定义SQL的分页写法返回值需要IPage 参数传Page 最后必须return page这个细节我放到后面的实战章节里演示。分页失效还有一个隐藏原因值得提醒如果你在同一条链路上配置了多个MybatisPlusInterceptor或者重复添加了PaginationInnerInterceptor就会导致SQL被处理多次分页可能算出错乱LIMIT被重复拼接。我的建议是全项目只有一个拦截器Bean所有插件按顺序add进去就好不要多个Interceptor。4. 进阶玩法代码生成器、自动填充与逻辑删除4.1 根据实体类生成建表SQL其实方向反了热搜里有个词叫“mybatisplus根据java实体类生成创建表的sql语句”我做项目的时候一般都会反过来用先用代码生成器生成实体类再让数据库来定结构但这确实有很多人在问。MyBatis-Plus本身确实没有提供直接从实体类生成建表SQL的官方工具但社区里有个插件叫mybatis-plus-generator它做的方向是反过来的从数据库表结构生成实体类、Mapper、Service、Controller一套代码。我用的就是这个三张表能在一分钟内生成全套CRUD代码。public class CodeGenerator { public static void main(String[] args) { FastAutoGenerator.create(jdbc:mysql://localhost:3306/your_db?serverTimezoneAsia/ShanghaiuseSSLfalse, root, password) .globalConfig(builder - builder .author(your_name) .outputDir(D:/generate-code) .enableSwagger() ) .packageConfig(builder - builder .parent(com.example.demo) .entity(entity) .mapper(mapper) .service(service) .serviceImpl(service.impl) .controller(controller) ) .strategyConfig(builder - builder .addInclude(sys_user, sys_role) .entityBuilder() .enableLombok() .logicDeleteColumnName(deleted) ) .execute(); } }代码生成器生成的实体类会自动带上TableName、TableId等注解Controller层带着基础CRUD接口Service带着IService实现直接就能跑。但生成的代码风格偏简单真实项目里还是需要二次调整比如自定义查询接口、权限注解、业务校验这些生成器帮不了你。如果你真的是想做从Java实体到建表SQL我的经验是别折腾工具了直接用数据库迁移工具管理表结构更靠谱。SpringBoot生态里可以用Flyway或Liquibase把建表SQL脚本放到resources/db/migration目录下版本化管理不会出现实体和表对不上的问题。这也是生产环境的主流做法。4.2 自动填充做审计字段别再手动setcreateTime、updateTime这类字段每次插入和更新都要手动set真的烦。好在MyBatis-Plus提供了MetaObjectHandler接口实现它就能统一处理。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()); } }实体类的字段上要加TableField(fill FieldFill.INSERT)和TableField(fill FieldFill.INSERT_UPDATE)这样insert和updateById的时候才会触发填充。有个小坑update的时候如果使用UpdateWrapper的lambdaUpdate().set()方式是不会经过MetaObjectHandler的因为它是直接写SQL。这是我在3.2节强调过的两个知识点在这里会相遇。4.3 逻辑删除配置字段设计技巧逻辑删除的好处是不用DELETE保留数据审计痕迹代价就是所有查询都要自动带条件。MyBatis-Plus在global-config里配置logic-delete-field之后就全局生效了。实体类里的deleted字段加TableLogic注解然后在application.yml里配好mybatis-plus: global-config: db-config: logic-delete-field: deleted logic-delete-value: 1 logic-not-delete-value: 0那么执行deleteById时实际执行的是UPDATE user SET deleted1 WHERE id? AND deleted0查询时自动拼接AND deleted0。这个机制工作得不错但有几个时候你要小心一是普通查询加条件时不用自己加deleted插件会处理好二是如果写自定义SQL拼XML必须自己手动加上deleted0这个条件三是统计总数的时候要看清楚selectCount返回的已经是过滤后的数量。4.4 乐观锁插件解决并发更新这个功能我用得比较多的时候是订单金额操作。两个并发请求同时读到同一个用户的余额各自加钱后提交的覆盖前一个。解决思路就是加乐观锁更新前检查版本号是否变化。实体类加version字段和Version注解然后配置乐观锁插件Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor()); return interceptor; }注意顺序分页插件通常加在optimisticLocker之前实际上官网文档建议的顺序是先添加乐观锁再添加分页因为拦截器是按照添加顺序执行链式的不过我这里强烈建议参考官方文档的当前顺序——先分页再乐观锁容易导致SQL中SET部分的version判断不准确。更稳妥的做法是只把乐观锁和分页这两个InnerInterceptor放在同一个MybatisPlusInterceptor里先add乐观锁再add分页代码写清楚注释不打乱顺序。5. 实操过程从零搭一个可运行的项目5.1 工程结构和表结构准备我这里以Gradle项目为例子因为很多教程都是Maven但Gradle的工程结构在SpringBoot 3时代越来越多人用。我建议你新建一个最简单的单体项目目录结构按下面来组织demo/ ├── build.gradle ├── settings.gradle ├── src/main/ │ ├── java/com/example/demo/ │ │ ├── config/MybatisPlusConfig.java │ │ ├── controller/UserController.java │ │ ├── entity/User.java │ │ ├── mapper/UserMapper.java │ │ ├── service/UserService.java │ │ └── service/impl/UserServiceImpl.java │ └── resources/ │ ├── application.yml │ └── mapper/UserMapper.xml对应建一张基础的user表来练习分页和CRUDCREATE TABLE sys_user ( id BIGINT PRIMARY KEY COMMENT 主键ID, user_name VARCHAR(50) COMMENT 用户名, email VARCHAR(100) COMMENT 邮箱, status TINYINT DEFAULT 1 COMMENT 状态 1正常 0禁用, deleted TINYINT DEFAULT 0 COMMENT 逻辑删除, version INT DEFAULT 0 COMMENT 乐观锁版本, create_time DATETIME COMMENT 创建时间, update_time DATETIME COMMENT 更新时间 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT用户表;这张表把前面讲到的逻辑删除、乐观锁、审计字段全包含了直接用代码生成器反向生成代码或者手写实体类都可以。5.2 完整代码串联Controller到Mapper先快速把四层代码全部列出来然后逐个讲注意点。首先是Mapper接口public interface UserMapper extends BaseMapperUser { // 自定义分页查询 IPageUser selectUserPage(PageUser page, Param(name) String name); }然后是Service接口与实现public interface UserService extends IServiceUser { IPageUser queryUserPage(long current, long size, String name); } Service public class UserServiceImpl extends ServiceImplUserMapper, User implements UserService { Override public IPageUser queryUserPage(long current, long size, String name) { PageUser page new Page(current, size); return baseMapper.selectUserPage(page, name); } }Controller层RestController RequestMapping(/user) public class UserController { Resource private UserService userService; GetMapping(/page) public ResultIPageUser page(RequestParam(defaultValue 1) long current, RequestParam(defaultValue 10) long size, RequestParam(required false) String name) { return Result.success(userService.queryUserPage(current, size, name)); } }Mapper.xml里自定义分页SQL?xml version1.0 encodingUTF-8? !DOCTYPE mapper PUBLIC -//mybatis.org//DTD Mapper 3.0//EN http://mybatis.org/dtd/mybatis-3-mapper.dtd mapper namespacecom.example.demo.mapper.UserMapper select idselectUserPage resultTypecom.example.demo.entity.User SELECT id, user_name, email, status, create_time FROM sys_user WHERE deleted 0 if testname ! null and name ! AND user_name LIKE CONCAT(%, #{name}, %) /if ORDER BY create_time DESC /select /mapper注意这两个核心点自定义分页SQL的Mapper方法返回值必须是IPage 参数必须有一个Page 缺一个都不行XML里面不需要写LIMIT分页插件会自动拼上。我在3.3节提醒过这地方漏掉任何一个标注都会让分页失效。5.3 启动验证与常见日志分析跑起来之后建议把mybatis-plus的log-impl配成StdOutImpl这样可以在控制台看到完整SQL。登录数据库手动插几条测试数据然后调用UserController的page接口正常应该看到控制台打印 Preparing: SELECT id,user_name,email,status,create_time FROM sys_user WHERE deleted0 ORDER BY create_time DESC LIMIT 0,10 Parameters: Total: 10看到LIMIT两个参数说明分页插件已生效。如果只看到查询全表没有LIMIT不用猜了肯定是分页插件没注册成功回去检查MybatisPlusInterceptor那个Bean有没有被Spring扫描到注解加没加。再试一下deleteById控制台应该输出UPDATE语句而不是DELETE验证逻辑删除是否生效。两条日志都对了说明这套整合已经彻底跑通了。6. 常见问题与排查技巧实录6.1 分页不生效或每页超出限制这是我收到最多的问题。分页不生效先看控制台有没有LIMIT关键字没有就查BeanMybatisPlusInterceptor是否成功注册、yml配置有没有写错位置、启动类扫描路径是否覆盖了config包。如果分页实用了但页码超出总页数时返回空数据检查overflow参数我一般关闭让它自动查询最后一页对用户体验友好。maxLimit我设为500既防止大分页拖垮数据库又不误伤正常查询。6.2 表和字段名映射对不上字段里出现userName查出来是nullSQL日志里显示user_name但实体属性接收不到先确认map-underscore-to-camel-case是否设置为true。如果还是不行检查Status这个字段它对应status一定不要漏掉自定义驼峰的这个规则。用别名也可以解决比如SQL里写AS userName但我更建议统一走全局配置比每次手动映射省心。6.3 多数据源与事务失效如果项目里配了多个数据源用DS注解切换到某个数据源时分页插件是对单个数据源生效的多数据源下插件注册方式略有不同通常需要给不同数据源的SqlSessionFactory分别配置拦截器。这个比较复杂我的建议是如果不是特别需要尽量避免在一个项目里同时操作多个数据库类型。还有Transactional事务当你在Service内部跨方法调用时可能出现事务失效的情况比如同类中this调用绕过代理要注入自己的代理对象或者拆到另一个Service里才能保证事务生效。这块也是Spring的老话题了但配合MyBatis-Plus一起出现时容易让人摸不着头脑。6.4 代码生成器生成的代码风格不合需求生成的Controller是裸的没有统一返回体和权限校验Service也没加事务注解。我的做法是生成完毕后再全局替换写个简单的正则批量加注解或者直接改生成器模板。FastAutoGenerator的模板可以自定义把template路径指到自己的模板目录就没问题。早期版本用Velocity模板改起来还麻烦一些新版已经不难了。6.5 Gradle项目资源目录注意事项热搜里有gradle项目搭建的条目我用Gradle踩过一个坑默认src/main/resources没有被打进jar包。需要在build.gradle里显式加上配置sourceSets { main { resources { srcDirs [src/main/resources] } } }否则Mapper.xml文件不在classpath里MyBatis-Plus扫描不到自定义XML运行时报Invalid bound statement。这个问题排查起来比较隐蔽因为启动不报错一调Mapper方法就挂。6.6 多模块项目中的Singleton与包扫描SpringBoot多模块项目里MyBatis-Plus最常见的问题就是MapperScan扫描不到其他模块的Mapper接口。解决方式是在启动类的MapperScan里把各个模块的mapper包路径都列出来或者在每个模块自己的配置类里加各自的MapperScan。实体类如果放在公共模块要确保启动类能扫描到否则MybatisPlus的TableInfoHelper缓存不到实体元数据各种CRUD直接失效。这个问题在多模块结构下尤其容易发生我遇到过好几次最后都是在启动类上把mapper扫描路径显式写完整解决的。7. 我的最后几点经验总结这一路写下来其实就是把一个SpringBoot整合Mybatisplus的项目从依赖、配置、基础CRUD、分页插件、代码生成器、逻辑删除、乐观锁到最终落地跑通的完整链路过了一遍。你翻到前面配好版本中间配好拦截器后面直接跑分页基本上不会再被日常的坑绊倒了。根据我个人的经验最后再分享一个小技巧遇到任何MyBatis-Plus相关的诡异问题第一件事不是改代码而是打开控制台看SQL日志SQL长什么样心里就有底了。绝大多数“为什么查出来不对”“为什么没更新”的问题看一眼日志就能定位。其次是把官网文档里的注意事项列表通读一遍它家文档写得很细连每个注解的边界条件都标出来了。如果后续想在这个项目上继续扩展可以往多租户插件、数据权限插件、动态表名插件方向走MyBatis-Plus还有不少高级玩法它对业务开发的提效能力比你想得还要大。这篇文章就先到这儿有问题评论区见。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑