feat: 管理端与用户端批量更新(订单/邀请双码/tabBar/审核与分销等)

- 管理端:用户详情、企业看板双小程序码、超管设置与用户列表等
- 后端:订单接口、邀请码企业版+个人版、分析与测试相关调整
- 微信小程序/抖音:自定义 tabBar 居中与拍摄钮上移、相机页与结果页、订单页、支付与统计
- 新增 faceResultDetail 工具与 mp 分析迁移脚本

Made-with: Cursor
This commit is contained in:
Ghost
2026-03-28 16:46:00 +08:00
parent 2bb8b979f1
commit 3317c84b5e
65 changed files with 4442 additions and 1839 deletions

View File

@@ -113,6 +113,13 @@ class Enterprise extends BaseController
return error('企业不存在', 404);
}
$wechatPage = max(1, (int) Request::param('wechatPage', 1));
$wechatPageSize = min(100, max(1, (int) Request::param('wechatPageSize', 10)));
$testPage = max(1, (int) Request::param('testPage', 1));
$testPageSize = min(100, max(1, (int) Request::param('testPageSize', 10)));
$orderPage = max(1, (int) Request::param('orderPage', 1));
$orderPageSize = min(100, max(1, (int) Request::param('orderPageSize', 10)));
$data = $enterprise->toArray();
// 获取企业下的所有用户ID只统计未删除的用户
@@ -152,13 +159,14 @@ class Enterprise extends BaseController
->alias('tr')
->leftJoin('users u', 'tr.userId = u.id')
->where('tr.userId', 'in', $userIds)
->field('tr.id,tr.testType,tr.createdAt,u.username')
->field('tr.id,tr.testType,tr.createdAt,tr.userId,tr.resultData,u.username')
->order('tr.createdAt', 'desc')
->limit(50) // 限制返回数量
->select()
->toArray();
}
$data['testResults'] = $testResults;
$this->attachResultSummaries($data['testResults']);
// 统计测试用量
if (!empty($userIds)) {
@@ -177,39 +185,46 @@ class Enterprise extends BaseController
} 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'] = [];
}
$wechatUserTotalCount = 0;
try {
$wechatUserTotalCount = (int) Db::name('wechat_users')->where('enterpriseId', $id)->count();
} catch (\Throwable $e) {
$wechatUserTotalCount = 0;
}
$data['wechatUserCount'] = $wechatUserTotalCount;
$data['wechatUsersTotal'] = $wechatUserTotalCount;
$data['wechatUsers'] = [];
try {
$data['wechatUsers'] = Db::name('wechat_users')
->where('enterpriseId', $id)
->field('id,openid,nickname,phone,avatar,status,lastLoginAt,createdAt')
->order('createdAt', 'desc')
->page($wechatPage, $wechatPageSize)
->select()
->toArray();
} catch (\Throwable $e) {
$data['wechatUsers'] = [];
}
// 该企业下、带 enterpriseId 的小程序测试记录
$data['miniprogramTestResults'] = [];
$data['miniprogramTestResults'] = [];
$data['miniprogramTestResultsTotal'] = 0;
try {
$data['miniprogramTestResultsTotal'] = (int) Db::name('test_results')->where('enterpriseId', $id)->count();
$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')
->field('tr.id,tr.testType,tr.createdAt,tr.userId,tr.resultData,w.nickname as wechatNickname')
->order('tr.createdAt', 'desc')
->limit(60)
->page($testPage, $testPageSize)
->select()
->toArray();
$this->attachResultSummaries($data['miniprogramTestResults']);
} catch (\Throwable $e) {
$data['miniprogramTestResults'] = [];
}
// 订单与消耗(金额分)
// 订单与消耗(金额分)
$paidStatuses = ['paid', 'completed'];
try {
$data['orderStats'] = [
@@ -217,10 +232,11 @@ class Enterprise extends BaseController
'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['recentOrdersTotal'] = (int) Db::name('orders')->where('enterpriseId', $id)->count();
$data['recentOrders'] = Db::name('orders')
->where('enterpriseId', $id)
->order('createdAt', 'desc')
->limit(25)
->page($orderPage, $orderPageSize)
->field('id,orderNo,status,amount,productType,userId,createdAt')
->select()
->toArray();
@@ -230,7 +246,8 @@ class Enterprise extends BaseController
'paidCount' => 0,
'paidAmountFen' => 0,
];
$data['recentOrders'] = [];
$data['recentOrders'] = [];
$data['recentOrdersTotal'] = 0;
}
// 埋点:近 30 天,归属该企业的小程序用户
@@ -547,5 +564,74 @@ class Enterprise extends BaseController
return success($enterprise, '操作成功');
}
/**
* 为列表行附加 resultSummary并移除原始 resultData减小响应体积
*/
private function attachResultSummaries(array &$rows): void
{
foreach ($rows as &$r) {
$type = (string) ($r['testType'] ?? '');
$r['resultSummary'] = $this->summarizeTestResultForAdmin($type, $r['resultData'] ?? null);
unset($r['resultData']);
}
unset($r);
}
/**
* 从 test_results.resultData 解析超管可读短摘要
*/
private function summarizeTestResultForAdmin(string $testType, $resultData): string
{
if ($resultData === null || $resultData === '') {
return '—';
}
$decoded = is_string($resultData) ? json_decode($resultData, true) : $resultData;
if (!is_array($decoded)) {
return '—';
}
$testType = strtolower($testType);
switch ($testType) {
case 'mbti':
if (!empty($decoded['mbtiType'])) {
return (string) $decoded['mbtiType'];
}
if (!empty($decoded['mbti']['type'])) {
return (string) $decoded['mbti']['type'];
}
return '—';
case 'disc':
$d = trim((string) ($decoded['dominantType'] ?? ''));
$s = trim((string) ($decoded['secondaryType'] ?? ''));
$line = $d . ($d !== '' && $s !== '' ? ' + ' : '') . $s;
return $line !== '' ? $line : '—';
case 'pdp':
$d = trim((string) ($decoded['dominantType'] ?? ''));
return $d !== '' ? $d : '—';
case 'face':
case 'ai':
$parts = [];
if (!empty($decoded['mbti']['type'])) {
$parts[] = 'MBTI ' . $decoded['mbti']['type'];
}
if (!empty($decoded['pdp']['primary'])) {
$parts[] = 'PDP ' . $decoded['pdp']['primary'];
}
if (!empty($decoded['disc']['primary'])) {
$parts[] = 'DISC ' . $decoded['disc']['primary'];
}
if ($parts !== []) {
return implode(' · ', $parts);
}
$sum = $decoded['personalitySummary'] ?? $decoded['overview'] ?? '';
$sum = is_string($sum) ? trim($sum) : '';
if ($sum !== '') {
return mb_strlen($sum) > 48 ? mb_substr($sum, 0, 48) . '…' : $sum;
}
return '面相/智能分析';
default:
return '—';
}
}
}