如果有人问我,毕设或课设做什么项目性价比最高,我的第一反应多半是校园失物招领系统。技术栈非常标准,SpringBoot+Vue,Java+MySQL,业务上却又绕不开信息发布、认领审核、通知公告这些经典场景,正好把一个全栈项目该有的东西都串起来了。最近我把这套系统重新梳理了一遍,发现它不只适合毕业设计和课程设计,对刚接触前后端分离开发的同学来说,也是一条非常完整的练手路径。
这篇就顺着我知道的常见实现思路,把需求拆解、数据库设计、后端接口、前端页面、环境启动和排坑经验都过一遍。文章里提到的做法不一定是最花哨的,但一定是能让项目跑起来、能答辩、能真正学到东西的方案。
1. 校园失物招领系统的需求是怎么拆出来的
很多同学拿到这类题目,第一反应是赶紧写代码,结果做着做着发现要么页面堆砌、要么逻辑混乱。其实这种业务型项目,最该先做的是需求拆解。你连谁在用、要用到什么程度都没搞清楚,后面写出来的功能大概率是废的。
1.1 丢东西的人、捡东西的人、管东西的人,三种视角看问题
失物招领表面上就是“有人丢东西、有人捡东西”,但一旦落地成系统,至少要回答三组问题。
丢东西的人想知道:发布后有没有人看到,有没有人捡到类似物品,认领的进度到了哪一步。所以系统必须提供“寻物启示发布”“浏览捡到物品列表”“提交认领申请”“查看我的发布和状态”这些功能。
捡东西的人想知道:捡到的物品该交到哪里,怎么证明我捡到的就是对方丢的那个,会不会白跑一趟。所以系统需要“拾取物品登记”“物品描述与会合地点”“认领凭证核验”这类支撑。
管理人员更关心:信息是不是真实有效,同学有没有拿错东西,要不要处理一些纠纷。所以后台需要“审核发布”“核实认领”“下架过期信息”“简单的统计”功能。
这三组问题一列,你就能很自然地分出普通用户端和管理员端两套界面,也能确定核心的业务对象就是“失物信息”“拾物信息”“认领记录”“用户信息”这四类。
1.2 核心业务流程:从发布到认领再到归档
我习惯把失物招领的流程画成两条主线,一条围绕“寻物”,一条围绕“拾取”。
寻物线是这样的:用户登录后发布寻物启示,填写物品名称、特征、丢失地点、图片,提交后进入待审核状态;管理员审核通过,信息公开展示;如果有人捡到了或看到了,可以在线上提交线索或认领线索;用户看到线索后自行确认,或者由管理员协助联系,最后关闭该启示。
拾取线的逻辑略有不同:用户捡到物品后先登记成“拾物信息”,管理员同样要审核;系统公开后,失主看到信息,提交认领申请并描述物品细节;管理员把失主描述与捡拾人登记的细节进行核验,核验一致则安排线下交接,交接完成后状态变成“已完成”。
这两条线看起来都是“发布-审核-匹配-闭环”,但匹配逻辑不太一样:寻物启示是主动找别人,拾物信息是等别人来认领。数据库设计阶段如果把这两类信息放到同一张表,业务上会更灵活,后面第3节我会详细说表结构怎么处理。
1.3 功能清单要控制住,防止做飘了
毕设和课设最大的坑是需求膨胀。我见过一个同学把在线客服、积分商城都塞进失物招领系统,结果答辩时老师一问,他连核心流程都说不清。功能不是越多越好,能自圆其说、每个功能都有存在价值,这才是评分标准。
建议按优先级分三档。第一档必须要有:登录注册、失物发布、拾物发布、信息列表、认领申请、管理员审核。第二档建议要有:图片上传、状态查询、模糊搜索、分页、公告栏、用户信息修改。第三档可选:数据统计、导出报表、消息提醒、路由懒加载、按钮权限。先把一二档做完,时间富余再做第三档,这样项目体量也刚好匹配毕设的篇幅要求。
2. 技术选型:SpringBoot+Vue+MySQL这一套为什么刚好合适
技术选型不是越新越好、越炫越好,而是要符合项目阶段和环境。失物招领系统用 SpringBoot+Vue+MySQL 的组合,可以说是目前校园项目里最成熟的搭配之一,原因其实很实在。
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或者Redis,MySQL对这个项目来说已经绰绰有余,还能让老师一眼看懂你的数据设计水平。
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`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 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`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 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`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 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`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='公告表';建表时有两个容易被忽略的细节。一是字符集一定要用 utf8mb4,否则用户输入个表情符号或者特殊字符,保存直接报错。二是联合索引 idx_type_status 很有必要,因为首页列表最常见的查询条件就是“按类型 + 按状态”,加了索引后数据量上来页面的响应速度也不会太慢。
4. 后端核心实现:接口分层的关键代码与细节
后端结构我建议这样分:controller 接收请求,service 写业务逻辑,mapper/dao 负责数据库操作,entity 对应表结构。再统一封装一个返回对象 Result,把 code、message、data 包起来,前端处理起来会非常舒服。这套结构没有什么高深的地方,但职责清晰,答辩也能讲明白。
4.1 项目分层与通用返回结构
通用返回类建议这么写:
public class Result<T> { private Integer code; private String message; private T data; public static <T> Result<T> success(T data) { Result<T> result = new Result<>(); result.code = 200; result.message = "success"; result.data = data; return result; } public static <T> Result<T> error(String message) { Result<T> 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 LambdaQueryWrapper<ClaimRecord>() .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 展示最新发布的待审核物品,列可以设置成:标题、类型、发布人、发布时间、操作。操作列放两个按钮:“通过”和“拒绝”。点击通过就调用审核接口传 status=1,点击拒绝传 status=4,然后刷新当前列表。
为了区分不同类型,el-tag 标签可以根据 type 和 status 动态变色。比如拾物用蓝色标签、寻物用橙色标签,待审核用灰色、发布中用绿色、已完成用深绿色。这种视觉细节不需要代码复杂度,但能让评审眼前一亮,觉得你考虑了用户体验。
后台页面最好也加一个简单的统计区块,展示“待审核数量”“已发布数量”“今日新发布数量”。这些数据可以直接调一个统计接口,后端用 group by 查询返回 map,前端展示到卡片上。功能不大,但能让管理端看起来像一个真正的管理系统。
6. 环境搭建、启动运行与部署避坑
一套代码能不能在别人电脑上跑起来,是检验项目完整性的核心标准。很多同学在自己电脑上开发得好好的,交付给同学或老师后却各种报错,问题基本都出在环境差异。下面我把从零启动这套系统的主要步骤捋一遍。
6.1 前置环境和使用版本建议
我实际调试过多种组合,最稳的一套是这样的:
- JDK 1.8 或 11(Spring 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.x,JDK 必须是 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 里必须写 name="file"。
如果上传成功但是访问图片 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图、状态图和核心接口设计都写清楚,这些内容是答辩时老师最关心的。如果只是做课程设计,可以适当砍掉统计报表、公告管理等外围功能,留核心流程也能完整交付。希望这篇文章能帮你把这个系统跑通、搞懂,再稍微做得比别人好一点。