|
@@ -5,13 +5,21 @@ namespace App\Services;
|
|
|
use App\Models\User;
|
|
use App\Models\User;
|
|
|
use App\Models\UserLogin;
|
|
use App\Models\UserLogin;
|
|
|
use Carbon\Carbon;
|
|
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 时间戳)
|
|
|
|
|
+ *
|
|
|
|
|
+ * 说明:last_active_time 多半只在登录时更新,仅靠它会导致「有会话却查不到」。
|
|
|
*/
|
|
*/
|
|
|
class OnlineUserService
|
|
class OnlineUserService
|
|
|
{
|
|
{
|
|
|
- /** 最近活跃秒数 */
|
|
|
|
|
|
|
+ /** 最近活跃兜底窗口(秒) */
|
|
|
public const ONLINE_WINDOW_SECONDS = 1800;
|
|
public const ONLINE_WINDOW_SECONDS = 1800;
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
@@ -22,23 +30,52 @@ class OnlineUserService
|
|
|
{
|
|
{
|
|
|
$page = max(1, (int)($params['page'] ?? 1));
|
|
$page = max(1, (int)($params['page'] ?? 1));
|
|
|
$limit = min(200, max(1, (int)($params['limit'] ?? 15)));
|
|
$limit = min(200, max(1, (int)($params['limit'] ?? 15)));
|
|
|
- $since = time() - self::ONLINE_WINDOW_SECONDS;
|
|
|
|
|
|
|
+ $now = time();
|
|
|
|
|
+ $activeSince = $now - self::ONLINE_WINDOW_SECONDS;
|
|
|
|
|
|
|
|
- $latestLogin = UserLogin::query()
|
|
|
|
|
|
|
+ $hasLoginStatus = Schema::hasColumn('user_login', 'status');
|
|
|
|
|
+ $hasLoginDomain = Schema::hasColumn('user_login', 'login_domain');
|
|
|
|
|
+ $hasPlatform = Schema::hasColumn('user_login', 'platform');
|
|
|
|
|
+ $hasBrowser = Schema::hasColumn('user_login', 'browser');
|
|
|
|
|
+ $hasOs = Schema::hasColumn('user_login', 'os');
|
|
|
|
|
+ $hasCountry = Schema::hasColumn('user_login', 'country');
|
|
|
|
|
+
|
|
|
|
|
+ // 每位用户最近一次成功登录
|
|
|
|
|
+ $loginBase = DB::table('user_login');
|
|
|
|
|
+ if ($hasLoginStatus) {
|
|
|
|
|
+ $loginBase->where('status', UserLogin::STATUS_SUCCESS);
|
|
|
|
|
+ }
|
|
|
|
|
+ $latestLogin = $loginBase
|
|
|
->selectRaw('user_id, MAX(id) as id')
|
|
->selectRaw('user_id, MAX(id) as id')
|
|
|
- ->where('status', UserLogin::STATUS_SUCCESS)
|
|
|
|
|
->groupBy('user_id');
|
|
->groupBy('user_id');
|
|
|
|
|
|
|
|
- $query = User::query()
|
|
|
|
|
- ->from('users')
|
|
|
|
|
|
|
+ $query = DB::table('users')
|
|
|
->leftJoin('wallets', 'wallets.member_id', '=', 'users.member_id')
|
|
->leftJoin('wallets', 'wallets.member_id', '=', 'users.member_id')
|
|
|
->leftJoinSub($latestLogin, 'latest_login', function ($join) {
|
|
->leftJoinSub($latestLogin, 'latest_login', function ($join) {
|
|
|
$join->on('latest_login.user_id', '=', 'users.user_id');
|
|
$join->on('latest_login.user_id', '=', 'users.user_id');
|
|
|
})
|
|
})
|
|
|
->leftJoin('user_login as ul', 'ul.id', '=', 'latest_login.id')
|
|
->leftJoin('user_login as ul', 'ul.id', '=', 'latest_login.id')
|
|
|
- ->where('users.last_active_time', '>=', $since)
|
|
|
|
|
|
|
+ ->where(function ($q) use ($now, $activeSince) {
|
|
|
|
|
+ // 1) 有效 token 会话
|
|
|
|
|
+ $q->whereExists(function ($sub) use ($now) {
|
|
|
|
|
+ $sub->select(DB::raw(1))
|
|
|
|
|
+ ->from('user_session as us')
|
|
|
|
|
+ ->where(function ($w) {
|
|
|
|
|
+ $w->whereColumn('us.user_id', 'users.user_id')
|
|
|
|
|
+ ->orWhereColumn('us.user_id', 'users.member_id');
|
|
|
|
|
+ })
|
|
|
|
|
+ ->where('us.expire_time', '>', $now);
|
|
|
|
|
+ })
|
|
|
|
|
+ // 2) 或最近活跃(unix 秒级时间戳)
|
|
|
|
|
+ ->orWhere(function ($q2) use ($activeSince) {
|
|
|
|
|
+ $q2->where('users.last_active_time', '>', 1000000000)
|
|
|
|
|
+ ->where('users.last_active_time', '>=', $activeSince);
|
|
|
|
|
+ });
|
|
|
|
|
+ })
|
|
|
|
|
+ // 排除虚拟游客
|
|
|
->where(function ($q) {
|
|
->where(function ($q) {
|
|
|
- $q->whereNull('users.from')->orWhere('users.from', '<>', '2');
|
|
|
|
|
|
|
+ $q->whereNull('users.from')
|
|
|
|
|
+ ->orWhere('users.from', '<>', 2);
|
|
|
});
|
|
});
|
|
|
|
|
|
|
|
if (!empty($params['member_id'])) {
|
|
if (!empty($params['member_id'])) {
|
|
@@ -53,11 +90,11 @@ class OnlineUserService
|
|
|
if (!empty($params['login_ip'])) {
|
|
if (!empty($params['login_ip'])) {
|
|
|
$query->where('ul.login_ip', 'like', '%' . $params['login_ip'] . '%');
|
|
$query->where('ul.login_ip', 'like', '%' . $params['login_ip'] . '%');
|
|
|
}
|
|
}
|
|
|
- if (!empty($params['platform']) && $params['platform'] !== '全部') {
|
|
|
|
|
|
|
+ if (!empty($params['platform']) && $params['platform'] !== '全部' && $hasPlatform) {
|
|
|
$query->where('ul.platform', $params['platform']);
|
|
$query->where('ul.platform', $params['platform']);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- $query->select([
|
|
|
|
|
|
|
+ $select = [
|
|
|
'users.id',
|
|
'users.id',
|
|
|
'users.member_id',
|
|
'users.member_id',
|
|
|
'users.user_id',
|
|
'users.user_id',
|
|
@@ -65,44 +102,62 @@ class OnlineUserService
|
|
|
'users.last_active_time',
|
|
'users.last_active_time',
|
|
|
'wallets.available_balance',
|
|
'wallets.available_balance',
|
|
|
'ul.login_ip',
|
|
'ul.login_ip',
|
|
|
- 'ul.country',
|
|
|
|
|
- 'ul.login_domain',
|
|
|
|
|
- 'ul.platform',
|
|
|
|
|
- 'ul.browser',
|
|
|
|
|
- 'ul.os',
|
|
|
|
|
'ul.created_at as login_time',
|
|
'ul.created_at as login_time',
|
|
|
- ]);
|
|
|
|
|
|
|
+ ];
|
|
|
|
|
+ if ($hasCountry) {
|
|
|
|
|
+ $select[] = 'ul.country';
|
|
|
|
|
+ }
|
|
|
|
|
+ if ($hasLoginDomain) {
|
|
|
|
|
+ $select[] = 'ul.login_domain';
|
|
|
|
|
+ }
|
|
|
|
|
+ if ($hasPlatform) {
|
|
|
|
|
+ $select[] = 'ul.platform';
|
|
|
|
|
+ }
|
|
|
|
|
+ if ($hasBrowser) {
|
|
|
|
|
+ $select[] = 'ul.browser';
|
|
|
|
|
+ }
|
|
|
|
|
+ if ($hasOs) {
|
|
|
|
|
+ $select[] = 'ul.os';
|
|
|
|
|
+ }
|
|
|
|
|
+ $query->select($select);
|
|
|
|
|
|
|
|
- $total = (int)(clone $query)->toBase()->distinct()->count('users.id');
|
|
|
|
|
|
|
+ $total = (int)(clone $query)
|
|
|
|
|
+ ->reorder()
|
|
|
|
|
+ ->selectRaw('COUNT(DISTINCT users.id) as aggregate')
|
|
|
|
|
+ ->value('aggregate');
|
|
|
|
|
|
|
|
- $list = $query
|
|
|
|
|
|
|
+ $rows = $query
|
|
|
->orderByDesc('users.last_active_time')
|
|
->orderByDesc('users.last_active_time')
|
|
|
- ->forPage($page, $limit)
|
|
|
|
|
- ->get()
|
|
|
|
|
- ->map(function ($row) {
|
|
|
|
|
- return [
|
|
|
|
|
- 'member_id' => $row->member_id ?: $row->user_id,
|
|
|
|
|
- 'first_name' => (string)($row->first_name ?? ''),
|
|
|
|
|
- 'available_balance' => (float)($row->available_balance ?? 0),
|
|
|
|
|
- 'login_ip' => (string)($row->login_ip ?? ''),
|
|
|
|
|
- 'location' => (string)($row->country ?? ''),
|
|
|
|
|
- 'login_domain' => (string)($row->login_domain ?? ''),
|
|
|
|
|
- 'platform' => (string)($row->platform ?: self::guessPlatformFromOs((string)($row->os ?? ''))),
|
|
|
|
|
- 'browser' => (string)($row->browser ?? ''),
|
|
|
|
|
- 'os' => (string)($row->os ?? ''),
|
|
|
|
|
- 'login_time' => self::formatTime($row->login_time ?? null),
|
|
|
|
|
- 'last_active_time' => $row->last_active_time,
|
|
|
|
|
- ];
|
|
|
|
|
- })
|
|
|
|
|
- ->values()
|
|
|
|
|
- ->all();
|
|
|
|
|
|
|
+ ->offset(($page - 1) * $limit)
|
|
|
|
|
+ ->limit($limit)
|
|
|
|
|
+ ->get();
|
|
|
|
|
+
|
|
|
|
|
+ $data = $rows->map(function ($row) {
|
|
|
|
|
+ return [
|
|
|
|
|
+ 'member_id' => $row->member_id ?: $row->user_id,
|
|
|
|
|
+ 'first_name' => (string)($row->first_name ?? ''),
|
|
|
|
|
+ 'available_balance' => (float)($row->available_balance ?? 0),
|
|
|
|
|
+ '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 ?? ''))
|
|
|
|
|
+ ),
|
|
|
|
|
+ 'browser' => (string)($row->browser ?? ''),
|
|
|
|
|
+ 'os' => (string)($row->os ?? ''),
|
|
|
|
|
+ 'login_time' => self::formatTime($row->login_time ?? null),
|
|
|
|
|
+ 'last_active_time' => self::formatLastActive($row->last_active_time ?? null),
|
|
|
|
|
+ ];
|
|
|
|
|
+ })->values()->all();
|
|
|
|
|
|
|
|
- return ['total' => $total, 'data' => $list];
|
|
|
|
|
|
|
+ return ['total' => $total, 'data' => $data];
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
private static function formatTime($value): string
|
|
private static function formatTime($value): string
|
|
|
{
|
|
{
|
|
|
- if (empty($value)) {
|
|
|
|
|
|
|
+ if ($value === null || $value === '') {
|
|
|
return '';
|
|
return '';
|
|
|
}
|
|
}
|
|
|
try {
|
|
try {
|
|
@@ -112,6 +167,17 @@ class OnlineUserService
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ private static function formatLastActive($value): string
|
|
|
|
|
+ {
|
|
|
|
|
+ if ($value === null || $value === '' || $value === 0 || $value === '0') {
|
|
|
|
|
+ return '';
|
|
|
|
|
+ }
|
|
|
|
|
+ if (is_numeric($value) && (int)$value > 1000000000) {
|
|
|
|
|
+ return Carbon::createFromTimestamp((int)$value, 'Asia/Shanghai')->format('Y-m-d H:i:s');
|
|
|
|
|
+ }
|
|
|
|
|
+ return self::formatTime($value);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
private static function guessPlatformFromOs(string $os): string
|
|
private static function guessPlatformFromOs(string $os): string
|
|
|
{
|
|
{
|
|
|
$os = strtolower($os);
|
|
$os = strtolower($os);
|