Commit 1af18042 by luoqi

revert(auth): 撤下品牌名字典 — 品牌无展示需求,模拟登录显示 id 即可

品牌中文名当前只有模拟登录对话框一处会用,显示 GUID 足够;为它引入一个数据源
(tenant_apply.tenant_info)+ 一个 Host 字段 + 契约档 + CLI 分支 + session 合并,
收益不抵复杂度 → 整条链路撤回,只留诊所名(工作台真实展示位)。

- friday export.sh / manifest:删 tenant_info 导出与 brand_directory 声明
- manifest.schema:删 brand_directory;Host.sourceUnitNames 与其迁移一并删除
  (分支未合未部署,本地 DROP COLUMN 即可,无需 down 迁移)
- TokenDictionary 删 brands 档;auth.service 恢复 getClinicNames;controller 恢复原合并
- refresh-clinic-names CLI 恢复单字典派生
- listMockOrgs:品牌名直接用 source_unit id(诊所名仍派生自病历表 organization_name)
- 契约文档:15 source 复原,「留意事项」加一条说明——品牌名未冗余进业务表,
  将来若需展示再约定推主档

验证:tsc 0(svc+web);mock-orgs 19 品牌显示 id、诊所仍中文名;session 无 brands 键、
clinics 11 家照旧;jvs-dw 全程未受影响。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
parent c9429413
--- ---
title: FRIDAY 推送数据契约 title: FRIDAY 推送数据契约
description: FRIDAY SaaS 按形态 A 推送的 16 个 source 及字段定义;与已验证的存量导出形状一致,push 无缝衔接。 description: FRIDAY SaaS 按形态 A 推送的 15 个 source 及字段定义;与已验证的存量导出形状一致,push 无缝衔接。
icon: FileJson icon: FileJson
--- ---
...@@ -31,7 +31,7 @@ icon: FileJson ...@@ -31,7 +31,7 @@ icon: FileJson
--- ---
## 2. 患者与关系(4 个 source) ## 2. 患者与关系(3 个 source)
### `customer_basic_info` — 患者主档 ### `customer_basic_info` — 患者主档
...@@ -62,21 +62,6 @@ icon: FileJson ...@@ -62,21 +62,6 @@ icon: FileJson
| `tel_type` | string/number | | 号码类型 | | `tel_type` | string/number | | 号码类型 |
| `relationship` | string/number | | 持号人与患者的关系(官方枚举 `PhoneRelationshipEnum`:`1`本人 `2`爸爸 `3`妈妈 `4`爷爷 `5`奶奶 `6`朋友 `7`配偶 `8`子女 `9`其他) | | `relationship` | string/number | | 持号人与患者的关系(官方枚举 `PhoneRelationshipEnum`:`1`本人 `2`爸爸 `3`妈妈 `4`爷爷 `5`奶奶 `6`朋友 `7`配偶 `8`子女 `9`其他) |
### `tenant_info` — 品牌主档(低频;变更时全量重推)
> 品牌展示名的**唯一来源**。PAC 侧 `source_unit` 存的是品牌 GUID(宿主原生 `tenant_id`),
> 靠本表派生出中文名下发前端(否则工作台只能显示 GUID)。
>
> ℹ️ **为什么诊所名不用单独推、品牌名要?** 贵方 `organization_name` 冗余进了 21 张业务表
> (病历/影像等都自带),诊所名随业务数据白送;而 `tenant_name` 只出现在两张投诉表
> (不在本契约范围),品牌名只存于本主档 —— 故品牌需单独推这一张小表(45 行级,低频)。
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `tenant_id` | string | ✅ | 品牌 GUID(= 各业务表的 `tenant_id`) |
| `name` | string | ✅ | 品牌名称(如"济南瑞泰济东口腔医院") |
| `name_ab` | string | | 品牌缩写 |
### `customer_referee_circle` — 转介绍圈(患者-患者关系) ### `customer_referee_circle` — 转介绍圈(患者-患者关系)
| 字段 | 类型 | 必填 | 说明 | | 字段 | 类型 | 必填 | 说明 |
...@@ -304,5 +289,6 @@ icon: FileJson ...@@ -304,5 +289,6 @@ icon: FileJson
| 项 | 说明 | | 项 | 说明 |
|---|---| |---|---|
| `organization_name` 覆盖 | 目前仅病历/影像表带诊所名,**只覆盖有病历记录的诊所**;其余诊所前端回退显示 GUID。若贵方有组织树接口(如 `regional_nodes_structure`),提供 `诊所 id → 名称` 全量表可补齐 | | `organization_name` 覆盖 | 目前仅病历/影像表带诊所名,**只覆盖有病历记录的诊所**;其余诊所前端回退显示 GUID。若贵方有组织树接口(如 `regional_nodes_structure`),提供 `诊所 id → 名称` 全量表可补齐 |
| 字典表推送频率 | `std_diag` / `std_check_class` / `tenant_info` 变更不频繁,按"变更时全量重推"即可,无需增量 | | 品牌名 | PAC 当前**不需要**品牌中文名(无展示位,内部按品牌 id 标识)。将来若有展示需求,再约定推品牌主档(`tenant_apply.tenant_info` 的 `tenant_id`/`name`)——贵方 `tenant_name` 未冗余进业务表,只能走主档 |
| 字典表推送频率 | `std_diag` / `std_check_class` 变更不频繁,按"变更时全量重推"即可,无需增量 |
| 测试环境数据特征 | 部分租户为开发沙盒(诊所名如"XX专用诊所勿动"),字典类映射(治疗类别关键词等)待**生产数据**回流后再校准一轮 | | 测试环境数据特征 | 部分租户为开发沙盒(诊所名如"XX专用诊所勿动"),字典类映射(治疗类别关键词等)待**生产数据**回流后再校准一轮 |
...@@ -59,8 +59,6 @@ mysql_csv "SELECT id,patient_appointment_id,organization_id,tenant_id,appointmen ...@@ -59,8 +59,6 @@ mysql_csv "SELECT id,patient_appointment_id,organization_id,tenant_id,appointmen
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" med_check.csv 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" med_check.csv
# 影像类型字典(class_code UUID 全局唯一,lookup 不需 tenant 限定) # 影像类型字典(class_code UUID 全局唯一,lookup 不需 tenant 限定)
mysql_csv "SELECT class_code,class_name,tenant_id FROM emr.std_check_class" std_check_class.csv mysql_csv "SELECT class_code,class_name,tenant_id FROM emr.std_check_class" std_check_class.csv
# 品牌主档(apply 服务 tenant_info;brand_directory 派生 host.sourceUnitNames → 前端显示品牌中文名)
mysql_csv "SELECT tenant_id,name,name_ab FROM tenant_apply.tenant_info" tenant_info.csv
# 诊断字典(linkCode → std_code;K 码用,非 K 落自由文本) # 诊断字典(linkCode → std_code;K 码用,非 K 落自由文本)
mysql_csv "SELECT diag_code,diag_name,std_code FROM emr.std_diag" std_diag.csv mysql_csv "SELECT diag_code,diag_name,std_code FROM emr.std_diag" std_diag.csv
# 治疗计划(头:方案名/期望;行:牙位/术式/价格区间)→ treatment_planned # 治疗计划(头:方案名/期望;行:牙位/术式/价格区间)→ treatment_planned
......
...@@ -35,12 +35,6 @@ clinic_directory: ...@@ -35,12 +35,6 @@ clinic_directory:
id_field: organization_id id_field: organization_id
name_field: organization_name name_field: organization_name
# 品牌名字典来源:apply 服务 tenant_info(品牌主档)。同 CLI 一并派生 → host.sourceUnitNames,
# 经 /auth/session 下发 dictionary.brands;多品牌 SaaS 的 source_unit 是 GUID,靠它显示中文名。
brand_directory:
table: tenant_info
id_field: tenant_id
name_field: name
# ── 增量水位声明(2026-07 与 jvs-dw 统一;file 源放 manifest 顶层)── # ── 增量水位声明(2026-07 与 jvs-dw 统一;file 源放 manifest 顶层)──
# file 装载不消费水位(每轮全量装载,幂等去重兜底);每轮跑完与 jvs-dw 走同一段 finally # file 装载不消费水位(每轮全量装载,幂等去重兜底);每轮跑完与 jvs-dw 走同一段 finally
...@@ -72,7 +66,6 @@ tables: ...@@ -72,7 +66,6 @@ tables:
- { table: med_check, file: med_check.csv } # 影像档案(挂 emr_id 病历号) - { table: med_check, file: med_check.csv } # 影像档案(挂 emr_id 病历号)
- { table: std_check_class, file: std_check_class.csv } # 影像类型字典(class_code→class_name) - { table: std_check_class, file: std_check_class.csv } # 影像类型字典(class_code→class_name)
- { table: std_diag, file: std_diag.csv } # 诊断字典(linkCode→std_code;K 码用) - { table: std_diag, file: std_diag.csv } # 诊断字典(linkCode→std_code;K 码用)
- { table: tenant_info, file: tenant_info.csv } # 品牌主档(brand_directory 派生品牌名)
- { table: customer_treat_plan, file: customer_treat_plan.csv } # 治疗计划头(方案名/期望) - { table: customer_treat_plan, file: customer_treat_plan.csv } # 治疗计划头(方案名/期望)
- { table: customer_treat_plan_item, file: customer_treat_plan_item.csv } # 治疗计划行(牙位/术式/价格) - { table: customer_treat_plan_item, file: customer_treat_plan_item.csv } # 治疗计划行(牙位/术式/价格)
- { table: patient_settlement, file: patient_settlement.csv } # 消费头单(status 1/3 且金额≥0) - { table: patient_settlement, file: patient_settlement.csv } # 消费头单(status 1/3 且金额≥0)
......
-- AlterTable
ALTER TABLE "hosts" ADD COLUMN "source_unit_names" JSONB NOT NULL DEFAULT '{}';
...@@ -128,12 +128,6 @@ model Host { ...@@ -128,12 +128,6 @@ model Host {
/// 形状: { "66701845dd2342e19f9e9f576c4ffe9c": "北京朝阳公园诊所", ... } /// 形状: { "66701845dd2342e19f9e9f576c4ffe9c": "北京朝阳公园诊所", ... }
clinicNames Json @default("{}") @map("clinic_names") clinicNames Json @default("{}") @map("clinic_names")
/// 品牌名字典:source_unit id 中文名。**从摄入数据派生**(FRIDAY tenant_info.name ),
/// clinicNames 同机制, /auth/session 合并进 dictionary.brands 下发。
/// 多品牌宿主(source_unit GUID)需要它显示品牌名;jvs-dw source_unit 本身即中文名,留空即可。
/// 形状: { "3a9d9c680c364df9a84bfd5de91e9324": "XX口腔", ... }
sourceUnitNames Json @default("{}") @map("source_unit_names")
/// 执行结果回执地址(宿主接收 POST HTTPS endpoint);null = 不启用回执(opt-in) /// 执行结果回执地址(宿主接收 POST HTTPS endpoint);null = 不启用回执(opt-in)
callbackUrl String? @map("callback_url") callbackUrl String? @map("callback_url")
/// 回执验签用的 HMAC 共享密钥;PAC **签名**,故存**原文**(不同于 appSecret 存哈希) /// 回执验签用的 HMAC 共享密钥;PAC **签名**,故存**原文**(不同于 appSecret 存哈希)
......
/** /**
* refresh-clinic-names — 从摄入源派生展示名字典写入 host: * refresh-clinic-names — 从摄入源派生「诊所 id → 名字」写入 host.clinicNames。
* - `clinic_directory` → host.clinicNames (诊所 id → 名)
* - `brand_directory` → host.sourceUnitNames (品牌/source_unit id → 名;多品牌宿主用)
* *
* /auth/session 会把 host.clinicNames 合并进 dictionary.clinics 下发, * /auth/session 会把 host.clinicNames 合并进 dictionary.clinics 下发,
* 前端显示诊所名不再依赖宿主换票时传 dictionary(见 Host.clinicNames 注释)。 * 前端显示诊所名不再依赖宿主换票时传 dictionary(见 Host.clinicNames 注释)。
* *
* 源在 manifest 的 `clinic_directory` / `brand_directory: { table, id_field, name_field }` 声明: * 源在 manifest 的 `clinic_directory: { table, id_field, name_field }` 声明:
* - sql_source 模式:SELECT DISTINCT id,name FROM <table>(连 DW,env 覆盖连接) * - sql_source 模式:SELECT DISTINCT id,name FROM <table>(连 DW,env 覆盖连接)
* - 文件模式:读 tables[] 里对应 <table> 的 CSV/JSON,去重 * - 文件模式:读 tables[] 里对应 <table> 的 CSV/JSON,去重
* *
...@@ -103,25 +101,18 @@ async function main(): Promise<void> { ...@@ -103,25 +101,18 @@ async function main(): Promise<void> {
); );
const hostName = flag('host') || manifest.host_name; const hostName = flag('host') || manifest.host_name;
const cd = manifest.clinic_directory; const cd = manifest.clinic_directory;
const bd = manifest.brand_directory; if (!cd) {
if (!cd && !bd) { logger.error(`manifest 未声明 clinic_directory —— 无法派生诊所名`);
logger.error(`manifest 未声明 clinic_directory / brand_directory —— 无可派生的名字典`);
process.exit(1); process.exit(1);
} }
// 两类字典同机制派生(声明了才跑);sql_source 走 CH,文件源读 tables[] const clinicMap = manifest.sql_source
const derive = async (d: { table: string; id_field: string; name_field: string }) => ? await deriveFromClickhouse(manifest.sql_source, cd, logger)
manifest.sql_source : deriveFromFile(manifestDir, manifest.tables ?? [], cd);
? await deriveFromClickhouse(manifest.sql_source, d, logger)
: deriveFromFile(manifestDir, manifest.tables ?? [], d);
const clinicMap = cd ? await derive(cd) : {};
const brandMap = bd ? await derive(bd) : {};
const nc = Object.keys(clinicMap).length; const nc = Object.keys(clinicMap).length;
const nb = Object.keys(brandMap).length; logger.log(`派生诊所名 ${nc} 家`);
logger.log(`派生:诊所名 ${nc} 家 / 品牌名 ${nb} 个`); if (nc === 0) {
if (nc === 0 && nb === 0) { logger.warn('派生结果为空,跳过写入(不清空既有 clinicNames)');
logger.warn('派生结果全空,跳过写入(不清空既有字典)');
process.exit(0); process.exit(0);
} }
...@@ -130,18 +121,8 @@ async function main(): Promise<void> { ...@@ -130,18 +121,8 @@ async function main(): Promise<void> {
const prisma = app.get(PrismaService); const prisma = app.get(PrismaService);
const host = await prisma.host.findFirst({ where: { name: hostName } }); const host = await prisma.host.findFirst({ where: { name: hostName } });
if (!host) throw new Error(`host "${hostName}" 不存在`); if (!host) throw new Error(`host "${hostName}" 不存在`);
// 空结果不覆盖既有(单侧声明时不误清另一侧) await prisma.host.update({ where: { id: host.id }, data: { clinicNames: clinicMap } });
await prisma.host.update({
where: { id: host.id },
data: {
...(nc > 0 ? { clinicNames: clinicMap } : {}),
...(nb > 0 ? { sourceUnitNames: brandMap } : {}),
},
});
if (nc > 0)
logger.log(`✓ host="${hostName}" clinicNames ${nc} 家(样例: ${Object.values(clinicMap).slice(0, 3).join(' / ')})`); logger.log(`✓ host="${hostName}" clinicNames ${nc} 家(样例: ${Object.values(clinicMap).slice(0, 3).join(' / ')})`);
if (nb > 0)
logger.log(`✓ host="${hostName}" sourceUnitNames ${nb} 个(样例: ${Object.values(brandMap).slice(0, 3).join(' / ')})`);
} finally { } finally {
await app.close(); await app.close();
} }
......
...@@ -113,19 +113,13 @@ export class AuthController { ...@@ -113,19 +113,13 @@ export class AuthController {
) { ) {
// 诊所名字典:服务端派生(host.clinicNames)打底,宿主换票传的 dictionary.clinics 覆盖优先。 // 诊所名字典:服务端派生(host.clinicNames)打底,宿主换票传的 dictionary.clinics 覆盖优先。
// 这样前端显示诊所名不再依赖每次登录都传全 —— 服务端补齐 GUID→中文名,登录传的仅作覆盖。 // 这样前端显示诊所名不再依赖每次登录都传全 —— 服务端补齐 GUID→中文名,登录传的仅作覆盖。
const serverNames = await this.auth.getDisplayNames(user.hostId); const serverClinicNames = await this.auth.getClinicNames(user.hostId);
const loginDict = user.dictionary; const loginDict = user.dictionary;
const mergedClinics = { ...serverNames.clinics, ...(loginDict?.clinics ?? {}) }; const mergedClinics = { ...serverClinicNames, ...(loginDict?.clinics ?? {}) };
// 品牌名同机制:服务端 host.sourceUnitNames 打底,登录传的覆盖。
// 多品牌宿主(source_unit 是 GUID)靠它显示中文名;jvs-dw 的 source_unit 本身即名字,恒空不影响。
const mergedBrands = { ...serverNames.brands, ...(loginDict?.brands ?? {}) };
const dictionary = const dictionary =
Object.keys(mergedClinics).length > 0 || Object.keys(mergedClinics).length > 0 || loginDict?.users
Object.keys(mergedBrands).length > 0 ||
loginDict?.users
? { ? {
...(Object.keys(mergedClinics).length > 0 ? { clinics: mergedClinics } : {}), ...(Object.keys(mergedClinics).length > 0 ? { clinics: mergedClinics } : {}),
...(Object.keys(mergedBrands).length > 0 ? { brands: mergedBrands } : {}),
...(loginDict?.users ? { users: loginDict.users } : {}), ...(loginDict?.users ? { users: loginDict.users } : {}),
} }
: undefined; : undefined;
......
...@@ -122,19 +122,13 @@ export class AuthService { ...@@ -122,19 +122,13 @@ export class AuthService {
return (host?.actionUrls as Record<string, string | null>) ?? {}; return (host?.actionUrls as Record<string, string | null>) ?? {};
} }
/// 服务端展示名字典(均从摄入数据派生):诊所 host.clinicNames / 品牌 host.sourceUnitNames。 /// 服务端诊所名字典(从摄入数据派生,存 host.clinicNames)。session 合并进 dictionary.clinics。
/// session 分别合并进 dictionary.clinics / dictionary.brands(宿主换票传的同名项覆盖优先)。 async getClinicNames(hostId: string): Promise<Record<string, string>> {
async getDisplayNames(
hostId: string,
): Promise<{ clinics: Record<string, string>; brands: Record<string, string> }> {
const host = await this.prisma.host.findUnique({ const host = await this.prisma.host.findUnique({
where: { id: hostId }, where: { id: hostId },
select: { clinicNames: true, sourceUnitNames: true }, select: { clinicNames: true },
}); });
return { return (host?.clinicNames as Record<string, string>) ?? {};
clinics: (host?.clinicNames as Record<string, string>) ?? {},
brands: (host?.sourceUnitNames as Record<string, string>) ?? {},
};
} }
async exchangeToken(req: TokenExchangeRequest): Promise<TokenExchangeResponse> { async exchangeToken(req: TokenExchangeRequest): Promise<TokenExchangeResponse> {
...@@ -599,8 +593,7 @@ export class AuthService { ...@@ -599,8 +593,7 @@ export class AuthService {
tenantId: 'friday-market', tenantId: 'friday-market',
role: req.role, role: req.role,
orgScope: fOrgScope, orgScope: fOrgScope,
// 名字典由 session 从 host.sourceUnitNames / clinicNames 补齐(派生自摄入数据), // 诊所名由 session 从 host.clinicNames 补齐(派生自摄入数据);此处只带造的身份名。
// 此处只带造的身份名。
dictionary: { users: { [fSub]: fName } }, dictionary: { users: { [fSub]: fName } },
}); });
this.logger.log( this.logger.log(
...@@ -687,7 +680,8 @@ export class AuthService { ...@@ -687,7 +680,8 @@ export class AuthService {
* **真实**组织结构(品牌 → 诊所)—— 给"快速登录"对话框选数据范围用。 * **真实**组织结构(品牌 → 诊所)—— 给"快速登录"对话框选数据范围用。
* *
* 结构取自 org 树(**从摄入数据 patient_transactions 派生**,零硬编码), * 结构取自 org 树(**从摄入数据 patient_transactions 派生**,零硬编码),
* 名字取自 host.sourceUnitNames / clinicNames(refresh-clinic-names 从摄入源派生)。 * 诊所名取自 host.clinicNames(refresh-clinic-names 从摄入源派生);品牌当前直接显示 id
* (无展示需求,不为此引入品牌主档数据源)。
* 多品牌宿主(FRIDAY)没有客服花名册 → 身份可造,但数据范围必须落在真实品牌/诊所上。 * 多品牌宿主(FRIDAY)没有客服花名册 → 身份可造,但数据范围必须落在真实品牌/诊所上。
* mock 关闭时返空。 * mock 关闭时返空。
*/ */
...@@ -695,24 +689,22 @@ export class AuthService { ...@@ -695,24 +689,22 @@ export class AuthService {
if (!this.isMockLoginEnabled()) return { groupId: tenantId, brands: [] }; if (!this.isMockLoginEnabled()) return { groupId: tenantId, brands: [] };
const host = await this.prisma.host.findFirst({ where: { name: hostName } }); const host = await this.prisma.host.findFirst({ where: { name: hostName } });
if (!host) return { groupId: tenantId, brands: [] }; if (!host) return { groupId: tenantId, brands: [] };
const [tree, names] = await Promise.all([ const [tree, clinicNames] = await Promise.all([
this.orgTree.getTree(host.id, tenantId), this.orgTree.getTree(host.id, tenantId),
this.getDisplayNames(host.id), this.getClinicNames(host.id),
]); ]);
// 品牌名当前无展示需求(仅本对话框用)→ 直接显示 source_unit id,不额外引入品牌主档数据源;
// 诊所名有(病历表自带 organization_name 派生),缺失回退 id。
const brands = (tree.root.children ?? []).map((b) => ({ const brands = (tree.root.children ?? []).map((b) => ({
brandId: b.id, brandId: b.id,
name: names.brands[b.id] ?? b.id, name: b.id,
clinics: (b.children ?? []).map((c) => ({ clinics: (b.children ?? []).map((c) => ({
clinicId: c.id, clinicId: c.id,
name: names.clinics[c.id] ?? c.id, name: clinicNames[c.id] ?? c.id,
})), })),
})); }));
// 有名字的品牌排前、诊所多的排前(演示时先看到真实机构) // 诊所多的排前(数据丰富的先看到)
brands.sort( brands.sort((a, b) => b.clinics.length - a.clinics.length);
(a, b) =>
Number(!!names.brands[b.brandId]) - Number(!!names.brands[a.brandId]) ||
b.clinics.length - a.clinics.length,
);
return { groupId: tenantId, brands }; return { groupId: tenantId, brands };
} }
......
...@@ -201,18 +201,6 @@ export const ColdImportManifestSchema = z ...@@ -201,18 +201,6 @@ export const ColdImportManifestSchema = z
}) })
.optional(), .optional(),
/// 品牌名字典来源(可选)—— 声明哪张表的哪两列 = 品牌(source_unit)id → 名字。
/// 同 `refresh-clinic-names` CLI 一并派生,写 host.sourceUnitNames,
/// 经 /auth/session 合并进 dictionary.brands 下发。多品牌宿主(source_unit 为 GUID)用;
/// source_unit 本身即中文名的宿主(jvs-dw)不必配。
brand_directory: z
.object({
table: z.string().min(1),
id_field: z.string().min(1),
name_field: z.string().min(1),
})
.optional(),
/// **Layer A.5 transforms**(可选)— 在 sources 之后、AssemblerEngine 之前跑。 /// **Layer A.5 transforms**(可选)— 在 sources 之后、AssemblerEngine 之前跑。
/// 6 operator 白名单:project / split_json_array / derive / route_by_pattern / pick_first_nonzero / filter。 /// 6 operator 白名单:project / split_json_array / derive / route_by_pattern / pick_first_nonzero / filter。
/// 详见 apps/pac-docs/content/docs/architecture/data-ingestion.mdx §五。 /// 详见 apps/pac-docs/content/docs/architecture/data-ingestion.mdx §五。
......
...@@ -15,11 +15,8 @@ export const TokenDictionarySchema = z ...@@ -15,11 +15,8 @@ export const TokenDictionarySchema = z
.object({ .object({
clinics: z.record(z.string(), z.string()).optional(), clinics: z.record(z.string(), z.string()).optional(),
users: z.record(z.string(), z.string()).optional(), users: z.record(z.string(), z.string()).optional(),
/// 品牌(source_unit)id→名。多品牌宿主(FRIDAY)source_unit 是 GUID,前端需名字显示;
/// 单品牌/名字即 source_unit 的宿主(jvs-dw '瑞尔')可不传。服务端 host.sourceUnitNames 补齐。
brands: z.record(z.string(), z.string()).optional(),
}) })
.describe('Optional id→displayName dictionary (clinics / users / brands), host-provided'); .describe('Optional id→displayName dictionary (clinics / users), host-provided');
export type TokenDictionary = z.infer<typeof TokenDictionarySchema>; export type TokenDictionary = z.infer<typeof TokenDictionarySchema>;
// `.strict()` on public-facing input: unknown keys are rejected with a clear // `.strict()` on public-facing input: unknown keys are rejected with a clear
......
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