340 lines
11 KiB
Go
340 lines
11 KiB
Go
// 管理端用户管理:列表、创建、更新、删除与批量停用。
|
|
|
|
package app
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
|
|
"gorm.io/gorm"
|
|
"infinite-canvas/backend/internal/model"
|
|
"infinite-canvas/backend/internal/repository"
|
|
)
|
|
|
|
func (s *Service) RequireAdmin(user *model.User) error {
|
|
if user == nil {
|
|
return Unauthorized("请先登录")
|
|
}
|
|
if user.Role != model.UserRoleAdmin {
|
|
return Forbidden("需要管理员权限")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *Service) AdminUsers(actor *model.User, query AdminListQuery) (*AdminUserPage, error) {
|
|
if err := s.RequireAdmin(actor); err != nil {
|
|
return nil, err
|
|
}
|
|
page, limit := normalizeAdminPage(query.Page, query.Limit)
|
|
users, total, err := s.repo.AdminUsers(query.Keyword, model.UserRole(query.Type), model.UserStatus(query.Status), limit, (page-1)*limit)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
userIDs := make([]string, 0, len(users))
|
|
for _, user := range users {
|
|
userIDs = append(userIDs, user.ID)
|
|
}
|
|
accounts, err := s.repo.CreditAccounts(userIDs)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
accountByUserID := make(map[string]model.CreditAccount, len(accounts))
|
|
for _, account := range accounts {
|
|
accountByUserID[account.UserID] = account
|
|
}
|
|
result := make([]AdminUser, 0, len(users))
|
|
for _, user := range users {
|
|
account := accountByUserID[user.ID]
|
|
result = append(result, AdminUser{User: user, AvailableMicrocredits: account.AvailableMicrocredits, ReservedMicrocredits: account.ReservedMicrocredits})
|
|
}
|
|
return &AdminUserPage{Users: result, Total: total, Page: page, Limit: limit}, nil
|
|
}
|
|
|
|
func (s *Service) AdminReferences(actor *model.User) (*AdminReferenceData, error) {
|
|
if err := s.RequireAdmin(actor); err != nil {
|
|
return nil, err
|
|
}
|
|
users, err := s.repo.AdminUserReferences()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
channels, err := s.repo.AdminSystemChannelReferences()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
channelIDs := make([]string, 0, len(channels))
|
|
for _, channel := range channels {
|
|
channelIDs = append(channelIDs, channel.ID)
|
|
}
|
|
channelModels, err := s.repo.ChannelModelReferences(channelIDs)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
modelsByChannel := make(map[string][]model.ChannelModel, len(channels))
|
|
for _, item := range channelModels {
|
|
modelsByChannel[item.ChannelID] = append(modelsByChannel[item.ChannelID], item)
|
|
}
|
|
result := &AdminReferenceData{
|
|
Users: make([]AdminUserReference, 0, len(users)),
|
|
Channels: make([]AdminChannelReference, 0, len(channels)),
|
|
}
|
|
for _, user := range users {
|
|
result.Users = append(result.Users, AdminUserReference{ID: user.ID, Username: user.Username, DisplayName: user.DisplayName})
|
|
}
|
|
for _, channel := range channels {
|
|
items := modelsByChannel[channel.ID]
|
|
models := make([]string, 0, len(items))
|
|
displayNames := make([]string, 0, len(items))
|
|
for _, item := range items {
|
|
if item.Enabled {
|
|
models = append(models, item.ModelKey)
|
|
}
|
|
displayNames = append(displayNames, firstNonEmpty(strings.TrimSpace(item.DisplayName), item.ModelKey))
|
|
}
|
|
result.Channels = append(result.Channels, AdminChannelReference{ID: channel.ID, Name: channel.Name, Enabled: channel.Enabled, Models: uniqueNonEmpty(models), ModelDisplayNames: uniqueNonEmpty(displayNames)})
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
func (s *Service) CreateAdminUser(actor *model.User, req CreateAdminUserRequest) (*AdminUser, error) {
|
|
if err := s.RequireAdmin(actor); err != nil {
|
|
return nil, err
|
|
}
|
|
username := normalizeUsername(req.Username)
|
|
email := normalizeEmail(req.Email)
|
|
displayName := normalizeDisplayName(req.DisplayName, username)
|
|
if err := validateUsername(username); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := validatePassword(req.Password); err != nil {
|
|
return nil, err
|
|
}
|
|
if email != "" {
|
|
if err := validateEmail(email); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
if req.Role != model.UserRoleAdmin && req.Role != model.UserRoleUser {
|
|
return nil, BadAuthRequest("\u7528\u6237\u89d2\u8272\u65e0\u6548")
|
|
}
|
|
if req.Status != model.UserStatusActive && req.Status != model.UserStatusDisabled {
|
|
return nil, BadAuthRequest("\u7528\u6237\u72b6\u6001\u65e0\u6548")
|
|
}
|
|
if _, err := s.repo.UserByUsername(username); err == nil {
|
|
return nil, BadAuthRequest("\u7528\u6237\u540d\u5df2\u5b58\u5728")
|
|
} else if !errors.Is(err, gorm.ErrRecordNotFound) {
|
|
return nil, err
|
|
}
|
|
if email != "" {
|
|
if _, err := s.repo.UserByEmail(email); err == nil {
|
|
return nil, BadAuthRequest("\u90ae\u7bb1\u5df2\u88ab\u6ce8\u518c")
|
|
} else if !errors.Is(err, gorm.ErrRecordNotFound) {
|
|
return nil, err
|
|
}
|
|
}
|
|
passwordHash, err := hashPassword(req.Password)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
now := time.Now()
|
|
user := &model.User{
|
|
ID: newID(),
|
|
Username: username,
|
|
Email: email,
|
|
DisplayName: displayName,
|
|
Role: req.Role,
|
|
Status: req.Status,
|
|
PasswordHash: passwordHash,
|
|
CreatedAt: now,
|
|
UpdatedAt: now,
|
|
}
|
|
if err := s.repo.Create(user); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := s.ensureSignupBonus(user.ID); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := s.appendAdminAudit(actor, "user.create", "user", user.ID, "\u521b\u5efa\u7528\u6237\u8d26\u53f7", map[string]any{"role": user.Role, "status": user.Status}); err != nil {
|
|
return nil, err
|
|
}
|
|
account, err := s.repo.CreditAccount(user.ID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &AdminUser{
|
|
User: *user,
|
|
AvailableMicrocredits: account.AvailableMicrocredits,
|
|
ReservedMicrocredits: account.ReservedMicrocredits,
|
|
}, nil
|
|
}
|
|
|
|
func (s *Service) UpdateUser(actor *model.User, userID string, req UpdateUserRequest) (*model.User, error) {
|
|
if err := s.RequireAdmin(actor); err != nil {
|
|
return nil, err
|
|
}
|
|
user, err := s.repo.User(userID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if actor.ID == user.ID && req.Status == model.UserStatusDisabled {
|
|
return nil, BadAuthRequest("不能禁用当前管理员账号")
|
|
}
|
|
nextRole := user.Role
|
|
if req.Role == model.UserRoleAdmin || req.Role == model.UserRoleUser {
|
|
nextRole = req.Role
|
|
}
|
|
nextStatus := user.Status
|
|
if req.Status == model.UserStatusActive || req.Status == model.UserStatusDisabled {
|
|
nextStatus = req.Status
|
|
}
|
|
if user.Role == model.UserRoleAdmin && nextRole != model.UserRoleAdmin {
|
|
count, err := s.repo.ActiveAdminCountExcluding(user.ID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if count == 0 {
|
|
return nil, BadAuthRequest("至少需要保留一个管理员")
|
|
}
|
|
}
|
|
if user.Role == model.UserRoleAdmin && nextStatus != model.UserStatusActive {
|
|
count, err := s.repo.ActiveAdminCountExcluding(user.ID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if count == 0 {
|
|
return nil, BadAuthRequest("至少需要保留一个可用管理员")
|
|
}
|
|
}
|
|
if strings.TrimSpace(req.DisplayName) != "" {
|
|
user.DisplayName = normalizeDisplayName(req.DisplayName, user.Username)
|
|
}
|
|
if req.Email != "" {
|
|
email := normalizeEmail(req.Email)
|
|
if err := validateEmail(email); err != nil {
|
|
return nil, err
|
|
}
|
|
existing, err := s.repo.UserByEmail(email)
|
|
if err == nil && existing.ID != user.ID {
|
|
return nil, BadAuthRequest("邮箱已被注册")
|
|
}
|
|
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
|
return nil, err
|
|
}
|
|
user.Email = email
|
|
}
|
|
if req.Password != "" {
|
|
if err := validatePassword(req.Password); err != nil {
|
|
return nil, err
|
|
}
|
|
hash, err := hashPassword(req.Password)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
user.PasswordHash = hash
|
|
if err := s.repo.DeleteUserAuthSessions(user.ID); err != nil {
|
|
return nil, fmt.Errorf("清理旧登录会话失败,密码未更新:%w", err)
|
|
}
|
|
}
|
|
if nextStatus == model.UserStatusDisabled {
|
|
if err := s.repo.DeleteUserAuthSessions(user.ID); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
user.Role = nextRole
|
|
user.Status = nextStatus
|
|
user.UpdatedAt = time.Now()
|
|
if err := s.repo.Save(user); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := s.appendAdminAudit(actor, "user.update", "user", user.ID, "更新用户账号状态或资料", map[string]any{"role": user.Role, "status": user.Status}); err != nil {
|
|
return nil, err
|
|
}
|
|
return user, nil
|
|
}
|
|
|
|
func (s *Service) DeleteUser(actor *model.User, userID string) error {
|
|
if err := s.RequireAdmin(actor); err != nil {
|
|
return err
|
|
}
|
|
if actor.ID == userID {
|
|
return BadAuthRequest("不能删除当前登录的管理员账号")
|
|
}
|
|
user, err := s.repo.User(userID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if user.Role == model.UserRoleAdmin {
|
|
count, err := s.repo.ActiveAdminCountExcluding(user.ID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if count == 0 {
|
|
return BadAuthRequest("至少需要保留一个管理员")
|
|
}
|
|
}
|
|
if err := s.repo.DeleteUserAuthSessions(user.ID); err != nil {
|
|
return err
|
|
}
|
|
if err := s.repo.DeleteUserTaskTextDeltas(user.ID); err != nil {
|
|
return err
|
|
}
|
|
// 有资金流水后必须保留用户主体,删除入口改为停用并清除全部登录态。
|
|
user.Status = model.UserStatusDisabled
|
|
user.UpdatedAt = time.Now()
|
|
if err := s.repo.Save(user); err != nil {
|
|
return err
|
|
}
|
|
return s.appendAdminAudit(actor, "user.disable", "user", user.ID, "停用用户并清除登录态", nil)
|
|
}
|
|
|
|
func (s *Service) BulkDisableUsers(actor *model.User, req BulkDisableUsersRequest) (*BulkDisableUsersResult, error) {
|
|
if err := s.RequireAdmin(actor); err != nil {
|
|
return nil, err
|
|
}
|
|
seen := make(map[string]struct{}, len(req.UserIDs))
|
|
userIDs := make([]string, 0, len(req.UserIDs))
|
|
for _, rawID := range req.UserIDs {
|
|
id := strings.TrimSpace(rawID)
|
|
if id == "" {
|
|
return nil, BadAuthRequest("用户 ID 无效")
|
|
}
|
|
if _, exists := seen[id]; exists {
|
|
continue
|
|
}
|
|
seen[id] = struct{}{}
|
|
userIDs = append(userIDs, id)
|
|
}
|
|
if len(userIDs) == 0 {
|
|
return nil, BadAuthRequest("请选择要停用的用户")
|
|
}
|
|
if len(userIDs) > 100 {
|
|
return nil, BadAuthRequest("单次最多停用 100 个用户")
|
|
}
|
|
metadata, err := json.Marshal(map[string]any{"userIds": userIDs, "count": len(userIDs)})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
now := time.Now()
|
|
events := make([]model.AdminAuditEvent, 0, len(userIDs))
|
|
for _, userID := range userIDs {
|
|
events = append(events, model.AdminAuditEvent{ID: newID(), ActorUserID: actor.ID, Action: "user.bulk_disable", TargetType: "user", TargetID: userID, Summary: "批量停用用户并清除登录态", MetadataJSON: string(metadata), CreatedAt: now})
|
|
}
|
|
users, err := s.repo.BulkDisableUsers(actor.ID, userIDs, events, now)
|
|
if errors.Is(err, repository.ErrBulkUserNotFound) {
|
|
return nil, BadAuthRequest("部分用户不存在,请刷新列表后重试")
|
|
}
|
|
if errors.Is(err, repository.ErrBulkCurrentAdmin) {
|
|
return nil, BadAuthRequest("不能停用当前登录的管理员账号")
|
|
}
|
|
if errors.Is(err, repository.ErrBulkLastActiveAdmin) {
|
|
return nil, BadAuthRequest("批量操作后至少需要保留一个可用管理员")
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &BulkDisableUsersResult{Users: users, DisabledCount: len(users)}, nil
|
|
}
|