☰
校园商铺管理系统全栈实战:SpringBoot+Vue+MySQL+MyBatis设计与实现
2026/10/13 3:57:04 网站建设 项目流程

校园商铺管理系统这类项目,说白了就是给学校里的便利店、打印店、水果摊、咖啡屋这类小商户用的。我用SpringBoot+Vue+MySQL+MyBatis这套组合从头到尾做了一版,从数据库建表到前端页面,从登录鉴权到订单统计,全程自己撸完。这篇文章就把整个系统的设计思路、核心代码实现、以及我在实际开发中踩过的坑全部拆开讲一遍,适合正在做毕设、课设或者想入门全栈开发的读者直接参考。

1. 项目背景与整体设计思路

1.1 校园商铺管理到底要管什么

很多人一听"商铺管理系统"就以为是个简单的增删改查,实际上完全不是这么回事。我最初跟某高校的后勤部门沟通需求时,他们列出来的痛点非常具体:校园里的商铺数量多、类型杂,有的是学校自营,有的是外部承包,每个商铺的租期、租金、经营范围、营业执照有效期都不一样。之前全靠Excel表格管理,几十个商铺的合同信息、缴费记录、检查情况堆在一起,经常出现租金漏收、执照过期没人提醒的情况。

所以这个系统要解决的核心问题有三个:一是商铺基础信息的统一管理,二是租金和费用的流水记录与提醒,三是给学校管理层看的数据汇总。再往后延伸,还可以把店铺的日常报修、卫生检查、投诉处理都放进来,形成一个闭环。我在第一版里优先实现了前两块,第三块用简单的统计图表先撑起来,后面的报修和检查功能预留了表结构。

从角色权限上看,系统分三类用户:管理员、商铺经营者、财务人员。管理员管全局,商铺经营者能维护自家店铺的信息和报修申请,财务人员只管租金流水和账单导出。这三个角色的权限边界必须一开始就设计清楚,否则后面做权限控制时要返工。

1.2 为什么选SpringBoot+Vue这套组合

这套组合现在几乎是全栈项目的事实标准。后端用SpringBoot的原因不需要多说,自动配置、内嵌服务器、生态成熟,配合MyBatis操作数据库非常灵活。前端选Vue不是因为它是"最新最火",而是因为Vue对中小型管理系统的开发效率确实高——组件化开发思路清晰,Element UI组件库一套就能把表格、表单、弹窗全部搞定,不需要自己写太多样式代码。

有人问我为什么不用前后端不分离的JSP方案,或者后端模板引擎Thymeleaf直接渲染。我的回答是:这个系统的交互逻辑并不是简单的页面跳转,而是包含大量的动态数据刷新、弹窗编辑、状态切换。比如商铺列表页,管理员需要在不刷新整个页面的情况下完成审核状态的切换、查看详情弹窗、编辑表单提交。这些操作如果用服务端渲染,每做一次操作就要刷新一次页面,体验很糟糕,开发效率也低。前后端分离之后,前端只负责渲染和交互,后端只提供JSON接口,两边职责清晰,并行开发也方便。

1.3 功能模块划分

整个系统我划分成了六个核心模块,对应数据库里六组核心表,也是后端Controller层的六个主要分组:

  • 用户与权限模块:登录认证、角色管理、用户信息维护
  • 商铺信息模块:商铺档案的增删改查、审核流程、状态管理
  • 合同与租金模块:租赁合同登记、租金账单生成、缴费记录与逾期提醒
  • 报修管理模块:商铺提交报修、管理员派单、维修进度跟踪
  • 公告通知模块:系统公告发布、到期提醒站内信
  • 数据统计模块:商铺数量与类型分布、租金收缴率统计、收入趋势图

这六个模块看起来多,但核心就是前三个。报修和公告在数据库设计上就是两张关联表,统计部分本质上是聚合查询加前端图表渲染。我建议做同类项目的读者也按照"核心业务优先、辅助功能延后"的顺序推进,不要一上来就想着把功能做全,先把商铺档案和租金流水跑通,整个系统的骨架就定了。

2. 技术选型与核心原理解析

2.1 后端核心:SpringBoot + MyBatis

SpringBoot我用的2.x版本,选择一个相对稳定的版本就好,不必追求最新。项目结构按标准的Controller-Service-Mapper三层来拆,另外加一层entity存放实体类,加一个common包放统一返回结果、异常处理和工具类。

我的包结构大概是这样:

com.campus.shop ├── controller // 接收前端请求,返回JSON ├── service // 业务逻辑层 │ └── impl ├── mapper // MyBatis数据访问层 ├── entity // 数据库实体类 ├── dto // 前端传参和返回值的封装 ├── common // 统一返回结果、全局异常、常量 └── config // 配置类,比如跨域、拦截器

MyBatis我选择了XML映射的方式,虽然注解方式写起来更短,但动态SQL一旦复杂起来,注解会非常难维护。比如商铺列表查询,管理员可能根据名称过滤,也可能根据审核状态过滤,还可能根据商铺类型过滤,这几个条件是组合的。XML里写动态SQL,条件拼接逻辑一目了然,比在Java代码里拼字符串靠谱得多。

权限控制我用拦截器实现,写一个LoginInterceptor,在请求进入Controller之前校验Session里有没有登录用户,没有就直接返回401状态码,前端统一处理跳转登录页。管理员的接口还要额外校验角色,我在User实体里带一个roleId字段,拦截器里判断一下就行。这种方式对付中小型系统足够用了,真要上Spring Security反而把简单问题复杂化。

2.2 前端核心:Vue2 + Element UI

前端我用的是Vue2配合Element UI,没有用Vue3。不是Vue3不好,而是Element UI对Vue2的成熟度最高,网上资料多,遇到问题好查。Vue3对应的Element Plus虽然也在完善,但中小型项目用Vue2完全够用,不需要为了追新版本增加学习成本。

前端项目用Vue CLI创建,目录结构按views、components、router、api、utils来组织。views放页面级组件,比如Login.vue、ShopList.vue、ContractList.vue。components放公共组件,比如上传图片的UploadFile.vue。api目录下按后端接口分组封装axios请求方法,这样页面里调用时非常干净。

axios封装是必须做的一步。我在utils/request.js里创建axios实例,统一设置baseURL和超时时间,然后在响应拦截器里做统一处理——后端返回的状态码不是200就弹出错误提示,是401就清除登录信息并跳转登录页。这样每个页面里只需要关心成功的业务逻辑,错误处理完全交给拦截器。

2.3 数据库设计要点

数据库我设计了几张核心表。商铺表shop是绝对的核心,字段包括id、shop_name、shop_type、contract_id、owner_name、owner_phone、license_no、license_expire_date、status、create_time等。合同表contract记录每份租赁合同的关键信息:商铺id、起止日期、月租金、押金、付款方式、合同状态。账单表bill用于生成每个商铺每个计费周期的租金账单,字段有shop_id、period、amount、status(待支付/已支付/逾期)、pay_time。

在设计这张表的时候,我特意强调了一个细节:账单的生成不能靠定时任务自动跑就算了,还得考虑数据追溯。比如某个月的账单生成了,之后租金标准调整了,历史账单应该保持原样不动。所以账单表里我冗余了一个rent_amount字段,把当时生成账单时的月租金金额直接记在账单行上,而不是每次去关联合同表计算。这样后续查历史数据时,看到的就是当时的账单金额,不会因为合同修改而产生歧义。

3. 系统核心模块实现

3.1 登录鉴权与用户管理

登录模块讲起来简单,做的时候还是有几个关键点要注意。密码不能明文存数据库,我用的MD5加盐的方式——用户注册时随机生成一个salt,密码存储为MD5(password + salt),登录时取出用户的salt重新计算比对。虽然现在更推荐BCrypt,但MD5加盐放在课设和毕设项目里已经能体现安全意识了,代码也不复杂。

登录成功之后,后端把用户对象放进Session,同时往前端返回用户的基本信息和角色标识。前端拿到角色标识后,在路由配置里做动态判断——比如只有管理员角色才能访问用户管理页面。不过要清楚,前端路由拦截只是用户体验层面的控制,真正的安全校验必须靠后端拦截器,不然绕过前端直接调接口就是安全问题。

3.2 商铺信息管理的关键实现

商铺新增是整个系统里字段最多的表单。除了基本的名称、类型、经营者姓名和电话,还有营业执照号码和有效期。这里我加了一个校验逻辑:执照有效期如果距今不到30天,系统给出"即将到期"的黄色标签;已经过期的,标签变红,并在待办提醒里出现一条催办记录。

列表页的查询我用了分页加条件过滤,前端传pageNum和pageSize,以及可选的shopName和status参数。后端Service里先根据条件得到总记录数,再查询当前页的数据,封装到MyBatis的PageHelper插件里是非常省事的做法。PageHelper用起来要小心中文文档里特别强调的坑——每次查询必须在紧跟着的第一行调用startPage方法,而且这个PageHelper对象不能复用,否则会出现线程安全问题。我团队里之前就有人在一个增删改操作之前误调用了startPage,结果分页条件串到了上次的查询上,查出了奇怪的数据,排查了半天才发现是PageHelper的锅。

3.3 租金账单与逾期提醒

账单生成的逻辑是这个系统业务上最核心的一段。我的实现方式是这样的:有一个定时任务,每个月1号凌晨跑一次,扫描Contract表里所有处于生效状态的合同,根据合同里的计费周期生成当月的账单记录,状态置为"待支付"。如果合同是按季度付款的,生成账单的周期就是三个月一条。

这里有个细节,合同可能在月中才签订生效,首次账单应该按实际占用天数折算,而不是收整月租金。我在代码里写了一个prorateAmount方法,传入合同开始日期和当前账期,按天折算金额。这个逻辑看起来简单,但实际很容易写错,我建议用几个边界情况测试一下:合同1号生效、合同15号生效、跨月签约。折算后的金额我还做了四舍五入保留两位小数,避免出现一分钱的误差。

逾期提醒我用的方式比较朴实——在管理后台的首页放一个提醒卡片,查出来状态为"已逾期"的账单数量,按商铺名称分组展示。同时给每个逾期账单生成一条站内通知记录,刷新页面就能看到。这个方案的优点是不需要引入消息队列,数据库里一张notice表就搞定了,对校园网这种场景完全够用。

3.4 数据统计与报表展示

统计模块用ECharts渲染图表,后端提供聚合查询接口。比如商铺类型分布,一个GROUP BY语句就查出来了,返回类型名称和数量,前端用饼图展示。租金收缴率则有点讲究,我分了两层计算:月度收缴率是当月实收金额除以应收金额,年度收缴率是累计值。前端用柱状图展示近六个月的应收实收对比,一眼就能看出来哪个月的回款有异常。

统计数据这块我建议不要直接在业务表上做复杂查询。因为bill表的数据量上来了之后,每次统计都全表扫描会很慢。我在bill表上加了(period, status)联合索引,并对统计查询单独写了Mapper方法,不跟业务查询混在一起。对于校园场景的数据量来说,这样的优化已经完全足够了。

4. 关键难点与踩坑实录

4.1 前后端联调的跨域问题

前端项目跑在8080端口,后端跑在8081端口,前后端分离必然遇到跨域。我第一时间想到的解决方案是后端写一个CorsConfig类,配置允许的源地址、允许的请求方法和请求头。结果第一次联调还是出了问题——前端发起请求时报了"Request header field authorization is not allowed by Access-Control-Allow-Headers"这个错,原因是axios在请求头里带了自定义的token字段,而后端配置里没把这个头加上。

这个问题的排查过程让我深刻体会到:遇到跨域错误不要只盯着浏览器Network看,要分清楚是预检请求(OPTIONS请求)没通过还是实际请求被拦截。浏览器在跨域请求时会先发一个OPTIONS请求探测,预检通过了才会发真正的请求。后端配置CorsFilter时,要同时处理预检请求的响应。我最终的配置代码如下:

@Configuration public class CorsConfig implements WebMvcConfigurer { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") .allowedOriginPatterns("*") .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") .allowedHeaders("*") .allowCredentials(true) .maxAge(3600); } }

注意allowedOriginPatterns不能用通配符""配合allowCredentials(true)使用,如果用allowedOrigins("")加allowCredentials(true),部分浏览器会直接报错。这个组合坑了不少人。

4.2 MyBatis动态SQL的常见坑

MyBatis用XML写动态SQL确实灵活,但有几个地方稍不注意就出错。第一个坑是标签里大于号和小于号的转义问题。写时间范围查询的时候,SQL条件是create_time >= startDate AND create_time <= endDate,直接用大于小于号在XML里会报错,必须转义成>=和<=,或者用 包起来。我第一次写的时候就报了个"元素内容必须由格式正确的字符数据或标记组成"的异常,查了半天才想起来是尖括号的问题。

第二个坑是if标签判断空字符串。我习惯在查询条件里写 ,如果只判断null不判断空串,前端传了一个空字符串过来,拼接出来的SQL就是WHERE shop_name = '',查出来的结果是空列表,还不好排查,只能一个一个条件去试。所以建议所有字符串条件都写成null和空串同时判断。

第三个坑是foreach批量插入的效率问题。商铺的几张关联表需要做批量初始化,我用foreach标签拼接多条INSERT语句,虽然能一次执行,但如果数据量几百条还好,上千条的话SQL语句会非常长,数据库解析时间久。后来我改成了MyBatis的ExecutorType.BATCH模式,通过SqlSessionTemplate批量执行,性能提升明显。校园商铺数量通常不到一百家,这个优化其实是给未来数据增长留的余量。

4.3 数据一致性与事务管理

涉及账单生成和合同变更的地方,事务管理必须到位。我在Service层使用@Transactional注解,遇到运行时异常自动回滚。这里有一个实际遇到过的坑:账单生成逻辑里有一处抛出的是Exception而不是RuntimeException,导致Spring默认的事务回滚策略没生效,账单生成了一半就停在那里,检查花费了不少时间。后来我明确在注解里写了rollbackFor = Exception.class,这个隐患才算彻底解决。

再一个容易忽略的点是,事务方法里调用同一个类里的其他方法,事务不会生效。因为Spring的事务是基于AOP代理的,同类内部调用走的是this.xxx(),绕过了代理对象。我在写商铺更新方法时需要同时更新合同信息,一开始直接在同一个类的私有方法里调,更新失败时事务没回滚。后来改成注入自己的代理对象,或者把另一个方法挪到单独的Service类,这个问题就消失了。

4.4 常见问题速查表

我把开发过程中实际遇到的高频问题整理成了一个速查表,开发同类系统遇到同样的报错直接对照着查:

现象可能原因解决办法
前端请求报404,后端接口确认存在接口路径与Controller层的@RequestMapping不一致,或方法参数没加@RequestBody用Postman直接测试接口路径,排除前端路由问题
提交表单后后台拿到的是null前端没设置Content-Type为application/json,或参数的字段名与Java实体属性名不一致检查axios的content-type,检查JSON字段命名是否驼峰一致
分页查询数量不对PageHelper.startPage被意外调用确保startPage紧跟查询方法,中间不穿插其他数据库操作
文件上传大小超限Spring默认限制单文件1MB在application.yml里配置spring.servlet.multipart.max-file-size
日期时间少了8小时数据库和Java时区不一致连接串加上serverTimezone=Asia/Shanghai,统一时区
修改密码后旧Session仍然有效Session中缓存的是旧用户信息,没有清理修改密码接口里主动清除Session或重新登录
数据库连接中断报错MySQL的wait_timeout默认8小时,长时间空闲连接被断开配置连接池的testWhileIdle和timeBetweenEvictionRunsMillis

这张表看着简单,每一条背后都对应着我几个小时的排错经历。特别是时区问题,第一次部署到服务器的时候,所有时间记录都比实际时间慢了8小时,业务部门说数据不对,我检查了半天才发现是连接字符串没加时区参数。

5. 数据库设计补充与性能优化细节

5.1 核心表结构设计思路

上面提到了几张核心表,这里补充一下我设计字段时的一些取舍。商铺表里有一个字段叫manage_type,区分是学校自营还是外部承包,这个字段在后续做统计时非常有用——不同经营类型的商铺,审核流程和合同模板是不同的。另一个需要提的是删除策略,我没有用硬删除,而是在表里加了deleted字段做逻辑删除。原因很简单:商铺的经营数据涉及历史追溯,比如某商铺关门了,但它的历史账单、报修记录还需要留存,硬删掉之后这些关联数据就断了。

校园范围内商铺的图片信息,我是把图片存到服务器本地目录的,数据库里只存一个相对路径。这种方案在部署时需要创建一个上传目录,并配一个虚拟路径映射,把/upload/**映射到磁盘目录,这样前端能直接通过URL访问图片。如果图片量很大,可以考虑上OSS,但校园场景下本地存储已经够用。

合同和账单的表结构上,我特意加了一个bill_code字段作为账单编号,格式类似"202401-SH001-01"。这个编号虽然是冗余的,但对账的时候特别方便,财务人员拿着纸质账单直接输入编号就能在系统里查到记录。设计数据库时要考虑实际使用场景,不要只追求范式,适当的冗余能大幅提升使用体验。

5.2 索引设计与慢SQL排查

我在这套系统里实践了一个原则:不为关联表和统计查询滥用索引,只为高频查询条件建索引。商铺表的status、shop_type字段查询频率高,建了普通索引。账单表的period字段用于月度统计,建了索引。(shop_id, period)这个组合在查询单个商铺某段时间的账单时很常用,我也建了联合索引。

MySQL的慢查询日志我建议在开发阶段就打开。my.cnf里配置slow_query_log=ON和long_query_time=2,跑一段时间后打开slow_query_log_file看看,哪些查询超过2秒,集中优化。我优化过最典型的一条SQL是在统计模块里,原来的写法是先查商铺list再循环查每个商铺的账单汇总,N+1问题严重,后来改成一条LEFT JOIN加GROUP BY,查询时间从几百毫秒降到几十毫秒。

5.3 项目部署与发布

项目部署我用了比较传统的方式:后端打包成jar包,直接扔到服务器上java -jar运行。前端npm run build之后把dist目录交给Nginx托管,同时配置Nginx反向代理/api路径到后端服务。这里有一个关键配置,如果历史记录模式用的是Vue Router的history模式,Nginx需要配置try_files,否则刷新页面就是404。我第一次部署测试环境时没配这个,前端页面刷一下直接白屏,还以为是构建出了问题。

server { listen 80; server_name shop.campus.example; location / { root /var/www/shop-frontend/dist; try_files $uri $uri/ /index.html; } location /api/ { proxy_pass http://127.0.0.1:8081/api/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }

后端jar包部署时,注意生产环境的配置要单独用application-prod.yml,数据库账号密码不要写在代码仓库里,用环境变量注入。日志方面,默认的logback配置写到了logs目录下按天滚动,保留30天,这样排查问题时有日志可查,也不至于把磁盘撑爆。安全方面,部署上线第一件事就是改默认密码,我把管理员初始密码在初始化SQL里置为强制修改的状态,第一次登录必须改密码才能继续使用。

6. 实战经验总结与后续扩展建议

6.1 开发流程中的流程控制

整个项目从数据库设计到前后端联调,我用的时间分配大概是:数据库设计占两成,后端接口开发占四成,前端页面开发占三成,联调测试占一成。很多人做类似的系统时容易把联调时间低估了,实际上前后端联调才是最容易出问题的阶段,接口参数对不上、字段名不一致、状态码没统一,都会在这里暴露出来。

接口文档在这个项目里帮了大忙。我前后端并行开发,如果只靠口头沟通,接口对接时肯定一团糟。我用一个简单的Markdown文档记录每个接口的路径、请求方式、参数含义和返回示例,写完一个记录一个。这个文档看着不起眼,但在联调阶段省去了大量互相询问的时间。如果团队更大,可以考虑上接口文档工具,但个人项目用Markdown就够了。

6.2 可以扩展的方向

这套系统的骨架搭好之后,扩展功能其实不难。我梳理过几个可以继续加的方向:一是引入商铺评分与评价功能,学生在每个商铺消费后打分,管理员可以查看各商铺的评分排行,这能倒逼商铺提升服务质量。二是把财务管理做得更细,对接学校的支付渠道,支持线上缴费,同时自动生成对应的会计凭证。三是增加移动端适配或者干脆做一个微信小程序版本,让商铺经营者用手机就能提交报修、查看账单,不用每次打开电脑。

我实际使用过程中最深刻的体会是,不管系统功能做得多全,真正决定能不能用起来的是操作体验。校园商铺的经营者不一定熟悉复杂的后台系统,所以表单校验提示、按钮文案、错误提示都要做得很直白。我在这套系统里把所有的提交按钮都加了防重复点击的处理,前后端都做了校验,防止经营者连点两下提交按钮生成两条重复数据。这些细节虽然都是小地方,但用户是能感知到的。

6.3 个人实操感受

最后分享一个我在开发过程中总结的小技巧:尽量把可复用的前端代码抽成组件。我第二版开发统计页面的时候,直接把第一版做的筛选器组件和分页组件拖过来用,省了很多重复工作。后端的公共逻辑也是同理,比如统一返回结果的Result对象、分页查询的参数封装,所有接口都复用同一套,前端处理响应时只需要写一次逻辑。

再提一个我踩过多次的坑:数据库字段的命名风格一定要统一。MySQL里我用下划线命名,Java实体里用驼峰命名,MyBatis开启map-underscore-to-camel-case配置自动转换。这个配置看似简单,但不小心的话,单独手写SQL返回的字段映射就会出问题——记得在Mapper XML里为聚合查询等特殊结果自定义映射关系,别指望自动转换全帮你搞定。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询