feat: 人物编辑弹窗改为CKB计划选择下拉框

- 新增 GET /api/admin/ckb/plans 获取存客宝获客计划列表
- 新增 GET /api/admin/ckb/plan-detail 获取计划详情
- PersonAddEditModal: 密钥字段改为可搜索的计划选择器
  选择计划后自动覆盖 greeting/tips/设备/时间等参数
- 删除"修复 CKB 密钥"按钮

Made-with: Cursor
This commit is contained in:
卡若
2026-03-15 23:12:43 +08:00
parent aca006e1b2
commit fa9903d235
17 changed files with 343 additions and 91 deletions

View File

@@ -306,3 +306,117 @@ func AdminCKBDevices(c *gin.Context) {
})
}
// AdminCKBPlans GET /api/admin/ckb/plans 管理端-存客宝获客计划列表
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.DefaultQuery("page", "1")
limitStr := c.DefaultQuery("limit", "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/plans?" + 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
}
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 {
if m, ok := item.(map[string]interface{}); ok {
plans = append(plans, map[string]interface{}{
"id": m["id"],
"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})
}
// AdminCKBPlanDetail GET /api/admin/ckb/plan-detail?planId=xxx 管理端-存客宝获客计划详情
func AdminCKBPlanDetail(c *gin.Context) {
planIDStr := c.Query("planId")
if planIDStr == "" {
c.JSON(http.StatusOK, gin.H{"success": false, "error": "缺少 planId"})
return
}
planID, _ := strconv.ParseInt(planIDStr, 10, 64)
if planID <= 0 {
c.JSON(http.StatusOK, gin.H{"success": false, "error": "planId 无效"})
return
}
token, err := ckbOpenGetToken()
if err != nil {
c.JSON(http.StatusOK, gin.H{"success": false, "error": err.Error()})
return
}
apiKey, err := ckbOpenGetPlanDetail(token, planID)
if err != nil {
c.JSON(http.StatusOK, gin.H{"success": false, "error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"success": true, "apiKey": apiKey})
}