- 新增上传分类归一化处理,统一白名单校验规则 - 新增COS存储记录同步逻辑 - 新增分享功能支持,配置多页面分享参数 - 新增订单案例分享按钮 - 新增订单自动校验和回收命令 - 新增测试脚本和图片同步脚本 - 优化后台上传分类配置,统一分类映射
131 行
5.2 KiB
PHP
131 行
5.2 KiB
PHP
<?php
|
||
|
||
namespace app\admin\command;
|
||
|
||
use think\Config;
|
||
use think\console\Command;
|
||
use think\console\Input;
|
||
use think\console\input\Option;
|
||
use think\console\Output;
|
||
use think\Db;
|
||
use think\Loader;
|
||
|
||
/**
|
||
* 孤儿附件扫描:检查 fa_attachment 中的文件是否被任何业务表引用
|
||
*
|
||
* 用法:
|
||
* php think attachment:scan 仅扫描,输出孤儿清单
|
||
* php think attachment:scan --delete 扫描并删除孤儿(记录 + 本地文件 + COS 对象)
|
||
*/
|
||
class AttachmentScan extends Command
|
||
{
|
||
/**
|
||
* 命令配置
|
||
*/
|
||
protected function configure()
|
||
{
|
||
$this->setName('attachment:scan')
|
||
->addOption('delete', null, Option::VALUE_NONE, '删除孤儿附件(记录+文件+COS对象)')
|
||
->setDescription('扫描无业务引用的孤儿附件');
|
||
}
|
||
|
||
/**
|
||
* 命令执行
|
||
*/
|
||
protected function execute(Input $input, Output $output)
|
||
{
|
||
$config = Config::get('database');
|
||
$database = $config['database'];
|
||
$prefix = $config['prefix'];
|
||
|
||
// 1. 收集业务表所有文本类列(排除附件表自身)
|
||
$columns = Db::query(
|
||
"SELECT TABLE_NAME, COLUMN_NAME FROM information_schema.COLUMNS "
|
||
. "WHERE TABLE_SCHEMA = ? AND DATA_TYPE IN ('varchar','char','text','tinytext','mediumtext','longtext') "
|
||
. "AND TABLE_NAME NOT IN (?, CONCAT(?, 'attachment'))",
|
||
[$database, $database . '.' . $prefix . 'attachment', $prefix]
|
||
);
|
||
if (!$columns) {
|
||
$output->writeln('<error>未找到业务表列</error>');
|
||
return;
|
||
}
|
||
|
||
// 2. 遍历附件记录,逐列检查 url 引用(每列一次查询,OR 组合全部 url)
|
||
$attachments = Db::name('attachment')->field('id,url,storage,filename,uploadtime')->select();
|
||
$urls = array_column($attachments, 'url');
|
||
$referenced = []; // 有引用的 url 集合
|
||
foreach ($columns as $col) {
|
||
$table = $col['TABLE_NAME'];
|
||
$field = $col['COLUMN_NAME'];
|
||
$wheres = [];
|
||
$bind = [];
|
||
foreach ($urls as $i => $url) {
|
||
$wheres[] = "`{$field}` LIKE :u{$i}";
|
||
$bind["u{$i}"] = '%' . addcslashes($url, '%_') . '%';
|
||
}
|
||
$sql = "SELECT `{$field}` AS v FROM `{$table}` WHERE " . implode(' OR ', $wheres) . " LIMIT 500";
|
||
try {
|
||
$rows = Db::query($sql, $bind);
|
||
} catch (\Exception $e) {
|
||
continue; // 个别列类型不兼容时跳过
|
||
}
|
||
foreach ($rows as $row) {
|
||
$v = (string)$row['v'];
|
||
foreach ($urls as $url) {
|
||
if ($url !== '' && strpos($v, $url) !== false) {
|
||
$referenced[$url] = true;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// 3. 输出孤儿清单
|
||
$orphans = array_values(array_filter($attachments, function ($a) use ($referenced) {
|
||
return empty($referenced[$a['url']]);
|
||
}));
|
||
if (!$orphans) {
|
||
$output->writeln('<info>扫描完成:所有附件均有业务引用,无孤儿文件</info>');
|
||
return;
|
||
}
|
||
$output->writeln(sprintf('扫描完成:共 %d 条附件,孤儿 %d 条:', count($attachments), count($orphans)));
|
||
foreach ($orphans as $o) {
|
||
$output->writeln(sprintf(' [%d] %s (%s) %s', $o['id'], $o['url'], $o['storage'], date('Y-m-d', $o['uploadtime'])));
|
||
}
|
||
|
||
// 4. 可选删除:手动清理本地文件 / COS 对象 / 记录
|
||
if ($input->getOption('delete')) {
|
||
Loader::addNamespace('Qcloud\Cos', ROOT_PATH . 'addons/cos/library/Qcloud/Cos/');
|
||
$cosConfig = get_addon_config('cos');
|
||
$client = null;
|
||
if ($cosConfig && !empty($cosConfig['cdnurl'])) {
|
||
$client = new \Qcloud\Cos\Client([
|
||
'region' => $cosConfig['region'],
|
||
'schema' => 'https',
|
||
'credentials' => ['secretId' => $cosConfig['secretId'], 'secretKey' => $cosConfig['secretKey']],
|
||
]);
|
||
}
|
||
foreach ($orphans as $o) {
|
||
$id = $o['id'];
|
||
// 同 URL 成对记录一起删
|
||
$pairs = Db::name('attachment')->where('url', $o['url'])->select();
|
||
foreach ($pairs as $pair) {
|
||
if ($pair['storage'] === 'local') {
|
||
$file = ROOT_PATH . 'public' . $pair['url'];
|
||
if (is_file($file)) {
|
||
@unlink($file);
|
||
}
|
||
} elseif ($pair['storage'] === 'cos' && $client) {
|
||
try {
|
||
$client->deleteObject(['Bucket' => $cosConfig['bucket'], 'Key' => ltrim($pair['url'], '/')]);
|
||
} catch (\Exception $e) {
|
||
$output->writeln('<comment>COS删除失败: ' . $pair['url'] . ' ' . $e->getMessage() . '</comment>');
|
||
}
|
||
}
|
||
Db::name('attachment')->where('id', $pair['id'])->delete();
|
||
}
|
||
$output->writeln('<info>已删除: ' . $o['url'] . '</info>');
|
||
}
|
||
}
|
||
}
|
||
}
|