推广系统源码实战:短链归因、佣金结算与防刷对账

发布时间:2026/10/30 17:11:22
推广系统源码实战:短链归因、佣金结算与防刷对账 简介面向有PHP基础的开发者与中小型推广平台搭建者这套推广系统源码提供可查看、可修改的完整实现用于搭建广告投放、推广者管理、转化跟踪与结算分析等业务流程。压缩包共14个文件、约907KB以6个PHP脚本为核心业务入口辅以3个url快捷链接、易语言模块与动态库文件以及备份和说明文本规模轻量但模块指向明确。已有1824人学习下载说明其在推广系统学习与二次开发场景中具有一定参考价值。读者可据此梳理用户管理、广告管理、跟踪代码、报表统计、支付结算和API对接等关键模块的实现思路并按自身业务调整推广算法、增加广告渠道或集成第三方服务同时也能借备份与说明文件了解目录组织和部署前检查要点适合作为PHP推广平台原型与功能拆解的学习素材。1. 一条推广链接背后的四个环节缺一个就是糊涂账上个月有个做工具类小程序的同行来问我投放后台里那条“注册来源”到底能不能信。他们花钱买量月底对账时渠道方报的量和自己库里的量差了两成谁也说不清差在哪。这类问题的根源往往不在投放而在推广系统本身——链接怎么发、落地页参数怎么带、注册时怎么落库、佣金按什么口径算四个环节任何一个环节搞不清楚后面就全是糊涂账。这份推广系统源码解决的就是这条链路渠道建档、推广链接与二维码生成、点击与注册归因、佣金冻结与结算后端服务、管理后台、数据库脚本是配套的不是只给几个页面截图的空壳。适合自己要做投放或分销的中小团队也适合手里接了个推广系统源码二开需求、想先有个能跑的底子对照的开发者。2. 把源码跑起来三端启动顺序与数据库初始化拿到一套没跑过的推广系统源码很多人第一反应是先看代码。我一般反过来先把目录结构、依赖版本、启动顺序三件事理清楚代码可以慢慢读环境跑不起来全是空谈。这类系统跟单体业务系统不一样的地方在于它天然是“对外接口 异步任务 管理后台”三段式三端跑起来的先后顺序错了你会看到一堆莫名其妙的空指针和连不上的 Redis。2.1 目录结构决定了你能改哪一层一套完整的推广系统源码通常拆成五块promotion-api是对外接口层落地页取参、短链跳转、注册回传、订单回调都走它promotion-job是定时任务层佣金解冻、每日对账、渠道日报都在这promotion-admin是后台前端渠道管理、链接生成、结算审核的界面promotion-common放公共实体和工具sql/目录放建表脚本和初始化数据。这个拆法不是随便定的api 层要扛外部流量可能被刷job 层跑批不能和 api 抢连接池admin 是内部人用的权限模型和 api 完全两套。你要二开的时候先想清楚改的是哪一层把结算逻辑塞进 api 层是新手最容易犯的错跑量的时候一次全表扫描能把接口拖死。依赖上常见配置是 JDK 8、Maven 3.6 以上、MySQL 5.7 或 8.0字符集必须是 utf8mb4渠道名和推广文案里带表情符号很常见utf8 存不进去、Redis 5 以上、Node 14 以上给前端。别小看字符集这一条我见过不止一次上线后渠道名称显示成问号回头改字符集要重建整张表。2.2 建表顺序和配置项先看这三处数据库脚本不要一次性全执行完就不管了先确认表之间的依赖顺序渠道表p_channel→ 推广链接表p_short_link→ 点击日志表p_click_log→ 用户绑定表p_user_bind→ 订单表p_order→ 佣金流水表p_commission_flow→ 提现表p_withdraw。外键我一般不加全靠索引和代码约束因为这类系统后面大概率要分库或者做冷热分离外键是纯累赘。初始化完成后先跑三条检查别急着启动服务-- 1. 渠道字典有没有初始数据没有的话后台登录进去一片空白 SELECT channel_code, channel_name, status FROM p_channel LIMIT 10; -- 2. 短码唯一索引这张表并发写最多索引缺了必然串号 SHOW INDEX FROM p_short_link WHERE Key_name uk_short_code; -- 3. 佣金流水的幂等唯一键是防重复结算的最后一道闸 SHOW INDEX FROM p_commission_flow WHERE Key_name uk_order_user_type;三条里任何一条查不到结果先别往下走。第二条缺索引后面高并发生成短链会出现两条记录同一个码用户点进去归因到别人头上第三条缺索引支付回调重试一次就多算一笔佣金这类问题上线后才发现追账能追到你怀疑人生。配置文件里改四个地方就能跑起来其余保持默认server: port: 8081 spring: datasource: url: jdbc:mysql://127.0.0.1:3306/promotion?useUnicodetruecharacterEncodingutf8mb4serverTimezoneUTC username: promo password: change_me redis: host: 127.0.0.1 port: 6379 database: 3 # 单独一个库别和业务缓存混用清库时容易误伤 promotion: short-domain: s.example.com # 短链域名换成自己的 sign-secret: 换成32位随机串 # 签名密钥源码里的默认值上线必须换 attr-window-days: 7 # 归因窗口点击后多少天内注册还算这个渠道 freeze-days: 7 # 佣金冻结天数对应退款风险期serverTimezoneUTC这一项别随手改成 Asia/Shanghai后面第 5 章会讲到跨天统计对不上的坑就是从这来的。sign-secret是链接防篡改的根源码里通常给了个默认字符串很多人本地跑通了就直接上生产等于把生成链接的权限公开了谁都能给你的渠道刷量。2.3 三端启动顺序与最小验证启动顺序固定为MySQL / Redis → 执行 sql 脚本 →promotion-api→promotion-job→promotion-admin。api 先起是因为 job 里的任务启动时会调 api 的健康检查反过来会报一堆连接拒绝看着吓人其实没坏。# 1. 接口服务落地页、短链、回调都走它 java -jar promotion-api.jar --spring.profiles.activedev # 2. 任务服务结算/解冻/对账在这里生产环境建议单独一台机器 java -jar promotion-job.jar --spring.profiles.activedev # 3. 后台前端本地调试用 dev server生产环境打静态包丢 nginx cd promotion-admin npm install npm run dev三端起来后别急着点界面先用命令行把主链路走一遍这是判断“源码真实可用”最快的方式# 生成一条推广链接sign 按 channelCode uid ts secret 拼接后取 md5 curl -X POST http://127.0.0.1:8081/api/link/create \ -H Content-Type: application/json \ -d {channelCode:C1001,uid:10086,ts:1710000000,sign:按规则算出的签名} # 拿到短码后走一次跳转重点看 Location 里原始参数有没有丢 curl -I http://127.0.0.1:8081/s/Ab3xK9第一条返回里应该有shortCode和fullUrl两个字段第二条必须是 302并且Location里带着ch和clk两个参数。如果 302 了但参数没了说明短链服务只存了目标地址没存原始 query这是这套源码最常见的改造成本所在第 5 章第一条会细说。3. 推广链接与归因落库短链发号、签名与绑定幂等推广系统的价值密度最高的部分就在这一章。前面跑起来了只是能看链接怎么发、参数怎么透传、注册时怎么把人和渠道对上决定了你后面所有数据的可信度。归因错了佣金算得再准也是错的。3.1 短码别用自增 ID签名别省短码生成有三个方案数据库自增 ID 转 62 进制、Redis 号段转 62 进制、纯随机 6 位加重试。自增 ID 最省事但最危险别人拿到s/1001就知道你一共发了多少条链接顺手遍历一遍就能把你的渠道数据摸干净。我一般用 Redis 号段加固定盐做异或混淆既保证唯一又不可顺序推测// 短码生成Redis 号段取序 62 进制 固定盐混淆 public String genShortCode(long seq) { long mixed seq ^ 0x5F3A9CL; // 混淆因子一旦上线就不能改改了历史短码全部失效 String alphabet 0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ; StringBuilder sb new StringBuilder(); while (mixed 0) { sb.append(alphabet.charAt((int) (mixed % 62))); mixed / 62; } return sb.reverse().toString(); // 6 位约 568 亿组合中小团队用十年都够 }逻辑很简单但参数有两处必须注意。混淆因子0x5F3A9C是硬编码在代码里的盐上线后不能动动了之后所有已投放的短链全部指向错误目标这个损失比你想的大。号段步长建议设 1000 并落库持久化当前最大值服务重启时从库里恢复纯内存计数一重启就从头开始短码会覆盖。号段用完后异步预取下一段别在请求线程里同步去抢。签名校验放在/api/link/create上规则是md5(channelCode uid ts secret)ts和服务端时间差超过 5 分钟直接拒。这不是防黑客是防自己的后台被人随手改个uid就生成一堆挂在别人名下的链接。3.2 参数透传前端只做兜底服务端才是准绳归因链路的标准做法是短链跳转时服务端下发一个clk点击 ID落地页把它和渠道码一起存进 localStorage注册请求带上后端以p_click_log里这条clk记录为准落绑定表前端传上来的渠道码只在前端异常时兜底。为什么不让前端说了算因为 localStorage 可以被清、可以被改、跨端还会丢而点击日志是你自己写的改不了。// 落地页脚本只负责留参不做任何判定 (function () { const qs new URLSearchParams(location.search); const ch qs.get(ch); // 渠道码用于兜底 const clk qs.get(clk); // 点击 ID服务端归因的唯一凭据 if (!ch !clk) return; // 覆盖写同一次会话以最后一次落地为准跨天由后端归因窗口兜底 localStorage.setItem(promo_ch, ch || ); localStorage.setItem(promo_clk, clk || ); localStorage.setItem(promo_ts, Date.now()); })();promo_ts这个时间戳别省注册时一起传上去后端用它判断有没有超出attr-window-days窗口。窗口内算这个渠道窗口外算自然量规则写死在代码里别让运营口头约定口头约定在结算日一定会变成争议。3.3 绑定表必须幂等一个用户只能属于一个渠道用户绑定表p_user_bind上一定要有uk_user_id唯一索引。这不是性能优化是业务约束一个注册用户只能归属一个渠道否则同一个订单会被两个渠道同时算佣金结算时你连该扣谁的钱都判断不了。常见做法是插入用INSERT IGNORE失败就直接返回已绑定不要抛异常给前端。首次归因还是末次归因这个选择要在系统上线前定死。投放买量场景一般是首次归因用户第一次从哪个渠道来就归谁避免渠道之间互相抢量分销裂变场景常用末次归因最后一个邀请人拿佣金激励更直接。源码里通常做成配置项改之前先把历史数据想清楚切换归因模型等于把过去的数据口径全改了运营那边会炸。4. 佣金结算链路规则表、状态机与对账脚本到这一章才是真正容易出事的环节。推广系统出 bug 不外乎两种归因错了或者钱算错了。归因错了运营骂你钱算错了财务和渠道一起找你。结算链路的代码量不大但每行都得想清楚重复执行会怎样。4.1 分润规则落表比例用万分比金额用分规则表设计的关键是支持多档阶梯还要支持同一渠道不同时间段的不同比例。我一般这么建-- 分润规则表同渠道多档按生效时间 金额区间命中唯一一条 CREATE TABLE p_commission_rule ( id BIGINT PRIMARY KEY AUTO_INCREMENT, channel_code VARCHAR(32) NOT NULL, level TINYINT NOT NULL DEFAULT 1 COMMENT 1 一级 2 二级, min_amount INT NOT NULL DEFAULT 0 COMMENT 订单金额下限单位分, max_amount INT NOT NULL DEFAULT 99999999 COMMENT 上限单位分, rate_bp INT NOT NULL COMMENT 比例万分比300 即 3%, effect_from DATETIME NOT NULL, effect_to DATETIME DEFAULT NULL, KEY idx_channel_time (channel_code, effect_from) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;两个参数选择值得说明。比例用万分比整数300而不是0.03是为了避开浮点误差0.03 * 199在 Java 里算出来是5.969999999999999四舍五入之后就差一分钱一天几千单就是几块钱的账对不上。金额统一用分存INT或BIGINT前端展示时再除 100这条规则在整套系统里必须贯彻到底中间任何一处用了double都要改回来。规则命中逻辑按channel_code查生效时间覆盖当前时间的记录再按订单金额落进[min_amount, max_amount)区间命中多条取effect_from最新的那条。别用BETWEEN区间边界会重叠max_amount那一条必然被算两次。4.2 冻结、解冻、提现状态机不能省佣金流水的状态我固定设五档0 冻结、1 可提现、2 已提现、3 已冲正、4 已取消。订单支付成功时写一条状态 0 的流水同时落unfreeze_at 当前时间 freeze_daysjob 每天扫一遍到期且状态为 0 的记录改成 1用户发起提现时锁住对应流水改成 2订单退款时把还没提现的流水改成 3。Transactional(rollbackFor Exception.class) public void settle(String orderNo, long userId, String channelCode, int payAmountFen) { // 依赖 uk_order_user_type 唯一键兜底重复回调会抛 DuplicateKeyException CommissionFlow flow new CommissionFlow(); flow.setOrderNo(orderNo); flow.setUserId(userId); flow.setChannelCode(channelCode); flow.setBaseFen(payAmountFen); // 计佣基数注意是实付不是应付 flow.setAmountFen(calcRule(channelCode, payAmountFen)); flow.setStatus(0); // 0 冻结 flow.setUnfreezeAt(LocalDateTime.now().plusDays(freezeDays)); flowMapper.insert(flow); }payAmountFen一定要用实付金额不是订单原价。用了优惠券的订单按原价算佣金等于平台自己把补贴也分给渠道了这个错误在带优惠券的电商场景里极其常见而且前几个月没人发现因为差异金额小、分散在几千单里。唯一键冲突的处理方式是在调用方 catch 掉DuplicateKeyException直接返回成功别往外抛支付回调那边看到失败会一直重试。4.3 每日对账差异要落到表里不要只看日志对账脚本每天凌晨跑一次把当天已支付订单和佣金流水做左连接比对差异写进临时表运营第二天上班第一件事就是看这张表有没有数据。光打日志没人看日志滚一圈就没了。-- 订单侧实付金额 vs 佣金侧计佣基数不等于就是异常 SELECT o.order_no, o.pay_amount_fen, IFNULL(c.base_fen, 0) AS base_fen FROM p_order o LEFT JOIN p_commission_flow c ON c.order_no o.order_no AND c.type 1 WHERE o.status 1 AND o.pay_time CURDATE() - INTERVAL 1 DAY AND o.pay_time CURDATE() AND IFNULL(c.base_fen, 0) o.pay_amount_fen;这条 SQL 会捞出两类问题一是订单支付了但没生成佣金流水通常是回调丢了或者渠道码为空二是有流水但基数对不上通常是部分退款后没同步冲正。两类都要人工介入脚本不做自动修复自动补钱的脚本一旦逻辑有误就是批量事故。5. 避坑与排查五条血泪记录从现象倒推原因这套源码我在不同环境里跑过几轮踩过的坑基本集中在这五条上。每条都按“现象 → 原因 → 解决”写你对照自己的现象直接找。5.1 归因链路断在哪儿现象一短链跳转 302 正常但落地页拿不到ch参数。原因短链服务建表时只存了target_url跳转时直接sendRedirect原始 query 在生成短码那一步就被丢掉了。很多人以为是 URL encode 问题改半天URLEncoder没用。解决p_short_link加一列params存原始参数串Base64 编码避免特殊字符跳转时解码回拼到Location后面。改完之后用curl -I看Location里是不是原样带着参数这是唯一可靠的验证方式。现象二一个用户被绑定到两个渠道两个渠道都来要佣金。原因p_user_bind只建了普通索引没建唯一索引加上注册接口有并发两个请求同时查到“未绑定”就都插入了。解决先洗数据保留最早一条再补uk_user_id唯一索引插入语句换成INSERT IGNORE。别想着靠代码里的if (exists)判断那个判断和插入之间永远有窗口。5.2 钱对不上的三类差异现象三支付回调重试一次佣金流水多一条。原因p_commission_flow的幂等键建在了order_no单列上但二级分销场景同一订单要给两个人分佣唯一键冲突被误判成重复请求。于是有人把唯一键删了问题反而更大。解决唯一键改成(order_no, user_id, type)三列组合既挡住重复回调又不影响多级分佣。冲突时返回成功不抛错。现象四财务报表差几分钱逐单核对又都对。原因某处代码用了double算比例0.03 * 199得5.969999...四舍五入后是 6 分还是 5 分取决于舍入方式。分散在上万单里就是几十块的差异。解决全链路改整数分比例用万分比最后一步Math.round(amountFen * rateBp / 10000.0)只在唯一一个工具方法里做其他任何地方不许直接算钱。现象五日报和前一天的日报对不上凌晨那一小时的单子两边都算或都不算。原因数据库连接串写了serverTimezoneUTC但统计 SQL 用的是CURDATE()跑在东八区服务器上跨天边界偏了 8 小时。解决统计口径统一用 UTC 存、按东八区展示CURDATE()全部换成显式的时间区间参数由应用层算好边界再传进去。这条改起来琐碎但不改的话每月 1 号的对账永远对不平。6. 上线前的防刷阈值与回归验证一条 click_id 串全链路源码能跑起来和能上线是两件事。推广系统天生暴露在公网链接一旦发出去就会有人刷你不可能靠事后审计发现问题必须在写点击那一步就把闸门放好。// 同一设备 渠道 60 秒内只记一次点击既防刷量也防日志表被打爆 public boolean tryClick(String deviceId, String channelCode) { String key clk: channelCode : deviceId; Boolean ok redis.opsForValue().setIfAbsent(key, 1, Duration.ofSeconds(60)); return Boolean.TRUE.equals(ok); }限频维度不能只用 IP移动网络下同一个出口 IP 可能对应上千个真实用户用 IP 会误杀设备指纹加渠道的组合更贴近真实场景。60 秒这个窗口是我试过比较平衡的值再短挡不住脚本再长会吃掉正常的重复点击。上线前我固定会把这张表过一遍阈值按自己的业务量调但每一项都必须配处置动作只告警不处置等于没做指标触发阈值处置动作单设备日点击超过 30 次丢弃点击并打标不写绑定单 IP 小时注册超过 20 个注册进人工审核队列佣金延后解冻单渠道日佣金环比超过 300%冻结该渠道当日结算人工核对提现单笔金额超过 5000 元强制人工复核不自动打款同一收款账户多账号提现关联 3 个以上全部挂起走风控审核验证方法比阈值更重要。我一般造三个渠道、每个渠道 100 次点击、20 笔订单其中混入 5 笔退款、3 笔重复回调跑完看四件事绑定表用户数是不是 60、佣金流水是不是 60 条、退款后冲正的是不是 5 条、对账脚本输出是不是空。四个数对上这套链路才算真的通了。最后一个技巧也是我认为最值钱的一条把clk这个点击 ID 贯穿到全链路。落地页带它、绑定表存它、订单备注里写它、佣金流水的扩展字段里留着它日志里每一行都打它。出问题的时候你只要拿到用户投诉的那一条记录就能把点击、注册、下单、结算整条链路串起来看不用在四张表里靠时间戳猜。没有这个 ID排查一次归因问题平均要两小时有了它十分钟。从那以后我每次接这类推广系统源码不管对方说得多完整我都会先用一个真实的clk从落地页走到佣金流水走不通就不谈后面的事。链路能串起来代码才值得读。希望帮到你。本文还有配套的精品资源点击获取

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询