
1. 项目概述与需求拆解做这个二手滑板交易系统最开始的起因其实特别简单我自己玩滑板有几年了身边一圈板友经常发愁换板麻烦。一块板面玩废了想便宜出给新手却没有合适的渠道新手想低价入门又怕买到暗伤板。市面上综合二手平台太杂滑板的参数、磨损程度、配件改装情况完全没人关心。所以我就想自己动手做一个基于Spring Boot的垂直类二手滑板交易平台把所有跟滑板交易相关的核心流程都跑通。系统的核心定位就是“滑板商城销售系统”但跟普通商品秒杀、标品电商不同二手滑板这个领域有很明显的垂直属性买家要看板面宽度、脚窝深度、支架尺寸、轴承等级、轮子硬度这些参数不是随便写一句话就能描述清楚的。所以我在需求阶段花了不少功夫去拆解“二手滑板”这个特殊商品的信息模型而不是直接把通用商城系统搬过来改个名。1.1 核心需求解析二手滑板交易系统面向两类用户滑手卖家和滑手买家实际上同一批人经常两边切换。这决定了系统不能做成传统B2C那样“商家后台用户前台”的隔离模式而应该是C2C模式每个注册用户既可以发布闲置也可以浏览和购买别人的闲置。我从真实使用场景倒推需求整理出这么几条硬性要求商品信息必须支持滑板专业参数能让卖家像填表一样把板面、支架、轮子、轴承分开描述。交易流程要覆盖“发布 - 浏览 - 下单 - 付款 - 发货 - 确认收货 - 评价”的完整闭环缺一环都不行。既然涉及金钱交易必须考虑订单超时自动取消、交易状态下单防重复、库存扣减防超卖这些问题。二手商品有天然的唯一性一件商品只能卖给一个人不能像普通SKU商品那样随便堆库存。另一个被很多人忽略的点是二手交易最大的痛点不是支付而是信任。所以系统在设计上必须包含用户信用评分、商品成色描述、交易评价这几个模块。我甚至在商品的详情页强制要求卖家选择成色等级全新、轻微磨损、明显划痕、战斗成色不允许含糊带过。1.2 功能模块边界划分这个系统从前端到后端都是我一个人写的所以功能模块的划分必须既符合逻辑又能让我在开发时不至于手忙脚乱。我最终把整个系统拆成六大模块用户模块注册、登录、个人资料、钱包余额、信用积分。商品模块商品发布、编辑、上下架、滑板参数填写、图片上传、商品列表检索。交易模块购物车可选我直接做了立即购买和购物车两条线、订单生成、支付、发货、收货、退款。消息模块站内信、交易状态变更通知。后台管理模块管理员对用户、商品、订单、举报进行审核处理。数据统计模块交易额、成交量、热门品牌、价格区间的粗略统计。在技术选型上后端采用Spring Boot 2.7.x作为主框架持久层用MyBatis-Plus数据库用MySQL 8.0缓存用Redis。前端部分用的是Vue 3 Element Plus但前后端完全分离接口走RESTful风格。之所以选择Spring Boot最大的原因是它把配置、事务、对象管理做得足够省心我一个人写后端不需要花时间在XML配置和Web容器上。1.3 技术选型背后的原因在选择Spring Boot版本的时候我特意避开了最新的3.x用了2.7.x。这倒不是追新不如求稳而是Spring Boot 3.x强制要求JDK 17并且把javax.servlet迁移成了jakarta.servlet我的服务器上部署环境是JDK 1.8所以直接选2.7.x最省事完全不用考虑兼容性迁移的问题。对于个人项目来说稳定跑起来比什么都重要。持久层选MyBatis-Plus纯粹是看中它的LambdaQueryWrapper写条件查询很顺手比如滑板参数筛选这种动态SQL场景用MyBatis-Plus的wrapper可以少写大量XML。Redis在这里的角色也很明确存登录token、做热点商品缓存、存临时订单的倒计时信息。支付这块我没有真的对接第三方支付因为个人项目拿不到商户号所以做了一个模拟钱包支付内部记录流水这就够了。2. 数据库设计与核心表结构二手滑板交易系统的数据表设计是整个项目里最需要提前想清楚的部分。如果只按通用电商的思路建表后面写业务代码时一定会被各种字段缺失折磨。我设计表时反复问自己一个问题每个表到底要为哪些业务场景服务答案清楚了再动手。我实际建了12张核心表包括user用户表、user_wallet钱包表、user_credit_log信用流水表、product商品主表、product_param滑板参数表、product_image图片表、cart购物车表、orders订单表、order_item订单项表、payment_log支付流水表、message消息表、evaluation评价表。下面挑最关键的几张详细讲。2.1 用户、商品、订单表用户表我把它设计成跟信用体系绑定的结构因为二手交易平台上的买家卖家实际上都是同一个角色信用分必须能双向影响。用户表核心字段有id、username、password_hash、phone、avatar_url、credit_score默认100、status正常/封禁、created_at。密码存储用的是BCrypt加密我在Spring Security里直接集成的这一步别偷懒用MD5明文和MD5在泄露后基本等于裸奔。商品表是这套系统里最复杂的因为二手滑板的信息维度太多。我把商品的基本通用信息放在product表里把滑板专业参数单独拆到product_param表这样将来如果想扩展到轮滑鞋、滑板车复用性会更好。product表核心字段是id、seller_id、title、description、category长板/双翘板/鱼板/陆冲板、condition_level成色等级、brand、price单位分避免浮点误差、status0待上架 1在售 2已售 3下架 4删除、view_count、favorite_count。订单表我也没省着直接设计成主订单和订单项分离。因为一个买家可能在一次交易中同时拍下卖家的多个滑板比如整套板面加轮子一起打包买虽然实际二手交易里很少这样但结构上必须支持。订单表orders里记录订单号、买家ID、卖家ID、总金额、订单状态、支付时间、发货时间、收货时间。order_item表记录每个商品ID、价格、数量并冗余了商品标题和图片快照防止卖家在交易过程中修改商品信息后订单里看不到当时买的是啥。2.2 状态机与交易流程订单状态是整个系统的核心状态机我在代码里用了枚举类OrderStatusEnum来定义一共六个状态PENDING_PAYMENT待支付、PAID已支付待发货、SHIPPED已发货待收货、COMPLETED已完成、CANCELLED已取消、REFUNDING退款中。状态机的流转规则我用一张约束表固定下来禁止状态跳跃。比如只有PENDING_PAYMENT能取消只有PAID才能发货只有SHIPPED才能确认收货。这里最大的坑是状态变更时必须考虑并发问题。两个线程同时读取同一笔订单都认为可以发货结果就会产生脏数据。我最终的解决办法是在“确认收货”和“取消订单”这类关键操作上增加version乐观锁字段UPDATE orders SET status #{newStatus}, version version 1 WHERE id #{orderId} AND version #{oldVersion}更新影响行数为0就说明状态已经变了直接抛出业务异常。2.3 索引与唯一约束建表时最容易忽略的就是索引。我一开始没有给product表的category和status加组合索引结果测试数据加到两万条后列表页按分类筛选明显变慢。后来加了组合索引(category, status, created_at)查询响应时间从几百毫秒降到了几十毫秒。另一个必须加唯一约束的字段是订单号。我在生成订单号时用了yyyyMMddHHmmss 随机6位的方式理论上重复概率很低但数据库层面我还是加了UNIQUE KEY兜底。一旦插入时发生主键或唯一键冲突就重新生成订单号再试一次这个机制写在了公共的订单创建方法里。3. 核心业务逻辑实现整个系统里最花精力的是三个业务功能商品发布时的参数处理、下单时的库存扣减和防重复支付、以及超时订单的自动处理。这三个功能任何一个出问题都会直接影响用户的真金白银必须想清楚再动手。3.1 发布商品与图片上传发布商品的接口入参结构大概长这样基础信息字段标题、描述、分类、品牌、价格、成色加上滑板参数对象板面长度、宽度、脚窝深度、支架尺寸、轮子硬度、轴承精度再加上图片列表。我用Valid注解做参数校验price字段用NotNull和DecimalMin(value 0.01)滑板参数里的数值字段用Min和Max做范围限制比如轮子硬度范围是78A到101A脚窝深度填负数这种脏数据直接拦在接口层。图片上传我用了本地磁盘存储方式没有接入OSS。因为个人项目没有云存储资源所以我的做法是上传接口接收MultipartFile服务端生成一个UUID文件名校验扩展名只允许jpg、jpeg、png格式大小限制在5MB以内然后存到服务器指定目录同时把访问URL返回给前端。生产环境如果图片量大这个方案一定会遇到磁盘扩容问题但项目阶段完全够用。这里必须提醒一下图片校验不能只靠前端前端校验只能提升用户体验服务端必须重新校验文件类型而且要避免直接信任Content-Type最好用FileMagic或者读取文件头部字节判断真实格式否则会被上传恶意脚本文件。3.2 交易流程下单、支付、确认收货下单流程我走的是“预下单 真实扣减”两步。预下单阶段只创建PENDING_PAYMENT状态的订单同时把商品状态status从“在售”改成“锁定中”这一步是通过UPDATE product SET status 2 WHERE id #{id} AND status 1完成的条件更新只有影响行数为1才说明锁单成功否则提示“商品已被拍下”。这个条件更新实际上就是一把数据库层面的乐观锁比在应用层先查后改要可靠得多。支付流程用的是模拟钱包。用户下单后必须在一定时间内完成支付我设置了30分钟的支付倒计时。前端用JavaScript计时器提醒用户真正到了后端执行超时关单用的是Redis的SET key value EX 1800特性把订单号作为key支付成功或取消时删除key。如果用户一直不支付等到30分钟到期会触发定时任务扫描Redis里已过期的订单key把订单状态改成已取消同时把对应的商品状态从“锁定中”改回“在售”。定时任务用的Spring的Scheduled注解这里要非常注意多实例部署时定时任务会重复执行必须用分布式锁我用Redis的setIfAbsent实现保证只有一台机器在跑任务。确认收货的过程更关键这里涉及资金流转买家点击确认收货后订单状态从SHIPPED变成COMPLETED同时卖家钱包余额增加订单金额。这两步必须放在同一个事务里而且加Transactional只能保证数据库事务钱包余额的更新我用的是UPDATE user_wallet SET balance balance #{amount} WHERE user_id #{userId}这样的乐观更新不做先查询再计算彻底避免余额更新的并发覆盖问题。3.3 滑板参数检索与筛选二手滑板商城跟普通电商最大的区别就在这用户会精确筛选“双翘板、轮子硬度80A以上、价格300到600区间”。而MyBatis-Plus的LambdaQueryWrapper玩动态条件查询确实方便但参数组合多了以后代码会写得非常冗长。我最终把检索逻辑拆成了两部分基础条件放SQL里滑板参数条件通过动态拼接SQL实现。具体做法是在ProductService里定义searchProduct(param)方法接收一个ProductSearchDTO对象DTO里包含category、brand、minPrice、maxPrice、conditionLevel以及滑板参数相关的minWheelHardness、maxWheelHardness、deckWidth、truckSize等字段。如果这些参数不为空就通过apply方法拼接动态SQL条件跟product_param表进行JOIN。这里有个性能细节如果滑板参数筛选条件为空就不要JOIN product_param表否则会导致大量无谓的关联查询拉低列表页整体性能。列表页的分页我用的是MyBatis-Plus的分页插件配置了PaginationInnerInterceptor后只要在Service层传入Page对象框架自动生成LIMIT语句用起来确实舒服。但要注意分页的时候不能把总记录数的SELECT COUNT(*)直接放在每次查询里跑如果数据量大这个count查询会拖慢响应。我的做法是对列表页加了Redis缓存缓存key为product:list:页码:筛选参数缓存时间120秒数据变更时主动删除相关缓存。二手商品列表的实时性要求不高这种缓存策略完全可行。4. 安全机制与并发控制安全问题不是聊天功能上线后才考虑的我从一开始就把登录鉴权、越权访问、接口防刷这些机制加进去了。做这个项目的过程也让我体会到一个道理安全框架不是越复杂越好而是要让所有接口都有默认的安全边界而不是靠开发人员自觉。4.1 登录鉴权与越权防护登录鉴权我用了JWTJSON Web Token方案不走Session。核心逻辑是用户登录成功后服务端生成一个包含用户ID、用户名、过期时间我设为24小时的token用HMAC-SHA256算法签名后返回给前端。前端在每个请求的Authorization头带上Bearer token后端通过一个OncePerRequestFilter过滤器拦截所有/api/**请求解析token并获取当前登录用户。这里有一个越权防护的关键点对product、order这类资源的所有修改操作我都必须校验当前登录用户是否是资源的所有者。比如删除商品接口第一件事就是从token里取出userId再去数据库查这个商品的seller_id是否一致不一致直接返回403。不能只靠前端隐藏按钮来防越权因为攻击者完全可以手动构造请求。角色权限方面我用到了Spring Security的PreAuthorize注解做接口级控制。管理员接口统一加上PreAuthorize(hasRole(ADMIN))普通用户接口不额外加限制但数据隔离靠代码逻辑来处理。权限这块没必要去追求RBAC全套小项目用了反而增加复杂度。4.2 接口防刷与限流滑板商城上线后会面对很多爬虫和恶意攻击尤其是商品价格查询、用户信息查询这些接口很容易被高频调用。我用了最简单的限流策略基于Redis的incr实现计数器限流。每个接口在Redis里维护一个key比如rate:api:/product/detail:user:userId:currentSecond每次请求对该key执行INCR并设置过期时间为1秒。如果请求数超过上限比如同一个用户对商品详情接口每秒最多5次直接返回“请求过于频繁”的提示。这个方案的优点是代码量少、部署后随时可调阈值缺点是使用Redis时如果有大量请求并发INCR同一keyRedis性能会有压力不过单机项目完全够用。接口防刷对二手交易系统的意义不只是防止服务器过载更重要的是防止恶意用户大量遍历商品接口把别人的手机号和卖家信息收集走。所以我在买家查看卖家信息时也做了额外的脱敏处理中间四位手机号用星号代替。4.3 事务一致性与分布式锁订单相关的操作频繁涉及多表更新比如下单时要INSERT orders、更新商品状态、扣减卖家的钱包虽然这里不这么设计、记录库存流水。这几步必须在一个数据库事务里完成否则一旦中间某一步失败会出现买家订单创建了但商品状态没改的脏数据。我在Service方法上加Transactional(rollbackFor Exception.class)并且特意把rollbackFor设置成所有异常因为Spring默认只回滚运行时异常如果捕获了受检异常却不抛出去事务是不会回滚的这点坑我踩过一次。对于超时关单和用户手动取消这两个操作它们都涉及“修改订单状态 恢复商品状态”这两个动作极端情况下可能同时执行导致订单状态在两边来回横跳。我用了一个简单的处理方式在订单表加一个version字段执行关单或取消时都先校验版本号。另外关单任务和前台取消操作处理同一笔订单时两个人同时拿到同一版本号但只有一个UPDATE会成功另一个因为版本号已变而失败从而保证了状态只会被修改一次。5. 部署配置与测试记录到这一步功能逻辑已经完整了但项目真正跑起来还需要解决环境配置、打包部署这些问题。Spring Boot虽然在开发阶段很省心但一些细枝末节配置不到位照样会出现线上和本地表现不一致的情况。5.1 本地运行环境准备我的开发环境是JDK 1.8、Maven 3.6.3、MySQL 8.0.29、Redis 6.2.6IDE用的IntelliJ IDEA。Spring Boot版本2.7.12引入的依赖包括spring-boot-starter-web、spring-boot-starter-security、mybatis-plus-boot-starter、mysql-connector-j、spring-boot-starter-data-redis、jjwt、hutool工具类库。配置方面最需要注意的是数据库连接池配置。我用的是HikariCPSpring Boot默认在application.yml里设置了maximum-pool-size: 10、minimum-idle: 2、connection-timeout: 30000。一开始没配maximum-pool-size开发时感觉不出问题后来用Jmeter连续压测两百个并发请求发现大量请求直接报连接获取超时原来是默认池大小10不够。调整到20后压测结果明显好了。不过连接池也不是越大越好MySQL的默认连接数是151池子设太大反而会把数据库拖垮个人项目设置10~20完全足够。5.2 常见问题与排查技巧实录开发过程中我记录了几个比较典型的坑拿出来分享都是网上教程不会细讲但实操一定会碰到的Spring Boot启动报Consider defining a bean of type xxx in your configuration。这个问题大多数是Mapper接口没加Mapper注解或者启动类没扫到Mapper包。解决办法有两个在启动类加MapperScan(com.skate.mapper)或者在每个Mapper接口上标注Mapper注解。两种方式等价我推荐用MapperScan因为更集中且不容易漏。MyBatis-Plus分页不生效。低版本的MyBatis-Plus需要手动配置PaginationInnerInterceptor不是引入依赖就自动分页的。我把配置类写好放到config包里之后才正常。版本上我用的3.5.3.1注意与Spring Boot 2.7兼容。返回给前端的日期格式不对。MySQL的datetime在Jackson序列化时会变成时间戳数组前端显示乱七八糟。我在application.yml里配置了spring.jackson.date-format: yyyy-MM-dd HH:mm:ss和time-zone: GMT8就解决了。Transactional没生效。最典型的原因是方法被其他类调用这个方法没有走代理导致事务注解失效。比如OrderService里的createOrder方法内部调了同类的dealAfterOrderPaid方法事务就管不住后者。解决方法是拆分到不同的Service类或者用AopContext.currentProxy()但最简单的还是把内部方法抽到别的Service里保持清晰的调用链。跨域问题。前端和后端端口不同必然会产生跨域。我在后端配置类里实现了WebMvcConfigurer的addCorsMappings方法设置allowedOriginPatterns(*)、allowedMethods(GET,POST,PUT,DELETE,OPTIONS)同时开启allowCredentials为true。注意开启allowCredentials后allowedOrigins不能写*必须用allowedOriginPatterns(*)否则浏览器会拒绝携带凭证的请求。JWT过期后前端无法正常跳转登录页。我在后端过滤器里对token解析失败的情况统一返回401前端在axios拦截器里收到401后清除本地token并跳转登录页。这个链路必须配置通否则用户token过期后点击任何操作都只会看到一个静默的失败体验非常差。5.3 压测与性能优化记录为了验证系统并发能力我用JMeter对几个核心接口做了简单压测。测试机是普通8核16G开发机数据库和Redis都在这台机器上跑压测参数是300个线程、循环5次。下单接口在没有任何优化的情况下TPS大约只有280错误率0.5%。分析后发现瓶颈集中在product表的条件更新上每次下单都要对该商品执行UPDATE ... WHERE id ? AND status 1MySQL行锁竞争激烈。我做了两处优化一是把商品状态为“锁定中”的检查从“先SELECT再UPDATE”改成直接用UPDATE的条件更新来判定减少一次查询二是在Redis里对商品加了一个简单的并发控制当商品被拍下时在Redis设置product:lock:商品ID并赋予600秒过期时间间隔几秒内新下单请求先查这个key有key就直接返回“已被拍下”。这个优化虽然不能根治并发锁但至少能挡掉大量无效的“条件更新”请求。另外一件值得提的事是日志系统。开发阶段我用的是Spring Boot自带的日志输出但上线后必须把日志落盘并划分级别。我在logback-spring.xml里配置了按天滚动、最多保留30天的策略同时把com.skate.mapper的日志级别设为DEBUG这样在测试阶段可以实时看SQL上线后改成WARN避免刷日志。6. 项目实战中的经验沉淀整个二手滑板交易系统从最初的需求梳理到最终部署上线我一个人大概花了三周业余时间。这个项目最大的价值倒不是功能多完善而是让我把一个电商交易系统从零到一的完整链路全走了一遍尤其是那些平时写CRUD完全不会接触的边界情况。印象最深的是超时关单这个需求看起来只是定时任务扫一下订单但真正落地时牵扯到分布式锁、事务补偿、状态校验好几层问题。我在测试时故意把支付倒计时改成5秒然后用两个浏览器同时操作同一件商品验证了几种极端场景一个用户下单后不支付另一个用户同时下单用户A支付成功后用户B在支付页面卡住30秒后点击支付按钮。这些场景如果没有提前设计好真实环境下一定会出现资金或库存不一致的问题。在做滑板参数筛选时我也意识到一个产品层面的问题用户搜索时很少会精确输入“78A”这种数据更多是选择“软”、“适中”、“硬”这种模糊档位。所以我在前端搜索框里做了下拉选项把轮子硬度分成三档后端再映射成对应的A值区间。这虽然不是技术难点但确实需要理解业务才能把功能做得顺手。最后再分享一个小技巧开发期间我一直用Swaggerspringfox生成接口文档前端同学对接接口非常方便。但到了正式部署时务必把Swagger关闭不然/swagger-ui/index.html接口文档直接暴露给公网等于把整个系统接口结构送给了攻击者。我是在配置类里通过Profile(!prod)限制Swagger只在非生产环境生效。这个细节很多人忽略但安全问题恰恰就在这些小地方积累形成的。项目后续如果继续迭代我会考虑引入消息队列做订单超时延迟处理比如RabbitMQ延迟队列替代现在的定时轮询扫描Redis进一步降低实时性和数据库压力商品图片存储也会迁移到云对象存储给图片加CDN加速。不过这些都是后话了现阶段这个基于Spring Boot和Java的二手滑板交易系统已经完整跑通了交易闭环也算是对自己滑板爱好和技术栈的一次双重交付。