FundLogCollector.php 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498
  1. <?php
  2. namespace App\Module\Game\Logics\UserLogCollectors;
  3. use App\Module\Fund\Models\FundLogModel;
  4. use App\Module\Fund\Services\AccountService;
  5. use App\Module\Game\Enums\REWARD_SOURCE_TYPE;
  6. /**
  7. * 资金日志收集器
  8. *
  9. * 收集fund_logs表的新增记录,转换为用户友好的日志消息
  10. */
  11. class FundLogCollector extends BaseLogCollector
  12. {
  13. /**
  14. * 源表名
  15. *
  16. * @var string
  17. */
  18. protected string $sourceTable = 'fund_logs';
  19. /**
  20. * 默认源类型 - 使用系统奖励枚举
  21. *
  22. * @var string
  23. */
  24. protected string $sourceType = 'system';
  25. /**
  26. * 商店商品名称缓存
  27. *
  28. * @var array
  29. */
  30. private array $shopItemNameCache = [];
  31. /**
  32. * 预加载商店商品信息
  33. *
  34. * @param \Illuminate\Database\Eloquent\Collection $records 记录集合
  35. * @return void
  36. */
  37. private function preloadShopItems($records): void
  38. {
  39. $shopItemIds = [];
  40. foreach ($records as $record) {
  41. if (strpos($record->remark, 'shop_buy') !== false) {
  42. if (preg_match('/ID:(\d+)/', $record->remark, $matches)) {
  43. $shopItemIds[] = (int)$matches[1];
  44. }
  45. }
  46. }
  47. if (!empty($shopItemIds)) {
  48. $shopItemIds = array_unique($shopItemIds);
  49. $shopItems = \App\Module\Shop\Models\ShopItem::whereIn('id', $shopItemIds)->get();
  50. foreach ($shopItems as $shopItem) {
  51. $this->shopItemNameCache[$shopItem->id] = $shopItem->name;
  52. }
  53. // 为未找到的商品设置默认名称
  54. foreach ($shopItemIds as $itemId) {
  55. if (!isset($this->shopItemNameCache[$itemId])) {
  56. $this->shopItemNameCache[$itemId] = "商品{$itemId}";
  57. }
  58. }
  59. }
  60. }
  61. /**
  62. * 获取新的记录
  63. *
  64. * @param int $lastProcessedId 上次处理的最大ID
  65. * @return \Illuminate\Database\Eloquent\Collection
  66. */
  67. protected function getNewRecords(int $lastProcessedId)
  68. {
  69. $records = FundLogModel::where('id', '>', $lastProcessedId)
  70. ->orderBy('id')
  71. ->limit($this->maxRecords)
  72. ->get();
  73. // 预加载商店商品信息
  74. $this->preloadShopItems($records);
  75. return $records;
  76. }
  77. /**
  78. * 转换记录为用户日志数据
  79. *
  80. * @param FundLogModel $record 资金日志记录
  81. * @return array|null 用户日志数据,null表示跳过
  82. */
  83. protected function convertToUserLog($record): ?array
  84. {
  85. try {
  86. // 获取资金名称
  87. $fundName = $this->getFundName($record->fund_id);
  88. // 判断是获得还是消耗
  89. $amount = abs($record->amount);
  90. $action = $record->amount > 0 ? '获得' : '消耗';
  91. // 解析备注信息,生成用户友好的消息
  92. $message = $this->buildUserFriendlyMessage($record, $fundName, $action, $amount);
  93. // 使用原始记录的时间
  94. $createdAt = date('Y-m-d H:i:s', $record->create_time);
  95. // 根据操作类型选择合适的source_type
  96. $sourceType = $this->getSourceTypeByRemark($record->remark);
  97. return $this->createUserLogDataWithSourceType(
  98. $record->user_id,
  99. $message,
  100. $record->id,
  101. $createdAt,
  102. $sourceType
  103. );
  104. } catch (\Exception $e) {
  105. \Illuminate\Support\Facades\Log::error("转换资金日志失败", [
  106. 'record_id' => $record->id,
  107. 'error' => $e->getMessage()
  108. ]);
  109. return null;
  110. }
  111. }
  112. /**
  113. * 构建用户友好的消息
  114. *
  115. * @param FundLogModel $record 资金日志记录
  116. * @param string $fundName 资金名称
  117. * @param string $action 操作类型(获得/消耗)
  118. * @param int $amount 金额
  119. * @return string
  120. */
  121. private function buildUserFriendlyMessage(FundLogModel $record, string $fundName, string $action, int $amount): string
  122. {
  123. // 解析备注信息
  124. $remarkInfo = $this->parseRemark($record->remark);
  125. // 根据来源类型生成不同的消息格式
  126. if (isset($remarkInfo['source']) && isset($remarkInfo['id'])) {
  127. $sourceMessage = $this->getSourceMessage($remarkInfo['source'], $remarkInfo['id'], $action);
  128. if ($sourceMessage) {
  129. return "{$sourceMessage}{$action}{$fundName} {$amount}";
  130. }
  131. }
  132. // 特殊处理operate_type=3(管理员操作)的情况
  133. $operateTypeValue = is_object($record->operate_type) ? $record->operate_type->value : $record->operate_type;
  134. if ($operateTypeValue == 3) {
  135. $sourceMessage = $this->getAdminOperationMessage($record, $action);
  136. if ($sourceMessage) {
  137. return "{$sourceMessage}{$action}{$fundName} {$amount}";
  138. }
  139. }
  140. // 如果无法解析来源信息,使用默认格式
  141. return "{$action}{$fundName} {$amount}";
  142. }
  143. /**
  144. * 获取资金名称
  145. *
  146. * @param mixed $fundId 资金ID
  147. * @return string
  148. */
  149. private function getFundName($fundId): string
  150. {
  151. try {
  152. // 使用静态映射避免调用可能有问题的服务
  153. $fundNames = [
  154. 1 => '金币',
  155. 2 => '钻石',
  156. 3 => '积分',
  157. 4 => '经验',
  158. // 可以根据需要添加更多
  159. ];
  160. // 如果是枚举对象,获取其值
  161. $fundKey = is_object($fundId) ? $fundId->value : $fundId;
  162. return $fundNames[$fundKey] ?? "资金{$fundKey}";
  163. } catch (\Exception $e) {
  164. return "未知资金";
  165. }
  166. }
  167. /**
  168. * 解析备注信息
  169. *
  170. * @param string $remark 备注内容
  171. * @return array 解析后的信息数组
  172. */
  173. private function parseRemark(string $remark): array
  174. {
  175. $info = [];
  176. // 解析格式:币种消耗:2,消耗组:16,来源:shop_buy,ID:7
  177. // 使用更宽松的正则表达式来匹配中文和英文字符
  178. if (preg_match_all('/([^:,]+):([^,]+)/', $remark, $matches)) {
  179. for ($i = 0; $i < count($matches[1]); $i++) {
  180. $key = trim($matches[1][$i]);
  181. $value = trim($matches[2][$i]);
  182. // 转换中文键名为英文
  183. switch ($key) {
  184. case '来源':
  185. $info['source'] = $value;
  186. break;
  187. case 'ID':
  188. $info['id'] = (int)$value;
  189. break;
  190. case '消耗组':
  191. $info['consume_group'] = (int)$value;
  192. break;
  193. case '币种消耗':
  194. $info['fund_type'] = (int)$value;
  195. break;
  196. default:
  197. $info[$key] = $value;
  198. }
  199. }
  200. }
  201. return $info;
  202. }
  203. /**
  204. * 根据来源信息获取操作描述
  205. *
  206. * @param string $source 来源类型
  207. * @param int $id 来源ID
  208. * @param string $action 操作类型
  209. * @return string|null 操作描述,null表示使用默认格式
  210. */
  211. private function getSourceMessage(string $source, int $id, string $action): ?string
  212. {
  213. switch ($source) {
  214. case 'shop_buy':
  215. $itemName = $this->getShopItemName($id);
  216. return "购买{$itemName}";
  217. case '开启宝箱':
  218. case 'chest_open':
  219. return "开启宝箱";
  220. case 'house_upgrade':
  221. return "房屋升级";
  222. case 'land_upgrade':
  223. return "土地升级";
  224. case 'task_reward':
  225. return "任务奖励";
  226. case 'system_gift':
  227. return "系统赠送";
  228. case 'admin_operation':
  229. return "管理员操作";
  230. case 'test_command':
  231. return "测试操作";
  232. default:
  233. return null;
  234. }
  235. }
  236. /**
  237. * 获取管理员操作消息
  238. *
  239. * @param FundLogModel $record 资金日志记录
  240. * @param string $action 操作类型
  241. * @return string|null 操作描述,null表示使用默认格式
  242. */
  243. private function getAdminOperationMessage(FundLogModel $record, string $action): ?string
  244. {
  245. try {
  246. // 尝试从fund_admin表获取详细信息
  247. $adminOperation = \App\Module\Fund\Models\FundAdminModel::find($record->operate_id);
  248. if ($adminOperation) {
  249. // 如果找到管理员操作记录,使用其备注信息
  250. if (!empty($adminOperation->remark) && $adminOperation->remark !== 'Admin Approved') {
  251. return "管理员操作({$adminOperation->remark})";
  252. }
  253. // 获取管理员信息
  254. $adminInfo = $this->getAdminInfo($adminOperation->admin_id);
  255. return "管理员操作({$adminInfo})";
  256. }
  257. // 如果没有找到对应的管理员操作记录,尝试使用fund_logs的备注
  258. if (!empty($record->remark) && $record->remark !== 'Admin Approved') {
  259. return "管理员操作({$record->remark})";
  260. }
  261. // 如果备注也没有有用信息,显示操作ID
  262. return "管理员操作(ID:{$record->operate_id})";
  263. } catch (\Exception $e) {
  264. \Illuminate\Support\Facades\Log::warning("获取管理员操作信息失败", [
  265. 'fund_log_id' => $record->id,
  266. 'operate_id' => $record->operate_id,
  267. 'error' => $e->getMessage(),
  268. 'collector' => $this->collectorName
  269. ]);
  270. return "管理员操作";
  271. }
  272. }
  273. /**
  274. * 获取管理员信息
  275. *
  276. * @param int $adminId 管理员ID
  277. * @return string
  278. */
  279. private function getAdminInfo(int $adminId): string
  280. {
  281. try {
  282. // 尝试获取管理员用户信息
  283. $adminUser = \Dcat\Admin\Models\Administrator::find($adminId);
  284. if ($adminUser && !empty($adminUser->name)) {
  285. return $adminUser->name;
  286. }
  287. return "管理员{$adminId}";
  288. } catch (\Exception $e) {
  289. return "管理员{$adminId}";
  290. }
  291. }
  292. /**
  293. * 获取商店商品名称
  294. *
  295. * @param int $itemId 商品ID
  296. * @return string
  297. */
  298. private function getShopItemName(int $itemId): string
  299. {
  300. // 检查缓存
  301. if (isset($this->shopItemNameCache[$itemId])) {
  302. return $this->shopItemNameCache[$itemId];
  303. }
  304. try {
  305. $shopItem = \App\Module\Shop\Models\ShopItem::find($itemId);
  306. if ($shopItem && $shopItem->name) {
  307. $this->shopItemNameCache[$itemId] = $shopItem->name;
  308. return $shopItem->name;
  309. }
  310. $defaultName = "商品{$itemId}";
  311. $this->shopItemNameCache[$itemId] = $defaultName;
  312. return $defaultName;
  313. } catch (\Exception $e) {
  314. \Illuminate\Support\Facades\Log::error("获取商店商品名称失败", [
  315. 'item_id' => $itemId,
  316. 'error' => $e->getMessage(),
  317. 'collector' => $this->collectorName
  318. ]);
  319. $defaultName = "商品{$itemId}";
  320. $this->shopItemNameCache[$itemId] = $defaultName;
  321. return $defaultName;
  322. }
  323. }
  324. /**
  325. * 是否应该记录此日志
  326. *
  327. * @param FundLogModel $record
  328. * @return bool
  329. */
  330. private function shouldLogRecord(FundLogModel $record): bool
  331. {
  332. // 可以在这里添加过滤规则
  333. // 例如:只记录金额大于某个值的变更
  334. // 跳过金额为0的记录
  335. if ($record->amount == 0) {
  336. return false;
  337. }
  338. // 可以添加更多过滤条件
  339. // 例如:跳过某些操作类型
  340. // if (in_array($record->operate_type, [...])) {
  341. // return false;
  342. // }
  343. return true;
  344. }
  345. /**
  346. * 重写转换方法,添加过滤逻辑
  347. *
  348. * @param FundLogModel $record 资金日志记录
  349. * @return array|null 用户日志数据,null表示跳过
  350. */
  351. protected function convertToUserLogWithFilter($record): ?array
  352. {
  353. // 检查是否应该记录此日志
  354. if (!$this->shouldLogRecord($record)) {
  355. return null;
  356. }
  357. return $this->convertToUserLog($record);
  358. }
  359. /**
  360. * 根据备注信息获取合适的source_type
  361. *
  362. * @param string $remark 备注内容
  363. * @return string
  364. */
  365. private function getSourceTypeByRemark(string $remark): string
  366. {
  367. $remarkInfo = $this->parseRemark($remark);
  368. if (isset($remarkInfo['source'])) {
  369. switch ($remarkInfo['source']) {
  370. case 'shop_buy':
  371. return REWARD_SOURCE_TYPE::SHOP_PURCHASE->value;
  372. case 'chest_open':
  373. case '开启宝箱':
  374. return REWARD_SOURCE_TYPE::CHEST->value;
  375. case 'house_upgrade':
  376. case 'land_upgrade':
  377. return REWARD_SOURCE_TYPE::FARM_UPGRADE->value;
  378. case 'task_reward':
  379. return REWARD_SOURCE_TYPE::TASK->value;
  380. case 'system_gift':
  381. return REWARD_SOURCE_TYPE::SYSTEM->value;
  382. case 'admin_operation':
  383. return REWARD_SOURCE_TYPE::ADMIN_GRANT->value;
  384. case 'test_command':
  385. return REWARD_SOURCE_TYPE::TEST->value;
  386. default:
  387. return REWARD_SOURCE_TYPE::SYSTEM->value;
  388. }
  389. }
  390. return REWARD_SOURCE_TYPE::SYSTEM->value;
  391. }
  392. /**
  393. * 创建用户日志数据数组(带自定义source_type)
  394. *
  395. * @param int $userId 用户ID
  396. * @param string $message 日志消息
  397. * @param int $sourceId 来源记录ID
  398. * @param string|null $originalTime 原始时间(业务发生时间),null则使用当前时间
  399. * @param string $sourceType 来源类型
  400. * @return array
  401. */
  402. protected function createUserLogDataWithSourceType(
  403. int $userId,
  404. string $message,
  405. int $sourceId,
  406. ?string $originalTime = null,
  407. string $sourceType = null
  408. ): array {
  409. $now = now()->toDateTimeString();
  410. $originalTime = $originalTime ?? $now;
  411. $sourceType = $sourceType ?? $this->sourceType;
  412. return [
  413. 'user_id' => $userId,
  414. 'message' => $message,
  415. 'source_type' => $sourceType,
  416. 'source_id' => $sourceId,
  417. 'source_table' => $this->sourceTable,
  418. 'original_time' => $originalTime, // 原始业务时间
  419. 'collected_at' => $now, // 收集时间
  420. 'created_at' => $now, // 兼容字段
  421. ];
  422. }
  423. }