1、文档规范化

2、增加cid核心功能entity
This commit is contained in:
2026-08-25 15:06:34 +08:00
parent b3c0855236
commit 6ce3ebbb91
22 changed files with 1108 additions and 194 deletions
+29 -6
View File
@@ -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
自有DSPPD直投(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 | 广告商品 ↔ 电商商品映射 |
+39
View File
@@ -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",
}
+61 -46
View File
@@ -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",
}
@@ -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",
+6
View File
@@ -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",
+42
View File
@@ -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",
}
+51
View File
@@ -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",
}
+71
View File
@@ -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",
}
+66
View File
@@ -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",
}
+45
View File
@@ -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",
}
+53
View File
@@ -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",
}
+44
View File
@@ -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",
}
+33
View File
@@ -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",
}
+47
View File
@@ -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",
}
+60
View File
@@ -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",
}
+44
View File
@@ -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",
}
+60
View File
@@ -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",
}
@@ -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",
}
@@ -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",
}
+3
View File
@@ -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",
+53
View File
@@ -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",
}
+270 -78
View File
@@ -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 队列 + 失败退避重试,用于广告系统优化投放)
**外投链路**
### 表设计(PGPhase 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, -- 对外 CIDbase62 短码)
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_taskDDL 见「已有表改造」)
#### 报表域
```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` JSONBattribution/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_logclick_log 按 day 分区可裁剪,转化订单生命周期长,反查不可靠
- click_log 平台透传IDcampaign_id/ad_id/creative_idstring)与本地IDlocal_campaign_id/local_ad_id/local_creative_idint64)分列:外投点击带平台侧ID,DSP 点击只填本地ID,透传字段留空,两体系不混用
- 无存量风险:attribution 域表尚未建(规划中),仅 ad_platform_account 等配置表有存量,改造成本低
### 关键设计决策
| 决策 | 方案 | 理由 |
|---|---|---|
| 平台扩展 | 平台字段枚举 + `extra/config JSONB` + 适配器 | 平台不确定,新增平台零 DDL、不改主流程 |
| 点击去重 | 接入层 Redis 布隆过滤器(热路径拦截) + 存储层 `UNIQUE(ad_platform, request_id)` 兜底 | Redis 挡 99% 重复;唯一约束是易失层之外的硬保障 |
| 计划树 | 四层统一模型 campaign/ad_group/ad/creative,各层可选,差异进 extracreative 冗余 campaign_id 保证 ad_id 可空时链路不断 | 千川/聚光/磁力层级不一,统一模型 + 零 DDL 适配 |
| 双轨ID | 本地 PK + platform_xxx_id + sync_status 状态机 | 本地草稿先行、同步失败可重试,不与平台强耦合 |
| ID语义分离 | click_log 平台透传IDstring)与本地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**:引入 MQNATS 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,一条服务两条流,按事件类型配置不同: