refactor(*): 平台架构 - 拆分后端领域模块与画布前端组件并补充测试
This commit is contained in:
1 parent
199e9190dc
commit
b1d854ee51
186 files changed
+33727
-29905
No files matched your search
@@ -0,0 +1,334 @@
|
||||
// 管理端用户管理:列表、创建、更新、删除与批量停用。
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
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
|
||||
}
|
||||
Reference in new issue
Block a user