feat(logs): 日志页搜索合并为单个搜索框
后端新增 /logs?kw=:命中提示词/模型/错误,或命中用户 (名称/邮箱/ID) 的记录均保留;前端两个搜索框合并为一个。 首页导航恢复 ✦ 品牌小星标,与其他页一致。 Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
@@ -7,6 +7,7 @@ import (
|
||||
"time"
|
||||
|
||||
"backend/internal/model"
|
||||
"backend/internal/repo"
|
||||
"backend/internal/service"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
@@ -99,7 +100,21 @@ func (h *AdminReadHandler) Logs(c *gin.Context) {
|
||||
userIDs = ids
|
||||
}
|
||||
|
||||
items, total, stats, err := h.admin.Logs(c.Request.Context(), limit, offset, kind, status, nil, since, "", userIDs, strings.TrimSpace(c.Query("q")), "", c.Query("source"), false, false, false)
|
||||
// ?kw= — 单搜索框:命中 提示词/模型/错误,或命中 用户(名称/邮箱/ID) 的记录都保留。
|
||||
var items []model.EventLog
|
||||
var total int64
|
||||
var stats *repo.EventStats
|
||||
var err error
|
||||
if kw := strings.TrimSpace(c.Query("kw")); kw != "" {
|
||||
kwIDs, uerr := h.admin.MatchUserIDs(c.Request.Context(), kw)
|
||||
if uerr != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"detail": "failed to load logs"})
|
||||
return
|
||||
}
|
||||
items, total, stats, err = h.admin.LogsKeyword(c.Request.Context(), limit, offset, kind, status, since, kw, kwIDs, c.Query("source"))
|
||||
} else {
|
||||
items, total, stats, err = h.admin.Logs(c.Request.Context(), limit, offset, kind, status, nil, since, "", userIDs, strings.TrimSpace(c.Query("q")), "", c.Query("source"), false, false, false)
|
||||
}
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"detail": "failed to load logs"})
|
||||
return
|
||||
|
||||
@@ -15,20 +15,21 @@ type EventRepository struct {
|
||||
}
|
||||
|
||||
type EventListFilter struct {
|
||||
Limit int
|
||||
Offset int
|
||||
Kind string
|
||||
Status string // single status (status = ?)
|
||||
Statuses []string // multiple statuses (status IN (?)) — used by the 画图台 grid
|
||||
Since *time.Time
|
||||
UserID string
|
||||
UserIDs []string // when set, keep ONLY rows whose user_id is in this list (admin 用户搜索)
|
||||
Query string // free-text search over prompt / model / error (server-side, 跨页)
|
||||
ExcludeSource string // when set, omit rows with this source (e.g. hide API-key "v1" usage from the customer logs page)
|
||||
Source string // when set, keep ONLY rows with this source (admin 来源 filter): "v1" (API key) / "user" (前台) / "admin" (测试模型)
|
||||
HasFile bool // when true, keep ONLY rows with a non-empty file (the 创作记录 gallery — paginates over real media)
|
||||
ExcludeFiles []string // when set, omit rows whose file is in this list (e.g. hide homepage showcase media from user galleries)
|
||||
MediaOnly bool // when true, keep only rows that are pending or have a stored file — the 画图台 grid, so deleted works don't eat a slot
|
||||
Limit int
|
||||
Offset int
|
||||
Kind string
|
||||
Status string // single status (status = ?)
|
||||
Statuses []string // multiple statuses (status IN (?)) — used by the 画图台 grid
|
||||
Since *time.Time
|
||||
UserID string
|
||||
UserIDs []string // when set, keep ONLY rows whose user_id is in this list (admin 用户搜索)
|
||||
Query string // free-text search over prompt / model / error (server-side, 跨页)
|
||||
KeywordUserIDs []string // with Query set: also match rows whose user_id is in this list (单搜索框: 用户 OR 内容)
|
||||
ExcludeSource string // when set, omit rows with this source (e.g. hide API-key "v1" usage from the customer logs page)
|
||||
Source string // when set, keep ONLY rows with this source (admin 来源 filter): "v1" (API key) / "user" (前台) / "admin" (测试模型)
|
||||
HasFile bool // when true, keep ONLY rows with a non-empty file (the 创作记录 gallery — paginates over real media)
|
||||
ExcludeFiles []string // when set, omit rows whose file is in this list (e.g. hide homepage showcase media from user galleries)
|
||||
MediaOnly bool // when true, keep only rows that are pending or have a stored file — the 画图台 grid, so deleted works don't eat a slot
|
||||
}
|
||||
|
||||
type EventStats struct {
|
||||
@@ -66,7 +67,11 @@ func (r *EventRepository) List(ctx context.Context, filter EventListFilter) ([]m
|
||||
}
|
||||
if term := strings.TrimSpace(filter.Query); term != "" {
|
||||
like := "%" + term + "%"
|
||||
q = q.Where("(prompt ILIKE ? OR model ILIKE ? OR error ILIKE ?)", like, like, like)
|
||||
if len(filter.KeywordUserIDs) > 0 {
|
||||
q = q.Where("(prompt ILIKE ? OR model ILIKE ? OR error ILIKE ? OR user_id IN ?)", like, like, like, filter.KeywordUserIDs)
|
||||
} else {
|
||||
q = q.Where("(prompt ILIKE ? OR model ILIKE ? OR error ILIKE ?)", like, like, like)
|
||||
}
|
||||
}
|
||||
if filter.ExcludeSource != "" {
|
||||
q = q.Where("(source IS NULL OR source <> ?)", filter.ExcludeSource)
|
||||
|
||||
@@ -89,11 +89,11 @@ func (s *AdminReadService) ModelsView(ctx context.Context) ([]map[string]any, er
|
||||
for _, item := range items {
|
||||
_, bound := routable[item.ID]
|
||||
out = append(out, map[string]any{
|
||||
"id": item.ID,
|
||||
"alias": item.Alias,
|
||||
"type": item.Type,
|
||||
"name": item.Name,
|
||||
"provider": item.Provider,
|
||||
"id": item.ID,
|
||||
"alias": item.Alias,
|
||||
"type": item.Type,
|
||||
"name": item.Name,
|
||||
"provider": item.Provider,
|
||||
// 配价即上架(无启停开关):全档位有价 + 绑定了启用渠道才算可售。
|
||||
"sellable": bound && ModelFullyPriced(&item),
|
||||
"ratios": repo.JSONStrings(item.Ratios),
|
||||
@@ -122,25 +122,35 @@ func (s *AdminReadService) ModelsView(ctx context.Context) ([]map[string]any, er
|
||||
}
|
||||
|
||||
func (s *AdminReadService) Logs(ctx context.Context, limit, offset int, kind, status string, statuses []string, since *time.Time, userID string, userIDs []string, query, excludeSource, source string, hasFile, excludeShowcase, mediaOnly bool) ([]model.EventLog, int64, *repo.EventStats, error) {
|
||||
return s.logs(ctx, limit, offset, kind, status, statuses, since, userID, userIDs, nil, query, excludeSource, source, hasFile, excludeShowcase, mediaOnly)
|
||||
}
|
||||
|
||||
// LogsKeyword 单搜索框:keyword 命中 提示词/模型/错误,或 user_id 在 kwUserIDs 内(名称/邮箱/ID 命中)即保留。
|
||||
func (s *AdminReadService) LogsKeyword(ctx context.Context, limit, offset int, kind, status string, since *time.Time, keyword string, kwUserIDs []string, source string) ([]model.EventLog, int64, *repo.EventStats, error) {
|
||||
return s.logs(ctx, limit, offset, kind, status, nil, since, "", nil, kwUserIDs, keyword, "", source, false, false, false)
|
||||
}
|
||||
|
||||
func (s *AdminReadService) logs(ctx context.Context, limit, offset int, kind, status string, statuses []string, since *time.Time, userID string, userIDs, kwUserIDs []string, query, excludeSource, source string, hasFile, excludeShowcase, mediaOnly bool) ([]model.EventLog, int64, *repo.EventStats, error) {
|
||||
var excludeFiles []string
|
||||
if excludeShowcase {
|
||||
excludeFiles = s.showcaseFileList(ctx)
|
||||
}
|
||||
items, total, err := s.events.List(ctx, repo.EventListFilter{
|
||||
Limit: limit,
|
||||
Offset: offset,
|
||||
Kind: kind,
|
||||
Status: status,
|
||||
Statuses: statuses,
|
||||
Since: since,
|
||||
UserID: userID,
|
||||
UserIDs: userIDs,
|
||||
Query: query,
|
||||
ExcludeSource: excludeSource,
|
||||
Source: source,
|
||||
HasFile: hasFile,
|
||||
ExcludeFiles: excludeFiles,
|
||||
MediaOnly: mediaOnly,
|
||||
Limit: limit,
|
||||
Offset: offset,
|
||||
Kind: kind,
|
||||
Status: status,
|
||||
Statuses: statuses,
|
||||
Since: since,
|
||||
UserID: userID,
|
||||
UserIDs: userIDs,
|
||||
Query: query,
|
||||
KeywordUserIDs: kwUserIDs,
|
||||
ExcludeSource: excludeSource,
|
||||
Source: source,
|
||||
HasFile: hasFile,
|
||||
ExcludeFiles: excludeFiles,
|
||||
MediaOnly: mediaOnly,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, 0, nil, err
|
||||
|
||||
Reference in New Issue
Block a user