UrsPartnerDividendLogic.php 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504
  1. <?php
  2. namespace App\Module\UrsPromotion\Logics;
  3. use App\Module\UrsPromotion\Models\UrsPartnerDividendRecord;
  4. use App\Module\UrsPromotion\Models\UrsPartnerDividendDetail;
  5. use App\Module\UrsPromotion\Models\UrsUserTalent;
  6. use App\Module\UrsPromotion\Services\UrsUserMappingService;
  7. use App\Module\Transfer\Services\FeeStatisticsService;
  8. use App\Module\Transfer\Services\FeeService;
  9. use App\Module\Transfer\Models\TransferApp;
  10. use Illuminate\Support\Facades\DB;
  11. use Illuminate\Support\Facades\Log;
  12. use Carbon\Carbon;
  13. /**
  14. * URS合伙人分红逻辑类
  15. *
  16. * 处理合伙人分红的核心业务逻辑
  17. */
  18. class UrsPartnerDividendLogic
  19. {
  20. /**
  21. * 分红比例 (20%)
  22. */
  23. const DIVIDEND_RATE = 0.20;
  24. /**
  25. * 合伙人等级 (顶级达人)
  26. */
  27. const PARTNER_LEVEL = 5;
  28. /**
  29. * 执行每日合伙人分红
  30. *
  31. * @param string|null $date 分红日期,默认为今天
  32. * @return array 分红结果
  33. */
  34. public function executeDailyDividend(?string $date = null): array
  35. {
  36. $date = $date ?: Carbon::today()->format('Y-m-d');
  37. Log::info("开始执行合伙人分红", ['date' => $date]);
  38. // 检查是否已经分红
  39. $existingRecord = UrsPartnerDividendRecord::getByDate($date);
  40. if ($existingRecord && $existingRecord->status === UrsPartnerDividendRecord::STATUS_COMPLETED) {
  41. $message = "日期 {$date} 的分红已经完成";
  42. Log::warning($message);
  43. return ['success' => false, 'message' => $message];
  44. }
  45. $transferAppId = 2;
  46. try {
  47. // 1. 获取今日手续费统计
  48. $totalFeeAmount = $this->getTodayTotalFeeAmount($date,$transferAppId);
  49. if ($totalFeeAmount <= 0) {
  50. $message = "日期 {$date} 没有手续费收入,无需分红";
  51. Log::info($message);
  52. return ['success' => false, 'message' => $message];
  53. }
  54. // 2. 计算分红金额 (总手续费的20%)
  55. $dividendAmount = bcmul($totalFeeAmount, self::DIVIDEND_RATE, 10);
  56. // 3. 获取所有合伙人
  57. $partners = $this->getAllPartners();
  58. if (empty($partners)) {
  59. $message = "没有找到合伙人,无需分红";
  60. Log::info($message);
  61. return ['success' => false, 'message' => $message];
  62. }
  63. // 4. 计算每个合伙人的分红金额
  64. $partnerCount = count($partners);
  65. $perPartnerAmount = bcdiv($dividendAmount, $partnerCount, 10);
  66. // 5. 创建或获取分红记录(幂等性)
  67. $dividendRecord = $this->createOrGetDividendRecord($date, $totalFeeAmount, $dividendAmount, $partnerCount, $perPartnerAmount,$transferAppId);
  68. // 6. 分批处理分红详情
  69. $result = $this->processDividendDetailsBatch($dividendRecord, $partners, $perPartnerAmount);
  70. // 7. 更新分红记录状态
  71. $this->updateDividendRecordStatus($dividendRecord, $result);
  72. Log::info("合伙人分红执行完成", [
  73. 'date' => $date,
  74. 'total_fee_amount' => $totalFeeAmount,
  75. 'dividend_amount' => $dividendAmount,
  76. 'partner_count' => $partnerCount,
  77. 'per_partner_amount' => $perPartnerAmount,
  78. 'success_count' => $result['success_count'],
  79. 'failed_count' => $result['failed_count']
  80. ]);
  81. return [
  82. 'success' => true,
  83. 'message' => '分红执行完成',
  84. 'data' => [
  85. 'date' => $date,
  86. 'total_fee_amount' => $totalFeeAmount,
  87. 'dividend_amount' => $dividendAmount,
  88. 'partner_count' => $partnerCount,
  89. 'per_partner_amount' => $perPartnerAmount,
  90. 'success_count' => $result['success_count'],
  91. 'failed_count' => $result['failed_count']
  92. ]
  93. ];
  94. } catch (\Exception $e) {
  95. Log::error("合伙人分红执行失败", [
  96. 'date' => $date,
  97. 'error' => $e->getMessage(),
  98. 'trace' => $e->getTraceAsString()
  99. ]);
  100. return [
  101. 'success' => false,
  102. 'message' => '分红执行失败: ' . $e->getMessage()
  103. ];
  104. }
  105. }
  106. /**
  107. * 获取今日总手续费金额
  108. */
  109. private function getTodayTotalFeeAmount(string $date,$appId): string
  110. {
  111. try {
  112. // 获取所有应用的手续费统计
  113. $result = FeeStatisticsService::getStatsByDateRange($date, $date,$appId);
  114. // 检查是否有错误
  115. if (isset($result['error'])) {
  116. Log::warning("获取手续费统计有错误", [
  117. 'date' => $date,
  118. 'error' => $result['error']
  119. ]);
  120. return '0.0000000000';
  121. }
  122. // 从返回结果中获取数据
  123. $stats = $result['data'] ?? [];
  124. $totalFee = '0.0000000000';
  125. foreach ($stats as $stat) {
  126. // $stat是数组格式,不是对象
  127. $feeAmount = $stat['total_fee_amount'] ?? '0.0000000000';
  128. $totalFee = bcadd($totalFee, $feeAmount, 10);
  129. }
  130. Log::info("获取今日手续费统计", [
  131. 'date' => $date,
  132. 'total_fee' => $totalFee,
  133. 'stats_count' => count($stats)
  134. ]);
  135. return $totalFee;
  136. } catch (\Exception $e) {
  137. Log::error("获取今日手续费统计失败", [
  138. 'date' => $date,
  139. 'error' => $e->getMessage()
  140. ]);
  141. return '0.0000000000';
  142. }
  143. }
  144. /**
  145. * 获取所有合伙人
  146. */
  147. private function getAllPartners(): array
  148. {
  149. // 获取所有顶级达人(合伙人)
  150. $talents = UrsUserTalent::where('talent_level', self::PARTNER_LEVEL)
  151. ->get();
  152. $partners = [];
  153. foreach ($talents as $talent) {
  154. // UrsUserTalent表中的user_id就是农场用户ID
  155. $userId = $talent->user_id;
  156. // 获取对应的URS用户ID
  157. $ursUserId = UrsUserMappingService::getMappingUrsUserId($userId);
  158. if ($ursUserId) {
  159. $partners[] = [
  160. 'user_id' => $userId,
  161. 'urs_user_id' => $ursUserId,
  162. 'talent_level' => $talent->talent_level
  163. ];
  164. }
  165. }
  166. Log::info("获取合伙人列表", [
  167. 'total_talents' => $talents->count(),
  168. 'valid_partners' => count($partners)
  169. ]);
  170. return $partners;
  171. }
  172. /**
  173. * 创建或获取分红记录(幂等性)
  174. */
  175. private function createOrGetDividendRecord(string $date, string $totalFeeAmount, string $dividendAmount, int $partnerCount, string $perPartnerAmount,int $transferAppId): UrsPartnerDividendRecord
  176. {
  177. // 先尝试获取已存在的记录
  178. $existingRecord = UrsPartnerDividendRecord::getByDate($date);
  179. if ($existingRecord) {
  180. // 如果状态是失败,重置为处理中
  181. if ($existingRecord->status === UrsPartnerDividendRecord::STATUS_FAILED) {
  182. $existingRecord->update(['status' => UrsPartnerDividendRecord::STATUS_PROCESSING]);
  183. }
  184. return $existingRecord;
  185. }
  186. return UrsPartnerDividendRecord::create([
  187. 'dividend_date' => $date,
  188. 'total_fee_amount' => $totalFeeAmount,
  189. 'dividend_amount' => $dividendAmount,
  190. 'partner_count' => $partnerCount,
  191. 'per_partner_amount' => $perPartnerAmount,
  192. 'transfer_app_id' => $transferAppId,
  193. 'status' => UrsPartnerDividendRecord::STATUS_PROCESSING
  194. ]);
  195. }
  196. /**
  197. * 分批处理分红详情并执行转账
  198. */
  199. private function processDividendDetailsBatch(UrsPartnerDividendRecord $dividendRecord, array $partners, string $perPartnerAmount): array
  200. {
  201. $batchSize = 10; // 每批处理10个合伙人
  202. $totalSuccessCount = 0;
  203. $totalFailedCount = 0;
  204. // 先创建所有分红详情记录(如果不存在)
  205. $this->createDividendDetailsIfNotExists($dividendRecord, $partners, $perPartnerAmount);
  206. // 获取所有待处理的分红详情
  207. $pendingDetails = UrsPartnerDividendDetail::where('dividend_record_id', $dividendRecord->id)
  208. ->where('status', UrsPartnerDividendDetail::STATUS_PENDING)
  209. ->get();
  210. Log::info("开始分批处理分红详情", [
  211. 'total_partners' => count($partners),
  212. 'pending_details' => $pendingDetails->count(),
  213. 'batch_size' => $batchSize
  214. ]);
  215. // 分批处理
  216. $batches = $pendingDetails->chunk($batchSize);
  217. foreach ($batches as $batchIndex => $batch) {
  218. Log::info("处理第 " . ($batchIndex + 1) . " 批分红", [
  219. 'batch_size' => $batch->count()
  220. ]);
  221. $batchResult = $this->processSingleBatch($dividendRecord, $batch, $perPartnerAmount);
  222. $totalSuccessCount += $batchResult['success_count'];
  223. $totalFailedCount += $batchResult['failed_count'];
  224. // 批次间短暂休息,避免系统压力过大
  225. if ($batchIndex < $batches->count() - 1) {
  226. usleep(100000); // 休息0.1秒
  227. }
  228. }
  229. return [
  230. 'success_count' => $totalSuccessCount,
  231. 'failed_count' => $totalFailedCount
  232. ];
  233. }
  234. /**
  235. * 创建分红详情记录(如果不存在)
  236. */
  237. private function createDividendDetailsIfNotExists(UrsPartnerDividendRecord $dividendRecord, array $partners, string $perPartnerAmount): void
  238. {
  239. foreach ($partners as $partner) {
  240. // 检查是否已存在
  241. $existingDetail = UrsPartnerDividendDetail::where('dividend_record_id', $dividendRecord->id)
  242. ->where('user_id', $partner['user_id'])
  243. ->first();
  244. if (!$existingDetail) {
  245. UrsPartnerDividendDetail::create([
  246. 'dividend_record_id' => $dividendRecord->id,
  247. 'user_id' => $partner['user_id'],
  248. 'urs_user_id' => $partner['urs_user_id'],
  249. 'talent_level' => $partner['talent_level'],
  250. 'dividend_amount' => $perPartnerAmount,
  251. 'status' => UrsPartnerDividendDetail::STATUS_PENDING
  252. ]);
  253. }
  254. }
  255. }
  256. /**
  257. * 处理单个批次
  258. */
  259. private function processSingleBatch(UrsPartnerDividendRecord $dividendRecord, $batch, string $perPartnerAmount): array
  260. {
  261. $successCount = 0;
  262. $failedCount = 0;
  263. foreach ($batch as $detail) {
  264. try {
  265. // 使用独立事务处理每个转账
  266. DB::transaction(function () use ($detail, $dividendRecord, $perPartnerAmount, &$successCount, &$failedCount) {
  267. // 执行手续费转移
  268. $transferResult = FeeService::transfer(
  269. $dividendRecord->transfer_app_id,
  270. $detail->user_id,
  271. floatval($perPartnerAmount),
  272. $dividendRecord->id,
  273. 'partner_dividend'
  274. );
  275. if ($transferResult === true) {
  276. // 转账成功
  277. $detail->update([
  278. 'status' => UrsPartnerDividendDetail::STATUS_COMPLETED
  279. ]);
  280. $successCount++;
  281. Log::info("合伙人分红转账成功", [
  282. 'user_id' => $detail->user_id,
  283. 'urs_user_id' => $detail->urs_user_id,
  284. 'amount' => $perPartnerAmount
  285. ]);
  286. } else {
  287. // 转账失败
  288. $detail->update([
  289. 'status' => UrsPartnerDividendDetail::STATUS_FAILED,
  290. 'error_message' => is_string($transferResult) ? $transferResult : '转账失败'
  291. ]);
  292. $failedCount++;
  293. Log::error("合伙人分红转账失败", [
  294. 'user_id' => $detail->user_id,
  295. 'urs_user_id' => $detail->urs_user_id,
  296. 'amount' => $perPartnerAmount,
  297. 'error' => $transferResult
  298. ]);
  299. }
  300. });
  301. } catch (\Exception $e) {
  302. $failedCount++;
  303. // 更新详情状态为失败
  304. try {
  305. $detail->update([
  306. 'status' => UrsPartnerDividendDetail::STATUS_FAILED,
  307. 'error_message' => $e->getMessage()
  308. ]);
  309. } catch (\Exception $updateException) {
  310. Log::error("更新分红详情状态失败", [
  311. 'detail_id' => $detail->id,
  312. 'error' => $updateException->getMessage()
  313. ]);
  314. }
  315. Log::error("处理合伙人分红详情失败", [
  316. 'user_id' => $detail->user_id,
  317. 'urs_user_id' => $detail->urs_user_id,
  318. 'error' => $e->getMessage()
  319. ]);
  320. }
  321. }
  322. return [
  323. 'success_count' => $successCount,
  324. 'failed_count' => $failedCount
  325. ];
  326. }
  327. /**
  328. * 更新分红记录状态
  329. */
  330. private function updateDividendRecordStatus(UrsPartnerDividendRecord $dividendRecord, array $result): void
  331. {
  332. if ($result['failed_count'] == 0) {
  333. // 全部成功
  334. $dividendRecord->update(['status' => UrsPartnerDividendRecord::STATUS_COMPLETED]);
  335. } else if ($result['success_count'] == 0) {
  336. // 全部失败
  337. $dividendRecord->update([
  338. 'status' => UrsPartnerDividendRecord::STATUS_FAILED,
  339. 'error_message' => '所有分红转账都失败了'
  340. ]);
  341. } else {
  342. // 部分成功
  343. $dividendRecord->update([
  344. 'status' => UrsPartnerDividendRecord::STATUS_COMPLETED,
  345. 'error_message' => "部分转账失败,成功{$result['success_count']}个,失败{$result['failed_count']}个"
  346. ]);
  347. }
  348. }
  349. /**
  350. * 重试失败的分红
  351. *
  352. * @param string $date 分红日期
  353. * @return array 重试结果
  354. */
  355. public function retryFailedDividend(string $date): array
  356. {
  357. Log::info("开始重试失败的分红", ['date' => $date]);
  358. $dividendRecord = UrsPartnerDividendRecord::getByDate($date);
  359. if (!$dividendRecord) {
  360. return ['success' => false, 'message' => '分红记录不存在'];
  361. }
  362. // 获取失败的分红详情
  363. $failedDetails = UrsPartnerDividendDetail::where('dividend_record_id', $dividendRecord->id)
  364. ->where('status', UrsPartnerDividendDetail::STATUS_FAILED)
  365. ->get();
  366. if ($failedDetails->isEmpty()) {
  367. return ['success' => false, 'message' => '没有失败的分红记录需要重试'];
  368. }
  369. Log::info("找到失败的分红记录", ['count' => $failedDetails->count()]);
  370. $successCount = 0;
  371. $failedCount = 0;
  372. foreach ($failedDetails as $detail) {
  373. try {
  374. // 重置状态为待处理
  375. $detail->update([
  376. 'status' => UrsPartnerDividendDetail::STATUS_PENDING,
  377. 'error_message' => null
  378. ]);
  379. // 重新执行转账
  380. DB::transaction(function () use ($detail, $dividendRecord, &$successCount, &$failedCount) {
  381. $transferResult = FeeService::transfer(
  382. $dividendRecord->transfer_app_id,
  383. $detail->user_id,
  384. floatval($detail->dividend_amount),
  385. $dividendRecord->id,
  386. 'partner_dividend_retry'
  387. );
  388. if ($transferResult === true) {
  389. $detail->update(['status' => UrsPartnerDividendDetail::STATUS_COMPLETED]);
  390. $successCount++;
  391. Log::info("重试分红转账成功", [
  392. 'user_id' => $detail->user_id,
  393. 'amount' => $detail->dividend_amount
  394. ]);
  395. } else {
  396. $detail->update([
  397. 'status' => UrsPartnerDividendDetail::STATUS_FAILED,
  398. 'error_message' => is_string($transferResult) ? $transferResult : '重试转账失败'
  399. ]);
  400. $failedCount++;
  401. Log::error("重试分红转账失败", [
  402. 'user_id' => $detail->user_id,
  403. 'error' => $transferResult
  404. ]);
  405. }
  406. });
  407. } catch (\Exception $e) {
  408. $failedCount++;
  409. $detail->update([
  410. 'status' => UrsPartnerDividendDetail::STATUS_FAILED,
  411. 'error_message' => '重试异常: ' . $e->getMessage()
  412. ]);
  413. Log::error("重试分红异常", [
  414. 'user_id' => $detail->user_id,
  415. 'error' => $e->getMessage()
  416. ]);
  417. }
  418. }
  419. // 更新分红记录状态
  420. $this->updateDividendRecordStatus($dividendRecord, [
  421. 'success_count' => $successCount,
  422. 'failed_count' => $failedCount
  423. ]);
  424. return [
  425. 'success' => true,
  426. 'message' => '重试完成',
  427. 'data' => [
  428. 'retry_count' => $failedDetails->count(),
  429. 'success_count' => $successCount,
  430. 'failed_count' => $failedCount
  431. ]
  432. ];
  433. }
  434. }