|
|
@@ -3,17 +3,20 @@
|
|
|
namespace App\Services;
|
|
|
|
|
|
use App\Models\UserLogin;
|
|
|
+use App\Support\LoginPlatform;
|
|
|
use Carbon\Carbon;
|
|
|
use Illuminate\Support\Facades\DB;
|
|
|
use Illuminate\Support\Facades\Schema;
|
|
|
|
|
|
/**
|
|
|
- * 在线用户列表(只读)
|
|
|
+ * 在线用户(只读)
|
|
|
*
|
|
|
* 在线判定(满足其一即可):
|
|
|
* 1. 存在未过期 user_session(expire_time 为 unix 时间戳)
|
|
|
* 2. last_active_time 在最近 ONLINE_WINDOW_SECONDS 内(unix 时间戳)
|
|
|
*
|
|
|
+ * platform 口径由登录写入侧一次写定;本服务只读、映射,不再二次推断 APP/H5。
|
|
|
+ *
|
|
|
* 注意:本库表前缀 + 多表 collation 不一致,涉及跨表字符串比较一律用带前缀表名 + COLLATE。
|
|
|
*/
|
|
|
class OnlineUserService
|
|
|
@@ -28,11 +31,9 @@ class OnlineUserService
|
|
|
{
|
|
|
$page = max(1, (int)($params['page'] ?? 1));
|
|
|
$limit = min(200, max(1, (int)($params['limit'] ?? 15)));
|
|
|
- $now = time();
|
|
|
- $activeSince = $now - self::ONLINE_WINDOW_SECONDS;
|
|
|
$offset = ($page - 1) * $limit;
|
|
|
|
|
|
- $p = DB::getTablePrefix(); // bot_
|
|
|
+ $p = DB::getTablePrefix();
|
|
|
$users = $p . 'users';
|
|
|
$session = $p . 'user_session';
|
|
|
$wallets = $p . 'wallets';
|
|
|
@@ -45,26 +46,8 @@ class OnlineUserService
|
|
|
$hasOs = Schema::hasColumn('user_login', 'os');
|
|
|
$hasCountry = Schema::hasColumn('user_login', 'country');
|
|
|
|
|
|
- $where = [];
|
|
|
- $bindings = [];
|
|
|
-
|
|
|
- // 在线条件
|
|
|
- $where[] = "(
|
|
|
- EXISTS (
|
|
|
- SELECT 1 FROM {$session} us
|
|
|
- WHERE (
|
|
|
- us.user_id COLLATE utf8mb4_general_ci = u.user_id COLLATE utf8mb4_general_ci
|
|
|
- OR us.user_id COLLATE utf8mb4_general_ci = u.member_id COLLATE utf8mb4_general_ci
|
|
|
- )
|
|
|
- AND us.expire_time > ?
|
|
|
- )
|
|
|
- OR (u.last_active_time > 1000000000 AND u.last_active_time >= ?)
|
|
|
- )";
|
|
|
- $bindings[] = $now;
|
|
|
- $bindings[] = $activeSince;
|
|
|
-
|
|
|
- // 排除虚拟游客
|
|
|
- $where[] = "(u.`from` IS NULL OR u.`from` <> 2)";
|
|
|
+ [$onlineSql, $bindings] = self::onlinePredicateSql($session, 'u');
|
|
|
+ $where = [$onlineSql, '(u.`from` IS NULL OR u.`from` <> 2)'];
|
|
|
|
|
|
if (!empty($params['member_id'])) {
|
|
|
$where[] = '(u.member_id = ? OR u.user_id = ?)';
|
|
|
@@ -76,8 +59,6 @@ class OnlineUserService
|
|
|
$bindings[] = '%' . $params['first_name'] . '%';
|
|
|
}
|
|
|
|
|
|
- // 登录 IP / 平台:按最近一次成功登录筛
|
|
|
- $loginJoinExtra = '';
|
|
|
$statusCond = $hasLoginStatus ? 'AND status = ' . (int)UserLogin::STATUS_SUCCESS : '';
|
|
|
if (!empty($params['login_ip']) || (!empty($params['platform']) && $params['platform'] !== '全部' && $hasPlatform)) {
|
|
|
$loginFilters = [];
|
|
|
@@ -170,11 +151,8 @@ class OnlineUserService
|
|
|
'login_ip' => (string)($row['login_ip'] ?? ''),
|
|
|
'location' => (string)($row['country'] ?? ''),
|
|
|
'login_domain' => (string)($row['login_domain'] ?? ''),
|
|
|
- 'platform' => (string)(
|
|
|
- ($row['platform'] ?? '') !== ''
|
|
|
- ? $row['platform']
|
|
|
- : self::guessPlatformFromOs((string)($row['os'] ?? ''))
|
|
|
- ),
|
|
|
+ // 不二次猜测:无 platform 则空,避免与计数口径漂移
|
|
|
+ 'platform' => (string)($row['platform'] ?? ''),
|
|
|
'browser' => (string)($row['browser'] ?? ''),
|
|
|
'os' => (string)($row['os'] ?? ''),
|
|
|
'login_time' => self::formatTime($row['login_time'] ?? null),
|
|
|
@@ -185,6 +163,88 @@ class OnlineUserService
|
|
|
return ['total' => $total, 'data' => $data];
|
|
|
}
|
|
|
|
|
|
+ /**
|
|
|
+ * 当前在线用户按端计数(与 list 相同在线判定)
|
|
|
+ *
|
|
|
+ * 信任 user_login.platform 写入值,GROUP BY 后映射:
|
|
|
+ * - apple ← iOS
|
|
|
+ * - android ← Android
|
|
|
+ * - pc ← PC
|
|
|
+ * - mobile ← H5 + WeChat(手机 H5)
|
|
|
+ *
|
|
|
+ * @return array{total:int,apple:int,android:int,pc:int,mobile:int,wechat:int,unknown:int}
|
|
|
+ */
|
|
|
+ public static function platformCounts(): array
|
|
|
+ {
|
|
|
+ $p = DB::getTablePrefix();
|
|
|
+ $users = $p . 'users';
|
|
|
+ $session = $p . 'user_session';
|
|
|
+ $login = $p . 'user_login';
|
|
|
+
|
|
|
+ $statusCond = Schema::hasColumn('user_login', 'status')
|
|
|
+ ? 'AND status = ' . (int)UserLogin::STATUS_SUCCESS
|
|
|
+ : '';
|
|
|
+
|
|
|
+ [$onlineSql, $bindings] = self::onlinePredicateSql($session, 'u');
|
|
|
+ $whereSql = $onlineSql . ' AND (u.`from` IS NULL OR u.`from` <> 2)';
|
|
|
+
|
|
|
+ $rows = DB::select(
|
|
|
+ "SELECT
|
|
|
+ COALESCE(NULLIF(ul.platform, ''), '') AS platform,
|
|
|
+ COUNT(*) AS c
|
|
|
+ FROM {$users} u
|
|
|
+ LEFT JOIN (
|
|
|
+ SELECT user_id, MAX(id) AS max_id
|
|
|
+ FROM {$login}
|
|
|
+ WHERE 1=1 {$statusCond}
|
|
|
+ GROUP BY user_id
|
|
|
+ ) latest ON latest.user_id COLLATE utf8mb4_general_ci = u.user_id COLLATE utf8mb4_general_ci
|
|
|
+ LEFT JOIN {$login} ul ON ul.id = latest.max_id
|
|
|
+ WHERE {$whereSql}
|
|
|
+ GROUP BY COALESCE(NULLIF(ul.platform, ''), '')",
|
|
|
+ $bindings
|
|
|
+ );
|
|
|
+
|
|
|
+ $counts = LoginPlatform::emptyCounts();
|
|
|
+ foreach ($rows as $row) {
|
|
|
+ $platform = (string)($row->platform ?? '');
|
|
|
+ $n = (int)($row->c ?? 0);
|
|
|
+ $counts['total'] += $n;
|
|
|
+
|
|
|
+ $bucket = LoginPlatform::toBucket($platform);
|
|
|
+ $counts[$bucket] += $n;
|
|
|
+
|
|
|
+ if (in_array(strtolower($platform), ['wechat', '微信'], true)) {
|
|
|
+ $counts['wechat'] += $n;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return $counts;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 在线判定 SQL 片段 + bindings(now, activeSince)
|
|
|
+ *
|
|
|
+ * @return array{0:string,1:list<int>}
|
|
|
+ */
|
|
|
+ private static function onlinePredicateSql(string $sessionTable, string $userAlias = 'u'): array
|
|
|
+ {
|
|
|
+ $now = time();
|
|
|
+ $activeSince = $now - self::ONLINE_WINDOW_SECONDS;
|
|
|
+ $sql = "(
|
|
|
+ EXISTS (
|
|
|
+ SELECT 1 FROM {$sessionTable} us
|
|
|
+ WHERE (
|
|
|
+ us.user_id COLLATE utf8mb4_general_ci = {$userAlias}.user_id COLLATE utf8mb4_general_ci
|
|
|
+ OR us.user_id COLLATE utf8mb4_general_ci = {$userAlias}.member_id COLLATE utf8mb4_general_ci
|
|
|
+ )
|
|
|
+ AND us.expire_time > ?
|
|
|
+ )
|
|
|
+ OR ({$userAlias}.last_active_time > 1000000000 AND {$userAlias}.last_active_time >= ?)
|
|
|
+ )";
|
|
|
+ return [$sql, [$now, $activeSince]];
|
|
|
+ }
|
|
|
+
|
|
|
private static function formatTime($value): string
|
|
|
{
|
|
|
if ($value === null || $value === '') {
|
|
|
@@ -207,16 +267,4 @@ class OnlineUserService
|
|
|
}
|
|
|
return self::formatTime($value);
|
|
|
}
|
|
|
-
|
|
|
- private static function guessPlatformFromOs(string $os): string
|
|
|
- {
|
|
|
- $os = strtolower($os);
|
|
|
- if (str_contains($os, 'android')) {
|
|
|
- return 'Android';
|
|
|
- }
|
|
|
- if (str_contains($os, 'ios') || str_contains($os, 'iphone') || str_contains($os, 'ipad')) {
|
|
|
- return 'iOS';
|
|
|
- }
|
|
|
- return 'PC';
|
|
|
- }
|
|
|
}
|