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 }