ModelScannerLogic.php 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389
  1. <?php
  2. namespace App\Module\Cleanup\Logics;
  3. use App\Module\Cleanup\Models\CleanupConfig;
  4. use App\Module\Cleanup\Enums\DATA_CATEGORY;
  5. use App\Module\Cleanup\Enums\CLEANUP_TYPE;
  6. use Illuminate\Support\Facades\Log;
  7. use Illuminate\Database\Eloquent\Model;
  8. use Illuminate\Database\Eloquent\SoftDeletes;
  9. use UCore\ModelCore;
  10. /**
  11. * Model扫描逻辑类
  12. *
  13. * 负责扫描系统中的Model类并生成清理配置
  14. */
  15. class ModelScannerLogic
  16. {
  17. /**
  18. * 扫描所有Model类
  19. *
  20. * @param bool $forceRefresh 是否强制刷新
  21. * @return array 扫描结果
  22. */
  23. public static function scanAllModels(bool $forceRefresh = false): array
  24. {
  25. $startTime = microtime(true);
  26. // 获取所有Model类
  27. $models = static::getAllModelClasses();
  28. $result = [
  29. 'total_models' => count($models),
  30. 'scanned_models' => 0,
  31. 'new_models' => 0,
  32. 'updated_models' => 0,
  33. 'models' => [],
  34. 'scan_time' => 0,
  35. ];
  36. foreach ($models as $modelClass) {
  37. try {
  38. $modelInfo = static::scanModel($modelClass, $forceRefresh);
  39. if ($modelInfo) {
  40. $result['models'][] = $modelInfo;
  41. $result['scanned_models']++;
  42. if ($modelInfo['is_new']) {
  43. $result['new_models']++;
  44. } elseif ($modelInfo['is_updated']) {
  45. $result['updated_models']++;
  46. }
  47. }
  48. } catch (\Exception|\Error $e) {
  49. Log::error("扫描Model {$modelClass} 失败: " . $e->getMessage());
  50. }
  51. }
  52. $result['scan_time'] = round(microtime(true) - $startTime, 3);
  53. return $result;
  54. }
  55. /**
  56. * 扫描单个Model
  57. *
  58. * @param string $modelClass Model类名
  59. * @param bool $forceRefresh 是否强制刷新
  60. * @return array Model信息
  61. */
  62. public static function scanModel(string $modelClass, bool $forceRefresh = false): ?array
  63. {
  64. try {
  65. // 检查是否已存在配置
  66. $existingConfig = CleanupConfig::where('model_class', $modelClass)->first();
  67. $isNew = !$existingConfig;
  68. $isUpdated = false;
  69. // 获取Model信息
  70. $modelInfo = static::getModelInfo($modelClass);
  71. // 如果获取Model信息失败,跳过
  72. if (isset($modelInfo['error'])) {
  73. Log::warning("跳过有问题的Model: {$modelClass} - " . $modelInfo['error']);
  74. return null;
  75. }
  76. // 自动识别数据分类
  77. $dataCategory = static::detectDataCategory($modelClass, $modelInfo);
  78. // 识别模块名称
  79. $moduleName = static::extractModuleName($modelClass);
  80. // 生成默认清理配置
  81. $defaultConfig = static::generateDefaultConfig($dataCategory, $modelInfo);
  82. if ($isNew || $forceRefresh) {
  83. // 创建或更新配置
  84. $configData = [
  85. 'model_class' => $modelClass,
  86. 'table_name' => $modelInfo['table_name'], // 保持兼容性
  87. 'model_info' => $modelInfo,
  88. 'module_name' => $moduleName,
  89. 'data_category' => $dataCategory->value,
  90. 'default_cleanup_type' => $defaultConfig['cleanup_type'],
  91. 'default_conditions' => $defaultConfig['conditions'],
  92. 'is_enabled' => $defaultConfig['is_enabled'],
  93. 'priority' => $defaultConfig['priority'],
  94. 'batch_size' => $defaultConfig['batch_size'],
  95. 'description' => $defaultConfig['description'],
  96. ];
  97. if ($isNew) {
  98. CleanupConfig::create($configData);
  99. } else {
  100. $existingConfig->update($configData);
  101. $isUpdated = true;
  102. }
  103. }
  104. return [
  105. 'model_class' => $modelClass,
  106. 'table_name' => $modelInfo['table_name'],
  107. 'module_name' => $moduleName,
  108. 'data_category' => $dataCategory,
  109. 'model_info' => $modelInfo,
  110. 'is_new' => $isNew,
  111. 'is_updated' => $isUpdated,
  112. ];
  113. } catch (\Exception|\Error $e) {
  114. Log::error("扫描Model失败: {$modelClass} - " . $e->getMessage());
  115. return null;
  116. }
  117. }
  118. /**
  119. * 获取所有Model类
  120. *
  121. * @return array Model类名列表
  122. */
  123. private static function getAllModelClasses(): array
  124. {
  125. $models = [];
  126. $modulePaths = glob(app_path('Module/*/Models'));
  127. foreach ($modulePaths as $modulePath) {
  128. $modelFiles = glob($modulePath . '/*.php');
  129. foreach ($modelFiles as $modelFile) {
  130. $modelClass = static::getModelClassFromFile($modelFile);
  131. if ($modelClass && static::isValidModel($modelClass)) {
  132. $models[] = $modelClass;
  133. }
  134. }
  135. }
  136. return $models;
  137. }
  138. /**
  139. * 从文件路径获取Model类名
  140. *
  141. * @param string $filePath 文件路径
  142. * @return string|null Model类名
  143. */
  144. private static function getModelClassFromFile(string $filePath): ?string
  145. {
  146. $relativePath = str_replace(app_path(), '', $filePath);
  147. $relativePath = str_replace('.php', '', $relativePath);
  148. $relativePath = str_replace('/', '\\', $relativePath);
  149. return 'App' . $relativePath;
  150. }
  151. /**
  152. * 验证是否为有效的Model类
  153. *
  154. * @param string $modelClass Model类名
  155. * @return bool 是否有效
  156. */
  157. private static function isValidModel(string $modelClass): bool
  158. {
  159. if (!class_exists($modelClass)) {
  160. return false;
  161. }
  162. try {
  163. $reflection = new \ReflectionClass($modelClass);
  164. // 检查是否继承自ModelCore或Eloquent
  165. $isValidModel = $reflection->isSubclassOf(ModelCore::class) ||
  166. $reflection->isSubclassOf(Model::class);
  167. // 排除抽象类
  168. if ($reflection->isAbstract()) {
  169. return false;
  170. }
  171. return $isValidModel;
  172. } catch (\Exception $e) {
  173. return false;
  174. }
  175. }
  176. /**
  177. * 获取Model信息
  178. *
  179. * @param string $modelClass Model类名
  180. * @return array Model信息
  181. */
  182. private static function getModelInfo(string $modelClass): array
  183. {
  184. try {
  185. $model = new $modelClass();
  186. return [
  187. 'class_name' => $modelClass,
  188. 'table_name' => $model->getTable(),
  189. 'primary_key' => $model->getKeyName(),
  190. 'timestamps' => $model->timestamps,
  191. 'soft_deletes' => static::hasSoftDeletes($model),
  192. 'fillable' => $model->getFillable(),
  193. 'guarded' => $model->getGuarded(),
  194. 'casts' => $model->getCasts(),
  195. 'connection' => $model->getConnectionName(),
  196. 'relations' => static::getModelRelations($model),
  197. ];
  198. } catch (\Exception|\Error $e) {
  199. Log::warning("获取Model信息失败: {$modelClass} - " . $e->getMessage());
  200. // 返回基本信息
  201. return [
  202. 'class_name' => $modelClass,
  203. 'table_name' => 'unknown',
  204. 'primary_key' => 'id',
  205. 'timestamps' => true,
  206. 'soft_deletes' => false,
  207. 'fillable' => [],
  208. 'guarded' => [],
  209. 'casts' => [],
  210. 'connection' => null,
  211. 'relations' => [],
  212. 'error' => $e->getMessage(),
  213. ];
  214. }
  215. }
  216. /**
  217. * 检查Model是否支持软删除
  218. *
  219. * @param Model $model Model实例
  220. * @return bool 是否支持软删除
  221. */
  222. private static function hasSoftDeletes(Model $model): bool
  223. {
  224. return in_array(SoftDeletes::class, class_uses_recursive($model));
  225. }
  226. /**
  227. * 获取Model的关系定义
  228. *
  229. * @param Model $model Model实例
  230. * @return array 关系列表
  231. */
  232. private static function getModelRelations(Model $model): array
  233. {
  234. $relations = [];
  235. $reflection = new \ReflectionClass($model);
  236. foreach ($reflection->getMethods(\ReflectionMethod::IS_PUBLIC) as $method) {
  237. if ($method->class === get_class($model) &&
  238. !$method->isStatic() &&
  239. $method->getNumberOfParameters() === 0) {
  240. $methodName = $method->getName();
  241. // 跳过一些已知的非关系方法
  242. if (in_array($methodName, ['getTable', 'getKeyName', 'getFillable', 'getGuarded', 'getCasts'])) {
  243. continue;
  244. }
  245. try {
  246. $result = $model->$methodName();
  247. if ($result instanceof \Illuminate\Database\Eloquent\Relations\Relation) {
  248. $relations[] = [
  249. 'name' => $methodName,
  250. 'type' => class_basename(get_class($result)),
  251. 'related' => get_class($result->getRelated()),
  252. ];
  253. }
  254. } catch (\Exception|\Error $e) {
  255. // 忽略调用失败的方法
  256. Log::debug("获取Model关系失败: {$methodName} - " . $e->getMessage());
  257. }
  258. }
  259. }
  260. return $relations;
  261. }
  262. /**
  263. * 从Model类名提取模块名称
  264. *
  265. * @param string $modelClass Model类名
  266. * @return string 模块名称
  267. */
  268. private static function extractModuleName(string $modelClass): string
  269. {
  270. // App\Module\Farm\Models\FarmUser -> Farm
  271. if (preg_match('/App\\\\Module\\\\([^\\\\]+)\\\\Models\\\\/', $modelClass, $matches)) {
  272. return $matches[1];
  273. }
  274. return 'Unknown';
  275. }
  276. /**
  277. * 检测数据分类
  278. *
  279. * @param string $modelClass Model类名
  280. * @param array $modelInfo Model信息
  281. * @return DATA_CATEGORY 数据分类
  282. */
  283. private static function detectDataCategory(string $modelClass, array $modelInfo): DATA_CATEGORY
  284. {
  285. $tableName = $modelInfo['table_name'];
  286. $className = class_basename($modelClass);
  287. // 根据表名和类名模式识别数据分类
  288. if (str_contains($tableName, '_logs') || str_contains($className, 'Log')) {
  289. return DATA_CATEGORY::LOG_DATA;
  290. }
  291. if (str_contains($tableName, '_cache') || str_contains($tableName, '_sessions') ||
  292. str_contains($className, 'Cache') || str_contains($className, 'Session')) {
  293. return DATA_CATEGORY::CACHE_DATA;
  294. }
  295. if (str_contains($tableName, '_configs') || str_contains($tableName, '_settings') ||
  296. str_contains($className, 'Config') || str_contains($className, 'Setting')) {
  297. return DATA_CATEGORY::CONFIG_DATA;
  298. }
  299. if (str_contains($tableName, '_orders') || str_contains($tableName, '_payments') ||
  300. str_contains($tableName, '_transactions') || str_contains($className, 'Order') ||
  301. str_contains($className, 'Payment') || str_contains($className, 'Transaction')) {
  302. return DATA_CATEGORY::TRANSACTION_DATA;
  303. }
  304. // 默认为用户数据
  305. return DATA_CATEGORY::USER_DATA;
  306. }
  307. /**
  308. * 生成默认清理配置
  309. *
  310. * @param DATA_CATEGORY $dataCategory 数据分类
  311. * @param array $modelInfo Model信息
  312. * @return array 默认配置
  313. */
  314. private static function generateDefaultConfig(DATA_CATEGORY $dataCategory, array $modelInfo): array
  315. {
  316. $defaultCleanupType = $dataCategory->getDefaultCleanupType();
  317. $defaultPriority = $dataCategory->getDefaultPriority();
  318. $isEnabled = $dataCategory->isDefaultEnabled();
  319. // 根据Model特性调整配置
  320. $conditions = [];
  321. if ($defaultCleanupType === CLEANUP_TYPE::DELETE_BY_TIME) {
  322. $timeField = 'created_at';
  323. if ($modelInfo['timestamps']) {
  324. $conditions = [
  325. 'time_field' => $timeField,
  326. 'before' => '30_days_ago'
  327. ];
  328. }
  329. }
  330. return [
  331. 'cleanup_type' => $defaultCleanupType->value,
  332. 'conditions' => $conditions,
  333. 'is_enabled' => $isEnabled,
  334. 'priority' => $defaultPriority,
  335. 'batch_size' => 1000,
  336. 'description' => "自动生成的{$dataCategory->getDescription()}清理配置",
  337. ];
  338. }
  339. }