自助广告投放系统源码拆解:广告位商品化与订单全链路实现
简介这是一套面向网站站长、个人博主及中小团队的自助广告投放系统源码将广告位展示、下单购买、审核投放、到期管理等环节串联起来实现全自动无人化出售网站广告位解决传统人工接单效率低、权限不清等问题。资源共807个文件压缩包约14.69MB主体为49个PHP业务文件与147个JS脚本搭配82个CSS样式、458个PNG图标以及SQL初始化文件前端基于Layui构建界面清爽功能模块区分明确。附带安装教程可快速部署至本地或服务器运行。目前已有119人学习下载适合想快速搭建广告管理平台或学习PHP后台开发流程的用户。通过源码可清晰理解广告位定价、订单状态流转、用户后台管理等闭环逻辑便于二次扩展与商用落地。1. 自助广告投放系统把广告位变成可自助下单的商品一个日活几千的站点首页头部横幅、侧边栏、文章内嵌位常常闲置另一边广告主想投个位置得反复私聊站长问报价、等上刊、盯到期。这套源码解决的正是这个信息差站长把广告位当作商品上架广告主在前台自助选位、下单、传素材、在线结算上刊与到期全部由系统在后台自动执行。对站长省掉了询价沟通的人力对广告主投放全程可视可控对开发者这套代码把「广告位资源化、订单状态机、支付回调、定时任务」串成一条完整的业务闭环值得拆开看实现。2. 站点架构与数据库设计广告位状态如何流转2.1 功能模块拆解三个角色三条链路这套系统按角色可以拆成三个使用面。站长后台负责维护站点白名单、配置广告位尺寸与刊例价、审核广告素材、查看结算汇总广告主前台负责注册、选择广告位、计算价格、上传素材、发起支付系统层则运行定时任务负责上下刊和订单过期关闭。三条链路不是平行关系而是通过一张订单表串起来站长设置广告位状态广告主基于状态生成订单系统根据订单时间推进上下刊。源码根目录下那一串 CSS 文件layui.css、skin.css、skin.mobile.css、toast.css、loading.css、admin.css说明前端的后台管理界面是围绕 layui 的皮肤体系搭建的PC 端管理台和移动端适配分了两套皮肤。这个信息在部署时是有用的如果线上用 Nginx 做了静态资源压缩需要把 admin.css 和 skin.min.css 这类压缩文件单独配置缓存规则否则后台样式可能时好时坏。2.2 核心数据表与字段设计广告投放系统的数据库设计核心不在「广告位表」本身而在订单与广告位之间的状态联动。下表列出我拆包后看到的几张核心表字段名按常见命名整理这决定了后续所有业务代码的查询逻辑。表名核心字段关键状态值作用ad_sitesite_id、site_name、site_url、daily_uv、statusstatus: 1 正常 / 0 停用记录站长名下收录的站点信息ad_positionpos_id、site_id、ad_type、width、height、price_day、price_month、audit_statusaudit_status: 1 在售 / 2 下架 / 3 待审核广告位资源池前台只展示 audit_status1 的数据ad_orderorder_no、pos_id、ad_owner、material_url、start_time、end_time、pay_status、order_statusorder_status: 0 待支付 / 1 投放中 / 2 已结束 / 3 已取消广告订单主表每笔投放记录ad_settlementid、order_no、site_id、amount、settle_statussettle_status: 0 未结算 / 1 已结算站长侧结算汇总数据从字段关系可以看出广告位表不加「占用」字段而是在创建订单时通过时间区间判断冲突。这样设计的优点是并发下单时不会因为更新同一个广告位字段而锁行缺点是查询可用广告位时写 SQL 要额外关联订单表排除时间重叠的数据。2.3 广告位可用性检索排除已有订单的时间区间广告主选位时系统执行的是「当前广告位在售 当前时段无重叠订单」双重判断。下面是一段核心查询逻辑我在本地测试时把时间参数写死便于复现SELECT p.pos_id, p.ad_type, p.price_day, s.site_name, s.site_url FROM ad_position p LEFT JOIN ad_site s ON p.site_id s.site_id WHERE p.audit_status 1 AND s.status 1 AND p.pos_id NOT IN ( SELECT pos_id FROM ad_order WHERE order_status IN (0, 1) AND start_time 2025-06-01 00:00:00 AND end_time 2025-05-25 00:00:00 ) ORDER BY p.price_day DESC LIMIT 0, 20;这段查询排除了「待支付」与「投放中」的订单只要目标投放周期与现有订单存在交叉这个广告位就不可售。两个时间参数分别代表用户选择的投放结束时间和开始时间判断逻辑是start_time 目标结束时间 AND end_time 目标开始时间能正确覆盖包含、相交、相邻三类边界情况。需要说明的是order_status IN (0,1)意味着待支付订单也会锁定广告位这样设计是为了防止用户反复提交相同周期的订单占位但代价是未支付订单过期前广告位不可售需要定时任务兜底释放。2.4 前端资源加载与皮肤切换后台页面的布局基于 layui 栅格系统源码中skin.css、skin.min.css、skin.mobile.css的并存是为了兼容不同端和不同浏览器缓存策略。实际部署时建议把 PC 与移动端入口分开配置模板变量根据User-Agent或后端配置选择加载哪一组皮肤避免移动端误加载 PC 端样式导致布局错位。3. PHP 环境部署与源码安装从压缩包到可访问站点3.1 环境准备与版本搭配建议源码基于 PHP MySQL 编写常见部署环境是 Nginx PHP-FPM MySQL或 MariaDB。从兼容性和安全更新角度我建议 PHP 使用 7.4 或 8.0 分支MySQL 使用 5.7 或 MariaDB 10.4 以上。PHP 版本不宜高于 8.1部分早年代码中each()、create_function()等函数在 PHP 8 后会报致命错误这类问题在安装后可以通过修改兼容层解决。推荐环境组件清单如下组件版本建议用途Nginx1.20静态文件与 PHP-FPM 转发PHP7.4 / 8.0运行业务代码PHP扩展PDO、mysqli、curl、gd数据库连接与图片素材处理MySQL5.7 或 MariaDB 10.5订单与广告位数据存储3.2 源码目录结构与入口检查将源码包解压后放到站点根目录例如/www/wwwroot/ad-site。压缩包内包含install目录安装向导、admin目录站长后台、index.php广告主前台入口以及config配置文件目录。上传完成后先检查目录权限cd /www/wwwroot/ad-site chown -R www:www /www/wwwroot/ad-site chmod -R 755 /www/wwwroot/ad-site chmod -R 777 /www/wwwroot/ad-site/data /www/wwwroot/ad-site/uploaddata目录存放缓存与日志upload目录存放广告主上传的素材文件这两个目录必须允许 PHP-FPM 进程写入否则素材上传和页面渲染都会报权限错误。chown -R www:www很关键很多安装后白屏的问题都源于 php-fpm 用户与文件所有者不一致。3.3 Nginx 站点配置与 PHP-FPM 转发Nginx 站点配置文件/www/server/panel/vhost/nginx/ad-site.conf不同面板路径有差异核心配置如下server { listen 80; server_name ad.example.com; root /www/wwwroot/ad-site; index index.php index.html; location / { if (!-e $request_filename) { rewrite ^/(.*)$ /index.php?$1 last; } } location ~ \.php$ { fastcgi_pass 127.0.0.1:9000; fastcgi_index index.php; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; include fastcgi_params; } location ~* \.(css|js|png|jpg|gif)$ { expires 30d; access_log off; } }配置里有两点需要解释。第一if (!-e $request_filename)是 PHP 项目的通用伪静态写法把不存在的文件请求转发给index.php广告投放系统中的订单详情页、素材预览页都依赖这个规则第二最后一段静态资源规则把 CSS、JS 和图片的缓存时间设为 30 天对 layui 这类体量较大的前端框架能显著减少后台打开时间。如果广告主上传的素材图片更新频繁这段规则应当只对skin.min.css这类带版本号的资源生效避免广告素材被客户端缓存。3.4 数据库初始化与配置文件修改在 MySQL 中导入数据库脚本源码包内一般有ad_system.sql。命令行导入方法mysql -u root -p -e CREATE DATABASE ad_system DEFAULT CHARACTER SET utf8mb4; mysql -u root -p ad_system ad_system.sql导入完成后修改/www/wwwroot/ad-site/config/database.phpreturn array( host 127.0.0.1, port 3306, dbname ad_system, username ad_system_user, password 你的数据库密码, charset utf8mb4, );charset参数建议保持utf8mb4这样广告主上传的素材名称、订单备注中的 emoji 表情不会在入库时报编码错误。部分源码默认写成utf8如果广告位名称包含生僻字会出现“Incorrect string value”报错直接修改上述配置即可。3.5 安装自检与常见错误排查配置完成后访问http://ad.example.com/install/index.php执行安装向导安装前检查以下几项curl -I http://ad.example.com返回 200 且未被重定向到安装页说明伪静态正常后台登录页能打开但验证码不显示检查gd扩展是否安装php -m | grep gd广告主上传素材提示文件过大修改php.ini中的upload_max_filesize与post_max_size建议均设为 20M页面出现 500 错误时查看 PHP-FPM 日志tail -f /www/wwwroot/ad-site/data/log/error.log4. 广告订单全链路下单、支付回调与自动上下线4.1 自助下单的计价逻辑与订单生成广告主选择广告位和投放周期后系统前端展示两种计费方式按天计价price_day与按月计价price_month计算逻辑并不复杂关键在于订单号的生成与时间参数的校验。下面是一段生成订单的 PHP 代码按常规写法整理public function createOrder() { $posId intval($_POST[pos_id]); $startTs strtotime($_POST[start_time]); $endTs strtotime($_POST[end_time]); $days ceil(($endTs - $startTs) / 86400); if ($days 0) { $this-error(投放时间不合法); } $pos $this-model-findPosition($posId); $amount $days * $pos[price_day]; $orderNo date(YmdHis) . mt_rand(1000, 9999); $data array( order_no $orderNo, pos_id $posId, ad_owner trim($_POST[ad_owner]), material_url trim($_POST[material_url]), start_time date(Y-m-d H:i:s, $startTs), end_time date(Y-m-d H:i:s, $endTs), amount $amount, order_status 0, create_time date(Y-m-d H:i:s), ); $this-model-insert(ad_order, $data); $this-redirect(pay/index?order_no . $orderNo); }代码中ceil(($endTs - $startTs) / 86400)将投放时长按天向上取整这样即使广告主选择当天下午到次日凌晨也会按 2 天计费避免系统被按小时薅价格差。order_status初始为 0待支付在支付完成前这条订单已经锁定了广告位时间段防止同一个位置被重复售卖。4.2 支付回调验签与订单状态推进在线支付接入时支付平台的异步通知是核心。处理回调的原则是收到通知后先验签再幂等更新订单状态。下面是回调处理的关键 PHP 片段$data $_POST; $sign $data[sign]; unset($data[sign]); ksort($data); $signStr urldecode(http_build_query($data)) . key . $this-paymentKey; if (md5($signStr) ! $sign) { exit(fail); } $orderNo $data[out_trade_no]; $order $this-model-findByOrderNo($orderNo); if ($order $order[order_status] 0) { $this-model-update(ad_order, array( order_status 1, pay_time date(Y-m-d H:i:s), pay_trade_no $data[trade_no], ), order_no {$orderNo}); // 写入对账日志 file_put_contents(/tmp/pay_callback.log, date(Y-m-d H:i:s) . . $orderNo . PHP_EOL, FILE_APPEND); } exit(success);验签流程分成三步去掉sign参数后对剩余字段按 key 排序拼接支付密钥后做 md5比对签名是否一致。urldecode(http_build_query($data))是为了处理支付回调中 URL 编码的空格与中文这个步骤容易漏漏掉之后签名一直不通过。幂等处理的逻辑在if ($order $order[order_status] 0)即使支付平台重复推送回调订单状态只会从待支付推进到投放中一次后续回调直接返回 success。4.3 定时任务驱动广告自动上下线投放中的订单到期后广告位必须自动释放否则广告主的素材会一直展示在站点上。系统中通过 crontab 定时任务执行上刊脚本核心逻辑如下*/5 * * * * cd /www/wwwroot/ad-site php cron/auto_trade.php /tmp/ad_cron.log 21auto_trade.php内部执行两类操作// 上刊将当前时间处于投放周期内的订单素材展示到前台 $now date(Y-m-d H:i:s); $sql UPDATE ad_order o LEFT JOIN ad_position p ON o.pos_id p.pos_id SET o.order_status 1 WHERE o.order_status 0 AND o.pay_status 1 AND o.start_time {$now} AND o.end_time {$now}; $this-model-execute($sql); // 下刊将已过期的订单状态改为已结束释放广告位 $sql2 UPDATE ad_order SET order_status 2 WHERE order_status 1 AND end_time {$now}; $this-model-execute($sql2);这里用 Cron 定期扫描代替了每笔订单设置一次性定时器原因在于服务器上的at命令在重启后会丢失任务而 Cron 每次执行时都是基于当前时间与订单周期做判断天然具备可恢复性。执行间隔设为 5 分钟意味着上下刊最大延迟 5 分钟对广告业务来说可以接受如果站长要求精确到分钟级别可以将间隔缩短为* * * * *但要注意数据库的 UPDATE 频率随之上升。4.4 结算对账按订单维度汇总站长收益广告投放结束后站长后台需要看到各站点、各广告位的累计收入。一条常用的月度结算查询 SQL 如下SELECT DATE_FORMAT(end_time, %Y-%m) AS month, site_id, SUM(amount) AS total_amount, COUNT(*) AS order_count FROM ad_order WHERE order_status 2 AND end_time 2025-06-01 AND end_time 2025-07-01 GROUP BY site_id, month ORDER BY month DESC, total_amount DESC;执行结果可以直接对接导出表格功能。查询中order_status 2限定了已结束订单避免把待支付与投放中的数据计入营收end_time的范围限制了结算归属月份。实际运营中站长通常按订单结束时间结算而不是按支付时间这样退款处理时口径一致不容易产生对不上的账目。5. 权限控制、防刷限流与缓存加速上线前的最后一步5.1 后台登录安全与操作权限这套系统是多角色共用一套代码的典型场景站长后台的 admin 模块与广告主前台必须严格分离权限。我建议在源码原有登录校验的基础上增加三项登录接口加图形验证码源码中已有验证码类文件确认gd扩展开启即可后台入口路径改名例如将admin目录重命名为admin202506降低自动扫描工具的命中概率Redis 或文件缓存记录连续失败次数5 次失败后锁定该 IP 15 分钟5.2 下单接口防刷与恶意请求拦截订单生成接口一旦被脚本刷会产生大量待支付订单导致广告位被锁定而真实用户无法购买。一个简单的限流方案是记录用户 ID 或 IP 在短时间内的下单次数$key order_limit_ . $_SESSION[user_id]; $count (int)Redis::get($key); if ($count 3) { $this-error(操作过于频繁请稍后再试); } Redis::incr($key); Redis::expire($key, 60);逻辑很直白同一用户 60 秒内最多提交 3 次订单超过即拒绝既不影响正常用户多次比价又挡住了脚本批量占位。如果将这个限制放宽到 IP 维度要注意 NAT 环境下同一出口 IP 背后可能有多个广告主容易被误伤。5.3 静态资源缓存与加速layui 全家桶涉及的 CSS 文件较多生产环境建议在 Nginx 层面对皮肤文件单独设置长缓存。配合前文 Nginx 配置中的expires 30d首次加载后台后后续刷新基本只请求接口数据。广告主前台上传的素材图片建议走独立域名或 CDN与主站静态资源分开避免大量图片请求拖慢后台操作响应。上线之前优先验证三件事执行一次安装向导并跑通完整的下单到支付流程、确认 Cron 任务在重启服务器后自动生效、检查 data 目录和 upload 目录的写入权限没被加固脚本改掉。这套系统稳定运行的关键不在某个单点而是时间状态机与定时任务之间不能出现断档。本文还有配套的精品资源点击获取