package handler import ( "crypto/rand" "encoding/base64" "encoding/json" "net/http" "strings" "soul-api/internal/cache" "soul-api/internal/database" "soul-api/internal/model" "github.com/gin-gonic/gin" ) const linkedMpConfigKey = "linked_miniprograms" // LinkedMpItem 关联小程序项,key 为 32 位密钥,链接标签存 key,小程序端用 key 查 appId type LinkedMpItem struct { Key string `json:"key"` Name string `json:"name"` AppID string `json:"appId"` Path string `json:"path,omitempty"` Sort int `json:"sort"` } // AdminLinkedMpList GET /api/admin/linked-miniprograms 管理端-关联小程序列表 func AdminLinkedMpList(c *gin.Context) { db := database.DB() var row model.SystemConfig if err := db.Where("config_key = ?", linkedMpConfigKey).First(&row).Error; err != nil { c.JSON(http.StatusOK, gin.H{"success": true, "data": []LinkedMpItem{}}) return } var list []LinkedMpItem if err := json.Unmarshal(row.ConfigValue, &list); err != nil { c.JSON(http.StatusOK, gin.H{"success": true, "data": []LinkedMpItem{}}) return } if list == nil { list = []LinkedMpItem{} } c.JSON(http.StatusOK, gin.H{"success": true, "data": list}) } // AdminLinkedMpCreate POST /api/admin/linked-miniprograms 管理端-新增关联小程序 func AdminLinkedMpCreate(c *gin.Context) { var body struct { Name string `json:"name" binding:"required"` AppID string `json:"appId" binding:"required"` Path string `json:"path"` Sort int `json:"sort"` } if err := c.ShouldBindJSON(&body); err != nil { c.JSON(http.StatusOK, gin.H{"success": false, "error": "请填写小程序名称和 AppID"}) return } body.Name = trimSpace(body.Name) body.AppID = trimSpace(body.AppID) if body.Name == "" || body.AppID == "" { c.JSON(http.StatusOK, gin.H{"success": false, "error": "小程序名称和 AppID 不能为空"}) return } key, err := genMpKey() if err != nil { c.JSON(http.StatusOK, gin.H{"success": false, "error": "生成密钥失败"}) return } item := LinkedMpItem{Key: key, Name: body.Name, AppID: body.AppID, Path: body.Path, Sort: body.Sort} db := database.DB() var row model.SystemConfig var list []LinkedMpItem if err := db.Where("config_key = ?", linkedMpConfigKey).First(&row).Error; err != nil { list = []LinkedMpItem{} } else { _ = json.Unmarshal(row.ConfigValue, &list) if list == nil { list = []LinkedMpItem{} } } list = append(list, item) valBytes, _ := json.Marshal(list) desc := "关联小程序列表,用于 wx.navigateToMiniProgram 跳转" if row.ConfigKey == "" { row = model.SystemConfig{ConfigKey: linkedMpConfigKey, ConfigValue: valBytes, Description: &desc} if err := db.Create(&row).Error; err != nil { c.JSON(http.StatusOK, gin.H{"success": false, "error": "保存失败: " + err.Error()}) return } } else { row.ConfigValue = valBytes if err := db.Save(&row).Error; err != nil { c.JSON(http.StatusOK, gin.H{"success": false, "error": "保存失败: " + err.Error()}) return } } cache.InvalidateConfig() c.JSON(http.StatusOK, gin.H{"success": true, "data": item}) } // AdminLinkedMpUpdate PUT /api/admin/linked-miniprograms 管理端-编辑关联小程序 func AdminLinkedMpUpdate(c *gin.Context) { var body struct { Key string `json:"key" binding:"required"` Name string `json:"name" binding:"required"` AppID string `json:"appId" binding:"required"` Path string `json:"path"` Sort int `json:"sort"` } if err := c.ShouldBindJSON(&body); err != nil { c.JSON(http.StatusOK, gin.H{"success": false, "error": "参数无效"}) return } body.Name = trimSpace(body.Name) body.AppID = trimSpace(body.AppID) if body.Name == "" || body.AppID == "" { c.JSON(http.StatusOK, gin.H{"success": false, "error": "小程序名称和 AppID 不能为空"}) return } db := database.DB() var row model.SystemConfig if err := db.Where("config_key = ?", linkedMpConfigKey).First(&row).Error; err != nil { c.JSON(http.StatusOK, gin.H{"success": false, "error": "未找到该记录"}) return } var list []LinkedMpItem if err := json.Unmarshal(row.ConfigValue, &list); err != nil || list == nil { c.JSON(http.StatusOK, gin.H{"success": false, "error": "数据格式错误"}) return } found := false for i := range list { if list[i].Key == body.Key { list[i].Name = body.Name list[i].AppID = body.AppID list[i].Path = body.Path list[i].Sort = body.Sort found = true break } } if !found { c.JSON(http.StatusOK, gin.H{"success": false, "error": "未找到该记录"}) return } valBytes, _ := json.Marshal(list) row.ConfigValue = valBytes if err := db.Save(&row).Error; err != nil { c.JSON(http.StatusOK, gin.H{"success": false, "error": "保存失败: " + err.Error()}) return } cache.InvalidateConfig() c.JSON(http.StatusOK, gin.H{"success": true}) } // AdminLinkedMpDelete DELETE /api/admin/linked-miniprograms/:id 管理端-删除(:id 实际传 key) func AdminLinkedMpDelete(c *gin.Context) { key := c.Param("id") if key == "" { c.JSON(http.StatusOK, gin.H{"success": false, "error": "缺少密钥"}) return } db := database.DB() var row model.SystemConfig if err := db.Where("config_key = ?", linkedMpConfigKey).First(&row).Error; err != nil { c.JSON(http.StatusOK, gin.H{"success": false, "error": "未找到该记录"}) return } var list []LinkedMpItem if err := json.Unmarshal(row.ConfigValue, &list); err != nil || list == nil { c.JSON(http.StatusOK, gin.H{"success": false, "error": "数据格式错误"}) return } newList := make([]LinkedMpItem, 0, len(list)) for _, item := range list { if item.Key != key { newList = append(newList, item) } } valBytes, _ := json.Marshal(newList) row.ConfigValue = valBytes if err := db.Save(&row).Error; err != nil { c.JSON(http.StatusOK, gin.H{"success": false, "error": "删除失败: " + err.Error()}) return } cache.InvalidateConfig() c.JSON(http.StatusOK, gin.H{"success": true}) } // genMpKey 生成 32 位英文+数字密钥,供链接标签引用 func genMpKey() (string, error) { b := make([]byte, 24) if _, err := rand.Read(b); err != nil { return "", err } // base64 编码后取 32 位,去掉 +/= 仅保留字母数字 s := base64.URLEncoding.EncodeToString(b) s = strings.ReplaceAll(s, "+", "") s = strings.ReplaceAll(s, "/", "") s = strings.ReplaceAll(s, "=", "") if len(s) >= 32 { return s[:32], nil } return s + "0123456789abcdefghijklmnopqrstuv"[:(32-len(s))], nil }