跳转至

MDS 主数据服务技术方案(TND-MDS)

文档编号:DOC-D10 / 简称:TND-MDS 版本:V1.0 创建日期:2026-08-22 维护人:TL / DT(听写编写,听码WDE消费;听云OCM评审§七PDPO合规加密) 决策审批人:TL + SPO 关联文档D01 TND-ARC ADR-015(四库→六库升级)/ ADR-016(PDPO加密)、MDS-D V1.3(业务设计基线)、SCL V1.0BPL V1.0WPL V1.2CPL V1.1PPL V1.1DLV V1.1TSMM V1.0

听云OCM加签项(必须评审):§七 AES-256-GCM列级加密 + §3.1 Keycloak RLS行级权限策略表


一、技术定位与兼容保证

1.1 定位

D10(TND-MDS)是MDS主数据服务的可编码级权威技术设计文档,落地MDS-D V1.3业务设计定义的六库(SCL/BPL/WPL/CPL/PPL/DLV)、8领域事件、5领域服务、6仓储端口。为 QSV(D02 QSVE)/ DIS(D04 DIS)/ CPT(D09 CPT)/ OPS(D05 B2B)四业务服务提供标准主数据供给(OHS)API。

1.2 向后兼容DIP1 V3.3.0保证(D01 §十一 铁律)

兼容维度 V3.3现状 D10设计 兼容方式
四库(SCL/BPL/WPL/CPL) 4张主表 + 8关联表 = 12张(Alembic 0001) 12张表结构零改动,字段名/类型/索引完全保留 Alembic 0003为纯增量:仅CREATE TABLE新增PPL/DLV 2主表+4关联表,无ALTER原列
原DSP派单(已升级为DIS) DSP无独立主数据表 DIS派单数据(D04 §三)独立于MDS六库,MDS仅新增DLV供给DIS 无影响
mds_params_config配置表 存在(P4配置外置原则) 新增catalog_type枚举值(原4种→6种:新增PPL/DLV) 枚举增量ALTER TYPE ADD VALUE(PG支持,非破坏性)
MDS供给接口路径 /api/mds/{scl,bpl,wpl,cpl}/* 新增2路径/api/mds/{ppl,dlv}/*,原4路径0改动 OpenAPI契约向后兼容

二、BAS V1.0 DDD 8要素战术落地(对齐D01 §六)

本章节为MDS服务DDD战术实现的唯一编码规范,听码WDE实现时必须严格遵循。

2.1 子域定位

子域 类型 实现方式
主数据供给 核心 backend/mds/domain/catalog/ 模块;聚合根 MasterDataCatalog 全局单例
标签体系管理 支撑 backend/mds/domain/label/ 模块;SK共享TSMM标签规则,LabelAggregate封装不变量

2.2 限界上下文(2BC)代码边界

backend/
  mds/                              # MDS模块(P3模块化单体,独立目录边界)
    interfaces/
      api/                          # OHS开放主机服务REST API(供给QSV/CPT/DIS/OPS)
        mds_supply_router.py        # 所有对外供给接口(仅返回ACTIVE状态+脱敏)
      webhooks/
    application/
      services/
        SixLibraryAppService.py     # 六库CRUD编排(调domain层服务)
        CalibrationAppService.py    # 校准引擎编排(订阅NATS cpt.actual.calibration事件)
        LabelAppService.py          # 标签CRUD+版本递增
    domain/                         # 纯Python,零框架依赖(P1铁律)
      catalog/                      # MasterData BC
        master_data_catalog.py      # 聚合根 MasterDataCatalog(全局单例,4不变量)
        entities/                   # 6实体:ServiceCategory/BuildingProfile/WorkerProfile/
                                     #           CustomerProfile/ProducerProfile/LogisticsProfile
        value_objects/              # 8 VO(MasterDataId/Label/PrivacyLevel/CatalogRateFactor/
                                     #        BuildingCoefficient/SkillMatrix/TrustBadge/LogisticsMode)
        services/                   # 5领域服务:SixLibraryCRUD/LabelManagement/MasterDataId/
                                     #           PrivacyDesensitization/SupplyAPIService
        ports.py                    # 6仓储端口(ABC抽象基类)
        events.py                   # 8领域事件(MDS-D §3.5对齐,D01 §五#10-11)
      label/                        # LabelManagement BC
        aggregate.py
        tsmm_compat.py              # SK共享TSMM合规校验
    infrastructure/
      db/
        models.py                   # SQLAlchemy ORM模型(12张原表+6张新增=18张+2枚举表)
        repositories.py             # 实现6仓储端口(P2依赖倒置)
        migrations/                 # Alembic版本(0003增量六库扩展)
      nats/
        mds_event_publisher.py      # 发布8领域事件到NATS
        cpt_calibration_subscriber.py  # 订阅CPT反哺校准事件(NATS主题#9)
      crypto/
        aead_encryption.py          # AES-256-GCM加解密(密钥来自阿里云KMS,不进Git)
        rls_policies.sql            # PostgreSQL行级权限策略初始化脚本
      cache/
        mds_cache_decorator.py      # Redis缓存装饰器(供给接口P95<500ms NFR保证)

2.3 上下文映射实现

对端服务 映射模式 实现位置
QSV OHS上游 mds_supply_router.py GET /api/mds/supply/qsv/factors?category_codes=[] 返回CatalogRateFactor VO
CPT OHS(ID供给)+ ACS(反哺消费) OHS: GET /api/mds/supply/cpt/entity-refs;ACS: 订阅NATS cpt.actual.calibration 主题(#9)
DIS OHS + ACL OHS: GET /api/mds/supply/dis/dlv-candidates;ACL: domain/catalog/services/supply_api_service.py 中 LogisticsProviderEntity→LogisticsMode VO(裁剪bank_account/tax_id)
OPS OHS GET /api/mds/supply/ops/marketing-profiles 返回MarketingCustomer VO(经PrivacyDesensitizationService脱敏至L1/L2)
TSMM SK共享内核 label/tsmm_compat.py 对齐TSMM标签三层分类+隐私规则

2.4 聚合根 MasterDataCatalog 4不变量代码校验

# backend/mds/domain/catalog/master_data_catalog.py(纯Python,零依赖)
from __future__ import annotations
from typing import Literal, Final
from .value_objects import MasterDataId, Label

LibraryCode = Literal["SCL","BPL","WPL","CPL","PPL","DLV"]

class MasterDataCatalog:
    """MDS聚合根,全局单例。所有六库操作必须通过该聚合根校验不变量。"""

    _instance: Final["MasterDataCatalog | None"] = None

    def __new__(cls):
        if cls._instance is None:
            cls._instance = super().__new__(cls)
        return cls._instance

    # ========== 不变量 Invariants(MDS-D §3.1 1:1对齐)==========

    def invariant_1_unique_business_key(
        self, library_code: LibraryCode, business_key: str, existing_keys: set[str]
    ) -> None:
        """I1: (libraryCode, businessKey)组合全局唯一"""
        composite = f"{library_code}::{business_key}"
        if composite in existing_keys:
            raise ValueError(f"MDS-I1 违反: 业务键重复 {composite}")

    def invariant_2_id_format(self, master_data_id: MasterDataId) -> None:
        """I2: ID格式严格 {lib}-{yyyyMMdd}-{NNNN}"""
        pattern = r"^(SCL|BPL|WPL|CPL|PPL|DLV)-\d{8}-\d{4}$"
        import re
        if not re.match(pattern, master_data_id.serialize()):
            raise ValueError(f"MDS-I2 违反: ID格式非法 {master_data_id.serialize()}")

    def invariant_3_label_version_monotonic(
        self, old_version: int, new_version: int, delta_labels: list[Label]
    ) -> None:
        """I3: labelVersion必须单调+1,禁止回退;delta_labels非空"""
        if delta_labels is None or len(delta_labels) == 0:
            raise ValueError("MDS-I3 违反: 标签变更delta为空")
        if new_version != old_version + 1:
            raise ValueError(f"MDS-I3 违反: 标签版本非单调 {old_version}{new_version},必须+1")

    def invariant_4_privacy_desensitized(
        self, data: dict, consumer_service: str, privacy_service: "PrivacyDesensitizationService"
    ) -> dict:
        """I4: 跨服务返回L3/L4字段必须脱敏(禁止反向解密)"""
        return privacy_service.apply(data=data, consumer=consumer_service)

2.5 8领域事件NATS对齐(D01 §五#10-11 + MDS-D §3.5)

MDS领域事件 NATS Topic(D01 §五#编号) 发布时机 Publisher位置
MasterDataCreated mds.entity.created(D01五扩展,补充到STREAM_MDS) SixLibraryCRUDService创建后 nats/mds_event_publisher.py
MasterDataUpdated mds.entity.updated(同上) 核心字段变更 同上
MasterDataDeleted mds.entity.deleted(同上) 逻辑删除(合规场景) 同上
MasterDataLabelChanged mds.label.changed(同上) labelVersion递增 同上
BuildingCoefficientExpired mds.bpl.coefficient_expired(同上) BPL季度未核实+偏差超阈值 同上
WorkerDowngraded mds.wpl.downgraded(同上) WPL月度跨降级阈值+审批 同上
CustomerConflictRaised mds.cpl.conflict_detected(同上) 手机号/邮箱重复疑似冲突 同上
MasterDataSuppliedToService mds.audit.supplied(同上) SupplyAPIService被调用(审计) 同上
MDS校准完成 → 消费方通知 mds.calibration.applied(D01五#10) 校准引擎3通道写入完成后 同上 → QSV/DIS/OPS订阅
批量入档完成 mds.entity.imported(D01五#11) PPL/DLV批量入驻完成 同上

2.6 仓储端口(ABC抽象基类)与实现

# backend/mds/domain/catalog/ports.py(领域层定义端口,P2依赖倒置铁律)
from abc import ABC, abstractmethod
from typing import Iterable, Optional
from .entities import (
    ServiceCategory, BuildingProfile, WorkerProfile,
    CustomerProfile, ProducerProfile, LogisticsProfile,
)
from .value_objects import MasterDataId

class ServiceCategoryRepository(ABC):
    @abstractmethod
    def get_by_code(self, scl_code: str) -> Optional[ServiceCategory]: ...
    @abstractmethod
    def list_by_category_family(self, family_code: str) -> Iterable[ServiceCategory]: ...
    @abstractmethod
    def save(self, entity: ServiceCategory) -> MasterDataId: ...

class BuildingProfileRepository(ABC):
    @abstractmethod
    def get_by_gps(self, lat: float, lng: float, radius_km: float, session) -> Iterable[BuildingProfile]: ...
    # PostGIS ST_DWithin调用,由基础设施层实现
    ...

# 同理定义 WorkerProfileRepository / CustomerProfileRepository /
# ProducerProfileRepository / LogisticsProfileRepository(共6个端口)

2.7 ACL防腐层实现(MDS→CPT反哺 + DIS消费DLV)

# backend/mds/domain/catalog/services/privacy_desensitization_service.py
class PrivacyDesensitizationService:
    """MDS作为上游时,裁剪/脱敏下游接收字段;对应MDS-D §2.4 5个ACL VO"""

    # 字段分级 → 消费方可访问性矩阵(对应D07 TND-SEC §四 PDPO分级)
    ACCESS_MATRIX = {
        "QSV": {
            "SCL": ["L1","L2"], "WPL": ["WPL-101等级","WPL-102系数"],
            "BPL": ["L1","L2"], "PPL": ["品牌等级","溢价系数"],
        },  # QSV绝不访问WPL-002姓名/WPL-004手机号等L3+
        "DIS": {
            "WPL": ["技能矩阵/服务区域/状态(L1-L2)"], "DLV": ["模式+M3子表(裁剪银行账号)"],
        },
        "CPT": {"ALL": ["ID引用(6类实体主键)"]},
        "OPS": {"CPL": ["标签+徽章(L1-L2)"], "WPL": ["个人品牌资料"]},
    }

三、PostgreSQL数据模型(六库 · 兼容V3.3 + 增量扩展)

3.1 数据库总表清单(共26表 = V3.3 22表保留 + 4表新增PPL/DLV)

保证:原22表(含SCL/BPL/WPL/CPL主表+关联表+配置表+枚举表)字段零改动。Alembic migration 0003为纯CREATE增量+2枚举类型ADD VALUE扩展。

# 表名 所属库 是否新增(V3.3) 行数预估(筹备期1年) 说明
主表6
1 mds_scl_category SCL 已有(V3.3) 32(32细分类目固定+扩展字段) 安装服务品类主表,对齐SCL §六 SCL-001~SCL-015
2 mds_bpl_building BPL 已有(V3.3) + 新增PostGIS geom列 ~3000(香港楼宇数) 楼宇档案主表,对齐BPL 25字段+10扩展+PostGIS geometry
3 mds_wpl_worker WPL 已有(V3.3) + 新增REGISTER_TYPE(V1.2) ~500 师傅档案主表,对齐WPL §三 6组50+字段
4 mds_cpl_customer CPL 已有(V3.3) ~10,000 客户档案主表,对齐CPL五维标签
5 mds_ppl_producer PPL 新增(V3.3无) ~50 生产商档案主表(品牌溢价1.2/1.4系数)
6 mds_dlv_logistics DLV 新增(V3.3无) ~30 物流商档案主表(M1/M2/M3模式)
关联表12(V3.3已有,零改动)
7-10 mds_scl_*×4(参数映射/技能/工具/费率历史) SCL 已有 ~200 SCL §三/§四关联
11-12 mds_bpl_*×2(核实历史/楼宇照片) BPL 已有 ~5000 BPL L2现场核实
13-17 mds_wpl_*×5(技能明细/证书/排班/异常/等级历史) WPL 已有 + 新增mds_wpl_m3_binding(M3绑定PPL企业) ~5000 WPL §三各子节
18-19 mds_cpl_*×2(标签明细/推荐链) CPL 已有 ~30,000 TSMM标签体系
PPL/DLV关联表(新增)
20 mds_ppl_brand_contracts PPL 新增 ~100 PPL合作条款、质保条款、品牌溢价历史
21 mds_ppl_products PPL 新增 ~500 MFR开放报价:MFR生产商20产品映射SCL类目
22 mds_dlv_m3_contract_fields DLV 新增 ~50 DLV-D §M3合同子表(月度保底价/超载费/跨境8800投保)
23 mds_dlv_insurance_claims DLV 新增 ~2000 C7四档赔付记录,DIS派单负向权重
MDM治理表(新增)
24 mds_version_history 全局MDM 新增 ~50,000 6库所有变更版本快照(谁+何时+改了什么字段)
25 mds_approval_workflow 全局MDM 新增 ~1000 档案生命周期审批流(DRAFT→PENDING_REVIEW→ACTIVE)
26 mds_calibration_audit 校准引擎 新增 ~10,000 CPT反哺校准全量审计(校准前/后/审批链)
枚举表(V3.3已有,2个扩展)
E1 enum_library_code 全局 已有,新增枚举值PPL/DLV 6 ALTER TYPE ... ADD VALUE(PG非破坏性)
E2 enum_privacy_level 全局 已有 4(L1-L4) 零改动

3.2 核心表DDL(以PPL/DLV新增表 + WPL REGISTER_TYPE增量为例)

-- ========== Alembic 0003 migration:增量脚本 零ALTER原表 ==========

-- 1. enum_library_code 扩展(原SCL/BPL/WPL/CPL → +PPL/DLV)
ALTER TYPE enum_library_code ADD VALUE IF NOT EXISTS 'PPL';
ALTER TYPE enum_library_code ADD VALUE IF NOT EXISTS 'DLV';

-- 2. WPL新增REGISTER_TYPE(WPL V1.2 §2.4三分类)+ M3_SME企业ID绑定
ALTER TABLE mds_wpl_worker
ADD COLUMN IF NOT EXISTS register_type VARCHAR(16) NOT NULL DEFAULT 'M2_FREE'
    CHECK (register_type IN ('M1_SELF','M2_FREE','M3_SME'));
COMMENT ON COLUMN mds_wpl_worker.register_type IS 'WPL-006a DIS派单安装三分类:自营/个体/签约小B';
ALTER TABLE mds_wpl_worker
ADD COLUMN IF NOT EXISTS ppl_enterprise_id VARCHAR(32) NULL
    REFERENCES mds_ppl_producer(ppl_id) ON DELETE RESTRICT;
COMMENT ON COLUMN mds_wpl_worker.ppl_enterprise_id IS 'WPL-006b M3_SME师傅必须绑定所属PPL企业ID';
-- 约束:当且仅当register_type=M3_SME时,ppl_enterprise_id非空
ALTER TABLE mds_wpl_worker
ADD CONSTRAINT wpl_register_check
CHECK ( (register_type = 'M3_SME') = (ppl_enterprise_id IS NOT NULL) );

-- 3. 新增PPL生产商档案主表
CREATE TABLE IF NOT EXISTS mds_ppl_producer (
    ppl_id            VARCHAR(32) PRIMARY KEY,          -- PPL-YYYYMMDD-NNNN
    business_key      VARCHAR(64) NOT NULL UNIQUE,     -- I1:(PPL, brn商业登记号)唯一
    brand_name_cn     VARCHAR(128) NOT NULL,           -- 品牌中文名(展示用)
    brand_name_en     VARCHAR(128),
    brand_tier        VARCHAR(16) NOT NULL DEFAULT 'GENERAL'
        CHECK (brand_tier IN ('GENERAL','PREMIUM_1_2','PREMIUM_1_4')),
    brand_multiplier  NUMERIC(3,2) NOT NULL DEFAULT 1.0
        CHECK (brand_multiplier IN (1.0, 1.2, 1.4)),   -- QSV消费:普通1.0/高端1.2/高端1.4
    contact_person    VARCHAR(64),
    contact_phone     BYTEA NOT NULL,                  -- L3加密存储:手机号AES-256-GCM
    contact_email     BYTEA,                            -- L3加密
    brn_reg_no        VARCHAR(32) NOT NULL,             -- 香港商业登记号
    warranty_clause   JSONB NOT NULL DEFAULT '{}',      -- 质保条款(MFR开放报价场景)
    cooperation_start DATE NOT NULL,
    cooperation_end   DATE,
    status            VARCHAR(16) NOT NULL DEFAULT 'DRAFT'
        CHECK (status IN ('DRAFT','PENDING_REVIEW','ACTIVE','INACTIVE','ARCHIVED','DELETED')),
    label_version     INT NOT NULL DEFAULT 0,           -- I3标签单调递增
    created_at        TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
    updated_at        TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
    CONSTRAINT ppl_format CHECK (ppl_id ~ '^PPL-\d{8}-\d{4}$')  -- I2 ID格式
);
CREATE INDEX idx_ppl_brand ON mds_ppl_producer(brand_tier);
-- RLS行级权限(Keycloak MFR角色仅看自己所属PPL企业;MFR-ADM全局看)
ALTER TABLE mds_ppl_producer ENABLE ROW LEVEL SECURITY;
-- MFR普通用户:只能SELECT ppl_id = current_setting('app.current_ppl_id')的行
CREATE POLICY mfr_user_self_ppl ON mds_ppl_producer
    FOR SELECT USING (
        current_setting('app.current_role') = 'MFR-USR'
        AND ppl_id = current_setting('app.current_ppl_id', TRUE)
    );

-- 4. 新增DLV物流商档案主表
CREATE TABLE IF NOT EXISTS mds_dlv_logistics (
    dlv_id              VARCHAR(32) PRIMARY KEY,      -- DLV-YYYYMMDD-NNNN
    business_key        VARCHAR(64) NOT NULL UNIQUE,  -- I1:(DLV, carrier_code)唯一
    company_name_cn     VARCHAR(128) NOT NULL,
    company_name_en     VARCHAR(128),
    business_mode       VARCHAR(8) NOT NULL CHECK (business_mode IN ('M1','M2','M3')),
    coverage_districts  TEXT[] NOT NULL DEFAULT '{}', -- 服务区域(港岛/九龙/新界/离岛)
    c7_insurance_rate   NUMERIC(5,4) NOT NULL DEFAULT 0.015,  -- DIS消费:投保费率默认1.5%
    c7_total_claims     INT NOT NULL DEFAULT 0,       -- DIS消费:C7赔付负向权重
    c7_composite_score  NUMERIC(3,2) NOT NULL DEFAULT 5.00, -- 评分1-10,派单权重因子
    contact_phone       BYTEA NOT NULL,
    bank_account_enc    BYTEA NOT NULL,               -- L4加密:结算账户(仅财务角色可见)
    cross_border_hs     BOOLEAN NOT NULL DEFAULT FALSE,  -- 是否支持跨境HS编码报关
    status              VARCHAR(16) NOT NULL DEFAULT 'DRAFT' CHECK (status IN ('DRAFT','PENDING_REVIEW','ACTIVE','INACTIVE','ARCHIVED','DELETED')),
    label_version       INT NOT NULL DEFAULT 0,
    created_at          TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
    updated_at          TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
    CONSTRAINT dlv_format CHECK (dlv_id ~ '^DLV-\d{8}-\d{4}$')
);
CREATE INDEX idx_dlv_mode_score ON mds_dlv_logistics(business_mode, c7_composite_score DESC);
ALTER TABLE mds_dlv_logistics ENABLE ROW LEVEL SECURITY;
CREATE POLICY lgp_user_self_dlv ON mds_dlv_logistics
    FOR SELECT USING (
        current_setting('app.current_role') = 'LGP-USR'
        AND dlv_id = current_setting('app.current_dlv_id', TRUE)
    );

-- 5. DLV M3合同子表(DIS消费:M3_MODE_FIELDS子表结构)
CREATE TABLE IF NOT EXISTS mds_dlv_m3_contract_fields (
    id                  BIGSERIAL PRIMARY KEY,
    dlv_id              VARCHAR(32) NOT NULL REFERENCES mds_dlv_logistics(dlv_id),
    monthly_min_fee     NUMERIC(12,2) NOT NULL,       -- 月度保底费
    per_trip_fee        NUMERIC(10,2) NOT NULL,       -- 每趟运费
    overload_fee_per_kg NUMERIC(8,2),                 -- 超载费
    cross_border_8800   NUMERIC(10,2),                -- 跨境8800清关费
    contract_start      DATE NOT NULL,
    contract_end        DATE,
    UNIQUE (dlv_id)  -- 1 DLV : 1 M3合同(M3模式才填,DLV级ACL非M3模式下此表为0行)
);
COMMENT ON TABLE mds_dlv_m3_contract_fields IS 'DLV DIS M3合同字段子表;DIS派单SK共享内核';

-- 6. C7保险赔付记录
CREATE TABLE IF NOT EXISTS mds_dlv_insurance_claims (
    claim_id        VARCHAR(32) PRIMARY KEY,          -- INS-YYYYMMDD-NNNN
    dlv_id          VARCHAR(32) NOT NULL REFERENCES mds_dlv_logistics(dlv_id),
    project_ref     VARCHAR(32) NOT NULL,             -- CPT project_id引用
    dispatch_ref    VARCHAR(32) NOT NULL,             -- DIS dispatch_id引用
    c7_level        CHAR(2) NOT NULL CHECK (c7_level IN ('C1','C3','C5','C7')),
    loss_amount     NUMERIC(12,2) NOT NULL,
    payout_amount   NUMERIC(12,2) NOT NULL,
    status          VARCHAR(16) NOT NULL DEFAULT 'PENDING',
    filed_at        TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX idx_dlv_claims_dlv_time ON mds_dlv_insurance_claims(dlv_id, filed_at DESC);

3.3 BPL楼宇库PostGIS实现(D03 TND-BPL,核心摘要段)

详细技术方案见D03楼宇库文档。此处仅列出MDS全局架构中必须的BPL核心PostGIS扩展。

-- 扩展启用(一次性,DBA执行;production阿里云RDS PG默认支持postgis)
CREATE EXTENSION IF NOT EXISTS postgis;
CREATE EXTENSION IF NOT EXISTS postgis_topology;

-- BPL楼宇表新增geometry列(Alembic 0003增量,零ALTER现有列)
SELECT AddGeometryColumn('mds_bpl_building', 'geom_point', 4326, 'POINT', 2);
CREATE INDEX idx_bpl_geom_gist ON mds_bpl_building USING GIST (geom_point);
-- GPS围栏查询:LGP司机打卡时ST_DWithin判断是否在楼宇围栏50m内
-- 见D03 TND-BPL §三 详细SQL样例

四、主数据供给API(OHS · 对齐MDS-D §六4服务对接)

所有API路径前缀 /api/mds/supply/{consumer_service}/;返回仅ACTIVE状态;L3/L4字段自动脱敏(MDS聚合根I4不变量自动校验)。供给接口强制Redis缓存30秒保证NFR P95<500ms。

4.1 API总览(40路由 = QSV8 + CPT10 + DIS12 + OPS10)

消费方 路由前缀 operationId数 典型接口 关键VO
QSV报价(8) /api/mds/supply/qsv/* 8 GET /factors/quote-params(品类费率+楼宇系数+师傅等级+PPL品牌溢价4合一) CatalogRateFactor VO(1次调用取齐QSV 32因子主数据部分)
CPT跟踪(10) /api/mds/supply/cpt/* 10 GET /entity-refs/batch?wpl_ids=&cpl_ids=&scl_codes=;POST /building/verify(CPT L2现场核实写入反哺入口) EntityRefBatch VO / BuildingVerifyResult VO
DIS派单(12) /api/mds/supply/dis/* 12 GET /dlv-candidates?mode=M3&district=新界&pickup_lat=&pickup_lng=;GET /worker-pool?register_type=M1_SELF&scl_code=APPL-01&district=九龙 LogisticsCandidate VO / WorkerCandidate VO(含SkillMatrix评分)
OPS运营(10) /api/mds/supply/ops/* 10 GET /marketing/customers-by-tags?tags=高净值,老客户;GET /personal-brand/workers?level=GOLD MarketingCustomer VO / WorkerBrandProfile VO

4.2 QSV核心报价参数接口样例(1次HTTP拉齐32因子的主数据部分)

openapi: 3.0.3
paths:
  /api/mds/supply/qsv/factors/quote-params:
    get:
      operationId: mdsSupplyQsvQuoteParams
      summary: "QSV报价主参数一次性拉齐(SCL费率+BPL系数+WPL等级+PPL溢价 4合1)"
      tags: [MDS Supply QSV]
      parameters:
        - name: scl_code
          in: query
          required: true
          schema: { type: string, example: FURN-01 }
        - name: bpl_id
          in: query
          schema: { type: string }
        - name: wpl_level
          in: query
          schema: { type: string, enum: [NORM, AUTH, GOLD], default: AUTH }
        - name: ppl_brand_tier
          in: query
          schema: { type: string, enum: [GENERAL, PREMIUM_1_2, PREMIUM_1_4], default: GENERAL }
      responses:
        '200':
          description: QSV报价主参数(已按Keycloak角色RLS过滤,隐私字段L3+已脱敏)
          content:
            application/json:
              schema:
                type: object
                required: [scl_factor, bpl_coeff, wpl_coeff, ppl_multiplier, currency, generated_at]
                properties:
                  scl_factor:
                    $ref: '#/components/schemas/CatalogRateFactorVO'
                  bpl_coeff:
                    $ref: '#/components/schemas/BuildingCoefficientVO'
                  wpl_coeff:
                    type: number
                    enum: [0.9, 1.0, 1.2]
                    description: WPL师傅等级系数(NORM×0.9/AUTH×1.0/GOLD×1.2)
                  ppl_multiplier:
                    type: number
                    enum: [1.0, 1.2, 1.4]
                    description: PPL品牌溢价(普通1.0/高端1.2/超高端1.4)
                  currency:
                    type: string
                    const: HKD
                  generated_at:
                    type: string
                    format: date-time
                    description: 生成时间戳(QSV用于版本快照归档)

五、校准引擎三通道(CPT L8反哺 → MDS画像持续进化)

对齐MDS-D §2.4 5个ACL VO中的3个核心校准通道 + MDS-D §3.5 2个校准事件 + D01 §五NATS Topic#9 cpt.actual.calibration批量T+1消费。

5.1 引擎总体架构

flowchart TD
    NATS_SUB["NATS JetStream Consumer: mds-calibration-engine<br/>订阅cpt.actual.calibration主题(D01五#9)"]
    DEAD_LETTER["DLQ.mds.calibration.dead_letter<br/>max_attempts=3失败入DLQ"]

    NATS_SUB --> DECODE["反序列化ActualDataCalibrationPayload<br/>(含project_id, actual_hours, actual_amount,
    worker_score, building_verify_result, damage_rate, cst_nps, worker_id, bpl_id, scl_code, dlv_id)"]
    DECODE --> CHANNEL_router{"按载荷字段分发三通道"}

    CHANNEL_router -->|"worker_score/actual_hours不为空"| CH1["通道一:WPL师傅画像校准"]
    CHANNEL_router -->|"building_verify_result不为空"| CH2["通道二:BPL楼宇系数校准"]
    CHANNEL_router -->|"actual_amount偏差>±10% 或 damage_rate>C1阈值"| CH3["通道三:SCL品类费率校准"]
    CHANNEL_router -->|"dlv_id不为空 + c7赔付"| CH4["通道四:DLV物流商评分校准(扩展新增)"]

    CH1 --> W1["计算月度综合评分<br/>(评分×0.4 + 准时率×0.2 + 工时偏差×0.2 + 客诉率×0.2)"]
    W1 --> W2{"是否跨等级阈值?<br/>NORM→AUTH需≥30单+≥4.5<br/>AUTH→GOLD需≥100单+≥4.8 + 高级资质"}
    W2 -- "是" --> W3["生成WorkerDowngrade/Upgrade事件<br/>→ OL审核→BO审批→NATS发布mds.wpl.downgraded(D01五扩展事件)"]
    W2 -- "否" --> W4["仅更新WPL累计字段 + 写入mds_calibration_audit"]

    CH2 --> B1["对比BPL原系数 vs 核实结果<br/>电梯配置/楼层区间/管理要求差异率"]
    B1 --> B2{"差异率>20% 或 季度未核实超过1次?"}
    B2 -- "是" --> B3["生成BuildingCoefficientExpired事件<br/>→ TL派发楼宇更新任务(D01五扩展事件)"]

    CH3 --> S1["计算同一SCL细分类目近30单偏差率"]
    S1 --> S2{"累计偏差>±10%?"}
    S2 -- "是" --> S3["生成费率校准建议草稿→BO审核→SPO审批→写入mds_scl_rate_history→NATS发布mds.calibration.applied(D01五#10)→QSV/DIS订阅缓存失效"]

    CH1 & CH2 & CH3 & CH4 --> COMMIT["统一写入mds_calibration_audit表<br/>(幂等键=SHA256(calibration_payload_id))"]
    NATS_SUB -->|"max_attempts=3全失败"| DEAD_LETTER
    DEAD_LETTER --> OPR页面["OPR死信队列页面人工重放(mds_calibration_audit.status='DLQ')"]

5.2 幂等保证与DLQ策略

保障 实现
消费幂等 消息头 Nats-Msg-Id = SHA256(project_id + l8_completed_time)mds_calibration_audit.idempotency_key UNIQUE约束;重复键直接ACK
重试 JetStream nack(delay=指数退避 30s/60s/120s),max_attempts=3
死信 超过3次 → DLQ.mds.calibration.dead_letter Stream;OPR校准管理页面DLQ Tab人工查看 → 一键重放

六、MDM数据治理(版本+审批+生命周期)

6.1 审批流程(MasterData状态机 MDS-D §3.7实现)

状态流转 触发操作 审批角色 审计写入
DRAFT → PENDING_REVIEW OPR提交审核 OL(客户/师傅/品类)/ BO(PPL/DLV企业级) mds_version_history diff快照(before/after JSONB)
PENDING_REVIEW → ACTIVE 审批通过 OL/BO → SPO(PPL/DLV加SPO双签) mds_approval_workflow 记录审批链 + 时间戳
PENDING_REVIEW → DRAFT 驳回 OL/BO 驳回原因必填
ACTIVE → INACTIVE 主体停用 BO签字 停止供给OHS API返回(API自动过滤status≠ACTIVE)
ACTIVE → ARCHIVED 365天无交易 自动批处理(每日凌晨cron) 逻辑归档:移至 mds_*_archived 同结构表 + _archived后缀(RLS仅TL/BO可查询)
ACTIVE → DELETED PDPO删除权请求 SPO双签 + 法务确认 逻辑删除标记 is_deleted=true + 字段全部置空,物理删除延迟30天

6.2 版本号单调与回滚机制

  • label_version(标签变更):聚合根不变量I3强制单调+1;禁止UPDATE回退
  • 所有核心字段变更:自动写入mds_version_history(before_json JSONB, after_json JSONB, changed_fields TEXT[]),任何变更可一键回滚(OPR「历史版本」页面)

七、隐私保护与权限(PDPO合规 · OCM加签评审点)

与D01 ADR-016(PDPO加密方案=AES-256-GCM列级 + Keycloak Scope脱敏 + PG RLS)1:1对齐。详细方案见D07 TND-SEC §四。

7.1 分级加密汇总(六库L1/L2/L3/L4)

L1公开(OPR/客户展示页可读) L2内部(平台员工可读) L3敏感(OPR OL及以上才脱敏可见) L4机密(仅财务/合规加密可见)
WPL 昵称、等级、技能矩阵、个人品牌(§五6节) 注册时间、审核状态、排班表、接单统计量 手机号(展示852-****5678,WhatsApp通知走内部推送服务不取明文)、GPS位置 身份证号、证件照片、银行账号
CPL 信任徽章、标签L1、复购展示标志 客户ID、下单频次、五维标签L2-L3 手机号(打码)、WhatsApp号、地址模糊区 身份证号、支付密码哈希
PPL 品牌名称、品牌等级(PREMIUM_1_4标志)、合作品类范围 联系人姓名(不展示联系方式) 联系人电话/邮箱(打码) 银行账户、税务登记号、合同金额
DLV 物流商名称、业务模式、服务区域、评分 业务联系人姓名 联系电话打码 银行账号、商业登记号加密字段
BPL/SCL 楼宇名称公开、品类目录全公开(均为公开信息) —(无L3/L4字段)

7.2 加密方案

  • AES-256-GCM:所有L3/L4字段存库时用AEAD(Authenticated Encryption with Associated Data)加密;密文结构=[12字节nonce][密文][16字节tag]
  • 密钥管理:主密钥存阿里云KMS(Key Encryption Key,KEK);数据库加密密钥(Data Encryption Key,DEK)由KEK加密后存mds_crypto_keys表;每月轮转DEK
  • 脱敏规则服务端强制:所有跨服务供给API必须通过PrivacyDesensitizationService.apply(),禁止任何路由直接返回原始ROW(PG RLS + View双层保障,D07 TND-SEC §五)

八、缓存与性能(NFR P95<500ms保证)

缓存场景 TTL 存储 失效触发
MDS QSV报价参数(mdsSupplyQsvQuoteParams接口) 30s(强一致短缓存) Redis key=mds:qsv:params:{scl}:{bpl}:{wpl}:{ppl} NATS收到mds.calibration.applied事件 → 批量DEL对应keys
DIS派单候选池(worker/dlv) 60s Redis mds.entity.updated(师傅状态/物流商评分变更)→ DEL
BPL楼宇GPS围栏查询 3600s(楼宇数据变动极低频) Redis GeoHash + idx_bpl_geom_gist PostGIS索引双缓存 mds.bpl.coefficient_expired事件 → DEL
全局枚举(library_code/privacy_level/status) 3600s(启动时加载) 应用内存LRU Cache Alembic migration变更后重启失效

九、与Dxx其他技术文档关联

graph TD
    D10["D10 TND-MDS(本文件)"]
    D01["D01 TND-ARC(ADR-015四库→六库/ADR-016 PDPO加密)"]
    D03["D03 TND-BPL(楼宇PostGIS详细实现)"]
    D02["D02 TND-QSVE(报价PricingEngine消费SCL/BPL/WPL/PPL供给)"]
    D04["D04 TND-DIS(派单决策消费DLV/WPL技能矩阵 + C7赔付负向权重)"]
    D09["D09 TND-CPT(L2楼宇核实L6工时L8 NPS反哺写MDS校准通道)"]
    D05["D05 TND-B2B(MFR/LGP门户读写PPL/DLV,RLS行级权限)"]
    D07["D07 TND-SEC(PDPO完整方案 + Keycloak RL映射矩阵 + AES密钥轮转)"]
    API["OpenAPI 3.0 YAML /api/mds/supply/* 40路由契约"]

    D01 --> D10
    D10 --> D02 & D03 & D04 & D09 & D05 & API
    D07 --> D10

修订记录

版本 日期 修订人 修订内容
V1.0 2026-08-22 DT 阶段三TND V2.0首版:① 向后兼容V3.3 22表保证(Alembic 0003纯增量);② BAS DDD 8要素落地(2BC代码目录 + MasterDataCatalog 4不变量代码样例 + 6仓储端口 + ACL矩阵 + 10NATS事件);③ PostgreSQL 26表总清单 + PPL/DLV新增核心DDL + WPL REGISTER_TYPE/M3_SME约束SQL + PostGIS BPL geom扩展;④ OHS供给API 40路由总览(QSV8/CPT10/DIS12/OPS10)+ QSV报价参数OpenAPI YAML样例;⑤ 校准引擎四通道架构图(WPL/BPL/SCL/DLV)+ NATS幂等/DLQ策略;⑥ MDM治理:6状态审批流 + label_version单调 + 版本回滚;⑦ PDPO隐私:六库L1-L4字段分级 + AES-256-GCM加密方案(OCM加签);⑧ 缓存与性能NFR P95<500ms 4场景TTL失效触发设计;⑨ Dxx关联图

本文件为 D10(TND-MDS V1.0 MDS主数据服务技术方案),向后100%兼容V3.3 4库表结构,Alembic 0003纯增量扩展PPL/DLV;上游Published Language角色为 QSV/DIS/CPT/OPS提供标准供给API;校准引擎四通道实现CPT L8→MDS画像闭环。详细PostGIS实现见D03 TND-BPL;详细PDPO加密/RLS策略见D07 TND-SEC(OCM必须加签评审)。