FundsSummaryService.php 13 KB

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