十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

SSM框架甜品店管理系统:从分层架构到并发扣库存实战

SSM框架甜品店管理系统:从分层架构到并发扣库存实战 简介一套基于SSM框架Spring、SpringMVC、MyBatis和MySQL的甜品饮品店/蛋糕店前后台管理系统主要面向计算机相关专业毕设学生及Java Web学习者用于快速搭建包含商品展示、购物车、订单处理和个人中心等模块的完整商城项目。资源包内共包含238个文件压缩包体积约69.29MB主要有JSP页面、Java类、XML配置、SQL脚本、图片样式等也包含class编译文件和jar依赖库结构清晰方便导入Eclipse或IDEA运行调试。该系统已有1419人学习下载前台功能包括用户注册登录、首页热销与新品推荐、商品分类、购物车、订单、个人中心后台包括订单管理、客户管理、商品管理、类目管理、修改密码覆盖甜品店运营核心流程。系统经严格调试Eclipse与IDEA均可运行界面简洁、功能划分清楚便于二次开发和扩展适合作为毕业设计、课程设计或SSM整合实战的参考项目。1. 一套甜品店管理系统SSM 框架到底在管什么做课程设计或毕业设计时「基于 SSM 的 XX 管理系统」是最常见的选题但很多人把 Spring、Spring MVC、MyBatis 三个框架拼起来后只会写 CRUD说不清请求是怎么从页面走到数据库的。甜品饮品店蛋糕店管理系统恰好是一个典型的进销存加订单场景商品有分类、有规格订单要拆明细库存要实时扣减会员要累计消费。它比单纯的学生管理系统多了一层事务和并发约束能把 SSM 框架的分层价值完整带出来。这套系统的落地路径很清晰MySQL 设计好表MyBatis 管 SQLSpring 管事务和对象装配Spring MVC 暴露 REST 接口前端用简单的 JSP 加 jQuery 或静态页面接手。源码加数据库脚本一起交付意味着拿到的不是半成品而是可以直接导入 IDEA、改配置、启动 Tomcat 就能跑的完整工程。下面按「架构分层 → 数据库设计 → 订单核心逻辑 → 部署排错 → 并发进阶」的顺序把这个项目从头到脚拆一遍。每一个环节都给出可以抄作业的代码和参数同时说明边界在哪、坑在哪。2. SSM 框架下甜品店系统的包结构与请求流转路径2.1 三个框架的分工不是各管一层而是管一段链路SSM 不是三个独立的工具它们合作完成一次 HTTP 请求的完整生命周期。Spring MVC 负责接收请求、解析参数、返回视图或 JSONSpring 本体负责管理 Controller、Service、Mapper 这些 Bean 的创建和注入同时在 Service 方法上开启数据库事务MyBatis 作为持久层框架把接口方法和 XML 里的 SQL 绑定起来处理结果集到 Java 对象的映射。一个三层的包结构大致是这个样子com.sweet.shop ├── controller # 接收 HTTP 请求参数校验返回 JSON │ ├── ProductController.java │ ├── OrderController.java │ └── UserController.java ├── service # 业务逻辑事务边界都在这一层 │ ├── OrderService.java │ └── impl/OrderServiceImpl.java ├── mapper # MyBatis 接口一个方法对应一条 SQL │ ├── ProductMapper.java │ ├── OrderMapper.java │ └── StockMapper.java ├── pojo # 实体和 VO/DTO │ ├── Product.java │ ├── Order.java │ └── OrderVO.java └── config # Spring 和 MyBatis 的 Java 配置或 XML请求流转的顺序是Tomcat 把请求交给 DispatcherServletHandlerMapping 找到对应的 Controller 方法Controller 调用 ServiceService 内部可能调用多个 MapperMapper 映射到 XML 里的 SQL 操作数据库。返回值从内到外再反向传回去序列化成 JSON 响应给前端。理解这一条链路的价值在于排查问题时有方向感。页面报 404先看 HandlerMapping 有没有扫到 Controller接口报 500 但 SQL 单独拿出来能跑去查 MyBatis 参数绑定数据没写入但没报错去看事务有没有生效。分层是手段链路是你定位问题的地图。2.2 Controller 层的一个完整示例商品列表带分类过滤以甜品店最常见的「按分类查商品」为例Controller 的代码可以这样写RestController RequestMapping(/api/product) public class ProductController { Autowired private ProductService productService; GetMapping(/list) public Result list(RequestParam(required false) Integer categoryId, RequestParam(defaultValue 1) Integer pageNum, RequestParam(defaultValue 10) Integer pageSize) { PageProductVO page productService.pageQuery(categoryId, pageNum, pageSize); return Result.success(page); } }这段代码的关键点有三个。RestController直接返回 JSON省掉每个方法写ResponseBodycategoryId是可选的传了就按分类过滤不传就全量分页参数给了默认值避免前端漏传导致 SQL 异常。Service 层在这里被抽象成接口Controller 只依赖接口不依赖实现这是 Spring 依赖注入最基础也最常见的用法。2.3 Service 层是事务的边界不是 CRUD 的堆叠很多 SSM 项目的通病是 Service 层只做了 Mapper 的转发这等于把事务边界推到了 Controller。SSM 的事务是通过 AOP 代理实现的Transactional加在 Service 实现类的方法上Spring 会生成一个代理对象在进入方法前开启事务方法正常结束提交抛出 RuntimeException 则回滚。理解 AOP 代理机制比记住注解本身更重要。自调用不会经过代理对象比如 OrderServiceImpl 里的方法 A 内部调用同类的方法 BB 上的Transactional不生效。排查事务失效时先确认是不是同一个类内部调用再确认异常是不是被 try-catch 吞掉了最后看 Spring 配置里有没有开启tx:annotation-driven或EnableTransactionManagement。Service public class OrderServiceImpl implements OrderService { Autowired private OrderMapper orderMapper; Override Transactional(rollbackFor Exception.class) public void createOrder(Order order, ListOrderItem items) { orderMapper.insert(order); for (OrderItem item : items) { orderMapper.insertItem(item); } } }rollbackFor Exception.class表示所有异常都触发回滚不写这个参数的话默认只在 RuntimeException 时回滚受检异常会被静默提交这是订单数据不完整最常见的暗坑。2.4 用注解配置还是 XML 配置SSM 发展到后期主流做法是完全注解化。Configuration类替代 Spring XMLMapperScan扫描 Mapper 接口SqlSessionFactoryBean通过配置类创建。MyBatis 的 SQL 本身仍然写在 XML 里因为复杂查询、动态 SQL 的结果映射在 XML 中维护成本更低。如果你是照着源码工程导入项目的很容易看到 web.xml、spring-mvc.xml、mybatis-config.xml 等文件。老项目用 XML 配置是为了兼容性和教学演示新写的代码建议切到 Java Config。两者的效果完全等价切换的核心就两个Configuration类替代 XML 根节点MapperScan替代手动注册每个 Mapper Bean。3. 甜品店管理系统的数据库建模订单、库存与商品规格3.1 先梳理业务边界再画表甜品店和普通电商的区别在于商品模型更灵活。一杯奶茶可以选择大小杯、温度、甜度一块蛋糕可以按尺寸售卖。直接用 product 表加一堆冗余字段会非常痛苦更合理的做法是拆出t_product_spec表把规格变体独立出来商品的公共属性留在t_product。整个系统的核心表大致是用户表、分类表、商品表、商品规格表、订单主表、订单明细表、库存流水表。订单主表和明细表是典型的父子结构主表存总金额、状态、下单时间明细表存每个商品的数量、单价、规格快照。库存流水表用来记录每次出库入库的操作痕迹是排查库存异常的重要依据。3.2 核心建表 SQL下面是去掉冗余字段后的核心表结构足够支撑一个甜品店的完整业务流程CREATE TABLE t_product ( id INT NOT NULL AUTO_INCREMENT, category_id INT NOT NULL COMMENT 分类ID, name VARCHAR(64) NOT NULL COMMENT 商品名称, main_image VARCHAR(255) DEFAULT NULL COMMENT 主图URL, status TINYINT NOT NULL DEFAULT 1 COMMENT 1上架 0下架, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_category (category_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT商品表; CREATE TABLE t_product_spec ( id INT NOT NULL AUTO_INCREMENT, product_id INT NOT NULL COMMENT 所属商品ID, spec_name VARCHAR(64) NOT NULL COMMENT 规格描述如大杯/中杯, price DECIMAL(10,2) NOT NULL COMMENT 规格售价, stock INT NOT NULL DEFAULT 0 COMMENT 当前库存, version INT NOT NULL DEFAULT 0 COMMENT 乐观锁版本号, PRIMARY KEY (id), KEY idx_product (product_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT商品规格表; CREATE TABLE t_orders ( id INT NOT NULL AUTO_INCREMENT, order_no VARCHAR(32) NOT NULL COMMENT 订单号, user_id INT NOT NULL, total_amount DECIMAL(10,2) NOT NULL COMMENT 订单总金额, status TINYINT NOT NULL DEFAULT 0 COMMENT 0待支付 1已支付 2已完成 3已取消, pay_time DATETIME DEFAULT NULL, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_order_no (order_no), KEY idx_user (user_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT订单主表; CREATE TABLE t_order_item ( id INT NOT NULL AUTO_INCREMENT, order_id INT NOT NULL, product_id INT NOT NULL, spec_id INT NOT NULL, product_name VARCHAR(64) NOT NULL COMMENT 商品名称快照, spec_name VARCHAR(64) NOT NULL COMMENT 规格快照, price DECIMAL(10,2) NOT NULL COMMENT 成交单价, quantity INT NOT NULL COMMENT 购买数量, PRIMARY KEY (id), KEY idx_order (order_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT订单明细表;关于字段设计有几点值得说明。金额一律用 DECIMAL(10,2)double 的二进制浮点在累计计算时会产生精度误差这在「数据库课程设计」答辩时是最容易被追问的点。订单明细里冗余了 product_name 和 spec_name这属于快照设计商品改名或下架后历史订单依然能还原当时的购买信息。库存放在规格表上因为奶茶的库存是按「大杯」「中杯」这种粒度的物理库存来管理的不是按商品维度。3.3 订单状态机与索引设计订单状态字段虽然只有 TINYINT但它的流转逻辑应该清晰待支付可以取消已支付做退款后回到已取消已完成后不可再改动。如果源码里没有做状态校验至少应该在 Service 层补上状态判断而不是让 SQL 直接 UPDATE 任意状态。索引设计上uk_order_no唯一索引保证订单号不重复idx_user支撑「我的订单」列表的查询。t_order_item的idx_order让订单详情查明细时命中索引。对于这种管理系统的数据量级两三个索引就够用了不需要过度设计。3.4 订单号生成策略不要用自增 ID 对外暴露自增 ID 适合做内部主键但不适合直接给用户看。原因有二一是暴露了平台的单量二是容易被遍历爬取。常见的做法是yyyyMMddHHmmss 用户ID后四位 随机数长度控制在 32 位内。生成放在 Service 层而不是 Mapper层避免数据库函数在不同版本间的兼容性差异。public String genOrderNo(Long userId) { String time LocalDateTime.now() .format(DateTimeFormatter.ofPattern(yyyyMMddHHmmss)); String uid String.format(%04d, userId % 10000); int random (int) ((Math.random() * 9 1) * 1000); return time uid random; }这个方案在单机场景下足够安全并发量上来后有两个隐患同一秒内同一个用户的下单随机数可能撞上以及时间戳回拨会导致订单号比之前的小。管理系统的流量基本不会触发这两个问题但你要知道边界在哪。真到了高并发场景应该用雪花算法或数据库序列表。4. 下单与扣库存的事务实现从 Controller 到 Mapper 的完整链路4.1 下单接口的代码骨架下单是这个系统里最核心的接口它同时涉及商品校验、金额计算、库存扣减、订单落地四件事。先看 Controller 和 Service 的完整实现PostMapping(/create) public Result create(RequestBody OrderCreateRequest req) { if (req.getUserId() null || req.getItems() null || req.getItems().isEmpty()) { return Result.error(参数不合法); } return Result.success(orderService.createOrder(req)); }Override Transactional(rollbackFor Exception.class) public OrderVO createOrder(OrderCreateRequest req) { BigDecimal total BigDecimal.ZERO; ListOrderItem itemList new ArrayList(); for (OrderCreateRequest.Item item : req.getItems()) { ProductSpec spec productSpecMapper.selectById(item.getSpecId()); if (spec null || spec.getStock() item.getQuantity()) { throw new BusinessException(库存不足: item.getSpecId()); } BigDecimal itemAmount spec.getPrice().multiply(BigDecimal.valueOf(item.getQuantity())); total total.add(itemAmount); OrderItem orderItem new OrderItem(); orderItem.setProductId(spec.getProductId()); orderItem.setSpecId(spec.getId()); orderItem.setPrice(spec.getPrice()); orderItem.setQuantity(item.getQuantity()); itemList.add(orderItem); } Order order new Order(); order.setOrderNo(genOrderNo(req.getUserId())); order.setUserId(req.getUserId()); order.setTotalAmount(total); order.setStatus(0); orderMapper.insert(order); for (OrderItem item : itemList) { item.setOrderId(order.getId()); orderItemMapper.insert(item); } return orderVO; }这段逻辑的关键不是业务繁复而是边界处理。每次校验库存前先查ProductSpec通过spec null判断规格是否存在再用stock quantity判断库存。金额计算用 BigDecimal 的 multiply绝不能用 int 乘以 double 再强转。落库顺序是先主表后明细保证外键关系完整。整个方法被Transactional修饰任何一步抛出异常主表和明细都不会留下半截数据。4.2 库存扣减为什么不能先查再改上面的代码有一个隐患先查库存再在内存里判断最后执行 UPDATE。如果两个请求同时读到库存为 5都判断库存充足然后各自扣减最终库存会变成负数。这就是经典的超卖问题。正确的做法是把判断和扣减合并成一条原子 SQLupdate iddeductStock UPDATE t_product_spec SET stock stock - #{quantity} WHERE id #{specId} AND stock #{quantity} /updateint rows productSpecMapper.deductStock(specId, quantity); if (rows 0) { throw new BusinessException(库存不足); }stock #{quantity}放在 WHERE 里数据库行锁保证同一时刻只有一个事务能成功更新这一行受影响行数为 0 就说明库存不够。这段逻辑替换掉上面代码里的「先查再改」后超卖问题才能从根上解决。4.3 事务回滚与并发控制的常见误区事务不回滚是这类系统最常见的问题。第一种情况是异常被吞了Service 里 catch 住后返回 false框架看方法正常结束就提交了这比抛异常更危险因为调用方完全感知不到。第二种情况是事务只加在 Mapper 上导致一个订单的多次写操作分散在多个事务里中间任何一步失败前面已经写入的数据就留在库里了。再说回并发。selectById默认是非阻塞读不会锁行所以就算外面包了事务两个并发请求依然能同时读到同一份库存数据。只有SELECT ... FOR UPDATE或者把扣减合并到 UPDATE 语句里才能规避。前者锁的粒度是行需要事务不提交才释放适合先查后改的复杂逻辑后者没有显式锁通过条件的原子性保证一致性性能更好。对甜品店这种秒级并发的系统用 UPDATE 条件扣减就够了。5. 系统部署与运行从数据库脚本到 Tomcat 启动5.1 环境准备与版本匹配拿到源码加数据库第一步不是双击运行而是确认环境版本。SSM 项目最常见的坑是 MyBatis 和 MySQL 驱动的兼容性。JDK 1.8 搭配 Tomcat 8.5 或 9.0MySQL 用 5.7 或 8.0驱动用mysql-connector-java的 5.1.49MySQL 5.7或 8.0.xMySQL 8.0。8.0 驱动要求 URL 里带serverTimezone这是最典型的启动时报错点。5.2 数据库初始化流程源码里通常附带 SQL 脚本常见的命名是sweet_shop.sql或init.sql。导入之前先检查脚本里的建库语句mysql -u root -p sweet_shop.sqlmysql -u root -p -e USE sweet_shop; SHOW TABLES;执行完成后重点核对三件事表数量是否和实体类对得上t_product_spec是否已有初始数据没有库存数据的话下单接口会直接报库存不足以及是否存在测试账号相关的t_user记录。5.3 数据库连接配置与启动参数SSM 项目的数据源配置一般集中在jdbc.properties里核心参数如下jdbc.drivercom.mysql.cj.jdbc.Driver jdbc.urljdbc:mysql://localhost:3306/sweet_shop?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai jdbc.usernameroot jdbc.password123456 # 连接池参数 pool.maxActive20 pool.initialSize5 pool.maxWait60000characterEncodingutf8控制中文读写不乱码serverTimezoneAsia/Shanghai解决 MySQL 8.0 的时区报错。连接池的maxActive按并发量调整管理系统 20 足够maxWait60000表示拿不到连接时最多等 60 秒超时抛异常防止请求无限堆积。Maven 工程在 IDEA 里直接执行 Tomcat 插件即可mvn clean package -DskipTestsmvn tomcat7:run注意 pom 里用的 tomcat 插件版本决定了运行时是 Tomcat 6 还是 7端口默认是 8080。如果本地 8080 被占用改 pom 里的port或 IDE 的配置。5.4 启动失败排查对照表启动报错基本集中在数据库相关环节下面按频率排序现象可能原因处理方式Access denied for user账号密码错误或权限不足用 root 登录执行 GRANT 授权Unknown database sweet_shop建库脚本没执行或库名写错核对 jdbc.url 和脚本里的 CREATE DATABASEPublic Key Retrieval is not allowedMySQL 8.0 驱动默认不允许 RSA 公钥获取URL 加allowPublicKeyRetrievaltrueSQLSyntaxErrorExceptionSQL 脚本导入的版本和驱动匹配不上确认数据库版本换对应驱动中文乱码数据库/表/连接任意一层编码不一致统一 utf8mb4URL 加 characterEncoding其中 Public Key Retrieval 是 MySQL 8.0 特有的问题不少人在 5.7 上跑得好好的换 8.0 就报错就是这个参数没加。这类配置细节排查一次就记住了比背文档管用。5.5 从源码建站角度检查项目完整性拿到一个 SSM 源码工程时最有效的检查线路是先看 pom.xml 依赖是否闭环再看 jdbc.properties 是否存在然后找 SQL 脚本导入最后看 web.xml 里 DispatcherServlet 的映射路径。如果 DispatcherServlet 映射的是/api/*而页面里的 ajax 路径写的是/product/list前端必然 404。这是源码交付项目里最常见的「看上去没问题跑起来全红」的原因。6. 压测验证与原子扣库存并发下单的进阶校验部署完成后先用现有接口把流程走通注册用户、上架商品、设置规格库存、创建订单、模拟支付改状态、查看订单详情然后进入并发验证环节。一个简单的压力测试可以用 curl 模拟。先确认接口参数比如商品规格 ID 为 1库存为 10用循环发起下单请求for i in $(seq 1 20); do curl -s -X POST http://localhost:8080/api/order/create \ -H Content-Type: application/json \ -d {userId:1,items:[{specId:1,quantity:1}]} done wait执行完查一下t_orders里那批订单的数量和t_product_spec里 specId1 的剩余库存。如果库存变成负数说明下单逻辑还是「先查再改」的写法如果库存是 0 但订单只有 10 条说明扣库存逻辑和下单落库不在同一事务里中间有数据丢失。最后看一个具体技巧验证扣减 SQL 是否真正生效时用SHOW ENGINE INNODB STATUS观察锁等待不直观更直接的办法是在 Mapper 方法上加一个返回受影响行数的日志Update(UPDATE t_product_spec SET stock stock - #{quantity} WHERE id #{specId} AND stock #{quantity}) int deductStock(Param(specId) Integer specId, Param(quantity) Integer quantity);把返回的 int 打出来等于该 SQL 实际影响的行数。0 表示库存不足1 表示扣减成功。配合压测脚本能立刻确认并发场景下有没有超卖。对于甜品店管理系统这个量级原子扣减已经足够如果后续要支撑秒杀级别的活动再考虑 Redis 预扣库存加 MQ 异步落库的方案但那是另一个复杂度的话题了。本文还有配套的精品资源点击获取
返回列表