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:
apple
2026-03-24 11:59:09 +08:00
parent 434aa8c69d
commit 76ccb24679
59 changed files with 2902 additions and 237 deletions

View 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;
}
}

View File

@@ -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]);

View 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('保存成功');
}
}

View 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));
}
}

View 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));
}
}

View File

@@ -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

View File

@@ -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;

View File

@@ -85,6 +85,8 @@ class User extends AuthController
['isMember', ''],
['label_ids', ''],
['is_channel', ''],
/** HJF按分销等级 grade04筛选对应 eb_user.agent_level */
['hjf_member_level', ''],
]);
if ($where['label_ids']) {
$where['label_id'] = stringToIntArray($where['label_ids']);

View 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)
);
}
}

View File

@@ -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,

View File

@@ -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,

View 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'));
}
}

View 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)
);
}
}

View 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];
}
}

View 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,
],
];
}
}

View 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, '.', ''),
];
}
}

View File

@@ -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']);

View 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;
}
}

View 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;
}
}

View 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;
}
}

View 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;
}
}

View File

@@ -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'], '', [], []]);

View File

@@ -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 */

View 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();
}
}

View 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 ? '已退款' : '排队中';
}
}

View File

@@ -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 获取等级 gradeHJF 会员等级数字 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

View File

@@ -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

View 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');
}
}

View 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());
}
}
/**
* 获取用户当前会员等级 grade0=普通, 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})");
}
}

View 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,
];
}
}

View 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,
]);
});
}
}

View 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}";
}
}

View File

@@ -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);

View File

@@ -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);
//事业部

View File

@@ -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=用户确认收货后发放'),
])
]);

View File

@@ -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

View File

@@ -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
],
];
/**

View File

@@ -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']] ?? '';

View File

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

View File

@@ -29,8 +29,8 @@ return [
'type' => env('DATABASE_TYPE', 'mysql'),
// 服务器地址
'hostname' => env('DATABASE_HOSTNAME', '127.0.0.1'),
// 数据库名(直接写死,避免 .env 路径问题
'database' => 'fsgx-shop',
// 数据库名(与 .env 中 [DATABASE] DATABASE= 一致键名database.database
'database' => env('database.database', 'fsgx-shop'),
// 用户名
'username' => env('DATABASE_USERNAME', 'root'),
// 密码

View File

@@ -0,0 +1,42 @@
/*
Navicat Premium Dump SQL
Source Server : jxy-hjf-db
Source Server Type : MySQL
Source Server Version : 50740 (5.7.40-log)
Source Host : 182.92.142.158:3306
Source Schema : hjfshop
Target Server Type : MySQL
Target Server Version : 50740 (5.7.40-log)
File Encoding : 65001
Date: 24/03/2026 11:17:55
*/
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- Table structure for eb_points_release_log
-- ----------------------------
DROP TABLE IF EXISTS `eb_points_release_log`;
CREATE TABLE `eb_points_release_log` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '自增主键',
`uid` int(11) NOT NULL DEFAULT '0' COMMENT '用户 ID',
`points` int(11) NOT NULL DEFAULT '0' COMMENT '积分数量(绝对值)',
`pm` tinyint(1) NOT NULL DEFAULT '1' COMMENT '收支方向1=收入 0=支出',
`type` varchar(50) NOT NULL DEFAULT '' COMMENT '类型reward_direct/reward_umbrella/release/consume',
`title` varchar(255) NOT NULL DEFAULT '' COMMENT '标题',
`mark` varchar(500) NOT NULL DEFAULT '' COMMENT '备注',
`status` varchar(30) NOT NULL DEFAULT 'frozen' COMMENT '状态frozen=冻结 released=已释放 consumed=已消费',
`order_id` varchar(50) NOT NULL DEFAULT '' COMMENT '关联订单号(奖励来源),释放记录为空',
`release_date` date DEFAULT NULL COMMENT '释放日期(每日释放时填写)',
`add_time` int(11) NOT NULL DEFAULT '0' COMMENT '记录时间Unix 时间戳)',
PRIMARY KEY (`id`),
KEY `idx_uid_type` (`uid`,`type`),
KEY `idx_uid_add_time` (`uid`,`add_time`),
KEY `idx_release_date` (`release_date`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='积分释放明细日志';
SET FOREIGN_KEY_CHECKS = 1;

View File

@@ -1,15 +1,17 @@
-- ============================================================
-- 黄精粉健康商城 HJF 数据库迁移脚本
-- 版本Phase 2
-- 日期2026-03-15
-- 版本Phase 3改造复用版
-- 日期2026-03-21
-- 执行说明:
-- 1. 兼容 MySQL 5.7+,数据库前缀为 eb_
-- 2. 按顺序执行 P2-01 ~ P2-05
-- 2. 按顺序执行 P3-01 ~ P3-07
-- 3. 所有操作均做幂等处理,可重复执行
-- 4. 遵循 PRD 改造复用原则:会员等级复用 eb_agent_level 体系,
-- 使用 eb_user.agent_level (FK) 代替独立的 member_level 字段
-- ============================================================
-- ============================================================
-- P2-01: 公排池表
-- P3-01: 公排池表
-- ============================================================
CREATE TABLE IF NOT EXISTS `eb_queue_pool` (
@@ -31,7 +33,7 @@ CREATE TABLE IF NOT EXISTS `eb_queue_pool` (
-- ============================================================
-- P2-02: 积分释放日志表
-- P3-02: 积分释放日志表
-- ============================================================
CREATE TABLE IF NOT EXISTS `eb_points_release_log` (
@@ -54,10 +56,42 @@ CREATE TABLE IF NOT EXISTS `eb_points_release_log` (
-- ============================================================
-- P2-03 / P2-04: eb_user / eb_store_product / eb_store_order 扩展字段
-- P3-03: eb_agent_level 扩展字段(改造复用:增加积分奖励字段)
-- ============================================================
DROP PROCEDURE IF EXISTS `hjf_migrate_agent_level`;
DELIMITER $$
CREATE PROCEDURE `hjf_migrate_agent_level`()
BEGIN
IF NOT EXISTS (
SELECT 1 FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'eb_agent_level' AND COLUMN_NAME = 'direct_reward_points'
) THEN
ALTER TABLE `eb_agent_level`
ADD COLUMN `direct_reward_points` int(11) NOT NULL DEFAULT 0 COMMENT '直推奖励积分(每单)';
END IF;
IF NOT EXISTS (
SELECT 1 FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'eb_agent_level' AND COLUMN_NAME = 'umbrella_reward_points'
) THEN
ALTER TABLE `eb_agent_level`
ADD COLUMN `umbrella_reward_points` int(11) NOT NULL DEFAULT 0 COMMENT '伞下奖励积分(每单,级差基数)';
END IF;
END$$
DELIMITER ;
CALL `hjf_migrate_agent_level`();
DROP PROCEDURE IF EXISTS `hjf_migrate_agent_level`;
-- ============================================================
-- P3-04: eb_user / eb_store_product / eb_store_order 扩展字段
--
-- MySQL 5.7 不支持 "ADD COLUMN IF NOT EXISTS"
-- 改用存储过程 + information_schema 实现幂等检查。
-- 注意:不再新增 member_level 字段,复用已有的 agent_level (FK→eb_agent_level.id)
-- ============================================================
DROP PROCEDURE IF EXISTS `hjf_migrate_columns`;
@@ -66,15 +100,7 @@ DELIMITER $$
CREATE PROCEDURE `hjf_migrate_columns`()
BEGIN
-- ---- eb_user 字段 ----
IF NOT EXISTS (
SELECT 1 FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'eb_user' AND COLUMN_NAME = 'member_level'
) THEN
ALTER TABLE `eb_user`
ADD COLUMN `member_level` tinyint(1) NOT NULL DEFAULT 0 COMMENT '会员等级0普通 1创客 2云店 3服务商 4分公司';
END IF;
-- ---- eb_user 字段(不含 member_level复用 agent_level----
IF NOT EXISTS (
SELECT 1 FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'eb_user' AND COLUMN_NAME = 'no_assess'
@@ -99,14 +125,6 @@ BEGIN
ADD COLUMN `available_points` int(11) NOT NULL DEFAULT 0 COMMENT '可用积分';
END IF;
-- eb_user 索引idx_member_level
IF NOT EXISTS (
SELECT 1 FROM information_schema.STATISTICS
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'eb_user' AND INDEX_NAME = 'idx_member_level'
) THEN
ALTER TABLE `eb_user` ADD INDEX `idx_member_level` (`member_level`);
END IF;
-- ---- eb_store_product 字段 ----
IF NOT EXISTS (
SELECT 1 FROM information_schema.COLUMNS
@@ -124,7 +142,6 @@ BEGIN
ADD COLUMN `allow_pay_types` varchar(255) NOT NULL DEFAULT '' COMMENT '允许积分支付类型JSON数组';
END IF;
-- eb_store_product 索引idx_is_queue_goods
IF NOT EXISTS (
SELECT 1 FROM information_schema.STATISTICS
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'eb_store_product' AND INDEX_NAME = 'idx_is_queue_goods'
@@ -141,7 +158,6 @@ BEGIN
ADD COLUMN `is_queue_goods` tinyint(1) NOT NULL DEFAULT 0 COMMENT '是否报单商品订单1=是';
END IF;
-- eb_store_order 索引idx_is_queue_goods
IF NOT EXISTS (
SELECT 1 FROM information_schema.STATISTICS
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'eb_store_order' AND INDEX_NAME = 'idx_is_queue_goods'
@@ -157,115 +173,221 @@ DROP PROCEDURE IF EXISTS `hjf_migrate_columns`;
-- ============================================================
-- P2-05: eb_system_config 初始化配置项
-- P3-05: 初始化会员等级数据到 eb_agent_level改造复用
--
-- 字段说明(与 CRMEB 原表保持一致)
-- menu_name = 配置键名(代码中 SystemConfigService::get() 读取)
-- value = 默认值(字符串)
-- info = 后台显示名称
-- desc = 说明文字
-- config_tab_id = 0不归属某分组便于独立管理
-- status = 1启用
-- 将原分销员等级改为五级会员等级体系
-- grade=1 → 创客 (direct=500, umbrella=0)
-- grade=2 → 云店 (direct=800, umbrella=300)
-- grade=3 → 服务商 (direct=1000, umbrella=200)
-- grade=4 → 分公司 (direct=1300, umbrella=300)
--
-- 注意:普通会员 = agent_level=0无记录不需要插入
--
-- 先将 CRMEB 原有 demo 等级软删除,然后插入 HJF 会员等级
-- ============================================================
UPDATE `eb_agent_level`
SET `is_del` = 1
WHERE `name` NOT IN ('创客', '云店', '服务商', '分公司')
AND `is_del` = 0;
INSERT INTO `eb_agent_level`
(`name`, `grade`, `image`, `color`, `one_brokerage`, `two_brokerage`,
`direct_reward_points`, `umbrella_reward_points`, `status`, `is_del`, `add_time`)
SELECT '创客', 1, '', '#FF9800', 0, 0, 500, 0, 1, 0, UNIX_TIMESTAMP()
FROM DUAL
WHERE NOT EXISTS (
SELECT 1 FROM `eb_agent_level` WHERE `name` = '创客' AND `is_del` = 0
);
INSERT INTO `eb_agent_level`
(`name`, `grade`, `image`, `color`, `one_brokerage`, `two_brokerage`,
`direct_reward_points`, `umbrella_reward_points`, `status`, `is_del`, `add_time`)
SELECT '云店', 2, '', '#2196F3', 0, 0, 800, 300, 1, 0, UNIX_TIMESTAMP()
FROM DUAL
WHERE NOT EXISTS (
SELECT 1 FROM `eb_agent_level` WHERE `name` = '云店' AND `is_del` = 0
);
INSERT INTO `eb_agent_level`
(`name`, `grade`, `image`, `color`, `one_brokerage`, `two_brokerage`,
`direct_reward_points`, `umbrella_reward_points`, `status`, `is_del`, `add_time`)
SELECT '服务商', 3, '', '#9C27B0', 0, 0, 1000, 200, 1, 0, UNIX_TIMESTAMP()
FROM DUAL
WHERE NOT EXISTS (
SELECT 1 FROM `eb_agent_level` WHERE `name` = '服务商' AND `is_del` = 0
);
INSERT INTO `eb_agent_level`
(`name`, `grade`, `image`, `color`, `one_brokerage`, `two_brokerage`,
`direct_reward_points`, `umbrella_reward_points`, `status`, `is_del`, `add_time`)
SELECT '分公司', 4, '', '#F44336', 0, 0, 1300, 300, 1, 0, UNIX_TIMESTAMP()
FROM DUAL
WHERE NOT EXISTS (
SELECT 1 FROM `eb_agent_level` WHERE `name` = '分公司' AND `is_del` = 0
);
-- ============================================================
-- P3-06: 初始化等级升级任务到 eb_agent_level_task改造复用
--
-- 新增任务类型:
-- type=6 → 直推报单单数
-- type=7 → 伞下报单业绩(含业绩分离)
-- type=8 → 最低直推人数
--
-- 各等级任务配置:
-- 创客(grade=1): type=6, number=3 (直推3单)
-- 云店(grade=2): type=7, number=30 (伞下30单) + type=8, number=3 (至少3直推)
-- 服务商(grade=3): type=7, number=100 + type=8, number=3
-- 分公司(grade=4): type=7, number=1000 + type=8, number=3
-- ============================================================
DROP PROCEDURE IF EXISTS `hjf_init_agent_tasks`;
DELIMITER $$
CREATE PROCEDURE `hjf_init_agent_tasks`()
BEGIN
DECLARE v_level_id_1 INT DEFAULT 0;
DECLARE v_level_id_2 INT DEFAULT 0;
DECLARE v_level_id_3 INT DEFAULT 0;
DECLARE v_level_id_4 INT DEFAULT 0;
SELECT id INTO v_level_id_1 FROM eb_agent_level WHERE grade = 1 AND is_del = 0 LIMIT 1;
SELECT id INTO v_level_id_2 FROM eb_agent_level WHERE grade = 2 AND is_del = 0 LIMIT 1;
SELECT id INTO v_level_id_3 FROM eb_agent_level WHERE grade = 3 AND is_del = 0 LIMIT 1;
SELECT id INTO v_level_id_4 FROM eb_agent_level WHERE grade = 4 AND is_del = 0 LIMIT 1;
-- 创客直推报单3单
IF v_level_id_1 > 0 AND NOT EXISTS (
SELECT 1 FROM eb_agent_level_task WHERE level_id = v_level_id_1 AND type = 6 AND is_del = 0
) THEN
INSERT INTO eb_agent_level_task (level_id, `name`, type, number, `desc`, sort, status, is_del, add_time)
VALUES (v_level_id_1, '直推报单满3单', 6, 3, '直推下级购买报单商品满3单升级为创客', 1, 1, 0, UNIX_TIMESTAMP());
END IF;
-- 云店伞下报单30单 + 至少3个直推
IF v_level_id_2 > 0 THEN
IF NOT EXISTS (
SELECT 1 FROM eb_agent_level_task WHERE level_id = v_level_id_2 AND type = 7 AND is_del = 0
) THEN
INSERT INTO eb_agent_level_task (level_id, `name`, type, number, `desc`, sort, status, is_del, add_time)
VALUES (v_level_id_2, '伞下报单满30单', 7, 30, '伞下业绩含分离达到30单升级为云店', 1, 1, 0, UNIX_TIMESTAMP());
END IF;
IF NOT EXISTS (
SELECT 1 FROM eb_agent_level_task WHERE level_id = v_level_id_2 AND type = 8 AND is_del = 0
) THEN
INSERT INTO eb_agent_level_task (level_id, `name`, type, number, `desc`, sort, status, is_del, add_time)
VALUES (v_level_id_2, '至少3个直推', 8, 3, '需至少3个直推下级才可升级为云店', 2, 1, 0, UNIX_TIMESTAMP());
END IF;
END IF;
-- 服务商伞下报单100单 + 至少3个直推
IF v_level_id_3 > 0 THEN
IF NOT EXISTS (
SELECT 1 FROM eb_agent_level_task WHERE level_id = v_level_id_3 AND type = 7 AND is_del = 0
) THEN
INSERT INTO eb_agent_level_task (level_id, `name`, type, number, `desc`, sort, status, is_del, add_time)
VALUES (v_level_id_3, '伞下报单满100单', 7, 100, '伞下业绩含分离达到100单升级为服务商', 1, 1, 0, UNIX_TIMESTAMP());
END IF;
IF NOT EXISTS (
SELECT 1 FROM eb_agent_level_task WHERE level_id = v_level_id_3 AND type = 8 AND is_del = 0
) THEN
INSERT INTO eb_agent_level_task (level_id, `name`, type, number, `desc`, sort, status, is_del, add_time)
VALUES (v_level_id_3, '至少3个直推', 8, 3, '需至少3个直推下级才可升级为服务商', 2, 1, 0, UNIX_TIMESTAMP());
END IF;
END IF;
-- 分公司伞下报单1000单 + 至少3个直推
IF v_level_id_4 > 0 THEN
IF NOT EXISTS (
SELECT 1 FROM eb_agent_level_task WHERE level_id = v_level_id_4 AND type = 7 AND is_del = 0
) THEN
INSERT INTO eb_agent_level_task (level_id, `name`, type, number, `desc`, sort, status, is_del, add_time)
VALUES (v_level_id_4, '伞下报单满1000单', 7, 1000, '伞下业绩含分离达到1000单升级为分公司', 1, 1, 0, UNIX_TIMESTAMP());
END IF;
IF NOT EXISTS (
SELECT 1 FROM eb_agent_level_task WHERE level_id = v_level_id_4 AND type = 8 AND is_del = 0
) THEN
INSERT INTO eb_agent_level_task (level_id, `name`, type, number, `desc`, sort, status, is_del, add_time)
VALUES (v_level_id_4, '至少3个直推', 8, 3, '需至少3个直推下级才可升级为分公司', 2, 1, 0, UNIX_TIMESTAMP());
END IF;
END IF;
END$$
DELIMITER ;
CALL `hjf_init_agent_tasks`();
DROP PROCEDURE IF EXISTS `hjf_init_agent_tasks`;
-- ============================================================
-- P3-07: eb_system_config 初始化配置项
-- ============================================================
-- 防止重复执行报错,使用 INSERT IGNORE
INSERT IGNORE INTO `eb_system_config`
(`is_store`, `menu_name`, `type`, `input_type`, `config_tab_id`,
`parameter`, `upload_type`, `required`, `width`, `high`,
`value`, `info`, `desc`, `sort`, `status`)
VALUES
-- 公排触发倍数:每入 N 单退款第1单默认 4
(0, 'hjf_trigger_multiple', 'text', 'input', 0,
'', 0, '', 100, 0,
'4', '公排触发倍数', '每进入N单公排触发退款第1单默认4', 10, 1),
-- 积分每日释放比例(‰,默认 4即 4‰
(0, 'hjf_release_rate', 'text', 'input', 0,
'', 0, '', 100, 0,
'4', '积分每日释放比例(‰)', '每日释放frozen_points × N / 1000默认4即4‰', 20, 1),
-- 提现手续费率(%,默认 7即 7%
(0, 'hjf_fee_rate', 'text', 'input', 0,
'', 0, '', 100, 0,
'7', '提现手续费率(%)', '申请提现时收取的手续费比例默认7%', 30, 1),
'7', '提现手续费率(%)', '申请提现时收取的手续费比例默认7%', 30, 1);
-- 等级升级门槛普通→创客直推N单
(0, 'hjf_level_direct_require_1', 'text', 'input', 0,
'', 0, '', 100, 0,
'3', '创客升级所需直推单数', '普通会员直推N单报单商品后升级为创客默认3', 40, 1),
-- 等级升级门槛创客→云店伞下N单
(0, 'hjf_level_umbrella_require_2', 'text', 'input', 0,
'', 0, '', 100, 0,
'30', '云店升级所需伞下单数', '创客伞下业绩达到N单后升级为云店默认30', 50, 1),
-- ============================================================
-- P3-08: 如果已有旧的 member_level 字段,将数据迁移到 agent_level
-- ============================================================
-- 等级升级门槛云店→服务商伞下N单
(0, 'hjf_level_umbrella_require_3', 'text', 'input', 0,
'', 0, '', 100, 0,
'100', '服务商升级所需伞下单数', '云店伞下业绩达到N单后升级为服务商默认100', 60, 1),
DROP PROCEDURE IF EXISTS `hjf_migrate_member_to_agent_level`;
-- 等级升级门槛服务商→分公司伞下N单
(0, 'hjf_level_umbrella_require_4', 'text', 'input', 0,
'', 0, '', 100, 0,
'1000', '分公司升级所需伞下单数', '服务商伞下业绩达到N单后升级为分公司默认1000', 70, 1),
DELIMITER $$
CREATE PROCEDURE `hjf_migrate_member_to_agent_level`()
BEGIN
-- 直推奖励积分创客直推可得N积分
(0, 'hjf_reward_direct_1', 'text', 'input', 0,
'', 0, '', 100, 0,
'500', '创客直推奖励积分', '创客等级直推一单报单商品可获得的冻结积分默认500', 80, 1),
IF EXISTS (
SELECT 1 FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'eb_user' AND COLUMN_NAME = 'member_level'
) THEN
-- 将 member_level 数值映射到 agent_level (FK → eb_agent_level.id)
UPDATE eb_user u
INNER JOIN eb_agent_level al ON al.grade = u.member_level AND al.is_del = 0
SET u.agent_level = al.id
WHERE u.member_level > 0 AND (u.agent_level = 0 OR u.agent_level IS NULL);
END IF;
-- 直推奖励积分:云店
(0, 'hjf_reward_direct_2', 'text', 'input', 0,
'', 0, '', 100, 0,
'800', '云店直推奖励积分', '云店等级直推一单报单商品可获得的冻结积分默认800', 90, 1),
END$$
DELIMITER ;
-- 直推奖励积分:服务商
(0, 'hjf_reward_direct_3', 'text', 'input', 0,
'', 0, '', 100, 0,
'1000', '服务商直推奖励积分', '服务商等级直推一单报单商品可获得的冻结积分默认1000', 100, 1),
-- 直推奖励积分:分公司
(0, 'hjf_reward_direct_4', 'text', 'input', 0,
'', 0, '', 100, 0,
'1300', '分公司直推奖励积分', '分公司等级直推一单报单商品可获得的冻结积分默认1300', 110, 1),
-- 伞下奖励积分:创客(无伞下奖励)
(0, 'hjf_reward_umbrella_1', 'text', 'input', 0,
'', 0, '', 100, 0,
'0', '创客伞下奖励积分', '创客等级伞下奖励积分级差默认0无伞下奖励', 120, 1),
-- 伞下奖励积分:云店
(0, 'hjf_reward_umbrella_2', 'text', 'input', 0,
'', 0, '', 100, 0,
'300', '云店伞下奖励积分', '云店等级伞下奖励积分级差默认300', 130, 1),
-- 伞下奖励积分:服务商
(0, 'hjf_reward_umbrella_3', 'text', 'input', 0,
'', 0, '', 100, 0,
'200', '服务商伞下奖励积分', '服务商等级伞下奖励积分级差默认200', 140, 1),
-- 伞下奖励积分:分公司
(0, 'hjf_reward_umbrella_4', 'text', 'input', 0,
'', 0, '', 100, 0,
'300', '分公司伞下奖励积分', '分公司等级伞下奖励积分级差默认300', 150, 1);
CALL `hjf_migrate_member_to_agent_level`();
DROP PROCEDURE IF EXISTS `hjf_migrate_member_to_agent_level`;
-- ============================================================
-- 迁移完成校验(可手动执行检查)
-- ============================================================
-- SELECT TABLE_NAME FROM information_schema.TABLES
-- WHERE TABLE_SCHEMA = DATABASE()
-- AND TABLE_NAME IN ('eb_queue_pool', 'eb_points_release_log');
-- SELECT id, name, grade, direct_reward_points, umbrella_reward_points
-- FROM eb_agent_level WHERE is_del = 0 ORDER BY grade;
-- SELECT alt.id, al.name AS level_name, alt.type, alt.number, alt.name AS task_name
-- FROM eb_agent_level_task alt
-- JOIN eb_agent_level al ON al.id = alt.level_id
-- WHERE alt.is_del = 0 AND al.is_del = 0
-- ORDER BY al.grade, alt.type;
-- SELECT COLUMN_NAME FROM information_schema.COLUMNS
-- WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'eb_user'
-- AND COLUMN_NAME IN ('member_level','no_assess','frozen_points','available_points');
-- SELECT COLUMN_NAME FROM information_schema.COLUMNS
-- WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'eb_store_product'
-- AND COLUMN_NAME IN ('is_queue_goods','allow_pay_types');
-- AND COLUMN_NAME IN ('no_assess','frozen_points','available_points');
-- SELECT menu_name, value FROM eb_system_config
-- WHERE menu_name LIKE 'hjf_%' ORDER BY sort;

View File

@@ -4,28 +4,92 @@
-- ============================================================
-- Step 1: eb_store_product 新增报单商品标记字段
-- 注MySQL 5.7 不支持 ADD COLUMN IF NOT EXISTS重复执行会报错已存在时跳过即可
ALTER TABLE `eb_store_product`
ADD COLUMN IF NOT EXISTS `is_queue_goods` tinyint(1) NOT NULL DEFAULT 0 COMMENT '报单商品:1=是,0=否' AFTER `is_brokerage`;
ADD COLUMN `is_queue_goods` tinyint(1) NOT NULL DEFAULT 0 COMMENT '报单商品:1=是,0=否' AFTER `is_brokerage`;
-- Step 1b: eb_store_order 新增报单商品标记(冗余存储,加速佣金周期计数)
ALTER TABLE `eb_store_order`
ADD COLUMN IF NOT EXISTS `is_queue_goods` tinyint(1) NOT NULL DEFAULT 0 COMMENT '报单商品订单:1=是,0=否' AFTER `spread_two_uid`;
ADD COLUMN `is_queue_goods` tinyint(1) NOT NULL DEFAULT 0 COMMENT '报单商品订单:1=是,0=否' AFTER `spread_two_uid`;
-- Step 2: eb_user 新增积分字段与不考核字段
ALTER TABLE `eb_user`
ADD COLUMN IF NOT EXISTS `frozen_points` int(11) NOT NULL DEFAULT 0 COMMENT '待释放积分' AFTER `integral`,
ADD COLUMN IF NOT EXISTS `available_points` int(11) NOT NULL DEFAULT 0 COMMENT '已释放积分' AFTER `frozen_points`,
ADD COLUMN IF NOT EXISTS `no_assess` tinyint(1) NOT NULL DEFAULT 0 COMMENT '不考核:1=是,0=否' AFTER `available_points`;
ADD COLUMN `frozen_points` int(11) NOT NULL DEFAULT 0 COMMENT '待释放积分' AFTER `integral`,
ADD COLUMN `available_points` int(11) NOT NULL DEFAULT 0 COMMENT '已释放积分' AFTER `frozen_points`,
ADD COLUMN `no_assess` tinyint(1) NOT NULL DEFAULT 0 COMMENT '不考核:1=是,0=否' AFTER `available_points`;
-- Step 2b: eb_agent_level 新增直推/伞下奖励积分字段 + 返佣比例展示字段
ALTER TABLE `eb_agent_level`
ADD COLUMN `direct_reward_points` int(11) NOT NULL DEFAULT 0 COMMENT '直推奖励积分' AFTER `two_brokerage`,
ADD COLUMN `umbrella_reward_points` int(11) NOT NULL DEFAULT 0 COMMENT '伞下奖励积分' AFTER `direct_reward_points`,
ADD COLUMN `one_brokerage_ratio` decimal(5,2) NOT NULL DEFAULT 0 COMMENT '一级返佣比例(上浮后)' AFTER `umbrella_reward_points`,
ADD COLUMN `two_brokerage_ratio` decimal(5,2) NOT NULL DEFAULT 0 COMMENT '二级返佣比例(上浮后)' AFTER `one_brokerage_ratio`;
-- Step 3: eb_system_timer 新增每日积分释放定时任务
INSERT IGNORE INTO `eb_system_timer` (`title`, `mark`, `type`, `cycle`, `is_open`, `status`, `add_time`)
VALUES ('fsgx每日释放待释放积分', 'fsgx_release_frozen_points', 1, '0 2 * * *', 1, 1, UNIX_TIMESTAMP());
-- type=4 表示"每天"cycle 格式为"小时/分钟"(如 2/0 = 凌晨2点整
-- 表中无 status 字段is_open 控制是否启用mark 无唯一索引,先删再插保证幂等
DELETE FROM `eb_system_timer` WHERE `mark` = 'fsgx_release_frozen_points';
-- Step 4: eb_system_config 新增返佣周期配置键
-- 使用 INSERT IGNORE 避免重复插入
INSERT IGNORE INTO `eb_system_config` (`menu_name`, `info`, `group_id`, `config_tab_id`, `type`, `input_type`, `config_value`, `desc`, `sort`, `status`)
INSERT INTO `eb_system_timer` (`name`, `title`, `mark`, `type`, `cycle`, `is_open`, `add_time`)
VALUES ('fsgx每日积分释放', 'fsgx每日释放待释放积分0.4‰转入可用积分)', 'fsgx_release_frozen_points', 4, '2/0', 1, UNIX_TIMESTAMP());
-- Step 4: eb_system_config 新增返佣周期配置键和提现手续费
-- 实际表字段value非 config_value、config_tab_id非 group_iddesc 为保留字需加反引号
-- 使用 DELETE+INSERT 保证幂等
DELETE FROM `eb_system_config` WHERE `menu_name` IN ('brokerage_cycle_count','brokerage_cycle_rates','brokerage_scope','brokerage_timing','extract_fee');
-- value 字段存储 JSON 编码后的值(与 save_basics 的 json_encode 行为一致)
-- 字符串类型:用双引号括起来,如 "queue_only";数字直接写;数组写 JSON 数组
INSERT INTO `eb_system_config` (`menu_name`, `info`, `config_tab_id`, `type`, `input_type`, `value`, `desc`, `sort`, `status`)
VALUES
('brokerage_cycle_count', '佣金周期人数', 32, 0, 'text', 'input_number', '3', '推荐返现一个周期所需人数默认3人', 10, 1),
('brokerage_cycle_rates', '佣金分档比例(JSON数组)', 32, 0, 'text', 'input', '[20,30,50]', '各档佣金比例JSON数组如[20,30,50]', 9, 1),
('brokerage_scope', '返佣范围', 32, 0, 'text', 'radio', 'queue_only', '返佣范围all=所有商品 queue_only=仅报单商品', 8, 1),
('brokerage_timing', '佣金发放时机', 32, 0, 'text', 'radio', 'on_pay', '发放时机on_pay=支付即发 on_confirm=确认收货后', 7, 1);
('brokerage_cycle_count', '佣金周期人数', 0, 'text', 'input', '3', '推荐N人为一个周期,循环计算各档佣金比例', 10, 1),
('brokerage_cycle_rates', '佣金分档比例(JSON)', 0, 'text', 'input', '[20,30,50]', '各档佣金比例JSON数组如[20,30,50]表示20%/30%/50%', 9, 1),
('brokerage_scope', '返佣范围', 0, 'text', 'input', '"queue_only"', '返佣范围all=所有商品 queue_only=仅报单商品', 8, 1),
('brokerage_timing', '佣金发放时机', 0, 'text', 'input', '"on_pay"', '发放时机on_pay=支付即发 on_confirm=确认收货后', 7, 1),
('extract_fee', '提现手续费率(%)', 0, 'text', 'input', '7', '提现时扣除的手续费百分比默认7%', 6, 1);
-- Step 5: 新建公排池表和积分释放日志表
CREATE TABLE IF NOT EXISTS `eb_queue_pool` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`uid` int(11) NOT NULL DEFAULT 0,
`order_id` varchar(50) NOT NULL DEFAULT '',
`amount` decimal(10,2) NOT NULL DEFAULT 3600.00,
`queue_no` int(11) NOT NULL DEFAULT 0,
`status` tinyint(1) NOT NULL DEFAULT 0,
`refund_time` int(11) NOT NULL DEFAULT 0,
`trigger_batch` int(11) NOT NULL DEFAULT 0,
`add_time` int(11) NOT NULL DEFAULT 0,
PRIMARY KEY (`id`),
UNIQUE KEY `uniq_queue_no` (`queue_no`),
INDEX `idx_uid` (`uid`),
INDEX `idx_status_add_time` (`status`, `add_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='公排池';
CREATE TABLE IF NOT EXISTS `eb_points_release_log` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`uid` int(11) NOT NULL DEFAULT 0,
`points` int(11) NOT NULL DEFAULT 0,
`pm` tinyint(1) NOT NULL DEFAULT 1,
`type` varchar(50) NOT NULL DEFAULT '',
`title` varchar(255) NOT NULL DEFAULT '',
`mark` varchar(500) NOT NULL DEFAULT '',
`status` varchar(30) NOT NULL DEFAULT 'frozen',
`order_id` varchar(50) NOT NULL DEFAULT '',
`release_date` date DEFAULT NULL,
`add_time` int(11) NOT NULL DEFAULT 0,
PRIMARY KEY (`id`),
INDEX `idx_uid_type` (`uid`, `type`),
INDEX `idx_uid_add_time` (`uid`, `add_time`),
INDEX `idx_release_date` (`release_date`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='积分释放明细日志';
-- Step 6: 更新分销等级升级任务配置(对齐验收清单)
DELETE FROM `eb_agent_level_task`;
INSERT INTO `eb_agent_level_task` (`level_id`, `name`, `type`, `number`, `is_must`, `sort`, `status`, `is_del`, `add_time`)
VALUES
(1, '直推人数>=1人', 8, 1, 1, 1, 1, 0, UNIX_TIMESTAMP()),
(2, '直推人数>=3人', 8, 3, 1, 1, 1, 0, UNIX_TIMESTAMP()),
(3, '直推人数>=10人', 8, 10, 1, 1, 1, 0, UNIX_TIMESTAMP()),
(3, '伞下队列订单>=30', 7, 30, 1, 2, 1, 0, UNIX_TIMESTAMP()),
(4, '直推人数>=30人', 8, 30, 1, 1, 1, 0, UNIX_TIMESTAMP()),
(4, '伞下队列订单>=100',7, 100, 1, 2, 1, 0, UNIX_TIMESTAMP());

View File

@@ -1,6 +1,6 @@
# 站点1默认 80 端口
# 站点1本地开发 8088 端口macOS 普通用户无法监听 80
server {
listen 80;
listen 8088;
server_name 127.0.0.1;
root /Users/apple/scott2026/huangjingfen/pro_v3.5.1/public;

View File

@@ -2309,6 +2309,8 @@ Route::group('adminapi', function () {
Route::post('timer/save', 'v1.system.SystemTimer/save')->option(['real_name' => '保存定时任务']);
//更新定时任务
Route::post('timer/update/:id', 'v1.system.SystemTimer/update')->option(['real_name' => '更新定时任务']);
//手动立即触发定时任务
Route::get('timer/run_now/:id', 'v1.system.SystemTimer/run_now')->option(['real_name' => '手动触发定时任务']);
//系统表单列表
Route::get('form/index', 'v1.system.form.SystemForm/index')->option(['real_name' => '系统表单列表']);

View File

@@ -3,7 +3,7 @@ NODE_ENV=production
VUE_APP_ENV='production'
# 页面 title
VUE_APP_TITLE=CRMEB
VUE_APP_TITLE=fsgx-shop
# socket 系统连接地址 (ws)或(wss)://www.crmeb.com(换成你的域名)/ws 非独立部署默认为空
VUE_APP_WS_ADMIN_URL='ws://fsgx.uj345.com/ws'
# 接口请求地址 (http)或 (https)://www.crmeb.com(换成你的域名)/adminapi 非独立部署默认为空

View File

@@ -679,6 +679,17 @@ export function timerTask() {
});
}
/**
* 手动立即触发定时任务
* @param {number} id
* @returns
*/
export function runTimerNow(id) {
return request({
url: `system/timer/run_now/${id}`,
});
}
export function systemFormData(id, params) {
return request({
url: `/system/form/data/${id}`,

View File

@@ -63,6 +63,11 @@
baseURL: Setting.apiBaseURL.replace(/adminapi/, '')
};
},
mounted() {
if (Array.isArray(this.valueModel) || (typeof this.valueModel === 'object' && this.valueModel !== null)) {
this.valueModel = JSON.stringify(this.valueModel);
}
},
methods: {
handleCopySuccess(){
this.$Message.success('复制成功');

View File

@@ -27,7 +27,10 @@
name: "radioBuild",
mixins:[build, components],
mounted() {
this.valueModel = parseFloat(this.valueModel);
const parsed = parseFloat(this.valueModel);
if (!isNaN(parsed) && String(parsed) === String(this.valueModel)) {
this.valueModel = parsed;
}
},
components: {
useComponent:() => import('./useComponent'),

View File

@@ -19,7 +19,7 @@
</Form>
<Drawer v-model="guideShow" :title="`${title}引导`" width="800">
<component :is="types"></component>
<component :is="types" v-if="guideComponents.includes(types)"></component>
</Drawer>
</div>
</template>
@@ -91,8 +91,13 @@
loading: false,
errorsValidate: [],
title: this.$parent.title,
types: this.$parent.typeMole || this.$parent.type,
types: (() => {
const t = this.$parent.typeMole || this.$parent.type;
const allowed = ['app', 'routine', 'wechat', 'work'];
return allowed.includes(t) ? t : (this.$parent.type || '');
})(),
guideShow: false,
guideComponents: ['app', 'routine', 'wechat', 'work'],
};
},
watch: {

View File

@@ -6,18 +6,6 @@ const chalk = require("chalk");
console.log(
chalk
.hex("#DEADED")
.underline("😄 Hello ~ 欢迎使用CRMEB Pro版我们将竭诚为您服务")
);
console.log(
chalk.yellow("[提示] 点击这里可以我们的更多产品~ ") +
chalk.blue.underline("https://www.crmeb.com")
);
console.log(
chalk.yellow("[提示] 点击这里可以查看开发文档喔~ ") +
chalk.blue.underline("https://doc.crmeb.com")
);
console.log(
chalk.yellow("[提示] 点击这里可以进入我们的论坛社区~ ") +
chalk.blue.underline("https://www.crmeb.com/ask")
.underline("😄 Hello ~ 欢迎使用Pro版我们将竭诚为您服务")
);
console.log(chalk.blue("info - [你知道吗?] 按 Ctrl+C 可以停止服务呢~"));

View File

@@ -101,6 +101,7 @@ export const defaultObj = {
is_sub: 0, //返佣方式
is_vip: 0,
level_type: 1, //默认比例 自定义
is_queue_goods: 0, //是否报单商品1=是0=否
};
export const GoodsTableHead = [

View File

@@ -239,6 +239,16 @@ export default {
minWidth: 130,
title: "二级返佣比例(上浮后)(%)",
},
{
key: "direct_reward_points",
minWidth: 120,
title: "直推奖励积分",
},
{
key: "umbrella_reward_points",
minWidth: 120,
title: "伞下奖励积分",
},
{
slot: "status",
minWidth: 80,

View File

@@ -29,6 +29,10 @@
>编辑</router-link
>
<Divider type="vertical" />
<a @click="handleRunNow(row)" :class="{ 'running': row._running }">
{{ row._running ? '执行中…' : '手动触发' }}
</a>
<Divider type="vertical" />
<a @click="handleDelete(row, '删除定时任务', index)">删除</a>
</template>
</Table>
@@ -46,7 +50,7 @@
</template>
<script>
import { timerIndex, showTimer } from '@/api/system';
import { timerIndex, showTimer, runTimerNow } from '@/api/system';
export default {
name: 'system_crontab',
@@ -79,7 +83,7 @@ export default {
slot: 'action',
align: 'center',
fixed: 'right',
minWidth: 100,
minWidth: 200,
},
],
tableData: [],
@@ -133,6 +137,21 @@ export default {
this.$Message.error(res.msg);
});
},
// 手动立即触发
handleRunNow(row) {
this.$set(row, '_running', true);
runTimerNow(row.id)
.then((res) => {
this.$Message.success(res.msg || '触发成功,任务已执行');
this.getList();
})
.catch((res) => {
this.$Message.error(res.msg || '触发失败');
})
.finally(() => {
this.$set(row, '_running', false);
});
},
// 是否开启
handleChange({ id, is_open }) {
showTimer(id, is_open)
@@ -152,4 +171,9 @@ export default {
};
</script>
<style lang="stylus" scoped></style>
<style lang="stylus" scoped>
.running
color #999
cursor not-allowed
pointer-events none
</style>

View File

@@ -54,15 +54,15 @@
<div class="iconfont iconxiayi"></div>
</div>
</FormItem>
<FormItem label="会员等级:" label-for="hjf_member_level">
<FormItem label="分销等级:" label-for="hjf_member_level">
<Select
v-model="userFrom.hjf_member_level"
placeholder="请选择"
placeholder="请选择分销等级"
element-id="hjf_member_level"
clearable
class="input-add"
>
<Option :value="0">普通会员</Option>
<Option :value="0">普通无分销等级</Option>
<Option :value="1">创客</Option>
<Option :value="2">云店</Option>
<Option :value="3">服务商</Option>
@@ -420,14 +420,15 @@
</div>
</template>
</vxe-column>
<vxe-column field="member_level" title="HJF等级" min-width="110">
<vxe-column field="member_level_name" title="分销等级" min-width="160">
<template v-slot="{ row }">
<HjfMemberBadge
v-if="row.member_level != null"
v-if="row.member_level != null && row.member_level > 0"
:level="Number(row.member_level)"
:level-name="row.member_level_name || ''"
size="small"
/>
<span v-else class="level-none">普通会员</span>
<span v-else class="level-none">普通无等级</span>
</template>
</vxe-column>
<vxe-column field="direct_count" title="直推人数" min-width="90">
@@ -452,22 +453,22 @@
title="推荐人"
min-width="100"
></vxe-column>
<vxe-column field="available_points" title="可用积分" min-width="100">
<template v-slot="{ row }">
<span>{{ row.available_points != null ? row.available_points : 0 }}</span>
</template>
</vxe-column>
<vxe-column field="frozen_points" title="待释放(冻结)积分" min-width="150">
<template v-slot="{ row }">
<span>{{ row.frozen_points != null ? row.frozen_points : 0 }}</span>
</template>
</vxe-column>
<vxe-column field="now_money" title="余额" min-width="100"></vxe-column>
<vxe-column field="frozen_points" title="待释放积分" min-width="100">
<template v-slot="{ row }">
<span>{{ row.frozen_points != null ? row.frozen_points : '-' }}</span>
</template>
</vxe-column>
<vxe-column field="available_points" title="已释放积分" min-width="100">
<template v-slot="{ row }">
<span>{{ row.available_points != null ? row.available_points : '-' }}</span>
</template>
</vxe-column>
<vxe-column
field="action"
title="操作"
align="center"
width="240"
width="180"
fixed="right"
>
<template v-slot="{ row }">
@@ -475,8 +476,6 @@
<Divider type="vertical" />
<a @click="changeMenu(row, '10')">编辑</a>
<Divider type="vertical" />
<a @click="openLevelModal(row)">调整等级</a>
<Divider type="vertical" />
<a @click="handleNoAssess(row)" :style="row.no_assess ? 'color:#19be6b' : 'color:#ed4014'">
{{ row.no_assess ? '取消不考核' : '不考核' }}
</a>
@@ -791,7 +790,7 @@ import {
exportUserData,
} from "@/api/user";
import { agentSpreadApi } from "@/api/agent";
import { memberList, memberSetLevel, memberSetNoAssess } from "@/api/hjfMember";
import { memberList, memberSetLevel, memberSetNoAssess } from "@/api/hjfMember"; // memberSetNoAssess 仍保留供 handleNoAssess 使用
import editFrom from "../../../components/from/from";
import sendFrom from "@/components/sendCoupons/index";
import userDetails from "./handle/userDetails";
@@ -881,7 +880,7 @@ export default {
group_id: "",
field_key: "",
is_channel: "",
/** 会员等级筛选(HJF 扩展字段0 普通 1 创客 2 云店 3 服务商 4 分公司,空串表示全部 */
/** 分销等级筛选(对应 eb_agent_level.grade / eb_user.agent_level,空串表示全部 */
hjf_member_level: "",
},
field_key: "",
@@ -1455,12 +1454,13 @@ export default {
this.userFrom.group_id === "all" ? "" : this.userFrom.group_id;
userList(this.userFrom)
.then(async (res) => {
let data = res.data;
data.list.forEach((item) => {
const data = res.data || {};
const rows = Array.isArray(data.list) ? data.list : [];
rows.forEach((item) => {
item.checkBox = false;
});
this.userLists = data.list;
this.total = data.count;
this.userLists = rows;
this.total = data.count != null ? data.count : 0;
this.loading = false;
this.$nextTick(function () {
if (this.isAll == 1) {

View File

@@ -59,5 +59,17 @@ export default {
},
component: () => import('@/pages/finance/commission/index')
},
{
path: 'extract_setting',
name: `${pre}extractSetting`,
meta: {
auth: ['finance-extract-setting'],
title: '提现设置'
},
component: () => import('@/components/fromSubmit/commonForm.vue'),
props: {
typeMole: 'advance'
}
},
]
};

View File

@@ -20,8 +20,9 @@
<button form-type="submit" class="confirmBnt bg-color">确认修改</button>
</form>
</view>
<Verify @success="success" :captchaType="captchaType" :imgSize="{ width: '330px', height: '155px' }"
ref="verify"></Verify>
<!-- 安全验证组件保留备用当前修改密码流程已跳过 -->
<!-- <Verify @success="success" :captchaType="captchaType" :imgSize="{ width: '330px', height: '155px' }"
ref="verify"></Verify> -->
<!-- #ifdef MP -->
<authorize v-if="isShowAuth" @authColse="authColse" @onLoadFun="onLoadFun"></authorize>
<!-- #endif -->
@@ -120,53 +121,66 @@
that.phone = phone;
});
},
success(data) {
console.log(data,'data');
this.$refs.verify.hide()
getCodeApi()
.then(res => {
this.keyCode = res.data.key;
this.getCode(data);
})
.catch(res => {
this.$util.Tips({
title: res
});
success(data) {
this.$refs.verify.hide()
getCodeApi()
.then(res => {
this.keyCode = res.data.key;
this.getCode(data);
})
.catch(res => {
this.$util.Tips({
title: res
});
},
/**
* 发送验证码
*
*/
code(data) {
let that = this;
if (!that.userInfo.phone) return that.$util.Tips({
title: '手机号码不存在,无法发送验证码!'
});
this.$refs.verify.show()
},
async getCode(data){
let that = this;
await registerVerify({
phone: that.userInfo.phone,
type: 'reset',
key: that.key,
captchaType: CAPTCHA_TYPE,
captchaVerification: data.captchaVerification,
})
.then(res => {
that.$util.Tips({
title: res.msg
});
that.sendCode();
})
.catch(res => {
this.$refs.verify.refresh()
that.$util.Tips({
title: res
});
},
/**
* 发送验证码(直接发送,不弹安全验证)
*/
code() {
let that = this;
if (!that.userInfo.phone) return that.$util.Tips({
title: '手机号码不存在,无法发送验证码!'
});
registerVerify({
phone: that.userInfo.phone,
type: 'reset',
key: that.key,
})
.then(res => {
that.$util.Tips({
title: res.msg
});
},
that.sendCode();
})
.catch(res => {
that.$util.Tips({
title: res
});
});
},
async getCode(data){
let that = this;
await registerVerify({
phone: that.userInfo.phone,
type: 'reset',
key: that.key,
captchaType: CAPTCHA_TYPE,
captchaVerification: data.captchaVerification,
})
.then(res => {
that.$util.Tips({
title: res.msg
});
that.sendCode();
})
.catch(res => {
this.$refs.verify.refresh()
that.$util.Tips({
title: res
});
});
},
/**
* H5登录 修改密码
*