文件
fenleixinxi/application/common/library/DistributionService.php

250 行
8.8 KiB
PHP

此文件含有模棱两可的 Unicode 字符

此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。

<?php
namespace app\common\library;
use think\Db;
/**
* 同城分类信息 · 全民分销核心业务服务
*
* 负责分销拓客关系链绑定(1级/2级关系)与订单支付后自动拆单分佣结算
*/
class DistributionService
{
/**
* 绑定分销上下级关系
*
* @param int $userId 新用户ID
* @param int $inviterId 推荐人用户ID(可选)
* @param string $inviteCode 推荐人邀请码(可选)
* @return bool 是否成功建立或已存在关系
*/
public static function bindRelation($userId, $inviterId = 0, $inviteCode = '')
{
$userId = (int)$userId;
if ($userId <= 0) {
return false;
}
// 幂等防御:若用户已有推荐人或已有上级关系记录,则保持原有归属(首绑锁定)
$ext = Db::name('fc_user_ext')->where('user_id', $userId)->find();
if ($ext && !empty($ext['inviter_id']) && (int)$ext['inviter_id'] > 0) {
return true;
}
$hasRelation = Db::name('fc_dist_relation')->where('user_id', $userId)->count();
if ($hasRelation > 0) {
return true;
}
$parent1Id = 0;
// 优先通过邀请码查询推荐人
$inviteCode = trim((string)$inviteCode);
if ($inviteCode !== '') {
$parentExt = Db::name('fc_user_ext')->where('invite_code', $inviteCode)->find();
if ($parentExt && (int)$parentExt['user_id'] > 0) {
$parent1Id = (int)$parentExt['user_id'];
}
}
// 次选通过直接传入的推荐人用户ID定位
if ($parent1Id <= 0 && (int)$inviterId > 0) {
$parentUser = Db::name('user')->where('id', (int)$inviterId)->find();
if ($parentUser) {
$parent1Id = (int)$parentUser['id'];
}
}
// 推荐人不存在,或推荐人是用户本人,则不建立关系
if ($parent1Id <= 0 || $parent1Id === $userId) {
return false;
}
$now = time();
// 更新用户扩展表的直接推荐人ID
Db::name('fc_user_ext')->where('user_id', $userId)->update([
'inviter_id' => $parent1Id,
'updatetime' => $now,
]);
// 1. 建立 1 级分销关系(直推)
Db::name('fc_dist_relation')->insert([
'user_id' => $userId,
'parent_id' => $parent1Id,
'level' => 1,
'createtime' => $now,
]);
// 2. 递归追溯 2 级分销关系(间推:查找 1 级推荐人的 1 级推荐人)
$parent2Row = Db::name('fc_dist_relation')
->where('user_id', $parent1Id)
->where('level', 1)
->find();
if ($parent2Row && (int)$parent2Row['parent_id'] > 0) {
$parent2Id = (int)$parent2Row['parent_id'];
// 避免循环推荐(2级推荐人不能为用户本人和1级推荐人)
if ($parent2Id !== $userId && $parent2Id !== $parent1Id) {
Db::name('fc_dist_relation')->insert([
'user_id' => $userId,
'parent_id' => $parent2Id,
'level' => 2,
'createtime' => $now,
]);
}
}
return true;
}
/**
* 订单支付成功后执行自动拆单分佣结算
*
* 适用于各类消费型订单(发布 post、会员开通 member、付费查看 payview 等)
*
* @param array $order 订单数据数组,必须包含 id, order_no, user_id, amount, type
* @return array 结算明细数组
*/
public static function settleOrder(array $order)
{
$orderId = (int)(isset($order['id']) ? $order['id'] : 0);
$orderNo = isset($order['order_no']) ? (string)$order['order_no'] : '';
$payerId = (int)(isset($order['user_id']) ? $order['user_id'] : 0);
$amount = round((float)(isset($order['amount']) ? $order['amount'] : 0), 2);
$type = isset($order['type']) ? (string)$order['type'] : 'post';
// 充值订单不分佣(避免双重套现),金额必须大于 0
if ($amount <= 0 || $payerId <= 0 || $type === 'recharge') {
return [];
}
// 读取分销全局配置
$enabled = self::getConfig('dist.enabled', 1);
if (!$enabled) {
return [];
}
// 幂等防御:已对此订单结算过佣金则直接跳过,防止并发重复发放
$alreadySettled = Db::name('fc_commission')
->where('order_no', $orderNo)
->find();
if ($alreadySettled) {
return [];
}
$maxLevel = (int)self::getConfig('dist.level', 2);
$rate1 = (float)self::getConfig('dist.level1Rate', 20); // 默认 20%
$rate2 = (float)self::getConfig('dist.level2Rate', 10); // 默认 10%
$settleDays = self::settleDays();
// 查询该付款用户的所有上级推广员(按层级正序)
$relations = Db::name('fc_dist_relation')
->where('user_id', $payerId)
->where('level', '<=', $maxLevel)
->order('level asc')
->select();
if (empty($relations)) {
return [];
}
$now = time();
$settledList = [];
foreach ($relations as $rel) {
$level = (int)$rel['level'];
$parentId = (int)$rel['parent_id'];
if ($parentId <= 0) {
continue;
}
// 获取该层级的有效比例
$rate = ($level === 1) ? $rate1 : (($level === 2) ? $rate2 : 0);
if ($rate <= 0) {
continue;
}
$commissionAmount = round($amount * ($rate / 100), 2);
if ($commissionAmount <= 0) {
continue;
}
// 写入分销佣金明细表(结算周期内为「未结算」,到期由 fc:settle 置为已结算)
$commId = Db::name('fc_commission')->insertGetId([
'user_id' => $parentId,
'from_user_id' => $payerId,
'order_id' => $orderId,
'order_no' => $orderNo,
'order_type' => $type,
'amount' => $commissionAmount,
'rate' => $rate,
'level' => $level,
'status' => $settleDays > 0 ? 0 : 1,
'settle_due' => $now + $settleDays * 86400,
'settletime' => $settleDays > 0 ? null : $now,
'createtime' => $now,
]);
// 发送分销佣金入账站内信通知(区分待结算周期与实时结算)
$levelText = ($level === 1) ? '直接下线' : '间接下线';
$settleTip = ($settleDays > 0)
? '已计入待结算佣金(预计 T+' . $settleDays . ' 自动结算转入可提现资产)。'
: '已计入可提现资产。';
$noticeContent = '您的 ' . $levelText . ' 产生了一笔消费(¥' . $amount . '),为您带来分销佣金 ¥' . $commissionAmount . ',' . $settleTip;
Db::name('fc_message')->insert([
'user_id' => $parentId,
'type' => 'interact',
'title' => '分销佣金到账提醒',
'content' => $noticeContent,
'isread' => 0,
'createtime' => $now,
]);
$settledList[] = [
'commissionId' => $commId,
'userId' => $parentId,
'level' => $level,
'amount' => $commissionAmount,
];
}
return $settledList;
}
/**
* 结算周期天数:解析 dist.settleCycle(支持 T+7 / T+1 / T+0 或纯数字 7 / 0),默认 7 天
*
* @return int
*/
public static function settleDays()
{
$cycle = trim((string)self::getConfig('dist.settleCycle', 'T+7'));
if (preg_match('/T\s*\+\s*(\d+)/i', $cycle, $m)) {
return max(0, (int)$m[1]);
}
if (is_numeric($cycle)) {
return max(0, (int)$cycle);
}
return 7;
}
/**
* 读取业务配置项
*
* @param string $key 配置键名
* @param mixed $default 默认回退值
* @return mixed
*/
protected static function getConfig($key, $default = null)
{
$row = Db::name('fc_config')->where('key', $key)->find();
if (!$row || !isset($row['value']) || $row['value'] === '') {
return $default;
}
$val = $row['value'];
$decoded = json_decode($val, true);
return (json_last_error() === JSON_ERROR_NONE) ? $decoded : $val;
}
}