feat(sms): 新增节点短信提醒(审核未通过/被租用需辅助/上架未成交)
This commit is contained in:
@@ -597,14 +597,15 @@ type DistributionTrendPoint struct {
|
||||
|
||||
// 分销站点独立短信配置结构体
|
||||
type SiteSMSConfigDTO struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
Provider string `json:"provider"`
|
||||
AccessKeyID string `json:"access_key_id"`
|
||||
MaskedSecret string `json:"masked_secret"`
|
||||
HasSecret bool `json:"has_secret"`
|
||||
Endpoint string `json:"endpoint"`
|
||||
SignName string `json:"sign_name"`
|
||||
LoginTemplateCode string `json:"login_template_code"`
|
||||
Enabled bool `json:"enabled"`
|
||||
Provider string `json:"provider"`
|
||||
AccessKeyID string `json:"access_key_id"`
|
||||
MaskedSecret string `json:"masked_secret"`
|
||||
HasSecret bool `json:"has_secret"`
|
||||
Endpoint string `json:"endpoint"`
|
||||
SignName string `json:"sign_name"`
|
||||
LoginTemplateCode string `json:"login_template_code"`
|
||||
BusinessTemplates map[string]string `json:"business_templates"`
|
||||
}
|
||||
|
||||
type UpdateSiteSMSConfigRequest struct {
|
||||
@@ -615,6 +616,8 @@ type UpdateSiteSMSConfigRequest struct {
|
||||
Endpoint *string `json:"endpoint"`
|
||||
SignName *string `json:"sign_name"`
|
||||
LoginTemplateCode *string `json:"login_template_code"`
|
||||
// BusinessTemplates 传入时整体覆盖站点节点模板映射,传空对象即清空。
|
||||
BusinessTemplates *map[string]string `json:"business_templates"`
|
||||
}
|
||||
|
||||
type TestSiteSMSConfigRequest struct {
|
||||
|
||||
@@ -1045,6 +1045,10 @@ func (r *Repository) UpdateSiteSMSConfig(ctx context.Context, adminID, siteID ui
|
||||
if req.LoginTemplateCode != nil {
|
||||
cfg.LoginTemplateCode = strings.TrimSpace(*req.LoginTemplateCode)
|
||||
}
|
||||
if req.BusinessTemplates != nil {
|
||||
// 整体覆盖语义:后台按节点逐一提交,空对象表示清空并回退主站模板。
|
||||
cfg.BusinessTemplates = normalizeBusinessTemplates(*req.BusinessTemplates)
|
||||
}
|
||||
|
||||
// 完整性校验:必须属于合法 provider 白名单,且开启时必须提供完整凭据
|
||||
providerType, err := r.validateSMSProvider(cfg.Provider)
|
||||
|
||||
@@ -0,0 +1,230 @@
|
||||
package distribution
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"hfb_sys/backend/internal/model"
|
||||
smsnode "hfb_sys/backend/internal/modules/sms"
|
||||
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
func TestParseSiteSMSConfigNormalizesBusinessTemplates(t *testing.T) {
|
||||
cfg, err := ParseSiteSMSConfig(`{
|
||||
"enabled": true,
|
||||
"sign_name": "分站签名",
|
||||
"business_templates": {
|
||||
"order_handoff_assist": " SMS_512540945 ",
|
||||
"listing_review_rejected": " ",
|
||||
"unknown_node": "SMS_BAD",
|
||||
"": "SMS_EMPTY"
|
||||
}
|
||||
}`)
|
||||
if err != nil {
|
||||
t.Fatalf("ParseSiteSMSConfig() error = %v", err)
|
||||
}
|
||||
if len(cfg.BusinessTemplates) != 1 {
|
||||
t.Fatalf("business_templates = %+v, want 仅保留已登记且非空的模板", cfg.BusinessTemplates)
|
||||
}
|
||||
if got := cfg.BusinessTemplates[smsnode.NodeOrderHandoffAssist]; got != "SMS_512540945" {
|
||||
t.Fatalf("order_handoff_assist = %q, want SMS_512540945", got)
|
||||
}
|
||||
if _, ok := cfg.BusinessTemplates["unknown_node"]; ok {
|
||||
t.Fatal("未登记节点不应保留")
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateSiteSMSConfigPersistsBusinessTemplates(t *testing.T) {
|
||||
db := distributionTestDB(t)
|
||||
repo := NewRepository(db, nil)
|
||||
ctx := context.Background()
|
||||
|
||||
site := model.DistributionSite{
|
||||
SiteCode: "site_sms_node",
|
||||
SiteName: "节点短信站",
|
||||
Domain: "smsnode.com",
|
||||
Status: "active",
|
||||
}
|
||||
if err := db.Create(&site).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
templates := map[string]string{
|
||||
smsnode.NodeOrderHandoffAssist: "SMS_512540945",
|
||||
smsnode.NodeListingReviewRejected: "SMS_512520993",
|
||||
"unknown_node": "SMS_BAD",
|
||||
}
|
||||
updated, err := repo.UpdateSiteSMSConfig(ctx, 0, site.ID, UpdateSiteSMSConfigRequest{
|
||||
BusinessTemplates: &templates,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("UpdateSiteSMSConfig() error = %v", err)
|
||||
}
|
||||
if len(updated.BusinessTemplates) != 2 {
|
||||
t.Fatalf("business_templates = %+v, want 2 项(过滤未登记节点)", updated.BusinessTemplates)
|
||||
}
|
||||
|
||||
// 未传 BusinessTemplates 时不能被清空。
|
||||
newSign := "分站签名"
|
||||
if _, err := repo.UpdateSiteSMSConfig(ctx, 0, site.ID, UpdateSiteSMSConfigRequest{SignName: &newSign}); err != nil {
|
||||
t.Fatalf("UpdateSiteSMSConfig() error = %v", err)
|
||||
}
|
||||
reloaded, err := repo.GetSiteSMSConfig(ctx, 0, site.ID)
|
||||
if err != nil {
|
||||
t.Fatalf("GetSiteSMSConfig() error = %v", err)
|
||||
}
|
||||
if reloaded.BusinessTemplates[smsnode.NodeOrderHandoffAssist] != "SMS_512540945" {
|
||||
t.Fatalf("未传该字段时模板应保留,实际 = %+v", reloaded.BusinessTemplates)
|
||||
}
|
||||
|
||||
// 传空对象表示清空,节点回退主站模板。
|
||||
empty := map[string]string{}
|
||||
cleared, err := repo.UpdateSiteSMSConfig(ctx, 0, site.ID, UpdateSiteSMSConfigRequest{BusinessTemplates: &empty})
|
||||
if err != nil {
|
||||
t.Fatalf("UpdateSiteSMSConfig() error = %v", err)
|
||||
}
|
||||
if len(cleared.BusinessTemplates) != 0 {
|
||||
t.Fatalf("business_templates 应被清空, 实际 = %+v", cleared.BusinessTemplates)
|
||||
}
|
||||
}
|
||||
|
||||
// 后台改了站点短信凭据后,长期运行的任务必须能自动重建 Provider,
|
||||
// 否则更换 AccessKey 要重启进程才会生效。
|
||||
func TestResolveBusinessSenderRebuildsProviderAfterCredentialChange(t *testing.T) {
|
||||
db := distributionTestDB(t)
|
||||
repo := NewRepository(db, nil)
|
||||
repo.SetAllowMockSMS(true)
|
||||
ctx := context.Background()
|
||||
|
||||
site := model.DistributionSite{
|
||||
SiteCode: "site_sms_rotate",
|
||||
SiteName: "凭据轮换站",
|
||||
Domain: "smsrotate.com",
|
||||
Status: "active",
|
||||
}
|
||||
if err := db.Create(&site).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
enabled := true
|
||||
provider := "mock"
|
||||
sign := "【分站签名】"
|
||||
oldKey := "LTAI_OLD_KEY"
|
||||
if _, err := repo.UpdateSiteSMSConfig(ctx, 0, site.ID, UpdateSiteSMSConfigRequest{
|
||||
Enabled: &enabled,
|
||||
Provider: &provider,
|
||||
SignName: &sign,
|
||||
AccessKeyID: &oldKey,
|
||||
}); err != nil {
|
||||
t.Fatalf("UpdateSiteSMSConfig() error = %v", err)
|
||||
}
|
||||
|
||||
resolver := NewSMSResolver(repo, zap.NewNop())
|
||||
first, _, _, err := resolver.ResolveBusinessSender(ctx, site.ID)
|
||||
if err != nil {
|
||||
t.Fatalf("ResolveBusinessSender() error = %v", err)
|
||||
}
|
||||
if first == nil {
|
||||
t.Fatal("站点已启用独立短信时应返回通道")
|
||||
}
|
||||
|
||||
// 凭据未变化时应命中缓存,返回同一实例。
|
||||
again, _, _, err := resolver.ResolveBusinessSender(ctx, site.ID)
|
||||
if err != nil {
|
||||
t.Fatalf("ResolveBusinessSender() error = %v", err)
|
||||
}
|
||||
if again != first {
|
||||
t.Fatal("凭据未变化时应复用缓存 Provider")
|
||||
}
|
||||
|
||||
newKey := "LTAI_NEW_KEY"
|
||||
if _, err := repo.UpdateSiteSMSConfig(ctx, 0, site.ID, UpdateSiteSMSConfigRequest{
|
||||
AccessKeyID: &newKey,
|
||||
}); err != nil {
|
||||
t.Fatalf("UpdateSiteSMSConfig() error = %v", err)
|
||||
}
|
||||
|
||||
rebuilt, _, _, err := resolver.ResolveBusinessSender(ctx, site.ID)
|
||||
if err != nil {
|
||||
t.Fatalf("ResolveBusinessSender() error = %v", err)
|
||||
}
|
||||
if rebuilt == nil {
|
||||
t.Fatal("凭据轮换后仍应返回可用通道")
|
||||
}
|
||||
if rebuilt == first {
|
||||
t.Fatal("凭据轮换后应重建 Provider,而不是继续复用旧缓存")
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveBusinessSenderReturnsSiteTemplates(t *testing.T) {
|
||||
db := distributionTestDB(t)
|
||||
repo := NewRepository(db, nil)
|
||||
repo.SetAllowMockSMS(true)
|
||||
ctx := context.Background()
|
||||
|
||||
site := model.DistributionSite{
|
||||
SiteCode: "site_sms_sender",
|
||||
SiteName: "节点短信通道站",
|
||||
Domain: "smssender.com",
|
||||
Status: "active",
|
||||
}
|
||||
if err := db.Create(&site).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
enabled := true
|
||||
provider := "mock"
|
||||
sign := "【分站签名】"
|
||||
templates := map[string]string{smsnode.NodeOrderHandoffAssist: "SMS_SITE"}
|
||||
if _, err := repo.UpdateSiteSMSConfig(ctx, 0, site.ID, UpdateSiteSMSConfigRequest{
|
||||
Enabled: &enabled,
|
||||
Provider: &provider,
|
||||
SignName: &sign,
|
||||
BusinessTemplates: &templates,
|
||||
}); err != nil {
|
||||
t.Fatalf("UpdateSiteSMSConfig() error = %v", err)
|
||||
}
|
||||
|
||||
resolver := NewSMSResolver(repo, zap.NewNop())
|
||||
sender, signName, siteTemplates, err := resolver.ResolveBusinessSender(ctx, site.ID)
|
||||
if err != nil {
|
||||
t.Fatalf("ResolveBusinessSender() error = %v", err)
|
||||
}
|
||||
if sender == nil {
|
||||
t.Fatal("站点已启用独立短信时应返回通道")
|
||||
}
|
||||
if signName != sign {
|
||||
t.Fatalf("signName = %q, want %q", signName, sign)
|
||||
}
|
||||
if siteTemplates[smsnode.NodeOrderHandoffAssist] != "SMS_SITE" {
|
||||
t.Fatalf("站点模板 = %+v", siteTemplates)
|
||||
}
|
||||
|
||||
// 站点未启用独立短信时返回 nil,调用方回退主站通道。
|
||||
other := model.DistributionSite{
|
||||
SiteCode: "site_sms_off",
|
||||
SiteName: "未启用短信站",
|
||||
Domain: "smsoff.com",
|
||||
Status: "active",
|
||||
}
|
||||
if err := db.Create(&other).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
disabledSender, _, _, err := resolver.ResolveBusinessSender(ctx, other.ID)
|
||||
if err != nil {
|
||||
t.Fatalf("ResolveBusinessSender() error = %v", err)
|
||||
}
|
||||
if disabledSender != nil {
|
||||
t.Fatal("站点未启用时应返回 nil 以便回退主站")
|
||||
}
|
||||
|
||||
// 站点不存在(或非 active)时同样返回 nil,不报错。
|
||||
missingSender, _, _, err := resolver.ResolveBusinessSender(ctx, 999999)
|
||||
if err != nil {
|
||||
t.Fatalf("ResolveBusinessSender() error = %v", err)
|
||||
}
|
||||
if missingSender != nil {
|
||||
t.Fatal("站点不存在时应返回 nil")
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,8 @@ package distribution
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
@@ -13,6 +15,7 @@ import (
|
||||
|
||||
smsprovider "hfb_sys/backend/internal/integrations/sms"
|
||||
"hfb_sys/backend/internal/model"
|
||||
smsnode "hfb_sys/backend/internal/modules/sms"
|
||||
|
||||
"go.uber.org/zap"
|
||||
"gorm.io/gorm"
|
||||
@@ -33,6 +36,30 @@ type SiteSMSConfig struct {
|
||||
Endpoint string `json:"endpoint,omitempty"`
|
||||
SignName string `json:"sign_name"`
|
||||
LoginTemplateCode string `json:"login_template_code"`
|
||||
// BusinessTemplates 站点自有的节点短信模板 CODE,key 为 sms 模块登记的节点标识。
|
||||
// 站点启用独立短信后,这里配置了模板的节点走站点通道与站点签名;
|
||||
// 未配置的节点回退主站通道与主站模板。
|
||||
BusinessTemplates map[string]string `json:"business_templates,omitempty"`
|
||||
}
|
||||
|
||||
// normalizeBusinessTemplates 只保留已登记节点且模板 CODE 非空的配置项。
|
||||
func normalizeBusinessTemplates(in map[string]string) map[string]string {
|
||||
if len(in) == 0 {
|
||||
return nil
|
||||
}
|
||||
out := make(map[string]string, len(in))
|
||||
for code, template := range in {
|
||||
code = strings.TrimSpace(code)
|
||||
template = strings.TrimSpace(template)
|
||||
if code == "" || template == "" || !smsnode.IsKnownNode(code) {
|
||||
continue
|
||||
}
|
||||
out[code] = template
|
||||
}
|
||||
if len(out) == 0 {
|
||||
return nil
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// MaskSecret 对敏感密钥进行掩码脱敏
|
||||
@@ -62,6 +89,7 @@ func (c *SiteSMSConfig) ToDTO() *SiteSMSConfigDTO {
|
||||
Endpoint: c.Endpoint,
|
||||
SignName: c.SignName,
|
||||
LoginTemplateCode: c.LoginTemplateCode,
|
||||
BusinessTemplates: normalizeBusinessTemplates(c.BusinessTemplates),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,6 +103,7 @@ func ParseSiteSMSConfig(raw string) (*SiteSMSConfig, error) {
|
||||
if err := json.Unmarshal([]byte(raw), &cfg); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cfg.BusinessTemplates = normalizeBusinessTemplates(cfg.BusinessTemplates)
|
||||
return &cfg, nil
|
||||
}
|
||||
|
||||
@@ -115,12 +144,35 @@ func BuildProvider(cfg SiteSMSConfig, logger *zap.Logger) (smsprovider.Provider,
|
||||
}
|
||||
}
|
||||
|
||||
// cachedSMSProvider 是站点 Provider 的缓存项。
|
||||
// fingerprint 取自站点凭据与关键字段,凭据轮换后指纹变化即自动重建,
|
||||
// 避免「后台改了 AccessKey,长期运行的任务仍在用旧缓存」。
|
||||
type cachedSMSProvider struct {
|
||||
provider smsprovider.Provider
|
||||
fingerprint string
|
||||
}
|
||||
|
||||
// cacheFingerprint 用凭据密文与关键字段生成缓存指纹。
|
||||
// 密文即每次保存都会变化,因此重复保存同一份配置最多只是重建一次 Provider,不影响正确性。
|
||||
func (c *SiteSMSConfig) cacheFingerprint() string {
|
||||
raw := strings.Join([]string{
|
||||
strings.TrimSpace(c.Provider),
|
||||
strings.TrimSpace(c.AccessKeyID),
|
||||
strings.TrimSpace(c.AccessKeySecret),
|
||||
strings.TrimSpace(c.Endpoint),
|
||||
strings.TrimSpace(c.SignName),
|
||||
strings.TrimSpace(c.LoginTemplateCode),
|
||||
}, "|")
|
||||
sum := sha256.Sum256([]byte(raw))
|
||||
return hex.EncodeToString(sum[:])
|
||||
}
|
||||
|
||||
// SMSResolver 管理分销站点的 SMS Provider 实例与缓存
|
||||
type SMSResolver struct {
|
||||
repo *Repository
|
||||
logger *zap.Logger
|
||||
mu sync.RWMutex
|
||||
providers map[uint64]smsprovider.Provider
|
||||
providers map[uint64]cachedSMSProvider
|
||||
testCooldown sync.Map
|
||||
}
|
||||
|
||||
@@ -128,7 +180,7 @@ func NewSMSResolver(repo *Repository, logger *zap.Logger) *SMSResolver {
|
||||
return &SMSResolver{
|
||||
repo: repo,
|
||||
logger: logger,
|
||||
providers: make(map[uint64]smsprovider.Provider),
|
||||
providers: make(map[uint64]cachedSMSProvider),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -139,6 +191,23 @@ func (r *SMSResolver) InvalidateCache(siteID uint64) {
|
||||
delete(r.providers, siteID)
|
||||
}
|
||||
|
||||
// cached 返回指纹一致的缓存 Provider,指纹不一致(凭据已轮换)时返回 nil 由调用方重建。
|
||||
func (r *SMSResolver) cached(siteID uint64, fingerprint string) smsprovider.Provider {
|
||||
r.mu.RLock()
|
||||
entry, ok := r.providers[siteID]
|
||||
r.mu.RUnlock()
|
||||
if !ok || entry.provider == nil || entry.fingerprint != fingerprint {
|
||||
return nil
|
||||
}
|
||||
return entry.provider
|
||||
}
|
||||
|
||||
func (r *SMSResolver) store(siteID uint64, fingerprint string, provider smsprovider.Provider) {
|
||||
r.mu.Lock()
|
||||
r.providers[siteID] = cachedSMSProvider{provider: provider, fingerprint: fingerprint}
|
||||
r.mu.Unlock()
|
||||
}
|
||||
|
||||
// ResolveSMSProvider 根据 siteCode 或 domain 解析分销站点专属的 SMS Provider。
|
||||
// 若分销站点未启用自定义短信或配置不完整,则返回 nil,调用方将优雅降级为主站默认 Provider。
|
||||
func (r *SMSResolver) ResolveSMSProvider(ctx context.Context, siteCode, domain string) (smsprovider.Provider, error) {
|
||||
@@ -168,12 +237,10 @@ func (r *SMSResolver) ResolveSMSProvider(ctx context.Context, siteCode, domain s
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// 检查内存缓存
|
||||
r.mu.RLock()
|
||||
p, ok := r.providers[site.ID]
|
||||
r.mu.RUnlock()
|
||||
if ok && p != nil {
|
||||
return p, nil
|
||||
// 检查内存缓存;凭据已轮换(指纹不一致)时视为未命中,重建 Provider
|
||||
fingerprint := cfg.cacheFingerprint()
|
||||
if cached := r.cached(site.ID, fingerprint); cached != nil {
|
||||
return cached, nil
|
||||
}
|
||||
|
||||
// 解密 AccessKeySecret;解密失败说明密钥轮换或密文损坏,回退主站并留下告警
|
||||
@@ -203,13 +270,70 @@ func (r *SMSResolver) ResolveSMSProvider(ctx context.Context, siteCode, domain s
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
r.mu.Lock()
|
||||
r.providers[site.ID] = provider
|
||||
r.mu.Unlock()
|
||||
r.store(site.ID, fingerprint, provider)
|
||||
|
||||
return provider, nil
|
||||
}
|
||||
|
||||
// ResolveBusinessSender 返回启用独立短信的分站通道,用于业务节点短信。
|
||||
// 返回值为站点 Provider、站点签名与该站点的节点模板映射;
|
||||
// 站点未启用、配置破损或密钥解密失败时返回 nil Provider,调用方回退主站通道。
|
||||
func (r *SMSResolver) ResolveBusinessSender(ctx context.Context, siteID uint64) (smsprovider.Provider, string, map[string]string, error) {
|
||||
if r == nil || r.repo == nil || siteID == 0 {
|
||||
return nil, "", nil, nil
|
||||
}
|
||||
|
||||
site, err := r.repo.FindActiveSiteByID(ctx, siteID)
|
||||
if err != nil {
|
||||
if errors.Is(err, ErrSiteNotFound) {
|
||||
return nil, "", nil, nil
|
||||
}
|
||||
return nil, "", nil, err
|
||||
}
|
||||
if site == nil || strings.TrimSpace(site.SMSConfigJSON) == "" {
|
||||
return nil, "", nil, nil
|
||||
}
|
||||
|
||||
cfg, err := ParseSiteSMSConfig(site.SMSConfigJSON)
|
||||
if err != nil || cfg == nil || !cfg.Enabled {
|
||||
return nil, "", nil, nil
|
||||
}
|
||||
|
||||
fingerprint := cfg.cacheFingerprint()
|
||||
if cached := r.cached(site.ID, fingerprint); cached != nil {
|
||||
return cached, strings.TrimSpace(cfg.SignName), cfg.BusinessTemplates, nil
|
||||
}
|
||||
|
||||
plainSecret, decErr := r.repo.decryptSecret(cfg.AccessKeySecret)
|
||||
if decErr != nil {
|
||||
if r.logger != nil {
|
||||
r.logger.Warn("解密分销站点短信密钥失败,节点短信将回退主站通道",
|
||||
zap.Uint64("site_id", site.ID),
|
||||
zap.String("site_code", site.SiteCode),
|
||||
zap.Error(decErr),
|
||||
)
|
||||
}
|
||||
return nil, "", cfg.BusinessTemplates, nil
|
||||
}
|
||||
cfg.AccessKeySecret = plainSecret
|
||||
|
||||
provider, buildErr := BuildProvider(*cfg, r.logger)
|
||||
if buildErr != nil || provider == nil {
|
||||
if r.logger != nil {
|
||||
r.logger.Warn("分销站点自定义短信配置无效,节点短信将回退主站通道",
|
||||
zap.Uint64("site_id", site.ID),
|
||||
zap.String("site_code", site.SiteCode),
|
||||
zap.Error(buildErr),
|
||||
)
|
||||
}
|
||||
return nil, "", cfg.BusinessTemplates, nil
|
||||
}
|
||||
|
||||
r.store(site.ID, fingerprint, provider)
|
||||
|
||||
return provider, strings.TrimSpace(cfg.SignName), cfg.BusinessTemplates, nil
|
||||
}
|
||||
|
||||
// TestSendSMS 使用给定的短信配置,向指定手机号发送一条测试验证码以检验连通性
|
||||
func (r *SMSResolver) TestSendSMS(ctx context.Context, siteID uint64, cfg SiteSMSConfig, phone string) error {
|
||||
phone = strings.TrimSpace(phone)
|
||||
@@ -303,3 +427,22 @@ func (r *Repository) FindActiveSiteByCodeOrDomain(ctx context.Context, code, dom
|
||||
}
|
||||
return &site, nil
|
||||
}
|
||||
|
||||
// FindActiveSiteByID 按主键查询状态为 active 的分销站点实体。
|
||||
func (r *Repository) FindActiveSiteByID(ctx context.Context, siteID uint64) (*model.DistributionSite, error) {
|
||||
if r == nil || r.db == nil {
|
||||
return nil, ErrDependencyUnavailable
|
||||
}
|
||||
if siteID == 0 {
|
||||
return nil, ErrSiteNotFound
|
||||
}
|
||||
|
||||
var site model.DistributionSite
|
||||
if err := r.db.WithContext(ctx).Where("id = ? AND status = 'active'", siteID).First(&site).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, ErrSiteNotFound
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return &site, nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user