ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

从零搭建校园二手交易平台:Spring Boot+小程序全栈实战与避坑指南

从零搭建校园二手交易平台:Spring Boot+小程序全栈实战与避坑指南

校园二手交易平台这个方向,说实话这几年在课程设计和毕业设计里出现频率非常高,我前前后后也带过好几个类似的项目。标题里的weixin108说白了就是一个工程代号,本质是面向高校师生的C2C二手物品流转系统。这东西做着不难,但它特别能考察你对业务理解、数据库设计、前后端协作的完整把控能力,所以拿来作为设计和实现的练手项目非常合适。

这篇博文我就按我自己带项目时的工作习惯来写,从需求拆解、技术选型、数据库设计,到核心模块的实现思路、实操过程,再把我踩过的坑一并整理出来。不管你是准备拿它做课程设计、毕业设计,还是单纯想练手一个完整的全栈系统,看完应该都能直接上手。

1. 项目概述与需求拆解:先搞清楚要建一个什么样的平台

1.1 校园二手交易的核心场景分析

先说为什么校园场景需要独立的二手交易平台。校园是一个典型的高密度熟人社区,师生每年会产生大量闲置物品:教材、考研资料、自行车、小家电、宿舍神器、体育器材等等,这些物品的流转需求非常旺盛。但通用电商二手平台存在几个问题:物流成本高、跨校区沟通不便、信任成本高,所以校园内部的交易平台一直有真实需求。

这个项目要解决的痛点就三个:信息展示、撮合交易、信任保障。信息展示解决的是“有什么东西可买”,撮合交易解决的是“买卖双方怎么联系、怎么成交”,信任保障解决的是“怎么让师生愿意在这个平台上交易”。我们设计的所有功能,最后都是围绕这三个核心展开的。

1.2 从标题反推功能需求清单

不急着写代码,先把功能清单列清楚。一个标准的校园二手交易平台,按用户角色可以拆成三类需求:

  • 普通用户(学生/教职工):注册登录、浏览商品、搜索筛选、发布闲置、修改/下架商品、收藏、发起购买或留言联系、确认订单、评价
  • 平台管理员:用户管理、商品审核与管理、分类管理、公告管理、订单监管
  • 系统公共能力:统一鉴权、图片存储、消息通知、数据统计

这里有一个容易忽略但很重要的点:商品审核。和通用C2C平台不同,校园平台如果完全不审核,很容易被外校人员或广告号钻空子。所以我在需求阶段就把审核功能定为必选项,而不是可选项。

从项目管理角度,我会建议按“三阶段”推进:第一阶段做完登录、商品CRUD、图片上传,这是骨架;第二阶段做订单流转、收藏、留言,这是业务闭环;第三阶段做后台审核、数据统计、权限完善,这是运营能力。千万不要第一周就想着把全部功能做完,很容易卡在某一个细节上出不来。

2. 技术选型与架构设计:经典组合为什么最稳

2.1 技术栈选择的底层逻辑

校园二手交易平台这种规模的项目,技术选型的原则是“成熟、可控、好解释”。我不太建议在这类项目里追求太冷门的框架,也不建议过度设计分布式架构,因为项目答辩或实际维护时需要把每个技术点讲清楚,选一个你自己真正能驾驭的栈比选一个看起来很唬人的栈重要得多。

这里我给出一个经过大量验证的组合方案:

后端:Java + Spring Boot + MyBatis-Plus + MySQL + Redis

Spring Boot 是目前Java生态里搭建Web应用最主流的方式,内置Tomcat,省去大量XML配置;MyBatis-Plus可以大幅减少单表CRUD代码量,同时保留手写SQL的能力;MySQL负责核心数据存储;Redis用于缓存热点数据、实现 token 会话管理。

前端:小程序端 + Web管理端

考虑到校园师生使用手机的习惯,面向普通用户的端用小程序实现,随开随用,不需要安装App;面向管理员的端用Web方式实现,用Vue 3 + Element Plus,因为后台功能天然就是表格和表单密集型的,Element Plus能极大提升开发效率。

这套技术栈最大的优势是学习资料极其丰富,遇到任何问题基本都能搜到解决方案。对于课程设计或毕业设计,你不需要验证某个新框架是否好用,你需要的是确定性。

2.2 数据库设计:五张核心表怎么定义

数据库设计是这类项目最关键的一环,表结构一旦定得不合理,后期改起来非常痛苦。我直接给出一个我在实际项目中验证过的核心表结构思路。

用户表(user):id、open_id或username、nickname、avatar、phone、school_id、role(普通用户/管理员)、status、create_time。这里需要注意,学号或工号这类信息不要直接做主键,用自增id或雪花id做逻辑主键,学号作为普通字段且加唯一索引。

商品表(product):id、user_id、title、description、category_id、price、original_price、images(逗号分隔图片URL)、status(0草稿、1审核中、2在售、3已下架、4已售出)、view_count、create_time、update_time。price字段建议用int类型以“分”为单位存储,避免浮点数精度问题,这是很多人容易踩的坑。

分类表(category):id、name、sort、create_time。像教材书籍、数码电器、生活用品、运动器材、其他,分类不需要建太深,一级分类就够了。

订单表(order):id、order_no、product_id、seller_id、buyer_id、amount、status(0待付款/1待确认/2已完成/3已取消)、create_time、update_time。订单号建议用时间戳加随机数生成,不要用自增id直接对外暴露。

图片表(image):如果把图片直接塞在商品表里,字段会非常臃肿。建议单独建一个图片表,或者按“逗号分隔URL”的方式存储在商品表的一个字段里。对于这种小型项目,逗号分隔是完全够用的,不用过度设计。

再补一个收藏表(favorite)和留言表(message),都做成简单的关联表即可。收藏表核心字段是user_id和product_id,留言表核心字段是product_id、from_user_id、content、create_time。五张核心表外加两张辅助表,整个系统的数据底座就成型了。

3. 核心模块的设计与实现:从登录到交易闭环

3.1 用户认证与权限设计:小程序登录为什么不能用传统账密

校园二手平台面向学生端用的是小程序,那就必须围绕小程序的登录机制来设计。小程序登录的核心思路是:用户在小程序端点击授权,前端拿到code,通过后端调用微信的接口换取open_id,再用open_id去查用户表,查不到就自动注册一个账号。

这个流程有几个要注意的细节。第一,不要信任前端传上来的用户昵称和头像,这些信息是可以伪造的,必须以接口返回的open_id为准。第二,登录成功后生成一个自定义token返回给前端,后续请求在请求头里带这个token,后端通过拦截器统一校验。用token而不是直接用open_id,是为了给token设定过期时间,也方便在管理员封禁用户时让token立即失效。

管理端的认证就简单很多,直接用账号密码登录。密码不能明文存储,至少要用BCrypt做哈希加密。数据库里给role加一个字段区分普通用户和管理员,但要注意,管理员账号不应该通过普通注册入口创建,应该在初始化脚本里直接插入,避免出现“自己注册一个账号然后把role改成管理员”这种安全漏洞。

3.2 商品发布与状态机设计:审核流程是平台安全的生命线

商品发布是整个平台业务的核心入口。用户在小程序端选择分类、填写标题描述、上传图片、设置价格,提交后商品状态从草稿变成待审核。

这里我强烈建议在商品表里加一个status字段管理商品生命周期状态机。状态流转如下:草稿 -> 待审核 -> 在售 -> 已下架/已售出。用户在“我的发布”里能对待审核商品执行撤回,管理员在后台能对待审核商品执行通过或驳回。这个状态机一旦设计清楚,后面代码写起来就是几个if/else的事,但如果你一开始不清不楚,很容易出现“商品明明下架了还能被购买”这种逻辑漏洞。

图片上传模块也值得说一下。小程序端上传图片前,先调用后端的预签名接口或直接走后端转发,将图片传到云存储或服务器本地目录。我建议做一个统一文件上传接口,返回URL地址,商品发布时存的是URL字符串。不要在前端自己拼本地路径,因为小程序端的临时文件随时可能过期,一定要先传后端再存链接。

商品浏览量用Redis来加,key设计为product:view:商品id,用户每次进入详情页就执行一次自增操作,然后定期把数据同步回MySQL。之所以不用MySQL直接加,是因为浏览量属于高频写操作,直接怼数据库会增加压力。一个小功能也能体现你对性能的思考,这在答辩环节是加分项。

3.3 订单交易与状态流转:交易不是一手交钱一手交货那么简单

校园二手交易平台的订单设计和电商订单有一个明显区别:校园交易绝大多数是线下见面交易,商品可能是一本书、一个台灯、一辆二手自行车,基本不需要物流配送,所以订单流程应该是“买家下单 -> 卖家确认 -> 线下交易完成 -> 双方确认”。

具体状态:买家发起购买请求后生成订单,状态为待确认;卖家在一定时间内确认订单,状态变为待交易;双方在线下完成商品交接后,由买家在平台上点击确认收货,或者卖家点击确认完成,状态变为已完成。任何一方在某个环节都可以取消订单,取消后商品状态恢复为在售。

这里有一个业务设计的难点:是否引入在线支付?以我的经验,这类课程设计和毕业设计项目一般不要做真实在线支付,第一没有支付牌照,第二涉及资金监管和退款流程,复杂度会翻好几倍。我们的做法是把支付留作可选的模拟环节,订单金额只做记录,不做真实扣款。这在答辩时一定要说明白,讲清楚你是用模拟支付的方式避开了资质问题,同时完整保留了交易链路。

其次则是消息留言模块。和公开竞价型平台不同,小规模校园平台更适合用留言+联系方式的方式。买家在商品详情页留言“我想买,请问还在吗”,卖家在“我的消息”里回复,双方也可以通过预留的手机号沟通。为了减少垃圾信息,建议对留言和联系方式做敏感词过滤,这个可以在后端统一做一层过滤拦截。

4. 实操记录:我从零搭建这个项目的完整过程

4.1 工程初始化与公共能力封装

我实际操作时一般先把后端工程搭起来,项目结构用标准的Maven多目录结构:controller、service、mapper、entity、common、config。common包里放统一返回结果类Result、全局异常处理器GlobalExceptionHandler、JWT工具类或Token工具类、拦截器配置。

统一返回结果很关键。所有接口返回固定格式:code(200代表成功)、message、data。不要有的接口直接返回HashMap,有的返回String,否则前端联调时每天都在找问题。

举个例子,拦截器代码的核心逻辑这样写:

@Component public class AuthInterceptor implements HandlerInterceptor { @Autowired private RedisTemplate<String, String> redisTemplate; @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 放行登录接口 if (request.getRequestURI().contains("/api/user/login")) { return true; } String token = request.getHeader("Authorization"); if (token == null || token.isEmpty()) { throw new BusinessException(401, "未登录"); } String userId = redisTemplate.opsForValue().get("token:" + token); if (userId == null) { throw new BusinessException(401, "登录已过期"); } // 把userId放入request上下文,后续接口直接用 request.setAttribute("userId", userId); return true; } }

这里用Redis存token和用户id的映射关系,就是为了实现服务端主动让token失效的能力。如果用无状态的JWT,用户被封禁后Token在有效期内仍然可用,反而不好处理。

前端部分我一般用微信官方的小程序开发工具创建项目,页面结构按底部导航拆分:首页、分类、发布、消息、我的。首页放搜索框、轮播图、分类入口、商品瀑布流列表;发布页是表单页;消息页是留言列表;我的页面放个人信息、我发布的、我收藏的、我买到的。

4.2 关键接口实现示例:商品列表的分页与条件筛选

商品列表是首页最核心的接口。需要考虑的参数有:页码page、每页数量size、关键词keyword、分类categoryId、价格排序priceSort、最新排序timeSort。接口层面用条件构造器让MyBatis-Plus帮忙生成动态SQL。

@Override public Page<ProductVO> queryProductPage(ProductQuery query) { LambdaQueryWrapper<Product> wrapper = new LambdaQueryWrapper<>(); // 只查审核通过的 wrapper.eq(Product::getStatus, 2); // 按关键词模糊匹配标题和描述 if (StringUtils.isNotBlank(query.getKeyword())) { wrapper.and(w -> w.like(Product::getTitle, query.getKeyword()) .or().like(Product::getDescription, query.getKeyword())); } // 按分类过滤 if (query.getCategoryId() != null) { wrapper.eq(Product::getCategoryId, query.getCategoryId()); } // 排序:默认按创建时间倒序 wrapper.orderByDesc(Product::getCreateTime); Page<Product> page = productMapper.selectPage( new Page<>(query.getPage(), query.getSize()), wrapper); // 组装VO,把卖家昵称、图片列表等字段填充好 Page<ProductVO> result = new Page<>(); BeanUtils.copyProperties(page, result, "records"); result.setRecords(convertToVO(page.getRecords())); return result; }

这里有一个工程细节:为什么接口层不建议直接返回数据库实体?因为用户在前端看到的“商品图列表”在数据库里是逗号分隔的字符串,直接返回字符串还需要前端额外处理。在Service层把它拆成数组,返回一个更友好的VO对象,这才是合理的前后端交互模式。

商品详情的访问量统计,在Controller层加一段逻辑:

@GetMapping("/detail/{id}") public Result<ProductDetailVO> detail(@PathVariable("id") Long id) { String viewKey = "product:view:" + id; // Redis 自增 redisTemplate.opsForValue().increment(viewKey); Product product = productService.getById(id); // 从Redis读取当前浏览量 Object viewCount = redisTemplate.opsForValue().get(viewKey); ProductDetailVO vo = convertToDetailVO(product); vo.setViewCount(viewCount == null ? product.getViewCount() : Integer.parseInt(viewCount.toString())); return Result.success(vo); }

你可能想问,浏览量累计到什么时候写回MySQL?最简单的方式是每次详情页读取的时候都先从Redis读,如果Redis里没有,就初始化商品的原始浏览量,然后每隔一段时间通过定时任务统一回写一遍数据库。对于这个项目,不需要引入消息队列,一个简单的定时任务就够用了。

4.3 管理端的核心实现:审核流和统计面板

管理端我采用Vue + Element Plus。页面规划是:登录页、仪表盘(统计卡片+图表)、商品管理(表格+审核弹窗)、用户管理、分类管理、留言管理。

商品审核实现思路很简单,管理端调用后端审核接口,传入商品id和审核结果(通过/驳回),后端修改商品状态。但这里我建议增加一个审核备注字段,驳回时管理员可以填写原因,用户在小程序端会看到“你的商品被驳回:图片不清晰”。这样能减少很多无意义的重复提交。

按时间维度统计是很多课程设计可能会漏掉的功能,但它的开发成本并不高。用MySQL的DATE_FORMAT函数按天分组统计新增商品数和新增用户数,再用ECharts在前端画折线图。这个功能做出来,整个平台的运营状态一目了然,演示效果也特别好。

5. 常见问题排查与避坑技巧:我积攒下来的经验

5.1 高频问题排查表

我把开发过程中最常遇到的几类问题整理成了表格,按这个问题出现的频率和解决成本排序:

问题现象根本原因解决办法
微信小程序里图片上传后访问404临时文件目录没有持久化,服务器重启后文件丢失统一上传到指定目录,配置静态资源映射,不要依赖临时目录
商品列表分页数据重复排序字段不是唯一的,比如只用create_time排序加上没有id排序排序条件加上id字段,保证结果有序且唯一
用户登录后请求接口一直提示未登录前端没有把token放到请求头,或者token过期没有做刷新在小程序请求封装里统一加Authorization头,服务端校验失败时返回特定码,前端自动跳转登录
中文乱码数据库连接参数没有加characterEncoding,或前后端编码不一致数据库连接串加useUnicode=true&characterEncoding=utf8,接口统一返回UTF-8
并发下单导致超卖同一商品同时被多个买家下单,没有锁限制下单时对商品状态做乐观锁校验,update语句带status条件,更新行数为0则提示已售出
管理端跨域请求失败前后端分离部署,没有配置跨域后端配置CorsFilter,允许指定域名或所有来源(开发环境)
Redis存储的中文乱码默认序列化器是JDK序列化,读出来是乱码统一配置StringRedisSerializer做key和value的序列化

这里面我最想重点强调超卖问题。校园二手平台上虽然并发量不会特别大,但抢热门商品(比如便宜的电动车、热门教材)时完全可能同时有几个买家下单。正确的逻辑是:下单前先执行一条带条件限制的更新语句,比如UPDATE product SET status=4 WHERE id=? AND status=2,如果返回的影响行数是1,说明抢到了,否则说明商品状态已经变化,提示用户手慢了。

5.2 几条独家建议:从开发到答辩全流程避坑

第一,数据库设计阶段一定要画出E-R图。这个不只是为了交付文档,更是让你自己把实体关系理清楚的过程。我见过太多项目前期直接建表,写到后面发现用户表和商品表缺联系字段,又回头改表。先画图再建表,能省后面一半的改代码时间。

第二,不要把项目推到最后一两周再开始写。校园二手交易平台听起来简单,但细算下来,前后端加起来有几十个接口,每个接口从参数校验、业务逻辑、返回组装到联调,至少需要一小时。如果你每天只有两三个小时可用,满打满算也需要两三周才能做出一个完整可演示的版本。我建议按照“功能模块清单”逐项打勾推进,每完成一个模块就做一次本地自测。

第三,演示和答辩前,至少准备几组真实感强的测试数据。比如二手教材、自行车、台灯的完整商品信息,找一个现成账号登录进去,截图保存关键页面。现场演示最怕的是临时连不上数据库、网络超时,所以我建议提前录好一份演示视频作为备用方案,同时确保本地环境可以一键启停。

第四,在代码注释上多花五分钟。不是要求你写论文式注释,而是在复杂业务逻辑处留两三行说明。比如“这里用乐观锁防止超卖”这种注释,等过两个星期自己回来看代码都能救你一命,更不用说答辩时老师会翻代码看。

写在最后的几句大实话

做过几个类似项目之后,我的体会是:校园二手交易平台这类系统真正的难点从来不在某个单独的技术点上,而在于把一条完整的业务链路串起来。从用户登录到商品发布,从审核到下单,从留言到评价,每一个环节都涉及角色、状态、数据的一致性。你能把这个链路理清楚并且稳定跑起来,你的系统设计和工程实践能力就实实在在过关了。

最后再分享一个小技巧:把项目中某个你觉得做得最到位的模块(比如商品状态的机设计、Redis缓存浏览量的方案)记在项目笔记里,面试或者答辩的时候拿出来讲。一个讲得清楚的细节,比十个含糊其辞的功能点更有说服力。希望这篇记录能帮你把项目做得顺利一些。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进