
如果有人问我毕设或课设做什么项目性价比最高我的第一反应多半是校园失物招领系统。技术栈非常标准SpringBootVueJavaMySQL业务上却又绕不开信息发布、认领审核、通知公告这些经典场景正好把一个全栈项目该有的东西都串起来了。最近我把这套系统重新梳理了一遍发现它不只适合毕业设计和课程设计对刚接触前后端分离开发的同学来说也是一条非常完整的练手路径。这篇就顺着我知道的常见实现思路把需求拆解、数据库设计、后端接口、前端页面、环境启动和排坑经验都过一遍。文章里提到的做法不一定是最花哨的但一定是能让项目跑起来、能答辩、能真正学到东西的方案。1. 校园失物招领系统的需求是怎么拆出来的很多同学拿到这类题目第一反应是赶紧写代码结果做着做着发现要么页面堆砌、要么逻辑混乱。其实这种业务型项目最该先做的是需求拆解。你连谁在用、要用到什么程度都没搞清楚后面写出来的功能大概率是废的。1.1 丢东西的人、捡东西的人、管东西的人三种视角看问题失物招领表面上就是“有人丢东西、有人捡东西”但一旦落地成系统至少要回答三组问题。丢东西的人想知道发布后有没有人看到有没有人捡到类似物品认领的进度到了哪一步。所以系统必须提供“寻物启示发布”“浏览捡到物品列表”“提交认领申请”“查看我的发布和状态”这些功能。捡东西的人想知道捡到的物品该交到哪里怎么证明我捡到的就是对方丢的那个会不会白跑一趟。所以系统需要“拾取物品登记”“物品描述与会合地点”“认领凭证核验”这类支撑。管理人员更关心信息是不是真实有效同学有没有拿错东西要不要处理一些纠纷。所以后台需要“审核发布”“核实认领”“下架过期信息”“简单的统计”功能。这三组问题一列你就能很自然地分出普通用户端和管理员端两套界面也能确定核心的业务对象就是“失物信息”“拾物信息”“认领记录”“用户信息”这四类。1.2 核心业务流程从发布到认领再到归档我习惯把失物招领的流程画成两条主线一条围绕“寻物”一条围绕“拾取”。寻物线是这样的用户登录后发布寻物启示填写物品名称、特征、丢失地点、图片提交后进入待审核状态管理员审核通过信息公开展示如果有人捡到了或看到了可以在线上提交线索或认领线索用户看到线索后自行确认或者由管理员协助联系最后关闭该启示。拾取线的逻辑略有不同用户捡到物品后先登记成“拾物信息”管理员同样要审核系统公开后失主看到信息提交认领申请并描述物品细节管理员把失主描述与捡拾人登记的细节进行核验核验一致则安排线下交接交接完成后状态变成“已完成”。这两条线看起来都是“发布-审核-匹配-闭环”但匹配逻辑不太一样寻物启示是主动找别人拾物信息是等别人来认领。数据库设计阶段如果把这两类信息放到同一张表业务上会更灵活后面第3节我会详细说表结构怎么处理。1.3 功能清单要控制住防止做飘了毕设和课设最大的坑是需求膨胀。我见过一个同学把在线客服、积分商城都塞进失物招领系统结果答辩时老师一问他连核心流程都说不清。功能不是越多越好能自圆其说、每个功能都有存在价值这才是评分标准。建议按优先级分三档。第一档必须要有登录注册、失物发布、拾物发布、信息列表、认领申请、管理员审核。第二档建议要有图片上传、状态查询、模糊搜索、分页、公告栏、用户信息修改。第三档可选数据统计、导出报表、消息提醒、路由懒加载、按钮权限。先把一二档做完时间富余再做第三档这样项目体量也刚好匹配毕设的篇幅要求。2. 技术选型SpringBootVueMySQL这一套为什么刚好合适技术选型不是越新越好、越炫越好而是要符合项目阶段和环境。失物招领系统用 SpringBootVueMySQL 的组合可以说是目前校园项目里最成熟的搭配之一原因其实很实在。2.1 SpringBoot让后端开发不必从零搭配置如果你用过传统的SSM项目一定还记得那一大堆XML配置什么springmvc.xml、mybatis.xml、web.xml项目还没写需求光配置就能折腾两三天。SpringBoot的核心优势就是“约定优于配置”内嵌Tomcat自动配置SpringMVC、数据源、JSON序列化这些基础设施。你只要在pom里加上依赖写一个启动类一个可运行的RESTful后端就立起来了。另外SpringBoot对校园项目特别友好的点是生态丰富。集成MyBatis或MyBatis-Plus做数据访问很快集成Spring Security或者手写拦截器做登录鉴权也都有成熟案例遇到问题网上一搜一大把不容易卡死。对基础薄弱的同学来说“能找到解决方案”这个优势比技术本身更值钱。2.2 Vue组件化让前端页面复用和管理Vue最直观的感受是组件化。一个失物卡片、一个上传组件、一个分页器写一次就能到处复用。首页、列表页、个人中心、后台管理页面看着很多实际上都是几个核心组件的排列组合。Vue的另一大优势是响应式数据和单文件组件。数据驱动视图发布一条失物信息页面列表自动更新不用像原生JS那样手动操作DOM。配合Vue Router实现前端路由Vuex或Pinia管理用户登录状态、全局信息整一个前端工程的结构就非常清晰。加上Element UI或Element Plus这套组件库后台管理页面半天就能搭出骨架把更多时间留给业务逻辑。2.3 MySQL结构化数据天然适合这类业务失物招领的数据有明显的结构化特征用户有用户名、密码、电话失物有名称、地点、时间、状态。这些数据之间的关系可以用主外键清晰表达MySQL作为关系型数据库事务支持好学习成本低几乎是校园项目的默认选项。MySQL还有一个隐藏优势很多人电脑里已经装好了版本兼容问题少。5.7和8.0都行只要驱动和连接串写对很少遇到奇奇怪怪的坑。相比引入MongoDB或者RedisMySQL对这个项目来说已经绰绰有余还能让老师一眼看懂你的数据设计水平。3. 数据库设计失物招领系统的数据模型是项目的灵魂数据库设计直接决定你的代码好不好写。很多同学一上来就建表边写边加字段最后表结构乱七八糟查询逻辑全是硬编码。我建议按“实体-关系”的思路先把表想清楚再动手写。3.1 五张核心表分别管什么我的设计习惯是把用户、物品、认领、公告分开其中物品这一块又拆成“失物信息”和“拾物信息”。如果你想让代码更精简也可以把两类物品合并成一张表用 type 字段区分是“寻物”还是“拾物”。合并的好处是列表和搜索逻辑统一坏处是有些字段语义要抽象。我做的项目里更偏向合并成一张 item 表这里给你看看这个思路。user 表存放用户账号信息区分普通用户和管理员。item 表存放寻物启示和拾物信息用 type 字段标识物品方向。claim_record 表认领记录无论是寻物还是拾物认领动作都记录在这张表。notice 表管理员发布的公告或站内通知。upload_file 表图片等附件记录方便扩展多图上传。这样的设计保持了业务表达的一致性又不会因为两张结构高度相似的物品表导致代码重复。放在毕设论文的数据库设计章节里也能讲得比较清楚。3.2 关键字段和状态枚举怎么定义item 表的核心字段大概有这些id、user_id、type、title、description、location、item_type、image_url、status、publish_time、end_time、click_count。其中 type 字段我建议这么设计0 表示拾物捡到东西等待认领1 表示寻物丢了东西寻找线索。status 字段表示生命周期状态一般是 0-待审核1-审核通过/发布中2-已认领/交接中3-已完成4-已下架。claim_record 表的核心字段id、item_id、claim_user_id、description、contact_info、status、create_time、update_time。认领状态可以设计成 0-待审核1-已通过2-已拒绝3-已完成。这里注意item 表的状态和 claim_record 表的状态是两套状态后端联动时要非常小心后面第4节我会专门讲这个状态机。user 表除了基本账号信息加一个 role 字段就够了0 是普通用户1 是管理员。不要自己设计复杂的权限表对于这种规模的项目角色字段比权限表好用十倍。3.3 建表SQL直接贴出来直接给一个简化版建表SQL大家可以根据自己系统的情况调整。我用的是 MySQL 5.7 的写法8.0 也完全兼容。CREATE DATABASE IF NOT EXISTS lost_found DEFAULT CHARACTER SET utf8mb4; USE lost_found; CREATE TABLE user ( id INT NOT NULL AUTO_INCREMENT, username VARCHAR(50) NOT NULL COMMENT 登录账号, password VARCHAR(100) NOT NULL COMMENT MD5加密后的密码, real_name VARCHAR(50) DEFAULT NULL COMMENT 真实姓名, phone VARCHAR(20) DEFAULT NULL COMMENT 联系电话, role TINYINT DEFAULT 0 COMMENT 0普通用户 1管理员, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_username (username) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT用户表; CREATE TABLE item ( id INT NOT NULL AUTO_INCREMENT, user_id INT NOT NULL COMMENT 发布人ID, type TINYINT NOT NULL COMMENT 0拾物 1寻物, title VARCHAR(100) NOT NULL COMMENT 标题, description TEXT COMMENT 详细描述, location VARCHAR(100) COMMENT 丢失或捡到地点, item_type VARCHAR(50) COMMENT 物品类别如证件、手机、书包, image_url VARCHAR(255) COMMENT 图片路径, status TINYINT DEFAULT 0 COMMENT 0待审核 1发布中 2交接中 3已完成 4已下架, click_count INT DEFAULT 0 COMMENT 浏览量, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_type_status (type, status) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT失物拾物信息表; CREATE TABLE claim_record ( id INT NOT NULL AUTO_INCREMENT, item_id INT NOT NULL COMMENT 物品ID, claim_user_id INT NOT NULL COMMENT 认领人ID, description TEXT COMMENT 认领描述比如物品特征、丢失经过, contact_info VARCHAR(100) COMMENT 认领人联系方式, status TINYINT DEFAULT 0 COMMENT 0待审核 1已通过 2已拒绝 3已完成, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT认领记录表; CREATE TABLE notice ( id INT NOT NULL AUTO_INCREMENT, title VARCHAR(100) NOT NULL, content TEXT, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT公告表;建表时有两个容易被忽略的细节。一是字符集一定要用 utf8mb4否则用户输入个表情符号或者特殊字符保存直接报错。二是联合索引 idx_type_status 很有必要因为首页列表最常见的查询条件就是“按类型 按状态”加了索引后数据量上来页面的响应速度也不会太慢。4. 后端核心实现接口分层的关键代码与细节后端结构我建议这样分controller 接收请求service 写业务逻辑mapper/dao 负责数据库操作entity 对应表结构。再统一封装一个返回对象 Result把 code、message、data 包起来前端处理起来会非常舒服。这套结构没有什么高深的地方但职责清晰答辩也能讲明白。4.1 项目分层与通用返回结构通用返回类建议这么写public class ResultT { private Integer code; private String message; private T data; public static T ResultT success(T data) { ResultT result new Result(); result.code 200; result.message success; result.data data; return result; } public static T ResultT error(String message) { ResultT result new Result(); result.code 500; result.message message; return result; } }这样一个 Result 类能解决大部分接口返回值风格统一的问题。前端 axios 拦截器里统一判断 code 是否为 200不是 200 就弹出 message开发体验会好非常多。控制器层的目录也可以按业务分比如 UserController、ItemController、ClaimController、NoticeController。Controller 本身要尽量薄不要在里面写一堆 SQL 或 if 判断把这些挪到 Service 层去。有个很常见的错误是 Controller 里直接调用 mapper一时图方便爽了后面想加一个逻辑判断就要修改很多地方维护成本直线上升。4.2 用户登录与JWT鉴权怎么落地登录鉴权这块校园项目最常用的方案是JWT 拦截器。用户输入账号密码后端校验通过后生成一个 token 返回给前端前端把 token 存在 localStorage 里每次请求都带上。这种方式不需要在服务端保存会话符合前后端分离的习惯。生成 token 的代码可以直接用 jjwt 库简单封装一个工具类public class JwtUtil { private static final String SECRET your-secret-key; private static final long EXPIRE 7 * 24 * 60 * 60 * 1000L; public static String generateToken(Integer userId, String username, Integer role) { return Jwts.builder() .setSubject(username) .claim(userId, userId) .claim(role, role) .setExpiration(new Date(System.currentTimeMillis() EXPIRE)) .signWith(SignatureAlgorithm.HS256, SECRET) .compact(); } public static Claims parseToken(String token) { return Jwts.parser().setSigningKey(SECRET).parseClaimsJws(token).getBody(); } }然后写一个拦截器在 preHandle 方法里从请求头取出 token校验通过就把用户信息放入 request 的 attribute 中Controller 里用 RequestAttribute 获取当前登录用户。管理员的接口再额外判断 role 是否等于 1这样权限控制就完成了。这里有个小细节前端请求头一般统一写成 Authorization: token值后端拦截器取的时候不要拼上“Bearer”前缀除非你前后端约定使用了它。很多同学以为 JWT 标准就是 Bearer但项目里没商量好前后端对不上结果登录后所有请求都 401排查了半天。4.3 失物、认领、审核三个接口的状态流转状态流转是这个项目后端最值得写的部分。用户发布物品状态为0待审核管理员通过状态变1发布中用户或管理员发起认领状态根据业务可能变2交接中交接完成变3已完成。我推荐把状态变化逻辑集中写在 Service 层不要让每个 Controller 随便改状态否则很容易出现“物品已经被认领了但发布接口还能修改它”这种数据不一致问题。以管理员审核为例大致的 Service 逻辑是Transactional public void auditItem(Integer itemId, Integer pass) { Item item itemMapper.selectById(itemId); if (item null) { throw new RuntimeException(物品信息不存在); } if (item.getStatus() ! 0) { throw new RuntimeException(该物品已处理请勿重复审核); } if (pass 1) { item.setStatus(1); } else { item.setStatus(4); // 不通过就视为下架 } itemMapper.updateById(item); }注意这里加了事务注解和状态校验。事务保证更新过程出异常能回滚状态校验防止重复审核把数据搞乱。这种细节在答辩时非常加分因为老师会问“如果同一个请求被提交两次怎么办”你能答上来自检逻辑和防重判断印象分完全不同。认领流程也要注意防重。一个物品可以被多个人提交认领申请这很合理但同一个用户不能对同一个物品重复提交认领否则列表里全是重复消息。可以在 claim_record 表加一个唯一索引或者提交前先查询一次。public void submitClaim(ClaimRecord claim) { Long count claimMapper.selectCount( new LambdaQueryWrapperClaimRecord() .eq(ClaimRecord::getItemId, claim.getItemId()) .eq(ClaimRecord::getClaimUserId, claim.getClaimUserId()) .in(ClaimRecord::getStatus, 0, 1) ); if (count 0) { throw new RuntimeException(你已经认领过该物品请等待处理); } claimMapper.insert(claim); }这段代码在认领功能里非常关键。不加这个判断用户疯狂点提交按钮数据库就会塞进几十条无效认领记录。4.4 图片上传与静态资源映射图片上传大概是失物招领系统里大家踩坑最多的功能。后端先配置一个上传目录比如 D:/upload 或 /data/upload然后把前端传过来的 MultipartFile 存到这个目录下文件名用 UUID 重命名避免中文名和重复名导致的问题。保存之后把文件的访问路径返回给前端。Spring Boot 里要做静态资源映射否则图片存到了磁盘上请求却访问不到。比较省事的方式是在 application.yml 里配置spring: web: resources: static-locations: file:${upload.path} servlet: multipart: max-file-size: 10MB max-request-size: 20MB upload: path: D:/upload/这样访问 http://localhost:8080/a.jpg 就能直接映射到 D:/upload/a.jpg。上传接口返回给前端的路径就写 /a.jpg 这种相对路径前端拼接成完整地址展示在 img 标签里。一个小提示本地开发用绝对路径没问题但当你要部署到服务器Windows 路径换到 Linux 路径要改配置文件。如果嫌麻烦直接把图片入库前转成 Base64 字符串存到一个字段里但这种方式会因为字符串太长每天产生大量数据库读写只适合图片非常少的场景不推荐。5. 前端页面实操Vue项目从路由到组件的完整串联前端部分可以说是最容易做“表面功夫”的但表面的背后也有不少门道。用 Vue 写这套系统核心是把页面和数据流处理好。很多人一开始就堆组件最后路由乱、状态乱、接口乱改一个需求要牵连七八个文件。5.1 前端工程结构和路由权限怎么安排比较标准的 Vue 前端工程目录可以这样组织src/api 目录按业务模块封装 API 请求比如 user.js、item.js、claim.js。src/router 目录配置路由表用 meta 字段标记是否需要登录、是否需要管理员权限。src/store 目录Vuex 或 Pinia 保存用户信息、token 等全局状态。src/views 目录页面级组件比如 Home.vue、PublishItem.vue、AdminItemAudit.vue。src/components 目录通用组件比如 ItemCard.vue、UploadImages.vue、Pagination.vue。路由守卫是必须写的。没有登录的用户点进“发布信息”页面应该被弹回登录页普通用户访问后台管理页面也应该被拦截。这段逻辑统一写在 router.beforeEach 里面router.beforeEach((to, from, next) { const token localStorage.getItem(token); if (to.meta.requiresAuth !token) { next(/login); } else if (to.meta.requiresAdmin) { const role store.state.user.role; if (role ! 1) { next(/); } else { next(); } } else { next(); } });路由守卫做得好用户根本不会遇到“页面白屏”或“操作才提示没权限”这种糟糕体验老师演示的时候也会觉得系统很规范。5.2 首页信息流卡片、分页、搜索筛选首页一般展示所有通过审核的失物和拾物信息。实现方式就是调用后端列表接口拿到 item 列表后用 v-for 渲染成一张张卡片。卡片上展示图片缩略图、标题、地点、时间、状态点击可以进入详情页。列表接口一定不能只返回所有数据。正经系统要做分页前端传 pageNum 和 pageSize后端返回总条数和当前页数据。Element UI 的 el-pagination 组件直接对上去就行。搜索和筛选同样通过请求参数传给后端比如按关键词搜标题、按物品类别过滤、按发布时间排序。前端调用接口的代码封装成 api 后页面里调用起来非常清爽// src/api/item.js import request from /utils/request; export function getItemList(params) { return request({ url: /item/list, method: get, params }); }这里的 request 是经过 axios 二次封装的实例统一注入了 baseURL、token 和响应拦截器。响应拦截器里如果检测到 code 为 401一般表示 token 过期就清理本地登录信息并跳转到登录页。5.3 发布表单与图片上传的细节发布表单是用户输入信息最多的页面字段比较多Element UI 的 el-form 配 rules 做校验非常方便。比如“标题”必填且长度不超过 100“地点”必填“详细描述”选填但至少 10 个字符。校验规则还可以在失物和拾物两种类型之间做一点差异比如拾物要求填写联系电话寻物要求填写物品特征。图片上传用 el-upload 组件是常规操作。要注意三点一是 action 属性要指到后端上传接口二是 headers 里带上 token三是 on-success 回调里把后端返回的图片路径存到表单字段中。很多同学忽略第二点导致上传图片一直 401因为后端拦截器会把上传接口也拦下来。图片上传成功后的预览直接使用后端返回的完整 URL 或相对路径拼接即可。注意图片回显时不要把 filenames 存在一个字符串里用逗号分隔那是一种很痛苦的体验。建议后端返回一个字符串数组前端通过 v-for 渲染多张图片。5.4 后台审核界面的快速实现后台管理页面不需要花里胡哨清爽的表格最好用。el-table 展示最新发布的待审核物品列可以设置成标题、类型、发布人、发布时间、操作。操作列放两个按钮“通过”和“拒绝”。点击通过就调用审核接口传 status1点击拒绝传 status4然后刷新当前列表。为了区分不同类型el-tag 标签可以根据 type 和 status 动态变色。比如拾物用蓝色标签、寻物用橙色标签待审核用灰色、发布中用绿色、已完成用深绿色。这种视觉细节不需要代码复杂度但能让评审眼前一亮觉得你考虑了用户体验。后台页面最好也加一个简单的统计区块展示“待审核数量”“已发布数量”“今日新发布数量”。这些数据可以直接调一个统计接口后端用 group by 查询返回 map前端展示到卡片上。功能不大但能让管理端看起来像一个真正的管理系统。6. 环境搭建、启动运行与部署避坑一套代码能不能在别人电脑上跑起来是检验项目完整性的核心标准。很多同学在自己电脑上开发得好好的交付给同学或老师后却各种报错问题基本都出在环境差异。下面我把从零启动这套系统的主要步骤捋一遍。6.1 前置环境和使用版本建议我实际调试过多种组合最稳的一套是这样的JDK 1.8 或 11Spring Boot 2.x 用 JDK8 很稳3.x 需要 JDK17建议直接用 JDK8 避开兼容问题。Maven 3.6 以上。Node.js 14 以上、npm 6 以上Vue CLI 或 Vite 都可以。MySQL 5.7 或 8.0。需要特别提醒的是 Spring Boot 和 JDK 的版本对应关系。如果你用 Spring Boot 3.xJDK 必须是 17 及以上对应的 javax.servlet 也变成了 jakarta很多网上教程的代码会不兼容。对毕设而言Spring Boot 2.5~2.7 加 JDK8 是最省心的组合不要为了“新版本”为难自己。6.2 后端启动步骤与配置修改拿到项目源码后第一步是用 IDE推荐 IntelliJ IDEA打开后端工程等待 Maven 下载依赖。要注意 Maven 仓库镜像是否配置好国内网络建议在 settings.xml 里加阿里云镜像不然下载依赖能等到怀疑人生。第二步是修改 application.yml 里的数据库连接信息。把 url、username、password 改成你自己本机的值。我第一次帮别人弄时发现他数据库账号是 root密码却填了个从来没设置过的字符串连接时报 Access denied不是项目的锅是配置没改。第三步是确保 MySQL 里已经导入了建表 SQL。这里注意 SQL 脚本路径如果项目自带 sql 目录就直接导入没有的话就执行你自己新建的建表语句。启动顺序是Navicat 导入 SQL → IDEA 启动 Application → 控制台不报错、Tomcat started on port 8080 才算成功。6.3 前端启动与代理配置前端部分用命令行工具进入项目目录依次执行 npm install 和 npm run serve。npm install 时间较长如果卡在某个包上可以切换一下 npm 镜像源为淘宝镜像。前后端分离开发时一定会遇跨域问题。解决方式有两种一种是在后端写全局 CORS 配置另一种是前端开发环境配代理。我建议直接用 Vue 的代理方式在 vue.config.js 里配module.exports { devServer: { port: 3000, proxy: { /api: { target: http://localhost:8080, changeOrigin: true, pathRewrite: { ^/api: } } } } };注意这里有个容易出错的地方如果前端请求的路径是 /api/xxx而后端接口路径是 /xxx你要么在后端统一设置 RequestMapping(/api)要么在前端代理里配 pathRewrite。如果两边没对齐要么代理匹配不上要么路径拼接多出一层请求结果全变成 404。建议前端 API 模块里所有请求路径统一以 /api 开头后端处理器也统一加 /api 前缀这样代理只用把 /api 按原样转发不用 pathRewrite更省事。6.4 打包上线的两种常用方式毕设通常只要求在本地演示但如果你想让项目打包上线也有两种最常用的方式。第一种是把前端打包后的 dist 目录放到后端 Spring Boot 的 static 目录下然后启动后端 jar 包访问 http://localhost:8080 就能同时看到页面和接口。这种方式对小型项目非常方便一个进程搞定所有。需要注意前端接口地址要写相对路径比如 /api/user/login不要写死成 http://localhost:3000。第二种是前后端分别部署前端用 Nginx 托管静态文件后端 jar 包用 java -jar 启动Nginx 再配置反向代理把 /api 转发给后端服务。这种方式更接近企业真实部署结构课设和毕设写进 “系统部署” 章节会显得更专业。nginx 核心配置大致是server { listen 80; server_name localhost; root /usr/share/nginx/html; index index.html; location /api/ { proxy_pass http://localhost:8080/api/; } }如果对 Nginx 不熟悉用第一种方式最容易成功面试或答辩时如果你还能说清楚两种部署方式的区别就已经比大部分同学强了。7. 实战中的问题排查与解决记录做这类项目遇到问题是常态关键是思路要清晰。下面这些坑基本是所有人都会遇见的我按症状、原因、解决办法整理成一个速查表方便你直接对照。7.1 启动报错速查表症状常见原因解决办法启动报错数据库连接失败数据库没启动或账号密码错误检查 MySQL 服务核对 yml 中的账号密码启动报错端口被占用8080 已被其他程序占用更换 server.port或杀掉占用进程启动报错Failed to configure a DataSource没配数据源或 pom 里没加 MySQL 驱动检查依赖确认 application.yml 有 datasource 配置前端请求全部404路由配错或代理没生效查看浏览器 Network确认请求路径对应后端接口中文乱码数据库字符集不是 utf8mb4建库时指定 CHARACTER SET utf8mb4上传图片后访问403/404静态资源映射没配好检查 yml 文件 static-locations 路径是否正确7.2 图片上传失败或访问不到图图片相关的问题非常集中。如果你上传时一直报错 “Required request part ‘file’ is not present”大概率是前端 el-upload 的 name 属性和后端接口的参数名没对上。后端如果是 RequestParam(file)el-upload 里必须写 namefile。如果上传成功但是访问图片 404排查顺序是先看本地目录里有没有这个文件再看 yml 里的映射路径是否正确最后看数据库里存的路径和前端的访问路径能否拼成完整 URL。我见过一个同学把图片路径存成了 D:/upload/a.jpg前端直接拿这个当 img src浏览器当然无法访问本地磁盘路径。正确做法是存相对路径 /a.jpg前端拼上后端域名。7.3 Token失效和跨域问题为什么登录后过一段时间又要重新登录一般是因为 JWT 的过期时间设得太短。建议设置为 7 天或按需设置对校园项目来说完全够用。当然如果你想做一个严格一点的系统可以考虑 Redis 管理会话但那会额外引入中间件课设阶段可以不用。跨域问题如果用了代理还是报 CORS可以检查一下后端是否也配置了 CorsFilter。如果后端配了允许所有来源代理请求也可能被浏览器拦截。实际开发中开发环境建议用代理上线时配置 Nginx 代理后端不要轻易放开全局跨域这样更安全。7.4 后端小细节数据库字段和实体类命名不一致如果表的字段是下划线风格比如 create_time、item_type而实体类是驼峰写法 createTime、itemType那么 MyBatis-Plus 默认的映射可以处理但原生 MyBatis 需要开启下划线转驼峰配置。很多新手遇到的 “Unknown column” 错误就是实体字段和表字段对不上导致的。解决方式是在 application.yml 里配置 mybatis-plus 的 map-underscore-to-camel-case: true或者直接让数据库字段和实体字段保持一致。这个问题看着小但一旦遇到报错信息往往不是很直观容易让人卡很久。建议建表时就和实体类统一命名要么都用下划线要么都用驼峰表字段用下划线、实体字段用驼峰是最常见且规范的做法。最后再说一点个人体会这套校园失物招领系统我前前后后帮人调试过不少次也见过很多种实现方式。一个很深的感受是这类项目的技术难度并不算大真正拉开差距的地方在于流程梳理和数据建模。你如果能清楚回答“物品从发布到完成都经历了哪些状态”“同一个用户重复认领怎么拦截”“前后端接口如何约定返回格式”基本上就掌握了整个项目的主心骨。如果你拿它做毕业设计建议在说明书里把需求分析、用例图、E-R图、状态图和核心接口设计都写清楚这些内容是答辩时老师最关心的。如果只是做课程设计可以适当砍掉统计报表、公告管理等外围功能留核心流程也能完整交付。希望这篇文章能帮你把这个系统跑通、搞懂再稍微做得比别人好一点。