chore: 管理端与 API 批量更新(测评恢复、看板、分销与用户)

1、修复了用户详情/测评结果与人脸结果解析展示问题;管理端订单、分销及超管用户相关接口与展示异常。

2、新增了 testResultParse 统一解析工具、mbti_test_results 恢复 SQL 及生成脚本;补充 API 路由与 Dashboard/用户侧能力。

3、优化了企业/超管数据看板与分销结算逻辑;精简超管 Settings 与布局代码;完善 faceResultDetail;小程序 app.js 小幅同步。

Made-with: Cursor
This commit is contained in:
Ghost
2026-03-31 10:56:28 +08:00
parent 3af7bb35dd
commit 3fdf9b6ed2
18 changed files with 1306 additions and 546 deletions

View File

@@ -73,13 +73,11 @@ class AppUser extends BaseController
->join([$dedupSql => 'd'], 'w.id = d.mid');
if ($keyword !== '') {
$kw = '%' . $keyword . '%';
$baseQuery->where(function ($q) use ($kw) {
$q->whereLike('w.nickname', $kw)
->whereOr('w.phone', 'like', $kw)
->whereOr('w.city', 'like', $kw)
->whereOr('w.province', 'like', $kw);
});
$like = '%' . addcslashes($keyword, '%_\\') . '%';
$baseQuery->whereRaw(
'(w.nickname LIKE ? OR w.phone LIKE ? OR w.city LIKE ? OR w.province LIKE ?)',
[$like, $like, $like, $like]
);
}
} else {
// 无企业归属(极少):沿用全表 openid 去重 + IN 列表
@@ -103,7 +101,7 @@ class AppUser extends BaseController
$total = (int) (clone $baseQuery)->distinct(true)->count('w.id');
$list = (clone $baseQuery)
->field('w.id,w.nickname,w.openid,w.avatar,w.phone,w.gender,w.country,w.province,w.city,w.status,w.lastLoginAt,w.createdAt')
->order('w.createdAt', 'desc')
->order('w.id', 'desc')
->page($page, $pageSize)
->select()
->toArray();
@@ -111,7 +109,7 @@ class AppUser extends BaseController
$total = (int) (clone $baseQuery)->count();
$list = (clone $baseQuery)
->field('id,nickname,openid,avatar,phone,gender,country,province,city,status,lastLoginAt,createdAt')
->order('createdAt', 'desc')
->order('id', 'desc')
->page($page, $pageSize)
->select()
->toArray();
@@ -324,4 +322,71 @@ class AppUser extends BaseController
return success($data);
}
/**
* 单条测试记录详情(与 testList 中单条结构一致,供后台「测试记录 → 详情」)
* GET /api/v1/admin/test-records/:id
*/
public function testRecord($id)
{
$user = $this->request->user ?? null;
if (!$user) {
return error('未登录', 401);
}
if (!in_array($user['role'] ?? '', ['admin', 'enterprise_admin'], true)) {
return error('无权限访问', 403);
}
$testId = (int) $id;
if ($testId <= 0) {
return error('记录ID无效', 400);
}
$tr = Db::name('test_results')->where('id', $testId)->find();
if (!$tr) {
return error('记录不存在', 404);
}
$wechatUserId = (int) ($tr['userId'] ?? 0);
if ($wechatUserId <= 0) {
return error('记录数据异常', 400);
}
$enterpriseId = $user['enterpriseId'] ?? null;
if (!$enterpriseId) {
$adminRow = Db::name('users')->where('id', $user['userId'] ?? 0)->find();
$enterpriseId = $adminRow['enterpriseId'] ?? null;
}
if ($enterpriseId) {
$has = Db::name('user_profile')
->where('userId', $wechatUserId)
->where('enterpriseId', $enterpriseId)
->find();
if (!$has) {
return error('无权限查看', 403);
}
$tid = isset($tr['enterpriseId']) ? (int) $tr['enterpriseId'] : 0;
if ($tid !== (int) $enterpriseId) {
return error('无权限查看该测试记录', 403);
}
}
$raw = $tr['resultData'] ?? '';
$out = [
'id' => $testId,
'userId' => $wechatUserId,
'testType' => $tr['testType'] ?? '',
'result' => is_string($raw) ? $raw : json_encode($raw, JSON_UNESCAPED_UNICODE),
'createdAt' => $tr['createdAt'] ?? null,
'requiresPayment' => (int) ($tr['requiresPayment'] ?? 0),
'isPaid' => (int) ($tr['isPaid'] ?? 0),
'paidAmount' => isset($tr['paidAmount']) ? (int) $tr['paidAmount'] : null,
'paidAt' => $tr['paidAt'] ?? null,
'orderId' => isset($tr['orderId']) ? (int) $tr['orderId'] : null,
'testScope' => !empty($tr['enterpriseId']) ? 'enterprise' : 'personal',
];
return success($out);
}
}

View File

@@ -48,21 +48,13 @@ class Dashboard extends BaseController
}
}
// 企业用户 ID 集合(用于后续统计个人版测试
$enterpriseUserIds = [];
// 总用户数:企业维度用 COUNT全局用 DISTINCT openid禁止拉全表 id
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);
$totalUsers = (int) Db::name('wechat_users')->where('enterpriseId', $enterpriseId)->count();
} else {
try {
$totalUsers = (int) Db::name('wechat_users')->count('openid', true);
$ru = Db::name('wechat_users')->field('COUNT(DISTINCT openid) AS c')->find();
$totalUsers = (int) ($ru['c'] ?? 0);
} catch (\Throwable $e) {
$totalUsers = (int) Db::name('wechat_users')->count();
}
@@ -77,20 +69,18 @@ class Dashboard extends BaseController
$testsCompleted = (int) Db::name('test_results')->count();
}
// 今日活跃用户数
// 今日活跃用户数(库内 COUNT DISTINCT禁止拉全量 userId
$todayStart = strtotime(date('Y-m-d 00:00:00'));
$todayEnd = strtotime(date('Y-m-d 23:59:59'));
$activeQuery = Db::name('test_results')
$activeRow = 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));
}
->where('createdAt', '<=', $todayEnd)
->when((bool) $enterpriseId, static function ($q) use ($enterpriseId) {
$q->where('enterpriseId', $enterpriseId);
})
->field('COUNT(DISTINCT userId) AS c')
->find();
$activeToday = (int) ($activeRow['c'] ?? 0);
// 待审核暂返回0
$pendingReviews = 0;
@@ -151,7 +141,7 @@ class Dashboard extends BaseController
}
}
$topTestUsers = $this->buildTopTestUsers($enterpriseId, 10);
$topTestUsers = $this->buildTopTestUsers($enterpriseId, 20);
$testCatalog = $this->buildTestCatalog($enterpriseId);
$distributionMbti = $this->aggregateTestLabels($enterpriseId, 'mbti', 14);
@@ -180,7 +170,7 @@ class Dashboard extends BaseController
/**
* 按测试完成次数排序,取前 N 名小程序用户与列表页口径一致test_results 按企业过滤)
*/
private function buildTopTestUsers(?int $enterpriseId, int $limit = 10): array
private function buildTopTestUsers(?int $enterpriseId, int $limit = 20): array
{
$limit = min(max($limit, 1), 50);
$q = Db::name('test_results')->field('userId, COUNT(*) as cnt')->group('userId')->order('cnt', 'desc')->limit($limit);
@@ -215,31 +205,74 @@ class Dashboard extends BaseController
$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();
// 每人每类测评仅取最新一条MAX id禁止拉取 TopN 用户历史上全部 test_results
$testsByUser = [];
foreach ($testRows as $row) {
$uid = (int) ($row['userId'] ?? 0);
if ($uid <= 0) {
continue;
try {
$aggSub = Db::name('test_results')
->whereIn('userId', $uids)
->whereIn('testType', ['face', 'mbti', 'disc', 'pdp']);
if ($enterpriseId) {
$aggSub->where('enterpriseId', $enterpriseId);
}
if (!isset($testsByUser[$uid])) {
$testsByUser[$uid] = [];
$aggSql = $aggSub
->field('userId, testType, MAX(id) AS mid')
->group('userId, testType')
->buildSql(true);
$testRows = Db::name('test_results')->alias('t')
->join([$aggSql => 'agg'], 't.userId = agg.userId AND t.testType = agg.testType AND t.id = agg.mid')
->field('t.userId, t.testType, t.resultData, t.createdAt')
->select()
->toArray();
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),
];
}
} catch (\Throwable $e) {
$trQuery = Db::name('test_results')->whereIn('userId', $uids);
if ($enterpriseId) {
$trQuery->where('enterpriseId', $enterpriseId);
}
$testRows = $trQuery
->field('userId, testType, resultData, createdAt')
->order('createdAt', 'desc')
->limit(5000)
->select()
->toArray();
foreach ($testRows as $row) {
$uid = (int) ($row['userId'] ?? 0);
if ($uid <= 0) {
continue;
}
if (!isset($testsByUser[$uid])) {
$testsByUser[$uid] = [];
}
$tt = strtolower((string) ($row['testType'] ?? ''));
foreach ($testsByUser[$uid] as $ex) {
if (strtolower((string) ($ex['testType'] ?? '')) === $tt) {
continue 2;
}
}
$raw = $row['resultData'] ?? '';
$testsByUser[$uid][] = [
'testType' => $row['testType'] ?? '',
'result' => is_string($raw) ? $raw : json_encode($raw, JSON_UNESCAPED_UNICODE),
'createdAt' => (int) ($row['createdAt'] ?? 0),
];
}
$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 = [];
@@ -283,24 +316,34 @@ class Dashboard extends BaseController
['key' => 'disc', 'label' => 'DISC'],
['key' => 'pdp', 'label' => 'PDP'],
];
$q = Db::name('test_results')
->whereIn('testType', ['face', 'mbti', 'disc', 'pdp']);
if ($enterpriseId) {
$q->where('enterpriseId', $enterpriseId);
}
$rows = $q->field('testType, COUNT(*) AS records, COUNT(DISTINCT userId) AS uniqueUsers')
->group('testType')
->select()
->toArray();
$byType = [];
foreach ($rows as $r) {
$byType[$r['testType'] ?? ''] = [
'records' => (int) ($r['records'] ?? 0),
'uniqueUsers' => (int) ($r['uniqueUsers'] ?? 0),
];
}
$out = [];
foreach ($defs as $def) {
$tt = $def['key'];
$q = Db::name('test_results')->where('testType', $tt);
if ($enterpriseId) {
$q->where('enterpriseId', $enterpriseId);
}
$records = (int) $q->count();
$q2 = Db::name('test_results')->where('testType', $tt);
if ($enterpriseId) {
$q2->where('enterpriseId', $enterpriseId);
}
$uniqueUsers = (int) $q2->distinct(true)->count('userId');
$st = $byType[$tt] ?? ['records' => 0, 'uniqueUsers' => 0];
$out[] = [
'key' => $tt,
'label' => $def['label'],
'records' => $records,
'uniqueUsers' => $uniqueUsers,
'records' => $st['records'],
'uniqueUsers' => $st['uniqueUsers'],
];
}
@@ -313,6 +356,90 @@ class Dashboard extends BaseController
* @return array<int, array{label:string,count:int}>
*/
private function aggregateTestLabels(?int $enterpriseId, string $testType, int $topN): array
{
try {
return $this->aggregateTestLabelsWithSql($enterpriseId, $testType, $topN);
} catch (\Throwable $e) {
return $this->aggregateTestLabelsChunked($enterpriseId, $testType, $topN);
}
}
/**
* 使用 MySQL JSON 函数在库内分组统计(避免全表 chunk
*
* @return array<int, array{label:string,count:int}>
*/
private function aggregateTestLabelsWithSql(?int $enterpriseId, string $testType, int $topN): array
{
if (!in_array($testType, ['mbti', 'disc', 'pdp'], true)) {
throw new \InvalidArgumentException('unsupported testType');
}
$table = $this->testResultsTableName();
$entClause = '';
$bind = [$testType];
if ($enterpriseId) {
$entClause = ' AND `enterpriseId` = ? ';
$bind[] = $enterpriseId;
}
if ($testType === 'mbti') {
$expr = "CASE
WHEN JSON_VALID(`resultData`) = 0 OR `resultData` IS NULL OR `resultData` = '' THEN '未识别'
ELSE COALESCE(
NULLIF(TRIM(JSON_UNQUOTE(JSON_EXTRACT(`resultData`, '$.mbtiType'))), ''),
IF(JSON_TYPE(JSON_EXTRACT(`resultData`, '$.type')) = 'STRING', NULLIF(TRIM(JSON_UNQUOTE(JSON_EXTRACT(`resultData`, '$.type'))), ''), NULL),
IF(JSON_TYPE(JSON_EXTRACT(`resultData`, '$.result')) = 'STRING', NULLIF(TRIM(JSON_UNQUOTE(JSON_EXTRACT(`resultData`, '$.result'))), ''), NULL),
'未识别'
)
END";
} elseif ($testType === 'disc') {
$expr = "CASE
WHEN JSON_VALID(`resultData`) = 0 OR `resultData` IS NULL OR `resultData` = '' THEN '未识别'
ELSE COALESCE(
IF(JSON_TYPE(JSON_EXTRACT(`resultData`, '$.description.type')) = 'STRING', NULLIF(TRIM(JSON_UNQUOTE(JSON_EXTRACT(`resultData`, '$.description.type'))), ''), NULL),
IF(JSON_TYPE(JSON_EXTRACT(`resultData`, '$.dominantType')) = 'STRING', NULLIF(TRIM(JSON_UNQUOTE(JSON_EXTRACT(`resultData`, '$.dominantType'))), ''), NULL),
NULLIF(TRIM(JSON_UNQUOTE(JSON_EXTRACT(`resultData`, '$.disc.primary'))), ''),
IF(JSON_TYPE(JSON_EXTRACT(`resultData`, '$.disc')) = 'STRING', NULLIF(TRIM(JSON_UNQUOTE(JSON_EXTRACT(`resultData`, '$.disc'))), ''), NULL),
IF(JSON_TYPE(JSON_EXTRACT(`resultData`, '$.type')) = 'STRING', NULLIF(TRIM(JSON_UNQUOTE(JSON_EXTRACT(`resultData`, '$.type'))), ''), NULL),
'未识别'
)
END";
} else {
$expr = "CASE
WHEN JSON_VALID(`resultData`) = 0 OR `resultData` IS NULL OR `resultData` = '' THEN '未识别'
ELSE COALESCE(
IF(JSON_TYPE(JSON_EXTRACT(`resultData`, '$.description.type')) = 'STRING', NULLIF(TRIM(JSON_UNQUOTE(JSON_EXTRACT(`resultData`, '$.description.type'))), ''), NULL),
IF(JSON_TYPE(JSON_EXTRACT(`resultData`, '$.dominantType')) = 'STRING', NULLIF(TRIM(JSON_UNQUOTE(JSON_EXTRACT(`resultData`, '$.dominantType'))), ''), NULL),
NULLIF(TRIM(JSON_UNQUOTE(JSON_EXTRACT(`resultData`, '$.pdp.primary'))), ''),
IF(JSON_TYPE(JSON_EXTRACT(`resultData`, '$.pdp')) = 'STRING', NULLIF(TRIM(JSON_UNQUOTE(JSON_EXTRACT(`resultData`, '$.pdp'))), ''), NULL),
IF(JSON_TYPE(JSON_EXTRACT(`resultData`, '$.type')) = 'STRING', NULLIF(TRIM(JSON_UNQUOTE(JSON_EXTRACT(`resultData`, '$.type'))), ''), NULL),
'未识别'
)
END";
}
$sql = "SELECT `label`, COUNT(*) AS `cnt` FROM (
SELECT ({$expr}) AS `label`
FROM `{$table}`
WHERE `testType` = ? {$entClause}
) `t`
GROUP BY `label`";
$rows = Db::query($sql, $bind);
$counts = [];
foreach ($rows as $r) {
$counts[(string) ($r['label'] ?? '')] = (int) ($r['cnt'] ?? 0);
}
arsort($counts);
return $this->countsToTopNWithOther($counts, $topN);
}
/**
* @return array<int, array{label:string,count:int}>
*/
private function aggregateTestLabelsChunked(?int $enterpriseId, string $testType, int $topN): array
{
$counts = [];
$query = Db::name('test_results')
@@ -321,7 +448,7 @@ class Dashboard extends BaseController
if ($enterpriseId) {
$query->where('enterpriseId', $enterpriseId);
}
$query->chunk(400, function ($rows) use (&$counts, $testType) {
$query->chunk(800, function ($rows) use (&$counts, $testType) {
foreach ($rows as $row) {
$raw = $row['resultData'] ?? '';
$label = $this->labelFromResultRow($testType, $raw);
@@ -342,6 +469,87 @@ class Dashboard extends BaseController
* @return array{mbti:array,disc:array,pdp:array}
*/
private function aggregateFaceSubtypeHints(?int $enterpriseId, int $topN): array
{
try {
return $this->aggregateFaceSubtypeHintsWithSql($enterpriseId, $topN);
} catch (\Throwable $e) {
return $this->aggregateFaceSubtypeHintsChunked($enterpriseId, $topN);
}
}
/**
* @return array{mbti:array,disc:array,pdp:array}
*/
private function aggregateFaceSubtypeHintsWithSql(?int $enterpriseId, int $topN): array
{
$table = $this->testResultsTableName();
$entClause = '';
$bind = [];
if ($enterpriseId) {
$entClause = ' AND `enterpriseId` = ? ';
$bind[] = $enterpriseId;
}
$sqlFaceMbti = "SELECT `label`, COUNT(*) AS `cnt` FROM (
SELECT COALESCE(
NULLIF(TRIM(JSON_UNQUOTE(JSON_EXTRACT(`resultData`, '$.mbti.type'))), ''),
NULLIF(TRIM(JSON_UNQUOTE(JSON_EXTRACT(`resultData`, '$.mbtiType'))), ''),
''
) AS `label`
FROM `{$table}`
WHERE `testType` = 'face' AND JSON_VALID(`resultData`) {$entClause}
) `t`
WHERE `label` <> ''
GROUP BY `label`";
$sqlFaceDisc = "SELECT `label`, COUNT(*) AS `cnt` FROM (
SELECT COALESCE(
NULLIF(TRIM(JSON_UNQUOTE(JSON_EXTRACT(`resultData`, '$.disc.primary'))), ''),
IF(JSON_TYPE(JSON_EXTRACT(`resultData`, '$.disc')) = 'STRING', NULLIF(TRIM(JSON_UNQUOTE(JSON_EXTRACT(`resultData`, '$.disc'))), ''), ''),
''
) AS `label`
FROM `{$table}`
WHERE `testType` = 'face' AND JSON_VALID(`resultData`) {$entClause}
) `t`
WHERE `label` <> ''
GROUP BY `label`";
$sqlFacePdp = "SELECT `label`, COUNT(*) AS `cnt` FROM (
SELECT COALESCE(
NULLIF(TRIM(JSON_UNQUOTE(JSON_EXTRACT(`resultData`, '$.pdp.primary'))), ''),
IF(JSON_TYPE(JSON_EXTRACT(`resultData`, '$.pdp')) = 'STRING', NULLIF(TRIM(JSON_UNQUOTE(JSON_EXTRACT(`resultData`, '$.pdp'))), ''), ''),
''
) AS `label`
FROM `{$table}`
WHERE `testType` = 'face' AND JSON_VALID(`resultData`) {$entClause}
) `t`
WHERE `label` <> ''
GROUP BY `label`";
$out = [];
foreach (
[
'mbti' => $sqlFaceMbti,
'disc' => $sqlFaceDisc,
'pdp' => $sqlFacePdp,
] as $k => $sql
) {
$rows = Db::query($sql, $bind);
$counts = [];
foreach ($rows as $r) {
$counts[(string) ($r['label'] ?? '')] = (int) ($r['cnt'] ?? 0);
}
arsort($counts);
$out[$k] = $this->countsToTopNWithOther($counts, $topN);
}
return $out;
}
/**
* @return array{mbti:array,disc:array,pdp:array}
*/
private function aggregateFaceSubtypeHintsChunked(?int $enterpriseId, int $topN): array
{
$subMaps = ['mbti' => [], 'disc' => [], 'pdp' => []];
$query = Db::name('test_results')
@@ -350,7 +558,7 @@ class Dashboard extends BaseController
if ($enterpriseId) {
$query->where('enterpriseId', $enterpriseId);
}
$query->chunk(400, function ($rows) use (&$subMaps) {
$query->chunk(800, function ($rows) use (&$subMaps) {
foreach ($rows as $row) {
$raw = $row['resultData'] ?? '';
$str = is_string($raw) ? $raw : json_encode($raw, JSON_UNESCAPED_UNICODE);
@@ -376,6 +584,11 @@ class Dashboard extends BaseController
return $out;
}
private function testResultsTableName(): string
{
return Db::name('test_results')->getTable();
}
/**
* @param array<string,int> $counts
* @return array<int, array{label:string,count:int}>

View File

@@ -11,6 +11,65 @@ use think\facade\Request;
*/
class Distribution extends BaseController
{
/**
* 企业管理员 enterpriseIdJWT 可能为空,需与 users 表对齐(与订单等接口一致)
*/
private function resolveEnterpriseIdForAdmin(?array $user): int
{
if (!$user || !in_array($user['role'] ?? '', ['admin', 'enterprise_admin'], true)) {
return 0;
}
$eid = $user['enterpriseId'] ?? null;
if (is_array($eid)) {
$eid = null;
}
$enterpriseId = $eid !== null && $eid !== '' ? (int) $eid : 0;
if ($enterpriseId <= 0) {
$adminRow = Db::name('users')->where('id', (int) ($user['userId'] ?? 0))->find();
$e2 = $adminRow['enterpriseId'] ?? null;
$enterpriseId = ($e2 !== null && $e2 !== '') ? (int) $e2 : 0;
}
return $enterpriseId > 0 ? $enterpriseId : 0;
}
/**
* 本企业维度的「推荐人 id」集合佣金表 + 企业绑定表 + 历史仅填 agentId 的佣金行
*
* @return int[]
*/
private function distributorUserIdsForEnterprise(int $enterpriseId): array
{
if ($enterpriseId <= 0) {
return [];
}
$a = Db::name('commission_records')
->where('enterpriseId', $enterpriseId)
->where('inviterId', '>', 0)
->distinct(true)
->column('inviterId');
$b = Db::name('distribution_bindings')
->where('enterpriseId', $enterpriseId)
->where('inviterId', '>', 0)
->distinct(true)
->column('inviterId');
$c = Db::name('commission_records')
->where('enterpriseId', $enterpriseId)
->where(function ($q) {
$q->whereNull('inviterId')->whereOr('inviterId', 0);
})
->where('agentId', '>', 0)
->distinct(true)
->column('agentId');
$merged = array_merge($a ?: [], $b ?: [], $c ?: []);
return array_values(array_unique(array_filter(array_map('intval', $merged), static function ($v) {
return $v > 0;
})));
}
// ─────────────────────────────────────────────────────────────
// GET distribution/overview
// ─────────────────────────────────────────────────────────────
@@ -21,7 +80,10 @@ class Distribution extends BaseController
return error('无权限', 403);
}
$enterpriseId = (int) ($user['enterpriseId'] ?? 0);
$enterpriseId = $this->resolveEnterpriseIdForAdmin($user);
if ($enterpriseId <= 0) {
return error('未绑定企业或企业无效', 400);
}
$todayStart = strtotime(date('Y-m-d 00:00:00'));
$days = 7;
$trendStart = strtotime(date('Y-m-d 00:00:00', strtotime('-' . ($days - 1) . ' days')));
@@ -105,83 +167,102 @@ class Distribution extends BaseController
return error('无权限', 403);
}
$enterpriseId = (int) ($user['enterpriseId'] ?? 0);
$enterpriseId = $this->resolveEnterpriseIdForAdmin($user);
if ($enterpriseId <= 0) {
return error('未绑定企业或企业无效', 400);
}
$search = trim((string) Request::param('search', ''));
$page = max(1, (int) Request::param('page', 1));
$pageSize = min(100, (int) Request::param('pageSize', 20));
try {
// 找出与本企业关联的所有有过邀请行为的用户(不限 scopeenterpriseId 筛选)
$query = Db::name('distribution_bindings')
->where('enterpriseId', $enterpriseId)
->distinct(true)
->field('inviterId')
->buildSql();
$poolIds = $this->distributorUserIdsForEnterprise($enterpriseId);
if (empty($poolIds)) {
return success(['list' => [], 'total' => 0, 'page' => $page, 'pageSize' => $pageSize]);
}
$inviterQuery = Db::name('wechat_users')
->alias('u')
->whereRaw("u.id IN {$query}")
->whereIn('u.id', $poolIds)
->field('u.id, u.nickname, u.avatar, u.createdAt');
if ($search !== '') {
$inviterQuery->where(function ($q) use ($search) {
$q->where('u.nickname', 'like', "%{$search}%")
->whereOr('u.id', '=', is_numeric($search) ? (int)$search : -1);
->whereOr('u.id', '=', is_numeric($search) ? (int) $search : -1);
});
}
$total = (clone $inviterQuery)->count();
$inviters = $inviterQuery->page($page, $pageSize)->select()->toArray();
$inviterQuery->order('u.id', 'desc');
$inviterIds = array_column($inviters, 'id');
$total = (int) (clone $inviterQuery)->count();
$inviters = $inviterQuery->page($page, $pageSize)->select()->toArray();
$inviterIds = array_values(array_filter(array_map('intval', array_column($inviters, 'id'))));
$commStats = [];
$withdrawnMap = [];
$teamMap = [];
// 各邀请人的累计佣金与可提现佣金
$commStats = [];
if (!empty($inviterIds)) {
$rows = Db::name('commission_records')
->whereIn('inviterId', $inviterIds)
$inStr = implode(',', $inviterIds);
$rows = Db::name('commission_records')
->where('enterpriseId', $enterpriseId)
->field('inviterId,
SUM(IF(status IN ("paid","frozen"), commissionFen, 0)) as totalFen,
SUM(IF(status = "paid", commissionFen, 0)) as paidFen')
->group('inviterId')
->select()->toArray();
->whereRaw('COALESCE(inviterId, agentId) IN (' . $inStr . ')')
->fieldRaw("COALESCE(inviterId, agentId) AS inviterKey, SUM(IF(status IN ('paid','frozen'), commissionFen, 0)) AS totalFen, SUM(IF(status = 'paid', commissionFen, 0)) AS paidFen")
->group('inviterKey')
->select()
->toArray();
foreach ($rows as $r) {
$commStats[$r['inviterId']] = $r;
$kid = (int) ($r['inviterKey'] ?? 0);
if ($kid > 0) {
$commStats[$kid] = $r;
}
}
// 已提现金额
$withdrawnRows = Db::name('distribution_withdrawals')
->whereIn('userId', $inviterIds)
// 提现金额统计0=审核中,2=待收款,3=已收款
->whereIn('status', [0, 2, 3])
->field('userId, SUM(amountFen) as withdrawnFen')
->group('userId')
->select()->toArray();
$withdrawnMap = [];
->alias('w')
->join('wechat_users u', 'w.userId = u.id')
->whereIn('w.userId', $inviterIds)
->where('u.enterpriseId', $enterpriseId)
->whereIn('w.status', [0, 2, 3])
->field('w.userId, SUM(w.amountFen) AS withdrawnFen')
->group('w.userId')
->select()
->toArray();
foreach ($withdrawnRows as $r) {
$withdrawnMap[$r['userId']] = (int)$r['withdrawnFen'];
$withdrawnMap[(int) ($r['userId'] ?? 0)] = (int) ($r['withdrawnFen'] ?? 0);
}
// 团队人数(绑定人数,不限 scope
$now = time();
$teamRows = Db::name('distribution_bindings')
->whereIn('inviterId', $inviterIds)
->where('enterpriseId', $enterpriseId)
->field('inviterId, COUNT(DISTINCT inviteeId) as teamCount')
->group('inviterId')
->select()->toArray();
$teamMap = [];
->alias('b')
->leftJoin('wechat_users w', 'w.id = b.inviteeId')
->whereIn('b.inviterId', $inviterIds)
->where(function ($q) use ($enterpriseId) {
$q->where('b.enterpriseId', $enterpriseId)
->whereOr(function ($q2) use ($enterpriseId) {
$q2->whereNull('b.enterpriseId')->where('w.enterpriseId', $enterpriseId);
});
})
->where('b.status', 'active')
->where('b.expireAt', '>', $now)
->fieldRaw('b.inviterId, COUNT(DISTINCT b.inviteeId) AS teamCount')
->group('b.inviterId')
->select()
->toArray();
foreach ($teamRows as $r) {
$teamMap[$r['inviterId']] = (int)$r['teamCount'];
$teamMap[(int) ($r['inviterId'] ?? 0)] = (int) ($r['teamCount'] ?? 0);
}
}
$list = [];
foreach ($inviters as $inv) {
$uid = $inv['id'];
$totalFen = (int)($commStats[$uid]['totalFen'] ?? 0);
$paidFen = (int)($commStats[$uid]['paidFen'] ?? 0);
$withdrawn = $withdrawnMap[$uid] ?? 0;
$uid = (int) ($inv['id'] ?? 0);
$totalFen = (int) ($commStats[$uid]['totalFen'] ?? 0);
$paidFen = (int) ($commStats[$uid]['paidFen'] ?? 0);
$withdrawn = (int) ($withdrawnMap[$uid] ?? 0);
$avail = max(0, $paidFen - $withdrawn);
$list[] = [
'id' => $uid,
@@ -189,7 +270,7 @@ class Distribution extends BaseController
'avatar' => $inv['avatar'] ?? '',
'totalCommission' => number_format($totalFen / 100, 2, '.', ''),
'availableCommission'=> number_format($avail / 100, 2, '.', ''),
'teamCount' => $teamMap[$uid] ?? 0,
'teamCount' => (int) ($teamMap[$uid] ?? 0),
'teamPerformance' => '-',
'inviteCode' => '-',
'level' => '-',
@@ -213,7 +294,11 @@ class Distribution extends BaseController
return error('无权限', 403);
}
$enterpriseId = (int) ($user['enterpriseId'] ?? 0);
$enterpriseId = $this->resolveEnterpriseIdForAdmin($user);
if ($enterpriseId <= 0) {
return error('未绑定企业或企业无效', 400);
}
$page = max(1, (int) Request::param('page', 1));
$pageSize = min(100, (int) Request::param('pageSize', 20));
$status = Request::param('status', '');
@@ -226,7 +311,16 @@ class Distribution extends BaseController
->leftJoin('wechat_users invt', 'b.inviteeId = invt.id')
->field('b.*, inv.nickname as inviterName, inv.avatar as inviterAvatar,
invt.nickname as inviteeName, invt.avatar as inviteeAvatar')
->where('b.enterpriseId', $enterpriseId);
->where(function ($q) use ($enterpriseId) {
$q->where('b.enterpriseId', $enterpriseId)
->whereOr(function ($q2) use ($enterpriseId) {
$q2->whereNull('b.enterpriseId')
->where(function ($q3) use ($enterpriseId) {
$q3->where('inv.enterpriseId', $enterpriseId)
->whereOr('invt.enterpriseId', $enterpriseId);
});
});
});
if ($inviterId > 0) {
$query->where('b.inviterId', $inviterId);
@@ -264,7 +358,11 @@ class Distribution extends BaseController
return error('无权限', 403);
}
$enterpriseId = (int) ($user['enterpriseId'] ?? 0);
$enterpriseId = $this->resolveEnterpriseIdForAdmin($user);
if ($enterpriseId <= 0) {
return error('未绑定企业或企业无效', 400);
}
$page = max(1, (int) Request::param('page', 1));
$pageSize = min(100, (int) Request::param('pageSize', 20));
$status = Request::param('status', '');
@@ -279,7 +377,7 @@ class Distribution extends BaseController
->where('c.enterpriseId', $enterpriseId);
if ($inviterId > 0) {
$query->where('c.inviterId', $inviterId);
$query->whereRaw('COALESCE(c.inviterId, c.agentId) = ?', [$inviterId]);
}
if ($status) {
$query->where('c.status', $status);
@@ -356,7 +454,11 @@ class Distribution extends BaseController
return error('无权限', 403);
}
$enterpriseId = (int) ($user['enterpriseId'] ?? 0);
$enterpriseId = $this->resolveEnterpriseIdForAdmin($user);
if ($enterpriseId <= 0) {
return error('未绑定企业或企业无效', 400);
}
$page = max(1, (int) Request::param('page', 1));
$pageSize = min(100, (int) Request::param('pageSize', 20));
$status = Request::param('status', '');
@@ -426,7 +528,11 @@ class Distribution extends BaseController
return error('无权限', 403);
}
$enterpriseId = (int) ($user['enterpriseId'] ?? 0);
$enterpriseId = $this->resolveEnterpriseIdForAdmin($user);
if ($enterpriseId <= 0) {
return error('未绑定企业或企业无效', 400);
}
$note = Request::param('note', '');
$now = time();
@@ -497,7 +603,11 @@ class Distribution extends BaseController
return error('无权限', 403);
}
$enterpriseId = (int) ($user['enterpriseId'] ?? 0);
$enterpriseId = $this->resolveEnterpriseIdForAdmin($user);
if ($enterpriseId <= 0) {
return error('未绑定企业或企业无效', 400);
}
$note = Request::param('note', '');
$now = time();
@@ -549,7 +659,10 @@ class Distribution extends BaseController
return error('无权限', 403);
}
$enterpriseId = (int) ($user['enterpriseId'] ?? 0);
$enterpriseId = $this->resolveEnterpriseIdForAdmin($user);
if ($enterpriseId <= 0) {
return error('未绑定企业或企业无效', 400);
}
try {
$config = Db::name('system_config')
@@ -593,7 +706,11 @@ class Distribution extends BaseController
return error('无权限', 403);
}
$enterpriseId = (int) ($user['enterpriseId'] ?? 0);
$enterpriseId = $this->resolveEnterpriseIdForAdmin($user);
if ($enterpriseId <= 0) {
return error('未绑定企业或企业无效', 400);
}
$settings = Request::only(['enabled', 'promoCenterTitle', 'bindingDays', 'testSettings']);
$promoTitle = trim((string)($settings['promoCenterTitle'] ?? ''));

View File

@@ -69,6 +69,10 @@ class Order extends BaseController
}
}
// 与列表相同筛选条件下的全量统计(非当前页):已支付/已完成单数、实收金额(分)
$paidCompletedCount = (int) (clone $query)->whereIn('status', ['paid', 'completed'])->count();
$totalRevenueFen = (int) (clone $query)->whereIn('status', ['paid', 'completed'])->sum('amount');
$query->order('createdAt', 'desc');
$total = (int) (clone $query)->count();
$list = (clone $query)->page($page, $pageSize)->select()->toArray();
@@ -122,7 +126,16 @@ class Order extends BaseController
$row['testData'] = $testsByOrder[$row['id']] ?? [];
}
return paginate_response($list, $total, $page, $pageSize);
return success([
'list' => $list,
'total' => $total,
'page' => $page,
'pageSize' => $pageSize,
'hasMore' => ($page * $pageSize) < $total,
// 看板卡片:全量口径(随 keyword / status / productType / 企业 筛选变化,与 total 一致)
'paidCompletedCount' => $paidCompletedCount,
'totalRevenueFen' => $totalRevenueFen,
]);
}
/**