Some checks failed
Tests / PHP 8.1 (push) Has been cancelled
Tests / PHP 8.2 (push) Has been cancelled
Tests / PHP 8.3 (push) Has been cancelled
298 lines
10 KiB
PHP
298 lines
10 KiB
PHP
<?php
|
|
|
|
namespace App\Utilities;
|
|
|
|
use App\Models\Banking\FundAccount;
|
|
use Illuminate\Http\UploadedFile;
|
|
use Illuminate\Support\Collection;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Illuminate\Support\Facades\Validator;
|
|
use Illuminate\Validation\ValidationException;
|
|
use PhpOffice\PhpSpreadsheet\Cell\Coordinate;
|
|
use PhpOffice\PhpSpreadsheet\Cell\DataType;
|
|
use PhpOffice\PhpSpreadsheet\IOFactory;
|
|
use PhpOffice\PhpSpreadsheet\RichText\RichText;
|
|
use PhpOffice\PhpSpreadsheet\Spreadsheet;
|
|
use PhpOffice\PhpSpreadsheet\Style\Fill;
|
|
use PhpOffice\PhpSpreadsheet\Style\NumberFormat;
|
|
use Symfony\Component\HttpFoundation\StreamedResponse;
|
|
use Throwable;
|
|
|
|
class FundAccountSpreadsheet
|
|
{
|
|
private const MAX_ROWS = 5000;
|
|
|
|
private const BANK_COLUMNS = [
|
|
'公司名称' => '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);
|
|
}
|
|
}
|