From 6ce3ebbb91d9a3b1155754d52430300e4324db8f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BC=A0=E6=96=8C?= <259278618@qq.com> Date: Tue, 25 Aug 2026 15:06:34 +0800 Subject: [PATCH] =?UTF-8?q?1=E3=80=81=E6=96=87=E6=A1=A3=E8=A7=84=E8=8C=83?= =?UTF-8?q?=E5=8C=96=202=E3=80=81=E5=A2=9E=E5=8A=A0cid=E6=A0=B8=E5=BF=83?= =?UTF-8?q?=E5=8A=9F=E8=83=BDentity?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 35 +- model/entity/account/advertiser.go | 39 ++ model/entity/attribution/click_log.go | 107 +++--- model/entity/attribution/conversion_order.go | 6 + model/entity/attribution/report_task.go | 6 + model/entity/delivery/ad.go | 42 +++ model/entity/delivery/ad_group.go | 51 +++ model/entity/delivery/campaign.go | 71 ++++ model/entity/delivery/creative.go | 66 ++++ model/entity/dsp/ad_slot.go | 45 +++ model/entity/dsp/auction_log.go | 53 +++ model/entity/dsp/bid_request_log.go | 44 +++ model/entity/dsp/media.go | 33 ++ model/entity/dsp/placement.go | 47 +++ model/entity/material/material.go | 60 +++ model/entity/material/material_audit.go | 44 +++ model/entity/platform/ad_account.go | 60 +++ model/entity/platform/ad_platform_account.go | 45 --- .../platform/commerce_platform_config.go | 44 ++- model/entity/platform/item_mapping.go | 3 + model/entity/report/daily_report.go | 53 +++ 技术设计.md | 348 ++++++++++++++---- 22 files changed, 1108 insertions(+), 194 deletions(-) create mode 100644 model/entity/account/advertiser.go create mode 100644 model/entity/delivery/ad.go create mode 100644 model/entity/delivery/ad_group.go create mode 100644 model/entity/delivery/campaign.go create mode 100644 model/entity/delivery/creative.go create mode 100644 model/entity/dsp/ad_slot.go create mode 100644 model/entity/dsp/auction_log.go create mode 100644 model/entity/dsp/bid_request_log.go create mode 100644 model/entity/dsp/media.go create mode 100644 model/entity/dsp/placement.go create mode 100644 model/entity/material/material.go create mode 100644 model/entity/material/material_audit.go create mode 100644 model/entity/platform/ad_account.go delete mode 100644 model/entity/platform/ad_platform_account.go create mode 100644 model/entity/report/daily_report.go diff --git a/README.md b/README.md index 951420c..6ffa081 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,10 @@ # CID -CID 服务,承载两块业务: +CID 服务,承载三块业务: 1. **内容审核(现有)**:将广告素材(图片/视频)提交易盾进行内容审核,提供 REST API、定时送检调度器和前端管理页 -2. **CID 归因平台(规划中)**:连接上游广告平台(抖音/小红书/快手等)与下游电商平台(京东/淘宝等),记录广告点击、归因转化、回传转化给广告平台 +2. **投放平台(规划中)**:外投对接广告平台营销 API(巨量/聚光/磁力等)建计划、传素材、启停调优、拉报表;自有 DSP 投自有媒体流量(PD 直投/程序化直购 + RTB 实时竞价) +3. **CID 归因平台(规划中)**:连接投放与电商平台(京东/淘宝等),记录广告点击、归因转化、回传转化给广告平台(自有流量转化回流自身优化) ## 目录结构与业务模块 @@ -25,8 +26,13 @@ sql/ DDL 与迁移脚本 | 模块 | 业务域 | 主要表 | |---|---|---| | check/ | 内容审核(现有):易盾检测提交/回调/轮询、素材校验、送检调度 | tencent_image、tencent_video、material_verify_log、tencent_content_check_log、tencent_account_relation | +| account/ | 账户域(规划):广告主(客户)与广告账户(多级层级 + API 凭据 + 能力) | advertiser、ad_account | +| delivery/ | 投放域(规划):计划树四层(计划/单元/广告/创意),本地实体 + 平台 ID 双轨同步 | campaign、ad_group、ad、creative | +| material/ | 素材域(规划):素材文件库与多通道审核状态机 | material、material_audit | +| dsp/ | 自有流量域(规划):自有媒体/广告位、PD 预订单、RTB 请求与展示计费 | media、ad_slot、placement、bid_request_log、auction_log | | attribution/ | 归因域(规划):点击记录、CID 生成、订单归因与转化回传(点击→转化→回传为同一链路) | click_log、conversion_order、report_task | -| platform/ | 平台配置域(规划):上游广告平台/下游电商平台对接配置与商品映射 | ad_platform_account、commerce_platform_config、item_mapping | +| platform/ | 平台配置域(规划):下游电商平台对接配置与商品映射 | commerce_platform_config、item_mapping | +| report/ | 报表域(规划):统一消耗/效果汇总(外投平台报表 + DSP 聚合) | daily_report | ## 数据流 @@ -39,10 +45,21 @@ sql/ DDL 与迁移脚本 → 结果写入 material_verify_log(cid库) + 回填素材表审核状态 ``` +**投放链路(规划)**: + +``` +外投:CID 创建计划(campaign/单元/广告/创意,本地草稿) + → 同步平台营销 API(计划/单元/广告/创意/素材上传) + → 平台审核(素材审核状态机)→ 启停/调价 → 拉取平台报表 → daily_report + +自有DSP:PD直投(placement 预订单)或 RTB 竞价(bid_request_log → auction_log) + → 展示/点击计费 → 点击进归因链路(channel=self_dsp) +``` + **CID 归因链路(规划)**: ``` -用户点击广告 → 广告平台跳转 CID(携带广告参数) +用户点击广告 → 广告平台/自有媒体跳转 CID(携带广告参数) → 生成 click_id、记录点击(click_log)→ 302 跳转电商平台(URL 带 click_id) → 用户下单支付 → 电商平台回调 CID(订单 + click_id) → 归因去重(conversion_order,幂等)→ 回传转化给广告平台(report_task 队列 + 失败重试) @@ -60,14 +77,20 @@ sql/ DDL 与迁移脚本 | tencent_content_check_log | cid | 送检日志 | | tencent_account_relation | cid | 腾讯广告账户关系 | -### attribution / platform 模块(规划,DDL 见技术设计.md) +### 投放/归因模块(规划,DDL 见技术设计.md) | 表 | 库 | 说明 | |---|---|---| +| advertiser | cid | 广告主(客户) | +| ad_account | cid | 广告账户(多级层级 + API 凭据 + 能力,由 ad_platform_account 改造) | +| campaign / ad_group / ad / creative | cid | 计划树四层(本地实体 + 平台 ID 双轨) | +| material / material_audit | cid | 素材库 + 多通道审核状态机 | +| media / ad_slot / placement | cid | 自有媒体/广告位/PD 预订单 | +| bid_request_log / auction_log | cid | RTB 请求日志 / 展示计费(按 day 分区) | | click_log | cid | 点击日志(不可变事件),按 day 分区 | | conversion_order | cid | 转化订单(归因与回传状态) | | report_task | cid | 转化回传任务队列(失败退避重试) | -| ad_platform_account | cid | 上游广告平台对接配置 | +| daily_report | cid | 统一消耗/效果汇总(外投报表 + DSP 聚合) | | commerce_platform_config | cid | 下游电商平台对接配置 | | item_mapping | cid | 广告商品 ↔ 电商商品映射 | diff --git a/model/entity/account/advertiser.go b/model/entity/account/advertiser.go new file mode 100644 index 0000000..2f08874 --- /dev/null +++ b/model/entity/account/advertiser.go @@ -0,0 +1,39 @@ +package account + +import ( + "gitea.redpowerfuture.com/red-future/common/beans" +) + +// Advertiser 广告主实体(客户,多级/单层账户模式的业务主体) +type Advertiser struct { + beans.SQLBaseDO `orm:",inherit"` + // 业务字段 + Name string `orm:"name" json:"name" description:"广告主名称"` + Industry string `orm:"industry" json:"industry" description:"行业"` + ContactName string `orm:"contact_name" json:"contactName" description:"联系人"` + ContactPhone string `orm:"contact_phone" json:"contactPhone" description:"联系电话"` + Status int `orm:"status" json:"status" description:"状态 1启用 0停用"` + Extra string `orm:"extra" json:"extra" description:"扩展配置(JSON)"` +} + +// AdvertiserCol 广告主表字段定义 +type AdvertiserCol struct { + beans.SQLBaseCol + Name string + Industry string + ContactName string + ContactPhone string + Status string + Extra string +} + +// AdvertiserCols 广告主表字段常量 +var AdvertiserCols = AdvertiserCol{ + SQLBaseCol: beans.DefSQLBaseCol, + Name: "name", + Industry: "industry", + ContactName: "contact_name", + ContactPhone: "contact_phone", + Status: "status", + Extra: "extra", +} diff --git a/model/entity/attribution/click_log.go b/model/entity/attribution/click_log.go index efdc414..8dbe681 100644 --- a/model/entity/attribution/click_log.go +++ b/model/entity/attribution/click_log.go @@ -10,59 +10,74 @@ import ( type ClickLog struct { beans.SQLBaseDO `orm:",inherit"` // 业务字段 - ClickID string `orm:"click_id" json:"clickId" description:"对外CID"` - RequestID string `orm:"request_id" json:"requestId" description:"广告平台请求号(去重键)"` - AdPlatform string `orm:"ad_platform" json:"adPlatform" description:"广告平台 douyin/xiaohongshu/kuaishou"` - AccountID string `orm:"account_id" json:"accountId" description:"广告账户ID"` - CampaignID string `orm:"campaign_id" json:"campaignId" description:"广告计划ID"` - AdGroupID string `orm:"ad_group_id" json:"adGroupId" description:"广告组ID"` - AdID string `orm:"ad_id" json:"adId" description:"广告ID"` - CreativeID string `orm:"creative_id" json:"creativeId" description:"创意ID"` - DeviceID string `orm:"device_id" json:"deviceId" description:"设备ID oaid/imei/idfa"` - IP string `orm:"ip" json:"ip" description:"点击IP"` - UA string `orm:"ua" json:"ua" description:"用户代理"` - LandingURL string `orm:"landing_url" json:"landingUrl" description:"原始落地页URL"` - RedirectURL string `orm:"redirect_url" json:"redirectUrl" description:"跳转下游URL(带click_id)"` - Extra string `orm:"extra" json:"extra" description:"平台透传参数(JSON)"` - Day *gtime.Time `orm:"day" json:"day" description:"分区日期"` + ClickID string `orm:"click_id" json:"clickId" description:"对外CID"` + RequestID string `orm:"request_id" json:"requestId" description:"广告平台请求号(去重键)"` + Channel string `orm:"channel" json:"channel" description:"投放通道 external外投/self_dsp自有"` + AdPlatform string `orm:"ad_platform" json:"adPlatform" description:"广告平台 douyin/xiaohongshu/kuaishou/self_dsp"` + AccountID string `orm:"account_id" json:"accountId" description:"广告账户ID"` + CampaignID string `orm:"campaign_id" json:"campaignId" description:"平台侧计划ID(透传)"` + LocalCampaignID int64 `orm:"local_campaign_id" json:"localCampaignId" description:"本地计划ID(回传路由)"` + LocalAdID int64 `orm:"local_ad_id" json:"localAdId" description:"本地广告ID(DSP必填,外投按映射回填)"` + LocalCreativeID int64 `orm:"local_creative_id" json:"localCreativeId" description:"本地创意ID(DSP必填,外投按映射回填)"` + SlotID int64 `orm:"slot_id" json:"slotId" description:"自有DSP广告位ID"` + AdGroupID string `orm:"ad_group_id" json:"adGroupId" description:"广告组ID"` + AdID string `orm:"ad_id" json:"adId" description:"广告ID"` + CreativeID string `orm:"creative_id" json:"creativeId" description:"创意ID"` + DeviceID string `orm:"device_id" json:"deviceId" description:"设备ID oaid/imei/idfa"` + IP string `orm:"ip" json:"ip" description:"点击IP"` + UA string `orm:"ua" json:"ua" description:"用户代理"` + LandingURL string `orm:"landing_url" json:"landingUrl" description:"原始落地页URL"` + RedirectURL string `orm:"redirect_url" json:"redirectUrl" description:"跳转下游URL(带click_id)"` + Extra string `orm:"extra" json:"extra" description:"平台透传参数(JSON)"` + Day *gtime.Time `orm:"day" json:"day" description:"分区日期"` } // ClickLogCol 点击日志表字段定义 type ClickLogCol struct { beans.SQLBaseCol - ClickID string - RequestID string - AdPlatform string - AccountID string - CampaignID string - AdGroupID string - AdID string - CreativeID string - DeviceID string - IP string - UA string - LandingURL string - RedirectURL string - Extra string - Day string + ClickID string + RequestID string + Channel string + AdPlatform string + AccountID string + CampaignID string + LocalCampaignID string + LocalAdID string + LocalCreativeID string + SlotID string + AdGroupID string + AdID string + CreativeID string + DeviceID string + IP string + UA string + LandingURL string + RedirectURL string + Extra string + Day string } // ClickLogCols 点击日志表字段常量 var ClickLogCols = ClickLogCol{ - SQLBaseCol: beans.DefSQLBaseCol, - ClickID: "click_id", - RequestID: "request_id", - AdPlatform: "ad_platform", - AccountID: "account_id", - CampaignID: "campaign_id", - AdGroupID: "ad_group_id", - AdID: "ad_id", - CreativeID: "creative_id", - DeviceID: "device_id", - IP: "ip", - UA: "ua", - LandingURL: "landing_url", - RedirectURL: "redirect_url", - Extra: "extra", - Day: "day", + SQLBaseCol: beans.DefSQLBaseCol, + ClickID: "click_id", + RequestID: "request_id", + Channel: "channel", + AdPlatform: "ad_platform", + AccountID: "account_id", + CampaignID: "campaign_id", + LocalCampaignID: "local_campaign_id", + LocalAdID: "local_ad_id", + LocalCreativeID: "local_creative_id", + SlotID: "slot_id", + AdGroupID: "ad_group_id", + AdID: "ad_id", + CreativeID: "creative_id", + DeviceID: "device_id", + IP: "ip", + UA: "ua", + LandingURL: "landing_url", + RedirectURL: "redirect_url", + Extra: "extra", + Day: "day", } diff --git a/model/entity/attribution/conversion_order.go b/model/entity/attribution/conversion_order.go index 203f937..0475825 100644 --- a/model/entity/attribution/conversion_order.go +++ b/model/entity/attribution/conversion_order.go @@ -12,6 +12,8 @@ type ConversionOrder struct { OrderID string `orm:"order_id" json:"orderId" description:"电商平台订单号"` CommercePlatform string `orm:"commerce_platform" json:"commercePlatform" description:"电商平台 jd/taobao"` ClickID string `orm:"click_id" json:"clickId" description:"关联点击CID"` + LocalCampaignID int64 `orm:"local_campaign_id" json:"localCampaignId" description:"本地计划ID(回传路由)"` + AdAccountID int64 `orm:"ad_account_id" json:"adAccountId" description:"广告账户ID(回传凭据)"` DeviceID string `orm:"device_id" json:"deviceId" description:"设备ID(无click_id时兜底归因)"` ItemID string `orm:"item_id" json:"itemId" description:"商品ID"` ItemName string `orm:"item_name" json:"itemName" description:"商品名称"` @@ -34,6 +36,8 @@ type ConversionOrderCol struct { OrderID string CommercePlatform string ClickID string + LocalCampaignID string + AdAccountID string DeviceID string ItemID string ItemName string @@ -56,6 +60,8 @@ var ConversionOrderCols = ConversionOrderCol{ OrderID: "order_id", CommercePlatform: "commerce_platform", ClickID: "click_id", + LocalCampaignID: "local_campaign_id", + AdAccountID: "ad_account_id", DeviceID: "device_id", ItemID: "item_id", ItemName: "item_name", diff --git a/model/entity/attribution/report_task.go b/model/entity/attribution/report_task.go index 065d0cf..cfcce6e 100644 --- a/model/entity/attribution/report_task.go +++ b/model/entity/attribution/report_task.go @@ -11,6 +11,8 @@ type ReportTask struct { ConversionID string `orm:"conversion_id" json:"conversionId" description:"关联转化事件ID"` ClickID string `orm:"click_id" json:"clickId" description:"点击CID"` AdPlatform string `orm:"ad_platform" json:"adPlatform" description:"回传目标广告平台"` + AdAccountID int64 `orm:"ad_account_id" json:"adAccountId" description:"广告账户ID(回传凭据)"` + CampaignID int64 `orm:"campaign_id" json:"campaignId" description:"回传目标本地计划ID"` TaskType string `orm:"task_type" json:"taskType" description:"回传类型 conversion/refund"` Status int `orm:"status" json:"status" description:"状态 0待回传 1回传中 2成功 3失败"` RetryCount int `orm:"retry_count" json:"retryCount" description:"重试次数"` @@ -26,6 +28,8 @@ type ReportTaskCol struct { ConversionID string ClickID string AdPlatform string + AdAccountID string + CampaignID string TaskType string Status string RetryCount string @@ -41,6 +45,8 @@ var ReportTaskCols = ReportTaskCol{ ConversionID: "conversion_id", ClickID: "click_id", AdPlatform: "ad_platform", + AdAccountID: "ad_account_id", + CampaignID: "campaign_id", TaskType: "task_type", Status: "status", RetryCount: "retry_count", diff --git a/model/entity/delivery/ad.go b/model/entity/delivery/ad.go new file mode 100644 index 0000000..82b5762 --- /dev/null +++ b/model/entity/delivery/ad.go @@ -0,0 +1,42 @@ +package delivery + +import ( + "gitea.redpowerfuture.com/red-future/common/beans" +) + +// Ad 广告实体(计划树第三层) +type Ad struct { + beans.SQLBaseDO `orm:",inherit"` + // 业务字段 + AdGroupID int64 `orm:"ad_group_id" json:"adGroupId" description:"所属单元ID"` + Name string `orm:"name" json:"name" description:"广告名称"` + PlatformAdID string `orm:"platform_ad_id" json:"platformAdId" description:"平台侧广告ID"` + Status string `orm:"status" json:"status" description:"本地状态 draft/active/paused/ended/rejected"` + PlatformStatus string `orm:"platform_status" json:"platformStatus" description:"平台侧状态快照"` + SyncStatus string `orm:"sync_status" json:"syncStatus" description:"同步状态 none/pending/synced/failed"` + Extra string `orm:"extra" json:"extra" description:"平台特有扩展(JSON)"` +} + +// AdCol 广告表字段定义 +type AdCol struct { + beans.SQLBaseCol + AdGroupID string + Name string + PlatformAdID string + Status string + PlatformStatus string + SyncStatus string + Extra string +} + +// AdCols 广告表字段常量 +var AdCols = AdCol{ + SQLBaseCol: beans.DefSQLBaseCol, + AdGroupID: "ad_group_id", + Name: "name", + PlatformAdID: "platform_ad_id", + Status: "status", + PlatformStatus: "platform_status", + SyncStatus: "sync_status", + Extra: "extra", +} diff --git a/model/entity/delivery/ad_group.go b/model/entity/delivery/ad_group.go new file mode 100644 index 0000000..324d5a4 --- /dev/null +++ b/model/entity/delivery/ad_group.go @@ -0,0 +1,51 @@ +package delivery + +import ( + "gitea.redpowerfuture.com/red-future/common/beans" +) + +// AdGroup 广告单元实体(计划树第二层,定向+出价) +type AdGroup struct { + beans.SQLBaseDO `orm:",inherit"` + // 业务字段 + CampaignID int64 `orm:"campaign_id" json:"campaignId" description:"所属计划ID"` + Name string `orm:"name" json:"name" description:"单元名称"` + PlatformAdGroupID string `orm:"platform_ad_group_id" json:"platformAdGroupId" description:"平台侧单元ID"` + Targeting string `orm:"targeting" json:"targeting" description:"定向(JSON) 地域/人群/时段"` + BidType string `orm:"bid_type" json:"bidType" description:"出价方式 cpm/ocpm/cpc/ocpc"` + BidAmount int64 `orm:"bid_amount" json:"bidAmount" description:"出价(分,cpm按千次展示)"` + Status string `orm:"status" json:"status" description:"本地状态 draft/active/paused/ended/rejected"` + PlatformStatus string `orm:"platform_status" json:"platformStatus" description:"平台侧状态快照"` + SyncStatus string `orm:"sync_status" json:"syncStatus" description:"同步状态 none/pending/synced/failed"` + Extra string `orm:"extra" json:"extra" description:"平台特有扩展(JSON)"` +} + +// AdGroupCol 广告单元表字段定义 +type AdGroupCol struct { + beans.SQLBaseCol + CampaignID string + Name string + PlatformAdGroupID string + Targeting string + BidType string + BidAmount string + Status string + PlatformStatus string + SyncStatus string + Extra string +} + +// AdGroupCols 广告单元表字段常量 +var AdGroupCols = AdGroupCol{ + SQLBaseCol: beans.DefSQLBaseCol, + CampaignID: "campaign_id", + Name: "name", + PlatformAdGroupID: "platform_ad_group_id", + Targeting: "targeting", + BidType: "bid_type", + BidAmount: "bid_amount", + Status: "status", + PlatformStatus: "platform_status", + SyncStatus: "sync_status", + Extra: "extra", +} diff --git a/model/entity/delivery/campaign.go b/model/entity/delivery/campaign.go new file mode 100644 index 0000000..d76fba0 --- /dev/null +++ b/model/entity/delivery/campaign.go @@ -0,0 +1,71 @@ +package delivery + +import ( + "github.com/gogf/gf/v2/os/gtime" + + "gitea.redpowerfuture.com/red-future/common/beans" +) + +// Campaign 广告计划实体(计划树第一层,外投/自有DSP共用,预算在计划层) +type Campaign struct { + beans.SQLBaseDO `orm:",inherit"` + // 业务字段 + AdvertiserID int64 `orm:"advertiser_id" json:"advertiserId" description:"广告主ID"` + AdAccountID int64 `orm:"ad_account_id" json:"adAccountId" description:"本地广告账户ID"` + Name string `orm:"name" json:"name" description:"计划名称"` + Channel string `orm:"channel" json:"channel" description:"投放通道 external外投/self_dsp自有"` + Platform string `orm:"platform" json:"platform" description:"广告平台 douyin/xiaohongshu/kuaishou"` + PlatformCampaignID string `orm:"platform_campaign_id" json:"platformCampaignId" description:"平台侧计划ID"` + Status string `orm:"status" json:"status" description:"本地状态 draft/active/paused/ended/rejected"` + PlatformStatus string `orm:"platform_status" json:"platformStatus" description:"平台侧状态快照"` + SyncStatus string `orm:"sync_status" json:"syncStatus" description:"同步状态 none/pending/synced/failed"` + BudgetMode string `orm:"budget_mode" json:"budgetMode" description:"预算方式 free不限/daily日预算"` + DailyBudget *int64 `orm:"daily_budget" json:"dailyBudget" description:"日预算(分),budget_mode=daily时必填,否则NULL"` + TotalBudget *int64 `orm:"total_budget" json:"totalBudget" description:"总预算(分),NULL=不限"` + OptimizeGoal string `orm:"optimize_goal" json:"optimizeGoal" description:"优化目标 conversion/click/exposure"` + StartTime *gtime.Time `orm:"start_time" json:"startTime" description:"投放开始时间"` + EndTime *gtime.Time `orm:"end_time" json:"endTime" description:"投放结束时间"` + Extra string `orm:"extra" json:"extra" description:"平台特有扩展(JSON)"` +} + +// CampaignCol 广告计划表字段定义 +type CampaignCol struct { + beans.SQLBaseCol + AdvertiserID string + AdAccountID string + Name string + Channel string + Platform string + PlatformCampaignID string + Status string + PlatformStatus string + SyncStatus string + BudgetMode string + DailyBudget string + TotalBudget string + OptimizeGoal string + StartTime string + EndTime string + Extra string +} + +// CampaignCols 广告计划表字段常量 +var CampaignCols = CampaignCol{ + SQLBaseCol: beans.DefSQLBaseCol, + AdvertiserID: "advertiser_id", + AdAccountID: "ad_account_id", + Name: "name", + Channel: "channel", + Platform: "platform", + PlatformCampaignID: "platform_campaign_id", + Status: "status", + PlatformStatus: "platform_status", + SyncStatus: "sync_status", + BudgetMode: "budget_mode", + DailyBudget: "daily_budget", + TotalBudget: "total_budget", + OptimizeGoal: "optimize_goal", + StartTime: "start_time", + EndTime: "end_time", + Extra: "extra", +} diff --git a/model/entity/delivery/creative.go b/model/entity/delivery/creative.go new file mode 100644 index 0000000..ff238b4 --- /dev/null +++ b/model/entity/delivery/creative.go @@ -0,0 +1,66 @@ +package delivery + +import ( + "gitea.redpowerfuture.com/red-future/common/beans" +) + +// Creative 创意实体(计划树第四层,引用素材库,落地页带click_id模板) +type Creative struct { + beans.SQLBaseDO `orm:",inherit"` + // 业务字段 + AdID int64 `orm:"ad_id" json:"adId" description:"所属广告ID(部分平台创意先于广告,可空)"` + CampaignID int64 `orm:"campaign_id" json:"campaignId" description:"所属计划ID(冗余,ad_id为空时链路不断)"` + AdvertiserID int64 `orm:"advertiser_id" json:"advertiserId" description:"广告主ID"` + Name string `orm:"name" json:"name" description:"创意名称"` + MaterialID int64 `orm:"material_id" json:"materialId" description:"素材ID(material.id)"` + Title string `orm:"title" json:"title" description:"标题"` + Description string `orm:"description" json:"description" description:"描述"` + ClickURL string `orm:"click_url" json:"clickUrl" description:"落地页URL(含click_id模板)"` + PlatformCreativeID string `orm:"platform_creative_id" json:"platformCreativeId" description:"平台侧创意ID"` + PlatformMaterialID string `orm:"platform_material_id" json:"platformMaterialId" description:"平台侧素材ID"` + AuditStatus string `orm:"audit_status" json:"auditStatus" description:"审核状态 pending/pass/reject"` + Status string `orm:"status" json:"status" description:"本地状态 draft/active/paused/ended/rejected"` + PlatformStatus string `orm:"platform_status" json:"platformStatus" description:"平台侧状态快照"` + SyncStatus string `orm:"sync_status" json:"syncStatus" description:"同步状态 none/pending/synced/failed"` + Extra string `orm:"extra" json:"extra" description:"平台特有扩展(JSON)"` +} + +// CreativeCol 创意表字段定义 +type CreativeCol struct { + beans.SQLBaseCol + AdID string + CampaignID string + AdvertiserID string + Name string + MaterialID string + Title string + Description string + ClickURL string + PlatformCreativeID string + PlatformMaterialID string + AuditStatus string + Status string + PlatformStatus string + SyncStatus string + Extra string +} + +// CreativeCols 创意表字段常量 +var CreativeCols = CreativeCol{ + SQLBaseCol: beans.DefSQLBaseCol, + AdID: "ad_id", + CampaignID: "campaign_id", + AdvertiserID: "advertiser_id", + Name: "name", + MaterialID: "material_id", + Title: "title", + Description: "description", + ClickURL: "click_url", + PlatformCreativeID: "platform_creative_id", + PlatformMaterialID: "platform_material_id", + AuditStatus: "audit_status", + Status: "status", + PlatformStatus: "platform_status", + SyncStatus: "sync_status", + Extra: "extra", +} diff --git a/model/entity/dsp/ad_slot.go b/model/entity/dsp/ad_slot.go new file mode 100644 index 0000000..7d095b0 --- /dev/null +++ b/model/entity/dsp/ad_slot.go @@ -0,0 +1,45 @@ +package dsp + +import ( + "gitea.redpowerfuture.com/red-future/common/beans" +) + +// AdSlot 广告位实体(自有媒体广告位,售卖给计划) +type AdSlot struct { + beans.SQLBaseDO `orm:",inherit"` + // 业务字段 + MediaID int64 `orm:"media_id" json:"mediaId" description:"媒体ID"` + Name string `orm:"name" json:"name" description:"广告位名称"` + SlotType string `orm:"slot_type" json:"slotType" description:"广告位类型 banner/feed/video/interstitial/reward"` + Size string `orm:"size" json:"size" description:"尺寸"` + Unit string `orm:"unit" json:"unit" description:"计费方式 cpm/cpc/cpa"` + BasePrice int64 `orm:"base_price" json:"basePrice" description:"底价(分,cpm按千次)"` + Status int `orm:"status" json:"status" description:"状态 1可售 0停售"` + Extra string `orm:"extra" json:"extra" description:"扩展(JSON) 定向能力声明等"` +} + +// AdSlotCol 广告位表字段定义 +type AdSlotCol struct { + beans.SQLBaseCol + MediaID string + Name string + SlotType string + Size string + Unit string + BasePrice string + Status string + Extra string +} + +// AdSlotCols 广告位表字段常量 +var AdSlotCols = AdSlotCol{ + SQLBaseCol: beans.DefSQLBaseCol, + MediaID: "media_id", + Name: "name", + SlotType: "slot_type", + Size: "size", + Unit: "unit", + BasePrice: "base_price", + Status: "status", + Extra: "extra", +} diff --git a/model/entity/dsp/auction_log.go b/model/entity/dsp/auction_log.go new file mode 100644 index 0000000..5653008 --- /dev/null +++ b/model/entity/dsp/auction_log.go @@ -0,0 +1,53 @@ +package dsp + +import ( + "github.com/gogf/gf/v2/os/gtime" + + "gitea.redpowerfuture.com/red-future/common/beans" +) + +// AuctionLog 展示计费日志实体(PD与RTB统一入口,按day分区) +type AuctionLog struct { + beans.SQLBaseDO `orm:",inherit"` + // 业务字段 + RequestID string `orm:"request_id" json:"requestId" description:"请求ID(RTB)/流水号(PD)"` + CampaignID int64 `orm:"campaign_id" json:"campaignId" description:"本地计划ID"` + AdID int64 `orm:"ad_id" json:"adId" description:"广告ID"` + SlotID int64 `orm:"slot_id" json:"slotId" description:"广告位ID"` + Event string `orm:"event" json:"event" description:"事件 show/click"` + Price int64 `orm:"price" json:"price" description:"成交价(分,cpm按千次)"` + Cost int64 `orm:"cost" json:"cost" description:"本次事件应计费用(分,写入时算好)"` + DeviceID string `orm:"device_id" json:"deviceId" description:"设备ID"` + IP string `orm:"ip" json:"ip" description:"请求IP"` + Day *gtime.Time `orm:"day" json:"day" description:"分区日期"` +} + +// AuctionLogCol 展示计费日志表字段定义 +type AuctionLogCol struct { + beans.SQLBaseCol + RequestID string + CampaignID string + AdID string + SlotID string + Event string + Price string + Cost string + DeviceID string + IP string + Day string +} + +// AuctionLogCols 展示计费日志表字段常量 +var AuctionLogCols = AuctionLogCol{ + SQLBaseCol: beans.DefSQLBaseCol, + RequestID: "request_id", + CampaignID: "campaign_id", + AdID: "ad_id", + SlotID: "slot_id", + Event: "event", + Price: "price", + Cost: "cost", + DeviceID: "device_id", + IP: "ip", + Day: "day", +} diff --git a/model/entity/dsp/bid_request_log.go b/model/entity/dsp/bid_request_log.go new file mode 100644 index 0000000..0ab619f --- /dev/null +++ b/model/entity/dsp/bid_request_log.go @@ -0,0 +1,44 @@ +package dsp + +import ( + "github.com/gogf/gf/v2/os/gtime" + + "gitea.redpowerfuture.com/red-future/common/beans" +) + +// BidRequestLog RTB请求日志实体(高并发,按day分区,可裁剪) +type BidRequestLog struct { + beans.SQLBaseDO `orm:",inherit"` + // 业务字段 + RequestID string `orm:"request_id" json:"requestId" description:"请求ID(去重键)"` + SlotID int64 `orm:"slot_id" json:"slotId" description:"广告位ID"` + DeviceID string `orm:"device_id" json:"deviceId" description:"设备ID"` + IP string `orm:"ip" json:"ip" description:"请求IP"` + UA string `orm:"ua" json:"ua" description:"用户代理"` + Extra string `orm:"extra" json:"extra" description:"上下文/定向特征(JSON)"` + Day *gtime.Time `orm:"day" json:"day" description:"分区日期"` +} + +// BidRequestLogCol RTB请求日志表字段定义 +type BidRequestLogCol struct { + beans.SQLBaseCol + RequestID string + SlotID string + DeviceID string + IP string + UA string + Extra string + Day string +} + +// BidRequestLogCols RTB请求日志表字段常量 +var BidRequestLogCols = BidRequestLogCol{ + SQLBaseCol: beans.DefSQLBaseCol, + RequestID: "request_id", + SlotID: "slot_id", + DeviceID: "device_id", + IP: "ip", + UA: "ua", + Extra: "extra", + Day: "day", +} diff --git a/model/entity/dsp/media.go b/model/entity/dsp/media.go new file mode 100644 index 0000000..7c3df38 --- /dev/null +++ b/model/entity/dsp/media.go @@ -0,0 +1,33 @@ +package dsp + +import ( + "gitea.redpowerfuture.com/red-future/common/beans" +) + +// Media 媒体实体(自有媒体,公司流量侧) +type Media struct { + beans.SQLBaseDO `orm:",inherit"` + // 业务字段 + Name string `orm:"name" json:"name" description:"媒体名称"` + Type string `orm:"type" json:"type" description:"媒体类型 app/web"` + Domain string `orm:"domain" json:"domain" description:"域名"` + Status int `orm:"status" json:"status" description:"状态 1启用 0停用"` +} + +// MediaCol 媒体表字段定义 +type MediaCol struct { + beans.SQLBaseCol + Name string + Type string + Domain string + Status string +} + +// MediaCols 媒体表字段常量 +var MediaCols = MediaCol{ + SQLBaseCol: beans.DefSQLBaseCol, + Name: "name", + Type: "type", + Domain: "domain", + Status: "status", +} diff --git a/model/entity/dsp/placement.go b/model/entity/dsp/placement.go new file mode 100644 index 0000000..22a8bb4 --- /dev/null +++ b/model/entity/dsp/placement.go @@ -0,0 +1,47 @@ +package dsp + +import ( + "github.com/gogf/gf/v2/os/gtime" + + "gitea.redpowerfuture.com/red-future/common/beans" +) + +// Placement 预订单实体(PD直投/程序化直购:计划×广告位预订) +type Placement struct { + beans.SQLBaseDO `orm:",inherit"` + // 业务字段 + AdSlotID int64 `orm:"ad_slot_id" json:"adSlotId" description:"广告位ID"` + CampaignID int64 `orm:"campaign_id" json:"campaignId" description:"本地计划ID"` + Quota int `orm:"quota" json:"quota" description:"预订量(展示次数)"` + Price int64 `orm:"price" json:"price" description:"协议价(分,按unit口径)"` + BookStart *gtime.Time `orm:"book_start" json:"bookStart" description:"预订开始时间"` + BookEnd *gtime.Time `orm:"book_end" json:"bookEnd" description:"预订结束时间"` + Status string `orm:"status" json:"status" description:"状态 pending/running/finished/ended"` + DeliveredCount int `orm:"delivered_count" json:"deliveredCount" description:"已投展示数"` +} + +// PlacementCol 预订单表字段定义 +type PlacementCol struct { + beans.SQLBaseCol + AdSlotID string + CampaignID string + Quota string + Price string + BookStart string + BookEnd string + Status string + DeliveredCount string +} + +// PlacementCols 预订单表字段常量 +var PlacementCols = PlacementCol{ + SQLBaseCol: beans.DefSQLBaseCol, + AdSlotID: "ad_slot_id", + CampaignID: "campaign_id", + Quota: "quota", + Price: "price", + BookStart: "book_start", + BookEnd: "book_end", + Status: "status", + DeliveredCount: "delivered_count", +} diff --git a/model/entity/material/material.go b/model/entity/material/material.go new file mode 100644 index 0000000..5c4a4fa --- /dev/null +++ b/model/entity/material/material.go @@ -0,0 +1,60 @@ +package material + +import ( + "gitea.redpowerfuture.com/red-future/common/beans" +) + +// Material 素材实体(素材文件库,md5去重) +type Material struct { + beans.SQLBaseDO `orm:",inherit"` + // 业务字段 + AdvertiserID int64 `orm:"advertiser_id" json:"advertiserId" description:"广告主ID"` + Type string `orm:"type" json:"type" description:"素材类型 image/video/text"` + Title string `orm:"title" json:"title" description:"素材标题"` + FilePath string `orm:"file_path" json:"filePath" description:"本地存储路径"` + FileSize int64 `orm:"file_size" json:"fileSize" description:"文件大小(字节)"` + MD5 string `orm:"md5" json:"md5" description:"文件MD5(去重键)"` + Width int `orm:"width" json:"width" description:"宽度(像素)"` + Height int `orm:"height" json:"height" description:"高度(像素)"` + Duration int `orm:"duration" json:"duration" description:"视频时长(秒)"` + Format string `orm:"format" json:"format" description:"文件格式"` + AuditStatus string `orm:"audit_status" json:"auditStatus" description:"审核状态 pending/auditing/pass/reject/disabled"` + Status int `orm:"status" json:"status" description:"状态 1可用 0禁用"` + Extra string `orm:"extra" json:"extra" description:"扩展(JSON) 如平台素材ID映射"` +} + +// MaterialCol 素材表字段定义 +type MaterialCol struct { + beans.SQLBaseCol + AdvertiserID string + Type string + Title string + FilePath string + FileSize string + MD5 string + Width string + Height string + Duration string + Format string + AuditStatus string + Status string + Extra string +} + +// MaterialCols 素材表字段常量 +var MaterialCols = MaterialCol{ + SQLBaseCol: beans.DefSQLBaseCol, + AdvertiserID: "advertiser_id", + Type: "type", + Title: "title", + FilePath: "file_path", + FileSize: "file_size", + MD5: "md5", + Width: "width", + Height: "height", + Duration: "duration", + Format: "format", + AuditStatus: "audit_status", + Status: "status", + Extra: "extra", +} diff --git a/model/entity/material/material_audit.go b/model/entity/material/material_audit.go new file mode 100644 index 0000000..ca627f5 --- /dev/null +++ b/model/entity/material/material_audit.go @@ -0,0 +1,44 @@ +package material + +import ( + "github.com/gogf/gf/v2/os/gtime" + + "gitea.redpowerfuture.com/red-future/common/beans" +) + +// MaterialAudit 素材审核记录实体(审核状态机全量日志,多通道) +type MaterialAudit struct { + beans.SQLBaseDO `orm:",inherit"` + // 业务字段 + MaterialID int64 `orm:"material_id" json:"materialId" description:"素材ID"` + AuditChannel string `orm:"audit_channel" json:"auditChannel" description:"审核通道 yidun/platform/manual"` + AuditAction string `orm:"audit_action" json:"auditAction" description:"审核动作 submit/pass/reject/revoke"` + AuditStatus string `orm:"audit_status" json:"auditStatus" description:"审核状态"` + Result string `orm:"result" json:"result" description:"平台返回详情(JSON) 原因/违规项"` + Operator string `orm:"operator" json:"operator" description:"操作人"` + AuditTime *gtime.Time `orm:"audit_time" json:"auditTime" description:"审核时间"` +} + +// MaterialAuditCol 素材审核记录表字段定义 +type MaterialAuditCol struct { + beans.SQLBaseCol + MaterialID string + AuditChannel string + AuditAction string + AuditStatus string + Result string + Operator string + AuditTime string +} + +// MaterialAuditCols 素材审核记录表字段常量 +var MaterialAuditCols = MaterialAuditCol{ + SQLBaseCol: beans.DefSQLBaseCol, + MaterialID: "material_id", + AuditChannel: "audit_channel", + AuditAction: "audit_action", + AuditStatus: "audit_status", + Result: "result", + Operator: "operator", + AuditTime: "audit_time", +} diff --git a/model/entity/platform/ad_account.go b/model/entity/platform/ad_account.go new file mode 100644 index 0000000..3b704a5 --- /dev/null +++ b/model/entity/platform/ad_account.go @@ -0,0 +1,60 @@ +package platform + +import ( + "gitea.redpowerfuture.com/red-future/common/beans" +) + +// AdAccount 广告账户实体(平台凭据 + 多级层级 + API能力标识) +type AdAccount struct { + beans.SQLBaseDO `orm:",inherit"` + // 业务字段 + AdvertiserID int64 `orm:"advertiser_id" json:"advertiserId" description:"归属广告主ID"` + Name string `orm:"name" json:"name" description:"账户显示名"` + Platform string `orm:"platform" json:"platform" description:"广告平台 douyin/xiaohongshu/kuaishou"` + AccountID string `orm:"account_id" json:"accountId" description:"平台侧账户ID"` + ParentAccountID int64 `orm:"parent_account_id" json:"parentAccountId" description:"上级账户ID(多级代理层级,自引用)"` + AccountRole string `orm:"account_role" json:"accountRole" description:"账户角色 agent/owner/sub"` + Capabilities string `orm:"capabilities" json:"capabilities" description:"API能力(JSON) attribution/delivery/report"` + AppID string `orm:"app_id" json:"appId" description:"应用ID"` + AppSecret string `orm:"app_secret" json:"appSecret" description:"应用密钥"` + AccessToken string `orm:"access_token" json:"accessToken" description:"访问令牌"` + TokenExpireAt int64 `orm:"token_expire_at" json:"tokenExpireAt" description:"令牌过期时间戳"` + Config string `orm:"config" json:"config" description:"平台特定配置(JSON) 含回传事件映射等"` + Enabled bool `orm:"enabled" json:"enabled" description:"是否启用"` +} + +// AdAccountCol 广告账户表字段定义 +type AdAccountCol struct { + beans.SQLBaseCol + AdvertiserID string + Name string + Platform string + AccountID string + ParentAccountID string + AccountRole string + Capabilities string + AppID string + AppSecret string + AccessToken string + TokenExpireAt string + Config string + Enabled string +} + +// AdAccountCols 广告账户表字段常量 +var AdAccountCols = AdAccountCol{ + SQLBaseCol: beans.DefSQLBaseCol, + AdvertiserID: "advertiser_id", + Name: "name", + Platform: "platform", + AccountID: "account_id", + ParentAccountID: "parent_account_id", + AccountRole: "account_role", + Capabilities: "capabilities", + AppID: "app_id", + AppSecret: "app_secret", + AccessToken: "access_token", + TokenExpireAt: "token_expire_at", + Config: "config", + Enabled: "enabled", +} diff --git a/model/entity/platform/ad_platform_account.go b/model/entity/platform/ad_platform_account.go deleted file mode 100644 index 8847328..0000000 --- a/model/entity/platform/ad_platform_account.go +++ /dev/null @@ -1,45 +0,0 @@ -package platform - -import ( - "gitea.redpowerfuture.com/red-future/common/beans" -) - -// AdPlatformAccount 广告平台账户配置实体(上游广告平台对接凭据) -type AdPlatformAccount struct { - beans.SQLBaseDO `orm:",inherit"` - // 业务字段 - Platform string `orm:"platform" json:"platform" description:"广告平台 douyin/xiaohongshu/kuaishou"` - AccountID string `orm:"account_id" json:"accountId" description:"平台侧账户ID"` - AppID string `orm:"app_id" json:"appId" description:"应用ID"` - AppSecret string `orm:"app_secret" json:"appSecret" description:"应用密钥"` - AccessToken string `orm:"access_token" json:"accessToken" description:"访问令牌"` - TokenExpireAt int64 `orm:"token_expire_at" json:"tokenExpireAt" description:"令牌过期时间戳"` - Config string `orm:"config" json:"config" description:"平台特定配置(JSON)"` - Enabled bool `orm:"enabled" json:"enabled" description:"是否启用"` -} - -// AdPlatformAccountCol 广告平台账户表字段定义 -type AdPlatformAccountCol struct { - beans.SQLBaseCol - Platform string - AccountID string - AppID string - AppSecret string - AccessToken string - TokenExpireAt string - Config string - Enabled string -} - -// AdPlatformAccountCols 广告平台账户表字段常量 -var AdPlatformAccountCols = AdPlatformAccountCol{ - SQLBaseCol: beans.DefSQLBaseCol, - Platform: "platform", - AccountID: "account_id", - AppID: "app_id", - AppSecret: "app_secret", - AccessToken: "access_token", - TokenExpireAt: "token_expire_at", - Config: "config", - Enabled: "enabled", -} diff --git a/model/entity/platform/commerce_platform_config.go b/model/entity/platform/commerce_platform_config.go index 9779fbc..d892326 100644 --- a/model/entity/platform/commerce_platform_config.go +++ b/model/entity/platform/commerce_platform_config.go @@ -8,32 +8,38 @@ import ( type CommercePlatformConfig struct { beans.SQLBaseDO `orm:",inherit"` // 业务字段 - Platform string `orm:"platform" json:"platform" description:"电商平台 jd/taobao"` - AppKey string `orm:"app_key" json:"appKey" description:"应用Key"` - AppSecret string `orm:"app_secret" json:"appSecret" description:"应用密钥"` - CallbackURL string `orm:"callback_url" json:"callbackUrl" description:"订单回调接收URL"` - Config string `orm:"config" json:"config" description:"平台特定配置(JSON)"` - Enabled bool `orm:"enabled" json:"enabled" description:"是否启用"` + AdvertiserID int64 `orm:"advertiser_id" json:"advertiserId" description:"归属广告主ID"` + Name string `orm:"name" json:"name" description:"配置显示名"` + Platform string `orm:"platform" json:"platform" description:"电商平台 jd/taobao"` + AppKey string `orm:"app_key" json:"appKey" description:"应用Key"` + AppSecret string `orm:"app_secret" json:"appSecret" description:"应用密钥"` + CallbackURL string `orm:"callback_url" json:"callbackUrl" description:"订单回调接收URL"` + Config string `orm:"config" json:"config" description:"平台特定配置(JSON)"` + Enabled bool `orm:"enabled" json:"enabled" description:"是否启用"` } // CommercePlatformConfigCol 电商平台配置表字段定义 type CommercePlatformConfigCol struct { beans.SQLBaseCol - Platform string - AppKey string - AppSecret string - CallbackURL string - Config string - Enabled string + AdvertiserID string + Name string + Platform string + AppKey string + AppSecret string + CallbackURL string + Config string + Enabled string } // CommercePlatformConfigCols 电商平台配置表字段常量 var CommercePlatformConfigCols = CommercePlatformConfigCol{ - SQLBaseCol: beans.DefSQLBaseCol, - Platform: "platform", - AppKey: "app_key", - AppSecret: "app_secret", - CallbackURL: "callback_url", - Config: "config", - Enabled: "enabled", + SQLBaseCol: beans.DefSQLBaseCol, + AdvertiserID: "advertiser_id", + Name: "name", + Platform: "platform", + AppKey: "app_key", + AppSecret: "app_secret", + CallbackURL: "callback_url", + Config: "config", + Enabled: "enabled", } diff --git a/model/entity/platform/item_mapping.go b/model/entity/platform/item_mapping.go index cb8cf89..ab90ecd 100644 --- a/model/entity/platform/item_mapping.go +++ b/model/entity/platform/item_mapping.go @@ -8,6 +8,7 @@ import ( type ItemMapping struct { beans.SQLBaseDO `orm:",inherit"` // 业务字段 + AdvertiserID int64 `orm:"advertiser_id" json:"advertiserId" description:"归属广告主ID"` AdPlatform string `orm:"ad_platform" json:"adPlatform" description:"广告平台"` AdItemID string `orm:"ad_item_id" json:"adItemId" description:"广告平台侧商品ID"` CommercePlatform string `orm:"commerce_platform" json:"commercePlatform" description:"电商平台"` @@ -19,6 +20,7 @@ type ItemMapping struct { // ItemMappingCol 商品映射表字段定义 type ItemMappingCol struct { beans.SQLBaseCol + AdvertiserID string AdPlatform string AdItemID string CommercePlatform string @@ -30,6 +32,7 @@ type ItemMappingCol struct { // ItemMappingCols 商品映射表字段常量 var ItemMappingCols = ItemMappingCol{ SQLBaseCol: beans.DefSQLBaseCol, + AdvertiserID: "advertiser_id", AdPlatform: "ad_platform", AdItemID: "ad_item_id", CommercePlatform: "commerce_platform", diff --git a/model/entity/report/daily_report.go b/model/entity/report/daily_report.go new file mode 100644 index 0000000..b3c6219 --- /dev/null +++ b/model/entity/report/daily_report.go @@ -0,0 +1,53 @@ +package report + +import ( + "github.com/gogf/gf/v2/os/gtime" + + "gitea.redpowerfuture.com/red-future/common/beans" +) + +// DailyReport 日报表实体(统一消耗/效果汇总:外投拉平台报表,DSP聚合auction_log) +type DailyReport struct { + beans.SQLBaseDO `orm:",inherit"` + // 业务字段 + ReportDate *gtime.Time `orm:"report_date" json:"reportDate" description:"报表日期"` + Channel string `orm:"channel" json:"channel" description:"投放通道 external外投/self_dsp自有"` + AdAccountID int64 `orm:"ad_account_id" json:"adAccountId" description:"广告账户ID"` + CampaignID int64 `orm:"campaign_id" json:"campaignId" description:"本地计划ID"` + Platform string `orm:"platform" json:"platform" description:"广告平台 douyin/xiaohongshu/kuaishou"` + Impressions int64 `orm:"impressions" json:"impressions" description:"展示数"` + Clicks int64 `orm:"clicks" json:"clicks" description:"点击数"` + Cost int64 `orm:"cost" json:"cost" description:"消耗(分)"` + Conversions int64 `orm:"conversions" json:"conversions" description:"转化数"` + Extra string `orm:"extra" json:"extra" description:"平台报表原始快照(JSON),对账用"` +} + +// DailyReportCol 日报表字段定义 +type DailyReportCol struct { + beans.SQLBaseCol + ReportDate string + Channel string + AdAccountID string + CampaignID string + Platform string + Impressions string + Clicks string + Cost string + Conversions string + Extra string +} + +// DailyReportCols 日报表字段常量 +var DailyReportCols = DailyReportCol{ + SQLBaseCol: beans.DefSQLBaseCol, + ReportDate: "report_date", + Channel: "channel", + AdAccountID: "ad_account_id", + CampaignID: "campaign_id", + Platform: "platform", + Impressions: "impressions", + Clicks: "clicks", + Cost: "cost", + Conversions: "conversions", + Extra: "extra", +} diff --git a/技术设计.md b/技术设计.md index 90844b3..2468bbe 100644 --- a/技术设计.md +++ b/技术设计.md @@ -1,112 +1,304 @@ # 技术设计 -## CID 归因平台(规划中) +## 投放+归因一体化平台(规划中) + +### 业务全景 + +平台承载三块能力,统一账户与素材体系: + +1. **投放-外投**:对接巨量引擎/聚光/磁力引擎等平台营销 API,为客户建计划、传素材、启停调优、拉取报表 +2. **投放-自有DSP**:自有媒体(APP/站点)与广告位,支持 PD 直投/程序化直购与 RTB 实时竞价两种方式 +3. **归因(CID)**:点击 → click_id → 落地页 → 转化回传,外投回传广告平台、自有流量转化回流自身优化 ### 核心链路 -1. 用户点击广告 → 广告平台(抖音/小红书/快手等)跳转到 CID 落地页(携带广告参数) -2. CID 生成唯一 click_id,记录点击,302 跳转电商平台(URL 带 click_id) -3. 用户下单支付 → 电商平台(京东/淘宝等)回调 CID(订单号 + click_id) -4. CID 归因去重(精确匹配 click_id,兜底 device_id + item + 归因窗口) -5. 回传转化给广告平台(report_task 队列 + 失败退避重试,用于广告系统优化投放) +**外投链路**: -### 表设计(PG,Phase 1) +``` +CID 创建计划(campaign/单元/广告/创意,本地草稿) + → 同步平台营销 API(计划/单元/广告/创意/素材上传) + → 平台审核(素材审核状态机)→ 启停/调价 → 定时拉取平台报表 → daily_report +``` -**click_log**(高频写入,按 day 分区): +**自有DSP链路**: + +``` +PD直投:placement 预订单 → 按定向直接投放 → auction_log(show/click) → 计费 +RTB竞价:用户请求 → bid_request_log → 竞价 → 胜出 → auction_log(show/click) → 计费 + → 点击进归因链路(click_log.channel=self_dsp)→ 转化回流自身优化 +``` + +**归因链路**(外投/自有共用,channel 区分): + +``` +用户点击广告 → 广告平台/自有媒体跳转 CID(携带广告参数) + → 生成 click_id、记录点击(click_log)→ 302 跳转电商平台(URL 带 click_id) + → 用户下单支付 → 电商平台回调 CID(订单 + click_id) + → 归因去重(conversion_order,幂等)→ 回传转化(report_task 队列 + 失败重试) +``` + +### 表设计(PG) + +公共字段(`id/tenant_id/creator/created_at/updater/updated_at/deleted_at`)以下 DDL 统一省略,对应 `beans.SQLBaseDO`;金额一律整数分;时间统一 TIMESTAMPTZ(PGTZ 东八区);平台特有差异一律进 JSONB,零 DDL 扩展。 + +#### 账户域 + +**advertiser 广告主(客户)**:多级/单层账户模式的业务主体 ```sql -CREATE TABLE click_log ( +CREATE TABLE advertiser ( id BIGINT PRIMARY KEY, - click_id VARCHAR(32) NOT NULL, -- 对外 CID(base62 短码) - request_id VARCHAR(64), -- 广告平台请求号(去重键) - ad_platform VARCHAR(20) NOT NULL, -- douyin/xiaohongshu/kuaishou - account_id VARCHAR(64), - campaign_id VARCHAR(64), - ad_group_id VARCHAR(64), - ad_id VARCHAR(64), - creative_id VARCHAR(64), - device_id VARCHAR(64), -- oaid/imei/idfa - ip VARCHAR(64), - ua TEXT, - landing_url TEXT, -- 原始落地页 - redirect_url TEXT, -- 跳转下游 URL(带 click_id) - extra JSONB, -- 平台透传参数(新平台零 DDL) - day DATE NOT NULL, -- 分区键 - create_time TIMESTAMP NOT NULL DEFAULT now(), - UNIQUE (click_id), - UNIQUE (ad_platform, request_id) -- 重复回调去重硬保障 -); -CREATE INDEX idx_click_platform_day ON click_log (ad_platform, day, ad_id); -CREATE INDEX idx_click_device ON click_log (device_id, day); -``` - -**conversion_order**(幂等约束为硬要求——转化回传涉及资金归因): - -```sql -CREATE TABLE conversion_order ( - id BIGINT PRIMARY KEY, - conversion_id VARCHAR(64) NOT NULL, -- 转化事件 ID(幂等键,防重复消费) - order_id VARCHAR(64) NOT NULL, -- 电商订单号 - commerce_platform VARCHAR(20) NOT NULL, -- jd/taobao - click_id VARCHAR(32), - device_id VARCHAR(64), -- 兜底归因 - item_id VARCHAR(64), - item_name VARCHAR(255), - sku_id VARCHAR(64), - conversion_type VARCHAR(20), -- order/pay/refund/cart - order_amount BIGINT, -- 金额一律以分存储 - pay_amount BIGINT, - quantity INT, - order_status VARCHAR(20), -- paid/refunded/canceled - attributed_by VARCHAR(16), -- click_id/device/manual - report_status SMALLINT DEFAULT 0, -- 0 待回传 1 已回传 2 失败 - report_count INT DEFAULT 0, - first_click_time BIGINT, - create_time TIMESTAMP NOT NULL DEFAULT now(), - update_time TIMESTAMP, - UNIQUE (commerce_platform, order_id), - UNIQUE (conversion_id) + name VARCHAR(128) NOT NULL, + industry VARCHAR(64), + contact_name VARCHAR(64), + contact_phone VARCHAR(32), + status SMALLINT DEFAULT 1, -- 1启用 0停用 + extra JSONB, + UNIQUE (name) ); ``` -**report_task**(回传任务队列,调度器消费,失败退避重试): +**ad_account 广告账户**(由 ad_platform_account 改造,见「已有表改造」):平台凭据 + 多级层级 + API 能力标识 + +#### 投放域(计划树四层,各层可选) ```sql -CREATE TABLE report_task ( +-- 计划:挂账户,区分外投/自有,预算在计划层 +CREATE TABLE campaign ( + id BIGINT PRIMARY KEY, + advertiser_id BIGINT NOT NULL, -- 冗余:查询隔离 + ad_account_id BIGINT NOT NULL, -- 本地账户(ad_account.id) + name VARCHAR(128) NOT NULL, + channel VARCHAR(16) NOT NULL, -- external(外投)/self_dsp(自有) + platform VARCHAR(20), -- external: douyin/xiaohongshu/kuaishou + platform_campaign_id VARCHAR(64), -- 平台侧计划ID(外投同步成功后有) + status VARCHAR(20) DEFAULT 'draft', -- 本地: draft/active/paused/ended/rejected + platform_status VARCHAR(20), -- 平台侧状态快照(同步回填) + sync_status VARCHAR(20) DEFAULT 'none', -- none/pending/synced/failed + budget_mode VARCHAR(10) DEFAULT 'free', -- free不限/daily日预算 + daily_budget BIGINT, -- 日预算(分),budget_mode=daily时必填,否则NULL + total_budget BIGINT, -- 总预算(分),NULL=不限(0与NULL语义分离) + optimize_goal VARCHAR(32), -- conversion/click/exposure + start_time TIMESTAMPTZ, + end_time TIMESTAMPTZ, + extra JSONB, + UNIQUE (ad_account_id, platform_campaign_id) +); + +-- 单元:定向+出价 +CREATE TABLE ad_group ( + id BIGINT PRIMARY KEY, + campaign_id BIGINT NOT NULL, + name VARCHAR(128) NOT NULL, + platform_ad_group_id VARCHAR(64), + targeting JSONB, -- 地域/人群/时段 + bid_type VARCHAR(16), -- cpm/ocpm/cpc/ocpc + bid_amount BIGINT, -- 分(cpm按千次展示) + status VARCHAR(20) DEFAULT 'draft', + platform_status VARCHAR(20), + sync_status VARCHAR(20) DEFAULT 'none', + extra JSONB, + UNIQUE (campaign_id, platform_ad_group_id) +); + +-- 广告 +CREATE TABLE ad ( + id BIGINT PRIMARY KEY, + ad_group_id BIGINT NOT NULL, + name VARCHAR(128), + platform_ad_id VARCHAR(64), + status VARCHAR(20) DEFAULT 'draft', + platform_status VARCHAR(20), + sync_status VARCHAR(20) DEFAULT 'none', + extra JSONB, + UNIQUE (ad_group_id, platform_ad_id) +); + +-- 创意:引用素材库,落地页带click_id模板,双平台ID(创意ID+素材ID) +CREATE TABLE creative ( + id BIGINT PRIMARY KEY, + ad_id BIGINT, -- 所属广告(部分平台创意先于广告,可空) + campaign_id BIGINT NOT NULL, -- 所属计划ID(冗余:ad_id为空时链路不断) + advertiser_id BIGINT NOT NULL, -- 冗余:素材按广告主隔离 + name VARCHAR(128), + material_id BIGINT NOT NULL, -- 引用material.id + title VARCHAR(255), + description TEXT, + click_url TEXT, -- 落地页(含click_id模板) + platform_creative_id VARCHAR(64), + platform_material_id VARCHAR(64), -- 平台侧素材ID(上传返回) + audit_status VARCHAR(20) DEFAULT 'pending', -- pending/pass/reject + status VARCHAR(20) DEFAULT 'draft', + platform_status VARCHAR(20), + sync_status VARCHAR(20) DEFAULT 'none', + extra JSONB +); +``` + +#### 素材域 + +```sql +CREATE TABLE material ( id BIGINT PRIMARY KEY, - conversion_id VARCHAR(64) NOT NULL, - click_id VARCHAR(32), - ad_platform VARCHAR(20) NOT NULL, - task_type VARCHAR(20), -- conversion/refund - status SMALLINT DEFAULT 0, -- 0 待回传 1 回传中 2 成功 3 失败 - retry_count INT DEFAULT 0, - next_retry_at BIGINT, -- 失败退避重试时间 - request_body TEXT, - response_body TEXT, - trace_id VARCHAR(64), - create_time TIMESTAMP NOT NULL DEFAULT now() + advertiser_id BIGINT NOT NULL, + type VARCHAR(10) NOT NULL, -- image/video/text + title VARCHAR(128), + file_path TEXT, + file_size BIGINT, + md5 VARCHAR(64), -- 去重键 + width INT, height INT, + duration INT, -- 视频时长(秒) + format VARCHAR(16), + audit_status VARCHAR(20) DEFAULT 'pending', -- pending/auditing/pass/reject/disabled + status SMALLINT DEFAULT 1, -- 1可用 0禁用 + extra JSONB, -- platform_material_id: {"douyin":"x"} + UNIQUE (md5) +); + +-- 审核状态机全量日志(易盾/平台审核回调/人工,多通道) +CREATE TABLE material_audit ( + id BIGINT PRIMARY KEY, + material_id BIGINT NOT NULL, + audit_channel VARCHAR(20) NOT NULL, -- yidun/platform/manual + audit_action VARCHAR(20) NOT NULL, -- submit/pass/reject/revoke + audit_status VARCHAR(20) NOT NULL, + result JSONB, -- 平台返回详情(原因/违规项) + operator VARCHAR(64), + audit_time TIMESTAMPTZ ); -CREATE INDEX idx_report_status ON report_task (status, next_retry_at); ``` -**配置表**(低频,JSONB 兜底平台特有配置,新平台只加一行配置 + 一个适配器): +#### DSP 自有流量域 -- `ad_platform_account`:上游广告平台对接配置(platform、account_id、app_id、app_secret、access_token、token_expire_at、config JSONB、enabled) -- `commerce_platform_config`:下游电商平台配置(platform、app_key、app_secret、callback_url、config JSONB、enabled) -- `item_mapping`:广告商品 ↔ 电商商品映射(ad_platform、ad_item_id、commerce_platform、commerce_item_id、sku_id、config JSONB) +```sql +CREATE TABLE media ( + id BIGINT PRIMARY KEY, + name VARCHAR(128) NOT NULL, + type VARCHAR(10) NOT NULL, -- app/web + domain VARCHAR(255), + status SMALLINT DEFAULT 1 +); + +CREATE TABLE ad_slot ( + id BIGINT PRIMARY KEY, + media_id BIGINT NOT NULL, + name VARCHAR(128) NOT NULL, + slot_type VARCHAR(20) NOT NULL, -- banner/feed/video/interstitial/reward + size VARCHAR(32), + unit VARCHAR(10) NOT NULL, -- cpm/cpc/cpa + base_price BIGINT, -- 底价(分, cpm按千次) + status SMALLINT DEFAULT 1, -- 1可售 0停售 + extra JSONB -- 定向能力声明等 +); + +-- PD预订单(直投/程序化直购) +CREATE TABLE placement ( + id BIGINT PRIMARY KEY, + ad_slot_id BIGINT NOT NULL, + campaign_id BIGINT NOT NULL, -- 本地计划 + quota INT, -- 预订量(展示次数) + price BIGINT, -- 协议价(分, 按unit口径) + book_start TIMESTAMPTZ, + book_end TIMESTAMPTZ, + status VARCHAR(20) DEFAULT 'pending', -- pending/running/finished/ended + delivered_count INT DEFAULT 0, -- 已投展示数 + UNIQUE (ad_slot_id, campaign_id) +); + +-- RTB请求日志(高并发,按day分区,可裁剪) +CREATE TABLE bid_request_log ( + id BIGINT PRIMARY KEY, + request_id VARCHAR(64) NOT NULL, + slot_id BIGINT NOT NULL, + device_id VARCHAR(64), + ip VARCHAR(64), + ua TEXT, + extra JSONB, -- 上下文/定向特征 + day DATE NOT NULL, + create_time TIMESTAMPTZ NOT NULL DEFAULT now(), + UNIQUE (request_id) +) PARTITION BY RANGE (day); + +-- 展示/计费(PD与RTB统一入口:event=show/click) +CREATE TABLE auction_log ( + id BIGINT PRIMARY KEY, + request_id VARCHAR(64) NOT NULL, -- RTB请求ID / PD流水号 + campaign_id BIGINT NOT NULL, + ad_id BIGINT, + slot_id BIGINT NOT NULL, + event VARCHAR(10) NOT NULL, -- show/click + price BIGINT, -- 成交价(分, cpm按千次) + cost BIGINT, -- 本次事件应计费用(分,写入时算好,计费直接取) + device_id VARCHAR(64), + ip VARCHAR(64), + day DATE NOT NULL, + create_time TIMESTAMPTZ NOT NULL DEFAULT now() +) PARTITION BY RANGE (day); +``` + +#### 归因域(click_log / conversion_order / report_task,DDL 见「已有表改造」) + +#### 报表域 + +```sql +-- 统一消耗/效果汇总:外投拉平台报表,DSP聚合auction_log +CREATE TABLE daily_report ( + id BIGINT PRIMARY KEY, + report_date DATE NOT NULL, + channel VARCHAR(16) NOT NULL, -- external/self_dsp + ad_account_id BIGINT, + campaign_id BIGINT NOT NULL, + platform VARCHAR(20), + impressions BIGINT DEFAULT 0, + clicks BIGINT DEFAULT 0, + cost BIGINT DEFAULT 0, -- 分 + conversions BIGINT DEFAULT 0, + extra JSONB, -- 平台报表原始快照(对账用) + UNIQUE (report_date, channel, campaign_id) +); +``` + +#### 电商配置与商品映射(commerce_platform_config / item_mapping,微调见「已有表改造」) + +### 已有表改造方案 + +| 表 | 改造动作 | 新增字段 | +|---|---|---| +| ad_platform_account | 更名 **ad_account** | `advertiser_id`、`name`、`parent_account_id`(自引用多级层级)、`account_role`(agent/owner/sub)、`capabilities` JSONB(attribution/delivery/report) | +| commerce_platform_config | 微调 | `advertiser_id`、`name` | +| item_mapping | 微调 | `advertiser_id`;加唯一约束 `UNIQUE(ad_platform, ad_item_id, commerce_platform, commerce_item_id)`(防重复映射) | +| click_log | 加5列 | `channel`(external/self_dsp)、`local_campaign_id`、`local_ad_id`、`local_creative_id`(本地ID,回传路由/归因)、`slot_id`(自有DSP广告位) | +| conversion_order | 加2列 | `local_campaign_id`、`ad_account_id`(回传凭据账户) | +| report_task | 加2列 | `ad_account_id`、`campaign_id`(回传目标本地计划) | + +- 改造方式:启动时 `ALTER TABLE ... ADD COLUMN IF NOT EXISTS`(幂等)+ 存量数据回填;ad_account 存量行 advertiser_id 置 NULL 待绑定、account_role 默认 owner +- 回传路由字段冗余落 conversion_order/report_task 而非反查 click_log:click_log 按 day 分区可裁剪,转化订单生命周期长,反查不可靠 +- click_log 平台透传ID(campaign_id/ad_id/creative_id,string)与本地ID(local_campaign_id/local_ad_id/local_creative_id,int64)分列:外投点击带平台侧ID,DSP 点击只填本地ID,透传字段留空,两体系不混用 +- 无存量风险:attribution 域表尚未建(规划中),仅 ad_platform_account 等配置表有存量,改造成本低 ### 关键设计决策 | 决策 | 方案 | 理由 | |---|---|---| | 平台扩展 | 平台字段枚举 + `extra/config JSONB` + 适配器 | 平台不确定,新增平台零 DDL、不改主流程 | -| 点击去重 | 接入层 Redis 布隆过滤器(热路径拦截) + 存储层 `UNIQUE(ad_platform, request_id)` 兜底 | Redis 挡 99% 重复;唯一约束是易失层之外的硬保障 | +| 计划树 | 四层统一模型 campaign/ad_group/ad/creative,各层可选,差异进 extra;creative 冗余 campaign_id 保证 ad_id 可空时链路不断 | 千川/聚光/磁力层级不一,统一模型 + 零 DDL 适配 | +| 双轨ID | 本地 PK + platform_xxx_id + sync_status 状态机 | 本地草稿先行、同步失败可重试,不与平台强耦合 | +| ID语义分离 | click_log 平台透传ID(string)与本地ID(int64)分列 | 外投点击带平台侧ID、DSP 点击只填本地ID,两体系不混用,回传/对账无歧义 | +| 点击去重 | 接入层 Redis 布隆过滤器(热路径拦截) + 存储层 `UNIQUE(platform, request_id)` 兜底 | Redis 挡 99% 重复;唯一约束是易失层之外的硬保障 | | 点击事件不可变 | click_log 不含转化状态字段,转化状态由 conversion_order.click_id 反查 | 高频写入表不做 UPDATE,避免行锁竞争与表膨胀 | | 转化幂等 | `UNIQUE(commerce_platform, order_id)` + `UNIQUE(conversion_id)` | 同单只记一次;重复事件直接忽略(MQ 至少一次语义) | | 回传可靠性 | report_task 状态机 + 定时重试(退避) | 回传失败必须重试,重试靠持久化状态而非内存 | +| 回传路由 | conversion_order/report_task 冗余 local_campaign_id + ad_account_id | click_log 按 day 分区可裁剪,转化生命周期长,反查不可靠 | +| 双通道点击 | click_log.channel 区分 external/self_dsp,自有流量并入同一归因链路 | 归因逻辑单一路径,自有流量也能拿转化数据优化 | | 兜底归因 | device_id + item_id + 归因窗口(默认 7 天) | 用户清 cookie 丢 click_id 时仍可归因 | | click_id 编码 | base62:平台前缀 + 分片位 + 时间 + 随机 + 校验位 | 短、唯一、不可预测;预留分片/路由能力 | | 金额 | 一律整数分(int64),外部平台以元回传时在接入边界转分 | 禁止浮点元(公司规范) | +| 计费精度 | CPM 价按千次存储,auction_log.cost 写入时按量算好单次费用(RoundInt),计费直接取 cost | 单次展示价非整数分,预计算避免各聚合点口径不一与浮点累加误差 | +| 高并发分区 | bid_request_log/auction_log 按 day 分区,与 click_log 同策略 | RTB 请求量大,沿用既有分区思路 | +| 消耗统一 | daily_report:外投拉平台报表、DSP 聚合 auction_log | 报表/对账单一路径 | +| 素材审核 | material 状态机 + material_audit 全量日志多通道(易盾/平台/人工) | 外投素材过平台审核,自有媒体素材只过易盾 | +| 多级账户 | ad_account 自引用 parent_account_id + account_role | 代理层级与单层统一(PG 递归用 CTE) | +| 素材去重 | material.md5 唯一 | 一处素材多处投放,省存储与重复审核 | ### 事件驱动演进(分阶段,不一步到位) @@ -114,7 +306,7 @@ CREATE INDEX idx_report_status ON report_task (status, next_retry_at); - **Phase 1(当前)**:PG 分区表 + JSONB 扩展字段,归因走 SQL;表结构与 Phase 3 事件模型兼容 - **Phase 2**:引入 MQ(NATS JetStream,已确定),点击/转化异步写事件流,PG 只做状态层;转化事件走低延迟通道(涉及资金回传,不进粗粒度批量管道),点击事件可批量消费 -- **Phase 3**:引入 ClickHouse 承接事件与分析(点击日百万级后归因/报表查询拖垮 PG 时再做),迁移事件表,归因/报表查询走列存 +- **Phase 3**:引入 ClickHouse 承接事件与分析(点击日百万级后归因/报表查询拖垮 PG 时再做),迁移事件表(click_log/bid_request_log/auction_log),归因/报表查询走列存 - **Phase 4**:流式归因,Phase 1 的 SQL 归因退役 **MQ 选型(已确定)**:NATS JetStream,一条服务两条流,按事件类型配置不同: