Files
huangjingfen/pro_v3.5.1_副本/app/services/hjf/MemberLevelServices.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

247 lines
7.7 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\user\UserDao;
use app\services\BaseServices;
use crmeb\services\SystemConfigService;
use think\annotation\Inject;
use think\facade\Db;
use think\facade\Log;
/**
* 会员等级升级服务
*
* 升级条件PRD 3.2.1
* - 普通会员 → 创客直推3单hjf_level_direct_require_1默认3
* - 创客 → 云店伞下业绩30单 + 至少3个直推hjf_level_umbrella_require_2默认30
* - 云店 → 服务商伞下业绩100单 + 至少3个直推hjf_level_umbrella_require_3默认100
* - 服务商 → 分公司伞下业绩1000单 + 至少3个直推hjf_level_umbrella_require_4默认1000
*
* 伞下业绩分离当某直推下级已升级到云店level≥2
* 该下级及其整个团队的业绩不再计入本级的伞下业绩。
*
* Class MemberLevelServices
* @package app\services\hjf
*/
class MemberLevelServices extends BaseServices
{
#[Inject]
protected UserDao $userDao;
/**
* 各等级升级所需直推单数0→1升级条件
*/
const DIRECT_REQUIRE_KEYS = [
1 => 'hjf_level_direct_require_1', // 普通→创客直推N单
];
/**
* 各等级升级所需伞下单数n-1→n升级条件n≥2
*/
const UMBRELLA_REQUIRE_KEYS = [
2 => 'hjf_level_umbrella_require_2', // 创客→云店
3 => 'hjf_level_umbrella_require_3', // 云店→服务商
4 => 'hjf_level_umbrella_require_4', // 服务商→分公司
];
/**
* 默认升级门槛
*/
const DEFAULT_DIRECT_REQUIRE = [1 => 3];
const DEFAULT_UMBRELLA_REQUIRE = [2 => 30, 3 => 100, 4 => 1000];
/**
* 最低直推人数要求云店及以上需要至少3个直推
*/
const MIN_DIRECT_SPREAD_COUNT = 3;
/**
* 检查并执行升级(异步触发入口)
*
* @param int $uid 被检查的用户 ID
*/
public function checkUpgrade(int $uid): void
{
try {
$user = $this->userDao->get($uid);
if (!$user) {
return;
}
$currentLevel = (int)($user['member_level'] ?? 0);
$nextLevel = $currentLevel + 1;
if ($nextLevel > 4) {
return; // 已是最高等级
}
$qualified = $this->checkLevelCondition($uid, $currentLevel, $nextLevel);
if ($qualified) {
$this->upgrade($uid, $nextLevel);
// 升级后继续检查是否可连续升级
$this->checkUpgrade($uid);
}
} catch (\Throwable $e) {
Log::error("[MemberLevel] checkUpgrade uid={$uid}: " . $e->getMessage());
}
}
/**
* 检查用户是否满足从 currentLevel 升到 nextLevel 的条件
*/
private function checkLevelCondition(int $uid, int $currentLevel, int $nextLevel): bool
{
if ($nextLevel === 1) {
// 普通→创客:统计直推报单数
$require = $this->getDirectRequire(1);
$count = $this->getDirectQueueOrderCount($uid);
return $count >= $require;
}
// 创客/云店/服务商→更高等级:伞下业绩 + 至少3个直推
$umbrellaRequire = $this->getUmbrellaRequire($nextLevel);
$umbrellaCount = $this->getUmbrellaQueueOrderCount($uid);
if ($umbrellaCount < $umbrellaRequire) {
return false;
}
// 需要至少3个直推对 level≥2 的升级)
$directCount = $this->getDirectSpreadCount($uid);
return $directCount >= self::MIN_DIRECT_SPREAD_COUNT;
}
/**
* 获取直推用户的报单订单数(直推层级 = 1 层)
*
* 报单商品标记:`is_queue_goods = 1`eb_store_order 中的字段)
*/
public function getDirectQueueOrderCount(int $uid): int
{
// 查询直推用户 uid 列表
$directUids = $this->userDao->getColumn(['spread_uid' => $uid], 'uid');
if (empty($directUids)) {
return 0;
}
return (int)Db::name('store_order')
->whereIn('uid', $directUids)
->where('is_queue_goods', 1)
->where('paid', 1)
->where('is_del', 0)
->count();
}
/**
* 获取直推人数
*/
public function getDirectSpreadCount(int $uid): int
{
return (int)$this->userDao->count(['spread_uid' => $uid]);
}
/**
* 获取伞下总报单订单数(含业绩分离逻辑)
*
* 业绩分离若某直推下级已升级为云店level≥2
* 则该下级及其团队的订单不计入本用户的伞下业绩。
*
* @param int $uid 统计对象用户 ID
* @param int $maxDepth 递归最大深度,防止死循环
*/
public function getUmbrellaQueueOrderCount(int $uid, int $maxDepth = 8): int
{
return $this->recursiveUmbrellaCount($uid, $maxDepth);
}
/**
* 递归统计伞下业绩DFS
*/
private function recursiveUmbrellaCount(int $uid, int $remainDepth): int
{
if ($remainDepth <= 0) {
return 0;
}
$directChildren = $this->userDao->selectList(
['spread_uid' => $uid],
'uid,member_level',
0, 0, 'uid', 'asc'
);
if (empty($directChildren)) {
return 0;
}
$total = 0;
foreach ($directChildren as $child) {
$childLevel = (int)($child['member_level'] ?? 0);
// 业绩分离直推下级已是云店或以上level≥2其团队业绩不计入本级
if ($childLevel >= 2) {
continue;
}
// 统计该下级自身的报单订单数
$total += (int)Db::name('store_order')
->where('uid', $child['uid'])
->where('is_queue_goods', 1)
->where('paid', 1)
->where('is_del', 0)
->count();
// 递归统计该下级的伞下
$total += $this->recursiveUmbrellaCount((int)$child['uid'], $remainDepth - 1);
}
return $total;
}
/**
* 执行升级
*
* @param int $uid 用户 ID
* @param int $newLevel 新等级
*/
public function upgrade(int $uid, int $newLevel): void
{
Db::transaction(function () use ($uid, $newLevel) {
$this->userDao->update($uid, ['member_level' => $newLevel], 'uid');
Log::info("[MemberLevel] uid={$uid} 升级到 level={$newLevel}");
});
// 升级后通知推荐链上级重新检查
$user = $this->userDao->get($uid);
if ($user && $user['spread_uid']) {
// 异步检查上级升级(防止递归过深直接调用)
try {
app(\app\jobs\hjf\MemberLevelCheckJob::class)::dispatch($user['spread_uid']);
} catch (\Throwable $e) {
Log::warning("[MemberLevel] 无法派发上级检查 Job: " . $e->getMessage());
}
}
}
private function getDirectRequire(int $level): int
{
$key = self::DIRECT_REQUIRE_KEYS[$level] ?? '';
if (!$key) {
return self::DEFAULT_DIRECT_REQUIRE[$level] ?? 3;
}
return (int)SystemConfigService::get($key, self::DEFAULT_DIRECT_REQUIRE[$level] ?? 3);
}
private function getUmbrellaRequire(int $level): int
{
$key = self::UMBRELLA_REQUIRE_KEYS[$level] ?? '';
if (!$key) {
return self::DEFAULT_UMBRELLA_REQUIRE[$level] ?? 9999;
}
return (int)SystemConfigService::get($key, self::DEFAULT_UMBRELLA_REQUIRE[$level] ?? 9999);
}
}