跳转至

报价引擎技术方案(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派单候选池联动。