ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

订单库存系统表结构设计实战:从业务梳理到避坑指南

订单库存系统表结构设计实战:从业务梳理到避坑指南 最近整理一个编号为 150 的实战案例案例主题是订单库存类系统的表结构和业务说明。这类系统看起来非常常见但真正动手设计时很多人会卡在同一个问题上先建表还是先理业务流程。我的建议是先理清业务再写第一行建表语句。下面不贴空泛概念我会把这套案例里涉及的表结构、业务含义、字段设计要点和上线后容易踩的坑按实际落地顺序拆开讲。如果你正在做电商后台、进销存系统、工单系统或者想把手动记录的业务搬进数据库这套表结构可以直接作为参考骨架。同一个业务在不同团队里可能被设计成完全不一样的表这不是 SQL 写法问题而是对业务边界的理解不同。所以这一篇不只是贴表结构还会把每张表为什么这么设计、业务上怎么用、出了问题怎么排查一起讲清楚。1. 先弄清楚案例里的业务边界我为什么会设计这一套表1.1 这个实战案例解决什么问题这套案例我按小型电商和门店销售结合的场景来设计整体上要支撑四个核心词商品、库存、订单、售后。系统规模没有到高并发的量级但日常几百人同时操作、一天几千单是必须稳定扛住的。所以表的数量不追求多核心设计原则是“状态可追踪、数字可复核、出问题能定位”。如果只建一张订单表把商品名称、数量、价格全部塞进去短时间看很方便。但后面做统计、做退款、做库存对账都会很难受。表结构首先得为未来一个月、三个月可能出现的问题留出余地。这套案例最终要满足这些要求商品信息可维护上下架状态明确。每个商品的库存变动都有流水不靠拍脑袋。订单从创建到完成每一步状态变化都能查。商品价格变化不影响历史订单。报表能直接聚合出销售、库存、退款数据。其实很多项目失败不是败在功能没实现而是败在表结构改不动。业务加一个退款类型代码要改表也要改改着改着就没人敢动了。所以这个案例里凡是和状态、金额、库存相关的字段我都会故意设计得保守一点宁可在前期多拆一张表也不在后期打补丁。1.2 建模最忌一上来就写字段先画业务流程拿到需求后我的习惯是先不打开数据库客户端先在纸上画实体关系。这个案例里的核心实体包括用户、商品、库存、订单、订单明细、库存流水、售后单。它们之间的关系大概是这样的用户和订单是 1 对多。订单和订单明细是 1 对多。商品和库存是 1 对 1在多规格场景下是 SKU 和库存 1 对 1。订单在流转过程中会产生库存流水。售后单挂在订单下面也可能多个售后单对应一笔订单。画完实体关系再把业务主流程走一遍商品录入、库存入库、用户下单、扣减库存、支付、发货、取消、售后。每一步都要问自己一个问题这一步需要读哪些数据写完哪些数据如果出错了要留什么线索。这一步看起来很简单但非常值钱。很多人一上来就建表然后建到一半发现订单表不知道该不该存收货地址库存不知道该不该存商品名称。这些问题的答案在业务流程图里其实已经出现了。订单表要不要存收货地址取决于会不会改地址、会不会按地址统计库存表要不要存商品名称取决于查询时能不能轻松关联商品表。而这些判断都需要先看业务流。等业务流清晰了再定义状态枚举。订单状态我先列出来待支付、已支付、已发货、已完成、已取消、已退款。库存变动类型也会预先定义入库、锁定、扣减、解锁、回滚。这一步的价值是避免后面业务加一个状态就改一次表结构。2. 核心表结构拆解从用户、商品到订单和库存流水2.1 基础资料表用户表与商品表先看用户表。用户表看起来简单但设计时容易在唯一索引和软删除之间踩坑。CREATE TABLE sys_user ( id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT 用户ID, username VARCHAR(64) NOT NULL COMMENT 登录名, password_hash VARCHAR(128) NOT NULL COMMENT 密码哈希, phone VARCHAR(32) DEFAULT NULL COMMENT 手机号, status TINYINT NOT NULL DEFAULT 1 COMMENT 状态1启用0禁用, deleted TINYINT NOT NULL DEFAULT 0 COMMENT 软删除0未删1已删, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_username (username) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT用户表;密码字段必须存哈希不能存明文。登录名一般会加唯一索引但一旦引入软删除麻烦就来了一个用户被软删除后再注册同名用户会报唯一键冲突。这个问题后文单独说这里先记住软删除不是免费的。商品表按单规格商品设计库存直接对商品维度CREATE TABLE product ( id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT 商品ID, product_code VARCHAR(64) NOT NULL COMMENT 商品编码/条码, product_name VARCHAR(200) NOT NULL COMMENT 商品名称, category_id BIGINT DEFAULT NULL COMMENT 分类ID, price DECIMAL(10,2) NOT NULL COMMENT 销售价, unit VARCHAR(20) DEFAULT 件 COMMENT 单位, status TINYINT NOT NULL DEFAULT 1 COMMENT 上架状态1上架0下架, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_product_code (product_code) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT商品表;价格字段我强烈建议用 DECIMAL不要用 FLOAT 或 DOUBLE。二进制浮点数做金额会有精度误差短期内可能看不出来一旦做对账就会发现各种差几分钱的问题。商品表还需要注意状态字段。很多项目喜欢通过删除记录来做下架其实更好的做法是用 status。下架只是改变商品状态不应该把历史订单关联的商品信息删掉。2.2 库存与流水表一张独立流水为什么比定时对账更省事库存表的设计关键是拆出可用库存和锁定库存两个概念。CREATE TABLE inventory ( id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, product_id BIGINT NOT NULL COMMENT 商品ID, quantity INT NOT NULL DEFAULT 0 COMMENT 可用库存, locked_quantity INT NOT NULL DEFAULT 0 COMMENT 锁定库存, version INT NOT NULL DEFAULT 0 COMMENT 乐观锁版本, updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_product_id (product_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT库存表;如果只有一个总库存字段用户下单后马上扣减到支付失败或取消时再回滚很容易出现回滚不干净的问题。把库存拆成可用库存和锁定库存之后下单时可用减少锁定增加支付完成时锁定减少取消时锁定减少可用增加。每一个阶段做什么很清楚数字也更容易对上。库存流水表则是整个库存设计里的关键很多项目会省略但我建议一定保留CREATE TABLE inventory_log ( id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, product_id BIGINT NOT NULL, change_type VARCHAR(32) NOT NULL COMMENT 入库/锁定/扣减/解锁/回滚, change_quantity INT NOT NULL COMMENT 变动数量正负数, before_quantity INT NOT NULL, after_quantity INT NOT NULL, order_no VARCHAR(64) DEFAULT NULL COMMENT 关联订单号, remark VARCHAR(500) DEFAULT NULL, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_product_time (product_id, created_at), KEY idx_order_no (order_no) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT库存流水表;这张表只增不改每次库存变动都记录变化前后的数量。这样即使线上库存数据出现问题也能靠流水还原。没有流水表的话库存对不上就只能导 Excel 手动核费时费力而且容易漏。流水表里的 order_no 字段建议保留。它的意义在于把库存变动和订单关联起来。排查“这个订单为什么扣了两次库存”时一条 SQL 就能把相关流水全部拉出来。2.3 订单主表与明细表头行结构到底怎么拆订单表采用主表和明细表分开的头行结构。主表存订单级信息明细表存商品级信息。CREATE TABLE orders ( id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, order_no VARCHAR(64) NOT NULL COMMENT 订单号, user_id BIGINT NOT NULL COMMENT 用户ID, total_amount DECIMAL(10,2) NOT NULL COMMENT 订单总金额, pay_amount DECIMAL(10,2) NOT NULL COMMENT 实际支付金额, status TINYINT NOT NULL COMMENT 状态1待支付2已支付3已发货4已完成5已取消6已退款, receiver_name VARCHAR(64) DEFAULT NULL, receiver_phone VARCHAR(32) DEFAULT NULL, receiver_address VARCHAR(500) DEFAULT NULL, pay_time DATETIME DEFAULT NULL, ship_time DATETIME DEFAULT NULL, finish_time DATETIME DEFAULT NULL, cancel_time DATETIME DEFAULT NULL, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_order_no (order_no), KEY idx_user_time (user_id, created_at) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT订单表;订单明细表CREATE TABLE order_item ( id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, order_id BIGINT NOT NULL, product_id BIGINT NOT NULL, product_code VARCHAR(64) DEFAULT NULL, product_name VARCHAR(200) NOT NULL, price DECIMAL(10,2) NOT NULL COMMENT 成交单价, quantity INT NOT NULL, subtotal DECIMAL(10,2) NOT NULL COMMENT 小计金额, PRIMARY KEY (id), KEY idx_order_id (order_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT订单明细表;头行拆分的原因很直接一张订单有多个商品主表放用户、状态、金额、收货信息明细表放商品快照。如果只建一张订单表每次查询订单详情都要拼一串商品信息维护起来非常痛苦。明细表里冗余了 product_name 和 price这不是偷懒而是必须。商品表的价格和名称随时可能调整历史订单在打印、售后、对账时必须还原当时的信息。如果直接用商品表当前数据历史订单的价格就可能被新价格覆盖对账就全乱了。订单表里的 status 字段不建议用字符串建议用 TINYINT 存数字然后在应用层维护枚举。数字的好处是节省空间、查询效率高缺点是可读性差。所以我会在字段注释里写清楚每个数字代表什么状态。上线时间长了以后发现代码里状态枚举比表结构注释更完整的人往往会付出补文档的代价。因此建表时把注释写详细是给未来的自己省事。3. 业务流程和表结构是如何对着走的一次下单经历哪些表3.1 下单阶段订单状态、库存预占和唯一约束下单不是一个 insert 订单表的动作而是一个包含多个步骤的事务。典型流程如下生成订单号插入 orders状态置为待支付。循环订单商品项往 order_item 插入明细累加总金额。更新 orders 的总金额。对每个商品执行库存扣减可用库存减少锁定库存增加。写入 inventory_log。提交事务。用一个伪 SQL 把这个事务串起来BEGIN; INSERT INTO orders(order_no, user_id, total_amount, pay_amount, status) VALUES(ORD20250101xxxx, 1001, 299.00, 0.00, 1); INSERT INTO order_item(order_id, product_id, product_code, product_name, price, quantity, subtotal) VALUES(1, 5001, P001, 测试商品A, 299.00, 1, 299.00); UPDATE inventory SET quantity quantity - 1, locked_quantity locked_quantity 1 WHERE product_id 5001 AND quantity 1; INSERT INTO inventory_log(product_id, change_type, change_quantity, before_quantity, after_quantity, order_no) VALUES(5001, 锁定, -1, 100, 99, ORD20250101xxxx); COMMIT;这里最需要注意的是库存更新语句。quantity 1这个条件能够保证在并发情况下不会超卖。如果影响行数为 0说明库存不足需要抛出异常并回滚整个事务。不能先查一遍库存数量再在代码里判断是否够用因为两个请求同时查到“有货”时就会发生超卖。下单阶段还有一个容易忽略的问题订单号必须唯一。支付平台、物流平台、内部对账都依赖订单号订单表里一定要对 order_no 建唯一索引。如果生成订单号的规则有缺陷出现重复后面的流程全部会乱。3.2 支付、发货和取消状态流转的每次变化都留痕下单之后订单状态会经历多次变化。支付成功时需要更新订单状态并记录支付时间。这个更新不能无脑写要带上前置状态条件UPDATE orders SET status 2, pay_time NOW() WHERE order_no ORD20250101xxxx AND status 1;这样做的目的是防止重复回调。支付平台的回调有时候会重复推送如果没带前置状态条件第一次回调把状态改成已支付第二次回调又处理一次就可能出现重复发券、重复发货的问题。支付成功之后还要处理库存逻辑把之前下单时锁定的库存扣减掉。也就是UPDATE inventory SET locked_quantity locked_quantity - 1 WHERE product_id 5001 AND locked_quantity 1;这一步和下单时的锁定动作是成对的。所以流水表里的 change_type 要区分“锁定”和“扣减”。如果只记录一个总数字后面核对时会分不清到底哪个环节出了问题。发货时订单状态从已支付变成已发货记录发货时间。用户确认收货后变成已完成。如果用户申请取消要看订单处于哪个状态待支付状态取消订单变成已取消同时释放锁定库存。已支付状态取消涉及退款需要走售后流程不能直接改订单状态。每操作一步最好都在日志或者操作记录表里留下痕迹。如果不想额外建操作日志表至少要保证订单表里的状态变更字段能够体现时间线。pay_time、ship_time、finish_time、cancel_time 这几个字段本质上就是内嵌的状态日志。3.3 退款与售后不要把业务数字写死要能追溯简单项目里退款就是把订单状态从已支付改成已退款。但一旦出现部分退款、多次退款、退款失败重试只靠订单表一个 status 字段就不够用了。所以我建议在订单之外单独增加一张售后表CREATE TABLE after_sale ( id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, after_sale_no VARCHAR(64) NOT NULL, order_id BIGINT NOT NULL, user_id BIGINT NOT NULL, type TINYINT NOT NULL COMMENT 类型1仅退款2退货退款, status TINYINT NOT NULL COMMENT 状态机, refund_amount DECIMAL(10,2) NOT NULL, reason VARCHAR(500) DEFAULT NULL, refund_no VARCHAR(128) DEFAULT NULL COMMENT 支付渠道退款单号, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_after_sale_no (after_sale_no), KEY idx_order_id (order_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT售后表;售后表里要记录退款金额和支付渠道返回的退款单号。退款单号如果能做唯一索引尽量做。因为支付渠道的回调可能重复如果第一次退款回调已经处理第二次又来处理一笔就会造成重复退款。用唯一索引在数据库层拦截是最后一道保险。售后单的加入也让订单主表变得干净。订单表只需要记录当前最新状态不需要把退款历史全部塞进去。否则订单状态字段会被各种组合状态逼疯比如“已支付部分退款”“已发货退款中”“退款失败重新申请”等等全部堆在 status 里查询和统计都会很难受。4. 索引、并发和参数设计避免超卖和查询缓慢4.1 哪些字段必须建索引哪些建了反而浪费索引不是越多越好而是要根据查询语句来设计。这个案例里的订单表最核心的查询场景有三个通过订单号查订单详情、支付回调、对账所以 order_no 要建唯一索引。用户中心查历史订单一般是按用户和时间倒序所以建 (user_id, created_at) 联合索引。后台按状态和日期过滤订单可以建 (status, created_at) 联合索引但要考虑状态字段区分度。订单明细表只需要重点关注 order_id 索引因为明细表都是通过 order_id 去查。商品表需要关注 product_code 唯一索引因为扫码、条码、导入都靠它定位。分类字段如果经常做分组统计可以建普通索引如果只是详情展示没必要建。库存流水表这种频繁写入的表索引必须克制。按商品和时间维度查询时建 (product_id, created_at) 联合索引足够。频繁插入对索引维护成本很高字段一多、索引一多写入性能会明显下降。判断一个索引有没有用不要靠猜。SQL 执行前用 EXPLAIN 查看执行计划重点看 type、key、rows 这几个字段。如果明明是等值查询却出现全表扫描说明索引没建对或没走上。4.2 防止库存超卖的几种手段锁、版本号、扣减方向库存扣减是并发问题最容易爆发的地方。最危险的做法是先 SELECT quantity然后代码判断是否大于 0再 UPDATE。这种“先查后改”的模式在高并发下几乎必然有问题。正确的做法是直接使用条件更新UPDATE inventory SET quantity quantity - #{count}, locked_quantity locked_quantity #{count}, version version 1 WHERE product_id #{productId} AND quantity #{count};如果影响行数为 1说明库存充足且扣减成功如果影响行数为 0说明库存不足或商品不存在。这个 SQL 本身已经包含了数量判断不需要额外加锁也不容易出现超卖。有些团队会额外使用乐观锁版本号让 UPDATE 语句带上version #{oldVersion}。这种写法也可以但要注意version 的作用是防止更新被覆盖不是直接解决库存不足的问题。真正防止超卖的核心条件仍然是quantity #{count}。如果业务跨多个服务比如库存服务独立部署或者存在多个应用实例同时写库存这时候才需要考虑分布式锁。但这套案例里的单体业务事务加条件更新已经足够。不要一上来就引入 Redis 锁、消息队列复杂度会随着组件数量翻倍增长。4.3 表结构维护时的参数注意点字符集、时间字段和软删除建表时把字符集统一成 utf8mb4。这样中文、生僻字、emoji 都能正常存储。如果用了 utf8后面碰到 emoji 或者特殊符号就会报 “Incorrect string value” 错误。时间字段推荐使用 DATETIME。TIMESTAMP 的范围到 2038 年而且受时区影响。DATETIME 虽然不自动处理时区但胜在范围大、稳定。应用层统一传入时间不要让数据库层做太多时区转换。软删除字段需要特别注意。这个案例里的用户表用了 deleted 字段但是 username 上有唯一索引软删除后再次注册相同用户名会冲突。实际项目中常见解决方式有三种用户不物理删除而是禁用账号保留登录名。删除时对 username 加随机后缀比如old_name_deleted_20250101。用 deleted_at 代替 deleted未删除为 NULL已删除为时间戳配合唯一索引可以避免一部分冲突但查询要处理 NULL。最稳妥的做法是尽量不在核心业务表上做软删除。状态用 status 控制日志类数据天然只增不改。这样既避免唯一索引冲突也不用在每一条 SQL 后面都加AND deleted 0。5. 表结构同步、迁移和常见排查DataGrip 这类工具怎么用5.1 同步表结构前先做什么检查很多人在用 DataGrip 同步数据库表结构时连接上开发库和生产库点一个同步就直接执行脚本。最怕的就是连错环境把一个测试用的临时字段同步到了生产库。正确顺序是这样的先确认连接信息登录名、地址、端口、库名逐一核对。使用工具的结构比较功能在 DataGrip 里可以右键 schema选择 Compare 或 Diff。查看差异列表明确哪些是新增表、新增字段、字段类型变更、索引新增或删除。重点标记有破坏性的变更比如字段类型从 VARCHAR(50) 改成 VARCHAR(20)、删除字段、删除索引。生成变更脚本后先在测试库执行一遍确认不会报错。生产库执行前备份目标表结构以及数据。工具生成差异脚本很方便但它只负责对比结构不负责判断业务影响。一个字段从 DATETIME 改成 VARCHAR工具可能告诉你只是类型变更但应用到代码里所有时间排序逻辑都可能失效。所以同步前一定要看应用代码。5.2 常见的同步差异字段类型、默认值、索引名数据库结构同步最常见的差异类型可以参考这个表格差异类型表现处理建议引擎不同MyISAM 与 InnoDB 表混用事务不可用统一使用 InnoDB字符集不同中文乱码索引长度超限统一 utf8mb4字段类型不一致插入报错查询结果异常以应用写入规则为准默认值不同未传字段时线上值不对对比默认值补全索引名不一致DDL 重复执行报错按统一命名规则字段删除但代码还在引用启动报错接口报字段不存在先改代码再删字段同步完成后不要只看表结构一致一定要跑一遍核心流程。这个案例里至少要跑用户登录、商品查询、下单、库存扣减、支付回调、列表查询。任何一个环节报错都要回滚结构变更别带着问题上线。5.3 上线后如果发现表结构有问题先看什么线上报“字段不存在”“数据超长”“唯一键冲突”时很多人第一反应是直接改表结构这样很容易把问题扩大。我一般按这个链排查先看报错日志确认 SQL 里访问的是哪张表、哪个字段。再查测试库和生产库的表结构比对是否有差异。看应用配置确认是否连接到了错误的库。看代码发布情况是不是新旧版本同时在跑导致新字段和旧字段不一致。确认需要动表结构时再评估表的数据量和锁表风险。改完结构之后用 EXPLAIN 检查相关 SQL 是否正常走上索引。最容易踩的是第 4 条。两个版本代码同时存在时老代码往表里插入旧格式数据新代码读取时可能直接报错。这个问题的根本原因不是表结构本身而是发布没做好协调。所以表结构变更应当和代码版本一起发布先兼容后迭代。6. 从表结构到前后端和报表怎么让数据真正被用起来6.1 前端接口需要的数据形态本质上决定是否要冗余字段后端表结构设计得再干净前端拿到的数据结构也不能完全等同于表结构。前端接口通常需要的是“可直接展示”的数据比如订单列表里要有用户名、商品名称、状态文本、金额文本。如果后端只把 orders 表字段直接返回前端就要自己处理状态映射、金额格式化还要自己拼订单明细。这样做不是不行但会让前端代码越来越难维护。正确的做法是在后端组装 VO视图对象。查询订单列表时先查出主表数据然后通过 order_id 批量查询明细按订单分组再塞进 VO。不要在一个循环里反复查询数据库否则接口会越来越慢。比如一次查 100 张订单每张订单再查 3 条明细就变成了 100 次额外查询。批量查询一次拉出来内存里做分组响应时间会稳定很多。表结构设计里的一些冗余字段比如 orders 表的 receiver_name就是为了让接口少做关联查询直接返回。但冗余要克制不能为了省一次查询把所有字段都塞进同一张表那样会导致更新异常和数据不一致。6.2 报表工具对接表结构时什么设计最友好项目里如果需要对接报表工具比如 jimuReport 这类 spring boot starter 组件或者自研报表模块都要注意表结构对报表的友好度。报表工具本身负责读取数据源、执行 SQL、渲染图表但它不能把设计得很差的表结构变好。报表工具最喜欢的数据形态是“宽表”或“视图”一行就是一个可统计的事实。比如订单明细宽表订单号用户ID用户昵称商品ID商品名称商品分类成交单价数量小计金额下单时间这样报表工具就能直接按分类、按时间段做聚合SQL 写起来很直观。如果要统计“每个分类下销售额 top10 商品”一条 GROUP BY 就能出来。但这不等于业务表也要做成宽表。我的建议是核心业务表仍然按头行结构设计然后单独建视图或中间宽表给报表用。这样做的好处是业务表保持稳定报表需求变化时只需要改视图不需要动核心表结构。像 jimuReport 这类工具通常只需要在数据源里配置 SQL底层表结构稳定报表维护成本才会低。6.3 后续升级预留拆分表、扩展字段和版本管理业务发展之后这套表结构大概率需要扩展。我常见的变化有这几种商品从单规格变成多规格需要拆出 product_sku 表库存表和库存流水表都要改成按 sku_id 存储。订单金额涉及优惠券、运费、税费只留一个 total_amount 不够可以增加 amount_detail 字段或者单独建金额明细表。订单状态从 TINYINT 扩展更多状态时要注意所有已跑的历史数据是否兼容。字段经常变化时可以预留一个 JSON 扩展字段但不能把 JSON 当万能口袋否则统计查询会越来越难。表结构版本管理建议接入 Flyway 或 Liquibase。每次变更都用版本化 SQL 文件记录而不是人肉到生产库执行。刚开始可能觉得多一道工序但时间一长它能避免大量“测试环境是好的生产环境怎么就不行”的问题。数据库表结构这件事真正决定成败的不是表建得多不多、字段全不全而是业务状态是否清晰、数据变化能否追溯、查询和写入是否可控。先理业务再建表然后带着状态流和排查思路去维护这套案例才算真正落地。如果只是自己学习这张表结构可以直接照着建跑一遍下单、退款流程就会理解为什么头行要拆分、流水为什么不能省。如果要长期维护那就要把日志、输出目录、变更版本、索引评审这些事提前安排上。踩过几次坑之后会发现很多问题不是工具能力不够而是前置环境和数据模型没有整理干净。
返回列表