- 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.
236 lines
6.5 KiB
Go
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
|
|
}
|