ARTICLE DETAIL

资讯详情

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

SpringBoot+Vue商铺管理系统毕设实战:从建模到部署答辩

SpringBoot+Vue商铺管理系统毕设实战:从建模到部署答辩 做了这么多年毕业设计指导每年都会遇到一批选商铺管理系统这个题目的学生。说实话这个题目属于典型的看起来简单、做起来琐碎的类型——你说它难吧无非就是增删改查加几张表你说它简单吧真要做出一套能答辩、敢演示、扛得住老师追问的系统里面涉及的坑一点都不少。这篇文章我打算把整套思路掰开揉碎从业务建模到表结构设计从代码分层到部署上线把我会怎么带学生做这套系统、每一步为什么这么做全部写出来。这篇文章适合几类人看正在做SpringBootVue毕设的学生、想把管理系统类项目做得更规范一点的开发者、以及时间紧张但不想糊弄毕业设计的同学。我会尽量把原理和实操都讲到争取让基础一般的同学也能照着落地。1. 先把业务想明白商铺管理系统到底在管什么很多同学拿到题目第一反应是建表、写接口、糊页面。这是典型的手比脑子快。做管理系统类的毕设最忌讳一上来就写代码。你连这个系统要解决什么问题、角色有哪些、业务流转路径是什么都没想清楚写出来的东西必然是散的答辩时一问三不知。1.1 核心角色与业务闭环商铺管理系统本质上是为高校或园区的商业空间提供一套数字化管理工具。以太原学院这样的场景为例系统面向的核心角色有三类系统管理员、商铺管理人员招商/运营岗、访客或普通用户比如校内师生。围绕这三类角色业务可以拆成一个闭环商铺入驻商铺信息登记、资质审核、入驻时间登记合同管理租期设定、租金标准、合同起止时间、续约或退租费用管理租金计算、费用记录、收缴状态跟踪日常运营商铺状态变更营业中/停业/装修、巡检记录、投诉反馈数据统计商铺数量、出租率、租金收缴率、经营状态分布这个闭环听起来不复杂但它是整个系统的业务骨架。你的数据库表设计、后端接口划分、前端页面路由全部要围绕这个骨架展开。很多同学做出来的系统页面很多但逻辑不通就是因为没有先画这个闭环。1.2 功能模块怎么切才不会给自己挖坑给毕设项目切功能模块我有一条铁律宁可少做几个功能也不要把每个功能做成半成品。答辩老师翻你系统的时候最反感的不是功能少而是点了某个菜单弹出一堆报错或者一个按钮点了没反应。基于这条铁律我建议把功能切成两大部分管理端面向管理员和运营人员商铺管理商铺信息CRUD、状态流转、入驻审核租户管理租户/商户信息维护与商铺建立关联合同管理合同登记、到期提醒、续约操作收费管理费用项配置、账单生成、缴费状态更新巡检管理巡检记录登记、异常上报数据概览核心指标的图表展示用户端/展示端面向普通访客商铺列表按分类、状态浏览商铺商铺详情查看经营内容、位置、营业时间公告信息园区通知、临时调整信息我见过不少学生想做一个商户自助入驻的流程就是商户自己注册账号、提交资料、等待审核。想法很好但这个功能会牵扯到多角色权限、工作流引擎、消息通知工作量瞬间翻倍。我的建议是如果时间充裕、基础好可以做成加分项如果时间紧就把这个功能砍掉改成管理员代录信息效果一样能说清楚。1.3 技术栈选型的实话题目限定了SpringBootVueMySQL这本身就是当前国内高校毕设的主流套餐也是企业里中小型管理系统最常用的一套组合。这里有个心态问题要摆正毕设不追求技术多新追求的是你在规定时间内把一套需求明确的东西跑通并且每个环节都能讲出道理来。版本选择上我推荐SpringBoot用2.7.x系列不要一上来追3.xVue用2.x版本配Element UI或者Vue3配Element PlusMySQL用8.0以上版本5.7也能跑但8.0是趋势SpringBoot 3.x相比2.x最大的变化是底层基于Jakarta命名空间很多老教程里的代码直接迁移会报错。你对SpringBoot还没熟到一定程度的时候用3.x是给自己增加排查成本。Vue这边同理Vue2的生态文档、Element UI的组件案例多得是遇到问题搜一下就出来了。Vue3Element Plus也很成熟个人偏好决定即可。2. 数据库设计表结构就是系统的地基表结构设计是整篇毕设里最见功底的部分。为什么这么说因为答辩老师一眼就能看出你的表是赶工乱画的还是认真设计过的。表与表之间的关系、字段命名规范、索引使用、时间字段的处理全是可以看出门道的地方。2.1 核心表结构落地方案给商铺管理系统建表我建议至少覆盖以下核心表。这里给出MySQL的建表核心字段思路实际创建时还需要根据你的功能扩展微调。商铺信息表shop字段id, shop_name, shop_code, category_id, floor, location_desc, area_size, rent_price, shop_status, owner_id, start_date, end_date, create_time, update_time, deletedshop_code给商铺编一个唯一编码方便合同、收费模块引用shop_status建议用数字字典0空置、1已入驻、2装修中、3停业、4已退租deleted是做逻辑删除的标志位。管理系统里物理删除是禁忌万一删错数据没法恢复逻辑删除还能留底租户/商户信息表tenant字段id, tenant_name, contact_person, contact_phone, id_card, business_license, audit_status, create_time, update_time, deleted租户和商铺的关系一个租户可以租多个商铺一个商铺在某个时间段只能对应一个租户。这是一对多关系在商铺表里加owner_id即可不用单独建关联表。合同表contract字段id, shop_id, tenant_id, contract_no, start_date, end_date, rent_amount, deposit_amount, payment_cycle, status, create_time, update_time, deleted合同表是整个系统的枢纽表。shop_id和tenant_id都指向各自的表status可以用来区分正常履约、到期、提前终止等状态。支付宝里的账单-合同-商铺链路以后扩展开票、缴费记录都从这里出发。费用记录表bill_record字段id, contract_id, shop_id, bill_type, amount, status, pay_date, remark, create_time, update_timebill_type涵盖租金、物业费、水电费等。为什么要单独建表而不直接写在合同表里因为一个合同会产生多期费用每次缴费都是一条独立记录这是典型的主表-明细表结构。巡检记录表inspection_record字段id, shop_id, inspector, inspection_date, content, result, issue_desc, create_time巡检算是管理系统的加分模块。很多同学不知道这个表该怎么设计才能兼顾信息完整和简洁这里给一个参考result字段用0/1表示正常/异常异常时issue_desc必填。前端可以据此做联动校验这是一个很小的细节但答辩时拿出来说很有说服力。系统用户表sys_user字段id, username, password, nickname, role, avatar, status, create_time, update_time密码存储不要用明文至少用MD5加盐或者BCrypt加密。Spring Security和Sa-Token都内置了BCrypt支持基本是现成的。2.2 表设计的三个实战经验**时间字段统一处理。**我在指导毕设时反复强调所有表必须有create_time和update_time两个字段。MySQL可以使用DEFAULT CURRENT_TIMESTAMP和ON UPDATE CURRENT_TIMESTAMP来自动维护或者由MyBatis-Plus的自动填充功能处理。统一时间字段的命名规范后面写按时间排序、按月统计查询会舒服很多。**金额字段统一用decimal。**租金、押金这类涉及金额的字段千万别用double或float。二进制浮点数在精度上有天生缺陷0.10.2算出来是0.30000000000000004。钱这种东西容不得误差用DECIMAL(10,2)最稳妥。这个知识点很简单但每次讲到都有学生踩坑。**外键约束能不加就不加。**这是企业开发里一条比较反直觉的经验。数据库物理外键会导致高并发场景下的锁竞争还会增加删除和更新数据的复杂度。现在的主流做法是在逻辑层Service层维护引用关系也就是所谓逻辑外键。一张表的shop_id引用另一张表的id但数据库层面不建FOREIGN KEY约束。你的数据完整性靠代码保证靠事务保证而不是靠数据库约束。毕设答辩时老师如果问为什么没有外键你就可以把这套逻辑讲出来绝对比忘了加好得多。3. 后端从零搭建SpringBoot项目的骨架和核心接口后端项目的搭建我见过太多学生栽在配置上爬不起来。其实SpringBoot的设计哲学就是约定大于配置你只需要把关键的地方配置对剩下的交给框架自动搞定。3.1 项目初始化与分层结构创建项目我用的是Spring Initializr可以idea自带的也可以start.spring.io网页端。关键依赖勾选Spring Web构建Web接口MySQL Driver数据库驱动MyBatis-Plus或Spring Data JPA数据访问层这里建议优先MyBatis-Plus。原因很实在JPA的懒加载、级联操作、实体关系映射比较难掌握一旦没用好查询N1次、级联删除失控这类问题够你排查好几天的。MyBatis-Plus的CRUD近乎傻瓜式BaseMapper里把增删改查全给你写好了还自带分页插件和条件构造器配合毕设项目的CRUD高频场景效率拉满。后端包结构我习惯这样分com.taiyuan.shop ├── common // 通用类统一返回结果、异常处理、常量定义 ├── config // 配置类跨域配置、MyBatis-Plus分页插件配置、拦截器配置 ├── controller // 接口层只做参数接收和结果返回 ├── service // 业务逻辑层核心代码都在这里 │ └── impl // Service实现类 ├── mapper // 数据访问层继承BaseMapper ├── entity // 实体类对应数据库表 └── vo // 视图对象给前端返回的组合数据这样的分包形式在毕设答辩中非常讨喜——它直接展示了你对分层架构的理解。每一层各司其职哪一层出问题就去哪一层排查这是长期开发积累下来的工程经验。3.2 商铺管理CRUD完整链路对一个管理系统来说CRUD就是地基中的地基。这部分我拆细一点以商铺管理为例把完整链路走一遍。实体类entity/Shop.javaData TableName(shop) public class Shop { TableId(type IdType.AUTO) private Long id; private String shopName; private String shopCode; private Long categoryId; private String floor; private String locationDesc; private BigDecimal areaSize; private BigDecimal rentPrice; private Integer shopStatus; private Long ownerId; private LocalDateTime startDate; private LocalDateTime endDate; private LocalDateTime createTime; private LocalDateTime updateTime; TableLogic private Integer deleted; }TableLogic是MyBatis-Plus的逻辑删除注解。加了它之后你调deleteById时框架自动改成UPDATE语句把deleted置为1查询时自动加WHERE deleted 0。讲这个注解的用法比自己封一层拦截器强得多。Controller层RestController RequestMapping(/api/shop) public class ShopController { Resource private ShopService shopService; PostMapping(/page) public ResultIPageShopVO page(RequestBody ShopQueryDTO queryDTO) { return Result.success(shopService.getShopPage(queryDTO)); } GetMapping(/detail/{id}) public ResultShopVO detail(PathVariable Long id) { return Result.success(shopService.getShopDetail(id)); } PostMapping(/save) public ResultVoid save(RequestBody Shop shop) { shopService.saveShop(shop); return Result.success(); } PutMapping(/update) public ResultVoid update(RequestBody Shop shop) { shopService.updateShop(shop); return Result.success(); } DeleteMapping(/delete/{id}) public ResultVoid delete(PathVariable Long id) { shopService.deleteShop(id); return Result.success(); } }用POST /page而不是GET /page做分页查询是因为查询条件可能包含对象嵌套、数组参数GET带一堆复杂参数在URL里既丑又容易触发长度限制。POSTJSON是全行业的主流做法。Service实现类Service Slf4j public class ShopServiceImpl extends ServiceImplShopMapper, Shop implements ShopService { Resource private ShopMapper shopMapper; Override public IPageShopVO getShopPage(ShopQueryDTO queryDTO) { PageShop page new Page(queryDTO.getPageNum(), queryDTO.getPageSize()); LambdaQueryWrapperShop wrapper new LambdaQueryWrapper(); wrapper.like(StringUtils.isNotBlank(queryDTO.getShopName()), Shop::getShopName, queryDTO.getShopName()) .eq(queryDTO.getShopStatus() ! null, Shop::getShopStatus, queryDTO.getShopStatus()) .orderByDesc(Shop::getCreateTime); // 注意分页插件要提前配置不然这里的分页不会生效 IPageShop shopPage shopMapper.selectPage(page, wrapper); return convertToVO(shopPage); } }**为什么用LambdaQueryWrapper**它的类型安全是真的香。字段名写错了编译期就能报出来而不是运行期告诉你SQLException: Unknown column。使用like(条件, 字段, 值)这种重载第一个boolean参数控制这个条件是否拼接查询参数为空时自动忽略不用写一堆if判断拼字符串SQL了。3.3 文件上传一个不显眼但人人都会问的考点商铺系统里租户的营业执照、商铺的实拍图、巡检拍的现场照片都涉及文件上传功能。这个功能看似简单但做不好会在答辩时露怯。后端核心实现PostMapping(/upload) public ResultString upload(RequestParam(file) MultipartFile file) { if (file.isEmpty()) { return Result.error(文件不能为空); } // 校验文件大小这里限制为5MB if (file.getSize() 5 * 1024 * 1024) { return Result.error(文件大小不能超过5MB); } // 校验文件后缀 String originalFilename file.getOriginalFilename(); String ext originalFilename.substring(originalFilename.lastIndexOf(.)); ListString allowExt Arrays.asList(.jpg, .jpeg, .png, .pdf); if (!allowExt.contains(ext.toLowerCase())) { return Result.error(不支持的文件类型); } // 重命名文件防止文件名冲突 String fileName UUID.randomUUID().toString().replace(-, ) ext; // 存储路径按日期分目录方便后续管理 String datePath new SimpleDateFormat(yyyyMMdd).format(new Date()); String filePath uploadDir / datePath / fileName; File dest new File(filePath); if (!dest.getParentFile().exists()) { dest.getParentFile().mkdirs(); } file.transferTo(dest); return Result.success(http://localhost:8080/files/ datePath / fileName); }要踩过的坑这里提前说一下跨域问题。页面在8081端口文件服务在8080端口直接img src访问8080端口的图片浏览器会出于安全策略拦截。解决办法在config里加一个CorsConfig或者在SpringBoot里写个WebMvcConfigurer配置addCorsMappings允许跨域访问。这个坑以前的学生几乎每个人都踩过。再有一个细节不要用原文件名直接存服务器。除了防止重名覆盖更重要的是防止路径穿越攻击——用户上传一个../../../../etc/passwd名字的文件服务端如果直接拼接路径就相当于给别人留了个后门。用UUID重命名是最稳妥的。3.4 Excel导出毕设里性价比最高的仪式感管理系统里谁也躲不过报表导出。把商铺列表、合同台账导出成Excel哪怕只是几十行数据给答辩老师演示的时候那个专业技能展示的观感比你在嘴上说我会POI要强得多。推荐用EasyExcel阿里巴巴开源的就是为了极简两个字PostMapping(/export) public void export(HttpServletResponse response) throws IOException { ListShopExportDTO list shopService.listAllForExport(); response.setContentType(application/vnd.openxmlformats-officedocument.spreadsheetml.sheet); response.setCharacterEncoding(utf-8); String fileName URLEncoder.encode(商铺信息, UTF-8).replace(, %20); response.setHeader(Content-Disposition, attachment;filename fileName .xlsx); EasyExcel.write(response.getOutputStream(), ShopExportDTO.class) .sheet(商铺列表) .doWrite(list); }ShopExportDTO里加ExcelProperty(商铺名称)之类的注解定义每一列的标题。导出的列顺序、列名全由注解控制几十行代码搞定一个工序繁杂的功能。这是典型的小体量大价值强烈建议加进毕设里。3.5 统一结果返回与全局异常处理所有Controller的返回类型不要各写各的定义好统一的结构体Data public class ResultT { private Integer code; private String message; private T data; public static T ResultT success() { return new Result(200, 操作成功, null); } public static T ResultT success(T data) { return new Result(200, 操作成功, data); } public static T ResultT error(String message) { return new Result(500, message, null); } }再配合RestControllerAdvice做全局异常处理。这样统一的好处前端axios拦截器只需要判断code是不是200就能决定弹成功提示还是错误提示。前端代码整洁后端也不用手忙脚乱地到处try-catch。4. 前端工程化Vue项目从搭建到功能的完整路径前端的工程量并不比后端少只是墙倒众人推很多人以为Vue就是套模板。真正写起来路由设计、状态管理、组件通信、接口封装处处都有门道。4.1 创建项目和目录规划前端我用Vite作为构建工具命令很简单npm create vitelatest shop-admin -- --template vue cd shop-admin npm install npm install axios vue-router pinia element-pluspinia是Vue3生态里的状态管理库比Vuex的API简单太多想存store直接defineStore一下就行。Vite相比Webpack最大的优势是快冷启动秒开热更新基本无感这对开发体验的提升非常明显。目录结构src ├── api // 接口请求封装每一类请求单独一个文件 ├── assets // 静态资源 ├── components // 公共组件 ├── router // 路由配置 ├── store // 状态管理 ├── views // 页面组件 │ ├── shop │ ├── contract │ ├── dashboard │ └── login ├── utils // 工具函数 ├── App.vue └── main.js4.2 axios封装请求拦截器就这么写接口封装是前端工程化的第一步。所有请求统一走一个axios实例好处是可以在拦截器里统一处理token、统一处理报错提示// src/utils/request.js import axios from axios import { ElMessage } from element-plus import router from ../router 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) { return res } else if (res.code 401) { ElMessage.error(登录已过期请重新登录) router.push(/login) return Promise.reject(new Error(unauthorized)) } else { ElMessage.error(res.message || 请求失败) return Promise.reject(new Error(res.message)) } }, error { ElMessage.error(error.message || 网络异常) return Promise.reject(error) } )这样做最大的收益是业务页面里完全不需要再写try-catch处理报错提示。弹错误提示、跳登录页这种横切逻辑全部收敛在拦截器里。页面代码只关心成功之后做什么代码阅读体验直线上升。有同学问token怎么放。现在主流做法是用localStorage或pinia存。我建议通过pinia存一份、localStorage存一份刷新页面时从localStorage恢复保证状态不丢。4.3 动态路由和权限控制毕设系统必然有角色之分管理员看的页面和普通用户看的页面不能一样。这就要用到动态路由根据登录用户角色动态往路由表里添加菜单。router/index.js里先定义基础路由login、404然后addRoute动态拼接权限路由// 动态注册路由 const modules { admin: [ { path: /shop, component: () import(../views/shop/index.vue), meta: { title: 商铺管理 } }, { path: /contract, component: () import(../views/contract/index.vue), meta: { title: 合同管理 } }, // ...其他管理员菜单 ], user: [ { path: /shop/list, component: () import(../views/shop/list.vue), meta: { title: 商铺列表 } } ] } export function setupDynamicRoutes(role) { const routes modules[role] || [] routes.forEach(route { router.addRoute(layout, route) }) }路由守卫配合使用router.beforeEach((to, from, next) { const token localStorage.getItem(token) if (!token to.path ! /login) { next(/login) } else { next() } })动态路由路由守卫按钮级权限控制这三个层次组合起来在答辩时可以讲出一个完整的权限控制方案菜单是动态生成的、请求有token校验、按钮有权限指令控制显示隐藏。哪怕你写的代码不复杂但这一套逻辑完整自洽很能体现思考深度。4.4 核心页面商铺列表页实现要点列表页是管理系统里出镜率最高的页面。以商铺管理为例一个标准列表页包含搜索区、操作区、数据表格、分页器。template div classshop-container el-card !-- 搜索区 -- el-form :modelqueryParams inline el-form-item label商铺名称 el-input v-modelqueryParams.shopName placeholder请输入商铺名称 clearable / /el-form-item el-form-item label状态 el-select v-modelqueryParams.shopStatus clearable placeholder请选择状态 el-option label空置 :value0 / el-option label已入驻 :value1 / el-option label装修中 :value2 / /el-select /el-form-item el-form-item el-button typeprimary clickhandleQuery搜索/el-button el-button clickresetQuery重置/el-button /el-form-item /el-form !-- 操作区 -- el-button typeprimary clickopenDialog()新增商铺/el-button el-button typedanger clickhandleBatchDelete批量删除/el-button !-- 表格区 -- el-table :datashopList border stripe el-table-column typeselection width55 / el-table-column propshopName label商铺名称 / el-table-column propfloor label楼层 / el-table-column propareaSize label面积(m²) / el-table-column proprentPrice label租金(元/月) / el-table-column label状态 template #default{ row } el-tag :typestatusMap[row.shopStatus].type {{ statusMap[row.shopStatus].label }} /el-tag /template /el-table-column el-table-column label操作 width200 template #default{ row } el-button link typeprimary clickopenDialog(row)编辑/el-button el-button link typedanger clickhandleDelete(row)删除/el-button /template /el-table-column /el-table !-- 分页器 -- el-pagination v-model:current-pagequeryParams.pageNum v-model:page-sizequeryParams.pageSize :totaltotal :page-sizes[10, 20, 50, 100] layouttotal, sizes, prev, pager, next, jumper changegetList / /el-card /div /template写筛选条件时有一道常见的考题后端用eq判断状态精确匹配用like模糊匹配名称这两者组合起来怎么不冲突答案很简单条件构造器里like和eq可以链式调用只要每个条件前面那个boolean参数做好判空框架会自动帮你在SQL里决定加不加这个条件。4.5 数据统计大屏毕设的演示视频或现场展示第一个打开的页面最好是个漂亮的数据概览页。如果你打开系统看到一堆增删改查表格评委的兴趣直接减半。用ECharts做数据可视化是我们的秘密武器之一它跟Vue的适配做得很好有现成的vue-echarts封装。我建议图表和数据要对应后端真实数据不要写死。具体做法是后端提供统计接口例如按照商铺状态统计总数、按照楼层统计出租率、近半年费用收缴趋势等。前端用axios拿到数据后动态更新ECharts的option。这样做的好处有两点一是答辩时老师随便改一条数据图表跟着变真实感拉满二是展现了你前后端数据流通的整体设计不只是套了个echarts模板。5. 锦上添花的进阶选择认证方案和脚手架如果你的毕设周期还有富余或者指导老师明确提出要有点技术含量那么下面几个方向值得花一点时间。注意我的用词——花一点时间不是让你把毕设变成科研项目控制投入产出比很重要。5.1 登录认证Sa-Token还是Spring Security管理系统必然要有登录功能但登录也分三六九等。最基础的是session登录前端cookie带着sessionId后端查session判断是否登录。稍微进阶一点的是JWT方案用户登录成功后后端签发一个token前端存在localStorage里每次请求带到请求头后端校验token合法性。这两种方案的原理差异本身就是答辩时的高频问题。我的推荐是Sa-Token。理由它把登录认证、权限认证、踢人下线、账号封禁这些功能封装好了API极其简单登录的时候StpUtil.login(userId)判断登录用StpUtil.isLogin()校验权限用SaCheckPermission(shop:add)。相比Spring Security那庞大的过滤器链和繁琐的配置Sa-Token对初学者友好得多。答辩时你能讲清楚token怎么签发、怎么校验、拦截器怎么配置就已经超过多数同学了。如果你用Spring Security那你要搞明白UserDetailsService、SecurityFilterChain、AuthenticationManager这一堆概念这些不带几个月的实战经验真不好消化。同样的功能Sa-Token十分钟配完Spring Security可能要半天。不是你不行是这框架本来对新手就不算友好它功能全但抽象度高。5.2 前后端分离部署的一个坑很多同学的毕设是前后端分开跑后端8080前端8081部署时各跑各的。如果你的时间足够我强烈建议学一下前端打包放进SpringBoot这一套。命令很简单npm run build然后dist目录下生成一堆静态文件。把dist目录的东西复制到SpringBoot的src/main/resources/static下重新打包SpringBoot工程你就会发现一个jar包搞定所有前端页面、后端接口、静态资源全部在一个端口上运行。演示的时候一个命令java -jar xxx.jar启动干净利落。这个做法有个需要配置的地方SpringBoot默认只扫描/static目录下的静态资源你直接访问http://localhost:8080/应该能看到首页但如果你用vue-router的history模式直接访问/shop这种路径会404因为后端没有对应的Controller处理它。解决方法是写一个WebMvcConfigurer把非API的所有路径都转发到index.html。Configuration public class WebConfig implements WebMvcConfigurer { Override public void addViewControllers(ViewControllerRegistry registry) { registry.addViewController(/{path:[^\\.]*}).setViewName(forward:/index.html); } }这段配置配合history模式路由是前后端分离项目打成单包部署的标准玩法。细节是正则[^\\.]*把带点的路径排除掉这样静态资源文件js、css、图片不会被误转发。5.3 Servlet路径的静态资源映射上面提到的文件上传功能如果你把图片存放路径放在项目之外的磁盘目录比如/Users/xxx/uploadsSpringBoot默认不扫描这个外部目录。需要在config里配置资源映射Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler(/files/**) .addResourceLocations(file: uploadDir /); }这个配置干了一件事浏览器访问http://localhost:8080/files/xxx.jpg时SpringBoot去磁盘上找对应文件返回给前端。很多学生上传成功但显示不了图片就是因为差这一步配置。6. 部署、性能排查、答辩Hold场最后的决胜环节代码写完了不代表万事大吉。部署是对系统的最终验收答辩是对你的最终验收。这两个环节的坑我见过的比代码里的还多。6.1 MySQL安装和配置的高频坑热搜词里一堆MySQL安装教程不是没有原因的。在Windows 10上安装MySQL确实有太多细节能绊人下载的安装包版本太新安装中途卡住没反应端口3306被占用服务起不来或者能装但起不来服务安装时设置了密码连接时又提示Access denied字符集没设置utf8mb4存中文变成问号我给出一个最稳的路线用MySQL Community Server的ZIP版解压后手动初始化适合熟悉命令行的人完全没经验的人装8.0.x的MSI安装包一路Next设置root密码时注意记住密码。装好之后一定要检查两件事-- 查看字符集是不是utf8mb4 SHOW VARIABLES LIKE character_set%; -- 查看时区设置 SHOW VARIABLES LIKE %time_zone%;MySQL 8.0默认时区是系统时区而SpringBoot的JDBC连接串里serverTimezone如果不配或者配错就会出现日期差8小时之类很头痛的问题。我习惯在JDBC连接串上显式声明jdbc:mysql://localhost:3306/shop_db?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai6.2 SpringBoot项目常见启动连锁问题启动不了或者启动后访问不通90%集中在以下几个点端口被占用。8080起不来用netstat -ano | findstr 8080查一下是谁占着然后任务管理器结束进程或者直接在application.yml里把端口换掉。server.port: 8080改一改就是了这不是什么大事不用慌。MyBatis-Plus分页插件没配置。你兴高采烈调selectPage结果所有数据一次查出来翻页完全没效果。这时候去看你的config是不是没有Configuration加MybatisPlusInterceptor并注册PaginationInnerInterceptor。依赖版本冲突。这个排查起来最费劲。我的建议是尽量在项目的初始阶段就把主要依赖的版本配对固定好像SpringBoot、MyBatis-Plus这些核心依赖不要随意升级。另外如果你是跟着网上的教程一步步敲的且视频更新时间比较早里面的版本号和依赖标定方式可能已经变了需要结合官方文档做适配。6.3 答辩高概率问题清单提前想好答案答辩的恐惧往往来自未知。我把这么多年高频出现的追问问题列出来你可以对着自己的项目逐个过一遍你这个项目有哪些亮点这是最经典的开场问题。不要说用了前后端分离这种话这种默认配置不算亮点。要说就说具体的比如我用了动态路由做权限控制不同角色登录后看到不同的菜单我用EasyExcel实现了数据导出支持大数据量场景我在文件上传时做了文件类型和大小双重校验并且把文件名重命名为UUID避免路径穿越攻击。每一项都要能展开讲别只丢一句名词。你的表结构为什么这么设计准备好讲清楚表与表之间的关联关系。比如为什么把合同和费用分开建表、为什么用逻辑删除而不是物理删除、为什么金额用decimal不用double。不建议背课本原话就用你做项目时实际遇到的情况讲比如我一开始用double算租金后来发现0.10.2精度有问题租金会差几分钱所以换成了decimal这种真实的踩坑故事比教科书回答可信十倍。你的项目还有什么不足这个问题不是让你承认项目一无是处而是考你对自己项目的认知深度。我建议这么回目前数据量较小没有引入缓存文件服务用的是本地磁盘如果部署到生产环境应该用对象存储或Nginx做静态资源服务系统目前只支持单机部署未来如果需要高可用可以引入负载均衡和分布式事务方案。重点是我知道它会遇到什么问题、我知道该怎么改而不是我不知道有什么问题。JWT和Session有什么区别这是原理类必考题。核心答法Session是服务端存储用户状态的方案Cookie里存SessionIdJWT是无状态方案服务端不存储用户会话token里本身包含用户信息和过期时间通过签名验证真伪。再说说各自的优劣Session需要服务端存储、集群场景需要做Session共享JWT天然适合分布式、跨域但token一旦签发在过期前无法主动失效可能被劫持重放。这两个知识点能各讲一分钟这个问题就稳过。你遇到的最大的技术难点是什么怎么解决的建议提前打好草稿。可以从你开发过程中挑一个真实的难题来讲。比如动态路由配置时刷新页面路由丢失、跨域携带token失效、Excel导出后前端下载文件格式损坏。不要怕问题幼稚——如果你能讲清楚报了什么错、怎么排查的、最终如何解决这本身就证明了你具备解决问题的能力。7. 写在最后的实在话系统开发完之后毕设还剩两个容易被低估的环节文档撰写和演示视频录制。论文和文档这块把系统设计一章写清楚比什么都重要——需求分析、表结构设计、接口设计、核心代码逻辑说明这四块每块写足你的论文篇幅和厚度基本就稳了。画图用Visio或ProcessOn画清楚流程图和架构图不要随手截图糊上去。写代码的时候随手记几个核心逻辑的备注或者留着当时的git提交记录会让论文的实现过程增加许多可信细节。现场演示前务必检查演示数据是否足够饱满数据列表没有几十条都显得很空、有没有提前录制好兜底视频现场网络抽风、数据库连接失败时用、F12控制台有没有红色报错哪怕这个报错不影响功能被老师瞄到还是会减分。我在带项目过程中反复说一句话**先跑通再完美最后才是写论文。**顺序不要颠倒——很多人一上来就埋头写论文导致代码全部是网上粘贴的自己没真正跑通答辩时一点底气都没有。而先把整个流程走一遍把每一步的坑都踩了填了你在答辩时讲出来的内容自然有底气、有细节、有画面。这篇内容算是我多年带毕设项目的一点经验沉淀。按照这个路线走下来你的商铺管理系统即使做不到惊艳但做到完整、能跑、说得清让答辩老师挑不出大毛病是完全可以实现的。如果操作过程中有哪个环节卡住了按照文章里的排查思路一步步过基本都能解决。祝顺利完成。
返回列表