尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

B端客户价格域表结构设计:从价格字段到多维定价模型的工程实践

B端客户价格域表结构设计:从价格字段到多维定价模型的工程实践 升鲜宝这类供应链系统里B端客户价格的复杂度往往被严重低估。业务方提需求时经常一句话带过——“给这个客户报个价就行”但真正落地到数据库层面你会发现一堆绕不开的问题一个客户在不同区域、不同商品线、不同账期条件下价格到底怎么算总部价、区域价、客户单独协议价谁优先客户下级的门店是统一价还是独立价这些如果不在表结构设计阶段就把“价格域”这个概念理清楚后面业务一跑起来数据改起来就是灾难。这篇文章就围绕升鲜宝供应链管理系统中B端客户价格域的表结构设计把业务场景、表模型、字段设计逻辑、价格匹配流程和实操踩坑一次性讲透。适合正在做供应链、电商中后台、ERP/CRM价格模块的产品经理、后端研发和DBA参考尤其是Bi端多层级组织架构下的定价规则设计这部分的通用性很强看完可以直接拿去改自己的业务模型。1. 为什么B端价格必须做“价格域”而不是简单存一个价格字段很多系统在最开始的时候B端客户价格就是客户表上加一个price字段或者一个discount字段简单粗暴。但这个做法在供应链行业几乎撑不过三个月原因很现实。1.1 B端定价是多维度的不是单一值生鲜供应链的业务现场是这样的一个连锁餐饮客户总部在北京上海和成都有分公司每个分公司的采购品类和采购量完全不同。总部跟升鲜宝签的可能是全品类统一框架价但上海分公司因为走量特别大单独申请了猪肉类的特价协议成都分公司则因为新拓市场总部给了一个首月85折的新客扶持价。这时候如果你问“这个客户的价格是多少”答案是不确定的——得看哪个组织、哪个品类、哪个时间窗口。更麻烦的是价格还会叠加各种业务条件现结和月结价格不同、起送量阶梯价、账期超过30天要加价、某些生鲜品类因为损耗率高不允许打折。这些都是业务侧非常合理的诉求但落到表结构上如果只是“客户表存一个价格字段”你根本没法表达。价格域的本质就是把“定价”从客户主数据里拆出来做成独立的、可组合、可叠加的领域模型。我一直把价格域理解成“定价的坐标系”——客户是横轴商品/品类是纵轴时间、组织、结算方式是纵深维度任意一个坐标点命中一个价格值。这个坐标系一旦建好后续的版本管理、权限隔离、价格审批、历史追溯全都有了清晰的落点。1.2 价格域要解决的三类核心问题结合升鲜宝这个场景价格域至少要扛住三类问题第一类是适配问题。一个客户可能横跨多个区域、多个法人主体、多个结算主体不同组合下价格不能串。比如同一家连锁客户A城市门店和B城市门店因为冷链配送成本不同同类商品的配送价相差5%-8%这就必须靠价格域把区域维度切开。第二类是优先级问题。不同层级的定价策略同时命中时谁说了算典型场景客户协议价 客户组协议价 区域标准价 总部标准价。而且实际业务中还有“特批价”“促销价”“阶梯价”这些临时策略叠加如果没有一个明确的优先级机制就会出现同一张订单不同人看到不同价格的情况。第三类是生命周期问题。报价单是有时效的合同一年一签促销可能只有一周损耗补偿价只针对某批次。价格域必须有版本、生效期、失效期、状态这几个基本属性否则过期价格会成为对账时最大的烂账来源。这三点想清楚了表结构设计的方向就不会跑偏。2. 价格域核心表结构设计从客户域到价格域的完整模型下面直接进入实操。我以升鲜宝实际项目里的表结构为蓝本把核心表拆开讲。整体模型分四层组织与客户相关表、价格域主表、价目明细表、规则辅助表。2.1 客户与组织基础表价格域的“坐标轴”价格域不是凭空存在的它首先要锚定在客户维度上。这里涉及三张基础表客户主表、客户组织关系表、客户分组表。客户主表是最基本的核心字段如下-- 客户主表核心字段 CREATE TABLE crm_customer ( id bigint(20) NOT NULL AUTO_INCREMENT COMMENT 主键, customer_code varchar(32) NOT NULL COMMENT 客户编码业务唯一, customer_name varchar(128) NOT NULL COMMENT 客户名称, customer_type tinyint(4) NOT NULL COMMENT 客户类型1-集团客户2-区域分公司3-单店客户, parent_id bigint(20) DEFAULT NULL COMMENT 上级客户ID用于集团-分公司-门店层级, status tinyint(4) NOT NULL DEFAULT 1 COMMENT 状态0-停用1-启用, price_level tinyint(4) DEFAULT 3 COMMENT 默认价格层级1-总部价2-区域价3-协议价, 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_customer_code (customer_code), KEY idx_parent_id (parent_id) ) ENGINEInnoDB COMMENTB端客户主表;这里有个容易被忽略的点很多系统把“集团客户”和“单店客户”混在一张表里靠parent_id自关联。实际运营中集团客户和单店客户的结算方式、开票主体、配送地址差异巨大业务上建议用customer_type做区分但物理上保持一张表。这样既方便统一查询又能在价格域里通过“是否包含下级”来控制价格适用范围。客户组织关系表解决的是“客户的下级组织也有独立定价或独立结算”的问题核心字段包括-- 客户组织关系表 CREATE TABLE crm_customer_org_rel ( id bigint(20) NOT NULL AUTO_INCREMENT, customer_id bigint(20) NOT NULL COMMENT 客户ID, org_id bigint(20) NOT NULL COMMENT 组织ID对应组织架构表中的分公司/门店, org_type tinyint(4) NOT NULL COMMENT 组织类型1-分公司2-门店/配送点3-仓库, is_price_scope tinyint(1) NOT NULL DEFAULT 1 COMMENT 是否参与独立定价1-是0-否, PRIMARY KEY (id), UNIQUE KEY uk_customer_org (customer_id, org_id) ) ENGINEInnoDB COMMENT客户与组织关系表;这张表最大的价值是打破了“客户单一法人”的思维惯性。连锁客户的不同门店可能由不同区域仓配送而区域仓的成本结构不同所以价格域必须能精确到“客户组织”这一层。客户分组表则是价格批量应用的关键运营人员可以把一批客户加入“KA客户组”“社区团购客户组”然后在价格域里直接对组定价省去逐客户配置的成本-- 客户分组表 CREATE TABLE crm_customer_group ( id bigint(20) NOT NULL AUTO_INCREMENT, group_code varchar(32) NOT NULL COMMENT 分组编码, group_name varchar(64) NOT NULL COMMENT 分组名称, description varchar(255) DEFAULT NULL, create_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_group_code (group_code) ) ENGINEInnoDB COMMENT客户分组表;另外还需要一张客户分组明细表将客户和分组关联起来。实际设计时建议用“分组-客户”一对多的关系表而不是在客户表里加group_id字段这样客户可以被归入多个分组价格命中的灵活性会大幅提升。2.2 价格域主表定义“一个价格策略的边界”价格域主表是整个模块的核心它定义了一个价格策略的适用范围、优先级、状态和生命周期。我把它命名为price_domain字段设计如下-- 价格域主表 CREATE TABLE price_domain ( id bigint(20) NOT NULL AUTO_INCREMENT, domain_code varchar(32) NOT NULL COMMENT 价格域编码业务唯一如PD-2024-0001, domain_name varchar(128) NOT NULL COMMENT 价格域名称, domain_type tinyint(4) NOT NULL COMMENT 价格域类型1-总部标准价2-区域标准价3-客户协议价4-客户组协议价5-特殊审批价, customer_id bigint(20) DEFAULT NULL COMMENT 客户ID当domain_type3或5时必填, customer_group_id bigint(20) DEFAULT NULL COMMENT 客户分组ID当domain_type4时必填, org_id bigint(20) DEFAULT NULL COMMENT 组织ID区域价时必填, start_date date NOT NULL COMMENT 生效日期, end_date date DEFAULT NULL COMMENT 失效日期NULL表示长期有效, priority tinyint(4) NOT NULL DEFAULT 10 COMMENT 优先级数字越小优先级越高, status tinyint(4) NOT NULL DEFAULT 0 COMMENT 状态0-草稿1-已发布2-已失效3-已废弃, check_status tinyint(4) NOT NULL DEFAULT 0 COMMENT 审批状态0-待提交1-审批中2-已通过3-已驳回, price_type tinyint(4) NOT NULL DEFAULT 1 COMMENT 价格类型1-固定价2-加价率3-折扣率4-阶梯价, currency char(3) NOT NULL DEFAULT CNY COMMENT 币种, remark varchar(255) DEFAULT NULL, create_by varchar(32) 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_code (domain_code), KEY uk_customer_start_date (customer_id, start_date), KEY idx_group_start_date (customer_group_id, start_date) ) ENGINEInnoDB COMMENT价格域主表;这张表里有几个字段值得展开说。domain_type决定了价格域的“坐标轴”是谁。类型1只绑定商品不绑定客户类型3绑定客户但不绑定组织类型5则可以是“客户商品批次”这种颗粒度。不同的类型天然有不同的优先级层级这个在设计价格引擎时会用到。priority字段非常重要。B端价格命中的核心逻辑就是先按规则筛选出所有命中的价格域再取优先级最高的那个。我习惯用数字越小优先级越高而且把优先级的跨度拉大一些比如总部价100、区域价80、客户组协议价50、客户协议价30、特殊审批价10这样中间插入新层级时不需要改大量已有数据。end_date留空表示长期有效但注意查询时永远要带“当前日期在start_date和end_date之间”的条件。很多价格事故都出在“只看了status1没看日期范围”。这里还建议加一个parent_domain_id字段用来做价格域的版本关联。比如2025年的客户协议价是从2024年的协议价复制修改过来的通过这个字段可以追溯到历史版本的沿革。这在审计和合同续签场景下非常有用。2.3 价格域明细表每个SKU或品类一个价格条目价格域主表只是定义了“这是谁的价哪个区域的价什么时间有效”真正的价格明细在子表里。我设计了price_domain_detail来装具体的SKU价格-- 价格域明细表 CREATE TABLE price_domain_detail ( id bigint(20) NOT NULL AUTO_INCREMENT, domain_id bigint(20) NOT NULL COMMENT 价格域主表ID, sku_id bigint(20) NOT NULL COMMENT 商品SKU ID, category_id bigint(20) DEFAULT NULL COMMENT 品类ID按品类定价时使用与sku_id二选一, base_price decimal(10,2) NOT NULL COMMENT 基准价含税, min_price decimal(10,2) DEFAULT NULL COMMENT 最低限价防止误操作, max_price decimal(10,2) DEFAULT NULL COMMENT 最高限价, quote_price decimal(10,2) NOT NULL COMMENT 最终报价, min_qty decimal(10,2) DEFAULT NULL COMMENT 最小起订量, unit varchar(16) NOT NULL COMMENT 计价单位斤、箱、件等, tax_rate decimal(5,2) NOT NULL DEFAULT 9.00 COMMENT 税率%, is_active tinyint(1) NOT NULL DEFAULT 1 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_sku (domain_id, sku_id), KEY idx_category_id (category_id) ) ENGINEInnoDB COMMENT价格域明细表;写到这里必须强调一个现实问题生鲜供应链的SKU数量动辄几千上万如果一个客户协议价要在明细表里逐SKU录一遍运营会做到崩溃。所以设计上必须支持sku_id和category_id两种粒度并存。我的做法是明细表允许sku_id为空、category_id非空表示这个价格应用于该品类下的全部SKU如果某SKU有专属价格则sku_id非空、优先命中。在实际查询时价格引擎先找SKU级找不到再向上找品类级。这个逻辑很像路由表的“最长前缀匹配”看着简单但能覆盖90%的定价场景。base_price和quote_price分开设计是刻意为之。base_price是标准成本价或参考价quote_price是给客户的报价。两者分开的好处是财务对账时可以算出“这单到底让了多少利”运营复盘时也能分析“这个客户毛利率是多少”。如果只存一个最终价这些分析就全做不了。min_price和max_price是安全护栏。B端业务中经常会因为手工录入失误把报价改错比如把一个30块钱的蔬菜报价写成3块钱。有了上下限在订单审核环节就能直接拦截比事后追责强一万倍。2.4 阶梯价与折扣规则表把“复杂业务规则”从价格表中拆出来生鲜行业的定价不只是“一口价”阶梯价和折扣规则几乎天天用。阶梯价表示采购量越大单价越低比如0-500斤按6.5元/斤500-1000斤按6.2元/斤1000斤以上按5.9元/斤。这种场景可以在price_domain_detail的price_type4时通过阶梯规则子表来实现-- 阶梯价规则表 CREATE TABLE price_step_rule ( id bigint(20) NOT NULL AUTO_INCREMENT, detail_id bigint(20) NOT NULL COMMENT 价格明细ID, start_qty decimal(10,2) NOT NULL COMMENT 区间起始量, end_qty decimal(10,2) DEFAULT NULL COMMENT 区间结束量NULL表示无上限, price decimal(10,2) NOT NULL COMMENT 该区间单价, create_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_detail_id (detail_id) ) ENGINEInnoDB COMMENT阶梯价规则表;查询时在订单明细中带入实际下单数量匹配start_qty qty end_qty注意边界我用的是包含下限、不包含上限的规则的区间即可。这里有三个容易踩的坑区间必须连续且不能重叠否则就会出现“同一种商品同一种数量匹配到两个价格”的情况。我的建议是在业务逻辑层增加区间重叠校验而不是靠数据库约束。边界值的定义要统一。建议统一采用 start_qty AND end_qty方便开发和运维共同理解避免出现“正好买500斤算哪个价”这种扯皮问题。阶梯价表不建议频繁更新。每次调价都应该生成新的价格明细记录或版本而不是在原记录上update这样月度对账才能追溯“这个价格是什么时候生效的”。折扣规则表也是类似的思路但更灵活。升鲜宝的实践中折扣分为两种一种是对整张订单的折扣另一种是对特定品类的折扣。我单独建了一张price_discount_rule表字段包括rule_type订单级/品类级、discount_type折扣率/减免金额、discount_value、priority同时绑定到价格域主表。这里要注意的是折扣规则的优先级应该比价格域的优先级低一级因为先决定基准价再在基准价之上打折这个顺序不能反。3. 价格匹配流程数据库表怎么支撑“一单出价”表结构设计得再好最终也要落在“订单创建时系统如何自动带出价格”这个动作上。这一节讲清楚升鲜宝的价格匹配流程和对应的SQL实现思路。3.1 价格确定分四步走第一步确定客户的价格身份。比如客户是集团客户还是单店客户这一步确定后系统就知道要在客户协议价、客户组协议价、区域标准价、总部标准价之间做筛选。这里的关键是要把客户的层级关系带出来因为集团客户下挂的分公司可能复用集团价格也可能有独立协议价。第二步匹配商品范围和品类范围。将订单中的SKU逐条拿到价格域里匹配。优先匹配SKU级如果SKU级没有则匹配品类级。品类匹配要按照商品类目树的路径向上递归比如“大白菜”先匹配“叶菜类”没有再匹配“蔬菜类”。这个递归层级建议控制在3层以内否则性能会很难看。第三步过滤时间、区域和状态。价格域的生效时间必须覆盖当前订单日期区域ID必须匹配客户的配送组织状态必须为“已发布”且审批“已通过”。第四步按优先级取值。在步骤一到三之后命中的所有价格域中按照priority取值数字最小者胜出。如果存在多个相同优先级的价格域命中则取创建时间最新的那一条并记录告警日志以便运营排查。这四步看着不复杂但必须做成独立的PriceEngine模块不要散落在订单Service里。因为价格计算不仅下单要用报价单、合同审批、订单改价、售后赔付都会用到独立成服务才能保证各处价格口径一致。3.2 核心SQL获取某客户某SKU在当前时间的价格我用一条示例SQL来演示价格匹配的关键逻辑。假设要查询客户ID1001、区域2002、SKU3003的当前价格SELECT pd.domain_type, pd.priority, pdd.id AS detail_id, pdd.quote_price, pdd.min_qty, pd.start_date, pd.end_date FROM price_domain pd INNER JOIN price_domain_detail pdd ON pd.id pdd.domain_id WHERE pd.status 1 AND pd.check_status 2 AND pd.start_date CURDATE() AND (pd.end_date IS NULL OR pd.end_date CURDATE()) AND ( -- 客户协议价 (pd.domain_type 3 AND pd.customer_id 1001 AND pd.org_id IS NULL) OR -- 客户区域协议价 (pd.domain_type 3 AND pd.customer_id 1001 AND pd.org_id 2002) OR -- 客户组协议价 (pd.domain_type 4 AND pd.customer_group_id IN ( SELECT group_id FROM crm_customer_group_detail WHERE customer_id 1001 )) OR -- 区域标准价 (pd.domain_type 2 AND pd.org_id 2002) OR -- 总部标准价 (pd.domain_type 1 AND pd.org_id IS NULL) ) AND (pdd.sku_id 3003 OR (pdd.sku_id IS NULL AND pdd.category_id IN ( -- 向上递归品类 SELECT category_id FROM cms_category WHERE category_path LIKE %/3003的内部/% ))) ORDER BY pd.priority ASC, pd.create_time DESC LIMIT 1;这条SQL在数据量不大价格域明细几十万条、联合索引命中良好时是够用的但一旦价格域明细突破百万级别或者订单并发很高建议做两层优化。第一层是“区域客户”的缓存预热把常用的客户-商品价格组合预载到Redis里key设计为price:{customerId}:{skuId}:{date}value就是价格JSON第二层是独立价格查询服务用倒排索引的思路把“客户/品类/时间”映射到价格域ID列表避免每次都全表扫描。实际项目中我见过很多团队纠结“能不能一条SQL搞定价格匹配”我的建议是不要在一条SQL里追求完美。把匹配拆成两个步骤——先用条件筛出候选价格域列表这一步数据量小很快再根据优先级排序取值这样更清晰也更容易做缓存和日志。3.3 特殊场景阶梯价和折扣在匹配后怎么算订单中如果商品命中的价格域是阶梯价price_type4上面的SQL只能拿到明细ID还需要用订单数量再去price_step_rule表里查一次区间价格。如果商品命中的价格域还绑定了品类折扣那么最后的成交价按如下顺序计算最终成交价 阶梯价或固定价 × (1 - 品类折扣率)注意折扣率和减免金额互斥在设计接口时要防止业务人员同时对同一客户设置两种类型的折扣。为了防止这种冲突我在price_discount_rule表上加了约束同一个detail_id下的discount_type必须唯一。业务逻辑上如果有减免金额的需求可以另建特殊审批通道不走通用折扣。4. 版本管理与价格变更审核价格域表结构设计中最容易忽略的环节新版的价格域表结构如果只管“当前生效价格”那只能算及格远不能说优秀。供应链系统里价格变更是高频操作合同续签、季节性调价、临时促销、恶意低价纠正每个月都会发生。价格域表结构设计中最容易忽略的就是版本管理和变更审批。4.1 用状态机管理价格域的完整生命周期在price_domain表里我设计了status和check_status两个字段配合使用。status表示业务状态check_status表示审批状态。两者分开的好处是一个价格域可以是“审批通过但还没生效”比如下个月才执行的新价格也可以是“已生效但临时停用”比如发现价格异常需要紧急下架。实践经验里最好用的组合是status \ check_status待提交审批中已通过已驳回草稿有效---已发布--有效-已失效--有效期过后自动置为-已废弃--手动作废-状态变更必须记录日志。我建议单独建一张price_domain_log表每次修改主表或明细表都记录操作人、操作时间、变更前后的关键字段快照。虽然会增加一点写入量但在对账纠纷和审计时价值极大。4.2 新版本生效老版本保留价格域更新时禁止直接修改原记录。正确的做法是复制原价格域生成新版本修改需要变更的字段然后走审批流程。这样设计的收益非常直接订单始终关联“创建时的价格域版本”历史订单不会因为价格调整而出现数据漂移。合同到期续签时直接复制上个版本再批量改价操作效率高且不容易漏SKU。万一新价格有误回滚时只需要把新版本失效、恢复旧版本即可不需要做逆向update。这里我在price_domain表上额外增加了一个parent_domain_id字段用来串起版本链。比如PD-2024-001的价格域是PD-2024-000的复制版本那么前者的parent_domain_id就指向后者。版本链和domain_code的唯一性约束配合能在很大程度上避免“分不清是哪个版本在生效”。我踩过一个挺惨的坑有次运营需要紧急调价直接在生产库update了价格明细结果第二天对账时发现前一天下午所有订单价格全部使用了调价后的新值。原因很简单订单创建时没有把价格快照写入订单表查询时实时join价格域。从那以后我在订单明细表里强制增加了price_snapshot字段下单时直接把当时的报价、折扣、税率全部冗余进来。这个调整非常关键。4.3 价格审批流程怎么融入表结构价格域的状态流转需要审批。虽然审批动作本身是业务逻辑但表结构设计要为它留好扩展位。我的方案是复用通用的审批表approval_flow_instance然后在price_domain表里通过process_instance_id关联审批实例。这样做的好处是不用为价格单独建审批表审批流程变更比如增加区域总经理节点不影响价格域的数据结构。审批通过与驳回的回调接口里更新check_status同时触发消息通知给创建人。这一块建议用事件驱动的方式去写别在审批回调里同步更新价格因为价格生效后还要清缓存、通知下游ERP、更新客户门户展示价格这些异步做会稳很多。5. 基于表结构的查询、缓存与性能优化B端价格查询的链路往往很长从客户门户浏览商品列表到下单生成订单每个环节都要触发价格计算。如果不做优化数据库压力会非常大。5.1 索引优化联合索引是生命线上面的ERD就是核心索引设计我再额外强调一下price_domain_detail表的最关键查询模式先通过domain_id定位到某个价格域再查明细。所以uk_domain_skudomain_idsku_id这个唯一索引就是生命线。查询时必须先让数据库能够快速过滤价格域列表再进明细表。实际上价格域列表的过滤customer_id start_date status才是最大瓶颈。我给price_domain表设计了uk_customer_start_date和idx_group_start_date两个联合索引就是为了让“客户日期”能快速定位候选价格域。在实际项目中这条查询语句是系统最高频SQL之一索引设计不当会直接把数据库CPU打爆。5.2 缓存策略按客户维度做价格快照由于价格域的查询有明确的“客户日期SKU”维度非常适合做缓存。我推荐二级缓存策略第一级是进程内缓存如Caffeine适合单机高频查询TTL设置为5分钟命中率极高。第二级是Redis缓存key格式为price:domain:{customerId}:{date}:{skuId}TTL设置为1小时或价格域变更时主动失效。在价格域数据变更时发送一个消息到Redis进行key清理同时更新一个price_version:{customerId}:{date}的版本号查询时先校验版本号不一致则重新加载。这套方案成本不高但能把价格查询接口的RT从200ms降到个位数毫秒级。这里必须提醒缓存里的价格和数据库的价格不应该被视为不同来源。数据库是唯一可信数据源Redis只是性能加速器任何写入都必须先落入数据库再异步刷新缓存。因为一旦缓存先写而事务回滚就会出现缓存污染。5.3 数据归档历史价格域无限增长怎么办price_domain和price_domain_detail表的数据增长速度比想象中快得多。每次合同续签、每次调价都会生成新版本两年下来轻松几十万条记录。虽然这个量级对MySQL还不至于构成严重性能问题但查询会越来越慢管理和排查也会越来越吃力。我的方案是按年份对价格域表做归档。当前年份最近一年的数据放在主表更早的数据迁移到price_domain_2023、price_domain_2024这样的归档表。归档操作每个月执行一次通过定时任务把end_date小于当前年份且没有关联有效订单的价格域迁走。注意归档不能破坏订单关联。订单明细中的price_snapshot和price_domain_id需要保留所以归档时订单表不能做物理删除只能做历史表的引用同步。归档前先跑一遍对账脚本确保没有未结算订单关联到归档数据这一点需要格外小心。6. 常见问题速查与避坑指南价格域表结构上的问题往往不是看代码能发现的都是业务跑起来后一个个暴露出来的。我按真实遇到过的频率整理了一张速查表。问题现象根因解决方案同一个客户在不同入口看到的价格不一样客户门户和下单后台走了两套价格查询逻辑统一封装PriceEngine所有入口强制走同一服务历史订单价格对不上账订单没有存价格快照实时join价格表订单明细表增加price_snapshot字段下单时冻结价格新价格生效了但客户门户没变只改了数据库没有通知缓存清理价格变更走事件发布订阅端主动清缓存阶梯价区间重叠导致价格错乱运营手工录入时区间填写重叠增加区间重叠校验并限制同一SKU同一价格域只能由系统生成阶梯区间新客户没有价格下单直接报错没有创建价格域初始化客户时同步生成“默认总部价”价格域引用无价格时落日志并告警审批通过的价格不生效校验了status但没同步check_status审批回调里必须同时更新status和check_status下面几个是新手最容易忽视的坑单列出来再唠叨一遍。价格值和币种单位要分离。升鲜宝有些客户涉及跨境结算价格域表里如果只存数值不存currency后续汇率换算会成一笔糊涂账。我的建议是价格域明细表统一以人民币存储为基准同时保留quote_currency和quote_rate字段在订单生成时再做币种换算。税率字段不可省略。生鲜品类里蔬菜9%、肉类13%增值税率不同直接导致含税价和无税价差异。价格域明细表里的tax_rate必须由税务系统配置驱动不要在价格计算时硬编码税率否则季度报税时财务会找上门。客户协议价和“一口价”不要混用。有些业务喜欢在订单上直接改价改多了就会依赖“订单改价”而忽略协议价维护。正确的逻辑是订单改价必须走特批流程且记录改价原因正常的合同价、促销价都应在价格域中维护。这个规矩不立起来价格域最终会形同虚设。7. 实操经验总结与后续扩展方向最后说说我个人在实际操作中的体会。升鲜宝的价格域从最初“客户表加个价格字段”演进到现在这套模型中间经历了不少业务侧的考验。最核心的认知转变是价格不是一个“值”而是一个“规则集合”。只要这个认知到位后面的表结构设计都会很顺。具体到设计技巧上我还想分享一个小建议在设计价格域时一定要和财务、运营一起过一遍“对账场景”。财务关注的是订单价格和历史价格的追溯运营关注的是调价效率和批量处理两者诉求不同对表结构的理解也会不同。提前让他们参与评审远比上线后提需求再改表结构要省力得多。这个模块后续还可以往两个方向扩展。一是引入价格预测和智能定价基于历史成交价、采购成本、市场竞争数据自动生成建议价格再走人工审批流程二是把价格域和合同管理系统打通合同里的账期、预付比例等条款直接联动到价格计算逻辑中。这两块都需要以当前的价格域模型为基础所以建议在搭建前期就把扩展位预留好比如在price_domain表里加上contract_id、settlement_type字段避免后续再大改。希望这套价格域表结构设计能给正在做B端供应链系统的朋友一些参考。具体字段名和分表方案可以根据自己项目调整但“价格是规则集合、价格域要独立建模、价格必须带版本和快照”这几个原则放到什么系统里都成立。
返回列表