WaterFinanceDashboard.php 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144
  1. <?php
  2. declare(strict_types=1);
  3. namespace plugin\saiadmin\app\service;
  4. use OpenSpout\Common\Entity\Row;
  5. use OpenSpout\Writer\XLSX\Writer;
  6. use plugin\saiadmin\app\model\water\WaterModel;
  7. use plugin\saiadmin\app\model\water\WaterOrder;
  8. use plugin\saiadmin\app\model\water\WaterCommission;
  9. use plugin\saiadmin\app\service\finance\FinanceFilters;
  10. use plugin\saiadmin\app\service\finance\ReceiptSources;
  11. use plugin\saiadmin\app\service\finance\WithdrawalDemo;
  12. use plugin\saiadmin\exception\ApiException;
  13. /** Finance read facade; only recorded water receipts are available in the current domain. */
  14. final class WaterFinanceDashboard
  15. {
  16. private readonly \DateTimeImmutable $now;
  17. public function __construct(?\DateTimeImmutable $now = null)
  18. {
  19. $this->now = ($now ?? new \DateTimeImmutable())->setTimezone(new \DateTimeZone('Asia/Shanghai'));
  20. }
  21. private function period(FinanceFilters $filters)
  22. {
  23. return WaterOrder::query()->financePeriod($filters->start->format('Y-m-d H:i:s'), $filters->end->format('Y-m-d H:i:s'))
  24. ->financeSource($filters->source);
  25. }
  26. private function rows(FinanceFilters $filters)
  27. {
  28. return $this->period($filters)->financeDetails($filters->keyword)->orderByDesc('o.created_at')->orderByDesc('o.id');
  29. }
  30. private function totals($query): array
  31. {
  32. $row = $query->selectRaw('COUNT(*) AS count, COALESCE(SUM(o.amount_cents),0) AS cents')->first();
  33. return ['count' => (int) $row->count, 'cents' => (int) $row->cents];
  34. }
  35. private function formatRow(array $row, array $sources = []): array
  36. {
  37. foreach (['id', 'enterprise_id', 'buckets', 'amount_cents'] as $key) {
  38. $row[$key] = (int) $row[$key];
  39. }
  40. $row['is_demo'] = (bool) $row['is_demo'];
  41. $row['number'] = 'WS' . str_pad((string) $row['id'], 10, '0', STR_PAD_LEFT);
  42. $row['enterprise_name'] = $row['enterprise_name'] ?? '企业档案不存在';
  43. $row['type_label'] = $sources[$row['id']] ?? '线下购水登记';
  44. $row['status_label'] = $row['is_demo'] ? '模拟登记' : '已登记';
  45. return $row;
  46. }
  47. public function dashboard(array $input): array
  48. {
  49. $filters = new FinanceFilters($input, $this->now);
  50. return WaterModel::transaction(function () use ($filters) {
  51. $monthly = $this->totals($this->period($filters));
  52. $today = $this->totals(WaterOrder::query()->financePeriod($this->now->format('Y-m-d 00:00:00'), $this->now->format('Y-m-d H:i:s'))->financeSource($filters->source));
  53. $demo = $this->totals($this->period($filters)->financeSource('demo'));
  54. $pendingCommission = (int) WaterCommission::query()->where('status', 'pending')->sum('amount_cents');
  55. $days = $this->period($filters)->selectRaw('DATE(o.created_at) AS day,SUM(o.amount_cents) AS cents,COUNT(*) AS count')
  56. ->groupByRaw('DATE(o.created_at)')->get()->keyBy('day');
  57. $trend = [];
  58. for ($day = $filters->start;$day->format('Y-m-d') <= $filters->end->format('Y-m-d');$day = $day->modify('+1 day')) {
  59. $key = $day->format('Y-m-d');
  60. $row = $days->get($key);
  61. $trend[] = ['day' => $key, 'receipt_cents' => (int) ($row->cents ?? 0), 'count' => (int) ($row->count ?? 0)];
  62. }
  63. $list = $this->rows($filters)->pageResult($filters->page, $filters->limit);
  64. $sources = ReceiptSources::label($list['data']);
  65. $list['data'] = array_map(fn ($row) => $this->formatRow($row, $sources), $list['data']);
  66. return ['list' => $list, 'summary' => ['today_cents' => $today['cents'], 'today_count' => $today['count'],
  67. 'month_cents' => $monthly['cents'], 'month_count' => $monthly['count'], 'demo_cents' => $demo['cents'], 'demo_count' => $demo['count'],
  68. 'today_refund_cents' => null, 'month_refund_cents' => null, 'pending_withdrawal_cents' => null,
  69. 'pending_commission_cents' => $pendingCommission, 'net_rate' => null],
  70. 'trend' => $trend, 'withdrawals' => [],
  71. 'withdrawal_demo' => (new WithdrawalDemo())->listing($filters),
  72. 'capabilities' => ['refunds' => false, 'withdrawals' => false, 'commissions' => true,
  73. 'rewards' => \plugin\saiadmin\app\service\promotion\PromotionPolicy::current()['enabled']],
  74. 'period' => ['month' => $filters->month, 'start' => $filters->start->format('Y-m-d'), 'end' => $filters->end->format('Y-m-d')],
  75. 'updated_at' => $this->now->format('Y-m-d H:i:s')];
  76. });
  77. }
  78. public function export(array $input): string
  79. {
  80. $filters = new FinanceFilters($input, $this->now);
  81. $rows = $this->rows($filters)->limit(50001)->get()->toArray();
  82. if (count($rows) > 50000) {
  83. throw new ApiException('导出超过50000条,请缩小查询范围');
  84. }
  85. $sources = ReceiptSources::label($rows);
  86. $path = tempnam(sys_get_temp_dir(), 'water-finance-');
  87. if ($path === false) {
  88. throw new ApiException('无法创建对账文件');
  89. }
  90. $writer = new Writer();
  91. $opened = false;
  92. try {
  93. $writer->openToFile($path);
  94. $opened = true;
  95. $writer->addRow(Row::fromValues(['流水号', '类型', '企业客户', '收款凭据', '套餐', '权益桶数', '金额(元)', '状态', '登记时间', '数据标记']));
  96. foreach ($rows as $raw) {
  97. $r = $this->formatRow($raw, $sources);
  98. $writer->addRow(Row::fromValues([$r['number'], $r['type_label'], self::excelText($r['enterprise_name']),
  99. self::excelText($r['reference']), self::excelText($r['package_name']), $r['buckets'], $r['amount_cents'] / 100,
  100. $r['status_label'], $r['created_at'], $r['is_demo'] ? '测试数据,无实际收款' : '未标记测试']));
  101. }
  102. $writer->close();
  103. $opened = false;
  104. $content = file_get_contents($path);
  105. if ($content === false) {
  106. throw new ApiException('读取对账文件失败');
  107. }
  108. return $content;
  109. } finally {
  110. try {
  111. if ($opened) {
  112. $writer->close();
  113. }
  114. } finally {
  115. if (is_file($path)) {
  116. unlink($path);
  117. }
  118. }
  119. }
  120. }
  121. private static function excelText(?string $value): string
  122. {
  123. $text = $value ?? '';
  124. return preg_match('/^[=+@\-\t\r\n]/u', $text) ? "'" . $text : $text;
  125. }
  126. }