package handler // 平台级管理 handler — 仅 super_admin 可访问,跨机构操作,不受 org_id 限制。 // 与 admin.go(机构级)的核心区别: // - admin.go 所有查询都加 WHERE org_id = $orgID 过滤 // - platform.go 不加 org_id 过滤,可看/操作所有机构数据 import ( "encoding/json" "errors" "net/http" "strconv" "time" "github.com/enterprise-ai-platform/server/internal/response" "github.com/go-chi/chi/v5" "github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5/pgxpool" ) type PlatformHandler struct { pool *pgxpool.Pool } func NewPlatformHandler(pool *pgxpool.Pool) *PlatformHandler { return &PlatformHandler{pool: pool} } // ==================== 平台总览 ==================== // Overview 平台级数据看板:跨所有机构的聚合统计 func (h *PlatformHandler) Overview(w http.ResponseWriter, r *http.Request) { now := time.Now() todayStart := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location()) monthStart := time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, now.Location()) var totalOrgs, activeOrgs, totalUsers, activeUsers, totalApps, approvedApps, todayLogins, todayConvs int var monthlyTokens int64 var monthlyCost float64 h.pool.QueryRow(r.Context(), `SELECT COUNT(*) FROM organizations`).Scan(&totalOrgs) h.pool.QueryRow(r.Context(), `SELECT COUNT(*) FROM organizations WHERE is_active = true`).Scan(&activeOrgs) h.pool.QueryRow(r.Context(), `SELECT COUNT(*) FROM users`).Scan(&totalUsers) h.pool.QueryRow(r.Context(), `SELECT COUNT(*) FROM users WHERE status = 'active'`).Scan(&activeUsers) h.pool.QueryRow(r.Context(), `SELECT COUNT(*) FROM applications`).Scan(&totalApps) h.pool.QueryRow(r.Context(), `SELECT COUNT(*) FROM applications WHERE status = 'approved'`).Scan(&approvedApps) h.pool.QueryRow(r.Context(), `SELECT COUNT(*) FROM users WHERE last_login_at >= $1`, todayStart).Scan(&todayLogins) h.pool.QueryRow(r.Context(), `SELECT COUNT(*) FROM app_usage_logs WHERE created_at >= $1`, todayStart).Scan(&todayConvs) h.pool.QueryRow(r.Context(), `SELECT COALESCE(SUM(total_tokens),0) FROM app_usage_logs WHERE created_at >= $1`, monthStart).Scan(&monthlyTokens) h.pool.QueryRow(r.Context(), `SELECT COALESCE(SUM(estimated_cost),0) FROM app_usage_logs WHERE created_at >= $1`, monthStart).Scan(&monthlyCost) response.JSON(w, http.StatusOK, map[string]any{ "total_orgs": totalOrgs, "active_orgs": activeOrgs, "total_users": totalUsers, "active_users": activeUsers, "total_apps": totalApps, "approved_apps": approvedApps, "today_logins": todayLogins, "today_convs": todayConvs, "monthly_tokens": monthlyTokens, "monthly_cost": monthlyCost, }) } // OrgRanking 各机构活跃度排行(用户数 / 应用数 / 月度对话数) func (h *PlatformHandler) OrgRanking(w http.ResponseWriter, r *http.Request) { now := time.Now() monthStart := time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, now.Location()) rows, err := h.pool.Query(r.Context(), ` SELECT o.id, o.name, COALESCE(o.short_name, ''), COUNT(DISTINCT u.id) AS users, COUNT(DISTINCT a.id) FILTER (WHERE a.status='approved') AS apps, COALESCE(SUM(usage.cnt), 0) AS conversations, COALESCE(SUM(usage.tk), 0) AS tokens FROM organizations o LEFT JOIN users u ON u.org_id = o.id LEFT JOIN applications a ON a.org_id = o.id LEFT JOIN LATERAL ( SELECT COUNT(*) AS cnt, COALESCE(SUM(total_tokens),0) AS tk FROM app_usage_logs l JOIN users uu ON l.user_id = uu.id WHERE uu.org_id = o.id AND l.created_at >= $1 ) usage ON true WHERE o.is_active = true GROUP BY o.id, o.name, o.short_name ORDER BY conversations DESC, users DESC LIMIT 50`, monthStart) if err != nil { response.InternalError(w, "查询失败") return } defer rows.Close() items := []map[string]any{} for rows.Next() { var ( id, name, short string users, apps int conversations, toks int64 ) if err := rows.Scan(&id, &name, &short, &users, &apps, &conversations, &toks); err != nil { continue } items = append(items, map[string]any{ "id": id, "name": name, "short_name": short, "users": users, "apps": apps, "conversations": conversations, "tokens": toks, }) } response.JSON(w, http.StatusOK, items) } // ==================== 机构管理 ==================== func (h *PlatformHandler) ListOrgs(w http.ResponseWriter, r *http.Request) { rows, err := h.pool.Query(r.Context(), ` SELECT o.id, o.name, o.slug, COALESCE(o.short_name,''), COALESCE(o.description,''), COALESCE(o.logo_url,''), o.sort_order, o.is_active, o.created_at, COALESCE((SELECT COUNT(*) FROM users WHERE org_id = o.id), 0) AS user_count, COALESCE((SELECT COUNT(*) FROM applications WHERE org_id = o.id), 0) AS app_count FROM organizations o ORDER BY o.sort_order, o.created_at`) if err != nil { response.InternalError(w, "查询机构失败") return } defer rows.Close() items := []map[string]any{} for rows.Next() { var ( id, name, slug, short, desc, logo string sortOrder int isActive bool createdAt time.Time userCount, appCount int ) if err := rows.Scan(&id, &name, &slug, &short, &desc, &logo, &sortOrder, &isActive, &createdAt, &userCount, &appCount); err != nil { continue } items = append(items, map[string]any{ "id": id, "name": name, "slug": slug, "short_name": short, "description": desc, "logo_url": logo, "sort_order": sortOrder, "is_active": isActive, "created_at": createdAt, "user_count": userCount, "app_count": appCount, }) } response.JSON(w, http.StatusOK, items) } func (h *PlatformHandler) CreateOrg(w http.ResponseWriter, r *http.Request) { var req struct { Name string `json:"name"` Slug string `json:"slug"` ShortName string `json:"short_name"` Description string `json:"description"` LogoURL string `json:"logo_url"` SortOrder int `json:"sort_order"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { response.BadRequest(w, "无效的请求格式") return } if req.Name == "" || req.Slug == "" { response.BadRequest(w, "机构名称和标识不能为空") return } var id string err := h.pool.QueryRow(r.Context(), ` INSERT INTO organizations (name, slug, short_name, description, logo_url, sort_order) VALUES ($1, $2, $3, $4, $5, $6) RETURNING id::text`, req.Name, req.Slug, req.ShortName, req.Description, req.LogoURL, req.SortOrder, ).Scan(&id) if err != nil { response.InternalError(w, "创建机构失败:标识可能已存在") return } response.JSON(w, http.StatusCreated, map[string]any{"id": id}) } func (h *PlatformHandler) UpdateOrg(w http.ResponseWriter, r *http.Request) { id := chi.URLParam(r, "id") var req struct { Name *string `json:"name"` ShortName *string `json:"short_name"` Description *string `json:"description"` LogoURL *string `json:"logo_url"` SortOrder *int `json:"sort_order"` IsActive *bool `json:"is_active"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { response.BadRequest(w, "无效的请求格式") return } _, err := h.pool.Exec(r.Context(), ` UPDATE organizations SET name = COALESCE($2, name), short_name = COALESCE($3, short_name), description = COALESCE($4, description), logo_url = COALESCE($5, logo_url), sort_order = COALESCE($6, sort_order), is_active = COALESCE($7, is_active), updated_at = NOW() WHERE id = $1`, id, req.Name, req.ShortName, req.Description, req.LogoURL, req.SortOrder, req.IsActive) if err != nil { response.InternalError(w, "更新失败") return } response.JSON(w, http.StatusOK, map[string]string{"message": "已更新"}) } func (h *PlatformHandler) DeleteOrg(w http.ResponseWriter, r *http.Request) { id := chi.URLParam(r, "id") // 安全检查:机构下还有用户或应用时禁止物理删除,引导走停用 var userCount, appCount int h.pool.QueryRow(r.Context(), `SELECT COUNT(*) FROM users WHERE org_id = $1`, id).Scan(&userCount) h.pool.QueryRow(r.Context(), `SELECT COUNT(*) FROM applications WHERE org_id = $1`, id).Scan(&appCount) if userCount > 0 || appCount > 0 { response.BadRequest(w, "该机构尚有用户或应用,无法删除。请先停用或迁移数据") return } _, err := h.pool.Exec(r.Context(), `DELETE FROM organizations WHERE id = $1`, id) if err != nil { response.InternalError(w, "删除失败") return } response.JSON(w, http.StatusOK, map[string]string{"message": "已删除"}) } // ==================== 全局用户管理 ==================== // ListAllUsers 全局用户列表(支持分页、按机构/角色/状态过滤) func (h *PlatformHandler) ListAllUsers(w http.ResponseWriter, r *http.Request) { q := r.URL.Query() page, _ := strconv.Atoi(q.Get("page")) if page < 1 { page = 1 } pageSize := 20 offset := (page - 1) * pageSize search := q.Get("q") roleFilter := q.Get("role") statusFilter := q.Get("status") orgFilter := q.Get("org_id") // 构造 WHERE 子句(list 与 count 共用) where := ` WHERE 1=1` args := []any{} argIdx := 1 if search != "" { where += ` AND (u.name ILIKE '%'||$` + strconv.Itoa(argIdx) + `||'%' OR u.email ILIKE '%'||$` + strconv.Itoa(argIdx) + `||'%')` args = append(args, search) argIdx++ } if roleFilter != "" { where += ` AND u.role = $` + strconv.Itoa(argIdx) args = append(args, roleFilter) argIdx++ } if statusFilter != "" { where += ` AND u.status = $` + strconv.Itoa(argIdx) args = append(args, statusFilter) argIdx++ } if orgFilter != "" { where += ` AND u.org_id = $` + strconv.Itoa(argIdx) args = append(args, orgFilter) argIdx++ } // 先查总数 var total int h.pool.QueryRow(r.Context(), `SELECT COUNT(*) FROM users u`+where, args...).Scan(&total) // 再分页查列表 listQuery := `SELECT u.id, u.name, u.email, u.avatar_url, u.role, u.status, u.employee_id, u.last_login_at, u.login_count, u.created_at, COALESCE(o.id::text, ''), COALESCE(o.name, ''), COALESCE(o.short_name, '') FROM users u LEFT JOIN organizations o ON u.org_id = o.id` + where + ` ORDER BY u.created_at DESC LIMIT $` + strconv.Itoa(argIdx) + ` OFFSET $` + strconv.Itoa(argIdx+1) listArgs := append(args, pageSize, offset) rows, err := h.pool.Query(r.Context(), listQuery, listArgs...) if err != nil { response.InternalError(w, "查询用户失败") return } defer rows.Close() items := []map[string]any{} for rows.Next() { var ( id, name, email, role, status string avatarURL, employeeID *string lastLoginAt *time.Time loginCount int createdAt time.Time orgID, orgName, orgShort string ) if err := rows.Scan(&id, &name, &email, &avatarURL, &role, &status, &employeeID, &lastLoginAt, &loginCount, &createdAt, &orgID, &orgName, &orgShort); err != nil { continue } items = append(items, map[string]any{ "id": id, "name": name, "email": email, "avatar_url": avatarURL, "role": role, "status": status, "employee_id": employeeID, "last_login_at": lastLoginAt, "login_count": loginCount, "created_at": createdAt, "org_id": orgID, "org_name": orgName, "org_short": orgShort, }) } response.JSON(w, http.StatusOK, map[string]any{ "items": items, "total": total, "page": page, "page_size": pageSize, }) } // AssignUserOrg 把用户分配/迁移到指定机构 func (h *PlatformHandler) AssignUserOrg(w http.ResponseWriter, r *http.Request) { userID := chi.URLParam(r, "id") var req struct { OrgID string `json:"org_id"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { response.BadRequest(w, "无效的请求格式") return } if req.OrgID == "" { response.BadRequest(w, "机构ID不能为空") return } _, err := h.pool.Exec(r.Context(), `UPDATE users SET org_id = $2 WHERE id = $1`, userID, req.OrgID) if err != nil { response.InternalError(w, "迁移失败") return } response.JSON(w, http.StatusOK, map[string]string{"message": "已迁移"}) } // UpdateUserRole 平台管理员可设置任意角色(包括 super_admin) func (h *PlatformHandler) UpdateUserRole(w http.ResponseWriter, r *http.Request) { userID := chi.URLParam(r, "id") var req struct { Role string `json:"role"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { response.BadRequest(w, "无效的请求格式") return } valid := map[string]bool{"user": true, "creator": true, "admin": true, "super_admin": true} if !valid[req.Role] { response.BadRequest(w, "无效的角色") return } _, err := h.pool.Exec(r.Context(), `UPDATE users SET role = $2 WHERE id = $1`, userID, req.Role) if err != nil { response.InternalError(w, "更新角色失败") return } response.JSON(w, http.StatusOK, map[string]string{"message": "已更新"}) } // UpdateUserStatus 启用/禁用任意用户 func (h *PlatformHandler) UpdateUserStatus(w http.ResponseWriter, r *http.Request) { userID := chi.URLParam(r, "id") var req struct { Status string `json:"status"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { response.BadRequest(w, "无效的请求格式") return } if req.Status != "active" && req.Status != "disabled" { response.BadRequest(w, "无效的状态") return } _, err := h.pool.Exec(r.Context(), `UPDATE users SET status = $2 WHERE id = $1`, userID, req.Status) if err != nil { response.InternalError(w, "更新状态失败") return } response.JSON(w, http.StatusOK, map[string]string{"message": "已更新"}) } // ==================== 全局应用管理 ==================== // ListAllApps 全局应用列表(支持分页、按机构/状态过滤) func (h *PlatformHandler) ListAllApps(w http.ResponseWriter, r *http.Request) { q := r.URL.Query() page, _ := strconv.Atoi(q.Get("page")) if page < 1 { page = 1 } pageSize := 20 offset := (page - 1) * pageSize statusFilter := q.Get("status") orgFilter := q.Get("org_id") where := ` WHERE 1=1` args := []any{} argIdx := 1 if statusFilter != "" { where += ` AND a.status = $` + strconv.Itoa(argIdx) args = append(args, statusFilter) argIdx++ } if orgFilter != "" { where += ` AND a.org_id = $` + strconv.Itoa(argIdx) args = append(args, orgFilter) argIdx++ } var total int h.pool.QueryRow(r.Context(), `SELECT COUNT(*) FROM applications a`+where, args...).Scan(&total) listQuery := `SELECT a.id, a.name, a.slug, a.description, a.icon_url, a.dify_app_type, a.status, a.visibility, a.usage_count, a.is_featured, a.created_at, COALESCE(c.name, ''), COALESCE(u.name, ''), COALESCE(o.id::text, ''), COALESCE(o.name, ''), COALESCE(o.short_name, '') FROM applications a LEFT JOIN categories c ON a.category_id = c.id LEFT JOIN users u ON a.creator_id = u.id LEFT JOIN organizations o ON a.org_id = o.id` + where + ` ORDER BY a.usage_count DESC, a.created_at DESC LIMIT $` + strconv.Itoa(argIdx) + ` OFFSET $` + strconv.Itoa(argIdx+1) listArgs := append(args, pageSize, offset) rows, err := h.pool.Query(r.Context(), listQuery, listArgs...) if err != nil { response.InternalError(w, "查询应用失败") return } defer rows.Close() items := []map[string]any{} for rows.Next() { var ( id, name, slug, status, visibility string desc, iconURL *string appType *string usageCount int64 isFeatured bool createdAt time.Time catName, creatorName string orgID, orgName, orgShort string ) if err := rows.Scan(&id, &name, &slug, &desc, &iconURL, &appType, &status, &visibility, &usageCount, &isFeatured, &createdAt, &catName, &creatorName, &orgID, &orgName, &orgShort); err != nil { continue } items = append(items, map[string]any{ "id": id, "name": name, "slug": slug, "description": desc, "icon_url": iconURL, "dify_app_type": appType, "status": status, "visibility": visibility, "usage_count": usageCount, "is_featured": isFeatured, "created_at": createdAt, "category_name": catName, "creator_name": creatorName, "org_id": orgID, "org_name": orgName, "org_short": orgShort, }) } response.JSON(w, http.StatusOK, map[string]any{ "items": items, "total": total, "page": page, "page_size": pageSize, }) } // SetFeatured 平台级精选/取消精选 func (h *PlatformHandler) SetFeatured(w http.ResponseWriter, r *http.Request) { appID := chi.URLParam(r, "id") var req struct { IsFeatured bool `json:"is_featured"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { response.BadRequest(w, "无效的请求格式") return } _, err := h.pool.Exec(r.Context(), `UPDATE applications SET is_featured = $2 WHERE id = $1`, appID, req.IsFeatured) if err != nil { response.InternalError(w, "操作失败") return } response.JSON(w, http.StatusOK, map[string]string{"message": "已更新"}) } // ForceDelist 平台强制下架(不受机构限制) func (h *PlatformHandler) ForceDelist(w http.ResponseWriter, r *http.Request) { appID := chi.URLParam(r, "id") _, err := h.pool.Exec(r.Context(), `UPDATE applications SET status = 'archived' WHERE id = $1`, appID) if err != nil { response.InternalError(w, "下架失败") return } response.JSON(w, http.StatusOK, map[string]string{"message": "已强制下架"}) } // ==================== 全局审计日志 ==================== // ListAllAuditLogs 全局审计日志(支持分页、按机构/操作类型过滤) func (h *PlatformHandler) ListAllAuditLogs(w http.ResponseWriter, r *http.Request) { q := r.URL.Query() page, _ := strconv.Atoi(q.Get("page")) if page < 1 { page = 1 } pageSize := 20 offset := (page - 1) * pageSize orgFilter := q.Get("org_id") actionFilter := q.Get("action") where := ` WHERE 1=1` args := []any{} argIdx := 1 if orgFilter != "" { where += ` AND u.org_id = $` + strconv.Itoa(argIdx) args = append(args, orgFilter) argIdx++ } if actionFilter != "" { where += ` AND al.action ILIKE '%'||$` + strconv.Itoa(argIdx) + `||'%'` args = append(args, actionFilter) argIdx++ } var total int h.pool.QueryRow(r.Context(), `SELECT COUNT(*) FROM audit_logs al LEFT JOIN users u ON al.user_id = u.id`+where, args...).Scan(&total) listQuery := `SELECT al.id, al.action, al.resource_type, al.resource_id::text, al.details, al.ip_address::text, al.created_at, COALESCE(u.name, ''), COALESCE(u.email, ''), COALESCE(o.name, ''), COALESCE(o.short_name, '') FROM audit_logs al LEFT JOIN users u ON al.user_id = u.id LEFT JOIN organizations o ON u.org_id = o.id` + where + ` ORDER BY al.created_at DESC LIMIT $` + strconv.Itoa(argIdx) + ` OFFSET $` + strconv.Itoa(argIdx+1) listArgs := append(args, pageSize, offset) rows, err := h.pool.Query(r.Context(), listQuery, listArgs...) if err != nil { response.InternalError(w, "查询审计日志失败") return } defer rows.Close() items := []map[string]any{} for rows.Next() { var ( id, action, resType string resID, ipAddr *string details json.RawMessage createdAt time.Time userName, userEmail, orgName, orgShort string ) if err := rows.Scan(&id, &action, &resType, &resID, &details, &ipAddr, &createdAt, &userName, &userEmail, &orgName, &orgShort); err != nil { continue } items = append(items, map[string]any{ "id": id, "action": action, "resource_type": resType, "resource_id": resID, "details": details, "ip_address": ipAddr, "created_at": createdAt, "user_name": userName, "user_email": userEmail, "org_name": orgName, "org_short": orgShort, }) } response.JSON(w, http.StatusOK, map[string]any{ "items": items, "total": total, "page": page, "page_size": pageSize, }) } // ==================== 模型提供商管理 ==================== func (h *PlatformHandler) ListProviders(w http.ResponseWriter, r *http.Request) { rows, err := h.pool.Query(r.Context(), ` SELECT id, name, base_url, models, is_active, priority, created_at, updated_at FROM model_providers ORDER BY priority DESC, created_at`) if err != nil { response.InternalError(w, "查询失败") return } defer rows.Close() items := []map[string]any{} for rows.Next() { var ( id, name, baseURL string models json.RawMessage isActive bool priority int createdAt, updatedAt time.Time ) if err := rows.Scan(&id, &name, &baseURL, &models, &isActive, &priority, &createdAt, &updatedAt); err != nil { continue } items = append(items, map[string]any{ "id": id, "name": name, "base_url": baseURL, "models": models, "is_active": isActive, "priority": priority, "created_at": createdAt, "updated_at": updatedAt, }) } response.JSON(w, http.StatusOK, items) } func (h *PlatformHandler) CreateProvider(w http.ResponseWriter, r *http.Request) { var req struct { Name string `json:"name"` BaseURL string `json:"base_url"` APIKey string `json:"api_key"` Models json.RawMessage `json:"models"` IsActive bool `json:"is_active"` Priority int `json:"priority"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { response.BadRequest(w, "无效的请求格式") return } if req.Name == "" || req.BaseURL == "" || req.APIKey == "" { response.BadRequest(w, "名称、URL、API Key 不能为空") return } if len(req.Models) == 0 { req.Models = []byte(`[]`) } var id string err := h.pool.QueryRow(r.Context(), ` INSERT INTO model_providers (name, base_url, api_key_encrypted, models, is_active, priority) VALUES ($1, $2, $3, $4, $5, $6) RETURNING id::text`, req.Name, req.BaseURL, req.APIKey, req.Models, req.IsActive, req.Priority, ).Scan(&id) if err != nil { response.InternalError(w, "创建失败") return } response.JSON(w, http.StatusCreated, map[string]any{"id": id}) } func (h *PlatformHandler) UpdateProvider(w http.ResponseWriter, r *http.Request) { id := chi.URLParam(r, "id") var req struct { Name *string `json:"name"` BaseURL *string `json:"base_url"` APIKey *string `json:"api_key"` Models json.RawMessage `json:"models"` IsActive *bool `json:"is_active"` Priority *int `json:"priority"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { response.BadRequest(w, "无效的请求格式") return } // 仅当 api_key 非空才更新(避免误清空) var apiKeyArg any = nil if req.APIKey != nil && *req.APIKey != "" { apiKeyArg = *req.APIKey } var modelsArg any = nil if len(req.Models) > 0 { modelsArg = req.Models } _, err := h.pool.Exec(r.Context(), ` UPDATE model_providers SET name = COALESCE($2, name), base_url = COALESCE($3, base_url), api_key_encrypted = COALESCE($4, api_key_encrypted), models = COALESCE($5::jsonb, models), is_active = COALESCE($6, is_active), priority = COALESCE($7, priority), updated_at = NOW() WHERE id = $1`, id, req.Name, req.BaseURL, apiKeyArg, modelsArg, req.IsActive, req.Priority) if err != nil { response.InternalError(w, "更新失败") return } response.JSON(w, http.StatusOK, map[string]string{"message": "已更新"}) } func (h *PlatformHandler) DeleteProvider(w http.ResponseWriter, r *http.Request) { id := chi.URLParam(r, "id") _, err := h.pool.Exec(r.Context(), `DELETE FROM model_providers WHERE id = $1`, id) if err != nil { response.InternalError(w, "删除失败") return } response.JSON(w, http.StatusOK, map[string]string{"message": "已删除"}) } // ==================== 全局配额管理 ==================== func (h *PlatformHandler) ListQuotas(w http.ResponseWriter, r *http.Request) { rows, err := h.pool.Query(r.Context(), ` SELECT q.id, q.target_type, q.target_id::text, q.model_name, q.daily_token_limit, q.monthly_token_limit, q.daily_request_limit, q.is_active, q.created_at, COALESCE(target_name.name, '') FROM model_quotas q LEFT JOIN LATERAL ( SELECT CASE q.target_type WHEN 'global' THEN '全局' WHEN 'department' THEN (SELECT name FROM departments WHERE id = q.target_id) WHEN 'user' THEN (SELECT name FROM users WHERE id = q.target_id) END AS name ) target_name ON true ORDER BY q.target_type, q.created_at DESC LIMIT 200`) if err != nil { response.InternalError(w, "查询配额失败") return } defer rows.Close() items := []map[string]any{} for rows.Next() { var ( id, targetType string targetID *string modelName *string dailyTokens, monthlyTokens *int64 dailyReqs *int isActive bool createdAt time.Time targetName string ) if err := rows.Scan(&id, &targetType, &targetID, &modelName, &dailyTokens, &monthlyTokens, &dailyReqs, &isActive, &createdAt, &targetName); err != nil { continue } items = append(items, map[string]any{ "id": id, "target_type": targetType, "target_id": targetID, "target_name": targetName, "model_name": modelName, "daily_token_limit": dailyTokens, "monthly_token_limit": monthlyTokens, "daily_request_limit": dailyReqs, "is_active": isActive, "created_at": createdAt, }) } response.JSON(w, http.StatusOK, items) } func (h *PlatformHandler) UpsertQuota(w http.ResponseWriter, r *http.Request) { var req struct { ID *string `json:"id"` TargetType string `json:"target_type"` TargetID *string `json:"target_id"` ModelName *string `json:"model_name"` DailyTokenLimit *int64 `json:"daily_token_limit"` MonthlyTokenLimit *int64 `json:"monthly_token_limit"` DailyRequestLimit *int `json:"daily_request_limit"` IsActive bool `json:"is_active"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { response.BadRequest(w, "无效的请求格式") return } valid := map[string]bool{"global": true, "department": true, "user": true} if !valid[req.TargetType] { response.BadRequest(w, "无效的目标类型") return } if req.TargetType != "global" && (req.TargetID == nil || *req.TargetID == "") { response.BadRequest(w, "部门/用户配额必须指定 target_id") return } if req.ID != nil && *req.ID != "" { _, err := h.pool.Exec(r.Context(), ` UPDATE model_quotas SET target_type = $2, target_id = NULLIF($3,'')::uuid, model_name = $4, daily_token_limit = $5, monthly_token_limit = $6, daily_request_limit = $7, is_active = $8, updated_at = NOW() WHERE id = $1`, *req.ID, req.TargetType, nullableString(req.TargetID), req.ModelName, req.DailyTokenLimit, req.MonthlyTokenLimit, req.DailyRequestLimit, req.IsActive) if err != nil { response.InternalError(w, "更新配额失败") return } response.JSON(w, http.StatusOK, map[string]string{"id": *req.ID}) return } var id string err := h.pool.QueryRow(r.Context(), ` INSERT INTO model_quotas (target_type, target_id, model_name, daily_token_limit, monthly_token_limit, daily_request_limit, is_active) VALUES ($1, NULLIF($2,'')::uuid, $3, $4, $5, $6, $7) RETURNING id::text`, req.TargetType, nullableString(req.TargetID), req.ModelName, req.DailyTokenLimit, req.MonthlyTokenLimit, req.DailyRequestLimit, req.IsActive, ).Scan(&id) if err != nil { if errors.Is(err, pgx.ErrNoRows) { response.InternalError(w, "创建配额失败") return } response.InternalError(w, "创建配额失败") return } response.JSON(w, http.StatusCreated, map[string]string{"id": id}) } func (h *PlatformHandler) DeleteQuota(w http.ResponseWriter, r *http.Request) { id := chi.URLParam(r, "id") _, err := h.pool.Exec(r.Context(), `DELETE FROM model_quotas WHERE id = $1`, id) if err != nil { response.InternalError(w, "删除失败") return } response.JSON(w, http.StatusOK, map[string]string{"message": "已删除"}) } func nullableString(s *string) string { if s == nil { return "" } return *s }