news 2026/9/30 3:06:30

Spring Boot+Vue 3家具商城实战:数据库设计、库存扣减与Docker部署

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Spring Boot+Vue 3家具商城实战:数据库设计、库存扣减与Docker部署

1. 家具商城这个选题,难的不是商城而是家具

先说个有意思的现象。每年到毕设季,或者想自己做点东西充实简历的时候,"商城系统"永远是出现频率最高的题目。书城、电商城、服装城、二手交易平台,改个名字就是一套新的。但如果你真打算做一个能拿得出手的家具商城Spring Boot + Vue 3项目,我的建议是:别急着写代码,先把"家具"这两个字琢磨透。

为什么这么说?因为普通电商和家具电商的系统设计差距,远比你想象的大。

家具商品有几个非常典型的特征,是图书、数码产品甚至服装都不太会遇到的。第一,体积和重量是核心交易要素。一张实木床和一个蓝牙耳机,在下单流程里涉及的信息密度完全不同。耳机你只需要管SKU和库存,床你得管尺寸、重量、是否包安装、有没有电梯、能不能进楼道。这些信息如果不在商品详情和下单页面里体现,用户买回去发现进不了门,售后纠纷直接拉满。第二,非标属性复杂。同一款沙发,可能有"三人位布艺款""三人位真皮款""四人位L型款",颜色、材质、尺寸排列组合起来就是一堆SKU。如果数据库设计阶段没把SKU的维度和库存关系理清,后面加属性就是牵一发动全身。第三,订单履约链路长。普通快递物流和家具的大件物流、上楼安装、旧家具处理完全是两套体系,订单状态机如果只设计成"待付款-已付款-已发货-已完成"这种四态模型,基本撑不住真实业务。

说白了,家具商城是一个"商品模型复杂 + 订单模型复杂 + 售后模型复杂"的综合体。把这三个复杂度吃下来,你收获的绝不是一个花架子demo,而是一套能迁移到任何中大型业务系统的建模思路。这也就是为什么我建议你选家具这个垂直品类练手,而不是又做一个消费电子商城。

这篇文章我会按自己做这个项目时的真实推进顺序来讲:从技术选型,到数据库设计,到前后端核心代码,再到部署上线,最后是几个容易翻车的坑。文章里出现的所有代码思路都基于Spring Boot 3.x + Vue 3 + MySQL + Redis这套经典组合,你可以直接照着搭。

2. 技术选型不是跟风:Spring Boot 和 Vue 3 各自解决什么问题

做技术选型的时候,我见过太多人纠结"Spring Boot到底比Spring MVC强在哪""Vue 3比Vue 2难用"这类问题。其实你换个角度想就通了:选择框架的本质是选择一套解决协作效率和工程化问题的规范。

2.1 Spring Boot 在后端承担的角色

如果你用过早期的Spring项目,你一定记得那些繁琐的XML配置,还要自己维护Tomcat、手动处理依赖版本冲突。Spring Boot最核心的价值,就是把"配置"这件最没有技术含量又最耗时间的事情,从你身上拿走了。

它通过自动装配机制,根据你在pom.xml里引入的依赖,自动帮你配置好数据源、事务管理器、Web容器、JSON序列化组件等等。这就是为什么你写一个spring-boot-starter-web起步依赖,什么都不用配,直接启动就能访问到Controller返回的接口。自动装配的原理其实不复杂:@SpringBootApplication注解里内置了@EnableAutoConfiguration,Spring Boot启动时会去读取META-INF/spring.factories或AutoConfiguration.imports文件里声明的自动配置类,然后通过@ConditionalOnClass、@ConditionalOnMissingBean这类条件注解,决定"这个组件在什么情况下才帮你配好"。

以用户登录为例,你会引入spring-boot-starter-security或者自己写JWT拦截器。如果你用Spring Security的自动配置,它会默认帮你生成一个登录页面和密码校验规则;但你实际的登录逻辑肯定要自定义,于是你实现UserDetailsService接口,覆盖它的loadUserByUsername方法,Spring Boot检测到你自己的实现后就会放弃默认的,直接使用你提供的。这就是@ConditionalOnMissingBean的作用:你只要提供更具体的实现,框架就不会再自作主张。

2.2 Vue 3 在前端承担的工程化能力

再说Vue 3。不夸张地说,组合式API(Composition API)是把Vue项目的组织方式从"散装零件"升级成了"模块化组装"。

Vue 2时代我们习惯了在data里放状态、在methods里放方法、在watch里监听变化,一个功能的变化逻辑被拆散到三个地方。写过稍微复杂点的后台管理系统的人,一定体会过"为了改一个搜索功能,要在模板、数据、方法、监听器之间来回跳"的痛苦。Vue 3的组合式API让你可以按功能维度组织代码,把某个业务模块涉及的状态、方法、计算属性放在一个setup区域里,这种组织方式对后台管理系统这种表格多、表单多、弹窗多的场景简直太友好了。

同时,Vue 3的响应式系统也换了底层实现,用Proxy替换了Object.defineProperty,所以对象新增属性、删除属性、数组索引修改都能被响应式追踪到,不再像Vue 2那样动不动就"改了数据但视图不更新"。这个特性在做购物车修改数量、商品筛选多条件联动时特别实用,少踩很多"手动this.$set才能刷新"的坑。

2.3 前后端分离的协作边界

单说框架还不够,得把"前后端分离"这个工程模型的边界理清楚。这套系统里,后端的职责是:提供稳定的数据接口、处理事务和并发、保证数据安全。前端的职责是:管好页面路由、交互状态、用户体验。两者通过HTTP接口用JSON通信,不共享模板,不共享Session。

实际开发中我会遵守三条约束。第一,后端接口必须幂等。特别是下单和支付回调这类接口,网络重试可能导致重复提交,后端要做防重处理。第二,前端所有请求统一走封装的axios实例,baseURL、请求拦截器(自动携带token)、响应拦截器(统一处理401跳转、错误提示)都集中管理,不要在页面里到处写axios.get。第三,接口文档先行。就算只有两个人开发,也要先定义好/api/goods/page、/api/goods/detail/{id}、/api/cart/add、/api/order/submit这些接口的路径、参数、返回值,再各自开发。前后端联调大部分吵架的场景,本质都是接口约定没对齐。

3. 数据库设计:家具 SKU、库存与订单状态机的串联逻辑

如果你去网上搜"商城系统数据库设计",会看到很多标准答案:用户表、商品表、订单表、订单项表,各表主键外键一连,看似完整。但放到家具场景里,这套模型会在"商品规格"和"物流履约"上直接漏风。

3.1 商品模型:SPU、SKU与家具属性表

先说结论,我的设计是四张核心表:product_spu(商品标准单元)、product_sku(库存量单元)、product_attribute(属性定义)、product_attribute_option(属性选项)。

SPU对应你看到的"北欧简约实木双人床",SKU对应"原木色-1.8m-裸床款""原木色-1.8m-床+床头柜套餐""胡桃色-1.5m-裸床款"这种具体可下单的规格。属性表存的是"颜色""尺寸""材质""是否含床头柜"这种属性名,属性选项表存的是每个属性下的具体值。SKU通过sku_attrs字段(可以存JSON,也可以拆成中间表,取决于你属性筛选的复杂程度)关联到具体的属性值组合。

家具商品还有两个普通商品不需要的字段:volume_weight(体积重)和install_fee(安装费)。体积重是物流计费的依据,大件家具走的是按体积重计费的物流渠道,不是按实际重量。安装费则要区分"免费安装""付费安装""不提供安装"三档。这两个字段如果在商品表上不单独建模,后面算运费、算订单金额的时候会非常痛苦。

3.2 库存模型:总库存与可售库存分开

家具商城的库存有个特点:同一个SKU可能在多个仓库有货,而每个仓库的现货数量是独立变动的。所以我的库存设计走的是warehouse(仓库表)+sku_stock(SKU库存表)模式,sku_stock里存total_quantity(总库存)和locked_quantity(锁定库存)。用户下单时不是直接扣减total_quantity,而是先增加locked_quantity,等支付成功后才真正扣减total_quantity并减少locked_quantity。这个"先锁后扣"的模式很关键,它能避免用户在支付期间库存被其他订单抢走,也能支撑"下单未支付保留库存15分钟"这种业务规则。

库存表里的available_quantity(可售库存)我建议用total_quantity - locked_quantity这样的计算逻辑实时算,不要冗余一个字段。因为冗余字段就要处理同步问题,Redis缓存失效、数据库更新失败,容易数据错乱。

3.3 订单状态机:状态与操作分离

订单表是最能体现系统成熟度的。我的order_info表里把订单状态设计成了这样几档:PENDING_PAYMENT(待付款)、PAID(已付款)、PENDING_DELIVERY(待发货)、DELIVERING(配送中)、COMPLETED(已完成)、CANCELLED(已取消)、REFUNDING(退款中)、REFUNDED(已退款)。

这里要提醒一个容易踩坑的点:状态字段存的是状态码,但引起状态变化的动作事件要单独记录。比如"已付款"状态可能是用户主动支付触发的,也可能是客服在后台手动标记已收款触发的,不同的触发方式对应不同的操作人、操作时间和资金来源记录。所以我会额外建一张order_status_log表,每次订单状态变更都插入一条日志。这不仅是为了后面对账查证,也是售后纠纷时保护自己的凭证。

订单状态流转里最绕的是退款流程。家具商品的退款经常不是全额退款,而是按比例退款。比如用户收到货发现有磕碰,客服协商补偿200元,这时候订单金额、实际支付金额、退款金额就出现三个不同数值了。我的建议是订单表里冗余total_amount(订单总额)、pay_amount(实付金额)、refund_amount(累计退款金额)三个字段,退款单单独建refund_order表,每次退款都走独立的退款单,不和主订单状态耦合死。如果直接把退款金额扣到主订单状态上,你后面做数据统计时会崩溃。

4. 后端核心落地:从商品检索到下单扣库存的链路实现

数据库设计完,就到了最考验功底的部分。我挑三个最核心的接口链路来讲:商品列表、购物车、下单扣库存。这三个链路把Spring Boot里的Controller、Service、事务管理、Redis缓存、并发控制全串起来了。

4.1 商品分页查询与多条件筛选

家具商城的商品筛选维度通常是:分类、材质、价格区间、尺寸范围、颜色。多条件筛选如果直接在SQL里写where拼接,条件一多久会变成重灾区。我用的方案是MyBatis-Plus的LambdaQueryWrapper动态拼接,加上ES或MySQL的全文索引二选一。对大部分个人项目来说,直接用MySQL的LIKE加组合索引就够了,没必要为了检索单独引入Elasticsearch,除非你的商品数据量到百万量级。

Controller层我用的是一个很常规的分页接口:

@RestController @RequestMapping("/api/goods") public class GoodsController { @GetMapping("/page") public Result<Page<SpuVO>> page(@RequestBody GoodsQuery query) { return Result.success(goodsService.pageQuery(query)); } }

Service层注意几个细节。-分类筛选:如果分类表有父子层级,查询时要包含子分类ID,不能只查当前分类。家具分类通常不超过三级(家具-卧室家具-床),用一个递归查询把子分类全部取出来。

  • 价格区间:前端传的是minPrice和maxPrice,后端要做参数校验,防止minPrice大于maxPrice。
  • 排序:我提供了sortField和sortOrder两个参数,字段白名单校验,不能允许前端传任意字段进去拼SQL,否则就是SQL注入点。

分页查询的缓存策略也要讲清楚。商品列表这种高频读接口,我会做Redis缓存,缓存Key设计为goods:page:{queryHash},其中queryHash是查询条件的MD5值。缓存更新策略是"创建缓存 + 定时过期",商品修改时主动删除相关缓存而不是直接更新,等下次查询再回填。这个方案比更新缓存简单,也更不容易出现并发写缓存导致的数据不一致。

4.2 购物车:用Redis还是数据库

购物车这个模块很多人会纠结用Redis还是存数据库。我的经验是:未登录购物车存Redis,登录后购物车合并进MySQL。

未登录用户的购物车数据用Redis的Hash结构存储,Key为cart:token:{sessionId},Field为SKU ID,Value为数量。这样做的原因是未登录状态下没有用户主键,Redis按Token维度存储最适合。用户登录后,前端把本地购物车数据传给后端,后端做两件事:一是把这些数据合并进MySQL的cart_item表,二是清空Redis里的临时购物车。

cart_item表的设计不复杂:id、user_id、sku_id、quantity、checked(是否选中)、create_time、update_time。唯一要注意的是user_id和sku_id要建联合唯一索引,防止同一个用户同一个SKU出现多条记录。每次加购操作都使用ON DUPLICATE KEY UPDATE这类"存在则更新,不存在则插入"的写法,避免先查再插入导致的条件竞争。

加购接口的伪代码逻辑是:

@Transactional public void addCartItem(Long userId, Long skuId, Integer quantity) { CartItem item = cartItemMapper.selectByUserIdAndSkuId(userId, skuId); if (item == null) { CartItem newItem = new CartItem(); // setUserId, setSkuId, setQuantity cartItemMapper.insert(newItem); } else { cartItemMapper.incrementQuantity(userId, skuId, quantity); } }

这里用@Transactional的意义在于,如果插入或更新过程中抛异常,整个操作回滚,购物车数据不会出现半截状态。你可能觉得一个加购操作没那么大风险,但别忘了,加购时通常还会同步校验SKU状态(是否下架)、库存数量(是否还有货),这些校验和写操作必须保证原子性。

4.3 下单链路:库存锁定与事务边界

下单是整个系统最核心的链路。我的设计顺序是:参数校验 -> 查询SKU信息 -> 锁定库存 -> 创建订单 -> 创建订单项 -> 清空购物车对应商品 -> 返回订单ID。每一步失败都要抛异常回滚。

先看下单时锁库存的SQL,这地方稍不注意就出超卖事故。

第一种写法,也是很多教程里的写法:

Stock stock = stockMapper.selectBySkuId(skuId); if (stock.getAvailable() >= quantity) { stock.setAvailable(stock.getAvailable() - quantity); stockMapper.updateById(stock); }

这种"先查再改"的写法在并发下必出问题。两个请求同时查到库存都是10,都认为可以扣减,都走完更新,最后数据库里库存变成8,但实际卖出了20件。这就是经典的超卖。

正确做法是用一条SQL完成条件更新,利用数据库行锁保证原子性:

@Update("UPDATE sku_stock SET available_quantity = available_quantity - #{quantity}, " + "locked_quantity = locked_quantity + #{quantity} " + "WHERE sku_id = #{skuId} AND available_quantity >= #{quantity}") int lockStock(@Param("skuId") Long skuId, @Param("quantity") Integer quantity);

这个SQL的关键在于AND available_quantity >= #{quantity},如果库存不够,更新影响行数为0,直接抛异常提示"库存不足"。用受影响行数判断是否成功,而不是先查库存再判断,这就是数据库层面的乐观锁思想——用条件约束代替独立校验。

高并发场景下你可能还想加Redis分布式锁,但我负责任地说,对个人项目和中小型商城系统,上述SQL方案已经足够。分布式锁是用来解决跨服务、跨实例的并发问题,单体应用里把它用上属于过度设计,还会引入Redis宕机后锁失效的新风险。

订单创建和库存锁定必须放在同一个事务里,否则会出现"库存扣了但订单没生成"或者"订单生成了但库存没扣"的数据不一致。Spring Boot里用@Transactional(在Service方法上)配合默认的REQUIRED传播级别即可,调用链中所有数据库操作都会在同一个事务里执行。需要注意的是,如果锁库存方法在另一个类里,要确保它不被同类调用绕过代理,否则@Transactional不会生效——这是Spring AOP的经典坑,自查一下自己的代码。

4.4 支付回调与订单状态翻转

接入支付(微信支付/支付宝)时,最核心的逻辑不在发起支付,而在异步通知处理。支付平台会异步通知你的回调接口,告诉你"这笔订单支付成功了"。这个回调接口有几个铁律:

  • 幂等性:同一个支付通知可能被平台推送多次,回调接口必须做到重复通知不产生副作用。
  • 验签:必须用平台公钥对通知内容做签名校验,防止伪造回调。
  • 金额校验:回调里的支付金额必须和订单表里的pay_amount一致,否则拒绝更新订单状态。

伪代码如下:

@PostMapping("/api/pay/notify") public String notify(@RequestBody String notifyData) { // 1. 验签 if (!payService.verifySign(notifyData)) { return "failure"; } // 2. 解析并查询订单 PayNotifyVO vo = payService.parseNotify(notifyData); OrderInfo order = orderService.getByOrderNo(vo.getOrderNo()); if (order == null || !order.getPayAmount().equals(vo.getPayAmount())) { return "failure"; } // 3. 如果订单未支付,则更新状态 if (OrderStatus.PENDING_PAYMENT.equals(order.getStatus())) { orderService.markPaid(order.getId(), vo.getTransactionId()); } return "success"; }

注意第3步的判断OrderStatus.PENDING_PAYMENT,这是幂等的基础——只有待付款状态的订单才能翻转为已付款,如果订单已经是已付款状态,说明是重复通知,直接返回成功即可。

5. Vue 3 前端骨架:路由守卫、状态管理与家具商品展示的实现经验

后端接口准备得差不多了,前端这块我按Vite + Vue 3 + Pinia + Vue Router + Element Plus这套组合来讲。家具商城的C端页面一般包含首页、商品列表页、商品详情页、购物车页、结算页、订单列表页,后台管理端则包含商品管理、订单管理、用户管理、数据统计。两边共用的基础设施是请求封装、路由守卫和全局状态。

5.1 工程结构与请求封装

前端项目我用Vite搭建,目录结构上有一件事一定要尽早做:把页面组件和业务逻辑分开。src/api/下按模块建文件,src/api/goods.js封装所有商品接口的调用,src/api/order.js封装订单接口。组件里不直接写fetch或axios,全部通过api模块调用。这样做的直接好处是,后端接口路径变了你只需要改一个文件,而不是在几十个组件里做文本替换。

axios封装是我每次必强调的部分。核心是拦截器:

const service = axios.create({ baseURL: '/api', timeout: 10000 }) service.interceptors.request.use(config => { const token = localStorage.getItem('token') if (token) { config.headers['Authorization'] = 'Bearer ' + token } return config }) service.interceptors.response.use( response => { const res = response.data if (res.code === 401) { router.push('/login') return Promise.reject(new Error('未登录')) } if (res.code !== 200) { ElMessage.error(res.message || '请求失败') return Promise.reject(new Error(res.message)) } return res }, error => { ElMessage.error(error.message || '网络异常') return Promise.reject(error) } )

注意401的跳转必须放在拦截器里做,而不是每个页面自己try-catch。如果你在每个请求里都写一遍"如果状态码是401就跳登录页",代码维护成本会直线上升。统一处理后,页面里只关心业务成功或失败。

5.2 路由守卫与权限控制

家具商城的前台和后台权限是分开的。前台用户和后台管理员都走同一个后端,但权限边界不同。我的设计是:/admin开头的路由需要管理员权限,其他路由需要用户登录(下单、购物车、订单页)。

Vue Router的全局前置守卫写法:

router.beforeEach((to, from, next) => { const token = localStorage.getItem('token') const roles = JSON.parse(localStorage.getItem('roles') || '[]') if (to.path.startsWith('/admin')) { if (!token) { next('/login') } else if (!roles.includes('ADMIN')) { next('/403') } else { next() } } else if (to.meta.requiresAuth) { if (!token) { next('/login?redirect=' + to.fullPath) } else { next() } } else { next() } })

有个细节容易被忽略:登录跳转时要带redirect参数。用户访问购物车页时被踢到登录页,登录成功后应该回到购物车页,而不是永远停在首页。很多教程里没写这个,我第一版也没写,被自己测试时踢了好几次才补上。

登录态保存我用的是localStorage存储token,后端每次请求通过Header里的Authorization校验。你可能会问为什么不放pinia里?因为Pinia的状态在刷新页面后会丢失,而localStorage是持久化的。通常的做法是:Pinia负责运行时的用户信息缓存,localStorage负责持久化token和用户信息。两者结合,避免刷新页面后用户信息丢失导致页面渲染异常。

5.3 家具商品详情的展示交互

家具商品详情页比普通商品复杂的地方在于:需要直观展示尺寸信息、材质信息、安装信息、不同SKU组合下的价格变化。SKU选择这块我遇到的坑是:当属性维度超过两个时,选中一个属性后被禁用的其他属性如何计算。

我用的是矩阵思路。先在前端把当前商品所有SKU组合加载出来,存成一个数组,数组里每个元素包含attrs对象和对应的价格、库存。当用户点击某个属性值时,遍历剩下的属性组合,看是否存在同时满足已选属性和当前属性的SKU,如果没有就置灰禁用。这块逻辑并不难,难的是数据量大的时候性能优化。家具商品的SKU组合通常不会超过几十个,前端全量计算完全够用。

购物车和订单提交时,前端需要传递的字段要清晰:skuId、quantity、checked。结算页还要展示商品的可选配送方式和安装服务。这些信息如果后端接口没返回,前端就只能硬编码,所以商品详情接口的返回结构设计很重要。我的/api/goods/detail/{id}返回结构大致是:

{ "spuInfo": { "id": 1, "name": "北欧实木双人床", "mainImage": "https://...", "detailImageList": [], "serviceTags": ["免费安装", "三年质保"] }, "skuList": [ { "id": 101, "specs": { "颜色": "原木色", "尺寸": "1.8m" }, "price": 3299.00, "stock": 15, "image": "https://..." } ], "freightInfo": { "freightType": "VOLUME", "volumeWeight": 0.8, "defaultFreight": 120 }, "installInfo": { "installType": "FREE", "installFee": 0 } }

这个结构保证前端拿一次接口就能渲染完整个详情页和SKU选择器,不用按用户点击再逐个请求。

6. 本地跑通到上线部署:Spring Boot Jar 包与 Vue 构建物的 Docker 部署实践

开发环境跑得再好,不部署到服务器上总感觉项目没做完。我建议这个项目从一开始就按"Docker部署"来设计,省掉后面在服务器上手动装JDK、MySQL、Redis、Nginx的繁琐过程。

6.1 后端镜像制作

Spring Boot项目执行mvn clean package后生成可执行的jar包。Dockerfile非常简单:

FROM openjdk:17-jdk-slim COPY target/furniture-mall.jar /app/furniture-mall.jar WORKDIR /app EXPOSE 8080 ENTRYPOINT ["java", "-jar", "furniture-mall.jar", "--spring.profiles.active=prod"]

这里选openjdk:17-jdk-slim是因为Spring Boot 3.x要求Java 17以上。如果你的项目用的Spring Boot 2.x,需要把JDK版本降到openjdk:8或openjdk:11,否则启动会直接报错。这是很多人在部署环节翻车的第一大原因:本地用的JDK版本和基础镜像的JDK版本不一致。

6.2 前端构建与Nginx配置

Vue 3项目执行npm run build后生成dist目录,里面是纯静态文件。Docker部署前端的思路是用Nginx镜像托管这些静态文件,同时把/api路径的请求反向代理到后端服务。

FROM nginx:1.25-alpine COPY dist /usr/share/nginx/html COPY nginx.conf /etc/nginx/conf.d/default.conf EXPOSE 80

nginx.conf里的关键配置:

server { listen 80; server_name your-domain.com; location / { root /usr/share/nginx/html; index index.html; try_files $uri $uri/ /index.html; } location /api/ { proxy_pass http://backend: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; } }

try_files $uri $uri/ /index.html;这一行非常重要。Vue Router默认是history模式,路由路径是/goods/detail/1这种,刷新页面时Nginx会去磁盘找/goods/detail/1这个文件,找不到就404。加上这行配置后,找不到文件就回退到index.html,由前端路由接管。

proxy_pass http://backend:8080;里的backend是Docker Compose里的服务名,容器之间通过服务名通信。如果你不用Docker Compose,这里就要填后端所在服务器的实际IP,不能填localhost——因为在Nginx容器里,localhost指的是Nginx容器自己,不是宿主机。

6.3 Docker Compose 编排

我用一个docker-compose.yml把MySQL、Redis、后端、前端四个服务串起来:

version: "3.8" services: mysql: image: mysql:8.0 environment: MYSQL_ROOT_PASSWORD: root123 MYSQL_DATABASE: furniture_mall ports: - "3306:3306" volumes: - mysql_data:/var/lib/mysql restart: always redis: image: redis:7.0 ports: - "6379:6379" restart: always backend: build: ./backend depends_on: - mysql - redis environment: SPRING_PROFILES_ACTIVE: prod DB_HOST: mysql DB_PORT: 3306 DB_USERNAME: root DB_PASSWORD: root123 REDIS_HOST: redis ports: - "8080:8080" restart: always frontend: build: ./frontend ports: - "80:80" depends_on: - backend restart: always volumes: mysql_data:

这个编排文件把环境变量传给容器,后端配置里通过${DB_HOST}这类占位符读取。用过Docker的朋友应该知道,容器里的环境变量比硬编码在application.yml里灵活得多——你换环境的时候不用重新打包,只要改环境变量就行。

6.4 部署后的常见问题

部署完系统后,我在测试阶段踩过几个比较有价值的坑,你可以提前规避。

  • 跨域问题:前端通过Nginx的/api代理访问后端,浏览器里看到的全是同源请求,所以不需要在后端配置CORS。但如果是本地开发环境(前端在localhost:5173,后端在localhost:8080),就需要后端配置CorsFilter允许跨域,或者在Vite的server.proxy配置代理。我的建议是开发用Vite代理,生产用Nginx代理,后端基本不碰CORS配置。
  • 数据库时区问题:连接MySQL时一定要在JDBC URL里配置serverTimezone=Asia/Shanghai,否则时间字段会差8小时。这是国内开发者几乎人人踩过的坑。
  • 前端白屏:如果Vue页面部署后打开是白屏,打开控制台看报错,多半是静态资源路径问题。Vite默认base是/,如果部署在域名子路径下,需要在vite.config.js里设置base: '/子路径/'。

7. 复盘:家具商城项目里最值得警惕的六个翻车点

最后把这套项目做完后我总结的几个经验写出来。这些话你在官方文档里看不到,但非常管用。

7.1 事务注解失效

@Transactional只在通过Spring代理调用时生效。如果一个类的方法A调用了同类的方法B,B上的@Transactional是不生效的,因为调用发生在对象内部,没有经过代理。解决方法是把事务方法拆到另一个Service类里,或者自己注入自己(不推荐)。排查方法很简单:在事务方法里故意抛个异常,看数据是否回滚,不回滚就是注解没生效。

7.2 超卖问题不止一种表现

超卖不仅出现在库存扣减,也出现在订单号生成。如果你用SimpleDateFormat格式化时间戳拼订单号,在高并发下可能生成重复订单号。我用的方案是Redis的INCR命令生成自增序号,配合日期前缀,比如20250101120000000001。Redis的INCR是原子操作,不会出现重复。

7.3 JSON 序列化循环引用

如果商品对象里关联了分类对象,分类对象里又关联了商品集合,直接用Jackson序列化就会StackOverflow。解决方式有几种:在关联字段加@JsonIgnore,或用@JsonManagedReference和@JsonBackReference配对,或改用DTO直接控制返回字段。我的建议是能用DTO尽量用DTO,把返回结构牢牢掌握在自己手里,而不是让实体类直接暴露给前端。实体类里可能存在的密码、手机号等敏感字段,一旦直接返回就成数据泄露了。

7.4 前端防重复提交

下单按钮如果用户手抖点两次,就会生成两个订单。前端有几种处理方式:按钮加loading状态置灰、提交前做节流、或者后端做防重令牌。我前后端都做了:前端点击后立即置灰,后端在创建订单前校验Redis里是否存在相同token的提交记录。最强的防重逻辑永远在后端,前端只是个体验层面的保险。

7.5 日志记录要尽早加

项目中要尽早引入AOP统一日志切面,记录每个接口的入参、出参、耗时和异常信息。不要等出了问题再去补。我用的是Spring Boot的HandlerInterceptor加@Around环绕通知,把日志输出到控制台和文件。排查线上问题时,日志就是唯一的线索。没有日志的线上系统,出了问题只能靠猜,猜的代价往往是几天起步。

7.6 分页查询的深坑

分页查询最常见的坑是前端传page=1但后端接收参数名是current,导致前端第一页永远和后端第一页对不上。解决方法是统一分页参数命名,用pageNum和pageSize,前端封装分页组件时严格按这个命名传参。另一个坑是排序字段直接拼到SQL里导致注入,要加字段白名单校验。

家具商城这个项目,我从画原型到上线,大概用了三周时间。如果只追求跑通流程,一周也够;但如果想真正理解商城系统的业务复杂度和工程化细节,我建议你把前面的数据库设计和下单链路多花点时间琢磨。尤其是库存锁定和状态机翻转,这两个点搞明白,你不仅能写好家具商城,任何带交易属性的系统都能摸到门道。后面如果你们在做类似项目时遇到具体报错,欢迎在评论区留言,我看到了会尽量回复。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/30 3:06:27

今年的第几天:日期计算与闰年判断的编程题详解

1. 题目拆解&#xff1a;到底在计算什么“KY18 今年的第几天&#xff1f;”这题我印象很深。它几乎是每套入门算法题单里的熟面孔&#xff0c;各大在线评测平台上都有类似的题号&#xff0c;描述也相当简洁&#xff1a;给你一个具体的日期&#xff0c;让程序算出它是这一年的第…

作者头像 李华
网站建设 2026/9/30 3:06:25

CSS弹性布局Flexbox核心原理与高频实战踩坑指南

做前端这么多年&#xff0c;我发现在页面上被问得最多的关键词之一就是“弹性布局”&#xff0c;你要是跟人聊CSS&#xff0c;不说一句flex怎么用&#xff0c;都不好意思说自己是搞布局的。弹性盒子&#xff08;flexbox&#xff09;真正解决的问题就一句话&#xff1a;在一维空…

作者头像 李华
网站建设 2026/9/30 3:06:09

基于PyTorch的高光谱图像分类实战:2D CNN从入门到跑通

1. 项目概述与核心思路拆解1.1 高光谱图像分类到底在做什么高光谱图像分类&#xff0c;简单说就是给遥感影像里的每一个像元打标签。普通照片只有R、G、B三个通道&#xff0c;而高光谱图像往往有几十上百个波段&#xff0c;覆盖从可见光到近红外的连续光谱范围。每个像元拿到的…

作者头像 李华
网站建设 2026/9/30 3:06:09

Mamba实战指南:从线性序列建模到视觉与点云落地

简介&#xff1a;本资源是一份面向人工智能方向研究生、算法工程师及NLP/序列建模学习者的论文汇报PPT&#xff0c;系统解读Mamba模型的核心思想与技术突破。PPT完整覆盖研究背景&#xff08;传统Transformer注意力效率瓶颈与SSM建模局限&#xff09;、解决方案&#xff08;选择…

作者头像 李华
网站建设 2026/9/30 3:05:50

计算机网络排错实战PPT:从OSI模型到Wireshark抓包

简介&#xff1a;本资源是一份系统讲解计算机网络基础知识的PPT课件&#xff0c;面向高校计算机类专业学生、IT初学者及网络入门从业者&#xff0c;旨在帮助读者建立清晰的网络知识框架&#xff0c;掌握核心概念与典型设备原理。课件内容覆盖网络概述、OSI与TCP/IP体系结构、IP…

作者头像 李华
网站建设 2026/9/30 3:05:48

Windows上跑Linux:VirtualBox安装Ubuntu虚拟机完整避坑指南

最近身边好几个朋友都来问我同一个事&#xff1a;想在 Windows 上体验一把 Linux&#xff0c;装 VirtualBox 虚拟机跑个 Ubuntu 到底行不行、麻不麻烦。答案当然是行&#xff0c;而且这是目前门槛最低、成本为零的一套组合——VirtualBox 免费开源&#xff0c;Ubuntu 社区版镜像…

作者头像 李华