312 lines
10 KiB
PHP
312 lines
10 KiB
PHP
<?php
|
||
namespace app\controller\superadmin;
|
||
|
||
use app\BaseController;
|
||
use app\model\SystemConfig as SystemConfigModel;
|
||
use think\facade\Db;
|
||
use think\facade\Request;
|
||
|
||
/**
|
||
* 超管「高考版管理」Hub:概览、用户列表、功能配置(与前端 GaokaoHub.vue 对齐)
|
||
*/
|
||
class Gaokao extends BaseController
|
||
{
|
||
private const CONFIG_KEY = 'gaokao_hub';
|
||
|
||
private function authOk(): bool
|
||
{
|
||
$user = $this->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<string,mixed> $r
|
||
* @return array<string,mixed>
|
||
*/
|
||
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<string,mixed>
|
||
*/
|
||
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<string,mixed>
|
||
*/
|
||
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);
|
||
}
|
||
}
|