CaiWuXiTong/modules/EcommerceFinance/Services/StoreReceivingBalanceOverviewService.php
qiuxiang f5694567f2 feat: 新增店铺收款账户余额总览 (#1019)
聚合8个收款平台的月度期初、收入与支出,按期初加收入减支出计算期末余额,并支持月份切换和缺失数据保护。
2026-11-09 18:44:13 +08:00

207 lines
8.3 KiB
PHP

<?php
namespace Modules\EcommerceFinance\Services;
use Carbon\CarbonImmutable;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Modules\EcommerceFinance\Http\Controllers\AccountBills;
use Modules\EcommerceFinance\Http\Controllers\AlipayBills;
use Modules\EcommerceFinance\Http\Controllers\PinduoduoAccountBills;
use Modules\EcommerceFinance\Http\Controllers\TaobaoWechatAccountBills;
use Modules\EcommerceFinance\Repositories\PinduoduoSettlementBillRepository;
use Modules\EcommerceFinance\Repositories\StoreReceivingAlipayBillMirrorRepository;
use Modules\EcommerceFinance\Repositories\TaobaoWechatOpeningBalanceRepository;
use Modules\EcommerceFinance\Repositories\TaobaoWechatSettlementBillRepository;
use RuntimeException;
use Throwable;
class StoreReceivingBalanceOverviewService
{
/** @var array<int,array{slug:string,name:string}> */
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<string,mixed> */
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),
};
}
}