feat: 管理端与用户端批量更新(订单/邀请双码/tabBar/审核与分销等)
- 管理端:用户详情、企业看板双小程序码、超管设置与用户列表等 - 后端:订单接口、邀请码企业版+个人版、分析与测试相关调整 - 微信小程序/抖音:自定义 tabBar 居中与拍摄钮上移、相机页与结果页、订单页、支付与统计 - 新增 faceResultDetail 工具与 mp 分析迁移脚本 Made-with: Cursor
This commit is contained in:
@@ -35,24 +35,31 @@ class AppUser extends BaseController
|
||||
return error('无权限访问', 403);
|
||||
}
|
||||
|
||||
// 用户数按 openid 去重
|
||||
// ========== 统计全部基于 wechat_users.enterpriseId(而非 test_results.enterpriseId) ==========
|
||||
|
||||
// openid 去重:每个 openid 只保留 id 最大的一条
|
||||
try {
|
||||
$totalUsers = (int) Db::name('wechat_users')->count('openid', true);
|
||||
$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) {
|
||||
$totalUsers = (int) Db::name('wechat_users')->count();
|
||||
$dedupIds = Db::name('wechat_users')->column('id');
|
||||
$dedupIds = $dedupIds ? array_values(array_filter($dedupIds)) : [];
|
||||
}
|
||||
$totalUsers = count($dedupIds);
|
||||
$last30d = time() - 30 * 86400;
|
||||
|
||||
// 全部池:去重后的测试用户 & 近 30 天活跃用户(按 userId 去重)
|
||||
// 这里使用逻辑表名 test_results,底层会自动加前缀生成 mbti_test_results
|
||||
// 已测试用户(在 test_results 有记录的 userId 与 dedupIds 取交集)
|
||||
$testedUserIds = Db::name('test_results')->distinct(true)->column('userId');
|
||||
$testedUsers = count(array_filter($testedUserIds));
|
||||
$testedUserIds = array_values(array_unique(array_filter(array_map('intval', $testedUserIds))));
|
||||
$testedUsers = count(array_intersect($testedUserIds, $dedupIds));
|
||||
|
||||
// 近 30 天活跃用户
|
||||
$activeUserIds = Db::name('test_results')
|
||||
->where('createdAt', '>=', $last30d)
|
||||
->distinct(true)
|
||||
->column('userId');
|
||||
$activeUsers = count(array_filter($activeUserIds));
|
||||
$activeUserIds = array_values(array_unique(array_filter(array_map('intval', $activeUserIds))));
|
||||
$activeUsers = count(array_intersect($activeUserIds, $dedupIds));
|
||||
|
||||
$userCards = [
|
||||
[
|
||||
@@ -64,73 +71,44 @@ class AppUser extends BaseController
|
||||
]
|
||||
];
|
||||
|
||||
$individualTotal = 0;
|
||||
$individualActive = 0;
|
||||
try {
|
||||
$individualIds = Db::name('test_results')
|
||||
->where(function ($q) {
|
||||
$q->whereNull('enterpriseId')->whereOr('enterpriseId', '')->whereOr('enterpriseId', 0);
|
||||
})
|
||||
->distinct(true)
|
||||
->column('userId');
|
||||
$individualTotal = count(array_filter($individualIds));
|
||||
|
||||
$individualActiveIds = Db::name('test_results')
|
||||
->where('createdAt', '>=', $last30d)
|
||||
->where(function ($q) {
|
||||
$q->whereNull('enterpriseId')->whereOr('enterpriseId', '')->whereOr('enterpriseId', 0);
|
||||
})
|
||||
->distinct(true)
|
||||
->column('userId');
|
||||
$individualActive = count(array_filter($individualActiveIds));
|
||||
} catch (\Throwable $e) {
|
||||
$individualTotal = 0;
|
||||
$individualActive = 0;
|
||||
}
|
||||
|
||||
$cunkbaoId = $this->resolveCunkbaoEnterpriseId();
|
||||
// 按 wechat_users.enterpriseId 分组统计
|
||||
$userEidMap = Db::name('wechat_users')->where('id', 'in', $dedupIds)->column('enterpriseId', 'id');
|
||||
|
||||
// 按企业统计:从注册表统计 total,再交叉 test_results 统计 active/tested
|
||||
$enterprises = Db::name('enterprises')->field('id,name')->select()->toArray();
|
||||
foreach ($enterprises as $e) {
|
||||
$eid = $e['id'];
|
||||
try {
|
||||
$ids = Db::name('test_results')
|
||||
->where('enterpriseId', $eid)
|
||||
->distinct(true)
|
||||
->column('userId');
|
||||
$total = count(array_filter($ids));
|
||||
$eid = (int) $e['id'];
|
||||
$eidUsers = array_keys(array_filter($userEidMap, function ($v) use ($eid) {
|
||||
return (int) $v === $eid;
|
||||
}));
|
||||
$total = count($eidUsers);
|
||||
$active = empty($eidUsers) ? 0 : count(array_intersect($activeUserIds, $eidUsers));
|
||||
$tested = empty($eidUsers) ? 0 : count(array_intersect($testedUserIds, $eidUsers));
|
||||
|
||||
$activeIds = Db::name('test_results')
|
||||
->where('enterpriseId', $eid)
|
||||
->where('createdAt', '>=', $last30d)
|
||||
->distinct(true)
|
||||
->column('userId');
|
||||
$active = count(array_filter($activeIds));
|
||||
} catch (\Throwable $ex) {
|
||||
$total = 0;
|
||||
$active = 0;
|
||||
}
|
||||
if ($cunkbaoId !== null && (int) $eid === $cunkbaoId) {
|
||||
$total += $individualTotal;
|
||||
$active += $individualActive;
|
||||
}
|
||||
$userCards[] = [
|
||||
'type' => 'enterprise',
|
||||
'enterpriseId' => $eid,
|
||||
'name' => $e['name'] ?? ('企业' . $eid),
|
||||
'total' => $total,
|
||||
'active' => $active,
|
||||
'tested' => $total
|
||||
'tested' => $tested
|
||||
];
|
||||
}
|
||||
|
||||
if ($cunkbaoId === null && ($individualTotal > 0 || $individualActive > 0)) {
|
||||
// 无企业归属的个人用户
|
||||
$individualUsers = array_keys(array_filter($userEidMap, function ($v) {
|
||||
return $v === null || $v === '' || (int) $v === 0;
|
||||
}));
|
||||
$individualTotal = count($individualUsers);
|
||||
if ($individualTotal > 0) {
|
||||
$individualActive = count(array_intersect($activeUserIds, $individualUsers));
|
||||
$individualTested = count(array_intersect($testedUserIds, $individualUsers));
|
||||
$userCards[] = [
|
||||
'type' => 'individual',
|
||||
'name' => '个人用户(无企业)',
|
||||
'total' => $individualTotal,
|
||||
'active' => $individualActive,
|
||||
'tested' => $individualTotal
|
||||
'tested' => $individualTested
|
||||
];
|
||||
}
|
||||
|
||||
@@ -189,7 +167,7 @@ class AppUser extends BaseController
|
||||
|
||||
/**
|
||||
* 测试用户列表:分页、关键词、池筛选、MBTI 筛选
|
||||
* GET /api/v1/superadmin/app-users?page=1&pageSize=20&keyword=&pool=all|individual|enterprise&enterpriseId=&mbti=
|
||||
* GET /api/v1/superadmin/app-users?page=1&pageSize=20&keyword=&pool=all|individual|enterprise&enterpriseId=&mbti=&includeZeroTests=
|
||||
*/
|
||||
public function index()
|
||||
{
|
||||
@@ -206,53 +184,12 @@ class AppUser extends BaseController
|
||||
$enterpriseId = Request::param('enterpriseId', '');
|
||||
$mbti = trim(Request::param('mbti', ''));
|
||||
|
||||
$cunkbaoEnterpriseId = $this->resolveCunkbaoEnterpriseId();
|
||||
if ($pool === 'individual' && $cunkbaoEnterpriseId) {
|
||||
$pool = 'enterprise';
|
||||
$enterpriseId = (string) $cunkbaoEnterpriseId;
|
||||
}
|
||||
|
||||
$where = [];
|
||||
if ($keyword !== '') {
|
||||
$where[] = ['nickname|phone|city|province', 'like', '%' . $keyword . '%'];
|
||||
}
|
||||
|
||||
$wechatIds = null;
|
||||
if ($pool === 'individual' || ($pool === 'enterprise' && $enterpriseId !== '')) {
|
||||
try {
|
||||
if ($pool === 'individual') {
|
||||
$trQuery = Db::name('test_results')->where(function ($q) {
|
||||
$q->whereNull('enterpriseId')->whereOr('enterpriseId', '')->whereOr('enterpriseId', 0);
|
||||
});
|
||||
$wechatIds = $trQuery->distinct(true)->column('userId');
|
||||
} else {
|
||||
$eid = (int) $enterpriseId;
|
||||
if ($cunkbaoEnterpriseId !== null && $eid === $cunkbaoEnterpriseId) {
|
||||
$idsEnt = Db::name('test_results')->where('enterpriseId', $eid)->distinct(true)->column('userId');
|
||||
$idsOrphan = Db::name('test_results')->where(function ($q) {
|
||||
$q->whereNull('enterpriseId')->whereOr('enterpriseId', '')->whereOr('enterpriseId', 0);
|
||||
})->distinct(true)->column('userId');
|
||||
$wechatIds = array_values(array_unique(array_filter(array_merge($idsEnt, $idsOrphan))));
|
||||
} else {
|
||||
$wechatIds = Db::name('test_results')->where('enterpriseId', $eid)->distinct(true)->column('userId');
|
||||
}
|
||||
}
|
||||
$wechatIds = array_values(array_unique(array_filter($wechatIds)));
|
||||
} catch (\Throwable $e) {
|
||||
$wechatIds = null;
|
||||
}
|
||||
}
|
||||
if ($mbti !== '') {
|
||||
$mbtiUserIds = Db::name('test_results')->where('testType', 'mbti')->distinct(true)->column('userId');
|
||||
$mbtiUserIds = array_values(array_unique(array_filter($mbtiUserIds)));
|
||||
if ($wechatIds !== null) {
|
||||
$wechatIds = array_values(array_intersect($wechatIds, $mbtiUserIds));
|
||||
} else {
|
||||
$wechatIds = $mbtiUserIds;
|
||||
}
|
||||
}
|
||||
|
||||
// 按 openid 去重:每个 openid 只保留 id 最大的一条;失败则不去重
|
||||
// 按 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)) : [];
|
||||
@@ -268,15 +205,45 @@ class AppUser extends BaseController
|
||||
if ($where) {
|
||||
$baseQuery->where($where);
|
||||
}
|
||||
if ($wechatIds !== null && !empty($wechatIds)) {
|
||||
$baseQuery->where('id', 'in', array_intersect($dedupIds, $wechatIds));
|
||||
} elseif ($wechatIds !== null && empty($wechatIds)) {
|
||||
return paginate_response([], 0, $page, $pageSize);
|
||||
|
||||
// 池筛选:直接基于 wechat_users.enterpriseId
|
||||
if ($pool === 'individual') {
|
||||
$baseQuery->where(function ($q) {
|
||||
$q->whereNull('enterpriseId')->whereOr('enterpriseId', '')->whereOr('enterpriseId', 0);
|
||||
});
|
||||
} elseif ($pool === 'enterprise' && $enterpriseId !== '') {
|
||||
$baseQuery->where('enterpriseId', (int) $enterpriseId);
|
||||
}
|
||||
|
||||
// 默认不展示「从未有过测试记录」的用户;?includeZeroTests=1 可显示全部(排查用)
|
||||
$includeZeroTests = Request::param('includeZeroTests', '');
|
||||
$showUntested = ($includeZeroTests === '1' || $includeZeroTests === 'true' || $includeZeroTests === true);
|
||||
if (!$showUntested) {
|
||||
$testedUserIds = Db::name('test_results')->distinct(true)->column('userId');
|
||||
$testedUserIds = array_values(array_unique(array_filter(array_map('intval', $testedUserIds))));
|
||||
if (empty($testedUserIds)) {
|
||||
return paginate_response([], 0, $page, $pageSize);
|
||||
}
|
||||
$baseQuery->whereIn('id', $testedUserIds);
|
||||
}
|
||||
|
||||
// MBTI 筛选:保留旧逻辑,从 test_results 取有 mbti 测试的用户
|
||||
if ($mbti !== '') {
|
||||
$mbtiUserIds = Db::name('test_results')->where('testType', 'mbti')->distinct(true)->column('userId');
|
||||
$mbtiUserIds = array_values(array_unique(array_filter($mbtiUserIds)));
|
||||
if (!empty($mbtiUserIds)) {
|
||||
$baseQuery->where('id', 'in', $mbtiUserIds);
|
||||
} else {
|
||||
return paginate_response([], 0, $page, $pageSize);
|
||||
}
|
||||
}
|
||||
|
||||
$total = $baseQuery->count();
|
||||
$list = (clone $baseQuery)
|
||||
->field('id,openid,nickname,avatar,phone,gender,country,province,city,status,lastLoginAt,createdAt')
|
||||
->field([
|
||||
'id', 'openid', 'nickname', 'avatar', 'phone', 'gender',
|
||||
'country', 'province', 'city', 'status', 'lastLoginAt', 'createdAt',
|
||||
])
|
||||
->order('createdAt', 'desc')
|
||||
->page($page, $pageSize)
|
||||
->select()
|
||||
@@ -310,38 +277,20 @@ class AppUser extends BaseController
|
||||
'createdAt' => $row['createdAt'],
|
||||
];
|
||||
}
|
||||
try {
|
||||
$trWithE = Db::name('test_results')
|
||||
->where('userId', 'in', $ids)
|
||||
->where('enterpriseId', '<>', null)
|
||||
->where('enterpriseId', '<>', '')
|
||||
->field('userId, enterpriseId')
|
||||
->select();
|
||||
$eids = array_unique(array_filter(array_column($trWithE, 'enterpriseId')));
|
||||
$enterpriseNames = [];
|
||||
if (!empty($eids)) {
|
||||
$enterpriseNames = Db::name('enterprises')->where('id', 'in', $eids)->column('name', 'id');
|
||||
}
|
||||
foreach ($trWithE as $r) {
|
||||
if (!isset($userEnterprise[$r['userId']])) {
|
||||
$userEnterprise[$r['userId']] = $enterpriseNames[$r['enterpriseId']] ?? ('企业' . $r['enterpriseId']);
|
||||
}
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
// test_results 可能无 enterpriseId 列
|
||||
}
|
||||
$cunkbaoName = null;
|
||||
if ($cunkbaoEnterpriseId) {
|
||||
$cunkbaoName = (string) (Db::name('enterprises')->where('id', $cunkbaoEnterpriseId)->value('name') ?: '存客宝');
|
||||
|
||||
// 所属企业直接从 wechat_users.enterpriseId 读取
|
||||
$userEids = Db::name('wechat_users')->where('id', 'in', $ids)->column('enterpriseId', 'id');
|
||||
$allEids = array_values(array_unique(array_filter(array_map('intval', $userEids))));
|
||||
$enterpriseNames = [];
|
||||
if (!empty($allEids)) {
|
||||
$enterpriseNames = Db::name('enterprises')->where('id', 'in', $allEids)->column('name', 'id');
|
||||
}
|
||||
foreach ($ids as $uid) {
|
||||
if (!isset($userEnterprise[$uid])) {
|
||||
$tc = (int) ($testCounts[$uid] ?? 0);
|
||||
if ($cunkbaoName !== null && $tc > 0) {
|
||||
$userEnterprise[$uid] = $cunkbaoName;
|
||||
} else {
|
||||
$userEnterprise[$uid] = '个人用户(无企业)';
|
||||
}
|
||||
$eid = isset($userEids[$uid]) ? (int) $userEids[$uid] : 0;
|
||||
if ($eid > 0 && isset($enterpriseNames[$eid])) {
|
||||
$userEnterprise[$uid] = $enterpriseNames[$eid];
|
||||
} else {
|
||||
$userEnterprise[$uid] = '个人用户(无企业)';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -370,6 +319,9 @@ class AppUser extends BaseController
|
||||
|
||||
foreach ($list as &$row) {
|
||||
$id = $row['id'];
|
||||
$av = $row['avatar'] ?? '';
|
||||
$row['avatar'] = is_scalar($av) ? trim((string) $av) : '';
|
||||
$row['avatarUrl'] = $row['avatar'];
|
||||
$testsForUser = $testTypes[$id] ?? [];
|
||||
$row['username'] = $row['nickname'] ?? ('用户' . $id);
|
||||
$row['testCount'] = (int) ($testCounts[$id] ?? 0);
|
||||
@@ -390,6 +342,7 @@ class AppUser extends BaseController
|
||||
$row['totalPaidAmount'] = $totalPaidFen;
|
||||
$row['totalPaidAmountYuan'] = $totalPaidFen > 0 ? round($totalPaidFen / 100, 2) : 0;
|
||||
}
|
||||
unset($row);
|
||||
|
||||
return paginate_response($list, $total, $page, $pageSize);
|
||||
}
|
||||
|
||||
@@ -11,6 +11,81 @@ use think\facade\Request;
|
||||
*/
|
||||
class DataMigration extends BaseController
|
||||
{
|
||||
/**
|
||||
* 小程序用户「未绑定企业」的 SQL 条件:仅 NULL 或 0。
|
||||
* 不使用 enterpriseId = ''(整型列 + ORM 易产生歧义),也不把已绑定企业(>0)算入。
|
||||
*/
|
||||
private function sqlWechatUserUnassignedEnterprise(): string
|
||||
{
|
||||
return '(`enterpriseId` IS NULL OR `enterpriseId` = 0)';
|
||||
}
|
||||
|
||||
private function sqlTestResultUnassignedEnterprise(): string
|
||||
{
|
||||
return '(`enterpriseId` IS NULL OR `enterpriseId` = 0)';
|
||||
}
|
||||
|
||||
/**
|
||||
* 将同一 userId 下 personal + enterprise(目标企业) 两条 user_profile 合并为一行:
|
||||
* 计数类字段相加;last* 类字段取 lastTestAt 更新的一侧(相同时优先企业行)
|
||||
*/
|
||||
private function mergePersonalUserProfileIntoEnterprise(int $userId, int $enterpriseId, int $now): void
|
||||
{
|
||||
if ($userId <= 0 || $enterpriseId <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
$personal = Db::name('user_profile')
|
||||
->where('userId', $userId)
|
||||
->where('userType', 'personal')
|
||||
->whereRaw('(`enterpriseId` IS NULL OR `enterpriseId` = 0)')
|
||||
->find();
|
||||
|
||||
if (!$personal) {
|
||||
return;
|
||||
}
|
||||
|
||||
$enterprise = Db::name('user_profile')
|
||||
->where('userId', $userId)
|
||||
->where('userType', 'enterprise')
|
||||
->where('enterpriseId', $enterpriseId)
|
||||
->find();
|
||||
|
||||
if (!$enterprise) {
|
||||
Db::name('user_profile')->where('id', (int) $personal['id'])->update([
|
||||
'userType' => 'enterprise',
|
||||
'enterpriseId' => $enterpriseId,
|
||||
'updatedAt' => $now,
|
||||
]);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$sumKeys = [
|
||||
'testsTotal', 'testsMbti', 'testsDisc', 'testsPdp', 'testsFace',
|
||||
'ordersTotal', 'paidOrders', 'totalPaidAmount',
|
||||
];
|
||||
$update = [];
|
||||
foreach ($sumKeys as $k) {
|
||||
$update[$k] = (int) ($personal[$k] ?? 0) + (int) ($enterprise[$k] ?? 0);
|
||||
}
|
||||
|
||||
$pAt = (int) ($personal['lastTestAt'] ?? 0);
|
||||
$eAt = (int) ($enterprise['lastTestAt'] ?? 0);
|
||||
$pickPersonal = $pAt > $eAt;
|
||||
$src = $pickPersonal ? $personal : $enterprise;
|
||||
|
||||
foreach (['lastTestResultId', 'lastTestType', 'lastTestAt', 'lastMbtiResultId', 'lastDiscResultId', 'lastPdpResultId', 'lastFaceResultId'] as $k) {
|
||||
if (array_key_exists($k, $src)) {
|
||||
$update[$k] = $src[$k];
|
||||
}
|
||||
}
|
||||
$update['updatedAt'] = $now;
|
||||
|
||||
Db::name('user_profile')->where('id', (int) $enterprise['id'])->update($update);
|
||||
Db::name('user_profile')->where('id', (int) $personal['id'])->delete();
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /api/v1/superadmin/data-migration/attach-orphan-orders
|
||||
*
|
||||
@@ -92,9 +167,7 @@ class DataMigration extends BaseController
|
||||
if ($syncWechatUsers && !empty($userIdsFromOrders)) {
|
||||
$wechatPatchCount = (int) Db::name('wechat_users')
|
||||
->whereIn('id', $userIdsFromOrders)
|
||||
->where(function ($q) {
|
||||
$q->whereNull('enterpriseId')->whereOr('enterpriseId', 0);
|
||||
})
|
||||
->whereRaw($this->sqlWechatUserUnassignedEnterprise())
|
||||
->count();
|
||||
}
|
||||
|
||||
@@ -169,11 +242,10 @@ class DataMigration extends BaseController
|
||||
if ($syncWechatUsers && !empty($userIdsFromOrders)) {
|
||||
Db::name('wechat_users')
|
||||
->whereIn('id', $userIdsFromOrders)
|
||||
->where(function ($q) {
|
||||
$q->whereNull('enterpriseId')->whereOr('enterpriseId', 0);
|
||||
})
|
||||
->whereRaw($this->sqlWechatUserUnassignedEnterprise())
|
||||
->update([
|
||||
'enterpriseId' => $targetEnterpriseId,
|
||||
'updatedAt' => $now,
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -277,10 +349,13 @@ class DataMigration extends BaseController
|
||||
}
|
||||
|
||||
/**
|
||||
* 将全平台「无 enterpriseId」的 test_results 与 wechat_users 归属到存客宝(或指定企业)
|
||||
* 存客宝归并:wechat_users 无企业用户写入目标企业;
|
||||
* 已/将归属该企业的用户下,test_results 中 enterpriseId 为空的记录补写目标企业;
|
||||
* user_profile 中 personal(NULL) 与 enterprise(目标) 合并计数后删除 personal 行。
|
||||
*
|
||||
* POST /api/v1/superadmin/data-migration/attach-orphans-to-cunkbao
|
||||
*
|
||||
* Body: targetEnterpriseId (可选)、dryRun (默认 true)、confirm、clonePersonalProfile (默认 true)
|
||||
* Body: targetEnterpriseId (可选)、dryRun (默认 true)、confirm
|
||||
*/
|
||||
public function attachOrphansToCunkbao()
|
||||
{
|
||||
@@ -296,7 +371,6 @@ class DataMigration extends BaseController
|
||||
|
||||
$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) {
|
||||
@@ -312,71 +386,84 @@ class DataMigration extends BaseController
|
||||
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);
|
||||
})
|
||||
$orphanUserIds = Db::name('wechat_users')
|
||||
->whereRaw($this->sqlWechatUserUnassignedEnterprise())
|
||||
->column('id');
|
||||
$distinctUserIds = array_values(array_unique(array_filter(array_merge($userIdsFromTests, $userIdsFromWechat))));
|
||||
$orphanUserIds = array_values(array_unique(array_filter(array_map('intval', $orphanUserIds))));
|
||||
$wechatAffected = count($orphanUserIds);
|
||||
|
||||
$boundNow = Db::name('wechat_users')
|
||||
->where('enterpriseId', $targetEnterpriseId)
|
||||
->column('id');
|
||||
$boundNow = array_values(array_unique(array_filter(array_map('intval', $boundNow))));
|
||||
$usersInScope = array_values(array_unique(array_merge($boundNow, $orphanUserIds)));
|
||||
|
||||
$testResultsAffected = empty($usersInScope) ? 0 : (int) Db::name('test_results')
|
||||
->whereIn('userId', $usersInScope)
|
||||
->whereRaw($this->sqlTestResultUnassignedEnterprise())
|
||||
->count();
|
||||
|
||||
$userProfilePersonalRows = empty($usersInScope) ? 0 : (int) Db::name('user_profile')
|
||||
->where('userType', 'personal')
|
||||
->whereRaw('(`enterpriseId` IS NULL OR `enterpriseId` = 0)')
|
||||
->whereIn('userId', $usersInScope)
|
||||
->count();
|
||||
|
||||
$preview = [
|
||||
'targetEnterpriseId' => $targetEnterpriseId,
|
||||
'enterpriseName' => $ent['name'] ?? '',
|
||||
'testResultsRows' => $testAffected,
|
||||
'wechatUsersRows' => $wechatAffected,
|
||||
'distinctUserIds' => $distinctUserIds,
|
||||
'dryRun' => $dryRun,
|
||||
'targetEnterpriseId' => $targetEnterpriseId,
|
||||
'enterpriseName' => $ent['name'] ?? '',
|
||||
'wechatUsersRows' => $wechatAffected,
|
||||
'testResultsRows' => $testResultsAffected,
|
||||
'userProfilePersonalRows' => $userProfilePersonalRows,
|
||||
'dryRun' => $dryRun,
|
||||
];
|
||||
|
||||
$nothingToDo = $wechatAffected === 0 && $testResultsAffected === 0 && $userProfilePersonalRows === 0;
|
||||
|
||||
if ($dryRun || !$confirm) {
|
||||
$preview['hint'] = $dryRun
|
||||
? '当前为预览。写入请传 dryRun=false 且 confirm=true。'
|
||||
: '未写入:请同时传 dryRun=false 与 confirm=true。';
|
||||
$preview['hint'] = $nothingToDo
|
||||
? '当前无需归并:归属用户、测试记录与画像均无待处理项。'
|
||||
: ($dryRun
|
||||
? '当前为预览。写入请传 dryRun=false 且 confirm=true。'
|
||||
: '未写入:请同时传 dryRun=false 与 confirm=true。');
|
||||
return success($preview);
|
||||
}
|
||||
|
||||
if ($nothingToDo) {
|
||||
$preview['hint'] = '无需写入。';
|
||||
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,
|
||||
]);
|
||||
if (!empty($orphanUserIds)) {
|
||||
Db::name('wechat_users')
|
||||
->whereIn('id', $orphanUserIds)
|
||||
->whereRaw($this->sqlWechatUserUnassignedEnterprise())
|
||||
->update([
|
||||
'enterpriseId' => $targetEnterpriseId,
|
||||
'updatedAt' => $now,
|
||||
]);
|
||||
}
|
||||
|
||||
Db::name('wechat_users')
|
||||
->where(function ($q) {
|
||||
$q->whereNull('enterpriseId')->whereOr('enterpriseId', 0);
|
||||
})
|
||||
->update([
|
||||
'enterpriseId' => $targetEnterpriseId,
|
||||
]);
|
||||
$boundAfter = Db::name('wechat_users')
|
||||
->where('enterpriseId', $targetEnterpriseId)
|
||||
->column('id');
|
||||
$boundAfter = array_values(array_unique(array_filter(array_map('intval', $boundAfter))));
|
||||
|
||||
if ($clonePersonalProfile && !empty($distinctUserIds)) {
|
||||
foreach ($distinctUserIds as $uid) {
|
||||
$this->ensureEnterpriseProfileFromPersonal((int) $uid, $targetEnterpriseId, $now);
|
||||
if (!empty($boundAfter)) {
|
||||
Db::name('test_results')
|
||||
->whereIn('userId', $boundAfter)
|
||||
->whereRaw($this->sqlTestResultUnassignedEnterprise())
|
||||
->update([
|
||||
'enterpriseId' => $targetEnterpriseId,
|
||||
'testScope' => 'enterprise',
|
||||
'updatedAt' => $now,
|
||||
]);
|
||||
|
||||
foreach ($boundAfter as $uid) {
|
||||
$this->mergePersonalUserProfileIntoEnterprise((int) $uid, $targetEnterpriseId, $now);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -387,7 +474,7 @@ class DataMigration extends BaseController
|
||||
}
|
||||
|
||||
$preview['executed'] = true;
|
||||
$preview['hint'] = '已写入。无企业归属的测试与用户已归属到目标企业。';
|
||||
$preview['hint'] = '已写入:小程序用户归属、无企业测试记录、画像 personal 行合并已完成。';
|
||||
return success($preview, '归并完成');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 '—';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user