☰
推广系统源码实战:绑定归因、佣金结算与二次开发避坑
2026/11/5 2:10:49 网站建设 项目流程

简介:这套基于PHP开发的推广系统源码面向具备一定PHP与Web开发基础、希望自建或二次开发推广平台的开发者。代码围绕用户与推广者管理、广告投放、点击与转化跟踪、报表分析、支付结算及API对接等模块展开,可用于搭建自动化营销与效果监测系统,也适合作为理解推广系统业务链路和佣金结算逻辑的参考实现。压缩包共14个文件,约907KB,主要由6个PHP程序文件、3个url快捷方式、1个ec模块、1个bak备份、1个dll组件、1个e文件及1个txt说明组成,覆盖业务代码、配置备份、运行依赖与使用提示,规模精简便于快速浏览和本地调试。目前已有1826人学习下载。读者可据此查看推广账户体系、广告管理、跟踪代码与统计报表的实现思路,并在此基础上调整推广算法、增加广告渠道或集成第三方服务;但正式使用前仍需做好代码审查、安全编码、错误处理与隐私合规检查,以降低源码落地风险。

1. 推广系统源码能解决什么:从一张归不了账的二维码说起

前段时间帮一个做本地生活的朋友捋数据,他手底下二十多个推广员,每人一张专属二维码,月底对账时发现有接近三成订单归不到人,钱只能按人头平摊,第二天就有人撂挑子。他手里那份推广系统源码是买来的,能跑,但链路是断的——扫码进店、下单支付、佣金入账这三段各走各的。这类系统真正要解决的只有一件事:把「这个用户是谁带来的」从第一次点击一路记到佣金到账,中间任一环断了,后面全是糊涂账。这套源码适合已经跑出一点单量、需要自己掌控归因规则和分佣比例的团队,也适合想拿一套结构完整、接口清晰的后台做二次开发的人,对着目录改比重写便宜太多。

2. 技术栈与目录结构:先看清这套代码的骨架

拿到一份不熟悉的源码,我的习惯是先不急着启动,花二十分钟把pom.xml、目录树和建表 SQL 过一遍。原因很实际:推广类系统的坑大多不在业务逻辑写得对不对,而在表和表之间的关联关系有没有设计好,关系设计歪了,后面怎么改代码都是在补窟窿。

2.1 技术选型:为什么是这套组合

这套代码走的是国内中小型业务系统最常见的那条路线,后端采用 Spring Boot 2.7 + MyBatis-Plus,数据库 MySQL 8.0,缓存与分布式锁用 Redis,鉴权走 JWT + 拦截器,前端是 Vue2 + Element UI 的经典后台模板,二维码生成用 ZXing,报表导出用 EasyExcel。这个组合谈不上先进,但胜在生态成熟、招人好招、出问题搜得到答案,对一个日订单量在几千到几万量级的推广系统来说完全够用。

值得说的是几个容易被忽略的依赖。Redis 在这套系统里不只是缓存,它还承担了三件事:邀请码到推广员 ID 的映射缓存、扫码点击的防刷计数、以及佣金结算任务的分布式锁。如果你的部署环境只有单机,锁那部分可以退化成本地锁,但建议还是把 Redis 装上,因为点击去重这块没它很难做干净。

MyBatis-Plus 的使用方式是代码里比较省事的一点,绝大部分单表查询直接走BaseMapper,只有佣金汇总、渠道报表这类多表统计才手写 XML。如果你后续想换成 JPA 或者 MyBatis 原生,改动量集中在mapper目录和几个 XML 文件上,Service 层基本不用动。

前端部分用的是 Vue2,不是 Vue3。这点要有心理准备:如果团队已经全面转向 Vue3 + Vite,迁移成本主要在路由和权限指令上,页面组件大部分能直接复用。后台模板自带的那套按钮级权限指令,在推广系统里正好用来区分「运营」「财务」「渠道管理员」三种角色,财务只能看提现审核,运营只能看数据和素材。

2.2 目录结构:从 controller 到 mapper 的一条线

源码解开后是个标准的 Maven 多模块结构,后端主模块下按业务域分包。我把关键路径画出来,你对着看会快很多:

promotion-server/ ├── pom.xml ├── sql/ │ ├── schema.sql # 建表脚本,先跑这个 │ └── init_data.sql # 初始管理员、测试推广员 ├── src/main/java/com/demo/promotion/ │ ├── PromotionApplication.java │ ├── config/ # 拦截器、跨域、Redis、线程池 │ │ ├── WebMvcConfig.java │ │ └── RedisConfig.java │ ├── controller/ │ │ ├── PromoterController.java # 推广员管理 │ │ ├── LinkController.java # 推广链接与二维码 │ │ ├── OrderController.java # 订单与归因 │ │ ├── CommissionController.java # 佣金查询 │ │ └── WithdrawController.java # 提现审核 │ ├── service/impl/ │ │ ├── RelationServiceImpl.java # 绑定关系(核心) │ │ ├── AttributionServiceImpl.java # 订单归因(核心) │ │ └── CommissionServiceImpl.java # 佣金计算与结算 │ ├── mapper/ # 与 resources/mapper 下 XML 对应 │ ├── entity/ # 与表一一对应 │ ├── task/ │ │ └── SettleTask.java # 佣金解冻定时任务 │ └── common/ │ ├── Result.java # 统一返回体 │ └── BizException.java └── src/main/resources/ ├── application.yml ├── application-dev.yml └── mapper/*.xml

前端在另一个仓库promotion-admin,src/views下按同样业务域分目录,src/api里每个模块一个 js 文件对应后端接口。这种「前后端模块名对齐」的约定看着朴素,实际改需求时非常省事——后端加一个接口,前端能立刻猜到文件该放哪。

需要留意的是service/impl下那三个类。推广系统的全部复杂度基本都压在RelationServiceImpl、AttributionServiceImpl、CommissionServiceImpl里,其余 Controller 大多是薄薄一层参数校验加转发。读代码时按「绑定 → 归因 → 结算」这个顺序读,比从头到尾翻文件效率高得多。

2.3 数据库表:八张表撑起整个推广链路

表结构我整理成一张清单,建表脚本里字段名可能略有出入,但关系不会变:

表名作用关键字段
sys_user平台用户id、phone、parent_id
promoter推广员档案id、user_id、invite_code、level、status
promoter_relation绑定关系(核心)user_id、promoter_id、bind_type、expire_time
promotion_link推广链接/渠道码id、promoter_id、channel、code
click_log点击日志code、ip、ua、create_time
biz_order业务订单order_no、amount、promoter_id、status
commission_record佣金流水order_no、promoter_id、level、amount、state
withdraw_apply提现申请promoter_id、amount、audit_state

promoter_relation是整套系统的地基。它记录「用户 A 是被推广员 B 拉来的」这条事实,只增不改,重新绑定走新记录 + 旧记录失效,不做物理更新。这个设计看起来有点浪费,但换来的是归因可追溯——用户投诉「这个单子明明是我带的」时,你能把历史绑定记录全拉出来对。

commission_record里state字段是状态机的关键,通常定义为 0 待结算、1 已结算、2 已失效。订单支付成功写 0,售后期结束转 1,中途退款转 2。所有对推广员余额的加减,都必须由这张表驱动,绝不允许在业务代码里直接改余额。这条规矩如果一开始没立住,后面查账会非常痛苦。

3. 核心链路拆解:从一条邀请链接到一笔佣金入账

第二章把骨架看清楚了,这一章进到肉里。推广系统的业务链路其实很短,但每一段都有细节,我按时间顺序拆成四段:生成邀请码、绑定关系、订单归因、佣金结算,最后补一段提现。

3.1 邀请码生成与关系绑定

邀请码要满足两个互相矛盾的要求:短到用户愿意手输,又不能撞。常见做法是用自增 ID 转 36 进制再补一位校验位,长度控制在 6 位左右,同时排除容易看错的字符。

@Service public class InviteCodeService { /** 去掉 0/O/1/I/L 这些手输时容易混的字符,降低客服成本 */ private static final char[] ALPHABET = "23456789ABCDEFGHJKMNPQRSTUVWXYZ".toCharArray(); /** * 用推广员 ID 生成 6 位邀请码 * @param promoterId 推广员主键,必须大于 0 */ public String encode(Long promoterId) { if (promoterId == null || promoterId <= 0) { throw new BizException("推广员ID非法"); } long num = promoterId; StringBuilder sb = new StringBuilder(); // 先做 32 进制展开,位置够用后补到 6 位 while (num > 0) { sb.append(ALPHABET[(int) (num % ALPHABET.length)]); num /= ALPHABET.length; } while (sb.length() < 6) { sb.append(ALPHABET[0]); } // 倒序,让低位变化体现在末尾,生成的码看起来更“连续” return sb.reverse().toString(); } }

这段代码的逻辑很简单,但有两个点值得说。一是ALPHABET的长度是 32 而不是 36,去掉的四个字符正是手输最容易出错的;二是while (sb.length() < 6)补位用的是数组第 0 位而不是空格或 0,保证码长固定,前端输入框可以直接限制 6 位。参数上唯一需要注意的是promoterId必须为正数,如果系统里有逻辑删除后复用 ID 的设计,这里要改成用独立的雪花 ID,否则会出现「老码指向新人」的情况。

有了邀请码,绑定关系的写入是推广系统的第一个关键动作:

@Transactional(rollbackFor = Exception.class) public void bind(Long userId, String inviteCode, String bindType) { Promoter promoter = promoterMapper.selectByInviteCode(inviteCode); if (promoter == null || promoter.getStatus() != 1) { // 渠道不存在或已停用,静默返回,不打断用户下单 return; } // 不能自己绑自己 if (promoter.getUserId().equals(userId)) { return; } PromoterRelation exist = relationMapper.selectEffective(userId); if (exist != null) { // 已有有效关系时是否允许覆盖,取决于业务策略,默认首次归因不覆盖 return; } PromoterRelation relation = new PromoterRelation(); relation.setUserId(userId); relation.setPromoterId(promoter.getId()); relation.setBindType(bindType); // 1 链接 2 二维码 3 手动录入 relation.setExpireTime(LocalDateTime.now().plusDays(30)); relationMapper.insert(relation); // 写缓存,归因时优先读缓存,减少一次查库 redisTemplate.opsForValue().set("bind:" + userId, promoter.getId(), 30, TimeUnit.DAYS); }

bindType这个字段在排查问题时价值很高。渠道方经常说「我的人是扫码来的」,后台一查bindType=1,说明是链接跳转进来的,多半是二维码被转发过,责任就清楚了。expire_time设 30 天是行业里比较常见的选择,太长会让「用户三个月后自己搜进来下单」也归给推广员,渠道方和平台方都容易有意见;太短又会让推广员觉得白干。这个值建议做成配置项,别写死在代码里。

3.2 订单归因:这单到底算谁的

归因是整个系统最容易翻车的地方。血泪经验是:归因逻辑必须做成幂等的、可重放的,因为订单回调会被重复推送,支付渠道重试是常态。

@Override public void attribution(BizOrder order) { // 已经归因过就直接返回,保证幂等 if (order.getPromoterId() != null && order.getPromoterId() > 0) { return; } Long promoterId = null; // 1. 优先看订单上有没有显式带渠道参数(分享下单场景) if (StringUtils.isNotBlank(order.getChannelCode())) { promoterId = linkService.getPromoterIdByCode(order.getChannelCode()); } // 2. 其次看用户绑定关系缓存 if (promoterId == null) { Object cache = redisTemplate.opsForValue().get("bind:" + order.getUserId()); if (cache != null) { promoterId = Long.valueOf(cache.toString()); } } // 3. 缓存穿透则查库,注意只查未过期且有效的关系 if (promoterId == null) { PromoterRelation relation = relationMapper.selectEffective(order.getUserId()); if (relation != null) { promoterId = relation.getPromoterId(); } } // 4. 兜底:按平台默认归属,通常为空,表示自然流量 if (promoterId == null) { return; } // 用带条件的更新,避免并发下被其他线程覆盖 int rows = orderMapper.updatePromoterId(order.getId(), promoterId); if (rows == 0) { log.warn("归因更新失败,可能已被并发处理, orderNo={}", order.getOrderNo()); } }

这段的三层兜底对应三种真实场景:用户点了分享链接直接下单、用户之前被绑过关系后来自己下单、用户关系只在库里没进缓存。优先级顺序不能反,反了会出现「老关系压掉新分享」的投诉。

updatePromoterId对应的 SQL 必须带and promoter_id is null这样的条件,靠数据库的行锁保证只写一次:

UPDATE biz_order SET promoter_id = #{promoterId}, attr_time = NOW() WHERE id = #{id} AND (promoter_id IS NULL OR promoter_id = 0);

这里返回受影响行数为 0 就说明被别人抢先了,正常打日志即可,不用抛异常。归因失败不应该阻塞订单主流程,这是原则。

3.3 佣金计算与状态机:钱怎么发、什么时候发

佣金计算我见过最省事的写法是直接amount * rate,用 double。这个写法在小数位上迟早出问题,比如 199.9 的 10%,用 double 算出来是 19.989999999999998,四舍五入到分是 19.99,但推广员自己拿计算器算是 19.99,再遇上分佣三人平摊,误差就会露出来。所以金额一律 BigDecimal,比例一律用整数万分比存储。

public List<CommissionItem> calc(BigDecimal payAmount, List<Long> promoterChain) { // 各层级比例,万分比:1000=10%,500=5%,200=2% int[] rates = {1000, 500, 200}; List<CommissionItem> items = new ArrayList<>(); BigDecimal remain = payAmount; for (int i = 0; i < promoterChain.size() && i < rates.length; i++) { BigDecimal amount = payAmount .multiply(BigDecimal.valueOf(rates[i])) .divide(BigDecimal.valueOf(10000), 2, RoundingMode.DOWN); // 防御:多级佣金总额不能超过订单金额,否则平台倒贴 if (amount.compareTo(remain) > 0) { amount = remain; } if (amount.compareTo(BigDecimal.ZERO) <= 0) { continue; } remain = remain.subtract(amount); CommissionItem item = new CommissionItem(); item.setPromoterId(promoterChain.get(i)); item.setLevel(i + 1); item.setAmount(amount); items.add(item); } return items; }

几个参数要说明白。rates数组按层级顺序对应,索引 0 是一级。RoundingMode.DOWN是向下取整,宁可少给一分也不多给,多给的那部分在对账时会被财务揪出来。remain是防超发的兜底,正常比例配置下用不到,但一旦运营把三级比例配成 30%+20%+10% 而订单还有退款,没有这层保护就会出现负余额。

佣金落库之后进入状态机。支付成功写 0(待结算),这个状态下钱不进推广员余额,只记流水;售后期结束(通常是签收后 7 天)由定时任务批量转 1(已结算),同时累加余额;期间发生退款则转 2(已失效),流水作废。

@Scheduled(cron = "0 30 2 * * ?") public void settle() { // 加分布式锁,多实例部署时只允许一个节点跑 String lockKey = "lock:commission:settle"; Boolean ok = redisTemplate.opsForValue() .setIfAbsent(lockKey, "1", 30, TimeUnit.MINUTES); if (!Boolean.TRUE.equals(ok)) { return; } try { // 每次只捞 500 条,避免大事务把库压住 List<CommissionRecord> list = commissionMapper.selectSettleAble(500); for (CommissionRecord r : list) { commissionMapper.updateState(r.getId(), 1); accountMapper.incrBalance(r.getPromoterId(), r.getAmount()); } } finally { redisTemplate.delete(lockKey); } }

定时任务这里有个细节要注意:updateState和incrBalance必须在同一个事务里,而且updateState要带上state = 0的条件,防止任务重跑时重复加钱。锁的过期时间给 30 分钟是估的,实际按你单批处理耗时乘以 3 来定。

3.4 提现与对账:余额扣减不能只靠代码

提现流程是申请、冻结、审核、打款、回调。这里唯一的硬要求是扣减余额必须用带条件的 UPDATE,靠数据库保证不会扣成负数:

UPDATE promoter_account SET balance = balance - #{amount}, frozen = frozen + #{amount}, version = version + 1 WHERE promoter_id = #{promoterId} AND balance >= #{amount} AND version = #{version};

受影响行数为 0 就抛业务异常,提示「余额不足或账户变动,请刷新重试」。用乐观锁而不是select ... for update,是因为提现申请量通常不大,乐观锁冲突率低,还能避免长事务占着行锁影响结算任务。

对账这块源码里给的是一个按日汇总的接口,把commission_record按promoter_id和日期分组,跟提现记录对。我建议再加一层:每天把平台侧支付渠道的对账单拉下来,跟biz_order核一遍,差异单单独标出来。这部分源码里只留了接口壳子,需要按你自己接的支付渠道补实现。

4. 把项目跑起来:环境准备、配置与冒烟验证

这一章是纯操作。我按自己实际跑通的顺序写,你照着做基本不会卡。

4.1 环境与依赖版本

需要的环境不多:JDK 8 或 11(源码里pom.xml的maven.compiler.source写的是 1.8,用 11 也能跑)、Maven 3.6+、MySQL 8.0、Redis 5+、Node 14 或 16。版本别乱升,Vue2 的这套后台模板在 Node 18 上大概率报ERR_OSSL_EVP_UNSUPPORTED,遇到就在启动命令里加NODE_OPTIONS=--openssl-legacy-provider。

MySQL 建库时字符集用utf8mb4,排序规则utf8mb4_general_ci。这个不是可选项,推广素材里经常有表情符号,utf8存不进去会直接报错。

4.2 配置文件要改的地方

application-dev.yml里真正需要你动的只有这几项,其余保持默认:

server: port: 8080 spring: datasource: url: jdbc:mysql://127.0.0.1:3306/promotion?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: 你的密码 redis: host: 127.0.0.1 port: 6379 database: 0 promotion: # 绑定关系有效期(天),运营可调 bind-expire-days: 30 # 佣金结算延迟天数,对应售后期 settle-delay-days: 7 # 分佣比例,万分比,逗号分隔,最多三级 commission-rates: 1000,500,200 # 推广落地页域名,生成二维码时拼接 landing-domain: http://127.0.0.1:8081

landing-domain是最容易忘的一项。忘了改,生成的二维码全指向示例域名,扫码进去是 404,你会以为是二维码生成坏了,实际是配置没动。commission-rates的个数决定实际生效的分佣层级,配两个就是两级分销,这点在二次开发时很有用。

4.3 启动与冒烟验证

建表、导数据、起服务、起前端,四步:

# 1. 建库并导入表结构与初始数据 mysql -uroot -p -e "CREATE DATABASE promotion DEFAULT CHARSET utf8mb4;" mysql -uroot -p promotion < sql/schema.sql mysql -uroot -p promotion < sql/init_data.sql # 2. 后端启动(首次会自动下载依赖,耐心等) mvn clean package -DskipTests java -jar target/promotion-server.jar --spring.profiles.active=dev # 3. 前端启动 cd promotion-admin npm install --registry=https://registry.npmmirror.com npm run dev

起来之后别急着点页面,先跑一遍接口冒烟,能最快定位是环境问题还是代码问题:

# 登录拿 token,初始账号在 init_data.sql 里 curl -X POST http://127.0.0.1:8080/api/login \ -H "Content-Type: application/json" \ -d '{"username":"admin","password":"123456"}' # 用返回的 token 查推广员列表,能出数据说明库连对了 curl http://127.0.0.1:8080/api/promoter/page?pageNum=1&pageSize=10 \ -H "Authorization: Bearer 上一步的token"

第一个接口返回 401 通常是拦截器把登录接口也拦了,去看WebMvcConfig里的excludePathPatterns有没有漏配/api/login。第二个接口返回空数组但状态码 200,说明库连上了但init_data.sql没导进去。这两个现象区分开了,后面排查就快了。

冒烟通过后再走一遍完整链路:后台建一个推广员、生成二维码、用手机浏览器扫(同一局域网改landing-domain为你的内网 IP)、下单、去佣金流水里看有没有一条state=0的记录。这条走通了,整套系统就算真正跑起来了。

提示:init_data.sql里的初始密码是明文示例,正式环境第一件事就是改掉,并且把sys_user表里的密码字段改成 BCrypt 存储。

5. 避坑与排查:这几个问题我踩过

下面四条都是我实际遇到过或者帮别人定位过的,按「现象 → 原因 → 解决」写,遇到类似症状可以直接对。

5.1 绑定关系突然丢失,推广员说「我的人跑了」

现象是推广员后台显示绑定人数从几百掉到几十,但订单量没明显变化。

原因是promoter_relation表在绑定新关系时用了update覆盖同一条记录,而某些渠道场景下用户会被重复绑定,旧记录的promoter_id被改掉,导致历史推广员的人数统计凭空消失。

解决办法是绑定关系只增不改:新绑定插新记录,旧记录改status=0加失效时间,统计时按status=1过滤。如果已经出过这个事故,从click_log和订单表反推能补回大部分关系,前提是点击日志没被清理——所以点击日志的保留周期别设太短,至少覆盖一个结算周期。

5.2 佣金超发,财务对账差出几千块

现象是推广员余额之和大于佣金流水中已结算金额之和。

原因基本都是定时任务在多实例下重复执行。@Scheduled在每台机器上都会跑,如果没加分布式锁,两台机器同时捞到同一批待结算记录,各加一次余额。还有一种情况是任务执行超时被下一轮重入,updateState没带state=0条件,直接把已结算的又结算了一遍。

解决分两层:任务入口加 Redis 分布式锁(第三章那段代码),updateState的 SQL 必须带状态条件,靠数据库保证只成功一次。已经超发的部分,从commission_record里按order_no去重能算出差额,做一次调账就行,不要直接改余额表,调账也要留流水。

5.3 二维码扫出来是空白页,换成链接就正常

现象是后台生成的二维码用微信扫提示无法访问,但把二维码里的 URL 复制到浏览器能打开。

原因通常是二维码内容里带了中文参数或者没有 URL 编码。渠道名、推广员昵称这类中文如果直接拼进 query string,部分扫码工具解析会出问题。另一个高频原因是landing-domain配的是localhost,手机扫码当然访问不到。

解决是把所有参数做URLEncoder.encode(value, "UTF-8")再拼,域名改成内网 IP 或正式域名。ZXing 生成时建议把纠错级别设成 M,容错和尺寸比较平衡;设成 H 虽然更抗污损,但码会变密,图片小了反而扫不出来。

5.4 订单归因时有时无,同一个用户两单归到不同人

现象是同一个用户前后两笔订单,一笔归给了 A,一笔归给了 B,但用户说自己只点过 A 的链接。

原因是归因时同时读了缓存和数据库,两边数据不一致。缓存写的时机是绑定成功那一刻,数据库写的是关系表,如果绑定后用户手动解绑或者关系过期清理任务跑了,缓存没同步删,就会出现「库里没有、缓存里有」的脏读。

解决办法是绑定关系只认一个数据源,缓存只作为加速层,并且所有变更关系的地方(绑定、解绑、过期)都要删缓存,不能只写不删。更稳的做法是缓存里存关系的 ID,归因时用 ID 回查一次状态,多一次查询但不会读错。归因这种影响分钱的逻辑,多一次查询换一致性是划算的。

6. 二次开发:三级改两级,顺手加一个自定义渠道

拿到源码后最常见的两个改动需求,一个是调整分销层级,一个是加自己的渠道类型。这两件事改动量都不大,但改的位置不对会连带影响结算。

分销层级这块,核心是三个地方必须同步改:配置项promotion.commission-rates的个数、CommissionServiceImpl里rates数组的长度、以及后台「分销设置」页面的层级下拉选项。前两个改完就生效了,第三个只是为了让运营在页面上看到正确的层数。如果只改了配置没改代码里的数组,代码会按数组长度跑,配置形同虚设——这是最容易踩的坑,改完一定要下两单验证,看佣金流水里是不是只出现level=1和level=2。

加自定义渠道则要动promotion_link表和归因逻辑。表里加一个channel_type字段,枚举值自己定,比如 1 是普通推广员、2 是异业合作、3 是线下门店。归因时按channel_type走不同的分佣规则:普通推广员按层级走,异业合作按固定金额,线下门店按区域分成。我给一个扩展点的写法:

@Component public class ChannelCommissionFactory { @Resource private Map<String, ChannelCommissionHandler> handlers; /** * 按渠道类型路由到不同处理器 * @param channelType 渠道类型,需与 handler 的 bean 名称对应 */ public ChannelCommissionHandler get(String channelType) { ChannelCommissionHandler handler = handlers.get(channelType + "Handler"); if (handler == null) { throw new BizException("未配置该渠道的分佣规则: " + channelType); } return handler; } }

把各个渠道的算法实现成ChannelCommissionHandler的实现类,Spring 注入时按 bean 名放进 Map,新增渠道只要加一个类,不用改工厂。这种写法比你在一堆 if-else 里加分支要安全,因为新渠道的规则和旧渠道完全隔离,出问题也只影响一个渠道。

改完怎么验证,我的做法是造三组数据跑回归:一个走普通推广员链接下单、一个走异业合作渠道下单、一个自然流量下单,三笔都走到已结算,然后对一遍余额和流水。这一步别省,我见过改完比例后忘了清 Redis 缓存,页面显示的分佣比例是新的,实际算出来还是旧的,两边对不上折腾了一下午。

从那以后我每次动分佣相关的代码,都会先把commission_record里最近一天的流水导出来存一份,改完再导一份做 diff,只看level和amount两列的变化。这个习惯帮我省了不止一次返工。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询