1. 项目概述与需求拆解
先聊点实在的。最近几年,几乎每隔一段时间就能看到有人问“Java企业产供销系统怎么做”“毕设想做个ERP方向的项目有没有思路”,这类问题在技术社区里反复出现。我本人也带过不少新人和实习生,说实话,企业生产、供应、销售一体化管理系统这个方向,确实是Java后端开发里非常典型、也很有含金量的一类业务系统。它不像那种纯CRUD的简单管理后台,而是涉及到多角色协作、复杂状态流转、库存账务一致性等真实企业场景中的核心问题。
这篇文章就围绕一个完整的Java企业产供销系统项目来展开。说一下这个项目到底在做什么:它把企业内部三条核心业务线——采购供应、生产制造、销售出库——整合到同一个平台里,配合库存管理、基础数据管理、权限控制等支撑模块,形成一条从原料进厂到成品销售的数据闭环。系统解决的核心痛点是信息孤岛,也就是采购部门不知道仓库还剩多少料、生产部门不知道订单什么时候要货、销售部门不清楚哪些产品有现货可卖。这些数据一旦打通,企业运转效率的提升是立竿见影的。
适合谁来参考?如果你是正在准备毕业设计的计算机专业学生,或者刚入行想找一个完整的业务系统练手的Java开发者,这个项目方向非常合适。它覆盖的知识面足够广,从Spring Boot框架、MyBatis持久层、MySQL数据库设计,到事务管理、权限控制、报表统计,几乎把Java后端开发的核心技能点都串起来了。而且产供销这个业务场景贴近真实企业,做完之后写在简历上,面试官问起来你也能讲出实质性的业务逻辑,而不是干巴巴地说“我做过一个管理系统”。
2. 技术选型与系统整体架构设计
2.1 为什么选择Spring Boot + MyBatis这套组合
先说结论:这个项目我推荐Spring Boot 2.x + MyBatis-Plus + MySQL + Redis + Vue/Element UI。这套技术栈是目前Java企业级开发里最主流、也最适合这类业务系统的组合。
为什么不用Spring Cloud那套微服务?原因很简单——产供销系统的业务复杂度虽然高,但它的并发量和数据规模还远没到需要微服务拆分的程度。微服务化带来的分布式事务、服务治理、链路追踪等问题,对于单体应用阶段的系统来说纯粹是增加开发负担。我见过不少毕设项目强行上微服务,结果服务拆了五六个,连最基本的下单流程都跑不通,这就本末倒置了。
单体应用+模块化设计的思路更适合这个项目。把业务按模块划分清楚,代码结构上做好分层,后续就算真的要拆微服务,按模块边界切分也是顺理成章的事。
Spring Boot选2.x版本而不是3.x,主要是因为3.x要求JDK 17+,而且很多第三方组件的兼容性还在磨合期。做项目追求的是稳定可控,没必要在版本上冒险。MyBatis-Plus则能省掉大量单表CRUD的样板代码,让开发重心放在复杂业务逻辑上。
2.2 系统功能模块划分
产供销系统从功能上可以分成六大核心模块,每个模块的职责边界要清晰:
| 模块 | 核心功能 | 关联角色 |
|---|---|---|
| 基础数据管理 | 物料档案、BOM清单、客户档案、供应商档案 | 系统管理员 |
| 采购管理 | 采购申请、采购订单、到货入库、退货处理 | 采购员、仓库管理员 |
| 生产管理 | 生产计划、生产工单、领料出库、成品入库 | 生产计划员、车间操作员 |
| 销售管理 | 销售订单、订单审核、销售出库、退货入库 | 销售员、仓库管理员 |
| 库存管理 | 实时库存查询、出入库流水、库存盘点、预警 | 仓库管理员 |
| 系统管理 | 用户管理、角色管理、菜单权限、操作日志 | 系统管理员 |
这六个模块不是孤立存在的,它们之间有明确的业务流程驱动关系。销售订单推着生产计划走,生产计划又触发采购需求,采购到货后进入库存,生产领料从库存扣减,成品完工再次入库,最后销售出库从库存扣减——整个数据流是一个完整的闭环。这也是产供销系统区别于普通管理系统的核心特征。
2.3 分层架构设计
系统采用经典的四层架构,我在代码里严格遵循这个分层规范:
Controller层:只负责参数接收、调用Service、返回结果,不做任何业务逻辑。所有接口统一返回Result对象(code + message + data),前端根据code判断请求是否成功。
Service层:业务逻辑的核心层。事务控制、业务规则校验、跨表操作都在这一层完成。我习惯在Service层接口设计上按业务语义来划分方法,而不是简单的CRUD一一对应。
Mapper层:数据访问层,基于MyBatis-Plus的BaseMapper,复杂查询用XML编写SQL。多表联查的性能优化也主要在这一层做。
domain层:实体对象、DTO(数据传输对象)、VO(视图对象)。这里要特别强调:实体对象和前端交互对象必须分开。比如物料表中有create_time字段,但前端新增物料时不需要传这个字段,如果直接用实体接收参数就容易出问题。DTO做参数接收,VO做结果返回,实体只对应数据库表结构。
实际开发中我发现很多新手会犯一个错误:写一个方法从头写到尾,Controller直接查数据库,所有逻辑都堆在一个方法里。这种代码当时写起来快,但后期维护的时候谁看谁崩溃。分层的好处是每个层次职责单一,出了问题能快速定位,而且后续做单元测试、接口文档生成都方便很多。
3. 数据库设计:产供销系统的数据基石
3.1 核心表结构设计
数据库设计是这类系统的重头戏,表结构设计得好不好,直接决定后面业务逻辑实现起来是顺畅还是别扭。我先列出核心表,再逐个说明设计思路。
物料表(material):存储所有物料信息,包括原材料、半成品、成品。核心字段有物料编码、物料名称、规格型号、计量单位、物料类型(原材料/半成品/成品)、默认仓库、状态。物料编码是唯一的业务标识,我采用“类目前缀+流水号”的规则,比如原材料用RM开头,成品用FG开头,这样一看编码就知道是什么类型的物料。
BOM表(bom):BOM(Bill of Materials)是物料清单,描述生产一个成品需要哪些原材料、各需要多少数量。BOM表的设计有两种方式:单层BOM表和多层BOM表。这个项目用单层BOM就够了,即一张表存储成品的直接原材料组成,不涉及递归展开。核心字段是父物料ID、子物料ID、用量、损耗率。
采购订单表(purchase_order)和采购订单明细表(purchase_order_item):主表存订单头信息,如采购单号、供应商ID、下单日期、预计到货日期、订单状态;明细表存具体的采购物料、数量、单价、金额。主表和明细表的设计是这类业务系统最经典的一对多关系,几乎所以涉及单据的业务都要这样设计——订单头和订单明细分开,目的是为了支持一张订单包含多种物料,同时方便对订单状态做整体控制,也能单独跟踪每种物料的到货情况。
生产工单表(production_order)和生产工单明细表(production_order_item):工单是生产模块的核心单据,主表存工单号、关联的销售订单号(如果有)、产品物料ID、计划数量、工单状态(创建/已领料/生产中/已完工/已入库)、计划开始日期、计划结束日期;明细表存领用的原材料清单,从BOM自动展开生成。
库存表(inventory):核心字段是物料ID、仓库ID、当前库存量、锁定库存量(下单后被预占的数量)、可用库存量(=当前库存 - 锁定库存)。很多新手做库存表只存一个总数,这是不够的。在企业实际操作中,经常出现在途库存、锁定库存、可用库存等不同口径,如果只用一个字段,后面做销售下单时的可用量判断就很难处理。
出入库流水表(inventory_transaction):记录每一笔库存变动的明细。核心字段有物料ID、仓库ID、变动类型(采购入库、生产领料、成品入库、销售出库、盘盈、盘亏、退货入库等)、变动数量(正负表示)、关联单据号、操作人、操作时间。库存流水表是库存模块的灵魂,任何一笔库存变动都必须留下痕迹,这是对账和审计的基础。
销售订单表(sale_order)和销售订单明细表(sale_order_item):主表存销售单号、客户ID、下单日期、交货日期、订单总金额、订单状态;明细表存销售的产品、数量、单价、折扣、金额小计。
3.2 关键设计细节与业务状态设计
数据库设计中除了表字段本身,有几个业务细节一定要处理好。
第一是金额字段精度。所有金额字段用DECIMAL(12,2),禁止用FLOAT或DOUBLE。浮点数在计算机中以二进制存储,2.1这样的数字无法精确表示,累加时会出误差。做财务相关功能时金额错了可是大事。
第二是数量字段。库存数量用DECIMAL(12,3),考虑部分物料的计量单位可能是千克,存在三位小数的情况。这里在实际业务中容易踩坑,比如不同物料的计量精度不一样,如果统一用整数就会有问题。
第三是单据编号生成。采购单号、销售单号、工单号这些单据编号要全局唯一。我采用的策略是:前缀 + 日期 + 当日流水号。比如采购单号PO20240615001,前缀PO标识单据类型,中间是日期,后面三位是当日流水号。在代码里用Redis的INCR命令生成流水号,确保同一时刻并发下单也不会重复。
第四是状态字段设计。每个单据都有一个状态字段,用字符串或整数表示,推荐用整数。状态流转要清晰,比如采购订单的状态:0-草稿、1-已审核、2-部分到货、3-全部到货、4-已关闭、5-已作废。状态字段在数据库层面要用TINYINT类型,不要用VARCHAR存中文状态值,浪费空间而且写SQL时容易出错。
第五是逻辑删除。所有表都设计deleted字段,用逻辑删除代替物理删除。业务数据被误删了还能恢复,审计时需要追溯历史记录也能找到。
第六是时间字段。每张表必须有create_time和update_time,MyBatis-Plus的@TableField(fill = FieldFill.INSERT)和@TableField(fill = FieldFill.INSERT_UPDATE)可以自动填充,不需要手动在代码里逐个set。
3.3 库存和流水表的联动设计
库存表和流水表是产供销系统里数据一致性要求最高的两张表。设计上遵循一个原则:库存表存结果,流水表存过程。
任何一笔入库或出库操作,在事务中必须同时完成两件事:
- 更新库存表的当前库存量,按物料+仓库维度做增减。
- 插入一条出入库流水记录,记录变动的明细信息。
这个设计带来的好处是:库存表可以支撑快速的实时查询(只需要查一行记录),而流水表可以支撑详细的追溯审计(查某段时间内某物料的全部变动)。如果只要流水表不要库存表,每次查库存都要SUM汇总流水,数据量大时性能跟不上;如果只要库存表不要流水表,出了账实不符的问题无从排查。
实际开发中我遇到的情况是,库存对不上账的排查过程主要就是靠流水表。比如用户反应系统库存和实物差了20件,我先按时间范围查流水,找到哪一笔操作可能有问题,再针对那笔操作的前后数据进行核对,很快就能定位问题。没有流水表的话,这种问题就只能瞎猜了。
3.4 权限控制设计
系统采用RBAC(Role-Based Access Control,基于角色的访问控制)模型,核心是五张表:用户表、角色表、菜单表、用户角色关联表、角色菜单关联表。
用户表存储登录账号、密码(BCrypt加密存储)、姓名、部门、状态。角色表存储角色名称和角色编码,比如admin、purchase、sales、warehouse、production。菜单表存储系统所有菜单和按钮权限,用父子结构组织,前端路由根据权限动态生成。
权限控制是这类企业系统不可忽视的模块。不同角色的用户登录系统,看到的菜单、能操作的按钮都不一样。比如采购员只能看到采购管理和基础数据查询的菜单,不能访问销售模块。这个需求通过拦截器判断当前用户角色拥有的权限即可实现。
4. 核心业务流程与关键代码实现
4.1 采购入库流程
采购入库是产供销系统的起点,流程是:创建采购订单 → 审核采购订单 → 供应商发货 → 仓库收货 → 质检合格 → 入库并增加库存。
核心的入库操作在Service层实现,关键点在于事务控制,入库操作必须同时更新采购订单状态、插入库存流水、更新库存表,这三步要么同时成功,要么同时失败,不能出现订单状态更新了但库存没加的情况。我在代码里用@Transactional(rollbackFor = Exception.class)来保证原子性。
@Service public class PurchaseServiceImpl implements PurchaseService { @Autowired private PurchaseOrderMapper purchaseOrderMapper; @Autowired private PurchaseOrderItemMapper purchaseOrderItemMapper; @Autowired private InventoryMapper inventoryMapper; @Autowired private InventoryTransactionMapper inventoryTransactionMapper; @Override @Transactional(rollbackFor = Exception.class) public void receiveGoods(PurchaseReceiveDTO dto) { // 1. 校验采购订单状态,只有已审核的订单才能收货 PurchaseOrder order = purchaseOrderMapper.selectById(dto.getOrderId()); if (order == null || order.getStatus() != 1) { throw new BusinessException("采购订单不存在或未审核"); } // 2. 查询订单明细,逐条处理到货商品 List<PurchaseOrderItem> items = purchaseOrderItemMapper .selectList(new LambdaQueryWrapper<PurchaseOrderItem>() .eq(PurchaseOrderItem::getOrderId, dto.getOrderId())); for (PurchaseOrderItem item : items) { // 3. 更新库存表:先查库存,存在则累加,不存在则初始化 Inventory inventory = inventoryMapper.selectOne( new LambdaQueryWrapper<Inventory>() .eq(Inventory::getMaterialId, item.getMaterialId()) .eq(Inventory::getWarehouseId, dto.getWarehouseId())); if (inventory == null) { inventory = new Inventory(); inventory.setMaterialId(item.getMaterialId()); inventory.setWarehouseId(dto.getWarehouseId()); inventory.setQuantity(item.getReceiveQuantity()); inventoryMapper.insert(inventory); } else { inventory.setQuantity(inventory.getQuantity() + item.getReceiveQuantity()); inventoryMapper.updateById(inventory); } // 4. 插入入库流水 InventoryTransaction transaction = new InventoryTransaction(); transaction.setMaterialId(item.getMaterialId()); transaction.setWarehouseId(dto.getWarehouseId()); transaction.setTransactionType("PURCHASE_IN"); transaction.setQuantity(item.getReceiveQuantity()); transaction.setRelatedOrderNo(order.getOrderNo()); transaction.setCreateBy(dto.getOperator()); inventoryTransactionMapper.insert(transaction); } // 5. 更新采购订单状态为“全部到货” order.setStatus(3); purchaseOrderMapper.updateById(order); } }代码逻辑梳理一遍:第一步校验订单状态,防止对未审核的订单做入库操作;第二步查出订单明细,逐条处理;第三步更新库存表,这里要注意不存在库存记录时需要初始化插入,不能直接做加法;第四步插入流水,记录操作的原始凭证;最后更新订单状态。整个流程在一个事务里,任何一步出错都会回滚。
实际上这里还有一个隐藏问题:如果一张采购订单分多次到货怎么办?比如訂了100件物料,第一批只到了60件。上面这段代码直接把订单状态置为3(全部到货)就是有问题的。更合理的做法是维护一个“累计到货数量”,每收到一批货就累加一次,当累加数量等于订单数量时才置为全部到货。这个逻辑就留给读者自行优化了,从0到1做个能跑的项目,和把细节做到严谨,这两者之间的距离就在这些地方体现。
4.2 生产领料与成品入库
生产模块的核心逻辑是从BOM展开到领料单,再到成品入库。生成生产工单时,系统根据成品的BOM清单自动计算需要领用的原材料种类和数量。
过程中有一个关键参数:损耗率。实际生产中,原材料不可能100%转化为成品,总会有边角料、损耗。所以在计算领料数量时要把损耗率算进去:
领料数量 = 产品计划数量 × BOM用量 × (1 + 损耗率)批量生产100件成品,某原材料单件用量是0.5kg,损耗率是2%,实际领料数量就是100 × 0.5 × 1.02 = 51kg。
领料出库后,仓库库存相应减少,同时工单状态从“已创建”变为“已领料”。等生产完成,成品入库时,库存中成品的数量增加。生产领料和成品入库这两个操作涉及多个步骤,两个操作都要做库存变化和流水记录。设计工单状态机的核心思路是:每一步操作都校验当前状态是否允许执行该操作。
在代码里,我会把状态校验的逻辑抽出来做成一个独立方法:
private void checkOrderStatus(ProductionOrder order, Integer expectedStatus, String actionName) { if (order.getStatus() != expectedStatus) { throw new BusinessException("工单当前状态不允许执行" + actionName); } }4.3 销售下单与库存扣减
销售模块的核心是下单时实时校验库存。用户在前端选择产品、输入数量,后端立即查询可用库存。如果够就下单成功并锁定库存,防止别的订单把库存抢走;如果不够就提示用户库存不足。
这里先锁定库存的设计非常关键。如果不锁定,就会出现两个用户几乎同时下单,都看到库存有100件,各自都订了80件,结果一个订单出库后发现另一个订单没货可发的尴尬情况。
锁定库存的实现方式是:下单时在库存表里扣减locked_quantity(锁定库存)字段,可用库存 = 当前库存 - 锁定库存。下单后,如果订单正常出库,再扣减当前库存、释放锁定库存;如果订单取消,直接释放锁定库存。
代码层面用UPDATE ... WHERE inventory_quantity >= needed_quantity这样的乐观锁/CAS方式来保证并发下单的安全性。用一条带条件的UPDATE语句,数据库层面保证只有一个事务能更新成功:
UPDATE inventory SET locked_quantity = locked_quantity + #{quantity} WHERE material_id = #{materialId} AND warehouse_id = #{warehouseId} AND (quantity - locked_quantity) >= #{quantity}如果这条UPDATE影响行数为0,说明库存不足,下单失败。这个方式比先SELECT再UPDATE更安全,避免了并发场景下的超卖问题。
4.4 库存预警与安全库存
库存管理模块中有一个很实用的功能:安全库存预警。每个物料可以设置一个安全库存阈值,当可用库存低于这个阈值时,系统自动标记为“库存不足”状态。
实现思路有两种。第一种是实时查询,每次查询物料列表时计算可用库存,如果低于阈值就高亮显示。这种方式简单直接,但每次查询都要做计算。第二种是异步扫描,用一个定时任务定期扫描所有物料,把库存不足的物料写入预警表,前端展示预警表的数据。这种方式适合物料数量多、需要给采购员推送待办通知的场景。
实际项目中我采用的方案是:安全阈值存在物料表里,库存预警列表页面实时计算展示,同时提供一个定时任务每天晚上扫描一次,给采购员生成一批“采购建议”记录。这样既能实时看到当前库存状态,又能给采购计划提供数据支持。
4.5 报表统计实现
产供销系统不做报表统计,业务价值就少了很大一部分。销售报表、采购报表、库存周转报表是企业管理者最常看的数据。
实现方案有两个选择:直接写SQL聚合查询,或者用ECharts等前端图表库做可视化。我采用的是两者结合:后端通过SQL按时间、按物料、按部门等维度聚合数据,返回JSON给前端;前端用ECharts渲染柱状图、折线图、饼图。
举个例子,按月统计销售额的SQL:
SELECT DATE_FORMAT(create_time, '%Y-%m') AS month, SUM(total_amount) AS sale_amount FROM sale_order WHERE status != 5 -- 排除作废订单 AND create_time BETWEEN #{startDate} AND #{endDate} GROUP BY DATE_FORMAT(create_time, '%Y-%m') ORDER BY month这类统计SQL看起来简单,但实际优化时要注意:一是大表聚合要加时间索引,否则扫全表会非常慢;二是统计口径要跟业务方对齐,比如“销售额”到底含不含税、“作废订单”算不算进去,这些细节直接影响报表的准确性。
5. 常见问题与踩坑排查实录
5.1 事务不生效的坑
场景:采购入库时,更新完库存表,插入流水时报错,结果库存增加了,流水没插入。排查后发现方法被@Transactional标注了,但事务没生效。
原因:类的内部方法调用this方法时,Spring AOP代理不生效。比如在PurchaseServiceImpl里有一个公开方法调用了同一个类的另一个带@Transactional的方法,事务就是不起作用的。
解决方案:把被调用方法拆到另一个Service类里,或者在内部方法中通过TransactionTemplate手动管理事务:
@Autowired private TransactionTemplate transactionTemplate; public void businessMethod() { transactionTemplate.execute(status -> { // 事务内的操作 return null; }); }这个问题我在实际项目里遇到过不止一次,排查的时候看日志发现根本没有开启事务,才反应过来是代理失效的问题。
5.2 并发扣减库存导致负数
场景:两个用户同时下单,都查到库存还有10件,A订了8件,B订了7件。结果A下完单后库存变2件,B也下单成功,但库存变成-5。
原因:代码是“先SELECT可用库存,判断够不够,再UPDATE”,并发时两条线程都通过了判断,然后各自执行UPDATE,库存就被扣成负数了。
解决方案:不允许出现负库存。在UPDATE语句中加上quantity >= 扣减数量的条件,同时用受影响行数判断是否更新成功。SQL写法和前面的锁定库存一样:
UPDATE inventory SET quantity = quantity - #{quantity} WHERE material_id = #{materialId} AND warehouse_id = #{warehouseId} AND quantity >= #{quantity}此外,在数据库层面设置quantity字段为DECIMAL(12,3) UNSIGNED,加上这个约束后即使代码有bug,数据库也不会允许负数入库,这是最后一道防线。
5.3 数据库ID生成策略
场景:使用MyBatis-Plus默认的ASSIGN_ID生成雪花ID,单表操作没问题。但多表操作时,比如采购订单主表和明细表需要关联,如果明细表生成ID时主表的ID还没有,就很难处理。
解决方案:采用@TableId(type = IdType.ASSIGN_ID)让MyBatis-Plus在插入前生成ID,主表插入后拿到完整的主表对象,再在明细表中引用主表ID。不需要添加额外的IdWorker操作。
5.4 前端反显时间和金额格式问题
场景:后端返回的时间是2024-06-15T10:30:00这种带T的格式,前端显示很难看。金额是1000.0,但前端希望显示1,000.00。
解决方案:时间格式统一在VO对象中用@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")注解格式化。金额格式化放在前端处理,用JavaScript的toLocaleString或format方法统一展示。
5.5 权限绕过风险
场景:用户直接输入接口路径URL,绕过了前端菜单权限,直接访问未授权的接口。
原因:前端的菜单隐藏只是”防君子不防小人“,所有接口必须在后端做权限校验。我在项目里用自定义拦截器实现:每次请求先解析用户的Token,获取用户信息和角色,再判断当前请求的API路径是否在角色允许的权限范围内。
关键技术点是Mapper查询时自动追加数据权限条件。在MyBatis-Plus中,我自定义了一个拦截器,解析SQL后自动拼接当前用户的组织、部门筛选条件,确保用户只能看到自己有权限的数据。这个功能实现在官网的插件机制里有相关介绍,用的时候注意拦截器执行顺序即可。
这里我要特别强调:权限安全是这类企业系统的生命线。做生产环境交付时,如果权限控制做得不够细,后面被客户方审计出来就是严重的交付事故。
6. 开发过程实录与持续优化方向
6.1 从零搭建项目的里程碑安排
说下我做这个项目的实际节奏,方便你规划时间。整个开发周期我控制在20天左右(每天投入4-6小时),分为四个阶段:
第一阶段(前3-5天):搭建项目骨架、配置数据库和公共模块。包括Spring Boot工程初始化、MyBatis-Plus集成、统一结果返回、全局异常处理、JWT登录认证、用户权限等基础模块。这些是系统运行的地基,建议优先搞定。
第二阶段(第6-10天):实现基础数据管理和库存管理模块。物料管理、BOM管理、客户管理、供应商管理、仓库管理、库存查询。基础数据模块相对独立,作为练手热身很好。
第三阶段(第11-15天):完成采购、生产、销售三大核心模块。这是整个系统的关键路径,需要特别注意业务流程的完整性和数据一致性。建议先做采购,再做销售,最后做生产,因为生产和销售的逻辑都依赖采购建立的单据流转模式。
第四阶段(第16-20天):联调测试、报表统计、权限完善、修复Bug、部署上线。这个阶段做的事情虽然不全是“新功能”,但在实际体验中非常重要。我自己做过很多项目,最深的体会是:一个功能“能跑”和“好用”之间的差距,往往就在这一周里体现出来。
6.2 系统性优化方向:从项目到产品的进阶路径
基础版本完成后,系统的扩展空间还很大。按投入产出比排序,以下几个方向值得考虑:
第一是引入工作流引擎。如果审批环节多、流转复杂,可以考虑接入Flowable或Activiti。这套东西的好处是审批流程可配置化,调整流程不需要改代码。但注意这会大大增加学习和调试成本,从0到1的项目阶段不建议引入。
第二是消息通知与待办提醒。采购审核通过、到货通知、库存预警提醒,这类消息如果一直要靠用户刷新页面才能看到,体验会比较差。接入WebSocket做实时推送,或者接入邮件、企业微信通知,不需要很高的技术门槛,但对使用者来说体验提升很大。
第三是数据权限与多租户支持。在业务扩展阶段,考虑支持多部门或多组织的用户,它们只能查看自己组织的数据。设计一个dept_id字段配合数据权限拦截器,就能实现这个效果。
第四是移动端适配。用H5或小程序给销售和仓库人员做一个移动端操作入口,进行下单、收货、盘点等高频操作。这部分单独拿出来是一个新的完整项目,适合作为毕设的亮点方向。
6.3 部署与最终交付
项目开发完成后,部署上线也是不可忽视的一环。我采用的方案是最简洁的:一台Linux服务器 + Docker Compose编排MySQL、Redis和Java应用三个容器。域名按需绑定,HTTPS证书用免费的Let‘s Encrypt即可。
部署的配置重点说三处。
一是MySQL的数据目录必须映射到宿主机,否则容器一删数据全没。Redis不做持久化,纯缓存场景不需要。二是JVM参数,根据服务器配置合理设置。我用的是-Xms256m -Xmx512m,足够支撑演示环境并发。三是日志要挂载到宿主机,实在找不到问题或想排查线上Bug时,日志是最重要的线索。
关于打包,用Maven的mvn clean package -DskipTests打出可执行的Jar包,然后写一个Dockerfile,基础镜像用eclipse-temurin:8-jre就够了,没必要装完整的JDK。镜像越小,部署越快,出问题的可能性也越小。
7. 项目复盘与实际操作心得
最后聊聊我做这个项目的一些真实感受和踩过的坑,这些内容不算在教科书上,但对打算做类似项目的朋友应该挺有帮助。
第一,业务理解比技术实现更重要。刚开始学Java时,我也比较容易陷入技术细节里,纠结于某个框架的API怎么用、某个注解该怎么写。但做产供销这类业务系统后,最大的体会是:真正的难点不是技术,而是能不能理解业务逻辑。采购订单有几种状态、每种状态之间怎么流转、什么情况下允许做什么操作,这些都是业务规则,想清楚之后再写代码就顺理成章了。我见过太多人代码写了一半发现业务逻辑想错了,推倒重来,原因就是动手之前没把业务吃透。
第二,数据库设计别怕返工,但返工成本是真的高。我最早设计库存表时就少设计了locked_quantity字段,做到销售下单模块时发现需要锁定库存来防止超卖,只能回头改表。中间改动的代码牵扯到采购入库、库存查询、出库等多个模块,花了大半天时间才把库存相关的逻辑全部理顺。这个教训让我在后面的项目中养成了一个习惯:动手写代码前,先把关键业务场景在主流程里完整过一遍,确认表结构能支撑所有场景再开工。
第三,事务和异常处理一开始就要规范化。我对组内新人的要求很简单:所有Service层方法,凡是有数据库写操作的,必须加@Transactional;所有业务异常,必须抛出带错误提示的自定义异常,禁止裸抛RuntimeException或返回null。这样虽然前期写代码需要多一些注意,但到了联调和后期维护阶段,优势就非常明显了——错误信息清晰,问题定位快,排查Bug不用猜来猜去。
第四,系统做完了一定要跑一遍完整的业务闭环再交付。所谓完整的业务闭环,就是从建物料档案、配置BOM、创建采购订单、收货入库、下达生产工单、领料出库、完工入库、创建销售订单、锁定库存、销售出库,直到最终库存扣减,整个流程走一遍。这个流程能走通,系统的基本功能就算验证过了。我遇到过的情况是,各个模块单独跑都没问题,一串联起来就有各种数据不一致的Bug——采购入库的流水类型和生产领料出库的流水类型逻辑重复了,导致库存流水查询时出现了类型冲突。这种集成问题只有在完整跑业务闭环时才会暴露出来,所以这一步不能省。
这个项目的后续还可以延伸的方向其实不少,比如接入工作流引擎做更灵活的审批配置,或者加一个仪表盘页面把销售趋势、库存周转、采购执行情况集中展示出来。如果你正在做类似的系统,我的建议是先圈定一个清晰的项目范围把它做扎实,比贪大求全做出一堆半成品要更有价值。毕竟对一个工程师来说,交付一个可靠运行的系统始终是最有成就感的事。