diff --git a/app/Business/TaobaoAlipayBillSpreadsheetImporter.php b/app/Business/TaobaoAlipayBillSpreadsheetImporter.php new file mode 100644 index 00000000..c6fad7a1 --- /dev/null +++ b/app/Business/TaobaoAlipayBillSpreadsheetImporter.php @@ -0,0 +1,149 @@ +getSheetByName(self::SHEET_NAME); + + if (! $sheet) { + throw new InvalidArgumentException('未找到工作表“' . self::SHEET_NAME . '”。'); + } + + $headers = $sheet->rangeToArray('A1:Y1', null, false, false)[0]; + $headers = array_map(fn ($value) => trim((string) $value), $headers); + + if ($headers !== self::HEADERS) { + throw new InvalidArgumentException('表头与淘系支付宝账户模板不一致。'); + } + + $inserted = 0; + $updated = 0; + $skipped = 0; + + DB::transaction(function () use ($sheet, $companyId, &$inserted, &$updated, &$skipped) { + for ($rowNumber = 2; $rowNumber <= $sheet->getHighestDataRow(); $rowNumber++) { + $values = $sheet->rangeToArray("A{$rowNumber}:Y{$rowNumber}", null, false, false)[0]; + + if (collect($values)->every(fn ($value) => $value === null || trim((string) $value) === '')) { + $skipped++; + continue; + } + + $data = array_combine(self::FIELDS, $values); + $data = $this->normalizeRow($data, $rowNumber); + $record = AlipayBill::query() + ->where('company_id', $companyId) + ->where('record_code', $data['record_code']) + ->first(); + + $record ??= new AlipayBill(); + $wasExisting = $record->exists; + $record->timestamps = false; + $record->fill(array_merge($data, [ + 'company_id' => $companyId, + 'platform' => 'taobao', + 'created_from' => 'attachment-import', + ])); + $record->created_at = $data['created_at']; + $record->updated_at = $data['updated_at']; + $record->save(); + + $wasExisting ? $updated++ : $inserted++; + } + }); + + $spreadsheet->disconnectWorksheets(); + + return compact('inserted', 'updated', 'skipped'); + } + + private function normalizeRow(array $data, int $rowNumber): array + { + foreach ($data as $field => $value) { + $data[$field] = is_string($value) ? trim($value) : $value; + } + + if ($data['record_code'] === null || $data['record_code'] === '') { + throw new InvalidArgumentException("第 {$rowNumber} 行缺少记录编码。"); + } + + foreach (['occurred_at', 'created_at', 'updated_at'] as $field) { + $data[$field] = $this->normalizeDateTime($data[$field], $rowNumber); + } + + foreach (['income', 'expense', 'balance'] as $field) { + $data[$field] = $this->normalizeDecimal($data[$field], $rowNumber); + } + + foreach (self::REQUIRED_TEXT_FIELDS as $field) { + $data[$field] = $data[$field] ?? ''; + } + + return $data; + } + + private function normalizeDateTime($value, int $rowNumber): ?string + { + if ($value === null || $value === '') { + return null; + } + + try { + return is_numeric($value) + ? ExcelDate::excelToDateTimeObject((float) $value)->format('Y-m-d H:i:s') + : Carbon::parse((string) $value)->format('Y-m-d H:i:s'); + } catch (\Throwable) { + throw new InvalidArgumentException("第 {$rowNumber} 行包含无效日期时间。"); + } + } + + private function normalizeDecimal($value, int $rowNumber): string + { + $value = str_replace([',', ',', '¥', '¥'], '', (string) ($value ?? '0')); + + if (! is_numeric($value)) { + throw new InvalidArgumentException("第 {$rowNumber} 行包含无效金额。"); + } + + return number_format((float) $value, 6, '.', ''); + } +} diff --git a/app/Console/Commands/ImportTaobaoAlipayBills.php b/app/Console/Commands/ImportTaobaoAlipayBills.php new file mode 100644 index 00000000..57dae215 --- /dev/null +++ b/app/Console/Commands/ImportTaobaoAlipayBills.php @@ -0,0 +1,36 @@ +import( + (string) $this->argument('file'), + max(1, (int) $this->option('company')), + ); + } catch (Throwable $e) { + $this->components->error($e->getMessage()); + + return self::FAILURE; + } + + $this->components->info( + "导入完成:新增 {$result['inserted']} 条,更新 {$result['updated']} 条,跳过 {$result['skipped']} 条。" + ); + + return self::SUCCESS; + } +} diff --git a/deploy/local-init.sh b/deploy/local-init.sh index fe32e159..a01ff624 100644 --- a/deploy/local-init.sh +++ b/deploy/local-init.sh @@ -205,7 +205,7 @@ if [ -n "$build_reason" ]; then build_attempt=1 while [ "$build_attempt" -le 2 ]; do local_image_fingerprint="$(calculate_local_image_fingerprint)" - echo "==> [1/5] 构建本地开发镜像:$build_reason。" + echo "==> [1/5] 构建本地开发镜像:${build_reason}。" docker compose -f "$compose_file" build \ --build-arg "LOCAL_BUILD_FINGERPRINT=$local_image_fingerprint" \ app diff --git a/modules/EcommerceFinance/Http/Controllers/AlipayBills.php b/modules/EcommerceFinance/Http/Controllers/AlipayBills.php index 5df6b2f7..92382aff 100644 --- a/modules/EcommerceFinance/Http/Controllers/AlipayBills.php +++ b/modules/EcommerceFinance/Http/Controllers/AlipayBills.php @@ -73,6 +73,15 @@ class AlipayBills extends Controller $perPage = min(max((int) $request->get('limit', 10), 1), 100); $rows = $query->paginate($perPage); + $rows->getCollection()->transform(function (AlipayBill $bill) { + $data = $bill->toArray(); + + foreach (['occurred_at', 'created_at', 'updated_at'] as $field) { + $data[$field] = $bill->{$field}?->format('Y-m-d H:i:s'); + } + + return $data; + }); // 汇总:基于当前筛选结果(分页前) $summaryRow = (clone $query)->selectRaw( diff --git a/resources/assets/js/components/business/TaobaoAlipayBills.vue b/resources/assets/js/components/business/TaobaoAlipayBills.vue index 72ea15f0..1190d90d 100644 --- a/resources/assets/js/components/business/TaobaoAlipayBills.vue +++ b/resources/assets/js/components/business/TaobaoAlipayBills.vue @@ -85,7 +85,7 @@
| 基础信息 | +基础信息 | 账户与交易 | 金额(元) | 业务关联 | @@ -94,9 +94,6 @@|||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 记录编码 ⇅ | 原始店铺 | -财务备注 | -订单号 | -金额 | 自定义备注 | 账单项目 | 支付宝账号 | @@ -126,9 +123,6 @@||||||||||||||||||||
| {{ r.record_code }} | {{ r.store_name }} | -{{ r.finance_remark || '—' }} | -{{ r.order_no || '—' }} | -{{ r.amount === null || r.amount === undefined ? '—' : money(r.amount) }} | {{ r.custom_remark || '—' }} | {{ r.bill_item }} | {{ r.alipay_account }} | @@ -156,7 +150,7 @@{{ r.modifier || '—' }} | |||||||||||||||||||
| 没有符合条件的账单记录 | +没有符合条件的账单记录 | ||||||||||||||||||||||||||