☰
SpringBoot2+Vue3+MyBatis-Plus隔离管理系统实战:从CRUD封装到部署交付
2026/10/5 4:25:42 网站建设 项目流程

接到这个项目需求的时候,是一个比较紧急的场景:需要在短时间内产出一套能够支撑隔离人员全流程管理的业务系统,包含人员登记、健康上报、隔离周期管理、房间分配这些核心动作。项目要求前后端分离,后端定的是 SpringBoot2 体系,前端要求 Vue3,持久层指定 MyBatis-Plus,数据库则是 MySQL8.0。这几乎就是当下 Java Web 后台管理系统最主流的一套组合拳了。

我记得当时评估过几个方向:要不要上 Spring Cloud?要不要用 JSP 那套老方案?前端要不要继续抱着 Vue2 不放?最后都推翻了。原因后面会展开讲。这篇文章我打算把整个项目从选型、初始化、通用 CRUD 服务设计、核心业务模块实现,到 MySQL8.0 的接入坑、最终文档交付,完整梳理一遍——不是贴源码完事,而是讲清楚每一个关键设计背后的理由,以及那些不跑一遍根本发现不了的细节。

1. 为什么是 SpringBoot2 + Vue3 + MyBatis-Plus:这套组合的取与舍

1.1 先从后端框架说起:单体不是保守,是对业务的清醒认知

隔离管理系统这种项目,并发量不会高到需要分布式来解决,业务边界也很清晰,就是人员、健康数据、房间资源、日志这几张主表之间的流转。用微服务去拆,除了增加运维复杂度,没有带来任何实际收益。我当时见过太多团队一上来就 Spring Cloud 全家桶,结果没人能说清楚服务间调用的边界在哪里。

SpringBoot2 在这个项目里有几个实打实的优势:内嵌 Tomcat,一个 jar 就能跑起来,不用单独装容器;自动配置让数据源、Redis、Jackson 这些组件的接入成本降到最低;生态足够成熟,遇到问题在社区里几乎都能找到答案。版本上我选了 2.7.x,没有奔着 3.x 去——当时 SpringBoot3 刚出来,对 Java 17 有硬性要求,很多中间件客户端的兼容性还在磨合期,没必要拿一个交付型项目去当小白鼠。

1.2 Vue3 到底比 Vue2 强在哪:Composition API 不是换个写法那么简单

说实话,Vue2 我写得挺顺手的,Options API 在中小项目里其实够用。但接 Vue3 是趋势,Vue2 在 2023 年底就停止维护了,新项目再起 Vue2,等于从第一天就开始背技术债。

Vue3 真正让我觉得"值"的,是 Composition API 带来的逻辑复用能力。同一个健康上报的校验逻辑,可以在登记页面和修改页面里直接复用同一个useHealthForm函数,不用再靠 mixin 那种容易命名冲突的方案。另外 Vue3 基于 Proxy 的响应式系统,在数据量稍大的列表页里性能表现确实比 Vue2 的 defineProperty 方案要从容。

热词里有人提到"vue3 的 ref 万能对象",我理解这个说法——ref在 Vue3 里既能包基本类型又能包对象,模板里自动解包,确实是无脑选择。但实战中我会提醒团队:对象类型尽量用reactive,只在需要整体替换、或者要传给子组件作为响应式引用时才用ref,这样代码可读性更好。

1.3 MyBatis-Plus 的价值:CRUD 只写一次,后面全在写业务

选 MyBatis-Plus 是个很务实的决定。项目要求"含文档"、可交付、可二次开发,意味着代码得能被后来者快速接手。MyBatis-Plus 提供了BaseMapper和IService,单表 CRUD 几乎零 SQL,分页插件也现成。我不需要为每张表写一套 XML 的 insert、update、selectById——这些无状态、无业务含义的操作,交给框架是合理的。

MySQL8.0 在这个项目里其实是配合着 MyBatis-Plus 一起选的。MySQL 5.7 到 8.0 的跳跃,主要体现在字符集默认值、窗口函数支持和性能提升上。当然,8.0 也带来了一些历史包袱的问题,比如加密插件变更导致的客户端连接报错,后面我会专门用一节来讲怎么处理。

2. 从零搭建前后端工程骨架:那些容易被跳过的初始化细节

2.1 后端工程初始化:Maven 依赖的版本是这个项目的第一个坑

后端我用 Spring Initializr 生成基础工程,Java 版本锁定 8。很多人会问:SpringBoot2 明明支持 Java 11,为什么不用?因为交付型项目要考虑甲方环境,JDK8 在服务器上最通用,出了问题也好找替代方案。

Maven 的pom.xml里最需要注意的是版本管理。当时踩过一个典型的坑:直接用 MyBatis-Plus 3.5.x 的默认版本,它能兼容 SpringBoot2,但如果你把 MySQL 驱动依赖写成了旧的mysql-connector-java5.1.x,连 MySQL8.0 时数据库连接池会不停报错。正确做法是使用带com.mysql坐标的 8.x 驱动,或者直接用mysql-connector-j这个新坐标。

<dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.5.3.1</version> </dependency> <dependency> <groupId>com.mysql</groupId> <artifactId>mysql-connector-j</artifactId> <scope>runtime</scope> </dependency>

项目结构上我没有开多模块,就用单模块打包。原因是:这个系统的代码量还没到需要拆 api、service、dal 多模块的程度,单模块配合清晰的包名划分,反而更容易让接手的人看懂。包结构大概是controller / service / mapper / entity / common这样五个主包,后面通用 CRUD 服务就放在 common 里。

2.2 前端工程初始化:Vite 比 Webpack 快在哪里

前端用的是npm create vue@3的官方脚手架,Vite 作为构建工具。Vite 在开发环境下的冷启动速度和热更新体验,比 Webpack 时代的 Cra 或 Vue CLI 强太多。项目里组件库选的 Element Plus,和 Vue3 的配合是官方级别的,后台管理系统常见的表格、表单、弹窗、标签页组件都有现成的。UI 组件这块没必要重复造轮子。

初始化时一个容易踩的细节是:Element Plus 默认是按需引入的,如果用全局引入方式,打包体积会很大;但如果按需引入,又要额外配unplugin-vue-components。我直接用了全量引入——对于内部管理系统来说,首屏体积的优化优先级没那么高,换来的是写页面时不用担心哪个组件忘了 import。

Vite 的代理配置也要在初始化阶段就设好,不然前后端联调时跨域问题会让你怀疑人生。我在vite.config.ts里做了/api前缀的代理,后端 Controller 统一用/api开头,这样生产环境部署时通过 Nginx 反代也不用手忙脚乱改代码。

server: { proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true } } }

2.3 前后端的目录职责划分:一开始就约定好,后面少吵架

前后端协作的项目,最容易出问题的是"接口边界不清晰"。我在项目启动前一天花半小时给团队定了一个简单的约定:后端只负责数据校验、业务处理、数据持久化;前端只负责交互逻辑、展示逻辑、表单校验,不直接拼 SQL 或操作业务状态机。

后端 Controller 层的命名直接按资源走:/api/isolated-person、/api/health-record、/api/room。前端 src 下按api / views / components / composables四个目录分。这样约定之后,前后端联调几乎没有出现过"这个接口到底归谁管"的争论。

3. 通用 CRUD 服务的设计:让每次增删改查不重复造轮子

3.1 为什么叫"无状态增删改查",以及它解决了什么

这个项目热词里有句描述特别准:基于 mybatis-plus db 工具类实现无状态增删改查。所谓无状态,是指这里的增删改查不绑定任何一张具体表的业务逻辑,拿到实体类和主键就能操作。它解决的核心痛点是:项目里有十几张表,如果每张表都写一套 Mapper 接口 + Service 实现 + Controller 方法,那光 CRUD 就够写三天。

MyBatis-Plus 其实已经给了两个现成的底座:BaseMapper<T>提供单表 CRUD 方法,IService<T>提供更强的通用服务能力。但真正到业务层,每张表的 Service 还是得新建接口、新建实现类、然后调父类方法。我的做法是多走一步:抽一个BaseService,把通用的分页查询、列表查询、按 id 启停这些操作全部放进去,子类只继承它,业务代码里几乎不出现save、removeById这种原始调用。

3.2 从 MyBatis-Plus 的 Db 工具类说起:什么时候可以直接用

MyBatis-Plus 从 3.5.x 版本开始内置了Db工具类,静态方法直接操作任意实体。比如Db.lambdaQuery(Person.class).eq(Person::getStatus, 1).list(),不需要注入任何 Mapper 就能完成一次查询。这个工具类的出现,让那些只有一两处数据访问的临时方法变得非常轻量。

但我在实际项目里对它做了限制:只允许在 Service 内部使用 Db 工具类,Controller 不允许直接调。原因有两个:一是为了事务控制——Db 工具类的操作不会被 Spring 的事务注解自动管理,如果你的方法里先查询后更新,中间出了异常,Db 工具类的操作不会自动回滚;二是为了代码规范,如果 Controller 里到处是Db.lambdaQuery,接手的人会困惑"这项目的 Mapper 到底去哪了"。

3.3 自己封装 BaseService 和 BaseController:代码量确实少了一大半

实际我写了一个BaseService<T>接口,定义了pageList、getById、saveOrUpdate、deleteByIds、changeStatus这几个通用方法。T 是实体类型,实现类里用 MyBatis-Plus 的ServiceImpl作为父类。子类继承后,就自动拥有了所有通用方法。

public abstract class BaseServiceImpl<M extends BaseMapper<T>, T> extends ServiceImpl<M, T> implements BaseService<T> { @Override public PageResult<T> pageList(PageQuery query) { Page<T> page = new Page<>(query.getPageNum(), query.getPageSize()); LambdaQueryWrapper<T> wrapper = Wrappers.lambdaQuery(); // 通用排序、过滤逻辑 return PageResult.of(this.page(page, wrapper)); } }

Controller 层我也抽了BaseController<T>,里面统一处理了响应包装:Result.ok(data)、Result.fail(message)。所有 Controller 返回的数据结构都是{ code: 200, data: {...}, message: "success" }这种格式。前端 axios 拦截器统一处理错误码,不再需要每个页面单独判断。

这里有一个很重要的心得:通用化程度不是越高越好。我见过有人把 Controller 也做成完全通用的,一个/api/generic/{entityName}的接口处理所有表的 CRUD——这种设计看起来极简,实际一用就崩,因为你无法对不同的实体做不同的参数校验和权限控制。我的边界是:Controller 保留每个资源的独立性,只是把公共逻辑下沉到 BaseController。

4. 隔离管理系统的核心业务实现:以人员流转为主线

4.1 业务全景:一张人员状态机把整个系统串起来

隔离管理系统本质上是一个状态流转系统。一个人从进入隔离区到解除观察,经历"待登记 -> 隔离中 -> 观察期 -> 已解除",中间还有"异常转诊"这种分支状态。我先用一张状态映射表把流转关系定死,再据此设计数据库字段和前端页面。

当前状态允许的操作目标状态
待登记确认入住隔离中
隔离中上报异常异常观察
隔离中到期评估观察期
异常观察复核正常隔离中
观察期期满登记已解除

这张表看起来简单,但它是整个系统设计的锚点。数据库里每个隔离人员的记录都带status字段,每次状态更新都会写入操作日志表。酒店的流程管理、健康上报模块、房间分配模块,全部围绕着这个状态机展开。

4.2 人员登记与档案管理:一个典型的增删改查是怎么变成业务的

人员登记页面包含的信息不算复杂:姓名、身份证号、联系电话、来源地、入住日期、关联房间号。但落到业务层,就不能只是简单地 insert 一条人员记录,还需要做三件事。

第一,身份证号的重复校验。一个人不可能在同一个隔离点有两条有效记录,这个校验不能只在前端做,后端 Service 里必须用 LambdaQueryWrapper 按身份证号查一次,并且要排除掉状态为"已解除"的历史数据。第二,房间号与状态的联动。登记时必须把房间状态从未占用改为已占用,这个操作要放在同一个事务里。第三,生成隔离周期。默认隔离周期是 14 天,根据入住日期自动算出预计解除日期,同时生成一批未来 14 天的健康上报空记录,这样前端日历面板可以直接展示。

我曾见过一个很不合理的做法:前端在登记成功后,又发了第二个请求去初始化健康记录。一旦第二个请求失败,人员登记了但每天的填报页面是空的。这种跨请求的一致性问题是典型的分布式事务陷阱,在一个单体系统里完全可以通过事务避免掉。

4.3 健康上报与异常预警:定时任务和状态联动

健康上报是一个高频次操作,每个人每天至少一次,上报的数据包括体温、咳嗽症状、是否接触疑似病例等。这个模块用到了两个技术点:一个是 MyBatis-Plus 的条件构造器做批量查询,另一个是 SpringBoot 自带的定时任务。

每天凌晨跑一个定时任务,扫描当天还没上报的人,给他们推送提醒。同时统计上报率,低于某个阈值时给管理人员发送汇总消息。这个功能用@Scheduled(cron = "0 30 6 * * ?")就能实现,不需要引入 Quartz。需要注意的坑是:@EnableScheduling注解极易漏加,加上之后还要确认任务线程池的大小,默认单线程在任务耗时稍长时会产生阻塞。

异常预警的逻辑则依托健康上报表里的异常标记字段。一旦某条上报记录里的体温超过阈值,系统立即把该人员状态改为"异常观察",同时生成一条异常记录,前端监控大屏通过 WebSocket 或轮询感知到变化后弹出提示。这里我用的是轮询,因为实际场景里对实时性的要求没高到必须上 WebSocket,轮询 10 秒一次的方案在实现成本和服务器压力上都更可控。

4.4 房间分配与容量统计:一个必须做权限控制的功能点

房间分配这个功能看名字很简单,但它是整个系统里最容易"写歪"的地方。核心难点在于并发,两个管理员同时分配同一间房时,后端必须保证只有一个请求能成功。我用了数据库条件更新来解决,在 UPDATE 语句的 WHERE 条件里加上status = 0,然后用返回的受影响行数来判断是否抢房成功。

UPDATE room SET status = 1 WHERE id = ? AND status = 0

MyBatis-Plus 里对应的写法是update(entity, wrapper),wrapper 里带eq(Room::getStatus, 0),最后判断updateCount > 0。这个方案比先查后改的流程少了加锁环节,性能更好,也不会出现超卖。容量统计则简单很多,一个group by status的查询就能得到各类房间的数量,前端用卡片和图表展示。

5. Vue3 前端的复用设计:列表页、表单页、状态变更页的抽象

5.1 登录鉴权与路由守卫:不要把 token 校验写散在每一个页面

后台管理系统都有用户体系,这个项目也不例外。我选了比较轻量的方案:登录接口返回 token,前端存到 localStorage,axios 请求拦截器自动带上Authorization头,响应拦截器统一处理 401 跳回登录页。路由守卫用的是 Vue Router 的beforeEach,判断没有 token 就重定向到/login。

这里有一个值得说的细节:动态路由和静态路由的取舍。对于隔离管理系统这种角色不复杂的场景,我偏向静态路由加菜单权限控制,而不是根据后端返回的菜单列表动态生成路由。原因是动态路由在刷新页面时要重新拉取菜单、重新匹配路由,处理不当时会有白屏闪烁。而我们的角色类型就那么两种——管理员和普通工作人员,静态路由注册好,前端根据角色字段控制菜单显示,实现简单,体验也更稳定。

5.2 列表页的抽象:search 条件、表格、分页三件套

系统中至少有五六个列表页,如果每个页面都复制粘贴一遍 Element Plus 的表格和分页代码,那页面代码会膨胀得很难维护。我抽了一个通用列表组件ProTable,它接收三个核心配置:columns描述表格列,searchConfig描述搜索表单的字段,apiMethod是获取数据的接口函数。

这样每个具体的列表页就变成一段很薄的配置代码:

const config = { columns: [ { prop: 'name', label: '姓名' }, { prop: 'idCard', label: '身份证号' }, { prop: 'status', label: '状态', type: 'tag', tagMap: statusTagMap } ], searchConfig: [ { prop: 'name', label: '姓名', type: 'input', placeholder: '请输入姓名' }, { prop: 'status', label: '状态', type: 'select', options: statusOptions } ], apiMethod: fetchPersonList }

不要小看这个抽象,它至少让列表页的平均代码量少了一半,而且把"新增一列"的改动从改模板变成了改一个对象属性。更关键的是,所有列表页的排序、分页、加载状态、异常提示逻辑都统一了,后端返回的数据结构只要约定一致,前端几乎不用额外适配。

5.3 表单页的交互细节:动态添加行与校验时机

表单方面有一个非常高频的需求:人员登记时可能要动态添加多名同行人员的健康信息。热词里提到的"vue3 动态添加删除 form 表单一行数据"就是这个场景。Vue3 里用reactive数组 + 循环渲染可以轻松实现,给数组 push 一个空对象模板,删除时splice。需要注意的点是,Element Plus 的表单校验要针对动态行的每一个prop单独配置,通常用:prop="'members.' + index + '.name'"的形式绑定。

我在这个项目里吃过的亏是:动态添加行时,新增的行的字段初始值是空字符串还是 undefined,会影响校验规则里的required是否正常触发。建议统一初始化为{ name: '', health: '', temperature: null },空字符串和 null 都会正常触发必填校验,而 undefined 在某些组件里会被跳过。

6. MySQL8.0 接入实录:安装、配置与第一周踩的坑

6.1 Docker 安装 MySQL8.0:一条命令起来的库,三个细节不能少

项目的标准部署环境我给了 Docker Compose 方案,这样不管在测试服务器还是客户的 Linux 环境上,都能快速拉起一套 MySQL8.0。Docker 安装本身不复杂,一条docker run就能跑起来,但有几个非调不可的参数:字符集、时区和数据卷。

docker run -d \ --name mysql8 \ -p 3306:3306 \ -e MYSQL_ROOT_PASSWORD=your_password \ -e TZ=Asia/Shanghai \ -v /data/mysql:/var/lib/mysql \ mysql:8.0

TZ环境变量负责时区,不设置的话默认是 UTC,后面 Java 程序里 LocalDateTime 和数据库中 datetime 字段的对应关系就会乱。数据卷必须挂载,否则容器一删数据全没了。还有一个容易被忽略的:默认字符集虽然 MySQL8.0 已经是 utf8mb4,但保险起见我会在启动后执行一条ALTER DATABASE xxx CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci,确保和 Java 端的字符串编码完全一致。

6.2 时区与连接参数:连接串里少写一个参数就噩梦

MySQL8.0 的 JDBC 连接串和 5.7 有区别。在 8.0 下,连接串必须显式加上serverTimezone=Asia/Shanghai,否则驱动会报时区错误。完整的连接串长这样:

jdbc:mysql://localhost:3306/isolated_db?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false&allowPublicKeyRetrieval=true

allowPublicKeyRetrieval=true是 MySQL8.0 加密插件带来的另一个必填项。8.0 默认用户认证插件是caching_sha2_password,首次连接时客户端需要向服务器请求公钥进行密码传输加密,如果不允许公钥获取,会报Public Key Retrieval is not allowed。这个错误在测试环境第一次连接时几乎必然遇到。

6.3 客户端工具连不上 MySQL8.0:不是密码错了,是认证插件变了

团队里用 Navicat 或者其他老版本客户端的人,在连接 MySQL8.0 时大概率会遇到Authentication plugin 'caching_sha2_password' cannot be loaded的报错。正常安装的 MySQL8.0 不会用 mysql_native_password,这是 5.7 时代的认证方式。

我的处理方式是:不强求把 MySQL8.0 的默认认证插件改回 old,这样太绕了。直接让所有同事把数据库客户端升级到支持 caching_sha2_password 的版本即可。如果是连接池、程序里用的驱动,只要用了 8.x 的 MySQL Connector/J,天然支持新认证方式。文中一开始就强调不要用 mysql-connector-java 5.x,最大的原因就是认证插件不兼容。

MySQL8.0 还有一个和 5.7 的行为差异:默认的sql_mode里多了ONLY_FULL_GROUP_BY。如果项目的 SQL 里用了select *加group by,在 5.7 下可能不报错,在 8.0 下就会直接报错。我的建议是尽量规范 SQL,按需求列字段并用聚合函数包裹非分组字段;如果确实要兼容历史代码,可以通过修改sql_mode来放松限制,但这不是长久之计。

6.4 关于 Docker 容器中的 MySQL:8.0 和宿主机资源的一个插曲

还有个容易忽略的坑,容器跑起来的 MySQL8.0 在低配服务器上启动特别慢。我一开始没注意,以为卡死了反复重启容器,导致数据目录损坏。后来发现 MySQL8.0 初始化时需要做大量的系统表操作,内存低于 1GB 的机器初始化时间可能长达几分钟。解决方案是配置performance_schema=OFF,或者给容器分配足够的内存。这个经验让我意识到,Docker 虽然方便,但资源限制一定是你在生产环境要考虑的第一优先级。

7. "含文档"的含金量:交付项目时我整理了哪些资料

7.1 除了源码,一个可交付项目必须有这几样东西

标题里带了"含文档"三个字。在项目交付中,源码只是一部分,文档才决定这个项目能不能被顺利接手、二次开发。我最终交付的资料包里包含下面这些内容:

文档 / 资料说明
数据库建表 SQL全部建表语句,包含注释、索引、初始演示数据
数据库设计说明每张表的字段含义、枚举值说明、表间关系
接口文档维护成 Markdown 或导入 Apifox,覆盖全部后端接口
部署文档从环境准备到打包、启动、Nginx 配置、数据库初始化的完整步骤
前端环境说明Node 版本要求、依赖安装命令、代理配置说明
演示账号说明管理员的初始账号密码、功能演示路径

数据库设计说明是被最多人忽略、但实际上最有价值的文档。我见过太多项目只给一个 SQL 文件,字段名没人解释,枚举值靠猜。在隔离管理系统里,"状态 0 1 2 3 是什么意思"如果不写清楚,接手的人光看代码要花半天。我在表注释里就把每个字段的枚举值都写了,表结构本身变成了文档的一部分。

7.2 接口文档与演示数据:让二次开发的人能立刻跑起来

接口文档我习惯在源码里加一个docs/api.md,维护成本低,和代码同仓库,不会产生版本漂移。内容不需要像 Swagger 那样面面俱到,但至少要把每个接口的请求路径、请求参数、响应示例写清楚——尤其是那些涉及状态流转的接口,比如"确认入住"这个动作,它同时修改了哪些表、有没有事务边界。

演示数据是另一个关键。我提供了一个init-data.sql,里面有十几个模拟的隔离人员记录、几十条健康上报记录、房间数据,覆盖了系统的所有状态。这样接手的人拿到项目后,导入数据库就能看到数据效果,不需要自己慢慢造数据。这个细节看似不起眼,但对项目的首次体验影响巨大。

7.3 项目部署流程:从后端 jar 到前端静态资源再到 Nginx

部署文档写的是标准的前后端分离部署流程:后端 Maven 打包成 jar,java -jar直接启动;前端npm run build生成 dist 目录,交给 Nginx 托管。Nginx 里核心配置是静态资源路径和反向代理/api到后端地址。

这里要提醒一个实际遇过的坑:前端构建出来的资源引用了绝对路径/assets/xxx.js,如果部署到子路径下(比如域名后面带个/admin),就会出现白屏。解决方案是在前端项目里的vite.config.ts设置base: './',这样资源引用就变成相对路径,放到哪个目录都能跑。这个坑不跑一次部署踩不到,但踩到了极其蛋疼。

7.4 一套源码如何被别人"用得起来":我的最后一个建议

最后给正在做同类项目的人一个建议:如果这套系统定位是"分享"或者"交付",请一定要把第一个启动体验做好。我见过很多开源项目,代码写得很漂亮,但 README 写的含糊其辞,数据库脚本放得乱七八糟,用户克隆下来第一步就跑不起来。所谓"含文档",不是在仓库里放一个 readme.md 就算数,而是要让一个完全没有参与过这个项目的人,照着文档能在半小时内把系统跑起来,并且看得懂数据、走得通流程。这个标准不高,但能达到的项目真的不多。

回到这个隔离管理系统本身。技术上它不算多难,但它逼着我完整地走了一遍从业务抽象到工程化交付的全过程:状态机设计让业务逻辑有了锚点,通用 CRUD 服务把重复代码压到了最低,MySQL8.0 和 Docker 的组合在部署阶段省了大量时间。最让我满意的其实是最后那套文档,因为三个月后我自己回来看这个项目,也是靠那份文档迅速找回了所有上下文。说白了,代码是写给机器的,文档是写给三个月之后的自己看的——这句话在我做过这么多项目之后,依然成立。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询