saas隔离使用admin-go中的租户体系

This commit is contained in:
2026-08-25 15:26:13 +08:00
parent 6ce3ebbb91
commit 13cd3875d3
9 changed files with 68 additions and 140 deletions
+3 -3
View File
@@ -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 | 素材库 + 多通道审核状态机 |
-39
View File
@@ -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",
}
-3
View File
@@ -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",
-3
View File
@@ -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",
+37 -40
View File
@@ -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",
}
-3
View File
@@ -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",
}
-3
View File
@@ -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",
+6 -21
View File
@@ -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` JSONBattribution/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` JSONBattribution/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_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 等配置表有存量,改造成本低
@@ -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-goCID 只存引用 |
| 素材去重 | material.md5 唯一 | 一处素材多处投放,省存储与重复审核 |
### 事件驱动演进(分阶段,不一步到位)