实施业务用户组成员管理
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 9m42s

This commit is contained in:
2026-09-21 21:20:38 +08:00
parent 4a16eb0b1e
commit 22c3e7cc1a
14 changed files with 836 additions and 106 deletions

View File

@@ -177,9 +177,8 @@ func (s *Service) Delete(ctx context.Context, groupID uint) error {
})
}
// SetMembers 把多个启用平台用户批量设置到指定启用组,直接替换每个账号的原归属
// 任一账号无效则整批不修改,成员前后值审计与业务事实同事务。
func (s *Service) SetMembers(ctx context.Context, groupID uint, request *dto.SetBusinessUserGroupMembersRequest) (*dto.BusinessUserGroupMembersResult, error) {
// AddMembers 增量增加多个启用且未删除的平台用户到指定启用组
func (s *Service) AddMembers(ctx context.Context, groupID uint, request *dto.AddBusinessUserGroupMembersRequest) (*dto.BusinessUserGroupMemberMutationResult, error) {
operatorID, err := s.requireOperator(ctx)
if err != nil {
return nil, err
@@ -191,7 +190,7 @@ func (s *Service) SetMembers(ctx context.Context, groupID uint, request *dto.Set
if groupID == 0 {
return nil, errors.New(errors.CodeInvalidParam)
}
result := &dto.BusinessUserGroupMembersResult{GroupID: groupID, AccountIDs: accountIDs}
result := &dto.BusinessUserGroupMemberMutationResult{GroupID: groupID, AccountIDs: accountIDs, RequestedCount: len(accountIDs)}
err = s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
store := s.groupStore.WithTx(tx)
group, err := store.LockByID(ctx, groupID)
@@ -201,22 +200,37 @@ func (s *Service) SetMembers(ctx context.Context, groupID uint, request *dto.Set
if group.Status != constants.StatusEnabled {
return errors.New(errors.CodeInvalidStatus, "目标用户组已停用,不能作为成员归属目标")
}
if err := ensureEnabledPlatformAccounts(ctx, tx, accountIDs); err != nil {
return err
}
// 按 id 升序锁账号行:账号行锁保证同一账号串行化,
// 同时消除「清空时无成员行导致锁不到行」的幻读与「多账号相反顺序」的死锁。
if err := store.LockAccountsByIDs(ctx, accountIDs); err != nil {
return errors.Wrap(errors.CodeDatabaseError, err, "锁定平台用户账号失败")
}
if err := ensureEnabledPlatformAccounts(ctx, tx, accountIDs); err != nil {
return err
}
before, err := store.MembersByAccountIDs(ctx, accountIDs)
if err != nil {
return errors.Wrap(errors.CodeDatabaseError, err, "读取平台用户原分组失败")
}
changed := make([]uint, 0, len(accountIDs))
for _, accountID := range accountIDs {
member, exists := before[accountID]
switch {
case !exists:
result.AddedCount++
result.AddedAccountIDs = append(result.AddedAccountIDs, accountID)
changed = append(changed, accountID)
case member.BusinessUserGroupID != groupID:
result.MovedCount++
result.MovedAccountIDs = append(result.MovedAccountIDs, accountID)
changed = append(changed, accountID)
default:
result.UnchangedCount++
result.UnchangedAccountIDs = append(result.UnchangedAccountIDs, accountID)
}
}
if err := store.ReplaceMemberGroup(ctx, accountIDs, group.ID, operatorID); err != nil {
return mapMemberWriteError(err)
}
return s.appendMemberAudits(ctx, tx, group, accountIDs, before, operatorID)
return s.appendMemberAudits(ctx, tx, group, changed, before, operatorID, false)
})
if err != nil {
return nil, err
@@ -224,8 +238,8 @@ func (s *Service) SetMembers(ctx context.Context, groupID uint, request *dto.Set
return result, nil
}
// ClearMembers 清空指定启用平台用户的业务用户组归属,任一账号无效则整批不修改
func (s *Service) ClearMembers(ctx context.Context, request *dto.ClearBusinessUserGroupMembersRequest) (*dto.BusinessUserGroupMembersResult, error) {
// RemoveMembers 按目标组作用域增量移除成员;目标组启用或停用均可操作
func (s *Service) RemoveMembers(ctx context.Context, groupID uint, request *dto.RemoveBusinessUserGroupMembersRequest) (*dto.BusinessUserGroupMemberMutationResult, error) {
operatorID, err := s.requireOperator(ctx)
if err != nil {
return nil, err
@@ -234,11 +248,15 @@ func (s *Service) ClearMembers(ctx context.Context, request *dto.ClearBusinessUs
if err != nil {
return nil, err
}
result := &dto.BusinessUserGroupMembersResult{AccountIDs: accountIDs}
if groupID == 0 {
return nil, errors.New(errors.CodeInvalidParam)
}
result := &dto.BusinessUserGroupMemberMutationResult{GroupID: groupID, AccountIDs: accountIDs, RequestedCount: len(accountIDs)}
err = s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
store := s.groupStore.WithTx(tx)
if err := ensureEnabledPlatformAccounts(ctx, tx, accountIDs); err != nil {
return err
group, err := store.LockByID(ctx, groupID)
if err != nil {
return groupLookupError(err)
}
if err := store.LockAccountsByIDs(ctx, accountIDs); err != nil {
return errors.Wrap(errors.CodeDatabaseError, err, "锁定平台用户账号失败")
@@ -247,10 +265,22 @@ func (s *Service) ClearMembers(ctx context.Context, request *dto.ClearBusinessUs
if err != nil {
return errors.Wrap(errors.CodeDatabaseError, err, "读取平台用户原分组失败")
}
if err := store.ClearMembers(ctx, accountIDs); err != nil {
for _, accountID := range accountIDs {
member, exists := before[accountID]
if !exists || member.BusinessUserGroupID != group.ID {
return errors.New(errors.CodeConflict, "存在账号已不属于目标用户组,整批未修改")
}
}
affected, err := store.RemoveMembersByGroup(ctx, group.ID, accountIDs, operatorID)
if err != nil {
return mapMemberWriteError(err)
}
return s.appendMemberAudits(ctx, tx, nil, accountIDs, before, operatorID)
if affected != int64(len(accountIDs)) {
return errors.New(errors.CodeConflict, "目标用户组成员已被并发修改,整批未修改")
}
result.RemovedCount = len(accountIDs)
result.RemovedAccountIDs = append(result.RemovedAccountIDs, accountIDs...)
return s.appendMemberAudits(ctx, tx, group, accountIDs, before, operatorID, true)
})
if err != nil {
return nil, err
@@ -418,7 +448,7 @@ func (s *Service) appendGroupAudit(ctx context.Context, tx *gorm.DB, action, sum
value := strconv.FormatUint(uint64(group.ID), 10)
resourceID = &value
}
s.auditWriter.Append(ctx, tx, audit.AppendInput{
_, err := s.auditWriter.AppendAndGet(ctx, tx, audit.AppendInput{
ActionCode: action, Summary: summary, Result: constants.AuditResultSuccess,
Actor: audit.ActorInput{Kind: constants.AuditActorAccount, ID: strconv.FormatUint(uint64(operatorID), 10)},
Source: constants.AuditSourceAdminAPI, ScopeType: constants.AuditScopePlatform,
@@ -429,12 +459,12 @@ func (s *Service) appendGroupAudit(ctx context.Context, tx *gorm.DB, action, sum
IdentitySnapshot: businessUserGroupIdentity(group), BeforeData: before, AfterData: after,
}},
})
return nil
return err
}
// appendMemberAudits 在业务事务内为每个账号追加一条成员归属事件。
// 账号是实际被替换归属的资源,因此作为主要资源;目标组仅作引用,清空操作没有目标组
func (s *Service) appendMemberAudits(ctx context.Context, tx *gorm.DB, group *model.BusinessUserGroup, accountIDs []uint, before map[uint]model.BusinessUserGroupMember, operatorID uint) error {
// appendMemberAudits 在业务事务内为每个实际变化账号追加一条成员归属事件。
// 账号是实际被替换归属的资源;目标组作为引用资源,移除时 after 为空
func (s *Service) appendMemberAudits(ctx context.Context, tx *gorm.DB, group *model.BusinessUserGroup, accountIDs []uint, before map[uint]model.BusinessUserGroupMember, operatorID uint, removing bool) error {
if s.auditWriter == nil {
return errors.New(errors.CodeInvalidStatus, "业务用户组统一审计接缝未配置")
}
@@ -446,16 +476,21 @@ func (s *Service) appendMemberAudits(ctx context.Context, tx *gorm.DB, group *mo
for _, account := range accounts {
accountByID[account.ID] = account
}
summary := "清空平台用户业务用户组归属"
summary := "增量移除平台用户业务用户组成员"
afterGroupID := any(nil)
if group != nil {
summary = "设置平台用户业务用户组归属"
afterGroupID = group.ID
if !removing {
summary = "增量维护平台用户业务用户组成员"
if group != nil {
afterGroupID = group.ID
}
}
if removing && group == nil {
return errors.New(errors.CodeInvalidStatus, "移除成员审计缺少原用户组")
}
for _, accountID := range accountIDs {
account, exists := accountByID[accountID]
if !exists {
continue
return errors.New(errors.CodeDatabaseError, "成员审计账号不存在")
}
beforeGroupID := any(nil)
if member, ok := before[accountID]; ok {
@@ -473,13 +508,15 @@ func (s *Service) appendMemberAudits(ctx context.Context, tx *gorm.DB, group *mo
IdentitySnapshot: businessUserGroupIdentity(group), SortOrder: 1,
})
}
s.auditWriter.Append(ctx, tx, audit.AppendInput{
if _, err := s.auditWriter.AppendAndGet(ctx, tx, audit.AppendInput{
ActionCode: constants.AuditActionBusinessUserGroupMembersUpdated, Summary: summary,
Result: constants.AuditResultSuccess,
Actor: audit.ActorInput{Kind: constants.AuditActorAccount, ID: strconv.FormatUint(uint64(operatorID), 10)},
Source: constants.AuditSourceAdminAPI, ScopeType: constants.AuditScopePlatform,
Resources: resources,
})
}); err != nil {
return err
}
}
return nil
}