client.go 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256
  1. package qiyuesuosdk
  2. import (
  3. "bytes"
  4. "crypto/md5"
  5. "encoding/base64"
  6. "encoding/hex"
  7. "encoding/json"
  8. "fmt"
  9. "io"
  10. "mime/multipart"
  11. "net/http"
  12. "net/url"
  13. "strings"
  14. "time"
  15. "git.sxidc.com/student-physical-examination/contract_lock_sdk/utils"
  16. "github.com/google/uuid"
  17. )
  18. // Client 契约锁开放平台客户端;仅封装 HTTP/签名与契约锁 API,不含业务状态机。
  19. type Client struct {
  20. cfg Config
  21. signDefaults SignDefaults
  22. sdk *utils.SdkClient
  23. http *http.Client
  24. }
  25. // New 创建客户端。signDefaults 可为零值,签章时可按请求单独传入 ProcessID/Launcher。
  26. func New(cfg Config, signDefaults SignDefaults) *Client {
  27. address := strings.TrimRight(cfg.Address, "/")
  28. return &Client{
  29. cfg: cfg,
  30. signDefaults: signDefaults,
  31. sdk: utils.NewSdkClient(address, cfg.AppToken, cfg.AppSecret),
  32. http: &http.Client{Timeout: 60 * time.Second},
  33. }
  34. }
  35. func (c *Client) baseURL() string {
  36. return strings.TrimRight(c.cfg.Address, "/")
  37. }
  38. // PortalURL 契约锁用户前台地址(制章、验证码登录)。
  39. func (c *Client) PortalURL() string {
  40. return UserPortalURL(c.baseURL())
  41. }
  42. func (c *Client) authHeaders() map[string]string {
  43. nonce := uuid.NewString()
  44. timestamp := fmt.Sprintf("%d", time.Now().UnixMilli())
  45. raw := c.cfg.AppToken + c.cfg.AppSecret + timestamp + nonce
  46. sum := md5.Sum([]byte(raw))
  47. return map[string]string{
  48. "x-qys-accesstoken": c.cfg.AppToken,
  49. "x-qys-timestamp": timestamp,
  50. "x-qys-nonce": nonce,
  51. "x-qys-signature": hex.EncodeToString(sum[:]),
  52. }
  53. }
  54. func (c *Client) postJSON(path string, body any, out any) error {
  55. payload, err := json.Marshal(body)
  56. if err != nil {
  57. return err
  58. }
  59. req, err := http.NewRequest(http.MethodPost, c.baseURL()+path, bytes.NewReader(payload))
  60. if err != nil {
  61. return err
  62. }
  63. req.Header.Set("Content-Type", "application/json;charset=UTF-8")
  64. for k, v := range c.authHeaders() {
  65. req.Header.Set(k, v)
  66. }
  67. return c.doJSON(req, out)
  68. }
  69. func (c *Client) getQuery(path string, query url.Values, out any) error {
  70. u := c.baseURL() + path
  71. if len(query) > 0 {
  72. u += "?" + query.Encode()
  73. }
  74. req, err := http.NewRequest(http.MethodGet, u, nil)
  75. if err != nil {
  76. return err
  77. }
  78. for k, v := range c.authHeaders() {
  79. req.Header.Set(k, v)
  80. }
  81. return c.doJSON(req, out)
  82. }
  83. func (c *Client) doJSON(req *http.Request, out any) error {
  84. resp, err := c.http.Do(req)
  85. if err != nil {
  86. return err
  87. }
  88. defer resp.Body.Close()
  89. data, err := io.ReadAll(resp.Body)
  90. if err != nil {
  91. return err
  92. }
  93. if out == nil {
  94. return nil
  95. }
  96. return json.Unmarshal(data, out)
  97. }
  98. func (c *Client) postMultipart(path string, fields map[string]string, fileField string, fileBytes []byte, out any) error {
  99. var buf bytes.Buffer
  100. w := multipart.NewWriter(&buf)
  101. for k, v := range fields {
  102. if err := w.WriteField(k, v); err != nil {
  103. return err
  104. }
  105. }
  106. if len(fileBytes) > 0 && fileField != "" {
  107. part, err := w.CreateFormFile(fileField, "document.pdf")
  108. if err != nil {
  109. return err
  110. }
  111. if _, err = part.Write(fileBytes); err != nil {
  112. return err
  113. }
  114. }
  115. if err := w.Close(); err != nil {
  116. return err
  117. }
  118. req, err := http.NewRequest(http.MethodPost, c.baseURL()+path, &buf)
  119. if err != nil {
  120. return err
  121. }
  122. req.Header.Set("Content-Type", w.FormDataContentType())
  123. for k, v := range c.authHeaders() {
  124. req.Header.Set(k, v)
  125. }
  126. return c.doJSON(req, out)
  127. }
  128. func (c *Client) postForImageBytes(path string, body any) ([]byte, string, error) {
  129. payload, err := json.Marshal(body)
  130. if err != nil {
  131. return nil, "", err
  132. }
  133. req, err := http.NewRequest(http.MethodPost, c.baseURL()+path, bytes.NewReader(payload))
  134. if err != nil {
  135. return nil, "", err
  136. }
  137. req.Header.Set("Content-Type", "application/json;charset=UTF-8")
  138. req.Header.Set("Accept", "application/json,image/*,*/*")
  139. for k, v := range c.authHeaders() {
  140. req.Header.Set(k, v)
  141. }
  142. resp, err := c.http.Do(req)
  143. if err != nil {
  144. return nil, "", err
  145. }
  146. defer resp.Body.Close()
  147. data, err := io.ReadAll(resp.Body)
  148. if err != nil {
  149. return nil, "", err
  150. }
  151. contentType := strings.TrimSpace(strings.Split(resp.Header.Get("Content-Type"), ";")[0])
  152. if len(data) > 0 && data[0] == '{' {
  153. var wrapper struct {
  154. apiResponse
  155. Result json.RawMessage `json:"result"`
  156. }
  157. if err = json.Unmarshal(data, &wrapper); err != nil {
  158. return nil, "", err
  159. }
  160. if err = wrapper.err(); err != nil {
  161. return nil, "", err
  162. }
  163. if len(wrapper.Result) == 0 || string(wrapper.Result) == "null" {
  164. return nil, "", ErrSealImageUnavailable
  165. }
  166. var resultStr string
  167. if err = json.Unmarshal(wrapper.Result, &resultStr); err == nil && resultStr != "" {
  168. img, decErr := base64.StdEncoding.DecodeString(resultStr)
  169. if decErr != nil {
  170. return nil, "", decErr
  171. }
  172. if contentType == "" {
  173. contentType = "image/png"
  174. }
  175. return img, contentType, nil
  176. }
  177. return nil, "", ErrSealImageUnavailable
  178. }
  179. if resp.StatusCode < http.StatusOK || resp.StatusCode >= http.StatusMultipleChoices {
  180. return nil, "", fmt.Errorf("qiyuesuo: http %d", resp.StatusCode)
  181. }
  182. if contentType == "" {
  183. contentType = "image/png"
  184. }
  185. return data, contentType, nil
  186. }
  187. func (c *Client) download(path string, query url.Values) ([]byte, error) {
  188. u := c.baseURL() + path
  189. if len(query) > 0 {
  190. u += "?" + query.Encode()
  191. }
  192. req, err := http.NewRequest(http.MethodGet, u, nil)
  193. if err != nil {
  194. return nil, err
  195. }
  196. for k, v := range c.authHeaders() {
  197. req.Header.Set(k, v)
  198. }
  199. resp, err := c.http.Do(req)
  200. if err != nil {
  201. return nil, err
  202. }
  203. defer resp.Body.Close()
  204. return io.ReadAll(resp.Body)
  205. }
  206. func (c *Client) serviceOK(resp *utils.SdkResponse) error {
  207. if resp == nil {
  208. return fmt.Errorf("qiyuesuo: empty sdk response")
  209. }
  210. if resp.Message == "SUCCESS" {
  211. return nil
  212. }
  213. return fmt.Errorf("qiyuesuo: %s", resp.Message)
  214. }
  215. func (c *Client) serviceString(resp *utils.SdkResponse) (string, error) {
  216. if err := c.serviceOK(resp); err != nil {
  217. return "", err
  218. }
  219. if resp.Result == nil {
  220. return "", nil
  221. }
  222. if s, ok := resp.Result.(string); ok {
  223. return s, nil
  224. }
  225. return "", fmt.Errorf("qiyuesuo: unexpected result type %T", resp.Result)
  226. }
  227. func (c *Client) resolveProcessID(id string) string {
  228. if id != "" {
  229. return id
  230. }
  231. return c.signDefaults.ProcessID
  232. }
  233. func (c *Client) resolveLauncher(name string) string {
  234. if name != "" {
  235. return name
  236. }
  237. return c.signDefaults.TenantName
  238. }