☰
校园二手闲置租售系统实战:从需求拆解到Spring Boot+Vue落地
2026/10/2 8:57:05 网站建设 项目流程

大三那年宿舍堆满了考研资料、旧教材和小电器,拍照发了几条二手群消息,三天没人问价,到最后全被收废品的大爷十块钱拉走。就是那一刻,我突然意识到校园里的闲置流转,根本不是一个微信群能解决的。后来我拉上两个同学花了三个月,从零做了一套校园二手闲置物品租售系统,一边开发一边在校内试运营,最后真跑通了早期用户和订单闭环。这篇文章就把这套系统的完整思路、设计取舍、落地实现和踩过的坑,原原本本写出来,给想在校内做类似项目的团队一个可复用的参考。

1. 项目定位与需求拆解

1.1 校园场景为什么值得做二手租售

校园和普通社区二手市场的最大区别,在于人群极其集中、物品共性极强、信任半径天然存在。一个四千人的学院,至少上千人拥有同款教材、同款宿舍小风扇、同款考研资料,但信息完全散落在不同的年级群、宿舍群和表白墙评论区里,供需匹配效率低到可以忽略。与此同时,学生的流动性是固定的,每年六七月毕业季集中释放大量闲置,每年九十月新生入学又有集中购买需求,这种周期性爆发是校园场景独有的。

单纯的二手出售,其实只覆盖了一半需求。实验室的仪器、体测用的羽毛球拍、拍毕业设计用到的一次性单反、只住一个月的短租台灯,这些物品的使用时长很短,买下来完全不划算。所以我一开始就打算做成“出售 + 租赁”双模式:出售解决所有权转移,租赁解决短期使用权需求。实测下来,租赁虽然业务逻辑复杂得多,但正是这个功能让系统在同类项目里有了差异化价值。

1.2 “出售”和“租赁”本质上是两种业务

很多人做二手系统,想当然地把租赁理解成“加一个日租金字段”,这是最大的误区。出售是瞬时交易:下单、付款、交付、确认,流程成型后基本没有后续。租赁是持续型交易,从起租日到归还日之间有完整的时间跨度,中间涉及押金冻结、按天计费、逾期判定、损坏定责、押金退还,每一环都是信用风险点。

系统里商品就分三种类型:仅出售、仅出租、可租可售。前两种实现起来相对简单,第三种最麻烦。商品详情页要同时展示售价、日租价、周租价、押金和最长租期,买家下单前必须选择交易方式。订单表里也要有一个trade_type字段,销售订单和租赁订单走完全不同的状态流转逻辑。如果前期设计时没有把这两个领域模型彻底分开,后面要么互相污染,要么代码里全是 if-else 分叉,维护成本极高。

产品原型阶段我花了整整一周梳理各种边界情况,最后形成一个核心原则:出售看库存,租赁看时间。两种模式共用商品库,但订单、计价、库存扣减和结算逻辑完全分离。

2. 技术选型与整体架构设计

2.1 为什么选 Spring Boot + Vue 前后端分离

技术栈选择上,我们对比过几条路线:SSM + JSP、Django 模板渲染、Spring Boot + Vue 前后端分离。最终选的是 Spring Boot + Vue 3 + Element Plus,原因很简单:团队里三个人对 Java 后端最熟,Vue 生态对校园项目的开发效率最高,前后端分离之后,小程序端或者移动端 H5 后续可以直接复用同一套后端接口。

后端版本用的 Spring Boot 2.7,搭配 MyBatis-Plus 做数据访问,MyBatis-Plus 对单表 CRUD 的封装度极高,写商品、订单这类基础接口基本不用手写 SQL。数据库用 MySQL 8.0,缓存用 Redis。登录体系没上 Spring Security 全家桶,而是用 JWT + 拦截器,校园项目没必要把框架堆得太重,轻量、能维护才是第一位。

前端初期走了些弯路,一开始想用 Vue 2 + Element UI,后来发现 Element Plus 对 Vue 3 的兼容性明显更好,配套的中后台模板也更成熟,项目做到一半整体升级过一次,建议后来者直接从 Vue 3 起步,不要被旧的教程带偏。

2.2 模块划分与核心数据链路

系统拆成五个核心模块:用户认证、商品中心、订单中心、信用评价、管理后台。用户认证负责注册登录、实名认证和角色权限;商品中心处理发布、审核、上下架、分类检索;订单中心是最复杂的模块,并行处理销售订单、租赁订单、押金冻结、租金计算和自动确认超时;信用评价做双方互评、投诉记录和用户信用分;管理后台则处理商品审核、用户管理和举报仲裁。

数据流向大致是:前端页面 → Controller → Service → Mapper → MySQL,热点商品详情和类目列表查完落在 Redis,下单时先通过 Redis 预扣库存,再异步落订单库。这个链路看着简单,但每一步都有细节。比如图片上传,一开始想直接传 Base64 字符串,后来发现一张 2MB 的照片转成 Base64 之后接近 2.7MB,请求体太臃肿,最后改成 MultipartFile 直传后用 Nginx 做静态映射,数据库里只存访问 URL。

2.3 模拟支付模块的取舍

学生团队接真实微信支付或支付宝,需要营业执照和商户资质,审核周期长,校园演示项目根本等不起。我们最终做了两套方案:一套是支付宝沙箱环境,供正式演示时走完整支付链路;另一套是系统内置的“虚拟余额 + 二维码线下转账”方案。买家下单后生成一个假订单状态“待付款”,页面展示收款码,付款后填写汇款单号,卖家确认到账后系统再流转到下一步。这套方案绕开了支付牌照问题,又保留了订单状态的完整性,实际运营中校园用户也更习惯扫码付款、当面自提的信任交易方式。

3. 数据库设计与表结构拆解

3.1 商品表:租赁与出售字段怎么共存

商品表是整个系统的地基。我的建议是,不要为了省事把所有商品都塞成一张宽表,但校园数据结构相对简单,用一张表加可空字段的方式反而最直观。核心 SQL 结构如下:

CREATE TABLE `product` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `user_id` bigint(20) NOT NULL COMMENT '发布者ID', `title` varchar(100) NOT NULL COMMENT '商品标题', `category` varchar(30) DEFAULT NULL COMMENT '类目,如教材/数码/生活用品', `description` text COMMENT '商品描述', `type` tinyint(1) NOT NULL DEFAULT '1' COMMENT '1=仅出售 2=仅出租 3=可租可售', `price` decimal(10,2) DEFAULT NULL COMMENT '售价,出售类必填', `daily_price` decimal(10,2) DEFAULT NULL COMMENT '日租价', `weekly_price` decimal(10,2) DEFAULT NULL COMMENT '周租价,可选', `deposit` decimal(10,2) DEFAULT NULL COMMENT '押金金额', `max_rent_days` int(11) DEFAULT NULL COMMENT '最长租期,单位为天', `stock` int(11) NOT NULL DEFAULT '1' COMMENT '库存数量', `status` tinyint(1) NOT NULL DEFAULT '0' COMMENT '0=待审核 1=在售 2=已下架 3=交易中 4=已售出', `view_count` int(11) NOT NULL DEFAULT '0' COMMENT '浏览量', `favorite_count` int(11) NOT NULL DEFAULT '0' COMMENT '收藏数', `created_at` datetime DEFAULT CURRENT_TIMESTAMP, `updated_at` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`), KEY `idx_user_id` (`user_id`), KEY `idx_category_status` (`category`, `status`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

这里有几个反直觉的设计要重点说明。第一,stock没有设置成默认 1,而是允许在二手场景下出现“1”。不要小看这个字段,校园里经常有人一次性出二十本相同教材,或者代卖整个宿舍的闲置,有库存字段就可以支持这种批量出售场景,也方便后面做并发扣减。第二,status里把“交易中”单独拉出来,是因为租赁订单生效期间商品必须被锁定,不能被另一个人下第二单,这个状态是租赁模式的命脉。

3.2 订单表与状态机设计

订单表比商品表更考验设计功力。我拆了两张表:销售订单表order_sale和租赁订单表order_rental。一开始想过合成一张,试了三天之后果断分开,理由是租赁订单比销售订单多一堆时间字段和押金字段,混在一起,查询时加条件判断和空值校验的痛苦远超建表的成本。

销售订单状态机:待付款 → 待发货 → 待收货 → 已完成,中间穿插已取消和退款申请。租赁订单状态机:待付款 → 待取货 → 使用中 → 待归还 → 已归还(待退押金)→ 已完成,中间有逾期标记、纠纷中、已取消。order_rental表的关键字段是borrow_start_date、borrow_end_date、actual_return_date,类型必须用 DATE 而不是 DATETIME。租赁按天计费,用 DATETIME 会带出几号几点起租的歧义。比如一个台灯租三天,从 6 月 1 日到 6 月 3 日,DATE 类型天然表达“3 号晚上归还即可”,用时间戳反而容易在时区和跨天上翻车。

3.3 租赁费用的计算规则

租赁计费是业务最核心的规则,必须要有一套明确的公式,否则客服能被打爆电话。我们的规则定得很直接:

  • 租金 = 日租金 × 天数,天数 = 归还日期 - 开始日期 + 1,不足一天的按一天算。
  • 周租优惠:如果天数大于等于 5,按“每周价格”折算,但需取整到周,剩下不足七天的按日租金补足。例如周租 20 元、日租 5 元,租 10 天就是 20 + 5 × 3 = 35 元。
  • 押金在下单时冻结,全额退还的前提是商品外观无损坏、功能正常、按时归还。损坏定责需要双方上传照片凭证,管理员介入判断。

这套规则看似简单,但真正运行之后,逾期问题才是最头疼的。如果买家到归还日没有归还,系统先自动打上“逾期中”标记,按日租金的 1.5 倍累计逾期费,同时冻结信用分和后续下单权限。逾期超过 7 天,卖家可发起“强制结算”,从押金里扣除订单总额和逾期费,剩余部分退还买家,商品标记为“遗失”。这个判定逻辑是运营一个月后根据真实纠纷总结出来的,一开始规则定得太松,逾期半个月都没有任何系统动作,形同虚设。

4. 核心功能模块落地实录

4.1 发布链路:从图片上传到敏感词过滤

商品发布是一个表面简单、细节极多的入口。前端的表单有标题、类目、描述、图片、价格、交易方式、押金、租期等字段,后端拿到数据以后要做三重校验。第一重是字段必填和格式:价格只能保留两位小数,押金不能超过售价的 1.5 倍,租期只能在 1 到 120 天之间。第二重是敏感词过滤:标题和描述要走一遍基于 DFA 算法的敏感词库,校园场景尤其要拦截代写论文、发票代开之类的违规信息。第三重是图片规范:单张图片不能超过 5MB,支持 jpg、png、webp,后端用 Thumbnailator 压缩成最大宽度 1200px 的版本再存储,避免详情页加载太慢。

发布之后商品默认进入“待审核”状态,管理员后台上架或者驳回。为什么要有审核环节?因为开放注册的 C2C 平台,一旦出现违规商品被投诉,责任会落到运营方头上,审核虽然慢一点,但能拦住大多数风险。后来上线之后发现自动审核通过率有 85% 左右,真正需要人工处理的也就教材盗印、电器三无产品这些。

4.2 下单:并发扣减与库存一致性

下单逻辑是整个系统并发压力最大的环节。同一个商品可能同时被多个人点击“立即购买”或者“立即租用”,如果没有并发控制,库存就超卖了。一开始我用的是最朴素的实现:先 select stock,判断 stock > 0,再 update stock = stock - 1。上线第一天就在一门最热门的考研资料上暴露了问题,三个人同时下单,三个人都看到库存剩 1,结果全都跳转到支付页。

后来改成 MyBatis-Plus 的乐观锁:

UPDATE product SET stock = stock - 1, version = version + 1 WHERE id = #{productId} AND stock > 0

执行 update 返回影响行数为 1 才继续创建订单,否则直接提示商品已售罄。这条 SQL 在校园项目的并发量级下足够稳定,Redis 预扣库存反而因为增加了一个分布式事务环节,导致代码复杂度和故障点成倍上升,后来直接去掉了。记住一个原则:并发方案不要追求最先进的,要追求最符合自身量级的,几百人同时在线的系统,一条带条件更新的 SQL 就够顶很久。

4.3 租赁订单时间轴与自动任务

租赁订单从付款成功那一刻起,就开始和时间赛跑了。起租当天生成“待取货”状态,买家和卖家约定在校内自提点见面,双方确认后点击“我已取货”,订单进入“使用中”。到期当天早上 8 点,系统自动给双方推送提醒,内容包括归还截止时间、归还地点、逾期后果。这里用 Spring 的@Scheduled定时任务,每分钟扫一次数据库里所有使用中且过期未归还的订单,打上逾期标记。

这块还踩过一个坑:定时任务第一次上线,凌晨两点扫库,把所有今天到期的订单全部标记成了逾期,因为比较条件写的是end_date < now(),而正常逻辑应该是end_date <= 当天日期,日期类型转换时又没取到当天的零点。修正后的逻辑是:先查出status = 使用中 AND borrow_end_date < 当前日期的订单集合,逐个判断是否已经是逾期状态,再幂等更新。如果没加幂等,定时任务每次重启都会把已处理过的订单再处理一遍,用户收到的通知会是重复的。

4.4 后台审核与控制台

管理后台是整个系统能平稳运转的运维底座。管理员能看到商品审核列表、用户举报、租赁纠纷、订单流水和交易统计。审核列表里最有用的是一个“同款检测”按钮,基于标题和类目做简单的相似度匹配,一键找出疑似批量发布相同商品的账号。用户管理页支持封禁账号和重置信用分,纠纷仲裁页则整合了买卖双方的凭证照片和时间线,管理员可以直接裁定押金扣除比例,裁定结果自动更新订单状态。

5. 上线阶段最值得记的五个坑

5.1 图片路径问题:本地跑得好好的,上线全裂了

本地开发时上传的图片都存到了项目根目录的upload文件夹,访问的时候直接写/upload/xxx.jpg,在 IDE 里一切正常。部署到云服务器后,我用java -jar启动服务,上传路径变成了 jar 包解压的临时目录,重启一次图片全没了。这个问题的根因是:项目运行时的工作目录和 IDE 里的工作目录不一致。解决方案是定义一个外部配置项file.upload-path,上线时指向/data/app/upload,并用 Nginx 把/upload/**映射到这个绝对路径。从那以后,我连前端静态资源也用 Nginx 托管了,后端的 Tomcat 只负责接口请求。

5.2 教材超卖:一条 SQL 解决的问题

前文已经讲了乐观锁处理库存,这里补充一个真实场景。我们运营时上架过一套六本的考研英语真题,标价 35 元,结果一个中午来了四个订单,全部支付成功。排查发现,当时库存更新走的是 MyBatis-Plus 的updateById,它默认不拼接stock > 0条件,五个人并发进来,谁都能减成功。改成上面的条件更新之后,那种“订单比库存多”的问题再也没出现过。另外,订单表一定要建唯一索引(product_id, user_id, order_type),防止用户手抖连点两次“提交订单”,生成两条一模一样的订单。

5.3 租赁跨天计费:DATE 和 DATETIME 的坑

租赁订单刚上线时,borrow_end_date用的是 DATETIME,有一单租台灯,起租 5 月 1 日晚上 9 点,应还 5 月 3 日晚上 9 点。计费逻辑算出来居然要收 4 天的钱,因为差值算法把起租时刻和归还时刻之间的实际小时数换算成天数后直接向上取整了。后来统一改成 DATE 类型,用Period.between(borrowStartDate, actualReturnDate).getDays() + 1计算天数,才彻底解决。在校园租赁场景里,“天数”永远是日期差,而不是时间戳差,这一点任何接手代码的人都容易看漏。

5.4 缓存穿透:空数据也能被打爆

商品详情页一开始直接查 MySQL,后来加了 Redis 缓存,key 是product:detail:{id}。结果有段时间后台日志里出现大量慢查询,排查发现是有人(也可能是自己人压测)用不存在的商品 ID 海量请求,每次都穿透到数据库。这是典型的缓存穿透问题,解决方案很简单:对查不到的 ID,也在 Redis 里缓存一个空值,设置 5 分钟过期,再配合布隆过滤器拦截明显不存在的 ID。校园系统不用把布隆过滤器做得很重,直接加空值缓存就足够挡住绝大多数恶意遍历。

5.5 搜索排名:LIKE 查询慢的优化

商品搜索用的是title LIKE '%keyword%',资料少的时候毫秒级响应,上架两千条数据之后,搜索一次要两秒多。后来用 MySQL 的全文索引解决,建了FULLTEXT KEY ft_search (title, description),查询改写为MATCH(title, description) AGAINST('keyword' IN NATURAL LANGUAGE MODE),响应时间降到 100ms 以内。再往后量大起来,可以再引入 Elasticsearch,但校园项目做到索引优化这一步已经完全够用。搜索排序上,按“浏览量 × 0.2 + 收藏数 × 3 + 成交数 × 10”生成一个热度分,再混合上架时间做倒序,既能保证基础质量,也照顾到新发布的商品有露脸机会。

6. 运营推广与冷启动经验

6.1 冷启动:先铺供给,再拉流量

校园项目最忌讳一上线就到处发传单,结果用户点开一看,里面就三件商品,转头就走。我当时第一步是“扫楼收闲置”。挨个宿舍问有没有要出的教材、电器、健身器材,承诺代拍照片、代写文案、上架后卖出再收 10% 服务费。三天收了 160 多件商品,全部按平台规格整理上线。供给量过了 100 这个门槛之后,用户进来才有东西可逛,后面的推广才不是白做。

6.2 毕业季专项活动

每年五到七月是校园二手流转的黄金窗口,系统专门做了一个“毕业季宿舍集市”版块,学生可以一键发布整屋闲置,平台自动生成打包清单和摆摊海报。线下配合做了一次“扫码下单、两天内校内到货”的驿站运营,教材类订单在毕业季占了总量的 40% 以上。针对大一新生,九月开学前上线了“教材预售”功能,学长学姐提前上架教材,新生按课程号搜索直接下单,完美承接了毕业季和开学季两个爆发期。

6.3 信用体系与纠纷处理

平台没有接第三方的信用分,因为学生的数据不在开放的信用体系里,所以自建了一套很简单但有效的信用规则。初始信用分 100 分,每完成一单正常交易加 1 分,逾期未归还一次扣 10 分,被管理员仲裁为责任方扣 20 分。信用分低于 70 分就不能发布租赁商品,低于 60 分禁止下单。纠纷处理遵循一个原则:看凭证而不是听故事。买家取货时必须拍照上传商品现状,归还时拍归还照片,运营侧只依据双方凭证截图做判断,管理员有一个专门的仲裁页面,可以查看订单全部时间线,这样的裁定结果双方接受度都高。

我个人在实际开发中最大的体会是,租赁模块的复杂度远超预期,任何和时间、押金、信用挂钩的业务,都不能想当然地套用普通交易逻辑。系统跑完一整届毕业季后,我明显感到,相比“把所有功能都做出来”,更难的是“在关键节点上做减法”。比如砍掉站内聊天、砍掉复杂推荐算法,反而让核心交易链路更顺畅。如果你也想做校园闲置交易系统,我建议不要急着堆功能,先把出售和租赁两种交易模型吃透,再设计表结构,再写代码。后续这套系统还可以往社团活动设备共享、考研资料代售、宿舍小卖部入仓这几个方向扩展,每一步都不缺真实需求,关键是把基础交易闭环做扎实。

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

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

立即咨询