尧图精选

基于SpringBoot3+Vue3的失物招领系统设计与实现

🕒 发布时间:2026/9/6 11:32:13 📁 来源:尧图网络
在学校、办公楼、园区这类人流量密集的场所“物品丢失”几乎是每天都在发生的小麻烦。捡到东西的人不知道交给谁丢东西的人不知道去哪里找靠贴告示和群聊转发效率又太低。实际上这类场景非常适合用一套前后端分离的管理系统来解决问题失主发布寻物启事拾到者发布招领信息双方在线匹配、留言联系全程留痕。本文就基于JAVA SpringBoot3 Vue.js3 MySQL完整实现一套失物招领系统。先从功能设计讲起再逐步搭建数据库、后端接口、前端页面最后给出运行验证、常见报错排查和生产部署建议。无论你是正在做课程设计、毕业设计还是想练手前后端分离项目这套代码和思路都可以直接参考。适合读者有 Java 基础、想学习 SpringBoot3 新特性。正在搭建 Vue3 Element Plus 前端项目。需要一套完整可跑的 MySQL 数据库设计案例。准备做失物招领、寻物启事、校园互助类课题。1. 需求分析与功能梳理1.1 系统到底解决什么问题失物招领的业务闭环其实很简单但线下执行时信息不透明拾到物品的人想还但找不到失主。丢失物品的人想找但不知道去哪里问。双方缺少一个“信息汇聚 快速匹配”的渠道。所以在线系统需要覆盖三个核心流程失主发布登记丢失物品的名称、时间、地点、特征、联系方式。拾到者发布登记拾获物品的信息等待失主认领。认领交接失主发起认领申请拾到者确认信息双方完成线下交接标记状态。1.2 角色划分系统不搞复杂权限模型只分两种角色角色核心权限普通用户注册、登录、发布失物、发布招领、申请认领、修改自己发布的状态管理员用户管理、物品信息审核、删除违规信息、查看全站认领记录1.3 功能模块拆分按业务拆分如下用户模块注册、登录、个人信息维护。失物模块发布“我丢了东西”、失物列表、失物详情。招领模块发布“我捡到了东西”、招领列表、招领详情。认领模块失主发起认领拾到者确认认领记录交接时间。留言/联系模块物品详情页可留言或查看联系方式。后台管理管理用户、管理物品、数据概览。这样一套功能前端 6 个左右页面后端 6 张左右表非常适合作为 SpringBoot3 Vue3 的综合实战项目。2. 技术选型与环境准备2.1 技术栈总览后端技术版本建议说明JDK17SpringBoot3 强制要求 JDK17SpringBoot3.x核心框架本文以 3.x 常见版本为例MyBatis-Plus3.5.5使用 spring-boot3 starterMySQL8.0数据库Maven3.6构建工具Hutool5.x工具库非必须前端技术版本建议说明Node.js18Vite6/Vite5 需要新版 NodeVue3.4组合式 APIVite5.x构建工具Element Plus2.xUI 组件库Axios1.xHTTP 请求库Pinia2.x状态管理可选版本需要根据你的项目实际情况调整本文示例以常见环境为例重点演示配置思路。JDK 版本不要低于 17否则 SpringBoot3 启动会直接报错。2.2 开发工具准备后端 IDEIntelliJ IDEA社区版即可。前端 IDEVSCode 或 IDEA。数据库工具Navicat、DataGrip或者直接用命令行。安装完 JDK、Maven、Node.js 后可以先验证环境java -version mvn -v node -v npm -v mysql --version2.3 项目结构规划整个项目采用前后端分离目录如下lost-found-system/ ├── backend/ # SpringBoot3 后端 │ ├── src/main/java │ ├── src/main/resources │ └── pom.xml └── frontend/ # Vue3 前端 ├── src/ ├── package.json ├── vite.config.js └── index.html3. 数据库设计6 张表跑通核心业务数据库表设计决定了业务能做多深。失物招领系统不必过度设计但认领流程必须单独建表因为一个物品可能存在多次认领申请。3.1 用户表t_userCREATE TABLE t_user ( id BIGINT NOT NULL AUTO_INCREMENT COMMENT 主键, username VARCHAR(50) NOT NULL COMMENT 用户名, password VARCHAR(100) NOT NULL COMMENT 密码(建议BCrypt加密), nickname VARCHAR(50) DEFAULT NULL COMMENT 昵称, phone VARCHAR(20) DEFAULT NULL COMMENT 手机号, role TINYINT NOT NULL DEFAULT 0 COMMENT 角色 0-用户 1-管理员, create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT 创建时间, PRIMARY KEY (id), UNIQUE KEY uk_username (username) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT用户表;3.2 物品表t_goods这张表同时存“失物”和“招领”用type字段区分CREATE TABLE t_goods ( id BIGINT NOT NULL AUTO_INCREMENT COMMENT 主键, type TINYINT NOT NULL COMMENT 类型 0-寻物 1-招领, title VARCHAR(100) NOT NULL COMMENT 标题, description TEXT COMMENT 详细描述, place VARCHAR(100) DEFAULT NULL COMMENT 丢失/拾获地点, contact VARCHAR(50) DEFAULT NULL COMMENT 联系方式, image VARCHAR(255) DEFAULT NULL COMMENT 图片URL, status TINYINT NOT NULL DEFAULT 0 COMMENT 状态 0-待认领 1-已认领 2-已关闭, user_id BIGINT NOT NULL COMMENT 发布人ID, create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT 发布时间, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT物品表;3.3 认领表t_claim认领申请是业务核心状态机要清晰CREATE TABLE t_claim ( id BIGINT NOT NULL AUTO_INCREMENT COMMENT 主键, goods_id BIGINT NOT NULL COMMENT 物品ID, user_id BIGINT NOT NULL COMMENT 认领人ID, reason VARCHAR(500) DEFAULT NULL COMMENT 认领描述/凭证, status TINYINT NOT NULL DEFAULT 0 COMMENT 状态 0-待确认 1-已通过 2-已拒绝, reply VARCHAR(500) DEFAULT NULL COMMENT 处理回复, create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT 申请时间, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT认领表;3.4 留言表t_messageCREATE TABLE t_message ( id BIGINT NOT NULL AUTO_INCREMENT, goods_id BIGINT NOT NULL, user_id BIGINT NOT NULL, content VARCHAR(500) NOT NULL, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT留言表;3.5 轮播图表、系统配置表这两张可选用来做首页轮播位和站点公告CREATE TABLE t_banner ( id BIGINT NOT NULL AUTO_INCREMENT, title VARCHAR(100) DEFAULT NULL, image VARCHAR(255) DEFAULT NULL, url VARCHAR(255) DEFAULT NULL, status TINYINT DEFAULT 1, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT轮播图表; CREATE TABLE t_config ( id BIGINT NOT NULL AUTO_INCREMENT, config_key VARCHAR(50) NOT NULL, config_value VARCHAR(500) DEFAULT NULL, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT配置表;设计说明为什么物品不拆成“失物表”和“招领表”两张因为字段几乎一样拆表反而增加查询复杂度。用枚举字段type区分业务清晰且减少冗余。为什么认领要单独建表因为一次被认领操作可能多次被申请如果直接在t_goods上改状态就丢失了申请记录无法追溯、无法审核。4. SpringBoot3 后端开发实战4.1 创建 SpringBoot3 项目推荐直接用 IDEA 创建选择Spring InitializrProjectMavenLanguageJavaSpring Boot3.x依赖Spring Web、MySQL Driver也可以直接编写pom.xml核心依赖如下parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version3.2.5/version relativePath/ /parent dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-spring-boot3-starter/artifactId version3.5.5/version /dependency dependency groupIdcom.mysql/groupId artifactIdmysql-connector-j/artifactId scoperuntime/scope /dependency dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId optionaltrue/optional /dependency dependency groupIdcn.hutool/groupId artifactIdhutool-all/artifactId version5.8.27/version /dependency /dependencies注意SpringBoot3 请务必使用mybatis-plus-spring-boot3-starter不能用旧的mybatis-plus-boot-starter否则启动会找不到自动配置类。4.2 配置文件application.yml完整的配置如下server: port: 8080 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/lost_found?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/ShanghaiallowPublicKeyRetrievaltrue username: root password: 123456 servlet: multipart: max-file-size: 10MB max-request-size: 20MB mybatis-plus: configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl global-config: db-config: id-type: auto配置说明allowPublicKeyRetrievaltrue是为了处理 MySQL8 认证插件导致的连接失败。map-underscore-to-camel-case自动把create_time映射为createTime配合 MP 使用非常丝滑。log-impl开启 SQL 日志开发阶段建议打开。4.3 实体类Goods 物品类package com.example.lostfound.entity; import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; import lombok.Data; import java.time.LocalDateTime; Data TableName(t_goods) public class Goods { TableId(type IdType.AUTO) private Long id; private Integer type; private String title; private String description; private String place; private String contact; private String image; private Integer status; private Long userId; private LocalDateTime createTime; }这里用LocalDateTime而不是Date更符合 JDK8 的时代习惯MyBatis-Plus 对 LocalDateTime 支持也很好。4.4 数据访问层 Mapperpackage com.example.lostfound.mapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.example.lostfound.entity.Goods; import org.apache.ibatis.annotations.Mapper; Mapper public interface GoodsMapper extends BaseMapperGoods { }BaseMapper已经内置了insert、deleteById、selectById、selectList、selectPage等常用方法普通 CRUD 不需要写 XML。4.5 服务层 GoodsServicepackage com.example.lostfound.service.impl; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.example.lostfound.entity.Goods; import com.example.lostfound.mapper.GoodsMapper; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; Service public class GoodsService { Autowired private GoodsMapper goodsMapper; public PageGoods pageList(int pageNum, int pageSize, Integer type, Integer status, String keyword) { PageGoods page new Page(pageNum, pageSize); LambdaQueryWrapperGoods wrapper new LambdaQueryWrapper(); if (type ! null) { wrapper.eq(Goods::getType, type); } if (status ! null) { wrapper.eq(Goods::getStatus, status); } if (keyword ! null !keyword.trim().isEmpty()) { wrapper.and(w - w.like(Goods::getTitle, keyword) .or().like(Goods::getDescription, keyword)); } wrapper.orderByDesc(Goods::getCreateTime); return goodsMapper.selectPage(page, wrapper); } public Goods getById(Long id) { return goodsMapper.selectById(id); } }LambdaQueryWrapper 是 MyBatis-Plus 中最常用的条件构造器类型安全、不会因为字段改名导致 SQL 写错。4.6 控制器 GoodsControllerpackage com.example.lostfound.controller; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.example.lostfound.entity.Goods; import com.example.lostfound.service.impl.GoodsService; import com.example.lostfound.common.Result; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.*; import java.time.LocalDateTime; RestController RequestMapping(/api/goods) public class GoodsController { Autowired private GoodsService goodsService; GetMapping(/list) public ResultPageGoods list( RequestParam(defaultValue 1) int pageNum, RequestParam(defaultValue 10) int pageSize, RequestParam(required false) Integer type, RequestParam(required false) Integer status, RequestParam(required false) String keyword) { return Result.success(goodsService.pageList(pageNum, pageSize, type, status, keyword)); } GetMapping(/{id}) public ResultGoods detail(PathVariable Long id) { return Result.success(goodsService.getById(id)); } PostMapping public ResultString add(RequestBody Goods goods) { goods.setCreateTime(LocalDateTime.now()); goodsService.add(goods); return Result.success(发布成功); } }前端跨域问题在联调阶段很常见可以在后端写一个配置类统一放行package com.example.lostfound.config; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.CorsRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; Configuration public class CorsConfig implements WebMvcConfigurer { Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping(/api/**) .allowedOriginPatterns(*) .allowedMethods(GET, POST, PUT, DELETE, OPTIONS) .allowedHeaders(*) .allowCredentials(true) .maxAge(3600); } }注意生产环境不要使用allowedOriginPatterns(*)应改为前端实际域名避免接口被任意站点跨域调用。4.7 登录认证的简化处理完整项目建议使用Sa-Token或者Spring Security JWT。为了课程设计演示方便这里提供一种“不引入重量级框架、但功能完整”的简单方案登录成功后生成一个 UUID Token存到 Redis 或内存 Map。前端请求头携带token后端用拦截器校验。package com.example.lostfound.interceptor; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import org.springframework.stereotype.Component; import org.springframework.web.servlet.HandlerInterceptor; Component public class AuthInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 放行登录、注册接口 String uri request.getRequestURI(); if (uri.contains(/login) || uri.contains(/register)) { return true; } String token request.getHeader(token); if (token null || token.isEmpty()) { response.setStatus(401); return false; } // 校验 token 逻辑可以从 Redis 中获取 userId return true; } }一个注意点拦截器里校验 token 时如果用户未登录返回 JSON 而不是 401 空响应前端处理起来会更容易。可以配合全局异常处理器输出统一结构。5. Vue3 前端开发实战5.1 创建 Vite Vue3 项目npm create vitelatest frontend -- --template vue cd frontend npm install npm install axios element-plus vue-router45.2vite.config.js配置代理import { defineConfig } from vite import vue from vitejs/plugin-vue export default defineConfig({ plugins: [vue()], server: { port: 5173, proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } } })使用代理后前端请求/api/xxx会被转发到后端8080避免开发阶段跨域问题。5.3 封装 Axios 请求工具// src/utils/request.js import axios from axios import { ElMessage } from element-plus const request axios.create({ baseURL: /api, timeout: 10000 }) // 请求拦截器自动带 token request.interceptors.request.use(config { const token localStorage.getItem(token) if (token) { config.headers[token] token } return config }) // 响应拦截器统一处理错误 request.interceptors.response.use( response { const res response.data if (res.code ! 200) { ElMessage.error(res.msg || 请求失败) return Promise.reject(new Error(res.msg)) } return res }, error { ElMessage.error(error.message || 网络异常) return Promise.reject(error) } ) export default request封装的好处是每个页面不用重复写错误处理token 统一注入接口地址只维护一份。5.4 失物列表页面核心代码template div el-card v-foritem in goodsList :keyitem.id classgoods-card div classgoods-info h3{{ item.title }}/h3 p{{ item.description }}/p el-tag :typeitem.type 0 ? danger : success {{ item.type 0 ? 寻物 : 招领 }} /el-tag el-tag v-ifitem.status 1 typeinfo已认领/el-tag el-button typeprimary clickgoDetail(item.id)查看详情/el-button /div /el-card /div /template script setup import { ref, onMounted } from vue import { useRouter } from vue-router import request from ../utils/request const router useRouter() const goodsList ref([]) const loadGoods async () { const res await request.get(/goods/list, { params: { pageNum: 1, pageSize: 10, type: 0 } }) goodsList.value res.data.records } const goDetail (id) { router.push(/goods/${id}) } onMounted(loadGoods) /script5.5 发布失物表单发布页面要注意几个点图片上传用 Element Plus 的el-uploadaction 指向后端/api/upload。发布后跳转到列表页并刷新。必须校验标题和描述不能为空。template el-form :modelform label-width80px el-form-item label类型 el-radio-group v-modelform.type el-radio :value0寻物/el-radio el-radio :value1招领/el-radio /el-radio-group /el-form-item el-form-item label标题 el-input v-modelform.title placeholder请输入标题 / /el-form-item el-form-item label详细描述 el-input typetextarea v-modelform.description / /el-form-item el-form-item label地点 el-input v-modelform.place / /el-form-item el-form-item label联系方式 el-input v-modelform.contact / /el-form-item el-form-item el-button typeprimary clicksubmit发布/el-button /el-form-item /el-form /template script setup import { reactive } from vue import { useRouter } from vue-router import request from ../utils/request const router useRouter() const form reactive({ type: 0, title: , description: , place: , contact: }) const submit async () { const res await request.post(/goods, form) ElMessage.success(res.data) router.push(/goods/list) } /script5.6 认领申请与接口对接认领流程用户查看招领详情。点击“我要认领”填写认领描述。后端生成一条t_claim记录状态为待确认。拾到者在“我发布的招领”里看到申请点通过/拒绝。通过后t_goods.status变为已认领。const applyClaim async (goodsId) { await request.post(/claim/add, { goodsId, reason: 这是我在图书馆丢失的课本封面有我的姓名贴 }) ElMessage.success(认领申请已提交) }后端对应接口PostMapping(/claim/add) public ResultString addClaim(RequestBody Claim claim) { claim.setStatus(0); claim.setCreateTime(LocalDateTime.now()); claimService.save(claim); return Result.success(申请成功); }6. 图片上传与静态资源映射失物招领场景中图片是刚需。没有图片靠文字描述很难确认物品。6.1 后端上传接口PostMapping(/upload) public ResultString upload(RequestParam(file) MultipartFile file) throws IOException { String originalFilename file.getOriginalFilename(); String suffix originalFilename.substring(originalFilename.lastIndexOf(.)); String fileName System.currentTimeMillis() suffix; String dir System.getProperty(user.dir) /upload/; File f new File(dir); if (!f.exists()) { f.mkdirs(); } file.transferTo(new File(dir fileName)); return Result.success(/upload/ fileName); }这个实现虽然简单但已经满足课程设计和小型项目需求。需要注意文件名用时间戳避免中文名乱码和重名。开发环境路径是项目根目录生产环境要改为绝对路径或对象存储。必须限制文件类型不要只按后缀判断。6.2 静态资源映射Configuration public class WebConfig implements WebMvcConfigurer { Override public void addResourceHandlers(ResourceHandlerRegistry registry) { String uploadPath System.getProperty(user.dir) /upload/; registry.addResourceHandler(/upload/**) .addResourceLocations(file: uploadPath); } }否则上传成功但前端访问不到图片会报 404。7. 运行与联调验证7.1 启动后端在 IDEA 中运行启动类看到如下日志说明启动成功Tomcat started on port 8080 (http) with context path Started LostFoundApplication in X.XXX seconds7.2 启动前端cd frontend npm run dev浏览器访问http://localhost:5173。7.3 联调自测清单建议按照以下顺序测试步骤操作预期结果1注册新用户数据库 t_user 增加记录2登录返回 token3发布一条“寻物启事”列表页出现该数据4发布一条“招领启事”首页招领板块出现5另一用户发起认领原发布人看到申请6通过认领物品状态变为已认领7上传图片详情页能显示图片8搜索关键词结果与描述匹配8. 常见问题与排查思路8.1 后端启动报错No active profileDescription: Failed to configure a DataSource: url attribute is not specified.原因application.yml没被加载或者文件放在src/main/resources之外。解决方案确认文件位置和数据库配置。8.2 数据库连接失败Public Key Retrieval is not allowed原因MySQL8 默认 caching_sha2_password 认证插件非 SSL 连接需要获取公钥。解决连接串加allowPublicKeyRetrievaltrueuseSSLfalse。8.3 前端请求后端 404 或跨域原因代理配置没生效或者后端 CORS 没配置。排查看浏览器 Network请求路径是否是/api/...。看后端日志是否接收到请求。确认vite.config.js修改后已重启 Vite改配置必须重启。8.4 SpringBoot3 与 MyBatis-Plus 版本不兼容错误表现启动后提示找不到SqlSessionFactory或MybatisPlusAutoConfiguration。解决确认依赖是mybatis-plus-spring-boot3-starter不要用mybatis-plus-boot-starter。8.5 数据库字段自动填充失败使用 MyBatis-Plus 时如果LocalDateTime属性没加TableField(fill FieldFill.INSERT)且没有配置MetaObjectHandler数据库默认值可能不生效。解决在实体中手动 set或者在配置类中实现元对象填充。8.6 上传中文文件名乱码Spring Boot 2.6 对multipart文件名中文处理有问题建议文件名统一生成不要保留用户原始文件名。问题现象常见原因解决思路后端 8080 端口占用多个项目占用更换端口或关闭旧进程前端 5173 端口占用Vite 默认端口被占用修改 server.port 或加 strictPort: falseToken 验证失败请求头名称不一致前后端统一token字段图片不显示静态资源映射缺失配置 addResourceHandlers认领按钮不显示发布时间格式错误后端序列化 LocalDateTime 加 JsonFormat9. 最佳实践与工程建议9.1 项目层面的建议密码不要明文存储。使用 BCrypt 加密Spring Security 自带 BCryptPasswordEncoder项目里也可以直接用cn.hutool.crypto做加盐哈希。不要在生产环境使用简单字符串作为 Token。推荐 Sa-Token 或 JWT配合 Redis 控制 Token 过期。代码分层要清晰。Controller 只做参数接收和结果封装Service 写业务逻辑Mapper 只负责数据库访问。很多同学把所有逻辑堆在 Controller 里后期改动非常痛苦。9.2 数据库安全与备份失物招领系统虽然不像金融系统那样对数据一致性要求苛刻但操作数据库前仍要养好习惯所有 UPDATE / DELETE 操作先 SELECT 确认影响范围。在测试库验证 SQL再同步到生产库。定期备份mysqldump -uroot -p lost_found lost_found_$(date %Y%m%d).sql恢复mysql -uroot -p lost_found lost_found_20250101.sql9.3 上线部署注意事项前端打包npm run build生成dist目录。后端打包mvn clean package -DskipTests生成 jar 包。可以单独部署也可以用 Nginx 托管前端静态文件并将/api反代到后端服务。Nginx 参考配置server { listen 80; server_name lostfound.example.com; location / { root /opt/frontend/dist; index index.html; try_files $uri $uri/ /index.html; } location /api/ { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }9.4 可扩展方向这套项目跑通之后可以向以下方向扩展消息通知认领被通过/拒绝时给用户发站内信或邮件。数据统计首页展示每日发布量、认领成功率。图片云存储把本地存储替换为 OSS 或 COS。管理员审核增加物品发布审核流程避免不实信息。地图标注丢失/拾获地点用地图选点提升信息匹配效率。10. 结语与源码使用建议以上就是失物招领系统从需求分析、数据库设计、SpringBoot3 后端到 Vue3 前端开发的完整过程。这套项目的核心价值不在代码量而在于它把 SpringBoot3 Vue3 MySQL 全链路串了起来你学会了建表、学会了写 CRUD、学会了前后端联调、学会了处理跨域和上传文件这些能力是做任何 JavaWeb 项目的通用底座。建议你看完文章后不要直接复制代码交作业而是按下面的顺序动手写一遍先建数据库用 Navicat 或 DataGrip 把表结构建出来。后端从Goods实体开始用 Postman 测试接口。前端用 Vite 创建项目先跑通列表页。再逐步增加登录、发布、认领功能。遇到报错不要慌先看后端控制台日志再看浏览器 Network最后检查数据库数据。如果本文对你有帮助可以收藏备用后面需要部署上线或者扩展功能时再翻出来对照。
上一篇/下一篇内容由系统自动关联 返回资讯列表 →