Commit c4636e36 by luoqi

feat(friday): 摄入契约收敛为 9 张自洽表 — 宿主 inline 自己的引用(全路径一致)

manifest 共用于所有摄入路径(cold-import/file/pull/push/reparse),改它一处全生效。
删 5 处跨表 lookup + 5 张源表,宿主导出时 within-host join 后 inline:
- 私有字典(std_diag/std_check_class)→ diag[].stdCode / med_check.class_name(删 lookup + 源表)
- 联系方式 1:N(customer_contacts)→ customer_basic_info.phone/phone_relationship(挑默认号)
- 计划头(customer_treat_plan)→ 计划行 organization_id/plan_name(删 lookup + 头源表 + group)
- 支付通道(settlement_modes)→ 不摄入(金额在结算头 net_receipts_this;payment 去掉 method)
- incremental 清废弃表名(patient_settlement_refund/spec_refund)+ 补 patient_settlement_spec

export.sh 同步改成自洽形态(within-host join;含 diag stdCode 跨库预取 map);
image.yaml primary image_rows→med_check;payment.yaml 去 method。

验证:元和王永 16472 患者 dry-run 逐资源计数与 inline 前**完全一致**
(patient/diagnosis 4458/treatment 5781/image 6170/payment 20995/refund 190+308);
中间表 41→33;typecheck 干净;全量 299 测试绿。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
parent 08c515b6
# diagnosis — FRIDAY 诊断 fact(med_emr_info.diag[] 拆行) # diagnosis — FRIDAY 诊断 fact(med_emr_info.diag[] 拆行)
# 消费 transforms 输出表 diagnosis_rows(split + std_diag lookup + normalize/substring/coalesce) # 消费 transforms 输出表 diagnosis_rows(split + normalize/substring/coalesce;std_code 由宿主 inline 进 diag 元素)
# #
# code 解析规则(用户定,2026-07-18): # code 解析规则(用户定,2026-07-18):
# linkCode 非空 → std_diag.std_code K 开头 → 截 K 大类(std_code_k 直通下方共享字典 passthrough); # linkCode 非空 → std_diag.std_code K 开头 → 截 K 大类(std_code_k 直通下方共享字典 passthrough);
......
# image — FRIDAY 影像 metadata(MySQL emr.med_check;PAC 不持文件本体) # image — FRIDAY 影像 metadata(MySQL emr.med_check;PAC 不持文件本体)
# 影像不在 Mongo 病历文档内(全字段枚举证实),挂 emr_id(10 位病历号)回病历。 # 影像不在 Mongo 病历文档内(全字段枚举证实),挂 emr_id(10 位病历号)回病历。
# 消费 transforms 输出表 image_rows(med_check lookup std_check_class 附 class_name) # 直接消费 med_check(class_name 由宿主 inline:class_code→std_check_class 已解析);modality 经共享字典翻
canonical: image canonical: image
emits: emits:
...@@ -9,7 +9,7 @@ emits: ...@@ -9,7 +9,7 @@ emits:
occurredAtField: uploadedAt occurredAtField: uploadedAt
primary: primary:
table: image_rows table: med_check
key: id key: id
dedup_by: id dedup_by: id
...@@ -21,7 +21,7 @@ field_mapping: ...@@ -21,7 +21,7 @@ field_mapping:
clinicId: organization_id clinicId: organization_id
uploadedAt: shooting_time # 拍摄时间 uploadedAt: shooting_time # 拍摄时间
encounterExternalId: emr_id # 病历号级挂载(= med_emr_info.emr_id,非 emr_sub_id) encounterExternalId: emr_id # 病历号级挂载(= med_emr_info.emr_id,非 emr_sub_id)
modality: class_name # lookup 自 std_check_class 字典 modality: class_name # 宿主 inline(class_code→std_check_class 已解析)→ 共享字典翻
fileUrl: file_url # OSS 相对路径(passthrough) fileUrl: file_url # OSS 相对路径(passthrough)
fileName: file_name fileName: file_name
......
# patient — 主档 upsert(不进 transaction,无 emits) # patient — 主档 upsert(不进 transaction,无 emits)
# 源:customer.customer_basic_info(原始表);phone 由 manifest.transforms.lookup # 源:customer.customer_basic_info;phone / phone_relationship 由宿主导出时挑默认号 inline 进主档
# 从 customer_contacts 挑默认号附加进来,故这里直接映射标量 phone # (不再 PAC 侧 lookup customer_contacts),故这里直接映射标量
canonical: patient canonical: patient
primary: primary:
...@@ -10,7 +10,7 @@ primary: ...@@ -10,7 +10,7 @@ primary:
field_mapping: field_mapping:
externalId: id # customer_basic_info 主键 = host 内部患者唯一 id(→ String 化) externalId: id # customer_basic_info 主键 = host 内部患者唯一 id(→ String 化)
name: name name: name
phone: phone # 由 transforms.lookup 从 customer_contacts 挑默认号附加 phone: phone # 宿主 inline(默认号:非空→is_default↓→contacts_type↑→id↑)
# 该号关系码(lookup 同源带出)→ preferences.contactPhone{relationshipCode, relationship, isSelf}。 # 该号关系码(lookup 同源带出)→ preferences.contactPhone{relationshipCode, relationship, isSelf}。
# 官方枚举(源码实锤 friday-saas/customer …/enums/PhoneRelationshipEnum.java,2026-07-20): # 官方枚举(源码实锤 friday-saas/customer …/enums/PhoneRelationshipEnum.java,2026-07-20):
# 1本人 2爸爸 3妈妈 4爷爷 5奶奶 6朋友 7配偶 8子女 9其他(语义=持号人是患者的X) # 1本人 2爸爸 3妈妈 4爷爷 5奶奶 6朋友 7配偶 8子女 9其他(语义=持号人是患者的X)
......
...@@ -26,7 +26,8 @@ field_mapping: ...@@ -26,7 +26,8 @@ field_mapping:
# 应收降级 receivableAmount(折扣率分母/权益客识别)。 # 应收降级 receivableAmount(折扣率分母/权益客识别)。
amount: net_receipts_this amount: net_receipts_this
receivableAmount: receivable_this receivableAmount: receivable_this
method: method # lookup 主导支付通道(现金/微信/刷卡/医保…原样入 fact) # method 暂不接:支付通道明细(settlement_modes)不摄入,金额取结算头 net_receipts_this。
# 将来要"支付方式分析"(医保占比/分次支付)再摄入 settlement_modes、恢复 method。
doctorId: doctor_id doctorId: doctor_id
encounterExternalId: registration_id # 关联接诊 encounterExternalId: registration_id # 关联接诊
settlementSerialNum: settlement_serial_num # 结算流水号(passthrough,对账用) settlementSerialNum: settlement_serial_num # 结算流水号(passthrough,对账用)
# treatment_planned — FRIDAY 治疗计划(customer_treat_plan + _item 独立表;用户确认此表为治疗计划) # treatment_planned — FRIDAY 治疗计划(消费 customer_treat_plan_item;头属性 organization_id/plan_name 由宿主 inline)
# 消费 transforms 输出表 treatment_planned_rows(行表 lookup 头表拿 organization_id/plan_name) # 消费 transforms 输出表 treatment_planned_rows(item 过滤无诊所行 + normalize 术式名)
# 行粒度 = 计划行项(牙位 + 术式 mode_name + 价格区间);externalId = 行表主键(host 稳定) # 行粒度 = 计划行项(牙位 + 术式 mode_name + 价格区间);externalId = 行表主键(host 稳定)
canonical: treatment canonical: treatment
...@@ -19,7 +19,7 @@ field_mapping: ...@@ -19,7 +19,7 @@ field_mapping:
updatedAt: updated_gmt_at updatedAt: updated_gmt_at
createdAt: created_gmt_at createdAt: created_gmt_at
patientExternalId: customer_id patientExternalId: customer_id
clinicId: organization_id # lookup 自头表(行表无诊所) clinicId: organization_id # 宿主 inline 自计划头(行表原生无诊所)
occurredAt: created_gmt_at # 计划制定时点 occurredAt: created_gmt_at # 计划制定时点
category: mode_norm # 归一术式名 → 共享字典翻 11 类闭集 category: mode_norm # 归一术式名 → 共享字典翻 11 类闭集
subtype: mode_name # 原始术式名 subtype: mode_name # 原始术式名
......
#!/usr/bin/env bash #!/usr/bin/env bash
# FRIDAY SaaS 测试环境 → PAC cold-import 导出(等价"host 侧单表 dump") # FRIDAY SaaS 测试环境 → PAC cold-import 导出(host 侧「自洽业务表」参考实现)
# #
# 纪律(同 jvs-dw SQL 朴素化):每条导出 = 列选 + WHERE,零 join/零变换; # 纪律(2026-07 对齐,契约见 docs/integration/friday-push-payload):
# (2026-07 对齐:一列都不改名 —— 宿主原生字段直出;与 PAC 租户概念的区分由 PAC 适配层消化。) # - 业务 WHERE / status 切分 / K 码截取 / 数组拆行 → 全在 PAC transforms 层(不在导出侧)。
# 形态改造(拆 JSON 数组/字典 join/K 码截取)全部在 PAC transforms 层做。 # - 但**宿主自己的引用**在导出时 within-host join 后 inline 进相关行(PAC 无从做,数据在宿主手里):
# 私有字典码(diag linkCode→std_code、影像 class_code→类型名)、1:N 派生(默认联系号+归属)、
# 头-行属性(计划头 诊所/方案名 下放到计划行)。故不再导出纯字典表/联系方式/计划头/支付通道。
# - 列名不改(宿主原生字段直出);inline 新增列用契约约定名(phone/phone_relationship/class_name/…)。
# #
# 用法(cohort 过滤,语义与 jvs-dw cold-import --clinics/--since 完全一致): # 用法(cohort 过滤,语义与 jvs-dw cold-import --clinics/--since 完全一致):
# ./export.sh # 全量(默认行为不变) # ./export.sh # 全量(默认行为不变)
...@@ -123,30 +126,24 @@ mysql_csv() { ...@@ -123,30 +126,24 @@ mysql_csv() {
} }
echo "── MySQL 导出 ──" echo "── MySQL 导出 ──"
# 患者主档(全量;既有 450 行样本 → 扩全量,EMR 覆盖 ~1.65 万患者的前置) # ⭐ 自洽形态:宿主 within-host join 把私有字典码 / 1:N 默认号 / 计划头属性 inline 进相关表
mysql_csv "SELECT id,name,sex,birthday,file_number,tenant_id,organization_id,created_gmt_at,updated_gmt_at FROM customer.customer_basic_info $(pf id)" customer_basic_info.csv # (契约见 docs/integration/friday-push-payload)。纯字典表 / 联系方式 1:N / 计划头 / 支付通道
# 联系方式 1:N(lookup 挑默认号 → patient.phone) # 不再单独导出成文件——PAC 只摄入自洽业务行(所有摄入路径同形态)。
mysql_csv "SELECT id,customer_id,contacts_tel,is_default,contacts_type,tel_type,relationship FROM customer.customer_contacts $(pf customer_id)" customer_contacts.csv # 患者主档 + inline 默认联系号(挑:非空号 → is_default↓ → contacts_type↑ → id↑;phone 与归属同源同一条)
mysql_csv "SELECT cbi.id,cbi.name,cbi.sex,cbi.birthday,cbi.file_number,cbi.tenant_id,cbi.organization_id,cbi.created_gmt_at,cbi.updated_gmt_at,
(SELECT c.contacts_tel FROM customer.customer_contacts c WHERE c.customer_id=cbi.id AND c.contacts_tel<>'' ORDER BY c.is_default DESC,c.contacts_type ASC,c.id ASC LIMIT 1) AS phone,
(SELECT c.relationship FROM customer.customer_contacts c WHERE c.customer_id=cbi.id AND c.contacts_tel<>'' ORDER BY c.is_default DESC,c.contacts_type ASC,c.id ASC LIMIT 1) AS phone_relationship
FROM customer.customer_basic_info cbi $(pf cbi.id)" customer_basic_info.csv
# 预约(全状态;transforms 丢草稿 10) # 预约(全状态;transforms 丢草稿 10)
mysql_csv "SELECT id,patient_appointment_id,organization_id,tenant_id,appointment_date,appointment_start,appointment_status,doctor_user_id,appointment_time_length,in_time,created_gmt_at,updated_gmt_at FROM \`arrail-appointment-server\`.appointment_base $(pf patient_appointment_id)" appointment_base.csv mysql_csv "SELECT id,patient_appointment_id,organization_id,tenant_id,appointment_date,appointment_start,appointment_status,doctor_user_id,appointment_time_length,in_time,created_gmt_at,updated_gmt_at FROM \`arrail-appointment-server\`.appointment_base $(pf patient_appointment_id)" appointment_base.csv
# 影像档案(挂 emr_id 病历号;modality 经 std_check_class 字典翻) # 影像档案 + inline class_name(class_code→std_check_class,宿主解析;PAC 再翻 modality)
mysql_csv "SELECT id,tenant_id,organization_id,organization_name,patient_id,emr_id,class_code,file_name,file_type,file_url,files_size,shooting_time,created_gmt_at,updated_gmt_at FROM emr.med_check $(pf patient_id)" med_check.csv mysql_csv "SELECT mc.id,mc.tenant_id,mc.organization_id,mc.organization_name,mc.patient_id,mc.emr_id,mc.class_code,sc.class_name,mc.file_name,mc.file_type,mc.file_url,mc.files_size,mc.shooting_time,mc.created_gmt_at,mc.updated_gmt_at FROM emr.med_check mc LEFT JOIN emr.std_check_class sc ON sc.class_code=mc.class_code $(pf mc.patient_id)" med_check.csv
# 影像类型字典(class_code UUID 全局唯一,lookup 不需 tenant 限定) # 治疗计划行 + inline 头属性(organization_id/plan_name 取自计划头;头不单独导出)
mysql_csv "SELECT class_code,class_name,tenant_id FROM emr.std_check_class" std_check_class.csv mysql_csv "SELECT it.id,it.tenant_id,it.customer_id,it.treat_plan_id,it.tooth_position,it.mode_name,it.charge_min,it.charge_max,it.created_gmt_at,it.updated_gmt_at,h.organization_id,h.plan_name FROM customer.customer_treat_plan_item it LEFT JOIN customer.customer_treat_plan h ON h.id=it.treat_plan_id $(pf it.customer_id)" customer_treat_plan_item.csv
# 诊断字典(linkCode → std_code;K 码用,非 K 落自由文本) # 结算头单原表(全 status,导出侧不做业务 WHERE;消费/退费切分全在 PAC)
mysql_csv "SELECT diag_code,diag_name,std_code FROM emr.std_diag" std_diag.csv
# 治疗计划(头:方案名/期望;行:牙位/术式/价格区间)→ treatment_planned
mysql_csv "SELECT id,tenant_id,organization_id,customer_id,plan_group_id,plan_name,desired_effect,emergency_urgency,expect_cost,created_gmt_at,updated_gmt_at FROM customer.customer_treat_plan $(pf customer_id)" customer_treat_plan.csv
mysql_csv "SELECT id,tenant_id,customer_id,treat_plan_id,tooth_position,mode_name,charge_min,charge_max,created_gmt_at,updated_gmt_at FROM customer.customer_treat_plan_item $(pf customer_id)" customer_treat_plan_item.csv
# 结算(消费/退费)。status 语义(2026-07-18 实测):1=已结算 3=含退费行的单 4=整单反向冲减(负额+ref 挂原单)。
# ⚠️ 2ac96f6d 品牌不用 status=4:整单冲减直接记 status=3 负金额(实测 200 行)→ 按金额正负切分:
# 消费 = status∈{1,3} 且金额≥0;整单退费 = status=4 或 status=3 负金额(退费第三表达)。
# 其余 status(0 草稿/2/5/6/7/8)语义未明,先不收 → WHERE 收窄(host 等价 dump 允许的过滤)
mysql_csv "SELECT uuid,tenant_id,organization_id,patient_id,doctor_id,status,receivable_this,net_receipts_this,billing_date,registration_id,ref_settlement_id,settlement_serial_num,reason,created_gmt_at,updated_gmt_at FROM \`arrail-settlement-server\`.patient_settlement $(pf patient_id)" patient_settlement.csv mysql_csv "SELECT uuid,tenant_id,organization_id,patient_id,doctor_id,status,receivable_this,net_receipts_this,billing_date,registration_id,ref_settlement_id,settlement_serial_num,reason,created_gmt_at,updated_gmt_at FROM \`arrail-settlement-server\`.patient_settlement $(pf patient_id)" patient_settlement.csv
# 退费行级明细(部分退费轨道:is_refund=1 全部归属 status=3 头单) # 结算明细原表(全量含 is_refund,PAC 侧切退费明细)。按 settlement_id 挂结算头(spec.patient_id 部分品牌是本地 id,不可信)
mysql_csv "SELECT id,tenant_id,organization_id,patient_id,settlement_id,cure_name,service_project_name,receivable_this,net_receipts_this,is_refund,created_gmt_at,updated_gmt_at FROM \`arrail-settlement-server\`.patient_settlement_spec WHERE is_refund=1 $(pfa patient_id)" patient_settlement_spec_refund.csv mysql_csv "SELECT id,tenant_id,organization_id,patient_id,settlement_id,cure_name,service_project_name,receivable_this,net_receipts_this,is_refund,created_gmt_at,updated_gmt_at FROM \`arrail-settlement-server\`.patient_settlement_spec $(pfs)" patient_settlement_spec.csv
# 支付通道长表(每单×通道一行;lookup 挑金额最大者为主导通道)
mysql_csv "SELECT id,tenant_id,settlement_id,modes_name,money,created_gmt_at,updated_gmt_at FROM \`arrail-settlement-server\`.settlement_modes $(pfs)" settlement_modes.csv
# 转介绍圈(患者-患者关系边,双向成对存;referee_relationship 为宿主字典 id,PAC 侧统计推断解码) # 转介绍圈(患者-患者关系边,双向成对存;referee_relationship 为宿主字典 id,PAC 侧统计推断解码)
mysql_csv "SELECT id,tenant_id,organization_id,customer_id,referee_patient_id,referee_relationship,type,created_gmt_at,updated_gmt_at FROM customer.customer_referee_circle $(pf customer_id)" customer_referee_circle.csv mysql_csv "SELECT id,tenant_id,organization_id,customer_id,referee_patient_id,referee_relationship,type,created_gmt_at,updated_gmt_at FROM customer.customer_referee_circle $(pf customer_id)" customer_referee_circle.csv
# 咨询(意向 potential_treatment/未成交原因;测试库仅 45 行,结构真实) # 咨询(意向 potential_treatment/未成交原因;测试库仅 45 行,结构真实)
...@@ -162,9 +159,22 @@ echo "── Mongo 导出(med_emr_info,status∈{3,4} 正式病历)──" ...@@ -162,9 +159,22 @@ echo "── Mongo 导出(med_emr_info,status∈{3,4} 正式病历)──"
# 按真 UTC 解释则一半病历落深夜——荒谬)。故导出**不能** toISOString 带 Z(会被 PAC 当真 # 按真 UTC 解释则一半病历落深夜——荒谬)。故导出**不能** toISOString 带 Z(会被 PAC 当真
# UTC 再 +8,EMR 链时间全偏 8 小时,已踩):把 UTC 字段值直读为墙钟,输出 naive # UTC 再 +8,EMR 链时间全偏 8 小时,已踩):把 UTC 字段值直读为墙钟,输出 naive
# "YYYY-MM-DD HH:mm:ss",交给 manifest timezone=Asia/Shanghai 解释 → 瞬间正确。 # "YYYY-MM-DD HH:mm:ss",交给 manifest timezone=Asia/Shanghai 解释 → 瞬间正确。
# 诊断字典 map(diag_code→std_code)—— diag[] inline stdCode 的 within-host join;linkCode 在 Mongo、
# 字典在 MySQL,故预取成 map 挂进 mongo 容器,eval 里逐元素解析(不再 PAC 侧 lookup std_diag)。
printf 'SELECT diag_code,std_code FROM emr.std_diag\n' | docker run -i --rm mysql:8 mysql \
-h"$FRIDAY_MYSQL_HOST" -P"$FRIDAY_MYSQL_PORT" -u"$FRIDAY_MYSQL_USER" -p"$FRIDAY_MYSQL_PASSWORD" \
--default-character-set=utf8mb4 --batch --raw=FALSE --connect-timeout=20 \
| node -e '
const lines = require("fs").readFileSync(0, "utf-8").split("\n"); const m = {};
for (let i = 1; i < lines.length; i++) { const l = lines[i]; if (!l) continue; const t = l.split("\t");
if (t[0] && t[0] !== "NULL") m[t[0]] = (t[1] && t[1] !== "NULL") ? t[1] : ""; }
process.stdout.write(JSON.stringify(m));
' > std_diag_map.json
echo " std_diag_map.json: $(node -e 'process.stdout.write(String(Object.keys(JSON.parse(require("fs").readFileSync("std_diag_map.json","utf-8"))).length))') 码"
# cohort 模式:挂载 id 文件供 $in 过滤(bash 3.2 + set -u 下空数组要用 ${arr[@]+...} 展开) # cohort 模式:挂载 id 文件供 $in 过滤(bash 3.2 + set -u 下空数组要用 ${arr[@]+...} 展开)
MONGO_ARGS=() MONGO_ARGS=(-v "$PWD/std_diag_map.json:/std_diag_map.json:ro")
if [ -n "$COHORT_IN" ]; then MONGO_ARGS=(-v "$PWD/cohort_ids.json:/cohort_ids.json:ro" -e "COHORT=1"); fi if [ -n "$COHORT_IN" ]; then MONGO_ARGS+=(-v "$PWD/cohort_ids.json:/cohort_ids.json:ro" -e "COHORT=1"); fi
docker run --rm ${MONGO_ARGS[@]+"${MONGO_ARGS[@]}"} mongo:7 mongosh "$FRIDAY_MONGO_URI" --quiet --eval ' docker run --rm ${MONGO_ARGS[@]+"${MONGO_ARGS[@]}"} mongo:7 mongosh "$FRIDAY_MONGO_URI" --quiet --eval '
const flat = (v) => v instanceof Date ? v.toISOString().slice(0, 19).replace("T", " ") const flat = (v) => v instanceof Date ? v.toISOString().slice(0, 19).replace("T", " ")
: (v && v.constructor && v.constructor.name === "Long") ? String(v) : (v && v.constructor && v.constructor.name === "Long") ? String(v)
...@@ -172,6 +182,8 @@ docker run --rm ${MONGO_ARGS[@]+"${MONGO_ARGS[@]}"} mongo:7 mongosh "$FRIDAY_MON ...@@ -172,6 +182,8 @@ docker run --rm ${MONGO_ARGS[@]+"${MONGO_ARGS[@]}"} mongo:7 mongosh "$FRIDAY_MON
// id 类字段字符串化:Mongo 存的是 number/Long 混型,canonical 契约要 string // id 类字段字符串化:Mongo 存的是 number/Long 混型,canonical 契约要 string
// (CSV 源天然全字符串;JSON 导出须显式对齐,否则 patientExternalId 等校验失败) // (CSV 源天然全字符串;JSON 导出须显式对齐,否则 patientExternalId 等校验失败)
const sid = (v) => (v === undefined || v === null || v === "") ? null : String(flat(v)); const sid = (v) => (v === undefined || v === null || v === "") ? null : String(flat(v));
// 诊断字典 map(within-host join):diag[].linkCode → std_code inline 进元素 stdCode
let DIAG = {}; try { DIAG = JSON.parse(require("fs").readFileSync("/std_diag_map.json", "utf-8")); } catch (e) {}
const q = { status: { $in: [3, 4] } }; const q = { status: { $in: [3, 4] } };
try { try {
// cohort 激活但文件读不到 → 硬失败(宁可让 .tmp 校验拒绝,不能静默退化成全量导出) // cohort 激活但文件读不到 → 硬失败(宁可让 .tmp 校验拒绝,不能静默退化成全量导出)
...@@ -189,7 +201,10 @@ docker run --rm ${MONGO_ARGS[@]+"${MONGO_ARGS[@]}"} mongo:7 mongosh "$FRIDAY_MON ...@@ -189,7 +201,10 @@ docker run --rm ${MONGO_ARGS[@]+"${MONGO_ARGS[@]}"} mongo:7 mongosh "$FRIDAY_MON
organization_name: flat(d.organizationName), // 诊所名(clinic_directory 派生 host.clinicNames) organization_name: flat(d.organizationName), // 诊所名(clinic_directory 派生 host.clinicNames)
patient_id: sid(d.patientId), user_id: sid(d.userId), user_name: flat(d.userName), patient_id: sid(d.patientId), user_id: sid(d.userId), user_name: flat(d.userName),
clinic_time: flat(d.clinicTime), visit_indicator: flat(d.visitIndicator), status: flat(d.status), clinic_time: flat(d.clinicTime), visit_indicator: flat(d.visitIndicator), status: flat(d.status),
diag: d.diag ?? [], treat: d.treat ?? [], dispose: d.dispose ?? [], examine: d.examine ?? [], // diag[] inline stdCode(宿主 within-host join);treat/dispose/examine 原生数组原样
diag: (d.diag ?? []).map((el) => Object.assign({}, el,
{ stdCode: (el && el.linkCode != null && el.linkCode !== "") ? (DIAG[String(el.linkCode)] ?? "") : "" })),
treat: d.treat ?? [], dispose: d.dispose ?? [], examine: d.examine ?? [],
// emr_record 自由文本位要字符串(fact 层 zod nullableString);数组原样给 split_json_array 拆行 // emr_record 自由文本位要字符串(fact 层 zod nullableString);数组原样给 split_json_array 拆行
dispose_text: (d.dispose ?? []).length ? JSON.stringify(d.dispose) : null, dispose_text: (d.dispose ?? []).length ? JSON.stringify(d.dispose) : null,
examine_text: (d.examine ?? []).length ? JSON.stringify(d.examine) : null, examine_text: (d.examine ?? []).length ? JSON.stringify(d.examine) : null,
......
...@@ -47,55 +47,32 @@ incremental: ...@@ -47,55 +47,32 @@ incremental:
med_emr_info: { cursor_column: updated_gmt_at } med_emr_info: { cursor_column: updated_gmt_at }
med_check: { cursor_column: updated_gmt_at } med_check: { cursor_column: updated_gmt_at }
patient_settlement: { cursor_column: updated_gmt_at } patient_settlement: { cursor_column: updated_gmt_at }
patient_settlement_refund: { cursor_column: updated_gmt_at } patient_settlement_spec: { cursor_column: updated_gmt_at }
patient_settlement_spec_refund: { cursor_column: updated_gmt_at }
settlement_modes: { cursor_column: updated_gmt_at }
customer_treat_plan: { cursor_column: updated_gmt_at }
customer_treat_plan_item: { cursor_column: updated_gmt_at } customer_treat_plan_item: { cursor_column: updated_gmt_at }
customer_referee_circle: { cursor_column: updated_gmt_at } customer_referee_circle: { cursor_column: updated_gmt_at }
customer_consult: { cursor_column: updated_gmt_at } customer_consult: { cursor_column: updated_gmt_at }
# ⭐ 宿主推「自洽的业务表」:私有字典码 / 1:N 派生 / 头-行属性 都由宿主 within-host join 后 inline 进相关行
# (契约见 docs/integration/friday-push-payload)。故不再摄入纯字典表(std_diag/std_check_class)、
# 联系方式 1:N(customer_contacts→inline 默认号进主档)、计划头(customer_treat_plan→inline 进行)、
# 支付通道(settlement_modes,金额已在结算头 net_receipts_this)。所有摄入路径(push/file/pull/reparse)同此形态。
tables: tables:
- { table: customer_basic_info, file: customer_basic_info.csv } # 患者主档(原始) - { table: customer_basic_info, file: customer_basic_info.csv } # 患者主档(含 inline phone/phone_relationship)
- { table: customer_contacts, file: customer_contacts.csv } # 联系方式 1:N(原始)
- { table: appointment_base, file: appointment_base.csv } # 预约(原始,全状态) - { table: appointment_base, file: appointment_base.csv } # 预约(原始,全状态)
# ── EMR 病历链(W FRIDAY-EMR:Mongo med_emr_info status∈{3,4} 正式病历;export.sh 平铺导出)── # ── EMR 病历链(W FRIDAY-EMR:Mongo med_emr_info status∈{3,4} 正式病历;export.sh 平铺导出)──
# 数组字段(diag/treat/dispose/examine)导出保留原生数组(split_json_array 原生支持); # diag[] 元素含 inline stdCode(宿主解析 linkCode→std_diag.std_code);treat/dispose/examine 原生数组。
# dispose_text/examine_text 是同数据的 JSON 字符串形态(emr_record 自由文本位要 string)。
- { table: med_emr_info, file: med_emr_info.json } # 病历正文(Mongo,含 diag/treat 数组) - { table: med_emr_info, file: med_emr_info.json } # 病历正文(Mongo,含 diag/treat 数组)
- { table: med_check, file: med_check.csv } # 影像档案(挂 emr_id 病历号) - { table: med_check, file: med_check.csv } # 影像档案(含 inline class_name;挂 emr_id)
- { table: std_check_class, file: std_check_class.csv } # 影像类型字典(class_code→class_name) - { table: customer_treat_plan_item, file: customer_treat_plan_item.csv } # 治疗计划行(含 inline organization_id/plan_name)
- { table: std_diag, file: std_diag.csv } # 诊断字典(linkCode→std_code;K 码用)
# 治疗计划 = 头+行强成组(item 是 primary,lookup 头表拿诊所/方案名;缺任一 → planned 产 0 行)。
# group 只约束网页上传(要么全在要么整组跳);CLI 全量不受此限。
- { table: customer_treat_plan, file: customer_treat_plan.csv, group: treatment_plan } # 治疗计划头(方案名/期望)
- { table: customer_treat_plan_item, file: customer_treat_plan_item.csv, group: treatment_plan } # 治疗计划行(牙位/术式/价格)
# 结算原表(全 status,导出侧不做业务 WHERE)—— 消费/退费/明细全部由 PAC transforms 切分(单一真理源) # 结算原表(全 status,导出侧不做业务 WHERE)—— 消费/退费/明细全部由 PAC transforms 切分(单一真理源)
- { table: patient_settlement, file: patient_settlement.csv } # 结算头单原表(全 status) - { table: patient_settlement, file: patient_settlement.csv } # 结算头单原表(全 status)
- { table: patient_settlement_spec, file: patient_settlement_spec.csv } # 结算明细原表(全量,含 is_refund) - { table: patient_settlement_spec, file: patient_settlement_spec.csv } # 结算明细原表(全量,含 is_refund)
- { table: settlement_modes, file: settlement_modes.csv } # 支付通道长表
- { table: customer_referee_circle, file: customer_referee_circle.csv } # 转介绍圈(患者-患者边) - { table: customer_referee_circle, file: customer_referee_circle.csv } # 转介绍圈(患者-患者边)
- { table: customer_consult, file: customer_consult.csv } # 咨询(意向/未成交原因) - { table: customer_consult, file: customer_consult.csv } # 咨询(意向/未成交原因)
transforms: transforms:
# ── 患者:把 1:N 联系方式归约成 patient.phone 标量(无需源侧 join)── # ── 患者 phone:宿主已按「默认号→本人→最早」挑好、inline 进 customer_basic_info 的
- kind: lookup # phone / phone_relationship 两列(不再 lookup customer_contacts)。patient.yaml 直接读。──
input: customer_basic_info
output: customer_basic_info # in-place 附加 phone 列
from: customer_contacts
left_key: id # 患者主键
right_key: customer_id # 联系方式指向患者的外键
where:
contacts_tel: { not_empty: true } # 空号候选剔除
order_by: # 挑一条:默认号 → 本人(type=1) → 最早 id
- { field: is_default, dir: desc }
- { field: contacts_type, dir: asc }
- { field: id, dir: asc }
select:
phone: contacts_tel # patient.phone ← 选中候选的 contacts_tel
# 该号与患者的关系码(host sys_dict 编码;实测 1=本人 78.7%(成人),儿童过半非本人号)。
# 先落原码(preferences.contactPhone,isSelf=码=='1');完整标签字典待 sys_dict 可读后补。
phone_relationship: relationship
# ── 预约:丢草稿 → 全部预约(拼 scheduledAt)+ 已到诊拆成接诊事件(→ encounter)── # ── 预约:丢草稿 → 全部预约(拼 scheduledAt)+ 已到诊拆成接诊事件(→ encounter)──
# 与 jvs-dw 同款设计:一个预约源喂两张 canonical(appointment / encounter)。 # 与 jvs-dw 同款设计:一个预约源喂两张 canonical(appointment / encounter)。
...@@ -154,19 +131,9 @@ transforms: ...@@ -154,19 +131,9 @@ transforms:
element_fields: element_fields:
value: value value: value
tooth_position: toothPosition tooth_position: toothPosition
link_code: linkCode std_code: stdCode # 宿主已解析 linkCode→std_diag.std_code、inline 进元素(不再 PAC 侧 lookup)
where: where:
value: { not_empty: true } # 只要求有诊断名;linkCode 可空(仅 0.3% 有) value: { not_empty: true } # 只要求有诊断名;stdCode 可空(非 K/未解析 → 走自由文本)
# E.2.1 linkCode → std_diag 字典(diag_code UUID 全局唯一,LEFT JOIN 未命中 → std_code=null)
- kind: lookup
input: _diag_raw
output: _diag_raw # in-place 附加 std_code
from: std_diag
left_key: link_code
right_key: diag_code
select:
std_code: std_code
# E.2.2 归一 + K 码截取 + coalesce # E.2.2 归一 + K 码截取 + coalesce
# 规则(用户定):std_code K 开头 → 截 K 大类;非 K(DA0E.50 等 ICD-11)/join 不上/linkCode 空 # 规则(用户定):std_code K 开头 → 截 K 大类;非 K(DA0E.50 等 ICD-11)/join 不上/linkCode 空
...@@ -305,30 +272,13 @@ transforms: ...@@ -305,30 +272,13 @@ transforms:
op: concat op: concat
parts: ['${emr_sub_id}', '|rec|', '${value}', '|', '${tooth_position}'] parts: ['${emr_sub_id}', '|rec|', '${value}', '|', '${tooth_position}']
# ── E.4 影像:med_check + 类型字典 → class_name(modality 经共享字典翻)── # ── E.4 影像:med_check 已含 inline class_name(宿主解析 class_code→std_check_class);
- kind: lookup # image.yaml 直接以 med_check 为 primary、读 class_name 翻 modality(不再 PAC 侧 lookup)。──
input: med_check
output: image_rows
from: std_check_class
left_key: class_code
right_key: class_code
select:
class_name: class_name
# ── E.5 治疗计划:行表 join 头表拿 诊所/方案名 → planned 候选 ── # ── E.5 治疗计划:计划行已含 inline organization_id/plan_name(宿主取自计划头);
- kind: lookup # 仅丢弃无诊所归属的行(canonical clinicId 必填;头无诊所 → 该行产 failed 噪音,提前剔除)。──
input: customer_treat_plan_item
output: _plan_item_joined
from: customer_treat_plan
left_key: treat_plan_id
right_key: id
select:
organization_id: organization_id
plan_name: plan_name
# 头表 lookup 未命中 / 头表无诊所 → 计划行无法归属诊所,显式丢弃(canonical clinicId 必填;
# 留着只会变 failed 噪音。测试库实测 75/187 行如此)
- kind: filter - kind: filter
input: _plan_item_joined input: customer_treat_plan_item
output: _plan_item_with_org output: _plan_item_with_org
where: where:
organization_id: { not_empty: true } organization_id: { not_empty: true }
...@@ -345,23 +295,14 @@ transforms: ...@@ -345,23 +295,14 @@ transforms:
# 消费 = status∈{1,3};退费双轨 = ①status=4 整单(ref_settlement_id 挂原单)②spec is_refund=1 行级。 # 消费 = status∈{1,3};退费双轨 = ①status=4 整单(ref_settlement_id 挂原单)②spec is_refund=1 行级。
# LTV = Σpayment − Σrefund 自然对账;不从 settlement 反推治疗(财务≠临床,jvs-dw 已验证的教训)。 # LTV = Σpayment − Σrefund 自然对账;不从 settlement 反推治疗(财务≠临床,jvs-dw 已验证的教训)。
# ── S.1 消费:status 1/3 → 主导支付通道 lookup(金额最大者)── # ── S.1 消费:status∈{1,3} 且应收≥0 → payment(金额取结算头 net_receipts_this)。
# 支付通道(settlement_modes)暂不摄入 → payment 无 method(总值在结算头,无需通道拆分)。──
- kind: filter - kind: filter
input: patient_settlement input: patient_settlement
output: _pay_hdr output: payment_rows
where: where:
status: { in: ['1', '3'] } status: { in: ['1', '3'] }
receivable_this: { gte: 0 } # 应收≥0(迁自导出侧 WHERE;数值算子) receivable_this: { gte: 0 } # 应收≥0(迁自导出侧 WHERE;数值算子)
- kind: lookup
input: _pay_hdr
output: payment_rows
from: settlement_modes
left_key: uuid
right_key: settlement_id
order_by:
- { field: money, dir: desc }
select:
method: modes_name # 主导支付通道(现金/微信/刷卡/医保…干净中文名,原样入 fact)
# ── S.2 退费①:整单冲减 —— PAC 侧切分(不再靠导出 WHERE)。两种表达 union: # ── S.2 退费①:整单冲减 —— PAC 侧切分(不再靠导出 WHERE)。两种表达 union:
# ① status=4 整单反向;② status=3 且 receivable_this<0(2ac96f6d 品牌用负额表达退费)。 # ① status=4 整单反向;② status=3 且 receivable_this<0(2ac96f6d 品牌用负额表达退费)。
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment