Constraint: 切换 main 前必须保存当前七月分支全部项目进展,套餐生效提案仅属于 Iteration/7-11。 Rejected: 将七月套餐修复直接移植到 main | 两个分支的可靠投递架构不同。 Confidence: medium Scope-risk: broad Directive: 不得将本提交整体 cherry-pick 到 main;main 套餐热修必须基于其纯 Asynq 代码独立实施。 Tested: git diff --check;openspec validate fix-package-activation-starvation --strict。 Not-tested: 按用户要求未运行自动化测试;go build ./... 因当前审计改造中的 Enterprise 模型字面量和 role.recordFailure 参数类型错误未通过。
331 lines
11 KiB
Go
331 lines
11 KiB
Go
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
|
|
}
|