Files
huangjingfen/pro_v3.5.1/app/services/agent/AgentLevelTaskServices.php
apple 8592243d36 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
2026-03-22 01:43:36 +08:00

567 lines
22 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
// +----------------------------------------------------------------------
// | CRMEB [ CRMEB赋能开发者助力企业发展 ]
// +----------------------------------------------------------------------
// | Copyright (c) 2016~2026 https://www.crmeb.com All rights reserved.
// +----------------------------------------------------------------------
// | Licensed CRMEB并不是自由软件未经许可不能去掉CRMEB相关版权
// +----------------------------------------------------------------------
// | Author: CRMEB Team <admin@crmeb.com>
// +----------------------------------------------------------------------
namespace app\services\agent;
use app\dao\agent\AgentLevelTaskDao;
use app\services\BaseServices;
use app\services\order\StoreOrderServices;
use app\services\user\UserServices;
use crmeb\exceptions\AdminException;
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
* @mixin AgentLevelTaskDao
*/
class AgentLevelTaskServices extends BaseServices
{
/**
* 任务类型
*
* type 1-5: 原 CRMEB 分销任务类型
* type 6-8: HJF 会员等级升级任务类型(改造新增)
* 6 = 直推报单单数(直推下级购买报单商品的订单数)
* 7 = 伞下报单业绩(含业绩分离逻辑)
* 8 = 最低直推人数
*/
protected array $TaskType = [
[
'type' => 1,
'method' => 'spread',
'name' => '邀请好友{$num}成为下级',
'real_name' => '邀请好友成为下级',
'max_number' => 0,
'min_number' => 1,
'unit' => '人',
'image' => '/uploads/system/agent_spread.png'
],
[
'type' => 2,
'method' => 'consumePrice',
'name' => '自身消费满{$num}',
'real_name' => '自身消费金额',
'max_number' => 0,
'min_number' => 0,
'unit' => '元',
'image' => '/uploads/system/agent_self_order_price.png'
],
[
'type' => 3,
'method' => 'consumeCount',
'name' => '自身消费满{$num}',
'real_name' => '自身消费单数',
'max_number' => 0,
'min_number' => 0,
'unit' => '单',
'image' => '/uploads/system/agent_self_order.png'
],
[
'type' => 4,
'method' => 'spreadConsumePrice',
'name' => '下级消费满{$num}',
'real_name' => '下级消费金额',
'max_number' => 0,
'min_number' => 0,
'unit' => '元',
'image' => '/uploads/system/agent_spread_order_price.png'
],
[
'type' => 5,
'method' => 'spreadConsumeCount',
'name' => '下级消费满{$num}',
'real_name' => '下级消费单数',
'max_number' => 0,
'min_number' => 0,
'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'
],
];
/**
* @var AgentLevelTaskDao
*/
#[Inject]
protected AgentLevelTaskDao $dao;
/**
* 获取某一个任务信息
* @param int $id
* @param string $field
* @param array $with
* @return array|\think\Model|null
* @throws \think\db\exception\DataNotFoundException
* @throws \think\db\exception\DbException
* @throws \think\db\exception\ModelNotFoundException
*/
public function getLevelTaskInfo(int $id, string $field = '*', array $with = [])
{
return $this->dao->getOne(['id' => $id, 'is_del' => 0], $field, $with);
}
/**
* 获取等级列表
* @param array $where
* @return array
* @throws \think\db\exception\DataNotFoundException
* @throws \think\db\exception\DbException
* @throws \think\db\exception\ModelNotFoundException
*/
public function getLevelTaskList(array $where)
{
$where['is_del'] = 0;
[$page, $limit] = $this->getPageValue();
$list = $this->dao->getTaskList($where, '*', [], $page, $limit);
if ($list) {
$allTyep = $this->getTaskTypeAll();
$allTyep = array_combine(array_column($allTyep, 'type'), $allTyep);
foreach ($list as &$item) {
$item['type_name'] = $allTyep[$item['type']]['real_name'] ?? '';
}
}
$count = $this->dao->count($where);
return compact('count', 'list');
}
/**
* 获取某个等级某个类型任务
* @param int $level_id
* @param int $type
* @return array|\think\Model|null
* @throws \think\db\exception\DataNotFoundException
* @throws \think\db\exception\DbException
* @throws \think\db\exception\ModelNotFoundException
*/
public function getLevelTypeTask(int $level_id, int $type = 1)
{
return $this->dao->get(['level_id' => $level_id, 'type' => $type, 'is_del' => 0]);
}
/**
* 添加等级任务表单
* @param int $id
* @return array
* @throws \FormBuilder\Exception\FormBuilderException
*/
public function createForm(int $level_id)
{
/** @var AgentLevelServices $levelServices */
$levelServices = app()->make(AgentLevelServices::class);
if (!$levelServices->getLevelInfo($level_id)) {
throw new AdminException('选择的等级不存在,请返回重新选择');
}
$taskList = $this->getTaskTypeAll();
$setOptionLabel = function () use ($taskList) {
$menus = [];
foreach ($taskList as $task) {
$menus[] = ['value' => $task['type'], 'label' => ($task['real_name'] ?? '') . '(' . ($task['unit'] ?? '') . ')'];
}
return $menus;
};
$field[] = Form::hidden('level_id', $level_id);
$field[] = Form::select('type', '任务类型:')->setOptions(Form::setOptions($setOptionLabel))->filterable(true)->info('请选择需要达成的任务类型')->appendValidate(Iview::validateInt()->message('请选择任务类型')->required());
$field[] = Form::number('number', '任务要求:', 0)->info('达成任务需要完成的人数、金额或订单数')->required('请填写任务要求')->min(0);
$field[] = Form::input('name', '任务名称:')->col(24)->required('请填写任务名称');
$field[] = Form::textarea('desc', '任务描述:');
$field[] = Form::number('sort', '排序:', 0)->min(0);
$field[] = Form::radio('status', '是否显示:', 1)->options([['value' => 1, 'label' => '显示'], ['value' => 0, 'label' => '隐藏']]);
return create_form('添加等级任务', $field, Url::buildUrl('/agent/level_task'), 'POST');
}
/**
* 获取修改任务数据
* @param int $id
* @return array
* @throws \FormBuilder\Exception\FormBuilderException
*/
public function editForm(int $id)
{
$levelTaskInfo = $this->getLevelTaskInfo($id);
if (!$levelTaskInfo)
throw new AdminException('数据不存在');
$field = [];
$field[] = Form::hidden('id', $id);
$taskList = $this->getTaskTypeAll();
$setOptionLabel = function () use ($taskList) {
$menus = [];
foreach ($taskList as $task) {
$menus[] = ['value' => $task['type'], 'label' => $task['real_name'] ?? '' . '(' . $task['unit'] ?? '' . ')'];
}
return $menus;
};
$field[] = Form::select('type', '任务类型', $levelTaskInfo['type'])->setOptions(Form::setOptions($setOptionLabel))->filterable(true)->info('请选择需要达成的任务类型')->appendValidate(Iview::validateInt()->message('请选择任务类型')->required());
$field[] = Form::number('number', '任务要求', $levelTaskInfo['number'])->min(0)->info('达成任务需要完成的人数、金额或订单数')->required('请填写任务要求');
$field[] = Form::input('name', '任务名称', $levelTaskInfo['name'])->required('请填写任务名称');
$field[] = Form::textarea('desc', '任务描述', $levelTaskInfo['desc']);
$field[] = Form::number('sort', '排序', $levelTaskInfo['sort'])->min(0);
$field[] = Form::radio('status', '是否显示', $levelTaskInfo['status'])->options([['value' => 1, 'label' => '显示'], ['value' => 0, 'label' => '隐藏']]);
return create_form('编辑等级任务', $field, Url::buildUrl('/agent/level_task/' . $id), 'PUT');
}
/**
* 获取任务类型
* @return array[]
*/
public function getTaskTypeAll()
{
return $this->TaskType;
}
/**
* 获取某个任务
* @param string $type 任务类型
* @return array
* */
public static function getTaskType($type)
{
foreach (self::$TaskType as $item) {
if ($item['type'] == $type) return $item;
}
}
/**
* 获取用户某一个分销等级任务情况
* @param int $uid
* @param int $level_id
* @return array
* @throws \think\db\exception\DataNotFoundException
* @throws \think\db\exception\DbException
* @throws \think\db\exception\ModelNotFoundException
*/
public function getUserLevelTaskList(int $uid, int $level_id)
{
//商城分销是否开启
if (!sys_config('brokerage_func_status')) {
return [];
}
/** @var UserServices $userServices */
$userServices = app()->make(UserServices::class);
$user = $userServices->getUserInfo($uid);
if (!$user) {
throw new ValidateException('没有此用户');
}
/** @var AgentLevelServices $levelServices */
$levelServices = app()->make(AgentLevelServices::class);
$levelInfo = $levelServices->getLevelInfo($level_id);
if (!$levelInfo) {//没有默认最低等级
$list = $levelServices->getList(['is_del' => 0, 'status' => 1]);
$levelInfo = $list[0] ?? [];
$level_id = $levelInfo['id'] ?? 0;
}
$taskList = $this->dao->getTaskList(['level_id' => $level_id, 'is_del' => 0, 'status' => 1]);
$speedAll = $speedCount = 0;
if ($taskList) {
$userLevel = [];
if ($user['agent_level'] ?? 0) $userLevel = $levelServices->getLevelInfo($user['agent_level']);
$allTyep = $this->getTaskTypeAll();
$allTyep = array_combine(array_column($allTyep, 'type'), $allTyep);
$baseUrl = sys_config('site_url');
foreach ($taskList as &$task) {
$task['finish'] = 1;
$task['task_type_title'] = '已完成';
$task['speed'] = 100;
$task['new_number'] = $task['number'];
//当前等级之前的等级任务 全部为完成
if (!$userLevel || $userLevel['grade'] < $levelInfo['grade']) {
[$title, $num, $isComplete] = $this->checkLevelTaskFinish($uid, (int)$task['id']);
if (!$isComplete) {
$scale = in_array($task['type'], [2, 4]) ? 2 : 0;
$task['finish'] = 0;
$numdata = bcsub($task['number'], $num, $scale);
$task['task_type_title'] = '还需' . str_replace('{$num}', $numdata . $allTyep[$task['type']]['unit'] ?? '', $title);
$task['speed'] = bcmul((string)bcdiv((string)$num, (string)$task['number'], 2), '100', 0);
$task['new_number'] = $num;
$task['image'] = $baseUrl . $allTyep[$task['type']]['image'] ?? '';
}
}
$speedCount = bcadd((string)$speedCount, $task['speed'], 0);
}
$speedAll = count($taskList) > 0 ? floatval(bcdiv($speedCount, (string)count($taskList), 2)) : 0;
}
return ['list' => $taskList, 'speedAll' => $speedAll];
}
/**
* 检测某个任务完成情况
*
* type 1-5: 原 CRMEB 分销任务
* type 6: 直推报单单数HJF 改造)
* type 7: 伞下报单业绩含业绩分离HJF 改造)
* type 8: 最低直推人数HJF 改造)
*
* @param int $uid
* @param int $task_id
* @return array|false
*/
public function checkLevelTaskFinish(int $uid, int $task_id, $levelTaskInfo = [])
{
if (!$levelTaskInfo) {
$levelTaskInfo = $this->getLevelTaskInfo($task_id);
}
if (!$levelTaskInfo) return false;
$allTyep = $this->getTaskTypeAll();
$allTyep = array_combine(array_column($allTyep, 'type'), $allTyep);
$userNumber = 0;
$msg = $allTyep[$levelTaskInfo['type']]['name'] ?? '';
switch ($levelTaskInfo['type']) {
case 1:
/** @var UserServices $userServices */
$userServices = app()->make(UserServices::class);
$userNumber = $userServices->count(['spread_uid' => $uid]);
break;
case 2:
/** @var StoreOrderServices $storeOrderServices */
$storeOrderServices = app()->make(StoreOrderServices::class);
$where = ['pid' => 0, 'uid' => $uid, 'paid' => 1, 'refund_status' => [0, 3], 'is_del' => 0, 'is_system_del' => 0];
$userNumber = $storeOrderServices->sum($where, 'pay_price');
break;
case 3:
/** @var StoreOrderServices $storeOrderServices */
$storeOrderServices = app()->make(StoreOrderServices::class);
$where = ['pid' => 0, 'uid' => $uid, 'paid' => 1, 'refund_status' => [0, 3], 'is_del' => 0, 'is_system_del' => 0];
$userNumber = $storeOrderServices->count($where);
break;
case 4:
/** @var UserServices $userServices */
$userServices = app()->make(UserServices::class);
$spread_uids = $userServices->getColumn(['spread_uid' => $uid], 'uid');
if ($spread_uids) {
/** @var StoreOrderServices $storeOrderServices */
$storeOrderServices = app()->make(StoreOrderServices::class);
$where = ['pid' => 0, 'uid' => $spread_uids, 'paid' => 1, 'refund_status' => [0, 3], 'is_del' => 0, 'is_system_del' => 0];
$userNumber = $storeOrderServices->sum($where, 'pay_price');
}
break;
case 5:
/** @var UserServices $userServices */
$userServices = app()->make(UserServices::class);
$spread_uids = $userServices->getColumn(['spread_uid' => $uid], 'uid');
if ($spread_uids) {
/** @var StoreOrderServices $storeOrderServices */
$storeOrderServices = app()->make(StoreOrderServices::class);
$where = ['pid' => 0, 'uid' => $spread_uids, 'paid' => 1, 'refund_status' => [0, 3], 'is_del' => 0, 'is_system_del' => 0];
$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;
}
$isComplete = false;
if ($userNumber >= $levelTaskInfo['number']) {
/** @var AgentLevelTaskRecordServices $agentLevelTaskRecordServices */
$agentLevelTaskRecordServices = app()->make(AgentLevelTaskRecordServices::class);
$isComplete = true;
if (!$agentLevelTaskRecordServices->get(['uid' => $uid, 'level_id' => $levelTaskInfo['level_id'], 'task_id' => $levelTaskInfo['id']])) {
$data = ['uid' => $uid, 'level_id' => $levelTaskInfo['level_id'], 'task_id' => $levelTaskInfo['id'], 'add_time' => time()];
$isComplete = $agentLevelTaskRecordServices->save($data);
}
}
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
* @param array $data
* @return bool
* @throws \think\db\exception\DataNotFoundException
* @throws \think\db\exception\DbException
* @throws \think\db\exception\ModelNotFoundException
*/
public function checkTypeTask(int $id, array $data)
{
if (!$id && (!isset($data['level_id']) || !$data['level_id'])) {
throw new ValidateException('缺少等级任务必要参数');
}
if ($id) {
$task = $this->getLevelTaskInfo($id);
if (!$task) {
throw new ValidateException('编辑的任务不存在');
}
$data['level_id'] = $task['level_id'];
}
/** @var AgentLevelServices $agentLevelServices */
$agentLevelServices = app()->make(AgentLevelServices::class);
$levelInfo = $agentLevelServices->getLevelInfo($data['level_id']);
if (!$levelInfo) {
throw new ValidateException('所选择的分销等级不存在或者删除,请重新选择');
}
$task = $this->dao->getOne(['level_id' => $data['level_id'], 'type' => $data['type'], 'is_del' => 0]);
if (($id && $task && $task['id'] != $id) || (!$id && $task)) {
throw new ValidateException('该等级已存在此类型任务');
}
$taskList = $this->dao->getTypTaskList($data['type']);
if ($taskList) {
foreach ($taskList as $taskInfo) {
if (is_null($taskInfo['grade'])) continue;
if ($levelInfo['grade'] > $taskInfo['grade'] && $data['number'] <= $taskInfo['number']) {
throw new ValidateException('不能小于低等级同类型任务限定数量');
}
if ($levelInfo['grade'] < $taskInfo['grade'] && $data['number'] >= $taskInfo['number']) {
throw new ValidateException('不能大于高等级同类型任务限定数量');
}
}
}
return true;
}
}