feat(distribution): 站点归属兜底——未带站点编码时按访问域名识别分站
- 新增 middleware.SiteDomainFallback:显式头/query 优先,两者都为空时用 Request.Host / X-Forwarded-Host 反查 distribution_sites.domain,并回填 X-Distribution-Site-Code 供后续 sitecode.FromRequest 使用 - 新增 distribution.Repository.ResolveSiteCodeByDomain(仅取 status=active 的站点) - 路由层在所有业务路由之前注册;域名到站点映射走 1 分钟进程内缓存(含负缓存);查库异常不阻断请求、退化为按主站处理;回环/本地地址跳过查询 - 背景:站点归属此前完全依赖前端注入头,首屏探测失败或漏带就会把分站写入记成主站(2026-10-10 前已因此丢失一批分站注册归属) - 新增单测:中间件(显式优先、域名解析、本地地址跳过、转发域兜底、缓存与容错)与仓储域名查询边界(大小写/空白/主站域名/停用站点)
This commit is contained in:
@@ -2679,6 +2679,34 @@ func (r *Repository) evictSiteInfoCache(ctx context.Context, site *model.Distrib
|
||||
}
|
||||
}
|
||||
|
||||
// ResolveSiteCodeByDomain 按访问域名解析启用中的分销站点编码,找不到返回空串。
|
||||
//
|
||||
// 用途:站点归属兜底。前端(SPA)会显式带 X-Distribution-Site-Code 头,
|
||||
// 但首屏探测、脚本调用、分享链接直达等场景可能没带;此时按请求域名判定站点,
|
||||
// 避免注册等写入把分站用户记成主站(历史上就丢过一批)。
|
||||
func (r *Repository) ResolveSiteCodeByDomain(ctx context.Context, domain string) (string, error) {
|
||||
if r == nil || r.db == nil {
|
||||
return "", ErrDependencyUnavailable
|
||||
}
|
||||
domain = strings.ToLower(strings.TrimSpace(domain))
|
||||
if domain == "" {
|
||||
return "", nil
|
||||
}
|
||||
var site model.DistributionSite
|
||||
err := r.db.WithContext(ctx).
|
||||
Select("site_code").
|
||||
Where("status = 'active' AND domain = ?", domain).
|
||||
Order("id ASC").
|
||||
First(&site).Error
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return "", nil
|
||||
}
|
||||
return "", err
|
||||
}
|
||||
return site.SiteCode, nil
|
||||
}
|
||||
|
||||
func (r *Repository) GetPublicSiteInfo(ctx context.Context, code, domain string) (*PublicSiteInfoDTO, error) {
|
||||
if r.db == nil {
|
||||
return nil, ErrDependencyUnavailable
|
||||
|
||||
@@ -42,6 +42,45 @@ func distributionTestDB(t *testing.T) *gorm.DB {
|
||||
return db
|
||||
}
|
||||
|
||||
// 站点归属兜底依赖「按访问域名反查站点编码」,这里覆盖大小写/空白、主站域名、停用站点等边界。
|
||||
func TestRepositoryResolveSiteCodeByDomain(t *testing.T) {
|
||||
db := distributionTestDB(t)
|
||||
repo := NewRepository(db)
|
||||
if err := db.Create(&model.DistributionSite{
|
||||
SiteCode: "gz_dist", SiteName: "天域商行", Domain: "tysh.hao858.com", Status: "active", TaxRateBPS: 100,
|
||||
}).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := db.Create(&model.DistributionSite{
|
||||
SiteCode: "down_site", SiteName: "停用站", Domain: "down.hao858.com", Status: "disabled", TaxRateBPS: 100,
|
||||
}).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
domain string
|
||||
want string
|
||||
}{
|
||||
{name: "命中并统一小写", domain: "TYSH.HAO858.COM", want: "gz_dist"},
|
||||
{name: "去除首尾空白", domain: " tysh.hao858.com ", want: "gz_dist"},
|
||||
{name: "主站域名无匹配", domain: "hao858.com", want: ""},
|
||||
{name: "停用站点不解析", domain: "down.hao858.com", want: ""},
|
||||
{name: "空域名", domain: " ", want: ""},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
got, err := repo.ResolveSiteCodeByDomain(t.Context(), tc.domain)
|
||||
if err != nil {
|
||||
t.Fatalf("ResolveSiteCodeByDomain(%q) error = %v", tc.domain, err)
|
||||
}
|
||||
if got != tc.want {
|
||||
t.Fatalf("ResolveSiteCodeByDomain(%q) = %q, want %q", tc.domain, got, tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDistributionOrderSnapshotAndQueries(t *testing.T) {
|
||||
db := distributionTestDB(t)
|
||||
repo := NewRepository(db)
|
||||
|
||||
Reference in New Issue
Block a user