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.0、BPL V1.0、WPL V1.2、CPL V1.1、PPL V1.1、DLV V1.1、TSMM 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必须加签评审)。