package shop import ( "context" stderrors "errors" "slices" accessauditapp "github.com/break/junhong_cmp_fiber/internal/application/accessaudit" "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" "gorm.io/gorm" "gorm.io/gorm/clause" ) func (s *Service) AssignRolesToShop(ctx context.Context, shopID uint, roleIDs []uint) ([]*model.ShopRole, error) { if err := middleware.CanManageShop(ctx, shopID); err != nil { return nil, err } shop, err := s.shopStore.GetByID(ctx, shopID) if err != nil { return nil, errors.New(errors.CodeNotFound, "店铺不存在") } shopRoles, changedRoles, err := s.assignShopRoles(ctx, shop, middleware.GetUserIDFromContext(ctx), roleIDs) if err != nil { s.recordShopRoleFailure(ctx, constants.AuditActionShopRolesAssigned, shop, changedRoles, err) return nil, err } return shopRoles, nil } func (s *Service) GetShopRoles(ctx context.Context, shopID uint) (*dto.ShopRolesResponse, error) { if err := middleware.CanManageShop(ctx, shopID); err != nil { return nil, err } _, err := s.shopStore.GetByID(ctx, shopID) if err != nil { return nil, errors.New(errors.CodeNotFound, "店铺不存在") } shopRoles, err := s.shopRoleStore.GetByShopID(ctx, shopID) if err != nil { return nil, errors.Wrap(errors.CodeInternalError, err, "查询店铺角色失败") } if len(shopRoles) == 0 { return &dto.ShopRolesResponse{ ShopID: shopID, Roles: []*dto.ShopRoleResponse{}, }, nil } roleIDs := make([]uint, 0, len(shopRoles)) for _, sr := range shopRoles { roleIDs = append(roleIDs, sr.RoleID) } roles, err := s.roleStore.GetByIDs(ctx, roleIDs) if err != nil { return nil, errors.Wrap(errors.CodeInternalError, err, "查询角色详情失败") } roleMap := make(map[uint]*model.Role) for _, role := range roles { roleMap[role.ID] = role } responses := make([]*dto.ShopRoleResponse, 0, len(shopRoles)) for _, sr := range shopRoles { role, exists := roleMap[sr.RoleID] if !exists { continue } responses = append(responses, &dto.ShopRoleResponse{ ShopID: sr.ShopID, RoleID: sr.RoleID, RoleName: role.RoleName, RoleDesc: role.RoleDesc, Status: sr.Status, }) } return &dto.ShopRolesResponse{ ShopID: shopID, Roles: responses, }, nil } func (s *Service) DeleteShopRole(ctx context.Context, shopID, roleID uint) error { if err := middleware.CanManageShop(ctx, shopID); err != nil { return err } shop, err := s.shopStore.GetByID(ctx, shopID) if err != nil { return errors.New(errors.CodeNotFound, "店铺不存在") } role, err := s.removeShopRole(ctx, shop, middleware.GetUserIDFromContext(ctx), roleID) if err != nil { roles := []*model.Role(nil) if role != nil { roles = []*model.Role{role} } s.recordShopRoleFailure(ctx, constants.AuditActionShopRoleDeleted, shop, roles, err) return err } return nil } func (s *Service) assignShopRoles(ctx context.Context, shop *model.Shop, operatorID uint, requested []uint) ([]*model.ShopRole, []*model.Role, error) { if s.db == nil || s.accessAudit == nil { return nil, nil, errors.New(errors.CodeInvalidStatus, "店铺角色审计接缝未配置") } var shopRoles []*model.ShopRole var changedRoles []*model.Role var accountIDs []uint err := s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).Select("id").First(&model.Shop{}, shop.ID).Error; err != nil { return errors.Wrap(errors.CodeDatabaseError, err, "锁定店铺角色关系失败") } shopRoleStore := postgres.NewShopRoleStore(tx, nil) roleStore := postgres.NewRoleStore(tx) beforeIDs, err := shopRoleStore.GetRoleIDsByShopID(ctx, shop.ID) if err != nil { return errors.Wrap(errors.CodeDatabaseError, err, "查询店铺现有角色失败") } requestedRoles, err := validateShopRoles(ctx, roleStore, requested) if err != nil { return err } changedRoles, err = loadChangedRoles(ctx, roleStore, beforeIDs, requested, requestedRoles) if err != nil { return err } if err := shopRoleStore.DeleteByShopID(ctx, shop.ID); err != nil { return errors.Wrap(errors.CodeDatabaseError, err, "删除现有店铺角色失败") } shopRoles = make([]*model.ShopRole, 0, len(requested)) for _, roleID := range requested { shopRoles = append(shopRoles, &model.ShopRole{ShopID: shop.ID, RoleID: roleID, Status: constants.StatusEnabled, Creator: operatorID, Updater: operatorID}) } if err := shopRoleStore.BatchCreate(ctx, shopRoles); err != nil { return errors.Wrap(errors.CodeDatabaseError, err, "批量创建店铺角色失败") } if err := s.accessAudit.WriteAccessChange(ctx, tx, accessauditapp.ChangeAudit{ ActionCode: constants.AuditActionShopRolesAssigned, Summary: "分配店铺默认角色", OperatorID: operatorID, Shop: shop, Roles: shopRoleChanges(changedRoles, beforeIDs, requested), BeforeData: map[string]any{"role_ids": sortedShopRoleIDs(beforeIDs)}, AfterData: map[string]any{"role_ids": sortedShopRoleIDs(requested)}, }); err != nil { return errors.Wrap(errors.CodeInternalError, err, "写入店铺角色审计失败") } accountIDs, err = shopPermissionCacheAccountIDs(ctx, tx, shop.ID) return err }) if err != nil { return nil, changedRoles, err } s.clearShopPermissionCaches(ctx, accountIDs) return shopRoles, changedRoles, nil } func (s *Service) removeShopRole(ctx context.Context, shop *model.Shop, operatorID, roleID uint) (*model.Role, error) { if s.db == nil || s.accessAudit == nil { return nil, errors.New(errors.CodeInvalidStatus, "店铺角色审计接缝未配置") } var removed *model.Role var accountIDs []uint err := s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).Select("id").First(&model.Shop{}, shop.ID).Error; err != nil { return errors.Wrap(errors.CodeDatabaseError, err, "锁定店铺角色关系失败") } shopRoles := postgres.NewShopRoleStore(tx, nil) beforeIDs, err := shopRoles.GetRoleIDsByShopID(ctx, shop.ID) if err != nil { return errors.Wrap(errors.CodeDatabaseError, err, "查询店铺现有角色失败") } if !slices.Contains(beforeIDs, roleID) { return nil } removed, err = postgres.NewRoleStore(tx).GetByID(ctx, roleID) if err != nil { return errors.Wrap(errors.CodeDatabaseError, err, "查询待移除店铺角色失败") } if err := shopRoles.Delete(ctx, shop.ID, roleID); err != nil { return errors.Wrap(errors.CodeDatabaseError, err, "删除店铺角色失败") } afterIDs := removeShopRoleID(beforeIDs, roleID) if err := s.accessAudit.WriteAccessChange(ctx, tx, accessauditapp.ChangeAudit{ ActionCode: constants.AuditActionShopRoleDeleted, Summary: "删除店铺默认角色", OperatorID: operatorID, Shop: shop, Roles: []accessauditapp.RoleChange{{Role: removed, BeforeData: map[string]any{"assigned": true}, AfterData: map[string]any{"assigned": false}}}, BeforeData: map[string]any{"role_ids": sortedShopRoleIDs(beforeIDs)}, AfterData: map[string]any{"role_ids": sortedShopRoleIDs(afterIDs)}, }); err != nil { return errors.Wrap(errors.CodeInternalError, err, "写入店铺角色审计失败") } accountIDs, err = shopPermissionCacheAccountIDs(ctx, tx, shop.ID) return err }) if err == nil { s.clearShopPermissionCaches(ctx, accountIDs) } return removed, err } func validateShopRoles(ctx context.Context, store *postgres.RoleStore, roleIDs []uint) ([]*model.Role, error) { if len(roleIDs) == 0 { return nil, nil } roles, err := store.GetByIDs(ctx, roleIDs) if err != nil { return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询角色失败") } if len(roles) != len(roleIDs) { return nil, errors.New(errors.CodeNotFound, "部分角色不存在") } for _, role := range roles { if role.RoleType != constants.RoleTypeCustomer { return nil, errors.New(errors.CodeInvalidParam, "店铺只能分配客户角色") } if role.Status != constants.StatusEnabled { return nil, errors.New(errors.CodeInvalidParam, "角色已禁用") } } return roles, nil } func loadChangedRoles(ctx context.Context, store *postgres.RoleStore, beforeIDs, afterIDs []uint, afterRoles []*model.Role) ([]*model.Role, error) { changedIDs := make([]uint, 0, len(beforeIDs)+len(afterIDs)) before, after := shopRoleIDSet(beforeIDs), shopRoleIDSet(afterIDs) for _, id := range beforeIDs { if !after[id] { changedIDs = append(changedIDs, id) } } for _, role := range afterRoles { if !before[role.ID] { changedIDs = append(changedIDs, role.ID) } } roles, err := store.GetByIDs(ctx, changedIDs) if err != nil { return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询变更角色失败") } return roles, nil } func shopPermissionCacheAccountIDs(ctx context.Context, tx *gorm.DB, shopID uint) ([]uint, error) { var accountIDs []uint if err := tx.WithContext(ctx).Model(&model.Account{}).Where("shop_id = ?", shopID).Pluck("id", &accountIDs).Error; err != nil { return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询店铺账号失败") } return accountIDs, nil } func (s *Service) clearShopPermissionCaches(ctx context.Context, accountIDs []uint) { if len(accountIDs) == 0 || s.redisClient == nil { return } keys := make([]string, 0, len(accountIDs)) for _, accountID := range accountIDs { keys = append(keys, constants.RedisUserPermissionsKey(accountID)) } _ = s.redisClient.Del(ctx, keys...).Err() } func (s *Service) recordShopRoleFailure(ctx context.Context, action string, shop *model.Shop, roles []*model.Role, originalErr error) { changes := make([]accessauditapp.RoleChange, 0, len(roles)) for _, role := range roles { changes = append(changes, accessauditapp.RoleChange{Role: role}) } accessauditapp.RecordFailure(ctx, s.db, s.accessAudit, accessauditapp.ChangeAudit{ ActionCode: action, Summary: "店铺角色操作失败", Result: shopRoleFailureResult(originalErr), OperatorID: middleware.GetUserIDFromContext(ctx), Shop: shop, Roles: changes, }, originalErr) } func shopRoleChanges(roles []*model.Role, beforeIDs, afterIDs []uint) []accessauditapp.RoleChange { before, after := shopRoleIDSet(beforeIDs), shopRoleIDSet(afterIDs) changes := make([]accessauditapp.RoleChange, 0, len(roles)) for _, role := range roles { changes = append(changes, accessauditapp.RoleChange{ Role: role, BeforeData: map[string]any{"assigned": before[role.ID]}, AfterData: map[string]any{"assigned": after[role.ID]}, }) } return changes } func shopRoleFailureResult(err error) string { var appErr *errors.AppError if stderrors.As(err, &appErr) { switch appErr.Code { case errors.CodeForbidden, errors.CodeInvalidParam, errors.CodeNotFound, errors.CodeRoleNotFound: return constants.AuditResultDenied } } return constants.AuditResultFailed } func shopRoleIDSet(ids []uint) map[uint]bool { set := make(map[uint]bool, len(ids)) for _, id := range ids { set[id] = true } return set } func sortedShopRoleIDs(ids []uint) []uint { result := append([]uint(nil), ids...) slices.Sort(result) return result } func removeShopRoleID(ids []uint, removed uint) []uint { result := make([]uint, 0, len(ids)) for _, id := range ids { if id != removed { result = append(result, id) } } return result }