Commit 92ecc815 by luoqi

refactor(cleanup): calcAge 4 份逐字节复制抽到 @pac/utils(审计 G1)

plan-script/plan-summary orchestrator + plan-aggregate + plan.service 各有一份
逐字节相同的 calcAge → 抽到 packages/utils/src/format.ts(加可选 now 参数便于测试),
4 处改 import。

types/utils build + service/web tsc 通过;jest 103 passed(8 个失败为存量:
script-facts 路径失效、priority-scorer.computeLikelihoodBonus 已移除、
plan-engine DB 集成测试、yaml emits — 均与本次改动无关,已 git 核对 cc3eb275 基线)。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
parent 0aa1e88b
import { Injectable, Logger, NotFoundException } from '@nestjs/common';
import { randomUUID } from 'node:crypto';
import type { Prisma } from '@prisma/client';
import { fmtYearMonth } from '@pac/utils';
import { calcAge, fmtYearMonth } from '@pac/utils';
import { planScenarioLabel, personaFeatureMeta, subLabelZh, treatmentCategoryNameZh } from '@pac/types';
import { PrismaService } from '../../../prisma/prisma.service';
import { AiCallRunnerService } from '../ai-call-runner.service';
......@@ -781,14 +781,6 @@ function renderMarkdownFromSections(
// 小工具(从 fact 提炼 LLM 真正需要的几条信息)
// ─────────────────────────────────────────────
function calcAge(birthDate: Date): number {
const now = new Date();
let age = now.getFullYear() - birthDate.getFullYear();
const m = now.getMonth() - birthDate.getMonth();
if (m < 0 || (m === 0 && now.getDate() < birthDate.getDate())) age--;
return age;
}
/**
* 病历 — 一次接诊的完整病历(SOAP 全字段,结构化),挂到每条 reason.medicalRecord。
* 字段集 + 同次诊断关联(source_encounter_external_id === emr_external_id)对齐前端 emr-soap-view。
......
import { Injectable, Logger, NotFoundException } from '@nestjs/common';
import { randomUUID } from 'node:crypto';
import type { Prisma } from '@prisma/client';
import { maskName } from '@pac/utils';
import { calcAge, maskName } from '@pac/utils';
import { planScenarioLabel, personaFeatureMeta } from '@pac/types';
import { PrismaService } from '../../../prisma/prisma.service';
import { ChainComposerService } from '../../plan/engine/chain-composer.service';
......@@ -333,14 +333,6 @@ export class PlanSummaryOrchestrator {
}
}
function calcAge(birthDate: Date): number {
const now = new Date();
let age = now.getFullYear() - birthDate.getFullYear();
const m = now.getMonth() - birthDate.getMonth();
if (m < 0 || (m === 0 && now.getDate() < birthDate.getDate())) age--;
return age;
}
// ─────────────────────────────────────────────
// 内部类型(本地投影,避免循环依赖)
// ─────────────────────────────────────────────
......
import { Injectable, NotFoundException } from '@nestjs/common';
import type { Prisma } from '@prisma/client';
import { maskName, maskPhone } from '@pac/utils';
import { calcAge, maskName, maskPhone } from '@pac/utils';
import { applyLiveDays, ApiCode } from '@pac/types';
import { BizError } from '../../common/errors/biz-error';
import { PrismaService } from '../../prisma/prisma.service';
......@@ -649,11 +649,3 @@ function sumAmount(facts: Array<{ content: Prisma.JsonValue }>): number {
}
return total;
}
function calcAge(birthDate: Date): number {
const now = new Date();
let age = now.getFullYear() - birthDate.getFullYear();
const m = now.getMonth() - birthDate.getMonth();
if (m < 0 || (m === 0 && now.getDate() < birthDate.getDate())) age--;
return age;
}
......@@ -15,7 +15,7 @@ import {
type PlanDetailResponse,
type PlanReasonBrief,
} from '@pac/types';
import { maskName, maskPhone } from '@pac/utils';
import { calcAge, maskName, maskPhone } from '@pac/utils';
import { PrismaService } from '../../prisma/prisma.service';
import { PERSONA_TAG_FILTER_DIMS, parsePersonaTags } from '@pac/types';
import type { TenantScopeContext } from '../../common/decorators/tenant-scope.decorator';
......@@ -578,14 +578,6 @@ function serializeExecution(e: PlanExecutionRow): import('@pac/types').PlanExecu
};
}
function calcAge(birthDate: Date): number {
const now = new Date();
let age = now.getFullYear() - birthDate.getFullYear();
const m = now.getMonth() - birthDate.getMonth();
if (m < 0 || (m === 0 && now.getDate() < birthDate.getDate())) age--;
return age;
}
function computePlanEvidence(
plan: PlanRow,
persona: PersonaWithFeatures | null,
......
......@@ -65,3 +65,15 @@ export function fmtYearMonthDay(d: Date | string): string {
const target = typeof d === 'string' ? new Date(d) : d;
return `${target.getFullYear()}.${String(target.getMonth() + 1).padStart(2, '0')}.${String(target.getDate()).padStart(2, '0')}`;
}
/**
* 周岁年龄(birthDate → 整岁),按月/日精确扣减未满周岁。
* @param now 参考时间(默认当下)— 便于测试注入
*/
export function calcAge(birthDate: Date | string, now: Date = new Date()): number {
const b = typeof birthDate === 'string' ? new Date(birthDate) : birthDate;
let age = now.getFullYear() - b.getFullYear();
const m = now.getMonth() - b.getMonth();
if (m < 0 || (m === 0 && now.getDate() < b.getDate())) age--;
return age;
}
......@@ -26,7 +26,7 @@ export function verifySecret(secret: string, stored: string): boolean {
}
// 格式化 / 掩码 / 相对时间 — 浏览器 + Node 双端可用
export { maskPhone, maskName, fmtRel, fmtDate, fmtYearMonth, fmtYearMonthDay } from './format';
export { maskPhone, maskName, fmtRel, fmtDate, fmtYearMonth, fmtYearMonthDay, calcAge } from './format';
export function chunk<T>(arr: T[], size: number): T[][] {
if (size <= 0) return [arr];
......
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