Node.js 数据契约实战:三方任务模型、字段归一化与 27 条断言

2026-07-29 数据合同回归

在 Node.js v24.14.1、npm 11.11.0 环境重新执行 npm test,输出 Smoke test passed.profiletimelinequestionsremindersfamilyBrief 的现有合同通过 27 条显式断言复核;非法 JSON、超大请求和 schemaVersion 迁移仍列为待补用例。

需求文档里的“老人、家属、医生三方协同”不能直接变成页面。工程首先要把每个角色需要完成的动作转换成稳定字段,再让接口、页面和测试共同依赖同一份合同。本文以真实 Node.js ES Modules 源码为依据,说明输入归一化、计划输出、普通/急症分支和回归断言。

复核环境为 Node.js v24.14.1、npm 11.11.0;仓库声明 Node.js >=20。执行 npm test 输出 Smoke test passed.,测试源码包含 27 条显式断言。

老年陪诊需求分析:家属、老人、医生三方到底缺什么 封面

一、三方需求落到字段,而不是口号

角色 需要完成的动作 对应字段
老人 知道带什么、去哪里、下一步做什么 timeline、cards、department
家属 获得简短摘要并安排提醒 familyBrief、reminders
医生沟通 按事实描述症状并准备问题 profile、questions

字段只是信息组织工具,不代表系统完成诊断。department 是就医准备方向,questions 是沟通清单,familyBrief 是摘要;最终科室、检查和用药必须由线下机构与医生确认。

二、输入归一化先限制形状

function normalizeProfile(input) {
  return {
    patient: String(input.patient || '家人').slice(0, 24),
    age: clamp(Number(input.age || 62), 1, 110),
    symptoms: String(input.symptoms || '').slice(0, 200),
    duration: String(input.duration || '3 天').slice(0, 40),
    history: String(input.history || '').slice(0, 160),
    medicines: String(input.medicines || '').slice(0, 160),
    familyConcern: String(input.familyConcern || '').slice(0, 160),
    audienceMode: ['elder', 'chronic', 'family']
      .includes(input.audienceMode)
      ? input.audienceMode
      : 'elder'
  }
}

这段逻辑处理默认值、类型转换、范围和长度,但仍有边界:Number('abc') 会得到 NaN,现有 clamp 是否能安全处理需要单独断言;裁剪超长字符串也应向用户说明,而不是静默丢失关键信息。高质量合同要同时定义正常值和错误语义。

三、输出合同由纯函数集中生成

return {
  generatedAt: new Date().toISOString(),
  disclaimer,
  profile,
  mode,
  department: dept,
  timeline,
  questions,
  reminders,
  familyBrief,
  cards
}

结构化输出让前端不必解析自然语言。页面可以按数组长度判断空状态,按 generatedAt 处理刷新,按 disclaimer 固定展示边界。若以后迁移到 ArkTS,应先建立对应 interface,再由 Repository 将 DTO 转成页面模型。

老年陪诊需求分析:家属、老人、医生三方到底缺什么 配图 1

四、用测试夹具固定合同

const plan = await post('/api/plan', {
  patient: '妈妈',
  age: 66,
  symptoms: '头晕、血压波动、晚上睡不好',
  duration: '5 天',
  appointment: '明天上午 9:30',
  history: '高血压',
  medicines: '降压药',
  familyConcern: '怕她说不清',
  audienceMode: 'chronic'
})

assert.equal(plan.department.dept, '心内科')
assert.equal(plan.mode.title, '慢病长期模式')
assert.ok(plan.questions.length >= 4)
assert.ok(plan.familyBrief.includes('妈妈'))

夹具中的值都能在响应里被回读,因此任何字段重命名或业务规则变化都会让测试失败。测试不是只检查 200,还检查科室方向、模式、问题数量、家属摘要、提醒与卡片。失败时应从第一个断言开始定位,而不是继续改页面掩盖接口变化。

五、普通与急症合同必须互斥

输入组 期望 intent 关键字段 禁止结果
挂号和科室问题 hospital actions、hospitalInfo 不得自动确诊
医生沟通 doctor doctorInfo、questions 不得生成处方
胸痛+呼吸困难 emergency urgency、急诊提示 不得继续普通门诊流程
空白或模糊文本 prep/待补充 补充提示 不得猜测敏感事实

六、错误合同仍需补齐

当前服务把解析错误和内部错误统一返回 500。下一轮应增加 400 invalid_json、413 request_too_large、404 route_not_found 和 405 method_not_allowed,并测试缺字段、超长字段、非法年龄、未知模式和重复提交。

type ApiError =
  | { code: 'invalid_json'; message: string }
  | { code: 'request_too_large'; message: string }
  | { code: 'validation_failed'; fields: string[] }
  | { code: 'route_not_found'; path: string }
老年陪诊需求分析:家属、老人、医生三方到底缺什么 配图 2

七、验收清单

  • 输入默认值、最大长度和非法值都有断言。
  • 输出字段名称、类型和数组最小条件稳定。
  • 普通、急症、模糊和冲突输入分别测试。
  • 安全声明始终存在,不输出诊断、处方和剂量。
  • 页面覆盖 loading、empty、error、content 和 retry。
  • 日志不记录完整健康与家庭信息。

八、版本升级与合同兼容

数据合同一旦被页面、测试或其他端消费,字段变化就不再是局部重构。新增字段应优先设计为可选并提供默认值,删除或改名需要迁移期,枚举扩展必须让旧客户端进入可解释的 unknown 分支。generatedAt 等时间字段要固定 ISO 8601 和时区语义,不能让不同设备自行猜测。

interface ContractVersion {
  schemaVersion: '1.0'
  generatedAt: string
  capabilities: string[]
}

function migratePlan(dto: CarePlanDto): CarePlan {
  if (!dto.schemaVersion) return migrateLegacyPlan(dto)
  if (dto.schemaVersion === '1.0') return normalizePlan(dto)
  throw new Error('unsupported_schema_version')
}

兼容测试至少保留一份旧响应夹具和一份当前响应夹具,分别验证迁移函数、缺省值和未知枚举。客户端遇到不支持的 schemaVersion 时,应停止写入并提示升级,而不是带着不完整字段继续生成提醒或摘要。版本号只有与迁移代码和回归用例绑定,才真正具备工程意义。

九、隐私字段的生命周期

profile 中的症状、病史和药物属于敏感内容,应定义创建、读取、更新、删除和过期策略。接口日志只记录 requestId、规则分支和错误码,不记录完整请求体;测试夹具使用虚构样例,不复制真实用户资料。删除任务时同时清理摘要、提醒和缓存,导出操作必须由用户主动触发并清楚说明范围。

阶段 最小数据 控制
输入 完成任务所需字段 长度与格式校验
处理 内存中的归一化模型 不输出敏感日志
保存 用户确认保留的任务 本地优先、可清除
分享 人工确认后的摘要 默认不包含多余病史
删除 任务、提醒、缓存 统一删除并回读结果

结论

把三方需求工程化的关键,不是写更多页面说明,而是建立可执行数据合同:输入先归一化,输出保持结构化,急症分支拥有明确优先级,测试用 27 条显式断言回读关键字段。当前冒烟测试已通过,但错误码、非法值、并发和隐私日志仍是下一轮必须补齐的合同。

AI 辅助声明:本文在人工复核真实源码、配置字段与验证记录的基础上,使用 AI 辅助整理结构和语言;功能边界、代码路径、版本信息与测试结论均以当前工程为准,未执行的真机、云测或平台操作不会写成已经通过。

从需求字段到真实接口断言

当前源码不是 TypeScript 项目,而是原生 JavaScript ES Modules;因此本文中的类型接口只承担“迁移合同”作用,真实实现证据来自 server/agent.jsserver/index.jstests/smoke-test.js。2026 年 7 月 28 日在 Node.js v24.14.1 上执行 npm test,27 条显式断言全部通过。

const plan = await post('/api/plan', {
  patient: '妈妈',
  age: 66,
  symptoms: '头晕、血压波动、晚上睡不好',
  duration: '5 天',
  appointment: '明天上午 9:30',
  history: '高血压',
  medicines: '降压药',
  familyConcern: '怕她说不清',
  audienceMode: 'chronic'
})

assert.equal(plan.department.dept, '心内科')
assert.equal(plan.mode.title, '慢病长期模式')
assert.ok(plan.questions.length >= 4)
assert.ok(plan.familyBrief.includes('妈妈'))

这组断言把三方需求落到了可回读字段:老人获得就医步骤,家属获得摘要与提醒,医生沟通由问题清单承接。它没有验证真实医疗结论,也没有验证临床效果;急症文本另走 intent=emergency 分支,未知或缺字段仍需补充负向用例。

需求 字段/断言 失败判定
老人知道下一步 timeline、cards、mode 步骤缺失或顺序不可理解
家属获得同步 familyBrief、reminders 摘要遗漏关键任务或暴露多余隐私
医生沟通有准备 questions 数量与内容 问题为空或写成诊断结论
急症优先 intent=emergency、urgency 含“急诊” 仍继续普通门诊建议
Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐