Spring Boot进销存系统实战:从数据模型到部署落地
2026/9/14 7:52:18 网站建设 项目流程

简介:面向毕业设计与 Java 开发者,这份基于 Spring Boot 的进销存管理系统是一套可直接运行的仓库管理项目,覆盖采购、库存、销售、查询统计、资料管理和系统管理六大模块,支持管理员与雇员双角色登录,适合作为课程设计、毕业设计或 Spring Boot 入门项目的参考方案。项目采用 Spring Boot、JPA、Maven、jQuery 技术栈,Eclipse 与 IDEA 均可打开运行,数据表与业务逻辑层次清晰,便于二次开发。压缩包约 99.79MB,包含完整源码、数据库文件、文档和 PPT,源码中具体实现了商品入库、出库、移库、盘点、销售订单、采购退货等业务闭环,配套文档可辅助理解设计思路与部署配置。目前已有 66 人浏览学习,适合需要参考完整进销存方案、快速搭建类似管理系统的开发者,同时也可作为答辩汇报与课程展示的参考素材,方便在此基础上按需扩展新功能。

1. 用Spring Boot建进销存系统,先想清楚业务闭环再写代码

进销存管理系统是最容易被低估的一类Java项目。表面上它只是商品、入库单、出库单三个页面的增删改查,但真正跑起来之后,库存对不上、单据回滚不干净、并发超卖这类问题全冒出来了。库存不是一个可以随手改的字段,它必须由单据和流水推导出来:先有入库单,才能增加库存;先有出库单,才能扣减库存;每一步变动都要留下流水记录。想明白这个闭环,再动手写Spring Boot工程,项目的质量会完全不一样。

你手上如果是这样一个标题:Spring Boot进销存管理系统加文档进销存仓库管理,Java项目且Eclipse和Idea都能打开,那大概率是一个标准的Maven多模块或单模块Web工程。技术栈通常是Spring Boot 2.x加MyBatis-Plus加MySQL,前端是Vue或Thymeleaf。这套组合也确实是目前中小公司内部管理系统、毕业设计和仓库管理系统最常用的路线。工程本身只要是标准Maven结构,Eclipse和IntelliJ Idea导入方式不同,但代码完全一致,不存在只能在一个IDE里跑的问题。

这篇内容围绕这个标题展开:先说进销存的数据模型怎么设计,再讲怎么在两个IDE里把工程跑起来,然后落地入库、出库、库存扣减的核心代码,最后补充盘点、报表和部署阶段的细节。新手可以照着步骤做完,有经验的人也能在并发处理和事务边界上找到可用的写法。

2. 进销存数据底座:库存模型、单据流与流水表设计

2.1 核心表拆分:主数据、单据、明细与流水四层

进销存系统第一步不是写代码,而是把表拆对。很多初学者把所有字段塞进一张商品表,包括库存数量、累计入库、累计出库,最后报表根本没法写。常见的做法是把数据按职责拆成四层:主数据、单据头、单据明细、库存流水。主数据包括商品和仓库;单据头记录一张入库单或出库单的整体信息;单据明细记录这张单子里的每一行商品、数量、单价;库存流水记录每一次库存变动,是后面所有报表和对账的依据。

表名分层核心字段说明
goods主数据goods_code, goods_name, spec, unit, price商品唯一编码,规格单位
warehouse主数据warehouse_code, warehouse_name仓库主数据
stock库存台账warehouse_id, goods_id, stock, version当前实时库存,结果表
inbound_order单据头order_no, warehouse_id, supplier, operate_time入库单整体信息
inbound_item单据明细order_id, goods_id, qty, price一个入库单对应多行商品
outbound_order单据头order_no, warehouse_id, customer, operate_time出库单整体信息
outbound_item单据明细order_id, goods_id, qty出库明细
stock_log流水表order_no, goods_id, warehouse_id, operate_type, qty, operate_time每次库存变动的原始凭证

库存表是整个系统最有争议的一张表。严格讲,库存可以从流水实时汇总出来,但业务系统通常需要快速的库存查询,所以专门放一张stock表做冗余。冗余的前提是:所有改动必须发生在事务里,并且流水表同步写入。库存表是结果,流水表是事实,这个优先级顺序必须清楚。

2.2 建表SQL:MySQL 8的InnoDB落地脚本

下面这份建表SQL是进销存项目最基础的核心表,去掉了审计字段和扩展字段,保留直接能用的部分。数据库选MySQL 8,引擎统一InnoDB,字符集utf8mb4,金额全部用DECIMAL,数量用INT或者DECIMAL取决于业务是否需要小数单位。涉及金额的字段禁止使用DOUBLE或FLOAT,否则累计对账会出现精度问题。

CREATE TABLE goods ( id BIGINT PRIMARY KEY AUTO_INCREMENT, goods_code VARCHAR(32) NOT NULL COMMENT '商品编码', goods_name VARCHAR(128) NOT NULL COMMENT '商品名称', spec VARCHAR(64) DEFAULT '' COMMENT '规格', unit VARCHAR(16) NOT NULL COMMENT '单位', price DECIMAL(12,2) NOT NULL DEFAULT 0 COMMENT '参考单价', update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, UNIQUE KEY uk_goods_code (goods_code) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='商品表'; CREATE TABLE stock ( id BIGINT PRIMARY KEY AUTO_INCREMENT, warehouse_id BIGINT NOT NULL, goods_id BIGINT NOT NULL, stock INT NOT NULL DEFAULT 0 COMMENT '当前库存', version INT NOT NULL DEFAULT 0 COMMENT '乐观锁版本号', UNIQUE KEY uk_warehouse_goods (warehouse_id, goods_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='库存台账'; CREATE TABLE inbound_order ( id BIGINT PRIMARY KEY AUTO_INCREMENT, order_no VARCHAR(64) NOT NULL COMMENT '入库单号', warehouse_id BIGINT NOT NULL, supplier VARCHAR(128) DEFAULT '' COMMENT '供应商', operate_time DATETIME NOT NULL, UNIQUE KEY uk_order_no (order_no) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='入库单头'; CREATE TABLE inbound_item ( id BIGINT PRIMARY KEY AUTO_INCREMENT, order_id BIGINT NOT NULL, goods_id BIGINT NOT NULL, qty INT NOT NULL, price DECIMAL(12,2) NOT NULL DEFAULT 0, KEY idx_order_id (order_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='入库单明细'; CREATE TABLE stock_log ( id BIGINT PRIMARY KEY AUTO_INCREMENT, order_no VARCHAR(64) NOT NULL COMMENT '关联业务单号', goods_id BIGINT NOT NULL, warehouse_id BIGINT NOT NULL, operate_type VARCHAR(16) NOT NULL COMMENT 'INBOUND/OUTBOUND/CHECK', qty INT NOT NULL COMMENT '正数入库,负数出库', operate_time DATETIME NOT NULL, KEY idx_goods_time (goods_id, operate_time) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='库存流水';

这里有几个字段值得单独说明。inbound_order的order_no必须唯一,这个单号由程序生成,不能用数据库自增主键直接当业务单号,因为外部系统或甲方会拿着单号来查单,自增ID在报表里难以阅读。stock表加了唯一约束uk_warehouse_goods,同一商品在同一仓库只允许一行库存记录,这是防止并发插入库存行的重要保障。stock_log里operate_type只存业务类型,qty统一用正数入库、负数出库,这样做统计时可以直接SUM,不需要关心方向判断。

2.3 为什么库存表要单独建并且带version字段

初学者最容易犯的错误是直接把库存数量放在goods表里,每次入库就UPDATE goods SET stock = stock + 1。这个做法在单机低并发时能跑,但一旦出现两张单子同时入库,就会出现丢失更新。库存表和商品表分离之后,还要在库存表里增加version字段,配合条件更新WHERE version = ?,在更新时校验版本号,防止两个事务互相覆盖。MyBatis-Plus的乐观锁插件会自动处理这个字段,但如果你手动写UpdateWrapper,也可以自己控制。

注意:version只在更新时作为条件出现,更新成功后要同时version = version + 1。如果更新影响行数为0,说明这一行已经被别人改过,需要重试或直接报错,不能静默忽略。

单据头和单据明细为什么要拆两张表?因为一张入库单会有多行商品,如果只存一张表,每一行都要重复存单据号、供应商、操作时间,数据冗余且不利于回滚。拆成一对多之后,修改明细时只需要按order_id删除后重建,订单头信息不用动。流水表的存在则是为了回答一个问题:这个月的入库总量是多少?某个商品在某个时间段的期初库存和期末库存分别是多少?这些问题在库存表里找不到答案,因为库存表只保留了当前值。

3. 在Eclipse和Idea里把Spring Boot工程跑通:版本选型与最小工程

3.1 版本匹配关系:JDK、Spring Boot和MySQL怎么选

标题里提到“Eclipse和idea都”,这一步的关键不是配置IDE,而是选对Spring Boot版本。现在很多讲解进销存项目的教程和视频录制于三四年之前,用的是Spring Boot 2.3或2.7,对应JDK 8。如果你直接创建Spring Boot 3.x项目,编译会报错,因为原来的javax.servlet包在3.x里全部换成了jakarta.servlet,老代码直接复制过来根本过不了编译。这个就是很多人遇到的“springboot版本太高”导致的连锁问题。

Spring Boot版本最低JDK包名适用场景
2.7.xJDK 8javax.*成熟稳定,教程最多,适合进销存这类业务系统
3.2.xJDK 17jakarta.*新项目可考虑,但旧代码迁移成本高
3.3及以上JDK 17jakarta.*追求新特性时使用,插件兼容性需要验证

进销存管理系统的技术挑战不在框架新特性上,而在事务、并发和数据一致性上,因此我一般会选Spring Boot 2.7.18配合JDK 8,这是整个2.x系列最后一个维护版本,稳定且生态兼容性最好。MySQL选5.7或8.0都可以,但建表SQL里的窗口函数和CHECK约束需要8.0才支持,推荐直接用MySQL 8。Maven版本3.6以上即可,IDEA自带Maven,Eclipse则用外部Maven,需要在Eclipse的Preferences里指定Maven安装目录和settings.xml位置。

3.2 pom.xml关键依赖与application.yml最小配置

创建一个Spring Boot工程,pom.xml里最核心的依赖是四个:Web、MyBatis-Plus、MySQL驱动和Lombok。MyBatis-Plus的版本要和Spring Boot版本匹配,Spring Boot 2.7对应的是mybatis-plus-boot-starter 3.5.x,不要用太老的3.4版本,否则分页插件和一些API不兼容。

<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.7.18</version> <relativePath/> </parent> <dependencies> <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.5</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-validation</artifactId> </dependency> </dependencies>

依赖版本锁定在Spring Boot父工程里,mysql-connector-java由父工程统一管理版本,2.7.x对应的驱动会自动适配MySQL 8。Lombok标记为optional后不会被打进war包,减少编译期注解处理带来的运行时报错。MyBatis-Plus单独指定版本,因为它不归Spring Boot父工程管理。

spring: datasource: url: jdbc:mysql://localhost:3306/inventory_db?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT+8 mybatis-plus: configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl global-config: db-config: logic-delete-value: 1 logic-not-delete-value: 0

url里必须写serverTimezone,否则数据库连接会报时区错误。useSSL=false在本地开发时可以关掉,生产环境根据数据库侧配置决定。map-underscore-to-camel-case开启后,数据库字段goods_name能自动映射到实体属性goodsName,不用手工写resultMap,这是MyBatis-Plus最实用的配置。log-impl输出SQL日志,开发阶段必须开,排错依赖它。

3.3 启动类与@MapperScan的写法

启动类是Spring Boot工程唯一的入口,进销存工程里它放在顶层包下,比如com.example.inventory。下面这个启动类除了标准的@SpringBootApplication,还加了@MapperScan,指定MyBatis的Mapper接口在哪个包下,这样就不用每个Mapper接口都写@Mapper注解。

package com.example.inventory; import org.mybatis.spring.annotation.MapperScan; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication @MapperScan("com.example.inventory.mapper") public class InventoryApplication { public static void main(String[] args) { SpringApplication.run(InventoryApplication.class, args); } }

@SpringBootApplication组合了@ComponentScan@EnableAutoConfiguration@Configuration三个注解的作用,Spring会扫描启动类所在包及其子包下的所有组件。如果没有@MapperScan,Mapper接口不会被Spring识别成Bean,启动时不会报错,但运行时注入Mapper一定会抛NoSuchBeanDefinitionException。这个错误在Idea里常见,在Eclipse里更常见,因为两个IDE对注解处理的检查方式不同。Eclipse用户还需要确认项目开启Lombok的Annotation Processing,否则实体的@Data注解不会生成getter和setter,编译直接失败。

3.4 Idea和Eclipse导入Maven工程的差异

同一个Spring Boot进销存工程,两种IDE的导入方式不同。IntelliJ Idea里选择File > Open,直接选中项目根目录或pom.xml文件,Idea会识别为Maven工程并自动下载依赖。如果依赖下载不完整,右侧Maven面板点刷新按钮强制重新导入。Eclipse则用File > Import > Maven > Existing Maven Projects,选择根目录后Eclipse会生成对应的.classpath文件。两者导入后都需要确认Project SDK和Java编译器级别一致,否则代码能打开但运行时报UnsupportedClassVersionError

提示:Eclipse内置的Maven版本通常偏旧,如果下载依赖时卡住或报错,建议换成外部Maven 3.8以上版本,并在maven的conf/settings.xml中配置本地仓库路径。Idea用户同样可以指定自己的Maven路径,不一定要用内置的Bundled Maven。

工程跑起来之后,访问http://localhost:8080能看到页面,控制台有Spring Boot启动日志,说明基础环境就绪。接下来要做的不是先写页面,而是把进销存的入库出库逻辑在后端落稳,这才是这个系统的核心。

4. 进销存核心业务落地:入库、出库与库存扣减的Spring事务实现

4.1 业务规则要写在代码前面

写进销存功能之前,先把业务规则定死。入库单提交时校验商品是否存在、数量是否大于0;出库单提交时校验库存是否充足,不足则整单失败回滚;单据一旦审核通过,关联的库存和流水必须同时变化,不允许出现单子保存成功但库存没变的中间状态。每一条规则对应一个明确的代码实现方式,而不是靠前端按钮去限制用户行为。

进销存里最核心的规则是:同一事务内完成单据写入、库存更新、流水记录三步。Spring的@Transactional注解解决事务问题,但它默认只对RuntimeException回滚,遇到检查异常如IOException不会回滚,所以注解上必须写rollbackFor = Exception.class。这个细节在面试里常被问到,实操里更是坑,很多进销存项目就是栽在事务没有覆盖所有异常类型上。

4.2 入库接口完整实现:单据、库存与流水一次写成功

入库的业务代码分成三层,Controller接收参数,Service处理业务逻辑,Mapper操作数据库。下面是一个简化但完整的入库实现,去掉了一些查询逻辑,保留了核心内容。注意Service里先查库存,不存在则创建库存行,存在则用条件更新增加数量。

@Override @Transactional(rollbackFor = Exception.class) public void createInbound(InboundCreateForm form) { String orderNo = generateOrderNo("IN"); InboundOrder order = new InboundOrder(); order.setOrderNo(orderNo); order.setWarehouseId(form.getWarehouseId()); order.setSupplier(form.getSupplier()); order.setOperateTime(LocalDateTime.now()); inboundOrderMapper.insert(order); for (InboundItemForm item : form.getItems()) { Stock stock = stockMapper.selectOne(new LambdaQueryWrapper<Stock>() .eq(Stock::getGoodsId, item.getGoodsId()) .eq(Stock::getWarehouseId, form.getWarehouseId())); if (stock == null) { stock = new Stock(); stock.setGoodsId(item.getGoodsId()); stock.setWarehouseId(form.getWarehouseId()); stock.setStock(item.getQty()); try { stockMapper.insert(stock); } catch (DuplicateKeyException e) { // 并发时两个请求同时发现库存不存在,一个插入成功,另一个走到更新 stockMapper.increaseStock(form.getWarehouseId(), item.getGoodsId(), item.getQty()); } } else { stockMapper.increaseStock(form.getWarehouseId(), item.getGoodsId(), item.getQty()); } StockLog log = new StockLog(); log.setOrderNo(orderNo); log.setGoodsId(item.getGoodsId()); log.setWarehouseId(form.getWarehouseId()); log.setOperateType("INBOUND"); log.setQty(item.getQty()); log.setOperateTime(LocalDateTime.now()); stockLogMapper.insert(log); } }

这段代码有两个关键点。第一个是并发场景的处理:当两个请求同时创建同一个商品的第一笔库存时,可能都查询到stock为null,然后都执行insert,这时唯一约束uk_warehouse_goods只能让其中一个成功,另一个会抛DuplicateKeyException,捕获后改成更新库存,既不会丢数据,也不会重复建行。第二个是流水与库存必须放在同一个事务中,任何一个异常都会让整个单子回滚,不会出现单据存在但库存没变的脏数据。

4.3 出库扣减用条件更新代替读改写

出库的难点不是扣减本身,而是防止超卖。最危险的做法是先查询库存,判断是否充足,再执行update,这个“读改写”模式在高并发下会出问题:两个请求同时读到库存为10,都判断可以出库,都执行更新,最终结果变成负库存或丢失一次扣减。正确做法是把检查和扣减合成一条原子SQL。

int rows = stockMapper.update(null, new UpdateWrapper<Stock>() .eq("warehouse_id", form.getWarehouseId()) .eq("goods_id", item.getGoodsId()) .ge("stock", item.getQty()) .setSql("stock = stock - {0}", item.getQty())); if (rows == 0) { throw new BizException("库存不足或商品未建档"); }

ge("stock", item.getQty())在SQL层面转化成stock >= ?,这个条件让扣减操作只有在库存足够时才执行。数据库会对命中的行加锁,第二个事务必须等第一个事务提交后才能执行update,因此不会出现两个事务同时扣减同一份库存的情况。rows == 0表示库存不足或者商品在该仓库没有库存行,此时直接抛出业务异常,整个出库单回滚。setSql里的{0}占位符会交给MyBatis参数绑定,避免字符串拼接SQL注入的问题。这个模式是进销存出库扣减的标准写法,既不用悲观锁SELECT FOR UPDATE,也不用在Java代码里加synchronized。

4.4 并发场景下的锁选择

进销存系统的并发通常集中在少数热门商品上,这时锁的粒度决定了系统的吞吐量。条件更新利用的是数据库行锁,锁范围只覆盖一个商品在一个仓库的一行库存,粒度最小,性能最好。MyBatis-Plus自带的乐观锁插件适合“更新不频繁、失败重试成本低”的场景,比如商品资料的修改。而SELECT FOR UPDATE悲观锁适合在更新库存前还需要读取其他表数据做判断的复杂场景,但锁持有时间更长,容易引发死锁。

注意:事务里不要执行远程调用、文件上传等耗时操作,这些会让数据库连接和行锁长时间不释放,并发一旦上来会拖垮整个应用。进销存系统的经验是事务方法只做数据库操作,其他事情放到事务外面处理。

5. 进销存账实一致:盘点差异、批次过期与报表查询

5.1 盘点流程与库存调整

账面库存和实际库存对不上是仓库管理的常态,原因可能是入库漏记、出库错发、商品损坏或者盘点计数错误。盘点的目标不是直接改库存数字,而是通过一张盘点单记录差异,再走调整流程。盘点单记录每个商品的账存数量、实盘数量、差异数量和调整原因,审核通过后才更新库存并写入盘点流水。

int diff = 实际盘点数量 - 账面库存数量; if (diff > 0) { stockMapper.increaseStock(warehouseId, goodsId, diff); operateType = "CHECK_IN"; } else if (diff < 0) { stockMapper.decreaseStock(warehouseId, goodsId, -diff); operateType = "CHECK_OUT"; } stockLog.setOperateType(operateType); stockLog.setQty(Math.abs(diff)); stockLogMapper.insert(stockLog);

盘盈和盘亏在流水表里用CHECK_IN和CHECK_OUT区分,本质和出入库一样是库存增减。盘点调整必须和库存更新在同一个事务里,防止出现盘点单生成了但库存没有调整的情况。实务中盘点一般会锁仓,也就是盘点期间停止该仓库的日常出入库,否则边盘点边出入库会让差异永远对不上。系统实现的常见做法是在盘点单上增加状态字段,审核后自动锁仓,盘点完成再解锁。

5.2 批次与有效期预警

涉及食品、药品或者元器件这类有保质期管理的仓库,库存模型里要加商品批次表,记录batch_code、production_date、expire_date、quantity和所在仓库。批次表让库存从单维度升级为多维度:同一个商品同一仓库下,不同批次的库存分开管理,出库时优先选择过期日期最近的批次,也就是先进先出策略。如果不做批次管理,有效期预警就无从谈起。

SELECT goods_name, batch_code, quantity, expire_date, DATEDIFF(expire_date, CURDATE()) AS remain_days FROM goods_batch WHERE expire_date BETWEEN CURDATE() AND DATE_ADD(CURDATE(), INTERVAL 30 DAY) ORDER BY expire_date ASC;

这段SQL查出未来30天内过期的所有批次,按剩余天数升序排列,业务侧拿到结果后可以发预警通知。INTERVAL 30 DAY是可配置参数,不同商品可以设置不同预警周期。批量查询时注意给expire_date字段建索引,否则数据量上来后这个查询会全表扫描。批次表的引入会让出库逻辑多一层:扣减批次库存的同时也要扣减该商品的总库存,两个操作必须放在同一事务中。

5.3 报表查询用流水表,不要直接查库存表

进销存报表最常见的三种是商品进销存汇总表、库存流水明细表、库存台账表。库存台账直接查stock表就可以,但进销存汇总表必须从stock_log流水表出发。原因是库存表只有当前一个时间点的值,回答不了“本月入库多少、出库多少”这类问题。流水表记录了每一次变动的类型、数量和时间,这些字段天然就是报表的数据来源。

SELECT DATE_FORMAT(operate_time, '%Y-%m-%d') AS biz_date, SUM(IF(operate_type IN ('INBOUND', 'CHECK_IN'), qty, 0)) AS in_qty, SUM(IF(operate_type IN ('OUTBOUND', 'CHECK_OUT'), qty, 0)) AS out_qty FROM stock_log WHERE goods_id = #{goodsId} AND operate_time >= #{startDate} AND operate_time < #{endDate} GROUP BY biz_date ORDER BY biz_date;

报表查询的条件一律用>=<,不要用BETWEEN,因为BETWEEN可能包含第二天的零点数据。这段SQL返回的是每天入库和出库总量,Java侧从上一个时点的期末库存开始累加,逐行计算每天的期末库存,公式是期末库存 = 期初库存 + 当日入库 - 当日出库。期初库存怎么取?可以从stock_log里查询开始日期之前最后一笔流水发生后的结存量,也可以直接查询stock表当前值再倒推,具体方式取决于系统是否保留了历史快照。这个计算逻辑建议在Service层做,不要把期初库存硬编码进SQL。

6. 把Spring Boot进销存项目交接出去的3个细节:多环境配置、索引优化与打包部署

6.1 多环境配置与启动参数

开发、测试、生产三个环境的数据源地址和密码都不一样,不要每次部署都改application.yml。Spring Boot支持spring.profiles.active参数动态切换环境,工程里按应用名加环境名拆分配置文件。application-dev.yml放本地开发数据库,application-prod.yml放生产库,公共配置留在application.yml里。

spring: profiles: active: dev

启动时通过命令行参数覆盖:java -jar inventory.jar --spring.profiles.active=prod。生产环境的数据库密码不要明文写在yml文件里,改成环境变量引用,比如password: ${DB_PASSWORD},这样即使代码仓库泄露也不会把生产密码带出去。

6.2 索引优化与数据库版本管理

进销存系统数据量上去之后,慢查询基本都集中在三张表:stock_log按时间范围查、inbound_order和outbound_order按单号查、stock按仓库和商品查。补上下面三个索引,大部分报表和单据查询都能走索引。stock的唯一索引在建表时已经有了,这组索引可以再验证一次是否生效。

ALTER TABLE stock_log ADD INDEX idx_log_time (operate_time); ALTER TABLE stock_log ADD INDEX idx_log_order_no (order_no); ALTER TABLE inbound_order ADD INDEX idx_order_warehouse (warehouse_id, operate_time);

数据库初始化不要靠手工执行SQL脚本。本地开发可以用spring.sql.init配合schema.sql和data.sql自动建表初始化,但生产环境推荐用Flyway,把建表SQL纳入版本管理,升级时自动执行增量脚本。进销存项目如果上了生产再改表结构,没有Flyway会很痛苦,少一张列多一个字段全靠人工操作,迟早出问题。

6.3 三种打包部署方式与安全提示

打包统一用Maven命令,Idea右侧Maven面板或者命令行都行。打包前先跑一遍测试,没有测试的也要-DskipTests跳过,避免测试类影响打包结果。

mvn clean package -DskipTests java -jar target/inventory-0.0.1-SNAPSHOT.jar --spring.profiles.active=prod

默认Spring Boot打成可执行jar包,直接java -jar运行。如果甲方服务器要求部署到Tomcat的webapps目录,需要改成war包:pom.xml里把spring-boot-starter-tomcat设为provided,启动类继承SpringBootServletInitializer并重写configure方法。Spring Boot 2.7对应Tomcat 9,Spring Boot 3.x对应Tomcat 10,部署时注意Tomcat版本兼容。容器化部署则用Dockerfile,基础镜像选openjdk:8-jre-alpine,把jar复制进镜像后指定启动命令。

FROM openjdk:8-jre-alpine COPY target/inventory.jar /app/inventory.jar ENTRYPOINT ["java", "-jar", "/app/inventory.jar", "--spring.profiles.active=prod"]

最后一个容易被忽略的安全点:如果工程引入了spring-boot-starter-actuator,应用程序会暴露一堆监控端点,包括heapdump。heapdump会把JVM堆内存里所有对象导出来,进销存系统里的用户名、token、数据库连接信息都在内存中,这个端点不关会直接导致敏感信息泄露。生产环境至少加一行配置把非必要端点关闭,management.endpoints.web.exposure.include=health,info,不要图省事暴露所有端点。部署完成后访问/actuator/health确认服务状态,再跑一遍入库、出库、盘点三条核心链路,项目才算真正能交付。

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

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

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

立即咨询