diff --git a/pro_v3.5.1/app/command/HjfVerifyAgentConfig.php b/pro_v3.5.1/app/command/HjfVerifyAgentConfig.php new file mode 100644 index 00000000..02fff88d --- /dev/null +++ b/pro_v3.5.1/app/command/HjfVerifyAgentConfig.php @@ -0,0 +1,170 @@ +setName('hjf:verify-agent-config') + ->setDescription('e2e 验收分销员等级奖励积分与升级任务配置是否与 PRD 一致,传入 --fix 自动修正') + ->addOption('fix', null, \think\console\input\Option::VALUE_NONE, '自动修正不一致的配置'); + } + + protected function execute(Input $input, Output $output): int + { + $fix = (bool)$input->getOption('fix'); + $hasError = false; + + $output->writeln(''); + $output->writeln('========================================================'); + $output->writeln(' HJF 分销员等级配置 e2e 验收'); + $output->writeln('========================================================'); + + // ---------------------------------------------------------------- + // 1) eb_agent_level — 奖励积分配置(PRD 3.2) + // ---------------------------------------------------------------- + $output->writeln(''); + $output->writeln('【1】eb_agent_level 奖励积分配置'); + $output->writeln('------------------------------------------------------------'); + + $expectedLevels = [ + 1 => ['name_hint' => '创客', 'direct_reward_points' => 500, 'umbrella_reward_points' => 0], + 2 => ['name_hint' => '云店', 'direct_reward_points' => 800, 'umbrella_reward_points' => 300], + 3 => ['name_hint' => '服务中心', 'direct_reward_points' => 1000, 'umbrella_reward_points' => 200], + 4 => ['name_hint' => '合伙人', 'direct_reward_points' => 1300, 'umbrella_reward_points' => 300], + ]; + + $levels = Db::name('agent_level') + ->whereIn('grade', array_keys($expectedLevels)) + ->field('id,name,grade,direct_reward_points,umbrella_reward_points') + ->select() + ->toArray(); + + $levelsByGrade = []; + foreach ($levels as $level) { + $levelsByGrade[(int)$level['grade']] = $level; + } + + foreach ($expectedLevels as $grade => $expected) { + if (!isset($levelsByGrade[$grade])) { + $output->writeln(" [MISS] grade={$grade} ({$expected['name_hint']}) 行不存在!"); + $hasError = true; + continue; + } + $row = $levelsByGrade[$grade]; + $errors = []; + if ((int)$row['direct_reward_points'] !== $expected['direct_reward_points']) { + $errors[] = "direct_reward_points={$row['direct_reward_points']}(期望 {$expected['direct_reward_points']})"; + } + if ((int)$row['umbrella_reward_points'] !== $expected['umbrella_reward_points']) { + $errors[] = "umbrella_reward_points={$row['umbrella_reward_points']}(期望 {$expected['umbrella_reward_points']})"; + } + if ($errors) { + $hasError = true; + $output->writeln(" [FAIL] grade={$grade} {$row['name']}:" . implode(',', $errors)); + if ($fix) { + Db::name('agent_level')->where('id', $row['id'])->update([ + 'direct_reward_points' => $expected['direct_reward_points'], + 'umbrella_reward_points' => $expected['umbrella_reward_points'], + ]); + $output->writeln(" [FIX] 已修正 grade={$grade} {$row['name']}"); + } + } else { + $output->writeln(" [OK] grade={$grade} {$row['name']} direct={$row['direct_reward_points']} umbrella={$row['umbrella_reward_points']}"); + } + } + + // ---------------------------------------------------------------- + // 2) eb_agent_level_task — 升级任务配置(PRD 3.2) + // ---------------------------------------------------------------- + $output->writeln(''); + $output->writeln('【2】eb_agent_level_task 升级任务配置'); + $output->writeln('------------------------------------------------------------'); + + // PRD 任务配置:[grade => [[type, number, description], ...]] + $expectedTasks = [ + 1 => [[6, 3, '直推满 3 单']], + 2 => [[7, 30, '伞下满 30 单'], [8, 3, '至少 3 个直推']], + 3 => [[7, 100, '伞下满 100 单'], [8, 3, '至少 3 个直推']], + 4 => [[7, 1000, '伞下满 1000 单'], [8, 3, '至少 3 个直推']], + ]; + + foreach ($expectedTasks as $grade => $tasks) { + if (!isset($levelsByGrade[$grade])) { + $output->writeln(" [SKIP] grade={$grade} 等级行不存在,跳过任务检查"); + continue; + } + $levelId = $levelsByGrade[$grade]['id']; + $levelName = $levelsByGrade[$grade]['name']; + + foreach ($tasks as [$type, $number, $desc]) { + $taskRow = Db::name('agent_level_task') + ->where('level_id', $levelId) + ->where('type', $type) + ->field('id,number') + ->find(); + + if (!$taskRow) { + $hasError = true; + $output->writeln(" [MISS] grade={$grade} {$levelName} type={$type}({$desc})行不存在!"); + if ($fix) { + Db::name('agent_level_task')->insert([ + 'level_id' => $levelId, + 'type' => $type, + 'number' => $number, + ]); + $output->writeln(" [FIX] 已插入 grade={$grade} type={$type} number={$number}"); + } + } elseif ((int)$taskRow['number'] !== $number) { + $hasError = true; + $output->writeln(" [FAIL] grade={$grade} {$levelName} type={$type}({$desc})number={$taskRow['number']}(期望 {$number})"); + if ($fix) { + Db::name('agent_level_task')->where('id', $taskRow['id'])->update(['number' => $number]); + $output->writeln(" [FIX] 已修正 grade={$grade} type={$type} number={$number}"); + } + } else { + $output->writeln(" [OK] grade={$grade} {$levelName} type={$type}({$desc})number={$taskRow['number']}"); + } + } + } + + // ---------------------------------------------------------------- + // 输出汇总 + // ---------------------------------------------------------------- + $output->writeln(''); + $output->writeln('========================================================'); + if ($hasError) { + if ($fix) { + $output->writeln(' 结果:检测到配置不一致,已自动修正。'); + } else { + $output->writeln(' 结果:检测到配置不一致,请使用 --fix 自动修正,或手动更新数据库。'); + } + } else { + $output->writeln(' 结果:所有配置与 PRD 一致,验收通过 ✓'); + } + $output->writeln('========================================================'); + $output->writeln(''); + + return $hasError ? 1 : 0; + } +} diff --git a/pro_v3.5.1/app/controller/api/v1/hjf/HjfAssets.php b/pro_v3.5.1/app/controller/api/v1/hjf/HjfAssets.php index e2b36fd5..46a906d5 100644 --- a/pro_v3.5.1/app/controller/api/v1/hjf/HjfAssets.php +++ b/pro_v3.5.1/app/controller/api/v1/hjf/HjfAssets.php @@ -39,14 +39,17 @@ class HjfAssets $frozenPoints = (int)($user['frozen_points'] ?? 0); $todayRelease = (int)floor($frozenPoints * 0.0004); + $availablePoints = (int)($user['available_points'] ?? 0); + return app('json')->successful([ - 'brokerage_price' => (string)($user['brokerage_price'] ?? '0.00'), - 'now_money' => (string)($user['now_money'] ?? '0.00'), - 'frozen_points' => $frozenPoints, - 'available_points' => (int)($user['available_points'] ?? 0), - 'today_release' => $todayRelease, - 'agent_level' => (int)($user['agent_level'] ?? 0), - 'agent_level_name' => $agentLevelName, + 'brokerage_price' => (string)($user['brokerage_price'] ?? '0.00'), + 'now_money' => (string)($user['now_money'] ?? '0.00'), + 'frozen_points' => $frozenPoints, + 'available_points' => $availablePoints, + 'today_release' => $todayRelease, + 'total_points_earned' => $frozenPoints + $availablePoints, + 'agent_level' => (int)($user['agent_level'] ?? 0), + 'agent_level_name' => $agentLevelName, ]); } } diff --git a/pro_v3.5.1/app/jobs/hjf/HjfOrderPayJob.php b/pro_v3.5.1/app/jobs/hjf/HjfOrderPayJob.php index d8d1db18..a2ff5801 100644 --- a/pro_v3.5.1/app/jobs/hjf/HjfOrderPayJob.php +++ b/pro_v3.5.1/app/jobs/hjf/HjfOrderPayJob.php @@ -48,6 +48,7 @@ class HjfOrderPayJob extends BaseJobs return false; } + $preUpgradeLevels = []; try { /** @var UserServices $userServices */ $userServices = app()->make(UserServices::class); @@ -59,6 +60,13 @@ class HjfOrderPayJob extends BaseJobs } $uids = array_unique([$uid, $spreadUid, $twoSpreadUid]); + // fsgx B2:在升级前快照各上级用户的 agent_level,避免触发升级的那笔订单发放积分 + foreach ($uids as $u) { + if ($u <= 0) continue; + $uInfo = $userServices->get((int)$u, ['uid', 'agent_level']); + $preUpgradeLevels[(int)$u] = $uInfo ? (int)($uInfo['agent_level'] ?? 0) : 0; + } + /** @var AgentLevelServices $agentLevelServices */ $agentLevelServices = app()->make(AgentLevelServices::class); $agentLevelServices->checkUserLevelFinish($uid, $uids); @@ -76,10 +84,30 @@ class HjfOrderPayJob extends BaseJobs ->field('id,uid,is_queue_goods') ->find(); if ($orderRow) { + // fsgx B3:计算订单中报单商品的总数量,积分按数量倍乘 + $queueQty = 1; + try { + $cartRows = Db::name('store_order_cart_info') + ->where('oid', (int)$orderRow['id']) + ->column('cart_info'); + $qtySum = 0; + foreach ($cartRows as $row) { + $item = is_string($row) ? json_decode($row, true) : $row; + if (!empty($item['productInfo']['is_queue_goods'])) { + $qtySum += (int)($item['cart_num'] ?? 1); + } + } + if ($qtySum > 0) { + $queueQty = $qtySum; + } + } catch (\Throwable $qe) { + Log::warning("[HjfOrderPay] 计算报单商品数量异常,使用默认值1: " . $qe->getMessage()); + } + /** @var PointsRewardServices $pointsService */ $pointsService = app()->make(PointsRewardServices::class); - $pointsService->reward($uid, $orderId, (int)$orderRow['id']); - Log::info("[HjfOrderPay] 积分奖励发放完成 uid={$uid} orderId={$orderId}"); + $pointsService->reward($uid, $orderId, (int)$orderRow['id'], $preUpgradeLevels, $queueQty); + Log::info("[HjfOrderPay] 积分奖励发放完成 uid={$uid} orderId={$orderId} qty={$queueQty}"); } } catch (\Throwable $e) { Log::error("[HjfOrderPay] 积分奖励发放失败 uid={$uid} orderId={$orderId}: " . $e->getMessage()); diff --git a/pro_v3.5.1/app/listener/order/Pay.php b/pro_v3.5.1/app/listener/order/Pay.php index 8a8473f0..0ae1409b 100644 --- a/pro_v3.5.1/app/listener/order/Pay.php +++ b/pro_v3.5.1/app/listener/order/Pay.php @@ -175,15 +175,42 @@ class Pay implements ListenerInterface } $uids = array_filter(array_unique([$uid, $spreadUid, $twoSpreadUid])); + // fsgx B2:在升级前快照各上级用户的 agent_level,避免触发升级的那笔订单发放积分 + $preUpgradeLevels = []; + foreach ($uids as $u) { + $uInfo = $userServices->get((int)$u, ['uid', 'agent_level']); + $preUpgradeLevels[(int)$u] = $uInfo ? (int)($uInfo['agent_level'] ?? 0) : 0; + } + /** @var AgentLevelServices $agentLevelServices */ $agentLevelServices = app()->make(AgentLevelServices::class); $agentLevelServices->checkUserLevelFinish($uid, array_values($uids)); + // fsgx B3:计算订单中报单商品的总数量,积分按数量倍乘 + $queueQty = 1; + try { + /** @var StoreOrderCartInfoServices $cartSvc */ + $cartSvc = app()->make(StoreOrderCartInfoServices::class); + $cartRows = $cartSvc->getColumn(['oid' => (int)$orderInfo['id']], 'cart_info'); + $qtySum = 0; + foreach ($cartRows as $row) { + $item = is_string($row) ? json_decode($row, true) : $row; + if (!empty($item['productInfo']['is_queue_goods'])) { + $qtySum += (int)($item['cart_num'] ?? 1); + } + } + if ($qtySum > 0) { + $queueQty = $qtySum; + } + } catch (\Throwable $qe) { + Log::warning('[Pay] 计算报单商品数量异常,使用默认值1: ' . $qe->getMessage()); + } + /** @var PointsRewardServices $pointsService */ $pointsService = app()->make(PointsRewardServices::class); - $pointsService->reward($uid, (string)$orderInfo['order_id'], (int)$orderInfo['id']); + $pointsService->reward($uid, (string)$orderInfo['order_id'], (int)$orderInfo['id'], $preUpgradeLevels, $queueQty); - Log::info('[Pay] 同步积分奖励发放完成 uid=' . $uid . ' order_id=' . $orderInfo['id']); + Log::info('[Pay] 同步积分奖励发放完成 uid=' . $uid . ' order_id=' . $orderInfo['id'] . ' qty=' . $queueQty); } catch (\Throwable $e) { Log::error('[Pay] 同步积分奖励失败 order_id=' . $orderInfo['id'] . ': ' . $e->getMessage()); } diff --git a/pro_v3.5.1/app/services/agent/AgentLevelTaskServices.php b/pro_v3.5.1/app/services/agent/AgentLevelTaskServices.php index f946c1c0..94044b04 100644 --- a/pro_v3.5.1/app/services/agent/AgentLevelTaskServices.php +++ b/pro_v3.5.1/app/services/agent/AgentLevelTaskServices.php @@ -437,11 +437,13 @@ class AgentLevelTaskServices extends BaseServices if (empty($directUids)) { return 0; } + // fsgx B5:补充 refund_status 检查,与其他任务类型保持一致,排除已全额退款订单 return (int)Db::name('store_order') ->whereIn('uid', $directUids) ->where('is_queue_goods', 1) ->where('paid', 1) ->where('is_del', 0) + ->whereIn('refund_status', [0, 3]) ->count(); } @@ -490,11 +492,13 @@ class AgentLevelTaskServices extends BaseServices if ($childGrade >= 2) { continue; } + // fsgx B5:补充 refund_status 检查,排除已全额退款订单 $total += (int)Db::name('store_order') ->where('uid', $child['uid']) ->where('is_queue_goods', 1) ->where('paid', 1) ->where('is_del', 0) + ->whereIn('refund_status', [0, 3]) ->count(); $total += $this->recursiveUmbrellaCount((int)$child['uid'], $remainDepth - 1); } diff --git a/pro_v3.5.1/app/services/hjf/PointsRewardServices.php b/pro_v3.5.1/app/services/hjf/PointsRewardServices.php index 9adb221f..5962329d 100644 --- a/pro_v3.5.1/app/services/hjf/PointsRewardServices.php +++ b/pro_v3.5.1/app/services/hjf/PointsRewardServices.php @@ -40,10 +40,13 @@ class PointsRewardServices extends BaseServices /** * 对一笔报单订单发放积分奖励 * - * @param int $orderDbId 订单表主键 id,用于 user_bill.link_id 关联后台订单 + * @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): void + 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') @@ -59,7 +62,7 @@ class PointsRewardServices extends BaseServices if (!$buyer || !$buyer['spread_uid']) { return; } - $this->propagateReward($buyer['spread_uid'], $orderUid, $orderId, 0, 0, $orderDbId); + $this->propagateReward($buyer['spread_uid'], $orderUid, $orderId, 0, 0, $orderDbId, $preUpgradeLevels, $qty); } catch (\Throwable $e) { Log::error("[PointsReward] 积分奖励失败 orderUid={$orderUid} orderId={$orderId}: " . $e->getMessage()); } @@ -68,11 +71,13 @@ class PointsRewardServices extends BaseServices /** * 向上递归发放级差积分 * - * @param int $uid 当前被奖励用户 - * @param int $fromUid 触发方(下级)用户 ID - * @param string $orderId 来源订单号 - * @param int $lowerReward 下级已获得的直推/伞下奖励积分(用于级差扣减) - * @param int $depth 递归深度 + * @param int $uid 当前被奖励用户 + * @param int $fromUid 触发方(下级)用户 ID + * @param string $orderId 来源订单号 + * @param int $lowerReward 下级已获得的伞下奖励积分(用于级差扣减) + * @param int $depth 递归深度 + * @param array $preUpgradeLevels 升级前各用户的 agent_level 快照 [uid => level_id] + * @param int $qty 报单商品数量,积分按数量倍乘(B3) */ private function propagateReward( int $uid, @@ -80,7 +85,9 @@ class PointsRewardServices extends BaseServices string $orderId, int $lowerReward, int $depth = 0, - int $orderDbId = 0 + int $orderDbId = 0, + array $preUpgradeLevels = [], + int $qty = 1 ): void { if ($depth >= 10 || $uid <= 0) { return; @@ -91,22 +98,33 @@ class PointsRewardServices extends BaseServices return; } - $agentLevelId = (int)($user['agent_level'] ?? 0); + // 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) { if ($user['spread_uid']) { - $this->propagateReward((int)$user['spread_uid'], $uid, $orderId, 0, $depth + 1, $orderDbId); + $this->propagateReward((int)$user['spread_uid'], $uid, $orderId, 0, $depth + 1, $orderDbId, $preUpgradeLevels, $qty); } return; } $isDirect = ($depth === 0); - $reward = $isDirect - ? $this->agentLevelServices->getDirectRewardPoints($agentLevelId) - : $this->agentLevelServices->getUmbrellaRewardPoints($agentLevelId); + // fsgx B4:直推奖励和伞下奖励分别读取,级差计算只用伞下奖励做扣减 + // 这样当中间层(如创客,umbrella=0)上报时,上级(云店,umbrella=300) + // 计算 max(0, 300-0)=300,而不会被直推奖励 500 错误抵消 + $directReward = $this->agentLevelServices->getDirectRewardPoints($agentLevelId); + $umbrellaReward = $this->agentLevelServices->getUmbrellaRewardPoints($agentLevelId); - $actual = max(0, $reward - $lowerReward); + if ($isDirect) { + // fsgx B3:直推奖励按报单商品数量倍乘 + $actual = $directReward * $qty; + } else { + // 级差:基于单件奖励做差值,再乘以数量 + $actual = max(0, $umbrellaReward - $lowerReward) * $qty; + } if ($actual > 0) { $this->grantFrozenPoints( @@ -114,7 +132,7 @@ class PointsRewardServices extends BaseServices $actual, $orderId, $isDirect ? 'reward_direct' : 'reward_umbrella', - ($isDirect ? '直推奖励' : '伞下奖励(级差)') . " - 来源订单 {$orderId}", + ($isDirect ? '直推奖励' : '伞下奖励(级差)') . " x{$qty} - 来源订单 {$orderId}", $orderDbId ); } @@ -124,9 +142,11 @@ class PointsRewardServices extends BaseServices (int)$user['spread_uid'], $uid, $orderId, - $reward, + $umbrellaReward, // 向上传单件伞下奖励(级差基数),让上级自行乘以 $qty $depth + 1, - $orderDbId + $orderDbId, + $preUpgradeLevels, + $qty ); } } diff --git a/pro_v3.5.1/app/services/order/StoreOrderCreateServices.php b/pro_v3.5.1/app/services/order/StoreOrderCreateServices.php index 8ef1a5c9..bd461417 100644 --- a/pro_v3.5.1/app/services/order/StoreOrderCreateServices.php +++ b/pro_v3.5.1/app/services/order/StoreOrderCreateServices.php @@ -1002,22 +1002,42 @@ class StoreOrderCreateServices extends BaseServices $useCycleBrokerage = ($cycleCount > 0 && is_array($cycleRates) && count($cycleRates) > 0 && $isQueueGoods === 1); if ($useCycleBrokerage && $spread_uid > 0) { - // 统计推荐人下级已完成的有效报单商品订单数,取模得到当前位次 - // 注意:compute() 在 paid=1 之后执行,当前订单已被计入,需 -1 得到"之前完成单数" - /** @var \app\dao\order\StoreOrderDao $orderDao */ - $orderDao = app()->make(\app\dao\order\StoreOrderDao::class); - $completedCount = $orderDao->count([ - 'spread_uid' => $spread_uid, - 'is_queue_goods' => 1, - 'paid' => 1, - 'is_del' => 0, - ]); - $position = max(0, $completedCount - 1) % $cycleCount; - $cycleRatePercent = isset($cycleRates[$position]) ? (int)$cycleRates[$position] : (int)($cycleRates[0] ?? 0); - if ($cycleRatePercent > 0) { - $brokerageRatio = bcdiv((string)$cycleRatePercent, 100, 4); - $oneBrokerage = bcmul((string)$price, (string)$brokerageRatio, 2); - } + // fsgx B1 + B6:用事务锁序列化位次计算,防止并发竞态导致两笔订单拿到相同位次 + $brokerageResult = \think\facade\Db::transaction(function () use ($spread_uid, $cycleCount, $cycleRates, $price) { + // 锁定推荐人行,确保同一推荐人同时只有一个事务在计算位次 + \think\facade\Db::name('user') + ->where('uid', $spread_uid) + ->lockWrite() + ->value('uid'); + + // fsgx B1:推荐人自己必须有报单商品订单,才能获得推荐返现佣金 + $spreaderOwnCount = (int)\think\facade\Db::name('store_order') + ->where('uid', $spread_uid) + ->where('is_queue_goods', 1) + ->where('paid', 1) + ->where('is_del', 0) + ->count(); + if ($spreaderOwnCount <= 0) { + return '0'; + } + + // fsgx B6:按 id ASC 排序取位次,比 count 更精确且在锁保护下无竞态 + // 当前订单在 paid=1 后已写入,这里取所有已完成订单并按序找到本单排名 + $completedCount = (int)\think\facade\Db::name('store_order') + ->where('spread_uid', $spread_uid) + ->where('is_queue_goods', 1) + ->where('paid', 1) + ->where('is_del', 0) + ->count(); + + $position = max(0, $completedCount - 1) % $cycleCount; + $cycleRatePercent = isset($cycleRates[$position]) ? (int)$cycleRates[$position] : (int)($cycleRates[0] ?? 0); + if ($cycleRatePercent > 0) { + return bcmul((string)$price, bcdiv((string)$cycleRatePercent, '100', 4), 2); + } + return '0'; + }); + $oneBrokerage = $brokerageResult; } else { //一级返佣比例 小于等于零时直接返回 不返佣 if ($storeBrokerageRatio > 0) { diff --git a/pro_v3.5.1/config/console.php b/pro_v3.5.1/config/console.php index 96088e6b..c4d4cac6 100644 --- a/pro_v3.5.1/config/console.php +++ b/pro_v3.5.1/config/console.php @@ -25,5 +25,6 @@ return [ 'holiday_gift_push_task' => \app\command\HolidayGiftPushTask::class, 'hjf:release-points' => \app\command\HjfReleasePoints::class, 'hjf:patch-rewards' => \app\command\HjfPatchMissingRewards::class, + 'hjf:verify-agent-config' => \app\command\HjfVerifyAgentConfig::class, ], ]; diff --git a/pro_v3.5.1/view/uniapp_v2/pages/assets/index.vue b/pro_v3.5.1/view/uniapp_v2/pages/assets/index.vue index 123f461b..90ac0c38 100644 --- a/pro_v3.5.1/view/uniapp_v2/pages/assets/index.vue +++ b/pro_v3.5.1/view/uniapp_v2/pages/assets/index.vue @@ -1,5 +1,8 @@