CaiWuXiTong/app/Http/Controllers/Business/PlantingReimbursementWorkbench.php
limeng f78149e630 feat(种菜报销): 接入钉钉群机器人(取消通知 @ 发起人、每天 10:25 推送汇总),看板职级为空改为取消 (#1150)
- 工作台新增「钉钉机器人设置」: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
2026-11-13 11:15:04 +08:00

127 lines
5.9 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\Http\Controllers\Business;
use Illuminate\Http\Request;
use Illuminate\Routing\Controller;
use InvalidArgumentException;
use Modules\EcommerceFinance\Services\PlantingReimbursementReferences as References;
use Modules\EcommerceFinance\Services\PlantingReimbursementRobot as Robot;
use Modules\EcommerceFinance\Services\PlantingReimbursementWorkbookExporter as Exporter;
use Modules\EcommerceFinance\View\PlantingReimbursementWorkbenchPageData as PageData;
use PhpOffice\PhpSpreadsheet\Writer\Xlsx;
/** 种菜报销工作台:上传参考名单、导出对账 / 打款明细 / 种菜任务工单导入模板。 */
class PlantingReimbursementWorkbench extends Controller
{
public function upload(Request $request, string $kind, References $references)
{
abort_unless(isset(References::KINDS[$kind]), 404);
$request->validate(['file' => ['required', 'file', 'max:51200']]);
$file = $request->file('file');
$name = (string) $file->getClientOriginalName();
$redirect = redirect()->route('business.pages.show', [
'company_id' => company_id(),
'section' => 'planting-data',
'page' => 'planting-reimbursement-workbench',
'date' => (string) $request->input('date', ''),
'paid_from' => (string) $request->input('paid_from', ''),
'scope' => (string) $request->input('scope', ''),
]);
try {
$rows = $references->parse($kind, (string) $file->getRealPath(), $name);
$references->replace((int) company_id(), $kind, $rows, $name, optional(user())->id);
} catch (InvalidArgumentException $exception) {
return $redirect->with('error', References::KINDS[$kind] . '上传失败:' . $exception->getMessage());
} catch (\Throwable $exception) {
report($exception);
return $redirect->with('error', References::KINDS[$kind] . '上传失败,请检查文件格式后重新上传。');
}
return $redirect->with('success', sprintf('%s已更新:%s 条。', References::KINDS[$kind], number_format(count($rows))));
}
public function export(string $type, PageData $page, Exporter $exporter)
{
abort_unless(isset(Exporter::TYPES[$type]), 404);
$filters = $page->filters();
// 导出前必须先在聚工单核对条数、金额一致。
if (request('verified') !== '1') {
return redirect()->route('business.pages.show', [
'company_id' => company_id(), 'section' => 'planting-data', 'page' => 'planting-reimbursement-workbench',
] + array_diff_key($filters, ['decision' => true]))->with('error', '请先在聚工单按同样条件查询,核对条数和最终报销金额一致后,勾选「已核对」再导出。');
}
try {
$result = $page->build($filters);
$spreadsheet = $exporter->build($type, $result);
} catch (\Throwable $exception) {
report($exception);
abort(503, '聚工单镜像未就绪,未生成不完整的导出文件,请稍后重试。');
}
return response()->streamDownload(static function () use ($spreadsheet): void {
(new Xlsx($spreadsheet))->save('php://output');
}, $exporter->fileName($type, $result['summary']), [
'Content-Type' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
]);
}
public function saveRobot(Request $request, Robot $robot)
{
$robot->save([
'webhook' => (string) $request->input('webhook', ''),
'secret' => (string) $request->input('secret', ''),
'keyword' => (string) $request->input('keyword', ''),
'reporters' => (string) $request->input('reporters', ''),
'digest_webhook' => (string) $request->input('digest_webhook', ''),
'digest_secret' => (string) $request->input('digest_secret', ''),
'clear_digest' => $request->boolean('clear_digest'),
]);
return $this->back($request)->with('success', '钉钉机器人设置已保存。');
}
/** 按发起人把取消 / 待复查通知发到钉钉群(须先勾选已与聚工单核对)。 */
public function notify(Request $request, PageData $page, Robot $robot)
{
$filters = $page->filters();
if ($request->input('verified') !== '1') {
return $this->back($request)->with('error', '请先在聚工单核对条数和金额一致,勾选「已核对」后再发通知。');
}
try {
$result = $page->build($filters);
$sent = $robot->sendNotices((int) company_id(), $filters['date'], $result['summary']['notices'], $request->boolean('force'));
} catch (\Throwable $exception) {
report($exception);
return $this->back($request)->with('error', '群通知发送失败:' . mb_substr($exception->getMessage(), 0, 120));
}
$message = sprintf('群通知:发送 %d 人,跳过已发 %d 人', $sent['sent'], $sent['skipped']);
if ($sent['failed'] > 0) {
$message .= ',失败 ' . $sent['failed'] . ' 人';
}
if ($sent['unmatched'] !== []) {
$message .= ';钉钉里没匹配到、未能 @ 的:' . implode('、', $sent['unmatched']);
}
return $this->back($request)->with($sent['failed'] > 0 ? 'error' : 'success', $message . '。');
}
private function back(Request $request)
{
return redirect()->route('business.pages.show', array_filter([
'company_id' => company_id(),
'section' => 'planting-data',
'page' => 'planting-reimbursement-workbench',
'date' => (string) $request->input('date', ''),
'paid_from' => (string) $request->input('paid_from', ''),
'scope' => (string) $request->input('scope', ''),
], static fn ($v) => $v !== ''));
}
}