diff --git a/contract/jktj.go b/contract/jktj.go index 05a959d..4800504 100644 --- a/contract/jktj.go +++ b/contract/jktj.go @@ -18,6 +18,16 @@ type HealthCheckSummary struct { CheckDate string `json:"checkDate"` } +// HealthCheckRecordSummary 是 JKTJ00002(某人全部体检)数组里一条体检的定位字段。 +// 完整体检内容(结构同 §11.3)由 osi.Result.Raw 保留,本结构只取定位/列举字段。 +// ⚠ 身份证字段是驼峰 idCard(≠ 最近一次 HealthCheckSummary 的小写 idcard)。 +type HealthCheckRecordSummary struct { + CheckID string `json:"checkId"` // 体检编号;历史记录为 null(→""),仅近年有值 + CheckDate string `json:"checkDate"` // 体检日期,历史记录用它定位(checkId 可能缺) + IDCard string `json:"idCard"` // 注意驼峰 + PersonName string `json:"personName"` +} + // HealthCheckPerson 是 JKTJLIST00002 已检/未检名单里的一行(人员 + 体检状态,非体检明细)。 // 字段名以实测样本为准(docx 多处有误,见 docs/04 §11.4)。字段稳定且均为字符串,故全部建模。 type HealthCheckPerson struct { diff --git a/handler/health_check.go b/handler/health_check.go index 72305b0..5984247 100644 --- a/handler/health_check.go +++ b/handler/health_check.go @@ -36,6 +36,27 @@ func (h *HealthCheckHandler) Last(w http.ResponseWriter, r *http.Request) { writeRawOrError(w, result, err) } +// All 处理 GET /api/health-check/all?idCard=..|phrid=..|empiId=.. +// 返回某人全部体检(JKTJ00002 数组)的平台完整响应;历史记录 checkId 可能为空。 +func (h *HealthCheckHandler) All(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.FindHealthCheckQuery{ + IDCard: q.Get("idCard"), + PHRID: q.Get("phrid"), + EMPIID: q.Get("empiId"), + } + if query.IDCard == "" && query.PHRID == "" && query.EMPIID == "" { + writeJSONError(w, http.StatusBadRequest, "provide one of: idCard, phrid, empiId") + return + } + _, result, err := h.client.QueryHealthChecks(r.Context(), query) + writeRawOrError(w, result, err) +} + // List 处理 GET /api/health-check/list?checkYear=..&idCard=..&checkType=..&page=..&rows=.. // 返回某年度已检/未检人员名单的平台完整响应(名单,非体检明细,见 docs/04 §11.4)。 func (h *HealthCheckHandler) List(w http.ResponseWriter, r *http.Request) { diff --git a/handler/health_check_test.go b/handler/health_check_test.go index a1ba0f4..dcd84e3 100644 --- a/handler/health_check_test.go +++ b/handler/health_check_test.go @@ -59,6 +59,36 @@ func TestHealthCheckLastRequiresIdentifier(t *testing.T) { } } +func TestHealthCheckAllReturnsArrayRaw(t *testing.T) { + client, seenPath := newTestClient(t, `{"code":"01","message":"操作成功","data":[{"checkId":"CHK-1","idCard":"TEST-1"},{"checkId":null,"idCard":"TEST-1"}]}`) + h := NewHealthCheckHandler(client) + + req := httptest.NewRequest(http.MethodGet, "/api/health-check/all?idCard=TEST-1", nil) + rec := httptest.NewRecorder() + h.All(rec, req) + + if rec.Code != http.StatusOK { + t.Fatalf("status = %d", rec.Code) + } + if *seenPath != "/osi/api/auto/jktj/query" { + t.Fatalf("osi path = %q", *seenPath) + } + body, _ := io.ReadAll(rec.Body) + if !strings.Contains(string(body), `"CHK-1"`) { + t.Fatalf("body missing records: %s", body) + } +} + +func TestHealthCheckAllRequiresIdentifier(t *testing.T) { + h := NewHealthCheckHandler(nil) + req := httptest.NewRequest(http.MethodGet, "/api/health-check/all", nil) + rec := httptest.NewRecorder() + h.All(rec, req) + if rec.Code != http.StatusBadRequest { + t.Fatalf("status = %d, want 400", rec.Code) + } +} + func TestHealthCheckListRequiresCheckYear(t *testing.T) { h := NewHealthCheckHandler(nil) // 缺 checkYear 在触达 client 前返回 400 req := httptest.NewRequest(http.MethodGet, "/api/health-check/list?idCard=TEST-1", nil) diff --git a/osi/codes.go b/osi/codes.go index ec1cfa4..aa934a8 100644 --- a/osi/codes.go +++ b/osi/codes.go @@ -15,10 +15,9 @@ const ( ServiceIDDrugs = "YPML00001" ServiceIDOrgs = "CXJG00002" - ServiceIDJKTJLast = "JKTJLSJL00002" // 最近一次体检(实测可用) - ServiceIDJKTJList = "JKTJLIST00002" // 已检/待检人员列表(入参待厂家确认) - // 单条查询 JKTJ00002:docx 标 auto/jktj/query,平台实测"没有url的接口配置"(未部署), - // auto/jktj/find 待确认;确认前不登记路由。见 docs/04 §11、docs/06 B5。 + ServiceIDJKTJLast = "JKTJLSJL00002" // 最近一次体检(实测可用) + ServiceIDJKTJQuery = "JKTJ00002" // 某人全部体检(实测可用,返回数组) + ServiceIDJKTJList = "JKTJLIST00002" // 已检/待检人员名单(实测可用) ) const codeRetryableTimeout = "405" @@ -33,6 +32,7 @@ var servicePaths = map[string]string{ ServiceIDDrugs: "/auto/ypmlcx/query", ServiceIDOrgs: "/auto/cxjg/query", ServiceIDJKTJLast: "/auto/jktjlscx/query", + ServiceIDJKTJQuery: "/auto/jktj/query", ServiceIDJKTJList: "/auto/jktjlist/query", } diff --git a/osi/jktj.go b/osi/jktj.go index 05fe80c..98b106a 100644 --- a/osi/jktj.go +++ b/osi/jktj.go @@ -25,6 +25,19 @@ func (c *Client) LastHealthCheck(ctx context.Context, query FindHealthCheckQuery return summary, result, err } +// QueryHealthChecks 查询某人全部体检(JKTJ00002 /auto/jktj/query)。 +// data 为数组=该人全部体检历史;summaries 只取定位字段(checkId/checkDate 等), +// 每条完整体检内容见返回的 Result.Raw。历史记录 checkId 可能为空,按 checkDate 区分。 +func (c *Client) QueryHealthChecks(ctx context.Context, query FindHealthCheckQuery) ([]contract.HealthCheckRecordSummary, Result, error) { + baseInfo, err := query.baseInfo() + if err != nil { + return nil, Result{}, err + } + var summaries []contract.HealthCheckRecordSummary + result, err := c.Call(ctx, ServiceIDJKTJQuery, baseInfo, &summaries) + return summaries, result, err +} + func (q FindHealthCheckQuery) baseInfo() (map[string]string, error) { fields := []queryField{ {name: "idCard", value: q.IDCard}, diff --git a/osi/jktj_test.go b/osi/jktj_test.go index f0c48e5..87bb871 100644 --- a/osi/jktj_test.go +++ b/osi/jktj_test.go @@ -44,6 +44,43 @@ func TestLastHealthCheckDecodesSummaryAndHitsPath(t *testing.T) { } } +func TestQueryHealthChecksDecodesArrayAndHitsPath(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 为数组=某人全部体检;idCard 驼峰;历史记录 checkId 为 null(脱敏假数据) + _, _ = w.Write([]byte(`{"code":"01","message":"操作成功","data":[` + + `{"checkId":"CHK-1","checkDate":"2025-06-23","idCard":"TEST-ID","personName":"测试"},` + + `{"checkId":null,"checkDate":"2013-06-07","idCard":"TEST-ID","personName":"测试"}]}`)) + })) + 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}) + + records, result, err := client.QueryHealthChecks(context.Background(), FindHealthCheckQuery{IDCard: "TEST-ID"}) + if err != nil { + t.Fatalf("QueryHealthChecks: %v", err) + } + if seenPath != "/osi/api/auto/jktj/query" { + t.Fatalf("path = %q", seenPath) + } + if !result.Success || len(records) != 2 { + t.Fatalf("records = %d result = %#v", len(records), result) + } + // 驼峰 idCard 应解出;历史记录 null checkId 解为空串且不报错 + if records[0].CheckID != "CHK-1" || records[0].IDCard != "TEST-ID" { + t.Fatalf("row0 = %#v", records[0]) + } + if records[1].CheckID != "" || records[1].CheckDate != "2013-06-07" { + t.Fatalf("row1 (null checkId) = %#v", records[1]) + } +} + func TestListHealthCheckPeopleDecodesRosterAndHitsPath(t *testing.T) { var seenPath string var seenBody map[string]any diff --git a/server.go b/server.go index 2e8c1c2..07a919d 100644 --- a/server.go +++ b/server.go @@ -20,11 +20,13 @@ func runServer(cfg config.Config, addr string) error { mux := http.NewServeMux() mux.HandleFunc("/api/health-record/find", hr.Find) 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-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) server := &http.Server{Addr: addr, Handler: mux}