From e5b752c12a0362f1d585f606c13e89885c1f8aae Mon Sep 17 00:00:00 2001 From: 92451 <924518135@qq.com> Date: Sat, 24 Oct 2026 19:24:19 +0800 Subject: [PATCH] feat: alert on unchanged withdrawal balances --- .../WithdrawalBalanceAlertSynchronizer.php | 238 ++++++++++++++++++ app/Business/WithdrawalManagementReport.php | 116 +++++++-- .../WithdrawalManagementSpreadsheet.php | 49 +++- .../Commands/ImportJushuitanShopBalances.php | 36 ++- app/Http/Controllers/Business/Pages.php | 28 +++ .../JushuitanShopBalanceRepository.php | 103 +++++--- .../withdrawal-management.blade.php | 77 ++++-- .../views/business/pages/todos.blade.php | 104 +++++--- tests/Feature/Business/ManagementCrudTest.php | 8 +- .../Business/WithdrawalBalanceAlertTest.php | 219 ++++++++++++++++ .../WithdrawalManagementImportExportTest.php | 25 +- 11 files changed, 885 insertions(+), 118 deletions(-) create mode 100644 app/Business/WithdrawalBalanceAlertSynchronizer.php create mode 100644 tests/Feature/Business/WithdrawalBalanceAlertTest.php diff --git a/app/Business/WithdrawalBalanceAlertSynchronizer.php b/app/Business/WithdrawalBalanceAlertSynchronizer.php new file mode 100644 index 00000000..b5eb2dee --- /dev/null +++ b/app/Business/WithdrawalBalanceAlertSynchronizer.php @@ -0,0 +1,238 @@ +,alerts:array>,created:int,updated:int} + */ + public function syncLatest(int $companyId): array + { + $latest = $this->shopBalances->latestBalanceDate(now()); + if ($latest === null) { + return [ + 'balance_date' => '', + 'dates' => [], + 'alerts' => [], + 'created' => 0, + 'updated' => 0, + ]; + } + + return $this->syncForDate($companyId, $latest); + } + + /** + * @return array{balance_date:string,dates:list,alerts:array>,created:int,updated:int} + */ + public function syncForDate(int $companyId, string $balanceDate): array + { + $dates = self::datesEndingAt($balanceDate); + $stores = StoreReceivingAccount::withoutGlobalScopes() + ->where('company_id', $companyId) + ->whereNull('deleted_at') + ->orderBy('id') + ->get(); + + if ($stores->isEmpty()) { + return [ + 'balance_date' => $balanceDate, + 'dates' => $dates, + 'alerts' => [], + 'created' => 0, + 'updated' => 0, + ]; + } + + $balancesByDate = $this->shopBalances->matchesForStoresOnDates($stores, $dates); + $reserveStandards = $stores->mapWithKeys( + static fn (StoreReceivingAccount $store): array => [ + (int) $store->id => WithdrawalReserveStandard::amountFor( + $store->store_platform, + $store->erp_store_name, + ), + ], + )->all(); + $alerts = self::detect($stores, $balancesByDate, $reserveStandards, $dates); + [$created, $updated] = DB::transaction(function () use ($companyId, $stores, &$alerts): array { + $created = 0; + $updated = 0; + + foreach ($stores as $store) { + $alert = $alerts[(int) $store->id] ?? null; + if ($alert === null) { + continue; + } + + $title = $this->todoTitle($store); + $description = $this->description($store, $alert); + $todo = Todo::withoutGlobalScopes() + ->where('company_id', $companyId) + ->where('category', self::CATEGORY) + ->where('title', $title) + ->whereNull('deleted_at') + ->whereNotIn('status', ['已完成', '已关闭']) + ->orderByDesc('id') + ->lockForUpdate() + ->first(); + + if ($todo === null) { + $todo = Todo::withoutGlobalScopes()->create([ + 'company_id' => $companyId, + 'title' => $title, + 'category' => self::CATEGORY, + 'priority' => '高', + 'status' => self::TODO_STATUS, + 'description' => $description, + ]); + $created++; + } else { + $attributes = [ + 'priority' => '高', + 'description' => $description, + ]; + if ($todo->status !== self::TODO_STATUS) { + $attributes['status'] = self::TODO_STATUS; + } + if ($todo->fill($attributes)->isDirty()) { + $todo->save(); + $updated++; + } + } + + $alerts[(int) $store->id]['todo_id'] = (int) $todo->id; + } + + return [$created, $updated]; + }); + + return [ + 'balance_date' => $balanceDate, + 'dates' => $dates, + 'alerts' => $alerts, + 'created' => $created, + 'updated' => $updated, + ]; + } + + /** + * @param iterable $stores + * @param array> $balancesByDate + * @param array $reserveStandards + * @param list $dates + * @return array + */ + public static function detect( + iterable $stores, + array $balancesByDate, + array $reserveStandards, + array $dates, + ): array { + if (count($dates) !== 3) { + return []; + } + + $alerts = []; + foreach ($stores as $store) { + $reserve = $reserveStandards[(int) $store->id] ?? null; + if ($reserve === null) { + continue; + } + + $balanceCents = null; + $unchanged = true; + foreach ($dates as $date) { + $match = $balancesByDate[(int) $store->id][$date] + ?? ['status' => 'not_ready', 'balance' => null]; + if (($match['status'] ?? '') !== 'matched' || ($match['balance'] ?? null) === null) { + $unchanged = false; + break; + } + + $cents = (int) round((float) $match['balance'] * 100); + if ($balanceCents === null) { + $balanceCents = $cents; + } elseif ($balanceCents !== $cents) { + $unchanged = false; + break; + } + } + + if (! $unchanged || $balanceCents === null || $balanceCents <= $reserve * 100) { + continue; + } + + $balance = $balanceCents / 100; + $alerts[(int) $store->id] = [ + 'balance' => $balance, + 'start_date' => $dates[0], + 'end_date' => $dates[2], + 'message' => sprintf( + '余额连续 3 天保持 %s 元,高于预留标准 %s 元。', + number_format($balance, 2), + number_format($reserve, 2), + ), + ]; + } + + return $alerts; + } + + /** @return list */ + public static function datesEndingAt(string $balanceDate): array + { + $end = CarbonImmutable::createFromFormat('!Y-m-d', $balanceDate); + if (! $end instanceof CarbonImmutable || $end->format('Y-m-d') !== $balanceDate) { + return []; + } + + return [ + $end->subDays(2)->toDateString(), + $end->subDay()->toDateString(), + $end->toDateString(), + ]; + } + + private function todoTitle(StoreReceivingAccount $store): string + { + return mb_substr( + '提现余额连续3天未变动|' . (string) $store->store_platform . '|' . (string) $store->erp_store_name, + 0, + 255, + ); + } + + /** @param array{balance:float,start_date:string,end_date:string,message:string} $alert */ + private function description(StoreReceivingAccount $store, array $alert): string + { + return implode("\n", array_filter([ + $alert['message'], + '统计期间:' . $alert['start_date'] . ' 至 ' . $alert['end_date'], + '店铺平台:' . ($store->store_platform ?: '未配置'), + 'ERP店铺名称:' . ($store->erp_store_name ?: '未配置'), + '店铺主体:' . ($store->store_entity_name ?: '未配置'), + '平台店铺ID:' . ($store->store_id ?: '未配置'), + '处理建议:核实店铺余额是否真实停滞,并检查是否应执行提现。', + ], static fn (string $line): bool => $line !== '')); + } +} diff --git a/app/Business/WithdrawalManagementReport.php b/app/Business/WithdrawalManagementReport.php index b448cade..60e702fc 100644 --- a/app/Business/WithdrawalManagementReport.php +++ b/app/Business/WithdrawalManagementReport.php @@ -18,7 +18,10 @@ class WithdrawalManagementReport 'withdrawal_path', 'withdrawal_account', 'bank_card', + 'balance_two_days_ago', + 'balance_yesterday', 'balance', + 'balance_alert', 'reserve_standard', 'withdrawal_amount', 'compliance', @@ -34,9 +37,12 @@ class WithdrawalManagementReport * filters:array{platform:string,withdrawalPath:string,storeEntity:string,keyword:string,balanceStatus:string,balanceDate:string}, * platforms:list,paths:list,storeEntities:list,rows:Collection, * balances:array, + * balancesByDate:array>, + * balanceDays:list, + * alerts:array, * reserveStandards:array,withdrawalAmounts:array,compliance:array, * summary:array{source:int,matched:int,ambiguous:int,unmatched:int,not_ready:int}, - * balanceColumnLabel:string,balanceDate:string,balanceDates:list,sort:array{field:string,direction:string} + * balanceColumnLabel:string,balanceDate:string,latestBalanceDate:?string,balanceDates:list,sort:array{field:string,direction:string} * } */ public function results(Request $request): array @@ -71,7 +77,17 @@ class WithdrawalManagementReport ? CarbonImmutable::parse($balanceDate) : $today; $rows = $query->orderByDesc('id')->get(); - $balances = $this->shopBalances->matchesForStores($rows, $balanceDate); + $historyDates = WithdrawalBalanceAlertSynchronizer::datesEndingAt($balanceDate); + $balancesByDate = $this->shopBalances->matchesForStoresOnDates($rows, $historyDates); + $balances = array_map( + static fn (array $dates): array => $dates[$balanceDate] ?? [ + 'status' => 'not_ready', + 'balance' => null, + 'source_store_name' => null, + 'source_platform' => null, + ], + $balancesByDate, + ); $balanceCounts = collect($balances)->countBy( static fn (array $match): string => $match['status'], ); @@ -90,6 +106,19 @@ class WithdrawalManagementReport ), ], )->all(); + $balanceDays = collect($historyDates)->map( + static fn (string $date, int $index): array => [ + 'key' => $index === 0 ? 'balance_two_days_ago' : ($index === 1 ? 'balance_yesterday' : 'balance'), + 'date' => $date, + 'label' => CarbonImmutable::parse($date)->format('n月j日') . '店铺余额', + ], + )->values()->all(); + $alerts = WithdrawalBalanceAlertSynchronizer::detect( + $rows, + $balancesByDate, + $reserveStandards, + $historyDates, + ); $withdrawalAmounts = $rows->mapWithKeys( fn (StoreReceivingAccount $row): array => [ (int) $row->id => $this->withdrawalAmountFor($row, $balanceDate), @@ -106,7 +135,16 @@ class WithdrawalManagementReport )->all(); $rows = $sort['field'] === '' ? $this->sortByBalanceStatus($rows, $balances) - : $this->sortRows($rows, $sort, $balances, $reserveStandards, $withdrawalAmounts, $compliance); + : $this->sortRows( + $rows, + $sort, + $balancesByDate, + $balanceDays, + $alerts, + $reserveStandards, + $withdrawalAmounts, + $compliance, + ); return [ 'filters' => $filters, @@ -115,6 +153,9 @@ class WithdrawalManagementReport 'storeEntities' => $storeEntities, 'rows' => $rows, 'balances' => $balances, + 'balancesByDate' => $balancesByDate, + 'balanceDays' => $balanceDays, + 'alerts' => $alerts, 'reserveStandards' => $reserveStandards, 'withdrawalAmounts' => $withdrawalAmounts, 'compliance' => $compliance, @@ -127,6 +168,7 @@ class WithdrawalManagementReport ], 'balanceColumnLabel' => $labelDate->format('n月j日') . '店铺余额', 'balanceDate' => $balanceDate, + 'latestBalanceDate' => $latestBalanceDate, 'balanceDates' => $this->shopBalances->availableDates($today), 'sort' => $sort, ]; @@ -209,17 +251,35 @@ class WithdrawalManagementReport /** * @param Collection $rows * @param array{field:string,direction:string} $sort - * @param array $balances + * @param array> $balancesByDate + * @param list $balanceDays + * @param array $alerts * @param array $reserveStandards * @param array $withdrawalAmounts * @param array $compliance * @return Collection */ - private function sortRows(Collection $rows, array $sort, array $balances, array $reserveStandards, array $withdrawalAmounts, array $compliance): Collection - { - return $rows->sort(function (StoreReceivingAccount $left, StoreReceivingAccount $right) use ($sort, $balances, $reserveStandards, $withdrawalAmounts, $compliance): int { - $leftValue = $this->sortValue($left, $sort['field'], $balances, $reserveStandards, $withdrawalAmounts, $compliance); - $rightValue = $this->sortValue($right, $sort['field'], $balances, $reserveStandards, $withdrawalAmounts, $compliance); + private function sortRows( + Collection $rows, + array $sort, + array $balancesByDate, + array $balanceDays, + array $alerts, + array $reserveStandards, + array $withdrawalAmounts, + array $compliance, + ): Collection { + return $rows->sort(function (StoreReceivingAccount $left, StoreReceivingAccount $right) use ( + $sort, + $balancesByDate, + $balanceDays, + $alerts, + $reserveStandards, + $withdrawalAmounts, + $compliance, + ): int { + $leftValue = $this->sortValue($left, $sort['field'], $balancesByDate, $balanceDays, $alerts, $reserveStandards, $withdrawalAmounts, $compliance); + $rightValue = $this->sortValue($right, $sort['field'], $balancesByDate, $balanceDays, $alerts, $reserveStandards, $withdrawalAmounts, $compliance); $comparison = $this->compareSortValues($leftValue, $rightValue, $sort['direction']); return $comparison !== 0 @@ -229,13 +289,38 @@ class WithdrawalManagementReport } /** - * @param array $balances + * @param array> $balancesByDate + * @param list $balanceDays + * @param array $alerts * @param array $reserveStandards * @param array $withdrawalAmounts * @param array $compliance */ - private function sortValue(StoreReceivingAccount $row, string $field, array $balances, array $reserveStandards, array $withdrawalAmounts, array $compliance): mixed - { + private function sortValue( + StoreReceivingAccount $row, + string $field, + array $balancesByDate, + array $balanceDays, + array $alerts, + array $reserveStandards, + array $withdrawalAmounts, + array $compliance, + ): mixed { + $balanceFor = static function (string $dayKey) use ($row, $balancesByDate, $balanceDays): ?float { + $date = null; + foreach ($balanceDays as $day) { + if ($day['key'] === $dayKey) { + $date = $day['date']; + break; + } + } + $match = $date === null + ? null + : ($balancesByDate[(int) $row->id][$date] ?? null); + + return ($match['status'] ?? null) === 'matched' ? (float) $match['balance'] : null; + }; + return match ($field) { 'platform' => $row->store_platform, 'erp_store_name' => $row->erp_store_name, @@ -244,9 +329,10 @@ class WithdrawalManagementReport 'withdrawal_path' => $row->withdrawal_path, 'withdrawal_account' => $row->withdrawal_account_name, 'bank_card' => $row->bank_card_last_four, - 'balance' => ($balances[(int) $row->id]['status'] ?? null) === 'matched' - ? $balances[(int) $row->id]['balance'] - : null, + 'balance_two_days_ago' => $balanceFor('balance_two_days_ago'), + 'balance_yesterday' => $balanceFor('balance_yesterday'), + 'balance' => $balanceFor('balance'), + 'balance_alert' => isset($alerts[(int) $row->id]) ? 1 : 0, 'reserve_standard' => $reserveStandards[(int) $row->id] ?? null, 'withdrawal_amount' => $withdrawalAmounts[(int) $row->id] ?? null, 'compliance' => $compliance[(int) $row->id] ?? null, diff --git a/app/Business/WithdrawalManagementSpreadsheet.php b/app/Business/WithdrawalManagementSpreadsheet.php index da0df249..0b55ab60 100644 --- a/app/Business/WithdrawalManagementSpreadsheet.php +++ b/app/Business/WithdrawalManagementSpreadsheet.php @@ -40,6 +40,9 @@ class WithdrawalManagementSpreadsheet * @param array{ * rows:\Illuminate\Support\Collection, * balances:array, + * balancesByDate:array>, + * balanceDays:list, + * alerts:array, * reserveStandards:array,withdrawalAmounts:array,compliance:array, * balanceColumnLabel:string,balanceDate:string * } $report @@ -271,19 +274,28 @@ class WithdrawalManagementSpreadsheet private function workbook(array $report): Spreadsheet { - $headers = [...self::CONFIG_HEADERS, $report['balanceColumnLabel'], '预留标准', '提现金额', '是否达标']; + $balanceHeaders = array_map( + static fn (array $day): string => (string) $day['label'], + $report['balanceDays'], + ); + $headers = [...self::CONFIG_HEADERS, ...$balanceHeaders, '余额预警', '预留标准', '提现金额', '是否达标']; $spreadsheet = new Spreadsheet(); $sheet = $spreadsheet->getActiveSheet(); $sheet->setTitle('提现管理'); $this->writeHeader($sheet, $headers); - $this->sizeColumns($sheet, [14, 32, 20, 32, 16, 24, 22, 20, 18, 18, 14]); + $this->sizeColumns($sheet, [14, 32, 20, 32, 16, 24, 22, 20, 20, 20, 18, 18, 18, 14]); $rowNumber = 2; foreach ($report['rows'] as $row) { - $match = $report['balances'][(int) $row->id] ?? ['status' => 'not_ready', 'balance' => null]; $reserve = $report['reserveStandards'][(int) $row->id] ?? null; $withdrawalAmount = $report['withdrawalAmounts'][(int) $row->id] ?? null; $compliance = $report['compliance'][(int) $row->id] ?? null; + $alert = $report['alerts'][(int) $row->id] ?? null; + $hasCompleteAlertSeries = collect($report['balanceDays'])->every( + fn (array $day): bool => ( + $report['balancesByDate'][(int) $row->id][(string) $day['date']]['status'] ?? '' + ) === 'matched', + ); $textValues = [ 1 => (string) ($row->store_platform ?? ''), 2 => (string) ($row->erp_store_name ?? ''), @@ -297,22 +309,33 @@ class WithdrawalManagementSpreadsheet $sheet->setCellValueExplicit([$column, $rowNumber], $value, DataType::TYPE_STRING); } - if ($match['status'] === 'matched') { - $sheet->setCellValue([8, $rowNumber], (float) $match['balance']); - } else { - $sheet->setCellValueExplicit([8, $rowNumber], $this->balanceStatus($match['status']), DataType::TYPE_STRING); + foreach ($report['balanceDays'] as $dayIndex => $day) { + $column = 8 + $dayIndex; + $match = $report['balancesByDate'][(int) $row->id][$day['date']] + ?? ['status' => 'not_ready', 'balance' => null]; + if ($match['status'] === 'matched') { + $sheet->setCellValue([$column, $rowNumber], (float) $match['balance']); + } else { + $sheet->setCellValueExplicit([$column, $rowNumber], $this->balanceStatus($match['status']), DataType::TYPE_STRING); + } } + + $sheet->setCellValueExplicit( + [11, $rowNumber], + $alert !== null ? '连续3天未变动' : ($hasCompleteAlertSeries ? '未触发' : '数据不足'), + DataType::TYPE_STRING, + ); if ($reserve === null) { - $sheet->setCellValueExplicit([9, $rowNumber], '待配置', DataType::TYPE_STRING); + $sheet->setCellValueExplicit([12, $rowNumber], '待配置', DataType::TYPE_STRING); } else { - $sheet->setCellValue([9, $rowNumber], $reserve); + $sheet->setCellValue([12, $rowNumber], $reserve); } if ($withdrawalAmount === null) { - $sheet->setCellValueExplicit([10, $rowNumber], '待接入', DataType::TYPE_STRING); + $sheet->setCellValueExplicit([13, $rowNumber], '待接入', DataType::TYPE_STRING); } else { - $sheet->setCellValue([10, $rowNumber], $withdrawalAmount); + $sheet->setCellValue([13, $rowNumber], $withdrawalAmount); } - $sheet->setCellValueExplicit([11, $rowNumber], match ($compliance) { + $sheet->setCellValueExplicit([14, $rowNumber], match ($compliance) { 'yes' => '是', 'no' => '否', default => '待核验', @@ -322,7 +345,9 @@ class WithdrawalManagementSpreadsheet if ($rowNumber > 2) { $sheet->getStyle('H2:J' . ($rowNumber - 1))->getNumberFormat()->setFormatCode('#,##0.00'); + $sheet->getStyle('L2:M' . ($rowNumber - 1))->getNumberFormat()->setFormatCode('#,##0.00'); $sheet->getStyle('H2:J' . ($rowNumber - 1))->getAlignment()->setHorizontal(Alignment::HORIZONTAL_RIGHT); + $sheet->getStyle('L2:M' . ($rowNumber - 1))->getAlignment()->setHorizontal(Alignment::HORIZONTAL_RIGHT); } $sheet->getStyle('C2:C' . max(2, $rowNumber - 1))->getNumberFormat()->setFormatCode('@'); $sheet->getStyle('G2:G' . max(2, $rowNumber - 1))->getNumberFormat()->setFormatCode('@'); diff --git a/app/Console/Commands/ImportJushuitanShopBalances.php b/app/Console/Commands/ImportJushuitanShopBalances.php index f8292e56..32a8ad6d 100644 --- a/app/Console/Commands/ImportJushuitanShopBalances.php +++ b/app/Console/Commands/ImportJushuitanShopBalances.php @@ -2,6 +2,8 @@ namespace App\Console\Commands; +use App\Business\WithdrawalBalanceAlertSynchronizer; +use App\Models\Business\StoreReceivingAccount; use Illuminate\Console\Command; use Modules\EcommerceFinance\Services\JushuitanShopBalanceImporter; use Throwable; @@ -16,7 +18,10 @@ class ImportJushuitanShopBalances extends Command protected $description = '校验并导入聚水潭平台原始账单中的店铺余额快照'; - public function handle(JushuitanShopBalanceImporter $importer): int + public function handle( + JushuitanShopBalanceImporter $importer, + WithdrawalBalanceAlertSynchronizer $alertSynchronizer, + ): int { try { $result = $importer->import( @@ -26,6 +31,7 @@ class ImportJushuitanShopBalances extends Command (bool) $this->option('dry-run'), ); } catch (Throwable $exception) { + report($exception); $this->error($exception->getMessage()); return self::FAILURE; @@ -39,6 +45,34 @@ class ImportJushuitanShopBalances extends Command $result['replaced'], )); + if (! $result['dry_run']) { + $companyIds = StoreReceivingAccount::withoutGlobalScopes() + ->whereNull('deleted_at') + ->distinct() + ->pluck('company_id') + ->filter() + ->map(static fn (mixed $id): int => (int) $id) + ->unique() + ->values(); + + foreach ($companyIds as $companyId) { + try { + $alerts = $alertSynchronizer->syncLatest($companyId); + if ($alerts['created'] > 0 || $alerts['updated'] > 0) { + $this->info(sprintf( + '余额预警待办:公司 %d 新增 %d 条、更新 %d 条。', + $companyId, + $alerts['created'], + $alerts['updated'], + )); + } + } catch (Throwable $exception) { + report($exception); + $this->warn("余额预警待办生成失败(公司 {$companyId}),余额快照已保留。"); + } + } + } + return self::SUCCESS; } } diff --git a/app/Http/Controllers/Business/Pages.php b/app/Http/Controllers/Business/Pages.php index fae9124d..ce11ec1a 100644 --- a/app/Http/Controllers/Business/Pages.php +++ b/app/Http/Controllers/Business/Pages.php @@ -24,6 +24,7 @@ use App\Models\Business\CourierQuoteVersion; use App\Models\Business\CourierSigningSubject; use App\Models\Business\InstallationDetail; use App\Models\Business\InventoryWarehouseNameMapping; +use App\Models\Business\Todo; use App\Business\CourierSurchargeAuditLogRepository; use App\Business\CourierSurchargeQuoteRepository; use App\Business\CourierSurchargeTypeRepository; @@ -4126,6 +4127,7 @@ class Pages extends Controller 'shipping-data' => $this->contentForShippingData(), 'return-pickup-management' => $this->contentForReturnPickupManagement(), 'withdrawal-management' => $this->contentForWithdrawalManagement(), + 'todos' => $this->contentForTodos(), 'bill-offset' => app(\Modules\EcommerceFinance\View\BillOffsetPageData::class)->page(), 'account-mapping' => $this->contentForAccountMapping(), 'ledger' => app(\Modules\EcommerceFinance\View\FinanceLedgerPageData::class)->page($tab), @@ -5101,6 +5103,9 @@ class Pages extends Controller 'withdrawalPaths' => $report['paths'], 'withdrawalStoreEntities' => $report['storeEntities'], 'withdrawalBalances' => $report['balances'], + 'withdrawalBalancesByDate' => $report['balancesByDate'], + 'withdrawalBalanceDays' => $report['balanceDays'], + 'withdrawalAlerts' => $report['alerts'], 'withdrawalReserveStandards' => $report['reserveStandards'], 'withdrawalAmounts' => $report['withdrawalAmounts'], 'withdrawalCompliance' => $report['compliance'], @@ -5112,6 +5117,29 @@ class Pages extends Controller ]; } + /** + * 待办事项:优先展示业务系统生成的待办;为空时继续使用目录页的字段说明。 + */ + protected function contentForTodos(): ?array + { + $query = Todo::query()->orderByRaw( + "CASE WHEN status IN ('已完成', '已关闭') THEN 1 ELSE 0 END" + )->orderByDesc('created_at')->orderByDesc('id'); + + if (! $query->exists()) { + return null; + } + + $page = max((int) request()->query('page', 1), 1); + $items = $query->paginate(50, ['*'], 'page', $page)->withQueryString(); + + return [ + 'view' => 'business.pages.todos', + 'items' => $items, + 'title' => '待办事项', + ]; + } + /** * 快递附加费报价维护。 * diff --git a/modules/EcommerceFinance/Repositories/JushuitanShopBalanceRepository.php b/modules/EcommerceFinance/Repositories/JushuitanShopBalanceRepository.php index bedb1218..69fbba5b 100644 --- a/modules/EcommerceFinance/Repositories/JushuitanShopBalanceRepository.php +++ b/modules/EcommerceFinance/Repositories/JushuitanShopBalanceRepository.php @@ -26,26 +26,49 @@ class JushuitanShopBalanceRepository */ public function matchesForStores(iterable $stores, string $balanceDate): array { - $stores = is_array($stores) ? $stores : iterator_to_array($stores); + $matches = $this->matchesForStoresOnDates($stores, [$balanceDate]); + + return array_map( + static fn (array $dates): array => $dates[$balanceDate], + $matches, + ); + } + + /** + * @param iterable $stores + * @param list $balanceDates + * @return array> + */ + public function matchesForStoresOnDates(iterable $stores, array $balanceDates): array + { + $stores = is_array($stores) ? array_values($stores) : iterator_to_array($stores, false); + $balanceDates = array_values(array_unique(array_filter( + $balanceDates, + static fn (mixed $date): bool => is_string($date) && $date !== '', + ))); $result = []; foreach ($stores as $store) { - $result[(int) $store->id] = $this->emptyMatch('not_ready'); + $result[(int) $store->id] = array_fill_keys( + $balanceDates, + $this->emptyMatch('not_ready'), + ); } - if (! $this->hasTable()) { + if ($balanceDates === [] || ! $this->hasTable()) { return $result; } - $sourceRows = $this->connection->table(self::TABLE) - ->where('balance_date', $balanceDate) + $sourceRowsByDate = $this->connection->table(self::TABLE) + ->whereIn('balance_date', $balanceDates) ->orderBy('id') ->get([ - 'id', 'source_platform', 'store_name', 'normalized_store_name', + 'id', 'balance_date', 'source_platform', 'store_name', 'normalized_store_name', 'platform_account_id', 'balance', - ]); + ]) + ->groupBy(static fn (object $row): string => substr((string) $row->balance_date, 0, 10)); - if ($sourceRows->isEmpty()) { + if ($sourceRowsByDate->isEmpty()) { return $result; } @@ -55,44 +78,46 @@ class JushuitanShopBalanceRepository self::normalizeStoreName((string) ($store->erp_store_name ?? '')), ]))); - $matches = $sourceRows->filter( - static fn (object $row): bool => in_array((string) $row->normalized_store_name, $names, true), - )->values(); - - if ($matches->isEmpty()) { - $result[(int) $store->id] = $this->emptyMatch('unmatched'); - continue; - } - - $targetPlatform = self::platformKey((string) ($store->store_platform ?? '')); - if ($targetPlatform !== null) { - $matches = $matches->filter( - static function (object $row) use ($targetPlatform): bool { - $sourcePlatform = self::platformKey((string) ($row->source_platform ?? '')) - ?? self::platformKey((string) self::sourcePlatform((string) ($row->store_name ?? ''))); - - return $sourcePlatform !== null && $sourcePlatform === $targetPlatform; - }, + foreach ($balanceDates as $balanceDate) { + $matches = ($sourceRowsByDate[$balanceDate] ?? collect())->filter( + static fn (object $row): bool => in_array((string) $row->normalized_store_name, $names, true), )->values(); if ($matches->isEmpty()) { - $result[(int) $store->id] = $this->emptyMatch('unmatched'); + $result[(int) $store->id][$balanceDate] = $this->emptyMatch('unmatched'); continue; } - } - if ($matches->count() !== 1) { - $result[(int) $store->id] = $this->emptyMatch('ambiguous'); - continue; - } + $targetPlatform = self::platformKey((string) ($store->store_platform ?? '')); + if ($targetPlatform !== null) { + $matches = $matches->filter( + static function (object $row) use ($targetPlatform): bool { + $sourcePlatform = self::platformKey((string) ($row->source_platform ?? '')) + ?? self::platformKey((string) self::sourcePlatform((string) ($row->store_name ?? ''))); - $match = $matches->first(); - $result[(int) $store->id] = [ - 'status' => 'matched', - 'balance' => (float) $match->balance, - 'source_store_name' => (string) $match->store_name, - 'source_platform' => $match->source_platform === null ? null : (string) $match->source_platform, - ]; + return $sourcePlatform !== null && $sourcePlatform === $targetPlatform; + }, + )->values(); + + if ($matches->isEmpty()) { + $result[(int) $store->id][$balanceDate] = $this->emptyMatch('unmatched'); + continue; + } + } + + if ($matches->count() !== 1) { + $result[(int) $store->id][$balanceDate] = $this->emptyMatch('ambiguous'); + continue; + } + + $match = $matches->first(); + $result[(int) $store->id][$balanceDate] = [ + 'status' => 'matched', + 'balance' => (float) $match->balance, + 'source_store_name' => (string) $match->store_name, + 'source_platform' => $match->source_platform === null ? null : (string) $match->source_platform, + ]; + } } return $result; diff --git a/resources/views/business/pages/fund-management/withdrawal-management.blade.php b/resources/views/business/pages/fund-management/withdrawal-management.blade.php index fccbf927..4a937157 100644 --- a/resources/views/business/pages/fund-management/withdrawal-management.blade.php +++ b/resources/views/business/pages/fund-management/withdrawal-management.blade.php @@ -25,11 +25,21 @@ ['key' => 'withdrawal_path', 'label' => '提现路径', 'numeric' => false, 'title' => null], ['key' => 'withdrawal_account', 'label' => '提现账户名称', 'numeric' => false, 'title' => null], ['key' => 'bank_card', 'label' => '银行卡号', 'numeric' => false, 'title' => null], - ['key' => 'balance', 'label' => $withdrawalBalanceColumnLabel, 'numeric' => true, 'title' => '余额数据日期:' . $withdrawalBalanceDate], + ...array_map( + static fn (array $day): array => [ + 'key' => $day['key'], + 'label' => $day['label'], + 'numeric' => true, + 'title' => '余额数据日期:' . $day['date'], + ], + $withdrawalBalanceDays, + ), + ['key' => 'balance_alert', 'label' => '余额预警', 'numeric' => false, 'title' => '连续3个自然日余额一致且高于预留标准'], ['key' => 'reserve_standard', 'label' => '预留标准', 'numeric' => true, 'title' => null], ['key' => 'withdrawal_amount', 'label' => '提现金额', 'numeric' => true, 'title' => null], ['key' => 'compliance', 'label' => '是否达标', 'numeric' => false, 'title' => null], ]; + $withdrawalAlertCount = count($withdrawalAlerts); @endphp
@@ -45,6 +55,9 @@ @if ($withdrawalBalanceSummary['ambiguous'] > 0) 多条匹配 {{ number_format($withdrawalBalanceSummary['ambiguous']) }} 条 @endif + @if ($withdrawalAlertCount > 0) + 连续3天未变动 {{ number_format($withdrawalAlertCount) }} 家 + @endif
@@ -127,7 +140,7 @@ data-finance-table-name="withdrawal-management-v4" data-finance-table-native data-withdrawal-table - data-column-storage-key="caiwu.withdrawal.columns.v1.{{ auth()->id() ?? 'guest' }}" + data-column-storage-key="caiwu.withdrawal.columns.v2.{{ auth()->id() ?? 'guest' }}" > @@ -167,20 +180,32 @@ ? '**** ' . $row->bank_card_last_four : '未配置'; $balanceMatch = $withdrawalBalances[(int) $row->id] ?? ['status' => 'not_ready', 'balance' => null]; - $balanceText = match ($balanceMatch['status']) { - 'matched' => number_format((float) $balanceMatch['balance'], 2), - 'ambiguous' => '多条匹配', - 'unmatched' => '未匹配', - default => '待同步', - }; - $balanceTitle = $balanceMatch['status'] === 'matched' - ? '聚水潭店铺:' . $balanceMatch['source_store_name'] . ';余额日期:' . $withdrawalBalanceDate - : '余额日期:' . $withdrawalBalanceDate; + $balanceCells = []; + foreach ($withdrawalBalanceDays as $day) { + $dayMatch = $withdrawalBalancesByDate[(int) $row->id][$day['date']] + ?? ['status' => 'not_ready', 'balance' => null]; + $balanceCells[$day['key']] = [ + 'matched' => $dayMatch['status'] === 'matched', + 'text' => match ($dayMatch['status']) { + 'matched' => number_format((float) $dayMatch['balance'], 2), + 'ambiguous' => '多条匹配', + 'unmatched' => '未匹配', + default => '待同步', + }, + 'title' => $dayMatch['status'] === 'matched' + ? '聚水潭店铺:' . $dayMatch['source_store_name'] . ';余额日期:' . $day['date'] + : '余额日期:' . $day['date'], + ]; + } $reserveStandard = $withdrawalReserveStandards[(int) $row->id] ?? null; $withdrawalAmount = $withdrawalAmounts[(int) $row->id] ?? null; $compliance = $withdrawalCompliance[(int) $row->id] ?? null; + $alert = $withdrawalAlerts[(int) $row->id] ?? null; + $hasCompleteAlertSeries = collect($balanceCells)->every( + static fn (array $cell): bool => $cell['matched'], + ); @endphp - + $alert !== null])> {{ $row->store_platform ?: '未配置' }} {{ $row->erp_store_name ?: '-' }} {{ $row->store_id ?: '-' }} @@ -188,7 +213,25 @@ {{ $row->withdrawal_path ?: '未配置' }} {{ $row->withdrawal_account_name ?: '未配置' }} {{ $bankCard }} - {{ $balanceText }} + @foreach ($withdrawalBalanceDays as $day) + {{ $balanceCells[$day['key']]['text'] }} + @endforeach + + @if ($alert !== null) + + + 连续3天未变动 + + @elseif (! $hasCompleteAlertSeries) + 数据不足 + @else + 未触发 + @endif + {{ $reserveStandard === null ? '待配置' : number_format($reserveStandard, 2) }} {{ $withdrawalAmount === null ? '待接入' : number_format($withdrawalAmount, 2) }} @@ -203,7 +246,7 @@ @empty - +
暂无店铺收款账户,请先在店铺收款账户中维护。
@@ -359,6 +402,12 @@ .withdrawal-management .biz-btn-primary:hover { background: #6f5429; } .withdrawal-management__heading { display:flex; min-width:0; flex-direction:column; gap:.55rem; } .withdrawal-management__summary { display:flex; flex-wrap:wrap; gap:.35rem; } + .withdrawal-alert-summary { border-color:#d8b15d; background:#fff7df; color:#935f07; } + .withdrawal-management tbody tr.is-withdrawal-warning > td { background:#fffdf5; } + .withdrawal-alert-badge { display:inline-flex; align-items:center; gap:.25rem; white-space:nowrap; color:#935f07; font-size:.75rem; font-weight:600; text-decoration:none; } + .withdrawal-alert-badge:hover, .withdrawal-alert-badge:focus-visible { color:#6e4603; text-decoration:underline; outline:none; } + .withdrawal-alert-badge .material-icons-outlined { font-size:1rem; } + .withdrawal-alert-none { color:#595959; font-size:.75rem; } .withdrawal-management__actions { display:flex; flex:0 0 auto; align-items:center; gap:.5rem; margin-left:auto; } .withdrawal-management__actions .biz-btn { min-width:5.5rem; justify-content:center; } .withdrawal-table-head { display:flex; min-width:max-content; align-items:center; justify-content:space-between; gap:.35rem; } diff --git a/resources/views/business/pages/todos.blade.php b/resources/views/business/pages/todos.blade.php index 676e4cb4..5ef673a0 100644 --- a/resources/views/business/pages/todos.blade.php +++ b/resources/views/business/pages/todos.blade.php @@ -1,36 +1,82 @@ -
- {{ $title ?? '待办事项' }} -
-
- - - - +
+
+
+
+ + 待办清单 +
+

集中展示系统预警和人工待办,保持事项从触发到处理的完整记录。

+
+ {{ number_format($items->total()) }} 条 +
+ +
+
序号
+ + + - - - - - @foreach($items as $item) - - - - - - - - - - @endforeach - @if($items->isEmpty()) - - @endif - -
序号 标题 分类 优先级 状态 截止日期 描述
{{ $loop->iteration }}{{ $item->title }}{{ $item->category }}{{ $item->priority }}{{ $item->status }}{{ $item->due_date }}{{ $item->description }}
暂无数据
-
\ No newline at end of file + + + @forelse ($items as $item) + + {{ $items->firstItem() + $loop->index }} + {{ $item->title }} + {{ $item->category ?: '-' }} + + $item->priority === '高'])> + {{ $item->priority ?: '-' }} + + + + in_array($item->status, ['已完成', '已关闭'], true)])> + {{ $item->status ?: '待处理' }} + + + {{ $item->due_date ?: '-' }} + {!! nl2br(e($item->description ?: '-')) !!} + + @empty + + +
+ +
暂无待办事项。
+
+ + + @endforelse + + +
+ + @if ($items->hasPages()) +
+ 共 {{ number_format($items->total()) }} 条 · 第 {{ $items->currentPage() }} / {{ $items->lastPage() }} 页 + + @if ($items->onFirstPage()) + ‹ 上一页 + @else + ‹ 上一页 + @endif + @if ($items->hasMorePages()) + 下一页 › + @else + 下一页 › + @endif + +
+ @endif +
+ + diff --git a/tests/Feature/Business/ManagementCrudTest.php b/tests/Feature/Business/ManagementCrudTest.php index 63548559..06068b9e 100644 --- a/tests/Feature/Business/ManagementCrudTest.php +++ b/tests/Feature/Business/ManagementCrudTest.php @@ -150,7 +150,10 @@ class ManagementCrudTest extends FeatureTestCase ->assertSeeText('TM-TEST-1001') ->assertSeeText('测试提现账户') ->assertSeeText('**** 4321') - ->assertSeeText('9月13日店铺余额') + ->assertSeeText('9月11日店铺余额') + ->assertSeeText('9月12日店铺余额') + ->assertSeeText('9月10日店铺余额') + ->assertSeeText('余额预警') ->assertSeeText('19,895.01') ->assertSeeText('余额源 1 条') ->assertSeeText('已显示余额 1 条') @@ -186,7 +189,10 @@ class ManagementCrudTest extends FeatureTestCase 'data-withdrawal-column="withdrawal_path"', 'data-withdrawal-column="withdrawal_account"', 'data-withdrawal-column="bank_card"', + 'data-withdrawal-column="balance_two_days_ago"', + 'data-withdrawal-column="balance_yesterday"', 'data-withdrawal-column="balance"', + 'data-withdrawal-column="balance_alert"', 'data-withdrawal-column="reserve_standard"', 'data-withdrawal-column="withdrawal_amount"', 'data-withdrawal-column="compliance"', diff --git a/tests/Feature/Business/WithdrawalBalanceAlertTest.php b/tests/Feature/Business/WithdrawalBalanceAlertTest.php new file mode 100644 index 00000000..a1437a6e --- /dev/null +++ b/tests/Feature/Business/WithdrawalBalanceAlertTest.php @@ -0,0 +1,219 @@ + */ + private array $temporaryFiles = []; + + protected function setUp(): void + { + parent::setUp(); + + $this->user->attachPermission( + Permission::where('name', 'read-admin-panel')->firstOrFail(), + ); + $this->withoutMiddleware(ThrottleRequests::class); + } + + public function test_three_unchanged_balances_above_reserve_show_warning_and_create_one_todo(): void + { + Carbon::setTestNow('2026-10-24 10:00:00'); + + try { + $store = StoreReceivingAccount::query()->create([ + 'company_id' => $this->company->id, + 'store_platform' => '京东', + 'erp_store_name' => '京东-连续余额测试店', + 'shop_name' => '京东-连续余额测试店', + 'store_entity_name' => '连续余额测试主体', + 'store_id' => 'JD-ALERT-001', + ]); + + foreach (['2026-10-21', '2026-10-22', '2026-10-23'] as $date) { + $this->insertBalanceSnapshot($date, '京东-连续余额测试店', 8000); + } + $sync = app(WithdrawalBalanceAlertSynchronizer::class); + $firstSync = $sync->syncForDate((int) $this->company->id, '2026-10-23'); + $secondSync = $sync->syncForDate((int) $this->company->id, '2026-10-23'); + $this->assertSame(1, $firstSync['created']); + $this->assertSame(0, $firstSync['updated']); + $this->assertSame(0, $secondSync['created']); + $this->assertSame(0, $secondSync['updated']); + + $url = route('business.pages.show', [ + 'company_id' => $this->company->id, + 'section' => 'fund-management', + 'page' => 'withdrawal-management', + 'balance_date' => '2026-10-23', + 'platform' => '京东', + ]); + $this->loginAs()->get($url) + ->assertOk() + ->assertSeeText('10月21日店铺余额') + ->assertSeeText('10月22日店铺余额') + ->assertSeeText('10月23日店铺余额') + ->assertSeeText('已显示余额 1 条') + ->assertSeeText('连续3天未变动') + ->assertSeeText('8,000.00'); + + $title = '提现余额连续3天未变动|京东|京东-连续余额测试店'; + $todo = Todo::withoutGlobalScopes()->where('company_id', $this->company->id)->where('title', $title)->first(); + $this->assertNotNull($todo); + $this->assertSame('提现余额预警', $todo->category); + $this->assertSame('高', $todo->priority); + $this->assertSame('待处理', $todo->status); + $this->assertStringContainsString('2026-10-21 至 2026-10-23', (string) $todo->description); + $this->assertStringContainsString((string) $store->store_entity_name, (string) $todo->description); + + $this->get($url)->assertOk(); + $this->assertSame( + 1, + Todo::withoutGlobalScopes() + ->where('company_id', $this->company->id) + ->where('category', '提现余额预警') + ->count(), + ); + + $this->get(route('business.pages.show', [ + 'company_id' => $this->company->id, + 'section' => 'other', + 'page' => 'todos', + ])) + ->assertOk() + ->assertSeeText($title) + ->assertSeeText('提现余额预警') + ->assertSeeText('连续余额测试主体'); + } finally { + Carbon::setTestNow(); + } + } + + public function test_alert_requires_three_matched_days_an_exact_unchanged_value_and_a_strict_excess(): void + { + Carbon::setTestNow('2026-10-24 10:00:00'); + + try { + foreach (['等于预留标准店', '余额发生变动店', '缺少一天店'] as $name) { + StoreReceivingAccount::query()->create([ + 'company_id' => $this->company->id, + 'store_platform' => '京东', + 'erp_store_name' => '京东-' . $name, + 'shop_name' => '京东-' . $name, + 'store_entity_name' => '预警边界测试主体', + ]); + } + + foreach (['2026-10-21' => 5000, '2026-10-22' => 5000, '2026-10-23' => 5000] as $date => $balance) { + $this->insertBalanceSnapshot($date, '京东-等于预留标准店', $balance); + } + foreach (['2026-10-21' => 8000, '2026-10-22' => 8000, '2026-10-23' => 7999.99] as $date => $balance) { + $this->insertBalanceSnapshot($date, '京东-余额发生变动店', $balance); + } + foreach (['2026-10-22' => 8000, '2026-10-23' => 8000] as $date => $balance) { + $this->insertBalanceSnapshot($date, '京东-缺少一天店', $balance); + } + $sync = app(WithdrawalBalanceAlertSynchronizer::class)->syncForDate( + (int) $this->company->id, + '2026-10-23', + ); + $this->assertSame(0, $sync['created']); + + $this->loginAs()->get(route('business.pages.show', [ + 'company_id' => $this->company->id, + 'section' => 'fund-management', + 'page' => 'withdrawal-management', + 'balance_date' => '2026-10-23', + 'platform' => '京东', + ]))->assertOk()->assertSeeText('未触发')->assertSeeText('数据不足'); + + $this->assertSame(0, DB::table('business_todos')->count()); + } finally { + Carbon::setTestNow(); + } + } + + public function test_successful_balance_import_runs_alert_generation_automatically(): void + { + Carbon::setTestNow('2026-10-24 10:00:00'); + + try { + StoreReceivingAccount::query()->create([ + 'company_id' => $this->company->id, + 'store_platform' => '京东', + 'erp_store_name' => '京东-导入预警测试店', + 'shop_name' => '京东-导入预警测试店', + 'store_entity_name' => '导入预警测试主体', + ]); + $this->insertBalanceSnapshot('2026-10-21', '京东-导入预警测试店', 7000); + $this->insertBalanceSnapshot('2026-10-22', '京东-导入预警测试店', 7000); + + $path = sys_get_temp_dir() . '/jst-balance-alert-' . uniqid('', true) . '.json'; + $this->temporaryFiles[] = $path; + file_put_contents($path, json_encode([ + 'fldHeads' => [ + ['title' => '店铺', 'fld' => '店铺'], + ['title' => '平台实际期末余额', 'fld' => '平台实际期末余额'], + ], + 'dataList' => [ + ['店铺' => '京东-导入预警测试店', '平台实际期末余额' => 7000], + ], + ], JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR)); + + $this->artisan('business:import-jushuitan-shop-balances', [ + 'path' => $path, + '--balance-date' => '2026-10-23', + '--scope' => 'jd-wallet', + ])->assertExitCode(0); + $this->assertDatabaseHas('business_todos', [ + 'company_id' => $this->company->id, + 'title' => '提现余额连续3天未变动|京东|京东-导入预警测试店', + 'category' => '提现余额预警', + 'status' => '待处理', + ]); + } finally { + Carbon::setTestNow(); + } + } + + protected function tearDown(): void + { + foreach ($this->temporaryFiles as $path) { + if (is_file($path)) { + unlink($path); + } + } + + parent::tearDown(); + } + + private function insertBalanceSnapshot(string $date, string $storeName, float $balance): void + { + DB::connection('business_mirror')->table(JushuitanShopBalanceRepository::TABLE)->insert([ + 'balance_date' => $date, + 'source_scope' => 'jd-wallet', + 'source_platform' => '京东', + 'store_name' => $storeName, + 'normalized_store_name' => JushuitanShopBalanceRepository::normalizeStoreName($storeName), + 'platform_account_id' => 'fixture-' . $storeName, + 'balance' => $balance, + 'source_row_key' => hash('sha256', $date . '|' . $storeName), + 'source_file_name' => 'fixture.xlsx', + 'source_sha256' => hash('sha256', 'fixture-' . $date . '-' . $storeName), + 'imported_at' => now(), + 'created_at' => now(), + 'updated_at' => now(), + ]); + } +} diff --git a/tests/Feature/Business/WithdrawalManagementImportExportTest.php b/tests/Feature/Business/WithdrawalManagementImportExportTest.php index ce98d369..e030f237 100644 --- a/tests/Feature/Business/WithdrawalManagementImportExportTest.php +++ b/tests/Feature/Business/WithdrawalManagementImportExportTest.php @@ -141,11 +141,14 @@ class WithdrawalManagementImportExportTest extends FeatureTestCase $sheet = $spreadsheet->getActiveSheet(); $this->assertSame([ ...self::HEADERS, - now()->format('n月j日') . '店铺余额', + now()->subDays(3)->format('n月j日') . '店铺余额', + now()->subDays(2)->format('n月j日') . '店铺余额', + now()->subDay()->format('n月j日') . '店铺余额', + '余额预警', '预留标准', '提现金额', '是否达标', - ], $sheet->rangeToArray('A1:K1')[0]); + ], $sheet->rangeToArray('A1:N1')[0]); $this->assertSame(2, $sheet->getHighestDataRow()); $this->assertSame('导出目标店', $sheet->getCell('B2')->getValue()); $this->assertSame('00001234', $sheet->getCell('C2')->getValue()); @@ -368,7 +371,14 @@ class WithdrawalManagementImportExportTest extends FeatureTestCase $mirror = Mockery::mock(JushuitanShopBalanceRepository::class); $mirror->shouldReceive('latestBalanceDate')->andReturn('2026-09-23'); $mirror->shouldReceive('availableDates')->andReturn(['2026-09-23', '2026-09-22']); - $mirror->shouldReceive('matchesForStores')->andReturn($balances); + $historyDates = ['2026-09-21', '2026-09-22', '2026-09-23']; + $balancesByDate = []; + foreach (array_keys($balances) as $storeId) { + foreach ($historyDates as $date) { + $balancesByDate[$storeId][$date] = $balances[$storeId]; + } + } + $mirror->shouldReceive('matchesForStoresOnDates')->andReturn($balancesByDate); $mirror->shouldReceive('countForDate')->andReturn(6); $this->app->instance(JushuitanShopBalanceRepository::class, $mirror); @@ -399,8 +409,8 @@ class WithdrawalManagementImportExportTest extends FeatureTestCase $values = []; for ($row = 2; $row <= $sheet->getHighestDataRow(); $row++) { $values[(string) $sheet->getCell("B{$row}")->getValue()] = [ - $sheet->getCell("J{$row}")->getValue(), - $sheet->getCell("K{$row}")->getValue(), + $sheet->getCell("M{$row}")->getValue(), + $sheet->getCell("N{$row}")->getValue(), ]; } $this->assertSame([5000.0, '是'], $values['达标测试店']); @@ -482,9 +492,10 @@ class WithdrawalManagementImportExportTest extends FeatureTestCase ])); $response->assertOk() - ->assertSeeText('10月20日店铺余额') + ->assertSeeText('10月14日店铺余额') + ->assertSeeText('10月15日店铺余额') ->assertSeeText('12,345.67') - ->assertDontSeeText('99,999.99') + ->assertSeeText('99,999.99') ->assertSee('余额日期:2026-10-15', false); $historyResponse->assertOk() -- 2.47.2