diff --git a/.gitignore b/.gitignore index 5b90e79..0126edd 100644 --- a/.gitignore +++ b/.gitignore @@ -1,27 +1,41 @@ -# ---> Go -# 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 -# -# Binaries for programs and plugins -*.exe -*.exe~ -*.dll -*.so -*.dylib - -# Test binary, built with `go test -c` -*.test - -# Output of the go coverage tool, specifically when used with LiteIDE -*.out - -# Dependency directories (remove the comment below to include it) -# vendor/ - -# Go workspace file -go.work -go.work.sum - -# env file -.env - +# ---> Go +# 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 +# +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, built with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# Dependency directories (remove the comment below to include it) +# vendor/ + +# Go workspace file +go.work +go.work.sum + +# env file +.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 + diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..d52acd7 --- /dev/null +++ b/CLAUDE.md @@ -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=&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 | 对应 | +|------|------|---|------|------| +| `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 ./...` 通过(或说明未验证的原因与风险) diff --git a/README.md b/README.md index e366fd6..1a1b5a7 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,3 @@ # chis_osi -chis官方接口对接. \ No newline at end of file +chis官方接口对接. diff --git a/docs/01-OSI接口规范分析.md b/docs/01-OSI接口规范分析.md new file mode 100644 index 0000000..f8bd20b --- /dev/null +++ b/docs/01-OSI接口规范分析.md @@ -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=&ask=`,其中 `` 必须与请求头里发送的 `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/` 包), +> 并以厂家沙箱环境的真实请求/响应样本做回归校准。 diff --git a/docs/02-旧项目架构评估.md b/docs/02-旧项目架构评估.md new file mode 100644 index 0000000..013d5ae --- /dev/null +++ b/docs/02-旧项目架构评估.md @@ -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 客户端**和一个**厚字段映射层**。 diff --git a/docs/03-目标架构设计.md b/docs/03-目标架构设计.md new file mode 100644 index 0000000..3730e8d --- /dev/null +++ b/docs/03-目标架构设计.md @@ -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://" # 不含 /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. **沙箱环境地址与一组真实可用账号**,用于回归样本采集。 + +> 这些问题不阻塞架构落地:薄客户端、契约、映射层、流水线均可先按现有文档搭骨架,再以联调样本校准。 diff --git a/docs/04-字段与接口映射.md b/docs/04-字段与接口映射.md new file mode 100644 index 0000000..681c286 --- /dev/null +++ b/docs/04-字段与接口映射.md @@ -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 +- [ ] 各列表/查询接口的分页与返回数组结构 diff --git a/docs/05-实施路线图.md b/docs/05-实施路线图.md new file mode 100644 index 0000000..27db42b --- /dev/null +++ b/docs/05-实施路线图.md @@ -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 会话/身份链)。 +- 映射层:**与旧项目相当或略增**(但从"逆向猜"变为"照文档写",更确定、更可测)。 +- 投递流水线:**基本复用**旧项目经验,少量适配。 +- 净效果:总复杂度显著下降,且代码意图清晰可交接。 diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..a6d9818 --- /dev/null +++ b/docs/README.md @@ -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 映射层** + +> 一个**复用旧项目经验的投递流水线(校验/映射/签名/调用/分类重试/幂等/批次报告)**, +> 替代旧项目里因网页逆向而堆积的会话与补齐逻辑。 diff --git a/docs/广东省基层医疗机构管理系统服务接口对接回复后需要的接口.xlsx b/docs/广东省基层医疗机构管理系统服务接口对接回复后需要的接口.xlsx new file mode 100644 index 0000000..bb00acb Binary files /dev/null and b/docs/广东省基层医疗机构管理系统服务接口对接回复后需要的接口.xlsx differ diff --git a/docs/统一对外服务接口文档-.docx b/docs/统一对外服务接口文档-.docx new file mode 100644 index 0000000..bf53f54 Binary files /dev/null and b/docs/统一对外服务接口文档-.docx differ