257 lines
7.8 KiB
Go
257 lines
7.8 KiB
Go
// 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、uploadToken;PDF 上传地址由 transit 改走 forward)
|
||
// GetPrescriptionPrintDetail → GET /api/hy/transit/prescription/detail(recipe 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"`
|
||
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 json.Unmarshal(envelope.Result, out)
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// 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)
|
||
}
|