☰
基于SpringBoot+Vue的物流管理系统开发与部署要点解析
2026/10/28 18:11:53 网站建设 项目流程

这两年找我聊项目源码、准备拿它做毕业设计或者改造成公司内部管理系统的朋友越来越多。大家反复提到的一套组合,不是什么包装花哨的商城,而是“基于SpringBoot+Vue的物流管理系统”。这个选题能一直火,不是没道理:物流业务的复杂度足够覆盖从下单、调度、运输、签收到结算的完整链条,技术栈又恰好是Java服务端开发里最稳的几块拼图——SpringBoot扛后端工程、Vue扛管理端页面、MyBatis管持久层、MySQL存核心数据。

这篇内容我不打算把某个仓库里的代码逐行贴出来,而是把这套系统从需求边界、数据建模、后端编码、前端联调,一直讲到部署阶段真正容易踩的坑。过程中我会把决定系统能否跑得顺的关键取舍单独标出来,正在做这套项目的人可以直接对照自己的代码检查一遍。

1. 先从业务建模说起:物流管理系统的边界和核心角色

1.1 一套管理系统的业务边界

很多人拿到“物流管理系统”这个需求时,第一反应是去造一个查快递的页面,然后堆几个CRUD接口,这很容易把自己绕晕。实际上,真实的物流管理系统是一个面向内部运营人员的多角色协作平台,用户群体包括客服、调度员、司机、仓管、财务和管理员,跟普通用户查快递的C端产品完全是两回事。

要判断自己做的是不是一套合格的物流管理系统,先看它有没有覆盖这几个基础场景:

  • 订单从客户侧进来,客服进行审核和改价;
  • 审核通过的订单被拆成运单,调度员安排车辆和司机;
  • 司机执行运输任务,过程中有装货、到达中转站、派送等节点记录;
  • 客户签收后生成回单,财务根据应收应付做结算;
  • 后台能按网点、时间、状态看到毛收入、成本、毛利等统计。

如果你把下载到的源码打开一看,发现它只有“增删改查”五个模块,没有业务流转,那基本只能说是个脚手架,还不能叫系统。真正能跑的源码项目,围绕的一定是上面这条业务主线,而不是单纯的数据管理界面。

1.2 核心业务流:从客户下单到回单结算

我在梳理这类系统时习惯先画一条状态流转主线,不夸张地说,状态设计直接决定了一张单据能不能从开始走到结束。一个简洁可落地的订单状态流长这样:

客户下单 → 客服审核(通过/驳回) → 调度生成运单并派车 → 司机确认出发 → 到达中转网点 → 派送中 → 客户签收或拒收 → 财务对账结算

中间还要挂异常分支,比如货物破损、退回原发货点、运输超时转异常单。很多第一次做物流项目的人在表结构里只留了一个订单状态字段,后面业务一复杂就不断加状态值,最后SQL里全是状态码判断,维护成本直线上升。

我的建议是:把订单状态和运单状态分开。一笔客户订单可能因为货物量较大被拆成多张运单,也可能为了整车运输把几位客户的订单合并到同一个车次。如果不做单据拆分,后面的车辆调度、费用分摊都会变得很难写。曾经有朋友拿一套“订单即运单”的逻辑来问为什么结算对不上,实际上就是因为一笔订单中途分成了两辆车运输,收款却还按原始订单计算。

1.3 模块划分和功能优先级怎么取舍

在模块划分上,这套系统通常需要包含以下部分,但并非每个模块都要一次做完:

  • 基础数据:客户资料、网点/分公司、车辆信息、司机信息、常用线路;
  • 订单中心:订单登记、客服审核、订单作废、订单查询;
  • 调度运输:运单生成、车次计划、装车交接、在途跟踪、签收拍照上传;
  • 财务结算:应收明细、应付明细、对账单;
  • 统计分析:业务看板、毛利润统计、异常单统计;
  • 系统权限:用户、角色、菜单、登录日志。

如果只是用来学习或者做毕设,把订单中心、调度运输、基础数据、权限这四块跑通就够看了。财务和报表可以做成简化版,但不建议完全去掉,因为缺少财务回写会让“运输完成”没有一个真正的业务落点。站在演示角度,加上一个简单的应收应付页面,整套系统的完整度会立刻上一个台阶。

2. 数据层才是这套系统的地基:表设计、索引、MyBatis持久层细节

2.1 先按业务域把表拆成三个层级

我在设计数据库时习惯把表分成三层:基础资料层、业务单据层、财务流水层。这样做的好处是数据归属清楚,后续做权限隔离和统计报表时不会把所有表搅在一起。

基础资料层常见的表包括:

  • sys_user:账号、密码、所属网点、状态;
  • sys_role / sys_menu:角色菜单,管理后台做权限分配;
  • bas_customer:客户资料、联系人、电话、信用额度;
  • bas_vehicle:车牌号、车型、载重、容积;
  • bas_driver:司机姓名、手机号、驾驶证号、状态;
  • bas_route:线路名称、起点、终点、公里数、预计时长。

业务单据层需要重点设计订单、运单和运输任务:

  • ord_order(订单主表)
  • ord_order_item(订单明细表)
  • tr_waybill(运单表)
  • tr_waybill_item(运单明细表)
  • tr_transport_task(运输任务/车次表)
  • tr_transport_track(运输轨迹节点表)

财务流水层不要直接去改业务单据金额,而是单独建应付和应收明细表,例如fin_receivable_detail、fin_payable_detail。每一条财务流水都记录来源单号、金额类型、关联客户或司机,这样将来和对方对账时,直接按来源单号分组过滤就行,不会把原始业务数据弄得面目全非。

2.2 订单表和运单表的字段设计思路

订单表里最重要的字段除了订单号、客户ID、商品名、件数、重量、体积之外,还有一个容易被忽略的字段:订单来源。如果是公司内部人工录入,那可以不需要接口来源标记,但如果未来有客户小程序或者Excel导入,就必须留一个来源字段,否则数据对不上时完全无法追溯。

运单表我通常会放这些核心字段:

  • 主键ID、运单号(对用户展示用,全局唯一);
  • 来源订单ID;
  • 线路ID、起点网点、目的网点;
  • 运输类型、货物类型;
  • 预计发货时间、实际发货时间、预计到达时间、签收时间;
  • 运费金额、是否保价、保价费、代收货款;
  • 状态(待调度、待运输、运输中、到达站点、派送中、已签收、异常、已退回)。

金额字段务必全部使用DECIMAL,不要用Double或Float。2025年了,凡是涉及金额精度问题的项目,根因大多不是框架问题,而是早年图省事选了浮点类型。还有一点:不要为了展示省事把所有表的主键都设置为业务编号,比如拿运单号直接当主键。运单号是给人和API查询用的,一旦系统上线后需要调整编号规则,你会被这个设计坑得非常难受。主键用自增Long或者雪花ID,运单号单独建唯一索引即可。

在MySQL的InnoDB里,自增主键本身在插入性能和页分裂控制上都有天然优势,物流系统不是高并发订单系统,用自增Long足够。只有在未来明确要做分库分表时,才值得在一开始引入雪花ID。

2.3 MyBatis的配置与结果映射细节

数据层使用MyBatis时,我会在application配置文件里把mapUnderscoreToCamelCase打开,让数据库里的create_time能自动映射成createTime。这个设置项目初始化时必须确认好,否则后面每写一个resultMap都会心累。绝大部分持久层的隐藏问题,都是在字段映射不一致上出现的。

Mapper XML文件建议统一放在resources/mapper目录下,不要和Java接口混在一堆。每个Mapper接口对应一个XML文件,命名一一对应。MyBatis在启动时如果扫描不到XML,会直接报Invalid bound statement异常,这个错只能说明一件事——你在application里配置Mapper XML位置时漏了路径,或者把XML放到了target不会编译的地方。

在我看过的物流系统源码里,常见的MyBatis坏味道是:一张订单主表加明细表,查询时直接在Service里用for循环一条条查明细,这就是经典的N+1查询问题。例如查询10张订单,就要额外发起10次明细查询。正确的做法是在XML中一次JOIN查出所有订单及明细,再用collection标签做一对多映射;或者先查出订单列表拼接ID集合,再用where id in (...)查明细,最后在Java里手动分组。

控制台打印的SQL日志要保留一个默认级别,不要把这个配置去掉。我在写复杂报表SQL前,一定会先在数据库客户端里跑一遍看结果,再复制到XML里,避免把SQL拼接问题带到运行时排查。

2.4 动态多条件查询里的一个高危坑

物流管理系统里“运单查询”页面基本是必做的,前端传查询条件,后端用动态SQL拼条件查询。这个功能看似简单,坑点却非常多,尤其是处理“或条件”时容易出现查全表的问题。

举个例子,做运单列表的多条件过滤,条件包括:运单号、客户名称、目的网点、运输状态。很多人会这样写:

<select id="listWaybillByCondition" resultMap="WaybillResultMap"> SELECT * FROM tr_waybill <where> <if test="waybillNo != null and waybillNo != ''"> AND waybill_no = #{waybillNo} </if> <if test="customerName != null and customerName != ''"> AND customer_name = #{customerName} </if> <if test="destId != null and destId != ''"> AND dest_network_id = #{destId} </if> <if test="status != null and status != ''"> AND status = #{status} </if> </where> </select>

这段代码在MyBatis的 标签里用AND连接,单条件传入时是没问题的。问题出在后续有人想加一个“按关键字查询客户名称或运单号”,直接改成OR条件但没加括号,就会出现这种情况:

<if test="keyword != null and keyword != ''"> AND customer_name = #{customerName} OR waybill_no LIKE CONCAT('%', #{keyword}, '%') </if>

当keyword不为空、customerName为空时,SQL会变成WHERE waybill_no LIKE '%xxx%',看起来倒也没错。但如果这个OR条件与其他AND条件混在一起写,例如:

WHERE dest_network_id = 1 AND customer_name = '' OR waybill_no LIKE '%xx%'

由于SQL里AND的优先级高于OR,最终查出来的数据就不是页面想要的结果,订单明明属于其他网点也会被带出来。排查这类问题最快的办法不是瞎猜,而是把MyBatis控制台打印出的完整SQL复制到Navicat里手工执行一遍,多试几个条件组合,看SQL是不是真的符合业务意图。修复这种动态SQL时,坚持两个原则:所有能并列的条件都用AND开头;所有多条件组合的OR一定要用一对括号包起来再拼进去。

3. 后端工程里的核心编码:状态流转、并发调度与轻量权限实现

3.1 Controller只做编排,状态流转逻辑交给Service

很多源码项目最大的问题不是没有代码,而是Controller里塞了大量业务逻辑。例如做订单审核,直接在Controller中判断订单状态,再调用Mapper更新,最终整个Controller上百行。这种写法在小项目里看起来“也能跑”,一旦加一个跨表操作,比如审核通过后要去更新客户应收账款,问题就出来了。

我在这个项目里坚持把业务动作放在Service层,并且给每个业务动作一个明确的语义方法名,比如auditOrder、cancelOrder、createWaybillFromOrder、scheduleTransportTask。Controller只接收参数并调用Service,不直接操作Mapper。这样做的好处是,将来哪怕接入了事务和消息通知,改动范围都只局限在Service里。

看一下设计较好的几个状态流转方法:

public interface TransportService { /** * 作废订单,同时释放已绑定的运输资源 */ boolean cancelOrder(Long orderId, String operator, String reason); /** * 从审核通过的订单生成一张或多张运单 */ List<Long> createWaybillFromOrder(OrderDispatchRequest request); /** * 调度员给运单派车,写入运输任务并更新运单状态 */ void assignVehicle(WaybillAssignRequest request); }

实际开发中,每个状态流转方法内部都要执行一个非常相似的“三步骤”:

  • 根据ID查出当前业务单数据,并校验它当前状态是否可以执行目标动作;
  • 对数据库行记录执行更新,同时把状态从旧值改为新值;
  • 写入一条轨迹记录或操作流水。

很多人写状态流时只改了状态字段,没有写流水。结果就是用户看到了“签收成功”的提示,但翻遍系统也不知道是谁操作的、什么时候操作的。物流系统里,操作留痕不是可有可无的加分项,而是业务追溯的底表。每一笔状态更新都应该顺带记录操作人、操作时间和备注。

3.2 调度冲突和并发问题:一张运单不能被两个调度员同时派车

物流系统的调度页面天然是多人同时在用的场景。客服在审核订单,多个调度员看着同一个待派车列表,很可能两个人同时点了一张运单的“派车”,造成数据错乱。如果是用简单的先查再更新逻辑,例如Service里先查运单状态还是“待调度”,再UPDATE成“已调度”,在高并发下就会遇到典型的丢失更新问题。

处理这个问题可以在代码里用两种思路,我优先推荐“乐观锁”方案。在运输任务表tr_transport_task里增加一个version版本号字段,每次更新时把版本号作为条件:

@Update("UPDATE tr_transport_task SET vehicle_id = #{vehicleId}, " + "driver_id = #{driverId}, status = 1, version = version + 1 " + "WHERE id = #{taskId} AND version = #{version} AND status = 0") int updateTaskWithVersion(TaskAssignDTO dto);

这条UPDATE执行后,如果返回的受影响行数为0,说明任务已经被人抢先派车了,此时Service层需要抛出业务异常,提示“该运输任务已被调度,请刷新后再试”。千万不要忽略受影响行数直接往下走,否则数据就会在不知不觉中被覆盖。

还有一种思路是使用数据库悲观锁,在查询运单时加SELECT ... FOR UPDATE,把这一行数据锁住直到事务提交。在单体管理系统的场景下也能用,但要注意事务里不能做太多耗时的外部调用,否则锁会持很久,其他页面操作也会明显卡顿。对于一个给中小物流公司用的管理系统,乐观锁已经足够了,而且修改量很小。

3.3 Token鉴权的轻量实现,而不是一上来就堆Security

Spring Security本身是好东西,但如果你拿到的源码目标是“跑通并能够二次开发”,在权限这块直接引入Security会出现一个尴尬局面:配置类非常多,初学者不知道怎么配过滤链,遇到一次请求被拦截就束手无策。所以我更建议在这套管理系统里用轻量的JWT Token加Spring拦截器方案。

核心逻辑是这样的:用户登录成功后,后端生成一个包含用户ID、用户名、角色编码的Token返回给前端。前端每次请求在Header里带上Authorization: Bearer xxx。后端自定义一个AuthInterceptor拦截器,在所有非登录请求进来时校验Token,解析成功后把用户信息放入ThreadLocal,业务层随时可以获取当前操作人。

@Component public class AuthInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 放行预检请求,否则前端会出现跨域报错 if ("OPTIONS".equalsIgnoreCase(request.getMethod())) { return true; } String token = request.getHeader("Authorization"); if (!StringUtils.hasText(token)) { throw new BusinessException("未登录或登录已过期"); } // 解析Token,并把用户信息放入上下文 LoginUser user = tokenService.parseToken(token.replace("Bearer ", "")); UserContext.set(user); return true; } }

注册拦截器时,记得把登录接口和静态资源放行,否则连登录页面都把请求挡住了。如果你用Nginx部署前端、后端独立服务,那么跨域问题的根源在于后端没有返回正确的CORS响应头,不是前端代码的问题。我一般建议在后端单独写一个CorsConfig,统一通过WebMvcConfigurer配置,允许指定来源或全部来源。而前端只负责处理业务状态码,不要在前端代码里写死跨域代理。

使用自定义Token方案也有代价:Token服务端无法主动失效,用户退出登录后在过期时间内Token仍然有效。这对内部管理系统来说基本可以接受,配合前端在跳转登录页时清除本地存储即可。如果业务要求必须做到用户封禁后立刻失效,那就需要引入Redis保存Token,每次请求查一次Redis,这个问题后面演进时再改也不迟。

4. 前端工程怎么组织和配合:Vue环境、目录结构、联调细节

4.1 拿源码后先别急着升级:Vue3还是Vue2要看底子

打开一份物流管理系统源码,最容易遇到的第一道坎就是前端依赖安装失败。很多项目至今跑在Vue2加Element UI上,突然被塞进2025年的新电脑,Node版本已经高到20以上,老项目的依赖版本可能直接编译不通过。

我的建议是,如果是第一次跑这套源码,先看它用的是Vue2还是Vue3,不要顺手就把Vue版本改了。Vue2生态成熟,Element UI组件全,但官方早已停止维护;从零开始做项目应该选Vue3加Vite加Element Plus。如果你是拿现成源码做毕业设计,非要花两天时间把Vue2升到Vue3,性价比非常低。在这个阶段,核心目标应该是理解业务逻辑和代码组织,而不是升级前端工程。

如果是新建前端工程,推荐用Vite而不是老的webpack。Vite创建项目命令:

npm create vite@latest logistics-web -- --template vue cd logistics-web npm install npm run dev

安装依赖如果卡住或报网络错误,把npm镜像源切成国内镜像,命令是:

npm config set registry=https://registry.npmmirror.com

注意不要混用npm和yarn的lock文件,团队里几个人用几种包管理器会非常痛苦。确定一种包管理器,删掉其他lock文件再安装。

4.2 前端目录、路由守卫与Axios封装

一套管理系统的前端工程,目录结构建议清晰划分:

src/ ├── api/ # 接口请求定义,按模块拆分 ├── assets/ # 静态资源 ├── components/ # 公共组件 ├── router/ # 路由配置 ├── store/ # 全局状态管理 ├── utils/ # request封装、权限判断等 └── views/ # 页面 ├── login/ ├── customer/ # 客户管理页面 ├── order/ # 订单中心页面 ├── transport/ # 调度运输页面 ├── finance/ # 财务结算页面 ├── system/ # 系统权限页面 └── dashboard/ # 首页看板

接口请求统一放在utils/request.js文件里做Axios封装,不要在每个页面里直接调用axios.get。统一封装的好处是可以集中处理Token注入、状态码报错、401跳转登录等逻辑。

import axios from 'axios' import { ElMessage } from 'element-plus' import router from '@/router' const service = axios.create({ baseURL: '/api', timeout: 10000 }) 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.message || '请求失败') return Promise.reject(new Error(res.message)) } return res.data }, error => { if (error.response && error.response.status === 401) { localStorage.removeItem('token') router.push('/login') } ElMessage.error(error.response?.data?.message || '网络异常') return Promise.reject(error) } ) export default service

路由这块,先在router/index.js里配置登录页和主布局,再给需要鉴权的页面加meta.roles。物流系统中存在明显角色差异:客服不需要打开财务结算菜单,财务不关心司机排班。因此前端在加载完用户信息后,可以通过后端返回的菜单列表,用addRoute动态注册路由,也可以在路由守卫里限制访问。

简单的路由守卫做法:

router.beforeEach((to, from, next) => { const token = localStorage.getItem('token') if (to.path === '/login') { next() return } if (!token) { next('/login') return } if (to.meta.roles && !to.meta.roles.includes(store.userInfo.roleCode)) { next('/403') return } next() })

这样整体代码量很小,但对演示和维护来说已经够清楚。不必为了展示工程能力把一个内部系统做成微前端那种复杂结构。

4.3 物流页面中值得说的几个小功能

订单列表和运单列表这类页面,核心交互是搜索条件区加表格加分页。在Vue3里,搜索条件绑定成一个reactive对象,点击查询时把当前条件传给后端,同时页码要重置成第一页,这是刚接触前端的人极容易漏掉的一个细节。很多人在第二页点搜索,搜索接口把第一页数据返回,页面却停留在第2页,视觉上像“搜索没生效”。

物流系统如果要做地图在途轨迹,不要一开始就去接重型地图SDK。没有真实GPS数据源时,可以先在运输轨迹表里存途经点坐标,然后通过ECharts的lines效果画出轨迹图。等真的要接地图服务时,再申请对应地图平台Key,把相关脚本动态加载到页面中,并务必把Key放在环境变量里,不要提交到公开仓库。无数人把Key直接写死在代码里提交到GitHub上,导致后台被刷爆,这是真实发生过的低级事故。

如果管理后台里有物流车辆视频回放功能,会遇到m3u8格式的直播流或回放流。前端如果直接放video标签,浏览器通常无法播放HLS流,需要引入hls.js来解析:

import Hls from 'hls.js' if (Hls.isSupported()) { const hls = new Hls() hls.loadSource('https://example.com/live/xxx.m3u8') hls.attachMedia(videoElement) }

这部分不算物流系统核心,但如果项目里计划做车辆监控扩展,这是常被问起的方向。

5. 上线部署时最容易被环境和配置绊倒的地方

5.1 SpringBoot版本不是越高越好:JDK版本对不上的惨痛经历

先说一个我在实际协助别人启动项目时反复遇到的状况:本地机器装了JDK 17,项目顺手用了SpringBoot 3.4,结果依赖里某个老包在编译期直接报错,或者启动时报ClassNotFoundException。原因很简单,SpringBoot 3.x是基于Jakarta EE的,包名已经从javax改成jakarta,很多老第三方库还没跟上,强行追新版本等于把自己架在火上烤。

如果环境是JDK 8,SpringBoot最高用到2.7.x即可,2.7是SpringBoot 2.x最后的维护版本。如果本机是JDK 17,可以用SpringBoot 3.2或3.3这类相对稳定的版本。2025年了,不要看到标题写着“最新”就认为依赖也一定要全部最新,稳定可用才是管理系统源码该有的态度。

在项目里检查Maven编译配置时,要确保pom.xml里的java.version与本地JDK一致:

<properties> <java.version>1.8</java.version> </properties>

换到JDK 17环境里,这里要改成17。同时IDEA里Project Structure的SDK也要保持一致,很多人改了pom没改IDEA配置,运行还是老版本,反复报错找不到原因。

5.2 MySQL8的安装、字符集、时区与连接串

MySQL安装这事,看着简单,实际能绊倒人的地方不少。用MySQL Installer选MySQL Server安装完成后,最容易踩的坑是端口被占用和字符集不是utf8mb4。MySQL 8默认字符集其实已经是utf8mb4,但在服务器上手动初始化或者使用旧版配置文件时,可能依然使用了latin1。建完库以后执行一下:

SHOW VARIABLES LIKE 'character_set_database';

如果不是utf8mb4,建库时显式指定:

CREATE DATABASE logistics DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;

在运维容器中跑MySQL也很常见,比较隐蔽的问题是容器时区。数据库容器默认使用UTC时间,Java应用插入订单时间后,查询出来比本地时间少了8个小时,然后大家开始怀疑代码里的日期写错了。实际上只要在启动容器时加环境变量TZ=Asia/Shanghai就会立刻正常。

JDBC连接串建议写成这样:

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

allowPublicKeyRetrieval=true这个参数,对MySQL 8的caching_sha2_password认证来说很重要,省略的话部分驱动版本连接时会报Public Key Retrieval is not allowed。只要连接串里带了useSSL=false,本地开发就不会被SSL证书问题耽误。

5.3 MyBatis日志配置与SQL排查

要排查业务数据为什么不对,最关键的是能看到MyBatis实际执行的SQL语句和参数。在SpringBoot项目里,最直接的方式是打开日志级别。假如你的Mapper接口包路径是com.example.logistics.mapper,配置如下:

logging: level: com.example.logistics.mapper: debug

这样控制台会打印每个Mapper方法执行的SQL、参数和返回行数。还有一种方式是在MyBatis配置里指定StdOutImpl:

mybatis: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl

注意StdOutImpl是把日志直接打到标准输出,配置类环境下更推荐用前面的slf4j日志级别。SQL日志是定位问题的第一工具,遇到动态SQL拼出来的结果和预想不一致,先去看日志,不要直接改代码碰运气。

还有一个小细节,如果项目引入了MyBatis的分页插件PageHelper,使用时要保证PageHelper.startPage和紧挨着的那条Mapper查询是一对的,中间不要隔其他数据库操作。分页插件是基于ThreadLocal实现的,乱用或没在finally里清理,容易出现“后续某一条查询突然被分页限制”的灵异现象。

5.4 Nginx部署前端时的刷新404和接口代理问题

前后端分离项目交给服务器后,前端打包出来的dist目录不能直接双击index.html打开。正确做法是用Nginx托管,并配置接口反向代理。

打包前端:

npm run build

打包完成之后,dist目录生成index.html和一堆静态资源。把dist下的文件上传到服务器某个目录,然后配置Nginx:

server { listen 80; server_name your-domain.com; root /usr/share/nginx/html; index index.html; location / { try_files $uri $uri/ /index.html; } location /api/ { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }

try_files那一行非常重要。如果没有它,你在物流管理后台点击某个深层路由后手动刷新,Nginx会去找这个真实路径,找不到就返回404,而加上try_files后会把所有前端路由都回退到index.html,再由Vue路由解析当前路径,刷新问题就解决了。

接口代理的路径需要配合后端context-path仔细核对,避免出现多一层api的404。如果后端接口路径本身不带/api,那proxy_pass可以写成http://127.0.0.1:8080/,这样请求/api/order/list会被转发成http://127.0.0.1:8080/order/list,注意代理配置里结尾的斜杠差别很大。

后端启动用java -jar比较简单:

nohup java -jar logistics-server.jar --spring.profiles.active=prod > logistics.log 2>&1 &

建议在项目里至少区分application-dev.yml和application-prod.yml,dev环境把SQL日志打开,prod关闭并调小日志级别。要确认系统正常,按顺序验证三件事:先检查MySQL能登录且服务在3306端口监听,再访问后端的健康检查接口或登录接口确认能返回JSON,最后访问Nginx的IP或域名能打开登录页并用初始账号完成登录。做完这三步,整套系统才算真正从源码走到了可用状态。

如果你打算拿这套SpringBoot+Vue的物流管理源码作为起步项目,我的个人建议是先把订单审核、运单生成、调度派车这段主链路亲手跑通,再去看财务和报表。主链路的每一张表、每一个状态流转背后都对应一个真实物流场景,把这些字段和状态弄明白,远比盲目升级框架或者多写几个页面有用得多。即便项目最终只用来做学习演示,能讲清楚“一笔订单为什么拆分成了两张运单,又怎么派给司机完成签收”,你已经超过大多数只会说CRUD的候选者了。

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

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

立即咨询