Initial commit: GovAI 政务AI平台

This commit is contained in:
freedakgmail
2026-06-15 23:48:37 +08:00
commit 0f490f72a9
245 changed files with 51669 additions and 0 deletions
+828
View File
@@ -0,0 +1,828 @@
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
}