'company_name', '银行名称' => 'bank_name', '银行类型' => 'bank_type', '账户账号' => 'account_number', '预留手机号' => 'reserved_phone', '登录密码' => 'login_password', '备注' => 'notes', ]; private const STORE_COLUMNS = [ '公司名称' => 'company_name', '账户名称' => 'account_name', '账户账号' => 'account_number', '预留手机号' => 'reserved_phone', '登录密码' => 'login_password', '备注' => 'notes', ]; public function downloadTemplate(string $platform): StreamedResponse { $spreadsheet = $this->makeSpreadsheet($platform, collect()); $filename = $this->title($platform) . '-导入模板.xlsx'; return $this->download($spreadsheet, $filename); } public function downloadAccounts(string $platform, Collection $accounts): StreamedResponse { $spreadsheet = $this->makeSpreadsheet($platform, $accounts); $filename = $this->title($platform) . '-批量数据-' . now()->format('Ymd-His') . '.xlsx'; return $this->download($spreadsheet, $filename); } public function import(string $platform, UploadedFile $file): int { try { return $this->readAndStore($platform, $file); } catch (ValidationException $exception) { throw $exception; } catch (Throwable $exception) { report($exception); throw ValidationException::withMessages([ 'import' => trans('fund_accounts.import_invalid_file'), ]); } } private function makeSpreadsheet(string $platform, Collection $accounts): Spreadsheet { $spreadsheet = new Spreadsheet(); $sheet = $spreadsheet->getActiveSheet(); $columns = $this->columns($platform); $columnCount = count($columns); $lastColumn = Coordinate::stringFromColumnIndex($columnCount); $sheet->setTitle(mb_substr($this->title($platform), 0, 31)); $sheet->freezePane('A2'); $sheet->setAutoFilter("A1:{$lastColumn}1"); $sheet->getRowDimension(1)->setRowHeight(26); foreach (array_keys($columns) as $index => $heading) { $column = Coordinate::stringFromColumnIndex($index + 1); $sheet->setCellValueExplicit("{$column}1", $heading, DataType::TYPE_STRING); } $header = $sheet->getStyle("A1:{$lastColumn}1"); $header->getFont()->setBold(true)->getColor()->setARGB('FFFFFFFF'); $header->getFill()->setFillType(Fill::FILL_SOLID)->getStartColor()->setARGB('FF4F7D3B'); $header->getAlignment()->setVertical('center'); foreach ($accounts->values() as $rowIndex => $account) { $excelRow = $rowIndex + 2; foreach (array_values($columns) as $columnIndex => $field) { $column = Coordinate::stringFromColumnIndex($columnIndex + 1); $value = $account->{$field}; $sheet->setCellValueExplicit( "{$column}{$excelRow}", is_null($value) ? '' : (string) $value, DataType::TYPE_STRING ); } } foreach (array_values($columns) as $index => $field) { $column = Coordinate::stringFromColumnIndex($index + 1); $sheet->getColumnDimension($column)->setWidth($this->columnWidth($field)); $sheet->getStyle("{$column}2:{$column}" . (self::MAX_ROWS + 1)) ->getNumberFormat() ->setFormatCode(NumberFormat::FORMAT_TEXT); } return $spreadsheet; } private function readAndStore(string $platform, UploadedFile $file): int { $reader = IOFactory::createReaderForFile($file->getRealPath()); $reader->setReadDataOnly(true); $spreadsheet = $reader->load($file->getRealPath()); try { $sheet = $spreadsheet->getActiveSheet(); $columns = $this->columns($platform); $expectedHeadings = array_keys($columns); $actualHeadings = []; foreach ($expectedHeadings as $index => $heading) { $column = Coordinate::stringFromColumnIndex($index + 1); $actualHeadings[] = trim($this->cellString($sheet->getCell("{$column}1")->getValue())); } if ($actualHeadings !== $expectedHeadings) { throw ValidationException::withMessages([ 'import' => trans('fund_accounts.import_header_mismatch', [ 'headers' => implode('、', $expectedHeadings), ]), ]); } $highestRow = $sheet->getHighestDataRow(); if ($highestRow > self::MAX_ROWS + 1) { throw ValidationException::withMessages([ 'import' => trans('fund_accounts.import_too_many_rows', ['count' => self::MAX_ROWS]), ]); } $rows = []; $errors = []; $fields = array_values($columns); $attributes = array_flip($columns); for ($excelRow = 2; $excelRow <= $highestRow; $excelRow++) { $row = []; foreach ($fields as $index => $field) { $column = Coordinate::stringFromColumnIndex($index + 1); $value = $this->cellString($sheet->getCell("{$column}{$excelRow}")->getValue()); $row[$field] = $field === 'login_password' ? $value : trim($value); } if ($this->rowIsEmpty($row)) { continue; } $validator = Validator::make($row, $this->rules($platform), [], $attributes); if ($validator->fails()) { $errors[] = trans('fund_accounts.import_row_error', [ 'row' => $excelRow, 'message' => $validator->errors()->first(), ]); continue; } $rows[] = $row; } if (! empty($errors)) { throw ValidationException::withMessages(['import' => $errors]); } if (empty($rows)) { throw ValidationException::withMessages([ 'import' => trans('fund_accounts.import_empty'), ]); } DB::transaction(function () use ($platform, $rows) { foreach ($rows as $row) { if ($platform === 'bank') { $row['account_name'] = $row['bank_name']; } else { $row['bank_name'] = null; $row['bank_type'] = null; } FundAccount::create(array_merge($row, [ 'company_id' => company_id(), 'platform' => $platform, 'created_from' => 'core::import', 'created_by' => user()->id, ])); } }); return count($rows); } finally { $spreadsheet->disconnectWorksheets(); } } private function download(Spreadsheet $spreadsheet, string $filename): StreamedResponse { return response()->streamDownload(function () use ($spreadsheet) { $writer = IOFactory::createWriter($spreadsheet, 'Xlsx'); $writer->setPreCalculateFormulas(false); $writer->save('php://output'); $spreadsheet->disconnectWorksheets(); }, $filename, [ 'Content-Type' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', 'Cache-Control' => 'no-store, no-cache, must-revalidate', ]); } private function columns(string $platform): array { return $platform === 'bank' ? self::BANK_COLUMNS : self::STORE_COLUMNS; } private function rules(string $platform): array { return [ 'company_name' => ['required', 'string', 'max:191'], 'account_name' => [$platform === 'bank' ? 'nullable' : 'required', 'nullable', 'string', 'max:191'], 'account_number' => ['required', 'string', 'max:191'], 'bank_name' => [$platform === 'bank' ? 'required' : 'nullable', 'nullable', 'string', 'max:191'], 'bank_type' => [$platform === 'bank' ? 'required' : 'nullable', 'nullable', 'string', 'max:191'], 'login_password' => ['nullable', 'string', 'max:191'], 'reserved_phone' => ['nullable', 'string', 'max:191'], 'notes' => ['nullable', 'string', 'max:2000'], ]; } private function rowIsEmpty(array $row): bool { foreach ($row as $value) { if ($value !== '') { return false; } } return true; } private function cellString(mixed $value): string { if ($value instanceof RichText) { return $value->getPlainText(); } if (is_null($value)) { return ''; } return (string) $value; } private function columnWidth(string $field): int { return match ($field) { 'company_name', 'account_name', 'account_number', 'login_password' => 24, 'bank_name' => 20, 'bank_type', 'reserved_phone' => 18, 'notes' => 36, default => 16, }; } private function title(string $platform): string { $key = $platform === 'bank' ? 'bank_accounts' : $platform . '_accounts'; return trans('fund_accounts.' . $key); } }