修复:管理员默认商户与账户设置

This commit is contained in:
yml2213
2026-08-13 14:12:25 +08:00
parent 6dae7fafa8
commit 538be52636
7 changed files with 181 additions and 23 deletions
+26
View File
@@ -49,6 +49,13 @@ type changePasswordReq struct {
NewPassword string `json:"new_password" binding:"required,min=8"`
}
type updateCurrentAccountReq struct {
CurrentPassword string `json:"current_password" binding:"required"`
Username string `json:"username" binding:"required"`
Nickname string `json:"nickname"`
NewPassword string `json:"new_password" binding:"omitempty,min=8"`
}
func (h *AuthHandler) ChangePassword(c *gin.Context) {
var req changePasswordReq
if err := c.ShouldBindJSON(&req); err != nil {
@@ -61,3 +68,22 @@ func (h *AuthHandler) ChangePassword(c *gin.Context) {
}
response.OK(c, nil)
}
func (h *AuthHandler) UpdateCurrentAccount(c *gin.Context) {
var req updateCurrentAccountReq
if err := c.ShouldBindJSON(&req); err != nil {
response.BadRequest(c, "请填写用户名和当前密码;新密码至少 8 位")
return
}
user, err := h.svc.UpdateCurrentAccount(middleware.GetUserID(c), service.UpdateCurrentAccountInput{
CurrentPassword: req.CurrentPassword,
Username: req.Username,
Nickname: req.Nickname,
NewPassword: req.NewPassword,
})
if err != nil {
response.BadRequest(c, err.Error())
return
}
response.OK(c, user)
}
+1
View File
@@ -146,6 +146,7 @@ func Setup(h *Handlers) *gin.Engine {
auth.Use(middleware.Tenant(h.Tenant))
{
auth.GET("/auth/profile", h.Auth.Profile)
auth.PUT("/auth/account", h.Auth.UpdateCurrentAccount)
auth.PUT("/auth/password", h.Auth.ChangePassword)
auth.GET("/dashboard", h.Dashboard.Dashboard)
+68
View File
@@ -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 {
+40 -3
View File
@@ -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}
+20 -6
View File
@@ -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)
}