固化七月迭代审计治理进展以隔离线上热修
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 参数类型错误未通过。
This commit is contained in:
@@ -3,6 +3,7 @@ package shop
|
||||
import (
|
||||
"context"
|
||||
|
||||
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"
|
||||
@@ -10,16 +11,28 @@ import (
|
||||
"github.com/break/junhong_cmp_fiber/pkg/constants"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/errors"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/middleware"
|
||||
"github.com/redis/go-redis/v9"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/clause"
|
||||
)
|
||||
|
||||
type Service struct {
|
||||
db *gorm.DB
|
||||
redisClient *redis.Client
|
||||
accessAudit accessauditapp.Writer
|
||||
shopStore *postgres.ShopStore
|
||||
accountStore *postgres.AccountStore
|
||||
shopRoleStore *postgres.ShopRoleStore
|
||||
roleStore *postgres.RoleStore
|
||||
}
|
||||
|
||||
// SetAccessAudit 注入店铺角色授权的事务、缓存和统一审计边界。
|
||||
func (s *Service) SetAccessAudit(db *gorm.DB, redisClient *redis.Client, writer accessauditapp.Writer) {
|
||||
s.db = db
|
||||
s.redisClient = redisClient
|
||||
s.accessAudit = writer
|
||||
}
|
||||
|
||||
func New(
|
||||
shopStore *postgres.ShopStore,
|
||||
accountStore *postgres.AccountStore,
|
||||
@@ -290,36 +303,90 @@ func (s *Service) Delete(ctx context.Context, id uint) error {
|
||||
return errors.New(errors.CodeUnauthorized, "未授权访问")
|
||||
}
|
||||
|
||||
shop, err := s.shopStore.GetByID(ctx, id)
|
||||
if err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return errors.New(errors.CodeShopNotFound, "店铺不存在")
|
||||
if s.db == nil || s.accessAudit == nil {
|
||||
return errors.New(errors.CodeInvalidStatus, "店铺删除审计接缝未配置")
|
||||
}
|
||||
var shop *model.Shop
|
||||
var parent *model.Shop
|
||||
var accounts []*model.Account
|
||||
var accountIDs []uint
|
||||
err := s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
var locked model.Shop
|
||||
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).First(&locked, id).Error; err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return errors.New(errors.CodeShopNotFound, "店铺不存在")
|
||||
}
|
||||
return errors.Wrap(errors.CodeDatabaseError, err, "获取店铺失败")
|
||||
}
|
||||
return errors.Wrap(errors.CodeInternalError, err, "获取店铺失败")
|
||||
}
|
||||
|
||||
accounts, err := s.accountStore.GetByShopID(ctx, shop.ID)
|
||||
if err != nil {
|
||||
return errors.Wrap(errors.CodeInternalError, err, "查询店铺账号失败")
|
||||
}
|
||||
|
||||
if len(accounts) > 0 {
|
||||
accountIDs := make([]uint, 0, len(accounts))
|
||||
shop = &locked
|
||||
parent = loadDeletedShopParent(tx, locked.ParentID)
|
||||
if err := tx.Where("shop_id = ?", locked.ID).Find(&accounts).Error; err != nil {
|
||||
return errors.Wrap(errors.CodeDatabaseError, err, "查询店铺账号失败")
|
||||
}
|
||||
accountChanges := make([]accessauditapp.AccountChange, 0, len(accounts))
|
||||
accountIDs = make([]uint, 0, len(accounts))
|
||||
for _, account := range accounts {
|
||||
accountIDs = append(accountIDs, account.ID)
|
||||
accountChanges = append(accountChanges, accessauditapp.AccountChange{
|
||||
Account: account, BeforeData: map[string]any{"status": account.Status}, AfterData: map[string]any{"status": constants.StatusDisabled},
|
||||
})
|
||||
}
|
||||
if err := s.accountStore.BulkUpdateStatus(ctx, accountIDs, constants.StatusDisabled, currentUserID); err != nil {
|
||||
return errors.Wrap(errors.CodeInternalError, err, "禁用店铺账号失败")
|
||||
if len(accountIDs) > 0 {
|
||||
if err := postgres.NewAccountStore(tx, nil).BulkUpdateStatus(ctx, accountIDs, constants.StatusDisabled, currentUserID); err != nil {
|
||||
return errors.Wrap(errors.CodeDatabaseError, err, "禁用店铺账号失败")
|
||||
}
|
||||
}
|
||||
if err := tx.Delete(&model.Shop{}, id).Error; err != nil {
|
||||
return errors.Wrap(errors.CodeDatabaseError, err, "删除店铺失败")
|
||||
}
|
||||
if err := s.accessAudit.WriteAccessChange(ctx, tx, accessauditapp.ChangeAudit{
|
||||
ActionCode: constants.AuditActionShopDeleted, Summary: "删除店铺", OperatorID: currentUserID,
|
||||
Shop: shop, ParentShop: parent, Accounts: accountChanges,
|
||||
BeforeData: map[string]any{"deleted": false}, AfterData: map[string]any{"deleted": true},
|
||||
SubjectVisibility: constants.AuditSubjectResult, SubjectSummary: "店铺已删除",
|
||||
}); err != nil {
|
||||
return errors.Wrap(errors.CodeInternalError, err, "写入店铺删除审计失败")
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
if shop != nil {
|
||||
accessauditapp.RecordFailure(ctx, s.db, s.accessAudit, accessauditapp.ChangeAudit{
|
||||
ActionCode: constants.AuditActionShopDeleted, Summary: "删除店铺失败", Result: shopRoleFailureResult(err),
|
||||
OperatorID: currentUserID, Shop: shop, ParentShop: parent, SubjectVisibility: constants.AuditSubjectInternalOnly,
|
||||
}, err)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
if err := s.shopStore.Delete(ctx, id); err != nil {
|
||||
return errors.Wrap(errors.CodeInternalError, err, "删除店铺失败")
|
||||
}
|
||||
|
||||
s.clearDeletedShopCaches(ctx, shop.ID, shop.ParentID, accountIDs)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Service) clearDeletedShopCaches(ctx context.Context, shopID uint, parentID *uint, accountIDs []uint) {
|
||||
if s.redisClient == nil {
|
||||
return
|
||||
}
|
||||
keys := []string{constants.RedisShopSubordinatesKey(shopID)}
|
||||
if parentID != nil {
|
||||
keys = append(keys, constants.RedisShopSubordinatesKey(*parentID))
|
||||
}
|
||||
for _, accountID := range accountIDs {
|
||||
keys = append(keys, constants.RedisUserPermissionsKey(accountID))
|
||||
}
|
||||
_ = s.redisClient.Del(ctx, keys...).Err()
|
||||
}
|
||||
|
||||
func loadDeletedShopParent(tx *gorm.DB, parentID *uint) *model.Shop {
|
||||
if parentID == nil {
|
||||
return nil
|
||||
}
|
||||
var parent model.Shop
|
||||
if err := tx.Unscoped().First(&parent, *parentID).Error; err != nil {
|
||||
return nil
|
||||
}
|
||||
return &parent
|
||||
}
|
||||
|
||||
// GetSubordinateShopIDs 获取下级店铺 ID 列表(包含自己)
|
||||
func (s *Service) GetSubordinateShopIDs(ctx context.Context, shopID uint) ([]uint, error) {
|
||||
return s.shopStore.GetSubordinateShopIDs(ctx, shopID)
|
||||
|
||||
Reference in New Issue
Block a user