ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

SpringBoot实战:在线家具商城系统设计、并发库存与Redis缓存方案

SpringBoot实战:在线家具商城系统设计、并发库存与Redis缓存方案 接手“基于SpringBoot的在线家具商城”这个项目的时候说实话我的第一反应不是急着去搭工程、写接口而是先问自己一个问题家具商城和普通数码商城、服装商城到底差在哪。这个问题想不清楚后面做的所有功能都是在给自己挖坑。很多人做这类系统最后做出来的东西看起来很完整商品管理、购物车、订单都有但实际经不起细问——比如家具这种大件商品怎么处理规格和定制属性下单的时候库存扣多了或者扣少了怎么办订单状态乱跳怎么办这些问题才是这个项目真正值钱的地方。这篇文章我会按我从零开始做这个项目的完整思路来写从功能边界划分、技术选型到数据库设计、后端核心链路、并发库存处理、Redis缓存引入、管理后台权限再到部署上线和踩坑记录一次性讲透。无论你是拿它当毕业设计还是想写进简历作为实战项目又或者单纯想练手SpringBoot全家桶这篇文章应该都能让你少走不少弯路。1. 功能地图与技术选型动手之前先把项目拆明白1.1 用户端和管理端到底要做什么在线家具商城这个题目听起来就是一个标准电商系统但如果你仔细拆一下会发现它的功能边界其实比想象中要大。我在做功能拆分的时候习惯先列用例把角色和操作一对一对地列出来而不是一上来就打开IDEA新建项目。用户端这边核心链路是注册登录 → 浏览商品 → 查看详情 → 加入购物车 → 提交订单 → 支付 → 查看订单状态 → 确认收货。注意这里我把“支付”写成了模拟支付因为真正的支付需要商户资质和第三方支付平台对接对个人开发者和学生项目来说不现实所以一般是用一个“模拟支付”按钮或者在后端直接标记支付成功。这块一定要在项目文档里写清楚否则答辩或者面试时容易被追问。管理端这边核心链路是管理员登录 → 商品管理上架、下架、编辑、库存维护→ 类目管理 → 订单管理发货、查看详情→ 用户管理 → 数据统计。管理端不用做花哨的图表一张简单的数据看板就够关键是订单状态的处理流程要闭环。1.2 为什么是SpringBoot全家桶加Vue这个项目的技术选型我最终定的是SpringBoot 2.7 MyBatis-Plus MySQL 8 Redis Vue 3 Element Plus。这套组合大概是目前做个人项目和毕设的主流配置好处是生态成熟、资料多、踩坑了也容易搜到解决方案。先聊SpringBoot本身。它最大的价值不是“快”而是“自动配置”这件事。你引入一个spring-boot-starter-web它就自动帮你配好内嵌Tomcat和SpringMVC引入spring-boot-starter-data-redis它就自动帮你创建RedisTemplate的Bean。这种“约定优于配置”的思想能让一个单人开发的项目从零到跑通花费的时间压缩到一个很夸张的程度。理解这一点比背一百道SpringBoot面试题都有用——因为那些题的核心往往就是自动配置和启动流程。如果你在答辩时把“SpringBoot启动的时候spring.factories或AutoConfiguration.imports里的配置类会被加载条件注解决定哪些Bean生效”这套讲清楚老师基本不会再难为你。再聊为什么选了MyBatis-Plus而不是纯MyBatis。纯MyBatis写单表CRUD其实是很痛苦的每个表都要写Mapper接口加XML文件。MyBatis-Plus把单表的增删改查封装好了你只需要继承一个BaseMapper接口常用的方法全有了分页也有现成的插件。这个选择尤其适合商城项目里面那些结构简单的表比如用户表、购物车表、地址表基本上不需要手写SQL。而复杂的多表关联查询比如订单加订单明细加商品信息再手写SQL也不迟。前端选Vue 3加Element Plus原因很简单Element Plus的表格、表单、弹窗、分页组件做得非常完整管理后台的前端界面基本是“拼组件”就能拼出来。至于为什么不选JSP加Thymeleaf模板渲染而坚持前后端分离——一是为了让后端接口更干净职责更单一二是现在简历上写“前后端分离项目”是加分项三是前端静态文件可以单独部署到Nginx后端只专心提供JSON接口排错和扩展都方便。1.3 前后端分离的工程目录怎么规划工程结构上前端我用Vite创建了一个Vue3项目后端用Spring Initializr创建了一个Maven工程。很多人在这里会纠结要不要用Gradle其实对单模块的开发来说没什么区别Maven的依赖管理对新手更友好。如果你是从网上clone了一个Gradle项目也不用慌本质都一样就是构建工具不同。后端这边我按包结构划分功能模块而不是按技术类型划分。也就是说不要搞那种controller包下面放所有Controller、service包下面放所有Service的“大锅炖”结构。我用的方式是com.example.furniture ├── common // 通用类统一返回结果、异常处理、工具类 ├── config // 配置类跨域、Redis、MyBatis-Plus分页 ├── controller // 控制层 ├── entity // 数据库实体 ├── mapper // MyBatis-Plus的Mapper接口 ├── service // 业务逻辑层 ├── dto // 接收前端参数的封装对象 └── vo // 返回给前端的视图对象这里有个关键习惯要养成接收前端的参数不要直接拿Entity接。比如注册时前端传过来的JSON你别让它直接绑定到User实体上因为你数据库表里的字段可能比前端传的参数多直接用Entity接容易出安全问题。正确做法是定义一个RegisterDTO只包含username、password、nickname这几个字段这样职责清晰也安全。2. 数据库设计家具商城和其他电商的差异点在哪2.1 核心实体关系梳理数据库设计是整个商城系统的地基。地基没打好后面写代码会处处别扭。我画实体关系图的时候核心实体是这几个用户、地址、商品分类、商品、商品SKU、购物车、订单、订单明细。先说家具商城的特殊之处。家具不是标品一套沙发可能有不同的颜色、材质、尺寸组合对应的价格和库存都不一样。这种“一个商品多个规格”的情况在电商里叫SPU和SKU的概念。SPU是商品抽象层比如“北欧风布艺沙发”SKU是具体可下单的规格层比如“北欧风布艺沙发 灰色 三人位 2.8米长”。一张商品表配一张SKU表商品表放公共属性SKU表放价格、库存、规格图片这是电商数据库设计的基础也是很多新手最容易缺失的一层。如果直接把商品和SKU混在一张表里那一个商品有五个规格你就得建五条记录查询和展示变成一场灾难。另外家具还有一个特点就是商品属性比较重。材质、风格、尺寸、安装方式、是否定制这些字段在不同的类目下完全不一样。最合理的做法不是给商品表加一堆可能永远用不到的列而是设计一个attributes字段用JSON格式存储。MySQL 8原生支持JSON类型查询时也能用JSON函数对单机项目来说完全够用还免去了拆成属性表的复杂度。2.2 关键表结构的字段设计我把核心表的字段设计列一下你可以直接照着建表。用户表t_user自增主键或者雪花ID都行。我建议用MyBatis-Plus默认的雪花ID因为后期如果要分库分表自增ID会有冲突风险雪花ID天生是分布式的。字段包括username、passwordBCrypt加密后的密文、nickname、phone、avatar、role0表示普通用户1表示管理员、create_time、update_time。地址表t_address字段包括user_id、receiver_name、receiver_phone、province、city、district、detail_address、is_default。家具是大件商品配送地址的准确性尤为重要所以地址信息要完整到省市区。分类表t_category字段包括name、parent_id、sort_order。注意parent_id这个字段支持无限级分类比如“客厅家具”下面挂“沙发”“沙发”下面再挂“布艺沙发”。查询的时候用递归或者直接查全表在内存里组装都行数据量不大不用太担心性能。商品表t_product字段包括category_id、name、subtitle卖点副标题、main_image、detail富文本详情、brand、attributesJSON、status1上架0下架、create_time、update_time。SKU表t_product_sku字段包括product_id、sku_name比如“灰色 三人位”、price以分存储避免浮点数精度问题、stock、sku_image、sales。价格用分存储这一点我强调一下数据库里不要直接用DECIMAL存元要么用DECIMAL(10,2)也行但更规范的做法是直接用整型存分Java里用BigDecimal处理前端展示时再转成元。这个习惯能帮你省掉一堆浮点精度相关的玄学Bug。购物车表t_cart字段包括user_id、product_id、sku_id、quantity、checked是否勾选用于批量结算。订单主表t_order字段包括order_no订单编号、user_id、order_status、total_amount、pay_amount、freight_amount、receiver_name、receiver_phone、receiver_address、pay_time、delivery_time、finish_time、cancel_time、create_time。订单明细表t_order_item字段包括order_id、product_id、sku_id、product_name、sku_name、product_image、price、quantity、total_price。注意明细表要冗余商品名称、SKU名称和图片因为商品信息后续可能被修改甚至删除但订单作为交易快照不能变这是电商系统的一个经典设计原则。2.3 为什么订单表必须单独抽一张订单明细表很多新手做订单功能的时候喜欢在一张订单表里放一个字段存商品列表比如用逗号分隔的ID串或者干脆塞一段JSON。这个设计第一个版本跑起来确实快但后面做订单详情、退货退款、按商品维度统计销量的时候全部卡壳。正确的做法永远是订单主表和订单明细表一对多拆分。主表管状态、金额、收货信息明细表管每一个商品项。一张订单买三个商品就是一条主表记录加三条明细记录。这样查询订单详情只需要一条主表查询加一个按order_id查明细的列表查询逻辑清晰扩展性也好。顺便说一句订单编号的生成也有讲究。不要用自增ID直接当订单号一是容易被猜到业务量二是多表合并订单信息时不好处理。我用的方案是时间戳加用户ID后四位加随机数最终拼成一条24位左右的字符串。网上有很多雪花算法生成的ID工具但订单号用可读性更强的自定义规则会更方便后续排查问题。3. 后端核心模块实现从注册登录到订单闭环3.1 JWT登录认证与拦截器设计登录模块是整个系统的基础它的设计影响到后面所有需要用户身份的操作。我不用Session而是用JWTJSON Web Token。为什么要用JWTSession依赖服务端存储状态在前后端分离的架构下你需要处理跨域携带Cookie的问题还要考虑如果以后扩展多个服务实例Session同步会很难处理。JWT把用户信息加密后放在客户端服务端无状态每次请求带Token就行本质上更贴合前后端分离的模式。登录流程是这样的用户提交用户名和密码后端用BCrypt校验密码。注意密码绝对不能明文存储也尽量不要用MD5——MD5已经能被彩虹表批量破解而且同一密码的MD5值固定容易被撞库。BCrypt每次加密同一个密码得到的密文都不一样因为它内置了随机盐安全性完全够用。校验通过后我用jjwt这个库生成一个Token载荷里存放userId和role设置过期时间为24小时。前端拿到Token后存在localStorage里每次请求在HTTP Header的Authorization字段里带上。后端写一个拦截器或者Spring MVC的HandlerInterceptor对所有需要登录的接口校验Token的有效性解析出来后把userId放到ThreadLocal里这样后面的业务代码里随时可以拿当前登录用户。这里有个非常容易踩的坑ThreadLocal用完忘记清理。在拦截器的afterCompletion方法里一定要remove否则在高并发下Tomcat的线程池复用线程线程中残留的userId会串到下一个请求里去查出来的数据就会出现“灵魂附体”一样的错乱。3.2 商品浏览与分类筛选查询商品模块看起来是纯查询其实也有一点设计空间。用户端浏览商品最常见的场景是进入首页 → 点击某个分类 → 看到商品列表 → 点击进入详情。所以接口层面我设计了三个分页查询商品列表支持分类ID和关键字筛选、查询商品详情包含SKU列表、查询所有分类树。分页查询用MyBatis-Plus的分页插件配置一个PaginationInnerInterceptor就行。调用时传入current和size两个参数插件自动帮你生成limit语句返回IPage对象里面包含总条数和当前页数据。前端用Element Plus的表格组件配pagination组件把总条数和当前页接起来就是一个完整的分页流程。查询逻辑上要支持三个条件分类ID、关键字、排序方式。分类ID这一层有一个细节就是用户点击一级分类“客厅家具”时要不要把二级分类“沙发”“茶几”下的商品也一起查出来。我这里的处理方式是先查出该分类下所有子分类ID的集合再用IN语句查询。如果数据量大了可以改成递归公共表表达式但单体项目这几个表的数据量用IN就足够了不要过度设计。商品详情接口返回的数据结构我用一个ProductDetailVO来封装里面包括商品基本信息、属性JSON解析后的Map、以及该商品下所有SKU的列表。前端拿到这个VO就可以渲染商品主图、详情参数、规格选择和价格库存展示。注意VO的字段命名要对齐前端的驼峰命名习惯否则前端接数据的时候总是undefined排查半天发现是字段名对不上这种低级问题浪费的时间比写代码还多。3.3 购物车模块的实现思路购物车在电商系统里是一个很有争议的模块因为做简单特别简单做复杂特别复杂。复杂到什么程度淘宝那种购物车要处理失效商品、优惠折扣、库存实时校验、跨店结算每一项都是一套独立系统。我这个项目定位是教学和毕设级别所以购物车做成MySQL存储的基础版就好不要一上来就上Redis。购物车表的操作就五个加购、改数量、勾选状态、删除、查询列表。加购的时传入product_id、sku_id、quantity后端先判断该用户购物车里有没有同款SKU有就把数量累加没有就新插一条记录。这里有一个查询时要注意的点购物车列表接口不能只查购物车表本身还要连表查出商品名称、SKU规格名称、商品图片和最新价格否则前端没法渲染。最简单的写法是查出购物车数据后用SKU ID集合批量查SKU表再按SKU ID映射回购物车数据里组装。购物车要不要用Redis我的答案是在数据量大到一定程度之前MySQL就够。购物车最核心的需求是数据不丢失用户加进购物车的商品如果因为缓存失效没了这个体验是致命的。Redis做购物车一般适合大促读多写少的场景对单机项目来说引入它只会增加缓存和数据库数据一致性的维护成本。我会在后面的章节详细讲Redis到底用在哪些地方更划算。3.4 订单状态机的设计与落地订单是整个商城里最容易写乱的部分。我见过太多人用一个int类型的status字段代码里到处写if(status 1)改着改着就乱套了。正确的做法是用状态机思维来管理订单状态。我定义的订单状态枚举是待支付、已支付、已发货、已完成、已取消。对应的流转边界如下待支付 → 已支付用户模拟支付待支付 → 已取消用户超时未支付或主动取消已支付 → 已发货管理员在后台点击发货已发货 → 已完成用户确认收货下单时的完整事务逻辑是校验购物车商品→校验库存→生成订单主表和明细→扣减库存→清空购物车对应商品→返回订单号。这一步必须在事务里执行任何一个环节失败都要回滚否则会出现订单建了但库存没扣或者库存扣了但订单没建这种数据不一致。事务用Spring的Transactional注解标注在方法上默认遇到RuntimeException就回滚。创建订单时还要注意防重复提交。用户手一抖点了两次“提交订单”如果没做处理就会生成两笔一模一样的订单。处理方案是在下单接口生成一个唯一的幂等键前端提交前先从后端获取提交时带着这个键后端判断键是否已存在存在就直接返回已创建的订单。简单一点的方案也可以用时间戳加随机数前端生成一个requestId放在请求里后端用Redis的setnx命令判断这个requestId是否处理过。订单创建完成后用户点击模拟支付其实就是把订单状态从待支付改成已支付并记录支付时间。之后管理员在后台看到已支付订单点击发货状态变成已发货。最后用户收到货点击确认收货状态变成已完成。这一套流程走完订单的整个生命周期就闭环了。4. 库存扣减与并发下单一个必考的难点4.1 超卖问题是怎么产生的如果说这个项目哪个点最容易被面试官和答辩老师追问那一定是库存扣减。原因很简单普通的CRUD谁都会写但并发场景下的数据一致性才是真正拉开差距的地方。假设一个商品SKU的库存还剩最后1件两个用户同时提交订单。如果代码逻辑是这样的先查库存发现库存大于0然后执行扣减。那问题就来了——两个请求同时查到库存为1两个都认为有货可以买然后都执行扣减数据库里库存变成-1但两个订单都创建成功了这就是超卖。根本原因是什么呢是“先查后改”这个非原子操作在并发环境下的时间差。查的时候库存是1改的时候条件已经变了但你的UPDATE语句没有把这个变化感知到所以照样执行成功。4.2 乐观锁方案与Redis预扣减方案的取舍解决超卖最常见也最容易被接受的方案是乐观锁。思路很简单在SKU表加一个version版本号字段或者直接用库存数本身作为版本号。更新库存的时候SQL写成一个带条件的原子更新UPDATE t_product_sku SET stock stock - 1 WHERE sku_id #{skuId} AND stock - 1 0Java代码里的逻辑就是执行这条UPDATE返回受影响的行数如果行数大于0说明扣减成功等于0说明库存不足或者已经被并发抢走了。这个方案的本质是把“判断库存够不够”和“扣库存”合并成一条SQL由数据库的行锁保证同一时刻只有一个事务能改这条记录从根源上消灭了时间差。我实测下来用这个方案配合事务单机项目哪怕压到几百并发也很稳。还有一个方案是用Redis的incr/decr原子操作做预扣减。先扣Redis里存的库存扣减成功再异步落库最后通过消息队列或者定时任务同步数据库。这个方案的优势是性能极高扛得住大促峰值缺点是架构复杂度上去了Redis和MySQL的数据一致性需要额外处理一旦Redis挂了库存数据就不可靠。对单体商城项目来说我建议还是老老实实用数据库乐观锁把复杂度降到最低。但如果答辩时想展示自己的知识面可以把两种方案拿出来对比然后重点解释Redis方案的适用场景——峰值流量极大、允许引入消息中间件的团队才会选它。4.3 订单超时未支付的兜底策略下了单但不支付库存一直被占着这是电商系统必须处理的问题。处理方案有两种主流做法一种是用延迟消息队列比如RabbitMQ的延迟消息插件、RocketMQ的定时消息让订单创建后触发一个30分钟后的事件事件里判断订单是否仍然待支付是就取消并回补库存另一种是在订单表加一个创建时间字段用一个定时任务每分钟扫描一次待支付且超过30分钟的订单批量取消并回补库存。对小项目来说第二种做法更实际因为不需要额外引入消息中间件。我用Spring的Scheduled注解写了一个定时任务每60秒执行一次查询条件就是status 待支付 AND create_time NOW() - 30分钟。查出订单集合后逐单执行取消操作——更新订单状态、回补SKU库存。这一套写下来代码量不大但很好地补上了电商系统的重要拼图。超时关单这里有一个细节容易漏就是在定时任务里逐单回补库存如果某一张订单的SKU已经被删除SQL会更新0行不影响其他订单。所以循环处理时每个订单的库存回补要单独捕获异常避免一个订单出问题导致整个批处理中断。5. Redis引入缓存加载与数据一致性5.1 热门商品数据的缓存策略Redis在这个项目里真正发挥价值的地方是商品详情和首页推荐位的缓存。为什么要缓存商品因为商品信息是典型的读多写少数据。用户访问商品详情的频率远高于管理员修改商品信息的频率每次查询都打到MySQL上不仅慢还给数据库增加无谓的压力。把热点商品的详情放到Redis里查询路径就变成了先查Redis命中直接返回没命中再查数据库并回填Redis。我用Spring Cache或者手动操作RedisTemplate都行。手动控制的逻辑更直观商品详情接口先根据productId生成一个缓存key比如product:detail:1001然后查询代码里先尝试从Redis取值取到直接转成ProductDetailVO返回取不到就走数据库逻辑然后设置一个随机的过期时间写入Redis。这里为什么要加随机过期时间我在后面章节展开讲。商品缓存失效之后第一次请求会打到数据库。这个行为本身没什么问题但要注意缓存击穿的情况——也就是某个热点商品的缓存刚好过期此时大量请求同时涌入全部打到数据库数据库瞬间压力大增。应对方案是加锁重建缓存在回填数据库这段逻辑上加一把互斥锁让一个请求去查数据库回填缓存其他请求先等待等缓存回填完成后直接从缓存读取。5.2 缓存穿透、击穿、雪崩的实际应对这三个词听起来吓人但本质都是缓存失效时的边界问题。缓存穿透是指查询一个数据库中根本不存在的数据。比如有人恶意用不存在的商品ID频繁请求每次都会穿过缓存打到数据库。解决办法有两个一个是缓存空值把不存在的key也缓存起来设置一个较短的过期时间另一个是接口层做参数校验商品ID的格式不合法就直接拒绝。缓存击穿就是我上面说的热点key过期瞬间大量请求打到数据库。解决办法是互斥锁重建缓存或者采用逻辑过期时间——缓存里存一个字段标记逻辑过期时间查询时发现逻辑过期了返回旧数据同时异步去刷新缓存。这个方案更复杂但体验最好。缓存雪崩是指大量key在同一时刻集中过期导致一波请求全部打到数据库。解决办法很简单就是给每个key的过期时间加一个随机值让过期时间均匀分散。比如基础过期时间是30分钟那就随机加1到5分钟这样即使同一批商品一起缓存进去过期时间也各不相同数据库不会在同一时刻承受全部流量。5.3 缓存和数据库的一致性怎么做Redis缓存MySQL数据最头疼的就是修改商品时缓存怎么办。最简单的方案是更新数据库后手动删除对应的缓存key下次请求时发现缓存没命中自然就把最新数据从数据库回填到缓存里了。这个方案叫Cache Aside也叫旁路缓存是业界使用最广泛的一致性方案。为什么更新数据库后删缓存而不是更新缓存因为更新缓存这个操作本身有风险——如果两个并发请求同时更新一个商品的缓存后更新的数据可能覆盖了先更新的数据刚好把旧数据写进去了。而删除缓存则没有这个问题因为删是幂等的下次查询重新构建缓存构建时读到的一定是数据库里的最新值。极端情况下删除缓存也可能失败。如果删缓存失败旧缓存会继续存在导致用户读到旧数据。解决办法是用延迟双删先删除缓存更新数据库隔几百毫秒再删除一次缓存。这个方案能覆盖绝大多数不一致问题但对单体项目来说直接用最基础的删缓存方案就够了把数据库更新和缓存删除放到同一个事务里失败回滚已经能解决绝大多数场景。6. 管理后台与权限控制别做裸奔的admin接口6.1 用户角色与权限模型怎么落地很多自学的项目管理后台接口是裸奔的——只要知道URL任何人都能访问。比如你在前端页面把/admin/product/list这个请求看一遍拿到接口地址然后用Postman直接发一个修改商品价格的请求这就能改掉商城里的商品价格了。这个安全隐患在答辩演示时一旦被老师点到印象分会大打折扣。我的处理方式是在JWT的载荷里写入用户角色普通用户的role是0管理员的role是1。然后在后端定义一个角色校验的注解比如RequireAdmin并实现一个拦截器在拦截器里解析Token后判断当前用户角色是否为管理员如果不是就返回无权限的提示。你可以用Spring MVC的拦截器对/admin/**路径统一做校验最简单。还有一种升级方案是引入Spring Security加JWT做认证授权但这套组合的学习曲线比较陡配置类写起来比拦截器复杂得多。考虑到项目体量我最终选择了拦截器加注解的轻量方案既能说明白权限控制的原理又不至于把代码复杂度拉太高。如果简历上写“熟悉Spring Security”那可以再往深了做否则用轻量方案反而更容易在面试时自圆其说。6.2 商品上下架、SKU维护与订单发货管理后台的商品管理功能核心是围绕SPU和SKU做的。新增商品时前端填完商品基本信息后继续维护SKU列表——每个SKU包含规格名称、价格、库存、图片。后端接收的时候用ProductDTO里面嵌套一个List 先插入商品主记录拿到productId然后批量插入SKU记录整个过程放在一个事务里。商品上下架说白了就是更新Product表的status字段。这里有一个连带逻辑要想清楚下架一个商品之后它下面的SKU也不能再被购买。所以在用户端的查询逻辑里只查status为1的商品而且下单创建订单时还要再校验一次商品和SKU的状态不能只在前端隐藏就算完事后端才是最后一道防线。订单发货是管理员最常见的操作。后台订单列表按订单状态筛选看到已支付的订单点发货按钮后端做两件事更新订单状态为已发货记录发货时间。这里可以加一个物流单号字段用户端查询订单详情时能看到但如果是教学项目字段可以先不接真实的物流API手动填写一个模拟单号即可。6.3 数据看板背后的SQL怎么写管理端的首页数据看板不需要上重型BI工具几张统计卡片加一个趋势图就够。统计口径一般包括用户总数、商品总数、今日订单数、今日销售额、近七天订单趋势。这些统计用简单的SQL聚合就能搞定。用户总数和商品总数直接用COUNT查询今日订单数就是WHERE create_time大于今天的零点再COUNT今日销售额是SUM已支付和已完成订单的pay_amount。近七天趋势用一条按日期分组的SQLSELECT DATE(create_time) AS d, COUNT(*) AS order_count, SUM(pay_amount) AS amount FROM t_order WHERE order_status IN (1, 2, 3) AND create_time DATE_SUB(CURDATE(), INTERVAL 7 DAY) GROUP BY DATE(create_time) ORDER BY d注意查询的日期函数尽量在数据库里完成不要把所有订单拉到Java内存里再按日期分组那样既慢又浪费资源。数据量小时无所谓但写SQL时养成“能用SQL就不要用Java算”的习惯对大促数据的处理会有很大帮助。7. 部署上线与踩坑清单7.1 从本地到服务器部署全流程项目开发完成后部署上线是检验整个系统能否真正运行的关键一步。环境我用的是一台最便宜的云服务器操作系统选CentOS 7或Ubuntu 20.04都行。服务器上需要装的东西有JDK版本跟本地开发保持一致、MySQL 8、Redis、Nginx。后端打包流程很简单在项目根目录执行mvn clean package -DskipTests打包完成后target目录下会生成一个jar包。这里的关键点是SpringBoot内置了Tomcat所以这个jar包是可直接运行的web服务不需要再单独装Tomcat解压war包放进去。启动命令是nohup java -jar furniture-admin.jar --spring.profiles.activeprod app.log 21 注意nohup和的组合这是让jar包在后台持续运行的标准姿势。日志重定向到app.log文件方便排查故障。前端部分在Vue项目根目录执行npm run build生成dist目录里面是一堆静态文件。把这堆文件传到服务器的Nginx站目录下然后配置Nginx把前端页面和API请求做反向代理。核心配置思想是所有/api/**路径的请求转发到后端jar包所在的端口其他路径一律走前端静态文件并加上try_files让刷新页面时不会404。7.2 部署后必踩的几个坑部署过程中最容易出问题的地方我列成清单每一条都是我在实际操作中撞过墙的JDK版本不匹配。如果你本机用的JDK 17或21但服务器装的是JDK 8大概率启动时报UnsupportedClassVersionError。SpringBoot 2.x支持JDK 8SpringBoot 3.x强制要求JDK 17以上。如果你习惯用JDK 1.8老老实实用SpringBoot 2.7。如果IDEA创建项目时选了SpringBoot 3.x又想回退到JDK 1.8大概率是建不起来或者编译不过这个时候别硬刚直接降低SpringBoot版本到2.7。数据库时区问题。MySQL连接串如果不指定serverTimezone在高版本MySQL驱动下有可能会报时区错误就算不报错存储的时间也和你本地时间差8个小时。连接串加上serverTimezoneAsia/Shanghai是最稳妥的做法。跨域问题。前端部署在Nginx上后端在8080端口两个域名或者端口不同就涉及跨域。解决方式有两个后端加一个全局CorsFilter允许指定来源或者在Nginx里把前后端配成同一个域名、通过路径区分这样浏览器就不会判定为跨域。我建议直接用Nginx反向代理处理这样前端代码里连axios的baseURL都不用区分环境全部写成相对路径/api就行了。MyBatis-Plus雪花ID在JavaScript里的精度丢失。MyBatis-Plus默认用雪花算法生成19位的Long型ID但JavaScript的Number类型最大安全整数只有900719925474099119位数字早就超了。前端拿到的商品ID末尾几位会变成0导致商品详情查不出来。解决办法有两种一个是在后端把Long转成String返回用JsonSerialize注解或者全局Jackson配置另一个是用串行化的VO类把ID字段改成String类型。前端刷新404。Vue是单页应用路由是前端控制的默认只有index.html用户在商品详情页刷新时Nginx会去找对应的URI路径文件找不到就404。解决办法是Nginx的location配置加上try_files $uri $uri/ /index.html;。7.3 这套架构后续还能怎么扩展如果这个项目做完了还想继续提升几个方向可以参考。一个是把用户端和管理端彻底拆成独立的SpringBoot服务用Nacos做服务注册发现也就是把单体架构升级成微服务架构虽然业务不大但架构设计的思路能练一遍。另一个是引入消息队列用RabbitMQ或RocketMQ处理订单超时取消和订单创建后的异步通知把定时任务轮询取消订单的方案替换成更实时的事件驱动方案。还有一个方向是引入全文检索用Elasticsearch做商品搜索替换掉目前的MySQL模糊查询搜索体验会好很多。在实际操作中我还有一个体会想多说一句做这个项目代码量本身不是最大的成本真正花时间的是把数据关系理清楚、把状态流转想明白、把并发边界测试到位。如果你也是自己一个人在搞千万别急着写代码先把数据库表设计好把订单状态图画出来把接口清单列出来——磨刀不误砍柴工这步省下来的时间后面都会还给你。
返回列表