saas隔离使用admin-go中的租户体系
This commit is contained in:
@@ -26,14 +26,15 @@ 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/ | 平台配置域(规划):下游电商平台对接配置与商品映射 | commerce_platform_config、item_mapping |
|
||||
| platform/ | 平台配置域(规划):广告账户(多级层级 + API 凭据 + 能力)、下游电商平台对接配置与商品映射 | ad_account、commerce_platform_config、item_mapping |
|
||||
| report/ | 报表域(规划):统一消耗/效果汇总(外投平台报表 + DSP 聚合) | daily_report |
|
||||
|
||||
> 租户/广告主信息由 admin-go 承载,CID 各表以 `tenant_id` 隔离,不建账户/租户表。
|
||||
|
||||
## 数据流
|
||||
|
||||
**内容审核链路**:
|
||||
@@ -81,7 +82,6 @@ sql/ DDL 与迁移脚本
|
||||
|
||||
| 表 | 库 | 说明 |
|
||||
|---|---|---|
|
||||
| advertiser | cid | 广告主(客户) |
|
||||
| ad_account | cid | 广告账户(多级层级 + API 凭据 + 能力,由 ad_platform_account 改造) |
|
||||
| campaign / ad_group / ad / creative | cid | 计划树四层(本地实体 + 平台 ID 双轨) |
|
||||
| material / material_audit | cid | 素材库 + 多通道审核状态机 |
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
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",
|
||||
}
|
||||
@@ -10,7 +10,6 @@ import (
|
||||
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自有"`
|
||||
@@ -31,7 +30,6 @@ type Campaign struct {
|
||||
// CampaignCol 广告计划表字段定义
|
||||
type CampaignCol struct {
|
||||
beans.SQLBaseCol
|
||||
AdvertiserID string
|
||||
AdAccountID string
|
||||
Name string
|
||||
Channel string
|
||||
@@ -52,7 +50,6 @@ type CampaignCol struct {
|
||||
// CampaignCols 广告计划表字段常量
|
||||
var CampaignCols = CampaignCol{
|
||||
SQLBaseCol: beans.DefSQLBaseCol,
|
||||
AdvertiserID: "advertiser_id",
|
||||
AdAccountID: "ad_account_id",
|
||||
Name: "name",
|
||||
Channel: "channel",
|
||||
|
||||
@@ -10,7 +10,6 @@ type Creative struct {
|
||||
// 业务字段
|
||||
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:"标题"`
|
||||
@@ -30,7 +29,6 @@ type CreativeCol struct {
|
||||
beans.SQLBaseCol
|
||||
AdID string
|
||||
CampaignID string
|
||||
AdvertiserID string
|
||||
Name string
|
||||
MaterialID string
|
||||
Title string
|
||||
@@ -50,7 +48,6 @@ var CreativeCols = CreativeCol{
|
||||
SQLBaseCol: beans.DefSQLBaseCol,
|
||||
AdID: "ad_id",
|
||||
CampaignID: "campaign_id",
|
||||
AdvertiserID: "advertiser_id",
|
||||
Name: "name",
|
||||
MaterialID: "material_id",
|
||||
Title: "title",
|
||||
|
||||
@@ -8,53 +8,50 @@ import (
|
||||
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映射"`
|
||||
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
|
||||
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",
|
||||
SQLBaseCol: beans.DefSQLBaseCol,
|
||||
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",
|
||||
}
|
||||
|
||||
@@ -8,7 +8,6 @@ import (
|
||||
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"`
|
||||
@@ -26,7 +25,6 @@ type AdAccount struct {
|
||||
// AdAccountCol 广告账户表字段定义
|
||||
type AdAccountCol struct {
|
||||
beans.SQLBaseCol
|
||||
AdvertiserID string
|
||||
Name string
|
||||
Platform string
|
||||
AccountID string
|
||||
@@ -44,7 +42,6 @@ type AdAccountCol struct {
|
||||
// AdAccountCols 广告账户表字段常量
|
||||
var AdAccountCols = AdAccountCol{
|
||||
SQLBaseCol: beans.DefSQLBaseCol,
|
||||
AdvertiserID: "advertiser_id",
|
||||
Name: "name",
|
||||
Platform: "platform",
|
||||
AccountID: "account_id",
|
||||
|
||||
@@ -8,38 +8,35 @@ import (
|
||||
type CommercePlatformConfig 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:"电商平台 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:"是否启用"`
|
||||
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
|
||||
AdvertiserID string
|
||||
Name string
|
||||
Platform string
|
||||
AppKey string
|
||||
AppSecret string
|
||||
CallbackURL string
|
||||
Config string
|
||||
Enabled string
|
||||
Name string
|
||||
Platform string
|
||||
AppKey string
|
||||
AppSecret string
|
||||
CallbackURL string
|
||||
Config string
|
||||
Enabled string
|
||||
}
|
||||
|
||||
// CommercePlatformConfigCols 电商平台配置表字段常量
|
||||
var CommercePlatformConfigCols = CommercePlatformConfigCol{
|
||||
SQLBaseCol: beans.DefSQLBaseCol,
|
||||
AdvertiserID: "advertiser_id",
|
||||
Name: "name",
|
||||
Platform: "platform",
|
||||
AppKey: "app_key",
|
||||
AppSecret: "app_secret",
|
||||
CallbackURL: "callback_url",
|
||||
Config: "config",
|
||||
Enabled: "enabled",
|
||||
SQLBaseCol: beans.DefSQLBaseCol,
|
||||
Name: "name",
|
||||
Platform: "platform",
|
||||
AppKey: "app_key",
|
||||
AppSecret: "app_secret",
|
||||
CallbackURL: "callback_url",
|
||||
Config: "config",
|
||||
Enabled: "enabled",
|
||||
}
|
||||
|
||||
@@ -8,7 +8,6 @@ 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:"电商平台"`
|
||||
@@ -20,7 +19,6 @@ type ItemMapping struct {
|
||||
// ItemMappingCol 商品映射表字段定义
|
||||
type ItemMappingCol struct {
|
||||
beans.SQLBaseCol
|
||||
AdvertiserID string
|
||||
AdPlatform string
|
||||
AdItemID string
|
||||
CommercePlatform string
|
||||
@@ -32,7 +30,6 @@ type ItemMappingCol struct {
|
||||
// ItemMappingCols 商品映射表字段常量
|
||||
var ItemMappingCols = ItemMappingCol{
|
||||
SQLBaseCol: beans.DefSQLBaseCol,
|
||||
AdvertiserID: "advertiser_id",
|
||||
AdPlatform: "ad_platform",
|
||||
AdItemID: "ad_item_id",
|
||||
CommercePlatform: "commerce_platform",
|
||||
|
||||
@@ -43,20 +43,7 @@ RTB竞价:用户请求 → bid_request_log → 竞价 → 胜出 → auction_l
|
||||
|
||||
#### 账户域
|
||||
|
||||
**advertiser 广告主(客户)**:多级/单层账户模式的业务主体
|
||||
|
||||
```sql
|
||||
CREATE TABLE advertiser (
|
||||
id BIGINT PRIMARY KEY,
|
||||
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)
|
||||
);
|
||||
```
|
||||
广告主(客户)= SaaS 租户,租户信息由 **admin-go 承载**,CID 不建账户/租户实体;所有表以 SQLBaseDO 自带 `tenant_id` 做业务隔离,广告主展示信息(名称/行业/联系人)由 admin-go 提供。
|
||||
|
||||
**ad_account 广告账户**(由 ad_platform_account 改造,见「已有表改造」):平台凭据 + 多级层级 + API 能力标识
|
||||
|
||||
@@ -66,7 +53,6 @@ CREATE TABLE advertiser (
|
||||
-- 计划:挂账户,区分外投/自有,预算在计划层
|
||||
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(自有)
|
||||
@@ -119,7 +105,6 @@ 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),
|
||||
@@ -140,7 +125,6 @@ CREATE TABLE creative (
|
||||
```sql
|
||||
CREATE TABLE material (
|
||||
id BIGINT PRIMARY KEY,
|
||||
advertiser_id BIGINT NOT NULL,
|
||||
type VARCHAR(10) NOT NULL, -- image/video/text
|
||||
title VARCHAR(128),
|
||||
file_path TEXT,
|
||||
@@ -264,14 +248,14 @@ CREATE TABLE daily_report (
|
||||
|
||||
| 表 | 改造动作 | 新增字段 |
|
||||
|---|---|---|
|
||||
| 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)`(防重复映射) |
|
||||
| ad_platform_account | 更名 **ad_account** | `name`、`parent_account_id`(自引用多级层级)、`account_role`(agent/owner/sub)、`capabilities` JSONB(attribution/delivery/report) |
|
||||
| commerce_platform_config | 微调 | `name` |
|
||||
| item_mapping | 微调 | 加唯一约束 `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
|
||||
- 改造方式:启动时 `ALTER TABLE ... ADD COLUMN IF NOT EXISTS`(幂等)+ 存量数据回填;ad_account 存量行 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 等配置表有存量,改造成本低
|
||||
@@ -298,6 +282,7 @@ CREATE TABLE daily_report (
|
||||
| 消耗统一 | daily_report:外投拉平台报表、DSP 聚合 auction_log | 报表/对账单一路径 |
|
||||
| 素材审核 | material 状态机 + material_audit 全量日志多通道(易盾/平台/人工) | 外投素材过平台审核,自有媒体素材只过易盾 |
|
||||
| 多级账户 | ad_account 自引用 parent_account_id + account_role | 代理层级与单层统一(PG 递归用 CTE) |
|
||||
| 租户隔离 | 广告主=租户,admin-go 承载;CID 以 tenant_id 隔离,不建账户/租户表 | SaaS 微服务边界:用户/权限/租户归 admin-go,CID 只存引用 |
|
||||
| 素材去重 | material.md5 唯一 | 一处素材多处投放,省存储与重复审核 |
|
||||
|
||||
### 事件驱动演进(分阶段,不一步到位)
|
||||
|
||||
Reference in New Issue
Block a user