修复项目无法删除问题
This commit is contained in:
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user