diff --git a/app/Console/Commands/SyncStoreReceiptOrderMatches.php b/app/Console/Commands/SyncStoreReceiptOrderMatches.php index c8d71398..334a487b 100644 --- a/app/Console/Commands/SyncStoreReceiptOrderMatches.php +++ b/app/Console/Commands/SyncStoreReceiptOrderMatches.php @@ -25,7 +25,7 @@ use Throwable; * - 订单侧:销售全量按「店铺 + 线上订单号」先聚合求和(一单多品/多子订单在此合并),金额取 goods_count > 0 的有效行 * - 单号清洗:去除日期前缀(如 260630-)、空格、横杠后按核心号匹配 * - 店铺:账单用「线上真实店铺」优先,订单侧用销售全量店铺名,统一把下划线归一为横杠 - * - 同一店铺同一订单号多笔收款 → 标注「一单多笔」,商品金额只挂一次 + * - 同一店铺同一订单号多笔收款 → 标注「同单多笔收款」,商品金额只挂一次 */ class SyncStoreReceiptOrderMatches extends Command { @@ -121,19 +121,30 @@ class SyncStoreReceiptOrderMatches extends Command private const STATUS_UNMATCHED = '未匹配'; - private const REMARK_EMPTY_NO = '空单号'; + /** + * 异常标注取值(页面「匹配状态」筛选与导出共用)。 + * + * 分三类,命名统一成「哪里出问题」: + * 单号问题:账单无单号 / 单号格式异常 + * 订单问题:订单表无此单 / 订单无商品金额 + * 匹配提示:店铺名不一致 / 订单侧同号多条 / 同单多笔收款 + */ + private const REMARK_EMPTY_NO = '账单无单号'; - private const REMARK_BAD_FORMAT = '格式不一致'; + private const REMARK_BAD_FORMAT = '单号格式异常'; private const REMARK_NO_ORDER = '订单表无此单'; private const REMARK_NO_AMOUNT = '订单无商品金额'; - private const REMARK_SHOP_DIFF = '店铺不一致'; + private const REMARK_SHOP_DIFF = '店铺名不一致'; private const REMARK_DUP_ORDER = '订单侧同号多条'; - private const REMARK_MULTI_PAY = '一单多笔'; + private const REMARK_MULTI_PAY = '同单多笔收款'; + + /** 优惠/冲减账单所在的订单没有收款行时,补行用这个标注。 */ + private const REMARK_PROMO_ONLY = '无收款账单'; public function handle(UpstreamReadOnlyQueryFactory $factory): int { @@ -271,6 +282,8 @@ class SyncStoreReceiptOrderMatches extends Command $ledgerMap = $remarkMap->map(); $incomeOverlapRows = 0; $incomeOverlapTotal = 0.0; + /** @var array $promoRowsByKey 门店+订单 维度的优惠/冲减账单行数(用于统计未挂单行数) */ + $promoRowsByKey = []; foreach ($bills as $bill) { $storeName = (string) ($bill->store_name ?? ''); $billItem = (string) ($bill->bill_item ?? ''); @@ -329,10 +342,12 @@ class SyncStoreReceiptOrderMatches extends Command $groups[$key]['discount_seen'] = true; $groups[$key]['discount_amount'] += $amount; $discountTotal += $amount; + $promoRowsByKey[$key] = ($promoRowsByKey[$key] ?? 0) + 1; } else { $groups[$key]['offset_seen'] = true; $groups[$key]['offset_amount'] += $amount; $offsetTotal += $amount; + $promoRowsByKey[$key] = ($promoRowsByKey[$key] ?? 0) + 1; } // 种菜金额只标记「这个订单是不是种菜单」,金额仍取该行的收入金额, @@ -346,6 +361,31 @@ class SyncStoreReceiptOrderMatches extends Command // 只有优惠/冲减、没有收款的订单号不单独成行。 $groups = array_filter($groups, static fn (array $group): bool => (int) $group['payment_count'] > 0); + // 口径对照:账单口径合计、已挂到订单行的合计、挂不上的行数(账单没有订单号 / 订单没有收款行)。 + $attachedDiscount = 0.0; + $attachedOffset = 0.0; + foreach ($groups as $group) { + $attachedDiscount += (float) $group['discount_amount']; + $attachedOffset += (float) $group['offset_amount']; + } + $unattachedRows = 0; + foreach ($promoRowsByKey as $key => $count) { + if (! isset($groups[$key])) { + $unattachedRows += $count; + } + } + // A 方案:挂不到订单行的优惠/冲减也补成行(收入/商品/差额留空),金额不丢。 + $promoOnlyGroups = $this->promoOnlyGroups( + $bills, + $groups, + $remarkMap, + $ledgerMap, + $tabPlatform, + $orderColumn, + $timeColumn, + $amountColumn, + $realStoreColumn, + ); $this->info('账单收款笔数:' . $paymentTotal . ',按「店铺 + 订单号」聚合后:' . count($groups) . ' 行'); $this->line(' 优惠金额合计:' . number_format($discountTotal, 2) . ',冲减金额合计:' . number_format($offsetTotal, 2)); foreach ($groups as $group) { @@ -360,6 +400,13 @@ class SyncStoreReceiptOrderMatches extends Command count($plantingCores), number_format($plantingTotal, 2), )); + $this->line(sprintf( + ' 已挂订单行:优惠 %s / 冲减 %s;未挂到订单行:%s(金额 %s,已补成「无收款账单 / 账单无单号」行)', + number_format($attachedDiscount, 2), + number_format($attachedOffset, 2), + number_format($unattachedRows) . ' 行', + number_format(($discountTotal + $offsetTotal) - ($attachedDiscount + $attachedOffset), 2), + )); if ($incomeOverlapRows > 0) { $this->line(sprintf( ' 提示:%d 笔(%s 元)按旧口径已计入收入金额,未重复计入优惠/冲减列。', @@ -369,7 +416,7 @@ class SyncStoreReceiptOrderMatches extends Command } if ((bool) $this->option('amounts-only')) { - return $this->backfillPromoAmounts($month, $source, $groups, $reportMonth); + return $this->backfillPromoAmounts($month, $source, $groups, $promoOnlyGroups, $reportMonth); } // 订单侧的单号在不同平台形态不同:淘系/京东是无前缀纯数字, @@ -448,7 +495,7 @@ class SyncStoreReceiptOrderMatches extends Command $now = now(); $buffer = []; $imported = 0; - $stats = ['已匹配' => 0, '未匹配' => 0, '一单多笔' => 0]; + $stats = ['已匹配' => 0, '未匹配' => 0, self::REMARK_MULTI_PAY => 0]; $remarked = []; $goodsTotal = 0.0; $differenceTotal = 0.0; @@ -534,7 +581,7 @@ class SyncStoreReceiptOrderMatches extends Command $stats[$status] = ($stats[$status] ?? 0) + 1; if ((int) $group['payment_count'] > 1) { - $stats['一单多笔']++; + $stats[self::REMARK_MULTI_PAY]++; } if ($remark !== '') { $remarked[$remark] = ($remarked[$remark] ?? 0) + 1; @@ -567,6 +614,16 @@ class SyncStoreReceiptOrderMatches extends Command } } + // A 方案补行:挂不到订单行的优惠/冲减也写进业绩明细(收入/商品/差额留空)。 + foreach ($promoOnlyGroups as $promoGroup) { + $buffer[] = self::promoOnlyRow($reportMonth, (string) $source['label'], $promoGroup, $now); + if (count($buffer) >= 500) { + $target->insertOrIgnore($buffer); + $imported += count($buffer); + $buffer = []; + } + } + if ($buffer !== []) { $target->insertOrIgnore($buffer); $imported += count($buffer); @@ -586,6 +643,7 @@ class SyncStoreReceiptOrderMatches extends Command return self::SUCCESS; } + /** * 只回填「种菜金额」:不读账单,直接按业绩明细里已有的「订单号」判断是不是种菜订单, * 是就把该行的收入金额写进去,不是就清空。适合给已经同步好的账期补这一列。 @@ -637,8 +695,9 @@ class SyncStoreReceiptOrderMatches extends Command * * @param array $source * @param array> $groups + * @param array> $promoOnlyGroups A 方案要补的行 */ - private function backfillPromoAmounts(string $month, array $source, array $groups, string $reportMonth): int + private function backfillPromoAmounts(string $month, array $source, array $groups, array $promoOnlyGroups, string $reportMonth): int { $connection = (string) config('upstream_mirrors.connection', 'business_mirror'); $platform = (string) $source['label']; @@ -684,14 +743,37 @@ class SyncStoreReceiptOrderMatches extends Command $this->updateAmountColumn($connection, $reportMonth, 'offset_amount', $offsets); $this->updateAmountColumn($connection, $reportMonth, 'planting_amount', $plantings); + // A 方案:本月本平台的补行(payment_count=0 的行)整体重建,保证幂等。 + $target() + ->where('report_month', $reportMonth) + ->where('platform', $platform) + ->where('payment_count', 0) + ->delete(); + + $buffer = []; + $promoRows = 0; + foreach ($promoOnlyGroups as $promoGroup) { + $buffer[] = self::promoOnlyRow($reportMonth, $platform, $promoGroup, now()); + if (count($buffer) >= 500) { + $target()->insertOrIgnore($buffer); + $promoRows += count($buffer); + $buffer = []; + } + } + if ($buffer !== []) { + $target()->insertOrIgnore($buffer); + $promoRows += count($buffer); + } + $this->info(sprintf( - '已回填 %s %s:优惠 %d 行(%s)/ 冲减 %d 行(%s)/ 种菜 %d 行(%s),镜像原有 %d 行,未重建业绩明细。', + '已回填 %s %s:优惠 %d 行(%s)/ 冲减 %d 行(%s)/ 补「无收款账单/无单号」优惠冲减行 %d 行 / 种菜 %d 行(%s),镜像原有 %d 行,未重建业绩明细。', $month, $platform, count($discounts), number_format(array_sum($discounts), 2), count($offsets), number_format(array_sum($offsets), 2), + $promoRows, count($plantings), number_format(array_sum($plantings), 2), count($existing), @@ -700,6 +782,116 @@ class SyncStoreReceiptOrderMatches extends Command return self::SUCCESS; } + /** + * A 方案:把挂不到订单行的优惠/冲减整理成「补行」。 + * + * - 有订单号、但该订单没有收款账单(成不了行)→ 一行 = 店铺 + 订单号 + * - 账单本身没有订单号 → 一行 = 店铺 + 账单项目(按账期汇总) + * + * @param iterable $bills + * @param array> $groups 已有收款行(已按 payment_count 过滤) + * @param array $ledgerMap + * @return array> + */ + private function promoOnlyGroups( + iterable $bills, + array $groups, + AccountLedgerRemarkMap $remarkMap, + array $ledgerMap, + ?string $tabPlatform, + string $orderColumn, + string $timeColumn, + string $amountColumn, + string $realStoreColumn, + ): array { + $result = []; + foreach ($bills as $bill) { + $storeName = (string) ($bill->store_name ?? ''); + $billItem = (string) ($bill->bill_item ?? ''); + $remark = $remarkMap->remark($storeName, $tabPlatform, $billItem); + $type = in_array($remark, self::DISCOUNT_REMARKS, true) ? 'discount' + : (in_array($remark, self::OFFSET_REMARKS, true) ? 'offset' : null); + if ($type === null) { + continue; + } + + // 已经算进「收入金额」白名单的行不重复补。 + $legacy = $ledgerMap[$remarkMap->platform($storeName, $tabPlatform) ?? ''][$billItem] ?? null; + if (in_array($legacy, self::LEDGER_REMARKS, true)) { + continue; + } + + $rawNo = trim((string) ($bill->{$orderColumn} ?? '')); + $core = self::cleanOrderNo($rawNo); + $realStore = $realStoreColumn !== '' ? (string) ($bill->{$realStoreColumn} ?? '') : ''; + $shopKey = self::normalizeShop($realStore !== '' ? $realStore : $storeName); + if ($core !== '' && isset($groups[$shopKey . '|' . $core])) { + // 该订单已有收款行,优惠/冲减已经挂在那一行上。 + continue; + } + + $key = $core === '' ? $shopKey . '||' . $billItem : $shopKey . '|' . $core; + if (! isset($result[$key])) { + $result[$key] = [ + 'shop_key' => $shopKey, + 'store_name' => $storeName, + 'online_order_no' => $rawNo, + 'order_core_no' => $core, + 'bill_item' => $billItem, + 'occurred_date' => substr((string) $bill->{$timeColumn}, 0, 10), + 'discount_amount' => 0.0, + 'discount_seen' => false, + 'offset_amount' => 0.0, + 'offset_seen' => false, + ]; + } + + $amount = (float) $bill->{$amountColumn}; + if ($type === 'discount') { + $result[$key]['discount_seen'] = true; + $result[$key]['discount_amount'] += $amount; + } else { + $result[$key]['offset_seen'] = true; + $result[$key]['offset_amount'] += $amount; + } + } + + return $result; + } + + /** + * A 方案补行的入库数据:收入/商品/差额留空,只带优惠/冲减金额与原因标注。 + * + * @param array $group + * @return array + */ + private static function promoOnlyRow(string $reportMonth, string $platform, array $group, mixed $now): array + { + $core = (string) $group['order_core_no']; + $rowKey = $core !== '' + ? md5($reportMonth . '|' . $platform . '|' . $group['shop_key'] . '|' . $core) + : md5($reportMonth . '|' . $platform . '|' . $group['shop_key'] . '|promo|' . $group['bill_item']); + + return [ + 'report_month' => $reportMonth, + 'platform' => $platform, + 'row_key' => $rowKey, + 'occurred_date' => $group['occurred_date'], + 'store_name' => $group['store_name'], + 'online_order_no' => $group['online_order_no'] !== '' ? $group['online_order_no'] : null, + 'order_core_no' => $core !== '' ? $core : null, + 'payment_count' => 0, + 'income_amount' => 0, + 'discount_amount' => ! empty($group['discount_seen']) ? round((float) $group['discount_amount'], 2) : null, + 'offset_amount' => ! empty($group['offset_seen']) ? round((float) $group['offset_amount'], 2) : null, + 'product_amount' => null, + 'difference' => null, + 'match_status' => self::STATUS_UNMATCHED, + 'remark' => $core === '' ? self::REMARK_EMPTY_NO : self::REMARK_PROMO_ONLY, + 'imported_at' => $now, + ]; + } + /** * 按 row_key 批量更新一列金额:只更新镜像里已有的行,不做插入。 * diff --git a/app/Http/Controllers/Business/Pages.php b/app/Http/Controllers/Business/Pages.php index c9d90538..d820840c 100644 --- a/app/Http/Controllers/Business/Pages.php +++ b/app/Http/Controllers/Business/Pages.php @@ -107,6 +107,24 @@ class Pages extends Controller '得物', ]; + /** 业绩明细「匹配状态」筛选项:只保留基础匹配状态。 */ + private const RECEIPT_STATUS_OPTIONS = [ + '已匹配', + '未匹配', + ]; + + /** 业绩明细「异常标注」筛选项,取值与同步命令写入的标注一致。 */ + private const RECEIPT_REMARK_OPTIONS = [ + '账单无单号', + '单号格式异常', + '订单表无此单', + '订单无商品金额', + '无收款账单', + '店铺名不一致', + '订单侧同号多条', + '同单多笔收款', + ]; + public function __construct( private readonly EdgeBandingRepository $edgeBanding, private readonly InventoryRepository $inventory, @@ -582,21 +600,7 @@ class Pages extends Controller protected function storeReceiptOrderMatchRows(): array { $connection = (string) config('upstream_mirrors.connection', 'business_mirror'); - $months = DB::connection($connection)->table('store_receipt_order_match_rows') - ->select('report_month') - ->distinct() - ->orderByDesc('report_month') - ->pluck('report_month') - ->map(static fn ($month): string => substr((string) $month, 0, 10)) - ->all(); - - $requested = substr(trim((string) request('month', '')), 0, 10); - if (preg_match('/^\d{4}-\d{2}$/', $requested) === 1) { - // 筛选栏的月份选择器提交 Y-m,这里补成当月 1 号再匹配。 - $requested .= '-01'; - } - // 明确指定月份时严格按该月份取数(该月没同步就显示空表)。 - $month = $requested !== '' ? $requested : ($months[0] ?? null); + [$months, $month] = $this->receiptOrderMatchMonths(); // 每页条数:业绩明细页面可切 10 / 20 / 50,未指定时沿用系统默认(上限 50)。 $requestedLimit = (int) request('per_page', 0); @@ -615,10 +619,10 @@ class Pages extends Controller // 平台 / 店铺与其它页面一致:可打字搜索的多选(一个都不勾=全部),同时兼容旧的单值链接。 $platformFilter = $this->receiptOrderMatchFilterValues(request('platform')); $storeFilter = $this->receiptOrderMatchFilterValues(request('store_name')); + [$statusFilter, $remarkFilter] = $this->receiptOrderMatchStatusFilters(request('match_status'), request('remark')); $orderNo = trim((string) request('online_order_no', '')); - $status = trim((string) request('match_status', '')); - $applyFilters = static function ($query) use ($platformFilter, $storeFilter, $orderNo, $status): void { + $applyFilters = static function ($query) use ($platformFilter, $storeFilter, $orderNo, $statusFilter, $remarkFilter): void { if ($platformFilter !== []) { $query->whereIn('platform', $platformFilter); } @@ -628,13 +632,12 @@ class Pages extends Controller if ($orderNo !== '') { $query->where('online_order_no', 'like', '%' . addcslashes($orderNo, '%_\\') . '%'); } - if ($status !== '') { - // 基础状态(已匹配/未匹配)按状态列过滤,其余按异常标注过滤。 - if (in_array($status, ['已匹配', '未匹配'], true)) { - $query->where('match_status', $status); - } else { - $query->where('remark', $status); - } + // 「匹配状态」和「异常标注」是两个独立筛选,各自多选内部是「或」,两者之间是「且」。 + if ($statusFilter !== []) { + $query->whereIn('match_status', $statusFilter); + } + if ($remarkFilter !== []) { + $query->whereIn('remark', $remarkFilter); } }; @@ -708,6 +711,8 @@ class Pages extends Controller $storeFilter, array_map('strval', $filterOptions['stores'] ?? []), ))); + $stats['status_options'] = self::RECEIPT_STATUS_OPTIONS; + $stats['remark_options'] = self::RECEIPT_REMARK_OPTIONS; if ($total === 0) { return [$empty, $stats]; @@ -750,6 +755,126 @@ class Pages extends Controller return array_keys($clean); } + /** + * 业绩明细的账期集合与当前账期:未指定月份时取镜像里最新的月份。 + * + * @return array{0:list,1:?string} 账期列表(Y-m-d)与当前账期(Y-m-d) + */ + private function receiptOrderMatchMonths(): array + { + $connection = (string) config('upstream_mirrors.connection', 'business_mirror'); + $months = DB::connection($connection)->table('store_receipt_order_match_rows') + ->select('report_month') + ->distinct() + ->orderByDesc('report_month') + ->pluck('report_month') + ->map(static fn ($month): string => substr((string) $month, 0, 10)) + ->all(); + + $requested = substr(trim((string) request('month', '')), 0, 10); + if (preg_match('/^\d{4}-\d{2}$/', $requested) === 1) { + // 筛选栏的月份选择器提交 Y-m,这里补成当月 1 号再匹配。 + $requested .= '-01'; + } + // 明确指定月份时严格按该月份取数(该月没同步就显示空表)。 + $month = $requested !== '' ? $requested : ($months[0] ?? null); + + return [$months, $month]; + } + + /** + * 归一化「匹配状态 / 异常标注」两组筛选值:状态列与标注列各自独立, + * 同时兼容历史链接把异常标注塞进 match_status 的写法。 + * + * @return array{0:list,1:list} 状态值、异常标注值 + */ + private function receiptOrderMatchStatusFilters(mixed $statusInput, mixed $remarkInput): array + { + $values = $this->receiptOrderMatchFilterValues($statusInput); + $statusValues = array_values(array_intersect($values, self::RECEIPT_STATUS_OPTIONS)); + + return [ + $statusValues, + array_values(array_unique(array_merge( + array_diff($values, $statusValues), + $this->receiptOrderMatchFilterValues($remarkInput), + ))), + ]; + } + + /** + * 业绩明细筛选联动:按「当前其它筛选条件」统计每个筛选项的可选值与行数, + * 行数为 0 的取值不返回(前端不展示)。 + */ + public function receiptOrderMatchFilterOptions() + { + [, $month] = $this->receiptOrderMatchMonths(); + if ($month === null) { + return response()->json(['month' => null, 'facets' => []]); + } + + [$statusValues, $remarkValues] = $this->receiptOrderMatchStatusFilters(request('match_status'), request('remark')); + $filters = [ + 'platform' => $this->receiptOrderMatchFilterValues(request('platform')), + 'store_name' => $this->receiptOrderMatchFilterValues(request('store_name')), + 'match_status' => $statusValues, + 'remark' => $remarkValues, + ]; + $orderNo = trim((string) request('online_order_no', '')); + + return response()->json([ + 'month' => substr($month, 0, 7), + 'facets' => [ + 'platform' => $this->receiptOrderMatchFacet($month, 'platform', $filters, $orderNo, self::RECEIPT_PLATFORM_OPTIONS), + 'store_name' => $this->receiptOrderMatchFacet($month, 'store_name', $filters, $orderNo), + 'match_status' => $this->receiptOrderMatchFacet($month, 'match_status', $filters, $orderNo, self::RECEIPT_STATUS_OPTIONS), + 'remark' => $this->receiptOrderMatchFacet($month, 'remark', $filters, $orderNo, self::RECEIPT_REMARK_OPTIONS), + ], + ]); + } + + /** + * 单个筛选项的可选值与行数:应用除该筛选项以外的所有条件。 + * + * @param array> $filters 已归一化的筛选条件(键=列名) + * @param list $order 固定展示顺序,未列出的取值排在后面 + * @return array{values:list,counts:array} + */ + private function receiptOrderMatchFacet(string $month, string $column, array $filters, string $orderNo, array $order = []): array + { + $query = DB::connection((string) config('upstream_mirrors.connection', 'business_mirror')) + ->table('store_receipt_order_match_rows') + ->where('report_month', $month); + + foreach ($filters as $key => $values) { + if ($key === $column || $values === []) { + continue; + } + $query->whereIn($key, $values); + } + if ($orderNo !== '') { + $query->where('online_order_no', 'like', '%' . addcslashes($orderNo, '%_\\') . '%'); + } + + $counts = []; + $rows = $query->select($column) + ->selectRaw('count(*) as total') + ->whereNotNull($column) + ->where($column, '!=', '') + ->groupBy($column) + ->pluck('total', $column); + foreach ($rows as $value => $total) { + $counts[(string) $value] = (int) $total; + } + + $ordered = array_values(array_intersect($order, array_keys($counts))); + $rest = array_values(array_diff(array_keys($counts), $ordered)); + sort($rest, SORT_NATURAL | SORT_FLAG_CASE); + + return ['values' => array_merge($ordered, $rest), 'counts' => $counts]; + } + + /** * 业绩明细 → 店铺收款匹配订单明细导出:按当前月份与筛选条件导出 CSV,列序跟随页面显示列。 */ @@ -772,15 +897,7 @@ class Pages extends Controller 'remark' => ['remark', '异常标注', 'text'], ]; - $months = DB::connection($connection)->table('store_receipt_order_match_rows') - ->select('report_month')->distinct()->orderByDesc('report_month') - ->pluck('report_month') - ->map(static fn ($month): string => substr((string) $month, 0, 10))->all(); - $requested = substr(trim((string) $request->query('month', '')), 0, 10); - if (preg_match('/^\d{4}-\d{2}$/', $requested) === 1) { - $requested .= '-01'; - } - $month = $requested !== '' ? $requested : ($months[0] ?? null); + [, $month] = $this->receiptOrderMatchMonths(); $query = DB::connection($connection)->table('store_receipt_order_match_rows') ->when($month !== null, static fn ($builder) => $builder->where('report_month', $month)); @@ -797,13 +914,13 @@ class Pages extends Controller if ($orderNo !== '') { $query->where('online_order_no', 'like', '%' . addcslashes($orderNo, '%_\\') . '%'); } - $status = trim((string) $request->query('match_status', '')); - if ($status !== '') { - if (in_array($status, ['已匹配', '未匹配'], true)) { - $query->where('match_status', $status); - } else { - $query->where('remark', $status); - } + $statusFilter = $this->receiptOrderMatchFilterValues($request->query('match_status')); + [$statusValues, $remarkValues] = $this->receiptOrderMatchStatusFilters($statusFilter, $request->query('remark')); + if ($statusValues !== []) { + $query->whereIn('match_status', $statusValues); + } + if ($remarkValues !== []) { + $query->whereIn('remark', $remarkValues); } $selected = array_values(array_intersect( diff --git a/config/business_navigation.php b/config/business_navigation.php index 5749969c..a24f7c22 100644 --- a/config/business_navigation.php +++ b/config/business_navigation.php @@ -789,7 +789,7 @@ $navigation = [ 'directory' => [ 'icon' => 'receipt_long', 'status' => '共享镜像', - 'description' => '覆盖店铺收款账户账单各平台页签(总账备注=订单收入),按「店铺 + 订单号」匹配订单侧商品金额;一行 = 一个店铺的一个订单,商品金额只挂一次。「优惠金额」「冲减金额」另取总账备注=优惠收入 / 冲减收入的账单金额,不计入收入金额;「种菜金额」是订单号在种菜明细表或聚水潭订单标签明细(是否种菜订单=是)任意一边出现过时,该订单的收入金额(两个条件是或的关系),只是收入金额里的一部分,不参与差额;差额=(收入金额+优惠金额+冲减金额)−商品金额,只反映真正对不上的部分。', + 'description' => '覆盖店铺收款账户账单各平台页签(总账备注=订单收入),按「店铺 + 订单号」匹配订单侧商品金额;一行 = 一个店铺的一个订单,商品金额只挂一次。「优惠金额」「冲减金额」另取总账备注=优惠收入 / 冲减收入的账单金额,不计入收入金额;「种菜金额」是订单号在种菜明细表或聚水潭订单标签明细(是否种菜订单=是)任意一边出现过时,该订单的收入金额(两个条件是或的关系),只是收入金额里的一部分,不参与差额;差额=(收入金额+优惠金额+冲减金额)−商品金额,只反映真正对不上的部分。挂不到订单行的优惠/冲减会补成一行(收入/商品/差额留空):订单没有收款账单标「无收款账单」,账单没有订单号标「账单无单号」。异常标注分三类:单号问题(账单无单号 / 单号格式异常)、订单问题(订单表无此单 / 订单无商品金额 / 无收款账单)、匹配提示(店铺名不一致 / 订单侧同号多条 / 同单多笔收款)。', 'empty' => '店铺收款匹配订单明细尚未同步,请先执行 store-receipt:order-match:sync。', 'fields' => [ ['name' => 'platform', 'label' => '平台'], diff --git a/resources/views/business/pages/partials/directory-table.blade.php b/resources/views/business/pages/partials/directory-table.blade.php index fa38285e..22b9e7b6 100644 --- a/resources/views/business/pages/partials/directory-table.blade.php +++ b/resources/views/business/pages/partials/directory-table.blade.php @@ -1393,6 +1393,20 @@ background: #fffdf8; } #directory-table-performance-details thead th[data-directory-fixed="start"] { z-index: 5; } + + + /* 异常标注整理:按问题类型上色,一眼区分「要处理」和「只是提示」 */ + .receipt-remark-tag { + display: inline-block; + padding: 0.05rem 0.4rem; + border-radius: 999px; + font-size: 0.72rem; + line-height: 1.5; + white-space: nowrap; + } + .receipt-remark-tag--error { background: #fdeaea; color: #b3261e; } + .receipt-remark-tag--warn { background: #fdf3e3; color: #96690a; } + .receipt-remark-tag--info { background: #eaf1fb; color: #1f5fa8; }
{{-- 查询时保留当前每页条数 --}} @@ -1449,15 +1463,28 @@ 线上单号: -