package administration import ( "context" "crypto/rand" "encoding/hex" "fmt" "strings" "time" ) type Service struct { store Store id func(string) string now func() time.Time hash func(string) (PasswordHash, error) } type Option func(*Service) func WithIDGenerator(fn func(string) string) Option { return func(s *Service) { s.id = fn } } func WithClock(fn func() time.Time) Option { return func(s *Service) { s.now = fn } } func WithPasswordHasher(fn func(string) (PasswordHash, error)) Option { return func(s *Service) { s.hash = fn } } func NewService(store Store, options ...Option) *Service { s := &Service{store: store, id: randomID, now: time.Now, hash: HashPassword} for _, option := range options { option(s) } return s } func randomID(prefix string) string { raw := make([]byte, 12) if _, err := rand.Read(raw); err != nil { return fmt.Sprintf("%s-%d", prefix, time.Now().UnixNano()) } return prefix + "-" + hex.EncodeToString(raw) } func (s *Service) ListAccounts(ctx context.Context, actor Actor, filters AccountFilters) ([]AccountProjection, error) { if actor.Role == RoleOrganizationAdmin { if actor.OrganizationID == "" { return nil, problem(ErrorForbidden, "当前账号没有组织归属。") } filters.OrganizationID, filters.Role = actor.OrganizationID, RoleUser } else if actor.Role != RoleSuperAdmin { return nil, problem(ErrorForbidden, "需要管理员权限。") } accounts, err := s.store.ListAccounts(ctx, filters) if err != nil { return nil, infrastructure("list accounts", err) } out := make([]AccountProjection, len(accounts)) for i, a := range accounts { out[i] = ProjectAccount(a) } return out, nil } func (s *Service) CreateAccount(ctx context.Context, actor Actor, input CreateAccountInput) (Account, error) { input.Phone = NormalizePhone(input.Phone) input.DisplayName = strings.TrimSpace(input.DisplayName) if !ValidPhone(input.Phone) { return Account{}, problem(ErrorValidation, "手机号格式不正确。") } if input.DisplayName == "" { return Account{}, problem(ErrorValidation, "显示名称不能为空。") } if len(input.Password) < 8 { return Account{}, problem(ErrorValidation, "初始密码至少需要 8 位。") } if !validRole(input.Role) { return Account{}, problem(ErrorValidation, "账号角色不正确。") } if actor.Role == RoleOrganizationAdmin { if input.Role != RoleUser { return Account{}, problem(ErrorForbidden, "组织管理员只能创建普通用户。") } if actor.OrganizationID == "" { return Account{}, problem(ErrorForbidden, "当前账号没有组织归属。") } input.OrganizationID = actor.OrganizationID } else if actor.Role != RoleSuperAdmin { return Account{}, problem(ErrorForbidden, "需要管理员权限。") } if err := s.validateMembership(ctx, input.Role, input.OrganizationID); err != nil { return Account{}, err } hashed, err := s.hash(input.Password) if err != nil { return Account{}, infrastructure("hash password", err) } now := s.now() a := Account{ID: s.id("user"), Phone: input.Phone, DisplayName: input.DisplayName, Role: input.Role, OrganizationID: input.OrganizationID, Status: StatusActive, PasswordHash: hashed.Hash, PasswordSalt: hashed.Salt, SessionVersion: 1, LegacySubject: strings.TrimSpace(input.LegacySubject), CreatedAt: now, UpdatedAt: now} created, err := s.store.CreateAccount(ctx, a) return created, infrastructure("create account", err) } func (s *Service) UpdateAccount(ctx context.Context, actor Actor, id string, patch UpdateAccountInput) (Account, error) { current, found, err := s.store.GetAccount(ctx, id) if err != nil { return Account{}, infrastructure("get account", err) } if !found { return Account{}, problem(ErrorNotFound, "账号不存在。") } if err := AuthorizeAccountTarget(actor, current); err != nil { return Account{}, err } if actor.Role != RoleSuperAdmin && ((patch.Role != nil && *patch.Role != current.Role) || patch.OrganizationID != nil) { return Account{}, problem(ErrorForbidden, "组织管理员不能修改账号角色或归属。") } next := current update := AccountUpdate{Actor: actor} mutates := false if patch.DisplayName != nil { name := strings.TrimSpace(*patch.DisplayName) if name == "" { return Account{}, problem(ErrorValidation, "显示名称不能为空。") } next.DisplayName = name update.DisplayName = &name } if patch.Role != nil { if !validRole(*patch.Role) { return Account{}, problem(ErrorValidation, "账号角色不正确。") } next.Role = *patch.Role update.Role = patch.Role mutates = true } if patch.OrganizationID != nil { next.OrganizationID = strings.TrimSpace(*patch.OrganizationID) organizationID := next.OrganizationID update.OrganizationID = &organizationID mutates = true } if patch.Status != nil { if !validStatus(*patch.Status) { return Account{}, problem(ErrorValidation, "账号状态不正确。") } next.Status = *patch.Status update.Status = patch.Status mutates = true } if err := s.validateMembership(ctx, next.Role, next.OrganizationID); err != nil { return Account{}, err } if patch.Password != nil { if len(*patch.Password) < 8 { return Account{}, problem(ErrorValidation, "新密码至少需要 8 位。") } hashed, err := s.hash(*patch.Password) if err != nil { return Account{}, infrastructure("hash password", err) } next.PasswordHash, next.PasswordSalt = hashed.Hash, hashed.Salt update.PasswordHash = &hashed mutates = true } if patch.ClearLoginLock { next.FailedLoginCount, next.LockedUntil = 0, nil update.ClearLoginLock = true } if mutates { next.SessionVersion++ update.IncrementSessionVersion = true } next.UpdatedAt = s.now() update.UpdatedAt = next.UpdatedAt if atomic, ok := s.store.(AtomicAccountUpdater); ok { updated, err := atomic.ApplyAccountUpdate(ctx, id, update) return updated, infrastructure("update account", err) } updated, err := s.store.UpdateAccount(ctx, next) return updated, infrastructure("update account", err) } func (s *Service) SetAccountStatus(ctx context.Context, actor Actor, id string, status Status) (Account, error) { if actor.ID == id && status == StatusDisabled { return Account{}, problem(ErrorValidation, "不能停用当前登录账号。") } return s.UpdateAccount(ctx, actor, id, UpdateAccountInput{Status: &status, ClearLoginLock: status == StatusActive}) } func (s *Service) ResetPassword(ctx context.Context, actor Actor, id, password string) (Account, error) { return s.UpdateAccount(ctx, actor, id, UpdateAccountInput{Password: &password, ClearLoginLock: true}) } func (s *Service) DeleteAccount(ctx context.Context, actor Actor, id string) (string, error) { current, found, err := s.store.GetAccount(ctx, id) if err != nil { return "", infrastructure("get account", err) } if !found { return "", problem(ErrorNotFound, "账号不存在。") } if err := AuthorizeAccountTarget(actor, current); err != nil { return "", err } if actor.ID == id { return "", problem(ErrorValidation, "不能删除当前登录账号。") } if current.Role == RoleSuperAdmin { return "", problem(ErrorValidation, "不能直接删除超级管理员账号。") } archive := "archive:global" if current.OrganizationID != "" { org, found, err := s.store.GetOrganization(ctx, current.OrganizationID) if err != nil { return "", infrastructure("get organization", err) } if found { archive = org.ArchiveOwnerID } } if err := s.store.DeleteAccount(ctx, id, archive); err != nil { return "", infrastructure("delete account", err) } return archive, nil } func (s *Service) validateMembership(ctx context.Context, role Role, organizationID string) error { if role == RoleSuperAdmin && organizationID == "" { return nil } if organizationID == "" { return problem(ErrorValidation, "普通账号必须归属有效组织。") } org, found, err := s.store.GetOrganization(ctx, organizationID) if err != nil { return infrastructure("get organization", err) } if !found || org.Status != StatusActive { return problem(ErrorValidation, "账号归属的组织不存在或已停用。") } return nil }