FSD m02 · 医院主数据与展示(hospital)
本册覆盖代码域
internal/domain/hospital。全局规则见 00-总则 §2;直传三步通用规则见 §2.5;医院/科室/专家三同构状态机见 §2.2。
m02 功能节目录
| ID | 名称 | 路由/入口 |
|---|---|---|
| F2.01 | 公开医院列表 | GET /pub/v1/hospitals |
| F2.02 | 公开医院详情 | GET /pub/v1/hospitals/:id |
| F2.03 | B 端医院维护与状态流转 | POST/PUT /api/hospitals、POST /api/hospitals/:id/status |
| F2.04 | 医院封面图直传与回显 | POST /api/hospitals/:id/image/presign + /confirm;读侧 GET /pub/v1/hospitals/:id/image + GET /api/hospitals/:id/image(M15) |
| F2.05 | 科室管理 | POST /api/hospitals/:id/depts、PUT /api/hospitals/depts/:id、POST .../status |
| F2.06 | 专家库管理 | POST /api/hospitals/:id/experts、GET /api/hospitals/:id/experts、PUT /api/hospitals/experts/:id、POST .../status |
F2.01 公开医院列表
对应 SRS:F-HOS-001 | 实现落点:
internal/domain/hospital/service.go:65(ListHospitals)/handler.go:22(路由) | 操作入口:app H5#/pages/hospital/list医院列表页
1 功能定义
匿名访客按城市/等级筛选、分页浏览 PUBLISHED 状态医院卡片(中英文名/等级/城市/地址/科室标签/特色服务)。科室标签由 dept_summary 列拆分派生,不 join hospital_dept 表(防 N+1,⚠ DIF-M2 ④)。
2 触发条件与前置状态
app 首页列表加载;前置 = 至少一所 status='PUBLISHED' 医院(seed 预置 4 所)。
3 输入与校验
| 参数 | 类型 | 校验 |
|---|---|---|
| city | query string | 可选;等值过滤 |
| grade | query string | 可选;等值过滤(如 三甲) |
| page / page_size | query string | 可选;ParseIntOr 解析 + NormalizePage 归一(缺省/非法回落默认页) |
无 bind body;参数解析在 service(handler 零逻辑)。
4 处理流程
5 输出与结果状态
{items: [{id, name_zh, name_en, grade, city, address, dept_tags[], features[]}], total, page, page_size};空数组归一 [] 非 null(NonNilStrings)。
6 状态流转
只读;只出 PUBLISHED(DRAFT/OFFLINE 对匿名不可见)。索引 idx_status_city(status, city)(⚠ DIF-M2 ⑧ W0 修复:city 在后的组合才走得通默认路径)。
7 边界与异常
| 场景 | 行为 |
|---|---|
| 无匹配医院 | 200 空列表(items=[] total=0) |
| dept_summary 为空串 | dept_tags=[](哨兵语义) |
| 分页越界 | 空页(OFFSET 超总数据量) |
| DB 错误 | 500 |
8 权限与数据规则
- guest JWT(/pub 面 RequireJWT)+ IP 限流 60/min。
- 列表列不含
intro_intl/visit_process(详情才返回,减载荷);运营中内容(非 PUBLISHED)不可见。
F2.02 公开医院详情
对应 SRS:F-HOS-002 | 实现落点:
internal/domain/hospital/service.go:101(GetHospital)/handler.go:46(Get) | 操作入口:app H5#/pages/hospital/detail医院详情页
1 功能定义
匿名访客查看单所 PUBLISHED 医院完整详情:国际部介绍、特色服务、就诊流程、PUBLISHED 科室列表(中英文,按 sort 排序)+ 咨询入口(前端引导建会话 F3.01)。
2 触发条件与前置状态
列表点入或直链;前置 = 医院 status='PUBLISHED'。
3 输入与校验
| 参数 | 类型 | 校验 |
|---|---|---|
| :id | path int64 | >0,解析失败 400 |
4 处理流程
5 输出与结果状态
{id, name_zh, name_en, grade, city, address, dept_tags[], intro_intl, services[], visit_process[], depts: [{id, name_zh, name_en}], image_url}。image_url(M15 封面回显):image_object_key 非空时派生相对路径 /pub/v1/hospitals/{id}/image(<img src> 免鉴权直用),''=未上传前端不渲染。
6 状态流转
只读;医院与科室状态独立流转(科室可先于院区 PUBLISHED,公开详情只显示 PUBLISHED 科室)。
7 边界与异常
| 场景 | 行为 |
|---|---|
| id 不存在 或非 PUBLISHED | 一律 404(ErrNotFound 二者不可区分——不向匿名暴露运营中内容的存在性,service.go:15 注释) |
| 未上传封面 | image_url=''(前端不渲染头图,渐变头维持) |
| 科室全 DRAFT | depts=[] |
| DB 错误 | 500 |
8 权限与数据规则
guest JWT + 限流;详情列含富文本(intro_intl TEXT);科室排序 sort, id。
F2.03 B 端医院维护与状态流转
对应 SRS:F-ADMIN-001(医院信息管理) | 实现落点:
internal/domain/hospital/service_write.go:61(Create)/:82(Update)/:111(TransitionStatus)/in_tx.go:15(TransitionHospitalInTx)/handler_bo.go:37-105| 操作入口:web/admin/hospitals医院管理页(新建/编辑/上架按钮)
1 功能定义
B 端运营维护医院主数据:建院(INSERT 恒 DRAFT)、编辑非状态列(version CAS)、状态流转(DRAFT→PUBLISHED→OFFLINE→PUBLISHED 单向环白名单)。管理列表出全态(含 DRAFT/OFFLINE)。
2 触发条件与前置状态
- 建院/编辑/流转:
hospital:manage权限(仅 ADMIN,见总则 §2.1 矩阵)。 - 上架前置:富文本三列与封面图可空(运营后补——建院即可流转 PUBLISHED)。
3 输入与校验
| 端点 | 字段 | 校验 |
|---|---|---|
| POST /api/hospitals | name_zh 必填;name_en/grade/city/address/dept_summary/reason;intro_intl/services/visit_process *string 可选,提交即落库(M14 DIF-F2-3 修复前 handler 不收三列致表单值静默丢弃) | name_zh 空 → 400 |
| PUT /api/hospitals/:id | 同上 + version 必填;富文本三列 *string 三态:缺省(null/不传)=保留现值、空串=显式置空、有值=更新(SQL COALESCE(?, col) 兜底);封面图不进本端点(走 F2.04 三步协议) | version CAS 不符 → 409 |
| POST /api/hospitals/:id/status | from/to/reason 三必填 | 白名单外迁移 → 409;from 与库内现状不符 → 409 |
4 处理流程
5 输出与结果状态
建院 201 {id};编辑/流转 204;AdminList 200 {items: [{id, name_zh, name_en, grade, city, address, dept_summary, intro_intl, services, visit_process, status, version, updated_at}]}(富文本三列 COALESCE(col,'') 下发空串非 null——NULL 不可扫入非指针 string,且前端不下发 null)。
6 状态流转
DRAFT → PUBLISHED → OFFLINE → PUBLISHED(单向环,无回 DRAFT;白名单 hospital/pure.go:99,守卫测试锁定)。流转 = TransitionHospitalInTx 唯一入口:status 谓词 CAS + lifecycle(from/to)+ op_log(reason) 同事务(迁移-日志同路径)。
7 边界与异常
| 场景 | 行为 |
|---|---|
| 并发编辑(version 过期) | 409 ErrVersionConflict |
| 非法迁移(如 DRAFT→OFFLINE) | 409 ErrInvalidHospitalTransition(白名单前置拦截) |
| from 与库内现状不符 | 409(CAS 谓词命中 0 行) |
| op_log 写失败 | 仅 Warn 不回滚(审计尽力而为;流转路径例外——同事务强一致) |
原状:Update SQL 硬编码置空富文本三列与封面图(service_write.go 传 nil/"" 占位),web 无编辑入口。修复(M13):三列改 *string 三态(缺省=保留,SQL COALESCE(?, col) 兜底防「不传即清空」);封面图从编辑语句剔除(只走 F2.04 三步协议);web 编辑 Modal 九列回填+全量提交(Hospitals.tsx)。回归锁:service_write_test.go 三态表驱动 + e2e F-ADMIN「编辑保留富文本与封面键」断言 |
8 权限与数据规则
hospital:manage(路由级 RequirePermission);op_log 带 actor_id/actor_role/reason。reason建院/编辑可不传、状态流转必填(敏感操作审计纪律)。
F2.04 医院封面图直传与回显
对应 SRS:F-ADMIN-001(图片上传) | 实现落点:写侧
internal/domain/hospital/service_write.go:287(PresignHospitalImage)/:301(ConfirmHospitalImage)+handler_bo.go:118(presign)/:136(confirm);读侧(M15 回显)service.go:151(GetHospitalImage)+handler.go:29(RegisterPublic)→:79(PublicImage)+handler_bo.go:156(HospitalImageBO) | 操作入口:写=web/admin/hospitals操作列「封面」按钮 → 独立 Modal(选图本地预览→直传三步;M14 前按钮不存在、头注失实;confirm 会 version+1 故独立于编辑表单);读=同 Modal 顶部「当前封面」回显(B 端全状态)+ app H5 医院详情页头图(pub 仅 PUBLISHED)
1 功能定义
医院封面图经直传三步协议(总则 §2.5)上传:presign(mime 白名单前置 + 5MB policy)→ 浏览器直传 MinIO → confirm(对象复核 + image_object_key 回填)。与病历协议的差异:mime 必须 image/*、5MB 上限、回填 hospital 列。
回显读侧(M15)为后端代理(否决 presigned GET:localfs 驱动无直链能力且 nginx 不暴露 MinIO 端点,代理沿 DownloadDocument 先例):
- pub
GET /pub/v1/hospitals/:id/image——免 JWT(仅享/pub组限流):<img src>带不了 Authorization 头;PUBLISHED-only 谓词(DRAFT/OFFLINE/未上传/不存在一律 404,存在性不泄露)。 - B 端
GET /api/hospitals/:id/image——JWT +hospital:manage,不限状态(运营上传后 DRAFT 期即可在 Modal 验证)。 - 公开详情(F2.02)派生
image_url相对路径,前端按空串判断是否渲染。
2 触发条件与前置状态
hospital:manage;医院行已存在(confirm 按 id 回填)。
3 输入与校验
| 端点 | 字段 | 校验 |
|---|---|---|
| POST …/image/presign | mime | IsImageMime 白名单前置(非 image/* → 400 ErrBadImageMime);5MB 上限由 PresignPut policy 执行 |
| POST …/image/confirm | key | 非空;HeadObject 复核实际 size/mime(第二道拦截) |
| GET /pub/v1/hospitals/:id/image | :id | >0;须 PUBLISHED 且 image_object_key 非空(否则 404);免 JWT 仅限流 |
| GET /api/hospitals/:id/image | :id | >0;image_object_key 非空(否则 404);不限状态 |
4 处理流程
回显读侧(M15):
5 输出与结果状态
presign 201 {key, upload_url, method, headers};confirm 204;hospital.image_object_key 已回填、version+1、op_log(hospital.image)。读侧(M15):200 image/* 字节(mime 取对象元数据)+ Cache-Control: public, max-age=300;web 封面 Modal 打开即拉 blob 回显「当前封面」,app 详情按 image_url 渲染头图。
6 状态流转
无状态变更(image_object_key 非状态列;version CAS 防并发覆盖)。
7 边界与异常
| 场景 | 行为 |
|---|---|
| mime 非 image/* | 400(第一道前置) |
| 实际 size 超限/类型不符(伪装申报) | 400(confirm 第二道 storage.ErrBadMIME/ErrTooLarge) |
| key 对应对象不存在 | 404(sql.ErrNoRows 映射 ErrHospitalNotFound——mapErrBO 同族) |
| hospital id 不存在 | 404 |
| confirm 与状态流转并发 | 409(服务端内部 version CAS,M8 核查 B2:客户端不传 version——状态流转后客户端版本必然过期) |
| pub 读侧遇 DRAFT/OFFLINE/未上传/不存在 | 一律 404(PUBLISHED-only 谓词,存在性不泄露——同 F2.02 口径);B 端读侧不限状态 |
8 权限与数据规则
hospital:manage;对象 key 由服务端生成(storage.NewKey("hospital"))不可客户端指定路径。读侧(M15):pub 免 JWT 仅 /pub 组 IP 限流 60/min(封面是 PUBLISHED 医院的公开内容,<img src> 带不了鉴权头);B 端读侧走 hospital:manage。
F2.05 科室管理
对应 SRS:F-ADMIN-001(科室专家管理) | 实现落点:
internal/domain/hospital/service_write.go:142-180(CreateDept/UpdateDept)/in_tx.go:40(TransitionDeptStatusInTx)/handler_bo.go:144-205| 操作入口:web/admin/hospitals科室管理
1 功能定义
B 端维护医院下属科室:建科室(INSERT 恒 DRAFT)、编辑(version CAS + 院区归属谓词防跨院改写)、状态流转(与院区同形单向环)。公开面只出 PUBLISHED 科室。
2 触发条件与前置状态
hospital:manage;科室状态随院区独立流转(科室可先于院区 PUBLISHED)。
3 输入与校验
| 端点 | 字段 | 校验 |
|---|---|---|
| POST /api/hospitals/:id/depts | name_zh 必填;name_en/sort | name_zh 空 → 400 |
| PUT /api/hospitals/depts/:id | name_zh/hospital_id/version 必填 | 归属谓词(hospital_id 不符)或 CAS 不符 → 409 |
| POST /api/hospitals/depts/:id/status | from/to/reason 三必填 | 白名单外/现状不符 → 409 |
4 处理流程
与 F2.03 同构(INSERT DRAFT / version CAS UPDATE / 白名单前置 + TransitionDeptStatusInTx 双日志同事务),差异仅在归属谓词 WHERE id=? AND hospital_id=? AND version=?。
5 输出与结果状态
201 {id} / 204;公开详情(F2.02)内嵌 depts[](PUBLISHED,ORDER BY sort,id)。
6 状态流转
DRAFT → PUBLISHED → OFFLINE → PUBLISHED(dept/pure.go:116 同形白名单);lifecycle entity_type=hospital_dept。
7 边界与异常
| 场景 | 行为 |
|---|---|
| hospital_id 与科室实际归属不符 | 409(CAS 命中 0 行——归属谓词防跨院改写) |
| 非法迁移/并发 | 409(同 F2.03) |
| 删除 | 无删除端点——科室只软下架(OFFLINE),历史数据保全 |
8 权限与数据规则
hospital:manage;name_en 实落 NOT NULL DEFAULT ''(not_null 守卫下自觉偏离计划「可空」口径,⚠ DIF-M2 ⑧);建科室 op_log reason 为空串(低敏操作)。
F2.06 专家库管理
对应 SRS:—(M10 扩展批次,SRS 无对应条目) | 实现落点:
internal/domain/hospital/service_write.go:199(CreateExpert)/:214(UpdateExpert)/:265(ListExperts)/in_tx.go:66(TransitionExpertStatusInTx)/handler_bo.go:218-302| 操作入口:web/admin/hospitals专家管理;医生选人消费面见 F6.01
1 功能定义
B 端维护医院科室下的出诊专家(姓名/职称/专长/简介/排序),供预诊断阶段医生锚定科室选人(F6.01)。与 dept 完全同构(状态机/双日志/归属谓词),M10 落地(delta/0010 加表)。
2 触发条件与前置状态
hospital:manage;专家挂 hospital_id + dept_id 双归属;状态随院区/科室独立流转。
3 输入与校验
| 端点 | 字段 | 校验 |
|---|---|---|
| POST /api/hospitals/:id/experts | dept_id/name/title/specialty/sort 必填;intro 可空指针 | name 空 → 400 |
| GET /api/hospitals/:id/experts | dept_id query 可选(0=不限) | 管理面出全态(含 DRAFT) |
| PUT /api/hospitals/experts/:id | dept_id/name/title/specialty/sort/hospital_id/version 必填;intro *string 三态同 F2.03(缺省=保留/空串=置空/有值=更新,COALESCE(?, intro)) | 归属+version CAS → 409 |
| POST /api/hospitals/experts/:id/status | from/to/reason 三必填 | 白名单 → 409 |
4 处理流程
与 F2.05 同构;ListExperts 的 WHERE 装配单点 BuildExpertListWhere(onlyPublished, deptID) 纯函数——管理面 onlyPublished=false,医生选人面(diagnosis 域独立 SQL,F6.01)onlyPublished=true 仅 dept_id+PUBLISHED 两谓词(⚠ DIF-M10 ⑤:不共享本函数,dept 锚下 hospitalID 参数无意义)。
5 输出与结果状态
201 {id} / 200 {items: [{id, hospital_id, dept_id, name, title, specialty, sort, status, version}]} / 204。
6 状态流转
DRAFT → PUBLISHED → OFFLINE → PUBLISHED(expert/pure.go:133 同形白名单);lifecycle entity_type=hospital_expert;TransitionExpertStatusInTx 为 AST 守卫白名单点名函数(M10 扩射程)。
7 边界与异常
| 场景 | 行为 |
|---|---|
| 专家改名/下架 | ticket.expert 文本快照不回写(医疗记录语义,F6.01) |
| 跨院改写 | 409(hospital_id 归属谓词) |
UpdateExpert 补 intro = COALESCE(?, intro) 三态(service_write.go:214);建专家表单补简介输入。专家列表/独立编辑 UI 仍未立项(「只建不列」口径不变——API 层已可编辑) |
8 权限与数据规则
- 管理 4 端点挂既有
hospital:manage(14 码矩阵不扩——⚠ DIF-M10 ⑤ 权限面不蔓延)。 - 无删除端点(只软下架);排序
sort, id。