docs: 初始化 chis_osi 设计文档与协作规范

- docs/: OSI 接口规范分析、旧项目架构评估、目标架构设计、字段与接口映射、实施路线图
- CLAUDE.md: 协作规则(项目认知、osi/mapping/pipeline 三层技术约束、安全/验证、提交规范)
- .gitignore: 补充项目特定忽略(config.yaml/logs/样本等)
- 随项目留存厂家 OSI 规范源材料(docx/xlsx,内部保密,仅限本私有仓库)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
ila
2026-05-30 13:10:17 +08:00
co-authored by Claude Opus 4.8
parent 18bf9a82af
commit 3498542b07
11 changed files with 1014 additions and 28 deletions
+41 -27
View File
@@ -1,27 +1,41 @@
# ---> Go # ---> Go
# If you prefer the allow list template instead of the deny list, see community template: # If you prefer the allow list template instead of the deny list, see community template:
# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore # https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore
# #
# Binaries for programs and plugins # Binaries for programs and plugins
*.exe *.exe
*.exe~ *.exe~
*.dll *.dll
*.so *.so
*.dylib *.dylib
# Test binary, built with `go test -c` # Test binary, built with `go test -c`
*.test *.test
# Output of the go coverage tool, specifically when used with LiteIDE # Output of the go coverage tool, specifically when used with LiteIDE
*.out *.out
# Dependency directories (remove the comment below to include it) # Dependency directories (remove the comment below to include it)
# vendor/ # vendor/
# Go workspace file # Go workspace file
go.work go.work
go.work.sum go.work.sum
# env file # env file
.env .env
# ---> chis_osi 项目特定
# 真实配置(含 orgCode/userName/ask 密钥、PHIS token、服务器地址)——只提交 config.yaml.example
/config.yaml
config.local.yaml
# 运行期产物:日志、报告、快照、幂等状态
/logs/
*.jsonl
# 本地存储/缓存
*.db
*.sqlite
/tmp/
# 含个人信息的联调样本(身份证/姓名等),如需保留请脱敏后手动 add
/samples/*.real.json
+164
View File
@@ -0,0 +1,164 @@
# CLAUDE.md
适用于 `chis_osi` 仓库的 Claude Code 协作规则。
> **当前为 pre-code 阶段**:已 git 管理,仅有设计文档与源材料,代码骨架尚未建立(见路线图阶段 0)。
---
## 会话启动
1. 读取 `docs/README.md` — 了解文档集结构与一句话结论
2. 执行 `git log --oneline -10` — 了解上次做到哪里
3. 确认当前所处阶段(见 `docs/05-实施路线图.md` 的阶段 0~6)
> 需要架构细节读 `docs/03-目标架构设计.md`;需要接口字段/码表读 `docs/01-OSI接口规范分析.md` 与 `docs/04-字段与接口映射.md`。
> 用户说"继续开发""继续上次的"时,完成以上步骤后直接接续,不重新介绍项目背景。
---
## 项目定位
**chis_osi**——对接「广东省基层医疗机构管理系统(CHIS)」厂家(和宇健康科技)提供的**官方统一对外服务接口(OSI)**,
把上游 PHIS(公卫系统)数据转换并投递到省基卫平台。
- **当前阶段**:设计完成、待编码(脚手架尚未建立,见路线图阶段 0)
- **语言/运行**:Go 1.24,单二进制双子命令(`cmd/server` 同步 API、`cmd/deliver` 投递 worker)
- **团队定位**:默认由初级程序员维护,所有改动优先保证可读、可理解、可接手,避免过度抽象
- **核心链路**:PHIS(拉取)→ 字段映射 → MD5 签名 + 统一信封 → CHIS OSI(POST `/osi/api/...`)→ 状态回写
**关键架构判断(务必牢记):**
OSI 是**无状态服务端接口**,鉴权 = 请求头 MD5 签名(`password=md5("ts=<ts>&ask=<ask>")` 32 位小写)。
参考项目 `/mnt/d/GoP/chis_upload`(逆向 Chrome F12 实现)里的 SM2 登录 / Cookie / Redis 会话 / 身份链反查 / 网页报文对齐这部分复杂度**在本项目不存在、不要引入**。
本项目核心资产是 **PHIS→OSI 表驱动字段映射层** 与 **投递流水线**(重试分类/幂等/熔断/批次报告),幂等键用 OSI 的 `checkId`。
源材料(`docs/`):`统一对外服务接口文档-.docx`(OSI 规范 V1.5.7)、`广东省基层...需要的接口.xlsx`(本期 25 个接口)。这两份是厂家标注"内部资料注意保密"的材料,仅在本私有仓库内随项目留存,不得外发或推送到公开仓库。
---
## 目录职责
> 以下为**目标**结构(见 `docs/03-目标架构设计.md` 第 3 节),多数目录尚未创建,按路线图阶段逐步建立。
| 目录 | 说明 |
|------|------|
| `cmd/server/` `cmd/deliver/` | 双入口:同步投递 API / 定时投递 worker |
| `internal/osi/` | ★ 薄客户端:签名、传输、`Call`、各业务域调用方法 |
| `internal/contract/` | ★ 校验后的接口契约(结构体 + serviceId 常量),docx 坑点的唯一修正点 |
| `internal/mapping/` | ★ PHIS→OSI 字段/字典映射 + checkId 生成(本项目核心) |
| `internal/source/phis/` | PHIS 拉取客户端 + 任务模型 + 状态回写 |
| `internal/pipeline/` | 投递编排:校验→映射→调用→分类重试→幂等→熔断→报告 |
| `internal/observ/` `internal/store/` | report log(redis 优先、文件降级)/ redis(可选)+文件存储 |
| `handler/` `router/` | server 模式对外 HTTP 接口与路由 |
| `config/` | viper 读取配置(osi / phis / redis / proxy) |
| `docs/` | 所有设计文档,与代码同等重要 |
---
## 修改原则
- 先读现有实现和相关文档,再动手;优先最小化改动范围。
- 已有实现可复用时,不新增平行实现。
- 先定位根因,再修复问题,不做只遮盖现象的补丁。
- 非任务要求,不改对外接口字段、返回结构、配置键名、文件名、编码。
- 涉及**签名逻辑、serviceId 常量、checkId 生成、码表、幂等存储**时必须保守处理——它们直接影响平台对账与重复建档。
- **不照搬 docx 字段表**:docx 有复制粘贴错误(serviceId 串台、字段名/类型不一致,见 `docs/01` 第 7 节)。以 `contract/` + 联调真实样本为准。
- 架构或字段映射发生变化时,同步更新 `docs/` 对应文件(`docs/03` 架构、`docs/04` 字段映射),不让代码改了文档没跟上。
---
## 技术约定
### 通用
- **语言**:Go 1.24,**优先标准库**,谨慎引入第三方依赖。
- **注释**:必要的中文注释,只写 WHY 不写 WHAT;重点注释业务规则不直观处(字段映射、码表、分支判定)、外部系统约束(OSI 固定字段/签名规则)、易误改的关键路径(签名、checkId、幂等)。
- **错误**:显式处理不吞错;**命名**直接表达用途,避免过度抽象。
### osi(薄客户端)
- 签名只在 `osi` 层完成,业务层无感;用 `crypto/md5` 即可,**不引入 SM2/任何加密库**。
- `ts` 取 13 位毫秒;`password` 取 32 位小写;`ask` **仅参与签名**,不进请求头、不进报文、不进日志。
- 返回判定集中在 `codes.go`:`code=="1"` 成功(注意是**字符串**);`405` 超时(可重试);其它失败。
- 传输层移植自 `chis_upload`(保留 SOCKS5/超时,**去掉 cookiejar 与网页拟态头**)。
### mapping(核心)
- 码表集中 `dict.go`,双向查表,**未命中显式报 ValidationError,绝不静默置空**。
- 映射函数为纯函数,返回结构化校验错误,便于用文档样例 + 联调样本单测。
- `checkId` 必须**确定性生成**:同一源记录重试得同一 checkId,防止平台重复建档。
- 完整度(completeLevel/perfection)**默认不本地计算**,依赖平台;联调确认需自算后再移植旧逻辑。
### pipeline / 可观测
- 重试分类:网络错误 / HTTP 5xx / 429 / `code==405` 可重试;参数错、权限错、映射校验错不可重试;退避 `2s*attempt`,最多 3 次。
- 幂等键 = `checkId`;存储 redis 优先、本地 JSON 降级。
- Redis 完全可选:启动失败不阻断(沿用 `chis_upload` 行为)。
- 一套 report log,不重建 apitrace/snapshot 多套追踪。
---
## 安全要求
- **严禁**将 `ask` 密钥、`orgCode`/`userName`/`deviceSN`、PHIS token、真实账号、含真实地址的配置提交到 git。
- `ask` 等敏感项走环境变量或部署密文,不写进仓库配置、不打印到日志。
- 配置示例用 `config.yaml.example`(占位值,不含真实地址与密钥)。
- 不提交 `logs/`、快照、抓包样本中含真实身份证/个人信息的文件。
---
## 验证要求
改动完成后做最小必要验证:
- **签名改动**:单测校验 `password` 形态(32 位小写)+ 用最简查询接口(机构查询 CXJG00002)打通真实请求。
- **映射改动**:补/跑 `mapping` 单测,用 docx 样例与联调样本对齐;确认必填/码表/格式校验生效。
- **投递流水线改动**:跑批确认批次报告字段(total/success/failed/skipped/retry)与幂等跳过、熔断行为正确。
- 至少执行 `go test ./...` 或与改动最相关的包级测试;无法验证时说明原因和风险。
---
## 文档同步要求
代码和文档同步,不允许代码改了文档没跟上。完成一组相关改动(1~3 个功能/修复/重构)后,自主判断是否同步更新 `docs/`,无需用户提醒。
| 发生什么 | 必须更新 |
|---------|---------|
| 架构或分层发生变化 | `docs/03-目标架构设计.md` |
| serviceId / 接口路径 / 字段映射 / 码表校准 | `docs/01-OSI接口规范分析.md`、`docs/04-字段与接口映射.md` |
| 联调确认了开放问题(完整度 / checkId 规则 / 缺漏接口等) | `docs/03` 第 8 节、`docs/04` 第 7 节待补清单 |
| 阶段推进或验收通过 | `docs/05-实施路线图.md` 勾选项与阶段状态 |
| 做了重要技术决策 | `docs/decisions/00N-简短描述.md`(背景 / 决策 / 原因 / 影响) |
---
## 提交规范
> 提交、推送仅在用户要求时进行。代码按每个逻辑改动单独 commit。
格式:`<type>(<scope>): <简短描述>`
| type | 用途 | | scope | 对应 |
|------|------|---|------|------|
| `feat` | 新功能 | | `osi` | 薄客户端 |
| `fix` | 修复问题 | | `mapping` | 字段/码表映射 |
| `docs` | 文档变更 | | `pipeline` | 投递流水线 |
| `refactor` | 重构(不改功能) | | `contract` | 接口契约 |
| `chore` | 构建/配置/依赖 | | `phis` | 任务源 |
| `test` | 测试相关 | | `config` `handler` … | 其余包/文档 |
```
feat(osi): 实现 MD5 头签名与统一 Call
feat(mapping): 落地健康档案字段映射与 checkId 生成
docs: 校准 jkda serviceId 并更新接口映射文档
chore: 初始化 go module 与双子命令骨架
```
---
## 提交前检查
- [ ] 未改动无关文件,未引入不必要重构或第三方依赖
- [ ] 未硬编码 `ask` 密钥、机构码、账号、PHIS token、服务器地址
- [ ] 未提交 `config.yaml`、`logs/`、含个人信息的样本
- [ ] 未留下临时代码、调试输出、未说明的 `TODO`
- [ ] serviceId / 字段映射 / 架构变更已同步更新 `docs/` 对应文件
- [ ] 重要技术决策已补 `docs/decisions/` ADR 文件
- [ ] `go test ./...` 通过(或说明未验证的原因与风险)
+1 -1
View File
@@ -1,3 +1,3 @@
# chis_osi # chis_osi
chis官方接口对接. chis官方接口对接.
+199
View File
@@ -0,0 +1,199 @@
# 01 · OSI 接口规范分析
来源:《广东省基层医疗机构管理系统 统一对外服务接口 API 规范文档 V1.5.7》(2023-02-02,和宇健康科技)
配套:《广东省基层医疗机构管理系统服务接口对接回复后需要的接口.xlsx》(本期需对接的 25 个接口清单)
---
## 1. 总体约定
| 项 | 约定 |
| --- | --- |
| 协议 | HTTP,方法统一 `POST` |
| 通用 URL | `http://${hostname}/osi/api/...`(各接口在此基础上拼子路径) |
| 报文格式 | 请求/响应均为 JSON 字符串 |
| 字符集 | UTF-8 |
| 返回码 | `code == "1"` 成功;其它值失败;`405` = 服务调用超时 |
> 注意:返回码 `code` 是**字符串** `"1"`,不是数字 `1`;判定成功务必按字符串比较或归一化处理。
### 1.1 响应统一结构
```json
{ "code": "1", "message": "操作成功", "data": { ... } }
```
- `data` 在创建类接口是对象(如 `{ "phrId": "...", "createUnit": "..." }`);
- 在查询/列表类接口可能是对象或数组,需按接口分别建模。
---
## 2. 鉴权:请求头 MD5 签名(无状态)
所有接口通过 **HTTP 请求头**鉴权,**没有登录、没有 Cookie、没有 Session**:
| 请求头 | 说明 |
| --- | --- |
| `Content-Type` | `application/json` |
| `orgCode` | 机构编码(机构的社会统一信用代码,约 18~20 位)。接入方先把组织/机构信息提供给平台,由平台生成 |
| `deviceSN` | 设备序列号(创建/更新/查询类接口均要求必填) |
| `ts` | 13 位毫秒级时间戳 |
| `userName` | 平台分配的用户名(对应一个 `ask` 密钥;同时也是报文里的第三方接入公司名称编码 `DSFMC`) |
| `password` | `md5("ts=<时间戳>&ask=<密钥>")`,取 **32 位小写** |
签名要点:
- `password` 的明文是字符串 `ts=<ts>&ask=<ask>`,其中 `<ts>` 必须与请求头里发送的 `ts` 完全一致;
- `ask` 为平台下发的密钥,**只参与签名,绝不放进请求头或报文**;
- 每个请求现算 `ts`/`password`,天然防重放(平台侧通常校验 `ts` 时效)。
> 对比旧项目:这里**不需要** SM2 公钥加密、不需要 `lw_d`/`d`/查询 `d` 加密参数、不需要按年份变化的动态字段名。
> 一个标准库 `crypto/md5` 即可完成全部鉴权。
---
## 3. 请求报文信封
请求体(HTTP body)统一为:
```json
{
"serviceId": "<固定服务码>",
"uploadinfo": { ... } // 创建/更新类:写入数据
}
```
或查询类:
```json
{
"serviceId": "<固定服务码>",
"baseInfo": { ... } // 查询条件
}
```
- `serviceId` 是每个接口的**固定常量**(见第 5 节映射表),平台据此路由业务。
- 创建/更新类信封内含 `manageInfo`(管理节点)+ 业务数据节点:
- `manageInfo.DSFMC` = 第三方接入公司名称编码(与请求头 `userName` 一致)
- `manageInfo.operateUnit` = 操作机构编码(与请求头 `orgCode` 一致)
- `manageInfo.operateUser` = 责任医生 ID
- 文档中创建样例外层出现的 `"headers": {...}` 仅用于演示 HTTP 头,**不是 body 的一部分**;实际 POST body 只发 `serviceId`+数据节点。
---
## 4. 本期需对接接口清单(来自 xlsx,25 项)
| # | 模块 | 接口名称 |
| --- | --- | --- |
| 1 | 档案 | 个人健康档案信息列表查询 |
| 2 | 档案 | 个人健康档案信息创建 |
| 3 | 档案 | 个人健康档案信息更新 |
| 4 | 档案 | 个人健康档案信息查询 |
| 5 | 档案 | 居民人群标记与子档案标记查询 |
| 6 | 体检 | 健康体检已检/待检人员列表查询 |
| 7 | 体检 | 最近一次健康体检查询 |
| 8 | 体检 | 健康体检创建 |
| 9 | 体检 | 健康体检更新 |
| 10 | 体检 | 健康体检查询 |
| 11 | 老年人·中医体质辨识 | 列表查询 |
| 12 | 老年人·中医体质辨识 | 创建 |
| 13 | 老年人·中医体质辨识 | 更新 |
| 14 | 老年人·生活自理能力评估 | 列表查询 |
| 15 | 老年人·生活自理能力评估 | 创建 |
| 16 | 老年人·生活自理能力评估 | 更新 |
| 17 | 老年人·生活自理能力评估 | 查询 |
| 18 | 老年人·中医健康指导 | 列表查询 |
| 19 | 老年人·中医健康指导 | 保存 |
| 20 | 老年人·中医健康指导 | 更新 |
| 21 | 老年人·中医健康指导 | 查询 |
| 22 | 公共服务 | 查询网格地址 |
| 23 | 公共服务 | 责任医生查询 |
| 24 | 公共服务 | 药品目录查询 |
| 25 | 公共服务 | 机构查询 |
---
## 5. 接口 → 路径 → serviceId 映射表
> 以文档正文为准整理。文档中部分 serviceId 因复制粘贴存在错误(见第 7 节),下表为校正后的推断值,**联调时需逐一回填确认**。
### 5.1 健康档案(JKDA)
| 业务 | 路径 | serviceId | 主数据节点 |
| --- | --- | --- | --- |
| 创建 | `/osi/api/jkda/create` | `JKDA00001` | `uploadinfo.healthRecord` + `pastHistory`/`jwsjb`/`jwsss`/`jwsws`/`jwssx`/`familyMiddle` |
| 查询 | `/osi/api/auto/jkda/find` | `JKDA00002` ⚠ 文档样例误写 `TNB00004` | `baseInfo`(idCard/phrid/personName 三选一) |
| 更新 | `/osi/api/jkda/update` | `JKDA00003` | 同创建 |
| 人群/子档案标记查询 | `/osi/api/jkda/findrqbj` | `JKDA00005` | `baseInfo`(phrid/idCard 二选一) → `data.personSign` |
| 列表查询 | (清单第 1 项,文档正文未见独立路径,疑与 `find` 合并或缺漏) | 待确认 | — |
`findrqbj` 返回的 `personSign` 取值:`PU` 普通 / `GRQY` 已签约 / `LAO` 老年人 / `GAO` 高血压 / `TANG` 糖尿病 / `FU` 孕产妇 / `ER` 儿童 / `FEI` 肺结核 / `JING` 精神障碍 / `CAN` 残疾人,多个以逗号分隔。
### 5.2 健康体检(JKTJ)
| 业务 | 路径 | serviceId |
| --- | --- | --- |
| 创建 | `/osi/api/jktj/create` | `JKTJ00001` |
| 查询 | `/osi/api/auto/jktj/query` | `JKTJ00002` |
| 更新 | `/osi/api/jktj/update` | `JKTJ00003` |
| 已检/待检人员列表 | `/osi/api/auto/jktjlist/query` | `JKTJLIST00002` |
| 最近一次体检 | `/osi/api/auto/jktjlscx/query` | `JKTJLSJL00002` |
> 体检报文体量大(hcData/lsData/exaData/aeData 等数十~上百字段),是字段映射工作量最大的一块。
### 5.3 老年人(LNR)
| 业务 | 路径 | serviceId |
| --- | --- | --- |
| 生活自理能力评估·创建 | `/osi/api/lnrzlpg/create` | `LNRZLPG00001` |
| 生活自理能力评估·更新 | `/osi/api/lnrzlpg/update` | `LNRZLPG00003` |
| 生活自理能力评估·查询/列表 | (清单第 14/17 项) | 待确认(推断 `LNRZLPG00002`) |
| 中医体质辨识·创建 | `/osi/api/lnrzyygl/create` | `LNRZYTZ00001` |
| 中医体质辨识·更新 | `/osi/api/lnrzyygl/update` | `LNRZYTZ00003` |
| 中医体质辨识·查询/列表 | (清单第 11 项) | 待确认(推断 `LNRZYTZ00002`) |
> 注意路径与 serviceId 的命名不一致:中医体质辨识的**路径**用 `lnrzyygl`,而 **serviceId** 用 `LNRZYTZ`。对接时以文档逐条为准,不要据路径猜 serviceId。
### 5.4 中医健康指导(ZYJKZD)
| 业务 | 路径 | serviceId |
| --- | --- | --- |
| 保存 | `/osi/api/auto/zyjkzd/create` | 待确认 |
| 更新 | `/osi/api/auto/zyjkzd/update` | 待确认 |
| 查询/列表 | (清单第 18/21 项) | 待确认 |
### 5.5 公共服务(查询类,请求体 `baseInfo`)
| 业务 | 路径 | serviceId | 关键入参 | 关键出参 |
| --- | --- | --- | --- | --- |
| 查询网格地址 | `/osi/api/auto/wgdzcx/query` | `WGDZ00001` | `parentCode`,`pageNo`,`operateUser` | `regionCode`,`regionName`,`isFamily`(层级) |
| 责任医生查询 | `/osi/api/auto/zryscx/query` | `ZRYS00001` | `manaUnitId`,`operateUser` | `personId`,`personName` |
| 药品目录查询 | `/osi/api/auto/ypmlcx/query` | `YPML00001` | `pageNo`,`ypmc`,`pym` | `ypmc`,`ypdw`,`ypgg`,`jldw` |
| 机构查询 | `/osi/api/auto/cxjg/query` | `CXJG00002` | `organizCode`,`parentId` | `organizCode`,`organizName`,`organizType`,`parentId` |
> 公共服务接口是**基础字典服务**:网格地址→`regionCode`、责任医生→`personId`、机构→`organizCode`。
> 它们正是创建类接口所需主数据(`regionCode`/`manaDoctorId`/`manaUnitId` 等)的来源,建议优先打通并本地缓存为字典。
---
## 6. 关键字段语义(创建健康档案为例)
- `checkId`:**第三方系统业务唯一识别码(流水码)**。这是接入方自己生成、用于和平台对账与去重的关键键,是新项目幂等设计的基石(见 04 文档)。
- `idCard` + `personName` + `sexCode` + `birthday`:人口学主键四要素,必填。
- `regionCode`:12 位行政区划/网格代码,来自「网格地址查询」。
- `manaDoctorId` / `manaUnitId` / `operateUser`:责任医生与管辖机构,来自「责任医生查询」「机构查询」。
- 大量字段是**带码表的枚举**(民族 56 项、职业、文化程度、婚姻、血型、医保支付方式、既往史/家族史多选用逗号拼接等),是映射层的主要工作量。
- `isFillShhj`(y/n)显式标记是否填写生活环境(`familyMiddle`)。
- 推断:完整度(旧项目里的 `completeLevel`/`perfection`)很可能由**平台服务端自行计算**,接入方只需如实上送文档字段;这与旧项目"客户端重算完整度"形成对比,需在联调中确认(见 03 文档"开放问题")。
---
## 7. 文档坑点(务必在联调中校验)
官方 docx 存在明显的复制粘贴/排版错误,建模前需逐一核对:
1. **serviceId 串台**:`jkda/find` 的请求样例里 serviceId 写成 `TNB00004`(糖尿病接口的码),实际应为 `JKDA00002`。
2. **字段名不一致**:创建档案家族史父亲在字段定义里叫 `jzsfqn`,在请求样例里叫 `jzsfq`;现住址门牌号在创建里 `addressNumber`、在查询里 `adressNumber`(少一个 d)。
3. **类型不一致**:`familyMiddle` 在创建接口标 `object`,在查询接口标 `list`;`pastHistory` 同样在不同接口标注不同。
4. **样例 JSON 非法**:多处查询样例花括号不配对(如 `{ "baseInfo": {...} }, "serviceId": "..." }`),应理解为 `{ "serviceId": "...", "baseInfo": {...} }`。
5. **列表查询路径缺漏**:xlsx 要求"档案列表查询""自理/体质/中医指导列表查询""自理查询"等,但 docx 正文未给出全部独立路径与 serviceId,需向厂家索要补充。
> 结论:**不能直接照搬 docx 字段表生成契约**。落地前应整理一份「校验后的接口契约」(见 03 文档 `contract/` 包),
> 并以厂家沙箱环境的真实请求/响应样本做回归校准。
+82
View File
@@ -0,0 +1,82 @@
# 02 · 旧项目(chis_upload)架构评估
`chis_upload` 是通过**逆向 Chrome F12 网页接口**实现的 PHIS→CHIS 上传工具(Go 1.24)。
本文梳理它的分层与组件,区分「哪些复杂度是被网页逆向逼出来的、新项目应删除」与「哪些工程经验值得保留」。
---
## 1. 整体定位与数据流
旧项目本质是一个**数据搬运管道**:
```
PHIS(上游公卫系统) --拉取任务--> chis_upload --(模拟网页登录态)--> CHIS 网页后端
```
它对外暴露一组 HTTP API(`/api/health-record/save` 等),内部再以网页同款报文调用 CHIS。
另有 worker 模式:轮询 PHIS 拉任务、按 `dataType` 分流、调用自身 save API 完成投递。
---
## 2. 分层与目录职责(摘自其 AGENTS.md 与源码)
| 目录 | 职责 |
| --- | --- |
| `config/` | viper 读取 `config.yaml` |
| `util/` | SM2 加密、Redis、报告日志(reportlog)、apitrace、日志 |
| `chis/` | CHIS 调用核心:client/transport/auth/login + 各业务域 save/query + 归一化器/补齐器/身份链/完整度计算 |
| `model/` | 请求/响应结构体 |
| `handler/` | 对外 HTTP API 处理函数 |
| `middleware/` | Cookie 中间件(登录态注入) |
| `router/` | 路由注册(每个 save/query 都套 `authMW`) |
| `worker/` | phis_poll_worker、mock_task_worker、幂等存储、批次报告 |
`chis/` 目录尤其庞大(30+ 文件),是复杂度集中地。
---
## 3. 复杂度来源分析
### 3.1 由「网页逆向」逼出来的复杂度 —— 新项目应整体删除
| 旧项目机制 | 为什么存在 | OSI 下的命运 |
| --- | --- | --- |
| **SM2 加密**(`util/encrypt.go`:`EncryptSM2`/`GenerateLwD`/`GenerateQueryD`/`YearLlxKey`) | 网页登录与查询参数 `lw_d`/`d` 用 SM2 公钥加密,按年份还有动态字段名 `2026llx` | ❌ 删除。OSI 用 MD5 头签名 |
| **登录流程**(`chis/login.go`、`LoginWithRoleHint`、`identity_chain.go`) | 网页要先登录拿 Cookie,且要处理多角色选择 | ❌ 删除。OSI 无登录 |
| **Cookie/会话**(`middleware/cookie.go`、`chis/auth.go` `RedisCookieAuth`、`validate_cookie.go`、`util/redis.go` cookie 缓存、`cookie_ttl` 配置) | 维持网页登录态、跨请求复用、过期重登 | ❌ 删除。OSI 每请求现签,无状态 |
| **身份链反查**(`chis/identity_chain.go`:`idCard → empiId/phrId/...`) | 网页保存需要先把身份证换成内部主键链路 | ❌ 删除/大幅简化。OSI 直接用 `idCard`+`checkId`,`phrId` 由创建接口回传 |
| **Chrome 报文对齐**(`chrome_payload_compat_enable`、各 `*_payload_normalizer.go`、`health_check_enricher.go`) | 必须把字段补齐成 Chrome F12 抓到的同款形态,否则后端拒绝 | ⚠ 转化。不再"对齐网页",而改为"映射到文档契约"——见下 |
| **完整度服务端重算**(`health_record_complete_level.go` 37/33 项、`health_check_perfection.go` 70 项) | 逆向得知网页会算 completeLevel/perfection,客户端不可信任传入值,需本地重算 | ⚠ 待定。OSI 有显式 `isFillShhj`,完整度很可能由平台算;需联调确认后决定保留与否 |
| **网页拟态请求头**(`buildCHISCallHeaders`:`Origin`/`Referer`/`X-Requested-With`/`Host`) | 让请求看起来像浏览器发的 | ❌ 删除。OSI 是正式服务接口,无需伪装 |
| **apitrace + 多套快照**(`util/apitrace.go`、`phis_snapshot_*`,且注释标"兼容保留") | 逆向期排障留下的多套追踪 | 🔁 收敛为一套 report log |
> 量级判断:`chis/` 目录约 70%~80% 的代码是为「骗过网页 + 复刻网页隐藏算法」服务的。
> OSI 接口让这部分**整体失去存在意义**。
### 3.2 值得保留的工程经验 —— 新项目应继承
| 旧项目机制 | 价值 | 新项目去向 |
| --- | --- | --- |
| **分层清晰 + 面向初级维护者**(AGENTS.md:可读、不过度抽象、必要中文注释) | 团队约定,降低接手成本 | ✅ 继承这套协作规范 |
| **统一调用入口**(`chis/client.go` `Call(ctx, account, path, req, out)`:拼地址→注入头→传输→反序列化→记日志) | 单点收敛传输与可观测 | ✅ 演化为 OSI `client.Call(ctx, serviceId, body, out)` |
| **传输层与代理**(`transport.go`/`http_client.go`,SOCKS5 支持、超时控制、避免 typed-nil jar panic) | 内网穿透/超时是真实运维需求 | ✅ 几乎原样保留(去掉 cookiejar) |
| **投递流水线**(`worker/`:任务校验分流、**重试分类**、**幂等去重**、**熔断**、**批次报告**、跑完自动退出) | 这是搬运管道的可靠性核心,与接口形态无关 | ✅ 重点保留并升级 |
| **重试错误分类**(网络/5xx/429/网络型 401 可重试;参数/权限不可重试;退避 `2s*attempt`) | 来之不易的运维经验 | ✅ 保留,适配 OSI 返回码(`405` 超时可重试) |
| **report log + 降级**(Redis 优先、不可用降级写本地 JSONL;带 trace_id) | 可观测 + 不强依赖 Redis | ✅ 保留,Redis 改为完全可选 |
| **统一错误体**(`APIError{error_code,error_message,trace_id}`) | 上游稳定解析 | ✅ 保留 |
| **配置驱动 + Redis 启动失败不阻断** | 联调友好 | ✅ 保留 |
### 3.3 旧项目的待改进点(新项目顺手修正)
- **幂等键弱**:旧键 `account|idCard|dataType|sha1(saveBody)`,依赖报文哈希,报文微调即视为新任务。OSI 有天然的 `checkId`,应作为幂等主键。
- **任务源是 mock 文件**:`mock_task_worker` 读本地 JSON,真实 PHIS 拉取/状态回写一直 TODO(见其 `phase9_todo_open_items.md`)。新项目应直接把 PHIS 接入做实。
- **多套追踪并存**:apitrace / reportlog / snapshot 三套,注释里自承"兼容保留",应收敛为一套。
- **handler 里大量"从原始 JSON 多路径兜底提取字段"**(`fillEMPIAndPHRFromContext` 等),是历史请求体不规范的补丁。新项目以明确契约杜绝。
- **server 与 worker 杂糅在 `main.go`**(flag 极多)。新项目用子命令拆分。
---
## 4. 给新项目的迁移启示(一句话)
> 把 `chis/` 里"对付网页"的 70% 删掉,保留并强化 `worker/` 那套"可靠投递"的 30%,
> 再把删掉的部分替换成一个**薄 OSI 客户端**和一个**厚字段映射层**。
+261
View File
@@ -0,0 +1,261 @@
# 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. 目录结构
```
chis_osi/
├── cmd/
│ ├── server/ main:HTTP 服务入口
│ └── deliver/ main:投递 worker 入口
├── config/ viper 配置(osi/phis/redis/log/proxy)
├── internal/
│ ├── 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/ 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 沿用旧项目协作规范
```
> `internal/` 用于约束包边界,避免被外部误用;若团队更习惯扁平结构,可去掉 `internal/` 层级,保持包名不变。
---
## 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/phis` —— 任务源
- 拉取待上送明细(替代旧项目的本地 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/phis 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_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. **沙箱环境地址与一组真实可用账号**,用于回归样本采集。
> 这些问题不阻塞架构落地:薄客户端、契约、映射层、流水线均可先按现有文档搭骨架,再以联调样本校准。
+147
View File
@@ -0,0 +1,147 @@
# 04 · 字段与接口映射
本文是 `mapping/` 与 `contract/` 两层的落地指引:接口能力映射、PHIS→OSI 字段映射策略、码表清单、checkId 幂等键设计。
---
## 1. 接口能力 → 内部方法映射
| OSI serviceId | 路径 | 内部方法(`osi` 包) | dataType(pipeline 分流) |
| --- | --- | --- | --- |
| JKDA00001 | `/jkda/create` | `CreateHealthRecord` | `health_record` |
| JKDA00003 | `/jkda/update` | `UpdateHealthRecord` | `health_record` |
| JKDA00002 | `/auto/jkda/find` | `FindHealthRecord` | (查询) |
| JKDA00005 | `/jkda/findrqbj` | `FindPersonSign` | (查询) |
| JKTJ00001 | `/jktj/create` | `CreateHealthCheck` | `health_check` |
| JKTJ00003 | `/jktj/update` | `UpdateHealthCheck` | `health_check` |
| JKTJ00002 | `/auto/jktj/query` | `QueryHealthCheck` | (查询) |
| JKTJLIST00002 | `/auto/jktjlist/query` | `ListHealthCheckPeople` | (查询) |
| JKTJLSJL00002 | `/auto/jktjlscx/query` | `LastHealthCheck` | (查询) |
| LNRZLPG00001 | `/lnrzlpg/create` | `CreateElderlySelfCare` | `elderly_self_care` |
| LNRZLPG00003 | `/lnrzlpg/update` | `UpdateElderlySelfCare` | `elderly_self_care` |
| LNRZYTZ00001 | `/lnrzyygl/create` | `CreateTCMConstitution` | `tcm` |
| LNRZYTZ00003 | `/lnrzyygl/update` | `UpdateTCMConstitution` | `tcm` |
| (待确认) | `/auto/zyjkzd/create` | `SaveTCMGuidance` | `tcm_guidance` |
| (待确认) | `/auto/zyjkzd/update` | `UpdateTCMGuidance` | `tcm_guidance` |
| WGDZ00001 | `/auto/wgdzcx/query` | `QueryGridAddress` | (字典) |
| ZRYS00001 | `/auto/zryscx/query` | `QueryDoctors` | (字典) |
| YPML00001 | `/auto/ypmlcx/query` | `QueryDrugs` | (字典) |
| CXJG00002 | `/auto/cxjg/query` | `QueryOrgs` | (字典) |
> ⚠ "(待确认)"项以 01 文档第 7 节为准,联调回填后更新 `osi/codes.go` 的 serviceId 常量与 `pathOf` 路由表。
---
## 2. 字段映射策略
### 2.1 三类字段,三种处理
1. **直传字段**:PHIS 与 OSI 同义且同形(如姓名、出生日期 `yyyy-MM-dd`)。直接拷贝,做必填/长度校验。
2. **码表字段**:枚举值需经字典转换(性别、民族、血型、职业…)。统一走 `dict.go` 双向查表,未命中报 `ValidationError`,**绝不静默置空**。
3. **结构/多选字段**:嵌套节点(既往史/家族史/手术/外伤/输血/生活环境)、逗号拼接多选。集中在各业务域映射函数里组装。
### 2.2 映射函数约定
```go
// 返回结构化校验错误,pipeline 在投递前据此拦截,避免把脏数据打到平台。
func MapHealthRecord(src phis.HealthRecord, ctx MapContext) (contract.HealthRecordCreate, []ValidationError)
```
- `MapContext` 携带机构级常量:`orgCode/userName/operateUser/regionCode` 默认值与字典快照。
- 校验项:必填(idCard/personName/sexCode/birthday/mobileNumber/regionCode/manaDoctorId/manaUnitId…)、长度上限、码表合法性、日期格式。
- 映射是**纯函数**,便于用 01 文档样例 + 联调真实样本做单测(对齐旧项目"可测试改动补最小必要测试")。
---
## 3. 码表清单(`dict.go` 初始内容)
> 取值来自 docx 字段说明,建模时全部落为常量表,并加注释标"来源:OSI 文档 §字段名"。
| 维度 | 取值(节选) |
| --- | --- |
| 性别 sexCode | 0 未知 / 1 男 / 2 女 / 9 未说明 |
| 户籍 registeredPermanent | 1 户籍 / 2 非户籍 |
| 血型 bloodTypeCode | 1 A / 2 B / 3 O / 4 AB / 5 不详 |
| RH rhBloodCode | 1 阳性 / 2 阴性 / 3 不详 |
| 文化程度 educationCode | 10 研究生 / 20 本科 / 31 大专 / 41 中专 / 47 技校 / 60 高中 / 70 初中 / 80 小学 / 90 文盲 / 91 不详 |
| 职业 workCode | 0 负责人 / 1·2 专技 / 3 办事 / 4 商业服务 / 5 农林牧渔 / 9-9 生产运输 / X 军人 / Y 其他 / 8 无职业 |
| 婚姻 maritalStatusCode | 10 未婚 / 20 已婚 / 30 丧偶 / 40 离婚 / 90 未说明 |
| 医保 insuranceCode(多选) | 01 城镇职工 / 02 城乡居民 / 04 贫困救助 / 05 商业 / 06 全公费 / 07 全自费 / 99 其他 |
| 民族 nationCode | 01 汉 … 56 基诺 / 99 其他(56 项全表) |
| 人群标记 personSign | PU 普通 / GRQY 已签约 / LAO 老年 / GAO 高血压 / TANG 糖尿病 / FU 孕产妇 / ER 儿童 / FEI 肺结核 / JING 精神 / CAN 残疾 |
| 药物过敏 ywgms(多选) | 0101 无 / 0102 青霉素 / 0103 磺胺 / 0104 链霉素 / 0109 其他 |
| 既往疾病 jwsjbcode | 0201 无 / 0202 高血压 / 0203 糖尿病 / … / 0299 其他 |
| 残疾 cjqk(多选) | 1101 无 / 1102 视力 / 1103 听力 / 1104 言语 / 1105 肢体 / 1106 智力 / 1107 精神 / 1108 孤独症 / 1109 脑瘫 / 1199 其他 |
| 厨房排风 cookAirTool | 1 无 / 2 油烟机 / 3 换气扇 / 4 烟囱 / 9 其他 |
| 燃料 fuelType | 1 液化气 / 2 煤 / 3 天然气 / 4 沼气 / 5 柴火 / 9 其他 |
| 饮水 waterSourceCode | 1 自来水 / 2 净化水 / 3 井水 / 4 河湖水 / 5 塘水 / 9 其他 |
| 厕所 washroom | 1 卫生厕所 / 2 粪池式 / 3 马桶 / 4 露天粪坑 / 5 简易棚厕 / 6 其他 |
| 机构类型 organizType | A 医院 / B 社区中心(站) / C 卫生院 / D 门诊诊所村室 / D6 村卫生室 / R 市卫生局 |
> 多选字段统一用英文逗号拼接(文档示例如 `"0102,0103"`)。注意 docx 个别返回样例用了中文逗号,解析时两者都要兼容。
---
## 4. 主数据依赖:字典服务先行
创建类接口的若干必填字段依赖公共服务查询结果,建议在投递前用字典缓存解析:
```
regionCode ← QueryGridAddress(parentCode) // 网格地址
manaDoctorId/operateUser ← QueryDoctors(manaUnitId) // 责任医生
manaUnitId/organizCode ← QueryOrgs(...) // 机构
```
策略:启动或定时拉取这些字典,缓存到本地(redis 可选 + 内存),映射时按 PHIS 的地址/医生/机构名称反查 OSI 码。命中失败计入校验错误,不投递。
---
## 5. checkId 与幂等键设计
`checkId`(第三方业务唯一识别码,长度 20)是新项目幂等的核心。
### 5.1 生成规则(`mapping/checkid.go`)
要求**确定性**:同一源记录无论重试多少次,都得到同一 checkId。
```
checkId = 截断20位( 编码( 源系统标识 | dataType | 源记录主键 [ | 版本/更新时间] ) )
```
- 同一条源记录的"创建"应使用稳定 checkId → 平台据此识别为同一档案,避免重复建档。
- "更新"沿用原 checkId(走 update 接口)。
- 若源记录内容变更需视为新版本上送,可把"更新时间/版本号"纳入 checkId 计算——**取决于平台是否以 checkId 去重**(见 03 文档开放问题 5)。
### 5.2 幂等存储
- 键:`checkId`(替代旧项目 `account|idCard|dataType|sha1(saveBody)`)。
- 值:`{status: success|failed, phrId, lastTs, attempts}`。
- 命中 `success`:跳过,不重复投递。
- `failed`:允许后续重试。
- 存储:redis 优先、本地 JSON 降级(沿用旧项目 `idempotency_store` 思路)。
> 用 checkId 做幂等键的好处:与平台对账口径一致(平台也认 checkId),且不受报文字段微调影响。
---
## 6. 校验与错误分类(与 pipeline 配合)
| 错误来源 | 分类 | 处理 |
| --- | --- | --- |
| 映射缺必填/码表未命中/格式非法 | 不可重试(数据错) | 直接 failed,回写 PHIS,附 ValidationError 明细 |
| 网络错误 / SOCKS / EOF / 超时 | 可重试 | 退避重试,计熔断 |
| OSI `code == "405"`(服务超时) | 可重试 | 退避重试 |
| OSI `code` 非 1 非 405(业务/权限拒绝) | 不可重试 | failed,记 message 供排查 |
| HTTP 5xx / 429 | 可重试 | 退避重试 |
---
## 7. 待补字段表
以下接口的完整字段表 docx 未充分给出或存在坑点,建模时以联调样本为准并在此登记:
- [ ] 体检 `jktj/create` 全量字段(hcData/lsData/exaData/aeData,体量最大)
- [ ] 老年人自理评估 `lnrzlpg` 字段
- [ ] 中医体质辨识 `lnrzyygl` 字段
- [ ] 中医健康指导 `zyjkzd` 字段 + serviceId
- [ ] 各列表/查询接口的分页与返回数组结构
+89
View File
@@ -0,0 +1,89 @@
# 05 · 实施路线图
分阶段落地 `chis_osi`,每阶段都「可运行、可回归、可联调」,沿用旧项目"小步、可测、可交接"的节奏。
---
## 阶段 0 · 脚手架与契约骨架
- [ ] 初始化 Go module、`cmd/server`、`cmd/deliver` 双入口、`config/`(viper)。
- [ ] 落地 `osi/sign.go`(MD5 签名)+ 单测:用文档约定的 `ts/ask` 校验 `password` 形态(32 位小写)。
- [ ] 移植旧项目 `transport.go`/`http_client.go` → `osi/transport.go`(保留 SOCKS5/超时,去 cookiejar 与拟态头)。
- [ ] `osi/client.go` 的 `Call(serviceId, body, out)`:注入头+信封+发送+判码(`code=="1"`/`405`)。
- [ ] `contract/envelope.go` + `osi/codes.go`(serviceId 常量 + `pathOf` 路由)。
- **验收**:对任一最简查询接口(如机构查询 CXJG00002)发真实请求,拿到 `code/message`。
## 阶段 1 · 字典服务打通
- [ ] 实现 `public.go` 四个查询:网格/责任医生/药品/机构。
- [ ] `mapping/dict.go` 落地全部码表(含 56 项民族)。
- [ ] 字典缓存(内存 + redis 可选),供映射层反查 `regionCode/manaDoctorId/manaUnitId`。
- **验收**:能用真实机构码查到下级网格、责任医生、机构树。
## 阶段 2 · 健康档案闭环(第一条业务线)
- [ ] `contract/jkda.go` + `mapping/health_record.go` + `mapping/checkid.go`。
- [ ] `osi/jkda.go`:Create/Update/Find/FindRqbj。
- [ ] `handler` + `router`:暴露 `/api/health-record/save`(server 模式联调用)。
- [ ] 用 01 文档样例 + 联调样本写映射单测。
- **验收**:一条 PHIS 档案 → 映射 → create → 平台返回 `code:"1"` 与 `phrId`;重复投递被幂等跳过。
## 阶段 3 · 投递流水线
- [ ] `pipeline`:`deliver.go`/`retry.go`/`idempotency.go`(checkId)/`circuit.go`/`report.go`。
- [ ] `observ`:一套 report log(redis 优先、文件降级)+ 快照 + trace。
- [ ] `cmd/deliver`:定时驱动(先用本地任务文件 mock,对齐旧项目可跑形态)。
- **验收**:批量任务跑完出批次报告(total/success/failed/skipped/retry + 失败 Top);网络不可达触发熔断且行为符合预期。
## 阶段 4 · 其余业务线
- [ ] 体检(JKTJ,字段最多,重点)、老年人自理评估、中医体质辨识、中医健康指导。
- [ ] 各自的 contract/mapping/osi 方法 + 单测。
- **验收**:四类 dataType 均能走通 create/update/query。
## 阶段 5 · PHIS 真实接入与状态回写
- [ ] `source/phis`:真实拉取接口替换 mock;任务模型对齐。
- [ ] 投递结果回写 PHIS(done/retry/failed),trace_id 用 PHIS 任务号贯穿。
- **验收**:PHIS→chis_osi→CHIS 全链路自动跑通,状态可回查。
## 阶段 6 · 加固与交接
- [ ] 完整度问题定论(见开放问题 1):若平台要求接入方算,移植旧项目 complete_level/perfection 到 `mapping/`。
- [ ] 配置/密钥走环境变量,`osi_ask` 不入库。
- [ ] `docs/` 补 `运维与排障.md`、`联调清单.md`。
- [ ] `go test ./...` 全绿。
---
## 联调前置清单(向厂家索要)
1. 沙箱环境 `hostname` 与一组可用 `orgCode/userName/ask/deviceSN/operateUser`。
2. docx 缺漏的列表/查询接口路径与 serviceId(档案列表、自理/体质/中医指导列表与查询)。
3. 中医健康指导 zyjkzd 的字段表与 serviceId。
4. checkId 唯一性与去重规则;create/update 的幂等语义。
5. 完整度是否由平台计算。
6. 错误码字典(除 `405` 外)。
7. 每个创建接口的一组真实成功请求/响应样本(做映射回归基线)。
---
## 风险与对策
| 风险 | 对策 |
| --- | --- |
| docx 字段表有错(serviceId 串台、字段名/类型不一致) | 以 `contract/` 为唯一修正点 + 联调样本回归(01 文档第 7 节) |
| 列表/查询接口文档缺漏 | 阶段性向厂家索要,不阻塞创建类主线 |
| 码表庞大易错(民族 56 项等) | 集中 `dict.go` + 单测覆盖 + 未命中显式报错 |
| 完整度算法不明 | 默认不算、依赖平台;联调定论后再决定是否移植旧逻辑 |
| 平台限流/超时 | 复用熔断 + `405` 重试;投递并发可配 |
| 密钥泄露 | `ask` 仅参与签名、走环境变量、不入库不入日志 |
---
## 工作量直觉(相对旧项目)
- 鉴权/会话/传输:**大幅减少**(无 SM2/登录/Cookie/Redis 会话/身份链)。
- 映射层:**与旧项目相当或略增**(但从"逆向猜"变为"照文档写",更确定、更可测)。
- 投递流水线:**基本复用**旧项目经验,少量适配。
- 净效果:总复杂度显著下降,且代码意图清晰可交接。
+30
View File
@@ -0,0 +1,30 @@
# chis_osi 对接设计文档
本目录是「广东省基层医疗机构管理系统(CHIS)统一对外服务接口(OSI)」对接项目 `chis_osi` 的设计文档集。
## 背景
省基卫厂家(和宇健康科技)已提供**官方服务端对接接口**《统一对外服务接口 API 规范文档 V1.5.7》。
本项目基于该官方接口重新对接,并参考既有项目 `chis_upload`(通过逆向 Chrome F12 网页接口实现)的工程经验,
设计一套**更简洁、更稳定、更易维护**的整体架构。
> 关键判断:官方 OSI 接口是**无状态的服务端到服务端 JSON 接口**,鉴权方式为请求头 MD5 签名。
> 这意味着 `chis_upload` 中为对付网页逆向而引入的大量复杂度(SM2 登录加密、Cookie/Redis 会话、
> 身份链路反查、Chrome 报文对齐补齐)在新项目中**可以整体删除**。新项目的核心复杂度从「如何骗过网页」
> 转移到「如何把上游 PHIS 数据正确映射成 OSI 文档约定的字段」。
## 文档索引
| 文档 | 内容 |
| --- | --- |
| [01-OSI接口规范分析.md](01-OSI接口规范分析.md) | 官方 OSI 接口的鉴权、报文约定、全量接口清单与 serviceId 映射、文档坑点 |
| [02-旧项目架构评估.md](02-旧项目架构评估.md) | `chis_upload` 的分层与组件、哪些复杂度由逆向驱动、哪些经验值得保留 |
| [03-目标架构设计.md](03-目标架构设计.md) | `chis_osi` 的整体架构、分层、目录结构、数据流与时序、关键设计决策 |
| [04-字段与接口映射.md](04-字段与接口映射.md) | OSI 接口↔内部能力映射、PHIS→OSI 字段/字典映射策略、checkId 幂等键 |
| [05-实施路线图.md](05-实施路线图.md) | 分阶段落地计划、配置项、可观测性、测试与联调清单 |
## 一句话结论
> 用一个**无状态 OSI 客户端(MD5 签名 + 统一信封)** + 一个**表驱动的 PHIS→OSI 映射层** +
> 一个**复用旧项目经验的投递流水线(校验/映射/签名/调用/分类重试/幂等/批次报告)**,
> 替代旧项目里因网页逆向而堆积的会话与补齐逻辑。
Binary file not shown.