'淘宝', '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> [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,excludePrefixes:list,items:list,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 $row * @return array */ 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> $rows * @return \Generator> */ public function decorateAll(iterable $rows, string $tab): \Generator { foreach ($rows as $row) { yield is_array($row) ? $this->decorate($row, $tab) : $row; } } }