ARTICLE DETAIL

资讯详情

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

SSM框架实战:手把手教你搭建食品商城系统

SSM框架实战:手把手教你搭建食品商城系统 很多同学拿到“093食品商城系统-ssm”这类课题时的第一反应是SSM 不就是 Spring、SpringMVC、MyBatis 三个框架拼在一起吗能有什么好写的。可一旦真正去梳理需求你会发现这个题目卡在了一个很巧妙的位置——比普通的增删改查难一点又够不上微服务那种复杂度恰好是检验 Java Web 基本功的试金石。这篇文章我按自己的实际开发顺序来写。我会把食品商城从业务建模、库表设计、SSM 三层整合到 MyBatis 动态 SQL再到目前大家很关心的 Vue3 怎么连 SSM 框架以及最后联调部署时容易踩的坑全部串起来讲一遍。不管你是做课程设计、毕业设计还是刚进公司要接手一个老 SSM 项目照着这条链路走一遍基本就能把一个食品类商城从零搭起来。1. 为什么这个题目值钱——SSM选型和食品商城的业务边界1.1 SSM还值不值得学这个题目卡在什么位置先说结论SSM 虽然已经不是新项目的主流选择但它的学习价值一点没打折。Spring 的 IoC 容器、AOP 事务、SpringMVC 的请求流转、MyBatis 的持久层映射这几块只要你换到 Spring Boot 项目里底层思想完全是同一套。Spring Boot 只是把配置做了自动约定真正遇到问题排查时你还是要懂 DispatcherServlet 在哪里、SqlSessionFactory 是什么时候创建的、事务代理是怎么生效的。食品商城这个题目比“学生管理系统”高一档就高在它有真实的业务规则。商品要分类、要管库存、要处理图片用户要注册登录、要加购物车、要下单、要支付、要查看订单状态管理员要上架商品、处理订单、统计销量。任何一个环节想做得像样都不是简单的 Mapper 里写几条 SQL 就能糊弄过去的。而且这个题目用 SSM 而不是 Spring Boot还有一个现实原因很多院校的课程大纲和项目验收标准还停留在 SSM。如果你把项目改成 Spring Boot虽然开发效率更高但在答辩时反而可能被追问“为什么不用课程要求的框架”。所以 SSM 不只是一个技术选型也是一个很务实的合规选择。1.2 食品商城的功能清单和角色划分动手写代码前先把功能边界划清楚。我的建议是分成三类角色普通用户、管理员以及一个可选的配送员角色。如果你是为了完成项目配送员可以先砍掉把订单状态改成管理员手动流转就行。用户端核心功能我列一下实际开发时以此为准省得后期需求蔓延注册、登录、退出个人资料修改商品分类浏览、关键词搜索、商品详情查看购物车加入、修改数量、删除、批量结算订单确认订单、选择收货地址、提交订单、取消订单、订单列表与详情个人中心收货地址管理、订单状态跟踪、商品评价管理端核心功能管理员登录与权限拦截商品管理分类维护、商品上架/下架、库存修改、商品图片上传订单管理订单列表、发货、查看订单详情用户管理用户列表、禁用/启用账号数据统计简单统计商品销量、订单金额可以用图表库展示功能清单定下来后数据库的表数量基本就确定了。食品类的特色在于商品属性比如保质期、产地、规格、存储方式这些字段在通用商城系统里容易被忽略但在食品商城里反而是用户下单时很关注的信息。2. 建表时就要想清楚的事——食品商城的库表设计与状态流转2.1 商品、分类、库存的设计要点数据库设计是整个项目的地基我见过太多人一上来就写代码做到订单模块时发现商品表缺字段、订单表没冗余商品快照然后回头改表结构、改代码浪费大量时间。表设计花半天后面能省三天的返工。商品分类建议用父子层级结构方便以后扩展二级分类。一张表就能搞定CREATE TABLE category ( category_id INT PRIMARY KEY AUTO_INCREMENT, category_name VARCHAR(50) NOT NULL, parent_id INT DEFAULT 0, sort_order INT DEFAULT 0 );商品表是核心字段要覆盖食品特有属性CREATE TABLE product ( product_id INT PRIMARY KEY AUTO_INCREMENT, category_id INT NOT NULL, product_name VARCHAR(100) NOT NULL, product_desc TEXT, main_image VARCHAR(255), detail_images VARCHAR(1000), price DECIMAL(10,2) NOT NULL, stock INT NOT NULL DEFAULT 0, unit VARCHAR(20) DEFAULT 件, shelf_life VARCHAR(50), origin VARCHAR(100), storage_method VARCHAR(50), sale_count INT DEFAULT 0, status TINYINT DEFAULT 1, create_time DATETIME, update_time DATETIME );这里有两个细节我特别提醒一下。第一价格永远用 DECIMAL不要用 FLOAT 或 DOUBLE否则计算金额时会出现 0.1 0.2 不等于 0.3 这种尴尬问题。第二商品状态字段建议用 TINYINT1 表示上架、0 表示下架查询列表时默认只查 status 1 的商品下架商品不能让用户看到。库存字段也可以设计成单独的 stock 表但食品商城单表就够了不要为了“范式”过度设计。2.2 订单主表、订单项表与状态机订单是商城系统里最复杂的一块核心是主表和子表的拆分。订单主表存一次下单的整体信息订单项表存每个商品的明细。为什么要拆因为一次下单可能有多个商品每个商品的单价、数量、小计都不同如果把多个商品塞进一行查询和统计都会很痛苦。订单主表的关键字段CREATE TABLE orders ( order_id INT PRIMARY KEY AUTO_INCREMENT, order_no VARCHAR(32) NOT NULL, user_id INT NOT NULL, total_amount DECIMAL(10,2), pay_amount DECIMAL(10,2), freight DECIMAL(10,2) DEFAULT 0, pay_type TINYINT DEFAULT 1, status TINYINT NOT NULL DEFAULT 0, receiver_name VARCHAR(50), receiver_phone VARCHAR(20), receiver_addr VARCHAR(255), create_time DATETIME, pay_time DATETIME, deliver_time DATETIME, finish_time DATETIME );订单项表CREATE TABLE order_item ( item_id INT PRIMARY KEY AUTO_INCREMENT, order_id INT NOT NULL, product_id INT NOT NULL, product_name VARCHAR(100), product_image VARCHAR(255), product_price DECIMAL(10,2), quantity INT NOT NULL, total_price DECIMAL(10,2) );两个细节值得说明。订单项表里冗余了 product_name、product_image、product_price这在数据库设计里叫快照。因为商品信息以后可能改价、改名称但用户下单那一刻的价格和商品信息必须固定下来不能商品改了价之前的订单也变。另外订单主表里我直接冗余了收货人姓名、电话、地址而不是只存一个 address_id同样是为了防止用户删除了地址后订单记录变得不可读。订单状态是这套系统的状态机建议用数字枚举展示层再转成文字状态值含义可执行操作0待支付取消、支付1待发货取消、管理员发货2已发货确认收货3已完成评价4已取消无5已退款无状态流转在 Service 层里要做合法性校验。比如已发货的订单不能直接取消待支付的订单不能直接确认收货。这种校验用 if 判断就行不需要引入什么状态机框架但一定要写否则订单状态会被前端传参直接改乱。2.3 用户、地址、购物车这几张“基础表”的坑用户表、地址表、购物车表看着简单但坑也不少。用户表我建议至少包含这些字段user_id、username、password、phone、email、avatar、status、create_time。密码不要明文存用 MD5 加盐或者 BCrypt 都行。如果项目要求不高MD5 加盐也够用想给自己加分就用 Spring Security 里的 BCryptPasswordEncoder。购物车表的设计有两种思路。一种是只存 cart_id、user_id、product_id、quantity、checked 这几个字段和商品表关联查询时再拿商品名、价格、图片。另一种是把商品名、价格、图片也冗余进去。我推荐前一种因为购物车是临时数据商品改了价用户重新加一次就能看到新价格没必要做快照。CREATE TABLE cart ( cart_id INT PRIMARY KEY AUTO_INCREMENT, user_id INT NOT NULL, product_id INT NOT NULL, quantity INT NOT NULL DEFAULT 1, checked TINYINT DEFAULT 1 );地址表要考虑到食品商城经常要做地区联动所以省份、城市、区县可以拆开存方便后续做运费规则。但如果只做基础功能用一个收货地址字符串字段也行关键是下单时把地址快照到订单表。3. Spring与MyBatis拼起来的三层架构——配置文件与事务细节3.1 分包规范和Service接口设计SSM 项目最怕的就是包结构混乱。我常用的分包方式是标准的 controller / service / mapper / entity / vo / commoncom.food.shop ├── controller ├── service │ └── impl ├── mapper ├── entity ├── vo ├── common │ ├── Result.java │ └── PageResult.java └── interceptorService 接口我会单独抽出来而不是直接写一个 class。虽然接口只有一个实现类时会显得有点多余但在 SSM 项目里这几乎是约定俗成的写法而且以后想加 mock 或者 AOP 切面也方便。实现类上要加 Service 注解Mapper 接口上加 Repository 或者 Mapper 注解。Spring 扫描的时候Service 和 Controller 用 component-scan 扫描Mapper 用 MapperScannerConfigurer 扫描。Controller 层要薄只做参数接收、校验调用和结果封装。业务逻辑尽量都放在 Service 里尤其是事务操作必须在 Service 层实现因为 Spring 的声明式事务是基于 AOP 代理的Controller 里调 Service 方法时事务才生效写在 Controller 里的逻辑是管不到事务的。3.2 整合配置数据源、Mapper扫描、事务管理器SSM 的配置可以用两种方式全 XML或者 XML 注解混合。我建议用 XML 注解混合既保留了 XML 配置 IoC 和 AOP 的直观性又让人物代码里的注解来减少 XML 的膨胀。一个标准的 spring-mybatis.xml 配置大概是这样的context:property-placeholder locationclasspath:jdbc.properties/ bean iddataSource classcom.alibaba.druid.pool.DruidDataSource property namedriverClassName value${jdbc.driver}/ property nameurl value${jdbc.url}/ property nameusername value${jdbc.username}/ property namepassword value${jdbc.password}/ /bean bean idsqlSessionFactory classorg.mybatis.spring.SqlSessionFactoryBean property namedataSource refdataSource/ property namemapperLocations valueclasspath:mapper/*.xml/ property nametypeAliasesPackage valuecom.food.shop.entity/ /bean bean classorg.mybatis.spring.mapper.MapperScannerConfigurer property namebasePackage valuecom.food.shop.mapper/ /bean bean idtransactionManager classorg.springframework.jdbc.datasource.DataSourceTransactionManager property namedataSource refdataSource/ /bean tx:annotation-driven transaction-managertransactionManager/这里有个容易漏的地方mapperLocations 指向 XML 文件位置如果你的 Mapper 接口和 XML 不在同一个包或者 XML 没有被 Maven 打包进 classpath运行时会报“Invalid bound statement (not found)”。排查方法很简单解压 target 目录里的 war 包看 mapper 目录下的 XML 有没有打进去。如果没有就在 pom.xml 里加 resources 配置把 src/main/java 下的 XML 也打包。另一个要点是事务。Transactional 注解要加在 Service 实现类的方法上并且要注意默认只回滚 RuntimeException如果方法里 catch 掉了异常事务就失效了。这点非常经典我在做下单接口时第一次就栽在这上面后面会细说。3.3 下单业务的事务实现扣库存与建订单下单接口是整个系统里最应该加事务的地方因为涉及多张表的数据变更检查并扣减库存、创建订单主表、创建订单项表、清空购物车。任何一个步骤失败前面的操作都要回滚。我先给一个典型的下单 Service 方法骨架Override Transactional(rollbackFor Exception.class) public boolean submitOrder(OrderSubmitVO vo, Integer userId) { // 1. 查询购物车选中的商品列表 ListCart cartList cartMapper.findCheckedByUserId(userId); if (cartList null || cartList.isEmpty()) { throw new BusinessException(没有选中商品); } BigDecimal totalAmount BigDecimal.ZERO; ListOrderItem orderItems new ArrayList(); for (Cart cart : cartList) { Product product productMapper.selectById(cart.getProductId()); // 2. 校验商品是否存在且已上架 if (product null || product.getStatus() ! 1) { throw new BusinessException(商品已下架 cart.getProductId()); } // 3. 校验库存 if (product.getStock() cart.getQuantity()) { throw new BusinessException(库存不足 product.getProductName()); } // 4. 扣减库存 int rows productMapper.reduceStock(product.getProductId(), cart.getQuantity()); if (rows 0) { throw new BusinessException(扣减库存失败); } // 5. 组装订单项 OrderItem item new OrderItem(); item.setProductId(product.getProductId()); item.setProductName(product.getProductName()); item.setProductImage(product.getMainImage()); item.setProductPrice(product.getPrice()); item.setQuantity(cart.getQuantity()); item.setTotalPrice(product.getPrice().multiply(BigDecimal.valueOf(cart.getQuantity()))); orderItems.add(item); totalAmount totalAmount.add(item.getTotalPrice()); } // 6. 创建订单 Orders order new Orders(); order.setOrderNo(generateOrderNo()); order.setUserId(userId); order.setTotalAmount(totalAmount); order.setPayAmount(totalAmount); order.setStatus(0); order.setReceiverName(vo.getReceiverName()); order.setReceiverPhone(vo.getReceiverPhone()); order.setReceiverAddr(vo.getReceiverAddr()); order.setCreateTime(new Date()); orderMapper.insert(order); // 7. 批量插入订单项 for (OrderItem item : orderItems) { item.setOrderId(order.getOrderId()); orderItemMapper.insert(item); } // 8. 清空已结算的购物车 cartMapper.deleteCheckedByUserId(userId); return true; }扣减库存这里我用了 reduceStock 而不是先查出来再 update是因为 update 语句里带上 stock quantity 条件在并发场景下能天然防止超卖UPDATE product SET stock stock - #{quantity} WHERE product_id #{productId} AND stock #{quantity}这条 SQL 影响行数为 0 就说明库存已经不够了直接抛异常回滚。这种写法比 select 出来在 Java 里判断再 update 要安全得多因为它把判断和扣减合并成了一个原子操作。事务的坑我也顺便说一下。如果代码里把异常 catch 了然后返回 falseSpring 会因为事务没有被异常打断而正常提交结果就是库存扣了、订单没建成功数据就错了。所以异常一定要往外抛或者至少用 TransactionAspectSupport.currentTransactionStatus().setRollbackOnly() 手动标记回滚。我自己的习惯是统一抛自定义 BusinessException由全局异常处理器去处理这样事务能保证回滚Controller 也干净。4. 商品搜索和订单查询里的MyBatis动态SQL4.1 多条件商品搜索的 组合商城首页和商品列表页最常用的就是多条件搜索用户可能按分类筛选、可能按关键词搜索、可能只看有货的、可能按价格区间过滤。这些条件不固定如果写死 SQL 就没法适配所有组合了。MyBatis 的动态 SQL 就是干这个的。看一个比较典型的商品列表查询select idsearchProducts resultTypecom.food.shop.entity.Product SELECT * FROM product where if testcategoryId ! null and categoryId ! 0 AND category_id #{categoryId} /if if testkeyword ! null and keyword ! AND (product_name LIKE CONCAT(%, #{keyword}, %) OR product_desc LIKE CONCAT(%, #{keyword}, %)) /if if testminPrice ! null AND price gt; #{minPrice} /if if testmaxPrice ! null AND price lt; #{maxPrice} /if if testonlyInStock ! null and onlyInStock AND stock gt; 0 /if AND status 1 /where ORDER BY product_id DESC /select标签会自动处理掉第一个 AND比写 WHERE 11 然后再拼条件干净多了。注意在 XML 里写大于号小于号一定要转义或者用 包起来否则 XML 解析直接报错。这个是我见过初学者踩得最多的问题之一。商品列表查询的参数一般有三个以上建议封装成一个 ProductQueryVO不要在接口方法里堆五六个参数。参数多了不光看着乱以后加条件还要改接口签名谁改谁知道。4.2 分页查询和订单详情的一对多映射分页有两个方案自己写 LIMIT或者直接用 PageHelper。我建议如果项目是单纯学习用先自己写一遍 LIMIT能理解分页的本质如果是赶进度直接用 PageHelper 最省事。PageHelper 的用法很简单查询前调用 PageHelper.startPage(pageNum, pageSize)紧接着的第一次查询就会被自动拦截并分页返回的结果用 PageInfo 包装就能拿到总条数PageHelper.startPage(pageNum, pageSize); ListOrderVO orderList orderMapper.selectOrderList(userId); PageInfoOrderVO pageInfo new PageInfo(orderList);订单详情查询涉及一对多映射一个订单对应多个订单项。MyBatis 的 标签可以把子表数据自动组装进父对象resultMap idOrderDetailMap typecom.food.shop.vo.OrderDetailVO id propertyorderId columnorder_id/ result propertyorderNo columnorder_no/ result propertytotalAmount columntotal_amount/ result propertystatus columnstatus/ collection propertyorderItems ofTypecom.food.shop.entity.OrderItem id propertyitemId columnitem_id/ result propertyproductId columnproduct_id/ result propertyproductName columnproduct_name/ result propertyproductPrice columnproduct_price/ result propertyquantity columnquantity/ result propertytotalPrice columntotal_price/ /collection /resultMap select idselectOrderDetail resultMapOrderDetailMap SELECT o.order_id, o.order_no, o.total_amount, o.status, i.item_id, i.product_id, i.product_name, i.product_price, i.quantity, i.total_price FROM orders o LEFT JOIN order_item i ON o.order_id i.order_id WHERE o.order_id #{orderId} /select这里我踩过一个坑直接用 resultType 而不是 resultMap然后想在 Service 里手动组装订单项。结果订单详情每次要查两次数据库一条查订单、一条查订单项代码又丑又慢。后来改成 一次性查出所有数据性能好了很多。能用一条 SQL 解决的问题就别在应用层凑。5. Vue3连接SSM框架——前端联调的关键细节5.1 统一返回体和接口文档现在做 SSM 项目前端很少再用 JSP 了大多数人都会像热词里问的那样——Vue3 怎么连接 SSM 框架。前端和后端分离之后第一个要解决的就是接口风格问题。我强烈建议所有接口统一返回一个 Result 对象不要有的返回 JSON、有的直接返回字符串。public class ResultT { private Integer code; private String message; private T data; public static T ResultT success(T data) { ResultT result new Result(); result.setCode(200); result.setMessage(success); result.setData(data); return result; } public static T ResultT error(Integer code, String message) { ResultT result new Result(); result.setCode(code); result.setMessage(message); return result; } }Controller 里统一返回 ResultSpring 通过 Jackson 自动序列化成 JSON。注意 SpringMVC 要返回 JSON 必须在方法上标注 ResponseBody或者类上标注 RestController。忘记加 ResponseBody 是 SSM 里最经典的坑——返回的是 ModelAndView前端拿到的不是 JSON 而是字符串。接口文档如果不想引入 Swagger至少要维护一份简单的 Markdown 文档写清楚每个接口的路径、请求方式、请求参数、返回结构。我自己的习惯是写完一个 Controller 就同步更新文档等全部写完再补会让前端等得很痛苦而且容易漏。5.2 Vue3请求封装、跨域与会话保持Vue3 里连接 SSM 最常用的是 axios。老 SSE 项目因为是同源部署没有跨域问题但前后端分离开发时Vue 跑在 5173 端口、后端跑在 8080 端口就必然遇到跨域。跨域有两种解决方式。第一种是后端加 CORS 配置第二种是前端 Vite 配置代理。开发阶段我建议用 Vite 代理因为真正上线时一般会把打包后的 dist 目录丢到 Tomcat 的 webapps 下和后端同源就不需要跨域了。用代理只是开发环境的事。Vite 配置export default defineConfig({ server: { port: 5173, proxy: { /api: { target: http://localhost:8080, changeOrigin: true, rewrite: path path.replace(/^\/api/, ) } } } })axios 封装时要注意两点。第一请求路径里带 /api 前缀的会走代理去掉前缀后请求到后端第二如果后端用 session 保持登录态axios 必须配置 withCredentials: true否则 axios 不会携带 Cookie。import axios from axios const request axios.create({ baseURL: /api, timeout: 10000, withCredentials: true }) request.interceptors.response.use( response { const res response.data if (res.code ! 200) { if (res.code 401) { window.location.href /login } return Promise.reject(new Error(res.message || 请求失败)) } return res.data }, error { return Promise.reject(error) } ) export default request响应拦截器统一处理 code 不等于 200 的情况业务代码里就不用每个接口都写 if 判断了。这里我特别说明一下如果后端返回的是 Result 对象拦截器里拿到的 response.data 就是整个 Result需要再取一层 .data 才是业务数据。很多同学刚联调时总是拿不到数据就是因为路径没捋清楚。5.3 前后端分离后登录态怎么设计SSM 传统做法是 session 存登录用户前端带 Cookie 就能维持登录。前后端分离后如果前端和后端部署在同域session Cookie 依然可行Tomcat 会自己维护 JSESSIONIDaxios 加了 withCredentials 就能正常使用。但如果你想要更“现代”一点的方案可以用 JWT 或者一个简单的 token 机制。登录成功后后端生成一个 token存到 Redis 或者内存 Cache 里token 作为 keyuserId 作为 value。前端把 token 存到 localStorage 里每次请求在拦截器里加到请求头request.interceptors.request.use(config { const token localStorage.getItem(token) if (token) { config.headers[Authorization] token } return config })后端用一个拦截器统一校验public class LoginInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String token request.getHeader(Authorization); if (token null || token.isEmpty()) { // 返回 401由前端跳转登录页 response.setStatus(401); return false; } Integer userId cache.get(token); if (userId null) { response.setStatus(401); return false; } request.setAttribute(userId, userId); return true; } }然后用 WebMvcConfigurer 配置拦截器的放行路径。这是很关键的一步登录、注册、商品列表、商品详情这些接口不能拦截否则用户没登录就没法看商品了。要把登录拦截范围控制在需要登录才能访问的接口上或者更省事一点默认拦截所有接口但白名单放行 /user/login、/user/register、/product/** 这些公开接口。6. 联调和部署期踩过的坑以及修复方案6.1 依赖与版本问题SSM 项目部署时最容易出的问题就是依赖冲突。Maven 项目里 spring-webmvc、spring-context、spring-jdbc 的版本不一致会导致运行时出现各种莫名其妙的 NoSuchMethodError。我的建议是直接在 pom.xml 里用 properties 统一管理版本号properties spring.version5.3.20/spring.version /properties所有 Spring 依赖都引用这个版本号保证它们完全一致。MyBatis 和 mybatis-spring 的版本也要注意对应关系mybatis-spring 2.x 对应 MyBatis 3.5.x这个组合比较稳定。另一个高频问题是数据库驱动版本。本地连 MySQL 5.7 好好的部署到服务器 MySQL 8.0 就连不上通常是驱动版本太旧不支持新的认证插件。用 MySQL 8.0 就配 mysql-connector-java 8.0.x驱动类名也变成 com.mysql.cj.jdbc.DriverURL 最好加上 useSSLfalse、serverTimezoneAsia/Shanghai 这些参数否则会报时区错误。6.2 静态资源404与拦截器放行SSM 项目里静态资源 404 几乎人人都遇到过。问题根源在于 web.xml 里把 DispatcherServlet 映射到了 /所有请求都进了 SpringMVC静态资源找不到对应的 Handler自然就 404。解决办法是在 SpringMVC 配置文件里加静态资源放行mvc:resources location/static/ mapping/static/**/或者用更现代的配置类方式Configuration public class WebConfig implements WebMvcConfigurer { Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler(/static/**) .addResourceLocations(/static/); } }如果你同时配置了登录拦截器还需要在拦截器注册那里把静态资源路径也放掉否则请求到不了静态资源处理器就被拦了。这个顺序很多人搞混先被拦截器拦了即使 mvc:resources 配了也白搭。6.3 连接池和并发库存问题数据库连接池我推荐用 Druid一方面是 Alibaba 出品、社区活跃另一方面它自带的监控页面特别好用开发联调时能看到每次请求的 SQL 执行时间、连接池活跃数排查慢 SQL 特别方便。Druid 有一个经典问题MySQL 服务端的 wait_timeout 默认是 8 小时如果连接池里一个连接超过 8 小时没有被使用MySQL 会把这个连接断开。Druid 还继续拿着这个“死连接”往数据库发请求就会报 Communications link failure。解决办法是在 Druid 配置里加上 testWhileIdle 和 validationQueryproperty nametestWhileIdle valuetrue/ property namevalidationQuery valueSELECT 1/ property nametimeBetweenEvictionRunsMillis value60000/这个配置让连接池每分钟检查一次空闲连接是否有效失效的连接直接移除避免“数据库连接失效”这个经典坑。并发库存问题我在前面提过用 update 语句原子扣减来解决这里再补充一个思路。高并发场景下一条 update 在数据库层面会对行加锁大量并发扣库存时会出现锁等待。如果你的项目并发量确实很大可以考虑把库存扣减放到 Redis 上做用 Redis 的 decr 命令原子扣减再异步同步到数据库。但食品商城这个量级完全用不到数据库行锁已经够用了。最后把整个项目的运行链路再理一遍前端 Vue3 发请求Vite 代理转发到 TomcatDispatcherServlet 分发给 ControllerController 调 ServiceService 调 MapperMapper 执行 SQL 操作 MySQL数据一层层返回Jackson 序列化成 JSON 回给前端。这个链路看着长但真正调通过一次后面写什么模块都不慌了。我在做这个食品商城时最大的体会是SSM 项目难点不在框架配置而在你有没有把业务规则想清楚以及踩过坑之后能不能把原因找出来。遇到问题不要急着改代码先从前端 Network 面板看请求有没有发出去、后端日志有没有报错、SQL 能不能查到数据这三步能定位九成的问题。
返回列表