From 0e1915fc2bbefa8a835f9039c84050e797bf540f Mon Sep 17 00:00:00 2001 From: ila Date: Wed, 8 Jul 2026 00:45:47 +0800 Subject: [PATCH] =?UTF-8?q?feat(osi):=20=E4=BD=93=E6=A3=80=E6=9C=80?= =?UTF-8?q?=E8=BF=91=E4=B8=80=E6=AC=A1=E6=9F=A5=E8=AF=A2=E6=89=93=E9=80=9A?= =?UTF-8?q?=EF=BC=88T-301=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - osi.LastHealthCheck(JKTJLSJL00002 /auto/jktjlscx/query) - contract.HealthCheckSummary 只强类型化定位字段,完整内容走 Result.Raw - codes.go 登记 JKTJLSJL00002 + JKTJLIST00002 路由 - 单测:路径/解码/Raw 保留/idcard 小写映射/单键校验(脱敏假数据) - 实测:单查 JKTJ00002 平台未部署;data 为单对象;身份证字段小写 idcard Co-Authored-By: Claude Opus 4.8 --- contract/jktj.go | 19 ++++++++++++++++ osi/codes.go | 7 ++++++ osi/jktj.go | 34 +++++++++++++++++++++++++++++ osi/jktj_test.go | 57 ++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 117 insertions(+) create mode 100644 contract/jktj.go create mode 100644 osi/jktj.go create mode 100644 osi/jktj_test.go diff --git a/contract/jktj.go b/contract/jktj.go new file mode 100644 index 0000000..1e7ba10 --- /dev/null +++ b/contract/jktj.go @@ -0,0 +1,19 @@ +package contract + +// 健康体检查询契约(JKTJ)。 +// +// 体检记录字段极多(实测最近一次样本约 260 项,分 4 个 object 子节点 + +// 3 个 array 子节点,见 docs/04 §11)。查询场景只需定位/摘要字段即可, +// 完整内容由 osi.Result.Raw 原样保留;全量字段的强类型建模留到体检 create +// 任务(写入映射才真正需要每个字段),避免照单样本猜 null 字段类型。 +// +// 注意与档案的两处差异: +// - 最近一次体检 data 是**单个对象**(档案 find 的 data 是数组)。 +// - 体检身份证字段是小写 `idcard`(档案是 `idCard`)。 +type HealthCheckSummary struct { + CheckID string `json:"checkId"` // 体检记录主键;各子节点用 healthCheck 外键指向它 + HealthCheck string `json:"healthCheck"` // 冗余外键,值等于 checkId + IDCard string `json:"idcard"` // 体检用小写 idcard + PersonName string `json:"personName"` + CheckDate string `json:"checkDate"` +} diff --git a/osi/codes.go b/osi/codes.go index 0b5d7ec..ec1cfa4 100644 --- a/osi/codes.go +++ b/osi/codes.go @@ -14,6 +14,11 @@ const ( ServiceIDDoctors = "ZRYS00001" ServiceIDDrugs = "YPML00001" ServiceIDOrgs = "CXJG00002" + + ServiceIDJKTJLast = "JKTJLSJL00002" // 最近一次体检(实测可用) + ServiceIDJKTJList = "JKTJLIST00002" // 已检/待检人员列表(入参待厂家确认) + // 单条查询 JKTJ00002:docx 标 auto/jktj/query,平台实测"没有url的接口配置"(未部署), + // auto/jktj/find 待确认;确认前不登记路由。见 docs/04 §11、docs/06 B5。 ) const codeRetryableTimeout = "405" @@ -27,6 +32,8 @@ var servicePaths = map[string]string{ ServiceIDDoctors: "/auto/zryscx/query", ServiceIDDrugs: "/auto/ypmlcx/query", ServiceIDOrgs: "/auto/cxjg/query", + ServiceIDJKTJLast: "/auto/jktjlscx/query", + ServiceIDJKTJList: "/auto/jktjlist/query", } func PathOf(serviceID string) (string, error) { diff --git a/osi/jktj.go b/osi/jktj.go new file mode 100644 index 0000000..6a94c24 --- /dev/null +++ b/osi/jktj.go @@ -0,0 +1,34 @@ +package osi + +import ( + "context" + + "chis_osi/contract" +) + +type FindHealthCheckQuery struct { + IDCard string + PHRID string + EMPIID string +} + +// LastHealthCheck 查询某人最近一次体检(JKTJLSJL00002 /auto/jktjlscx/query)。 +// data 为单个体检对象(非数组);summary 只取定位字段,完整内容见返回的 Result.Raw。 +func (c *Client) LastHealthCheck(ctx context.Context, query FindHealthCheckQuery) (contract.HealthCheckSummary, Result, error) { + baseInfo, err := query.baseInfo() + if err != nil { + return contract.HealthCheckSummary{}, Result{}, err + } + var summary contract.HealthCheckSummary + result, err := c.Call(ctx, ServiceIDJKTJLast, baseInfo, &summary) + return summary, result, err +} + +func (q FindHealthCheckQuery) baseInfo() (map[string]string, error) { + fields := []queryField{ + {name: "idCard", value: q.IDCard}, + {name: "empiId", value: q.EMPIID}, + {name: "phrid", value: q.PHRID}, + } + return singleBaseInfo("last health check", fields) +} diff --git a/osi/jktj_test.go b/osi/jktj_test.go new file mode 100644 index 0000000..41f14c5 --- /dev/null +++ b/osi/jktj_test.go @@ -0,0 +1,57 @@ +package osi + +import ( + "context" + "net/http" + "net/http/httptest" + "testing" + "time" +) + +func TestLastHealthCheckDecodesSummaryAndHitsPath(t *testing.T) { + var seenPath string + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + seenPath = r.URL.Path + w.Header().Set("Content-Type", "application/json") + // data 为单个对象(非数组);字段名取自实测结构,值均为脱敏假数据 + _, _ = w.Write([]byte(`{"code":"01","message":"操作成功","data":{"checkId":"CHK-TEST-001","healthCheck":"CHK-TEST-001","idcard":"TEST-ID","personName":"测试","checkDate":"2026-01-01"}}`)) + })) + defer server.Close() + + transport, err := NewTransport(TransportConfig{Timeout: time.Second}) + if err != nil { + t.Fatalf("NewTransport: %v", err) + } + client := NewClient(ClientConfig{BaseURL: server.URL, UserName: "u", Ask: "k", Transport: transport}) + + summary, result, err := client.LastHealthCheck(context.Background(), FindHealthCheckQuery{IDCard: "TEST-ID"}) + if err != nil { + t.Fatalf("LastHealthCheck: %v", err) + } + if seenPath != "/osi/api/auto/jktjlscx/query" { + t.Fatalf("path = %q", seenPath) + } + if summary.CheckID != "CHK-TEST-001" || summary.HealthCheck != "CHK-TEST-001" { + t.Fatalf("summary keys = %#v", summary) + } + // 体检身份证是小写 idcard,确认 tag 正确映射 + if summary.IDCard != "TEST-ID" { + t.Fatalf("summary.IDCard = %q (体检 idcard tag 映射错误)", summary.IDCard) + } + if !result.Success || len(result.Raw) == 0 { + t.Fatalf("result should be success with raw retained: %#v", result) + } +} + +func TestFindHealthCheckQueryRequiresExactlyOneKey(t *testing.T) { + if _, err := (FindHealthCheckQuery{}).baseInfo(); err == nil { + t.Fatal("empty query should error") + } + if _, err := (FindHealthCheckQuery{IDCard: "a", PHRID: "b"}).baseInfo(); err == nil { + t.Fatal("two keys should error") + } + bi, err := (FindHealthCheckQuery{IDCard: "a"}).baseInfo() + if err != nil || bi["idCard"] != "a" { + t.Fatalf("baseInfo = %v err = %v", bi, err) + } +}