fix(settlement): 操作记录分页、流水动作与坏账幂等加固
Some checks failed
lotterLaravel CI / test (push) Has been cancelled
lotterLaravel E2E / e2e-api (push) Has been cancelled

- 新增 settlement-operations 合并列表 API,收付/调账接口改为标准分页
- actionable_only 流水在内存过滤后正确分页
- 已结账单流水不再展示补差/冲正快捷动作
- 坏账核销支持 idempotency_key 并写入 result_bill_id
- 补充操作记录、流水与坏账幂等相关测试
This commit is contained in:
2026-06-26 16:40:46 +08:00
parent 7ec8f4c5a2
commit c83343e989
20 changed files with 1045 additions and 212 deletions

View File

@@ -2,14 +2,16 @@
namespace App\Services\AgentSettlement;
use Carbon\Carbon;
use App\Models\AdminUser;
use App\Support\LimitedQuery;
use App\Support\CurrencyFormatter;
use App\Support\PlayerFundingMode;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;
use Illuminate\Database\Query\Builder;
use App\Support\AdminAgentSettlementScope;
use App\Support\AgentSettlementPeriodWindow;
use App\Support\CurrencyFormatter;
use App\Support\LimitedQuery;
use App\Support\PlayerFundingMode;
use Carbon\Carbon;
use Illuminate\Support\Facades\DB;
/** 结算中心统一账务流水credit_ledger + 收付 + 调账)。 */
final class SettlementCenterLedgerService
@@ -36,6 +38,7 @@ final class SettlementCenterLedgerService
private readonly SettlementPartyEnrichment $partyEnrichment,
private readonly CreditLedgerBetFlowPresenter $betFlowPresenter,
) {}
/**
* @return array{
* items: list<array<string, mixed>>,
@@ -95,6 +98,19 @@ final class SettlementCenterLedgerService
];
}
if ($filters->actionableOnly) {
return $this->listUnifiedActionableFiltered(
$admin,
$siteCode,
$page,
$perPage,
$filters,
$stubQueries,
$playerBills,
$billsTruncated,
);
}
$offset = max(0, ($page - 1) * $perPage);
if (count($stubQueries) === 1) {
$base = $stubQueries[0];
@@ -122,11 +138,6 @@ final class SettlementCenterLedgerService
$items = $this->hydrateLedgerStubs($admin, $siteCode, $stubs, $playerBills);
$items = $this->applyFilters($items, $filters);
$filteredCount = count($items);
if ($filteredCount !== count($stubs)) {
$total = $filteredCount;
}
return [
'items' => array_values($items),
@@ -248,7 +259,7 @@ final class SettlementCenterLedgerService
string $siteCode,
?array $range,
SettlementLedgerListFilters $filters,
): ?\Illuminate\Database\Query\Builder {
): ?Builder {
$query = DB::table('share_ledger as sl')
->join('players as p', 'p.id', '=', 'sl.player_id')
->where('p.site_code', $siteCode)
@@ -275,7 +286,7 @@ final class SettlementCenterLedgerService
string $siteCode,
?array $range,
SettlementLedgerListFilters $filters,
): ?\Illuminate\Database\Query\Builder {
): ?Builder {
$query = DB::table('credit_ledger as cl')
->join('players as p', function ($join): void {
$join->on('p.id', '=', 'cl.owner_id')
@@ -313,7 +324,7 @@ final class SettlementCenterLedgerService
string $siteCode,
?int $periodId,
SettlementLedgerListFilters $filters,
): ?\Illuminate\Database\Query\Builder {
): ?Builder {
$adminSiteId = (int) DB::table('admin_sites')->where('code', $siteCode)->value('id');
if ($adminSiteId <= 0) {
return null;
@@ -347,7 +358,7 @@ final class SettlementCenterLedgerService
}
private function applyPaymentPlayerFilters(
\Illuminate\Database\Query\Builder $query,
Builder $query,
SettlementLedgerListFilters $filters,
): void {
if ($filters->playerId !== null && $filters->playerId > 0) {
@@ -366,7 +377,7 @@ final class SettlementCenterLedgerService
string $siteCode,
?int $periodId,
SettlementLedgerListFilters $filters,
): ?\Illuminate\Database\Query\Builder {
): ?Builder {
$adminSiteId = (int) DB::table('admin_sites')->where('code', $siteCode)->value('id');
if ($adminSiteId <= 0) {
return null;
@@ -409,14 +420,14 @@ final class SettlementCenterLedgerService
}
private function applyAdjustmentPlayerScope(
\Illuminate\Database\Query\Builder $query,
Builder $query,
AdminUser $admin,
string $siteCode,
SettlementLedgerListFilters $filters,
): void {
$query->where(function (\Illuminate\Database\Query\Builder $outer) use ($admin, $siteCode, $filters): void {
$query->where(function (Builder $outer) use ($admin, $siteCode, $filters): void {
$outer->whereNull('p.id')
->orWhere(function (\Illuminate\Database\Query\Builder $scoped) use ($admin, $siteCode, $filters): void {
->orWhere(function (Builder $scoped) use ($admin, $siteCode, $filters): void {
$scoped->where('p.site_code', $siteCode);
AdminAgentSettlementScope::applyDirectPlayersToAlias($scoped, $admin, 'p');
$this->applyLedgerPlayerFilters($scoped, 'p', $filters);
@@ -425,7 +436,7 @@ final class SettlementCenterLedgerService
}
private function applyTxnNoStubFilter(
\Illuminate\Database\Query\Builder $query,
Builder $query,
string $idColumn,
string $prefix,
?string $txnNo,
@@ -435,7 +446,7 @@ final class SettlementCenterLedgerService
}
$needle = strtolower(trim($txnNo));
$query->where(function (\Illuminate\Database\Query\Builder $match) use ($idColumn, $prefix, $needle): void {
$query->where(function (Builder $match) use ($idColumn, $prefix, $needle): void {
if (ctype_digit($needle)) {
$match->where($idColumn, (int) $needle);
}
@@ -447,7 +458,7 @@ final class SettlementCenterLedgerService
});
}
private function applyLedgerSiteScope(\Illuminate\Database\Query\Builder $query, AdminUser $admin, string $periodsAlias): void
private function applyLedgerSiteScope(Builder $query, AdminUser $admin, string $periodsAlias): void
{
$siteIds = $admin->accessibleAdminSiteIds();
if ($siteIds === null) {
@@ -464,7 +475,7 @@ final class SettlementCenterLedgerService
}
private function applyLedgerPlayerFilters(
\Illuminate\Database\Query\Builder $query,
Builder $query,
string $playerAlias,
SettlementLedgerListFilters $filters,
): void {
@@ -474,7 +485,7 @@ final class SettlementCenterLedgerService
if ($filters->playerAccount !== null && $filters->playerAccount !== '') {
$like = '%'.addcslashes($filters->playerAccount, '%_\\').'%';
$query->where(function (\Illuminate\Database\Query\Builder $match) use ($playerAlias, $like): void {
$query->where(function (Builder $match) use ($playerAlias, $like): void {
$match->where("{$playerAlias}.username", 'like', $like)
->orWhere("{$playerAlias}.site_player_id", 'like', $like)
->orWhere("{$playerAlias}.nickname", 'like', $like);
@@ -483,14 +494,14 @@ final class SettlementCenterLedgerService
}
/**
* @param \Illuminate\Support\Collection<int, object> $stubs
* @param Collection<int, object> $stubs
* @param array<int, object> $playerBills
* @return list<array<string, mixed>>
*/
private function hydrateLedgerStubs(
AdminUser $admin,
string $siteCode,
\Illuminate\Support\Collection $stubs,
Collection $stubs,
array $playerBills,
): array {
if ($stubs->isEmpty()) {
@@ -641,6 +652,71 @@ final class SettlementCenterLedgerService
return $selected === $kind;
}
/**
* actionable_only 依赖 hydrated 行的 available_actions需在内存中过滤后再分页。
*
* @param list<Builder> $stubQueries
* @param array<int, object> $playerBills
* @return array{
* items: list<array<string, mixed>>,
* total: int,
* page: int,
* per_page: int,
* ledger_source: string,
* truncated: bool,
* }
*/
private function listUnifiedActionableFiltered(
AdminUser $admin,
string $siteCode,
int $page,
int $perPage,
SettlementLedgerListFilters $filters,
array $stubQueries,
array $playerBills,
bool $billsTruncated,
): array {
$stubs = $this->fetchAllLedgerStubs($stubQueries);
$items = $this->hydrateLedgerStubs($admin, $siteCode, $stubs, $playerBills);
$items = $this->applyFilters($items, $filters);
$total = count($items);
$offset = max(0, ($page - 1) * $perPage);
$pageItems = array_slice($items, $offset, $perPage);
return [
'items' => array_values($pageItems),
'total' => $total,
'page' => $page,
'per_page' => $perPage,
'ledger_source' => 'settlement_ledger',
'truncated' => $billsTruncated || $total > 5000,
];
}
/**
* @param list<Builder> $stubQueries
*/
private function fetchAllLedgerStubs(array $stubQueries): Collection
{
if (count($stubQueries) === 1) {
return (clone $stubQueries[0])
->orderByDesc('sort_at')
->orderByDesc('entry_id')
->get();
}
$union = null;
foreach ($stubQueries as $stubQuery) {
$union = $union === null ? $stubQuery : $union->unionAll($stubQuery);
}
return DB::query()->fromSub($union, 'ledger_page')
->orderByDesc('sort_at')
->orderByDesc('entry_id')
->get();
}
/**
* @param list<array<string, mixed>> $items
* @return list<array<string, mixed>>
@@ -1541,7 +1617,7 @@ final class SettlementCenterLedgerService
}
if ($billStatus !== null
&& in_array($billStatus, ['confirmed', 'partial_paid', 'settled', 'overdue'], true)
&& in_array($billStatus, ['confirmed', 'partial_paid', 'overdue'], true)
&& ! in_array((string) $billType, ['adjustment', 'reversal', 'bad_debt'], true)) {
$actions[] = 'adjustment';
$actions[] = 'reversal';