diff --git a/app/Console/Commands/SyncStoreReceiptOrderMatches.php b/app/Console/Commands/SyncStoreReceiptOrderMatches.php index 20d5bf5a..661b6647 100644 --- a/app/Console/Commands/SyncStoreReceiptOrderMatches.php +++ b/app/Console/Commands/SyncStoreReceiptOrderMatches.php @@ -37,8 +37,8 @@ class SyncStoreReceiptOrderMatches extends Command {--company=1 : 公司 ID(种菜订单号取自该公司的种菜明细表与聚水潭订单标签明细)} {--chunk=1200 : 单次向上游查询的订单号数量} {--limit=0 : 仅处理前 N 笔账单(调试用,0 表示全量)} - {--amounts-only : 只回填「优惠金额 / 冲减金额 / 种菜金额」三列,不重跑订单侧匹配} - {--planting-only : 只回填「种菜金额」一列(不读账单,直接按该月已有明细行的订单号刷,一次覆盖全平台)} + {--amounts-only : 只回填「优惠金额 / 冲减金额 / 种菜金额 / 标签」,不重跑订单侧匹配} + {--planting-only : 只回填「种菜金额 / 标签」(不读账单,直接按该月已有明细行的订单号刷,一次覆盖全平台)} {--dry-run : 只计算并打印统计,不写库(用于评估口径调整的影响)}'; protected $description = '按店铺收款账户账单匹配订单侧商品金额,生成「业绩明细」核对镜像'; @@ -142,6 +142,10 @@ class SyncStoreReceiptOrderMatches extends Command private const STATUS_UNMATCHED = '未匹配'; + private const PLANTING_LABEL_REIMBURSED = '种菜报销'; + + private const PLANTING_LABEL_UNREIMBURSED = '种菜未报销'; + /** * 异常标注取值(页面「匹配状态」筛选与导出共用)。 * @@ -401,8 +405,7 @@ class SyncStoreReceiptOrderMatches extends Command if (! empty($group['planting_reimbursement_seen'])) { $plantingReimbursementRows++; $plantingReimbursementTotal += (float) $group['income_amount']; - } - if (! empty($group['planting_unreimbursed_seen'])) { + } elseif (! empty($group['planting_unreimbursed_seen'])) { $plantingUnreimbursedRows++; $plantingUnreimbursedTotal += (float) $group['income_amount']; } @@ -414,12 +417,12 @@ class SyncStoreReceiptOrderMatches extends Command number_format($plantingTotal, 2), )); $this->line(sprintf( - ' 种菜报销:%d 行(按种菜明细表订单号),合计 %s', + ' 标签「种菜报销」:%d 行(按种菜明细表订单号),收入合计 %s', $plantingReimbursementRows, number_format($plantingReimbursementTotal, 2), )); $this->line(sprintf( - ' 种菜未报销:%d 行(按聚水潭种菜标签),合计 %s', + ' 标签「种菜未报销」:%d 行(按聚水潭种菜标签),收入合计 %s', $plantingUnreimbursedRows, number_format($plantingUnreimbursedTotal, 2), )); @@ -796,8 +799,7 @@ 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, - 'planting_reimbursement_amount' => ! empty($group['planting_reimbursement_seen']) ? $income : null, - 'planting_unreimbursed_amount' => ! empty($group['planting_unreimbursed_seen']) ? $income : null, + 'planting_label' => self::plantingLabel($group), 'product_amount' => $productAmount, 'paid_amount' => $paidAmount, 'difference' => $difference, @@ -912,9 +914,7 @@ class SyncStoreReceiptOrderMatches extends Command } - /** - * 只回填「种菜金额 / 种菜报销 / 种菜未报销」:不读账单,直接按订单号判断,是就写入收入金额。 - */ + /** 只回填「种菜金额 / 标签」:按种菜明细表与聚水潭种菜标签的并集判断。 */ private function backfillPlantingAmounts(string $reportMonth): int { if ((bool) $this->option('dry-run')) { @@ -926,7 +926,7 @@ class SyncStoreReceiptOrderMatches extends Command $reimbursementCores = $this->plantingReimbursementCores(); $unreimbursedCores = $this->plantingUnreimbursedCores(); if ($cores === []) { - $this->warn('没有识别到任何种菜订单号(种菜明细表与聚水潭订单标签明细的交集为空),本列将全部为空。'); + $this->warn('没有识别到任何种菜订单号(种菜明细表与聚水潭订单标签明细的并集为空),本列将全部为空。'); } $target = static fn () => DB::connection($connection)->table(self::TARGET_TABLE) @@ -935,16 +935,14 @@ class SyncStoreReceiptOrderMatches extends Command $rows = $target()->get(['row_key', 'order_core_no', 'income_amount']); $target()->update([ 'planting_amount' => null, - 'planting_reimbursement_amount' => null, - 'planting_unreimbursed_amount' => null, + 'planting_label' => null, ]); $values = []; + $labels = []; $total = 0.0; - $reimbursementValues = []; - $reimbursementTotal = 0.0; - $unreimbursedValues = []; - $unreimbursedTotal = 0.0; + $reimbursementRows = 0; + $unreimbursedRows = 0; foreach ($rows as $row) { $core = self::orderCoreNo((string) $row->order_core_no); if ($core === '') { @@ -957,31 +955,27 @@ class SyncStoreReceiptOrderMatches extends Command $total += $amount; } if (isset($reimbursementCores[$core])) { - $reimbursementValues[(string) $row->row_key] = $amount; - $reimbursementTotal += $amount; - } - if (isset($unreimbursedCores[$core])) { - $unreimbursedValues[(string) $row->row_key] = $amount; - $unreimbursedTotal += $amount; + $labels[(string) $row->row_key] = self::PLANTING_LABEL_REIMBURSED; + $reimbursementRows++; + } elseif (isset($unreimbursedCores[$core])) { + $labels[(string) $row->row_key] = self::PLANTING_LABEL_UNREIMBURSED; + $unreimbursedRows++; } } - $this->updateAmountColumn($connection, $reportMonth, 'planting_amount', $values); - $this->updateAmountColumn($connection, $reportMonth, 'planting_reimbursement_amount', $reimbursementValues); - $this->updateAmountColumn($connection, $reportMonth, 'planting_unreimbursed_amount', $unreimbursedValues); + $this->updateColumnValues($connection, $reportMonth, 'planting_amount', $values); + $this->updateColumnValues($connection, $reportMonth, 'planting_label', $labels); $this->info(sprintf( - '已回填 %s:种菜金额 %d / %d 行,合计 %s(识别 %d 个种菜订单号);种菜报销 %d 行,合计 %s(识别 %d 个种菜明细订单号);种菜未报销 %d 行,合计 %s(识别 %d 个聚水潭种菜标签订单号)。', + '已回填 %s:种菜金额 %d / %d 行,合计 %s(识别 %d 个种菜订单号);标签「种菜报销」%d 行(识别 %d 个种菜明细订单号);标签「种菜未报销」%d 行(识别 %d 个聚水潭种菜标签订单号)。', $reportMonth, count($values), count($rows), number_format($total, 2), count($cores), - count($reimbursementValues), - number_format($reimbursementTotal, 2), + $reimbursementRows, count($reimbursementCores), - count($unreimbursedValues), - number_format($unreimbursedTotal, 2), + $unreimbursedRows, count($unreimbursedCores), )); @@ -1022,16 +1016,14 @@ class SyncStoreReceiptOrderMatches extends Command 'offset_amount' => null, 'refund_amount' => null, 'planting_amount' => null, - 'planting_reimbursement_amount' => null, - 'planting_unreimbursed_amount' => null, + 'planting_label' => null, ]); $discounts = []; $offsets = []; $refunds = []; $plantings = []; - $plantingReimbursements = []; - $plantingUnreimbursed = []; + $plantingLabels = []; foreach ($groups as $group) { if (empty($group['discount_seen']) && empty($group['offset_seen']) @@ -1065,19 +1057,17 @@ class SyncStoreReceiptOrderMatches extends Command $plantings[$rowKey] = round((float) $group['income_amount'], 2); } if (! empty($group['planting_reimbursement_seen'])) { - $plantingReimbursements[$rowKey] = round((float) $group['income_amount'], 2); - } - if (! empty($group['planting_unreimbursed_seen'])) { - $plantingUnreimbursed[$rowKey] = round((float) $group['income_amount'], 2); + $plantingLabels[$rowKey] = self::PLANTING_LABEL_REIMBURSED; + } elseif (! empty($group['planting_unreimbursed_seen'])) { + $plantingLabels[$rowKey] = self::PLANTING_LABEL_UNREIMBURSED; } } - $this->updateAmountColumn($connection, $reportMonth, 'discount_amount', $discounts); - $this->updateAmountColumn($connection, $reportMonth, 'offset_amount', $offsets); - $this->updateAmountColumn($connection, $reportMonth, 'refund_amount', $refunds); - $this->updateAmountColumn($connection, $reportMonth, 'planting_amount', $plantings); - $this->updateAmountColumn($connection, $reportMonth, 'planting_reimbursement_amount', $plantingReimbursements); - $this->updateAmountColumn($connection, $reportMonth, 'planting_unreimbursed_amount', $plantingUnreimbursed); + $this->updateColumnValues($connection, $reportMonth, 'discount_amount', $discounts); + $this->updateColumnValues($connection, $reportMonth, 'offset_amount', $offsets); + $this->updateColumnValues($connection, $reportMonth, 'refund_amount', $refunds); + $this->updateColumnValues($connection, $reportMonth, 'planting_amount', $plantings); + $this->updateColumnValues($connection, $reportMonth, 'planting_label', $plantingLabels); // A 方案:本月本平台的补行(payment_count=0 的行)整体重建,保证幂等。 $target() @@ -1114,7 +1104,7 @@ class SyncStoreReceiptOrderMatches extends Command ]); $this->info(sprintf( - '已回填 %s %s:优惠 %d 行(%s)/ 冲减 %d 行(%s)/ 补「无收款账单/无单号」优惠冲减行 %d 行 / 种菜 %d 行(%s)/ 种菜报销 %d 行(%s)/ 种菜未报销 %d 行(%s),镜像原有 %d 行,未重建业绩明细。', + '已回填 %s %s:优惠 %d 行(%s)/ 冲减 %d 行(%s)/ 补「无收款账单/无单号」优惠冲减行 %d 行 / 种菜 %d 行(%s)/ 标签「种菜报销」%d 行 / 标签「种菜未报销」%d 行,镜像原有 %d 行,未重建业绩明细。', $month, $platform, count($discounts), @@ -1124,10 +1114,8 @@ class SyncStoreReceiptOrderMatches extends Command $promoRows, count($plantings), number_format(array_sum($plantings), 2), - count($plantingReimbursements), - number_format(array_sum($plantingReimbursements), 2), - count($plantingUnreimbursed), - number_format(array_sum($plantingUnreimbursed), 2), + count(array_filter($plantingLabels, static fn (string $label): bool => $label === self::PLANTING_LABEL_REIMBURSED)), + count(array_filter($plantingLabels, static fn (string $label): bool => $label === self::PLANTING_LABEL_UNREIMBURSED)), count($existing), )); @@ -1377,8 +1365,7 @@ class SyncStoreReceiptOrderMatches extends Command 'refund_amount' => ! empty($group['refund_seen']) ? round((float) $group['refund_amount'], 2) : null, // 补行没有订单侧商品金额,种菜金额同样留空;列集合与顺序必须与主行完全一致,批量插入才不会错列。 'planting_amount' => null, - 'planting_reimbursement_amount' => null, - 'planting_unreimbursed_amount' => null, + 'planting_label' => null, 'product_amount' => null, 'paid_amount' => null, 'difference' => null, @@ -1390,12 +1377,25 @@ class SyncStoreReceiptOrderMatches extends Command ]; } + /** @param array $group */ + private static function plantingLabel(array $group): ?string + { + if (! empty($group['planting_reimbursement_seen'])) { + return self::PLANTING_LABEL_REIMBURSED; + } + if (! empty($group['planting_unreimbursed_seen'])) { + return self::PLANTING_LABEL_UNREIMBURSED; + } + + return null; + } + /** - * 按 row_key 批量更新一列金额:只更新镜像里已有的行,不做插入。 + * 按 row_key 批量更新一列字段值:只更新镜像里已有的行,不做插入。 * - * @param array $values row_key => 金额 + * @param array $values row_key => 金额或文本 */ - private function updateAmountColumn(string $connection, string $reportMonth, string $column, array $values): void + private function updateColumnValues(string $connection, string $reportMonth, string $column, array $values): void { if ($values === []) { return; diff --git a/app/Http/Controllers/Business/Pages.php b/app/Http/Controllers/Business/Pages.php index 2d1372a4..4729c0ee 100644 --- a/app/Http/Controllers/Business/Pages.php +++ b/app/Http/Controllers/Business/Pages.php @@ -2082,8 +2082,7 @@ class Pages extends Controller 'offset_amount' => ['offset_amount', '冲减金额', 'money'], 'refund_amount' => ['refund_amount', '退款金额', 'money'], 'planting_amount' => ['planting_amount', '种菜金额', 'money'], - 'planting_reimbursement_amount' => ['planting_reimbursement_amount', '种菜报销', 'money'], - 'planting_unreimbursed_amount' => ['planting_unreimbursed_amount', '种菜未报销', 'money'], + 'planting_label' => ['planting_label', '标签', '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 2ed20800..a993fc41 100644 --- a/config/business_navigation.php +++ b/config/business_navigation.php @@ -834,8 +834,7 @@ $navigation = [ ['name' => 'offset_amount', 'label' => '冲减金额', 'kind' => 'number'], ['name' => 'refund_amount', 'label' => '退款金额', 'kind' => 'number'], ['name' => 'planting_amount', 'label' => '种菜金额', 'kind' => 'number'], - ['name' => 'planting_reimbursement_amount', 'label' => '种菜报销', 'kind' => 'number'], - ['name' => 'planting_unreimbursed_amount', 'label' => '种菜未报销', 'kind' => 'number'], + ['name' => 'planting_label', '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_23_170000_replace_planting_amount_columns_with_label.php b/database/migrations/2026_10_23_170000_replace_planting_amount_columns_with_label.php new file mode 100644 index 00000000..1b744e0c --- /dev/null +++ b/database/migrations/2026_10_23_170000_replace_planting_amount_columns_with_label.php @@ -0,0 +1,72 @@ +connection()); + if (! $schema->hasTable('store_receipt_order_match_rows')) { + return; + } + + if (! $schema->hasColumn('store_receipt_order_match_rows', 'planting_label')) { + $schema->table('store_receipt_order_match_rows', function (Blueprint $table): void { + $table->string('planting_label', 32) + ->nullable() + ->after('planting_amount') + ->comment('种菜标签:种菜报销 / 种菜未报销'); + }); + } + + $oldColumns = array_values(array_filter( + ['planting_reimbursement_amount', 'planting_unreimbursed_amount'], + static fn (string $column): bool => $schema->hasColumn('store_receipt_order_match_rows', $column), + )); + if ($oldColumns !== []) { + $schema->table('store_receipt_order_match_rows', function (Blueprint $table) use ($oldColumns): void { + $table->dropColumn($oldColumns); + }); + } + } + + public function down(): void + { + $schema = Schema::connection($this->connection()); + if (! $schema->hasTable('store_receipt_order_match_rows')) { + return; + } + + $hasReimbursement = $schema->hasColumn('store_receipt_order_match_rows', 'planting_reimbursement_amount'); + $hasUnreimbursed = $schema->hasColumn('store_receipt_order_match_rows', 'planting_unreimbursed_amount'); + $hasLabel = $schema->hasColumn('store_receipt_order_match_rows', 'planting_label'); + + if (! $hasReimbursement) { + $schema->table('store_receipt_order_match_rows', function (Blueprint $table): void { + $table->decimal('planting_reimbursement_amount', 20, 4) + ->nullable() + ->after('planting_amount'); + }); + } + if (! $hasUnreimbursed) { + $schema->table('store_receipt_order_match_rows', function (Blueprint $table): void { + $table->decimal('planting_unreimbursed_amount', 20, 4) + ->nullable() + ->after('planting_reimbursement_amount'); + }); + } + if ($hasLabel) { + $schema->table('store_receipt_order_match_rows', function (Blueprint $table): void { + $table->dropColumn('planting_label'); + }); + } + } +}; diff --git a/resources/views/business/pages/partials/directory-table.blade.php b/resources/views/business/pages/partials/directory-table.blade.php index 089e98c9..20df4686 100644 --- a/resources/views/business/pages/partials/directory-table.blade.php +++ b/resources/views/business/pages/partials/directory-table.blade.php @@ -1256,6 +1256,7 @@
  • 优惠/冲减:分别取优惠收入、冲减收入,不计入收入
  • 退款:单独列示,不参与收入和差额
  • 种菜:种菜订单的收入子集,不参与差额
  • +
  • 标签:种菜明细表命中为“种菜报销”,其余聚水潭种菜标签命中为“种菜未报销”
  • diff --git a/tests/Feature/Settings/NavigationTest.php b/tests/Feature/Settings/NavigationTest.php index b6cd5b61..fd9c36cb 100644 --- a/tests/Feature/Settings/NavigationTest.php +++ b/tests/Feature/Settings/NavigationTest.php @@ -99,16 +99,20 @@ class NavigationTest extends FeatureTestCase ->assertSee('data-column-key="discount_amount"', false) ->assertSee('data-column-key="offset_amount"', false) ->assertSee('data-column-key="planting_amount"', false) + ->assertSee('data-column-key="planting_label"', false) ->assertSee('data-column-key="product_amount"', false) ->assertSee('data-column-key="difference"', false) ->assertSee('data-column-key="match_status"', false) ->assertSee('data-column-key="remark"', false) + ->assertSeeText('标签:种菜明细表命中为“种菜报销”,其余聚水潭种菜标签命中为“种菜未报销”') // 子订单号已按业务要求移除。 ->assertDontSeeText('子订单号') // 展示粒度=一笔账单收款,不再展开发货明细列。 ->assertDontSee('data-column-key="warehouse"', false) ->assertDontSee('data-column-key="product_code"', false) ->assertDontSee('data-column-key="quantity"', false) + ->assertDontSee('data-column-key="planting_reimbursement_amount"', false) + ->assertDontSee('data-column-key="planting_unreimbursed_amount"', false) // 筛选栏:月份选择器 + 店铺名称 + 线上单号 + 匹配状态。 ->assertSee('data-inventory-month-picker', false) ->assertSee('name="store_name"', false) diff --git a/tests/Unit/EcommerceFinance/StoreReceiptMemoryTest.php b/tests/Unit/EcommerceFinance/StoreReceiptMemoryTest.php index 7750fe44..02981dac 100644 --- a/tests/Unit/EcommerceFinance/StoreReceiptMemoryTest.php +++ b/tests/Unit/EcommerceFinance/StoreReceiptMemoryTest.php @@ -21,7 +21,13 @@ class StoreReceiptMemoryTest extends TestCase ], $extra); } - private function aggregate(iterable $rows, array $planting = [], ?AccountLedgerRemarkMap $map = null): array + private function aggregate( + iterable $rows, + array $planting = [], + array $plantingReimbursement = [], + array $plantingUnreimbursed = [], + ?AccountLedgerRemarkMap $map = null, + ): array { $map ??= new class extends AccountLedgerRemarkMap { public function remark(?string $storeName, ?string $tabPlatform, ?string $billItem, array $businessRemarks = [], string $goodsName = ''): ?string @@ -32,7 +38,8 @@ class StoreReceiptMemoryTest extends TestCase return (new ReflectionMethod(SyncStoreReceiptOrderMatches::class, 'aggregateBills'))->invoke( new SyncStoreReceiptOrderMatches(), $rows, ['label' => '淘系支付宝'], 'online_order_no', 'occurred_at', - 'income_amount', 'expense_amount', 'online_real_store', $planting, [], [], $map, '淘宝', + 'income_amount', 'expense_amount', 'online_real_store', $planting, + $plantingReimbursement, $plantingUnreimbursed, $map, '淘宝', ); } @@ -156,13 +163,27 @@ class StoreReceiptMemoryTest extends TestCase )->willReturn('优惠收入'); $result = $this->aggregate([$this->bill('动态项目', '', 1.5, 0, [ 'custom_remark' => '活动优惠', 'second_remark' => '活动优惠', 'goods_name' => '商品关键词', - ])], [], $map); + ])], [], [], [], $map); $this->assertSame(1.5, $result['discountTotal']); $this->assertSame([], $result['groups']); $this->assertCount(1, $result['promoOnlyGroups']); $this->assertSame(0, $this->aggregate([])['billCount']); } + public function test_planting_label_prefers_reimbursed_and_uses_unreimbursed_fallback(): void + { + $method = new ReflectionMethod(SyncStoreReceiptOrderMatches::class, 'plantingLabel'); + $this->assertSame('种菜报销', $method->invoke(null, [ + 'planting_reimbursement_seen' => true, + 'planting_unreimbursed_seen' => true, + ])); + $this->assertSame('种菜未报销', $method->invoke(null, [ + 'planting_reimbursement_seen' => false, + 'planting_unreimbursed_seen' => true, + ])); + $this->assertNull($method->invoke(null, [])); + } + public function test_stream_uses_bound_filter_and_sql_limit_in_sqlite(): void { $db = DB::connection('business_mirror');