基于SpringBoot+Vue的旧衣物捐赠系统开发实战:从数据库设计到前后端联调
2026/9/11 6:27:31 网站建设 项目流程

旧衣物捐赠系统这类选题,在最近的项目咨询里出现的频率是真的高。它表面看着就是个普普通通的信息管理系统,但真到了动手写的阶段,很多人才发现——涉及角色权限、图片上传、状态流转、前后端联调这些环节,随便一个地方卡住,整个项目就推进不下去。我最近刚好完整做完了一个基于 SpringBoot + Vue 的旧衣物捐赠系统,前端用了 Vue 全家桶,后端走的是 SpringBoot + MyBatis,也就是标题里那个 "ssm" 容易让人困惑的组合。这篇就纯粹以我的实际开发经历为线索,把这个系统从数据库设计到接口开发,再到前端页面联调的完整过程拆开讲清楚,希望能给正在做类似选题的朋友省下一些弯路。

这套系统适合谁参考?一是准备拿它当毕业设计或课程设计的同学,二是刚接触前后端分离开发、想找一个完整业务练手的初级开发者。我会尽量把每一步为什么这么做也讲清楚,而不是只给结论。代码示例、SQL 建表、踩坑记录都会有,你照着思路改改就能用到自己的项目里。

1. 项目到底做什么:技术选型拆解与整体业务设计

1.1 "ssm"和"springboot"到底是什么关系

先解决一个最容易让人懵的问题:题目里既有 "ssm" 又有 "springboot",到底该用哪一套?

老一批的 Java Web 项目,ssm 指的是 Spring + SpringMVC + MyBatis 这三件套。Spring 管对象,SpringMVC 管请求路由,MyBatis 管数据库操作。SpringBoot 出现之后,SpringMVC 的功能已经被 SpringBoot 的 web 模块直接整合了,Spring 更是 SpringBoot 的地基,所以实际开发中只要用 SpringBoot + MyBatis 就等价于把原来的 ssm 能力全拿到手了,而且配置还少了一大堆。

我建议的做法是:项目骨架直接用 SpringBoot,持久层用 MyBatis,前端用 Vue。这样既满足 "ssm" 的业务实质——Spring 容器 + 类 MVC 的分层结构 + MyBatis 操作数据库,又符合现代 Java Web 开发的主流习惯,配置量也小得多。真用传统 SSM 的 XML 配置去写一个捐赠系统,光是 springmvc.xml、applicationContext.xml 这些配置文件就够折腾一天,完全没必要。

1.2 角色权限与核心业务闭环

旧衣物捐赠系统,表面上是"发布衣物、申请领取"这么简单,但把业务捋清楚之后,至少要有三个角色参与:

  • 捐赠者:登录后发布旧衣物、查看自己发布的记录、处理他人的申请。
  • 受助者(也可以叫申请人):浏览在架衣物、提交捐赠申请、查看申请状态。
  • 管理员:审核用户、审核衣物上下架、管理公告、查看所有捐赠记录。

从用户发布一件旧衣服,到它真正被送到需要的人手里,中间要经过一条完整的状态链:衣物上架 -> 受助者发起申请 -> 捐赠者确认 -> 管理员审核 -> 线下交接 -> 完成捐赠

我最初设计时忽略了一个问题:衣物状态和申请状态必须分开管理。衣物本身有"审核中/在架/已下架"的状态,申请记录有"待确认/已通过/已完成/已取消"的状态。如果把这两个状态合并成单个字段,后面业务逻辑根本没法写。这一点在数据库设计章节会详细展开。

1.3 技术栈明细:为什么前端选 Vue

整个项目的技术栈相对主流,没有为了炫技引入冷门框架:

  • 后端:SpringBoot 2.x + MyBatis + MySQL 5.7,Java 8
  • 前端:Vue 2 + Element UI + Axios + Vue Router
  • 开发工具:IDEA + Navicat + Postman
  • 环境:Node 14 以上,Maven 3.6+

为什么前端选 Vue 而不是其他框架?核心原因有两个。第一,Vue 的学习曲线比很多框架平缓,对于要写管理后台和几个业务页面的项目来说非常合适;第二,Element UI 组件库几乎把后台管理需要的表格、表单、弹窗、上传组件全部封装好了,不夸张地说,页面开发效率至少提高一倍。而且 Vue 2 的生态非常成熟,遇到问题几乎都能搜到解决方案,非常适合项目开发周期紧张的场景。

2. 数据库设计:先把表结构想清楚,后面代码能少改一半

2.1 用户表设计:用一个字段区分三种角色

用户表是系统最基本的一张表,设计得是否合理直接影响后续所有功能的开发。我的建议是不要建三张用户表(管理员表、捐赠者表、受助者表),一张用户表加一个角色字段就够了。这是因为三种角色本质上都属于"系统用户",很多字段是重叠的,拆成三张表反而是给自己挖坑。

CREATE TABLE `user` ( `id` int(11) NOT NULL AUTO_INCREMENT, `username` varchar(50) NOT NULL COMMENT '用户名/登录账号', `password` varchar(100) NOT NULL COMMENT '密码(MD5加密)', `nickname` varchar(50) DEFAULT NULL COMMENT '昵称', `phone` varchar(20) DEFAULT NULL COMMENT '手机号', `avatar` varchar(255) DEFAULT NULL COMMENT '头像地址', `role` tinyint(4) NOT NULL DEFAULT '1' COMMENT '角色: 0管理员 1捐赠者 2受助者', `status` tinyint(4) NOT NULL DEFAULT '1' COMMENT '状态: 1正常 0禁用', `create_time` datetime DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4;

这里特别提醒两个细节。密码字段长度建议设 100,因为如果是 MD5 加盐或者其他加密方式,32 位固定长度还行,但如果后面要换成 BCrypt,60 位是跑不掉的,字段不够长就得改表。角色字段用 tinyint 而不是字符串,是为了查询效率,但代码里一定要写好枚举常量,不然时间一长你自己都忘了 1 和 2 代表什么。

2.2 衣物信息表:图片存储不要用 base64

衣物信息表是整个系统的核心业务表。设计的时候要考虑哪些字段?衣物的名称、描述、分类(上衣/裤子/鞋帽/其他)、成色、图片、发布者、状态、发布时间,还有一个容易被忽略的"领取条件"或"备注"字段。

CREATE TABLE `clothes` ( `id` int(11) NOT NULL AUTO_INCREMENT, `title` varchar(100) NOT NULL COMMENT '衣物名称', `category` varchar(20) DEFAULT NULL COMMENT '分类: 上衣/裤装/鞋靴/配饰/其他', `condition_level` varchar(20) DEFAULT NULL COMMENT '成色: 全新/九成新/七成新/有瑕疵', `description` text COMMENT '详细描述', `image` varchar(255) DEFAULT NULL COMMENT '图片路径', `publisher_id` int(11) NOT NULL COMMENT '发布者ID', `status` tinyint(4) NOT NULL DEFAULT '0' COMMENT '状态: 0待审核 1在架 2已下架 3已捐出', `create_time` datetime DEFAULT NULL, `update_time` datetime DEFAULT NULL, PRIMARY KEY (`id`), KEY `idx_publisher` (`publisher_id`), KEY `idx_status` (`status`) ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4;

有一个我踩过的坑必须讲:图片字段千万不要用 base64 字符串往数据库里塞。当时我图省事,前端直接把图片转成 base64 传到后端,一张照片差不多能有几 MB 的文本,数据库一下子就膨胀了,页面加载也是肉眼可见的卡。正确的做法是把图片文件传到服务器的某个目录,数据库里只存访问路径。后面在接口开发章节我会给出具体的文件上传方案。

2.3 捐赠申请表:状态机设计是业务核心

捐赠申请表记录了"谁申请了哪件衣物"以及这笔申请现在的处理进度。这里的状态设计我建议至少要有四个:待确认、已通过、已拒绝、已完成(线下交接后由管理员或捐赠者确认完成)。

CREATE TABLE `donation_apply` ( `id` int(11) NOT NULL AUTO_INCREMENT, `clothes_id` int(11) NOT NULL COMMENT '衣物ID', `applicant_id` int(11) NOT NULL COMMENT '申请人ID', `apply_reason` varchar(500) DEFAULT NULL COMMENT '申请说明', `status` tinyint(4) NOT NULL DEFAULT '0' COMMENT '状态: 0待确认 1已通过 2已拒绝 3已完成', `apply_time` datetime DEFAULT NULL, `handover_time` datetime DEFAULT NULL COMMENT '交接完成时间', PRIMARY KEY (`id`), KEY `idx_clothes` (`clothes_id`), KEY `idx_applicant` (`applicant_id`) ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4;

这里还有一个业务上的关键点:一件衣物被申请之后,是不是要立刻下架让其他人不能再申请?我的处理方式是不用立刻下架,但同一件衣物在同一时刻只允许有一条"待确认"或"已通过"的申请记录。这个约束在代码里判断,逻辑是:查询这件衣物是否已有 status 为 0 或 1 的申请,如果有就不允许再次申请。不然就会出现一件衣服被好几个人同时申请通过,线下交接的时候打架的尴尬情况。

2.4 辅助表:公告与轮播图

除了业务主表,一个好的信息管理系统最好还有公告管理功能。公告表结构很简单:标题、内容、发布时间、发布人。如果还有首页轮播图需求,再加一张 banner 表,字段就是图片路径、跳转链接、排序号、状态。

这些辅助表不复杂,但功能上有画龙点睛的作用,也让系统的完整度更高。有需要就拿去用:

CREATE TABLE `notice` ( `id` int(11) NOT NULL AUTO_INCREMENT, `title` varchar(100) NOT NULL, `content` text, `create_time` datetime DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

3. 后端接口开发实录:SpringBoot 的落地细节

3.1 项目初始化和关键依赖配置

用 IDEA 新建一个 SpringBoot 项目,选择 Java 8,勾选 Web、MyBatis、MySQL 驱动,这样一个基础工程就出来了。如果你的 IDEA 创建项目时网速不给力,可以直接去 Spring Initializr 网站下载压缩包再导入,效果一样。

pom.xml 里的关键依赖要特别注意版本匹配。SpringBoot 2.7.x 对应 MyBatis Starter 2.x,对应 MySQL 连接器 8.0.x。我最初用 SpringBoot 3.x 搭的时候发现 MyBatis 官方 Starter 还没完全适配,折腾了很久。所以稳妥起见,直接上 SpringBoot 2.7.x:

<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.7.18</version> <relativePath/> </parent> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>2.3.1</version> </dependency> <dependency> <groupId>com.mysql</groupId> <artifactId>mysql-connector-j</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid-spring-boot-starter</artifactId> <version>1.2.20</version> </dependency> </dependencies>

3.2 application.yml 配置:端口、数据库、上传路径一次配齐

配置文件建议直接上 yml,比 properties 可读性好很多。除了常规的数据源配置,还有两个东西必须提前配好:MyBatis 的 mapper 扫描路径、文件上传的路径。

server: port: 8080 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/donation?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: 123456 type: com.alibaba.druid.pool.DruidDataSource servlet: multipart: max-file-size: 10MB max-request-size: 20MB mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.donation.entity configuration: map-underscore-to-camel-case: true

map-underscore-to-camel-case这个配置强烈建议开启。数据库字段 create_time,对应 Java 实体类属性 createTime,开启驼峰映射后 MyBatis 会自动转换,不用在 XML 里写一堆 resultMap,能少写很多代码。

3.3 用户登录与 Token 验证

用户登录是第一个要写的核心接口。我的方案是:登录成功之后生成一个 Token 返回给前端,前端存到 localStorage,后续每次请求在请求头里带上这个 Token。后端用一个拦截器统一校验 Token。

Token 的生成不需要引入特别复杂的框架,用 UUID 加上用户 ID 和时间戳拼一个字符串,再用 MD5 处理一下就够了,毕竟这只是个课设/毕设级别的系统。如果你要追求更规范的做法,可以用 JWT,但对这个项目来说有点大材小用。

@PostMapping("/login") public Result login(@RequestBody User loginUser) { User user = userService.login(loginUser.getUsername(), loginUser.getPassword()); if (user == null) { return Result.error("用户名或密码错误"); } // 生成token:md5(uuid + userId + timestamp) String token = Md5Util.md5(UUID.randomUUID() + user.getId() + System.currentTimeMillis()); // 存入数据库或Redis,这里直接存一张token表,简单可靠 tokenService.saveToken(user.getId(), token); Map<String, Object> data = new HashMap<>(); data.put("token", token); data.put("userInfo", user); return Result.success(data); }

这里我要分享一下为什么不用 JWT:JWT 最大的问题是一旦签发,在有效期内无法在服务端主动作废。比如管理员想封禁某个用户,如果用的是 JWT,除非把密钥换掉,不然这个用户拿着旧 Token 还是能继续访问接口。自己维护一个 Token 表,想做登录失效、强制下线都很容易实现。

3.4 衣物发布与多条件查询接口

衣物发布接口涉及文件上传和表单数据提交两部分。我推荐的做法是分成两个接口:一个负责上传图片返回访问路径,一个负责提交衣物表单数据。这样前端可以先把图片传完拿到路径,再随表单一起提交,逻辑清晰,也方便图片上传失败时单独重试。

衣物列表查询要支持多条件筛选:按分类、按成色、按状态、按关键词搜索。MyBatis 的动态 SQL 在这里派上用场:

<select id="findClothesList" resultType="com.donation.entity.Clothes"> SELECT * FROM clothes <where> <if test="category != null and category != ''"> AND category = #{category} </if> <if test="conditionLevel != null and conditionLevel != ''"> AND condition_level = #{conditionLevel} </if> <if test="keyword != null and keyword != ''"> AND (title LIKE CONCAT('%', #{keyword}, '%') OR description LIKE CONCAT('%', #{keyword}, '%')) </if> <if test="status != null"> AND status = #{status} </if> </where> ORDER BY create_time DESC </select>

使用<where>标签有个好处:当所有条件都为空时,它不会生成 WHERE 关键字,SQL 不会出错。用CONCAT('%', #{keyword}, '%')而不是直接在 Java 里拼好再传入,是为了防止 SQL 注入。这些都是 MyBatis 开发中非常基础的但也很关键的细节。

3.5 文件上传接口与静态资源映射

文件上传这块我前面提过不要存 base64。后端的处理逻辑是:接收 MultipartFile,按日期分目录存储,生成唯一文件名,然后把可访问的 URL 返回给前端。

@PostMapping("/upload") public Result upload(@RequestParam("file") MultipartFile file) { if (file.isEmpty()) { return Result.error("上传文件不能为空"); } // 生成存储目录: upload/2024/05/20/ String datePath = new SimpleDateFormat("yyyy/MM/dd").format(new Date()); String uploadDir = "D:/donation-upload/" + datePath; File dir = new File(uploadDir); if (!dir.exists()) { dir.mkdirs(); } // 生成唯一文件名 String originalFilename = file.getOriginalFilename(); String ext = originalFilename.substring(originalFilename.lastIndexOf(".")); String fileName = UUID.randomUUID().toString().replace("-", "") + ext; file.transferTo(new File(uploadDir + "/" + fileName)); String url = "/upload/" + datePath + "/" + fileName; return Result.success(url); }

这里有个关键问题:上传的文件存在磁盘目录 D:/donation-upload/ 下,浏览器访问 http://localhost:8080/upload/2024/05/20/xxx.jpg 能不能直接看到?答案是不能,因为 SpringBoot 默认只处理 classpath 下的静态资源。必须做一个静态资源映射,把 /upload/** 的访问请求映射到磁盘目录:

@Configuration public class WebConfig implements WebMvcConfigurer { @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler("/upload/**") .addResourceLocations("file:D:/donation-upload/"); } }

这个映射配置是我在实战中见过被问得最多的问题之一。有人图片传上去了,数据库路径也存对了,就是页面上图片裂了,十有八九是这个配置没写。

4. 前端 Vue 实现:从零搭建到接口联调

4.1 初始化 Vue 项目与安装依赖

前端我用 Vue 2 + Element UI。创建项目用官方脚手架:

npm install -g @vue/cli vue create donation-web

创建的时候选择默认的 Vue 2 预设就行。进入项目目录后安装 Element UI、Axios、Vue Router:

npm install element-ui npm install axios npm install vue-router@3

这里有个版本坑必须提醒:Vue Router 4.x 是给 Vue 3 用的,Vue 2 项目必须安装 vue-router@3,否则安装完了一启动就报错。Element UI 也一样,Element Plus 是 Vue 3 的,Vue 2 只能用 Element UI。我第一次搭的时候没注意版本,装成了 Element Plus,结果按钮都渲染不出来,排查半天才发现是版本号的问题。

在 main.js 里注册:

import Vue from 'vue' import App from './App.vue' import ElementUI from 'element-ui' import 'element-ui/lib/theme-chalk/index.css' import router from './router' Vue.use(ElementUI) Vue.config.productionTip = false new Vue({ router, render: h => h(App) }).$mount('#app')

4.2 Axios 封装与请求拦截器

所有接口请求都需要携带 Token,如果每次请求都要手动加请求头,那就太痛苦了。正确的做法是封装一个统一的 Axios 实例,用拦截器统一处理。

import axios from 'axios' import { Message } from 'element-ui' import router from '../router' const request = axios.create({ baseURL: 'http://localhost:8080', 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) { Message.error(res.msg || '请求失败') return Promise.reject(new Error(res.msg)) } return res }, error => { if (error.response && error.response.status === 401) { Message.error('登录已过期,请重新登录') localStorage.removeItem('token') router.push('/login') } else { Message.error('网络异常,请稍后重试') } return Promise.reject(error) } ) export default request

统一封装的好处非常明显:以后后端如果调整了接口返回格式,或者要统一处理某种错误码,只需要改拦截器一个地方,所有页面都生效。这也是前后端分离项目的基本功。

4.3 路由配置与登录守卫

路由配置要区分哪些页面需要登录才能访问,哪些是公开的。用 Vue Router 的导航守卫实现:

const routes = [ { path: '/login', component: Login }, { path: '/register', component: Register }, { path: '/', component: Layout, redirect: '/home', children: [ { path: 'home', component: Home, meta: { title: '首页' } }, { path: 'clothes', component: ClothesList, meta: { title: '衣物广场' } }, { path: 'my/release', component: MyRelease, meta: { title: '我的发布', requiresAuth: true } }, { path: 'my/apply', component: MyApply, meta: { title: '我的申请', requiresAuth: true } }, { path: 'admin/user', component: AdminUser, meta: { title: '用户管理', requiresAuth: true, adminOnly: true } }, { path: 'admin/clothes', component: AdminClothes, meta: { title: '衣物审核', requiresAuth: true, adminOnly: true } } ] } ] router.beforeEach((to, from, next) => { const token = localStorage.getItem('token') if (to.meta.requiresAuth && !token) { next('/login') } else if (to.meta.adminOnly) { // 校验用户角色 const role = localStorage.getItem('role') if (role !== '0') { Message.error('无权访问该页面') next('/home') } else { next() } } else { next() } })

adminOnly 这个思路是我后期补上的。最初我天真地以为后端接口校验了权限就够了,后来发现前端页面照样能通过地址栏直接访问。虽然后端拦截器会挡住非法请求,但前端做个权限控制,用户在界面上体验会好很多,不会看到一堆报错页面。

4.4 衣物发布表单与图片上传组件

前端发布衣物页面,用 Element UI 的 el-form 和 el-upload 组件。el-upload 的关键配置是 action 属性指向后台上传接口,headers 带上 Token,on-success 回调里把返回的图片路径存到表单字段中:

<el-form :model="form" label-width="80px"> <el-form-item label="衣物名称"> <el-input v-model="form.title" placeholder="请输入衣物名称" /> </el-form-item> <el-form-item label="分类"> <el-select v-model="form.category"> <el-option label="上衣" value="上衣" /> <el-option label="裤装" value="裤装" /> <el-option label="鞋靴" value="鞋靴" /> <el-option label="配饰" value="配饰" /> <el-option label="其他" value="其他" /> </el-select> </el-form-item> <el-form-item label="成色"> <el-radio-group v-model="form.conditionLevel"> <el-radio label="全新">全新</el-radio> <el-radio label="九成新">九成新</el-radio> <el-radio label="七成新">七成新</el-radio> <el-radio label="有瑕疵">有瑕疵</el-radio> </el-radio-group> </el-form-item> <el-form-item label="图片"> <el-upload action="http://localhost:8080/upload" :headers="{ token: getToken() }" :on-success="handleUploadSuccess" list-type="picture-card" :limit="1"> <i class="el-icon-plus"></i> </el-upload> </el-form-item> <el-form-item label="描述"> <el-input type="textarea" v-model="form.description" rows="5" /> </el-form-item> <el-form-item> <el-button type="primary" @click="submitForm">发布</el-button> </el-form-item> </el-form>

图片上传成功后的回调处理:

handleUploadSuccess(response) { if (response.code === 200) { this.form.image = response.data this.$message.success('图片上传成功') } else { this.$message.error('图片上传失败') } }

4.5 前后端联调中的跨域问题

前后端分离开发必然遇到跨域问题。前端地址是 http://localhost:8081 ,后端接口是 http://localhost:8080 ,浏览器会拦截跨域请求。解决跨域最简单的方式是后端起一个全局 CORS 配置类:

@Configuration public class CorsConfig implements WebMvcConfigurer { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") .allowedOriginPatterns("*") .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") .allowedHeaders("*") .allowCredentials(true) .maxAge(3600); } }

但我建议实际开发时用更简单的方式:前端在 vue.config.js 里配置 devServer 的 proxy,把 /api 开头的请求都代理到后端地址。这样浏览器看到的请求还是同源的,不存在跨域问题,也就不用后端做额外配置。如果用代理方案,注意 Axios 的 baseURL 要改成 '/api',后端接口地址保持不变。

5. 常见问题与排查技巧实录

5.1 前端请求一直报 404 或 405

这个问题的排查思路:先在浏览器 F12 里看 Network 面板,确认请求 URL 和后端实际接口是否一致。最常见的原因是后端 Controller 的 @RequestMapping 路径和前端 Axios 请求路径不一致,或者 GET/POST 方法类型不匹配。

注意:检查路径时不要忽略 context-path。如果后端设置了server.servlet.context-path: /api,那所有请求前缀都要带上。我见过有人在 application.yml 里加了 context-path,前端却忘了加,结果浪费半天时间排查。

5.2 图片上传成功但页面显示裂图

这个我前面已经埋了伏笔。核心排查步骤是三步:第一,确认上传接口返回的 URL 是什么;第二,在浏览器直接访问这个 URL,看能不能打开;第三,如果打不开,检查 SpringBoot 的静态资源映射是否配置正确。另外注意图片跨域防盗链的问题,如果是前后端不同端口,前端页面里 img 的 src 是 http://localhost:8080/upload/xxx.jpg,这个属于跨域图片加载,一般不会被拦截,但如果后端加了图片鉴权拦截器就可能出问题。

5.3 LocalDateTime 传到前端变成一串 T 格式的字符

这个问题特别常见:数据库字段是 datetime,Java 实体类是 LocalDateTime,Jackson 默认序列化的格式是 "2024-05-20T10:30:00",而前端想要的是 "2024-05-20 10:30:00"。解决办法是在 application.yml 里配置全局时间格式化:

spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT+8

5.4 Maven 依赖冲突导致的启动失败

典型场景是 spring-boot-starter 自带的日志框架和其他依赖自带的日志框架冲突。启动时报 SLF4J 相关的异常,或者莫名的 ClassNotFoundException。排查方式是 mvn dependency:tree 查看依赖树,找到冲突的依赖排除掉。用 IDEA 的话,直接在 pom.xml 里右键 -> Diagragrams -> Show Dependencies 就能看到依赖冲突情况。

另外 SpringBoot 版本太高也是个隐患。有些教程推荐直接用最新的 3.x,但 3.x 要求 Java 17 起步,很多老项目的代码和第三方库都不兼容。如果你只是做这个捐赠系统,不要追求新版本,稳定压倒一切。

5.5 用户只能看自己的数据:数据权限的 SQL 写法

很多新手会忽略数据权限问题。比如"我的发布"页面,查询结果不应该把别人的衣物也查出来。最容易实现的方案是在 SQL 里加 publisher_id 条件,但如果每个查询都手动加,容易漏。我的做法是 Service 层统一封装一个当前用户工具类,在需要数据过滤的地方调用,保证当前登录用户 ID 一定被拼进查询条件。

public class UserContext { private static ThreadLocal<Integer> currentUser = new ThreadLocal<>(); public static void set(Integer userId) { currentUser.set(userId); } public static Integer get() { return currentUser.get(); } public static void clear() { currentUser.remove(); } }

登录拦截器里在请求进入 Controller 之前解析 Token,拿到用户 ID 放入 UserContext,请求结束后在 finally 里 clear。这样一个请求链路内,任何 Service 层代码都能拿到当前登录用户 ID。但要注意,Async 线程和定时任务里 ThreadLocal 会失效,这个项目用不到,知道即可。

写在最后

这个旧衣物捐赠系统,我从数据库建表到前后端联调完成,大概用了两周左右的业余时间。整体上不算难,但小坑确实不少。我的体会是:做这类系统,最容易出问题的不是业务功能本身,而是前后端衔接的那些"细枝末节"——跨域、Token 传递、文件路径、时间格式、状态同步。如果一开始就把技术方案定好,基础设施搭扎实,后面写业务代码反而很快。最后再分享一个小技巧:联调时不要闷头写代码,先拿 Postman 把所有后端接口全部自测通过,再开始写前端页面。接口通了,前端出问题就一定是前端的事,排查范围缩小一大半,这个习惯能帮你省掉大量无意义的联调时间。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询