简介:这份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 核心依赖 Flask==2.3.2 SQLAlchemy==2.0.19 Flask-SQLAlchemy==3.0.5 alembic==1.11.1 redis==4.6.0 celery==5.3.1 PyJWT==2.8.0 # 生成渠道追踪 token qrcode==7.4.2 # 生成推广二维码 pymysql==1.1.0 # MySQL 驱动这里重点说两个依赖的用途。PyJWT 不是用来做用户登录态的,而是用来生成渠道追踪 token——每个推广员拿到一个带签名的短链参数,用户点击后服务端能验证这个渠道标识没被篡改。qrcode 用来把推广链接转成二维码图片,方便线下地推场景。Celery 的 broker 和 backend 都指向 Redis,本地开发时直接复用同一个 Redis 实例就行。
提示:如果本地没有 Redis,可以把 Celery 配置里的
task_always_eager=True打开,任务会同步执行,方便调试,但生产环境必须关掉。
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(佣金流水)。关系是:一个推广员可以创建多个渠道,一个渠道对应多条点击记录,一条支付成功的订单生成一条佣金流水。
关键字段说明:
| 表名 | 字段 | 类型 | 说明 |
|---|---|---|---|
| channels | channel_code | varchar(32) | 渠道唯一标识,用于短链参数 |
| channels | promoter_id | int | 所属推广员 ID |
| click_logs | visitor_fingerprint | varchar(64) | 访客指纹,用于防刷 |
| click_logs | click_time | datetime | 点击时间,归因时按时间窗口匹配 |
| commission_records | order_no | varchar(64) | 关联订单号,唯一索引防重复 |
| commission_records | amount | decimal(10,2) | 佣金金额 |
| commission_records | status | tinyint | 0待结算 1已结算 2已失效 |
order_no上建唯一索引是防重复结算的第一道防线,后面在代码层还会加 Redis 锁做第二道。visitor_fingerprint用 IP 加 User-Agent 的哈希生成,不存原始值,避免隐私问题。
3. 核心链路拆解:一个点击从进入到佣金入账的完整过程
3.1 渠道追踪与用户绑定
用户点击推广链接时,请求先打到/api/track/click,服务端从 URL 参数里取出channel_code和token,校验 token 签名后记录点击日志,同时把渠道标识写入 Redis,key 是访客指纹,过期时间设 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_code=channel_code, visitor_fingerprint=fingerprint, click_time=datetime.utcnow() ) db.session.add(log) db.session.commit() # 4. 渠道标识写入 Redis,24 小时内注册可归因 redis_client.setex( f"track:{fingerprint}", timedelta(hours=24), 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(f"track:{fingerprint}") if not channel_code: return None # 无归因,不产生佣金 channel = Channel.query.filter_by(channel_code=channel_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_id=channel.promoter_id, order_no=order_no, amount=amount, status=0 # 待结算 ) db.session.add(record) db.session.commit() return recordcommission_rate字段用 decimal 类型存,比如 0.05 表示 5%。金额计算全程用 Decimal,不用 float,这是血泪经验——float 在多次累加后会出现 0.1+0.2 不等于 0.3 的玄学问题,结算时对不上账。order_no的唯一索引会在插入重复订单时抛异常,外层捕获后直接返回,避免重复计算。
3.3 佣金结算与提现审核
佣金不是支付后立刻可提现,通常要等订单过了退款期才结算。系统用一个 Celery 定时任务每天凌晨跑一次,把status=0且订单创建时间超过 7 天的记录改成status=1。结算时加 Redis 分布式锁,防止多实例部署时重复执行。
# app/services/commission_service.py from app.utils.redis_lock import acquire_lock def settle_commission(): lock = acquire_lock("settle_commission", timeout=300) if not lock: return # 已有实例在执行,直接跳过 try: # 找出 7 天前创建且待结算的记录 deadline = datetime.utcnow() - timedelta(days=7) 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 也能跑,但涉及并发结算时建议还是上 MySQL,SQLite 的写锁在 Celery 多任务下容易卡住。
# config.py 开发环境配置片段 class DevConfig: SQLALCHEMY_DATABASE_URI = "mysql+pymysql://root:password@127.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 --loglevel=info # 终端 3:启动 Celery beat 定时任务 celery -A app.celery_app beat --loglevel=info三个终端缺一不可。只启动 web 服务的话,佣金结算任务不会自动跑,需要手动调接口触发。Celery beat 负责按计划发任务,worker 负责执行,分开部署时 beat 只需要一个实例。
4.3 模拟一条完整链路验证
服务起来后,用 curl 模拟一次点击和支付回调:
# 1. 模拟点击推广链接 curl "http://127.0.0.1:5000/api/track/click?channel_code=CH001&token=xxx" # 2. 模拟用户注册(省略参数细节) curl -X POST "http://127.0.0.1:5000/api/user/register" \ -d "username=testuser&fingerprint=上一步返回的指纹" # 3. 模拟支付回调 curl -X POST "http://127.0.0.1:5000/api/callback/pay" \ -d "user_id=1&order_no=ORD20240101&amount=100.00"第三步执行完后,去数据库查commission_records表,应该能看到一条amount=5.00(假设佣金比例 5%)、status=0的记录。如果查不到,先看 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 库默认不支持中文,需要指定encoding='utf-8'并确保字体文件存在。解决方法是生成二维码时传入qrcode.QRCode(version=None, error_correction=qrcode.constants.ERROR_CORRECT_L, box_size=10, border=4),然后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(day=1, hour=0, minute=0, second=0) 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,不通过直接返回失败,不要往下走。
我自己的习惯是每次改完佣金规则,都强制走一遍「点击→注册→支付→结算→提现」的完整链路,用测试数据跑一遍,确认金额对得上再提交代码。有一次偷懒只测了支付回调没测结算,结果上线后发现阶梯比例在结算时没生效,又回滚了一版。从那以后我每次改佣金逻辑都强制走一遍全链路验证,不省这一步。希望帮到你。
本文还有配套的精品资源,点击获取