高考版优化
审核模式优化
This commit is contained in:
@@ -133,13 +133,18 @@ class Analytics extends BaseController
|
||||
}
|
||||
$days = min(180, max(1, (int) Request::param('days', 30)));
|
||||
$since = date('Y-m-d H:i:s', time() - $days * 86400);
|
||||
$page = max(1, (int) Request::param('page', 1));
|
||||
$pageSize = min(100, max(1, (int) Request::param('pageSize', 20)));
|
||||
$offset = ($page - 1) * $pageSize;
|
||||
|
||||
try {
|
||||
$rows = Db::name('analytics_events')
|
||||
$baseQ = Db::name('analytics_events')
|
||||
->where('userId', $userId)
|
||||
->where('createdAt', '>=', $since)
|
||||
->where('createdAt', '>=', $since);
|
||||
$total = (int) (clone $baseQ)->count();
|
||||
$rows = (clone $baseQ)
|
||||
->order('id', 'desc')
|
||||
->limit(200)
|
||||
->limit($offset, $pageSize)
|
||||
->select()
|
||||
->toArray();
|
||||
|
||||
@@ -156,10 +161,12 @@ class Analytics extends BaseController
|
||||
$rows = AnalyticsEventLabels::withCn($rows);
|
||||
|
||||
return success([
|
||||
'userId' => $userId,
|
||||
'days' => $days,
|
||||
'list' => $rows,
|
||||
'total' => count($rows),
|
||||
'userId' => $userId,
|
||||
'days' => $days,
|
||||
'list' => $rows,
|
||||
'total' => $total,
|
||||
'page' => $page,
|
||||
'pageSize' => $pageSize,
|
||||
]);
|
||||
} catch (\Throwable $e) {
|
||||
return success([
|
||||
@@ -167,6 +174,8 @@ class Analytics extends BaseController
|
||||
'days' => $days,
|
||||
'list' => [],
|
||||
'total' => 0,
|
||||
'page' => $page,
|
||||
'pageSize' => $pageSize,
|
||||
'tableMissing' => true,
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -348,6 +348,63 @@ class DataMigration extends BaseController
|
||||
Db::name('user_profile')->insert($base);
|
||||
}
|
||||
|
||||
/**
|
||||
* 归属目标企业且仍存在 personal(NULL) 画像行的用户,逐批合并(避免一次载入全部 userId / 超长事务)。
|
||||
*
|
||||
* @return int 实际调用合并逻辑的用户数
|
||||
*/
|
||||
private function mergePersonalProfilesForEnterpriseInBatches(int $targetEnterpriseId, int $now, int $batchSize = 400): int
|
||||
{
|
||||
if ($targetEnterpriseId <= 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$tUp = Db::name('user_profile')->getTable();
|
||||
$tWu = Db::name('wechat_users')->getTable();
|
||||
$batchSize = max(50, min($batchSize, 2000));
|
||||
$lastUid = 0;
|
||||
$merged = 0;
|
||||
|
||||
while (true) {
|
||||
$sql = 'SELECT DISTINCT `up`.`userId` AS `uid` FROM `' . $tUp . '` `up` '
|
||||
. 'INNER JOIN `' . $tWu . '` `w` ON `w`.`id` = `up`.`userId` '
|
||||
. 'WHERE `up`.`userType` = \'personal\' '
|
||||
. 'AND (`up`.`enterpriseId` IS NULL OR `up`.`enterpriseId` = 0) '
|
||||
. 'AND `w`.`enterpriseId` = ? '
|
||||
. 'AND `up`.`userId` > ? '
|
||||
. 'ORDER BY `up`.`userId` ASC '
|
||||
. 'LIMIT ' . $batchSize;
|
||||
|
||||
$rows = Db::query($sql, [$targetEnterpriseId, $lastUid]);
|
||||
if ($rows === null || $rows === [] || !is_array($rows)) {
|
||||
break;
|
||||
}
|
||||
|
||||
$uids = [];
|
||||
foreach ($rows as $row) {
|
||||
$uid = (int) ($row['uid'] ?? 0);
|
||||
if ($uid > 0) {
|
||||
$uids[] = $uid;
|
||||
}
|
||||
}
|
||||
if ($uids === []) {
|
||||
break;
|
||||
}
|
||||
|
||||
foreach ($uids as $uid) {
|
||||
$this->mergePersonalUserProfileIntoEnterprise($uid, $targetEnterpriseId, $now);
|
||||
$merged++;
|
||||
}
|
||||
|
||||
$lastUid = max($uids);
|
||||
if (count($rows) < $batchSize) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return $merged;
|
||||
}
|
||||
|
||||
/**
|
||||
* 存客宝归并:wechat_users 无企业用户写入目标企业;
|
||||
* 已/将归属该企业的用户下,test_results 中 enterpriseId 为空的记录补写目标企业;
|
||||
@@ -386,27 +443,25 @@ class DataMigration extends BaseController
|
||||
return error('目标企业不存在', 404);
|
||||
}
|
||||
|
||||
$orphanUserIds = Db::name('wechat_users')
|
||||
$eid = $targetEnterpriseId;
|
||||
|
||||
$wechatAffected = (int) Db::name('wechat_users')
|
||||
->whereRaw($this->sqlWechatUserUnassignedEnterprise())
|
||||
->column('id');
|
||||
$orphanUserIds = array_values(array_unique(array_filter(array_map('intval', $orphanUserIds))));
|
||||
$wechatAffected = count($orphanUserIds);
|
||||
|
||||
$boundNow = Db::name('wechat_users')
|
||||
->where('enterpriseId', $targetEnterpriseId)
|
||||
->column('id');
|
||||
$boundNow = array_values(array_unique(array_filter(array_map('intval', $boundNow))));
|
||||
$usersInScope = array_values(array_unique(array_merge($boundNow, $orphanUserIds)));
|
||||
|
||||
$testResultsAffected = empty($usersInScope) ? 0 : (int) Db::name('test_results')
|
||||
->whereIn('userId', $usersInScope)
|
||||
->whereRaw($this->sqlTestResultUnassignedEnterprise())
|
||||
->count();
|
||||
|
||||
$userProfilePersonalRows = empty($usersInScope) ? 0 : (int) Db::name('user_profile')
|
||||
->where('userType', 'personal')
|
||||
->whereRaw('(`enterpriseId` IS NULL OR `enterpriseId` = 0)')
|
||||
->whereIn('userId', $usersInScope)
|
||||
$wechatScopeExpr = '((w.`enterpriseId` IS NULL OR w.`enterpriseId` = 0) OR w.`enterpriseId` = ' . $eid . ')';
|
||||
|
||||
$testResultsAffected = (int) Db::name('test_results')->alias('tr')
|
||||
->join('wechat_users w', 'w.id = tr.userId')
|
||||
->whereRaw('(tr.`enterpriseId` IS NULL OR tr.`enterpriseId` = 0)')
|
||||
->whereRaw($wechatScopeExpr)
|
||||
->count();
|
||||
|
||||
$userProfilePersonalRows = (int) Db::name('user_profile')->alias('up')
|
||||
->join('wechat_users w', 'w.id = up.userId')
|
||||
->where('up.userType', 'personal')
|
||||
->whereRaw('(up.`enterpriseId` IS NULL OR up.`enterpriseId` = 0)')
|
||||
->whereRaw($wechatScopeExpr)
|
||||
->count();
|
||||
|
||||
$preview = [
|
||||
@@ -434,47 +489,52 @@ class DataMigration extends BaseController
|
||||
return success($preview, '无需归并');
|
||||
}
|
||||
|
||||
if (function_exists('set_time_limit')) {
|
||||
@set_time_limit(0);
|
||||
}
|
||||
@ignore_user_abort(true);
|
||||
|
||||
$now = time();
|
||||
|
||||
Db::startTrans();
|
||||
try {
|
||||
if (!empty($orphanUserIds)) {
|
||||
Db::name('wechat_users')
|
||||
->whereIn('id', $orphanUserIds)
|
||||
->whereRaw($this->sqlWechatUserUnassignedEnterprise())
|
||||
->update([
|
||||
'enterpriseId' => $targetEnterpriseId,
|
||||
'updatedAt' => $now,
|
||||
]);
|
||||
}
|
||||
Db::name('wechat_users')
|
||||
->whereRaw($this->sqlWechatUserUnassignedEnterprise())
|
||||
->update([
|
||||
'enterpriseId' => $targetEnterpriseId,
|
||||
'updatedAt' => $now,
|
||||
]);
|
||||
|
||||
$boundAfter = Db::name('wechat_users')
|
||||
->where('enterpriseId', $targetEnterpriseId)
|
||||
->column('id');
|
||||
$boundAfter = array_values(array_unique(array_filter(array_map('intval', $boundAfter))));
|
||||
|
||||
if (!empty($boundAfter)) {
|
||||
Db::name('test_results')
|
||||
->whereIn('userId', $boundAfter)
|
||||
->whereRaw($this->sqlTestResultUnassignedEnterprise())
|
||||
->update([
|
||||
'enterpriseId' => $targetEnterpriseId,
|
||||
'testScope' => 'enterprise',
|
||||
'updatedAt' => $now,
|
||||
]);
|
||||
|
||||
foreach ($boundAfter as $uid) {
|
||||
$this->mergePersonalUserProfileIntoEnterprise((int) $uid, $targetEnterpriseId, $now);
|
||||
}
|
||||
}
|
||||
$trTable = Db::name('test_results')->getTable();
|
||||
$wuTable = Db::name('wechat_users')->getTable();
|
||||
Db::execute(
|
||||
'UPDATE `' . $trTable . '` AS `tr` INNER JOIN `' . $wuTable . '` AS `w` ON `tr`.`userId` = `w`.`id` '
|
||||
. 'SET `tr`.`enterpriseId` = ?, `tr`.`testScope` = \'enterprise\', `tr`.`updatedAt` = ? '
|
||||
. 'WHERE `w`.`enterpriseId` = ? AND (`tr`.`enterpriseId` IS NULL OR `tr`.`enterpriseId` = 0)',
|
||||
[$targetEnterpriseId, $now, $targetEnterpriseId]
|
||||
);
|
||||
|
||||
Db::commit();
|
||||
} catch (\Throwable $e) {
|
||||
Db::rollback();
|
||||
return error('归并失败:' . $e->getMessage(), 500);
|
||||
|
||||
return error('归并失败(用户/测试写入):' . $e->getMessage(), 500);
|
||||
}
|
||||
|
||||
$preview['executed'] = true;
|
||||
$preview['hint'] = '已写入:小程序用户归属、无企业测试记录、画像 personal 行合并已完成。';
|
||||
try {
|
||||
$mergedProfiles = $this->mergePersonalProfilesForEnterpriseInBatches($targetEnterpriseId, $now);
|
||||
} catch (\Throwable $e) {
|
||||
return error(
|
||||
'用户与测试记录已写入,但画像合并失败(可重试本接口):' . $e->getMessage(),
|
||||
500
|
||||
);
|
||||
}
|
||||
|
||||
$preview['executed'] = true;
|
||||
$preview['profileMergeProcessed'] = $mergedProfiles;
|
||||
$preview['hint'] = '已写入:小程序用户归属、无企业测试记录已批量更新;personal 画像已合并 '
|
||||
. $mergedProfiles . ' 人次。';
|
||||
|
||||
return success($preview, '归并完成');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -392,6 +392,8 @@ class Enterprise extends BaseController
|
||||
$enterprise->contactName = $data['contactName'] ?? null;
|
||||
$enterprise->contactPhone = $data['contactPhone'] ?? null;
|
||||
$enterprise->contactEmail = $data['contactEmail'] ?? null;
|
||||
$enterprise->idCardNumber = $data['idCardNumber'] ?? null;
|
||||
$enterprise->businessLicenseUrl = $data['businessLicenseUrl'] ?? null;
|
||||
$enterprise->balance = $data['balance'] ?? 0.00;
|
||||
$enterprise->status = $status;
|
||||
$enterprise->trialExpireAt = ($status === 'trial' && isset($data['trialExpireAt'])) ? $data['trialExpireAt'] : null;
|
||||
|
||||
311
api/app/controller/superadmin/Gaokao.php
Normal file
311
api/app/controller/superadmin/Gaokao.php
Normal file
@@ -0,0 +1,311 @@
|
||||
<?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);
|
||||
}
|
||||
}
|
||||
@@ -1,260 +1,319 @@
|
||||
<?php
|
||||
namespace app\controller\superadmin;
|
||||
|
||||
use app\BaseController;
|
||||
use app\model\PricingConfig as PricingConfigModel;
|
||||
use think\facade\Request;
|
||||
|
||||
/**
|
||||
* 全局定价管理控制器(超管专用)
|
||||
*/
|
||||
class Pricing extends BaseController
|
||||
{
|
||||
/**
|
||||
* 获取定价配置
|
||||
* @return \think\response\Json
|
||||
*/
|
||||
public function index()
|
||||
{
|
||||
// 验证是否为超级管理员
|
||||
$user = $this->request->user ?? null;
|
||||
if (!$user || $user['role'] !== 'superadmin') {
|
||||
return error('无权限访问', 403);
|
||||
}
|
||||
|
||||
$type = Request::param('type', ''); // personal/enterprise/deep
|
||||
$enterpriseId = Request::param('enterpriseId', null); // 仅 type=enterprise 时有效,不传为全局
|
||||
|
||||
if ($type) {
|
||||
$enterpriseId = $enterpriseId !== null && $enterpriseId !== '' ? (int) $enterpriseId : null;
|
||||
$query = PricingConfigModel::where('type', $type);
|
||||
if ($type === 'enterprise') {
|
||||
$query->where(empty($enterpriseId) ? 'enterpriseId' : 'enterpriseId', empty($enterpriseId) ? 'null' : '=', empty($enterpriseId) ? null : $enterpriseId);
|
||||
if (empty($enterpriseId)) {
|
||||
$query->whereNull('enterpriseId');
|
||||
} else {
|
||||
$query->where('enterpriseId', $enterpriseId);
|
||||
}
|
||||
} else {
|
||||
$query->whereNull('enterpriseId');
|
||||
}
|
||||
$config = $query->find();
|
||||
if (!$config) {
|
||||
return error('定价配置不存在', 404);
|
||||
}
|
||||
$cfg = $config->config;
|
||||
if (is_array($cfg)) {
|
||||
if ($type === 'personal') {
|
||||
$cfg = self::normalizePersonalPricingConfig($cfg);
|
||||
} elseif ($type === 'enterprise') {
|
||||
$cfg = self::normalizeEnterprisePricingConfig($cfg);
|
||||
}
|
||||
}
|
||||
|
||||
return success([
|
||||
'type' => $config->type,
|
||||
'enterpriseId' => $config->enterpriseId,
|
||||
'config' => $cfg
|
||||
]);
|
||||
} else {
|
||||
// 获取所有:个人/深度各一条(全局),企业=全局默认定价 + 各企业专属列表
|
||||
$configs = PricingConfigModel::select()->toArray();
|
||||
$result = ['personal' => null, 'enterprise' => null, 'deep' => null, 'enterpriseList' => []];
|
||||
foreach ($configs as $row) {
|
||||
if ($row['enterpriseId'] === null || $row['enterpriseId'] === '') {
|
||||
$result[$row['type']] = $row['config'];
|
||||
} else {
|
||||
if ($row['type'] === 'enterprise') {
|
||||
$result['enterpriseList'][] = ['enterpriseId' => (int) $row['enterpriseId'], 'config' => $row['config']];
|
||||
}
|
||||
}
|
||||
}
|
||||
if (is_array($result['personal'])) {
|
||||
$result['personal'] = self::normalizePersonalPricingConfig($result['personal']);
|
||||
}
|
||||
if (is_array($result['enterprise'])) {
|
||||
$result['enterprise'] = self::normalizeEnterprisePricingConfig($result['enterprise']);
|
||||
}
|
||||
foreach ($result['enterpriseList'] as $i => $entRow) {
|
||||
if (isset($entRow['config']) && is_array($entRow['config'])) {
|
||||
$result['enterpriseList'][$i]['config'] = self::normalizeEnterprisePricingConfig($entRow['config']);
|
||||
}
|
||||
}
|
||||
|
||||
return success($result);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新定价配置
|
||||
* @return \think\response\Json
|
||||
*/
|
||||
public function update()
|
||||
{
|
||||
// 验证是否为超级管理员
|
||||
$user = $this->request->user ?? null;
|
||||
if (!$user || $user['role'] !== 'superadmin') {
|
||||
return error('无权限访问', 403);
|
||||
}
|
||||
|
||||
// PUT JSON body 需显式解析,直接用 param() 读深层嵌套数组可能丢失数据
|
||||
$rawBody = (string) $this->request->getContent();
|
||||
$jsonData = ($rawBody !== '') ? json_decode($rawBody, true) : null;
|
||||
|
||||
if (is_array($jsonData)) {
|
||||
$type = (string) ($jsonData['type'] ?? '');
|
||||
$enterpriseId = $jsonData['enterpriseId'] ?? null;
|
||||
$config = $jsonData['config'] ?? [];
|
||||
} else {
|
||||
$type = (string) Request::param('type', '');
|
||||
$enterpriseId = Request::param('enterpriseId', null);
|
||||
$config = Request::param('config', []);
|
||||
}
|
||||
|
||||
if (empty($type)) {
|
||||
return error('定价类型不能为空', 400);
|
||||
}
|
||||
|
||||
if (!in_array($type, ['personal', 'enterprise', 'deep', 'deep_personal', 'deep_enterprise'])) {
|
||||
return error('定价类型无效', 400);
|
||||
}
|
||||
|
||||
if (empty($config) || !is_array($config)) {
|
||||
return error('配置数据不能为空', 400);
|
||||
}
|
||||
|
||||
$enterpriseId = ($type === 'enterprise' && $enterpriseId !== null && $enterpriseId !== '') ? (int) $enterpriseId : null;
|
||||
if ($type !== 'enterprise') {
|
||||
$enterpriseId = null;
|
||||
}
|
||||
|
||||
$query = PricingConfigModel::where('type', $type);
|
||||
if ($type === 'enterprise') {
|
||||
if ($enterpriseId !== null) {
|
||||
$query->where('enterpriseId', $enterpriseId);
|
||||
} else {
|
||||
$query->whereNull('enterpriseId');
|
||||
}
|
||||
} else {
|
||||
$query->whereNull('enterpriseId');
|
||||
}
|
||||
// deep_personal / deep_enterprise 仅全局一条,不按企业分
|
||||
$pricingConfig = $query->find();
|
||||
|
||||
if (!$pricingConfig) {
|
||||
$pricingConfig = PricingConfigModel::create([
|
||||
'type' => $type,
|
||||
'enterpriseId' => $enterpriseId,
|
||||
'config' => $config
|
||||
]);
|
||||
} else {
|
||||
$pricingConfig->config = $config;
|
||||
$pricingConfig->save();
|
||||
}
|
||||
|
||||
return success([
|
||||
'type' => $pricingConfig->type,
|
||||
'enterpriseId' => $pricingConfig->enterpriseId,
|
||||
'config' => $pricingConfig->config
|
||||
], '保存成功');
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量更新定价配置
|
||||
* @return \think\response\Json
|
||||
*/
|
||||
public function batchUpdate()
|
||||
{
|
||||
// 验证是否为超级管理员
|
||||
$user = $this->request->user ?? null;
|
||||
if (!$user || $user['role'] !== 'superadmin') {
|
||||
return error('无权限访问', 403);
|
||||
}
|
||||
|
||||
$rawBody = (string) $this->request->getContent();
|
||||
$jsonData = ($rawBody !== '') ? json_decode($rawBody, true) : null;
|
||||
$data = is_array($jsonData) ? ($jsonData['data'] ?? []) : Request::param('data', []);
|
||||
|
||||
if (empty($data) || !is_array($data)) {
|
||||
return error('配置数据不能为空', 400);
|
||||
}
|
||||
|
||||
$successCount = 0;
|
||||
$errors = [];
|
||||
|
||||
foreach ($data as $type => $config) {
|
||||
if (!in_array($type, ['personal', 'enterprise', 'deep', 'deep_personal', 'deep_enterprise'])) {
|
||||
$errors[] = "类型 {$type} 无效";
|
||||
continue;
|
||||
}
|
||||
|
||||
if (empty($config) || !is_array($config)) {
|
||||
$errors[] = "类型 {$type} 的配置数据无效";
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
$pricingConfig = PricingConfigModel::where('type', $type)->whereNull('enterpriseId')->find();
|
||||
|
||||
if (!$pricingConfig) {
|
||||
PricingConfigModel::create([
|
||||
'type' => $type,
|
||||
'config' => $config
|
||||
]);
|
||||
} else {
|
||||
$pricingConfig->config = $config;
|
||||
$pricingConfig->save();
|
||||
}
|
||||
$successCount++;
|
||||
} catch (\Exception $e) {
|
||||
$errors[] = "保存类型 {$type} 失败:" . $e->getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($errors)) {
|
||||
return error('部分配置保存失败:' . implode(';', $errors), 400);
|
||||
}
|
||||
|
||||
return success(null, "成功保存 {$successCount} 个配置");
|
||||
}
|
||||
|
||||
/**
|
||||
* 旧库 JSON 可能缺少高考等字段,合并默认值便于管理端展示与保存
|
||||
*
|
||||
* @param array<string,mixed> $cfg
|
||||
* @return array<string,mixed>
|
||||
*/
|
||||
private static function normalizePersonalPricingConfig(array $cfg): array
|
||||
{
|
||||
$defaults = [
|
||||
'face' => 0,
|
||||
'mbti' => 0,
|
||||
'disc' => 0,
|
||||
'pdp' => 0,
|
||||
'sbti' => 0,
|
||||
'gaokao' => 0,
|
||||
];
|
||||
|
||||
return array_merge($defaults, $cfg);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string,mixed> $cfg
|
||||
* @return array<string,mixed>
|
||||
*/
|
||||
private static function normalizeEnterprisePricingConfig(array $cfg): array
|
||||
{
|
||||
$defaults = [
|
||||
'face' => 0,
|
||||
'mbti' => 0,
|
||||
'pdp' => 0,
|
||||
'disc' => 0,
|
||||
'sbti' => 0,
|
||||
'gaokao' => 0,
|
||||
'minRecharge' => 0,
|
||||
];
|
||||
|
||||
return array_merge($defaults, $cfg);
|
||||
}
|
||||
}
|
||||
|
||||
<?php
|
||||
namespace app\controller\superadmin;
|
||||
|
||||
use app\BaseController;
|
||||
use app\model\PricingConfig as PricingConfigModel;
|
||||
use think\facade\Request;
|
||||
|
||||
/**
|
||||
* 全局定价管理控制器(超管专用)
|
||||
*/
|
||||
class Pricing extends BaseController
|
||||
{
|
||||
/**
|
||||
* 获取定价配置
|
||||
* @return \think\response\Json
|
||||
*/
|
||||
public function index()
|
||||
{
|
||||
// 验证是否为超级管理员
|
||||
$user = $this->request->user ?? null;
|
||||
if (!$user || $user['role'] !== 'superadmin') {
|
||||
return error('无权限访问', 403);
|
||||
}
|
||||
|
||||
$type = Request::param('type', ''); // personal/enterprise/gaokao/deep
|
||||
$enterpriseId = Request::param('enterpriseId', null); // 仅 type=enterprise 时有效,不传为全局
|
||||
|
||||
if ($type) {
|
||||
$enterpriseId = $enterpriseId !== null && $enterpriseId !== '' ? (int) $enterpriseId : null;
|
||||
$query = PricingConfigModel::where('type', $type);
|
||||
if ($type === 'enterprise') {
|
||||
$query->where(empty($enterpriseId) ? 'enterpriseId' : 'enterpriseId', empty($enterpriseId) ? 'null' : '=', empty($enterpriseId) ? null : $enterpriseId);
|
||||
if (empty($enterpriseId)) {
|
||||
$query->whereNull('enterpriseId');
|
||||
} else {
|
||||
$query->where('enterpriseId', $enterpriseId);
|
||||
}
|
||||
} else {
|
||||
$query->whereNull('enterpriseId');
|
||||
}
|
||||
$config = $query->find();
|
||||
if (!$config) {
|
||||
return error('定价配置不存在', 404);
|
||||
}
|
||||
$cfg = $config->config;
|
||||
if (is_array($cfg)) {
|
||||
if ($type === 'personal') {
|
||||
$cfg = self::normalizePersonalPricingConfig($cfg);
|
||||
} elseif ($type === 'enterprise') {
|
||||
$cfg = self::normalizeEnterprisePricingConfig($cfg);
|
||||
} elseif ($type === 'gaokao') {
|
||||
$cfg = self::normalizeGaokaoPricingConfig($cfg);
|
||||
}
|
||||
}
|
||||
|
||||
return success([
|
||||
'type' => $config->type,
|
||||
'enterpriseId' => $config->enterpriseId,
|
||||
'config' => $cfg
|
||||
]);
|
||||
} else {
|
||||
// 获取所有:个人/深度各一条(全局),企业=全局默认定价 + 各企业专属列表
|
||||
$configs = PricingConfigModel::select()->toArray();
|
||||
$result = ['personal' => null, 'enterprise' => null, 'gaokao' => null, 'deep' => null, 'enterpriseList' => []];
|
||||
foreach ($configs as $row) {
|
||||
if ($row['enterpriseId'] === null || $row['enterpriseId'] === '') {
|
||||
$result[$row['type']] = $row['config'];
|
||||
} else {
|
||||
if ($row['type'] === 'enterprise') {
|
||||
$result['enterpriseList'][] = ['enterpriseId' => (int) $row['enterpriseId'], 'config' => $row['config']];
|
||||
}
|
||||
}
|
||||
}
|
||||
if (is_array($result['personal'])) {
|
||||
$result['personal'] = self::normalizePersonalPricingConfig($result['personal']);
|
||||
}
|
||||
if (is_array($result['enterprise'])) {
|
||||
$result['enterprise'] = self::normalizeEnterprisePricingConfig($result['enterprise']);
|
||||
}
|
||||
if (is_array($result['gaokao'])) {
|
||||
$result['gaokao'] = self::normalizeGaokaoPricingConfig($result['gaokao']);
|
||||
}
|
||||
foreach ($result['enterpriseList'] as $i => $entRow) {
|
||||
if (isset($entRow['config']) && is_array($entRow['config'])) {
|
||||
$result['enterpriseList'][$i]['config'] = self::normalizeEnterprisePricingConfig($entRow['config']);
|
||||
}
|
||||
}
|
||||
|
||||
return success($result);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新定价配置
|
||||
* @return \think\response\Json
|
||||
*/
|
||||
public function update()
|
||||
{
|
||||
// 验证是否为超级管理员
|
||||
$user = $this->request->user ?? null;
|
||||
if (!$user || $user['role'] !== 'superadmin') {
|
||||
return error('无权限访问', 403);
|
||||
}
|
||||
|
||||
// PUT JSON body 需显式解析,直接用 param() 读深层嵌套数组可能丢失数据
|
||||
$rawBody = (string) $this->request->getContent();
|
||||
$jsonData = ($rawBody !== '') ? json_decode($rawBody, true) : null;
|
||||
|
||||
if (is_array($jsonData)) {
|
||||
$type = (string) ($jsonData['type'] ?? '');
|
||||
$enterpriseId = $jsonData['enterpriseId'] ?? null;
|
||||
$config = $jsonData['config'] ?? [];
|
||||
} else {
|
||||
$type = (string) Request::param('type', '');
|
||||
$enterpriseId = Request::param('enterpriseId', null);
|
||||
$config = Request::param('config', []);
|
||||
}
|
||||
|
||||
if (empty($type)) {
|
||||
return error('定价类型不能为空', 400);
|
||||
}
|
||||
|
||||
if (!in_array($type, ['personal', 'enterprise', 'gaokao', 'deep', 'deep_personal', 'deep_enterprise'], true)) {
|
||||
return error('定价类型无效', 400);
|
||||
}
|
||||
|
||||
if (empty($config) || !is_array($config)) {
|
||||
return error('配置数据不能为空', 400);
|
||||
}
|
||||
|
||||
$enterpriseId = ($type === 'enterprise' && $enterpriseId !== null && $enterpriseId !== '') ? (int) $enterpriseId : null;
|
||||
if ($type !== 'enterprise') {
|
||||
$enterpriseId = null;
|
||||
}
|
||||
|
||||
$config = self::sanitizeConfigByType($type, $config);
|
||||
|
||||
$query = PricingConfigModel::where('type', $type);
|
||||
if ($type === 'enterprise') {
|
||||
if ($enterpriseId !== null) {
|
||||
$query->where('enterpriseId', $enterpriseId);
|
||||
} else {
|
||||
$query->whereNull('enterpriseId');
|
||||
}
|
||||
} else {
|
||||
$query->whereNull('enterpriseId');
|
||||
}
|
||||
// deep_personal / deep_enterprise 仅全局一条,不按企业分
|
||||
$pricingConfig = $query->find();
|
||||
|
||||
if (!$pricingConfig) {
|
||||
$pricingConfig = PricingConfigModel::create([
|
||||
'type' => $type,
|
||||
'enterpriseId' => $enterpriseId,
|
||||
'config' => $config
|
||||
]);
|
||||
} else {
|
||||
$pricingConfig->config = $config;
|
||||
$pricingConfig->save();
|
||||
}
|
||||
|
||||
return success([
|
||||
'type' => $pricingConfig->type,
|
||||
'enterpriseId' => $pricingConfig->enterpriseId,
|
||||
'config' => $pricingConfig->config
|
||||
], '保存成功');
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量更新定价配置
|
||||
* @return \think\response\Json
|
||||
*/
|
||||
public function batchUpdate()
|
||||
{
|
||||
// 验证是否为超级管理员
|
||||
$user = $this->request->user ?? null;
|
||||
if (!$user || $user['role'] !== 'superadmin') {
|
||||
return error('无权限访问', 403);
|
||||
}
|
||||
|
||||
$rawBody = (string) $this->request->getContent();
|
||||
$jsonData = ($rawBody !== '') ? json_decode($rawBody, true) : null;
|
||||
$data = is_array($jsonData) ? ($jsonData['data'] ?? []) : Request::param('data', []);
|
||||
|
||||
if (empty($data) || !is_array($data)) {
|
||||
return error('配置数据不能为空', 400);
|
||||
}
|
||||
|
||||
$successCount = 0;
|
||||
$errors = [];
|
||||
|
||||
foreach ($data as $type => $config) {
|
||||
if (!in_array($type, ['personal', 'enterprise', 'gaokao', 'deep', 'deep_personal', 'deep_enterprise'], true)) {
|
||||
$errors[] = "类型 {$type} 无效";
|
||||
continue;
|
||||
}
|
||||
|
||||
if (empty($config) || !is_array($config)) {
|
||||
$errors[] = "类型 {$type} 的配置数据无效";
|
||||
continue;
|
||||
}
|
||||
|
||||
$config = self::sanitizeConfigByType($type, $config);
|
||||
|
||||
try {
|
||||
$pricingConfig = PricingConfigModel::where('type', $type)->whereNull('enterpriseId')->find();
|
||||
|
||||
if (!$pricingConfig) {
|
||||
PricingConfigModel::create([
|
||||
'type' => $type,
|
||||
'config' => $config
|
||||
]);
|
||||
} else {
|
||||
$pricingConfig->config = $config;
|
||||
$pricingConfig->save();
|
||||
}
|
||||
$successCount++;
|
||||
} catch (\Exception $e) {
|
||||
$errors[] = "保存类型 {$type} 失败:" . $e->getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($errors)) {
|
||||
return error('部分配置保存失败:' . implode(';', $errors), 400);
|
||||
}
|
||||
|
||||
return success(null, "成功保存 {$successCount} 个配置");
|
||||
}
|
||||
|
||||
/**
|
||||
* 旧库 JSON 可能缺少高考等字段,合并默认值便于管理端展示与保存
|
||||
*
|
||||
* @param array<string,mixed> $cfg
|
||||
* @return array<string,mixed>
|
||||
*/
|
||||
private static function normalizePersonalPricingConfig(array $cfg): array
|
||||
{
|
||||
$defaults = [
|
||||
'face' => 0,
|
||||
'mbti' => 0,
|
||||
'disc' => 0,
|
||||
'pdp' => 0,
|
||||
'sbti' => 0,
|
||||
];
|
||||
|
||||
return array_merge($defaults, $cfg);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string,mixed> $cfg
|
||||
* @return array<string,mixed>
|
||||
*/
|
||||
private static function normalizeEnterprisePricingConfig(array $cfg): array
|
||||
{
|
||||
$defaults = [
|
||||
'face' => 0,
|
||||
'mbti' => 0,
|
||||
'pdp' => 0,
|
||||
'disc' => 0,
|
||||
'sbti' => 0,
|
||||
'minRecharge' => 0,
|
||||
];
|
||||
|
||||
return array_merge($defaults, $cfg);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string,mixed> $cfg
|
||||
* @return array<string,mixed>
|
||||
*/
|
||||
private static function normalizeGaokaoPricingConfig(array $cfg): array
|
||||
{
|
||||
$defaults = [
|
||||
'face' => 0,
|
||||
'mbti' => 0,
|
||||
'disc' => 0,
|
||||
'pdp' => 0,
|
||||
'sbti' => 0,
|
||||
'gaokao' => 0,
|
||||
];
|
||||
|
||||
return array_merge($defaults, $cfg);
|
||||
}
|
||||
|
||||
/**
|
||||
* 按档位过滤可写字段,防止跨档字段污染配置。
|
||||
*
|
||||
* @param array<string,mixed> $config
|
||||
* @return array<string,mixed>
|
||||
*/
|
||||
private static function sanitizeConfigByType(string $type, array $config): array
|
||||
{
|
||||
if (in_array($type, ['deep', 'deep_personal', 'deep_enterprise'], true)) {
|
||||
return $config;
|
||||
}
|
||||
|
||||
$allowMap = [
|
||||
'personal' => ['face', 'mbti', 'disc', 'pdp', 'sbti'],
|
||||
'enterprise' => ['face', 'mbti', 'disc', 'pdp', 'sbti', 'minRecharge'],
|
||||
'gaokao' => ['face', 'mbti', 'disc', 'pdp', 'sbti', 'gaokao'],
|
||||
];
|
||||
$allow = $allowMap[$type] ?? [];
|
||||
$out = [];
|
||||
foreach ($allow as $key) {
|
||||
if (!array_key_exists($key, $config)) {
|
||||
continue;
|
||||
}
|
||||
$val = $config[$key];
|
||||
if (is_numeric($val)) {
|
||||
$out[$key] = (float) $val;
|
||||
} else {
|
||||
$out[$key] = 0;
|
||||
}
|
||||
}
|
||||
|
||||
return $out;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user