ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

基于Spring Boot+Vue的校园二手交易系统:从状态机到毕设答辩的关键实践

基于Spring Boot+Vue的校园二手交易系统:从状态机到毕设答辩的关键实践 每年的毕业设计季我都能收到大量类似基于Java的校园网络跳蚤市场系统的设计与实现这样的题目咨询。这个题看得多了多少有点感触它既有电商系统该有的基本骨架又不像秒杀系统那样需要极致的并发方案难度刚好卡在本科生能够独立完成、又足以写满一篇论文的位置上。但正因为做的人太多反而特别容易做成一坨千篇一律的CRUD打开页面是商品列表点进去是详情后台是增删改查答辩PPT里再放两张截图——完事。如果只做到这种程度老师不追问你一个深一点的问题我都觉得奇怪。这篇文章想聊的不是如何把毕设抄完而是怎么把一个平平无奇的跳蚤市场系统从选型、建模到代码实现做出一个能自洽、能解释、能过答辩的完整方案。适合正在选题、正在写代码、或者已经写完准备答辩的同学对照着参考。我会把自己做过这类系统时的真实选择逻辑、踩过的坑、以及那些课本上不会写但实际很关键的细节尽量讲透。1. 为什么把毕设定位在校园跳蚤市场这件事本身有讲究1.1 这个题目表面上考CRUD实际上考的是业务状态流转很多人看到校园二手交易几个字第一反应就是电商平台简化版用户注册登录、商家发布商品、买家下单付款。但这恰恰是最大的理解偏差。校园跳蚤市场和淘宝京东的本质区别在于它几乎是一个单件商品市场——一台二手显示器、一本旧教材、一张用过的健身卡库存天然就是1。这意味着传统电商里复杂的库存扣减逻辑到这里全部退化为一个强约束任何一件商品只能被一个人锁定、一次交易成功。毕设要想拿高分你得把这件事在文档里、答辩里、代码里讲清楚。受益于这种简化你不必写购物车表因为单品不需要购物车把多个SKU汇集到一起你也不必写复杂的SKU规格模型因为旧物没有颜色尺码之分。但这不代表没有难点真正的难点在于状态机的设计商品从展示中到已被下单到已售出订单从待确认到已完成到已取消每一次流转什么时候发生、由谁触发、要不要校验前提条件才是老师最爱追问的地方。我建议把题目理解成两层表层是信息的展示与检索里层是交易关系的状态控制。这两者都要写但里层才是系统设计的真正骨架。1.2 为什么我推荐Spring Boot Vue MySQL而不是SSH或纯JSP毕设技术栈没有绝对标准但选型要在自己能驾驭和老师认可度之间取平衡。这里不是越难越好一个用了分布式缓存却讲不清楚缓存一致性问题的学生反而容易被问倒。我更推荐下面这套组合在本科毕设里属于性价比极高的主流方案技术推荐备选理由后端Spring Boot 2.xSSM、SSH自动配置省掉一堆XML内嵌Tomcat直接启动组文档时模块化好讲持久层MyBatis-PlusJPA / MyBatisLambdaQueryWrapper写条件查询很爽分页插件开箱即用前端Vue 3 Element PlusJSPBootstrap / Thymeleaf前后端分离页面交付质感高组件化让工作量更可控数据库MySQL 8Oracle本地可能没环境8有窗口函数索引也够用社区资料多遇到问题好查选Spring Boot还有一个实际原因答辩演示时你需要在别人的电脑上快速跑起来。Spring Boot的java -jar一体启动体验比起在Tomcat里部署war包要省心太多。MyBatis-Plus则是效率和可解释性兼得它不强迫你写一堆蹩脚的JDBC模板同时保留了SQL可控能力哪怕老师平时用原生的MyBatis也能无障碍看懂你的Mapper接口。前端选Vue不是因为它不可替代而是因为它能把管理后台的表格、表单、弹窗组件化得很快。Element Plus的表格组件配合分页组件后台商品管理模块几乎就是拖组件搭出来的。答辩时讲组件复用前后端分离天然比一张JSP页面传遍所有数据要更有说服力。1.3 工作量的分配不要第五周还在搭框架这个题目的合理周期是8到10周。我的切分经验是前两周做需求分析和数据库建模这两件事没想清楚之前不要写一行业务代码第三到第五周做用户、商品、图片上传、分类这些基础模块第六到第七周死磕订单状态流转、收藏、留言这些交互感较强的功能第八周做前端页面和接口联调最后两周留出来做测试、部署和论文。很多同学容易犯的毛病是流程图画得很漂亮数据库表却只有三四张。其实写文档的时间必须和代码的时间错开不是先干完再补文档而是建模阶段就把论文里的核心E-R图同步输出。一张精确到字段级别的数据库表设计论文里看起来是实打实的工作量答辩时也是你讲解系统结构的最好底稿。2. 需求梳理与角色边界别把简单题做成糊涂账2.1 三主角模型管理员、卖家、买家其实是同一批人校园场景有个很有趣的点卖家就是学生买家也是学生两个人的身份随时互换。我不建议你设计成平台用户和商家两张表而是做一个user表通过角色字段区分普通用户和管理员。普通用户天然拥有两种操作维度作为卖家可以发布闲置商品、编辑自己的商品信息作为买家可以浏览、收藏、下单、评价。管理员则是独立角色负责商品分类维护、用户管理、商品上下架审核、留言和举报处理。这种设计在答辩时很好讲权限控制不是复杂RBAC但每一类接口都能说清楚谁可以访问、谁不能访问。我用的是简单的role字段加后端拦截器校验前端再根据角色隐藏按钮。虽然朴素但闭环是完整的前端隐藏按钮只是体验优化后端拦截器才是安全边界。2.2 核心业务闭环从发布到线下交易完成校园二手交易有一个和其他电商平台不同的关键特征交易过程绝大多数发生在线下。所以平台不应该去设计在线支付而应该把重心放在撮合上。完整的闭环我建议这样串用户在首页或搜索页浏览商品看到感兴趣的宝贝后可以收藏也可以直接发起下单——下单动作本质是表达购买意向。此时系统把商品状态从在售改为已锁定并给卖家生成一个订单。卖家在个人中心看到订单后有两种选择同意交易状态进入待线下交付或者取消交易商品状态回滚为在售。买家在线下拿到商品后点击确认完成订单进入已完成状态双方可以互相评价。如果买家下单后超过约定时间没进一步操作系统是否需要自动释放这个属于可选的亮点功能如果你时间充裕完全可以加一个定时任务来处理这也是答辩时可以展示的加分项。如果愿意再做得细一点可以让买家在下单时填写期望交易时间段和联系方式订单详情页把这个信息高亮展示。实际使用中这一个小字段的体验提升比很多花哨功能都大。2.3 容易弄错的边界订单状态机和商品状态机必须分开最典型的错误是把订单状态和商品状态混为一谈。订单有订单自己的生命周期商品也有自己的展示生命周期二者不是一一对应的。举例来说一个未支付订单超时被取消商品应该从锁定回到在售但订单状态永远是已取消这是一种历史事实。商品状态是当前的实时状态订单状态是历史路径的记录它们必须有各自的枚举定义和独立的流转代码。我在做系统时后台维护了两份状态枚举GoodsStatusEnum0待审核、1在售、2已锁定、3已售出、4已下架和OrderStatusEnum0待确认、1已同意、2已完成、3已取消。商品状态的变化要经过订单状态的驱动但订单状态变化却不总是改变商品状态。比如买家取消订单时商品回滚到在售但当管理员把商品强制下架时已存在的订单可以继续完成交易商品状态则进入已下架。理解了这个关系你的Service层代码才不会到处互相耦合。3. 数据库建模的取舍六张表的设计逻辑3.1 核心表结构是怎么设计的整个系统我最终只维护了六张业务表加两张辅助表。业务表是user用户、category分类、goods商品、goods_order订单、collect收藏、talk留言或feedback举报辅助表是简单的file文件记录和system_log操作日志。别嫌表少关键是每张表的存在都有不能删除的理由。以goods表为例关键字段大概是这些id、user_id发布者、category_id分类、title、description、price用decimal(10,2)存绝不用double存钱、original_price可选展示成色、quality成色等级、images我这里用逗号分隔存储多图地址避免单独建图片子表造成查询麻烦、status、view_count、deleted、create_time、update_time。为什么把多图放在一个字段里因为跳蚤市场商品最多上传9张图而且展示商品详情时一定是一次性查出来的单独建子表没有任何查询收益反而多一次关联。但这种设计要在论文里说明这是根据业务场景做的反规范化取舍。订单表的字段我会额外关注三个order_no业务单号、goods_id、buyer_id、seller_id、price_snapshot成交快照。price_snapshot很关键它记录了下单那一刻的成交价即使卖家事后把价格改了这笔订单依然以快照价格为准这在二手议价场景里尤其合理。3.2 为什么我坚持不建购物车表校园旧物不是标品单品单件不存在把多个商品加购后统一结算的诉求。一个二手显示器和一个旧台灯几乎不可能在同一订单里成交因为它们是两个卖家、两笔线下交付。加了购物车反而会把简单模型搞复杂。那用户想留存商品怎么办用收藏表解决。收藏表是典型的用户-商品多对多关系id、user_id、goods_id、create_time。查询某用户收藏了哪些商品一条联表SQL搞定。这就是为什么要把业务场景想清楚再定表结构——如果照抄电商教程你很容易多画一张购物车表面试官和答辩老师一问下单流程怎么走购物车你自己都会觉得别扭。3.3 索引、外键和软删除的实践建议索引方面我会在以下三个位置建立索引goods(user_id, status)用于个人中心快速查询我发布的商品goods(category_id, status, create_time)用于首页分类浏览和排序goods_order(buyer_id, create_time)和goods_order(seller_id, create_time)分别服务买家订单列表和卖家订单列表。复合索引的顺序要和查询条件的常见组合匹配这一点在论文数据库设计部分写出来是实打实的加分项。物理外键我建议不要建但要在应用层维护逻辑外键关系。原因很现实毕设系统需要频繁操作测试数据物理外键会在删除用户或者清库时引发大量约束报错而你的Service层每一处插入、更新本来就应该做好关联校验。不过要注意答辩时老师如果问为什么没有外键不要回答偷懒要回答为了保证业务可扩展性、降低级联删除风险逻辑外键通过应用层保证数据一致性。deleted字段做逻辑删除也是一样的道理商品和用户数据都有历史追溯价值物理删除会造成订单表和评论表留下悬空引用。MyBatis-Plus里加一个TableLogic注解全局逻辑删除就配好了成本极低。4. 后端实现的关键点鉴权、上传、搜索与交易一致性4.1 JWT登录鉴权拦截器比注解过滤器更直观我在用户模块用的方案是JWT Spring拦截器。用户登录成功后后端生成一个有效期7天的token前端存到localStorage每次请求在Authorization头里带上。拦截器对所有/api/**路径做token校验白名单放行登录、注册、商品列表、商品详情这几个无需登录的接口。拦截器的写法很标准但有几个细节经常做错。第一解析token失败要返回统一的401响应结构而不是直接抛出500第二拦截器里解析出的用户id要放到request.setAttribute(loginUserId, ...)这样Controller层不需要重复解析token第三密码加密一定要用BCrypt不要用MD5或SHA裸加密这一点也是答辩时老师大概率会问的。// 一个建议的拦截器核心逻辑 Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String token request.getHeader(Authorization); if (token null || !token.startsWith(Bearer )) { return writeUnauthorized(response); } try { Claims claims Jwts.parser() .setSigningKey(your-secret-key) .parseClaimsJws(token.substring(7)) .getBody(); request.setAttribute(loginUserId, Integer.valueOf(claims.get(userId).toString())); return true; } catch (Exception e) { return writeUnauthorized(response); } }4.2 图片上传的安全处理不只是存一个文件那么简单商品图片上传是这类系统最容易出安全问题的地方。如果只是把文件保存到服务器并返回一个路径至少要落实这几条文件后缀白名单校验只允许jpg、png、gif、webp不要靠前端传的Content-Type做信任文件大小限制Spring的配置文件里设置max-file-size10MB同时前端也要限制一次最多选9张文件名必须重命名直接用UUID.randomUUID().toString()拼接合法后缀彻底避免用户上传../../a.jsp这类路径穿越或可解析脚本文件存储路径不要放在项目的源码目录里可以放在一个固定的绝对路径/data/upload/下再通过配置映射为虚拟访问路径。// 文件名合法性校验片段 String originalFilename file.getOriginalFilename(); String suffix originalFilename.substring(originalFilename.lastIndexOf(.)).toLowerCase(); if (!Arrays.asList(jpg, jpeg, png, gif, webp).contains(suffix)) { throw new BusinessException(图片格式不支持); } String newFileName UUID.randomUUID().toString().replace(-, ) . suffix;为什么强调这些因为毕业设计的评阅老师里很可能有信息安全方向的老师。他不需要你把安全做到企业级但你只要讲出白名单后缀校验UUID重命名文件大小限制虚拟路径映射这四个点就已经远远超过平均水平的毕设系统了。4.3 多条件搜索MyBatis-Plus的条件构造器与防注入商品搜索是页面数据流最核心的接口。我的搜索支持关键词模糊匹配、分类筛选、价格区间、成色筛选、排序。直接用MyBatis-Plus的LambdaQueryWrapper代码逻辑非常清楚而且天然使用预编译参数不存在SQL注入风险。public PageResultGoodsVO searchGoods(String keyword, Integer categoryId, BigDecimal minPrice, BigDecimal maxPrice, int page, int size) { LambdaQueryWrapperGoods wrapper new LambdaQueryWrapper(); wrapper.eq(Goods::getStatus, 1) .eq(categoryId ! null, Goods::getCategoryId, categoryId) .and(StringUtils.hasText(keyword), w - w.like(Goods::getTitle, keyword) .or().like(Goods::getDescription, keyword)) .ge(minPrice ! null, Goods::getPrice, minPrice) .le(maxPrice ! null, Goods::getPrice, maxPrice) .orderByDesc(Goods::getCreateTime); return goodsMapper.selectPage(new Page(page, size), wrapper); }这里最值得讲的是那一段and(...)嵌套条件。如果没有这个嵌套keyword相关的两个like条件和前面的categoryId条件会被or割裂导致分类筛选失效。MyBatis-Plus很多时候写起来爽但条件组合括号还是要自己拎清楚。数据量在十万条以内这种like查询完全够用论文里也不用刻意吹牛说上ES。4.4 交易流程的唯一库存问题乐观锁防重复下单前面反复强调跳蚤市场的商品库存只有1件。那么并发场景下两个买家同时点击立即下单系统该怎么办这是整个项目最有技术含量的一道题。我的方案是把下单动作拆成预占商品和创建订单两步用一条带状态条件的Update语句做原子性预占。Transactional public Long createOrder(Integer goodsId, Integer buyerId) { // 原子性地把商品从“在售”改为“已锁定” int result goodsMapper.updateStatusIfCurrent(goodsId, GoodsStatusEnum.ON_SALE.getCode(), GoodsStatusEnum.LOCKED.getCode()); if (result 0) { throw new BusinessException(该商品已被其他同学抢先下单了); } Goods goods goodsMapper.selectById(goodsId); GoodsOrder order new GoodsOrder(); order.setOrderNo(generateOrderNo()); // 填充买家、卖家、价格快照等字段... orderMapper.insert(order); return order.getId(); }对应的SQL是UPDATE goods SET status #{newStatus}, update_time now() WHERE id #{goodsId} AND status #{oldStatus}。这条语句在数据库层面保证了同一时间只能有一个事务把状态从在售改成已锁定天然就是乐观锁。订单创建和状态更新都在同一个事务里任何一步失败都会回滚不会出现商品锁了但订单没生成的情况。这里还有一个容易被忽略的细节generateOrderNo()要保证不重复。我采用年月日时分秒 三位随机数 用户id后四位的拼接方式量级上足够应付毕设场景。千万不要用数据库自增id当业务单号暴露给前端那样等于把自己的业务体量暴露给别人。4.5 防止接口被刷的轻量手段既然安全聊到这里顺便提一下爬虫和恶意刷接口的防护。这一类毕设系统没必要上复杂的网关限流但可以在拦截器里做一个最简单的IP访问频率计数。用ConcurrentHashMapString, AtomicInteger记录某个IP在60秒内对某个接口的访问次数超过阈值就返回提示。不用引入Redis代码二十行就能写完答辩时还能体现你对接口防护有认知这就够了。5. 前端与接口联调Vue3 Element Plus的实用做法5.1 页面结构与组件怎么拆前端我采用Vue3 Vite Element Plus Pinia。页面分成两块面向学生的前台和面向管理员的后台。前台页面包括首页分类导航 商品流、搜索列表页、商品详情页、发布页、个人中心我的发布、我的订单、我的收藏、我的留言。后台页面包括仪表盘、商品管理、订单管理、分类管理、用户管理、举报与留言管理。所有页面用Vue Router管理其中个人中心和后台通过路由守卫检查登录状态和角色。组件拆分的原则是一个模块一个组件比如商品卡片GoodsCard.vue在首页、搜索页、列表页、收藏页里都会被复用。只要你把商品卡片做一次后面四个页面就有统一视觉和统一懒加载图片逻辑。这个点在写论文系统实现章节时特别好展开也符合软件工程里的复用思想。5.2 接口返回格式统一约定前后端少吵很多架联调最怕的是每个接口返回结构都不一样有的返回{code:0, data:...}有的返回result: true。我在后端定义了一个统一的ResultT包装类固定为code、message、data三个字段。业务成功code200业务失败用明确的错误码比如405表示库存不足、401表示未登录、403表示无权限。前端Axios封装时只做两件全局事情第一统一读取code不是200就弹错误提示第二遇到401自动清理登录状态并跳转登录页。这样每个页面里几乎不用写重复的错误处理。// axios 响应拦截器统一处理逻辑 service.interceptors.response.use( res { const r res.data if (r.code ! 200) { ElMessage.error(r.message || 操作失败) if (r.code 401) { localStorage.removeItem(token) router.push(/login) } return Promise.reject(r) } return r }, err { ElMessage.error(网络异常请稍后重试) return Promise.reject(err) } )5.3 文件预览与分页加载的体验细节商品图片预览Element Plus的el-image组件自带preview-src-list可以支持点击大图预览记得把图片完整URL拼好。发布图片组件我用的el-upload加list-typepicture-card后端上传成功后把返回的路径push到表单字段里提交商品时一起传给后端。图片加载不要全部高清原图后端返回的图片地址可以拼一个缩略图参数比如/files/xxx.jpg?thumb1在列表页显示缩略图详情页显示大图。毕设系统用户基数小这个优化未必有性能收益但体现了一个开发者对体验的敏感度。分页我建议前台用加载更多按钮或滚动加载后台用el-pagination。首页推荐按发布时间倒序 浏览量倒序的混合排序浏览量高说明商品热门但长期高浏览量的老商品会霸榜。可以加上review_weight这样的隐藏排序分用create_time和view_count做一个简单的热度公式这个细节写进论文里比单纯堆页面好看得多。6. 测试、部署与答辩准备6.1 本地联调和边界用例代码写完只是第一步联调阶段我会建议你把下面的场景至少过一遍场景预期结果容易踩的坑两个账号对同一件商品同时下单只有一方成功另一方提示已锁定不用原子Update会出现两笔订单未登录访问个人中心接口返回401并跳转登录拦截器白名单写错未登录也能访问上传非图片文件被拦截并提示格式不支持校验只信前端传来类型卖家下架商品后买家还能下单吗应禁止提示商品已下架商品状态未同步校验买家取消订单后商品状态回滚为在售可被再次下单状态回滚漏写输入超长标题数据库插入报错但页面没提示后端没有做长度校验斩客价改了之后老订单价格保持快照价格不用price_snapshot会查实时价这里重点说并发测试。不用什么Jmeter最简单的方式是写一个测试接口用CountDownLatch模拟几十个线程同时调用下单接口看是否只有一条成功。这个测试代码可以保留在项目中论文的测试章节里放一张测试结果图分量很足。6.2 打包成单jar部署答辩演示不用慌答辩现场最怕的不是功能缺失而是启动失败。你在自己的笔记本上能跑换到教室的电脑上就起不来那前面的所有准备都白费。我的建议是把前端构建产物直接放进后端静态资源目录最终打成一个jar包。具体操作是前端执行npm run build后把dist目录下的内容复制到后端项目的src/main/resources/static下然后Maven打包成一个Spring Boot可执行jar。启动时运行java -jar campus-market-0.0.1-SNAPSHOT.jarVue路由建议使用hash模式如果不小心用history模式刷新二级页面时会404需要额外加一个把404转发到index.html的ErrorController这个细节很多人不知道。数据库的初始化脚本要单独准备一个init.sql里面包含建库建表和默认管理员账号。不要在答辩现场手动一条条执行SQL直接mysql -uroot -p init.sql一次搞定。如果时间允许还可以写一个application-prod.yml样例文件把数据库连接、文件上传路径、JWT密钥都作为外部配置项换环境只改一个配置文件这也是专业度的体现。6.3 答辩时最容易被问到的十个问题我根据往年经验整理了一份高频问题清单每个都值得提前准备一个两分钟的回应版本为什么用JWT而不用Session可以强调无状态、适合前后端分离的特点但也要承认Session在小规模内网系统里并不差用JWT是考虑到移动端和扩展场景。同一件商品同时被两个人下单你怎么保证不超卖直接讲那条原子Update的SQL再补充事务回滚的细节。密码是怎么保存的BCrypt加盐哈希不要说MD5。图片为什么存在本地服务器而不是OSS先承认本地存储有单点风险再说毕设场景数据量和并发不高本地存储足够体现设计能力如果上OSS也需要讲清楚OSS是什么、流程如何。你的项目有哪些表为什么没有购物车表用前文的单品单件、线下交易逻辑去回答。商品搜索是怎么实现的LIKE模糊查询 组合索引说明为什么这个规模不需要ES。状态机如何保证不会出现已锁定的商品还在列表里被浏览列表查询已在SQL层过滤status1详情页也要校验状态。如果用户A上传了一个超大文件会怎样Spring的multipart限制 前端校验双保险。你的系统能不能改成多商户模式如果能改要改哪些表和接口这个开放性问题的答案是把user拆成用户表和商户表goods增加shop_id字段订单增加商户维度拆分结算提前想清层次。什么是逻辑删除为什么用deleted字段解释保留历史数据、避免级联删除影响订单和收藏。6.4 做完整个项目之后回头看的心得最后想多说两句实在的。做毕设最忌讳的是伪创新——动不动就说自己用了Redis、ES、MQ结果项目里连缓存穿透是什么都答不上来。老师其实有很强的识别能力你做的系统规模有多大、代码是不是你自己写的从你回答问题时的流畅度和细节颗粒度就能判断。踏踏实实把Spring Boot的模块化、MyBatis-Plus的查询条件、状态机的每一条流转关系讲清楚这份扎实感比任何炫技都更有说服力。如果这个系统还有余力我建议往两个方向扩展一是加一个简单的站内私信功能让买家和卖家可以针对商品询价这比留言板更贴近真实交易场景二是给管理员加一个简单的销售数据统计页用MySQL的GROUP BY按日汇总成交订单数画一个折线图展示趋势。这两个功能代码量都不大但能让你的系统在功能和论文图表上都显得更完整答辩时也更容易讲出我根据实际需求做了取舍和扩展这样的底气。
返回列表