GiftCardExpiryService.php 2.1 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364
  1. <?php
  2. namespace Longyi\Gift\Services;
  3. use Carbon\Carbon;
  4. use Illuminate\Support\Facades\DB;
  5. use Longyi\Gift\Models\GiftCards;
  6. /**
  7. * 礼品卡过期处理。
  8. *
  9. * 判定规则:
  10. * - 只处理「未使用」(giftcard_status = 1)的礼品卡,已使用(2)的卡不会被覆盖;
  11. * - expirationdate 不为空且 <= 判定时间,才视为过期;
  12. * - expirationdate 为空的卡视为永久有效,不处理;
  13. * - 幂等:状态已是「已过期」(3)的卡不会再被命中,重复执行不会产生副作用。
  14. */
  15. class GiftCardExpiryService
  16. {
  17. /**
  18. * 默认每批处理条数。
  19. */
  20. public const DEFAULT_BATCH_SIZE = 500;
  21. /**
  22. * 把已过期的未使用礼品卡标记为「已过期」。
  23. *
  24. * @param Carbon|null $at 过期判定基准时间,默认当前时间
  25. * @param bool $dryRun true 时只统计不写库
  26. * @param int $batchSize 每批处理条数
  27. * @return int 被更新(dry-run 时为被判定过期)的礼品卡数量
  28. */
  29. public function expire(?Carbon $at = null, bool $dryRun = false, int $batchSize = self::DEFAULT_BATCH_SIZE): int
  30. {
  31. $at = $at ?: now();
  32. $batchSize = $batchSize > 0 ? $batchSize : self::DEFAULT_BATCH_SIZE;
  33. $pending = fn () => DB::table('gift_cards')
  34. ->where('giftcard_status', GiftCards::STATUS_UNUSED)
  35. ->whereNotNull('expirationdate')
  36. ->where('expirationdate', '<=', $at);
  37. if ($dryRun) {
  38. return (int) $pending()->count();
  39. }
  40. $total = 0;
  41. // 按 id 分批,避免一次性锁定/更新过多行。
  42. $pending()
  43. ->select('id')
  44. ->chunkById($batchSize, function ($rows) use (&$total, $at) {
  45. $total += DB::table('gift_cards')
  46. ->whereIn('id', $rows->pluck('id')->all())
  47. ->update([
  48. 'giftcard_status' => GiftCards::STATUS_EXPIRED,
  49. 'updated_at' => $at,
  50. ]);
  51. });
  52. return $total;
  53. }
  54. }