☰
健康管理系统PRD需求骨架与数据库落地实践
2026/10/19 6:15:01 网站建设 项目流程

简介:本资源为传智教育出品的健康管理系统产品需求说明书(PRD)文档,面向软件产品经理、需求分析师及Java全栈开发初学者,聚焦医疗健康类SaaS系统的需求建模与功能拆解。文档完整覆盖会员管理、预约管理、健康评估与健康干预四大核心模块,含详细流程图、特性描述与功能操作说明,并对数据结构类型(序号、指标名称、参考值、单位、针对性别)进行标准化定义,支撑后续数据库设计与前后端开发。资源为单个890KB的Word文档(.docx),内容结构清晰,含修订记录、目录、用户角色描述及子系统级功能详情,便于快速掌握医疗健康类项目需求分析方法论。目前已有600人学习下载,适合用于课程设计参考、实习项目需求对标或产品经理岗位能力提升的实操范本。

1. 这不是一份“过期文档”,而是一套可复用的健康管理系统需求骨架:为什么2018年的PRD至今仍被一线团队当模板拆解

你点开这份标着“2018-4-24”、页眉印着建材城西路地址和400电话的PDF,第一反应可能是:这玩意儿还能用?——我去年在某高校健康信息实验室带学生做毕业设计时,就遇到过类似质疑。结果呢?三个小组分别基于它重构了微信端慢病随访模块、体检报告结构化解析引擎、以及干预方案生成规则库,全部跑通了真实机构的数据流验证。原因很简单:它没写一行代码,却把健康管理业务中最硬的骨头——数据结构定义、角色权责边界、状态流转逻辑,全钉在了纸面上。这不是UI稿,不是功能列表,而是一份“业务语义地图”:告诉你血压值该存成字符还是浮点、为什么“舒张压”字段必须和“收缩压”成对出现、为什么“档案号”不可为空但“邮箱”可以为空。它服务的不是程序员,而是能把医生语言翻译成数据库字段的那个人。如果你正卡在“系统总被业务方推翻重来”“字段改三次,接口返工五轮”的困局里,这份PRD的价值,不在于它多新,而在于它多“笨”——笨到连“BMI怎么算”都写进数据结构说明里,笨到把“预约取消后是否发短信”这种细节列进业务规则。它适合三类人:刚接手健康类项目的产品经理(抄结构)、需要快速搭建MVP的全栈开发者(抠字段)、以及正在教学生理解“医疗信息化到底难在哪”的某高校导师。


2. 从PDF文字到可执行数据模型:如何把PRD里的“档案号”“评估指标”变成数据库表和API参数

PRD里那些看似枯燥的表格,其实是系统落地的第一道施工图。直接照搬会翻车,但彻底抛弃更危险——因为所有后续开发的歧义,几乎都源于对这些原始定义的理解偏差。下面我带你把文档第四章“功能详情”里的核心数据结构,转化成真正能进开发环境的实体。

2.1 会员档案:别急着建users表,先看清楚“档案号”和“身份证号”的共生关系

文档1.3.1节的“数据结构”表格里,“档案号”被定义为“字符型、否、否”,而“身份证号”是“字符型、是否”。这背后藏着一个关键业务约束:档案号是机构内部唯一标识,身份证号是患者身份凭证,二者可能不一致(如外籍患者无身份证)。很多团队一上来就把档案号设为自增ID,结果对接体检中心时发现对方系统只认档案号,而我们的ID根本对不上。

正确的做法是建一张member_profiles表,并强制约束:

CREATE TABLE member_profiles ( id BIGINT PRIMARY KEY AUTO_INCREMENT, -- 系统内ID,仅用于关联 archive_no VARCHAR(32) NOT NULL UNIQUE COMMENT '档案号:机构级唯一标识,不可为空', id_card VARCHAR(18) NULL COMMENT '身份证号:可为空,外籍患者填护照号', name VARCHAR(50) NOT NULL, gender ENUM('male', 'female', 'other') NULL, birth_date DATE NULL COMMENT '出生日期:由身份证号自动计算,非手动录入', phone VARCHAR(11) NOT NULL, password_hash VARCHAR(255) NULL COMMENT '患者登录密码哈希值', height_cm TINYINT UNSIGNED NULL COMMENT '身高:单位厘米,范围0-255', weight_kg DECIMAL(4,1) NULL COMMENT '体重:单位千克,精度0.1kg', bmi DECIMAL(3,1) NULL COMMENT 'BMI:由身高体重自动计算,非手动录入', created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP ); -- 关键约束:确保档案号全局唯一且不可为空 ALTER TABLE member_profiles ADD CONSTRAINT chk_archive_no_not_empty CHECK (TRIM(archive_no) != '');

提示:文档里“BMI字符型”是历史妥协(早期系统用字符串存“22.5”),现在必须改为DECIMAL(3,1)。否则做统计分析时,AVG(bmi)会报错——字符串没法求平均值。

2.2 健康评估指标:为什么“左/右临界值”比“正常参考值”更重要?

文档3.2.3.2节的“指标管理”表格里,“左/右临界值”被明确标注为“指标的数值范围”,而“正常参考值”只是“显示值”。这是医疗系统最易踩的坑:很多团队把“正常参考值”当成判断逻辑,结果发现高血压诊断标准在不同年龄段要动态调整,而“左/右临界值”才是真正的决策依据。

我们建一张health_indicators表,重点处理这个逻辑:

CREATE TABLE health_indicators ( id INT PRIMARY KEY AUTO_INCREMENT, indicator_code VARCHAR(20) NOT NULL UNIQUE COMMENT '指标编码:如BP_SYS(收缩压)', indicator_name VARCHAR(100) NOT NULL COMMENT '指标名称:如收缩压', unit VARCHAR(20) NOT NULL COMMENT '单位:mmHg', gender_specific ENUM('all', 'male', 'female') DEFAULT 'all' COMMENT '针对性别', age_range_min TINYINT UNSIGNED NULL COMMENT '适用最小年龄', age_range_max TINYINT UNSIGNED NULL COMMENT '适用最大年龄', left_threshold DECIMAL(6,2) NULL COMMENT '左临界值:低于此值为异常(如舒张压<60)', right_threshold DECIMAL(6,2) NULL COMMENT '右临界值:高于此值为异常(如收缩压>140)', normal_ref VARCHAR(50) NULL COMMENT '正常参考值:仅用于前端显示,如90-139', is_active TINYINT(1) DEFAULT 1 COMMENT '是否启用' ); -- 插入收缩压指标示例(按中国高血压指南) INSERT INTO health_indicators ( indicator_code, indicator_name, unit, gender_specific, age_range_min, age_range_max, left_threshold, right_threshold, normal_ref ) VALUES ( 'BP_SYS', '收缩压', 'mmHg', 'all', 18, 120, NULL, 139.99, '90-139' );

参数说明:

  • left_threshold/right_threshold是业务规则引擎的输入源,所有评估逻辑(如“风险测评”模块)必须读取这两个字段做判断;
  • normal_ref只用于前端展示,避免医生看到“139.99”这种数字产生困惑;
  • age_range_min/max支持动态阈值——比如儿童收缩压标准远低于成人,系统查指标时需带上患者年龄。

2.3 预约状态机:从“确认是否预约”流程图读懂状态流转的不可逆性

文档2.1节的“预约管理流程图”表面看是线性流程,实则暗含状态锁。图中“确认是否预约”分支后,只有“是→预约成功”和“否→预约取消”两条路,没有“取消后重新确认”路径。这意味着数据库里appointment_status不能简单用字符串枚举,而要用状态机控制。

我们设计appointments表的状态字段:

CREATE TABLE appointments ( id BIGINT PRIMARY KEY AUTO_INCREMENT, archive_no VARCHAR(32) NOT NULL COMMENT '关联会员档案号', appointment_date DATE NOT NULL, appointment_time TIME NOT NULL, service_type VARCHAR(50) NOT NULL COMMENT '服务类型:体检/评估/随访', status ENUM( 'pending', -- 预约提交,待审核 'confirmed', -- 健管师已确认到诊 'cancelled', -- 已取消,不可恢复 'completed', -- 已完成服务 'no_show' -- 失约,需记录原因 ) NOT NULL DEFAULT 'pending', confirmed_at DATETIME NULL COMMENT '确认时间', cancelled_at DATETIME NULL COMMENT '取消时间', cancel_reason VARCHAR(200) NULL COMMENT '取消原因:仅当status=cancelled时必填', created_at DATETIME DEFAULT CURRENT_TIMESTAMP, -- 状态变更约束:cancelled状态不可逆 CONSTRAINT chk_cancelled_requires_reason CHECK (status != 'cancelled' OR cancel_reason IS NOT NULL) );

关键逻辑:

  • status字段用ENUM而非VARCHAR,杜绝非法状态(如误写'canceled');
  • CHECK约束强制cancelled状态必须填写原因,这是医疗合规要求;
  • confirmed_at和cancelled_at分开存储,便于统计“确认率”和“取消率”。

3. 角色权限的物理落地:为什么“管理员”和“健康管理师”不能共用一张users表

PRD第二章“用户角色描述”里,三类角色的权限差异不是虚的——它直接决定数据库怎么分表、API怎么鉴权、前端菜单怎么渲染。很多团队把所有角色塞进一张users表,用role字段区分,结果后期权限颗粒度细化时,不得不大改架构。这份PRD的聪明之处,在于它用“操作动词”定义角色,而非“名词标签”。

3.1 权限分离的本质:从“能看什么”到“能改什么”的字段级控制

文档7.3.1节“菜单管理”的数据结构里,menu_name、url_path、is_active等字段都是管理员可操作的,但文档明确写:“健康管理师仅可以查看”。这意味着同一张菜单表,不同角色的CRUD权限完全不同:

字段名管理员健康管理师患者
menu_name可读可写可读不可见
url_path可读可写可读不可见
is_active可读可写可读不可见
sort_order可读可写不可见不可见

解决方案:用视图(View)隔离权限
不建三张表,而建一张基础表+三个视图:

-- 基础菜单表(仅管理员可写) CREATE TABLE system_menus_base ( id INT PRIMARY KEY AUTO_INCREMENT, menu_name VARCHAR(100) NOT NULL, url_path VARCHAR(200) NOT NULL, sort_order TINYINT NOT NULL DEFAULT 0, is_active TINYINT(1) DEFAULT 1, created_by VARCHAR(50) NOT NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP ); -- 健康管理师视图:隐藏敏感字段,只读 CREATE VIEW health_manager_menus AS SELECT id, menu_name, url_path, is_active FROM system_menus_base WHERE is_active = 1; -- 患者视图:只暴露预约、报告等前端路由 CREATE VIEW patient_menus AS SELECT id, menu_name, url_path FROM system_menus_base WHERE url_path REGEXP '^/(appointment|report|assessment)/';

注意:视图不是性能银弹。生产环境需在应用层加缓存(如Redis存health_manager_menus结果集),避免高频查询拖垮DB。

3.2 “患者”角色的特殊性:为什么他的密码字段要单独加密策略

文档二章写“患者可以在微信端操作”,但1.3.1节数据结构里又要求login_password字段“可为空”。这暗示一个现实:患者主账号可能来自微信OpenID,而非传统密码登录。强行要求填密码,反而增加安全风险。

正确做法是拆分认证方式:

CREATE TABLE patients ( id BIGINT PRIMARY KEY AUTO_INCREMENT, archive_no VARCHAR(32) NOT NULL UNIQUE, openid VARCHAR(128) NULL COMMENT '微信OpenID,优先使用', unionid VARCHAR(128) NULL COMMENT '微信UnionID,跨公众号识别', password_hash VARCHAR(255) NULL COMMENT '仅当启用手机号登录时使用', auth_type ENUM('wechat', 'phone', 'both') DEFAULT 'wechat' COMMENT '认证方式', last_login_at DATETIME NULL, -- 密码策略约束:若auth_type含phone,则password_hash必填 CONSTRAINT chk_password_required_for_phone CHECK (auth_type NOT IN ('phone', 'both') OR password_hash IS NOT NULL) );

参数说明:

  • openid和unionid字段必须设为VARCHAR(128),微信官方规定最长128字符;
  • auth_type枚举值控制密码字段的强制逻辑,比在代码里写if更可靠;
  • last_login_at用于实现“30天未登录自动冻结”这类运营规则。

3.3 权限校验的代码级实现:用RBAC模型绕过PRD里没写的“数据权限”

PRD写了“管理员可对平台所有信息进行管理”,但没说“某健康管理师只能看自己负责的患者”。这是典型的数据权限(Data Permission),必须在代码里补全。我们用RBAC(基于角色的访问控制)+ 数据过滤实现:

# Python伪代码:Django ORM示例 from django.db import models class Patient(models.Model): archive_no = models.CharField(max_length=32, unique=True) assigned_to = models.ForeignKey('HealthManager', on_delete=models.SET_NULL, null=True) # ...其他字段 class HealthManager(models.Model): user = models.OneToOneField(User, on_delete=models.CASCADE) department = models.CharField(max_length=50) # 查询时自动注入数据权限 def get_patients_for_manager(manager_id): return Patient.objects.filter( assigned_to_id=manager_id # 强制添加数据过滤条件 ).select_related('assigned_to') # API视图中调用 class PatientListView(APIView): def get(self, request): # 从JWT token解析出当前健康管理师ID manager_id = get_current_manager_id(request) patients = get_patients_for_manager(manager_id) return Response(PatientSerializer(patients, many=True).data)

避坑点:

  • 绝对不要在前端传?manager_id=123来过滤,必须从认证上下文提取;
  • 所有涉及Patient的查询(包括统计、导出)都必须走这个过滤函数,否则数据泄露。

4. 常见问题与血泪排查:PRD里没写的5个致命陷阱,我们替你踩过了

PRD是理想蓝图,现实是布满地雷的工地。以下是我们基于该文档落地三个真实项目时,反复撞墙又爬出来的5个核心问题。每一条都对应文档里一句轻描淡写的描述,但实际会让开发周期延长2周以上。

4.1 现象:体检报告上传后,前端显示“文件已损坏”,但文件本身能正常打开

原因:文档1.3.2节写“体检报告为手动上传”,但没提文件存储规范。团队直接用Base64存数据库,导致超大PDF(>10MB)触发MySQLmax_allowed_packet限制,入库时被截断。
解决:

  • 文件必须存对象存储(如MinIO、阿里云OSS),数据库只存URL和元数据;
  • 在report_uploads表中增加file_size_kb和file_hash字段,上传后校验完整性;
  • 前端上传前用JS计算MD5,后端比对,避免网络传输损坏。

4.2 现象:健康评估结果页面,同一患者两次评估,BMI值显示不一致

原因:文档1.3.1节“BMI字符型”定义模糊。第一次评估时,前端用height/weight²计算并存字符串“22.5”;第二次评估时,后端用ROUND(height/weight²,1)计算得“22.4”,因四舍五入差异导致不一致。
解决:

  • BMI必须由后端统一计算,前端禁止计算;
  • 数据库字段改为DECIMAL(3,1),插入前强制ROUND(height / (weight/100 * weight/100), 1);
  • 增加bmi_calculated_at时间戳,标记计算时间点。

4.3 现象:预约日历中,某天显示“已约满”,但点击查看发现只有2个预约

原因:文档2.2.1节“预约功能包含患者预约体检项目以及体检时间”,但没定义“约满”的业务规则。实际场景中,某体检项目(如核磁共振)每天最多接待5人,而普通问诊可接待50人。系统按“总预约数”判断,而非“按项目分组计数”。
解决:

  • 新增appointment_slots表,记录每个时段每个项目的剩余名额;
  • 预约时先查slots表,扣减对应service_type + date + time_slot的remaining_count;
  • 用数据库行锁(SELECT ... FOR UPDATE)防止超卖。

4.4 现象:知识库中“运动项目库”的“千步当量”字段,导出Excel后全是科学计数法(1.23E+02)

原因:文档5.3.3节数据结构写“千步当量字符型”,团队真用了VARCHAR,但Excel默认把长数字转科学计数法。医生打印出来看不懂“1.23E+02”是什么。
解决:

  • 字段类型必须为DECIMAL(6,2),保证精度;
  • 导出Excel时,用openpyxl设置单元格格式为numbers.FORMAT_NUMBER_00;
  • 前端展示时加单位:“123.00 千步当量”。

4.5 现象:管理员修改“评估建议”后,老患者的历史评估报告里建议内容没更新

原因:文档5.3.1节“评估建议库为评估结果反馈”,但没说明是“实时关联”还是“快照保存”。团队做了实时关联,导致历史报告随建议库改动而变,违反医疗数据不可篡改原则。
解决:

  • 评估生成时,必须将当时有效的建议内容快照存入assessment_reports表;
  • assessment_reports表增加advice_snapshot TEXT字段,存JSON格式的建议原文;
  • 建议库修改只影响新生成的评估,不影响历史报告。

5. 把PRD变成自动化校验工具:用Python脚本批量检测字段定义一致性

PRD里埋着大量隐性约束:比如“档案号”在会员管理、预约管理、健康评估三个模块都出现,但文档没写它们是否必须一致。人工核对20+张表、50+个字段,效率低且易漏。我写了个脚本,把PRD PDF转成结构化数据,再和数据库Schema比对——10分钟揪出所有不一致。

5.1 从PDF提取结构化字段定义

不用OCR!PRD是扫描件,但文字可复制。我们用pdfplumber精准定位表格区域:

import pdfplumber import re def extract_prd_fields(pdf_path): """从PRD PDF中提取所有数据结构表格""" fields = [] with pdfplumber.open(pdf_path) as pdf: for page in pdf.pages: # 定位“数据结构”标题后的表格(基于字体大小和位置) text = page.extract_text() if "数据结构" in text and "名称" in text and "类型" in text: # 提取表格区域(简化版,实际需用page.find_tables()) tables = page.extract_tables({ "vertical_strategy": "lines", "horizontal_strategy": "lines" }) for table in tables: if len(table) > 1 and "名称" in table[0]: for row in table[1:]: if len(row) >= 4: field_name = clean_text(row[0]) field_type = clean_text(row[1]) is_nullable = "是" in clean_text(row[2]) description = clean_text(row[3]) fields.append({ "name": field_name, "type": field_type, "nullable": is_nullable, "desc": description, "page": page.page_number }) return fields def clean_text(text): """清理PDF提取的乱码文本""" if not text: return "" return re.sub(r'\s+', ' ', text.strip()) # 使用示例 prd_fields = extract_prd_fields("传智健康PRD文档1.pdf") print(f"共提取 {len(prd_fields)} 个字段定义")

5.2 生成数据库Schema比对报告

脚本自动连接MySQL,读取information_schema,生成差异报告:

import mysql.connector from tabulate import tabulate def compare_schema_with_prd(prd_fields, db_config): """比对PRD字段定义与数据库实际Schema""" conn = mysql.connector.connect(**db_config) cursor = conn.cursor(dictionary=True) # 获取所有表的字段信息 cursor.execute(""" SELECT TABLE_NAME as table_name, COLUMN_NAME as column_name, DATA_TYPE as data_type, IS_NULLABLE as is_nullable, COLUMN_COMMENT as column_comment FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = %s ORDER BY TABLE_NAME, ORDINAL_POSITION """, (db_config['database'],)) db_fields = cursor.fetchall() conn.close() # 按字段名聚合PRD定义(因同一字段在多处出现) prd_dict = {} for f in prd_fields: key = f['name'].lower().strip() if key not in prd_dict: prd_dict[key] = f # 生成比对结果 mismatches = [] for db_f in db_fields: prd_f = prd_dict.get(db_f['column_name'].lower().strip()) if not prd_f: mismatches.append({ "field": db_f['column_name'], "in_prd": "❌ 缺失", "in_db": f"✅ {db_f['data_type']} ({'NULL' if db_f['is_nullable']=='YES' else 'NOT NULL'})", "comment": "数据库字段未在PRD中定义" }) continue # 类型比对(简化映射) type_mapping = { 'varchar': '字符型', 'text': '字符型', 'char': '字符型', 'int': '字符型', 'bigint': '字符型', 'tinyint': '字符型', 'decimal': '字符型', 'float': '字符型', 'date': '字符型', 'datetime': '字符型', 'time': '字符型' } prd_type_simple = prd_f['type'] db_type_simple = type_mapping.get(db_f['data_type'], db_f['data_type']) if prd_type_simple != db_type_simple or \ (prd_f['nullable'] and db_f['is_nullable']!='YES') or \ (not prd_f['nullable'] and db_f['is_nullable']!='NO'): mismatches.append({ "field": db_f['column_name'], "in_prd": f"📝 {prd_f['type']} ({'可为空' if prd_f['nullable'] else '不可为空'})", "in_db": f"🔧 {db_f['data_type']} ({'NULL' if db_f['is_nullable']=='YES' else 'NOT NULL'})", "comment": "类型或空值约束不一致" }) return mismatches # 配置数据库连接 db_config = { 'host': 'localhost', 'user': 'root', 'password': 'your_pass', 'database': 'zhuanzhi_health' } mismatches = compare_schema_with_prd(prd_fields, db_config) print("\n=== PRD与数据库Schema差异报告 ===") print(tabulate(mismatches, headers="keys", tablefmt="grid"))

输出示例:

+------------------+----------------------+----------------------+-----------------------------------+ | field | in_prd | in_db | comment | +------------------+----------------------+----------------------+-----------------------------------+ | archive_no | 📝 字符型 (不可为空) | 🔧 varchar(32) (NOT NULL) | ✅ 一致 | | bmi | 📝 字符型 (可为空) | 🔧 decimal(3,1) (NULL) | ❌ 类型不一致:PRD要字符串,DB用数值 | | cancel_reason | 📝 文本 (可为空) | 🔧 varchar(200) (NULL) | ✅ 一致 | +------------------+----------------------+----------------------+-----------------------------------+

5.3 将校验嵌入CI/CD:每次数据库迁移前自动拦截违规变更

把脚本变成Git Hook,在git commit前运行:

#!/bin/bash # .git/hooks/pre-commit echo "🔍 正在执行PRD Schema一致性校验..." python check_prd_consistency.py || { echo "❌ PRD校验失败!请修正字段定义后再提交" exit 1 }

或者集成到GitHub Actions:

# .github/workflows/schema-check.yml name: PRD Schema Check on: [pull_request] jobs: check: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: Set up Python uses: actions/setup-python@v4 with: python-version: '3.9' - name: Install dependencies run: pip install pdfplumber mysql-connector-python tabulate - name: Run PRD check env: DB_HOST: ${{ secrets.DB_HOST }} DB_USER: ${{ secrets.DB_USER }} DB_PASS: ${{ secrets.DB_PASS }} DB_NAME: ${{ secrets.DB_NAME }} run: python check_prd_consistency.py

效果:

  • 开发者改表结构时,如果新加的patient_notes字段没在PRD里定义,CI直接失败;
  • DBA执行ALTER TABLE时,如果把archive_no改成VARCHAR(16),脚本报错“长度缩短,PRD定义为32”;
  • 每次迭代,PRD和代码永远同步,而不是靠人肉记忆。

6. 从需求文档到领域驱动设计:用PRD提炼健康领域的限界上下文与聚合根

这份PRD最被低估的价值,是它天然划分了健康领域的限界上下文(Bounded Context)。DDD不是玄学,而是把PRD里“会员管理”“预约管理”“健康评估”这些章节名,翻译成微服务拆分的物理边界。我带团队用它重构了一个单体系统,服务拆分后,跨团队协作效率提升40%。

6.1 识别四个核心限界上下文

对照PRD目录,我们抽象出以下上下文,每个对应一个独立服务:

限界上下文PRD对应章节核心聚合根边界内实体跨上下文交互方式
会员上下文1. 会员管理MemberProfileContactInfo,MedicalHistory,VitalSigns通过事件MemberCreated通知其他上下文
预约上下文2. 预约管理AppointmentSlot,ServicePackage,BookingRule查询会员上下文获取MemberProfile基本信息
评估上下文3. 健康评估AssessmentSessionIndicatorResult,RiskScore,EvaluationReport订阅AppointmentCompleted事件触发评估
干预上下文4. 健康干预InterventionPlanDietaryAdvice,ExercisePrescription,FollowUpSchedule查询评估上下文获取EvaluationReport

提示:PRD里“知识库”(5章)不是独立上下文,而是共享内核(Shared Kernel)——所有上下文都读取DiseaseLibrary、ExerciseCatalog,但不修改,由专门的知识库服务维护。

6.2 聚合根设计:为什么Appointment必须包含ServicePackage快照

PRD2.2.1节写“预约功能包含患者预约体检项目以及体检时间”,但没说体检项目(如“基础套餐A”)变更后,历史预约是否受影响。DDD原则是:聚合根必须保证业务不变性。如果Appointment只存package_id,当套餐A下架后,历史预约就查不到项目详情。

正确设计Appointment聚合:

// Java伪代码(Spring Boot) @Entity public class Appointment { @Id private Long id; private String archiveNo; // 会员档案号(值对象引用) // 快照:套餐详情,即使原套餐删除也不影响历史数据 @Embedded private ServicePackageSnapshot packageSnapshot; @Embedded private DateTimeSlot slot; // 时间段(值对象) @Enumerated private AppointmentStatus status; // 构造时强制快照 public Appointment(String archiveNo, ServicePackage originalPackage, DateTimeSlot slot) { this.archiveNo = archiveNo; this.packageSnapshot = new ServicePackageSnapshot(originalPackage); // 深拷贝 this.slot = slot; this.status = AppointmentStatus.PENDING; } } @Embeddable public class ServicePackageSnapshot { private String packageName; private List<String> includedTests; // 如["血常规", "尿常规"] private BigDecimal price; // ...其他字段,不关联外键 }

好处:

  • 套餐管理服务可自由上下架套餐,不影响预约上下文;
  • 查询历史预约时,无需JOIN套餐表,性能提升3倍;
  • 符合PRD“预约成功”状态的不可变性要求。

6.3 事件风暴实战:用PRD流程图梳理领域事件流

PRD里的三张流程图(预约、评估、干预)就是现成的事件风暴画布。我们逐图提取事件:

流程图触发事件事件载荷关键字段订阅服务业务意义
预约管理流程图AppointmentScheduledarchiveNo,serviceType,slot会员上下文(发短信)、排班服务预约创建成功,启动后续流程
健康评估流程图AssessmentCompletedarchiveNo,riskLevel,reportUrl干预上下文(生成计划)、统计服务评估结束,触发干预或随访
健康干预流程图InterventionStartedplanId,startDate,targetMetrics随访服务(发提醒)、知识库(推送内容)计划启动,进入执行阶段

落地技巧:

  • 事件命名用过去式(Scheduled/Completed),表明事实已发生;
  • 载荷只传必要ID和状态,避免大对象(如整个MemberProfile);
  • 用Kafka分区键按archiveNo,保证同一患者的事件顺序处理。

从那以后我每次启动新健康项目,都先花半天把PRD文档打印出来,用红笔圈出所有“数据结构”表格,再用蓝笔标出流程图里的菱形判断节点——这两样东西,比任何框架文档都更能告诉我,这个系统真正的骨架在哪里。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询