package dataReport import ( "bytes" "context" "encoding/json" "fmt" "io/ioutil" "math" "math/rand" "net/http" "net/url" "strconv" "strings" "time" "videoweb/pub/common/godef" "videoweb/pub/common/log" "videoweb/pub/common/useragent" "videoweb/pub/pubdef" "github.com/google/uuid" "github.com/moul/http2curl" ) var config = Config{} func Init(c Config) { config = c // 根据运行模式设置SDK配置API地址 switch c.RunMod { case "prod", "release": sdkConfigAPIURL = sdkConfigAPIProd default: sdkConfigAPIURL = sdkConfigAPIDev } } // GenerateGlobalEventID 生成全局唯一的 event_id func GenerateGlobalEventID() string { // 生成一个新的 UUID return strings.ReplaceAll(uuid.New().String(), "-", "") } // Enabled 当前是否允许上报:Disabled=true 或 ApiUrl 为空都视为关闭。 func Enabled() bool { return !config.Disabled && strings.TrimSpace(config.ApiUrl) != "" } func AsyncReport(eventData interface{}, retryCount int) { if !Enabled() { return } // TODO 后续可改成kafka异步处理 godef.Go(func() { // 自动重试 baseDelay := 1 * time.Second for i := 0; i < retryCount; i++ { err := report(eventData) if err != nil { log.Error("Event.report failed", log.Any("retryCount", retryCount), log.Any("eventData", eventData), log.E(err)) // 指数退避 + 随机抖动 sleepTime := time.Duration(math.Pow(2, float64(i)))*baseDelay + time.Duration(rand.Intn(1000))*time.Millisecond time.Sleep(sleepTime) continue } break } }) } // 上报事件 func report(eventData interface{}) error { // 将结构体转换为表单数据 formData, err := structToFormData(eventData) if err != nil { return err } apiUrl := config.ApiUrl + "/api/eventTracking/report.json" req, err := http.NewRequest("POST", apiUrl, bytes.NewBufferString(formData)) if err != nil { log.Error("Event report NewRequest fail", log.E(err)) return fmt.Errorf("failed to create request: %v", err) } req.Header.Set("Content-Type", "application/x-www-form-urlencoded") client := &http.Client{ Timeout: 5 * time.Second, // 设置超时时间为 5 秒 } command, _ := http2curl.GetCurlCommand(req) fmt.Println(command) resp, err := client.Do(req) if err != nil { log.Error("Event report fail", log.E(err)) return fmt.Errorf("failed to send request: %v", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { log.Error("Event report fail", log.Any("http.code", resp.StatusCode)) return fmt.Errorf("failed to report event, http.code: %d", resp.StatusCode) } // 读取返回的 body body, err := ioutil.ReadAll(resp.Body) if err != nil { log.Error("Event report fail", log.E(err)) return fmt.Errorf("failed to read response body: %v", err) } res := &Response{} err = json.Unmarshal(body, res) if err != nil { log.Error("Event report json.Unmarshal(body, res) fail", log.Any("body", string(body)), log.E(err)) return err } if res.Code != 0 { log.Error("Event report fail", log.Any("code", res.Code), log.Any("msg", res.Msg), log.Any("data", res.Data)) return fmt.Errorf("failed to report event, res.code: %d", res.Code) } // 输出响应状态和返回的 body log.Info("Event report Response", log.Any("eventData", eventData), log.Any("body", string(body))) return nil } func structToFormData(data interface{}) (string, error) { // 序列化结构体为 JSON 字符串 jsonData, err := json.Marshal(data) if err != nil { return "", err } // 反序列化为 map var dataMap map[string]interface{} err = json.Unmarshal(jsonData, &dataMap) if err != nil { return "", err } var formData []string // 遍历 map,将其转换为 form 数据 for key, value := range dataMap { if value == nil { continue // 如果值为 nil,跳过 } // 将值转换为字符串 valueStr := fmt.Sprintf("%v", value) // 使用 URL 编码 formData = append(formData, fmt.Sprintf( "%s=%s", url.QueryEscape(key), url.QueryEscape(valueStr), )) } return strings.Join(formData, "&"), nil } func NewCommonField(eventId, event string, districtCode string, ua useragent.UA, uid, ip string) CommonField { return NewCommonFieldAt(eventId, event, districtCode, ua, uid, ip, time.Now().Unix()) } func NewCommonFieldAt(eventId, event string, districtCode string, ua useragent.UA, uid, ip string, clientTS int64) CommonField { channel := strings.TrimSpace(districtCode) if eventId == "" { eventId = GenerateGlobalEventID() } // 五项公共字段和 app_version 使用当前请求中的 SDK 原值。 // device_id 只取 SDK,不用业务 DevID 补,避免和客户端埋点不是同一个编号。 // device 为空时才用本次请求的 SysType 映射成 Android / iOS / PC。 sdk := ua.SDKCommon if missing := sdk.MissingFields(); len(missing) > 0 { log.Warn("【SDK公共参数缺失】NewCommonFieldAt", log.Any("接口", ua.RequestPath), log.Any("事件", event), log.Any("eventId", eventId), log.Any("userId", uid), log.Any("客户端版本", ua.Ver), log.Any("已获取SDK参数", sdk != nil), log.Any("缺失字段", missing)) } if sdk == nil { sdk = &useragent.SDKCommonFields{} } sid := strings.TrimSpace(sdk.SID) if sid == "" { sid = strings.TrimSpace(ua.SessionID) } return CommonField{ UID: uid, EventID: eventId, Event: event, Channel: channel, AppID: pubdef.TrackerAppId(), AppVersion: sdk.AppVersion, ClientTS: fmt.Sprintf("%v", clientTS), Device: reportDevice(sdk.Device, ua.SysType, ua.SystemName), DeviceID: sdk.DeviceID, DeviceFingerprint: sdk.DeviceFingerprint, FPVersion: sdk.FPVersion, UserAgent: strings.TrimSpace(ua.UserAgent), DeviceBrand: strings.TrimSpace(ua.DeviceBrand), DeviceModel: strings.TrimSpace(ua.DeviceModel), SystemName: strings.TrimSpace(ua.SystemName), SystemVersion: strings.TrimSpace(ua.SystemVersion), SID: sid, IP: ip, } } // reportDevice 优先使用 SDK 的设备类型。SDK 没给时,用本次请求的系统类型映射。 func reportDevice(sdkDevice, sysType, systemName string) string { if device := strings.TrimSpace(sdkDevice); device != "" { return device } text := strings.ToLower(strings.TrimSpace(sysType) + " " + strings.TrimSpace(systemName)) // "macos" 含 "ios",先归一化,避免 Mac 被判成 iOS。 text = strings.ReplaceAll(text, "macos", "mac") switch { case strings.Contains(text, "ios") || strings.Contains(text, "iphone") || strings.Contains(text, "ipad"): return "iOS" case strings.Contains(text, "android"): return "Android" case strings.Contains(text, "pc") || strings.Contains(text, "windows") || strings.Contains(text, "mac") || strings.Contains(text, "linux") || strings.Contains(text, "h5"): return "PC" default: return "" } } func JsonNumber(v int64) json.Number { return json.Number(strconv.FormatInt(v, 10)) } func JsonRawMessage(v interface{}) json.RawMessage { b, _ := json.Marshal(v) return b } // 批量上报事件 func BatchReport(ctx context.Context, data interface{}) error { if !Enabled() { return nil } apiUrl := config.ApiUrl + "/api/eventTracking/batchReport.json" reqBody, err := json.Marshal(data) if err != nil { log.Error("Event report json.Marshal fail", log.E(err)) return err } req, err := http.NewRequest("POST", apiUrl, bytes.NewReader(reqBody)) if err != nil { log.Error("Event report NewRequest fail", log.E(err)) return fmt.Errorf("failed to create request: %v", err) } req.Header.Set("Content-Type", "application/json") curl, _ := http2curl.GetCurlCommand(req) client := &http.Client{ Timeout: 5 * time.Second, // 设置超时时间为 5 秒 } resp, err := client.Do(req) if err != nil { log.Error("Event report fail", log.Any("curl", curl), log.E(err)) return fmt.Errorf("failed to send request: %v", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { log.Error("Event report fail", log.Any("curl", curl), log.Any("http.code", resp.StatusCode)) return fmt.Errorf("failed to report event, status code: %d", resp.StatusCode) } // 读取返回的 body body, err := ioutil.ReadAll(resp.Body) if err != nil { log.Error("Event report fail", log.Any("curl", curl), log.E(err)) return fmt.Errorf("failed to read response body: %v", err) } // 输出响应状态和返回的 body log.Info("Event report Response", log.Any("body", string(body))) return nil }