news 2026/10/7 10:25:59

SpringBoot+Vue二手滑板交易系统:从数据库设计到部署实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
SpringBoot+Vue二手滑板交易系统:从数据库设计到部署实战

滑板圈子里有个很实在的现象:装备的流通速度比大多数运动器材都快。原因不复杂——动作练到一定程度,板面磨穿了要换,桥和轮子的损耗程度不一样要拆开来出,新手入坑又想先收一套成色好的练手,二手市场就这么被需求撑起来了。我一直在想,与其去闲鱼上和一堆通用商品混在一起,不如干脆做一个专门面向滑板装备的交易系统,把“品牌、板面宽度、桥距、轮子硬度、成色等级”这类滑板特有的属性结构化,交易流程也能做得更贴合圈子习惯。于是就有了这个用 SpringBoot + Vue 搭建的“二手滑板交易系统”项目。

这套系统前端是 Vue + Element UI,后端是 SpringBoot 2.x + MyBatis-Plus + MySQL,Redis 做缓存和会话管理,覆盖了商品发布、商品浏览与筛选、购物车、订单交易、支付模拟、个人中心、后台管理这些电商核心链路。如果你正准备做 Java 后端方向的项目练手,或者毕业设计想选一个“技术栈完整但不夸张”的题目,这个项目可以当一份直接能复现、能跑通、能讲明白的参考。

我把整个项目的设计思路、库表结构、核心模块实现、踩坑实录分几块讲清楚,按这个顺序看下来,你就能知道一张订单从“买家看到滑板”到“卖家确认发货”的完整逻辑,也能明白为什么状态字段要用枚举而不是数字散着写,为什么金额必须用 BigDecimal,为什么前端打包之后能直接塞进 SpringBoot 里。

1. 项目整体设计与思路拆解

1.1 为什么选“二手滑板”这个切入点

做电商类项目,最怕的不是技术难度,而是业务模型太泛。做通用商城,商品的多规格、sku、促销、库存预警、多级分类这些全部堆上来,一个人很难在有限时间内做出深度。二手滑板这个切入点的好处在于:

  • 商品属性垂直且明确:滑板有品牌、板面尺寸、支架(桥)型号、轮子硬度、轴承等级,这些字段天然适合结构化建模,而且和“能不能卖出去”直接挂钩。
  • 价格体系简单:二手商品不搞预售、不搞满减,核心是“卖家定价 + 买家可议价”,交易链路短,适合聚焦核心流程。
  • 供需两侧需求真实:我身边玩板的同事就有“出旧板回血、收二手练招”的习惯,拿这种场景做系统,演示和答辩的时候都能讲得生动。

这样选型让系统在覆盖“商品、订单、用户”三大电商核心链路的同时,不会陷入复杂的营销逻辑,能把每个环节做扎实。

1.2 技术选型背后的理由

后端主框架我用的是 SpringBoot 2.7.x + Java 8,这个组合太成熟了,网上遇到问题基本都有现成答案。ORM 层没有用 JPA 而是选了 MyBatis-Plus,原因很直接:二手商品列表涉及大量条件组合查询(品牌 + 价格区间 + 成色 + 关键词分页),MyBatis-Plus 的 LambdaQueryWrapper 写动态条件非常顺手,而且代码量比写 XML 少得多。数据库用的 MySQL 5.7,Redis 用来做登录会话和热门的浏览计数。前端是 Vue 2 + Element UI,打包之后直接静态资源放进 SpringBoot 的 static 目录,部署时只需要一个 jar 包,省去了单独配 Nginx 的麻烦。

这套方案最大的优势是“可解释性”强。别人问“为什么用 Redis 存登录状态”,你可以从分布式会话、session 共享、token 有效期这些角度展开;问“为什么用 MyBatis-Plus”,你可以从分页插件、条件构造器、字段自动填充这些点去讲。桩都站在常规实践上,不虚不偏。

2. 数据库设计与核心表结构

2.1 用户表:别只存账号密码

用户表 базово包含 id、username、password、phone、avatar,但我在设计时加了两个字段:credit_score(信用分)和 status(账号状态),信用分给后续“卖家权限、交易互评”预留了入口,账号状态则用于后台封禁管理。密码存储这里要提醒一句:明文存储是绝对红线,工程实践最低要求也是 MD5 加盐,我建议直接用 BCrypt,Spring Security 自带的 BCryptPasswordEncoder 就可以了,不用自己造轮子。

用户表的字段规划大致如下:

字段类型说明
idbigint用户ID,雪花ID
usernamevarchar(64)唯一用户名,登录账号
passwordvarchar(128)BCrypt 加密存储
phonevarchar(20)注册手机号,做唯一索引
avatarvarchar(255)头像图片地址
credit_scoreint信用分,默认100
statustinyint1正常,0冻结

之所以把 username 和 phone 都设唯一索引,是因为登录和找回密码两条链路都用得到这两个字段,查询频率高,索引是必需的。

2.2 商品表:把滑板的规格字段做规范

商品表是整个系统的核心。除了常规的 title、price、description、images、status、view_count,我专门设计了一组滑板垂直字段:brand(品牌)、board_width(板面宽度)、truck_model(支架型号)、wheel_hardness(轮子硬度)、condition_level(成色等级 9成新/7成新/有明显磨损)、category(整板/板面/桥/轮子配件)。二手交易最在意的就是成色和配置,这两类信息做进结构化字段后,列表页就能直接按条件筛选,比在长文本描述里搜索可靠得多。

商品表核心字段:

字段类型说明
idbigint商品ID
user_idbigint发布者ID,关联用户表
titlevarchar(128)商品标题
brandvarchar(64)滑板品牌
board_widthvarchar(32)板面宽度(如 8.0/8.125 英寸)
truck_modelvarchar(64)支架型号
wheel_hardnessvarchar(32)轮子硬度(如 101A)
condition_leveltinyint成色等级,1-5
pricedecimal(10,2)售价
statustinyint1在售,2下架,3已售出
imagesvarchar(2000)图片地址,逗号分隔
view_countint浏览数
create_timedatetime发布时间

status 字段单独拿出来说一下。商品状态从“在售”到“已售出”不是一步到位的,中间要经过“买家下单”这个中间状态,对应我后面会讲的“锁定库存”逻辑。数据库里的状态值只存数字,业务含义靠枚举类去映射,这是为了杜绝魔法数字散落各处。

2.3 订单与购物车:交易链路的关键载体

订单表的设计要点是“冗余”。买家、卖家、商品快照这三者在订单里都必须有,买家可以用 user_id + buyer_id 区分,seller_id 是发布者冗余进去的,因为同一个商品的买卖双方都是平台注册用户。我额外加了 goods_snapshot(下单时的商品快照,JSON 字符串),这是二手交易项目的重点——滑板可能被卖家改过价格或下架,但买家下单那一刻看到的配置和价格需要定格下来,否则产生纠纷时没有依据。

购物车表比较常规:user_id、goods_id、create_time,加一个唯一索引 (user_id, goods_id) 防止重复加车。订单表结构简化如下:

字段类型说明
idbigint订单ID
order_novarchar(64)订单号,唯一索引
buyer_idbigint买家ID
seller_idbigint卖家ID
goods_idbigint商品ID
goods_snapshottext下单时的商品快照,JSON
amountdecimal(10,2)实付金额
statustinyint订单状态机,见下节
pay_typetinyint1微信,2支付宝,3模拟支付
receiver_name / receiver_phone / receiver_addressvarchar收货信息
create_timedatetime下单时间

订单状态我用了 0待支付、1待发货、2待收货、3已完成、4已取消、5售后申诉,状态流转严格按照“待支付→待发货→待收货→已完成”的顺序,任意跳转都要做合法性校验。

3. 后端核心模块实现

3.1 登录注册与全局会话

会话这块我用了 Redis + Token 模式。用户登录成功后生成 UUID token,以 token 为 key、用户 ID 为 value 存入 Redis,并设置 7 天过期时间。前端后续请求在 Header 里带 Authorization,后端通过拦截器统一解析 token,从 Redis 拿到用户信息后放入 ThreadLocal,方便业务代码随时取当前登录用户。

这样做而不是直接用 HttpSession,核心原因是前后端分离后,Session 跨域处理比较麻烦(要配 CORS 的 allowCredentials,还要保证前端 withCredentials 对齐),而 Token 模式天然跨域无状态,后端只有 Redis 需要保持会话数据,扩展出水平部署节点时也不会断登录。

登录接口的典型实现,核心代码如下:

@PostMapping("/api/auth/login") public Result login(@RequestBody LoginDTO dto) { User user = userService.findByUsername(dto.getUsername()); if (user == null || !BCryptPasswordEncoder.matches(dto.getPassword(), user.getPassword())) { return Result.error("用户名或密码错误"); } String token = UUID.randomUUID().toString().replace("-", ""); redisTemplate.opsForValue().set("token:" + token, user.getId().toString(), 7, TimeUnit.DAYS); return Result.success(token); }

拦截器部分要注意放行路径。登录接口、注册接口、商品列表页和商品详情页是公开的,下单、购物车、个人中心全部要拦截。我会用一个 PathPattern 的 exclude 列表管理放行路径,新增接口时最常漏的就是忘记加白名单导致线上 401 排查半天。

3.2 商品发布与图片上传的实操细节

商品发布是卖家操作的第一步,流程包含:填写滑板规格信息、上传图片、设置价格和成色、提交入库。后端校验的核心有两点:一是 title 不能为空、价格必须在 0.01 到 99999 之间;二是图片数量限制在 5 张以内、单张不能超过 5MB。

图片上传这一块,我推荐直接走本地上传 + 虚拟静态映射。在 application.yml 里配置 upload.path,配合一个 WebMvcConfigurer 把本地目录映射为 /images/** 的访问路径:

upload: path: /data/skate-market/images/
@Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler("/images/**") .addResourceHandler("file:" + uploadPath + "/"); }

这里有一个必须要处理的坑:上传的图片要按日期分目录存储,文件名不能保留用户原始文件名,否则可能出现中文乱码、重名覆盖、非法字符穿透目录这些风险。我统一用 UUID 重命名,目录结构形如 /20240312/uuid.jpg,既便于排查又避免冲突。如果需要生产级方案,直接把存储抽象成 OSS 或 MinIO,流程不变,只替换存储实现即可。

3.3 商品列表查询与筛选:MyBatis-Plus 条件构造的实战

二手滑板列表页的筛选条件很典型:品牌、价格区间、成色、搜索关键词、排序(最新/价格/浏览量)。用 MyBatis-Plus 的 LambdaQueryWrapper 实现起来非常清爽:

Page<Goods> page = new Page<>(pageNum, pageSize); LambdaQueryWrapper<Goods> wrapper = new LambdaQueryWrapper<>(); wrapper.eq(StringUtils.hasText(brand), Goods::getBrand, brand) .eq(conditionLevel != null, Goods::getConditionLevel, conditionLevel) .between(minPrice != null && maxPrice != null, Goods::getPrice, minPrice, maxPrice) .and(StringUtils.hasText(keyword), w -> w.like(Goods::getTitle, keyword).or().like(Goods::getDescription, keyword)) .eq(Goods::getStatus, GoodsStatus.ON_SALE.getCode()) .orderByDesc(sort == 1, Goods::getPrice) .orderByDesc(sort == 2, Goods::getViewCount); return goodsMapper.selectPage(page, wrapper);

这套写法的优势是“条件与参数解耦”:某个筛选项不传就是 null,wrapper 的 condition 参数自动忽略这一条件,不会拼出错误的 SQL。综合排序我默认按 create_time 倒序。注意“最新发布”本身也要建立在 status=在售 的基础上,不能把已下架的商品展出来。

3.4 购物车下单与订单状态机

购物车功能是常规的增删改查,但“从购物车创建订单”这一步是整个系统的核心事务场景。流程是:勾选购物车商品 → 后端校验商品状态 → 冻结商品(status 变更为“锁定中”)→ 生成待支付订单 → 支付成功后卖家发货。

这里事务设计最容易翻车。如果只锁商品、生成订单分两步执行且不在同一事务中,用户支付超时取消订单后商品状态恢复就容易出现并发问题。我的做法是:在同一个事务方法里完成“创建订单 + 更新商品状态”,商品状态采用乐观锁版本号控制:

@Transactional(rollbackFor = Exception.class) public Order createOrder(Long goodsId, Long buyerId) { Goods goods = goodsMapper.selectByIdForUpdate(goodsId); if (goods == null || goods.getStatus() != GoodsStatus.ON_SALE.getCode()) { throw new BizException("商品不存在或已下架"); } // 生成订单号: 时间戳 + 用户ID + 随机数 String orderNo = generateOrderNo(buyerId); Order order = buildOrder(goods, buyerId, orderNo); orderMapper.insert(order); // 锁定商品 goods.setStatus(GoodsStatus.LOCKED.getCode()); goodsMapper.updateById(goods); return order; }

selectByIdForUpdate 用数据库行锁防并发下单,两个买家同时点“立即购买”时只有一个能成功,这是这个流程里我踩过坑后加上的关键一行。如果是纯代码层面的 synchronize 或者分布式锁,在这个单体项目里都能解决问题,但行锁是最贴合数据库语义、最容易讲清楚的一种。

支付流程我用的是模拟支付:买家点“支付”后,后端模拟第三方支付平台回调,直接更新订单状态为“待发货”,同时生成一笔流水记录。接真实支付不需要改表结构,只需在 pay 接口换成调用微信/支付宝下单 API,再写一个回调接收方法把订单状态推进即可。

3.5 后台管理:商品审核与会员管理

后台管理端我给管理员配置了几组核心操作:商品列表审核(批量下架违规商品)、用户列表(冻结账号)、订单总览、数据看板。因为项目不大,后台不单独做权限框架,用 role=1 的字段标记管理员即可。但要注意一点:后台所有写操作必须校验当前用户角色,不能仅靠前端隐藏入口,接口层也要拦截。用自定义注解 @RequireAdmin + 拦截器实现,逻辑很直接。

数据看板部分我做了三个统计:今日新增用户、在售商品总数、今日订单金额,用就是 SUM 和 COUNT 的 SQL,配合 ECharts 画简单折线图。

4. 前端页面与工程化部署

4.1 页面清单与路由设计

前端页面我按用户链路拆成了六个核心页面:

  • 首页:商品流推荐,展示在售滑板
  • 商品详情页:滑板规格、成色、卖家信息、立即购买
  • 发布商品页:滑板信息表单 + 图片上传
  • 购物车页:购物车商品数量与下单
  • 订单列表页:买家订单、卖家订单分 Tab 展示
  • 个人中心页:个人信息、地址管理、我发布的商品

路由设计遵循“懒加载”原则,每个页面单独走 vue-router 的 component: () => import(...),首次加载体积会明显小。axios 封装了 request 拦截器,统一从 localStorage 取 token 注入 Header,响应拦截器统一处理 401 跳转登录。

4.2 Vue 打包后塞进 SpringBoot:一条命令搞定部署

开发调试时前后端分离跑两个端口,需要配前端代理 /api 到后端 localhost:8080。但生产环境更省事的方式是让前端把路由刷成 history 模式,然后执行 npm run build,生成 dist 目录后把里面的 static 和 index.html 复制到 SpringBoot 的 src/main/resources/static 下。重新打包 jar,一个进程跑起整站,前端路由刷新 404 的问题也不会出现。

如果只是复制文件,SpringBoot 默认的静态资源处理就能覆盖掉。要注意的地方是 Vite 或 Webpack 配置的 publicPath 必须改成相对路径 "./",否则打包后的 JS/CSS 路径是绝对路径,部署到子路径时全部 404。

前端部署的关键命令汇总:

npm run build cp -r dist/* src/main/resources/static/ mvn clean package -DskipTests java -jar target/skate-market.jar

4.3 跨域与接口调试的经验

开发环境跨域,前端代理就能解决,不需要后端开启全局 CORS。如果你确实要在后端配 CORS,注意两点:allowedOriginPatterns 不能配 "*" 之后再 allowCredentials(true),这会直接报错;而且不要把 CorsFilter 注册在拦截器之前,否则带 Authorization 的请求会被拦截器拦截成 401,排查起来非常隐蔽。

我用 Chrome DevTools 的 Network 面板调试时,习惯把 jwt token 手动填到 Authorization Header 里测试受保护接口,这样能快速定位是后端逻辑问题还是前端没带 token 的问题。SpringBoot 项目里配置了统一的异常处理 @RestControllerAdvice 之后,接口返回的错误信息就能做到规范统一,前端直接弹 Message 提示即可。

5. 常见问题与排查技巧实录

5.1 BigDecimal 与精度问题

金额计算是电商项目的第一位的坑。如果金额字段用 double 存储,计算折扣、分摊、统计时会出现 0.1 + 0.2 != 0.3 这种经典问题。我在项目里所有涉及金额的字段都用 decimal(10,2),Java 侧用 BigDecimal,枚举里写金额常量也用 BigDecimal.valueOf。价格比较时一律用 compareTo 而不是 equals,因为 equals 会连小数位一起比较。这些细节在代码评审里是高频扣分点,但很多人直到线上出账不平才回过头来改,代价就很大。

5.2 事务失效的三种隐性场景

事务这块我有过三次教训,列出来给你提个醒:

  • 同一个类内部方法调用,比如 saveOrder 调用 sendMessage,sendMessage 加 @Transactional 并不会生效,因为 Spring AOP 代理默认不拦截自身调用。解决方法是分离 Service 或者注入自身代理。
  • 事务方法内部 try-catch 吞掉了异常,事务不会回滚。rollbackFor 默认只能捕获 RuntimeException 和 Error,如果你 catch 了所有异常却没手动回滚,事务就悄悄提交了危险数据。
  • 表引擎不是 InnoDB,事务直接失效。MySQL 5.7 默认是 InnoDB 不用管,如果碰到 MyISAM 的表,要注意。

5.3 商品状态不同步问题

最初版本我没有“锁定中”这个状态。两个买家同时下单时,A 买家创建了订单,B 买家还能继续下单,最后两个订单指向同一件滑板,卖家根本没有能力履约。加上“锁定中”状态之后,状态流转就清晰了:在售(1)→ 锁定中(2)→ 已售出(3),订单取消则从 2 回到 1;只有状态为 1 时才能被下单。这个模型是所有交易系统的基本功,花几天时间慢慢吃透,比多写一百个 CRUD 接口有价值。

5.4 SpringBoot 版本与依赖冲突速查

我项目里用的 SpringBoot 2.7.x,搭配 MyBatis-Plus 3.5.x。新手最容易踩的是 MyBatis-Plus 版本和 SpringBoot 版本不兼容,导致启动时自动装配报错。通用经验是:SpringBoot 2.x 配 MyBatis-Plus 3.4 以上,SpringBoot 3.x 配 MyBatis-Plus 3.5.4 以上,同时注意 javax.* 和 jakarta.* 包名差异。另一个高频问题是 Lombok 版本过低会报 java.lang.ClassNotFoundException,把 Lombok 升到 IDEA 推荐的当前版本即可。

5.5 图片上传失败的常见原因

图片上传功能最容易出错的实际不是代码,而是三个外部因素:目录没有写权限、请求大小超过 SpringBoot 默认 max-file-size(默认 1MB)、Nginx 或网关配置了 body 大小限制。我的配置是把上传大小放宽到 10MB,指定了合理的 upload.path 并且确保目录存在,同时给 upload 目录设置可写权限。排查时可以看后端日志有没有 MultipartException,有的话基本就是这三个原因。

6. 把这个项目往深做的三个方向

如果你想把项目继续做深、做出差异化,我列三个方向供参考:

  • 交易保障和申诉流程:最常见的方向。买家收货后发现“描述与实物不符”时,增加申诉入口和客服介入流程,这会让系统比普通商城更有“平台感”。
  • 议价与消息通知:二手交易场景里,“小刀(砍价)”是高频行为。增加订单议价列表和站内信通知,业务闭环更完整。
  • 推荐系统简单版:根据用户历史浏览和收藏记录,按品牌、预算做协同过滤或标签匹配,在首页做“你可能感兴趣”,这个方向适合想冲算法实习的同学。

项目做完之后,我个人最大的体会是:做一个二手交易系统,核心难点不在 CRUD,而在于状态机的严谨设计、并发下的数据一致性、以及金额精度这种基础但绝对不能错的基础工程习惯。滑板商城这个垂直场景让这些通用问题有了实在的载体,无论是面试讲项目还是写论文做分析,都会有很清晰的落脚点。希望这份拆解能帮你少走点弯路,也欢迎你在这个框架上继续加自己的创意。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/7 10:25:56

差分数组经典应用:从“最高的牛”理解区间更新与前缀和

说实话&#xff0c;第一次拿到这题的时候&#xff0c;我盯着题目愣了好一会儿。题目描述绕来绕去的&#xff0c;又是"最高的牛"又是"互相看见"&#xff0c;乍一看跟差分数组八竿子打不着。但等我把条件翻译完&#xff0c;才发现这就是差分的一个标准模板题…

作者头像 李华
网站建设 2026/10/7 10:25:50

Modbus RTU单报文收发:协议边界与CRC校验实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/7 10:25:47

UE编辑器工具开发:用HighlightPickedActors实现视口点选高亮

做自定义编辑器工具时&#xff0c;最常遇到的一个需求就是&#xff1a;让用户在关卡视口里点一下某个物件&#xff0c;工具立刻把这个物件高亮出来&#xff0c;然后拿着这个物件去干后续的活——批量改材质、收集资产信息、检查贴图尺寸&#xff0c;诸如此类。 这个动作在运行…

作者头像 李华
网站建设 2026/10/7 10:24:29

Hot 100普通数组刷题笔记:六道高频面试题的边界与复杂度解析

如果你准备面试或者正在刷题&#xff0c;LeetCode Hot 100应该是绕不开的一份清单。这份榜单把高频面试题按数据结构分成了十几个分区&#xff0c;其中“普通数组”这一栏很不起眼&#xff0c;题量不大&#xff0c;也不涉及链表、树、图这些复杂结构&#xff0c;但它是我刷了三…

作者头像 李华
网站建设 2026/10/7 10:23:21

从Docker到Kubernetes:容器化部署到集群运维的实战排错指南

如果你已经能熟练地写 Dockerfile、能跑通docker-compose up -d&#xff0c;甚至习惯了把 MySQL、Redis 都塞进容器里跑&#xff0c;那说实话&#xff0c;单机容器化这一关你已经过了。但"阶段二"的挑战&#xff0c;恰好是从你试图把这些经验搬到 Kubernetes 集群里那…

作者头像 李华
网站建设 2026/10/7 10:23:20

SpringBoot+Vue问卷系统实战:从表设计到部署避坑指南

做一个基于SpringBoot的调查问卷系统&#xff0c;听起来像是毕业设计里最经典的那类选题&#xff0c;但实际上手之后你会发现&#xff0c;它远没有题目看起来那么“标准”。问卷要支持多少种题型、答案怎么存才能方便统计、如何防止同一个人重复提交、前端怎么和一个后端工程打…

作者头像 李华