1.1 原生分销的核心数据模型
LikeShop 的分销体系以 ls_user 表的上下级关系字段为核心;每个用户通过 invite_user_id (或类似字段)记录其直接上级,三级分销的佣金计算依赖这条链路的逐级向上查找。
分销关系的绑定逻辑为:A 分享商品或邀请链接给 B,B 通过链接注册或首次绑定后,A 成为 B 的上级。如果 A 是 B 的上级,B 是 C 的上级,那么 B 是 C 的一级分销,A 是 C 的二级分销;C 购买商品时,B 获得一级分销佣金,A 获得二级分销佣金。
佣金金额的计算公式为:佣金计算金额 = 商品总金额 - 优惠券金额 - 用户折扣金额 -/+ 商品改价金额 - 运费金额;这个公式有几个关键约束:运费不计算佣金;使用优惠券或积分等非现金支付的部分不计算佣金。
原生系统支持三种分销员模式:人人分销(注册即成为分销会员)、申请分销(需要申请并审核通过)、指定分销(通过后台开通指定会员分销权);指定分销状态按会员等级指定,未指定的分销等级会员不允许参与分销。
1.2 佣金结算的三个必要条件
原生系统的佣金结算逻辑为:商品已确认收货 → 已过商品售后期 → 已过结算时间,且需要定时任务正常执行;未配置定时任务时,佣金不会自动结算;佣金提现至钱包余额时不扣手续费,其他提现方式均需要手续费。
1.3 原生体系的四个核心缺口
缺口一:奖励类单一。原生分销仅支持佣金(现金)一种奖励类型;虽然系统在推广层面可以配合优惠券和积分使用,但奖励类型和分销佣金没有打通——邀请人无法获得“积分+优惠券”的组合奖励,被邀请人也无法自动获得新人礼包。
缺口二:邀请注册无独立奖励。原生系统的佣金仅在“下级购买分销商品”时产生;如果下级只是注册了但未购买,邀请人不会获得任何奖励;这意味着“拉新”和“转化”两个环节是捆绑的,无法激励纯拉新行为。
缺口三:无防刷判定机制。原生系统没有对邀请关系的真实性做校验;羊毛党可以通过同一设备批量注册账号互相邀请、同一 IP 下多账号绑定关系、同一手机号换绑等方式刷取佣金,而系统层面没有任何拦截。
缺口四:邀请奖励的配置粒度粗。原生的佣金比例在分销商品级别设置,按分销等级(一级/二级)区分比例;但没有“邀请注册奖励”“邀请首单奖励”“邀请多人的阶梯奖励”等更细粒度的配置能力。
1.4 二开策略定位
遵循 LikeShop 的“扩展优先”原则,邀请裂变二开采用以下策略:
- 不修改原生分销的佣金计算和结算逻辑,新建独立的 InviteRewardService 作为邀请奖励的扩展层
- 新增邀请奖励配置表,支持现金/积分/优惠券三种奖励类型的独立配置
- 新增邀请关系验证表,记录每次邀请行为的设备指纹、IP、手机号等风控维度
- 防刷判定在邀请关系绑定定时执行,判定不通过的邀请关系不建立上下级绑定、从源头阻断
2.1 邀请奖励配置表
这是邀请奖励体系的核心配置表,支持按“邀请注册”“邀请首单”两个节点分别配置奖励:
sql
CREATE TABLE `ls_invite_reward_config` (
`id` int UNSIGNED NOT NULL AUTO_INCREMENT,
`config_name` varchar(50) NOT NULL COMMENT '配置名称',
`reward_node` tinyint NOT NULL DEFAULT 1 COMMENT '奖励节点:1-邀请注册,2-邀请首单,3-邀请复购',
`inviter_reward_type` tinyint NOT NULL DEFAULT 1 COMMENT '邀请人奖励类型:1-现金,2-积分,3-优惠券,4-组合',
`inviter_cash_amount` decimal(10,2) NOT NULL DEFAULT 0.00 COMMENT '邀请人现金奖励金额',
`inviter_integral` int UNSIGNED NOT NULL DEFAULT 0 COMMENT '邀请人积分奖励数量',
`inviter_coupon_ids` varchar(255) NOT NULL DEFAULT '' COMMENT '邀请人优惠券ID,逗号分隔',
`invitee_reward_type` tinyint NOT NULL DEFAULT 1 COMMENT '被邀请人奖励类型:1-现金,2-积分,3-优惠券,4-组合',
`invitee_cash_amount` decimal(10,2) NOT NULL DEFAULT 0.00 COMMENT '被邀请人现金奖励金额',
`invitee_integral` int UNSIGNED NOT NULL DEFAULT 0 COMMENT '被邀请人积分奖励数量',
`invitee_coupon_ids` varchar(255) NOT NULL DEFAULT '' COMMENT '被邀请人优惠券ID,逗号分隔',
`first_order_min_amount` decimal(10,2) NOT NULL DEFAULT 0.00 COMMENT '首单最低金额门槛,0为不限',
`max_reward_per_day` int UNSIGNED NOT NULL DEFAULT 0 COMMENT '邀请人每日最多获得奖励次数,0为不限',
`max_reward_total` int UNSIGNED NOT NULL DEFAULT 0 COMMENT '邀请人累计最多获得奖励次数,0为不限',
`is_enable` tinyint NOT NULL DEFAULT 1 COMMENT '是否启用',
`create_time` int UNSIGNED NOT NULL DEFAULT 0,
`update_time` int UNSIGNED NOT NULL DEFAULT 0,
PRIMARY KEY (`id`),
KEY `idx_reward_node` (`reward_node`),
KEY `idx_enable` (`is_enable`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='邀请奖励配置表';
reward_node 的设计将奖励拆分为三个节点:注册奖励(拉新激励)、首单奖励(转化激励)、复购奖励(留存激励);每个节点可以独立配置邀请人和被邀请人的奖励类型和金额。
2.2 邀请关系验证表
记录每次邀请绑定时的风控维度数据:
sql
CREATE TABLE `ls_invite_verify_log` (
`id` int UNSIGNED NOT NULL AUTO_INCREMENT,
`inviter_user_id` int UNSIGNED NOT NULL COMMENT '邀请人ID',
`invitee_user_id` int UNSIGNED NOT NULL COMMENT '被邀请人ID',
`invitee_mobile` varchar(20) NOT NULL DEFAULT '' COMMENT '被邀请人手机号',
`invitee_ip` varchar(45) NOT NULL DEFAULT '' COMMENT '被邀请人IP',
`invitee_device` varchar(128) NOT NULL DEFAULT '' COMMENT '被邀请人设备指纹',
`inviter_ip` varchar(45) NOT NULL DEFAULT '' COMMENT '邀请人IP',
`inviter_device` varchar(128) NOT NULL DEFAULT '' COMMENT '邀请人设备指纹',
`verify_result` tinyint NOT NULL DEFAULT 1 COMMENT '验证结果:1-通过,2-设备重复,3-IP重复,4-手机号重复,5-其他异常',
`verify_remark` varchar(255) NOT NULL DEFAULT '' COMMENT '验证备注',
`create_time` int UNSIGNED NOT NULL DEFAULT 0,
PRIMARY KEY (`id`),
KEY `idx_inviter` (`inviter_user_id`),
KEY `idx_invitee` (`invitee_user_id`),
KEY `idx_device` (`invitee_device`),
KEY `idx_ip` (`invitee_ip`),
KEY `idx_mobile` (`invitee_mobile`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='邀请关系验证记录表';
2.3 邀请奖励发放记录表
sql
CREATE TABLE `ls_invite_reward_log` (
`id` int UNSIGNED NOT NULL AUTO_INCREMENT,
`config_id` int UNSIGNED NOT NULL COMMENT '奖励配置ID',
`inviter_user_id` int UNSIGNED NOT NULL COMMENT '邀请人ID',
`invitee_user_id` int UNSIGNED NOT NULL COMMENT '被邀请人ID',
`reward_node` tinyint NOT NULL DEFAULT 1 COMMENT '奖励节点:1-注册,2-首单,3-复购',
`reward_type` tinyint NOT NULL DEFAULT 1 COMMENT '奖励类型:1-现金,2-积分,3-优惠券',
`reward_value` decimal(10,2) NOT NULL DEFAULT 0.00 COMMENT '奖励数量/金额',
`coupon_id` int UNSIGNED NOT NULL DEFAULT 0 COMMENT '优惠券ID(奖励类型为券时)',
`order_id` int UNSIGNED NOT NULL DEFAULT 0 COMMENT '关联订单ID(首单/复购奖励时)',
`status` tinyint NOT NULL DEFAULT 1 COMMENT '状态:1-已发放,2-已失效',
`create_time` int UNSIGNED NOT NULL DEFAULT 0,
PRIMARY KEY (`id`),
KEY `idx_inviter_node` (`inviter_user_id`, `reward_node`),
KEY `idx_invitee` (`invitee_user_id`),
KEY `idx_order` (`order_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='邀请奖励发放记录表';
2.4 用户邀请统计表
用于快速查询用户的邀请数据,避免每次从关系聚合:
sql
CREATE TABLE `ls_user_invite_stat` (
`id` int UNSIGNED NOT NULL AUTO_INCREMENT,
`user_id` int UNSIGNED NOT NULL COMMENT '用户ID',
`total_invite_count` int UNSIGNED NOT NULL DEFAULT 0 COMMENT '累计邀请注册人数',
`total_first_order_count` int UNSIGNED NOT NULL DEFAULT 0 COMMENT '下级首单人数',
`today_invite_count` int UNSIGNED NOT NULL DEFAULT 0 COMMENT '今日邀请人数',
`today_reward_count` int UNSIGNED NOT NULL DEFAULT 0 COMMENT '今日已获得奖励次数',
`total_reward_cash` decimal(12,2) NOT NULL DEFAULT 0.00 COMMENT '累计现金奖励',
`total_reward_integral` int UNSIGNED NOT NULL DEFAULT 0 COMMENT '累计积分奖励',
`last_invite_time` int UNSIGNED NOT NULL DEFAULT 0 COMMENT '最后邀请时间',
`update_time` int UNSIGNED NOT NULL DEFAULT 0 COMMENT '统计更新时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_user_id` (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户邀请统计表';
3.1 奖励链路设计
邀请奖励的触发链路为:
被邀请人注册 → 绑定邀请关系 → 触发注册奖励 → 被邀请人首单支付 → 触发首单奖励 → 被邀请人复购 → 触发复购奖励。
其中,一级奖励发放给直接邀请人,二级奖励发放给邀请人的上级;二级奖励的配置是可选开启的,如果只需要一级奖励,将二级奖励配置金额设为 0 即可。
3.2 邀请关系绑定与注册奖励
用户注册时,通过分享链接携带的邀请人ID 或邀请码绑定上下级关系;绑定成功后,根据配置发放注册奖励:
php
namespace app\common\service;
class InviteRewardService
{
/**
* 用户注册时绑定邀请关系并发放注册奖励
*/
public function handleRegister($newUserId, $inviterId, $registerData)
{
if ($inviterId <= 0 || $inviterId == $newUserId) {
return ['bound' => false, 'reason' => '邀请人无效'];
}
// 1. 防刷校验
$verifyResult = $this->verifyInviteRelation($inviterId, $newUserId, $registerData);
if (!$verifyResult['passed']) {
// 记录验证失败日志
InviteVerifyLogModel::create([
'inviter_user_id' => $inviterId,
'invitee_user_id' => $newUserId,
'invitee_mobile' => $registerData['mobile'] ?? '',
'invitee_ip' => $registerData['ip'] ?? '',
'invitee_device' => $registerData['device'] ?? '',
'verify_result' => $verifyResult['code'],
'verify_remark' => $verifyResult['reason'],
'create_time' => time(),
]);
return ['bound' => false, 'reason' => $verifyResult['reason']];
}
// 2. 绑定上下级关系(写入 ls_user 的 invite_user_id)
Db::startTrans();
try {
UserModel::where('id', $newUserId)->update([
'invite_user_id' => $inviterId,
'invite_time' => time(),
]);
// 记录验证通过日志
InviteVerifyLogModel::create([
'inviter_user_id' => $inviterId,
'invitee_user_id' => $newUserId,
'invitee_mobile' => $registerData['mobile'] ?? '',
'invitee_ip' => $registerData['ip'] ?? '',
'invitee_device' => $registerData['device'] ?? '',
'verify_result' => 1,
'create_time' => time(),
]);
Db::commit();
} catch (\Exception $e) {
Db::rollback();
throw $e;
}
// 3. 发放注册奖励
$this->grantReward($inviterId, $newUserId, 1, null);
// 4. 更新邀请统计
$this->updateInviteStat($inviterId, 'register');
return ['bound' => true, 'reason' => ''];
}
/**
* 发放邀请奖励
* @param int $inviterId 邀请人ID
* @param int $inviteeId 被邀请人ID
* @param int $rewardNode 奖励节点:1-注册,2-首单,3-复购
* @param int $orderId 关联订单ID
*/
private function grantReward($inviterId, $inviteeId, $rewardNode, $orderId = null)
{
// 查询该节点的奖励配置
$config = InviteRewardConfigModel::where('reward_node', $rewardNode)
->where('is_enable', 1)
->find();
if (!$config) return;
// 检查邀请人的每日/累计奖励上限
if (!$this->checkRewardLimit($inviterId, $config)) {
return;
}
Db::startTrans();
try {
// 发放邀请人奖励
$this->grantToUser($inviterId, $config, 'inviter', $rewardNode, $orderId);
// 发放被邀请人奖励(仅注册和首单节点)
if (in_array($rewardNode, [1, 2])) {
$this->grantToUser($inviteeId, $config, 'invitee', $rewardNode, $orderId);
}
// 更新奖励统计
$this->updateRewardStat($inviterId, $config, $rewardNode);
Db::commit();
} catch (\Exception $e) {
Db::rollback();
Log::error("邀请奖励发放失败 inviter={$inviterId}, invitee={$inviteeId}, node={$rewardNode}: " . $e->getMessage());
}
}
/**
* 给指定用户发放奖励
*/
private function grantToUser($userId, $config, $role, $rewardNode, $orderId)
{
$prefix = ($role == 'inviter') ? 'inviter_' : 'invitee_';
// 1. 现金奖励
$cashAmount = $config[$prefix . 'cash_amount'];
if ($cashAmount > 0) {
UserModel::where('id', $userId)->inc('user_money', $cashAmount)->update();
AccountLogModel::create([
'sn' => generateSn(),
'user_id' => $userId,
'change_object' => 1,
'change_type' => 邀请奖励变动类型,
'action' => 1,
'change_amount' => $cashAmount,
'left_amount' => UserModel::where('id', $userId)->value('user_money'),
'remark' => '邀请奖励-现金',
'create_time' => time(),
]);
InviteRewardLogModel::create([
'config_id' => $config['id'],
'inviter_user_id' => $userId,
'invitee_user_id' => 0,
'reward_node' => $rewardNode,
'reward_type' => 1,
'reward_value' => $cashAmount,
'order_id' => $orderId ?: 0,
'status' => 1,
'create_time' => time(),
]);
}
// 2. 积分奖励
$integral = $config[$prefix . 'integral'];
if ($integral > 0) {
IntegralService::addIntegral($userId, $integral, '邀请奖励-积分', '');
InviteRewardLogModel::create([
'config_id' => $config['id'],
'inviter_user_id' => $userId,
'reward_node' => $rewardNode,
'reward_type' => 2,
'reward_value' => $integral,
'create_time' => time(),
]);
}
// 3. 优惠券奖励
$couponIds = $config[$prefix . 'coupon_ids'];
if (!empty($couponIds)) {
foreach (explode(',', $couponIds) as $couponId) {
CouponService::sendCoupon($userId, (int)$couponId, '邀请奖励');
}
InviteRewardLogModel::create([
'config_id' => $config['id'],
'inviter_user_id' => $userId,
'reward_node' => $rewardNode,
'reward_type' => 3,
'reward_value' => count(explode(',', $couponIds)),
'create_time' => time(),
]);
}
}
}
3.3 首单奖励的触发
被邀请人的首单支付成功后,触发首单奖励;需要校验订单是否为被邀请人的首单,以及是否满足配置中的最低金额门槛:
php
/**
* 订单支付成功后检查是否触发首单邀请奖励
*/
public function handleOrderPay($orderId)
{
$order = OrderModel::find($orderId);
if (!$order) return;
$userId = $order['user_id'];
// 1. 获取用户的邀请人
$user = UserModel::field('id, invite_user_id')->find($userId);
if (!$user || $user['invite_user_id'] <= 0) return;
// 2. 校验是否为该用户的首单
$orderCount = OrderModel::where('user_id', $userId)
->where('pay_status', 1)
->where('id', '<>', $orderId)
->count();
if ($orderCount > 0) return; // 非首单
// 3. 查询首单奖励配置,校验金额门槛
$config = InviteRewardConfigModel::where('reward_node', 2)
->where('is_enable', 1)->find();
if (!$config) return;
if ($config['first_order_min_amount'] > 0
&& $order['order_amount'] < $config['first_order_min_amount']) {
return; // 未达到金额门槛
}
// 4. 幂等校验:该被邀请人是否已发放过首单奖励
$exists = InviteRewardLogModel::where('invitee_user_id', $userId)
->where('reward_node', 2)
->where('status', 1)
->count();
if ($exists > 0) return;
// 5. 发放首单奖励
$this->grantReward($user['invite_user_id'], $userId, 2, $orderId);
// 6. 更新统计
$this->updateInviteStat($user['invite_user_id'], 'first_order');
}
3.4 二级奖励的实现
二级奖励的核心是在发放一级奖励的同时,向上追溯一级,找到邀请人的上级,给其发放二级奖励;二级奖励的金额通常是给一级奖励的上级(即被邀请人的二级上级),而非给被邀请人的直接上级。
php
/**
* 发放二级邀请奖励
*/
private function grantSecondLevelReward($inviterId, $inviteeId, $rewardNode, $orderId = null)
{
// 查询邀请人的上级(即被邀请人的二级上级)
$inviter = UserModel::field('id, invite_user_id')->find($inviterId);
if (!$inviter || $inviter['invite_user_id'] <= 0) return;
$secondInviterId = $inviter['invite_user_id'];
// 校验二级奖励配置(独立于一级配置,金额为0则不发放)
$secondConfig = InviteRewardConfigModel::where('reward_node', $rewardNode)
->where('is_enable', 1)
->where('inviter_cash_amount', '>', 0)
->find();
if (!$secondConfig) return;
// 发放二级奖励给二级上级
$this->grantToUser($secondInviterId, $secondConfig, 'inviter', $rewardNode, $orderId);
}
二级奖励的配置可以在邀请奖励配置表中增加独立的二级奖励字段,或者新建一条 reward_node 相同,但 config_type = 2 的配置记录来区分;推荐后者,保持配置结构的简洁。
3.5 后台配置
在“分销 → 邀请奖励设置”中增加配置页面:
| 配置项 | 类型 | 说明 |
|---|---|---|
| 奖励节点 | 单选 | 邀请注册 / 邀请首单 / 邀请复购 |
| 邀请人奖励类型 | 多选 | 现金 / 积分 / 优惠券,可组合 |
| 邀请人现金金额 | 金额输入 | |
| 邀请人积分数量 | 数字输入 | |
| 邀请人优惠券 | 多选 | 从优惠券列表中选择 |
| 被邀请人奖励类型 | 多选 | 同邀请人 |
| 被邀请人奖励内容 | 组合配置 | 现金/积分/优惠券 |
| 首单最低金额 | 金额输入 | 仅首单节点有效 |
| 每日奖励上限 | 数字输入 | 0 为不限 |
| 累计奖励上限 | 数字输入 | 0 为不限 |
4.1 三层防刷校验
在邀请关系绑定时,执行三层防刷校验:同设备校验、同 IP 校验、同手机号校验。
php
/**
* 邀请关系防刷校验
*/
private function verifyInviteRelation($inviterId, $inviteeId, $registerData)
{
$inviteeIp = $registerData['ip'] ?? '';
$inviteeDevice = $registerData['device'] ?? '';
$inviteeMobile = $registerData['mobile'] ?? '';
// 第一层:同设备校验
// 如果被邀请人的设备指纹与邀请人或其他已绑定用户相同,判定为无效邀请
if (!empty($inviteeDevice)) {
$deviceConflict = InviteVerifyLogModel::where('invitee_device', $inviteeDevice)
->where('verify_result', 1)
->where('invitee_user_id', '<>', $inviteeId)
->count();
if ($deviceConflict > 0) {
return ['passed' => false, 'code' => 2, 'reason' => '该设备已注册过其他账号,邀请无效'];
}
// 检查邀请人自身是否使用同一设备
$inviterDevice = InviteVerifyLogModel::where('inviter_user_id', $inviterId)
->where('verify_result', 1)
->value('invitee_device');
if ($inviterDevice && $inviterDevice == $inviteeDevice) {
return ['passed' => false, 'code' => 2, 'reason' => '邀请人与被邀请人使用同一设备'];
}
}
// 第二层:同IP校验
if (!empty($inviteeIp)) {
// 检查该IP下已注册的账号数
$ipUserCount = InviteVerifyLogModel::where('invitee_ip', $inviteeIp)
->where('verify_result', 1)
->group('invitee_user_id')
->count();
$ipLimit = ConfigModel::get('invite_ip_limit', 3);
if ($ipUserCount >= $ipLimit) {
return ['passed' => false, 'code' => 3, 'reason' => "该IP下已注册{$ipUserCount}个账号,超过限制"];
}
// 检查邀请人与被邀请人是否同一IP
$inviterLog = InviteVerifyLogModel::where('inviter_user_id', $inviterId)
->where('verify_result', 1)
->order('create_time', 'desc')
->find();
if ($inviterLog && $inviterLog['invitee_ip'] == $inviteeIp) {
return ['passed' => false, 'code' => 3, 'reason' => '邀请人与被邀请人使用同一网络'];
}
}
// 第三层:同手机号校验
if (!empty($inviteeMobile)) {
// 检查该手机号是否已被其他账号使用过(包括邀请人和被邀请人)
$mobileConflict = InviteVerifyLogModel::where('invitee_mobile', $inviteeMobile)
->where('verify_result', 1)
->where('invitee_user_id', '<>', $inviteeId)
->count();
if ($mobileConflict > 0) {
return ['passed' => false, 'code' => 4, 'reason' => '该手机号已注册过其他账号'];
}
// 检查邀请人的手机号是否与被邀请人相同
$inviterMobile = UserModel::where('id', $inviterId)->value('mobile');
if ($inviterMobile && $inviterMobile == $inviteeMobile) {
return ['passed' => false, 'code' => 4, 'reason' => '邀请人与被邀请人使用同一手机号'];
}
}
return ['passed' => true, 'code' => 1, 'reason' => ''];
}
4.2 设备指纹采集
设备指纹的采集在前端完成,通过 device_id 或 openid + unionid 组合生成;后端接口在接收注册请求时,从请求头中读取 X-Device-Id 字段;如果前端未传递设备指纹,则降级使用 IP + User-Agent 的哈希值作为设备标识的替代。
设备指纹的生成建议包含以下维度:设备型号、操作系统版本、屏幕分辨率、浏览器/微信版本、设备唯一标识(如 IOS 的 IDFV、AndroidID);多个维度的组合哈希可显著提高设备识别的准确性。
4.3 奖励发放时的二次校验
邀请关系绑定时已经做了防刷校验,但奖励发放时仍需二次校验,防止在绑定后到发放前的窗口期内发生异常:
php
/**
* 奖励发放前的二次校验
*/
private function checkRewardLimit($inviterId, $config)
{
// 1. 每日奖励上限
if ($config['max_reward_per_day'] > 0) {
$todayStart = strtotime('today');
$todayCount = InviteRewardLogModel::where('inviter_user_id', $inviterId)
->where('create_time', '>=', $todayStart)
->where('status', 1)
->count();
if ($todayCount >= $config['max_reward_per_day']) {
return false;
}
}
// 2. 累计奖励上限
if ($config['max_reward_total'] > 0) {
$totalCount = InviteRewardLogModel::where('inviter_user_id', $inviterId)
->where('status', 1)
->count();
if ($totalCount >= $config['max_reward_total']) {
return false;
}
}
// 3. 检查邀请人的账户状态
$inviter = UserModel::field('id, status, is_distributor')->find($inviterId);
if (!$inviter || $inviter['status'] != 1) {
return false;
}
return true;
}
4.4 异常数据的人工审核
对于触发防刷规则但被系统放行的边缘情况(如设备指纹缺失、IP 处于共享网络),系统将这些记录标记为“待审核”状态;运营人员在后台可以查看待审核的邀请关系,确认后手动放行或拒绝:
php
/**
* 人工审核邀请关系
*/
public function auditInviteRelation($verifyLogId, $adminId, $action, $remark = '')
{
$log = InviteVerifyLogModel::find($verifyLogId);
if (!$log) throw new Exception('记录不存在');
Db::startTrans();
try {
if ($action == 'approve') {
// 审核通过:绑定关系并发放奖励
InviteVerifyLogModel::where('id', $verifyLogId)->update([
'verify_result' => 1,
'verify_remark' => $remark,
]);
// 绑定上下级关系
UserModel::where('id', $log['invitee_user_id'])->update([
'invite_user_id' => $log['inviter_user_id'],
'invite_time' => time(),
]);
// 发放奖励
$this->grantReward($log['inviter_user_id'], $log['invitee_user_id'], 1, null);
} else {
// 审核拒绝:不绑定关系
InviteVerifyLogModel::where('id', $verifyLogId)->update([
'verify_result' => 5,
'verify_remark' => $remark,
]);
}
Db::commit();
} catch (\Exception $e) {
Db::rollback();
throw $e;
}
}
4.5 后台配置
在“分销 → 邀请防刷设置”中增加:
| 配置项 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| 同设备校验 | 开关 | 开启 | 同设备注册的账号邀请无效 |
| 同IP校验 | 开关 | 开启 | 同IP下多账号邀请限制 |
| 同IP账号上限 | 数字输入 | 3 | 同IP下最多允许注册的账号数 |
| 同手机号校验 | 开关 | 开启 | 同手机号换绑后邀请无效 |
| 异常数据人工审核 | 开关 | 开启 | 边缘情况是否进入人工审核 |
5.1 数据库变更清单
| 操作 | 表名 | 说明 |
|---|---|---|
| CREATE | ls_invite_reward_config | 邀请奖励配置表 |
| CREATE | ls_invite_verify_log | 邀请关系验证记录表 |
| CREATE | ls_invite_reward_log | 邀请奖励发放记录表 |
| CREATE | ls_user_invite_stat | 用户邀请统计表 |
5.2 后端开发清单
| 模块 | 文件/类 | 工作内容 |
|---|---|---|
| 数据层 | InviteRewardConfigModel | 奖励配置 Model |
| 数据层 | InviteVerifyLogModel | 验证记录 Model |
| 数据层 | InviteRewardLogModel | 奖励发放 Model |
| 数据层 | UserInviteStatModel | 邀请统计 Model |
| 业务层 | InviteRewardService | 邀请关系绑定、奖励发放核心逻辑 |
| 业务层 | InviteVerifyService | 防刷校验三层逻辑 |
| 业务层 | InviteStatService | 邀请统计更新和查询 |
| 事件 | UserRegisterEvent | 用户注册后触发邀请绑定和注册奖励 |
| 事件 | OrderPaySuccessEvent | 首单支付后触发首单奖励 |
| 后台接口 | InviteRewardController | 奖励配置管理、人工审核、统计查询 |
| 定时任务 | Crontab.php | 每日重置邀请统计中的“今日”计数 |
5.3 后台前端开发清单
| 页面 | 工作内容 |
|---|---|
| 分销 → 邀请奖励设置 | 按节点配置奖励类型和金额,支持一级/二级独立配置 |
| 分销 → 邀请防刷设置 | 设备/IP/手机号校验开关和阈值配置 |
| 分销 → 邀请记录 | 邀请关系列表(含验证状态、奖励发放状态) |
| 分销 → 人工审核 | 待审核的邀请关系列表,支持通过/拒绝 |
| 分销 → 邀请统计 | 邀请人数、转化率、奖励发放总额等数据看板 |
5.4 开发注意事项
防刷校验的优先级。设备指纹的校验优先级最高,因为同一设备批量注册是最常见的作弊手段;IP 校验次之,手机号校验作为第三道防线;三层校验全部通过后,邀请关系才被建立。
奖励发放的幂等性。同一对邀请关系在同一奖励节点只能发放一次奖励;在发放前必须查询 ls_invite_reward_log 是否已有记录,避免因重复触发导致多次发放。
二级奖励的合规性。二级奖励的层级不得超过两级;参考行业实践,分销层级通常控制在 3 级以内(一级分销、二级分销、三级分销),且需符合 《禁止传销条例》 的要求;本方案的二级奖励指“一级邀请人的上级”,实际层级为 2 级,合规性可控。
统计数据的实时性。 ls_user_invite_stat 表的“今日邀请人数”和“今日已获得奖励次数”需要每日凌晨重置;建议在定时任务中执行重置操作,并在每次邀请和奖励发放时实时更新。
兼容性。新增表均为独立表,不修改原生分销的 ls_user 表结构和佣金计算逻辑;邀请奖励的发放不影响原生分销佣金的计算和结算,两者是独立的激励体系;防刷校验仅作用于邀请奖励的绑定环节,不影响原生的分销关系绑定。
