跳转到主要内容

MediLink Global 功能规格说明书(FSD)v1.0 · 总则

上游文档:prd/srs-medilink-v1.md(v1.1,需求唯一权威源)。本文档以代码实现为事实基准,还原系统真实运转规则;与 SRS 的差异以 ⚠ DIF 注记标出并登记 99-附录 差异台账。 功能 ID 规则:F<m>.<两位序号>(m = 模块号 1~10,与代码域包对应)。ID 全局唯一、只增不改号。 技术设计(为什么这样实现)见 docs/tech-design.md(15 节);本文只记 as-is 行为。

0. 修订记录

版本日期对应 SRS 版本修订说明作者
v1.02026-10-09srs-medilink-v1.1首版:按代码实现还原全模块功能规格。P0 总则 + 50 功能 ID 分配冻结—

0.1 文档定位

  • SRS 管 to-be 契约(需求意图、验收口径唯一权威);FSD 记 as-is 实现(系统当前真实行为)。
  • 每个 FSD 功能节标注「对应 SRS 需求 ID」;实现与 SRS 不一致处行内 > ⚠ DIF-F<m>-<序号> 注记,并登记 99-附录差异台账(现状 file:line / SRS 期望 / 影响面 / 处置建议)。注:正文另有 DIF-M<n> ① 引用系 tech-design 里程碑批次差异编号(权威源 tech-design §14/开发任务总表),与 DIF-F 两套并存、不换算——见 99-附录 B 头注。
  • SRS 有而未实现的功能照常占 ID,状态列标 未实现;代码有而 SRS 无的功能标 代码先行(此类隐性规则正是本档最值钱部分)。MVP 已收口(M1~M12),当前全集为 实现。
  • 分册结构:功能详述按代码域拆分为 m01~m10 共 10 册 + 99-附录,F<m>.<nn> 位于 m<m>-*.md;跨文件一律以 F-ID 纯文本引用,锚点不受拆分影响。
  • 行内引用的技术背景(「为什么」)指 docs/tech-design.md §n,本文不重复论证。

1. 术语与数据字典

1.1 术语对照(SRS 叫法 / 本文统一叫法 = 代码字段名)

收录判据:SRS、界面文案、代码命名三方叫法不一致的词必收。

SRS/口语本文统一叫法代码字段/表
工单工单ticket(域内模型 ticket.Ticket)
8 个状态 / 流程状态A 轴(办理环节)ticket.status(8 态 + 3 回退边,见 2.2)
诊疗 8 阶段C 轴(医疗旅程)ticket.care_stage(8 阶段,见 2.2)
患者进度条 8 步派生八步DerivePatientSteps 纯函数(跨 A+C 投影,不反灌状态机)
匿名咨询访客会话chat_session + guest JWT claim gsid
注册后转正绑定(guest→patient)chat_session.patient_id CAS 谓词更新(0=未绑定哨兵)
两段式登录 / 2FATOTP 两段式POST /api/auth/login → mfa_token → POST /api/auth/mfa;user_account.totp_status 三态
病历直传 / 三步协议直传三步presign → PUT 直传 → confirm(HeadObject 复核,见 2.5)
支付残留单残留单payment_order.status='PENDING' 且未回调的孤儿行(人工关单)
注册卡片注册卡片chat_message.msg_type='CARD',content={"type":"register"} 服务端构造
站内通知 / 待办INBOXnotification_outbox.channel='INBOX'(read_at 列标记已读,status 恒 PENDING)
邮件队列outbox(EMAIL 通道)notification_outbox.channel='EMAIL'(指数退避 3 败 FAILED)
重新翻译重译POST .../messages/:id/retranslate
译文补推译文回填SSE 事件 msgTranslated(worker 异步回填 chat_msg_ext)
推荐专家专家引用ticket.expert_id(选人)+ ticket.expert 文本快照(不可变)
科室锚定dept 锚医生选人按 hospital_dept.id(dept 全局唯一隐含院区)
阶段资料版本版本组medical_document.doc_group / version_no / is_current
权限码权限码permission.code(14 个,<domain>:<action>)
生命周期视图双日志轨entity_lifecycle_event(状态/阶段时间线)+ operation_log(操作审计)

1.2 数据字典(按功能模块归组的表清单)

DDL 权威:medilink/migrations/full/medilink_full.sql(19 表,本文不复制字段,只给「表 → 功能节」挂点;字段级规则在各功能节 3 记录;全表逐表展开见 docs/数据库总览.md)。

模块核心表
m01 认证与权限user_account permission role_permission
m02 医院主数据hospital hospital_dept hospital_expert
m03 双语咨询chat_session chat_message(日志)chat_msg_ext
m04 注册与病历user_account patient_profile consent_record(日志)medical_document
m05 工单与看板ticket
m06 预诊断(无新表——写 ticket 列)
m07 支付payment_order payment_webhook_event(技术)
m08 诊疗过程medical_document(版本组复用)
m09 出行协助trip_record(detail JSON)
m10 系统与审计entity_lifecycle_event(日志)operation_log(日志)notification_outbox(技术)

tech-design §5.1 记「21 张」为设计期口径,实施收敛为 19 张(translation_cache 改 Redis 实装不建表、care_stage_record/trip_arrangement 合并为 trip_record + lifecycle 行集——DIF-M3 ⑥/DIF-M7 ①),以 full.sql 实测为准,差异台账见 99-附录 B。

2. 全局规则

每批发现新全局规则必须回填本章,正文只引用不重复。

2.1 权限模型

  • 三 issuer JWT 互不认(自实现 HMAC-SHA256,gateway.Signer):/api 面 iss=bo(claims uid/role/pv,exp 12h);/pt 面 iss=pt(exp 7d);/pub 面 iss=guest(claim gsid,exp 7d);另有第 4 issuer mfa(5min、仅 uid,无业务权限——结构性过不了 bo 面必填 claim 校验)。
  • 4 角色:user_account.role = ADMIN / CONSULTANT / DOCTOR / PATIENT。PATIENT 不走权限码表——靠 /pt 路由面物理隔离 + service 层「只准读写自己数据」强制谓词;权限码只管 B 端三角色。
  • 14 个权限码(permission 字典表 seed 幂等,gateway.NewDBPermissionSource 消费):chat:read(1) chat:send(2) ticket:read(3) ticket:assign(4) ticket:transition(5) diag:write(6) hospital:manage(7) patient:manage(8) user:manage(9) payment:read(10) care:write(11) trip:write(12) ops:read(13) ops:manage(14)。
  • 守卫方式:路由级 gateway.RequirePermission(code, permSrc, rdb, dbname, log) 中间件(main.go 逐条挂载,模式 C);redis 权限集缓存 60s TTL(key rbac:<库名>:perm:<role>:pv<pv>,JWT claim pv 隔离版本);DB 错误 fail-closed 返回空集;未挂守卫的端点 = 登录即可(如 /api/inbox——通知消费按角色谓词隔离,无权限码)。
  • 「角色 × 权限码」矩阵(权威源 scripts/dev/seed.sql:127-141;语义:chat:send 属顾问操盘职责、diag:write 属医生临床行为,ADMIN 不代写不代发):
角色权限码
CONSULTANTchat:read chat:send ticket:read ticket:assign ticket:transition payment:read care:write trip:write ops:read(9 码——全程操盘)
DOCTORchat:read ticket:read diag:write(3 码——只读 + 写预诊)
ADMIN全 14 码 − {chat:send, diag:write}(12 码)
PATIENT无权限码(/pt 面 + 谓词隔离)
  • 登录快照(⚠ DIF-M8 ⑤):LoginResult.permissions[] 登录时直查字典表一次(不走 redis 缓存);web 前端 src/authz.ts 纯函数 has(code) 做菜单/按钮显隐——权限漂移靠重新登录刷新,PV 不变。

2.2 状态机总览

各业务对象状态机的权威枚举汇总于此(共 8 组:ticket/payment/care_stage/trip + hospital·dept·expert 三同构计 7 组为 transition 白名单机,经 pure.go 转移表代码化+守卫测试锁定;user_account.status/TOTP 三态/translation_status 为 CAS 直写非白名单机,在 transition() 射程外);每个功能节 6 只画本功能触及的迁移,引用本章。

工单 ticket.status(A 轴)(ticket/pure.go:92,8 态 + 3 回退边):

状态含义
CREATED已创建(注册事务链开单)
PENDING_ASSIGN待分配(CREATED 自动流转至此)
PREDIAGNOSING预诊断中(已分配医生)
PLAN_CONFIRMING方案确认中(医生已提交预诊)
PENDING_PAYMENT待支付(患者进入支付)
PENDING_DEPARTURE待出行(支付成功回调)
IN_TREATMENT就医中(C 轴自此推进)
COMPLETED已完成(终态)
  • COMPLETED 终态不可迁;三条回退边是设计补充(边 1 由支付失败回调驱动、边 3 由医生拒绝驱动,边 2 手动触发权限待产品确认——tech-design §15 风险 3);迁移只准走 TransitionInTx(status 谓词 CAS + lifecycle + op_log 同事务)。

支付单 payment_order.status(payment/pure.go:15):

  • SUCCEEDED→REFUNDED 不入白名单(MVP 无自动退款,人工线下改库必走 operation_log 带 reason);FAILED 不复活——重谈后新建支付单(PENDING 复用谓词:同工单已有 PENDING 单则复用重新 CreateCheckout,防残留单堆积,⚠ DIF-M8 ⑦)。

诊疗阶段 ticket.care_stage(C 轴)(care 域 stageTransitions,8 阶段单向链,无回退边;推进前置 status=IN_TREATMENT 由 service 层校验):

出行记录 trip_record.status(trip 域 tripTransitions):PLANNED → {BOOKED, CANCELLED};BOOKED → {COMPLETED, CANCELLED}。CANCELLED/COMPLETED 终态不可迁。

账号 user_account.status(SetUserStatusInTx):ACTIVE ⇄ DISABLED 双向(禁用/启用)。

医院/科室/专家 status(三处同构,hospital/pure.go:99/116/133):DRAFT → PUBLISHED → OFFLINE → PUBLISHED 单向环(无回 DRAFT——已发布语义不可撤销为草稿)。

TOTP user_account.totp_status 三态(user 域,⚠ DIF-M8 ①):NONE → PENDING(setup 生成密钥)→ ENABLED(验首码转正);ENABLED → NONE(disable 验现码解绑 / ADMIN 重置清密钥);PENDING 态不触发两段式登录(否则 setup 与登录互相锁死)。

chat_msg_ext.translation_status(非 transition 封装、worker CAS 回写):NONE(不投翻译:CARD/IMAGE)→ PENDING → DONE / FAILED(FAILED 可 retranslate 重投)。

2.3 通用校验与一致性(全系统不变量)

  1. 单库 DB 事务 + version CAS(tech-design §1 降级裁决,比模板三层补偿更强更简):写方法 service 层 WithTx 包事务;状态表必带 version 列,状态变更走状态谓词 CAS(WHERE id=? AND status=?,SET version=version+1——from 漂移即 ErrVersionConflict 409)。
  2. 禁裸 UPDATE 状态列(AST 守卫强制):状态列 UPDATE 的字符串字面量只准出现在 transition 封装内;白名单函数全集 = TransitionInTx(ticket)/ TransitionOrderInTx(payment)/ TransitionStageInTx(care_stage)/ TransitionTripInTx(trip)/ SetUserStatusInTx(user)/ TransitionHospitalInTx / TransitionDeptStatusInTx / TransitionExpertStatusInTx(hospital 三同构);行内注释 ast-status-guard:exempt 申报豁免(非状态列回填如 ticket_no/receipt_object_key)。
  3. 迁移-日志同路径:每次状态变更 = transition 封装内同事务写 entity_lifecycle_event(from/to)+ operation_log(actor/reason)——无独立日志通道。
  4. 读路径绝不加锁:全部业务读直查 dbmap,无任何串行化设施。
  5. 幂等:payment_webhook_event(provider+event_id UNIQUE,INSERT 冲突=重复投递直接 200);注册绑定 CAS 谓词(WHERE patient_id=0)天然幂等;seed 幂等 INSERT IGNORE。
  6. SSE 推送在事务 Commit 后(⚠ DIF-M5 ③):事务内推则回滚放假事件;失败仅 warn 不反压业务。
  7. NOT NULL + 哨兵值(原则六):全表基础类型列 NOT NULL + 哨兵(''/0/'NONE',ENUM 必须显式 DEFAULT);NULL 豁免仅限「尚未发生」语义列(paid_at/read_at/prediagnosis_summary/plan/translated_text 等)+ 守卫白名单两条(user_account.totp_secret、payment_order.refund_amount);not_null_guard / pointer_field_guard 双守卫防 stale。
  8. 守卫总表:9 类机器守卫全表见 tech-design §13.2(check_consistency / not_null / pointer_field / lifecycle 入口覆盖 / 迁移-日志 AST / 状态机白名单 / 翻译覆盖锁 / SSE 事件注册表 / e2e 十段断言)。

2.4 错误响应约定(纯 HTTP 状态码 + 哨兵错误映射)

  • 机制:与模板的字符串错误码不同,本项目 handler 统一返回 echo.NewHTTPError(status) 裸 HTTP 状态码(全仓零 ErrResp 调用——grep 盘点实证);业务语义由 service 层哨兵错误(errors.New 包级单例)+ handler mapErr 的 errors.Is 映射承载。响应体不携带业务码字符串,前端按状态码分流。
  • 状态码分布(grep 实测全仓 NewHTTPError 计数,domain+gateway):400×87 / 500×17 / 409×13 / 404×12 / 401×7 / 429×1 / 503×2 / 403×2。
  • 哨兵错误 → 状态码映射惯例(权威映射在各域 handler mapErr;正面样例):
    • 400:bind/校验失败、ErrWeakPassword 族、ErrBadMIME/ErrTooLarge、ErrInvalidStageTransition 例外见下、ErrNotRetranslatab、ErrReasonRequired;
    • 401:auth.ErrInvalidCredentials(三端登录统一——防枚举);403:ticket.ErrForbidden(归属谓词);
    • 404:ErrNotFound 族(ticket/patient/chat.ErrNotFound、ErrOrderNotFound、ErrExpertNotFound、ErrTicketNotFound——归属不符与不存在同语义不泄露存在性);
    • 409:ErrVersionConflict(CAS)、ErrInvalidTransition(白名单)、ErrSessionBound、ErrEmailTaken、ErrDocQuota、ErrDocOwnership、ErrNotPayable、ErrReceiptNotReady、ErrNotInTreatment、ErrTOTPAlreadyEnabled、ErrDocGroupConflict、ErrInvalidHospitalTransition、ErrNotPatient;
    • 503:可选依赖未配置(jwt.secret 空→受保护面 401 例外、webhook_secret 空、master_key 空的 2FA 路径)。
  • 收敛方向(tech-design §2.4 同族):资源不存在=404、CAS 冲突/业务拒绝=409、可判定校验=400——现状 gorp 层错误多归 500 族(127 处中 15 处),是已知待收敛项不扩面。
  • 哨兵错误清单全表:见 99-附录 C。

2.5 直传三步协议通用规则(病历 / 聊天图 / 医院图 / 阶段资料四处同构)

  1. 三步:①POST .../presign(校验登录态+配额+size/mime 申报)→ 签发带 policy 的预签名 PUT URL(content-length-range 0..N MB + mime 白名单 + 15min)——policy 签名在存储侧硬性拦截超限;②浏览器直传(不占后端带宽);③POST .../confirm:服务端 HeadObject 复核实际 size/mime(不信任客户端申报,第二道拦截)→ 配额复核 → 落库。
  2. 限制的执行点在服务端两道,存储只是执行器不是规则源;配额=第一道 presign 前 COUNT + confirm 二次 COUNT(⚠ DIF-M4 ⑭①:confirm 复核带 ticket_id=0 谓词,已绑定历史文档不占新配额)。
  3. mime 白名单:病历 pdf/jpg/png 20MB;聊天图 jpg/png 10MB(mini 两步);医院图/阶段资料同病历口径。
  4. 对象存储:storage.Store 接口隔离——dev/e2e=minio S3 兼容(LAN 192.168.50.190:9000),单测=localfs 不连外部服务;S3 的「对象 mime」=上传方 Content-Type 元数据(presign 已锁死申报),真内容校验随生产 OSS driver 加服务端嗅探(⚠ DIF-M4 ① 已知局限)。

2.6 双日志轨与可选依赖降级

  1. 双日志轨分工:entity_lifecycle_event(对象全时间线,A/C 两轴事件同表靠值区分)服务「对象经历了什么」;operation_log(标准列 actor_id/reason/trace_id/changes JSON)服务「谁在什么时候改了什么」——/api 全部写方法经中间件统一记录 + 豁免注册表,关键操作 service 内显式补记带 reason。actor 落 actor_id BIGINT(0=系统/匿名)——GDPR 匿名化自动传播到全部历史日志(⚠ DIF-M1)。
  2. 可选依赖降级矩阵(缺省=功能降级可见,不阻塞起服):jwt.secret 空→受保护面一律 401;master_key 空→TOTP 相关返 ErrKeyNotConfigured;webhook_secret 空→/webhooks/stripe 一律 503;redis 缺→限流回落内存内核、RBAC 直查 DB、翻译缓存直查(不降级放行也不误杀);receiptIO 未注入→凭证生成跳过、下载 409;mailer=log 档(日志即送达)。
  3. 通知双通道:EMAIL(dispatcher 30s 扫描指数退避、3 败 FAILED 可人工重发)/ INBOX(常驻 PENDING + read_at 列,dispatcher 不消费——已读语义独立于投递语义)。
  4. SSE hub 单实例(进程内 map[sessionKey]map[connID]conn,buffer 8 慢消费者丢弃+失步标记,25s 心跳);多实例扩容换 Redis pub/sub,接口不变(tech-design §15 风险 7)。

3. 功能模块清单(ID 冻结 v1.0)

状态列:实现 / 实现(代码先行)(代码有而 SRS 无)。 代码域 ↔ 功能模块映射:auth+gateway→m01;hospital→m02;chat→m03;patient→m04;ticket→m04(直传三步)+m05;diagnosis→m06;payment→m07;care→m08;trip→m09;user+audit+infra/notify→m10。

m01 账号认证与权限

功能 ID名称对应 SRS状态
F1.01B 端两段式登录(密码+TOTP)SRS §4.2(机制细化)实现(代码先行)
F1.02患者登录(email+密码)—(DIF-001 待产品确认)实现(代码先行)
F1.03访客会话签发 guest-session—(获客漏斗前置件)实现(代码先行)
F1.04JWT 五面鉴权与路由矩阵SRS §4.2实现(代码先行)
F1.05RBAC 权限码字典与路由守卫SRS §4.2实现(代码先行)
F1.06/pub IP 限流(内存/Redis 双内核)SRS §4.1(防滥用)实现(代码先行)

m02 医院主数据与展示

功能 ID名称对应 SRS状态
F2.01公开医院列表F-HOS-001实现
F2.02公开医院详情F-HOS-002实现
F2.03B 端医院维护与状态流转F-ADMIN-001实现
F2.04医院封面图直传F-ADMIN-001实现
F2.05科室管理F-ADMIN-001实现
F2.06专家库管理—(M10 扩展批次)实现(代码先行)

m03 双语咨询

功能 ID名称对应 SRS状态
F3.01创建匿名咨询会话F-CHAT-001实现
F3.02SSE 流与心跳重连F-CHAT-002实现
F3.03发消息与异步翻译回填F-CHAT-002实现
F3.04图片消息F-CHAT-002实现
F3.05已读回执F-CHAT-002实现
F3.06B 端会话管理与回复F-CHAT-002实现
F3.07注册卡片推送F-CHAT-003实现
F3.08患者端聊天(/pt 转正+重译)F-CHAT-002实现

m04 患者注册与病历

功能 ID名称对应 SRS状态
F4.01注册事务链(建账号+绑会话+开单+通知)F-REG-001 / F-TICK-001实现
F4.02病历直传三步协议F-REG-002实现
F4.03注册完成通知(确认邮件+顾问 INBOX)F-REG-003实现
F4.04患者资料与进度派生(/pt/me、/pt/progress)F-TICK-003(患者侧)实现
F4.05GDPR 导出与抹除SRS §4.2(落地为 ADMIN 直触发)实现(细节代码先行)

m05 工单与看板

功能 ID名称对应 SRS状态
F5.01工单自动生成F-TICK-001实现
F5.02工单分配与流转F-TICK-002实现
F5.03状态机 transition() 与三条回退边F-TICK-002(状态机)实现(机制代码先行)
F5.04病历下载与版本组读F-CARE-002(就医前部分)实现(部分代码先行)
F5.05运营看板(聚合+三超时+对账 tab)F-ADMIN-003实现

m06 预诊断与方案

功能 ID名称对应 SRS状态
F6.01医生工作台(预诊+专家选人 dept 锚)F-DIAG-001实现(专家选人代码先行)
F6.02拒绝与重派F-TICK-002(医生确认/拒绝)实现(代码先行)
F6.03患者方案确认F-DIAG-002实现

m07 支付与对账

功能 ID名称对应 SRS状态
F7.01定金支付(Stripe Checkout)F-PAY-001实现
F7.02webhook 验签与幂等F-PAY-001实现
F7.03支付凭证 PDFF-PAY-001实现(代码先行)
F7.04后台支付管理与对账F-PAY-001(状态更新)实现(对账部分代码先行)
F7.05残留单人工关单—(对账兜底)实现(代码先行)

m08 诊疗过程

功能 ID名称对应 SRS状态
F8.01C 轴 8 阶段推进F-CARE-001实现
F8.02阶段资料版本组F-CARE-002实现
F8.03患者进度同步F-CARE-003实现

m09 出行协助

功能 ID名称对应 SRS状态
F9.01酒店安排F-TRIP-001实现
F9.02交通安排F-TRIP-002实现
F9.03患者只读拉取(/pt/trips)F-TRIP(患者可见)实现(代码先行)

m10 系统管理与审计

功能 ID名称对应 SRS状态
F10.01用户管理(CRUD/禁用/改角色/重密)F-ADMIN-002实现
F10.02TOTP 2FA 管理(三态机+重置)SRS §4.2(细化)实现(代码先行)
F10.03生命周期时间线查询F-TICK-003(后台日志)实现(代码先行)
F10.04操作日志审计页SRS §4.2实现(代码先行)
F10.05INBOX 待办与邮件对账重发F-REG-003(扩展)实现(代码先行)
F10.06outbox 投递与重试—(基础设施)实现(代码先行)

4. 功能详述

一功能一节(#### F<n>.<nn> 名称),固定 8 小节:1 功能定义 / 2 触发条件与前置状态 / 3 输入与校验 / 4 处理流程 / 5 输出与结果状态 / 6 状态流转 / 7 边界与异常 / 8 权限与数据规则。 节头元数据行:> 对应 SRS:… | 实现落点:file:line | 操作入口:…(操作入口 = 客户端词表 + 页面路由 + 关键操作;无 UI 节标注 API/定时任务/SSE)。 各节按模块分册:m01 · m02 · m03 · m04 · m05 · m06 · m07 · m08 · m09 · m10 · 99-附录