SpringBoot电商项目实战:从核心配置到订单库存与Flowable集成
我搞过很多电商类的练手项目但“欢乐购”这个运动服饰购物网站算是我第一次把SpringBoot玩得比较完整的一次。整站从零写起用户端、管理端、下单流程、支付对接沙箱、后台统计一张图一张表敲出来整个过程踩了不少坑也沉淀了不少经验。如果你正要做一个SpringBoot全栈项目或者正在准备毕业设计、跳槽项目复盘这篇内容可以当成一份完整的实战笔记来看。写这篇文章之前我梳理了一下SpringBoot相关的搜索热度一直很高尤其springboot配置、springboot面试题这些词明显是两类人在找一类在赶项目进度一类在赶面试进度。我这个项目恰好把这两件事都覆盖了——代码怎么落地、关键配置怎么调、线上问题怎么排查都会提到。1. 项目整体设计与技术选型思路1.1 运动服饰电商的核心需求解析“欢乐购”定位很明确一个面向运动服饰垂直品类的B2C购物网站。和淘宝、京东这种全品类平台不同垂直电商要把精力放在“商品维度”和“用户购买路径”上。我拆解需求的时候把系统画成了两个端加一条主链路用户端注册登录、商品分类浏览、商品搜索、商品详情、购物车、订单确认、在线支付、个人中心、订单管理。管理端运营后台管理用户、分类、商品、库存、订单、轮播图、公告等。主链路用户搜索/浏览商品 → 加入购物车 → 提交订单 → 支付/取消 → 商家发货 → 确认收货。这条链路是整个项目的主心骨。无论是数据库设计还是接口规划我都要求自己“能覆盖主链路的所有环节”宁可功能朴素一点也不要为了凑功能砍链路。1.2 为什么坚持选择SpringBoot而非SSM或SpringCloud这个问题也是我在项目初期反复纠结过的。很多人会问都2025年了做个商城不是应该直接上微服务吗我的答案是要看项目规模和目标人群。这个项目本质上是一个单体应用核心目标是快速落地、逻辑清晰、部署简单。SpringBoot天然适合这种场景——内置Tomcat不用打WAR包自动装配省掉一堆XML配置起步成本极低。如果用SpringCloud强行拆成商品服务、订单服务、用户服务在这个体量下带来的不是性能提升而是运维噩梦服务注册发现、配置中心、链路追踪、分布式事务……任何一个点都够喝一壶。毕业设计或者个人作品集项目重要的是把业务逻辑做深、把代码写干净而不是把架构堆得很高。1.3 项目目录结构与数据库设计我采用的分层结构是标准的Controller-Service-Mapper三层外加config、common、utils等支撑包。目录建得好后面写代码会有种“东西该放哪早就定好了”的踏实感不会东一榔头西一棒槌。com.huanglego ├── controller接口层 ├── service业务逻辑层 │ └── impl ├── mapper数据访问层 ├── entity数据库实体 ├── dto入参/出参对象 ├── vo视图对象 ├── config配置类 ├── common统一返回、异常处理 ├── utils工具类 └── HuanglegoApplication.java数据库我设计了10张核心表用户表、商品分类表、商品表、商品图片表、购物车表、订单表、订单明细表、收货地址表、轮播图表、公告表。订单表和订单明细表是典型的一对多主从表结构这也是电商系统的标配设计后面讲订单模块的时候细说。提示表结构设计阶段一定不要偷懒字段注释要写全。时间长了你回头看自己写的SQL如果没有注释有些字段连自己都看不懂是什么业务含义。2. SpringBoot关键配置详解与踩坑记录2.1 核心配置文件的编写思路SpringBoot最大的魅力之一就是“约定大于配置”但“约定”不代表不配置。我见过太多项目死在application.yml上所以我把自己迭代过好几轮后的配置模板直接放出来。这个配置已经考虑了开发环境和生产环境的切换核心是不同环境之间的参数隔离server: port: 8080 servlet: context-path: / spring: profiles: active: dev datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/huanglego?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalse username: root password: yourpassword jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT8 servlet: multipart: max-file-size: 10MB max-request-size: 20MB mybatis-plus: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl map-underscore-to-camel-case: true global-config: db-config: logic-delete-field: deleted logic-delete-value: 1 logic-not-delete-value: 0这里有个我一开始没太在意、后来被坑惨的点数据库连接URL中的serverTimezone参数。如果你用MySQL 8.x不设置时区会报The server time zone value Öйú±ê׼ʱ¼ä is unrecognized这一长串乱码能把人看懵。直接写成serverTimezoneAsia/Shanghai问题就消失了。另外map-underscore-to-camel-case这个配置很多人会忽略。数据库字段一般是user_name这样的下划线命名Java属性是userName驼峰命名不开启这个自动映射查询出来的结果很多字段是null排查起来还挺费劲。2.2 多环境配置与热部署方案开发环境、测试环境、生产环境的配置肯定不一样我直接把配置拆成三个文件application.yml公共配置只放端口、应用名、SpringBoot基础设置。application-dev.yml本地开发库连接、日志级别为DEBUG、打印SQL日志。application-prod.yml部署到云服务器时的连接地址关闭SQL打印开启连接池参数。切换环境只需要修改spring.profiles.active的值。这个方案比改一个全家桶配置文件靠谱得多永远不会出现“上线忘改数据库地址”的尴尬。热部署方面我加了spring-boot-devtools依赖。但我要说句实在话这个依赖在IDEA里新老版本表现不太一样有时候改完代码不会自动重启还需要手动按CtrlF9编译一下。后来我干脆用JRebel不过那玩意儿收费。如果你只是做项目交付devtools就够了别花那个冤枉钱。2.3 连接池与线程池的参数调优很多初学者忽略连接池配置觉得不配也能跑。确实能跑但并发一上来就难受了。HikariCP是SpringBoot默认的数据库连接池性能很好但默认配置对于我们这种项目其实太保守。我实际项目中是这样调的spring: datasource: hikari: minimum-idle: 5 maximum-pool-size: 20 connection-timeout: 30000 idle-timeout: 600000maximum-pool-size20是参考了PostgreSQL官方的计算公式connections ((core_count * 2) effective_spindle_count)。我这台机器8核算出来大概18取整20比较合理。这个参数不是越大越好连接池超过一定规模后数据库反而会因为大量空转连接而变慢这点在配置异步任务和并发下单时尤其明显。异步线程池我也做了独立配置因为SpringBoot默认的Async是简单的SimpleAsyncTaskExecutor每次调用都new线程生产环境绝对不能直接这么用。我写了ThreadPoolTaskExecutor的配置类核心线程10个最大20个队列容量200拒绝策略用CallerRunsPolicy——线程池满了之后让调用线程自己执行这样能在高峰期起到天然降级的作用不会把任务直接丢弃。3. 业务核心环节的实现与避坑指南3.1 用户注册登录模块的完整流程注册登录是每个系统的门面但很多项目把这个模块写得过于敷衍。我这里的做法是用JWTJSON Web Token做无状态认证基本流程是用户注册前端传用户名、密码、手机号后端用BCrypt加密存储不存明文。用户登录校验用户名密码成功后生成JWT令牌返回给前端前端存储在本地并在请求头中携带。接口鉴权通过拦截器或Spring Security的过滤器链在进入Controller前解析JWT校验通过才放行。密码加密这块必须说清楚绝对不要用MD5。MD5加盐也不过是拖延时间BCrypt本身内置随机盐同一个密码每次加密结果都不一样破解成本高一个数量级。Spring Security里直接new BCryptPasswordEncoder().encode(password)就完事。JWT令牌我这里设置了两小时过期同时用Redis存了一份当前有效token。为啥多此一举因为JWT本身是无状态的你没法主动让它失效——“退出登录”只是前端删了本地token后端根本不知道。加了Redis之后服务端可以主动踢人比如管理员封号时就能立刻让这个用户的token失效。3.2 商品分类与多条件检索的实现商品分类我用了两级结构一级分类上衣、裤装、鞋类、配件二级分类如跑步鞋、篮球鞋、休闲鞋。后台管理时可以动态添加分类不需要改代码灵活性好很多。商品检索是最能体现实战水平的地方。我开发的时候用的是一个方法跑通单条件查询再去逐步扩展多条件组合查询的思路Override public PageProduct searchProducts(String keyword, Integer categoryId, BigDecimal minPrice, BigDecimal maxPrice, String sort, Integer page, Integer size) { PageProduct pageParam new Page(page, size); LambdaQueryWrapperProduct wrapper new LambdaQueryWrapper(); // 关键字模糊搜索商品名称或描述 if (StringUtils.hasText(keyword)) { wrapper.and(w - w.like(Product::getProductName, keyword) .or().like(Product::getDescription, keyword)); } // 分类过滤 if (categoryId ! null) { wrapper.eq(Product::getCategoryId, categoryId); } // 价格区间过滤 if (minPrice ! null) { wrapper.ge(Product::getPrice, minPrice); } if (maxPrice ! null) { wrapper.le(Product::getPrice, maxPrice); } // 排序new(最新) / sales(销量) / price_asc(价格升序) / price_desc(价格降序) if (sales.equals(sort)) { wrapper.orderByDesc(Product::getSales); } else if (price_asc.equals(sort)) { wrapper.orderByAsc(Product::getPrice); } else if (price_desc.equals(sort)) { wrapper.orderByDesc(Product::getPrice); } else { wrapper.orderByDesc(Product::getCreateTime); } return productMapper.selectPage(pageParam, wrapper); }MyBatis-Plus的LambdaQueryWrapper用起来非常顺手不用手写SQL也能完成动态条件拼接而且用Lambda表达式写实体字段编译期就能发现字段名写错的问题这个特性在字段较多的时候特别友好。3.3 购物车设计的两种思路对比购物车我最终用的是MySQL来存表结构大致是id, user_id, product_id, product_count, checked, create_time, update_time。每次操作购物车就往表里写数据查询时关联商品表返回实时价格和库存。为什么不用Redis实现购物车我客观对比一下用RedisHash结构存储性能确实好用户的增删改查都在内存里完成不用走数据库。但问题是购物车数据是非强一致的用户关掉浏览器或过期之后Redis数据一清空购物车就没了。除非做持久化定期同步这个复杂度对单体项目来说性价比不高。所以“欢乐购”的购物车我选了MySQL方案理由是简单可靠、可追溯加上分页查询好实现。如果你要做一个高并发演示的卖点那可以再加一层Redis缓存来缓存热点商品比如运动鞋和紧身衣这类点击率高的数据而购物车本身的存贮还是以数据库为主。这里要提醒一下购物车接口必须做用户隔离。我见过有人写查询购物车时只传了userId没做token校验结果任意登录用户都能遍历别人的购物车这是严重的越权漏洞。正确做法是从JWT里解析出当前登录用户IDSQL条件只按这个ID查询。3.4 订单模块事务、状态机与幂等性订单模块是整个系统最核心、也最容易出问题的部分。一个订单从创建到完成经历了待支付 → 已支付/已取消 → 待发货 → 已发货 → 已完成的流转管理端还涉及退款/售后的相关状态。我先说下单的流程。用户在购物车勾选商品后点“去结算”后端需要做的操作至少有三步验证校验商品状态是否上架、库存是否充足计算订单总价以数据库商品表的价格为准不信任前端传的价格创建订单主表和订单明细表扣减库存。这三步操作必须放在同一个事务里。我在项目里用了Transactional(rollbackFor Exception.class)这里有一个坑要讲Transactional默认只在遇到RuntimeException时回滚不会在遇到检查异常时回滚。比如你想主动抛一个new Exception(库存不足)来触发回滚它是不会回滚的要么抛RuntimeException的子类要么在注解上显式加上rollbackFor Exception.class。我就是因为这个原因早期写的代码在某个异常场景下订单建了但库存没扣货超卖了。订单状态我使用了状态机模式来约束流转而不是简单地写一堆if-else。核心思想是每个状态节点只允许执行预定义的动作跳转到下一状态。比如“已支付”状态下只允许“发货”或“申请退款”不允许直接变成“已完成”。我抽了一个枚举类OrderStatusEnum来管理状态码和描述在状态变更方法里做前置校验这样能把非法的状态跳转直接拦截在入口。关于幂等性主要针对的是“提交订单”这个操作。用户快速连点两次“提交订单”会产生两个一模一样的订单。我用了一个简单有效的方案前端生成一个唯一订单号业务单号提交时后端先查业务单号是否存在存在就直接返回已有订单不存在才创建新订单。这种方式叫“业务唯一键防重”比单纯的分布式锁实现成本低很多对付这种场景绰绰有余。Transactional(rollbackFor Exception.class) public OrderVO createOrder(OrderCreateDTO dto) { // 1. 查询订单号是否已存在 Order existing orderMapper.selectOne(new LambdaQueryWrapperOrder() .eq(Order::getOrderNo, dto.getOrderNo())); if (existing ! null) { return convertToVO(existing); } // 2. 校验商品信息、计算总价 // 3. 创建订单主表信息 // 4. 创建订单明细 // 5. 扣减库存 // 6. 清空购物车对应商品 }3.5 库存扣减的并发安全处理库存扣减这个点我一开始写的是select stock from product where id?然后在Java代码里判断stock是否大于0小于0就报错再update product set stocknewStock。测试环境单用户没问题用JMeter一压就原形毕露——超卖了。后来我改成了一条SQL原子操作UPDATE product SET stock stock - #{count} WHERE id #{productId} AND stock #{count}stock count这个条件保证了扣减操作是原子的而且天然带库存校验功能。影响行数大于0说明扣减成功等于0说明库存不足或商品不存在。这是在高并发场景下最经典的数据库层防超卖方案也不复杂。如果后续你想挑战更高并发可以引入RedisLua脚本做库存预扣或者用消息队列异步扣减。但在一开始做单体项目的阶段先别碰那些重型武器把基础的原子SQL写好已经能应付大部分课堂项目和面试场景了。4. SpringBoot集成Flowable工作流的思考与实践4.1 项目里为什么会出现Flowable的影子网上关于springboot使用flowable的搜索热度一直很高说明很多人已经意识到当一个系统的流程变得复杂比如多层审批、多角色流转、动态会签用硬编码写if-else就完全扛不住了。我这个项目最初并没有引入Flowable订单状态是自己在状态机里写的。但后来增加了一个“售后工单审批”场景流程是用户提交退款申请 → 客服初审 → 财务复核 → 退款执行/驳回每一步还有超时处理和多级驳回。如果再靠状态字段维护流转逻辑会越来越绕光看代码根本理不清流程。所以我把售后审批改造成了一个BPMN流程直接用Flowable引擎来驱动。用可视化流程定义文件代替了一半的硬编码这个改动在后期扩展“退货流程”和“补偿流程”时收益非常明显。4.2 BPMN流程定义与SpringBoot集成方式我用的依赖是flowable-spring-boot-starter版本和SpringBoot 2.7搭配是6.x系列。流程定义文件放在resources/processes目录下Flowable启动时会自动部署。一个简单的售后审批流程BPMN文件核心大概是这样的结构process idafterSaleProcess name售后审批流程 isExecutabletrue startEvent idstartEvent / userTask idcustomerServiceTask name客服初审 flowable:assignee${assignee1} / userTask idfinanceTask name财务复审 flowable:assignee${assignee2} / serviceTask idrefundTask name执行退款 flowable:classcom.huanglego.flow.RefundServiceTask / endEvent idendEvent / sequenceFlow sourceRefstartEvent targetRefcustomerServiceTask / sequenceFlow sourceRefcustomerServiceTask targetReffinanceTask / sequenceFlow sourceReffinanceTask targetRefrefundTask / sequenceFlow sourceRefrefundTask targetRefendEvent / /process运行时通过RuntimeService.startProcessInstanceByKey(afterSaleProcess, businessKey, variables)启动流程把订单号、申请人、退款金额等作为流程变量传入。每步审批通过TaskService.complete(taskId, variables)驱动流程往下走。Flowable的核心模型是“流程实例 任务 变量”理解了这个三角关系用起来就很顺手。4.3 什么时候该用Flowable什么时候不该用说实话我在项目里前后对比下来最深的一个体会是**Flowable是把双刃剑。**它适合需要可视化、可调整、多人协作的长流程但如果是订单状态这种流程固定、节点少、无人工干预的场景Flowable反而增加了系统的复杂度和维护成本。建议用Flowable的场景多级审批、会签、或签、驳回、超时自动处理、流程版本变更。建议用状态机的场景订单状态流转、支付状态变化、物流状态跟踪流程固定不变。很多人在技术选型时容易走向极端看到一个框架火就想往里塞最后搞得项目臃肿不堪。从架构角度看好的代码是“刚好满足需求”而不是“把所有热门技术都用上”。5. 常见问题排查与SpringBoot面试高频考点5.1 开发期我踩过的5个典型问题这个项目开发过程中我记录了不少问题挑几个有代表性的分享有些是新手必踩有些是老手也可能忽略的问题现象根本原因解决办法前端请求接口报跨域错误后端未配置跨域支持写一个 implements WebMvcConfigurer 的配置类重写 addCorsMappingsPageHelper分页在复杂SQL下失效分页插件与多表Join或子查询不兼容尽量用MyBatis-Plus自带分页插件Page不要混用两个插件查询商品列表时图片URL显示不全图片路径存的是相对路径没拼接域名写一个统一的VO转换工具返回前拼接OSS完整地址JWT过期后接口报401但前端不跳转拦截器返回了错误码但没有统一处理统一异常处理器拦截401给前端固定JSON结构跳转登录页Redis缓存了商品信息后台改了不生效没有做缓存同步更新或删除在商品更新接口中手动删除缓存让下次查询自动回填第一个跨域问题特别常见。你本地用Vue起前端环境跑在8081端口后端在8080端口不配置跨域的话浏览器直接拦截你的请求。写配置的时候注意allowedOriginPatterns而不是allowedOrigins后者在新版Spring里已经不允许带*了。商品图片URL问题也比较有意思。数据库存的是/images/product/2025/xxx.jpg这种相对路径列表页展示直接拿这个相对路径去拼接在本地没问题一到Nginx部署就全裂图。后来我建了一个ProductVO把图片字段从String改成List每个元素都拼上OSS的Bucket域名才算彻底解决。5.2 围绕SpringBoot面试官最爱问的几个点做完整套项目如果只是跑通就没意义了。其实整个项目的过程就是你回答SpringBoot面试题最好的素材。我挑几个被问过多次的问题说一下面试时结合本项目怎么答第一个问题SpringBoot自动配置的原理面试官问这个是想看你是单纯背过还是真了解。你可以这样回答以RedisAutoConfiguration为例SpringBoot的spring-boot-autoconfigure包里的META-INF/spring.factories或AutoConfiguration.imports文件声明了所有自动配置类通过EnableAutoConfiguration导入结合你引入的依赖和配置项通过ConditionalOnClass、ConditionalOnMissingBean这类条件注解决定是否创建对应的Bean。我项目里RedisTemplate就是在启动时自动注入的我只需要写配置不需要自己new对象。第二个问题SpringBoot为什么比SSM好用这个问题不要列一堆空话直接说本质SSM的痛点是不在业务代码而在配置繁琐。SpringBoot解决的是把“基于最佳实践的默认配置”内置到框架里同时用spring-boot-starter-*完成依赖打包。我说个真实对比SSM建一个项目光是applicationContext.xml、spring-mvc.xml、mybatis-config.xml三个文件就要写上百行配置SpringBoot一个application.yml几十行搞定。这不是说配置没用而是框架把常见场景的配置收敛了。第三个问题SpringBoot配置文件的加载优先级这个是真的在实际部署时踩过才知道的。外部配置文件比如/config目录下的、jar包同级的优先级高于jar包内部的application.yml。这意味着运维可以直接在部署目录外放一份配置文件来覆盖jar包内的配置而不需要重新打包。生产环境数据库密码、第三方密钥一般都放在jar包外部的配置里这也是我项目采用多环境配置外部配置覆盖的原因。5.3 部署上线与环境配置经验项目部署我用的是Docker Compose把所有服务打包成容器一条命令启动。这个方案的好处是环境一致再也不用担心“我本机是好的”这种问题。我写的docker-compose.yml里主要包含三个服务MySQL 8、Redis、应用容器。这里有个参数我反复调过就是JVM内存参数。默认情况下SpringBoot应用启动就要占几百MB内存在小内存云服务器上很容易被系统杀掉。我最终在Dockerfile里设置的是FROM openjdk:8-jre-alpine COPY target/huanglego.jar /app.jar CMD [java, -Xms256m, -Xmx512m, -jar, /app.jar]部署完用docker logs看日志配合前面说的log-impl: StdOutImpl打印SQL排查线上问题效率会提高很多。6. 写在最后的个人实操心得项目做完回头看我最想对后来者说的一句话是SpringBoot只是工具箱里的一个工具整套项目最有价值的不是框架用得多花哨而是主链路的设计是否经得起推敲。我在做“欢乐购”的过程中最满意的不是某段代码写得多漂亮而是整个订单和库存主链路没有出现脏数据。这得益于我在动手写代码前花了两天时间把状态流转、表结构、接口文档全部设计好。这种设计习惯比多熟悉一个注解、多背一道面试题重要得多。还有一个很实用的建议项目做完之后一定要自己画一张架构图和数据流图放进项目文档里。一方面是自己理清思路另一方面是面试的时候直接拿图讲面试官能快速理解你的项目你也不容易讲到一半把自己讲晕。注意这里不是要求你做多高深的架构设计而是真实地记录“数据怎么流、订单怎么转、库存怎么扣”能把这三件事讲明白这个项目就成功了一大半。