Initial commit: web_hghl_server

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-10-26 16:28:14 +08:00
co-authored by Claude Opus 4.7
commit 65fca60622
653 changed files with 135901 additions and 0 deletions
+83
View File
@@ -0,0 +1,83 @@
package useragent
import (
"encoding/base64"
"encoding/json"
"fmt"
"strings"
"unicode/utf8"
)
// SDKCommonHeader 由 H5/Flutter 将 SDK 完整公共参数转为 UTF-8 JSON,再用标准 Base64 编码后传入。
const SDKCommonHeader = "X-SDK-Common"
// 限制解析开销;正常 SDK 公共参数远小于此大小。
const maxSDKCommonHeaderSize = 16 * 1024
// SDKCommonFields 接收五项 SDK 公共字段及客户端 app_version,完整 fields 中的其他字段自动忽略。
// 业务 DevID、用户 ID、渠道等继续走原有业务参数,不能被 SDK 对象覆盖。
type SDKCommonFields struct {
SID string `json:"sid" bson:"sid"`
DeviceID string `json:"device_id" bson:"device_id"`
Device string `json:"device" bson:"device"`
DeviceFingerprint string `json:"device_fingerprint" bson:"device_fingerprint"`
FPVersion string `json:"fp_version" bson:"fp_version"`
AppVersion string `json:"app_version" bson:"app_version"` // 客户端版本随下单快照保存,支付回调不使用当前版本补值。
}
// ParseSDKCommonHeader 缺少请求头返回 nil;格式错误交给调用方记录,不阻断业务请求。
// 不修剪、截断或推算字段值,SDK 指纹生成失败时也保留其空值。
func ParseSDKCommonHeader(header string) (*SDKCommonFields, error) {
header = strings.TrimSpace(header)
if header == "" {
return nil, nil
}
if len(header) > maxSDKCommonHeaderSize {
return nil, fmt.Errorf("SDK 公共参数请求头超过 %d 字节", maxSDKCommonHeaderSize)
}
data, err := base64.StdEncoding.DecodeString(header)
if err != nil {
return nil, fmt.Errorf("SDK 公共参数 Base64 解码失败: %w", err)
}
if !utf8.Valid(data) {
return nil, fmt.Errorf("SDK 公共参数不是有效的 UTF-8")
}
var fields *SDKCommonFields
if err := json.Unmarshal(data, &fields); err != nil {
return nil, fmt.Errorf("SDK 公共参数 JSON 解析失败: %w", err)
}
if fields == nil {
return nil, fmt.Errorf("SDK 公共参数必须是 JSON 对象,不能为 null")
}
return fields, nil
}
// Clone 下单时保存独立快照,避免请求对象后续变化影响支付事件。
func (fields *SDKCommonFields) Clone() *SDKCommonFields {
if fields == nil {
return nil
}
snapshot := *fields
return &snapshot
}
// MissingFields 仅用于缺失观测;合法空值仍原样上报,不作为业务失败。
func (fields *SDKCommonFields) MissingFields() []string {
if fields == nil {
fields = &SDKCommonFields{}
}
var missing []string
for _, field := range []struct{ name, value string }{
{"sid", fields.SID},
{"device_id", fields.DeviceID},
{"device", fields.Device},
{"device_fingerprint", fields.DeviceFingerprint},
{"fp_version", fields.FPVersion},
{"app_version", fields.AppVersion},
} {
if strings.TrimSpace(field.value) == "" {
missing = append(missing, field.name)
}
}
return missing
}
+109
View File
@@ -0,0 +1,109 @@
package useragent
import (
"net/url"
"strings"
"videoweb/pub/common/enum/consts"
"videoweb/pub/common/log"
"videoweb/pub/pubdef"
"github.com/gin-gonic/gin"
)
type UA struct {
DevID string
DevType string
SysType pubdef.SysType
Ver string
BuildID string
UserAgent string
SDKCommon *SDKCommonFields // SDK 公共参数独立保存,不能与原业务 UA 字段混用。
RequestPath string // 仅用于定位埋点参数缺失的业务接口。
}
func IsH5(sysType pubdef.SysType) bool {
return sysType.IsH5()
}
func (s UA) IsH5() bool {
return s.SysType.IsH5()
}
func UserAgent(c *gin.Context) {
var uastr string
ua := UA{RequestPath: c.FullPath()}
var sdkErr error
ua.SDKCommon, sdkErr = ParseSDKCommonHeader(c.GetHeader(SDKCommonHeader))
if sdkErr != nil {
log.Error("【SDK公共参数解析失败】UserAgent", log.Any("接口", ua.RequestPath), log.E(sdkErr))
}
userAgent := c.GetHeader("X-User-Agent") //兼容H5 UserAgent
if userAgent == "" {
userAgent = c.Request.UserAgent()
}
ua.UserAgent = userAgent
if userAgent != "" {
// "BuildID=com.abc.Butterfly;SysType=h5;DevID=B9A00938-FE68-4686-9461-A365C7E5110D;Ver=0.0.1;DevType=iPhone;"
decodeUa, err := url.QueryUnescape(userAgent)
if err != nil {
log.Warn("URLDecode user-agent error", log.Any("ua", userAgent))
uastr = userAgent
} else {
uastr = decodeUa
}
uas := strings.Split(uastr, ";")
for _, v := range uas {
vss := strings.Split(strings.TrimSpace(v), "=")
if len(vss) < 2 {
c.Set(consts.CtxUAH5, vss[0])
// log.Warn("user-agent miss", log.Any("ua", userAgent), log.Any("v", v))
continue
}
switch strings.TrimSpace(vss[0]) {
case "DevID":
ua.DevID = strings.TrimSpace(vss[1])
c.Set(consts.CtxDevId, ua.DevID)
case "DevType":
ua.DevType = strings.TrimSpace(vss[1])
c.Set(consts.CtxDevType, ua.DevType)
case "SysType":
input := strings.ToLower(strings.TrimSpace(vss[1]))
switch {
case strings.Contains(input, "h5_android"):
ua.SysType = pubdef.ST_H5Android
case strings.Contains(input, "h5_ios"):
ua.SysType = pubdef.ST_H5IOS
case strings.Contains(input, "h5_pc"):
ua.SysType = pubdef.ST_H5PC
case strings.Contains(input, "android"):
ua.SysType = pubdef.ST_Android
case strings.Contains(input, "ios"):
ua.SysType = pubdef.ST_IOS
case strings.Contains(input, "h5"):
ua.SysType = pubdef.ST_H5
case strings.Contains(input, "pc"):
ua.SysType = pubdef.ST_PC
case strings.Contains(input, "light"):
ua.SysType = pubdef.ST_Light
case strings.Contains(input, "tg"):
ua.SysType = pubdef.ST_TG
case strings.Contains(input, "web"):
ua.SysType = pubdef.ST_Web
}
// 存入 context 时用 string,保持 ctx.GetString 兼容
c.Set(consts.CtxSysType, string(ua.SysType))
case "Ver":
ua.Ver = strings.TrimSpace(vss[1])
c.Set(consts.CtxVersion, ua.Ver)
case "BuildID":
ua.BuildID = strings.TrimSpace(vss[1])
c.Set(consts.CtxBuildId, ua.BuildID)
default:
log.Warn("user-agent unknown", log.Any("ua", userAgent))
}
}
} else {
log.Warn("user-agent empty")
}
c.Set(consts.CtxUA, ua)
}