拿到这套SSM批发超市库存管理系统,我第一反应不是解压跑通,而是先琢磨它背面的业务逻辑。批发超市和普通便利店完全是两种业态:商品杂、单价低、毛利薄,供应商一波接一波送货,库存量稍微压几天,资金链就开始发紧。这种场景下的库存系统,要求其实非常朴素——录入快、账目实时、出了问题能查得清。Spring、Spring MVC、MyBatis这个三件套虽然不如Spring Boot时髦,但在这种业务里依然是性价比很高的选择。整篇文章我会从技术选型、需求建模、数据库设计、核心代码链路、上线排错和部署运维六个维度,把这套系统完整拆一遍,希望刚学完SSM想拿项目练手的同学能照着复现,也想给门店做进销存管理的朋友一个清晰的设计参考。
1. 低价批发超市场景下的技术选型:SSM为什么依然能打
1.1 低价批发超市对系统的真实诉求
先说业务。低价批发超市的典型画面是什么样的?早上七八点,供应商的货车停在门口,仓库的人拿着送货单一件一件扫码入库;上午门店开始营业,收银员一边开单一边让搬运工从货架取货;下午老板打开电脑问的第一句话就是:这个月毛利多少?哪些货压着卖不动?这些都是系统的硬需求。
低价批发超市和普通零售便利店有几个显著差异。第一,SKU数量大,饮料、调料、日用品、零食能上千种,而且同一种商品有不同规格,500ml一瓶和500ml乘以24一箱是同一个商品的两套单位。第二,出货方式既有整箱批发也有拆零零售,库存单位需要灵活处理。第三,毛利控制极其严格,可能整体毛利率只有5%到8%,库存稍微积压就把利润吃掉了。第四,供应商众多,每家供货价、结算周期都不同。
这些业务特点决定了系统不能只有增删改查,必须围绕“库存变动轨迹”来设计。选型也就不用追求新潮,稳定、可控、易维护才是第一诉求。SSM正好满足:Spring管对象和事务,Spring MVC管接口和页面流转,MyBatis管SQL,每层职责都很清晰,出了问题容易定位。
1.2 SSM和Spring Boot在类似项目里的取舍逻辑
有人会问,都2025年了,新项目为什么不直接上Spring Boot?这个问题要分场景回答。如果是从零起步、无历史包袱,Spring Boot当然更高效,内嵌容器、自动配置、起步依赖,五分钟能起一个工程。但SSM的价值在于它“拆得开”——Spring容器怎么初始化的、Spring MVC的DispatcherServlet怎么接进来的、MyBatis的SqlSessionFactory怎么和Spring整合的,全都看得见。对于需要深入学习底层原理的开发者,SSM是一份非常好的教材。
从实际维护角度看,很多中小型门店的服务器配置仍然不高,JDK版本还停留在8。SSM单体war包扔进Tomcat就能跑,不挑环境,占用的内存也比Spring Boot默认启动小不少。而且这类系统的页面数量有限,后端用JSP加Layui,一个后端开发就能全包,不用额外搭前端工程、不用配Node环境,交付成本和维护成本都低。
我用一张表格把两者放在一起看:
| 对比维度 | SSM单体架构 | Spring Boot单体架构 |
|---|---|---|
| 环境要求 | JDK 8以上,Tomcat 8.5够用 | JDK 8以上,内嵌容器,打包即可跑 |
| 开发效率 | 需要手写XML配置,前期稍慢 | 自动配置,起步快 |
| 学习价值 | 框架整合原理清晰 | 配置简化但封装深 |
| 维护难度 | 配置显式,问题可查 | 遇到自动配置坑时排查成本更高 |
| 适用场景 | 传统企业系统、教学项目、旧机器部署 | 新业务快速迭代、前后端分离 |
我的判断是:如果是给批发门店做一套以进销存为核心的项目,SSM完全够用,而且在成本敏感的场景下反而更稳妥。如果团队本身熟悉Spring Boot,那也不用强行换,业务设计才是核心。
1.3 工程结构与模块划分
这套系统的包结构建议分成这样几层:
controller:接收请求、参数校验、返回页面或JSONservice:业务逻辑,比如入库、出库、盘点、报表mapper:MyBatis接口,对应XML里的SQLentity:数据库实体对象dto:前端交互用的数据传输对象,避免直接暴露实体util:通用工具,比如订单号生成器、日期处理
配置文件集中在resources目录下,包括数据库连接、Spring和Spring MVC的XML、MyBatis配置。前端页面放webapp/WEB-INF/views,静态资源放webapp/static。整体就是经典SSM三层架构。
选型定下来之后,接下来要解决的问题是:系统到底要装哪些功能?这需要从业务出发倒推,不能拍脑袋。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 业务建模:先搞懂批发超市怎么赚差价,再想菜单怎么摆
2.1 从收货到出货的主流程设计
我习惯在写代码前先把业务流程画成一条线,每个环节有哪些单据、谁操作、影响哪些数据,都要交代清楚。
批发超市的库存主流程大概是这样的:供应商送货到总仓,仓管员在系统里做采购入库单,填供应商、商品、数量、进价,系统自动更新库存并写一条入库流水。然后门店通过销售出库单把商品卖出去,系统扣减库存并写一条出库流水。库存不够了,系统根据安全库存下限提醒补货;有商品临近保质期,系统预警让门店优先处理。月底仓库例行盘点,盘出来的差异走盘盈盘亏单修正。
这个流程里最敏感的动作是“库存扣减”。如果只更新库存表而不记录流水,后面对账就无从谈起。所以我在设计时给自己定了一条纪律:涉及库存变动的操作,必须同时写业务单据和库存流水,两张表缺一不可。
2.2 功能模块清单
基于上面的流程,功能模块可以梳理成一张清单:
| 模块 | 核心功能 | 操作人 |
|---|---|---|
| 系统登录 | 账号密码登录、角色区分 | 所有人 |
| 商品档案 | 商品信息、条码、规格、单位、价格维护 | 管理员 |
| 商品分类 | 树形分类维护 | 管理员 |
| 供应商管理 | 供应商档案、联系人、结算方式 | 管理员 |
| 采购入库 | 采购单录入、入库审核、采购明细 | 库管 |
| 销售出库 | 销售单录入、出库审核、库存扣减 | 收银/库管 |
| 库存查询 | 实时库存、批次查询、进出明细 | 所有角色 |
| 库存盘点 | 盘点单、盘盈盘亏处理 | 库管 |
| 预警中心 | 库存下限预警、临期预警 | 管理员/库管 |
| 报表中心 | 采购汇总、销售汇总、毛利分析、周转率 | 老板/管理员 |
| 系统设置 | 用户管理、密码修改、操作日志 | 管理员 |
每个模块都不复杂,但组合在一起就是一套完整的进销存闭环。我的经验是,做这类系统不要一上来设计复杂的菜单权限模型,先用“管理员、库管、收银”三种角色把界面和按钮区分开,后期需要细粒度再升级。
2.3 预警和报表为什么是老板的刚需
低价批发超市老板最在乎的是周转和毛利,因此预警和报表不能做成鸡肋功能。我见过不少库存系统把预警做成“库存数量低于0才提醒”,等触发的时候货早就断档了。正确做法是给每个商品设置一个安全库存下限,比如某饮料日均卖5箱,进货周期3天,那么安全库存可以设为15箱,低于这个值就出现在预警列表。
临期预警同样重要。食品饮料类商品保质期短,系统里的商品表要预留expire_days字段,入库时记录生产日期,系统自动计算剩余天数,低于30天就预警。这个逻辑在SQL里写比在Java里循环处理高效得多,后面会详细说。
报表方面,第一个必须做的是毛利统计。毛利等于销售额减去成本额,注意成本额不能用一个固定参考进价,而是要用该批次商品的实际入库成本。第二个是商品周转率,周转天数等于平均库存除以日均出库量,这个指标直接反映哪些货压仓了。报表用ECharts做柱状图和折线图,老板一眼能看懂。
业务流程和功能清单确定后,下一步就是数据库设计了。数据库设计是在这套系统里最值得花时间的地方,直接决定后续开发的顺畅程度。
3. 数据库设计:把每一分钱和每一瓶水都管清楚
3.1 核心表结构与关系
整套系统的核心表可以分成三组:基础资料表(商品、分类、供应商、用户)、业务单据表(采购单、销售单、盘点单)、库存账表(实时库存、库存流水)。这里我只列出最关键的几张建表语句,你能看到字段的思路:
sql复制CREATE TABLE tb_product (
id INT PRIMARY KEY AUTO_INCREMENT,
bar_code VARCHAR(32) DEFAULT NULL COMMENT '条码,允许为空,多规格时可不唯一',
product_name VARCHAR(100) NOT NULL COMMENT '商品名称',
category_id INT NOT NULL COMMENT '分类ID',
spec VARCHAR(50) DEFAULT NULL COMMENT '规格描述,如500ml*24瓶',
unit VARCHAR(10) NOT NULL COMMENT '基本单位,如瓶/箱/斤',
cost_price DECIMAL(10,2) NOT NULL DEFAULT 0.00 COMMENT '最近成本价',
sell_price DECIMAL(10,2) NOT NULL DEFAULT 0.00 COMMENT '建议售价',
safe_stock DECIMAL(12,3) NOT NULL DEFAULT 0.000 COMMENT '安全库存下限',
expire_days INT DEFAULT NULL COMMENT '保质期天数,无保质期则为NULL',
status TINYINT NOT NULL DEFAULT 1 COMMENT '1上架 0下架',
is_deleted TINYINT NOT NULL DEFAULT 0,
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
KEY idx_category (category_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='商品档案表';
CREATE TABLE tb_stock (
id INT PRIMARY KEY AUTO_INCREMENT,
product_id INT NOT NULL COMMENT '商品ID',
quantity DECIMAL(12,3) NOT NULL DEFAULT 0.000 COMMENT '当前库存数量',
update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
UNIQUE KEY uk_product (product_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='实时库存表';
CREATE TABLE tb_stock_flow (
id INT PRIMARY KEY AUTO_INCREMENT,
order_no VARCHAR(32) NOT NULL COMMENT '关联业务单号',
product_id INT NOT NULL COMMENT '商品ID',
change_type TINYINT NOT NULL COMMENT '1采购入库 2销售出库 3盘盈 4盘亏 5报损 6期初',
change_qty DECIMAL(12,3) NOT NULL COMMENT '变动数量,正数为增加,负数为减少',
before_qty DECIMAL(12,3) NOT NULL COMMENT '变动前库存',
after_qty DECIMAL(12,3) NOT NULL COMMENT '变动后库存',
create_by INT NOT NULL COMMENT '操作人',
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
remark VARCHAR(255) DEFAULT NULL,
KEY idx_product_time (product_id, create_time),
KEY idx_order_no (order_no)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='库存流水表';
另外还有tb_purchase_order、tb_purchase_item、tb_sale_order、tb_sale_item等业务单据表,核心字段就是订单号、商品ID、数量、单价、金额、操作人、时间。业务明细表里的商品快照信息(商品名、规格、单位)建议冗余存储,因为商品档案后续可能改名,但历史单据不能跟着变。
3.2 字段类型的几个关键细节
这一节的内容非常实际,都是我在真实项目里踩过之后才总结出来的。
金额字段必须用DECIMAL(10,2),绝对不要用FLOAT或DOUBLE。浮点数在计算机里是二进制近似值,0.1加0.2可能等于0.30000000000000004,对账的时候差一分钱都让人头疼。Java实体对应要用BigDecimal,不要用Double接收。
数量字段建议用DECIMAL(12,3),因为批发超市有按重量计价的商品,比如散装糖果、干货,3位小数能满足克级精度。如果只用INT,这类商品就没法录入。
时间字段统一用DATETIME,不要混合用TIMESTAMP和DATETIME,免得排序和比较出问题。另外连接串要配serverTimezone=Asia/Shanghai,否则查出来的时间会和本地差8小时。
逻辑删除字段is_deleted必须带上,但查询时不要漏掉条件,否则列表会出现已删除的数据。
3.3 库存流水表是整套系统的审计底账
我见过很多库存系统只维护一张实时库存表,出库就减数量,入库就加数量,看起来简单,一旦数据对不上,没有任何手段去追溯差异是怎么产生的。所以tb_stock_flow这张流水表是整个数据库设计的灵魂。
流水表设计时我特意加了before_qty和after_qty两个冗余字段。虽然从数据规范的角度看,这两个字段可以通过流水推算出来,但冗余存放会让对账和排查快得多。比如某商品账面库存是120,实际盘点只有117,我只需要查这个商品的流水,看看最后一次变动前后的库存数字,很快就能发现是哪一笔单据没走对。如果没有这两个字段,就要把所有流水累加一遍才能算出来,效率低还容易出错。
任何涉及库存变动的业务动作,采购入库、销售出库、盘盈盘亏、报损、期初建账,都必须向流水表插入记录。这应该成为业务代码里不可绕过的规则。
3.4 索引设计不需要铺太多
索引不是越多越好,写多读少的表尤其要注意。我的建议是:
- 商品表的
category_id加普通索引,分类查询会用到 - 实时库存表的
product_id加唯一索引,保证一个商品只有一条库存记录 - 库存流水的
(product_id, create_time)建复合索引,查某个商品的进出历史很快 - 业务单据表的
order_no加唯一索引,防止重复提交
不需要对remark、spec这种字段加索引,查询条件里很少用到,加了反而拖慢写入速度。订单状态类的枚举字段可以加普通索引,方便按状态过滤,但也要根据实际查询频率决定。
4. 核心代码链路:入库、出库与库存回写的完整闭环
4.1 采购入库:四张表在一个事务里联动
采购入库这个操作,表面上是填一张单子,背后其实涉及四张表的联动:采购主表、采购明细表、实时库存表、库存流水表。任何一步失败,整个事务都要回滚。
核心Service代码如下:
java复制@Transactional(rollbackFor = Exception.class)
public void createPurchaseOrder(PurchaseOrderDTO dto, Integer operatorId) {
// 1. 创建采购主单
PurchaseOrder order = new PurchaseOrder();
order.setOrderNo(generateOrderNo("PU"));
order.setSupplierId(dto.getSupplierId());
order.setTotalAmount(dto.getTotalAmount());
order.setCreateBy(operatorId);
purchaseOrderMapper.insert(order);
// 2. 循环处理明细
for (PurchaseItemDTO item : dto.getItems()) {
// 插入采购明细
PurchaseItem pi = new PurchaseItem();
pi.setPurchaseId(order.getId());
pi.setProductId(item.getProductId());
pi.setQuantity(item.getQuantity());
pi.setCostPrice(item.getCostPrice());
purchaseItemMapper.insert(pi);
// 增加实时库存
stockMapper.increaseStock(item.getProductId(), item.getQuantity());
// 查询变动前后库存,写入流水
BigDecimal after = stockMapper.selectQuantity(item.getProductId());
StockFlow flow = new StockFlow();
flow.setOrderNo(order.getOrderNo());
flow.setProductId(item.getProductId());
flow.setChangeType(1);
flow.setChangeQty(item.getQuantity());
flow.setBeforeQty(after.subtract(item.getQuantity()));
flow.setAfterQty(after);
flow.setCreateBy(operatorId);
stockFlowMapper.insert(flow);
}
}
这段代码的关键点是@Transactional注解。没有事务保护的话,可能主表插入成功、明细插入失败,库存却已经加了,最后账实不符。另外,stockMapper.increaseStock建议写成一条原子SQL:
sql复制<update id="increaseStock">
update tb_stock
set quantity = quantity + #{qty}
where product_id = #{productId}
</update>
这里就不要先查再更新了,直接让数据库做加法,天然支持并发。
4.2 销售出库:一条Update语句防超卖
销售出库的难点在于防止超卖。两个收银员同时卖同一个商品,如果代码逻辑是先查库存、判断够不够、再扣减,那么并发场景下两个请求都可能查到库存充足,最后把库存扣成负数。
解决方式很简单,把判断和扣减合并成一条Update语句:
sql复制<update id="deductStock">
update tb_stock
set quantity = quantity - #{qty}
where product_id = #{productId}
and quantity >= #{qty}
</update>
这段SQL的意思是:只有当当前库存大于等于要扣减的数量时才更新,并且直接在原值上减。数据库的行锁机制保证同一时刻只有一个事务能更新这条记录,另一个事务会阻塞等待,更新完成后发现条件不满足,受影响行数为0,我们在Java里判断这个行数,等于0就抛出库存不足的异常。
java复制int rows = stockMapper.deductStock(productId, qty);
if (rows == 0) {
throw new BusinessException("商品[" + productName + "]库存不足");
}
如果希望给用户更友好的提示,可以在Service层先查一次库存作为提示信息,但绝对不要把判断库存是否足够的逻辑建立在查到的数值上,最终扣减必须靠上面这条带条件的Update。
4.3 MyBatis批量插入与订单号回填
采购单明细少则几十条,多则上千条。MyBatis批量插入有两种常见写法。
第一种是用<foreach>拼接多条INSERT语句:
xml复制<insert id="batchInsertItems">
insert into tb_purchase_item(purchase_id, product_id, quantity, cost_price)
values
<foreach collection="list" item="item" separator=",">
(#{item.purchaseId}, #{item.productId}, #{item.quantity}, #{item.costPrice})
</foreach>
</insert>
这种写法简单直观,但是明细量过大的时候,拼接出来的SQL文本可能超过MySQL的max_allowed_packet限制,导致报PacketTooBigException。我之前遇到过一个订单有900多条明细,就是被这个问题折腾了半天。
第二种是使用MyBatis的Batch执行器:
java复制SqlSession sqlSession = sqlSessionFactory.openSession(ExecutorType.BATCH);
// 通过 sqlSession.getMapper(...) 获取Mapper
// 循环调用 insert,最后统一 commit
Batch模式不会一次性拼接一条巨长的SQL,而是复用一条预处理语句批量执行,性能和稳定性都更好。我的建议是明细在100条以内用<foreach>拼接没问题,超过200条就用Batch,并且每500条左右提交一次,避免事务过长占用数据库资源。
还有一个细节是订单主表的ID回填。插入主表时加上useGeneratedKeys="true" keyProperty="id",MyBatis会自动把数据库自增主键回填到实体的id字段,这样后续插入明细时才能拿到purchaseId。
4.4 盘点与报损:用统一的变动逻辑扩展
盘点单的业务逻辑是:选择商品,录入实盘数量,系统对比账面数量,自动计算差异。如果实盘大于账面,生成盘盈记录,库存增加;如果实盘小于账面,生成盘亏记录,库存减少。报损单的逻辑也类似,只是原因固定为破损或过期。
这两种场景的代码完全可以复用入库出库的库存更新逻辑,只是change_type不同。我的做法是抽一个内部方法changeStock(orderNo, productId, changeType, qty),统一处理库存更新和流水写入,采购入库、销售出库、盘盈盘亏都调用这个方法。这样做的好处是后续扩展“其他入库”“其他出库”之类的场景时,只需要加一个枚举值,不用改底层逻辑。
5. 从开发到上线:我在真实项目里踩过的几个实实在在的坑
5.1 MySQL连接串参数:MySQL 8.0和5.7的明显差异
有一次系统部署到客户那边,数据库是MySQL 8.0,项目启动直接报错,Druid连接池初始化失败,日志里一行Public Key Retrieval is not allowed。原因很简单:MySQL 8.0默认使用caching_sha2_password认证,连接串里需要加allowPublicKeyRetrieval=true。
正确的连接串应该这样写:
properties复制jdbc:mysql://localhost:3306/db_wholesale?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
这里四个参数都很重要。useSSL=false去掉SSL握手,减少连接耗时;characterEncoding=utf8保证中文不乱码;serverTimezone=Asia/Shanghai解决时间差8小时;allowPublicKeyRetrieval=true解决MySQL 8.0的认证报错。MySQL 5.7不需要最后这个参数,所以如果项目同时在两种版本上跑,建议统一加上,兼容性更好。
排查这个问题时我的思路是:先看驱动类,MySQL 8.0要用com.mysql.cj.jdbc.Driver,5.7可以用com.mysql.jdbc.Driver;再看连接串参数;最后登录MySQL执行SELECT NOW(),确认服务器时区。按这个顺序排查,大部分连接问题都能定位。
5.2 金额字段用Float的代价:对账差几分钱
我早期维护过一个类似系统,里面订单金额字段用的是Float,月底财务对账,订单金额加起来总是和供应商的对账单差几分钱,客户一度怀疑是系统逻辑有bug。最后定位到根因:浮点数精度丢失。
举个最简单的例子,0.1加0.2在二进制浮点数里算出来是0.30000000000000004,单独看没什么,累计几千上万条订单后误差就明显了。如果金额字段用DECIMAL(10,2),数据库层面就是精确的定点数计算,Java对象用BigDecimal接收,金额相关的计算全部用BigDecimal的add、multiply,不要用+、*直接算基本类型。
这个坑我建议在一开始建表时就避免,改起来很痛苦,涉及所有金额字段、所有统计SQL、所有Java实体类,还可能影响历史数据。
5.3 大批量采购明细插入失败的完整排查链路
前面提到900条明细导致SQL过大的问题,这里说说排查过程。当时的现象是点击保存采购单后,后台报错:MySQLSyntaxErrorException,紧接着还出现过PacketTooBigException。
第一反应是查看SQL语句,把MyBatis日志打印出来,发现拼接出来的INSERT语句有几百KB,明显不正常。接着查看MySQL配置SHOW VARIABLES LIKE 'max_allowed_packet',默认值是4MB,正常情况下一条SQL不至于超过这个值,但900条明细的SQL文本确实可能逼近。
最终解决方案是改用Batch执行器,并且每500条提交一次。实践下来,900条明细的插入时间从原来的2秒多降到400毫秒左右,而且不再报错。这里我提醒一句:日志里打印超长SQL在生产环境非常危险,会占用大量磁盘和内存,最好限制MyBatis日志只输出统计信息,不输出完整SQL。
5.4 Tomcat部署时第三方jar冲突导致NoSuchMethodError
这个问题不是SSM特有的,但SSM项目部署到Tomcat时很容易踩。现象是同一个Tomcat下部署了多个项目,其中一个项目启动正常,另一个项目某些功能调用时报NoSuchMethodError或ClassNotFoundException。
排查后发现是Tomcat的lib目录下有一个旧版本的第三方jar包,两个项目用到了同一个类库但版本不同,Tomcat的类加载机制优先加载公共lib下的类,导致其中一个项目用错了版本。处理方法很简单:清理Tomcat lib目录下非Tomcat自带的jar包,项目依赖全部用Maven打包进WEB-INF/lib。
对于SSM项目,还要特别关注Jackson和MyBatis的版本兼容。Spring MVC 4.x默认支持Jackson 2.x,手动往Tomcat lib里扔Jackson 1.x就会出问题。排查这类问题可以用mvn dependency:tree查看依赖树,确认项目里实际引用的版本。
5.5 角色权限:三个角色足够了,但必须严格区分
这套系统的使用者主要是三类人:老板或管理员、库管、收银前台。权限设计建议从第一个版本就做,不要等上线后再补。
管理员拥有所有菜单的权限,可以改商品档案、供应商信息、用户账号。库管负责入库、盘点、报损,能看到库存和流水,但不能修改商品价格,也不能审核销售单。收银前台只能开销售单、查库存,不能入库,不能看成本价和毛利报表。
用拦截器在Controller层做角色校验,是比较简单可控的方案。页面上的按钮也要按角色隐藏,不要只拦截接口不控制界面,否则收银员点开页面发现一堆不能点的菜单,体验很不好。
6. 低成本部署与日常运营建议
6.1 一台旧服务器也能扛住几十家门店
批发超市的IT预算通常不高,我不建议一上来就买高性能服务器。这套系统配置4核CPU、8G内存、SSD硬盘就已经非常宽裕,数据库和Tomcat装在同一台机器上就行。操作系统用Ubuntu Server或CentOS,装JDK 8、Tomcat 8.5、MySQL 5.7或8.0,把war包扔到Tomcat的webapps目录,启动即可。
如果门店不止一家,可以选择把系统部署到一台有公网IP的云主机上,各门店通过域名或IP访问,数据库连接数和带宽在初期都不用担心。日常订单量几百单的批发超市,这套配置可以说是绰绰有余。
部署时建议把Tomcat端口改成非默认端口,比如8081,减少被扫描的风险;MySQL不对外开放端口,只允许本机访问。数据安全比花里胡哨的功能更重要。
6.2 每天自动备份数据库
库存业务数据一旦丢失很难找回,备份必须自动化。写一个Shell脚本加入crontab,每天凌晨2点执行:
bash复制#!/bin/bash
BACKUP_DIR=/data/backup/mysql
DATE=$(date +%F)
mysqldump -u用户名 -p密码 db_wholesale > $BACKUP_DIR/db_wholesale_$DATE.sql
find $BACKUP_DIR -mtime +7 -delete
脚本做了两件事:导出当天数据库快照、删除7天前的旧备份。建议一个月至少验证一次备份文件的完整性和恢复流程,不要等到出问题了才发现备份文件是坏的。war包和配置文件单独备份,数据库备份文件不要和程序放在同一个磁盘,防止磁盘损坏时一起丢。
6.3 日常运营中必须养成的几个数据习惯
系统用得久了,"数据漂移"是必然的。比如有人图省事直接改数据库里的库存数字、删掉一笔错误订单,这些操作都会让库存流水变得不可信。我的习惯是:
第一,任何库存调整都走系统单据。盘盈走盘盈单,过期破损走报损单,期初数据走初始化单,不要手工改库。
第二,每月月底必须盘点。盘点差异金额小的,检查流水记录找到原因;差异大的,停下来彻底排查,不要反复用盘盈盘亏单去"调平"。
第三,大版本升级前先备份数据库和war包,升级窗口选在打烊后,并准备回滚方案。
第四,定期清理和归档历史订单数据。业务跑了几年后,订单明细表可能上百万条,查询会明显变慢。可以按月份把历史数据归档到独立表,保证热表查询性能。
这个系统我前前后后改过几轮,最大的发现是:库存管理系统的复杂度从来不在于用了多新的框架,而在于每一笔业务变动能不能从头到尾都追得到。后来我再接触任何类似系统,第一件事肯定是看有没有一张像tb_stock_flow这样完整的流水表。有,后面开发报表、对账、预警都会很顺;没有,就只能一边救火一边补底账。如果你现在也在写或者维护类似的项目,建议把"流水先行"这四个字刻在脑子里。
