报价引擎技术方案(TND-QSVE)¶
文档编号:DOC-D02 / 简称:TND-QSVE 版本:V1.0 创建日期:2026-08-22 维护人:TL / DT(听写编写,听码WDE消费) 决策审批人:TL + SPO 关联文档:D01 TND-ARC ADR-013(规则引擎渐进策略)/ NFR P95<200ms、D10 TND-MDS §四QSV供给API、QMD V2.0(8大参数32因子业务基线)、QSV-D V1.2(DDD PricingEngine聚合根+不变量)
业务性能硬约束:QSV报价P95<200ms(D01 §十NFR铁律),单次报价HTTP调用必须在200ms内完成,含MDS供给API请求+PricingEngine计算+DB版本快照写回。
一、技术定位与8大参数总览¶
1.1 定位¶
TND-QSVE(DOC-D02)落地QSV报价服务的核心计算逻辑:PricingEngine聚合根封装QMD V2.0定义的8大类参数、32项因子加权公式、封顶价机制、品牌溢价(1.2/1.4)、加价险档位、报价版本快照与CPT L8反哺校准接口。为 CPT L3(报价请求)/ MFR开放报价(生产商门户)/ CST客户端自助询价 三类调用方提供统一计算能力。
1.2 8大参数与32因子业务总览(对齐QMD V2.0 §三→§十)¶
| 参数编号 | 参数类 | 简称 | MDS供给来源 | 因子数量 | 影响方向(±) |
|---|---|---|---|---|---|
| P1 | 基础人工费(冷启动主参数) | BASE | D10 SCL:mds_scl_category.base_fee × 预计工时 | 5(8品类费率×32细目) | + |
| P2 | 产品参数调整 | PROD | D10 SCL:mds_scl_param_mapping表(尺寸/重量/材质/开孔等8维度→工时难度系数) | 8 | + |
| P3 | 楼宇系数 | BLDG | D10 BPL:BuildingCoefficient VO(类型系数×电梯×楼层附加×管理罚金) | 5 | ± |
| P4 | 区域系数 | AREA | 硬编码枚举(港岛×1.15/九龙×1.05/新界×1.0/离岛×1.25),热参数进mds_params_config表可OPR配置 | 4 | + |
| P5 | 运输成本 | TRANS | DME三模式运输模型(§四核心):M1自营车/ M2个体独立司机/ M3专业物流公司;×运输距离×重量×体积 | 6 | + |
| P6 | 安装时间附加 | TIME | 周末×1.15 / 夜间18:00-22:00×1.25 / 法定假日×1.5 / 紧急2h内×2.0 | 4 | + |
| P7 | 折扣与优惠 | DISC | 历史客户×0.95 / 推荐客户×0.93 / OPS券×[优惠券值] / MFR批量≥5×0.9 | 4 | - |
| P8 | 师傅等级系数与品牌溢价 | WRKR_PREMIUM | WPL等级(GOLD×1.2/AUTH×1.0/NORM×0.9)+ PPL品牌等级(普通1.0/高端1.2/超高端1.4,D10 PPL表brand_multiplier) | 2 | + |
合计:32因子(5+8+5+4+6+4+4+2)
二、BAS V1.0 DDD 8要素战术落地¶
2.1 代码边界(模块化单体QSV模块,D01 P1-P5铁律)¶
backend/
qsv/
interfaces/
api/
qsv_quote_router.py # REST:/api/qsv/*(25路由:询价/重算/版本快照/校准历史)
mfr_open_quote_bridge_router.py # MFR开放报价→QSV API桥(D05 §二MFR门户对接)
webhooks/
application/
services/
QuoteApplicationService.py # 用例编排:校验请求→调MDS供给→创建PricingEngine→calc→写快照→NATS发布
RecalibrationAppService.py # 校准反哺:订阅NATS mds.calibration.applied → 缓存失效
domain/ # 纯Python,零框架依赖(D01 P1铁律)
pricing_engine.py # 聚合根 PricingEngine(4不变量 + calc()主算法9步)
entities/
quote.py # Quote实体(quote_id + version双主键,快照不可变)
quote_snapshot.py # QuoteSnapshot 版本快照
value_objects/
quote_params_32.py # 32因子VO(P1~P8分组封装,不可变NamedTuple)
quote_result.py # QuoteResult VO(给CPT的返回:quote_id/version/final_amount)
dme_transport_mode.py # DME运输模型VO:M1/M2/M3参数载荷
services/
rule_engine_port.py # ADR-013 规则引擎抽象端口(JSON-Rules<→Drools无缝切换)
cap_price_policy.py # 封顶价策略(CapPricePolicy 领域服务)
brand_premium_policy.py # 品牌溢价策略(PPL 1.0/1.2/1.4 + 客户选择高端档位)
ports.py # 仓储端口(ABC):QuoteRepository / QuoteSnapshotRepository
events.py # NATS领域事件:qsv.quote.created / qsv.quote.recalibrated(D01五#1-2)
infrastructure/
db/
models.py # SQLAlchemy:qsv_quotes主表 + qsv_snapshots版本表
repositories.py # 实现2仓储端口
migrations/0004_qsv_schema.py # Alembic增量(向后兼容V3.3,纯新建无ALTER)
rules/
json_rules_engine_impl.py # ADR-013阶段一:轻量 durable_rules 库实现RuleEnginePort
# drools_engine_impl.py # 成熟期启用(规则≥200条时切Drools KIE Server)
cache/
qsv_param_cache.py # MDS供给参数缓存:30s TTL + mds.calibration事件失效
nats/
qsv_event_publisher.py
mds_calibration_subscriber.py
2.2 聚合根 PricingEngine 4不变量(强制代码校验)¶
# backend/qsv/domain/pricing_engine.py(纯Python,零FastAPI/SQLAlchemy依赖)
from __future__ import annotations
from dataclasses import dataclass
from typing import Final
from .value_objects import QuoteParams32, QuoteResult
from .services import RuleEnginePort, CapPricePolicy
@dataclass(frozen=True) # 聚合根不可变,calc()产生新的QuoteResult而非修改自身
class PricingEngine:
"""QSV报价核心聚合根。所有报价计算必须通过本类执行。"""
engine_version: Final[str] = "QSVE-V1.0"
rule_engine: RuleEnginePort # 依赖倒置:依赖抽象端口而非具体JSON-Rules/Drools实现
cap_policy: CapPricePolicy # 封顶价策略
HKD: Final[str] = "HKD"
# ========== 4不变量 ==========
def invariant_1_subtotal_positive(self, subtotal_hkd: float) -> None:
"""I1: subtotal必须>0(不可能出现0或负数报价)"""
if subtotal_hkd <= 0:
raise ValueError(f"QSVE-I1 违反: subtotal={subtotal_hkd}必须>0")
def invariant_2_cap_price_monotonic(self, after_cap: float, before_cap: float, cap_value: float) -> None:
"""I2: 封顶价单调递减或不变:after_cap <= before_cap;after_cap <= cap_value"""
if not (after_cap <= before_cap and after_cap <= cap_value):
raise ValueError(f"QSVE-I2 违反: after={after_cap} <= before={before_cap}且<={cap_value}")
def invariant_3_snapshot_immutable(self, quote_snapshot) -> None:
"""I3: 版本快照字段不可修改;一旦落库不可UPDATE,只能INSERT新版本"""
if quote_snapshot.persisted and quote_snapshot.is_dirty:
raise ValueError("QSVE-I3 违反: 已落库快照被非法修改,必须INSERT新版本")
def invariant_4_brand_premium_range(self, premium_applied: float) -> None:
"""I4: 品牌溢价系数范围严格∈{1.0, 1.2, 1.4};不可出现非法值如1.1"""
if premium_applied not in (1.0, 1.2, 1.4):
raise ValueError(f"QSVE-I4 违反: 溢价={premium_applied},必须∈(1.0,1.2,1.4)")
# ========== 核心计算9步(对齐QSV-D PricingEngine伪代码)==========
def calculate(self, params: QuoteParams32) -> QuoteResult:
# Step 1: BASE 人工费 = SCL费率(HKD/h) × 预计工时(h)
step1_base = params.P1.hourly_rate * params.P1.estimated_hours
self.invariant_1_subtotal_positive(step1_base)
# Step 2: 产品参数8维度调整(规则引擎1:PARAM-*规则集)
step2_prod_adj = self.rule_engine.evaluate_ruleset("PARAM_ADJUSTMENT", params=params.P2)
subtotal_after_p2 = step1_base * (1 + step2_prod_adj.diff_coefficient)
# Step 3: BPL楼宇5系数叠加
step3_bldg = (params.P3.building_type_coef
* params.P3.elevator_coef
* params.P3.floor_multiplier
* params.P3.management_penalty_coef
* params.P3.special_elevator_coef)
subtotal_after_p3 = subtotal_after_p2 * step3_bldg
# Step 4: P4区域系数(港岛1.15/九龙1.05/新界1.0/离岛1.25)
subtotal_after_p4 = subtotal_after_p3 * params.P4.area_coef
# Step 5: P5 运输成本(DME三模式运输模型 §四详细展开)
step5_transport = self._calc_transport_cost(params.P5, dme_mode=params.P5.preferred_mode)
# Step 6: P6时间附加(周末/夜间/假日/紧急)
subtotal_after_p6 = (subtotal_after_p4 + step5_transport) * params.P6.time_multiplier
# Step 7: P8 师傅等级×品牌溢价 乘法叠加
premium_applied = params.P8.worker_level_coef * params.P8.brand_multiplier
self.invariant_4_brand_premium_range(params.P8.brand_multiplier)
subtotal_after_p8 = subtotal_after_p6 * premium_applied
# Step 8: P7 折扣优惠(减法,最后做减法)
discount_total = (params.P7.historical_customer_discount
+ params.P7.referral_discount
+ params.P7.coupon_amount) # 折扣封顶=25%
discount_total = min(discount_total, subtotal_after_p8 * 0.25)
subtotal_after_p7 = subtotal_after_p8 - discount_total
# Step 9: 封顶价(CapPricePolicy 执行 → I2校验)
cap_price = self.cap_policy.calculate_cap(params=params, amount_before_cap=subtotal_after_p7)
final_amount = min(subtotal_after_p7, cap_price)
self.invariant_2_cap_price_monotonic(after_cap=final_amount, before_cap=subtotal_after_p7, cap_value=cap_price)
# C7加价险附加(可选,CPT-L3客户是否勾选)
if params.P0.include_c7_insurance:
c7_fee = self._calc_c7_insurance(params, base=step1_base, final=final_amount)
final_amount += c7_fee
# 四舍五入到0.5 HKD(香港小额现金惯例)
final_amount = round(final_amount * 2) / 2
return QuoteResult(
subtotal_steps={
"P1_BASE": step1_base, "P2_PROD": subtotal_after_p2,
"P3_BLDG": subtotal_after_p3, "P4_AREA": subtotal_after_p4,
"P5_TRANS": step5_transport, "P6_TIME": subtotal_after_p6,
"P8_WRKR_PREMIUM": subtotal_after_p8, "P7_DISC": subtotal_after_p7,
"CAP": cap_price, "FINAL_BEFORE_ROUND": final_amount
},
final_amount=final_amount,
currency=self.HKD,
engine_version=self.engine_version,
calculation_timestamp=None, # AppService层写回
)
三、规则引擎选型与渐进策略(ADR-013落地)¶
3.1 双引擎抽象端口(依赖倒置,无缝切换)¶
# backend/qsv/domain/services/rule_engine_port.py(ABC抽象)
from abc import ABC, abstractmethod
from typing import Any
from dataclasses import dataclass
@dataclass
class RuleEvaluationResult:
matched_rules: int
diff_coefficient: float # 加/减系数(如+0.3表示加价30%)
applied_reasons: list[str] # 命中规则名,供OPR报价明细展示(为什么这么贵?)
class RuleEnginePort(ABC):
@abstractmethod
def evaluate_ruleset(self, ruleset_id: str, *, params: Any) -> RuleEvaluationResult: ...
# ruleset_id = "PARAM_ADJUSTMENT" / "CAP_PRICE_POLICY" / "BRAND_PREMIUM" / ...
3.2 JSON-Rules实现(筹备-试运营期·默认启用,规则<200条)¶
Python生态使用
durable_rules或 自研极简JSON规则执行器(避免JVM依赖)。规则文件存储在backend/qsv/infrastructure/rules/*.json,热加载无需重启。
规则示例(PARAM_ADJUSTMENT:电视尺寸98时难度+100%):
{
"ruleset_id": "PARAM_ADJUSTMENT",
"rules": [
{
"id": "PARAM_TV_SIZE_98",
"when": "params.product_code == 'APPL-02' && params.tv_size_inch >= 98",
"then": {
"diff_coefficient_add": 1.0,
"reason_cn": "98吋及以上超大尺寸:需3人+专业挂架,工时翻倍"
},
"priority": 100
},
{
"id": "PARAM_AC_HIGH_ALTITUDE",
"when": "params.product_code == 'APPL-01' && params.floor_without_elev >= 8",
"then": {
"diff_coefficient_add": 0.5,
"reason_cn": "空调8楼及以上无电梯:高空作业费+50%"
}
}
]
}
3.3 Drools KIE Server实现(成熟期·规则≥200条时切换)¶
切换只需要新增 drools_engine_impl.py 实现 RuleEnginePort,在DI容器中替换注入;PricingEngine聚合根零改动(ADR-013原则)。Drools优势:DRL语言专业决策表支持+KIE工作台可视化规则管理+BO无需改代码即可调参数。
3.4 规则引擎性能对比(选型依据)¶
| 维度 | JSON-Rules(默认) | Drools(成熟期切换) |
|---|---|---|
| NFR P95<200ms | ✅ 规则<200条时P50<5ms,P99<20ms | ✅ KIE Server会话复用,P99<30ms |
| JVM依赖 | ❌ 纯Python无JVM,部署简单 | ✅ 需要JVM+KIE Server部署(运维复杂度增加) |
| 可视化规则编辑 | ❌ 需OPR开发规则管理页 | ✅ KIE工作台开箱即用,BO可拖拽编辑决策表 |
| 复杂度上限 | ~200条(线性遍历性能退化) | 上万条(Rete/Phreak算法) |
四、DME运输模型三模式(M1自营/M2独立/M3专业跨境)¶
对齐DIS-D V1.1 §6.1 运输三模式 × 安装三模式=9组合矩阵;DME计算运输成本后写回CPT L5 DIS派单首选模式候选池。
flowchart TD
P5_INPUT["P5运输输入:pickup_addr(取货点)/delivery_addr(送货点)/weight_kg/volume_m3/insurance_c7_flag/preferred_mode"]
ROUTER{"首选模式<br/>客户/系统指定?"}
ROUTER -->|"M1自营车"| CALC_M1["CALC_M1(mode=M1)"]
ROUTER -->|"M2独立司机"| CALC_M2["CALC_M2(mode=M2)"]
ROUTER -->|"M3专业跨境"| CALC_M3["CALC_M3(mode=M3)"]
ROUTER -->|"系统自动(AUTO默认)"| COMPARE["三模式全部计算→按性价比排序→取TOP1(客户可切换)"]
CALC_M1 --> OUT["DMEOutput{mode, cost_hkd, distance_km, eta_hours, capacity_left}"]
CALC_M2 --> OUT
CALC_M3 --> OUT
COMPARE --> OUT
OUT --> DIS_INTEGRATION["写入CPT L5→DIS派单候选池<br/>(DIS D04 §5.1 三模式候选池)"]
OUT --> QSVE_RETURN["叠加到报价P5项(§二calc Step5)"]
4.1 三模式计算公式¶
| 模式 | 基础费(HKD) | 里程费(HKD/km) | 重量附加 | 体积附加 | 跨境HS编码费 | 典型适用场景 |
|---|---|---|---|---|---|---|
| M1自营车(织布鸟自有) | 150(起步含首10km) | 6/km | 0.5/kg 超100kg | 20/m³ 超2m³ | — | 标准客单(默认首选,性价比最高) |
| M2独立司机(本地注册) | 120(起步含首8km) | 5.5/km | 0.6/kg超80kg | 25/m³超1.5m³ | — | 高峰期弹性补充、离岛单 |
| M3专业物流(跨境公司) | 500(起步含首50km+口岸清关) | 10/km(跨境段) | 1.0/kg 超200kg | 50/m³ | 150×HS编码申报费(可叠加) | MFR生产商批量货、中港跨境(≥C7保险勾选必选M3) |
# 伪代码(D09 §5.2 calc()中Step5实际调用)
def _calc_transport_cost(self, p5, dme_mode):
from math import max
km = geo_distance_km(p5.pickup_addr.gps, p5.delivery_addr.gps)
if dme_mode == 'M1':
base = 150 if km <=10 else 150 + (km-10)*6
weight = 0 if p5.weight_kg <=100 else (p5.weight_kg-100)*0.5
volume = 0 if p5.volume_m3 <=2 else (p5.volume_m3-2)*20
return round(base + weight + volume, 2)
elif dme_mode == 'M2': ... # 同理
elif dme_mode == 'M3': ... # 含跨境HS编码申报费150×n
五、报价版本快照与封顶价机制¶
5.1 报价快照不可变原则(I3)¶
每次 calculate() → INSERT 新行到 qsv_snapshots,quote_id + version = 复合主键。禁止任何UPDATE:客户修改参数(如「换品牌」「换师傅等级」「加购C7」)→ 系统生成新版本version+1。CPT L3阶段绑定具体version;L4签约时version不可变更(防止「坐地起价」投诉)。
5.2 封顶价策略(CapPricePolicy)¶
# 封顶价算法:分档硬上限 + 品类软上限(BASE×max_ratio)
class CapPricePolicy:
def calculate_cap(self, params, amount_before_cap: float) -> float:
# 硬上限:任何订单封顶(HONG KONG市场):50000 HKD
HARD_CAP = 50_000.0
# 软上限:按SCL品类BASE费最大值×8
scl_soft_cap = params.P1.max_possible_base_fee * 8.0
# 客户选择「简装版服务」时×0.7、「高端版服务」时×1.5
tier_coef = {
"STANDARD": 1.0, "SIMPLE": 0.7,
"PREMIUM_1_2": 1.2, # 高端客户PPL溢价档与封顶联动
"PREMIUM_1_4": 1.5, # 超高端1.4档:封顶同步放宽到1.5倍
}.get(params.P0.service_tier, 1.0)
return min(HARD_CAP, scl_soft_cap * tier_coef)
六、校准接口(CPT L8反哺 → MDS→QSV闭环)¶
对齐D10 §五校准引擎通道三(SCL费率校准)、通道一(WPL等级):
| 事件流 | 触发方 | QSV动作 |
|---|---|---|
NATS mds.calibration.applied(D01五#10) |
MDS校准引擎3通道写完成后 | QSV RecalibrationAppService订阅 → Redis DEL mds:qsv:params:* → 下一次询价拉新参数 |
NATS mds.wpl.downgraded(D01五扩展) |
WPL师傅月度评分跨阈值 | 缓存失效 + 重新计算师傅等级系数 |
HTTP POST /api/qsv/calibrate/suggest-rate(BO/SCM调用) |
人工校验SCL费率偏差>10%草稿 | 先写mds_params_config配置表新草稿值 → 发布 mds.calibration.pending事件→审批→应用→生成 qsv.quote.recalibrated事件 |
七、PostgreSQL表DDL(Alembic 0004 新增,无ALTER V3.3表)¶
-- qsv_quotes主表:绑定CPT project_id
CREATE TABLE IF NOT EXISTS qsv_quotes (
quote_id VARCHAR(32) PRIMARY KEY, -- QSV-YYYYMMDD-NNNN
project_id_fk VARCHAR(32) NULL, -- CPT project_id引用(L3询价时为空,L4签约后回填)
latest_version INT NOT NULL DEFAULT 1,
latest_final_amount NUMERIC(12,2) NOT NULL,
currency CHAR(3) NOT NULL DEFAULT 'HKD',
include_c7_ins BOOLEAN NOT NULL DEFAULT FALSE,
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
);
-- qsv_snapshots版本快照:quote_id + version唯一;一旦INSERT,禁止UPDATE(I3)
CREATE TABLE IF NOT EXISTS qsv_snapshots (
id BIGSERIAL PRIMARY KEY,
quote_id_fk VARCHAR(32) NOT NULL REFERENCES qsv_quotes(quote_id),
version INT NOT NULL,
params_json JSONB NOT NULL, -- 32因子完整快照(不可变)
step_results_json JSONB NOT NULL, -- calc() 9步明细P1~P9分解
final_amount NUMERIC(12,2) NOT NULL,
cap_price_applied NUMERIC(12,2),
dme_mode_selected VARCHAR(8) NOT NULL CHECK (dme_mode_selected IN ('M1','M2','M3')),
engine_version VARCHAR(16) NOT NULL, -- QSVE-V1.0 用于未来回归
rule_engine_hint VARCHAR(16) NOT NULL DEFAULT 'JSON-RULES', -- 便于未来切换Drools时对比
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
UNIQUE (quote_id_fk, version)
);
CREATE INDEX idx_qsnap_project_time ON qsv_quotes(created_at DESC);
CREATE INDEX idx_qsnap_amount ON qsv_snapshots(final_amount);
八、性能(NFR P95<200ms)保障机制¶
| 步骤 | 耗时预算 | 优化手段 |
|---|---|---|
| HTTP请求→参数Pydantic校验 | ≤5ms | Pydantic v2 Rust核心 |
| MDS供给API调用 | ≤80ms(90%预算) | D10 §八缓存:同scl/bpl/wpl组合30s命中;MDS供给接口P95<50ms + 内网RTT≤30ms |
| PricingEngine.calc()(32因子9步 + 规则引擎) | ≤40ms | 纯CPU计算,Py3.12+优化;规则引擎命中Rete缓存 |
| DB INSERT快照 | ≤30ms | 异步INSERT(Celery/NATS任务队列),同步路径先读缓存写临时表;或同步INSERT但Alembic建表时FILLFACTOR=90优化写入 |
| 返回HTTP响应序列化 | ≤10ms | Pydantic model_validate_json |
| 合计 | ≤165ms → P95<200ms | 缓冲安全余量35ms |
九、与Dxx关联¶
graph TD
D02["D02 TND-QSVE(本文件)"]
D01["D01(ADR-013规则渐进 / P95<200ms)"]
D10["D10 MDS(P1~P4 / P8主参数供给API)"]
D04["D04 DIS(§四DME三模式运输写回派单候选池 + C7保险档位联动)"]
D09["D09 CPT(L3询价→QSV调用 / L4签约锁定version / L8成交价反哺校准)"]
D05["D05 B2B(MFR门户开放报价桥接API)"]
API["OpenAPI /api/qsv/* 25路由:询价/重算/快照/校准"]
D01 --> D02
D10 --> D02
D02 --> D04 & D09 & D05 & API
修订记录¶
| 版本 | 日期 | 修订人 | 修订内容 |
|---|---|---|---|
| V1.0 | 2026-08-22 | DT | 首版:①8大参数32因子总览表;②BAS DDD 8要素落地代码边界+PricingEngine聚合根4不变量+calc()9步完整Python伪代码;③规则引擎双端口ADR-013 JSON-Rules(默认)/Drools(成熟期)切换样例;④DME运输三模式M1/M2/M3计算明细矩阵+流程图;⑤报价快照不可变I3+封顶价策略代码;⑥校准NATS事件流接口;⑦Alembic 0004新增qsv表DDL(无ALTER V3.3);⑧P95<200ms预算分解表;⑨Dxx关联图 |
本文件为 D02(TND-QSVE V1.0 报价引擎技术方案),32因子计算封装在纯Python PricingEngine聚合根,D01 P1-P5原则保障;规则引擎按ADR-013在JSON-Rules/Drools之间无缝演进;NFR P95<200ms通过80msMDS缓存+40ms计算+30ms异步写快照路径达成;运输成本DME三模式与DIS派单候选池联动。