Files
huangjingfen/pro_v3.5.1/app/services/hjf/PointsRewardServices.php
apple 1c0cf5204f fix(fsgx): 直推积分奖励链路校验(问题2)
- 买家直推人非分销员时中断直推级差链,避免祖先越级拿 reward_direct
- 同步更新 fsgx-issues-0330 文档问题2用例说明

Made-with: Cursor
2026-03-31 15:25:48 +08:00

222 lines
9.0 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
declare(strict_types=1);
namespace app\services\hjf;
use app\dao\hjf\PointsReleaseLogDao;
use app\dao\user\UserDao;
use app\services\agent\AgentLevelServices;
use app\services\BaseServices;
use app\services\user\UserBillServices;
use think\annotation\Inject;
use think\facade\Db;
use think\facade\Log;
/**
* 积分奖励服务(级差计算)—— 改造复用版
*
* 改造要点PRD 3.2.2
* - 使用 eb_user.agent_level (FK → eb_agent_level.id) 获取会员等级
* - 从 eb_agent_level 表的 direct_reward_points / umbrella_reward_points 字段读取奖励积分
* - 不再使用独立的 member_level 字段和系统配置表中的 hjf_reward_* 键
*
* Class PointsRewardServices
* @package app\services\hjf
*/
class PointsRewardServices extends BaseServices
{
#[Inject]
protected PointsReleaseLogDao $logDao;
#[Inject]
protected UserDao $userDao;
#[Inject]
protected AgentLevelServices $agentLevelServices;
#[Inject]
protected UserBillServices $userBillServices;
/**
* 对一笔报单订单发放积分奖励
*
* @param int $orderDbId 订单表主键 id用于 user_bill.link_id 关联后台订单
* @param array $preUpgradeLevels 升级前各用户的 agent_level 快照 [uid => level_id],用于 B2 校验
* @param int $qty 订单中报单商品数量积分按数量倍乘B3
*/
public function reward(int $orderUid, string $orderId, int $orderDbId = 0, array $preUpgradeLevels = [], int $qty = 1): void
{
$qty = max(1, $qty);
try {
// 幂等检查:若该订单已有积分奖励记录则跳过,防止重复发放
$exists = Db::name('points_release_log')
->where('order_id', $orderId)
->whereIn('type', ['reward_direct', 'reward_umbrella'])
->count();
if ($exists > 0) {
Log::info("[PointsReward] 订单 {$orderId} 已有积分奖励记录,跳过重复发放");
return;
}
$buyer = $this->userDao->get($orderUid);
if (!$buyer || !$buyer['spread_uid']) {
return;
}
// 取买家自身的直推奖励积分作为级差下限,确保第一级父节点只拿差额
$buyerLevelId = array_key_exists($orderUid, $preUpgradeLevels)
? $preUpgradeLevels[$orderUid]
: (int)($buyer['agent_level'] ?? 0);
$buyerDirectReward = $this->agentLevelServices->getDirectRewardPoints($buyerLevelId);
$this->propagateReward((int)$buyer['spread_uid'], $orderUid, $orderId, $buyerDirectReward, 0, $orderDbId, $preUpgradeLevels, $qty);
} catch (\Throwable $e) {
Log::error("[PointsReward] 积分奖励失败 orderUid={$orderUid} orderId={$orderId}: " . $e->getMessage());
}
}
/**
* 向上递归发放直推积分奖励(标准逐级级差)
*
* 算法沿推荐链向上遍历每个分销等级大于0的祖先获得
* max(0, 自身 direct_reward_points - 链中已见最高 direct_reward_points) * qty
* 非分销员grade=0跳过但继续向上传递$lowerDirectReward 不变。
*
* 直推关系约束($directCascadeActive
* 当买家直接推荐人depth=0不是分销员grade=0直推级差链终止
* 其所有上级祖先不得获得 reward_direct不存在直推关系
* 伞下奖励reward_umbrella不受此约束仍按自身开关独立发放。
*
* @param int $uid 当前被奖励用户
* @param int $fromUid 触发方(下级)用户 ID
* @param string $orderId 来源订单号
* @param int $lowerDirectReward 链中已被下级拿走的最高 direct_reward_points级差下限
* @param int $depth 递归深度(防止无限递归)
* @param int $orderDbId 订单表主键 id
* @param array $preUpgradeLevels 升级前各用户的 agent_level 快照 [uid => level_id]B2
* @param int $qty 报单商品数量积分按数量倍乘B3
* @param bool $directCascadeActive 直推级差链是否仍然有效
*/
private function propagateReward(
int $uid,
int $fromUid,
string $orderId,
int $lowerDirectReward,
int $depth = 0,
int $orderDbId = 0,
array $preUpgradeLevels = [],
int $qty = 1,
bool $directCascadeActive = true
): void {
if ($depth >= 10 || $uid <= 0) {
return;
}
$user = $this->userDao->get($uid);
if (!$user) {
return;
}
// fsgx B2使用升级前的 agent_level 判断资格,避免触发升级的那笔订单就给新等级积分
$agentLevelId = array_key_exists($uid, $preUpgradeLevels)
? $preUpgradeLevels[$uid]
: (int)($user['agent_level'] ?? 0);
$grade = $this->agentLevelServices->getGradeByLevelId($agentLevelId);
if ($grade === 0) {
// 非分销员:跳过奖励,继续向上传递
// 若当前节点是买家的直接推荐人depth=0直推级差链从此中断
// 上级祖先与买家之间没有直推关系,不应获得 reward_direct
$nextCascadeActive = ($depth === 0) ? false : $directCascadeActive;
if ($user['spread_uid']) {
$this->propagateReward((int)$user['spread_uid'], $uid, $orderId, $lowerDirectReward, $depth + 1, $orderDbId, $preUpgradeLevels, $qty, $nextCascadeActive);
}
return;
}
$directReward = $this->agentLevelServices->getDirectRewardPoints($agentLevelId);
// 直推级差:仅在直推链有效时发放(买家直接推荐人必须是分销员)
if ($directCascadeActive) {
$actual = max(0, $directReward - $lowerDirectReward) * $qty;
if ($actual > 0) {
$this->grantFrozenPoints(
$uid,
$actual,
$orderId,
'reward_direct',
'直推奖励(级差)' . " x{$qty} - 来源订单 {$orderId}",
$orderDbId
);
}
}
// 伞下积分奖励独立逻辑仅对间接上级depth>0生效受开关控制
// 使用 umbrella_reward_points 平额发放(非级差),各等级独立拿自身额度
if ($depth > 0) {
$umbrellaRewardEnable = (int)sys_config('hjf_umbrella_reward_enable', 0);
if ($umbrellaRewardEnable) {
$umbrellaPoints = $this->agentLevelServices->getUmbrellaRewardPoints($agentLevelId);
if ($umbrellaPoints > 0) {
$this->grantFrozenPoints(
$uid,
$umbrellaPoints * $qty,
$orderId,
'reward_umbrella',
'伞下奖励' . " x{$qty} - 来源订单 {$orderId}",
$orderDbId
);
}
}
}
// 向上传递时,下限取当前节点与已有下限中的较大值,确保上级只拿增量
$nextLower = max($directReward, $lowerDirectReward);
if ($user['spread_uid']) {
$this->propagateReward(
(int)$user['spread_uid'],
$uid,
$orderId,
$nextLower,
$depth + 1,
$orderDbId,
$preUpgradeLevels,
$qty,
$directCascadeActive
);
}
}
/**
* 写入待释放积分frozen_points并记录明细
*/
private function grantFrozenPoints(
int $uid,
int $points,
string $orderId,
string $type,
string $mark,
int $orderDbId = 0
): void {
Db::transaction(function () use ($uid, $points, $orderId, $type, $mark, $orderDbId) {
$this->userDao->bcInc($uid, 'frozen_points', (string)$points, 'uid');
$this->logDao->save([
'uid' => $uid,
'points' => $points,
'pm' => 1,
'type' => $type,
'title' => ($type === 'reward_direct') ? '直推奖励' : '伞下奖励',
'mark' => $mark,
'status' => 'frozen',
'order_id' => $orderId,
]);
// PRD §3.3:营销后台积分日志读 eb_user_bill双写待释放积分明细不增加可消费 integral
$integralBalance = (int)($this->userDao->value(['uid' => $uid], 'integral') ?: 0);
$billType = ($type === 'reward_direct') ? 'hjf_reward_direct_integral' : 'hjf_reward_umbrella_integral';
$this->userBillServices->income($billType, $uid, $points, $integralBalance, $orderDbId, 0, $mark);
});
}
}