diff --git a/app/Http/Controllers/Business/Pages.php b/app/Http/Controllers/Business/Pages.php index ff0a59c6..7fac7142 100644 --- a/app/Http/Controllers/Business/Pages.php +++ b/app/Http/Controllers/Business/Pages.php @@ -7040,7 +7040,6 @@ class Pages extends Controller $pending = false; $indexPending = false; $pageLabel = $pendingConfirmation ? '退款待确认' : '售后(退货退款)'; - $refundMonthlySummary = []; $refundPlatformMonthlySummary = ['months' => [], 'platforms' => []]; try { @@ -7065,7 +7064,6 @@ class Pages extends Controller $defaultTo, ), ); - $refundMonthlySummary = $refundPlatformMonthlySummary['months'] ?? []; } else { $result = $this->nonJingxiRefunds->paginate($filters, $page, $limit); $rows = new LengthAwarePaginator( @@ -7096,7 +7094,6 @@ class Pages extends Controller 'refundPageLabel' => $pageLabel, 'refundPendingConfirmationMode' => $pendingConfirmation, 'refundOnlineStatusLabel' => $pendingConfirmation ? '线上状态' : '店铺状态', - 'refundMonthlySummary' => $refundMonthlySummary, 'refundPlatformMonthlySummary' => $refundPlatformMonthlySummary, 'refundShowExport' => ! $pendingConfirmation || BusinessOperationPermissions::can( @@ -7148,20 +7145,29 @@ class Pages extends Controller public function refundPendingMonthDetails(Request $request) { $month = trim((string) $request->query('month', '')); - if (preg_match('/^\d{4}-\d{2}$/', $month) !== 1) { + $allMonths = $month === '' || $month === 'all'; + if (! $allMonths && preg_match('/^\d{4}-\d{2}$/', $month) !== 1) { return response()->json(['message' => '退款月份格式不正确'], 422); } - $start = Carbon::createFromFormat('!Y-m', $month)->startOfMonth(); $today = now()->startOfDay(); - if ($start->format('Y-m') < '2026-10' || $start->greaterThan($today)) { - return response()->json(['message' => '退款月份超出可查询范围'], 422); - } - - $end = $start->copy()->endOfMonth()->startOfDay(); - if ($end->greaterThan($today)) { + if ($allMonths) { + $monthKey = 'all'; + $start = Carbon::createFromFormat('!Y-m-d', '2026-10-01')->startOfDay(); $end = $today; + } else { + $monthKey = $month; + $start = Carbon::createFromFormat('!Y-m', $month)->startOfMonth(); + if ($start->format('Y-m') < '2026-10' || $start->greaterThan($today)) { + return response()->json(['message' => '退款月份超出可查询范围'], 422); + } + + $end = $start->copy()->endOfMonth()->startOfDay(); + if ($end->greaterThan($today)) { + $end = $today; + } } + $platform = mb_substr(trim((string) $request->query('platform', '')), 0, 120); $page = max((int) $request->query('page', 1), 1); $limit = min(max((int) $request->query('limit', 25), 1), 50); $filters = [ @@ -7171,6 +7177,7 @@ class Pages extends Controller 'waybill_no' => '', 'shop_name' => '', 'sku_id' => '', + 'platform' => $platform, ]; try { @@ -7180,8 +7187,9 @@ class Pages extends Controller return response()->json(['message' => '退款待确认镜像尚未就绪'], 503); } + $platformKey = sha1($platform); $details = \Illuminate\Support\Facades\Cache::remember( - "refund-pending.month-details.v1:{$month}:{$page}:{$limit}", + "refund-pending.details.v2:{$monthKey}:{$end->toDateString()}:{$platformKey}:{$page}:{$limit}", now()->addMinutes(5), function () use ($filters, $page, $limit): array { $result = $this->nonJingxiRefunds->pagePendingConfirmation($filters, $page, $limit); @@ -7215,7 +7223,8 @@ class Pages extends Controller } return response()->json([ - 'month' => $start->format('Y-m'), + 'month' => $monthKey, + 'platform' => $platform, 'page' => $page, 'limit' => $limit, 'has_more' => $details['has_more'], diff --git a/modules/EcommerceFinance/Repositories/NonJingxiRefundMirrorRepository.php b/modules/EcommerceFinance/Repositories/NonJingxiRefundMirrorRepository.php index 93d79c7b..9999f1dc 100644 --- a/modules/EcommerceFinance/Repositories/NonJingxiRefundMirrorRepository.php +++ b/modules/EcommerceFinance/Repositories/NonJingxiRefundMirrorRepository.php @@ -48,6 +48,9 @@ class NonJingxiRefundMirrorRepository private ?array $excludedShopIds = null; + /** @var array>|null shop_id => historical shop_site values */ + private ?array $supplementShopSites = null; + private ?string $supplementDateIndex = null; public function __construct( @@ -158,7 +161,7 @@ class NonJingxiRefundMirrorRepository } /** - * @param array{from:string,to:string,order_no:string,waybill_no:string,shop_name:string,sku_id:string} $filters + * @param array{from:string,to:string,order_no:string,waybill_no:string,shop_name:string,sku_id:string,platform?:string} $filters * @return array{data:list,total:int} */ public function paginate(array $filters, int $page, int $perPage): array @@ -180,7 +183,7 @@ class NonJingxiRefundMirrorRepository /** * 售后状态为待确认、且线上退款已成功的记录。 * - * @param array{from:string,to:string,order_no:string,waybill_no:string,shop_name:string,sku_id:string} $filters + * @param array{from:string,to:string,order_no:string,waybill_no:string,shop_name:string,sku_id:string,platform?:string} $filters * @return array{data:list,total:int} */ public function paginatePendingConfirmation(array $filters, int $page, int $perPage): array @@ -196,7 +199,7 @@ class NonJingxiRefundMirrorRepository /** * 按页读取待确认明细,不做全月 COUNT;has_more 由下一页键探测。 * - * @param array{from:string,to:string,order_no:string,waybill_no:string,shop_name:string,sku_id:string} $filters + * @param array{from:string,to:string,order_no:string,waybill_no:string,shop_name:string,sku_id:string,platform?:string} $filters * @return array{data:list,has_more:bool} */ public function pagePendingConfirmation(array $filters, int $page, int $perPage): array @@ -452,10 +455,9 @@ class NonJingxiRefundMirrorRepository if ($supplement) { // A shop may have several historical rows. An ambiguous platform is // left unknown, and must never multiply the refund rows. - $this->excludedShopIds ??= $this->connection->table('ops_jst_shop_info') - ->get(['shop_id', 'shop_site'])->groupBy('shop_id') - ->filter(function ($rows): bool { - $sites = $rows->pluck('shop_site')->filter()->unique(); + $this->excludedShopIds ??= collect($this->supplementShopSites()) + ->filter(function (array $sites): bool { + $sites = collect($sites)->filter()->unique(); $site = (string) $sites->first(); return $sites->count() === 1 && (str_contains($site, '京喜') || str_contains(strtolower($site), 'jingxi')); })->keys()->map('strval')->all(); @@ -484,6 +486,7 @@ class NonJingxiRefundMirrorRepository }); }); } + $this->applyPlatformFilter($query, $filters, $supplement); $this->applyFilters($query, $filters); if ($filters['sku_id'] !== '') { if ($withItems) { @@ -640,7 +643,7 @@ class NonJingxiRefundMirrorRepository return implode('|', [$row->_source_rank, $row->_head_id, $row->_item_id, $row->_source_id]); } - /** @param array{from:string,to:string,order_no:string,waybill_no:string,shop_name:string,sku_id:string} $filters */ + /** @param array{from:string,to:string,order_no:string,waybill_no:string,shop_name:string,sku_id:string,platform?:string} $filters */ private function applyFilters(Builder $query, array $filters): void { $query->where('r.as_date', '>=', $filters['from'] . ' 00:00:00') @@ -665,8 +668,84 @@ class NonJingxiRefundMirrorRepository } } + private function applyPlatformFilter(Builder $query, array $filters, bool $supplement): void + { + $target = trim((string) ($filters['platform'] ?? '')); + if ($target === '') { + return; + } + + if (! $supplement) { + if ($target === '待确认') { + $query->where(function (Builder $platform): void { + $platform->whereNull('r.shop_site') + ->orWhere('r.shop_site', '') + ->orWhere('r.shop_site', '待确认'); + }); + } else { + $query->where('r.shop_site', $target); + } + + return; + } + + $matchedShopIds = []; + $knownShopIds = []; + foreach ($this->supplementShopSites() as $shopId => $sites) { + $sites = array_values(array_unique(array_filter(array_map( + static fn (mixed $site): string => trim((string) $site), + $sites, + )))); + $site = count($sites) === 1 ? $sites[0] : '待确认'; + if ($site === $target) { + $matchedShopIds[] = (string) $shopId; + } + if ($site !== '待确认') { + $knownShopIds[] = (string) $shopId; + } + } + + if ($target === '待确认') { + if ($knownShopIds !== []) { + $query->where(function (Builder $platform) use ($knownShopIds): void { + $platform->whereNull('r.shop_id') + ->orWhereNotIn('r.shop_id', $knownShopIds); + }); + } + + return; + } + + if ($matchedShopIds === []) { + $query->whereRaw('1 = 0'); + + return; + } + + $query->whereIn('r.shop_id', $matchedShopIds); + } + + /** @return array> */ + private function supplementShopSites(): array + { + if ($this->supplementShopSites !== null) { + return $this->supplementShopSites; + } + + $this->supplementShopSites = []; + foreach ($this->connection->table('ops_jst_shop_info')->get(['shop_id', 'shop_site']) as $row) { + $shopId = trim((string) ($row->shop_id ?? '')); + if ($shopId === '') { + continue; + } + $this->supplementShopSites[$shopId][] = (string) ($row->shop_site ?? ''); + } + + return $this->supplementShopSites; + } + /** - * @param array{from:string,to:string,order_no:string,waybill_no:string,shop_name:string,sku_id:string} $filters + * @param array{from:string,to:string,order_no:string,waybill_no:string,shop_name:string,sku_id:string,platform?:string} $filters * @return array{from:string,to:string,order_no:string,waybill_no:string,shop_name:string,sku_id:string,status:string,shop_status:string} */ private function pendingConfirmationFilters(array $filters): array diff --git a/resources/views/business/pages/refunds/non-jingxi-refund-data.blade.php b/resources/views/business/pages/refunds/non-jingxi-refund-data.blade.php index d5c5d3cd..bef557ce 100644 --- a/resources/views/business/pages/refunds/non-jingxi-refund-data.blade.php +++ b/resources/views/business/pages/refunds/non-jingxi-refund-data.blade.php @@ -218,61 +218,92 @@ .refund-platform-summary__table td:first-child { text-align: left; } .refund-platform-summary__table th { position: sticky; top: 0; z-index: 1; background: var(--vip-content-accent-soft, #f7f0de); font-weight: 600; } .refund-platform-summary__table td:first-child { font-weight: 600; } - .refund-month-group + .refund-month-group { border-top: 1px solid var(--vip-border, #d8d2c7); } - .refund-month-toggle { - display: grid; - width: 100%; - min-height: 3.25rem; - grid-template-columns: minmax(7rem, 1fr) auto auto; + .refund-summary-count { + display: inline-flex; + min-width: 2.75rem; + min-height: 1.65rem; align-items: center; - gap: 1rem; - padding: 0.65rem 1rem; + justify-content: flex-end; + padding: 0 0.35rem; border: 0; - background: var(--vip-surface, #ffffff); - color: var(--vip-content-ink, #171717); + border-radius: 4px; + background: transparent; + color: var(--vip-content-accent-dark, #856632); font: inherit; - text-align: left; + font-variant-numeric: tabular-nums; + text-decoration: underline dotted; + text-underline-offset: 3px; cursor: pointer; } - .refund-month-toggle:hover { background: #fbf8f0; } - .refund-month-toggle:focus-visible { outline: 2px solid var(--vip-content-accent-dark, #856632); outline-offset: -2px; } - .refund-month-toggle__month { font-size: 0.8125rem; font-weight: 600; } - .refund-month-toggle__count { color: var(--vip-content-accent-dark, #856632); font-size: 0.8125rem; font-weight: 600; } - .refund-month-toggle__icon { transition: transform 150ms ease; } - .refund-month-toggle[aria-expanded="true"] .refund-month-toggle__icon { transform: rotate(180deg); } - .refund-month-panel { - padding: 0 1rem 1rem; - border-top: 1px solid #ece8df; + .refund-summary-count:hover, + .refund-summary-count[aria-pressed="true"] { background: var(--vip-content-accent-soft, #f7f0de); text-decoration-style: solid; } + .refund-summary-count:focus-visible { outline: 2px solid var(--vip-content-accent-dark, #856632); outline-offset: 1px; } + .refund-platform-summary__table .refund-platform-summary__total-row th, + .refund-platform-summary__table .refund-platform-summary__total-row td { + position: sticky; + top: auto; + bottom: 0; + z-index: 2; + border-top: 2px solid var(--vip-content-accent, #b9954f); + background: #fffaf0; + font-weight: 700; + } + .refund-summary-detail { + padding: 1rem; + border-top: 1px solid var(--vip-border, #d8d2c7); background: #fbfaf6; } - .refund-month-panel[hidden] { display: none; } - .refund-month-panel__status { - padding: 1rem 0.25rem; + .refund-summary-detail[hidden] { display: none; } + .refund-summary-detail__head { + display: flex; + align-items: center; + justify-content: space-between; + gap: 1rem; + margin-bottom: 0.75rem; + } + .refund-summary-detail__title { + color: var(--vip-content-ink, #171717); + font-size: 0.8125rem; + font-weight: 600; + } + .refund-summary-detail__close { + min-height: 1.85rem; + padding: 0 0.65rem; + border: 1px solid var(--vip-border, #d8d2c7); + border-radius: 6px; + background: var(--vip-surface, #ffffff); + color: var(--vip-content-muted, #595959); + font: inherit; + font-size: 0.75rem; + cursor: pointer; + } + .refund-summary-detail__status { + padding: 0.5rem 0.25rem; color: var(--vip-content-muted, #595959); font-size: 0.75rem; } - .refund-month-detail-scroll { + .refund-summary-detail__scroll { overflow: auto; max-height: min(34rem, 62vh); } - .refund-month-detail-table { + .refund-summary-detail__table { width: 100%; min-width: 92rem; border-collapse: collapse; background: var(--vip-surface, #ffffff); font-size: 0.75rem; } - .refund-month-detail-table th, - .refund-month-detail-table td { + .refund-summary-detail__table th, + .refund-summary-detail__table td { padding: 0.55rem 0.65rem; border-right: 1px solid var(--vip-border, #d8d2c7); border-bottom: 1px solid var(--vip-border, #d8d2c7); text-align: left; white-space: nowrap; } - .refund-month-detail-table th { position: sticky; top: 0; z-index: 2; background: var(--vip-content-accent-soft, #f7f0de); font-weight: 600; } - .refund-month-detail-table .is-number { text-align: right; font-variant-numeric: tabular-nums; } - .refund-month-panel__pagination { + .refund-summary-detail__table th { position: sticky; top: 0; z-index: 2; background: var(--vip-content-accent-soft, #f7f0de); font-weight: 600; } + .refund-summary-detail__table .is-number { text-align: right; font-variant-numeric: tabular-nums; } + .refund-summary-detail__pagination { display: flex; align-items: center; justify-content: space-between; @@ -281,7 +312,7 @@ color: var(--vip-content-muted, #595959); font-size: 0.75rem; } - .refund-month-panel__pagination button { + .refund-summary-detail__pagination button { min-height: 1.85rem; padding: 0 0.65rem; border: 1px solid var(--vip-border, #d8d2c7); @@ -291,10 +322,7 @@ font: inherit; cursor: pointer; } - .refund-month-panel__pagination button:disabled { cursor: default; opacity: 0.45; } - @media (prefers-reduced-motion: reduce) { - .refund-month-toggle__icon { transition: none; } - } + .refund-summary-detail__pagination button:disabled { cursor: default; opacity: 0.45; } @if ($refundWarningMode) @@ -346,7 +374,7 @@
按退款月份统计 - 点击月份展开待确认明细 + 点击汇总数字查看对应待确认明细
@if ($refundShowExport) @endif
- @if (!empty($refundPlatformMonthlySummary['platforms'])) + @php + $summaryMonths = $refundPlatformMonthlySummary['months'] ?? []; + $summaryGrandTotal = array_sum(array_map( + static fn (array $summaryMonth): int => (int) ($summaryMonth['total'] ?? 0), + $summaryMonths, + )); + @endphp + @if (!empty($summaryMonths))
平台月度汇总
@@ -370,7 +405,7 @@ 平台 - @foreach ($refundPlatformMonthlySummary['months'] as $summaryMonth) + @foreach ($summaryMonths as $summaryMonth) {{ \Illuminate\Support\Carbon::parse($summaryMonth['month'] . '-01')->format('Y年n月') }} @endforeach 合计 @@ -380,49 +415,81 @@ @foreach ($refundPlatformMonthlySummary['platforms'] as $platformSummary) {{ $platformSummary['platform'] }} - @foreach ($refundPlatformMonthlySummary['months'] as $summaryMonth) - {{ number_format((int) ($platformSummary['months'][$summaryMonth['month']] ?? 0)) }} + @foreach ($summaryMonths as $summaryMonth) + @php + $summaryMonthLabel = \Illuminate\Support\Carbon::parse($summaryMonth['month'] . '-01')->format('Y年n月'); + $platformMonthCount = (int) ($platformSummary['months'][$summaryMonth['month']] ?? 0); + @endphp + + + @endforeach - {{ number_format((int) $platformSummary['total']) }} + + + @endforeach + + 月度合计 + @foreach ($summaryMonths as $summaryMonth) + + + + @endforeach + + + +
+ @else +
从 2026 年 10 月起暂无待确认记录。
@endif - @forelse ($refundMonthlySummary as $monthSummary) - @php - $monthKey = $monthSummary['month']; - $panelId = 'refund-month-panel-' . str_replace('-', '', $monthKey); - @endphp -
- - -
- @empty -
从 2026 年 10 月起暂无待确认记录。
- @endforelse + @else diff --git a/tests/Feature/Business/NonJingxiRefundPageTest.php b/tests/Feature/Business/NonJingxiRefundPageTest.php index 418b1a24..dfd1fa8a 100644 --- a/tests/Feature/Business/NonJingxiRefundPageTest.php +++ b/tests/Feature/Business/NonJingxiRefundPageTest.php @@ -180,9 +180,15 @@ class NonJingxiRefundPageTest extends FeatureTestCase ->assertSeeText('京东商城') ->assertSeeText('天猫') ->assertSeeText('2026年10月') - ->assertSeeText('5,641 条待确认') ->assertSeeText('2026年11月') - ->assertSeeText('3,232 条待确认') + ->assertSeeText('点击汇总数字查看对应待确认明细') + ->assertSeeText('月度合计') + ->assertSee('data-refund-detail-trigger', false) + ->assertSee('data-refund-detail-month="2026-10"', false) + ->assertSee('data-refund-detail-platform="京东商城"', false) + ->assertSee('data-refund-summary-detail', false) + ->assertDontSee('data-refund-month-toggle', false) + ->assertDontSeeText('5,641 条待确认') ->assertSeeText('导出全部明细') ->assertSee('data-refund-all-export', false) ->assertSee('from=2026-10-01&to=2026-11-04', false) @@ -252,6 +258,7 @@ class NonJingxiRefundPageTest extends FeatureTestCase 'waybill_no' => '', 'shop_name' => '', 'sku_id' => '', + 'platform' => '京东商城', ] && $page === 2 && $limit === 25 )->andReturn([ 'data' => [(object) [ @@ -278,9 +285,11 @@ class NonJingxiRefundPageTest extends FeatureTestCase $this->loginAs()->getJson(route('business.refunds.refund-pending.month-details', [ 'month' => '2026-10', 'page' => 2, + 'platform' => '京东商城', ])) ->assertOk() ->assertJsonPath('month', '2026-10') + ->assertJsonPath('platform', '京东商城') ->assertJsonPath('page', 2) ->assertJsonPath('has_more', true) ->assertJsonPath('rows.0.after_sales_no', '743441809') @@ -293,6 +302,38 @@ class NonJingxiRefundPageTest extends FeatureTestCase ->assertJsonPath('rows.0.sku_id', 'SKU-PENDING-1'); } + public function test_refund_pending_all_details_endpoint_uses_october_to_today(): void + { + $this->grantAdminPermission(); + $repository = \Mockery::mock(NonJingxiRefundMirrorRepository::class); + $repository->shouldReceive('available')->once()->andReturnTrue(); + $repository->shouldReceive('ready')->once()->andReturnTrue(); + $repository->shouldReceive('queryReady')->once()->andReturnTrue(); + $repository->shouldReceive('pagePendingConfirmation')->once()->withArgs( + static fn (array $filters, int $page, int $limit): bool => + $filters === [ + 'from' => '2026-10-01', + 'to' => '2026-11-04', + 'order_no' => '', + 'waybill_no' => '', + 'shop_name' => '', + 'sku_id' => '', + 'platform' => '', + ] && $page === 1 && $limit === 25 + )->andReturn(['data' => [], 'has_more' => false]); + $this->app->instance(NonJingxiRefundMirrorRepository::class, $repository); + + $this->loginAs()->getJson(route('business.refunds.refund-pending.month-details', [ + 'month' => 'all', + ])) + ->assertOk() + ->assertJsonPath('month', 'all') + ->assertJsonPath('platform', '') + ->assertJsonPath('page', 1) + ->assertJsonPath('has_more', false) + ->assertJsonCount(0, 'rows'); + } + public function test_refund_export_downloads_filtered_csv(): void { $this->grantAdminPermission(); diff --git a/tests/Unit/EcommerceFinance/NonJingxiRefundMirrorRepositoryTest.php b/tests/Unit/EcommerceFinance/NonJingxiRefundMirrorRepositoryTest.php index 25cec323..fb5a0079 100644 --- a/tests/Unit/EcommerceFinance/NonJingxiRefundMirrorRepositoryTest.php +++ b/tests/Unit/EcommerceFinance/NonJingxiRefundMirrorRepositoryTest.php @@ -217,6 +217,53 @@ class NonJingxiRefundMirrorRepositoryTest extends TestCase ], $platformSummary['platforms']); } + public function test_pending_confirmation_details_filter_by_platform_across_both_sources(): void + { + $db = DB::connection('business_mirror'); + $db->table('ops_jst_return_refund_info')->insert([ + array_replace($this->refundInfo(1, 101, '2026-10-02 10:00:00', '天猫', '天猫店'), [ + 'status' => 'WaitConfirm', + 'shop_status' => 'SUCCESS', + ]), + array_replace($this->refundInfo(2, 102, '2026-10-03 10:00:00', '京东商城', '京东店'), [ + 'status' => 'WaitConfirm', + 'shop_status' => 'SUCCESS', + ]), + ]); + $knownSupplement = $this->supplement(201, '2026-10-04 10:00:00', 'SHOP-JD'); + $knownSupplement['status'] = 'WaitConfirm'; + $knownSupplement['shop_status'] = 'SUCCESS'; + $unknownSupplement = $this->supplement(202, '2026-10-05 10:00:00', 'SHOP-UNKNOWN'); + $unknownSupplement['status'] = 'WaitConfirm'; + $unknownSupplement['shop_status'] = 'SUCCESS'; + $db->table('ops_jst_qmcust_refunds')->insert([$knownSupplement, $unknownSupplement]); + $db->table('ops_jst_shop_info')->insert([ + ['Id' => 1, 'shop_id' => 'SHOP-JD', 'shop_site' => '京东商城'], + ]); + + $filters = [ + 'from' => '2026-10-01', + 'to' => '2026-10-31', + 'order_no' => '', + 'waybill_no' => '', + 'shop_name' => '', + 'sku_id' => '', + ]; + $idsFor = function (string $platform) use ($filters): array { + $result = $this->repository->pagePendingConfirmation( + $filters + ['platform' => $platform], + 1, + 25, + ); + + return array_map(static fn (object $row): int => (int) $row->as_id, $result['data']); + }; + + $this->assertSame([101], $idsFor('天猫')); + $this->assertEqualsCanonicalizing([102, 201], $idsFor('京东商城')); + $this->assertSame([202], $idsFor('待确认')); + } + public function test_supplement_and_header_only_refunds_survive_without_invented_details(): void { $db = DB::connection('business_mirror');