request->user ?? null; return !!($user && ($user['role'] ?? '') === 'superadmin'); } /** * GET /api/v1/superadmin/gaokao/overview */ public function overview() { if (!$this->authOk()) { return error('无权限访问', 403); } $todayStart = strtotime('today'); $totalUsers = (int) Db::name('gaokao_user_profile')->count(); $completedUsers = (int) Db::name('gaokao_user_profile') ->where(function ($q) { $q->where('analyzeStatus', 1)->whereOr('latestReportId', '>', 0); }) ->count(); $todayNew = (int) Db::name('gaokao_user_profile') ->where('createdAt', '>=', $todayStart) ->count(); $reportsGenerated = (int) Db::name('test_results') ->where('testType', 'gaokao') ->count(); $avgRow = Db::name('gaokao_user_profile') ->where('estimatedScore', '>', 0) ->fieldRaw('AVG(`estimatedScore`) AS a') ->find(); $avgScore = (int) round((float) ($avgRow['a'] ?? 0)); $conversionRate = $totalUsers > 0 ? (string) round($completedUsers / $totalUsers * 100, 1) . '%' : '0%'; $dist = [ '700plus' => 0, '650_700' => 0, '600_650' => 0, '550_600' => 0, '500_550' => 0, 'below500' => 0, ]; $scores = Db::name('gaokao_user_profile') ->where('estimatedScore', '>', 0) ->column('estimatedScore'); foreach ($scores as $s) { $n = (int) $s; if ($n >= 700) { $dist['700plus']++; } elseif ($n >= 650) { $dist['650_700']++; } elseif ($n >= 600) { $dist['600_650']++; } elseif ($n >= 550) { $dist['550_600']++; } elseif ($n >= 500) { $dist['500_550']++; } else { $dist['below500']++; } } return success([ 'totalUsers' => $totalUsers, 'completedUsers' => $completedUsers, 'todayNew' => $todayNew, 'reportsGenerated' => $reportsGenerated, 'conversionRate' => $conversionRate, 'avgScore' => $avgScore, 'scoreDistribution'=> $dist, ]); } /** * GET /api/v1/superadmin/gaokao/users * Query: q, status (completed|in_progress|abandoned), page, pageSize, tenantId(可选) */ public function users() { if (!$this->authOk()) { return error('无权限访问', 403); } $page = max(1, (int) Request::param('page', 1)); $pageSize = min(100, max(1, (int) Request::param('pageSize', 20))); $keyword = trim((string) Request::param('q', Request::param('keyword', ''))); $status = trim((string) Request::param('status', '')); $tenantId = (int) Request::param('tenantId', 0); $q = Db::name('gaokao_user_profile')->alias('g') ->join('wechat_users w', 'w.id = g.userId') ->leftJoin('enterprises e', 'e.id = g.tenantId'); if ($keyword !== '') { $like = '%' . addcslashes($keyword, '%_\\') . '%'; $q->whereRaw('(w.nickname LIKE ? OR w.phone LIKE ? OR g.name LIKE ?)', [$like, $like, $like]); } if ($tenantId > 0) { $q->where('g.tenantId', $tenantId); } if ($status === 'completed') { $q->where(function ($w) { $w->where('g.analyzeStatus', 1)->whereOr('g.latestReportId', '>', 0); }); } elseif ($status === 'in_progress') { $q->whereRaw('(g.`latestReportId` IS NULL OR g.`latestReportId` = 0) AND IFNULL(g.`analyzeStatus`,0) = 0') ->where(function ($w) { $w->where('g.entryStatus', 1) ->whereOr('g.formStatus', '>', 0) ->whereOr('g.mbtiStatus', '>', 0) ->whereOr('g.pdpStatus', '>', 0) ->whereOr('g.discStatus', '>', 0); }); } elseif ($status === 'abandoned') { $q->where('g.analyzeStatus', 2); } $total = (int) (clone $q)->count(); $rows = (clone $q) ->field('g.*,w.nickname,w.phone,w.avatar,e.name as tenantName') ->order('g.id', 'desc') ->page($page, $pageSize) ->select() ->toArray(); $list = []; foreach ($rows as $r) { $list[] = $this->formatGaokaoUserRow($r); } return paginate_response($list, $total, $page, $pageSize); } /** * @param array $r * @return array */ private function formatGaokaoUserRow(array $r): array { $formJson = $r['formJson'] ?? null; if (is_string($formJson)) { $formJson = json_decode($formJson, true) ?: []; } if (!is_array($formJson)) { $formJson = []; } $tagsJson = $r['tagsJson'] ?? null; if (is_string($tagsJson)) { $tagsJson = json_decode($tagsJson, true) ?: []; } if (!is_array($tagsJson)) { $tagsJson = []; } $mbtiType = (string) ($tagsJson['mbtiType'] ?? $tagsJson['mbti'] ?? $formJson['mbtiType'] ?? ''); $majorDirection = (string) ($formJson['majorDirection'] ?? $formJson['majorFocus'] ?? $tagsJson['majorDirection'] ?? ''); $rid = (int) ($r['latestReportId'] ?? 0); $an = (int) ($r['analyzeStatus'] ?? 0); if ($rid > 0 || $an === 1) { $st = 'completed'; } elseif ($an === 2) { $st = 'abandoned'; } elseif ((int) ($r['entryStatus'] ?? 0) === 1 || (int) ($r['formStatus'] ?? 0) > 0 || (int) ($r['mbtiStatus'] ?? 0) > 0 || (int) ($r['pdpStatus'] ?? 0) > 0 || (int) ($r['discStatus'] ?? 0) > 0) { $st = 'in_progress'; } else { // 尚未进入流程或仅占位档案:前端按「进行中」展示,避免误判为放弃 $st = 'in_progress'; } $lastAt = isset($r['lastAnalyzeAt']) ? (int) $r['lastAnalyzeAt'] : 0; return [ 'id' => (int) ($r['id'] ?? 0), 'userId' => (int) ($r['userId'] ?? 0), 'nickname' => (string) ($r['nickname'] ?? ''), 'phone' => (string) ($r['phone'] ?? ''), 'gaokaoScore' => isset($r['estimatedScore']) && $r['estimatedScore'] !== null ? (int) $r['estimatedScore'] : null, 'mbtiType' => $mbtiType !== '' ? $mbtiType : null, 'majorDirection' => $majorDirection !== '' ? $majorDirection : null, 'status' => $st, 'reportId' => $rid > 0 ? $rid : null, 'completedAt' => $lastAt > 0 ? date('Y-m-d H:i:s', $lastAt) : null, 'tenantName' => (string) ($r['tenantName'] ?? ''), ]; } /** * GET /api/v1/superadmin/gaokao/config */ public function getConfig() { if (!$this->authOk()) { return error('无权限访问', 403); } return success($this->loadGaokaoHubConfig()); } /** * POST /api/v1/superadmin/gaokao/config */ public function saveConfig() { if (!$this->authOk()) { return error('无权限访问', 403); } $body = Request::post(); if (!is_array($body)) { $body = []; } $merged = array_merge($this->defaultGaokaoHubConfig(), $body); $merged['reportPrice'] = max(0, (float) ($merged['reportPrice'] ?? 0)); $merged['freeQuota'] = max(0, (int) ($merged['freeQuota'] ?? 0)); $merged['maxMajors'] = max(3, min(20, (int) ($merged['maxMajors'] ?? 10))); foreach (['gaokaoEnabled', 'faceEnabled', 'mbtiEnabled', 'aiRecommendEnabled', 'paidReportEnabled', 'privacyNoticeEnabled'] as $k) { $merged[$k] = !empty($merged[$k]); } $row = SystemConfigModel::where('key', self::CONFIG_KEY)->where('enterprise_id', 0)->find(); $now = time(); if ($row) { $row->value = $merged; $row->updatedAt = $now; $row->save(); } else { SystemConfigModel::create([ 'key' => self::CONFIG_KEY, 'enterprise_id' => 0, 'value' => $merged, 'description' => '超管高考版 Hub 开关与参数', 'createdAt' => $now, 'updatedAt' => $now, ]); } return success($merged, '保存成功'); } /** * @return array */ private function defaultGaokaoHubConfig(): array { return [ 'gaokaoEnabled' => true, 'faceEnabled' => true, 'mbtiEnabled' => true, 'aiRecommendEnabled' => true, 'paidReportEnabled' => false, 'privacyNoticeEnabled' => true, 'reportPrice' => 9.9, 'freeQuota' => 1, 'maxMajors' => 10, ]; } /** * @return array */ private function loadGaokaoHubConfig(): array { $defaults = $this->defaultGaokaoHubConfig(); $row = SystemConfigModel::where('key', self::CONFIG_KEY)->where('enterprise_id', 0)->find(); if (!$row || $row->value === null || $row->value === '') { return $defaults; } $v = $row->value; if (!is_array($v)) { return $defaults; } return array_merge($defaults, $v); } }