CaiWuXiTong/modules/EcommerceFinance/View/PlantingReimbursementWorkbenchPageData.php
limeng 951e553fae feat(种菜报销): 新增种菜报销工作台,逐单校验并生成打款明细与钉钉流程明细 (#1140)
- 种菜数据汇总新增「种菜报销工作台」:按聚工单锁单口径取单(待处理 WAIT_CONFIRM、付款 D-7~D-1、ERP订单状态非取消/删除/空、售后单号为空;可切换处理中 CONFIRMING 和付款起始日),逐单校验重复报销、金额公式、佣金资格、收款人在职、以旧换新买家实付(取买家实付/看板订单实付中最低,查不到标签的待复查)、看板超额(同日按付款先后报到日限额)与看板未匹配
- 导出前须勾选「已在聚工单核对条数和金额一致」;页面给出应核对的条数/金额、各口径合计自检,聚工单镜像停更超过 20 分钟时提示先别报销
- 页面给出可报销合计(三大天猫/其他)、钉钉「种菜报销流程」明细行、按发起人的发群通知与取消清单
- 导出种菜对账、种菜报销明细(打款卡号明细表)、聚工单种菜任务工单导入模版,格式对齐手工文件
- 财务可上传种菜监控看板、报销佣金人员、花名册在职、店铺信息;名单存 storage 持久卷 JSON,花名册只存姓名
- 聚工单镜像读取补充 ERP订单标签、待报销工单和已报销订单查询
涉及:只读聚工单镜像;不涉及迁移、上游写入、钉钉和聚工单写操作。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VANWYsNUbu2piMtCKymoyd
2026-11-12 18:54:21 +08:00

120 lines
5.7 KiB
PHP

<?php
namespace Modules\EcommerceFinance\View;
use InvalidArgumentException;
use Modules\EcommerceFinance\Repositories\PlantingReimbursementRepository as Repository;
use Modules\EcommerceFinance\Services\PlantingReimbursementAuditor as Auditor;
use Modules\EcommerceFinance\Services\PlantingReimbursementReferences as References;
use Modules\EcommerceFinance\Services\PlantingReimbursementWorkbench as Workbench;
/** 种菜数据汇总 → 种菜报销工作台:待报销种菜工单逐单校验,出打款明细、钉钉流程明细和要取消的工单。 */
class PlantingReimbursementWorkbenchPageData
{
public const TITLE = '种菜报销工作台';
/** 聚工单镜像超过这么久没同步成功就提示数据不是最新的。 */
public const STALE_MINUTES = 20;
public const DECISION_FILTERS = ['all' => '全部', Auditor::DECISION_PASS => '可报销', Auditor::DECISION_CANCEL => '取消工单', Auditor::DECISION_RECHECK => '待复查'];
public const COLUMNS = [
'decision_label' => '处理结果', 'issue_text' => '原因', 'work_order_no' => '工单编号', 'status' => '工单状态',
'paid_at' => '付款时间', 'initiator' => '发起人', 'shop_name' => '店铺', 'profit_center' => '所属利润中心',
'online_order_no' => '线上订单号', 'task_payment' => '任务付款', 'task_freight' => '快递费', 'task_commission' => '佣金',
'jingxi_price_diff' => '京喜差价', 'task_deduction' => '扣款', 'task_empty_package_fee' => '空包费',
'reimbursement_amount' => '最终报销金额', 'actual_paid_amount' => '买家实付', 'payee_name' => '收款人',
'payee_account' => '账号', 'payee_bank' => '开户行', 'erp_order_label' => 'ERP订单标签', 'task_remark' => '任务备注',
];
public const MONEY_COLUMNS = [
'task_payment', 'task_freight', 'task_commission', 'jingxi_price_diff', 'task_deduction',
'task_empty_package_fee', 'reimbursement_amount', 'actual_paid_amount',
];
public function __construct(
private readonly Workbench $workbench,
private readonly Repository $repository,
private readonly References $references,
) {}
/** @return array{date:string,paid_from:string,scope:string,decision:string} */
public function filters(): array
{
$date = trim((string) request('date', '')) ?: Workbench::defaultDate();
try {
$paidFrom = trim((string) request('paid_from', '')) ?: Workbench::defaultPaidFrom($date);
Workbench::paidWindow($date, $paidFrom);
} catch (InvalidArgumentException $exception) {
abort(422, $exception->getMessage());
}
$scope = (string) request('scope', '') ?: 'pending';
abort_unless(isset(Workbench::SCOPES[$scope]), 422, '取单范围无效。');
$decision = (string) request('decision', '') ?: 'all';
abort_unless(isset(self::DECISION_FILTERS[$decision]), 422, '处理结果筛选无效。');
return ['date' => $date, 'paid_from' => $paidFrom, 'scope' => $scope, 'decision' => $decision];
}
/** @param array{date:string,paid_from:string,scope:string} $filters */
public function build(array $filters): array
{
return $this->workbench->build((int) company_id(), $filters['date'], $filters['paid_from'], Workbench::scopeStatuses($filters['scope']));
}
public function page(): array
{
$filters = $this->filters();
$sync = $this->repository->syncStatus();
$result = null;
$error = null;
if (! in_array($sync['code'], ['ready', 'failed'], true)) {
$error = '聚工单镜像未就绪,暂时无法生成报销批次。';
} else {
try {
$result = $this->build($filters);
} catch (\Throwable $exception) {
report($exception);
$error = '种菜报销工作台数据暂时无法读取,请稍后重试。';
}
}
$rows = $result['rows'] ?? [];
if ($filters['decision'] !== 'all') {
$rows = array_values(array_filter($rows, static fn (array $row): bool => $row['decision'] === $filters['decision']));
}
// 先看要处理的:取消、待复查在前。
$order = [Auditor::DECISION_CANCEL => 0, Auditor::DECISION_RECHECK => 1, Auditor::DECISION_PASS => 2];
usort($rows, static fn (array $a, array $b): int => [$order[$a['decision']], $a['initiator'], $a['paid_at']] <=> [$order[$b['decision']], $b['initiator'], $b['paid_at']]);
// 镜像停更超过 20 分钟或最近一次同步失败:数据不是最新的,提醒先别报销。
$syncStale = $sync['code'] !== 'ready' || $sync['last_succeeded_at'] === null
|| now()->diffInMinutes(\Illuminate\Support\Carbon::parse($sync['last_succeeded_at']), true) > self::STALE_MINUTES;
return [
'view' => 'business.pages.billing-transactions.planting-reimbursement-workbench',
'workbenchSyncStale' => $syncStale,
'workbenchFilters' => $filters,
'workbenchWindow' => Workbench::paidWindow($filters['date'], $filters['paid_from']),
'workbenchSync' => $sync,
'workbenchError' => $error,
'workbenchSummary' => $result['summary'] ?? null,
'workbenchRows' => $rows,
'workbenchUploads' => $result['uploads'] ?? $this->safeUploads(),
];
}
/** @return array<string,mixed> */
private function safeUploads(): array
{
try {
return $this->references->uploads((int) company_id());
} catch (\Throwable $exception) {
report($exception);
return array_fill_keys(array_keys(References::KINDS), null);
}
}
}