UrsPartnerDividendLogic.php 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505
  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. // 这是业务需求,与一般Logic层规范不同
  267. DB::transaction(function () use ($detail, $dividendRecord, $perPartnerAmount, &$successCount, &$failedCount) {
  268. // 执行手续费转移
  269. $transferResult = FeeService::transfer(
  270. $dividendRecord->transfer_app_id,
  271. $detail->user_id,
  272. floatval($perPartnerAmount),
  273. $dividendRecord->id,
  274. 'partner_dividend'
  275. );
  276. if ($transferResult === true) {
  277. // 转账成功
  278. $detail->update([
  279. 'status' => UrsPartnerDividendDetail::STATUS_COMPLETED
  280. ]);
  281. $successCount++;
  282. Log::info("合伙人分红转账成功", [
  283. 'user_id' => $detail->user_id,
  284. 'urs_user_id' => $detail->urs_user_id,
  285. 'amount' => $perPartnerAmount
  286. ]);
  287. } else {
  288. // 转账失败
  289. $detail->update([
  290. 'status' => UrsPartnerDividendDetail::STATUS_FAILED,
  291. 'error_message' => is_string($transferResult) ? $transferResult : '转账失败'
  292. ]);
  293. $failedCount++;
  294. Log::error("合伙人分红转账失败", [
  295. 'user_id' => $detail->user_id,
  296. 'urs_user_id' => $detail->urs_user_id,
  297. 'amount' => $perPartnerAmount,
  298. 'error' => $transferResult
  299. ]);
  300. }
  301. });
  302. } catch (\Exception $e) {
  303. $failedCount++;
  304. // 更新详情状态为失败
  305. try {
  306. $detail->update([
  307. 'status' => UrsPartnerDividendDetail::STATUS_FAILED,
  308. 'error_message' => $e->getMessage()
  309. ]);
  310. } catch (\Exception $updateException) {
  311. Log::error("更新分红详情状态失败", [
  312. 'detail_id' => $detail->id,
  313. 'error' => $updateException->getMessage()
  314. ]);
  315. }
  316. Log::error("处理合伙人分红详情失败", [
  317. 'user_id' => $detail->user_id,
  318. 'urs_user_id' => $detail->urs_user_id,
  319. 'error' => $e->getMessage()
  320. ]);
  321. }
  322. }
  323. return [
  324. 'success_count' => $successCount,
  325. 'failed_count' => $failedCount
  326. ];
  327. }
  328. /**
  329. * 更新分红记录状态
  330. */
  331. private function updateDividendRecordStatus(UrsPartnerDividendRecord $dividendRecord, array $result): void
  332. {
  333. if ($result['failed_count'] == 0) {
  334. // 全部成功
  335. $dividendRecord->update(['status' => UrsPartnerDividendRecord::STATUS_COMPLETED]);
  336. } else if ($result['success_count'] == 0) {
  337. // 全部失败
  338. $dividendRecord->update([
  339. 'status' => UrsPartnerDividendRecord::STATUS_FAILED,
  340. 'error_message' => '所有分红转账都失败了'
  341. ]);
  342. } else {
  343. // 部分成功
  344. $dividendRecord->update([
  345. 'status' => UrsPartnerDividendRecord::STATUS_COMPLETED,
  346. 'error_message' => "部分转账失败,成功{$result['success_count']}个,失败{$result['failed_count']}个"
  347. ]);
  348. }
  349. }
  350. /**
  351. * 重试失败的分红
  352. *
  353. * @param string $date 分红日期
  354. * @return array 重试结果
  355. */
  356. public function retryFailedDividend(string $date): array
  357. {
  358. Log::info("开始重试失败的分红", ['date' => $date]);
  359. $dividendRecord = UrsPartnerDividendRecord::getByDate($date);
  360. if (!$dividendRecord) {
  361. return ['success' => false, 'message' => '分红记录不存在'];
  362. }
  363. // 获取失败的分红详情
  364. $failedDetails = UrsPartnerDividendDetail::where('dividend_record_id', $dividendRecord->id)
  365. ->where('status', UrsPartnerDividendDetail::STATUS_FAILED)
  366. ->get();
  367. if ($failedDetails->isEmpty()) {
  368. return ['success' => false, 'message' => '没有失败的分红记录需要重试'];
  369. }
  370. Log::info("找到失败的分红记录", ['count' => $failedDetails->count()]);
  371. $successCount = 0;
  372. $failedCount = 0;
  373. foreach ($failedDetails as $detail) {
  374. try {
  375. // 重置状态为待处理
  376. $detail->update([
  377. 'status' => UrsPartnerDividendDetail::STATUS_PENDING,
  378. 'error_message' => null
  379. ]);
  380. // 重新执行转账
  381. DB::transaction(function () use ($detail, $dividendRecord, &$successCount, &$failedCount) {
  382. $transferResult = FeeService::transfer(
  383. $dividendRecord->transfer_app_id,
  384. $detail->user_id,
  385. floatval($detail->dividend_amount),
  386. $dividendRecord->id,
  387. 'partner_dividend_retry'
  388. );
  389. if ($transferResult === true) {
  390. $detail->update(['status' => UrsPartnerDividendDetail::STATUS_COMPLETED]);
  391. $successCount++;
  392. Log::info("重试分红转账成功", [
  393. 'user_id' => $detail->user_id,
  394. 'amount' => $detail->dividend_amount
  395. ]);
  396. } else {
  397. $detail->update([
  398. 'status' => UrsPartnerDividendDetail::STATUS_FAILED,
  399. 'error_message' => is_string($transferResult) ? $transferResult : '重试转账失败'
  400. ]);
  401. $failedCount++;
  402. Log::error("重试分红转账失败", [
  403. 'user_id' => $detail->user_id,
  404. 'error' => $transferResult
  405. ]);
  406. }
  407. });
  408. } catch (\Exception $e) {
  409. $failedCount++;
  410. $detail->update([
  411. 'status' => UrsPartnerDividendDetail::STATUS_FAILED,
  412. 'error_message' => '重试异常: ' . $e->getMessage()
  413. ]);
  414. Log::error("重试分红异常", [
  415. 'user_id' => $detail->user_id,
  416. 'error' => $e->getMessage()
  417. ]);
  418. }
  419. }
  420. // 更新分红记录状态
  421. $this->updateDividendRecordStatus($dividendRecord, [
  422. 'success_count' => $successCount,
  423. 'failed_count' => $failedCount
  424. ]);
  425. return [
  426. 'success' => true,
  427. 'message' => '重试完成',
  428. 'data' => [
  429. 'retry_count' => $failedDetails->count(),
  430. 'success_count' => $successCount,
  431. 'failed_count' => $failedCount
  432. ]
  433. ];
  434. }
  435. }