做物流信息管理系统这个项目我从2024年底就开始折腾到2025年初才算真正跑顺前后踩了不少坑。今天把这套基于SpringBootVueMyBatisMySQL的完整实现思路拿出来聊一聊。这个项目并不是什么高深莫测的东西但它恰好覆盖了后端接口、前端交互、数据库设计、权限控制、报表导出这些最常被问到的技术点特别适合正在准备项目经验、做毕业设计、或者想快速搭一套内网管理系统的同学。项目源码我是整理好的数据库脚本、前后端环境配置、启动文档都在里面读这篇文章相当于把整个开发过程重新走了一遍。1. 项目整体设计与技术选型思路1.1 为什么选择前后端分离的SpringBootVue组合一提到管理系统很多人第一反应是直接用JSP、Thymeleaf这些服务端模板渲染数据都在后端拼页面开发起来好像更省事。但我选择SpringBootVue前后端分离不是因为赶时髦而是这套分工对物流业务真的更合适。物流信息管理系统最典型的特征是操作频率高、字段多、状态流转快运营人员从开单、调度、到站、签收每一步都要快速反馈。如果所有交互都靠后端渲染页面每个操作都是整页刷新体验会很糟糕而且前端像地图轨迹展示、批量选择承运商、拖拽排序这类交互用Vue的响应式数据模型写起来要自然得多。另外一个实际原因是分工和复用。公司里一般有人擅长Java有人擅长浏览器端前后端分离之后后端只需要暴露一套规范的RESTful接口前端按自己的节奏做页面。同一个后端接口将来如果做小程序端、移动端直接复用不用再额外开发一套。我在项目里把前端工程放在frontend目录端口是8080后端工程是backend目录端口默认8081生产环境再用Nginx做一层转发接口走/api前缀这样联调和部署都省心。技术选型上前端用Vue 3 Vite Element Plus。Vue 3的Composition API在处理运单列表这种多条件筛选、多状态切换的页面时逻辑聚合度更高。Element Plus提供现成的表格、表单、弹窗、树形菜单开发效率非常高。如果你还在用Vue 2我的建议是直接新项目就上Vue 3生态已经非常成熟了没必要抱着旧版本。后端用SpringBoot 2.7.14搭配JDK 8。为什么不用SpringBoot 3说实话SpringBoot 3功能肯定更强但如果你用的是JDK 8环境、老MyBatis版本或某些中间件升级起来会遇到一堆兼容性问题。尤其SpringBoot 3把javax迁移到了jakarta命名空间很多企业内网的私有依赖没跟进服务直接起不来。这个项目的目标是稳定跑通不是尝鲜新特性所以我选了最稳妥的组合SpringBoot 2.7 JDK 8 MySQL 8.0。1.2 MyBatis和MySQL在项目里的实际定位数据持久层我选了MyBatis而不是Spring Data JPA核心原因是物流业务的SQL太复杂了。运单查询经常要联合客户表、线路表、承运商表做条件过滤还要按日期范围、收货人、运单号模糊搜索甚至需要根据下单时间、签收时间做区间统计。MyBatis允许我直接写SQL多表关联、子查询、动态SQL都尽在掌控SQL写一步调一步性能问题直接能在SQL层面看到。JPA这种“自动生成SQL”的方式在简单CRUD上确实省事但是遇到复杂查询要么写JPQL要么还是得走原生SQL反而绕弯子。数据库用MySQL 8.0生产环境是5.7也没问题SQL基本通用。在表设计上我有一点点强迫症字段带注释、统一用bigint做物理主键、创建时间和更新时间用datetime这样后面写代码不会出现“这个字段是干嘛的”这种灵魂拷问。MySQL生态的成熟度无需多言无论是本机安装、Docker部署还是云数据库RDS资料一搜一大把对新手非常友好。物流系统并发量往往不会特别夸张但数据量会增长得比较快单表到几百万条很常见所以索引设计从第一天就要考虑到。2. 物流业务拆解与数据库设计2.1 核心业务模块和实体关系梳理物流信息管理系统听起来很大其实核心就那几条线订单管理、运单管理、仓库管理、承运商管理、结算管理和系统管理。我第一版设计的时候先画业务流转图再抽象数据表这个习惯非常重要。很多新手上来就建表建到后面发现字段不够、关系乱了返工成本出奇地高。你可以把物流订单看作“客户委托”运单看作“实际承运任务”。一个订单可以拆成多个运单比如客户要同时发货到三个城市那就生成三个子运单。运单到了中间节点会产生“到站”“发车”“签收”“异常”等记录这些节点信息再独立成一张运单节点表方便做轨迹回溯和时效统计。核心实体大概包括客户、承运商、线路、仓库、物流订单、运单、运单节点、计费规则、费用结算单、系统用户、角色、菜单权限。实体关系上客户和订单是一对多订单和运单是一对多运单和节点是一对多承运商和线路是多对多角色和菜单是多对多。我先用PowerDesigner画了概念模型再落成MySQL建表脚本。物理表一共20张左右不算多但覆盖了从开单到结算的全流程已经完全能支撑小中型物流企业的日常运营场景。2.2 主要数据表字段设计和索引策略运单表是整个系统的核心字段设计直接影响后续开发。我列举几个关键表的设计思路方便你照着自己设计时参考。运单表waybill的关键字段运单号wb_code用前缀日期自增数生成比如WB20250101001客户id customer_id关联客户表接入线路route_id关联线路表承运商id carrier_id发货地址和收货地址分别存省市区文本和详细地址冗余存储主要是为了列表展示不频繁关联查询货物信息简化成货物名称、件数、重量、体积运费金额、结算状态当前状态status这里的状态值我用了int型字典比如0待调度、1已调度、2运输中、3已签收、4异常、5已取消。状态用数字而非字符串是因为这个字段要加到索引里数字索引的效率和占空间都优于字符串。运单节点表waybill_track相对简单一些字段有运单id、节点类型创建、装车、发车、到站、签收、异常上报、节点描述、操作人、操作时间。这张表只做insert和select不需要update所以设计时可以更粗暴一点粗糙记录就是它的特点。分页查询是物流系统的家常便饭运单列表按条件筛选时我建了几个常用组合索引(customer_id, status, create_time)、(carrier_id, status, create_time)、(route_id, create_time)。MySQL索引最左前缀原则大家都懂但实际建索引时经常忘了考虑查询条件的组合顺序导致索引失效出现慢查询。我在压测阶段用慢查询日志帮助发现过一个问题按收货人手机号模糊查询时因为用了前缀%号索引直接失效后来改成只查询“号码前7位等于”的匹配方式性能才解决问题。2.3 什么是合理的表关系拒绝过度设计第一次做物流系统时我容易把表拆得过于碎片化。比如地址信息单独拆一张地址表再关联客户和运单看起来是很标准的关系设计但实际用起来难受无比。每次查运单都要join两次地址表写起来啰嗦不说后期数据同步和逻辑删除维护也麻烦。在这个项目里我的原则是高频率一起展示的数据直接冗余存储低频且不变的基础数据才用关联表。地址放到运单表里冗余一份线路和承运商这种可能频繁改名的数据才做关联。只要保证冗余字段不参与统计口径就不会有数据不一致的明显风险。另一个经验是逻辑删除的取舍。用户表、角色表、菜单表用逻辑删除is_deleted字段标记因为删除后还要保留历史审计线索。但运单和节点表不做逻辑删除直接物理删或只做状态流转因为业务数据价值高删除操作本身都是慎之又慎的一旦删错查无实据更麻烦。我见过很多系统不管什么表都搞逻辑删除结果每个查询都要带is_deleted0条件稍一漏写就出现脏数据这种“一视同仁”的偷懒做法并不值得提倡。3. 后端核心模块开发实现3.1 SpringBoot工程结构搭建和基础配置后端工程我用Maven管理标准的分层结构controller、service、mapper、entity、dto、vo、config、common、utils。实际项目里controller层只做参数接收和响应封装service层写业务mapper层只负责数据库交互这样职责清晰同事接手也容易看懂。包名我统一用com.lmslogistics management system不会有歧义。启动类上除了SpringBootApplication还加上了MapperScan扫描mapper接口包这样不用在每个Mapper接口上单独写Mapper注解干净利落。application.yml里的关键配置如下server: port: 8081 servlet: context-path: /api spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/lms_db?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalseallowPublicKeyRetrievaltrue username: root password: yourpassword jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: Asia/Shanghai mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.lms.entity configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl需要特别说明的是map-underscore-to-camel-case这个配置它帮我把数据库的create_time自动映射成Java实体里的createTime省掉大量ResultMap手写映射。但要注意如果字段名对不上比如数据库叫is_deleteJava叫做deleted这种缩写差异是不认识的需要单独用TableField或ResultMap处理。log-impl开启后MyBatis会在控制台打印SQL联调阶段强烈建议开着排查问题能少走一半弯路。等上线前记得关闭否则日志量太大。3.2 登录认证与权限控制的实操方案物流系统虽然没有互联网大厂那么高的安全要求但也不能裸奔。第一个版本我只做了简单的session拦截器发现有两个问题里头的用户信息是耦合在HttpSession里的联调时前端拿不到稍一改版运维要求支持多端登录又得重构。第二个版本改成了JWT方案。用户登录成功后后端签发一个token过期时间设置为12小时前端把token存到localStorage每次请求通过axios拦截器自动放到Authorization头里。后端用拦截器统一解析token把用户id塞到ThreadLocal上下文里后续的业务代码直接取不用每个方法都传参。权限模型是经典的RBAC设计用户关联角色角色关联菜单权限菜单表里有一个perms字段存的是权限标识比如lms:waybill:add、lms:waybill:update。后端拦截器只校验“是否登录”细粒度权限用自定义注解RequirePermission配合AOP实现。被这个注解标记的方法执行前会检查当前用户角色有没有对应权限没有就抛出401异常由全局异常处理器统一返回JSON。这个方案写起来不费事但效果很扎实后续新增接口时只需要加上注解权限就自动收口了。有一点容易忽略前端菜单也需要根据权限动态渲染否则无权限的菜单按钮照样展示出来用户点了才报错体验很差。我的处理是登录成功后再调用一次“获取用户信息和菜单树”接口后端根据用户角色返回他能看到的菜单列表前端动态添加路由没有权限的路由根本不会注册。这样菜单和接口双重控制前端兜一层只是体验优化后端兜底才是安全关键。3.3 运单状态流转与节点轨迹的实现运单状态流转是物流系统的核心难点。如果是在service里到处写if (status 1) { status 2; }这种代码状态多了之后逻辑会越写越乱要改一个流程就得翻遍整个service层。我用了一个相对轻量的状态机模式定义好各状态之间的合法流转关系封装一个WaybillStatusHandler每个状态对应一个处理器类通过Spring的工厂模式按状态匹配处理“当前状态目标状态操作原因”的组合逻辑。比如“已签收”这个状态合法前置状态只能是“运输中”或者“派送中”如果一张运单还在“已调度”状态就要签收直接抛业务异常提示“运单当前状态不允许签收”。这样把校验逻辑收拢到一个地方运营人员操作时就能及时得到提示不会提交一堆脏数据到库里。状态变更的同时强制向waybill_track表插入一条节点记录记录操作人、操作时间、节点描述这既保证了追溯能力也为前端的时间轴组件和轨迹地图提供了数据源。节点轨迹是所有物流系统的标配功能。前端收到运单详情后调用后端“查询轨迹列表”接口后端按节点时间倒序返回。前端用Element Plus的时间线组件渲染加上loadmore分页运单节点多的时候体验也很顺畅。如果以后要对接地图轨迹节点表里预留了经纬度字段后续从司机App上报GPS数据即可整个扩展思路很清晰。3.4 报表统计与Excel导出的核心技术点物流运营每天都要看数据今日开单量、签收率、承运商准时率、客户发货趋势。报表模块如果每次都在接口里写一堆聚合SQL代码会很难维护。我的做法是建立独立的统计SQL文件按场景拆分。签收率统计的核心SQL大致长这样SELECT DATE_FORMAT(create_time, %Y-%m-%d) AS stat_date, COUNT(*) AS total_count, SUM(CASE WHEN status 3 THEN 1 ELSE 0 END) AS signed_count, ROUND(SUM(CASE WHEN status 3 THEN 1 ELSE 0 END) / COUNT(*) * 100, 2) AS signed_rate FROM waybill WHERE create_time #{startDate} AND create_time #{endDate} GROUP BY DATE_FORMAT(create_time, %Y-%m-%d) ORDER BY stat_date DESC这种分组统计SQL在MySQL 8.0里的执行效率可以接受但如果数据量非常大建议再加一层汇总表通过定时任务每小时统计后写入汇总表界面查询只读汇总表。这是一类“空间换时间”的架构思想日常管理系统足够用。Excel导出用了EasyExcel库结合自定义注解写了一个通用导出service任何实体类的字段列表一转就能导出。这里踩过一个坑EasyExcel的BigNumberExcelReader会自动把数字变成科学计数法运单号、手机号导出到Excel后显示异常。解决方式是对字符串类型数字在导出时指定ExcelProperty的converter或者干脆用字符串模板。这个问题的排查过程并不难但如果不提前注意上线后运营导出运单明细表格打开一列乱码那场面就尴尬了。4. 前端Vue实现和联调实践4.1 Vue3工程搭建与基础环境配置前端工程用Vite创建命令一行搞定npm create vitelatest frontend -- --template vue然后安装Element Plus、Vue Router、Pinia、Axios这些依赖。Vite开发服务器的端口我改成了8080并且开启了proxy代理把/api前缀的请求转发到后端的8081端口。这个代理配置是这个项目里前端联调体验的关键之一。如果不配代理开发环境请求接口时就会遇到跨域问题cookie、token传递都会出幺蛾子。有了代理前端只管请求/api/xxx剩下的交给Vite处理。// vite.config.js server: { port: 8080, proxy: { /api: { target: http://localhost:8081, changeOrigin: true } } }Element Plus的按需引入用unplugin-auto-import和unplugin-vue-components这两个插件自动导入组件和API打包体积能明显减小。可能有人嫌按需引入麻烦整个引入不就完了对大型项目来说全量引入会让首屏加载变慢物流系统页面虽然不算特别重但按需引入的配置成本就几分钟值得做。4.2 动态路由与权限菜单的实现细节前端路由我用了hash模式而不是history模式。原因很实际这套系统要部署在内网服务器可能用Nginx也可能是Tomcat直接跑甚至有的小公司就在一个静态目录里放着不需要服务器特殊配置。hash模式天然兼容这种“放到服务器路径下就能访问”的需求虽然地址栏会多个#号但内部系统用户根本不在意。如果你坚持用history模式部署时记得配置Nginx的try_files否则刷新页面就404了。动态路由的实现分三步。第一步登录成功后调用后端菜单接口拿到当前用户的菜单树包括菜单路径、组件路径、图标、是否目录等信息。第二步前端把菜单树里的前端组件路径映射成import函数用router.addRoute动态注册路由。第三步全局路由守卫里判断已登录用户是否有路由表没有就拉取并动态添加加载完成后再放行。这里最容易踩的坑是动态添加路由时机不对用户刷新页面路由表丢失addRoute后需要调用next({ ...to, replace: true })重新进入一次目标路由否则页面空白。这个问题我断断续续调了一天最后发现就是少了这个重新进入的处理。4.3 关键页面设计运单列表、地图轨迹和看板运单列表页面是整个系统使用频率最高的页面设计上要把查询条件、表格、分页、批量操作合理安排好。查询条件我用ElForm配合折叠面板默认展示常用筛选运单号、客户、承运商、状态、下单日期区间更多条件收起来。重置和后端的查询参数对象强绑定重置时整个条件对象重新初始化避免出现“重置了但查询条件没清掉”这种低级问题。表格列我用动态列方案通过一个columns数组控制哪些列显示用户可以根据偏好勾选显示列这个设置保存到localStorage下次登录自动恢复。做这个功能的原因是运营人员分工不同客服关心客户名和状态财务关心运费和结算状态同一个列表如果硬编码显示全部字段表格横向滚动条拉得又长又难用动态列虽然代码多一点但用户体验提升非常明显。地图轨迹页用了高德地图JS API根据运单节点表里的经纬度坐标连成轨迹线用Polylines画线。这里有一个关键点是经纬度并非每个节点都有需要在轨迹查询接口里把经纬度为空的数据过滤掉否则地图画线时会直接连接两个跨省的点轨迹看起来非常诡异。看板页用ECharts做图表订单趋势用折线图、承运商占比用饼图、签收率用仪表盘数据来源全部是后端聚合接口前端只管渲染。图表组件我封装成了ChartCard通用组件传入标题和option就能生成一张图表看板页代码大幅简化。4.4 前后端联调中必须掌握的工具和技巧联调阶段我的经验是Mock不是必须的而是在后端接口明确之后直接对接。前端拿到接口文档后我们先在后端把空接口跑起来返回假数据前端开发进度完全不受影响。后端写好空壳Controller返回符合前端约定的临时JSON等真实业务实现后再替换内部逻辑。这种方式比引入Mock服务更贴近实际交付因为接口字段定义在一开始就对齐了中途不会出现“前端等后端、后端等前端”的僵局。接口联调最常用的工具是Postman或Apifox。Apifox在国内使用体验更友好接口文档和Mock数据整合在一起后端写完接口顺手就能生成文档前端直接参照文档调试。调试时一定要关注网络面板的Details看请求头、响应体、耗时和状态码。跨域问题是联调最常见的问题如果发现前端请求发出了但后端没收到先看是否被拦截器拦了如果后端收到但前端报错先看响应头里的CORS配置。我的后端是专门处理了CORS的开发环境允许http://localhost:8080跨域访问生产环境由Nginx统一代理跨域策略更严格。5. 常见问题与排查技巧实录5.1 MyBatis踩坑与SQL打印实战MyBatis用得多了总会遇到一些奇奇怪怪的小问题。首先是最常见的“Invalid bound statement (not found)”异常。这个提示虽然直白但触发原因非常多mapper.xml文件的namespace和接口全限定名不一致、xml文件没编译到target目录、Mapper接口方法名和xml里id不匹配、接口方法重载导致映射歧义。排查时我第一件事就是检查target/classes下有没有对应的xml文件很多次都是maven没有把src/main/resources目录下的xml打包进去需要在pom.xml里配置resources节点把xml和properties包含进来。MyBatis打印SQL的配置我在上面已经提到了就是log-impl: StdOutImpl。联调阶段开着打印SQL分析慢查询时可以直接从控制台复制SQL到Navicat里用EXPLAIN分析执行计划。但生产环境一定要关掉否则每次请求打印几行SQL日志系统压力成倍上涨排查其他问题时还会被海量日志淹没。如果你面对的数据库是MySQL 8.0执行计划里重点关注type列是否为ALL全表扫描、key列是否用了期望的索引、rows列估算行数是否过大。有一次运单查询速度很慢EXPLAIN显示possible_keys有索引但实际用的不是最优组合索引最后调整了查询条件的顺序让组合索引真正命中耗时从2.3秒降到了60毫秒这个对比让我印象极其深刻。5.2 MySQL安装与常见连接问题的解决办法按热门的搜索词来看不少人在Windows上装MySQL会遇到各种状况。我装的是MySQL 8.0安装包直接选ZIP解压版配置好my.ini后执行mysqld --initialize-insecure然后手动注册Windows服务启动。很多新手卡在root密码上--initialize-insecure方式初始化后root默认密码为空navicat里直接填root不填密码就能连上然后ALTER USER修改密码。如果初始化时用了--initialize则密码会写在data目录里的.err日志中首次登录后必须按日志里给的临时密码进行修改。还有一个高频问题是“Public Key Retrieval is not allowed”。这是MySQL 8.0默认使用caching_sha2_password认证插件导致的连接串上加上allowPublicKeyRetrievaltrue就好我前面的yml配置里已经写了。用Docker跑MySQL的同学可能会遇到容器起不来日志显示“Failed to initialize database”或者端口占用排查思路是需要确认容器内的3306端口有没有被映射到宿主机已被占用的端口或者挂载的数据目录权限不对。Docker跑数据库一般不会有问题但要注意容器重启后数据是否持久化必须用数据卷把/var/lib/mysql挂载出来否则容器删了数据就全没了这个教训很多人都踩过。5.3 分页慢查询和统计性能的优化手段分页查询在数据量大之后会出现一个经典问题深分页慢。比如查询第100000条之后的数据MySQL需要先把前100000条都扫描出来再丢弃耗时非常明显。我在运单列表的分页查询上做过优化核心思路是使用基于游标的分页即通过上一次查询返回的最后一条数据的id或创建时间来定位下一页而不是用OFFSET。查询条件里加入“id大于上一页最大id”的限制配合主键索引查询速度基本恒定。当然这种方案会让前端的分页组件从“页码跳转”变成“加载更多”的交互方式需要对产品有个交代。统计报表的性能优化我前面也提过可以增加汇总中间表并用定时任务更新。如果不想引入定时任务还可以用MySQL的物化视图概念手动维护一张统计表在运单表插入或状态更新时同步更新统计字段。不过这个方案引入了额外的数据一致性问题如果统计口径经常变维护成本会很高。我现在的做法是运营看板上需要“实时性”的数据直接查询源表但接口做了Redis缓存缓存5分钟需要“全局趋势”的数据走汇总表每天凌晨全量重算一次。实时和准实时分开两者互不干扰。5.4 缓存设计和并发场景的踩坑总结前端登录信息缓存用localStorage后端热点数据缓存用Redis。我在项目里用Redis缓存了承运商列表、线路列表、省市区树这些低频修改的基础数据缓存键带上版本号数据变更时递增版本号前端请求时自动拉取新版本。这样做的好处是规避了缓存更新的一致性问题代码实现比“删除缓存回填”简单得多。运单状态并发更新是个需要提防的场景。比如运营人员A把运单从“运输中”改为“已签收”同时运营人员B把同一张运单从“运输中”改为“异常”如果代码里是“读状态-判断-更新”两个请求可能同时读到同一状态最终谁后提交谁就赢另一个操作被无声覆盖。这种问题通常用乐观锁处理在更新语句里加上status old_status条件影响行数为0说明状态已被改变事务回滚并提醒用户刷新重试。update语句的写法大致如下UPDATE waybill SET status #{newStatus}, update_time NOW() WHERE id #{id} AND status #{oldStatus}日志里看到“更新影响行数为0”时不要以为是数据没更新要第一时间意识到是并发触发了乐观锁拦截这是系统的自我保护机制在起作用。6. 源码整理、部署上线与个人经验总结6.1 部署流程和服务器环境注意事项源码整理时要重点关注三件事数据库脚本、启动文档、环境变量。我建了一个doc目录里面放数据库初始化脚本init.sql、后端启动说明README-backend.md、前端构建说明README-frontend.md。数据库脚本采用可重复执行的幂等设计也就是DROP TABLE IF EXISTS之后再CREATE TABLE哪怕执行两遍也不会报错。启动文档要写清楚JDK版本、Maven版本、Node版本因为版本匹配问题能浪费掉新手一整个下午。部署时我推荐用Docker Compose把后端、前端、MySQL、Redis一起编排起来毕竟内网环境配置混乱是常态统一一个docker-compose.yml管理能省太多事。后端构建成jar后用Dockerfile打包镜像基础镜像用eclipse-temurin:8-jre。前端build后产出dist目录做成Nginx镜像把dist目录COPY到/usr/share/nginx/htmlnginx.conf里配置/api反向代理到后端容器。四个容器用内部网络连通对外只暴露Nginx的80端口和MySQL的3306端口。这套方案我从开发到测试再到交付只花了两天时间比裸机部署要稳定太多。6.2 我个人在实际操作中的一些心得做这个项目让我发现一个看似普通的管理系统把细节做好并不简单。数据库设计阶段花的时间越多后面写代码越顺畅状态字段用数字字典、冗余地址信息、分页查询建立组合索引这些决策都能在开发后期看到回报。很多人写代码喜欢追求酷炫的新技术但真正到了项目中“稳定、可维护”永远比“花里胡哨”更重要。SpringBootMyBatisMySQL这套组合虽然基础但恰恰是因为基础资料全、坑少、团队都能接这类项目的价值就在于它足够通用、足够扎实。如果你是拿这套系统做面试项目我强烈建议你重点把运单状态流转的设计思路讲清楚以及聊一聊你在分页慢查询、动态权限菜单等细节上是如何思考的。面试官问的问题不一定多深但如果你能主动把技术选型背后的原因、踩过的具体坑和排查过程讲出来他会觉得你是真正做过这件事的人而不是照着教程敲了一遍。最后再分享一个小技巧项目根目录一定要放一个.gitignore把target、node_modules、.idea、dist这些目录都过滤掉别把构建产物提交到仓库。我当时第一次提交时没注意把node_modules提交进去了整个仓库瞬间膨胀到几百MB拉代码拉得怀疑人生。这类细节虽然不值一提但真正开发时碰上一次就知道有多痛了。