feat: 增强报表功能,支持新参数和导出类型
- 在 `ReportJobStoreRequest` 中新增 `draw_id`、`draw_no` 和 `normalized_number` 参数的验证规则。 - 更新 `AdminReportJobService` 以支持动态生成输出路径后缀,确保导出文件名包含相关信息。 - 在 `AdminReportQueryService` 中新增多个报表类型的处理逻辑,包括 `draw_profit_summary` 和 `sold_out_number_report`。 - 添加相应的测试用例,确保新功能的正确性和稳定性。
This commit is contained in:
@@ -25,9 +25,15 @@ final class ReportJobStoreRequest extends FormRequest
|
|||||||
'parameters.player_id' => ['sometimes', 'nullable', 'integer', 'min:1'],
|
'parameters.player_id' => ['sometimes', 'nullable', 'integer', 'min:1'],
|
||||||
'parameters.play_code' => ['sometimes', 'nullable', 'string', 'max:32'],
|
'parameters.play_code' => ['sometimes', 'nullable', 'string', 'max:32'],
|
||||||
'parameters.operator_id' => ['sometimes', 'nullable', 'integer', 'min:1'],
|
'parameters.operator_id' => ['sometimes', 'nullable', 'integer', 'min:1'],
|
||||||
|
'parameters.draw_id' => ['sometimes', 'nullable', 'integer', 'min:1'],
|
||||||
|
'parameters.draw_no' => ['sometimes', 'nullable', 'string', 'max:64'],
|
||||||
|
'parameters.normalized_number' => ['sometimes', 'nullable', 'regex:/^[0-9]{4}$/'],
|
||||||
'filter_json' => ['sometimes', 'array'],
|
'filter_json' => ['sometimes', 'array'],
|
||||||
'filter_json.date_from' => ['sometimes', 'nullable', 'date_format:Y-m-d'],
|
'filter_json.date_from' => ['sometimes', 'nullable', 'date_format:Y-m-d'],
|
||||||
'filter_json.date_to' => ['sometimes', 'nullable', 'date_format:Y-m-d', 'after_or_equal:filter_json.date_from'],
|
'filter_json.date_to' => ['sometimes', 'nullable', 'date_format:Y-m-d', 'after_or_equal:filter_json.date_from'],
|
||||||
|
'filter_json.draw_id' => ['sometimes', 'nullable', 'integer', 'min:1'],
|
||||||
|
'filter_json.draw_no' => ['sometimes', 'nullable', 'string', 'max:64'],
|
||||||
|
'filter_json.normalized_number' => ['sometimes', 'nullable', 'regex:/^[0-9]{4}$/'],
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
|||||||
/** 彩票钱包流水 {@see wallet_txns} */
|
/** 彩票钱包流水 {@see wallet_txns} */
|
||||||
final class WalletTxn extends Model
|
final class WalletTxn extends Model
|
||||||
{
|
{
|
||||||
|
protected $table = 'wallet_txns';
|
||||||
|
|
||||||
protected $fillable = [
|
protected $fillable = [
|
||||||
'txn_no',
|
'txn_no',
|
||||||
'player_id',
|
'player_id',
|
||||||
|
|||||||
@@ -32,6 +32,8 @@ final class AdminReportJobService
|
|||||||
$jobNo = 'RPT'.now()->format('YmdHis').strtoupper(Str::random(4));
|
$jobNo = 'RPT'.now()->format('YmdHis').strtoupper(Str::random(4));
|
||||||
$exportFormat = $exportFormat === 'xlsx' ? 'xlsx' : 'csv';
|
$exportFormat = $exportFormat === 'xlsx' ? 'xlsx' : 'csv';
|
||||||
|
|
||||||
|
$pathSuffix = $this->queryService->resolveOutputPathSuffix($reportType, $params, $dateFrom, $dateTo);
|
||||||
|
|
||||||
$job = ReportJob::query()->create([
|
$job = ReportJob::query()->create([
|
||||||
'job_no' => $jobNo,
|
'job_no' => $jobNo,
|
||||||
'admin_user_id' => (int) $admin->getKey(),
|
'admin_user_id' => (int) $admin->getKey(),
|
||||||
@@ -39,7 +41,7 @@ final class AdminReportJobService
|
|||||||
'export_format' => $exportFormat,
|
'export_format' => $exportFormat,
|
||||||
'filter_json' => $params,
|
'filter_json' => $params,
|
||||||
'status' => 'completed',
|
'status' => 'completed',
|
||||||
'output_path' => 'reports/'.$this->queryService->reportLabel($reportType).'_'.$dateFrom.'_'.$dateTo.'.'.$exportFormat,
|
'output_path' => 'reports/'.$this->queryService->reportLabel($reportType).'_'.$pathSuffix.'.'.$exportFormat,
|
||||||
'error_message' => null,
|
'error_message' => null,
|
||||||
'finished_at' => now(),
|
'finished_at' => now(),
|
||||||
]);
|
]);
|
||||||
|
|||||||
@@ -3,6 +3,14 @@
|
|||||||
namespace App\Services\Admin;
|
namespace App\Services\Admin;
|
||||||
|
|
||||||
use App\Models\AuditLog;
|
use App\Models\AuditLog;
|
||||||
|
use App\Models\Draw;
|
||||||
|
use App\Models\RiskPool;
|
||||||
|
use App\Models\RiskPoolLockLog;
|
||||||
|
use App\Models\SettlementBatch;
|
||||||
|
use App\Models\TicketItem;
|
||||||
|
use App\Models\TicketOrder;
|
||||||
|
use App\Models\TransferOrder;
|
||||||
|
use App\Models\WalletTxn;
|
||||||
use Carbon\Carbon;
|
use Carbon\Carbon;
|
||||||
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
|
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
|
||||||
use Illuminate\Pagination\LengthAwarePaginator as PaginatorInstance;
|
use Illuminate\Pagination\LengthAwarePaginator as PaginatorInstance;
|
||||||
@@ -351,11 +359,16 @@ final class AdminReportQueryService
|
|||||||
$dateTo = $range['date_to'];
|
$dateTo = $range['date_to'];
|
||||||
|
|
||||||
return match ($reportType) {
|
return match ($reportType) {
|
||||||
|
'draw_profit_summary' => $this->drawProfitExportRows($filterJson),
|
||||||
'daily_profit_summary' => $this->dailyProfitExportRows($dateFrom, $dateTo),
|
'daily_profit_summary' => $this->dailyProfitExportRows($dateFrom, $dateTo),
|
||||||
'player_win_loss' => $this->playerWinLossExportRows($filterJson, $dateFrom, $dateTo),
|
'player_win_loss' => $this->playerWinLossExportRows($filterJson, $dateFrom, $dateTo),
|
||||||
'play_dimension_report' => $this->playDimensionExportRows($filterJson, $dateFrom, $dateTo),
|
'play_dimension_report' => $this->playDimensionExportRows($filterJson, $dateFrom, $dateTo),
|
||||||
'rebate_commission_report' => $this->rebateCommissionExportRows($filterJson, $dateFrom, $dateTo),
|
'rebate_commission_report' => $this->rebateCommissionExportRows($filterJson, $dateFrom, $dateTo),
|
||||||
'audit_operation_report' => $this->auditExportRows($filterJson, $dateFrom, $dateTo),
|
'audit_operation_report' => $this->auditExportRows($filterJson, $dateFrom, $dateTo),
|
||||||
|
'wallet_transfer_report', 'transfer_orders_daily' => $this->transferOrdersExportRows($filterJson, $dateFrom, $dateTo),
|
||||||
|
'wallet_txns_daily' => $this->walletTxnsExportRows($filterJson, $dateFrom, $dateTo),
|
||||||
|
'hot_number_risk_report' => $this->hotNumberRiskExportRows($filterJson),
|
||||||
|
'sold_out_number_report' => $this->soldOutNumberExportRows($filterJson),
|
||||||
default => [
|
default => [
|
||||||
['报表类型', '开始日期', '结束日期'],
|
['报表类型', '开始日期', '结束日期'],
|
||||||
[$this->reportLabel($reportType), $dateFrom, $dateTo],
|
[$this->reportLabel($reportType), $dateFrom, $dateTo],
|
||||||
@@ -363,6 +376,24 @@ final class AdminReportQueryService
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function resolveOutputPathSuffix(string $reportType, ?array $filterJson, string $dateFrom, string $dateTo): string
|
||||||
|
{
|
||||||
|
if (in_array($reportType, ['draw_profit_summary', 'hot_number_risk_report', 'sold_out_number_report'], true)) {
|
||||||
|
$draw = $this->resolveDrawForReport($filterJson);
|
||||||
|
if ($draw !== null) {
|
||||||
|
$suffix = (string) $draw->draw_no;
|
||||||
|
$number = $this->normalizedNumberFromFilters($filterJson);
|
||||||
|
if ($reportType === 'hot_number_risk_report' && $number !== null) {
|
||||||
|
return $suffix.'_'.$number;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $suffix;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $dateFrom.'_'.$dateTo;
|
||||||
|
}
|
||||||
|
|
||||||
public function reportLabel(string $reportType): string
|
public function reportLabel(string $reportType): string
|
||||||
{
|
{
|
||||||
return match ($reportType) {
|
return match ($reportType) {
|
||||||
@@ -566,6 +597,359 @@ final class AdminReportQueryService
|
|||||||
return $query;
|
return $query;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return list<array<int, string|int|float|null>>
|
||||||
|
*/
|
||||||
|
private function drawProfitExportRows(?array $filterJson): array
|
||||||
|
{
|
||||||
|
$draw = $this->resolveDrawForReport($filterJson);
|
||||||
|
if ($draw === null) {
|
||||||
|
return [['提示', '请提供 draw_id 或 draw_no']];
|
||||||
|
}
|
||||||
|
|
||||||
|
$drawId = (int) $draw->id;
|
||||||
|
$totalBetMinor = (int) TicketOrder::query()->where('draw_id', $drawId)->sum('total_actual_deduct');
|
||||||
|
$orderCount = (int) TicketOrder::query()->where('draw_id', $drawId)->count();
|
||||||
|
$itemCount = (int) TicketItem::query()->where('draw_id', $drawId)->count();
|
||||||
|
$currencyCode = (string) (TicketOrder::query()->where('draw_id', $drawId)->value('currency_code') ?? '');
|
||||||
|
$totalWinMinor = (int) TicketItem::query()->where('draw_id', $drawId)->sum('win_amount');
|
||||||
|
$totalJackpotWinMinor = (int) TicketItem::query()->where('draw_id', $drawId)->sum('jackpot_win_amount');
|
||||||
|
$totalPayoutMinor = $totalWinMinor + $totalJackpotWinMinor;
|
||||||
|
$approxHouseGrossMinor = $totalBetMinor - $totalPayoutMinor;
|
||||||
|
|
||||||
|
$rows = [
|
||||||
|
[
|
||||||
|
'行类型', '期号', '状态', '币种', '订单数', '注单数', '下注', '派彩', '平台盈亏',
|
||||||
|
'结算批次ID', '结算状态', '批次数', '中奖数', '批次派彩', '批次Jackpot', '完成时间',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'summary',
|
||||||
|
$draw->draw_no,
|
||||||
|
$draw->status,
|
||||||
|
$currencyCode !== '' ? $currencyCode : null,
|
||||||
|
$orderCount,
|
||||||
|
$itemCount,
|
||||||
|
$totalBetMinor,
|
||||||
|
$totalPayoutMinor,
|
||||||
|
$approxHouseGrossMinor,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
$batches = SettlementBatch::query()
|
||||||
|
->where('draw_id', $drawId)
|
||||||
|
->orderByDesc('id')
|
||||||
|
->limit(100)
|
||||||
|
->get();
|
||||||
|
|
||||||
|
foreach ($batches as $batch) {
|
||||||
|
$rows[] = [
|
||||||
|
'settlement_batch',
|
||||||
|
$draw->draw_no,
|
||||||
|
$draw->status,
|
||||||
|
$currencyCode !== '' ? $currencyCode : null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
(int) $batch->id,
|
||||||
|
$batch->status,
|
||||||
|
(int) $batch->total_ticket_count,
|
||||||
|
(int) $batch->total_win_count,
|
||||||
|
(int) $batch->total_payout_amount,
|
||||||
|
(int) $batch->total_jackpot_payout_amount,
|
||||||
|
$batch->finished_at?->toIso8601String(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $rows;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return list<array<int, string|int|float|null>>
|
||||||
|
*/
|
||||||
|
private function soldOutNumberExportRows(?array $filterJson): array
|
||||||
|
{
|
||||||
|
$draw = $this->resolveDrawForReport($filterJson);
|
||||||
|
if ($draw === null) {
|
||||||
|
return [['提示', '请提供 draw_id 或 draw_no']];
|
||||||
|
}
|
||||||
|
|
||||||
|
$rows = [
|
||||||
|
['期号', '号码', '封顶', '已占用', '剩余', '售罄', '使用率%'],
|
||||||
|
];
|
||||||
|
|
||||||
|
$pools = RiskPool::query()
|
||||||
|
->where('draw_id', $draw->id)
|
||||||
|
->where('sold_out_status', 1)
|
||||||
|
->orderBy('normalized_number')
|
||||||
|
->limit(10000)
|
||||||
|
->get();
|
||||||
|
|
||||||
|
foreach ($pools as $pool) {
|
||||||
|
$cap = (int) $pool->total_cap_amount;
|
||||||
|
$locked = (int) $pool->locked_amount;
|
||||||
|
$rows[] = [
|
||||||
|
$draw->draw_no,
|
||||||
|
$pool->normalized_number,
|
||||||
|
$cap,
|
||||||
|
$locked,
|
||||||
|
(int) $pool->remaining_amount,
|
||||||
|
'是',
|
||||||
|
$this->riskUsageRatioPercent($cap, $locked),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $rows;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return list<array<int, string|int|float|null>>
|
||||||
|
*/
|
||||||
|
private function hotNumberRiskExportRows(?array $filterJson): array
|
||||||
|
{
|
||||||
|
$draw = $this->resolveDrawForReport($filterJson);
|
||||||
|
if ($draw === null) {
|
||||||
|
return [['提示', '请提供 draw_id 或 draw_no']];
|
||||||
|
}
|
||||||
|
|
||||||
|
$number = $this->normalizedNumberFromFilters($filterJson);
|
||||||
|
$rows = [
|
||||||
|
[
|
||||||
|
'行类型', '期号', '号码', '封顶', '已占用', '剩余', '售罄', '使用率%',
|
||||||
|
'日志ID', '动作', '金额', '玩法', '注单号', '玩家ID', '原因', '时间',
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
if ($number !== null) {
|
||||||
|
$pool = RiskPool::query()
|
||||||
|
->where('draw_id', $draw->id)
|
||||||
|
->where('normalized_number', $number)
|
||||||
|
->first();
|
||||||
|
|
||||||
|
if ($pool === null) {
|
||||||
|
$rows[] = ['pool', $draw->draw_no, $number, null, null, null, null, null, null, null, null, null, null, null, null, null];
|
||||||
|
|
||||||
|
return $rows;
|
||||||
|
}
|
||||||
|
|
||||||
|
$cap = (int) $pool->total_cap_amount;
|
||||||
|
$locked = (int) $pool->locked_amount;
|
||||||
|
$rows[] = [
|
||||||
|
'pool',
|
||||||
|
$draw->draw_no,
|
||||||
|
$pool->normalized_number,
|
||||||
|
$cap,
|
||||||
|
$locked,
|
||||||
|
(int) $pool->remaining_amount,
|
||||||
|
(int) $pool->sold_out_status === 1 ? '是' : '否',
|
||||||
|
$this->riskUsageRatioPercent($cap, $locked),
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
];
|
||||||
|
|
||||||
|
$logs = RiskPoolLockLog::query()
|
||||||
|
->where('draw_id', $draw->id)
|
||||||
|
->where('normalized_number', $number)
|
||||||
|
->with(['ticketItem:id,ticket_no,play_code,player_id'])
|
||||||
|
->orderByDesc('id')
|
||||||
|
->limit(5000)
|
||||||
|
->get();
|
||||||
|
|
||||||
|
foreach ($logs as $log) {
|
||||||
|
$rows[] = [
|
||||||
|
'lock_log',
|
||||||
|
$draw->draw_no,
|
||||||
|
$number,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
(int) $log->id,
|
||||||
|
$log->action_type,
|
||||||
|
(int) $log->amount,
|
||||||
|
$log->ticketItem?->play_code,
|
||||||
|
$log->ticketItem?->ticket_no,
|
||||||
|
$log->ticketItem?->player_id,
|
||||||
|
$log->source_reason,
|
||||||
|
$log->created_at?->toIso8601String(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $rows;
|
||||||
|
}
|
||||||
|
|
||||||
|
$pools = RiskPool::query()
|
||||||
|
->where('draw_id', $draw->id)
|
||||||
|
->orderByDesc('locked_amount')
|
||||||
|
->limit(10000)
|
||||||
|
->get();
|
||||||
|
|
||||||
|
foreach ($pools as $pool) {
|
||||||
|
$cap = (int) $pool->total_cap_amount;
|
||||||
|
$locked = (int) $pool->locked_amount;
|
||||||
|
$rows[] = [
|
||||||
|
'pool',
|
||||||
|
$draw->draw_no,
|
||||||
|
$pool->normalized_number,
|
||||||
|
$cap,
|
||||||
|
$locked,
|
||||||
|
(int) $pool->remaining_amount,
|
||||||
|
(int) $pool->sold_out_status === 1 ? '是' : '否',
|
||||||
|
$this->riskUsageRatioPercent($cap, $locked),
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $rows;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return list<array<int, string|int|float|null>>
|
||||||
|
*/
|
||||||
|
private function transferOrdersExportRows(?array $filterJson, string $dateFrom, string $dateTo): array
|
||||||
|
{
|
||||||
|
$rows = [
|
||||||
|
['转账单号', '玩家ID', '用户名', '昵称', '方向', '币种', '金额', '状态', '外部单号', '失败原因', '创建时间', '完成时间'],
|
||||||
|
];
|
||||||
|
|
||||||
|
$query = TransferOrder::query()
|
||||||
|
->with(['player:id,username,nickname'])
|
||||||
|
->orderByDesc('id');
|
||||||
|
|
||||||
|
$playerId = isset($filterJson['player_id']) ? (int) $filterJson['player_id'] : null;
|
||||||
|
if ($playerId !== null && $playerId > 0) {
|
||||||
|
$query->where('player_id', $playerId);
|
||||||
|
}
|
||||||
|
|
||||||
|
$query->where('created_at', '>=', $dateFrom.' 00:00:00')
|
||||||
|
->where('created_at', '<=', $dateTo.' 23:59:59');
|
||||||
|
|
||||||
|
foreach ($query->limit(10000)->get() as $order) {
|
||||||
|
$player = $order->player;
|
||||||
|
$rows[] = [
|
||||||
|
$order->transfer_no,
|
||||||
|
(int) $order->player_id,
|
||||||
|
$player?->username,
|
||||||
|
$player?->nickname,
|
||||||
|
$order->direction,
|
||||||
|
$order->currency_code,
|
||||||
|
(int) $order->amount,
|
||||||
|
$order->status,
|
||||||
|
$order->external_ref_no,
|
||||||
|
$order->fail_reason,
|
||||||
|
$order->created_at?->toIso8601String(),
|
||||||
|
$order->finished_at?->toIso8601String(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $rows;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return list<array<int, string|int|float|null>>
|
||||||
|
*/
|
||||||
|
private function walletTxnsExportRows(?array $filterJson, string $dateFrom, string $dateTo): array
|
||||||
|
{
|
||||||
|
$rows = [
|
||||||
|
['流水号', '玩家ID', '用户名', '业务类型', '业务单号', '方向', '金额', '变动前余额', '变动后余额', '状态', '外部单号', '备注', '创建时间'],
|
||||||
|
];
|
||||||
|
|
||||||
|
$query = WalletTxn::query()
|
||||||
|
->with(['player:id,username'])
|
||||||
|
->orderByDesc('id');
|
||||||
|
|
||||||
|
$playerId = isset($filterJson['player_id']) ? (int) $filterJson['player_id'] : null;
|
||||||
|
if ($playerId !== null && $playerId > 0) {
|
||||||
|
$query->where('player_id', $playerId);
|
||||||
|
}
|
||||||
|
|
||||||
|
$query->where('created_at', '>=', $dateFrom.' 00:00:00')
|
||||||
|
->where('created_at', '<=', $dateTo.' 23:59:59');
|
||||||
|
|
||||||
|
foreach ($query->limit(10000)->get() as $txn) {
|
||||||
|
$rows[] = [
|
||||||
|
$txn->txn_no,
|
||||||
|
(int) $txn->player_id,
|
||||||
|
$txn->player?->username,
|
||||||
|
$txn->biz_type,
|
||||||
|
$txn->biz_no,
|
||||||
|
(int) $txn->direction,
|
||||||
|
(int) $txn->amount,
|
||||||
|
(int) $txn->balance_before,
|
||||||
|
(int) $txn->balance_after,
|
||||||
|
$txn->status,
|
||||||
|
$txn->external_ref_no,
|
||||||
|
$txn->remark,
|
||||||
|
$txn->created_at?->toIso8601String(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $rows;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function resolveDrawForReport(?array $filterJson): ?Draw
|
||||||
|
{
|
||||||
|
if (! is_array($filterJson)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! empty($filterJson['draw_id'])) {
|
||||||
|
return Draw::query()->find((int) $filterJson['draw_id']);
|
||||||
|
}
|
||||||
|
|
||||||
|
$drawNo = trim((string) ($filterJson['draw_no'] ?? ''));
|
||||||
|
if ($drawNo !== '') {
|
||||||
|
return Draw::query()->where('draw_no', $drawNo)->first();
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function normalizedNumberFromFilters(?array $filterJson): ?string
|
||||||
|
{
|
||||||
|
if (! is_array($filterJson)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$number = trim((string) ($filterJson['normalized_number'] ?? ''));
|
||||||
|
if (preg_match('/^[0-9]{4}$/', $number) === 1) {
|
||||||
|
return $number;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function riskUsageRatioPercent(int $cap, int $locked): ?string
|
||||||
|
{
|
||||||
|
if ($cap <= 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (string) round($locked / $cap * 100, 2);
|
||||||
|
}
|
||||||
|
|
||||||
private function formatBusinessDateValue(mixed $value): ?string
|
private function formatBusinessDateValue(mixed $value): ?string
|
||||||
{
|
{
|
||||||
if ($value === null) {
|
if ($value === null) {
|
||||||
|
|||||||
@@ -3,17 +3,25 @@
|
|||||||
use App\Models\AuditLog;
|
use App\Models\AuditLog;
|
||||||
use App\Models\AdminRole;
|
use App\Models\AdminRole;
|
||||||
use App\Models\AdminUser;
|
use App\Models\AdminUser;
|
||||||
|
use App\Models\Draw;
|
||||||
use App\Models\ReportJob;
|
use App\Models\ReportJob;
|
||||||
|
use App\Models\RiskPool;
|
||||||
use App\Lottery\ErrorCode;
|
use App\Lottery\ErrorCode;
|
||||||
use App\Models\ReconcileJob;
|
use App\Models\ReconcileJob;
|
||||||
use App\Services\AuditLogger;
|
use App\Services\AuditLogger;
|
||||||
use Illuminate\Support\Facades\DB;
|
use Illuminate\Support\Facades\DB;
|
||||||
use Illuminate\Support\Facades\Hash;
|
use Illuminate\Support\Facades\Hash;
|
||||||
use App\Support\AdminPermissionBridge;
|
use App\Support\AdminPermissionBridge;
|
||||||
|
use Database\Seeders\AdminRbacAndUserSeeder;
|
||||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
|
|
||||||
uses(RefreshDatabase::class);
|
uses(RefreshDatabase::class);
|
||||||
|
|
||||||
|
beforeEach(function (): void {
|
||||||
|
$this->seed(AdminRbacAndUserSeeder::class);
|
||||||
|
$this->artisan('lottery:admin-auth-sync')->assertExitCode(0);
|
||||||
|
});
|
||||||
|
|
||||||
function phase15SuperToken(): string
|
function phase15SuperToken(): string
|
||||||
{
|
{
|
||||||
$admin = AdminUser::query()->create([
|
$admin = AdminUser::query()->create([
|
||||||
@@ -124,6 +132,67 @@ test('report jobs support xlsx export filename convention', function (): void {
|
|||||||
->assertHeader('content-type', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet');
|
->assertHeader('content-type', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('report jobs export draw profit and sold out reports by draw_no', function (): void {
|
||||||
|
$token = phase15SuperToken();
|
||||||
|
|
||||||
|
$draw = Draw::query()->create([
|
||||||
|
'draw_no' => 'RPT-DRAW-001',
|
||||||
|
'business_date' => '2026-05-20',
|
||||||
|
'sequence_no' => 1,
|
||||||
|
'status' => 'settled',
|
||||||
|
'start_time' => now()->subDay(),
|
||||||
|
'close_time' => now()->subDay(),
|
||||||
|
'draw_time' => now()->subDay(),
|
||||||
|
'cooling_end_time' => null,
|
||||||
|
'result_source' => null,
|
||||||
|
'current_result_version' => 1,
|
||||||
|
'settle_version' => 1,
|
||||||
|
'is_reopened' => false,
|
||||||
|
]);
|
||||||
|
|
||||||
|
RiskPool::query()->create([
|
||||||
|
'draw_id' => $draw->id,
|
||||||
|
'normalized_number' => '1234',
|
||||||
|
'total_cap_amount' => 10000,
|
||||||
|
'locked_amount' => 10000,
|
||||||
|
'remaining_amount' => 0,
|
||||||
|
'sold_out_status' => 1,
|
||||||
|
'version' => 1,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$profit = $this->withHeader('Authorization', 'Bearer '.$token)
|
||||||
|
->postJson('/api/v1/admin/report-jobs', [
|
||||||
|
'report_type' => 'draw_profit_summary',
|
||||||
|
'export_format' => 'csv',
|
||||||
|
'parameters' => ['draw_no' => 'RPT-DRAW-001'],
|
||||||
|
]);
|
||||||
|
$profit->assertOk()->assertJsonPath('code', ErrorCode::Success->value);
|
||||||
|
$profitId = (int) $profit->json('data.id');
|
||||||
|
expect(ReportJob::query()->find($profitId)?->output_path)->toContain('RPT-DRAW-001');
|
||||||
|
|
||||||
|
$profitCsv = $this->withHeader('Authorization', 'Bearer '.$token)
|
||||||
|
->get('/api/v1/admin/report-jobs/'.$profitId.'/download')
|
||||||
|
->assertOk()
|
||||||
|
->streamedContent();
|
||||||
|
expect($profitCsv)->toContain('summary')
|
||||||
|
->and($profitCsv)->toContain('RPT-DRAW-001');
|
||||||
|
|
||||||
|
$soldOut = $this->withHeader('Authorization', 'Bearer '.$token)
|
||||||
|
->postJson('/api/v1/admin/report-jobs', [
|
||||||
|
'report_type' => 'sold_out_number_report',
|
||||||
|
'export_format' => 'csv',
|
||||||
|
'parameters' => ['draw_id' => $draw->id],
|
||||||
|
]);
|
||||||
|
$soldOut->assertOk();
|
||||||
|
$soldOutId = (int) $soldOut->json('data.id');
|
||||||
|
$soldOutCsv = $this->withHeader('Authorization', 'Bearer '.$token)
|
||||||
|
->get('/api/v1/admin/report-jobs/'.$soldOutId.'/download')
|
||||||
|
->assertOk()
|
||||||
|
->streamedContent();
|
||||||
|
expect($soldOutCsv)->toContain('1234')
|
||||||
|
->and($soldOutCsv)->toContain('是');
|
||||||
|
});
|
||||||
|
|
||||||
test('reconcile job create with items and nested items index', function (): void {
|
test('reconcile job create with items and nested items index', function (): void {
|
||||||
$token = phase15SuperToken();
|
$token = phase15SuperToken();
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user