initialize(); // 参数解析 $argMap = []; foreach ($argv as $arg) { if (strpos($arg, '=') !== false) { list($k, $v) = explode('=', $arg, 2); $argMap[trim($k)] = trim($v); } else { $argMap[trim($arg)] = true; } } $phone = $argMap['phone'] ?? ''; $stage = $argMap['stage'] ?? 'before'; $mode = isset($argMap['diff']) ? 'diff' : 'snapshot'; if (!$phone || !preg_match('/^1[3-9]\d{9}$/', $phone)) { fwrite(STDERR, "usage: snapshot.php phone=1XXXXXXXXXX stage=before|after\n"); fwrite(STDERR, " snapshot.php diff phone=1XXXXXXXXXX\n"); exit(1); } $runtimeDir = dirname(__DIR__, 2) . '/runtime/migration/' . $phone; if (!is_dir($runtimeDir)) { @mkdir($runtimeDir, 0755, true); } /** * 采集快照 * * @return array */ function collectSnapshot(string $phone): array { $users = Db::name('users') ->where('phone', $phone) ->whereOr('account', $phone) ->select(); $userIds = array_column((array)$users, 'id'); $companyIds = array_values(array_unique(array_filter(array_column((array)$users, 'companyId')))); $accounts = Db::table('s2_company_account') ->whereIn('departmentId', $companyIds ?: [0]) ->column('id'); $deviceTotal = $accounts ? (int)Db::table('s2_device')->whereIn('currentAccountId', $accounts)->where(['isDeleted' => 0])->count() : 0; $wechatTotal = $accounts ? (int)Db::table('s2_wechat_account')->whereIn('deviceAccountId', $accounts)->count() : 0; $friendTotal = $accounts ? (int)Db::table('s2_wechat_friend')->whereIn('accountId', $accounts)->where(['isDeleted' => 0])->count() : 0; $planRows = $companyIds ? Db::name('customer_acquisition_task') ->whereIn('companyId', $companyIds) ->where('deleteTime', 0) ->field('sceneId, status, COUNT(*) as cnt') ->group('sceneId, status') ->select() : []; $tokens = $userIds ? Db::name('tokens_company')->whereIn('userId', $userIds)->select() : []; return [ 'phone' => $phone, 'collectedAt' => date('c'), 'userIds' => $userIds, 'companyIds' => $companyIds, 'accountIds' => array_map('intval', $accounts ?: []), 'totals' => [ 'users' => count($userIds), 'companies' => count($companyIds), 's2Accounts' => count($accounts ?: []), 'devices' => $deviceTotal, 'wechats' => $wechatTotal, 'friends' => $friendTotal, ], 'plans' => array_map(static function ($r) { return [ 'sceneId' => (int)$r['sceneId'], 'status' => (int)$r['status'], 'count' => (int)$r['cnt'], ]; }, (array)$planRows), 'tokens' => array_map(static function ($t) { return [ 'userId' => (int)($t['userId'] ?? 0), 'balance' => (int)($t['totalTokens'] ?? 0), 'used' => (int)($t['totalConsumed'] ?? 0), ]; }, (array)$tokens), ]; } if ($mode === 'diff') { $files = glob($runtimeDir . '/snapshot-*.json'); if (!$files || count($files) < 2) { fwrite(STDERR, "需要至少两个快照(before / after)才能 diff,当前: " . count($files) . "\n"); exit(2); } sort($files); $before = json_decode((string)file_get_contents($files[0]), true); $after = json_decode((string)file_get_contents(end($files)), true); $diff = []; foreach ($before['totals'] ?? [] as $k => $v) { $aft = $after['totals'][$k] ?? 0; $diff[$k] = ['before' => $v, 'after' => $aft, 'delta' => $aft - $v]; } $report = [ 'phone' => $phone, 'beforeAt' => $before['collectedAt'] ?? null, 'afterAt' => $after['collectedAt'] ?? null, 'beforeFile' => basename($files[0]), 'afterFile' => basename(end($files)), 'totals' => $diff, 'planBefore' => $before['plans'] ?? [], 'planAfter' => $after['plans'] ?? [], 'tokensBefore' => $before['tokens'] ?? [], 'tokensAfter' => $after['tokens'] ?? [], ]; $reportPath = $runtimeDir . '/diff-' . date('YmdHis') . '.json'; file_put_contents($reportPath, json_encode($report, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE)); echo "diff written: $reportPath\n"; echo json_encode($diff, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE) . "\n"; exit(0); } $snap = collectSnapshot($phone); $path = $runtimeDir . '/snapshot-' . $stage . '-' . date('YmdHis') . '.json'; file_put_contents($path, json_encode($snap, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE)); echo "snapshot written: $path\n"; echo "totals: " . json_encode($snap['totals'], JSON_UNESCAPED_UNICODE) . "\n"; exit(0);