feat: 管理端与用户端批量更新(订单/邀请双码/tabBar/审核与分销等)
- 管理端:用户详情、企业看板双小程序码、超管设置与用户列表等 - 后端:订单接口、邀请码企业版+个人版、分析与测试相关调整 - 微信小程序/抖音:自定义 tabBar 居中与拍摄钮上移、相机页与结果页、订单页、支付与统计 - 新增 faceResultDetail 工具与 mp 分析迁移脚本 Made-with: Cursor
This commit is contained in:
@@ -78,9 +78,28 @@ class AppUser extends BaseController
|
||||
$baseQuery->where($where);
|
||||
}
|
||||
|
||||
// 默认不展示「从未有过测试记录」的用户(企业后台按本企业 test_results 判定);?includeZeroTests=1 显示全部
|
||||
$includeZeroTests = Request::param('includeZeroTests', '');
|
||||
$showUntested = ($includeZeroTests === '1' || $includeZeroTests === 'true' || $includeZeroTests === true);
|
||||
if (!$showUntested) {
|
||||
$trQ = Db::name('test_results');
|
||||
if ($enterpriseId) {
|
||||
$trQ->where('enterpriseId', (int) $enterpriseId);
|
||||
}
|
||||
$testedUserIds = $trQ->distinct(true)->column('userId');
|
||||
$testedUserIds = array_values(array_unique(array_filter(array_map('intval', $testedUserIds))));
|
||||
if (empty($testedUserIds)) {
|
||||
return paginate_response([], 0, $page, $pageSize);
|
||||
}
|
||||
$baseQuery->whereIn('id', $testedUserIds);
|
||||
}
|
||||
|
||||
$total = (int) $baseQuery->count();
|
||||
$list = (clone $baseQuery)
|
||||
->field('id,nickname,openid,avatar,phone,gender,country,province,city,status,lastLoginAt,createdAt')
|
||||
->field([
|
||||
'id', 'nickname', 'openid', 'avatar', 'phone', 'gender',
|
||||
'country', 'province', 'city', 'status', 'lastLoginAt', 'createdAt',
|
||||
])
|
||||
->order('createdAt', 'desc')
|
||||
->page($page, $pageSize)
|
||||
->select()
|
||||
@@ -155,6 +174,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);
|
||||
@@ -172,6 +194,7 @@ class AppUser extends BaseController
|
||||
$row['paidOrders'] = $pay ? (int) ($pay['paidOrders'] ?? 0) : 0;
|
||||
$row['totalPaidAmount'] = $pay ? (int) ($pay['totalPaidAmount'] ?? 0) : 0;
|
||||
}
|
||||
unset($row);
|
||||
|
||||
return paginate_response($list, $total, $page, $pageSize);
|
||||
}
|
||||
|
||||
@@ -6,16 +6,16 @@ use app\common\service\WechatService;
|
||||
use think\facade\Db;
|
||||
|
||||
/**
|
||||
* 管理端 - 小程序邀请二维码(带企业参数)
|
||||
* 管理端 - 小程序邀请二维码(企业版 + 个人版)
|
||||
*/
|
||||
class Invite extends BaseController
|
||||
{
|
||||
/**
|
||||
* 生成专属邀请小程序码,scene 带企业 ID,扫码进入 pages/enterprise/index 可解析
|
||||
* 一次生成两枚小程序码:企业测评入口 + 个人版首页入口
|
||||
* GET /api/v1/admin/invite/qrcode
|
||||
* 可选:?enterpriseId=1 仅普通管理员指定企业时传;企业管理员用自身 enterpriseId
|
||||
*
|
||||
* 返回 data:image/png;base64,... 形式的图片地址
|
||||
* 返回 enterprise / personal 各含 data:image/png;base64,... ;兼容旧字段 qrcode=企业版图
|
||||
*/
|
||||
public function qrcode()
|
||||
{
|
||||
@@ -38,26 +38,48 @@ class Invite extends BaseController
|
||||
return error('无法确定企业,仅企业管理员或指定 enterpriseId 可生成邀请码', 400);
|
||||
}
|
||||
|
||||
// 场景值:e_企业ID,小程序 onLoad(options.scene) 可解析
|
||||
$scene = 'e_' . $enterpriseId;
|
||||
$page = 'pages/enterprise/index';
|
||||
$sceneEnterprise = 'e_' . $enterpriseId;
|
||||
$pageEnterprise = 'pages/enterprise/index';
|
||||
|
||||
$result = WechatService::getWxacodeUnlimited($scene, $page, 430);
|
||||
if (isset($result['errcode'])) {
|
||||
return error('获取小程序码失败:' . ($result['errmsg'] ?? ''), 500);
|
||||
$resultEnt = WechatService::getWxacodeUnlimited($sceneEnterprise, $pageEnterprise, 430);
|
||||
if (isset($resultEnt['errcode'])) {
|
||||
return error('企业版小程序码失败:' . ($resultEnt['errmsg'] ?? ''), 500);
|
||||
}
|
||||
|
||||
$binary = $result['binary'] ?? '';
|
||||
if ($binary === '') {
|
||||
return error('小程序码生成失败', 500);
|
||||
$binEnt = $resultEnt['binary'] ?? '';
|
||||
if ($binEnt === '') {
|
||||
return error('企业版小程序码生成失败', 500);
|
||||
}
|
||||
$b64Ent = 'data:image/png;base64,' . base64_encode($binEnt);
|
||||
|
||||
$base64 = 'data:image/png;base64,' . base64_encode($binary);
|
||||
// 个人版:首页,scene 使用短串 p(index 无 eid 则留在个人版流程)
|
||||
$scenePersonal = 'p';
|
||||
$pagePersonal = 'pages/index/index';
|
||||
$resultPer = WechatService::getWxacodeUnlimited($scenePersonal, $pagePersonal, 430);
|
||||
if (isset($resultPer['errcode'])) {
|
||||
return error('个人版小程序码失败:' . ($resultPer['errmsg'] ?? ''), 500);
|
||||
}
|
||||
$binPer = $resultPer['binary'] ?? '';
|
||||
if ($binPer === '') {
|
||||
return error('个人版小程序码生成失败', 500);
|
||||
}
|
||||
$b64Per = 'data:image/png;base64,' . base64_encode($binPer);
|
||||
|
||||
return success([
|
||||
'qrcode' => $base64,
|
||||
'scene' => $scene,
|
||||
'page' => $page,
|
||||
'qrcode' => $b64Ent,
|
||||
'scene' => $sceneEnterprise,
|
||||
'page' => $pageEnterprise,
|
||||
'enterprise' => [
|
||||
'qrcode' => $b64Ent,
|
||||
'scene' => $sceneEnterprise,
|
||||
'page' => $pageEnterprise,
|
||||
'label' => '企业版',
|
||||
],
|
||||
'personal' => [
|
||||
'qrcode' => $b64Per,
|
||||
'scene' => $scenePersonal,
|
||||
'page' => $pagePersonal,
|
||||
'label' => '个人版',
|
||||
],
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,11 +25,24 @@ class Analytics extends BaseController
|
||||
|
||||
$userId = null;
|
||||
$openid = null;
|
||||
$source = 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;
|
||||
if ($payload) {
|
||||
$source = $payload['source'] ?? null;
|
||||
if (in_array($source, ['wechat', 'douyin'], true)) {
|
||||
$userId = (int) ($payload['userId'] ?? $payload['user_id'] ?? 0) ?: null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$deviceJson = null;
|
||||
$deviceRaw = $body['device'] ?? null;
|
||||
if (is_array($deviceRaw) && !empty($deviceRaw)) {
|
||||
$deviceJson = json_encode($deviceRaw, JSON_UNESCAPED_UNICODE | JSON_INVALID_UTF8_SUBSTITUTE);
|
||||
if (strlen($deviceJson) > 2000) {
|
||||
$deviceJson = mb_substr($deviceJson, 0, 2000);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,6 +58,25 @@ class Analytics extends BaseController
|
||||
}
|
||||
$pagePath = isset($ev['page_path']) ? mb_substr(trim((string) $ev['page_path']), 0, 255) : '';
|
||||
$props = $ev['props'] ?? null;
|
||||
$platform = isset($ev['platform']) ? mb_substr(trim((string) $ev['platform']), 0, 16) : ($source ?: null);
|
||||
$sessionId = isset($ev['session_id']) ? mb_substr(trim((string) $ev['session_id']), 0, 64) : null;
|
||||
if ($props !== null && $props !== []) {
|
||||
if ($deviceJson) {
|
||||
$props['_device'] = $deviceRaw;
|
||||
}
|
||||
if (isset($ev['network'])) {
|
||||
$props['_network'] = $ev['network'];
|
||||
}
|
||||
if (isset($ev['scene'])) {
|
||||
$props['_scene'] = $ev['scene'];
|
||||
}
|
||||
} else {
|
||||
$props = [];
|
||||
if ($deviceJson) $props['_device'] = $deviceRaw;
|
||||
if (isset($ev['network'])) $props['_network'] = $ev['network'];
|
||||
if (isset($ev['scene'])) $props['_scene'] = $ev['scene'];
|
||||
if (empty($props)) $props = null;
|
||||
}
|
||||
$propsJson = null;
|
||||
if ($props !== null && $props !== []) {
|
||||
$propsJson = json_encode($props, JSON_UNESCAPED_UNICODE | JSON_INVALID_UTF8_SUBSTITUTE);
|
||||
@@ -64,6 +96,8 @@ class Analytics extends BaseController
|
||||
'pagePath' => $pagePath ?: null,
|
||||
'propsJson' => $propsJson,
|
||||
'clientTs' => $clientTs ?: null,
|
||||
'platform' => $platform,
|
||||
'sessionId' => $sessionId,
|
||||
'createdAt' => $now,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -171,12 +171,15 @@ class Analyze extends BaseController
|
||||
|
||||
if ($testResultId) {
|
||||
UserProfileModel::recordTest($userId, 'face', $testResultId, $writeEnterpriseId, $now);
|
||||
// 仅当 enterpriseId 来自请求体(企业分享链接)时才更新绑定关系
|
||||
if ($enterpriseFromRequest && $enterpriseId !== null && $enterpriseId > 0) {
|
||||
Db::name('wechat_users')->where('id', $userId)->update([
|
||||
'enterpriseId' => $enterpriseId,
|
||||
'updatedAt' => $now,
|
||||
]);
|
||||
// 用户尚无企业归属时,从本次测试上下文补写
|
||||
if ($writeEnterpriseId !== null && (int) $writeEnterpriseId > 0) {
|
||||
$curEid = Db::name('wechat_users')->where('id', $userId)->value('enterpriseId');
|
||||
if ($curEid === null || $curEid === '' || (int) $curEid === 0) {
|
||||
Db::name('wechat_users')->where('id', $userId)->update([
|
||||
'enterpriseId' => (int) $writeEnterpriseId,
|
||||
'updatedAt' => $now,
|
||||
]);
|
||||
}
|
||||
}
|
||||
// 面相分析由本接口直接写入 test_results,也要补触发测试完成佣金
|
||||
try {
|
||||
@@ -334,12 +337,15 @@ class Analyze extends BaseController
|
||||
|
||||
if ($testResultId > 0) {
|
||||
UserProfileModel::recordTest($userId, 'resume', $testResultId, $enterpriseId > 0 ? $enterpriseId : null, $now);
|
||||
// 如果是企业分享链接进入,更新用户绑定关系
|
||||
if ($enterpriseIdFromRequest && $enterpriseId > 0) {
|
||||
Db::name('wechat_users')->where('id', $userId)->update([
|
||||
'enterpriseId' => $enterpriseId,
|
||||
'updatedAt' => $now,
|
||||
]);
|
||||
// 用户尚无企业归属时,从本次测试上下文补写
|
||||
if ($enterpriseId > 0) {
|
||||
$curEid = Db::name('wechat_users')->where('id', $userId)->value('enterpriseId');
|
||||
if ($curEid === null || $curEid === '' || (int) $curEid === 0) {
|
||||
Db::name('wechat_users')->where('id', $userId)->update([
|
||||
'enterpriseId' => $enterpriseId,
|
||||
'updatedAt' => $now,
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
|
||||
@@ -230,17 +230,27 @@ class Auth extends BaseController
|
||||
$now = time();
|
||||
$ip = Request::ip();
|
||||
|
||||
$loginEnterpriseId = isset($input['enterpriseId']) && (int) $input['enterpriseId'] > 0
|
||||
? (int) $input['enterpriseId']
|
||||
: null;
|
||||
|
||||
if ($wechatUser) {
|
||||
Db::name('wechat_users')->where('id', $wechatUser['id'])->update([
|
||||
$updateFields = [
|
||||
'sessionKey' => $sessionKey,
|
||||
'unionid' => $unionid,
|
||||
'lastLoginAt' => $now,
|
||||
'lastLoginIp' => $ip,
|
||||
'updatedAt' => $now,
|
||||
]);
|
||||
];
|
||||
// 老用户未绑定企业时,从本次登录上下文补写
|
||||
$existingEid = $wechatUser['enterpriseId'] ?? null;
|
||||
if (($existingEid === null || $existingEid === '' || (int) $existingEid === 0) && $loginEnterpriseId !== null) {
|
||||
$updateFields['enterpriseId'] = $loginEnterpriseId;
|
||||
}
|
||||
Db::name('wechat_users')->where('id', $wechatUser['id'])->update($updateFields);
|
||||
$wechatUser = Db::name('wechat_users')->where('id', $wechatUser['id'])->find();
|
||||
} else {
|
||||
$id = Db::name('wechat_users')->insertGetId([
|
||||
$insertData = [
|
||||
'openid' => $openid,
|
||||
'unionid' => $unionid,
|
||||
'sessionKey' => $sessionKey,
|
||||
@@ -256,7 +266,11 @@ class Auth extends BaseController
|
||||
'lastLoginIp' => $ip,
|
||||
'createdAt' => $now,
|
||||
'updatedAt' => $now,
|
||||
]);
|
||||
];
|
||||
if ($loginEnterpriseId !== null) {
|
||||
$insertData['enterpriseId'] = $loginEnterpriseId;
|
||||
}
|
||||
$id = Db::name('wechat_users')->insertGetId($insertData);
|
||||
$wechatUser = Db::name('wechat_users')->where('id', $id)->find();
|
||||
}
|
||||
|
||||
|
||||
119
api/app/controller/api/Order.php
Normal file
119
api/app/controller/api/Order.php
Normal file
@@ -0,0 +1,119 @@
|
||||
<?php
|
||||
namespace app\controller\api;
|
||||
|
||||
use app\BaseController;
|
||||
use think\facade\Db;
|
||||
use think\facade\Request;
|
||||
|
||||
/**
|
||||
* 小程序端:当前登录用户的支付订单列表(mbti_orders.userId = wechat_users.id)
|
||||
*/
|
||||
class Order extends BaseController
|
||||
{
|
||||
/**
|
||||
* GET /api/orders?page=1&pageSize=20
|
||||
*/
|
||||
public function index()
|
||||
{
|
||||
$user = $this->request->user ?? null;
|
||||
if (!$user) {
|
||||
return error('未登录', 401);
|
||||
}
|
||||
$source = $user['source'] ?? '';
|
||||
if (!in_array($source, ['wechat', 'douyin'], true)) {
|
||||
return error('仅支持小程序用户', 403);
|
||||
}
|
||||
|
||||
$userId = (int) ($user['user_id'] ?? $user['userId'] ?? 0);
|
||||
if ($userId <= 0) {
|
||||
return error('用户不存在', 400);
|
||||
}
|
||||
|
||||
$page = (int) Request::param('page', 1);
|
||||
$pageSize = (int) Request::param('pageSize', 20);
|
||||
$pageSize = min(max($pageSize, 1), 100);
|
||||
|
||||
$query = Db::name('orders')
|
||||
->where('userId', $userId)
|
||||
->order('createdAt', 'desc');
|
||||
|
||||
$total = (int) (clone $query)->count();
|
||||
$rows = (clone $query)->page($page, $pageSize)->select()->toArray();
|
||||
|
||||
$list = [];
|
||||
foreach ($rows as $row) {
|
||||
$list[] = $this->formatOrderRow($row);
|
||||
}
|
||||
|
||||
return paginate_response($list, $total, $page, $pageSize);
|
||||
}
|
||||
|
||||
private function formatOrderRow(array $row): array
|
||||
{
|
||||
$amountFen = (int) ($row['amount'] ?? 0);
|
||||
$productType = (string) ($row['productType'] ?? '');
|
||||
$title = trim((string) ($row['productTitle'] ?? ''));
|
||||
if ($title === '') {
|
||||
$title = $this->productTypeLabel($productType);
|
||||
}
|
||||
|
||||
return [
|
||||
'id' => (int) ($row['id'] ?? 0),
|
||||
'orderNo' => (string) ($row['orderNo'] ?? ''),
|
||||
'productType' => $productType,
|
||||
'productTitle' => $title,
|
||||
'amountFen' => $amountFen,
|
||||
'amountYuan' => number_format($amountFen / 100, 2, '.', ''),
|
||||
'status' => (string) ($row['status'] ?? ''),
|
||||
'statusText' => $this->statusLabel((string) ($row['status'] ?? '')),
|
||||
'payMethod' => (string) ($row['payMethod'] ?? ''),
|
||||
'payTime' => isset($row['payTime']) ? (int) $row['payTime'] : null,
|
||||
'payTimeStr' => $this->formatTs($row['payTime'] ?? null),
|
||||
'createdAt' => isset($row['createdAt']) ? (int) $row['createdAt'] : null,
|
||||
'createdAtStr' => $this->formatTs($row['createdAt'] ?? null),
|
||||
];
|
||||
}
|
||||
|
||||
private function formatTs($ts): string
|
||||
{
|
||||
if ($ts === null || $ts === '') {
|
||||
return '';
|
||||
}
|
||||
$t = (int) $ts;
|
||||
if ($t <= 0) {
|
||||
return '';
|
||||
}
|
||||
return date('Y-m-d H:i', $t);
|
||||
}
|
||||
|
||||
private function statusLabel(string $s): string
|
||||
{
|
||||
$map = [
|
||||
'pending' => '待支付',
|
||||
'paid' => '已支付',
|
||||
'completed' => '已完成',
|
||||
'cancelled' => '已取消',
|
||||
];
|
||||
return $map[$s] ?? ($s !== '' ? $s : '未知');
|
||||
}
|
||||
|
||||
private function productTypeLabel(string $t): string
|
||||
{
|
||||
$map = [
|
||||
'face' => '人脸分析',
|
||||
'mbti' => 'MBTI测试',
|
||||
'disc' => 'DISC测试',
|
||||
'pdp' => 'PDP测试',
|
||||
'report' => '完整报告',
|
||||
'resume' => '简历分析',
|
||||
'recharge' => '企业充值',
|
||||
'vip' => '会员服务',
|
||||
'deep_personal' => '深度服务(个人)',
|
||||
'deep_team' => '深度服务(团队)',
|
||||
'single_test' => '单次测试',
|
||||
'test_count' => '测试次数包',
|
||||
'team_analysis' => '团队分析',
|
||||
];
|
||||
return $map[$t] ?? ($t !== '' ? $t : '订单');
|
||||
}
|
||||
}
|
||||
@@ -495,12 +495,15 @@ class Test extends BaseController
|
||||
|
||||
if ($id > 0) {
|
||||
UserProfileModel::recordTest($userId, $testType, $id, $writeEnterpriseId, $now);
|
||||
// 仅当 enterpriseId 来自请求体(企业分享链接)时才更新绑定关系
|
||||
if ($enterpriseFromRequest && $enterpriseId !== null && $enterpriseId > 0) {
|
||||
Db::name('wechat_users')->where('id', $userId)->update([
|
||||
'enterpriseId' => $enterpriseId,
|
||||
'updatedAt' => $now,
|
||||
]);
|
||||
// 用户尚无企业归属时,从本次测试上下文补写(请求体带 eid 或已绑定企业回落值)
|
||||
if ($writeEnterpriseId !== null && (int) $writeEnterpriseId > 0) {
|
||||
$curEid = Db::name('wechat_users')->where('id', $userId)->value('enterpriseId');
|
||||
if ($curEid === null || $curEid === '' || (int) $curEid === 0) {
|
||||
Db::name('wechat_users')->where('id', $userId)->update([
|
||||
'enterpriseId' => (int) $writeEnterpriseId,
|
||||
'updatedAt' => $now,
|
||||
]);
|
||||
}
|
||||
}
|
||||
// 测试完成佣金结算(无需付款,异步不影响主流程)
|
||||
try {
|
||||
|
||||
@@ -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