safeName($batchNo, 'batch'), $this->safeName($verifyCode, 'code') ); } public function qrBatchDirectory(string $batchNo): string { return 'uploads/material-qrcodes/' . $this->safeName($batchNo, 'batch'); } public function packageRelativePath(string $batchNo): string { $safeBatchNo = $this->safeName($batchNo, 'batch'); return sprintf( 'uploads/material-packages/%s/material-batch-%s.zip', $safeBatchNo, $safeBatchNo ); } public function packageBatchDirectory(string $batchNo): string { return 'uploads/material-packages/' . $this->safeName($batchNo, 'batch'); } public function materialTagTemplatePath(): string { return dirname(__DIR__, 2) . '/resources/material-tag-template.jpg'; } public function publicPath(string $relativePath): string { return public_path() . '/' . ltrim($relativePath, '/'); } public function publicUrl(string $relativePath): string { $baseUrl = $this->localBaseUrl(); if ($baseUrl === '') { throw new \RuntimeException('本地文件公开访问域名未配置,无法生成二维码下载链接'); } return rtrim($baseUrl, '/') . '/' . ltrim($relativePath, '/'); } public function ensureParentDirectory(string $filePath): void { $dir = dirname($filePath); if (!is_dir($dir) && !mkdir($dir, 0775, true) && !is_dir($dir)) { throw new \RuntimeException('本地文件目录创建失败'); } } public function deleteBatchResources(string $batchNo): void { $this->deleteDirectory($this->publicPath($this->qrBatchDirectory($batchNo))); $this->deleteDirectory($this->publicPath($this->packageBatchDirectory($batchNo))); } public function deleteDirectory(string $dir): void { if (!is_dir($dir)) { return; } $items = scandir($dir); if ($items === false) { return; } foreach ($items as $item) { if ($item === '.' || $item === '..') { continue; } $path = $dir . DIRECTORY_SEPARATOR . $item; if (is_dir($path)) { $this->deleteDirectory($path); continue; } if (is_file($path)) { @unlink($path); } } @rmdir($dir); } public function safeName(string $value, string $fallback): string { $safe = preg_replace('/[^a-zA-Z0-9_-]/', '-', trim($value)); $safe = trim((string)$safe, '-_'); return $safe !== '' ? $safe : $fallback; } private function localBaseUrl(): string { foreach (['MATERIAL_LOCAL_BASE_URL', 'APP_PUBLIC_BASE_URL', 'PUBLIC_FILE_BASE_URL'] as $key) { $value = trim((string)($_ENV[$key] ?? '')); if ($value !== '') { return $this->normalizeBaseUrl($value); } } $notifyUrl = Db::name('system_configs') ->where('config_group', 'payment') ->where('config_key', 'notify_url') ->value('config_value'); if (is_string($notifyUrl) && trim($notifyUrl) !== '') { return $this->extractOrigin($notifyUrl); } if (!in_array(strtolower((string)($_ENV['APP_ENV'] ?? '')), ['production', 'prod'], true)) { return 'http://' . '127.0.0.' . '1:8787'; } return ''; } private function normalizeBaseUrl(string $baseUrl): string { $baseUrl = trim($baseUrl); if ($baseUrl === '') { return ''; } if (!preg_match('/^https?:\/\//i', $baseUrl)) { $baseUrl = 'https://' . ltrim($baseUrl, '/'); } return rtrim($baseUrl, '/'); } private function extractOrigin(string $url): string { $parts = parse_url(trim($url)); $scheme = (string)($parts['scheme'] ?? ''); $host = (string)($parts['host'] ?? ''); $port = (string)($parts['port'] ?? ''); if ($host === '') { return $this->normalizeBaseUrl($url); } $origin = ($scheme !== '' ? $scheme : 'https') . '://' . $host; if ($port !== '' && !(($scheme === 'http' && $port === '80') || ($scheme === 'https' && $port === '443'))) { $origin .= ':' . $port; } return rtrim($origin, '/'); } }