Node.js 数据契约实战:三方任务模型、字段归一化与 27 条断言
Node.js 数据契约实战:三方任务模型、字段归一化与 27 条断言
2026-07-29 数据合同回归
在 Node.js v24.14.1、npm 11.11.0 环境重新执行 npm test,输出 Smoke test passed.。profile、timeline、questions、reminders 与 familyBrief 的现有合同通过 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 转成页面模型。
四、用测试夹具固定合同
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 }
七、验收清单
- 输入默认值、最大长度和非法值都有断言。
- 输出字段名称、类型和数组最小条件稳定。
- 普通、急症、模糊和冲突输入分别测试。
- 安全声明始终存在,不输出诊断、处方和剂量。
- 页面覆盖 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.js、server/index.js 和 tests/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 含“急诊” | 仍继续普通门诊建议 |
更多推荐




所有评论(0)