Commit 76dac918 by luoqi

refactor(cleanup): dedup getHostOrThrow + triggerTypeOf(审计 G5/G2)

- hosts.service: 7 处 `host.findUnique + if(!p) throw NotFound` 收口为私有 getHostOrThrow(id)
- treatment-initiation-recall.scenario: 两处逐字相同的 trigger-type 三元抽 triggerTypeOf(row)

service tsc 通过。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
parent 0a0ab389
......@@ -44,6 +44,13 @@ import { PrismaService } from '../../prisma/prisma.service';
export class HostsService {
constructor(private readonly prisma: PrismaService) {}
/** 按 id 取 host,不存在抛 NotFound(收口多处重复的 findUnique + guard) */
private async getHostOrThrow(id: string): Promise<Host> {
const host = await this.prisma.host.findUnique({ where: { id } });
if (!host) throw new NotFoundException(`host ${id} not found`);
return host;
}
// ─────────────────────────────────────────────────────────
// Create
// ─────────────────────────────────────────────────────────
......@@ -90,8 +97,7 @@ export class HostsService {
}
async get(id: string): Promise<HostSummary> {
const p = await this.prisma.host.findUnique({ where: { id } });
if (!p) throw new NotFoundException(`host ${id} not found`);
const p = await this.getHostOrThrow(id);
return this.toSummary(p);
}
......@@ -100,8 +106,7 @@ export class HostsService {
// ─────────────────────────────────────────────────────────
async update(id: string, req: UpdateHostRequest): Promise<HostSummary> {
const before = await this.prisma.host.findUnique({ where: { id } });
if (!before) throw new NotFoundException(`host ${id} not found`);
const before = await this.getHostOrThrow(id);
const data: Prisma.HostUpdateInput = {};
if (req.pullConfig !== undefined) data.pullConfig = this.toPrismaJson(req.pullConfig);
......@@ -117,8 +122,7 @@ export class HostsService {
// ─────────────────────────────────────────────────────────
async rotateAppSecret(id: string): Promise<RotateSecretResponse> {
const p = await this.prisma.host.findUnique({ where: { id } });
if (!p) throw new NotFoundException(`host ${id} not found`);
const p = await this.getHostOrThrow(id);
const appSecret = randomCode(32);
await this.prisma.host.update({
where: { id },
......@@ -132,8 +136,7 @@ export class HostsService {
// ─────────────────────────────────────────────────────────
async rotatePushSecret(id: string): Promise<RotatePushSecretResponse> {
const p = await this.prisma.host.findUnique({ where: { id } });
if (!p) throw new NotFoundException(`host ${id} not found`);
const p = await this.getHostOrThrow(id);
const pushSecret = randomCode(32);
const newHashes = [hashSecret(pushSecret), ...p.pushSecretHashes];
await this.prisma.host.update({
......@@ -148,8 +151,7 @@ export class HostsService {
// ─────────────────────────────────────────────────────────
async deactivate(id: string): Promise<HostSummary> {
const p = await this.prisma.host.findUnique({ where: { id } });
if (!p) throw new NotFoundException(`host ${id} not found`);
const p = await this.getHostOrThrow(id);
const updated = await this.prisma.host.update({
where: { id },
data: { active: false },
......@@ -162,8 +164,7 @@ export class HostsService {
// ─────────────────────────────────────────────────────────
async getDetail(id: string): Promise<HostDetail> {
const p = await this.prisma.host.findUnique({ where: { id } });
if (!p) throw new NotFoundException(`host ${id} not found`);
const p = await this.getHostOrThrow(id);
const stats = await this.getStats(id);
return {
id: p.id,
......@@ -263,8 +264,7 @@ export class HostsService {
// ─────────────────────────────────────────────────────────
async removePushSecretBySuffix(id: string, suffix: string): Promise<RemovePushSecretResponse> {
const p = await this.prisma.host.findUnique({ where: { id } });
if (!p) throw new NotFoundException(`host ${id} not found`);
const p = await this.getHostOrThrow(id);
const matchIdx = p.pushSecretHashes.findIndex((h) => extractSuffix(h) === suffix);
if (matchIdx < 0) {
throw new BadRequestException(`push secret suffix=${suffix} not found in rotation`);
......
......@@ -423,12 +423,7 @@ export class TreatmentInitiationRecallScenario implements PlanScenarioPlugin {
const sourceStr = hasDx && hasRec ? '(诊断+医生建议)' : hasRec ? '(医生建议)' : isImg ? '(影像AI)' : '(诊断)';
// 触发信号类型(原 enum,不语义化;前端用 triggerTypeLabelZh 翻译)
// lead 单一类型 — 想精准列出所有 sig 的话需要把 triggers[] 改成 cluster 全量,目前 lead 代表
const triggerType =
r.signal_type === 'recommendation_record'
? 'recommendation'
: r.code_source === 'image_ai'
? 'image_finding'
: 'diagnosis';
const triggerType = triggerTypeOf(r);
hits.push({
patientId,
patientExternalId: r.patient_external_id,
......@@ -652,17 +647,21 @@ function mergeRowsByToothOverlap(rows: HitRow[]): HitRow[] {
/// 提 cluster 内 unique (type, code) 触发,给 signals.triggers
/// type='diagnosis' 排在前(给前端首选展示),其次 'recommendation'
/// 触发信号类型:recommendation_record→recommendation / code_source=image_ai→image_finding / 否则 diagnosis
function triggerTypeOf(row: { signal_type: string; code_source: string | null }): string {
return row.signal_type === 'recommendation_record'
? 'recommendation'
: row.code_source === 'image_ai'
? 'image_finding'
: 'diagnosis';
}
function uniqueTriggers(rows: HitRow[]): Array<{ type: string; code: string }> {
const seen = new Set<string>();
const out: Array<{ type: string; code: string }> = [];
for (const r of rows) {
// 影像AI 诊断 → image_finding(前端渲染"影像AI"),区别于医生诊断 diagnosis
const type =
r.signal_type === 'recommendation_record'
? 'recommendation'
: r.code_source === 'image_ai'
? 'image_finding'
: 'diagnosis';
const type = triggerTypeOf(r);
const key = `${type}|${r.signal_code}`;
if (!seen.has(key)) {
seen.add(key);
......
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