ARTICLE DETAIL

资讯详情

深耕商务建站与企业官网运营的一线实战洞察。

Spring Boot家具电商实战:SKU多规格、Redis购物车与状态机订单

Spring Boot家具电商实战:SKU多规格、Redis购物车与状态机订单 简介这是一套面向Java初学者与Spring Boot进阶开发者的家具电商实战项目源码聚焦Web应用开发全流程实践适用于课程设计、毕业设计及中小型商城系统二次开发。资源包含798个文件主体为115个Java后端业务类、45个Vue前端组件、164个JS交互逻辑与162个SVG图标资源辅以CSS样式、HTML模板、SQL建表脚本及yml配置文件完整覆盖前后端分离架构压缩包仅13.86MB轻量易部署。已有458人学习下载项目结构规范含install/run/build三阶段bat脚本预览可见IndexMain.vue.bak等模块化视图文件及index.html.bak等静态入口体现清晰的MVC分层与组件化设计思路。读者可直接运行调试深入理解Spring Boot自动配置、MyBatis数据访问、JWT鉴权、Thymeleaf模板渲染及Redis缓存集成等核心实践快速掌握企业级电商系统开发范式。1. 这不是一个“能跑就行”的 Spring Boot 商城 demo而是一套可直接切入真实家具电商业务逻辑的 Java 工程骨架你打开1-install.bat执行完看到BUILD SUCCESS再双击2-run.bat启动成功——这不叫掌握这个项目。真正有价值的是它用MyBatis-Plus封装了商品 SKU 多规格组合如“橡木色1.8m带抽屉”在ProductServiceImpl里通过LambdaQueryWrapper动态拼接查询条件规避了传统 XML 中if test...嵌套过深导致的 SQL 可读性崩塌它的购物车不是存 Session而是用RedisTemplateString, CartItemVO存储key 设计为cart:${userId}value 序列化用GenericJackson2JsonRedisSerializer避免了 JDK 默认序列化在升级后反序列化失败的线上事故它的订单状态流转不是靠 if-else 判断而是用StateMachineBuilder配置了WAIT_PAY → PAID → SHIPPED → COMPLETED的有向状态图并在OrderStateListener中监听STATE_ENTERED事件触发库存扣减和物流单号生成。这套代码没写一行注释讲“Spring Boot 是什么”但它每一处Transactional(rollbackFor Exception.class)、每一个Validated分组校验、每一条application-prod.yml里的spring.redis.jedis.pool.max-wait3000ms都在回答一个实际问题当家具类目日均 UV 5 万、SKU 超 2000、下单峰值达 300 TPS 时Java 层怎么扛住它适合两类人刚写完 CRUD 想看真实业务如何落地的 Java 初级开发者以及需要快速搭建家具垂类 MVP 并验证供应链对接流程的技术负责人。2. 从1-install.bat到IndexMain.vue.bak解析三层构建链路与前后端耦合点2.1 构建脚本链.bat文件背后的真实 Maven 生命周期控制项目根目录下三个批处理文件不是简单封装mvn clean package。1-install.bat的核心命令是mvn clean compile -Dmaven.test.skiptrue -Pdev其中-Pdev激活pom.xml中定义的devprofile该 profile 绑定了mybatis-plus-generator插件在compile阶段自动生成entity、mapper、service三层代码——这意味着你修改数据库表结构后只需重跑1-install.batProduct.java和ProductMapper.java就会同步更新无需手动维护 POJO 字段。2-run.bat实际执行java -jar target/furniture-shop-0.0.1-SNAPSHOT.jar --spring.profiles.activedev --server.port8081注意--server.port8081覆盖了application-dev.yml中的默认端口这是为避免与本地已启动的其他 Spring Boot 服务冲突。而3-build.bat则调用mvn clean package -Pprod -Dmaven.test.skiptrue其中prodprofile 关闭了 HikariCP 连接池的leakDetectionThreshold生产环境禁用内存泄漏检测并启用spring-boot-maven-plugin的repackage目标生成可执行 jar。提示若执行1-install.bat报错Could not resolve dependencies for project检查pom.xml中repository是否指向了公司内网 Nexus 地址如http://nexus.internal:8081/repository/maven-public/而非默认的 Maven Central。此时需将settings.xml中的mirror配置同步到本地 Maven 安装目录的conf/下。2.2 前端静态资源.vue.bak文件揭示的渐进式迁移路径目录中大量.vue.bak文件如IndexMain.vue.bak并非备份而是项目从传统 JSP/Thymeleaf 模板引擎向 Vue 单页应用迁移的中间态证据。以IndexMain.vue.bak为例其script区域包含export default { data() { return { productList: [], categoryList: [] } }, mounted() { this.loadProducts() this.loadCategories() }, methods: { loadProducts() { // 注意此处调用的是 /api/product/list 接口而非 Thymeleaf 的 {/product/list} axios.get(/api/product/list).then(res { this.productList res.data.data }) } } }这说明后端已提供标准 RESTful API路径前缀/api/但前端尚未完成 Vue Router 路由配置故仍依赖index.html.bak中的div idapp/div作为挂载点。对比IndexAsideStatic.vue.bak中的硬编码导航菜单lia href/admin/product商品管理/a/li lia href/admin/order订单管理/a/li这些/admin/*路径指向 Spring Security 配置的antMatchers(/admin/**).hasRole(ADMIN)证明权限控制已下沉到 Controller 层而非前端路由守卫。这种混合模式允许团队分阶段改造先确保后端 API 稳定再逐步替换前端视图层。2.3 后端核心模块ProductController与ProductServiceImpl的事务边界设计ProductController中的关键方法签名如下PostMapping(/list) public ResultListProductVO list(RequestBody Validated ProductQuery query) { return Result.success(productService.listByQuery(query)); }ProductQuery类使用Min(value 1, message 页码必须大于0)等注解进行参数校验校验失败时由GlobalExceptionHandler拦截并返回Result.fail(页码必须大于0)。而ProductServiceImpl.listByQuery()方法内部实现为Override Transactional(readOnly true) public ListProductVO listByQuery(ProductQuery query) { LambdaQueryWrapperProduct wrapper new LambdaQueryWrapper(); wrapper.eq(query.getCategoryId() ! null, Product::getCategoryId, query.getCategoryId()) .like(query.getKeyword() ! null, Product::getName, query.getKeyword()) .orderByDesc(Product::getSalesCount); return productMapper.selectList(wrapper).stream() .map(this::convertToVO).collect(Collectors.toList()); }这里Transactional(readOnly true)显式声明只读事务HikariCP 会自动设置Connection.setReadOnly(true)数据库可据此优化执行计划如 MySQL 5.7 对只读事务禁用 binlog 写入。更关键的是convertToVO()方法中对关联数据的处理private ProductVO convertToVO(Product product) { ProductVO vo new ProductVO(); BeanUtils.copyProperties(product, vo); // 避免 N1 查询此处应通过 MyBatis-Plus 的 SelectJoinTable 注解或手写 SQL JOIN 获取品牌名 vo.setBrandName(brandService.getById(product.getBrandId()).getName()); return vo; }这段代码存在性能隐患——若一次查 20 个商品就会触发 20 次brandService.getById()查询。正确做法是在ProductMapper.xml中编写 JOIN SQLselect idselectProductWithBrand resultTypecom.example.furniture.vo.ProductVO SELECT p.*, b.name as brandName FROM product p LEFT JOIN brand b ON p.brand_id b.id WHERE p.category_id #{categoryId} /select然后在ProductMapper接口中定义ListProductVO selectProductWithBrand(Param(categoryId) Long categoryId);。这正是源码中未完全优化但留出明确改进路径的设计。3. 数据库与安全MySQL 表结构设计与 Spring Security 权限模型落地3.1 核心表关系product、product_sku、order的范式化实践项目使用 MySQL 8.0product表存储家具基础信息名称、描述、主图 URL而具体销售单元如“北欧风布艺沙发-米白色-2.2m”存于product_sku表其关键字段包括字段名类型说明idBIGINT PKSKU 主键product_idBIGINT FK关联product.idspecificationJSON存储规格组合如{color:米白色,length:2.2m}stockINT实时库存下单时扣减priceDECIMAL(10,2)销售价格这种设计避免了为每个规格单独建表如sofa_white_220同时利用 MySQL 5.7 的 JSON 函数支持动态查询-- 查找所有“米白色”且长度≥2m的沙发 SKU SELECT * FROM product_sku WHERE product_id IN (SELECT id FROM product WHERE category_id 101) AND JSON_CONTAINS(specification, 米白色, $.color) AND CAST(JSON_EXTRACT(specification, $.length) AS DECIMAL) 2.0;order表则采用乐观锁控制并发超卖version字段初始为 0每次更新库存时UPDATE product_sku SET stock stock - 1, version version 1 WHERE id ? AND version ?若ROW_COUNT() 0则抛出OptimisticLockException触发重试逻辑。3.2 Spring Security 配置基于角色的 URL 访问控制与密码加密策略SecurityConfig.java中定义了细粒度权限Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests() .antMatchers(/api/user/login, /api/user/register).permitAll() .antMatchers(/api/product/**, /api/category/**).permitAll() // 商品浏览无需登录 .antMatchers(/api/cart/**, /api/order/**).authenticated() // 购物车和订单需登录 .antMatchers(/api/admin/**).hasRole(ADMIN) // 后台管理仅管理员 .anyRequest().authenticated(); }用户密码加密使用BCryptPasswordEncoder强度 factor12其encode()方法生成的密文形如$2a$12$ZQVJzX9YvKqLmNpOcRtS.uEwFgHiJkLmNoPqRsTuVwXyZaBcDeFgH前缀$2a$表示 BCrypt 算法12为迭代轮数。在UserDetailsServiceImpl.loadUserByUsername()中框架自动比对明文密码与密文哈希值无需手动调用matches()。注意application-dev.yml中spring.security.user.name和password仅用于开发环境快速登录生产环境必须禁用此配置改用数据库用户表认证。3.3 敏感操作审计PreAuthorize注解与操作日志切面对高危操作如删除商品、修改订单状态使用 SpEL 表达式校验PreAuthorize(securityService.canDeleteProduct(#productId, principal.username)) DeleteMapping(/{id}) public ResultVoid delete(PathVariable Long id) { productService.removeById(id); return Result.success(); }securityService.canDeleteProduct()方法检查当前用户是否为商品创建者或 ADMIN 角色。同时OperationLogAspect切面捕获所有PostMapping方法的执行Around(annotation(org.springframework.web.bind.annotation.PostMapping)) public Object logOperation(ProceedingJoinPoint joinPoint) throws Throwable { long start System.currentTimeMillis(); Object result joinPoint.proceed(); long cost System.currentTimeMillis() - start; // 记录到 operation_log 表操作人、URL、参数、耗时、返回状态 operationLogService.save(new OperationLog( SecurityContextHolder.getContext().getAuthentication().getName(), ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest().getRequestURL().toString(), JSON.toJSONString(joinPoint.getArgs()), cost, result instanceof Result ? ((Result?) result).getCode() : 500 )); return result; }该日志表结构包含operator操作人、url、paramsJSON 字符串、cost_time毫秒、status_codeHTTP 状态码为后续排查“谁在什么时间删了哪个商品”提供直接证据。4. 生产就绪配置Redis 缓存穿透防护与日志分级策略4.1 缓存设计CacheManager配置与空值缓存防穿透RedisConfig.java中定义了两级缓存策略Bean public CacheManager cacheManager(RedisConnectionFactory factory) { RedisCacheConfiguration config RedisCacheConfiguration.defaultCacheConfig() .entryTtl(Duration.ofHours(2)) // 默认 2 小时过期 .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer())) .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer())); // 为商品详情单独配置10 分钟过期且启用空值缓存 MapString, RedisCacheConfiguration cacheConfigurations new HashMap(); cacheConfigurations.put(productDetail, config.entryTtl(Duration.ofMinutes(10))); cacheConfigurations.put(categoryList, config.entryTtl(Duration.ofHours(1))); return new RedisCacheManager.RedisCacheManagerBuilder(factory) .cacheDefaults(config) .withInitialCacheConfigurations(cacheConfigurations) .build(); }在ProductServiceImpl.getProductDetail()方法上添加CacheableCacheable(value productDetail, key #id, unless #result null) public ProductVO getProductDetail(Long id) { Product product productMapper.selectById(id); if (product null) { // 防穿透缓存空对象过期时间设为 2 分钟短于正常缓存 redisTemplate.opsForValue().set(product:detail: id, null, Duration.ofMinutes(2)); return null; } return convertToVO(product); }当查询不存在的商品 ID如id999999时redisTemplate手动写入null字符串后续请求直接命中 Redis 返回空避免穿透到数据库。unless #result null确保只有非空结果才进入CacheManager的自动缓存流程。4.2 日志体系Logback 分环境输出与 ERROR 级别告警logback-spring.xml中定义了三套输出策略!-- 开发环境控制台输出 DEBUG 级别 -- springProfile namedev root levelDEBUG appender-ref refCONSOLE/ /root /springProfile !-- 测试环境文件输出 INFO 级别按天滚动 -- springProfile nametest appender nameFILE classch.qos.logback.core.rolling.RollingFileAppender filelogs/app-test.log/file rollingPolicy classch.qos.logback.core.rolling.TimeBasedRollingPolicy fileNamePatternlogs/app-test.%d{yyyy-MM-dd}.%i.log/fileNamePattern /rollingPolicy encoder pattern%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n/pattern /encoder /appender root levelINFO appender-ref refFILE/ /root /springProfile !-- 生产环境ERROR 级别独立文件 控制台 -- springProfile nameprod appender nameERROR_FILE classch.qos.logback.core.rolling.RollingFileAppender filelogs/error-prod.log/file filter classch.qos.logback.core.filter.LevelFilter levelERROR/level onMatchACCEPT/onMatch onMismatchDENY/onMismatch /filter !-- ... 滚动策略 -- /appender root levelWARN appender-ref refERROR_FILE/ appender-ref refCONSOLE/ /root /springProfile关键点在于prod环境下ERROR_FILE使用LevelFilter严格隔离 ERROR 日志确保运维人员收到告警邮件时日志内容不含 INFO/WARN 噪声。同时root级别设为WARN保证System.out.println()等调试语句不会污染生产日志。5. 快速验证与压测技巧用 curl 模拟高并发下单与 Redis 监控命令5.1 接口连通性验证三步确认核心链路健康在项目启动后2-run.bat成功执行以下命令验证# 1. 检查服务存活返回 HTTP 200 curl -I http://localhost:8081/actuator/health # 2. 查询商品列表验证 MyBatis-Plus MySQL 连通 curl http://localhost:8081/api/product/list?pageNum1pageSize10 | jq .code # 3. 模拟用户登录获取 token验证 Spring Security TOKEN$(curl -s -X POST http://localhost:8081/api/user/login \ -H Content-Type: application/json \ -d {username:admin,password:123456} | jq -r .data.token) # 4. 用 token 添加商品到购物车验证 Redis 认证 curl -X POST http://localhost:8081/api/cart/add \ -H Authorization: Bearer $TOKEN \ -H Content-Type: application/json \ -d {productId:1,skuId:1,count:2}若第 4 步返回{code:200,msg:success}说明从认证、缓存、数据库写入的全链路已打通。jq命令需提前安装macOSbrew install jqWindowschoco install jq。5.2 Redis 实时监控识别缓存雪崩与热点 Key连接 Redis 服务器后执行以下命令定位问题# 查看当前所有 key 的内存占用按大小降序 redis-cli --bigkeys # 监控最近 1 秒内被访问最多的 10 个 key识别热点 redis-cli --hotkeys # 检查是否存在大量过期 key 导致的 CPU 占用飙升 redis-cli info | grep expired_keys # 手动触发 key 清理生产环境慎用 redis-cli config set activedefrag yes针对家具商城场景重点监控cart:*和productDetail:*前缀的 key。若cart:*key 数量随用户数线性增长如 10 万用户产生 10 万个 key需评估是否启用 Redis Cluster 分片若productDetail:123的 TTL 频繁重置为 10 分钟说明该商品被高频访问应考虑将其提升至本地 Caffeine 缓存。5.3 JMeter 压测下单接口验证库存扣减的线程安全性创建 JMeter 测试计划配置线程组100 线程循环 10 次HTTP 请求目标为POST http://localhost:8081/api/order/createBody Data 为{ cartItems: [ {skuId: 1, count: 1}, {skuId: 2, count: 1} ], addressId: 1001 }添加“响应断言”检查返回$.code 200添加“聚合报告”观察错误率。若错误率 0%检查OrderServiceImpl.createOrder()中的库存扣减逻辑// 正确使用 Redis Lua 脚本保证原子性 String script if redis.call(exists, KEYS[1]) 1 then local stock tonumber(redis.call(hget, KEYS[1], stock)) if stock tonumber(ARGV[1]) then redis.call(hincrby, KEYS[1], stock, 0-ARGV[1]) return 1 end end return 0; Long result redisTemplate.execute(new DefaultRedisScript(script, Long.class), Collections.singletonList(sku:1), 1); if (result 0) throw new BusinessException(库存不足);此脚本在 Redis 服务端原子执行彻底规避 JVM 层多线程竞争。若压测中出现超卖则说明代码中仍存在getStock()updateStock()的非原子操作需立即替换为 Lua 方案。本文还有配套的精品资源点击获取
返回列表
PREV
查看更多资讯
NEXT
返回资讯列表