CaiWuXiTong/modules/EcommerceFinance/Services/AccountLedgerRemarkMap.php
qiuxiang 7175d3f73e 店铺收款账户账单:所有页签新增「总账备注」筛选(页面与导出同口径)
- AccountLedgerRemarkMap:新增 页签→平台→账单项目 的翻译(remarksForTab / billItemGroupsForTab / billItemsForTab);淘系支付宝、淘系微信同页签混有多个平台,按店铺名前缀再分平台
- LedgerRemarkFilter(新):把「总账备注」翻译成 SQL 条件,单平台页签 whereIn(账单项目),多平台页签 (店铺前缀 AND 账单项目 IN ...) OR ...
- 控制器/仓库:AccountBills、AlipayBills、Pinduoduo/Dewu/TaobaoWechat 账户账单 + 对应仓库(含抖音权威、京东日账单、京东钱包、共享原始账单镜像)全部接入 ledger_remark 筛选;filter-options 统一返回 ledger_remarks 供下拉
- 前端:AccountBills.vue / TaobaoAlipayBills.vue 在「店铺」右侧新增「总账备注」下拉,参与查询、重置与导出
- 测试:新增映射翻译、SQL 条件、唯品会JX 页面+导出筛选用例;同步 filter-options 断言
2026-10-06 18:46:16 +08:00

303 lines
9.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\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',
'阿里巴巴' => '阿里巴巴',
'拼多多' => '拼多多',
'天猫' => '天猫',
'淘宝' => '淘宝',
'抖音' => '抖音',
'得物' => '得物',
'京喜' => '京喜',
'京东' => '京东',
];
/**
* @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;
}
}
}