option('date')) ?: Workbench::defaultDate(); try { Workbench::parseDate($date); } catch (Throwable $exception) { $this->error($exception->getMessage()); return self::FAILURE; } $failed = false; foreach (Company::enabled()->orderBy('id')->get() as $company) { $company->makeCurrent(); if (! $robot->enabled()) { $this->line("#{$company->id}: 未配置种菜报销群机器人,跳过"); continue; } $lock = "planting-reimbursement:digest:{$company->id}:{$date}"; if (! $this->option('dry-run') && ! $this->option('force') && ! Cache::add($lock, now()->toDateTimeString(), now()->addDay())) { $this->line("#{$company->id}: {$date} 已推送过"); continue; } try { [$title, $text] = $this->message($workbench, $repository, (int) $company->id, $date); if ($this->option('dry-run')) { $this->line($title . "\n" . $text); continue; } $robot->sendDigest($title, $text); $this->info("#{$company->id}: 已推送 {$date} 汇总"); } catch (Throwable $exception) { Cache::forget($lock); report($exception); $this->error("#{$company->id}: 推送失败:" . $exception->getMessage()); $failed = true; } } return $failed ? self::FAILURE : self::SUCCESS; } /** @return array{0:string,1:string} */ private function message(Workbench $workbench, Repository $repository, int $companyId, string $date): array { $sync = $repository->syncStatus(); $url = route('business.pages.show', ['company_id' => $companyId, 'section' => 'planting-data', 'page' => 'planting-reimbursement-workbench', 'date' => $date]); $title = '种菜报销 ' . $date . ' 批次'; $lines = ['#### ' . $title]; $stale = $sync['code'] !== 'ready' || $sync['last_succeeded_at'] === null || now()->diffInMinutes(\Illuminate\Support\Carbon::parse($sync['last_succeeded_at']), true) > 20; if ($stale) { $lines[] = '> **聚工单数据不是最新的**(最后同步成功:' . ($sync['last_succeeded_at'] ? substr($sync['last_succeeded_at'], 0, 16) : '无') . '),先别按下面结果报销。'; } if (! in_array($sync['code'], ['ready', 'failed'], true)) { $lines[] = '- 聚工单镜像未就绪,暂时无法生成批次。'; $lines[] = '- [打开工作台](' . $url . ')'; return [$title, implode("\n", $lines)]; } $result = $workbench->build($companyId, $date); $s = $result['summary']; $lines[] = '- 待处理(付款 ' . substr($result['window']['from'], 0, 10) . ' ~ ' . substr($result['window']['to'], 0, 10) . '):**' . $s['source']['count'] . ' 条 / ' . number_format((float) $s['source']['amount'], 2) . ' 元**,请先在聚工单核对'; $lines[] = '- 可报销:' . $s['counts'][Auditor::DECISION_PASS] . ' 条 / **' . number_format((float) $s['total'], 2) . ' 元**(三大天猫 ' . number_format((float) $s['group_amounts'][Auditor::GROUP_BIG_TMALL], 2) . ',其他 ' . number_format((float) $s['group_amounts'][Auditor::GROUP_OTHER], 2) . ')'; $lines[] = '- 取消工单:' . $s['counts'][Auditor::DECISION_CANCEL] . ' 条;待复查:' . $s['counts'][Auditor::DECISION_RECHECK] . ' 条'; $bad = array_column(array_filter($s['checks'], static fn (array $check): bool => ! $check['ok']), 'label'); $lines[] = '- 自检:' . ($bad === [] ? '全部通过' : '未通过 ' . implode('、', $bad)); if (($result['uploads']['monitor'] ?? null) === null || substr((string) ($result['uploads']['monitor']['uploaded_at'] ?? ''), 0, 10) !== $date) { $lines[] = '- 今天还没上传种菜监控看板,超额尚未校验'; } if (($result['roster_source'] ?? '') === 'none') { $lines[] = '- 取不到在职名单,收款人在职未校验'; } $lines[] = '- [打开工作台核对](' . $url . ')'; return [$title, implode("\n", $lines)]; } }