订单与派单系统技术设计(TND-DIS)¶
文档编号:DOC-D04 / 简称:TND-DIS 版本:V1.0 创建日期:2026-08-22 维护人:TL / DT(听写编写,听码WDE消费;听云OCM评审§七SAGA补偿可靠性+§六C7保险资金清算审计) 决策审批人:TL + SPO 关联文档:D01 TND-ARC ADR-018(SAGA补偿Choreography)/ §五NATS DIS 5事件(#12-16)、CPT-D V2.5(8阶段生命周期+跟踪池+异常处理)、DIS-D V1.1(派单决策引擎+M1/M2/M3 × 安装三模式9组合 + C7赔付策略 + OL人工调整)、D02 TND-QSVE §四DME运输模型写回、D10 TND-MDS WPL/DLV/C7供给、D09 TND-CPT(183字段完整模型)
一、定位与服务划分(CPT跟踪 + DIS派单 = 2服务聚合在TND-D04)¶
为什么D04合一? DIS派单与CPT跟踪强耦合:CPT L5签约→NATS触发DIS派单;DIS派单结果写回CPT L5 assignment_id;DIS异常自动跳CPT异常池。代码层按模块边界分离,但技术设计在同一份文档中统一描述跨服务交互。
| 子服务 | 核心职责 | 聚合根 | 主要交互方 |
|---|---|---|---|
| CPT 客户项目跟踪(数据主线核心) | 8阶段生命周期状态机、跟踪池8类、183字段7类采集物采集、L7客诉L8 NPS | CustomerProject(project_id) |
QSV(L3报价绑定)/ MDS(L2主体引用)/ DIS(L5派单触发+结果回写)/ OPS(L1线索入/L8 NPS出) |
| DIS 派单决策引擎(决策核心) | 9组合(运输3×安装3)候选池排序、OL人工调整、C7保险4档触发、拒单降级重试 | DispatchOrder(dispatch_id) + DispatchAssignment(3候选) |
MDS(DLV档案/WPL技能/C7历史)/ CPT(L5触发/L7客诉SAGA回滚)/ OPS(C7预警出)/ LGP门户(接单API) |
二、BAS V1.0 DDD 8要素落地(双服务)¶
2.1 代码边界¶
backend/
cpt/ # ========== CPT客户跟踪模块 ==========
interfaces/api/cpt_router.py # 40路由
application/services/
ProjectLifecycleAppService.py # 8阶段编排(状态机驱动)
TrackingPoolAppService.py # 8跟踪池调度
ExceptionL7AppService.py # L7客诉升级 + SAGA补偿触发
domain/
customer_project.py # 聚合根 CustomerProject + 8阶段状态机
tracking_pool.py # 跟踪池聚合根(8类池)
value_objects/
stage_status.py # L1-L8 枚举 + 流转规则
collection_items_183.py # 7类183采集字段VO(D09 §二1:1)
nps_feedback.py # L8 9档NPS + REF推荐码
ports.py # ProjectRepository / PoolRepository等
events.py # cpt.* 7事件(D01五#3-9)
infrastructure/
db/models.py # cpt_projects主表 / 183字段采集表 / 状态历史表
fsm/statemachine_cpt.py # 状态机实现:transitions库(Python)或自研
nats/* # 发布L1~L8事件 + 订阅DIS派单事件
dis/ # ========== DIS派单决策模块 ==========
interfaces/api/dis_router.py # 35路由:dispatch调度台API / LGP门户接单API / OL人工调整API
application/services/
DispatchMatchingAppService.py # 派单编排:L5事件→查候选→加权排序→出3推荐
InsuranceC7AppService.py # C7保险4档赔付计算 + 记录写回
OLManualAdjustAppService.py # OL人工改派:写入adjusted_reason必填 + 审计
domain/
dispatch_order.py # 聚合根 DispatchOrder(5不变量)
dispatch_assignment.py # 实体:每单3候选(M1/M2/M3)
value_objects/
composite_score.py # 综合分VO:运输分×0.55 + 安装分×0.45(0~100)
nine_comb_matrix.py # 3运输×3安装=9组合矩阵
c7_payout_tiers.py # C1/C3/C5/C7 4档阈值
services/
dispatch_matching_engine.py # 9组合匹配器(领域服务)
c7_insurance_policy.py # C7保险策略(领域服务)
ports.py # DispatchOrderRepository等
events.py # dis.* 5事件(D01五#12-16)
infrastructure/
db/models.py # dis_orders / dis_assignments / dis_ins_claims 三主表
rules/dispatch_rules.json # JSON-Rules(同D02 §三策略:<200条用JSON-Rules → Drools演进)
nats/*
dispatch_req_subscriber.py # 订阅cpt.dispatch.requested(D01五#5)→ 触发派单计算
event_publisher.py # 发布dispatch.created/adjusted/rejected/warning/completed
三、CPT:CustomerProject 8阶段状态机(核心数据主线)¶
对齐CPT-D §4.1 L1-L8生命周期。状态机实现选型:Python
transitions库(轻量,单体优先P3原则;成熟期若拆服务,事件驱动SAGA即可)。
3.1 状态流转图(Mermaid stateDiagram-v2)¶
stateDiagram-v2
direction LR
[*] --> L1_LEAD: NATS ops.lead.generated(OPS获客线索入池)
L1_LEAD --> L2_REQ_CONFIRMED: OL联系客户+L2楼宇核实(BPL反哺MDS)
L1_LEAD --> POOL_DROPPED: 3次未接通→流失池
L2_REQ_CONFIRMED --> L3_QUOTED: CPT调用QSV→报价version绑定project
L3_QUOTED --> L3_QUOTE_REVISED: 客户改参数→QSV新版本(version++)
L3_QUOTED --> L4_CONTRACTED: 客户签字确认(电子签章→R2归档)+ quote_version_lock不可变
L4_CONTRACTED --> L5_DISPATCHING: CPT发布cpt.dispatch.requested(NATS D01五#5)→ DIS订阅
L5_DISPATCHING --> L5_DISPATCHED: DIS发布dis.dispatch.created(D01五#12)→ assignment_id写回CPT
L5_DISPATCHING --> L5_DISPATCH_RETRY: dis.dispatch.rejected(#14)→ 模式降级,重试max=3
L5_DISPATCH_RETRY --> POOL_EXCEPTION: 三模式全拒绝→异常池,OL人工介入
L5_DISPATCHED --> L6_INSTALLATION: WKR师傅L6上门打卡(GPS+时间+照片三重校验,见D08 §三)
L6_INSTALLATION --> L6_INSTALL_COMPLETED: 师傅上传完工照片+客户签字(CPT发布cpt.install.completed #6)
L6_INSTALL_COMPLETED --> L7_ACCEPTANCE: 客户验收
L7_ACCEPTANCE --> L7_COMPLAINT_ESCALATION: 客户发起L7客诉→SAGA补偿(§七)
L7_ACCEPTANCE --> L8_NPS: 验收通过→L8回访NPS采集
L7_COMPLAINT_ESCALATION --> L8_NPS: 客诉闭环后进入NPS
L8_NPS --> COMPLETED: NPS评分+REF推荐码→发布cpt.nps.feedback #8 + cpt.actual.calibration #9(T+1批量)
COMPLETED --> [*]
POOL_DROPPED --> POOL_REACTIVATE: 30天后OL重激活(二次营销OPS接管)
POOL_EXCEPTION --> L1_LEAD: 人工解决(如换师傅/加价)后重新入L1
state L5_DISPATCH_RETRY {
note right of L5_DISPATCH_RETRY
重试策略:优先模式→降级→再降级
例:首选M3→M2→M1→全模式不可用
每次reject:WKR师傅M2连续2拒=-25分+30min挂起
end note
}
3.2 状态机不变量(强制代码校验)¶
# backend/cpt/domain/customer_project.py(聚合根,不变量对应状态机)
class CustomerProject:
# ===== 不变量(6条)=====
def I1_stage_order(self, from_stage, to_stage):
"""I1: 阶段跳级禁止,仅允许 L(n)→L(n±1) 或进入异常池"""
legal_jump = {
("L1","L2"), ("L2","L3"), ("L3","L4"), ("L4","L5"),
("L5","L6"), ("L6","L7"), ("L7","L8"), ("L8","COMPLETED"),
("L*","POOL_*"), # 异常跳池允许
}
if (from_stage, to_stage) not in legal_jump:
raise ValueError(f"CPT-I1 违反: 非法跳级 {from_stage}→{to_stage}")
def I2_quote_locked_after_L4(self, stage, quote_version_changed: bool):
"""I2: L4签约后quote_version不可变(防止坐地起价)"""
if stage >= "L4" and quote_version_changed:
raise ValueError("CPT-I2 违反: L4签约后报价不可变更,需走L7客诉+补偿流程")
def I3_assignment_only_L5(self, stage, operation="assign"):
"""I3: 派单操作仅允许在L5阶段"""
if stage != "L5_DISPATCHING" and operation == "assign":
raise ValueError("CPT-I3 违反: 非L5阶段不可派单")
def I4_gps_check_required_L6(self, wkr_gps, bpl_geom):
"""I4: L6师傅打卡必须满足GPS在楼宇50m围栏内(PostGIS ST_DWithin)"""
from geoalchemy2.functions import ST_DWithin
if not ST_DWithin(wkr_gps, bpl_geom, 50):
raise ValueError("CPT-I4 违反: L6 GPS未在楼宇50m围栏内,拒绝打卡")
def I5_refund_requires_l7(self, refund_requested):
"""I5: 退款/扣款逆向SAGA必须先有L7客诉工单记录"""
if refund_requested and not self.l7_complaint_id:
raise ValueError("CPT-I5 违反: 无L7客诉工单不可发起资金SAGA")
def I6_183_required_percentage(self, stage, collection_completion_pct):
"""I6: 各阶段采集完成率最低阈值:L3≥70%、L6≥95%、L8=100%"""
threshold = {"L3": 0.70, "L6": 0.95, "L8": 1.00}.get(stage)
if threshold and collection_completion_pct < threshold:
raise ValueError(f"CPT-I6 违反: {stage}采集完成度{collection_completion_pct}<{threshold}")
四、DIS:派单决策引擎9组合 + 综合分加权(DIS-D V1.1 落地)¶
4.1 派单总体流程(对齐DIS-D §三+D02 §四DME)¶
flowchart TD
NATS_TRIGGER["订阅NATS cpt.dispatch.requested(D01五#5)<br/>载荷:project_id/pickup_addr/delivery_addr/preferred_mode/c7_level/weight_kg/volume_m3/scl_code"]
STEP1["Step1: MDS供给查询拉候选<br/>GET /api/mds/supply/dis/dlv-candidates(D10 §四.12接口×2:DLV物流商+WKR师傅)<br/>Redis缓存60s"]
STEP2["Step2: 9组合矩阵生成(运输M1/M2/M3 × 安装M1_SELF/M2_FREE/M3_SME = 3×3=9行)<br/>模式过滤:<100kg且港岛=M1自营优先;≥200kg+跨境=强制M3;小众品类≥2=M2强制"]
STEP3["Step3: 每行候选分计算<br/>运输分 = DME性价比分(0~100)×历史准时率 × 投诉率惩罚<br/>安装分 = 技能匹配分 × 师傅等级系数 × 实时可用 + 距离GPS衰减<br/>综合分 = 运输分×0.55 + 安装分×0.45<br/>C7赔付历史负向加权:DLV赔付率>C1阈值→运输分-15"]
STEP4["Step4: 过滤非法组合(M3_SME必须绑定PPL企业ID等WPL不变量#2)"]
STEP5["Step5: 按综合分降序→取每模式Top1→生成3个Assignment候选(不是Top3,保证三模式各一推荐)"]
STEP6["Step6: 规则引擎二次过滤(如:客户指定高端品牌=必须M1+M1_SELF组合)"]
FINAL["生成DispatchOrder聚合根 → INSERT 3 assignment行<br/>→ NATS发布dis.dispatch.created(D01五#12)<br/>→ 写回CPT L5阶段assignment_id<br/>→ 推送WKR/LGP门户通知(Expo Push)"]
NATS_TRIGGER --> STEP1 --> STEP2 --> STEP3 --> STEP4 --> STEP5 --> STEP6 --> FINAL
4.2 9组合矩阵定义¶
对齐WPL V1.2 §2.4 REGISTER_TYPE三分类 × D02 §四 DME运输三模式。
| 组合ID | 运输模式 | 安装模式 | REGISTER_TYPE匹配 | 典型客单 | 综合分基础权重(冷启动) |
|---|---|---|---|---|---|
| C11 | M1自营车 | M1_SELF自营师傅 | M1(必须M1绑定) | 高价值客户/指定(优先级×1.15) | 基础=90(质量优先) |
| C12 | M1自营车 | M2_FREE个体师傅 | M2(自雇注册) | 标准客单弹性供给 | 基础=70 |
| C13 | M1自营车 | M3_SME签约小B | M3+PPL企业绑定 | 批量工程/厂家合作 | 基础=65(企业成本另付) |
| C21 | M2独立司机 | M1_SELF | M1 | 离岛/过海单 | 基础=60 |
| C22 | M2独立司机 | M2_FREE | M2 | 高峰期默认首选 | 基础=75(性价比最优) |
| C23 | M2独立司机 | M3_SME | M3 | 中等规模 | 基础=60 |
| C31 | M3专业物流 | M1_SELF | M1 | 跨境/厂家批量 | 基础=80 |
| C32 | M3专业物流 | M2_FREE | M2 | 跨境+弹性 | 基础=70 |
| C33 | M3专业物流 | M3_SME | M3 | MFR批量/≥C7保险必选 | 基础=95(合规优先)×强制场景MULT=1.5 |
4.3 综合分计算公式(伪代码,Python)¶
# backend/dis/domain/services/dispatch_matching_engine.py
def calc_composite_score(combo_id: str, dlv_cand, wkr_cand, project_ctx) -> CompositeScore:
# 基础权重(§4.2矩阵)
base = {
"C11":90, "C12":70, "C13":65, "C21":60, "C22":75, "C23":60,
"C31":80, "C32":70, "C33":95,
}[combo_id]
# === 运输分(0~100):DME性价比 + 准时率 + 投诉率惩罚 + C7历史扣分 ===
dme_value = 100 - (dlv_cand.quoted_cost / project_ctx.budget_transport_max) * 80
dme_value = max(0, min(100, dme_value)) # clamp 0~100
on_time_coef = dlv_cand.on_time_rate_last_30d # 0~1.0,如0.95
complaint_penalty = min(20, dlv_cand.complaint_rate_last_30d * 200) # 如10%=20分扣
c7_penalty = min(15, dlv_cand.c7_payout_count_90d * 3) # 每1次90天赔付扣3分,上限15
transport_score = dme_value * on_time_coef - complaint_penalty - c7_penalty
transport_score = max(0, min(100, transport_score))
# === 安装分(0~100):技能匹配 + 等级系数 + 实时可用 + GPS距离衰减 ===
skill_match = wkr_cand.skill_match_score_scl(project_ctx.scl_code) # 0~100 技能矩阵匹配SCL细目
level_coef = {"NORM":0.9, "AUTH":1.0, "GOLD":1.2}[wkr_cand.level]
availability_bonus = 30 if wkr_cand.is_online_and_idle_now else 0 # 在线空闲+30分
distance_penalty = min(20, wkr_cand.distance_from_pickup_km * 2) # 超10km扣满20
install_score = skill_match * level_coef + availability_bonus - distance_penalty
install_score = max(0, min(100, install_score))
# === 综合分加权(核心公式:运输×0.55 + 安装×0.45)===
composite_raw = transport_score * 0.55 + install_score * 0.45
# 场景乘数(如:MFR≥5单 → C33分×1.5;跨境勾选→所有非M3运输分×0.8惩罚)
scene_multiplier = 1.0
if project_ctx.is_mfr_batch_order and combo_id == "C33":
scene_multiplier = 1.5
if project_ctx.requires_cross_border and combo_id[1] != '3':
scene_multiplier *= 0.8
final = round(composite_raw * scene_multiplier, 2)
return CompositeScore(
combo_id=combo_id, base_weight=base,
transport_score=transport_score, install_score=install_score,
composite_raw=composite_raw, multiplier=scene_multiplier, final=final
)
4.4 DispatchOrder聚合根5不变量¶
# backend/dis/domain/dispatch_order.py
class DispatchOrder:
def I1_three_modes_at_least_one(self, assignments):
"""I1: 至少一候选,且三模式(M1/M2/M3)各有一条(非null)"""
modes = {a.mode_group for a in assignments}
if not {"M1","M2","M3"}.issubset(modes):
raise ValueError("DIS-I1 违反: 三模式候选池缺项")
def I2_composite_0_100_range(self, score):
"""I2: 综合分严格∈[0,100],禁止负值或>100"""
if not 0 <= score.final <= 100:
raise ValueError(f"DIS-I2 违反: 综合分={score.final},必须0~100")
def I3_reject_2_consecutive_penalty(self, wkr_id, reject_count, current_time):
"""I3: M2师傅2连拒=-25分 + 30分钟挂起(对齐WPL不变量#3)"""
if reject_count >= 2: # 连续拒单
MDS_CALIBRATION_PAYLOAD.publish({
"event":"wpl_score_penalty", "wkr_id": wkr_id, "penalty": -25
})
# SET Redis wkr:{id}:suspended_until = NOW + 30min
return PenaltyResult(points=-25, suspend_minutes=30)
def I4_ol_adjust_required_reason(self, adjusted, reason):
"""I4: OL人工调整必须写明原因(≥20字),强制审计"""
if adjusted and (not reason or len(reason.strip()) < 20):
raise ValueError("DIS-I4 违反: 人工改派原因≥20字,强制审计留痕")
def I5_m3_sme_must_bind_ppl(self, wkr_register_type, ppl_enterprise_id):
"""I5: M3_SME安装师傅必须绑定PPL企业ID(对齐WPL V1.2不变量#2)"""
if wkr_register_type == "M3_SME" and not ppl_enterprise_id:
raise ValueError("DIS-I5 违反: M3_SME安装模式缺少绑定PPL企业ID,D10 WPL CHECK约束")
五、拒单降级重试(Saga Choreography级本地补偿)¶
flowchart TD
ASSIGN1["候选1(默认首选综合分TOP1:如C22 M2×M2)派单→推送LGP/WKR"]
ASSIGN1 --> R1{"LGP/WKR接受?30s超时=拒"}
R1 -- "接受" | SUCCESS --> SUCCESS["dis.dispatch.created → CPT L5→L6推进"]
R1 -- "拒绝/超时" | REJECTED1 --> LOG1["记录拒单原因 → 执行DIS I3处罚(M2连拒=-25+30min)"]
LOG1 --> NEXT2["降级:候选2(次高分,如C11 M1×M1)"]
NEXT2 --> R2{"接受?"}
R2 -- "是" --> SUCCESS
R2 -- "否/超时" --> LOG2["第二次拒单记录+处罚"]
LOG2 --> NEXT3["降级:候选3(如C33 M3×M3兜底,成本最高但最可靠)"]
NEXT3 --> R3{"接受?"}
R3 -- "是" --> SUCCESS
R3 -- "否/超时" --> EXCEPTION["全模式不可用→发布dis.exception.no_resource→CPT L5→POOL_EXCEPTION→OL报警→人工调度台dispatch.html处理"]
六、C7保险赔付4档策略(C1/C3/C5/C7,对齐DIS-D §6.4)¶
与D02 §五.C7加价险联动;C7资金流由听云OCM §生产清算审计。
| C7档位 | 赔付触发条件(损率=损失/报价商品价值) | 赔付比例(客户获赔) | 免赔额 | 典型场景 |
|---|---|---|---|---|
| C1(基础档,258HKD客户加购价) | 损率 ≥ 5% | 实际损失×50% | 最高 5,000 HKD | 标准家具(≤2万货值) |
| C3(中档) | 损率 ≥ 3% | 实际损失×70% | 最高 20,000 HKD | 家电+部分家具(货值2-10万) |
| C5(高档) | 损率 ≥ 1% | 实际损失×90% | 最高 50,000 HKD | 高档家具、钢琴、易碎品 |
| C7(全额档,跨境必选) | 任何损失,含丢件/跨境清关损 | 实际损失×100% | 无上限+跨境HS报关索赔 | 奢侈品、跨境物流、MFR批量货值>10万 |
6.1 C7事件流¶
DIS发布 InsuranceWarning → OPS订阅(D01五#15):
- 损率命中阈值 → DIS触发预警事件
- OPS创建保险工单,通知客户/师傅补充证据
- C7赔付完成 → 写D10 mds_dlv_insurance_claims(负向影响DLV派单评分)
- 发布dis.insurance.warning主题 → OPS保险理赔页Dashboard展示
七、L7客诉SAGA补偿(ADR-018落地 · 5服务Choreography链式补偿)¶
7.1 典型场景:客户验收时发现货损+安装错误→发起L7客诉→需要1)退款2)撤销师傅评分3)撤销物流商派单评价4)撤销C7保险预警¶
Orchestrator?No: Choreography无中心节点(ADR-018原则),每服务订阅正向事件→触发本地补偿→发布对应
.compensate反向事件。
flowchart LR
TRIGGER["触发:客户在CST App L7页面点击【客诉升级】<br/>→ CPT ExceptionL7AppService.validate_I5(refund=True→必须有l7_ticket_id)"]
CPT_REV["CPT发布<br/>cpt.l7.complaint_opened(正向)<br/>载荷:project_id/refund_requested_amount/reason"]
CPT_REV --> QSV_SUB["QSV订阅→冻结该quote_version<br/>不可再做重算"]
QSV_SUB --> QSV_PUB["QSV发布<br/>qsv.quote.frozen"]
CPT_REV --> WPL_SUB["MDS订阅→撤销此次L8对该师傅的加分<br/>如果是虚高评分"]
WPL_SUB --> MDS_W_PUB["MDS发布<br/>mds.wpl.score_adjusted.compensate<br/>(反向事件,补偿撤销)"]
CPT_REV --> DLV_SUB["DIS订阅→撤销此次DLV派单5星好评<br/>DLV综合分回滚(如+1.5→回退)"]
DLV_SUB --> DIS_D_PUB["DIS发布<br/>dis.dlv_score.rollback.compensate"]
CPT_REV --> INS_SUB["DIS订阅→若之前已经C7警告则标记为<br/>客诉争议→暂停赔付,等待仲裁"]
INS_SUB --> DIS_I_PUB["DIS发布 dis.insurance.paused.compensate"]
QSV_PUB & MDS_W_PUB & DIS_D_PUB & DIS_I_PUB --> ALL_DONE["SAGA Orchestrator(可选)监控所有compensate事件完成后<br/>→ 触发资金退款SAGA(生产由OCM执行,支付宝HK/FPS退)<br/>→ saga_state DB表写CLOSED"]
7.2 SAGA状态表(持久化补偿)¶
CREATE TABLE dis_saga_state (
saga_id VARCHAR(32) PRIMARY KEY, # SAGA-YYYYMMDD-NNNN
saga_type VARCHAR(32) NOT NULL, # L7_REFUND / DISPATCH_RETRY_ROLLBACK
trigger_id VARCHAR(32) NOT NULL, # project_id / dispatch_id
steps_total INT NOT NULL, # 5步(QSV+MDS WPL+DIS DLV+DIS C7+资金)
steps_completed INT NOT NULL DEFAULT 0,
status VARCHAR(16) NOT NULL DEFAULT 'RUNNING'
CHECK (status IN ('RUNNING','COMPLETED','FAILED_COMPENSATING','DEAD')),
dead_letter_reason JSONB, # 补偿失败时死信原因
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
closed_at TIMESTAMPTZ
);
CREATE INDEX idx_saga_status ON dis_saga_state(status);
八、PostgreSQL DDL(Alembic 0005,纯新建无ALTER V3.3)¶
183字段CPT跟踪表完整DDL详见D09 TND-CPT §三;此处仅列DIS派单三主表 + SAGA状态表 + CPT核心主表摘要DDL。
-- ========== DIS三主表 ==========
CREATE TABLE dis_orders (
dispatch_id VARCHAR(32) PRIMARY KEY, -- DIS-YYYYMMDD-NNNN
project_id_fk VARCHAR(32) NOT NULL REFERENCES cpt_projects(project_id),
preferred_mode VARCHAR(16), -- M1/M2/M3(系统建议,客户可改)
final_combo_id VARCHAR(8), -- 最终选中C11~C33
final_assignment_id BIGINT, -- 最终选中的assignment FK
final_score NUMERIC(5,2),
c7_level CHAR(2) NOT NULL DEFAULT 'C1' CHECK (c7_level IN ('C1','C3','C5','C7')),
dispatch_status VARCHAR(32) NOT NULL DEFAULT 'MATCHING',
ol_adjusted_by VARCHAR(32), -- OL人工调整人ID
ol_adjusted_reason TEXT, -- I4 ≥20字强制校验
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
completed_at TIMESTAMPTZ
);
CREATE INDEX idx_dis_project ON dis_orders(project_id_fk);
CREATE INDEX idx_dis_status_created ON dis_orders(dispatch_status, created_at DESC);
CREATE TABLE dis_assignments (
id BIGSERIAL PRIMARY KEY,
dispatch_id_fk VARCHAR(32) NOT NULL REFERENCES dis_orders(dispatch_id) ON DELETE CASCADE,
combo_id VARCHAR(8) NOT NULL CHECK (combo_id ~ '^C[123][123]$'), -- C11~C33
dlv_id_fk VARCHAR(32) NOT NULL REFERENCES mds_dlv_logistics(dlv_id),
wkr_id_fk VARCHAR(32) NOT NULL REFERENCES mds_wpl_worker(wpl_id),
transport_score NUMERIC(5,2) NOT NULL CHECK (transport_score BETWEEN 0 AND 100),
install_score NUMERIC(5,2) NOT NULL CHECK (install_score BETWEEN 0 AND 100),
composite_final NUMERIC(5,2) NOT NULL CHECK (composite_final BETWEEN 0 AND 100), -- I2
quoted_cost_total NUMERIC(12,2) NOT NULL,
eta_delivery TIMESTAMPTZ,
state VARCHAR(16) NOT NULL DEFAULT 'PROPOSED'
CHECK (state IN ('PROPOSED','ACCEPTED','REJECTED','TIMEOUT','ROLLBACK')),
reject_code VARCHAR(8), -- R01价格低/R02时间错/R03能力不足/R04其他
UNIQUE (dispatch_id_fk, combo_id)
);
CREATE TABLE dis_insurance_claims ( -- C7四档(与D10 mds_dlv_insurance_claims 1:1对齐)
claim_id VARCHAR(32) PRIMARY KEY,
dispatch_id_fk VARCHAR(32) NOT NULL REFERENCES dis_orders(dispatch_id),
c7_level CHAR(2) NOT NULL,
damage_rate_pct NUMERIC(5,2) NOT NULL, -- 损率,触发阈值C1≥5/C3≥3/C5≥1/C7任何
payout_amount NUMERIC(12,2) NOT NULL,
status VARCHAR(16) NOT NULL DEFAULT 'PENDING'
);
-- ========== CPT核心主表摘要(183字段完整见D09)==========
CREATE TABLE cpt_projects (
project_id VARCHAR(32) PRIMARY KEY, -- CPT-YYYYMMDD-NNNN
stage VARCHAR(32) NOT NULL DEFAULT 'L1_LEAD',
quote_id_fk VARCHAR(32) NULL REFERENCES qsv_quotes(quote_id),
quote_version_lock INT NULL, -- I2 L4后不可变
cst_id_fk VARCHAR(32) NOT NULL REFERENCES mds_cpl_customer(cpl_id),
wkr_id_fk VARCHAR(32) NULL REFERENCES mds_wpl_worker(wpl_id),
bpl_id_fk VARCHAR(32) NULL REFERENCES mds_bpl_building(bpl_id),
scl_code VARCHAR(16) NOT NULL,
ppl_id_fk VARCHAR(32) NULL REFERENCES mds_ppl_producer(ppl_id),
l7_complaint_id VARCHAR(32) NULL, -- I5 客诉ID
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX idx_cpt_stage_time ON cpt_projects(stage, created_at DESC);
-- SAGA状态表(§七.2)同前...
九、NFR:DIS派单P95<5s + CPT查询P95<500ms¶
| 步骤 | 耗时预算 | 优化手段 |
|---|---|---|
| DIS派单P95<5s(D01 §十NFR对齐) | ||
| MDS供给DLV/WKR候选拉取 | ≤500ms | D10 Redis缓存60s;DB加组合索引(register_type, level, coverage) |
| 9组合计算(3×3=9候选×综合分公式) | ≤500ms | 纯CPU计算,Py3.12;批量分数计算用numpy向量化 |
| LGP/WKR推送+等待30s超时(单次) | ~30000ms(太长) | 异步推送并行:3候选同时推送,不阻塞HTTP响应;CPT L5状态显示「派单中」;状态用WebSocket/SSE推送OPR调度台 |
| 候选1接受 | ≤200ms + 用户点击耗时(秒级) | LGP门户/App推送高优先级通知(APNs/FCM high priority) |
| CPT列表P95<500ms | ||
| OPR跟踪池分页查 | ≤400ms | PG索引+Redis缓存OPR常用跟踪池筛选条件结果集10s |
十、Dxx关联图¶
graph TD
D04["D04 TND-DIS(本文件)"]
D01["D01(SAGA ADR-018 / NATS 5事件流)"]
D02["D02 QSVE(L3报价绑定L4锁 / DME三模式成本写回)"]
D10["D10 MDS(WPL REGISTER_TYPE不变量 / DLV档案+C7历史供给)"]
D09["D09 TND-CPT(183字段完整模型 / L6三重GPS校验)"]
D08["D08 TND-WKR(师傅端L6打卡三重校验 + 拒单UI处罚通知)"]
D05["D05 TND-B2B(LGP门户接单方面 + MFR批量强制C33场景)"]
D07["D07 TND-SEC(C7资金审计 + SAGA状态表RLS)"]
API["OpenAPI 75路由(CPT40 + DIS35)"]
D01 --> D04
D02 & D10 --> D04
D04 --> D09 & D08 & D05 & D07 & API
修订记录¶
| 版本 | 日期 | 修订人 | 修订内容 |
|---|---|---|---|
| V1.0 | 2026-08-22 | DT | 首版:①CPT+DIS双服务合一说明与代码边界;②CustomerProject聚合根6不变量代码+8阶段状态机Mermaid图;③DIS派单引擎Step1-6流程图+9组合矩阵定义(C11~C33)+ 综合分加权公式代码(运输×0.55+安装×0.45);④DispatchOrder 5不变量(含M3_SME↔PPL绑定校验 I5);⑤3候选拒单降级重试流;⑥C7四档赔付策略矩阵+事件流;⑦L7客诉SAGA Choreography 5服务补偿图 + saga_state表DDL;⑧Alembic 0005 DDL(DIS三主表+CPT主表摘要);⑨NFR分解(派单P95<5s+CPT<500ms);⑩Dxx关联图 |
本文件为 D04(TND-DIS V1.0 订单与派单系统技术设计),覆盖CPT 8阶段状态机 + DIS派单9组合加权决策 + C7保险4档 + L7 SAGA补偿;所有跨服务交互通过D01 §五 NATS JetStream 20主题中的14个实现,确保最终一致性;派单P95<5s采用异步并行推送 + OPR调度台WebSocket实时刷新策略。