推广系统源码拆解:从渠道追踪到佣金结算的完整链路实现 简介这份PHP推广系统源码面向具备一定PHP与MySQL基础的开发者用于搭建可二次定制的在线营销与广告投放平台解决推广账户管理、广告效果追踪与佣金结算等业务需求。压缩包共14个文件约907KB以php脚本为主体辅以url快捷方式、ec易语言模块、dll动态库、bak备份及txt说明等覆盖配置、注册登录、推广逻辑与工具函数等核心环节。资源已积累1824人学习下载热度可观。系统内置用户管理、广告管理、跟踪代码、报表分析、支付结算与API接口等模块读者可查看并修改源码调整推广算法、接入新广告渠道或集成第三方服务从而构建更贴合自身业务的推广平台。对于希望深入理解推广系统运行机制、练习代码审查与安全编码的开发者这是一份可直接上手研究的实战素材。1. 推广系统源码拆解从渠道追踪到佣金结算一套能跑通的真实项目渠道链接发出去之后用户到底是从哪个入口进来的、注册后该给谁算佣金、结算时怎么防止刷单——这三件事如果靠人工表格维护超过五十个推广员就会乱成一锅粥。我手里这套推广系统源码就是冲着这个场景写的它把渠道标识生成、用户绑定关系、订单归因、佣金计算和提现审核串成了一条完整链路后端用 Python 搭服务前端是常规的后台管理界面数据库用 MySQL 存关系数据。适合两类人看一类是手里有流量但缺一套归属统计工具的开发者另一类是接私活时需要快速交付一套分销后台的人。代码结构不绕核心逻辑集中在几个 service 文件里改起来不用翻遍整个工程。2. 技术栈与目录结构先搞清楚代码往哪放、数据往哪流2.1 后端选型与依赖清单这套系统后端用的是 Python 3 加 Flask 框架没有上 Django 是因为推广系统本身业务不复杂Flask 的轻量反而让路由和中间件更好控制。数据库层用 SQLAlchemy 做 ORM配合 Alembic 管理表结构变更缓存用 Redis 存渠道点击的临时会话和防重放 token。任务队列用 Celery 处理佣金结算和提现审核的异步任务避免结算逻辑卡住主请求。依赖清单大致如下# requirements.txt 核心依赖 Flask2.3.2 SQLAlchemy2.0.19 Flask-SQLAlchemy3.0.5 alembic1.11.1 redis4.6.0 celery5.3.1 PyJWT2.8.0 # 生成渠道追踪 token qrcode7.4.2 # 生成推广二维码 pymysql1.1.0 # MySQL 驱动这里重点说两个依赖的用途。PyJWT 不是用来做用户登录态的而是用来生成渠道追踪 token——每个推广员拿到一个带签名的短链参数用户点击后服务端能验证这个渠道标识没被篡改。qrcode 用来把推广链接转成二维码图片方便线下地推场景。Celery 的 broker 和 backend 都指向 Redis本地开发时直接复用同一个 Redis 实例就行。提示如果本地没有 Redis可以把 Celery 配置里的task_always_eagerTrue打开任务会同步执行方便调试但生产环境必须关掉。2.2 目录结构与模块职责工程目录按功能分层不按技术分层这样找业务代码时不用在 controllers 和 services 之间来回跳promo-system/ ├── app/ │ ├── __init__.py # 应用工厂注册蓝图和扩展 │ ├── models/ │ │ ├── user.py # 用户表、推广员表 │ │ ├── channel.py # 渠道表、点击记录表 │ │ └── commission.py # 佣金流水表、提现记录表 │ ├── services/ │ │ ├── track_service.py # 渠道追踪与归因核心逻辑 │ │ ├── commission_service.py # 佣金计算与结算 │ │ └── withdraw_service.py # 提现审核流程 │ ├── api/ │ │ ├── track.py # 对外追踪接口 │ │ ├── admin.py # 后台管理接口 │ │ └── callback.py # 支付回调与订单归因 │ └── utils/ │ ├── token_helper.py # JWT 签发与校验 │ └── redis_lock.py # 防重复结算的分布式锁 ├── migrations/ # Alembic 迁移脚本 ├── config.py # 多环境配置 └── manage.py # 启动入口与 CLI 命令track_service.py是整个系统的入口逻辑用户点击推广链接时先经过这里。commission_service.py负责在订单支付成功后计算该给哪个推广员多少钱。withdraw_service.py处理推广员发起提现后的审核状态流转。三个 service 之间通过数据库外键关联不直接互相调用降低耦合。2.3 数据表关系与关键字段核心表有四张promoters推广员、channels渠道、click_logs点击记录、commission_records佣金流水。关系是一个推广员可以创建多个渠道一个渠道对应多条点击记录一条支付成功的订单生成一条佣金流水。关键字段说明表名字段类型说明channelschannel_codevarchar(32)渠道唯一标识用于短链参数channelspromoter_idint所属推广员 IDclick_logsvisitor_fingerprintvarchar(64)访客指纹用于防刷click_logsclick_timedatetime点击时间归因时按时间窗口匹配commission_recordsorder_novarchar(64)关联订单号唯一索引防重复commission_recordsamountdecimal(10,2)佣金金额commission_recordsstatustinyint0待结算 1已结算 2已失效order_no上建唯一索引是防重复结算的第一道防线后面在代码层还会加 Redis 锁做第二道。visitor_fingerprint用 IP 加 User-Agent 的哈希生成不存原始值避免隐私问题。3. 核心链路拆解一个点击从进入到佣金入账的完整过程3.1 渠道追踪与用户绑定用户点击推广链接时请求先打到/api/track/click服务端从 URL 参数里取出channel_code和token校验 token 签名后记录点击日志同时把渠道标识写入 Rediskey 是访客指纹过期时间设 24 小时。这样用户后续注册时即使没带参数也能通过指纹找回渠道来源。# app/services/track_service.py import hashlib from datetime import datetime, timedelta from app.utils.token_helper import verify_token from app.models.channel import ClickLog from app.extensions import redis_client, db def handle_click(channel_code, token, request): # 1. 校验 token 合法性防止渠道参数被伪造 if not verify_token(token, channel_code): return {code: 4001, msg: invalid token} # 2. 生成访客指纹不存原始 IP raw f{request.remote_addr}:{request.user_agent.string} fingerprint hashlib.sha256(raw.encode()).hexdigest()[:32] # 3. 写入点击日志 log ClickLog( channel_codechannel_code, visitor_fingerprintfingerprint, click_timedatetime.utcnow() ) db.session.add(log) db.session.commit() # 4. 渠道标识写入 Redis24 小时内注册可归因 redis_client.setex( ftrack:{fingerprint}, timedelta(hours24), channel_code ) return {code: 0, msg: ok}这段代码的关键点在第二步和第四步。指纹用 SHA256 截断到 32 位既保证唯一性又不会太长。Redis 的过期时间设 24 小时是常见做法太短会导致用户犹豫后注册丢失归因太长会让归因窗口过于宽松。verify_token内部用 JWT 的 HS256 算法密钥存在环境变量里不硬编码。3.2 订单归因与佣金计算用户注册后下单支付支付回调打到/api/callback/pay这里要做两件事找到该用户对应的渠道然后根据订单金额和渠道佣金比例算出佣金。归因逻辑是先查用户注册时绑定的渠道如果注册时没绑定再用支付时的访客指纹去 Redis 里查。# app/services/commission_service.py from decimal import Decimal from app.models.user import User from app.models.channel import Channel from app.models.commission import CommissionRecord from app.extensions import redis_client, db def calc_commission(user_id, order_no, order_amount): user User.query.get(user_id) channel_code user.bind_channel # 注册时没绑定渠道尝试从 Redis 指纹找回 if not channel_code: fingerprint user.register_fingerprint channel_code redis_client.get(ftrack:{fingerprint}) if not channel_code: return None # 无归因不产生佣金 channel Channel.query.filter_by(channel_codechannel_code).first() if not channel: return None # 佣金比例存在渠道表里支持不同渠道不同比例 rate Decimal(str(channel.commission_rate)) amount Decimal(str(order_amount)) * rate # 保留两位小数向下取整避免超额 amount amount.quantize(Decimal(0.01)) record CommissionRecord( promoter_idchannel.promoter_id, order_noorder_no, amountamount, status0 # 待结算 ) db.session.add(record) db.session.commit() return recordcommission_rate字段用 decimal 类型存比如 0.05 表示 5%。金额计算全程用 Decimal不用 float这是血泪经验——float 在多次累加后会出现 0.10.2 不等于 0.3 的玄学问题结算时对不上账。order_no的唯一索引会在插入重复订单时抛异常外层捕获后直接返回避免重复计算。3.3 佣金结算与提现审核佣金不是支付后立刻可提现通常要等订单过了退款期才结算。系统用一个 Celery 定时任务每天凌晨跑一次把status0且订单创建时间超过 7 天的记录改成status1。结算时加 Redis 分布式锁防止多实例部署时重复执行。# app/services/commission_service.py from app.utils.redis_lock import acquire_lock def settle_commission(): lock acquire_lock(settle_commission, timeout300) if not lock: return # 已有实例在执行直接跳过 try: # 找出 7 天前创建且待结算的记录 deadline datetime.utcnow() - timedelta(days7) records CommissionRecord.query.filter( CommissionRecord.status 0, CommissionRecord.created_at deadline ).all() for r in records: r.status 1 db.session.commit() finally: lock.release()提现流程是推广员在后台发起申请withdraw_service.py里生成提现单状态为待审核。管理员审核通过后调用支付渠道的转账接口打款成功后把提现单状态改成已完成同时扣减推广员余额。余额字段用乐观锁版本号控制防止并发提现导致余额扣成负数。注意提现审核接口一定要加权限校验普通推广员只能查自己的提现记录不能查别人的。常见做法是在 JWT 里带promoter_id查询时强制拼上这个条件。4. 本地跑起来从建库到看到第一条佣金记录4.1 环境准备与配置修改先把代码拉下来建虚拟环境装依赖python3 -m venv venv source venv/bin/activate pip install -r requirements.txt然后改config.py里的数据库和 Redis 连接。本地开发用 SQLite 也能跑但涉及并发结算时建议还是上 MySQLSQLite 的写锁在 Celery 多任务下容易卡住。# config.py 开发环境配置片段 class DevConfig: SQLALCHEMY_DATABASE_URI mysqlpymysql://root:password127.0.0.1:3306/promo_dev REDIS_URL redis://127.0.0.1:6379/0 JWT_SECRET dev-secret-change-in-production CELERY_BROKER_URL REDIS_URL CELERY_RESULT_BACKEND REDIS_URLJWT_SECRET生产环境必须换掉而且不能提交到代码仓库。常见做法是读环境变量本地开发时用一个默认值兜底。4.2 初始化数据库与启动服务建库和跑迁移mysql -u root -p -e CREATE DATABASE promo_dev DEFAULT CHARSET utf8mb4; alembic upgrade head然后启动 Flask 和 Celery# 终端 1启动 web 服务 python manage.py runserver --port 5000 # 终端 2启动 Celery worker celery -A app.celery_app worker --loglevelinfo # 终端 3启动 Celery beat 定时任务 celery -A app.celery_app beat --loglevelinfo三个终端缺一不可。只启动 web 服务的话佣金结算任务不会自动跑需要手动调接口触发。Celery beat 负责按计划发任务worker 负责执行分开部署时 beat 只需要一个实例。4.3 模拟一条完整链路验证服务起来后用 curl 模拟一次点击和支付回调# 1. 模拟点击推广链接 curl http://127.0.0.1:5000/api/track/click?channel_codeCH001tokenxxx # 2. 模拟用户注册省略参数细节 curl -X POST http://127.0.0.1:5000/api/user/register \ -d usernametestuserfingerprint上一步返回的指纹 # 3. 模拟支付回调 curl -X POST http://127.0.0.1:5000/api/callback/pay \ -d user_id1order_noORD20240101amount100.00第三步执行完后去数据库查commission_records表应该能看到一条amount5.00假设佣金比例 5%、status0的记录。如果查不到先看 Celery worker 的日志有没有报错再检查 Redis 里track:开头的 key 是否存在。5. 避坑与排查那些让我加班到凌晨的坑5.1 归因丢失用户注册时渠道标识查不到现象是用户明明点了推广链接注册后却没绑定渠道佣金算给了别人或者干脆没算。原因通常是 Redis 里的track:key 过期了或者用户注册时用的设备和点击时不是同一台指纹对不上。解决方法是把归因窗口从 24 小时延长到 72 小时同时在注册接口里增加一个兜底逻辑如果指纹查不到尝试用手机号或邮箱去点击日志里反查最近一次记录。5.2 佣金重复计算同一订单生成两条流水现象是支付回调被重复触发时commission_records表里出现两条相同order_no的记录。原因是回调接口没有做幂等支付渠道在超时后会重试。解决方法是在order_no上建唯一索引插入时捕获IntegrityError直接返回成功同时在 Redis 里用order_no做一次 setnx 预占位。5.3 提现余额扣成负数现象是推广员并发发起两笔提现余额只够一笔但两笔都审核通过了。原因是余额扣减没有加锁两个请求同时读到相同余额。解决方法是用 SQL 的UPDATE ... SET balance balance - amount WHERE balance amount原子操作根据 affected rows 判断是否扣减成功而不是先查再改。5.4 Celery 任务不执行定时结算没跑现象是 Celery beat 启动了但 worker 没收到任务。常见原因是 beat 和 worker 用的 broker 不是同一个 Redis 库或者时区配置不对导致任务被排到了错误的时间。检查celery_app.conf.timezone是否设成了Asia/Shanghai以及 beat 的 schedule 文件是否被正确加载。5.5 二维码生成中文乱码现象是推广二维码里的渠道名称显示成方块。原因是 qrcode 库默认不支持中文需要指定encodingutf-8并确保字体文件存在。解决方法是生成二维码时传入qrcode.QRCode(versionNone, error_correctionqrcode.constants.ERROR_CORRECT_L, box_size10, border4)然后qr.add_data(data.encode(utf-8))。6. 二次开发改佣金规则和接支付渠道的实操思路这套系统最常被改的两个地方是佣金计算规则和支付回调对接。佣金规则现在写死在calc_commission里按渠道比例一刀切。如果要支持阶梯佣金——比如月推广额超过一万后比例上浮——可以在commission_service.py里加一个get_rate_by_promoter函数先查推广员当月累计订单金额再返回对应档位的比例。# 阶梯佣金示例按当月累计金额动态取比例 def get_rate_by_promoter(promoter_id, channel_rate): month_start datetime.utcnow().replace(day1, hour0, minute0, second0) total db.session.query( func.sum(CommissionRecord.amount) ).filter( CommissionRecord.promoter_id promoter_id, CommissionRecord.created_at month_start, CommissionRecord.status ! 2 ).scalar() or Decimal(0) if total Decimal(10000): return channel_rate Decimal(0.02) # 上浮 2 个点 elif total Decimal(5000): return channel_rate Decimal(0.01) return channel_rate改完之后记得在calc_commission里把rate channel.commission_rate替换成调用这个函数。验证方法是手动往commission_records插几条不同金额的记录然后调支付回调接口看生成的佣金金额是否符合阶梯预期。接支付渠道时重点改callback.py里的验签逻辑。不同支付平台的签名算法不一样常见做法是把验签部分抽成一个独立函数用策略模式按pay_channel字段分发。验签通过后再调calc_commission不通过直接返回失败不要往下走。我自己的习惯是每次改完佣金规则都强制走一遍「点击→注册→支付→结算→提现」的完整链路用测试数据跑一遍确认金额对得上再提交代码。有一次偷懒只测了支付回调没测结算结果上线后发现阶梯比例在结算时没生效又回滚了一版。从那以后我每次改佣金逻辑都强制走一遍全链路验证不省这一步。希望帮到你。本文还有配套的精品资源点击获取