Documentation
¶
Overview ¶
Package ocpcx 实现广告媒体的 OCPC 转化回传协议。
它只回答一个问题:**怎么把一次转化发给某个媒体账户**。转化数据从哪来、失败了怎么办、 什么时候发、凭据从哪读,全部由调用方决定——本包不含队列、调度、重试与存储概念, 也不产生任何日志(错误经返回值上抛,由调用方带业务上下文记录)。
两类回传 ¶
两条链路的归因方式与数据模型完全不同,因此是两套接口,不要混用:
网页推广 [Reporter] 点击 ID 归因(bd_vid / qhclickid) 百度网页、360 搜索 应用推广 [AppReporter] 设备+回调串归因(oaid / callback) 百度应用、华为
百度那条同时服务搜索与信息流两种流量(官方归类为线索 API),360 那条是搜索推广产品线; 两家的共性是**按点击 ID 归因**,故合用同一套 Reporter。
尤其注意**两个百度不是同一个接口**:网页推广走 ocpcapi/api/uploadConvertData, 应用推广走 ocpcapi/cb/actionCb,媒体标识分别是 MediaBaidu 与 MediaBaiduApp。
快速上手 ¶
r, err := ocpcx.NewBaidu(ocpcx.BaiduConfig{
Token: token,
LandingURL: "https://example.com/",
ConversionType: ocpcx.BaiduConvFormSubmit, // 转化类型编码,取值见 BaiduConv* 常量
})
if err != nil {
return err
}
if err := r.Report(ctx, "bd_vid_from_landing_page"); err != nil {
return err
}
360 的事务号(trans_id)是媒体侧的去重键,本包按 (点击 ID, 转化事件) 确定性派生, 重传得到同一个值。按自己的业务主键(订单号等)去重时,用可选接口 ConversionReporter 显式给出。
配置驱动 ¶
需要按配置文件选媒体时用 New 与 Config:Config 带 yaml/json tag, 可直接内联进调用方自己的配置结构(`yaml:",inline"`),新增媒体时调用方 的装配代码与配置结构体都不用改。
type Target struct {
Name string `yaml:"name"`
ocpcx.Config `yaml:",inline"`
}
if err := t.Config.Validate(); err != nil { // 启动期校验凭据
return fmt.Errorf("targets[%d]: %w", i, err)
}
r, err := ocpcx.New(t.Config, ocpcx.WithHTTPClient(shared))
错误处理 ¶
媒体明确拒绝(HTTP 非 200,或 HTTP 200 但应答表示业务失败)返回 *APIError, 重试无意义,应告警并检查凭据/参数;网络与超时类故障返回其他错误,重试才有意义。 媒体自报服务异常且协议标注可重试时(如百度应用的 error_code=500)返回 ErrMediaUnavailable,它同样属于可重试的一类。
var apiErr *ocpcx.APIError
switch {
case errors.As(err, &apiErr):
alert("媒体拒绝", apiErr.Media, apiErr.Code, apiErr.Message)
case err != nil:
retryLater()
}
应用推广 ¶
AppReporter 收的是 ConversionEvent(设备标识 + 回调串 + 转化类型 + 时间), 用法与点击归因侧对称:
r, err := ocpcx.NewHuawei(ocpcx.HuaweiConfig{Secret: secret})
err = r.Report(ctx, ocpcx.ConversionEvent{
OAID: oaid,
Callback: callback,
Type: ocpcx.ConversionActivate,
ConvertTime: convertedAt,
})
媒体不支持某个转化类型时返回错误而**不是**降级成激活上报,支持范围见 AppReporter 的文档。
并发 ¶
Reporter 与 AppReporter 构造后只读,可并发使用;凭据在构造期已拷成实例自有字段, 实例之间零共享。多个实例应经 WithHTTPClient 共享同一个客户端,以复用连接池。
构造一次、长期持有;不要每次上报都 New——那会让每次调用都新建连接池, 重做 TLS 握手并堆积 TIME_WAIT。
Index ¶
- Constants
- Variables
- func AppMedias() []string
- func Medias() []string
- type APIError
- type AppConfig
- type AppReporter
- type BaiduAppConfig
- type BaiduConfig
- type ClickConversion
- type Config
- type ConversionEvent
- type ConversionReporter
- type ConversionType
- type HuaweiConfig
- type JoinType
- type Option
- type Qihoo360Config
- type Reporter
Examples ¶
Constants ¶
const ( // MediaHuawei 是华为应用推广转化回传。 MediaHuawei = "huawei" // MediaBaiduApp 是百度**应用**推广转化回传(区别于网页推广的 MediaBaidu)。 MediaBaiduApp = "baidu_app" )
应用推广已实现的媒体标识。
与网页推广的 MediaBaidu 刻意区分:百度这两条链路是**不同的接口** (网页走 ocpcapi/api/uploadConvertData 报 bd_vid,应用走 ocpcapi/cb/actionCb 报 oaid), 配置里若都叫 "baidu" 就分不清是哪条,故应用侧叫 MediaBaiduApp。
const ( // 页面按钮点击类:JS、小程序与线索 API 支持。 BaiduConvConsultButton = 1 // 咨询按钮点击 BaiduConvPhoneButton = 2 // 电话按钮点击 BaiduConvFormButton = 5 // 表单按钮点击 BaiduConvDownloadButton = 6 // 下载按钮点击(信息流为「下载预约按钮点击」) BaiduConvPurchaseButton = 7 // 购买按钮点击 BaiduConvSMSButton = 8 // 短信咨询按钮点击 BaiduConvMapButton = 31 // 地图按钮点击 BaiduConvQQButton = 32 // QQ咨询按钮点击 BaiduConvLotteryButton = 33 // 抽奖按钮点击 BaiduConvVoteButton = 34 // 投票按钮点击 BaiduConvWechatCopyClick = 35 // 微信复制按钮点击 // 线索与成交类。 BaiduConvFormSubmit = 3 // 表单提交成功 BaiduConvPurchase = 10 // 服务购买成功 BaiduConvOrderSubmit = 14 // 订单提交成功 BaiduConvConsultThree = 17 // 三句话咨询 BaiduConvLeaveClue = 18 // 留线索 BaiduConvConsultOne = 19 // 一句话咨询(信息流为「有效咨询」) BaiduConvDeepPageVisit = 20 // 深度页面访问 BaiduConvPhoneConnected = 30 // 电话拨通 BaiduConvApply = 41 // 申请 BaiduConvCredit = 42 // 授信 BaiduConvGoodsOrder = 45 // 商品下单成功(电商购买) BaiduConvAddToCart = 46 // 加入购物车 BaiduConvLoginAfterReg = 49 // 注册激活后登录 BaiduConvReservation = 50 // 预约 BaiduConvIntentCustomer = 51 // 有意向客户 BaiduConvSecondRedirect = 61 // 二次跳转 // 应用类:应用 API 与应用 SDK 支持。 BaiduConvAppActivate = 4 // APP激活 BaiduConvAppRegister = 25 // APP注册 BaiduConvAppPaid = 26 // APP付费 BaiduConvAppRetain = 28 // 次日留存 BaiduConvDeepUsage = 52 // 深度使用 BaiduConvAppInvoke = 71 // 应用调起 // BaiduConvCustom 是客户自定义。 BaiduConvCustom = 27 )
百度转化类型编码,取值与名称照抄官方「转化类型编码」表,供 BaiduConfig.ConversionType 使用。
常量只是给编码一个名字,**不构成校验**:BaiduConfig.ConversionType 仍接受表外的任何正整数。 百度在持续增补编码(下面的号段本就有空缺),而某个账户实际能用哪些类型由推广后台 「新建转化追踪」时披露——本包看不到那个信息,填哪个请以后台为准。
官方表按对接方式(JS / 小程序 / 线索 API / 应用 API / 应用 SDK)标注了各类型的支持范围, 下面按此分组。本包的 NewBaidu 以 bd_vid 归因,走的是线索 API / JS 那条路径。
const ( BaiduErrEmptyConversions = "9018000" // 上传的 conversionTypes 数组为空 BaiduErrTooManyConversions = "9018001" // conversionTypes 数组长度超过 100 BaiduErrInvalidLogidURL = "9018002" // logidUrl 无效 BaiduErrInvalidIsConvert = "9018003" // isConvert 无效 BaiduErrInvalidConvType = "9018004" // newType 或 convertType 无效 BaiduErrInvalidConvertTime = "9018005" // convertTime 无效或超过有效上报时间 BaiduErrInvalidConvValue = "9018006" // convertValue 无效 )
百度应答 header.errors[].code 的官方错误码,照「返回 errors 错误码说明」表收全, 取值是官方码的十进制字符串——与 APIError.DetailCode 同类型,可直接比较:
var e *ocpcx.APIError
if errors.As(err, &e) && e.DetailCode == ocpcx.BaiduErrInvalidLogidURL {
alert("landing_url 配错了")
}
常量只是给编码一个名字,**不构成校验**:百度返回表外的码时照样带进 APIError.DetailCode,不会被丢弃(与 BaiduConv* 的取舍一致)。
const ( // Qihoo360IndustryPCSearch 是 PC 搜索推广,[Qihoo360Config.DataIndustry] 的缺省值。 Qihoo360IndustryPCSearch = "ocpc_ps_convert" // Qihoo360IndustryMobileSearch 是移动搜索。 Qihoo360IndustryMobileSearch = "ocpc_ms_convert" )
360 的产品线标识(data_industry)。四条线的参数要求互不相通,混用会让数据缺失或错乱, 本包按点击 ID(qhclickid)组装请求,故只支持以点击 ID 归因的两条。
const ( // 页面按钮点击类。 Qihoo360EventSubmitButton = "SUBMIT_BUTTON" // 点击页面表单按钮 Qihoo360EventAdvisoryButton = "ADVISORY_BUTTON" // 点击页面咨询按钮 Qihoo360EventCallButton = "CALL_BUTTON" // 点击页面电话按钮 Qihoo360EventShopButton = "SHOP_BUTTON" // 点击页面购买按钮 Qihoo360EventCartButton = "CART_BUTTON" // 点击页面加入购物车按钮 Qihoo360EventRegisterButton = "REGISTER_BUTTON" // 点击页面注册按钮 Qihoo360EventScanButton = "SCAN_BUTTON" // 点击页面扫码按钮 Qihoo360EventWechatCopyButton = "WX_BUTTON_C" // 微信复制按钮点击 Qihoo360EventSiteDownload = "SITEDOWNLOAD" // 点击页面下载按钮 // 线索与咨询类。 Qihoo360EventSubmit = "SUBMIT" // 成功提交表单内容 Qihoo360EventCall = "CALL" // 有效电话拨打 Qihoo360EventAdvisory = "ADVISORY" // 访客发起一次对话 Qihoo360EventAdvisoryDepth = "ADVISORY_DEPTH" // 一次咨询对话中,用户发出大于等于 3 条内容 Qihoo360EventEffectiveAdvisory = "EFFECTIVE_ADVISORY" // 产生有效的咨询行为 Qihoo360EventLeaveContact = "LEAVE_CONTACT" // 留下联系方式 Qihoo360EventAddFansWechat = "ADD_FANS_WX" // 微信加粉 Qihoo360EventIntentional = "INTENTIONAL" // 用户产生消费意向 Qihoo360EventVisitClinic = "VISIT_CLINIC" // 客户预约后到医院就诊 // 订单与付费类。 Qihoo360EventOrder = "ORDER" // 成功产生一次订单;[Qihoo360Config.Event] 的缺省值 Qihoo360EventPlaceOrder = "PLACE_ORDER" // 订单提交 Qihoo360EventOrderValidity = "ORDER_VALIDITY" // 确认为有效的订单 Qihoo360EventLowPay = "LOW_PAY" // 低价订单付费 Qihoo360EventPay = "PAY" // 正式订单付费 Qihoo360EventPaySuccess = "PAY_SUCCESS" // 支付成功 Qihoo360EventAddToCart = "ADD_TO_CART" // 将商品添加到购物车 Qihoo360EventCredit = "CREDIT" // 用户小额贷款申请通过 // 账号与身份类。 Qihoo360EventRegistered = "REGISTERED" // 成功产生一次注册 Qihoo360EventLogin = "LOGIN" // 通过账号登录平台 Qihoo360EventRealName = "REAL_NAME" // 用户实名认证 Qihoo360EventEnterpriseCertification = "ENTERPRISE_CERTIFICATION" // 企业提交资质进行平台认证 Qihoo360EventActivation = "ACTIVATION" // 程序激活 Qihoo360EventRetention = "RETENTION" // 次日留存 // 页面浏览与到达类。 Qihoo360EventSiteVisitDepth = "SITE_VISIT_DEPTH" // 经广告进入落地页,访问深度大于等于 3 Qihoo360EventVPPV = "VPPV" // 深度页面访问 Qihoo360EventBrowseDepth = "BROWSE_DEPTH" // 页面浏览完成,到达指定位置 Qihoo360EventBrowseTime = "BROWSETIME" // 浏览时长大于等于定义值时计入一次转化 Qihoo360EventDetailsPageArrived = "DETAILS_PAGE_ARRIVED" // 详情页到达 Qihoo360EventMiddlePage = "MIDDLE_PAGE" // 扫描 PC 页面二维码进入移动中间页 // 扫码与小程序类。 Qihoo360EventScanCode = "SCAN_CODE" // 用户进行扫码行为 Qihoo360EventAppletStartup = "APPLET_STARTUP" // 用户点广告打开微信小程序 // 游戏与内容类。 Qihoo360EventRoleCreate = "ROLE_CREAT" // 成功创建一个游戏角色(媒体侧字面量为 ROLE_CREAT) Qihoo360EventTryToPlay = "TRY_TO_PLAY" // 进行游戏试玩 Qihoo360EventTrial = "TRIAL" // 用户注册或登录后体验该推广产品 Qihoo360EventConcern = "CONCEM" // 关注(媒体侧字面量为 CONCEM) Qihoo360EventRelease = "RELEASE" // 发布招聘职位等相关信息 Qihoo360EventSubmitResume = "SUBMIT_RESUME" // 投递简历 // 展示广告线(ocpc_zs_convert)披露的四项;本包服务的产品线见 [Qihoo360Config.DataIndustry]。 // 列出它们是为了这张表与官方表逐项对得上。 Qihoo360EventAppletPay = "APPLET_PAY" // 在小程序游戏内充值 Qihoo360EventAppletRoleCreate = "APPLET_ROLE_CREAT" // 在小程序游戏内创建角色 Qihoo360EventTrialLessonPerform = "TRIAL_LESSON_PERFORM" // 在微信社群中进入试听课课程学习 Qihoo360EventTrialLessonComplete = "TRIAL_LESSON_COMPLETE" // 完成当日大部分课程试听内容学习 // Qihoo360EventCustomize 是客户自定义类型(媒体侧字面量为 COUSTOMIZE)。 Qihoo360EventCustomize = "COUSTOMIZE" )
360 转化类型,取值与描述照抄官方「转化类型参数值」表,供 Qihoo360Config.Event 使用。
常量只是给字面量一个名字,**不构成校验**:Qihoo360Config.Event 仍接受表外的任何取值。 360 在持续增补枚举,而某个账户实际能用哪些类型由后台「创建 API 回传转化类型」时披露 ——本包看不到那个信息,填哪个请以后台为准。账户没开通的类型发过去会被静默丢弃:回传照常 返回成功,只有后台报表里那个类型的数字一直是 0。
官方表里有三项的拼写是笔误:COUSTOMIZE、ROLE_CREAT、CONCEM。常量照抄媒体侧字面量, 名字用可读英文,下面逐项标注了这层出入。
官方表按产品线(移动推广 / PC 搜索 / 移动搜索 / 展示广告)分列各类型的转化名称。本包按 点击 ID(qhclickid)组装请求,服务的产品线见 Qihoo360Config.DataIndustry;下面按业务 语义分组。
const ( // MediaQihoo360 是 360 搜索推广转化回传。 MediaQihoo360 = "qihoo360" // MediaBaidu 是百度**网页**推广转化回传,同时服务搜索与信息流两种流量 // (区别于应用推广的 [MediaBaiduApp])。 MediaBaidu = "baidu" )
已实现的媒体标识。新增媒体时在此加常量,并在 Medias、Config、New 与 Config.Validate 各加一处;调用方无需改动。
const DefaultTimeout = 10 * time.Second
DefaultTimeout 是未注入 HTTP 客户端时的缺省单次调用超时。
const Version = "v1.4.0"
Version 是本包的当前版本号,与 git 附注标签保持一致。
发版走 make release-patch / make release-minor:脚本会原地自增下面这行的版本号、 提交并据此打标签,因此这一行的形状——Version 常量赋值为带 v 前缀的三段版本号—— 不能改动,本注释内也不得出现版本号字面量(脚本取文件里第一个匹配到的版本号)。
Variables ¶
var ( // ErrUnknownMedia 表示 Config.Media 不是本包已实现的媒体(含空串)。 ErrUnknownMedia = errors.New("ocpcx: unknown media") // ErrInvalidConfig 表示凭据或投放参数缺失/格式不合法。 ErrInvalidConfig = errors.New("ocpcx: invalid config") // 百度网页的 header.status=4(官方示例代码对它继续重试)、 // 百度应用的 error_code=500(官方标注可重试)。 // // 它刻意**不是** [APIError]:那个类型表示「媒体明确拒绝,重试无意义」。 // 媒体自己说「我这会儿不行,再试」时按可重试处理,调用方退避后重发即可。 ErrMediaUnavailable = errors.New("ocpcx: media temporarily unavailable") )
构造期 sentinel 错误,调用方可用 errors.Is 判断。
var ErrResponseTooLarge = errors.New("ocpcx: response body too large")
ErrResponseTooLarge 表示媒体响应体超过客户端配置的上限。
与「解码失败」分开报:截断后的 JSON 一样解不出来,但那个错误信息会把人引向 「媒体返回了非法 JSON」,而真实原因是响应大得离谱(多半是被网关塞了错误页)。
Functions ¶
Types ¶
type APIError ¶
type APIError struct {
// Media 是媒体标识,取值见 Media* 常量。
Media string
// StatusCode 是 HTTP 状态码。
StatusCode int
// Code 是媒体侧的业务码,各家取自应答里的不同字段:
//
// 360 error(字符串,如 "InvalidClickId")
// 百度网页 header.status
// 百度应用 error_code
// 华为 resultCode
//
// HTTP 层就失败时为空。
Code string
// DetailCode 是媒体在 [APIError.Code] 之外给出的**更细粒度**错误码。
//
// 百度网页的应答是两级结构:header.status 只分成功/部分成功/全部失败/token 失败/服务异常
// 五类,指明是哪个字段出错的是 header.errors[].code。粗码不足以定位问题,故单独取出
// ——否则调用方只能从 [APIError.Message] 的文本里匹配。
//
// 官方七个错误码的取值与说明见 [BaiduErrInvalidLogidURL] 所在的常量块。
//
// 百度网页 header.errors[0].code(如 "9018002")
// 360 空(应答只有单级业务码)
// 百度应用 空(同上)
// 华为 空(同上)
//
// 取值不限于 BaiduErr* 列出的官方码:表外的码原样带出,不会被丢弃。
DetailCode string
// Message 是媒体侧给的描述(百度 header.desc);媒体未提供时为空。
Message string
// Body 是截断后的原始响应,供排查。
Body string
}
APIError 表示媒体**明确拒绝**了这次回传:HTTP 状态码非 200,或 HTTP 200 但应答里 的业务字段表示失败。
它与网络/超时类错误的区别对调用方是有意义的:媒体拒绝(凭据错、点击 ID 非法、 转化类型不对)重试多少次都没用,该告警让人改配置;网络故障重试才有价值。 用 errors.As 取出本类型即可区分。
本类型不携带任何凭据。
Example ¶
区分「媒体明确拒绝」与「网络故障」:前者重试无用,后者才该重试。
package main
import (
"context"
"errors"
"fmt"
"github.com/gtkit/ocpcx"
)
func main() {
r, err := ocpcx.NewBaidu(ocpcx.BaiduConfig{
Token: "t", LandingURL: "https://example.com/", ConversionType: ocpcx.BaiduConvFormSubmit,
})
if err != nil {
return
}
var apiErr *ocpcx.APIError
switch err := r.Report(context.Background(), "vid-1"); {
case err == nil:
fmt.Println("成功")
case errors.As(err, &apiErr):
// 凭据错、点击 ID 非法、转化类型不对——重试多少次都没用,该告警。
fmt.Printf("媒体拒绝: media=%s code=%s desc=%s\n", apiErr.Media, apiErr.Code, apiErr.Message)
default:
// 网络、超时、被取消——稍后重试有意义。
fmt.Println("传输故障,稍后重试")
}
}
Output:
type AppConfig ¶
type AppConfig struct {
// Media 是媒体标识,必填,取值见 [MediaHuawei] / [MediaBaiduApp]。
Media string `yaml:"media" json:"media"`
// Huawei 在 Media 为 [MediaHuawei] 时必填。
Huawei *HuaweiConfig `yaml:"huawei" json:"huawei,omitempty"`
// BaiduApp 在 Media 为 [MediaBaiduApp] 时必填。
BaiduApp *BaiduAppConfig `yaml:"baidu_app" json:"baidu_app,omitempty"`
}
AppConfig 是构造 AppReporter 的统一入参:Media 选协议,同名子块提供该媒体的账户凭据。
与 Config 同样带 yaml/json tag,可 `yaml:",inline"` 内联进调用方自己的配置结构。
func (AppConfig) Validate ¶
Validate 校验媒体标识与对应凭据,不发起任何网络调用。用法与 Config.Validate 一致: 返回的错误只描述「哪里不合法」,调用方应包装上自己的配置路径。
type AppReporter ¶
type AppReporter interface {
// Media 返回媒体标识,取值见 [MediaHuawei] / [MediaBaiduApp]。
Media() string
// Report 上报一条转化事件;返回 nil 即媒体侧确认接收。
//
// 媒体明确拒绝时返回 *APIError(重试无意义);网络与超时类故障返回其他错误。
// 该媒体不支持这个 [ConversionType] 时返回错误而**不是**降级成激活上报
// ——把注册静默报成激活,数据错了也看不出来。当前两家对三种类型全支持。
//
// 本方法不重试:只有调用方知道自己的幂等性与退避节奏。
Report(ctx context.Context, e ConversionEvent) error
}
AppReporter 是「向某个应用推广媒体账户上报一条转化事件」的能力。 账户凭据在构造期绑定,实现构造后只读,可并发使用。
Example ¶
应用推广:媒体不支持某个转化类型时直接报错,不会降级成激活上报。
package main
import (
"context"
"fmt"
"time"
"github.com/gtkit/ocpcx"
)
func main() {
r, err := ocpcx.NewHuawei(ocpcx.HuaweiConfig{Secret: "your-secret"})
if err != nil {
return
}
err = r.Report(context.Background(), ocpcx.ConversionEvent{
Callback: "callback-from-click",
Type: ocpcx.ConversionType("paid"), // 不在归一化取值内
ConvertTime: time.Now(),
})
fmt.Println(err)
}
Output: ocpcx: huawei 不支持转化类型 "paid"
func NewApp ¶
func NewApp(cfg AppConfig, opts ...Option) (AppReporter, error)
NewApp 按 cfg.Media 构造对应媒体的 AppReporter,构造前先执行 AppConfig.Validate。
配置驱动的调用方用它;不需要按配置选媒体时直接用 NewHuawei / NewBaiduApp——两条路径共用同一份协议实现。
func NewBaiduApp ¶
func NewBaiduApp(cfg BaiduAppConfig, opts ...Option) (AppReporter, error)
NewBaiduApp 构造百度应用转化回传 AppReporter(区别于网页推广的 NewBaidu)。
func NewHuawei ¶
func NewHuawei(cfg HuaweiConfig, opts ...Option) (AppReporter, error)
NewHuawei 构造华为应用转化回传 AppReporter。凭据不合法返回 ErrInvalidConfig 类错误。
Example ¶
应用推广:直接构造某一家媒体的 AppReporter。
package main
import (
"context"
"fmt"
"time"
"github.com/gtkit/ocpcx"
)
func main() {
r, err := ocpcx.NewHuawei(ocpcx.HuaweiConfig{Secret: "your-secret"})
if err != nil {
fmt.Println("构造失败:", err)
return
}
err = r.Report(context.Background(), ocpcx.ConversionEvent{
OAID: "device-oaid",
Callback: "callback-from-click",
Type: ocpcx.ConversionActivate,
ConvertTime: time.Now(),
})
if err != nil {
fmt.Println("回传失败:", err)
return
}
fmt.Println(r.Media(), "回传成功")
}
Output:
type BaiduAppConfig ¶
type BaiduAppConfig struct {
// Secret 参与 sign 计算,必填。不会出现在任何错误信息里。
Secret string `yaml:"secret" json:"secret"`
}
BaiduAppConfig 是百度应用推广账户凭据。
type BaiduConfig ¶
type BaiduConfig struct {
// Token 是百度 OCPC 回传 token,必填。不会出现在任何错误信息里。
Token string `yaml:"token" json:"token"`
// LandingURL 是落地页地址,必填。必须是 http/https 绝对地址且不带 query
// ——回传时会在其后拼 "?bd_vid={点击ID}"。
LandingURL string `yaml:"landing_url" json:"landing_url"`
// ConversionType 是百度的转化类型 newType,必填且为正。
//
// 取值见 BaiduConv* 常量;它们只是给编码起名,不限制取值——表外的正整数照样放行,
// 具体填哪个以推广后台「新建转化追踪」披露的类型为准。
ConversionType int `yaml:"conversion_type" json:"conversion_type"`
}
BaiduConfig 是百度账户凭据与投放参数。
type ClickConversion ¶
type ClickConversion struct {
// ClickID 是媒体下发的点击标识,必填。
ClickID string
// TransID 是本次转化的事务号,可选。**仅 360 读取**,百度没有事务号概念,给了会被忽略。
//
// 360 用它对重复上报去重:同一事务号重复到达只计一次转化。留空时本包按
// (ClickID, 转化事件) 确定性派生,重传得到同一个值;按自己的业务主键去重时
// (订单号等)在这里给出即可,本包原样使用,不做二次派生。
TransID string
// ConvertTime 是转化**实际发生**的时间(不是上报时刻),可选。两家都读取:
//
// 360 作为 event_time 发出;留空则回退到当前时刻。
// 百度 转化类型为 [BaiduConvAddToCart](46)时**必填**,未给会在上报前报错;
// 其余类型给了就一并发出,留空则不发该字段。
//
// 补数据、重试、批处理这类延迟上报的场景务必给准:拿上报时刻当转化时间,
// 媒体会把这条转化归到错误的时间点上。
//
// 补得太晚则整条会被拒:百度的 9018005([BaiduErrInvalidConvertTime])是
// 「convertTime 无效**或超过有效上报时间**」。官方未公布该时效窗口的长度,
// 本包不按猜测的窗口在本地拦截——发出去由媒体判定,补数据的时限请按自己与媒体的约定把握。
ConvertTime time.Time
// Event 是本次上报的转化类型,可选。**仅 360 读取**,百度请用 [ClickConversion.ConversionType]。
//
// 留空表示未指定,沿用构造期的 [Qihoo360Config.Event]。给了就覆盖它——同一套凭据下
// 按单次上报发不同转化类型,不必为此多造一个 Reporter。取值见 Qihoo360Event* 常量;
// 本包不设白名单,取值以账户在 360 后台「创建 API 回传转化类型」时披露的为准。
//
// 它同时决定 [ClickConversion.TransID] 留空时的派生结果:trans_id 按 (ClickID, 生效事件)
// 确定性生成,换事件即换去重键,360 视为两笔不同的转化各计一次。这正是同一点击 ID
// 回传多种转化类型时要的语义;要自己控去重键就显式给 TransID。
Event string
// ConversionType 是本次上报的转化类型编码,可选。**仅百度读取**,360 请用 [ClickConversion.Event]。
//
// 0 表示未指定,沿用构造期的 [BaiduConfig.ConversionType];正数覆盖它;负数在上报前报错
// (构造期对该字段的约束是「必须为正」,per-call 是另一条入口,同样受这条约束)。
// 取值见 BaiduConv* 常量。
//
// 生效编码为 [BaiduConvAddToCart](46)时 [ClickConversion.ConvertTime] 必填——判定按
// **生效后**的编码走,不看构造期配的那个。
ConversionType int
}
ClickConversion 是一次按点击 ID 归因的转化上报。
type Config ¶
type Config struct {
// Media 是媒体标识,必填,取值见 Media* 常量。
Media string `yaml:"media" json:"media"`
// Qihoo360 在 Media 为 [MediaQihoo360] 时必填。
Qihoo360 *Qihoo360Config `yaml:"qihoo360" json:"qihoo360,omitempty"`
// Baidu 在 Media 为 [MediaBaidu] 时必填。
Baidu *BaiduConfig `yaml:"baidu" json:"baidu,omitempty"`
}
Config 是构造 Reporter 的统一入参:Media 选协议,同名子块提供该媒体的账户凭据。
带 yaml/json tag,可直接内联进调用方自己的配置结构:
type Target struct {
Name string `yaml:"name"`
ocpcx.Config `yaml:",inline"`
}
每个媒体一个具名子块而非 map[string]any:调用方启用 YAML 严格解析 (yaml.Decoder.KnownFields(true))时,拼错的键会在启动期报错而非静默失效。
func (Config) Validate ¶
Validate 校验媒体标识与对应凭据的完整性、格式,不发起任何网络调用。
供调用方在**启动期**调用,把配置错误挡在服务对外提供能力之前。返回的错误只描述 「哪里不合法」,不含调用方的配置路径——调用方应包装上自己的位置信息:
if err := t.Config.Validate(); err != nil {
return fmt.Errorf("targets[%d](%s): %w", i, t.Name, err)
}
错误可用 errors.Is 匹配 ErrUnknownMedia 或 ErrInvalidConfig。
Example ¶
未知媒体与凭据缺失都在构造期暴露,可用 errors.Is 判定。
package main
import (
"errors"
"fmt"
"github.com/gtkit/ocpcx"
)
func main() {
err := ocpcx.Config{Media: "toutiao"}.Validate()
fmt.Println(errors.Is(err, ocpcx.ErrUnknownMedia))
err = ocpcx.Config{Media: ocpcx.MediaBaidu}.Validate()
fmt.Println(errors.Is(err, ocpcx.ErrInvalidConfig))
}
Output: true true
type ConversionEvent ¶
type ConversionEvent struct {
// OAID 是设备标识符(明文)。
//
// 华为可选:协议里设备 ID 非必选,广告转化回传按 [ConversionEvent.Callback] 归因;
// 给了就以 device_id + id_type=10(OAID 明文)发出。
//
// 百度应用在 [ConversionEvent.JoinType] 留空时读它(按 oaid 维度归因);
// JoinType 非空时改读 [ConversionEvent.JoinValue],本字段被忽略。
OAID string
// JoinType 是归因维度,标明这条转化实际靠哪个标识匹配成功,取值见 Join* 常量。**仅百度应用读取**。
//
// 留空时按 [JoinOAID] 处理并取 [ConversionEvent.OAID] 的值——既有调用方无需改动。
// 非空时标识值取 [ConversionEvent.JoinValue],未给出会在上报前报错。
//
// iOS 设备没有 OAID,归因走 [JoinIDFA] 或 [JoinCAID];开通了 BD_VID 注入的账户用 [JoinBDVID]。
JoinType JoinType
// JoinValue 是 [ConversionEvent.JoinType] 所声明维度对应的标识值,JoinType 非空时必填。
//
// 发出时的参数名即维度名:JoinType 为 [JoinIDFA] 时发 idfa=<JoinValue>,
// 为 [JoinBDVID] 时发 bd_vid=<JoinValue>。
JoinValue string
// Callback 是媒体在点击时下发的回调串,两家媒体都必填,作为普通参数值原样携带。
Callback string
// Type 是转化事件类型,必填。各媒体的支持范围见 [AppReporter.Report] 的说明。
Type ConversionType
// ConvertTime 是转化事件**实际发生**的时间(不是上报时刻),必填。
//
// 与点击归因侧的 [ClickConversion.ConvertTime] 同名同义。补数据、重试这类延迟上报的场景
// 务必给准:拿上报时刻当转化时间,媒体会把这条转化归到错误的时间点上。
//
// 华为 conversion_time(秒)
// 百度应用 a_time(秒)
//
// 百度对 a_time 的约束见官方错误码 110:须为 10 位秒级时间戳,且不得是未来时间。
// 本包不在本地拦截未来时间——那要拿本机时钟与媒体服务器比较,本机略快就会误拒合法上报,
// 由媒体判定。
ConvertTime time.Time
// Count 是转化数量。
//
// Deprecated: 它对应的 conversion_count 已被华为协议删除(v1.1.4, 2024-02-23),
// 而华为是唯一用过该字段的媒体。向华为上报时设置为正数会**报错**——静默不发会让
// 调用方以为它还在生效,而字段失效从外面看不出来。转化数量请按条上报。
Count int
// ContentID 是素材 ID,可选(仅华为上报)。
ContentID string
// CampaignID 是计划 ID,可选(仅华为上报)。
CampaignID string
// UserAgent 是用户设备的 User-Agent。
//
// Deprecated: 它对应的 user_agent 已被华为协议删除(v2.1.3, 2025-06-20),
// 而华为是唯一用过该字段的媒体。向华为上报时设置为非空会**报错**,原因同 [ConversionEvent.Count]。
UserAgent string
}
ConversionEvent 是一次应用推广转化事件。
与点击 ID 归因只需一个字符串不同,应用推广按「设备 + 媒体下发的回调串」归因, 因此 AppReporter 收的是本结构体而不是一个字符串。
type ConversionReporter ¶
type ConversionReporter interface {
Reporter
// ReportConversion 上报一条转化,语义与 [Reporter.Report] 一致,
// 区别在于可以携带 [ClickConversion] 的可选字段。
ReportConversion(ctx context.Context, c ClickConversion) error
}
ConversionReporter 是 Reporter 的可选扩展:上报一条可携带额外信息的转化。
Reporter.Report 只收一个点击 ID,够用于最常见的即时上报;需要指定去重键或转化发生时间时 用本接口。360 与百度都实现了它,各取 ClickConversion 里对自己有意义的字段:
360 TransID(去重键)、ConvertTime(event_time) 百度 ConvertTime(convertTime,转化类型 46 时必填)
按配置构造 Reporter 的调用方用类型断言取用:
if cr, ok := r.(ocpcx.ConversionReporter); ok {
err = cr.ReportConversion(ctx, ocpcx.ClickConversion{ClickID: id, ConvertTime: convertedAt})
} else {
err = r.Report(ctx, id)
}
Example ¶
携带去重键与转化发生时间:Report 只收点击 ID,这些要经 ConversionReporter 给。
package main
import (
"context"
"fmt"
"time"
"github.com/gtkit/ocpcx"
)
func main() {
r, err := ocpcx.NewQihoo360(ocpcx.Qihoo360Config{AppKey: "k", AppSecret: "s"})
if err != nil {
return
}
convertedAt := time.Now().Add(-2 * time.Hour) // 两小时前发生的转化,现在补报
if cr, ok := r.(ocpcx.ConversionReporter); ok {
err = cr.ReportConversion(context.Background(), ocpcx.ClickConversion{
ClickID: "qhclickid-1",
// 留空时本包按 (点击 ID, 转化事件) 确定性派生;按订单号去重就在这里给。
TransID: "order-42",
// 不给的话 360 会把这条转化记在上报时刻,而不是它真正发生的时刻。
ConvertTime: convertedAt,
})
} else {
err = r.Report(context.Background(), "qhclickid-1")
}
if err != nil {
fmt.Println("回传失败")
return
}
fmt.Println("回传成功")
}
Output:
type ConversionType ¶
type ConversionType string
ConversionType 是归一化的应用转化事件类型。
各媒体的实际取值不同(如百度的次留是 retain_1day),由各自实现翻译; 调用方只面对这三个常量。
const ( // ConversionActivate 是激活。 ConversionActivate ConversionType = "activate" // ConversionRegister 是注册。 ConversionRegister ConversionType = "register" // ConversionRetain 是次日留存。 ConversionRetain ConversionType = "retain" )
type HuaweiConfig ¶
type HuaweiConfig struct {
// Secret 是 HMAC-SHA256 的密钥,参与 Authorization 头计算,必填。不会出现在任何错误信息里。
Secret string `yaml:"secret" json:"secret"`
}
HuaweiConfig 是华为账户凭据。
type JoinType ¶ added in v1.2.0
type JoinType string
JoinType 是转化数据的归因维度,标识该条转化**实际**靠哪个标识匹配成功 (百度应用的 join_type,官方标注必填)。
归因由调用方完成,本包不推断:调用方按自己的优先级(如 OAID > IMEI > IP+UA)匹配, 同时持有多个标识却只有一个命中是常态。若由本包按「哪个字段有值」去猜, 报错维度会让媒体侧的归因统计失真——而请求照常返回成功,回传侧看不出任何异常。
const ( JoinIMEI JoinType = "imei" // 通过 imei_md5 归因成功 JoinOAID JoinType = "oaid" // 通过 oaid 或 oaid_md5 归因成功 JoinAndroidID JoinType = "android_id" // 通过 android_id 或 android_id_md5 归因成功 JoinIP JoinType = "ip" // 通过 ip 和系统信息(ua、os_version、model)归因成功 JoinIDFA JoinType = "idfa" // 通过 idfa 归因成功 JoinCAID JoinType = "caid" // 通过 caid 归因成功 JoinMAC JoinType = "mac" // 通过 mac 或 mac_md5 归因成功 JoinPAID JoinType = "paid" // 通过 paid 归因成功(官方标注灰度) JoinBTUT JoinType = "bt_ut" // 通过 bt+ut 归因成功(官方标注灰度) // JoinBDVID 通过注入 APK V2 签名块的广告 ID(bd_vid)归因成功, // 见官方《Android BD_VID 注入归因增强方案》。需先申请开通该能力。 JoinBDVID JoinType = "bd_vid" )
百度应用的归因维度取值,照官方 callback_url 参数表收录。
常量只是给取值一个名字,**不构成校验**:ConversionEvent.JoinType 接受表外的任何取值, 原样发出(媒体不认识时以 error_code 105 拒绝,可由 APIError.Code 直接看出)。 百度仍在扩充维度(paid、bt_ut 目前标注为灰度),白名单会让本包成为调用方的瓶颈。
type Option ¶
type Option func(*options)
Option 配置 Reporter 的构造。
func WithHTTPClient ¶
WithHTTPClient 注入调用方自己的 HTTP 客户端。
构造多个 Reporter 时应注入同一个客户端:连接池挂在客户端底层的 transport 上, 各自新建会让 keep-alive 复用失效。注入后 WithTimeout 不再生效——超时由注入的 客户端自己持有;响应体上限同理,由注入的客户端配置决定(httpc 缺省 10 MiB, 本包缺省客户端设的是 64 KiB)。
func WithTimeout ¶
WithTimeout 设置单次媒体调用的超时,缺省 DefaultTimeout。 仅在未使用 WithHTTPClient 时生效。
type Qihoo360Config ¶
type Qihoo360Config struct {
// AppKey 放进请求头 App-Key,必填。
AppKey string `yaml:"app_key" json:"app_key"`
// AppSecret 参与 App-Sign 计算,必填。不会出现在任何错误信息里。
AppSecret string `yaml:"app_secret" json:"app_secret"`
// DataIndustry 是产品线标识 data_industry,可选,留空取 [Qihoo360IndustryPCSearch]。
//
// 必须与账户实际投放的产品线一致:360 明确要求不同产品线的参数不能混用,
// 传错会被当作无效数据丢弃,而丢弃是静默的。
DataIndustry string `yaml:"data_industry" json:"data_industry,omitempty"`
// Event 是转化类型,可选,留空取 [Qihoo360EventOrder]。取值见 qihoo360_convtype.go 里的
// Qihoo360Event* 全表,须与账户在 360 后台「创建 API 回传转化类型」时选定的类型一致:
// 取值随媒体侧持续新增,故本包不设白名单。
Event string `yaml:"event" json:"event,omitempty"`
}
Qihoo360Config 是 360 账户凭据与投放参数。
type Reporter ¶
type Reporter interface {
// Media 返回媒体标识,取值见 Media* 常量。供调用方记录日志用。
Media() string
// Report 上报一条转化;返回 nil 即媒体侧确认接收。
//
// 媒体明确拒绝时返回 *APIError(重试无意义);网络与超时类故障返回其他错误。
// 本方法不重试:只有调用方知道自己的幂等性与退避节奏。
Report(ctx context.Context, clickID string) error
}
Reporter 是「向某个媒体账户上报一条转化」的能力。 账户凭据在构造期绑定,实现构造后只读,可并发使用。
func New ¶
New 按 cfg.Media 构造对应媒体的 Reporter,构造前先执行 Config.Validate。
配置驱动的调用方用它;不需要按配置选媒体时直接用 NewQihoo360 / NewBaidu ——两条路径共用同一份协议实现。
Example ¶
配置驱动:按 Config.Media 选媒体,多个 Reporter 共享同一个 HTTP 客户端。
package main
import (
"fmt"
"time"
"github.com/gtkit/httpc"
"github.com/gtkit/ocpcx"
)
func main() {
shared := httpc.New(httpc.WithTimeout(10 * time.Second))
configs := []ocpcx.Config{
{Media: ocpcx.MediaQihoo360, Qihoo360: &ocpcx.Qihoo360Config{AppKey: "k", AppSecret: "s"}},
{Media: ocpcx.MediaBaidu, Baidu: &ocpcx.BaiduConfig{
Token: "t", LandingURL: "https://example.com/", ConversionType: ocpcx.BaiduConvFormSubmit,
}},
}
for i, cfg := range configs {
// 启动期先校验,把配置错误挡在对外服务之前。
if err := cfg.Validate(); err != nil {
fmt.Printf("targets[%d]: %v\n", i, err)
return
}
r, err := ocpcx.New(cfg, ocpcx.WithHTTPClient(shared))
if err != nil {
fmt.Printf("targets[%d]: %v\n", i, err)
return
}
fmt.Println(r.Media())
}
}
Output: qihoo360 baidu
func NewBaidu ¶
func NewBaidu(cfg BaiduConfig, opts ...Option) (Reporter, error)
NewBaidu 构造百度转化回传 Reporter。凭据/参数不合法返回 ErrInvalidConfig 类错误。
Example ¶
直接构造某一家媒体的 Reporter。
package main
import (
"context"
"fmt"
"time"
"github.com/gtkit/ocpcx"
)
func main() {
r, err := ocpcx.NewBaidu(ocpcx.BaiduConfig{
Token: "your-token",
LandingURL: "https://example.com/",
ConversionType: ocpcx.BaiduConvFormSubmit,
}, ocpcx.WithTimeout(5*time.Second))
if err != nil {
fmt.Println("构造失败:", err)
return
}
// 点击 ID 来自落地页回传的 bd_vid。
if err := r.Report(context.Background(), "bd_vid_from_landing_page"); err != nil {
fmt.Println("回传失败:", err)
return
}
fmt.Println(r.Media(), "回传成功")
}
Output:
func NewQihoo360 ¶
func NewQihoo360(cfg Qihoo360Config, opts ...Option) (Reporter, error)
NewQihoo360 构造 360 转化回传 Reporter。凭据不合法返回 ErrInvalidConfig 类错误。