*/ public const PLATFORMS = [ ['slug' => 'taobao-alipay', 'name' => '淘系支付宝账户'], ['slug' => 'taobao-wechat', 'name' => '淘系微信账户'], ['slug' => 'pinduoduo', 'name' => '拼多多账户'], ['slug' => 'jd-wallet', 'name' => '京东网银钱包账户'], ['slug' => 'douyin', 'name' => '抖音账户'], ['slug' => 'vipshop', 'name' => '唯品会MP账户'], ['slug' => 'vipshop-jx', 'name' => '唯品会JX账户'], ['slug' => 'xiaohongshu', 'name' => '小红书账户'], ]; public function __construct( private readonly AlipayBills $alipayBills, private readonly TaobaoWechatAccountBills $taobaoWechatBills, private readonly PinduoduoAccountBills $pinduoduoBills, private readonly AccountBills $accountBills, ) {} /** @return array */ public function overview(string $month): array { $start = CarbonImmutable::createFromFormat('!Y-m', $month, 'Asia/Shanghai'); if (! $start instanceof CarbonImmutable || $start->format('Y-m') !== $month) { throw new RuntimeException('invalid_balance_overview_month'); } $periodStart = $start->startOfMonth()->format('Y-m-d'); $periodEnd = $start->endOfMonth()->format('Y-m-d'); $platforms = []; $availableTotals = [ 'opening_balance' => 0.0, 'income' => 0.0, 'expense' => 0.0, ]; $availableCount = 0; foreach (self::PLATFORMS as $definition) { try { $summary = $this->fetchPlatformSummary( $definition['slug'], $periodStart, $periodEnd, ); if ($summary['opening_balance'] === null) { throw new RuntimeException('平台期初余额未就绪'); } $availableCount++; $availableTotals['opening_balance'] += (float) $summary['opening_balance']; $availableTotals['income'] += (float) $summary['income']; $availableTotals['expense'] += (float) $summary['expense']; $platforms[] = [ 'slug' => $definition['slug'], 'name' => $definition['name'], 'available' => true, 'opening_balance' => round((float) $summary['opening_balance'], 2), 'income' => round((float) $summary['income'], 2), 'expense' => round((float) $summary['expense'], 2), 'balance' => round( (float) $summary['opening_balance'] + (float) $summary['income'] - (float) $summary['expense'], 2, ), 'error' => null, ]; } catch (Throwable $exception) { report($exception); $platforms[] = [ 'slug' => $definition['slug'], 'name' => $definition['name'], 'available' => false, 'opening_balance' => null, 'income' => null, 'expense' => null, 'balance' => null, 'error' => $this->safeErrorMessage($exception), ]; } } $complete = $availableCount === count(self::PLATFORMS); $openingBalance = $complete ? round($availableTotals['opening_balance'], 2) : null; $income = $complete ? round($availableTotals['income'], 2) : null; $expense = $complete ? round($availableTotals['expense'], 2) : null; return [ 'month' => $month, 'period' => [ 'start' => $periodStart, 'end' => $periodEnd, ], 'summary' => [ 'opening_balance' => $openingBalance, 'income' => $income, 'expense' => $expense, 'balance' => $complete ? round($openingBalance + $income - $expense, 2) : null, 'platform_count' => count(self::PLATFORMS), 'available_platform_count' => $availableCount, 'complete' => $complete, ], 'platforms' => $platforms, ]; } /** * @return array{opening_balance:float|null,income:float,expense:float} */ protected function fetchPlatformSummary(string $platform, string $start, string $end): array { $request = Request::create('/ecommerce-finance/account-bills/overview', 'GET', [ 'start' => $start, 'end' => $end, 'page' => 1, 'limit' => 1, 'sort' => 'occurred_at', 'dir' => 'desc', ], [], [], ['HTTP_ACCEPT' => 'application/json']); $response = match ($platform) { 'taobao-alipay' => $this->alipayBills->index( $request, app(StoreReceivingAlipayBillMirrorRepository::class), ), 'taobao-wechat' => $this->taobaoWechatBills->index( $request, app(TaobaoWechatSettlementBillRepository::class), app(TaobaoWechatOpeningBalanceRepository::class), ), 'pinduoduo' => $this->pinduoduoBills->index( $request, app(PinduoduoSettlementBillRepository::class), ), 'jd-wallet', 'douyin', 'vipshop', 'vipshop-jx', 'xiaohongshu' => $this->accountBills->index( $request, $platform, ), default => throw new RuntimeException('unsupported_balance_overview_platform'), }; return $this->summaryFromResponse($response); } /** @return array{opening_balance:float|null,income:float,expense:float} */ private function summaryFromResponse(JsonResponse $response): array { $payload = json_decode($response->getContent(), true, 512, JSON_THROW_ON_ERROR); if ($response->getStatusCode() !== 200 || ! is_array($payload)) { throw new RuntimeException((string) ($payload['message'] ?? '平台账单暂时无法读取')); } $summary = $payload['summary'] ?? null; if (! is_array($summary) || ! array_key_exists('opening_balance', $summary) || ! is_numeric($summary['income'] ?? null) || ! is_numeric($summary['expense'] ?? null) ) { throw new RuntimeException('平台账单汇总数据不完整'); } return [ 'opening_balance' => is_numeric($summary['opening_balance']) ? round((float) $summary['opening_balance'], 2) : null, 'income' => round((float) $summary['income'], 2), 'expense' => round((float) $summary['expense'], 2), ]; } private function safeErrorMessage(Throwable $exception): string { $message = trim($exception->getMessage()); return match ($message) { '平台期初余额未就绪' => $message, '平台账单汇总数据不完整' => $message, 'invalid_balance_overview_month' => '月份格式无效', 'unsupported_balance_overview_platform' => '平台类型无效', '' => '平台账单暂时无法读取', default => mb_substr($message, 0, 160), }; } }