☰
超市进销存源码跑通与二次开发:表结构、库存流水与并发扣减
2026/10/31 5:23:37 网站建设 项目流程

简介:面向零售门店信息化教学、课程设计与小型超市管理系统实践者,这份超市进销存管理系统源码提供了一套可运行的 Java 桌面应用案例,覆盖登录身份校验、销售单与退货单处理、进货与入库查询、商品修改及销售统计等核心环节,便于理解进销存业务与数据库交互。压缩包共 239 个文件,约 1.24MB,包含 49 个 Java 源码与 146 个 class 编译文件,另有 png、jpg 界面截图,db、mdf、ldf 数据文件,以及 xml、project、classpath 等工程配置和 doc、txt 说明文档,目录按模块组织,方便对照阅读。已有 50 人学习。读者可据此梳理 Dao 分层、面板事件处理、单据增删改查、库存更新与统计报表实现,并借助数据库与配置资料快速还原项目结构,适合作为课程设计、毕业设计或小型零售管理系统的参考底稿。

1. 超市进销存管理系统源码:先判断它是「能跑的工程」还是「能看的作业」

「超市进销存管理系统 源码」这个资源,最能筛人的地方不是代码量,而是拿到手后的第一个小时。有人解压、导库、改个数据库密码,十分钟就跑出登录页;也有人卡在依赖拉不下来、建表脚本报 1064、前端跨域,一晚上过去连首页都进不去。我拆过若干份同类工程,有个结论挺反直觉:这类源码跑不起来,八成问题不在业务代码,而在字符集、时区、端口这三件小事上。它适合想拿一套完整单据流(采购、销售、库存、盘点)做二次开发的人,也适合刚学完框架、想看看真实表结构长什么样的新手;不适合指望开箱即用直接上线的人。下面按「看清结构、跑通链路、稳住账目、避开雷区、动手改」的顺序往下拆。

2. 表结构先立住:商品主档、库存与单据流水怎么分层

进销存这类系统,代码写得好不好先放一边,表结构基本决定了它有没有二次开发的价值。我拿到一份不熟悉的源码,第一件事不是看 Controller,而是把sql/目录下的建表语句从头到尾读一遍,看它把「商品」和「库存」分没分开、把「库存余量」和「出入库流水」分没分开。这两刀切得干净,后面改起来就顺;切不干净,加个门店、加个批次就得推倒重来。

2.1 商品主档、条码与规格:为什么不该只做一张 goods 表

最常见的偷懒做法是把条码、规格、单位、价格全塞进一张goods表。演示够了,一上真实商品就翻车:同一款饮料有 500ml 和 1.5L,条码不同但属于同一商品;散称商品一个 PLU 码对应无数个重量。所以看源码先确认它有没有把「商品主档」和「条码/规格」拆开。我一般会按下面的最小结构去核对,缺哪张表心里先记一笔,后面二次开发补。

-- 商品主档:只存商品的公共属性,不存条码 CREATE TABLE `goods` ( `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键', `goods_no` VARCHAR(32) NOT NULL COMMENT '内部编码,业务唯一', `goods_name` VARCHAR(128) NOT NULL COMMENT '商品名称', `category_id` BIGINT NOT NULL COMMENT '分类ID', `unit` VARCHAR(16) NOT NULL DEFAULT '件' COMMENT '基本单位', `sale_price` DECIMAL(12,2) NOT NULL DEFAULT 0.00 COMMENT '零售价', `status` TINYINT NOT NULL DEFAULT 1 COMMENT '1上架 0下架', `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_goods_no` (`goods_no`), KEY `idx_category` (`category_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='商品主档'; -- 条码表:一个商品可以有多个条码,收银扫码从这里查 CREATE TABLE `goods_barcode` ( `id` BIGINT NOT NULL AUTO_INCREMENT, `goods_id` BIGINT NOT NULL COMMENT '关联 goods.id', `barcode` VARCHAR(64) NOT NULL COMMENT '国际条码或店内码', `spec` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '规格描述,如 500ml', `ratio` DECIMAL(10,3) NOT NULL DEFAULT 1.000 COMMENT '相对基本单位的换算率', PRIMARY KEY (`id`), UNIQUE KEY `uk_barcode` (`barcode`), KEY `idx_goods` (`goods_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='商品条码';

两处参数值得多说。sale_price用DECIMAL(12,2)而不是DOUBLE,是因为金额做浮点累加会产生0.30000000000000004这种误差,日结报表早晚对不上;ratio用三位小数是为了支持「箱/瓶」换算,一箱 24 瓶就填 24.000,散称按 0.5 千克结算也能塞得进去。uk_barcode必须是唯一索引,收银台扫码是高频查询,唯一索引既保证不重复也顺手把查询性能兜住了。

如果源码里只有一张goods表还带barcode字段,也不算不能改,但要有心理准备:加第二个条码时得动 SQL、动实体、动前端表单,改造成本比一开始就分表高得多。

2.2 库存不是一列数字:余量表 + 流水表才是账实一致的根

第二刀是库存。新手写的库存往往长这样:goods表里加一列stock,入库+、出库-。跑演示没问题,一旦要查「上周三这个商品为什么少了 3 件」,就只能靠肉眼翻单据,这就是我说的黑匣子。

合格的结构至少两张表:一张stock存当前余量,一张stock_flow存每一次变动。余量表保证查询快,流水表保证可追溯,两张表在同一个事务里写。

-- 库存余量:按「商品 + 仓库」维度,粒度再细就加 batch_no CREATE TABLE `stock` ( `id` BIGINT NOT NULL AUTO_INCREMENT, `goods_id` BIGINT NOT NULL, `warehouse_id` BIGINT NOT NULL COMMENT '仓库/门店ID', `available_qty` DECIMAL(14,3) NOT NULL DEFAULT 0.000 COMMENT '可用库存', `lock_qty` DECIMAL(14,3) NOT NULL DEFAULT 0.000 COMMENT '锁定库存,下单未出库', `update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_goods_wh` (`goods_id`, `warehouse_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='库存余量'; -- 库存流水:只增不改,任何一次数量变化都必须有一条 CREATE TABLE `stock_flow` ( `id` BIGINT NOT NULL AUTO_INCREMENT, `goods_id` BIGINT NOT NULL, `warehouse_id` BIGINT NOT NULL, `biz_type` VARCHAR(16) NOT NULL COMMENT 'PURCHASE_IN/SALE_OUT/CHECK_ADJUST/RETURN', `biz_no` VARCHAR(32) NOT NULL COMMENT '来源单号,用于反查', `change_qty` DECIMAL(14,3) NOT NULL COMMENT '正数入库,负数出库', `before_qty` DECIMAL(14,3) NOT NULL COMMENT '变动前余量,对账用', `after_qty` DECIMAL(14,3) NOT NULL COMMENT '变动后余量', `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), KEY `idx_goods_time` (`goods_id`, `create_time`), KEY `idx_biz_no` (`biz_no`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='库存流水';

uk_goods_wh这个唯一索引是关键,它把「同一商品同一仓库只能有一行余量」这条约束交给数据库,应用层写并发代码时少一层担心。before_qty/after_qty看着冗余,但对账时极其好用——余量表被谁改坏了,顺着流水一路加回去就能定位到具体哪一笔。biz_no加索引是为了「按单号查流水」,退货、客诉、盘点复核都靠它。

2.3 单据表怎么设计:头表 + 明细表的固定套路

采购单、销售单、盘点单,形式不同,骨架是一样的:一张头表存单号、供应商/客户、总金额、状态、制单人;一张明细表存商品、数量、单价、金额。判断一份源码质量,看它有没有把「状态」做成显式状态机,而不是靠几个布尔字段拼。

表关键字段作用
purchase_orderorder_no、supplier_id、status、total_amount采购单头,status走 DRAFT→WAIT_IN→IN_DONE
purchase_order_itemorder_id、goods_id、qty、price、amount采购明细,金额由数量乘单价算出后落库
sale_orderorder_no、status、pay_type、total_amount销售单头,status走 WAIT_OUT→OUT_DONE→SETTLED
sale_order_itemorder_id、goods_id、qty、price销售明细
stock_checkcheck_no、warehouse_id、status盘点单头
stock_check_itemcheck_id、goods_id、book_qty、real_qty账面数 vs 实盘数,差异生成调账流水

status用字符串而不是数字,是我个人的偏好,也是看源码时的一个加分项:OUT_DONE比3好排查太多,日志里一眼能看懂。代价是索引稍大,但单据表的数据量撑得住。

2.4 建库脚本怎么跑:字符集、账号、导入顺序

看完结构就该动手了。这一步翻车的人最多,原因集中在字符集。

# 1) 建库时就把字符集定死,别等建完表再改 mysql -uroot -p -e "CREATE DATABASE market DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;" # 2) 建业务账号,应用不要用 root 连库 mysql -uroot -p -e "CREATE USER 'market'@'127.0.0.1' IDENTIFIED BY 'change_me'; \ GRANT ALL PRIVILEGES ON market.* TO 'market'@'127.0.0.1'; FLUSH PRIVILEGES;" # 3) 导入时显式指定客户端字符集,否则中文直接变问号 mysql -umarket -p --default-character-set=utf8mb4 market < ./sql/schema.sql mysql -umarket -p --default-character-set=utf8mb4 market < ./sql/data.sql

三个参数各有讲究。utf8mb4不是utf8,后者在部分版本里只有三字节,存不了生僻字和表情,商品名里带个特殊符号就截断报错。--default-character-set是客户端侧参数,很多人只改了库的字符集,导入时客户端仍按默认编码解析,结果表结构是 utf8mb4、数据是乱码,症状特别像「代码有 bug」。导入顺序不能颠倒,schema.sql建表、data.sql灌初始数据(管理员账号、分类、仓库),反过来会报表不存在。

注意:如果data.sql里有SET NAMES latin1之类的语句,先删掉再导,它会把连接字符集强拉回去。

3. 把工程跑起来:依赖、配置与前端联调的最小闭环

结构看明白了,接下来是最容易劝退的一段。我不建议上来就mvn spring-boot:run,先花五分钟反推技术栈,能省掉一半的瞎试。

3.1 先反推技术栈:三个文件说明一切

# 后端:看依赖清单,spring-boot-starter-* 决定启动方式和配置文件名 grep -E "artifactId" pom.xml | head -30 # 前端:有 vite.config.js 是 Vite,有 vue.config.js 是 vue-cli,都没有大概是纯静态页 ls -1 | grep -E "vite\.config|vue\.config|webpack\.config|package\.json" # 数据库:sql 目录里的文件名顺序通常就是导入顺序 ls -1 sql/ 2>/dev/null || ls -1 *.sql

如果根目录是pom.xml,那就是 Maven 工程,配置在src/main/resources/application.yml;如果是build.gradle,配置位置一样但依赖写法不同;如果压根没有pom.xml只有package.json,那后端可能是 Node 写的,或者这是个纯前端壳子配了 mock 数据。这一步判断错,后面所有配置都白改。

顺便看一眼 JDK 版本要求,pom.xml里的<java.version>或者maven.compiler.source会写。用 JDK 8 去跑要求 17 的工程,报的是Unsupported class file major version,新手容易误以为是依赖坏了。

3.2 配置文件里必须改的四处

拿到application.yml,别急着启动,先把这四处改掉:

server: port: 8080 # ① 端口冲突时改这里,别去改代码 spring: datasource: url: jdbc:mysql://127.0.0.1:3306/market?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false&allowPublicKeyRetrieval=true username: market # ② 用 2.4 建的业务账号,不用 root password: change_me hikari: maximum-pool-size: 20 # ③ 本地调试 10~20 足够,调大反而拖慢启动 minimum-idle: 5 connection-timeout: 3000 jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: Asia/Shanghai # ④ 和 url 里的 serverTimezone 保持一致

serverTimezone=Asia/Shanghai和jackson.time-zone必须一致,一个东八区一个 UTC,页面上显示的单据时间就会差 8 小时,看起来像「刚入库的单子跑到昨天去了」。characterEncoding=utf8是 JDBC 侧参数,配合 2.4 的 utf8mb4 库使用,两边不匹配照样乱码。maximum-pool-size调成 100 是常见误用:本地机器跑一个实例,连接池开太大只会让 MySQL 端连接数早早打满,启动反而更慢。

allowPublicKeyRetrieval=true在 MySQL 8 默认认证插件下经常是必需的,不加会报Public Key Retrieval is not allowed,这个报错文本跟密码错误长得很像,容易误判。

3.3 启动顺序与初始化数据

顺序错了会报一堆「表不存在」「字段找不到」,其实只是数据没灌。

# 1) 先确认库和表都在 mysql -umarket -p -e "USE market; SHOW TABLES;" | wc -l # 2) 再起后端,前台跑方便看启动日志里的第一处异常 mvn clean package -DskipTests java -jar target/*.jar --spring.profiles.active=dev # 3) 后端起来后先探一下健康接口,别急着开前端 curl -s http://127.0.0.1:8080/actuator/health

第 2 步用-DskipTests是有意的:这类源码自带的单元测试经常依赖真实数据库或者外部服务,跑测试会先失败一轮,干扰判断。等项目跑通了再回头跑测试,那时候失败才是有意义的信号。第 3 步的/actuator/health如果没开,就换成登录接口试一次,能返回 JSON 就说明 Spring 上下文起来了、数据源连上了。

启动日志里第一处Caused by才是根因,后面几十行都是连锁反应。这个习惯我强调过很多次,但每次还是能看到有人盯着最后一行报错改半天。

3.4 前端联调:代理只改一处,别在业务代码里写死域名

前端最常见的坑是把后端地址硬编码在 axios 的baseURL里,换个端口就要全局搜替换。正确做法是走构建工具的代理:

// vite.config.js —— 前端只需要改这一处 import { defineConfig } from 'vite' export default defineConfig({ server: { port: 5173, proxy: { '/api': { target: 'http://127.0.0.1:8080', // 后端实际地址 changeOrigin: true, // 改请求头里的 Host,避免后端校验来源失败 rewrite: (path) => path.replace(/^\/api/, '') // 后端没配 context-path 时去掉前缀 } } } })

changeOrigin不设为true时,某些后端会因为 Host 不匹配直接返回 403,症状是「浏览器直接访问接口正常、前端请求 403」。rewrite这一行取决于后端有没有配server.servlet.context-path:配了/api就去掉 rewrite,没配就必须加,否则请求打到/api/goods/list而后端只认/goods/list,返回 404。

前端业务代码里统一写/api/goods/list,换环境只改代理配置,这是能少加很多班的小习惯。

4. 核心链路:入库、出库、盘点怎么做到账实一致

到这一步工程能跑了,接下来是最值钱的部分——库存扣减。这类系统出问题,九成都出在并发和幂等上,而不是页面画得好不好看。

4.1 采购入库:幂等比性能重要

采购入库的典型场景是仓管连点两下「确认入库」,或者网络超时后用户手动重试。没有幂等保护的代码会把同一批货加两次,库存直接虚高。

常见做法是用状态机做幂等,而不是靠前端按钮置灰:

@Transactional(rollbackFor = Exception.class) public void confirmIn(Long orderId) { PurchaseOrder order = orderMapper.selectById(orderId); // 状态不对直接返回,重复提交第二次就落到这里 if (order == null || !"WAIT_IN".equals(order.getStatus())) { return; } // 用带状态条件的更新抢锁,影响行数为 0 说明被别人抢先处理了 int locked = orderMapper.casStatus(orderId, "WAIT_IN", "IN_DONE"); if (locked == 0) { return; } for (PurchaseItem item : itemMapper.listByOrder(orderId)) { stockMapper.increase(item.getGoodsId(), item.getWarehouseId(), item.getQty()); stockFlowMapper.insert(StockFlow.in(item, order.getOrderNo())); } }

casStatus对应的 SQL 是UPDATE purchase_order SET status = #{to} WHERE id = #{id} AND status = #{from}。这条语句同时承担了「状态校验」和「加锁」两个职责,比SELECT ... FOR UPDATE轻,也比纯应用层判断可靠。事务边界放在方法级,单据状态和库存变动要么全成要么全败,中间挂掉不会出现「单据已完成但库存没加」。

4.2 销售出库:把判断和扣减塞进一条 SQL

销售出库是并发压力最大的环节。先查再改的写法(SELECT available_qty判断够不够,再UPDATE扣减)在两人同时下单时必然超卖,因为两次查询都能读到扣减前的值。

稳妥做法是让数据库来判:

-- 判断和扣减合成一条语句,靠 InnoDB 行锁保证原子性 UPDATE stock SET available_qty = available_qty - #{qty}, update_time = NOW() WHERE goods_id = #{goodsId} AND warehouse_id = #{warehouseId} AND available_qty >= #{qty};

调用侧只看影响行数:

@Transactional(rollbackFor = Exception.class) public void saleOut(Long orderId) { SaleOrder order = saleOrderMapper.selectById(orderId); if (order == null || !"WAIT_OUT".equals(order.getStatus())) { return; } saleOrderMapper.casStatus(orderId, "WAIT_OUT", "OUT_DONE"); // 按 goodsId 升序扣减,避免两个订单交叉持锁造成死锁 List<SaleItem> items = itemMapper.listByOrder(orderId).stream() .sorted(Comparator.comparing(SaleItem::getGoodsId)) .collect(Collectors.toList()); for (SaleItem item : items) { int rows = stockMapper.deduct(item.getGoodsId(), item.getWarehouseId(), item.getQty()); if (rows == 0) { throw new BizException("库存不足:" + item.getGoodsName()); // 抛出去整体回滚 } stockFlowMapper.insert(StockFlow.out(item, order.getOrderNo())); } }

排序这一步不是洁癖。两个订单同时扣商品 A 和 B,一个按 A→B,一个按 B→A,就会互相等对方的行锁,触发死锁检测。统一按主键升序,冲突概率基本归零。

有人会问为什么不加锁重试。我的看法是:库存不足本身就是业务结果,重试没意义;真正需要重试的是死锁异常,那个交给框架的重试机制处理更合适。

4.3 盘点调账:差异必须走流水,不能直接改余量

盘点是最容易被写坏的一环。新手做法是盘完直接把stock.available_qty改成实盘数,省事,但账就断了——半个月后有人问「这个商品上周为什么少了两件」,查不到任何记录。

正确姿势是算差异、走调账流水:

-- 盘点差异转调账:只对数量不一致的行生成流水 INSERT INTO stock_flow (goods_id, warehouse_id, biz_type, biz_no, change_qty, before_qty, after_qty) SELECT i.goods_id, c.warehouse_id, 'CHECK_ADJUST', c.check_no, i.real_qty - i.book_qty, -- 盘盈为正,盘亏为负 i.book_qty, i.real_qty FROM stock_check_item i JOIN stock_check c ON c.id = i.check_id WHERE c.id = #{checkId} AND i.real_qty <> i.book_qty;

配套的余量更新用SET available_qty = #{realQty}是可以的,因为流水里已经记了before_qty,追溯链条没断。这条 SQL 里的<>条件很重要,一致的行不生成流水,否则一次全盘盘点能往流水表里灌几万条零变动记录,后面查对账能被拖死。

4.4 退货与负库存:这条红线别越

销售退货的处理顺序是「先回库、再退款」,不是反过来。退款成功但回库失败,就会出现「钱退了货没回来」的账缺口,而且很难补。

负库存要不要允许,取决于业务形态。零售场景我一般建议关掉,用available_qty >= qty这条约束卡死;如果有「先出后进」的场景(比如生鲜日结),那就允许负库存但必须把allow_negative做成仓库级开关,别全局放开。全局放开之后,任何一次代码写错都表现为「库存变成 -9999」,而且没有任何告警。

5. 避坑与排查:五条踩过的血泪记录

这一章按「现象 → 原因 → 解决」记,都是这类源码上高频复现的问题,看到相似症状可以直接对号入座。

5.1 中文全变问号,但表结构看着没问题

现象:建表语句执行成功,data.sql导入也成功,但SELECT出来的商品名全是???,前端列表一片问号。

原因:库和表都是 utf8mb4,但导入时客户端连接用的是默认字符集,中文在传输阶段就被转成了问号,落库已经是坏数据,改表字符集也救不回来。

解决:删库重来,导入命令显式带上--default-character-set=utf8mb4。如果源码里data.sql开头有SET NAMES latin1,先注释掉。已经导坏的库没法通过ALTER TABLE CONVERT恢复,只能重导。

5.2 单据时间差 8 小时,看着像「跑到昨天去了」

现象:刚提交的采购单,列表里显示的时间比当前早 8 小时,按日期筛选还会漏单。

原因:JDBC URL 里的serverTimezone和 ORM 层时区配置不一致,或者一个配 UTC、一个配东八区,写入和读取各按各的理解转了一次。

解决:serverTimezone=Asia/Shanghai、jackson.time-zone=Asia/Shanghai、数据库服务器时区三者对齐。改完记得把已有数据里明显偏移的记录挑出来修,否则新旧数据混在一起更乱。

5.3 库存扣成负数,日志里却查不到异常

现象:某个商品余量变成-3.000,翻遍应用日志没有任何报错。

原因:扣减 SQL 里没有AND available_qty >= #{qty}这个条件,或者判断写在 Java 里而没进 SQL。并发下两次判断都通过,扣减就超了。

解决:把条件挪进UPDATE的WHERE里,用影响行数判断成败。已经为负的数据,用流水表倒推:从最新一条往前累加change_qty,找到余量第一次变负的那笔,通常是某次出库没走流水、或者入库被重复提交。

5.4 日结报表金额差几分钱,怎么算都对不上

现象:明细逐条相加和汇总表的总额差 0.01 到 0.05,商品越多差得越明显。

原因:金额字段用了FLOAT/DOUBLE,或者 Java 侧用double做累加,浮点误差在多行相加时被放大。

解决:数据库用DECIMAL(12,2),Java 侧用BigDecimal且统一setScale(2, RoundingMode.HALF_UP),加减乘之后都显式定标度。历史数据如果已经是浮点存进去的,只能重新计算并覆盖,别指望前端格式化能掩盖。

5.5 依赖拉不下来,报了一堆 artifacts 找不到

现象:mvn clean package卡在下载,最后报几个内部仓库地址拒绝连接。

原因:pom.xml里留着原作者的内网repositories,或者依赖了私有包;前端package.json里可能有指向私有源的registry。

解决:删掉pom.xml里的自定义<repositories>,回到公共源;检查.npmrc有没有残留的 registry 配置。依赖版本冲突导致的NoSuchMethodError另说,那个要看mvn dependency:tree找重复坐标,再用<exclusions>排掉旧版本。

注意:改依赖之前先备份pom.xml,这类源码的依赖树经常是「删一个崩一片」,有备份才敢动手。

6. 二次开发落点:把小票打印、权限粒度、报表口径改顺手

工程跑通、链路稳住之后,多数人要做的第一件事是「按自己门店的规矩改」。我把改动频率最高的三块列在这,附上我自己的习惯做法。

小票打印容易被想复杂。浏览器端用window.print()配一段打印样式表基本够用,58mm 热敏纸把页面宽度设成 58mm、去掉页边距、字号压到 12px 就能出票。要接 ESC/POS 指令的打印机,就得走后端拼字节流,注意中文要按打印机支持的编码(常见 GBK)转码,直接发 UTF-8 会打出乱码。我一般会先做一版浏览器打印兜底,再慢慢替换。

权限粒度的问题更常见。这类源码默认多半是「角色 → 菜单」两级,实际用起来立刻不够:店长只能看自己门店的销售,区域经理能看多店,财务能看全部但不能改单据。光加菜单权限解决不了,得在 SQL 层加数据权限。我的做法是在sale_order上加store_id,然后在 MyBatis 的拦截器里统一拼AND store_id IN (...),业务代码不感知。这样即便某个接口忘了加判断,也不会越权。

改造点常改位置注意事项
数据权限Mapper 拦截器 +store_id字段别在 Controller 里逐个判断,漏一个就是漏洞
报表口径汇总 SQL 的WHERE条件毛利算法先定:售价减进价,还是减移动加权成本
单据编号单号生成器用「前缀 + 日期 + 序列」,别用时间戳,并发下会重
打印模板前端打印样式表58mm 和 80mm 两套宽度都要留

报表口径这块我踩过坑。同一张「商品毛利表」,采购按最近进价算、销售按移动加权成本算,两个数永远对不上,业务方天天来问。后来定了个规矩:所有毛利报表统一走「销售金额 − 出库成本」,出库成本在出库那一刻按移动加权算好写进流水,报表只做汇总不做二次计算。这样口径唯一,出问题也好查——顺着流水倒推就行。

如果要加一个「库存周转天数」的指标,直接基于流水表算,别去改余量表:

-- 近 30 天日均出库量,用于估算周转天数 SELECT goods_id, SUM(-change_qty) / 30.0 AS avg_out_qty FROM stock_flow WHERE biz_type = 'SALE_OUT' AND create_time >= DATE_SUB(CURDATE(), INTERVAL 30 DAY) GROUP BY goods_id;

注意这里用了-change_qty,因为出库流水记的是负数。这个符号约定最好在表注释里写死,不然隔两个月回来看,自己都要愣一下。

从那以后我每次拿到一份新的进销存源码,都强制先走一遍「读表结构 → 跑通一条采购入库 → 查流水对不对得上」这个最小闭环,跑通了再谈改功能。跳过这一步直接改页面,最后大概率是页面好看了、账还是错的。希望帮到你。

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

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

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

立即咨询