广告投放系统设计:从计划到创意的三层架构
广告投放系统,说白了就是广告主花钱买流量的「指挥中心」。我做了这么多年广告系统,最核心的一点体会就是:层级结构设计得好不好,直接决定了系统的灵活性和可维护性。今天咱们就来聊聊广告计划、广告组、创意这三层到底该怎么设计。
为什么是三层?不是两层或四层?
你可能会问,搞这么复杂干嘛?直接一个广告,设置预算、出价、素材不就完了?
嗯,我刚开始做的时候也这么想。直到有一次,一个电商客户说:「我想对同一款商品,在上午和晚上用不同的出价,但素材是一样的。」如果只有一层,那就得建两个广告,素材重复存储,浪费空间不说,后续修改素材还得改两遍,太痛苦了。
所以,三层结构其实是为了解决一个核心问题:预算、定向、出价、创意这些维度的解耦。
- 广告计划(Campaign):管预算和投放目标。比如「双十一大促计划」,预算10万,目标是转化量。
- 广告组(Ad Group):管定向和出价。比如「25-35岁女性,出价2元/点击」。
- 创意(Creative):管素材本身。图片、视频、文案,都在这一层。
这样设计的好处是:一个计划下可以有多个广告组,一个广告组下可以有多个创意。预算在计划层控制,定向在组层控制,素材在创意层控制。各司其职,互不干扰。
广告计划:预算的「总闸门」
广告计划是最高层级。我习惯把它比作一个「总预算池」。所有广告组花的钱,都不能超过这个池子的上限。
设计时要注意几个关键字段:
| 字段名 | 类型 | 说明 |
|---|---|---|
| campaign_id | bigint | 主键,自增 |
| campaign_name | varchar(128) | 计划名称,比如「618大促」 |
| budget | decimal(12,2) | 总预算,单位元 |
| budget_mode | tinyint | 0=日预算,1=总预算 |
| status | tinyint | 0=暂停,1=启用,2=删除 |
| start_time | datetime | 投放开始时间 |
| end_time | datetime | 投放结束时间 |
这里有个坑:预算的扣减一定要用乐观锁。我在项目中遇到过并发扣减导致超预算的情况,后来改成用 update ... where budget_used + cost <= budget 这种原子操作才解决。
广告组:定向与出价的「策略中心」
广告组是三层中最复杂的一层。它承载了投放策略的核心逻辑:你要投给谁?愿意花多少钱?
定向方式五花八门,但归纳起来就几类:
- 人口属性:性别、年龄、地域
- 行为兴趣:比如「最近30天有购买母婴用品行为」
- 设备定向:iOS还是Android?WiFi还是4G?
- 自定义人群:广告主上传的ID包,比如老客户名单
出价策略也有讲究。我见过最常用的三种:
- CPC(按点击付费):适合品牌曝光,控制点击成本
- OCPM(目标转化出价):系统自动优化,按转化目标出价。说白了就是「你告诉系统你愿意为一个转化花多少钱,系统帮你找转化」
- CPM(按千次展示付费):适合强曝光需求
数据库设计上,广告组表大概长这样:
CREATE TABLE ad_group (
group_id BIGINT PRIMARY KEY AUTO_INCREMENT,
campaign_id BIGINT NOT NULL,
group_name VARCHAR(128),
targeting JSON, -- 定向条件,用JSON存储
bid_type TINYINT, -- 1=CPC, 2=OCPM, 3=CPM
bid_price DECIMAL(10,2), -- 出价
daily_budget DECIMAL(12,2), -- 单日预算上限
status TINYINT DEFAULT 1,
created_at DATETIME,
updated_at DATETIME,
INDEX idx_campaign (campaign_id)
);
你注意看,我把定向条件存成了JSON字段。为什么?因为定向条件太灵活了,今天加个「天气定向」,明天加个「LBS定向」,如果用关系表来存,改一次就要加一张表,太折腾。JSON字段配合查询时的条件解析,反而更灵活。
创意:素材的「百宝箱」
创意层是最贴近用户的。用户看到的是什么?就是创意。所以这一层要管好素材的版本、审核状态、尺寸规格。
我记得有一次,一个广告主上传了一张1920x1080的大图,但投放位是320x480的手机小图,结果图片被压缩得面目全非。从那以后,我就在创意表里加了一个尺寸校验字段,上传时自动检查素材尺寸是否匹配投放位。
创意表的核心字段:
| 字段名 | 类型 | 说明 |
|---|---|---|
| creative_id | bigint | 主键 |
| group_id | bigint | 所属广告组 |
| title | varchar(64) | 广告标题 |
| description | varchar(256) | 广告描述 |
| image_url | varchar(512) | 图片素材地址 |
| video_url | varchar(512) | 视频素材地址 |
| width | int | 素材宽度 |
| height | int | 素材高度 |
| audit_status | tinyint | 0=待审核,1=通过,2=拒绝 |
投放系统的数据库设计:别小看这三张表
上面三张表(campaign、ad_group、creative)是核心。但实际生产环境中,还需要几张辅助表:
- 预算消耗流水表:记录每一笔扣费,用于对账和预算回滚
- 定向维度字典表:把定向条件中的ID映射成中文描述,方便运营查看
- 素材审核记录表:记录每次审核的结果和原因,方便广告主修改
我个人的习惯是,预算相关的操作一定要做事务处理。扣费时,先插入流水,再更新预算余额,两个操作在一个事务里完成。否则一旦扣费成功但更新余额失败,就会出现「钱花了但预算没扣」的bug。
避坑指南:我曾经踩过的三个坑
最后分享几个实战中遇到的坑,希望能帮你少走弯路:
- 预算扣减的并发问题:我之前用程序里先查余额再扣减,高并发下超预算严重。后来改成数据库层的原子更新,问题解决。
- 定向条件的存储设计:一开始用多张关联表存定向,结果每次查询都要join七八张表,慢得要命。后来改成JSON存储+Elasticsearch索引,查询速度提升了10倍。
- 创意审核状态同步:创意审核通过后,需要同步更新广告组的状态(比如组下所有创意都被拒,组应该自动暂停)。这个逻辑一开始没做,导致广告主花了钱但创意没展示。后来加了个定时任务做状态同步。
嗯,广告投放系统的三层架构设计,说白了就是把预算、策略、素材这三个维度拆开管理。每一层只关心自己的事,层与层之间通过ID关联。这样设计,系统既灵活又稳定。下次你设计类似系统时,不妨试试这个思路。