fix: 同步原生账号启停与商务审核状态

This commit is contained in:
Yvan
2026-11-03 22:02:51 +08:00
parent 075d94a51e
commit fc17b384ee
10 changed files with 423 additions and 7 deletions
+29 -1
View File
@@ -52,7 +52,9 @@ type UserRepo interface {
ListUsers(context.Context, int, int, *UserListFilter) ([]*User, int64, error)
CreateUserWithAuthorities(context.Context, *User, []uint) (*User, error)
UpdateUser(context.Context, *User) error
UpdateUserWithStatus(context.Context, *User, func(int) (int, error)) error
UpdateSelfUser(context.Context, *User) error
UpdateSelfUserWithStatus(context.Context, *User, func(int) (int, error)) error
DeleteUser(context.Context, uint) error
UpdatePassword(context.Context, uint, string, bool) error
SetUserAuthorities(context.Context, uint, []uint) error
@@ -108,6 +110,10 @@ func (uc *UserUsecase) ListUsers(ctx context.Context, page, pageSize int, filter
return uc.repo.ListUsers(ctx, page, pageSize, filter)
}
func (uc *UserUsecase) CreateUser(ctx context.Context, user *User, authorityIDs []uint) (*User, error) {
if user.Enable == 0 {
user.Enable = 1
}
user.BusinessStatus = businessStatusForEnable(0, user.Enable)
hash, err := bcrypt.GenerateFromPassword([]byte(user.Password), bcrypt.DefaultCost)
if err != nil {
return nil, err
@@ -125,10 +131,32 @@ func (uc *UserUsecase) UpdateUser(ctx context.Context, user *User, authorityIDs
return err
}
}
return uc.repo.UpdateUser(ctx, user)
return uc.repo.UpdateUserWithStatus(ctx, user, func(current int) (int, error) {
return businessStatusForEnable(current, user.Enable), nil
})
}
// 启用代表恢复正常;未启用时保留待审核/驳回,避免编辑资料丢失业务审核状态。
func businessStatusForEnable(current, enable int) int {
if enable == 1 {
return 0
}
if current == 2 || current == 3 {
return current
}
return 1
}
func (uc *UserUsecase) UpdateSelfUser(ctx context.Context, user *User) error {
if user.Enable != 0 {
return uc.repo.UpdateSelfUserWithStatus(ctx, user, func(current int) (int, error) {
// 个人资料请求不能越过管理员刚写入的停用或审核状态。
if current != 0 {
return current, ErrUserDisabled
}
return businessStatusForEnable(current, user.Enable), nil
})
}
return uc.repo.UpdateSelfUser(ctx, user)
}
+51
View File
@@ -59,3 +59,54 @@ func TestSetUserAuthoritiesRejectsEmptyListAsBadRequest(t *testing.T) {
t.Fatalf("SetUserAuthorities() error = %v, want ErrAuthoritiesRequired", err)
}
}
type statusUpdateRepo struct {
UserRepo
current, next int
created *User
}
func (r *statusUpdateRepo) UpdateUserWithStatus(_ context.Context, _ *User, policy func(int) (int, error)) error {
var err error
r.next, err = policy(r.current)
return err
}
func (r *statusUpdateRepo) UpdateSelfUserWithStatus(ctx context.Context, user *User, policy func(int) (int, error)) error {
return r.UpdateUserWithStatus(ctx, user, policy)
}
func (r *statusUpdateRepo) CreateUserWithAuthorities(_ context.Context, user *User, _ []uint) (*User, error) {
r.created = user
return user, nil
}
func TestNativeEnablePreservesBusinessReviewStates(t *testing.T) {
for _, tc := range []struct{ current, enable, want int }{{0, 2, 1}, {1, 1, 0}, {2, 0, 2}, {3, 2, 3}, {2, 1, 0}, {3, 1, 0}} {
repo := &statusUpdateRepo{current: tc.current}
if err := NewUserUsecase(repo).UpdateUser(context.Background(), &User{ID: 1, Enable: tc.enable}, nil); err != nil || repo.next != tc.want {
t.Fatalf("商务状态与启停不同步: %+v next=%d err=%v", tc, repo.next, err)
}
}
}
func TestNativeCreationStatusMatchesDefaultAndDisabledEnable(t *testing.T) {
for _, tc := range []struct{ enable, wantEnable, wantStatus int }{{0, 1, 0}, {1, 1, 0}, {2, 2, 1}} {
repo := &statusUpdateRepo{}
_, err := NewUserUsecase(repo).CreateUser(context.Background(), &User{Username: "test", Password: "test-password", Enable: tc.enable}, nil)
if err != nil || repo.created.Enable != tc.wantEnable || repo.created.BusinessStatus != tc.wantStatus {
t.Fatalf("新账号状态错误: %+v %v", repo.created, err)
}
}
}
func TestSelfProfileCannotApproveOrReenableBusinessState(t *testing.T) {
for _, status := range []int{1, 2, 3} {
repo := &statusUpdateRepo{current: status}
if err := NewUserUsecase(repo).UpdateSelfUser(context.Background(), &User{ID: 1, Enable: 1}); err != ErrUserDisabled || repo.next != status {
t.Fatal("个人资料修改越过了停用/审核状态")
}
}
repo := &statusUpdateRepo{current: 0}
if err := NewUserUsecase(repo).UpdateSelfUser(context.Background(), &User{ID: 1, Enable: 2}); err != nil || repo.next != 1 {
t.Fatal("个人资料停用字段没有同步商务状态")
}
}
+47 -6
View File
@@ -342,7 +342,7 @@ func (r *userRepo) CreateUserWithAuthorities(ctx context.Context, user *system.U
user.NickName = "系统用户"
}
now := time.Now()
po := userPO{UUID: user.UUID, Username: user.Username, Password: user.Password, NickName: user.NickName, HeaderImg: user.HeaderImg, AuthorityID: user.AuthorityID, Phone: user.Phone, Email: user.Email, Enable: user.Enable, PasswordUpdatedAt: &now, MustChangePassword: user.MustChangePassword}
po := userPO{UUID: user.UUID, Username: user.Username, Password: user.Password, NickName: user.NickName, HeaderImg: user.HeaderImg, AuthorityID: user.AuthorityID, Phone: user.Phone, Email: user.Email, Enable: user.Enable, BusinessStatus: user.BusinessStatus, PasswordUpdatedAt: &now, MustChangePassword: user.MustChangePassword}
var createdAuthorities []system.Authority
if err := r.data.DB().WithContext(ctx).Transaction(func(tx *gorm.DB) error {
// The compatible registration rejects an existing non-deleted username before creating the
@@ -397,6 +397,34 @@ func (r *userRepo) UpdateUser(ctx context.Context, user *system.User) error {
return r.updateUser(r.data.DB().WithContext(ctx), user)
}
func (r *userRepo) UpdateUserWithStatus(ctx context.Context, user *system.User, updateStatus func(int) (int, error)) error {
if err := r.checkUserUpdateAuth(ctx, user); err != nil {
return err
}
return r.updateUserAndStatus(ctx, user.ID, userUpdateFields(user), updateStatus)
}
func (r *userRepo) updateUserAndStatus(ctx context.Context, userID uint, updates map[string]any, updateStatus func(int) (int, error)) error {
return r.data.DB().WithContext(ctx).Transaction(func(tx *gorm.DB) error {
var current userPO
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).Select("id,business_status").First(&current, userID).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return system.ErrUserNotFound
}
return err
}
// 使用锁内的最新审核状态运行领域策略,与 enable 在同一次更新中提交。
if updateStatus != nil {
status, err := updateStatus(current.BusinessStatus)
if err != nil {
return err
}
updates["business_status"] = status
}
return tx.Model(&userPO{}).Where("id = ?", userID).Updates(updates).Error
})
}
func (r *userRepo) checkUserUpdateAuth(ctx context.Context, user *system.User) error {
access := &authorityAccessRepo{data: r.data}
if err := access.checkUserIDAuth(ctx, user.ID, false); err != nil {
@@ -409,6 +437,18 @@ func (r *userRepo) checkUserUpdateAuth(ctx context.Context, user *system.User) e
}
func (r *userRepo) UpdateSelfUser(ctx context.Context, user *system.User) error {
updates := selfUserUpdateFields(user)
if len(updates) == 0 {
return nil
}
return r.data.DB().WithContext(ctx).Model(&userPO{}).Where("id = ?", user.ID).Updates(updates).Error
}
func (r *userRepo) UpdateSelfUserWithStatus(ctx context.Context, user *system.User, updateStatus func(int) (int, error)) error {
return r.updateUserAndStatus(ctx, user.ID, selfUserUpdateFields(user), updateStatus)
}
func selfUserUpdateFields(user *system.User) map[string]any {
updates := make(map[string]any)
if user.NickName != "" {
updates["nick_name"] = user.NickName
@@ -425,10 +465,7 @@ func (r *userRepo) UpdateSelfUser(ctx context.Context, user *system.User) error
if user.Enable != 0 {
updates["enable"] = user.Enable
}
if len(updates) == 0 {
return nil
}
return r.data.DB().WithContext(ctx).Model(&userPO{}).Where("id = ?", user.ID).Updates(updates).Error
return updates
}
func (r *userRepo) FillDepartmentNamePaths(ctx context.Context, user *system.User) error {
@@ -473,11 +510,15 @@ func (r *userRepo) FillDepartmentNamePaths(ctx context.Context, user *system.Use
}
func (r *userRepo) updateUser(tx *gorm.DB, user *system.User) error {
return tx.Model(&userPO{}).Where("id = ?", user.ID).Updates(userUpdateFields(user)).Error
}
func userUpdateFields(user *system.User) map[string]any {
updates := map[string]any{"nick_name": user.NickName, "header_img": user.HeaderImg, "phone": user.Phone, "email": user.Email, "enable": user.Enable}
if user.AuthorityID != 0 {
updates["authority_id"] = user.AuthorityID
}
return tx.Model(&userPO{}).Where("id = ?", user.ID).Updates(updates).Error
return updates
}
func (r *userRepo) DeleteUser(ctx context.Context, id uint) error {
@@ -0,0 +1,43 @@
package system
import (
"context"
"testing"
"kra/internal/biz/system"
)
func TestUserStatusPolicyReadsLockedStateAndWritesTogether(t *testing.T) {
data := newTransactionTestData(t)
db := data.gormDB.WithContext(context.Background())
user := userPO{Username: "status-user", AuthorityID: 888, Enable: 0, BusinessStatus: 3, NickName: "old"}
if err := db.Create(&user).Error; err != nil {
t.Fatal(err)
}
repo := &userRepo{data: data}
seen := -1
err := repo.UpdateUserWithStatus(context.Background(), &system.User{ID: user.ID, NickName: "new", Enable: 1}, func(current int) (int, error) { seen = current; return 0, nil })
if err != nil {
t.Fatal(err)
}
var stored userPO
if err := db.First(&stored, user.ID).Error; err != nil {
t.Fatal(err)
}
if seen != 3 || stored.Enable != 1 || stored.BusinessStatus != 0 || stored.NickName != "new" {
t.Fatalf("状态策略和更新不同步: %+v seen=%d", stored, seen)
}
if err := db.Exec("CREATE TRIGGER fail_status_update BEFORE UPDATE OF business_status ON sys_users BEGIN SELECT RAISE(ABORT, 'test rejection'); END").Error; err != nil {
t.Fatal(err)
}
err = repo.UpdateUserWithStatus(context.Background(), &system.User{ID: user.ID, NickName: "failed", Enable: 2}, func(int) (int, error) { return 1, nil })
if err == nil {
t.Fatal("失败的状态更新应回滚")
}
if err := db.First(&stored, user.ID).Error; err != nil {
t.Fatal(err)
}
if stored.Enable != 1 || stored.BusinessStatus != 0 || stored.NickName != "new" {
t.Fatal("留下部分状态或资料更新")
}
}
+3
View File
@@ -40,6 +40,9 @@ func TestStrictUserMutationsRejectOutsideTargetAndAllowSelfPassword(t *testing.T
if err := repo.UpdateUser(ctx, &system.User{ID: users[2].ID, NickName: "changed"}); err == nil {
t.Fatal("UpdateUser() accepted an out-of-scope target")
}
if err := repo.UpdateUserWithStatus(ctx, &system.User{ID: users[2].ID, Enable: 1}, func(int) (int, error) { t.Fatal("越权时不应执行状态策略"); return 0, nil }); err == nil {
t.Fatal("启停同步接口放行了角色范围外用户")
}
if err := repo.DeleteUser(ctx, users[2].ID); err == nil {
t.Fatal("DeleteUser() accepted an out-of-scope target")
}