package audit import ( "errors" "net/http" "strconv" "strings" "time" "aigateway.local/core/internal/identity" "aigateway.local/core/internal/platform/apiresponse" ) type AdminHTTPHandler struct { query *QueryService identity *identity.Service mux *http.ServeMux } func NewAdminHTTPHandler(query *QueryService, identityService *identity.Service) *AdminHTTPHandler { h := &AdminHTTPHandler{query: query, identity: identityService, mux: http.NewServeMux()} h.mux.HandleFunc("GET /api/v1/admin/audit-events", h.listEvents) h.mux.HandleFunc("GET /api/v1/admin/usage/daily", h.listDailyUsage) return h } func (h *AdminHTTPHandler) ServeHTTP(writer http.ResponseWriter, request *http.Request) { h.mux.ServeHTTP(writer, request) } func (h *AdminHTTPHandler) listEvents(writer http.ResponseWriter, request *http.Request) { if !h.requirePermission(writer, request, identity.PermissionAuditRead) { return } now := time.Now().UTC() from, err := queryTime(request, "from", now.Add(-24*time.Hour)) if err != nil { apiresponse.Error(writer, http.StatusBadRequest, "from 时间无效") return } to, err := queryTime(request, "to", now.Add(time.Second)) if err != nil || !to.After(from) || to.Sub(from) > 366*24*time.Hour { apiresponse.Error(writer, http.StatusBadRequest, "审计查询时间范围无效或超过 366 天") return } limit := 50 if value := request.URL.Query().Get("limit"); value != "" { limit, err = strconv.Atoi(value) if err != nil || limit < 1 || limit > 200 { apiresponse.Error(writer, http.StatusBadRequest, "limit 必须在 1 到 200 之间") return } } var before *time.Time if value := request.URL.Query().Get("before"); value != "" { parsed, parseErr := time.Parse(time.RFC3339Nano, value) if parseErr != nil { apiresponse.Error(writer, http.StatusBadRequest, "before 游标无效") return } before = &parsed } var status *int if value := request.URL.Query().Get("status"); value != "" { parsed, parseErr := strconv.Atoi(value) if parseErr != nil || parsed < 100 || parsed > 599 { apiresponse.Error(writer, http.StatusBadRequest, "status 无效") return } status = &parsed } items, err := h.query.ListEvents(request.Context(), EventFilter{ From: from, To: to, Before: before, APIKeyID: strings.TrimSpace(request.URL.Query().Get("api_key_id")), Provider: strings.TrimSpace(request.URL.Query().Get("provider")), Model: strings.TrimSpace(request.URL.Query().Get("model")), StatusCode: status, Limit: limit, }) if err != nil { apiresponse.Error(writer, http.StatusServiceUnavailable, "审计查询服务暂不可用") return } next := "" if len(items) == limit { next = items[len(items)-1].RecordedAt.Format(time.RFC3339Nano) } apiresponse.OK(writer, map[string]any{"items": items, "next_before": next}) } func (h *AdminHTTPHandler) listDailyUsage(writer http.ResponseWriter, request *http.Request) { if !h.requirePermission(writer, request, identity.PermissionUsageRead) { return } now := time.Now().UTC() from, err := queryDate(request, "from", now.AddDate(0, 0, -29)) if err != nil { apiresponse.Error(writer, http.StatusBadRequest, "from 日期无效") return } to, err := queryDate(request, "to", now) if err != nil || to.Before(from) || to.Sub(from) > 366*24*time.Hour { apiresponse.Error(writer, http.StatusBadRequest, "usage 查询日期范围无效或超过 366 天") return } items, err := h.query.ListDailyUsage(request.Context(), UsageFilter{ From: from, To: to, APIKeyID: strings.TrimSpace(request.URL.Query().Get("api_key_id")), Provider: strings.TrimSpace(request.URL.Query().Get("provider")), Model: strings.TrimSpace(request.URL.Query().Get("model")), }) if err != nil { apiresponse.Error(writer, http.StatusServiceUnavailable, "usage 查询服务暂不可用") return } apiresponse.OK(writer, items) } func (h *AdminHTTPHandler) requirePermission(writer http.ResponseWriter, request *http.Request, permission string) bool { account, err := h.identity.Authenticate(request.Context(), identity.KindAdmin, request.Header.Get("Authorization")) if err != nil { status := http.StatusUnauthorized if !errors.Is(err, identity.ErrInvalidSession) && !errors.Is(err, identity.ErrNotFound) { status = http.StatusServiceUnavailable } apiresponse.Error(writer, status, "登录状态无效或身份服务暂不可用") return false } if !identity.HasPermission(account, permission) { apiresponse.Error(writer, http.StatusForbidden, "缺少审计或 usage 查看权限") return false } return true } func queryTime(request *http.Request, name string, fallback time.Time) (time.Time, error) { value := request.URL.Query().Get(name) if value == "" { return fallback, nil } return time.Parse(time.RFC3339, value) } func queryDate(request *http.Request, name string, fallback time.Time) (time.Time, error) { value := request.URL.Query().Get(name) if value == "" { value = fallback.Format("2006-01-02") } return time.Parse("2006-01-02", value) }