多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

校园物品置换平台JavaWeb毕设实战:从需求到部署全流程指南

校园物品置换平台JavaWeb毕设实战:从需求到部署全流程指南 作为一个带过多届计算机专业毕设的过来人我见过太多人拿着校园物品置换网站这个题目光速跑偏。表面看它就是个商品发布浏览的CRUD项目但真做起来里面全是细节——物品属于置换而不是买卖、学生身份认证怎么做、图片上传怎么处理、管理员审核该管到哪一层。这篇内容我就以JavaWeb技术栈为例把整个项目的选题逻辑、架构选型、数据库设计、核心模块落地、部署演示和答辩话术一次性讲透。无论你是自己当毕设做还是打算拿这套思路接私活都能直接照着抄。1. 为什么校园物品置换这个题目年年有人做、年年有人挂——先把需求拆明白再动手1.1 这个题目的真正痛点在哪里很多同学拿到题目第一反应是这不就是个二手交易平台吗于是直接套着闲鱼的思路去做用户注册、发布商品、下单购买、支付结算。结果做到一半发现卡住了——校园场景下的置换根本不是为了盈利而是为了处理闲置物品毕业生离校前的教材、考研资料、健身器材、小家电、自行车大一新生想低价接盘但又不完全是买有时候是拿自己的东西等价换。这就是第一个要拆清楚的需求置换和买卖是两套业务逻辑。买卖的核心是价格、订单、支付置换的核心是物品匹配和双方意愿——你看中我的蓝牙音箱我看中你的显示器两边谈拢了才叫达成。所以这个系统里价值不能只靠金额衡量得有一条双方交换申请、协商、确认的流程。第二个痛点是身份边界。校园平台面对的是本校师生不能像闲鱼那样开放注册必须有一定的校园属性约束比如必须填学号/工号、必须选学校学院、后台做审核。否则一个校外人员混进来发虚假物品平台整个信誉就崩了。第三个痛点是闲置物品的信息展示。二手物品和商品的最大区别是一物一况——成色、划痕、缺不缺配件、有没有维修过。如果只给一个商品描述框根本没法把信息表达清楚。所以图片、成色等级、交接方式当面/校内指定地点都是必需字段。把这三个痛点放到需求文档里功能范围就清楚了用户注册登录含学生认证、物品发布与管理、物品分类浏览与关键词搜索、置换申请与状态流转、收藏与点赞、私信沟通、管理员后台审核。要是想拿高分再加一个置换成功率统计之类的小亮点就行。1.2 角色与核心业务流程梳理这个系统一共三类角色正常人第一版往往只做两类用户和管理员容易漏掉访客——也就是未登录也能浏览物品列表的那种人。我建议三类角色都做访客浏览首页、按分类搜索物品、查看物品详情。登录前不能发物品、不能申请置换。注册用户完善个人信息、发布闲置物品、管理自己发布的物品、对别人的物品发起置换申请、处理他人发来的申请、收藏物品、发起站内私信。管理员审核新注册用户、审核用户发布的物品、管理分类字典、封禁违规用户/删除违规物品、查看平台整体统计数据物品总数、置换成功数、活跃用户数。核心流程其实只有一条用户A发布物品并加入心愿单期望换到什么 → 用户B浏览物品并提交我愿意用某某物品来换的申请 → A在消息中心看到申请并同意/拒绝 → 双方线下见面完成互换 → 在平台上点击确认完成 → 平台记录一次成功置换。至于这两个人会不会约在图书馆地下一层的储物柜旁见面平台管不着毕设也一样不必管。这里有一个非常容易被忽略的设计点置换申请不能只是一句话留言它必须携带愿意用来交换的物品信息。也就是说B提出置换的时候系统应当提供一个输入框让B关联到他自己发布过的物品ID或者至少让他填写我提供XXX物品成色/大概情况……的表格。原因在于如果没有这个携带信息的动作A根本没法判断B的交换条件靠不靠谱双方只能绕到微信去聊平台的价值就没了。1.3 功能模块拆分与用例图之外的做法网上大部分资料会直接甩给你一张用例图但说实话对我来说更实用的是先画一个功能检查表一个模块一个模块地核对有没有遗漏。我在实际带项目的时候通常让学生按四个包去脑补全部功能用户端portal注册/登录/找回密码、个人资料编辑、我发布的物品CRUD、我收藏的物品、收到的置换申请、我发起的置换申请、站内私信、统计我的发布物品总数/被收藏数/成功置换数。物品端item发布物品必填标题、分类、成色、期望置换方向、物品描述、图片、成交位置/校区、联系电话或微信、编辑/下架/删除、浏览列表分页条件筛选分类、成色、校区、发布日期排序、关键词搜索、详情页、收藏、举报。置换端exchange发起置换申请附带提供物品说明、处理收到的申请同意/拒绝/待定、状态机待处理→同意→已互换/拒绝→已取消、置换成功后物品自动标记为已置换。后台管理admin登录、用户管理审核/禁用/解锁、物品管理审核/下架/恢复、分类管理增删改、举报管理、数据看板总注册量、总物品量、总置换量、近7天发布趋势。做完这个表你再去看功能设计就心里有底了——这不叫CRUD这叫每一个按钮背后都有一条业务规则。比如物品被下架/置换成功之后就不能再被申请用户被禁用之后他发布的所有物品要同步隐藏举报处理完成后要能追溯到物品。这些规则在写代码之前如果能一条条列出来后面数据库设计就顺手很多也不会做一版改一版。2. 技术栈选型的取舍与理由——Servlet/JSP够不够要不要直接上Spring Boot MyBatis-Plus2.1 先回答毕设最纠结的问题用JSP还是用Spring Boot我知道你在想什么很多学校的JavaWeb课程还在教JSP Servlet连数据库还是JDBC直连但网上已经铺天盖地全是Spring Boot。这里我直接给结论别纠结如果你的毕设时间只有两个月以内、或者你对Spring Boot只是听说过但没写过多层项目那就老老实实选Spring Boot。原因很简单——Spring Boot的自动配置帮你解决了Tomcat部署问题内嵌Tomcat打成jar包直接跑解决了数据源配置问题application.yml几行搞定不用写xml、解决了依赖版本冲突问题起步依赖统一管理。对毕设来说Spring Boot把大量配置细节藏起来你能把时间花在业务实现上而不是花在为什么我的Servlet注册不上、为什么web.xml又报错这种环境问题上。JSP也不是不能用确实有很多老资料是基于JSP Servlet JDBC的如果你的学校模板恰好是这一套而且你的指导老师明确说要体现JavaWeb基础那用JSP Servlet MyBatis也完全没问题。有些老师觉得Spring Boot对你来说太高级了答辩不问过程一样能过。但一旦走JSP这条路你必须多花两到三周去处理前后端混编、页面改动重启、路径映射这些烦心事。我的立场是技术是为功能服务的项目做得完吃得透比用什么跑更能在答辩场上加分。这里给一个折中方案如果学校课程是JSP/Servlet但你心里想用Spring Boot那就在开题报告里写基于Spring Boot的JavaWeb应用开发实践再在系统架构图里把分层画清楚同时说明自己是在Servlet/JSP基础上引入了Spring Boot的约定优于配置思想。答辩的时候老师一般不会在这个点上死磕反而会觉得你愿意学新东西。2.2 持久层选型JDBC、MyBatis还是MyBatis-Plus持久层是另一个大坑。JDBC直连适合课程实验做完整毕设有点硬核——几十张表的CRUD写起来实在费神。原生MyBatis需要自己写一堆mapper.xml对于只有一两个月写码时间的毕设选手来说也是负担。MyBatis-Plus是最合适的理由有三。第一内置通用Mapper和Service单表CRUD基本不用写SQL几十行代码就能把一个模块的增删改查做掉。拿用户表来说你只需要建一个实体类继承BaseMapperUserselectById、selectList、updateById这些方法全部开箱即用。第二它自带分页插件配合PageUser几行代码就完成列表分页不用手拼LIMIT offset, size。第三这也是你最后能写进简历加分的点——MyBatis-Plus现在是很多中小公司的实际选择做毕设期间学会它的TableLogic逻辑删除、TableField(fill FieldFill.INSERT)自动填充时间戳面试时能直接拿来讲。顺带提一句热词里那个mybatisplus根据java实体类生成创建表的sql语句。MyBatis-Plus真的有一个叫代码生成器的工具AutoGenerator它能根据数据库表反向生成实体类、Mapper、Service、Controller也可以反过来——你先写实体类然后用工具/插件去生成建表SQL。毕设阶段我的建议是先把MySQL表设计好用Navicat建表再用MyBatis-Plus的代码生成器一键生成底层代码。这样生成的实体类注解齐全TableName、TableId(type IdType.AUTO)、TableField启动项目时连驼峰映射都帮你处理好了比手写实体类快一个小时不止。2.3 前端方案为什么用Bootstrap jQuery就够了以及一个加分选项我见过有同学非要在毕设里上Vue3 Element Plus结果前后端分离搞了一个月接口调不通CORS跨域、token过期、代理配置轮番轰炸。毕设页面数量本来就不算多大部分是带管理后台的经典布局Bootstrap 4/5加jQuery足够撑起完整界面。Bootstrap的栅格系统帮你搞定所有响应式布局组件库自带导航栏、卡片、标签页、Tab、模态框、分页表单样式也够看。配合ThymeleafSpring Boot官方推荐的模板引擎做服务端渲染完全不需要单独起一个前端服务。数据交互用jQuery的$.ajax/$.get请求后端接口拿到JSON再渲染表格或卡片就这样。如果你实在想加一点现代感可以考虑只加一个弹层组件比如Layer或sweetalert2用来做图片上传预览、确认弹窗这些交互。把复杂度控制在一个可控的范围内页面照样能用还不会给自己挖坑。另外一个值得考虑的小加分项是用ECharts做一个管理员数据看板——它支持纯前端引入不用npm工程化只需要在后端提供一个统计接口返回JSON比如近7天每日发布物品数、分类占比前端用ECharts画折线图和饼图。这个对毕设的视觉冲击力极大答辩时打开后台首页图表一亮相工作量三个字就摆在那儿了。实现难度其实很低但给人的感觉完全不一样。2.4 项目分层与包结构设计分层写项目不光是老师的要求更重要的是对你自己的代码维护有利。一个典型的Spring Boot项目包结构如下com.school.exchange ├── controller # 控制层接收请求、参数校验、调用service │ ├── admin # 后台管理接口 │ └── portal # 前台用户接口 ├── service # 业务层接口 │ └── impl # 业务实现 ├── mapper # MyBatis-Plus的Mapper接口 ├── entity # 数据库实体类 ├── dto # 前端传参或返回的封装对象比如UserDTO、ItemQueryDTO ├── vo # 视图对象比如ItemDetailVO、AdminStatVO ├── common # 通用返回结果、枚举、异常处理 │ ├── Result.java # 统一返回结构code、msg、data │ └── GlobalExceptionHandler.java ├── config # 配置类拦截器、文件上传路径映射、分页插件 └── utils # 工具类JWT/Redis会话、日期处理、文件存储这里有一个关键习惯Controller里不要写业务逻辑只做参数接收、简单校验、调用Service、返回Result。业务逻辑放在Service里比如用户提交置换申请时必须校验目标物品未下架、未置换成功、自己不能申请自己的物品这种判断就写在Service里而不是散落在Controller里。好处是答辩时老师说如果要从A状态流转到B状态你觉得应该在哪里控制你能有理有据地答出来。统一返回ResultT也很重要。定义code200成功、code500系统错误、code403权限不足、code406业务逻辑冲突。页面端拿到Result判断code再做跳转或弹窗。这比直接返回String或Map规范得多也更好查问题。3. 数据库设计的核心逻辑——把置换而不是买卖这件事设计明白3.1 核心表拆解用户、物品、置换申请三张主表数据库是整个项目的地基。我见过太多人一上来就建一张大表把什么字段都塞进去最后改来改去返工。我的做法是永远从业务实体出发建立清晰的关系宁可多拆一张表也不要在一个表里堆几十个字段。这个项目我最终设计了六张核心表user用户、category分类、item物品、item_image物品图片、exchange置换申请、message站内私信再加上favorite收藏和report举报作为附属表。用户表重点关注身份认证相关的字段。不要只设计username和password还要有student_no学号、college学院、phone、avatar、status0禁用/1正常/2待审核。密码必须加密存储至少用MD5salt或者BCrypt裸存密码是答辩现场最容易被问倒的一个点。status这个字段一定要从第一天就设计进去否则后面再做用户审核就得挪数据加字段麻烦。物品表是核心中的核心。字段设计上除了常规的title、description、category_id、user_id、price_ref参考价注意是参考不是成交价因为在置换场景里这个字段可以填0或者不填、expect_exchange期望换到的物品描述、quality成色全新/几乎全新/轻微使用痕迹/明显使用痕迹、campus校区/交接地点、status0待审核/1展示中/2已被下架/3置换成功、view_count、create_time、update_time。特别说明status这个字段会伴随整个置换流程来回切换状态后面代码里我会用到。所有状态用int存页面显示时通过枚举或字典映射成中文这样数据库层面不是一堆已上架/已下架的字符串便于统计和管理。3.2 置换申请表的状态机设计以及一张容易漏掉的表置换申请表exchange是很多人设计不明白的地方。我建议这样设计exchange ├── id ├── item_id # 被申请的物品 ├── from_user_id # 发起置换的用户 ├── to_user_id # 物品发布者 ├── offer_item_id # 发起方愿意用来交换的物品id可空若未发布则用offer_desc ├── offer_desc # 发起方补充的交换说明我有一台九成新的小米显示器 ├── status # 0待处理 1同意 2拒绝 3已互换 4已取消 ├── create_time └── handle_time # 物品发布者处理时间状态机要定义清楚哪些转换是允许的0→1同意、0→2拒绝、0→4发起方取消、1→3线下确认互换完成、1→4也可以同意后有一方反悔。这两个转换条件必须在Service层写死不能在前端随便改。比如物品已经置换成功item.status3就不允许新的置换申请再创建出来否则逻辑就乱了。容易漏掉的是offer_item_id这个关联。我曾见过一个版本置换申请只存一句话审核时根本不知道对方想拿什么来换还要不停去聊天里翻记录。加上offer_item_id之后A可以直接看到B要换的东西的详情页这是置换平台体验的关键一步数据库一开始就要把这个字段建出来。私信表message建议也提前设计。虽然有些同学想偷懒不做了但答辩时如果老师问如果两个人想聊聊物品细节怎么办你总不能说加微信吧。一张表就搞定id、from_user_id、to_user_id、content、is_read、create_time。列表接口按会话分组——以当前用户为视角查对方按对方ID分组取最新一条即可MySQL的GROUP BY加子查询就能解决。有了这个模块前面需求里的站内私信/消息中心就完整了平台闭环的感觉马上就有了。3.3 用MyBatis-Plus自动填充与逻辑删除省下一堆重复代码数据库字段中有两个是所有表都需要的create_time和update_time。手动在每个insert里写new Date()太蠢了用MyBatis-Plus的自动填充功能解决。在实体类的字段上加注解TableField(fill FieldFill.INSERT) private Date createTime; TableField(fill FieldFill.INSERT_UPDATE) private Date updateTime;然后写一个MetaObjectHandler配置类Component public class MyMetaObjectHandler implements MetaObjectHandler { Override public void insertFill(MetaObject metaObject) { this.strictInsertFill(metaObject, createTime, Date.class, new Date()); this.strictInsertFill(metaObject, updateTime, Date.class, new Date()); } Override public void updateFill(MetaObject metaObject) { this.strictUpdateFill(metaObject, updateTime, Date.class, new Date()); } }这样所有新增和更新操作都自动带时间戳一条多余代码都不用写。物品的下架删除最好也别真的物理删除。如果用户只是把物品撤下来不卖了你还留着一份记录后面想恢复都困难。在item表加一个deleted字段配合TableLogic注解MyBatis-Plus在selectList时自动帮你拼接WHERE deleted0delete操作自动转成update。逻辑删除带来的好处不仅是可恢复更关键的是统计报表的数据不丢比如你要算历史累计发布了多少物品物理删除的数据就再也数不回来了。毕设阶段很少有人想到这一点但如果你在答辩时说出我用逻辑删除保证用户误删后还可以恢复后台统计也不会失真这个小细节很容易让老师眼前一亮。3.4 字段校验与索引建议这里说一个很常见的坑物品表里的price_ref用BigDecimal而不是double因为在财务相关的计算场景下double的精度问题迟早会坑你。虽然是参考价但如果有用户填了199.9double可能算成199.899999。用BigDecimal就避免了这些幺蛾子。索引方面分页查询经常按category_id、status、create_time来筛选所以至少要给这三者建组合索引。我的建议是idx_category_status_time(category_id, status, create_time)这样某个分类下、状态正常、按时间排序的分页查询走索引就快很多。如果选了findByKeyword做模糊匹配给title建一个普通索引就够了全文索引对一个毕设项目来说没必要。exchange表的from_user_id、to_user_id也要建索引因为消息中心要频繁查这两个字段。数据库设计完成后建议用Navicat的模型功能或show create table导出SQL并留档这是写开题报告和答辩PPT时一个很好的素材把ER图贴上去整个系统架构就清晰了。4. 核心模块实现拆解——从物品发布到置换对接的完整链路4.1 用户注册登录与拦截器别让未登录的人随便发物品登录模块看起来简单实际上有3个细节值得好好写密码加密、会话保持、请求拦截。密码加密用Spring Security的BCryptPasswordEncoder。虽然你不需要把整个Spring Security引进来但单独引入spring-security-crypto这个依赖只用来做密码哈希就够了。注册时encode(password)存入数据库登录时matches(rawPassword, encodedPassword)校验。这样数据库里即使被脱库也不会出现明文密码裸奔的局面。会话保持有两个选择传统Session或JWT。如果你做的是前后端不分离的Thymeleaf方案用Session更符合服务端渲染的习惯因为你在模板里可以直接通过${session.user}拿到登录用户。JWT适合前后端分离但你要处理token过期自动跳登录页、跨域携带header等问题对毕设来说有点绕远。我倾向用Session加一个简单的登录拦截器。拦截器实现方案如下Component public class LoginInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { HttpSession session request.getSession(false); if (session ! null session.getAttribute(loginUser) ! null) { return true; } // 如果是Ajax请求返回JSON提示未登录否则重定向到登录页 String requestedWith request.getHeader(X-Requested-With); if (XMLHttpRequest.equals(requestedWith)) { response.setContentType(application/json;charsetUTF-8); response.getWriter().write({\code\:403,\msg\:\请先登录\}); return false; } response.sendRedirect(/login); return false; } }再写一个WebConfig配置拦截白名单Configuration public class WebConfig implements WebMvcConfigurer { Resource private LoginInterceptor loginInterceptor; Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(loginInterceptor) .addPathPatterns(/**) .excludePathPatterns(/, /index, /login, /register, /item/list, /item/detail/**, /css/**, /js/**, /images/**); } }注意两个坑第一个是静态资源路径必须加进排除名单不然登录页的CSS画面全崩第二个是sendRedirect(/login)如果在Ajax请求里会导致前端拿到的是一整个HTML而不是JSON。所以上面那个X-Requested-With判断必须有。踩过这个坑的人都知道现在写任何登录拦截都默认带上这个分支。4.2 物品发布与图片上传数据库存路径不进数据库存文件物品发布页面是学生最重视的页面之一实际上涉及一个比较大的坑图片上传。方案有三条路把图片转成Base64直接存到item_image表的img_url字段。这个方法省事但数据库表膨胀极快而且数据库备份会变得巨大不推荐。上传到本地磁盘数据库只存一个相对路径比如/upload/20250612/xxx.jpg。这是最推荐的做法实现简单、可控性好。传到阿里云OSS数据库存URL。如果使用OSS Key可以避免自己管理磁盘文件但需要有云资源且要配置SDK适合想加分的同学。我推荐第二种。具体做法在application.yml里配置上传路径前缀然后写一个FileController统一处理图片上传RestController RequestMapping(/api/upload) public class FileUploadController { Value(${upload.path}) private String uploadPath; Value(${upload.base-url}) private String baseUrl; PostMapping(/image) public ResultString uploadImage(RequestParam(file) MultipartFile file) { if (file.isEmpty() || file.getSize() 5 * 1024 * 1024) { return Result.error(文件为空或超过5MB); } String originalFilename file.getOriginalFilename(); String ext originalFilename.substring(originalFilename.lastIndexOf(.)); String filename UUID.randomUUID().toString().replace(-, ) ext; // 按日期分目录防止同一目录文件过多 String datePath new SimpleDateFormat(yyyyMMdd).format(new Date()); File dir new File(uploadPath File.separator datePath); if (!dir.exists()) dir.mkdirs(); try { file.transferTo(new File(dir, filename)); return Result.success(baseUrl / datePath / filename); } catch (IOException e) { log.error(上传失败, e); return Result.error(上传失败); } } }然后在WebConfig里把/upload/**路径映射到磁盘目录Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler(/upload/**) .addResourceLocations(file: uploadPath /); }这是很关键的映射如果不配置前端页面访问不到上传的图片全是一堆404。另外要注意一定要做一个文件类型校验至少检查扩展名是否为jpg、png、gif、webp不然用户传个exe上来安全审计这一关就过不去。图片存到一张item_image表的好处是可以支持一个物品对应多张图这样物品详情页可以做轮播图。发布表单里用添加图片按钮动态生成文件输入框一次表单提交把图片一张张传上去后端拿到返回的URL拼接成列表再绑定到ItemImageVO。用jQuery的FormData循环上传就行不太难但视觉上效果很丰富。4.3 物品列表与搜索条件筛选加一个干净的分页物品列表接口应该支持分类筛选、成色筛选、校区筛选、关键词模糊搜索、按最新/最热排序。用MyBatis-Plus的分页插件实现只需要在WebConfig里注册一个PaginationInnerInterceptor然后在Service里这样写public PageItemVO queryItemPage(ItemQueryDTO query, int pageNum, int pageSize) { LambdaQueryWrapperItem wrapper new LambdaQueryWrapper(); wrapper.eq(Item::getStatus, 1) // 只展示已审核上架 .eq(query.getCategoryId() ! null, Item::getCategoryId, query.getCategoryId()) .eq(StringUtils.hasText(query.getQuality()), Item::getQuality, query.getQuality()) .eq(StringUtils.hasText(query.getCampus()), Item::getCampus, query.getCampus()) .like(StringUtils.hasText(query.getKeyword()), Item::getTitle, query.getKeyword()) .orderByDesc(publishTime.equals(query.getSort()) ? create_time : view_count); PageItem page itemMapper.selectPage(new Page(pageNum, pageSize), wrapper); return new Page(page.getTotal(), page.getCurrent(), page.getSize(), page.getRecords().stream().map(item - convert(item)).toList()); }有一个容易踩的坑orderByDesc(String column)虽然传了列名字符串但存在SQL注入风险。更稳妥的做法是用wrapper.orderByDesc(query.isNew() ? Item::getCreateTime : Item::getViewCount)把列名作为属性引用传进去避免拼接字符串。MyBatis-Plus的LambdaQueryWrapper就是这么设计的别为了图省事用字符串。列表页的分页组件用Bootstrap的pagination或一个简单的自定义分页ul首页、上一页、下一页、末页数据从Page对象里取。这里记一个经验分页参数从pageNum和pageSize里传默认pageNum1, pageSize12前端的排序按钮只是重新加载url带上参数别把排序逻辑写在JS里否则刷新就丢了。4.4 置换申请与意向清单为什么需要双向确认前面说了置换不是买卖所以核心动作不是下单付款而是申请→确认→线下互换→标记完成。我在Service里这样设计接口Transactional(rollbackFor Exception.class) public Result? createExchange(ExchangeCreateDTO dto, Long fromUserId) { Item item itemMapper.selectById(dto.getItemId()); if (item null || item.getDeleted() 1) { return Result.error(物品不存在); } if (!item.getStatus().equals(1)) { return Result.error(该物品当前不可置换); } if (item.getUserId().equals(fromUserId)) { return Result.error(不能置换自己发布的物品); } // 同一用户对同一物品只能发起一次待处理申请 Long count exchangeMapper.selectCount(new LambdaQueryWrapperExchange() .eq(Exchange::getItemId, dto.getItemId()) .eq(Exchange::getFromUserId, fromUserId) .in(Exchange::getStatus, 0, 1)); if (count 0) { return Result.error(你已提交过置换申请请勿重复提交); } Exchange exchange new Exchange(); exchange.setItemId(item.getId()); exchange.setFromUserId(fromUserId); exchange.setToUserId(item.getUserId()); exchange.setOfferItemId(dto.getOfferItemId()); exchange.setOfferDesc(dto.getOfferDesc()); exchange.setStatus(0); exchangeMapper.insert(exchange); return Result.success(申请已提交等待对方确认); }这段代码里面那条同一用户对同一物品只能发起一次待处理申请就是典型的业务规则写Service时你不写前端只用一个按钮禁用是挡不住的总有人会绕过前端狂点结果数据库里一大堆重复记录。虽然可以从controller给前端一个if但在Service利用数据库约束来做才是后端程序员正确的做法。处理申请的接口逻辑handleExchange大致是接收exchangeId、status两个参数。如果是同意status1更新exchange.status1同时可以考虑给fromUser生成一条站内私信或站内通知。如果是拒绝status2同样更新状态并留一个handle_time。如果是确认互换完成status3则还要把item.status同时更新为3置换成功。这里需要用到Transactional因为涉及两张表的更新必须保证原子性。如果漏了事务节点一旦中途报错exchange.status变了而item.status没变数据就脏了。另外我强烈建议在exchange状态变为3之后给物品发布者发一条私信内容类似你们的置换已完成感谢使用校园物品置换平台。这个小细节能让系统看起来非常完整答辩时可以直接演示我从发布物品到别人发起申请再到我同意并互换完成全程在站内沟通闭环完整度立刻上去。4.5 个人中心与我的消息不只是几个CRUD页面个人中心是用户在系统中待的时间最长的地方也是面试官容易盯上的地方。我建议做到这些功能左侧菜单我的资料、我发布的物品、我收藏的物品、我收到的置换申请、我发起的置换申请、我的消息。个人资料页可以修改手机号、微信号、头像、学院。头像上传用那套FileController即可。物品管理页以表格列出所有物品状态列显示审核中/展示中/下架/置换成功右侧按钮随状态变化编辑、下架、删除、重新上架。注意逻辑删除之后按钮应该变成已删除不可操作。消息中心这里有一个普遍的经验不要只做一个简单的列表要把未读消息数做出来。比如导航栏右上角一个红色小数字每次加载页面时用ajax请求/message/unreadCount拿到数字渲染。实现很简单Long unread messageMapper.selectCount(new LambdaQueryWrapperMessage() .eq(Message::getToUserId, userId) .eq(Message::getIsRead, 0));但这个小红点给老师看着很专业也在答辩演示时很有系统感。就算在别的系统里做未读红点也同样是通用技能。我强烈建议加上。5. 页面交互与细节打磨——让老师觉得这工作量够了5.1 首页展示导航栏、分类入口、热门物品、最新上架四个模块一个不少首页的布局基本是有套路的照着成熟二手平台来就行但不用做得太复杂。我的建议是顶部导航栏Logo、分类下拉、搜索框、登录/注册按钮未登录或用户头像已登录、发布闲置按钮。分类入口一排5-8个分类图标每个分类点击后跳到带categoryId参数的列表页。分类的icon用Font Awesome或简单Bootstrap的badge风格就行。热门物品按view_count倒序取8个物品卡片显示缩略图、标题、参考价、成色。最新上架按create_time倒序取8个物品卡片。首页的重点是取数性能。你可以让HomeController一次性调多个Service方法然后塞进ModelAndView但是我建议单独建一个HomeService把日程统一到一个方法里返回HomeVO。这样模板只需要${homeVO.hotItems}、${homeVO.newItems}、${homeVO.categoryList}非常干净。如果每个区块去查一次也行但面对为什么返回同一个页面的数据没有归拢的提问时一个HomeService会让你的代码条理性大大加分。Thymeleaf模板里注意图片渲染的坑你的存储路径可能以/upload/20250612/xxx.jpg开头直接拼上th:src${item.coverImage}即可不需要再拼contextPath。但如果你用了server.servlet.context-path比如/exchange那就要在前面拼上${#request.getContextPath()}否则图片路径会变成/exchange/upload/xxx正确而不是/upload/xxx错误。这个坑我踩过一次排查了很久还是建议在配置阶段想清楚到底要不要context-path。对毕设来说我建议干脆不设置context-path直接根路径部署少一个问题就是多一份顺畅。5.2 物品详情页成色标签、图片轮播、收藏按钮、置换申请弹窗详情页是整个项目中细节控最能发挥的地方。左侧图片轮播用Bootstrap的Carousel组件多图数据从ItemImage表中取。右侧信息分成三个层次标题、成色标签、参考价、浏览数、发布时间。物品描述、期望置换方向expect_exchange、校区/交接地点。发布者的头像、昵称、私信TA按钮、发起置换申请按钮。发起置换申请的弹窗里应该有一个表单选择我要提供哪件物品下拉框动态加载当前登录用户已发布的、未置换成功的物品 补充说明textarea。如果没有可选物品也可以提供一个我没有匹配物品但我愿意花钱买这个选项——这里你可以选择不做支付只提示暂不支持现金交易请与发布者私信沟通。这样做既避免了支付系统的复杂度也让业务边界很清楚。收藏按钮用Ajax实现图标切换提示收藏成功。注意收藏表favorite的主键应该是user_id item_id联合唯一索引防止同一用户重复收藏。上报举报按钮可以放在一个下拉小菜单里提交理由、提交后异步提示举报已收到平台将尽快处理。这个功能虽然不起眼但出现在系统里说明你有平台治理的意识答辩时小小一页就能加印象分。5.3 后台管理页表格、状态标签、弹窗确认三层套路后台管理其实是用相同的套路做四件事列表查询、状态切换、弹窗确认、统计看板。admin/item/list展示一张表格物品标题、发布人、分类、状态、发布时间、操作按钮。状态列用Bootstrap的badge颜色区分——灰色待审核、绿色展示中、橙色已下架、深色置换成功。操作按钮根据状态显示通过审核下架删除。注意删除这里要用TableLogic的逻辑删除用户端立即可见但数据还在库里可恢复。弹窗确认统一用layer.js的layer.confirmfunction changeStatus(id, action) { layer.confirm(确定执行该操作吗, function () { $.post(/admin/item/changeStatus, {id: id, action: action}, function (res) { if (res.code 200) { layer.msg(操作成功, {icon: 1}); window.location.reload(); } else { layer.msg(res.msg, {icon: 2}); } }, json); }); }管理员数据看板用ECharts接口大概这样{ code: 200, data: { totalUser: 123, totalItem: 356, totalExchangeSuccess: 27, todayItem: 5, categoryPie: [{name: 书籍教材, value: 120}, {name: 数码电子, value: 80}], sevenDayLine: [{date: 06-06, count: 4}, {date: 06-07, count: 7}] } }后台把这些数据塞进ModelAndView前端模板里用th:inlinejavascript直接把对象传给ECharts初始化。如果你不太熟悉th:inline直接用script里的一个自定义全局变量window.adminData /*[[${adminStatVO}]]*/ null;ECharts初始化时再读取这个对象。这个技巧在服务端渲染项目里很常用我平时代个人也一直这么干。5.4 用Thymeleaf时最容易犯的错对象为null、日期格式化、布尔状态Thymeleaf本身简单但有几个坑必须记住。第一取不到属性时页面直接报错或显示跟没解析似的比如${item.coverImage}如果为nullth:src会出错。处理方式是用${item.coverImage ! null ? item.coverImage : /images/default.jpg}或者在后端VO里就给默认值尽量把前端逻辑减到最少。第二日期格式化实体里是Date页面上不能直接${item.createTime}那样输出的是Thu Jun 12 10:20:00 CST 2025难看极了。用${#temporals.format(item.createTime, yyyy-MM-dd HH:mm)}Java 8时间或者简单点在VO里把日期转成String返回。我倾向于VO转换时把日期统一格式化成字符串因为模板端和接口端都能直接用不会因为时区之类的问题突然冒出一个奇怪结果。第三布尔字段的状态展示。比如item.isDeleted页面里显示是/否比显示true/false好看多了。可以写一个工具方法或者在VO里新增一个deletedText字段放一个已删除/正常模板里直接引用。所有类似的状态→中文都建议在VO层做映射别在模板里堆th:if嵌套。6. 部署到演示环境与答辩现场的避坑清单6.1 IDEA启动阶段三个高频报错及解决思路第一端口被占用。Spring Boot默认8080如果你之前跑过别的服务启动时报Port 8080 was already in use。解决办法改application.yml的server.port或者命令行查占用并kill掉。对毕设来说直接把端口改成8081更省心反正没人会在意你用的哪个端口。第二MySQL驱动或时区问题。Spring Boot 2.7对应MySQL 8.x时连接串如果不加serverTimezone参数会报The server time zone value Öйú±ê׼ʱ¼ä is unrecognized。在application.yml的jdbc url后面加上?serverTimezoneAsia/ShanghaiuseUnicodetruecharacterEncodingutf8。有人用GMT%2B8也能凑合但用Asia/Shanghai更标准。第三文件上传路径权限问题。部署到Linux演示环境时如果upload.path指向一个没有写权限的目录上传图片会直接报FileNotFoundException。提前跑一次chmod -R 777 /你的上传目录或者用mkdir -p建好目录再接上绝对路径别用相对路径相对路径在不同启动目录下会到处乱飘。6.2 演示环境准备数据库初始化与账号数据答辩演示最怕的是现场翻车。我见过一家伙带着笔记本上演讲台结果是直接打开浏览器访问localhost结果一刷新发现MySQL没启动。我的建议是准备三样东西一个init.sql脚本包含建库、建表、初始数据管理员账号、几个测试用户、几十条物品数据、几条置换记录。答辩前在干净环境里重新执行一遍脚本确认无报错。一个预置的演示账号。管理员账号建议是admin/admin123当然真正生产环境要强密码普通用户账号也设计两个一个是物品发布者一个是要发起置换的申请人现场演示时直接用现成账号登录减少输入耗时也降低输错密码的概率。一个README文件写清楚启动步骤安装MySQL、导入脚本、改连接配置、运行mvn spring-boot:run或直接运行jar包。要是老师或答辩评委想在你自己电脑之外的环境跑这个README就能直接派上用场。另外最好准备一个在线演示环境或者录屏因为现场网络不可控哪怕你就是本机演示也建议录一份完整操作视频放在桌面。这不是怂是稳妥。下面这句话我每次带学生都会说答辩演示宁可多录一份屏也不要在现场赌网络和电源。6.3 答辩现场老师最常问的五个问题及如何答得不露怯根据我带毕业设计的经验评委围绕这个题目翻来覆去就是这几个问题你提前准备好回答得当底气完全不一样。问你为什么选择这个课题别回答因为好做。比较好的说法是观察到校园内闲置物品体量大、公益性置换需求真实存在现有商业二手平台在学校私域场景下缺少身份认证和校内交接机制所以做这个平台。问置换和普通买卖有什么区别你的系统怎么体现这是一个很关键的题眼。答传统买卖有明确价格和支付环节我的系统核心流程是物品匹配双方意向协商线下完成平台状态确认不涉及支付模块通过exchange表的双向申请流程和offer_item_id字段让置换请求绑定具体的提供物品这就是置换业务的设计。问如果你的表设计有什么问题比如这里为什么用逻辑删除这是加分题。答物理删除会丢失数据逻辑删除可以在用户误删后恢复同时后台统计仍能算入历史数据用MyBatis-Plus的TableLogic实现查询时自动过滤删除自动转更新。问你如何防止用户发违规物品答在发布端设置敏感词拦截简单实现可以做一个关键词列表过滤后台有物品审核机制用户举报功能作为兜底三个层级同时存在前台校验不能替代后台审核。问如果并发很大你这个系统最先出问题的是什么怎么优化答最先会卡在MySQL的读写或者图片文件访问优化方向是给列表查询加缓存Redis、按时间分表或加缓存、图片接入CDN、会话信息用Redis存储。答到这个层次即使你没有真做缓存老师的印象也会认为你知道边界在哪。6.4 从毕设答辩到简历项目的一条过度路线做完这个项目不要急着扔。有个很现实的现象很多学生的简历上写着基于JavaWeb的校园物品置换平台面试官看到这个题目第一反应绝对是问有没有上线有没有解决真实用户问题。如果你只是拿了个毕设那这个项目含金量就普通。我建议你做完后做三件小事提升它的性价比把项目从jar部署到一个云服务器上哪怕购买一台最便宜的云主机配一个域名挂一个测试版页签让简历上能写已独立部署上线。把管理员后台的数据看板模块单独整理成一篇文章发到某个技术社区顺便梳理成博客一方面是你自己复盘另一方面面试官看到你写的博客或成绩也能拿来说。针对性能或工程化做一次小升级比如把密码从BCrypt换成Spring Security整套认证或者把图片上传换成OSS然后把升级前后对比写成文档。一个毕设项目和可上线的工程之间差的往往不是功能多少而是你是否有意识地做工程化处理比如统一异常处理、统一返回结构、参数校验、日志输出和部署脚本。这些都可以抽时间补齐。写在最后的实际操作建议这个题目看似普通但做完一遍你会发现它把JavaWeb的核心知识——分层架构、状态流转、图片上传、拦截权限、聚合统计、管理后台——全都过了一遍。有一个经验特别想分享千万别在第一天就去写代码先花三天把需求、表结构和状态流转画清楚。我在自己做的过程里第一版就是因为没想清楚置换状态机后面修了整整一周才把逻辑理顺。你踩过的坑可能就是别人的通关梯只要把项目完整跑通一遍哪怕过程中多掉几次头发答辩时站在台上讲解流程的那一刻你会觉得这些投入都值。如果做的时候卡在哪一步回头看看这篇文章里的那几张表和那几段Service逻辑大概率能帮你找到方向。
返回列表