ARTICLE · INTELLIGENCE

战地情报 · 详情页

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

Spring Boot+Vue3全栈开发:淘物平台C2C二手交易系统设计与实践

Spring Boot+Vue3全栈开发:淘物平台C2C二手交易系统设计与实践

接手这个标题的时候,我第一反应是笑了一下——同一个项目名称里同时出现 PHP、asp.net、java、Spring Boot、SSM、Vue3,这几乎就是毕设/课设选题里最常见的"全家桶式命名法"。说难听点,看起来像要写五个系统,说直白点,这其实就是一个典型的全栈 Web 应用:前端一套页面,后端一套接口,数据库一套表。真正动手前最需要做的,不是纠结会不会写五种技术,而是先把"选型"这件事想明白。

这篇文章我就完整复盘一下,我是怎么把这个"淘物平台"从一堆技术名词里拆出来、设计清楚、最终跑通的。项目定位是校园闲置物品交易场景,也就是常说的 C2C 二手交易平台。如果你正准备做同类电商/交易类毕设,或者想快速搭一个能演示的全栈项目,这篇会给你一条可以直接照着走的路。

1. 五门技术凑一个标题:先别慌,这是选型题不是全做题

1.1 为什么标题里会同时出现五门技术

我非常确定,这个标题大概率是参考了网上大量同类题目后拼出来的。很多课题模板喜欢把"调研过哪几门技术"写进题目里,显得体量大、有工作量。但实际开发时,一个人不可能也不必要同时用五套语言写完一个系统。

我看到这类题目的第一反应,是先给它归类:这到底是个什么类型的系统?"淘物平台",本质就是电商交易平台,细分一点是 C2C 的闲置物品交换平台。这类系统的核心功能是固定的——用户注册登录、商品发布展示、搜索筛选、下单交易、订单管理、个人中心。至于用 PHP 还是 Java 实现,只是不同赛道,业务本身不随语言变化。

1.2 我的取舍逻辑:Java 主线,PHP 和 asp.net 放弃

标题里的六个关键词,实际可分成三组:

  • 第一组:PHP、asp.net——这是两门"古老但仍在用"的服务端语言/框架体系;
  • 第二组:Java、Spring Boot、SSM——同属 Java 体系,Spring Boot 是整合工具,SSM 是三个框架的简称;
  • 第三组:Vue3——前端框架。

正常人是没法同时用三组方案做同一个系统的,所以我必须做选择。我选的是:Spring Boot + SSM + Vue3 这套主线,PHP 和 asp.net 直接从实现名单里划掉。

理由很实际:

维度Java 主线PHP 主线asp.net 主线
个人熟练度高,毕设常见中,写过小站低,不熟
社区资料量极大,问题基本都能搜到大,但优质教程偏老中等,新手容易卡壳
部署成本一个 jar 包 + MySQL需要 PHP 环境 + NginxWindows + IIS 更顺
就业/答辩价值高,面试常问一般一般

更多是省时间。Spring Boot 能自动配置 Web 容器、数据库连接池、JSON 解析、参数校验,开箱即用;SSM 里的三个框架又是一个学了 Java 半年的人早晚要接触的东西。用这套方案,我只需要把精力放在业务流程上。

1.3 最终技术组合与分工

确定主线后,我列了一份非常清晰的技术分工表:

  • 后端:Spring Boot 2.7.x + MyBatis + Spring MVC(SSM 的现代整合形态),用 JWT 做登录态;
  • 数据库:MySQL 8,两张以上核心业务表,外键关系用逻辑关联维护;
  • 前端:Vue3 + Vite + Element Plus + Pinia + Axios,前后端完全分离;
  • 周边:本地文件存储做图片上传,阿里云/腾讯云等服务器部署 Nginx 反向代理。

你可能会问,SSM 不是三个框架吗,和 Spring Boot 冲突吗?完全不冲突。这句话我放到第四章详细讲。

2. 淘物平台的业务本质:做撮合,不是做自营商城

2.1 想清楚这个项目到底在解决什么问题

很多人在设计"淘物平台"时最大的误区,是把它做成了简化版淘宝。但二手闲置交易和 B2C 自营商城有一个本质区别:自营商城是"平台卖货给用户",二手的逻辑是"用户卖货给用户,平台只做撮合和担保"。

所以核心角色有三种:

  • 买家:浏览、搜索、收藏、下单、付款(模拟)、确认收货、评价;
  • 卖家:发布商品、管理在售/已售、发货(或线下交付)、回复咨询、收款(模拟);
  • 管理员:管理用户状态、审核违规商品、查看交易统计。

在这个定位下,功能模块怎么切就很清晰了:

  1. 用户模块:注册、登录、个人信息、密码修改;
  2. 商品模块:发布、编辑、上下架、分类浏览、关键词搜索、图片展示;
  3. 交易模块:生成订单、支付状态模拟、发货、确认收货、取消订单;
  4. 互动模块:收藏、站内留言/咨询;
  5. 管理后台:用户列表、商品审核、分类管理、订单查看。

2.2 和 B2C 商城的一个关键差异:下单流程

B2C 商城有库存、SKU、运费模板,买了一单可能拆成多个包裹。二手闲置平台通常没有库存概念,一件商品只有一个卖家,卖掉就下架,也不存在"多件 SKU"。

所以我把订单表设计得很轻:一单只对应一件商品,订单状态只有七个:待付款、待发货(虚拟线上交付场景)、待收货、待评价、已完成、已取消、售后中。这极大简化了工作量和代码量,我后面在状态机设计上只花了一个晚上就写完。

2.3 功能边界:别被"支付"两个字带偏

很多同学一看到交易平台就想做真实的微信/支付宝支付对接,我强烈建议不要碰。第一,个人开发者申请支付接口有门槛;第二,答辩时考官不会真让你付一笔钱。

我的做法是做一个"模拟支付":用户下单后进入待支付状态,点"模拟支付",前端调后端接口,后端直接把订单状态改成"已支付",同时给卖家增加一笔"待入账金额"的记录。流程被完整走通,逻辑闭环也成立,但完全不碰真实资金。

提示:如果题目里明确要求对接真实支付接口,那你需要走商户申请流程,并准备好证书和回调验签逻辑。毕设场景下,模拟支付完全够用。

3. 数据库设计:七张核心表怎么把买卖双方串起来

3.1 先画关系,再建表

我一般不建议一上来就写建表 SQL,而是先在纸上画出实体关系:用户和商品是一对多,商品和图片是一对多,用户和收藏是多对多,订单关联买家、卖家和商品,消息关联用户和商品。画清楚后,表结构就是一目了然的了。

最终我用到的核心表有这些:

表名作用关键字段
user用户表id, username, password, nickname, avatar, role, status
category分类表id, name, parent_id, sort
goods商品表id, seller_id, category_id, title, description, price, status, browse_count
goods_image商品图片表id, goods_id, image_url, sort
favorite收藏表id, user_id, goods_id, create_time
orders订单表id, order_no, goods_id, buyer_id, seller_id, amount, order_status
message咨询消息表id, goods_id, from_user_id, to_user_id, content, create_time

3.2 几个容易翻车的字段设计经验

第一处:价格字段类型。很多人喜欢用 float 或 double 存价格,我只能说,做演示没出事只是因为你运气好。用 decimal(10,2) 最稳妥,Java 侧的对应类型是 BigDecimal,加减乘除不会出现 0.1 + 0.2 = 0.30000000000000004 这种经典问题。

第二处:订单状态字段。我建议用 tinyint 存状态码,在代码里写一个常量类做映射,而不是直接用字符串存"待付款""已付款"。好处是方便扩展和改文案,数据库也省空间。

第三处:商品图片存储。千万别在 goods 表里放一个 image_urls 字段,用逗号把五六张图片拼成一个字符串。这会让"查详情页时只取封面图"变成一个拆字符串的噩梦。单独建一张图片表,一对多关联,查询和删除都干净。

第四处:全部表都要有 create_time、update_time 两个时间字段。MySQL 8 支持 DEFAULT CURRENT_TIMESTAMP 和 ON UPDATE CURRENT_TIMESTAMP,建表时直接定义好,后端代码里就不用手动维护修改时间了。

3.3 建表语句里最值得抄的一段

商品表是整张库的灵魂,我直接把我当时写的核心建表语句贴出来参考:

CREATE TABLE `goods` ( `id` bigint NOT NULL AUTO_INCREMENT COMMENT '商品ID', `seller_id` bigint NOT NULL COMMENT '卖家用户ID', `category_id` bigint DEFAULT NULL COMMENT '分类ID', `title` varchar(100) NOT NULL COMMENT '商品标题', `description` text COMMENT '商品描述', `price` decimal(10,2) NOT NULL COMMENT '价格(元)', `status` tinyint NOT NULL DEFAULT '0' COMMENT '0草稿 1在售 2已售 3下架 4审核拒绝', `browse_count` int NOT NULL DEFAULT '0' COMMENT '浏览数', `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`), KEY `idx_seller_id` (`seller_id`), KEY `idx_category_id` (`category_id`), KEY `idx_status` (`status`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='商品表';

这里三个索引是必须的:按卖家查其发布的商品、按分类筛选、按状态过滤。首页列表和"我的发布"都会高频走这些查询条件,不加索引在小数据量时没感觉,到了几千条数据切开就明显。

4. Spring Boot + SSM 这套组合到底怎么分工

4.1 SSM 不是第四个框架,它是整合方式

这是我给很多同学讲过的一句话。SSM 指的是 Spring、Spring MVC、MyBatis 三个框架的名字拼在一起。Spring 管对象和事务,Spring MVC 管接口路由,MyBatis 管数据库操作。Spring Boot 则是把这套东西进一步封装,让我少写一堆 XML 配置。

所以在项目里实际的结构是这样的:

  • 用 Spring MVC 的 @RestController 写 Controller 层接口;
  • 用 MyBatis 写 Mapper 接口和 XML SQL;
  • 用 Spring 的 @Service 写 Service 层业务逻辑,并在事务方法上加 @Transactional;
  • 用 Spring Boot 作为整个应用的启动器,通过 application.yml 配置数据源、端口、上传大小等。

换句话说,标题里的 Spring Boot 和 SSM 不是两种并列方案,而是"汽车整车"和"核心零件"的关系。

4.2 登录鉴权:用 JWT 而不是复杂的安全框架

如果你只是想快速让平台跑起来,Spring Security 的学习成本和配置成本都可能让你崩溃。我选了更轻量的方案:JWT + 拦截器。

流程很简单:

  1. 登录成功后,后端生成 token,把用户 id 和角色放进 token 里;
  2. 前端把 token 存到 localStorage,每次请求在 Header 里加 Authorization 字段;
  3. 后端写一个拦截器,拦截需要登录的路由,校验 token,解析出当前用户;
  4. 把用户放到 ThreadLocal 里,这样 Service 层任何地方都能拿到当前登录人。

JWT 本身不需要服务端保存状态,对前后端分离的项目非常友好。我在工具类里封装了生成和解析两个方法,一共几十行代码,比引入一套 Security 配置省太多事。

关键是接口要做角色区分。比如"下架商品"这个操作,卖家和管理员都能做,但普通买家不行。拦截器只解决"有没有登录","有没有权限"我放在 Service 层判断:查出商品,比较 seller_id 和当前用户 id,不一致就直接抛业务异常。

4.3 统一返回体:前端联调不吵架的前提

前端和后端最容易扯皮的就是接口返回值格式不统一。我在项目里从第一天就用统一返回体:

public class Result<T> { private Integer code; // 200 成功,其他失败 private String message; private T data; public static <T> Result<T> success(T data) { ... } public static <T> Result<T> error(String message) { ... } }

每个 Controller 方法都返回 Result,成功时 code=200、data 里放真实数据,失败时 code=500(或业务码)并带上错误信息。前端 Axios 接收后统一判断 code,不用每个页面单独猜后端返回结构。

4.4 商品发布里的文件上传:比想象中多两个坑

商品发布要支持多图上传,最多五张,第一张当封面。

我用的是 MultipartFile 接收文件,上传到本地指定目录,把访问地址返回给前端。这里有两个典型坑:

第一个坑是文件类型校验。千万别只信前端选的图片类型,后端必须验证。我用的是文件名后缀白名单:.jpg、.jpeg、.png、.gif、.webp,同时限制单张不超过 2MB。

第二个坑是上传目录问题。Spring Boot 启动后,当前工作目录有时和你预期的不一样。我最后用了绝对路径配置:

file: upload-dir: /data/taowu/img/

然后在代码里判断目录不存在就自动创建。每次重启服务后,图片还在,这就够了。部署到服务器时,我把这个目录配置为 Nginx 的静态资源目录,访问路径和项目完全解耦。

4.5 订单状态机的实现心得

订单状态我用常量类定义:

public class OrderStatus { public static final int WAIT_PAY = 1; public static final int WAIT_DELIVER = 2; public static final int WAIT_RECEIVE = 3; public static final int WAIT_COMMENT = 4; public static final int FINISHED = 5; public static final int CANCELLED = 6; }

并且我在 Service 层写了一个私有方法canChange(current, target),里面是一张状态迁移表,只允许合法跳转。比如从"已付款"不能直接跳到"已完成",必须经历"已发货"和"确认收货"。

这种做法在答辩时非常好讲:你可以拍着胸脯说,我考虑了非法状态流转,所有操作都做了状态校验。哪怕实现只有几十行,也比"随便改状态字段"显得规范得多。

5. Vue3 前端:从搭建到和后端正常协作

5.1 工程初始化与目录组织

前端我用 Vite 创建 Vue3 项目,UI 库选了 Element Plus。之所以选 Vite 不选 Vue CLI,是因为启动速度快,开发体验好,网上教程也足够多。

目录结构是很传统的:

src/ api/ // 接口请求封装 assets/ // 静态资源 components/ // 公共组件 router/ // 路由 store/ // Pinia 状态管理 views/ // 页面 utils/ // 工具函数

有几个页面是逃不掉的:首页(商品瀑布流)、商品详情、发布商品、我的订单、购物车或收藏夹、登录注册、管理后台。

5.2 Axios 拦截器:把 token 管理做在前面

我在 utils/request.js 里封装了 Axios 实例,并加上了拦截器。请求拦截器负责把 token 放到 Headers,响应拦截器统一处理 401 跳转登录。

request.interceptors.request.use(config => { const token = localStorage.getItem('token'); if (token) { config.headers.Authorization = token; // 注意要和后端拦截器约定一致 } return config; }); request.interceptors.response.use( response => { const res = response.data; if (res.code !== 200) { ElMessage.error(res.message); if (res.code === 401) { router.push('/login'); } return Promise.reject(new Error(res.message)); } return res; }, error => { ElMessage.error('网络异常,请稍后重试'); return Promise.reject(error); } );

有这层统一处理,我在每个页面里就只需要写await api.getGoodsList(params),拿到 result 直接用,不用每页重复弹错误提示。

5.3 几个典型交互的实现细节

首页的搜索和分类筛选我用的是路由参数:点击分类、输入关键词后,跳转到列表页并带上 query,列表页 watch 路由变化自动重新拉数据。这个方案简洁,不需要引入新的状态管理,页面刷新后也能保持筛选条件。

商品发布页的表单验证交给 Element Plus 的 form rules 处理,图片上传用 el-upload 组件。上传组件在提交时不会自动把图片传给后端,需要拿到返回的 URL 后,在提交表单时把 URL 数组一起传给后端。这个顺序很多新手搞反——先把图片上传的请求发出去,拿到地址,再发商品主信息的请求。

我的做法是:el-upload 的 http-request 自定义上传事件,每次选完图就立刻调后端 /api/upload 接口,把文件地址存进表单的 imageUrls 数组里;提交商品时,把标题、描述、价格、分类、imageUrls 一次性提交。

5.4 "我的订单"页面的角色切换

我个人觉得"我的订单"是前端最容易写乱的地方,因为买家视角和卖家视角完全不同。

我的解决办法是做一个 Tab 切换:买家订单和卖家订单。买家订单看自己买过的商品,操作是"付款""确认收货""评价";卖家订单看自己卖出的商品,操作是"发货""查看买家信息"。

后端接口我分开写:GET /api/order/buyer/list和GET /api/order/seller/list,前端各调各的,互不干扰。这样代码逻辑简单,答辩时也好解释:我的角色判断只出现在后端接口,前端只关心当前 Tab 是哪个,代码里完全没有"判断当前用户是不是卖家"这种满屏 if。

6. 部署时真正卡住我的三个地方

6.1 跨域问题:开发环境和生产环境是两套解法

前后端分离项目一定要接触 CORS。开发环境我推荐用 Vue 的 vite.config.js 里的 proxy 代理:

server: { proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true } } }

这样前端页面里所有请求都发到同一个源,浏览器看不到跨域。

但生产环境里,我不用后端代码开 CORS,而是让 Nginx 把前端静态页面和 /api 请求都代理到同一个域名下。我的 Nginx 配置核心就两块:

server { listen 80; server_name 你的域名或IP; location / { root /data/taowu/dist; # 前端打包后的目录 try_files $uri $uri/ /index.html; } location /api/ { proxy_pass http://127.0.0.1:8080; } location /img/ { alias /data/taowu/img/; # 商品图片目录 } }

这样前端请求 https://域名/api/xxx 会被转给后端 jar 服务,图片域名 /img/ 直接落到本地目录,浏览器层面没有任何跨域问题。

6.2 图片上传后访问 404

我当时在本地一切正常,一上服务器,前端商品列表的图片全都 404。排查后发现问题出在路径映射:后端启动时的工作目录不是 /data/taowu,而是某个临时目录,导致上传的图片虽然在 /data/taowu/img 下,但访问时根本找不到。

我最后在 Spring Boot 里加了一个 WebMvcConfigurer 配置,把所有 /img/** 请求映射到本地绝对路径:

@Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler("/img/**") .addResourceLocations("file:/data/taowu/img/"); }

注意 file: 前缀后面的路径一定要以 / 结尾。加完配置重启,问题解决。如果你也想用 Nginx 托管图片,就不再需要这段 Java 配置,二者选其一即可。

6.3 MySQL 端口和时区问题

用云服务器部署时,MySQL 默认只监听本机,Spring Boot 连接时一定要确认用的是真实的 IP 或域名而不是 localhost,否则 jar 包可能连不上。另外 MySQL 连接串里加一个参数,能省掉不少时区引发的恶心问题:

jdbc:mysql://127.0.0.1:3306/taowu?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai

serverTimezone不指定时,在有些服务器上查询时间会差 8 小时。我之前因为这个查订单创建时间对不上,排查了半天,实际上就是时区配置的问题。

6.4 部署顺序和运维心得

我的部署顺序是:先打包前端npm run build,生成 dist 目录后上传服务器;再打包后端mvn package -DskipTests,上传 jar 包后用 nohup 启动。最关键的坑是:改了代码不要只覆盖 jar 包,一定要重启进程;Nginx 改动后要nginx -s reload而不是重启,否则会打断现有连接。

我在服务器上还配了一个简单的启动脚本:

#!/bin/bash nohup java -jar /data/taowu/taowu-api.jar --server.port=8080 > /data/taowu/logs/out.log 2>&1 &

日志文件一定要留,线上程序出问题、靠打印 debug 都比什么都快。

7. 给正在做同类毕设/课设的人的几条实在建议

如果让我重做一遍这题目,我会在动手前先做三件事:

一是把功能边界砍到最小闭环。注册登录、商品发布、商品列表、下单、订单管理,这五个功能串起来,就是一个能完整演示的平台。收藏、消息、后台统计属于锦上添花,做完核心再去加,时间不够时先保证闭环不破。

二是不追求真实支付和真实短信。模拟支付完全可以演示业务逻辑,短信验证码也可以做成"万能验证码"方便测试。把精力花在数据设计和前后端交互上,答辩效果好得多。

三是每做一个模块就立刻自测,不要等所有功能写完再统一测。我吃过这个亏:全部代码写完后联调,前端接口字段对不上、后端返回结构不统一,一晚上都在改返工。如果每完成一个接口就马上用 Postman 测试,再让前端页面接一次数据,问题当场就会暴露。

我在整个项目里最满意的一个决定,是坚持用统一返回体和无条件加状态校验。项目做完回头看,代码量并不算多,但结构清晰度确实保证了一件事:无论答辩评委问哪个环节的流程,我都能自信地说清楚数据从哪个表来、状态如何流转、前端怎么展示。这,才是这类综合型项目真正想考核的东西。

RELATED READING

延伸阅读

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