- 工作台新增「钉钉机器人设置」: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
127 lines
5.9 KiB
PHP
127 lines
5.9 KiB
PHP
<?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 !== ''));
|
||
}
|
||
}
|