## 实际改动 - 为“其他附加费汇总”增加与“运费差”一致的导入弹窗和模板下载入口。 - 新增空白 Excel 模板,字段为日期、负责人、快递公司、快递单号、商品ID、条码、店铺、交易单号、金额、备注、成品条码。 - 日期及编号列按文本保存,金额列保留数值格式;模板不包含用户提供文件中的真实业务数据。 - 扩展模板下载路由与控制器映射,并补充权限、格式、长编号和实际导入回归测试。 ## 修改文件 - app/Http/Controllers/Business/CourierPayableSummaries.php - resources/templates/courier-payables/other-surcharge-import-template.xlsx - resources/views/business/pages/courier-payables/summary-table.blade.php - routes/admin.php - tests/Feature/Business/CourierPayableSummaryPagesTest.php ## 验证结果 - CourierPayableSummaryPagesTest:8 passed,88 assertions。 - Blade 编译:php artisan view:cache 通过。 - git diff --check 通过。 - 模板已核对为空白,且未提交真实财务数据。 ## 风险检查 - 涉及导入和页面权限,已增加对应回归测试。 - 不涉及数据库迁移、生产配置或数据修复。 - 前端复用现有 VIP 黑金组件样式,符合 DESIGN.md。 Reviewed-on: http://git.1080p.hk/WuYongJie/CaiWuXiTong/pulls/211
150 lines
5.2 KiB
PHP
150 lines
5.2 KiB
PHP
<?php
|
|
|
|
namespace App\Http\Controllers\Business;
|
|
|
|
use App\Business\CourierPayableSummaryStore;
|
|
use App\Utilities\SidebarPermissions;
|
|
use Illuminate\Http\RedirectResponse;
|
|
use Illuminate\Http\Request;
|
|
use Illuminate\Routing\Controller;
|
|
use RuntimeException;
|
|
use Symfony\Component\HttpFoundation\BinaryFileResponse;
|
|
use Symfony\Component\HttpFoundation\StreamedResponse;
|
|
|
|
class CourierPayableSummaries extends Controller
|
|
{
|
|
private const IMPORT_TEMPLATES = [
|
|
'freight-difference' => [
|
|
'path' => 'templates/courier-payables/freight-difference-import-template.xlsx',
|
|
'ascii_name' => 'freight_difference_import_template.xlsx',
|
|
'download_name' => '运费差导入模板.xlsx',
|
|
],
|
|
'other-surcharge-summary' => [
|
|
'path' => 'templates/courier-payables/other-surcharge-import-template.xlsx',
|
|
'ascii_name' => 'other_surcharge_import_template.xlsx',
|
|
'download_name' => '其他附加费导入模板.xlsx',
|
|
],
|
|
];
|
|
|
|
public function __construct(private readonly CourierPayableSummaryStore $store)
|
|
{
|
|
$this->middleware('auth');
|
|
$this->middleware('company.identify');
|
|
}
|
|
|
|
public function import(Request $request, string $summaryPage): RedirectResponse
|
|
{
|
|
$this->authorizePage($summaryPage);
|
|
$this->store->title($summaryPage);
|
|
$request->validate([
|
|
'import' => ['required', 'file', 'mimes:xlsx,xls,csv', 'max:10240'],
|
|
]);
|
|
|
|
try {
|
|
$result = $this->store->import(
|
|
$request->file('import'),
|
|
(int) (company_id() ?: 1),
|
|
$summaryPage,
|
|
user_id() ? (int) user_id() : null,
|
|
);
|
|
} catch (RuntimeException $exception) {
|
|
report($exception);
|
|
|
|
return $this->redirectToPage($summaryPage)->with('error', '汇总数据保存失败,请稍后重试。');
|
|
}
|
|
|
|
return $this->redirectToPage($summaryPage)->with(
|
|
'success',
|
|
"导入完成:{$result['rows']} 条数据,{$result['columns']} 个字段。",
|
|
);
|
|
}
|
|
|
|
public function template(string $summaryPage): BinaryFileResponse
|
|
{
|
|
$this->authorizePage($summaryPage);
|
|
$template = self::IMPORT_TEMPLATES[$summaryPage] ?? null;
|
|
abort_unless(is_array($template), 404);
|
|
|
|
$path = resource_path($template['path']);
|
|
abort_unless(is_file($path), 404);
|
|
|
|
$response = response()->download($path, $template['ascii_name'], [
|
|
'Content-Type' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
|
'Cache-Control' => 'private, no-store',
|
|
]);
|
|
|
|
return $response->setContentDisposition(
|
|
'attachment',
|
|
$template['download_name'],
|
|
$template['ascii_name'],
|
|
);
|
|
}
|
|
|
|
public function export(string $summaryPage): StreamedResponse|RedirectResponse
|
|
{
|
|
$this->authorizePage($summaryPage);
|
|
$title = $this->store->title($summaryPage);
|
|
|
|
try {
|
|
$snapshot = $this->store->snapshot((int) (company_id() ?: 1), $summaryPage);
|
|
} catch (RuntimeException) {
|
|
return $this->redirectToPage($summaryPage)->with('error', '汇总数据暂时无法读取,请重新导入后再试。');
|
|
}
|
|
|
|
if ($snapshot === null) {
|
|
return $this->redirectToPage($summaryPage)->with('error', '当前没有可导出的汇总数据。');
|
|
}
|
|
|
|
return response()->streamDownload(function () use ($snapshot): void {
|
|
$csv = fopen('php://output', 'w');
|
|
|
|
if ($csv === false) {
|
|
throw new RuntimeException('无法创建汇总数据导出流。');
|
|
}
|
|
|
|
fputs($csv, chr(0xEF) . chr(0xBB) . chr(0xBF));
|
|
fputcsv($csv, array_map([$this, 'csvText'], $snapshot['headers']));
|
|
|
|
foreach ($snapshot['rows'] as $row) {
|
|
fputcsv($csv, array_map([$this, 'csvText'], $row));
|
|
}
|
|
|
|
fclose($csv);
|
|
}, $title . '_' . now()->format('Ymd_His') . '.csv', [
|
|
'Content-Type' => 'text/csv; charset=UTF-8',
|
|
'Cache-Control' => 'private, no-store',
|
|
]);
|
|
}
|
|
|
|
private function redirectToPage(string $summaryPage): RedirectResponse
|
|
{
|
|
return redirect()->route('business.pages.show', [
|
|
'section' => 'courier-payables',
|
|
'page' => $summaryPage,
|
|
]);
|
|
}
|
|
|
|
private function authorizePage(string $summaryPage): void
|
|
{
|
|
$section = collect(SidebarPermissions::navigationFor(user()))
|
|
->firstWhere('slug', 'courier-payables');
|
|
$pages = collect($section['pages'] ?? [])->flatMap(static fn (array $page): array => [
|
|
$page,
|
|
...($page['children'] ?? []),
|
|
]);
|
|
|
|
abort_unless($pages->contains('slug', $summaryPage), 403, '当前账号没有该页面权限。');
|
|
}
|
|
|
|
private function csvText(mixed $value): string
|
|
{
|
|
$value = (string) $value;
|
|
|
|
if (preg_match('/^[+-]?(?:(?:\d{1,3}(?:,\d{3})+|\d+)(?:\.\d+)?|\.\d+)%?$/u', $value) === 1) {
|
|
return $value;
|
|
}
|
|
|
|
return preg_match('/^[=+\-@\t\r]/u', $value) === 1 ? "\t" . $value : $value;
|
|
}
|
|
}
|