跳转到主要内容

FSD m04 · 患者注册与病历(patient + ticket 直传)

本册覆盖代码域 internal/domain/patient 与 internal/domain/ticket 的直传三步/注册事务链部分。全局规则见 00-总则 §2;直传三步通用规则 §2.5。

m04 功能节目录

ID名称路由/入口
F4.01注册事务链(建账号+绑会话+开单+通知)POST /pub/v1/register
F4.02病历直传三步协议POST /pub/v1/documents/presign + /confirm
F4.03注册完成通知(确认邮件+顾问 INBOX)outbox(事务链内入列)
F4.04患者资料与进度派生GET /pt/me、GET /pt/progress
F4.05GDPR 导出与抹除GET /pt/me/export、POST /api/patients/:id/erase

F4.01 注册事务链(建账号+绑会话+开单+通知)

对应 SRS:F-REG-001 / F-TICK-001(开单腿) | 实现落点:internal/domain/patient/service.go:70(Register)/chat.BindPatientInTx(chat/service.go:480)/ticket.CreateInTx(ticket/service.go:236)/ticket.BindDocumentsInTx(ticket/service.go:126)/handler.go:30(路由) | 操作入口:app H5 #/pages/register(聊天卡片点击或直达)

1 功能定义

访客提交注册表单,单一事务完成:建 user_account(PATIENT) + patient_profile → CAS 绑定聊天会话(三态语义)→ 开工单(INSERT + ticket_no 回填 + lifecycle + op_log)→ 病历文档归属对账回填 → outbox×2(确认邮件 + 顾问 INBOX)→ consent 留痕。任一步失败回滚整链。响应发 pt JWT(旧 guest token 语义作废)。

2 触发条件与前置状态
  • guest JWT(gsid 识别访客;注册页直达无会话 = 合法主路径,跳过绑定不报错)。
  • 前置校验:email 未注册(预检 COUNT + 事务内 uk 1062 兜底同映射 409);表单过 ValidateRegister 纯函数。
3 输入与校验
字段类型校验(patient/pure.go:64 ValidateRegister,返回首个错误 400)
first_name / last_namestring拼接非空(FullName 单空格规范化)
emailstring格式 + NormalizeEmail 归一;唯一(409 ErrEmailTaken)
passwordstring8~72 字符(bcryptMaxPasswordLen 硬限不静默截断)
nationalitystringISO 3166-1 alpha-2(入库大写)
genderenumMALE / FEMALE / OTHER
ageint1~120
phonestringtrim 非空
chief_complaintstringtrim 非空、≤2000 rune(→ ticket.chief_complaint)
expect_city / expect_windowstringcity 必填;window 可选
insurance_infostring可选
consentbool必须 true(ErrConsentRequired)
document_ids[]int64可选;presign/confirm 已落库文档 id,事务内归属对账
4 处理流程
5 输出与结果状态

{token, expires_in:604800, patient_id, ticket: {id, ticket_no}};历史聊天经 session.patient_id 自动归属(消息表零回填);前端切 /pt 面 + 双存 pt token。

6 状态流转
  • chat_session.patient_id: 0 → uid(CAS 谓词,天然幂等——非状态机列,AST 守卫申报豁免)。
  • ticket.status: → CREATED(创建即 lifecycle ‘CREATED’ 事件);下一迁移 PENDING_ASSIGN 归 F5.01。
  • consent_record 追加 REGISTER 行(append-only)。
7 边界与异常
场景行为
email 已注册409 ErrEmailTaken(预检+uk 双兜底)
会话已被其他 gsid 绑定409 ErrSessionBound(回滚整链)
document_ids 含他人/不存在文档409 ErrDocOwnership(affected 对账)
表单任一校验失败400(返回首个错误哨兵)
consent=false400
事务任一步失败整链回滚(无半注册状态)
孤儿文档(confirm 后未 register)不对账放任(⚠ DIF-M4 ⑪ 申报:对象存在性非事务资源,随 gsid 生命周期)
8 权限与数据规则
  • guest JWT + /pub 限流;密码 bcrypt(cost 10)。
  • consent_record 记录版本(v1)/时间/IP——数据出境告知(医疗数据存储于中国境内,SRS §4.2)。
  • op_log actor=SYSTEM(register 动作);lifecycle ActorID=patient_id。

F4.02 病历直传三步协议

对应 SRS:F-REG-002 | 实现落点:internal/domain/ticket/service.go:70(PresignDocument)/:91(ConfirmDocument)/handler.go:24-25(路由,实现在 ticket 域 Register(pubV)) | 操作入口:app 注册页上传区(拖拽+XHR 进度条,⚠ DIF-M5 ⑧)

1 功能定义

患者注册前上传病历文件(PDF/JPG/PNG,20MB/文件,每 gsid 上限 10 份,自填标签描述):presign 签发带 policy 的 PUT URL → 浏览器直传 → confirm 服务端复核落库(ticket_id=0 哨兵 + session_gsid 所有权锚),注册时 F4.01 归属回填。

2 触发条件与前置状态

guest JWT;配额第一道(presign 前 COUNT 未绑定文档,第 11 份 409)。

3 输入与校验
端点字段校验
POST /pub/v1/documents/presignmimepdf/jpeg/png 白名单(PresignPut policy 20MB + 15min)
POST /pub/v1/documents/confirmkey/label/file_nameConfirm 复核实际 size/mime(第二道);配额复核带 ticket_id=0 谓词(已绑定历史文档不占新配额,⚠ DIF-M4 ⑭①);label/file_name 截断 255
4 处理流程
5 输出与结果状态

文档 JSON(id/object_key/file_name/label/mime/size_bytes/stage=‘NONE’/version_no=1/is_current=1);对象与文档行落库,ticket_id=0 待 F4.01 回填。

6 状态流转

无状态变更(medical_document 版本组三列 M4 恒 doc_group=0/version_no=1/is_current=1——版本语义 M7 阶段资料启用,F8.02)。

7 边界与异常
场景行为
第 11 份文档409 ErrDocQuota(两道同谓词)
对象不存在(confirm)404(os.ErrNotExist)
实际超 20MB / mime 白名单外400(ErrTooLarge/ErrBadMIME)
并发竞态(COUNT+INSERT 非原子)上限可能多塞个位数,MVP 可接受(恶意损耗面小——service.go:90 注释申报)
ENUM 严格模式stage 显式 ‘NONE’(空串 500 教训,⚠ DIF-M4 ⑬)
UI 三项(标签描述/拖拽/进度)M5 补齐(⚠ DIF-M5 ⑧ 用户裁决不砍)
8 权限与数据规则

guest JWT + 限流;对象 key 服务端生成(storage.NewKey("medical"));SSE-AES 服务端加密 + 15min 预签名(tech-design §11.2 三层加密之一)。


F4.03 注册完成通知(确认邮件+顾问 INBOX)

对应 SRS:F-REG-003(确认邮件、自动通知顾问) | 实现落点:internal/domain/patient/service.go:139-152(事务链内 EnqueueInTx×2)/internal/infra/notify(dispatcher 投递) | 操作入口:—(系统自动;INBOX 消费见 F10.05)

1 功能定义

注册事务链内同事务入列两条通知:患者确认邮件(EMAIL 通道,dispatcher 30s 扫描投递)+ 顾问站内通知(INBOX 通道,待办列表)。投递成功页面语义由 F4.01 响应直接承载(成功页+工单号),通知是异步补充。

2 触发条件与前置状态

F4.01 事务 Commit(与业务同事务 all-or-nothing——业务失败通知必不入列)。

3 输入与校验

payload = {ticket_no, patient_name};recipient = 患者 email / role:CONSULTANT(角色谓词待办)。

4 处理流程
5 输出与结果状态

notification_outbox 两行(PENDING);EMAIL 行被 dispatcher 投递后 SENT;INBOX 行常驻 PENDING 待顾问标读。

6 状态流转

EMAIL:PENDING → SENT / FAILED(FAILED 可 F10.05 人工重发);INBOX:status 恒 PENDING,已读只回写 read_at(⚠ DIF-M5 ⑥——投递语义与已读语义分离)。

7 边界与异常
场景行为
dev/e2e(mailer=log 档)日志即送达(SendGrid 未配时降级可见)
SendGrid 投递失败指数退避(30s 起翻倍)3 败 FAILED → 人工重发(F10.05)
事务回滚通知行随整链回滚(无孤儿通知)
8 权限与数据规则

按患者语言选模板(i18n,tech-design §10 邮件行);payload JSON 透传。


F4.04 患者资料与进度派生(/pt/me、/pt/progress)

对应 SRS:F-TICK-003(患者进度条) | 实现落点:internal/domain/patient/service.go:249(Me)/:280(Progress)/ticket/pure.go(DerivePatientSteps)/handler.go:99-102(路由) | 操作入口:app H5 #/pages/progress 进度页;API curl

1 功能定义

患者查本人账号+最新工单概要(/pt/me)与派生八步进度视图(/pt/progress)。进度 = DerivePatientSteps 纯函数跨 A+C 两轴投影(口径 B,tech-design §6.3)——前 2 步事实驱动(有会话/已注册),后 6 步投影 status/stage。

2 触发条件与前置状态

pt JWT(iss=pt);无工单时前 2 步仍可点亮(保持 CREATED/NONE 兜底)。

3 输入与校验

无入参(patient_id = claims.Sub)。

4 处理流程
5 输出与结果状态
  • /pt/me:{patient_id, email, name, ticket: {id, ticket_no, status} | null}。
  • /pt/progress:{steps: [PatientStep...]}(派生视图,i18n key 按步骤组织——改 UX 词序不动领域模型)。
6 状态流转

只读派生,不反灌状态机(口径 B 合法身份=投影——tech-design §6.1 三套口径裁决)。

7 边界与异常
场景行为
无工单ticket=null / steps 前 2 步按事实
多工单(未来)ORDER BY id DESC 取最新(一人一单 MVP 语义,⚠ DIF-M6 ⑤ 直查口径)
跨域直查读路径直查 dbmap 申报(写边界仍由 BindPatientInTx/CreateInTx 收口)
8 权限与数据规则

pt JWT + patient_id 谓词(不走权限码);M8 起 export 按钮入口(web TicketDetail 亦有)。


F4.05 GDPR 导出与抹除

对应 SRS:§4.2(数据可携/删除请求——落地为 ADMIN 直触发,⚠ DIF-M8 ③) | 实现落点:internal/domain/patient/service_gdpr.go:39(EraseByAdmin)/handler.go:124(ExportMe)/:134(ErasePatient)/chat/scrub.go:26(ScrubSessionMessagesInTx) | 操作入口:app/web 「导出我的数据」按钮;web /admin/users 或工单详情 GDPR 抹除入口(ADMIN)

1 功能定义

②数据可携:GET /pt/me/export 聚合本人 8 表数据为 JSON 下载。③删除请求:ADMIN 执行 EraseByAdmin 单一主事务——账号墓碑化(email=anon+<id>@anonymized.local、phone 清空、name=‘已抹除’、DISABLED、totp 清)+ patient_profile 物理删 + chat 脱敏(TEXT 正文置 ‘’/译文 NULL/结构保留)+ consent WITHDRAWAL 留痕 + op_log(gdpr.erase) 同事务;OSS 对象 Commit 后循环删除(补偿语义)。

2 触发条件与前置状态
  • 导出:pt JWT(本人)。
  • 抹除:patient:manage(仅 ADMIN);目标须 role='PATIENT'(员工账号走离职停用链不走 GDPR,409 ErrNotPatient);reason 必填。
3 输入与校验
端点输入校验
GET /pt/me/export无—
POST /api/patients/:id/erase:id + reasonreason 空 → 400;账号不存在 → 404;非 PATIENT → 409;version CAS 不符 → 409
4 处理流程
5 输出与结果状态

导出:application/json Blob(8 表聚合)。抹除:204;库内已净(墓碑可查审计链),桶内对象尽力删。

6 状态流转

user_account.status: ACTIVE → DISABLED(墓碑);chat_message TEXT 正文置 ‘’(append-only 纪律的显式合规豁免——ScrubSessionMessagesInTx 头注申报,content 非状态列 AST 射程外)。

7 边界与异常
场景行为
OSS 对象删除失败不回滚主事务(库净桶脏)——独立 op_log(gdpr.oss_cleanup) 记失败清单,人工重试口径,不做自动重试 worker(⚠ DIF-M8 ③)
历史日志中的个人信息actor 落 actor_id(0=系统)——匿名化自动传播全部历史日志(⚠ DIF-M1 设计红利)
IMAGE content(object_key)保留(对象本体已删,key 留作结构审计);CARD 无 PII 保留
并发变更(version 漂移)409
8 权限与数据规则
  • 导出=本人;抹除=patient:manage(ADMIN)——ADMIN 操作本身即「人工审批」语义(op_log 同事务留痕即审计链,不另建工单流)。
  • 抹除范围=医疗数据(profile/文档对象/聊天正文);账号保留脱敏锚维持审计链(tech-design §7.1 分表理由)。