|
|
@@ -0,0 +1,202 @@
|
|
|
+package api
|
|
|
+
|
|
|
+import (
|
|
|
+ "bytes"
|
|
|
+ "context"
|
|
|
+ "crypto/md5"
|
|
|
+ "encoding/hex"
|
|
|
+ "encoding/json"
|
|
|
+ "errors"
|
|
|
+ "fmt"
|
|
|
+ "io"
|
|
|
+ "net/http"
|
|
|
+ "sort"
|
|
|
+ "strconv"
|
|
|
+ "strings"
|
|
|
+ "time"
|
|
|
+)
|
|
|
+
|
|
|
+const xiaoQiRoleReportMethod = "common.roleReport"
|
|
|
+
|
|
|
+// XiaoQiReporter 封裝小七上報能力。
|
|
|
+type XiaoQiReporter struct {
|
|
|
+ Endpoint string
|
|
|
+ AppID string
|
|
|
+ AppSecret string
|
|
|
+ HTTPClient *http.Client
|
|
|
+}
|
|
|
+
|
|
|
+// XiaoQiRole 角色信息(對應文檔 Role 欄位)。
|
|
|
+type XiaoQiRole struct {
|
|
|
+ RoleID string `json:"roleId"` // 必填
|
|
|
+ GUID string `json:"guid"` // 必填
|
|
|
+ RoleName string `json:"roleName"` // 必填
|
|
|
+ ServerID string `json:"serverId"` // 必填
|
|
|
+ ServerName string `json:"serverName"` // 必填
|
|
|
+ RoleLevel string `json:"roleLevel"` // 必填
|
|
|
+ RoleCE string `json:"roleCE"` // 必填
|
|
|
+ RoleStage string `json:"roleStage"` // 必填(可放 JSON 字串)
|
|
|
+ RoleRechargeAmount float64 `json:"roleRechargeAmount"` // 必填(兩位精度)
|
|
|
+ RoleGuild string `json:"roleGuild,omitempty"` // 選填
|
|
|
+ RoleGuildID string `json:"roleGuildId,omitempty"` // 選填
|
|
|
+}
|
|
|
+
|
|
|
+// XiaoQiRoleReportBizParams 業務參數。
|
|
|
+type XiaoQiRoleReportBizParams struct {
|
|
|
+ Role XiaoQiRole `json:"role"`
|
|
|
+}
|
|
|
+
|
|
|
+// xiaoQiCommonRequest 小七公共請求包。
|
|
|
+type xiaoQiCommonRequest struct {
|
|
|
+ APIMethod string `json:"apiMethod"`
|
|
|
+ AppID string `json:"appId"`
|
|
|
+ Timestamp int64 `json:"timestamp"`
|
|
|
+ BizParams interface{} `json:"bizParams"`
|
|
|
+ Sign string `json:"sign"`
|
|
|
+}
|
|
|
+
|
|
|
+// XiaoQiBizResp 業務回應(文檔 bizResp)。
|
|
|
+type XiaoQiBizResp struct {
|
|
|
+ RespCode string `json:"respCode"`
|
|
|
+ RespMsg string `json:"respMsg"`
|
|
|
+}
|
|
|
+
|
|
|
+// XiaoQiCommonResponse 通用回包容器。
|
|
|
+type XiaoQiCommonResponse struct {
|
|
|
+ BizResp XiaoQiBizResp `json:"bizResp"`
|
|
|
+}
|
|
|
+
|
|
|
+// ReportRole 創建/變更角色時,上報角色信息到小七。
|
|
|
+// 成功條件:HTTP 2xx 且 bizResp.respCode == SUCCESS。
|
|
|
+func (r *XiaoQiReporter) ReportRole(ctx context.Context, role XiaoQiRole) (*XiaoQiBizResp, error) {
|
|
|
+ if err := r.validate(); err != nil {
|
|
|
+ return nil, err
|
|
|
+ }
|
|
|
+ if err := validateRole(role); err != nil {
|
|
|
+ return nil, err
|
|
|
+ }
|
|
|
+
|
|
|
+ // 保留兩位小數,避免浮點顯示差異導致簽名/平台解析問題。
|
|
|
+ role.RoleRechargeAmount, _ = strconv.ParseFloat(fmt.Sprintf("%.2f", role.RoleRechargeAmount), 64)
|
|
|
+
|
|
|
+ biz := XiaoQiRoleReportBizParams{Role: role}
|
|
|
+ reqBody := xiaoQiCommonRequest{
|
|
|
+ APIMethod: xiaoQiRoleReportMethod,
|
|
|
+ AppID: r.AppID,
|
|
|
+ Timestamp: time.Now().Unix(),
|
|
|
+ BizParams: biz,
|
|
|
+ }
|
|
|
+ //reqBody.Sign = buildSign(reqBody.APIMethod, reqBody.AppID, reqBody.Timestamp, biz, r.AppSecret)
|
|
|
+
|
|
|
+ var resp XiaoQiCommonResponse
|
|
|
+ if err := r.postJSON(ctx, reqBody, &resp); err != nil {
|
|
|
+ return nil, err
|
|
|
+ }
|
|
|
+
|
|
|
+ if !strings.EqualFold(resp.BizResp.RespCode, "SUCCESS") {
|
|
|
+ return &resp.BizResp, fmt.Errorf("xiaoqi role report failed, respCode=%s respMsg=%s", resp.BizResp.RespCode, resp.BizResp.RespMsg)
|
|
|
+ }
|
|
|
+ return &resp.BizResp, nil
|
|
|
+}
|
|
|
+
|
|
|
+func (r *XiaoQiReporter) validate() error {
|
|
|
+ if strings.TrimSpace(r.Endpoint) == "" {
|
|
|
+ return errors.New("xiaoqi endpoint is empty")
|
|
|
+ }
|
|
|
+ return nil
|
|
|
+}
|
|
|
+
|
|
|
+func validateRole(role XiaoQiRole) error {
|
|
|
+ if strings.TrimSpace(role.RoleID) == "" {
|
|
|
+ return errors.New("roleId is required")
|
|
|
+ }
|
|
|
+ if strings.TrimSpace(role.GUID) == "" {
|
|
|
+ return errors.New("guid is required")
|
|
|
+ }
|
|
|
+ if strings.TrimSpace(role.RoleName) == "" {
|
|
|
+ return errors.New("roleName is required")
|
|
|
+ }
|
|
|
+ if strings.TrimSpace(role.ServerID) == "" {
|
|
|
+ return errors.New("serverId is required")
|
|
|
+ }
|
|
|
+ if strings.TrimSpace(role.ServerName) == "" {
|
|
|
+ return errors.New("serverName is required")
|
|
|
+ }
|
|
|
+ if strings.TrimSpace(role.RoleLevel) == "" {
|
|
|
+ return errors.New("roleLevel is required")
|
|
|
+ }
|
|
|
+ if strings.TrimSpace(role.RoleCE) == "" {
|
|
|
+ return errors.New("roleCE is required")
|
|
|
+ }
|
|
|
+ if strings.TrimSpace(role.RoleStage) == "" {
|
|
|
+ return errors.New("roleStage is required")
|
|
|
+ }
|
|
|
+ if role.RoleRechargeAmount < 0 {
|
|
|
+ return errors.New("roleRechargeAmount must be >= 0")
|
|
|
+ }
|
|
|
+ return nil
|
|
|
+}
|
|
|
+
|
|
|
+// buildSign 生成簽名:將公共參數與 bizParams(JSON字串)按 key 排序拼接,再拼 appSecret 後做 MD5。
|
|
|
+// 具體拼接規則若你們文檔有固定格式,可只替換此函數,不影響上層業務調用。
|
|
|
+func buildSign(apiMethod, appID string, timestamp int64, bizParams interface{}, secret string) string {
|
|
|
+ bizBytes, _ := json.Marshal(bizParams)
|
|
|
+ params := map[string]string{
|
|
|
+ "apiMethod": apiMethod,
|
|
|
+ "appId": appID,
|
|
|
+ "bizParams": string(bizBytes),
|
|
|
+ "timestamp": strconv.FormatInt(timestamp, 10),
|
|
|
+ }
|
|
|
+
|
|
|
+ keys := make([]string, 0, len(params))
|
|
|
+ for k := range params {
|
|
|
+ keys = append(keys, k)
|
|
|
+ }
|
|
|
+ sort.Strings(keys)
|
|
|
+
|
|
|
+ var sb strings.Builder
|
|
|
+ for _, k := range keys {
|
|
|
+ sb.WriteString(k)
|
|
|
+ sb.WriteString("=")
|
|
|
+ sb.WriteString(params[k])
|
|
|
+ sb.WriteString("&")
|
|
|
+ }
|
|
|
+ sb.WriteString("appSecret=")
|
|
|
+ sb.WriteString(secret)
|
|
|
+
|
|
|
+ sum := md5.Sum([]byte(sb.String()))
|
|
|
+ return hex.EncodeToString(sum[:])
|
|
|
+}
|
|
|
+
|
|
|
+func (r *XiaoQiReporter) postJSON(ctx context.Context, reqBody interface{}, out interface{}) error {
|
|
|
+ client := r.HTTPClient
|
|
|
+ if client == nil {
|
|
|
+ client = &http.Client{Timeout: 5 * time.Second}
|
|
|
+ }
|
|
|
+
|
|
|
+ payload, err := json.Marshal(reqBody)
|
|
|
+ if err != nil {
|
|
|
+ return fmt.Errorf("marshal request failed: %w", err)
|
|
|
+ }
|
|
|
+
|
|
|
+ req, err := http.NewRequestWithContext(ctx, http.MethodPost, r.Endpoint, bytes.NewReader(payload))
|
|
|
+ if err != nil {
|
|
|
+ return fmt.Errorf("build request failed: %w", err)
|
|
|
+ }
|
|
|
+ req.Header.Set("Content-Type", "application/json")
|
|
|
+
|
|
|
+ resp, err := client.Do(req)
|
|
|
+ if err != nil {
|
|
|
+ return fmt.Errorf("http post failed: %w", err)
|
|
|
+ }
|
|
|
+ defer resp.Body.Close()
|
|
|
+
|
|
|
+ body, _ := io.ReadAll(resp.Body)
|
|
|
+ if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
|
|
+ return fmt.Errorf("http status=%d body=%s", resp.StatusCode, string(body))
|
|
|
+ }
|
|
|
+ if err := json.Unmarshal(body, out); err != nil {
|
|
|
+ return fmt.Errorf("unmarshal response failed: %w body=%s", err, string(body))
|
|
|
+ }
|
|
|
+ return nil
|
|
|
+}
|