Some checks failed
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Failing after 1h43m42s
- 迁移 000221:新增 tb_business_user_group、tb_business_user_group_member、tb_shop_business_owner_import_task,成员一账号一行由部分唯一索引保证,店铺所属组按当前负责人实时推导,不回填历史分组。 - 用户组 CRUD、成员改组/清空归属、店铺批量交接(原子失败不部分写入)。 - 店铺负责人 CSV 导入任务:逐行独立事务、逐行明细、任务级与行级失败分离。 - 读侧推导与筛选:未分组、业务线、停用组可筛出并带停用标记。 - 补齐操作审计动作与资源、openapi 清单、发布门禁巡检表清单。 - 归档 add-shop-salesperson-groups 变更并同步 openspec/specs/business-user-group,补齐 AUG26-003 验证证据链。
494 lines
19 KiB
Go
494 lines
19 KiB
Go
// Package businessusergroup 收口业务用户组、成员归属与店铺负责人批量交接的写用例。
|
|
// 组只描述平台用户的业务分类,不改变后台角色、登录、权限或数据范围;
|
|
// 店铺所属组始终由当前负责人实时推导,因此本包不写任何店铺组字段。
|
|
package businessusergroup
|
|
|
|
import (
|
|
"context"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"gorm.io/gorm"
|
|
|
|
"github.com/break/junhong_cmp_fiber/internal/infrastructure/audit"
|
|
"github.com/break/junhong_cmp_fiber/internal/model"
|
|
"github.com/break/junhong_cmp_fiber/internal/model/dto"
|
|
"github.com/break/junhong_cmp_fiber/internal/store/postgres"
|
|
"github.com/break/junhong_cmp_fiber/pkg/constants"
|
|
"github.com/break/junhong_cmp_fiber/pkg/errors"
|
|
"github.com/break/junhong_cmp_fiber/pkg/middleware"
|
|
)
|
|
|
|
// Service 业务用户组与成员归属的简单写事务脚本。
|
|
type Service struct {
|
|
db *gorm.DB
|
|
groupStore *postgres.BusinessUserGroupStore
|
|
auditWriter *audit.Writer
|
|
}
|
|
|
|
// New 创建业务用户组事务脚本。
|
|
func New(db *gorm.DB, groupStore *postgres.BusinessUserGroupStore, auditWriters ...*audit.Writer) *Service {
|
|
service := &Service{db: db, groupStore: groupStore}
|
|
if len(auditWriters) > 0 {
|
|
service.auditWriter = auditWriters[0]
|
|
}
|
|
return service
|
|
}
|
|
|
|
// Create 创建业务用户组并在同一事务写入审计。
|
|
func (s *Service) Create(ctx context.Context, request *dto.CreateBusinessUserGroupRequest) (*dto.BusinessUserGroupResponse, error) {
|
|
operatorID, err := s.requireOperator(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
code := strings.TrimSpace(request.Code)
|
|
name := strings.TrimSpace(request.Name)
|
|
if code == "" || name == "" {
|
|
return nil, errors.New(errors.CodeInvalidParam, "业务用户组编码与名称不能为空")
|
|
}
|
|
if !constants.IsValidBusinessLine(request.BusinessLine) {
|
|
return nil, errors.New(errors.CodeInvalidParam, "业务线取值非法")
|
|
}
|
|
group := &model.BusinessUserGroup{
|
|
Code: code, Name: name, BusinessLine: request.BusinessLine,
|
|
SortOrder: sortValue(request.Sort), Status: statusValue(request.Enabled),
|
|
Remark: request.Remark, BaseModel: model.BaseModel{Creator: operatorID, Updater: operatorID},
|
|
}
|
|
var response *dto.BusinessUserGroupResponse
|
|
if err := s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
|
store := s.groupStore.WithTx(tx)
|
|
exists, err := store.ExistsCode(ctx, code, 0)
|
|
if err != nil {
|
|
return errors.Wrap(errors.CodeDatabaseError, err, "校验业务用户组编码失败")
|
|
}
|
|
if exists {
|
|
return errors.New(errors.CodeInvalidParam, "业务用户组编码已存在")
|
|
}
|
|
if err := store.Create(ctx, group); err != nil {
|
|
return mapCodeConflict(err)
|
|
}
|
|
if err := s.appendGroupAudit(ctx, tx, constants.AuditActionBusinessUserGroupCreated, "创建业务用户组", group, operatorID, nil, groupSnapshot(group)); err != nil {
|
|
return err
|
|
}
|
|
response = toGroupResponse(group)
|
|
return nil
|
|
}); err != nil {
|
|
return nil, err
|
|
}
|
|
return response, nil
|
|
}
|
|
|
|
// Update 更新业务用户组名称、业务线、排序、启停与备注;稳定编码永不允许修改。
|
|
func (s *Service) Update(ctx context.Context, groupID uint, request *dto.UpdateBusinessUserGroupRequest) (*dto.BusinessUserGroupResponse, error) {
|
|
operatorID, err := s.requireOperator(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if groupID == 0 {
|
|
return nil, errors.New(errors.CodeInvalidParam)
|
|
}
|
|
if request.BusinessLine != nil && !constants.IsValidBusinessLine(*request.BusinessLine) {
|
|
return nil, errors.New(errors.CodeInvalidParam, "业务线取值非法")
|
|
}
|
|
if request.Name != nil && strings.TrimSpace(*request.Name) == "" {
|
|
return nil, errors.New(errors.CodeInvalidParam, "业务用户组名称不能为空")
|
|
}
|
|
var response *dto.BusinessUserGroupResponse
|
|
err = s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
|
store := s.groupStore.WithTx(tx)
|
|
group, err := store.LockByID(ctx, groupID)
|
|
if err != nil {
|
|
return groupLookupError(err)
|
|
}
|
|
before := groupSnapshot(group)
|
|
if request.Name != nil {
|
|
group.Name = strings.TrimSpace(*request.Name)
|
|
}
|
|
if request.BusinessLine != nil {
|
|
group.BusinessLine = *request.BusinessLine
|
|
}
|
|
if request.Sort != nil {
|
|
group.SortOrder = *request.Sort
|
|
}
|
|
// 停用保留成员关系:已有成员继续显示已停用,只是不得新增成员或作为批量目标。
|
|
if request.Enabled != nil {
|
|
group.Status = statusValue(request.Enabled)
|
|
}
|
|
if request.Remark != nil {
|
|
group.Remark = *request.Remark
|
|
}
|
|
if err := store.Update(ctx, group, operatorID); err != nil {
|
|
return errors.Wrap(errors.CodeDatabaseError, err, "更新业务用户组失败")
|
|
}
|
|
after := groupSnapshot(group)
|
|
// 启停是独立状态事实,与资料变更分开记录,保证审计动作可被独立检索。
|
|
if before["status"] != after["status"] {
|
|
action, summary := constants.AuditActionBusinessUserGroupEnabled, "启用业务用户组"
|
|
if group.Status != constants.StatusEnabled {
|
|
action, summary = constants.AuditActionBusinessUserGroupDisabled, "停用业务用户组"
|
|
}
|
|
if err := s.appendGroupAudit(ctx, tx, action, summary, group, operatorID,
|
|
map[string]any{"status": before["status"]}, map[string]any{"status": after["status"]}); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if groupProfileChanged(before, after) {
|
|
if err := s.appendGroupAudit(ctx, tx, constants.AuditActionBusinessUserGroupUpdated, "更新业务用户组", group, operatorID, before, after); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
response = toGroupResponse(group)
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return response, nil
|
|
}
|
|
|
|
// Delete 删除无成员的业务用户组;有成员时只能停用或先移走成员。
|
|
func (s *Service) Delete(ctx context.Context, groupID uint) error {
|
|
operatorID, err := s.requireOperator(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if groupID == 0 {
|
|
return errors.New(errors.CodeInvalidParam)
|
|
}
|
|
return s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
|
store := s.groupStore.WithTx(tx)
|
|
group, err := store.LockByID(ctx, groupID)
|
|
if err != nil {
|
|
return groupLookupError(err)
|
|
}
|
|
count, err := store.CountMembers(ctx, group.ID)
|
|
if err != nil {
|
|
return errors.Wrap(errors.CodeDatabaseError, err, "统计业务用户组成员失败")
|
|
}
|
|
if count > 0 {
|
|
return errors.New(errors.CodeInvalidStatus, "用户组仍有成员,只能停用或先移走成员")
|
|
}
|
|
before := groupSnapshot(group)
|
|
if err := store.Delete(ctx, group.ID, operatorID); err != nil {
|
|
return errors.Wrap(errors.CodeDatabaseError, err, "删除业务用户组失败")
|
|
}
|
|
return s.appendGroupAudit(ctx, tx, constants.AuditActionBusinessUserGroupDeleted, "删除业务用户组", group, operatorID, before, nil)
|
|
})
|
|
}
|
|
|
|
// SetMembers 把多个启用平台用户批量设置到指定启用组,直接替换每个账号的原归属。
|
|
// 任一账号无效则整批不修改,成员前后值审计与业务事实同事务。
|
|
func (s *Service) SetMembers(ctx context.Context, groupID uint, request *dto.SetBusinessUserGroupMembersRequest) (*dto.BusinessUserGroupMembersResult, error) {
|
|
operatorID, err := s.requireOperator(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
accountIDs, err := normalizeAccountIDs(request.AccountIDs)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if groupID == 0 {
|
|
return nil, errors.New(errors.CodeInvalidParam)
|
|
}
|
|
result := &dto.BusinessUserGroupMembersResult{GroupID: groupID, AccountIDs: accountIDs}
|
|
err = s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
|
store := s.groupStore.WithTx(tx)
|
|
group, err := store.LockByID(ctx, groupID)
|
|
if err != nil {
|
|
return groupLookupError(err)
|
|
}
|
|
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, "锁定平台用户账号失败")
|
|
}
|
|
before, err := store.MembersByAccountIDs(ctx, accountIDs)
|
|
if err != nil {
|
|
return errors.Wrap(errors.CodeDatabaseError, err, "读取平台用户原分组失败")
|
|
}
|
|
if err := store.ReplaceMemberGroup(ctx, accountIDs, group.ID, operatorID); err != nil {
|
|
return mapMemberWriteError(err)
|
|
}
|
|
return s.appendMemberAudits(ctx, tx, group, accountIDs, before, operatorID)
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
// ClearMembers 清空指定启用平台用户的业务用户组归属,任一账号无效则整批不修改。
|
|
func (s *Service) ClearMembers(ctx context.Context, request *dto.ClearBusinessUserGroupMembersRequest) (*dto.BusinessUserGroupMembersResult, error) {
|
|
operatorID, err := s.requireOperator(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
accountIDs, err := normalizeAccountIDs(request.AccountIDs)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
result := &dto.BusinessUserGroupMembersResult{AccountIDs: 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
|
|
}
|
|
if err := store.LockAccountsByIDs(ctx, accountIDs); err != nil {
|
|
return errors.Wrap(errors.CodeDatabaseError, err, "锁定平台用户账号失败")
|
|
}
|
|
before, err := store.MembersByAccountIDs(ctx, accountIDs)
|
|
if err != nil {
|
|
return errors.Wrap(errors.CodeDatabaseError, err, "读取平台用户原分组失败")
|
|
}
|
|
if err := store.ClearMembers(ctx, accountIDs); err != nil {
|
|
return mapMemberWriteError(err)
|
|
}
|
|
return s.appendMemberAudits(ctx, tx, nil, accountIDs, before, operatorID)
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
// mapMemberWriteError 把成员关系写入失败收敛为稳定业务错误。
|
|
// 并发为同一账号新增成员关系时唯一索引是最终裁决,不能把约束冲突暴露成 500。
|
|
func mapMemberWriteError(err error) error {
|
|
if err == nil {
|
|
return nil
|
|
}
|
|
if postgres.IsAccountMemberConflict(err) {
|
|
return errors.New(errors.CodeConflict, "平台用户分组归属已被并发修改,请重试")
|
|
}
|
|
return errors.Wrap(errors.CodeDatabaseError, err, "更新平台用户分组失败")
|
|
}
|
|
|
|
// requireOperator 校验调用者具备平台维护入口身份,并返回其账号 ID。
|
|
func (s *Service) requireOperator(ctx context.Context) (uint, error) {
|
|
userType := middleware.GetUserTypeFromContext(ctx)
|
|
if userType != constants.UserTypeSuperAdmin && userType != constants.UserTypePlatform {
|
|
return 0, errors.New(errors.CodeForbidden, constants.PlatformManagementForbiddenMessage)
|
|
}
|
|
operatorID := middleware.GetUserIDFromContext(ctx)
|
|
if operatorID == 0 {
|
|
return 0, errors.New(errors.CodeUnauthorized)
|
|
}
|
|
return operatorID, nil
|
|
}
|
|
|
|
// normalizeAccountIDs 去重并保持首次出现顺序,空集合视为非法参数。
|
|
func normalizeAccountIDs(values []uint) ([]uint, error) {
|
|
if len(values) == 0 {
|
|
return nil, errors.New(errors.CodeInvalidParam, "账号列表不能为空")
|
|
}
|
|
seen := make(map[uint]struct{}, len(values))
|
|
result := make([]uint, 0, len(values))
|
|
for _, value := range values {
|
|
if value == 0 {
|
|
return nil, errors.New(errors.CodeInvalidParam, "账号ID非法")
|
|
}
|
|
if _, exists := seen[value]; exists {
|
|
continue
|
|
}
|
|
seen[value] = struct{}{}
|
|
result = append(result, value)
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
// ensureEnabledPlatformAccounts 校验全部账号都是当前启用的平台用户,任一不满足即整批失败。
|
|
// 账号有效性统一走共享谓词,避免各入口对「平台 + 启用 + 未软删」出现口径分叉。
|
|
func ensureEnabledPlatformAccounts(ctx context.Context, tx *gorm.DB, accountIDs []uint) error {
|
|
var accounts []model.Account
|
|
if err := tx.WithContext(ctx).Model(&model.Account{}).
|
|
Where("id IN ?", accountIDs).Find(&accounts).Error; err != nil {
|
|
return errors.Wrap(errors.CodeDatabaseError, err, "校验平台用户失败")
|
|
}
|
|
valid := 0
|
|
for _, account := range accounts {
|
|
if constants.IsAvailablePlatformBusinessOwner(account.UserType, account.Status, account.DeletedAt.Valid) {
|
|
valid++
|
|
}
|
|
}
|
|
if valid != len(accountIDs) {
|
|
return errors.New(errors.CodeInvalidParam, "存在无效或非启用的平台用户账号,整批未修改")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func groupLookupError(err error) error {
|
|
if err == gorm.ErrRecordNotFound {
|
|
return errors.New(errors.CodeNotFound, "业务用户组不存在")
|
|
}
|
|
return errors.Wrap(errors.CodeDatabaseError, err, "查询业务用户组失败")
|
|
}
|
|
|
|
// mapCodeConflict 把稳定编码唯一索引冲突映射为稳定业务错误,并发创建以唯一索引为最终裁决。
|
|
func mapCodeConflict(err error) error {
|
|
if err == nil {
|
|
return nil
|
|
}
|
|
if strings.Contains(strings.ToLower(err.Error()), "uk_business_user_group_code") {
|
|
return errors.New(errors.CodeInvalidParam, "业务用户组编码已存在")
|
|
}
|
|
return errors.Wrap(errors.CodeDatabaseError, err, "创建业务用户组失败")
|
|
}
|
|
|
|
func sortValue(value *int64) int64 {
|
|
if value == nil {
|
|
return 0
|
|
}
|
|
return *value
|
|
}
|
|
|
|
func statusValue(enabled *bool) int {
|
|
if enabled == nil || *enabled {
|
|
return constants.StatusEnabled
|
|
}
|
|
return constants.StatusDisabled
|
|
}
|
|
|
|
func toGroupResponse(group *model.BusinessUserGroup) *dto.BusinessUserGroupResponse {
|
|
return &dto.BusinessUserGroupResponse{
|
|
ID: group.ID, Code: group.Code, Name: group.Name,
|
|
BusinessLine: group.BusinessLine, BusinessLineName: constants.GetBusinessLineName(group.BusinessLine),
|
|
Sort: group.SortOrder, Enabled: group.Status == constants.StatusEnabled, Remark: group.Remark,
|
|
CreatedAt: group.CreatedAt.Format(time.RFC3339), UpdatedAt: group.UpdatedAt.Format(time.RFC3339),
|
|
}
|
|
}
|
|
|
|
// groupSnapshot 生成业务用户组的前后值快照,不含任何凭证或敏感信息。
|
|
func groupSnapshot(group *model.BusinessUserGroup) map[string]any {
|
|
if group == nil {
|
|
return nil
|
|
}
|
|
return map[string]any{
|
|
"id": group.ID, "code": group.Code, "name": group.Name,
|
|
"business_line": group.BusinessLine, "sort_order": group.SortOrder, "status": group.Status,
|
|
"remark": group.Remark,
|
|
}
|
|
}
|
|
|
|
// groupProfileChanged 判断除启停外的可维护字段是否发生变化;编码不可修改,不参与比较。
|
|
func groupProfileChanged(before, after map[string]any) bool {
|
|
for _, field := range []string{"name", "business_line", "sort_order", "remark"} {
|
|
if before[field] != after[field] {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// businessUserGroupKey 返回业务用户组审计资源的稳定 Key。
|
|
func businessUserGroupKey(group *model.BusinessUserGroup) string {
|
|
if group == nil {
|
|
return ""
|
|
}
|
|
if group.Code != "" {
|
|
return group.Code
|
|
}
|
|
return strconv.FormatUint(uint64(group.ID), 10)
|
|
}
|
|
|
|
// businessUserGroupIdentity 返回业务用户组审计身份快照,字段必须落在注册表白名单内。
|
|
func businessUserGroupIdentity(group *model.BusinessUserGroup) map[string]any {
|
|
if group == nil {
|
|
return nil
|
|
}
|
|
return map[string]any{
|
|
"id": group.ID, "code": group.Code, "name": group.Name,
|
|
"business_line": group.BusinessLine, "status": group.Status,
|
|
}
|
|
}
|
|
|
|
// appendGroupAudit 在业务事务内追加业务用户组事件。
|
|
func (s *Service) appendGroupAudit(ctx context.Context, tx *gorm.DB, action, summary string, group *model.BusinessUserGroup, operatorID uint, before, after map[string]any) error {
|
|
if s.auditWriter == nil {
|
|
return errors.New(errors.CodeInvalidStatus, "业务用户组统一审计接缝未配置")
|
|
}
|
|
var resourceID *string
|
|
if group.ID != 0 {
|
|
value := strconv.FormatUint(uint64(group.ID), 10)
|
|
resourceID = &value
|
|
}
|
|
s.auditWriter.Append(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,
|
|
Resources: []audit.ResourceInput{{
|
|
Type: constants.AuditResourceBusinessUserGroup, ID: resourceID,
|
|
Key: businessUserGroupKey(group), DisplayName: group.Name,
|
|
Relation: constants.AuditResourceRelationPrimary, Role: constants.AuditResourceRoleBusinessUserGroupTarget,
|
|
IdentitySnapshot: businessUserGroupIdentity(group), BeforeData: before, AfterData: after,
|
|
}},
|
|
})
|
|
return nil
|
|
}
|
|
|
|
// appendMemberAudits 在业务事务内为每个账号追加一条成员归属事件。
|
|
// 账号是实际被替换归属的资源,因此作为主要资源;目标组仅作引用,清空操作没有目标组。
|
|
func (s *Service) appendMemberAudits(ctx context.Context, tx *gorm.DB, group *model.BusinessUserGroup, accountIDs []uint, before map[uint]model.BusinessUserGroupMember, operatorID uint) error {
|
|
if s.auditWriter == nil {
|
|
return errors.New(errors.CodeInvalidStatus, "业务用户组统一审计接缝未配置")
|
|
}
|
|
var accounts []model.Account
|
|
if err := tx.WithContext(ctx).Unscoped().Where("id IN ?", accountIDs).Find(&accounts).Error; err != nil {
|
|
return errors.Wrap(errors.CodeDatabaseError, err, "查询成员审计账号失败")
|
|
}
|
|
accountByID := make(map[uint]model.Account, len(accounts))
|
|
for _, account := range accounts {
|
|
accountByID[account.ID] = account
|
|
}
|
|
summary := "清空平台用户业务用户组归属"
|
|
afterGroupID := any(nil)
|
|
if group != nil {
|
|
summary = "设置平台用户业务用户组归属"
|
|
afterGroupID = group.ID
|
|
}
|
|
for _, accountID := range accountIDs {
|
|
account, exists := accountByID[accountID]
|
|
if !exists {
|
|
continue
|
|
}
|
|
beforeGroupID := any(nil)
|
|
if member, ok := before[accountID]; ok {
|
|
beforeGroupID = member.BusinessUserGroupID
|
|
}
|
|
resource := audit.AccountResource(&account, constants.AuditResourceRelationPrimary, constants.AuditResourceRoleAccountTarget)
|
|
resource.BeforeData = map[string]any{"business_user_group_id": beforeGroupID}
|
|
resource.AfterData = map[string]any{"business_user_group_id": afterGroupID}
|
|
resources := []audit.ResourceInput{resource}
|
|
if group != nil {
|
|
resources = append(resources, audit.ResourceInput{
|
|
Type: constants.AuditResourceBusinessUserGroup, ID: optionalID(group.ID),
|
|
Key: businessUserGroupKey(group), DisplayName: group.Name,
|
|
Relation: constants.AuditResourceRelationReference, Role: constants.AuditResourceRoleBusinessUserGroupTarget,
|
|
IdentitySnapshot: businessUserGroupIdentity(group), SortOrder: 1,
|
|
})
|
|
}
|
|
s.auditWriter.Append(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,
|
|
})
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func optionalID(id uint) *string {
|
|
if id == 0 {
|
|
return nil
|
|
}
|
|
value := strconv.FormatUint(uint64(id), 10)
|
|
return &value
|
|
}
|