// +---------------------------------------------------------------------- namespace app\services\system; use app\services\product\CenterProductServices; use app\services\system\attachment\SystemAttachmentServices; use app\services\system\module\ModuleInstaller; use crmeb\basic\BaseService; use crmeb\exceptions\AdminException; use crmeb\services\CacheService; use crmeb\trade\product\ProductTypeRegistry; use think\facade\Config; use think\facade\Db; /** * 清除数据 * Class SystemClearServices * 本类为系统数据清理工具,Db::execute 用于执行 TRUNCATE/DELETE 等清理 SQL, * 不属于业务数据操作,保留直接调用。 * @package app\services\system */ class SystemClearServices extends BaseService { /** * 清除表数据(跳过当前库不存在的表,兼容插件未安装/表已迁移场景) * @param string|array $table_name * @param bool $status true=TRUNCATE,false=DELETE */ public function clearData($table_name, bool $status) { $prefix = config('database.connections.' . config('database.default'))['prefix']; $clearData = is_string($table_name) ? [$table_name] : $table_name; $existing = $this->listExistingTables($prefix); foreach ($clearData as $name) { if (!isset($existing[$name])) { continue; } if ($status) { Db::execute('TRUNCATE TABLE `' . $prefix . $name . '`'); } else { Db::execute('DELETE FROM `' . $prefix . $name . '`'); } } } /** * 仅允许被 SystemClearDataController 指定方法调用 */ protected function assertCalledFromClearDataController(string $function): void { $expectedClass = \app\adminapi\controller\system\SystemClearDataController::class; foreach (debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 16) as $frame) { if (($frame['class'] ?? '') === $expectedClass && ($frame['function'] ?? '') === $function) { return; } } throw new AdminException('禁止绕过入口直接调用清除数据能力'); } /** * 清除临时附件与二维码表 */ public function clearTempData(): void { $this->assertCalledFromClearDataController('userTemp'); /** @var SystemAttachmentServices $attachmentServices */ $attachmentServices = app()->make(SystemAttachmentServices::class); $attachmentServices->clearTemporaryAttachments(); $this->clearData(['qrcode'], true); } /** * 清除回收站商品(含各产品类型扩展表广播) */ public function clearRecycleProduct(): void { $this->assertCalledFromClearDataController('recycleProduct'); /** @var CenterProductServices $product */ $product = app()->make(CenterProductServices::class); $product->recycleProduct(0); } /** * 清除商品分类 */ public function clearCategoryData(): void { $this->assertCalledFromClearDataController('categoryData'); $this->clearData(['store_category'], true); } /** * 清除订单及相关履约/售后/支付流水(不含用户主档与商品主数据) */ public function clearOrderData(): void { $this->assertCalledFromClearDataController('orderData'); $prefix = (string)config('database.connections.' . config('database.default') . '.prefix'); $existing = $this->listExistingTables($prefix); $tables = array_merge( $this->discoverIndirectUserTables($prefix, $existing), [ 'cart', 'store_cart', 'store_pink', 'store_visit', 'member_order', 'member_order_status', 'store_integral_order', 'store_integral_order_status', 'order_center', 'order_economize', 'order_invoice', 'order_item', 'order_log', 'store_order_invoice', 'after_sale', 'after_sale_item', 'pay_record', 'refund_effect', 'refund_order', 'offline_receipt', 'offline_receipt_status', 'capital_flow', 'printer_task', 'express_dump_log', 'sms_record', ] ); /** @var ProductTypeRegistry $registry */ $registry = app()->make(ProductTypeRegistry::class); foreach ($registry->all() as $typeKey) { $tables[] = 'pt_' . $typeKey . '_order'; } foreach ($existing as $table => $_) { if (preg_match('/^pt_.+_order$/', $table)) { $tables[] = $table; } } $tables = array_values(array_unique(array_filter( $tables, fn(string $table): bool => isset($existing[$table]) && !$this->isUserClearProtectedTable($table) ))); $this->clearData($tables, true); $this->releaseProductTypeOrderBindings($prefix, $existing); } /** * 清除客服数据 */ public function clearKefuData(): void { $this->assertCalledFromClearDataController('kefuData'); $this->clearData([ 'store_service', 'store_service_log', 'store_service_record', 'store_service_feedback', 'store_service_speechcraft', 'kefu_ai_session', 'kefu_ai_reply_log', ], true); $this->delDirAndFile('./public/uploads/store/service'); } /** * 清除微信相关运营数据 */ public function clearWechatData(): void { $this->assertCalledFromClearDataController('wechatData'); $this->clearData([ 'cache', 'wechat_key', 'wechat_media', 'wechat_message', 'wechat_news_category', 'wechat_qrcode', 'wechat_qrcode_cate', 'wechat_qrcode_record', 'wechat_reply', 'wechat_user', ], true); $this->delDirAndFile('./public/uploads/wechat'); } /** * 清除全部附件记录与本地 uploads */ public function clearAttachmentData(): void { $this->assertCalledFromClearDataController('attachmentData'); $this->clearData([ 'system_attachment', 'system_attachment_category', ], true); $this->delDirAndFile('./public/uploads/'); } /** * 清除文章与分类 */ public function clearArticleData(): void { $this->assertCalledFromClearDataController('articleData'); $this->clearData([ 'article_category', 'article', 'article_content', ], true); } /** * 清除系统运行记录 */ public function clearSystemLogData(): void { $this->assertCalledFromClearDataController('systemData'); $this->clearData([ 'system_log', 'notice_log', 'notice_read', 'upgrade_log', 'module_install_log', 'express_dump_log', 'sms_record', 'cache', ], true); } /** * 清除商城业务数据(用户/订单/商品/内容/客服等,保留系统配置与后台账号体系) */ public function clearStoreData(): void { $this->assertCalledFromClearDataController('storeData'); $prefix = (string)config('database.connections.' . config('database.default') . '.prefix'); $existing = $this->listExistingTables($prefix); $tables = [ // 用户 'user', 'user_address', 'user_bill', 'user_brokerage_frozen', 'user_cancel', 'user_enter', 'user_friends', 'user_group', 'user_invoice', 'user_label', 'user_label_relation', 'user_level', 'user_member', 'user_money', 'user_recharge', 'user_search', 'user_sign', 'user_spread', 'user_visit', // 订单/售后/支付流水 'cart', 'store_cart', 'order_center', 'order_economize', 'order_invoice', 'order_item', 'order_log', 'store_order_invoice', 'store_pink', 'store_visit', 'after_sale', 'after_sale_item', 'pay_record', 'refund_effect', 'refund_order', 'offline_receipt', 'offline_receipt_status', 'member_order', 'member_order_status', 'store_integral_order', 'store_integral_order_status', 'capital_flow', 'printer_task', 'express_dump_log', 'sms_record', 'qrcode', // 商品 'product_center', 'store_category', 'store_product_attr_value', 'store_product_cate', 'store_product_coupon', 'store_product_description', 'store_product_label', 'store_product_label_cate', 'store_product_log', 'store_product_param', 'store_product_protection', 'store_product_relation', 'store_product_reply', 'store_product_rule', 'store_product_virtual', 'store_coupon_issue', 'store_coupon_issue_user', 'store_coupon_product', 'store_coupon_user', // 活动/会员卡 'store_advance', 'store_bargain', 'store_bargain_user', 'store_bargain_user_help', 'store_combination', 'store_seckill', 'store_integral', 'luck_lottery', 'luck_lottery_record', 'luck_prize', 'member_card', 'member_card_batch', 'distribution_level_task', 'distribution_level_task_record', 'division_agent_apply', // 内容/客服/门店/微信用户侧 'article', 'article_category', 'article_content', 'auxiliary', 'category', 'live_anchor', 'live_goods', 'live_room', 'live_room_goods', 'store_service', 'store_service_feedback', 'store_service_log', 'store_service_record', 'store_service_speechcraft', 'kefu_ai_session', 'kefu_ai_reply_log', 'system_store', 'system_store_staff', 'delivery_service', 'wechat_user', 'wechat_message', 'wechat_qrcode_record', 'system_log', 'cache', ]; /** @var ProductTypeRegistry $registry */ $registry = app()->make(ProductTypeRegistry::class); foreach ($registry->all() as $typeKey) { $tables[] = 'pt_' . $typeKey; $tables[] = 'pt_' . $typeKey . '_sku'; $tables[] = 'pt_' . $typeKey . '_stock'; $tables[] = 'pt_' . $typeKey . '_order'; } foreach ($existing as $table => $_) { if (preg_match('/^pt_/', $table)) { $tables[] = $table; } } $this->clearData(array_values(array_unique($tables)), true); $this->delDirAndFile('./public/uploads/store/comment'); $this->delDirAndFile('./public/uploads/store/service'); } /** * 清除用户相关业务数据 * - 直接关联:含 uid/user_id/openid/unionid/spread_uid/to_uid 的表 * - 间接关联:订单/售后/支付等附属表、pt_*_order、客服会话等 * - 保护名单:商品/活动主数据、系统配置、运营配置等不会被清 */ public function clearUserRelevantData(): void { $this->assertCalledFromClearDataController('userRelevantData'); $prefix = (string)config('database.connections.' . config('database.default') . '.prefix'); $existing = $this->listExistingTables($prefix); $tables = array_merge( $this->discoverDirectUserTables($prefix, $existing), $this->discoverIndirectUserTables($prefix, $existing), $this->userClearExtraTables() ); // 已注册 + 库内残留的产品类型扩展订单表 /** @var ProductTypeRegistry $registry */ $registry = app()->make(ProductTypeRegistry::class); foreach ($registry->all() as $typeKey) { $tables[] = 'pt_' . $typeKey . '_order'; } foreach ($existing as $table => $_) { if (preg_match('/^pt_.+_order$/', $table)) { $tables[] = $table; } } $tables = array_values(array_unique(array_filter( $tables, fn(string $table): bool => isset($existing[$table]) && !$this->isUserClearProtectedTable($table) ))); $this->clearData($tables, true); $this->releaseProductTypeOrderBindings($prefix, $existing); $this->delDirAndFile('./public/uploads/store/comment'); $this->delDirAndFile('./public/uploads/store/service'); } /** * 直接关联用户的表(按身份字段发现) * @param array $existing * @return list */ protected function discoverDirectUserTables(string $prefix, array $existing): array { $database = (string)config('database.connections.' . config('database.default') . '.database'); $rows = Db::query( 'SELECT DISTINCT table_name FROM information_schema.columns WHERE table_schema = ? AND table_name LIKE ? AND column_name IN (?,?,?,?,?,?)', [$database, $prefix . '%', 'uid', 'user_id', 'openid', 'unionid', 'spread_uid', 'to_uid'] ); $tables = []; $prefixLen = strlen($prefix); foreach ($rows as $row) { $full = (string)($row['table_name'] ?? $row['TABLE_NAME'] ?? ''); if ($full === '' || !str_starts_with($full, $prefix)) { continue; } $name = substr($full, $prefixLen); if (isset($existing[$name])) { $tables[] = $name; } } return $tables; } /** * 间接关联用户业务的表(订单/售后/支付等附属,不含商品主数据) * @param array $existing * @return list */ protected function discoverIndirectUserTables(string $prefix, array $existing): array { $database = (string)config('database.connections.' . config('database.default') . '.database'); $rows = Db::query( 'SELECT DISTINCT table_name FROM information_schema.columns WHERE table_schema = ? AND table_name LIKE ? AND column_name IN (?,?,?,?,?)', [$database, $prefix . '%', 'order_no', 'order_id', 'after_sale_no', 'refund_order_no', 'oid'] ); $tables = []; $prefixLen = strlen($prefix); foreach ($rows as $row) { $full = (string)($row['table_name'] ?? $row['TABLE_NAME'] ?? ''); if ($full === '' || !str_starts_with($full, $prefix)) { continue; } $name = substr($full, $prefixLen); if (!isset($existing[$name]) || $this->isUserClearProtectedTable($name)) { continue; } // 仅 product_id 关联的商品/活动主数据已在保护名单;这里再排除纯库存扩展表 if (preg_match('/^pt_.+_(sku|stock)$/', $name) || preg_match('/^pt_[a-z0-9]+$/', $name)) { continue; } $tables[] = $name; } // 售后明细等无 order_no/uid、但必然挂在用户售后下的表 foreach (['after_sale_item', 'refund_effect', 'pay_record', 'printer_task', 'express_dump_log'] as $name) { if (isset($existing[$name])) { $tables[] = $name; } } return $tables; } /** * 无通用身份/订单字段、但仍属用户侧业务的补充表 * @return list */ protected function userClearExtraTables(): array { return [ 'user_group', 'user_label', 'user_brokerage_frozen', 'member_card', 'member_card_batch', 'qrcode', 'store_pink', 'store_integral_order', 'store_integral_order_status', 'store_order_invoice', 'luck_lottery_record', 'distribution_level_task_record', 'division_agent_apply', 'store_service', 'store_service_feedback', 'store_service_log', 'store_service_record', 'kefu_ai_session', 'kefu_ai_reply_log', ]; } /** * 清除用户数据时不可动的表(商品/活动主数据、系统与运营配置) */ protected function isUserClearProtectedTable(string $table): bool { static $exact = null; if ($exact === null) { $exact = array_flip([ // 商品与规格主数据 'product_center', 'store_category', 'store_product_attr_value', 'store_product_cate', 'store_product_coupon', 'store_product_description', 'store_product_label', 'store_product_label_cate', 'store_product_param', 'store_product_protection', 'store_product_rule', 'store_coupon_issue', 'store_coupon_product', // 活动主数据(参与记录另清) 'store_activity', 'store_bargain', 'store_combination', 'store_seckill', 'store_seckill_time', 'activity_seckill_sku', 'store_integral', 'luck_lottery', 'luck_prize', // 会员档位/等级配置 'member_ship', 'member_right', 'system_user_level', // 渠道码配置(扫码记录会清) 'wechat_qrcode', 'wechat_qrcode_cate', 'wechat_key', 'wechat_reply', 'wechat_media', 'wechat_news_category', // 支付/通知/驱动配置 'pay_channel', 'pay_channel_platform', 'notice', 'notice_channel', 'notice_config', 'notice_template', 'driver', 'printer_channel', // 内容与装修 'article', 'article_category', 'article_content', 'diy', 'theme', 'theme_download', 'theme_module', 'page_categroy', 'page_link', 'agreement', 'cache', 'category', 'auxiliary', // 直播/小程序运营配置 'live_anchor', 'live_goods', 'live_room', 'live_room_goods', 'routine_scheme', ]); } if (isset($exact[$table])) { return true; } // 系统表(店员账号例外,随用户侧清理) if (str_starts_with($table, 'system_') && $table !== 'system_store_staff') { return true; } // 产品类型主表/SKU/库存表:pt_normal、pt_virtual_sku、pt_cardkey_stock 等 if (preg_match('/^pt_[a-z0-9]+$/', $table) || preg_match('/^pt_.+_(sku|stock)$/', $table)) { return true; } return false; } /** * 释放产品类型库存上挂着的用户订单占用(不删库存本体) * @param array $existing */ protected function releaseProductTypeOrderBindings(string $prefix, array $existing): void { foreach ($existing as $table => $full) { if (!preg_match('/^pt_.+_stock$/', $table)) { continue; } $cols = array_column(Db::query('SHOW COLUMNS FROM `' . $full . '`'), 'Field'); if (!in_array('order_no', $cols, true)) { continue; } Db::execute('UPDATE `' . $full . '` SET `order_no` = \'\' WHERE `order_no` <> \'\''); } } /** * 当前库已存在的业务表名(不含前缀)=> 全名 * @return array */ protected function listExistingTables(string $prefix): array { $database = (string)config('database.connections.' . config('database.default') . '.database'); $rows = Db::query( 'SELECT table_name FROM information_schema.tables WHERE table_schema = ? AND table_name LIKE ?', [$database, $prefix . '%'] ); $map = []; $prefixLen = strlen($prefix); foreach ($rows as $row) { $full = (string)($row['table_name'] ?? $row['TABLE_NAME'] ?? ''); if ($full === '' || !str_starts_with($full, $prefix)) { continue; } $map[substr($full, $prefixLen)] = $full; } return $map; } /** * 递归删除文件,只能删除 public/uploads下的文件 * @param $dirName * @param bool $subdir */ public function delDirAndFile(string $dirName, $subdir = true) { if (strstr($dirName, 'public/uploads') === false) { return true; } if ($handle = @opendir("$dirName")) { while (false !== ($item = readdir($handle))) { if ($item != "." && $item != "..") { if (is_dir("$dirName/$item")) $this->delDirAndFile("$dirName/$item", false); else @unlink("$dirName/$item"); } } closedir($handle); if (!$subdir) @rmdir($dirName); } } /** * 替换域名 * @param string $url 新域名 * @param string $siteUrl 要被替换的原域名;为空时取系统 site_url * @return mixed */ public function replaceSiteUrl(string $url, string $siteUrl = '') { $this->assertCalledFromClearDataController('replaceSiteUrl'); $siteUrl = $siteUrl !== '' ? $siteUrl : (string)sys_config('site_url'); if ($siteUrl === '' || $url === '') { throw new AdminException('原域名和新域名不能为空'); } if ($siteUrl === $url) { throw new AdminException('原域名与新域名不能相同'); } // 解析站点 URL 的协议 $siteUrlScheme = parse_url($siteUrl)['scheme'] ?? ''; if ($siteUrlScheme === '') { throw new AdminException('原域名不合法'); } // 将站点 URL 中的协议替换为 JSON 格式 $siteUrlJson = str_replace($siteUrlScheme . '://', $siteUrlScheme . ':\\\/\\\/', $siteUrl); // 获取当前 URL 的协议 $urlScheme = parse_url($url)['scheme'] ?? ''; if ($urlScheme === '') { throw new AdminException('新域名不合法'); } // 将当前 URL 中的协议替换为 JSON 格式 $urlJson = str_replace($urlScheme . '://', $urlScheme . ':\\\/\\\/', $url); // 获取数据库表前缀 $prefix = Config::get('database.connections.' . Config::get('database.default') . '.prefix'); // 构建 SQL 语句数组 $sql = [ "UPDATE `{$prefix}distribution_level` SET `image` = replace(`image` ,'{$siteUrl}','{$url}')", "UPDATE `{$prefix}agreement` SET `content` = replace(content ,'{$siteUrl}','{$url}')", "UPDATE `{$prefix}article` SET `image_input` = replace(`image_input` ,'{$siteUrl}','{$url}')", "UPDATE `{$prefix}article_category` SET `image` = replace(`image` ,'{$siteUrl}','{$url}')", "UPDATE `{$prefix}article_content` SET `content` = replace(`content` ,'{$siteUrl}','{$url}')", "UPDATE `{$prefix}cache` SET `result` = replace(result ,'{$siteUrlJson}','{$urlJson}')", "UPDATE `{$prefix}delivery_service` SET `avatar` = replace(`avatar` ,'{$siteUrl}','{$url}')", "UPDATE `{$prefix}division_agent_apply` SET `images` = replace(images ,'{$siteUrlJson}','{$urlJson}')", "UPDATE `{$prefix}diy` SET `value` = replace(value ,'{$siteUrlJson}','{$urlJson}'),`default_value` = replace(default_value ,'{$siteUrlJson}','{$urlJson}')", "UPDATE `{$prefix}diy` SET `value` = replace(value ,'{$siteUrl}','{$url}'),`default_value` = replace(default_value ,'{$siteUrl}','{$url}')", "UPDATE `{$prefix}live_anchor` SET `cover_img` = replace(`cover_img` ,'{$siteUrl}','{$url}')", "UPDATE `{$prefix}live_goods` SET `cover_img` = replace(`cover_img` ,'{$siteUrl}','{$url}')", "UPDATE `{$prefix}live_room` SET `cover_img` = replace(`cover_img` ,'{$siteUrl}','{$url}'),`share_img` = replace(`share_img` ,'{$siteUrl}','{$url}')", "UPDATE `{$prefix}luck_lottery` SET `image` = replace(image ,'{$siteUrl}','{$url}'),`content` = replace(content ,'{$siteUrl}','{$url}')", "UPDATE `{$prefix}luck_prize` SET `image` = replace(image ,'{$siteUrl}','{$url}')", "UPDATE `{$prefix}member_card_batch` SET `qrcode` = replace(qrcode ,'{$siteUrl}','{$url}')", "UPDATE `{$prefix}member_right` SET `image` = replace(image ,'{$siteUrl}','{$url}')", "UPDATE `{$prefix}out_interface` SET `return_params` = replace(return_params ,'{$siteUrl}','{$url}'),`request_example` = replace(request_example ,'{$siteUrl}','{$url}'),`return_example` = replace(return_example ,'{$siteUrl}','{$url}')", "UPDATE `{$prefix}qrcode` SET `url` = replace(url ,'{$siteUrl}','{$url}'),`qrcode_url` = replace(qrcode_url ,'{$siteUrl}','{$url}')", "UPDATE `{$prefix}store_bargain` SET `image` = replace(image ,'{$siteUrl}','{$url}'),`images` = replace(images,'{$siteUrlJson}','{$urlJson}')", "UPDATE `{$prefix}store_category` SET `pic` = replace(`pic` ,'{$siteUrl}','{$url}'),`big_pic` = replace(`big_pic` ,'{$siteUrl}','{$url}')", "UPDATE `{$prefix}store_combination` SET `image` = replace(image ,'{$siteUrl}','{$url}'),`images` = replace(images,'{$siteUrlJson}','{$urlJson}')", "UPDATE `{$prefix}store_integral` SET `image` = replace(image ,'{$siteUrl}','{$url}'),`images` = replace(images,'{$siteUrlJson}','{$urlJson}')", "UPDATE `{$prefix}store_integral_order` SET `image` = replace(`image` ,'{$siteUrl}','{$url}')", "UPDATE `{$prefix}store_pink` SET `avatar` = replace(`avatar` ,'{$siteUrl}','{$url}')", "UPDATE `{$prefix}product_center` SET `image` = replace(image ,'{$siteUrl}','{$url}'),`slider_image` = replace(slider_image ,'{$siteUrlJson}','{$urlJson}'),`recommend_image` = replace(recommend_image ,'{$siteUrl}','{$url}')", "UPDATE `{$prefix}store_product_description` SET `description`= replace(description,'{$siteUrl}','{$url}')", "UPDATE `{$prefix}store_product_reply` SET `avatar` = replace(avatar ,'{$siteUrl}','{$url}'),`pics` = replace(pics,'{$siteUrlJson}','{$urlJson}')", "UPDATE `{$prefix}store_seckill` SET `image` = replace(image ,'{$siteUrl}','{$url}'),`images` = replace(images,'{$siteUrlJson}','{$urlJson}')", "UPDATE `{$prefix}store_service` SET `avatar` = replace(avatar ,'{$siteUrl}','{$url}')", "UPDATE `{$prefix}store_service_log` SET `msn` = replace(msn ,'{$siteUrlJson}','{$urlJson}')", "UPDATE `{$prefix}store_service_record` SET `avatar` = replace(avatar ,'{$siteUrl}','{$url}'),`message` = replace(message,'{$siteUrlJson}','{$urlJson}')", "UPDATE `{$prefix}system_admin` SET `head_pic` = replace(head_pic ,'{$siteUrl}','{$url}')", "UPDATE `{$prefix}system_attachment` SET `att_dir` = replace(att_dir ,'{$siteUrl}','{$url}'),`satt_dir` = replace(satt_dir ,'{$siteUrl}','{$url}')", "UPDATE `{$prefix}system_config` SET `value` = replace(value ,'{$siteUrl}','{$url}')", "UPDATE `{$prefix}system_config` SET `value` = replace(value ,'{$siteUrlJson}','{$urlJson}')", "UPDATE `{$prefix}system_group_data` SET `value` = replace(value ,'{$siteUrlJson}','{$urlJson}')", "UPDATE `{$prefix}system_store` SET `image` = replace(image ,'{$siteUrl}','{$url}'),`oblong_image` = replace(oblong_image ,'{$siteUrl}','{$url}')", "UPDATE `{$prefix}system_store_staff` SET `avatar` = replace(avatar ,'{$siteUrl}','{$url}')", "UPDATE `{$prefix}system_user_level` SET `image` = replace(image ,'{$siteUrl}','{$url}'),`icon` = replace(icon ,'{$siteUrl}','{$url}')", "UPDATE `{$prefix}user` SET `avatar` = replace(avatar ,'{$siteUrl}','{$url}')", "UPDATE `{$prefix}wechat_qrcode` SET `image` = replace(image ,'{$siteUrl}','{$url}')", "UPDATE `{$prefix}wechat_user` SET `headimgurl` = replace(headimgurl ,'{$siteUrl}','{$url}')", "UPDATE `{$prefix}theme` SET `home_data` = replace(home_data ,'{$siteUrlJson}','{$urlJson}'),`detail_data` = replace(detail_data ,'{$siteUrlJson}','{$urlJson}'),`user_data` = replace(user_data ,'{$siteUrlJson}','{$urlJson}')", "UPDATE `{$prefix}theme` SET `home_default_data` = replace(home_default_data ,'{$siteUrlJson}','{$urlJson}'),`detail_default_data` = replace(detail_default_data ,'{$siteUrlJson}','{$urlJson}'),`user_default_data` = replace(user_default_data ,'{$siteUrlJson}','{$urlJson}')", "UPDATE `{$prefix}theme` SET `home_data` = replace(home_data ,'{$siteUrl}','{$url}'),`detail_data` = replace(detail_data ,'{$siteUrl}','{$url}'),`user_data` = replace(user_data ,'{$siteUrl}','{$url}')", "UPDATE `{$prefix}theme` SET `home_default_data` = replace(home_default_data ,'{$siteUrl}','{$url}'),`detail_default_data` = replace(detail_default_data ,'{$siteUrl}','{$url}'),`user_default_data` = replace(user_default_data ,'{$siteUrl}','{$url}')", "UPDATE `{$prefix}theme` SET `home_image` = replace(home_image ,'{$siteUrl}','{$url}'),`category_image` = replace(category_image ,'{$siteUrl}','{$url}'),`detail_image` = replace(detail_image ,'{$siteUrl}','{$url}'),`user_image` = replace(user_image ,'{$siteUrl}','{$url}'),`home_default_image` = replace(home_default_image ,'{$siteUrl}','{$url}'),`category_default_image` = replace(category_default_image ,'{$siteUrl}','{$url}'),`detail_default_image` = replace(detail_default_image ,'{$siteUrl}','{$url}'),`user_default_image` = replace(user_default_image ,'{$siteUrl}','{$url}')", ]; // 执行 SQL 语句 return $this->transaction(function () use ($sql) { try { foreach ($sql as $item) { Db::execute($item); } } catch (\Throwable $e) { throw new AdminException('替换失败,失败原因:{:msg}', ['msg' => $e->getMessage()]); } }); } /** * 恢复出厂:按当前安装包 SQL 重建核心表并回灌种子,再安装默认插件; * 仅保留传入的超级管理员,并把原 system_config 覆盖回去。 * * 业务门禁(登录密码 / app_debug / 超管)在 Controller;此处只允许被 * SystemClearDataController::factoryReset 调用。 * * @return list */ public function factoryReset(int $adminId): array { $this->assertCalledFromClearDataController('factoryReset'); @set_time_limit(0); $steps = []; $prefix = (string)config('database.connections.' . config('database.default') . '.prefix'); $helper = app()->make(InstallSqlHelper::class); $admin = Db::name('system_admin')->where('id', $adminId)->where('is_del', 0)->find(); if (!$admin) { throw new AdminException('管理员不存在'); } $admin = is_array($admin) ? $admin : $admin->toArray(); $configSnapshot = Db::name('system_config')->column('value', 'menu_name'); $steps[] = '已快照超级管理员与系统配置'; $siteUrl = (string)sys_config('site_url'); $parts = parse_url($siteUrl) ?: []; $scheme = (string)($parts['scheme'] ?? 'http'); $host = (string)($parts['host'] ?? ''); if ($host !== '' && !empty($parts['port'])) { $host .= ':' . $parts['port']; } if ($host === '') { $host = (string)request()->host(); $scheme = request()->isSsl() ? 'https' : 'http'; } $tableStatements = $helper->getTableStatements(); $dataGroups = $helper->getDataGroups(); if ($tableStatements === [] || $dataGroups === []) { throw new AdminException('安装 SQL 不完整,请检查 public/install/crmeb_table.sql 与 install/data'); } // 先卸掉当前前缀下全部表,避免插件扩展表残留旧数据 $existing = $this->listExistingTables($prefix); Db::execute('SET FOREIGN_KEY_CHECKS=0'); foreach ($existing as $full) { Db::execute('DROP TABLE IF EXISTS `' . $full . '`'); } Db::execute('SET FOREIGN_KEY_CHECKS=1'); $steps[] = '已删除旧表 ' . count($existing) . ' 张'; $database = (string)config('database.connections.' . config('database.default') . '.database'); foreach ($tableStatements as $statement) { $sql = trim($statement); preg_match('/CREATE TABLE(?: IF NOT EXISTS)? `eb_([^`]+)`/i', $sql, $matches); $fullTableName = $prefix . ($matches[1] ?? ''); $sql = $helper->prepareSql($sql, $prefix, $scheme, $host); try { Db::execute($sql); } catch (\Throwable $e) { $check = Db::query( 'SELECT 1 FROM information_schema.tables WHERE table_schema = ? AND table_name = ? LIMIT 1', [$database, $fullTableName] ); if (empty($check)) { throw new AdminException('创建表失败[{:table}]: {:msg}', [ 'table' => $fullTableName, 'msg' => $e->getMessage(), ]); } } } $steps[] = '已按最新 crmeb_table.sql 重建表结构 ' . count($tableStatements) . ' 张'; foreach ($dataGroups as $group) { foreach ($group['statements'] as $statement) { $sql = $helper->prepareSql($statement, $prefix, $scheme, $host); try { Db::execute($sql); } catch (\Throwable $e) { throw new AdminException('写入默认数据失败[{:table}]: {:msg}', [ 'table' => $group['table'], 'msg' => $e->getMessage(), ]); } } } $steps[] = '已回灌安装默认数据 ' . count($dataGroups) . ' 组'; /** @var ModuleInstaller $moduleInstaller */ $moduleInstaller = app()->make(ModuleInstaller::class); $moduleInstaller->installRequired(); $moduleInstaller->installDefault(); $moduleInstaller->installBundledDrivers(); $steps[] = '已安装默认插件与驱动'; // 只保留当前超管(固定 id=1) Db::name('system_admin')->whereRaw('1=1')->delete(); $adminColumns = array_column(Db::query('SHOW COLUMNS FROM `' . $prefix . 'system_admin`'), 'Field'); $adminRow = array_intersect_key($admin, array_flip($adminColumns)); $adminRow['id'] = 1; if (in_array('roles', $adminColumns, true)) { $adminRow['roles'] = '1'; } if (in_array('level', $adminColumns, true)) { $adminRow['level'] = 0; } if (in_array('status', $adminColumns, true)) { $adminRow['status'] = 1; } if (in_array('is_del', $adminColumns, true)) { $adminRow['is_del'] = 0; } Db::name('system_admin')->insert($adminRow); $steps[] = '已写回超级管理员'; foreach ($configSnapshot as $menuName => $value) { Db::name('system_config')->where('menu_name', (string)$menuName)->update(['value' => $value]); } $steps[] = '已覆盖回原系统配置'; CacheService::clear(); $this->delDirAndFile('./public/uploads/store/comment'); $this->delDirAndFile('./public/uploads/store/service'); $steps[] = '已清理缓存与用户侧临时上传'; $this->writeFactoryResetLog($steps); return $steps; } /** * @param list $steps */ protected function writeFactoryResetLog(array $steps): void { $line = date('Y-m-d H:i:s') . ' factory_reset ' . implode(' | ', $steps) . PHP_EOL; @file_put_contents(runtime_path() . 'factory_reset.log', $line, FILE_APPEND); } }