Files
ttstd-infinite-canvas/backend/internal/repository/admin_audit.go

161 lines
5.7 KiB
Go

package repository
import (
"errors"
"time"
"yingce/backend/internal/model"
"gorm.io/gorm"
"gorm.io/gorm/clause"
)
var (
ErrBulkUserNotFound = errors.New("所选用户中有不存在的账号")
ErrBulkCurrentAdmin = errors.New("批量操作不能包含当前管理员自己")
ErrBulkLastActiveAdmin = errors.New("不能移除最后一个可用的管理员")
)
type AdminUserCounts struct {
LedgerEntries int64 `json:"ledgerEntries"`
Tasks int64 `json:"tasks"`
APICalls int64 `json:"apiCalls"`
AuditEvents int64 `json:"auditEvents"`
RechargeMicrocredits int64 `json:"rechargeMicrocredits"`
CheckinMicrocredits int64 `json:"checkinMicrocredits"`
}
func (r *Repository) AppendAdminAudit(event *model.AdminAuditEvent) error {
return r.db.Create(event).Error
}
// BulkDisableUsers 把管理员保留校验、会话清理、状态更新和审计写入放在同一事务中,避免部分停用。
func (r *Repository) BulkDisableUsers(actorID string, userIDs []string, events []model.AdminAuditEvent, now time.Time) ([]model.User, error) {
var users []model.User
err := r.db.Transaction(func(tx *gorm.DB) error {
query := tx.Where("id IN ?", userIDs)
if r.Dialect() == "postgres" {
query = query.Clauses(clause.Locking{Strength: "UPDATE"})
}
if err := query.Find(&users).Error; err != nil {
return err
}
if len(users) != len(userIDs) {
return ErrBulkUserNotFound
}
for _, user := range users {
if user.ID == actorID {
return ErrBulkCurrentAdmin
}
}
var remainingAdmins int64
if err := tx.Model(&model.User{}).
Where("role = ? AND status = ? AND id NOT IN ?", model.UserRoleAdmin, model.UserStatusActive, userIDs).
Count(&remainingAdmins).Error; err != nil {
return err
}
if remainingAdmins == 0 {
return ErrBulkLastActiveAdmin
}
if err := tx.Delete(&model.AuthSession{}, "user_id IN ?", userIDs).Error; err != nil {
return err
}
if err := tx.Delete(&model.TaskTextDelta{}, "user_id IN ?", userIDs).Error; err != nil {
return err
}
if err := tx.Model(&model.User{}).Where("id IN ?", userIDs).Updates(map[string]any{"status": model.UserStatusDisabled, "updated_at": now}).Error; err != nil {
return err
}
if len(events) > 0 {
if err := tx.Create(&events).Error; err != nil {
return err
}
}
for index := range users {
users[index].Status = model.UserStatusDisabled
users[index].UpdatedAt = now
}
return nil
})
return users, err
}
func (r *Repository) AdminAuditEvents(targetType string, targetID string, limit int, offset int) ([]model.AdminAuditEvent, int64, error) {
query := r.db.Model(&model.AdminAuditEvent{})
if targetType != "" {
query = query.Where("target_type = ?", targetType)
}
if targetID != "" {
query = query.Where("target_id = ?", targetID)
}
var total int64
if err := query.Count(&total).Error; err != nil {
return nil, 0, err
}
var events []model.AdminAuditEvent
err := query.Order("created_at desc").Limit(limit).Offset(offset).Find(&events).Error
return events, total, err
}
func (r *Repository) AdminUserCounts(userID string) (AdminUserCounts, error) {
var counts AdminUserCounts
queries := []struct {
model any
where string
value *int64
}{
{&model.CreditLedgerEntry{}, "user_id = ?", &counts.LedgerEntries},
{&model.Task{}, "user_id = ?", &counts.Tasks},
{&model.ApiCallLog{}, "user_id = ?", &counts.APICalls},
{&model.AdminAuditEvent{}, "target_type = 'user' AND target_id = ?", &counts.AuditEvents},
}
for _, query := range queries {
if err := r.db.Model(query.model).Where(query.where, userID).Count(query.value).Error; err != nil {
return AdminUserCounts{}, err
}
}
for entryType, target := range map[model.CreditLedgerType]*int64{
model.CreditLedgerPaymentTopup: &counts.RechargeMicrocredits,
model.CreditLedgerCheckinBonus: &counts.CheckinMicrocredits,
} {
if err := r.db.Model(&model.CreditLedgerEntry{}).Where("user_id = ? AND type = ?", userID, entryType).Select("COALESCE(SUM(amount_microcredits), 0)").Scan(target).Error; err != nil {
return AdminUserCounts{}, err
}
}
return counts, nil
}
func (r *Repository) AdminUserTasks(userID string, limit int, offset int) ([]model.Task, int64, error) {
query := r.db.Model(&model.Task{}).Where("user_id = ?", userID)
var total int64
if err := query.Count(&total).Error; err != nil {
return nil, 0, err
}
var tasks []model.Task
err := query.Select("id", "user_id", "project_id", "type", "status", "stage", "progress", "operation", "provider", "model", "billing_order_id", "provider_request_id", "poll_stage", "attempts", "started_at", "completed_at", "created_at", "updated_at").
Order("created_at desc").Limit(limit).Offset(offset).Find(&tasks).Error
return tasks, total, err
}
func (r *Repository) DisableRedeemBatch(batchID string, now time.Time) (int64, error) {
result := r.db.Model(&model.RedeemCode{}).
Where("batch_id = ? AND status = ? AND (expires_at IS NULL OR expires_at > ?)", batchID, model.RedeemCodeUnused, now).
Updates(map[string]any{"status": model.RedeemCodeDisabled, "updated_at": now})
return result.RowsAffected, result.Error
}
func (r *Repository) DisableRedeemCode(batchID string, codeID string, now time.Time) (bool, error) {
result := r.db.Model(&model.RedeemCode{}).
Where("id = ? AND batch_id = ? AND status = ? AND (expires_at IS NULL OR expires_at > ?)", codeID, batchID, model.RedeemCodeUnused, now).
Updates(map[string]any{"status": model.RedeemCodeDisabled, "updated_at": now})
return result.RowsAffected == 1, result.Error
}
func (r *Repository) APICallLog(id string) (*model.ApiCallLog, error) {
var log model.ApiCallLog
if err := r.db.First(&log, "id = ?", id).Error; err != nil {
return nil, err
}
return &log, nil
}