2026-05-30 00:37:34 +08:00
|
|
|
|
# 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. 目录结构
|
|
|
|
|
|
|
2026-05-30 12:17:41 +08:00
|
|
|
|
采用**扁平布局**(顶层平铺包、单 `main.go` + `-mode` 子命令),与参考项目 `chis_upload` 保持同款形状,降低团队接手成本。
|
|
|
|
|
|
与 `chis_upload` 的差别只在"内容"——无 `chis/`(逆向)、无 `middleware/`(cookie)、无 SM2,新增 `osi/`/`mapping/`/`pipeline/`。
|
|
|
|
|
|
|
2026-05-30 00:37:34 +08:00
|
|
|
|
```
|
|
|
|
|
|
chis_osi/
|
2026-05-30 12:17:41 +08:00
|
|
|
|
├── 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 客户端封装(可选)+ 文件存储
|
2026-05-30 00:37:34 +08:00
|
|
|
|
├── handler/ HTTP handlers(server 模式对外接口)
|
|
|
|
|
|
├── router/ 路由
|
|
|
|
|
|
├── docs/
|
2026-05-30 12:17:41 +08:00
|
|
|
|
└── AGENTS.md / CLAUDE.md 协作规范
|
2026-05-30 00:37:34 +08:00
|
|
|
|
```
|
|
|
|
|
|
|
2026-05-30 12:17:41 +08:00
|
|
|
|
> 不使用 `internal/` 与 `cmd/`:团队 `AGENTS.md` 偏好扁平、避免过度抽象,且 `chis_upload` 已是单 `main.go`+`-mode` 形态,保持一致优先于"标准 Go 布局"。
|
2026-05-30 00:37:34 +08:00
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## 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 节)。
|
|
|
|
|
|
|
2026-05-30 12:17:41 +08:00
|
|
|
|
### 4.4 `source` —— 任务源
|
2026-05-30 00:37:34 +08:00
|
|
|
|
|
|
|
|
|
|
- 拉取待上送明细(替代旧项目的本地 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. 关键时序:一条体检记录的投递
|
|
|
|
|
|
|
|
|
|
|
|
```
|
2026-05-30 12:17:41 +08:00
|
|
|
|
deliver-worker source mapping osi.Client CHIS OSI
|
2026-05-30 00:37:34 +08:00
|
|
|
|
│ 拉取任务 │ │ │ │
|
|
|
|
|
|
│ ───────────────────▶│ │ │ │
|
|
|
|
|
|
│ ◀─ 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_base_url: "http://<hostname>" # 不含 /osi/api
|
|
|
|
|
|
osi_org_code: "<机构社会信用代码>"
|
|
|
|
|
|
osi_user_name: "<平台分配用户名/DSFMC>"
|
|
|
|
|
|
osi_ask: "<平台下发密钥,仅参与签名>" # 敏感,建议走环境变量
|
|
|
|
|
|
osi_device_sn: "<设备序列号>"
|
|
|
|
|
|
osi_operate_user: "<默认责任医生ID>" # 可被任务覆盖
|
|
|
|
|
|
osi_timeout_sec: 20
|
|
|
|
|
|
socks5_proxy: "" # 可选,保留
|
|
|
|
|
|
|
|
|
|
|
|
# === 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
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
**已删除**(相比旧项目):`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. **沙箱环境地址与一组真实可用账号**,用于回归样本采集。
|
|
|
|
|
|
|
|
|
|
|
|
> 这些问题不阻塞架构落地:薄客户端、契约、映射层、流水线均可先按现有文档搭骨架,再以联调样本校准。
|