1. 项目整体设计思路与关键技术选型
作为Java后端开发,也兼职前端,这段时间把“企业超市仓库进销存管理系统”从需求梳理到上线完整做了一遍。这套系统采用SpringBoot作为后端框架,Vue作为前端框架,数据库用的MySQL,整体是典型的前后端分离架构。超市库存管理核心无非进、销、存三个字:进货入库、销售出库、剩余库存和盘点报表。项目虽然不是什么高并发大平台,但麻雀虽小五脏俱全,把采购单、销售单、库存流水、权限体系、报表统计这些模块拆开又连起来以后,对理解企业级Web系统开发非常有帮助。
这个项目非常适合正在找毕业设计课题的同学、给中小超市做管理软件的开发者,以及刚接触前后端分离开发的人参考。下文不打算只贴代码,而是把“为什么这么设计”“踩过哪些坑”一起讲透。特别是库存扣减的并发处理、前端路由刷新404、SpringBoot版本兼容这几个地方,几乎每个做类似系统的人都会遇到。
1.1 为什么选择SpringBoot + Vue,而不是传统单体项目
最早很多同学做课设会选Spring MVC + JSP + JQuery,所有页面都在服务端渲染,做简单的增删改查可以,但一旦页面交互复杂,比如采购单要在表格里一口气录十几行明细、还要自动算金额和校验库存,JSP那套就变得非常难受。Vue的双向绑定和组件化在表单密集型场景下效率会高很多,数据变了页面自动更新,这是前后端分离带来的直接收益。
SpringBoot的价值则在于“少写配置”。用传统Spring写一个可运行项目,要配置web.xml、DispatcherServlet、数据库连接池、事务管理器、MyBatis的SqlSessionFactory,一个不小心就启动失败。SpringBoot通过自动装配原理,把spring-boot-starter-web、spring-boot-starter-jdbc这些starter引入后,框架自动帮我把内嵌Tomcat、默认数据源、MVC解析器配好。我只要在application.yml里写明数据源地址和账号密码,项目就能跑起来。这也是后期项目能快速上手的关键。
前后端分离带来的另一个隐性优势是分工。后端只提供RESTful接口,不需要关心页面长什么样;前端只调接口渲染数据,也不需要被迫嵌入Java代码。后期如果要把管理端换成移动端H5,或者给仓管员做一个扫码PDA页面,后端接口可以完全复用,这点对进销存系统向多端扩展特别重要。
1.2 超市进销存业务模块怎么拆
开门见山说结论,超市进销存系统我最终拆成六个模块:基础资料、采购管理、销售管理、库存管理、报表统计、系统管理。基础资料负责维护商品分类、商品档案、供应商、客户和仓库信息;采购管理部门解决“货怎么进来”;销售管理部门解决“货怎么卖出去”;库存管理关注“现在还剩多少、什么时候要补货”;报表统计回答“赚了多少、哪个商品卖得好”;系统管理负责用户登录和菜单权限。
模块与模块之间不是孤立的。商品表是公共基础,采购单、销售单和库存表都围绕着商品编号和条码做关联。采购单审核通过后,库存增加;销售单审核通过后,库存减少;如果发生退货,再反过来冲销。每一步操作都会写入库存流水表,保证账实可追溯。整个系统的核心闭环就是:先有商品档案,再有出入库单据,最后形成库存数据和业务报表。
对做毕业设计的人来说,模块拆得太散会导致工作量爆炸,拆得太粗又显得没有深度。上述六个模块刚好覆盖进销存系统的全部主线,同时又能把SpringBoot的事务、权限、定时任务、Excel导入导出这些技术点都展示出来,是一个比较合理的粒度。
1.3 技术栈选型和版本选择
下面是我当时使用的技术栈清单,整理成表格方便参考。
| 层级 | 技术框架 | 版本建议 | 备注 |
|---|---|---|---|
| 后端基础 | JDK | 1.8 或 17 | 如果SpringBoot选2.7.x建议JDK8,选3.x必须JDK17 |
| 后端框架 | SpringBoot | 2.7.14 | 2.7是2.x系列中较稳定的版本,配套兼容性最好 |
| ORM框架 | MyBatis-Plus | 3.5.3 | 提供BaseMapper,单表CRUD不必写SQL |
| 数据库 | MySQL | 8.0 | 使用InnoDB,支持事务和行锁 |
| 缓存可选 | Redis | 5.x | 用于登录token缓存或非实时库存扣减,本系统非必需 |
| 安全认证 | JWT + HandlerInterceptor | jjwt 0.11.5 | 无状态认证,比Session更适合前后端分离 |
| 前端框架 | Vue | 2.7.6 | Vue2最终发行系列,生态成熟 |
| UI组件库 | Element UI | 2.15.14 | 表格、弹窗、表单足够用 |
| 前端路由 | Vue Router | 3.x | 配合Vue2使用,懒加载页面组件 |
| 构建工具 | Maven / npm | —— | 后端Maven,前端npm |
这里重点提一句“SpringBoot版本太高”的问题。SpringBoot 3.0把javax命名空间改成了jakarta,很多老教程代码直接报错,若本机又是JDK8,连启动都会失败。做企业项目我通常不会追最新版本,选择2.7.x或对应LTS版本,踩坑最少。等熟悉整套机制后再研究3.x不迟。前端同理,不是非要用最新Vue3,如果团队之前没有用过Composition API,直接上Vue3反而拖慢进度。技术选型最重要的是稳定、顺手、资料多,而不是“版本数字越大越高级”。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 数据库设计与核心表结构
2.1 进销存领域的模型抽象
数据库设计我放在整个项目的最前面来做,因为进销存系统一旦表结构不合理,后期开发会不停返工。听我一句劝,先花一个下午想清楚表,再动手写接口,比写了一半再改表省太多时间。
最核心的几张表可以抽象成这样:商品档案表(product)、商品分类表(category)、供应商表(supplier)、客户表(customer)、仓库表(warehouse)、实时库存表(stock)、库存流水表(stock_record)、采购单主表(purchase_order)、采购单明细表(purchase_order_detail)、销售单主表(sales_order)、销售单明细表(sales_order_detail)、系统用户表(sys_user)。
这里要特别注意,商品表和库存表是分开的。商品表负责描述“这个东西叫什么、成本多少、卖多少”,库存表负责描述“这个商品放在哪个仓库、还剩多少”。如果不拆开,一个商品在多仓库场景下就没法维护,而且每次商品资料修改都会影响库存记录,耦合度太高。
2.2 核心表字段说明
为了不把篇幅全部放在建表语句上,我把关键表的字段和设计意图用表格列出来,方便对照。
| 表名 | 核心字段 | 关键说明 |
|---|---|---|
| product | id, category_id, name, barcode, spec, unit, purchase_price, sale_price, threshold, status | barcode建议建唯一索引;spec是规格如500ml;threshold用于库存预警 |
| stock | id, product_id, warehouse_id, quantity, lock_quantity, update_time | 用(product_id, warehouse_id)建唯一索引,任意商品在任何仓库只有一个库存记录 |
| stock_record | id, product_id, warehouse_id, before_qty, change_qty, after_qty, biz_type, biz_no, remark | biz_type记录入库、出库、退货、盘点;biz_no对应采购单号或销售单号 |
| purchase_order | id, order_no, supplier_id, warehouse_id, total_amount, status, create_time | order_no唯一,按日期+流水号生成;status:0待审核,1已入库,2已作废 |
| purchase_order_detail | id, order_id, product_id, quantity, price, amount | 一张采购单关联多条明细,订单头和明细分离是进销存标准设计 |
| sales_order | id, order_no, customer_id, warehouse_id, total_amount, discount_amount, status, create_time | status:0待审核,1已出库,2部分退货,3已作废 |
| sales_order_detail | id, order_id, product_id, quantity, price, cost_price, amount | cost_price冗余在明细里,出库时可以算毛利 |
| sys_user | id, username, password, real_name, role_id, status | password保存BCrypt加密串 |
有几个字段设计是我从真实项目里得来的经验。第一个是金额字段一律用DECIMAL,不能用double,否则商品买一送一、折扣满减算下来经常有0.000001的误差。第二个是数量字段也要用DECIMAL,超市里卖散装糖果、水果按斤称,数量是2.35斤,不能定义成int。第三个是每条库存流水都要记录变动前和变动后的值,方便以后对账,比如一张销售单扣了5件,岗位审计人员要能看到扣减前后库存分别是多少。
2.3 索引、约束与数据一致性
数据库设计阶段我强制要求自己遵守几条原则。商品编码或条码必须唯一,不然录入采购单时根本没法定位商品。在stock表上建(product_id, warehouse_id)联合唯一索引,防止同一仓库对同一商品出现两条库存记录,这个错误一旦出现,上面所有统计都会错。所有订单号使用“前缀+日期+序列”,例如PO20240115001,避免主键数字过长且方便人眼识别。
外键我建议在逻辑上保留,但数据库物理外键能不加就不要加。真实项目里,采购单被删除时希望保留流水痕迹,物理外键会导致删不动;而且强关联会让锁竞争更明显。业务层的校验和唯一约束比数据库外键更灵活。不过每条业务记录的父亲字段(比如detail表的order_id)一定要加索引,否则明细查询会随着数据量增大而变得很慢。
数据一致性方面,进销存系统必须依赖MySQL事务。InnoDB下,采购入库时修改采购单状态、扣减库存、插入库存流水三个操作要放在同一个事务里,要么全部成功,要么全部失败。这也是后文接口设计里最重要的一个技术点。
3. 后端SpringBoot核心实现
3.1 项目初始化和代码分层
后端项目我通常直接在IDEA里用Spring Initializr创建,或者去start.spring.io生成后导入。工程结构采用最实用的四层拆分:controller(接口入口)、service(业务逻辑)、mapper(数据访问)、entity(数据库实体)。再额外加common包用来放Result、异常处理、常量,加config包放跨域、拦截器配置,加dto包放前端入参。
一个典型的pom.xml核心依赖如下:
xml复制<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.14</version>
</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.3</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.33</version>
</dependency>
</dependencies>
这个组合的好处是启动即用,不需要写xxx.xml配置MyBatis插件,也不需要手动管理连接池。MyBatis-Plus的BaseMapper提供了insert、deleteById、selectById、updateById,单表CRUD基本不用手写SQL。复杂多表关联再写一个XML,方便维护。
3.2 统一返回结果、参数校验与全局异常
前后端分离后,接口返回格式必须统一,否则前端处理起来会非常痛苦。我使用的响应结构如下:
json复制{
"code": 0,
"message": "ok",
"data": {}
}
后端定义一个Result<T>泛型类,成功返回Result.success(data),失败返回Result.error(code,message)。Controller里不主动捕获业务异常,而是由@RestControllerAdvice统一处理。比如库存不足我抛一个BusinessException,前端拿到code=1002就知道是库存不足,可以弹提示。这样做避免了无数个if-else判断,也让接口代码非常干净。
参数校验我会在Controller入参上用@Validated配合@NotNull、@DecimalMin,例如数量必须大于0、商品ID不能为空。采购入库时的单价由后端重新从product表取价,不信任前端传来的单价,防止有人通过篡改请求把采购价改成负数。虽然本项目不是银行系统,但从一开始守住边界会更安全。
3.3 事务控制与库存并发扣减
库存扣减是进销存系统里最容易出Bug的地方。很多人第一版会这样写:
java复制Stock stock = stockMapper.selectByProductId(productId);
if (stock.getQuantity() >= quantity) {
stock.setQuantity(stock.getQuantity() - quantity);
stockMapper.updateById(stock);
}
这段代码在没有并发时没问题,一旦两个人同时下单购买同一件商品,线程A和线程B都读到了库存为10,A扣到5,B也基于10扣到5,最终库存变成5而不是0,超卖就发生了。@Transactional解决不了这个问题,因为默认事务隔离级别下两个线程读到的是提交前的数据,互相覆盖就会丢更新。
我在项目里采用比较实用的方案:使用带条件的UPDATE语句,让数据库原子地完成“库存充足才扣减”:
java复制int rows = stockMapper.deductStock(productId, warehouseId, quantity);
if (rows == 0) {
throw new BusinessException("库存不足");
}
对应的SQL是:
sql复制UPDATE stock
SET quantity = quantity - #{quantity}
WHERE product_id = #{productId}
AND warehouse_id = #{warehouseId}
AND quantity >= #{quantity}
这条UPDATE是原子操作,MySQL的行锁会保证同一行库存同时只能被一个更新操作执行。影响行数为0说明库存不足,直接抛异常。扣减成功后再插入库存流水。库存流水插入和条件更新放在同一个@Transactional里,这样单据成功、库存扣减、流水记录三者保持一致。对于这样一个中小型超市进销存,这个方案比Redis分布式锁简单,也比乐观锁更直接,实测效果稳定。
3.4 登录认证与接口权限控制
系统管理模块我用了JWT做无状态登录。用户提交用户名密码后,后端校验通过生成一个签名token,前端存在localStorage,以后每次请求在请求头带上Authorization: Bearer xxx。后端用HandlerInterceptor拦截非登录接口,解析token并放到ThreadLocal中,方便Service层获取当前用户。
权限这块做得比较轻量,没有引入完整的Spring Security,因为超市进销存的角色很固定:管理员、采购员、销售员、仓管员。我给用户表挂一个role_id,前端根据角色动态显示菜单;后端在敏感接口上判断角色,比如销售单审核接口只允许店长角色访问。如果是毕业设计,这一步已经足够表现出权限设计的完整性;如果业务更复杂,再升级到Spring Security + RBAC。
4. 前端Vue实现
4.1 Vue项目初始化与环境配置
前端我用Vue CLI创建工程,命令如下:
bash复制vue create supermarket-admin
npm i element-ui
npm i axios
npm i vue-router@3
npm i vuex@3
这里特别说下vue安装及环境配置容易踩的坑。Node版本不能太高也不能太低,太高时一些老脚手架编译会报OpenSSL错误,太低时npm安装依赖又慢又容易失败。建议使用16或18 LTS版本。国内网络环境下,可以在项目里加.npmrc文件配置镜像,也可以用nvm来切换Node版本,避免“在A项目正常,在B项目一堆报错”的尴尬。
创建完成后,在main.js里注册Element UI和路由。门户页面用Layout布局,左侧动态菜单,右侧router-view承载业务页面。日常最多的是表格和弹窗,Element UI的el-table、el-dialog、el-form足够撑起整个项目,不需要另外引入重型组件。
4.2 路由组织与访问控制
路由设计直接照着后端模块划分,路径用语义化名称,例如/product对应商品管理,/purchase/order对应采购单,/sales/order对应销售单,/stock/current对应实时库存。组件全部采用懒加载:
javascript复制{
path: '/product',
name: 'Product',
component: () => import('@/views/Product.vue')
}
懒加载的作用是首屏只加载登录页和dashboard,其他页面等路由跳转时再按需加载,对大表单页面来说能明显改善初始白屏时间。
路由守卫是必须加的。我在router.beforeEach里检查是否有token,没有就强制跳到登录页;有token但访问了无权限路由则提示403。这个方案配合后端接口校验,前端是“减少错误请求”,后端才是“真正防线”。很多项目只做前端隐藏菜单,忽略后端校验,直接绕过前端调接口就能越权,这是不对的。
4.3 Axios封装与跨域处理
axios我封装成request.js,所有请求统一走一个实例:
javascript复制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 !== 0) {
Message.error(res.message);
return Promise.reject(new Error(res.message));
}
return res.data;
},
error => {
Message.error('网络异常,请稍后重试');
return Promise.reject(error);
}
);
这样做有个好处:页面代码不需要反复处理token和错误弹窗,只需要productApi.getList(params).then(data => ...)即可。开发环境跨域直接用vue.config.js的proxy配置:
javascript复制devServer: {
proxy: {
'/api': {
target: 'http://localhost:8080',
changeOrigin: true
}
}
}
生产环境则让Nginx把/api开头的请求转发到后端服务,前后端同域部署,自然没有跨域问题。如果后端单独部署在不同域名,就需要在SpringBoot里写CorsFilter允许指定来源,千万不能直接用*,否则携带cookie或凭证时会有问题。
4.4 核心页面逻辑:采购单多行明细
采购单页面是整个前端最复杂的页面,我在这里多说两句。页面顶部是采购单头信息:供应商、入库仓库、采购日期、备注。中间是一个动态表格,用户点击“新增商品”,弹出一个商品选择对话框,选中商品后把商品的默认采购价格带回到表格行,表格行数量、单价都可编辑,小计自动计算。多行明细的数据结构就是一个JavaScript数组,新增一行就push一个对象,删除一行就splice。
这种实现依赖Vue响应式的数组方法,注意不要直接用下标赋值去修改行数据,否则界面不会刷新。提交时把表头数据和明细数组一起发送给后端,后端一次性保存订单头与明细。这种“表格式表单”模式在进销存系统里非常普遍,掌握了采购单,销售单和退货单基本是复制改改字段的事情。
5. 业务核心流程实现
5.1 采购入库:从采购单到库存增加
采购入库我设计为“先建单,后审核”的两步流程。第一步,采购员填写供应商和商品明细,保存草稿,此时订单状态为待审核,库存完全不变。第二步,店长或管理员看到待审核单据,确认价格和数量无误后点击入库,后端在事务里把订单状态改为已入库,同时完成库存累加与库存流水写入。
为什么非要两步?因为直接一步入库虽然省操作,但改错了一个数量或单价,库存数据就被污染了。两步操作可以让不是采购员的角色进行复核,顺便形成“采购申请-审核入库”的审计链路。对超市这种流动性大、人员权限不那么严格的场景,这个设计非常实用。
入库时后端要做的一件事是从商品表重新取成本价,而不是完全信任前端明细表里的price。如果单据审核时市场价已经变动,允许修改单据价格,并在界面提示影响总额。这个流程可以通过“期初成本、移动加权成本”算得更专业,但小超市项目用当前成本价代入已经够用。
5.2 销售出库:库存校验与毛利核算
销售出库是系统里另一个关键环节。前台创建一个销售单,选择客户(零散客户可以选“散客”)、仓库,然后录入销售商品明细。后端收到创建请求后,立即走一遍库存条件更新,如果库存不足直接拒绝整单保存。审核出库时再做一次扣减,不过第一版为了简单,也可以只在审核时一次性扣减,前端会通过实时库存接口提前提示可售数量。
销售单明细里我冗余了cost_price即成本价,为什么?因为出库时商品成本可能已经变化,如果不把销售那瞬间的成本存下来,月底毛利统计只能到商品表去取当前成本,毛利率就可能失真。销售金额减掉存下来的成本金额,就是这条销售单的毛利。对超市经营者来说,按日、按月统计毛利是最刚需的功能,数据库层面有这个冗余字段会舒服很多。
销售退货我做成反向流程:退货单关联原销售单,选择退货商品,增加库存并记一条“销售退货”类型的流水。这里注意退货数量不能大于原销售单剩余可退数,否则会出现“退得比卖得多”的数据混乱。每个细节都加校验,这是进销存系统稳定的根本。
5.3 库存预警与报表统计
库存预警我采用最简单的阈值方案。商品档案上有threshold字段,比如某个饮料低于20瓶时提示补货。后端提供一个/stock/alert接口,查询条件就是quantity <= threshold。查询SQL大约为:
sql复制SELECT p.name, p.barcode, s.quantity, p.threshold
FROM stock s
JOIN product p ON p.id = s.product_id
WHERE s.quantity <= p.threshold
AND s.quantity > 0
加上pagination返回给前端列表,报表页做一个醒目颜色标识。如果要做通知,还可以每天定时任务把预警商品汇总后推送企业微信机器人,但这是二期优化,第一版先做查询就足够。
报表统计我用了按日分组。销售报表按月统计销售总额、成本总额、毛利:
sql复制SELECT DATE(s.create_time) AS biz_date,
SUM(s.total_amount) AS sale_amount,
SUM(s.cost_amount) AS cost_amount,
SUM(s.total_amount - s.cost_amount) AS profit_amount
FROM sales_order s
WHERE s.status = 1 AND s.create_time >= #{startDate}
GROUP BY DATE(s.create_time)
ORDER BY biz_date DESC
这里如果把销售金额明细从sales_order_detail再聚合一次会更精确,我在实际项目里为了减少聚合数据量,直接在sales_order主表增加了total_amount和cost_amount字段,出库审核时用明细汇总后回写。这是一个典型的“用空间换查询速度”的做法,报表接口的SQL会简单很多。
6. 常见问题与排查技巧实录
6.1 SpringBoot启动失败:版本、端口、依赖冲突
我在初期遇到过“SpringBoot版本太高”的恶心问题。有次直接选了3.2.0版本,启动时报java.lang.NoClassDefFoundError: javax/servlet/ServletException,因为3.x已经把Java EE迁移到了jakarta命名空间,网上老代码全用javax。我的JDK还是8,直接不支持。后来统一降回2.7.x,问题彻底消失。因此建议做类似课设或外包项目的人,现阶段就选SpringBoot 2.7 + JDK8,资料最多,兼容性最好。
启动失败还常见于端口占用。项目默认8080端口被其他进程占用时,SpringBoot启动日志里会有Port 8080 was already in use。可以改用server.port=8081,但更推荐用命令查出占用进程并杀掉。IDEA配置启动端口时注意,SpringBoot界面只能配置程序运行参数,真正的端口在application.yml的server.port,别改错位置。
6.2 Vue依赖安装与路由404
前端最常遇到的问题就是npm install报错。常见错误有Python和node-gyp编译失败,大多数情况下是Node版本不匹配,切换到16/18 LTS版本基本能解决。遇到ERESOLVE unable to resolve dependency tree时,先检查依赖版本,不要把一套项目里的Element UI、Vue和Vue Router混搭到不同主版本。
部署后刷新404是前后端分离项目的经典问题。Vue是单页应用,所有页面路由都由前端history接管,但Nginx默认找不到/product这样的物理路径,会返回404。需要配置:
nginx复制location / {
try_files $uri $uri/ /index.html;
}
这样刷新时Nginx会把请求重写到index.html,再由Vue Router解析路径。我在第一次部署时没有这行配置,用户一点刷新就白屏,排查很久才发现是Nginx fallback没配。
6.3 库存并发扣减的死锁与超卖
条件UPDATE解决了超卖问题,但并发高的场景下还要防止死锁。出现死锁的典型情况是:两个事务A和B都先操作商品1再操作商品2,A拿到了商品1的锁,B拿到了商品2的锁,接着相互等待对方释放锁。解决方案是让多个商品按固定顺序处理,比如在Service层对产品ID数组做一次排序,再按顺序执行库存更新。这样所有事务都以相同顺序拿锁,就不会形成循环等待。
还有一次线上数据对不上,排查后发现问题出在“先查库存再批量扣减”的循环里。有人在一个事务里多次执行select ... for update,MySQL对同一行记录加锁没问题,但不同行的锁叠加后容易造成死锁。改成单条批量UPDATE stock SET quantity = CASE ...一次更新所有商品库存,不仅锁更少,性能也更好。
6.4 MyBatis-Plus使用与逻辑删除的坑
MyBatis-Plus虽然方便,但有些默认行为需要留意。实体字段为null时,默认updateById不会更新该字段,如果业务里要把某个字段置空,需要手动在实体类上用@TableField(updateStrategy = FieldStrategy.IGNORED)或者在UpdateWrapper里显式set。逻辑删除字段也容易埋雷,库存表一旦设置逻辑删除,后续统计库存时所有mybatis查询都会自动带上deleted=0条件,如果哪次更新SQL是自定义的,条件漏写,删除的库存数据又会跑出来。我的建议是核心单据表不要开逻辑删除,直接做作废状态,数据行留档即可。
最后说点实际的
我做完这套系统后最大的体会是:进销存项目的技术难度并不在于SpringBoot和Vue本身的API,而在于业务流程不能乱。先把库存表设计成“一商品一仓库一行记录”,再把出入库和库存流水绑定在同一个事务里,这比花时间研究各种高级框架有用得多。如果你也要做一个类似的项目,我建议从采购入库这个闭环开始编码,先把库存转动起来,再补销售、报表和权限。后续想扩展也很顺手:给商品表加一个图片字段就能支持图片上传,把报表接口接上EasyExcel就能导出,再加一条库存调拨流程就能覆盖多仓场景。这个基础架构不挑业务,改改目录和字段,照样能复用到其他行业的仓库管理系统上。最后再提醒一句,生产环境记得给数据库做定时备份,库存数据丢了,超市老板会来找你拼命的。
