197 lines
7.9 KiB
PHP

<?php
namespace App\Http\Controllers\Business;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Routing\Controller;
use Illuminate\Validation\ValidationException;
use Modules\EcommerceFinance\Repositories\ProductPriceMirrorRepository;
use Modules\EcommerceFinance\Services\ProductPriceWorkbookImporter;
use Modules\EcommerceFinance\Services\SupplierPayableImportTemplate;
use RuntimeException;
use Symfony\Component\HttpFoundation\StreamedResponse;
class ProductPrices extends Controller
{
public function __construct(
private readonly ProductPriceMirrorRepository $repository,
private readonly ProductPriceWorkbookImporter $importer,
private readonly SupplierPayableImportTemplate $template,
) {
$this->middleware('auth');
$this->middleware('company.identify');
}
public function export(Request $request): StreamedResponse|RedirectResponse
{
if (! $this->repository->ready() || ! $this->repository->initialized()) {
return $this->redirectToPage($request)
->with('error', '商品价格镜像尚未完成首次同步,暂时不能导出。');
}
$filters = $this->filters($request);
$companyId = (int) (company_id() ?: 1);
return response()->streamDownload(function () use ($filters, $companyId): void {
$csv = fopen('php://output', 'w');
if ($csv === false) {
throw new RuntimeException('无法创建商品价格导出流。');
}
fputs($csv, chr(0xEF) . chr(0xBB) . chr(0xBF));
fputcsv($csv, [
'记录标识', '业务编号', '商品编码', 'SPU', '商品名称', '类目', '生产状态',
'最低售价', '计算限价', '采购价', '运费', '京喜限价', '流程状态',
'审批结果', '流程标题', '流程完成时间', '人工维护', '维护时间',
]);
foreach ($this->repository->exportRows($filters, $companyId) as $row) {
fputcsv($csv, [
$this->csvText($row->upstream_key ?? ''),
$this->csvText($row->business_id ?? ''),
$this->csvText($row->barcode ?? ''),
$this->csvText($row->spu ?? ''),
$this->csvText($row->product_name ?? ''),
$this->csvText($row->category ?? ''),
$this->csvText($row->production_status ?? ''),
$row->minimum_price ?? null,
$row->price_limit ?? null,
$row->purchase_price ?? null,
$row->shipping_fee ?? null,
$row->jingxi_limit_price ?? null,
$this->csvText($row->approval_status ?? ''),
$this->csvText($row->approval_result ?? ''),
$this->csvText($row->workflow_title ?? ''),
$this->csvText($row->completed_at ?? ''),
(int) ($row->is_overridden ?? 0) === 1 ? '是' : '否',
$this->csvText($row->override_updated_at ?? ''),
]);
}
fclose($csv);
}, '商品价格_' . now()->format('Ymd_His') . '.csv', [
'Content-Type' => 'text/csv; charset=UTF-8',
'Cache-Control' => 'private, no-store',
]);
}
public function import(Request $request): RedirectResponse
{
$request->validate([
'import' => ['required', 'file', 'mimes:xlsx,xls,csv', 'max:10240'],
]);
$result = $this->importer->import(
$request->file('import'),
(int) (company_id() ?: 1),
user_id() ? (int) user_id() : null,
);
return $this->redirectToPage($request)->with(
'success',
"导入完成:新增维护 {$result['created']} 条,更新维护 {$result['updated']} 条。",
);
}
public function template(): StreamedResponse
{
return $this->template->download('商品价格', $this->importer->templateFields());
}
public function update(Request $request, string $upstreamKey): RedirectResponse
{
$values = $request->validate(
[
'barcode' => ['required', 'string', 'max:191'],
'spu' => ['nullable', 'string', 'max:191'],
'product_name' => ['nullable', 'string', 'max:5000'],
'category' => ['nullable', 'string', 'max:255'],
'production_status' => ['nullable', 'string', 'max:120'],
'minimum_price' => ['nullable', 'numeric', 'decimal:0,4', 'min:0', 'max:9999999999999999.9999'],
'price_limit' => ['nullable', 'numeric', 'decimal:0,4', 'min:0', 'max:9999999999999999.9999'],
'purchase_price' => ['nullable', 'numeric', 'decimal:0,4', 'min:0', 'max:9999999999999999.9999'],
'shipping_fee' => ['nullable', 'numeric', 'decimal:0,4', 'min:0', 'max:9999999999999999.9999'],
'jingxi_limit_price' => ['nullable', 'numeric', 'decimal:0,4', 'min:0', 'max:9999999999999999.9999'],
],
[
'minimum_price.decimal' => '最低售价最多支持 4 位小数。',
'price_limit.decimal' => '计算限价最多支持 4 位小数。',
'purchase_price.decimal' => '采购价最多支持 4 位小数。',
'shipping_fee.decimal' => '运费最多支持 4 位小数。',
'jingxi_limit_price.decimal' => '京喜限价最多支持 4 位小数。',
],
);
try {
$this->repository->applyOverrides(
[$upstreamKey => $values],
(int) (company_id() ?: 1),
user_id() ? (int) user_id() : null,
'manual',
);
} catch (RuntimeException $exception) {
if (str_starts_with($exception->getMessage(), 'product_price_record_not_found:')) {
abort(404, '商品价格记录不存在。');
}
if ($exception->getMessage() === 'product_price_maintenance_schema_missing') {
throw ValidationException::withMessages([
'barcode' => '商品价格维护功能尚未完成初始化,请稍后重试。',
]);
}
throw $exception;
}
return $this->redirectToPage($request)->with('success', '商品价格已保存。');
}
/** @return array{barcode:string,completed_from:string,completed_to:string} */
private function filters(Request $request): array
{
return [
'barcode' => mb_substr(trim((string) $request->query('barcode', '')), 0, 191),
'completed_from' => $this->date((string) $request->query('completed_from', '')),
'completed_to' => $this->date((string) $request->query('completed_to', '')),
];
}
private function redirectToPage(Request $request): RedirectResponse
{
$filters = $this->filters($request);
$query = array_filter([
...$filters,
'page' => max((int) $request->query('page', 1), 1),
], static fn (mixed $value): bool => $value !== '');
$url = route('business.pages.show', [
'company_id' => company_id() ?: 1,
'section' => 'supplier-payables',
'page' => 'product-prices',
]);
return redirect()->to($url . ($query === [] ? '' : '?' . http_build_query($query)));
}
private function date(string $value): string
{
$value = trim($value);
if (preg_match('/^\d{4}-\d{2}-\d{2}$/', $value) !== 1) {
return '';
}
[$year, $month, $day] = array_map('intval', explode('-', $value));
return checkdate($month, $day, $year) ? $value : '';
}
private function csvText(mixed $value): string
{
$value = (string) $value;
return preg_match('/^[=+\-@\t\r]/u', $value) === 1 ? "\t" . $value : $value;
}
}