CaiWuXiTong/app/Console/Commands/PreviewStoreReceiptDefaultCompensation.php
yueyang f7197b4976 财务分析新增开票/不开票费用,并补齐违约赔付预览 (#480)
## 任务占用与边界

- 任务名称:财务分析开票费用与业绩明细违约赔付预览
- 实际开发者:yueyang
- 任务来源/团队分工:负责人直接指定
- 目标结果:财务经营分析表按科目映射表拆分开票费用/不开票费用;业绩明细增加违约赔付(冲减收入)列并按指定顺序展示。
- 预计修改路径:业务 Pages Controller、财务分析视图、账单明细数据服务、业绩明细配置与相关测试。
- 将写入的公共契约:财务分析费用聚合、默认补偿预览命令、业绩明细列配置。
- 仅复用、明确不修改的公共契约:store_receipt_order_match_rows、ec_account_mapping_rules。
- 数据源、表/视图和字段:店铺收款账单镜像、store_receipt_order_match_rows、store_receipt_product_cost_rows、ec_account_mapping_rules。
- 明确不修改的范围:账单抓取与上游同步链路。

## 开工碰撞检查

- [x] Codex 已执行 scripts/codex-task-preflight.ps1
- [x] 已检查最新 main 和远程任务分支
- [x] 已搜索主线既有页面、接口和数据服务
- [x] 已将写入和复用契约分开
- [x] 预检列出的残留分支均无开放 PR;当前 2 个开放 PR 与本任务无关,确认无活跃写入冲突

## 实际改动

- 财务经营分析表增加“开票费用”“不开票费用”及合计;财务类型=费用,票据口径 0.06/0.09 归入开票,不开票归入不开票,金额按支出减收入净额。
- 费用聚合并入现有月度缓存,缓存键包含科目映射哈希,映射变化后自动失效。
- 业绩明细增加违约赔付(冲减收入)列,放在冲减金额后。
- CSV 导出列同步更新。

## 验证证据

- 正常流程:2026-07/08/09 实库数据计算成功;2026-09 开票费用 3,896,538.65,不开票费用 81,377.62。
- 实际页面:2026-09 财务经营分析表 438 行,两列与合计正常展示。
- 性能:单月费用聚合 7.0–13.6 秒,PHP 峰值约 65.5 MB;页面结果缓存 5 分钟。
- 自动测试:NavigationTest 目标 2 条通过(35 assertions),StoreReceiptMappingPreviewOverrideTest 2 条通过(5 assertions)。

## 风险检查

- [x] 不涉及数据库迁移或生产数据写入
- [x] 涉及金额口径,已用实库页面和固定总额核对
- [x] 未提交 .env、密码、Token、真实数据或构建产物
- [x] 前端符合 DESIGN.md 黑金表格样式

## 合并前复查

- [x] 已基于最新 main 53bd1dc4 变基
- [x] 完整 diff 自审,无冲突标记
- [x] 目标测试通过
- [x] 负责人已授权 Squash merge 和发布

Reviewed-on: #480
2026-10-23 19:05:35 +08:00

63 lines
2.8 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 App\Console\Commands;
use Illuminate\Console\Command;
use Modules\EcommerceFinance\Services\StoreReceiptDefaultCompensationSnapshotBuilder;
use Throwable;
class PreviewStoreReceiptDefaultCompensation extends Command
{
protected $signature = 'store-receipt:default-compensation:preview
{month : 账期 YYYY-MM}
{mapping : 最新科目映射表 xlsx 路径}
{--output= : 输出 JSON 路径;默认写入 storage/app/local/performance-details/default-compensation}';
protected $description = '本地预览:按最新科目映射表生成业绩明细「违约赔付(冲减收入)」金额快照,不写数据库';
public function handle(StoreReceiptDefaultCompensationSnapshotBuilder $builder): int
{
$startedAt = hrtime(true);
$month = trim((string) $this->argument('month'));
$mapping = (string) $this->argument('mapping');
try {
$snapshot = $builder->build($month, $mapping);
$path = (string) ($this->option('output') ?: storage_path(
'app/local/performance-details/default-compensation/' . $month . '.json'
));
$directory = dirname($path);
if (! is_dir($directory) && ! mkdir($directory, 0775, true) && ! is_dir($directory)) {
throw new \RuntimeException('无法创建快照目录:' . $directory);
}
$temporary = $path . '.tmp';
$json = json_encode($snapshot, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT);
if ($json === false || file_put_contents($temporary, $json, LOCK_EX) === false) {
throw new \RuntimeException('无法写入本地快照。');
}
rename($temporary, $path);
$this->info('本地预览快照已生成:' . $path);
$this->line('映射条数:' . $snapshot['mapping_rows']);
$this->line('关联行数:' . count($snapshot['amounts']));
$this->line('金额合计:' . number_format((float) $snapshot['total'], 2, '.', ''));
$this->line('耗时:' . number_format((hrtime(true) - $startedAt) / 1e6, 0) . ' ms');
$this->line('PHP 峰值内存:' . number_format(memory_get_peak_usage(true) / 1024 / 1024, 1) . ' MB');
foreach ($snapshot['source_stats'] as $platform => $stats) {
$this->line(sprintf(
' %s:账单行 %d,金额 %s',
$platform,
$stats['rows'],
number_format((float) $stats['amount'], 2, '.', ''),
));
}
return self::SUCCESS;
} catch (Throwable $exception) {
$this->error($exception->getMessage());
return self::FAILURE;
}
}
}