You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
wx.sstbc.com/cleanup-imported-supply.php

81 lines
6.4 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

<?php
// CLI only. Upload to the Laravel project root, beside artisan.
if (PHP_SAPI !== 'cli') { http_response_code(403); exit; }
class ImportedSupplyCleanup
{
public static function run($execute, $backupDirectory)
{
$db = \Illuminate\Support\Facades\DB::connection();
foreach (['supply_demands'=>['id','is_proxy','import_key','deleted_at','updated_at'], 'dialogues'=>['id','supply_demand_id'], 'messages'=>['id','supply_demand_id','dialogue_id']] as $table=>$columns) {
if (!$db->getSchemaBuilder()->hasColumns($table,$columns)) throw new \RuntimeException('缺少必要表或字段:'.$table.',已停止');
}
if ($execute && $db->getDriverName() === 'mysql') {
foreach (['supply_demands','dialogues','messages'] as $table) {
$engine=$db->selectOne('SELECT ENGINE AS engine FROM information_schema.TABLES WHERE TABLE_SCHEMA = ? AND TABLE_NAME = ?',[$db->getDatabaseName(),$db->getTablePrefix().$table]);
if (!$engine || strcasecmp($engine->engine,'InnoDB') !== 0) throw new \RuntimeException('表不支持预期事务保护:'.$table.',已停止');
}
}
return $db->transaction(function () use ($db,$execute,$backupDirectory) {
// Raw queries deliberately include previously soft-deleted imported rows.
$demands=$db->table('supply_demands')->where('is_proxy',1)->whereNotNull('import_key')->where('import_key','!=','')->orderBy('id')->lockForUpdate()->get();
$ids=$demands->pluck('id')->all();
$dialogues=$db->table('dialogues')->whereIn('supply_demand_id',$ids)->orderBy('id')->lockForUpdate()->get();
$dialogueIds=$dialogues->pluck('id')->all();
$messages=$db->table('messages')->where(function($q)use($ids,$dialogueIds){$q->whereIn('supply_demand_id',$ids)->orWhereIn('dialogue_id',$dialogueIds);})->orderBy('id')->lockForUpdate()->get();
$report=['database'=>$db->getDatabaseName(),'host'=>$db->getConfig('host'),'mode'=>$execute?'备份并按条件清理':'只读检查', 'imported_demands'=>count($ids),'dialogues'=>$dialogues->count(),'messages'=>$messages->count(),'cleaned'=>0,'backup'=>null];
if (!$execute || !$ids) {
$report['result']=!$ids?'没有需要清理的旧导入记录':($messages->count()?'存在历史私信,不允许清理':'未发现历史私信,可使用 --execute 备份并清理');
return $report;
}
$snapshot=['format_version'=>1,'created_at'=>date(DATE_ATOM),'database'=>$db->getDatabaseName(),'host'=>$db->getConfig('host'),'scope'=>'is_proxy=1 AND import_key IS NOT NULL AND import_key <> empty','supply_demands'=>$demands->all(),'dialogues'=>$dialogues->all(),'messages'=>$messages->all()];
$report['backup']=self::backup($backupDirectory,$snapshot);
if ($messages->count()) {
$report['result']='发现历史私信:已备份,未清理任何业务数据,请先核对';
return $report;
}
$now=date('Y-m-d H:i:s');
$report['cleaned']=$db->table('supply_demands')->whereIn('id',$ids)->where('is_proxy',1)->whereNotNull('import_key')->update(['deleted_at'=>$db->raw('COALESCE(deleted_at, CURRENT_TIMESTAMP)'),'import_key'=>null,'updated_at'=>$now]);
if ($report['cleaned'] !== count($ids)) throw new \RuntimeException('清理数量不符,事务已回滚,请保留备份并核对');
$remaining=$db->table('supply_demands')->whereIn('id',$ids)->where(function($q){$q->whereNull('deleted_at')->orWhereNotNull('import_key');})->exists();
if ($remaining) throw new \RuntimeException('清理结果校验失败,事务已回滚');
$report['result']='已备份并清理旧导入记录及查重标记,可以自行导入新表';
return $report;
});
}
private static function backup($directory, array $snapshot)
{
if (!is_dir($directory) && !mkdir($directory,0700,true) && !is_dir($directory)) throw new \RuntimeException('无法创建备份目录,停止清理');
if (!chmod($directory,0700)) throw new \RuntimeException('无法保护备份目录权限,停止清理');
$file=rtrim($directory,'/').'/imported-supply-'.date('Ymd-His').'-'.bin2hex(random_bytes(4)).'.json';
$json=json_encode($snapshot,JSON_UNESCAPED_UNICODE|JSON_PRETTY_PRINT|JSON_THROW_ON_ERROR);
$handle=fopen($file,'x');
if (!$handle) throw new \RuntimeException('无法创建备份文件,停止清理');
try {
if (!chmod($file,0600)) throw new \RuntimeException('无法保护备份文件权限,停止清理');
$offset=0;$size=strlen($json);
while ($offset<$size) { $written=fwrite($handle,substr($json,$offset)); if (!$written) throw new \RuntimeException('备份写入失败,停止清理'); $offset+=$written; }
if (!fflush($handle)) throw new \RuntimeException('备份刷新失败,停止清理');
if (function_exists('fsync') && !fsync($handle)) throw new \RuntimeException('备份落盘失败,停止清理');
} finally { fclose($handle); }
if (!hash_equals(hash('sha256',$json),hash_file('sha256',$file))) throw new \RuntimeException('备份校验失败,停止清理');
json_decode(file_get_contents($file),true,512,JSON_THROW_ON_ERROR);
return ['file'=>$file,'sha256'=>hash('sha256',$json)];
}
}
if (realpath($_SERVER['SCRIPT_FILENAME'] ?? '') === __FILE__) {
try {
if (!is_file(__DIR__.'/artisan') || !is_file(__DIR__.'/vendor/autoload.php')) throw new \RuntimeException('请把脚本放到包含 artisan 的 Laravel 项目根目录');
require __DIR__.'/vendor/autoload.php';
$app=require __DIR__.'/bootstrap/app.php';
$app->make(\Illuminate\Contracts\Console\Kernel::class)->bootstrap();
$args=array_slice($argv,1);
if ($args && $args !== ['--check'] && $args !== ['--execute']) throw new \RuntimeException('用法:php cleanup-imported-supply.php [--check|--execute]');
$report=ImportedSupplyCleanup::run($args === ['--execute'],storage_path('app/private/supply-demand-cleanup'));
// Printed only after the transaction has committed successfully.
echo json_encode($report,JSON_UNESCAPED_UNICODE|JSON_PRETTY_PRINT|JSON_THROW_ON_ERROR).PHP_EOL;
} catch (\Throwable $e) { fwrite(STDERR,'执行停止:'.$e->getMessage().PHP_EOL); exit(1); }
}