基于JAVA的物流信息管理系统,是Java后端学习里非常经典的一类项目。无论是用来练手、写课程设计还是作为毕业设计的题目,它都绕不开订单、运单、库存、财务这几块核心业务。很多人一开始拿到这种题目,第一反应是“不就是个CRUD吗”,真正动手之后才发现:订单状态怎么流转才能不乱、运费按什么规则算、查询列表为什么越来越慢、权限怎么做到不同角色看到不同菜单,每一环都有坑。这篇文章我会从项目设计到核心实现再到性能优化,把整个系统的搭建过程掰开揉碎讲清楚。
如果你正准备做类似的物流管理系统,或者已经写了一半卡在某个模块上,这篇文章应该能帮你省不少力气。我会按实际开发的顺序来讲:先想清楚业务边界,再定技术选型和数据库结构,然后拆解每个核心模块的实现思路,最后聊性能优化和部署落地。整个过程不是我凭空想的,是实实在在做项目时反复调整后沉淀下来的方案,照着这个思路走,后面会顺很多。
1. 先想清楚:这个物流系统到底要解决什么问题
1.1 业务背景与核心痛点
做系统最忌讳一上来就建表写代码。我见过不少同学花两周时间把增删改查写完了,答辩的时候被老师问了一句“你的系统如何避免仓库超发”,当场愣住。原因很简单:没有先理解业务。
物流信息管理系统的业务链路其实很清晰:客户下单 -> 平台受理 -> 仓库备货 -> 车辆运输 -> 末端派送 -> 客户签收。如果中间出现破损退货,还要走异常单流程。传统的小型物流公司靠Excel表格加微信群去管理这些流程,订单信息散落在不同人的电脑里,运单状态靠电话确认,月底对账要翻几百条聊天记录。做系统要解决的,就是把这些散落的信息集中起来,让每一单都能被追踪、被统计、被复盘。
所以这个系统的核心价值不是“记录数据”,而是“让数据能支撑业务决策”。管理者需要知道今天发了几单、哪个仓库库存告急、哪个司机运输超时、本月毛利是多少。这些需求直接决定了系统的功能边界:订单、运单、仓库、车辆、财务,这五个模块是底子,缺一不可。权限系统是骨架,没有它,财务数据给司机看到就麻烦了。
1.2 功能边界:哪些必须做,哪些可以砍
很多第一次做项目的人喜欢贪大求全,恨不得把物流公司的老板考勤都做进去。我的经验是:核心闭环必须完整,边缘功能能砍就砍。
必须做的是:
- 订单管理:客户信息、货物信息、下单时间、订单状态。
- 运单管理:订单审核后生成运单,运单关联车辆和司机,状态贯穿整个运输链路。
- 仓库管理:入库、出库、库存查询、库存预警。
- 财务管理:运费计算、应收应付、收入成本统计。
- 系统管理:用户登录、角色权限、菜单管理。
可以不做的或者二期再做的:
- 车辆GPS实时定位(需要对接硬件,成本高)
- 客户App或小程序(可以作为扩展方向)
- 自动调度派单(涉及算法,需要真实业务数据支撑)
把边界划清楚,开发的时候就不会乱。你写的是“某某物流系统”,不是“物流行业全套解决方案”,能把上面五条做到位,已经是一个结构完整、亮点充足的系统了。
1.3 为什么是Java,而不是Python或PHP
标题里写的是“基于JAVA”,那我们就得说清楚为什么选Java。物流管理系统本质上是典型的企业级信息管理系统,它需要稳定的事务支持、成熟的权限方案、方便后续接第三方接口。Java在这方面的积累非常厚,Spring Boot加MyBatis这套组合,在网上能找到海量资料,遇到问题很容易搜到解决方案。
你可能会问:用Python写不是更快吗?确实,用Python写个小Demo可能两三天就出来了,但到后期做高并发优化、做复杂权限控制、做多环境部署的时候,Java生态的成熟度优势就体现出来了。像Spring Security、JWT、MyBatis Plus这种组件,都是经过大量生产环境验证的。而且从课设和求职的角度看,Java后端岗位的需求量一直很大,做这个项目还能顺手把面试常问的Spring、MySQL、Redis这些知识串起来,一举两得。
2. 技术选型与整体架构设计
2.1 后端:Spring Boot 2.7 + MyBatis Plus 3.5
我最早做这类系统的时候,用的还是SSH(Spring + Struts + Hibernate)那套老古董,配置XML能写到手抽筋。现在做新项目,我建议直接上Spring Boot,不用纠结版本——2.7.x足够稳定,等熟悉了再尝试Spring Boot 3也行。使用Spring Boot以后,内置Tomcat,无需额外部署WAR包,一个jar包就能跑起来,这对本地开发极其友好。
持久层我用的是MyBatis Plus。有人可能觉得“MyBatis挺够用了,为什么要用Plus”?原因很简单:单表CRUD不用手写SQL。MyBatis Plus内置了BaseMapper,提供selectById、insert、updateById这些现成方法,写物流订单的增删改查只需要定义实体类,几十行代码就搞定。多表关联查询再自己写XML,集中管理。
这里给一个典型的pom依赖片段,照着引入就行:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.5.3.1</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.33</version> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>0.9.1</version> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </dependency>2.2 前端:Vue 2 + Element UI,还是服务端渲染?
物流管理系统的前端有两种主流做法:一种是传统的服务端渲染,用Thymeleaf模板引擎配合Bootstrap;另一种是前后端分离,Vue加Element UI调用后端接口。我的建议是:如果你已经学过Vue,直接上前后端分离;如果没学过,时间又紧,Thymeleaf也能完成任务,但后期做数据可视化大屏的时候会比较难受。
我做这个项目时用的是Vue 2 + Element UI,原因有三点:组件丰富,表格、表单、弹窗都有现成的;和Java后端联调方便,接口返回JSON直接绑定;网上有大量后台管理系统的模板可以参考,比如vue-element-admin的布局思路,自己改造一下就能用。
这里提醒一句:别一上来就复制整套后台模板,里面的权限逻辑、路由守卫、请求封装都很复杂,看不懂反而容易改出bug。建议只用到登录页和首页布局,然后自己写菜单路由,踩一遍坑才能真正理解前后端交互。
2.3 架构分层:Controller-Service-Mapper三层
我做项目时始终遵守三层架构,简单说就是:Controller只做参数接收和结果返回,不写业务逻辑;Service负责业务处理,比如订单审核、运费计算、库存扣减;Mapper负责数据库操作,一句SQL都不许在Service里拼接。
为什么要这么较真?因为物流系统的业务逻辑是不断变化的。我今天算运费用的是“按重量”,下次客户可能要求“按公里数加重量”,如果运费计算散落在Controller里,改起来要翻遍所有接口。集中到Service层之后,只需要改一个方法。而且三层架构的代码看起来非常规整,答辩的时候老师一看结构清晰,印象分就会高不少。
前端请求的路径设计也顺便说一下,我习惯用REST风格,但不用太死板:
- POST /api/order/create 创建订单
- POST /api/order/audit 审核订单
- GET /api/order/page 分页查询订单
- PUT /api/order/status 更新订单状态
统一返回结果用Result类包装,包含code、message、data三个字段,前端判断code是否等于200,这样异常处理起来非常统一。
3. 数据库设计:物流系统的地基
3.1 核心数据表,别漏了逻辑外键与冗余字段
数据库设计的好坏,直接决定了后面写代码的流畅程度。我总结出了一套物流系统常用的核心表:用户表、客户表、订单表、运单表、仓库表、库存表、车辆表、财务流水表。
订单表和运单表是最核心的两张表,我单独拆开说。订单表咱们可以这样设计:
CREATE TABLE `tb_order` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `order_no` varchar(32) NOT NULL COMMENT '订单编号', `customer_id` bigint(20) NOT NULL COMMENT '客户ID', `sender_name` varchar(64) NOT NULL COMMENT '发货人姓名', `sender_phone` varchar(20) NOT NULL COMMENT '发货人电话', `sender_address` varchar(255) NOT NULL COMMENT '发货地址', `receiver_name` varchar(64) NOT NULL COMMENT '收货人姓名', `receiver_phone` varchar(20) NOT NULL COMMENT '收货人电话', `receiver_address` varchar(255) NOT NULL COMMENT '收货地址', `goods_name` varchar(128) NOT NULL COMMENT '货物名称', `goods_weight` decimal(10,2) NOT NULL COMMENT '货物重量(kg)', `goods_volume` decimal(10,2) DEFAULT NULL COMMENT '货物体积(m³)', `freight_amount` decimal(10,2) NOT NULL COMMENT '运费金额', `status` tinyint(4) NOT NULL DEFAULT '0' COMMENT '订单状态', `create_time` datetime NOT NULL COMMENT '创建时间', `update_time` datetime NOT NULL COMMENT '更新时间', PRIMARY KEY (`id`), UNIQUE KEY `uk_order_no` (`order_no`), KEY `idx_status` (`status`), KEY `idx_create_time` (`create_time`) ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COMMENT='订单表';这里有个坑要注意:不要把客户名称、客户电话直接存在订单表里,那样看起来省事,但客户改名或者补全信息后,所有历史订单都会同步错乱。正确的做法是用customer_id关联客户表,发货人、收货人信息单独冗余在订单表里是因为物流订单经常需要追溯历史快照,这两个字段跟客户主数据无关,是订单发生那一刻的信息,必须固化下来。
运单表则要额外包含订单ID、车辆ID、司机ID、当前节点、上一节点、下一节点等信息,因为运单是一单到底的全程跟踪凭证,每个状态变化都应该记录操作人和时间。
3.2 运单状态机:用数字还是用枚举?
物流系统最容易被问倒的一个问题就是:如果订单被拒收了,状态应该跳到哪里?如果运输中丢件了,财务怎么处理?这背后其实是一个状态机设计的问题。
我把运单状态设计成了这样一组枚举值:
| 状态值 | 状态名称 | 说明 |
|---|---|---|
| 0 | 待审核 | 客户已下单,等待平台确认 |
| 1 | 已受理 | 审核通过,等待仓库备货 |
| 2 | 待揽收 | 司机已派单,等待上门取件 |
| 3 | 运输中 | 货物在途 |
| 4 | 派送中 | 到达目的站,末端派送 |
| 5 | 已签收 | 客户签收,订单完成 |
| 6 | 异常拒收 | 客户拒收,进入异常处理 |
| 7 | 已取消 | 审核不通过或客户取消 |
在数据库里我用tinyint存数字,在代码里用枚举类统一定义,禁止在业务代码里裸写数字。为什么要定义得这么细?因为每一层业务关注的信息不同,比如仓管只看待揽收的,司机只看运输中的,财务只看已签收的。如果状态只有“进行中”和“已完成”两种,那就没法精细管理了。
状态流转一定不要写成“随便哪个状态都能跳到哪个状态”,必须做状态校验。我在Service里写了一个状态迁移校验表,每次更新状态前先比对,防止出现“已签收”的订单又变回“运输中”这种低级事故。
3.3 运费计算:起步价加续重的经典规则
运费是物流系统的财务基础,别小看了这个模块。我一直用“起步价加续重”的分段计费规则:首重1公斤内收取起步价,超过部分按续重单价计费。比如:
计费规则: - 首重1kg:10元 - 续重:每0.5kg加收3元 - 不足0.5kg按0.5kg计算 - 超出50kg部分:每0.5kg加收2元 - 最低收费:10元计算流程在Service层用一个独立方法实现,输入重量,输出金额,纯函数式的逻辑方便单元测试。注意重量一定要向上取整到0.5kg,一开始我用的是四舍五入,结果财务那边对不上账,后来改成向上取整就好了。
3.4 索引设计:为什么列表查询越来越慢
做物流系统,最容易被忽略的是索引。订单表一开始只有几万条数据的时候,怎么查都很快;等数据跑到几十万条,按时间范围查询就开始卡了。我在这个项目里遇到过查询需要5秒以上的情况,后来定位发现是SQL里用了where status = ? order by create_time desc,但只给status建了索引,排序字段没有进入索引,导致文件排序。
优化方案很直接:建立联合索引(status, create_time),这样既过滤状态又能让排序走索引。另外订单编号order_no要建唯一索引,物流公司每天要按面单号查单,这个查询频率极高。
还有一点要给新手提醒:不要在状态这类低基数列上单独建索引,效果很差。MySQL优化器看到返回行数超过全表的20%以后,基本就放弃索引走全表扫描了。正确做法是配合查询条件里的高频列建组合索引。
4. 核心功能模块的设计与实现
4.1 登录与权限控制:JWT加RBAC权限模型
物流管理系统涉及多角色操作,司机、仓管、财务、管理员各自的可见范围完全不同。我用的权限方案是JWT加RBAC(基于角色的访问控制),核心思路是:登录成功后发一个token,之后每次请求都在Header里带上token,后端解析token拿到用户ID和角色,再根据角色判断能不能访问某个接口。
密码存储一定要用BCrypt加密,千万别明文存。我见过有人图省事直接把密码存在数据库里,答辩演示的时候打开数据库表,密码全裸着,旁边老师看着都尴尬。BCrypt不需要自己去设计加盐逻辑,内置随机盐,每次加密结果都不同,安全性相对可靠。
后端权限校验我用Spring Boot拦截器实现:
public class JwtInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 放行登录接口 if (request.getRequestURI().contains("/api/auth/login")) { return true; } String token = request.getHeader("Authorization"); if (token == null || !JwtUtil.verify(token)) { response.setStatus(401); return false; } // 解析用户角色 Claims claims = JwtUtil.parse(token); Integer roleId = (Integer) claims.get("roleId"); request.setAttribute("roleId", roleId); return true; } }前端拿到401之后自动跳回登录页,这种闭环体验一定要做好。我刚开始做的时候只写了后端拦截,前端不管,结果token过期之后页面一直白屏,用户根本不知道发生了什么。
路由权限的另一种做法是在前端用路由守卫配合动态菜单实现,这个等对Vue熟悉之后再上手也不迟。核心原则是:前端控制只影响交互体验,真正的安全边界在后端,千万别把前端路由当作唯一的校验手段。
4.2 订单管理与运单流转:核心业务的完整闭环
订单管理的入口是客户下单,出口是财务对账。中间的逻辑我拆成几步:
第一步,客户创建订单,订单状态变为待审核。创建时要校验发货地址不能为空、货物重量不能为负,这些基础校验可以用后端注解实现,不给前端留出错的机会。
第二步,管理员审核订单。审核通过后,系统自动生成运单记录,同时把订单状态改成待揽收。这里有个小细节:生成运单一定要放在事务里,避免订单状态更新成功而运单创建失败,导致数据不一致。
第三步,仓管安排车辆和司机,把运单标记为待揽收或运输中。这一步涉及车辆调度,业务上还需要检查司机和车辆的可用状态。
第四步,司机更新节点。司机在手机端或者电脑端更新运单状态,比如“运输中”“派送中”“已签收”,每一步都会记录操作时间和操作人ID,形成日志。
第五步,订单签收后,生成财务流水,记录运费收入,同时更新订单状态为已签收。整个流程是一个有头有尾的闭环。
我用伪代码描述一下事务的实现:
@Transactional(rollbackFor = Exception.class) public void auditOrder(Long orderId) { Order order = orderMapper.selectById(orderId); // 状态校验 if (order.getStatus() != OrderStatus.PENDING) { throw new BusinessException("订单状态不允许审核"); } // 更新订单状态 order.setStatus(OrderStatus.APPROVED); orderMapper.updateById(order); // 生成运单 TransportOrder transportOrder = new TransportOrder(); transportOrder.setOrderId(order.getId()); transportOrder.setStatus(TransportStatus.PENDING); transportOrderMapper.insert(transportOrder); }这个事务方法里有两个要点:rollbackFor = Exception.class一定要写,否则只回滚RuntimeException,遇到受检异常就不会回滚;状态校验必须放在事务内,否则并发操作下可能出现重复审核。
4.3 仓储管理:库存扣减千万别超发
仓储模块看起来简单,就是入库出库、查询库存,但最容易出问题的是并发扣减库存。如果仓库里有10件货,两个订单同时来抢,不加控制就可能同时扣减成功,最后库存变成负数。
解决思路有两种:一种是数据库层面的乐观锁,在库存表加version字段,更新时带上版本号:
UPDATE tb_stock SET quantity = quantity - #{num}, version = version + 1 WHERE id = #{id} AND quantity >= #{num} AND version = #{version};另一种是悲观锁,使用SELECT ... FOR UPDATE锁住库存行,等事务结束再释放。小体量系统用乐观锁就够了,主要是简单。我能给大家的忠告是:做库存扣减时,一定要把“扣减并校验库存”放在同一条SQL里完成,不要在Java代码里先查库存再判断再扣减,这样遇到并发请求就会被中间时间差击穿。
库存预警我用了简单的定时任务:每天凌晨检查库存表,低于阈值的商品自动推送到管理员的待办列表里。定时任务用Spring自带的@Scheduled就够用了,不用额外引入分布式任务调度框架。
4.4 财务统计:收入成本利润要能查明细
财务模块做得好不好,直接决定这个系统能不能真正被用起来。物流公司最关心的是毛利率,每月多少收入、多少成本、每个客户贡献了多少利润。
我在财务模块做了三个层次的统计:
- 日汇总:按天统计订单数、总收入、总成本、毛利率。
- 月汇总:按月统计,支持图表展示,前端用ECharts折线图显示趋势。
- 客户维度:统计每个客户的发货量、运费总额、异常率。
成本项的来源要理清楚:车辆油费、司机工资、仓储租金、物料消耗。这些数据不是物流系统直接产生的,期初数据可以从Excel导入,这个功能需要额外设计导入模板解析,我用了EasyExcel来处理,几行代码就能把Excel文件读进来。
利润计算不要直接在SQL里做,因为计算口径经常变化。我的做法是查询出收入和成本,在Java代码里按类型分组再计算利润,口径调整的时候只改一个MapReduce逻辑。
4.5 数据可视化大屏:让数据会说话
带一个可视化大屏作为展示亮点,是提升系统完成度的捷径。大屏不是花架子,它能真正帮管理者快速掌握全局:今日订单量、运输中包裹数、各仓库库存水位、近7天收入曲线、各区域订单占比。
前端用ECharts的折线图、柱状图、饼图来渲染,后端提供三个统计接口:getDashboardSummary、getOrderTrend、getStockTopWarning。大屏的数据接口和普通列表接口要分开写,响应快、数据轻,不要让前端去内存里聚合筛选。
常见的坑是:大屏数据一旦缺失某个字段,整个页面白屏。所以接口要给默认值,后端查出null的时候要赋0,前端渲染的时候也要写兜底逻辑。
5. 性能优化与部署落地
5.1 列表查询优化:别再用select * 和时间函数
物流系统的列表页面非常多,订单列表、运单列表、流水列表,每一个都涉及分页加筛选。一旦表格的查询条件里有时间范围,就会出现性能隐患,特别常见的一个坑是:在WHERE条件里对时间字段用了函数,让索引失效。
比如我见过下面这种写法:
SELECT * FROM tb_order WHERE DATE_FORMAT(create_time, '%Y-%m-%d') >= '2024-01-01'这种写法哪怕create_time有索引也会失效,因为这个条件让数据库不得不把每个create_time都先转成字符串再比较。正确做法是把时间范围直接写成区间条件:
SELECT * FROM tb_order WHERE create_time >= '2024-01-01 00:00:00' AND create_time < '2024-01-02 00:00:00'查询字段也不要无脑select *,只查列表页需要的字段,这样可以显著减少IO与网络传输。MyBatis Plus里用LambdaQueryWrapper的.select()方法就能指定字段。我优化过一个运单列表接口,去掉select *并把辅助的count查询改为只统计id之后,响应时间从1.8秒降到300毫秒以内。
5.2 缓存:什么数据值得加Redis
如果工作量允许,给系统接入Redis是一个很加分的优化,但别把所有数据都往Redis里塞。物流系统里适合加缓存的数据有:
- 字典数据,比如订单状态枚举、运输方式,这类数据很久才变一次。
- 客户列表和车辆列表,更新频率低,又有较高的查询频度。
- 首页统计大屏数据,可以设置60秒过期,减少数据库压力。
不适合缓存的是运单详情和财务流水,这些数据要求实时准确,一旦缓存还会引发一致性问题。
我用缓存的方式很保守:先查Redis,没命中就查MySQL,再把结果写回Redis,设置过期时间。更新操作发生时,直接删除对应缓存,而不是更新缓存。删除缓存的方式简单可靠,下次查询时自动回填,这样能避免并发更新带来的脏数据。
5.3 Docker部署:本地跑通到服务器部署
改完代码之后到处找Tomcat配置的日子已经过去了,现在部署Spring Boot项目最常见的方式就是Docker。把代码打包成镜像,服务器上一句docker run就能跑起来,迁移换服务器也方便。下面是一个最简单的Dockerfile:
FROM openjdk:8-jdk-alpine COPY target/logistics-system.jar app.jar EXPOSE 8080 ENTRYPOINT ["java", "-jar", "/app.jar", "--spring.profiles.active=prod"]数据库同样用Docker起一台MySQL容器,但要注意挂载数据卷,不然容器一删,数据全部丢失。-v /mydata/mysql:/var/lib/mysql这行必须加。
多环境配置在Spring Boot里是直接支持的:application-dev.yml、application-prod.yml,通过启动参数--spring.profiles.active=dev切换。开发环境连接本地数据库,生产环境连接服务器数据库,日志级别可以不同。这个配置习惯最好从第一天写代码就养成,不要等到部署的时候再改。
6. 常见问题与排查技巧实录
6.1 状态机被“跳状态”,数据莫名错乱
我在写运单状态接口时,测试时发现一个bug:已签收的订单,竟然还能被更新成“派送中”。排查下来发现是Service代码里没有做状态前置校验,任何一个状态都能调用更新状态接口直接改。虽然前端按钮隐藏了,但接口还是能直接调通。
修复方案是在更新状态的方法里加一个合法的状态迁移Map,Map里key是当前状态,value是允许迁移到的状态集合:
private static final Map<Integer, Set<Integer>> ALLOWED_TRANSITIONS = new HashMap<>(); static { ALLOWED_TRANSITIONS.put(0, Set.of(1, 7)); ALLOWED_TRANSITIONS.put(1, Set.of(2, 7)); ALLOWED_TRANSITIONS.put(2, Set.of(3, 7)); ALLOWED_TRANSITIONS.put(3, Set.of(4, 6)); ALLOWED_TRANSITIONS.put(4, Set.of(5, 6)); ALLOWED_TRANSITIONS.put(6, Set.of(5, 7)); }这个表在启动时加载一次,每次状态变更先查一下。从那以后状态相关的bug再也没有出现。做物流系统,状态机的严谨性怎么强调都不过分。
6.2 数据库时间差8小时
这个问题非常经典:本地开发时显示的时间正常,部署到服务器上之后,所有订单时间都少了8小时。原因很简单:MySQL连接参数里没有加serverTimezone,或者服务器的系统时区不是中国标准时间。
解决方法是JDBC连接串上明确指定时区:
jdbc:mysql://localhost:3306/logistics?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai顺手在Spring Boot配置里也把Jackson的时间格式化统一一下:
spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: Asia/Shanghai这样数据库、后端、前端三个环节的时间显示才一致。
6.3 前端导出Excel乱码
物流系统里导出货单、财务报表是高频需求。我用Apache POI生成xlsx文件后,前端下载下来打开,中文全部乱码。问题出在响应头里缺少字符集声明,或者文件名编码不正确。正确的设置是:
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"); response.setCharacterEncoding("utf-8"); String fileName = URLEncoder.encode("物流运单报表.xlsx", "UTF-8").replaceAll("\\+", "%20"); response.setHeader("Content-Disposition", "attachment;filename*=utf-8''" + fileName);注意文件名是经过URL编码的,不能直接把中文拼在Header里。
6.4 MyBatis Plus插件与多租户问题
使用MyBatis Plus时,如果引入的插件顺序不对,可能导致分页查询失效或者逻辑删除不生效。我的经验是:分页插件要放在最后。多租户插件、乐观锁插件和分页插件的顺序不能乱,这个顺序写错了在日志里很难看出来,因为结果看起来只是“偶尔多返回了几行数据”。
如果暂时用不到的插件就干脆别加。比如逻辑删除插件,如果在数据库里没有对应的deleted字段,启动时可能不报错,但执行update的时候会自动附加条件,导致数据更新失败,排查时会让人非常困惑。
6.5 本地测试接口正常,部署后登录失败
有一个很隐蔽的问题:本地开发时,前端通过http://localhost:8080访问后端,登录正常;部署到服务器之后,把前端静态文件也放到Nginx里,结果登录接口一直报跨域错误。这是因为前端的访问地址变成了http://服务器IP,而后端接口地址在代码里写死了localhost。
解决办法是把后端接口地址改为相对路径,统一用/api前缀,前端通过Nginx反向代理转发:
server { listen 80; location /api/ { proxy_pass http://127.0.0.1:8080/api/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }这样一来,前端代码里不需要硬编码任何IP地址,换环境只是改Nginx配置,非常省心。运行JWT登录时同样要留意,token里不要放敏感信息,过期时间一般设2小时,为了用户体验可以设置一个刷新机制,但初版直接过期重新登录也够用。
做物流信息管理系统,我个人的体会是:业务逻辑的复杂度不在于技术多难,而在于细节多、状态多、角色多。把状态机画清楚,把数据表关系理明白,把事务边界划清楚,这个项目的骨架就立住了。剩下的事情,比如优化SQL、部署上线,都是可以慢慢来的。写代码的过程中,一定要多用日志,特别是运单状态变更和库存操作,每次都打一条带操作人和时间的日志,排查问题能少掉很多头发。希望这篇内容能帮你把系统做扎实,代码跑起来只是一个开始,真正能回答清楚“每一单现在在哪儿、这个月赚了多少、为什么慢了”这三个问题,这个系统的价值才算真正体现出来。