feat(admin): 优化后台运维与任务管理
完善平台、基准模型、定价规则和实时负载的紧凑查询与滚动展示,并固定关键操作列。 新增管理员任务记录、批量计费结算和用户组限流、额度及模型权限维护能力,同时补充任务脱敏、查询索引与 OpenAPI 契约。 验证:pnpm openapi、Go 全量测试、pnpm lint、pnpm test、pnpm build、gofmt 与差异格式检查均通过。
This commit is contained in:
@@ -0,0 +1,213 @@
|
||||
package httpapi
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/easyai/easyai-ai-gateway/apps/api/internal/store"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// listAdminTasks godoc
|
||||
// @Summary 管理员列出任务
|
||||
// @Description 跨租户分页查询任务;请求、结果和执行快照中的常见敏感字段默认脱敏。
|
||||
// @Tags admin-tasks
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Param q query string false "关键词,匹配任务、用户、租户、模型、平台和 API Key"
|
||||
// @Param tenantId query string false "网关租户 ID"
|
||||
// @Param userId query string false "网关用户 ID"
|
||||
// @Param userGroupId query string false "用户组 ID"
|
||||
// @Param status query string false "任务状态"
|
||||
// @Param platformId query string false "执行平台 ID"
|
||||
// @Param model query string false "调用或实际模型名称"
|
||||
// @Param modelType query string false "模型类型"
|
||||
// @Param runMode query string false "运行模式"
|
||||
// @Param billingStatus query string false "计费状态"
|
||||
// @Param apiKey query string false "API Key ID、名称或前缀"
|
||||
// @Param createdFrom query string false "创建时间起点,支持 RFC3339 或日期格式"
|
||||
// @Param createdTo query string false "创建时间终点,支持 RFC3339 或日期格式"
|
||||
// @Param page query int false "页码" default(1)
|
||||
// @Param pageSize query int false "每页数量" default(50)
|
||||
// @Success 200 {object} AdminTaskListResponse
|
||||
// @Failure 400 {object} ErrorEnvelope
|
||||
// @Failure 401 {object} ErrorEnvelope
|
||||
// @Failure 403 {object} ErrorEnvelope
|
||||
// @Failure 500 {object} ErrorEnvelope
|
||||
// @Router /api/admin/tasks [get]
|
||||
func (s *Server) listAdminTasks(w http.ResponseWriter, r *http.Request) {
|
||||
query := r.URL.Query()
|
||||
page, err := positiveQueryInt(query.Get("page"), 1)
|
||||
if err != nil {
|
||||
writeError(w, http.StatusBadRequest, "invalid page")
|
||||
return
|
||||
}
|
||||
pageSize, err := positiveQueryInt(query.Get("pageSize"), 50)
|
||||
if err != nil {
|
||||
writeError(w, http.StatusBadRequest, "invalid pageSize")
|
||||
return
|
||||
}
|
||||
createdFrom, err := parseTaskListTime(query.Get("createdFrom"), query.Get("from"))
|
||||
if err != nil {
|
||||
writeError(w, http.StatusBadRequest, "invalid createdFrom")
|
||||
return
|
||||
}
|
||||
createdTo, err := parseTaskListTime(query.Get("createdTo"), query.Get("to"))
|
||||
if err != nil {
|
||||
writeError(w, http.StatusBadRequest, "invalid createdTo")
|
||||
return
|
||||
}
|
||||
for name, value := range map[string]string{
|
||||
"tenantId": query.Get("tenantId"),
|
||||
"userId": query.Get("userId"),
|
||||
"userGroupId": query.Get("userGroupId"),
|
||||
"platformId": query.Get("platformId"),
|
||||
} {
|
||||
if value != "" && !validUUID(value) {
|
||||
writeError(w, http.StatusBadRequest, "invalid "+name)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
result, err := s.store.ListAdminTasks(r.Context(), store.AdminTaskListFilter{
|
||||
Query: firstNonEmpty(query.Get("q"), query.Get("query")),
|
||||
GatewayTenant: query.Get("tenantId"),
|
||||
GatewayUser: query.Get("userId"),
|
||||
UserGroup: query.Get("userGroupId"),
|
||||
Status: query.Get("status"),
|
||||
Platform: query.Get("platformId"),
|
||||
Model: query.Get("model"),
|
||||
ModelType: query.Get("modelType"),
|
||||
RunMode: query.Get("runMode"),
|
||||
BillingStatus: query.Get("billingStatus"),
|
||||
APIKey: query.Get("apiKey"),
|
||||
CreatedFrom: createdFrom,
|
||||
CreatedTo: createdTo,
|
||||
Page: page,
|
||||
PageSize: pageSize,
|
||||
})
|
||||
if err != nil {
|
||||
s.logger.Error("list admin tasks failed", "error", err)
|
||||
writeError(w, http.StatusInternalServerError, "list admin tasks failed")
|
||||
return
|
||||
}
|
||||
for index := range result.Items {
|
||||
result.Items[index] = store.MaskAdminGatewayTask(result.Items[index])
|
||||
}
|
||||
writeJSON(w, http.StatusOK, map[string]any{
|
||||
"items": result.Items,
|
||||
"total": result.Total,
|
||||
"page": result.Page,
|
||||
"pageSize": result.PageSize,
|
||||
})
|
||||
}
|
||||
|
||||
// getAdminTask godoc
|
||||
// @Summary 管理员获取任务详情
|
||||
// @Description 返回任意租户的任务详情;默认脱敏,sensitive=full 时返回完整存储内容。
|
||||
// @Tags admin-tasks
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Param taskID path string true "任务 ID"
|
||||
// @Param sensitive query string false "敏感字段模式" Enums(masked,full) default(masked)
|
||||
// @Success 200 {object} store.AdminGatewayTask
|
||||
// @Failure 400 {object} ErrorEnvelope
|
||||
// @Failure 401 {object} ErrorEnvelope
|
||||
// @Failure 403 {object} ErrorEnvelope
|
||||
// @Failure 404 {object} ErrorEnvelope
|
||||
// @Failure 500 {object} ErrorEnvelope
|
||||
// @Router /api/admin/tasks/{taskID} [get]
|
||||
func (s *Server) getAdminTask(w http.ResponseWriter, r *http.Request) {
|
||||
taskID := r.PathValue("taskID")
|
||||
if !validUUID(taskID) {
|
||||
writeError(w, http.StatusBadRequest, "invalid taskID")
|
||||
return
|
||||
}
|
||||
full, ok := parseAdminTaskSensitiveMode(r.URL.Query().Get("sensitive"))
|
||||
if !ok {
|
||||
writeError(w, http.StatusBadRequest, "invalid sensitive mode")
|
||||
return
|
||||
}
|
||||
task, err := s.store.GetAdminTask(r.Context(), taskID)
|
||||
if err != nil {
|
||||
if store.IsNotFound(err) {
|
||||
writeError(w, http.StatusNotFound, "task not found")
|
||||
return
|
||||
}
|
||||
s.logger.Error("get admin task failed", "taskID", taskID, "error", err)
|
||||
writeError(w, http.StatusInternalServerError, "get admin task failed")
|
||||
return
|
||||
}
|
||||
task.GatewayTask, err = s.hydrateTaskResult(r.Context(), task.GatewayTask)
|
||||
if err != nil {
|
||||
writeStoredBinaryResultError(w, err)
|
||||
return
|
||||
}
|
||||
if !full {
|
||||
task = store.MaskAdminGatewayTask(task)
|
||||
}
|
||||
writeJSON(w, http.StatusOK, task)
|
||||
}
|
||||
|
||||
// adminTaskParamPreprocessing godoc
|
||||
// @Summary 管理员获取任务参数预处理日志
|
||||
// @Description 返回任意租户任务的参数改写、校验或模板处理日志;默认脱敏。
|
||||
// @Tags admin-tasks
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Param taskID path string true "任务 ID"
|
||||
// @Param sensitive query string false "敏感字段模式" Enums(masked,full) default(masked)
|
||||
// @Success 200 {object} TaskParamPreprocessingLogListResponse
|
||||
// @Failure 400 {object} ErrorEnvelope
|
||||
// @Failure 401 {object} ErrorEnvelope
|
||||
// @Failure 403 {object} ErrorEnvelope
|
||||
// @Failure 404 {object} ErrorEnvelope
|
||||
// @Failure 500 {object} ErrorEnvelope
|
||||
// @Router /api/admin/tasks/{taskID}/param-preprocessing [get]
|
||||
func (s *Server) adminTaskParamPreprocessing(w http.ResponseWriter, r *http.Request) {
|
||||
taskID := r.PathValue("taskID")
|
||||
if !validUUID(taskID) {
|
||||
writeError(w, http.StatusBadRequest, "invalid taskID")
|
||||
return
|
||||
}
|
||||
full, ok := parseAdminTaskSensitiveMode(r.URL.Query().Get("sensitive"))
|
||||
if !ok {
|
||||
writeError(w, http.StatusBadRequest, "invalid sensitive mode")
|
||||
return
|
||||
}
|
||||
if _, err := s.store.GetAdminTask(r.Context(), taskID); err != nil {
|
||||
if store.IsNotFound(err) {
|
||||
writeError(w, http.StatusNotFound, "task not found")
|
||||
return
|
||||
}
|
||||
s.logger.Error("get admin task failed", "taskID", taskID, "error", err)
|
||||
writeError(w, http.StatusInternalServerError, "get admin task failed")
|
||||
return
|
||||
}
|
||||
items, err := s.store.ListTaskParamPreprocessingLogs(r.Context(), taskID)
|
||||
if err != nil {
|
||||
s.logger.Error("list admin task parameter preprocessing logs failed", "taskID", taskID, "error", err)
|
||||
writeError(w, http.StatusInternalServerError, "list admin task parameter preprocessing logs failed")
|
||||
return
|
||||
}
|
||||
if !full {
|
||||
items = store.MaskTaskParamPreprocessingLogs(items)
|
||||
}
|
||||
writeJSON(w, http.StatusOK, map[string]any{"items": items})
|
||||
}
|
||||
|
||||
func parseAdminTaskSensitiveMode(value string) (bool, bool) {
|
||||
switch strings.ToLower(strings.TrimSpace(value)) {
|
||||
case "", "masked":
|
||||
return false, true
|
||||
case "full":
|
||||
return true, true
|
||||
default:
|
||||
return false, false
|
||||
}
|
||||
}
|
||||
|
||||
func validUUID(value string) bool {
|
||||
_, err := uuid.Parse(strings.TrimSpace(value))
|
||||
return err == nil
|
||||
}
|
||||
Reference in New Issue
Block a user