Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
116 lines
2.8 KiB
Go
116 lines
2.8 KiB
Go
package laosiji
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
var (
|
|
// LaosijiDomains 老司机上传凭证缓存。由 web server 初始化。
|
|
LaosijiDomains *Domains
|
|
// LaosijiClient 老司机业务 API 客户端。由 web server 初始化。
|
|
LaosijiClient *Client
|
|
)
|
|
|
|
// Client 老司机「业务 API」客户端(区别于上传站 upload_url)。
|
|
type Client struct {
|
|
baseURL string
|
|
appid string
|
|
appkey string
|
|
hc *http.Client
|
|
}
|
|
|
|
// New 创建业务 API 客户端。baseURL 含 /lsjapi 前缀。
|
|
func New(baseURL, appid, appkey string) *Client {
|
|
return &Client{
|
|
baseURL: strings.TrimRight(baseURL, "/"),
|
|
appid: appid,
|
|
appkey: appkey,
|
|
hc: &http.Client{Timeout: 40 * time.Second},
|
|
}
|
|
}
|
|
|
|
// SetHTTPClient 替换底层 http.Client(测试/自定义超时用)。
|
|
func (c *Client) SetHTTPClient(hc *http.Client) { c.hc = hc }
|
|
|
|
type apiResp struct {
|
|
Status string `json:"status"`
|
|
Data string `json:"data"`
|
|
Error string `json:"error"`
|
|
}
|
|
|
|
// ErrEmptyData status=y 但 data 为空。
|
|
var ErrEmptyData = errors.New("laosiji: empty response data")
|
|
|
|
// Call 发起一次业务 API 调用。
|
|
func (c *Client) Call(ctx context.Context, path string, req, out interface{}) error {
|
|
reqJSON := []byte("{}")
|
|
if req != nil {
|
|
b, err := json.Marshal(req)
|
|
if err != nil {
|
|
return fmt.Errorf("laosiji: marshal req: %w", err)
|
|
}
|
|
reqJSON = b
|
|
}
|
|
encBody, err := EncryptBase64(reqJSON, c.appkey)
|
|
if err != nil {
|
|
return fmt.Errorf("laosiji: encrypt req: %w", err)
|
|
}
|
|
|
|
httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, c.baseURL+path, strings.NewReader(encBody))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
httpReq.Header.Set("appid", c.appid)
|
|
httpReq.Header.Set("Content-Type", "application/json")
|
|
|
|
resp, err := c.hc.Do(httpReq)
|
|
if err != nil {
|
|
return fmt.Errorf("laosiji: http do: %w", err)
|
|
}
|
|
defer func() { _ = resp.Body.Close() }()
|
|
|
|
body, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
return fmt.Errorf("laosiji: read resp: %w", err)
|
|
}
|
|
if resp.StatusCode != http.StatusOK {
|
|
return fmt.Errorf("laosiji: http %d: %s", resp.StatusCode, truncate(body, 200))
|
|
}
|
|
|
|
var outer apiResp
|
|
if err := json.Unmarshal(body, &outer); err != nil {
|
|
return fmt.Errorf("laosiji: unmarshal outer: %w (body=%s)", err, truncate(body, 200))
|
|
}
|
|
if outer.Status != "y" {
|
|
return fmt.Errorf("laosiji: api status=%q error=%q", outer.Status, outer.Error)
|
|
}
|
|
if out == nil {
|
|
return nil
|
|
}
|
|
if outer.Data == "" {
|
|
return ErrEmptyData
|
|
}
|
|
plain, err := DecryptBase64(outer.Data, c.appkey)
|
|
if err != nil {
|
|
return fmt.Errorf("laosiji: decrypt data: %w", err)
|
|
}
|
|
if err := json.Unmarshal(plain, out); err != nil {
|
|
return fmt.Errorf("laosiji: unmarshal data: %w (plain=%s)", err, truncate(plain, 200))
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func truncate(b []byte, n int) string {
|
|
if len(b) > n {
|
|
return string(b[:n]) + "..."
|
|
}
|
|
return string(b)
|
|
}
|