☰
进销存系统开发实战:从用例图到库存扣减与对账避坑指南
2026/10/10 3:11:48 网站建设 项目流程

简介:一套面向超市运营场景的进销存管理系统项目资源,围绕商品进货、销售管理、库存统计、订单管理等核心业务流程设计,并附带用例图辅助理解系统角色与功能关系。资源共103个文件,以C#源文件(cs)和ASP.NET页面(aspx)为主,配合CSS样式、jpg/gif界面示意图,以及mdl、mdf等数据库文件,压缩包约2.03MB。内容覆盖供应商与商品信息录入、采购订单生成、条形码销售、库存阈值预警、订单状态追踪等具体实现,用例图清晰展现员工、顾客、供应商与各功能模块的交互流程。目前已有4379人学习下载,适合用于课程设计、毕业设计或系统二次开发时快速掌握进销存业务流程与代码组织方式。

1. 这套超市进销存管理系统,到底解决什么问题?

很多刚接触管理系统开发的人,第一次接到的需求就是"做个进销存"。听起来简单:进货、卖货、改库存。可真做起来才发现,进销存系统的核心根本不是增删改查,而是"账实一致"——数据库里的库存、今天卖了多少、供应商送了多少货,这些数据在一天结束的时候能不能对得上。我见过好几个开发者在自测时一切正常,一上线跑一周,库存负数、销售金额对不上、进货单被重复提交,问题全冒出来了。这篇文章要讲的这套超市进销存管理系统,是一个基于经典分层架构的Web应用,代码仓库里附带完整的用例图(UML用例图),用来明确系统边界和角色权限。

这套系统适合谁?如果你是正在做课程设计、毕业设计,或者刚入行想找一个业务完整的练手项目,它比单纯的书店管理系统、学生管理系统更贴近真实商业场景:涉及多角色、多单据、库存流水、金额计算,还有并发扣库存这种经典坑。整套系统的业务范围从供应商管理、商品档案,到进货入库、前台收银、库存盘点,全部覆盖。用例图在这个项目里不是应付文档用的,它直接决定了后台的菜单权限是怎么划分的——后面我会专门用一章讲怎么从用例图推导出权限表。

下面进入正题,我先从最值得参考的用例图讲起,接着是数据库建模,然后是库存扣减的核心逻辑,最后是避坑清单和对账验证。

2. 从用例图开始的角色与权限设计:这张图不是画着好看的

2.1 用例图里的四个角色,对应着系统里的哪些操作权限

这套系统的用例图一共涉及四个角色:店长(管理员)、收银员、采购员、仓管员。店长的用例是最多的,包含员工管理、商品管理、供应商管理、进货审核、销售报表查看、盘点审核,几乎覆盖全系统;收银员只有两个用例——前台收银和退货;采购员的用例集中在供应商管理和进货申请,采购员可以创建进货单,但不能审核入库;仓管员负责入库确认和库存盘点。这种划分符合大多数超市的日常分工。

用例图在这个项目里直接引导了后端权限表的设计。看不到图的情况下,我们可以推导出最核心的规则:权限最小化,单据创建和审核分离。采购员能建进货单但不能审核入库,就是为了避免"一个人既买菜又记账"的漏洞。你在实现的时候,不要把权限控制做成简单的"管理员全部放行,其他角色只能读",而是要把页面按钮级别的权限做出来。比如进货审核通过后,"审核通过"按钮对采购员不可见,只能看到自己创建的进货单列表。

2.2 用UML工具画出用例图:步骤、边界和颗粒度

画用例图我一般用两个工具:简单快速用在线绘图工具(比如draw.io),要更规范就用UML工具(比如StarUML)。用例图不需要画得特别复杂,关键是边界清晰。步骤如下:

第一步,先画一个系统边界矩形框,框里面的左边放置角色,右边放置用例。角色用"火柴人"图标表示,从角色到用例之间画实线。第二步,把上面说的四个角色分别放在左右两侧,用例按业务模块分组:商品管理、进货管理、销售管理、库存管理、系统管理。第三步,用include关系处理"公共操作"。比如"前台收银"这个用例,必然包含"查询商品信息"这个子用例;"进货入库"必然包含"查询商品库存"。include关系用虚线箭头加《include》字样标注。

第四步,处理"进货申请"和"进货审核"的关系。这两个用例虽然都叫"进货",但使用者不同:采购员执行申请,店长执行审核。在用例图上它们必须拆成两个独立的用例,否则权限模型会跟着出错。我在画图的时候见过不少开发者把这两个合并成一个"进货管理",结果到了写权限拦截器的时候,纠结半天也没法分清谁能操作哪个按钮。

这里要强调一个画用例图的颗粒度问题:用例图不要画到"新增商品"、"编辑商品"、"删除商品"这种CRUD粒度。用例图的目的是表达角色与业务目标的交互,"商品信息维护"就是一个完整用例,拆成"新增/编辑/删除"会让图变得琐碎,而且在推导权限时没有帮助。

2.3 从用例图到权限控制:Spring Security或拦截器的配置思路

用例图定下来之后,权限控制就好写了。如果项目用的是Spring Boot,我建议不需要引入完整的Spring Security(那套框架对刚做中小型系统的人来说偏重、配置繁琐),用拦截器加注解的方式就够用。核心是一个自定义注解@RequirePermission,加在Controller的方法上,然后写一个HandlerInterceptor去校验当前登录用户的角色。

@Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface RequirePermission { String[] roles() default {}; // 允许访问的角色编码,如 {"ADMIN", "CASHIER"} }
public class PermissionInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { if (handler instanceof HandlerMethod) { HandlerMethod handlerMethod = (HandlerMethod) handler; RequirePermission annotation = handlerMethod.getMethodAnnotation(RequirePermission.class); if (annotation == null) { return true; // 未加注解的接口不做拦截 } User currentUser = (User) request.getSession().getAttribute("LOGIN_USER"); String[] requiredRoles = annotation.roles(); for (String requiredRole : requiredRoles) { if (currentUser.getRoleCode().equals(requiredRole)) { return true; } } response.setStatus(403); response.setContentType("application/json;charset=UTF-8"); response.getWriter().write("{\"code\":403,\"msg\":\"无权限访问\"}"); return false; } return true; } }

这段代码的逻辑很简单:每个接口方法标注上允许访问的角色列表,拦截器在请求进入Controller之前,先看当前用户的角色码是否在允许列表里,不在就直接返回403。这里有两个细节值得注意:第一,roles()是数组,意味着一个接口可以同时允许多个角色访问,比如"查询商品列表"这个接口,收银员和仓管员都需要用;第二,注解只标在Controller方法上,Service层不用加,因为Service层不处理登录状态,拦截器层面统一处理就够了。

权限拦截器在注册时要排除登录接口和静态资源路径,否则会出现死循环:登录接口本身也被拦截,用户永远登录不进去。注册代码如下:

@Configuration public class WebConfig implements WebMvcConfigurer { @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new PermissionInterceptor()) .addPathPatterns("/**") .excludePathPatterns("/login", "/logout", "/static/**", "/error"); } }

3. 数据库建模:六张核心表和一张库存流水表

3.1 商品表、供应商表、进货单、销售单:字段怎么设计才合理

超市进销存系统的数据库表设计,是这套系统能不能稳定运行的关键。我见过不少把商品信息和库存数量放在同一张表里的做法,也不反对,但建议至少拆成商品表、进货单表、进货单明细表、销售单表、销售单明细表、供应商表六张核心表,再加一张库存流水表用于对账。

商品表(product)的核心字段是:id、product_name、specification(规格,比如"500ml/瓶")、unit(单位,包/瓶/袋)、purchase_price(进货价)、sale_price(销售价)、stock_quantity(当前库存)、warning_quantity(库存预警阈值)、category_id(商品分类ID)、status(上架/下架状态)。其中stock_quantity这个字段是冗余字段——它可以从库存流水表聚合出来,保留它是为了查询效率。真正要保证数据准确的是库存流水表,后面会讲。

供应商表(supplier)比较简单:id、supplier_name、contact_person、phone、address、remark。进货单表(purchase_order)是主表,字段包括:id、purchase_no(进货单编号,用时间戳加随机数生成)、supplier_id、purchase_total_amount(进货总金额)、status(待审核/审核通过/已入库/已作废)、create_time、audit_time、create_by(创建人ID)、audit_by(审核人ID)。进货单明细表(purchase_order_item)用于记录每一笔进货的商品明细:id、purchase_order_id(关联主表)、product_id、purchase_quantity、purchase_price、subtotal(小计金额)。为什么要拆主表和明细表?因为一张进货单关联多种商品,如果只建一张表存所有商品,数据冗余和更新异常会非常严重。这也是三范式里第二范式的典型应用。

销售单表(sales_order)逻辑上跟进货单对称:id、sales_no、total_amount、discount_amount(优惠金额)、pay_amount(实付金额)、payment_method(现金/微信/支付宝)、cashier_id(收银员ID)、create_time。销售单明细表(sales_order_item)字段:id、sales_order_id、product_id、sale_quantity、sale_price、subtotal。

3.2 库存流水表:每个库存变动都留痕迹,这是对账的唯一凭据

库存流水表(stock_flow)是整个系统最值得花时间设计的表。每一笔进货入库、销售出库、退货、盘点调整,都必须在流水表里插入一条记录。字段设计如下:id、product_id、change_type(变动类型:PURCHASE_IN入库、SALE_OUT销售出库、SALE_RETURN退货入库、CHECK_ADJUST盘点调整)、change_quantity(变动数量,正数入库负数出库)、before_quantity(变动前库存)、after_quantity(变动后库存)、order_no(关联的单据编号,方便追溯)、operator_id(操作人ID)、operate_time(操作时间)、remark。

表格:库存流水表字段说明

字段名类型说明
change_typeVARCHAR(20)变动类型,入库正数、出库负数
before_quantityINT变动前的库存数值,用于审计
after_quantityINT变动后的库存数值,必须等于 before + change
order_noVARCHAR(40)关联的进货单号或销售单号
operator_idBIGINT操作人ID,定位责任人的关键字段

这里要说明一个原则:任何情况下,商品表的stock_quantity字段都不能直接被UPDATE语句修改。库存的变更只能从流水表计算出来。具体做法是:在事务里先插入流水记录,然后根据流水记录更新商品表的库存字段。这个顺序很重要,保证操作失败时事务回滚,库存和流水保持一致。

3.3 创建表的SQL代码:主键、外键、唯一索引和事务

下面是六张核心表的建表SQL,这里给出商品表、进货单主表和库存流水表的示例,完整建表脚本在项目的SQL文件里:

-- 商品表 CREATE TABLE product ( id BIGINT PRIMARY KEY AUTO_INCREMENT, product_name VARCHAR(100) NOT NULL, specification VARCHAR(100), unit VARCHAR(20) NOT NULL, purchase_price DECIMAL(10,2) NOT NULL, sale_price DECIMAL(10,2) NOT NULL, stock_quantity INT NOT NULL DEFAULT 0, warning_quantity INT NOT NULL DEFAULT 0, status TINYINT NOT NULL DEFAULT 1, category_id BIGINT, INDEX idx_category_id (category_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 进货单主表 CREATE TABLE purchase_order ( id BIGINT PRIMARY KEY AUTO_INCREMENT, purchase_no VARCHAR(40) NOT NULL UNIQUE, supplier_id BIGINT NOT NULL, total_amount DECIMAL(10,2) NOT NULL DEFAULT 0, status TINYINT NOT NULL DEFAULT 0, create_by BIGINT NOT NULL, create_time DATETIME NOT NULL, audit_by BIGINT, audit_time DATETIME ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 库存流水表 CREATE TABLE stock_flow ( id BIGINT PRIMARY KEY AUTO_INCREMENT, product_id BIGINT NOT NULL, change_type VARCHAR(20) NOT NULL, change_quantity INT NOT NULL, before_quantity INT NOT NULL, after_quantity INT NOT NULL, order_no VARCHAR(40), operator_id BIGINT, operate_time DATETIME NOT NULL, INDEX idx_product_time (product_id, operate_time) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

建表时有三个地方需要额外说明。第一,金额字段必须用DECIMAL,不能用DOUBLE或者FLOAT,这个在避坑章节会详细解释。第二,purchase_no字段加了UNIQUE约束,防止并发时生成重复的进货单号。第三,所有表统一使用InnoDB引擎,因为进销存系统的事务要求很高,MyISAM不支持事务,一旦写入过程中断,库存和单据会出现部分数据丢失的现象,这种问题排查起来非常痛苦。

4. 用 MyBatis 实现库存扣减:事务和锁的并发控制

4.1 进货入库和销售出库的代码实现

进货入库的核心逻辑在Service层。整个流程是:校验进货单状态是"审核通过"且未入库 → 遍历进货单明细 → 检查商品是否存在 → 修改商品库存 → 写入库存流水 → 更新进货单状态为"已入库"。代码如下:

@Transactional(rollbackFor = Exception.class) public void purchaseInbound(Long purchaseOrderId, Long operatorId) { PurchaseOrder order = purchaseOrderMapper.findById(purchaseOrderId); if (order == null || !order.getStatus().equals(OrderStatus.AUDITED)) { throw new BizException("进货单不存在或状态不允许入库"); } List<PurchaseOrderItem> items = purchaseOrderItemMapper.findByOrderId(purchaseOrderId); for (PurchaseOrderItem item : items) { Product product = productMapper.findById(item.getProductId()); if (product == null) { throw new BizException("商品不存在,商品ID=" + item.getProductId()); } int beforeQty = product.getStockQuantity(); int afterQty = beforeQty + item.getPurchaseQuantity(); // 更新商品库存,注意这里使用了乐观锁 int affected = productMapper.updateStockWithVersion( product.getId(), beforeQty, afterQty, product.getVersion()); if (affected == 0) { throw new BizException("商品库存更新冲突,请重试"); } // 写入库存流水 StockFlow flow = new StockFlow(); flow.setProductId(item.getProductId()); flow.setChangeType(StockFlowType.PURCHASE_IN); flow.setChangeQuantity(item.getPurchaseQuantity()); flow.setBeforeQuantity(beforeQty); flow.setAfterQuantity(afterQty); flow.setOrderNo(order.getPurchaseNo()); flow.setOperatorId(operatorId); flow.setOperateTime(new Date()); stockFlowMapper.insert(flow); } // 更新进货单状态 purchaseOrderMapper.updateStatus(purchaseOrderId, OrderStatus.INBOUND); }

这段代码里的关键逻辑在于updateStockWithVersion这个方法。它对应的SQL是:

UPDATE product SET stock_quantity = #{afterQty}, version = version + 1 WHERE id = #{id} AND version = #{version}

这个更新语句的意思是说:只有当商品表里的version(版本号)还是我查询时候的版本号时,才允许更新。如果在两次查询之间,另一个线程已经改过这条商品记录的库存,version就变了,UPDATE影响的行数为0,当前事务抛出冲突异常并回滚,从而避免库存错乱。这就是乐观锁——不锁住整张表,而是靠记录级版本号来防止并发覆盖。进货入库并发发生的概率不如销售出库高(因为进货是批次操作),但销售出库的并发扣减是必须处理的问题。

4.2 销售出库的库存扣减:为什么用悲观锁而不是直接UPDATE

销售出库的逻辑与进货类似,差别在于数量的方向(负值)和并发强度。收银台可能有多个收银员同时结账,同一件商品被两个人同时购买是常见场景。如果你只做"读取库存→检查库存足够→扣减库存",在并发高的时候就会翻车。举个例子:A收银员和B收银员同时读到库存是5,A卖出去1件,库存变成4;B也卖出去1件,如果B在扣减时没有约束,A和B都执行UPDATE product SET stock_quantity = 4,最后库存变成4而不是3,卖出去的2件商品只扣了1件库存——这就是超卖。

解决这个并发问题,我推荐使用SELECT ... FOR UPDATE悲观锁。在事务内查询商品记录时,直接加上行锁,其他事务只能等这个事务提交之后才能查询同一行数据。实现如下:

@Transactional(rollbackFor = Exception.class) public void createSaleOrder(List<SaleItemParam> params, Long cashierId) { // 1. 生成销售单号 String saleNo = generateSaleNo(); // 2. 计算总金额并扣减库存 BigDecimal totalAmount = BigDecimal.ZERO; for (SaleItemParam param : params) { // 关键:使用FOR UPDATE锁定商品行 Product product = productMapper.findByIdForUpdate(param.getProductId()); if (product == null) { throw new BizException("商品不存在"); } if (product.getStockQuantity() < param.getQuantity()) { throw new BizException("商品库存不足:" + product.getProductName()); } int beforeQty = product.getStockQuantity(); int afterQty = beforeQty - param.getQuantity(); productMapper.updateStockQuantity(product.getId(), afterQty); // 记录库存流水 StockFlow flow = new StockFlow(); flow.setProductId(product.getId()); flow.setChangeType(StockFlowType.SALE_OUT); flow.setChangeQuantity(-param.getQuantity()); flow.setBeforeQuantity(beforeQty); flow.setAfterQuantity(afterQty); flow.setOrderNo(saleNo); flow.setOperatorId(cashierId); flow.setOperateTime(new Date()); stockFlowMapper.insert(flow); // 计算小计 BigDecimal subtotal = product.getSalePrice().multiply(BigDecimal.valueOf(param.getQuantity())); totalAmount = totalAmount.add(subtotal); } // 3. 插入销售单和明细 SaleOrder saleOrder = new SaleOrder(); saleOrder.setSaleNo(saleNo); saleOrder.setTotalAmount(totalAmount); saleOrder.setPayAmount(totalAmount); saleOrder.setCashierId(cashierId); saleOrder.setCreateTime(new Date()); saleOrderMapper.insert(saleOrder); // 插入明细略 }

这里有两个选择问题需要想清楚。选悲观锁的原因在于,销售出库的并发写操作频率高,冲突概率大,用乐观锁会导致失败的客户端不断重试,而悲观锁是串行化的排队等待,对用户体验更友好。进货入库的并发概率低,用乐观锁就够了(上面的入库示例我用了乐观锁,出库示例我用悲观锁,这两种锁都是好方案,关键是知道自己选的是什么锁)。还有一种做法是直接用原子UPDATE,比如UPDATE product SET stock_quantity = stock_quantity - #{qty} WHERE id = #{id} AND stock_quantity >= #{qty},利用受影响行数判断库存是否不足。这种做法也可以,但缺点是拿不到变动前的库存值,库存流水里的before_quantity就没法填,所以我更推荐FOR UPDATE。

5. 进销存系统避坑:5个让数据对不上的经典坑位

5.1 用DOUBLE存金额导致库存金额对不上

现象:进货单的总金额加起来和明细小计对不上,月底对账相差几毛几分。原因:DOUBLE和FLOAT是浮点数,二进制无法精确表示0.1这样的十进制小数,在累加过程中会产生舍入误差。解决:所有金额字段一律使用DECIMAL(10,2),实体类中对应使用BigDecimal而不是Double。在MyBatis的resultMap里,金额字段的jdbcType要写DECIMAL,Java属性的setter接收BigDecimal,不要自己转成字符串再去运算。

5.2 事务不生效:Service方法被同类调用,库存更新一半就提交了

现象:方法明明标了@Transactional,但运行时抛异常,库存流水有记录,商品库存没更新,数据就错乱了。原因:Spring的事务基于AOP代理。同类内部调用(this.method())不会经过代理类,事务注解失效。比如在PurchaseService里,一个方法调用同类下的另一个带@Transactional的方法,事务就不会开启。解决:把事务方法放到另一个Service类中调用,或者注入自身代理(@Autowired+@Lazy用ApplicationContext取代理),最稳妥的就是"事务方法必须从外部进入"。

5.3 库存扣减后没有回滚,单据状态和库存不一致

现象:销售单创建成功,但库存没有扣,或者库存扣了,销售单没生成。原因:库存扣减和单据插入不在同一个事务中。比如先调用库存服务扣减,再调用下单服务保存,中间抛异常时,库存服务已经提交了。解决:在一个@Transactional方法里完成"扣库存 + 生成单据 + 写流水",任何一个步骤抛异常,全部回滚。如果你系统里服务拆分得很细,那就需要引入分布式事务(比如Seata),但对这个单体项目,一个事务方法就够了。

5.4 删除商品或供应商时出现外键报错或孤儿数据

现象:删除一个商品时提示有子记录关联无法删除,或者强行删除后进货明细里的商品名变成了空。原因:商品表被进货单明细和销售单明细引用,没有设计软删除字段。解决:给商品表增加status字段(0下架/1上架),删除操作的业务含义是"下架"而不是物理删除。这样历史单据里的商品信息始终完整,报表统计也不会丢数据。供应商同理,除非确认历史没有任何关联单据,否则不要物理删除。

5.5 并发进货采购单审核,导致一批货被入库两次

现象:仓管员同时打开两个浏览器标签页,对同一张进货单点了两次"入库",库存翻倍增加。原因:入库操作没有做单据状态校验,也没有加锁。第一次入库后状态已经变成"已入库",第二次操作时没有重新检查。解决:在purchaseInbound方法一开始就SELECT ... FOR UPDATE锁住进货单记录,在状态判断之后、执行入库之前,再次检查状态是否为"可入库"。只要状态更新和库存扣减在同一个事务里,第二个请求会等待第一个事务提交后,再读取到状态已经是"已入库",直接返回业务异常。

6. 库存不准怎么办:用对账脚本把问题定位到具体单据

系统上线运行后,不管你写完时多自信,最终都必须面对一个现实:真数据跑起来,库存一定会出问题。可能是人工误操作,可能是代码没走到的异常分支,也可能是数据库被外部改过。所以最后一件事,不是写新功能,而是写一个对账脚本,每天定时跑一遍,把账实不一致的数据查出来。

对账的核心思路是交叉验证三条独立的数据链路。第一条链路:销售单明细表里,每个商品的总销售数量之和;进货单明细表里(入库状态)的总进货数量之和;退货表同理。第二条链路:库存流水表里,每个商品的累计变动数量,flow里加上初始库存(假设系统上线前有过一次期初盘点,初始库存记录在盘点表里)。第三条链路:商品表里的当前stock_quantity。这三条链路的结果应该完全相等。用SQL实现:

-- 对账SQL:统计每个商品的流水累计变动数量 vs 商品表当前库存 SELECT p.id AS product_id, p.product_name, p.stock_quantity AS current_stock, IFNULL(SUM(sf.change_quantity), 0) + IFNULL((SELECT init_quantity FROM stock_initial WHERE product_id = p.id), 0) AS flow_stock FROM product p LEFT JOIN stock_flow sf ON sf.product_id = p.id GROUP BY p.id, p.product_name, p.stock_quantity HAVING current_stock != flow_stock;

这条SQL的输出结果,就是所有库存对不上的商品。找到了商品,接下来要定位是哪个单据。做法是查询该商品的库存流水,按时间排序,手工检查哪一条流水的before_quantity不等于前一条流水的after_quantity,中间必然有缺口或重复记录。如果流水的连续性没有问题,那就检查流水操作当日有没有外部手动修改数据库的痕迹。

这里分享一个血的教训:我在做一个模拟项目X的时候,系统上线第三周,老板说某个大件商品的库存数量明显不对。跑了对账SQL,发现流水连续,商品表库存也和流水一致,但实物库存就是多了两件。最后查出来,是运营人员通过后台直接把商品库存字段改了,改完库存流水没有记录。从那以后,我在系统里就多做了一个保护:任何商品表库存字段的变更,必须通过写库存流水接口完成,删掉所有直接对product表UPDATE库存的入口,包含SQL管理工具的使用规范也写进了交接文档。

对于这个项目,你可以在此基础上扩展自动修复功能:当对账发现不一致时,生成一条盘点调整单,把库存调成流水计算出的正确值。这一步要谨慎,必须让店长人工审核后再执行。盘点调整同样要走库存流水,change_type设为CHECK_ADJUST,这样整条链路始终有审计记录。

如果你的系统还在压测或试运行阶段,还有一个更快的验证办法:制造一笔极端数据。比如把某个商品库存改成1,然后同时发起两笔销售请求,看系统是否只允许一笔成功。用JMeter或Postman的并发功能发两个请求,落库后检查库存是否是0、失败的那笔请求是否收到了"库存不足"的提示。这种测试跑一遍,系统能不能抗住真实收银台的并发压力,心里就有底了。

最后养成一个习惯:每次上线或变更涉及库存计算的代码,跑一遍上面的对账SQL。库存不准不是什么玄学,无非是并发、精度、事务三件事没做好。把这个项目做透,从用例图到数据库到并发控制一条线走下来,以后接任何进销存、仓储、订单类的系统,核心逻辑你都不会怵。希望帮到你。

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

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

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

立即咨询