恒美微站 Logo 恒美微站
  • 首页
  • 关于我们
  • 建站服务
  • 主题模板
  • 案例展示
  • 资讯中心
  • 联系我们

Spring MVC + MyBatis + Vue3食品商城系统:从架构设计到演示视频全流程实战

  • 首页
  • 资讯中心
  • /
  • Spring MVC + MyBatis + Vue3食品商城系统:从架构设计到演示视频全流程实战

相关资讯

Easy Dataset:从领域文档到RAG知识库可量化评测基准 2026/9/9 9:08:38
Wayfinder:AI应用评估的参考实现与工程实践 2026/9/9 9:08:38
機器人怎么才能“记住“十秒钟前发生的事 2026/9/9 9:08:38

最新资讯

HTOOL-SL6H便携信号源评测:从按键到SCPI的射频测试实战指南
Python作品集实战:5个项目帮你搞定面试官
Magnitude:本地大模型服务的协议抽象层与Agent编排枢纽
免费音频转文字工具实测:录音转文本、视频转字幕与格式处理全攻略
AI Agent记忆系统三层架构:短期、长期与工作记忆实战解析
Magnitude:从向量模长到星等震级,理解“量级”如何重塑技术决策

今日推荐

基于MongoDB的图书管理系统:数据建模与Spring Boot+Vue实战
Claude Code安装配置全攻略:从零开始用上终端AI编程助手
tmux 会话管理与终端复用:AI 编程工作流的调度中枢实战

本周热门

超人会飞不算本事:系统稳定依赖清晰规则与边界设计
超人VS蜘蛛侠:拆解超级IP的影响力与传播方法论
基于CNN的调制信号识别:MATLAB实现时频图分类实战

本月精选

自研推理加速器Redwood:两周内实现PyTorch模型高效部署的实战教程
V4L2摄像头采集实战:从camera_client.rar到出图全流程解析
从“谁发明了钢琴键”到知识问答智能体:RAG与记忆工程实践

Spring MVC + MyBatis + Vue3食品商城系统:从架构设计到演示视频全流程实战

发布时间:2026/9/9 9:08:38
Spring MVC + MyBatis + Vue3食品商城系统:从架构设计到演示视频全流程实战 这个项目标题一出来我第一反应就是“标准的Java全栈毕设/实训项目”——Spring MVC做后端控制层MyBatis管数据库操作前端用Vue3搭商城界面最后再录个演示视频。看起来是技术栈比较老派的组合但仔细一琢磨这套东西反而是现在Java岗求职和课设选题里最稳的搭配Spring MVC让你把路由、拦截器、参数绑定这些基础吃透MyBatis让你把SQL能力练扎实Vue3又能证明你不是只会写后端接口的“半吊子”。标题里最值得琢磨的是“视频”两个字。我做过不少类似的项目演示很多人代码写完了一到录视频就翻车要么对着屏幕干讲十分钟没人愿意看要么演示到一半页面报错就慌了。后面我会专门拿出一整章说清楚录制演示视频的脚本设计、演示节奏和“翻车急救方案”这部分网上很少有人讲。这篇文章我会把整个项目的搭建思路、核心表结构、Spring MVC和MyBatis的整合细节、Vue3前台页面的实现方式、MyBatis缓存机制以及面试官最常追问的问题一次性讲透。适合正在做毕设选题、想进Java岗但项目经验薄弱、或者想从SSM老项目过渡到前后端分离架构的同学参考。整套代码结构我会按照“后端三层架构 前端单页应用”来设计既保留了Spring MVC的教学价值又能体现Vue3的现代前端开发思路。1. 项目定位与技术选型思路1.1 为什么是Spring MVC MyBatis而不是直接SpringBoot很多新手一上来就质疑都什么年代了还在用Spring MVC直接SpringBoot整一个不香吗我一开始也有这个疑问但真正把这个项目从头到尾过了一遍之后我发现了这套组合的独特价值。SpringBoot确实是当下Java后端的主流脚手架但Spring MVC作为Spring家族里最核心的Web框架很多原理性的东西在SpringBoot里被“自动配置”给掩盖了。比如DispatcherServlet是怎么分发请求的、HandlerMapping怎么找到对应的RequestMapping方法、ViewResolver如何解析视图这些在SpringBoot里你几乎感知不到但在Spring MVC项目里你必须手动配出来。从学习角度来说自己手动引入spring-webmvc依赖在web.xml里配置DispatcherServlet再写一个spring-mvc.xml开启注解驱动和包扫描你对整个请求流转过程的理解会比直接点开SpringBoot的自动配置类要深刻得多。面试的时候面试官问“一次HTTP请求从进入到返回Spring MVC经历哪些环节”你按这个顺序答前端控制器DispatcherServlet接收请求、查询HandlerMapping找到Controller、调用HandlerAdapter执行方法、Controller返回ModelAndView、ViewResolver解析视图——这一套答下来比背SpringBoot源码有说服力。MyBatis的选择逻辑也是一样的。如果你用MyBatis-Plus写Mapper接口都有现成的selectPage、selectListSQL都不用手写但面试官问“#{}和${}有什么区别”的时候你脑子里是没有画面感的。而这个项目里大量用到动态SQL——零食分类的多条件筛选、商品模糊搜索、订单状态统计你被迫去写if、where、foreach这些标签写完之后再去看那些MyBatis面试题会发现全是实操中踩过的坑。1.2 “视频”这个关键词对项目形态的影响标题里的“视频”不要理解成“视频网站项目”它讲的是“交付形态”——系统开发完成后需要用视频演示。这个细节影响了我对整个项目架构的判断。既然要演示就要保证项目能稳定跑起来不能一会儿报个错一会儿白屏。所以我选择了前后端分离但部署在同一台机器上的结构后端打war包丢进Tomcat前端构建后用Nginx托管静态资源这两个服务在本机端口互不冲突演示的时候打开浏览器就能玩不需要现场切来切去。如果你愣要在演示现场启动IdEA、等待Maven下载依赖、再查数据库密码那视频录出来基本没法看。为了演示效果我在设计商品数据时也特意下了功夫宁可用真实感强的“三只松鼠坚果礼盒”“辣条大礼包”“无糖气泡水”这类商品也不要写“零食1”“零食2”这种占位数据。列表页的商品卡片的图片、价格、标签、销量字段都要真实这样录屏的时候页面才饱满。这一点做项目的人常常忽略但恰恰是展示环节成败的关键——观众第一眼看的是画面而不是你的代码。1.3 核心业务模块拆解网上食品零食商城系统核心业务脱离不了“用户-商品-购物车-订单”这条主线。我最终敲定了6个模块用户模块注册、登录、个人信息查看与修改密码用MD5加盐存储登录状态用Session管理商品分类模块一级分类加二级分类前台导航按分类展示零食列表商品模块零食的分页列表、综合排序/销量排序/价格排序、按名称模糊搜索、商品详情购物车模块加入购物车、修改数量、删除购物项、批量结算订单模块订单提交、订单列表、订单详情、取消订单、模拟支付后台管理模块管理员登录、商品上架下架、编辑库存、查看所有订单前台用户走商店首页→分类浏览→商品详情→加入购物车→提交订单→支付后台管理员走商品管理→订单管理。这两条用户路径刚好是演示视频里的主线脚本后面讲视频录制时我会再展开。2. 数据库设计与后端核心实现2.1 表结构设计从业务推导字段数据库设计我建议不要直接用网上别人的建表SQL而是自己先理一遍业务关系再画表。画出5张核心表就够了用户表 user - id BIGINT 主键自增 - username VARCHAR(50) 唯一 - password VARCHAR(64) 加盐后的MD5 - nickname VARCHAR(50) - phone VARCHAR(20) - avatar VARCHAR(255) - created_time DATETIME 商品分类表 category - id BIGINT 主键 - name VARCHAR(50) - parent_id BIGINT 父分类ID一级分类为0 - sort INT 排序权重 商品表 product - id BIGINT 主键 - name VARCHAR(100) - description VARCHAR(500) - price DECIMAL(10,2) - stock INT 库存 - sales INT 销量 - image VARCHAR(255) 封面图URL - category_id BIGINT 关联分类 - status TINYINT 1上架 0下架 - created_time DATETIME 购物车表 cart_item - id BIGINT 主键 - user_id BIGINT - product_id BIGINT - quantity INT - checked TINYINT 是否勾选结算 - UNIQUE KEY(user_id, product_id) 订单表 orders - id BIGINT 主键 - order_no VARCHAR(32) 订单编号 - user_id BIGINT - total_amount DECIMAL(10,2) - status TINYINT 状态码0待付款 1待发货 2已发货 3已完成 4已取消 - receiver_name VARCHAR(50) - receiver_phone VARCHAR(20) - receiver_address VARCHAR(255) - created_time DATETIME这里有两个字段要特别说明。第一个是orders表的order_no订单编号不要直接用主键id展示给用户我用的规则是“时间戳 三位随机数”比如20250115153000123长度20位左右既看起来真实又方便排查。第二个是cart_item表的联合唯一键(user_id, product_id)这一个约束能直接省掉重复加入购物车的问题在业务代码里就不用先查一遍有没有重复数据再决定insert还是update直接靠唯一键冲突后走ON DUPLICATE KEY UPDATE搞定少写不少判断逻辑。2.2 Spring MVC分层搭建过程搭建项目的第一步是在IDEA里创建一个Maven webapp工程然后手动引入依赖。我习惯把依赖一次性配全避免代码写一半才想起缺东缺西。食品商城项目里最核心的依赖就这13个坐标放pom.xml的dependencies里!-- Spring核心 -- dependency groupIdorg.springframework/groupId artifactIdspring-context/artifactId version5.3.39/version /dependency !-- Spring MVC -- dependency groupIdorg.springframework/groupId artifactIdspring-webmvc/artifactId version5.3.39/version /dependency !-- MyBatis -- dependency groupIdorg.mybatis/groupId artifactIdmybatis/artifactId version3.5.16/version /dependency !-- MyBatis与Spring整合 -- dependency groupIdorg.mybatis/groupId artifactIdmybatis-spring/artifactId version2.1.2/version /dependency !-- MySQL驱动 -- dependency groupIdcom.mysql/groupId artifactIdmysql-connector-j/artifactId version8.0.33/version /dependency !-- 数据库连接池 -- dependency groupIdcom.alibaba/groupId artifactIddruid/artifactId version1.2.23/version /dependency !-- Jackson用于JSON序列化 -- dependency groupIdcom.fasterxml.jackson.core/groupId artifactIdjackson-databind/artifactId version2.17.1/version /dependency !-- Servlet API -- dependency groupIdjavax.servlet/groupId artifactIdjavax.servlet-api/artifactId version4.0.1/version scopeprovided/scope /dependency !-- JSTL -- dependency groupIdjavax.servlet/groupId artifactIdjstl/artifactId version1.2/version /dependency !-- 文件上传用于后台管理商品图片 -- dependency groupIdcommons-fileupload/groupId artifactIdcommons-fileupload/artifactId version1.5/version /dependency !-- Lombok简化实体类 -- dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId version1.18.32/version optionaltrue/optional /dependency依赖配好之后最关键的步骤是在web.xml里注册DispatcherServlet。新手容易在这里被绕晕我一句话说清楚DispatcherServlet就是整个Spring MVC的大门所有请求都要先进这个门所以web.xml里肯定有一个servlet标签指向它。再配合ContextLoaderListener加载Spring容器让Service、Mapper这些Bean也一并创建出来。web-app xmlnshttp://xmlns.jcp.org/xml/ns/javaee xmlns:xsihttp://www.w3.org/2001/XMLSchema-instance xsi:schemaLocationhttp://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd version4.0 !-- 全局上下文配置 -- context-param param-namecontextConfigLocation/param-name param-valueclasspath:spring-context.xml/param-value /context-param listener listener-classorg.springframework.web.context.ContextLoaderListener/listener-class /listener !-- 前端控制器 -- servlet servlet-namedispatcher/servlet-name servlet-classorg.springframework.web.servlet.DispatcherServlet/servlet-class init-param param-namecontextConfigLocation/param-name param-valueclasspath:spring-mvc.xml/param-value /init-param load-on-startup1/load-on-startup /servlet servlet-mapping servlet-namedispatcher/servlet-name url-pattern//url-pattern /servlet-mapping /web-app2.3 MyBatis配置借助插件定位SQL执行问题spring-mvc.xml里面开启注解驱动并配置视图解析器spring-context.xml里面配置数据源、SqlSessionFactoryBean和MapperScannerConfigurer这两段配置是项目能跑起来的命根子。由于代码比较长我把最关键的一段数据源和MyBatis整合配置贴出来!-- 配置druid数据库连接池 -- bean iddataSource classcom.alibaba.druid.pool.DruidDataSource property namedriverClassName valuecom.mysql.cj.jdbc.Driver/ property nameurl valuejdbc:mysql://localhost:3306/snack_mall?useUnicodetrueamp;characterEncodingutf8amp;serverTimezoneAsia/Shanghai/ property nameusername valueroot/ property namepassword value你的密码/ /bean !-- 配置MyBatis的SqlSessionFactory -- bean idsqlSessionFactory classorg.mybatis.spring.SqlSessionFactoryBean property namedataSource refdataSource/ property nameconfigLocation valueclasspath:mybatis-config.xml/ !-- 支持给Mapper接口中的方法起别名等配置 -- property nametypeAliasesPackage valuecom.snack.entity/ /bean !-- 扫描Mapper接口 -- bean classorg.mybatis.spring.mapper.MapperScannerConfigurer property namebasePackage valuecom.snack.mapper/ /beanMapperScannerConfigurer这个配置让MyBatis在Spring容器启动时自动扫描com.snack.mapper包下的所有接口并且动态生成代理对象。所以你在Service里使用Autowired注入一个Mapper接口时实际注入的是MyBatis生成的一个代理对象它负责找到XML里对应的SQL并执行。想明白这一点你就能理解为什么Mapper接口里不能有任何自己想实现的方法只能写方法签名。在实际调试过程中我强烈安利IDEA插件MyBatis Log Free。装上之后它会自动抓取MyBatis执行SQL时输出的日志并且把参数值直接拼进SQL里展示出来。以前排查SQL问题看到控制台输出的???占位符还得自己拿参数值去替换用了这个插件直接看到完整可执行SQL一行就能看出来是WHERE条件拼错了还是参数传错了。智能提示的SQL能直接复制到Navicat里去跑五分钟解决一个排查半小时的bug不是开玩笑。mybatis-config.xml全局配置文件里我开了两个核心设置。第一个是setting namemapUnderscoreToCamelCase valuetrue/打开驼峰命名自动转换数据库字段created_time就能自动映射到实体类的createdTime属性省去一堆resultMap的书写量。第二个是setting namelogImpl valueSTDOUT_LOGGING/让MyBatis在控制台输出完整的SQL日志方便边跑边看执行过程。?xml version1.0 encodingUTF-8 ? !DOCTYPE configuration PUBLIC -//mybatis.org//DTD Config 3.0//EN https://mybatis.org/dtd/mybatis-3-config.dtd configuration settings !-- 开启驼峰命名自动映射 -- setting namemapUnderscoreToCamelCase valuetrue/ !-- 打印SQL到控制台 -- setting namelogImpl valueSTDOUT_LOGGING/ /settings /configuration2.4 商品搜索和分页一个典型的动态SQL案例商城的商品列表页是动态SQL需求量最大的地方。用户可能按分类选、可能按关键词搜、可能要求按价格升序、还可能组合起来做多条件筛选。如果把每种可能性都写一个SQL方法那Mapper接口会膨胀得没法看。MyBatis的if标签就是专门解决这个问题的。select idselectByCondition parameterTypemap resultTypecom.snack.entity.Product SELECT id, name, description, price, stock, sales, image, category_id, status, created_time FROM product where if testcategoryId ! null AND category_id #{categoryId} /if if testkeyword ! null and keyword ! AND name LIKE CONCAT(%, #{keyword}, %) /if if testminPrice ! null AND price gt; #{minPrice} /if if testmaxPrice ! null AND price lt; #{maxPrice} /if AND status 1 /where choose when testorderBy priceAsc ORDER BY price ASC /when when testorderBy priceDesc ORDER BY price DESC /when when testorderBy sales ORDER BY sales DESC /when otherwise ORDER BY id DESC /otherwise /choose LIMIT #{offset}, #{pageSize} /select注意where标签会自动去掉第一个AND所以要保证每个条件前面都带AND这是MyBatis动态SQL使用中非常容易犯的错。choose则对应Java里的switch用来做排序方式的单选。价格比较要用gt;和lt;因为XML里裸的和会被解析器当成标签符号直接报错。分页这里我没有引入PageHelper插件而是手动计算offset (currentPage - 1) * pageSize再写一个countByCondition查询总数。两个SQL执行一个取总数、一个取列表数据总共不到20行代码。PageHelper的原理本质上也是拦截器帮你自动拼LIMIT但自己手写一次之后你会彻底理解分页的数学逻辑——先算偏移量再拼SQL最后把总记录数、总页数、当前页数封装成一个PageResult返回给前端。3. 前端Vue3商城页面实现3.1 工程创建与核心配置后端接口写完用Postman测过了再开始写前端。Vue3项目的创建我用的是Vite命令很简单npm create vitelatest snack-mall-web # 选择vue选择JavaScript也可以选TypeScript不过这个项目用JS更省事 cd snack-mall-web npm install npm install vue-router4 axios element-plus依赖装完后我先过一遍main.js注册路由、挂载Vue实例、注册Element Plusimport { createApp } from vue import App from ./App.vue import router from ./router import ElementPlus from element-plus import element-plus/dist/index.css const app createApp(App) app.use(router) app.use(ElementPlus) app.mount(#app)项目里我特意用到了Vue Router因为商城需要多个页面互跳首页、分类页、商品详情页、购物车页、订单页面。在路由文件里给页面路径一一配好import { createRouter, createWebHistory } from vue-router const routes [ { path: /, name: Home, component: () import(../views/Home.vue) }, { path: /category/:id, name: Category, component: () import(../views/Category.vue) }, { path: /detail/:id, name: ProductDetail, component: () import(../views/ProductDetail.vue) }, { path: /cart, name: Cart, component: () import(../views/Cart.vue) }, { path: /orders, name: Orders, component: () import(../views/Orders.vue) }, { path: /login, name: Login, component: () import(../views/Login.vue) }, ] const router createRouter({ history: createWebHistory(), routes, }) export default router3.2 用Composition API封装商品列表逻辑Vue3里最核心的思维转变是从Option API到Composition API。同样的商品列表功能如果还用Vue2的写法数据、计算属性、方法分散在data、computed、methods三块而Vue3把逻辑写在一起像写普通函数一样。我在商品分类页就是直接复用卡片组件数据请求通过fetchProductList这个函数根据当前路由参数区分加载逻辑。script setup import { ref, reactive, onMounted, watch } from vue import { useRoute } from vue-router import axios from axios const route useRoute() const productList ref([]) const loading ref(false) const currentPage ref(1) const total ref(0) const keyword ref() const sortType ref(default) async function loadProducts() { loading.value true const categoryId route.params.id const params { page: currentPage.value, pageSize: 12, categoryId: categoryId, keyword: keyword.value, orderBy: sortType.value, } const { data } await axios.get(/api/product/list, { params }) productList.value data.list total.value data.total loading.value false } onMounted(loadProducts) watch([currentPage, () route.params.id, keyword, sortType], loadProducts) /script这段代码常见的一个坑是watch监听的是route.params.id如果直接监听route对象任何路由变化都会触发加载不符合分类切换的实际需求。另一个坑是每次切分类时要把keyword清空sortType复位否则查询条件会串味。我后来加了一个watch回调用currentPage.value 1的方式做了兜底确保新查询从第一页开始。3.3 Axios封装与接口联调商城项目里所有涉及后端接口的地方都要发HTTP请求。如果每个组件都直接import axios改个baseURL得翻遍所有文件。最佳实践是封装一个request.jsimport axios from axios import { ElMessage } from element-plus const request axios.create({ baseURL: /api, timeout: 10000, }) // 请求拦截器带上session凭证 request.interceptors.request.use(config { // 拿token或sessionId加到header里 const token sessionStorage.getItem(token) if (token) { config.headers.Authorization token } return config }) // 响应拦截器统一处理错误提示 request.interceptors.response.use( response { const res response.data if (res.code ! 200) { ElMessage.error(res.msg || 请求失败) return Promise.reject(new Error(res.msg)) } return res }, error { ElMessage.error(error.message) return Promise.reject(error) } ) export default request前后端联调时有一件事必须提前处理跨域。我是用Vite的proxy代理解决的在vite.config.js里配置import { defineConfig } from vite import vue from vitejs/plugin-vue export default defineConfig({ plugins: [vue()], server: { port: 5173, proxy: { /api: { target: http://localhost:8080/, changeOrigin: true, rewrite: path path.replace(/^\/api/, ), }, }, }, })这里的原理是浏览器发的请求由Vite开发服务器先接住再由它转发到后端的8080端口因为这不是浏览器直接发出去的所以不会触犯同源策略。开发完成后打包前端生成的静态文件丢到Nginx里再在Nginx配置一个反向代理转发/api就可以了。3.4 购物车与结算流程的交互设计购物车页面是整个商城交互逻辑最重的一个功能。用户能勾选商品、改数量、删除每次操作都要同步更新小计和总价。Vue3的computed计算属性在这里用得最多script setup import { ref, computed } from vue import request from ../utils/request const cartItems ref([]) // 已选中的商品 const checkedItems computed(() { return cartItems.value.filter(item item.checked) }) // 总价格 const totalPrice computed(() { return checkedItems.value.reduce( (sum, item) sum item.price * item.quantity, 0 ).toFixed(2) }) // 数量加减 async function updateQuantity(item, delta) { if (item.quantity delta 0) return item.quantity delta await request.post(/cart/updateQuantity, { cartItemId: item.id, quantity: item.quantity, }) } // 提交订单 async function submitOrder() { const res await request.post(/order/create, { cartItemIds: checkedItems.value.map(item item.id), receiverName: receiverInfo.value.name, receiverPhone: receiverInfo.value.phone, receiverAddress: receiverInfo.value.address, }) if (res.code 200) { ElMessage.success(下单成功) // 跳转到订单列表页 router.push(/orders) } } /script这里有三个细节值得说。第一toFixed(2)返回的是字符串后端的DECIMAL类型在JSON序列化后也可能是字符串前端如果需要再参与计算要先parseFloat不然0.2 0.1这种浮点数精度问题会让价格显示变成0.30000000000000004。第二前端修改数量要和后端同步时最好只传cartItemId和quantity不要把整个购物车对象传过去减少无效数据传递也能避免无意义的报错。第三提交订单前需要把选中的商品id数组一起传给后端后端拿到这批id再去查询价格、计算总价千万不能直接信任前端传的金额。前端传totalAmount只作为一个展示参考最终价格以后端计算为准这也是电商系统的基本安全红线。4. MyBatis进阶与常见坑4.1 一级缓存、二级缓存到底在缓存什么项目里我用到了MyBatis的查询缓存机制这是面试官非常爱问的一块。MyBatis的缓存分两级一级缓存默认开启作用域是SqlSession也就是说同一个SqlSession中执行两条相同的SQL必须参数也相同第二条直接走缓存不会真正查数据库。二级缓存需要手动开启作用域是Mapper的namespace多个SqlSession之间可以共享。但二级缓存在分布式场景下会带来问题。如果项目部署了多个实例不同实例之间的缓存互不相通一个实例改了数据另一个实例的缓存还留着旧数据就会出现“缓存雪崩前的黎明”——用户看到的数据一会儿新一会儿旧。而且MySQL的InnoDB自身有Buffer PoolMyBatis的缓存机制更适合少量热点数据的场景。在商城系统里我会对商品详情这种读多写少的数据开启二级缓存对库存这类必须实时准确的数据绝不缓存。配置文件里就一句话setting namecacheEnabled valuetrue/再在对应的Mapper XML文件里加一行cache/。建议最多做到这一步不要再追求更复杂的自定义缓存策略项目资料里能把一级缓存、二级缓存、何时失效、脏数据问题讲明白已经比大部分候选人强了。4.2${}和#{}的区别一次SQL注入的教训开发中踩过最疼的一个坑是排序字段的动态拼接。最开始为了图省事把前端传的orderBy直接用${}拼到SQL里比如ORDER BY ${orderBy}前端传priceAsc还好但如果被别有用心的人传个id; DROP TABLE product; --那这条SQL就变成了多条语句直接执行后果不堪设想。这就是典型的SQL注入。MyBatis中#{}是预编译的参数会通过PreparedStatement的占位符传进去值被MySQL驱动做了转义天然防注入而${}是字符串拼接把内容原样拼到SQL里有注入风险。那为什么还要用${}因为它可以拼表名、列名和关键字比如ORDER BY后面这个列名没法用占位符替代。安全做法是做一个白名单校验前端传orderBy后端只允许它等于几个预设值之一不在列表里就默认按id DESC。String orderBy request.getParameter(orderBy); if (priceAsc.equals(orderBy) || priceDesc.equals(orderBy) || sales.equals(orderBy)) { // 允许传入拼到SQL里 } else { orderBy id; }4.3 多条件批量插入与库存扣减“加入购物车”和“下单”都有批量操作需求。批量插入购物车项如果循环单条插入一次加10件零食就要执行10遍数据库网络IO性能很差。MyBatis的foreach标签可以把多条数据拼接成一条insert语句insert idbatchInsertCartItems INSERT INTO cart_item (user_id, product_id, quantity, checked) VALUES foreach collectionlist itemitem separator, (#{item.userId}, #{item.productId}, #{item.quantity}, 1) /foreach /insert同时用ON DUPLICATE KEY UPDATE处理已存在记录的场景。SQL变成INSERT INTO cart_item (user_id, product_id, quantity) VALUES (...) ON DUPLICATE KEY UPDATE quantity quantity 1;这样就不用先查再决定新增还是更新一步到位。唯一索引已经建好了所以重复的(user_id, product_id)会触发更新分支把数量加1。这个写法我曾不小心踩过一次“数量翻倍”的坑第一次执行时插入数量2但ON DUPLICATE KEY UPDATE又加了一次2结果购物车里变成了4件。后面改进为前端只传“要增加几个”而不是“现在应该有几个”彻底规避了这个问题。订单提交时的库存扣减也是一样的道理用原子性SQL比先查再改更安全UPDATE product SET stock stock - #{quantity}, sales sales #{quantity} WHERE id #{productId} AND stock #{quantity}WHERE stock #{quantity}这个条件保证不会在库存不足时仍然执行扣减同时数据库的行锁保证了并发情况下不会超卖。如果update影响的行数为0就说明库存不够回滚整个订单。这种写法比在Java代码里先查库存再判断再更新的做法更可靠因为从“查”到“改”之间可能有并发时间差而数据库行锁则天然解决了问题。5. 面试高频追问与项目复盘5.1 项目介绍怎么说才不像背稿面试的时候在“请介绍一个你最熟悉的项目”环节很多人的回答是“我开发了一个零食商城前端用Vue3后端用Spring MVC数据库用MySQL”。这种介绍在面试官耳朵里等于什么都没说。我建议的讲述框架是业务场景→技术难点→解决方案→你的思考。对于这个项目一段比较好的自述开头是“我做的是一个面向C端用户的零食电商系统核心流程是用户注册登录、浏览商品、加入购物车、下单支付。后端我用Spring MVC做分层架构Controller负责接收参数和返回JSONService做业务编排Mapper层用MyBatis处理数据库操作。前端用Vue3开发。这个项目里我遇到两个比较棘手的问题第一个是商品列表的多条件动态SQL拼接因为搜索条件可能是分类、关键词、价格区间任意组合所以我在MyBatis的XML里用where、if、choose标签做了一个通用查询方案第二个是下单时的库存扣减我通过SQL语句加条件判定的方式保证了高并发场景下不会超卖。”这一段介绍里没有任何一句空话每一句都指向具体的技术点和解决方案。面试官听完基本就能判断出这个项目是真实写过的而不是照着网上视频敲了一遍。5.2 MyBatis、Vue3、Spring MVC的高频提问清单围绕这个项目面试官最常问的问题我整理了一张速查表每道题都可以从你自己项目的具体实现出发回答提问方向高频问题从本项目出发的回答要点MyBatis#{}和${}有什么区别本项目排序字段用${}但做了白名单校验其他参数一律用#{}说清楚预编译和字符串拼接的区别MyBatis一级缓存和二级缓存分别是什么本项目二级缓存开在商品详情这类读多写少的Mapper上库存这种实时性要求高的不缓存MyBatisMyBatis接口怎么找到XML里对应的SQLMapper接口的全限定名就是XML文件里namespace的值方法名就是SQL语句的idMyBatisresultType和resultMap有什么区别本项目开了mapUnderscoreToCamelCase所以大部分用resultType但订单关联用户这类查询字段不对应时用了resultMapSpring MVC一次请求从进入到Controller的过程DispatcherServlet→HandlerMapping→HandlerAdapter→Controller→返回值处理→视图解析或ResponseBody直接JSONSpring MVC拦截器和过滤器的区别本项目用拦截器校验后台管理员的登录状态过滤器是Servlet层面拦截器是Spring MVC层面Vue3Composition API对比Option API本项目购物车页面用ref、computed、watch管理状态对比Vue2的data/methods逻辑内聚性更好Vue3ref和reactive怎么选本项目简单类型和数组用ref处理包含嵌套属性的复杂对象时才用reactiveVue3Vue路由守卫做什么本项目用路由守卫让未登录用户访问购物车页时自动跳转登录页表格里这些问题如果你能顺着自己真实用过的场景回答给面试官的画面感是最强的。最好的状态不是你背好了标准答案而是面试官问到任何细节你都能接上“这个点我在项目里确实处理过是因为……”。5.3 把“老技术栈”讲成“基础扎实”而不是“跟不上时代”Spring MVC和MyBatis确实不是最新的技术但面试官不会因为你用了老技术就否定你。关键在于你如何解释自己的选型。我的应对策略是主动承认“我知道Spring Boot和MyBatis-Plus目前更主流但我选择Spring MVC和原生MyBatis是有意为之因为我希望把Spring MVC的请求分发链路、MyBatis的SQL与Java映射这些底层机制亲手验证一遍而不是被自动配置挡住”。这个说法既诚实又有分寸。面试官听得出来你不是不会Spring Boot而是选择了先扎实底层再上手框架。如果能顺带说一句“我的项目里前后端交互已经完全是前后端分离的思路Controller都返回JSON和Spring Boot的接口写法几乎一样Spring Boot对我是迁移成本而不是学习成本”这波回答基本就稳了。6. 演示视频录制与答辩展示技巧6.1 视频脚本设计90秒抓住注意力不少同学录项目演示视频开头就是打开IDEA然后开始敲键盘录了20分钟代码。这其实是演示视频最容易翻车的做法。作为一个看过大量同类视频的人我强烈建议你按照“场景式演示脚本”来录。整段视频控制在8到10分钟分四个阶段第一阶段0:00-1:30项目概览。屏幕上先展示项目的核心页面——商城首页、商品列表、购物车、订单配一句简短介绍“这是一个基于Spring MVC和MyBatis的网上食品零食商城系统前端使用Vue3开发支持用户浏览商品、加入购物车、模拟支付下单以及后台商品管理。”第二阶段1:30-4:00用户主流程演示。从首页进入商品详情页加入购物车修改数量提交订单到支付成功。这一步要一次走通不要出岔子。第三阶段4:00-6:30后台管理演示。管理员登录编辑商品、调整库存、查看订单状态。第四阶段6:30-8:00亮点展示。展示项目里最能体现技术含量的功能比如商品搜索动态SQL、数据库表结构、核心代码片段。这里才是体现“你真实做过”的关键。我录视频有个祖传习惯录之前先自己从头到尾走一遍流程把每一步的操作方式、页面变化都写下来。比如“点击分类‘坚果炒货’页面列表出现8个商品第一条是‘每日坚果’”这个细节脚本能保证录的时候不迷路也能让观众跟着你的节奏走。6.2 实操演示数据准备和“演员道具”演示翻车多半是数据问题。我建议在展示之前专门准备一份“演示数据包”10个一级分类每个分类下3到5个商品商品图片可以直接用真实零食品牌的官网图或者电商素材图。购物车里提前放两三件商品。订单列表里准备几个不同状态待付款、已完成、已发货的订单。这样演示的时候无论点到哪个页面界面都是丰满的观众不用看你的“空壳”。后台管理员的账号密码也要提前测好不要到操作的时候才发现密码不对。如果演示过程担心网络加载图片太慢可以提前把图片文件放到本地静态资源目录或者用压缩后的图片断网也不影响展示。6.3 录屏软件选择与基本功录屏工具我用过几种最终固定用的是国内网络环境下比较稳定的OBS Studio免费开源、无时长限制、画质也好。配置很简单设置 - 输出 - 录像格式 - MP4 设置 - 视频 - 输出分辨率 - 1920x1080 画面源 - 显示器采集 - 选择你要录制的屏幕另外强烈建议同时录麦克风这样你能边操作边讲解。如果出水不方便宁愿后期配字幕也不要静默操作。观众看视频的注意力是很容易涣散的你的讲解声是控制节奏的唯一抓手。录制前先在后台把无关的软件关掉尤其是需要联网的通讯软件避免录制过程中弹出消息通知那画面在汇报场合相当尴尬。6.4 演示过程中的“翻车急救方案”即使准备工作再充分也可能遇到临场意外。最常见的三种故障对应的急救方案如下页面加载不出来——按F5刷新前先确认前端静态资源服务器是否有异常。如果还不行八成是后端服务挂了赶紧去启动Tomcat属于那种必须提前想好的事情。如果演示中途真出问题了千万不要在视频里长时间沉默处理。最稳妥的做法是关掉录制、修好之后重新录而不是硬扛着把故障展示给观众。数据库连不上——先在Navicat验证一下数据库连接是否正常。解决方法是把数据库服务设置为开机自启演示前启动好。千万别在演示时现场改配置文件。页面样式错乱——多数是因为本地开发模式下的资源加载问题。项目构建打包后的静态文件放到Nginx后要先在浏览器里完整走一遍核心流程确认一切正常再开始录。Nginx下404是因为路由用了createWebHistory模式刷新子页面路径找不到对应的静态文件需要在Nginx配置里加上try_files $uri $uri/ /index.html;这个坑我踩过对方说“刷新页面变白屏”问题的根源就在这里。7. 认认真真总结一下做这个项目的过程中我最大的体会是技术选型老不老不是最关键的最关键的是你在这个项目里真正掌握了多少底层原理能不能把每一个实现细节讲得清清楚楚。Spring MVC和MyBatis这两套技术虽然年纪不小但它们的核心思想——请求分发、控制反转、ORM映射、动态SQL——是整个Java后端体系的基石。用它们做完一个完整的商城系统再上一个Vue3做前后端分离的实践这其实是性价比很高的学习路径。最后再分享一个小技巧做演示视频时不要只录操作过程最好在关键节点按下暂停用箭头和文字框标注出当前这个操作背后的技术点。比如你在商品列表页输入关键词搜索暂停一下在旁边标注“这里调用了MyBatis的动态SQL通过if标签完成了多条件模糊查询”。这样做出来的视频既有操作展示又有技术讲解比单纯录操作过程有价值得多。这个项目后续能扩展的方向也不少接入Redis做热点商品缓存和Session共享、用Spring Security做更完整的权限控制、把前端打造成真正的管理后台前端框架、引入消息队列做订单超时自动取消。但这些都是后话先把当前的Spring MVC MyBatis Vue3整条链路跑通把它变成你脑子里真正能复述、能改编、能应对面试追问的东西再想别的。

关于恒美微站

恒美微站专注于为个体商户、工作室提供极简自助建站服务,让每个人都能轻松拥有专业网站。

快速链接

  • 关于我们
  • 建站服务
  • 主题模板
  • 案例展示
  • 资讯中心

服务项目

  • 可视化建站
  • 拖拽编辑
  • 主题定制
  • SEO 优化
  • 网站托管

联系方式

  • 📍 地址:北京市朝阳区建国路 88 号
  • 📞 电话:400-888-8888
  • ✉️ 邮箱:info@hmyw.cn
  • 🕐 时间:周一至周日 9:00-18:00

© 2024 恒美微站 hmyw.cn 版权所有 | 京 ICP 备 12345678 号