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:
@@ -1,358 +1,261 @@
|
||||
<?php
|
||||
namespace app\controller\admin;
|
||||
|
||||
use app\BaseController;
|
||||
use think\facade\Db;
|
||||
use think\facade\Request;
|
||||
|
||||
/**
|
||||
* 测试用户(小程序用户)管理 - 只读列表与详情
|
||||
* 数据来源:wechat_users,测试记录来自 test_results(userId 关联 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_results(userId 关联 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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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'] ?? '');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
// 使用企业题库
|
||||
|
||||
@@ -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_config(enterprise_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_config(enterprise_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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
101
api/app/controller/admin/concern/ExtractsTestResults.php
Normal file
101
api/app/controller/admin/concern/ExtractsTestResults.php
Normal 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 '';
|
||||
}
|
||||
}
|
||||
81
api/app/controller/api/Analytics.php
Normal file
81
api/app/controller/api/Analytics.php
Normal file
@@ -0,0 +1,81 @@
|
||||
<?php
|
||||
namespace app\controller\api;
|
||||
|
||||
use app\BaseController;
|
||||
use app\common\service\JwtService;
|
||||
use think\facade\Db;
|
||||
use think\facade\Request;
|
||||
|
||||
/**
|
||||
* 小程序埋点上报(可匿名;带 token 时关联用户)
|
||||
* POST /api/analytics/events
|
||||
*/
|
||||
class Analytics extends BaseController
|
||||
{
|
||||
public function batch()
|
||||
{
|
||||
$body = Request::post();
|
||||
$events = $body['events'] ?? [];
|
||||
if (!is_array($events) || count($events) === 0) {
|
||||
return success(['accepted' => 0], 'ok');
|
||||
}
|
||||
if (count($events) > 50) {
|
||||
return error('单次最多 50 条', 400);
|
||||
}
|
||||
|
||||
$userId = null;
|
||||
$openid = null;
|
||||
$token = JwtService::getTokenFromRequest($this->request);
|
||||
if ($token) {
|
||||
$payload = JwtService::verifyToken($token);
|
||||
if ($payload && ($payload['source'] ?? '') === 'wechat') {
|
||||
$userId = (int) ($payload['userId'] ?? $payload['user_id'] ?? 0) ?: null;
|
||||
}
|
||||
}
|
||||
|
||||
$now = date('Y-m-d H:i:s');
|
||||
$rows = [];
|
||||
foreach ($events as $ev) {
|
||||
if (!is_array($ev)) {
|
||||
continue;
|
||||
}
|
||||
$name = isset($ev['event_name']) ? trim((string) $ev['event_name']) : '';
|
||||
if ($name === '' || strlen($name) > 128) {
|
||||
continue;
|
||||
}
|
||||
$pagePath = isset($ev['page_path']) ? mb_substr(trim((string) $ev['page_path']), 0, 255) : '';
|
||||
$props = $ev['props'] ?? null;
|
||||
$propsJson = null;
|
||||
if ($props !== null && $props !== []) {
|
||||
$propsJson = json_encode($props, JSON_UNESCAPED_UNICODE | JSON_INVALID_UTF8_SUBSTITUTE);
|
||||
if (strlen($propsJson) > 8000) {
|
||||
$propsJson = mb_substr($propsJson, 0, 8000);
|
||||
}
|
||||
}
|
||||
$clientTs = isset($ev['client_ts']) ? (int) $ev['client_ts'] : null;
|
||||
$rowOpenid = null;
|
||||
if (!$userId && isset($ev['openid'])) {
|
||||
$rowOpenid = mb_substr(trim((string) $ev['openid']), 0, 64) ?: null;
|
||||
}
|
||||
$rows[] = [
|
||||
'userId' => $userId,
|
||||
'openid' => $rowOpenid,
|
||||
'eventName' => $name,
|
||||
'pagePath' => $pagePath ?: null,
|
||||
'propsJson' => $propsJson,
|
||||
'clientTs' => $clientTs ?: null,
|
||||
'createdAt' => $now,
|
||||
];
|
||||
}
|
||||
if (count($rows) === 0) {
|
||||
return success(['accepted' => 0], 'ok');
|
||||
}
|
||||
try {
|
||||
Db::name('analytics_events')->insertAll($rows);
|
||||
} catch (\Throwable $e) {
|
||||
// 表未创建时不抛 500,避免小程序端刷屏;超管端「小程序埋点」会提示建表 SQL
|
||||
return success(['accepted' => 0, 'skipped' => true], 'ok');
|
||||
}
|
||||
return success(['accepted' => count($rows)], 'ok');
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -5,6 +5,7 @@ use app\BaseController;
|
||||
use app\model\PricingConfig as PricingConfigModel;
|
||||
use app\model\UserProfile as UserProfileModel;
|
||||
use app\common\service\JwtService;
|
||||
use app\common\service\FeishuLeadWebhookService;
|
||||
use think\facade\Request;
|
||||
use think\facade\Db;
|
||||
|
||||
@@ -313,6 +314,8 @@ class Payment extends BaseController
|
||||
return error('订单不存在', 404);
|
||||
}
|
||||
|
||||
$prevStatus = (string) ($order['status'] ?? '');
|
||||
|
||||
// 仅允许从 pending → 其他状态,避免重复更新已完成订单
|
||||
if ($order['status'] !== 'pending' && $order['status'] !== 'paid') {
|
||||
return success(null, '订单状态已更新,无需重复通知');
|
||||
@@ -339,6 +342,12 @@ class Payment extends BaseController
|
||||
|
||||
// 支付成功时:将关联该订单的测试结果标记为已付款,并记录当时付款金额(分)
|
||||
if ($status === 'success') {
|
||||
if ($prevStatus === 'pending') {
|
||||
try {
|
||||
FeishuLeadWebhookService::onOrderPaid((int) $order['id'], (int) ($order['userId'] ?? 0));
|
||||
} catch (\Throwable $e) {
|
||||
}
|
||||
}
|
||||
$paidAmountFen = isset($order['amount']) ? (int) $order['amount'] : 0;
|
||||
|
||||
Db::name('test_results')
|
||||
@@ -458,6 +467,11 @@ class Payment extends BaseController
|
||||
// 佣金结算失败不影响主流程
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
FeishuLeadWebhookService::onOrderPaid((int) $localOrder['id'], (int) ($localOrder['userId'] ?? 0));
|
||||
} catch (\Throwable $e) {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
100
api/app/controller/superadmin/Analytics.php
Normal file
100
api/app/controller/superadmin/Analytics.php
Normal file
@@ -0,0 +1,100 @@
|
||||
<?php
|
||||
namespace app\controller\superadmin;
|
||||
|
||||
use app\BaseController;
|
||||
use think\facade\Db;
|
||||
use think\facade\Request;
|
||||
|
||||
/**
|
||||
* 小程序埋点统计(仅超级管理员)
|
||||
*/
|
||||
class Analytics extends BaseController
|
||||
{
|
||||
/**
|
||||
* GET /api/v1/superadmin/analytics/summary?days=7
|
||||
*/
|
||||
public function summary()
|
||||
{
|
||||
$days = min(90, max(1, (int) Request::param('days', 7)));
|
||||
$since = date('Y-m-d H:i:s', time() - $days * 86400);
|
||||
|
||||
try {
|
||||
$list = Db::name('analytics_events')
|
||||
->field('eventName, COUNT(*) AS cnt')
|
||||
->where('createdAt', '>=', $since)
|
||||
->group('eventName')
|
||||
->order('cnt', 'desc')
|
||||
->select()
|
||||
->toArray();
|
||||
|
||||
$total = Db::name('analytics_events')
|
||||
->where('createdAt', '>=', $since)
|
||||
->count();
|
||||
|
||||
return success([
|
||||
'days' => $days,
|
||||
'total' => (int) $total,
|
||||
'list' => $list,
|
||||
'tableMissing' => false,
|
||||
]);
|
||||
} catch (\Throwable $e) {
|
||||
return success([
|
||||
'days' => $days,
|
||||
'total' => 0,
|
||||
'list' => [],
|
||||
'tableMissing' => true,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /api/v1/superadmin/analytics/events?days=7&page=1&pageSize=50
|
||||
*/
|
||||
public function events()
|
||||
{
|
||||
$days = min(90, max(1, (int) Request::param('days', 7)));
|
||||
$since = date('Y-m-d H:i:s', time() - $days * 86400);
|
||||
$page = max(1, (int) Request::param('page', 1));
|
||||
$pageSize = min(100, max(10, (int) Request::param('pageSize', 50)));
|
||||
|
||||
try {
|
||||
$total = (int) Db::name('analytics_events')
|
||||
->where('createdAt', '>=', $since)
|
||||
->count();
|
||||
$offset = ($page - 1) * $pageSize;
|
||||
$rows = Db::name('analytics_events')
|
||||
->where('createdAt', '>=', $since)
|
||||
->order('id', 'desc')
|
||||
->limit($offset, $pageSize)
|
||||
->select()
|
||||
->toArray();
|
||||
|
||||
foreach ($rows as &$r) {
|
||||
if (!empty($r['propsJson'])) {
|
||||
$decoded = json_decode($r['propsJson'], true);
|
||||
$r['props'] = is_array($decoded) ? $decoded : null;
|
||||
} else {
|
||||
$r['props'] = null;
|
||||
}
|
||||
unset($r['propsJson']);
|
||||
}
|
||||
unset($r);
|
||||
|
||||
return success([
|
||||
'list' => $rows,
|
||||
'total' => $total,
|
||||
'page' => $page,
|
||||
'pageSize' => $pageSize,
|
||||
'tableMissing' => false,
|
||||
]);
|
||||
} catch (\Throwable $e) {
|
||||
return success([
|
||||
'list' => [],
|
||||
'total' => 0,
|
||||
'page' => $page,
|
||||
'pageSize' => $pageSize,
|
||||
'tableMissing' => true,
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
393
api/app/controller/superadmin/DataMigration.php
Normal file
393
api/app/controller/superadmin/DataMigration.php
Normal file
@@ -0,0 +1,393 @@
|
||||
<?php
|
||||
namespace app\controller\superadmin;
|
||||
|
||||
use app\BaseController;
|
||||
use think\facade\Db;
|
||||
use think\facade\Request;
|
||||
|
||||
/**
|
||||
* 将「平台侧」无企业归属的订单/测试/用户画像归并到指定企业,供企业后台与概览统计一致展示。
|
||||
* 仅超级管理员可调用;默认 dryRun 预览,正式执行需 confirm=true。
|
||||
*/
|
||||
class DataMigration extends BaseController
|
||||
{
|
||||
/**
|
||||
* POST /api/v1/superadmin/data-migration/attach-orphan-orders
|
||||
*
|
||||
* Body JSON:
|
||||
* - targetEnterpriseId (int, 必填) 目标企业 ID
|
||||
* - dryRun (bool, 默认 true) true 只统计不写入
|
||||
* - confirm (bool, 默认 false) 与 dryRun=false 同时为真时才写入
|
||||
* - orderIds (int[], 可选) 仅处理这些订单 id(仍须满足当前无企业归属)
|
||||
* - userIds (int[], 可选) 仅处理这些小程序用户 id 名下的无归属订单
|
||||
* - syncPersonalTestResults (bool, 默认 true) 是否把同用户下 enterpriseId 为空的 personal 测试记录一并标到目标企业
|
||||
* - syncWechatUsers (bool, 默认 true) 是否将 wechat_users.enterpriseId 为空的用户标到目标企业
|
||||
* - clonePersonalProfile (bool, 默认 true) 若无 (userId,enterprise,enterprise) 画像行,则从 personal 行复制一条 enterprise 画像(便于「用户运营」列表出现)
|
||||
*/
|
||||
public function attachOrphanOrders()
|
||||
{
|
||||
$actor = $this->request->user ?? null;
|
||||
if (!$actor || ($actor['role'] ?? '') !== 'superadmin') {
|
||||
return error('仅超级管理员可操作', 403);
|
||||
}
|
||||
|
||||
$body = Request::post();
|
||||
if (!is_array($body)) {
|
||||
$body = [];
|
||||
}
|
||||
|
||||
$targetEnterpriseId = (int) ($body['targetEnterpriseId'] ?? 0);
|
||||
if ($targetEnterpriseId <= 0) {
|
||||
return error('targetEnterpriseId 无效', 400);
|
||||
}
|
||||
|
||||
$ent = Db::name('enterprises')->where('id', $targetEnterpriseId)->find();
|
||||
if (!$ent) {
|
||||
return error('目标企业不存在', 404);
|
||||
}
|
||||
|
||||
$dryRun = array_key_exists('dryRun', $body) ? (bool) $body['dryRun'] : true;
|
||||
$confirm = !empty($body['confirm']);
|
||||
$syncPersonalTestResults = array_key_exists('syncPersonalTestResults', $body) ? (bool) $body['syncPersonalTestResults'] : true;
|
||||
$syncWechatUsers = array_key_exists('syncWechatUsers', $body) ? (bool) $body['syncWechatUsers'] : true;
|
||||
$clonePersonalProfile = array_key_exists('clonePersonalProfile', $body) ? (bool) $body['clonePersonalProfile'] : true;
|
||||
|
||||
$orderIdsFilter = $this->normalizeIdList($body['orderIds'] ?? null);
|
||||
$userIdsFilter = $this->normalizeIdList($body['userIds'] ?? null);
|
||||
|
||||
$orderQuery = Db::name('orders')->where(function ($q) {
|
||||
$q->whereNull('enterpriseId')->whereOr('enterpriseId', 0);
|
||||
});
|
||||
|
||||
if (!empty($orderIdsFilter)) {
|
||||
$orderQuery->whereIn('id', $orderIdsFilter);
|
||||
}
|
||||
if (!empty($userIdsFilter)) {
|
||||
$orderQuery->whereIn('userId', $userIdsFilter);
|
||||
}
|
||||
|
||||
$orderRows = $orderQuery->field('id,userId,orderNo,enterpriseId,status,amount')->select()->toArray();
|
||||
$affectedOrderIds = array_values(array_unique(array_filter(array_column($orderRows, 'id'))));
|
||||
$userIdsFromOrders = array_values(array_unique(array_filter(array_column($orderRows, 'userId'))));
|
||||
|
||||
$testByOrderCount = 0;
|
||||
if (!empty($affectedOrderIds)) {
|
||||
$testByOrderCount = (int) Db::name('test_results')
|
||||
->whereIn('orderId', $affectedOrderIds)
|
||||
->count();
|
||||
}
|
||||
|
||||
$personalTestExtraCount = 0;
|
||||
if ($syncPersonalTestResults && !empty($userIdsFromOrders)) {
|
||||
$personalTestExtraCount = (int) Db::name('test_results')
|
||||
->whereIn('userId', $userIdsFromOrders)
|
||||
->where('testScope', 'personal')
|
||||
->where(function ($q) {
|
||||
$q->whereNull('enterpriseId')->whereOr('enterpriseId', 0);
|
||||
})
|
||||
->count();
|
||||
}
|
||||
|
||||
$wechatPatchCount = 0;
|
||||
if ($syncWechatUsers && !empty($userIdsFromOrders)) {
|
||||
$wechatPatchCount = (int) Db::name('wechat_users')
|
||||
->whereIn('id', $userIdsFromOrders)
|
||||
->where(function ($q) {
|
||||
$q->whereNull('enterpriseId')->whereOr('enterpriseId', 0);
|
||||
})
|
||||
->count();
|
||||
}
|
||||
|
||||
$profileCloneCount = 0;
|
||||
if ($clonePersonalProfile && !empty($userIdsFromOrders)) {
|
||||
foreach ($userIdsFromOrders as $uid) {
|
||||
$hasEnt = Db::name('user_profile')
|
||||
->where('userId', $uid)
|
||||
->where('userType', 'enterprise')
|
||||
->where('enterpriseId', $targetEnterpriseId)
|
||||
->find();
|
||||
if (!$hasEnt) {
|
||||
$profileCloneCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$preview = [
|
||||
'targetEnterpriseId' => $targetEnterpriseId,
|
||||
'enterpriseName' => $ent['name'] ?? '',
|
||||
'ordersMatched' => count($orderRows),
|
||||
'orderIds' => $affectedOrderIds,
|
||||
'distinctUserIds' => $userIdsFromOrders,
|
||||
'testResultsByOrder' => $testByOrderCount,
|
||||
'testResultsPersonalExtra' => $personalTestExtraCount,
|
||||
'wechatUsersToPatch' => $wechatPatchCount,
|
||||
'userProfilesToClone' => $profileCloneCount,
|
||||
'dryRun' => $dryRun,
|
||||
];
|
||||
|
||||
if ($dryRun || !$confirm) {
|
||||
$preview['hint'] = $dryRun
|
||||
? '当前为预览(dryRun=true)。若要执行写入,请传 dryRun=false 且 confirm=true。'
|
||||
: '未执行写入:请同时传 dryRun=false 与 confirm=true。';
|
||||
return success($preview);
|
||||
}
|
||||
|
||||
$now = time();
|
||||
Db::startTrans();
|
||||
try {
|
||||
if (!empty($affectedOrderIds)) {
|
||||
Db::name('orders')
|
||||
->whereIn('id', $affectedOrderIds)
|
||||
->update([
|
||||
'enterpriseId' => $targetEnterpriseId,
|
||||
'updatedAt' => $now,
|
||||
]);
|
||||
|
||||
Db::name('test_results')
|
||||
->whereIn('orderId', $affectedOrderIds)
|
||||
->update([
|
||||
'enterpriseId' => $targetEnterpriseId,
|
||||
'testScope' => 'enterprise',
|
||||
'updatedAt' => $now,
|
||||
]);
|
||||
}
|
||||
|
||||
if ($syncPersonalTestResults && !empty($userIdsFromOrders)) {
|
||||
Db::name('test_results')
|
||||
->whereIn('userId', $userIdsFromOrders)
|
||||
->where('testScope', 'personal')
|
||||
->where(function ($q) {
|
||||
$q->whereNull('enterpriseId')->whereOr('enterpriseId', 0);
|
||||
})
|
||||
->update([
|
||||
'enterpriseId' => $targetEnterpriseId,
|
||||
'testScope' => 'enterprise',
|
||||
'updatedAt' => $now,
|
||||
]);
|
||||
}
|
||||
|
||||
if ($syncWechatUsers && !empty($userIdsFromOrders)) {
|
||||
Db::name('wechat_users')
|
||||
->whereIn('id', $userIdsFromOrders)
|
||||
->where(function ($q) {
|
||||
$q->whereNull('enterpriseId')->whereOr('enterpriseId', 0);
|
||||
})
|
||||
->update([
|
||||
'enterpriseId' => $targetEnterpriseId,
|
||||
]);
|
||||
}
|
||||
|
||||
if ($clonePersonalProfile && !empty($userIdsFromOrders)) {
|
||||
foreach ($userIdsFromOrders as $uid) {
|
||||
$this->ensureEnterpriseProfileFromPersonal((int) $uid, $targetEnterpriseId, $now);
|
||||
}
|
||||
}
|
||||
|
||||
Db::commit();
|
||||
} catch (\Throwable $e) {
|
||||
Db::rollback();
|
||||
return error('迁移失败:' . $e->getMessage(), 500);
|
||||
}
|
||||
|
||||
$preview['executed'] = true;
|
||||
$preview['hint'] = '已写入。企业管理员刷新「订单运营 / 概览 / 用户运营」即可看到归属数据。超管仍可见全平台订单。';
|
||||
return success($preview, '迁移完成');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $raw
|
||||
* @return int[]
|
||||
*/
|
||||
private function normalizeIdList($raw): array
|
||||
{
|
||||
if (!is_array($raw) || $raw === []) {
|
||||
return [];
|
||||
}
|
||||
$out = [];
|
||||
foreach ($raw as $v) {
|
||||
$n = (int) $v;
|
||||
if ($n > 0) {
|
||||
$out[] = $n;
|
||||
}
|
||||
}
|
||||
return array_values(array_unique($out));
|
||||
}
|
||||
|
||||
private function ensureEnterpriseProfileFromPersonal(int $userId, int $enterpriseId, int $now): void
|
||||
{
|
||||
if ($userId <= 0 || $enterpriseId <= 0) {
|
||||
return;
|
||||
}
|
||||
$exists = Db::name('user_profile')
|
||||
->where('userId', $userId)
|
||||
->where('userType', 'enterprise')
|
||||
->where('enterpriseId', $enterpriseId)
|
||||
->find();
|
||||
if ($exists) {
|
||||
return;
|
||||
}
|
||||
|
||||
$personal = Db::name('user_profile')
|
||||
->where('userId', $userId)
|
||||
->where('userType', 'personal')
|
||||
->where(function ($q) {
|
||||
$q->whereNull('enterpriseId')->whereOr('enterpriseId', 0);
|
||||
})
|
||||
->order('id', 'desc')
|
||||
->find();
|
||||
|
||||
$base = [
|
||||
'userId' => $userId,
|
||||
'userType' => 'enterprise',
|
||||
'enterpriseId' => $enterpriseId,
|
||||
'testsTotal' => 0,
|
||||
'testsMbti' => 0,
|
||||
'testsDisc' => 0,
|
||||
'testsPdp' => 0,
|
||||
'testsFace' => 0,
|
||||
'ordersTotal' => 0,
|
||||
'paidOrders' => 0,
|
||||
'totalPaidAmount' => 0,
|
||||
'lastTestResultId' => null,
|
||||
'lastTestType' => null,
|
||||
'lastTestAt' => null,
|
||||
'lastMbtiResultId' => null,
|
||||
'lastDiscResultId' => null,
|
||||
'lastPdpResultId' => null,
|
||||
'lastFaceResultId' => null,
|
||||
'createdAt' => $now,
|
||||
'updatedAt' => $now,
|
||||
];
|
||||
|
||||
if ($personal) {
|
||||
$copyFields = [
|
||||
'testsTotal', 'testsMbti', 'testsDisc', 'testsPdp', 'testsFace',
|
||||
'ordersTotal', 'paidOrders', 'totalPaidAmount',
|
||||
'lastTestResultId', 'lastTestType', 'lastTestAt',
|
||||
'lastMbtiResultId', 'lastDiscResultId', 'lastPdpResultId', 'lastFaceResultId',
|
||||
];
|
||||
foreach ($copyFields as $f) {
|
||||
if (array_key_exists($f, $personal) && $personal[$f] !== null) {
|
||||
$base[$f] = $personal[$f];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Db::name('user_profile')->insert($base);
|
||||
}
|
||||
|
||||
/**
|
||||
* 将全平台「无 enterpriseId」的 test_results 与 wechat_users 归属到存客宝(或指定企业)
|
||||
* POST /api/v1/superadmin/data-migration/attach-orphans-to-cunkbao
|
||||
*
|
||||
* Body: targetEnterpriseId (可选)、dryRun (默认 true)、confirm、clonePersonalProfile (默认 true)
|
||||
*/
|
||||
public function attachOrphansToCunkbao()
|
||||
{
|
||||
$actor = $this->request->user ?? null;
|
||||
if (!$actor || ($actor['role'] ?? '') !== 'superadmin') {
|
||||
return error('仅超级管理员可操作', 403);
|
||||
}
|
||||
|
||||
$body = Request::post();
|
||||
if (!is_array($body)) {
|
||||
$body = [];
|
||||
}
|
||||
|
||||
$dryRun = array_key_exists('dryRun', $body) ? (bool) $body['dryRun'] : true;
|
||||
$confirm = !empty($body['confirm']);
|
||||
$clonePersonalProfile = array_key_exists('clonePersonalProfile', $body) ? (bool) $body['clonePersonalProfile'] : true;
|
||||
|
||||
$targetEnterpriseId = (int) ($body['targetEnterpriseId'] ?? 0);
|
||||
if ($targetEnterpriseId <= 0) {
|
||||
$row = Db::name('enterprises')->where('name', 'like', '%存客宝%')->order('id', 'asc')->find();
|
||||
if (!$row) {
|
||||
return error('未找到名称包含「存客宝」的企业,请先在企业管理中创建或传入 targetEnterpriseId', 404);
|
||||
}
|
||||
$targetEnterpriseId = (int) $row['id'];
|
||||
}
|
||||
|
||||
$ent = Db::name('enterprises')->where('id', $targetEnterpriseId)->find();
|
||||
if (!$ent) {
|
||||
return error('目标企业不存在', 404);
|
||||
}
|
||||
|
||||
$testAffected = (int) Db::name('test_results')
|
||||
->where(function ($q) {
|
||||
$q->whereNull('enterpriseId')->whereOr('enterpriseId', 0)->whereOr('enterpriseId', '');
|
||||
})
|
||||
->count();
|
||||
|
||||
$wechatAffected = (int) Db::name('wechat_users')
|
||||
->where(function ($q) {
|
||||
$q->whereNull('enterpriseId')->whereOr('enterpriseId', 0);
|
||||
})
|
||||
->count();
|
||||
|
||||
$userIdsFromTests = Db::name('test_results')
|
||||
->where(function ($q) {
|
||||
$q->whereNull('enterpriseId')->whereOr('enterpriseId', 0)->whereOr('enterpriseId', '');
|
||||
})
|
||||
->distinct(true)
|
||||
->column('userId');
|
||||
$userIdsFromWechat = Db::name('wechat_users')
|
||||
->where(function ($q) {
|
||||
$q->whereNull('enterpriseId')->whereOr('enterpriseId', 0);
|
||||
})
|
||||
->column('id');
|
||||
$distinctUserIds = array_values(array_unique(array_filter(array_merge($userIdsFromTests, $userIdsFromWechat))));
|
||||
|
||||
$preview = [
|
||||
'targetEnterpriseId' => $targetEnterpriseId,
|
||||
'enterpriseName' => $ent['name'] ?? '',
|
||||
'testResultsRows' => $testAffected,
|
||||
'wechatUsersRows' => $wechatAffected,
|
||||
'distinctUserIds' => $distinctUserIds,
|
||||
'dryRun' => $dryRun,
|
||||
];
|
||||
|
||||
if ($dryRun || !$confirm) {
|
||||
$preview['hint'] = $dryRun
|
||||
? '当前为预览。写入请传 dryRun=false 且 confirm=true。'
|
||||
: '未写入:请同时传 dryRun=false 与 confirm=true。';
|
||||
return success($preview);
|
||||
}
|
||||
|
||||
$now = time();
|
||||
Db::startTrans();
|
||||
try {
|
||||
Db::name('test_results')
|
||||
->where(function ($q) {
|
||||
$q->whereNull('enterpriseId')->whereOr('enterpriseId', 0)->whereOr('enterpriseId', '');
|
||||
})
|
||||
->update([
|
||||
'enterpriseId' => $targetEnterpriseId,
|
||||
'testScope' => 'enterprise',
|
||||
'updatedAt' => $now,
|
||||
]);
|
||||
|
||||
Db::name('wechat_users')
|
||||
->where(function ($q) {
|
||||
$q->whereNull('enterpriseId')->whereOr('enterpriseId', 0);
|
||||
})
|
||||
->update([
|
||||
'enterpriseId' => $targetEnterpriseId,
|
||||
]);
|
||||
|
||||
if ($clonePersonalProfile && !empty($distinctUserIds)) {
|
||||
foreach ($distinctUserIds as $uid) {
|
||||
$this->ensureEnterpriseProfileFromPersonal((int) $uid, $targetEnterpriseId, $now);
|
||||
}
|
||||
}
|
||||
|
||||
Db::commit();
|
||||
} catch (\Throwable $e) {
|
||||
Db::rollback();
|
||||
return error('归并失败:' . $e->getMessage(), 500);
|
||||
}
|
||||
|
||||
$preview['executed'] = true;
|
||||
$preview['hint'] = '已写入。无企业归属的测试与用户已归属到目标企业。';
|
||||
return success($preview, '归并完成');
|
||||
}
|
||||
}
|
||||
@@ -1,437 +1,551 @@
|
||||
<?php
|
||||
namespace app\controller\superadmin;
|
||||
|
||||
use app\BaseController;
|
||||
use app\model\Enterprise as EnterpriseModel;
|
||||
use think\facade\Request;
|
||||
use think\facade\Db;
|
||||
|
||||
/**
|
||||
* 企业管理控制器(超管专用)
|
||||
*/
|
||||
class Enterprise extends BaseController
|
||||
{
|
||||
/**
|
||||
* 获取企业列表
|
||||
* @return \think\response\Json
|
||||
*/
|
||||
public function index()
|
||||
{
|
||||
// 验证是否为超级管理员
|
||||
$user = $this->request->user ?? null;
|
||||
if (!$user || $user['role'] !== 'superadmin') {
|
||||
return error('无权限访问', 403);
|
||||
}
|
||||
|
||||
$page = Request::param('page', 1);
|
||||
$pageSize = Request::param('pageSize', 20);
|
||||
$keyword = Request::param('keyword', '');
|
||||
$status = Request::param('status', '');
|
||||
|
||||
$where = [];
|
||||
|
||||
// 搜索条件
|
||||
if ($keyword) {
|
||||
$where[] = ['name|contactName|contactPhone|code', 'like', '%' . $keyword . '%'];
|
||||
}
|
||||
|
||||
// 状态筛选
|
||||
if ($status !== '') {
|
||||
$where['status'] = $status;
|
||||
}
|
||||
|
||||
// 查询企业列表
|
||||
$list = EnterpriseModel::where($where)
|
||||
->order('createdAt', 'desc')
|
||||
->page($page, $pageSize)
|
||||
->select()
|
||||
->toArray();
|
||||
|
||||
// 统计每个企业的用户数和测试用量
|
||||
foreach ($list as &$item) {
|
||||
// 统计用户数(只统计未删除的用户)
|
||||
$item['userCount'] = Db::name('users')
|
||||
->where('enterpriseId', $item['id'])
|
||||
->where('deletedAt', null)
|
||||
->count();
|
||||
|
||||
// 统计测试用量(测试结果数)- 通过企业下的用户ID统计(只统计未删除的用户)
|
||||
$userIds = Db::name('users')
|
||||
->where('enterpriseId', $item['id'])
|
||||
->where('deletedAt', null)
|
||||
->column('id');
|
||||
|
||||
if (!empty($userIds)) {
|
||||
$item['testUsage'] = Db::name('test_results')
|
||||
->where('userId', 'in', $userIds)
|
||||
->count();
|
||||
} else {
|
||||
$item['testUsage'] = 0;
|
||||
}
|
||||
}
|
||||
|
||||
$total = EnterpriseModel::where($where)->count();
|
||||
|
||||
// 统计活跃企业数(status为operating)
|
||||
$activeCount = EnterpriseModel::where('status', 'operating')->count();
|
||||
|
||||
return success([
|
||||
'list' => $list,
|
||||
'total' => $total,
|
||||
'page' => $page,
|
||||
'pageSize' => $pageSize,
|
||||
'activeCount' => $activeCount
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取企业详情
|
||||
* @param int $id
|
||||
* @return \think\response\Json
|
||||
*/
|
||||
public function detail($id = null)
|
||||
{
|
||||
// 验证是否为超级管理员
|
||||
$user = $this->request->user ?? null;
|
||||
if (!$user || $user['role'] !== 'superadmin') {
|
||||
return error('无权限访问', 403);
|
||||
}
|
||||
|
||||
// 如果路由参数没有传递,尝试从请求参数获取
|
||||
if (empty($id)) {
|
||||
$id = Request::param('id');
|
||||
}
|
||||
|
||||
if (empty($id)) {
|
||||
return error('企业ID不能为空', 400);
|
||||
}
|
||||
|
||||
$enterprise = EnterpriseModel::find($id);
|
||||
|
||||
if (!$enterprise) {
|
||||
return error('企业不存在', 404);
|
||||
}
|
||||
|
||||
$data = $enterprise->toArray();
|
||||
|
||||
// 获取企业下的所有用户ID(只统计未删除的用户)
|
||||
$userIds = Db::name('users')
|
||||
->where('enterpriseId', $id)
|
||||
->where('deletedAt', null)
|
||||
->column('id');
|
||||
|
||||
// 统计用户数
|
||||
$data['userCount'] = count($userIds);
|
||||
|
||||
// 获取管理员账号列表(企业管理员角色,只获取未删除的)
|
||||
$adminAccounts = Db::name('users')
|
||||
->where('enterpriseId', $id)
|
||||
->where('role', 'enterprise_admin')
|
||||
->where('deletedAt', null)
|
||||
->field('id,username,email,phone,role,status,createdAt,lastLoginTime')
|
||||
->select()
|
||||
->toArray();
|
||||
$data['adminAccounts'] = $adminAccounts;
|
||||
|
||||
// 获取用户列表(排除管理员,只获取未删除的)
|
||||
$users = Db::name('users')
|
||||
->where('enterpriseId', $id)
|
||||
->where('role', '<>', 'enterprise_admin')
|
||||
->where('deletedAt', null)
|
||||
->field('id,username,email,phone,mbtiType,status,createdAt')
|
||||
->limit(50) // 限制返回数量
|
||||
->select()
|
||||
->toArray();
|
||||
$data['users'] = $users;
|
||||
|
||||
// 获取测试结果列表
|
||||
$testResults = [];
|
||||
if (!empty($userIds)) {
|
||||
$testResults = Db::name('test_results')
|
||||
->alias('tr')
|
||||
->leftJoin('users u', 'tr.userId = u.id')
|
||||
->where('tr.userId', 'in', $userIds)
|
||||
->field('tr.id,tr.testType,tr.createdAt,u.username')
|
||||
->order('tr.createdAt', 'desc')
|
||||
->limit(50) // 限制返回数量
|
||||
->select()
|
||||
->toArray();
|
||||
}
|
||||
$data['testResults'] = $testResults;
|
||||
|
||||
// 统计测试用量
|
||||
if (!empty($userIds)) {
|
||||
$data['testUsage'] = Db::name('test_results')
|
||||
->where('userId', 'in', $userIds)
|
||||
->count();
|
||||
} else {
|
||||
$data['testUsage'] = 0;
|
||||
}
|
||||
|
||||
return success($data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建企业
|
||||
* @return \think\response\Json
|
||||
*/
|
||||
public function create()
|
||||
{
|
||||
// 验证是否为超级管理员
|
||||
$user = $this->request->user ?? null;
|
||||
if (!$user || $user['role'] !== 'superadmin') {
|
||||
return error('无权限访问', 403);
|
||||
}
|
||||
|
||||
$data = Request::post();
|
||||
|
||||
// 验证必填字段
|
||||
if (empty($data['name'])) {
|
||||
return error('企业名称不能为空', 400);
|
||||
}
|
||||
|
||||
// 验证管理员账号信息
|
||||
if (empty($data['adminUsername'])) {
|
||||
return error('管理员用户名不能为空', 400);
|
||||
}
|
||||
|
||||
if (empty($data['adminPassword'])) {
|
||||
return error('管理员密码不能为空', 400);
|
||||
}
|
||||
|
||||
if (strlen($data['adminPassword']) < 6) {
|
||||
return error('密码长度至少6位', 400);
|
||||
}
|
||||
|
||||
// 检查企业代码是否重复(如果提供了代码)
|
||||
if (!empty($data['code'])) {
|
||||
if (EnterpriseModel::where('code', $data['code'])->find()) {
|
||||
return error('企业代码已存在', 400);
|
||||
}
|
||||
}
|
||||
|
||||
// 检查管理员用户名是否已存在
|
||||
if (Db::name('users')->where('username', $data['adminUsername'])->find()) {
|
||||
return error('管理员用户名已存在', 400);
|
||||
}
|
||||
|
||||
// 状态映射(前端使用operating/trial/disabled)
|
||||
$status = $data['status'] ?? 'operating';
|
||||
if (!in_array($status, ['operating', 'trial', 'disabled'])) {
|
||||
$status = 'operating';
|
||||
}
|
||||
|
||||
// 验证试用到期时间
|
||||
if ($status === 'trial') {
|
||||
if (empty($data['trialExpireAt'])) {
|
||||
return error('选择试用状态时,必须设置试用到期时间', 400);
|
||||
}
|
||||
// 确保到期时间大于当前时间
|
||||
if ($data['trialExpireAt'] <= time()) {
|
||||
return error('试用到期时间必须大于当前时间', 400);
|
||||
}
|
||||
}
|
||||
|
||||
// 开启事务
|
||||
Db::startTrans();
|
||||
try {
|
||||
// 创建企业
|
||||
$enterprise = new EnterpriseModel();
|
||||
$enterprise->name = $data['name'];
|
||||
$enterprise->code = $data['code'] ?? null;
|
||||
$enterprise->contactName = $data['contactName'] ?? null;
|
||||
$enterprise->contactPhone = $data['contactPhone'] ?? null;
|
||||
$enterprise->contactEmail = $data['contactEmail'] ?? null;
|
||||
$enterprise->balance = $data['balance'] ?? 0.00;
|
||||
$enterprise->status = $status;
|
||||
$enterprise->trialExpireAt = ($status === 'trial' && isset($data['trialExpireAt'])) ? $data['trialExpireAt'] : null;
|
||||
$enterprise->save();
|
||||
|
||||
$enterpriseId = $enterprise->id;
|
||||
|
||||
// 创建企业管理员账号
|
||||
$adminUser = [
|
||||
'username' => $data['adminUsername'],
|
||||
'password' => password_hash($data['adminPassword'], PASSWORD_DEFAULT),
|
||||
'email' => $data['contactEmail'] ?? null,
|
||||
'phone' => $data['contactPhone'] ?? null,
|
||||
'role' => 'enterprise_admin',
|
||||
'enterpriseId' => $enterpriseId,
|
||||
'status' => 1,
|
||||
'createdAt' => time(),
|
||||
'updatedAt' => time()
|
||||
];
|
||||
|
||||
Db::name('users')->insert($adminUser);
|
||||
|
||||
// 提交事务
|
||||
Db::commit();
|
||||
|
||||
$enterpriseData = $enterprise->toArray();
|
||||
$enterpriseData['userCount'] = 1; // 刚创建的企业管理员
|
||||
$enterpriseData['testUsage'] = 0;
|
||||
|
||||
return success($enterpriseData, '企业创建成功,管理员账号已创建');
|
||||
} catch (\Exception $e) {
|
||||
// 回滚事务
|
||||
Db::rollback();
|
||||
return error('创建失败:' . $e->getMessage(), 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新企业
|
||||
* @param int $id
|
||||
* @return \think\response\Json
|
||||
*/
|
||||
public function update($id)
|
||||
{
|
||||
// 验证是否为超级管理员
|
||||
$user = $this->request->user ?? null;
|
||||
if (!$user || $user['role'] !== 'superadmin') {
|
||||
return error('无权限访问', 403);
|
||||
}
|
||||
|
||||
$enterprise = EnterpriseModel::find($id);
|
||||
|
||||
if (!$enterprise) {
|
||||
return error('企业不存在', 404);
|
||||
}
|
||||
|
||||
$data = Request::put();
|
||||
$oldBalance = (float) ($enterprise->balance ?? 0);
|
||||
|
||||
// 如果更新企业代码,检查是否重复
|
||||
if (isset($data['code']) && $data['code'] != $enterprise->code) {
|
||||
if (EnterpriseModel::where('code', $data['code'])->find()) {
|
||||
return error('企业代码已存在', 400);
|
||||
}
|
||||
}
|
||||
|
||||
// 状态验证
|
||||
if (isset($data['status']) && !in_array($data['status'], ['operating', 'trial', 'disabled'])) {
|
||||
return error('状态值无效', 400);
|
||||
}
|
||||
|
||||
// 验证试用到期时间
|
||||
$status = $data['status'] ?? $enterprise->status;
|
||||
if ($status === 'trial') {
|
||||
if (empty($data['trialExpireAt'])) {
|
||||
return error('选择试用状态时,必须设置试用到期时间', 400);
|
||||
}
|
||||
// 确保到期时间大于当前时间
|
||||
if ($data['trialExpireAt'] <= time()) {
|
||||
return error('试用到期时间必须大于当前时间', 400);
|
||||
}
|
||||
$enterprise->trialExpireAt = $data['trialExpireAt'];
|
||||
} else {
|
||||
// 如果不是试用状态,清空到期时间
|
||||
$enterprise->trialExpireAt = null;
|
||||
}
|
||||
|
||||
$enterprise->save($data);
|
||||
|
||||
$newBalance = (float) ($enterprise->balance ?? 0);
|
||||
if ($newBalance > $oldBalance) {
|
||||
try {
|
||||
\app\controller\api\Distribution::unfreezeCommissions((int) $id);
|
||||
} catch (\Throwable $e) {
|
||||
// 余额已更新成功,解冻失败不阻断主流程
|
||||
}
|
||||
}
|
||||
|
||||
$enterpriseData = $enterprise->toArray();
|
||||
|
||||
// 统计用户数和测试用量(只统计未删除的用户)
|
||||
$enterpriseData['userCount'] = Db::name('users')
|
||||
->where('enterpriseId', $id)
|
||||
->where('deletedAt', null)
|
||||
->count();
|
||||
|
||||
$userIds = Db::name('users')
|
||||
->where('enterpriseId', $id)
|
||||
->where('deletedAt', null)
|
||||
->column('id');
|
||||
|
||||
if (!empty($userIds)) {
|
||||
$enterpriseData['testUsage'] = Db::name('test_results')
|
||||
->where('userId', 'in', $userIds)
|
||||
->count();
|
||||
} else {
|
||||
$enterpriseData['testUsage'] = 0;
|
||||
}
|
||||
|
||||
return success($enterpriseData, '更新成功');
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除企业(软删除)
|
||||
* @param int $id
|
||||
* @return \think\response\Json
|
||||
*/
|
||||
public function delete($id)
|
||||
{
|
||||
// 验证是否为超级管理员
|
||||
$user = $this->request->user ?? null;
|
||||
if (!$user || $user['role'] !== 'superadmin') {
|
||||
return error('无权限访问', 403);
|
||||
}
|
||||
|
||||
$enterprise = EnterpriseModel::find($id);
|
||||
|
||||
if (!$enterprise) {
|
||||
return error('企业不存在', 404);
|
||||
}
|
||||
|
||||
// 检查是否已删除
|
||||
if ($enterprise->deletedAt) {
|
||||
return error('企业已被删除', 400);
|
||||
}
|
||||
|
||||
// 检查是否有用户关联(只检查未删除的用户)
|
||||
$userCount = Db::name('users')
|
||||
->where('enterpriseId', $id)
|
||||
->where('deletedAt', null)
|
||||
->count();
|
||||
if ($userCount > 0) {
|
||||
return error('该企业下还有用户,无法删除', 400);
|
||||
}
|
||||
|
||||
// 软删除(设置 deletedAt 时间戳)
|
||||
$enterprise->delete();
|
||||
|
||||
return success(null, '删除成功');
|
||||
}
|
||||
|
||||
/**
|
||||
* 启用/禁用企业
|
||||
* @param int $id
|
||||
* @return \think\response\Json
|
||||
*/
|
||||
public function toggleStatus($id)
|
||||
{
|
||||
// 验证是否为超级管理员
|
||||
$user = $this->request->user ?? null;
|
||||
if (!$user || $user['role'] !== 'superadmin') {
|
||||
return error('无权限访问', 403);
|
||||
}
|
||||
|
||||
$enterprise = EnterpriseModel::find($id);
|
||||
|
||||
if (!$enterprise) {
|
||||
return error('企业不存在', 404);
|
||||
}
|
||||
|
||||
// 切换状态:operating <-> disabled
|
||||
if ($enterprise->status === 'operating') {
|
||||
$enterprise->status = 'disabled';
|
||||
} else {
|
||||
$enterprise->status = 'operating';
|
||||
}
|
||||
|
||||
$enterprise->save();
|
||||
|
||||
return success($enterprise, '操作成功');
|
||||
}
|
||||
}
|
||||
|
||||
<?php
|
||||
namespace app\controller\superadmin;
|
||||
|
||||
use app\BaseController;
|
||||
use app\model\Enterprise as EnterpriseModel;
|
||||
use app\model\SystemConfig as SystemConfigModel;
|
||||
use think\facade\Request;
|
||||
use think\facade\Db;
|
||||
|
||||
/**
|
||||
* 企业管理控制器(超管专用)
|
||||
*/
|
||||
class Enterprise extends BaseController
|
||||
{
|
||||
/**
|
||||
* 获取企业列表
|
||||
* @return \think\response\Json
|
||||
*/
|
||||
public function index()
|
||||
{
|
||||
// 验证是否为超级管理员
|
||||
$user = $this->request->user ?? null;
|
||||
if (!$user || $user['role'] !== 'superadmin') {
|
||||
return error('无权限访问', 403);
|
||||
}
|
||||
|
||||
$page = Request::param('page', 1);
|
||||
$pageSize = Request::param('pageSize', 20);
|
||||
$keyword = Request::param('keyword', '');
|
||||
$status = Request::param('status', '');
|
||||
|
||||
$where = [];
|
||||
|
||||
// 搜索条件
|
||||
if ($keyword) {
|
||||
$where[] = ['name|contactName|contactPhone|code', 'like', '%' . $keyword . '%'];
|
||||
}
|
||||
|
||||
// 状态筛选
|
||||
if ($status !== '') {
|
||||
$where['status'] = $status;
|
||||
}
|
||||
|
||||
// 查询企业列表
|
||||
$list = EnterpriseModel::where($where)
|
||||
->order('createdAt', 'desc')
|
||||
->page($page, $pageSize)
|
||||
->select()
|
||||
->toArray();
|
||||
|
||||
// 统计每个企业的用户数和测试用量
|
||||
foreach ($list as &$item) {
|
||||
// 统计用户数(只统计未删除的用户)
|
||||
$item['userCount'] = Db::name('users')
|
||||
->where('enterpriseId', $item['id'])
|
||||
->where('deletedAt', null)
|
||||
->count();
|
||||
|
||||
// 统计测试用量(测试结果数)- 通过企业下的用户ID统计(只统计未删除的用户)
|
||||
$userIds = Db::name('users')
|
||||
->where('enterpriseId', $item['id'])
|
||||
->where('deletedAt', null)
|
||||
->column('id');
|
||||
|
||||
if (!empty($userIds)) {
|
||||
$item['testUsage'] = Db::name('test_results')
|
||||
->where('userId', 'in', $userIds)
|
||||
->count();
|
||||
} else {
|
||||
$item['testUsage'] = 0;
|
||||
}
|
||||
}
|
||||
|
||||
$total = EnterpriseModel::where($where)->count();
|
||||
|
||||
// 统计活跃企业数(status为operating)
|
||||
$activeCount = EnterpriseModel::where('status', 'operating')->count();
|
||||
|
||||
return success([
|
||||
'list' => $list,
|
||||
'total' => $total,
|
||||
'page' => $page,
|
||||
'pageSize' => $pageSize,
|
||||
'activeCount' => $activeCount
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取企业详情
|
||||
* @param int $id
|
||||
* @return \think\response\Json
|
||||
*/
|
||||
public function detail($id = null)
|
||||
{
|
||||
// 验证是否为超级管理员
|
||||
$user = $this->request->user ?? null;
|
||||
if (!$user || $user['role'] !== 'superadmin') {
|
||||
return error('无权限访问', 403);
|
||||
}
|
||||
|
||||
// 如果路由参数没有传递,尝试从请求参数获取
|
||||
if (empty($id)) {
|
||||
$id = Request::param('id');
|
||||
}
|
||||
|
||||
if (empty($id)) {
|
||||
return error('企业ID不能为空', 400);
|
||||
}
|
||||
|
||||
$enterprise = EnterpriseModel::find($id);
|
||||
|
||||
if (!$enterprise) {
|
||||
return error('企业不存在', 404);
|
||||
}
|
||||
|
||||
$data = $enterprise->toArray();
|
||||
|
||||
// 获取企业下的所有用户ID(只统计未删除的用户)
|
||||
$userIds = Db::name('users')
|
||||
->where('enterpriseId', $id)
|
||||
->where('deletedAt', null)
|
||||
->column('id');
|
||||
|
||||
// 统计用户数
|
||||
$data['userCount'] = count($userIds);
|
||||
|
||||
// 获取管理员账号列表(企业管理员角色,只获取未删除的)
|
||||
$adminAccounts = Db::name('users')
|
||||
->where('enterpriseId', $id)
|
||||
->where('role', 'enterprise_admin')
|
||||
->where('deletedAt', null)
|
||||
->field('id,username,email,phone,role,status,createdAt,lastLoginTime')
|
||||
->select()
|
||||
->toArray();
|
||||
$data['adminAccounts'] = $adminAccounts;
|
||||
|
||||
// 获取用户列表(排除管理员,只获取未删除的)
|
||||
$users = Db::name('users')
|
||||
->where('enterpriseId', $id)
|
||||
->where('role', '<>', 'enterprise_admin')
|
||||
->where('deletedAt', null)
|
||||
->field('id,username,email,phone,mbtiType,status,createdAt')
|
||||
->limit(50) // 限制返回数量
|
||||
->select()
|
||||
->toArray();
|
||||
$data['users'] = $users;
|
||||
|
||||
// 获取测试结果列表
|
||||
$testResults = [];
|
||||
if (!empty($userIds)) {
|
||||
$testResults = Db::name('test_results')
|
||||
->alias('tr')
|
||||
->leftJoin('users u', 'tr.userId = u.id')
|
||||
->where('tr.userId', 'in', $userIds)
|
||||
->field('tr.id,tr.testType,tr.createdAt,u.username')
|
||||
->order('tr.createdAt', 'desc')
|
||||
->limit(50) // 限制返回数量
|
||||
->select()
|
||||
->toArray();
|
||||
}
|
||||
$data['testResults'] = $testResults;
|
||||
|
||||
// 统计测试用量
|
||||
if (!empty($userIds)) {
|
||||
$data['testUsage'] = Db::name('test_results')
|
||||
->where('userId', 'in', $userIds)
|
||||
->count();
|
||||
} else {
|
||||
$data['testUsage'] = 0;
|
||||
}
|
||||
|
||||
// —— 小程序侧用户(wechat_users.enterpriseId)——
|
||||
$wechatIds = [];
|
||||
try {
|
||||
$wechatIds = Db::name('wechat_users')->where('enterpriseId', $id)->column('id');
|
||||
$wechatIds = array_values(array_filter($wechatIds));
|
||||
} catch (\Throwable $e) {
|
||||
$wechatIds = [];
|
||||
}
|
||||
$data['wechatUserCount'] = count($wechatIds);
|
||||
$data['wechatUsers'] = [];
|
||||
if (!empty($wechatIds)) {
|
||||
try {
|
||||
$data['wechatUsers'] = Db::name('wechat_users')
|
||||
->where('id', 'in', $wechatIds)
|
||||
->field('id,openid,nickname,phone,avatar,status,lastLoginAt,createdAt')
|
||||
->order('createdAt', 'desc')
|
||||
->limit(120)
|
||||
->select()
|
||||
->toArray();
|
||||
} catch (\Throwable $e) {
|
||||
$data['wechatUsers'] = [];
|
||||
}
|
||||
}
|
||||
|
||||
// 该企业下、带 enterpriseId 的小程序测试记录
|
||||
$data['miniprogramTestResults'] = [];
|
||||
try {
|
||||
$data['miniprogramTestResults'] = Db::name('test_results')
|
||||
->alias('tr')
|
||||
->leftJoin('wechat_users w', 'tr.userId = w.id')
|
||||
->where('tr.enterpriseId', $id)
|
||||
->field('tr.id,tr.testType,tr.createdAt,tr.userId,w.nickname as wechatNickname')
|
||||
->order('tr.createdAt', 'desc')
|
||||
->limit(60)
|
||||
->select()
|
||||
->toArray();
|
||||
} catch (\Throwable $e) {
|
||||
$data['miniprogramTestResults'] = [];
|
||||
}
|
||||
|
||||
// 订单与消耗(金额分)
|
||||
$paidStatuses = ['paid', 'completed'];
|
||||
try {
|
||||
$data['orderStats'] = [
|
||||
'totalCount' => (int) Db::name('orders')->where('enterpriseId', $id)->count(),
|
||||
'paidCount' => (int) Db::name('orders')->where('enterpriseId', $id)->whereIn('status', $paidStatuses)->count(),
|
||||
'paidAmountFen' => (int) (Db::name('orders')->where('enterpriseId', $id)->whereIn('status', $paidStatuses)->sum('amount') ?? 0),
|
||||
];
|
||||
$data['recentOrders'] = Db::name('orders')
|
||||
->where('enterpriseId', $id)
|
||||
->order('createdAt', 'desc')
|
||||
->limit(25)
|
||||
->field('id,orderNo,status,amount,productType,userId,createdAt')
|
||||
->select()
|
||||
->toArray();
|
||||
} catch (\Throwable $e) {
|
||||
$data['orderStats'] = [
|
||||
'totalCount' => 0,
|
||||
'paidCount' => 0,
|
||||
'paidAmountFen' => 0,
|
||||
];
|
||||
$data['recentOrders'] = [];
|
||||
}
|
||||
|
||||
// 埋点:近 30 天,归属该企业的小程序用户
|
||||
$data['analyticsStats'] = [
|
||||
'eventTotal' => 0,
|
||||
'pageViewCount' => 0,
|
||||
'byEvent' => [],
|
||||
'hint' => null,
|
||||
'windowDays' => 30,
|
||||
];
|
||||
if (empty($wechatIds)) {
|
||||
$data['analyticsStats']['hint'] = '暂无 enterpriseId 归属该企业的微信小程序用户,无法按企业聚合埋点';
|
||||
} else {
|
||||
try {
|
||||
$since = date('Y-m-d H:i:s', time() - 30 * 86400);
|
||||
$data['analyticsStats']['eventTotal'] = (int) Db::name('analytics_events')
|
||||
->where('userId', 'in', $wechatIds)
|
||||
->where('createdAt', '>=', $since)
|
||||
->count();
|
||||
$data['analyticsStats']['pageViewCount'] = (int) Db::name('analytics_events')
|
||||
->where('userId', 'in', $wechatIds)
|
||||
->where('createdAt', '>=', $since)
|
||||
->where('eventName', 'page_view')
|
||||
->count();
|
||||
$byEvent = Db::name('analytics_events')
|
||||
->where('userId', 'in', $wechatIds)
|
||||
->where('createdAt', '>=', $since)
|
||||
->field('eventName, COUNT(*) AS cnt')
|
||||
->group('eventName')
|
||||
->order('cnt', 'desc')
|
||||
->limit(20)
|
||||
->select()
|
||||
->toArray();
|
||||
$data['analyticsStats']['byEvent'] = $byEvent ?: [];
|
||||
} catch (\Throwable $e) {
|
||||
$data['analyticsStats']['hint'] = '埋点表未就绪或查询失败(请确认已建 analytics_events 表)';
|
||||
}
|
||||
}
|
||||
|
||||
// 全局通知策略(超管在系统设置中配置,影响余额类提醒等)
|
||||
$data['notificationPolicy'] = null;
|
||||
try {
|
||||
$nc = SystemConfigModel::where('key', 'notification')->where('enterprise_id', 0)->find();
|
||||
if ($nc) {
|
||||
$val = $nc->getAttr('value');
|
||||
$data['notificationPolicy'] = is_array($val) ? $val : null;
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
$data['notificationPolicy'] = null;
|
||||
}
|
||||
|
||||
return success($data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建企业
|
||||
* @return \think\response\Json
|
||||
*/
|
||||
public function create()
|
||||
{
|
||||
// 验证是否为超级管理员
|
||||
$user = $this->request->user ?? null;
|
||||
if (!$user || $user['role'] !== 'superadmin') {
|
||||
return error('无权限访问', 403);
|
||||
}
|
||||
|
||||
$data = Request::post();
|
||||
|
||||
// 验证必填字段
|
||||
if (empty($data['name'])) {
|
||||
return error('企业名称不能为空', 400);
|
||||
}
|
||||
|
||||
// 验证管理员账号信息
|
||||
if (empty($data['adminUsername'])) {
|
||||
return error('管理员用户名不能为空', 400);
|
||||
}
|
||||
|
||||
if (empty($data['adminPassword'])) {
|
||||
return error('管理员密码不能为空', 400);
|
||||
}
|
||||
|
||||
if (strlen($data['adminPassword']) < 6) {
|
||||
return error('密码长度至少6位', 400);
|
||||
}
|
||||
|
||||
// 检查企业代码是否重复(如果提供了代码)
|
||||
if (!empty($data['code'])) {
|
||||
if (EnterpriseModel::where('code', $data['code'])->find()) {
|
||||
return error('企业代码已存在', 400);
|
||||
}
|
||||
}
|
||||
|
||||
// 检查管理员用户名是否已存在
|
||||
if (Db::name('users')->where('username', $data['adminUsername'])->find()) {
|
||||
return error('管理员用户名已存在', 400);
|
||||
}
|
||||
|
||||
// 状态映射(前端使用operating/trial/disabled)
|
||||
$status = $data['status'] ?? 'operating';
|
||||
if (!in_array($status, ['operating', 'trial', 'disabled'])) {
|
||||
$status = 'operating';
|
||||
}
|
||||
|
||||
// 验证试用到期时间
|
||||
if ($status === 'trial') {
|
||||
if (empty($data['trialExpireAt'])) {
|
||||
return error('选择试用状态时,必须设置试用到期时间', 400);
|
||||
}
|
||||
// 确保到期时间大于当前时间
|
||||
if ($data['trialExpireAt'] <= time()) {
|
||||
return error('试用到期时间必须大于当前时间', 400);
|
||||
}
|
||||
}
|
||||
|
||||
// 开启事务
|
||||
Db::startTrans();
|
||||
try {
|
||||
// 创建企业
|
||||
$enterprise = new EnterpriseModel();
|
||||
$enterprise->name = $data['name'];
|
||||
$enterprise->code = $data['code'] ?? null;
|
||||
$enterprise->contactName = $data['contactName'] ?? null;
|
||||
$enterprise->contactPhone = $data['contactPhone'] ?? null;
|
||||
$enterprise->contactEmail = $data['contactEmail'] ?? null;
|
||||
$enterprise->balance = $data['balance'] ?? 0.00;
|
||||
$enterprise->status = $status;
|
||||
$enterprise->trialExpireAt = ($status === 'trial' && isset($data['trialExpireAt'])) ? $data['trialExpireAt'] : null;
|
||||
$enterprise->save();
|
||||
|
||||
$enterpriseId = $enterprise->id;
|
||||
|
||||
// 创建企业管理员账号
|
||||
$adminUser = [
|
||||
'username' => $data['adminUsername'],
|
||||
'password' => password_hash($data['adminPassword'], PASSWORD_DEFAULT),
|
||||
'email' => $data['contactEmail'] ?? null,
|
||||
'phone' => $data['contactPhone'] ?? null,
|
||||
'role' => 'enterprise_admin',
|
||||
'enterpriseId' => $enterpriseId,
|
||||
'status' => 1,
|
||||
'createdAt' => time(),
|
||||
'updatedAt' => time()
|
||||
];
|
||||
|
||||
Db::name('users')->insert($adminUser);
|
||||
|
||||
// 提交事务
|
||||
Db::commit();
|
||||
|
||||
$enterpriseData = $enterprise->toArray();
|
||||
$enterpriseData['userCount'] = 1; // 刚创建的企业管理员
|
||||
$enterpriseData['testUsage'] = 0;
|
||||
|
||||
return success($enterpriseData, '企业创建成功,管理员账号已创建');
|
||||
} catch (\Exception $e) {
|
||||
// 回滚事务
|
||||
Db::rollback();
|
||||
return error('创建失败:' . $e->getMessage(), 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新企业
|
||||
* @param int $id
|
||||
* @return \think\response\Json
|
||||
*/
|
||||
public function update($id)
|
||||
{
|
||||
// 验证是否为超级管理员
|
||||
$user = $this->request->user ?? null;
|
||||
if (!$user || $user['role'] !== 'superadmin') {
|
||||
return error('无权限访问', 403);
|
||||
}
|
||||
|
||||
$enterprise = EnterpriseModel::find($id);
|
||||
|
||||
if (!$enterprise) {
|
||||
return error('企业不存在', 404);
|
||||
}
|
||||
|
||||
$data = Request::put();
|
||||
$oldBalance = (float) ($enterprise->balance ?? 0);
|
||||
|
||||
// 如果更新企业代码,检查是否重复
|
||||
if (isset($data['code']) && $data['code'] != $enterprise->code) {
|
||||
if (EnterpriseModel::where('code', $data['code'])->find()) {
|
||||
return error('企业代码已存在', 400);
|
||||
}
|
||||
}
|
||||
|
||||
// 状态验证
|
||||
if (isset($data['status']) && !in_array($data['status'], ['operating', 'trial', 'disabled'])) {
|
||||
return error('状态值无效', 400);
|
||||
}
|
||||
|
||||
// 验证试用到期时间
|
||||
$status = $data['status'] ?? $enterprise->status;
|
||||
if ($status === 'trial') {
|
||||
if (empty($data['trialExpireAt'])) {
|
||||
return error('选择试用状态时,必须设置试用到期时间', 400);
|
||||
}
|
||||
// 确保到期时间大于当前时间
|
||||
if ($data['trialExpireAt'] <= time()) {
|
||||
return error('试用到期时间必须大于当前时间', 400);
|
||||
}
|
||||
$enterprise->trialExpireAt = $data['trialExpireAt'];
|
||||
} else {
|
||||
// 如果不是试用状态,清空到期时间
|
||||
$enterprise->trialExpireAt = null;
|
||||
}
|
||||
|
||||
$enterprise->save($data);
|
||||
|
||||
$newBalance = (float) ($enterprise->balance ?? 0);
|
||||
if ($newBalance > $oldBalance) {
|
||||
try {
|
||||
\app\controller\api\Distribution::unfreezeCommissions((int) $id);
|
||||
} catch (\Throwable $e) {
|
||||
// 余额已更新成功,解冻失败不阻断主流程
|
||||
}
|
||||
}
|
||||
|
||||
$enterpriseData = $enterprise->toArray();
|
||||
|
||||
// 统计用户数和测试用量(只统计未删除的用户)
|
||||
$enterpriseData['userCount'] = Db::name('users')
|
||||
->where('enterpriseId', $id)
|
||||
->where('deletedAt', null)
|
||||
->count();
|
||||
|
||||
$userIds = Db::name('users')
|
||||
->where('enterpriseId', $id)
|
||||
->where('deletedAt', null)
|
||||
->column('id');
|
||||
|
||||
if (!empty($userIds)) {
|
||||
$enterpriseData['testUsage'] = Db::name('test_results')
|
||||
->where('userId', 'in', $userIds)
|
||||
->count();
|
||||
} else {
|
||||
$enterpriseData['testUsage'] = 0;
|
||||
}
|
||||
|
||||
return success($enterpriseData, '更新成功');
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除企业(软删除)
|
||||
* @param int $id
|
||||
* @return \think\response\Json
|
||||
*/
|
||||
public function delete($id)
|
||||
{
|
||||
// 验证是否为超级管理员
|
||||
$user = $this->request->user ?? null;
|
||||
if (!$user || $user['role'] !== 'superadmin') {
|
||||
return error('无权限访问', 403);
|
||||
}
|
||||
|
||||
$enterprise = EnterpriseModel::find($id);
|
||||
|
||||
if (!$enterprise) {
|
||||
return error('企业不存在', 404);
|
||||
}
|
||||
|
||||
// 检查是否已删除
|
||||
if ($enterprise->deletedAt) {
|
||||
return error('企业已被删除', 400);
|
||||
}
|
||||
|
||||
// 检查是否有用户关联(只检查未删除的用户)
|
||||
$userCount = Db::name('users')
|
||||
->where('enterpriseId', $id)
|
||||
->where('deletedAt', null)
|
||||
->count();
|
||||
if ($userCount > 0) {
|
||||
return error('该企业下还有用户,无法删除', 400);
|
||||
}
|
||||
|
||||
// 软删除(设置 deletedAt 时间戳)
|
||||
$enterprise->delete();
|
||||
|
||||
return success(null, '删除成功');
|
||||
}
|
||||
|
||||
/**
|
||||
* 启用/禁用企业
|
||||
* @param int $id
|
||||
* @return \think\response\Json
|
||||
*/
|
||||
public function toggleStatus($id)
|
||||
{
|
||||
// 验证是否为超级管理员
|
||||
$user = $this->request->user ?? null;
|
||||
if (!$user || $user['role'] !== 'superadmin') {
|
||||
return error('无权限访问', 403);
|
||||
}
|
||||
|
||||
$enterprise = EnterpriseModel::find($id);
|
||||
|
||||
if (!$enterprise) {
|
||||
return error('企业不存在', 404);
|
||||
}
|
||||
|
||||
// 切换状态:operating <-> disabled
|
||||
if ($enterprise->status === 'operating') {
|
||||
$enterprise->status = 'disabled';
|
||||
} else {
|
||||
$enterprise->status = 'operating';
|
||||
}
|
||||
|
||||
$enterprise->save();
|
||||
|
||||
return success($enterprise, '操作成功');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
namespace app\controller\superadmin;
|
||||
|
||||
use app\BaseController;
|
||||
use app\common\service\FeishuLeadWebhookService;
|
||||
use app\model\SystemConfig as SystemConfigModel;
|
||||
use app\model\User as UserModel;
|
||||
use app\model\Enterprise as EnterpriseModel;
|
||||
@@ -276,6 +277,69 @@ class Settings extends BaseController
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 飞书获客 Webhook(与 admin 共用配置)
|
||||
*/
|
||||
public function getFeishuLeadConfig()
|
||||
{
|
||||
$user = $this->request->user ?? null;
|
||||
if (!$user || $user['role'] !== 'superadmin') {
|
||||
return error('无权限访问', 403);
|
||||
}
|
||||
$cfg = FeishuLeadWebhookService::getConfig();
|
||||
return success([
|
||||
'enabled' => !empty($cfg['enabled']),
|
||||
'webhookUrl' => (string) ($cfg['webhookUrl'] ?? ''),
|
||||
'contactPerson' => (string) ($cfg['contactPerson'] ?? '运营'),
|
||||
]);
|
||||
}
|
||||
|
||||
public function updateFeishuLeadConfig()
|
||||
{
|
||||
$user = $this->request->user ?? null;
|
||||
if (!$user || $user['role'] !== 'superadmin') {
|
||||
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, '已保存');
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新超管账户信息
|
||||
* @return \think\response\Json
|
||||
|
||||
Reference in New Issue
Block a user