如果你在机械键盘圈子里待过一阵子一定对“蹲团购”不陌生群里发个在线表格大家填外壳颜色、填配列、选PCB、选轴体、选键帽然后自己拿计算器把价格一项项加起来最后给群主转账接下来就是漫长的等待。这套流程放在十年前没什么问题但放在今天体验实在过于原始了。所以我决定用 Spring Boot 做一套完整的电商练手项目时脑子里第一个冒出来的需求就是它——客制化键盘的个性化商城网站。项目代号就叫 springboot101这里的 101 有两层意思一是 Spring Boot 从入门到实践二是像键盘配列里的基础键位一样把基本功打扎实。这套系统做出来后能在线选配置、实时算价格还能把订单、库存、支付串成完整闭环是真正能跑通的商城而不是一堆 Demo 拼起来的玩具。文章适合两类人看一类是想用 Spring Boot 做完整毕业设计或简历项目的开发者另一类是客制化键盘圈子里想自己搭个小商城卖货的爱好者。前者能学到可变 SKU、订单状态机、库存扣减这些电商核心逻辑后者能直接把这套思路搬到自己店铺里用。我尽量把设计思路、核心代码、踩坑过程都交代清楚不搞藏着掖着那套。1. 为什么选客制化键盘做 Spring Boot 实战项目而不选通用电商1.1 客制化键盘的购买流程与普通商品差异很大普通商城卖手机SKU 是事先定死的颜色、存储版本、网络制式几个维度一组合库存和价格都有固定表格。但客制化键盘不一样它的核心玩法就是“拼”。同一个套件外壳用户可以选择 65%、75%、87、98 不同配列外壳用黑还是白还是阳极银PCB 是热插拔还是焊接定位板用 PC 还是黄铜再往上叠加轴体、键帽、卫星轴、夹心棉……一套下来排列组合上千种。如果按传统电商的 SPU/SKU 建模思路去做光维护 SKU 就能让人崩溃。假设 3 种外壳颜色、2 种 PCB、3 种定位板、10 种轴体、8 种键帽组合就是 3×2×3×10×81440 个 SKU。而且客制化圈的库存逻辑也很特殊外壳颜色可能各备 50 件但轴体库存是独立的键帽又是另一批货。你没法把一个完整套件当成一个 SKU 去管理库存。这种“组件自由搭配”的模型正是普通电商平台不愿意碰、但客制化用户又特别需要的东西。我还观察到一个现象客制化键盘很多是“团购预售”模式——先付款再排队生产等几个月后发货。这和普通现货电商完全不同。所以系统不能只做现货直购还要能支持类似定金订单的简化流程。当然完整版团购系统很复杂我第一期只做了“现货直购 定制工单”两个核心链路但数据模型上已经为预售预留了字段。1.2 “101”项目名的含义与这套系统的定位“101”在美国课程体系里是入门课编号放在这里就是“从零开始做电商”的意思。项目定位是教学级但生产可用的商城不搞微服务那套花活就是一个 Spring Boot 单体应用加 MySQL 加 Redis。这样做的原因很实际单人开发、部署简单、问题排查快而且学到的知识可以完整覆盖电商主流程。整个系统要回答三个核心问题第一可变配置的产品怎么建模第二实时报价怎么算才准第三定制产品下单后订单怎么记录、库存怎么扣这三个问题只要想透后面迁移到 DIY 电脑商城、礼品定制商城、甚至装修报价系统都是同一套逻辑。所以我花在设计和建模上的时间比写代码的时间多得多。1.3 这套商城需要解决的三个典型问题第一个是动态定价。用户选不同配置价格实时变化价格计算必须以后端为准不能信前端传过来的数字。第二个是维度化库存。外壳、轴体、键帽、PCB、定位板各卖各的库存不能用一个总库存字段糊弄。第三个是方案快照。用户下单时选的是一套完整配置几个月后收到货时这套配置在数据库里可能已经下架或改版了但订单记录里必须能原样还原他当时买了什么。这三个问题解决完系统的骨架基本就立住了。我后面所有章节都会围绕这三点展开。2. 整体架构与功能模块先画一张能落地的模块图再动手写代码2.1 技术栈选型与取舍后端我用的 Spring Boot 3.x Java 17ORM 选了 MyBatis-Plus数据库是 MySQL 8缓存用 Redis支付对接第三方沙箱。前端没有做前后端分离用的是 Thymeleaf 服务端渲染 Bootstrap 原生 JavaScript。很多同学会问为什么不直接上 Vue 加 Spring Boot 前后端分离我的理由是这个项目重点在电商业务逻辑不在前端工程化。前后端分离意味着要处理跨域、Token 鉴权、接口文档维护、前端构建打包一堆事对单人项目来说负担太重。服务端渲染让页面和接口天然同源登录态用 Session 就能搞定开发效率高得多。等以后真需要做复杂的键盘配色预览交互再单独抽前端不迟。技术栈具体清单如下后端框架Spring Boot 3.x对应 Java 17持久层MyBatis-Plus避免写大量繁琐的 XML 映射数据库MySQL 8存储业务数据缓存Redis缓存配置模板、首页热销、验证码模板引擎Thymeleaf负责商品详情页、购物车、订单页渲染前端库Bootstrap 5 原生 JS负责交互支付第三方支付沙箱环境模拟支付回调2.2 功能模块清单与用户旅程系统划分成这几个模块用户模块、商品模块、配置模板模块、定制工单模块、购物车模块、订单模块、支付模块、库存模块、后台管理模块。用户旅程大概是这样的用户注册登录后进入套件商品详情页选择配列、外壳颜色、PCB、定位板再选轴体、键帽、个性刻字服务页面通过 Ajax 实时请求后端报价。确认方案后加入购物车购物车里可以同时存在普通商品比如单买一套键帽和定制方案一套完整键盘。结算时填写收货地址提交订单跳转收银台完成模拟支付支付回调成功后订单状态变成“生产中”后台管理员看到订单后点击发货用户收货确认完成。这条链路对第一次做电商项目的人来说不算短但每一步都能拆成独立模块去开发和测试。我建议你不要一上来就写代码先把这条用户路径打印出来贴在桌上每完成一个环节就打一个勾。2.3 核心流程从选配置到下单的完整链路这里有个关键决策定制方案在购物车里怎么存我设计了一个名为“定制方案单”的概念。用户在商品详情页选完配置点“加入购物车”时后端先把整套配置保存为一条方案单记录购物车里只存方案单 ID。这样购物车页面不用存一堆配置明细结算时再根据方案单 ID 拉取完整配置转成订单项快照。这样做的好处是购物车和订单的数据边界非常清晰。用户在购物车里删掉方案只是删除关联关系方案单本身还可以保留防止手滑删除后配置丢失。订单生成后方案单里的配置会被序列化成 JSON 快照写进订单项此后即使后台改了配置价格历史订单也不受影响。3. 数据库设计一张配置模板表撑起整个个性化定制3.1 普通商品与可变配置商品的建模差异数据库建模是整个项目最核心的部分我前后改了三版才定稿。第一版我试图把客制化键盘当成普通 SKU 处理结果发现行不通因为每种组合并没有固定库存。第二版我打算用“可选配 JSON”字段把所有配置塞进一个 JSON 里查询和统计时又非常别扭。最终采用了“配置模板 配置项”的建模方式。这里区分两类商品非常重要。一类是标准品比如单卖的轴体、键帽、卫星轴、数据线它们有固定规格和固定库存继续沿用传统商品表加 SKU 表。另一类是可变配置商品比如套件、成品键盘它们本身没有完整规格需要绑定一个“配置模板”模板里定义有多少个可选维度每个维度下有多少个配置项每个配置项有独立价格增量和库存。后台管理员上架一个套件商品时第一步创建商品基本信息第二步创建配置模板第三步往模板里添加配置项。例如 75 套件模板里“外壳颜色”维度下有黑色、白色、阳极银、电泳粉 4 个配置项“PCB”维度下有热插拔、焊接 2 个配置项“定位板”维度下有 PC、黄铜、碳纤维 3 个配置项如此类推。3.2 核心表结构配置模板、配置项、定制方案单下面给出三张核心表的精简 DDL实际项目里还有商品表、SKU 表、购物车表、订单表等这里重点展示和“个性化配置”相关的部分。CREATE TABLE product_config_template ( id BIGINT PRIMARY KEY AUTO_INCREMENT, product_id BIGINT NOT NULL COMMENT 关联的商品SPU, template_name VARCHAR(100) NOT NULL, status TINYINT NOT NULL DEFAULT 1 COMMENT 1启用 0停用, version INT NOT NULL DEFAULT 1 COMMENT 缓存版本号, created_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP ) COMMENT 配置模板; CREATE TABLE product_config_item ( id BIGINT PRIMARY KEY AUTO_INCREMENT, template_id BIGINT NOT NULL COMMENT 所属模板, dimension_code VARCHAR(50) NOT NULL COMMENT 维度编码如layout/case/pcb/plate, dimension_name VARCHAR(50) NOT NULL COMMENT 维度名称如配列/外壳/PCB/定位板, item_name VARCHAR(100) NOT NULL COMMENT 配置项名称如热插拔/焊接, price_delta DECIMAL(10,2) NOT NULL DEFAULT 0.00 COMMENT 价格增量可为负数, stock INT NOT NULL DEFAULT 0 COMMENT 该配置项独立库存, sort_no INT NOT NULL DEFAULT 0, deleted TINYINT NOT NULL DEFAULT 0, KEY idx_template (template_id) ) COMMENT 配置项;定制方案单表记录用户每一次完整的搭配选择CREATE TABLE custom_config_scheme ( id BIGINT PRIMARY KEY AUTO_INCREMENT, user_id BIGINT NOT NULL, product_id BIGINT NOT NULL, template_id BIGINT NOT NULL, scheme_json TEXT NOT NULL COMMENT 配置维度到配置项ID的映射JSON, total_price DECIMAL(10,2) NOT NULL COMMENT 方案总价, status TINYINT NOT NULL DEFAULT 0 COMMENT 0草稿 1已加入购物车 2已下单, created_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ) COMMENT 定制方案单;我特意加了模板版本号字段后面讲缓存优化时会用到。配置项表里 stock 字段就是这个维度下的独立库存比如“外壳颜色-电泳粉”库存 20 件每次有人下单购买这个颜色就扣这里的库存。3.3 为什么金额字段必须用 Decimal库存字段必须用整数这是老生常谈但在写商城项目时仍然看到很多人踩坑。金额一律用DECIMAL(10,2)不要在数据库和 Java 代码里用 Float 或 Double 运算。二进制浮点数存金额会有精度问题比如 0.1 加 0.2 得到 0.30000000000000004这在价格计算里是绝对不能接受的。Java 侧用BigDecimal并且所有金额计算都走同一个 Money 工具类禁止散落到处写加减。库存字段必须用整数而且要是有符号整数。为什么强调有符号因为库存扣减时我们用的 SQL 是SET stock stock - #{num}如果库存已经为 0再执行这条语句会让库存变成负数。为了避免这个问题我在 SQL 后面加条件AND stock #{num}影响行数为 0 就说明库存不足。这个技巧配合有符号整数能兼顾性能和正确性不用先查询再判断减少一次数据库往返。4. 可变 SKU 实时价格计算这个系统最核心的代码逻辑4.1 价格由哪几部分组成一个定制方案的最终价格等于商品基础价格加上所有选中配置项的价格增量之和再加上可选服务费。我举个例子某个 75 套件基础价格是 599 元。用户选择了“电泳粉外壳”30 元、“热插拔 PCB”50 元、“黄铜定位板”80 元、“某国产线性轴 90 颗”260 元、“PBT 键帽一套”180 元那么总价就是 5993050802601801199 元。这里有三个细节需要特别注意。第一价格增量可以是负数比如从“外壳黑色”换成“外壳白色”可能不加钱从“PC 定位板”换成“普通钢板定位板”可能减 40 元。后台设计配置项时价格增量可正可负这样能灵活表达商家的定价策略。第二轴体和键帽虽然也是配置项但它们本质上是标准品 SKU所以配置模板里允许配置项关联一个可选的 SKU ID。选中这个配置项时不仅要累加价格还要在生成订单时把对应 SKU 的库存一起锁定。第三刻字服务这类“个性化增值服务”单独立表按字符数计费代码里单独处理。4.2 前端联动与后端接口设计用户在商品详情页操作时前端 JavaScript 会维护一个“当前选中配置”的 Map键是维度编码值是配置项 ID。每次用户点击选择项就触发一次 Ajax 请求把整个 Map 传给后端报价接口后端返回最新总价、每个配置项的可选状态和库存情况。前端用返回结果更新报价区域同时把库存为 0 的配置项置灰。这个接口的设计要求是幂等、只读、快速。它不应该修改任何数据只负责计算和校验。接口路径我定义为GET /api/quote请求参数是productId加itemIds配置项 ID 列表响应结构如下{ code: 0, data: { totalPrice: 1199.00, basePrice: 599.00, items: [ { id: 1001, dimensionCode: caseColor, itemName: 电泳粉外壳, priceDelta: 30.00, stock: 18, available: true } ] } }前端拿到这个响应直接渲染即可。注意不要在前端自行拼价格前端只展示后端计算好的结果防止用户修改页面脚本篡改报价。4.3 完整的价格计算 Service 代码与说明报价服务是项目里最值得反复阅读的一段代码。下面我给出简化后的核心逻辑Service RequiredArgsConstructor public class QuoteService { private final ProductConfigTemplateMapper templateMapper; private final ProductConfigItemMapper configItemMapper; private final ProductMapper productMapper; Transactional(readOnly true) public QuoteResult quote(Long productId, ListLong itemIds) { Product product productMapper.selectById(productId); ProductConfigTemplate template templateMapper.selectByProductId(productId); Assert.notNull(template, 商品未配置可选项); ListProductConfigItem selectedItems configItemMapper.selectBatchIds(itemIds); // 校验选中的配置项都属于该商品模板 boolean allMatch selectedItems.stream() .allMatch(item - item.getTemplateId().equals(template.getId())); if (!allMatch) { throw new BusinessException(存在无效的配置项); } // 按维度去重同一维度只能选一个配置项 SetString dimensions selectedItems.stream() .map(ProductConfigItem::getDimensionCode) .collect(Collectors.toSet()); if (dimensions.size() ! selectedItems.size()) { throw new BusinessException(同一维度不能选择多个选项); } BigDecimal total product.getBasePrice(); for (ProductConfigItem item : selectedItems) { total total.add(item.getPriceDelta()); } // 附加刻字服务费用 if (selections.containsKey(engraving)) { total total.add(engravingService.calculatePrice(selections.get(engraving))); } QuoteResult result new QuoteResult(); result.setTotalPrice(total.setScale(2, RoundingMode.HALF_UP)); result.setBasePrice(product.getBasePrice()); result.setItems(selectedItems); return result; } }这段代码逻辑不复杂但有两个隐藏点值得细讲。第一个是“按维度去重”的校验存货代码里很容易漏掉。如果用户提交了两个同维度配置项比如同时选了“热插拔 PCB”和“焊接 PCB”后端必须拦截否则价格会重复累加订单数据也会脏。第二个是事务注解只用了readOnly true因为报价查询不会修改数据这样能减轻数据库负载也让调用方明确这是只读操作防止有人在里面偷偷写数据。有了报价接口接下来要处理的就是把选好的方案保存成方案单然后进入购物车。方案单保存时会把 totalPrice 也存进去但注意这个价格只是“参考价”最终订单金额在生成订单时还会重新计算一遍避免用户拿着旧报价单来下单。5. 下单、支付与库存扣减状态机设计比接口更重要5.1 订单状态机订单模块最忌讳的就是用一堆 if else 去判断状态能不能流转。我画了一张状态流转表然后严格按照这张表开发状态编码状态名称可流转到0待支付1 已支付生产中、6 已取消1已支付生产中2 待发货2待发货3 已发货3已发货4 已完成4已完成无5退款中4 已完成、6 已取消6已取消无每个状态节点只允许跳转到表格里的目标状态任何非法跳转直接抛业务异常。在 Java 里我封装了一个OrderStatusMachine组件维护一个EnumMap判断当前状态到目标状态是否合法。这样做的好处是后续再加新状态时只用改这一张表和一个枚举不用到处找散落的 if else。支付回调是这个模块里最需要小心的入口。第三方支付平台回调时携带订单号和支付结果我们必须保证回调处理逻辑的幂等性。我的做法是在支付回调处理业务前先查询支付流水表如果该流水号已经处理过就直接返回成功不重复执行订单状态流转和库存扣减。5.2 库存扣减的两种时序下单锁定还是支付锁定库存扣减的时机有两种选择。第一种是“下单锁定库存”用户在创建订单时立刻扣减库存取消订单时归还。这种方案用户体验好但有个致命问题大量用户下单不支付库存会被无效占用后面真正想买的人反而买不到。第二种是“支付成功后再扣库存”用户在订单创建时不扣库存只有支付成功才真正扣减。这样库存占用和真实销售是同步的但可能出现支付成功后扣库存失败的尴尬情况。我选择了第二种并且做了补偿机制。支付回调里执行“扣减库存”和“更新订单状态”两个操作放在同一个本地事务里。库存不足时抛异常让事务回滚同时把支付回调标记为“待处理”后续人工介入处理退款。客制化键盘本身有生产周期用户支付后本来就有一段等待期所以“支付后再扣库存”在业务上完全说得通。库存扣减的 SQL 也很关键直接用乐观锁方式int rows inventoryMapper.deductStock(item.getSkuId(), quantity); if (rows 0) { throw new BusinessException(库存不足 item.getItemName()); }对应 SQL 是UPDATE sku_inventory SET stock stock - #{quantity} WHERE sku_id #{skuId} AND stock #{quantity}rows 0意味着库存不足整个订单事务回滚。这里不用悲观锁是因为客制化商城并发量没那么高乐观锁足够而且不会长时间持有行锁影响其他操作。5.3 定制工单快照如何进入订单用户下单时购物车里有普通 SKU 行也有定制方案行。生成订单时普通 SKU 行直接创建订单项关联 SKU ID 和购买数量定制方案行则要把方案单里的 JSON 配置序列化成带结构的信息存到订单项的spec_snapshot字段里。这个快照非常重要。订单项里如果只存一个方案单 ID等后台把某个配置项下架或改价后用户查看历史订单看到的价格和配置就对不上了。我在生成订单项时把配置维度名称、配置项名称、单价增量、小计金额全部写进 JSON 快照确保订单页能原样还原用户当时的选择。这是一个“空间换时间”的方案虽然多占一点存储空间但换来了历史订单的绝对稳定。我这里特意设置了订单金额的两段式校验生成订单时后端根据快照重新累加一遍价格然后和订单总价比对不一致就截断生成并提示“商品价格已变动请重新确认”。这个设计能防止用户拿旧报价下单占便宜。6. 前端页面与后端接口的对接本地预览如何变成真实渲染6.1 套件组装预览的前端交互设计客制化商城的前端交互重点是商品详情页。我希望用户打开页面就能直观看到当前配置左侧是键盘配列的示意图右侧是配置选择区。由于第一期没有引入复杂的 Canvas 渲染我用的是“配置摘要卡片 价格明细列表”的方案不追求像素级还原但要求信息完整清晰。交互逻辑是这样的页面初始化时加载默认配置同时向后端发一次报价请求渲染出基础价格。用户每点击一个配置项页面先高亮选中项然后调用报价接口更新总价。如果某个配置项库存为 0前端直接渲染成灰色禁用态并且不发起请求。对大量可选配件采用折叠分组方式展示避免页面一次性撑得太长。这里用到的是一段原生 JavaScript核心是防抖函数let timer null; function refreshQuote(selectedItems) { clearTimeout(timer); timer setTimeout(() { fetch(/api/quote?productId productId itemIds selectedItems.join(,)) .then(res res.json()) .then(data { if (data.code 0) { document.getElementById(totalPrice).innerText ¥ data.data.totalPrice; renderStockStatus(data.data.items); } }); }, 300); }防抖时间设为 300 毫秒用户快速连续点击不同配置项时只在最后一次点击后发起请求避免瞬间发起多个请求打爆后端。6.2 配置项联动什么是必须在前端做的什么是必须留给后端的前端能做的是给用户即时反馈比如选中高亮、库存置灰、价格展示。但所有“真实计算”都必须交给后端。我在开发时给自己定了一条铁律前端任何状态都不能影响最终交易数据。用户从商品详情页把方案加入购物车时前端传过去的只是方案单里的配置项 ID 列表后端会重新校验每个配置项是否存在、是否属于该商品模板、库存是否足够然后重新计算价格。前端传什么都不可信。很多新手在这里容易翻车图省事把计算好的总价直接传给后端保存导致用户通过修改请求参数把 1199 元的键盘改成 1 块钱下单。我见过不止一个项目出这种事故。所以方案单保存接口、订单生成接口里的金额全部以后端计算为准前端传的金额直接忽略。6.3 购物车行、结算页与订单页的渲染衔接购物车页面做起来比想象中麻烦因为要同时渲染两类行。普通 SKU 行显示商品图片、名称、单价、数量定制方案行显示“75 套件定制方案”点开能看到配置摘要比如“电泳粉外壳 热插拔 PCB 黄铜定位板 某线性轴 90 颗 PBT 键帽”。数量默认是 1因为一套定制键盘就是一台。结算页除了展示商品明细和总价还要回收货地址。我这里简单处理为首次填写后保存到用户地址簿下次自动带出最近地址。订单页则根据订单项里的spec_snapshot字段渲染完整配置清单保证用户看到的永远是他下单时的那套配置。7. 上线与调优一次报价请求从 120ms 降到 20ms 的实践7.1 用 Redis 缓存配置模板的注意事项报价接口是详情页被调用最频繁的接口每次都查数据库的话压力全在 MySQL 上。我的做法是配置模板和配置项在后台变更时写入 Redis 缓存一份 JSON 结构。缓存 key 用config:template:{id}:v{version}版本号从数据库模板表里取。后台管理员修改配置后版本号加 1新请求自然落到新缓存上。缓存的加载逻辑是经典的 Cache Aside 模式先查 Redis命中直接返回未命中则查数据库、写入 Redis、设置过期时间可以设 30 分钟。这样配置模板不会长期不更新也不会因为频繁更新把 Redis 拖垮。这里要注意一个细节写缓存和更新版本号必须放在后台修改配置的同一个事务里否则可能出现数据库已经更新、缓存还是旧数据的窗口期。加了缓存以后报价接口的响应时间从平均 120ms 降到了 20ms 左右数据库 QPS 也下来了。后面做简单压测时单机并发 300 线程持续调用报价接口P95 响应时间依然在 50ms 以内完全够用。7.2 防超卖的两道闸门商城项目最怕的是超卖明明库存只有 3 件结果卖出去了 5 单。我这里设置了两道闸门。第一道在支付回调阶段执行库存扣减 SQL 时用stock #{quantity}条件做乐观锁校验扣减影响行数为 0 就回滚事务这条是最硬的一道防线。第二道在创建订单前把方案单里涉及的配置项库存都查出来只要有一个配置库存为 0就提示用户调整配置。第二道属于提前拦截为了减少用户走到支付才发现买不了的挫败感。两道闸门不能互相替代。如果只做第一道用户下单时体验差如果只做第二道高并发下多个请求同时查到库存充足但支付扣减时库存已经被别人扣光就会出现支付成功但库存不足的异常。双闸门配合才稳妥。7.3 部署形态与 JVM 参数的一些要点部署我用的方式是 Spring Boot 打成可执行 jar 包放在一台 2C4G 的轻量服务器上前面用 Nginx 做反向代理。Nginx 主要负责静态资源缓存、HTTP 转发和简单的限流配置。jar 包启动时的 JVM 参数如下java -Xms512m -Xmx1024m \ -XX:UseG1GC \ -XX:MaxGCPauseMillis200 \ -Dfile.encodingUTF-8 \ -jar springboot101-shop.jar-Xms512m -Xmx1024m设置初始堆和最大堆避免频繁扩容。G1 垃圾回收器适合这种内存不太大但并发存在的情况。数据库连接池我用的 HikariCP最大连接数设为 20最小空闲连接数设为 5完全够用。还有一个容易忽略的点是 MySQL 连接字符串要加useSSLfalseserverTimezoneAsia/ShanghaicharacterEncodingutf8否则运行时会遇到时区错误或者 SSL 握手过慢的问题。我第一次部署时忘记配时区结果所有时间字段都差了 8 个小时排查了半天。8. 开发中踩过的坑这些问题不写出来真的对不起读者8.1 MySQL 字段名用了保留字上线前一天接口直接 500我第一版商品表里有个字段叫desc想着是描述的意思结果 MyBatis-Plus 生成 SQL 时把它拼进查询语句直接报 SQL 语法错误。后来才知道desc是 MySQL 的保留字用于降序排序。排查过程也很简单控制台打印出的 SQL 一看就知道问题但当时确实费了点时间。教训就是建表前先查一下字段名是否在保留字列表里像order、group、desc、rank这些都不要用。还有一个相关问题是 MyBatis-Plus 的下划线转驼峰。数据库字段item_name映射到 Java 属性itemName本来一切正常。但有次我写了一个联表查询返回字段里既有name又有item_name结果两个字段同时映射到同名属性上数据互相覆盖。后来我所有查询都用明确的 DTO 接收不用 Map 接收结果集避免这种弯弯绕绕的问题。8.2 前端传来的配置项 ID 不能直接信我被“一折键盘”案例吓出一身汗开发过程中我自己测试过一次极端场景用浏览器开发者工具修改请求参数把方案单里的配置项全部清空只剩一个基础商品总价直接从 1000 多变成 599。如果后端直接信任这个总价用户就能用基础价买到全套配置。还好我在设计时就定了铁律后端永远重新计算金额。实际开发中我也见过有人把价格计算写在前端后端只做保存这种项目上线等于送钱。建议所有做电商的开发者把“后端重新计算金额”这条规则写进自己的编码规范里每次下单接口都做一次金额核对。8.3 事务里调用第三方支付接口导致连接耗尽早期版本我在创建订单的事务里直接发起第三方支付请求想着一步到位。结果有次支付的沙箱环境网络缓慢每个请求要等 30 秒才超时而事务一直拿着数据库连接不放。压测时很快出现连接池耗尽整个应用陷入假死状态。后来我改成“订单创建事务只做本地数据落库”支付请求放到事务提交之后异步发起。支付回调进来时开启一个新事务处理状态更新和库存扣减。这样数据库事务的持有时间从原来的几十秒缩短到几十毫秒再也没出现过连接耗尽的问题。8.4 金额用浮点数导致的对账差异项目初期我图省事价格字段在 Java 里用了Double结果跑了一段时间后对账发现偶尔差几分钱。比如 0.1 加 0.2 用二进制浮点算出来是 0.30000000000000004四舍五入后虽然接近但累计到几十条订单后误差就放大了。后来我把所有金额字段全部改成BigDecimal并且在数据库里统一用DECIMAL(10,2)对账问题彻底消失。这个教训很基础但真的很重要商城项目千万别在金额上省事。再补充一个缓存更新顺序的坑。最早我更新配置后直接删除 Redis key但删除操作在数据库事务提交之前执行导致旧数据被重新加载。后来我把“缓存版本号1”放进数据库事务接口从缓存读取时始终携带最新版本号才真正解决了缓存和数据一致性的问题。项目做到这里回头看看最值钱的其实不是代码量而是把“可变配置商品”的建模想透了。客制化键盘这个品类天然逼着你去面对动态 SKU、维度库存、方案快照这些问题——这些恰恰是通用电商教程里很少讲透的。如果你也想拿 Spring Boot 做项目我建议别再做图书管理系统或者简单的 CRUD 商城了试试这个方向做完以后你对电商核心链路的理解会上一个台阶。 SEO 优化官网定制响应式建站教育培训建站