尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

基于SpringBoot+Vue的高校物品捐赠管理系统设计与实现

基于SpringBoot+Vue的高校物品捐赠管理系统设计与实现 高校里的物品捐赠一直是学生工作、校友会、基金会最头疼的环节之一。以前靠Excel登记物资种类一多就乱受赠人信息靠手工查重领用记录更是没法追溯。一个学生捐了三本书、一件军训服、一台旧电脑三个部门各登记一遍数据口径对不上审计的时候谁也说不清。后来我基于SpringBootVueMyBatisMySQL这套技术栈从零做起了一个企业级高校物品捐赠管理系统把捐赠人登记、物资入库、库存台账、领用申请、审批流转、出库记录、统计报表全链路串了起来。这篇就把我完整的设计思路、核心代码实现、以及我在实际开发和部署中踩过的坑一次性整理出来供做课程设计、毕业设计或者准备接手类似校园管理系统的同学直接参考。项目本身的技术选型其实没什么花哨的后端用SpringBoot做业务底座MyBatis作为数据持久层框架数据库用MySQL存核心业务数据前端用Vue搭管理界面配合Element UI做后台风格组件。这套组合是目前Java Web领域最常见、也最适合中小型管理信息系统的搭配——学习资料多、社区问答齐全、出了问题能快速找到解决方案。系统核心围绕“捐赠入库—库存管理—申请领用—审批出库—报表统计”这条业务主线展开角色分为系统管理员、捐赠登记员如辅导员、领用申请人员如学生会、班级、贫困生资助中心通过登录认证和角色权限控制实现不同人员的操作边界。适合谁来参考如果你是正在为课程设计、毕业设计找Java Web项目的学生或者你在学校信息化部门工作想帮学校打通捐赠物资管理流程这篇值得从头看一遍如果你已经有SpringBoot基础只是想快速移植一套可用系统可以直接跳到第4节的代码实现和第5节的部署清单。1. 项目整体设计与技术选型拆解1.1 为什么是SpringBootVueMyBatisMySQL先说后端框架。现在Java Web领域的选择其实很多有Spring Cloud微服务体系、有SpringBoot单体架构、也有比较小众的JFinal、Nutz这类国产框架。但我最终坚持用SpringBoot核心原因是它在中小型系统中的“收敛力”非常强——一个内嵌Tomcat、一套自动配置、一个统一的启动入口大大降低了部署和运维的心智负担。高校物品捐赠管理系统本质是典型的CRUD密集型系统捐赠单增删改查、库存台账、领用记录、用户管理、数据统计。这种场景下的核心诉求是“稳定、清晰、快速交付”而不是“弹性伸缩、服务治理、链路追踪”。SpringBoot天然适合这种项目它把Spring生态的Bean管理、事务控制、AOP切片能力全部保留下来同时像spring-boot-starter-web、spring-boot-starter-jdbc这些起步依赖自带默认配置让我能集中精力写业务逻辑而不是花半个月去调XML配置文件。再说持久层。有的同学会问“现在不是流行MyBatis-Plus吗为什么还选原生MyBatis”我承认MyBatis-Plus在单表CRUD上确实方便自带BaseMapper和Wrapper查询省去大量手写SQL的功夫。但捐赠管理系统有一个绕不开的硬需求——物资台账涉及大量多表联查和分组统计比如按捐赠类别统计数量、按领用单位统计金额、按时间统计入库趋势这种复杂SQL的定制能力反而是原生MyBatis配合XML映射更顺手。而且MyBatis的缓存机制、动态SQL、参数绑定规则是面试里高频考察的知识点用原生实现一遍对理解框架底层的收益更大。前端选Vue也是一样的逻辑。管理后台界面不需要什么炫酷动效核心是表单交互、表格展示、状态流转。Vue的双向数据绑定让表单状态管理变得非常直观组件化开发把捐赠登记、库存列表、审批流程拆成独立组件互不干扰、方便复用。配合Vue Router做页面路由比如/donation/add、/inventory/list、/approval/pending配合Vuex或Pinia做登录态和全局用户信息管理整体开发节奏会非常流畅。如果要对接数据可视化还可以集成ECharts统计报表组件但这是后话前面用Element UI表格就足够支撑表格类展示。1.2 系统模块划分与业务流程梳理这个系统我按业务域拆成了六个核心模块用户与权限模块、捐赠登记模块、库存台账模块、领用申请模块、审批管理模块、统计报表模块。用户与权限模块解决“谁能登进系统、能做什么”。采用经典的RBAC模型用户表关联角色表角色表关联权限表。普通学生账号只能看到捐赠登记和个人捐赠记录辅导员或院系管理员能进行物资入库、库存查询系统管理员拥有全部权限包括用户管理、角色分配和数据统计。通过Shiro或Spring Security做登录认证和接口授权登录成功后签发Token前端保存Token并在请求头中携带后端通过拦截器统一校验。捐赠登记模块是整个系统的数据入口。捐赠人可以是学生、教师、校友或社会企业登记时记录捐赠人信息、捐赠物品名称、类别书籍类/衣物类/电子产品类/文体用品类/其他、数量、预估价值、捐赠日期。登记完成后生成一条唯一的捐赠单号例如DON20250613001规则是DON 年月日 当日序号。库存台账模块是系统的数据核心。每一批捐赠物资入库时系统自动在库存表中新增相应记录或累加已有相同物品的库存数量同时记录库位编号、入库批次号、负责人、存放地点。出库时同步扣减并保留完整的出入库流水确保每一件物资有迹可循。领用申请模块服务于受捐对象。贫困生资助中心、学生会活动部、班级事务负责人可以在线提交领用申请填写申请事由、所需物资清单、预计用途提交后进入审批流。审批管理模块完成流转控制。辅导员初审确认物资需求真实性管理员复审确认库存是否足够每个环节都留审批意见全程可审计。统计报表模块面向管理决策。系统按月份、院系、物品种类等维度统计捐赠量和领用量生成柱状图和明细报表辅助学校了解捐赠物资的流转情况。1.3 角色权限数据模型设计权限模型如果设计不好后期会出现严重的越权访问问题。我采用的方案是五张表sys_user、sys_role、sys_menu、sys_user_role、sys_role_menu。用户注册时分配初始角色为ROLE_STUDENT管理员登录后可以通过用户管理页手动提升角色。菜单表里维护的是前端路由信息和后端接口地址权限控制到按钮级别——比如普通学生看不到“审批管理”菜单即使手输URL也进不去因为后端的接口过滤器会校验角色标识。安全方面密码绝对不能明文存储我使用BCrypt加密算法每个用户的盐值随机生成即使数据库泄露暴力破解成本也极高。登录接口增加验证码校验防止恶意脚本刷接口。2. 数据库设计核心表结构与关键索引实践2.1 物资与捐赠核心表设计数据库是整个系统的地基。我建库命名为donation_system统一使用utf8mb4字符集排序规则用utf8mb4_general_ci避免中文乱码问题。核心表之一donation_records用于存储每一笔捐赠登记信息CREATE TABLE donation_records ( id bigint(20) NOT NULL AUTO_INCREMENT COMMENT 主键ID, donation_no varchar(32) NOT NULL COMMENT 捐赠单号, donor_name varchar(50) NOT NULL COMMENT 捐赠人姓名, donor_type tinyint(4) NOT NULL DEFAULT 1 COMMENT 捐赠人类型1学生 2教师 3校友 4社会企业, donor_contact varchar(30) DEFAULT NULL COMMENT 联系电话, donor_unit varchar(80) DEFAULT NULL COMMENT 所属院系/单位, item_name varchar(80) NOT NULL COMMENT 物品名称, item_category varchar(20) NOT NULL COMMENT 物品类别, item_quantity int(11) NOT NULL DEFAULT 1 COMMENT 捐赠数量, estimated_value decimal(10,2) DEFAULT 0.00 COMMENT 预估价值, donation_date date NOT NULL COMMENT 捐赠日期, status tinyint(4) NOT NULL DEFAULT 0 COMMENT 状态0待入库 1已入库 2已驳回, remark varchar(255) DEFAULT NULL COMMENT 备注, create_by bigint(20) DEFAULT NULL COMMENT 登记人ID, create_time datetime DEFAULT CURRENT_TIMESTAMP COMMENT 创建时间, update_time datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 更新时间, PRIMARY KEY (id), UNIQUE KEY uk_donation_no (donation_no), KEY idx_item_category (item_category), KEY idx_donation_date (donation_date) ) ENGINEInnoDB AUTO_INCREMENT1 DEFAULT CHARSETutf8mb4 COMMENT捐赠登记表;这里有几个细节我要特意说明。第一单据编号字段加唯一索引uk_donation_no这是防止重复入库的第一道屏障——如果用代码判重并发情况下容易漏判用数据库唯一约束兜底生成编号时报错在Service层捕获异常再生成新编号重试即可。第二查询条件最频繁的是“按时间范围查”和“按类别查”所以donation_date和item_category一定要建索引否则数据量过万后列表查询会明显变慢。库存表inventory_stock我单独建一张而不是直接在捐赠表上改数量。原因很简单捐赠流水和库存快照是两种不同性质的数据流水记录“发生过什么”库存记录“现在还剩下什么”。如果把两者混在一张表里那么一次领用出库就要修改捐赠记录捐赠历史就被篡改了审计时根本说不清。CREATE TABLE inventory_stock ( id bigint(20) NOT NULL AUTO_INCREMENT, item_name varchar(80) NOT NULL, item_category varchar(20) NOT NULL, total_quantity int(11) NOT NULL DEFAULT 0 COMMENT 总入库数量, remaining_quantity int(11) NOT NULL DEFAULT 0 COMMENT 剩余数量, location varchar(80) DEFAULT NULL COMMENT 存放位置, warehouse varchar(50) DEFAULT NULL COMMENT 库房, last_update_time datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_item_warehouse (item_name,warehouse) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT库存台账表;uk_item_warehouse联合唯一索引保证同一库房内的同一种物品只有一条库存记录。库存更新采用“乐观锁”机制——更新时带上version字段或用剩余数量做条件防止多用户并发领用导致超发。2.2 领用申请与审批流数据设计领用单和审批记录要拆成两张表。apply_orders保存申请单主表信息apply_items保存申请明细因为一个申请单里可能同时申请“书包2个、笔记本5本、台灯1个”明细表能更灵活地支撑多物品申请。审批环节是一个经典的“状态机”模式。我在表里设计一个status字段从0到4依次表示待初审、初审通过待终审、终审通过待出库、已完成、已驳回。这种设计允许审批过程中随时查看日志表来追溯谁在什么时间做了什么操作。审批记录表approval_logs会记录审批人、审批动作、审批意见和时间戳这个表在生产环境里非常重要能为高校资产管理部门提供完整的责任追踪依据。在事务处理上一定要在Transactional注解中处理整个审批链路。比如终审通过后要扣减库存、更新申请单状态、写入流水、插入审批日志四个操作任何一个失败都要整体回滚绝不能出现“状态改成已完成但库存没扣掉”的幽灵数据。2.3 常用查询SQL与报表统计优化报表统计如果用SELECT *然后在Java代码里做分组聚合数据量一大必然卡成幻灯片。我直接把聚合逻辑写在SQL里让MySQL原生计算返回结果SELECT DATE_FORMAT(donation_date, %Y-%m) AS month, item_category AS category, SUM(item_quantity) AS total_quantity, COUNT(DISTINCT donation_no) AS donation_times FROM donation_records WHERE donation_date BETWEEN #{startDate} AND #{endDate} GROUP BY month, category ORDER BY month DESC;对于按月趋势统计我建议前端用ECharts折线图展示后端只返回月份、类别的聚合数组。注意优化GROUP BY查询给donation_date和item_category建联合索引idx_date_category能够显著降低临时表和文件排序的概率。3. 后端核心模块实现认证、权限与事务3.1 SpringSecurityJWT登录认证全流程登录认证是后端安全的第一道门。我选用Spring Security加JWT的方案。JWT自包含用户标识和过期时间前后端分离架构下天然免Session非常适合REST API风格的系统。用户在登录页输入账号密码前端调/api/auth/login接口后端先用BCryptPasswordEncoder.matches()校验密码再加载用户角色和权限集合用Jwts.builder()生成Token返回前端。前端拿到Token后存到localStorage每次请求通过Axios请求拦截器在Authorization头携带Bearer xxx。安全过滤器里要放行登录接口、验证码接口、静态资源其余接口全部走JWT校验。Spring Security的配置核心片段如下Configuration EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { Autowired private JwtAuthenticationFilter jwtAuthenticationFilter; Override protected void configure(HttpSecurity http) throws Exception { http.csrf().disable() .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) .and() .authorizeRequests() .antMatchers(/api/auth/login, /api/auth/captcha).permitAll() .antMatchers(/api/admin/**).hasRole(ADMIN) .antMatchers(/api/approval/**).hasAnyRole(ADMIN, COUNSELOR) .anyRequest().authenticated() .and() .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class); } }JWT最大的坑是“过期时间设太短导致用户老是被踢下线设太长又有安全风险”。我的经验是Access Token有效期2小时同时前端保存一个Refresh Token有效期7天。Access Token过期后前端自动用Refresh Token换新Token用户完全无感知而一旦Refresh Token也过期就强制回登录页重新登录。3.2 MyBatis分页插件与动态SQL的实际用法列表页必做分页这是后端的基本功。MyBatis自带的分页原理是在Executor层拦截SQL自动拼接LIMIT语句。我试过最稳的方式是引入了PageHelper分页插件核心用法极简PageHelper.startPage(pageNum, pageSize); ListDonationRecordVO records donationRecordMapper.selectRecordList(queryVO); PageInfoDonationRecordVO pageInfo new PageInfo(records);这里有个我最初踩过的坑PageHelper.startPage()之后必须紧跟第一次查询不能在中间穿插其他数据库操作。因为PageHelper内部用ThreadLocal保存分页参数如果第一次执行的Mapper方法不是目标查询分页参数就会被意外消耗或应用到错误的SQL上。另外分页查询返回的总记录数是通过自动执行SELECT COUNT(*)实现的如果原SQL带有GROUP BY或者DISTINCTPageHelper的自动Count SQL很可能会算错最好手写一个count查询。动态SQL是MyBatis的杀手锏。捐赠记录列表查询条件经常是“捐赠人姓名、物品类别、时间范围可以任意组合也可以都不选”用动态SQL可以优雅实现这种不确定条件的查询select idselectRecordList resultTypecom.donation.vo.DonationRecordVO SELECT * FROM donation_records where if testdonorName ! null and donorName ! AND donor_name LIKE CONCAT(%, #{donorName}, %) /if if testitemCategory ! null and itemCategory ! AND item_category #{itemCategory} /if if teststartDate ! null AND donation_date gt; #{startDate} /if if testendDate ! null AND donation_date lt; #{endDate} /if /where ORDER BY donation_date DESC /select3.3 库存扣减事务与并发控制爱心物资被多人同时申领时库存并发问题特别容易出现。假设库存剩2件两个审批员同时通过两张各领1件的申请单如果不加控制两个请求都读到剩余量2各自扣成1最后库存还是2实际上应该变成0。这就是经典的并发超发问题。我的解决方案是“数据库行锁库存预扣校验”双保险。更新SQL改为条件更新UPDATE inventory_stock SET remaining_quantity remaining_quantity - #{quantity} WHERE id #{stockId} AND remaining_quantity #{quantity}返回值是受影响行数如果为0说明当前库存不足Service层直接抛出“库存不足”的异常结束流程。这条语句在InnoDB引擎下执行时会自动锁定对应行其他事务只能等待锁释放从而彻底避免超发。在这个基础上整个审批出库流程再包一层Transactional保证扣减库存和更新状态在同一个事务内原子提交。4. 前端Vue实现从路由配置到组件开发4.1 Vue项目结构划分与组件规划前端我基于Vue CLI或Vite搭建目录结构做了标准化分层src/views页面级组件如登录页、捐赠登记页、库存列表页、审批页、报表页src/components公共业务组件如物资明细表格、搜索表单、分页组件src/api统一封装的接口请求模块按业务域拆成donation.js、inventory.js、approval.jssrc/router路由配置使用动态路由方式根据角色加载菜单src/storeVuex模块存储用户信息、Token、菜单权限组件规划上有个心得把“搜索表单表格分页”三段式结构抽取成公共组件SearchTableLayout通过插槽传入搜索控件和表格列配置。后端系统80%的页面都是这种结构抽成组件后新增一个列表页只需要写几十行配置代码能节省大量重复劳动。4.2 路由守卫与权限控制前端路由守卫是权限控制的第一道展示层防线。我在全局前置守卫router.beforeEach中做三件事判断Token是否存在、判断用户角色是否已加载、判断当前页面路由是否在用户权限菜单列表中。router.beforeEach((to, from, next) { const token store.state.token; if (!token) { if (to.path /login) { next(); } else { next(/login); } return; } if (!store.state.userInfo) { store.dispatch(fetchUserInfo).then(() { if (store.state.menus.some(m m.path to.path)) { next(); } else { next(/403); } }); return; } next(); });注意前端守卫只是提升用户体验真正的安全边界还是要在后端接口做。前端隐藏了按钮不代表攻击者不会绕过界面直接调接口所以后端每个接口务必校验角色权限这是我一直强调的底线。4.3 Axios封装与表单交互细节Axios请求封装是前端的隐蔽大坑。我的封装会统一处理请求头Token注入、响应状态码解析、错误提示、加载动画。响应拦截器里要特殊处理两个场景HTTP 401说明Token失效需要调刷新接口换新TokenHTTP 403说明越权访问弹出“无权限”提示跳转403页面。表单交互上捐赠登记页有几个小细节值得特意优化捐赠日期默认当天、物品类别用下拉选择而非手输、数量字段用el-input-number限制最小值为1。提交成功后不要直接清空表单而是先调接口生成下一张捐赠单号回显给用户方便连续录入。5. 部署实战从打包到服务器上线5.1 前端构建与Nginx配置要点前端开发完成后执行npm run build生成dist目录。这个目录里的index.html、static资源文件我通常会放到Nginx的html目录下。关键一步是配置反向代理解决跨域问题让前端请求/api路径时Nginx自动转发到后端Java服务server { listen 80; server_name donation.example.edu.cn; location / { root /usr/share/nginx/html/dist; index index.html index.htm; 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; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } }try_files这一行特别重要。Vue是SPA单页应用如果用户直接访问/inventory/list再刷新页面Nginx默认会去找物理文件/inventory/list必然404。加上try_files按URI逐一查找找不到就回退到index.html由前端路由接管才对。5.2 SpringBoot多环境配置与打包部署后端我用Maven打包。在SpringBoot的application.yml里配置多环境profile开发环境用本地MySQL生产环境用线上数据库。打生产包时执行mvn clean package -DskipTests生成donation-system-0.0.1.jar。启动命令用nohup java -jar加上--spring.profiles.activeprod指定生产配置日志输出到独立文件nohup java -jar donation-system-0.0.1.jar --spring.profiles.activeprod /logs/donation-system.log 21 这里我觉得有个很值得说的细节MySQL连接串务必加上serverTimezoneAsia/ShanghaiuseUnicodetruecharacterEncodingutf8否则日期字段查出来会差8小时中文写入可能乱码这些都是坑。生产环境还要在application-prod.yml中把logging.level.com.donation.mapper设置为warn避免打印太多SQL日志撑爆磁盘。6. 常见问题与排查技巧实录6.1 登录后页面空白或使用Token失效我遇到过几次这种情况最后发现都是Token存储或请求头拼写问题。前端存储Token用的localStorage.setItem(token, response.data.token)但请求拦截器里取成了localStorage.getItem(Token)大小写没对上。排查这类问题最快的方法是按F12打开Network面板看请求头Authorization字段是否正常拼接了Bearer前缀。另一种常见情况是JWT解析报SignatureException。多半是后端把秘钥写死在过滤器里但生成Token的JwtUtil使用了不同秘钥两边不一致。解决方法是把秘钥统一抽到常量类或配置文件中保证生成和校验读的是同一份配置。6.2 MyBatis缓存导致数据不一致MyBatis默认一级缓存是SqlSession级别的对每个数据库会话有效。但我在用Spring整合MyBatis时踩过一个坑一个请求里连续两次查询同一条捐赠记录第二次没有查数据库直接返回了缓存结果刚好第一次查询后另一个管理员在后台改了数据导致界面上看到的是旧数据。这个问题的根源在于一级缓存生命周期太短每次请求结束就销毁所以通常不会出现严重的数据不一致但如果同一个SqlSession里先查后更新再查却因为缓存了第一次结果导致第二次查询拿到旧值就是实打实的bug。排查技巧是在MyBatis配置里设置localCacheScopeSTATEMENT或者在同会话操作后用sqlSession.clearCache()手动清理。二级缓存我用得比较谨慎只在很少变化的字典表上开启业务数据表一律不开宁可每次查库也不要出现脏读。6.3 MySQL时区异常和唯一键冲突处理上线后最常看到的报错是The server time zone value CST is unrecognized。这是因为MySQL驱动6.x以上对时区要求严格。解决方法是连接串加serverTimezoneAsia/Shanghai或者执行SQL设置时区SET GLOBAL time_zone 8:00。唯一键冲突也是经典的并发问题。比如两个管理员几乎同时提交同一捐赠单号的登记数据库会有一个请求撞上uk_donation_no唯一索引。处理方式不是让用户看到一条MySQL原生异常而是在Service层捕获DuplicateKeyException自动生成新编号重试三次三次都失败才返回友好错误提示。6.4 跨域请求被拦截开发环境前端在localhost:8081后端在localhost:8080必出跨域问题。最简单的方案是后端写一个CORS配置类Configuration public class CorsConfig { Bean public CorsFilter corsFilter() { CorsConfiguration config new CorsConfiguration(); config.addAllowedOriginPattern(*); config.addAllowedHeader(*); config.addAllowedMethod(*); config.setAllowCredentials(true); UrlBasedCorsConfigurationSource source new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration(/**, config); return new CorsFilter(source); } }注意addAllowedOriginPattern(*)不要写成addAllowedOrigin(*)前者在SpringBoot 2.4版本中配合allowCredentials(true)才能正常工作。生产环境如果走Nginx反向代理则不需要后端开启CORS——因为前端和后端同域CORS配置反而多余。7. 我的实操心得与后续扩展建议这个系统我从架构设计到部署上线大概花了一个月业余时间前两周集中开发核心功能后两周做联调、测试和修数据一致性问题。我最有体感的一个教训是主从表结构的审批流程一定要先把“状态机流转图”画在纸上再动手写代码确认每个状态下允许执行什么操作否则后期加需求时改起来非常痛苦。如果你准备拿这个项目做课程设计或毕设我建议在原有六大模块基础上再扩展两个方向。其一是消息通知模块——审批通过或驳回时通过邮件或系统站内信通知申请人和捐赠人这个功能非常实用展示效果也好其二是移动端适配——用Vue的移动端组件库如Vant做一套H5页面让辅导员在手机上就能完成物资审批这在真实校园场景里需求非常强烈。这两个扩展都紧扣“高校管理数字化”的痛点拿出去讲也更有说服力。还有一点我特别想提醒无论谁拿到这套源码请务必先把数据库初始化脚本里的测试账号密码改掉。我把初始管理员密码设为Admin123456只是方便本地演示直接部署到公网环境是绝对不行的——配合定期巡检登录日志才能保证系统真正安全。
返回列表