206 lines
6.1 KiB
PHP
206 lines
6.1 KiB
PHP
<?php
|
||
|
||
declare(strict_types=1);
|
||
|
||
namespace app\common\library;
|
||
|
||
use app\common\model\MallDailyPush;
|
||
use support\Response;
|
||
use Throwable;
|
||
|
||
/**
|
||
* 每日推送数据导出(流式 CSV,兼容 Excel 打开)
|
||
*/
|
||
class MallDailyPushExport
|
||
{
|
||
public const MAX_EXPORT_LIMIT = 100000;
|
||
|
||
public const CHUNK_SIZE = 2000;
|
||
|
||
/**
|
||
* @var array<string, string>
|
||
*/
|
||
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<string, string>
|
||
*/
|
||
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<int, string> $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<int, string> $fields
|
||
* @return array<int, string>
|
||
*/
|
||
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<int, string>
|
||
*/
|
||
public function getDefaultFields(): array
|
||
{
|
||
return array_keys($this->fieldLabels);
|
||
}
|
||
|
||
public function getMaxExportLimit(): int
|
||
{
|
||
return self::MAX_EXPORT_LIMIT;
|
||
}
|
||
|
||
/**
|
||
* @param array<int, string> $fields
|
||
* @param array<string, string> $labels
|
||
* @return array<int, string>
|
||
*/
|
||
private function buildHeaderRow(array $fields, array $labels): array
|
||
{
|
||
$header = [];
|
||
foreach ($fields as $field) {
|
||
$header[] = $labels[$field] ?? $field;
|
||
}
|
||
return $header;
|
||
}
|
||
|
||
/**
|
||
* @param array<string, mixed> $row
|
||
* @param array<int, string> $fields
|
||
* @return array<int, string>
|
||
*/
|
||
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<string, string>
|
||
*/
|
||
private function resolveFieldLabels(string $lang): array
|
||
{
|
||
$lang = strtolower(str_replace('_', '-', $lang));
|
||
return $lang === 'en' ? $this->fieldLabelsEn : $this->fieldLabels;
|
||
}
|
||
}
|