修复项目无法删除问题

This commit is contained in:
2026-08-13 20:42:09 +08:00
parent cb88a69012
commit a407e69ba7
18 changed files with 623 additions and 63 deletions

View File

@@ -5,6 +5,7 @@ import (
"errors"
"net/http"
"regexp"
"slices"
"strings"
"time"
_ "time/tzdata"
@@ -111,7 +112,7 @@ func (s *Server) updateProject(w http.ResponseWriter, r *http.Request) {
}
var project model.Project
err = s.db.WithContext(r.Context()).Transaction(func(tx *gorm.DB) error {
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).First(&project, "id = ?", projectID).Error; err != nil {
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).First(&project, "id = ? AND archived_at IS NULL", projectID).Error; err != nil {
return mapNotFound(err, "PROJECT_NOT_FOUND", "项目不存在")
}
before := projectView(project)
@@ -141,7 +142,7 @@ func (s *Server) deleteProject(w http.ResponseWriter, r *http.Request) {
err := s.db.WithContext(r.Context()).Transaction(func(tx *gorm.DB) error {
var project model.Project
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).First(&project, "id = ?", projectID).Error; err != nil {
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).First(&project, "id = ? AND archived_at IS NULL", projectID).Error; err != nil {
return mapNotFound(err, "PROJECT_NOT_FOUND", "项目不存在")
}
@@ -150,11 +151,19 @@ func (s *Server) deleteProject(w http.ResponseWriter, r *http.Request) {
return err
}
if historyCount > 0 {
return &apiError{
Status: http.StatusConflict,
Code: "PROJECT_HAS_HISTORY",
Message: "该项目已有排队运营历史,不能删除;如需停止使用,请结束该项目。",
if project.Status != model.ProjectEnded {
return &apiError{Status: http.StatusConflict, Code: "PROJECT_MUST_BE_ENDED", Message: "请先结束项目,再归档并保留历史记录。"}
}
now := s.now()
if err := tx.Model(&project).Updates(map[string]any{"archived_at": now, "updated_at": now}).Error; err != nil {
return err
}
if err := tx.Where("project_id = ?", projectID).Delete(&model.UserProject{}).Error; err != nil {
return err
}
actor := currentPrincipal(r.Context()).User
return s.addAudit(tx, r, &projectID, &actor.ID, "PROJECT_ARCHIVED", "PROJECT", &projectID,
map[string]any{"project": projectView(project), "archived_at": now})
}
if err := tx.Model(&model.AuditEntry{}).
@@ -191,7 +200,10 @@ func (s *Server) adminUsers(w http.ResponseWriter, r *http.Request) {
return
}
var grants []model.UserProject
if err := s.db.WithContext(r.Context()).Find(&grants).Error; err != nil {
if err := s.db.WithContext(r.Context()).Model(&model.UserProject{}).
Select("user_projects.*").
Joins("JOIN projects AS granted_project ON granted_project.id = user_projects.project_id AND granted_project.archived_at IS NULL").
Find(&grants).Error; err != nil {
writeError(w, err)
return
}
@@ -237,9 +249,27 @@ func validateAdminUserRequest(input adminUserRequest, creating bool) (adminUserR
return input, &apiError{Status: 422, Code: "INVALID_PROJECT", Message: "所属项目无效"}
}
}
slices.Sort(input.ProjectIDs)
input.ProjectIDs = slices.Compact(input.ProjectIDs)
return input, nil
}
func lockActiveProjects(tx *gorm.DB, projectIDs []string) error {
if len(projectIDs) == 0 {
return nil
}
var projects []model.Project
if err := tx.Clauses(clause.Locking{Strength: "KEY SHARE"}).
Where("id IN ? AND archived_at IS NULL", projectIDs).
Order("id ASC").Find(&projects).Error; err != nil {
return err
}
if len(projects) != len(projectIDs) {
return &apiError{Status: http.StatusUnprocessableEntity, Code: "INVALID_PROJECT", Message: "所属项目无效"}
}
return nil
}
func (s *Server) createAdminUser(w http.ResponseWriter, r *http.Request) {
var input adminUserRequest
if err := decodeJSON(r, &input); err != nil {
@@ -265,6 +295,9 @@ func (s *Server) createAdminUser(w http.ResponseWriter, r *http.Request) {
}
user := model.User{ID: uuid.NewString(), Username: input.Username, PasswordHash: hash, Role: input.Role, Active: active, CreatedAt: s.now(), UpdatedAt: s.now()}
err = s.db.WithContext(r.Context()).Transaction(func(tx *gorm.DB) error {
if err := lockActiveProjects(tx, input.ProjectIDs); err != nil {
return err
}
if err := tx.Create(&user).Error; err != nil {
return &apiError{Status: 409, Code: "USERNAME_EXISTS", Message: "该账号已存在"}
}
@@ -301,6 +334,9 @@ func (s *Server) updateAdminUser(w http.ResponseWriter, r *http.Request) {
}
var user model.User
err = s.db.WithContext(r.Context()).Transaction(func(tx *gorm.DB) error {
if err := lockActiveProjects(tx, input.ProjectIDs); err != nil {
return err
}
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).First(&user, "id = ?", id).Error; err != nil {
return mapNotFound(err, "USER_NOT_FOUND", "账号不存在")
}
@@ -392,11 +428,12 @@ func (s *Server) adminOverview(w http.ResponseWriter, r *http.Request) {
SELECT session.id
FROM queue_sessions AS session
JOIN projects AS project ON project.id = session.project_id
WHERE session.status IN ('RUNNING', 'PAUSED')
WHERE project.archived_at IS NULL
AND session.status IN ('RUNNING', 'PAUSED')
AND session.business_date = (? AT TIME ZONE project.timezone)::date
)
SELECT
(SELECT count(*) FROM projects) AS projects,
(SELECT count(*) FROM projects WHERE archived_at IS NULL) AS projects,
(SELECT count(*) FROM current_sessions) AS active_sessions,
(SELECT count(*) FROM queue_tickets AS ticket JOIN current_sessions AS session ON session.id = ticket.queue_session_id WHERE ticket.status = 'WAITING') AS waiting_tickets,
(SELECT COALESCE(sum(ticket.party_size), 0) FROM queue_tickets AS ticket JOIN current_sessions AS session ON session.id = ticket.queue_session_id WHERE ticket.status = 'WAITING') AS waiting_people,
@@ -407,12 +444,15 @@ func (s *Server) adminOverview(w http.ResponseWriter, r *http.Request) {
return
}
var projects []model.Project
if err := s.db.WithContext(r.Context()).Order("name ASC").Find(&projects).Error; err != nil {
if err := s.db.WithContext(r.Context()).Where("archived_at IS NULL").Order("name ASC").Find(&projects).Error; err != nil {
writeError(w, err)
return
}
var simulations []model.DeviceSimulation
if err := s.db.WithContext(r.Context()).Order("created_at DESC").Limit(20).Find(&simulations).Error; err != nil {
if err := s.db.WithContext(r.Context()).Model(&model.DeviceSimulation{}).
Select("device_simulations.*").
Joins("JOIN projects AS simulation_project ON simulation_project.id = device_simulations.project_id AND simulation_project.archived_at IS NULL").
Order("device_simulations.created_at DESC").Limit(20).Find(&simulations).Error; err != nil {
writeError(w, err)
return
}
@@ -423,6 +463,7 @@ func (s *Server) adminOverview(w http.ResponseWriter, r *http.Request) {
Joins("JOIN projects AS current_project ON current_project.id = queue_tickets.project_id").
Where("queue_tickets.status IN ?", []string{model.TicketWaiting, model.TicketCalled, model.TicketArrived}).
Where("current_session.status IN ?", []string{"RUNNING", "PAUSED"}).
Where("current_project.archived_at IS NULL").
Where("current_session.business_date = (? AT TIME ZONE current_project.timezone)::date", s.now()).
Order("queue_tickets.created_at ASC").Find(&activeTickets).Error; err != nil {
writeError(w, err)
@@ -636,7 +677,7 @@ func (s *Server) updateProjectSettings(w http.ResponseWriter, r *http.Request) {
user := currentPrincipal(r.Context()).User
var project model.Project
err = s.db.WithContext(r.Context()).Transaction(func(tx *gorm.DB) error {
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).First(&project, "id = ?", projectID).Error; err != nil {
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).First(&project, "id = ? AND archived_at IS NULL", projectID).Error; err != nil {
return mapNotFound(err, "PROJECT_NOT_FOUND", "项目不存在")
}
before := projectView(project)