feat(fsgx): HJF queue merge, brokerage timing, cycle commission, points release
- Add HJF jobs, services, DAOs, models, admin/API controllers, release command - Respect brokerage_timing (on_pay vs confirm); dispatch HjfOrderPayJob for queue goods - Queue-only cycle commission and position index fix in StoreOrderCreateServices - UserBill income types: frozen_points_brokerage, frozen_points_release - Timer: fsgx_release_frozen_points -> PointsReleaseServices - Agent tasks: no_assess filtering for direct/umbrella counts - Migrations: queue_pool, points_release_log, fsgx_v1 checklist updates - Admin/uniapp: crontab preset, membership level, user list, finance routes, docs Made-with: Cursor
This commit is contained in:
49
pro_v3.5.1/app/command/HjfReleasePoints.php
Normal file
49
pro_v3.5.1/app/command/HjfReleasePoints.php
Normal file
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace app\command;
|
||||
|
||||
use app\services\hjf\PointsReleaseServices;
|
||||
use think\console\Command;
|
||||
use think\console\Input;
|
||||
use think\console\Output;
|
||||
|
||||
/**
|
||||
* 积分每日释放命令
|
||||
*
|
||||
* 用法:
|
||||
* php think hjf:release-points
|
||||
*
|
||||
* 触发时机:
|
||||
* - 每天凌晨 00:01 由 crontab 或 Swoole Timer 调用
|
||||
* - P4-05 联调验证时手动执行
|
||||
*
|
||||
* Class HjfReleasePoints
|
||||
* @package app\command
|
||||
*/
|
||||
class HjfReleasePoints extends Command
|
||||
{
|
||||
protected function configure(): void
|
||||
{
|
||||
$this->setName('hjf:release-points')
|
||||
->setDescription('执行黄精粉健康商城每日积分释放(frozen_points × 4‰ → available_points)');
|
||||
}
|
||||
|
||||
protected function execute(Input $input, Output $output): int
|
||||
{
|
||||
$output->writeln('[HjfReleasePoints] 开始执行积分释放...');
|
||||
|
||||
/** @var PointsReleaseServices $service */
|
||||
$service = app()->make(PointsReleaseServices::class);
|
||||
$result = $service->executeRelease();
|
||||
|
||||
$output->writeln(sprintf(
|
||||
'[HjfReleasePoints] 完成:处理 %d 人,共释放 %d 积分,日期 %s',
|
||||
$result['processed'],
|
||||
$result['total_released'],
|
||||
$result['release_date']
|
||||
));
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
@@ -109,7 +109,8 @@ class Login
|
||||
try {
|
||||
aj_captcha_check_two($captchaType, $captchaVerification);
|
||||
} catch (\Throwable $e) {
|
||||
return app('json')->fail($e->getError());
|
||||
$msg = method_exists($e, 'getError') ? $e->getError() : $e->getMessage();
|
||||
return app('json')->fail($msg);
|
||||
}
|
||||
}
|
||||
validate(\app\validate\admin\setting\SystemAdminValidate::class)->scene('get')->check(['account' => $account, 'pwd' => $password]);
|
||||
|
||||
161
pro_v3.5.1/app/controller/admin/v1/hjf/MemberController.php
Normal file
161
pro_v3.5.1/app/controller/admin/v1/hjf/MemberController.php
Normal file
@@ -0,0 +1,161 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace app\controller\admin\v1\hjf;
|
||||
|
||||
use app\controller\admin\AuthController;
|
||||
use app\dao\user\UserDao;
|
||||
use app\services\agent\AgentLevelServices;
|
||||
use app\services\hjf\MemberLevelServices;
|
||||
use think\annotation\Inject;
|
||||
|
||||
/**
|
||||
* Admin · 会员管理接口(改造复用版)
|
||||
*
|
||||
* 复用 eb_agent_level 体系,使用 eb_user.agent_level 字段。
|
||||
*
|
||||
* GET /adminapi/hjf/member/list — 会员列表
|
||||
* PUT /adminapi/hjf/member/level/:uid — 手动调整会员等级
|
||||
* GET /adminapi/hjf/member/config — 获取会员等级配置(从 eb_agent_level 读取)
|
||||
* POST /adminapi/hjf/member/config — 保存会员等级配置(写入 eb_agent_level)
|
||||
*
|
||||
* Class MemberController
|
||||
* @package app\controller\admin\v1\hjf
|
||||
*/
|
||||
class MemberController extends AuthController
|
||||
{
|
||||
#[Inject]
|
||||
protected UserDao $userDao;
|
||||
|
||||
#[Inject]
|
||||
protected MemberLevelServices $levelServices;
|
||||
|
||||
#[Inject]
|
||||
protected AgentLevelServices $agentLevelServices;
|
||||
|
||||
/**
|
||||
* 会员列表(分页)
|
||||
*/
|
||||
public function memberList(): mixed
|
||||
{
|
||||
$where = $this->request->getMore([
|
||||
['keyword', ''],
|
||||
['member_level', ''],
|
||||
['page', 1],
|
||||
['limit', 20],
|
||||
]);
|
||||
$page = (int)$where['page'];
|
||||
$limit = (int)$where['limit'];
|
||||
|
||||
$condition = [];
|
||||
if ($where['keyword'] !== '') {
|
||||
$condition['uid|nickname|phone'] = ['like', '%' . $where['keyword'] . '%'];
|
||||
}
|
||||
|
||||
if ($where['member_level'] !== '') {
|
||||
$grade = (int)$where['member_level'];
|
||||
if ($grade === 0) {
|
||||
$condition['agent_level'] = 0;
|
||||
} else {
|
||||
$agentLevelId = $this->agentLevelServices->getLevelIdByGrade($grade);
|
||||
$condition['agent_level'] = $agentLevelId ?: -1;
|
||||
}
|
||||
}
|
||||
|
||||
$count = $this->userDao->count($condition);
|
||||
$list = $this->userDao->selectList(
|
||||
$condition,
|
||||
'uid,nickname,avatar,phone,agent_level,frozen_points,available_points,now_money,spread_uid,add_time',
|
||||
$page,
|
||||
$limit,
|
||||
'uid',
|
||||
'desc'
|
||||
);
|
||||
|
||||
$levelList = $this->agentLevelServices->dao->getList(['is_del' => 0, 'status' => 1]);
|
||||
$levelMap = array_column($levelList, null, 'id');
|
||||
|
||||
foreach ($list as &$item) {
|
||||
$agentLevelId = (int)($item['agent_level'] ?? 0);
|
||||
$levelInfo = $levelMap[$agentLevelId] ?? null;
|
||||
$item['member_level'] = $levelInfo ? (int)$levelInfo['grade'] : 0;
|
||||
$item['member_level_name'] = $levelInfo ? $levelInfo['name'] : '普通会员';
|
||||
$item['direct_order_count'] = $this->levelServices->getDirectQueueOrderCount((int)$item['uid']);
|
||||
$item['umbrella_order_count'] = $this->levelServices->getUmbrellaQueueOrderCount((int)$item['uid']);
|
||||
$item['direct_spread_count'] = $this->levelServices->getDirectSpreadCount((int)$item['uid']);
|
||||
}
|
||||
unset($item);
|
||||
|
||||
return $this->success(compact('list', 'count'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 手动调整会员等级
|
||||
*/
|
||||
public function updateLevel(int $uid): mixed
|
||||
{
|
||||
$data = $this->request->getMore([
|
||||
['member_level', 0],
|
||||
]);
|
||||
$grade = (int)$data['member_level'];
|
||||
|
||||
if ($grade < 0 || $grade > 4) {
|
||||
return $this->fail('等级范围 0-4');
|
||||
}
|
||||
|
||||
$user = $this->userDao->get($uid);
|
||||
if (!$user) {
|
||||
return $this->fail('用户不存在');
|
||||
}
|
||||
|
||||
$this->levelServices->setUserLevel($uid, $grade);
|
||||
|
||||
return $this->success('更新成功');
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取会员等级配置(从 eb_agent_level 表读取)
|
||||
*/
|
||||
public function getConfig(): mixed
|
||||
{
|
||||
$levelList = $this->agentLevelServices->dao->getList(['is_del' => 0, 'status' => 1]);
|
||||
$config = [];
|
||||
foreach ($levelList as $level) {
|
||||
$config[] = [
|
||||
'id' => $level['id'],
|
||||
'name' => $level['name'],
|
||||
'grade' => $level['grade'],
|
||||
'direct_reward_points' => $level['direct_reward_points'] ?? 0,
|
||||
'umbrella_reward_points' => $level['umbrella_reward_points'] ?? 0,
|
||||
];
|
||||
}
|
||||
return $this->success($config);
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存会员等级配置(写入 eb_agent_level 表)
|
||||
*/
|
||||
public function saveConfig(): mixed
|
||||
{
|
||||
$levels = $this->request->post('levels', []);
|
||||
if (!is_array($levels)) {
|
||||
return $this->fail('参数格式错误');
|
||||
}
|
||||
|
||||
foreach ($levels as $item) {
|
||||
if (empty($item['id'])) continue;
|
||||
$updateData = [];
|
||||
if (isset($item['direct_reward_points'])) {
|
||||
$updateData['direct_reward_points'] = (int)$item['direct_reward_points'];
|
||||
}
|
||||
if (isset($item['umbrella_reward_points'])) {
|
||||
$updateData['umbrella_reward_points'] = (int)$item['umbrella_reward_points'];
|
||||
}
|
||||
if ($updateData) {
|
||||
$this->agentLevelServices->dao->update((int)$item['id'], $updateData);
|
||||
}
|
||||
}
|
||||
|
||||
return $this->success('保存成功');
|
||||
}
|
||||
}
|
||||
42
pro_v3.5.1/app/controller/admin/v1/hjf/PointsController.php
Normal file
42
pro_v3.5.1/app/controller/admin/v1/hjf/PointsController.php
Normal file
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace app\controller\admin\v1\hjf;
|
||||
|
||||
use app\controller\admin\AuthController;
|
||||
use app\dao\hjf\PointsReleaseLogDao;
|
||||
use think\annotation\Inject;
|
||||
|
||||
/**
|
||||
* Admin · 积分管理接口
|
||||
*
|
||||
* GET /adminapi/hjf/points/release-log — 积分释放日志(分页)
|
||||
*
|
||||
* Class PointsController
|
||||
* @package app\controller\admin\v1\hjf
|
||||
*/
|
||||
class PointsController extends AuthController
|
||||
{
|
||||
#[Inject]
|
||||
protected PointsReleaseLogDao $dao;
|
||||
|
||||
/**
|
||||
* 积分释放日志(分页)
|
||||
*/
|
||||
public function releaseLog(): mixed
|
||||
{
|
||||
$where = $this->request->getMore([
|
||||
['keyword', ''],
|
||||
['type', ''],
|
||||
['start_time', ''],
|
||||
['end_time', ''],
|
||||
['page', 1],
|
||||
['limit', 20],
|
||||
]);
|
||||
$page = (int)$where['page'];
|
||||
$limit = (int)$where['limit'];
|
||||
unset($where['page'], $where['limit']);
|
||||
|
||||
return $this->success($this->dao->getAdminList($where, $page, $limit));
|
||||
}
|
||||
}
|
||||
105
pro_v3.5.1/app/controller/admin/v1/hjf/QueueController.php
Normal file
105
pro_v3.5.1/app/controller/admin/v1/hjf/QueueController.php
Normal file
@@ -0,0 +1,105 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace app\controller\admin\v1\hjf;
|
||||
|
||||
use app\controller\admin\AuthController;
|
||||
use app\dao\hjf\QueuePoolDao;
|
||||
use app\services\system\config\SystemConfigServices;
|
||||
use crmeb\services\SystemConfigService;
|
||||
use think\annotation\Inject;
|
||||
|
||||
/**
|
||||
* Admin · 公排管理接口
|
||||
*
|
||||
* GET /adminapi/hjf/queue/order — 公排订单列表
|
||||
* GET /adminapi/hjf/queue/config — 获取公排配置
|
||||
* POST /adminapi/hjf/queue/config — 保存公排配置
|
||||
* GET /adminapi/hjf/queue/finance — 公排退款财务流水
|
||||
*
|
||||
* Class QueueController
|
||||
* @package app\controller\admin\v1\hjf
|
||||
*/
|
||||
class QueueController extends AuthController
|
||||
{
|
||||
#[Inject]
|
||||
protected QueuePoolDao $dao;
|
||||
|
||||
/**
|
||||
* 公排订单列表(分页 + 筛选)
|
||||
*/
|
||||
public function orderList(): mixed
|
||||
{
|
||||
$where = $this->request->getMore([
|
||||
['keyword', ''],
|
||||
['status', ''],
|
||||
['start_time', ''],
|
||||
['end_time', ''],
|
||||
['page', 1],
|
||||
['limit', 20],
|
||||
]);
|
||||
$page = (int)$where['page'];
|
||||
$limit = (int)$where['limit'];
|
||||
unset($where['page'], $where['limit']);
|
||||
|
||||
return $this->success($this->dao->getAdminList($where, $page, $limit));
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取公排配置
|
||||
*/
|
||||
public function getConfig(): mixed
|
||||
{
|
||||
$config = [
|
||||
'trigger_multiple' => (int)SystemConfigService::get('hjf_trigger_multiple', 4),
|
||||
'release_rate' => (int)SystemConfigService::get('hjf_release_rate', 4),
|
||||
'withdraw_fee_rate' => (int)SystemConfigService::get('hjf_withdraw_fee_rate', 7),
|
||||
'enabled' => (bool)SystemConfigService::get('hjf_queue_enabled', 1),
|
||||
];
|
||||
return $this->success($config);
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存公排配置
|
||||
*/
|
||||
public function saveConfig(SystemConfigServices $configServices): mixed
|
||||
{
|
||||
$data = $this->request->getMore([
|
||||
['trigger_multiple', 4],
|
||||
['release_rate', 4],
|
||||
['withdraw_fee_rate', 7],
|
||||
['enabled', 1],
|
||||
]);
|
||||
|
||||
$map = [
|
||||
'hjf_trigger_multiple' => (int)$data['trigger_multiple'],
|
||||
'hjf_release_rate' => (int)$data['release_rate'],
|
||||
'hjf_withdraw_fee_rate' => (int)$data['withdraw_fee_rate'],
|
||||
'hjf_queue_enabled' => (int)$data['enabled'],
|
||||
];
|
||||
|
||||
foreach ($map as $key => $value) {
|
||||
$configServices->setConfig($key, (string)$value);
|
||||
}
|
||||
|
||||
return $this->success('保存成功');
|
||||
}
|
||||
|
||||
/**
|
||||
* 公排退款财务流水(分页)
|
||||
*/
|
||||
public function financeList(): mixed
|
||||
{
|
||||
$where = $this->request->getMore([
|
||||
['start_time', ''],
|
||||
['end_time', ''],
|
||||
['page', 1],
|
||||
['limit', 20],
|
||||
]);
|
||||
$page = (int)$where['page'];
|
||||
$limit = (int)$where['limit'];
|
||||
unset($where['page'], $where['limit']);
|
||||
|
||||
return $this->success($this->dao->getFinanceList($where, $page, $limit));
|
||||
}
|
||||
}
|
||||
@@ -106,6 +106,22 @@ class SystemTimer extends AuthController
|
||||
return $this->success('添加定时器成功!');
|
||||
}
|
||||
|
||||
/**
|
||||
* 手动立即触发一个定时任务
|
||||
* @param $id
|
||||
* @return mixed
|
||||
*/
|
||||
public function run_now($id)
|
||||
{
|
||||
$timer = $this->services->getOneTimer($id);
|
||||
$mark = $timer['mark'] ?? '';
|
||||
if (!$mark) {
|
||||
return $this->fail('定时任务标识不存在');
|
||||
}
|
||||
$this->services->runNow($mark);
|
||||
return $this->success('任务已触发');
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新定时任务
|
||||
* @param $id
|
||||
|
||||
@@ -421,7 +421,13 @@ class SystemConfig extends AuthController
|
||||
|
||||
$is_store_stock = isset($post['store_stock']) && $post['store_stock'] != sys_config('store_stock');
|
||||
|
||||
// radio 类型字段:若历史 bug 导致 value 为单元素数组,自动展开为标量
|
||||
$radioScalarFields = ['brokerage_scope', 'brokerage_timing'];
|
||||
foreach ($post as $k => $v) {
|
||||
if (in_array($k, $radioScalarFields) && is_array($v) && count($v) === 1) {
|
||||
$v = $v[0];
|
||||
$post[$k] = $v;
|
||||
}
|
||||
$config_one = $this->services->getOne(['menu_name' => $k]);
|
||||
if ($config_one) {
|
||||
$config_one['value'] = $v;
|
||||
|
||||
@@ -85,6 +85,8 @@ class User extends AuthController
|
||||
['isMember', ''],
|
||||
['label_ids', ''],
|
||||
['is_channel', ''],
|
||||
/** HJF:按分销等级 grade(0–4)筛选,对应 eb_user.agent_level */
|
||||
['hjf_member_level', ''],
|
||||
]);
|
||||
if ($where['label_ids']) {
|
||||
$where['label_id'] = stringToIntArray($where['label_ids']);
|
||||
|
||||
64
pro_v3.5.1/app/controller/api/v1/hjf/AssetsController.php
Normal file
64
pro_v3.5.1/app/controller/api/v1/hjf/AssetsController.php
Normal file
@@ -0,0 +1,64 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace app\controller\api\v1\hjf;
|
||||
|
||||
use app\Request;
|
||||
use app\services\hjf\HjfAssetsServices;
|
||||
use think\annotation\Inject;
|
||||
|
||||
/**
|
||||
* 用户端 · 资产接口
|
||||
*
|
||||
* GET /api/hjf/assets/overview — 资产总览(余额 + 积分)
|
||||
* GET /api/hjf/assets/cash/detail — 现金流水(分页)
|
||||
*
|
||||
* Class AssetsController
|
||||
* @package app\controller\api\v1\hjf
|
||||
*/
|
||||
class AssetsController
|
||||
{
|
||||
#[Inject]
|
||||
protected HjfAssetsServices $assetsServices;
|
||||
|
||||
/**
|
||||
* 资产总览
|
||||
*
|
||||
* @param Request $request
|
||||
* @return mixed
|
||||
*/
|
||||
public function overview(Request $request): mixed
|
||||
{
|
||||
$uid = (int)$request->uid();
|
||||
return app('json')->success(
|
||||
$this->assetsServices->getOverview($uid)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 现金流水(分页)
|
||||
*
|
||||
* 查询参数:
|
||||
* - type: '' | queue_refund | withdraw | recharge
|
||||
* - page, limit
|
||||
*
|
||||
* @param Request $request
|
||||
* @return mixed
|
||||
*/
|
||||
public function cashDetail(Request $request): mixed
|
||||
{
|
||||
$uid = (int)$request->uid();
|
||||
$type = (string)$request->param('type', '');
|
||||
$page = max(1, (int)$request->param('page', 1));
|
||||
$limit = min(50, max(1, (int)$request->param('limit', 15)));
|
||||
|
||||
$validTypes = ['', 'queue_refund', 'withdraw', 'recharge', 'pay'];
|
||||
if (!in_array($type, $validTypes, true)) {
|
||||
$type = '';
|
||||
}
|
||||
|
||||
return app('json')->success(
|
||||
$this->assetsServices->getCashDetail($uid, $type, $page, $limit)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -8,9 +8,8 @@ namespace app\controller\api\v1\hjf;
|
||||
use app\Request;
|
||||
use app\services\user\UserServices;
|
||||
use app\services\agent\AgentLevelServices;
|
||||
use app\controller\api\AuthController;
|
||||
|
||||
class HjfAssets extends AuthController
|
||||
class HjfAssets
|
||||
{
|
||||
/**
|
||||
* GET /api/hjf/assets/overview
|
||||
@@ -25,7 +24,7 @@ class HjfAssets extends AuthController
|
||||
$user = $userServices->get($uid, ['uid', 'brokerage_price', 'frozen_points', 'available_points', 'agent_level', 'now_money']);
|
||||
|
||||
if (!$user) {
|
||||
return $this->fail('用户不存在');
|
||||
return app('json')->fail('用户不存在');
|
||||
}
|
||||
|
||||
$agentLevelName = '';
|
||||
@@ -40,7 +39,7 @@ class HjfAssets extends AuthController
|
||||
$frozenPoints = (int)($user['frozen_points'] ?? 0);
|
||||
$todayRelease = (int)floor($frozenPoints * 0.0004);
|
||||
|
||||
return $this->success([
|
||||
return app('json')->successful([
|
||||
'brokerage_price' => (string)($user['brokerage_price'] ?? '0.00'),
|
||||
'now_money' => (string)($user['now_money'] ?? '0.00'),
|
||||
'frozen_points' => $frozenPoints,
|
||||
|
||||
@@ -6,12 +6,9 @@
|
||||
namespace app\controller\api\v1\hjf;
|
||||
|
||||
use app\Request;
|
||||
use app\services\order\StoreOrderServices;
|
||||
use app\services\user\UserBrokerageServices;
|
||||
use app\services\user\UserServices;
|
||||
use app\controller\api\AuthController;
|
||||
|
||||
class HjfBrokerage extends AuthController
|
||||
class HjfBrokerage
|
||||
{
|
||||
/**
|
||||
* GET /api/hjf/brokerage/progress
|
||||
@@ -45,7 +42,7 @@ class HjfBrokerage extends AuthController
|
||||
// 累计佣金
|
||||
$totalBrokerage = $userInfo ? (string)($userInfo['brokerage_price'] ?? '0.00') : '0.00';
|
||||
|
||||
return $this->success([
|
||||
return app('json')->successful([
|
||||
'cycle_total' => $cycleCount,
|
||||
'cycle_current' => $cycleCurrent,
|
||||
'cycle_rates' => $cycleRates,
|
||||
|
||||
178
pro_v3.5.1/app/controller/api/v1/hjf/MemberController.php
Normal file
178
pro_v3.5.1/app/controller/api/v1/hjf/MemberController.php
Normal file
@@ -0,0 +1,178 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace app\controller\api\v1\hjf;
|
||||
|
||||
use app\Request;
|
||||
use app\services\agent\AgentLevelServices;
|
||||
use app\services\agent\AgentLevelTaskServices;
|
||||
use app\services\hjf\MemberLevelServices;
|
||||
use app\services\hjf\PointsRewardServices;
|
||||
use app\dao\hjf\PointsReleaseLogDao;
|
||||
use app\services\user\UserServices;
|
||||
use think\annotation\Inject;
|
||||
use think\facade\Db;
|
||||
|
||||
/**
|
||||
* 用户端 · 会员信息接口(改造复用版)
|
||||
*
|
||||
* 复用 eb_agent_level 体系,使用 eb_user.agent_level 字段。
|
||||
*
|
||||
* GET /api/hjf/member/info — 当前用户等级信息
|
||||
* GET /api/hjf/member/team — 团队成员列表
|
||||
* GET /api/hjf/member/income — 团队收益明细
|
||||
*
|
||||
* Class MemberController
|
||||
* @package app\controller\api\v1\hjf
|
||||
*/
|
||||
class MemberController
|
||||
{
|
||||
#[Inject]
|
||||
protected MemberLevelServices $memberLevelServices;
|
||||
|
||||
#[Inject]
|
||||
protected AgentLevelServices $agentLevelServices;
|
||||
|
||||
#[Inject]
|
||||
protected AgentLevelTaskServices $agentLevelTaskServices;
|
||||
|
||||
/**
|
||||
* 获取当前用户会员信息
|
||||
*/
|
||||
public function info(Request $request): \think\Response
|
||||
{
|
||||
$uid = (int)$request->uid();
|
||||
|
||||
$agentLevel = (int)Db::name('user')->where('uid', $uid)->value('agent_level');
|
||||
// 直接从 eb_agent_level 取 name,避免 grade 解析失败时等级徽章不显示
|
||||
$levelRow = $agentLevel > 0 ? $this->agentLevelServices->getLevelInfo($agentLevel) : null;
|
||||
$grade = $levelRow ? (int)$levelRow['grade'] : 0;
|
||||
$levelName = $levelRow ? ($levelRow['name'] ?? '普通会员') : '普通会员';
|
||||
|
||||
$directCount = $this->memberLevelServices->getDirectSpreadCount($uid);
|
||||
$umbrellaCount = $this->memberLevelServices->getUmbrellaQueueOrderCount($uid);
|
||||
$directOrderCount = $this->memberLevelServices->getDirectQueueOrderCount($uid);
|
||||
|
||||
// 用已修正的 level row ID 查找下一等级,避免旧 status=0 记录导致 grade 被误判为 0
|
||||
$effectiveLevelId = $levelRow ? (int)$levelRow['id'] : 0;
|
||||
$nextLevel = $this->agentLevelServices->getNextLevelInfo($effectiveLevelId);
|
||||
$nextLevelName = $nextLevel ? $nextLevel['name'] : null;
|
||||
|
||||
$upgradeProgress = [];
|
||||
if ($nextLevel) {
|
||||
$taskList = $this->agentLevelTaskServices->getUpgradeTasksForLevel((int)$nextLevel['id']);
|
||||
foreach ($taskList as $task) {
|
||||
$item = ['name' => $task['name'], 'number' => $task['number']];
|
||||
switch ($task['type']) {
|
||||
case 6:
|
||||
$item['current'] = $directOrderCount;
|
||||
break;
|
||||
case 7:
|
||||
$item['current'] = $umbrellaCount;
|
||||
break;
|
||||
case 8:
|
||||
$item['current'] = $directCount;
|
||||
break;
|
||||
default:
|
||||
$item['current'] = 0;
|
||||
}
|
||||
$item['completed'] = $item['current'] >= $item['number'];
|
||||
$upgradeProgress[] = $item;
|
||||
}
|
||||
}
|
||||
|
||||
return app('json')->success([
|
||||
'agent_level' => $agentLevel, // eb_user.agent_level 原始 ID,供前端判断是否有等级
|
||||
'member_level' => $grade,
|
||||
'member_level_name' => $levelName, // eb_agent_level.name 直接值
|
||||
'direct_count' => $directCount,
|
||||
'umbrella_count' => $umbrellaCount,
|
||||
'direct_order_count' => $directOrderCount,
|
||||
'next_level_name' => $nextLevelName,
|
||||
'upgrade_progress' => $upgradeProgress,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 团队成员列表(直推/伞下)
|
||||
*/
|
||||
public function team(Request $request): \think\Response
|
||||
{
|
||||
$uid = (int)$request->uid();
|
||||
$page = (int)$request->get('page', 1);
|
||||
$limit = (int)$request->get('limit', 20);
|
||||
$type = $request->get('type', 'direct');
|
||||
|
||||
/** @var UserServices $userServices */
|
||||
$userServices = app()->make(UserServices::class);
|
||||
|
||||
if ($type === 'direct') {
|
||||
$where = ['spread_uid' => $uid];
|
||||
} else {
|
||||
$directUids = $userServices->getColumn(['spread_uid' => $uid], 'uid');
|
||||
if (empty($directUids)) {
|
||||
return app('json')->success(['list' => [], 'count' => 0]);
|
||||
}
|
||||
$where = [['spread_uid', 'in', $directUids]];
|
||||
}
|
||||
|
||||
$count = $userServices->count($where);
|
||||
$list = Db::name('user')
|
||||
->where($where)
|
||||
->field('uid,nickname,avatar,phone,agent_level,add_time')
|
||||
->page($page, $limit)
|
||||
->order('uid desc')
|
||||
->select()
|
||||
->toArray();
|
||||
|
||||
$maps = $this->agentLevelServices->loadHjfUserListLevelMaps();
|
||||
|
||||
foreach ($list as &$item) {
|
||||
$alId = (int)($item['agent_level'] ?? 0);
|
||||
$levelInfo = $this->agentLevelServices->pickHjfLevelRowForUserListDisplay($alId, $maps);
|
||||
$item['member_level'] = $levelInfo ? (int)$levelInfo['grade'] : 0;
|
||||
$item['member_level_name'] = $levelInfo ? $levelInfo['name'] : '普通会员';
|
||||
$item['join_time'] = date('Y-m-d', (int)$item['add_time']);
|
||||
$item['direct_orders'] = $this->agentLevelTaskServices->getDirectQueueOrderCount((int)$item['uid']);
|
||||
unset($item['agent_level'], $item['add_time']);
|
||||
}
|
||||
unset($item);
|
||||
|
||||
return app('json')->success(compact('list', 'count'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 团队收益明细(积分奖励记录)
|
||||
*/
|
||||
public function income(Request $request): \think\Response
|
||||
{
|
||||
$uid = (int)$request->uid();
|
||||
$page = (int)$request->get('page', 1);
|
||||
$limit = (int)$request->get('limit', 20);
|
||||
|
||||
/** @var PointsReleaseLogDao $logDao */
|
||||
$logDao = app()->make(PointsReleaseLogDao::class);
|
||||
|
||||
$where = [
|
||||
'uid' => $uid,
|
||||
'pm' => 1,
|
||||
];
|
||||
$where[] = ['type', 'in', ['reward_direct', 'reward_umbrella']];
|
||||
|
||||
$count = $logDao->count($where);
|
||||
$list = Db::name('points_release_log')
|
||||
->where($where)
|
||||
->field('id,uid,points,type,title,mark,order_id,add_time')
|
||||
->page($page, $limit)
|
||||
->order('id desc')
|
||||
->select()
|
||||
->toArray();
|
||||
|
||||
foreach ($list as &$item) {
|
||||
$item['time'] = date('Y-m-d H:i:s', (int)$item['add_time']);
|
||||
}
|
||||
unset($item);
|
||||
|
||||
return app('json')->success(compact('list', 'count'));
|
||||
}
|
||||
}
|
||||
49
pro_v3.5.1/app/controller/api/v1/hjf/PointsController.php
Normal file
49
pro_v3.5.1/app/controller/api/v1/hjf/PointsController.php
Normal file
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace app\controller\api\v1\hjf;
|
||||
|
||||
use app\Request;
|
||||
use app\dao\hjf\PointsReleaseLogDao;
|
||||
use think\annotation\Inject;
|
||||
|
||||
/**
|
||||
* 用户端 · 积分明细接口
|
||||
*
|
||||
* GET /api/hjf/points/detail — 积分明细(分页,支持5种类型筛选)
|
||||
*
|
||||
* Class PointsController
|
||||
* @package app\controller\api\v1\hjf
|
||||
*/
|
||||
class PointsController
|
||||
{
|
||||
#[Inject]
|
||||
protected PointsReleaseLogDao $dao;
|
||||
|
||||
/**
|
||||
* 积分明细(分页)
|
||||
*
|
||||
* 查询参数:
|
||||
* - type: '' | reward_direct | reward_umbrella | release | consume
|
||||
* - page, limit
|
||||
*
|
||||
* @param Request $request
|
||||
* @return mixed
|
||||
*/
|
||||
public function detail(Request $request): mixed
|
||||
{
|
||||
$uid = (int)$request->uid();
|
||||
$type = (string)$request->param('type', '');
|
||||
$page = max(1, (int)$request->param('page', 1));
|
||||
$limit = min(50, max(1, (int)$request->param('limit', 15)));
|
||||
|
||||
$validTypes = ['', 'reward_direct', 'reward_umbrella', 'release', 'consume'];
|
||||
if (!in_array($type, $validTypes, true)) {
|
||||
$type = '';
|
||||
}
|
||||
|
||||
return app('json')->success(
|
||||
$this->dao->getDetailList($uid, $type, $page, $limit)
|
||||
);
|
||||
}
|
||||
}
|
||||
60
pro_v3.5.1/app/controller/api/v1/hjf/QueueController.php
Normal file
60
pro_v3.5.1/app/controller/api/v1/hjf/QueueController.php
Normal file
@@ -0,0 +1,60 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace app\controller\api\v1\hjf;
|
||||
|
||||
use app\Request;
|
||||
use app\services\hjf\QueuePoolServices;
|
||||
use think\annotation\Inject;
|
||||
|
||||
/**
|
||||
* 用户端 · 公排接口
|
||||
*
|
||||
* GET /api/hjf/queue/status — 公排状态摘要
|
||||
* GET /api/hjf/queue/history — 公排历史记录(分页)
|
||||
*
|
||||
* Class QueueController
|
||||
* @package app\controller\api\v1\hjf
|
||||
*/
|
||||
class QueueController
|
||||
{
|
||||
#[Inject]
|
||||
protected QueuePoolServices $services;
|
||||
|
||||
/**
|
||||
* 公排状态摘要
|
||||
* 返回:全平台总单数、当前批次进度、用户自己的订单列表(含预估等待)
|
||||
*
|
||||
* @param Request $request
|
||||
* @return mixed
|
||||
*/
|
||||
public function status(Request $request): mixed
|
||||
{
|
||||
$uid = (int)$request->uid();
|
||||
return app('json')->success($this->services->getUserStatus($uid));
|
||||
}
|
||||
|
||||
/**
|
||||
* 公排历史记录(分页)
|
||||
*
|
||||
* @param Request $request
|
||||
* @return mixed
|
||||
*/
|
||||
public function history(Request $request): mixed
|
||||
{
|
||||
$uid = (int)$request->uid();
|
||||
$status = (int)$request->param('status', -1); // -1=全部, 0=排队中, 1=已退款
|
||||
[$page, $limit] = $this->getPage($request);
|
||||
|
||||
return app('json')->success(
|
||||
$this->services->getUserHistory($uid, $status, $page, $limit)
|
||||
);
|
||||
}
|
||||
|
||||
private function getPage(Request $request): array
|
||||
{
|
||||
$page = max(1, (int)$request->param('page', 1));
|
||||
$limit = min(50, max(1, (int)$request->param('limit', 15)));
|
||||
return [$page, $limit];
|
||||
}
|
||||
}
|
||||
84
pro_v3.5.1/app/dao/hjf/PointsReleaseLogDao.php
Normal file
84
pro_v3.5.1/app/dao/hjf/PointsReleaseLogDao.php
Normal file
@@ -0,0 +1,84 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace app\dao\hjf;
|
||||
|
||||
use app\dao\BaseDao;
|
||||
use app\model\hjf\PointsReleaseLog;
|
||||
|
||||
/**
|
||||
* 积分释放日志 DAO
|
||||
* Class PointsReleaseLogDao
|
||||
* @package app\dao\hjf
|
||||
*/
|
||||
class PointsReleaseLogDao extends BaseDao
|
||||
{
|
||||
protected function setModel(): string
|
||||
{
|
||||
return PointsReleaseLog::class;
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询积分明细列表(分页,支持按 type 筛选)
|
||||
* type: reward_direct | reward_umbrella | release | consume | ''(全部)
|
||||
*/
|
||||
public function getDetailList(int $uid, string $type, int $page, int $limit): array
|
||||
{
|
||||
$model = $this->getModel()->where('uid', $uid);
|
||||
if ($type !== '') {
|
||||
$model = $model->where('type', $type);
|
||||
}
|
||||
$count = (clone $model)->count();
|
||||
$list = $model->order('add_time', 'desc')
|
||||
->page($page, $limit)
|
||||
->select()
|
||||
->toArray();
|
||||
return compact('list', 'count');
|
||||
}
|
||||
|
||||
/**
|
||||
* Admin 积分释放日志(分页)
|
||||
*/
|
||||
public function getAdminList(array $where, int $page, int $limit): array
|
||||
{
|
||||
$model = $this->getModel();
|
||||
if (!empty($where['keyword'])) {
|
||||
$model = $model->where('uid', 'like', '%' . $where['keyword'] . '%');
|
||||
}
|
||||
if (!empty($where['type'])) {
|
||||
$model = $model->where('type', $where['type']);
|
||||
}
|
||||
if (!empty($where['start_time'])) {
|
||||
$model = $model->where('add_time', '>=', strtotime($where['start_time']));
|
||||
}
|
||||
if (!empty($where['end_time'])) {
|
||||
$model = $model->where('add_time', '<=', strtotime($where['end_time']) + 86399);
|
||||
}
|
||||
$count = (clone $model)->count();
|
||||
$list = $model->order('add_time', 'desc')
|
||||
->page($page, $limit)
|
||||
->select()
|
||||
->toArray();
|
||||
|
||||
// 今日统计
|
||||
$todayStart = strtotime(date('Y-m-d'));
|
||||
$todayReleased = $this->getModel()
|
||||
->where('type', 'release')
|
||||
->where('add_time', '>=', $todayStart)
|
||||
->sum('points');
|
||||
$todayUsers = $this->getModel()
|
||||
->where('type', 'release')
|
||||
->where('add_time', '>=', $todayStart)
|
||||
->group('uid')
|
||||
->count();
|
||||
|
||||
return [
|
||||
'list' => $list,
|
||||
'count' => $count,
|
||||
'statistics' => [
|
||||
'total_released_today' => (int)$todayReleased,
|
||||
'total_users_released' => (int)$todayUsers,
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
140
pro_v3.5.1/app/dao/hjf/QueuePoolDao.php
Normal file
140
pro_v3.5.1/app/dao/hjf/QueuePoolDao.php
Normal file
@@ -0,0 +1,140 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace app\dao\hjf;
|
||||
|
||||
use app\dao\BaseDao;
|
||||
use app\model\hjf\QueuePool;
|
||||
use crmeb\basic\BaseModel;
|
||||
|
||||
/**
|
||||
* 公排池 DAO
|
||||
* Class QueuePoolDao
|
||||
* @package app\dao\hjf
|
||||
*/
|
||||
class QueuePoolDao extends BaseDao
|
||||
{
|
||||
protected function setModel(): string
|
||||
{
|
||||
return QueuePool::class;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户的公排记录列表(分页)
|
||||
*/
|
||||
public function getUserList(int $uid, int $status, int $page, int $limit): array
|
||||
{
|
||||
$model = $this->getModel()->where('uid', $uid);
|
||||
if ($status >= 0) {
|
||||
$model = $model->where('status', $status);
|
||||
}
|
||||
$count = (clone $model)->count();
|
||||
$list = $model->order('add_time', 'desc')
|
||||
->page($page, $limit)
|
||||
->select()
|
||||
->toArray();
|
||||
return compact('list', 'count');
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取全局公排列表(Admin 分页)
|
||||
*/
|
||||
public function getAdminList(array $where, int $page, int $limit): array
|
||||
{
|
||||
$model = $this->getModel();
|
||||
if (!empty($where['keyword'])) {
|
||||
$model = $model->where('order_id|uid', 'like', '%' . $where['keyword'] . '%');
|
||||
}
|
||||
if (isset($where['status']) && $where['status'] !== '') {
|
||||
$model = $model->where('status', (int)$where['status']);
|
||||
}
|
||||
if (!empty($where['start_time'])) {
|
||||
$model = $model->where('add_time', '>=', strtotime($where['start_time']));
|
||||
}
|
||||
if (!empty($where['end_time'])) {
|
||||
$model = $model->where('add_time', '<=', strtotime($where['end_time']) + 86399);
|
||||
}
|
||||
$count = (clone $model)->count();
|
||||
$list = $model->order('queue_no', 'asc')
|
||||
->page($page, $limit)
|
||||
->select()
|
||||
->toArray();
|
||||
return compact('list', 'count');
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取最早尚未退款的一条记录
|
||||
*/
|
||||
public function getEarliestPending(): ?array
|
||||
{
|
||||
$row = $this->getModel()
|
||||
->where('status', 0)
|
||||
->order('queue_no', 'asc')
|
||||
->find();
|
||||
return $row ? $row->toArray() : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 当前排队中总单数
|
||||
*/
|
||||
public function countPending(): int
|
||||
{
|
||||
return $this->getModel()->where('status', 0)->count();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取全局总单数(含已退款)
|
||||
*/
|
||||
public function countTotal(): int
|
||||
{
|
||||
return $this->getModel()->count();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取下一个全局排队序号(MAX queue_no + 1)
|
||||
*/
|
||||
public function nextQueueNo(): int
|
||||
{
|
||||
$max = $this->getModel()->max('queue_no');
|
||||
return (int)$max + 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* 标记一条记录为已退款
|
||||
*/
|
||||
public function markRefunded(int $id, int $batchNo): bool
|
||||
{
|
||||
return (bool)$this->getModel()
|
||||
->where('id', $id)
|
||||
->update([
|
||||
'status' => 1,
|
||||
'refund_time' => time(),
|
||||
'trigger_batch' => $batchNo,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取退款财务流水(Admin 分页)
|
||||
*/
|
||||
public function getFinanceList(array $where, int $page, int $limit): array
|
||||
{
|
||||
$model = $this->getModel()->where('status', 1);
|
||||
if (!empty($where['start_time'])) {
|
||||
$model = $model->where('refund_time', '>=', strtotime($where['start_time']));
|
||||
}
|
||||
if (!empty($where['end_time'])) {
|
||||
$model = $model->where('refund_time', '<=', strtotime($where['end_time']) + 86399);
|
||||
}
|
||||
$count = (clone $model)->count();
|
||||
$totalRefund = (clone $model)->sum('amount');
|
||||
$list = $model->order('refund_time', 'desc')
|
||||
->page($page, $limit)
|
||||
->select()
|
||||
->toArray();
|
||||
return [
|
||||
'list' => $list,
|
||||
'count' => $count,
|
||||
'total_refund' => number_format((float)$totalRefund, 2, '.', ''),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -187,6 +187,10 @@ class UserWechatUserDao extends BaseDao
|
||||
}
|
||||
}
|
||||
|
||||
// HJF / 分销等级:eb_user.agent_level(由 hjf_member_level 归一化得到)
|
||||
if (isset($where['hjf_agent_level_id']) && $where['hjf_agent_level_id'] !== '' && $where['hjf_agent_level_id'] !== null) {
|
||||
$model = $model->where($userAlias . 'agent_level', (int)$where['hjf_agent_level_id']);
|
||||
}
|
||||
//用户等级
|
||||
if (isset($where['level']) && $where['level']) {
|
||||
$model = $model->where($userAlias . 'level', $where['level']);
|
||||
|
||||
80
pro_v3.5.1/app/jobs/hjf/HjfOrderPayJob.php
Normal file
80
pro_v3.5.1/app/jobs/hjf/HjfOrderPayJob.php
Normal file
@@ -0,0 +1,80 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace app\jobs\hjf;
|
||||
|
||||
use app\services\agent\AgentLevelServices;
|
||||
use app\services\hjf\PointsRewardServices;
|
||||
use app\services\hjf\QueuePoolServices;
|
||||
use app\services\user\UserServices;
|
||||
use crmeb\basic\BaseJobs;
|
||||
use crmeb\traits\QueueTrait;
|
||||
use think\exception\ValidateException;
|
||||
use think\facade\Log;
|
||||
|
||||
/**
|
||||
* 报单商品支付成功异步处理 Job
|
||||
*
|
||||
* 触发时机:Pay 监听器检测到 is_queue_goods=1 时派发。
|
||||
*
|
||||
* 执行流程(改造复用版):
|
||||
* 1. 调用 QueuePoolServices::enqueue() 将订单写入公排池
|
||||
* 2. 调用 PointsRewardServices::reward() 沿推荐链发放级差积分
|
||||
* 3. 调用 AgentLevelServices::checkUserLevelFinish() 检查升级
|
||||
* (复用 CRMEB 分销等级升级流程,已支持 HJF 任务类型 6/7/8)
|
||||
*
|
||||
* Class HjfOrderPayJob
|
||||
* @package app\jobs\hjf
|
||||
*/
|
||||
class HjfOrderPayJob extends BaseJobs
|
||||
{
|
||||
use QueueTrait;
|
||||
|
||||
public function doJob(int $uid, string $orderId, float $amount = 3600.00): bool
|
||||
{
|
||||
try {
|
||||
/** @var QueuePoolServices $queueServices */
|
||||
$queueServices = app()->make(QueuePoolServices::class);
|
||||
$queueServices->enqueue($uid, $orderId, $amount);
|
||||
Log::info("[HjfOrderPay] 公排入队成功 uid={$uid} orderId={$orderId}");
|
||||
} catch (ValidateException $e) {
|
||||
Log::warning("[HjfOrderPay] 入队被锁,延迟重试 uid={$uid} orderId={$orderId}: " . $e->getMessage());
|
||||
static::dispatchSece(5, [$uid, $orderId, $amount]);
|
||||
return true;
|
||||
} catch (\Throwable $e) {
|
||||
Log::error("[HjfOrderPay] 公排入队异常 uid={$uid} orderId={$orderId}: " . $e->getMessage());
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
/** @var PointsRewardServices $pointsServices */
|
||||
$pointsServices = app()->make(PointsRewardServices::class);
|
||||
$pointsServices->reward($uid, $orderId);
|
||||
Log::info("[HjfOrderPay] 积分奖励发放完成 uid={$uid} orderId={$orderId}");
|
||||
} catch (\Throwable $e) {
|
||||
Log::error("[HjfOrderPay] 积分奖励失败 uid={$uid} orderId={$orderId}: " . $e->getMessage());
|
||||
}
|
||||
|
||||
try {
|
||||
/** @var UserServices $userServices */
|
||||
$userServices = app()->make(UserServices::class);
|
||||
$userInfo = $userServices->getUserCacheInfo($uid);
|
||||
$spreadUid = $userInfo ? (int)($userInfo['spread_uid'] ?? 0) : 0;
|
||||
$twoSpreadUid = 0;
|
||||
if ($spreadUid > 0 && $oneUserInfo = $userServices->getUserCacheInfo($spreadUid)) {
|
||||
$twoSpreadUid = $userServices->getSpreadUid($spreadUid, $oneUserInfo, false);
|
||||
}
|
||||
$uids = array_unique([$uid, $spreadUid, $twoSpreadUid]);
|
||||
|
||||
/** @var AgentLevelServices $agentLevelServices */
|
||||
$agentLevelServices = app()->make(AgentLevelServices::class);
|
||||
$agentLevelServices->checkUserLevelFinish($uid, $uids);
|
||||
|
||||
Log::info("[HjfOrderPay] 等级升级检查完成 uid={$uid}");
|
||||
} catch (\Throwable $e) {
|
||||
Log::error("[HjfOrderPay] 等级升级检查失败 uid={$uid}: " . $e->getMessage());
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
39
pro_v3.5.1/app/jobs/hjf/MemberLevelCheckJob.php
Normal file
39
pro_v3.5.1/app/jobs/hjf/MemberLevelCheckJob.php
Normal file
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace app\jobs\hjf;
|
||||
|
||||
use app\services\agent\AgentLevelServices;
|
||||
use crmeb\basic\BaseJobs;
|
||||
use crmeb\traits\QueueTrait;
|
||||
|
||||
/**
|
||||
* 会员等级异步检查 Job(改造复用版)
|
||||
*
|
||||
* 委托给 AgentLevelServices::checkUserLevelFinish() 复用 CRMEB 分销等级升级流程。
|
||||
*
|
||||
* Class MemberLevelCheckJob
|
||||
* @package app\jobs\hjf
|
||||
*/
|
||||
class MemberLevelCheckJob extends BaseJobs
|
||||
{
|
||||
use QueueTrait;
|
||||
|
||||
public function doJob(int $uid): bool
|
||||
{
|
||||
try {
|
||||
/** @var AgentLevelServices $levelServices */
|
||||
$levelServices = app()->make(AgentLevelServices::class);
|
||||
$levelServices->checkUserLevelFinish($uid);
|
||||
} catch (\Throwable $e) {
|
||||
response_log_write([
|
||||
'message' => "会员等级检查失败 uid={$uid}: " . $e->getMessage(),
|
||||
'file' => $e->getFile(),
|
||||
'line' => $e->getLine(),
|
||||
]);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
47
pro_v3.5.1/app/jobs/hjf/PointsReleaseJob.php
Normal file
47
pro_v3.5.1/app/jobs/hjf/PointsReleaseJob.php
Normal file
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace app\jobs\hjf;
|
||||
|
||||
use app\services\hjf\PointsReleaseServices;
|
||||
use crmeb\basic\BaseJobs;
|
||||
use crmeb\traits\QueueTrait;
|
||||
use think\facade\Log;
|
||||
|
||||
/**
|
||||
* 积分每日释放 Job
|
||||
*
|
||||
* 由定时任务(crontab 或 Swoole Timer)在每天凌晨 00:01 触发。
|
||||
* 调用方式:PointsReleaseJob::dispatch()
|
||||
*
|
||||
* Class PointsReleaseJob
|
||||
* @package app\jobs\hjf
|
||||
*/
|
||||
class PointsReleaseJob extends BaseJobs
|
||||
{
|
||||
use QueueTrait;
|
||||
|
||||
/**
|
||||
* 执行积分释放
|
||||
* @return bool
|
||||
*/
|
||||
public function doJob(): bool
|
||||
{
|
||||
try {
|
||||
/** @var PointsReleaseServices $releaseServices */
|
||||
$releaseServices = app()->make(PointsReleaseServices::class);
|
||||
$result = $releaseServices->executeRelease();
|
||||
|
||||
Log::info('[PointsReleaseJob] 执行完成', $result);
|
||||
} catch (\Throwable $e) {
|
||||
response_log_write([
|
||||
'message' => '积分每日释放任务失败: ' . $e->getMessage(),
|
||||
'file' => $e->getFile(),
|
||||
'line' => $e->getLine(),
|
||||
]);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
95
pro_v3.5.1/app/jobs/hjf/QueueRefundJob.php
Normal file
95
pro_v3.5.1/app/jobs/hjf/QueueRefundJob.php
Normal file
@@ -0,0 +1,95 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace app\jobs\hjf;
|
||||
|
||||
use app\dao\hjf\QueuePoolDao;
|
||||
use app\dao\user\UserBillDao;
|
||||
use app\dao\user\UserDao;
|
||||
use crmeb\basic\BaseJobs;
|
||||
use crmeb\traits\QueueTrait;
|
||||
use think\facade\Db;
|
||||
use think\facade\Log;
|
||||
|
||||
/**
|
||||
* 公排退款异步 Job
|
||||
*
|
||||
* 由 QueuePoolServices::checkAndTriggerRefund() 派发。
|
||||
* 执行流程:
|
||||
* 1. 二次检查记录状态(防止重复退款)
|
||||
* 2. 在数据库事务中:标记记录已退款 + 写入用户余额 + 写 user_bill 流水
|
||||
*
|
||||
* Class QueueRefundJob
|
||||
* @package app\jobs\hjf
|
||||
*/
|
||||
class QueueRefundJob extends BaseJobs
|
||||
{
|
||||
use QueueTrait;
|
||||
|
||||
/**
|
||||
* 执行退款
|
||||
*
|
||||
* @param int $queueId eb_queue_pool.id
|
||||
* @param int $uid 用户 ID
|
||||
* @param float $amount 退款金额
|
||||
* @param int $batchNo 批次号
|
||||
* @return bool
|
||||
*/
|
||||
public function doJob(int $queueId, int $uid, float $amount, int $batchNo): bool
|
||||
{
|
||||
try {
|
||||
/** @var QueuePoolDao $queueDao */
|
||||
$queueDao = app()->make(QueuePoolDao::class);
|
||||
|
||||
// 二次检查:防止重复退款
|
||||
$record = $queueDao->get($queueId);
|
||||
if (!$record || (int)$record['status'] === 1) {
|
||||
Log::info("[QueueRefund] 记录 {$queueId} 已退款或不存在,跳过");
|
||||
return true;
|
||||
}
|
||||
|
||||
Db::transaction(function () use ($queueId, $uid, $amount, $batchNo, $queueDao) {
|
||||
// 1. 标记公排记录为已退款
|
||||
$queueDao->markRefunded($queueId, $batchNo);
|
||||
|
||||
// 2. 写入用户余额(使用 bcadd 避免浮点误差)
|
||||
/** @var UserDao $userDao */
|
||||
$userDao = app()->make(UserDao::class);
|
||||
$user = $userDao->get($uid);
|
||||
if (!$user) {
|
||||
throw new \RuntimeException("用户 {$uid} 不存在");
|
||||
}
|
||||
$newMoney = bcadd((string)$user['now_money'], (string)$amount, 2);
|
||||
$userDao->update($uid, ['now_money' => $newMoney], 'uid');
|
||||
|
||||
// 3. 写 user_bill 流水记录
|
||||
/** @var UserBillDao $billDao */
|
||||
$billDao = app()->make(UserBillDao::class);
|
||||
$billDao->save([
|
||||
'uid' => $uid,
|
||||
'link_id' => $queueId,
|
||||
'pm' => 1,
|
||||
'title' => '公排退款',
|
||||
'type' => 'queue_refund',
|
||||
'category' => 'now_money',
|
||||
'number' => $amount,
|
||||
'balance' => $newMoney,
|
||||
'mark' => "公排触发退款,批次#{$batchNo}",
|
||||
'status' => 1,
|
||||
'add_time' => time(),
|
||||
]);
|
||||
});
|
||||
|
||||
Log::info("[QueueRefund] 退款成功 uid={$uid} amount={$amount} batch={$batchNo}");
|
||||
} catch (\Throwable $e) {
|
||||
response_log_write([
|
||||
'message' => '公排退款失败: ' . $e->getMessage(),
|
||||
'file' => $e->getFile(),
|
||||
'line' => $e->getLine(),
|
||||
]);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -13,6 +13,7 @@ namespace app\listener\order;
|
||||
|
||||
|
||||
use app\jobs\agent\AgentJob;
|
||||
use app\jobs\hjf\HjfOrderPayJob;
|
||||
use app\jobs\order\OrderCreateAfterJob;
|
||||
use app\jobs\order\OrderDeliveryJob;
|
||||
use app\jobs\order\OrderJob;
|
||||
@@ -31,6 +32,7 @@ use app\services\order\StoreOrderCartInfoServices;
|
||||
use app\services\order\StoreOrderComputedServices;
|
||||
use app\services\order\StoreOrderCreateServices;
|
||||
use app\services\order\StoreOrderInvoiceServices;
|
||||
use app\services\order\StoreOrderTakeServices;
|
||||
use app\services\user\channel\ChannelMerchantServices;
|
||||
use app\services\user\UserMoneyServices;
|
||||
use app\services\user\UserServices;
|
||||
@@ -51,6 +53,19 @@ class Pay implements ListenerInterface
|
||||
//计算订单实际金额
|
||||
// OrderJob::dispatchDo('compute', [$orderInfo['uid'], $orderInfo['id']]);
|
||||
$this->compute($userInfo['uid'] ?? 0, $orderInfo['id']);
|
||||
|
||||
// fsgx: brokerage_timing=on_pay 时,支付即发放佣金(跳过确认收货流程)
|
||||
$brokerageTiming = sys_config('brokerage_timing', 'on_confirm');
|
||||
if ($brokerageTiming === 'on_pay' && !empty($orderInfo['uid'])) {
|
||||
try {
|
||||
/** @var \app\services\order\StoreOrderTakeServices $takeServices */
|
||||
$takeServices = app()->make(StoreOrderTakeServices::class);
|
||||
$takeServices->backOrderBrokerage($orderInfo, $userInfo);
|
||||
} catch (\Throwable $e) {
|
||||
Log::error('[Pay] brokerage_timing=on_pay 佣金发放失败 order_id=' . $orderInfo['id'] . ': ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
//创建拼团
|
||||
if ($orderInfo['activity_id'] && !$orderInfo['refund_status']) {
|
||||
//拼团
|
||||
@@ -126,6 +141,15 @@ class Pay implements ListenerInterface
|
||||
event('notice.notice', [$orderInfo, 'admin_pay_success_code']);
|
||||
//对外接口推送事件
|
||||
event('out.outPush', ['order_pay_push', ['order_id' => (int)$orderInfo['id']]]);
|
||||
|
||||
// 公排入队 + 积分奖励 + 等级升级(仅报单商品订单)
|
||||
if (!empty($orderInfo['is_queue_goods'])) {
|
||||
HjfOrderPayJob::dispatch([
|
||||
(int)$orderInfo['uid'],
|
||||
(string)$orderInfo['order_id'],
|
||||
(float)($orderInfo['pay_price'] ?? 3600.00),
|
||||
]);
|
||||
}
|
||||
//自动打标签
|
||||
event('user.auto.label', [$orderInfo['uid'], '', [], []]);
|
||||
|
||||
|
||||
@@ -194,7 +194,7 @@ class SystemTimer extends Cron implements ListenerInterface
|
||||
case 'holiday_gift_push_task':
|
||||
return app()->make(HolidayGiftPushServices::class)->handleHolidayGiftTask();
|
||||
case 'fsgx_release_frozen_points': // fsgx: 每日释放待释放积分 (0.4‰)
|
||||
return app()->make(\app\services\hjf\HjfPointsServices::class)->dailyReleasePoints();
|
||||
return app()->make(\app\services\hjf\PointsReleaseServices::class)->executeRelease();
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
/** @var SystemTimerServices $timerServices */
|
||||
|
||||
30
pro_v3.5.1/app/model/hjf/PointsReleaseLog.php
Normal file
30
pro_v3.5.1/app/model/hjf/PointsReleaseLog.php
Normal file
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace app\model\hjf;
|
||||
|
||||
use crmeb\basic\BaseModel;
|
||||
use crmeb\traits\ModelTrait;
|
||||
|
||||
/**
|
||||
* 积分释放日志模型
|
||||
* Class PointsReleaseLog
|
||||
* @package app\model\hjf
|
||||
*/
|
||||
class PointsReleaseLog extends BaseModel
|
||||
{
|
||||
use ModelTrait;
|
||||
|
||||
protected $pk = 'id';
|
||||
|
||||
protected $name = 'points_release_log';
|
||||
|
||||
protected $autoWriteTimestamp = 'int';
|
||||
|
||||
protected $createTime = 'add_time';
|
||||
|
||||
public function setAddTimeAttr(): int
|
||||
{
|
||||
return time();
|
||||
}
|
||||
}
|
||||
40
pro_v3.5.1/app/model/hjf/QueuePool.php
Normal file
40
pro_v3.5.1/app/model/hjf/QueuePool.php
Normal file
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace app\model\hjf;
|
||||
|
||||
use crmeb\basic\BaseModel;
|
||||
use crmeb\traits\ModelTrait;
|
||||
|
||||
/**
|
||||
* 公排池模型
|
||||
* Class QueuePool
|
||||
* @package app\model\hjf
|
||||
*/
|
||||
class QueuePool extends BaseModel
|
||||
{
|
||||
use ModelTrait;
|
||||
|
||||
protected $pk = 'id';
|
||||
|
||||
protected $name = 'queue_pool';
|
||||
|
||||
protected $autoWriteTimestamp = 'int';
|
||||
|
||||
protected $createTime = 'add_time';
|
||||
|
||||
public function setAddTimeAttr(): int
|
||||
{
|
||||
return time();
|
||||
}
|
||||
|
||||
/**
|
||||
* 状态文本
|
||||
* @param int $value
|
||||
* @return string
|
||||
*/
|
||||
public function getStatusTextAttr(mixed $value, array $data): string
|
||||
{
|
||||
return ($data['status'] ?? 0) === 1 ? '已退款' : '排队中';
|
||||
}
|
||||
}
|
||||
@@ -40,6 +40,133 @@ class AgentLevelServices extends BaseServices
|
||||
#[Inject]
|
||||
protected AgentLevelDao $dao;
|
||||
|
||||
/**
|
||||
* HJF 官方会员等级名称(与数据库插入数据一致)
|
||||
* 用于区分 CRMEB 默认「等级一/等级二…」与 HJF 创客/云店…
|
||||
*/
|
||||
public const HJF_OFFICIAL_LEVEL_NAMES = ['创客', '云店', '服务商', '分公司'];
|
||||
|
||||
/**
|
||||
* 一次查询并返回用户列表展示所需等级索引(供 UserServices::index() 调用)
|
||||
*
|
||||
* @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);
|
||||
}
|
||||
|
||||
/**
|
||||
* 从等级行列表构建用户列表展示用索引
|
||||
*
|
||||
* @param array $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])) {
|
||||
$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 默认行时,按 grade 改用 HJF 官方行
|
||||
* - 旧 id 已软删或误把 grade 写入 agent_level 时,按 byGradeAny 回退
|
||||
*
|
||||
* @param int $agentLevelId 用户的 agent_level 字段值
|
||||
* @param array $maps loadHjfUserListLevelMaps() 返回的索引
|
||||
* @return array|null
|
||||
*/
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据 agent_level ID 获取等级 grade(HJF 会员等级数字 0-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);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取某一个等级信息
|
||||
* @param int $id
|
||||
|
||||
@@ -20,6 +20,7 @@ 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;
|
||||
|
||||
|
||||
@@ -38,6 +39,11 @@ class AgentLevelTaskServices extends BaseServices
|
||||
* max_number 最大设定数值 0为不限定
|
||||
* min_number 最小设定数值
|
||||
* unit 单位
|
||||
*
|
||||
* type 6-8: HJF 会员等级升级任务类型(改造新增)
|
||||
* 6 = 直推报单单数(直推下级购买报单商品的订单数)
|
||||
* 7 = 伞下报单业绩(含业绩分离逻辑)
|
||||
* 8 = 最低直推人数
|
||||
* */
|
||||
protected array $TaskType = [
|
||||
[
|
||||
@@ -90,6 +96,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'
|
||||
],
|
||||
];
|
||||
|
||||
/**
|
||||
@@ -356,6 +392,20 @@ 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 +422,85 @@ class AgentLevelTaskServices extends BaseServices
|
||||
return [$msg, $userNumber, $isComplete];
|
||||
}
|
||||
|
||||
/**
|
||||
* 统计直推下级的报单订单数(type=6 任务)
|
||||
*
|
||||
* @param int $uid 用户 ID
|
||||
* @return int
|
||||
*/
|
||||
public function getDirectQueueOrderCount(int $uid): int
|
||||
{
|
||||
/** @var UserServices $userServices */
|
||||
$userServices = app()->make(UserServices::class);
|
||||
// no_assess=1 的用户不计入升级任务,仅统计正常考核下级
|
||||
$directUids = $userServices->getColumn(['spread_uid' => $uid, 'no_assess' => 0], '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)
|
||||
->where('no_assess', 0) // 不考核用户不计入伞下业绩
|
||||
->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
|
||||
|
||||
89
pro_v3.5.1/app/services/hjf/HjfAssetsServices.php
Normal file
89
pro_v3.5.1/app/services/hjf/HjfAssetsServices.php
Normal file
@@ -0,0 +1,89 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace app\services\hjf;
|
||||
|
||||
use app\dao\user\UserBillDao;
|
||||
use app\dao\user\UserDao;
|
||||
use app\services\BaseServices;
|
||||
use think\annotation\Inject;
|
||||
|
||||
/**
|
||||
* HJF 资产服务
|
||||
*
|
||||
* 提供用户资产总览和现金流水查询功能。
|
||||
* 复用 CRMEB 原有 eb_user(余额/积分)和 eb_user_bill(流水)字段。
|
||||
*
|
||||
* Class HjfAssetsServices
|
||||
* @package app\services\hjf
|
||||
*/
|
||||
class HjfAssetsServices extends BaseServices
|
||||
{
|
||||
#[Inject]
|
||||
protected UserDao $userDao;
|
||||
|
||||
#[Inject]
|
||||
protected UserBillDao $billDao;
|
||||
|
||||
/**
|
||||
* 获取用户资产总览
|
||||
*
|
||||
* 返回:
|
||||
* - now_money 现金余额
|
||||
* - frozen_points 待释放积分
|
||||
* - available_points 已释放积分(可消费)
|
||||
* - total_points 总积分(frozen + available)
|
||||
*
|
||||
* @param int $uid
|
||||
* @return array
|
||||
*/
|
||||
public function getOverview(int $uid): array
|
||||
{
|
||||
$user = $this->userDao->get($uid, 'uid,now_money,frozen_points,available_points');
|
||||
if (!$user) {
|
||||
return [
|
||||
'now_money' => '0.00',
|
||||
'frozen_points' => 0,
|
||||
'available_points' => 0,
|
||||
'total_points' => 0,
|
||||
];
|
||||
}
|
||||
|
||||
$frozen = (int)($user['frozen_points'] ?? 0);
|
||||
$available = (int)($user['available_points'] ?? 0);
|
||||
|
||||
return [
|
||||
'now_money' => number_format((float)($user['now_money'] ?? 0), 2, '.', ''),
|
||||
'frozen_points' => $frozen,
|
||||
'available_points' => $available,
|
||||
'total_points' => $frozen + $available,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取现金流水(分页)
|
||||
*
|
||||
* 复用 eb_user_bill 表,筛选 category='now_money' 的记录。
|
||||
*
|
||||
* @param int $uid
|
||||
* @param string $type 流水类型筛选('' = 全部,'queue_refund' 公排退款,'withdraw' 提现等)
|
||||
* @param int $page
|
||||
* @param int $limit
|
||||
* @return array
|
||||
*/
|
||||
public function getCashDetail(int $uid, string $type, int $page, int $limit): array
|
||||
{
|
||||
$where = [
|
||||
'uid' => $uid,
|
||||
'category' => 'now_money',
|
||||
];
|
||||
if ($type !== '') {
|
||||
$where['type'] = $type;
|
||||
}
|
||||
|
||||
$count = $this->billDao->count($where);
|
||||
$list = $this->billDao->getBalanceRecord($where, $page, $limit);
|
||||
|
||||
return compact('list', 'count');
|
||||
}
|
||||
}
|
||||
136
pro_v3.5.1/app/services/hjf/MemberLevelServices.php
Normal file
136
pro_v3.5.1/app/services/hjf/MemberLevelServices.php
Normal file
@@ -0,0 +1,136 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace app\services\hjf;
|
||||
|
||||
use app\services\agent\AgentLevelServices;
|
||||
use app\services\agent\AgentLevelTaskServices;
|
||||
use app\services\BaseServices;
|
||||
use app\services\user\UserServices;
|
||||
use think\annotation\Inject;
|
||||
use think\facade\Db;
|
||||
use think\facade\Log;
|
||||
|
||||
/**
|
||||
* 会员等级升级服务(改造复用版)
|
||||
*
|
||||
* 基于 CRMEB Pro 的团队分销等级功能进行改造:
|
||||
* - 使用 eb_user.agent_level (FK → eb_agent_level.id) 代替独立的 member_level
|
||||
* - 升级条件通过 eb_agent_level_task 的 type 6/7/8 定义
|
||||
* - 升级逻辑委托给 AgentLevelServices::checkUserLevelFinish()
|
||||
*
|
||||
* 本服务保留为薄封装层,提供 HJF 特有的查询方法供控制器调用。
|
||||
*
|
||||
* Class MemberLevelServices
|
||||
* @package app\services\hjf
|
||||
*/
|
||||
class MemberLevelServices extends BaseServices
|
||||
{
|
||||
#[Inject]
|
||||
protected AgentLevelServices $agentLevelServices;
|
||||
|
||||
#[Inject]
|
||||
protected AgentLevelTaskServices $agentLevelTaskServices;
|
||||
|
||||
/**
|
||||
* 检查并执行升级(异步触发入口)
|
||||
*
|
||||
* 委托给 CRMEB 的 AgentLevelServices 复用原有升级检测流程,
|
||||
* 该流程已支持 type 6/7/8 的 HJF 任务类型。
|
||||
*/
|
||||
public function checkUpgrade(int $uid): void
|
||||
{
|
||||
try {
|
||||
/** @var UserServices $userServices */
|
||||
$userServices = app()->make(UserServices::class);
|
||||
$userInfo = $userServices->getUserCacheInfo($uid);
|
||||
if (!$userInfo) {
|
||||
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]);
|
||||
|
||||
$this->agentLevelServices->checkUserLevelFinish($uid, $uids);
|
||||
} catch (\Throwable $e) {
|
||||
Log::error("[MemberLevel] checkUpgrade uid={$uid}: " . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户当前会员等级 grade(0=普通, 1=创客, 2=云店, 3=服务商, 4=分公司)
|
||||
*/
|
||||
public function getUserGrade(int $uid): int
|
||||
{
|
||||
$agentLevel = (int)Db::name('user')->where('uid', $uid)->value('agent_level');
|
||||
return $this->agentLevelServices->getGradeByLevelId($agentLevel);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户当前等级名称
|
||||
*/
|
||||
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
|
||||
{
|
||||
return $this->agentLevelTaskServices->getDirectQueueOrderCount($uid);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取直推人数
|
||||
*/
|
||||
public function getDirectSpreadCount(int $uid): int
|
||||
{
|
||||
/** @var UserServices $userServices */
|
||||
$userServices = app()->make(UserServices::class);
|
||||
return (int)$userServices->count(['spread_uid' => $uid]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取伞下总报单订单数(含业绩分离逻辑)
|
||||
*/
|
||||
public function getUmbrellaQueueOrderCount(int $uid): int
|
||||
{
|
||||
return $this->agentLevelTaskServices->getUmbrellaQueueOrderCount($uid);
|
||||
}
|
||||
|
||||
/**
|
||||
* 手动设置会员等级(管理后台使用)
|
||||
*
|
||||
* @param int $uid 用户 ID
|
||||
* @param int $grade 目标等级 grade (0-4)
|
||||
*/
|
||||
public function setUserLevel(int $uid, int $grade): void
|
||||
{
|
||||
$agentLevelId = 0;
|
||||
if ($grade > 0) {
|
||||
$agentLevelId = $this->agentLevelServices->getLevelIdByGrade($grade);
|
||||
if ($agentLevelId <= 0) {
|
||||
throw new \think\exception\ValidateException("等级 grade={$grade} 在 eb_agent_level 中不存在");
|
||||
}
|
||||
}
|
||||
|
||||
/** @var UserServices $userServices */
|
||||
$userServices = app()->make(UserServices::class);
|
||||
$userServices->update($uid, ['agent_level' => $agentLevelId]);
|
||||
|
||||
Log::info("[MemberLevel] 手动设置 uid={$uid} agent_level={$agentLevelId} (grade={$grade})");
|
||||
}
|
||||
}
|
||||
112
pro_v3.5.1/app/services/hjf/PointsReleaseServices.php
Normal file
112
pro_v3.5.1/app/services/hjf/PointsReleaseServices.php
Normal file
@@ -0,0 +1,112 @@
|
||||
<?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;
|
||||
|
||||
/**
|
||||
* 积分每日释放服务
|
||||
*
|
||||
* 由定时任务(每天凌晨00:01)或 Command 触发。
|
||||
* 计算公式:release_amount = FLOOR(frozen_points × rate / 1000)
|
||||
* 其中 rate = hjf_release_rate(默认 4,即 4‰)
|
||||
*
|
||||
* Class PointsReleaseServices
|
||||
* @package app\services\hjf
|
||||
*/
|
||||
class PointsReleaseServices extends BaseServices
|
||||
{
|
||||
#[Inject]
|
||||
protected PointsReleaseLogDao $logDao;
|
||||
|
||||
#[Inject]
|
||||
protected UserDao $userDao;
|
||||
|
||||
/**
|
||||
* 执行今日积分释放(批量)
|
||||
*
|
||||
* @return array 统计:['processed' => int, 'total_released' => int]
|
||||
*/
|
||||
public function executeRelease(): array
|
||||
{
|
||||
$rate = (int)SystemConfigService::get('hjf_release_rate', 4);
|
||||
$releaseDate = date('Y-m-d');
|
||||
$processed = 0;
|
||||
$totalReleased = 0;
|
||||
|
||||
// 分批处理,每批 200 条,避免内存溢合
|
||||
$page = 1;
|
||||
$limit = 200;
|
||||
|
||||
do {
|
||||
$users = $this->userDao->selectList(
|
||||
['frozen_points' => ['>', 0]],
|
||||
'uid,frozen_points,available_points',
|
||||
$page,
|
||||
$limit,
|
||||
'uid',
|
||||
'asc'
|
||||
);
|
||||
|
||||
if (empty($users)) {
|
||||
break;
|
||||
}
|
||||
|
||||
foreach ($users as $user) {
|
||||
$frozenBefore = (int)$user['frozen_points'];
|
||||
// 使用 bcmath 确保精度
|
||||
$releaseAmount = (int)bcdiv(bcmul((string)$frozenBefore, (string)$rate), '1000');
|
||||
|
||||
if ($releaseAmount <= 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$frozenAfter = $frozenBefore - $releaseAmount;
|
||||
|
||||
try {
|
||||
Db::transaction(function () use ($user, $releaseAmount, $frozenBefore, $frozenAfter, $releaseDate) {
|
||||
// 更新用户积分字段
|
||||
$this->userDao->update($user['uid'], [
|
||||
'frozen_points' => $frozenAfter,
|
||||
'available_points' => Db::raw('available_points + ' . $releaseAmount),
|
||||
], 'uid');
|
||||
|
||||
// 写 points_release_log(本次每日释放记录)
|
||||
$this->logDao->save([
|
||||
'uid' => $user['uid'],
|
||||
'points' => $releaseAmount,
|
||||
'pm' => 1,
|
||||
'type' => 'release',
|
||||
'title' => '每日释放',
|
||||
'mark' => "积分每日自动解冻,释放日期 {$releaseDate}",
|
||||
'status' => 'released',
|
||||
'release_date' => $releaseDate,
|
||||
]);
|
||||
});
|
||||
|
||||
$totalReleased += $releaseAmount;
|
||||
$processed++;
|
||||
} catch (\Throwable $e) {
|
||||
Log::error("[PointsRelease] uid={$user['uid']} 释放失败: " . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
$page++;
|
||||
} while (count($users) === $limit);
|
||||
|
||||
Log::info("[PointsRelease] 完成,processed={$processed} total_released={$totalReleased}");
|
||||
|
||||
return [
|
||||
'processed' => $processed,
|
||||
'total_released' => $totalReleased,
|
||||
'release_date' => $releaseDate,
|
||||
];
|
||||
}
|
||||
}
|
||||
135
pro_v3.5.1/app/services/hjf/PointsRewardServices.php
Normal file
135
pro_v3.5.1/app/services/hjf/PointsRewardServices.php
Normal file
@@ -0,0 +1,135 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace app\services\hjf;
|
||||
|
||||
use app\dao\hjf\PointsReleaseLogDao;
|
||||
use app\dao\user\UserDao;
|
||||
use app\services\agent\AgentLevelServices;
|
||||
use app\services\BaseServices;
|
||||
use think\annotation\Inject;
|
||||
use think\facade\Db;
|
||||
use think\facade\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
|
||||
*/
|
||||
class PointsRewardServices extends BaseServices
|
||||
{
|
||||
#[Inject]
|
||||
protected PointsReleaseLogDao $logDao;
|
||||
|
||||
#[Inject]
|
||||
protected UserDao $userDao;
|
||||
|
||||
#[Inject]
|
||||
protected AgentLevelServices $agentLevelServices;
|
||||
|
||||
/**
|
||||
* 对一笔报单订单发放积分奖励
|
||||
*/
|
||||
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 递归深度
|
||||
*/
|
||||
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;
|
||||
}
|
||||
|
||||
$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;
|
||||
}
|
||||
|
||||
$isDirect = ($depth === 0);
|
||||
$reward = $isDirect
|
||||
? $this->agentLevelServices->getDirectRewardPoints($agentLevelId)
|
||||
: $this->agentLevelServices->getUmbrellaRewardPoints($agentLevelId);
|
||||
|
||||
$actual = max(0, $reward - $lowerReward);
|
||||
|
||||
if ($actual > 0) {
|
||||
$this->grantFrozenPoints(
|
||||
$uid,
|
||||
$actual,
|
||||
$orderId,
|
||||
$isDirect ? 'reward_direct' : 'reward_umbrella',
|
||||
($isDirect ? '直推奖励' : '伞下奖励(级差)') . " - 来源订单 {$orderId}"
|
||||
);
|
||||
}
|
||||
|
||||
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) {
|
||||
$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,
|
||||
]);
|
||||
});
|
||||
}
|
||||
}
|
||||
193
pro_v3.5.1/app/services/hjf/QueuePoolServices.php
Normal file
193
pro_v3.5.1/app/services/hjf/QueuePoolServices.php
Normal file
@@ -0,0 +1,193 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace app\services\hjf;
|
||||
|
||||
use app\dao\hjf\QueuePoolDao;
|
||||
use app\jobs\hjf\QueueRefundJob;
|
||||
use app\services\BaseServices;
|
||||
use app\services\user\UserServices;
|
||||
use crmeb\services\CacheService;
|
||||
use crmeb\services\SystemConfigService;
|
||||
use think\annotation\Inject;
|
||||
use think\exception\ValidateException;
|
||||
use think\facade\Db;
|
||||
use think\facade\Log;
|
||||
|
||||
/**
|
||||
* 公排池服务
|
||||
*
|
||||
* 负责:入队(enqueue)+ 退款触发条件判断 + 统计信息查询。
|
||||
* 退款的实际执行委托给 QueueRefundJob(异步)以避免支付回调阻塞。
|
||||
*
|
||||
* Class QueuePoolServices
|
||||
* @package app\services\hjf
|
||||
* @mixin QueuePoolDao
|
||||
*/
|
||||
class QueuePoolServices extends BaseServices
|
||||
{
|
||||
#[Inject]
|
||||
protected QueuePoolDao $dao;
|
||||
|
||||
/** Redis 分布式锁 Key */
|
||||
const LOCK_KEY = 'hjf:queue:enqueue_lock';
|
||||
|
||||
/** 锁超时(秒) */
|
||||
const LOCK_TTL = 10;
|
||||
|
||||
/**
|
||||
* 报单商品订单入队
|
||||
*
|
||||
* 使用 Redis SET NX EX 分布式锁保证同一时刻只有一个入队+触发检测操作执行。
|
||||
*
|
||||
* @param int $uid 用户 ID
|
||||
* @param string $orderId 原始订单号
|
||||
* @param float $amount 金额(默认 3600.00)
|
||||
* @return array 新入队记录数组
|
||||
* @throws ValidateException
|
||||
*/
|
||||
public function enqueue(int $uid, string $orderId, float $amount = 3600.00): array
|
||||
{
|
||||
$lockKey = self::LOCK_KEY;
|
||||
$lockValue = uniqid('', true);
|
||||
|
||||
// 获取 Redis 实例
|
||||
/** @var \Redis $redis */
|
||||
$redis = CacheService::getRedis();
|
||||
|
||||
// SET NX EX 原子锁
|
||||
$acquired = $redis->set($lockKey, $lockValue, ['NX', 'EX' => self::LOCK_TTL]);
|
||||
if (!$acquired) {
|
||||
throw new ValidateException('公排入队繁忙,请稍后重试');
|
||||
}
|
||||
|
||||
try {
|
||||
return Db::transaction(function () use ($uid, $orderId, $amount, $redis, $lockKey, $lockValue) {
|
||||
$queueNo = $this->dao->nextQueueNo();
|
||||
|
||||
$record = $this->dao->save([
|
||||
'uid' => $uid,
|
||||
'order_id' => $orderId,
|
||||
'amount' => $amount,
|
||||
'queue_no' => $queueNo,
|
||||
'status' => 0,
|
||||
'refund_time' => 0,
|
||||
'trigger_batch' => 0,
|
||||
]);
|
||||
|
||||
$data = $record->toArray();
|
||||
|
||||
// 检查是否触发退款条件
|
||||
$this->checkAndTriggerRefund();
|
||||
|
||||
return $data;
|
||||
});
|
||||
} finally {
|
||||
// 释放锁(Lua 原子删除,防止误删他人的锁)
|
||||
$script = <<<'LUA'
|
||||
if redis.call("GET", KEYS[1]) == ARGV[1] then
|
||||
return redis.call("DEL", KEYS[1])
|
||||
else
|
||||
return 0
|
||||
end
|
||||
LUA;
|
||||
$redis->eval($script, [$lockKey, $lockValue], 1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否达到退款触发条件,若是则派发异步退款 Job
|
||||
*
|
||||
* 触发条件:当前排队中总单数 ≥ triggerMultiple(默认4),
|
||||
* 即每进入4单就对最早的1单触发退款。
|
||||
*/
|
||||
public function checkAndTriggerRefund(): void
|
||||
{
|
||||
$multiple = (int)SystemConfigService::get('hjf_trigger_multiple', 4);
|
||||
$pending = $this->dao->countPending();
|
||||
|
||||
if ($pending < $multiple) {
|
||||
return;
|
||||
}
|
||||
|
||||
$earliest = $this->dao->getEarliestPending();
|
||||
if (!$earliest) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 批次号 = 历史已退款总数 + 1
|
||||
$batchNo = $this->dao->count(['status' => 1]) + 1;
|
||||
|
||||
// 派发异步退款 Job
|
||||
QueueRefundJob::dispatch($earliest['id'], $earliest['uid'], $earliest['amount'], $batchNo);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户的公排状态摘要(用于状态页)
|
||||
*/
|
||||
public function getUserStatus(int $uid): array
|
||||
{
|
||||
$multiple = (int)SystemConfigService::get('hjf_trigger_multiple', 4);
|
||||
$pending = $this->dao->countPending();
|
||||
$total = $this->dao->countTotal();
|
||||
|
||||
// 当前批次已入队单数(本批次进度)
|
||||
$batchCount = $pending % $multiple;
|
||||
|
||||
// 用户自己的订单
|
||||
$myOrders = $this->dao->getModel()
|
||||
->where('uid', $uid)
|
||||
->order('add_time', 'desc')
|
||||
->select()
|
||||
->toArray();
|
||||
|
||||
foreach ($myOrders as &$item) {
|
||||
$item['estimated_wait'] = $item['status'] === 1
|
||||
? '已退款'
|
||||
: $this->estimateWait((int)$item['queue_no'], $pending, $multiple);
|
||||
}
|
||||
unset($item);
|
||||
|
||||
return [
|
||||
'total_orders' => $total,
|
||||
'my_orders' => $myOrders,
|
||||
'progress' => [
|
||||
'current_batch_count' => $batchCount,
|
||||
'trigger_multiple' => $multiple,
|
||||
'next_refund_queue_no' => $this->dao->getEarliestPending()['queue_no'] ?? 0,
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户公排历史(分页,支持按状态筛选)
|
||||
*/
|
||||
public function getUserHistory(int $uid, int $status, int $page, int $limit): array
|
||||
{
|
||||
$result = $this->dao->getUserList($uid, $status, $page, $limit);
|
||||
|
||||
foreach ($result['list'] as &$item) {
|
||||
$item['time_key'] = date('Y-m-d', (int)$item['add_time']);
|
||||
}
|
||||
unset($item);
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 简单估算等待时间(基于队列位置)
|
||||
*/
|
||||
private function estimateWait(int $queueNo, int $pending, int $multiple): string
|
||||
{
|
||||
$earliest = $this->dao->getEarliestPending();
|
||||
if (!$earliest) {
|
||||
return '--';
|
||||
}
|
||||
$positionFromFront = $queueNo - (int)$earliest['queue_no'];
|
||||
if ($positionFromFront <= 0) {
|
||||
return '即将退款';
|
||||
}
|
||||
$waitCycles = (int)ceil($positionFromFront / $multiple);
|
||||
return "约等待 {$waitCycles} 轮";
|
||||
}
|
||||
}
|
||||
@@ -954,7 +954,11 @@ class StoreOrderCreateServices extends BaseServices
|
||||
} else {
|
||||
$is_brokerage = $productInfo['is_brokerage'] ?? 0;
|
||||
}
|
||||
if ($is_brokerage == 0) {
|
||||
// fsgx: is_queue_goods=1 的报单商品,即使 is_brokerage=0 也参与周期佣金计算
|
||||
$isQueueGoodsProduct = (int)($productInfo['is_queue_goods'] ?? 0);
|
||||
$brokerageScopeCheck = sys_config('brokerage_scope', 'all');
|
||||
$queueGoodsBypassBrokerage = ($brokerageScopeCheck === 'queue_only' && $isQueueGoodsProduct === 1);
|
||||
if ($is_brokerage == 0 && !$queueGoodsBypassBrokerage) {
|
||||
continue;
|
||||
}
|
||||
//指定返佣金额
|
||||
@@ -999,7 +1003,7 @@ class StoreOrderCreateServices extends BaseServices
|
||||
|
||||
if ($useCycleBrokerage && $spread_uid > 0) {
|
||||
// 统计推荐人下级已完成的有效报单商品订单数,取模得到当前位次
|
||||
// is_queue_goods 冗余存储在订单表(eb_store_order),由创建订单时写入
|
||||
// 注意:compute() 在 paid=1 之后执行,当前订单已被计入,需 -1 得到"之前完成单数"
|
||||
/** @var \app\dao\order\StoreOrderDao $orderDao */
|
||||
$orderDao = app()->make(\app\dao\order\StoreOrderDao::class);
|
||||
$completedCount = $orderDao->count([
|
||||
@@ -1008,7 +1012,7 @@ class StoreOrderCreateServices extends BaseServices
|
||||
'paid' => 1,
|
||||
'is_del' => 0,
|
||||
]);
|
||||
$position = $completedCount % $cycleCount;
|
||||
$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);
|
||||
|
||||
@@ -139,8 +139,9 @@ class StoreOrderTakeServices extends BaseServices
|
||||
$res = $this->transaction(function () use ($order, $userInfo, $storeTitle) {
|
||||
//赠送积分
|
||||
$res1 = $this->gainUserIntegral($order, $userInfo, $storeTitle);
|
||||
//返佣
|
||||
$res2 = $this->backOrderBrokerage($order, $userInfo);
|
||||
// fsgx: brokerage_timing=on_pay 时佣金已在支付时发放,此处跳过
|
||||
$brokerageTiming = sys_config('brokerage_timing', 'on_confirm');
|
||||
$res2 = ($brokerageTiming === 'on_pay') ? true : $this->backOrderBrokerage($order, $userInfo);
|
||||
//经验
|
||||
$res3 = $this->gainUserExp($order, $userInfo);
|
||||
//事业部
|
||||
|
||||
@@ -1494,9 +1494,9 @@ class SystemConfigServices extends BaseServices implements ServeConfigInterface
|
||||
])->trueValue('开启', 1)->falseValue('关闭', 0)->info($data['brokerage_user_status']['desc']),
|
||||
])->option('推荐佣金(fsgx)', [
|
||||
Build::inputNum('brokerage_cycle_count', $data['brokerage_cycle_count']['info'] ?: '佣金周期人数', (int)($data['brokerage_cycle_count']['value'] ?: 3))->min(1)->info('推荐N人为一个周期,循环计算各档佣金比例'),
|
||||
Build::input('brokerage_cycle_rates', $data['brokerage_cycle_rates']['info'] ?: '各档佣金比例(JSON)', $data['brokerage_cycle_rates']['value'] ?: '[20,30,50]')->info('JSON数组,元素为百分比整数,如[20,30,50]表示第1人20%、第2人30%、第3人50%'),
|
||||
Build::radio('brokerage_scope', $data['brokerage_scope']['info'] ?: '返佣范围', $data['brokerage_scope']['value'] ?: 'queue_only')->options([['label' => '所有商品', 'value' => 'all'], ['label' => '仅报单商品', 'value' => 'queue_only']])->info('queue_only=仅is_queue_goods=1的商品参与佣金计算'),
|
||||
Build::radio('brokerage_timing', $data['brokerage_timing']['info'] ?: '佣金发放时机', $data['brokerage_timing']['value'] ?: 'on_pay')->options([['label' => '支付即发放', 'value' => 'on_pay'], ['label' => '确认收货后发放', 'value' => 'on_confirm']])->info('on_pay=订单支付后立即发放 on_confirm=用户确认收货后发放'),
|
||||
Build::input('brokerage_cycle_rates', $data['brokerage_cycle_rates']['info'] ?: '各档佣金比例(JSON)', is_array($data['brokerage_cycle_rates']['value']) ? json_encode($data['brokerage_cycle_rates']['value']) : ($data['brokerage_cycle_rates']['value'] ?: '[20,30,50]'))->info('JSON数组,元素为百分比整数,如[20,30,50]表示第1人20%、第2人30%、第3人50%'),
|
||||
Build::radio('brokerage_scope', $data['brokerage_scope']['info'] ?: '返佣范围', is_array($data['brokerage_scope']['value']) ? ($data['brokerage_scope']['value'][0] ?? 'queue_only') : ($data['brokerage_scope']['value'] ?: 'queue_only'))->options([['label' => '所有商品', 'value' => 'all'], ['label' => '仅报单商品', 'value' => 'queue_only']])->info('queue_only=仅is_queue_goods=1的商品参与佣金计算'),
|
||||
Build::radio('brokerage_timing', $data['brokerage_timing']['info'] ?: '佣金发放时机', is_array($data['brokerage_timing']['value']) ? ($data['brokerage_timing']['value'][0] ?? 'on_pay') : ($data['brokerage_timing']['value'] ?: 'on_pay'))->options([['label' => '支付即发放', 'value' => 'on_pay'], ['label' => '确认收货后发放', 'value' => 'on_confirm']])->info('on_pay=订单支付后立即发放 on_confirm=用户确认收货后发放'),
|
||||
])
|
||||
]);
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ namespace app\services\system\timer;
|
||||
use app\services\BaseServices;
|
||||
use crmeb\exceptions\AdminException;
|
||||
use app\dao\system\timer\SystemTimerDao;
|
||||
use app\listener\system\timer\SystemTimer as SystemTimerListener;
|
||||
use think\annotation\Inject;
|
||||
|
||||
/**
|
||||
@@ -231,6 +232,19 @@ class SystemTimerServices extends BaseServices
|
||||
return $timer->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* 手动立即执行指定定时任务(HTTP 请求上下文,绕过 Swoole Cron 构造,用反射直接调用 implement_timer)
|
||||
* @param string $mark
|
||||
* @return void
|
||||
*/
|
||||
public function runNow(string $mark): void
|
||||
{
|
||||
$this->update(['mark' => $mark], ['last_execution_time' => time()]);
|
||||
$ref = new \ReflectionClass(SystemTimerListener::class);
|
||||
$instance = $ref->newInstanceWithoutConstructor();
|
||||
$instance->implement_timer($mark);
|
||||
}
|
||||
|
||||
/**获取下次执行时间
|
||||
* @param $type
|
||||
* @param $cycle
|
||||
|
||||
@@ -204,6 +204,24 @@ class UserBillServices extends BaseServices
|
||||
'status' => 1,
|
||||
'pm' => 1
|
||||
],
|
||||
// fsgx: 佣金转冻结积分
|
||||
'frozen_points_brokerage' => [
|
||||
'title' => '佣金奖励积分(待释放)',
|
||||
'category' => 'integral',
|
||||
'type' => 'frozen_points_brokerage',
|
||||
'mark' => '获得待释放积分{%num%}',
|
||||
'status' => 1,
|
||||
'pm' => 1
|
||||
],
|
||||
// fsgx: 每日释放冻结积分
|
||||
'frozen_points_release' => [
|
||||
'title' => '积分每日释放',
|
||||
'category' => 'integral',
|
||||
'type' => 'frozen_points_release',
|
||||
'mark' => '每日释放积分{%num%}',
|
||||
'status' => 1,
|
||||
'pm' => 1
|
||||
],
|
||||
];
|
||||
|
||||
/**
|
||||
|
||||
@@ -770,6 +770,21 @@ class UserServices extends BaseServices
|
||||
// 添加过滤条件
|
||||
$where['is_filter_del'] = 1;
|
||||
|
||||
// HJF:按分销等级 grade 筛选,转换为 agent_level ID 范围
|
||||
if (isset($where['hjf_member_level']) && $where['hjf_member_level'] !== '') {
|
||||
$grade = (int)$where['hjf_member_level'];
|
||||
/** @var AgentLevelServices $agentLevelSvc */
|
||||
$agentLevelSvc = app()->make(AgentLevelServices::class);
|
||||
if ($grade > 0) {
|
||||
$levelId = $agentLevelSvc->getLevelIdByGrade($grade);
|
||||
$where['agent_level'] = $levelId > 0 ? $levelId : -1;
|
||||
} else {
|
||||
// grade=0 表示"无分销等级"
|
||||
$where['agent_level'] = 0;
|
||||
}
|
||||
}
|
||||
unset($where['hjf_member_level']);
|
||||
|
||||
/** @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';
|
||||
@@ -799,6 +814,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;
|
||||
@@ -878,6 +898,14 @@ class UserServices extends BaseServices
|
||||
$item['svip_over_day'] = 0;
|
||||
}
|
||||
|
||||
// 分销等级(HJF 扩展:member_level=grade 数值,member_level_name=等级名称)
|
||||
$agentLevelId = (int)($item['agent_level'] ?? 0);
|
||||
$hjfLevelInfo = $agentLevelServices->pickHjfLevelRowForUserListDisplay($agentLevelId, $hjfLevelMaps);
|
||||
$item['member_level'] = $hjfLevelInfo ? (int)$hjfLevelInfo['grade'] : null;
|
||||
$item['member_level_name'] = $hjfLevelInfo ? ($hjfLevelInfo['name'] ?? '') : '';
|
||||
$item['available_points'] = (int)($item['available_points'] ?? 0);
|
||||
$item['frozen_points'] = (int)($item['frozen_points'] ?? 0);
|
||||
|
||||
// 标签
|
||||
$item['labels'] = $userlabel[$item['uid']] ?? '';
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user