Files
cunkebao_v3/Server/script/migration/snapshot.php
Manus AI 5517457929 sync: 以本地为准同步全量变更至 GitHub
含四端需求文档、前端/后端/超管/触客宝迭代及部署脚本更新;未拉取远程。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-30 04:24:56 +08:00

171 lines
5.6 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
/**
* 存客宝数据迁移 · 快照脚本
*
* 用途:在迁移前/迁移后采集账号、公司、设备、微信、好友、计划、积分快照,
* 并对比生成差异报表。仅本地执行,不直接修改业务数据。
*
* 用法:
* docker compose exec -T server php /var/www/html/script/migration/snapshot.php phone=13779954946 stage=before
* docker compose exec -T server php /var/www/html/script/migration/snapshot.php phone=13779954946 stage=after
* docker compose exec -T server php /var/www/html/script/migration/snapshot.php diff phone=13779954946
*
* 输出文件位于 Server/runtime/migration/{phone}/{stage}-{timestamp}.json
*
* 卡若 2026-05-27 · 数据迁移 D1/D2/E1 快照需求
*/
declare(strict_types=1);
require __DIR__ . '/../../thinkphp/base.php';
use think\Db;
use think\App;
$app = new App(realpath(__DIR__ . '/../../application'));
$app->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);