ARTICLE DETAIL

资讯详情

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

校园商铺管理系统:SpringBoot2+Vue3+MyBatis-Plus全栈实战解析

校园商铺管理系统:SpringBoot2+Vue3+MyBatis-Plus全栈实战解析 搞校园商铺这种前后端分离的管理系统其实最怕的不是功能多复杂而是技术栈选得“看起来新、用起来坑”。我见过太多人一上来就追SpringBoot 3.x JDK 17结果第三方依赖一堆不兼容折腾两天还没跑起来。这套项目反手选了SpringBoot2 Vue3 MyBatis-Plus MySQL8.0的组合乍一看可能觉得“不够最新”但真正上手跑通的人会明白这套组合是目前国内中小型管理系统、课程设计、毕业设计里最稳的梯队之一。本文会把这套项目从数据库设计、后端鉴权、分页插件、文件上传、前端路由守卫到联调部署整个链路拆开讲适合正在做毕设的在校生、想快速搭一套管理后台的初级工程师以及所有想搞明白这几个技术点怎么协作的读者。1. 为什么是这套技术栈校园商铺项目的选型逻辑与版本抉择很多同学拿到一个“校园商铺管理系统”的需求第一反应是去搜最新的框架版本但真正写业务代码的时候才发现框架的新旧远没有生态的成熟度重要。这里把我为什么坚持用SpringBoot2 Vue3 MyBatis-Plus MySQL8.0这套组合的考虑写清楚你看完再决定要不要照搬。1.1 SpringBoot2与SpringBoot3不是越新越好用SpringBoot3在2022年底正式发布确实带来了AOT编译、Jakarta EE命名空间、GraalVM原生镜像这些新特性但它最大的代价是必须JDK17起步。如果你的电脑装的是JDK8或者你需要在Windows Server 2012这种老环境上部署那SpringBoot2仍然是更实际的选择。SpringBoot2.x的最后一个稳定小版本是2.7.x它已经积累了大量生产环境的踩坑修复网上随便一搜就能找到各类异常的解决方案。更重要的是SpringCloud Alibaba、ShardingSphere、XXL-Job这些国内常用组件对SpringBoot2的兼容性测试最多你用起来遇到问题基本都有人趟过路。在这个项目里SpringBoot2.7.x JDK8的组合完全可以撑起校园商铺的并发量——校园商铺的定位本身就是学生和教职工群体峰值在线人数撑死几百人订单量一天几千笔这个量级连数据库连接池的极限都没摸到。提示选型从来不是选“最新的”而是选“最适合当前场景且周围资料最丰富的”。工作后你会发现很多老项目的生产环境还停在SpringBoot1.5但项目一样跑得稳稳的。1.2 MyBatis-Plus凭什么替代传统MyBatis传统MyBatis写SQL很灵活但单表CRUD你得写一堆重复的XML mapper比如insert、updateById、selectList、deleteById百分之八十的单表操作代码长得一模一样纯粹是体力劳动。MyBatis-Plus相当于在MyBatis之上叠了一层通用Mapper和通用Service。你定义一个BaseMapperT接口继承它之后单表的CRUD方法全都自动有了你只需要关注那些真正复杂的多表关联查询和统计报表SQL。在校园商铺项目里商品表的增删改查、用户表的分页列表、订单表的条件查询这些全是单表操作MyBatis-Plus的selectPage、selectList配合LambdaQueryWrapper能干掉大概一半的Mapper代码量。你要是用原生的MyBatis写这些代码会淹没你真正需要写的核心业务逻辑。1.3 Vue3组合式API与Vue2选项式的实际体验差异Vue3发布已经好几年了Vite的启动速度和热更新体验对比Webpack时代简直是降维打击。用Vue3写后台管理系统最大的变化是逻辑复用从Mixin转向了组合式函数。Vue2时代做登录态管理你得在created生命周期里写一堆数据初始化逻辑换个页面还得重新复制一遍。Vue3里用setup语法糖加ref、reactive、computed一个useUserStore组合函数就能把用户信息获取、权限校验、退出登录全部封装起来哪个组件需要就直接调用。Element Plus是Element UI的Vue3版本坑的地方在于部分组件API改动比较大比如el-dialog的visible.sync变成了v-modelel-table的slot-scope变成了#defaultscope。但这些问题你只要先跑通一个最小Demo基本都知道怎么回事了。2. 校园商铺核心数据模型设计从需求到建表的完整映射校园商铺系统表面上是个“电商系统”但它的业务边界比淘宝京东小得多核心就四个角色管理员、商户、普通用户、订单。把这四块的数据模型理清楚后端代码和前端页面才能有据可依。2.1 商户、商品、订单、用户四张主表如何落库我习惯先画出一张“业务数据流”用户注册登录→浏览商户列表→查看某个商户的商品→下单→商户处理订单。这张图决定了你的主表至少有四条user、merchant、product、order。用户表字段不算多但要注意区分普通用户和管理员我是用一个role字段1-普通用户2-商户管理员3-超级管理员来区分的别拆成两张表因为登录逻辑和密码字段完全一致。商品表要关联商户ID价格字段用DECIMAL(10,2)存千万别用FLOAT浮点数算金额会出现0.10.20.30000000000000004这种问题。库存字段用INT同时加一个status字段控制上下架状态。订单表是核心字段比较杂订单编号、用户ID、商户ID、商品ID、数量、总金额、收货地址、支付方式、订单状态。这里有一个设计细节订单编号不要用数据库自增ID业务上需要展示给用户看的单号我建议用时间戳加随机数生成比如yyyyMMddHHmmss加四位随机数避免用户猜出订单总量。2.2 状态字段与枚举映射避免魔法数字满天飞很多新手写订单状态喜欢直接用数字0、1、2硬编码写到后面自己都忘了1代表什么。我会在项目里建一个OrderStatusEnum把状态定义成可读性强的枚举并在MyBatis-Plus的实体字段上写好对应的注释。public enum OrderStatusEnum { UNPAID(0, 待付款), PAID(1, 已付款), SHIPPED(2, 已发货), COMPLETED(3, 已完成), CANCELLED(4, 已取消); private final int code; private final String desc; OrderStatusEnum(int code, String desc) { this.code code; this.desc desc; } }实体类映射上用EnumValue注解让MyBatis-Plus自动做枚举和数据库数字之间的转换代码里写的是OrderStatusEnum.PAID数据库里存的是1既保证可读性又保证存储效率。MySQL8.0相比5.7有几点需要注意一是字符集默认是utf8mb4而不是utf8建表时用utf8mb4_0900_ai_ci排序规则不然emojis存不进去二是WITH子句CTE这回真的得到了良好支持写复杂统计SQL能省很多事。3. SpringBoot2后端骨架搭建认证、分页、上传一个都不能少数据模型设计好后就该搭后端骨架了。一套成熟的后端项目核心就三块登录鉴权怎么做、列表分页怎么查最爽、文件上传怎么存怎么取。这三块搞定其他都是堆接口。3.1 JWT登录校验的拦截器实现与白名单设计前后端分离项目的登录态管理主流方案是JWT。用户登录成功后后端签发一个Token返回给前端前端每次请求都把它塞进请求头的Authorization里后端拦截器统一校验。这个方案的好处是服务端无状态换服务器不用同步Session适合部署在轻量级云服务器环境。这套项目拦截器的核心逻辑是先判断请求路径是否在白名单里比如/api/user/login、/api/user/register如果在就直接放行。如果不在白名单解析请求头中的Token校验签名和有效期解析失败就返回401提示重新登录。Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 放行预检请求 if (OPTIONS.equals(request.getMethod())) { return true; } String token request.getHeader(Authorization); if (StringUtils.hasText(token) token.startsWith(Bearer )) { token token.substring(7); try { Claims claims JwtUtil.parseToken(token); request.setAttribute(userId, claims.get(userId)); return true; } catch (Exception e) { // Token无效返回401 } } response.setStatus(401); response.setContentType(application/json;charsetUTF-8); response.getWriter().write({\code\:401,\msg\:\未登录或登录已过期\}); return false; }这里有一个必须注意的坑跨域请求时浏览器会先发一个OPTIONS预检请求这个请求不会携带自定义请求头如果你在拦截器里直接拿Authorization去解析一定会报空指针。所以拦截器里第一步必须先放行OPTIONS方法否则前端所有请求都会死在CORS阶段。3.2 MyBatis-Plus分页插件配置从写SQL到一条selectPageMyBatis-Plus的分页和传统MyBatis最大的区别不用写LIMIT ? , ?不用写select查询总条数你只需要配置一个分页拦截器然后调selectPage方法。在配置类里注入MybatisPlusInterceptor并添加PaginationInnerInterceptor。这里配置的数据库类型要写成DbType.MYSQL因为Oracle、PostgreSQL的分页语法各不相同MyBatis-Plus需要知道你用的哪种数据库才能生成正确的分页SQL。Configuration public class MybatisPlusConfig { Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } }然后在Service层调用PageProduct page new Page(current, size); LambdaQueryWrapperProduct wrapper new LambdaQueryWrapper(); wrapper.eq(Product::getMerchantId, merchantId); wrapper.orderByDesc(Product::getCreateTime); productMapper.selectPage(page, wrapper);注意Page对象当传入current和size后MyBatis-Plus会自动查询总数并计算总页数你不用自己再写一个count(*)查询。返回值里getRecords()拿当前页数据getTotal()拿总记录数前端分页组件直接绑定这两个字段即可。3.3 文件上传与静态资源映射商铺图片的存取方案商户入驻后要传店铺Logo、商品要传展示图这属于典型的文件上传场景。本地存储方案是最适合这一项目的——直接存在服务器某个目录下然后通过SpringBoot的静态资源映射暴露给前端。在application.yml里配置一个自定义上传路径比如D:/upload/或者Linux下的/data/shop/upload/然后写一个通用文件上传接口PostMapping(/upload) public ResultString upload(RequestParam(file) MultipartFile file) { if (file.isEmpty()) { return Result.error(上传文件不能为空); } String originalFilename file.getOriginalFilename(); String suffix originalFilename.substring(originalFilename.lastIndexOf(.)); // 重命名文件防止重名覆盖 String newFileName UUID.randomUUID().toString().replace(-, ) suffix; File dest new File(uploadPath newFileName); try { FileUtils.copyInputStreamToFile(file.getInputStream(), dest); return Result.success(/files/ newFileName); } catch (IOException e) { log.error(文件上传失败, e); return Result.error(文件上传失败); } }文件重命名为UUID是必须的操作不然用户上传两个同名文件会互相覆盖。然后配置WebMvcConfigurer把/files/**映射到本地磁盘目录Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler(/files/**) .addResourceHandler(/files/**) .addResourceLocations(file: uploadPath); }前端拿到返回的URL直接拼在图片地址上就能访问了。这里的坑点在于Windows和Linux的路径分隔符不同写配置文件时最好用相对路径或者做了兼容处理不然部署到服务器上图片全部加载不出来。4. Vue3Element Plus前端模块登录态、动态路由与数据表格后端骨架搭好后前端这边的活儿也不少。Vue3的项目组织方式和Vue2有挺大区别特别是路由守卫、状态管理、网络请求这三块处理不好项目越写越乱。4.1 Vite项目初始化与目录结构创建一个Vue3项目我用的是Vite命令很简单npm create vitelatest shop-admin -- --template vue不是非要用pnpm但建议把依赖装完后把目录按功能划分好我习惯的结构是src/ ├── api/ // 接口请求定义 ├── assets/ // 静态资源 ├── components/ // 公共组件 ├── router/ // 路由配置 ├── stores/ // Pinia状态管理 ├── utils/ // 工具函数axios封装在这里 ├── views/ // 页面组件 ├── App.vue └── main.js很多新手喜欢把所有请求直接写在组件里当时确实爽但页面一多你就发现接口地址到处散落有个接口字段要改得全局搜索。把接口按模块拆分放到api目录每个页面对应一个JS文件维护成本低得多。4.2 axios实例封装与组合式函数调用Axios封装的核心是统一处理请求头、统一拦截错误。我在utils/request.js里创建一个axios实例设置baseURL、超时时间并在请求拦截器里自动带Tokenimport axios from axios const request axios.create({ baseURL: /api, timeout: 10000 }) request.interceptors.request.use(config { const token localStorage.getItem(token) if (token) { config.headers.Authorization Bearer ${token} } return config }) request.interceptors.response.use( response { const res response.data if (res.code ! 200) { ElMessage.error(res.msg || 请求失败) if (res.code 401) { router.push(/login) } return Promise.reject(new Error(res.msg)) } return res }, error { ElMessage.error(error.message || 网络异常) return Promise.reject(error) } )注意baseURL我用的是/api然后在Vite配置文件里配devServer的代理把/api转发到后端的localhost:8080这样前端开发时不会遇到跨域问题部署时也能直接交给Nginx统一转发。4.3 Element Plus表格分页组件联动后台管理系统用得最多的场景就是表格列表Element Plus的el-table配合分页组件其实有一套固定写法。核心思想是所有查询条件都绑定到一个查询对象上点击搜索或切换页码时调用同一个查询方法。template div el-form :inlinetrue el-form-item label商品名称 el-input v-modelqueryForm.name placeholder请输入商品名称 clearable / /el-form-item el-form-item el-button typeprimary clickhandleSearch查询/el-button el-button clickhandleReset重置/el-button /el-form-item /el-form el-table :datatableData border stripe v-loadingloading el-table-column propid labelID width80 / el-table-column propname label商品名称 / el-table-column propprice label价格 / el-table-column propstock label库存 / el-table-column label操作 width200 template #defaultscope el-button sizesmall clickhandleEdit(scope.row)编辑/el-button el-button sizesmall typedanger clickhandleDelete(scope.row)删除/el-button /template /el-table-column /el-table el-pagination v-model:current-pagequeryForm.current v-model:page-sizequeryForm.size :totaltotal :page-sizes[10, 20, 50, 100] layouttotal, sizes, prev, pager, next, jumper size-changegetList current-changegetList / /div /template这里有个Vue3的版本差异el-pagination用了v-model:current-page和v-model:page-size这是Element Plus针对Vue3的写法老写法current-page.sync在Vue3里已经不适用。5. 联调与部署阶段的坑MySQL8.0时区、跨域、驼峰映射后端和前端都写完真正痛苦的阶段才开始。联调和部署中遇到的环境问题比业务代码本身更磨人。我把这套项目里最容易踩的三个坑提前列出来你能避开就避开。5.1 MySQL8.0驱动与serverTimezone的折腾MySQL8.0的JDBC驱动从com.mysql.jdbc.Driver改成了com.mysql.cj.jdbc.Driver连接URL里还强制要求加serverTimezone参数不然会报The server time zone value Öйú±ê׼ʱ¼ä is unrecognized。spring: datasource: url: jdbc:mysql://localhost:3306/shop_db?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/ShanghaiallowPublicKeyRetrievaltrue username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.DriverallowPublicKeyRetrievaltrue这个参数是在MySQL8.0下用caching_sha2_password认证插件时必须的不然你用Navicat连上了但Java连接时会报Public Key Retrieval is not allowed。密码加密方式最好在创建用户时指定成mysql_native_password减少一堆莫名的认证问题。另外一个容易忽略的点MySQL8.0的utf8mb4是真正的四字节UTF-8MySQL5.7里的utf8本质是utf8mb3存不了emoji。如果你在MySQL8.0的建表语句里直接写CHARSETutf8虽然也能创建成功但遇到特殊字符照样炸统一用utf8mb4最稳。5.2 跨域配置的三种写法与正确姿势前端Vite默认端口5173后端SpringBoot端口8080两者开发环境必然要解决跨域。这里的跨域问题其实分两层前端的代理转发和后端的CORS配置。开发环境最省事的方案是在Vite配置里做代理前端代码里所有请求都走相对路径/apiVite把它转发到后端这种情况下浏览器看到的请求是同源的根本不会有跨域问题。// vite.config.js export default defineConfig({ server: { port: 5173, proxy: { /api: { target: http://localhost:8080, changeOrigin: true, rewrite: path path.replace(/^\/api/, ) } } } })生产环境部署时Nginx反向代理也是类似的配置逻辑把/api转发给Java进程。如果某些特殊场景必须走前端直连后端那你才需要后端加CrossOrigin或CORS配置类。注意跨域配置类里allowedOriginPatterns要写成*别用allowedOrigins(*)因为这方法在携带凭证的情况下会直接被浏览器拒绝。5.3 逻辑删除与唯一索引冲突的连锁问题MyBatis-Plus默认支持逻辑删除在实体字段上加TableLogic注解删除时自动执行UPDATE SET deleted1 WHERE id?SELECT时自动拼接AND deleted0。这个机制本身没问题但配合数据库唯一索引时会出现一个经典bug。假设商品表给product_code字段加了唯一索引。用户删除了商品A它的deleted变成了1但记录还在表里。这时新增一个商品Bproduct_code又恰好和A一样数据库直接报Duplicate entry。因为MySQL的唯一索引对逻辑删除的记录依然生效。解决方案有三种一是删除唯一索引让代码层面自己去查重不推荐二是唯一索引改成联合字段(deleted, product_code)MySQL不允许唯一索引里有两个NULL但deleted默认值为0不是NULL所以可以三是彻底物理删除。最稳妥的办法其实是在业务设计上避免这种冲突商品编码可以带上UUID或时间戳后缀从源头保证唯一性。逻辑删除字段作为兜底别指望它解决所有问题。6. 这套源码怎么用起来从导入到部署的完整动作清单说再多理论最后还是要落地上手。这里给一份从拿到源码到跑起来的完整操作顺序每一步都验证过照着做基本半小时内能启动。6.1 安装MySQL8.0并初始化数据库脚本Windows和Linux装MySQL8.0的流程不太一样。Windows下载msi安装包一路Next安装时选Server only编码选utf8mb4密码设为root方便本地调试。Linux的Debian系可以用apt直接装sudo apt update sudo apt install mysql-server-8.0 sudo systemctl enable --now mysql sudo mysql_secure_installation装好后用命令行或Navicat执行项目提供的shop_db.sql脚本这个脚本里包含建库、建表、插入初始数据。执行前检查一下SQL脚本开头的CREATE DATABASE语句确认编码是utf8mb4。6.2 导入后端Maven项目并修改配置后端项目用IDEA打开选择pom.xml作为Maven项目导入等依赖下载完成。然后改application.yml里的数据库账号密码spring: datasource: username: root password: 你的密码JDK版本确认是1.8Maven镜像如果下载慢在settings.xml里配置阿里云镜像。启动ShopApplication.java看到控制台输出Started ShopApplication in 5.32 seconds就说明后端起来了。6.3 启动前端Vue项目前端项目目录下执行npm install npm run dev如果npm install报错大概率是Node版本太老或太新Vite3要求Node14.18或16Node18是最稳的选择。启动成功后浏览器访问http://localhost:5173看到登录页说明前后端已经打通。6.4 生产环境的轻量部署参考开发环境跑通只是第一步真正部署时可以只用一台轻量云服务器搞定。后端打成jar包前端构建成静态文件Nginx配置如下server { listen 80; server_name your.domain.com; # 前端静态文件 location / { root /opt/shop-frontend/dist; index index.html; try_files $uri $uri/ /index.html; } # 后端API反向代理 location /api/ { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } # 上传图片访问 location /files/ { alias /opt/shop-upload/; } }这里try_files是给Vue的history路由用的保证前端路由刷新时不会404。图片路径单独配置alias别把上传目录暴露在静态资源根目录下安全性和可维护性都更好。7. 我实际用这套组合做项目时的几点体会最后说点不一定写进文档、但确实影響开发效率的体会。先说我踩过最大的坑MyBatis-Plus的老版本和SpringBoot2.7的兼容性问题。如果你在pom里引入了某个2.x早期的MyBatis-Plus依赖版本可能会出现启动实例化失败。这不是代码写错是版本冲突。建议直接用mybatis-plus-boot-starter的3.5.3版本这个版本适配SpringBoot2.7.x稳定我跑下来基本没出过幺蛾子。再说Vue3的开发体验。用习惯setup语法糖后确实回不去Vue2的Options API了但要注意reactive和ref的选择表单对象我习惯用reactive单独的基础类型变量用ref混用容易在模板里多个.value写错。模板里ref会自动解包reactive不会你只需要记住这个原则就行。还有一点关于JWT的时效设计。校园商铺里有“找回密码”“修改密码”这类操作Token有效期设太短会影响体验设太长又不安全。我通常会拆成两个参数一个短期访问Token2小时一个长期刷新Token7天。但这个项目是单体应用不搞微服务也要按这个思路去设计谁也不想用户在逛商铺逛到一半突然被踢回登录页。整套项目跑通后后续可以扩展的方向其实不少比如引入Redis做商铺缓存和Token黑名单、接入微信扫码支付、用WebSocket做订单状态实时推送。但那是后面的事了先把这套地基打牢往后想加什么功能都不慌。
返回列表