- 后端支持分销站点编辑(域名唯一性校验、收号压价点位及税率更新) - 后端支持分销站点安全删除校验(阻断存在业务订单、财务流水、结算批次或钱包余额的站点删除;对干净站点连带清理分成规则、展示账号绑定、支付通道配置及专有站长账号) - 后端支持站长账号展示与超级管理员重置站长密码 - 主管理后台与分销后台前端全面支持筛选搜索、编辑模态窗、状态快捷切换、站长密码重置与安全删除确认 - 补充后端 CRUD、删除保护与密码重置覆盖率测试用例
449 lines
14 KiB
Go
449 lines
14 KiB
Go
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
|
|
}
|
|
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) 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)
|
|
}
|