CaiWuXiTong/app/Utilities/FundAccountSpreadsheet.php
WuYongJie 9595267ee7
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
chore: 导入 Akaunting 初始源码与团队协作规则
2026-07-20 18:20:25 +08:00

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);
}
}