Files
chis_osi/docs/03-目标架构设计.md
T

16 KiB
Raw Blame History

03 · 目标架构设计(chis_osi)

本文给出 chis_osi 的整体架构、目录结构、数据流、分层职责与关键设计决策。 设计原则延续团队约定:清晰分层、面向初级维护者、不过度抽象、优先标准库。


1. 设计目标

  1. 薄客户端:OSI 调用收敛为「MD5 签名 + 统一信封 + HTTP 传输」三件事,一个 Call 贯穿。
  2. 厚映射层:把"PHIS 数据 → OSI 文档契约"的字段/字典映射做成显式、表驱动、可单测的独立层——这是新项目的核心资产。
  3. 可靠投递:继承旧项目的重试分类/幂等/熔断/批次报告,并用 checkId 升级幂等。
  4. 无状态、弱依赖:删除登录/Cookie/Redis 会话;Redis 仅用于幂等与报告缓存,且完全可选。
  5. 可联调、可对账、可交接:一套 report log + 快照,trace 贯穿 PHIS 任务号 / checkId。

2. 系统上下文

┌────────────┐   拉取待上送任务/明细    ┌─────────────────────────────┐   POST /osi/api/...   ┌──────────────┐
│   PHIS     │ ───────────────────────▶ │           chis_osi          │ ────签名+信封+JSON──▶ │  CHIS OSI    │
│ 公卫上游   │ ◀─────────────────────── │  (本项目:转换 + 投递 + 对账) │ ◀──── code/data ───── │  省基卫平台  │
└────────────┘   回写状态(done/fail)     └─────────────────────────────┘                       └──────────────┘
                                              │  ▲
                                  字典缓存读写 │  │ 可观测/幂等
                                              ▼  │
                                        ┌──────────────┐
                                        │ Redis(可选)  │
                                        │ + 本地文件   │
                                        └──────────────┘

两种运行形态(同一份代码、子命令切换):

  • server:对外提供同步 HTTP API(供其它系统直接调用投递),便于联调与按需触发。
  • deliver-worker:定时从 PHIS 拉取 → 映射 → 投递 → 回写状态,是生产主力。

3. 目录结构

采用扁平布局(顶层平铺包、单 main.go + -mode 子命令),与参考项目 chis_upload 保持同款形状,降低团队接手成本。 与 chis_upload 的差别只在"内容"——无 chis/(逆向)、无 middleware/(cookie)、无 SM2,新增 osi//mapping//pipeline/。

chis_osi/
├── main.go            入口:-mode server|deliver(沿用 chis_upload 的单入口 + flag 风格)
├── config/            viper 配置(osi/phis/redis/proxy)
├── osi/               ★ 薄客户端:与平台的全部交互
│   ├── sign.go        MD5 签名(ts + password)
│   ├── transport.go   HTTP 发送、超时、SOCKS5
│   ├── client.go      Call(ctx, serviceId, body, &out):注入头+信封+发送+判码
│   ├── codes.go       返回码常量与成功/可重试判定
│   ├── jkda.go        档案:Create/Update/Find/FindRqbj
│   ├── jktj.go        体检:Create/Update/Query/List/Last
│   ├── lnr.go         老年人:自理评估、中医体质辨识
│   ├── zyjkzd.go      中医健康指导
│   └── public.go      网格/责任医生/药品/机构 字典查询
├── contract/          ★ 校验后的接口契约(请求/响应结构体 + serviceId 常量)
│   ├── envelope.go    通用信封:{serviceId, uploadinfo|baseInfo, manageInfo}
│   └── jkda.go / jktj.go / lnr.go / ...
├── cache/             公开字典缓存:网格/责任医生/机构 → 映射层主数据反查
├── mapping/           ★ PHIS→OSI 映射(本项目核心)
│   ├── dict.go        码表:性别/民族/血型/职业/文化程度/婚姻/医保...(双向)
│   ├── health_record.go  档案字段映射 + 校验
│   ├── health_check.go    体检字段映射
│   ├── elderly.go         老年人映射
│   └── checkid.go     checkId 生成(确定性,幂等键来源)
├── source/            PHIS 拉取客户端 + 任务模型 + 状态回写
├── pipeline/          投递编排:校验→映射→调用→分类→重试→幂等→报告
│   ├── deliver.go     单条投递
│   ├── retry.go       重试与错误分类
│   ├── idempotency.go 基于 checkId 的去重存储(redis|file)
│   ├── circuit.go     熔断
│   └── report.go      批次报告
├── observ/            report log(redis 优先,文件降级)+ 快照 + trace
├── store/             redis 客户端封装(可选)+ 文件存储
├── handler/           HTTP handlers(server 模式对外接口)
├── router/            路由
├── docs/
└── AGENTS.md / CLAUDE.md   协作规范

不使用 internal/ 与 cmd/:团队 AGENTS.md 偏好扁平、避免过度抽象,且 chis_upload 已是单 main.go+-mode 形态,保持一致优先于"标准 Go 布局"。


4. 分层职责(自底向上)

4.1 osi —— 薄客户端

唯一与平台对话的层。核心是一个泛化调用:

// 伪代码:统一调用。serviceId 固定常量;body 是已映射好的数据节点;out 反序列化目标。
func (c *Client) Call(ctx context.Context, serviceID string, body any, out any) (Result, error) {
    ts := nowMillis()                                   // 13 位毫秒
    pwd := md5Hex("ts=" + ts + "&ask=" + c.ask)         // 32 位小写
    headers := map[string]string{
        "Content-Type": "application/json",
        "orgCode":  c.orgCode,
        "deviceSN": c.deviceSN,
        "ts":       ts,
        "userName": c.userName,
        "password": pwd,
    }
    payload := envelope{ServiceID: serviceID, Body: body} // 按 create/query 决定 uploadinfo|baseInfo
    status, raw, err := c.transport.PostJSON(ctx, c.baseURL+"/osi/api"+pathOf(serviceID), payload, headers)
    // 记录 report log;解析 {code,message,data};按 code 判定成功/超时(405)
    ...
}

要点:

  • 签名只在这一层,业务层完全无感。
  • code == "1" 成功;405 超时(可重试);其它失败。统一在 codes.go 判定。
  • 各 jkda.go/jktj.go/... 只是给 Call 包一层带类型的方法(如 CreateHealthRecord(ctx, *contract.HealthRecordCreate))。
  • 传输层从旧项目 transport.go/http_client.go 直接移植(去掉 cookiejar、去掉网页拟态头)。

4.2 contract —— 校验后的接口契约

按 01 文档第 7 节,不照搬 docx。这里维护一份经联调校准的结构体与常量,承担"文档坑点的唯一修正点"。 信封统一:

type Envelope struct {
    ServiceID  string `json:"serviceId"`
    UploadInfo any    `json:"uploadinfo,omitempty"` // 查询/创建/更新统一信封
}
type UploadInfo struct {
    BaseInfo   any        `json:"baseInfo,omitempty"`
    ManageInfo ManageInfo `json:"manageInfo"`
}
type ManageInfo struct { DSFMC, OperateUnit, OperateUser string }

4.3 cache —— 公开字典缓存

承接 osi/public.go 的网格、责任医生、机构查询结果,生成内存 DictionarySnapshot,按名称反查创建档案所需的 regionCode、manaDoctorId、manaUnitId。持久化存储通过可选接口接入,Redis 不可用时只影响持久化,不阻断内存快照刷新和映射。

4.4 mapping —— PHIS→OSI 映射(核心)

把旧项目"对齐 Chrome"的隐式逻辑,重写为"对齐文档"的显式逻辑:

  • 字典层 dict.go:集中所有码表(性别 0/1/2/9、民族 56 项、血型、职业、文化程度、婚姻、医保支付方式、人群标记 personSign 等)。提供 PHISToOSI / OSIToPHIS 双向查表,未命中有明确报错而非静默丢值。
  • 字段映射:每个业务域一个文件,函数签名形如 func MapHealthRecord(src phis.Record) (contract.HealthRecordCreate, []ValidationError)。返回结构化校验错误,缺必填字段在投递前就拦截。
  • 多选字段:既往史/家族史/残疾等"逗号拼接多选"集中处理。
  • checkId 生成 checkid.go:确定性生成(如 源系统ID|业务类型|源记录主键 哈希),保证同一源记录重试得到同一 checkId → 平台侧不重复建档。这是幂等的根。

完整度(completeLevel/perfection):默认不本地计算,按文档字段如实上送,依赖平台计算。 若联调发现平台要求接入方计算,再把旧项目 health_record_complete_level.go/health_check_perfection.go 移植进 mapping/(开放问题,见第 8 节)。

4.5 source —— 任务源

  • 拉取待上送明细(替代旧项目的本地 mock 文件)。
  • 任务模型:{taskId, dataType, payload(PHIS原始), ...}。
  • 投递后回写状态 done/retry/failed(旧项目一直 TODO,新项目做实)。
  • trace_id 建议直接用 PHIS 任务号,贯穿日志与报告。

4.6 pipeline —— 投递编排

单条投递流程(继承旧项目 worker 经验):

拉取任务 → 校验+按 dataType 分流 → mapping 映射(+校验) → 计算 checkId
        → 幂等检查(命中 success 则跳过) → osi.Call → 按 code 分类
        → 成功:记 success + 回写 PHIS done
        → 可重试失败:退避重试(最多 N 次) / 触发熔断
        → 不可重试失败:记 failed + 回写 PHIS failed
        → 全程写 report log,结束出批次报告
  • 重试分类(移植并适配 OSI):网络错误 / HTTP 5xx / 429 / code==405(超时) 可重试;参数错、权限错、映射校验错不可重试;退避 2s*attempt,最多 3 次。
  • 幂等:键 = checkId(替代旧项目报文哈希);存储 redis 优先、文件降级。
  • 熔断:连续网络失败达阈值则暂停(阈值/休眠秒可配)。
  • 批次报告:total/success/failed/skipped/retry、失败 Top、逐条明细(沿用旧项目格式)。

4.7 observ / store —— 可观测与存储

  • 一套 report log(收敛旧项目的 apitrace/reportlog/snapshot 三套):记录每次 PHIS 输入、映射结果、OSI 请求/响应、最终判定。
  • Redis 优先、本地 JSONL 降级;Redis 启动失败不阻断(沿用旧项目)。

5. 关键时序:一条体检记录的投递

deliver-worker          source             mapping            osi.Client        CHIS OSI
     │  拉取任务            │                  │                   │                 │
     │ ───────────────────▶│                  │                   │                 │
     │ ◀─ task(原始体检)    │                  │                   │                 │
     │  MapHealthCheck ─────────────────────▶ │                   │                 │
     │ ◀─ contract + 校验ok │                  │                   │                 │
     │  计算 checkId / 幂等检查(未命中)         │                   │                 │
     │  Call(JKTJ00001) ───────────────────────────────────────▶ │                 │
     │                     │                  │   签名+信封+POST ──────────────────▶ │
     │                     │                  │                   │ ◀─ {code:"1"} ── │
     │ ◀── Result(success, phrId...) ──────────────────────────── │                 │
     │  记 success + report │                  │                   │                 │
     │  回写 PHIS done ────▶│                  │                   │                 │

失败分支:code!="1" 且非超时 → 不重试,记 failed + 回写;网络错误/405 → 退避重试,超次数计熔断。


6. 配置(对比旧项目,做减法)

# === OSI 平台 ===
# 访问模式:OSI 主机在内网、不可直连,必须经 SOCKS5 代理(host+proxy 组合,与 chis_upload 一致)
osi_base_url: "http://<内网hostname:port>"  # OSI 主机内网地址,不含 /osi/api
osi_org_code: "<机构社会信用代码>"
osi_user_name: "<平台分配用户名/DSFMC>"
osi_ask: "<平台下发密钥,仅参与签名>"   # 敏感,建议走环境变量
osi_device_sn: "<设备序列号>"
osi_operate_user: "<默认责任医生ID>"    # 可被任务覆盖
osi_timeout_sec: 20
socks5_proxy: "<ip:port>"              # 必填:到达 OSI 内网主机的唯一通路,留空才直连

# === PHIS 任务源 ===
phis_base_url: "..."
phis_token: "..."
phis_region_code: "..."
phis_poll_interval_sec: 5

# === Redis(可选,仅幂等/报告缓存)===
redis_addr: ""                         # 留空=纯文件模式
redis_db: 0
report_log_file_dir: "logs"

# === 投递 ===
retry_max: 3
circuit_fail_threshold: 5
circuit_sleep_sec: 60

host + proxy 组合是固定访问方式,不是可选项:参考 chis_upload 用 chis_host=172.29.20.71:9002 + socks5_proxy=192.168.3.148:11003,OSI 主机同样位于内网、只能经 SOCKS5 代理访问。传输层(移植自 chis_upload 的 http_client.go)已内建 SOCKS5 拨号支持。 注意作用域:socks5_proxy 仅作用于 OSI 客户端;PHIS 任务源走各自地址直连(chis_upload 里 PHIS 是公网 163.177.185.170:4000,不经代理),不要把 PHIS 流量也塞进该代理。

访问语义(固定,不做"智能回退"):socks5_proxy 非空即强制走代理、不因代理故障回退直连;空才直连。初始化仅校验代理地址格式,不探测连通性;代理不通时请求按网络错误如实失败,交由重试/熔断处理,以暴露"代理故障"而非掩盖成"主机不可达"。

已删除(相比旧项目):sm2_pub_key、cookie_ttl、chrome_payload_compat_enable、health_record_debug_log、api_trace_*、登录账号密码、role hint。

敏感项 osi_ask 不写进仓库配置,按 AGENTS.md「不硬编码密钥」走环境变量或部署密文。


7. 与旧项目的映射对照(一图流)

旧项目 新项目 变化
util/encrypt.go(SM2) osi/sign.go(MD5) 重写,体量骤降
chis/login.go+middleware/cookie.go+RedisCookieAuth+validate_cookie.go — 整体删除
chis/identity_chain.go — 删除(用 idCard+checkId)
chis/client.go/transport.go/http_client.go osi/client.go/transport.go 移植简化(去 cookiejar/拟态头)
chis/*_payload_normalizer.go/*_enricher.go mapping/*.go 语义从"对齐网页"改为"对齐文档"
chis/*_complete_level.go/*_perfection.go (默认删除,待联调) 开放问题
worker/(retry/idempotency/circuit/report) pipeline/ 保留升级,checkId 做幂等键
util/reportlog.go+apitrace.go+snapshot observ/ 三套收敛为一套
model/ contract/ 改为"校验后契约"

8. 开放问题(需厂家/联调确认)

  1. 完整度是否需接入方计算?(completeLevel/perfection 是否由平台算;isFillShhj 之外有无完整度入参)
  2. 列表/查询类接口的完整路径与 serviceId(档案列表、自理/体质/中医指导的列表与查询,docx 缺漏)。
  3. 中医健康指导(zyjkzd)的 serviceId 与字段表。
  4. 405 之外的错误码字典(是否有更细的失败码用于重试/告警分级)。
  5. checkId 规则约束(长度 20、是否要求全局唯一、平台是否以 checkId 做幂等去重——若是,则更新走 update 还是 create 幂等覆盖)。
  6. deviceSN 取值来源(一机构一值还是一设备一值)。
  7. 沙箱环境地址与一组真实可用账号,用于回归样本采集。

这些问题不阻塞架构落地:薄客户端、契约、映射层、流水线均可先按现有文档搭骨架,再以联调样本校准。