feat(hjf): H5路由修复、分销等级显示优化、个人中心等级徽章

H5 部署与路由:
- manifest.json: router.base 改为 "/" 适配 public/ 根目录部署
- nginx-crmeb.conf: 恢复与 feature/fsgx 一致的原始配置
- App.vue: PC端重定向路径改为动态推导,修复死循环加载问题
- static/html/pc.html: 动态推导 H5 根路径,适配本地/云端两种部署

H5登录:
- pages/users/login/index.vue: H5端获取验证码跳过安全验证(条件编译)

分销等级展示修复:
- AgentLevelServices: 新增 loadHjfUserListLevelMaps/pickHjfLevelRowForUserListDisplay
  统一等级名称解析逻辑,优先返回 HJF 官方名称;新增 getUpgradeTasksForLevel 封装
- UserServices/MemberLevelServices: 改用统一解析方法,修复 protected $dao 访问错误
- api/hjf/MemberController: 直接取 eb_agent_level.name,新增 agent_level 原始值返回
- admin/v1/hjf/MemberController: team() 改用封装方法替代直接访问 protected dao

个人中心等级徽章:
- pages/user/index.vue + member/index.vue: memberInfo 沿链路透传
- member/template1.vue: UID右侧显示HjfMemberBadge,直接读 userInfo.agent_level_name
  无需等待异步 memberInfo,agentLevelGrade 计算属性从名称推导颜色等级

商品列表修复:
- BaseController.php/Common.php: 恢复加密版,修复 CRMEB 授权检查失败导致的400错误
- StoreProduct model: 移除冲突的 model maker 回调

数据库:
- hjf_migration.sql: 完善会员等级体系迁移脚本
- eb_agent_level.sql: 新增等级初始数据脚本

Made-with: Cursor
This commit is contained in:
apple
2026-03-22 01:43:36 +08:00
parent 590eca8c22
commit 8592243d36
34 changed files with 1467 additions and 745 deletions

View File

@@ -27,7 +27,13 @@ use think\facade\Route as Url;
/**
* 分销等级
* 分销等级(改造后同时作为 HJF 会员等级服务)
*
* PRD 改造说明:
* - 将原有的"分销员等级"概念替换为"会员等级"
* - 升级条件从"推广订单数/消费金额"改为"直推单数 + 伞下业绩单数"(通过 task type 6/7/8 实现)
* - 佣金计算从"按比例返佣"改为"按等级发放固定积分"(通过 direct_reward_points / umbrella_reward_points 实现)
*
* Class AgentLevelServices
* @package app\services\agent
* @mixin AgentLevelDao
@@ -55,6 +61,88 @@ class AgentLevelServices extends BaseServices
return $this->dao->getOne(['id' => $id, 'is_del' => 0], $field, $with);
}
/**
* HJF 官方会员等级名称(与 database/hjf_migration.sql 插入数据一致)
* 用于区分 CRMEB 默认「等级一/等级二…」与 HJF 创客/云店…
*/
public const HJF_OFFICIAL_LEVEL_NAMES = ['创客', '云店', '服务商', '分公司'];
/**
* 一次查询并返回用户列表展示所需等级索引(供外部服务调用)
* 不暴露 dao 属性,避免外部直接访问 protected $dao
*
* @return array{byId: array<int,array>, byGradeAny: array<int,array>, byGradeOfficial: array<int,array>}
*/
public function loadHjfUserListLevelMaps(): array
{
$rows = $this->dao->getList(['is_del' => 0]);
return $this->buildHjfUserListLevelMaps($rows);
}
/**
* 从 is_del=0 的等级行列表构建用户列表展示用索引(一次查询后复用)
*
* @param array<int,array<string,mixed>> $hjfLevelRows
* @return array{byId: array<int,array>, byGradeAny: array<int,array>, byGradeOfficial: array<int,array>}
*/
public function buildHjfUserListLevelMaps(array $hjfLevelRows): array
{
$byId = [];
$byGradeAny = [];
$byGradeOfficial = [];
$official = self::HJF_OFFICIAL_LEVEL_NAMES;
foreach ($hjfLevelRows as $hjfRow) {
$lid = (int)($hjfRow['id'] ?? 0);
if ($lid > 0) {
$byId[$lid] = $hjfRow;
}
$g = (int)($hjfRow['grade'] ?? 0);
if ($g > 0 && !isset($byGradeAny[$g])) {
// dao 已 order grade asc,id desc同 grade 先出现者为较大 id
$byGradeAny[$g] = $hjfRow;
}
$nm = (string)($hjfRow['name'] ?? '');
if ($g > 0 && in_array($nm, $official, true) && !isset($byGradeOfficial[$g])) {
$byGradeOfficial[$g] = $hjfRow;
}
}
return [
'byId' => $byId,
'byGradeAny' => $byGradeAny,
'byGradeOfficial' => $byGradeOfficial,
];
}
/**
* 用户列表等场景:解析应展示的等级行
*
* - agent_level 指向 CRMEB 默认行(如 id=2「等级二」按 grade 改用 HJF 官方行(如「云店」)
* - 旧 id 已软删或误把 grade 写入 agent_level 时,按 byGradeAny 回退
*/
public function pickHjfLevelRowForUserListDisplay(int $agentLevelId, array $maps): ?array
{
if ($agentLevelId <= 0) {
return null;
}
$byId = $maps['byId'] ?? [];
$byGradeAny = $maps['byGradeAny'] ?? [];
$byGradeOfficial = $maps['byGradeOfficial'] ?? [];
$official = self::HJF_OFFICIAL_LEVEL_NAMES;
$row = $byId[$agentLevelId] ?? null;
if ($row === null) {
return $byGradeAny[$agentLevelId] ?? null;
}
$nm = (string)($row['name'] ?? '');
$g = (int)($row['grade'] ?? 0);
if ($g > 0 && !in_array($nm, $official, true) && isset($byGradeOfficial[$g])) {
return $byGradeOfficial[$g];
}
return $row;
}
/**
* 获取等级列表
* @param array $where
@@ -270,13 +358,10 @@ class AgentLevelServices extends BaseServices
}
/**
* 分销等级上浮
* @param int $uid
* @param array $userInfo
* @return array|int[]
* @throws \think\db\exception\DataNotFoundException
* @throws \think\db\exception\DbException
* @throws \think\db\exception\ModelNotFoundException
* 分销等级上浮(保留兼容,普通商品分销仍使用原逻辑)
*
* 注意:报单商品的积分奖励已由 PointsRewardServices 通过 direct_reward_points/umbrella_reward_points 处理,
* 此方法仅用于普通商品的分销佣金计算。
*/
public function getAgentLevelBrokerage(int $uid, $userInfo = [])
{
@@ -285,7 +370,6 @@ class AgentLevelServices extends BaseServices
if (!$uid) {
return $data;
}
//商城分销是否开启
if (!sys_config('brokerage_func_status')) {
return $data;
}
@@ -297,7 +381,6 @@ class AgentLevelServices extends BaseServices
if (!$userInfo) {
return $data;
}
//获取上级uid 开启自购返回自己uid
$spread_uid = $userServices->getSpreadUid($uid, $userInfo);
$one_agent_level = 0;
$two_agent_level = 0;
@@ -308,17 +391,66 @@ class AgentLevelServices extends BaseServices
$two_agent_level = $two_user_info['agent_level'] ?? 0;
}
}
//获取后台一级返佣比例
$storeBrokerageRatio = sys_config('store_brokerage_ratio');
//一级上浮之后的反佣比例
$storeBrokerageRatio = $one_agent_level ? bcadd($storeBrokerageRatio, bcmul($storeBrokerageRatio, bcdiv(($this->getLevelInfo($one_agent_level)['one_brokerage'] ?? 0), 100, 2), 2), 2) : $storeBrokerageRatio;
//获取二级返佣比例
$storeBrokerageTwo = sys_config('store_brokerage_two');
//二级上浮之后的反佣比例
$storeBrokerageTwo = $two_agent_level ? bcadd($storeBrokerageTwo, bcmul($storeBrokerageTwo, bcdiv(($this->getLevelInfo($two_agent_level)['two_brokerage'] ?? 0), 100, 2), 2), 2) : $storeBrokerageTwo;
return [$storeBrokerageRatio, $storeBrokerageTwo, $spread_uid, $spread_two_uid];
}
/**
* 根据 agent_level ID 获取等级 gradeHJF 会员等级数字 0-4
*
* @param int $agentLevelId eb_user.agent_level 值
* @return int grade0=普通会员, 1=创客, 2=云店, 3=服务商, 4=分公司)
*/
public function getGradeByLevelId(int $agentLevelId): int
{
if ($agentLevelId <= 0) {
return 0;
}
$levelInfo = $this->getLevelInfo($agentLevelId);
return (int)($levelInfo['grade'] ?? 0);
}
/**
* 根据 agent_level ID 获取直推奖励积分
*/
public function getDirectRewardPoints(int $agentLevelId): int
{
if ($agentLevelId <= 0) {
return 0;
}
$levelInfo = $this->getLevelInfo($agentLevelId);
return (int)($levelInfo['direct_reward_points'] ?? 0);
}
/**
* 根据 agent_level ID 获取伞下奖励积分
*/
public function getUmbrellaRewardPoints(int $agentLevelId): int
{
if ($agentLevelId <= 0) {
return 0;
}
$levelInfo = $this->getLevelInfo($agentLevelId);
return (int)($levelInfo['umbrella_reward_points'] ?? 0);
}
/**
* 根据 grade 获取 agent_level ID
*
* @param int $grade 等级数字 (1=创客, 2=云店, 3=服务商, 4=分公司)
* @return int agent_level ID找不到返回 0
*/
public function getLevelIdByGrade(int $grade): int
{
if ($grade <= 0) {
return 0;
}
return (int)$this->dao->value(['grade' => $grade, 'is_del' => 0, 'status' => 1], 'id') ?: 0;
}
/**
* 计算一二级返佣比率上浮
* @param $ratio
@@ -338,10 +470,7 @@ class AgentLevelServices extends BaseServices
}
/**
* 添加等级表单
* @param int $id
* @return array
* @throws \FormBuilder\Exception\FormBuilderException
* 添加等级表单(改造后包含积分奖励字段)
*/
public function createForm()
{
@@ -352,17 +481,16 @@ class AgentLevelServices extends BaseServices
$field[] = Form::number('grade', '等级:', 0)->min(0)->precision(0);
$field[] = Form::frameImage('image', '背景图:', Url::buildUrl('admin/widget.images/index', array('fodder' => 'image')))->icon('ios-add')->width('960px')->height('505px')->modal(['footer-hide' => true])->appendValidate(Iview::validateStr()->required()->message('请选择背景图'));
$field[] = Form::color('color', '字体颜色:')->required('请选择字体颜色');
$field[] = Form::number('direct_reward_points', '直推奖励积分:', 0)->info('该等级会员每直推1单报单商品获得的冻结积分数')->min(0);
$field[] = Form::number('umbrella_reward_points', '伞下奖励积分:', 0)->info('该等级会员伞下每入1单报单商品获得的冻结积分数级差基数')->min(0);
$field[] = Form::number('one_brokerage', '一级上浮:', 0)->info('在分销一级佣金基础上浮0-1000之间整数百分比目前一级返佣比率' . $store_brokerage_ratio . '%例如上浮10%,则返佣比率:一级返佣比率 * (1 + 一级上浮比率) = ' . $this->compoteBrokerage($store_brokerage_ratio, 10) . '%')->min(0)->max(1000);
$field[] = Form::number('two_brokerage', '二级上浮:', 0)->info('在分销二级佣金基础上浮0-1000之间整数百分比目前二级返佣比率' . $store_brokerage_two . '%例如上浮10%,则返佣比率:二级返佣比率 * (1 + 二级上浮比率) = ' . $this->compoteBrokerage($store_brokerage_two, 10) . '%')->min(0)->max(1000);
$field[] = Form::radio('status', '是否显示:', 1)->options([['value' => 1, 'label' => '显示'], ['value' => 0, 'label' => '隐藏']]);
return create_form('添加分销员等级', $field, Url::buildUrl('/agent/level'), 'POST');
return create_form('添加员等级', $field, Url::buildUrl('/agent/level'), 'POST');
}
/**
* 获取修改等级表单
* @param int $id
* @return array
* @throws \FormBuilder\Exception\FormBuilderException
* 获取修改等级表单(改造后包含积分奖励字段)
*/
public function editForm(int $id)
{
@@ -377,11 +505,13 @@ class AgentLevelServices extends BaseServices
$field[] = Form::number('grade', '等级', $levelInfo['grade'])->min(0)->precision(0);
$field[] = Form::frameImage('image', '背景图', Url::buildUrl('admin/widget.images/index', array('fodder' => 'image')), $levelInfo['image'])->icon('ios-add')->width('960px')->height('505px')->modal(['footer-hide' => true])->appendValidate(Iview::validateStr()->required()->message('请选择背景图'));
$field[] = Form::color('color', '字体颜色', $levelInfo['color'] ?? '')->required('请选择字体颜色');
$field[] = Form::number('direct_reward_points', '直推奖励积分', $levelInfo['direct_reward_points'] ?? 0)->info('该等级会员每直推1单报单商品获得的冻结积分数')->min(0);
$field[] = Form::number('umbrella_reward_points', '伞下奖励积分', $levelInfo['umbrella_reward_points'] ?? 0)->info('该等级会员伞下每入1单报单商品获得的冻结积分数级差基数')->min(0);
$field[] = Form::number('one_brokerage', '一级上浮', $levelInfo['one_brokerage'])->info('在分销一级佣金基础上浮0-1000之间整数百分比目前一级返佣比率' . $store_brokerage_ratio . '%,上浮' . $levelInfo['one_brokerage'] . '%,则返佣比率:一级返佣比率 * (1 + 一级上浮比率) = ' . $this->compoteBrokerage($store_brokerage_ratio, $levelInfo['one_brokerage']) . '%')->min(0)->max(1000);
$field[] = Form::number('two_brokerage', '二级上浮', $levelInfo['two_brokerage'])->info('在分销二级佣金基础上浮0-1000之间整数百分比目前二级返佣比率' . $store_brokerage_two . '%,上浮' . $levelInfo['two_brokerage'] . '%,则返佣比率:二级返佣比率 * (1 + 二级上浮比率) = ' . $this->compoteBrokerage($store_brokerage_two, $levelInfo['two_brokerage']) . '%')->min(0)->max(1000);
$field[] = Form::radio('status', '是否显示', $levelInfo['status'])->options([['value' => 1, 'label' => '显示'], ['value' => 0, 'label' => '隐藏']]);
return create_form('编辑分销员等级', $field, Url::buildUrl('/agent/level/' . $id), 'PUT');
return create_form('编辑员等级', $field, Url::buildUrl('/agent/level/' . $id), 'PUT');
}
/**

View File

@@ -20,10 +20,12 @@ use crmeb\services\FormBuilder as Form;
use FormBuilder\Factory\Iview;
use think\annotation\Inject;
use think\exception\ValidateException;
use think\facade\Db;
use think\facade\Route as Url;
/**
* 分销等级任务(改造后同时支持 HJF 会员等级升级任务)
*
* Class AgentLevelTaskServices
* @package app\services\agent
@@ -33,12 +35,13 @@ class AgentLevelTaskServices extends BaseServices
{
/**
* 任务类型
* type 记录在数据库中用来区分任务
* name 任务名 (任务名中的{$num}会自动替换成设置的数字 + 单位)
* max_number 最大设定数值 0为不限定
* min_number 最小设定数值
* unit 单位
* */
*
* type 1-5: 原 CRMEB 分销任务类型
* type 6-8: HJF 会员等级升级任务类型(改造新增)
* 6 = 直推报单单数(直推下级购买报单商品的订单数)
* 7 = 伞下报单业绩(含业绩分离逻辑)
* 8 = 最低直推人数
*/
protected array $TaskType = [
[
'type' => 1,
@@ -90,6 +93,36 @@ class AgentLevelTaskServices extends BaseServices
'unit' => '单',
'image' => '/uploads/system/agent_spread_order.png'
],
[
'type' => 6,
'method' => 'directQueueOrderCount',
'name' => '直推报单满{$num}',
'real_name' => '直推报单单数',
'max_number' => 0,
'min_number' => 1,
'unit' => '单',
'image' => '/uploads/system/agent_spread_order.png'
],
[
'type' => 7,
'method' => 'umbrellaQueueOrderCount',
'name' => '伞下报单满{$num}',
'real_name' => '伞下报单业绩',
'max_number' => 0,
'min_number' => 1,
'unit' => '单',
'image' => '/uploads/system/agent_spread_order.png'
],
[
'type' => 8,
'method' => 'directSpreadCount',
'name' => '至少{$num}个直推',
'real_name' => '最低直推人数',
'max_number' => 0,
'min_number' => 1,
'unit' => '人',
'image' => '/uploads/system/agent_spread.png'
],
];
/**
@@ -299,12 +332,15 @@ class AgentLevelTaskServices extends BaseServices
/**
* 检测某个任务完成情况
*
* type 1-5: 原 CRMEB 分销任务
* type 6: 直推报单单数HJF 改造)
* type 7: 伞下报单业绩含业绩分离HJF 改造)
* type 8: 最低直推人数HJF 改造)
*
* @param int $uid
* @param int $task_id
* @return array|false
* @throws \think\db\exception\DataNotFoundException
* @throws \think\db\exception\DbException
* @throws \think\db\exception\ModelNotFoundException
*/
public function checkLevelTaskFinish(int $uid, int $task_id, $levelTaskInfo = [])
{
@@ -356,6 +392,17 @@ class AgentLevelTaskServices extends BaseServices
$userNumber = $storeOrderServices->count($where);
}
break;
case 6:
$userNumber = $this->getDirectQueueOrderCount($uid);
break;
case 7:
$userNumber = $this->getUmbrellaQueueOrderCount($uid);
break;
case 8:
/** @var UserServices $userServices */
$userServices = app()->make(UserServices::class);
$userNumber = $userServices->count(['spread_uid' => $uid]);
break;
default:
return false;
}
@@ -372,6 +419,104 @@ class AgentLevelTaskServices extends BaseServices
return [$msg, $userNumber, $isComplete];
}
/**
* 获取指定等级的升级任务列表(不分页,供外部服务/控制器调用,避免直接访问 protected $dao
*
* @param int $level_id eb_agent_level.id
* @return array
*/
public function getUpgradeTasksForLevel(int $level_id): array
{
if ($level_id <= 0) {
return [];
}
return $this->dao->getTaskList(['level_id' => $level_id, 'is_del' => 0, 'status' => 1]) ?: [];
}
/**
* 统计直推下级的报单订单数type=6 任务)
*
* @param int $uid 用户 ID
* @return int
*/
public function getDirectQueueOrderCount(int $uid): int
{
/** @var UserServices $userServices */
$userServices = app()->make(UserServices::class);
$directUids = $userServices->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();
}
/**
* 统计伞下报单业绩type=7 任务,含业绩分离逻辑)
*
* 业绩分离若某直推下级已升级为云店或更高grade≥2
* 则该下级及其团队的订单不计入本用户的伞下业绩。
*
* @param int $uid 用户 ID
* @param int $maxDepth 递归最大深度
* @return int
*/
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 = Db::name('user')
->where('spread_uid', $uid)
->field('uid,agent_level')
->select()
->toArray();
if (empty($directChildren)) {
return 0;
}
/** @var AgentLevelServices $levelServices */
$levelServices = app()->make(AgentLevelServices::class);
$total = 0;
foreach ($directChildren as $child) {
$childGrade = 0;
if (!empty($child['agent_level'])) {
$childLevelInfo = $levelServices->getLevelInfo((int)$child['agent_level']);
$childGrade = (int)($childLevelInfo['grade'] ?? 0);
}
if ($childGrade >= 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 $id

View File

@@ -3,24 +3,23 @@ declare(strict_types=1);
namespace app\services\hjf;
use app\dao\user\UserDao;
use app\services\agent\AgentLevelServices;
use app\services\agent\AgentLevelTaskServices;
use app\services\BaseServices;
use crmeb\services\SystemConfigService;
use app\services\user\UserServices;
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
* 基于 CRMEB Pro 的团队分销等级功能进行改造
* - 使用 eb_user.agent_level (FK → eb_agent_level.id) 代替独立的 member_level
* - 升级条件通过 eb_agent_level_task 的 type 6/7/8 定义
* - 升级逻辑委托给 AgentLevelServices::checkUserLevelFinish()
*
* 伞下业绩分离当某直推下级已升级到云店level≥2
* 该下级及其整个团队的业绩不再计入本级的伞下业绩。
* 本服务保留为薄封装层,提供 HJF 特有的查询方法供控制器调用。
*
* Class MemberLevelServices
* @package app\services\hjf
@@ -28,111 +27,70 @@ use think\facade\Log;
class MemberLevelServices extends BaseServices
{
#[Inject]
protected UserDao $userDao;
protected AgentLevelServices $agentLevelServices;
/**
* 各等级升级所需直推单数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;
#[Inject]
protected AgentLevelTaskServices $agentLevelTaskServices;
/**
* 检查并执行升级(异步触发入口)
*
* @param int $uid 被检查的用户 ID
* 委托给 CRMEB 的 AgentLevelServices 复用原有升级检测流程,
* 该流程已支持 type 6/7/8 的 HJF 任务类型。
*/
public function checkUpgrade(int $uid): void
{
try {
$user = $this->userDao->get($uid);
if (!$user) {
/** @var UserServices $userServices */
$userServices = app()->make(UserServices::class);
$userInfo = $userServices->getUserCacheInfo($uid);
if (!$userInfo) {
return;
}
$currentLevel = (int)($user['member_level'] ?? 0);
$nextLevel = $currentLevel + 1;
if ($nextLevel > 4) {
return; // 已是最高等级
$spreadUid = $userServices->getSpreadUid($uid, $userInfo);
$twoSpreadUid = 0;
if ($spreadUid > 0 && $oneUserInfo = $userServices->getUserCacheInfo($spreadUid)) {
$twoSpreadUid = $userServices->getSpreadUid($spreadUid, $oneUserInfo, false);
}
$uids = array_unique([$uid, $spreadUid, $twoSpreadUid]);
$qualified = $this->checkLevelCondition($uid, $currentLevel, $nextLevel);
if ($qualified) {
$this->upgrade($uid, $nextLevel);
// 升级后继续检查是否可连续升级
$this->checkUpgrade($uid);
}
$this->agentLevelServices->checkUserLevelFinish($uid, $uids);
} catch (\Throwable $e) {
Log::error("[MemberLevel] checkUpgrade uid={$uid}: " . $e->getMessage());
}
}
/**
* 检查用户是否满足从 currentLevel 升到 nextLevel 的条件
* 获取用户当前会员等级 grade0=普通, 1=创客, 2=云店, 3=服务商, 4=分公司)
*/
private function checkLevelCondition(int $uid, int $currentLevel, int $nextLevel): bool
public function getUserGrade(int $uid): int
{
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;
$agentLevel = (int)Db::name('user')->where('uid', $uid)->value('agent_level');
return $this->agentLevelServices->getGradeByLevelId($agentLevel);
}
/**
* 获取直推用户的报单订单数(直推层级 = 1 层)
*
* 报单商品标记:`is_queue_goods = 1`eb_store_order 中的字段)
* 获取用户当前等级名称
*/
public function getUserLevelName(int $uid): string
{
$agentLevel = (int)Db::name('user')->where('uid', $uid)->value('agent_level');
if ($agentLevel <= 0) {
return '普通会员';
}
$maps = $this->agentLevelServices->loadHjfUserListLevelMaps();
$info = $this->agentLevelServices->pickHjfLevelRowForUserListDisplay($agentLevel, $maps);
return $info['name'] ?? '普通会员';
}
/**
* 获取直推用户的报单订单数
*/
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();
return $this->agentLevelTaskServices->getDirectQueueOrderCount($uid);
}
/**
@@ -140,107 +98,39 @@ class MemberLevelServices extends BaseServices
*/
public function getDirectSpreadCount(int $uid): int
{
return (int)$this->userDao->count(['spread_uid' => $uid]);
/** @var UserServices $userServices */
$userServices = app()->make(UserServices::class);
return (int)$userServices->count(['spread_uid' => $uid]);
}
/**
* 获取伞下总报单订单数(含业绩分离逻辑)
*
* 业绩分离若某直推下级已升级为云店level≥2
* 则该下级及其团队的订单不计入本用户的伞下业绩。
*
* @param int $uid 统计对象用户 ID
* @param int $maxDepth 递归最大深度,防止死循环
*/
public function getUmbrellaQueueOrderCount(int $uid, int $maxDepth = 8): int
public function getUmbrellaQueueOrderCount(int $uid): int
{
return $this->recursiveUmbrellaCount($uid, $maxDepth);
return $this->agentLevelTaskServices->getUmbrellaQueueOrderCount($uid);
}
/**
* 递归统计伞下业绩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 新等级
* @param int $uid 用户 ID
* @param int $grade 目标等级 grade (0-4)
*/
public function upgrade(int $uid, int $newLevel): void
public function setUserLevel(int $uid, int $grade): 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());
$agentLevelId = 0;
if ($grade > 0) {
$agentLevelId = $this->agentLevelServices->getLevelIdByGrade($grade);
if ($agentLevelId <= 0) {
throw new \think\exception\ValidateException("等级 grade={$grade} 在 eb_agent_level 中不存在");
}
}
}
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);
}
/** @var UserServices $userServices */
$userServices = app()->make(UserServices::class);
$userServices->update($uid, ['agent_level' => $agentLevelId]);
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);
Log::info("[MemberLevel] 手动设置 uid={$uid} agent_level={$agentLevelId} (grade={$grade})");
}
}

View File

@@ -5,22 +5,19 @@ namespace app\services\hjf;
use app\dao\hjf\PointsReleaseLogDao;
use app\dao\user\UserDao;
use app\services\agent\AgentLevelServices;
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 记录明细
* 改造要点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
@@ -33,50 +30,19 @@ class PointsRewardServices extends BaseServices
#[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];
#[Inject]
protected AgentLevelServices $agentLevelServices;
/**
* 对一笔报单订单发放积分奖励
*
* @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; // 无推荐人,不发奖励
return;
}
// 沿推荐链向上遍历,计算级差奖励
$this->propagateReward($buyer['spread_uid'], $orderUid, $orderId, 0);
} catch (\Throwable $e) {
Log::error("[PointsReward] 积分奖励失败 orderUid={$orderUid} orderId={$orderId}: " . $e->getMessage());
@@ -86,11 +52,11 @@ class PointsRewardServices extends BaseServices
/**
* 向上递归发放级差积分
*
* @param int $uid 当前被奖励用户
* @param int $fromUid 触发方(下级)用户 ID
* @param string $orderId 来源订单号
* @param int $lowerReward 下级已获得的直推/伞下奖励积分(用于级差扣减)
* @param int $depth 递归深度最多遍历10层
* @param int $uid 当前被奖励用户
* @param int $fromUid 触发方(下级)用户 ID
* @param string $orderId 来源订单号
* @param int $lowerReward 下级已获得的直推/伞下奖励积分(用于级差扣减)
* @param int $depth 递归深度
*/
private function propagateReward(
int $uid,
@@ -108,22 +74,21 @@ class PointsRewardServices extends BaseServices
return;
}
$level = (int)($user['member_level'] ?? 0);
if ($level === 0) {
// 普通会员不获得奖励,但继续向上传递
$agentLevelId = (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);
}
return;
}
// 判断是直推还是伞下depth=0 说明是第一个上级,即直推)
$isDirect = ($depth === 0);
$reward = $isDirect
? $this->getDirectReward($level)
: $this->getUmbrellaReward($level);
$reward = $isDirect
? $this->agentLevelServices->getDirectRewardPoints($agentLevelId)
: $this->agentLevelServices->getUmbrellaRewardPoints($agentLevelId);
// 级差:本次实发 = 本等级应得 - 下级已获得
$actual = max(0, $reward - $lowerReward);
if ($actual > 0) {
@@ -136,13 +101,12 @@ class PointsRewardServices extends BaseServices
);
}
// 继续向上传递(使用本级应得的 reward 作为下一级的 lowerReward
if ($user['spread_uid']) {
$this->propagateReward(
(int)$user['spread_uid'],
$uid,
$orderId,
$reward, // 传递本级"应得"(而非实发)给上级做级差
$reward,
$depth + 1
);
}
@@ -154,10 +118,8 @@ class PointsRewardServices extends BaseServices
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,
@@ -170,22 +132,4 @@ class PointsRewardServices extends BaseServices
]);
});
}
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);
}
}

View File

@@ -766,23 +766,31 @@ class UserServices extends BaseServices
*/
public function index(array $where)
{
// 添加过滤条件
$where['is_filter_del'] = 1;
/** @var UserWechatuserServices $userWechatUser */
$userWechatUser = app()->make(UserWechatuserServices::class);
$fields = 'u.*,w.country,w.province,w.city,w.sex,w.unionid,w.openid,w.user_type as w_user_type,w.groupid,w.tagid_list,w.subscribe,w.subscribe_time';
try {
// 添加过滤条件
$where['is_filter_del'] = 1;
/** @var UserWechatuserServices $userWechatUser */
$userWechatUser = app()->make(UserWechatuserServices::class);
$fields = 'u.*,w.country,w.province,w.city,w.sex,w.unionid,w.openid,w.user_type as w_user_type,w.groupid,w.tagid_list,w.subscribe,w.subscribe_time';
// 获取用户列表
[$list, $count] = $userWechatUser->getWhereUserList($where, $fields);
} catch (\Throwable $e) {
Log::error('User index list query failed: ' . $e->getMessage());
if ($list) {
// 提取唯一 UID 列表
$uids = array_unique(array_column($list, 'uid'));
return ['count' => 0, 'list' => []];
}
// 获取相关服务数据
$userlabel = $this->getUserLablel($uids);
if (!$list) {
return compact('count', 'list');
}
try {
// 提取唯一 UID 列表
$uids = array_unique(array_column($list, 'uid'));
// 获取相关服务数据
$userlabel = $this->getUserLablel($uids);
$groupIds = array_unique(array_column($list, 'group_id'));
$userGroupService = app()->make(UserGroupServices::class);
$userGroup = $userGroupService->getUsersGroupName($groupIds);
@@ -799,6 +807,11 @@ class UserServices extends BaseServices
$clientData = $workClientService->getList(['uid' => $uids], ['id', 'uid', 'name', 'external_userid', 'corp_id', 'unionid'], false);
$clientlist = $clientData['list'] ?? [];
/** HJF分销等级展示索引is_del=0按 id/grade 双索引,优先 HJF 官方等级名称) */
/** @var AgentLevelServices $agentLevelServices */
$agentLevelServices = app()->make(AgentLevelServices::class);
$hjfLevelMaps = $agentLevelServices->loadHjfUserListLevelMaps();
// 补充信息
$extendInfo = SystemConfigService::get('user_extend_info', []);
$is_extend_info = false;
@@ -826,6 +839,13 @@ class UserServices extends BaseServices
// 补充每个用户的详细信息
foreach ($list as &$item) {
$agentLevelId = (int)($item['agent_level'] ?? 0);
$hjfLevelInfo = $agentLevelServices->pickHjfLevelRowForUserListDisplay($agentLevelId, $hjfLevelMaps);
$item['member_level'] = $hjfLevelInfo ? (int)$hjfLevelInfo['grade'] : 0;
$item['member_level_name'] = $hjfLevelInfo ? ($hjfLevelInfo['name'] ?? '') : '普通会员';
$item['available_points'] = (int)($item['available_points'] ?? 0);
$item['frozen_points'] = (int)($item['frozen_points'] ?? 0);
// 地址补充
if (empty($item['addres'])) {
if (!empty($item['country']) || !empty($item['province']) || !empty($item['city'])) {
@@ -895,13 +915,13 @@ class UserServices extends BaseServices
// 扩展信息标志
$item['is_extend_info'] = $is_extend_info;
}
}
return compact('count', 'list');
} catch (\Exception $e) {
// 异常处理
Log::error('Error in user index: ' . $e->getMessage());
return ['count' => 0, 'list' => []];
} catch (\Throwable $e) {
// 加工阶段失败时仍返回查询结果,避免整页空白(常见:企微/等级等扩展服务异常)
Log::error('User index enrichment failed: ' . $e->getMessage());
return compact('count', 'list');
}
}

View File

@@ -12,6 +12,7 @@ declare (strict_types=1);
namespace app\services\user;
use app\services\agent\AgentLevelServices;
use app\services\BaseServices;
use app\dao\user\UserWechatUserDao;
use think\annotation\Inject;
@@ -48,6 +49,7 @@ class UserWechatuserServices extends BaseServices
*/
public function getWhereUserList(array $where, string $field): array
{
$where = $this->normalizeHjfMemberLevelWhere($where);
[$page, $limit] = $this->getPageValue();
$order_string = '';
$order_arr = ['asc', 'desc'];
@@ -58,4 +60,40 @@ class UserWechatuserServices extends BaseServices
$count = $this->dao->getCountByWhere($where);
return [$list, $count];
}
/**
* 将会员列表筛选「HJF 等级grade」转为 eb_user.agent_level 条件,供 UserWechatUserDao 使用。
*/
protected function normalizeHjfMemberLevelWhere(array $where): array
{
if (!array_key_exists('hjf_member_level', $where)) {
return $where;
}
$raw = $where['hjf_member_level'];
if ($raw === null) {
unset($where['hjf_member_level']);
return $where;
}
if (is_string($raw)) {
$raw = trim($raw);
}
// 空串/仅空白:不按分销等级筛选(避免 (int)' '=>0 误加 agent_level=0
if ($raw === '') {
unset($where['hjf_member_level']);
return $where;
}
$grade = (int)$raw;
/** @var AgentLevelServices $agentLevel */
$agentLevel = app()->make(AgentLevelServices::class);
if ($grade === 0) {
$where['hjf_agent_level_id'] = 0;
} else {
$where['hjf_agent_level_id'] = $agentLevel->getLevelIdByGrade($grade) ?: -1;
}
unset($where['hjf_member_level']);
return $where;
}
}