SSM框架打造超市进销存系统:从数据库设计到库存预警实战 简介一份基于SSM的宜佰丰超市进销存管理系统毕业设计源码包主要面向计算机相关专业正在做毕设的学生以及需要项目实战练习的Java学习者也可用于课程设计或期末大作业。系统分为管理员、员工、普通用户三类角色管理员可管理用户、员工、商品分类、商品信息、商品维护、出入库记录、销售统计、留言板、订单等员工侧重商品管理和出入库、订单处理前台支持商品浏览、新闻资讯、留言反馈和购物车。整包共1391个文件、64.48MB以Java源码、JSP页面、JS/CSS前端脚本、图片素材和SQL脚本为主并附带数据库脚本、开发说明文档、答辩PPT、演示视频和代码注释。项目已严格调试可正常启动需使用MySQL 5.7导入数据库脚本并配置JDK1.8、Tomcat7运行环境。当前已有130人学习浏览适合需要直接复用或二次开发的毕业设计场景。1. 基于SSM的宜佰丰超市进销存管理系统这个项目到底在解决超市的什么痛点宜佰丰超市进销存管理系统拆开来看就是“SSMSpring SpringMVC MyBatis 超市进销存业务”。线下超市最容易翻车的地方不是卖货而是月底对账进货单记了、收银系统走了、库存表却还停留在上周的数字。这个系统的核心价值是把商品档案、供应商、进货入库、销售出库、实时库存全部串成一条数据链路让每一件商品的进出都有据可查账面库存和实物盘点能对得上。它适合三类人准备做JavaWeb课设的在校生、想系统梳理SSM框架的中级开发以及门店规模不大但Excel已经管不住库存的小老板。下面我按自己复现这类项目的完整路径把框架选型、表结构、配置、代码和踩坑一次讲透。2. 为什么选SSM而不是Spring Boot进销存系统的选型逻辑2.1 三层架构怎么各司其职Controller、Service、Dao的边界SSM给进销存系统带来的最大价值不是某个功能而是清晰的分层约束。Controller只做参数接收和结果返回不碰SQLService层包住业务规则比如进货单录入后必须同时更新库存这两步要么都成功要么都失败Dao层也就是MyBatis的Mapper只负责和数据库对话。我见过不少把SQL直接写在Controller里的项目当时觉得方便后面加一个“进货单审核”功能就要改三处这就是分层没立住的后遗症。在SSM里这三个层次的代码位置也是约定俗成的controller包放页面跳转和接口入口service包放业务逻辑mapper包放MyBatis的接口对应的XML文件放在resources/mapper下。进销存系统的核心业务——进货入库、销售出库、库存预警——都是跨表的比如销售出库要扣库存、要记销售明细、要更新商品销量如果不在Service层做事务控制任何一步失败都会让账面和实物对不上。2.2 一次进货请求在SSM里走多远从JSP页面到数据库的完整链路拿“进货入库”这个功能举例用户在前端页面填好商品、数量、进价点击提交后这条请求的完整路径是这样的JSP页面发出POST请求到DispatcherServletSpringMVC根据RequestMapping注解找到对应的Controller方法Controller把请求参数封装成PurchaseBill对象调用Service接口Service实现类里先插入进货单主表再循环插入进货明细表最后更新库存表MyBatis通过Mapper接口动态生成SQL完成数据库操作事务提交后返回结果。整个过程看起来长但每一层只干一件事出了问题也容易定位。这条链路上有四个注解是SSM项目里最高频的也是很多人面试时被问到的Controller标记控制器、Service标记业务类、Autowired做依赖注入、Transactional声明事务。Spring容器启动时会扫描这些注解把对象的创建和依赖关系接管过来这就是控制反转IoC。你不在代码里new对象而是告诉Spring“我需要一个PurchaseService”Spring会从容器里给你一个可用的实例。2.3 选SSM还是Spring Boot进销存这种业务系统怎么权衡现在很多新项目直接上Spring Boot但SSM在课设和老项目中仍然大量存在原因有三点第一SSM的XML配置是显式的数据源、事务、拦截器都能看得见对理解框架原理帮助很大第二MyBatis的SQL是手写的进销存这类查询条件多变的业务按商品名、供应商、时间段、入库单号组合查用动态SQL比JPA更方便第三很多学校和企业内部系统还在维护SSM代码能看懂SSM的人接手成本更低。如果你对比的是“基于Spring Boot Vue的商品管理系统”这类方案差别主要在两点Boot把自动配置做到极致省去了大量XML适合快速搭建Vue则把前端从JSP换成了前后端分离。但SSM这套“服务端渲染 表单直接提交”的玩法在局域网内的小型进销存场景里反而更简单——不用处理跨域不用单独部署前端一台Tomcat就能跑完。我的建议是做课设、学原理、维护老系统就选SSM从头做商业项目且不要求手写SQL才考虑Boot。进销存的核心是库存准确性和框架新老没有必然关系。3. 先把数据库设计做对进销存系统的核心表结构与建表SQL3.1 业务规则先定清楚一单一据、主明细分离、库存表怎么存进销存系统的表结构设计第一步不是画ER图而是和业务方确认三条规则。第一进货和销售都要“一单一据”一张进货单包含多个商品所以必须有主表和明细表两张表主表存单号、供应商、日期、总金额明细表存每一行商品的数量和进价。第二库存表只存当前实时库存不做历史流水历史进出记录由进货明细和销售明细去追溯。第三库存字段用DECIMAL而不是FLOAT钱和数量都不能有浮点误差。主明细分离的设计在进销存里几乎是必须的因为查“某张进货单有哪些商品”和查“某商品最近从哪些供应商进过货”是两种完全不同的查询维度。如果只建一张大宽表主单信息在每一行里重复存储修改供应商名称时就要UPDATE几十行而且数据冗余会导致统计口径混乱。主表、明细表各司其职单号作为外键关联这是进销存系统的经典设计。3.2 建表SQL落地商品、供应商、进货、销售、库存、用户全量脚本下面是我搭建同类项目时使用的MySQL建表脚本覆盖了进销存的最小闭环。你可以直接复制到Navicat或命令行执行注意表的创建顺序先建不依赖外键的基础表再建业务表和中间表。-- 商品表存放商品基础档案 CREATE TABLE product ( product_id INT PRIMARY KEY AUTO_INCREMENT COMMENT 商品ID, product_name VARCHAR(100) NOT NULL COMMENT 商品名称, barcode VARCHAR(32) DEFAULT NULL COMMENT 条码, category VARCHAR(50) DEFAULT NULL COMMENT 分类, sell_price DECIMAL(10,2) NOT NULL COMMENT 零售价, unit VARCHAR(10) DEFAULT 件 COMMENT 单位, warn_low INT DEFAULT 10 COMMENT 库存预警下限, warn_high INT DEFAULT 9999 COMMENT 库存预警上限, status TINYINT DEFAULT 1 COMMENT 1上架 0停用, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_barcode (barcode) ) COMMENT商品表;-- 供应商表 CREATE TABLE supplier ( supplier_id INT PRIMARY KEY AUTO_INCREMENT, supplier_name VARCHAR(100) NOT NULL, contact_person VARCHAR(50), phone VARCHAR(20), address VARCHAR(200), status TINYINT DEFAULT 1, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) COMMENT供应商表;-- 进货单主表 CREATE TABLE purchase_bill ( bill_id INT PRIMARY KEY AUTO_INCREMENT, bill_no VARCHAR(32) NOT NULL COMMENT 进货单号, supplier_id INT NOT NULL, total_amount DECIMAL(12,2) DEFAULT 0 COMMENT 总金额, bill_status TINYINT DEFAULT 1 COMMENT 1已入库 0作废, create_user VARCHAR(50), create_time DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_bill_no (bill_no), KEY idx_supplier (supplier_id) ) COMMENT进货单主表;-- 进货单明细表 CREATE TABLE purchase_item ( item_id INT PRIMARY KEY AUTO_INCREMENT, bill_id INT NOT NULL, product_id INT NOT NULL, quantity INT NOT NULL COMMENT 进货数量, purchase_price DECIMAL(10,2) NOT NULL COMMENT 进货单价, KEY idx_bill (bill_id), KEY idx_product (product_id) ) COMMENT进货单明细表;-- 销售单主表 CREATE TABLE sale_bill ( bill_id INT PRIMARY KEY AUTO_INCREMENT, bill_no VARCHAR(32) NOT NULL, total_amount DECIMAL(12,2) DEFAULT 0, pay_type TINYINT DEFAULT 1 COMMENT 1现金 2微信 3支付宝, create_user VARCHAR(50), create_time DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_bill_no (bill_no) ) COMMENT销售单主表;-- 销售单明细表 CREATE TABLE sale_item ( item_id INT PRIMARY KEY AUTO_INCREMENT, bill_id INT NOT NULL, product_id INT NOT NULL, quantity INT NOT NULL, sale_price DECIMAL(10,2) NOT NULL, cost_price DECIMAL(10,2) DEFAULT 0 COMMENT 进货成本用于毛利计算, KEY idx_bill (bill_id), KEY idx_product (product_id) ) COMMENT销售单明细表;-- 库存表只存当前实时库存 CREATE TABLE stock ( stock_id INT PRIMARY KEY AUTO_INCREMENT, product_id INT NOT NULL, quantity INT NOT NULL DEFAULT 0 COMMENT 当前库存, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, UNIQUE KEY uk_product (product_id) ) COMMENT库存表;-- 用户表 CREATE TABLE sys_user ( user_id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL, password VARCHAR(100) NOT NULL COMMENT 建议存MD5或BCrypt, real_name VARCHAR(50), role TINYINT DEFAULT 1 COMMENT 1管理员 2收银员 3仓管, status TINYINT DEFAULT 1 ) COMMENT系统用户表;3.3 字段设计的细节DECIMAL精度、唯一键、逻辑删除一个都不能省上面这段SQL里隐藏着几个容易忽略的细节值得逐个说明。第一金额和数量全部用DECIMAL进货价、零售价、库存数量都不允许出现“1.999999”这种浮点误差这是财务对账的底线。第二进货单号和销售单号都加了UNIQUE KEY业务上单号不允许重复如果应用层重复提交数据库会直接报错兜底。第三商品表的条码字段也加了唯一键同一条码对应一种商品这是超市扫码收银的前提。关于逻辑删除我的习惯是基础资料表商品、供应商加status字段做停用/启用而不是物理删除但进货单、销售单这类流水表一旦产生通常不允许删除只能做“作废”标记。这样设计的好处是历史数据永远可追溯对账时即使发现某张单子有问题也可以通过bill_status0把它排除在有效统计之外而不是彻底找不到这张单子。4. 在IDEA里把SSM项目跑通配置、代码与三个常用注解的落地4.1 Maven项目结构与依赖搭建SSM架构的最少文件清单从零搭建SSM项目时我习惯用Maven管理依赖Java版本用1.8Web容器用Tomcat 8.5或9。项目结构上源码放在src/main/java配置文件放在src/main/resourcesJSP页面放在src/main/webapp/WEB-INF/views下。进销存系统的页面不多但不建议把JSP直接扔在webapp根目录放在WEB-INF下可以禁止用户直接通过URL访问未登录页面。pom.xml里需要引入的核心依赖是spring-webmvcSpringMVC、spring-jdbc事务、mybatis与mybatis-springMyBatis集成、mysql-connector-java数据库驱动、jackson-databind返回JSON用、jstlJSP标签库。如果你是第一次配最容易出问题的是mybatis-spring和Spring版本的兼容性我一般用Spring 5.2.x MyBatis 3.5.x mybatis-spring 2.0.x这个组合这套搭配经过大量项目验证坑最少。4.2 三个核心配置文件数据源、Spring容器、SpringMVC各管一摊SSM的配置分散在三个文件里各管一摊。第一个是jdbc.properties只管数据库连接参数第二个是spring-mybatis.xml负责数据源、事务和MyBatis的整合第三个是spring-mvc.xml负责控制器扫描、注解驱动和视图解析。下面逐个贴出最小可用配置。# jdbc.properties jdbc.drivercom.mysql.jdbc.Driver jdbc.urljdbc:mysql://localhost:3306/yibaifeng?useUnicodetruecharacterEncodingutf-8useSSLfalseserverTimezoneAsia/Shanghai jdbc.usernameroot jdbc.password123456这四行配置里最容易被忽略的是URL上的characterEncoding和serverTimezone参数。不带characterEncodingutf-8中文商品名入库就会变成问号不带serverTimezoneMySQL 8.x会报时区错误。这两个参数是SSM连接MySQL的标配少了任何一个都是玄学问题。!-- spring-mybatis.xml -- bean iddataSource classcom.mchange.v2.c3p0.ComboPooledDataSource property namedriverClass value${jdbc.driver}/ property namejdbcUrl value${jdbc.url}/ property nameuser value${jdbc.username}/ property namepassword value${jdbc.password}/ property nameinitialPoolSize value5/ property namemaxPoolSize value20/ /bean bean idsqlSessionFactory classorg.mybatis.spring.SqlSessionFactoryBean property namedataSource refdataSource/ property namemapperLocations valueclasspath:mapper/*.xml/ property nametypeAliasesPackage valuecom.yibaifeng.pojo/ /bean bean classorg.mybatis.spring.mapper.MapperScannerConfigurer property namebasePackage valuecom.yibaifeng.mapper/ /bean bean idtransactionManager classorg.springframework.jdbc.datasource.DataSourceTransactionManager property namedataSource refdataSource/ /bean tx:annotation-driven transaction-managertransactionManager/数据源这里用c3p0连接池initialPoolSize和maxPoolSize分别是初始连接数和最大连接数。超市收银系统的并发量不会太高5到20的连接数完全够用但连接池必须配否则每次请求都新建数据库连接会拖垮Tomcat。sqlSessionFactory里的mapperLocations告诉MyBatis去哪里找SQL映射文件typeAliasesPackage让Mapper返回值可以直接写类名不用每次都写全限定名。!-- spring-mvc.xml -- mvc:annotation-driven/ mvc:default-servlet-handler/ context:component-scan base-packagecom.yibaifeng.controller/ bean classorg.springframework.web.servlet.view.InternalResourceViewResolver property nameprefix value/WEB-INF/views// property namesuffix value.jsp/ /beanspring-mvc.xml里最关键的是mvc:annotation-driven和mvc:default-servlet-handler这两行。前者启用SpringMVC的注解支持让RequestMapping、ResponseBody生效后者把静态资源CSS、JS、图片的请求交回Tomcat默认处理器否则登录页的CSS样式会被DispatcherServlet拦截导致页面裸奔这个问题在SSM项目里非常常见。4.3 进货入库的完整链路Controller、Service、Mapper各写什么配置配好之后代码实现就是顺着SSM的固定套路走。进货入库是进销存里最核心的功能我以它为例把Controller、Service、Mapper三个层次的代码完整贴出来。首先是Controller层负责接收页面表单并返回结果。Controller RequestMapping(/purchase) public class PurchaseController { Autowired private PurchaseService purchaseService; RequestMapping(/add) ResponseBody public Result add(RequestBody PurchaseBill bill) { try { purchaseService.addBill(bill); return Result.success(入库成功); } catch (Exception e) { return Result.error(入库失败 e.getMessage()); } } }Controller的代码很薄只做三件事通过Autowired拿到Service实例、调用业务方法、把结果包装成Result对象返回。RequestBody的作用是把前端传来的JSON字符串直接绑定成PurchaseBill对象前提是前端提交时设置了Content-Type为application/json并且PurchaseBill类里嵌套了List 类型的items属性否则会报“不能转换成PurchaseBill”的错误。紧接着是Service实现类这里要处理真正的业务规则保存主单、保存明细、更新库存三步必须在一个事务里完成。Service public class PurchaseServiceImpl implements PurchaseService { Autowired private PurchaseBillMapper billMapper; Autowired private PurchaseItemMapper itemMapper; Autowired private StockMapper stockMapper; Override Transactional(rollbackFor Exception.class) public void addBill(PurchaseBill bill) { // 1. 生成单号并插入主表 bill.setBillNo(generateBillNo()); bill.setBillStatus(1); billMapper.insert(bill); // 2. 循环插入明细 for (PurchaseItem item : bill.getItems()) { item.setBillId(bill.getBillId()); itemMapper.insert(item); stockMapper.increaseStock(item.getProductId(), item.getQuantity()); } // 3. 更新商品表中的最近进价 for (PurchaseItem item : bill.getItems()) { productMapper.updateLastPurchasePrice(item.getProductId(), item.getPurchasePrice()); } } }这段代码的逻辑说明Transactional(rollbackFor Exception.class)是SSM项目里最核心的注解之一它保证任何一步抛异常都会回滚所有已执行的SQL。rollbackFor指定只有Exception才回滚默认配置下运行时异常才回滚检查异常不会这里显式写出来最安全。generateBillNo()是自定义方法常见格式是“20240507 四位流水号”生成时要注意并发重复可以在SQL里用主键唯一键兜底。Mapper接口和XML文件是SSM的最后一环。这一步是MyBatis的拿手活库存变更的SQL要特别注意写法。public interface StockMapper { int increaseStock(Param(productId) Integer productId, Param(quantity) Integer quantity); int decreaseStock(Param(productId) Integer productId, Param(quantity) Integer quantity); }!-- StockMapper.xml -- update idincreaseStock UPDATE stock SET quantity quantity #{quantity} WHERE product_id #{productId} /update update iddecreaseStock UPDATE stock SET quantity quantity - #{quantity} WHERE product_id #{productId} AND quantity #{quantity} /updateincreaseStock用“quantity quantity 传入数量”的方式做原子自增不需要先查后改也就避免了并发覆盖。真正值得关注的是decreaseStock它在WHERE条件里加了一个quantity #{quantity}的约束——如果库存不够这条UPDATE会返回影响行数为0Service层就能据此判定“库存不足”并抛出异常。这是防超卖最简单有效的方式比先SELECT再比较的写法可靠得多因为SELECT和UPDATE之间可能插入其他事务而一条UPDATE本身就是原子的。4.4 销售出库的库存扣减一条带条件的UPDATE解决并发超卖销售出库对应的代码逻辑和进货入库基本对称但多了一个毛利计算销售明细表里既要存零售价也要存当时的进货成本这样月底才能算毛利。在Service层处理销售单时常见做法是先查一次商品的当前成本价填到sale_item.cost_price里再调用decreaseStock扣库存最后更新库存表。很多人在这一步会踩到“超卖”的坑高并发下两个收银台同时卖出最后一件同一商品两条decreaseStock同时执行如果没有quantity #{quantity}这个条件库存就可能变成负数。解决方式就是上面那段SQL它在数据库层面做了约束失败的那条UPDATE返回0行Service层捕获后给出“库存不足”的友好提示并把整个事务回滚保证销售单不会生成。5. 进销存系统避坑五个让库存对不上账面、页面直接报错的高频问题5.1 中文乱码商品名变成“???”页面提交全是问号现象从JSP页面提交商品名“洗发水”后数据库里存成了“???”控制台打印SQL时也能看到参数是问号。原因请求参数编码和数据传输编码不一致。Tomcat默认的POST请求编码是ISO-8859-1而页面是UTF-8同时jdbc连接串如果没带characterEncodingutf-8MySQL也会按默认字符集存数据。解决第一在web.xml里配置CharacterEncodingFilter并设置forceEncoding为true让所有请求都按UTF-8解码第二确保jdbc.url里带了characterEncodingutf-8第三数据库表本身要建成utf8mb4。这三点缺一不可只配一处往往还会零星出现乱码。5.2 Invalid bound statement (not found):Mapper接口和XML对不上现象Tomcat启动后没报错但一调用进货单查询接口就抛“Invalid bound statement (not found)”异常页面直接500。原因MyBatis在启动时没有把XML映射文件绑定到Mapper接口上。常见原因是spring-mybatis.xml里的mapperLocations路径写错比如XML实际在resources/mapper/下配置写的却是classpath:mapperXml/*.xml。解决先看控制台启动日志里有没有“Parsed 0 mapper”之类的提示如果解析到0个XML就是路径问题。检查mapperLocations的路径和XML文件实际位置是否一致同时确认Mapper接口和XML的namespace、statement id、返回类型都对得上比如接口方法名是selectBillListXML里的id就必须也是selectBillList。5.3 库存越扣越少甚至变负数超卖只靠代码判断不保险现象销售出库后库存数量偶尔对不上周末高峰期甚至出现负库存月底盘点差异很大。原因在Service层先查库存再比较、然后UPDATE的做法在低并发下没问题但收银高峰期多个请求同时读到相同库存时“检查通过”和“实际扣减”之间产生了空窗期导致超卖。解决把检查能力下沉到SQL里用UPDATE ... WHERE quantity #{quantity}的原子操作替代“先查后改”。应用层只根据影响行数判断是否扣减成功影响行数为0就抛业务异常“库存不足”。这个做法在中小超市的并发量下足够可靠同时保持了对数据库的兼容性。5.4 进货单双击提交一张单入库两次库存多出一倍现象客户反馈库存盘点时发现某商品多出一倍查进货明细发现同一单号存在两条几乎相同的数据只有创建时间差了几百毫秒。原因用户连续点击“提交”按钮两次请求都成功进入了ControllerService层没有做幂等控制两次插入走了不同的事务数据库层的UNIQUE KEY没拦住——因为应用层生成的单号两次可能不同。解决前端在提交后立即禁用按钮这是第一道防线后端在Service层根据“供应商进货日期操作人”的一致性哈希生成幂等键插入前先查是否已存在。更稳妥的做法是让单号由数据库序列或时间戳随机数生成给bill_no加UNIQUE约束重复插入时数据库直接报错迫使其回滚。5.5 页面样式全丢了登录页裸奔无CSS静态资源被DispatcherServlet拦截现象部署后访问登录页HTML结构还在但所有CSS、JS全部加载失败F12看到style.css返回403或404。原因web.xml里把DispatcherServlet映射到了“/”导致所有请求都进入SpringMVC包括CSS、JS、图片这些静态资源而SpringMVC默认没有处理静态资源的能力。解决在spring-mvc.xml里加 mvc:default-servlet-handler/ 把静态资源请求交回Tomcat默认的DefaultServlet处理同时确认JSP页面里引用静态资源的路径写的是上下文路径比如${pageContext.request.contextPath}/css/style.css。另外如果你引用了外部字体或图表库注意这些资源也要能通过同样的路径访问到否则页面在部分浏览器上会显示异常——这类问题往往比后端错误定位更费时间。6. 进阶验证给SSM进销存系统补上一套对账与预警机制6.1 用一条SQL验证“账面库存和实物盘点是否一致”进销存系统上线后最核心的验收标准是拿一个时间点的期初库存加上该时间段内的进货总数量减去销售总数量算出来的结果是否等于当前库存。下面这条SQL可以实现这个验算SELECT p.product_name, s.quantity AS stock_qty, IFNULL(pur.total_in, 0) AS total_purchase, IFNULL(sal.total_out, 0) AS total_sale, s.quantity - IFNULL(pur.total_in, 0) IFNULL(sal.total_out, 0) AS diff FROM stock s JOIN product p ON p.product_id s.product_id LEFT JOIN ( SELECT product_id, SUM(quantity) AS total_in FROM purchase_item GROUP BY product_id ) pur ON pur.product_id s.product_id LEFT JOIN ( SELECT product_id, SUM(quantity) AS total_out FROM sale_item GROUP BY product_id ) sal ON sal.product_id s.product_id;这段SQL基于一个假设初始化库存从0开始所有入库都走进货单、所有出库都走销售单。diff字段如果为0说明每张业务单据和当前库存完全吻合如果某行diff不为0就说明那张单据的库存更新逻辑有问题需要回溯业务日志。我自己的习惯是把这条SQL放在一个只读接口里盘点时直接调用省去手工跑Navicat。6.2 库存预警低于下限自动提醒补货高于上限提醒积压库存预警功能不需要多复杂的架构一个定时任务加一条查询就能做。用Spring的Scheduled注解在Service层写一个每天早晨执行的任务扫描所有低于商品表里warn_low字段的库存把预警清单生成消息通知给采购员。金额和库存阈值的设定要区别对待快消品饮料、零食下限可以放宽单价高的商品烟酒、进口食品下限要收紧避免资金被压在货架上。做这个功能时我踩过一个细节坑Scheduled默认是单线程的如果任务里不小心写了耗时很长的报表导出预警就会延迟到报表跑完才执行。解决方式是加一个线程池配置或者把预警任务独立成专门的Service和业务主链路解耦。这个思路也提醒我SSM项目里不是所有问题都要靠改代码解决对上账、优化掉慢查询、把关键SQL看得懂往往才是进销存系统稳定运行的真正命门。希望这些能帮到你让你少走我当初走过的弯路。本文还有配套的精品资源点击获取