CaiWuXiTong/modules/EcommerceFinance/Repositories/JushuitanShopBalanceRepository.php
92451 37c11f4e8e fix: 提现管理店铺余额保留最近5天 (#349)
Co-authored-by: 92451 <924518135@qq.com>
Co-committed-by: 92451 <924518135@qq.com>
2026-10-16 19:00:49 +08:00

226 lines
7.7 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
namespace Modules\EcommerceFinance\Repositories;
use Carbon\CarbonInterface;
use Illuminate\Database\ConnectionInterface;
use Illuminate\Database\DatabaseManager;
use Illuminate\Support\Facades\Schema;
class JushuitanShopBalanceRepository
{
public const TABLE = 'jushuitan_shop_balance_snapshots';
/**
* 店铺余额快照按导入日期保留最近 5 个自然日。
* 页面在这 5 天内取最新余额日期,当天尚未导入时继续沿用最近一次数据。
*/
public const RETENTION_DAYS = 5;
private readonly ConnectionInterface $connection;
public function __construct(DatabaseManager $database)
{
$this->connection = $database->connection(
(string) config('upstream_mirrors.connection', 'business_mirror'),
);
}
/**
* @param iterable<object> $stores
* @return array<int,array{status:string,balance:?float,source_store_name:?string,source_platform:?string}>
*/
public function matchesForStores(iterable $stores, string $balanceDate, ?CarbonInterface $today = null): array
{
$stores = is_array($stores) ? $stores : iterator_to_array($stores);
$result = [];
foreach ($stores as $store) {
$result[(int) $store->id] = $this->emptyMatch('not_ready');
}
if (! $this->hasTable()) {
return $result;
}
$sourceRows = $this->connection->table(self::TABLE)
->where('balance_date', $balanceDate)
->where('imported_at', '>=', $this->retentionImportedFrom($today))
->orderBy('id')
->get([
'id', 'source_platform', 'store_name', 'normalized_store_name',
'platform_account_id', 'balance',
]);
if ($sourceRows->isEmpty()) {
return $result;
}
foreach ($stores as $store) {
$names = array_values(array_unique(array_filter([
self::normalizeStoreName((string) ($store->shop_name ?? '')),
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;
},
)->values();
if ($matches->isEmpty()) {
$result[(int) $store->id] = $this->emptyMatch('unmatched');
continue;
}
}
if ($matches->count() !== 1) {
$result[(int) $store->id] = $this->emptyMatch('ambiguous');
continue;
}
$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 $result;
}
public function countForDate(string $balanceDate, ?CarbonInterface $today = null): int
{
if (! $this->hasTable()) {
return 0;
}
return $this->connection->table(self::TABLE)
->where('balance_date', $balanceDate)
->where('imported_at', '>=', $this->retentionImportedFrom($today))
->count();
}
/**
* 取最近 5 个自然日内导入过的最新余额日期;没有可用导入时返回 null。
*/
public function latestDateInRetention(?CarbonInterface $today = null): ?string
{
if (! $this->hasTable()) {
return null;
}
$today = ($today ?? now())->copy()->startOfDay();
$latest = $this->connection->table(self::TABLE)
->where('imported_at', '>=', $this->retentionImportedFrom($today))
->where('balance_date', '<=', $today->toDateString())
->max('balance_date');
return $latest === null ? null : substr((string) $latest, 0, 10);
}
/**
* 清理导入时间早于最近 5 个自然日的快照,返回删除行数。
*/
public function pruneOutsideRetention(?CarbonInterface $today = null): int
{
if (! $this->hasTable()) {
return 0;
}
return $this->connection->table(self::TABLE)
->where('imported_at', '<', $this->retentionImportedFrom($today))
->delete();
}
public static function normalizeStoreName(string $value): string
{
$value = trim(str_replace(['-', '—', '–'], '-', $value));
$value = preg_replace(
'/^(支付宝|天猫|淘宝|阿里巴巴|1688|京东商城|京东|京喜小店|京喜|拼多多|抖音|快手|小红书|唯品会|得物|淘工厂)[\s\-_::]+/u',
'',
$value,
) ?? $value;
$value = preg_replace('/\s+/u', '', $value) ?? $value;
return mb_substr(mb_strtolower($value), 0, 255);
}
public static function sourcePlatform(string $storeName): ?string
{
if (preg_match(
'/^(支付宝|天猫|淘宝|阿里巴巴|1688|京东商城|京东|京喜小店|京喜|拼多多|抖音|快手|小红书|唯品会|得物|淘工厂)[\s\-—–_::]+/u',
trim($storeName),
$matches,
) === 1) {
return $matches[1];
}
return str_ends_with(mb_strtolower(trim($storeName)), '1688') ? '1688' : null;
}
private static function platformKey(string $platform): ?string
{
$platform = mb_strtolower(trim($platform));
foreach ([
'tmall' => ['天猫'],
'taobao' => ['淘宝'],
'1688' => ['1688', '阿里巴巴'],
'jd' => ['京东'],
'jingxi' => ['京喜'],
'pinduoduo' => ['拼多多'],
'douyin' => ['抖音', '头条放心购'],
'kuaishou' => ['快手'],
'xiaohongshu' => ['小红书'],
'vipshop' => ['唯品会'],
'dewu' => ['得物'],
'taogongchang' => ['淘工厂'],
] as $key => $aliases) {
foreach ($aliases as $alias) {
if (str_contains($platform, mb_strtolower($alias))) {
return $key;
}
}
}
return null;
}
/** @return array{status:string,balance:null,source_store_name:null,source_platform:null} */
private function emptyMatch(string $status): array
{
return [
'status' => $status,
'balance' => null,
'source_store_name' => null,
'source_platform' => null,
];
}
private function hasTable(): bool
{
return Schema::connection($this->connection->getName())->hasTable(self::TABLE);
}
private function retentionImportedFrom(?CarbonInterface $today = null): CarbonInterface
{
return ($today ?? now())->copy()->startOfDay()->subDays(self::RETENTION_DAYS - 1);
}
}