调整UI
This commit is contained in:
+13
-216
@@ -1,23 +1,16 @@
|
||||
package useragent
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
"videoweb/app/appg"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
const (
|
||||
DefaultVer = "1.0.0"
|
||||
SDKCommonHeader = "X-SDK-Common"
|
||||
)
|
||||
const DefaultVer = "1.0.0"
|
||||
|
||||
// Info 与前端 X-User-Agent 字段对齐,供 SSR 组装上游请求头。
|
||||
type Info struct {
|
||||
@@ -42,23 +35,17 @@ type BuildOptions struct {
|
||||
}
|
||||
|
||||
var (
|
||||
reIOSVer = regexp.MustCompile(`OS (\d+[_\d]*) like Mac OS X`)
|
||||
reAndroidVer = regexp.MustCompile(`Android ([\d.]+)`)
|
||||
reAndroidModel = regexp.MustCompile(`Android\s+[\d.]+;\s*([^;)]+?)(?:\s+Build\/|\))`)
|
||||
reWinNT = regexp.MustCompile(`Windows NT ([\d.]+)`)
|
||||
reMacVer = regexp.MustCompile(`Mac OS X ([\d_]+)`)
|
||||
reEdge = regexp.MustCompile(`(?i)Edg/([\d.]+)`)
|
||||
reOpera = regexp.MustCompile(`(?i)(?:OPR|Opera)/([\d.]+)`)
|
||||
reChrome = regexp.MustCompile(`(?i)Chrome/([\d.]+)`)
|
||||
reSafari = regexp.MustCompile(`(?i)Version/([\d.]+).*Safari`)
|
||||
reFirefox = regexp.MustCompile(`(?i)Firefox/([\d.]+)`)
|
||||
reIE = regexp.MustCompile(`(?i)(?:MSIE\s|Trident.*rv:)([\d.]+)`)
|
||||
reTrackerRequestID = regexp.MustCompile(`^[a-fA-F0-9]{32}$`)
|
||||
reTrackerCtx = regexp.MustCompile(`^[a-zA-Z0-9_-]+$`)
|
||||
reTrackerEvent = regexp.MustCompile(`^drama_(like|comment|collect|purchase)$`)
|
||||
reTrackerAppID = regexp.MustCompile(`^[A-Z]{1,6}-[0-9]{1,6}$`)
|
||||
reTrackerUID = regexp.MustCompile(`^[A-Za-z0-9_]+$`)
|
||||
reTrackerChannel = regexp.MustCompile(`^[A-Za-z0-9_.-]+$`)
|
||||
reIOSVer = regexp.MustCompile(`OS (\d+[_\d]*) like Mac OS X`)
|
||||
reAndroidVer = regexp.MustCompile(`Android ([\d.]+)`)
|
||||
reAndroidModel = regexp.MustCompile(`Android\s+[\d.]+;\s*([^;)]+?)(?:\s+Build\/|\))`)
|
||||
reWinNT = regexp.MustCompile(`Windows NT ([\d.]+)`)
|
||||
reMacVer = regexp.MustCompile(`Mac OS X ([\d_]+)`)
|
||||
reEdge = regexp.MustCompile(`(?i)Edg/([\d.]+)`)
|
||||
reOpera = regexp.MustCompile(`(?i)(?:OPR|Opera)/([\d.]+)`)
|
||||
reChrome = regexp.MustCompile(`(?i)Chrome/([\d.]+)`)
|
||||
reSafari = regexp.MustCompile(`(?i)Version/([\d.]+).*Safari`)
|
||||
reFirefox = regexp.MustCompile(`(?i)Firefox/([\d.]+)`)
|
||||
reIE = regexp.MustCompile(`(?i)(?:MSIE\s|Trident.*rv:)([\d.]+)`)
|
||||
)
|
||||
|
||||
// ParseDeviceFromUA 从浏览器原生 User-Agent 解析设备信息(服务端无 navigator)。
|
||||
@@ -335,36 +322,7 @@ func BuildUpstreamHeader(c *gin.Context, ver string) map[string]string {
|
||||
return header
|
||||
}
|
||||
|
||||
var trackerEventRoutes = map[string]string{
|
||||
"/api/playlet/chapter/like/set": "drama_like",
|
||||
"/api/playlet/like/set": "drama_like",
|
||||
"/api/playlet/chapter/collect/set": "drama_collect",
|
||||
"/api/playlet/collect/set": "drama_collect",
|
||||
"/api/comment/send": "drama_comment",
|
||||
"/api/playlet/chapter/unlock": "drama_purchase",
|
||||
}
|
||||
|
||||
var trackerCommonFieldKeys = map[string]struct{}{
|
||||
"app_id": {}, "uid": {}, "channel": {}, "sid": {}, "client_ts": {},
|
||||
"device": {}, "device_id": {}, "device_brand": {}, "device_model": {},
|
||||
"device_fingerprint": {}, "fp_version": {}, "user_agent": {},
|
||||
"sdk_version": {}, "app_version": {},
|
||||
}
|
||||
|
||||
var trackerDramaFieldKeys = map[string]struct{}{
|
||||
"media_id": {}, "drama_id": {}, "episode": {}, "video_id": {}, "recommend_trace_id": {},
|
||||
}
|
||||
|
||||
type trackerContextEnvelope struct {
|
||||
Version int `json:"v"`
|
||||
RequestID string `json:"request_id"`
|
||||
EventName string `json:"event_name"`
|
||||
Common map[string]json.RawMessage `json:"common"`
|
||||
Drama map[string]json.RawMessage `json:"drama"`
|
||||
}
|
||||
|
||||
// ApplyToHeader 将来源请求的网络信息与 SDK 原始公共字段快照写入上游 header。
|
||||
// 埋点上下文只通过 canonical envelope 白名单透传,不在 BFF 重新生成或覆盖。
|
||||
// ApplyToHeader 将来源请求的网络信息写入上游 header。
|
||||
func ApplyToHeader(header map[string]string, c *gin.Context, ver string) {
|
||||
if header == nil {
|
||||
return
|
||||
@@ -378,165 +336,4 @@ func ApplyToHeader(header map[string]string, c *gin.Context, ver string) {
|
||||
}
|
||||
}
|
||||
header["X-User-Agent"] = BuildXUserAgentFromRequest(c, ver)
|
||||
if c == nil || c.Request == nil {
|
||||
return
|
||||
}
|
||||
if sdk := strings.TrimSpace(c.GetHeader(SDKCommonHeader)); sdk != "" {
|
||||
header[SDKCommonHeader] = sdk
|
||||
}
|
||||
requestID := strings.TrimSpace(c.GetHeader("X-Tracker-Request-Id"))
|
||||
eventName := strings.TrimSpace(c.GetHeader("X-Tracker-Event-Name"))
|
||||
packedContext := strings.TrimSpace(c.GetHeader("X-DC-Request-Context"))
|
||||
expectedEvent := trackerEventRoutes[c.FullPath()]
|
||||
if expectedEvent == "" {
|
||||
expectedEvent = trackerEventRoutes[c.Request.URL.Path]
|
||||
}
|
||||
if c.Request.Method != http.MethodPost || expectedEvent == "" || eventName != expectedEvent ||
|
||||
!reTrackerRequestID.MatchString(requestID) || !reTrackerEvent.MatchString(eventName) {
|
||||
return
|
||||
}
|
||||
canonicalContext, ok := canonicalPackedTrackerContext(packedContext, requestID, eventName)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
header["X-DC-Request-Context"] = canonicalContext
|
||||
header["X-Tracker-Request-Id"] = requestID
|
||||
header["X-Tracker-Event-Name"] = eventName
|
||||
}
|
||||
|
||||
func canonicalPackedTrackerContext(value, requestID, eventName string) (string, bool) {
|
||||
if value == "" || len(value) > 8192 || !reTrackerCtx.MatchString(value) {
|
||||
return "", false
|
||||
}
|
||||
raw, err := base64.RawURLEncoding.DecodeString(value)
|
||||
if err != nil || len(raw) > 6144 {
|
||||
return "", false
|
||||
}
|
||||
var envelope trackerContextEnvelope
|
||||
if err := json.Unmarshal(raw, &envelope); err != nil {
|
||||
return "", false
|
||||
}
|
||||
if envelope.Version != 1 || envelope.RequestID != requestID ||
|
||||
envelope.EventName != eventName || !reTrackerRequestID.MatchString(envelope.RequestID) ||
|
||||
!reTrackerEvent.MatchString(envelope.EventName) ||
|
||||
!hasOnlyTrackerFields(envelope.Common, trackerCommonFieldKeys) ||
|
||||
!hasOnlyTrackerFields(envelope.Drama, trackerDramaFieldKeys) ||
|
||||
!validTrackerCommonFields(envelope.Common) || !validTrackerDramaFields(envelope.Drama) {
|
||||
return "", false
|
||||
}
|
||||
// 公共字段的值保持 SDK 原值;重新编码仅移除重复 JSON key,防止上下游解析差异。
|
||||
canonical, err := json.Marshal(envelope)
|
||||
if err != nil || len(canonical) > 6144 {
|
||||
return "", false
|
||||
}
|
||||
return base64.RawURLEncoding.EncodeToString(canonical), true
|
||||
}
|
||||
|
||||
func hasOnlyTrackerFields(values map[string]json.RawMessage, allowed map[string]struct{}) bool {
|
||||
if len(values) != len(allowed) {
|
||||
return false
|
||||
}
|
||||
for key := range values {
|
||||
if _, ok := allowed[key]; !ok {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func validTrackerCommonFields(values map[string]json.RawMessage) bool {
|
||||
appID, ok := trackerStringField(values, "app_id", 50, false)
|
||||
if !ok || !reTrackerAppID.MatchString(appID) || appID != appg.TrackerAppID() {
|
||||
return false
|
||||
}
|
||||
uid, ok := trackerStringField(values, "uid", 32, true)
|
||||
if !ok || (uid != "" && (utf8.RuneCountInString(uid) < 4 || !reTrackerUID.MatchString(uid) ||
|
||||
uid == "u_1001" || uid == "stress_user" || uid == "u_lab")) {
|
||||
return false
|
||||
}
|
||||
channel, ok := trackerStringField(values, "channel", 50, true)
|
||||
if !ok || (channel != "" && (!reTrackerChannel.MatchString(channel) ||
|
||||
channel == "{}" || channel == "0" || channel == "-" || strings.EqualFold(channel, "unknown"))) {
|
||||
return false
|
||||
}
|
||||
if _, ok = trackerStringField(values, "sid", 50, true); !ok {
|
||||
return false
|
||||
}
|
||||
clientTS, ok := trackerIntegerField(values, "client_ts")
|
||||
if !ok || clientTS < 1767196800 || clientTS > 9999999999 {
|
||||
return false
|
||||
}
|
||||
device, ok := trackerStringField(values, "device", 30, true)
|
||||
if !ok || (device != "" && device != "Android" && device != "iOS" && device != "PC" && device != "OTHER") {
|
||||
return false
|
||||
}
|
||||
if _, ok = trackerStringField(values, "device_id", 50, false); !ok {
|
||||
return false
|
||||
}
|
||||
if _, ok = trackerStringField(values, "device_brand", 100, true); !ok {
|
||||
return false
|
||||
}
|
||||
if _, ok = trackerStringField(values, "device_model", 100, true); !ok {
|
||||
return false
|
||||
}
|
||||
if _, ok = trackerStringField(values, "device_fingerprint", 128, true); !ok {
|
||||
return false
|
||||
}
|
||||
if _, ok = trackerStringField(values, "fp_version", 128, true); !ok {
|
||||
return false
|
||||
}
|
||||
if _, ok = trackerStringField(values, "user_agent", 4096, true); !ok {
|
||||
return false
|
||||
}
|
||||
if _, ok = trackerStringField(values, "sdk_version", 128, true); !ok {
|
||||
return false
|
||||
}
|
||||
_, ok = trackerStringField(values, "app_version", 128, true)
|
||||
return ok
|
||||
}
|
||||
|
||||
func validTrackerDramaFields(values map[string]json.RawMessage) bool {
|
||||
if _, ok := trackerStringField(values, "media_id", 512, true); !ok {
|
||||
return false
|
||||
}
|
||||
if _, ok := trackerStringField(values, "drama_id", 512, false); !ok {
|
||||
return false
|
||||
}
|
||||
episode, ok := trackerIntegerField(values, "episode")
|
||||
if !ok || episode <= 0 {
|
||||
return false
|
||||
}
|
||||
if _, ok = trackerStringField(values, "video_id", 512, true); !ok {
|
||||
return false
|
||||
}
|
||||
_, ok = trackerStringField(values, "recommend_trace_id", 2048, true)
|
||||
return ok
|
||||
}
|
||||
|
||||
func trackerStringField(values map[string]json.RawMessage, key string, maxLen int, allowEmpty bool) (string, bool) {
|
||||
raw, ok := values[key]
|
||||
if !ok {
|
||||
return "", false
|
||||
}
|
||||
var decoded *string
|
||||
if err := json.Unmarshal(raw, &decoded); err != nil || decoded == nil {
|
||||
return "", false
|
||||
}
|
||||
value := *decoded
|
||||
if utf8.RuneCountInString(value) > maxLen || (!allowEmpty && value == "") {
|
||||
return "", false
|
||||
}
|
||||
return value, true
|
||||
}
|
||||
|
||||
func trackerIntegerField(values map[string]json.RawMessage, key string) (int64, bool) {
|
||||
raw, ok := values[key]
|
||||
if !ok {
|
||||
return 0, false
|
||||
}
|
||||
var decoded *int64
|
||||
if err := json.Unmarshal(raw, &decoded); err != nil || decoded == nil {
|
||||
return 0, false
|
||||
}
|
||||
return *decoded, true
|
||||
}
|
||||
|
||||
@@ -1,80 +0,0 @@
|
||||
package useragent
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
// 限制解析开销;正常 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
|
||||
}
|
||||
Reference in New Issue
Block a user