chore: sync release updates
This commit is contained in:
@@ -1045,7 +1045,11 @@ class AppraisalTasksController
|
||||
}
|
||||
|
||||
try {
|
||||
$asset = $this->evidenceService()->upload($request);
|
||||
$scene = (string)$request->input('upload_scene', 'appraisal_evidence');
|
||||
if (!in_array($scene, ['appraisal_evidence', 'zhongjian_report'], true)) {
|
||||
$scene = 'appraisal_evidence';
|
||||
}
|
||||
$asset = $this->evidenceService()->upload($request, 'file', $scene);
|
||||
return api_success($asset);
|
||||
} catch (\Throwable $e) {
|
||||
return api_error($e->getMessage(), 422);
|
||||
@@ -1859,11 +1863,13 @@ class AppraisalTasksController
|
||||
'brand_name' => $product['brand_name'] ?? '',
|
||||
'color' => $product['color'] ?? '',
|
||||
'size_spec' => $product['size_spec'] ?? '',
|
||||
'serial_no' => $product['serial_no'] ?? '',
|
||||
], JSON_UNESCAPED_UNICODE),
|
||||
'result_snapshot_json' => json_encode([
|
||||
'result_status' => $resultPayload['result_status'],
|
||||
'result_text' => $resultPayload['result_text'],
|
||||
'result_desc' => $resultPayload['result_desc'],
|
||||
'external_remark' => $resultPayload['external_remark'] ?? '',
|
||||
'key_points' => $this->loadLatestOrderKeyPoints($orderId),
|
||||
], JSON_UNESCAPED_UNICODE),
|
||||
'appraisal_snapshot_json' => json_encode($appraisalSnapshot, JSON_UNESCAPED_UNICODE),
|
||||
|
||||
24
server-api/app/controller/admin/FileUploadController.php
Normal file
24
server-api/app/controller/admin/FileUploadController.php
Normal file
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
namespace app\controller\admin;
|
||||
|
||||
use app\support\FileUploadService;
|
||||
use support\Request;
|
||||
|
||||
class FileUploadController
|
||||
{
|
||||
public function directPolicy(Request $request)
|
||||
{
|
||||
try {
|
||||
return api_success((new FileUploadService())->createOssDirectUploadPolicy(
|
||||
$request,
|
||||
(string)$request->input('upload_scene', ''),
|
||||
(string)$request->input('original_name', ''),
|
||||
(int)$request->input('file_size', 0),
|
||||
(string)$request->input('mime_type', '')
|
||||
));
|
||||
} catch (\Throwable $e) {
|
||||
return api_error($e->getMessage(), 422);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -275,7 +275,16 @@ class SystemConfigsController
|
||||
'title' => 'OSS Endpoint',
|
||||
'field_type' => 'text',
|
||||
'placeholder' => '例如 oss-cn-shenzhen.aliyuncs.com',
|
||||
'remark' => '填写 Bucket 所在地域的公网 Endpoint。',
|
||||
'remark' => '后台服务端 SDK 使用的 Endpoint。可填公网 Endpoint;如服务器在同地域内网,也可填内网 Endpoint。',
|
||||
'is_secret' => false,
|
||||
'visible_when' => ['config_key' => 'driver', 'equals' => 'oss'],
|
||||
],
|
||||
[
|
||||
'config_key' => 'oss_upload_endpoint',
|
||||
'title' => 'OSS 直传 Endpoint',
|
||||
'field_type' => 'text',
|
||||
'placeholder' => '例如 oss-cn-shenzhen.aliyuncs.com',
|
||||
'remark' => '前端直传 OSS 使用的公网 Endpoint。为空时沿用 OSS Endpoint;如 OSS Endpoint 填了内网地址,这里必须填写公网地址。',
|
||||
'is_secret' => false,
|
||||
'visible_when' => ['config_key' => 'driver', 'equals' => 'oss'],
|
||||
],
|
||||
@@ -324,6 +333,16 @@ class SystemConfigsController
|
||||
'is_secret' => false,
|
||||
'visible_when' => ['config_key' => 'driver', 'equals' => 'oss'],
|
||||
],
|
||||
[
|
||||
'config_key' => 'direct_upload_max_size_mb',
|
||||
'title' => '直传文件大小上限 MB',
|
||||
'field_type' => 'text',
|
||||
'placeholder' => '默认 200',
|
||||
'remark' => '前端直传 OSS 的单文件最大大小,单位 MB。建议按业务网络环境设置,允许范围 1-2048。',
|
||||
'is_secret' => false,
|
||||
'default_value' => '200',
|
||||
'visible_when' => ['config_key' => 'driver', 'equals' => 'oss'],
|
||||
],
|
||||
[
|
||||
'config_key' => 'qiniu_bucket',
|
||||
'title' => '七牛 Bucket',
|
||||
@@ -452,6 +471,11 @@ class SystemConfigsController
|
||||
}
|
||||
}
|
||||
|
||||
$directUploadMaxSizeMb = trim((string)($configValueMap['file_storage.direct_upload_max_size_mb'] ?? '200'));
|
||||
if ($directUploadMaxSizeMb !== '' && (!ctype_digit($directUploadMaxSizeMb) || (int)$directUploadMaxSizeMb < 1 || (int)$directUploadMaxSizeMb > 2048)) {
|
||||
throw new \RuntimeException('直传文件大小上限需填写 1-2048 之间的整数');
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -43,7 +43,7 @@ class WarehouseWorkbenchController
|
||||
{
|
||||
$evidenceService = new AppraisalEvidenceService();
|
||||
try {
|
||||
$asset = $evidenceService->upload($request);
|
||||
$asset = $evidenceService->upload($request, 'file', 'warehouse_inbound_evidence');
|
||||
if (!in_array((string)($asset['file_type'] ?? ''), ['image', 'video'], true)) {
|
||||
$evidenceService->delete((string)($asset['file_url'] ?? ''));
|
||||
return api_error('拆包附件仅支持上传图片或视频', 422);
|
||||
@@ -59,7 +59,7 @@ class WarehouseWorkbenchController
|
||||
{
|
||||
$evidenceService = new AppraisalEvidenceService();
|
||||
try {
|
||||
$asset = $evidenceService->upload($request);
|
||||
$asset = $evidenceService->upload($request, 'file', 'warehouse_return_packing');
|
||||
if (!in_array((string)($asset['file_type'] ?? ''), ['image', 'video'], true)) {
|
||||
$evidenceService->delete((string)($asset['file_url'] ?? ''));
|
||||
return api_error('打包装箱附件仅支持上传图片或视频', 422);
|
||||
|
||||
@@ -104,7 +104,7 @@ class ReportsController
|
||||
'valuation_snapshot' => $this->decodeJsonField($content['valuation_snapshot_json'] ?? null),
|
||||
'risk_notice_text' => ($content['risk_notice_text'] ?? '') !== '' ? $content['risk_notice_text'] : $defaultRiskNotice,
|
||||
];
|
||||
$productDisplay = $this->buildProductDisplay($reportData, $payload['product_snapshot'], $payload['result_snapshot']);
|
||||
$productDisplay = $this->buildProductDisplay($reportData, $payload['product_snapshot'], $payload['result_snapshot'], $payload['valuation_snapshot']);
|
||||
$reportMedia = [
|
||||
'images' => $this->filterAssetsByType($evidenceAttachments, 'image'),
|
||||
];
|
||||
@@ -261,7 +261,7 @@ class ReportsController
|
||||
->find();
|
||||
$publishTime = (string)($report['publish_time'] ?: date('Y-m-d H:i:s'));
|
||||
$relativeDir = 'uploads/reports/' . date('Ymd', strtotime($publishTime));
|
||||
$filename = $report['report_no'] . '-v2.pdf';
|
||||
$filename = $report['report_no'] . '-v3.pdf';
|
||||
$relativePath = $relativeDir . '/' . $filename;
|
||||
|
||||
if ($existingFile && !empty($existingFile['file_url'])) {
|
||||
@@ -317,20 +317,26 @@ class ReportsController
|
||||
return $this->storage()->publicUrl($request, $relativePath);
|
||||
}
|
||||
|
||||
private function buildProductDisplay(array $report, array $productInfo, array $resultInfo): array
|
||||
private function buildProductDisplay(array $report, array $productInfo, array $resultInfo, array $valuationInfo = []): array
|
||||
{
|
||||
$items = [
|
||||
[
|
||||
'label' => '检测结论',
|
||||
'value' => $this->textValue($resultInfo['result_text'] ?? '') ?: '-',
|
||||
'remark' => $this->textValue($resultInfo['result_desc'] ?? ''),
|
||||
],
|
||||
[
|
||||
'label' => '品牌',
|
||||
'value' => $this->textValue($productInfo['brand_name'] ?? '') ?: '-',
|
||||
'remark' => '',
|
||||
],
|
||||
];
|
||||
$items = [];
|
||||
$this->appendDisplayItem(
|
||||
$items,
|
||||
'检测结论',
|
||||
$this->textValue($resultInfo['result_text'] ?? '') ?: '-',
|
||||
$this->textValue($resultInfo['result_desc'] ?? ''),
|
||||
true
|
||||
);
|
||||
|
||||
foreach ([
|
||||
'品类' => $productInfo['category_name'] ?? '',
|
||||
'品牌' => $productInfo['brand_name'] ?? '',
|
||||
'颜色' => $productInfo['color'] ?? '',
|
||||
'规格/尺寸' => $productInfo['size_spec'] ?? '',
|
||||
'序列号/编码' => $productInfo['serial_no'] ?? '',
|
||||
] as $label => $value) {
|
||||
$this->appendDisplayItem($items, $label, $value);
|
||||
}
|
||||
|
||||
foreach (($resultInfo['key_points'] ?? []) as $point) {
|
||||
if (!is_array($point)) {
|
||||
@@ -340,11 +346,30 @@ class ReportsController
|
||||
if ($label === '') {
|
||||
continue;
|
||||
}
|
||||
$items[] = [
|
||||
'label' => $label,
|
||||
'value' => $this->textValue($point['point_value'] ?? '') ?: '-',
|
||||
'remark' => $this->textValue($point['point_remark'] ?? ''),
|
||||
];
|
||||
$this->appendDisplayItem(
|
||||
$items,
|
||||
$label,
|
||||
$this->textValue($point['point_value'] ?? '') ?: '-',
|
||||
$this->textValue($point['point_remark'] ?? ''),
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
$conditionGrade = $this->textValue($valuationInfo['condition_grade'] ?? '');
|
||||
$conditionDesc = $this->textValue($valuationInfo['condition_desc'] ?? '');
|
||||
if ($conditionGrade !== '' || $conditionDesc !== '') {
|
||||
$this->appendDisplayItem($items, '成色评级', $conditionGrade ?: '-', $conditionDesc, true);
|
||||
}
|
||||
|
||||
$valuationRange = $this->formatValuationRange($valuationInfo['valuation_min'] ?? 0, $valuationInfo['valuation_max'] ?? 0);
|
||||
$valuationDesc = $this->textValue($valuationInfo['valuation_desc'] ?? '');
|
||||
if ($valuationRange !== '' || $valuationDesc !== '') {
|
||||
$this->appendDisplayItem($items, '估值区间', $valuationRange ?: '-', $valuationDesc, true);
|
||||
}
|
||||
|
||||
$externalRemark = $this->textValue($resultInfo['external_remark'] ?? '');
|
||||
if ($externalRemark !== '') {
|
||||
$this->appendDisplayItem($items, '备注', $externalRemark);
|
||||
}
|
||||
|
||||
return [
|
||||
@@ -354,6 +379,42 @@ class ReportsController
|
||||
];
|
||||
}
|
||||
|
||||
private function appendDisplayItem(array &$items, string $label, mixed $value, mixed $remark = '', bool $keepEmpty = false): void
|
||||
{
|
||||
$valueText = $this->textValue($value);
|
||||
$remarkText = $this->textValue($remark);
|
||||
if (!$keepEmpty && $valueText === '' && $remarkText === '') {
|
||||
return;
|
||||
}
|
||||
|
||||
$items[] = [
|
||||
'label' => $label,
|
||||
'value' => $valueText !== '' ? $valueText : '-',
|
||||
'remark' => $remarkText,
|
||||
];
|
||||
}
|
||||
|
||||
private function formatValuationRange(mixed $min, mixed $max): string
|
||||
{
|
||||
$minValue = (float)($min ?? 0);
|
||||
$maxValue = (float)($max ?? 0);
|
||||
if ($minValue <= 0 && $maxValue <= 0) {
|
||||
return '';
|
||||
}
|
||||
if ($minValue > 0 && $maxValue > 0) {
|
||||
return '¥' . $this->formatMoney($minValue) . ' - ¥' . $this->formatMoney($maxValue);
|
||||
}
|
||||
if ($minValue > 0) {
|
||||
return '¥' . $this->formatMoney($minValue) . ' 起';
|
||||
}
|
||||
return '¥' . $this->formatMoney($maxValue) . ' 内';
|
||||
}
|
||||
|
||||
private function formatMoney(float $value): string
|
||||
{
|
||||
return rtrim(rtrim(number_format($value, 2, '.', ''), '0'), '.');
|
||||
}
|
||||
|
||||
private function buildTraceInfo(int $orderId, array $appraisalInfo, array $evidenceAttachments, Request $request): array
|
||||
{
|
||||
$logs = $orderId > 0
|
||||
|
||||
@@ -52,6 +52,7 @@ class AdminAuthMiddleware implements MiddlewareInterface
|
||||
{
|
||||
return match (true) {
|
||||
str_starts_with($path, '/api/admin/dashboard') => ['dashboard.view'],
|
||||
str_starts_with($path, '/api/admin/file-upload/') => ['warehouse_workbench.manage', 'appraisal_tasks.manage', 'orders.manage'],
|
||||
str_starts_with($path, '/api/admin/manual-order/') => ['orders.manage', 'warehouse_workbench.manage'],
|
||||
str_starts_with($path, '/api/admin/orders') && strtoupper($method) === 'GET' => ['orders.manage', 'warehouse_workbench.manage'],
|
||||
str_starts_with($path, '/api/admin/order/') && strtoupper($method) === 'GET' => ['orders.manage', 'warehouse_workbench.manage'],
|
||||
|
||||
@@ -3,54 +3,20 @@
|
||||
namespace app\support;
|
||||
|
||||
use support\Request;
|
||||
use function pathinfo;
|
||||
use function parse_url;
|
||||
use function str_starts_with;
|
||||
use function strtolower;
|
||||
|
||||
class AppraisalEvidenceService
|
||||
{
|
||||
private const IMAGE_EXTENSIONS = ['jpg', 'jpeg', 'png', 'webp', 'gif', 'bmp', 'heic'];
|
||||
private const VIDEO_EXTENSIONS = ['mp4', 'mov', 'm4v', 'webm', 'avi', 'mpeg', 'mpg'];
|
||||
private const PDF_EXTENSIONS = ['pdf'];
|
||||
|
||||
public function upload(Request $request, string $inputName = 'file'): array
|
||||
public function upload(Request $request, string $inputName = 'file', string $scene = 'appraisal_evidence'): array
|
||||
{
|
||||
$file = $request->file($inputName);
|
||||
if (!$file || !$file->isValid()) {
|
||||
throw new \RuntimeException('上传文件无效');
|
||||
}
|
||||
|
||||
$extension = strtolower($file->getUploadExtension() ?: '');
|
||||
$fileType = $this->detectFileType($extension);
|
||||
if ($fileType === 'file') {
|
||||
throw new \RuntimeException('仅支持上传图片、视频或 PDF 文件');
|
||||
}
|
||||
|
||||
$filename = sprintf('evidence_%s.%s', uniqid(), $extension ?: 'dat');
|
||||
$relativeDir = 'uploads/appraisal-evidence/' . date('Ymd');
|
||||
$relativePath = $relativeDir . '/' . $filename;
|
||||
$this->storage()->putUploadedFile($file, $relativePath);
|
||||
|
||||
$fileUrl = $this->storage()->publicUrl($request, $relativePath);
|
||||
|
||||
return [
|
||||
'file_id' => md5($relativePath),
|
||||
'file_url' => $fileUrl,
|
||||
'thumbnail_url' => $fileType === 'image' ? $fileUrl : '',
|
||||
'name' => $file->getUploadName(),
|
||||
'file_type' => $fileType,
|
||||
'mime_type' => $this->mimeType($fileType, $extension),
|
||||
];
|
||||
return $this->fileUploadService()->upload($request, $scene, $inputName);
|
||||
}
|
||||
|
||||
public function delete(string $fileUrl): void
|
||||
{
|
||||
$relativePath = $this->storage()->storagePath($fileUrl);
|
||||
if (!str_starts_with($relativePath, 'uploads/appraisal-evidence/')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->storage()->delete($relativePath);
|
||||
$this->fileUploadService()->delete($fileUrl);
|
||||
}
|
||||
|
||||
public function normalize(mixed $attachments, ?Request $request = null, bool $forStorage = false): array
|
||||
@@ -112,30 +78,21 @@ class AppraisalEvidenceService
|
||||
|
||||
public function detectFileType(string $extension): string
|
||||
{
|
||||
if (in_array($extension, self::IMAGE_EXTENSIONS, true)) {
|
||||
return 'image';
|
||||
}
|
||||
if (in_array($extension, self::VIDEO_EXTENSIONS, true)) {
|
||||
return 'video';
|
||||
}
|
||||
if (in_array($extension, self::PDF_EXTENSIONS, true)) {
|
||||
return 'pdf';
|
||||
}
|
||||
return 'file';
|
||||
return $this->fileUploadService()->detectFileType($extension);
|
||||
}
|
||||
|
||||
private function mimeType(string $fileType, string $extension): string
|
||||
{
|
||||
return match ($fileType) {
|
||||
'image' => 'image/' . ($extension === 'jpg' ? 'jpeg' : ($extension ?: 'jpeg')),
|
||||
'video' => 'video/' . ($extension ?: 'mp4'),
|
||||
'pdf' => 'application/pdf',
|
||||
default => 'application/octet-stream',
|
||||
};
|
||||
return $this->fileUploadService()->mimeType($fileType, $extension);
|
||||
}
|
||||
|
||||
private function storage(): FileStorageService
|
||||
{
|
||||
return new FileStorageService();
|
||||
}
|
||||
|
||||
private function fileUploadService(): FileUploadService
|
||||
{
|
||||
return new FileUploadService();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,11 +18,13 @@ class FileStorageConfigService
|
||||
'driver' => $this->normalizeDriver((string)($rows['driver'] ?? 'local')),
|
||||
'public_base_url' => trim((string)($rows['public_base_url'] ?? '')),
|
||||
'oss_endpoint' => trim((string)($rows['oss_endpoint'] ?? '')),
|
||||
'oss_upload_endpoint' => trim((string)($rows['oss_upload_endpoint'] ?? '')),
|
||||
'oss_bucket' => trim((string)($rows['oss_bucket'] ?? '')),
|
||||
'oss_access_key_id' => trim((string)($rows['oss_access_key_id'] ?? '')),
|
||||
'oss_access_key_secret' => trim((string)($rows['oss_access_key_secret'] ?? '')),
|
||||
'oss_bucket_domain' => trim((string)($rows['oss_bucket_domain'] ?? '')),
|
||||
'oss_path_prefix' => trim((string)($rows['oss_path_prefix'] ?? '')),
|
||||
'direct_upload_max_size_mb' => trim((string)($rows['direct_upload_max_size_mb'] ?? '200')),
|
||||
'qiniu_bucket' => trim((string)($rows['qiniu_bucket'] ?? '')),
|
||||
'qiniu_access_key' => trim((string)($rows['qiniu_access_key'] ?? '')),
|
||||
'qiniu_secret_key' => trim((string)($rows['qiniu_secret_key'] ?? '')),
|
||||
@@ -136,6 +138,27 @@ class FileStorageConfigService
|
||||
return $this->normalizeEndpointHost($this->getConfig()['oss_endpoint']);
|
||||
}
|
||||
|
||||
public function uploadEndpoint(): string
|
||||
{
|
||||
$config = $this->getConfig();
|
||||
$endpoint = $config['oss_upload_endpoint'] !== '' ? $config['oss_upload_endpoint'] : $config['oss_endpoint'];
|
||||
|
||||
return $this->normalizeEndpointHost($endpoint);
|
||||
}
|
||||
|
||||
public function directUploadMaxBytes(): int
|
||||
{
|
||||
$value = (int)$this->getConfig()['direct_upload_max_size_mb'];
|
||||
$megabytes = max(1, min(2048, $value > 0 ? $value : 200));
|
||||
|
||||
return $megabytes * 1024 * 1024;
|
||||
}
|
||||
|
||||
public function directUploadMaxLabel(): string
|
||||
{
|
||||
return sprintf('%dMB', (int)($this->directUploadMaxBytes() / 1024 / 1024));
|
||||
}
|
||||
|
||||
public function accessKeyId(): string
|
||||
{
|
||||
return $this->getConfig()['oss_access_key_id'];
|
||||
|
||||
270
server-api/app/support/FileUploadService.php
Normal file
270
server-api/app/support/FileUploadService.php
Normal file
@@ -0,0 +1,270 @@
|
||||
<?php
|
||||
|
||||
namespace app\support;
|
||||
|
||||
use support\Request;
|
||||
use function base64_encode;
|
||||
use function gmdate;
|
||||
use function hash_hmac;
|
||||
use function in_array;
|
||||
use function json_encode;
|
||||
use function ltrim;
|
||||
use function pathinfo;
|
||||
use function preg_replace;
|
||||
use function sprintf;
|
||||
use function str_starts_with;
|
||||
use function strtolower;
|
||||
use function time;
|
||||
use function trim;
|
||||
|
||||
class FileUploadService
|
||||
{
|
||||
public const MAX_SERVER_UPLOAD_BYTES = 50 * 1024 * 1024;
|
||||
public const MAX_SERVER_UPLOAD_LABEL = '50MB';
|
||||
|
||||
private const IMAGE_EXTENSIONS = ['jpg', 'jpeg', 'png', 'webp', 'gif', 'bmp', 'heic'];
|
||||
private const VIDEO_EXTENSIONS = ['mp4', 'mov', 'm4v', 'webm', 'avi', 'mpeg', 'mpg'];
|
||||
private const PDF_EXTENSIONS = ['pdf'];
|
||||
|
||||
private const SCENES = [
|
||||
'appraisal_evidence' => [
|
||||
'base_dir' => 'uploads/appraisal-evidence',
|
||||
'filename_prefix' => 'evidence',
|
||||
'allowed_file_types' => ['image', 'video', 'pdf'],
|
||||
'invalid_type_message' => '仅支持上传图片、视频或 PDF 文件',
|
||||
],
|
||||
'zhongjian_report' => [
|
||||
'base_dir' => 'uploads/zhongjian-report',
|
||||
'filename_prefix' => 'zhongjian',
|
||||
'allowed_file_types' => ['image', 'video', 'pdf'],
|
||||
'invalid_type_message' => '仅支持上传图片、视频或 PDF 文件',
|
||||
],
|
||||
'warehouse_inbound_evidence' => [
|
||||
'base_dir' => 'uploads/warehouse-inbound-evidence',
|
||||
'filename_prefix' => 'inbound',
|
||||
'allowed_file_types' => ['image', 'video'],
|
||||
'invalid_type_message' => '拆包附件仅支持上传图片或视频',
|
||||
],
|
||||
'warehouse_return_packing' => [
|
||||
'base_dir' => 'uploads/warehouse-return-packing',
|
||||
'filename_prefix' => 'packing',
|
||||
'allowed_file_types' => ['image', 'video'],
|
||||
'invalid_type_message' => '打包装箱附件仅支持上传图片或视频',
|
||||
],
|
||||
];
|
||||
|
||||
public function upload(Request $request, string $scene, string $inputName = 'file'): array
|
||||
{
|
||||
$sceneConfig = $this->sceneConfig($scene);
|
||||
$file = $request->file($inputName);
|
||||
if (!$file || !$file->isValid()) {
|
||||
throw new \RuntimeException('上传文件无效');
|
||||
}
|
||||
if ($file->getSize() > self::MAX_SERVER_UPLOAD_BYTES) {
|
||||
throw new \RuntimeException('上传文件不能超过' . self::MAX_SERVER_UPLOAD_LABEL . ',请压缩后再上传');
|
||||
}
|
||||
|
||||
$extension = strtolower($file->getUploadExtension() ?: '');
|
||||
$fileType = $this->detectFileType($extension);
|
||||
$this->assertAllowedFileType($fileType, $sceneConfig);
|
||||
|
||||
$relativePath = $this->buildRelativePath($sceneConfig, $extension ?: 'dat');
|
||||
$this->storage()->putUploadedFile($file, $relativePath);
|
||||
|
||||
return $this->buildAsset(
|
||||
$request,
|
||||
$relativePath,
|
||||
$fileType,
|
||||
$extension,
|
||||
(string)$file->getUploadName(),
|
||||
''
|
||||
);
|
||||
}
|
||||
|
||||
public function createOssDirectUploadPolicy(
|
||||
Request $request,
|
||||
string $scene,
|
||||
string $originalName,
|
||||
int $fileSize = 0,
|
||||
string $mimeType = ''
|
||||
): array {
|
||||
$config = $this->configService();
|
||||
if (!$config->isOss()) {
|
||||
return ['enabled' => false];
|
||||
}
|
||||
|
||||
$sceneConfig = $this->sceneConfig($scene);
|
||||
$config->assertReady();
|
||||
$maxDirectUploadBytes = $config->directUploadMaxBytes();
|
||||
$maxDirectUploadLabel = $config->directUploadMaxLabel();
|
||||
|
||||
if ($fileSize > $maxDirectUploadBytes) {
|
||||
throw new \RuntimeException('上传文件不能超过' . $maxDirectUploadLabel . ',请压缩后再上传');
|
||||
}
|
||||
|
||||
$extension = $this->extensionFromNameOrMimeType($originalName, $mimeType);
|
||||
$fileType = $this->detectFileType($extension);
|
||||
$this->assertAllowedFileType($fileType, $sceneConfig);
|
||||
|
||||
$relativePath = $this->buildRelativePath($sceneConfig, $extension);
|
||||
$objectKey = $config->objectKey($relativePath);
|
||||
$expiration = gmdate('Y-m-d\TH:i:s\Z', time() + 600);
|
||||
$policy = base64_encode((string)json_encode([
|
||||
'expiration' => $expiration,
|
||||
'conditions' => [
|
||||
['content-length-range', 1, $maxDirectUploadBytes],
|
||||
['eq', '$key', $objectKey],
|
||||
['eq', '$success_action_status', '200'],
|
||||
],
|
||||
], JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE));
|
||||
$signature = base64_encode(hash_hmac('sha1', $policy, $config->accessKeySecret(), true));
|
||||
|
||||
return [
|
||||
'enabled' => true,
|
||||
'upload_url' => sprintf('https://%s.%s', $config->bucket(), $config->uploadEndpoint()),
|
||||
'form_data' => [
|
||||
'key' => $objectKey,
|
||||
'policy' => $policy,
|
||||
'OSSAccessKeyId' => $config->accessKeyId(),
|
||||
'signature' => $signature,
|
||||
'success_action_status' => '200',
|
||||
],
|
||||
'asset' => $this->buildAsset($request, $relativePath, $fileType, $extension, $originalName, $mimeType),
|
||||
'max_size' => $maxDirectUploadBytes,
|
||||
'max_size_text' => $maxDirectUploadLabel,
|
||||
'expires_at' => $expiration,
|
||||
];
|
||||
}
|
||||
|
||||
public function delete(string $fileUrl): void
|
||||
{
|
||||
$relativePath = $this->storage()->storagePath($fileUrl);
|
||||
if (!$this->isManagedUploadPath($relativePath)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->storage()->delete($relativePath);
|
||||
}
|
||||
|
||||
public function detectFileType(string $extension): string
|
||||
{
|
||||
if (in_array($extension, self::IMAGE_EXTENSIONS, true)) {
|
||||
return 'image';
|
||||
}
|
||||
if (in_array($extension, self::VIDEO_EXTENSIONS, true)) {
|
||||
return 'video';
|
||||
}
|
||||
if (in_array($extension, self::PDF_EXTENSIONS, true)) {
|
||||
return 'pdf';
|
||||
}
|
||||
return 'file';
|
||||
}
|
||||
|
||||
public function mimeType(string $fileType, string $extension): string
|
||||
{
|
||||
return match ($fileType) {
|
||||
'image' => 'image/' . ($extension === 'jpg' ? 'jpeg' : ($extension ?: 'jpeg')),
|
||||
'video' => 'video/' . ($extension ?: 'mp4'),
|
||||
'pdf' => 'application/pdf',
|
||||
default => 'application/octet-stream',
|
||||
};
|
||||
}
|
||||
|
||||
private function buildAsset(
|
||||
Request $request,
|
||||
string $relativePath,
|
||||
string $fileType,
|
||||
string $extension,
|
||||
string $originalName,
|
||||
string $mimeType
|
||||
): array {
|
||||
$fileUrl = $this->storage()->publicUrl($request, $relativePath);
|
||||
$name = trim($originalName) !== '' ? trim($originalName) : pathinfo($relativePath, PATHINFO_BASENAME);
|
||||
|
||||
return [
|
||||
'file_id' => md5($relativePath),
|
||||
'file_url' => $fileUrl,
|
||||
'thumbnail_url' => $fileType === 'image' ? $fileUrl : '',
|
||||
'name' => $name,
|
||||
'file_type' => $fileType,
|
||||
'mime_type' => trim($mimeType) !== '' ? trim($mimeType) : $this->mimeType($fileType, $extension),
|
||||
];
|
||||
}
|
||||
|
||||
private function buildRelativePath(array $sceneConfig, string $extension): string
|
||||
{
|
||||
return sprintf(
|
||||
'%s/%s/%s_%s.%s',
|
||||
$sceneConfig['base_dir'],
|
||||
date('Ymd'),
|
||||
$sceneConfig['filename_prefix'],
|
||||
uniqid(),
|
||||
$extension
|
||||
);
|
||||
}
|
||||
|
||||
private function sceneConfig(string $scene): array
|
||||
{
|
||||
$scene = trim($scene);
|
||||
if (!isset(self::SCENES[$scene])) {
|
||||
throw new \RuntimeException('未知上传场景');
|
||||
}
|
||||
|
||||
return self::SCENES[$scene];
|
||||
}
|
||||
|
||||
private function assertAllowedFileType(string $fileType, array $sceneConfig): void
|
||||
{
|
||||
if (!in_array($fileType, $sceneConfig['allowed_file_types'], true)) {
|
||||
throw new \RuntimeException((string)$sceneConfig['invalid_type_message']);
|
||||
}
|
||||
}
|
||||
|
||||
private function extensionFromNameOrMimeType(string $originalName, string $mimeType): string
|
||||
{
|
||||
$extension = strtolower((string)pathinfo(trim($originalName), PATHINFO_EXTENSION));
|
||||
$extension = preg_replace('/[^a-z0-9]/', '', $extension) ?: '';
|
||||
if ($extension !== '') {
|
||||
return $extension;
|
||||
}
|
||||
|
||||
return match (strtolower(trim($mimeType))) {
|
||||
'image/jpeg' => 'jpg',
|
||||
'image/png' => 'png',
|
||||
'image/webp' => 'webp',
|
||||
'image/gif' => 'gif',
|
||||
'image/bmp' => 'bmp',
|
||||
'image/heic' => 'heic',
|
||||
'video/mp4' => 'mp4',
|
||||
'video/quicktime' => 'mov',
|
||||
'video/x-m4v' => 'm4v',
|
||||
'video/webm' => 'webm',
|
||||
'video/x-msvideo' => 'avi',
|
||||
'video/mpeg' => 'mpeg',
|
||||
'application/pdf' => 'pdf',
|
||||
default => '',
|
||||
};
|
||||
}
|
||||
|
||||
private function isManagedUploadPath(string $relativePath): bool
|
||||
{
|
||||
$relativePath = ltrim($relativePath, '/');
|
||||
foreach (self::SCENES as $sceneConfig) {
|
||||
if (str_starts_with($relativePath, $sceneConfig['base_dir'] . '/')) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private function storage(): FileStorageService
|
||||
{
|
||||
return new FileStorageService();
|
||||
}
|
||||
|
||||
private function configService(): FileStorageConfigService
|
||||
{
|
||||
return new FileStorageConfigService();
|
||||
}
|
||||
}
|
||||
@@ -46,6 +46,7 @@ use app\controller\admin\SystemConfigsController as AdminSystemConfigsController
|
||||
use app\controller\admin\AuthController as AdminAuthController;
|
||||
use app\controller\admin\CustomersController as AdminCustomersController;
|
||||
use app\controller\admin\WarehouseWorkbenchController as AdminWarehouseWorkbenchController;
|
||||
use app\controller\admin\FileUploadController as AdminFileUploadController;
|
||||
use app\controller\open\OrdersController as OpenOrdersController;
|
||||
|
||||
Route::get('/', [app\controller\IndexController::class, 'json']);
|
||||
@@ -193,6 +194,7 @@ Route::get('/api/admin/ping', function () {
|
||||
Route::post('/api/admin/auth/login', [AdminAuthController::class, 'login']);
|
||||
Route::get('/api/admin/auth/me', [AdminAuthController::class, 'me']);
|
||||
Route::post('/api/admin/auth/logout', [AdminAuthController::class, 'logout']);
|
||||
Route::post('/api/admin/file-upload/direct-policy', [AdminFileUploadController::class, 'directPolicy']);
|
||||
Route::get('/api/admin/dashboard', [AdminDashboardController::class, 'index']);
|
||||
Route::get('/api/admin/orders', [AdminOrdersController::class, 'index']);
|
||||
Route::get('/api/admin/order/detail', [AdminOrdersController::class, 'detail']);
|
||||
|
||||
@@ -18,6 +18,7 @@ return [
|
||||
'pid_file' => runtime_path() . '/webman.pid',
|
||||
'status_file' => runtime_path() . '/webman.status',
|
||||
'stdout_file' => runtime_path() . '/logs/stdout.log',
|
||||
'log_file' => runtime_path() . '/logs/workerman.log',
|
||||
'max_package_size' => 10 * 1024 * 1024
|
||||
];
|
||||
'log_file' => runtime_path() . '/logs/workerman.log',
|
||||
// Keep this above the 50m Nginx upload limit so Workerman does not reject videos first.
|
||||
'max_package_size' => 64 * 1024 * 1024
|
||||
];
|
||||
|
||||
Reference in New Issue
Block a user