高考版优化

审核模式优化
This commit is contained in:
Ghost
2026-04-29 18:01:26 +08:00
parent 59419c4ce9
commit a411bf7b21
134 changed files with 19898 additions and 3371 deletions

View File

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

View File

@@ -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, '归并完成');
}
}

View File

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

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

View File

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