// Package sitecode 统一解析请求中的分销站点编码。 // // 此前各业务模块(auth/order/wallet/chat/notification/withdrawal/listing/distribution) // 各自内联解析,优先级和 TrimSpace 处理不一致,且对旧头 X-Site-Code 的支持参差不齐。 // 本包收敛为两个入口: // // - FromRequest:常规业务接口使用。SPA 前端(frontend/src/shared/api/client.ts) // 会为每个请求统一注入 X-Distribution-Site-Code 头,因此头是权威来源; // query 参数与旧头仅作为非 SPA 调用(脚本、排障)的兜底。 // - FromQueryThenHeader:仅用于 GET /distribution/site-info 站点探测。 // SPA 初始化探测新站点时会话头尚未建立,此时显式 query 参数必须压过会话头。 package sitecode import ( "net" "strings" "github.com/gin-gonic/gin" ) // HeaderName 是前端统一注入的权威站点编码请求头。 const HeaderName = "X-Distribution-Site-Code" // LegacyHeaderName 是历史版本使用的站点编码请求头,仅作兜底兼容。 const LegacyHeaderName = "X-Site-Code" // FromRequest 按权威优先级解析站点编码: // X-Distribution-Site-Code 头 → X-Site-Code 旧头 → distribution_site_code / site_code query。 // 所有来源均做 TrimSpace,返回空串表示主站。 func FromRequest(c *gin.Context) string { return firstNonEmpty( c.GetHeader(HeaderName), c.GetHeader(LegacyHeaderName), c.Query("distribution_site_code"), c.Query("site_code"), ) } // FromQueryThenHeader 仅用于站点探测接口(GET /distribution/site-info): // 显式 query 参数优先于会话头。SPA 探测新站点时会话头里还是上一个站点的编码, // 必须让 query 压过它,否则从主站跳转到分站时会探测到错误站点。 func FromQueryThenHeader(c *gin.Context) string { return firstNonEmpty( c.Query("code"), c.Query("site_code"), c.Query("distribution_site_code"), c.GetHeader(HeaderName), c.GetHeader(LegacyHeaderName), ) } // ExtractHost 从 HTTP Host(可能带端口、IPv6 方括号)中提取纯主机名,小写化。 func ExtractHost(rawHost string) string { rawHost = strings.TrimSpace(rawHost) if rawHost == "" { return "" } var host string if h, _, err := net.SplitHostPort(rawHost); err == nil { host = h } else { host = rawHost } return strings.ToLower(strings.Trim(host, "[]")) } // IsLocalHost 判断主机名是否属于本地开发地址(不作为站点域名解析)。 func IsLocalHost(host string) bool { return host == "localhost" || host == "127.0.0.1" || host == "::1" } func firstNonEmpty(values ...string) string { for _, v := range values { if trimmed := strings.TrimSpace(v); trimmed != "" { return trimmed } } return "" }