feat(handler): 暴露健康档案 upsert 接口(T-204)

This commit is contained in:
ila
2026-07-16 02:00:56 +08:00
parent 3a30ebdc45
commit 48263d8b9a
16 changed files with 852 additions and 44 deletions
+96
View File
@@ -0,0 +1,96 @@
package handler
import (
"context"
"encoding/json"
"errors"
"io"
"net/http"
"chis_osi/pipeline"
"chis_osi/source"
)
const maxHealthRecordUpsertBody = 1 << 20
type HealthRecordUpserter interface {
Upsert(context.Context, source.HealthRecordTask) (pipeline.Outcome, error)
}
type HealthRecordUpsertHandler struct {
upserter HealthRecordUpserter
}
func NewHealthRecordUpsertHandler(upserter HealthRecordUpserter) *HealthRecordUpsertHandler {
return &HealthRecordUpsertHandler{upserter: upserter}
}
func (h *HealthRecordUpsertHandler) Upsert(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
w.Header().Set("Allow", http.MethodPost)
writeJSONError(w, http.StatusMethodNotAllowed, "only POST is supported")
return
}
if h.upserter == nil {
writeJSONError(w, http.StatusServiceUnavailable, "health record upsert is unavailable")
return
}
r.Body = http.MaxBytesReader(w, r.Body, maxHealthRecordUpsertBody)
raw, err := io.ReadAll(r.Body)
if err != nil {
var maxBytesErr *http.MaxBytesError
if errors.As(err, &maxBytesErr) {
writeJSONError(w, http.StatusRequestEntityTooLarge, "request body exceeds 1 MiB")
return
}
writeJSONError(w, http.StatusBadRequest, "read request body failed")
return
}
task, err := source.DecodeHealthRecordTask(raw)
if err != nil {
writeJSONError(w, http.StatusBadRequest, "invalid PHIS health record envelope")
return
}
outcome, err := h.upserter.Upsert(r.Context(), task)
if err != nil {
response := healthRecordUpsertErrorResponse{Error: "health record upsert failed", RetrySafe: outcome.Status == ""}
if outcome.Status != "" {
response.Outcome = &outcome
}
writeJSON(w, http.StatusBadGateway, response)
return
}
writeJSON(w, outcomeHTTPStatus(outcome), outcome)
}
type healthRecordUpsertErrorResponse struct {
Error string `json:"error"`
RetrySafe bool `json:"retrySafe"`
Outcome *pipeline.Outcome `json:"outcome,omitempty"`
}
func outcomeHTTPStatus(outcome pipeline.Outcome) int {
switch outcome.Status {
case pipeline.StatusDone:
return http.StatusOK
case pipeline.StatusManualReview:
return http.StatusConflict
case pipeline.StatusRetry:
return http.StatusServiceUnavailable
case pipeline.StatusFailed:
if outcome.ServiceID != "" {
return http.StatusBadGateway
}
return http.StatusUnprocessableEntity
default:
return http.StatusInternalServerError
}
}
func writeJSON(w http.ResponseWriter, status int, value any) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.WriteHeader(status)
_ = json.NewEncoder(w).Encode(value)
}