news 2026/9/26 5:59:06

SpringBoot+Vue民宿管理系统实战:从数据库设计到订单状态机

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
SpringBoot+Vue民宿管理系统实战:从数据库设计到订单状态机

简介:这份资源是一篇基于SpringBoot与Vue的Java民宿管理系统毕业论文文档,面向计算机相关专业需要完成毕业设计的学生,以及想参考前后端分离项目实战的开发者。论文围绕民宿管理场景,从需求分析、三层架构设计到功能实现展开,涵盖管理员对用户、新闻公告、民宿信息的管理,以及用户浏览民宿、查看公告等模块,可帮助读者理解如何用SpringBoot处理业务逻辑、Vue构建展示层、MySQL完成数据存储与检索。压缩包内共1个doc文件,约1.11MB,即完整论文正文,包含摘要、目录、开发环境与技术、系统分析与数据库设计等章节,结构完整,可直接作为选题参考或写作模板。目前已有249人学习下载,适合需要快速搭建论文框架、梳理系统设计思路的读者借鉴使用。

1. 民宿管理系统为什么值得用 SpringBoot + Vue 重写一遍

很多做毕业设计或接私活的朋友,第一次接触「民宿管理系统」这个题目时,脑子里蹦出来的往往是「不就是个 CRUD 嘛」。真动手才发现,民宿这个场景比酒店管理更碎:房源可能是分散的独栋小院,一间房在不同平台有不同价格,入住时间按整晚还是按小时算,退房后还要算清洁费。用一套老式的 JSP + Servlet 硬写,光是订单状态流转就能把人绕晕。SpringBoot + Vue 这套组合之所以在近几年的课程设计和中小型项目里反复出现,核心原因是它把「后端接口」和「前端交互」彻底拆开了:后端只负责数据与业务规则,前端只负责展示与操作,两边通过 JSON 通信。这样一来,房源、订单、用户、评价这几个模块可以并行开发,调试时也能单独定位是接口问题还是页面问题。这篇文章面向的是需要把民宿管理系统真正跑起来的人——不管你是要交毕业论文、做课程设计,还是给一个小型民宿做内部工具,下面这套路径都能照着复现。

2. 技术选型与数据库设计:先定骨架再写代码

2.1 为什么是 SpringBoot 而不是传统 SSM

传统 SSM 要配 web.xml、applicationContext.xml、spring-mvc.xml,光配置文件就能写满三页。SpringBoot 的自动装配把大部分默认配置收进 starter 里,一个 application.yml 就能启动。对于民宿管理系统这种模块数量中等、但需要快速迭代的项目,SpringBoot 的优势体现在三个地方:第一,内嵌 Tomcat,打成 jar 直接跑,不用单独装容器;第二,starter 依赖把版本冲突提前解决,比如 spring-boot-starter-web 已经锁定了 Jackson、Tomcat、Spring MVC 的兼容版本;第三,配合 MyBatis-Plus 或 JPA,单表增删改查几乎不用写 SQL。常见做法是后端用 SpringBoot 2.7.x 或 3.x,数据库 MySQL 8,连接池用 HikariCP(SpringBoot 默认自带),ORM 选 MyBatis-Plus,因为民宿管理里分页查询、条件筛选特别多,MyBatis-Plus 的 QueryWrapper 能省掉大量手写 SQL。

2.2 民宿业务的数据表怎么切

民宿管理系统的表设计不能照搬酒店,因为民宿的房源属性更复杂。我一般会切成七张核心表:用户表、房源表、房型表、订单表、评价表、图片表、管理员表。房源表和房型表要分开,因为一个院子可能包含「大床房」「亲子房」两种房型,价格和库存都不同。订单表里必须留出 check_in_date、check_out_date、guest_count、total_price、status 这几个字段,status 用枚举值而不是数字,方便排查。下面是一个精简的建表 SQL,可以直接在 MySQL 里执行。

-- 房源表:一个民宿院子或一套独立房源 CREATE TABLE `house` ( `id` BIGINT NOT NULL AUTO_INCREMENT, `title` VARCHAR(100) NOT NULL COMMENT '房源名称', `address` VARCHAR(255) DEFAULT NULL COMMENT '详细地址', `cover_img` VARCHAR(255) DEFAULT NULL COMMENT '封面图', `description` TEXT COMMENT '房源描述', `status` TINYINT DEFAULT 1 COMMENT '1上架 0下架', `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 房型表:属于某个房源下的具体可订单元 CREATE TABLE `room_type` ( `id` BIGINT NOT NULL AUTO_INCREMENT, `house_id` BIGINT NOT NULL, `name` VARCHAR(50) NOT NULL COMMENT '房型名', `price` DECIMAL(10,2) NOT NULL COMMENT '每晚价格', `stock` INT DEFAULT 0 COMMENT '可订数量', `bed_count` INT DEFAULT 1, PRIMARY KEY (`id`), KEY `idx_house` (`house_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 订单表:核心业务表,状态流转都在这里 CREATE TABLE `orders` ( `id` BIGINT NOT NULL AUTO_INCREMENT, `user_id` BIGINT NOT NULL, `room_type_id` BIGINT NOT NULL, `check_in_date` DATE NOT NULL, `check_out_date` DATE NOT NULL, `guest_count` INT DEFAULT 1, `total_price` DECIMAL(10,2) NOT NULL, `status` VARCHAR(20) DEFAULT 'PENDING' COMMENT 'PENDING/PAID/CONFIRMED/CANCELLED', `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), KEY `idx_user` (`user_id`), KEY `idx_room` (`room_type_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

这段 SQL 里最需要注意的是 orders 表的 status 字段。用字符串而不是数字,是因为在前后端联调时,前端拿到 "PENDING" 比拿到 0 更容易判断,日志里也一眼能看懂。索引方面,user_id 和 room_type_id 都要加,因为「我的订单」和「房型订单列表」是两个高频查询。check_in_date 和 check_out_date 用 DATE 而不是 DATETIME,因为民宿按天计费,不需要精确到秒,这样还能避免时区带来的玄学问题。

2.3 前后端分离后的接口约定

前后端分离最容易翻车的地方不是代码,而是接口约定。我一般会在项目根目录放一个 api.md,把每个接口的路径、方法、请求参数、返回结构写清楚。比如订单创建接口统一返回{ code: 200, msg: "success", data: {...} },前端在 axios 拦截器里统一处理 code 不等于 200 的情况。这样后端改字段时,前端只需要看 api.md 就能同步。Vue 这边用 axios 而不是 fetch,因为 axios 的拦截器和错误处理更顺手。路由用 vue-router 的 history 模式,但要注意 Nginx 部署时加 try_files,否则刷新页面会 404。

3. 后端接口实现:从房源查询到订单状态机

3.1 用 MyBatis-Plus 写房源分页查询

房源列表是民宿管理系统里访问量最大的接口,通常要支持按地址模糊搜、按价格区间筛、按上架状态过滤,还要分页。如果手写 SQL,动态条件拼接很容易漏掉AND或者多一个逗号。MyBatis-Plus 的 QueryWrapper 可以把条件写成链式调用,代码可读性高很多。下面是一个典型的 Service 层实现。

@Service public class HouseServiceImpl extends ServiceImpl<HouseMapper, House> implements HouseService { @Override public Page<HouseVO> pageQuery(HouseQuery query) { // 构造分页对象,current 是页码,size 是每页条数 Page<House> page = new Page<>(query.getPageNum(), query.getPageSize()); QueryWrapper<House> wrapper = new QueryWrapper<>(); // 地址模糊搜索,只有传了 keyword 才拼接条件 if (StringUtils.hasText(query.getKeyword())) { wrapper.like("address", query.getKeyword()); } // 状态过滤,null 表示查全部 if (query.getStatus() != null) { wrapper.eq("status", query.getStatus()); } // 按创建时间倒序,新上架的房源排前面 wrapper.orderByDesc("create_time"); Page<House> result = this.page(page, wrapper); // 把 Entity 转成 VO,补充房型最低价等字段 return result.convert(this::toVO); } }

这段代码的关键参数有三个:pageNum 默认给 1,pageSize 默认给 10,但前端可以传 20 或 50,后端要限制最大值比如 100,防止一次拉太多数据拖垮数据库。keyword 用 like 而不是 eq,因为用户搜「西湖」时希望匹配到「西湖区」「西湖边」等地址。orderByDesc 用 create_time 而不是 id,因为自增 id 在数据迁移后可能不连续,时间排序更符合业务直觉。转换 VO 的时候,我一般会再查一次房型表,把该房源下的最低价和房型数量带出来,这样前端列表页不用再发额外请求。

3.2 订单创建时怎么防止超卖

订单创建是民宿管理系统里最需要小心的地方。假设一个房型 stock 是 2,两个用户同时下单,如果代码里先查 stock 再减,中间没有锁,就会卖出 3 间。常见的做法有两种:一种是在 SQL 里用UPDATE room_type SET stock = stock - 1 WHERE id = ? AND stock > 0,根据 affected rows 判断是否扣减成功;另一种是用 Redis 分布式锁,但民宿项目一般并发不高,用数据库乐观锁就够了。下面这段代码展示了第一种做法。

@Transactional(rollbackFor = Exception.class) public Order createOrder(Long userId, OrderCreateDTO dto) { // 先校验房型是否存在且库存充足 RoomType roomType = roomTypeMapper.selectById(dto.getRoomTypeId()); if (roomType == null || roomType.getStock() <= 0) { throw new BizException("房型库存不足"); } // 原子扣减库存,stock > 0 是防止并发下扣成负数 int affected = roomTypeMapper.decreaseStock(dto.getRoomTypeId()); if (affected == 0) { throw new BizException("手慢了,库存已被抢完"); } // 计算总价:晚数 * 单价,这里用 ChronoUnit 算天数差 long nights = ChronoUnit.DAYS.between(dto.getCheckInDate(), dto.getCheckOutDate()); if (nights <= 0) { throw new BizException("离店日期必须晚于入住日期"); } Order order = new Order(); order.setUserId(userId); order.setRoomTypeId(dto.getRoomTypeId()); order.setCheckInDate(dto.getCheckInDate()); order.setCheckOutDate(dto.getCheckOutDate()); order.setTotalPrice(roomType.getPrice().multiply(BigDecimal.valueOf(nights))); order.setStatus("PENDING"); orderMapper.insert(order); return order; }

decreaseStock 对应的 Mapper 方法里写的是UPDATE room_type SET stock = stock - 1 WHERE id = #{id} AND stock > 0,返回 int 表示影响行数。如果返回 0,说明库存已经被别人扣完了,直接抛业务异常。@Transactional 保证扣库存和插订单在同一个事务里,任何一步失败都会回滚。这里有个血泪经验:check_in_date 和 check_out_date 一定要在 DTO 里用 @DateTimeFormat 或 @JsonFormat 指定格式,否则前端传 "2024-01-01" 字符串时,后端可能解析失败或者按 UTC 时区差一天。

3.3 订单状态流转与定时任务

订单状态从 PENDING 到 PAID 再到 CONFIRMED,中间还可能 CANCELLED。状态流转不能随便改,我一般会写一个状态机校验方法,只允许特定状态跳到下一个状态。另外,未支付的订单需要超时自动取消,释放库存。SpringBoot 里用 @Scheduled 就能实现,不需要引入额外框架。

@Component public class OrderTimeoutTask { @Autowired private OrderMapper orderMapper; @Autowired private RoomTypeMapper roomTypeMapper; // 每 5 分钟执行一次,取消 30 分钟未支付的订单 @Scheduled(cron = "0 0/5 * * * ?") public void cancelTimeoutOrders() { LocalDateTime deadline = LocalDateTime.now().minusMinutes(30); List<Order> timeoutOrders = orderMapper.selectList( new QueryWrapper<Order>() .eq("status", "PENDING") .lt("create_time", deadline) ); for (Order order : timeoutOrders) { order.setStatus("CANCELLED"); orderMapper.updateById(order); // 释放库存,加回去 roomTypeMapper.increaseStock(order.getRoomTypeId()); } } }

cron 表达式0 0/5 * * * ?表示每 5 分钟的第 0 秒触发。deadline 取当前时间减 30 分钟,只查 PENDING 且创建时间早于 deadline 的订单。释放库存用 increaseStock,对应UPDATE room_type SET stock = stock + 1 WHERE id = #{id}。这里要注意,定时任务在多实例部署时会重复执行,如果以后要上多台服务器,得加分布式锁或者用数据库行锁。单机部署的话,这个方案足够稳。

4. 前端 Vue 页面与联调:把接口变成能点的界面

4.1 Vue 项目初始化和路由配置

Vue 这边我一般用 Vue CLI 或 Vite 创建项目,Vue 3 配合 Element Plus 做后台管理界面。民宿管理系统的前端分两块:用户端(浏览房源、下单)和管理端(房源维护、订单处理)。路由用 vue-router 的嵌套路由,管理端放在 /admin 下,用户端放在 / 下。下面是一个路由配置片段。

// router/index.js import { createRouter, createWebHistory } from 'vue-router' const routes = [ { path: '/', component: () => import('@/layouts/UserLayout.vue'), children: [ { path: '', name: 'Home', component: () => import('@/views/Home.vue') }, { path: 'house/:id', name: 'HouseDetail', component: () => import('@/views/HouseDetail.vue') }, { path: 'order/confirm', name: 'OrderConfirm', component: () => import('@/views/OrderConfirm.vue') } ] }, { path: '/admin', component: () => import('@/layouts/AdminLayout.vue'), meta: { requiresAuth: true }, children: [ { path: 'house', name: 'AdminHouse', component: () => import('@/views/admin/HouseList.vue') }, { path: 'order', name: 'AdminOrder', component: () => import('@/views/admin/OrderList.vue') } ] } ] const router = createRouter({ history: createWebHistory(), routes }) // 全局前置守卫,管理端需要登录 router.beforeEach((to, from, next) => { if (to.meta.requiresAuth && !localStorage.getItem('token')) { next({ path: '/login', query: { redirect: to.fullPath } }) } else { next() } }) export default router

路由懒加载用() => import(...),这样首页不会一次性加载管理端的代码。meta.requiresAuth 标记需要登录的页面,beforeEach 里检查 localStorage 里的 token。redirect 参数记录用户原本想去的页面,登录后跳回去。这里有个容易忽略的点:vue-router 的 history 模式在开发环境没问题,但打包后部署到 Nginx,必须加try_files $uri $uri/ /index.html;,否则用户刷新 /admin/house 会直接 404。

4.2 axios 封装与接口联调

前端调后端接口,我习惯把 axios 封装一层,统一加 token、统一处理错误码。这样每个页面里只写业务逻辑,不用重复写 headers。

// utils/request.js import axios from 'axios' import { ElMessage } from 'element-plus' const service = axios.create({ baseURL: import.meta.env.VITE_API_BASE_URL || '/api', timeout: 10000 }) // 请求拦截器:自动带上 token service.interceptors.request.use(config => { const token = localStorage.getItem('token') if (token) { config.headers.Authorization = `Bearer ${token}` } return config }) // 响应拦截器:统一处理业务错误 service.interceptors.response.use( response => { const res = response.data if (res.code !== 200) { ElMessage.error(res.msg || '请求失败') // 401 表示登录过期,跳回登录页 if (res.code === 401) { localStorage.removeItem('token') window.location.href = '/login' } return Promise.reject(new Error(res.msg)) } return res.data }, error => { ElMessage.error(error.message || '网络异常') return Promise.reject(error) } ) export default service

baseURL 用环境变量 VITE_API_BASE_URL,开发时指向http://localhost:8080,生产时指向 Nginx 代理的/api。请求拦截器从 localStorage 取 token 塞进 Authorization 头。响应拦截器里判断 code,不等于 200 就弹提示并 reject。401 单独处理,清 token 并跳登录。这样页面里调用接口只需要const data = await request.get('/house/page', { params }),拿到的直接是 data 部分,不用每次写res.data.data。

4.3 房源详情页的日期选择与价格计算

房源详情页是用户下单前的最后一步,日期选择器要禁用已过期的日期,还要实时算总价。Element Plus 的 el-date-picker 支持 disabled-date 属性,可以传入一个函数来禁用日期。

// HouseDetail.vue 中的日期与价格逻辑 const dateRange = ref([]) const totalPrice = ref(0) // 禁用今天之前的日期 const disabledDate = (time) => { return time.getTime() < Date.now() - 24 * 60 * 60 * 1000 } // 监听日期变化,重新计算总价 watch(dateRange, (val) => { if (val && val.length === 2) { const nights = Math.ceil((val[1] - val[0]) / (24 * 60 * 60 * 1000)) totalPrice.value = nights * roomType.price } else { totalPrice.value = 0 } })

disabledDate 里减一天是为了让今天仍可选。watch 监听 dateRange,两个日期都有值时算晚数。用 Math.ceil 而不是 Math.round,因为跨天时差可能不是整 24 小时,向上取整更符合民宿按晚计费的逻辑。totalPrice 直接在前端算,提交订单时后端还会再算一遍,两边结果要一致,否则用户会觉得被坑。这里建议后端返回的单价和前端展示的单价用同一个字段,避免前端写死价格。

5. 避坑与排查:民宿管理系统联调中最容易翻车的五件事

5.1 跨域问题:开发时好好的,一部署就 404

现象是本地 Vue 跑在 5173 端口,SpringBoot 跑在 8080,浏览器控制台报 CORS 错误。原因是浏览器的同源策略拦截了跨域请求。开发阶段可以在 SpringBoot 里加一个全局 CORS 配置,或者用 Vite 的 proxy 把 /api 转发到 8080。生产环境则用 Nginx 反向代理,把 /api 的请求转给后端,这样前端和后端同源,不存在跨域。我一般开发时用 Vite proxy,部署时用 Nginx,两边都不需要后端写 CORS 代码。

5.2 日期格式差一天:前端传 2024-01-01,后端收到 2023-12-31

现象是用户选的入住日期是 1 号,数据库里存成了 31 号。原因是 JavaScript 的 Date 默认按 UTC 时区,而国内是 UTC+8,序列化时可能减了 8 小时。解决办法是在 DTO 的日期字段上加@JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8"),前端传字符串而不是 Date 对象。如果前端用 el-date-picker 的 value-format 属性指定 "YYYY-MM-DD",传的就是纯字符串,后端按 LocalDate 接收,基本不会出时区问题。

5.3 订单状态乱跳:已取消的订单还能确认

现象是用户取消了订单,管理员后台还能点「确认入住」。原因是状态流转没有校验,任何状态都能改成任何状态。解决办法是写一个状态机工具类,定义每个状态允许的下一步。比如 PENDING 只能到 PAID 或 CANCELLED,PAID 只能到 CONFIRMED,CONFIRMED 只能到 COMPLETED。每次更新状态前先校验,不合法就抛异常。这个校验放在 Service 层,不要只靠前端按钮置灰,因为接口是可以被直接调用的。

5.4 图片上传后访问 404:路径配错了

现象是房源图片上传成功,但前端 img 标签加载不出来。原因是后端把文件存到了本地磁盘的某个目录,但静态资源映射没配,或者 Nginx 没配 alias。SpringBoot 里可以用WebMvcConfigurer的 addResourceHandlers 把/upload/**映射到磁盘目录。生产环境更推荐用 Nginx 直接托管静态文件,后端只负责上传,返回文件相对路径。上传时要注意文件名用 UUID 重命名,避免中文名和特殊字符导致 URL 编码问题。

5.5 分页查询总数不对:count 语句被条件干扰

现象是列表只显示了 3 条,但分页组件显示总共 100 条。原因是 MyBatis-Plus 的分页插件在自动生成 count 语句时,如果 QueryWrapper 里带了 orderBy,count 语句可能也会带上,导致统计出错。解决办法是在构造 Page 对象时,把 searchCount 设为 true(默认就是 true),但 orderBy 尽量放在最后,或者用page.setOptimizeCountSql(true)让插件优化 count 语句。如果还是不对,就手写 count 查询,别依赖自动生成。

6. 论文与项目文档的写法:让代码之外的部分也站得住

6.1 毕业论文里技术章节怎么组织

民宿管理系统的论文,评审老师最想看的是「你为什么这么设计」而不是「你用了什么技术」。技术选型章节不要罗列 SpringBoot 的优点,而是对比 SSM 和 SpringBoot 在这个项目里的具体差异,比如配置文件从 3 个减到 1 个,启动时间从 8 秒降到 3 秒。数据库设计章节要放 E-R 图,但更重要的是解释字段为什么这么定,比如订单状态为什么用字符串枚举而不是数字。功能实现章节挑 2 到 3 个核心流程写,比如订单创建和库存扣减,配上流程图和关键代码,不要每个页面都截图。

6.2 接口文档和部署说明的模板

接口文档用 Markdown 写,每个接口一张表,列出路径、方法、请求参数、返回字段、错误码。部署说明写清楚环境要求:JDK 17、MySQL 8、Node 18、Nginx 1.24。后端打包用mvn clean package -DskipTests,前端用npm run build,产物放到 Nginx 的 html 目录。数据库初始化脚本单独放一个 init.sql,包含建库、建表、插入管理员账号。这样别人拿到项目,按文档走一遍就能跑起来,不用问你「为什么我启动报错」。

6.3 一个验证系统是否跑通的最小清单

部署完之后,按这个顺序点一遍:管理员登录 → 新增房源 → 新增房型 → 用户注册 → 浏览房源 → 选择日期下单 → 管理员确认订单 → 用户查看订单状态。每一步都成功,说明核心链路通了。如果某一步失败,先看浏览器 Network 面板的接口返回,再看后端控制台日志。日志里我一般会打log.info("订单创建成功,orderId={}", order.getId()),这样排查时能顺着 orderId 查数据库。这个清单我每次交付前都会跑一遍,比写多少测试用例都实在。

希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/26 5:58:23

MySQL原子DDL、IF NOT EXISTS与OpenTelemetry实战指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/26 5:57:38

AI编程模板库实战:用CLAUDE.md与Agent Skills根治会话失忆

做 CLI 工具的人大概都有过这种经历&#xff1a;代码写完了&#xff0c;换台机器、隔一周再打开终端&#xff0c;一切都要从零开始。AI 编程助手也一样——我最早用 Claude Code 的时候&#xff0c;每一个新会话都在重复解释同一个项目的背景、技术栈、代码规范、测试命令&…

作者头像 李华
网站建设 2026/9/26 5:57:16

U9订单列表中实现PLM零件承认书的校验

公司要求在订单列表中执行提交时&#xff0c;对PLM零件承认书合规记录做一次校验。做过几次了&#xff0c;念念不忘这种管理思维。效果如下。前几天忙于MES系统项目&#xff0c;思维不够清楚&#xff0c;没有做出来给那帮投机取巧的家伙利用上了&#xff01;让它们偷偷乐几天吧…

作者头像 李华
网站建设 2026/9/26 5:56:18

从零手搓旋转目标检测核心算子:Conv2d、BN与SiLU实战

1. 从零手搓旋转目标检测网络&#xff1a;核心算子到底在搓什么做旋转目标检测&#xff08;Rotated Object Detection&#xff09;的人&#xff0c;绕不开一个现实&#xff1a;你可以在GitHub上找到一堆开源框架&#xff0c;配置好环境、改改配置文件就能跑起来&#xff0c;但一…

作者头像 李华
网站建设 2026/9/26 5:53:48

Flink CDC:MongoDB多集合实时同步到ClickHouse

前阵子帮一个做电商数据分析的团队处理数据同步&#xff0c;他们在 MongoDB 里存了用户、订单、商品、库存等七八个集合&#xff0c;这些数据要求实时进 ClickHouse 做 OLAP 分析。最早是每个集合单独写一个同步脚本&#xff0c;MongoDB 侧一有新集合上线就要再开发一轮&#x…

作者头像 李华