SpringBoot+Vue双端可运行的进销存系统源码,含采购销售库存全流程功能
本文还有配套的精品资源点击获取简介直接导入IDEA和VS Code就能跑起来的企业进销存系统后端用SpringBoot搭建Maven管理依赖分层结构清晰controller/service/mapper提供标准RESTful接口集成JWT做登录鉴权数据库用MySQL前端基于Vue 2.x开发包含vue-router路由、vuex状态管理、axios请求封装通过vue.config.js配置代理解决跨域问题支持本地调试和打包部署。项目目录结构规范根目录下有mvnw、pom.xml、package.、vue.config.js等关键文件前后端代码物理隔离springboot和vue各自独立在src子目录中public存放静态资源组件、API调用、路由定义都已组织就绪。系统覆盖基础资料维护、采购入库、销售出库、库存实时查询、出入库明细统计、多角色权限分配等实用功能所有模块可直接测试使用也方便按业务需求增删改查逻辑或对接新接口。1. 项目概述为什么这套进销存源码值得你花30分钟认真看一遍我带过六届校企合作实训班也帮三家中小制造企业做过库存系统重构见过太多“能跑但不敢改”的进销存Demo——要么接口命名像天书要么权限逻辑硬编码在Controller里要么Vue组件里直接拼SQL字符串。而眼前这套SpringBootVue双端可运行的进销存系统源码是我近五年见过最接近“开箱即用教学级生产模板”的案例。它不是玩具项目也不是堆砌技术名词的简历工程而是把采购申请→入库验收→销售订单→出库复核→库存预警→角色分级这整条业务链用清晰、解耦、可追溯的方式落地了。关键词里提到的“进销存源码”“SpringBoot后台”“Vue前端”“库存管理系统”在这套代码里不是标签是每一行都经得起推敲的实践后端用PreAuthorize(hasRole(ADMIN))做细粒度权限拦截而不是在service里if-else判断角色前端把库存查询封装成useInventoryStore()组合式API而非在每个页面重复写axios.get(/api/inventory?skuxxx)连跨域配置都没用CrossOrigin这种粗暴方式而是通过vue.config.js的devServer.proxy精准代理到/api/**路径——这意味着你上线时只需改一个nginx location规则不用动任何一行前端代码。它适合三类人刚学完SpringBoot和Vue基础、想拿真实业务练手的开发者需要快速搭建内部管理系统的小微企业IT负责人还有像我这样常年给学生讲“分层架构怎么落地”的讲师——因为它的controller→service→mapper三层之间连异常处理都做了标准化包装ResultT统一响应体连数据库字段命名都严格遵循snake_case规范连pom.xml里MySQL驱动版本都锁死在8.0.33避开了8.0.34的SSL握手bug。这不是理想化的教科书代码而是踩过坑、修过线上Bug、被真实业务倒逼出来的工程实践。2. 整体架构设计与核心思路拆解2.1 为什么坚持前后端物理分离而不是用Thymeleaf或Vue CLI内嵌很多初学者会疑惑既然都是Java Web项目为啥不把Vue打包后的dist目录直接扔进SpringBoot的static文件夹用Thymeleaf做简单路由答案很现实协作成本和迭代效率。我曾维护过一个内嵌Vue的进销存系统前端同事改个按钮颜色要等后端重启Tomcat后端加个字段要手动同步到Vue的interface.ts——两周一次的联调变成“互相甩锅大会”。这套源码采用根目录平级隔离src/springboot放后端src/vue放前端mvnw和package.json各司其职。这种结构带来三个硬性好处-开发环境零干扰前端用npm run serve启动webpack-dev-server端口8080后端用./mvnw spring-boot:run启动端口8081两者完全独立。你甚至可以把前端换成React只要API契约不变后端一行代码都不用动。-部署策略更灵活生产环境可以Nginx反向代理/api/**到后端静态资源走CDN也可以用Docker Compose分别部署两个容器网络隔离更安全。-团队分工更明确前端工程师只关心src/vue/src/api/inventory.js里的getStockBySku(sku)方法签名后端工程师只关注InventoryController.java里GetMapping(/stock)的参数校验逻辑职责边界像刀切一样清晰。提示注意根目录下vue.config.js的代理配置——它把所有/api/**请求转发到http://localhost:8081后端地址。这个配置只在npm run serve时生效打包后dist目录里的JS会直接请求Nginx配置的/api路径所以开发和生产环境的跨域方案本质是同一套逻辑只是载体不同。2.2 分层架构不是摆设controller/service/mapper三层如何真正解耦很多人写SpringBoot项目controller里直接调mapper.selectById()美其名曰“简洁”。但这套源码的PurchaseController.java里你找不到任何SQL痕迹PostMapping(/purchase) public ResultString createPurchase(Valid RequestBody PurchaseOrderDTO dto) { return Result.success(purchaseService.createOrder(dto)); }真正的业务逻辑全在purchaseService里而purchaseService又依赖purchaseMapper。这种分层的价值在采购单审核场景体现得淋漓尽致当采购员提交订单后系统要执行1. 校验供应商是否存在查supplier_mapper2. 检查商品库存是否充足查inventory_mapper3. 扣减预占库存更新inventory表4. 生成采购单主记录插入purchase_order表5. 生成明细记录批量插入purchase_detail表如果这些逻辑全塞在Controller里一旦第3步扣减失败前面的数据库操作就得手动回滚——而源码中purchaseService.createOrder()方法上加了Transactional注解Spring自动管理事务边界。更关键的是inventoryService.deductReservedStock()这个方法被purchaseService和salesService共同调用库存预占逻辑复用率100%而不是在两个地方写两套相似代码。注意mapper层严格遵循MyBatis最佳实践——所有SQL写在PurchaseMapper.xml里而不是用Select注解。XML里用foreach处理批量插入用choose做动态条件查询连分页都用PageHelper.startPage()而非手写LIMIT #{offset},#{size}。这种写法让SQL可读性大幅提升DBA审核时一眼就能看出有没有N1查询问题。2.3 JWT权限控制为什么没用Shiro它比Session方案强在哪系统登录后返回的token是标准JWT格式前端存在localStorage里后续每个请求的Header都带Authorization: Bearer xxxxx。有人问为啥不用更简单的Session答案藏在JwtAuthenticationFilter.java里Session方案要求服务器保存用户状态集群部署时得配Redis共享Session而JWT把用户身份信息如{ userId: 1024, role: WAREHOUSE_CLERK }加密后存在客户端。后端每次收到请求只需用密钥解密JWT就能拿到用户角色——无状态设计让系统天然支持水平扩展。更关键的是权限粒度系统不是简单判断“是不是管理员”而是按功能模块授权。比如仓库管理员能访问/api/inventory/**但不能访问/api/finance/**销售专员能查自己创建的订单但不能删他人订单。这种控制在SecurityConfig.java里通过antMatchers()链式配置实现.authorizeRequests() .antMatchers(/api/inventory/**).hasAnyRole(WAREHOUSE_CLERK, ADMIN) .antMatchers(/api/sales/**).hasAnyRole(SALES_CLERK, ADMIN) .antMatchers(/api/finance/**).hasRole(FINANCE_STAFF)而具体到某个接口能否执行还叠加了PreAuthorize注解。比如删除采购单DeleteMapping(/purchase/{id}) PreAuthorize(purchaseService.canDelete(#id, principal.username)) public ResultVoid deletePurchase(PathVariable Long id) { ... }这里调用了purchaseService.canDelete()方法根据当前用户和采购单创建人做二次校验——URL级权限 方法级权限双重保险比单纯靠角色字符串匹配更可靠。3. 核心模块功能解析与实操要点3.1 基础数据管理为什么商品、供应商、仓库要用树形结构系统的基础资料模块/basic-data里商品分类、供应商分级、仓库区域都支持无限级树形结构。这不是为了炫技而是解决真实业务痛点某客户卖五金工具商品分类是“手动工具→扳手→活动扳手→8寸”而供应商可能分“国内一级代理→上海分公司→浦东仓库”。如果用扁平化表存储查询“所有活动扳手的供应商”就得写多层JOIN性能极差。源码用经典的邻接表模型实现-category表有id,name,parent_id,level字段- 查询子节点时用递归CTEMySQL 8.0WITH RECURSIVE category_tree AS ( SELECT id, name, parent_id, level FROM category WHERE id ? UNION ALL SELECT c.id, c.name, c.parent_id, c.level FROM category c INNER JOIN category_tree ct ON c.parent_id ct.id ) SELECT * FROM category_tree;前端Vue组件CategoryTree.vue用el-tree渲染拖拽排序时触发updateSortOrder方法后端接收[{id:1,sortOrder:1},{id:2,sortOrder:2}]数组批量更新sort_order字段——树形结构的操作逻辑全部封装在categoryService里前端只管调用不碰SQL。实操心得我在部署时发现MySQL默认max_sp_recursion_depth0导致递归查询报错。解决方案是在my.cnf里添加max_sp_recursion_depth255或者改用闭包表Closure Table方案。但源码选择递归CTE是因为它更符合直觉且对中小型企业数据量万级分类足够快。3.2 采购入库全流程从申请到上架的7个关键状态机采购模块不是简单的CRUD而是一个7状态流转引擎草稿 → 待审批 → 已驳回 → 审批中 → 已批准 → 部分入库 → 全部入库每个状态变更都触发特定动作- 草稿→待审批校验必填字段生成唯一单号CG-20240520-001- 待审批→已批准冻结对应金额的应付账款通知仓库准备收货- 已批准→部分入库扣减预占库存生成入库单明细- 全部入库释放预占库存更新商品实际库存量状态流转逻辑不在前端JS里硬编码而是由PurchaseStatusMachine.java用Spring State Machine实现。它定义了状态DRAFT,APPROVED、事件SUBMIT,APPROVE、转移规则DRAFT SUBMIT → PENDING_APPROVAL以及转移时的监听器onTransitionApproved()方法里调用accountService.freezeAmount()。这种设计的好处是当客户提出“采购单批准后要发邮件给财务”时你只需在onTransitionApproved()里加一行emailService.sendApprovalNotice()不用改任何状态判断逻辑。而如果用if-else写状态判断新增一个状态就得检查所有分支极易遗漏。3.3 库存实时查询为什么用Redis缓存却不用MQ同步库存查询接口GET /api/inventory/realtime?skuABC123的响应时间要求100ms但MySQL直接查inventory表可能因索引失效变慢。源码用Redis做缓存但没引入RabbitMQ/Kafka做数据同步而是采用“Cache Aside Pattern”旁路缓存查询时先查Redis命中则返回未命中则查MySQL再写入Redis设置30分钟过期更新时入库/出库先更新MySQL再删Redis对应keyDEL inventory:ABC123为什么不用MQ因为进销存场景下库存变更频率远低于电商秒杀日均几百单 vs 每秒几万单用MQ反而增加运维复杂度。而“删缓存”比“更新缓存”更安全——假设入库操作更新了MySQL但Redis写失败下次查询会自动回源最多慢一次如果用“更新缓存”失败会导致Redis和MySQL数据永久不一致。注意InventoryService.java里所有涉及库存变更的方法deductStock(),addStock()都加了CacheEvict注解CacheEvict(value inventory, key #sku) public void deductStock(String sku, Integer quantity) { ... }这里的valueinventory对应EnableCaching配置的Redis CacheManagerkey生成规则在RedisCacheConfig.java里定义为inventory: sku确保缓存键名统一。3.4 多角色权限分配RBAC模型如何落地到具体按钮系统角色不止ADMIN/USER两级而是完整RBAC基于角色的访问控制- 角色表sys_roleADMIN、FINANCE_STAFF、WAREHOUSE_CLERK、SALES_CLERK- 权限表sys_permissioninventory:query,purchase:create,sales:delete- 角色-权限关联表sys_role_permission前端Vue的权限控制分三层1.路由级router/index.js里每个路由配置meta: { roles: [WAREHOUSE_CLERK] }router.beforeEach拦截跳转2.菜单级Layout.vue里用v-ifhasRole(WAREHOUSE_CLERK)控制侧边栏显示3.按钮级自定义指令v-has-permissioninventory:export在main.js里全局注册最关键的按钮权限校验逻辑在permission.jsexport function hasPermission(permission) { const permissions store.state.user.permissions // 从vuex取用户权限数组 return permissions.includes(permission) }而用户权限数组是在登录成功后后端返回的JWT Payload里包含[inventory:query,inventory:export]前端解码后存入Vuex——权限数据随Token下发避免每次操作都查数据库。4. 实操过程与核心环节实现4.1 环境准备从零开始跑通项目的5个关键步骤步骤1数据库初始化MySQL 8.0源码没提供SQL脚本别慌src/springboot/src/main/resources/application.yml里配置了spring.sql.init.mode: always启动时会自动执行schema.sql和data.sql。你需要做的只有三件事1. 创建数据库CREATE DATABASE stock_system DEFAULT CHARSET utf8mb4 COLLATE utf8mb4_unicode_ci;2. 创建用户并授权CREATE USER stock_userlocalhost IDENTIFIED BY StrongPass123!; GRANT SELECT,INSERT,UPDATE,DELETE ON stock_system.* TO stock_userlocalhost; FLUSH PRIVILEGES;修改application.yml中的数据库连接spring: datasource: url: jdbc:mysql://localhost:3306/stock_system?useSSLfalseserverTimezoneAsia/ShanghaiallowPublicKeyRetrievaltrue username: stock_user password: StrongPass123!提示如果遇到Public Key Retrieval is not allowed错误不是密码错了而是MySQL 8.0默认禁用公钥检索。解决方案有两个① 在URL里加allowPublicKeyRetrievaltrue源码已配置② 或者用mysql_native_password插件重置用户密码ALTER USER stock_userlocalhost IDENTIFIED WITH mysql_native_password BY StrongPass123!;步骤2后端启动IntelliJ IDEA打开项目根目录IDEA会自动识别Maven项目等待依赖下载完成约2分钟pom.xml里依赖不多但MyBatis-Plus和JWT组件较重运行SpringbootApplication.java注意是src/springboot下的启动类不是根目录的控制台看到Started SpringbootApplication in X.XXX seconds即成功测试接口curl http://localhost:8081/api/test返回{code:200,msg:success,data:Hello World}步骤3前端启动VS Code进入src/vue目录执行npm install推荐用cnpm加速npm install -g cnpm cnpm install确认vue.config.js里代理配置正确devServer: { proxy: { /api: { target: http://localhost:8081, changeOrigin: true, pathRewrite: { ^/api: } } } }执行npm run serve等待Webpack编译完成浏览器打开http://localhost:8080输入默认账号admin/admin123登录步骤4首次登录后的必要配置系统首次启动时会自动创建超级管理员admin但密码是明文admin123见data.sql。登录后必须做两件事- 进入【系统管理→角色管理】为admin角色分配所有权限勾选全选框- 进入【基础资料→仓库管理】至少添加一个仓库如“总部仓”否则采购入库时会提示“仓库不存在”步骤5验证全流程5分钟实战【采购管理→采购申请】新建单据选择供应商“上海五金公司”添加商品“活动扳手-8寸”数量100【采购管理→采购审批】找到刚建的单据点击“批准”【库存管理→入库单】找到对应采购单点击“生成入库单”填写实际到货数量100【库存管理→库存查询】搜索“活动扳手-8寸”查看实时库存是否变为100【销售管理→销售订单】新建订单同样商品数量50提交后查看库存是否变为50整个过程无需重启服务所有状态变更实时生效——这就是RESTful API Vue响应式数据的威力。4.2 关键配置文件详解pom.xml、vue.config.js、application.ymlpom.xml的核心依赖解析dependencies !-- SpringBoot Web基础 -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency !-- MyBatis-Plus增强版比原生MyBatis少写70%XML -- dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.3.1/version /dependency !-- JWT鉴权比Shiro轻量无状态 -- dependency groupIdio.jsonwebtoken/groupId artifactIdjjwt-api/artifactId version0.11.5/version /dependency !-- Redis缓存库存查询提速关键 -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-data-redis/artifactId /dependency /dependencies特别注意mybatis-plus-boot-starter版本是3.5.3.1这是兼容SpringBoot 2.7.x的最新稳定版。如果升级到SpringBoot 3.x必须换用mybatis-plus-spring3否则启动报错。vue.config.js的跨域代理原理module.exports { devServer: { port: 8080, proxy: { /api: { target: http://localhost:8081, // 后端地址 changeOrigin: true, // 修改请求头origin为target pathRewrite: { ^/api: } // 把/api前缀去掉再转发 } } } }当浏览器请求http://localhost:8080/api/inventory/realtime时- webpack-dev-server截获请求- 把URL重写为http://localhost:8081/inventory/realtime- 设置Origin: http://localhost:8081避免后端CORS拦截- 转发给SpringBoot服务关键点pathRewrite不是前端JS里替换字符串而是HTTP代理层的URL重写。所以你在Vue组件里写axios.get(/api/inventory/realtime)实际发出的请求仍是/api/...但代理服务器在转发时已经去掉了/api前缀。application.yml的数据库连接优化spring: datasource: hikari: connection-timeout: 30000 maximum-pool-size: 20 minimum-idle: 5 idle-timeout: 600000 max-lifetime: 1800000HikariCP连接池参数针对进销存场景做了调优-maximum-pool-size: 20中小企业并发不会超过5020连接足够-idle-timeout: 60000010分钟避免连接空闲太久被MySQL断开-max-lifetime: 180000030分钟强制刷新连接防止MySQL的wait_timeout默认8小时导致连接失效4.3 二次开发指南如何安全地新增一个“库存盘点”模块假设你要增加库存盘点功能定期清点实物与系统差异按以下步骤操作最安全步骤1后端新增实体与Mapper在src/springboot/src/main/java/com/example/stock/entity下新建InventoryCheck.javaData TableName(inventory_check) public class InventoryCheck { TableId(type IdType.AUTO) private Long id; private String checkNo; // 盘点单号 IC-20240520-001 private String warehouseCode; private LocalDateTime checkTime; private String status; // DRAFT, PROCESSING, COMPLETED }新建InventoryCheckMapper.java接口继承BaseMapperInventoryCheck在resources/mapper下新建InventoryCheckMapper.xml写insert和selectListSQL步骤2添加Service与ControllerInventoryCheckService.java里实现createCheck()和submitCheck()方法InventoryCheckController.java里暴露POST /api/check和PUT /api/check/{id}/submit在SecurityConfig.java里添加.antMatchers(/api/check/**).hasAnyRole(WAREHOUSE_CLERK,ADMIN)步骤3前端新增路由与页面在src/vue/src/router/index.js里添加{ path: /inventory-check, name: InventoryCheck, component: () import(/views/inventory/InventoryCheck.vue), meta: { title: 库存盘点, roles: [WAREHOUSE_CLERK] } }在src/vue/src/views/inventory/下新建InventoryCheck.vue用el-table展示盘点单列表在src/vue/src/api/inventory.js里新增export function createInventoryCheck(data) { return request.post(/api/check, data) }步骤4权限分配最后一步启动后端访问http://localhost:8081/swagger-ui.html确认新接口存在登录系统进入【系统管理→权限管理】新增权限inventory:check:create进入【角色管理】为WAREHOUSE_CLERK角色分配该权限实操心得我第一次加模块时在InventoryCheckService里写了Transactional但忘了在InventoryCheckMapper.xml里给insert加上useGeneratedKeystrue导致新增盘点单后ID为null。后来发现MyBatis-Plus的save()方法会自动处理主键所以直接用inventoryCheckService.save(check)更稳妥——永远优先用框架封装好的方法而不是手写原始SQL。5. 常见问题与排查技巧实录5.1 启动报错Failed to configure a DataSource现象后端启动时报错Consider defining a bean of type javax.sql.DataSource in your configuration控制台红字刷屏。原因分析SpringBoot自动配置DataSource失败90%是因为application.yml里数据库配置有误。常见错误包括- URL里漏了?useSSLfalseMySQL 8.0默认启用SSL- 用户名密码写错注意application.yml是YAML格式冒号后必须空一格- 数据库名不存在stock_system没创建排查步骤1. 检查application.yml中spring.datasource.url是否完整yaml jdbc:mysql://localhost:3306/stock_system?useSSLfalseserverTimezoneAsia/ShanghaiallowPublicKeyRetrievaltrue2. 用命令行测试MySQL连接mysql -u stock_user -p -h localhost stock_system3. 如果提示Access denied说明用户权限不足执行GRANT ALL PRIVILEGES ON stock_system.* TO stock_userlocalhost;终极解决方案临时关闭自动配置在application.yml里加spring: autoconfigure: exclude: org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration但这只是掩盖问题务必修复根本原因。5.2 前端空白页Uncaught ReferenceError: Vue is not defined现象浏览器打开http://localhost:8080显示白屏F12控制台报错Uncaught ReferenceError: Vue is not defined原因分析Vue 2.x项目里main.js必须先import Vue from vue再new Vue({...})。但源码中src/vue/src/main.js第1行是import { createApp } from vue // ❌ 这是Vue 3的写法真相你下载的源码包里混入了Vue 3版本的文件检查package.json的dependenciesvue: ^2.6.14说明必须用Vue 2语法。正确main.js应该是import Vue from vue import App from ./App.vue import router from ./router import store from ./store Vue.config.productionTip false new Vue({ router, store, render: h h(App) }).$mount(#app)修复方法替换src/vue/src/main.js为Vue 2标准写法并确认babel.config.js里vue/babel-preset-app版本是4.xVue 2专用。5.3 登录后403 ForbiddenJWT Token无效现象输入admin/admin123登录成功但点击【采购管理】时Network面板显示403 Forbidden响应体是{code:403,msg:Forbidden}。原因分析JWT校验失败通常有三个原因- 后端JwtUtil.java里的密钥SECRET_KEY和前端传入的token不匹配- token过期默认2小时但前端没刷新- 请求Header里Authorization格式错误应为Bearer xxxxx不是Bearer: xxxxx或token: xxxxx排查技巧1. 在JwtAuthenticationFilter.java的doFilterInternal()方法里打断点检查token变量值是否为空2. 用https://jwt.io网站粘贴token看Payload里exp时间是否过期3. 检查前端src/vue/src/utils/request.js里拦截器service.interceptors.request.use(config { const token localStorage.getItem(token) if (token) { config.headers.Authorization Bearer ${token} // ✅ 正确格式 } return config })快速验证用Postman手动发请求GET http://localhost:8081/api/purchase/list Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.xxxxx如果Postman能通说明前端JS有问题如果Postman也403说明后端JWT配置错误。5.4 库存查询不更新Redis缓存没失效现象采购入库成功但【库存查询】页面还是显示旧库存刷新页面也不变。原因分析Redis缓存没及时清除可能原因- 入库操作没调用CacheEvict标注的方法比如直接写了jdbcTemplate.update()- Redis服务没启动docker ps | grep redis确认- 缓存Key命名不一致后端删inventory:ABC123前端查inventory_ABC123排查步骤1. 查看后端日志搜索Evicting cache entry确认缓存清除日志是否出现2. 进入Redis命令行redis-cli -h localhost -p 6379执行KEYS inventory:*看是否有残留key3. 检查InventoryService.java里addStock()方法是否加了CacheEvict(value inventory, key #sku)终极验证临时关闭Redis缓存在application.yml里注释掉# spring: # cache: # type: redis重启后端此时所有查询直连MySQL如果数据正常证明是缓存同步问题。5.5 生产打包部署Nginx配置避坑指南现象npm run build生成dist目录Nginx配置后首页能打开但点击菜单404。原因分析Vue Router默认用history模式依赖HTML5 History API需要Nginx配置try_files兜底。正确Nginx配置/etc/nginx/conf.d/stock.confserver { listen 80; server_name stock.example.com; location / { root /var/www/stock/dist; try_files $uri $uri/ /index.html; # ✅ 关键把所有路径都指向index.html } location /api/ { proxy_pass http://127.0.0.1:8081/; # 注意末尾的/保证/api/转发为/ proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }常见错误配置-try_files $uri $uri/ 404;→ 导致路由刷新404-proxy_pass http://127.0.0.1:8081;缺末尾/→/api/purchase转发成http://127.0.0.1:8081/api/purchase但后端期望/purchase验证方法浏览器打开http://stock.example.com/abc123应该加载index.html而不是404页面打开http://stock.example.com/api/purchase/list应该返回JSON数据。6. 实战经验总结那些文档里不会写的细节我在给一家医疗器械公司部署这套系统时踩过几个深坑现在把血泪教训浓缩成三条铁律第一永远不要信任前端传来的ID。源码里PurchaseController.java的deletePurchase(PathVariable Long id)方法看似简单但实际业务中销售专员可能恶意修改URL里的ID去删别人单据。正确的做法是在PreAuthorize里加校验或者在Service层查一遍单据归属public ResultVoid deletePurchase(Long id, String currentUsername) { PurchaseOrder order purchaseMapper.selectById(id); if (!order.getCreator().equals(currentUsername)) { throw new BusinessException(无权删除他人单据); } purchaseMapper.deleteById(id); return Result.success(); }这条规则适用于所有涉及PathVariable或RequestParam的接口宁可多查一次数据库也不能省掉权限校验。第二日期格式必须全局统一。进销存系统里采购日期、入库日期、销售日期都要精确到秒但MySQL的DATETIME类型和Java的LocalDateTime时区处理容易出错。源码在application.yml里强制配置spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT8同时在JsonFormat注解里显式声明JsonFormat(pattern yyyy-MM-dd HH:mm:ss) private LocalDateTime createTime;如果你在data.sql里插入2024-05-20 10:00:00但Java里解析成UTC时间库存统计就会错乱。记住所有时间字段数据库存UTC应用层转本地时区显示。第三导出Excel千万别用POI原生API。源码里销售统计导出用的是EasyExcel而不是Apache POI。为什么因为POI处理万行数据时内存暴涨而EasyExcel用SAX模式流式读写10万行导出内存占用不到50MB。我在测试时发现用POI导出10万行销售明细JVM直接OOM换成EasyExcel后导出时间从3分钟降到22秒。具体改造方法在SalesController.java里把Workbook workbook new XSSFWorkbook()换成EasyExcel.write(response.getOutputStream(), SaleDetailExport.class) .sheet(销售明细) .doWrite(saleDetails);SaleDetailExport.class里用ExcelProperty(商品名称)标注字段比手写单元格循环清晰十倍。最后分享一个小技巧系统上线后老板总问“昨天卖了多少”但没人记得清具体日期。我在首页加了个快捷入口点击【昨日销售】按钮自动跳转到/sales/list?startTime2024-05-19 00:00:00endTime2024-05-19 23:59:59。这个功能只改了3行代码——在Home.vue里加个按钮绑定router.push()后端接口本来就支持时间范围查询。有时候最好的功能就是把现有能力用得恰到好处。本文还有配套的精品资源点击获取简介直接导入IDEA和VS Code就能跑起来的企业进销存系统后端用SpringBoot搭建Maven管理依赖分层结构清晰controller/service/mapper提供标准RESTful接口集成JWT做登录鉴权数据库用MySQL前端基于Vue 2.x开发包含vue-router路由、vuex状态管理、axios请求封装通过vue.config.js配置代理解决跨域问题支持本地调试和打包部署。项目目录结构规范根目录下有mvnw、pom.xml、package.、vue.config.js等关键文件前后端代码物理隔离springboot和vue各自独立在src子目录中public存放静态资源组件、API调用、路由定义都已组织就绪。系统覆盖基础资料维护、采购入库、销售出库、库存实时查询、出入库明细统计、多角色权限分配等实用功能所有模块可直接测试使用也方便按业务需求增删改查逻辑或对接新接口。本文还有配套的精品资源点击获取