- Introduced `navigateToMiniProgramAppIdList` in app.json for mini program navigation. - Updated link tag handling in the read page to support mini program keys and app IDs. - Enhanced content parsing to include app ID and mini program key in link tags. - Added linked mini programs management in the admin panel with API endpoints for CRUD operations. - Improved UI for selecting linked mini programs in the content creation page.
213 lines
6.6 KiB
Go
213 lines
6.6 KiB
Go
package handler
|
||
|
||
import (
|
||
"crypto/rand"
|
||
"encoding/base64"
|
||
"encoding/json"
|
||
"net/http"
|
||
"strings"
|
||
|
||
"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"`
|
||
ID string `json:"id,omitempty"` // 兼容旧数据,新数据 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{}
|
||
}
|
||
// 兼容旧数据:无 key 时用 id 作为 key
|
||
for i := range list {
|
||
if list[i].Key == "" && list[i].ID != "" {
|
||
list[i].Key = list[i].ID
|
||
}
|
||
}
|
||
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
|
||
}
|
||
}
|
||
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].Key == "" && list[i].ID == body.Key) {
|
||
list[i].Name = body.Name
|
||
list[i].AppID = body.AppID
|
||
list[i].Path = body.Path
|
||
list[i].Sort = body.Sort
|
||
if list[i].Key == "" {
|
||
list[i].Key = list[i].ID
|
||
}
|
||
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
|
||
}
|
||
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 && (item.Key != "" || item.ID != 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
|
||
}
|
||
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
|
||
}
|