277 lines
7.4 KiB
Go
277 lines
7.4 KiB
Go
package adminuser
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/csv"
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
|
|
"hyapp-admin-server/internal/config"
|
|
"hyapp-admin-server/internal/model"
|
|
"hyapp-admin-server/internal/modules/shared"
|
|
"hyapp-admin-server/internal/repository"
|
|
"hyapp-admin-server/internal/security"
|
|
)
|
|
|
|
const defaultInitialPassword = "Hyapp@123456"
|
|
|
|
type AdminUserService struct {
|
|
store *repository.Store
|
|
cfg config.Config
|
|
}
|
|
|
|
type CreateUserInput struct {
|
|
Username string
|
|
Name string
|
|
Team string
|
|
TeamID *uint
|
|
Status string
|
|
Password string
|
|
MFAEnabled bool
|
|
RoleIDs []uint
|
|
}
|
|
|
|
type UpdateUserInput struct {
|
|
Name *string
|
|
Team *string
|
|
TeamID *uint
|
|
Status *string
|
|
MFAEnabled *bool
|
|
RoleIDs *[]uint
|
|
}
|
|
|
|
type UserStatusInput struct {
|
|
Status string
|
|
}
|
|
|
|
type BatchUserStatusInput struct {
|
|
IDs []uint
|
|
Status string
|
|
}
|
|
|
|
type PasswordResult struct {
|
|
Password string
|
|
}
|
|
|
|
type UserExport struct {
|
|
FileName string
|
|
Content []byte
|
|
Count int
|
|
}
|
|
|
|
type MoneyScopeInput struct {
|
|
AppCode string
|
|
RegionID int64
|
|
}
|
|
|
|
type AppScopeInput struct {
|
|
Mode string
|
|
AppCodes []string
|
|
}
|
|
|
|
func NewService(store *repository.Store, cfg config.Config) *AdminUserService {
|
|
return &AdminUserService{store: store, cfg: cfg}
|
|
}
|
|
|
|
func (s *AdminUserService) ListUsers(actor shared.Actor, options repository.ListOptions) ([]model.User, int64, error) {
|
|
access, err := s.store.DataAccessForUser(actor.UserID)
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
return s.store.ListUsersWithAccess(options, access)
|
|
}
|
|
|
|
func (s *AdminUserService) CreateUser(input CreateUserInput) (*model.User, PasswordResult, error) {
|
|
password := strings.TrimSpace(input.Password)
|
|
if password == "" {
|
|
password = defaultInitialPassword
|
|
}
|
|
hash, err := security.HashPassword(password)
|
|
if err != nil {
|
|
return nil, PasswordResult{}, err
|
|
}
|
|
teamID, teamName, err := s.store.NormalizeUserTeam(input.TeamID, input.Team)
|
|
if err != nil {
|
|
return nil, PasswordResult{}, err
|
|
}
|
|
|
|
user := model.User{
|
|
Username: strings.TrimSpace(input.Username),
|
|
Name: strings.TrimSpace(input.Name),
|
|
PasswordHash: hash,
|
|
Team: teamName,
|
|
TeamID: teamID,
|
|
Status: shared.DefaultString(input.Status, model.UserStatusActive),
|
|
// 新用户先以零 App 权限落库,后续独立 scope 请求即使失败也不会产生短暂的全 App 越权窗口。
|
|
AppScopeMode: model.UserAppScopeModeNone,
|
|
MFAEnabled: input.MFAEnabled,
|
|
}
|
|
if err := s.store.CreateUser(&user, input.RoleIDs); err != nil {
|
|
return nil, PasswordResult{}, err
|
|
}
|
|
created, err := s.store.FindUserByID(user.ID)
|
|
if err != nil {
|
|
return nil, PasswordResult{}, err
|
|
}
|
|
return created, PasswordResult{Password: password}, nil
|
|
}
|
|
|
|
func (s *AdminUserService) GetUser(id uint) (*model.User, error) {
|
|
return s.store.FindUserByID(id)
|
|
}
|
|
|
|
func (s *AdminUserService) ListUserMoneyScopes(id uint) ([]model.UserMoneyScope, error) {
|
|
return s.store.ListUserMoneyScopes(id)
|
|
}
|
|
|
|
// MoneyScopeAssignment 描述某个 (App, 区域) 当前被哪位后台用户持有,用于分配弹窗把他人已占的区域置灰。
|
|
type MoneyScopeAssignment struct {
|
|
UserID uint
|
|
UserName string
|
|
UserAccount string
|
|
UserStatus string
|
|
AppCode string
|
|
RegionID int64
|
|
}
|
|
|
|
func (s *AdminUserService) ListMoneyScopeAssignments() ([]MoneyScopeAssignment, error) {
|
|
scopes, err := s.store.ListAllUserMoneyScopes()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
userIDs := make([]uint, 0, len(scopes))
|
|
for _, scope := range scopes {
|
|
userIDs = append(userIDs, scope.UserID)
|
|
}
|
|
users, err := s.store.FindUsersByIDs(userIDs)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
out := make([]MoneyScopeAssignment, 0, len(scopes))
|
|
for _, scope := range scopes {
|
|
assignment := MoneyScopeAssignment{UserID: scope.UserID, AppCode: scope.AppCode, RegionID: scope.RegionID}
|
|
if user, ok := users[scope.UserID]; ok {
|
|
assignment.UserName = user.Name
|
|
assignment.UserAccount = user.Username
|
|
assignment.UserStatus = user.Status
|
|
}
|
|
out = append(out, assignment)
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
func (s *AdminUserService) ReplaceUserMoneyScopes(id uint, input []MoneyScopeInput) ([]model.UserMoneyScope, error) {
|
|
scopes := make([]model.UserMoneyScope, 0, len(input))
|
|
for _, item := range input {
|
|
scopes = append(scopes, model.UserMoneyScope{
|
|
AppCode: strings.TrimSpace(item.AppCode),
|
|
RegionID: item.RegionID,
|
|
})
|
|
}
|
|
if err := s.store.ReplaceUserMoneyScopes(id, scopes); err != nil {
|
|
return nil, err
|
|
}
|
|
return s.store.ListUserMoneyScopes(id)
|
|
}
|
|
|
|
func (s *AdminUserService) GetUserAppScopes(id uint) (repository.AppAccess, error) {
|
|
return s.store.AppAccessForUser(id)
|
|
}
|
|
|
|
func (s *AdminUserService) ReplaceUserAppScopes(id uint, input AppScopeInput) (repository.AppAccess, error) {
|
|
return s.store.ReplaceUserAppScopes(id, input.Mode, input.AppCodes)
|
|
}
|
|
|
|
func (s *AdminUserService) UpdateUser(id uint, input UpdateUserInput) (*model.User, error) {
|
|
updates := map[string]any{}
|
|
if input.Name != nil {
|
|
updates["name"] = strings.TrimSpace(*input.Name)
|
|
}
|
|
if input.Team != nil {
|
|
teamID, teamName, err := s.store.NormalizeUserTeam(input.TeamID, *input.Team)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
updates["team"] = teamName
|
|
updates["team_id"] = teamID
|
|
} else if input.TeamID != nil {
|
|
teamID, teamName, err := s.store.NormalizeUserTeam(input.TeamID, "")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
updates["team"] = teamName
|
|
updates["team_id"] = teamID
|
|
}
|
|
if input.Status != nil {
|
|
updates["status"] = strings.TrimSpace(*input.Status)
|
|
}
|
|
if input.MFAEnabled != nil {
|
|
updates["mfa_enabled"] = *input.MFAEnabled
|
|
}
|
|
return s.store.UpdateUser(id, updates, input.RoleIDs)
|
|
}
|
|
|
|
func (s *AdminUserService) UpdateUserStatus(id uint, input UserStatusInput) (*model.User, error) {
|
|
return s.store.UpdateUserStatus(id, input.Status)
|
|
}
|
|
|
|
func (s *AdminUserService) ResetUserPassword(id uint, password string) (PasswordResult, error) {
|
|
value := strings.TrimSpace(password)
|
|
if value == "" {
|
|
value = defaultInitialPassword
|
|
}
|
|
if err := s.store.ResetUserPassword(id, value); err != nil {
|
|
return PasswordResult{}, err
|
|
}
|
|
return PasswordResult{Password: value}, nil
|
|
}
|
|
|
|
func (s *AdminUserService) BatchUpdateUserStatus(input BatchUserStatusInput) error {
|
|
return s.store.BatchUpdateUserStatus(input.IDs, input.Status)
|
|
}
|
|
|
|
func (s *AdminUserService) ExportUsers(actor shared.Actor, options repository.ListOptions) (UserExport, error) {
|
|
access, err := s.store.DataAccessForUser(actor.UserID)
|
|
if err != nil {
|
|
return UserExport{}, err
|
|
}
|
|
users, err := s.store.ExportUsersWithAccess(options, access)
|
|
if err != nil {
|
|
return UserExport{}, err
|
|
}
|
|
|
|
var buf bytes.Buffer
|
|
writer := csv.NewWriter(&buf)
|
|
_ = writer.Write([]string{"ID", "账号", "姓名", "角色", "团队", "状态", "MFA", "最后登录"})
|
|
for _, user := range users {
|
|
lastLogin := ""
|
|
if user.LastLoginAtMS != nil {
|
|
lastLogin = time.UnixMilli(*user.LastLoginAtMS).UTC().Format("2006-01-02 15:04:05")
|
|
}
|
|
_ = writer.Write([]string{
|
|
fmt.Sprintf("%d", user.ID),
|
|
user.Username,
|
|
user.Name,
|
|
strings.Join(roleNames(user.Roles), "、"),
|
|
shared.UserTeamName(user),
|
|
user.Status,
|
|
shared.BoolText(user.MFAEnabled),
|
|
lastLogin,
|
|
})
|
|
}
|
|
writer.Flush()
|
|
if err := writer.Error(); err != nil {
|
|
return UserExport{}, err
|
|
}
|
|
return UserExport{FileName: "hyapp-users.csv", Content: buf.Bytes(), Count: len(users)}, nil
|
|
}
|
|
|
|
func roleNames(roles []model.Role) []string {
|
|
names := make([]string, 0, len(roles))
|
|
for _, role := range roles {
|
|
names = append(names, role.Name)
|
|
}
|
|
return names
|
|
}
|