- 工作台新增「钉钉机器人设置」:Webhook、加签密钥加密存公司设置,页面不回显;支持自定义关键词、汇总单独发群、@ 报销人花名 - 核对后点「发取消通知到钉钉群」:按发起人一人一条,按钉钉花名册(花名 → userid)@ 发起人;同一批已发过的不重复发,可勾选重发 - 新增 planting-reimbursement:daily-digest,每天 10:25(宿主机 cron)生成当天批次,把待核对条数/金额、可报销合计、取消/待复查数、自检结果和工作台链接推到群里;数据停更或看板未上传会提醒 - 种菜监控看板提示「职级为空」的单改为取消工单,群通知原因写「职级为空」(2026-11-13 报销人确认) - 没上传花名册时,收款人在职改按钉钉花名册在职真名判断;两边都取不到时不做该项校验并提示,避免误判 涉及:只读聚工单镜像与钉钉花名册副本;钉钉群机器人发消息;不涉及迁移,不写聚工单。 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VANWYsNUbu2piMtCKymoyd
136 lines
6.4 KiB
PHP
136 lines
6.4 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,
|
|
private readonly \Modules\EcommerceFinance\Services\PlantingReimbursementRobot $robot,
|
|
) {}
|
|
|
|
/** @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,
|
|
'workbenchRobot' => $this->robotSummary(),
|
|
'workbenchNoticeLog' => $this->robot->noticeLog((int) company_id(), $filters['date']),
|
|
'workbenchRosterSource' => $result['roster_source'] ?? null,
|
|
'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);
|
|
}
|
|
}
|
|
|
|
/** @return array<string,mixed> */
|
|
private function robotSummary(): array
|
|
{
|
|
try {
|
|
return $this->robot->summary();
|
|
} catch (\Throwable $exception) {
|
|
report($exception);
|
|
|
|
return ['configured' => false, 'webhook' => '', 'has_secret' => false, 'keyword' => '', 'reporters' => '', 'digest_webhook' => '', 'has_digest_secret' => false];
|
|
}
|
|
}
|
|
}
|