生鲜B端供应链价格域表结构设计:六张核心表与版本化管理

发布时间:2026/10/6 21:19:35
生鲜B端供应链价格域表结构设计:六张核心表与版本化管理 做生鲜B端供应链的人应该都经历过这种场面凌晨四点半采购回来运营对着今日批发市场行情一批一批改价早上九点客户刚下单又打电话来问“昨天谈好的协议价怎么没了”月底对账客户拿着历史订单说当初承诺过阶梯价而你翻遍系统都找不到当时那张价目表。这种问题十有八九不是运营流程的问题而是价格相关的表结构从一开始就没设计好。升鲜宝这类面向餐饮连锁、生鲜门店、商超食堂的供应链管理系统B端客户和C端消费者最大的区别就是价格。C端一套标准零售价就能打天下B端则是每个客户的价格都可能不一样价格天天变变了还得留证据。这篇文章我把升鲜宝B端客户价格域的表结构设计完整梳理一遍六张核心表的字段定义、每个关键字段为什么这么设计、订单查价时怎么走、以及实际踩过的坑。适合做供应链、进销存、SaaS电商后台的开发同学参考产品经理也能拿来理解价格域背后的数据逻辑。1. 为什么B端价格要单独做成一个“域”1.1 生鲜B端定价的四个特殊性先说清楚生鲜这个品类在定价上有多特殊不然你很难理解后面那些表为什么长这样。第一价格日更甚至时更。蔬菜、水产、肉禽的价格跟着批发市场走早市的成交价决定了今天白天的销售价台风一来、产地断货价格半天就能调一次。这和工业品一卖卖半年的价格周期完全不在一个量级。第二一客一价。连锁餐饮总部签了月结协议价街边小店只能拿挂牌价大客户要求锁价保底新客户给七天体验价。每个客户背后的商务条款都不一样不可能用一张统一价目表糊弄过去。第三区域成本差异。同样一斤土豆配送到城东和配送到郊县冷链车成本和损耗率完全不同销售价就必须按区域拉开差距。第四事后要能追溯。客户下单那一刻看到的价格必须冻结在订单上月底对账时客户说“你当时不是这个价”你得能把当天那版价目表翻出来当证据。这四个特殊性叠加在一起价格就不适合做成订单模块里的一个附属字段必须独立成一个领域来设计。1.2 价格域到底是个什么概念价格域price domain这个词往大了说是在用领域驱动的思路给系统划边界价格自己的版本、明细、规则、日志自成一套不跟订单、商品、客户这些主数据搅在一起。往具体了说价格域可以理解成“一批适用相同价格策略的客户集合”。比如升鲜宝里可以按配送区域建“华东区价格域”“华南区价格域”也可以按客户类型建“连锁餐饮域”“生鲜门店域”。同一个域里的客户共享一套默认销售价、一套加价规则、一套调价流程。某个客户想在域默认价基础上单独谈价格就在域下面给他挂一份客户专项价格覆盖默认值。这个思路和商品分类的“类目-属性”其实很像先把共性收拢成模板再用个性化去覆盖共性。表结构设计的核心就是把这个“共性收拢、个性覆盖”的关系落成外键和优先级规则。1.3 不做价格域会出什么问题有人会说不就是价格表吗客户表加个price字段、SKU表加个sale_price搞这么复杂干什么我见过太多系统就是这么起步的然后在一个月内集体翻车。客户表直接加价格字段的问题最明显一个客户只能存一个当前价,历史价格根本没有地方落,月底对账只能对着库存流水猜。SKU表加统一售价的问题也很快暴露连锁客户要协议价、区域客户要运费加成,你的商品主数据会被价格字段塞得乱七八糟,改价还会直接污染商品基础资料。再退一步就算你单独建了一张customer_price表没有价格域这层抽象你会发现每个客户的价目表都是独立维护的。一万个客户就是一万份价目表采购价一变运营要手动去改几百个客户的价漏改一个就是毛利损失加客户投诉。价格域把“一批客户共享一份底价、各自按规则生成售价”这件事落到了数据模型上运营只需要维护域的默认价和少数大客户的专项价维护成本直接降一个数量级。2. 价格域六张核心表的分工总览整个价格域我最终拆成了六张表各管一段互不越界表名职责核心作用p_price_domain价格域主表定义哪些客户群体构成一个独立定价单元p_price_domain_customer价格域客户关系表维护客户和价格域的归属关系及有效期p_price_version价格版本表给每个价格域每天/每周的价格快照建版本号p_price_detail客户价格明细表域默认价和客户专项价的落表位置p_price_tier阶梯价格表实现按数量区间定价的量价关系p_price_rule定价规则表定义加价率、取整策略等自动定价参数p_price_adjust_log调价日志表记录每一次价格变动的完整审计链路设计时有几个基本原则贯穿始终第一版本化。所有价格都不能只有“当前值”必须挂在某个版本下面。生鲜价格本质上是时间序列数据版本表就是时间轴。第二查询冗余。明细表里冗余domain_id、customer_id写的时候多占一点空间查的时候少做几次join。价格查询是订单创建时的高频路径宁可冗余不要过度范式化。第三逻辑删除。价格数据不能物理删删了就什么都没了。所有主表都带is_deleted字段配合调价日志做双保险。第四金额精度用decimal永远不用float。价格算错一分的后果远比省那一点存储空间严重。3. 主表设计价格域表与客户关系表3.1 p_price_domain价格域主表这是价格域的门面定义了一个价格域的基本属性。建表语句如下CREATE TABLE p_price_domain ( id bigint unsigned NOT NULL AUTO_INCREMENT COMMENT 主键, domain_code varchar(32) NOT NULL COMMENT 价格域编码, domain_name varchar(64) NOT NULL COMMENT 价格域名称, domain_type tinyint NOT NULL DEFAULT 1 COMMENT 类型: 1-区域 2-客户等级 3-渠道, region_id bigint DEFAULT NULL COMMENT 关联配送区域ID, currency varchar(8) NOT NULL DEFAULT CNY COMMENT 币种, base_price_source tinyint NOT NULL DEFAULT 1 COMMENT 基准价来源: 1-采购价加权 2-市价 3-手工, markup_rule_id bigint DEFAULT NULL COMMENT 默认加价规则ID, status tinyint NOT NULL DEFAULT 1 COMMENT 状态: 0-停用 1-启用, valid_from datetime NOT NULL COMMENT 生效开始, valid_to datetime DEFAULT NULL COMMENT 生效结束, NULL表示永久, create_by varchar(32) DEFAULT NULL COMMENT 创建人, create_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 创建时间, update_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 更新时间, is_deleted tinyint NOT NULL DEFAULT 0 COMMENT 逻辑删除, PRIMARY KEY (id), UNIQUE KEY uk_domain_code (domain_code), KEY idx_region_id (region_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENTB端客户价格域表;几个字段重点说一下。domain_type区分区域、客户等级、渠道三种建模角度。区域价格域适合把配送范围内的客户统一管起来客户等级价格域适合按KA、普通、新客分层定价渠道价格域适合区分线上商城、线下批发、团购等销售通路。三种类型最终都落到同一张表只是region_id和关联对象不同业务隔离但模型统一。base_price_source是给自动调价用的。生鲜价格不能全靠人工拍脑袋价格域要能声明自己的基准价从哪来采购价加权平均、批发市场市价、还是运营手工录入。这个字段加上markup_rule_id就是在告诉系统“这个域的默认售价自动算的时候怎么算”为后面接定价引擎留好了口子。valid_from和valid_to很多表会漏掉结果就是价格域想停用的时候只能改status想指定生效日期还得另开字段。直接在主表上带有效期状态机就完整了草稿、启用、到期停用。3.2 p_price_domain_customer客户归属关系表客户和价格域是多对多的关系吗严格说任意时刻一个客户只能有一个生效的主价格域。但实现的时候一定要用独立的关系表而不是在客户主表上加domain_id字段。原因很简单客户有可能调整归属比如从普通域升级到KA域你得留下调域的历史记录客户还可能临时进入某个促销域结束后自动回到主域。CREATE TABLE p_price_domain_customer ( id bigint unsigned NOT NULL AUTO_INCREMENT, domain_id bigint NOT NULL COMMENT 价格域ID, customer_id bigint NOT NULL COMMENT B端客户ID, belong_type tinyint NOT NULL DEFAULT 1 COMMENT 归属类型: 1-主价格域 2-临时价格域, effective_from datetime NOT NULL COMMENT 生效起, effective_to datetime DEFAULT NULL COMMENT 生效止, NULL表示永久, priority int NOT NULL DEFAULT 0 COMMENT 优先级, 数字越大越优先, remark varchar(255) DEFAULT NULL COMMENT 备注, create_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, update_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_domain_customer_time (customer_id, domain_id, effective_from), KEY idx_domain_id (domain_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT价格域客户关系表;belong_type区分主价格域和临时价格域是生鲜促销场景的刚需。比如某个区域要做三天节日促销把一批客户临时划进促销域促销结束关系自动失效客户无缝回到主域。priority字段解决一个客户同时命中多条关系记录时的取舍问题。查客户当前属于哪个域标准SQL是这样SELECT domain_id FROM p_price_domain_customer WHERE customer_id :customerId AND effective_from NOW() AND (effective_to IS NULL OR effective_to NOW()) ORDER BY priority DESC, effective_from DESC LIMIT 1;3.3 为什么不直接在订单表上记价格这个问题值得展开。B端系统最终的订单结算肯定要冻结价格快照这是对的。但订单表记的是“最终成交价”是一个结果值价格域表管的是“这个客户今天应该看到什么价”是一个过程值。两个职责必须分开。如果只依赖订单冻结价客户问“为什么今天给我这个价”的时候你需要反查价格域看是不是运营今天调了价、是不是这个客户的协议价没生效、是不是域关系已经过期。没有一套完整的价格域数据这种问题只能靠人去猜。做B端核心系统我们要的是任何价格争议都能在数据层面一查到底而不是靠销售拍胸脯。4. 价格版本与明细表把“日更价”做成可回滚的快照4.1 p_price_version价格版本表生鲜价格的版本化核心是给每一个价格域、每一个日期或周期建一个独立的版本记录。版本号就是时间轴所有价格变动都挂在这个时间轴上。CREATE TABLE p_price_version ( id bigint unsigned NOT NULL AUTO_INCREMENT, version_no varchar(32) NOT NULL COMMENT 版本号, 如V20240617, domain_id bigint NOT NULL COMMENT 价格域ID, version_date date NOT NULL COMMENT 价格生效日期, publish_status tinyint NOT NULL DEFAULT 0 COMMENT 状态: 0-草稿 1-待审批 2-已发布 3-已作废, approve_by varchar(32) DEFAULT NULL COMMENT 审批人, approve_time datetime DEFAULT NULL COMMENT 审批时间, publish_by varchar(32) DEFAULT NULL COMMENT 发布人, publish_time datetime DEFAULT NULL COMMENT 发布时间, price_cycle tinyint NOT NULL DEFAULT 1 COMMENT 价格周期: 1-日价 2-周价 3-月价, remark varchar(255) DEFAULT NULL, create_by varchar(32) DEFAULT NULL, create_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, update_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, is_deleted tinyint NOT NULL DEFAULT 0, PRIMARY KEY (id), UNIQUE KEY uk_domain_date_version (domain_id, version_date, version_no), KEY idx_domain_date (domain_id, version_date) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT价格版本表;你可能会问为什么不直接在明细表上带一个valid_from和valid_to字段非要再加一层版本表区别在于批量操作和审批流。价格域每天要发布几百上千个SKU的价格如果每个SKU各自独立生效运营根本没法统一检查“今天这个域的价格齐不齐、有没有漏项的”。版本表让一批价格成为一个可审批、可发布、可作废的整体这就是把价格当作一个域级对象来管理。publish_status的流转是运营建草稿导入或录入价格明细提交审批主管批量通过发布。发布之后当天价格才对客户可见。这个状态机看着简单但它直接决定了调价流程能不能在系统里闭环也决定了价格发布这个动作是原子的——要么整版发布要么整版退回。4.2 p_price_detail客户价格明细表版本表是壳明细表才是肉。每个SKU在这个版本里卖多少钱、卖给谁多少钱都落在这一层。CREATE TABLE p_price_detail ( id bigint unsigned NOT NULL AUTO_INCREMENT, version_id bigint NOT NULL COMMENT 价格版本ID, domain_id bigint NOT NULL COMMENT 价格域ID, 冗余便于查询, sku_id bigint NOT NULL COMMENT 商品SKU ID, price_type tinyint NOT NULL DEFAULT 1 COMMENT 价格类型: 1-标准销售价 2-协议价 3-促销价, customer_id bigint NOT NULL DEFAULT 0 COMMENT 客户ID, 0表示域默认价, price decimal(12,3) NOT NULL COMMENT 销售单价, min_price decimal(12,3) DEFAULT NULL COMMENT 最低限价, 自动调价保护用, max_price decimal(12,3) DEFAULT NULL COMMENT 最高限价, unit varchar(16) NOT NULL DEFAULT 斤 COMMENT 计价单位, pricing_method tinyint NOT NULL DEFAULT 1 COMMENT 计价方式: 1-固定价 2-规则自动价 3-阶梯价, tax_rate decimal(5,2) DEFAULT NULL COMMENT 税率百分比, sort_order int NOT NULL DEFAULT 0 COMMENT 同一SKU多价格时的优先级, remark varchar(255) DEFAULT NULL, create_by varchar(32) DEFAULT NULL, create_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, update_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, is_deleted tinyint NOT NULL DEFAULT 0, PRIMARY KEY (id), UNIQUE KEY uk_version_sku_customer (version_id, sku_id, customer_id), KEY idx_domain_sku (domain_id, sku_id), KEY idx_customer_sku (customer_id, sku_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT客户价格明细表;这张表最核心的设计是customer_id用0当“域默认价”的哨兵值。比如华东区价格域里西红柿的默认售价是3.5元/斤就插一条customer_id0的记录某连锁客户签了协议价3.2元/斤就再插一条customer_id具体客户ID的记录两条记录通过sort_order和查询逻辑保证优先级。这里有个很多新手会踩的坑如果customer_id允许为NULL并且你对(version_id, sku_id, customer_id)建唯一索引MySQL里NULL不会被唯一索引去重同一个版本同一个SKU可以插入多条customer_id为NULL的记录数据就脏了。用0做哨兵值唯一索引才能真正生效。price字段用decimal(12,3)很多人习惯用两位小数但生鲜按斤计价时经常出现单价到小数点后三位的情况比如白条猪按斤的折算价3.455元。保存时保留三位行总价再按分四舍五入精度损失比全程两位小数小很多。domain_id冗余在明细表里看起来违背范式但实际查询场景中用户给我的最常见问题就是“今天华东区所有SKU的价格发我一份”没有domain_id就得先从version_id反查域再嵌套子查询性能差且SQL难写。价格域表的定位是支撑高频查询我这里明确选择了冗余字段换查询性能。4.3 订单查价时的完整路径客户下单时系统查价走的是一条三级链路第一级确定客户当前所属的价格域SELECT domain_id FROM p_price_domain_customer WHERE customer_id :customerId AND effective_from NOW() AND (effective_to IS NULL OR effective_to NOW()) ORDER BY priority DESC, effective_from DESC LIMIT 1;第二级取出今天已发布的版本SELECT id FROM p_price_version WHERE domain_id :domainId AND version_date CURDATE() AND publish_status 2 AND is_deleted 0 LIMIT 1;第三级先在明细表里查该客户的专项价查不到就回退到customer_id0的域默认价SELECT price FROM p_price_detail WHERE version_id :versionId AND sku_id :skuId AND customer_id :customerId AND is_deleted 0 UNION ALL SELECT price FROM p_price_detail WHERE version_id :versionId AND sku_id :skuId AND customer_id 0 AND is_deleted 0 LIMIT 1;三级链路里前两级的结果可以在缓存层按customer_id, date做key缓存第三级按domain_id, sku_id, date缓存订单创建高峰期的查价压力就能控住。5. 阶梯价、定价规则和单位换算量价关系怎么落地5.1 p_price_tier阶梯价格表生鲜B端的大客户普遍有“买得越多越便宜”的诉求阶梯价是躲不开的功能。阶梯价挂在价格明细下面一个SKU一个版本的固定价可以带多个阶梯区间。CREATE TABLE p_price_tier ( id bigint unsigned NOT NULL AUTO_INCREMENT, price_detail_id bigint NOT NULL COMMENT 价格明细ID, tier_no tinyint NOT NULL COMMENT 阶梯序号, min_qty decimal(12,3) NOT NULL COMMENT 数量下限, 含该值, max_qty decimal(12,3) DEFAULT NULL COMMENT 数量上限, 不含该值, NULL表示不封顶, tier_price decimal(12,3) NOT NULL COMMENT 该阶梯单价, unit varchar(16) NOT NULL DEFAULT 斤, create_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, update_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_detail_tier (price_detail_id, tier_no), KEY idx_detail_qty (price_detail_id, min_qty) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT阶梯价格表;区间设计上我坚持“下限包含、上限不包含”1到50斤是一个价50到100斤是一个价这样50斤到底算哪个区间就永远不会歧义。tier_no给出阶梯序号是为了防止运营录数据时把区间顺序录反了程序可以按tier_no强制校验min_qty必须递增。有个经验是阶梯价的区间数量别贪多。生鲜品按斤卖50斤、100斤设两到三个阶梯足够设十个阶梯运营维护成本极高而且客户根本买不到那个量级纯粹给自己找麻烦。5.2 p_price_rule定价规则表基准价定了之后往往还要套一层规则才能变成最终售价。升鲜宝里我设计了一张独立的规则表而不是把规则逻辑全部写死在Java代码里CREATE TABLE p_price_rule ( id bigint unsigned NOT NULL AUTO_INCREMENT, domain_id bigint NOT NULL COMMENT 价格域ID, rule_code varchar(32) NOT NULL COMMENT 规则编码, rule_name varchar(64) NOT NULL COMMENT 规则名称, rule_type tinyint NOT NULL COMMENT 类型: 1-加价率 2-固定加价 3-毛利保护 4-运费加成, rule_value decimal(12,4) DEFAULT NULL COMMENT 规则值, 加价率为百分比, 固定加价为金额, calc_base tinyint NOT NULL COMMENT 计算基准: 1-采购价 2-前日销售价 3-市场指导价, round_type tinyint NOT NULL DEFAULT 1 COMMENT 取整策略: 1-四舍五入 2-向上 3-向下, round_digit tinyint NOT NULL DEFAULT 1 COMMENT 取整位数, priority int NOT NULL DEFAULT 0 COMMENT 执行优先级, status tinyint NOT NULL DEFAULT 1 COMMENT 状态: 0-停用 1-启用, create_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, update_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_domain_rule (domain_id, rule_code), KEY idx_domain_type (domain_id, rule_type) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT定价规则表;这张表解决的是生鲜定价里一个很实际的诉求采购价天天变销售价如果每次都靠人工维护几百个SKU既不现实又容易出错。有了规则表运营只需要维护核心SKU的基准价系统自动按加价率计算再按取整策略做尾数处理。比如规则配成采购价基础上加12%四舍五入保留一位小数最后强制个位逢九x.x9这样客户看到的报价就是那种熟悉的带尾数的生鲜价不会出现3.1415926这种一塌糊涂的数字。毛利保护是另外一个容易被忽略的场景。大客户的协议价往往压得很低采购价一涨按原售价卖就倒挂。min_price和max_price字段加在明细表上配合规则表里的毛利保护规则系统自动调价时一旦算出来的价低于最低限价就触发提醒或者走审批而不是默默生成一个亏本价。这里我踩过很深的坑后面复盘的时候详细说。5.3 单位换算按斤、按份、按箱的隐藏坑生鲜里一个SKU可能同时存在三种计价单位西红柿可以按斤报价净菜车间可以按份报价整箱西红柿又可以按箱报价。如果价格域表不考虑单位三个价格在明细表里各算各的订单模块换算时很容易算错。我的建议是单位换算别放在价格域里做而是放在商品主数据里做好标准单位、辅单位的换算率价格域明细表只负责记录“这个SKU在当前域用哪个单位计价”。也就是说p_price_detail.unit字段为什么不能省就是因为它显式声明了该域该SKU的计价单位。从订单进来开始所有数量、金额都以这个unit为准换算问题在商品主数据层一次解决价格域不背这个锅。6. 调价日志表所有价签变化的完整证据链6.1 p_price_adjust_log调价日志表价格域的数据是过程数据过程数据必须有审计。生鲜行业最怕的就是调价没有记录客户投诉“你们乱涨价”的时候你连谁改的、什么时候改的、为什么改的都答不上来。CREATE TABLE p_price_adjust_log ( id bigint unsigned NOT NULL AUTO_INCREMENT, domain_id bigint NOT NULL COMMENT 价格域ID, version_id bigint NOT NULL COMMENT 价格版本ID, sku_id bigint NOT NULL COMMENT 商品SKU ID, customer_id bigint DEFAULT NULL COMMENT 客户ID, NULL表示域默认价调整, old_price decimal(12,3) DEFAULT NULL COMMENT 原单价, new_price decimal(12,3) NOT NULL COMMENT 新单价, adjust_type tinyint NOT NULL COMMENT 调整类型: 1-新建 2-修改 3-作废 4-批量调价, batch_no varchar(32) DEFAULT NULL COMMENT 批量调价批次号, source_type tinyint NOT NULL DEFAULT 1 COMMENT 来源: 1-人工 2-规则自动 3-接口导入, adjust_reason varchar(255) DEFAULT NULL COMMENT 调价原因, adjust_by varchar(32) NOT NULL COMMENT 操作人, adjust_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 调整时间, PRIMARY KEY (id), KEY idx_domain_time (domain_id, adjust_time), KEY idx_sku_time (sku_id, adjust_time), KEY idx_customer_sku (customer_id, sku_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT调价日志表;old_price必须存这是审计和争议处理的核心。只记新价格、靠看版本历史反推旧价的方式在生产环境里效率太低而且版本明细被物理调整后很难还原。把old_price和new_price放在一条记录里任何争议都能直接回答“从什么价改成了什么价”。batch_no字段是给批量调价用的。一次批量调价可能涉及几百个SKU每个SKU产生的日志行共享同一个batch_no系统可以一次性反查这批调价的整体影响算算这个域调价后毛利总额变化多少这在生鲜行业每天凌晨批量调价的场景下特别有用。6.2 调价日志和审批流程怎么配合调价日志表只管记录审批流程由独立的审批单据控制两张表通过version_id和batch_no做关联。运营发起调价生成一条审批单和一批明细变更主管审批通过系统批量执行变更并写调价日志审批驳回日志不生成。这样设计的目的是把“业务状态”和“审计事实”分开审批单记录的是审批这件事的状态流转日志表记录的是价格这件事的事实变更。前者可以作废、退回、重新提后者一旦写入就只追加不可修改。逻辑上我禁止对p_price_adjust_log做任何update操作代码层面也只暴露insert接口。7. 踩坑复盘与性能建议7.1 五个我实际踩过的坑第一用客户主表的字段存价格。早期版本为了省事在customer表加了price_level和default_price两个字段。后果是客户调域时历史价格完全丢失运营要查一个客户上个月的价格得靠订单反推最后全部返工迁移到关系表。第二用float或double存价格。单价、金额这类数据必须decimal浮点数在生鲜这种大量小数参与计算、还要四舍五入的场景里误差非常容易被放大。西红柿3.455元/斤10000斤的货款差一个分账年底审计就是事故。第三明细表customer_id允许NULL导致数据重复。前面说过MySQL唯一索引不约束NULL值我见过生产库同一版本同一SKU出现三条默认价记录订单查价随机命中一条客户今天买3.5明天买3.6投诉电话被打爆。用0做哨兵值之后彻底解决。第四批量调价不做事务。有一版调价接口用循环逐条update调到一半进程重启结果前半段改了后半段没改价格版本状态还是已发布客户下单直接是半个新价。后来所有批量调价都包在一个事务里版本状态先置为草稿全部明细更新成功后再置为已发布。第五价格域有效期和客户归属不同步。客户在三号从华东域调到华南域但关系表里华东域的关系记录没有设置effective_to查当前域的SQL就同时命中两个域priority又不一致最终走了一个运营根本没想到的域。关系表每次变更都要强制校验新关系的生效时间和旧关系的失效时间不能有空档。7.2 索引设计与查价性能价格域查询有几条高频SQL索引都是围绕它们建的。查客户归属域走p_price_domain_customer的customer_id索引查当天版本走p_price_version的(domain_id, version_date)索引查价格明细走(version_id, sku_id, customer_id)唯一索引。订单创建高峰期查价并发很高绝大多数查询都是这三类索引命中后单次查询在10毫秒以内。如果后续规模上来建议在查询前加一层缓存。刚才说的三级链路里第一二级是客户维度的可以缓存customer_id, date第三级是SKU维度面向价格域缓存domain_id, sku_id, date。当天价格发布后清一次缓存整个系统的查价压力就完全可控了。7.3 冗余和规范化怎么取舍整个价格域我做了三处刻意的冗余明细表里的domain_id、diffine冗余在多张表的customer_id关系表、明细表、日志表都有、版本表里的domain_id。冗余的原则是只冗余常用来查询过滤的高基数字段不冗余大文本和金额结果。每次冗余都提前评估了写放大和一致性维护成本在这些表里写频率远低于读频率所以收益远大于成本。一致性维护的办法是严格走Service层。价格域表不允许任何模块直接改库所有写操作统一走PriceDomainService、PriceVersionService这些领域服务代码评审时重点盯这几条写入路径从机制上保证冗余字段不会失同步。8. 关于这套设计的后续扩展一点真实体会价格域这套表结构跑通之后后面接什么功能都有底。我们后续在升鲜宝上陆续接了价格审批流、自动调价任务、毛利模拟沙盘都是在这六张表的基础上加逻辑没有再动过表结构。自动调价任务每天凌晨拉取价格域的base_price_source按p_price_rule里的规则算出建议价先落到草稿版本运营确认后再发布全程不碰订单在用的正式版本。我个人做这块最大的体会是生鲜B端价格的核心不是“定一个价”而是“管理一个随时在变、还一客一价的价格生命期”。表结构设计得稳产线运营和商务扯皮就少系统能走多远不取决于算法多花哨而是底层数据模型能不能把每一个价签的变化都讲清楚。你如果也在做类似的供应链系统建议先把“域默认价客户专项价版本快照调价日志”这条主链路搭扎实再碰那些花哨的自动定价和促销玩法。