fix(settlement): 操作记录分页、流水动作与坏账幂等加固
- 新增 settlement-operations 合并列表 API,收付/调账接口改为标准分页 - actionable_only 流水在内存过滤后正确分页 - 已结账单流水不再展示补差/冲正快捷动作 - 坏账核销支持 idempotency_key 并写入 result_bill_id - 补充操作记录、流水与坏账幂等相关测试
This commit is contained in:
@@ -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';
|
||||
|
||||
Reference in New Issue
Block a user