diff --git a/docs/fsgx-phase7-config-checklist.md b/docs/fsgx-phase7-config-checklist.md index a7e4a97b..94f1f84a 100644 --- a/docs/fsgx-phase7-config-checklist.md +++ b/docs/fsgx-phase7-config-checklist.md @@ -62,7 +62,7 @@ mysql -u root -p fsgx-shop < pro_v3.5.1/help/migrations/fsgx_v1.sql ### 7.3.3 提现设置 -路径:**营销 → 分销 → 提现设置** +路径:**财务 → 分销财务 → 提现设置(已存在)** - 提现手续费率:`7%` - 最低提现金额:`100 元` diff --git a/docs/issues-0323-1.md b/docs/issues-0323-1.md index 993c8f79..f620e965 100644 --- a/docs/issues-0323-1.md +++ b/docs/issues-0323-1.md @@ -1,16 +1,42 @@ # 管理后台 -## 编辑商品页面,路径:admin/product/add_product?id=10&from_page=1 -1. 是否报单商品:1=是,修改保存后没有更新数据库中的值 +## 编辑商品页面,路径:admin/product/add_product?id=18&from_page=1 -## 返佣设置:,路径:/admin/setting/system_config_rake_back -1. 一级返佣比例每3单比例不一样 -2. 新增返佣范围:所有商品/报单商品 +1. **已修复**是否报单商品:1=是,修改保存后没有更新数据库中的值 +## 返佣设置页面,路径:/admin/setting/system_config_rake_back + +1. **已修复**”返佣范围、佣金发放时机“修改后保存不落库(数据库没有修改) +2. **已修复**推荐佣金(fsgx)tab页中”返佣范围、佣金发放时机“没有显示选中项 + +## 用户列表页面,路径: /admin/user/list + +1. **已修复**操作“不考核”,提交后报错 +2. **已修复**HJF等级改为分销等级,但是没有关联用户的分销等级,没显示数据(queue分支下该功能是ok的) + +## 分销等级页面,路径:admin/setting/membership_level/index + +1. **已修复**列表中显示“直推奖励积分、伞下奖励积分”和升级等级任务(queue分支下该功能是ok的,可以直接合并过来) + +## 定时任务页面,路径:/admin/system/crontab + +1. **已修复**增加“手动触发”功能按钮,可以手动触发即执行任务立, + +--- + + +# uniapp移动端 + +## 修改密码页面 +1. 点击获取验证,去除安全验证,直接发送验证码 + +--- # 测试 ## 测试数据: + 1. 测试账号:UID:1, 手机号:18621813282; UID:2, 手机号:15821676725 ;UID:3, 手机号:17887996868 ; - UID:4, 手机号:15324401259。 -2. 推荐关系: uid=1推荐uid=2推荐uid=3推荐uid=4 \ No newline at end of file + UID:4, 手机号:15324401259。 +2. 推荐关系: uid=1推荐uid=2推荐uid=3推荐uid=4 + diff --git a/pro_v3.5.1/app/command/HjfReleasePoints.php b/pro_v3.5.1/app/command/HjfReleasePoints.php new file mode 100644 index 00000000..c2f6823b --- /dev/null +++ b/pro_v3.5.1/app/command/HjfReleasePoints.php @@ -0,0 +1,49 @@ +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; + } +} diff --git a/pro_v3.5.1/app/controller/admin/Login.php b/pro_v3.5.1/app/controller/admin/Login.php index 8b9cafb4..3beb27c5 100644 --- a/pro_v3.5.1/app/controller/admin/Login.php +++ b/pro_v3.5.1/app/controller/admin/Login.php @@ -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]); diff --git a/pro_v3.5.1/app/controller/admin/v1/hjf/MemberController.php b/pro_v3.5.1/app/controller/admin/v1/hjf/MemberController.php new file mode 100644 index 00000000..ea245b17 --- /dev/null +++ b/pro_v3.5.1/app/controller/admin/v1/hjf/MemberController.php @@ -0,0 +1,161 @@ +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('保存成功'); + } +} diff --git a/pro_v3.5.1/app/controller/admin/v1/hjf/PointsController.php b/pro_v3.5.1/app/controller/admin/v1/hjf/PointsController.php new file mode 100644 index 00000000..fe8d7e15 --- /dev/null +++ b/pro_v3.5.1/app/controller/admin/v1/hjf/PointsController.php @@ -0,0 +1,42 @@ +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)); + } +} diff --git a/pro_v3.5.1/app/controller/admin/v1/hjf/QueueController.php b/pro_v3.5.1/app/controller/admin/v1/hjf/QueueController.php new file mode 100644 index 00000000..d3d5df65 --- /dev/null +++ b/pro_v3.5.1/app/controller/admin/v1/hjf/QueueController.php @@ -0,0 +1,105 @@ +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)); + } +} diff --git a/pro_v3.5.1/app/controller/admin/v1/system/SystemTimer.php b/pro_v3.5.1/app/controller/admin/v1/system/SystemTimer.php index 83794f8d..7957a3c2 100644 --- a/pro_v3.5.1/app/controller/admin/v1/system/SystemTimer.php +++ b/pro_v3.5.1/app/controller/admin/v1/system/SystemTimer.php @@ -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 diff --git a/pro_v3.5.1/app/controller/admin/v1/system/config/SystemConfig.php b/pro_v3.5.1/app/controller/admin/v1/system/config/SystemConfig.php index 8c5bc172..7b85f84d 100644 --- a/pro_v3.5.1/app/controller/admin/v1/system/config/SystemConfig.php +++ b/pro_v3.5.1/app/controller/admin/v1/system/config/SystemConfig.php @@ -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; diff --git a/pro_v3.5.1/app/controller/admin/v1/user/User.php b/pro_v3.5.1/app/controller/admin/v1/user/User.php index 89d42bd1..efe1c6ab 100644 --- a/pro_v3.5.1/app/controller/admin/v1/user/User.php +++ b/pro_v3.5.1/app/controller/admin/v1/user/User.php @@ -85,6 +85,8 @@ class User extends AuthController ['isMember', ''], ['label_ids', ''], ['is_channel', ''], + /** HJF:按分销等级 grade(0–4)筛选,对应 eb_user.agent_level */ + ['hjf_member_level', ''], ]); if ($where['label_ids']) { $where['label_id'] = stringToIntArray($where['label_ids']); diff --git a/pro_v3.5.1/app/controller/api/v1/hjf/AssetsController.php b/pro_v3.5.1/app/controller/api/v1/hjf/AssetsController.php new file mode 100644 index 00000000..6f874094 --- /dev/null +++ b/pro_v3.5.1/app/controller/api/v1/hjf/AssetsController.php @@ -0,0 +1,64 @@ +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) + ); + } +} diff --git a/pro_v3.5.1/app/controller/api/v1/hjf/HjfAssets.php b/pro_v3.5.1/app/controller/api/v1/hjf/HjfAssets.php index ec91d498..e2b36fd5 100644 --- a/pro_v3.5.1/app/controller/api/v1/hjf/HjfAssets.php +++ b/pro_v3.5.1/app/controller/api/v1/hjf/HjfAssets.php @@ -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, diff --git a/pro_v3.5.1/app/controller/api/v1/hjf/HjfBrokerage.php b/pro_v3.5.1/app/controller/api/v1/hjf/HjfBrokerage.php index 7e049dfe..a4143be5 100644 --- a/pro_v3.5.1/app/controller/api/v1/hjf/HjfBrokerage.php +++ b/pro_v3.5.1/app/controller/api/v1/hjf/HjfBrokerage.php @@ -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, diff --git a/pro_v3.5.1/app/controller/api/v1/hjf/MemberController.php b/pro_v3.5.1/app/controller/api/v1/hjf/MemberController.php new file mode 100644 index 00000000..380b75e4 --- /dev/null +++ b/pro_v3.5.1/app/controller/api/v1/hjf/MemberController.php @@ -0,0 +1,178 @@ +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')); + } +} diff --git a/pro_v3.5.1/app/controller/api/v1/hjf/PointsController.php b/pro_v3.5.1/app/controller/api/v1/hjf/PointsController.php new file mode 100644 index 00000000..57052a2b --- /dev/null +++ b/pro_v3.5.1/app/controller/api/v1/hjf/PointsController.php @@ -0,0 +1,49 @@ +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) + ); + } +} diff --git a/pro_v3.5.1/app/controller/api/v1/hjf/QueueController.php b/pro_v3.5.1/app/controller/api/v1/hjf/QueueController.php new file mode 100644 index 00000000..e352a39f --- /dev/null +++ b/pro_v3.5.1/app/controller/api/v1/hjf/QueueController.php @@ -0,0 +1,60 @@ +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]; + } +} diff --git a/pro_v3.5.1/app/dao/hjf/PointsReleaseLogDao.php b/pro_v3.5.1/app/dao/hjf/PointsReleaseLogDao.php new file mode 100644 index 00000000..7650faae --- /dev/null +++ b/pro_v3.5.1/app/dao/hjf/PointsReleaseLogDao.php @@ -0,0 +1,84 @@ +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, + ], + ]; + } +} diff --git a/pro_v3.5.1/app/dao/hjf/QueuePoolDao.php b/pro_v3.5.1/app/dao/hjf/QueuePoolDao.php new file mode 100644 index 00000000..90d81872 --- /dev/null +++ b/pro_v3.5.1/app/dao/hjf/QueuePoolDao.php @@ -0,0 +1,140 @@ +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, '.', ''), + ]; + } +} diff --git a/pro_v3.5.1/app/dao/user/UserWechatUserDao.php b/pro_v3.5.1/app/dao/user/UserWechatUserDao.php index db64781e..267a2cf1 100644 --- a/pro_v3.5.1/app/dao/user/UserWechatUserDao.php +++ b/pro_v3.5.1/app/dao/user/UserWechatUserDao.php @@ -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']); diff --git a/pro_v3.5.1/app/jobs/hjf/HjfOrderPayJob.php b/pro_v3.5.1/app/jobs/hjf/HjfOrderPayJob.php new file mode 100644 index 00000000..1555eb3a --- /dev/null +++ b/pro_v3.5.1/app/jobs/hjf/HjfOrderPayJob.php @@ -0,0 +1,80 @@ +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; + } +} diff --git a/pro_v3.5.1/app/jobs/hjf/MemberLevelCheckJob.php b/pro_v3.5.1/app/jobs/hjf/MemberLevelCheckJob.php new file mode 100644 index 00000000..e60ffb87 --- /dev/null +++ b/pro_v3.5.1/app/jobs/hjf/MemberLevelCheckJob.php @@ -0,0 +1,39 @@ +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; + } +} diff --git a/pro_v3.5.1/app/jobs/hjf/PointsReleaseJob.php b/pro_v3.5.1/app/jobs/hjf/PointsReleaseJob.php new file mode 100644 index 00000000..42acf3f9 --- /dev/null +++ b/pro_v3.5.1/app/jobs/hjf/PointsReleaseJob.php @@ -0,0 +1,47 @@ +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; + } +} diff --git a/pro_v3.5.1/app/jobs/hjf/QueueRefundJob.php b/pro_v3.5.1/app/jobs/hjf/QueueRefundJob.php new file mode 100644 index 00000000..e5055ff1 --- /dev/null +++ b/pro_v3.5.1/app/jobs/hjf/QueueRefundJob.php @@ -0,0 +1,95 @@ +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; + } +} diff --git a/pro_v3.5.1/app/listener/order/Pay.php b/pro_v3.5.1/app/listener/order/Pay.php index 96b3eb85..9f658bb4 100644 --- a/pro_v3.5.1/app/listener/order/Pay.php +++ b/pro_v3.5.1/app/listener/order/Pay.php @@ -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'], '', [], []]); diff --git a/pro_v3.5.1/app/listener/system/timer/SystemTimer.php b/pro_v3.5.1/app/listener/system/timer/SystemTimer.php index e7b0b3a5..4d42e7f6 100644 --- a/pro_v3.5.1/app/listener/system/timer/SystemTimer.php +++ b/pro_v3.5.1/app/listener/system/timer/SystemTimer.php @@ -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 */ diff --git a/pro_v3.5.1/app/model/hjf/PointsReleaseLog.php b/pro_v3.5.1/app/model/hjf/PointsReleaseLog.php new file mode 100644 index 00000000..d36af86a --- /dev/null +++ b/pro_v3.5.1/app/model/hjf/PointsReleaseLog.php @@ -0,0 +1,30 @@ +, byGradeAny: array, byGradeOfficial: array} + */ + public function loadHjfUserListLevelMaps(): array + { + $rows = $this->dao->getList(['is_del' => 0]); + return $this->buildHjfUserListLevelMaps($rows); + } + + /** + * 从等级行列表构建用户列表展示用索引 + * + * @param array $hjfLevelRows + * @return array{byId: array, byGradeAny: array, byGradeOfficial: array} + */ + public function buildHjfUserListLevelMaps(array $hjfLevelRows): array + { + $byId = []; + $byGradeAny = []; + $byGradeOfficial = []; + $official = self::HJF_OFFICIAL_LEVEL_NAMES; + foreach ($hjfLevelRows as $hjfRow) { + $lid = (int)($hjfRow['id'] ?? 0); + if ($lid > 0) { + $byId[$lid] = $hjfRow; + } + $g = (int)($hjfRow['grade'] ?? 0); + if ($g > 0 && !isset($byGradeAny[$g])) { + $byGradeAny[$g] = $hjfRow; + } + $nm = (string)($hjfRow['name'] ?? ''); + if ($g > 0 && in_array($nm, $official, true) && !isset($byGradeOfficial[$g])) { + $byGradeOfficial[$g] = $hjfRow; + } + } + return ['byId' => $byId, 'byGradeAny' => $byGradeAny, 'byGradeOfficial' => $byGradeOfficial]; + } + + /** + * 用户列表场景:解析应展示的等级行 + * - agent_level 指向 CRMEB 默认行时,按 grade 改用 HJF 官方行 + * - 旧 id 已软删或误把 grade 写入 agent_level 时,按 byGradeAny 回退 + * + * @param int $agentLevelId 用户的 agent_level 字段值 + * @param array $maps loadHjfUserListLevelMaps() 返回的索引 + * @return array|null + */ + public function pickHjfLevelRowForUserListDisplay(int $agentLevelId, array $maps): ?array + { + if ($agentLevelId <= 0) { + return null; + } + $byId = $maps['byId'] ?? []; + $byGradeAny = $maps['byGradeAny'] ?? []; + $byGradeOfficial = $maps['byGradeOfficial'] ?? []; + $official = self::HJF_OFFICIAL_LEVEL_NAMES; + + $row = $byId[$agentLevelId] ?? null; + if ($row === null) { + return $byGradeAny[$agentLevelId] ?? null; + } + $nm = (string)($row['name'] ?? ''); + $g = (int)($row['grade'] ?? 0); + if ($g > 0 && !in_array($nm, $official, true) && isset($byGradeOfficial[$g])) { + return $byGradeOfficial[$g]; + } + return $row; + } + + /** + * 根据 grade 获取 agent_level ID(用于筛选条件转换) + * + * @param int $grade 等级数字 (1=创客, 2=云店, 3=服务商, 4=分公司) + * @return int agent_level ID,找不到返回 0 + */ + public function getLevelIdByGrade(int $grade): int + { + if ($grade <= 0) { + return 0; + } + return (int)$this->dao->value(['grade' => $grade, 'is_del' => 0, 'status' => 1], 'id') ?: 0; + } + + /** + * 根据 agent_level ID 获取等级 grade(HJF 会员等级数字 0-4) + */ + public function getGradeByLevelId(int $agentLevelId): int + { + if ($agentLevelId <= 0) { + return 0; + } + $levelInfo = $this->getLevelInfo($agentLevelId); + return (int)($levelInfo['grade'] ?? 0); + } + + /** + * 根据 agent_level ID 获取直推奖励积分 + */ + public function getDirectRewardPoints(int $agentLevelId): int + { + if ($agentLevelId <= 0) { + return 0; + } + $levelInfo = $this->getLevelInfo($agentLevelId); + return (int)($levelInfo['direct_reward_points'] ?? 0); + } + + /** + * 根据 agent_level ID 获取伞下奖励积分 + */ + public function getUmbrellaRewardPoints(int $agentLevelId): int + { + if ($agentLevelId <= 0) { + return 0; + } + $levelInfo = $this->getLevelInfo($agentLevelId); + return (int)($levelInfo['umbrella_reward_points'] ?? 0); + } + /** * 获取某一个等级信息 * @param int $id diff --git a/pro_v3.5.1/app/services/agent/AgentLevelTaskServices.php b/pro_v3.5.1/app/services/agent/AgentLevelTaskServices.php index c79ecbcc..f946c1c0 100644 --- a/pro_v3.5.1/app/services/agent/AgentLevelTaskServices.php +++ b/pro_v3.5.1/app/services/agent/AgentLevelTaskServices.php @@ -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 diff --git a/pro_v3.5.1/app/services/hjf/HjfAssetsServices.php b/pro_v3.5.1/app/services/hjf/HjfAssetsServices.php new file mode 100644 index 00000000..1b3dd4f2 --- /dev/null +++ b/pro_v3.5.1/app/services/hjf/HjfAssetsServices.php @@ -0,0 +1,89 @@ +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'); + } +} diff --git a/pro_v3.5.1/app/services/hjf/MemberLevelServices.php b/pro_v3.5.1/app/services/hjf/MemberLevelServices.php new file mode 100644 index 00000000..9fd556c4 --- /dev/null +++ b/pro_v3.5.1/app/services/hjf/MemberLevelServices.php @@ -0,0 +1,136 @@ +make(UserServices::class); + $userInfo = $userServices->getUserCacheInfo($uid); + if (!$userInfo) { + return; + } + + $spreadUid = $userServices->getSpreadUid($uid, $userInfo); + $twoSpreadUid = 0; + if ($spreadUid > 0 && $oneUserInfo = $userServices->getUserCacheInfo($spreadUid)) { + $twoSpreadUid = $userServices->getSpreadUid($spreadUid, $oneUserInfo, false); + } + $uids = array_unique([$uid, $spreadUid, $twoSpreadUid]); + + $this->agentLevelServices->checkUserLevelFinish($uid, $uids); + } catch (\Throwable $e) { + Log::error("[MemberLevel] checkUpgrade uid={$uid}: " . $e->getMessage()); + } + } + + /** + * 获取用户当前会员等级 grade(0=普通, 1=创客, 2=云店, 3=服务商, 4=分公司) + */ + public function getUserGrade(int $uid): int + { + $agentLevel = (int)Db::name('user')->where('uid', $uid)->value('agent_level'); + return $this->agentLevelServices->getGradeByLevelId($agentLevel); + } + + /** + * 获取用户当前等级名称 + */ + public function getUserLevelName(int $uid): string + { + $agentLevel = (int)Db::name('user')->where('uid', $uid)->value('agent_level'); + if ($agentLevel <= 0) { + return '普通会员'; + } + $maps = $this->agentLevelServices->loadHjfUserListLevelMaps(); + $info = $this->agentLevelServices->pickHjfLevelRowForUserListDisplay($agentLevel, $maps); + + return $info['name'] ?? '普通会员'; + } + + /** + * 获取直推用户的报单订单数 + */ + public function getDirectQueueOrderCount(int $uid): int + { + return $this->agentLevelTaskServices->getDirectQueueOrderCount($uid); + } + + /** + * 获取直推人数 + */ + public function getDirectSpreadCount(int $uid): int + { + /** @var UserServices $userServices */ + $userServices = app()->make(UserServices::class); + return (int)$userServices->count(['spread_uid' => $uid]); + } + + /** + * 获取伞下总报单订单数(含业绩分离逻辑) + */ + public function getUmbrellaQueueOrderCount(int $uid): int + { + return $this->agentLevelTaskServices->getUmbrellaQueueOrderCount($uid); + } + + /** + * 手动设置会员等级(管理后台使用) + * + * @param int $uid 用户 ID + * @param int $grade 目标等级 grade (0-4) + */ + public function setUserLevel(int $uid, int $grade): void + { + $agentLevelId = 0; + if ($grade > 0) { + $agentLevelId = $this->agentLevelServices->getLevelIdByGrade($grade); + if ($agentLevelId <= 0) { + throw new \think\exception\ValidateException("等级 grade={$grade} 在 eb_agent_level 中不存在"); + } + } + + /** @var UserServices $userServices */ + $userServices = app()->make(UserServices::class); + $userServices->update($uid, ['agent_level' => $agentLevelId]); + + Log::info("[MemberLevel] 手动设置 uid={$uid} agent_level={$agentLevelId} (grade={$grade})"); + } +} diff --git a/pro_v3.5.1/app/services/hjf/PointsReleaseServices.php b/pro_v3.5.1/app/services/hjf/PointsReleaseServices.php new file mode 100644 index 00000000..7241c510 --- /dev/null +++ b/pro_v3.5.1/app/services/hjf/PointsReleaseServices.php @@ -0,0 +1,112 @@ + 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, + ]; + } +} diff --git a/pro_v3.5.1/app/services/hjf/PointsRewardServices.php b/pro_v3.5.1/app/services/hjf/PointsRewardServices.php new file mode 100644 index 00000000..c1cdfa23 --- /dev/null +++ b/pro_v3.5.1/app/services/hjf/PointsRewardServices.php @@ -0,0 +1,135 @@ +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, + ]); + }); + } +} diff --git a/pro_v3.5.1/app/services/hjf/QueuePoolServices.php b/pro_v3.5.1/app/services/hjf/QueuePoolServices.php new file mode 100644 index 00000000..ad2cf983 --- /dev/null +++ b/pro_v3.5.1/app/services/hjf/QueuePoolServices.php @@ -0,0 +1,193 @@ +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} 轮"; + } +} diff --git a/pro_v3.5.1/app/services/order/StoreOrderCreateServices.php b/pro_v3.5.1/app/services/order/StoreOrderCreateServices.php index 9fd19270..8ef1a5c9 100644 --- a/pro_v3.5.1/app/services/order/StoreOrderCreateServices.php +++ b/pro_v3.5.1/app/services/order/StoreOrderCreateServices.php @@ -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); diff --git a/pro_v3.5.1/app/services/order/StoreOrderTakeServices.php b/pro_v3.5.1/app/services/order/StoreOrderTakeServices.php index 26544c30..e04fa957 100644 --- a/pro_v3.5.1/app/services/order/StoreOrderTakeServices.php +++ b/pro_v3.5.1/app/services/order/StoreOrderTakeServices.php @@ -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); //事业部 diff --git a/pro_v3.5.1/app/services/system/config/SystemConfigServices.php b/pro_v3.5.1/app/services/system/config/SystemConfigServices.php index e97162cc..bc603f14 100644 --- a/pro_v3.5.1/app/services/system/config/SystemConfigServices.php +++ b/pro_v3.5.1/app/services/system/config/SystemConfigServices.php @@ -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=用户确认收货后发放'), ]) ]); diff --git a/pro_v3.5.1/app/services/system/timer/SystemTimerServices.php b/pro_v3.5.1/app/services/system/timer/SystemTimerServices.php index fd165c82..01bd8fe6 100644 --- a/pro_v3.5.1/app/services/system/timer/SystemTimerServices.php +++ b/pro_v3.5.1/app/services/system/timer/SystemTimerServices.php @@ -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 diff --git a/pro_v3.5.1/app/services/user/UserBillServices.php b/pro_v3.5.1/app/services/user/UserBillServices.php index 661fb5e6..458a5199 100644 --- a/pro_v3.5.1/app/services/user/UserBillServices.php +++ b/pro_v3.5.1/app/services/user/UserBillServices.php @@ -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 + ], ]; /** diff --git a/pro_v3.5.1/app/services/user/UserServices.php b/pro_v3.5.1/app/services/user/UserServices.php index b321ad36..e49e67f9 100644 --- a/pro_v3.5.1/app/services/user/UserServices.php +++ b/pro_v3.5.1/app/services/user/UserServices.php @@ -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']] ?? ''; diff --git a/pro_v3.5.1/app/services/user/UserWechatuserServices.php b/pro_v3.5.1/app/services/user/UserWechatuserServices.php index 36007c08..df6b26fd 100644 --- a/pro_v3.5.1/app/services/user/UserWechatuserServices.php +++ b/pro_v3.5.1/app/services/user/UserWechatuserServices.php @@ -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; + } } diff --git a/pro_v3.5.1/config/database.php b/pro_v3.5.1/config/database.php index b73937ce..8c23b9b9 100644 --- a/pro_v3.5.1/config/database.php +++ b/pro_v3.5.1/config/database.php @@ -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'), // 密码 diff --git a/pro_v3.5.1/database/eb_points_release_log.sql b/pro_v3.5.1/database/eb_points_release_log.sql new file mode 100644 index 00000000..a102dc4d --- /dev/null +++ b/pro_v3.5.1/database/eb_points_release_log.sql @@ -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; diff --git a/pro_v3.5.1/database/hjf_migration.sql b/pro_v3.5.1/database/hjf_migration.sql index 2b9f5197..76676965 100644 --- a/pro_v3.5.1/database/hjf_migration.sql +++ b/pro_v3.5.1/database/hjf_migration.sql @@ -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; diff --git a/pro_v3.5.1/help/migrations/fsgx_v1.sql b/pro_v3.5.1/help/migrations/fsgx_v1.sql index 7db49c92..6066a9ac 100644 --- a/pro_v3.5.1/help/migrations/fsgx_v1.sql +++ b/pro_v3.5.1/help/migrations/fsgx_v1.sql @@ -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_id),desc 为保留字需加反引号 +-- 使用 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()); diff --git a/pro_v3.5.1/nginx-crmeb.conf b/pro_v3.5.1/nginx-crmeb.conf index 024de327..77e64928 100644 --- a/pro_v3.5.1/nginx-crmeb.conf +++ b/pro_v3.5.1/nginx-crmeb.conf @@ -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; diff --git a/pro_v3.5.1/route/admin.php b/pro_v3.5.1/route/admin.php index 3ade55b8..aae585f2 100644 --- a/pro_v3.5.1/route/admin.php +++ b/pro_v3.5.1/route/admin.php @@ -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' => '系统表单列表']); diff --git a/pro_v3.5.1/view/admin/.env.production b/pro_v3.5.1/view/admin/.env.production index dadf2827..6be27295 100644 --- a/pro_v3.5.1/view/admin/.env.production +++ b/pro_v3.5.1/view/admin/.env.production @@ -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 非独立部署默认为空 diff --git a/pro_v3.5.1/view/admin/src/api/system.js b/pro_v3.5.1/view/admin/src/api/system.js index 8d3cde3e..721abe9a 100644 --- a/pro_v3.5.1/view/admin/src/api/system.js +++ b/pro_v3.5.1/view/admin/src/api/system.js @@ -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}`, diff --git a/pro_v3.5.1/view/admin/src/components/fromBuild/inputBuild.vue b/pro_v3.5.1/view/admin/src/components/fromBuild/inputBuild.vue index 47f5cf25..e8d4d5b0 100644 --- a/pro_v3.5.1/view/admin/src/components/fromBuild/inputBuild.vue +++ b/pro_v3.5.1/view/admin/src/components/fromBuild/inputBuild.vue @@ -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('复制成功'); diff --git a/pro_v3.5.1/view/admin/src/components/fromBuild/radioBuild.vue b/pro_v3.5.1/view/admin/src/components/fromBuild/radioBuild.vue index 4300c602..03ed48da 100644 --- a/pro_v3.5.1/view/admin/src/components/fromBuild/radioBuild.vue +++ b/pro_v3.5.1/view/admin/src/components/fromBuild/radioBuild.vue @@ -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'), diff --git a/pro_v3.5.1/view/admin/src/components/fromSubmit/fromSubmit.vue b/pro_v3.5.1/view/admin/src/components/fromSubmit/fromSubmit.vue index fb6d5267..e9971d55 100644 --- a/pro_v3.5.1/view/admin/src/components/fromSubmit/fromSubmit.vue +++ b/pro_v3.5.1/view/admin/src/components/fromSubmit/fromSubmit.vue @@ -19,7 +19,7 @@ - + @@ -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: { diff --git a/pro_v3.5.1/view/admin/src/libs/start.js b/pro_v3.5.1/view/admin/src/libs/start.js index 77245c8f..7757d229 100644 --- a/pro_v3.5.1/view/admin/src/libs/start.js +++ b/pro_v3.5.1/view/admin/src/libs/start.js @@ -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 可以停止服务呢~")); diff --git a/pro_v3.5.1/view/admin/src/pages/product/productAdd/formModel.js b/pro_v3.5.1/view/admin/src/pages/product/productAdd/formModel.js index 99a839b7..af99dcb0 100644 --- a/pro_v3.5.1/view/admin/src/pages/product/productAdd/formModel.js +++ b/pro_v3.5.1/view/admin/src/pages/product/productAdd/formModel.js @@ -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 = [ diff --git a/pro_v3.5.1/view/admin/src/pages/setting/membershipLevel/index.vue b/pro_v3.5.1/view/admin/src/pages/setting/membershipLevel/index.vue index 0f9df107..2926a97c 100644 --- a/pro_v3.5.1/view/admin/src/pages/setting/membershipLevel/index.vue +++ b/pro_v3.5.1/view/admin/src/pages/setting/membershipLevel/index.vue @@ -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, diff --git a/pro_v3.5.1/view/admin/src/pages/system/crontab/index.vue b/pro_v3.5.1/view/admin/src/pages/system/crontab/index.vue index 1afd24d2..1dbfab43 100644 --- a/pro_v3.5.1/view/admin/src/pages/system/crontab/index.vue +++ b/pro_v3.5.1/view/admin/src/pages/system/crontab/index.vue @@ -29,6 +29,10 @@ >编辑 + + {{ row._running ? '执行中…' : '手动触发' }} + + 删除 @@ -46,7 +50,7 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/pro_v3.5.1/view/admin/src/pages/user/list/index.vue b/pro_v3.5.1/view/admin/src/pages/user/list/index.vue index 324dc0a0..a0d53358 100644 --- a/pro_v3.5.1/view/admin/src/pages/user/list/index.vue +++ b/pro_v3.5.1/view/admin/src/pages/user/list/index.vue @@ -54,15 +54,15 @@
- +