171 lines
5.6 KiB
PHP
171 lines
5.6 KiB
PHP
<?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);
|