163 lines
6.1 KiB
PHP
163 lines
6.1 KiB
PHP
<?php
|
||
|
||
namespace App\Business;
|
||
|
||
use App\Models\Business\FundForecast;
|
||
use Illuminate\Http\UploadedFile;
|
||
use Illuminate\Support\Facades\DB;
|
||
use Illuminate\Validation\ValidationException;
|
||
use PhpOffice\PhpSpreadsheet\IOFactory;
|
||
use PhpOffice\PhpSpreadsheet\Shared\Date as ExcelDate;
|
||
|
||
class FundForecastWorkbookImporter
|
||
{
|
||
private const HEADER_FIELDS = [
|
||
'预策月份' => 'budget_month',
|
||
'预测月份' => 'budget_month',
|
||
'公司名称' => 'company_name',
|
||
'余额/银行' => 'balance_bank',
|
||
'余额/京东' => 'balance_jd',
|
||
'余额/抖音' => 'balance_douyin',
|
||
'余额/得物' => 'balance_dewu',
|
||
'余额/淘系支付宝' => 'balance_taobao_alipay',
|
||
'余额/微信' => 'balance_wechat',
|
||
'收入/京东' => 'income_jd',
|
||
'收入/抖音' => 'income_douyin',
|
||
'收入/得物' => 'income_dewu',
|
||
'收入/淘系支付宝' => 'income_taobao_alipay',
|
||
'收入/货款' => 'income_goods_payment',
|
||
'收入/其他收入' => 'income_other',
|
||
'支出/供应商货款' => 'expense_supplier_goods',
|
||
'支出/快递款' => 'expense_courier',
|
||
'支出/纸厂压贴' => 'expense_paper_lamination',
|
||
'支出/封边条' => 'expense_edge_band',
|
||
'支出/日常报销' => 'expense_daily_reimbursement',
|
||
'支出/淘宝红包' => 'expense_taobao_red_packet',
|
||
'支出/板材' => 'expense_board',
|
||
'支出/店铺充值' => 'expense_store_top_up',
|
||
'支出/种菜' => 'expense_planting',
|
||
'支出/工资' => 'expense_salary',
|
||
'支出/税金' => 'expense_tax',
|
||
'支出/店铺花费暂估10%' => 'expense_store_spend',
|
||
];
|
||
|
||
public function import(UploadedFile $file, int $companyId): array
|
||
{
|
||
$spreadsheet = IOFactory::load($file->getRealPath());
|
||
$worksheet = $spreadsheet->getSheet(0);
|
||
$rows = $worksheet->toArray(null, true, false, false);
|
||
$columnMap = $this->columnMap($rows[0] ?? [], $rows[1] ?? []);
|
||
|
||
foreach (['budget_month', 'company_name'] as $required) {
|
||
if (! in_array($required, $columnMap, true)) {
|
||
throw ValidationException::withMessages([
|
||
'import' => '资金预测表缺少“预测月份”或“公司名称”表头。',
|
||
]);
|
||
}
|
||
}
|
||
|
||
$created = 0;
|
||
$updated = 0;
|
||
|
||
DB::transaction(function () use ($rows, $columnMap, $companyId, &$created, &$updated): void {
|
||
foreach (array_slice($rows, 2) as $offset => $row) {
|
||
if (collect($row)->every(fn ($value): bool => $value === null || trim((string) $value) === '')) {
|
||
continue;
|
||
}
|
||
|
||
$rowNumber = $offset + 3;
|
||
$data = [];
|
||
foreach ($columnMap as $columnIndex => $field) {
|
||
$data[$field] = $this->value($row[$columnIndex] ?? null, $field, $rowNumber);
|
||
}
|
||
|
||
if (($data['budget_month'] ?? '') === '' || ($data['company_name'] ?? '') === '') {
|
||
throw ValidationException::withMessages([
|
||
'import' => "第 {$rowNumber} 行必须填写预测月份和公司名称。",
|
||
]);
|
||
}
|
||
|
||
$identity = [
|
||
'company_id' => $companyId,
|
||
'budget_month' => $data['budget_month'],
|
||
'company_name' => $data['company_name'],
|
||
];
|
||
$record = FundForecast::query()->where($identity)->first();
|
||
$data['report_status'] = '待审核';
|
||
|
||
if ($record) {
|
||
$record->update($data);
|
||
$updated++;
|
||
} else {
|
||
FundForecast::create(array_merge($identity, $data));
|
||
$created++;
|
||
}
|
||
}
|
||
});
|
||
|
||
if ($created + $updated === 0) {
|
||
throw ValidationException::withMessages([
|
||
'import' => '表格中没有可导入的数据行。',
|
||
]);
|
||
}
|
||
|
||
return compact('created', 'updated');
|
||
}
|
||
|
||
private function columnMap(array $groups, array $headers): array
|
||
{
|
||
$map = [];
|
||
$group = '';
|
||
$count = max(count($groups), count($headers));
|
||
|
||
for ($index = 0; $index < $count; $index++) {
|
||
$candidate = trim((string) ($groups[$index] ?? ''));
|
||
if ($candidate !== '') {
|
||
$group = $candidate;
|
||
}
|
||
$header = trim((string) ($headers[$index] ?? ''));
|
||
$combined = $header === '' ? $group : ($group === '' ? $header : $group . '/' . $header);
|
||
$field = self::HEADER_FIELDS[$this->normalizeHeader($combined)] ?? null;
|
||
if ($field !== null) {
|
||
$map[$index] = $field;
|
||
}
|
||
}
|
||
|
||
return $map;
|
||
}
|
||
|
||
private function normalizeHeader(string $header): string
|
||
{
|
||
return preg_replace('/[\s_\-()()]+/u', '', trim($header));
|
||
}
|
||
|
||
private function value(mixed $value, string $field, int $rowNumber): mixed
|
||
{
|
||
if ($value === null || trim((string) $value) === '') {
|
||
return null;
|
||
}
|
||
if ($field === 'budget_month') {
|
||
if (is_numeric($value)) {
|
||
return ExcelDate::excelToDateTimeObject($value)->format('Y-m');
|
||
}
|
||
$text = trim((string) $value);
|
||
if (preg_match('/^(\d{4})[年\/.\-](\d{1,2})(?:月)?$/u', $text, $matches) === 1) {
|
||
$month = (int) $matches[2];
|
||
if ($month >= 1 && $month <= 12) {
|
||
return sprintf('%04d-%02d', (int) $matches[1], $month);
|
||
}
|
||
}
|
||
throw ValidationException::withMessages(['import' => "第 {$rowNumber} 行预测月份格式不正确。"]);
|
||
}
|
||
if ($field === 'company_name') {
|
||
return mb_substr(trim((string) $value), 0, 191);
|
||
}
|
||
$number = str_replace([',', ',', '¥', '¥'], '', trim((string) $value));
|
||
if (! is_numeric($number)) {
|
||
throw ValidationException::withMessages(['import' => "第 {$rowNumber} 行包含无效金额。"]);
|
||
}
|
||
|
||
return (float) $number;
|
||
}
|
||
}
|