## 任务占用与边界 - 任务名称:财务分析开票费用与业绩明细违约赔付预览 - 实际开发者: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
63 lines
2.8 KiB
PHP
63 lines
2.8 KiB
PHP
<?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;
|
||
}
|
||
}
|
||
}
|