☰
校园二手交易平台:SSM+Flask混合架构设计与实践
2026/10/14 5:57:30 网站建设 项目流程

每年毕业季和开学季,校园里总会出现两个极端:一边是毕业生恨不得把四年攒下的书、台灯、置物架全部清空,另一边是新生什么都想买又舍不得按原价买。前两年我在某高校做项目实践时,宿舍楼下公告栏里贴满手写转让小纸条,楼道群里的消息能刷几百条。这种场景大家都不陌生,但真正去做一个校园二手交易平台的时候会发现,它跟市面上的综合二手平台看着像,实际做起来的业务侧重、技术取舍都不一样。

这篇博文就围绕我参与完成的这个基于Java+SSM+Flask的校园二手交易平台展开,从需求收敛、架构选型聊到数据库设计、核心代码实现,再到部署联调阶段实打实踩过的五个坑。适合正在做类似课题、准备毕业设计答辩,或者想了解SSM与Flask混合架构如何落地的同学参考。

1. 校园二手场景下,业务需求到底该怎么收敛

很多同学一上来就照着闲鱼、转转的功能列表抄:拍卖、直播、信用分、保价理赔……全都想要。但校园场景就那么大,用户在宿舍楼和教学楼之间活动,交易半径很小,核心诉求其实非常聚焦。做项目的第一步不是堆功能,而是把需求收敛到能跑通交易闭环的程度。

1.1 校园二手与综合二手平台的三个本质差异

第一,信任机制不同。综合平台靠芝麻信用之类的外部信用体系背书,校园平台更依赖身份认证。所以必须把"学生认证"做成硬门槛,没有校园认证只能浏览不能下单。

第二,交易节奏不同。校园二手有极强的季节性,毕业季和开学季是交易高峰,平时也有零散买卖,但波动大。这意味着商品上架要有"急售"标识,列表页要能按"发布时间"和"离我最近"这种维度排序。

第三,定价逻辑不同。学生之间讲价更直接,定价也不会太高。所以商品需要支持"可小刀"标记,让买家知道哪些商品可以议价。

1.2 核心业务流程与角色边界

平台角色就三类:买家、卖家、管理员。卖家发布商品,买家浏览、收藏、发起询价、下单,管理员负责审核违禁品和处置纠纷。整个核心闭环是"发布商品 → 浏览检索 → 私信沟通 → 下单交易 → 状态流转"。

值得一提的还有消息系统。很多人只做了商品模块和订单模块,忽略了私信。但校园场景里私信其实是成交前最高频的操作,买家往往要先问一句"还在吗""能少点吗"。在实践中我把私信做成了单聊会话模式,买卖双方围绕一笔订单或一个商品沟通,成交前能留存上下文,这一点对后续订单纠纷回溯很有帮助。

1.3 需求优先级排序:先做能跑通闭环的,再谈锦上添花

功能优先级上,我的排法是:第一优先级是用户注册登录、学生认证、商品发布、商品列表、商品详情、下单购买;第二优先级是收藏、私信、个人中心、订单管理;第三优先级才是推荐、数据统计、后台审核这些辅助能力。

实际操作里我给自己定了个原则:任何功能如果不能服务于"让一笔交易顺利完成",就先不动手。比如拍卖功能就能砍掉,因为校园二手交易要的就是快,拍卖拉长决策周期反而影响成交意愿。

2. 为什么是SSM坐镇主业务、Flask干算法杂活:混合架构的取舍

标题里出现了两套技术栈:Java+SSM和Flask。项目最终交付的架构,是SSM作为主业务系统,Flask作为辅助算法服务。这种"一主一辅"的混合架构不是炫技,而是根据两边生态的真实优势做的分工。

2.1 SSM负责什么,Flask负责什么

SSM(Spring + SpringMVC + MyBatis)负责所有主链路业务:用户、商品、订单、收藏、私信。原因很直接:这套组合在事务管理、权限控制上面非常成熟,尤其是Spring的声明式事务,一个注解就能把下单时扣库存、生成订单、更新商品状态三个操作包在同一个事务里。这些核心业务一旦出错,资金和物品状态就对不上,必须交给事务能力强的框架。

Flask负责两件SSM不擅长的事:图片合规检查、个性化商品推荐。Python生态在这两个方向上有天然优势,Pillow处理图像、基于标签的相似度计算用Python写非常顺手,而Java在这类场景写起来冗长且不出效果。

2.2 两个服务之间的通信与鉴权设计

Java服务与Flask服务之间用HTTP + JSON通信,SSM作为调用方,Flask只提供接口被调用。这里有一个关键设计:Flask服务不直接暴露给前端,所有请求都先打到Java后端,再由Java后端作为代理去请求Flask。

这么做的好处是:前端只需要跟一个后端打交道,跨域问题只在Java端解决一次;Flask接口的调用凭证也由Java端统一管理,不会在浏览器里泄露。鉴权方面,我在Java端调用Flask时,请求头里带了一个内部token,Flask端校验token合法才响应,避免把算法服务裸奔在内网里。

2.3 这种架构的代价在哪里

混合架构不是没有代价。最直观的问题是部署复杂度的增加——原来一个Tomcat就能跑完的应用,现在需要多维护一个Python进程。我当时用supervisor同时守护两个服务,Java进程挂了自动拉起,Flask进程也做了同样的托管。

此外,两个服务之间的网络开销也不小。商品发布做图片审核时,Java要把图片多转一次发给Flask,如果图片大,单张耗时容易到几百毫秒。我的处理方式是把审核做成异步,商品先以上架状态写入,后台异步调用Flask,审核通过后自动置为在售;审核不通过则回滚商品状态并通知用户。这样用户发布体验不被审核链路拖住。

3. 数据库设计:那些毕设答辩时老师最爱追问的表细节

数据库设计是整个项目里最值得花时间的部分。表设计得好不好,直接决定后面的代码写起来是顺滑还是别扭。这里我挑几张核心表展开说说,建表思路比建表语句本身更重要。

3.1 用户表与校园身份校验

用户表除了常规的id、用户名、密码、手机号之外,我额外加了三个字段:student_no(学号)、campus_auth_status(认证状态)、avatar_url(头像)。

这里的关键在于认证状态的流转。注册时用户的campus_auth_status默认是0(未认证),可以浏览商品但不能下单。用户提交学号后,后台有两种认证方式:一种是对接学校的统一身份认证接口,但很多学校不开放;另一种是人工审核或邮箱后缀校验。我当时用的是学生证照片上传 + 管理员后台人工审核,虽然重了一点,但答辩时老师会觉得业务考虑周全。

3.2 商品表的状态设计与检索字段

商品表是核心中的核心。字段方面,除了商品名称、描述、价格,我加了一个tag字段存标签,用逗号分隔,比如"数码,宿舍神器"。这个字段在后面的Flask推荐服务里是核心匹配依据。

商品状态的枚举值要提前定义清楚:0待审核、1在售、2已售出、3下架、4审核拒绝、5删除。这个状态的流转顺序要跟代码逻辑一一对应。很多同学把"已售出"和"下架"混在一起,其实语义完全不同——已售出是交易结果,下架是卖家主动动作。

检索字段方面,我在category_id、price、campus_id上建了复合索引。校园平台的检索场景很特殊:用户基本只搜本校区,所以campus_id作为第一查询条件效率非常高。注意不要在主键之外滥用索引,像description这种大文本字段千万不要加索引。

3.3 订单表的状态机与并发安全

订单表设计我建议把状态机画清楚再动手。我的订单状态是:待付款 → 待发货 → 待收货 → 已完成,另外有已取消和售后中两个分支状态。每一步都记录操作时间和操作人,方便后续追溯。

这里有一个时不时会考到的点:下单时的并发安全,也就是防超卖。在校园平台上,一件商品不可能卖给两个人,所以下单更新商品状态时的SQL必须带上条件。正确写法是:

UPDATE product SET status = 2 WHERE id = #{productId} AND status = 1

这个update返回受影响行数为1,才说明抢占商品成功;返回0则说明商品已经被人买走或已下架。事务层再配合Spring的@Transactional,能保证原子性。

4. SSM主业务链路的实现:从商品发布到订单状态机

业务代码部分我挑三条主链路讲:商品发布、列表检索、订单状态流转。这三段几乎覆盖了SSM日常开发中最常见的操作模式。

4.1 商品发布与图片上传的完整链路

商品发布接口的Controller接收一个包含商品信息和图片列表的请求。图片上传我选择了本地磁盘存储 + 数据库存相对路径的方式,而不是存OSS,因为校园项目对公网访问的需求不高,部署在一台服务器上时本地存储更快更省事。

存储路径设计有个小技巧,按日期分目录存放:

String uploadDir = "/data/upload/" + new SimpleDateFormat("yyyyMMdd").format(new Date());

这样能避免单个目录文件太多导致查找慢,也给后续清理过期图片留了余地。Controller层通过MultipartFile接收文件,将文件保存到磁盘后,把相对路径存入数据库,前端通过一个映射路径访问图片。注意要对文件类型做白名单校验,只允许jpg、png、jpeg,后端加了文件大小限制,最大5MB,防止有人传超大文件撑爆磁盘。

商品发布后不是立即可售,而是先进入待审核状态。这与前面说的异步Flask图片审核配合。发布接口的业务逻辑就是:保存商品信息 → 保存图片记录 → 发送异步审核消息。

4.2 多条件检索的MyBatis动态SQL写法

列表页的检索条件往往组合多变:关键词、分类、价格区间、成色、校区、排序。如果每个条件单独写一个SQL,代码会爆炸。MyBatis的动态SQL就是为这个场景设计的。

<select id="searchProducts" resultType="com.example.vo.ProductVO"> SELECT * FROM product <where> <if test="keyword != null and keyword != ''"> AND title LIKE CONCAT('%', #{keyword}, '%') </if> <if test="categoryId != null"> AND category_id = #{categoryId} </if> <if test="minPrice != null"> AND price &gt;= #{minPrice} </if> <if test="maxPrice != null"> AND price &lt;= #{maxPrice} </if> AND status = 1 AND campus_id = #{campusId} </where> ORDER BY <choose> <when test="sort == 'time'">publish_time DESC</when> <when test="sort == 'price_asc'">price ASC</when> <otherwise>publish_time DESC</otherwise> </choose> LIMIT #{offset}, #{pageSize} </select>

注意我用<where>标签而不是直接拼where 1=1,这样可以自动去掉第一个多余的AND。排序字段用<choose>做白名单映射,坚决不直接拼接前端传进来的排序内容,防止SQL注入。

4.3 订单状态机与防超卖设计

下单接口是整个系统里事务最复杂的操作。我的代码流程是:先校验商品状态为在售 → 更新商品状态为已售出(带条件更新) → 创建订单 → 若更新失败则抛出异常触发事务回滚。

@Override @Transactional(rollbackFor = Exception.class) public Order createOrder(Long productId, Long buyerId) { // 1. 抢占商品 int updated = productMapper.soldOut(productId); if (updated == 0) { throw new BizException("商品已售出或已下架"); } // 2. 创建订单 Order order = new Order(); order.setProductId(productId); order.setBuyerId(buyerId); order.setStatus(OrderStatus.PENDING_PAYMENT.getCode()); orderMapper.insert(order); // 3. 扣减卖家积分或记录日志(可选) return order; }

订单状态流转我用了一个独立的handler分发机制,每个状态变化都对应一个方法,类似状态机模式。比如"确认收货"状态只有"待收货"状态下的订单才能合法流转,避免前端乱传状态导致业务混乱。这个设计在答辩时非常加分,因为面试官/老师一看就知道你有工程意识。

5. Flask辅助服务:图片审核与个性化推荐的轻量落地

按理说SSM已经能完成主要功能,为什么还要单独写一个Flask服务?因为在实践中,图像处理和推荐计算用Java写起来实在太繁琐了,Python生态一个几行的脚本就能顶上。但辅助服务不能变成系统的累赘,落地时要有清晰的边界。

5.1 Flask提供了哪两个核心接口

我最终实现的两个接口分别是:

  • POST /api/image/review:接收图片URL(或base64),返回审核结果(通过/不通过+原因)
  • POST /api/recommend:接收用户最近浏览的商品标签和分类ID列表,返回推荐商品ID列表

两个接口都是无状态服务,只接收必要参数,返回固定结构JSON。这样做的好处是,哪怕以后把图片审核换成更专业的第三方服务,也只需要改Java端的调用逻辑,Flask服务可以直接下线。

5.2 轻量级图片合规检查的实现思路

严格来说这不是一个完整的鉴黄系统,而是基于规则的合法性初筛。我写了一个Python脚本,主要做这几件事:

  • 读取图片,用Pillow解析尺寸,限制宽度不能小于200像素
  • 检查图片是否为纯色或白底图占用过大面积,疑似广告图或二维码
  • 检查图片中是否包含太多文字区域,用简单的边缘密度判断,疑似广告图会被拦下
def is_suspicious_ad(image_path): img = Image.open(image_path).convert("L") # 边缘检测:如果边缘像素占比过高,大概率是文字密集的截图或海报 edges = np.gradient(np.asarray(img, dtype=np.float32)) edge_ratio = (np.abs(edges) > 50).sum() / edges[0].size return edge_ratio > 0.18

这个逻辑当然不算高深,但它在实际项目中的价值是:把明显违规的图片在第一道关口拦截下来,人工审核的压力就小很多。我在答辩时也坦诚地讲了这只是规则初筛,后续可以引入深度学习模型提升准确率。

5.3 基于标签与浏览记录的推荐逻辑

推荐服务用了一个非常朴素的思路:基于标签的相似度计算。用户浏览商品时,Java端会记录浏览历史,并同步异步地把用户最近浏览商品的标签列表传给Flask。推荐接口做两件事:

  • 读取所有在售商品,拆解它们的tag字段
  • 计算每个候选商品与用户历史标签的交集数量,按交集数降序返回Top 10

交集数相同的情况下,再按商品浏览量作为次排序依据。整个计算用Python写成,几十行代码就完成了。效果虽然不如协同过滤那么精细,但不依赖历史交易数据,冷启动问题小,非常适合数据量不大的校园场景。

5.4 服务化过程中的超时与降级处理

辅助服务最怕的就是"调用很开心,挂了很崩溃"。由于Flask服务是Python进程,它可能因为某个图片处理耗时过长而阻塞,进而拖慢Java端的商品发布接口。我在Java端用HttpClient调用Flask时统一设置3秒超时,超时后降级为"审核通过"。因为审核本来就是辅助功能,用户发布商品不应被它卡住。

同时,Flask端也做了线程池限制,默认最大4个worker线程,图片处理任务排队执行,避免并发高时单进程被彻底打垮。这个设计说白了就是让主业务系统不依赖任何非核心组件,非核心组件挂了就挂,业务照走。

6. 联调部署阶段我踩过的五个坑,以及对应的修复方案

这一段是整个项目里最折腾的部分,也是收获最大的阶段。真刀真枪部署到Linux服务器上跑起来之后,很多在本地Windows上根本不会出现的问题全都浮出水面了。盘一盘我印象最深的五个坑,希望能帮正在做类似项目的同学少走弯路。

6.1 坑一:JSON日期序列化后的时区混乱

前后端联调时,前端反馈订单列表时间显示比本地时间早8个小时。排查后发现是SpringMVC默认使用Jackson的日期序列化时,把Date对象按UTC格式输出了,而前端浏览器解析时把UTC时间当成北京时间直接展示。

修复方案很简单,在application.yml里统一指定日期格式和时区:

spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT+8

但更稳妥的做法是后端直接返回时间戳或标准的"yyyy-MM-dd HH:mm:ss"字符串,避免前后端各自做解析产生歧义。这是一个很典型的联调细节,也是最容易被忽视的。

6.2 坑二:Linux部署后图片路径访问404

本地开发一切正常,部署到服务器后所有商品图片全部404。排查路径:先看Tomcat日志没报错,再检查磁盘路径,发现图片确实写入了/data/upload目录,但前端通过http://xxx/upload/xxx.jpg访问时Tomcat根本不会把请求路由到本地磁盘目录。

最后在SpringMVC配置里加了一个资源映射:

@Configuration public class WebConfig implements WebMvcConfigurer { @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler("/upload/**") .addResourceHandler("file:/data/upload/"); } }

这个坑提醒我:本地开发的相对路径幻觉会在部署的第一天被彻底打碎。资源文件的绝对路径、上传目录的权限、磁盘空间的监控,这些都是上线前必须确认的事。

6.3 坑三:数据库连接池打架导致的接口偶发超时

系统跑了一周后,偶尔出现某个接口响应特别慢甚至超时,重启Tomcat又恢复正常。查了Druid监控,发现数据库连接池的最大活跃连接数经常飙满,大量线程都在等待获取连接。

根因有两个:一是某个循环调用数据库的代码没有复用Mapper查询,而是在for循环里逐条查询,导致短连接暴涨。二是个别慢SQL没建索引,锁表时间变长,连接持有时长超预期。

解决方案是把循环内逐条查询改成批量查询,一条IN语句搞定;另外对订单表和商品表上几个高频查询字段补了联合索引。Druid的监控面板是排查这类问题的最强辅助,数据会告诉你到底哪里出了问题。

6.4 坑四:跨域配置放行过宽引发的安全风险

一开始为了方便前端联调,我用了一个允许所有来源的CORS配置:

registry.addMapping("/**").allowedOrigins("*")

这个配置在开发阶段确实方便,但问题很大——任何网站都能往接口发请求,相当于把平台的数据暴露给了所有第三方。修复方案是改成白名单模式,只允许部署前端页面的域名或IP进行跨域请求。

registry.addMapping("/api/**") .allowedOrigins("http://localhost:8080", "http://你的前端地址")

对于带cookie的请求,allowedOrigins不能用通配符,必须明确列出。这也是答辩时老师通常关注的细节,能体现你的安全意识。

6.5 坑五:商品发布重复提交造成脏数据

用户连续点击"发布"按钮两次,结果商品表里出现了两条一模一样的商品记录。排查发现Controller层完全没有做幂等控制。

修复方案是前端先禁用按钮再发请求,后端再做一层校验:同一个用户在30秒内发布标题完全相同的商品就拒绝。我在发布请求里加了一个token机制,前端先获取唯一token,发布时带上,后端校验token是否已被消费,消费过就拒绝。

if (!redisTemplate.opsForValue().setIfAbsent("publish_" + token, "1", 30, TimeUnit.SECONDS)) { throw new BizException("请勿重复提交"); }

用Redis的setIfAbsent实现一个简单的分布式锁,30秒内重复提交会被拦截。这个方案简单高效,既防止用户误触,也防止脚本刷数据。

7. 一点个人体会

项目全部写完、部署稳定后,我的一个实际感受是:别急着写代码,前期把需求边界、表结构、状态机想清楚,后面省下的时间远超多花的那几天。尤其是状态字段的枚举定义,项目做到一半再改动,代价是巨大的。

另一个体会是混合架构的"度"要把握好。SSM主业务、Flask做辅助这个模式确实好用,但不是说Python能做的事都要搬过去。只有当你发现现有技术栈做某件事的性价比极低时,才值得引入第二个服务。架构是手段,不是目的;业务闭环能顺利走通,用户愿意用,才是这个项目最大的成功。

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

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

立即咨询