diff --git a/app/Console/Commands/SyncStoreReceiptOrderMatches.php b/app/Console/Commands/SyncStoreReceiptOrderMatches.php index 01402f1c..7798d086 100644 --- a/app/Console/Commands/SyncStoreReceiptOrderMatches.php +++ b/app/Console/Commands/SyncStoreReceiptOrderMatches.php @@ -11,6 +11,7 @@ use App\Business\JstOrderLabelPlantingRules; use App\Business\PlantingOrderLabelLookup; use Modules\EcommerceFinance\Services\AccountLedgerRemarkMap; use Modules\EcommerceFinance\Services\AccountMappingStore; +use Modules\EcommerceFinance\Services\LedgerRemarkFilter; use Modules\EcommerceFinance\Services\StoreReportMirrorSynchronizer; use Throwable; @@ -130,12 +131,6 @@ class SyncStoreReceiptOrderMatches extends Command '退款', ]; - /** 「费用项」排除的科目映射费用备注:这两类不是费用。 */ - private const FEE_ITEM_EXCLUDED_MEMOS = [ - '收入', - '往来', - ]; - /** 这些收款账户不进入业绩明细,收、支都不取。 */ private const EXCLUDED_TARGET_SOURCES = [ 'jd-wallet', @@ -352,6 +347,18 @@ class SyncStoreReceiptOrderMatches extends Command $billQuery->whereIn('generation', $generations); $this->info('活动代:' . count($generations) . ' 个'); } + $billQuery->where(function ($group) use ($baseGroups, $discountGroups, $offsetGroups, $refundGroups, $promoItems): void { + LedgerRemarkFilter::applyGroups( + $group, + array_merge($baseGroups, $discountGroups, $offsetGroups, $refundGroups), + 'bill_item', + static fn ($sub, string $prefix) => $sub->where('store_name', 'like', $prefix . '%'), + static fn ($sub, string $prefix) => $sub->where('store_name', 'not like', $prefix . '%'), + ); + if ($promoItems !== []) { + $group->orWhereIn('bill_item', $promoItems); + } + }); if ($limit > 0) { $billQuery->limit($limit); } @@ -366,7 +373,6 @@ class SyncStoreReceiptOrderMatches extends Command 'discountTotal' => $discountTotal, 'offsetTotal' => $offsetTotal, 'refundTotal' => $refundTotal, 'moneyRemarkRows' => $moneyRemarkRows, 'attachedDiscount' => $attachedDiscount, 'attachedOffset' => $attachedOffset, 'unattachedRows' => $unattachedRows, 'billCount' => $billCount, - 'feeItemOrderCount' => $feeItemOrderCount, 'feeMappingGaps' => $feeMappingGaps, ] = $this->aggregateBills( $this->streamBillRows($billQuery->orderBy($timeColumn)), $source, $orderColumn, $timeColumn, $amountColumn, $expenseColumn, $realStoreColumn, @@ -393,11 +399,6 @@ class SyncStoreReceiptOrderMatches extends Command count($plantingCores), number_format($plantingTotal, 2), )); - $this->line(sprintf( - ' 费用项:%d 个订单有费用项,缺失映射 %d 组', - $feeItemOrderCount, - count($feeMappingGaps), - )); $this->line(sprintf( ' 已挂订单行:优惠 %s / 冲减 %s;未挂到订单行:%s(金额 %s,已补成「无收款账单 / 账单无单号」行)', number_format($attachedDiscount, 2), @@ -750,7 +751,6 @@ class SyncStoreReceiptOrderMatches extends Command 'offset_amount' => ! empty($group['offset_seen']) ? round((float) $group['offset_amount'], 2) : null, 'refund_amount' => ! empty($group['refund_seen']) ? round((float) $group['refund_amount'], 2) : null, 'planting_amount' => ! empty($group['planting_seen']) ? $income : null, - 'fee_item' => $group['fee_item'] ?? null, 'product_amount' => $productAmount, 'paid_amount' => $paidAmount, 'difference' => $difference, @@ -827,34 +827,6 @@ class SyncStoreReceiptOrderMatches extends Command return self::FAILURE; } - if ($feeMappingGaps !== []) { - ksort($feeMappingGaps); - $gapPath = $this->writeFeeMappingGapReport($month, (string) $source['label'], $feeMappingGaps); - $this->error(sprintf( - '费用项映射待补:%s %s 共 %d 组总账备注在科目映射表中没有可用费用备注。', - $month, - (string) $source['label'], - count($feeMappingGaps), - )); - foreach (array_slice($feeMappingGaps, 0, 20) as $gap) { - $this->line(sprintf( - ' %s / %s → %s(%s,%d 行,金额 %s)', - $gap['platform'], - $gap['bill_item'], - $gap['ledger_remark'], - $gap['reason'], - $gap['rows'], - number_format((float) $gap['amount'], 2), - )); - } - if (count($feeMappingGaps) > 20) { - $this->line(' 其余缺失项见清单文件。'); - } - if ($gapPath !== null) { - $this->line(' 缺失清单:' . $gapPath); - } - } - // 业绩明细变了,顺手刷新店铺维度报表的销量汇总(销量=这里的订单实付/实收金额)。 // 汇总失败不影响业绩明细本身,只提示一次,避免把已写入的账期数据判成失败。 return self::SUCCESS; @@ -1109,11 +1081,6 @@ class SyncStoreReceiptOrderMatches extends Command $discountTotal = 0.0; $offsetTotal = 0.0; $refundTotal = 0.0; - /** @var array> $feeItemsByKey 门店+订单 维度的费用项集合 */ - $feeItemsByKey = []; - /** @var array> $feeMappingGaps 科目映射缺失统计 */ - $feeMappingGaps = []; - $feeItemOrderCount = 0; /** @var array $promoRowsByKey 门店+订单 维度的优惠/冲减账单行数(用于统计未挂单行数) */ $promoRowsByKey = []; // 本月账单里真正命中「优惠收入 / 冲减收入 / 退款」的行数(重建后自检用: @@ -1125,61 +1092,15 @@ class SyncStoreReceiptOrderMatches extends Command $billCount++; $storeName = (string) ($bill->store_name ?? ''); $billItem = (string) ($bill->bill_item ?? ''); - $businessRemarks = $this->billBusinessRemarks($bill); - $goodsName = $this->billGoodsName($bill); - // 一次匹配同时取财务备注和费用备注;科目映射表没有规则时才回落旧总账备注。 - $accountMatch = $remarkMap->accountMappingMatch( + // 收入 / 优惠 / 冲减 / 退款统一按科目映射表的当前财务备注判桶; + // 只有当前科目映射表没有规则时,remark() 才回落到旧总账备注表。 + $remark = $remarkMap->remark( $storeName, $tabPlatform, $billItem, - $businessRemarks, - $goodsName, + $this->billBusinessRemarks($bill), + $this->billGoodsName($bill), ); - $accountMemo = trim((string) ($accountMatch['rule']['memo'] ?? '')); - $remark = $accountMemo !== '' - ? $accountMemo - : $remarkMap->ledgerRemarkFallback($storeName, $tabPlatform, $billItem); - $feeMemo = trim((string) ($accountMatch['rule']['feeMemo'] ?? '')); - - $rawNo = trim((string) ($bill->{$orderColumn} ?? '')); - $core = self::cleanOrderNo($rawNo); - $realStore = $realStoreColumn !== '' ? (string) ($bill->{$realStoreColumn} ?? '') : ''; - $shopKey = self::normalizeShop($realStore !== '' ? $realStore : (string) $bill->store_name); - $key = $shopKey . '|' . $core; - $amount = (float) $bill->{$amountColumn} - - ($expenseColumn !== '' ? (float) ($bill->{$expenseColumn} ?? 0) : 0.0); - - if ($core !== '' && $feeMemo !== '' - && ! in_array($feeMemo, self::FEE_ITEM_EXCLUDED_MEMOS, true)) { - $feeItemsByKey[$key][$feeMemo] = true; - } - - // 科目映射缺失时保留旧总账备注继续跑,同时把待补项记录到 CSV。 - if (trim((string) $remark) !== '') { - $gapReason = null; - if ($accountMatch === null) { - $gapReason = '科目映射表无平台+账单项目'; - } elseif ($feeMemo === '') { - $gapReason = '费用备注为空'; - } - - if ($gapReason !== null) { - $platformName = $remarkMap->platform($storeName, $tabPlatform) ?? (string) $source['label']; - $gapKey = $platformName . '|' . $billItem . '|' . $remark . '|' . $gapReason; - $feeMappingGaps[$gapKey] ??= [ - 'platform' => $platformName, - 'bill_item' => $billItem, - 'ledger_remark' => (string) $remark, - 'reason' => $gapReason, - 'rows' => 0, - 'amount' => 0.0, - 'sample_store' => $storeName, - ]; - $feeMappingGaps[$gapKey]['rows']++; - $feeMappingGaps[$gapKey]['amount'] += $amount; - } - } - $isPromoRemark = in_array($remark, self::DISCOUNT_REMARKS, true) || in_array($remark, self::OFFSET_REMARKS, true); $isRefundRemark = in_array($remark, self::REFUND_REMARKS, true); @@ -1198,6 +1119,12 @@ class SyncStoreReceiptOrderMatches extends Command continue; } + $rawNo = trim((string) ($bill->{$orderColumn} ?? '')); + $core = self::cleanOrderNo($rawNo); + $realStore = $realStoreColumn !== '' ? (string) ($bill->{$realStoreColumn} ?? '') : ''; + $shopKey = self::normalizeShop($realStore !== '' ? $realStore : (string) $bill->store_name); + $key = $shopKey . '|' . $core; + if (! isset($groups[$key])) { $groups[$key] = [ 'platform' => (string) $source['label'], @@ -1220,6 +1147,8 @@ class SyncStoreReceiptOrderMatches extends Command // 三列统一按「行金额=收入侧 − 支出侧」取数(负数表示净扣减)。 // 得物/天猫/淘系这类平台把「卖家承担优惠 / 扣回」记在支出侧,只取收入侧会漏掉扣减。 + $amount = (float) $bill->{$amountColumn} + - ($expenseColumn !== '' ? (float) ($bill->{$expenseColumn} ?? 0) : 0.0); // 同一遍读取同时累计补行,避免把整月原始账单留到第二遍。 // 首笔元数据、无单号按账单项目拆分、收入行随后出现的语义与原有补行一致。 if ($bucket !== 'income') { @@ -1275,16 +1204,6 @@ class SyncStoreReceiptOrderMatches extends Command // 只有优惠/冲减、没有收款的订单号不单独成行。 $groups = array_filter($groups, static fn (array $group): bool => (int) $group['payment_count'] > 0); - foreach ($groups as &$group) { - $items = array_keys($feeItemsByKey[$group['shop_key'] . '|' . $group['order_core_no']] ?? []); - sort($items, SORT_NATURAL | SORT_FLAG_CASE); - $group['fee_item'] = $items === [] ? null : implode('、', $items); - if ($items !== []) { - $feeItemOrderCount++; - } - } - unset($group); - // 口径对照:账单口径合计、已挂到订单行的合计、挂不上的行数(账单没有订单号 / 订单没有收款行)。 $attachedDiscount = 0.0; $attachedOffset = 0.0; @@ -1304,17 +1223,10 @@ class SyncStoreReceiptOrderMatches extends Command unset($promoOnlyGroups[$key]); } } - foreach ($promoOnlyGroups as &$group) { - $items = array_keys($feeItemsByKey[$group['shop_key'] . '|' . $group['order_core_no']] ?? []); - sort($items, SORT_NATURAL | SORT_FLAG_CASE); - $group['fee_item'] = $items === [] ? null : implode('、', $items); - } - unset($group); return compact( 'groups', 'promoOnlyGroups', 'paymentTotal', 'incomeTotal', 'discountTotal', 'offsetTotal', 'refundTotal', 'moneyRemarkRows', 'attachedDiscount', 'attachedOffset', 'unattachedRows', 'billCount', - 'feeItemOrderCount', 'feeMappingGaps', ); } @@ -1346,7 +1258,6 @@ class SyncStoreReceiptOrderMatches extends Command 'refund_amount' => ! empty($group['refund_seen']) ? round((float) $group['refund_amount'], 2) : null, // 补行没有订单侧商品金额,种菜金额同样留空;列集合与顺序必须与主行完全一致,批量插入才不会错列。 'planting_amount' => null, - 'fee_item' => $group['fee_item'] ?? null, 'product_amount' => null, 'paid_amount' => null, 'difference' => null, @@ -1518,43 +1429,6 @@ class SyncStoreReceiptOrderMatches extends Command return (string) config('performance_details.difference_basis', 'paid') === 'product' ? 'product' : 'paid'; } - /** - * 写出「科目映射表待补」清单,供财务补规则后重新同步。 - * - * @param array> $gaps - */ - private function writeFeeMappingGapReport(string $month, string $platform, array $gaps): ?string - { - $dir = storage_path('app/performance-fee-item-gaps'); - if (! is_dir($dir) && ! @mkdir($dir, 0775, true) && ! is_dir($dir)) { - return null; - } - - $safePlatform = preg_replace('/[^\p{L}\p{N}_-]+/u', '_', $platform) ?: 'platform'; - $path = $dir . DIRECTORY_SEPARATOR . $month . '_' . $safePlatform . '_' . now()->format('Ymd_His') . '.csv'; - $handle = @fopen($path, 'wb'); - if ($handle === false) { - return null; - } - - fwrite($handle, "\xEF\xBB\xBF"); - fputcsv($handle, ['平台', '账单项目', '总账备注', '缺失原因', '账单行数', '金额', '示例店铺']); - foreach ($gaps as $gap) { - fputcsv($handle, [ - $gap['platform'], - $gap['bill_item'], - $gap['ledger_remark'], - $gap['reason'], - $gap['rows'], - number_format((float) $gap['amount'], 2, '.', ''), - $gap['sample_store'], - ]); - } - fclose($handle); - - return $path; - } - /** * 按「店铺 + 订单号」在订单侧取金额,返回 [匹配状态, 订单侧金额, 原因标注]。 * diff --git a/app/Http/Controllers/Business/Pages.php b/app/Http/Controllers/Business/Pages.php index 97193f11..053d46a1 100644 --- a/app/Http/Controllers/Business/Pages.php +++ b/app/Http/Controllers/Business/Pages.php @@ -1809,7 +1809,6 @@ class Pages extends Controller 'offset_amount' => ['offset_amount', '冲减金额', 'money'], 'refund_amount' => ['refund_amount', '退款金额', 'money'], 'planting_amount' => ['planting_amount', '种菜金额', 'money'], - 'fee_item' => ['fee_item', '费用项', 'text'], 'product_amount' => ['product_amount', '商品金额', 'money'], 'paid_amount' => ['paid_amount', '订单实付金额', 'money'], 'difference' => ['difference', '差额', 'money'], diff --git a/config/business_navigation.php b/config/business_navigation.php index b4cc54a8..b10868d3 100644 --- a/config/business_navigation.php +++ b/config/business_navigation.php @@ -822,7 +822,7 @@ $navigation = [ 'directory' => [ 'icon' => 'receipt_long', 'status' => '共享镜像', - 'description' => '覆盖店铺收款账户账单各平台页签(总账备注=订单收入),按「店铺 + 订单号」匹配订单侧商品金额与订单实付金额;一行 = 一个店铺的一个订单,商品金额只挂一次。「商品金额」按「主单(拆单子单不参与)→ 原单行(merge_so_id 为空)→ 全部行」优先级取数,合单发货(原单与合并行共用同一线上单号、没有主单)时只取本单原单行,避免把合并的另一单算进来;并排除虚拟条码(X0000x:信誉订单 / 虚拟订单)明细行;「订单实付金额」取订单级实付(同一套优先级,末档兜底取该单号下最大值)再减去虚拟条码行实付。「优惠金额」「冲减金额」另取总账备注=优惠收入 / 冲减收入的账单金额,不计入收入金额;「退款金额」另取总账备注=退款的账单金额,只单独列示,不计入收入金额、也不参与差额;「种菜金额」是订单号在种菜明细表或聚水潭订单标签明细(是否种菜订单=是)任意一边出现过时,该订单的收入金额(两个条件是或的关系),只是收入金额里的一部分,不参与差额;「费用项」按科目映射表的费用备注归集,一单多费用时用顿号连接,只展示、不参与收入与差额;差额=(收入金额+优惠金额+冲减金额)−订单实付金额,匹配状态与差额异常备注同样按订单实付金额判定,未匹配到订单实付金额的行按订单实付金额=0 计入差额,只反映真正对不上的部分。挂不到订单行的优惠/冲减会补成一行(收入/商品留空)。「差额异常备注」只说明差额为什么产生,按顺序自动判定:订单实付未匹配(未匹配到订单实付金额,具体原因看「匹配原因」列);订单实付取数为0(种菜)(订单实付=0 且是种菜单)、订单实付取数为0(订单实付=0 但不是种菜单)、订单实付未减联合营销费(仅得物负差额:联合营销费未让利尾数对上才标注)、订单实付取供货价(仅京喜正差额:差额=该单直营服务费(供货模式净收)时标注)、差额=直营补贴款(待开票结算)(仅京喜账单·京喜自营工厂店负差额且无退款(出类拔萃等 10 家已逐店核验,其余工厂店按实付≈商品金额的供货价特征识别):京东直营补贴款待开票结算时标注)、差额=直营补贴款(订单退款已冲回)(同批店负差额但订单已退款:补贴款已随退款冲回)、差额=订单实付包含商家承担优惠和以旧换新国补(仅京东账单·索菲亚客厅/办公/书房/功能、亿家达全屋、朴美家居店负差额、且该单含国补(聚水潭应付−账单收入>0):差额为商家承担优惠(店铺东券/单品折扣等)与以旧换新国补之和,已用营销对账逐单核实)、差额=订单实付包含商家承担优惠(同店负差额但不含国补的单:差额即商家承担优惠)、订单实付未减冲减(订单实付=收入+优惠 → 差额=冲减金额)、订单实付未减优惠(订单实付=收入+冲减 → 差额=优惠金额)、订单实付未减优惠和冲减(订单实付=收入 → 差额=优惠+冲减)(以上三档等式对带口径标识的行让位给口径兜底)、订单实付含已退款部分(抖音负差额:聚水潭退款单/取消子单金额≈差额,或退款合计+账单退款净额≈实付−收入,或整单全退(退款合计≈实付)对上才标注;淘系支付宝/微信负差额:差额=退款合计、或退款合计=实付−收入(收款缺口)、或整单全退(退款合计≈实付)时同样标注(退款取聚水潭已确认记录));都不满足时,若「卖家实收」或「应付/已付」能对上账单,差额按 0 计并在「匹配原因」列标注口径核对;其余标「差额待核实」。「匹配原因」单独一列,取值:账单无单号 / 账单无单号(采销收款)/ 单号格式异常 / 订单表无此单 / 1688采销收款(非销售单)/ 订单无实付金额 / 无收款账单(「采销收款」两档专指「支付宝-1688采销专用」账户:该账户收的是采销/供货结算款,不是店铺销售单收款,不按丢单处理)。', + 'description' => '覆盖店铺收款账户账单各平台页签(总账备注=订单收入),按「店铺 + 订单号」匹配订单侧商品金额与订单实付金额;一行 = 一个店铺的一个订单,商品金额只挂一次。「商品金额」按「主单(拆单子单不参与)→ 原单行(merge_so_id 为空)→ 全部行」优先级取数,合单发货(原单与合并行共用同一线上单号、没有主单)时只取本单原单行,避免把合并的另一单算进来;并排除虚拟条码(X0000x:信誉订单 / 虚拟订单)明细行;「订单实付金额」取订单级实付(同一套优先级,末档兜底取该单号下最大值)再减去虚拟条码行实付。「优惠金额」「冲减金额」另取总账备注=优惠收入 / 冲减收入的账单金额,不计入收入金额;「退款金额」另取总账备注=退款的账单金额,只单独列示,不计入收入金额、也不参与差额;「种菜金额」是订单号在种菜明细表或聚水潭订单标签明细(是否种菜订单=是)任意一边出现过时,该订单的收入金额(两个条件是或的关系),只是收入金额里的一部分,不参与差额;差额=(收入金额+优惠金额+冲减金额)−订单实付金额,匹配状态与差额异常备注同样按订单实付金额判定,未匹配到订单实付金额的行按订单实付金额=0 计入差额,只反映真正对不上的部分。挂不到订单行的优惠/冲减会补成一行(收入/商品留空)。「差额异常备注」只说明差额为什么产生,按顺序自动判定:订单实付未匹配(未匹配到订单实付金额,具体原因看「匹配原因」列);订单实付取数为0(种菜)(订单实付=0 且是种菜单)、订单实付取数为0(订单实付=0 但不是种菜单)、订单实付未减联合营销费(仅得物负差额:联合营销费未让利尾数对上才标注)、订单实付取供货价(仅京喜正差额:差额=该单直营服务费(供货模式净收)时标注)、差额=直营补贴款(待开票结算)(仅京喜账单·京喜自营工厂店负差额且无退款(出类拔萃等 10 家已逐店核验,其余工厂店按实付≈商品金额的供货价特征识别):京东直营补贴款待开票结算时标注)、差额=直营补贴款(订单退款已冲回)(同批店负差额但订单已退款:补贴款已随退款冲回)、差额=订单实付包含商家承担优惠和以旧换新国补(仅京东账单·索菲亚客厅/办公/书房/功能、亿家达全屋、朴美家居店负差额、且该单含国补(聚水潭应付−账单收入>0):差额为商家承担优惠(店铺东券/单品折扣等)与以旧换新国补之和,已用营销对账逐单核实)、差额=订单实付包含商家承担优惠(同店负差额但不含国补的单:差额即商家承担优惠)、订单实付未减冲减(订单实付=收入+优惠 → 差额=冲减金额)、订单实付未减优惠(订单实付=收入+冲减 → 差额=优惠金额)、订单实付未减优惠和冲减(订单实付=收入 → 差额=优惠+冲减)(以上三档等式对带口径标识的行让位给口径兜底)、订单实付含已退款部分(抖音负差额:聚水潭退款单/取消子单金额≈差额,或退款合计+账单退款净额≈实付−收入,或整单全退(退款合计≈实付)对上才标注;淘系支付宝/微信负差额:差额=退款合计、或退款合计=实付−收入(收款缺口)、或整单全退(退款合计≈实付)时同样标注(退款取聚水潭已确认记录));都不满足时,若「卖家实收」或「应付/已付」能对上账单,差额按 0 计并在「匹配原因」列标注口径核对;其余标「差额待核实」。「匹配原因」单独一列,取值:账单无单号 / 账单无单号(采销收款)/ 单号格式异常 / 订单表无此单 / 1688采销收款(非销售单)/ 订单无实付金额 / 无收款账单(「采销收款」两档专指「支付宝-1688采销专用」账户:该账户收的是采销/供货结算款,不是店铺销售单收款,不按丢单处理)。', 'empty' => '店铺收款匹配订单明细尚未同步,请先执行 store-receipt:order-match:sync。', 'fields' => [ ['name' => 'platform', 'label' => '平台'], @@ -834,7 +834,6 @@ $navigation = [ ['name' => 'offset_amount', 'label' => '冲减金额', 'kind' => 'number'], ['name' => 'refund_amount', 'label' => '退款金额', 'kind' => 'number'], ['name' => 'planting_amount', 'label' => '种菜金额', 'kind' => 'number'], - ['name' => 'fee_item', 'label' => '费用项'], ['name' => 'product_amount', 'label' => '商品金额', 'kind' => 'number'], ['name' => 'paid_amount', 'label' => '订单实付/实收金额', 'kind' => 'number'], ['name' => 'difference', 'label' => '差额', 'kind' => 'number'], diff --git a/database/migrations/2026_10_20_010000_drop_fee_item_from_store_receipt_order_match_rows.php b/database/migrations/2026_10_20_010000_drop_fee_item_from_store_receipt_order_match_rows.php new file mode 100644 index 00000000..9105f8ee --- /dev/null +++ b/database/migrations/2026_10_20_010000_drop_fee_item_from_store_receipt_order_match_rows.php @@ -0,0 +1,42 @@ +connection()); + if (! $schema->hasTable('store_receipt_order_match_rows') + || ! $schema->hasColumn('store_receipt_order_match_rows', 'fee_item')) { + return; + } + + $schema->table('store_receipt_order_match_rows', function (Blueprint $table): void { + $table->dropColumn('fee_item'); + }); + } + + public function down(): void + { + $schema = Schema::connection($this->connection()); + if (! $schema->hasTable('store_receipt_order_match_rows') + || $schema->hasColumn('store_receipt_order_match_rows', 'fee_item')) { + return; + } + + $schema->table('store_receipt_order_match_rows', function (Blueprint $table): void { + $table->string('fee_item', 191)->nullable()->after('planting_amount'); + }); + } +}; diff --git a/modules/EcommerceFinance/Services/AccountLedgerRemarkMap.php b/modules/EcommerceFinance/Services/AccountLedgerRemarkMap.php index 21e4e043..fd33d349 100644 --- a/modules/EcommerceFinance/Services/AccountLedgerRemarkMap.php +++ b/modules/EcommerceFinance/Services/AccountLedgerRemarkMap.php @@ -405,110 +405,6 @@ class AccountLedgerRemarkMap return $tabPlatform; } - /** - * 返回命中的科目映射规则和费用备注,供业绩明细等需要保留规则详情的地方使用。 - * - * 查找顺序与 remark() 一致:先按店铺平台匹配,再按页签平台兜底; - * 每个平台先取原始账单项目,bill_item 以 wx 开头时再取去掉 wx 的候选。 - * - * @param array $businessRemarks - * @return array{platform:string,bill_item:string,rule:array,matched:bool,reason:string}|null - */ - public function accountMappingMatch( - ?string $storeName, - ?string $tabPlatform, - ?string $billItem, - array $businessRemarks = [], - string $goodsName = '', - ): ?array { - $item = trim((string) $billItem); - if ($item === '') { - return null; - } - - $platforms = array_values(array_unique(array_filter( - [$this->platform($storeName, $tabPlatform), $tabPlatform], - static fn (?string $platform): bool => $platform !== null && trim($platform) !== '', - ))); - $index = $this->accountMappingIndex(); - - foreach ($platforms as $platform) { - $platformKey = $this->normalizeKey($platform); - foreach ($this->itemLookupKeys($item) as $candidate) { - $rules = $index[$platformKey][$this->normalizeKey($candidate)] ?? []; - if ($rules === []) { - continue; - } - - if (count($rules) === 1) { - return [ - 'platform' => $platform, - 'bill_item' => $candidate, - 'rule' => $rules[0], - 'matched' => true, - 'reason' => '唯一规则', - ]; - } - - try { - $result = app(AccountMappingMatcher::class)->match( - $platform, - $candidate, - $businessRemarks, - $goodsName, - ); - } catch (Throwable) { - $result = null; - } - - if (is_array($result) && isset($result['rule'])) { - return [ - 'platform' => $platform, - 'bill_item' => $candidate, - 'rule' => $result['rule'], - 'matched' => (bool) ($result['matched'] ?? false), - 'reason' => (string) ($result['reason'] ?? ''), - ]; - } - } - } - - return null; - } - - /** - * 旧「总账备注映射表」的兜底值;已按平台和 wx 前缀规则查找。 - */ - public function ledgerRemarkFallback( - ?string $storeName, - ?string $tabPlatform, - ?string $billItem, - ): ?string { - $item = trim((string) $billItem); - if ($item === '') { - return null; - } - - $map = $this->map(); - if ($map === []) { - return null; - } - - $platforms = array_values(array_unique(array_filter( - [$this->platform($storeName, $tabPlatform), $tabPlatform], - static fn (?string $platform): bool => $platform !== null && trim($platform) !== '', - ))); - foreach ($platforms as $platform) { - foreach ($this->itemLookupKeys($item) as $candidate) { - if (isset($map[$platform][$candidate])) { - return (string) $map[$platform][$candidate]; - } - } - } - - return null; - } - /** * 查总账备注;没有匹配返回 null。 * @@ -541,7 +437,24 @@ class AccountLedgerRemarkMap } } - return $this->ledgerRemarkFallback($storeName, $tabPlatform, $item); + $map = $this->map(); + if ($map === []) { + return null; + } + + foreach ($platforms as $platform) { + if ($platform === null || $platform === '') { + continue; + } + // 先按原样匹配,再按「去掉 wx 前缀」的账单项目匹配(淘系部分行带 wx 前缀) + foreach ($this->itemLookupKeys($item) as $candidate) { + if (isset($map[$platform][$candidate])) { + return $map[$platform][$candidate]; + } + } + } + + return null; } /** 科目映射表按「平台 + 账单项目」预索引,避免逐行扫全部规则。 */ diff --git a/tests/Feature/Business/AccountMappingMatcherTest.php b/tests/Feature/Business/AccountMappingMatcherTest.php index c717a3b0..7d350199 100644 --- a/tests/Feature/Business/AccountMappingMatcherTest.php +++ b/tests/Feature/Business/AccountMappingMatcherTest.php @@ -3,7 +3,6 @@ namespace Tests\Feature\Business; use Illuminate\Support\Facades\File; -use Modules\EcommerceFinance\Services\AccountLedgerRemarkMap; use Modules\EcommerceFinance\Services\AccountMappingMatcher; use Modules\EcommerceFinance\Services\AccountMappingStore; use Modules\EcommerceFinance\Services\AccountMappingValidator; @@ -124,28 +123,6 @@ class AccountMappingMatcherTest extends FeatureTestCase $this->assertFalse($miss['matched']); } - public function test_performance_fee_item_uses_the_matched_rule_fee_memo(): void - { - $rule = $this->rule([ - 'platform' => '京东', - 'item' => '平台服务费', - 'memo' => '平台服务费', - 'feeMemo' => '平台服务费', - 'category' => '费用', - ]); - - $matched = app(AccountLedgerRemarkMap::class)->accountMappingMatch( - '京东-测试店铺', - 'jd-transactions', - '平台服务费', - ); - - $this->assertNotNull($matched); - $this->assertTrue($matched['matched']); - $this->assertSame($rule['id'], $matched['rule']['id']); - $this->assertSame('平台服务费', $matched['rule']['feeMemo']); - } - /** 校验:同项目、条件不同 = 正常;条件相同 = 才算重复 */ public function test_validator_flags_only_same_condition_pairs(): void {