fix: 审核模式 runtime 同步、邀请码与小端 tabBar/个人中心及管理端用户页

- api: AppConfig runtime 返回 reviewMode;Invite/Settings/AppUser 等调整
- 微信小程序与抖音:app 与 tabBar、enterprise/camera/index/upload/profile 对齐审核与邀请逻辑
- 管理端:Dashboard、Settings、Users、UserDetailDialog 等更新

Made-with: Cursor
This commit is contained in:
Ghost
2026-03-28 17:46:36 +08:00
parent 3317c84b5e
commit 6584417ed8
31 changed files with 2058 additions and 2008 deletions

View File

@@ -78,28 +78,9 @@ class AppUser extends BaseController
$baseQuery->where($where);
}
// 默认不展示「从未有过测试记录」的用户(企业后台按本企业 test_results 判定);?includeZeroTests=1 显示全部
$includeZeroTests = Request::param('includeZeroTests', '');
$showUntested = ($includeZeroTests === '1' || $includeZeroTests === 'true' || $includeZeroTests === true);
if (!$showUntested) {
$trQ = Db::name('test_results');
if ($enterpriseId) {
$trQ->where('enterpriseId', (int) $enterpriseId);
}
$testedUserIds = $trQ->distinct(true)->column('userId');
$testedUserIds = array_values(array_unique(array_filter(array_map('intval', $testedUserIds))));
if (empty($testedUserIds)) {
return paginate_response([], 0, $page, $pageSize);
}
$baseQuery->whereIn('id', $testedUserIds);
}
$total = (int) $baseQuery->count();
$list = (clone $baseQuery)
->field([
'id', 'nickname', 'openid', 'avatar', 'phone', 'gender',
'country', 'province', 'city', 'status', 'lastLoginAt', 'createdAt',
])
->field('id,nickname,openid,avatar,phone,gender,country,province,city,status,lastLoginAt,createdAt')
->order('createdAt', 'desc')
->page($page, $pageSize)
->select()
@@ -174,9 +155,6 @@ class AppUser extends BaseController
foreach ($list as &$row) {
$id = $row['id'];
$av = $row['avatar'] ?? '';
$row['avatar'] = is_scalar($av) ? trim((string) $av) : '';
$row['avatarUrl'] = $row['avatar'];
$testsForUser = $testTypes[$id] ?? [];
$row['username'] = $row['nickname'] ?? ('用户' . $id);
$row['testCount'] = (int) ($testCounts[$id] ?? 0);
@@ -194,7 +172,6 @@ class AppUser extends BaseController
$row['paidOrders'] = $pay ? (int) ($pay['paidOrders'] ?? 0) : 0;
$row['totalPaidAmount'] = $pay ? (int) ($pay['totalPaidAmount'] ?? 0) : 0;
}
unset($row);
return paginate_response($list, $total, $page, $pageSize);
}

View File

@@ -1,86 +1,64 @@
<?php
namespace app\controller\admin;
use app\BaseController;
use app\common\service\WechatService;
use think\facade\Db;
/**
* 管理端 - 小程序邀请二维码(企业版 + 个人版
*/
class Invite extends BaseController
{
/**
* 一次生成两枚小程序码:企业测评入口 + 个人版首页入口
* GET /api/v1/admin/invite/qrcode
* 可选:?enterpriseId=1 仅普通管理员指定企业时传;企业管理员用自身 enterpriseId
*
* 返回 enterprise / personal 各含 data:image/png;base64,... ;兼容旧字段 qrcode=企业版图
*/
public function qrcode()
{
$admin = $this->request->user ?? null;
if (!$admin || !in_array($admin['role'] ?? '', ['admin', 'enterprise_admin'])) {
return error('无权限访问', 403);
}
$enterpriseId = null;
if (($admin['role'] ?? '') === 'enterprise_admin') {
$row = Db::name('users')->where('id', (int) ($admin['userId'] ?? 0))->find();
$enterpriseId = isset($row['enterpriseId']) ? (int) $row['enterpriseId'] : null;
} else {
$enterpriseId = (int) $this->request->param('enterpriseId', 0);
if ($enterpriseId <= 0) {
return error('请指定企业(企业管理员无需传参,使用所属企业)', 400);
}
}
if ($enterpriseId <= 0) {
return error('无法确定企业,仅企业管理员或指定 enterpriseId 可生成邀请码', 400);
}
$sceneEnterprise = 'e_' . $enterpriseId;
$pageEnterprise = 'pages/enterprise/index';
$resultEnt = WechatService::getWxacodeUnlimited($sceneEnterprise, $pageEnterprise, 430);
if (isset($resultEnt['errcode'])) {
return error('企业版小程序码失败:' . ($resultEnt['errmsg'] ?? ''), 500);
}
$binEnt = $resultEnt['binary'] ?? '';
if ($binEnt === '') {
return error('企业版小程序码生成失败', 500);
}
$b64Ent = 'data:image/png;base64,' . base64_encode($binEnt);
// 个人版首页scene 使用短串 pindex 无 eid 则留在个人版流程)
$scenePersonal = 'p';
$pagePersonal = 'pages/index/index';
$resultPer = WechatService::getWxacodeUnlimited($scenePersonal, $pagePersonal, 430);
if (isset($resultPer['errcode'])) {
return error('个人版小程序码失败:' . ($resultPer['errmsg'] ?? ''), 500);
}
$binPer = $resultPer['binary'] ?? '';
if ($binPer === '') {
return error('个人版小程序码生成失败', 500);
}
$b64Per = 'data:image/png;base64,' . base64_encode($binPer);
return success([
'qrcode' => $b64Ent,
'scene' => $sceneEnterprise,
'page' => $pageEnterprise,
'enterprise' => [
'qrcode' => $b64Ent,
'scene' => $sceneEnterprise,
'page' => $pageEnterprise,
'label' => '企业版',
],
'personal' => [
'qrcode' => $b64Per,
'scene' => $scenePersonal,
'page' => $pagePersonal,
'label' => '个人版',
],
]);
}
}
<?php
namespace app\controller\admin;
use app\BaseController;
use app\common\service\WechatService;
use think\facade\Db;
/**
* 管理端 - 小程序邀请二维码(企业参数
*/
class Invite extends BaseController
{
/**
* 生成专属邀请小程序码scene 带企业 ID扫码进入 pages/enterprise/index 可解析
* GET /api/v1/admin/invite/qrcode
* 可选:?enterpriseId=1 仅普通管理员指定企业时传;企业管理员用自身 enterpriseId
*
* 返回 data:image/png;base64,... 形式的图片地址
*/
public function qrcode()
{
$admin = $this->request->user ?? null;
if (!$admin || !in_array($admin['role'] ?? '', ['admin', 'enterprise_admin'])) {
return error('无权限访问', 403);
}
$enterpriseId = null;
if (($admin['role'] ?? '') === 'enterprise_admin') {
$row = Db::name('users')->where('id', (int) ($admin['userId'] ?? 0))->find();
$enterpriseId = isset($row['enterpriseId']) ? (int) $row['enterpriseId'] : null;
} else {
$enterpriseId = (int) $this->request->param('enterpriseId', 0);
if ($enterpriseId <= 0) {
return error('请指定企业(企业管理员无需传参,使用所属企业)', 400);
}
}
if ($enterpriseId <= 0) {
return error('无法确定企业,仅企业管理员或指定 enterpriseId 可生成邀请码', 400);
}
// 场景值e_企业ID小程序 onLoad(options.scene) 可解析
$scene = 'e_' . $enterpriseId;
$page = 'pages/enterprise/index';
$result = WechatService::getWxacodeUnlimited($scene, $page, 430);
if (isset($result['errcode'])) {
return error('获取小程序码失败:' . ($result['errmsg'] ?? ''), 500);
}
$binary = $result['binary'] ?? '';
if ($binary === '') {
return error('小程序码生成失败', 500);
}
$base64 = 'data:image/png;base64,' . base64_encode($binary);
return success([
'qrcode' => $base64,
'scene' => $scene,
'page' => $page,
]);
}
}

View File

@@ -158,6 +158,9 @@ class AppConfig extends BaseController
}
}
// 与 maintenanceMode 同源:小程序旧逻辑读 reviewMode
$reviewMode = $maintenanceMode;
return success([
'pricingType' => $pricingType,
'pricing' => $pricing,
@@ -169,6 +172,7 @@ class AppConfig extends BaseController
'siteTitle' => $siteTitle,
'textConfig' => $textConfig,
'maintenanceMode' => $maintenanceMode,
'reviewMode' => $reviewMode,
'defaultEnterpriseId' => $defaultEnterpriseId,
]);
}

View File

@@ -167,7 +167,7 @@ class AppUser extends BaseController
/**
* 测试用户列表分页、关键词、池筛选、MBTI 筛选
* GET /api/v1/superadmin/app-users?page=1&pageSize=20&keyword=&pool=all|individual|enterprise&enterpriseId=&mbti=&includeZeroTests=
* GET /api/v1/superadmin/app-users?page=1&pageSize=20&keyword=&pool=all|individual|enterprise&enterpriseId=&mbti=
*/
public function index()
{
@@ -215,18 +215,6 @@ class AppUser extends BaseController
$baseQuery->where('enterpriseId', (int) $enterpriseId);
}
// 默认不展示「从未有过测试记录」的用户;?includeZeroTests=1 可显示全部(排查用)
$includeZeroTests = Request::param('includeZeroTests', '');
$showUntested = ($includeZeroTests === '1' || $includeZeroTests === 'true' || $includeZeroTests === true);
if (!$showUntested) {
$testedUserIds = Db::name('test_results')->distinct(true)->column('userId');
$testedUserIds = array_values(array_unique(array_filter(array_map('intval', $testedUserIds))));
if (empty($testedUserIds)) {
return paginate_response([], 0, $page, $pageSize);
}
$baseQuery->whereIn('id', $testedUserIds);
}
// MBTI 筛选:保留旧逻辑,从 test_results 取有 mbti 测试的用户
if ($mbti !== '') {
$mbtiUserIds = Db::name('test_results')->where('testType', 'mbti')->distinct(true)->column('userId');
@@ -240,10 +228,7 @@ class AppUser extends BaseController
$total = $baseQuery->count();
$list = (clone $baseQuery)
->field([
'id', 'openid', 'nickname', 'avatar', 'phone', 'gender',
'country', 'province', 'city', 'status', 'lastLoginAt', 'createdAt',
])
->field('id,openid,nickname,avatar,phone,gender,country,province,city,status,lastLoginAt,createdAt')
->order('createdAt', 'desc')
->page($page, $pageSize)
->select()
@@ -319,9 +304,6 @@ class AppUser extends BaseController
foreach ($list as &$row) {
$id = $row['id'];
$av = $row['avatar'] ?? '';
$row['avatar'] = is_scalar($av) ? trim((string) $av) : '';
$row['avatarUrl'] = $row['avatar'];
$testsForUser = $testTypes[$id] ?? [];
$row['username'] = $row['nickname'] ?? ('用户' . $id);
$row['testCount'] = (int) ($testCounts[$id] ?? 0);
@@ -342,7 +324,6 @@ class AppUser extends BaseController
$row['totalPaidAmount'] = $totalPaidFen;
$row['totalPaidAmountYuan'] = $totalPaidFen > 0 ? round($totalPaidFen / 100, 2) : 0;
}
unset($row);
return paginate_response($list, $total, $page, $pageSize);
}

View File

@@ -51,17 +51,42 @@ class Settings extends BaseController
}
}
$systemDefault = [
'siteName' => '神仙团队AI性格测试',
'siteDescription' => '专业的AI性格测试平台',
'miniprogramName' => '神仙团队AI性格测试',
'maintenanceMode' => false,
'maxTestsPerDay' => 100,
'trialTestCount' => 10,
'defaultEnterpriseId' => null,
];
$systemOut = $systemDefault;
if ($systemConfig && !empty($systemConfig->value)) {
$raw = $systemConfig->value;
$decoded = is_string($raw) ? json_decode($raw, true) : $raw;
if (is_array($decoded)) {
$systemOut = array_merge($systemDefault, $decoded);
}
}
// 审核模式唯一口径system.maintenanceMode布尔。兼容旧版 review_mode.enabled
$maint = false;
if (isset($systemOut['maintenanceMode'])) {
$maint = (bool) $systemOut['maintenanceMode'];
}
if (!$maint && $reviewModeConfig && !empty($reviewModeConfig->value)) {
$rv = $reviewModeConfig->value;
if (is_string($rv)) {
$rv = json_decode($rv, true);
}
if (is_array($rv) && !empty($rv['enabled'])) {
$maint = true;
}
}
$systemOut['maintenanceMode'] = $maint;
return success([
'system' => $systemConfig ? $systemConfig->value : [
'siteName' => '神仙团队AI性格测试',
'siteDescription' => '专业的AI性格测试平台',
'miniprogramName' => '神仙团队AI性格测试',
'maintenanceMode' => false,
'maxTestsPerDay' => 100,
'trialTestCount' => 10,
'defaultEnterpriseId' => null,
],
'reviewMode' => $reviewModeConfig && !empty($reviewModeConfig->value) ? $reviewModeConfig->value : ['enabled' => false],
'system' => $systemOut,
'reviewMode' => ['enabled' => $maint],
'notification' => $notificationConfig ? $notificationConfig->value : [
'emailNotification' => true,
'lowBalanceAlert' => true,
@@ -111,6 +136,9 @@ class Settings extends BaseController
if (empty($data)) {
$data = Request::only($allowedKeys);
}
if (array_key_exists('maintenanceMode', $data)) {
$data['maintenanceMode'] = filter_var($data['maintenanceMode'], FILTER_VALIDATE_BOOLEAN);
}
// 默认企业0 或空视为不启用
if (array_key_exists('defaultEnterpriseId', $data)) {
$de = $data['defaultEnterpriseId'];
@@ -452,9 +480,8 @@ class Settings extends BaseController
}
/**
* 更新审核模式配置
* PUT body: { "enabled": true/false }
* 开启后小程序隐藏AI面相分析功能仅展示问卷测试用于通过微信审核
* 更新审核模式:仅写入 system 顶层 maintenanceModetrue/false与 runtime 一致。
* PUT body: { "enabled": true } 或 { "maintenanceMode": true }(二者等价,任一即可)
*/
public function updateReviewMode()
{
@@ -467,19 +494,34 @@ class Settings extends BaseController
if (!is_array($input)) {
$input = Request::param();
}
$enabled = !empty($input['enabled']);
$on = false;
if (array_key_exists('maintenanceMode', $input)) {
$on = !empty($input['maintenanceMode']);
} elseif (array_key_exists('enabled', $input)) {
$on = !empty($input['enabled']);
}
try {
$config = SystemConfigModel::where('key', 'review_mode')->where('enterprise_id', 0)->find();
$config = SystemConfigModel::where('key', 'system')->where('enterprise_id', 0)->find();
if (!$config) {
$config = new SystemConfigModel();
$config->key = 'review_mode';
$config->key = 'system';
$config->enterprise_id = 0;
$config->description = '审核模式开启后隐藏AI功能以通过微信审核';
$config->description = '系统基础配置';
}
$config->value = ['enabled' => $enabled];
$oldVal = $config->value;
$oldArr = is_array($oldVal) ? $oldVal : (is_string($oldVal) ? (json_decode($oldVal, true) ?: []) : []);
if (!is_array($oldArr)) {
$oldArr = [];
}
$oldArr['maintenanceMode'] = (bool) $on;
$config->value = $oldArr;
$config->save();
return success($config->value, '审核模式已' . ($enabled ? '开启' : '关闭'));
return success([
'maintenanceMode' => (bool) $on,
'enabled' => (bool) $on,
], '审核模式已' . ($on ? '开启' : '关闭'));
} catch (\Exception $e) {
return error('保存失败:' . $e->getMessage(), 500);
}