修复:管理员默认商户与账户设置
This commit is contained in:
@@ -26,6 +26,13 @@ type LoginResult struct {
|
||||
User *model.User `json:"user"`
|
||||
}
|
||||
|
||||
type UpdateCurrentAccountInput struct {
|
||||
CurrentPassword string
|
||||
Username string
|
||||
Nickname string
|
||||
NewPassword string
|
||||
}
|
||||
|
||||
func (s *AuthService) Login(username, password string) (*LoginResult, error) {
|
||||
var user model.User
|
||||
if err := s.db.Where("username = ?", username).First(&user).Error; err != nil {
|
||||
@@ -86,6 +93,67 @@ func (s *AuthService) ChangePassword(userID uint, currentPassword, newPassword s
|
||||
return s.db.Model(&user).Update("password_hash", string(hash)).Error
|
||||
}
|
||||
|
||||
// UpdateCurrentAccount lets a user maintain their own login identity. The
|
||||
// current password is always required before username, nickname, or password
|
||||
// changes are accepted.
|
||||
func (s *AuthService) UpdateCurrentAccount(userID uint, in UpdateCurrentAccountInput) (*model.User, error) {
|
||||
if userID == 0 {
|
||||
return nil, errors.New("无效的用户身份")
|
||||
}
|
||||
in.Username = strings.TrimSpace(in.Username)
|
||||
if len(in.Username) < 3 || len(in.Username) > 64 {
|
||||
return nil, errors.New("用户名长度需为 3 至 64 位")
|
||||
}
|
||||
if len(in.Nickname) > 64 {
|
||||
return nil, errors.New("昵称不能超过 64 位")
|
||||
}
|
||||
if in.NewPassword != "" && len(in.NewPassword) < 8 {
|
||||
return nil, errors.New("新密码至少 8 位")
|
||||
}
|
||||
|
||||
updated := &model.User{}
|
||||
err := s.db.Transaction(func(tx *gorm.DB) error {
|
||||
var user model.User
|
||||
if err := tx.First(&user, userID).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return errors.New("用户不存在")
|
||||
}
|
||||
return err
|
||||
}
|
||||
if err := bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(in.CurrentPassword)); err != nil {
|
||||
return errors.New("当前密码错误")
|
||||
}
|
||||
if in.Username != user.Username {
|
||||
var count int64
|
||||
if err := tx.Model(&model.User{}).Where("username = ? AND id <> ?", in.Username, user.ID).Count(&count).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if count > 0 {
|
||||
return errors.New("用户名已存在")
|
||||
}
|
||||
}
|
||||
updates := map[string]interface{}{"username": in.Username, "nickname": in.Nickname}
|
||||
if in.NewPassword != "" {
|
||||
if err := bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(in.NewPassword)); err == nil {
|
||||
return errors.New("新密码不能与当前密码相同")
|
||||
}
|
||||
hash, err := bcrypt.GenerateFromPassword([]byte(in.NewPassword), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
updates["password_hash"] = string(hash)
|
||||
}
|
||||
if err := tx.Model(&user).Updates(updates).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
return tx.First(updated, user.ID).Error
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return updated, nil
|
||||
}
|
||||
|
||||
// EnsureAdmin creates the first platform administrator for an empty database.
|
||||
// Existing administrator accounts are never changed by environment variables.
|
||||
func (s *AuthService) EnsureAdmin(username, password string) error {
|
||||
|
||||
@@ -227,7 +227,7 @@ func TestUserServiceCannotDeleteOrDisableLastAdmin(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestPlatformAdminCRUDDoesNotCreateMerchantMembership(t *testing.T) {
|
||||
func TestPlatformAdminCRUDUsesHiddenSelfMerchantContext(t *testing.T) {
|
||||
db := newServiceTestDB(t)
|
||||
svc := NewUserService(db, NewTenantService(db))
|
||||
actor, err := svc.CreatePlatformAdmin("existing-admin", "password123", "现有管理员")
|
||||
@@ -242,8 +242,15 @@ func TestPlatformAdminCRUDDoesNotCreateMerchantMembership(t *testing.T) {
|
||||
if err := db.Model(&model.MerchantMember{}).Where("user_id = ?", admin.ID).Count(&membershipCount).Error; err != nil {
|
||||
t.Fatalf("count merchant memberships: %v", err)
|
||||
}
|
||||
if membershipCount != 0 {
|
||||
t.Fatalf("platform admin must not have merchant memberships, count=%d", membershipCount)
|
||||
if membershipCount != 1 {
|
||||
t.Fatalf("platform admin should have one hidden default membership, count=%d", membershipCount)
|
||||
}
|
||||
var membership model.MerchantMember
|
||||
if err := db.Where("user_id = ?", admin.ID).First(&membership).Error; err != nil {
|
||||
t.Fatalf("load default membership: %v", err)
|
||||
}
|
||||
if membership.Role != model.MemberRoleOwner || !membership.IsDefault {
|
||||
t.Fatalf("unexpected default membership: %+v", membership)
|
||||
}
|
||||
updated, err := svc.UpdatePlatformAdmin(admin.ID, UpdatePlatformAdminInput{
|
||||
Username: "platform-admin-updated", Nickname: "更新后管理员", Password: "new-password", Status: 1,
|
||||
@@ -266,6 +273,36 @@ func TestPlatformAdminCRUDDoesNotCreateMerchantMembership(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateCurrentAccountCanChangeUsernameNicknameAndPassword(t *testing.T) {
|
||||
db := newServiceTestDB(t)
|
||||
tenant := NewTenantService(db)
|
||||
auth := NewAuthService(db, nil, tenant)
|
||||
user, err := NewUserService(db, tenant).CreatePlatformAdmin("current-account", "old-password", "旧昵称")
|
||||
if err != nil {
|
||||
t.Fatalf("create account: %v", err)
|
||||
}
|
||||
updated, err := auth.UpdateCurrentAccount(user.ID, UpdateCurrentAccountInput{
|
||||
CurrentPassword: "old-password",
|
||||
Username: "updated-account",
|
||||
Nickname: "新昵称",
|
||||
NewPassword: "new-password",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("update current account: %v", err)
|
||||
}
|
||||
if updated.Username != "updated-account" || updated.Nickname != "新昵称" {
|
||||
t.Fatalf("unexpected updated account: %+v", updated)
|
||||
}
|
||||
if err := bcrypt.CompareHashAndPassword([]byte(updated.PasswordHash), []byte("new-password")); err != nil {
|
||||
t.Fatalf("updated password should match: %v", err)
|
||||
}
|
||||
if _, err := auth.UpdateCurrentAccount(user.ID, UpdateCurrentAccountInput{
|
||||
CurrentPassword: "wrong-password", Username: "should-not-save", Nickname: "", NewPassword: "",
|
||||
}); err == nil {
|
||||
t.Fatal("expected current password verification error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestListMerchantAccountGroupsExcludesPlatformAdmins(t *testing.T) {
|
||||
db := newServiceTestDB(t)
|
||||
merchantA := model.Merchant{Code: "merchant-accounts-a", Name: "商户账号 A", Status: model.MerchantStatusActive}
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/clause"
|
||||
)
|
||||
|
||||
type UserService struct {
|
||||
@@ -96,17 +97,30 @@ func (s *UserService) Create(username, password, nickname, role string, merchant
|
||||
if err := tx.Create(user).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if role == model.RoleAdmin && s.tenant != nil {
|
||||
var selfMerchant model.Merchant
|
||||
if err := tx.Where("code = ?", model.MerchantCodeSelfOperated).First(&selfMerchant).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
member := model.MerchantMember{MerchantID: selfMerchant.ID, UserID: user.ID, Role: model.MemberRoleOwner, Status: user.Status, IsDefault: true}
|
||||
if err := tx.Clauses(clause.OnConflict{
|
||||
Columns: []clause.Column{{Name: "merchant_id"}, {Name: "user_id"}},
|
||||
DoUpdates: clause.Assignments(map[string]interface{}{
|
||||
"role": model.MemberRoleOwner, "status": user.Status, "is_default": true,
|
||||
}),
|
||||
}).Create(&member).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
if s.tenant != nil && merchantID != 0 {
|
||||
memberRole := model.MemberRoleOperator
|
||||
if role == model.RoleAdmin {
|
||||
memberRole = model.MemberRoleOwner
|
||||
}
|
||||
member := model.MerchantMember{
|
||||
MerchantID: merchantID,
|
||||
UserID: user.ID,
|
||||
Role: memberRole,
|
||||
Status: user.Status,
|
||||
IsDefault: role == model.RoleAdmin,
|
||||
IsDefault: false,
|
||||
}
|
||||
if err := tx.Create(&member).Error; err != nil {
|
||||
return err
|
||||
@@ -180,8 +194,8 @@ func (s *UserService) ListMerchantAccountGroups(page, size int) ([]MerchantMembe
|
||||
return groups, total, nil
|
||||
}
|
||||
|
||||
// CreatePlatformAdmin creates an account with platform-only privileges. It
|
||||
// deliberately does not assign the account to any merchant.
|
||||
// CreatePlatformAdmin creates an account with platform privileges and the
|
||||
// hidden self-operated merchant context required by merchant-side routes.
|
||||
func (s *UserService) CreatePlatformAdmin(username, password, nickname string) (*model.User, error) {
|
||||
return s.Create(username, password, nickname, model.RoleAdmin, 0)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user