Files
xk-hy-transit-go/internal/xkapi/client.go
2026-05-25 12:53:48 +08:00

270 lines
8.3 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Package xkapi 调用云端 xk-api前缀 /api/hy鉴权 X-Hy-Transit-Token
//
// 与 sync.runStep 的对应关系:
//
// BatchCreate → POST /api/hy/transit/batch/create
// Pull → GET /api/hy/supervise/{step}?batch_id=&date=
// BatchCallback→ POST /api/hy/transit/batch/callback路径可配 XK_API_CALLBACK_PATH
// BatchFinish → POST /api/hy/transit/batch/finish
// Config → GET /api/hy/supervise/config机构 ID、uploadTokenPDF 上传地址由 transit 改走 forward
// GetPrescriptionPrintDetail → GET /api/hy/transit/prescription/detailrecipe PDF HTML
// SaveRecipeFile → POST /api/hy/transit/prescription/recipe-file回写 recipeFileId
//
// 响应统一为 code=0 时 result 字段;见 docs/hy-transit-api.md。
package xkapi
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"strings"
"time"
)
// Client 云端 API 客户端。
type Client struct {
baseURL string
token string
callbackPath string
http *http.Client
}
// New 创建客户端callbackPath 为批量回调路径,如 /api/hy/transit/batch/callback。
func New(baseURL, token, callbackPath string) *Client {
if callbackPath == "" {
callbackPath = "/api/hy/transit/batch/callback"
}
return &Client{
baseURL: strings.TrimRight(baseURL, "/"),
token: token,
callbackPath: callbackPath,
http: &http.Client{Timeout: 120 * time.Second},
}
}
// PullResponse 单次拉取响应。
type PullResponse struct {
AnchorDate string `json:"anchor_date"`
BatchID int `json:"batch_id"`
Items []PullItem `json:"items"`
}
// PullItem 单条组包记录(含云端 record_id
type PullItem struct {
RecordID int `json:"record_id"`
BizKey string `json:"biz_key"`
Payload map[string]any `json:"payload"`
Meta map[string]any `json:"meta"`
ValidationErrors []string `json:"validation_errors"`
}
// ConfigResponse 机构公开配置Runner 启动时拉取一次)。
// FileUploadURL 在 FILE_UPLOAD_VIA_FORWARD=true 时由 transit 忽略,改走 forward 路径。
type ConfigResponse struct {
OrganID string `json:"organID"`
UnitID string `json:"unitID"`
OrganName string `json:"organName"`
HosCode string `json:"hosCode"`
HosName string `json:"hosName"`
FileUploadURL string `json:"fileUploadUrl"`
FileBucket string `json:"fileBucket"`
UploadToken string `json:"uploadToken"`
}
// CallbackItem 批量回调单条结果。
type CallbackItem struct {
RecordID int `json:"record_id"`
BizKey string `json:"biz_key,omitempty"`
PushStatus string `json:"push_status"`
CallbackStatus string `json:"callback_status"`
HTTPCode int `json:"http_code,omitempty"`
MsgCode int `json:"msg_code,omitempty"`
Msg string `json:"msg,omitempty"`
TraceID string `json:"trace_id,omitempty"`
ResponseBody string `json:"response_body,omitempty"`
ErrorMessage string `json:"error_message,omitempty"`
}
func (c *Client) setAuth(req *http.Request) {
req.Header.Set("X-Hy-Transit-Token", c.token)
}
func (c *Client) doJSON(req *http.Request, out any) error {
c.setAuth(req)
resp, err := c.http.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("http %d: %s", resp.StatusCode, string(body))
}
var envelope struct {
Code int `json:"code"`
Result json.RawMessage `json:"result"`
ResultEncrypted bool `json:"result_encrypted"`
Message string `json:"message"`
}
if err := json.Unmarshal(body, &envelope); err != nil {
return err
}
if envelope.Code != 0 {
return fmt.Errorf("api code %d: %s", envelope.Code, envelope.Message)
}
if out == nil || len(envelope.Result) == 0 {
return nil
}
resultJSON := envelope.Result
if envelope.ResultEncrypted {
var cipherStr string
if err := json.Unmarshal(envelope.Result, &cipherStr); err != nil {
return fmt.Errorf("parse encrypted result: %w", err)
}
plain, err := DecryptBody(cipherStr, c.token)
if err != nil {
return fmt.Errorf("decrypt result: %w", err)
}
resultJSON = []byte(plain)
}
return json.Unmarshal(resultJSON, out)
}
// BatchCreate 创建同步批次,返回 batch_id。
func (c *Client) BatchCreate(anchorDate, step string) (int, error) {
payload := map[string]string{
"anchor_date": anchorDate,
"step": step,
}
raw, _ := json.Marshal(payload)
req, err := http.NewRequest(http.MethodPost, c.baseURL+"/api/hy/transit/batch/create", bytes.NewReader(raw))
if err != nil {
return 0, err
}
req.Header.Set("Content-Type", "application/json")
var result struct {
BatchID int `json:"batch_id"`
}
if err := c.doJSON(req, &result); err != nil {
return 0, err
}
return result.BatchID, nil
}
// BatchCallback 整批回写上报结果。
func (c *Client) BatchCallback(batchID int, items []CallbackItem) error {
payload := map[string]any{
"batch_id": batchID,
"items": items,
}
raw, _ := json.Marshal(payload)
req, err := http.NewRequest(http.MethodPost, c.baseURL+c.callbackPath, bytes.NewReader(raw))
if err != nil {
return err
}
req.Header.Set("Content-Type", "application/json")
return c.doJSON(req, nil)
}
// BatchFinish 标记批次拉取结束。
func (c *Client) BatchFinish(batchID int, pullStatus string) error {
payload := map[string]any{
"batch_id": batchID,
"pull_status": pullStatus,
}
raw, _ := json.Marshal(payload)
req, err := http.NewRequest(http.MethodPost, c.baseURL+"/api/hy/transit/batch/finish", bytes.NewReader(raw))
if err != nil {
return err
}
req.Header.Set("Content-Type", "application/json")
return c.doJSON(req, nil)
}
// Pull 拉取指定 step 的组包列表(需 batch_id
func (c *Client) Pull(step, date string, batchID int) (*PullResponse, error) {
u, err := url.Parse(c.baseURL + "/api/hy/supervise/" + step)
if err != nil {
return nil, err
}
q := u.Query()
q.Set("date", date)
q.Set("batch_id", fmt.Sprintf("%d", batchID))
u.RawQuery = q.Encode()
req, err := http.NewRequest(http.MethodGet, u.String(), nil)
if err != nil {
return nil, err
}
var result PullResponse
if err := c.doJSON(req, &result); err != nil {
return nil, fmt.Errorf("pull %s: %w", step, err)
}
return &result, nil
}
// Config 获取机构配置(加密上报用)。
func (c *Client) Config() (*ConfigResponse, error) {
req, err := http.NewRequest(http.MethodGet, c.baseURL+"/api/hy/supervise/config", nil)
if err != nil {
return nil, err
}
var result ConfigResponse
if err := c.doJSON(req, &result); err != nil {
return nil, err
}
return &result, nil
}
// PrescriptionPrintDetail transit 处方详情(含打印 HTML
type PrescriptionPrintDetail struct {
RecipeFileHTML string `json:"recipe_file_html"`
PatientName string `json:"patient_name"`
StoreName string `json:"store_name"`
PrescriptionNo string `json:"prescription_no"`
CreatedAt string `json:"created_at"`
}
// GetPrescriptionPrintDetail 拉取与 PC detail 一致的处方数据及 recipe_file_html。
func (c *Client) GetPrescriptionPrintDetail(prescriptionID int) (*PrescriptionPrintDetail, error) {
u, err := url.Parse(c.baseURL + "/api/hy/transit/prescription/detail")
if err != nil {
return nil, err
}
q := u.Query()
q.Set("prescription_id", fmt.Sprintf("%d", prescriptionID))
u.RawQuery = q.Encode()
req, err := http.NewRequest(http.MethodGet, u.String(), nil)
if err != nil {
return nil, err
}
var result PrescriptionPrintDetail
if err := c.doJSON(req, &result); err != nil {
return nil, err
}
if strings.TrimSpace(result.RecipeFileHTML) == "" {
return nil, fmt.Errorf("empty recipe_file_html for prescription %d", prescriptionID)
}
return &result, nil
}
// SaveRecipeFile 回写处方监管文件 ID。
func (c *Client) SaveRecipeFile(prescriptionID int, fileID string, recordID int) error {
payload := map[string]any{
"prescription_id": prescriptionID,
"recipe_file_id": fileID,
"record_id": recordID,
}
raw, _ := json.Marshal(payload)
req, err := http.NewRequest(http.MethodPost, c.baseURL+"/api/hy/transit/prescription/recipe-file", bytes.NewReader(raw))
if err != nil {
return err
}
req.Header.Set("Content-Type", "application/json")
return c.doJSON(req, nil)
}