Files
huangjingfen/pro_v3.5.1_副本/app/services/hjf/PointsRewardServices.php
apple 434aa8c69d feat(fsgx): 完成全部24项开发任务 Phase1-7
Phase1 后端核心:
- 新增 fsgx_v1.sql 迁移脚本(is_queue_goods/frozen_points/available_points/no_assess)
- SystemConfigServices 返佣设置扩展(周期人数/分档比例/范围/时机)
- StoreOrderCreateServices 周期循环佣金计算
- StoreOrderTakeServices 佣金发放后同步冻结积分
- StoreProductServices/StoreProduct 保存 is_queue_goods

Phase2 后端接口:
- GET /api/hjf/brokerage/progress 佣金周期进度
- GET /api/hjf/assets/overview 资产总览
- HjfPointsServices 每日 frozen_points 0.4‰ 释放定时任务
- PUT /adminapi/hjf/member/{uid}/no_assess 不考核接口
- GET /adminapi/hjf/points/release_log 积分日志接口

Phase3 前端清理:
- hjfCustom.js 路由精简(仅保留 points/log)
- hjfQueue.js/hjfMember.js API 清理/重定向至 CRMEB 原生接口
- pages.json 公排→推荐佣金/佣金记录/佣金规则

Phase4-5 前端改造:
- queue/status.vue 推荐佣金进度页整体重写
- 商品详情/订单确认/支付结果页文案与逻辑改造
- 个人中心/资产页/引导页/规则页文案改造
- HjfQueueProgress/HjfRefundNotice/HjfAssetCard 组件改造
- 推广中心嵌入佣金进度摘要
- hjfMockData.js 全量更新(公排字段→佣金字段)

Phase6 Admin 增强:
- 用户列表新增 frozen_points/available_points 列及不考核操作按钮
- hjfPoints.js USE_MOCK=false 对接真实积分日志接口

Phase7 配置文档:
- docs/fsgx-phase7-config-checklist.md 后台配置与全链路验收清单

Made-with: Cursor
2026-03-23 22:32:19 +08:00

192 lines
6.1 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\BaseServices;
use crmeb\services\SystemConfigService;
use think\annotation\Inject;
use think\facade\Db;
use think\facade\Log;
/**
* 积分奖励服务(级差计算)
*
* 触发时机:报单商品订单支付回调成功后调用 reward($orderUid, $orderId)。
*
* 奖励规则PRD 3.2
* - 推荐人(直推上级)获得 直推奖励积分(按推荐人等级)
* - 更上级获得 级差积分(上级积分 - 直接下级已获得的积分)
* - 所有奖励积分写入 frozen_points待释放状态
* - 同时写 points_release_log 记录明细
*
* Class PointsRewardServices
* @package app\services\hjf
*/
class PointsRewardServices extends BaseServices
{
#[Inject]
protected PointsReleaseLogDao $logDao;
#[Inject]
protected UserDao $userDao;
/**
* 各等级直推奖励积分配置键
*/
const DIRECT_REWARD_KEYS = [
0 => 0, // 普通会员:无直推奖励
1 => 'hjf_reward_direct_1', // 创客
2 => 'hjf_reward_direct_2', // 云店
3 => 'hjf_reward_direct_3', // 服务商
4 => 'hjf_reward_direct_4', // 分公司
];
/**
* 各等级伞下奖励积分配置键
*/
const UMBRELLA_REWARD_KEYS = [
0 => 0,
1 => 'hjf_reward_umbrella_1',
2 => 'hjf_reward_umbrella_2',
3 => 'hjf_reward_umbrella_3',
4 => 'hjf_reward_umbrella_4',
];
/**
* 默认积分奖励(当系统配置未初始化时使用)
*/
const DEFAULT_DIRECT = [0 => 0, 1 => 500, 2 => 800, 3 => 1000, 4 => 1300];
const DEFAULT_UMBRELLA = [0 => 0, 1 => 0, 2 => 300, 3 => 200, 4 => 300];
/**
* 对一笔报单订单发放积分奖励
*
* @param int $orderUid 下单用户 ID
* @param string $orderId 订单号
*/
public function reward(int $orderUid, string $orderId): void
{
try {
// 获取下单用户信息
$buyer = $this->userDao->get($orderUid);
if (!$buyer || !$buyer['spread_uid']) {
return; // 无推荐人,不发奖励
}
// 沿推荐链向上遍历,计算级差奖励
$this->propagateReward($buyer['spread_uid'], $orderUid, $orderId, 0);
} catch (\Throwable $e) {
Log::error("[PointsReward] 积分奖励失败 orderUid={$orderUid} orderId={$orderId}: " . $e->getMessage());
}
}
/**
* 向上递归发放级差积分
*
* @param int $uid 当前被奖励用户
* @param int $fromUid 触发方(下级)用户 ID
* @param string $orderId 来源订单号
* @param int $lowerReward 下级已获得的直推/伞下奖励积分(用于级差扣减)
* @param int $depth 递归深度最多遍历10层
*/
private function propagateReward(
int $uid,
int $fromUid,
string $orderId,
int $lowerReward,
int $depth = 0
): void {
if ($depth >= 10 || $uid <= 0) {
return;
}
$user = $this->userDao->get($uid);
if (!$user) {
return;
}
$level = (int)($user['member_level'] ?? 0);
if ($level === 0) {
// 普通会员不获得奖励,但继续向上传递
if ($user['spread_uid']) {
$this->propagateReward((int)$user['spread_uid'], $uid, $orderId, 0, $depth + 1);
}
return;
}
// 判断是直推还是伞下depth=0 说明是第一个上级,即直推)
$isDirect = ($depth === 0);
$reward = $isDirect
? $this->getDirectReward($level)
: $this->getUmbrellaReward($level);
// 级差:本次实发 = 本等级应得 - 下级已获得
$actual = max(0, $reward - $lowerReward);
if ($actual > 0) {
$this->grantFrozenPoints(
$uid,
$actual,
$orderId,
$isDirect ? 'reward_direct' : 'reward_umbrella',
($isDirect ? '直推奖励' : '伞下奖励(级差)') . " - 来源订单 {$orderId}"
);
}
// 继续向上传递(使用本级应得的 reward 作为下一级的 lowerReward
if ($user['spread_uid']) {
$this->propagateReward(
(int)$user['spread_uid'],
$uid,
$orderId,
$reward, // 传递本级"应得"(而非实发)给上级做级差
$depth + 1
);
}
}
/**
* 写入待释放积分frozen_points并记录明细
*/
private function grantFrozenPoints(int $uid, int $points, string $orderId, string $type, string $mark): void
{
Db::transaction(function () use ($uid, $points, $orderId, $type, $mark) {
// 增加 frozen_points
$this->userDao->bcInc($uid, 'frozen_points', $points, 'uid');
// 写明细日志
$this->logDao->save([
'uid' => $uid,
'points' => $points,
'pm' => 1,
'type' => $type,
'title' => ($type === 'reward_direct') ? '直推奖励' : '伞下奖励',
'mark' => $mark,
'status' => 'frozen',
'order_id' => $orderId,
]);
});
}
private function getDirectReward(int $level): int
{
$key = self::DIRECT_REWARD_KEYS[$level] ?? 0;
if (!$key) {
return self::DEFAULT_DIRECT[$level] ?? 0;
}
return (int)SystemConfigService::get($key, self::DEFAULT_DIRECT[$level] ?? 0);
}
private function getUmbrellaReward(int $level): int
{
$key = self::UMBRELLA_REWARD_KEYS[$level] ?? 0;
if (!$key) {
return self::DEFAULT_UMBRELLA[$level] ?? 0;
}
return (int)SystemConfigService::get($key, self::DEFAULT_UMBRELLA[$level] ?? 0);
}
}