FundsSummaryService.php 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311
  1. <?php
  2. namespace App\Services;
  3. use Carbon\Carbon;
  4. use Illuminate\Support\Facades\DB;
  5. class FundsSummaryService
  6. {
  7. public static function summary(?string $startDate = null, ?string $endDate = null): array
  8. {
  9. [$start, $end] = $startDate !== null && $endDate !== null
  10. ? self::dateRange($startDate, $endDate)
  11. : [null, null];
  12. $balance = self::balanceAggregates($start, $end);
  13. $withdraw = self::withdrawAggregates($start, $end);
  14. $paymentOrder = self::paymentOrderAggregates($start, $end);
  15. $combinedPeople = self::combinedPeople($start, $end);
  16. $commission = app(ThirdGameCommissionService::class)->summary($start?->toDateTimeString(), $end?->toDateTimeString());
  17. $income = [
  18. self::item('company_deposit', '公司入款', $balance->company_deposit_amount, $balance->company_deposit_people),
  19. self::item(
  20. 'third_party_deposit',
  21. '第三方支付(入款)',
  22. $balance->third_party_deposit_amount,
  23. $balance->third_party_deposit_people
  24. ),
  25. self::item('manual_credit', '人工上分', $balance->manual_credit_amount, $balance->manual_credit_people),
  26. self::item(
  27. 'member_withdraw_deduction',
  28. '会员出款扣除',
  29. (float)$withdraw->member_withdraw_deduction_amount
  30. + (float)$paymentOrder->manual_withdrawal_fee_amount,
  31. $combinedPeople['member_withdraw_deduction']
  32. ),
  33. ];
  34. $expense = [
  35. self::item(
  36. 'member_withdrawal',
  37. '会员出款',
  38. $withdraw->member_withdrawal_amount,
  39. $withdraw->member_withdrawal_people
  40. ),
  41. self::item(
  42. 'manual_withdrawal',
  43. '人工出款',
  44. (float)$balance->manual_debit_amount + (float)$paymentOrder->manual_withdrawal_amount,
  45. $combinedPeople['manual_withdrawal']
  46. ),
  47. self::item('vip_discount', '给予优惠(VIP 折扣)', 0, 0),
  48. self::item('rebate', '给予返点', $balance->rebate_amount, $balance->rebate_people),
  49. self::item('bonus', '优惠金', $balance->bonus_amount, $balance->bonus_people),
  50. self::item(
  51. 'third_party_fee',
  52. '第三方手续费',
  53. $paymentOrder->third_party_fee_amount,
  54. $paymentOrder->third_party_fee_people
  55. ),
  56. self::item(
  57. 'third_party_auto_withdrawal',
  58. '第三方支付出款(自动出款)',
  59. $paymentOrder->auto_withdrawal_amount,
  60. $paymentOrder->auto_withdrawal_people
  61. ),
  62. self::item(
  63. 'third_party_auto_withdrawal_fee',
  64. '第三方支付自动出款手续费',
  65. $paymentOrder->auto_withdrawal_fee_amount,
  66. $paymentOrder->auto_withdrawal_fee_people
  67. ),
  68. ['key' => 'third_game_commission', 'label' => '第三方游戏平台抽成',
  69. 'amount' => $commission['amount'] === null ? null : self::commissionMoney($commission['amount']),
  70. 'people' => $commission['people']],
  71. ];
  72. return [
  73. 'start_date' => $startDate,
  74. 'end_date' => $endDate,
  75. 'income' => $income,
  76. 'expense' => $expense,
  77. 'income_total' => self::sumItems($income),
  78. 'expense_total' => $commission['complete'] ? self::sumItems($expense) : null,
  79. 'known_expense_total' => bcadd(self::sumItems($expense),
  80. $commission['complete'] ? '0.00' : self::commissionMoney($commission['known_amount']), 2),
  81. 'third_game_net_profit' => $commission['net_profit'] === null ? null : self::commissionMoney($commission['net_profit']),
  82. 'third_game_commission_meta' => $commission,
  83. ];
  84. }
  85. private static function balanceAggregates(?Carbon $start, ?Carbon $end): object
  86. {
  87. $query = DB::table('balance_logs')
  88. ->whereIn('change_type', [
  89. '充值', '三方充值', '人工充值', '订单补偿', '人工扣款',
  90. '比比返', '笔笔返', '返水', '回水',
  91. '注册赠送', '优惠活动', '即充即送', '充值返现', '老用户回归',
  92. ]);
  93. return self::applyDateRange($query, 'created_at', $start, $end)
  94. ->selectRaw(<<<'SQL'
  95. COALESCE(SUM(CASE
  96. WHEN change_type = '充值' AND amount > 0 THEN amount ELSE 0 END), 0
  97. ) AS company_deposit_amount,
  98. COUNT(DISTINCT CASE
  99. WHEN change_type = '充值' AND amount > 0 THEN member_id END
  100. ) AS company_deposit_people,
  101. COALESCE(SUM(CASE
  102. WHEN change_type = '三方充值' AND amount > 0 THEN amount ELSE 0 END), 0
  103. ) AS third_party_deposit_amount,
  104. COUNT(DISTINCT CASE
  105. WHEN change_type = '三方充值' AND amount > 0 THEN member_id END
  106. ) AS third_party_deposit_people,
  107. COALESCE(SUM(CASE
  108. WHEN ((change_type = '人工充值' AND related_id IS NULL) OR change_type = '订单补偿') AND amount > 0 THEN amount ELSE 0 END), 0
  109. ) AS manual_credit_amount,
  110. COUNT(DISTINCT CASE
  111. WHEN ((change_type = '人工充值' AND related_id IS NULL) OR change_type = '订单补偿') AND amount > 0 THEN member_id END
  112. ) AS manual_credit_people,
  113. COALESCE(SUM(CASE
  114. WHEN change_type = '人工扣款' AND amount < 0 THEN ABS(amount) ELSE 0 END), 0
  115. ) AS manual_debit_amount,
  116. COALESCE(SUM(CASE
  117. WHEN change_type IN ('比比返', '笔笔返', '返水', '回水') AND amount > 0 THEN amount ELSE 0 END), 0
  118. ) AS rebate_amount,
  119. COUNT(DISTINCT CASE
  120. WHEN change_type IN ('比比返', '笔笔返', '返水', '回水') AND amount > 0 THEN member_id END
  121. ) AS rebate_people,
  122. COALESCE(SUM(CASE
  123. WHEN (
  124. change_type IN ('注册赠送', '优惠活动', '即充即送', '充值返现', '老用户回归')
  125. OR (change_type = '人工充值' AND related_id = 0)
  126. ) AND amount > 0 THEN amount ELSE 0 END), 0
  127. ) AS bonus_amount,
  128. COUNT(DISTINCT CASE
  129. WHEN (
  130. change_type IN ('注册赠送', '优惠活动', '即充即送', '充值返现', '老用户回归')
  131. OR (change_type = '人工充值' AND related_id = 0)
  132. ) AND amount > 0 THEN member_id END
  133. ) AS bonus_people
  134. SQL)
  135. ->first();
  136. }
  137. private static function withdrawAggregates(?Carbon $start, ?Carbon $end): object
  138. {
  139. $balanceLogAlias = self::rawAlias('bl');
  140. $withdrawAlias = self::rawAlias('w');
  141. return self::successfulWithdrawLogs($start, $end)
  142. ->selectRaw(<<<SQL
  143. COALESCE(SUM(ABS({$balanceLogAlias}.`amount`)), 0) AS member_withdrawal_amount,
  144. COUNT(DISTINCT {$balanceLogAlias}.`member_id`) AS member_withdrawal_people,
  145. COALESCE(SUM(CASE
  146. WHEN {$withdrawAlias}.`service_charge` > 0
  147. THEN ABS({$balanceLogAlias}.`amount`) * {$withdrawAlias}.`service_charge`
  148. / NULLIF({$withdrawAlias}.`amount`, 0)
  149. ELSE 0 END), 0
  150. ) AS member_withdraw_deduction_amount
  151. SQL)
  152. ->first();
  153. }
  154. private static function paymentOrderAggregates(?Carbon $start, ?Carbon $end): object
  155. {
  156. $pay = PaymentOrderService::TYPE_PAY;
  157. $payout = PaymentOrderService::TYPE_PAYOUT;
  158. $manualPayout = PaymentOrderService::TYPE_SELF_PAYOUT;
  159. $query = DB::table('payment_orders')
  160. ->where('status', PaymentOrderService::STATUS_SUCCESS)
  161. ->whereIn('type', [$pay, $payout, $manualPayout]);
  162. return self::applyDateRange($query, 'created_at', $start, $end)
  163. ->selectRaw(<<<SQL
  164. COALESCE(SUM(CASE WHEN type = {$pay} AND fee > 0 THEN fee ELSE 0 END), 0)
  165. AS third_party_fee_amount,
  166. COUNT(DISTINCT CASE WHEN type = {$pay} AND fee > 0 THEN member_id END)
  167. AS third_party_fee_people,
  168. COALESCE(SUM(CASE WHEN type = {$payout} THEN amount ELSE 0 END), 0)
  169. AS auto_withdrawal_amount,
  170. COUNT(DISTINCT CASE WHEN type = {$payout} THEN member_id END)
  171. AS auto_withdrawal_people,
  172. COALESCE(SUM(CASE WHEN type = {$payout} AND fee > 0 THEN fee ELSE 0 END), 0)
  173. AS auto_withdrawal_fee_amount,
  174. COUNT(DISTINCT CASE WHEN type = {$payout} AND fee > 0 THEN member_id END)
  175. AS auto_withdrawal_fee_people,
  176. COALESCE(SUM(CASE WHEN type = {$manualPayout} THEN amount ELSE 0 END), 0)
  177. AS manual_withdrawal_amount,
  178. COALESCE(SUM(CASE WHEN type = {$manualPayout} AND fee > 0 THEN fee ELSE 0 END), 0)
  179. AS manual_withdrawal_fee_amount
  180. SQL)
  181. ->first();
  182. }
  183. /**
  184. * Count unique members across categories backed by more than one table.
  185. *
  186. * @return array{member_withdraw_deduction:int, manual_withdrawal:int}
  187. */
  188. private static function combinedPeople(?Carbon $start, ?Carbon $end): array
  189. {
  190. $withdrawFees = self::successfulWithdrawLogs($start, $end)
  191. ->where('w.service_charge', '>', 0)
  192. ->selectRaw("'member_withdraw_deduction' AS category")
  193. ->addSelect('bl.member_id as member_id');
  194. $manualPayoutFees = self::applyDateRange(DB::table('payment_orders as po')
  195. ->where('po.type', PaymentOrderService::TYPE_SELF_PAYOUT)
  196. ->where('po.status', PaymentOrderService::STATUS_SUCCESS)
  197. ->where('po.fee', '>', 0), 'po.created_at', $start, $end)
  198. ->selectRaw("'member_withdraw_deduction' AS category")
  199. ->addSelect('po.member_id as member_id');
  200. $manualDebits = self::applyDateRange(DB::table('balance_logs as bl')
  201. ->where('bl.change_type', '人工扣款')
  202. ->where('bl.amount', '<', 0), 'bl.created_at', $start, $end)
  203. ->selectRaw("'manual_withdrawal' AS category")
  204. ->addSelect('bl.member_id as member_id');
  205. $manualPayouts = self::applyDateRange(DB::table('payment_orders as po')
  206. ->where('po.type', PaymentOrderService::TYPE_SELF_PAYOUT)
  207. ->where('po.status', PaymentOrderService::STATUS_SUCCESS), 'po.created_at', $start, $end)
  208. ->selectRaw("'manual_withdrawal' AS category")
  209. ->addSelect('po.member_id as member_id');
  210. $union = $withdrawFees
  211. ->unionAll($manualPayoutFees)
  212. ->unionAll($manualDebits)
  213. ->unionAll($manualPayouts);
  214. $rows = DB::query()
  215. ->fromSub($union, 'finance_members')
  216. ->whereNotNull('member_id')
  217. ->select('category')
  218. ->selectRaw('COUNT(DISTINCT member_id) AS people')
  219. ->groupBy('category')
  220. ->get();
  221. $result = [
  222. 'member_withdraw_deduction' => 0,
  223. 'manual_withdrawal' => 0,
  224. ];
  225. foreach ($rows as $row) {
  226. $result[(string)$row->category] = (int)$row->people;
  227. }
  228. return $result;
  229. }
  230. private static function successfulWithdrawLogs(?Carbon $start, ?Carbon $end)
  231. {
  232. $query = DB::table('balance_logs as bl')
  233. ->join('withdraws as w', 'w.id', '=', 'bl.related_id')
  234. ->where('w.status', 1)
  235. ->where('bl.change_type', '提现')
  236. ->where('bl.amount', '<', 0);
  237. return self::applyDateRange($query, 'bl.created_at', $start, $end);
  238. }
  239. private static function applyDateRange($query, string $column, ?Carbon $start, ?Carbon $end)
  240. {
  241. if ($start !== null && $end !== null) {
  242. $query->whereBetween($column, [$start, $end]);
  243. }
  244. return $query;
  245. }
  246. private static function rawAlias(string $alias): string
  247. {
  248. return '`' . str_replace('`', '``', DB::getTablePrefix() . $alias) . '`';
  249. }
  250. private static function item(string $key, string $label, $amount, $people): array
  251. {
  252. return [
  253. 'key' => $key,
  254. 'label' => $label,
  255. 'amount' => self::money((float)$amount),
  256. 'people' => (int)$people,
  257. ];
  258. }
  259. private static function sumItems(array $items): string
  260. {
  261. return array_reduce($items, static fn (string $sum, array $item): string
  262. => bcadd($sum, $item['amount'] ?? '0.00', 2), '0.00');
  263. }
  264. private static function commissionMoney(string $amount): string
  265. {
  266. return bcadd($amount, bccomp($amount, '0', 10) < 0 ? '-0.005' : '0.005', 2);
  267. }
  268. private static function money(float $amount): string
  269. {
  270. return number_format($amount, 2, '.', '');
  271. }
  272. /**
  273. * @return array{0: Carbon, 1: Carbon}
  274. */
  275. private static function dateRange(string $startDate, string $endDate): array
  276. {
  277. $timezone = config('app.timezone', 'Asia/Shanghai');
  278. return [
  279. Carbon::createFromFormat('Y-m-d', $startDate, $timezone)->startOfDay(),
  280. Carbon::createFromFormat('Y-m-d', $endDate, $timezone)->endOfDay(),
  281. ];
  282. }
  283. }