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" "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" "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, shopRoleStore *postgres.ShopRoleStore, roleStore *postgres.RoleStore, ) *Service { return &Service{ shopStore: shopStore, accountStore: accountStore, shopRoleStore: shopRoleStore, roleStore: roleStore, } } func (s *Service) Update(ctx context.Context, id uint, req *dto.UpdateShopRequest) (*dto.ShopResponse, error) { currentUserID := middleware.GetUserIDFromContext(ctx) if currentUserID == 0 { return nil, errors.New(errors.CodeUnauthorized, "未授权访问") } shop, err := s.shopStore.GetByID(ctx, id) if err != nil { return nil, errors.New(errors.CodeShopNotFound, "店铺不存在") } shop.ShopName = req.ShopName shop.ContactName = req.ContactName shop.ContactPhone = req.ContactPhone shop.Province = req.Province shop.City = req.City shop.District = req.District shop.Address = req.Address shop.Status = req.Status shop.Updater = currentUserID if err := s.shopStore.Update(ctx, shop); err != nil { return nil, err } parentShopName := "" if shop.ParentID != nil { parentShop, err := s.shopStore.GetByID(ctx, *shop.ParentID) if err == nil { parentShopName = parentShop.ShopName } } return &dto.ShopResponse{ ID: shop.ID, ShopName: shop.ShopName, ShopCode: shop.ShopCode, ParentID: shop.ParentID, ParentShopName: parentShopName, Level: shop.Level, ContactName: shop.ContactName, ContactPhone: shop.ContactPhone, Province: shop.Province, City: shop.City, District: shop.District, Address: shop.Address, Status: shop.Status, StatusName: constants.GetStatusName(shop.Status), CreatedAt: shop.CreatedAt.Format("2006-01-02 15:04:05"), UpdatedAt: shop.UpdatedAt.Format("2006-01-02 15:04:05"), }, nil } // Disable 禁用店铺 func (s *Service) Disable(ctx context.Context, id uint) error { // 获取当前用户 ID currentUserID := middleware.GetUserIDFromContext(ctx) if currentUserID == 0 { return errors.New(errors.CodeUnauthorized, "未授权访问") } // 查询店铺 shop, err := s.shopStore.GetByID(ctx, id) if err != nil { return errors.New(errors.CodeShopNotFound, "店铺不存在") } // 更新状态 shop.Status = constants.StatusDisabled shop.Updater = currentUserID return s.shopStore.Update(ctx, shop) } // Enable 启用店铺 func (s *Service) Enable(ctx context.Context, id uint) error { // 获取当前用户 ID currentUserID := middleware.GetUserIDFromContext(ctx) if currentUserID == 0 { return errors.New(errors.CodeUnauthorized, "未授权访问") } // 查询店铺 shop, err := s.shopStore.GetByID(ctx, id) if err != nil { return errors.New(errors.CodeShopNotFound, "店铺不存在") } // 更新状态 shop.Status = constants.StatusEnabled shop.Updater = currentUserID return s.shopStore.Update(ctx, shop) } // GetByID 获取店铺详情 func (s *Service) GetByID(ctx context.Context, id uint) (*model.Shop, error) { shop, err := s.shopStore.GetByID(ctx, id) if err != nil { return nil, errors.New(errors.CodeShopNotFound, "店铺不存在") } return shop, nil } func (s *Service) ListShopResponses(ctx context.Context, req *dto.ShopListRequest) ([]*dto.ShopResponse, int64, error) { opts := &store.QueryOptions{ Page: req.Page, PageSize: req.PageSize, OrderBy: "created_at DESC", } if opts.Page == 0 { opts.Page = 1 } if opts.PageSize == 0 { opts.PageSize = constants.DefaultPageSize } filters := make(map[string]interface{}) if req.ShopName != "" { filters["shop_name"] = req.ShopName } if req.ShopCode != "" { filters["shop_code"] = req.ShopCode } if req.ContactPhone != "" { filters["contact_phone"] = req.ContactPhone } if req.ParentID != nil { filters["parent_id"] = *req.ParentID } if req.Level != nil { filters["level"] = *req.Level } if req.Status != nil { filters["status"] = *req.Status } shops, total, err := s.shopStore.List(ctx, opts, filters) if err != nil { return nil, 0, errors.Wrap(errors.CodeInternalError, err, "查询店铺列表失败") } parentShopNameMap, err := s.buildParentShopNameMap(ctx, shops) if err != nil { return nil, 0, errors.Wrap(errors.CodeInternalError, err, "查询上级店铺名称失败") } responses := make([]*dto.ShopResponse, 0, len(shops)) for _, shop := range shops { parentShopName := "" if shop.ParentID != nil { parentShopName = parentShopNameMap[*shop.ParentID] } responses = append(responses, &dto.ShopResponse{ ID: shop.ID, ShopName: shop.ShopName, ShopCode: shop.ShopCode, ParentID: shop.ParentID, ParentShopName: parentShopName, Level: shop.Level, ContactName: shop.ContactName, ContactPhone: shop.ContactPhone, Province: shop.Province, City: shop.City, District: shop.District, Address: shop.Address, Status: shop.Status, StatusName: constants.GetStatusName(shop.Status), CreatedAt: shop.CreatedAt.Format("2006-01-02 15:04:05"), UpdatedAt: shop.UpdatedAt.Format("2006-01-02 15:04:05"), }) } return responses, total, nil } // buildParentShopNameMap 批量查询店铺上级名称,避免列表接口出现 N+1 查询。 func (s *Service) buildParentShopNameMap(ctx context.Context, shops []*model.Shop) (map[uint]string, error) { parentIDs := make([]uint, 0, len(shops)) parentIDSet := make(map[uint]struct{}, len(shops)) for _, shop := range shops { if shop.ParentID == nil { continue } if _, exists := parentIDSet[*shop.ParentID]; exists { continue } parentIDSet[*shop.ParentID] = struct{}{} parentIDs = append(parentIDs, *shop.ParentID) } if len(parentIDs) == 0 { return map[uint]string{}, nil } parentShops, err := s.shopStore.GetByIDs(ctx, parentIDs) if err != nil { return nil, err } parentShopNameMap := make(map[uint]string, len(parentShops)) for _, parentShop := range parentShops { parentShopNameMap[parentShop.ID] = parentShop.ShopName } return parentShopNameMap, nil } func (s *Service) List(ctx context.Context, opts *store.QueryOptions, filters map[string]interface{}) ([]*model.Shop, int64, error) { return s.shopStore.List(ctx, opts, filters) } // ListCascade 联级查询店铺,用于新增店铺或其他场景选择上级店铺 func (s *Service) ListCascade(ctx context.Context, req *dto.ShopCascadeRequest) ([]*dto.ShopCascadeItem, error) { shops, err := s.shopStore.ListForCascade(ctx, req.ShopName, req.ParentID, req.ExcludeSelf) if err != nil { return nil, errors.Wrap(errors.CodeInternalError, err, "查询店铺失败") } if len(shops) == 0 { return []*dto.ShopCascadeItem{}, nil } ids := make([]uint, 0, len(shops)) for _, shop := range shops { ids = append(ids, shop.ID) } childParentIDs, err := s.shopStore.GetParentIDsWithChildren(ctx, ids) if err != nil { return nil, errors.Wrap(errors.CodeInternalError, err, "查询下级店铺失败") } hasChildrenSet := make(map[uint]bool, len(childParentIDs)) for _, pid := range childParentIDs { hasChildrenSet[pid] = true } result := make([]*dto.ShopCascadeItem, 0, len(shops)) for _, shop := range shops { result = append(result, &dto.ShopCascadeItem{ ID: shop.ID, ShopName: shop.ShopName, HasChildren: hasChildrenSet[shop.ID], }) } return result, nil } func (s *Service) Delete(ctx context.Context, id uint) error { currentUserID := middleware.GetUserIDFromContext(ctx) if currentUserID == 0 { return errors.New(errors.CodeUnauthorized, "未授权访问") } 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, "获取店铺失败") } 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 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 } 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) }