ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

基于SpringBoot+Vue+MySQL的高校商铺管理系统设计与实现

基于SpringBoot+Vue+MySQL的高校商铺管理系统设计与实现

很多同学在毕业设计选题时会挑类似“某高校XX管理系统”一类的题目,这次要聊的“高校商铺管理系统”就是其中典型。它基于 SpringBoot + Vue + MySQL 这套成熟框架组合,表面上是普通的增删改查后台,实际上却包含角色权限、商铺租赁、合同管理、账单结算、通知公告等多个业务闭环。说白了,就是把校园里几十家门店的入驻、租金、合同、续约、退租管理全部塞进一个 Web 系统里,让管理员、运营人员和商户各司其职。

这类题目适合两类人:一是想用“全栈项目”体现综合能力、但又不想碰高并发和分布式复杂架构的本科生;二是刚入门 Java 后端、想找一个能完整落地、也能顺畅答辩的练手项目的开发者。它不会让你在技术上显得“炫技”,但能把基本功和业务逻辑展示得很扎实。下面我就按实际做毕设的流程,把这个项目从需求到部署、再到答辩避坑,完整拆一遍。

1. 动手前必须理清的事:把题目翻译成需求

1.1 角色划分与核心业务路径

很多同学拿到题目第一反应是“赶紧建表写代码”,但商铺管理系统不是单纯管“商铺”两个字,它真正管的是“经营关系”。我习惯先画一条业务主线:商铺建档 -> 合同签订 -> 账单生成 -> 费用缴纳 -> 状态变更。

围绕这条主线,系统至少要有三类角色:

  • 系统管理员:管理后台配置,负责审核商铺入驻、管理运营人员账号、查看全局营收统计、处理异常数据。
  • 运营专员:日常操盘手,录入商铺信息、发起租赁合同、录入抄表数据、生成账单、标记付款状态。
  • 商户(租户):商铺实际使用者,登录后查看自己的商铺详情、租赁合同、历史账单,线上缴纳租金并申请续租或退租。

生活化的类比是,这套系统就像一个“校园物业中心”:管理员是物业负责人,运营专员是前台接待和收费员,商户就是各个门面房的租户。物业不能把合同和账单混在一张表里瞎记,系统也一样,角色和业务模块必须清晰分开。

1.2 技术选型为什么是 SpringBoot + Vue + MySQL,而不是更复杂的东西

先看一组对比:

对比维度SpringBoot 单体SSM 传统项目SpringCloud 微服务
配置成本自动配置,极低XML 配置多,繁琐服务治理组件多,复杂
部署方式内置 Tomcat,直接跑 jar外置 Tomcat,麻烦多服务部署,环境要求高
学习成本低,适合毕设偏高过时很高,明显过度设计
毕设性价比高中低

选 SpringBoot 不是因为它是“新技术”,而是因为它把开发效率拉满。它内置了 Tomcat,Maven 引入几个 starter 就能跑起来;配合 MyBatis-Plus 后连基础 SQL 都不用自己写。同样道理,前端选 Vue 是因为组件化和响应式数据模型非常适合后台管理系统:Element Plus 提供现成的表格、表单、弹窗、树形控件,能让界面在短时间内达到可演示的状态。MySQL 则有成熟的关系型事务支持,合同和账单这种强一致性数据恰恰需要事务兜底。

另外一个态度问题我也想说一下。现在网上动不动就建议在毕设里堆 Redis、RabbitMQ、Spring Cloud Alibaba,看起来“技术栈全”,实际上答辩时评委问一句“你为什么要用它”就很容易卡壳。商铺管理系统的并发量撑不到微服务的层级,事务放在单体里处理反而简单可靠。毕业设计的核心是展示你对业务的理解和代码的规范度,不是展示你堆了多少中间件。

2. 数据库设计:整个项目好不好做的分水岭

2.1 核心表结构与字段规划

这个项目至少要设计这么几张表:

表名作用关键字段
sys_user统一保存管理员、运营、商户账号user_id, username, password, real_name, role, phone, status
shop_type商铺分类,如餐饮、打印店type_id, type_name, sort_order
shop_info商铺档案shop_id, shop_name, type_id, owner_user_id, floor_no, position, area, rent, status
rental_contract租赁合同contract_id, shop_id, renter_id, start_date, end_date, rent_amount, deposit, status
payment_order账单流水payment_id, contract_id, period, amount, status, pay_time
notice_info通知公告notice_id, title, content, publish_time

字段设计上有两个细节特别容易被忽视,我逐一说明。

第一,owner_user_id 到底关联谁?商铺在“在租”状态下属于某个商户,但在“空置”状态下没有 owner。所以不能用 owner_user_id 作为商铺的唯一归属,而是在合同表里维护“当前有效合同的商户”。查询商铺时通过“合同状态为生效中”去关联租户,而不是直接取商铺表的某个字段。

第二,金额字段不要用 double 或 float。这类项目里你会涉及房租、物业费、保证金,浮点数在累计计算时会丢失精度。建议用 decimal(10,2),在 Java 里对应 BigDecimal。这些细节虽然小,但答辩时拿出来讲会显得你很有工程经验。

2.2 状态字段、软删除与“为什么不用外键”的道理

商铺状态我建议用整数枚举存储:0 待审核,1 空置,2 在租,3 停用,4 到期未续。数据库里存 tinyint,Java 里用枚举做映射。不要直接存中文状态名,否则以后想改文案就会面临全表数据更新。

再说一个毕设常见的坑:不要建物理外键。物理外键会让数据插入顺序变得僵化,比如你先插入合同、后补商铺资料,外键直接就拦截你了。项目里逻辑外键就够了,关联关系在 Service 层控制,或者用 MyBatis-Plus 的关联查询来做。这样删除数据时也更可控,不会因为一条外键约束导致整串数据删不掉。

软删除也建议做上。给所有核心业务表增加 deleted 字段,默认 0,删除时改成 1。这么做不是为了炫技,是防止答辩现场演示“删除商铺”时把历史合同和账单一起物理删掉了,那数据就真的找不回来了。尤其在演示过程中误操作也是常见事故,软删除是低成本高收益的保险。

最后别忘了加 created_time、updated_time 两个公共字段。如果用 MyBatis-Plus,可以让公共字段自动填充。跑批查询或者分析时,时间线清晰会舒服很多。

3. 后端实现要点:认证、权限与账单逻辑

3.1 工程结构与统一返回体

创建 SpringBoot 工程时勾选 Spring Web、MySQL Driver、MyBatis-Plus、Lombok 就能起步。建议再引入 spring-boot-starter-validation 做参数校验,蛮实用的。目录结构我习惯按这种分层组织:

controller/ # 对接前端接口 service/ # 业务逻辑 mapper/ # 数据库访问层 entity/ # 数据表实体 dto/ # 接收前端参数的传输对象 vo/ # 返回给前端的视图对象 config/ # 跨域、拦截器、全局异常处理

很多同学写完一个项目,controller 里直接塞一大坨业务代码,返回给前端的数据又是 Map 又是 String,答辩时一翻代码很容易被扣印象分。建议先做一个统一响应体:

@Data public class Result<T> { private Integer code; private String msg; private T data; public static <T> Result<T> ok(T data) { ... } public static <T> Result<T> error(String msg) { ... } }

另外每个实体最好都有对应的 VO,不要直接把数据库实体暴露给前端。比如商铺实体里有 deleted 字段,就不应该传给前端。VO 可以做字段裁剪和组合,这也是答辩时可以讲的规范点。

3.2 登录认证用 JWT 还是 Session

商铺管理系统属于前后端分离项目,我推荐用 JWT 做登录态管理。流程是:用户提交账号密码,后端校验通过后生成 token 返回,前端存到 localStorage 或 Pinia 里,随后每次请求都在 header 里带上 token。

实现上不必引入 Spring Security 全家桶,自己写一个拦截器足够。核心逻辑如下:

@PostMapping("/login") public Result<String> login(@RequestBody LoginDTO dto) { User user = userService.login(dto.getUsername(), dto.getPassword()); // 比对 BCrypt 密码,通过后生成 token String token = JwtUtil.generate(user.getId(), user.getRole()); return Result.ok(token); }

密码存储不要用明文,后端使用 BCrypt 加密。很多学生图省事直接把密码以明文字符串存进数据库,一旦代码被评审展示,这就是严重隐患。BCrypt 加盐 hash 的效果非常可靠,Spring Security 里自带的 BCryptPasswordEncoder 可以直接用。

拦截器负责三件事:解析 token 确认用户存在、判断接口角色权限、设置当前用户上下文。代码很短,但这是整个后台系统的安全骨架。管理员接口和商户接口要分开控制,比如“删除商铺”只允许管理员,“查看我的合同”只允许商户本人,这种权限颗粒度在答辩中是加分点。

3.3 账单生成:最容易踩坑的业务核心

租赁合同生效后,每个月要生成一张账单。业务逻辑看起来简单,但有三个坑必须处理。

坑一:账单重复生成。如果运营人员在月结时连续点击了两次“生成账单”,系统不应产生两条记录。解决办法是给 payment_order 表加唯一索引,组合键是 (contract_id, period)。在代码里也要先做存在性检查,写成一个幂等操作。

坑二:金额计算的精度和拆分。一张账单通常包含月租、管理费、水电分摊等。建议在 Service 层分别计算,最后用 BigDecimal 相加。不要在前端做金额合计,后端必须重新计算一遍,防止接口被篡改。

坑三:缴费状态的变更。商户付款后,后端要更新 payment_order 的状态,同时可能推迟合同到期时间。这里要放到一个事务里,否则就会出现“钱已付但合同没延期”的不一致状态。

伪代码逻辑大概是:

@Transactional public void generateBill(Long contractId, String period) { // 1. 查询生效中的合同 // 2. 校验该周期是否已存在账单 // 3. 计算租金和管理费 // 4. 插入 payment_order 记录 }

这部分写扎实后,整个系统的可信度就上来了。评委最喜欢问的就是“你这个账单是怎么保证不重复生成、数据不错乱的”,能答清楚这一层,基本等于稳了。

4. 前端 Vue 实现:从接口到可用页面

4.1 工程初始化与目录规划

前端我建议直接用 Vue 3 + Vite + Element Plus + Pinia。为什么不用 Vue 2?一是 Vue 3 已经是主流,新项目再用旧版没有意义;二是 Element Plus 对 Vue 3 的支持很好,表格和表单组件完善,做这种管理系统效率很高。

初始化工程后,目录大概这样:

src/ api/ # 后端接口封装 assets/ # 静态资源 components/ # 通用组件 layout/ # 后台整体布局(侧边栏、顶栏) router/ # 路由配置 stores/ # Pinia 状态 views/ # 页面 login/ dashboard/ shop/ contract/ payment/

页面划分建议跟着后端模块走,每个模块包含列表页、表单页、详情页三种常见形态。

4.2 axios 拦截器与路由守卫

前后端分离后,所有接口请求都建议封装在 api 目录下,不要在每个页面里直接写 axios。统一封装的好处是拦截器只写一次:

// axios 请求拦截 service.interceptors.request.use(config => { config.headers.Authorization = `Bearer ${localStorage.getItem('token')}` return config }) // axios 响应拦截 service.interceptors.response.use(res => { if (res.data.code === 401) { router.push('/login') } return res.data })

路由守卫也要按角色过滤。管理员可以进入系统管理页面,商户只能看到自己的合同和账单。实现方式是路由 meta 里标记 roles,在 beforeEach 里判断当前登录用户的角色,不匹配就直接跳转首页并给出提示。这种体验会让系统显得“有权限意识”,远胜于一个所有人都能看所有页面的 demo。

4.3 列表页和表单页的通用实现套路

商铺管理列表是整个系统的门面。Element Plus 里 el-table 绑定列表数据,上方放搜索栏,底部放分页组件。一个标准列表页的骨架是这样:

<el-table :data="shopList"> <el-table-column prop="shopName" label="商铺名称" /> <el-table-column prop="typeName" label="类型" /> <el-table-column prop="rent" label="月租金" /> <el-table-column label="状态"> <template #default="{ row }"> <el-tag :type="statusTag(row.status)">{{ statusText(row.status) }}</el-tag> </template> </el-table-column> <el-table-column label="操作"> <el-button link type="primary" @click="openDetail(row)">详情</el-button> </el-table-column> </el-table>

表单页我建议用弹窗形式承载,使用 el-dialog 包裹 el-form,配合 rules 做校验。宽度控制在 600px 左右,标题就取“新增商铺”“编辑合同”“登记账单”这类直白文案。不要做得太复杂,小程序能用、好看、不啰嗦才最合适。

记住一个重要原则:页面里的数据不要用回调地狱方式一层层嵌套拿,而是通过 Pinia 或页面级的 onMounted 调用统一的 API 函数。比如进入合同列表页就调用 contractApi.list(params),拿到数据直接赋值给响应式变量,状态管理和页面渲染都会干净很多。

5. 联调、部署与演示环境搭建

5.1 开发环境的跨域处理

本地开发时,Vite 默认端口是 5173,SpringBoot 默认 8080,浏览器直接请求会产生跨域。最优雅的解决办法是配置 Vite 代理:

server: { proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true } } }

这样前端请求 /api/shop/list 在开发环境下会自动转发到后端 8080 端口,浏览器层面不感知跨域。后端也可以配置 CORS 规则,但开发时以 Vite 代理为主即可。我碰到过很多项目明明后端 CORS 配好了,部署时还是突然 401 或 404,原因大都是没有区分“开发模式代理”和“生产模式反向代理”。

5.2 前后端两种打包部署方式

方式一:前后端合并部署。前端执行 npm run build,生成 dist 目录,把整个 dist 文件夹复制到 SpringBoot 的 src/main/resources/static 下,然后用 Maven 打包:

mvn clean package -DskipTests java -jar target/shop-manage.jar

这样访问 http://localhost:8080 就能直接打开页面,后端接口也在同一个端口上。适合演示环境,流程短。

方式二:前后端分离部署。前端 dist 部署到 Nginx,后端 jar 单独跑在 8080 端口。Nginx 配置反向代理,把 /api 前缀的请求转发到后端:

location /api/ { proxy_pass http://127.0.0.1:8080/; }

注意 proxy_pass 最后面的斜杠,少了它路径拼接时会出现 /api/api 的尴尬问题。这个细节我见过不下三次,导致前端页面接口大面积 404。

生产或答辩环境建议用方式二,因为更像真实项目部署。如果你担心答辩现场网络不稳定,就在笔记本本地用方式一跑通,再把后端 jar 和前端 dist 都准备好备用。

5.3 数据库连接配置与初始化

数据库连接配置不要硬编码在 application.yml 里。虽然本地开发硬编码最省事,但答辩演示往往要连另一台机器的数据库。更好的做法是使用环境变量:

spring: datasource: url: jdbc:mysql://${DB_HOST:localhost}:3306/${DB_NAME:shop_db}?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: ${DB_USER:root} password: ${DB_PASSWORD:root}

另外一定要准备一份 db/init.sql,里面包含建库、建表、插入初始账号和模拟数据。答辩前只要执行一遍 init.sql 就能还原一个可演示的环境,而不是依赖之前的测试数据。把初始化脚本当作交付物之一,是很多评审老师会看重的好习惯。

6. 答辩与避坑:这类毕设最容易翻车的四个地方

6.1 高频问题速查表

现象可能原因解决办法
登录后接口 401token 过期或拦截器放行路径没配好检查拦截器放行 /login、/register 等路径
MySQL 连接报时区错误连接串缺少 serverTimezone在 JDBC URL 加 Asia/Shanghai
账单金额对不上使用 double 累加全部改为 BigDecimal,数据库用 decimal
删除商铺却连带账单没了物理删除导致关联数据丢失改用软删除,保留历史数据
前端页面接口 404Nginx 代理路径配置错误检查 proxy_pass 末尾斜杠和 /api 前缀
分页数据紊乱每页数量前端和后端不一致统一 pageSize 参数,后端做兜底 maxPageSize

我把这些情况整理成表格,是因为它们几乎覆盖了我见过的大部分毕设事故现场。尤其“物理删除导致数据丢失”这条,很多同学答辩前满怀信心,结果演示到“删除一间没签合同的商铺”时,把关联的历史提示记录全部删掉,页面直接崩溃,十分可惜。

6.2 让项目更有“灵魂”的加分点

如果时间充裕,我建议再做三件锦上添花的事。

第一,加一张 operation_log 操作日志表。管理员审核入驻、运营录入合同、商户提交退租请求,全部记录在案。答辩时你可以演示“查看操作日志”,瞬间有一个可追溯的审计链路,这在管理系统中非常加分。

第二,把“新建商铺 -> 签订合同 -> 按周期生成账单 -> 模拟缴费 -> 合同到期预警”做成一条完整演示路径。不要只介绍一个个孤立页面。让评委跟着你走完一个业务流程,他们对项目的理解深度会完全不同。

第三,做一个简单的租期预警。在合同列表页增加筛选条件,查询“距到期不足30天”的合同,然后推送给运营端提醒。这其实就是一个普通查询,但业务价值感很强。

6.3 心态与技术之外的提醒

最后聊点心态。答辩时不要背稿子,也不要把每一个接口都念一遍。评委更想听到的是你作为开发者如何拆解业务、如何取舍技术方案、如何解决实际问题。哪怕你说“这里我考虑了软删除,避免误操作导致数据丢失”都比“这里我用了一个 for 循环”得分高得多。

我个人带项目时的经验是:把代码读一遍,把数据库设计过一遍,把核心事务方法讲一遍,这三点比界面多漂亮都重要。界面整洁即可,真正决定成绩下限的是数据正确性和业务闭环,上限则看你的表达思路是否清楚。

这里也分享一个我自己的独门小技巧:答辩前一天,把数据库重新初始化一次,用 init.sql 跑一遍,然后完整操作一遍“合同到期后自动停用商铺”的流程。这个操作基本能查出来你是否漏配了定时任务、是否有人在正式环境依赖了手工清理的数据。我见过不止一个团队在演示前一晚发现自己漏了一条 SQL,提前跑底prepared是唯一能兜底的办法。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进