middleware('auth'); $this->middleware('company.identify'); } public function template(): BinaryFileResponse { $path = $this->template->path(); abort_unless(is_file($path), 404, '对账单模板不存在。'); return response()->download($path, $this->template->filename()); } public function upload(Request $request): JsonResponse|RedirectResponse { $request->validate([ 'import' => ['required', 'file', 'mimes:xlsx,xls,csv', 'max:51200'], ]); $supplier = $this->effectiveSupplier($request); $records = $this->template->parse($request->file('import'), $supplier); $rows = $this->statements->save( $supplier, $records, (string) $request->file('import')->getClientOriginalName(), (int) (company_id() ?: 1), user_id() ? (int) user_id() : null, ); $message = "对账单已保存:{$supplier} 共 {$rows} 行。"; return $request->expectsJson() ? response()->json(['message' => $message, 'rows' => $rows]) : back()->with('success', $message); } public function confirmReturn(Request $request): RedirectResponse { return $this->markReturn($request, SupplierReturnConfirmation::STATUS_CONFIRMED, '已确认该行退货扣减。'); } public function disputeReturn(Request $request): RedirectResponse { return $this->markReturn($request, SupplierReturnConfirmation::STATUS_DISPUTED, '已提交异议,复核通过前该行暂不扣减。'); } public function confirmAllReturns(Request $request): RedirectResponse { [$supplier, $companyId] = $this->returnTarget($request); $keys = array_values(array_map( static fn (array $row): string => (string) ($row['_action_key'] ?? ''), $this->pageData->returnRowsForFilters($supplier, $companyId, $request->all()), )); $confirmed = $this->returnConfirmations->confirmAll( $supplier, $keys, $companyId, user_id() ? (int) user_id() : null, ); return back()->with('success', "已全部确认:{$confirmed} 行退货扣减生效(异议行保持暂不扣减)。"); } public function exportReturns(Request $request): StreamedResponse { [$supplier, $companyId] = $this->returnTarget($request); $rows = $this->pageData->returnRowsForFilters($supplier, $companyId, $request->query()); return $this->returnCsvExporter->download($rows, $supplier); } public function exportComparison(Request $request): StreamedResponse { $request->validate([ 'month' => ['nullable', 'string', 'regex:/^\d{4}-\d{2}$/'], 'reconciler' => ['nullable', 'string', 'max:191'], ]); [$supplier, $companyId] = $this->returnTarget($request); $detail = $this->pageData->financeDetailForFilters($supplier, $companyId, $request->query()); return $this->comparisonCsvExporter->download( $detail, $supplier, trim((string) $request->query('month', '')), trim((string) $request->query('reconciler', '')), ); } public function saveDingTalkRobot(Request $request): RedirectResponse { $request->validate([ 'supplier' => ['required', 'string', 'max:191'], 'dingtalk_webhook' => ['nullable', 'string', 'max:2048'], 'dingtalk_secret' => ['nullable', 'string', 'max:256'], 'dingtalk_at_all' => ['nullable', 'boolean'], ]); [$supplier] = $this->returnTarget($request); $this->dingTalkNotifier->save( $supplier, (string) $request->input('dingtalk_webhook', ''), (string) $request->input('dingtalk_secret', ''), $request->boolean('dingtalk_at_all'), ); return back()->with('success', "{$supplier} 的钉钉群机器人已保存。"); } public function pushDifferences(Request $request): RedirectResponse { $request->validate([ 'supplier' => ['required', 'string', 'max:191'], 'month' => ['nullable', 'string', 'regex:/^\d{4}-\d{2}$/'], 'reconciler' => ['nullable', 'string', 'max:191'], 'scope' => ['required', 'in:all,row,selected'], 'purchase_no' => ['nullable', 'string', 'max:191'], 'date' => ['nullable', 'string', 'max:32'], 'barcode' => ['nullable', 'string', 'max:191'], 'difference_type' => ['nullable', 'string', 'max:32'], 'selected_rows' => ['nullable', 'array', 'max:600'], 'selected_rows.*' => ['string', 'max:2048'], ]); [$supplier, $companyId] = $this->returnTarget($request); $detail = $this->pageData->financeDetailForFilters($supplier, $companyId, $request->all()); $differences = array_values(array_filter( $detail['rows'], static fn (array $row): bool => ($row['type'] ?? '') !== SupplierStatementMatcher::TYPE_MATCH, )); if ($request->input('scope') === 'row') { $purchaseNo = trim((string) $request->input('purchase_no')); $date = trim((string) $request->input('date')); $barcode = trim((string) $request->input('barcode')); $type = trim((string) $request->input('difference_type')); $differences = array_values(array_filter( $differences, static fn (array $row): bool => (string) ($row['purchaseNo'] ?? '') === $purchaseNo && (string) ($row['date'] ?? '') === $date && (string) ($row['barcode'] ?? '') === $barcode && (string) ($row['type'] ?? '') === $type )); } elseif ($request->input('scope') === 'selected') { $selected = []; foreach ((array) $request->input('selected_rows', []) as $token) { $row = json_decode((string) $token, true); if (is_array($row)) { $selected[$this->differenceRowKey($row)] = true; } } $differences = array_values(array_filter( $differences, fn (array $row): bool => isset($selected[$this->differenceRowKey($row)]), )); } if ($differences === []) { return back()->withErrors(['dingtalk_push' => '当前筛选没有可推送的差异。']); } try { $result = $this->dingTalkNotifier->push($supplier, $differences, [ 'month' => (string) $request->input('month', ''), 'reconciler' => (string) $request->input('reconciler', ''), ]); } catch (Throwable $exception) { return back()->withErrors(['dingtalk_push' => '推送失败:' . $exception->getMessage()]); } return back()->with( 'success', "已推送 {$result['rows']} 条差异到 {$supplier} 钉钉群({$result['messages']} 条消息)。", ); } public function pushReturns(Request $request): RedirectResponse { $request->validate([ 'supplier' => ['required', 'string', 'max:191'], 'month' => ['required', 'string', 'regex:/^\d{4}-\d{2}$/'], ]); [$supplier, $companyId] = $this->returnTarget($request); $month = trim((string) $request->input('month')); $returns = $this->pageData->returnRowsForMonth($supplier, $companyId, $month); if ($returns === []) { return back()->withErrors([ 'dingtalk_return_push' => "该厂家在 {$month} 没有可推送的退货数据。", ]); } try { $result = $this->dingTalkNotifier->pushReturns($supplier, $returns, $month, $companyId); } catch (Throwable $exception) { return back()->withErrors(['dingtalk_return_push' => '推送失败:' . $exception->getMessage()]); } return back()->with( 'success', "已推送 {$result['rows']} 条退货到 {$supplier} 钉钉群({$month},{$result['messages']} 条消息)。", ); } public function pushReturnsBatch(Request $request): RedirectResponse { $request->validate([ 'selected_returns' => ['required', 'array', 'min:1', 'max:200'], 'selected_returns.*' => ['string', 'max:2048'], ]); $targets = []; foreach ((array) $request->input('selected_returns', []) as $encoded) { $selection = json_decode((string) $encoded, true); $supplier = is_array($selection) ? trim((string) ($selection['supplier'] ?? '')) : ''; $month = is_array($selection) ? trim((string) ($selection['month'] ?? '')) : ''; if ($supplier === '' || preg_match('/^\d{4}-\d{2}$/', $month) !== 1) { throw ValidationException::withMessages([ 'batch_return_push' => '批量退货选择无效,请刷新页面后重新选择。', ]); } $targets[$supplier] = $month; } $companyId = (int) (company_id() ?: 1); $returnsBySupplier = $this->pageData->returnRowsForMonthTargets($targets, $companyId); $pushedSuppliers = 0; $pushedRows = 0; $messages = 0; $skipped = []; foreach ($targets as $supplier => $month) { $returns = $returnsBySupplier[$supplier] ?? []; if ($returns === []) { $skipped[] = "{$supplier}({$month} 无退货)"; continue; } try { $result = $this->dingTalkNotifier->pushReturns($supplier, $returns, $month, $companyId); } catch (Throwable $exception) { $reason = $exception instanceof ValidationException ? (string) (collect($exception->errors())->flatten()->first() ?: $exception->getMessage()) : $exception->getMessage(); $skipped[] = $supplier . '(' . ($reason !== '' ? $reason : '推送失败') . ')'; continue; } $pushedSuppliers++; $pushedRows += (int) $result['rows']; $messages += (int) $result['messages']; } $skippedNote = ''; if ($skipped !== []) { $preview = implode('、', array_slice($skipped, 0, 6)); $skippedNote = ';未推送 ' . count($skipped) . ' 家:' . $preview . (count($skipped) > 6 ? ' 等' : ''); } if ($pushedSuppliers === 0) { return back()->withErrors([ 'batch_return_push' => '批量退货推送未完成' . $skippedNote, ]); } return back()->with( 'success', "已批量推送 {$pushedSuppliers} 家供应商,共 {$pushedRows} 条退货({$messages} 条钉钉消息){$skippedNote}。", ); } private function markReturn(Request $request, string $status, string $message): RedirectResponse { $request->validate(['key' => ['required', 'string', 'max:191']]); [$supplier, $companyId] = $this->returnTarget($request); $key = (string) $request->input('key'); $owned = collect($this->pageData->returnRows($supplier, $companyId)) ->contains(static fn (array $row): bool => (string) ($row['_action_key'] ?? '') === $key); abort_unless($owned, 403, '该退货明细不属于当前厂家账号。'); $this->returnConfirmations->mark( $supplier, $key, $status, $companyId, user_id() ? (int) user_id() : null, ); return back()->with('success', $message); } /** @return array{0:string,1:int} */ private function returnTarget(Request $request): array { return [$this->effectiveSupplier($request), (int) (company_id() ?: 1)]; } private function effectiveSupplier(Request $request): string { $user = $request->user(); if ($user && SupplierPortalAccess::isSupplierUser($user)) { $supplier = $this->supplierPortalAccess->supplierFor($user); abort_if(! $supplier, 403, '当前钉钉账号未绑定有效供应商,请联系财务人员。'); return $supplier; } $request->validate(['supplier' => ['required', 'string', 'max:191']]); $supplier = trim((string) $request->input('supplier')); abort_if($supplier === '', 422, '请选择厂家。'); return $supplier; } /** @param array $row */ private function differenceRowKey(array $row): string { return implode("\0", [ trim((string) ($row['purchaseNo'] ?? $row['purchase_no'] ?? '')), trim((string) ($row['date'] ?? '')), trim((string) ($row['barcode'] ?? '')), trim((string) ($row['type'] ?? '')), ]); } }