package distribution import ( "context" "errors" "strings" smsprovider "hfb_sys/backend/internal/integrations/sms" "hfb_sys/backend/internal/modules/auth" ) var ErrDependencyUnavailable = errors.New("distribution dependency unavailable") type Service struct { repo *Repository jwt *auth.JWTManager smsResolver *SMSResolver } func NewService(repo *Repository, jwt ...*auth.JWTManager) *Service { s := &Service{repo: repo} if len(jwt) > 0 { s.jwt = jwt[0] } return s } func (s *Service) SetSMSResolver(resolver *SMSResolver) { s.smsResolver = resolver } func (s *Service) SMSResolver() *SMSResolver { return s.smsResolver } // ResolveSMSProvider 实现 auth.SiteSMSProviderResolver 接口 func (s *Service) ResolveSMSProvider(ctx context.Context, siteCode, domain string) (smsprovider.Provider, error) { if s == nil || s.smsResolver == nil { return nil, nil } return s.smsResolver.ResolveSMSProvider(ctx, siteCode, domain) } func (s *Service) available() error { if s == nil || s.repo == nil { return ErrDependencyUnavailable } return nil } func (s *Service) ListSites(ctx context.Context) ([]SiteDTO, error) { if err := s.available(); err != nil { return nil, err } return s.repo.ListSites(ctx) } func (s *Service) ListSitesForAdmin(ctx context.Context, adminID uint64) ([]SiteDTO, error) { if err := s.available(); err != nil { return nil, err } items, err := s.repo.ListSites(ctx) if err != nil { return nil, err } global, err := s.repo.hasGlobalDistributionAccess(ctx, adminID) if err != nil { return nil, err } if global { return items, nil } // 客服(qrcode:view)是平台级可信角色,统一维护各分站二维码池。 // 二维码池接口本身只校验 qrcode:* 权限、不按站点归属收窄,因此这里直接放行全量站点, // 让码池页面能拿到站点 id;站长/分销商等站点成员仍在下方按其绑定站点收窄。 // 注意:不要为了这个需求给共享角色(如 ops,同时是分站站长)授予 distribution:all_view—— // 该权限是跨站写操作的总开关(改站/删站/重置站长密码),会击穿多租户隔离。 canViewQr, err := s.repo.hasPermission(ctx, adminID, "qrcode:view") if err != nil { return nil, err } if canViewQr { return items, nil } siteIDs, err := s.repo.listActiveMemberSiteIDs(ctx, adminID) if err != nil { return nil, err } if len(siteIDs) == 0 { return []SiteDTO{}, nil } allowed := make(map[uint64]struct{}, len(siteIDs)) for _, siteID := range siteIDs { allowed[siteID] = struct{}{} } filtered := make([]SiteDTO, 0, len(allowed)) for _, item := range items { if _, ok := allowed[item.ID]; ok { filtered = append(filtered, item) } } return filtered, nil } func (s *Service) ScopeSiteID(ctx context.Context, adminID, requested uint64) (uint64, error) { if err := s.available(); err != nil { return 0, err } return s.repo.ScopeSiteID(ctx, adminID, requested) } func (s *Service) FindSite(ctx context.Context, adminID, id uint64) (*SiteDTO, error) { if err := s.available(); err != nil { return nil, err } return s.repo.FindSite(ctx, adminID, id) } func (s *Service) CreateSite(ctx context.Context, req CreateSiteRequest) (*SiteDTO, error) { if err := s.available(); err != nil { return nil, err } return s.repo.CreateSite(ctx, req) } func (s *Service) DefaultSettlementHoldDays(ctx context.Context) (int, error) { if err := s.available(); err != nil { return 0, err } return s.repo.DefaultSettlementHoldDays(ctx), nil } func (s *Service) UpdateSite(ctx context.Context, adminID, id uint64, req UpdateSiteRequest) (*SiteDTO, error) { if err := s.available(); err != nil { return nil, err } return s.repo.UpdateSite(ctx, adminID, id, req) } func (s *Service) DeleteSite(ctx context.Context, adminID, siteID uint64) error { if err := s.available(); err != nil { return err } return s.repo.DeleteSite(ctx, adminID, siteID) } func (s *Service) ResetSiteAdminPassword(ctx context.Context, operatorAdminID, siteID uint64, newPassword string) error { if err := s.available(); err != nil { return err } return s.repo.ResetSiteAdminPassword(ctx, operatorAdminID, siteID, newPassword) } func (s *Service) ListRules(ctx context.Context, adminID uint64, siteID *uint64, mode string) ([]RuleDTO, error) { if err := s.available(); err != nil { return nil, err } return s.repo.ListRules(ctx, adminID, siteID, mode) } func (s *Service) CreateRule(ctx context.Context, adminID uint64, req CreateRuleRequest) (*RuleDTO, error) { if err := s.available(); err != nil { return nil, err } return s.repo.CreateRule(ctx, adminID, req) } func (s *Service) UpdateRuleStatus(ctx context.Context, adminID, id uint64, status string) (*RuleDTO, error) { if err := s.available(); err != nil { return nil, err } return s.repo.UpdateRuleStatus(ctx, adminID, id, status) } func (s *Service) ListOrders(ctx context.Context, query DistributionOrderListQuery) (*DistributionOrderPage, error) { if err := s.available(); err != nil { return nil, err } return s.repo.ListOrders(ctx, query) } func (s *Service) FindOrder(ctx context.Context, adminID, id uint64) (*DistributionOrderDTO, error) { if err := s.available(); err != nil { return nil, err } return s.repo.FindOrder(ctx, adminID, id) } func (s *Service) ListAccountDisplays(ctx context.Context, siteID uint64, status string) ([]AccountDisplayDTO, error) { if err := s.available(); err != nil { return nil, err } return s.repo.ListAccountDisplays(ctx, siteID, status) } func (s *Service) UpdateAccountDisplay(ctx context.Context, adminID, id uint64, status string) (*AccountDisplayDTO, error) { if err := s.available(); err != nil { return nil, err } return s.repo.UpdateAccountDisplay(ctx, adminID, id, status) } func (s *Service) ListPaymentChannels(ctx context.Context, siteID uint64) ([]PaymentChannelDTO, error) { if err := s.available(); err != nil { return nil, err } return s.repo.ListPaymentChannels(ctx, siteID) } func (s *Service) BindPaymentChannel(ctx context.Context, siteID uint64, req BindPaymentChannelRequest) (*PaymentChannelDTO, error) { if err := s.available(); err != nil { return nil, err } return s.repo.BindPaymentChannel(ctx, siteID, req) } func (s *Service) UnbindPaymentChannel(ctx context.Context, adminID, id uint64) error { if err := s.available(); err != nil { return err } return s.repo.UnbindPaymentChannel(ctx, adminID, id) } func (s *Service) ListSettlementBatches(ctx context.Context, siteID uint64) ([]SettlementBatchDTO, error) { if err := s.available(); err != nil { return nil, err } return s.repo.ListSettlementBatches(ctx, siteID) } func (s *Service) CreateSettlementBatch(ctx context.Context, adminID uint64, req CreateSettlementBatchRequest) (*SettlementBatchDTO, error) { if err := s.available(); err != nil { return nil, err } return s.repo.CreateSettlementBatch(ctx, adminID, req) } func (s *Service) ListSettlementItems(ctx context.Context, adminID, batchID uint64) ([]SettlementItemDTO, error) { if err := s.available(); err != nil { return nil, err } return s.repo.ListSettlementItems(ctx, adminID, batchID) } func (s *Service) MarkSettlementBatch(ctx context.Context, adminID, id uint64) (*SettlementBatchDTO, error) { if err := s.available(); err != nil { return nil, err } return s.repo.MarkSettlementBatch(ctx, adminID, id) } func (s *Service) CancelSettlementBatch(ctx context.Context, adminID, id uint64) (*SettlementBatchDTO, error) { if err := s.available(); err != nil { return nil, err } return s.repo.CancelSettlementBatch(ctx, adminID, id) } func (s *Service) CreateFinancialSnapshot(ctx context.Context, adminID, orderID uint64, req CreateFinancialSnapshotRequest) (*DistributionOrderDTO, error) { if err := s.available(); err != nil { return nil, err } return s.repo.CreateFinancialSnapshot(ctx, adminID, orderID, req) } func (s *Service) Login(ctx context.Context, req DistributionLoginRequest, clientIP string) (*DistributionLoginResponse, error) { if err := s.available(); err != nil { return nil, err } if s.jwt == nil { return nil, ErrDependencyUnavailable } admin, member, err := s.repo.LoginMember(ctx, req.Username, req.Password) if err != nil { return nil, err } pair, err := s.jwt.GenerateSubjectPairWithVersion(admin.ID, admin.Username, "admin", admin.TokenVersion) if err != nil { return nil, err } return &DistributionLoginResponse{ Token: pair.AccessToken, RefreshToken: pair.RefreshToken, Member: *member, }, nil } func (s *Service) Me(ctx context.Context, adminID uint64) (*DistributionCurrentMemberDTO, error) { if err := s.available(); err != nil { return nil, err } return s.repo.GetMemberProfile(ctx, adminID) } func (s *Service) OnboardSite(ctx context.Context, req OnboardSiteRequest) (*OnboardSiteResponse, error) { if err := s.available(); err != nil { return nil, err } return s.repo.OnboardSite(ctx, req) } func (s *Service) SyncSiteAccounts(ctx context.Context, adminID, siteID uint64, req SyncSiteAccountsRequest) (*SyncSiteAccountsResult, error) { if err := s.available(); err != nil { return nil, err } return s.repo.SyncSiteAccounts(ctx, adminID, siteID, req) } func (s *Service) ListPublicSiteListings(ctx context.Context, siteCode string, page, pageSize int) (*PublicDistributionListingsResult, error) { if err := s.available(); err != nil { return nil, err } return s.repo.ListPublicSiteListings(ctx, siteCode, page, pageSize) } func (s *Service) GetPublicSiteInfo(ctx context.Context, code, domain string) (*PublicSiteInfoDTO, error) { if err := s.available(); err != nil { return nil, err } return s.repo.GetPublicSiteInfo(ctx, code, domain) } func (s *Service) UpdateSiteBranding(ctx context.Context, adminID, siteID uint64, req UpdateSiteBrandingRequest) (*SiteDTO, error) { if err := s.available(); err != nil { return nil, err } return s.repo.UpdateSiteBranding(ctx, adminID, siteID, req) } func (s *Service) UpdateAccountMarkup(ctx context.Context, adminID, id uint64, ratioDelta float64) (*AccountDisplayDTO, error) { if err := s.available(); err != nil { return nil, err } return s.repo.UpdateAccountMarkup(ctx, adminID, id, ratioDelta) } func (s *Service) BatchUpdateAccountMarkup(ctx context.Context, adminID, siteID uint64, req BatchUpdateAccountMarkupRequest) error { if err := s.available(); err != nil { return err } return s.repo.BatchUpdateAccountMarkup(ctx, adminID, siteID, req) } func (s *Service) BatchUpdateAccountDisplay(ctx context.Context, adminID, siteID uint64, req BatchUpdateAccountDisplayRequest) error { if err := s.available(); err != nil { return err } return s.repo.BatchUpdateAccountDisplay(ctx, adminID, siteID, req) } func (s *Service) ListSiteAnnouncements(ctx context.Context, adminID, siteID uint64, status string, page, pageSize int) (*SiteAnnouncementPage, error) { if err := s.available(); err != nil { return nil, err } return s.repo.ListSiteAnnouncements(ctx, adminID, siteID, status, page, pageSize) } func (s *Service) CreateSiteAnnouncement(ctx context.Context, adminID, siteID uint64, req CreateSiteAnnouncementRequest) (*SiteAnnouncementDTO, error) { if err := s.available(); err != nil { return nil, err } return s.repo.CreateSiteAnnouncement(ctx, adminID, siteID, req) } func (s *Service) UpdateSiteAnnouncement(ctx context.Context, adminID, siteID, id uint64, req UpdateSiteAnnouncementRequest) (*SiteAnnouncementDTO, error) { if err := s.available(); err != nil { return nil, err } return s.repo.UpdateSiteAnnouncement(ctx, adminID, siteID, id, req) } func (s *Service) UpdateSiteAnnouncementStatus(ctx context.Context, adminID, siteID, id uint64, action string) (*SiteAnnouncementDTO, error) { if err := s.available(); err != nil { return nil, err } return s.repo.UpdateSiteAnnouncementStatus(ctx, adminID, siteID, id, action) } func (s *Service) DeleteSiteAnnouncement(ctx context.Context, adminID, siteID, id uint64) error { if err := s.available(); err != nil { return err } return s.repo.DeleteSiteAnnouncement(ctx, adminID, siteID, id) } func (s *Service) ListReceivedListings(ctx context.Context, adminID, siteID uint64, q ReceivedListingQuery) (*ReceivedListingPage, error) { if err := s.available(); err != nil { return nil, err } return s.repo.ListReceivedListings(ctx, adminID, siteID, q) } func (s *Service) UpdateReceivedListingStatus(ctx context.Context, adminID, listingID uint64, status string) error { if err := s.available(); err != nil { return err } return s.repo.UpdateReceivedListingStatus(ctx, adminID, listingID, status) } func (s *Service) GetSiteWallet(ctx context.Context, adminID, siteID uint64) (*SiteWalletDTO, error) { if err := s.available(); err != nil { return nil, err } return s.repo.GetSiteWallet(ctx, adminID, siteID) } func (s *Service) ApplySiteWithdrawal(ctx context.Context, adminID, siteID uint64, req ApplySiteWithdrawalRequest) (*SiteWithdrawalDTO, error) { if err := s.available(); err != nil { return nil, err } return s.repo.ApplySiteWithdrawal(ctx, adminID, siteID, req) } func (s *Service) ListSiteWithdrawals(ctx context.Context, adminID, siteID uint64, page, size int) (*SiteWithdrawalPage, error) { if err := s.available(); err != nil { return nil, err } return s.repo.ListSiteWithdrawals(ctx, adminID, siteID, page, size) } func (s *Service) GetDashboardAnalytics(ctx context.Context, adminID, siteID uint64) (*DistributionAnalyticsDTO, error) { if err := s.available(); err != nil { return nil, err } return s.repo.GetDashboardAnalytics(ctx, adminID, siteID) } func (s *Service) GetSiteSMSConfig(ctx context.Context, adminID, siteID uint64) (*SiteSMSConfigDTO, error) { if err := s.available(); err != nil { return nil, err } return s.repo.GetSiteSMSConfig(ctx, adminID, siteID) } func (s *Service) UpdateSiteSMSConfig(ctx context.Context, adminID, siteID uint64, req UpdateSiteSMSConfigRequest) (*SiteSMSConfigDTO, error) { if err := s.available(); err != nil { return nil, err } dto, err := s.repo.UpdateSiteSMSConfig(ctx, adminID, siteID, req) if err != nil { return nil, err } if s.smsResolver != nil { s.smsResolver.InvalidateCache(siteID) } return dto, nil } func (s *Service) TestSiteSMSConfig(ctx context.Context, adminID, siteID uint64, req TestSiteSMSConfigRequest) error { if err := s.available(); err != nil { return err } if s.smsResolver == nil { return errors.New("sms resolver unavailable") } phone := strings.TrimSpace(req.Phone) if phone == "" { return errors.New("测试手机号不能为空") } // 先获取当前站点已持久化的配置及已解密的密钥 existingCfg, err := s.repo.GetDecryptedSiteSMSConfig(ctx, adminID, siteID) if err != nil { return err } // 构造测试配置(优先取请求参数,缺省回退至已有配置) testCfg := SiteSMSConfig{ Enabled: true, } if req.Provider != nil && strings.TrimSpace(*req.Provider) != "" { testCfg.Provider = strings.TrimSpace(*req.Provider) } else if existingCfg != nil && existingCfg.Provider != "" { testCfg.Provider = existingCfg.Provider } if req.AccessKeyID != nil && strings.TrimSpace(*req.AccessKeyID) != "" { testCfg.AccessKeyID = strings.TrimSpace(*req.AccessKeyID) } else if existingCfg != nil { testCfg.AccessKeyID = existingCfg.AccessKeyID } if req.AccessKeySecret != nil && strings.TrimSpace(*req.AccessKeySecret) != "" { testCfg.AccessKeySecret = strings.TrimSpace(*req.AccessKeySecret) } else if existingCfg != nil { testCfg.AccessKeySecret = existingCfg.AccessKeySecret } if req.Endpoint != nil && strings.TrimSpace(*req.Endpoint) != "" { testCfg.Endpoint = strings.TrimSpace(*req.Endpoint) } else if existingCfg != nil { testCfg.Endpoint = existingCfg.Endpoint } if req.SignName != nil && strings.TrimSpace(*req.SignName) != "" { testCfg.SignName = strings.TrimSpace(*req.SignName) } else if existingCfg != nil { testCfg.SignName = existingCfg.SignName } if req.LoginTemplateCode != nil && strings.TrimSpace(*req.LoginTemplateCode) != "" { testCfg.LoginTemplateCode = strings.TrimSpace(*req.LoginTemplateCode) } else if existingCfg != nil { testCfg.LoginTemplateCode = existingCfg.LoginTemplateCode } providerType, err := s.repo.validateSMSProvider(testCfg.Provider) if err != nil { return err } testCfg.Provider = providerType if providerType == "aliyun" { if strings.TrimSpace(testCfg.AccessKeyID) == "" || strings.TrimSpace(testCfg.AccessKeySecret) == "" || strings.TrimSpace(testCfg.SignName) == "" || strings.TrimSpace(testCfg.LoginTemplateCode) == "" { return ErrSMSConfigInvalid } } return s.smsResolver.TestSendSMS(ctx, siteID, testCfg, phone) } func (s *Service) ListDistributionUsers(ctx context.Context, adminID, siteID uint64, query ListDistributionUsersQuery) (*DistributionUserPageDTO, error) { if err := s.available(); err != nil { return nil, err } return s.repo.ListDistributionUsers(ctx, adminID, siteID, query) }