init: 初始化 web_hgdjb_server 项目
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -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
|
||||
}
|
||||
@@ -0,0 +1,194 @@
|
||||
package useragent
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/url"
|
||||
"strings"
|
||||
"videoweb/pub/common/enum/consts"
|
||||
"videoweb/pub/common/log"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type UA struct {
|
||||
UserAgent string
|
||||
DevID string
|
||||
DevType string
|
||||
SysType string
|
||||
Ver string
|
||||
BuildID string
|
||||
SystemName string
|
||||
SystemVersion string
|
||||
DeviceBrand string
|
||||
DeviceModel string
|
||||
SessionID string
|
||||
SDKCommon *SDKCommonFields // SDK 公共参数独立保存,不能与原业务 UA 字段混用。
|
||||
RequestPath string // 仅用于定位埋点参数缺失的业务接口。
|
||||
}
|
||||
|
||||
func IsH5(sysType string) bool {
|
||||
if sysType == consts.H5IOS || sysType == consts.H5Android || sysType == consts.H5PC || sysType == consts.H5 || sysType == consts.Light {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (s UA) IsH5() bool {
|
||||
return IsH5(s.SysType)
|
||||
}
|
||||
|
||||
// FromContext 读取当前请求已经解析好的 UA。下单要在请求结束前把这份值存进订单。
|
||||
func FromContext(ctx context.Context) UA {
|
||||
if ctx == nil {
|
||||
return UA{}
|
||||
}
|
||||
if c, ok := ctx.(*gin.Context); ok {
|
||||
if value, exists := c.Get(consts.CtxUA); exists {
|
||||
if ua, ok := value.(UA); ok {
|
||||
return ua
|
||||
}
|
||||
}
|
||||
}
|
||||
if value := ctx.Value(consts.CtxUA); value != nil {
|
||||
if ua, ok := value.(UA); ok {
|
||||
return ua
|
||||
}
|
||||
}
|
||||
return UA{}
|
||||
}
|
||||
|
||||
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))
|
||||
}
|
||||
ua.SessionID = c.GetHeader("sid")
|
||||
userAgent := c.GetHeader("X-User-Agent") // 兼容 H5 UserAgent
|
||||
if userAgent == "" {
|
||||
userAgent = c.Request.UserAgent()
|
||||
}
|
||||
c.Set(consts.CtxUAH5, c.Request.UserAgent())
|
||||
if userAgent != "" {
|
||||
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 {
|
||||
segment := strings.TrimSpace(v)
|
||||
if segment == "" {
|
||||
continue
|
||||
}
|
||||
vss := strings.SplitN(segment, "=", 2)
|
||||
if len(vss) < 2 {
|
||||
continue
|
||||
}
|
||||
key := strings.TrimSpace(vss[0])
|
||||
value := strings.TrimSpace(vss[1])
|
||||
lowerKey := strings.ToLower(key)
|
||||
switch lowerKey {
|
||||
case "devid":
|
||||
ua.DevID = value
|
||||
c.Set(consts.CtxDevId, ua.DevID)
|
||||
case "devtype":
|
||||
ua.DevType = value
|
||||
c.Set(consts.CtxDevType, ua.DevType)
|
||||
case "systype":
|
||||
inputSysType := strings.ToLower(strings.TrimSpace(value))
|
||||
switch {
|
||||
case strings.Contains(inputSysType, "h5_android"):
|
||||
ua.SysType = consts.H5Android
|
||||
case strings.Contains(inputSysType, "h5_ios"):
|
||||
ua.SysType = consts.H5IOS
|
||||
case strings.Contains(inputSysType, "h5_pc"):
|
||||
ua.SysType = consts.H5PC
|
||||
case strings.Contains(inputSysType, "android"):
|
||||
ua.SysType = consts.Android
|
||||
case strings.Contains(inputSysType, "ios"):
|
||||
ua.SysType = consts.IOS
|
||||
case strings.Contains(inputSysType, "h5"):
|
||||
ua.SysType = consts.H5
|
||||
case strings.Contains(inputSysType, "pc"):
|
||||
ua.SysType = consts.PC
|
||||
case strings.Contains(inputSysType, "light"):
|
||||
ua.SysType = consts.Light
|
||||
}
|
||||
c.Set(consts.CtxSysType, ua.SysType)
|
||||
case "ver":
|
||||
ua.Ver = value
|
||||
c.Set(consts.CtxVersion, ua.Ver)
|
||||
case "buildid":
|
||||
ua.BuildID = value
|
||||
c.Set(consts.CtxBuildId, ua.BuildID)
|
||||
case "system_name", "system-name", "systemname":
|
||||
if ua.SystemName == "" {
|
||||
ua.SystemName = value
|
||||
}
|
||||
case "system_version", "system-version", "systemversion":
|
||||
if ua.SystemVersion == "" {
|
||||
ua.SystemVersion = value
|
||||
}
|
||||
case "device_brand", "device-brand", "devicebrand":
|
||||
if ua.DeviceBrand == "" {
|
||||
ua.DeviceBrand = value
|
||||
}
|
||||
case "device_model", "device-model", "devicemodel":
|
||||
if ua.DeviceModel == "" {
|
||||
ua.DeviceModel = value
|
||||
}
|
||||
case "sid":
|
||||
if ua.SessionID == "" {
|
||||
ua.SessionID = value
|
||||
}
|
||||
default:
|
||||
log.Warn("user-agent unknown", log.Any("ua", userAgent), log.Any("segment", segment))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
log.Warn("user-agent empty")
|
||||
}
|
||||
ua.UserAgent = uastr
|
||||
if ua.DeviceBrand == "" {
|
||||
brand := strings.TrimSpace(c.GetHeader("device_brand"))
|
||||
if brand == "" {
|
||||
brand = strings.TrimSpace(c.GetHeader("device-brand"))
|
||||
}
|
||||
ua.DeviceBrand = brand
|
||||
}
|
||||
if ua.DeviceModel == "" {
|
||||
model := strings.TrimSpace(c.GetHeader("device_model"))
|
||||
if model == "" {
|
||||
model = strings.TrimSpace(c.GetHeader("device-model"))
|
||||
}
|
||||
ua.DeviceModel = model
|
||||
}
|
||||
if ua.SystemName == "" {
|
||||
name := strings.TrimSpace(c.GetHeader("system_name"))
|
||||
if name == "" {
|
||||
name = strings.TrimSpace(c.GetHeader("system-name"))
|
||||
}
|
||||
ua.SystemName = name
|
||||
}
|
||||
if ua.SystemVersion == "" {
|
||||
version := strings.TrimSpace(c.GetHeader("system_version"))
|
||||
if version == "" {
|
||||
version = strings.TrimSpace(c.GetHeader("system-version"))
|
||||
}
|
||||
ua.SystemVersion = version
|
||||
}
|
||||
if ua.SystemName == "" {
|
||||
ua.SystemName = ua.SysType
|
||||
}
|
||||
if ua.SystemVersion == "" {
|
||||
ua.SystemVersion = ua.Ver
|
||||
}
|
||||
c.Set(consts.CtxSystemName, ua.SystemName)
|
||||
c.Set(consts.CtxSystemVersion, ua.SystemVersion)
|
||||
c.Set(consts.CtxUA, ua)
|
||||
}
|
||||
Reference in New Issue
Block a user