
写毕设项目的时候最怕的不是题目难而是选了一个自己完全hold不住的方向最后做到一半发现推倒重来都来不及。今天要聊的这个项目——基于SpringBootVueMySQL的太原学院商铺管理系统属于那种“看起来朴素、实际上很能打”的典型选题。技术栈主流、业务模型清晰、工作量适中而且往上可以加权限控制、统计报表往下可以拆成最基础的CRUD弹性空间非常大。我完整走了一遍从选题分析、数据库建模、前后端实现到部署答辩的全过程这篇就把整个思路和坑位都摊开讲清楚。1. 毕业设计选题与技术栈分析1.1 为什么商铺管理系统值得做很多同学选毕设题目的时候容易陷入两个极端要么选一个特别宏大的方向——什么基于深度学习的智能推荐系统、基于微服务架构的电商平台听起来很唬人但以本科阶段的精力和能力大概率会烂尾要么选一个特别简单的——比如简单的图书管理系统做完发现技术含量太低答辩的时候老师问几句就露馅了。商铺管理系统恰好卡在中间它不是一个纯CRUD的玩具项目因为商铺管理涉及租户、合同、费用缴纳、到期提醒、统计数据这些真实的业务逻辑同时它又没有复杂到需要分布式、消息队列、微服务那一套。以SpringBootVueMySQL这套黄金组合实现工作量大概在三到四周可以稳定完成而且每一步做出来的东西都能看得见、讲得清。以太原学院作为业务场景核心需求非常明确学校里的商铺需要统一管理包括商铺基本信息、入驻商户租户、租赁合同、租金和物业费缴纳、到期续约提醒。这些功能映射到系统里就是商铺模块、租户模块、合同模块、费用模块、统计模块。整个业务链条是闭环的——商铺出租给租户租户签合同合同关联费用费用产生统计数据。这个逻辑在答辩的时候特别好讲因为它是真实世界的业务不是凭空捏造的功能。1.2 技术栈选择的底层逻辑先聊SpringBoot。Java后端框架里SpringBoot现在就是事实上的标准。为什么不用SSHSpringStrutsHibernate那套东西配置太繁琐光XML配置就能写到手抽筋而且市面上已经很少有公司用了写进简历里反而是减分项。为什么不用Spring Cloud微服务本科毕设用微服务纯粹是给自己挖坑——服务注册、配置中心、网关、链路追踪每一样都要额外学习成本而且一个小型商铺管理系统根本不需要分布式。SpringBoot的优势在于自动配置帮你去掉了大量繁琐的配置工作内嵌Tomcat让部署变得极其简单一个jar包就能跑生态成熟到什么坑都有人踩过网上资料一搜一大把。再看Vue。前端选Vue而不是React一个很现实的原因是Vue的学习曲线更平缓中文文档和教程极其丰富对毕设党非常友好。Vue的双向数据绑定、组件化开发、Vue Router路由管理、Vuex/Pinia状态管理这些够用且够讲。模板语法比JSX更直观Element UI或Element Plus组件库一引入表格、表单、弹窗、分页这些后台管理系统的标配功能全都有了不需要自己从零写CSS布局。最后是MySQL。这个没什么好纠结的——免费、稳定、资料多、面试常问。SQL语法是通用的学会了MySQL以后用PostgreSQL、Oracle也能快速上手。对毕设来说MySQL 8.0配合Navicat或DBeaver做可视化操作建表、导数据、调试SQL都非常方便。这套组合还有一个隐性好处企业在招聘的时候SpringBootVueMySQL几乎是后端开发岗的标配技能组合。做完这个项目简历上可以写“独立开发基于SpringBootVue的商铺管理系统”面试官看到的是你具备前后端全栈开发能力而且用的是主流技术栈不是自己捣鼓的冷门框架。1.3 系统功能全景拆解把整个系统拆开来看功能模块划分如下登录与权限模块用户登录、角色区分管理员、普通员工、基于拦截器的登录校验商铺管理模块商铺信息的增删改查、状态管理出租中、空置、停用、按区域和类型筛选租户管理模块租户档案维护、证件信息记录、租户与商铺的关联合同管理模块合同创建、起止日期设置、合同状态跟踪履行中、已到期、已终止费用管理模块租金和物业费的记录、缴费状态标记、欠费提醒数据统计模块商铺出租率统计、月租金收入趋势、费用类型分布公告管理模块系统公告发布与展示可作为扩展功能每个模块相互独立又彼此关联展示层用Vue实现接口层由SpringBoot提供RESTful API数据层通过MyBatis Plus操作MySQL。这个架构在答辩的时候画一张系统架构图从浏览器到Controller、Service、Mapper、MySQL的调用链路一目了然老师一听就知道你不是只写了几个增删改查的接口。2. 数据库设计与建模把地基打牢2.1 核心数据表设计与字段规划数据库设计是整个系统最关键的环节一旦表结构设计不合理后面写代码的时候会处处别扭。我建议在动手写代码之前先把表建好字段想清楚。基于商铺管理系统的业务需求核心表分为六张。用户表tb_user是最基础的表字段包括id、username、password、real_name、role、phone、status、create_time。密码字段不要明文存储至少用MD5加盐或者BCrypt加密。角色字段用字符串ADMIN/STAFF比用数字更直观省得每次都要在代码里翻译数字含义。商铺表tb_shop的字段设计要能完整描述一个商铺的基本属性id、shop_no商铺编号、shop_name商铺名称、location位置描述、area面积平方米、category商铺类型如餐饮、零售、服务、monthly_rent月租金、status状态0空置、1出租中、2停用、create_time、update_time。这里有个细节月租金要放在商铺表还是合同表我建议商铺表里放一个默认租金作为参考值实际合同租金放在合同表里因为不同时期签约的租金可能不同合同价才是真实成交价。租户表tb_tenant记录入驻商户的信息id、tenant_no租户编号、name租户名称/商户老板姓名、phone、id_card身份证号、company_name注册公司名可选、address、register_time、status。身份证号建议做唯一约束或至少加索引因为后续可能需要按身份证查历史合同。合同表tb_contract是业务的枢纽id、contract_no合同编号、shop_id关联商铺、tenant_id关联租户、start_date合同开始日期、end_date合同结束日期、monthly_rent合同月租金、deposit押金、status合同状态1履行中、2已到期、3已终止、create_time。合同编号建议格式如HT202501001体现年份和序号看起来专业也方便检索。费用表tb_fee记录每笔缴费id、fee_no、contract_id关联合同、fee_type费用类型1租金、2物业费、3押金、amount金额、pay_date缴费日期、pay_method支付方式现金、转账、扫码、status缴费状态0未缴、1已缴、remark备注。金额字段一定要用DECIMAL类型比如DECIMAL(10,2)千万别用float——浮点数做金额计算会出现0.10.2不等于0.3的经典问题。公告表tb_notice比较简单id、title、content、publish_user、publish_time、status。这个表不关联业务主体是纯信息发布功能。2.2 表关系梳理与设计取舍表关系是整个数据库设计的核心理解清楚之后写代码和画E-R图都顺理成章。商铺和租户之间是什么关系逻辑上一个商铺在一个时间段内只属于一个租户一个租户可以租多个商铺比如一个人同时开了奶茶店和炸鸡店所以是“多对多”关系。但在实际设计中我们通过合同表来解耦合同关联商铺和租户只要合同有效就说明该商铺被该租户租用。这样设计的好处是支持历史追溯——一个商铺被不同租户租过多次签过多份合同全部记录下来需要查某个商铺的历史租赁情况时只需按shop_id查合同表即可。表关系可以这样梳理tb_contract通过shop_id关联tb_shop通过tenant_id关联tb_tenant通过contract_id串联tb_fee。这样所有的查询路径都是清晰明确的查一个商铺的当前租户先按shop_id和status履行中查合同再从合同拿tenant_id去查租户表。这种设计方式比在商铺表里直接加一个tenant_id字段更规范因为直接加字段的话一旦合同变更历史数据就乱了。另外一个设计取舍是外键约束。很多课程设计里老师要求建外键但在真实项目中我建议只保留逻辑关联不加物理外键。原因很简单物理外键在插入、更新、删除时都会有额外的约束检查影响性能而且一旦业务上需要“先删子表再删主表”这种操作外键会变成绊脚石。MyBatis Plus操作数据时也不关心物理外键所以保持逻辑关联就足够了。答辩的时候如果老师问为什么没有外键可以回答外键约束放在应用层通过业务逻辑控制降低数据库耦合大数据量场景下性能更好这也是业界常见的做法。2.3 数据设计避坑指南在数据表设计过程中有几个坑是很多同学容易踩的我在这里集中说一下。状态字段用int类型而不用varchar。比如商铺状态不要存“出租中”这种字符串而是存0、1、2在代码里用常量或枚举去对照。原因很简单字符串占空间更大容易被拼写错误污染数据而且不好做索引。同时在展示层需要转成中文时可以在前端做映射也可以在SQL里用CASE WHEN处理。金额用DECIMAL(10,2)绝对不要用float或double。float是近似值存储在做金额累加、比较时会有精度误差。比如0.10.2在二进制浮点数里不等于0.3这在财务报表里是致命的。DECIMAL是按字符串存储的定点数精度可控适合金融场景。时间字段统一用datetime不要用timestamp。datetime的范围是1000年到9999年timestamp只到2038年32位系统下。更重要的是datetime不依赖时区设置不会出现插入的数据自动偏移几小时的问题。虽然MySQL 8.0的timestamp处理时区更智能了但为了省心统一用datetime。唯一索引不要滥用但要善用。合同编号、商铺编号、租户编号这类业务编号字段应该加唯一索引防止重复数据。而姓名、位置描述这类字段不要加唯一索引否则后续数据稍微有点变化就插入失败。3. 核心功能实现从后端到前端完整跑通3.1 后端项目骨架与公共类封装后端项目结构按标准的三层架构来分包名可以这样规划com.tyxy.shop主包名controllerRestController层service业务接口层service.impl业务实现层mapperMyBatis Plus的Mapper接口entity数据库实体类config配置类如跨域、拦截器、MyBatis Plus分页插件common公共类如统一返回结果、异常处理、常量类utils工具类如JWT工具类这个结构是经过实践验证的经典结构层次清晰职责分明。Controller只负责接收请求、调用Service、返回结果Service写业务逻辑Mapper只做数据访问。每一层不要越界——我最怕看到有人把业务逻辑全写在Controller里一个方法几百行查完数据库后在前端返回之前又做一堆判断这是代码坏味道。项目中要封装一个统一的返回结果类Result这是前后端联调的基础。格式建议为Data public class ResultT { private Integer code; // 200成功500失败401未登录 private String message; private T data; public static T ResultT success(T data) { ResultT result new Result(); result.setCode(200); result.setMessage(操作成功); result.setData(data); return result; } public static T ResultT error(String message) { ResultT result new Result(); result.setCode(500); result.setMessage(message); return result; } }这样前端axios拦截器里可以统一判断code是否为200不是200就直接弹错误提示不需要每个接口都写一遍错误处理逻辑。登录认证建议用JWTJSON Web Token实现SpringBoot后端生成token前端存储并在每次请求时放入请求头。和Session方案相比JWT是无状态的服务器不需要保存会话信息更适合前后端分离架构。具体实现就是用jjwt库生成token写一个拦截器在请求到达Controller之前校验token是否存在和有效配合一个LoginRequired注解做细粒度的权限控制。3.2 商铺管理模块代码级拆解以商铺管理模块为例把完整的后端链路走一遍。这是整个系统最核心的模块也是最典型的CRUD列表查询场景。实体类ShopData TableName(tb_shop) public class Shop { TableId(type IdType.AUTO) private Integer id; private String shopNo; private String shopName; private String location; private BigDecimal area; private String category; private BigDecimal monthlyRent; private Integer status; // 0空置 1出租中 2停用 private LocalDateTime createTime; private LocalDateTime updateTime; }这里有两个细节需要说。TableName注解指定了对应的表名因为Java类名Shop和表名tb_shop不对应必须显式指定否则MyBatis Plus默认映射的会是shop表。TableId(type IdType.AUTO)表示主键自增MySQL的auto_increment策略如果是雪花ID或者其他生成策略要在这里标明。Mapper接口public interface ShopMapper extends BaseMapperShop { }继承BaseMapper之后基础的增删改查方法全都有了。insert、deleteById、selectById、selectList这些都不需要自己写SQL。复杂查询比如“按名称模糊查询按状态筛选分页”用MyBatis Plus的QueryWrapper或LambdaQueryWrapper来构造条件不需要手写XML。Service接口和实现public interface ShopService extends IServiceShop { PageShop getShopPage(int pageNum, int pageSize, String keyword, Integer status); }Service public class ShopServiceImpl extends ServiceImplShopMapper, Shop implements ShopService { Override public PageShop getShopPage(int pageNum, int pageSize, String keyword, Integer status) { PageShop page new Page(pageNum, pageSize); LambdaQueryWrapperShop wrapper new LambdaQueryWrapper(); // 关键字匹配商铺名称或编号 if (StrUtil.isNotBlank(keyword)) { wrapper.like(Shop::getShopName, keyword) .or().like(Shop::getShopNo, keyword); } // 状态筛选 if (status ! null) { wrapper.eq(Shop::getStatus, status); } wrapper.orderByDesc(Shop::getCreateTime); return this.page(page, wrapper); } }ServiceImpl是MyBatis Plus提供的一个通用实现类自带了很多方法。分页查询的关键是new Page(pageNum, pageSize)然后调用this.page(page, wrapper)返回的Page对象里有records、total、current、size这些属性前端拿到之后直接可以做分页展示。ControllerRestController RequestMapping(/api/shop) public class ShopController { Autowired private ShopService shopService; GetMapping(/page) public ResultPageShop page( RequestParam(defaultValue 1) int pageNum, RequestParam(defaultValue 10) int pageSize, RequestParam(required false) String keyword, RequestParam(required false) Integer status) { return Result.success(shopService.getShopPage(pageNum, pageSize, keyword, status)); } PostMapping public Result? add(RequestBody Shop shop) { shop.setCreateTime(LocalDateTime.now()); shop.setUpdateTime(LocalDateTime.now()); shopService.save(shop); return Result.success(null); } PutMapping public Result? update(RequestBody Shop shop) { shop.setUpdateTime(LocalDateTime.now()); shopService.updateById(shop); return Result.success(null); } DeleteMapping(/{id}) public Result? delete(PathVariable Integer id) { shopService.removeById(id); return Result.success(null); } }RESTful风格接口GET做查询、POST做新增、PUT做修改、DELETE做删除。接口路径统一以/api开头加上模块名这样前端代理和后端路由都比较清晰。3.3 前端页面实现与联调细节前端使用Vue 2 Element UI或者Vue 3 Element Plus创建项目用Vue CLI或者Vite。这里我想说一个实操建议如果之前没有接触过Vue或者毕设时间紧直接用Vue 2 Element UI的方案因为教程最多、踩坑记录最全Element UI的表格、表单、对话框组件对后台管理系统非常友好。项目结构大致为srcapiaxios请求封装和各模块APIrouter路由配置views页面组件components公共组件utils工具类storeVuex/Pinia状态管理如果不需要可以省登录页面是系统的门面用el-form做表单校验用户名和密码非空提交时调用后端登录接口成功后把token存到localStorage然后跳转到首页。首页布局用el-container做侧边栏顶栏内容区的经典后台结构侧边栏根据路由配置自动生成菜单。axios封装是非常重要的一步。在utils/request.js里创建一个axios实例设置baseURL为/api然后添加请求拦截器和响应拦截器import axios from axios const request axios.create({ baseURL: /api, timeout: 10000 }) // 请求拦截器每个请求自动携带token request.interceptors.request.use(config { const token localStorage.getItem(token) if (token) { config.headers[Authorization] token } return config }) // 响应拦截器统一处理业务错误和登录过期 request.interceptors.response.use( response { const res response.data if (res.code ! 200) { this.$message.error(res.message) return Promise.reject(new Error(res.message)) } return res }, error { this.$message.error(网络异常请稍后重试) return Promise.reject(error) } ) export default request这里有一个前后端联调最容易遇到的问题跨域。前端开发服务器在8080端口后端在8080端口SpringBoot默认浏览器会拦截跨域请求。解决方式有两种一是在SpringBoot里配置CORS推荐更规范二是在Vue的vue.config.js里配置devServer的proxy代理。我建议开发阶段用proxy代理这样前端代码里请求的baseURL直接用/api开头不需要写完整地址后端也不需要额外处理跨域因为代理在开发服务器层面就把请求转发过去了。生产环境打包后前端文件和后端jar包放在同一个服务里根本不存在跨域。// vue.config.js module.exports { devServer: { port: 3000, proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } } }商铺管理页面用el-table展示数据el-pagination分页el-dialog做新增和编辑表单。搜索区域放一个el-input输入关键字和一个el-select选择状态点击查询按钮重新加载表格数据。这个页面做完后面租户管理、合同管理、费用管理的页面都是同一套模式复制改造开发效率非常高。4. 实战踩坑记录与答辩准备4.1 环境配置高频问题环境配置是整个项目的第一道坎很多同学还没开始写代码就卡在这里了。MySQL安装是第一个坑MySQL 8.0在Windows上安装时如果选择的是zip解压版需要手动创建my.ini配置文件初始化data目录设置root密码步骤比较多容易出错。建议直接用官方installer安装图形化界面一路下一步但要注意记住root密码后期连接数据库都要用它。另一个容易忽略的问题是时区MySQL 8.0的默认时区不是中国时区在JDBC连接串里必须加上serverTimezoneAsia/Shanghai否则查询出来的时间比正常时间少8小时。SpringBoot版本选择也是一个大坑。Spring Boot 3.x发布之后很多教程还在用2.x但同学们下载依赖的时候默认可能是最新版。Spring Boot 3.0以上版本有Breaking Changes比如javax.servlet包换成了jakarta.servletMyBatis Plus的starter也必须是适配3.x的版本。如果项目里引入的依赖还是javax开头的老写法编译直接报错。我的建议是如果网上的教程和依赖版本都是基于2.x的就用Spring Boot 2.7.x。等做完了想升级3.x再说毕设阶段没必要追新版本给自己增加不确定性。Maven依赖下载慢的问题用阿里云镜像仓库配置在settings.xml里速度能快好几倍。Node环境和Vue项目创建是个独立的环境问题。建议安装Node.js 16.x或18.x长期支持版然后用npm config set registry https://registry.npmmirror.com 把npm源换成国内镜像。创建Vue项目时如果使用Vue CLI执行npm install时可能会卡很久换成镜像源之后会好很多。Vite创建Vue 3项目时同理。4.2 后端与前端逻辑Bug盘点后端开发中的Bug我挑几个高频出现并且有代表性的来说。第一个是MyBatis Plus分页插件没有生效。MyBatis Plus 3.4.0之后分页插件不再默认注册必须手动配置PaginationInnerInterceptor否则调用page方法时分页无效会把全表数据查出来。配置方法是在config包里写一个MybatisPlusConfig类Configuration public class MybatisPlusConfig { Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } }写完之后一定要确认这个配置类被SpringBoot扫描到了。如果包路径不对配置不起作用分页照样失效。第二个是日期格式化问题。实体类里用了LocalDateTime类型返回给前端的时候默认格式是“2025-01-15T10:30:00”这种带T的ISO格式前端展示难看。解决方式有两种在实体类的时间字段上加JsonFormat(pattern yyyy-MM-dd HH:mm:ss)注解或者在application.yml里配置全局时间格式。推荐用全局配置省得每个字段都加注解spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT8第三个是前端查询日期范围的传参问题。合同管理里经常要按合同起止日期筛选前端用的是el-date-picker选择范围默认传回来的是一个数组需要手动拆成startDate和endDate两个字段传给后端。后端接收时要用DateTimeFormat注解标注格式GetMapping(/list) public Result? list(RequestParam(required false) DateTimeFormat(pattern yyyy-MM-dd) LocalDate startDate, RequestParam(required false) DateTimeFormat(pattern yyyy-MM-dd) LocalDate endDate) { // 查询逻辑 }前端的日期组件格式和后端的解析格式不一致时最常见的结果就是后端报400错误但页面只显示“网络异常”排查起来费时间。前端部署相关的坑也值得单独说。Vue默认的路由模式是hash模式地址栏带个#号不会有什么问题。但如果改成history模式打包部署之后刷新页面会404因为后端没有配置路径回退。SpringBoot项目里解决方式是写一个WebMvcConfigurer把404的请求forward到index.htmlComponent public class WebConfig implements WebMvcConfigurer { Override public void addViewControllers(ViewControllerRegistry registry) { registry.addViewController(/{spring:[a-zA-Z0-9-_]}) .setViewName(forward:/index.html); } }但如果路由层级比较深这个简单配置可能不够用。说实话毕设项目部署演示的时候直接用hash模式最省心把这个问题说清楚怎么解决就够了不需要真的去追求history模式。4.3 项目部署与答辩要点项目部署到底怎么做这是每个毕设党在最后阶段最关心的问题。整个项目打包部署的完整流程是这样的后端在IDEA里执行mvn clean package命令打出jar包然后在服务器上执行java -jar shop-system.jar启动。前端在项目目录执行npm run build产出dist目录里面是纯静态文件。有两个承载方式一是把dist目录里的文件直接放进SpringBoot项目的src/main/resources/static目录下然后重新打包这样前后端就在同一个jar包里只需要跑一个java进程访问同一个端口这个方法最简单适合演示和答辩。二是把dist目录部署到Nginx里通过反向代理把/api开头的请求转发到后端服务。这个方案更接近企业真实部署方式但配置Nginx本身需要额外的时间成本。答辩环节是最后一个大关卡提前准备几个核心问题的答案非常必要。老师大概率会问的问题有这些为什么选择SpringBootVueMySQL这套技术栈这时候要回答清楚技术选型的对比和理由比如SpringBoot自动配置解决了传统Spring项目的配置繁琐问题、Vue的组件化开发提升了前端代码的复用性、MySQL是开源免费的关系型数据库应用最广泛。系统架构是怎么设计的要把三层架构和调用流程讲清楚画一张架构图。数据库表之间是什么关系要把E-R图讲明白重点解释为什么通过合同表来关联商铺和租户。密码是怎么做的加密可以用BCrypt算法加盐哈希讲一下为什么不能明文存储。还有一个加分点建议在系统里做一个稍微复杂一点的业务逻辑比如合同到期自动提醒或者租金统计图表。用定时任务在合同到期的前一天把通知插入到公告表或消息表里用ECharts画一个柱状图和饼图展示各月租金收入和费用类型占比。这两个功能工作量不大但在答辩的时候属于“亮点功能”能够让老师觉得这个系统不是简单的增删改查。最后的几点实在话在做这个项目的过程中我最深的体会是写代码本身不是最难的难的是把每个环节都想明白之后再动手。数据库表设计时多想一步后面写接口就顺很多前端页面规划和接口设计保持同步联调的时候就少很多返工。整个项目做下来SpringBootVueMySQL这套技术栈的配合已经非常成熟了只要跟着一条主线走——从建表到后端接口再到前端页面——基本上不会走偏。如果时间充裕学有余力可以考虑往后端加Redis缓存热点数据比如商铺状态统计结果往前端加一个ECharts的展示大屏给系统加一个导入导出Excel的功能。这些方向都可以作为简历上的项目亮点来写但在做之前一定要评估好自己的时间和能力毕设的核心是先完成再完美千万不要在最后阶段给自己挖新坑。