feat: v0.3.1 — Go/Gin 后端重写 + gh-ost 集成
架构变更: - Python/FastAPI → Go/Gin 重写(Python 后端保留在 backend-python/) - GORM + SQLite(兼容现有数据库表结构) - JWT 认证 + RBAC 权限中间件 Go 后端结构: - cmd/server/main.go — 入口,端口 9000 - config/config.go — 环境变量配置 - internal/model/ — GORM 模型(30张表,完全兼容 SQLite) - internal/handler/ — auth/instance/ticket/dashboard/ghost handler - internal/middleware/ — JWT 认证 + CORS - internal/ghost/ — gh-ost 集成模块(同步/异步执行,任务管理) - router/router.go — API 路由注册(/api/v1/...) gh-ost 集成: - RunGhOst() 同步执行 - RunGhOstAsync() 异步执行 + 进度追踪 - TaskManager 管理多个并发任务 - API: POST /ghost/dry-run, /ghost/execute, GET /ghost/status/:taskId 核心 API 已对齐: - POST /auth/login (JWT) - GET /dashboard/stats, /dashboard/my-tickets - GET /tickets/statistics, CRUD + approve/execute - GET/POST/PUT/DELETE /instances - GET /users, /audit-rules, /announcements, /settings 已验证: - 登录 admin/admin123 ✅ - Dashboard 99工单/110查询/4实例 ✅ - 工单统计按角色过滤 ✅
This commit is contained in:
1 parent
43d1f3a234
commit
ca308e277d
202 files changed
+2265
-51181
No files matched your search
@@ -0,0 +1,266 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"sql-platform/internal/middleware"
|
||||
"sql-platform/internal/model"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// GET /api/v1/tickets/statistics
|
||||
func TicketStatistics(c *gin.Context) {
|
||||
userID, _, role := middleware.GetCurrentUser(c)
|
||||
|
||||
type statusCount struct {
|
||||
Status string `json:"status"`
|
||||
Count int64 `json:"count"`
|
||||
}
|
||||
var counts []statusCount
|
||||
|
||||
query := model.DB.Model(&model.Ticket{}).Select("status, count(*) as count")
|
||||
|
||||
// Filter by user role
|
||||
switch role {
|
||||
case "developer":
|
||||
query = query.Where("applicant_id = ?", userID)
|
||||
case "dba", "auditor":
|
||||
// DBA sees tickets they submitted + tickets they need to approve
|
||||
query = query.Where(
|
||||
"applicant_id = ? OR id IN (SELECT ticket_id FROM ticket_approvals WHERE approver_id = ?)",
|
||||
userID, userID,
|
||||
)
|
||||
// admin sees everything
|
||||
}
|
||||
|
||||
query.Group("status").Scan(&counts)
|
||||
|
||||
stats := model.TicketStatistics{}
|
||||
for _, c := range counts {
|
||||
switch c.Status {
|
||||
case "pending":
|
||||
stats.Pending = int(c.Count)
|
||||
case "reviewing":
|
||||
stats.Reviewing = int(c.Count)
|
||||
case "executing":
|
||||
stats.Executing = int(c.Count)
|
||||
case "completed":
|
||||
stats.Completed = int(c.Count)
|
||||
case "failed":
|
||||
stats.Failed = int(c.Count)
|
||||
case "rejected":
|
||||
stats.Rejected = int(c.Count)
|
||||
}
|
||||
}
|
||||
stats.Total = stats.Pending + stats.Reviewing + stats.Executing + stats.Completed + stats.Failed + stats.Rejected
|
||||
|
||||
c.JSON(http.StatusOK, stats)
|
||||
}
|
||||
|
||||
// GET /api/v1/tickets
|
||||
func ListTickets(c *gin.Context) {
|
||||
userID, _, role := middleware.GetCurrentUser(c)
|
||||
status := c.Query("status")
|
||||
page := 1
|
||||
limit := 20
|
||||
|
||||
query := model.DB.Model(&model.Ticket{}).Order("id DESC")
|
||||
|
||||
// Filter by user role
|
||||
switch role {
|
||||
case "developer":
|
||||
query = query.Where("applicant_id = ?", userID)
|
||||
case "dba", "auditor":
|
||||
query = query.Where(
|
||||
"applicant_id = ? OR id IN (SELECT ticket_id FROM ticket_approvals WHERE approver_id = ?)",
|
||||
userID, userID,
|
||||
)
|
||||
}
|
||||
|
||||
if status != "" && status != "all" {
|
||||
query = query.Where("status = ?", status)
|
||||
}
|
||||
|
||||
var total int64
|
||||
query.Count(&total)
|
||||
|
||||
var tickets []model.Ticket
|
||||
query.Offset((page - 1) * limit).Limit(limit).Find(&tickets)
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"items": tickets,
|
||||
"total": total,
|
||||
"page": page,
|
||||
"limit": limit,
|
||||
})
|
||||
}
|
||||
|
||||
// GET /api/v1/tickets/:id
|
||||
func GetTicket(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
var ticket model.Ticket
|
||||
if err := model.DB.First(&ticket, id).Error; err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"detail": "Ticket not found"})
|
||||
return
|
||||
}
|
||||
|
||||
// Get approvals
|
||||
var approvals []model.TicketApproval
|
||||
model.DB.Where("ticket_id = ?", id).Order("step_number").Find(&approvals)
|
||||
|
||||
// Get SQL items
|
||||
var sqlItems []model.TicketSQLItem
|
||||
model.DB.Where("ticket_id = ?", id).Order("seq").Find(&sqlItems)
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"ticket": ticket,
|
||||
"approvals": approvals,
|
||||
"sql_items": sqlItems,
|
||||
})
|
||||
}
|
||||
|
||||
// POST /api/v1/tickets
|
||||
func CreateTicket(c *gin.Context) {
|
||||
userID, _, _ := middleware.GetCurrentUser(c)
|
||||
|
||||
var input struct {
|
||||
Title string `json:"title" binding:"required"`
|
||||
DBType string `json:"db_type" binding:"required"`
|
||||
InstanceID int `json:"instance_id" binding:"required"`
|
||||
TargetDatabase string `json:"target_database" binding:"required"`
|
||||
SQLContent string `json:"sql_content" binding:"required"`
|
||||
RollbackSQL *string `json:"rollback_sql"`
|
||||
Priority *string `json:"priority"`
|
||||
Reason *string `json:"reason"`
|
||||
WorkflowID *int `json:"workflow_id"`
|
||||
RiskLevel *string `json:"risk_level"`
|
||||
ExecStrategy *string `json:"exec_strategy"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&input); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"detail": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
status := "pending"
|
||||
priority := "medium"
|
||||
riskLevel := "low"
|
||||
execStrategy := "stop_on_error"
|
||||
|
||||
if input.Priority != nil {
|
||||
priority = *input.Priority
|
||||
}
|
||||
if input.RiskLevel != nil {
|
||||
riskLevel = *input.RiskLevel
|
||||
}
|
||||
if input.ExecStrategy != nil {
|
||||
execStrategy = *input.ExecStrategy
|
||||
}
|
||||
|
||||
// Generate ticket number
|
||||
now := time.Now()
|
||||
ticketNo := now.Format("20060102") + "-" + fmt.Sprintf("%04d", time.Now().UnixNano()%10000)
|
||||
|
||||
ticket := model.Ticket{
|
||||
Title: input.Title,
|
||||
DBType: input.DBType,
|
||||
InstanceID: input.InstanceID,
|
||||
TargetDatabase: input.TargetDatabase,
|
||||
SQLContent: input.SQLContent,
|
||||
RollbackSQL: input.RollbackSQL,
|
||||
Status: &status,
|
||||
Priority: &priority,
|
||||
ApplicantID: userID,
|
||||
Reason: input.Reason,
|
||||
WorkflowID: input.WorkflowID,
|
||||
RiskLevel: &riskLevel,
|
||||
ExecStrategy: &execStrategy,
|
||||
TicketNo: &ticketNo,
|
||||
}
|
||||
|
||||
if err := model.DB.Create(&ticket).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"detail": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusCreated, ticket)
|
||||
}
|
||||
|
||||
// POST /api/v1/tickets/:id/approve
|
||||
func ApproveTicket(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
userID, _, _ := middleware.GetCurrentUser(c)
|
||||
|
||||
var ticket model.Ticket
|
||||
if err := model.DB.First(&ticket, id).Error; err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"detail": "Ticket not found"})
|
||||
return
|
||||
}
|
||||
|
||||
var input struct {
|
||||
Action string `json:"action" binding:"required"` // approve, reject
|
||||
Comment *string `json:"comment"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&input); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"detail": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
|
||||
// Update approval record
|
||||
var approval model.TicketApproval
|
||||
if err := model.DB.Where("ticket_id = ? AND approver_id = ? AND action IS NULL", id, userID).First(&approval).Error; err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"detail": "No pending approval for you"})
|
||||
return
|
||||
}
|
||||
|
||||
approval.Action = &input.Action
|
||||
approval.Comment = input.Comment
|
||||
approval.ActualApproverID = &userID
|
||||
approval.CompletedAt = &now
|
||||
model.DB.Save(&approval)
|
||||
|
||||
// Update ticket status
|
||||
newStatus := "approved"
|
||||
if input.Action == "reject" {
|
||||
newStatus = "rejected"
|
||||
} else if input.Action == "approve" {
|
||||
// Check if there are more pending approvals
|
||||
var pendingCount int64
|
||||
model.DB.Model(&model.TicketApproval{}).Where("ticket_id = ? AND action IS NULL", id).Count(&pendingCount)
|
||||
if pendingCount > 0 {
|
||||
newStatus = "reviewing"
|
||||
}
|
||||
}
|
||||
|
||||
ticket.Status = &newStatus
|
||||
ticket.ReviewTime = &now
|
||||
model.DB.Save(&ticket)
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"ticket": ticket, "approval": approval})
|
||||
}
|
||||
|
||||
// POST /api/v1/tickets/:id/execute
|
||||
func ExecuteTicket(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
|
||||
var ticket model.Ticket
|
||||
if err := model.DB.First(&ticket, id).Error; err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"detail": "Ticket not found"})
|
||||
return
|
||||
}
|
||||
|
||||
status := "executing"
|
||||
ticket.Status = &status
|
||||
now := time.Now()
|
||||
ticket.ExecuteTime = &now
|
||||
model.DB.Save(&ticket)
|
||||
|
||||
// TODO: actual SQL execution
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "Execution started", "ticket_id": ticket.ID})
|
||||
}
|
||||
Reference in new issue
Block a user