ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

SSM+JSP百货供应链管理系统实战:表结构设计、库存扣减与事务优化

SSM+JSP百货供应链管理系统实战:表结构设计、库存扣减与事务优化 简介面向百货零售业的供应链管理系统项目基于 Java SSM 整合 Vue/JSP 技术栈构建覆盖采购、库存、销售等核心业务环节适合用作毕业设计或 Java Web 综合实战参考。压缩包共 924 个文件大小约 9.97MB主要包含 136 个 Java 后端源码、72 个 JSP 页面、224 个 JS 脚本与 102 个 CSS 样式文件另有 2 个 SQL 数据库脚本、项目功能说明文档及 Eclipse 工程配置目录结构清晰便于导入与二次开发。系统技术栈涉及 Spring、SpringMVC、MyBatis、Vue 组件化开发可帮助学习者深入理解前后端交互、SSM 整合流程及供应链模块设计。资源经过严格调试可直接部署运行并附数据库脚本和功能文档能快速搭建演示环境。已有 3522 人学习适合需要完整项目源码的学生与开发者参考。1. 拿到压缩包之后先看懂这套百货供应链的SSM骨架解压ssm685百货中心供应链管理系统jsp-lw.zip之后大概率会看到一个标准的Maven工程src/main/java下面分层放着controller、service、mappersrc/main/webapp/WEB-INF/jsp里躺着所有页面根目录还有一份带lw字样的说明文档。这套组合就是典型的SSM框架项目——Spring管对象、Spring MVC管请求分发、MyBatis管数据库读写视图层用JSP渲染是近十年Java毕业设计和中小型企业内部系统最常见的结构。百货中心的供应链管理业务上绕不开采购、入库、库存、销售、供应商这几张核心表系统要做的事就是把它们串成闭环。这篇博文不评价项目的代码质量只讲清楚一件事拿到这类SSMJSP项目后怎么快速建立数据模型和请求链路的认知然后改造成自己能跑、能讲、能扩展的状态。2. 数据先于代码供应链表结构与MyBatis映射的设计逻辑2.1 百货供应链最少需要几张表百货中心做供应链管理核心是把商品从供应商送到消费者手里中间经过仓库。最少得有四类数据供应商、商品、入库/采购、出库/销售。常见做法是再加一张库存表或者直接在商品表冗余库存字段小系统为了减少join查询通常会直接在goods表里放stock字段。CREATE TABLE supplier ( sid int(11) NOT NULL AUTO_INCREMENT, sname varchar(50) NOT NULL, contact varchar(20) DEFAULT NULL, phone varchar(20) DEFAULT NULL, address varchar(100) DEFAULT NULL, PRIMARY KEY (sid) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE goods ( gid int(11) NOT NULL AUTO_INCREMENT, gname varchar(50) NOT NULL, price decimal(10,2) NOT NULL, stock int(11) NOT NULL DEFAULT 0, sid int(11) DEFAULT NULL, category varchar(30) DEFAULT NULL, PRIMARY KEY (gid), KEY fk_supplier (sid), CONSTRAINT fk_supplier FOREIGN KEY (sid) REFERENCES supplier (sid) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;这两张表是基础采购单和销售单需要单独建表。采购单记录每次从供应商进货的商品、数量和入库时间销售单记录客户买走的商品、数量和出库时间。订单表里的外键指向goods.gid不要直接存商品名称字符串否则供应商改价或者商品改名后历史数据会出现不一致。CREATE TABLE purchase_order ( poid int(11) NOT NULL AUTO_INCREMENT, gid int(11) NOT NULL, quantity int(11) NOT NULL, unit_price decimal(10,2) NOT NULL, create_time datetime DEFAULT CURRENT_TIMESTAMP, status tinyint(1) DEFAULT 1, PRIMARY KEY (poid), KEY fk_goods (gid), CONSTRAINT fk_goods FOREIGN KEY (gid) REFERENCES goods (gid) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;MySQL 8.0以下版本推荐utf8mb4原因不复杂百货商品的描述信息里可能带上emoji或者生僻字utf8mb3也就是常说的utf8存不了四个字节的字符。设计表时要注意供应商和商品是一对多关系这个关系在后续的JSP下拉框联动中会用到。2.2 MyBatis Mapper如何对应到JSON和页面字段SSM项目里的MyBatis层通常一个GoodsMapper.xml对应一张表的CRUD。核心是写一个分页查询和库存变更的SQL这两段是供应链系统的重头戏。分页查询在MyBatis里用手写LIMIT最直接PageHelper插件虽然方便但老项目里未必引了依赖。select idselectGoodsPage resultTypecom.demo.entity.Goods SELECT g.gid, g.gname, g.price, g.stock, g.category, s.sname AS supplierName FROM goods g LEFT JOIN supplier s ON g.sid s.sid where if testgname ! null and gname ! AND g.gname LIKE CONCAT(%, #{gname}, %) /if if testcategory ! null and category ! AND g.category #{category} /if /where ORDER BY g.gid DESC LIMIT #{offset}, #{pageSize} /select#{offset}和#{pageSize}分别代表跳过的行数和每页条数前端传页码过来时Controller里做一个简单换算offset (currentPage - 1) * pageSize。这里容易踩的坑是LIMIT后面不能直接用#{}拼写列名或关键字MyBatis的预编译只处理参数值不处理表名和列名。resultType直接映射到Java实体类实体类里如果加了supplierName这个属性注意left join出来的别名必须和属性名严格一致否则数据返回null。Mapper接口和XML的绑定是另一个高频故障点对应关系的代码长这样public interface GoodsMapper { ListGoods selectGoodsPage(Param(gname) String gname, Param(category) String category, Param(offset) int offset, Param(pageSize) int pageSize); }XML里select的id必须与接口方法名完全一样namespace指向接口的全限定名这两个地方对不上启动Tomcat时MyBatis绑定异常就会直接报错。用Param注解传多个参数是通用做法比用Map更安全因为XML里写#{gname}时编译器能校验到类型。2.3 库存扣减为什么不能先查再改供应链系统的库存逻辑是并发事故重灾区。商品表里如果只有stock字段记库存的减法时不能用SELECT stock查出当前值然后在Java里算出新值再UPDATE这种思路在单人操作时没问题两个管理员同时卖同一件商品时就会超卖。正确做法是把扣减写进一条SQL的条件里UPDATE goods SET stock stock - #{quantity} WHERE gid #{gid} AND stock #{quantity}这条SQL的返回值是受影响的行数等于1说明扣减成功等于0说明库存不足或商品不存在。Service层判断这个返回值失败就抛业务异常JSP页面捕获后提示用户。这个写法在很多老项目里没有改造时建议优先补上。想要更强的保障就用SELECT ... FOR UPDATE锁行但小系统里stock #{quantity}条件配合行锁已经够用。3. JSP视图层与Controller的参数流转从表单到数据库的完整链路3.1 请求从哪里来JSP表单的name与Controller的参数绑定百货供应链系统里最常见的操作是做一张采购单。JSP页面用表单提交数据每个input标签的name属性值就是Controller方法参数的绑定依据这个对应关系是SSM里最基础也最容易忽略的规则。form action${pageContext.request.contextPath}/purchase/add methodpost input typehidden namegid value${goods.gid} / select namesupplierId c:forEach items${supplierList} varsup option value${sup.sid}${sup.sname}/option /c:forEach /select input typenumber namequantity min1 required / input typetext nameremark maxlength100 / button typesubmit提交采购单/button /form对应的Controller接收参数常见写法是用JavaBean直接接收也可以用RequestParam逐个接收。JavaBean方式更贴合这套项目的习惯因为页面字段与实体属性一一对应时Spring MVC会自动完成类型转换和注入。Controller RequestMapping(/purchase) public class PurchaseController { Autowired private PurchaseService purchaseService; PostMapping(/add) public String addPurchase(PurchaseOrder order, RedirectAttributes attr) { boolean success purchaseService.createPurchaseOrder(order); if (success) { attr.addFlashAttribute(msg, 采购单创建成功); } else { attr.addFlashAttribute(msg, 库存扣减失败请检查库存量); } return redirect:/purchase/list; } }方法签名里PurchaseOrder order这个参数对象属性名必须是gid、quantity、remark对应JSP里的name。RedirectAttributes用来跨重定向传递提示消息防止刷新页面时表单重复提交。这里的redirect:前缀非常重要——如果直接返回purchase/list地址栏还是/purchase/addF5一刷新就会重复插入一条采购单。3.2 JSP渲染数据时的El表达式与JSTL标签范围页面端循环数据一般用c:forEach配合EL表达式取值。$符号的规则是取作用域里的属性Controller返回的ModelAndView里放了什么页面就能取到什么。table border1 cellpadding8 tr th商品名称/thth采购数量/thth单价/thth创建时间/thth操作/th /tr c:forEach items${purchaseList} varpo c:if test${po.status 1} tr td${po.goodsName}/td td${po.quantity}/td td${po.unitPrice}/td tdfmt:formatDate value${po.createTime} patternyyyy-MM-dd HH:mm:ss//td tda href${pageContext.request.contextPath}/purchase/detail/${po.poid}详情/a/td /tr /c:if /c:forEach /table注意c:if的test条件里po.status 1这个写法EL表达式里数字会被当作Long类型处理JSP页面不会自动把整形转成Integer所以实体类里status字段要定义成Integer避免空指针或类型不匹配。fmt:formatDate用来格式化时间页面显示的createTime类型是java.util.Date时才能生效如果写的是LocalDateTime需要额外处理或者改成String类型再显示。JSP的搜索意图里有个高频问题修改完页面后浏览器还在显示旧内容。这套老项目通常用Tomcat部署IDE里改JSP文件后有时需要重启或清理work目录——JSP会被Tomcat编译成Java文件再编译成Class存放在CATALINA_HOME/work/Catalina/localhost/应用名/org/apache/jsp目录下改完页面不刷新时去这个目录删掉对应的编译产物即可这算是一个JSP改后不生效的排查手段。3.3 Controller返回JSON给页面做异步刷新供应链管理系统的供应商下拉框在商品页和采购页都会用到。传统做法是页面加载时用EL直接渲染supplierList数据量几百条没问题但如果有二级联动选供应商后再加载该供应商的商品就需要Controller返回JSONJSP用AJAX异步获取。Spring MVC对JSON的原生支持来自ResponseBody注解GetMapping(/supplier/goods) ResponseBody public ListGoods getGoodsBySupplier(RequestParam(sid) Integer sid) { return goodsService.getGoodsBySupplierId(sid); }只要pom.xml里引入了jackson-databind依赖返回值就会被自动序列化成JSON数组。页面端配合jQuery发起请求数据渲染到select里即可。有不少SSM项目用fastjson代替jackson但期间闹出过反序列化漏洞现阶段的建议是用jackson或者gson。JSON处理在JSP页面里还有一个高频搜索点是jsp jsonarray import这个通常指在JSP里导入Java的JSON工具类老项目里会直接% page importnet.sf.json.JSONArray %但这写法不推荐——逻辑应该写到Controller或者Service里JSP只做显示。4. 部署运行时必踩的坑Tomcat版本、JDK编译与数据一致性4.1 从zip到能访问war包能跑起来的三要素解压ssm685百货中心供应链管理系统jsp-lw.zip之后先把目录结构弄成Maven标准布局确认pom.xml里JDK版本然后二选一用IDEA打开直接跑或者用命令行Maven打包成war放进Tomcat。比较稳妥的方式是在IDEA里配置Tomcat运行这套SSM项目能跑起来的组合通常锁定JDK 8 Tomcat 8.5/9 MySQL 5.7原因在于老项目里的依赖版本大多基于JDK 8编译比如CGLIB代理和JSP编译器在新JDK上偶发不兼容。pom.xml里关键的坐标配置长这样properties maven.compiler.source1.8/maven.compiler.source maven.compiler.target1.8/maven.compiler.target project.build.sourceEncodingUTF-8/project.build.sourceEncoding /properties dependencies dependency groupIdorg.springframework/groupId artifactIdspring-context/artifactId version5.1.20.RELEASE/version /dependency dependency groupIdorg.springframework/groupId artifactIdspring-webmvc/artifactId version5.1.20.RELEASE/version /dependency dependency groupIdorg.mybatis/groupId artifactIdmybatis/artifactId version3.5.5/version /dependency dependency groupIdorg.mybatis/groupId artifactIdmybatis-spring/artifactId version2.0.5/version /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version5.1.49/version /dependency /dependenciesSpring 5.x版本要求JDK 8以上Tomcat 9对应Servlet 4.0规范而Spring MVC的DispatcherServlet在这个容器里不需要额外引javax.servlet-api的依赖容器自带。MySQL驱动选5.1.49配合MySQL 5.7最稳如果用的是MySQL 8.0驱动需要升到com.mysql.cj.jdbc.Driver并把URL改造成jdbc:mysql://localhost:3306/ssm_db?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai——少了serverTimezone参数会直接抛时区错误这是老项目迁移MySQL 8.0时最容易卡住的位置。4.2 启动报错看哪几行驱动类、端口、JDBC驱动版本运行过程中有四个高频报错按频率排每个对应的原因和处理手段都不同。第一个是ClassNotFoundException: com.mysql.jdbc.Driver字面意思是没有这个驱动类实际是mysql-connector-java依赖没打进war包或者版本太高导致类路径变化解决办法是检查WEB-INF/lib下是否有jar包。第二个是Port 8080 already in use这是端口占用。Windows下用netstat -ano | findstr 8080查出PID然后taskkill /PID 进程号 /F结束进程。Linux下用lsof -i:8080定位kill -9处理。换端口就改Tomcat的server.xml里的Connector port8080注意同时核对项目代码里有没有写死端口号比如数据库连接URL里误写端口。第三个是Public Key Retrieval is not allowed这是MySQL 8.0的驱动与数据库握手时要求获取RSA公钥但连接URL没开启对应开关。解决办法是URL末尾加allowPublicKeyRetrievaltrue。第四个是JSP编译错误页面报Unable to compile class for JSP这种一般是JDK版本太高Tomcat 9以下的容器无法用JDK 11编译JSP或者是JSP文件里用了JDK 8之后移除的类比如StringUtils的旧包路径。所以最省力的组合就是JDK 8能避开八成的兼容问题。4.3 数据一致性一个采购事务里的两张表供应链系统里一次采购操作要同时影响purchase_order和goods两张表这就涉及事务。老项目里常见的是写只更新订单不扣库存或者扣了库存但订单失败了库存改不回来。SSM的事务控制要在Service方法上加Transactional注解Service public class PurchaseServiceImpl implements PurchaseService { Autowired private PurchaseOrderMapper purchaseOrderMapper; Autowired private GoodsMapper goodsMapper; Override Transactional(rollbackFor Exception.class) public boolean createPurchaseOrder(PurchaseOrder order) { int updated goodsMapper.reduceStock(order.getGid(), order.getQuantity()); if (updated 0) { throw new BusinessException(库存不足); } return purchaseOrderMapper.insert(order) 0; } }Transactional(rollbackFor Exception.class)的语义是运行时异常和检查异常都能触发回滚。小坑在于Spring声明式事务默认只对RuntimeException回滚加异常时如果不把rollbackFor配上业务抛出的Exception子类根本不会回滚数据库里就出现订单有了、库存没减的一致性问题。另一个验证方法是看控制台有没有Creating new transaction日志没有说明事务管理器没生效检查Spring配置文件里tx:annotation-driven transaction-managertransactionManager/是否配置。事务在并发量不高的管理系统里性能损耗可控不要为了省事去掉事务。Transactional加在private方法上是无效的这点是很多JSP老项目改造时的隐含bug——注解只有加在public方法上且通过代理调用才会被拦截同类内部调用不会走代理。5. 压轴的优化手法库存扣减与页面缓存的工程化写法5.1 用乐观锁改造扣库存的最优解在2.3节的stock #{quantity}写法基础上再叠加一个乐观锁版本号字段。供应链系统的库存扣减在高并发下需要更严谨的防线version字段每次更新加1更新条件里带AND version #{version}影响行数为0则重试标准的CAS操作。ALTER TABLE goods ADD COLUMN version INT NOT NULL DEFAULT 0; UPDATE goods SET stock stock - #{quantity}, version version 1 WHERE gid #{gid} AND stock #{quantity} AND version #{version}Service层拿到更新行数为0就抛异常让用户重新提交。版本号不必暴露给前端从商品详情查出来放在表单的隐藏字段里即可。这么做能防止ABA问题——库存被两个请求读出相同的值又写回导致丢失一次扣减。反映到JSP页面商品详情按钮跳转时带上version参数采购表单把这个字段作为隐藏输入项提交这条链路就完整了。5.2 JSP页面响应速度的三个手段百货系统的商品列表页往往会随着数据量增大变得越来越卡第一个手段是JSP片段缓存商品分类导航、供应商信息这类不常变的内容用c:import或JSTL的c:cache做片段缓存注意c:cache需要引入jstl的cache标签库很多老项目没有用Apache Commons JSP Tags替代来给页面加时间缓存。第二个手段是SQL层面及时分页前端LIMIT只查当前页数据禁止查出全部List再在内存里截取。第三个手段是把商品图片、静态CSS和JS从JSP目录移到独立的静态资源路径用Nginx这类Web服务器做静态文件响应Tomcat只处理动态请求压力会下降明显。5.3 改完之后的验证清单部署优化完成后至少要验证三条主链路。先验证一条商品浏览路径点开首页搜索商品进详情看库存量是否与数据库一致。再验证一条采购链路提交采购单库存减少采购列表出现新记录供应商名称正确显示。最后验证并发扣减用JMeter或Postman并发发5个相同的采购请求库存只会扣减成功一次订单表里只有一条记录其余请求提示库存不足。验证时把日志级别调整到DEBUG重点观察MyBatis输出的SQL中UPDATE goods SET stock stock - ?的affected rows返回值结合事务日志看是否有提交回滚记录。一套SSMJSP的供应链系统掌握了请求从JSP到Controller再到Mapper再回到页面的完整回路理解了库存扣减的数据一致性问题剩下的就是在这个骨架上继续加报表、加登录权限、加供应商结算模块。每个版本迭代都从最小可运行状态开始改改一步验证一步比推翻重来更能积累可复用的技术资产。本文还有配套的精品资源点击获取
返回列表