GiftCardStatisticsService.php 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147
  1. <?php
  2. namespace Longyi\Gift\Services;
  3. use Carbon\Carbon;
  4. use Carbon\CarbonPeriod;
  5. use Illuminate\Support\Facades\DB;
  6. /**
  7. * 礼品卡统计。
  8. *
  9. * - 新增礼品卡:`gift_cards.created_at` 落在统计区间内的数量。
  10. * - 过期礼品卡:`gift_cards.expirationdate` 落在统计区间内的数量(不区分是否已使用)。
  11. */
  12. class GiftCardStatisticsService
  13. {
  14. /**
  15. * 区间超过这个天数就按月聚合,否则按天。
  16. */
  17. public const MAX_DAILY_DAYS = 92;
  18. /**
  19. * 单次统计允许的最大区间(天),防止区间过大把图表撑爆。
  20. */
  21. public const MAX_RANGE_DAYS = 730;
  22. /**
  23. * @return array{
  24. * labels:list<string>,
  25. * granularity:string,
  26. * new:list<int>,
  27. * expired:list<int>,
  28. * summary:array{available:int, used:int, new:int, expired:int}
  29. * }
  30. */
  31. public function get(Carbon $start, Carbon $end): array
  32. {
  33. [$start, $end] = $this->normalizeRange($start, $end);
  34. $granularity = $start->diffInDays($end) > self::MAX_DAILY_DAYS ? 'month' : 'day';
  35. $labels = $this->labels($start, $end, $granularity);
  36. $format = $granularity === 'month' ? '%Y-%m' : '%Y-%m-%d';
  37. $newRows = DB::table('gift_cards')
  38. ->selectRaw("DATE_FORMAT(created_at, '".$format."') as period, COUNT(*) as total")
  39. ->whereBetween('created_at', [$start, $end])
  40. ->groupBy('period')
  41. ->pluck('total', 'period');
  42. $expiredRows = DB::table('gift_cards')
  43. ->selectRaw("DATE_FORMAT(expirationdate, '".$format."') as period, COUNT(*) as total")
  44. ->whereNotNull('expirationdate')
  45. ->whereBetween('expirationdate', [$start, $end])
  46. ->groupBy('period')
  47. ->pluck('total', 'period');
  48. $new = [];
  49. $expired = [];
  50. foreach ($labels as $label) {
  51. $new[] = (int) ($newRows[$label] ?? 0);
  52. $expired[] = (int) ($expiredRows[$label] ?? 0);
  53. }
  54. return [
  55. 'labels' => $labels,
  56. 'granularity' => $granularity,
  57. 'new' => $new,
  58. 'expired' => $expired,
  59. 'summary' => [
  60. 'available' => $this->availableCount(),
  61. 'used' => $this->usedCount(),
  62. 'new' => array_sum($new),
  63. 'expired' => array_sum($expired),
  64. ],
  65. ];
  66. }
  67. /**
  68. * 校正区间:保证 start <= end,并且区间长度不超过 MAX_RANGE_DAYS。
  69. *
  70. * @return array{0:Carbon, 1:Carbon}
  71. */
  72. protected function normalizeRange(Carbon $start, Carbon $end): array
  73. {
  74. $start = $start->copy()->startOfDay();
  75. $end = $end->copy()->endOfDay();
  76. if ($start->greaterThan($end)) {
  77. [$start, $end] = [$end->copy()->startOfDay(), $start->copy()->endOfDay()];
  78. }
  79. if ($start->diffInDays($end) > self::MAX_RANGE_DAYS) {
  80. $start = $end->copy()->subDays(self::MAX_RANGE_DAYS)->startOfDay();
  81. }
  82. return [$start, $end];
  83. }
  84. /**
  85. * 生成完整的横轴标签(没有数据的区间补 0,避免图表出现断点)。
  86. *
  87. * @return list<string>
  88. */
  89. protected function labels(Carbon $start, Carbon $end, string $granularity): array
  90. {
  91. $period = $granularity === 'month'
  92. ? CarbonPeriod::create($start->copy()->startOfMonth(), '1 month', $end->copy()->startOfMonth())
  93. : CarbonPeriod::create($start->copy(), '1 day', $end->copy());
  94. $format = $granularity === 'month' ? 'Y-m' : 'Y-m-d';
  95. $labels = [];
  96. foreach ($period as $date) {
  97. $labels[] = $date->format($format);
  98. }
  99. return $labels;
  100. }
  101. /**
  102. * 当前可用(未使用且未过期)的礼品卡数量。
  103. */
  104. protected function availableCount(): int
  105. {
  106. return (int) DB::table('gift_cards')
  107. ->where('giftcard_status', 1)
  108. ->where(function ($query) {
  109. $query->whereNull('expirationdate')
  110. ->orWhere('expirationdate', '>=', now());
  111. })
  112. ->count();
  113. }
  114. /**
  115. * 已经用完(状态为已使用)的礼品卡数量。
  116. */
  117. protected function usedCount(): int
  118. {
  119. return (int) DB::table('gift_cards')
  120. ->where('giftcard_status', 2)
  121. ->count();
  122. }
  123. }