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