广告投放系统设计:从计划到创意的三层架构

广告投放系统,说白了就是广告主花钱买流量的「指挥中心」。我做了这么多年广告系统,最核心的一点体会就是:层级结构设计得好不好,直接决定了系统的灵活性和可维护性。今天咱们就来聊聊广告计划、广告组、创意这三层到底该怎么设计。

为什么是三层?不是两层或四层?

你可能会问,搞这么复杂干嘛?直接一个广告,设置预算、出价、素材不就完了?

嗯,我刚开始做的时候也这么想。直到有一次,一个电商客户说:「我想对同一款商品,在上午和晚上用不同的出价,但素材是一样的。」如果只有一层,那就得建两个广告,素材重复存储,浪费空间不说,后续修改素材还得改两遍,太痛苦了。

所以,三层结构其实是为了解决一个核心问题:预算、定向、出价、创意这些维度的解耦

  • 广告计划(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包,比如老客户名单

出价策略也有讲究。我见过最常用的三种:

  1. CPC(按点击付费):适合品牌曝光,控制点击成本
  2. OCPM(目标转化出价):系统自动优化,按转化目标出价。说白了就是「你告诉系统你愿意为一个转化花多少钱,系统帮你找转化」
  3. 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字段配合查询时的条件解析,反而更灵活。

注意:JSON字段虽然灵活,但查询性能是个问题。如果定向条件需要频繁做复杂查询(比如「找出所有定向了北京的用户」),建议把关键定向维度单独建索引字段,或者用Elasticsearch做倒排索引。

创意:素材的「百宝箱」

创意层是最贴近用户的。用户看到的是什么?就是创意。所以这一层要管好素材的版本、审核状态、尺寸规格。

我记得有一次,一个广告主上传了一张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。

核心原则:广告投放系统的数据库设计,要围绕「预算安全」和「策略灵活」两个目标展开。预算安全靠事务和乐观锁保证,策略灵活靠JSON字段和扩展表实现。

避坑指南:我曾经踩过的三个坑

最后分享几个实战中遇到的坑,希望能帮你少走弯路:

  1. 预算扣减的并发问题:我之前用程序里先查余额再扣减,高并发下超预算严重。后来改成数据库层的原子更新,问题解决。
  2. 定向条件的存储设计:一开始用多张关联表存定向,结果每次查询都要join七八张表,慢得要命。后来改成JSON存储+Elasticsearch索引,查询速度提升了10倍。
  3. 创意审核状态同步:创意审核通过后,需要同步更新广告组的状态(比如组下所有创意都被拒,组应该自动暂停)。这个逻辑一开始没做,导致广告主花了钱但创意没展示。后来加了个定时任务做状态同步。

嗯,广告投放系统的三层架构设计,说白了就是把预算、策略、素材这三个维度拆开管理。每一层只关心自己的事,层与层之间通过ID关联。这样设计,系统既灵活又稳定。下次你设计类似系统时,不妨试试这个思路。