diff --git a/docs/07-本项目HTTP接口.md b/docs/07-本项目HTTP接口.md index 37694df..cc06c6b 100644 --- a/docs/07-本项目HTTP接口.md +++ b/docs/07-本项目HTTP接口.md @@ -50,6 +50,19 @@ GET /api/health-record/find - 上游:`JKDA00002` `/auto/jkda/find` - 返回:`data` 为**数组**(档案聚合,含 healthRecord/pastHistory/既往史等,见 docs/04 §8) +### 2. 查询人群分类/子档案标记 + +``` +GET /api/health-record/crowd +``` + +| 参数 | 必填 | 说明 | +| --- | --- | --- | +| `idCard` / `phrid` | **二选一** | 查询标识符,只能给一个 | + +- 上游:`JKDA00005` `/auto/jkda/findrqbj` +- 返回:`data` 为对象,含 `personSign`、`idCard`、`phrId`(见 docs/04 §12) +- `personSign` 是人群分类码,可能为逗号分隔多值;码表见 docs/04 §3 ### 2. 最近一次体检 ``` @@ -63,7 +76,7 @@ GET /api/health-check/last - 上游:`JKTJLSJL00002` `/auto/jktjlscx/query` - 返回:`data` 为**单个对象**(最近一次完整体检,约 260 字段,见 docs/04 §11.3) -### 3. 某人全部体检 +### 4. 某人全部体检 ``` GET /api/health-check/all @@ -77,7 +90,7 @@ GET /api/health-check/all - 返回:`data` 为**数组**(该人全部体检历史,每条完整) - 注意:历史记录 `checkId` 可能为 `null`(仅近年有),历史体检按 `checkDate` 区分(docs/04 §11.1) -### 4. 年度已检/未检人员名单 +### 5. 年度已检/未检人员名单 ``` GET /api/health-check/list @@ -99,6 +112,7 @@ GET /api/health-check/list ```bash curl "http://127.0.0.1:8080/api/health-record/find?idCard=<身份证>" +curl "http://127.0.0.1:8080/api/health-record/crowd?idCard=<身份证>" curl "http://127.0.0.1:8080/api/health-check/last?idCard=<身份证>" curl "http://127.0.0.1:8080/api/health-check/all?idCard=<身份证>" curl "http://127.0.0.1:8080/api/health-check/list?checkYear=2025&idCard=<身份证>" diff --git a/docs/current-state.md b/docs/current-state.md index f949d98..1cf3065 100644 --- a/docs/current-state.md +++ b/docs/current-state.md @@ -5,12 +5,12 @@ ## 当前快照 -- 日期:2026-07-07 +- 日期:2026-07-09 - 阶段:**Phase D 创建/更新代码基线完成,真实写入验收受阻**;T-001~T-006、T-201、T-203、T-102、T-202、T-205、T-101、T-103 已验收;T-206 已完成本地契约/客户端/映射组装代码,但未执行真实 `JKDA00001` create -- 技术栈:Go 1.24 单二进制;`main.go -mode server|deliver`;配置读取使用 viper,支持环境变量覆盖;OSI 客户端已具备签名、信封、传输、基础判码、JKDA00002 Find、JKDA00005 FindRqbj、JKDA00001 Create、JKDA00003 Update,以及 WGDZ/ZRYS/YPML/CXJG 四个公开查询薄封装 -- 生产代码:已有 `main.go`、`config/`、`contract/envelope.go`、`contract/jkda.go`、`osi/` 薄客户端、`cache/dictionary.go`、`verify_jkda.go`、`go.mod`/`go.sum`;`mapping/dict.go`、`mapping/health_record.go`、`mapping/checkid.go` 已建立映射纯函数、码表基线、主数据反查接入点和创建请求组装;`handler/health_record.go`+`server.go` 提供 server 模式查询端点;`pipeline/` 等业务模块仍待后续任务建立 +- 技术栈:Go 1.24 单二进制;`main.go -mode server|deliver`;配置读取使用 viper,支持环境变量覆盖;OSI 客户端已具备签名、信封、传输、基础判码、JKDA00002 Find、JKDA00005 FindRqbj(人群分类,已按实测 auto 路径校准)、JKDA00001 Create、JKDA00003 Update,以及 WGDZ/ZRYS/YPML/CXJG 四个公开查询薄封装 +- 生产代码:已有 `main.go`、`config/`、`contract/envelope.go`、`contract/jkda.go`、`osi/` 薄客户端、`cache/dictionary.go`、`verify_jkda.go`、`go.mod`/`go.sum`;`mapping/dict.go`、`mapping/health_record.go`、`mapping/checkid.go` 已建立映射纯函数、码表基线、主数据反查接入点和创建请求组装;`handler/health_record.go`+`server.go` 提供 server 模式档案查询与人群分类查询端点;`pipeline/` 等业务模块仍待后续任务建立 - 联调现实:**JKDA00002 个人档案查询已用 Go 侧真实请求打通**,返回 `code="01" message="操作成功" data_count=1`;**公开查询 WGDZ00001/ZRYS00001/CXJG00002 已用真实档案主数据验证通过**,均返回 `code="01"` 且数组非空;药品目录 YPML00001 已完成客户端封装和单测,尚未做真实药品关键字样本验证;**JKDA00001 create 未跑真实请求**,避免在没有安全测试居民/写入授权时污染平台档案 -- 测试:`go test ./...` 通过;当前测试覆盖 mode 解析、配置加载与环境变量覆盖、MD5 签名、请求头组装、JSON POST 传输、头名大小写保留、identity 响应编码声明、超时配置、SOCKS5 代理地址校验、信封结构、serviceId 路由、成功/重试判码、Client.Call 请求与响应解析、JKDA00002 查询响应契约、JKDA00001/00003 创建更新请求契约与客户端方法、JKDA00002 Find、JKDA00005 FindRqbj、公开查询四接口、字典缓存快照与持久化失败不阻断、映射码表双向查找、民族 01~56 完整性、健康档案映射必填/码表校验、主数据名称反查、创建请求组装、checkId 确定性、docx/联调风格映射样本基线、JKDA00002 验证入口 +- 测试:`go test ./...` 通过;当前测试覆盖 mode 解析、配置加载与环境变量覆盖、MD5 签名、请求头组装、JSON POST 传输、头名大小写保留、identity 响应编码声明、超时配置、SOCKS5 代理地址校验、信封结构、serviceId 路由、成功/重试判码、Client.Call 请求与响应解析、JKDA00002 查询响应契约、JKDA00001/00003 创建更新请求契约与客户端方法、JKDA00002 Find、JKDA00005 FindRqbj(personSign/idCard/phrId)、公开查询四接口、字典缓存快照与持久化失败不阻断、映射码表双向查找、民族 01~56 完整性、健康档案映射必填/码表校验、主数据名称反查、创建请求组装、checkId 确定性、docx/联调风格映射样本基线、JKDA00002 验证入口 - 标准启动路径:`./init.sh` 已配置三步:依赖下载、`go test ./...`、`go run . -mode server -config config.yaml.example` - 标准验证路径:`go test ./...`、`go build ./...` - 当前 blocker:T-206 真实 create 验收缺少可写入沙箱的安全测试居民资料或明确写入授权。软限制:当前机器从 Git Bash 启动 Go 会出现标准库路径/构建缓存权限异常,`init.sh` 无法完整跑完;PowerShell 下等价 Go 命令和真实查询通过。厂家侧 B1/B2/B4/B5 契约缺口只影响阶段 4 @@ -27,7 +27,7 @@ | `osi/transport.go` | 已有 | OSI JSON POST 传输层;原始 HTTP/1.1 写请求以保留头名大小写;支持超时与可选 SOCKS5 | | `osi/codes.go` | 已有 | serviceId 路由与成功/可重试判码;已包含 JKDA 查询/创建/更新与四个公开查询 serviceId | | `osi/client.go` | 已有 | 通用 `Call`:签名头 + 查询信封 + 传输 + 响应解析;内部 `callUploadInfo` 支持创建/更新完整 uploadinfo | -| `osi/jkda.go` | 已有 | JKDA00001 Create、JKDA00003 Update、JKDA00002 Find、JKDA00005 FindRqbj | +| `osi/jkda.go` | 已有 | JKDA00001 Create、JKDA00003 Update、JKDA00002 Find、JKDA00005 FindRqbj(`/auto/jkda/findrqbj`) | | `osi/public.go` | 已有 | WGDZ00001 网格、ZRYS00001 责任医生、YPML00001 药品、CXJG00002 机构查询 | | `cache/dictionary.go` | 已有 | 公开字典内存快照;按名称反查 `regionCode`/`manaDoctorId`/`manaUnitId`;可选持久化失败不阻断 | | `verify_jkda.go` | 已有 | JKDA00002 真实请求验证辅助入口 | @@ -40,7 +40,7 @@ | `mapping/dict.go` | 已有 | 性别/民族/血型/RH/文化程度/职业/婚姻/医保/personSign 双向码表;未命中返回 `ValidationError` | | `mapping/health_record.go` `mapping/checkid.go` | 已有 | 健康档案映射草稿、字典快照注入、主数据反查、创建请求组装(含 createUser/createUnit 派生)、必填/码表校验、确定性 checkId | | `mapping/health_record_baseline_test.go` | 已有 | docx 风格、联调风格、脏数据映射回归样本 | -| `handler/health_record.go` | 已有 | `GET /api/health-record/find` 查询端点,回写平台完整响应 | +| `handler/health_record.go` | 已有 | `GET /api/health-record/find` 档案查询、`GET /api/health-record/crowd` 人群分类查询,均回写平台完整响应 | | `handler/health_check.go` | 已有 | 体检端点 `GET /api/health-check/last`(最近一次)+ `/all`(全部体检)+ `/list`(年度名单)| | `server.go` `osi_client.go` | 已有 | server 模式起 HTTP 服务、注册路由;`buildOSIClient` 共用构造(verify 与 server 复用) | | `pipeline/` 等 | 待建 | 后续任务 | @@ -80,6 +80,7 @@ OSI_VERIFY_ID_CARD= go run . -verify-jkda -config config.yaml # server 模式 HTTP 查询端点(用真实 config.yaml,默认仅绑 127.0.0.1:8080) go run . -mode server # 起服务(阻塞) curl "http://127.0.0.1:8080/api/health-record/find?idCard=" # 档案(完整 JSON) +curl "http://127.0.0.1:8080/api/health-record/crowd?idCard=" # 人群分类/子档案标记 curl "http://127.0.0.1:8080/api/health-check/last?idCard=" # 最近一次体检 curl "http://127.0.0.1:8080/api/health-check/all?idCard=" # 某人全部体检(数组) curl "http://127.0.0.1:8080/api/health-check/list?checkYear=2025&idCard=" # 年度已检/未检名单 diff --git a/handler/health_record.go b/handler/health_record.go index ec11cf4..93e161c 100644 --- a/handler/health_record.go +++ b/handler/health_record.go @@ -51,6 +51,37 @@ func (h *HealthRecordHandler) Find(w http.ResponseWriter, r *http.Request) { writeJSONError(w, http.StatusBadGateway, "empty response from OSI") } +// Crowd 处理 GET /api/health-record/crowd?idCard=..|phrid=.. +// 原样回写 JKDA00005 人群分类与子档案标记响应。 +func (h *HealthRecordHandler) Crowd(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodGet { + writeJSONError(w, http.StatusMethodNotAllowed, "only GET is supported") + return + } + + q := r.URL.Query() + query := osi.FindRqbjQuery{ + IDCard: q.Get("idCard"), + PHRID: q.Get("phrid"), + } + if query.IDCard == "" && query.PHRID == "" { + writeJSONError(w, http.StatusBadRequest, "provide one of: idCard, phrid") + return + } + + _, result, err := h.client.FindRqbj(r.Context(), query) + if len(result.Raw) > 0 { + w.Header().Set("Content-Type", "application/json; charset=utf-8") + _, _ = w.Write(result.Raw) + return + } + if err != nil { + writeJSONError(w, http.StatusBadGateway, err.Error()) + return + } + writeJSONError(w, http.StatusBadGateway, "empty response from OSI") +} + func writeJSONError(w http.ResponseWriter, status int, msg string) { w.Header().Set("Content-Type", "application/json; charset=utf-8") w.WriteHeader(status) diff --git a/handler/health_record_test.go b/handler/health_record_test.go index 8b289ef..034643d 100644 --- a/handler/health_record_test.go +++ b/handler/health_record_test.go @@ -39,6 +39,38 @@ func TestHealthRecordFindReturnsRawPlatformResponse(t *testing.T) { } } +func TestHealthRecordCrowdReturnsRawPlatformResponse(t *testing.T) { + var seenPath string + osiServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + seenPath = r.URL.Path + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write([]byte(`{"code":"01","message":"操作成功","data":{"personSign":"LAO","idCard":"440100199001011234","phrId":"PHR001"}}`)) + })) + defer osiServer.Close() + + transport, err := osi.NewTransport(osi.TransportConfig{Timeout: time.Second}) + if err != nil { + t.Fatalf("NewTransport: %v", err) + } + client := osi.NewClient(osi.ClientConfig{BaseURL: osiServer.URL, UserName: "dyytgw", Ask: "secret", Transport: transport}) + h := NewHealthRecordHandler(client) + + req := httptest.NewRequest(http.MethodGet, "/api/health-record/crowd?idCard=440100199001011234", nil) + rec := httptest.NewRecorder() + h.Crowd(rec, req) + + if rec.Code != http.StatusOK { + t.Fatalf("status = %d", rec.Code) + } + if seenPath != "/osi/api/auto/jkda/findrqbj" { + t.Fatalf("upstream path = %q", seenPath) + } + body, _ := io.ReadAll(rec.Body) + if !strings.Contains(string(body), `"personSign":"LAO"`) || !strings.Contains(string(body), `"phrId":"PHR001"`) { + t.Fatalf("body missing crowd fields: %s", body) + } +} + func TestHealthRecordFindRequiresIdentifier(t *testing.T) { h := NewHealthRecordHandler(nil) // 无标识符时在触达 client 前就返回 400 req := httptest.NewRequest(http.MethodGet, "/api/health-record/find", nil) diff --git a/osi/codes.go b/osi/codes.go index aa934a8..fb06643 100644 --- a/osi/codes.go +++ b/osi/codes.go @@ -26,7 +26,7 @@ var servicePaths = map[string]string{ ServiceIDJKDACreate: "/jkda/create", ServiceIDJKDAFind: "/auto/jkda/find", ServiceIDJKDAUpdate: "/jkda/update", - ServiceIDJKDAFindRqbj: "/jkda/findrqbj", + ServiceIDJKDAFindRqbj: "/auto/jkda/findrqbj", ServiceIDGridAddress: "/auto/wgdzcx/query", ServiceIDDoctors: "/auto/zryscx/query", ServiceIDDrugs: "/auto/ypmlcx/query", diff --git a/osi/jkda.go b/osi/jkda.go index 59c0a31..0aa66e9 100644 --- a/osi/jkda.go +++ b/osi/jkda.go @@ -21,6 +21,8 @@ type FindRqbjQuery struct { type PersonSignResult struct { PersonSign string `json:"personSign"` + IDCard string `json:"idCard"` + PHRID string `json:"phrId"` } func (c *Client) CreateHealthRecord(ctx context.Context, req contract.HealthRecordCreate) (contract.HealthRecordSaveResult, Result, error) { diff --git a/osi/jkda_test.go b/osi/jkda_test.go index b0e4073..11c57c7 100644 --- a/osi/jkda_test.go +++ b/osi/jkda_test.go @@ -172,29 +172,29 @@ func TestFindRqbjCallsJKDA00005AndDecodesPersonSign(t *testing.T) { if err := json.NewDecoder(r.Body).Decode(&seenPayload); err != nil { t.Fatalf("decode request: %v", err) } - _, _ = w.Write([]byte(`{"code":"01","message":"操作成功","data":{"personSign":"PU,LAO"}}`)) + _, _ = w.Write([]byte(`{"code":"01","message":"操作成功","data":{"personSign":"LAO","idCard":"440000********1234","phrId":"PHR001"}}`)) })) defer server.Close() client := newTestJKDAClient(t, server.URL) - personSign, result, err := client.FindRqbj(context.Background(), FindRqbjQuery{PHRID: "phr-001"}) + personSign, result, err := client.FindRqbj(context.Background(), FindRqbjQuery{IDCard: "440000********1234"}) if err != nil { t.Fatalf("FindRqbj: %v", err) } if !result.Success || result.Code != "01" { t.Fatalf("result = %#v", result) } - if seenPath != "/osi/api/jkda/findrqbj" { + if seenPath != "/osi/api/auto/jkda/findrqbj" { t.Fatalf("path = %q", seenPath) } if seenPayload["serviceId"] != ServiceIDJKDAFindRqbj { t.Fatalf("serviceId = %v", seenPayload["serviceId"]) } baseInfo := seenPayload["uploadinfo"].(map[string]any)["baseInfo"].(map[string]any) - if baseInfo["phrid"] != "phr-001" { + if baseInfo["idCard"] != "440000********1234" { t.Fatalf("baseInfo = %#v", baseInfo) } - if personSign.PersonSign != "PU,LAO" { + if personSign.PersonSign != "LAO" || personSign.IDCard != "440000********1234" || personSign.PHRID != "PHR001" { t.Fatalf("personSign = %#v", personSign) } } diff --git a/progress.md b/progress.md index 7a1b24e..50a72df 100644 --- a/progress.md +++ b/progress.md @@ -256,3 +256,13 @@ - 发现:JKDA00005 实测请求路径为 `/osi/api/auto/jkda/findrqbj`,响应 `data` 是对象,字段含 `personSign`、`idCard`、`phrId`;当前 Go 侧只建模 `personSign`,且路径仍是早期 `/jkda/findrqbj`。 - 变更:`tasks.md` 新增 T-209;`docs/01`、`docs/04 §12` 登记实测契约和待改点。 - 下一步:实现 T-209,修正 `osi` 路由/响应结构,并补 server 模式人群分类查询端点。 +## 2026-07-09 T-209 人群分类查询优化 + +- 状态:DONE +- 变更:`osi/codes.go` 修正 JKDA00005 路由为 `/auto/jkda/findrqbj`;`osi.PersonSignResult` 补 `idCard`、`phrId` 字段;`handler.HealthRecordHandler` 新增 `GET /api/health-record/crowd`,原样回写平台响应;`server.go` 注册新端点。 +- RED:`go test ./osi -run TestFindRqbjCallsJKDA00005AndDecodesPersonSign -count=1` 初次失败,缺少 `idCard/phrId` 字段;`go test ./handler -run TestHealthRecordCrowdReturnsRawPlatformResponse -count=1` 初次失败,缺少 `Crowd` handler。 +- GREEN:补实现后上述目标测试通过。 +- 验证:`go test ./...` 通过。 +- 验证:`go build ./...` 通过。 +- 验证:`git diff --check` 通过。 +- 决策:HTTP 端点继续采用查询类统一策略,直接回写 `Result.Raw`,避免裁掉平台后续新增字段;真实样本 JSON 仍只本地留存,不提交。 diff --git a/server.go b/server.go index 07a919d..703e7b7 100644 --- a/server.go +++ b/server.go @@ -8,7 +8,7 @@ import ( "chis_osi/handler" ) -// runServer 启动 server 模式 HTTP 服务,暴露健康档案查询端点。 +// runServer 启动 server 模式 HTTP 服务,暴露查询端点。 func runServer(cfg config.Config, addr string) error { client, err := buildOSIClient(cfg) if err != nil { @@ -19,12 +19,14 @@ func runServer(cfg config.Config, addr string) error { hc := handler.NewHealthCheckHandler(client) mux := http.NewServeMux() mux.HandleFunc("/api/health-record/find", hr.Find) + mux.HandleFunc("/api/health-record/crowd", hr.Crowd) mux.HandleFunc("/api/health-check/last", hc.Last) mux.HandleFunc("/api/health-check/all", hc.All) mux.HandleFunc("/api/health-check/list", hc.List) fmt.Printf("chis_osi server listening on %s\n", addr) fmt.Printf(" GET http://%s/api/health-record/find?idCard=<身份证>\n", addr) + fmt.Printf(" GET http://%s/api/health-record/crowd?idCard=<身份证>\n", addr) fmt.Printf(" GET http://%s/api/health-check/last?idCard=<身份证>\n", addr) fmt.Printf(" GET http://%s/api/health-check/all?idCard=<身份证>\n", addr) fmt.Printf(" GET http://%s/api/health-check/list?checkYear=<年度>&idCard=<身份证>\n", addr) diff --git a/tasks.md b/tasks.md index d559c2d..6b62155 100644 --- a/tasks.md +++ b/tasks.md @@ -45,7 +45,7 @@ | T-201 | `contract/jkda.go`:以 docs/04 §8 实测契约建**查询响应**结构体 | T-004 | Find 响应能完整反序列化本地联调样本(`data` 数组、null 字段用指针/omitempty、`adressNumber` 坑点拼写);创建请求结构体延到 T-206 补 | DONE | | T-203 | `osi/jkda.go`:**Find + FindRqbj** | T-201 | 用 JKDA00002 真实请求打通,拿到 `code="01"` 与档案数据,与 Python 脚本结果一致 | DONE | | T-208 | `server` 模式健康档案查询端点 `GET /api/health-record/find` | T-203 | server 模式起 HTTP 服务,curl 按 idCard/phrid/personName/empiId 查询返回平台完整档案 JSON;默认仅绑本机 | DONE | -| T-209 | 优化人群分类查询 JKDA00005(路径/契约/HTTP 端点) | T-203, T-208 | 按实测 `auto/jkda/findrqbj` 查询,响应结构含 `personSign/idCard/phrId`;server 模式提供人群分类查询端点并原样回写平台 JSON | TODO | +| T-209 | 优化人群分类查询 JKDA00005(路径/契约/HTTP 端点) | T-203, T-208 | 按实测 `auto/jkda/findrqbj` 查询,响应结构含 `personSign/idCard/phrId`;server 模式提供人群分类查询端点并原样回写平台 JSON | DONE | ## Phase M · 映射层(码表 + 纯函数,注入假字典快照)