feat: 管理端聚合页、小程序/抖音埋点与统计、飞书线索 webhook、API 迁移与路由

- admin:OrdersHub/UsersHub、Commerce/Ops/Enterprise Hub、MpAnalytics、Feishu/小程序配置面板、鉴权存储
- api:Analytics、DataMigration、FeishuLeadWebhook、mp 事件迁移 SQL
- 微信/抖音小程序:analytics 上报与相关页面调整
- 开发文档与 scripts 补充

Made-with: Cursor
This commit is contained in:
卡若
2026-03-27 17:18:25 +08:00
parent 9601b6955b
commit aca2e263bb
188 changed files with 41937 additions and 27625 deletions

View File

@@ -1,358 +1,261 @@
<?php
namespace app\controller\admin;
use app\BaseController;
use think\facade\Db;
use think\facade\Request;
/**
* 测试用户(小程序用户)管理 - 只读列表与详情
* 数据来源wechat_users测试记录来自 test_resultsuserId 关联 wechat_users.id
*/
class AppUser extends BaseController
{
/**
* 测试用户列表:分页、关键词搜索
* GET /api/v1/admin/app-users?page=1&pageSize=20&keyword=
*/
public function index()
{
$user = $this->request->user ?? null;
if (!$user) {
return error('未登录', 401);
}
if (!in_array($user['role'] ?? '', ['admin', 'enterprise_admin'])) {
return error('无权限访问', 403);
}
$page = (int) Request::param('page', 1);
$pageSize = (int) Request::param('pageSize', 20);
$pageSize = min(max($pageSize, 1), 100);
$keyword = trim(Request::param('keyword', ''));
$where = [];
if ($keyword !== '') {
$where[] = ['nickname|phone|city|province', 'like', '%' . $keyword . '%'];
}
// admin / enterprise_admin 均只能看本企业数据
$enterpriseId = $user['enterpriseId'] ?? null;
if (!$enterpriseId) {
// JWT 未含 enterpriseId 时回退查库(兼容旧 token
$adminRow = Db::name('users')->where('id', $user['userId'] ?? 0)->find();
$enterpriseId = $adminRow['enterpriseId'] ?? null;
}
// 若有企业ID先从 user_profile 中取出属于本企业的 userId 列表(以画像为主表)
$profileUserIds = [];
if ($enterpriseId) {
$profileUserIds = Db::name('user_profile')
->where('enterpriseId', $enterpriseId)
->column('userId');
$profileUserIds = $profileUserIds ? array_values(array_unique(array_filter($profileUserIds))) : [];
if (empty($profileUserIds)) {
return paginate_response([], 0, $page, $pageSize);
}
}
// 按 openid 去重:每个 openid 只保留 id 最大的一条;失败则不去重
try {
$dedupIds = Db::name('wechat_users')->field('openid, MAX(id) as mid')->group('openid')->column('mid');
$dedupIds = $dedupIds ? array_values(array_filter($dedupIds)) : [];
} catch (\Throwable $e) {
$dedupIds = Db::name('wechat_users')->column('id');
$dedupIds = $dedupIds ? array_values(array_filter($dedupIds)) : [];
}
if (empty($dedupIds)) {
return paginate_response([], 0, $page, $pageSize);
}
$baseQuery = Db::name('wechat_users')->where('id', 'in', $dedupIds);
// 若从画像表中筛出了当前企业的用户池,则仅保留这些 userId
if (!empty($profileUserIds)) {
$baseQuery->whereIn('id', $profileUserIds);
}
if ($where) {
$baseQuery->where($where);
}
$total = (int) $baseQuery->count();
$list = (clone $baseQuery)
->field('id,nickname,openid,avatar,phone,gender,country,province,city,status,lastLoginAt,createdAt')
->order('createdAt', 'desc')
->page($page, $pageSize)
->select()
->toArray();
// 为每条用户附加测试统计test_results.userId 对应 wechat_users.id
$ids = array_column($list, 'id');
$testCounts = [];
$lastTestAt = [];
$testTypes = []; // 每个用户最新几条测试类型,用于展示 MBTI/PDP/DISC
$payStats = [];
$enterpriseName = null;
if ($enterpriseId) {
$ent = Db::name('enterprises')->where('id', $enterpriseId)->find();
$enterpriseName = $ent['name'] ?? ('企业' . $enterpriseId);
}
if (!empty($ids)) {
// 测试统计严格按 test_results.enterpriseId 归属企业过滤
$trBase = Db::name('test_results')->where('userId', 'in', $ids);
if ($enterpriseId) {
$trBase->where('enterpriseId', $enterpriseId);
}
$counts = (clone $trBase)
->group('userId')
->column('COUNT(*) as cnt', 'userId');
$testCounts = $counts ?: [];
$lastRows = (clone $trBase)
->field('id, userId, testType, resultData, createdAt, enterpriseId as testEnterpriseId')
->order('createdAt', 'desc')
->select();
foreach ($lastRows as $row) {
$uid = $row['userId'];
if (!isset($lastTestAt[$uid])) {
$lastTestAt[$uid] = $row['createdAt'];
}
if (!isset($testTypes[$uid])) {
$testTypes[$uid] = [];
}
$testTypes[$uid][] = [
'testType' => $row['testType'],
'result' => is_string($row['resultData'] ?? '') ? ($row['resultData'] ?? '') : json_encode($row['resultData'] ?? '', JSON_UNESCAPED_UNICODE),
'createdAt' => $row['createdAt'],
'testScope' => !empty($row['testEnterpriseId']) ? 'enterprise' : 'personal',
];
}
// 付款统计user_profile按当前企业过滤
try {
$profilesQuery = Db::name('user_profile')
->where('userId', 'in', $ids);
if ($enterpriseId) {
$profilesQuery->where('enterpriseId', $enterpriseId);
}
$profiles = $profilesQuery
->field('userId, SUM(paidOrders) AS paidOrders, SUM(totalPaidAmount) AS totalPaidAmount')
->group('userId')
->select()
->toArray();
foreach ($profiles as $p) {
$uid = (int) ($p['userId'] ?? 0);
if ($uid > 0) {
$payStats[$uid] = [
'paidOrders' => (int) ($p['paidOrders'] ?? 0),
'totalPaidAmount' => (int) ($p['totalPaidAmount'] ?? 0),
];
}
}
} catch (\Throwable $e) {
$payStats = [];
}
}
foreach ($list as &$row) {
$id = $row['id'];
$testsForUser = $testTypes[$id] ?? [];
$row['username'] = $row['nickname'] ?? ('用户' . $id);
$row['testCount'] = (int) ($testCounts[$id] ?? 0);
$row['lastTestAt'] = $lastTestAt[$id] ?? null;
$row['tests'] = $testsForUser;
$row['mbtiType'] = $this->extractResultType($testsForUser, 'mbti');
$row['pdpType'] = $this->extractResultType($testsForUser, 'pdp');
$row['discType'] = $this->extractResultType($testsForUser, 'disc');
$row['faceType'] = $this->extractResultType($testsForUser, 'face');
$row['faceMbtiType'] = $this->extractFaceSubType($testsForUser, 'mbti');
$row['faceDiscType'] = $this->extractFaceSubType($testsForUser, 'disc');
$row['facePdpType'] = $this->extractFaceSubType($testsForUser, 'pdp');
$row['enterprise'] = $enterpriseName !== null ? $enterpriseName : '全部';
$pay = $payStats[$id] ?? null;
$row['paidOrders'] = $pay ? (int) ($pay['paidOrders'] ?? 0) : 0;
$row['totalPaidAmount'] = $pay ? (int) ($pay['totalPaidAmount'] ?? 0) : 0;
}
return paginate_response($list, $total, $page, $pageSize);
}
/**
* 测试用户详情:基本信息 + 测试记录列表
* GET /api/v1/admin/app-users/:id
*/
public function detail($id)
{
$user = $this->request->user ?? null;
if (!$user) {
return error('未登录', 401);
}
if (!in_array($user['role'] ?? '', ['admin', 'enterprise_admin'])) {
return error('无权限访问', 403);
}
// admin / enterprise_admin 均只能查看本企业的用户
$enterpriseId = $user['enterpriseId'] ?? null;
if (!$enterpriseId) {
$adminRow = Db::name('users')->where('id', $user['userId'] ?? 0)->find();
$enterpriseId = $adminRow['enterpriseId'] ?? null;
}
if ($enterpriseId) {
// 使用 user_profile 判断该用户是否属于当前企业(以画像为主表)
$has = Db::name('user_profile')
->where('userId', $id)
->where('enterpriseId', $enterpriseId)
->find();
if (!$has) {
return error('无权限查看该用户', 403);
}
}
$row = Db::name('wechat_users')->where('id', $id)->find();
if (!$row) {
return error('用户不存在', 404);
}
$data = [
'id' => (int) $row['id'],
'username' => $row['nickname'] ?? ('用户' . $row['id']),
'nickname' => $row['nickname'] ?? '',
'avatar' => $row['avatar'] ?? '',
'phone' => $row['phone'] ?? '',
'email' => '',
'gender' => (int) ($row['gender'] ?? 0),
'country' => $row['country'] ?? '',
'province' => $row['province'] ?? '',
'city' => $row['city'] ?? '',
'status' => (int) ($row['status'] ?? 1),
'lastLoginAt' => isset($row['lastLoginAt']) ? (int) $row['lastLoginAt'] : null,
'createdAt' => isset($row['createdAt']) ? (int) $row['createdAt'] : null,
'updatedAt' => isset($row['updatedAt']) ? (int) $row['updatedAt'] : null,
];
// 测试列表:严格按 test_results.enterpriseId 归属本企业过滤
$testQuery = Db::name('test_results')->where('userId', $id);
if ($enterpriseId) {
$testQuery->where('enterpriseId', $enterpriseId);
}
$tests = $testQuery
->field('id, testType, resultData, enterpriseId as testEnterpriseId, createdAt, requiresPayment, isPaid, paidAmount, paidAt, orderId')
->order('createdAt', 'desc')
->select()
->toArray();
foreach ($tests as &$t) {
$raw = $t['resultData'] ?? '';
$t['result'] = is_string($raw) ? $raw : json_encode($raw, JSON_UNESCAPED_UNICODE);
$t['testScope'] = !empty($t['testEnterpriseId']) ? 'enterprise' : 'personal';
unset($t['testEnterpriseId']);
}
$data['testCount'] = count($tests);
$data['testList'] = $tests;
$data['mbtiType'] = $this->extractResultType($tests, 'mbti');
$data['pdpType'] = $this->extractResultType($tests, 'pdp');
$data['discType'] = $this->extractResultType($tests, 'disc');
$data['faceType'] = $this->extractResultType($tests, 'face');
$data['faceMbtiType'] = $this->extractFaceSubType($tests, 'mbti');
$data['faceDiscType'] = $this->extractFaceSubType($tests, 'disc');
$data['facePdpType'] = $this->extractFaceSubType($tests, 'pdp');
return success($data);
}
/**
* 从测试记录中取出某类型的最近结果result 可能是 JSON 字符串,取 type 或 result 字段)
*/
private function extractResultType(array $tests, string $type): string
{
$targetType = strtolower($type);
foreach ($tests as $t) {
if (strtolower($t['testType'] ?? '') !== $targetType) {
continue;
}
$result = $t['result'] ?? '';
if (!is_string($result)) {
continue;
}
$dec = json_decode($result, true);
if (!is_array($dec)) {
return $targetType === 'face' ? '人脸分析' : trim($result);
}
if ($targetType === 'face') {
return '人脸分析';
}
if ($targetType === 'mbti') {
return (string) ($dec['mbtiType'] ?? $dec['type'] ?? $dec['result'] ?? '');
}
if ($targetType === 'disc') {
$desc = $dec['description']['type'] ?? null;
if (is_string($desc) && $desc !== '') {
return $desc;
}
if (!empty($dec['dominantType'])) {
return (string) $dec['dominantType'];
}
return (string) ($dec['disc'] ?? '');
}
if ($targetType === 'pdp') {
$desc = $dec['description']['type'] ?? null;
if (is_string($desc) && $desc !== '') {
return $desc;
}
if (!empty($dec['dominantType'])) {
return (string) $dec['dominantType'];
}
return (string) ($dec['pdp'] ?? '');
}
return (string) ($dec['type'] ?? $dec['result'] ?? '');
}
return '';
}
/**
* 从人脸分析结果中提取对应的 MBTI / DISC / PDP 文本
*/
private function extractFaceSubType(array $tests, string $subType): string
{
$target = strtolower($subType);
foreach ($tests as $t) {
if (strtolower($t['testType'] ?? '') !== 'face') {
continue;
}
$result = $t['result'] ?? '';
if (!is_string($result)) {
continue;
}
$dec = json_decode($result, true);
if (!is_array($dec)) {
continue;
}
if ($target === 'mbti') {
if (!empty($dec['mbti']['type'])) {
return (string) $dec['mbti']['type'];
}
if (!empty($dec['mbtiType'])) {
return (string) $dec['mbtiType'];
}
} elseif ($target === 'disc') {
if (!empty($dec['disc']['primary'])) {
return (string) $dec['disc']['primary'];
}
if (!empty($dec['disc'])) {
return (string) $dec['disc'];
}
} elseif ($target === 'pdp') {
if (!empty($dec['pdp']['primary'])) {
return (string) $dec['pdp']['primary'];
}
if (!empty($dec['pdp'])) {
return (string) $dec['pdp'];
}
}
}
return '';
}
}
<?php
namespace app\controller\admin;
use app\BaseController;
use app\controller\admin\concern\ExtractsTestResults;
use think\facade\Db;
use think\facade\Request;
/**
* 测试用户(小程序用户)管理 - 只读列表与详情
* 数据来源wechat_users测试记录来自 test_resultsuserId 关联 wechat_users.id
*/
class AppUser extends BaseController
{
use ExtractsTestResults;
/**
* 测试用户列表:分页、关键词搜索
* GET /api/v1/admin/app-users?page=1&pageSize=20&keyword=
*/
public function index()
{
$user = $this->request->user ?? null;
if (!$user) {
return error('未登录', 401);
}
if (!in_array($user['role'] ?? '', ['admin', 'enterprise_admin'])) {
return error('无权限访问', 403);
}
$page = (int) Request::param('page', 1);
$pageSize = (int) Request::param('pageSize', 20);
$pageSize = min(max($pageSize, 1), 100);
$keyword = trim(Request::param('keyword', ''));
$where = [];
if ($keyword !== '') {
$where[] = ['nickname|phone|city|province', 'like', '%' . $keyword . '%'];
}
// admin / enterprise_admin 均只能看本企业数据
$enterpriseId = $user['enterpriseId'] ?? null;
if (!$enterpriseId) {
// JWT 未含 enterpriseId 时回退查库(兼容旧 token
$adminRow = Db::name('users')->where('id', $user['userId'] ?? 0)->find();
$enterpriseId = $adminRow['enterpriseId'] ?? null;
}
// 若有企业ID先从 user_profile 中取出属于本企业的 userId 列表(以画像为主表)
$profileUserIds = [];
if ($enterpriseId) {
$profileUserIds = Db::name('user_profile')
->where('enterpriseId', $enterpriseId)
->column('userId');
$profileUserIds = $profileUserIds ? array_values(array_unique(array_filter($profileUserIds))) : [];
if (empty($profileUserIds)) {
return paginate_response([], 0, $page, $pageSize);
}
}
// 按 openid 去重:每个 openid 只保留 id 最大的一条;失败则不去重
try {
$dedupIds = Db::name('wechat_users')->field('openid, MAX(id) as mid')->group('openid')->column('mid');
$dedupIds = $dedupIds ? array_values(array_filter($dedupIds)) : [];
} catch (\Throwable $e) {
$dedupIds = Db::name('wechat_users')->column('id');
$dedupIds = $dedupIds ? array_values(array_filter($dedupIds)) : [];
}
if (empty($dedupIds)) {
return paginate_response([], 0, $page, $pageSize);
}
$baseQuery = Db::name('wechat_users')->where('id', 'in', $dedupIds);
// 若从画像表中筛出了当前企业的用户池,则仅保留这些 userId
if (!empty($profileUserIds)) {
$baseQuery->whereIn('id', $profileUserIds);
}
if ($where) {
$baseQuery->where($where);
}
$total = (int) $baseQuery->count();
$list = (clone $baseQuery)
->field('id,nickname,openid,avatar,phone,gender,country,province,city,status,lastLoginAt,createdAt')
->order('createdAt', 'desc')
->page($page, $pageSize)
->select()
->toArray();
// 为每条用户附加测试统计test_results.userId 对应 wechat_users.id
$ids = array_column($list, 'id');
$testCounts = [];
$lastTestAt = [];
$testTypes = []; // 每个用户最新几条测试类型,用于展示 MBTI/PDP/DISC
$payStats = [];
$enterpriseName = null;
if ($enterpriseId) {
$ent = Db::name('enterprises')->where('id', $enterpriseId)->find();
$enterpriseName = $ent['name'] ?? ('企业' . $enterpriseId);
}
if (!empty($ids)) {
// 测试统计严格按 test_results.enterpriseId 归属企业过滤
$trBase = Db::name('test_results')->where('userId', 'in', $ids);
if ($enterpriseId) {
$trBase->where('enterpriseId', $enterpriseId);
}
$counts = (clone $trBase)
->group('userId')
->column('COUNT(*) as cnt', 'userId');
$testCounts = $counts ?: [];
$lastRows = (clone $trBase)
->field('id, userId, testType, resultData, createdAt, enterpriseId as testEnterpriseId')
->order('createdAt', 'desc')
->select();
foreach ($lastRows as $row) {
$uid = $row['userId'];
if (!isset($lastTestAt[$uid])) {
$lastTestAt[$uid] = $row['createdAt'];
}
if (!isset($testTypes[$uid])) {
$testTypes[$uid] = [];
}
$testTypes[$uid][] = [
'testType' => $row['testType'],
'result' => is_string($row['resultData'] ?? '') ? ($row['resultData'] ?? '') : json_encode($row['resultData'] ?? '', JSON_UNESCAPED_UNICODE),
'createdAt' => $row['createdAt'],
'testScope' => !empty($row['testEnterpriseId']) ? 'enterprise' : 'personal',
];
}
// 付款统计:user_profile(按当前企业过滤)
try {
$profilesQuery = Db::name('user_profile')
->where('userId', 'in', $ids);
if ($enterpriseId) {
$profilesQuery->where('enterpriseId', $enterpriseId);
}
$profiles = $profilesQuery
->field('userId, SUM(paidOrders) AS paidOrders, SUM(totalPaidAmount) AS totalPaidAmount')
->group('userId')
->select()
->toArray();
foreach ($profiles as $p) {
$uid = (int) ($p['userId'] ?? 0);
if ($uid > 0) {
$payStats[$uid] = [
'paidOrders' => (int) ($p['paidOrders'] ?? 0),
'totalPaidAmount' => (int) ($p['totalPaidAmount'] ?? 0),
];
}
}
} catch (\Throwable $e) {
$payStats = [];
}
}
foreach ($list as &$row) {
$id = $row['id'];
$testsForUser = $testTypes[$id] ?? [];
$row['username'] = $row['nickname'] ?? ('用户' . $id);
$row['testCount'] = (int) ($testCounts[$id] ?? 0);
$row['lastTestAt'] = $lastTestAt[$id] ?? null;
$row['tests'] = $testsForUser;
$row['mbtiType'] = $this->extractResultType($testsForUser, 'mbti');
$row['pdpType'] = $this->extractResultType($testsForUser, 'pdp');
$row['discType'] = $this->extractResultType($testsForUser, 'disc');
$row['faceType'] = $this->extractResultType($testsForUser, 'face');
$row['faceMbtiType'] = $this->extractFaceSubType($testsForUser, 'mbti');
$row['faceDiscType'] = $this->extractFaceSubType($testsForUser, 'disc');
$row['facePdpType'] = $this->extractFaceSubType($testsForUser, 'pdp');
$row['enterprise'] = $enterpriseName !== null ? $enterpriseName : '全部';
$pay = $payStats[$id] ?? null;
$row['paidOrders'] = $pay ? (int) ($pay['paidOrders'] ?? 0) : 0;
$row['totalPaidAmount'] = $pay ? (int) ($pay['totalPaidAmount'] ?? 0) : 0;
}
return paginate_response($list, $total, $page, $pageSize);
}
/**
* 测试用户详情:基本信息 + 测试记录列表
* GET /api/v1/admin/app-users/:id
*/
public function detail($id)
{
$user = $this->request->user ?? null;
if (!$user) {
return error('未登录', 401);
}
if (!in_array($user['role'] ?? '', ['admin', 'enterprise_admin'])) {
return error('无权限访问', 403);
}
// admin / enterprise_admin 均只能查看本企业的用户
$enterpriseId = $user['enterpriseId'] ?? null;
if (!$enterpriseId) {
$adminRow = Db::name('users')->where('id', $user['userId'] ?? 0)->find();
$enterpriseId = $adminRow['enterpriseId'] ?? null;
}
if ($enterpriseId) {
// 使用 user_profile 判断该用户是否属于当前企业(以画像为主表)
$has = Db::name('user_profile')
->where('userId', $id)
->where('enterpriseId', $enterpriseId)
->find();
if (!$has) {
return error('无权限查看该用户', 403);
}
}
$row = Db::name('wechat_users')->where('id', $id)->find();
if (!$row) {
return error('用户不存在', 404);
}
$data = [
'id' => (int) $row['id'],
'username' => $row['nickname'] ?? ('用户' . $row['id']),
'nickname' => $row['nickname'] ?? '',
'avatar' => $row['avatar'] ?? '',
'phone' => $row['phone'] ?? '',
'email' => '',
'gender' => (int) ($row['gender'] ?? 0),
'country' => $row['country'] ?? '',
'province' => $row['province'] ?? '',
'city' => $row['city'] ?? '',
'status' => (int) ($row['status'] ?? 1),
'lastLoginAt' => isset($row['lastLoginAt']) ? (int) $row['lastLoginAt'] : null,
'createdAt' => isset($row['createdAt']) ? (int) $row['createdAt'] : null,
'updatedAt' => isset($row['updatedAt']) ? (int) $row['updatedAt'] : null,
];
// 测试列表:严格按 test_results.enterpriseId 归属本企业过滤
$testQuery = Db::name('test_results')->where('userId', $id);
if ($enterpriseId) {
$testQuery->where('enterpriseId', $enterpriseId);
}
$tests = $testQuery
->field('id, testType, resultData, enterpriseId as testEnterpriseId, createdAt, requiresPayment, isPaid, paidAmount, paidAt, orderId')
->order('createdAt', 'desc')
->select()
->toArray();
foreach ($tests as &$t) {
$raw = $t['resultData'] ?? '';
$t['result'] = is_string($raw) ? $raw : json_encode($raw, JSON_UNESCAPED_UNICODE);
$t['testScope'] = !empty($t['testEnterpriseId']) ? 'enterprise' : 'personal';
unset($t['testEnterpriseId']);
}
$data['testCount'] = count($tests);
$data['testList'] = $tests;
$data['mbtiType'] = $this->extractResultType($tests, 'mbti');
$data['pdpType'] = $this->extractResultType($tests, 'pdp');
$data['discType'] = $this->extractResultType($tests, 'disc');
$data['faceType'] = $this->extractResultType($tests, 'face');
$data['faceMbtiType'] = $this->extractFaceSubType($tests, 'mbti');
$data['faceDiscType'] = $this->extractFaceSubType($tests, 'disc');
$data['facePdpType'] = $this->extractFaceSubType($tests, 'pdp');
return success($data);
}
}

View File

@@ -1,179 +1,277 @@
<?php
namespace app\controller\admin;
use app\BaseController;
use think\facade\Db;
use think\facade\Request;
/**
* 数据概览控制器(普通管理员)
*/
class Dashboard extends BaseController
{
/**
* 获取统计数据
* @return \think\response\Json
*/
public function index()
{
$user = $this->request->user ?? null;
if (!$user) {
return error('未登录', 401);
}
// 验证是否为管理员
if (!in_array($user['role'], ['admin', 'enterprise_admin'])) {
return error('无权限访问', 403);
}
try {
// admin / enterprise_admin 均只统计本企业数据
$enterpriseId = $user['enterpriseId'] ?? null;
if (!$enterpriseId) {
$adminRow = Db::name('users')->where('id', $user['userId'] ?? 0)->find();
$enterpriseId = $adminRow['enterpriseId'] ?? null;
}
// 企业用户 ID 集合(用于后续统计个人版测试)
$enterpriseUserIds = [];
if ($enterpriseId) {
$enterpriseUserIds = Db::name('wechat_users')
->where('enterpriseId', $enterpriseId)
->column('id');
$enterpriseUserIds = array_values(array_filter($enterpriseUserIds));
}
// 总用户数wechat_users.enterpriseId = 本企业
if ($enterpriseId) {
$totalUsers = count($enterpriseUserIds);
} else {
try {
$totalUsers = (int) Db::name('wechat_users')->count('openid', true);
} catch (\Throwable $e) {
$totalUsers = (int) Db::name('wechat_users')->count();
}
}
// 已完成测试数:严格按 test_results.enterpriseId 归属企业统计
if ($enterpriseId) {
$testsCompleted = (int) Db::name('test_results')
->where('enterpriseId', $enterpriseId)
->count();
} else {
$testsCompleted = (int) Db::name('test_results')->count();
}
// 今日活跃用户数
$todayStart = strtotime(date('Y-m-d 00:00:00'));
$todayEnd = strtotime(date('Y-m-d 23:59:59'));
$activeQuery = Db::name('test_results')
->where('createdAt', '>=', $todayStart)
->where('createdAt', '<=', $todayEnd);
if ($enterpriseId) {
$activeQuery->where('enterpriseId', $enterpriseId);
$activeIds = $activeQuery->distinct(true)->column('userId');
$activeToday = count(array_filter($activeIds));
} else {
$activeIds = $activeQuery->distinct(true)->column('userId');
$activeToday = count(array_filter($activeIds));
}
// 待审核暂返回0
$pendingReviews = 0;
// 最近 14 天测试趋势
$days = 14;
$startDate = strtotime(date('Y-m-d 00:00:00', strtotime('-' . ($days - 1) . ' days')));
$trendQuery = Db::name('test_results')
->where('createdAt', '>=', $startDate)
->whereIn('testType', ['face', 'mbti', 'disc', 'pdp']);
if ($enterpriseId) {
$trendQuery->where('enterpriseId', $enterpriseId);
}
$trendRows = $trendQuery
->field("FROM_UNIXTIME(createdAt, '%Y-%m-%d') as d, testType, COUNT(*) as c")
->group('d,testType')
->order('d', 'asc')
->select()
->toArray();
// 组装为按日期汇总的数组
$trendMap = [];
foreach ($trendRows as $row) {
$d = $row['d'];
$type = $row['testType'];
$cnt = (int) ($row['c'] ?? 0);
if (!isset($trendMap[$d])) {
$trendMap[$d] = [
'date' => $d,
'face' => 0,
'mbti' => 0,
'disc' => 0,
'pdp' => 0,
'total' => 0,
];
}
if (in_array($type, ['face', 'mbti', 'disc', 'pdp'], true)) {
$trendMap[$d][$type] += $cnt;
$trendMap[$d]['total'] += $cnt;
}
}
// 补齐没有数据的日期
$trendData = [];
for ($i = 0; $i < $days; $i++) {
$d = date('Y-m-d', strtotime('-' . ($days - 1 - $i) . ' days'));
if (isset($trendMap[$d])) {
$trendData[] = $trendMap[$d];
} else {
$trendData[] = [
'date' => $d,
'face' => 0,
'mbti' => 0,
'disc' => 0,
'pdp' => 0,
'total' => 0,
];
}
}
return success([
'totalUsers' => $totalUsers,
'testsCompleted' => $testsCompleted,
'activeToday' => $activeToday,
'pendingReviews' => $pendingReviews,
'testTrends' => $trendData,
]);
} catch (\Exception $e) {
return error('获取统计数据失败:' . $e->getMessage(), 500);
}
}
/**
* 格式化时间
* @param int $timestamp
* @return string
*/
private function formatTime($timestamp)
{
if (!$timestamp) {
return '';
}
$now = time();
$diff = $now - $timestamp;
if ($diff < 60) {
return '刚刚';
} elseif ($diff < 3600) {
return floor($diff / 60) . '分钟前';
} elseif ($diff < 86400) {
return floor($diff / 3600) . '小时前';
} elseif ($diff < 604800) {
return floor($diff / 86400) . '天前';
} else {
return date('Y-m-d H:i', $timestamp);
}
}
}
<?php
namespace app\controller\admin;
use app\BaseController;
use app\controller\admin\concern\ExtractsTestResults;
use think\facade\Db;
use think\facade\Request;
/**
* 数据概览控制器(普通管理员)
*/
class Dashboard extends BaseController
{
use ExtractsTestResults;
/**
* 获取统计数据
* @return \think\response\Json
*/
public function index()
{
$user = $this->request->user ?? null;
if (!$user) {
return error('未登录', 401);
}
// 验证是否为管理员
if (!in_array($user['role'], ['admin', 'enterprise_admin'])) {
return error('无权限访问', 403);
}
try {
// admin / enterprise_admin 均只统计本企业数据
$enterpriseId = $user['enterpriseId'] ?? null;
if (!$enterpriseId) {
$adminRow = Db::name('users')->where('id', $user['userId'] ?? 0)->find();
$enterpriseId = $adminRow['enterpriseId'] ?? null;
}
// 企业用户 ID 集合(用于后续统计个人版测试)
$enterpriseUserIds = [];
if ($enterpriseId) {
$enterpriseUserIds = Db::name('wechat_users')
->where('enterpriseId', $enterpriseId)
->column('id');
$enterpriseUserIds = array_values(array_filter($enterpriseUserIds));
}
// 总用户数wechat_users.enterpriseId = 本企业
if ($enterpriseId) {
$totalUsers = count($enterpriseUserIds);
} else {
try {
$totalUsers = (int) Db::name('wechat_users')->count('openid', true);
} catch (\Throwable $e) {
$totalUsers = (int) Db::name('wechat_users')->count();
}
}
// 已完成测试数:严格按 test_results.enterpriseId 归属企业统计
if ($enterpriseId) {
$testsCompleted = (int) Db::name('test_results')
->where('enterpriseId', $enterpriseId)
->count();
} else {
$testsCompleted = (int) Db::name('test_results')->count();
}
// 今日活跃用户数
$todayStart = strtotime(date('Y-m-d 00:00:00'));
$todayEnd = strtotime(date('Y-m-d 23:59:59'));
$activeQuery = Db::name('test_results')
->where('createdAt', '>=', $todayStart)
->where('createdAt', '<=', $todayEnd);
if ($enterpriseId) {
$activeQuery->where('enterpriseId', $enterpriseId);
$activeIds = $activeQuery->distinct(true)->column('userId');
$activeToday = count(array_filter($activeIds));
} else {
$activeIds = $activeQuery->distinct(true)->column('userId');
$activeToday = count(array_filter($activeIds));
}
// 待审核暂返回0
$pendingReviews = 0;
// 最近 14 天测试趋势
$days = 14;
$startDate = strtotime(date('Y-m-d 00:00:00', strtotime('-' . ($days - 1) . ' days')));
$trendQuery = Db::name('test_results')
->where('createdAt', '>=', $startDate)
->whereIn('testType', ['face', 'mbti', 'disc', 'pdp']);
if ($enterpriseId) {
$trendQuery->where('enterpriseId', $enterpriseId);
}
$trendRows = $trendQuery
->field("FROM_UNIXTIME(createdAt, '%Y-%m-%d') as d, testType, COUNT(*) as c")
->group('d,testType')
->order('d', 'asc')
->select()
->toArray();
// 组装为按日期汇总的数组
$trendMap = [];
foreach ($trendRows as $row) {
$d = $row['d'];
$type = $row['testType'];
$cnt = (int) ($row['c'] ?? 0);
if (!isset($trendMap[$d])) {
$trendMap[$d] = [
'date' => $d,
'face' => 0,
'mbti' => 0,
'disc' => 0,
'pdp' => 0,
'total' => 0,
];
}
if (in_array($type, ['face', 'mbti', 'disc', 'pdp'], true)) {
$trendMap[$d][$type] += $cnt;
$trendMap[$d]['total'] += $cnt;
}
}
// 补齐没有数据的日期
$trendData = [];
for ($i = 0; $i < $days; $i++) {
$d = date('Y-m-d', strtotime('-' . ($days - 1 - $i) . ' days'));
if (isset($trendMap[$d])) {
$trendData[] = $trendMap[$d];
} else {
$trendData[] = [
'date' => $d,
'face' => 0,
'mbti' => 0,
'disc' => 0,
'pdp' => 0,
'total' => 0,
];
}
}
$topTestUsers = $this->buildTopTestUsers($enterpriseId, 10);
return success([
'totalUsers' => $totalUsers,
'testsCompleted' => $testsCompleted,
'activeToday' => $activeToday,
'pendingReviews' => $pendingReviews,
'testTrends' => $trendData,
'topTestUsers' => $topTestUsers,
]);
} catch (\Exception $e) {
return error('获取统计数据失败:' . $e->getMessage(), 500);
}
}
/**
* 按测试完成次数排序,取前 N 名小程序用户与列表页口径一致test_results 按企业过滤)
*/
private function buildTopTestUsers(?int $enterpriseId, int $limit = 10): array
{
$limit = min(max($limit, 1), 50);
$q = Db::name('test_results')->field('userId, COUNT(*) as cnt')->group('userId')->order('cnt', 'desc')->limit($limit);
if ($enterpriseId) {
$q->where('enterpriseId', $enterpriseId);
}
$rankRows = $q->select()->toArray();
if (empty($rankRows)) {
return [];
}
$uids = array_values(array_filter(array_map(static function ($r) {
return (int) ($r['userId'] ?? 0);
}, $rankRows)));
$countMap = [];
foreach ($rankRows as $r) {
$uid = (int) ($r['userId'] ?? 0);
if ($uid > 0) {
$countMap[$uid] = (int) ($r['cnt'] ?? 0);
}
}
if (empty($uids)) {
return [];
}
$users = Db::name('wechat_users')
->whereIn('id', $uids)
->field('id,nickname,phone,avatar,createdAt')
->select()
->toArray();
$userMap = [];
foreach ($users as $u) {
$userMap[(int) $u['id']] = $u;
}
$trQuery = Db::name('test_results')->whereIn('userId', $uids);
if ($enterpriseId) {
$trQuery->where('enterpriseId', $enterpriseId);
}
$testRows = $trQuery
->field('userId, testType, resultData, createdAt')
->order('createdAt', 'desc')
->select()
->toArray();
$testsByUser = [];
foreach ($testRows as $row) {
$uid = (int) ($row['userId'] ?? 0);
if ($uid <= 0) {
continue;
}
if (!isset($testsByUser[$uid])) {
$testsByUser[$uid] = [];
}
$raw = $row['resultData'] ?? '';
$testsByUser[$uid][] = [
'testType' => $row['testType'] ?? '',
'result' => is_string($raw) ? $raw : json_encode($raw, JSON_UNESCAPED_UNICODE),
'createdAt' => (int) ($row['createdAt'] ?? 0),
];
}
$out = [];
foreach ($uids as $uid) {
$wu = $userMap[$uid] ?? null;
$tests = $testsByUser[$uid] ?? [];
$lastAt = 0;
foreach ($tests as $t) {
$lastAt = max($lastAt, (int) ($t['createdAt'] ?? 0));
}
$out[] = [
'id' => $uid,
'username' => $wu ? ($wu['nickname'] ?? ('用户' . $uid)) : ('用户' . $uid),
'nickname' => $wu ? ($wu['nickname'] ?? '') : '',
'phone' => $wu ? ($wu['phone'] ?? '') : '',
'avatar' => $wu ? ($wu['avatar'] ?? '') : '',
'testCount' => $countMap[$uid] ?? 0,
'lastTestAt' => $lastAt > 0 ? $lastAt : null,
'mbtiType' => $this->extractResultType($tests, 'mbti'),
'pdpType' => $this->extractResultType($tests, 'pdp'),
'discType' => $this->extractResultType($tests, 'disc'),
'faceMbtiType' => $this->extractFaceSubType($tests, 'mbti'),
'faceDiscType' => $this->extractFaceSubType($tests, 'disc'),
'facePdpType' => $this->extractFaceSubType($tests, 'pdp'),
];
}
return $out;
}
/**
* 格式化时间
* @param int $timestamp
* @return string
*/
private function formatTime($timestamp)
{
if (!$timestamp) {
return '';
}
$now = time();
$diff = $now - $timestamp;
if ($diff < 60) {
return '刚刚';
} elseif ($diff < 3600) {
return floor($diff / 60) . '分钟前';
} elseif ($diff < 86400) {
return floor($diff / 3600) . '小时前';
} elseif ($diff < 604800) {
return floor($diff / 86400) . '天前';
} else {
return date('Y-m-d H:i', $timestamp);
}
}
}

View File

@@ -1,168 +1,171 @@
<?php
namespace app\controller\admin;
use app\BaseController;
use think\facade\Db;
use think\facade\Request;
/**
* 管理端订单列表(只读),包含用户信息与关联的测试数据
*/
class Order extends BaseController
{
/**
* 订单列表:分页、关键词、状态/产品筛选;企业管理员仅本企业订单
* GET /api/v1/admin/orders?page=1&pageSize=20&keyword=&status=&productType=
*/
public function index()
{
$user = $this->request->user ?? null;
if (!$user) {
return error('未登录', 401);
}
if (!in_array($user['role'] ?? '', ['admin', 'enterprise_admin'])) {
return error('无权限访问', 403);
}
$page = (int) Request::param('page', 1);
$pageSize = (int) Request::param('pageSize', 20);
$pageSize = min(max($pageSize, 1), 100);
$keyword = trim(Request::param('keyword', ''));
$status = trim(Request::param('status', ''));
$productType = trim(Request::param('productType', ''));
// admin / enterprise_admin 均只能看本企业订单
$enterpriseId = $user['enterpriseId'] ?? null;
if (!$enterpriseId) {
$adminRow = Db::name('users')->where('id', $user['userId'] ?? 0)->find();
$enterpriseId = $adminRow['enterpriseId'] ?? null;
}
$query = Db::name('orders');
if ($enterpriseId !== null) {
$query->where('enterpriseId', $enterpriseId);
}
if ($status !== '') {
$query->where('status', $status);
}
if ($productType !== '') {
$query->where('productType', $productType);
}
if ($keyword !== '') {
if (is_numeric($keyword)) {
$query->where(function ($q) use ($keyword) {
$q->whereLike('orderNo', '%' . $keyword . '%')->whereOr('userId', (int) $keyword);
});
} else {
$userIdsMatch = Db::name('wechat_users')->where('nickname|phone', 'like', '%' . $keyword . '%')->column('id');
$userIdsMatch = array_values(array_filter($userIdsMatch));
$query->where(function ($q) use ($keyword, $userIdsMatch) {
$q->whereLike('orderNo', '%' . $keyword . '%');
if (!empty($userIdsMatch)) {
$q->whereOr('userId', 'in', $userIdsMatch);
}
});
}
}
$query->order('createdAt', 'desc');
$total = (int) (clone $query)->count();
$list = (clone $query)->page($page, $pageSize)->select()->toArray();
$userIds = array_values(array_unique(array_filter(array_column($list, 'userId'))));
$usersMap = [];
if (!empty($userIds)) {
$users = Db::name('wechat_users')
->where('id', 'in', $userIds)
->field('id, nickname, phone')
->select()
->toArray();
foreach ($users as $u) {
$usersMap[(int) $u['id']] = $u;
}
}
$orderIds = array_column($list, 'id');
$testsByOrder = [];
if (!empty($orderIds)) {
$tests = Db::name('test_results')
->where('orderId', 'in', $orderIds)
->field('id, orderId, userId, testType, resultData, createdAt')
->order('createdAt', 'desc')
->select()
->toArray();
foreach ($tests as $t) {
$oid = (int) ($t['orderId'] ?? 0);
if ($oid <= 0) {
continue;
}
if (!isset($testsByOrder[$oid])) {
$testsByOrder[$oid] = [];
}
$raw = $t['resultData'] ?? '';
$resultStr = is_string($raw) ? $raw : json_encode($raw, JSON_UNESCAPED_UNICODE);
$testsByOrder[$oid][] = [
'id' => (int) $t['id'],
'testType' => $t['testType'] ?? '',
'resultSummary' => $this->extractResultSummary($t['testType'] ?? '', $resultStr),
'createdAt' => isset($t['createdAt']) ? (int) $t['createdAt'] : null,
];
}
}
foreach ($list as &$row) {
$uid = (int) ($row['userId'] ?? 0);
$u = $usersMap[$uid] ?? null;
$row['userName'] = $u ? ($u['nickname'] ?? ('用户' . $uid)) : ('用户' . $uid);
$row['userPhone'] = $u ? ($u['phone'] ?? '') : '';
$row['testData'] = $testsByOrder[$row['id']] ?? [];
}
return paginate_response($list, $total, $page, $pageSize);
}
/**
* 从 resultData 字符串中提取简要结果(用于列表展示)
*/
private function extractResultSummary(string $testType, string $resultStr): string
{
if ($resultStr === '') {
return '-';
}
$data = json_decode($resultStr, true);
if (!is_array($data)) {
return mb_substr($resultStr, 0, 30) . (mb_strlen($resultStr) > 30 ? '…' : '');
}
$type = strtolower($testType);
if ($type === 'mbti') {
return (string) ($data['mbtiType'] ?? $data['type'] ?? $data['result'] ?? '');
}
if ($type === 'disc') {
$desc = $data['description']['type'] ?? null;
if (is_string($desc) && $desc !== '') {
return $desc;
}
if (!empty($data['dominantType'])) {
return (string) $data['dominantType'] . '型';
}
return (string) ($data['disc'] ?? '');
}
if ($type === 'pdp') {
$desc = $data['description']['type'] ?? null;
if (is_string($desc) && $desc !== '') {
return $desc;
}
if (!empty($data['dominantType'])) {
return (string) $data['dominantType'];
}
return (string) ($data['pdp'] ?? '');
}
if ($type === 'face' || $type === 'ai') {
return '人脸分析';
}
return (string) ($data['type'] ?? $data['result'] ?? '');
}
}
<?php
namespace app\controller\admin;
use app\BaseController;
use think\facade\Db;
use think\facade\Request;
/**
* 管理端订单列表(只读),包含用户信息与关联的测试数据
*/
class Order extends BaseController
{
/**
* 订单列表:分页、关键词、状态/产品筛选;企业管理员仅本企业订单
* GET /api/v1/admin/orders?page=1&pageSize=20&keyword=&status=&productType=
*/
public function index()
{
$user = $this->request->user ?? null;
if (!$user) {
return error('未登录', 401);
}
if (!in_array($user['role'] ?? '', ['admin', 'enterprise_admin', 'superadmin'])) {
return error('无权限访问', 403);
}
$page = (int) Request::param('page', 1);
$pageSize = (int) Request::param('pageSize', 20);
$pageSize = min(max($pageSize, 1), 100);
$keyword = trim(Request::param('keyword', ''));
$status = trim(Request::param('status', ''));
$productType = trim(Request::param('productType', ''));
// 超管:全平台订单;其余管理员仅本企业
$enterpriseId = null;
if (($user['role'] ?? '') !== 'superadmin') {
$enterpriseId = $user['enterpriseId'] ?? null;
if (!$enterpriseId) {
$adminRow = Db::name('users')->where('id', $user['userId'] ?? 0)->find();
$enterpriseId = $adminRow['enterpriseId'] ?? null;
}
}
$query = Db::name('orders');
if ($enterpriseId !== null) {
$query->where('enterpriseId', $enterpriseId);
}
if ($status !== '') {
$query->where('status', $status);
}
if ($productType !== '') {
$query->where('productType', $productType);
}
if ($keyword !== '') {
if (is_numeric($keyword)) {
$query->where(function ($q) use ($keyword) {
$q->whereLike('orderNo', '%' . $keyword . '%')->whereOr('userId', (int) $keyword);
});
} else {
$userIdsMatch = Db::name('wechat_users')->where('nickname|phone', 'like', '%' . $keyword . '%')->column('id');
$userIdsMatch = array_values(array_filter($userIdsMatch));
$query->where(function ($q) use ($keyword, $userIdsMatch) {
$q->whereLike('orderNo', '%' . $keyword . '%');
if (!empty($userIdsMatch)) {
$q->whereOr('userId', 'in', $userIdsMatch);
}
});
}
}
$query->order('createdAt', 'desc');
$total = (int) (clone $query)->count();
$list = (clone $query)->page($page, $pageSize)->select()->toArray();
$userIds = array_values(array_unique(array_filter(array_column($list, 'userId'))));
$usersMap = [];
if (!empty($userIds)) {
$users = Db::name('wechat_users')
->where('id', 'in', $userIds)
->field('id, nickname, phone')
->select()
->toArray();
foreach ($users as $u) {
$usersMap[(int) $u['id']] = $u;
}
}
$orderIds = array_column($list, 'id');
$testsByOrder = [];
if (!empty($orderIds)) {
$tests = Db::name('test_results')
->where('orderId', 'in', $orderIds)
->field('id, orderId, userId, testType, resultData, createdAt')
->order('createdAt', 'desc')
->select()
->toArray();
foreach ($tests as $t) {
$oid = (int) ($t['orderId'] ?? 0);
if ($oid <= 0) {
continue;
}
if (!isset($testsByOrder[$oid])) {
$testsByOrder[$oid] = [];
}
$raw = $t['resultData'] ?? '';
$resultStr = is_string($raw) ? $raw : json_encode($raw, JSON_UNESCAPED_UNICODE);
$testsByOrder[$oid][] = [
'id' => (int) $t['id'],
'testType' => $t['testType'] ?? '',
'resultSummary' => $this->extractResultSummary($t['testType'] ?? '', $resultStr),
'createdAt' => isset($t['createdAt']) ? (int) $t['createdAt'] : null,
];
}
}
foreach ($list as &$row) {
$uid = (int) ($row['userId'] ?? 0);
$u = $usersMap[$uid] ?? null;
$row['userName'] = $u ? ($u['nickname'] ?? ('用户' . $uid)) : ('用户' . $uid);
$row['userPhone'] = $u ? ($u['phone'] ?? '') : '';
$row['testData'] = $testsByOrder[$row['id']] ?? [];
}
return paginate_response($list, $total, $page, $pageSize);
}
/**
* 从 resultData 字符串中提取简要结果(用于列表展示)
*/
private function extractResultSummary(string $testType, string $resultStr): string
{
if ($resultStr === '') {
return '-';
}
$data = json_decode($resultStr, true);
if (!is_array($data)) {
return mb_substr($resultStr, 0, 30) . (mb_strlen($resultStr) > 30 ? '…' : '');
}
$type = strtolower($testType);
if ($type === 'mbti') {
return (string) ($data['mbtiType'] ?? $data['type'] ?? $data['result'] ?? '');
}
if ($type === 'disc') {
$desc = $data['description']['type'] ?? null;
if (is_string($desc) && $desc !== '') {
return $desc;
}
if (!empty($data['dominantType'])) {
return (string) $data['dominantType'] . '型';
}
return (string) ($data['disc'] ?? '');
}
if ($type === 'pdp') {
$desc = $data['description']['type'] ?? null;
if (is_string($desc) && $desc !== '') {
return $desc;
}
if (!empty($data['dominantType'])) {
return (string) $data['dominantType'];
}
return (string) ($data['pdp'] ?? '');
}
if ($type === 'face' || $type === 'ai') {
return '人脸分析';
}
return (string) ($data['type'] ?? $data['result'] ?? '');
}
}

View File

@@ -48,10 +48,14 @@ class Question extends BaseController
// 如果指定了企业ID优先查询企业题库
// 如果没有企业题库则查询超管题库enterpriseId = NULL
if ($enterpriseId !== null) {
// 先检查企业是否有自己的题库
$enterpriseQuestionCount = QuestionModel::where('enterpriseId', $enterpriseId)
->where('type', $type ?: ['mbti', 'disc', 'pdp'])
->count();
// 先检查企业是否有自己的题库(未指定 type 时需统计 mbti/disc/pdp 三类)
$countQuery = QuestionModel::where('enterpriseId', $enterpriseId);
if ($type !== '') {
$countQuery->where('type', $type);
} else {
$countQuery->whereIn('type', ['mbti', 'disc', 'pdp']);
}
$enterpriseQuestionCount = $countQuery->count();
if ($enterpriseQuestionCount > 0) {
// 使用企业题库

View File

@@ -1,418 +1,486 @@
<?php
namespace app\controller\admin;
use app\BaseController;
use app\model\SystemConfig as SystemConfigModel;
use app\model\User as UserModel;
use think\facade\Request;
use think\facade\Db;
/**
* 系统设置控制器(普通管理员)
*/
class Settings extends BaseController
{
/**
* 获取系统配置
* @return \think\response\Json
*/
public function index()
{
$user = $this->request->user ?? null;
if (!$user) {
return error('未登录', 401);
}
// 验证是否为管理员
if (!in_array($user['role'], ['admin', 'enterprise_admin'])) {
return error('无权限访问', 403);
}
try {
// 获取当前管理员用户名
$jwtUsername = $user['username'] ?? null;
$username = 'admin';
if ($jwtUsername) {
$currentUser = UserModel::where('username', $jwtUsername)
->whereIn('role', ['admin', 'enterprise_admin'])
->find();
if ($currentUser) {
$username = $currentUser->username;
} else {
$username = $jwtUsername;
}
}
return success([
'username' => $username
]);
} catch (\Exception $e) {
return error('获取配置失败:' . $e->getMessage(), 500);
}
}
/**
* 获取可用字体列表
* GET /api/v1/admin/settings/fonts
*/
public function getFonts()
{
$user = $this->request->user ?? null;
if (!$user || !in_array($user['role'], ['admin', 'enterprise_admin'])) {
return error('无权限访问', 403);
}
$fonts = \app\common\service\PosterService::getAvailableFonts();
return success([
'fonts' => $fonts,
'fontDir' => root_path() . 'public/fonts/',
'dirExist' => is_dir(root_path() . 'public/fonts/'),
]);
}
/**
* 获取海报配置
* GET /api/v1/admin/settings/poster
* 有 enterpriseId 则读企业专属行否则读全局enterprise_id=0
*/
public function getPosterConfig()
{
$user = $this->request->user ?? null;
if (!$user || !in_array($user['role'], ['admin', 'enterprise_admin'])) {
return error('无权限访问', 403);
}
$eid = (int)($user['enterpriseId'] ?? 0);
$row = self::getConfig('poster_config', $eid);
$poster = $row ?: ['bgColor' => '#ffffff', 'bgImage' => '', 'elements' => []];
return success(['poster' => $poster]);
}
/**
* 保存海报配置
* PUT /api/v1/admin/settings/poster
*/
public function updatePosterConfig()
{
$user = $this->request->user ?? null;
if (!$user || !in_array($user['role'], ['admin', 'enterprise_admin'])) {
return error('无权限访问', 403);
}
$input = json_decode($this->request->getContent(), true);
if (!is_array($input)) {
$input = [];
}
$data = [
'bgColor' => $input['bgColor'] ?? '#ffffff',
'bgImage' => $input['bgImage'] ?? '',
'elements' => $input['elements'] ?? []
];
$eid = (int)($user['enterpriseId'] ?? 0);
try {
self::saveConfig('poster_config', $data, $eid, '分销海报可视化配置');
return success(null, '海报配置已保存');
} catch (\Exception $e) {
return error('保存失败:' . $e->getMessage(), 500);
}
}
/**
* 读取配置key + enterprise_id有企业专属则取否则降级到 enterprise_id=0
*/
private static function getConfig(string $key, int $enterpriseId = 0, bool $fallbackGlobal = false): ?array
{
$row = Db::name('system_config')
->where('key', $key)
->where('enterprise_id', $enterpriseId)
->find();
if ($row && !empty($row['value'])) {
$val = is_string($row['value']) ? json_decode($row['value'], true) : $row['value'];
if (is_array($val)) return $val;
}
if ($fallbackGlobal && $enterpriseId > 0) {
$row = Db::name('system_config')
->where('key', $key)
->where('enterprise_id', 0)
->find();
if ($row && !empty($row['value'])) {
$val = is_string($row['value']) ? json_decode($row['value'], true) : $row['value'];
if (is_array($val)) return $val;
}
}
return null;
}
/**
* 保存配置key + enterprise_id存在则 update否则 insert
*/
private static function saveConfig(string $key, array $value, int $enterpriseId = 0, string $description = ''): void
{
$now = time();
$json = json_encode($value, JSON_UNESCAPED_UNICODE);
$exists = Db::name('system_config')
->where('key', $key)
->where('enterprise_id', $enterpriseId)
->find();
if ($exists) {
Db::name('system_config')
->where('key', $key)
->where('enterprise_id', $enterpriseId)
->update(['value' => $json, 'updatedAt' => $now]);
} else {
Db::name('system_config')->insert([
'key' => $key,
'enterprise_id' => $enterpriseId,
'value' => $json,
'description' => $description,
'createdAt' => $now,
'updatedAt' => $now,
]);
}
}
/**
* 安全解码 JSON处理可能的多重编码
*/
private static function decodeJsonSafe($raw): ?array
{
if (!$raw) return null;
$val = $raw;
for ($i = 0; $i < 5 && is_string($val); $i++) {
$decoded = json_decode($val, true);
if ($decoded === null && json_last_error() !== JSON_ERROR_NONE) break;
$val = $decoded;
}
return is_array($val) ? $val : null;
}
/**
* 获取小程序配置
* 读取全局 text_configenterprise_id=0作为默认值再用企业专属行覆盖
* GET /api/v1/admin/settings/miniprogram
*/
public function getMiniprogramConfig()
{
$user = $this->request->user ?? null;
if (!$user || !in_array($user['role'], ['admin', 'enterprise_admin'])) {
return error('无权限访问', 403);
}
try {
$eid = (int)($user['enterpriseId'] ?? 0);
// 全局小程序名称(仅超管可改,此处只读)
$miniprogramName = '神仙团队AI性格测试';
$siteInfo = Db::name('system_config')
->where('key', 'site_info')
->where('enterprise_id', 0)
->find();
if ($siteInfo && !empty($siteInfo['value'])) {
$val = is_string($siteInfo['value']) ? json_decode($siteInfo['value'], true) : $siteInfo['value'];
$miniprogramName = (string) ($val['miniprogramName'] ?? $val['siteName'] ?? $miniprogramName);
}
$tcDefaults = [
'analyzingTitle' => '正在分析中',
'startButtonText' => '开始面相测试',
'startButtonEnterprise' => '开始面测试',
'reportTitle' => '分析报告',
'aiAnalysisText' => '智能分析',
];
// 全局文案enterprise_id=0作为基础
$globalTc = self::getConfig('text_config', 0);
$textConfigData = $globalTc
? array_merge($tcDefaults, array_intersect_key($globalTc, $tcDefaults))
: $tcDefaults;
// 企业专属文案 + 小程序名称 覆盖
if ($eid > 0) {
$eidTc = self::getConfig('text_config', $eid);
if ($eidTc) {
$textConfigData = array_merge($textConfigData, array_intersect_key($eidTc, $tcDefaults));
if (!empty($eidTc['miniprogramName'])) {
$miniprogramName = (string) $eidTc['miniprogramName'];
}
}
}
return success([
'miniprogramName' => $miniprogramName,
'textConfig' => $textConfigData,
]);
} catch (\Exception $e) {
return error('获取配置失败:' . $e->getMessage(), 500);
}
}
/**
* 更新小程序配置
* 写入 text_config 行enterprise_id={eid}(有企业)或 0无企业
* PUT /api/v1/admin/settings/miniprogram
*/
public function updateMiniprogramConfig()
{
$user = $this->request->user ?? null;
if (!$user || !in_array($user['role'], ['admin', 'enterprise_admin'])) {
return error('无权限访问', 403);
}
$rawBody = $this->request->getContent();
if (empty($rawBody)) {
$rawBody = file_get_contents('php://input');
}
$input = $rawBody ? json_decode($rawBody, true) : null;
if (!is_array($input)) {
$input = [
'miniprogramName' => Request::param('miniprogramName', ''),
'textConfig' => Request::param('textConfig', []),
];
}
$miniprogramName = trim((string) ($input['miniprogramName'] ?? ''));
$textConfig = $input['textConfig'] ?? [];
if ($miniprogramName === '') {
return error('小程序名称不能为空', 400);
}
$tcKeys = ['analyzingTitle', 'startButtonText', 'startButtonEnterprise', 'reportTitle', 'aiAnalysisText'];
$tcDefaults = [
'analyzingTitle' => '正在分析中',
'startButtonText' => '开始面相测试',
'startButtonEnterprise' => '开始面部测试',
'reportTitle' => '分析报告',
'aiAnalysisText' => '智能分析',
];
$tcData = is_array($textConfig) ? array_intersect_key($textConfig, array_flip($tcKeys)) : [];
$tcMerge = array_merge($tcDefaults, $tcData);
$eid = (int)($user['enterpriseId'] ?? 0);
try {
// eid=0更新 site_info 的小程序名称(全局)
if ($eid === 0) {
$siteRow = Db::name('system_config')->where('key', 'site_info')->where('enterprise_id', 0)->find();
$siteInfo = $siteRow && !empty($siteRow['value'])
? (is_string($siteRow['value']) ? json_decode($siteRow['value'], true) : $siteRow['value'])
: [];
$siteInfo = is_array($siteInfo) ? $siteInfo : [];
$siteInfo['miniprogramName'] = $miniprogramName;
$siteInfo['siteName'] = $siteInfo['siteName'] ?? $miniprogramName;
$siteInfo['updatedAt'] = time();
self::saveConfig('site_info', $siteInfo, 0, '站点信息');
} else {
// 企业专属:把 miniprogramName 一并写入 text_config
$tcMerge['miniprogramName'] = $miniprogramName;
}
// 统一写到 text_config企业行已含 miniprogramName全局行不含
self::saveConfig('text_config', $tcMerge, $eid, $eid > 0 ? "小程序文案配置(企业{$eid}" : '小程序文案配置(全局)');
return success([
'miniprogramName' => $miniprogramName,
'textConfig' => $tcMerge,
], '小程序配置已保存');
} catch (\Exception $e) {
return error('保存失败:' . $e->getMessage(), 500);
}
}
/**
* 更新管理员账户信息
* @return \think\response\Json
*/
public function updateCredentials()
{
$user = $this->request->user ?? null;
if (!$user) {
return error('未登录', 401);
}
// 验证是否为管理员
if (!in_array($user['role'], ['admin', 'enterprise_admin'])) {
return error('无权限访问', 403);
}
// 兼容 axios JSON PUT 与表单提交
$rawBody = $this->request->getContent();
if (empty($rawBody)) {
$rawBody = file_get_contents('php://input');
}
$input = $rawBody ? json_decode($rawBody, true) : null;
if (!is_array($input)) {
$input = [];
}
$username = trim((string)($input['username'] ?? Request::param('username', '')));
$currentPassword = (string)($input['currentPassword'] ?? Request::param('currentPassword', ''));
$newPassword = (string)($input['newPassword'] ?? Request::param('newPassword', ''));
$confirmPassword = (string)($input['confirmPassword'] ?? Request::param('confirmPassword', ''));
if (empty($username)) {
return error('用户名不能为空', 400);
}
try {
// 优先使用JWT中的username来查找用户
$jwtUsername = $user['username'] ?? null;
if (empty($jwtUsername)) {
return error('无法获取用户信息,请重新登录', 400);
}
// 直接通过username查找用户
$userModel = UserModel::where('username', $jwtUsername)
->whereIn('role', ['admin', 'enterprise_admin'])
->find();
if (!$userModel) {
return error('用户不存在,请检查登录状态', 404);
}
// 如果要修改密码,需要验证当前密码
if (!empty($newPassword)) {
if (empty($currentPassword)) {
return error('请输入当前密码', 400);
}
if ($newPassword !== $confirmPassword) {
return error('两次输入的密码不一致', 400);
}
// 验证当前密码User 模型已有原始加密密码)
if (!password_verify($currentPassword, $userModel->password)) {
return error('当前密码错误', 400);
}
// 更新密码:传入明文,交由 User 模型的 setPasswordAttr 自动加密
$userModel->password = $newPassword;
}
// 更新用户名
if ($username !== $userModel->username) {
// 检查用户名是否已存在(排除当前用户)
$exists = UserModel::where('username', $username)
->where('id', '<>', $userModel->id)
->find();
if ($exists) {
return error('用户名已存在', 400);
}
$userModel->username = $username;
}
$userModel->save();
return success([
'username' => $userModel->username
], '账户信息已更新');
} catch (\Exception $e) {
return error('更新失败:' . $e->getMessage(), 500);
}
}
}
<?php
namespace app\controller\admin;
use app\BaseController;
use app\common\service\FeishuLeadWebhookService;
use app\model\SystemConfig as SystemConfigModel;
use app\model\User as UserModel;
use think\facade\Request;
use think\facade\Db;
/**
* 系统设置控制器(普通管理员)
*/
class Settings extends BaseController
{
/**
* 获取系统配置
* @return \think\response\Json
*/
public function index()
{
$user = $this->request->user ?? null;
if (!$user) {
return error('未登录', 401);
}
// 验证是否为管理员
if (!in_array($user['role'], ['admin', 'enterprise_admin'])) {
return error('无权限访问', 403);
}
try {
// 获取当前管理员用户名
$jwtUsername = $user['username'] ?? null;
$username = 'admin';
if ($jwtUsername) {
$currentUser = UserModel::where('username', $jwtUsername)
->whereIn('role', ['admin', 'enterprise_admin'])
->find();
if ($currentUser) {
$username = $currentUser->username;
} else {
$username = $jwtUsername;
}
}
return success([
'username' => $username
]);
} catch (\Exception $e) {
return error('获取配置失败:' . $e->getMessage(), 500);
}
}
/**
* 获取可用字体列表
* GET /api/v1/admin/settings/fonts
*/
public function getFonts()
{
$user = $this->request->user ?? null;
if (!$user || !in_array($user['role'], ['admin', 'enterprise_admin'])) {
return error('无权限访问', 403);
}
$fonts = \app\common\service\PosterService::getAvailableFonts();
return success([
'fonts' => $fonts,
'fontDir' => root_path() . 'public/fonts/',
'dirExist' => is_dir(root_path() . 'public/fonts/'),
]);
}
/**
* 获取海报配置
* GET /api/v1/admin/settings/poster
* 有 enterpriseId 则读企业专属行否则读全局enterprise_id=0
*/
public function getPosterConfig()
{
$user = $this->request->user ?? null;
if (!$user || !in_array($user['role'], ['admin', 'enterprise_admin'])) {
return error('无权限访问', 403);
}
$eid = (int)($user['enterpriseId'] ?? 0);
$row = self::getConfig('poster_config', $eid);
$poster = $row ?: ['bgColor' => '#ffffff', 'bgImage' => '', 'elements' => []];
return success(['poster' => $poster]);
}
/**
* 保存海报配置
* PUT /api/v1/admin/settings/poster
*/
public function updatePosterConfig()
{
$user = $this->request->user ?? null;
if (!$user || !in_array($user['role'], ['admin', 'enterprise_admin'])) {
return error('无权限访问', 403);
}
$input = json_decode($this->request->getContent(), true);
if (!is_array($input)) {
$input = [];
}
$data = [
'bgColor' => $input['bgColor'] ?? '#ffffff',
'bgImage' => $input['bgImage'] ?? '',
'elements' => $input['elements'] ?? []
];
$eid = (int)($user['enterpriseId'] ?? 0);
try {
self::saveConfig('poster_config', $data, $eid, '分销海报可视化配置');
return success(null, '海报配置已保存');
} catch (\Exception $e) {
return error('保存失败:' . $e->getMessage(), 500);
}
}
/**
* 读取配置key + enterprise_id有企业专属则取否则降级到 enterprise_id=0
*/
private static function getConfig(string $key, int $enterpriseId = 0, bool $fallbackGlobal = false): ?array
{
$row = Db::name('system_config')
->where('key', $key)
->where('enterprise_id', $enterpriseId)
->find();
if ($row && !empty($row['value'])) {
$val = is_string($row['value']) ? json_decode($row['value'], true) : $row['value'];
if (is_array($val)) return $val;
}
if ($fallbackGlobal && $enterpriseId > 0) {
$row = Db::name('system_config')
->where('key', $key)
->where('enterprise_id', 0)
->find();
if ($row && !empty($row['value'])) {
$val = is_string($row['value']) ? json_decode($row['value'], true) : $row['value'];
if (is_array($val)) return $val;
}
}
return null;
}
/**
* 保存配置key + enterprise_id存在则 update否则 insert
*/
private static function saveConfig(string $key, array $value, int $enterpriseId = 0, string $description = ''): void
{
$now = time();
$json = json_encode($value, JSON_UNESCAPED_UNICODE);
$exists = Db::name('system_config')
->where('key', $key)
->where('enterprise_id', $enterpriseId)
->find();
if ($exists) {
Db::name('system_config')
->where('key', $key)
->where('enterprise_id', $enterpriseId)
->update(['value' => $json, 'updatedAt' => $now]);
} else {
Db::name('system_config')->insert([
'key' => $key,
'enterprise_id' => $enterpriseId,
'value' => $json,
'description' => $description,
'createdAt' => $now,
'updatedAt' => $now,
]);
}
}
/**
* 安全解码 JSON处理可能的多重编码
*/
private static function decodeJsonSafe($raw): ?array
{
if (!$raw) return null;
$val = $raw;
for ($i = 0; $i < 5 && is_string($val); $i++) {
$decoded = json_decode($val, true);
if ($decoded === null && json_last_error() !== JSON_ERROR_NONE) break;
$val = $decoded;
}
return is_array($val) ? $val : null;
}
/**
* 获取小程序配置
* 读取全局 text_configenterprise_id=0作为默认值再用企业专属行覆盖
* GET /api/v1/admin/settings/miniprogram
*/
public function getMiniprogramConfig()
{
$user = $this->request->user ?? null;
if (!$user || !in_array($user['role'], ['admin', 'enterprise_admin'])) {
return error('无权限访问', 403);
}
try {
$eid = (int)($user['enterpriseId'] ?? 0);
// 全局小程序名称(仅超管可改,此处只读)
$miniprogramName = '神仙团队AI性格测试';
$siteInfo = Db::name('system_config')
->where('key', 'site_info')
->where('enterprise_id', 0)
->find();
if ($siteInfo && !empty($siteInfo['value'])) {
$val = is_string($siteInfo['value']) ? json_decode($siteInfo['value'], true) : $siteInfo['value'];
$miniprogramName = (string) ($val['miniprogramName'] ?? $val['siteName'] ?? $miniprogramName);
}
$tcDefaults = [
'analyzingTitle' => '正在分析中',
'startButtonText' => '开始面测试',
'startButtonEnterprise' => '开始面部测试',
'reportTitle' => '分析报告',
'aiAnalysisText' => '智能分析',
];
// 全局文案enterprise_id=0作为基础
$globalTc = self::getConfig('text_config', 0);
$textConfigData = $globalTc
? array_merge($tcDefaults, array_intersect_key($globalTc, $tcDefaults))
: $tcDefaults;
// 企业专属文案 + 小程序名称 覆盖
if ($eid > 0) {
$eidTc = self::getConfig('text_config', $eid);
if ($eidTc) {
$textConfigData = array_merge($textConfigData, array_intersect_key($eidTc, $tcDefaults));
if (!empty($eidTc['miniprogramName'])) {
$miniprogramName = (string) $eidTc['miniprogramName'];
}
}
}
return success([
'miniprogramName' => $miniprogramName,
'textConfig' => $textConfigData,
]);
} catch (\Exception $e) {
return error('获取配置失败:' . $e->getMessage(), 500);
}
}
/**
* 飞书获客 Webhook全局 enterprise_id=0
* GET /api/v1/admin/settings/feishu-lead
*/
public function getFeishuLeadConfig()
{
$user = $this->request->user ?? null;
if (!$user || !in_array($user['role'], ['admin', 'enterprise_admin'])) {
return error('无权限访问', 403);
}
$cfg = FeishuLeadWebhookService::getConfig();
return success([
'enabled' => !empty($cfg['enabled']),
'webhookUrl' => (string) ($cfg['webhookUrl'] ?? ''),
'contactPerson' => (string) ($cfg['contactPerson'] ?? '运营'),
]);
}
/**
* PUT /api/v1/admin/settings/feishu-lead
*/
public function updateFeishuLeadConfig()
{
$user = $this->request->user ?? null;
if (!$user || !in_array($user['role'], ['admin', 'enterprise_admin'])) {
return error('无权限访问', 403);
}
$raw = $this->request->getContent();
$input = $raw ? json_decode($raw, true) : [];
if (!is_array($input)) {
$input = [];
}
$enabled = !empty($input['enabled']);
$webhookUrl = trim((string) ($input['webhookUrl'] ?? ''));
$contactPerson = trim((string) ($input['contactPerson'] ?? '运营'));
if ($contactPerson === '') {
$contactPerson = '运营';
}
if ($enabled && $webhookUrl !== '' && stripos($webhookUrl, 'http') !== 0) {
return error('Webhook 须以 http(s) 开头', 400);
}
$json = json_encode([
'enabled' => $enabled,
'webhookUrl' => $webhookUrl,
'contactPerson' => $contactPerson,
], JSON_UNESCAPED_UNICODE);
$now = time();
$key = FeishuLeadWebhookService::CONFIG_KEY;
$exists = Db::name('system_config')->where('key', $key)->where('enterprise_id', 0)->find();
if ($exists) {
Db::name('system_config')
->where('key', $key)
->where('enterprise_id', 0)
->update(['value' => $json, 'updatedAt' => $now]);
} else {
Db::name('system_config')->insert([
'key' => $key,
'enterprise_id' => 0,
'value' => $json,
'description' => '飞书获客 Webhook',
'createdAt' => $now,
'updatedAt' => $now,
]);
}
return success(null, '已保存');
}
/**
* 更新小程序配置
* 写入 text_config 行enterprise_id={eid}(有企业)或 0无企业
* PUT /api/v1/admin/settings/miniprogram
*/
public function updateMiniprogramConfig()
{
$user = $this->request->user ?? null;
if (!$user || !in_array($user['role'], ['admin', 'enterprise_admin'])) {
return error('无权限访问', 403);
}
$rawBody = $this->request->getContent();
if (empty($rawBody)) {
$rawBody = file_get_contents('php://input');
}
$input = $rawBody ? json_decode($rawBody, true) : null;
if (!is_array($input)) {
$input = [
'miniprogramName' => Request::param('miniprogramName', ''),
'textConfig' => Request::param('textConfig', []),
];
}
$miniprogramName = trim((string) ($input['miniprogramName'] ?? ''));
$textConfig = $input['textConfig'] ?? [];
if ($miniprogramName === '') {
return error('小程序名称不能为空', 400);
}
$tcKeys = ['analyzingTitle', 'startButtonText', 'startButtonEnterprise', 'reportTitle', 'aiAnalysisText'];
$tcDefaults = [
'analyzingTitle' => '正在分析中',
'startButtonText' => '开始面相测试',
'startButtonEnterprise' => '开始面部测试',
'reportTitle' => '分析报告',
'aiAnalysisText' => '智能分析',
];
$tcData = is_array($textConfig) ? array_intersect_key($textConfig, array_flip($tcKeys)) : [];
$tcMerge = array_merge($tcDefaults, $tcData);
$eid = (int)($user['enterpriseId'] ?? 0);
try {
// eid=0更新 site_info 的小程序名称(全局)
if ($eid === 0) {
$siteRow = Db::name('system_config')->where('key', 'site_info')->where('enterprise_id', 0)->find();
$siteInfo = $siteRow && !empty($siteRow['value'])
? (is_string($siteRow['value']) ? json_decode($siteRow['value'], true) : $siteRow['value'])
: [];
$siteInfo = is_array($siteInfo) ? $siteInfo : [];
$siteInfo['miniprogramName'] = $miniprogramName;
$siteInfo['siteName'] = $siteInfo['siteName'] ?? $miniprogramName;
$siteInfo['updatedAt'] = time();
self::saveConfig('site_info', $siteInfo, 0, '站点信息');
} else {
// 企业专属:把 miniprogramName 一并写入 text_config
$tcMerge['miniprogramName'] = $miniprogramName;
}
// 统一写到 text_config企业行已含 miniprogramName全局行不含
self::saveConfig('text_config', $tcMerge, $eid, $eid > 0 ? "小程序文案配置(企业{$eid}" : '小程序文案配置(全局)');
return success([
'miniprogramName' => $miniprogramName,
'textConfig' => $tcMerge,
], '小程序配置已保存');
} catch (\Exception $e) {
return error('保存失败:' . $e->getMessage(), 500);
}
}
/**
* 更新管理员账户信息
* @return \think\response\Json
*/
public function updateCredentials()
{
$user = $this->request->user ?? null;
if (!$user) {
return error('未登录', 401);
}
// 验证是否为管理员
if (!in_array($user['role'], ['admin', 'enterprise_admin'])) {
return error('无权限访问', 403);
}
// 兼容 axios JSON PUT 与表单提交
$rawBody = $this->request->getContent();
if (empty($rawBody)) {
$rawBody = file_get_contents('php://input');
}
$input = $rawBody ? json_decode($rawBody, true) : null;
if (!is_array($input)) {
$input = [];
}
$username = trim((string)($input['username'] ?? Request::param('username', '')));
$currentPassword = (string)($input['currentPassword'] ?? Request::param('currentPassword', ''));
$newPassword = (string)($input['newPassword'] ?? Request::param('newPassword', ''));
$confirmPassword = (string)($input['confirmPassword'] ?? Request::param('confirmPassword', ''));
if (empty($username)) {
return error('用户名不能为空', 400);
}
try {
// 优先使用JWT中的username来查找用户
$jwtUsername = $user['username'] ?? null;
if (empty($jwtUsername)) {
return error('无法获取用户信息,请重新登录', 400);
}
// 直接通过username查找用户
$userModel = UserModel::where('username', $jwtUsername)
->whereIn('role', ['admin', 'enterprise_admin'])
->find();
if (!$userModel) {
return error('用户不存在,请检查登录状态', 404);
}
// 如果要修改密码,需要验证当前密码
if (!empty($newPassword)) {
if (empty($currentPassword)) {
return error('请输入当前密码', 400);
}
if ($newPassword !== $confirmPassword) {
return error('两次输入的密码不一致', 400);
}
// 验证当前密码User 模型已有原始加密密码)
if (!password_verify($currentPassword, $userModel->password)) {
return error('当前密码错误', 400);
}
// 更新密码:传入明文,交由 User 模型的 setPasswordAttr 自动加密
$userModel->password = $newPassword;
}
// 更新用户名
if ($username !== $userModel->username) {
// 检查用户名是否已存在(排除当前用户)
$exists = UserModel::where('username', $username)
->where('id', '<>', $userModel->id)
->find();
if ($exists) {
return error('用户名已存在', 400);
}
$userModel->username = $username;
}
$userModel->save();
return success([
'username' => $userModel->username
], '账户信息已更新');
} catch (\Exception $e) {
return error('更新失败:' . $e->getMessage(), 500);
}
}
}

View File

@@ -0,0 +1,101 @@
<?php
namespace app\controller\admin\concern;
/**
* 从测试记录数组中解析 MBTI / DISC / PDP / 人脸子类型(与 AppUser 逻辑一致)
*/
trait ExtractsTestResults
{
private function extractResultType(array $tests, string $type): string
{
$targetType = strtolower($type);
foreach ($tests as $t) {
if (strtolower($t['testType'] ?? '') !== $targetType) {
continue;
}
$result = $t['result'] ?? '';
if (!is_string($result)) {
continue;
}
$dec = json_decode($result, true);
if (!is_array($dec)) {
return $targetType === 'face' ? '人脸分析' : trim($result);
}
if ($targetType === 'face') {
return '人脸分析';
}
if ($targetType === 'mbti') {
return (string) ($dec['mbtiType'] ?? $dec['type'] ?? $dec['result'] ?? '');
}
if ($targetType === 'disc') {
$desc = $dec['description']['type'] ?? null;
if (is_string($desc) && $desc !== '') {
return $desc;
}
if (!empty($dec['dominantType'])) {
return (string) $dec['dominantType'];
}
return (string) ($dec['disc'] ?? '');
}
if ($targetType === 'pdp') {
$desc = $dec['description']['type'] ?? null;
if (is_string($desc) && $desc !== '') {
return $desc;
}
if (!empty($dec['dominantType'])) {
return (string) $dec['dominantType'];
}
return (string) ($dec['pdp'] ?? '');
}
return (string) ($dec['type'] ?? $dec['result'] ?? '');
}
return '';
}
private function extractFaceSubType(array $tests, string $subType): string
{
$target = strtolower($subType);
foreach ($tests as $t) {
if (strtolower($t['testType'] ?? '') !== 'face') {
continue;
}
$result = $t['result'] ?? '';
if (!is_string($result)) {
continue;
}
$dec = json_decode($result, true);
if (!is_array($dec)) {
continue;
}
if ($target === 'mbti') {
if (!empty($dec['mbti']['type'])) {
return (string) $dec['mbti']['type'];
}
if (!empty($dec['mbtiType'])) {
return (string) $dec['mbtiType'];
}
} elseif ($target === 'disc') {
if (!empty($dec['disc']['primary'])) {
return (string) $dec['disc']['primary'];
}
if (!empty($dec['disc'])) {
return (string) $dec['disc'];
}
} elseif ($target === 'pdp') {
if (!empty($dec['pdp']['primary'])) {
return (string) $dec['pdp']['primary'];
}
if (!empty($dec['pdp'])) {
return (string) $dec['pdp'];
}
}
}
return '';
}
}