diff --git a/app/common/library/MallDailyPushExport.php b/app/common/library/MallDailyPushExport.php new file mode 100644 index 0000000..785a10d --- /dev/null +++ b/app/common/library/MallDailyPushExport.php @@ -0,0 +1,205 @@ + + */ + private array $fieldLabels = [ + 'id' => 'ID', + 'user_id' => 'playX-ID', + 'date' => '业务日期', + 'username' => '用户名', + 'yesterday_win_loss_net' => '昨日净输赢', + 'yesterday_total_deposit' => '昨日总充值', + 'lifetime_total_deposit' => '历史总充值', + 'lifetime_total_withdraw' => '历史总提现', + 'create_time' => '创建时间', + ]; + + /** + * @var array + */ + private array $fieldLabelsEn = [ + 'id' => 'ID', + 'user_id' => 'PlayX user ID', + 'date' => 'Business date', + 'username' => 'Username', + 'yesterday_win_loss_net' => 'Yesterday net win/loss', + 'yesterday_total_deposit' => 'Yesterday total deposit', + 'lifetime_total_deposit' => 'Lifetime total deposit', + 'lifetime_total_withdraw' => 'Lifetime total withdraw', + 'create_time' => 'Created at', + ]; + + /** + * @param array $fields + */ + public function export( + MallDailyPush $model, + array $where, + array $alias, + array $order, + array $fields, + int $limit, + string $lang = 'zh-cn' + ): Response { + $fields = $this->normalizeFields($fields); + if ($fields === []) { + throw new \InvalidArgumentException(__('Parameter error')); + } + + $limit = max(1, min(self::MAX_EXPORT_LIMIT, $limit)); + + $exportDir = runtime_path('export'); + if (!is_dir($exportDir) && !mkdir($exportDir, 0755, true) && !is_dir($exportDir)) { + throw new \RuntimeException('Failed to create export directory'); + } + + $filepath = $exportDir . DIRECTORY_SEPARATOR . 'daily_push_' . date('YmdHis') . '_' . bin2hex(random_bytes(4)) . '.csv'; + $labels = $this->resolveFieldLabels($lang); + $handle = fopen($filepath, 'wb'); + if ($handle === false) { + throw new \RuntimeException('Failed to create export file'); + } + + try { + fwrite($handle, "\xEF\xBB\xBF"); + fputcsv($handle, $this->buildHeaderRow($fields, $labels)); + + $exported = 0; + $page = 1; + while ($exported < $limit) { + $batchSize = min(self::CHUNK_SIZE, $limit - $exported); + $rows = $model + ->field($fields) + ->alias($alias) + ->where($where) + ->order($order) + ->page($page, $batchSize) + ->select(); + + if ($rows->isEmpty()) { + break; + } + + foreach ($rows as $row) { + fputcsv($handle, $this->formatRow($row->toArray(), $fields)); + $exported++; + if ($exported >= $limit) { + break; + } + } + + if ($rows->count() < $batchSize) { + break; + } + $page++; + } + } catch (Throwable $e) { + fclose($handle); + if (is_file($filepath)) { + @unlink($filepath); + } + throw $e; + } + + fclose($handle); + register_shutdown_function(static function () use ($filepath): void { + if (is_file($filepath)) { + @unlink($filepath); + } + }); + + $filename = 'daily_push_' . date('YmdHis') . '.csv'; + return (new Response())->file($filepath, $filename); + } + + /** + * @param array $fields + * @return array + */ + public function normalizeFields(array $fields): array + { + $allowed = array_keys($this->fieldLabels); + $normalized = []; + foreach ($fields as $field) { + $field = trim(strval($field)); + if ($field !== '' && in_array($field, $allowed, true) && !in_array($field, $normalized, true)) { + $normalized[] = $field; + } + } + return $normalized; + } + + /** + * @return array + */ + public function getDefaultFields(): array + { + return array_keys($this->fieldLabels); + } + + public function getMaxExportLimit(): int + { + return self::MAX_EXPORT_LIMIT; + } + + /** + * @param array $fields + * @param array $labels + * @return array + */ + private function buildHeaderRow(array $fields, array $labels): array + { + $header = []; + foreach ($fields as $field) { + $header[] = $labels[$field] ?? $field; + } + return $header; + } + + /** + * @param array $row + * @param array $fields + * @return array + */ + private function formatRow(array $row, array $fields): array + { + $line = []; + foreach ($fields as $field) { + $value = $row[$field] ?? ''; + if ($field === 'create_time' && $value !== '' && $value !== null) { + $timestamp = is_numeric($value) ? intval($value) : strtotime(strval($value)); + $line[] = $timestamp > 0 ? date('Y-m-d H:i:s', $timestamp) : ''; + continue; + } + $line[] = $value === null ? '' : strval($value); + } + return $line; + } + + /** + * @return array + */ + private function resolveFieldLabels(string $lang): array + { + $lang = strtolower(str_replace('_', '-', $lang)); + return $lang === 'en' ? $this->fieldLabelsEn : $this->fieldLabels; + } +} diff --git a/web/src/views/backend/mall/dailyPush/exportDialog.vue b/web/src/views/backend/mall/dailyPush/exportDialog.vue new file mode 100644 index 0000000..6ea6bd0 --- /dev/null +++ b/web/src/views/backend/mall/dailyPush/exportDialog.vue @@ -0,0 +1,263 @@ + + + + +