- Added a new entry for the 存客宝计划 Key in the documentation, ensuring consistency between the displayed planKey and the apiKey used for retries. - Implemented a caching mechanism for the JWT token in the ckbOpenGetToken function to reduce redundant API calls. - Refactored the lead management logic to ensure that the same planKey is used for both displaying leads and retrying failed pushes, enhancing data integrity and reducing discrepancies. This update aims to streamline lead management processes and improve the reliability of API interactions.
670 lines
18 KiB
Go
670 lines
18 KiB
Go
package handler
|
||
|
||
import (
|
||
"bytes"
|
||
"crypto/md5"
|
||
"encoding/hex"
|
||
"encoding/json"
|
||
"fmt"
|
||
"io"
|
||
"net/http"
|
||
"net/url"
|
||
"strconv"
|
||
"strings"
|
||
"sync"
|
||
"time"
|
||
|
||
"github.com/gin-gonic/gin"
|
||
|
||
"soul-api/internal/config"
|
||
)
|
||
|
||
const ckbOpenBaseURL = "https://ckbapi.quwanzhi.com"
|
||
|
||
// ckbOpenSign 按 open-api-sign.md:sign = MD5(MD5(account+timestamp) + apiKey)
|
||
func ckbOpenSign(account string, ts int64, apiKey string) string {
|
||
plain := account + strconv.FormatInt(ts, 10)
|
||
h := md5.Sum([]byte(plain))
|
||
first := hex.EncodeToString(h[:])
|
||
h2 := md5.Sum([]byte(first + apiKey))
|
||
return hex.EncodeToString(h2[:])
|
||
}
|
||
|
||
func getCkbOpenConfig() (apiKey, account string) {
|
||
cfg := config.Get()
|
||
if cfg != nil {
|
||
apiKey = cfg.CkbOpenAPIKey
|
||
account = cfg.CkbOpenAccount
|
||
}
|
||
return
|
||
}
|
||
|
||
var (
|
||
ckbOpenTokenMu sync.Mutex
|
||
ckbOpenCachedToken string
|
||
ckbOpenCachedExpiry time.Time
|
||
)
|
||
|
||
// ckbOpenTokenCacheTTL 开放 API JWT 内存缓存时长(避免 GET /api/db/persons 等高频路径对存客宝重复 /v1/open/auth/token)
|
||
const ckbOpenTokenCacheTTL = 45 * time.Minute
|
||
|
||
// ckbOpenGetToken 获取开放 API JWT(带进程内短期缓存)
|
||
func ckbOpenGetToken() (string, error) {
|
||
apiKey, account := getCkbOpenConfig()
|
||
if apiKey == "" || account == "" {
|
||
return "", fmt.Errorf("无效的apiKey:CKB_OPEN_API_KEY 或 CKB_OPEN_ACCOUNT 未配置,请在后端 .env 中配置后重试")
|
||
}
|
||
|
||
ckbOpenTokenMu.Lock()
|
||
if ckbOpenCachedToken != "" && time.Now().Before(ckbOpenCachedExpiry) {
|
||
tok := ckbOpenCachedToken
|
||
ckbOpenTokenMu.Unlock()
|
||
return tok, nil
|
||
}
|
||
ckbOpenTokenMu.Unlock()
|
||
|
||
ts := time.Now().Unix()
|
||
sign := ckbOpenSign(account, ts, apiKey)
|
||
authBody := map[string]interface{}{
|
||
"apiKey": apiKey,
|
||
"account": account,
|
||
"timestamp": ts,
|
||
"sign": sign,
|
||
}
|
||
raw, _ := json.Marshal(authBody)
|
||
authResp, err := http.Post(ckbOpenBaseURL+"/v1/open/auth/token", "application/json", bytes.NewReader(raw))
|
||
if err != nil {
|
||
return "", fmt.Errorf("请求存客宝鉴权失败: %w", err)
|
||
}
|
||
defer authResp.Body.Close()
|
||
authBytes, _ := io.ReadAll(authResp.Body)
|
||
var authResult struct {
|
||
Code int `json:"code"`
|
||
Message string `json:"message"`
|
||
Data struct {
|
||
Token string `json:"token"`
|
||
} `json:"data"`
|
||
}
|
||
_ = json.Unmarshal(authBytes, &authResult)
|
||
if authResult.Code != 200 || authResult.Data.Token == "" {
|
||
msg := authResult.Message
|
||
if msg == "" {
|
||
msg = "存客宝鉴权失败"
|
||
}
|
||
return "", fmt.Errorf("%s", msg)
|
||
}
|
||
|
||
ckbOpenTokenMu.Lock()
|
||
ckbOpenCachedToken = authResult.Data.Token
|
||
ckbOpenCachedExpiry = time.Now().Add(ckbOpenTokenCacheTTL)
|
||
out := ckbOpenCachedToken
|
||
ckbOpenTokenMu.Unlock()
|
||
return out, nil
|
||
}
|
||
|
||
// ckbOpenCreatePlan 调用 /v1/plan/create 创建获客计划,返回 planId、存客宝原始 data、以及完整响应(失败时便于排查)
|
||
func ckbOpenCreatePlan(token string, payload map[string]interface{}) (planID int64, createData map[string]interface{}, ckbResponse map[string]interface{}, err error) {
|
||
raw, _ := json.Marshal(payload)
|
||
req, err := http.NewRequest(http.MethodPost, ckbOpenBaseURL+"/v1/plan/create", bytes.NewReader(raw))
|
||
if err != nil {
|
||
return 0, nil, nil, fmt.Errorf("构造创建计划请求失败: %w", err)
|
||
}
|
||
req.Header.Set("Authorization", "Bearer "+token)
|
||
req.Header.Set("Content-Type", "application/json")
|
||
|
||
resp, err := http.DefaultClient.Do(req)
|
||
if err != nil {
|
||
return 0, nil, nil, fmt.Errorf("请求存客宝创建计划失败: %w", err)
|
||
}
|
||
defer resp.Body.Close()
|
||
b, _ := io.ReadAll(resp.Body)
|
||
var result struct {
|
||
Code int `json:"code"`
|
||
Message string `json:"message"`
|
||
Data json.RawMessage `json:"data"`
|
||
}
|
||
_ = json.Unmarshal(b, &result)
|
||
// 始终组装完整响应,便于失败时返回给调用方查看存客宝实际返回
|
||
ckbResponse = map[string]interface{}{
|
||
"code": result.Code,
|
||
"message": result.Message,
|
||
"data": nil,
|
||
}
|
||
if len(result.Data) > 0 {
|
||
var dataObj interface{}
|
||
_ = json.Unmarshal(result.Data, &dataObj)
|
||
ckbResponse["data"] = dataObj
|
||
}
|
||
if result.Code != 200 {
|
||
if result.Message == "" {
|
||
result.Message = "创建计划失败"
|
||
}
|
||
return 0, nil, ckbResponse, fmt.Errorf("%s", result.Message)
|
||
}
|
||
// 原始 data 转为 map 供响应展示
|
||
createData = make(map[string]interface{})
|
||
_ = json.Unmarshal(result.Data, &createData)
|
||
// 存客宝可能返回 planId 为数字或字符串(如 "629"),兼容解析
|
||
planID = parsePlanIDFromData(createData)
|
||
if planID != 0 {
|
||
return planID, createData, ckbResponse, nil
|
||
}
|
||
return 0, createData, ckbResponse, fmt.Errorf("创建计划返回结果中缺少 planId")
|
||
}
|
||
|
||
// ckbOpenUpdatePlan 调用 PUT /v1/plan/update 更新获客计划(用于停用/启用)
|
||
// payload 至少包含 planId
|
||
func ckbOpenUpdatePlan(token string, payload map[string]interface{}) (ckbResponse map[string]interface{}, err error) {
|
||
raw, _ := json.Marshal(payload)
|
||
req, err := http.NewRequest(http.MethodPut, ckbOpenBaseURL+"/v1/plan/update", bytes.NewReader(raw))
|
||
if err != nil {
|
||
return nil, fmt.Errorf("构造更新计划请求失败: %w", err)
|
||
}
|
||
req.Header.Set("Authorization", "Bearer "+token)
|
||
req.Header.Set("Content-Type", "application/json")
|
||
|
||
resp, err := http.DefaultClient.Do(req)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("请求存客宝更新计划失败: %w", err)
|
||
}
|
||
defer resp.Body.Close()
|
||
b, _ := io.ReadAll(resp.Body)
|
||
var result struct {
|
||
Code int `json:"code"`
|
||
Message string `json:"message"`
|
||
Data json.RawMessage `json:"data"`
|
||
}
|
||
_ = json.Unmarshal(b, &result)
|
||
ckbResponse = map[string]interface{}{
|
||
"code": result.Code,
|
||
"message": result.Message,
|
||
"data": nil,
|
||
}
|
||
if len(result.Data) > 0 {
|
||
var dataObj interface{}
|
||
_ = json.Unmarshal(result.Data, &dataObj)
|
||
ckbResponse["data"] = dataObj
|
||
}
|
||
if result.Code != 200 {
|
||
msg := result.Message
|
||
if msg == "" {
|
||
msg = "更新计划失败"
|
||
}
|
||
return ckbResponse, fmt.Errorf("%s", msg)
|
||
}
|
||
return ckbResponse, nil
|
||
}
|
||
|
||
// parseApiKeyFromCreateData 从 create 返回的 data 中解析 apiKey(若存客宝直接返回则复用,避免二次请求)
|
||
func parseApiKeyFromCreateData(data map[string]interface{}) string {
|
||
for _, key := range []string{"apiKey", "api_key"} {
|
||
v, ok := data[key]
|
||
if !ok || v == nil {
|
||
continue
|
||
}
|
||
if s, ok := v.(string); ok && strings.TrimSpace(s) != "" {
|
||
return strings.TrimSpace(s)
|
||
}
|
||
}
|
||
return ""
|
||
}
|
||
|
||
// parsePlanIDFromData 从 data 中解析 planId,支持 number 或 string;若无则尝试 id
|
||
func parsePlanIDFromData(data map[string]interface{}) int64 {
|
||
for _, key := range []string{"planId", "id"} {
|
||
v, ok := data[key]
|
||
if !ok || v == nil {
|
||
continue
|
||
}
|
||
switch val := v.(type) {
|
||
case float64:
|
||
if val > 0 {
|
||
return int64(val)
|
||
}
|
||
case int:
|
||
if val > 0 {
|
||
return int64(val)
|
||
}
|
||
case int64:
|
||
if val > 0 {
|
||
return val
|
||
}
|
||
case string:
|
||
if val == "" {
|
||
continue
|
||
}
|
||
n, err := strconv.ParseInt(val, 10, 64)
|
||
if err == nil && n > 0 {
|
||
return n
|
||
}
|
||
}
|
||
}
|
||
return 0
|
||
}
|
||
|
||
// ckbOpenGetPlanDetail 调用 /v1/plan/detail?planId=,返回计划级 apiKey
|
||
func ckbOpenGetPlanDetail(token string, planID int64) (string, error) {
|
||
u := fmt.Sprintf("%s/v1/plan/detail?planId=%d", ckbOpenBaseURL, planID)
|
||
req, err := http.NewRequest(http.MethodGet, u, nil)
|
||
if err != nil {
|
||
return "", fmt.Errorf("构造计划详情请求失败: %w", err)
|
||
}
|
||
req.Header.Set("Authorization", "Bearer "+token)
|
||
resp, err := http.DefaultClient.Do(req)
|
||
if err != nil {
|
||
return "", fmt.Errorf("请求存客宝计划详情失败: %w", err)
|
||
}
|
||
defer resp.Body.Close()
|
||
b, _ := io.ReadAll(resp.Body)
|
||
var result struct {
|
||
Code int `json:"code"`
|
||
Message string `json:"message"`
|
||
Data struct {
|
||
APIKey string `json:"apiKey"`
|
||
} `json:"data"`
|
||
}
|
||
_ = json.Unmarshal(b, &result)
|
||
if result.Code != 200 {
|
||
if result.Message == "" {
|
||
result.Message = "获取计划详情失败"
|
||
}
|
||
return "", fmt.Errorf("%s", result.Message)
|
||
}
|
||
if result.Data.APIKey == "" {
|
||
return "", fmt.Errorf("计划详情中缺少 apiKey")
|
||
}
|
||
return result.Data.APIKey, nil
|
||
}
|
||
|
||
// ckbOpenProbePlanReachable 仅用于连通性探测:GET /v1/plan/detail 返回 code==200 即认为计划可访问。
|
||
// 与 ckbOpenGetPlanDetail 不同:不少计划详情接口不返回 apiKey(或字段名不同),不应因此判失败,更不应降级去调 update 触发「更新计划失败」误报。
|
||
func ckbOpenProbePlanReachable(token string, planID int64) error {
|
||
u := fmt.Sprintf("%s/v1/plan/detail?planId=%d", ckbOpenBaseURL, planID)
|
||
req, err := http.NewRequest(http.MethodGet, u, nil)
|
||
if err != nil {
|
||
return fmt.Errorf("构造计划详情请求失败: %w", err)
|
||
}
|
||
req.Header.Set("Authorization", "Bearer "+token)
|
||
resp, err := http.DefaultClient.Do(req)
|
||
if err != nil {
|
||
return fmt.Errorf("请求存客宝计划详情失败: %w", err)
|
||
}
|
||
defer resp.Body.Close()
|
||
b, _ := io.ReadAll(resp.Body)
|
||
var result struct {
|
||
Code int `json:"code"`
|
||
Message string `json:"message"`
|
||
Data json.RawMessage `json:"data"`
|
||
}
|
||
_ = json.Unmarshal(b, &result)
|
||
if result.Code != 200 {
|
||
msg := result.Message
|
||
if msg == "" {
|
||
msg = "获取计划详情失败"
|
||
}
|
||
return fmt.Errorf("%s", msg)
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// ckbOpenGetDefaultDeviceID 获取默认设备 ID:拉设备列表,取第一个 memo 或 nickname 包含 "soul" 的设备;用于 deviceGroups 必填时的默认值
|
||
func ckbOpenGetDefaultDeviceID(token string) (int64, error) {
|
||
u := ckbOpenBaseURL + "/v1/devices?keyword=soul&page=1&limit=50"
|
||
req, err := http.NewRequest(http.MethodGet, u, nil)
|
||
if err != nil {
|
||
return 0, fmt.Errorf("构造设备列表请求失败: %w", err)
|
||
}
|
||
req.Header.Set("Authorization", "Bearer "+token)
|
||
resp, err := http.DefaultClient.Do(req)
|
||
if err != nil {
|
||
return 0, fmt.Errorf("请求存客宝设备列表失败: %w", err)
|
||
}
|
||
defer resp.Body.Close()
|
||
b, _ := io.ReadAll(resp.Body)
|
||
var parsed map[string]interface{}
|
||
if err := json.Unmarshal(b, &parsed); err != nil {
|
||
return 0, fmt.Errorf("解析设备列表失败: %w", err)
|
||
}
|
||
var listAny interface{}
|
||
if dataVal, ok := parsed["data"].(map[string]interface{}); ok {
|
||
listAny = dataVal["list"]
|
||
} else if la, ok := parsed["list"]; ok {
|
||
listAny = la
|
||
}
|
||
arr, ok := listAny.([]interface{})
|
||
if !ok {
|
||
return 0, fmt.Errorf("设备列表格式异常")
|
||
}
|
||
// 优先匹配 memo/nickname 包含 soul 的设备;若无则取第一个(keyword 可能已过滤)
|
||
for _, item := range arr {
|
||
m, ok := item.(map[string]interface{})
|
||
if !ok {
|
||
continue
|
||
}
|
||
memo := toString(m["memo"])
|
||
if memo == "" {
|
||
memo = toString(m["imei"])
|
||
}
|
||
nickname := toString(m["nickname"])
|
||
lowerMemo := strings.ToLower(memo)
|
||
lowerNick := strings.ToLower(nickname)
|
||
if strings.Contains(lowerMemo, "soul") || strings.Contains(lowerNick, "soul") {
|
||
id := parseDeviceID(m["id"])
|
||
if id > 0 {
|
||
return id, nil
|
||
}
|
||
}
|
||
}
|
||
// 未找到含 soul 的,取第一个
|
||
if len(arr) > 0 {
|
||
if m, ok := arr[0].(map[string]interface{}); ok {
|
||
id := parseDeviceID(m["id"])
|
||
if id > 0 {
|
||
return id, nil
|
||
}
|
||
}
|
||
}
|
||
return 0, fmt.Errorf("未找到名为 soul 的设备,请先在存客宝添加设备并设置 memo 或 nickname 包含 soul")
|
||
}
|
||
|
||
func toString(v interface{}) string {
|
||
if v == nil {
|
||
return ""
|
||
}
|
||
switch val := v.(type) {
|
||
case string:
|
||
return val
|
||
case float64:
|
||
return strconv.FormatFloat(val, 'f', -1, 64)
|
||
case int:
|
||
return strconv.Itoa(val)
|
||
case int64:
|
||
return strconv.FormatInt(val, 10)
|
||
default:
|
||
return fmt.Sprint(v)
|
||
}
|
||
}
|
||
|
||
func parseDeviceID(v interface{}) int64 {
|
||
if v == nil {
|
||
return 0
|
||
}
|
||
switch val := v.(type) {
|
||
case float64:
|
||
if val > 0 {
|
||
return int64(val)
|
||
}
|
||
case int:
|
||
if val > 0 {
|
||
return int64(val)
|
||
}
|
||
case int64:
|
||
if val > 0 {
|
||
return val
|
||
}
|
||
case string:
|
||
if val == "" {
|
||
return 0
|
||
}
|
||
n, err := strconv.ParseInt(val, 10, 64)
|
||
if err == nil && n > 0 {
|
||
return n
|
||
}
|
||
}
|
||
return 0
|
||
}
|
||
|
||
// ckbOpenDeletePlan 调用 DELETE /v1/plan/delete 删除存客宝获客计划
|
||
func ckbOpenDeletePlan(token string, planID int64) error {
|
||
payload := map[string]interface{}{"planId": planID}
|
||
raw, _ := json.Marshal(payload)
|
||
req, err := http.NewRequest(http.MethodDelete, ckbOpenBaseURL+"/v1/plan/delete", bytes.NewReader(raw))
|
||
if err != nil {
|
||
return fmt.Errorf("构造删除计划请求失败: %w", err)
|
||
}
|
||
req.Header.Set("Authorization", "Bearer "+token)
|
||
req.Header.Set("Content-Type", "application/json")
|
||
|
||
resp, err := http.DefaultClient.Do(req)
|
||
if err != nil {
|
||
return fmt.Errorf("请求存客宝删除计划失败: %w", err)
|
||
}
|
||
defer resp.Body.Close()
|
||
b, _ := io.ReadAll(resp.Body)
|
||
var result struct {
|
||
Code int `json:"code"`
|
||
Message string `json:"message"`
|
||
}
|
||
_ = json.Unmarshal(b, &result)
|
||
if result.Code != 200 {
|
||
if result.Message == "" {
|
||
result.Message = "删除计划失败"
|
||
}
|
||
return fmt.Errorf("%s", result.Message)
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// AdminCKBDevices GET /api/admin/ckb/devices 管理端-存客宝设备列表(供链接人与事选择设备)
|
||
// 通过开放 API 获取 JWT,再调用 /v1/devices,返回精简后的设备列表。
|
||
func AdminCKBDevices(c *gin.Context) {
|
||
token, err := ckbOpenGetToken()
|
||
if err != nil {
|
||
c.JSON(http.StatusOK, gin.H{"success": false, "error": err.Error()})
|
||
return
|
||
}
|
||
|
||
// 2. 调用 /v1/devices 获取设备列表
|
||
pageStr := c.Query("page")
|
||
if pageStr == "" {
|
||
pageStr = "1"
|
||
}
|
||
limitStr := c.Query("limit")
|
||
if limitStr == "" {
|
||
limitStr = "20"
|
||
}
|
||
keyword := c.Query("keyword")
|
||
|
||
values := url.Values{}
|
||
values.Set("page", pageStr)
|
||
values.Set("limit", limitStr)
|
||
if keyword != "" {
|
||
values.Set("keyword", keyword)
|
||
}
|
||
deviceURL := ckbOpenBaseURL + "/v1/devices"
|
||
if len(values) > 0 {
|
||
deviceURL += "?" + values.Encode()
|
||
}
|
||
|
||
req, err := http.NewRequest(http.MethodGet, deviceURL, nil)
|
||
if err != nil {
|
||
c.JSON(http.StatusOK, gin.H{"success": false, "error": "构造设备列表请求失败"})
|
||
return
|
||
}
|
||
req.Header.Set("Authorization", "Bearer "+token)
|
||
resp, err := http.DefaultClient.Do(req)
|
||
if err != nil {
|
||
c.JSON(http.StatusOK, gin.H{"success": false, "error": "请求存客宝设备列表失败"})
|
||
return
|
||
}
|
||
defer resp.Body.Close()
|
||
b, _ := io.ReadAll(resp.Body)
|
||
|
||
// 设备返回结构:参考 Cunkebao getDeviceList 使用方式,形如 { code, msg, data: { list, total } } 或 { code, msg, list, total }
|
||
var parsed map[string]interface{}
|
||
if err := json.Unmarshal(b, &parsed); err != nil {
|
||
c.JSON(http.StatusOK, gin.H{"success": false, "error": "解析存客宝设备列表失败"})
|
||
return
|
||
}
|
||
|
||
// 尝试从 data.list 或 list 提取设备列表与 total
|
||
var listAny interface{}
|
||
if dataVal, ok := parsed["data"].(map[string]interface{}); ok {
|
||
listAny = dataVal["list"]
|
||
if _, ok := parsed["total"]; !ok {
|
||
if tv, ok := dataVal["total"]; ok {
|
||
parsed["total"] = tv
|
||
}
|
||
}
|
||
} else if la, ok := parsed["list"]; ok {
|
||
listAny = la
|
||
}
|
||
|
||
devices := make([]map[string]interface{}, 0)
|
||
if arr, ok := listAny.([]interface{}); ok {
|
||
for _, item := range arr {
|
||
if m, ok := item.(map[string]interface{}); ok {
|
||
id := m["id"]
|
||
memo := m["memo"]
|
||
if memo == nil || memo == "" {
|
||
memo = m["imei"]
|
||
}
|
||
wechatID := m["wechatId"]
|
||
status := "offline"
|
||
if alive, ok := m["alive"].(float64); ok && int(alive) == 1 {
|
||
status = "online"
|
||
}
|
||
devices = append(devices, map[string]interface{}{
|
||
"id": id,
|
||
"memo": memo,
|
||
"imei": m["imei"],
|
||
"wechatId": wechatID,
|
||
"status": status,
|
||
"avatar": m["avatar"],
|
||
"nickname": m["nickname"],
|
||
"usedInPlan": m["usedInPlans"],
|
||
"totalFriend": m["totalFriend"],
|
||
})
|
||
}
|
||
}
|
||
}
|
||
|
||
total := 0
|
||
switch tv := parsed["total"].(type) {
|
||
case float64:
|
||
total = int(tv)
|
||
case int:
|
||
total = tv
|
||
case string:
|
||
if n, err := strconv.Atoi(tv); err == nil {
|
||
total = n
|
||
}
|
||
}
|
||
|
||
c.JSON(http.StatusOK, gin.H{
|
||
"success": true,
|
||
"devices": devices,
|
||
"total": total,
|
||
})
|
||
}
|
||
|
||
// AdminCKBPlans GET /api/admin/ckb/plans 管理端-存客宝获客计划列表(供链接人与事选择计划一键覆盖参数)
|
||
// 通过开放 API 获取 JWT,再调用 /v1/plan/list,返回精简后的计划列表。
|
||
func AdminCKBPlans(c *gin.Context) {
|
||
token, err := ckbOpenGetToken()
|
||
if err != nil {
|
||
c.JSON(http.StatusOK, gin.H{"success": false, "error": err.Error()})
|
||
return
|
||
}
|
||
|
||
pageStr := c.Query("page")
|
||
if pageStr == "" {
|
||
pageStr = "1"
|
||
}
|
||
limitStr := c.Query("limit")
|
||
if limitStr == "" {
|
||
limitStr = "50"
|
||
}
|
||
keyword := c.Query("keyword")
|
||
|
||
values := url.Values{}
|
||
values.Set("page", pageStr)
|
||
values.Set("limit", limitStr)
|
||
if keyword != "" {
|
||
values.Set("keyword", keyword)
|
||
}
|
||
|
||
planURL := ckbOpenBaseURL + "/v1/plan/list"
|
||
if len(values) > 0 {
|
||
planURL += "?" + values.Encode()
|
||
}
|
||
|
||
req, err := http.NewRequest(http.MethodGet, planURL, nil)
|
||
if err != nil {
|
||
c.JSON(http.StatusOK, gin.H{"success": false, "error": "构造计划列表请求失败"})
|
||
return
|
||
}
|
||
req.Header.Set("Authorization", "Bearer "+token)
|
||
|
||
resp, err := http.DefaultClient.Do(req)
|
||
if err != nil {
|
||
c.JSON(http.StatusOK, gin.H{"success": false, "error": "请求存客宝计划列表失败"})
|
||
return
|
||
}
|
||
defer resp.Body.Close()
|
||
b, _ := io.ReadAll(resp.Body)
|
||
|
||
var parsed map[string]interface{}
|
||
if err := json.Unmarshal(b, &parsed); err != nil {
|
||
c.JSON(http.StatusOK, gin.H{"success": false, "error": "解析存客宝计划列表失败"})
|
||
return
|
||
}
|
||
|
||
// 计划返回结构:参考设备列表解析方式,尽可能兼容 {data:{list,total}} 或 {list,total}
|
||
var listAny interface{}
|
||
if dataVal, ok := parsed["data"].(map[string]interface{}); ok {
|
||
listAny = dataVal["list"]
|
||
if _, ok := parsed["total"]; !ok {
|
||
if tv, ok := dataVal["total"]; ok {
|
||
parsed["total"] = tv
|
||
}
|
||
}
|
||
} else if la, ok := parsed["list"]; ok {
|
||
listAny = la
|
||
}
|
||
|
||
plans := make([]map[string]interface{}, 0)
|
||
if arr, ok := listAny.([]interface{}); ok {
|
||
for _, item := range arr {
|
||
m, ok := item.(map[string]interface{})
|
||
if !ok {
|
||
continue
|
||
}
|
||
plans = append(plans, map[string]interface{}{
|
||
"id": m["planId"],
|
||
"name": m["name"],
|
||
"apiKey": m["apiKey"],
|
||
"sceneId": m["sceneId"],
|
||
"scenario": m["scenario"],
|
||
"enabled": m["enabled"],
|
||
"greeting": m["greeting"],
|
||
"tips": m["tips"],
|
||
"remarkType": m["remarkType"],
|
||
"remarkFormat": m["remarkFormat"],
|
||
"addInterval": m["addInterval"],
|
||
"startTime": m["startTime"],
|
||
"endTime": m["endTime"],
|
||
"deviceGroups": m["deviceGroups"],
|
||
})
|
||
}
|
||
}
|
||
|
||
total := 0
|
||
switch tv := parsed["total"].(type) {
|
||
case float64:
|
||
total = int(tv)
|
||
case int:
|
||
total = tv
|
||
case string:
|
||
if n, err := strconv.Atoi(tv); err == nil {
|
||
total = n
|
||
}
|
||
}
|
||
|
||
c.JSON(http.StatusOK, gin.H{
|
||
"success": true,
|
||
"plans": plans,
|
||
"total": total,
|
||
})
|
||
}
|