Files
Mycontent/soul-api/internal/handler/open_platform_admin.go
Alex-larget d4399a5d7a chore: update project index and meeting notes for March 31, 2026
- Added new entries for meetings discussing the integration of the "超级个体" and "@" lists across various project components.
- Updated the last modified date to March 31, 2026, in multiple project index files.
- Enhanced the read page functionality to include user phone number in navigation to other mini-programs.
- Documented changes in the sync log for assistant-doc-sync regarding the meeting outcomes.
2026-03-31 18:10:02 +08:00

236 lines
6.5 KiB
Go

package handler
import (
"crypto/rand"
"crypto/sha256"
"encoding/hex"
"net/http"
"strconv"
"strings"
"time"
"soul-api/internal/database"
"soul-api/internal/model"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
)
func generateOpenPlatformKeyMaterial() (secret string, prefix string, hashHex string, err error) {
b := make([]byte, 16)
if _, err = rand.Read(b); err != nil {
return "", "", "", err
}
hexPart := hex.EncodeToString(b)
secret = "op_sk_" + hexPart
prefix = "op_sk_" + hexPart[:8]
sum := sha256.Sum256([]byte(secret))
hashHex = hex.EncodeToString(sum[:])
return secret, prefix, hashHex, nil
}
// OpenPlatformVerifySecret 校验明文 secret 是否匹配库中记录(供后续开放路由中间件调用)
func OpenPlatformVerifySecret(db *gorm.DB, secret string) (*model.OpenPlatformApiKey, bool) {
secret = strings.TrimSpace(secret)
if secret == "" {
return nil, false
}
sum := sha256.Sum256([]byte(secret))
hashHex := hex.EncodeToString(sum[:])
var row model.OpenPlatformApiKey
if err := db.Where("key_hash = ? AND revoked_at IS NULL", hashHex).First(&row).Error; err != nil {
return nil, false
}
return &row, true
}
// AdminOpenPlatformKeysList GET /api/admin/open-platform/keys
func AdminOpenPlatformKeysList(c *gin.Context) {
db := database.DB()
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
pageSize, _ := strconv.Atoi(c.DefaultQuery("pageSize", "10"))
if page < 1 {
page = 1
}
if pageSize < 1 {
pageSize = 10
}
if pageSize > 100 {
pageSize = 100
}
search := strings.TrimSpace(c.Query("search"))
q := db.Model(&model.OpenPlatformApiKey{})
if search != "" {
like := "%" + search + "%"
q = q.Where("name LIKE ? OR key_prefix LIKE ?", like, like)
}
var total int64
if err := q.Count(&total).Error; err != nil {
c.JSON(http.StatusOK, gin.H{"success": false, "error": err.Error()})
return
}
var list []model.OpenPlatformApiKey
offset := (page - 1) * pageSize
if err := q.Order("id DESC").Offset(offset).Limit(pageSize).Find(&list).Error; err != nil {
c.JSON(http.StatusOK, gin.H{"success": false, "error": err.Error()})
return
}
records := make([]gin.H, 0, len(list))
for _, row := range list {
records = append(records, gin.H{
"id": row.ID,
"name": row.Name,
"keyPrefix": row.KeyPrefix,
"revokedAt": row.RevokedAt,
"createdAt": row.CreatedAt,
})
}
totalPages := int((total + int64(pageSize) - 1) / int64(pageSize))
c.JSON(http.StatusOK, gin.H{
"success": true,
"records": records,
"total": total,
"page": page,
"pageSize": pageSize,
"totalPages": totalPages,
})
}
// AdminOpenPlatformKeysCreate POST /api/admin/open-platform/keys
func AdminOpenPlatformKeysCreate(c *gin.Context) {
var body struct {
Name string `json:"name"`
}
_ = c.ShouldBindJSON(&body)
name := strings.TrimSpace(body.Name)
if name == "" {
name = "未命名密钥"
}
secret, prefix, hashHex, genErr := generateOpenPlatformKeyMaterial()
if genErr != nil {
c.JSON(http.StatusOK, gin.H{"success": false, "error": "生成密钥失败"})
return
}
row := model.OpenPlatformApiKey{
Name: name,
KeyPrefix: prefix,
KeyHash: hashHex,
}
db := database.DB()
if err := db.Create(&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": gin.H{
"id": row.ID,
"name": row.Name,
"keyPrefix": row.KeyPrefix,
"secret": secret,
"createdAt": row.CreatedAt,
},
})
}
// AdminOpenPlatformKeyRevoke POST /api/admin/open-platform/keys/:id/revoke
func AdminOpenPlatformKeyRevoke(c *gin.Context) {
id64, err := strconv.ParseUint(c.Param("id"), 10, 64)
if err != nil || id64 == 0 {
c.JSON(http.StatusBadRequest, gin.H{"success": false, "error": "无效 id"})
return
}
db := database.DB()
now := time.Now()
res := db.Model(&model.OpenPlatformApiKey{}).Where("id = ? AND revoked_at IS NULL", id64).Update("revoked_at", now)
if res.Error != nil {
c.JSON(http.StatusOK, gin.H{"success": false, "error": res.Error.Error()})
return
}
if res.RowsAffected == 0 {
c.JSON(http.StatusOK, gin.H{"success": false, "error": "密钥不存在或已吊销"})
return
}
c.JSON(http.StatusOK, gin.H{"success": true, "message": "已吊销"})
}
// AdminOpenPlatformLogsList GET /api/admin/open-platform/logs
func AdminOpenPlatformLogsList(c *gin.Context) {
db := database.DB()
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
pageSize, _ := strconv.Atoi(c.DefaultQuery("pageSize", "10"))
if page < 1 {
page = 1
}
if pageSize < 1 {
pageSize = 10
}
if pageSize > 100 {
pageSize = 100
}
search := strings.TrimSpace(c.Query("search"))
q := db.Model(&model.OpenPlatformApiLog{})
if search != "" {
like := "%" + search + "%"
q = q.Where(
"path LIKE ? OR method LIKE ? OR client_ip LIKE ? OR key_prefix LIKE ? OR request_body LIKE ? OR response_body LIKE ?",
like, like, like, like, like, like,
)
}
var total int64
if err := q.Count(&total).Error; err != nil {
c.JSON(http.StatusOK, gin.H{"success": false, "error": err.Error()})
return
}
var list []model.OpenPlatformApiLog
offset := (page - 1) * pageSize
if err := q.Order("id DESC").Offset(offset).Limit(pageSize).Find(&list).Error; err != nil {
c.JSON(http.StatusOK, gin.H{"success": false, "error": err.Error()})
return
}
records := make([]gin.H, 0, len(list))
for _, row := range list {
records = append(records, gin.H{
"id": row.ID,
"apiKeyId": row.ApiKeyID,
"keyPrefix": row.KeyPrefix,
"method": row.Method,
"path": row.Path,
"statusCode": row.StatusCode,
"clientIp": row.ClientIP,
"durationMs": row.DurationMs,
"requestBody": row.RequestBody,
"responseBody": row.ResponseBody,
"createdAt": row.CreatedAt,
})
}
totalPages := int((total + int64(pageSize) - 1) / int64(pageSize))
c.JSON(http.StatusOK, gin.H{
"success": true,
"records": records,
"total": total,
"page": page,
"pageSize": pageSize,
"totalPages": totalPages,
})
}
const openPlatformLogMaxRunes = 8000
// OpenPlatformInsertLog 写入开放平台访问日志(供后续开放网关 / 中间件调用)
func OpenPlatformInsertLog(db *gorm.DB, rec *model.OpenPlatformApiLog) error {
if rec == nil {
return nil
}
trunc := func(s string) string {
runes := []rune(s)
if len(runes) <= openPlatformLogMaxRunes {
return s
}
return string(runes[:openPlatformLogMaxRunes]) + "…(truncated)"
}
rec.RequestBody = trunc(rec.RequestBody)
rec.ResponseBody = trunc(rec.ResponseBody)
return db.Create(rec).Error
}