feat: 更新项目代码
This commit is contained in:
@@ -2,6 +2,7 @@ package handler
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"strings"
|
||||
@@ -51,8 +52,14 @@ func validateTextLen(text string, label string) (string, error) {
|
||||
return t, nil
|
||||
}
|
||||
|
||||
// POST /api/aimodelapp/chat
|
||||
func (h *AIModelHandler) Chat(c *gin.Context) {
|
||||
type aiModelChatInput struct {
|
||||
Messages []service.ChatMessage
|
||||
Provider string
|
||||
Model string
|
||||
}
|
||||
|
||||
// bindAIModelChat 绑定并校验统一 chat / chat/stream 请求体;失败时已写入 JSON 响应。
|
||||
func bindAIModelChat(c *gin.Context) (aiModelChatInput, bool) {
|
||||
var req struct {
|
||||
Messages []service.ChatMessage `json:"messages"`
|
||||
Provider string `json:"provider"`
|
||||
@@ -60,7 +67,7 @@ func (h *AIModelHandler) Chat(c *gin.Context) {
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "请求数据为空"})
|
||||
return
|
||||
return aiModelChatInput{}, false
|
||||
}
|
||||
if req.Provider == "" {
|
||||
req.Provider = "deepseek"
|
||||
@@ -68,44 +75,99 @@ func (h *AIModelHandler) Chat(c *gin.Context) {
|
||||
if req.Model == "" {
|
||||
req.Model = "deepseek-chat"
|
||||
}
|
||||
|
||||
// 模型白名单校验
|
||||
if models, ok := allowedModels[req.Provider]; !ok || !models[req.Model] {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "不支持的模型"})
|
||||
return
|
||||
return aiModelChatInput{}, false
|
||||
}
|
||||
|
||||
if len(req.Messages) == 0 {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "消息内容不能为空"})
|
||||
return
|
||||
return aiModelChatInput{}, false
|
||||
}
|
||||
if len(req.Messages) > maxChatMsgCount {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("消息数量不能超过 %d 条", maxChatMsgCount)})
|
||||
return
|
||||
return aiModelChatInput{}, false
|
||||
}
|
||||
// 校验每条消息的长度
|
||||
for _, m := range req.Messages {
|
||||
if len(m.Content) > maxInputLen {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("单条消息长度不能超过 %d 字符", maxInputLen)})
|
||||
return
|
||||
return aiModelChatInput{}, false
|
||||
}
|
||||
}
|
||||
return aiModelChatInput{Messages: req.Messages, Provider: req.Provider, Model: req.Model}, true
|
||||
}
|
||||
|
||||
content, err := service.CallAI(req.Provider, req.Model, req.Messages)
|
||||
// POST /api/aimodelapp/chat
|
||||
func (h *AIModelHandler) Chat(c *gin.Context) {
|
||||
in, ok := bindAIModelChat(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
content, err := service.CallAI(in.Provider, in.Model, in.Messages)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": safeAIError(err)})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
"content": content,
|
||||
"provider": req.Provider,
|
||||
"model": req.Model,
|
||||
"provider": in.Provider,
|
||||
"model": in.Model,
|
||||
"timestamp": time.Now().Format(time.RFC3339),
|
||||
})
|
||||
}
|
||||
|
||||
// POST /api/aimodelapp/chat/stream — SSE 透传上游 OpenAI 兼容流(data: {...}\\n\\n / [DONE])
|
||||
func (h *AIModelHandler) ChatStream(c *gin.Context) {
|
||||
in, ok := bindAIModelChat(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
upstream, statusCode, err := service.OpenAIChatStream(c.Request.Context(), in.Provider, in.Model, in.Messages)
|
||||
if err != nil {
|
||||
if statusCode == 0 {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": safeAIError(err)})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusBadGateway, gin.H{"error": safeAIError(err)})
|
||||
return
|
||||
}
|
||||
defer upstream.Close()
|
||||
|
||||
hdr := c.Writer.Header()
|
||||
hdr.Set("Content-Type", "text/event-stream; charset=utf-8")
|
||||
hdr.Set("Cache-Control", "no-cache")
|
||||
hdr.Set("Connection", "keep-alive")
|
||||
hdr.Set("X-Accel-Buffering", "no")
|
||||
c.Status(http.StatusOK)
|
||||
|
||||
flusher, ok := c.Writer.(http.Flusher)
|
||||
if !ok {
|
||||
_, _ = io.Copy(c.Writer, upstream)
|
||||
return
|
||||
}
|
||||
buf := make([]byte, 8192)
|
||||
for {
|
||||
select {
|
||||
case <-c.Request.Context().Done():
|
||||
return
|
||||
default:
|
||||
}
|
||||
n, rerr := upstream.Read(buf)
|
||||
if n > 0 {
|
||||
if _, werr := c.Writer.Write(buf[:n]); werr != nil {
|
||||
return
|
||||
}
|
||||
flusher.Flush()
|
||||
}
|
||||
if rerr == io.EOF {
|
||||
return
|
||||
}
|
||||
if rerr != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// POST /api/aimodelapp/name-analysis
|
||||
func (h *AIModelHandler) NameAnalysis(c *gin.Context) {
|
||||
var req struct {
|
||||
|
||||
95
infogenie-backend-go/internal/handler/feature_card_clicks.go
Normal file
95
infogenie-backend-go/internal/handler/feature_card_clicks.go
Normal file
@@ -0,0 +1,95 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"infogenie-backend/internal/database"
|
||||
"infogenie-backend/internal/model"
|
||||
)
|
||||
|
||||
var featureCardSections = map[string]struct{}{
|
||||
"60sapi": {},
|
||||
"smallgame": {},
|
||||
"toolbox": {},
|
||||
"aimodel": {},
|
||||
}
|
||||
|
||||
func normalizeFeatureSection(s string) (string, bool) {
|
||||
key := strings.ToLower(strings.TrimSpace(s))
|
||||
if key == "" {
|
||||
return "", false
|
||||
}
|
||||
if _, ok := featureCardSections[key]; !ok {
|
||||
return "", false
|
||||
}
|
||||
return key, true
|
||||
}
|
||||
|
||||
func sanitizeFeatureItemID(raw string) (string, bool) {
|
||||
id := strings.TrimSpace(raw)
|
||||
if id == "" || len(id) > 128 {
|
||||
return "", false
|
||||
}
|
||||
return id, true
|
||||
}
|
||||
|
||||
// GetFeatureCardClicks 公开:按板块返回各功能 id 的点击次数(未出现过的 id 前端视为 0)
|
||||
func (h *SiteConfigHandler) GetFeatureCardClicks(c *gin.Context) {
|
||||
section, ok := normalizeFeatureSection(c.Query("section"))
|
||||
if !ok {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid_section"})
|
||||
return
|
||||
}
|
||||
var rows []model.SiteFeatureCardClick
|
||||
if err := database.DB.Where("section = ?", section).Find(&rows).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "db_error"})
|
||||
return
|
||||
}
|
||||
counts := make(map[string]uint64, len(rows))
|
||||
for _, r := range rows {
|
||||
counts[r.ItemID] = r.ClickCount
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"section": section, "counts": counts})
|
||||
}
|
||||
|
||||
type postFeatureCardClickBody struct {
|
||||
Section string `json:"section"`
|
||||
ItemID string `json:"item_id"`
|
||||
}
|
||||
|
||||
// PostFeatureCardClickIncrement 公开:记录一次卡片点击并返回最新计数
|
||||
func (h *SiteConfigHandler) PostFeatureCardClickIncrement(c *gin.Context) {
|
||||
var body postFeatureCardClickBody
|
||||
if err := c.ShouldBindJSON(&body); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid_json"})
|
||||
return
|
||||
}
|
||||
section, ok := normalizeFeatureSection(body.Section)
|
||||
if !ok {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid_section"})
|
||||
return
|
||||
}
|
||||
itemID, ok := sanitizeFeatureItemID(body.ItemID)
|
||||
if !ok {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid_item_id"})
|
||||
return
|
||||
}
|
||||
|
||||
const q = `INSERT INTO site_feature_card_clicks (section, item_id, click_count, updated_at)
|
||||
VALUES (?, ?, 1, NOW())
|
||||
ON DUPLICATE KEY UPDATE click_count = click_count + 1, updated_at = NOW()`
|
||||
if err := database.DB.Exec(q, section, itemID).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "db_error"})
|
||||
return
|
||||
}
|
||||
|
||||
var row model.SiteFeatureCardClick
|
||||
if err := database.DB.Where("section = ? AND item_id = ?", section, itemID).First(&row).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "db_error"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"section": section, "item_id": itemID, "count": row.ClickCount})
|
||||
}
|
||||
@@ -131,6 +131,19 @@ func resolve60sUpstream(sourceID string) (id string, info sixtySrcInfo) {
|
||||
return id, info
|
||||
}
|
||||
|
||||
// EffectiveSixtyUpstream 返回当前站点配置的 60s API 根地址(读库;无记录或库不可用则回落默认 self)
|
||||
func EffectiveSixtyUpstream(db *gorm.DB) (sourceID string, base string, label string) {
|
||||
sid := ""
|
||||
if db != nil {
|
||||
var row model.Site60sUpstream
|
||||
if err := db.First(&row, 1).Error; err == nil {
|
||||
sid = row.SourceID
|
||||
}
|
||||
}
|
||||
id, info := resolve60sUpstream(sid)
|
||||
return id, info.Base, info.Label
|
||||
}
|
||||
|
||||
// Get60sSource 公开:当前站点使用的 60s 上游 base_url(供静态页 iframe 传参)
|
||||
func (h *SiteConfigHandler) Get60sSource(c *gin.Context) {
|
||||
var row model.Site60sUpstream
|
||||
|
||||
Reference in New Issue
Block a user