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:
@@ -0,0 +1,132 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"hfb_sys/backend/internal/sitecode"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
// SiteDomainResolver 按访问域名解析启用中的分销站点编码;无匹配返回空串。
|
||||
type SiteDomainResolver interface {
|
||||
ResolveSiteCodeByDomain(ctx context.Context, domain string) (string, error)
|
||||
}
|
||||
|
||||
// siteDomainCacheTTL 域名→站点编码的进程内缓存时长。
|
||||
// 站点域名极少变更,1 分钟足以吸收首屏探测与脚本产生的无站点头请求,
|
||||
// 又不会让改绑后的域名长时间解析到旧站点。
|
||||
const siteDomainCacheTTL = time.Minute
|
||||
|
||||
type siteDomainCacheEntry struct {
|
||||
code string
|
||||
expiresAt time.Time
|
||||
}
|
||||
|
||||
type siteDomainCache struct {
|
||||
mu sync.RWMutex
|
||||
items map[string]siteDomainCacheEntry
|
||||
}
|
||||
|
||||
func newSiteDomainCache() *siteDomainCache {
|
||||
return &siteDomainCache{items: make(map[string]siteDomainCacheEntry)}
|
||||
}
|
||||
|
||||
func (c *siteDomainCache) get(host string) (string, bool) {
|
||||
c.mu.RLock()
|
||||
entry, ok := c.items[host]
|
||||
c.mu.RUnlock()
|
||||
if !ok || time.Now().After(entry.expiresAt) {
|
||||
return "", false
|
||||
}
|
||||
return entry.code, true
|
||||
}
|
||||
|
||||
func (c *siteDomainCache) set(host, code string) {
|
||||
c.mu.Lock()
|
||||
c.items[host] = siteDomainCacheEntry{code: code, expiresAt: time.Now().Add(siteDomainCacheTTL)}
|
||||
c.mu.Unlock()
|
||||
}
|
||||
|
||||
// SiteDomainFallback 在请求未显式携带站点编码时,按访问域名兜底识别分销站点,
|
||||
// 并把结果回填到权威请求头,使后续所有 sitecode.FromRequest 调用自动生效。
|
||||
//
|
||||
// 背景:站点归属此前完全依赖前端注入的 X-Distribution-Site-Code 头,
|
||||
// 首屏探测失败或漏带就会把分站写入当成主站(2026-10-10 之前已因此丢过一批注册归属)。
|
||||
// 显式头/query 始终优先,本中间件只在两者都为空时兜底;数据库异常时不阻断请求,
|
||||
// 退化为原有行为(视为主站)。
|
||||
func SiteDomainFallback(resolver SiteDomainResolver, logger *zap.Logger) gin.HandlerFunc {
|
||||
if resolver == nil {
|
||||
return func(c *gin.Context) { c.Next() }
|
||||
}
|
||||
cache := newSiteDomainCache()
|
||||
return func(c *gin.Context) {
|
||||
if sitecode.FromRequest(c) != "" {
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
if code := resolveSiteCodeByHost(c, resolver, cache, logger); code != "" {
|
||||
c.Request.Header.Set(sitecode.HeaderName, code)
|
||||
}
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
func resolveSiteCodeByHost(c *gin.Context, resolver SiteDomainResolver, cache *siteDomainCache, logger *zap.Logger) string {
|
||||
for _, host := range candidateHosts(c) {
|
||||
if code, ok := cache.get(host); ok {
|
||||
if code != "" {
|
||||
return code
|
||||
}
|
||||
continue
|
||||
}
|
||||
code, err := resolver.ResolveSiteCodeByDomain(c.Request.Context(), host)
|
||||
if err != nil {
|
||||
// 记入缓存,避免数据库故障时每个请求都打一次日志与查询。
|
||||
cache.set(host, "")
|
||||
if logger != nil {
|
||||
logger.Warn("按域名兜底解析分销站点失败,已按未指定站点处理",
|
||||
zap.String("host", host),
|
||||
zap.Error(err),
|
||||
)
|
||||
}
|
||||
continue
|
||||
}
|
||||
cache.set(host, code)
|
||||
if code != "" {
|
||||
return code
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// candidateHosts 返回可能承载站点域名的候选主机名(去空、去本地地址、去重)。
|
||||
// 依次尝试请求 Host 与反代透传的 X-Forwarded-Host。
|
||||
func candidateHosts(c *gin.Context) []string {
|
||||
raw := []string{c.Request.Host}
|
||||
for _, value := range strings.Split(c.GetHeader("X-Forwarded-Host"), ",") {
|
||||
raw = append(raw, value)
|
||||
}
|
||||
hosts := make([]string, 0, len(raw))
|
||||
for _, item := range raw {
|
||||
host := sitecode.ExtractHost(item)
|
||||
if host == "" || sitecode.IsLocalHost(host) {
|
||||
continue
|
||||
}
|
||||
duplicated := false
|
||||
for _, existing := range hosts {
|
||||
if existing == host {
|
||||
duplicated = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !duplicated {
|
||||
hosts = append(hosts, host)
|
||||
}
|
||||
}
|
||||
return hosts
|
||||
}
|
||||
Reference in New Issue
Block a user