Files
chis_osi/docs/03-目标架构设计.md
T
ilaandClaude Opus 4.8 4594f02378 docs: 明确 OSI 访问为 host+proxy 组合及代理语义
- socks5_proxy 扶正为必填(OSI 主机在内网、不可直连,经 SOCKS5 到达)
- 作用域限定:代理仅用于 OSI 客户端,PHIS 走直连
- 访问语义:非空即强制走代理、不因代理故障回退直连,空才直连;
  代理不通按网络错误如实失败,交重试/熔断处理

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-30 15:53:14 +08:00

268 lines
16 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 / ...
├── 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` —— 薄客户端
唯一与平台对话的层。核心是一个泛化调用:
```go
// 伪代码:统一调用。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**。这里维护一份经联调校准的结构体与常量,承担"文档坑点的唯一修正点"。
信封统一:
```go
type Envelope struct {
ServiceID string `json:"serviceId"`
UploadInfo any `json:"uploadinfo,omitempty"` // 创建/更新
BaseInfo any `json:"baseInfo,omitempty"` // 查询
}
type ManageInfo struct { DSFMC, OperateUnit, OperateUser string }
```
### 4.3 `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.4 `source` —— 任务源
- 拉取待上送明细(替代旧项目的本地 mock 文件)。
- 任务模型:`{taskId, dataType, payload(PHIS原始), ...}`。
- 投递后回写状态 `done/retry/failed`(旧项目一直 TODO,新项目做实)。
- `trace_id` 建议直接用 PHIS 任务号,贯穿日志与报告。
### 4.5 `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.6 `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. 配置(对比旧项目,做减法)
```yaml
# === 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. **沙箱环境地址与一组真实可用账号**,用于回归样本采集。
> 这些问题不阻塞架构落地:薄客户端、契约、映射层、流水线均可先按现有文档搭骨架,再以联调样本校准。