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) + } +}