这套源码我从头到尾读过一遍,看的是 SpringBoot2 + Vue3 + MyBatis-Plus + MySQL8.0 那一套,业务背景是某轴承制造企业的进销存。刚拿到的时候我也觉得“不就是增删改查”,真正跑起来才发现,进销存的价值全在单据与库存的联动上。所以这篇内容就围绕这套 Java Web 进销存源码展开,把业务模块、表结构、核心接口和部署过程里容易被忽略的细节逐一说清楚,给打算做毕业设计、中小型管理系统开发,或者单纯想研究进销存源码的人做个参考。
进销存系统看起来是管理系统的入门课,采购单、销售单、库存表、往来账,功能谁都说得出来。但真到落地,每一环都有讲究:一张销售单保存时,库存怎么扣、流水怎么记、客户欠款怎么更新?一张采购单审核后,成本价要不要回写?所有环节串起来,才会发现“库存余额”不是简单加减,而是一堆约束条件共同作用的结果。
1. 先把业务账算清:轴承进销存到底管哪几条“线”
1.1 进销存最容易被低估的地方:不是录入,是台账一致性
很多人拿到进销存项目,第一反应是把采购、销售、库存三张表建出来,然后写一套 CRUD 接口就收工。但真实业务里,库存余额绝对不能只存在一张表里当“状态”用,因为状态可以被改、可以被覆盖,一旦出现脏数据,整个系统就失去参考价值。
这套源码给我的感觉是,它的设计重心不在页面好不好看,而在台账。所谓台账,就是一张张单据按发生顺序排列后形成的一条可追溯链条。采购入库生成入库单,销售出库生成出库单,库存数量不是被“手动改”出来的,而是被一张张单据“推”出来的。我后面会详细讲表结构怎么设计,但这里先建立概念:进销存的核心不是表单,而是单据流。
1.2 采购、销售、退货、调拨四条单据线的流转
以轴承制造企业为例,日常业务大致可以拆成四条线:
- 采购线:向供应商买轴承毛坯、外圈、内圈、滚子、保持架,到货后仓库验收入库。
- 销售线:客户下订单,销售开单,仓库出库,财务按应收账款催款。
- 退货线:采购退给供应商、客户退回我们,都涉及库存回补或扣减。
- 调拨线:厂区多个仓库之间调货,跨仓移动但不改变总库存。
这套源码在单据上做了主表加明细表的标准结构。一张采购单,主表记录单号、供应商、单据日期、经手人、审核状态、备注;明细表记录每一行商品、数量、单价、金额。为什么不能把商品直接塞在主表字段里?因为一张单可能有几十种轴承型号,每种型号数量、单价各不相同,只有拆分明细才能支撑入库、出库、退货、对账、统计这一整套后续动作。
我自己的体会是,单据设计里最容易漏的是“单据状态”。新单、已提交、已审核、已入库、已作废,如果不用状态字段管住,业务人员就可能改已经出库的单据,导致库存和账目对不上。这套源码里基本上每一张业务单据都带着审批流转字段,这也是进销存和普通接口 Demo 拉开差距的地方。
1.3 角色权限与操作留痕
进销存系统还有一个麻烦点:角色分工。销售员可以开销售单,但不能改价格;仓管员可以操作出入库,但不能看客户欠款;财务只看报表和应收应付;管理员管基础资料和用户权限。
这套源码的权限模型走的是主流的 RBAC,也就是用户、角色、权限点三层。放在前端体现为菜单和按钮显隐,后端则每个接口做权限点校验。只在前端藏菜单是不安全的,因为别人可以直接调接口,所以后端拦截器或注解必须兜底。除了权限,还有操作日志,谁在什么时间改了哪张单、原值是什么、现值是什么,一套好的进销存系统不能没有这个,否则出了账目纠纷很难追溯。
2. 技术选型:SpringBoot2 + Vue3 + MyBatis-Plus + MySQL8.0 是激进还是保守
2.1 SpringBoot2.7 不是老,是成熟
现在 SpringBoot3 已经出来很长时间了,JDK17 也是主流,为什么这套源码还在用 SpringBoot2?我自己做中小型企业项目也有类似偏好,因为 SpringBoot2.7 正好卡在“生态完善”和“技术不算旧”之间。很多第三方工具、监控组件、老版本运维脚本还在用 SpringBoot2 的规范,团队接手成本低,部署也不需要频繁升级中间件。
从源码上看,项目用的还是 Servlet 体系下的 spring-boot-starter-web,不是 WebFlux。进销存这类管理系统是典型的 IO 密集型加少量计算任务,业务逻辑大多是事务性增删改查,同步阻塞模型完全够用,异步响应式反而会让代码更绕。SpringBoot2 对应的 Java 版本通常是 JDK8,这也意味着开发机、服务器都不用装太高版本,兼容面很广。
2.2 MyBatis-Plus 解决 80% 的 CRUD,但报表还得手写 SQL
MyBatis-Plus 在这个项目里承担了几乎所有单表操作。BaseMapper 自带 insert、deleteById、selectById、updateById,配合 LambdaQueryWrapper 可以少写大量 XML。比如按条件查询库存列表,直接链式拼接条件就行。
但它不代表万能。进销存里的库存汇总、销售统计、往来账余额这类报表查询,一旦涉及多表 join 和分组聚合,MyBatis-Plus 的 QueryWrapper 反而难维护。这套源码的做法是:常规增删改查走 MyBatis-Plus,复杂统计走自定义 XML SQL。这个边界划得清楚,代码就不会变成一个堆满条件构造器的怪对象。
这里补充一句,很多人学 MyBatis-Plus 只记住了IService和ServiceImpl,但我更推荐大的业务接口自己定义方法,不硬套 IService。原因很简单,进销存的保存动作往往不是一个单表插入,而是要同时更新库存、流水、往来账,多表事务放在一个“业务 Service”里更直观。如果所有逻辑都写到继承了 ServiceImpl 的类里,后期会越写越重。
2.3 Vue3 + Element Plus 的后台组合很顺手
前端这套用的是 Vue3 组合式 API 加 Element Plus。组件式开发管理后台,体验明显比 Vue2 选项式写起来干净。页面按“列表页 + 表单弹窗 + 详情抽屉”拆分,库存列表、销售开单、报表查询都可以复用同一套表格封装。
组合式 API 对进销存这种表单密集型项目尤其友好:需要把采购单主表和明细表放在同一个页面上维护,用 ref、reactive 管理响应式数据比在 data 里堆嵌套属性更直观。Vue3 配合 Vite 的开发服务器启动速度也快,改代码热更新几乎是秒级,联调阶段会省很多时间。
2.4 MySQL8.0 的窗口函数和杂项
数据库用 MySQL8.0,最值的部分是窗口函数和更好的优化器。进销存报表里经常要算“本月环比上月”“库存排名前多少”,MySQL8.0 的ROW_NUMBER()、SUM() OVER (...)可以直接在一个 SQL 里写完。另外 MySQL8.0 默认字符集已经是 utf8mb4,存中文、生僻字、表情符号都没有问题,不需要像 MySQL5.7 那样单独改库。
事务方面,进销存对 ACID 的要求很高,MySQL8.0 的 InnoDB 行锁、外键、崩溃恢复都比较成熟。下面讲数据库设计的时候,我会专门说怎样利用唯一索引和事务约束来保证库存不超卖、不重号。
3. 数据库设计:库存流水怎样保证账实相符
3.1 基础资料表:商品、客户、供应商、仓库
基础资料是进销存的静态主数据,这套源码里包含:商品信息表、客户表、供应商表、仓库表、用户表、角色表等。
轴承这类商品有几个特殊属性:型号、规格、材质、尺寸系列,单纯放一个“商品名称”字段不够用。设计时至少要有商品编码、品名、规格型号、单位、默认采购价、默认销售价、库存上下限。商品编码一定要唯一,因为后续采购单、销售单、盘点单全部要根据编码关联对应关系,如果编码可以重复,系统越用越乱。
客户和供应商都属于“往来单位”,有些企业客户和供应商可能是同一家,但从业务严谨性出发,这套源码分成了两张表。客户表维护联系人、电话、收货地址、信用额度、应收余额;供应商表维护采购联系人、账期天数、应付余额。在界面上客户和供应商是分开管理的,这也符合财务上应收、应付分账的习惯。
需要注意的是,像“信用额度”和“应收余额”这种字段,虽然冗余在客户表里,日常查询很方便,但它应该由销售单、收款单自动累加或扣减,不能开放给手工编辑。一旦变成可以手改的字段,月底对账就会出现莫名其妙的差异。源码里这类汇总字段都放在 Service 层统一更新,DAO 层没有直接 update 的暴露,这是正确的做法。
3.2 核心单据表:主表 + 明细表到底怎么关联
单据表是进销存的骨架。采购、销售、退货、调拨四类业务,每一类基本都有一张主表加一张明细表。以销售出库为例:
- 销售主表:字段有单据编号、客户 id、单据日期、销售类型、仓库 id、经手人、审核状态、单据总金额、已收款金额、备注。
- 销售明细表:字段有销售单主表 id、商品 id、商品编码、品名快照、数量、单价、金额、税率、库存批次(如果有批次管理)。
- 流水表:字段有出入库方向、商品 id、仓库 id、关联单据类型、关联单据编号、变动数量、变动前库存、变动后库存、操作人、操作时间。
主表和明细表为什么要分开?因为一张销售单可能有几十种商品,分开后明细行数不受主表字段数限制,查询汇总也方便。更重要的是,明细表里要保留“品名快照”。哪怕以后某个商品的基础资料改名了,历史单据里的商品名和单价也不能跟着变,否则对账、审计全部失真。这一点是新人在设计进销存时常忽略的地方。
3.3 库存表与流水表的“双写”约束
库存怎么存?最常见的是库存表 design:仓库 id + 商品 id 作为联合唯一索引,一个仓库里一种商品只能有一行库存记录,字段是当前库存量、冻结库存量、安全库存量、更新时间。
这张表的数量永远是结果,它只被两张东西驱动:入库/出库单和库存流水。正确流程是:
- 插入或更新业务单据明细,单据审核后才生效。
- 在同一事务里更新库存表数量。
- 在同一事务里写一条库存流水记录。
- 库存流水记录不准修改、不准删除。
这三步必须在一个事务里,否则就出现“单据保存成功但库存没变”的严重 bug。这套源码的库存更新用了非常关键的一条 SQL:
UPDATE stock SET quantity = quantity - #{quantity} WHERE warehouse_id = #{warehouseId} AND product_id = #{productId} AND quantity >= #{quantity}重点在最后的quantity >= #{quantity}。这相当于告诉数据库:如果库存不够,不允许减。MyBatis-Plus 返回受影响行数,代码判断影响行数为 0,直接抛业务异常“库存不足”。这个方案比“先 select 库存,再用 JAVA 判断,再 update”安全得多。因为 select 和 update 之间可能存在并发,两个请求同时读到库存 5,同时卖出 5 件,后写的一方就可能把库存改成负数。直接在 update 条件里做判断,依赖数据库行锁,把并发问题挡在 SQL 执行这一层。
3.4 库存流水为什么不能省
有些初级项目只更新库存表,不写流水,表面看也能跑。但一旦用户手一抖改错单据,或者某个商品突然对不上账,没有流水就只能干瞪眼。
流水表的价值有两个:第一是追溯,每一笔出入库都能知道是由哪张采购单、销售单、盘点单产生的;第二是对账,用流水表可以分段重算某段时间的库存变化,比如月初库存加本月入库减本月出库,算出来应该等于月末库存,如果有偏差,说明某张单据被修改过或漏记了。
这套源码还把“变动前库存”“变动后库存”写进了流水,这就让数据库层面能够自查连续性和一致性。比如某条流水显示变动前 100,变动后 90,那应该有一条相邻流水或者期初库存衔接上。有了这些细节,系统才敢叫“进销存”,而不是库存记录。
3.5 报表统计表:给 MySQL 减负
进销存跑久了,单据量会很大,每次打开销售统计报表都实时 group by 几百万行明细,压力不小。这套源码的做法是:在日结或月结时生成汇总统计表。
典型的是销售日汇总表,字段包括统计日期、商品 id、销售数量、销售金额、成本金额、毛利。当天单据落库后,系统可以执行一个定时任务把当日明细聚合到汇总表,报表页直接查汇总表。这样即时查询速度很快,也方便看趋势。代价是实时性会差一点,一般后台管理系统的报表晚几分钟更新完全可以接受。
定时任务可以用 Spring 自带的@Scheduled,晚上执行统计,也可以在每次单据审核时同步更新汇总。像这种中小型进销存项目,我建议先用@Scheduled做每日汇总,简单稳定,后续量大再考虑用消息队列异步更新也不迟。
4. 后端实现:接口分层、库存扣减和报表 SQL 的实战细节
4.1 Controller、Service、Mapper 怎么分层才不磨叽
这套源码的分层很规矩:Controller 只负责接收参数、调用 Service、返回统一结构;Service 负责业务规则、事务、跨表操作;Mapper 只负责 SQL。不要在 Controller 里直接写一大堆业务判断,否则后续测试和复用都会很痛苦。
统一返回结构是用一个Result<T>类实现的,里面有 code、message、data 三个字段。所有接口返回成功就是 code=0,失败则抛全局异常,由@RestControllerAdvice统一捕获返回。这样做最大的好处是前端不用每个接口都写各自的异常处理,拦截 response 后只看 code 就行。
分页上用的是 MyBatis-Plus 的分页插件,配置一个PaginationInnerInterceptor即可。需要注意版本兼容性,MyBatis-Plus 3.5 系列和 SpringBoot2 搭配要用匹配的 mybatis-plus-boot-starter,选错版本会出现启动报错或者 SQL 解析失败。
4.2 登录鉴权和权限点校验
登录这块,源码选的是 JWT 鉴权。用户密码一般会做 BCrypt 加密,数据库里不存明文。登录成功后,服务端返回 token,前端存储在本地,并在请求头里带上Authorization: Bearer xxx。后端过滤器负责解析 token,把用户信息放到 ThreadLocal 或请求上下文中,后续业务接口通过注解或拦截器获取当前用户。
权限校验我建议分两级:第一级校验是否登录,第二级校验该角色是否有某个权限点。比如“销售开单”可能对应一个sale:add权限点,只有销售角色拥有,财务角色调用这个接口就会被拦截。权限点可以写死在注解里,也可以用字典表存储,这对进销存这类角色边界清晰的系统足够。
这里有一个经常踩的坑:修改密码、禁用用户、删除角色之后,旧 token 可能仍然有效。解决思路是在 JWT 里保存一个登录版本号或修改时间,后端每次鉴权时比对数据库里的值,不一致就强制重新登录。如果不想频繁查库,也可以把 token 有效期调短一点,配合 token 刷新机制,兼顾安全性和用户体验。
4.3 销售出库的完整事务实现
销售开单不只是插入一条销售单记录,还要扣库存、记流水、更新客户应收余额。我简化一下核心代码思路:
@Transactional(rollbackFor = Exception.class) public void createSaleOrder(SaleOrderVO orderVO) { // 1. 保存销售单主表,状态为待审核 SaleOrder order = new SaleOrder(); order.setOrderNo(generateOrderNo("SO")); order.setCustomerId(orderVO.getCustomerId()); order.setStatus(0); saleOrderMapper.insert(order); // 2. 保存销售明细 for (SaleOrderItemVO item : orderVO.getItemList()) { SaleOrderItem detail = new SaleOrderItem(); detail.setOrderId(order.getId()); detail.setProductId(item.getProductId()); detail.setQuantity(item.getQuantity()); detail.setPrice(item.getPrice()); saleOrderItemMapper.insert(detail); } }到了“审核”环节,才真正动库存:
@Transactional(rollbackFor = Exception.class) public void auditSaleOrder(Long orderId) { SaleOrder order = saleOrderMapper.selectById(orderId); // 校验状态必须为待审核 List<SaleOrderItem> items = saleOrderItemMapper.selectList( new LambdaQueryWrapper<SaleOrderItem>() .eq(SaleOrderItem::getOrderId, orderId)); for (SaleOrderItem item : items) { // 查出对应仓库商品的库存记录 Stock stock = stockMapper.selectOne( new LambdaQueryWrapper<Stock>() .eq(Stock::getWarehouseId, order.getWarehouseId()) .eq(Stock::getProductId, item.getProductId())); if (stock == null || stock.getQuantity() < item.getQuantity()) { throw new BizException("商品库存不足:" + item.getProductId()); } int rows = stockMapper.deductStock( order.getWarehouseId(), item.getProductId(), item.getQuantity()); if (rows == 0) { throw new BizException("库存扣减失败,可能库存不足:" + item.getProductId()); } // 写库存流水 stockFlowMapper.insert(buildFlow(stock, item, "销售出库", order.getOrderNo())); } order.setStatus(1); saleOrderMapper.updateById(order); }看到没有,库存扣减根本不先 select 再判断,而是直接调deductStock这种带条件的 update。这是进销存后端代码里含金量最高的一小段。只要事务在,三张表的更新要么全成,要么全败。
4.4 采购入库和采购退货的相反逻辑
采购入库逻辑和销售出库相反,是增加库存、增加应付余额、写正向流水。采购退货则是减少库存、冲减应付。方向反了没关系,但有一个细节:采购入库时要考虑成本价格是否回写商品资料。
轴承采购价波动不算小,同一种商品可能这次 8 元,下次 8.5 元。如果不做批次核算,财务会很难受。完整解决方案是把库存细分到批次,比如每批入库生成一个批次号,销售出库时按先进先出法选批次并出成本。但批次管理对系统复杂度是成倍增加的。如果只是中小型进销存,可以采用“移动加权平均”法:每入一笔货,更新商品的成本均价。这套源码在这个位置做了一个可配置项,普通企业选移动加权平均就够,不必强行上批次。
4.5 报表查询 SQL:能一次查完就不要循环查
进销存报表最坏的习惯是循环查数据库。比如统计客户销售额,有人会在 Java 里遍历客户列表,每个客户再执行一次 count 或 sum,客户一多数据库直接就卡。正确写法是一条分组 SQL:
SELECT c.customer_name, SUM(d.amount) AS total_amount, COUNT(DISTINCT h.id) AS order_count FROM sale_order h INNER JOIN sale_order_item d ON d.order_id = h.id INNER JOIN customer c ON c.id = h.customer_id WHERE h.order_date BETWEEN #{startDate} AND #{endDate} AND h.status = 1 GROUP BY c.id, c.customer_name ORDER BY total_amount DESC要注意给 h.order_date 和 h.status 建联合索引,否则报表数据量大时查询会走全表扫描。我自己见过不少项目前期数据量小很流畅,上线几个月报表就开始慢,最后看执行计划十有八九是索引没建到位。
4.6 事务边界:不要把查询塞进事务里
刚才核心逻辑都加了@Transactional,但有一种情况要小心:如果你在事务里调用了远程接口、导出 Excel、发了通知短信,这些操作会拖长事务时间,甚至因为外部依赖导致数据库连接长时间被占用。事务里只应该放和数据库强相关、必须保证原子性的操作。导出 Excel 这种耗时操作应该先查出数据、释放事务,再生成文件。
另外,事务不要随便加在 Controller 层方法上。Controller 层是请求入口,一个接口可能只调一个 Service 方法,但如果你直接在 Controller 上开事务,所有子调用都挤在大事务里,后续不好细化回滚粒度。正确做法是把事务加在 Service 实现方法上,并且是具体负责跨表更新的方法上。
5. 前端实现:Vue3 页面路由、表单联动和权限按钮
5.1 管理后台的目录与路由设计
前端项目结构看过之后挺清楚的:页面放在 src/views 下面,按业务模块建目录,比如 procurement、sales、inventory、report。公共组件放在 src/components,接口请求统一放在 src/api,状态管理用的是 Pinia。
路由有两种组织方式:一种是全部静态路由,适合模块少、页面固定的系统;另一种是动态路由,登录后根据角色返回菜单,再动态注册。这套源码采用的是动态路由加静态路由结合。像登录页、404 页是静态路由,业务页面放到动态加载,需要权限判断的角色菜单由后端下发。
动态路由有个需要注意的坑:刷新页面时路由可能会丢失。因为 token 还在,但 Vue Router 里的动态路由是登录后 addRoute 进去的,刷新后内存清空,页面找不到组件。解决办法是在应用初始化时,如果检测到本地有 token,就重新拉取用户信息和菜单,再 addRoute。这个逻辑必须赶在路由跳转之前执行完,否则一刷新就跳到 404。
5.2 路由守卫里不要只校验 token
路由守卫是前端权限控制的第一道闸:每次跳转前判断有没有 token,没有就跳登录页。有 token 但用户信息为空,就拉一次用户信息。这看起来简单,但我见过很多项目只判断“有没有 token”,结果用户被强制退出登录后重新拿了一个别人账号的 token,也照样能进系统。
更稳妥的做法是在路由守卫里校验当前用户是否有某个角色或权限码。既然后端动态菜单是根据角色生成的,校验菜单其实就是校验角色。按钮级别的权限,比如“审核”按钮只有仓管员可见,可以在组件里封装一个v-permission指令,传入权限码,没有权限就在绑定阶段把按钮移除。
但请记住,前端的按钮隐藏只是用户体验,不是安全措施。只要接口没做权限校验,绕过前端照样可以提交请求,所以前后端权限校验必须同时存在,谁也不能替代谁。
5.3 销售开单页面:选商品、带价格、算金额
进销存前端最核心的交互是销售开单和采购开单。页面通常是左侧一个商品选择弹窗,右侧一个明细表格,用户逐行添加商品、修改数量、查看金额。
商品选择弹窗的基本逻辑是:
- 输入商品编码或名称关键字,调后端搜索接口。
- 接口返回商品列表,包含商品编码、品名、规格、单位、销售价、当前库存。
- 用户点击某一行,前端判断当前明细表格里有没有这个商品。
- 如果已有,则数量加一;如果没有,则往明细数组 push 一行。
在选择商品后,默认带出销售价,这个价格优先取商品基础资料里的销售价,但允许销售人员手工改。每行价格、数量变化时,前端实时计算该行金额和总金额。这里我建议用计算属性而不是在模板里塞复杂表达式。否则明细一多,页面渲染很容易卡。
数量字段还要做个交互细节:用户输入数量后,前端要和当前库存做一次轻校验,库存不足直接标红提示,不必等提交后端才报错。但前端校验不能阻止用户提交,因为有的客户允许预留销售,让仓管员后续分配实际库存。
5.4 表格分页、搜索条件和导出 Excel
列表页基本模式是:顶部搜索区,中间表格,底部翻页。搜索条件常见的有日期范围、商品分类、客户名称、单据编号。这块的实现套路很固定:前端维护一个 searchForm 对象,查询按钮把对象传给后端;翻页时也把对象带上,保证换页不会丢条件。
导出 Excel 是进销存项目的高频需求。常见做法是后端生成文件流,前端用 blob 下载。后端用 EasyExcel 或 POI 都行。这里有个容易踩的坑:导出接口的查询条件要和列表接口保持一致,否则用户明明筛选了某个时间范围,导出却把全量数据倒出来。比较稳妥的方案是导出接口接收和列表一模一样的查询条件对象,后台复用同一个查询 Service。
另外,导出不要放在查询接口里,让前端用“查询”和“导出”两个按钮分别触发。原因是导出通常很慢,如果和普通查询混在一起,接口超时可能会拖垮前端页面请求。前后端配合时可以在导出请求前弹一个“正在导出”遮罩,完成后下载文件,体验会好很多。
6. 部署、联调与排错:源码拿回来从哪看起
6.1 环境版本清单:先统一再动手
拿到这套源码,第一件事不是打开 IDE 直接跑,而是先确认本地环境版本是不是和源码匹配。根据这套系统的技术栈,我整理了一个比较稳妥的版本组合:
| 组件 | 推荐版本 | 说明 |
|---|---|---|
| JDK | 8 或 8+ | SpringBoot2 系列常用 JDK8,部分兼容 JDK11 |
| Maven | 3.6 及以上 | 依赖管理,版本太高一般也没问题 |
| MySQL | 8.0.x | 注意字符集选 utf8mb4 |
| Node.js | 16 或 18 | 对应 Vite 4 / Vue3 比较稳妥 |
| npm/yarn/pnpm | 与 Node 匹配 | 团队习惯,pnpm 安装速度快 |
这些版本只要不是偏差太大,通常都能跑。最容易出的问题是 Node 版本过高,比如 Node20 搭配某些老版本 Vite 会报错;MySQL 版本如果是 5.7,有些 SQL 语法比如窗口函数就可能有兼容问题。所以先看文档里的版本说明,再决定要不要升级或降级。
6.2 数据库初始化和后端启动步骤
后端启动前要把 application.yml 里的数据库连接改成本地地址,比如jdbc:mysql://localhost:3306/xxx?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai。MySQL8 的驱动类名是com.mysql.cj.jdbc.Driver,如果项目里还是老驱动com.mysql.jdbc.Driver,启动时会有警告。
初始化数据库时,源码一般带了一个 xxx.sql 或 init.sql 脚本,包括建库建表语句、初始菜单、初始角色、初始管理员账号密码。执行完脚本后,记得检查 admin. 用户状态是否正常,密码是 BCrypt 加密的。如果原项目数据库里已经有测试业务数据,建议先清空或复制一份到测试库里,避免后面联调时数据干扰。
启动后端后先不要急着开前端,先用接口测试工具或浏览器直接访问一下健康检查接口。确认后端能通,再启动前端开发服务器。否则前后端同时有问题,排查起来会互相干扰。
6.3 联调阶段最常见的四类问题
联调时我总结下来,大多数问题集中在四个方面。
第一个是跨域。前端端口在 5173,后端在 8080,直接请求必然跨域。解决方式是在后端加 CORS 配置,或者前端开发服务器配置代理。个人更推荐后端统一加跨域过滤器,生产环境前后端可能不同域名,用一个配置项解决。
第二个是时间字段显示不对。后端接口返回日期时间,前端用new Date()解析,很容易出现时区偏移 8 小时问题。解决办法是后端统一返回格式化字符串如yyyy-MM-dd HH:mm:ss,或者前端封装一个日期解析函数,全局处理时间类型。
第三个是金额精度。价格、金额一定要用 BigDecimal,不能直接存 double。如果数据库中某张表的金额字段使用的是 decimal(10, 2),但 Java 类里却是 Double,查询结果在某些情况下可能出现精度损失或四舍五入不一致。建议全局搜索 Double 类型,涉及金额全部替换成 BigDecimal。
第四个是分页参数。MyBatis-Plus 分页需要先配置分页插件,否则Page只会返回全部数据,只有配置了拦截器,总数和页数才正确。另外前端传 pageNum 是第几页,pageSize 是每页数量,后端的Page<T>构造参数是当前页和每页条数,含义要对上,否则第一页数据会变成第二页。
6.4 文档怎么读才不算浪费
这套源码带了一份项目文档,如果一上来就从头到尾通读,很容易看到一半就没兴趣。我的建议是倒着读,或者按问题驱动读。
第一步先看“环境搭建”或“快速开始”章节,把项目跑起来。第二步看数据库设计文档中的 ER 图和数据字典,重点关注库存表、流水表、单据主表和明细表。第三步看接口文档中的销售和采购相关接口,对照后端代码理解关键逻辑。最后再看前端页面和菜单配置,理解整个系统的用户操作路径。
遇到看不懂的名词,比如“期初库存”“移动加权平均”“应收核销”,不要跳过,这些往往是进销存业务的关键概念。建议先查一下通用含义,再回到代码里看对应字段,这样理解会快很多。文档里没有覆盖的部分,多翻数据库的表注释和代码注释,这套源码的表注释和字段注释比较全,比单独的 Word 文档更贴近实际代码。
个人在实际操作中的体会是,进销存项目最值得反复研究的就是那张“销售审核+库存扣减+流水记录”的事务链路。把它弄懂,相当于拿到了所有单据类功能的通用钥匙。如果你也想在这套源码上扩展新功能,比如增加多仓调拨、批次保质期管理、供应商对账,建议先在表设计上规划好流水字段和状态字段,再考虑接口。最后再分享一个小技巧:单据编号最好不要依赖数据库自增 id,而是用日期加序列生成,比如SO20250526001,这样用户看图说话、售后查单都方便得多。