Files
opsxsql/backend/internal/handler/dashboard.go
T
Logan.Li 16f1f7d65f feat(backend): unified response helpers, refresh token, security hardening
- new resp package (OK/Page/Fail/BadRequest/.../InternalErr): unified error
  envelope {detail, code?}, sanitized 500s (full error logged server-side,
  generic message to client - stops DB error leakage), pagination envelope
  {items,total,page,page_size}; migrate 200+ inline c.JSON error sites
- refresh token: 7d rotation tokens (SHA-256 hashed at rest), public
  POST /auth/refresh with single-use rotation, POST /auth/logout revokes
  all user refresh tokens (endpoint previously missing - frontend 404)
- security: gh-ost command line password masked in task status payload;
  Instance.MarshalJSON guarantees encrypted passwords never leave the API
  (removes reliance on per-handler manual masking); production (DEBUG=false)
  with default JWT secret now auto-generates and persists a random secret
- safe.Go recover wrapper for background goroutines (instance probe,
  gh-ost async)
- delete dead service layer (zero callers)
- fix pre-existing go vet unkeyed bson.E literals
2026-10-26 17:21:00 +08:00

99 lines
2.8 KiB
Go

package handler
import (
"net/http"
"sql-platform/internal/middleware"
"sql-platform/internal/model"
"github.com/gin-gonic/gin"
)
// GET /api/v1/dashboard/stats — 匹配前端 DashboardStats 类型
func DashboardStats(c *gin.Context) {
var ticketTotal, ticketPending, ticketExecuting int64
var instanceTotal, instanceOnline, userTotal int64
var queriesToday int64
model.DB.Model(&model.Ticket{}).Count(&ticketTotal)
model.DB.Model(&model.Ticket{}).Where("status IN ?", []string{"pending", "reviewing"}).Count(&ticketPending)
model.DB.Model(&model.Ticket{}).Where("status = ?", "executing").Count(&ticketExecuting)
model.DB.Model(&model.Instance{}).Count(&instanceTotal)
model.DB.Model(&model.Instance{}).Where("status = ?", "online").Count(&instanceOnline)
model.DB.Model(&model.User{}).Count(&userTotal)
model.DB.Model(&model.QueryHistory{}).Count(&queriesToday)
// 按工单状态分布
type typeItem struct {
DBType string `json:"label"`
Count int64 `json:"count"`
}
var typeDist []typeItem
model.DB.Model(&model.Ticket{}).Select("db_type as db_type, count(*) as count").Group("db_type").Scan(&typeDist)
c.JSON(http.StatusOK, gin.H{
"ticket_total": ticketTotal,
"ticket_pending": ticketPending,
"ticket_month": ticketTotal, // 简化
"ticket_executing": ticketExecuting,
"instance_total": instanceTotal,
"instance_online": instanceOnline,
"user_total": userTotal,
"queries_today": queriesToday,
"type_distribution": typeDist,
"query_stats": gin.H{
"total": queriesToday,
"avg_time_ms": 0,
"slow_count": 0,
"type_distribution": typeDist,
},
})
}
// GET /api/v1/dashboard/my-tickets
func MyTickets(c *gin.Context) {
userID, _, role := middleware.GetCurrentUser(c)
var tickets []model.Ticket
query := model.DB.Order("id DESC").Limit(10)
switch role {
case "admin":
query = query.Where("status IN ?", []string{"pending", "reviewing", "executing"})
case "dba", "auditor":
query = query.Where(
"(applicant_id = ? AND status IN ?) OR (id IN (SELECT ticket_id FROM ticket_approvals WHERE approver_id = ?) AND status IN ?)",
userID, []string{"pending", "executing"},
userID, []string{"pending", "reviewing", "executing"},
)
default:
query = query.Where("applicant_id = ?", userID)
}
query.Find(&tickets)
type ticketWithRelation struct {
model.Ticket
Relation string `json:"relation"`
PendingInfo *string `json:"pending_info"`
}
var result []ticketWithRelation
for _, t := range tickets {
rel := "我提交的"
if t.ApplicantID != userID {
rel = "待我审批"
}
pi := ""
if t.ApplicantID != userID {
pi = "等待您审批"
}
result = append(result, ticketWithRelation{Ticket: t, Relation: rel, PendingInfo: &pi})
}
if result == nil {
result = []ticketWithRelation{}
}
c.JSON(http.StatusOK, result)
}