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