Commit cb2d0a5f by luoqi

feat(tenant): P0-1 患者 source_unit 列 + 唯一键(结构地基,行为不变)

- patients.source_unit(通用命名空间,NOT NULL 默认 ''):各宿主"在哪层发患者号"
  不同,jvs-dw=品牌;'' 非 null 防唯一索引 NULL-distinct 失效
- 唯一键 (host,tenant,external_id) → (host,tenant,source_unit,external_id);
  存量 source_unit='' 常量,新键与旧键唯一性等价,迁移安全(95k 患者完整)
- 3 处患者 upsert + 2 CLI 改新键(过渡用 sourceUnit:'';stub 与真主档同 '' 不双行)
- patientReturnVisit 的 (host,tenant,external_id) 键未动(只改 patients)

下一步:source_unit 真填值(assembler sourceUnit:brand)+ tenant→集团 slug remap。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
parent 12ffbfd5
-- 患者命名空间 source_unit(发证来源):给 external_id 在集团内消歧。
-- 通用字段,各宿主"在哪层发患者号"不同;单命名空间宿主留空串 ''。
-- 对存量安全:source_unit 默认 ''(常量),新唯一键 (host,tenant,'',external_id)
-- 与旧 (host,tenant,external_id) 唯一性等价,不会冲突。
-- DropIndex
DROP INDEX "patients_host_id_tenant_id_external_id_key";
-- AlterTable
ALTER TABLE "patients" ADD COLUMN "source_unit" TEXT NOT NULL DEFAULT '';
-- CreateIndex
CREATE UNIQUE INDEX "patients_host_id_tenant_id_source_unit_external_id_key" ON "patients"("host_id", "tenant_id", "source_unit", "external_id");
...@@ -166,8 +166,14 @@ model Patient { ...@@ -166,8 +166,14 @@ model Patient {
id String @id @default(uuid()) @db.Uuid id String @id @default(uuid()) @db.Uuid
/// 接入宿主(FK hosts.id) /// 接入宿主(FK hosts.id)
hostId String @map("host_id") @db.Uuid hostId String @map("host_id") @db.Uuid
/// 宿主侧的客户/集团 id(来自换票 token;PAC 不维护 tenants 主数据) /// 集团 id(= PAC 租户;稳定 slug, ruier-grp)。来自换票 token;PAC 不维护 tenants 主数据
tenantId String @map("tenant_id") tenantId String @map("tenant_id")
/// 源组织命名空间(发证来源): external_id 在集团内消歧用。
/// 通用字段 各宿主"在哪一层发患者号"不同:jvs-dw=品牌(瑞尔/瑞泰),别的 host 可能=区域/诊所/无。
/// manifest identity_namespace_field 声明取哪个源字段填;**单命名空间宿主留空串 ''**( null:
/// null 在唯一索引里算 distinct 会让去重失效;'' 是具体值,唯一键对所有宿主都稳)
/// 语义="发证命名空间"(历史不可变),"当前品牌"(当前归属由组织树解析)
sourceUnit String @default("") @map("source_unit")
/// 宿主侧患者 id,回链用 /// 宿主侧患者 id,回链用
externalId String @map("external_id") externalId String @map("external_id")
...@@ -211,8 +217,8 @@ model Patient { ...@@ -211,8 +217,8 @@ model Patient {
/// 诊所回访任务记录(展示用,5 试点;非召回信号) /// 诊所回访任务记录(展示用,5 试点;非召回信号)
returnVisits PatientReturnVisit[] returnVisits PatientReturnVisit[]
/// 跨同步 upsert 去重:同一物理人在集团内共享一行 /// 跨同步 upsert 去重 + 集团内 external_id 撞号消歧(同号不同源 = 不同人, source_unit 分开)
@@unique([hostId, tenantId, externalId]) @@unique([hostId, tenantId, sourceUnit, externalId])
@@index([hostId, tenantId]) @@index([hostId, tenantId])
@@index([phone]) @@index([phone])
/// 召回池热查询路径(active = 是否活跃);合规过滤走 PatientProfile join /// 召回池热查询路径(active = 是否活跃);合规过滤走 PatientProfile join
......
...@@ -97,14 +97,9 @@ async function bootstrap() { ...@@ -97,14 +97,9 @@ async function bootstrap() {
logger.error(`host "${args.host}" not found`); logger.error(`host "${args.host}" not found`);
process.exit(2); process.exit(2);
} }
const patient = await prisma.patient.findUnique({ // findFirst:source_unit 加入唯一键后,CLI 按 externalId 查不带命名空间,用 findFirst 取一条
where: { const patient = await prisma.patient.findFirst({
hostId_tenantId_externalId: { where: { hostId: host.id, tenantId: args.tenant, externalId: args.pid },
hostId: host.id,
tenantId: args.tenant,
externalId: args.pid,
},
},
}); });
if (!patient) { if (!patient) {
logger.error(`patient externalId="${args.pid}" not found in ${args.host}/${args.tenant}`); logger.error(`patient externalId="${args.pid}" not found in ${args.host}/${args.tenant}`);
......
...@@ -108,14 +108,9 @@ async function bootstrap() { ...@@ -108,14 +108,9 @@ async function bootstrap() {
// 解析 patient.id // 解析 patient.id
let patientId = args.id; let patientId = args.id;
if (!patientId && args.pid) { if (!patientId && args.pid) {
const p = await prisma.patient.findUnique({ // findFirst:source_unit 加入唯一键后,CLI 按 externalId 查不带命名空间,用 findFirst 取一条
where: { const p = await prisma.patient.findFirst({
hostId_tenantId_externalId: { where: { hostId: host.id, tenantId: args.tenant, externalId: args.pid },
hostId: host.id,
tenantId: args.tenant,
externalId: args.pid,
},
},
}); });
if (!p) { if (!p) {
logger.error(`patient with externalId="${args.pid}" not found in ${args.host}/${args.tenant}`); logger.error(`patient with externalId="${args.pid}" not found in ${args.host}/${args.tenant}`);
......
...@@ -1313,7 +1313,8 @@ export class ColdImportService { ...@@ -1313,7 +1313,8 @@ export class ColdImportService {
() => () =>
this.prisma.patient.upsert({ this.prisma.patient.upsert({
where: { where: {
hostId_tenantId_externalId: { hostId, tenantId, externalId }, // sourceUnit:''(过渡)— 真填值随 tenant→集团 remap 一起做
hostId_tenantId_sourceUnit_externalId: { hostId, tenantId, sourceUnit: '', externalId },
}, },
create: { hostId, tenantId, externalId, ...patientData }, create: { hostId, tenantId, externalId, ...patientData },
update: patientData, update: patientData,
...@@ -1894,7 +1895,8 @@ export class ColdImportService { ...@@ -1894,7 +1895,8 @@ export class ColdImportService {
const patient = await this.withDbRetry( const patient = await this.withDbRetry(
() => () =>
this.prisma.patient.upsert({ this.prisma.patient.upsert({
where: { hostId_tenantId_externalId: { hostId, tenantId, externalId } }, // sourceUnit:''(过渡)— stub 与真主档同用 '',不产生双行;真填值随 remap 一起处理
where: { hostId_tenantId_sourceUnit_externalId: { hostId, tenantId, sourceUnit: '', externalId } },
create: { hostId, tenantId, externalId, active: true }, create: { hostId, tenantId, externalId, active: true },
update: {}, // 有则 noop(姓名等真实主档 upsert 走 processPatients 路径) update: {}, // 有则 noop(姓名等真实主档 upsert 走 processPatients 路径)
}), }),
......
...@@ -241,8 +241,9 @@ export class PipelineDispatcher { ...@@ -241,8 +241,9 @@ export class PipelineDispatcher {
row.referralAmount != null ? Math.round(Number(row.referralAmount) * 100) : null, row.referralAmount != null ? Math.round(Number(row.referralAmount) * 100) : null,
}; };
// sourceUnit:''(过渡)— 现 tenant 仍按命名空间隔离;真填值随 tenant→集团 remap 一起做
const patient = await this.prisma.patient.upsert({ const patient = await this.prisma.patient.upsert({
where: { hostId_tenantId_externalId: { hostId, tenantId, externalId } }, where: { hostId_tenantId_sourceUnit_externalId: { hostId, tenantId, sourceUnit: '', externalId } },
create: { hostId, tenantId, externalId, ...patientData }, create: { hostId, tenantId, externalId, ...patientData },
update: patientData, update: patientData,
}); });
......
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