基于SpringBoot+Vue.js+Redis的电商系统前后端分离实战复盘
SpringBoot、Vue.js、BootStrap、jQuery、JPA、Redis这一串关键词组合在一起其实就是目前中小型电商系统里最常见的一套“混搭”技术栈。我近期把一个基于前后端分离的SpringBoot天猫商城实战项目完整复现了一遍从用户中心、商品检索、购物车到下单、库存扣减、订单查询整条电商核心链路都跑通了。这篇文章不打算做那种教科书式的技术宣讲直接按我自己动手实现时的思路来复盘为什么要这么选每个环节具体怎么实现以及我在实际编码和调试过程中踩到过的那些坑。写这个项目的主要价值是给正在学SpringBoot全家桶、打算做毕业设计或者想自己从零搭一套电商系统的人一个完整参考。很多人学了教程里零散的知识点但不知道把它们组合在一起会碰到什么问题——比如JPA的懒加载序列化报错、Redis缓存和数据库的数据一致性、前端跨域请求带不上Cookie、秒杀场景下库存怎么扣才不超卖这些光看文档是学不到的。这篇文章会把这些细节全部拆开讲。1. 项目整体定位与技术选型的来龙去脉选技术栈之前先得搞清楚一个前提你要做的到底是什么系统。这个项目的定位是一个具备完整电商主流程的教学型商城核心模块包括用户注册登录、商品分类与展示、购物车、订单提交、库存管理、后台管理。它不是淘宝那种规模的分布式架构不需要微服务、消息队列、分库分表重的是“链路完整、交互合理、代码分层清晰”。确认这个定位之后技术选型的逻辑就清楚了。1.1 为什么是SpringBootJPARedis这一套后端组合SpringBoot在这几年几乎成了Java Web开发的事实标准它解决了配置繁琐的问题内嵌Tomcat让打包部署变得极其简单一个jar包就能跑起来这对开发效率和后续部署都是很有价值的特性。项目里选它没什么好犹豫的这不是追新而是大家都在用、资料多、上手快。JPA的选择就有点讲究了。Hibernate作为JPA的默认实现在表结构设计和复杂关联关系管理上非常高效特别是像订单表和订单明细表这种一对多关联直接用实体关系映射代码量比MyBatis写一堆XML要少很多。更重要的是JPA的懒加载机制配合Session管理能迫使你整清楚“到底什么时候访问数据库”这对手写电商项目的人来说是个很好的技术积累。不过JPA也有问题——复杂的动态查询、多表联查性能优化都非常容易踩坑这点会在后续章节详细展开。Redis在这个项目里用的是Data Redis访问框架它承担了三个核心任务热点数据缓存、购物车临时存储、分布式Session共享。特别是购物车这个场景用Redis的Hash结构实现天然适合高并发场景下的频繁读写而且还解决了集群环境下Session不共享的痛点这种组合方式在真实的互联网项目中也是比较常见的过渡方案。1.2 前端为什么“既要Bootstrap/jQuery又要Vue.js”标题里同时出现BootStrap、jQuery、Vue.js很多人第一次看到都会觉得奇怪这不是重复了吗实际在做项目的时候这种组合是完全合理的过渡状态。Bootstrap主要干的是“页面框架响应式布局”的活。商品列表、后台管理页面、登录注册的表单样式这些不追求复杂的动态交互用BootStrap的栅格系统、卡片组件、表单控件可以在很短的时间内把整站“壳子”立起来省去大量手写CSS的时间。这个项目规模不大前端不可能多层团队精细化开发先用框架把视觉效果撑住是效率最高的起手式。jQuery负责的是传统Ajax请求和DOM操作。在Vue还没有覆盖到的区域——比如后台管理页面的批量操作表格、商品上下架的按钮状态切换这些临时交互用jQuery的“插件式”写法很顺手并且它和Bootstrap的组件生态天然兼容。Vue.js则用在用户端真正需要“响应式数据驱动”的核心区域。典型的场景是购物车页面勾选某个商品、改变数量、计算总价如果全部用jQuery手动操作DOM代码会非常繁琐而且容易出错。Vue里声明一个cartList数组页面上用v-for渲染勾选状态绑定到v-model总额用v-if和v-show去控制交互逻辑立刻清晰了很多。再比如商品搜索用户输入搜索词、前端实时过滤推荐词这种场景用Vue的computed属性来做比jQuery事件叠加高了一个维度。所以这套前端组合不是重复建设而是“Bootstrap打底、jQuery做控制流、Vue做数据流”的分层配合这种模式在中小型项目中非常务实。当然如果是大型项目建议还是统一技术栈别搞这种混搭维护成本扛不住。1.3 前后端分离的关键设计决策前后端分离不等于“把HTML放到前端静态目录就完了”。真正的分离在架构上要考虑三个层面的问题接口怎么定、状态怎么存、跨域怎么处理。接口层面项目里统一约定了一个ResultT返回结构包括状态码、提示信息和数据体三个字段任何Controller方法都返回这个结构。前端只需要判断状态码是否为200就可以决定走业务正常逻辑还是错误提示逻辑不用每次解析响应出错信息。别小看这个约定等页面多起来之后没有统一的返回结构联调成本会成倍上升。状态怎么存是这个项目里必须花功夫的地方。传统的单体SSM项目Session很容易取但是目前这套系统前端部署在一个服务器、后端部署在另一个服务器两个服务之间没有Session共享机制。方案有两个方向一是用JWT前端存Token二是SpringSession把Session放到Redis中。项目实际采用的是SpringSession方案一方面是为了枚举前后端分离下顺手把Session集中管理的问题做了另一方面是因为很多购物车、登录态逻辑不必推翻重写。后面踩坑的时候会发现设置好Redis的Session序列化方式并绑定域名路径这个方案运行起来意外地顺滑。2. 后端基础设施搭建与核心代码拆解2.1 Maven多模块结构设计本项目初期直接用了单模块写了一半发现不行。User实体、Order服务、商品Repository、Controller全挤在同一个包里横向依赖严重。中途重构成了多模块结构建议一开始就按下面这个方式分mall-parent ├── mall-common // 统一返回、异常定义、工具类 ├── mall-entity // 所有JPA实体类 ├── mall-repository // 数据持久层接口 ├── mall-service // 业务逻辑层 └── mall-web // 控制器、启动类、配置文件分模块之后依赖顺序必须严格控制web依赖serviceservice依赖repository和entitycommon被所有人依赖。如果发现某个实体只有订单模块用到也不要懒直接放entity里统一管理。JPA实体在模块间传递依赖关系太乱的时候编译期报错查起来很不方便。2.2 JPA实体设计与DAO层要点实体设计是整个项目的地基。以订单为例如果表结构设计得不合理后续所有业务逻辑都会变得臃肿。核心实体关系是这样的用户User和订单Order是一对多商品Goods和订单明细OrderItem是多对多通过明细表解耦。在JPA当中配置关系时我强烈建议用双向关系来配置尤其是订单与订单明细Entity Table(name t_order) public class Order { Id GeneratedValue(strategy GenerationType.IDENTITY) private Long id; ManyToOne(fetch FetchType.LAZY) JoinColumn(name user_id) private User user; OneToMany(mappedBy order, cascade CascadeType.ALL, fetch FetchType.LAZY, orphanRemoval true) private ListOrderItem items; }为什么订单明细使用mappedBy因为关联关系只应该由一方来维护如果两边都维护保存时就会出现重复更新外键的问题。使用cascade ALL让创建订单时能级联保存明细配合事务操作一条服务方法就能把订单主表和订单明细全部落库。DAO层的接口继承JpaRepository即可但在查询时要注意两个关键点。其一凡是涉及到关联查询的列表页建议在Repository里主动加上EntityGraph(attributePaths {items})把原本懒加载的一次查询改为预加载避免后面在循环中逐条触发N1。其二不要指望JPA生成的高效SQL能覆盖所有场景复杂的动态搜索场景直接用Query写JPQL甚至原生SQL该放手就放手。2.3 Redis接入与序列化细节Redis接入本身不难但序列化的坑非常典型。项目中最常见的错误是直接在SpringBoot中把RedisTemplate的key和value序列化方式设成JdkSerializationRedisSerializer导致保存的key变成一串\xAC\xED\x00\x05t\x1A乱码Redis桌面客户端里看到的全是“外星文字”。比较稳妥的方式是建一个配置类明确指定Configuration public class RedisConfig { Bean public RedisTemplateString, Object redisTemplate( RedisConnectionFactory factory) { RedisTemplateString, Object template new RedisTemplate(); template.setConnectionFactory(factory); // key使用String序列化 template.setKeySerializer(new StringRedisSerializer()); // value使用Jackson序列化 Jackson2JsonRedisSerializerObject jsonSerializer new Jackson2JsonRedisSerializer(Object.class); ObjectMapper om new ObjectMapper(); om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY); om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL); jsonSerializer.setObjectMapper(om); template.setValueSerializer(jsonSerializer); template.setHashKeySerializer(new StringRedisSerializer()); template.setHashValueSerializer(jsonSerializer); template.afterPropertiesSet(); return template; } }需要注意value如果使用JSON序列化反序列化时对泛型类型的处理会比较麻烦尤其是ListGoods这种类型。一个常见做法是缓存商品详情时就先转成JSON字符串再手动存入读取时通过objectMapper.readValue(json, new TypeReferenceListGoods(){})来恢复。虽然代码啰嗦了一点但至少不会在运行期莫名奇妙抛类型转换异常。Redis里存的不仅是对象还有个很关键的“缓存Key约定”。项目阶段的Key设计没有统一规范的话多写几个接口就会乱套。比如商品详情缓存的Key是mall:goods:detail:{id}购物车Key是mall:cart:{userId}验证码Key是mall:sms:code:{phone}Session的Key由SpringSession自动管理。统一加上前缀以后排查问题时能一眼看出来这条数据是哪个业务模块产生的。2.4 统一的接口返回与异常处理前面提到统一返回结构具体实现上就是一个泛型类public class ResultT { private Integer code; private String message; private T data; public static T ResultT success(T data) { ResultT result new Result(); result.setCode(200); result.setMessage(操作成功); result.setData(data); return result; } public static T ResultT error(Integer code, String message) { ResultT result new Result(); result.setCode(code); result.setMessage(message); return result; } }Controller方法返回这个类型后再用RestControllerAdvice统一捕获异常特别要注意的是捕捉到MethodArgumentNotValidException后直接把校验失败的原因返回给前端而不要整段堆栈抛过去。这个异常处理器是前后端分离场景下最重要的“守门员”漏掉任何一个未捕获异常前端就只能在控制台看到一片红。碰到底层数据库异常时不建议直接把SQL异常信息拼到Response中这不仅会暴露内部细节而且前端展示也不友好。正确的做法是日志完整记录下来返回给前端的是一个更通用的错误信息“系统繁忙请稍后重试”。这个细节在高并发生产环境下尤其重要。3. 前端真机实战从静态页面到动态数据渲染3.1 BootStrap负责的“壳”与静态布局这个项目页面数量不少用户端有首页、商品列表、商品详情、购物车、订单确认、个人中心后台有商品管理、订单管理、用户管理。如果每个页面都从头手写CSS布局工作量极大而且审美上很难保证稳定好用。Bootstrap的栅格系统和预置组件非常适合电商页面的快速铺量。我的做法是先在草稿纸上粗略画一个通用的页面骨架顶部导航栏、左侧分类菜单、中间内容区、页脚版权信息。确认好结构之后用Bootstrap的Navbar、Breadcrumb、卡片式Card组件把骨架搭出来。页面先全部是静态的布局看着差不多后再逐步接数据。Bootstrap有个容易被新手忽略的细节栅格系统搭配d-none d-md-block这些响应式工具类使用就能轻松控制某个区域在手机端隐藏。电商页面里像用户评价这种次要模块小屏端直接隐藏渲染压力小体验还更好。3.2 jQuery负责的经典Ajax交互页面上很多操作属于“一次点击、一次反馈”的模式例如添加到购物车、删除商品、切换商品规格、提交订单这些如果用Vue来写其实有点“杀鸡用牛刀”用jQuery的$.ajax或者$.post会更直接。比如登录表单的提交前端先做必填校验通过后把表单数据序列化交给后端$(#loginBtn).click(function () { let username $(#username).val().trim(); let password $(#password).val().trim(); if (!username || !password) { layer.msg(用户名和密码不能为空); return; } $.ajax({ url: /api/user/login, type: POST, contentType: application/json, data: JSON.stringify({username: username, password: password}), dataType: json, success: function (res) { if (res.code 200) { location.href /index.html; } else { layer.msg(res.message); } }, error: function () { layer.msg(网络异常); } }); });这里穿插一个踩坑心得前后端分离模式下如果想用jQuery直接传明文密码务必在SpringBoot后端开启CrossOrigin时不要暴露全部接口也不要忽略CSRF风险。虽然这是教学项目但把它当成以后生产的习惯来写会少很多安全漏洞。3.3 Vue.js负责的数据驱动区域如果说jQuery是“手动挡”Vue就是“自动挡”。同一个页面里最典型的使用场景是购物车。购物车的数据放在Vue的data里勾选、取消勾选、修改数量只是改变数据页面自动重新渲染根本不用手动找DOM节点。为了不引入构建工具加大项目学习成本这里的Vue是直接通过CDN引入的不需要npm、不需要Webpack。页面里用一个div包住购物车区域整个区域内所有交互都交给Vue管理new Vue({ el: #cartApp, data: { cartList: [], selectedIds: [], editingIndex: -1 }, computed: { totalPrice() { let sum 0; this.cartList.forEach(item { if (this.selectedIds.includes(item.id)) { sum item.price * item.quantity; } }); return sum.toFixed(2); } }, methods: { changeQuantity(item, delta) { let newQty item.quantity delta; if (newQty 1) return; item.quantity newQty; // 异步请求后端更新数量 this.updateCart(item.id, newQty); }, deleteItem(id) { this.cartList this.cartList.filter(item item.id ! id); this.syncDelete(id); } } });这里最令人感慨的是用Vue之后原先jQuery里大量处理“勾选联动的总价刷新”的DOM查找代码都消失了逻辑一下清晰很多。如果你的项目里还有商品规格切换、SKU联动这种更复杂的状态Vue后面的这种自由度会更明显。3.4 前后端联调与跨域配置前后端分离上线时前端静态资源放在一个独立目录或服务器上后端跑在另外一个端口这就必须处理跨域问题。项目里开发时前端启动的地址是http://localhost:8081后端是http://localhost:8080。为了让Cookie和Session正常工作CORS不能简单粗暴全部放开。后端全局跨域配置我给了一个比较严谨的方案。允许的域名写死到配置文件中而不是*最重要的allowCredentials设为true否则浏览器会拒绝携带Cookie同时要指定allowedMethods为POST、GET、PUT、DELETE等具体动作不随意让OPTIONS请求透传Configuration public class CorsConfig implements WebMvcConfigurer { Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping(/api/**) .allowedOrigins(http://localhost:8081) .allowedMethods(GET, POST, PUT, DELETE, OPTIONS) .allowCredentials(true) .maxAge(3600); } }这里注意当你使用allowCredentials(true)时allowedOrigins不能使用通配符*必须写明确切域名浏览器会直接阻止这种不安全的跨域请求。另外Ajax请求前端也要设置xhrFields: {withCredentials: true}否则Cookie也不会自动带上。两边同时配置Session才能真正跨域共享。4. 电商核心链路实战商品、购物车、下单、库存4.1 商品列表页的缓存穿透与雪崩处理商品列表和详情是商城访问量最大的入口。如果每次请求都直接查数据库压力很快会上去。使用Redis缓存时需要考虑的不只是“存起来”还有“缓存失效后怎么办”的完整策略。热门商品的详情缓存Key是mall:goods:detail:{id}过期时间设置为30分钟。查询时先走缓存缓存不存在就查数据库把数据库结果回填到缓存这条链路大家都能理解。容易出现的是两类问题缓存穿透和缓存雪崩。缓存穿透指的是查出很多数据库根本不存在的商品ID每次都绕过缓存直接打数据库。解决方案是先查数据库中是否存在不存在则把一个空值对象也缓存起来并设置短过期时间。更硬核的做法是在业务入口加一个布隆过滤器对所有可能存在的商品ID做预估初始化命中不了就直接提示“商品不存在”且不再访问数据库。布隆过滤器这套方案在真实高并发系统中很常见教学项目里至少要理解它的原理。缓存雪崩则是指大量商品缓存集中在同一个时间点过期瞬间所有请求都落到数据库数据库在压力下被击穿。做法是在设置过期时间时加上随机偏移量比如30分钟±随机1-10分钟让过期时间错开。这是改变一行代码就能显著降低风险的操作值得所有做缓存的人留意。4.2 购物车的Redis Hash设计与一致性补偿购物车建议使用Redis的Hash结构保存Key为mall:cart:{userId}字段为商品ID值为购买数量。这样在购物车页面上遍历整个Hash就能得到所有明细。一个细节值得展开Hash里的值只是商品ID和数量商品名称、图片、单价等信息必须实时从“商品服务”中获取而不能在加入购物车时把价格快照也塞进去。原因很简单商家改价了、平台有促销活动了购物车页面显示的价格应该就是当前价格而不是用户几小时前加入时的价格。如果确实有“加车锁价”的业务需求比如双11预售价那是另外一套设计逻辑普通商城不需要沿用。实际操作时购物车相关的接口包含查询购物车列表、加入购物车、修改数量、删除单项、清空购物车。改数量时要注意Redis的HINCRBY操作它是一个原子操作天然避免了并发加减购物车数量导致的覆盖问题。伪代码风格如下public void updateCartItem(Long userId, Long goodsId, int delta) { String key mall:cart: userId; Long newQty redisTemplate.opsForHash().increment(key, goodsId.toString(), delta); if (newQty 0) { redisTemplate.opsForHash().delete(key, goodsId.toString()); } }这里还有一点补充说明改用Redis做购物车之后别忘了定期把购物车内容同步到数据库做持久化备份避免Redis宕机时用户心爱的购物车全部丢失。可以在用户主动退出登录时做一次同步也可以设置一个定时的冷备任务。完全不落库的购物车在真实业务中是不敢想象的。4.3 下单与库存扣减的超卖问题下单流程是整个项目里最容易“出事”的地方尤其是在并发请求下库存很容易超卖。一个简单的实现是前端下单后端查库存SELECT stock FROM goods WHERE id?判断stock 0后执行UPDATE goods SET stockstock-1。在并发场景下这样做是错的两个线程同时读到库存为1都判定可以购买最后的库存就成了-1。正确的资源做法应分为两层。第一层是Redis预减库存可以大幅降低数据库压力。下单时先在Redis中通过DECR命令扣减库存如果扣减后库存小于0说明库存不足后续流程直接终止。如果扣减成功再执行实际的订单生成。第二层是数据库操作要利用乐观锁。商品实体中增加一个version字段扣减库存的SQL写成UPDATE goods SET stock stock - 1, version version 1 WHERE id ? AND stock 0通过这个WHERE stock 0条件数据库行锁层面直接拦截了超卖问题。执行后判断受影响行数如果为0表示库存不足业务层就要做回滚处理。这里需要讲清楚Redis和数据库之间的状态协同。Redis预减缓存库存之后用户如果最终没有支付还要在订单超时或者用户主动取消时把Redis库存“加回来”同时修改数据库库存两边才不会出现长期不一致。在项目里我是注册了一个定时任务扫描超时未支付的订单执行关闭订单、回补库存的操作事务这个逻辑在真实电商系统里也大同小异。4.4 订单状态流转与幂等性问题订单模块的核心是状态机设计。项目里订单状态至少包括待支付、已支付、已发货、已完成、已取消。每个状态的流转必须有明确的触发动作不能存在“任意跳到任意状态”的可能性。实现上最简单的方式是用一个常量枚举类把状态定义出来在每个状态流转方法里做前置校验。幂等性也需要引起注意。用户在订单确认页不小心点了多次“提交订单”如果后端不做任何处理就会创建出好几笔重复订单。比较通用的方案是前端生成一个唯一的orderToken在下单请求中带上这个Token后端收到请求后先在Redis中以该Token为Key执行SETNX如果执行成功说明是首次请求继续下单逻辑如果执行失败说明重复请求直接返回“请勿重复提交”。订单生成完毕之后的支付回调也是电商项目的常见场景。由于教学项目没有对接真实的支付渠道我使用轮询支付状态来实现模拟支付。提交订单后前端每隔3秒请求一次/api/order/status/{orderId}查到状态为“已支付”后再跳转到订单详情页。如果用户选择取消支付前端调用取消接口订单状态“待支付”切换到“已取消”同时释放库存。整条链路跑通后项目成品的完整度会很明显地上一个台阶。5. 部署上线与性能优化实战5.1 jar包部署与前端静态资源托管这个项目前后端分离部署时后端只需要把Maven打出来的jar包放到服务器上使用默认的内嵌Tomcat启动java -jar mall-web-1.0.0.jar --spring.profiles.activeprod前端静态资源不在jar包内我在服务器上使用Nginx单独托管。Nginx不仅负责静态文件服务还要把/api/开头的请求反向代理到后端的8080端口。这样部署之后浏览器访问前端地址请求接口时走的是同域跨域问题在正式环境就不存在了Cookie和Session机制也就顺理成章。Nginx的关键配置形如下面这样注意proxy_set_header中的主机和IP信息必须带上否则后端的请求日志里看到的一律是127.0.0.1排查问题会困难不少server { listen 80; server_name mall.example.com; location / { root /opt/frontend; index index.html; try_files $uri $uri/ /index.html; } location /api/ { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } }5.2 多环境配置与数据库初始化和很多教学项目一个环境跑到底不同这个项目分了dev和prod两个配置文件通过spring.profiles.active切换。开发环境使用本地数据库和本地Redis生产环境当然是用云厂商或自行搭建的服务。绕不开的一个细节是数据库表结构初始化。JPA有ddl-auto配置开发时可以设置update让程序自动维护表结构生产环境中必须设置为none由运维执行SQL脚本避免应用启动时篡改表结构。推荐的做法是提供一个基础的schema.sql和data.sql里面除了建表语句还包含管理员账号、初始分类数据、测试商品等。这样任何一个新同事把项目拉下来运行数据库脚本就能直接开始开发不用为环境配置折腾一整天。5.3 性能压测与热点接口优化项目跑完所有功能后我在真实运行数据上做了简单的性能测试。当数据库中商品数据量只有几百条时商品列表接口的响应速度很快当把数据量人工灌到几十万条时“分类查询排序”的接口性能问题就开始暴露了。观察SQL日志不难发现问题往往出在关联表查询没有合理利用索引。具体的优化措施有三个第一给订单表增加(user_id, status)的联合索引用户查看自己的订单列表时过滤效率和排序效率同时提升。第二商品表加(category_id, sales_count)的联合索引让热门商品排序走索引而不用全表扫。第三最核心的商品列表和详情接口在Redis中做缓存的同时后端加了一个CacheEvict注解当管理者修改商品信息时自动删除对应缓存保证更新的即时失效。这套优化做完后页面响应时间在测试数据下从原来的600ms左右降到100ms以内。当然真实生产环境还有更多层次的优化空间像是分库分表、读写分离、消息队列削峰等但至少在这个量级的项目中上面的优化已经能让系统跑得非常轻快。6. 常见问题与排查技巧实录6.1 Redis序列化乱码、类型转换异常这个坑出现的频率极高。项目里最容易遇到的问题就是Redis存进去的key在客户端看完全不可读或者取出时提示类型无法转换。核心原因就是之前提到的序列化方式没有统一配置。解决思路很明确所有通过RedisTemplate写入的key和value统一使用StringRedisSerializer加JSON序列化器。如果只用了StringRedisTemplate存字符串就千万不要混着RedisTemplate去读否则类型对不上反序列化必然报错。调试的时候还有一个实操技巧代码报错后不要光看日志直接用Redis客户端检查对应Key是否存在、里面存的值是什么格式。这样很快就能判断问题是出在缓存未命中还是数据被污染。能省去很多猜测的时间。6.2 JPA懒加载导致JSON序列化报错用好JPA几乎必然会在Controller返回实体时遇到懒加载序列化失败的问题。原因是Jackson序列化实体时JPA的懒加载代理对象还没有初始化一旦序列化触发到关联属性Hibernate就会抛出LazyInitializationException。我解决这个问题的思路很简单坚持“Controller不返回实体始终返回VO”。专门创建GoodsVO、OrderVO这些视图对象搭配合适的构造方法或者工具方法从实体中抽取需要的字段组装成VO返回给前端。这样不仅解决了懒加载异常而且不会把实体中不想暴露的字段比如密码哈希泄露出去。可选方案是在项目里引入jackson-datatype-hibernate5模块序列化时先强制初始化关联对象但这样只是掩盖问题长期来看不如VO方案干净。6.3 跨域请求带不上Cookie导致登录态失效前面虽然配置了CORS但实际联调过程中这个问题还是反复出现过。最常见的情况是前端登录成功了但是再发起购物车请求时后端一直认为是未登录状态原因是前端没有在Ajax请求中带Cookie。解决方案前面提过jQuery的$.ajaxSetup里统一设置xhrFields: { withCredentials: true }如果使用原生fetch则要设置credentials: include。另外一个同样容易漏掉的是Cookie的SameSite属性。浏览器默认的SameSite策略如果是Lax跨站的Ajax请求就不会携带Cookie这种场景下需要在后端配置中显式把Session Cookie的SameSite设置为None同时加上Secure属性。实际操作下来这组配置和跨域配置需要配合调整很多项目上线后“用户登录后时不时掉线”的问题大概率就出在这里。6.4 Redis缓存与数据库数据不一致商城里的商品基本信息一般不会频繁变动但在后台修改商品价格或者下架后就存在缓存和数据库临时不一致的问题。这个问题不存在代码层面的万能解锁只有权衡后的方案。项目里的做法是“Cache Aside”模式更新数据库时直接删除缓存不主动设置缓存更新等到下一次读请求时发现缓存不存在再回填新的数据库内容。这个方案对商品这类读多写少的场景很适用而且实现成本极低。要注意的是删除缓存和更新数据库这两个动作之间有时间窗口如果恰好有并发读请求可能读走旧数据后又回填了旧数据导致缓存脏数据存在一段时间。为了避免这个问题可以给回填加一个短暂的过期时间即使缓存被旧数据污染也会很快过期刷新。还有一种思路是引入消息队列但在这个量级的项目中完全没必要过度设计本身就是一种成本。6.5 下单并发导致超卖超卖问题的排查需要同时看两块内容。如果发现Redis库存已经是负值说明预扣减之后回补逻辑出了问题如果Redis库存和数据库库存对不上则很大可能性是某个操作没有在同一个事务中执行。调试时我用一个脚本模拟一百个并发请求抢购库存只有10件的商品结果大多数情况下数据库库存最终是负值。后来通过打印SQL日志发现更新语句没有带stock 0这个条件修正SQL之后并发测试才通过。并发问题测试不建议人工点页面一个简单的并发请求脚本比如用Jmeter就可以稳定复现问题。遇到超卖时先单独验证SQL语句再验证事务边界再检查Redis和数据库的同步逻辑。排错顺序按这个节奏走很少会有排查不清楚的情况。从我个人的实际操作体验来看把这一整套商城系统从零写一遍比看十套视频教程都有用。尤其是那些只有自己动手踩一遍才会理解的问题比如JPA为何不能在Service层之外调用懒加载对象、CORS和Cookie之间为何需要微妙的配置平衡、Redis的缓存结构为何不能草率地全部用String一把梭这些问题不做项目是完全不会产生体感的。如果正准备复现这个项目我建议你也照着“前后端分离 SpringBoot JPA Redis”这个组合去做。前端不要一步跨到特别复杂的工程化架构里面就把交互最重的几个区域购物车、订单确认、商品规格用Vue管理起来其余页面用Bootstrap和jQuery快速铺完你会发现在有限时间里能顾全并跑通整条电商闭环这种“完成感”比单点深入一个技术细节重要得多。项目完成之后如果还有余力可以继续在这个基础上去加搜索功能、促销组件、单点登录这些扩展点后面的路就好走很多了。

相关新闻

PDF敏感信息遮盖、扫描件擦除与去水印的彻底处理指南

PDF敏感信息遮盖、扫描件擦除与去水印的彻底处理指南

聊到PDF处理,日常被问得最多的三件事就是:合同里的身份证号怎么遮掉再发人、扫描件上的手写标注怎么弄干净、文档页眉那个显眼的“内部资料”水印怎么去掉。这三个需求,说难不难,但很多人第一反应是打开画图工具直接涂黑&#xff…

2026/10/16 4:34:22 阅读更多 →
ARM Linux下OpenCV交叉编译实战:从工具链配置到部署踩坑

ARM Linux下OpenCV交叉编译实战:从工具链配置到部署踩坑

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

2026/10/16 4:34:22 阅读更多 →
LLM驱动的移动端自动化测试:ARTEMIS双模式架构解析

LLM驱动的移动端自动化测试:ARTEMIS双模式架构解析

先说一个我在移动端自动化测试上反复撞墙后的真实感受:大多数团队从纯手工点按切换到自动化脚本之后,省下的时间并没有想象中那么多,反而多出一堆修脚本、调选择器、处理弹窗超时的活儿。这两年LLM的出现确实把不少顽疾松动了,但真…

2026/10/16 4:34:22 阅读更多 →

最新新闻

CVAE的TensorFlow实现:从原理到工程落地全解析

CVAE的TensorFlow实现:从原理到工程落地全解析

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

2026/10/16 5:14:32 阅读更多 →
Spring Cloud Gateway WebSocketRoutingFilter源码解析与实战避坑指南

Spring Cloud Gateway WebSocketRoutingFilter源码解析与实战避坑指南

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

2026/10/16 5:14:32 阅读更多 →
Spring Cloud Gateway NettyRoutingFilter源码解析与转发机制详解

Spring Cloud Gateway NettyRoutingFilter源码解析与转发机制详解

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

2026/10/16 5:14:32 阅读更多 →
两串锂电池主动均衡方案:MP2642GR-Z与STM32F415ZG实战

两串锂电池主动均衡方案:MP2642GR-Z与STM32F415ZG实战

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

2026/10/16 5:14:32 阅读更多 →
PyQt5桌面开发实战:鼠标坐标、右键禁用与QScrollArea尺寸调整

PyQt5桌面开发实战:鼠标坐标、右键禁用与QScrollArea尺寸调整

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

2026/10/16 5:14:32 阅读更多 →
Qt/QML多语言国际化实战:从tr/qsTr到动态切换

Qt/QML多语言国际化实战:从tr/qsTr到动态切换

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

2026/10/16 5:13:32 阅读更多 →

日新闻

个人 AI Agent 平台接入 TaoToken:统一 Key 与 API 通道的配置大纲

个人 AI Agent 平台接入 TaoToken:统一 Key 与 API 通道的配置大纲

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

2026/10/16 0:00:05 阅读更多 →
信号与系统频域分析作业解题指南

信号与系统频域分析作业解题指南

1. 作业背景与核心要求解析信号与系统作为电子信息类专业的核心基础课,其作业设计往往直接关联课程的核心知识框架。2026年春季第六次作业的布置,通常对应着课程进展到频域分析的关键阶段。根据常规教学进度推测,本次作业可能涉及以下核心内容…

2026/10/16 0:00:05 阅读更多 →
Django MVT实战:从零构建图书借阅系统,彻底搞懂Model-View-Template分层

Django MVT实战:从零构建图书借阅系统,彻底搞懂Model-View-Template分层

1. 从一个"四不像"的练手项目说起如果你已经看完了Django的官方教程前几章,跟着敲完了投票应用,大概会有一种"我好像会了,又好像什么都不会"的割裂感。教程里每个知识点都是拆开的——这一章讲模型,下一章讲视…

2026/10/16 0:00:05 阅读更多 →

周新闻

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

简介:基于 ARIMA、LSTM、Transformer 等模型的流感时间序列预测 Python 源码,面向计算机相关专业课程设计与期末大作业学生,以及项目实战学习者。内容覆盖预处理、平稳性检验、定阶、残差分析、多模型对比预测的完整时序建模流程,…

2026/10/15 16:18:54 阅读更多 →
影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别 做影刀RPA自动化,十个新手有八个栽在"往输入框里填东西"这件事上:要么填不进去,要么填了一半,要么直接把原来内容追加在后面。这背后的根因&…

2026/10/15 19:41:51 阅读更多 →
影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容 1. 认识影刀:什么场景该用RPA采小说数据 起点中文网的页面结构相对稳定——分类榜单、书籍详情、章节内容三块独立页面,跳转链路清晰。这种场景非常适合影刀自动化&#x…

2026/10/15 13:48:15 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

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

2026/10/15 0:14:36 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

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

2026/10/16 3:49:43 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

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

2026/10/15 9:49:14 阅读更多 →