新佰广1688、1688xxx 店铺名不带「阿里巴巴」前缀,之前落到页签默认平台(淘系支付宝=淘宝), 1688 系账单项目匹配不到总账备注;新增两条前缀映射(1688、新佰广 → 阿里巴巴)并补单测断言。
307 lines
10 KiB
PHP
307 lines
10 KiB
PHP
<?php
|
||
|
||
namespace Modules\EcommerceFinance\Services;
|
||
|
||
use Illuminate\Support\Facades\Cache;
|
||
use Illuminate\Support\Facades\Schema;
|
||
use Throwable;
|
||
|
||
/**
|
||
* 「总账备注」取值:按(平台 + 账单项目)匹配 ec_account_ledger_remarks。
|
||
*
|
||
* 店铺收款账户账单各页签的账单项目名称不一致,平台口径以店铺名前缀为准
|
||
* (如「抖音-xxx」「天猫-xxx」),页签默认平台只作为兜底,避免淘系页签里
|
||
* 天猫/淘宝/阿里巴巴三类项目混用同一份映射。
|
||
*/
|
||
class AccountLedgerRemarkMap
|
||
{
|
||
public const CACHE_KEY = 'ec_account_ledger_remarks_map_v1';
|
||
|
||
/** 页签默认平台(店铺名没有平台前缀时使用)。 */
|
||
private const TAB_PLATFORM = [
|
||
'taobao-alipay' => '淘宝',
|
||
'taobao-wechat' => '淘宝',
|
||
'pinduoduo' => '拼多多',
|
||
'jd-transactions' => '京东',
|
||
'jingxi' => '京喜',
|
||
'jd-wallet' => '京东',
|
||
'douyin' => '抖音',
|
||
'dewu' => '得物',
|
||
'vipshop' => '唯品会MP',
|
||
'vipshop-jx' => '唯品会jx',
|
||
];
|
||
|
||
/**
|
||
* 页签 → 该页签可能出现的平台。
|
||
*
|
||
* 大部分页签只有一个平台;淘系支付宝与淘系微信账户同一页签内混有
|
||
* 天猫/淘宝/阿里巴巴的原始店铺,需要按店铺名前缀再分平台。
|
||
*/
|
||
private const TAB_PLATFORMS = [
|
||
'taobao-alipay' => ['淘宝', '天猫', '阿里巴巴'],
|
||
'taobao-wechat' => ['淘宝', '天猫'],
|
||
'pinduoduo' => ['拼多多'],
|
||
'jd-transactions' => ['京东'],
|
||
'jingxi' => ['京喜'],
|
||
'jd-wallet' => ['京东'],
|
||
'douyin' => ['抖音'],
|
||
'dewu' => ['得物'],
|
||
'vipshop' => ['唯品会MP'],
|
||
'vipshop-jx' => ['唯品会jx'],
|
||
];
|
||
|
||
/** 店铺名前缀可识别的平台(顺序敏感:先长后短)。 */
|
||
private const STORE_PREFIX_PLATFORMS = [
|
||
'唯品会JX' => '唯品会jx',
|
||
'唯品会MP' => '唯品会MP',
|
||
'阿里巴巴' => '阿里巴巴',
|
||
// 1688 系店铺:新佰广1688 / 1688xxx 等店铺名不带「阿里巴巴」前缀,
|
||
// 按平台口径归到阿里巴巴,账单项目与阿里巴巴一致(阿里-xxx 仍走上面的前缀)。
|
||
'1688' => '阿里巴巴',
|
||
'新佰广' => '阿里巴巴',
|
||
'拼多多' => '拼多多',
|
||
'天猫' => '天猫',
|
||
'淘宝' => '淘宝',
|
||
'抖音' => '抖音',
|
||
'得物' => '得物',
|
||
'京喜' => '京喜',
|
||
'京东' => '京东',
|
||
];
|
||
|
||
/**
|
||
* @return array<string,array<string,string>> [platform => [bill_item => ledger_remark]]
|
||
*/
|
||
public function map(): array
|
||
{
|
||
return Cache::rememberForever(self::CACHE_KEY, function (): array {
|
||
try {
|
||
if (! Schema::hasTable('ec_account_ledger_remarks')) {
|
||
return [];
|
||
}
|
||
|
||
$map = [];
|
||
foreach (\Illuminate\Support\Facades\DB::table('ec_account_ledger_remarks')
|
||
->select(['platform', 'bill_item', 'ledger_remark'])
|
||
->get() as $row) {
|
||
$platform = trim((string) $row->platform);
|
||
$item = trim((string) $row->bill_item);
|
||
if ($platform === '' || $item === '') {
|
||
continue;
|
||
}
|
||
$map[$platform][$item] = (string) $row->ledger_remark;
|
||
}
|
||
|
||
return $map;
|
||
} catch (Throwable) {
|
||
return [];
|
||
}
|
||
});
|
||
}
|
||
|
||
public function forget(): void
|
||
{
|
||
Cache::forget(self::CACHE_KEY);
|
||
}
|
||
|
||
/** 页签默认平台。 */
|
||
public function defaultPlatform(string $tab): ?string
|
||
{
|
||
return self::TAB_PLATFORM[$tab] ?? null;
|
||
}
|
||
|
||
/** 页签涉及的平台(筛选下拉与账单项目翻译共用)。 */
|
||
public function platformsForTab(string $tab): array
|
||
{
|
||
if (isset(self::TAB_PLATFORMS[$tab])) {
|
||
return self::TAB_PLATFORMS[$tab];
|
||
}
|
||
|
||
$default = $this->defaultPlatform($tab);
|
||
|
||
return $default === null ? [] : [$default];
|
||
}
|
||
|
||
/** 平台 → 店铺名前缀(一个平台可能对应多个前缀)。 */
|
||
private function prefixesForPlatform(string $platform): array
|
||
{
|
||
$prefixes = [];
|
||
foreach (self::STORE_PREFIX_PLATFORMS as $prefix => $candidate) {
|
||
if ($candidate === $platform) {
|
||
$prefixes[] = $prefix;
|
||
}
|
||
}
|
||
|
||
return $prefixes;
|
||
}
|
||
|
||
/** 页签可选的「总账备注」(该页签涉及平台的去重并集)。 */
|
||
public function remarksForTab(string $tab): array
|
||
{
|
||
$map = $this->map();
|
||
$remarks = [];
|
||
foreach ($this->platformsForTab($tab) as $platform) {
|
||
foreach (array_values($map[$platform] ?? []) as $remark) {
|
||
$remark = trim((string) $remark);
|
||
if ($remark !== '') {
|
||
$remarks[$remark] = true;
|
||
}
|
||
}
|
||
}
|
||
|
||
$list = array_keys($remarks);
|
||
sort($list, SORT_NATURAL | SORT_FLAG_CASE);
|
||
|
||
return $list;
|
||
}
|
||
|
||
/**
|
||
* 「总账备注」→ 按平台分组的账单项目集合。
|
||
*
|
||
* 单平台页签只有一组、且不做店铺名前缀限制,可直接 whereIn(bill_item, items);
|
||
* 多平台页签(淘系支付宝 / 淘系微信)必须连店铺名前缀一起过滤,才能与页面上
|
||
* 按「店铺名前缀 → 平台 + 账单项目」逐行取到的总账备注完全一致。
|
||
*
|
||
* @return list<array{platform:string,prefixes:list<string>,excludePrefixes:list<string>,items:list<string>,isDefault:bool}>
|
||
*/
|
||
public function billItemGroupsForTab(string $tab, ?string $remark): array
|
||
{
|
||
$want = trim((string) $remark);
|
||
if ($want === '') {
|
||
return [];
|
||
}
|
||
|
||
$map = $this->map();
|
||
if ($map === []) {
|
||
return [];
|
||
}
|
||
|
||
$platforms = $this->platformsForTab($tab);
|
||
$single = count($platforms) <= 1;
|
||
$default = $this->defaultPlatform($tab);
|
||
$known = array_keys(self::STORE_PREFIX_PLATFORMS);
|
||
|
||
$groups = [];
|
||
foreach ($platforms as $platform) {
|
||
$items = [];
|
||
foreach ($map[$platform] ?? [] as $item => $value) {
|
||
if (trim((string) $value) === $want) {
|
||
$items[] = (string) $item;
|
||
}
|
||
}
|
||
if ($items === []) {
|
||
continue;
|
||
}
|
||
sort($items);
|
||
|
||
$prefixes = $single ? [] : $this->prefixesForPlatform($platform);
|
||
$isDefault = ! $single && $platform === $default;
|
||
|
||
$groups[] = [
|
||
'platform' => $platform,
|
||
// 单平台页签不做店铺名前缀限制(页签本身就是该平台口径)。
|
||
'prefixes' => $prefixes,
|
||
// 默认平台还要包含「没有已知平台前缀」的店铺。
|
||
'excludePrefixes' => $isDefault ? array_values(array_diff($known, $prefixes)) : [],
|
||
'items' => $items,
|
||
'isDefault' => $isDefault,
|
||
];
|
||
}
|
||
|
||
return $groups;
|
||
}
|
||
|
||
/** 「总账备注」→ 该页签全部相关账单项目(多平台页签为各平台并集)。 */
|
||
public function billItemsForTab(string $tab, ?string $remark): array
|
||
{
|
||
$items = [];
|
||
foreach ($this->billItemGroupsForTab($tab, $remark) as $group) {
|
||
foreach ($group['items'] as $item) {
|
||
$items[$item] = true;
|
||
}
|
||
}
|
||
|
||
$list = array_keys($items);
|
||
sort($list);
|
||
|
||
return $list;
|
||
}
|
||
|
||
/** 解析某一行账单所属平台:优先店铺名前缀,其次页签默认。 */
|
||
public function platform(?string $storeName, ?string $tabPlatform): ?string
|
||
{
|
||
$name = trim((string) $storeName);
|
||
if ($name !== '') {
|
||
foreach (self::STORE_PREFIX_PLATFORMS as $prefix => $platform) {
|
||
if (str_starts_with($name, $prefix)) {
|
||
return $platform;
|
||
}
|
||
}
|
||
}
|
||
|
||
return $tabPlatform;
|
||
}
|
||
|
||
/** 查总账备注;没有匹配返回 null。 */
|
||
public function remark(?string $storeName, ?string $tabPlatform, ?string $billItem): ?string
|
||
{
|
||
$item = trim((string) $billItem);
|
||
if ($item === '') {
|
||
return null;
|
||
}
|
||
|
||
$map = $this->map();
|
||
if ($map === []) {
|
||
return null;
|
||
}
|
||
|
||
foreach ([$this->platform($storeName, $tabPlatform), $tabPlatform] as $platform) {
|
||
if ($platform === null || $platform === '') {
|
||
continue;
|
||
}
|
||
if (isset($map[$platform][$item])) {
|
||
return $map[$platform][$item];
|
||
}
|
||
}
|
||
|
||
return null;
|
||
}
|
||
|
||
/**
|
||
* 给一行账单补上 payload.ledger_remark(内部字段名 ledger_remark)。
|
||
*
|
||
* @param array<string,mixed> $row
|
||
* @return array<string,mixed>
|
||
*/
|
||
public function decorate(array $row, string $tab): array
|
||
{
|
||
$tabPlatform = $this->defaultPlatform($tab);
|
||
$remark = $this->remark(
|
||
$row['store_name'] ?? null,
|
||
$tabPlatform,
|
||
$row['bill_item'] ?? null,
|
||
);
|
||
if ($remark === null) {
|
||
return $row;
|
||
}
|
||
|
||
$payload = is_array($row['payload'] ?? null) ? $row['payload'] : [];
|
||
$payload['ledger_remark'] = $remark;
|
||
$row['payload'] = $payload;
|
||
// 淘系支付宝页签按顶层字段取值,这里同时补一份,页面与导出共用。
|
||
$row['ledger_remark'] = $remark;
|
||
|
||
return $row;
|
||
}
|
||
|
||
/**
|
||
* @param iterable<int,array<string,mixed>> $rows
|
||
* @return \Generator<int,array<string,mixed>>
|
||
*/
|
||
public function decorateAll(iterable $rows, string $tab): \Generator
|
||
{
|
||
foreach ($rows as $row) {
|
||
yield is_array($row) ? $this->decorate($row, $tab) : $row;
|
||
}
|
||
}
|
||
}
|