diff --git a/.gitignore b/.gitignore index 6e4eacd0c..cb038edbd 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,4 @@ src/runtime -src/upload \ No newline at end of file +src/upload +ftp-src +update.zip \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index 11a257c87..1a1bcc4c2 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -28,6 +28,7 @@ services: - "${PHP_FPM_PORT:-9000}:9000" # PHP-FPM - "${XDEBUG_POST:-9003}:9003" # Xdebug volumes: + - ./:/var/www/all_source - ./src:/var/www/html # 更新下载源列表以加速apt-get - ./docker/debian/sources.list:/etc/apt/sources.list:ro diff --git a/docker/php/Dockerfile b/docker/php/Dockerfile index d79591777..2f2f1d101 100644 --- a/docker/php/Dockerfile +++ b/docker/php/Dockerfile @@ -77,5 +77,19 @@ RUN echo "zend_extension=xdebug.so" > /usr/local/etc/php/conf.d/xdebug.ini # 暴露端口 EXPOSE 9000 9003 + +############ 查看 cron 进程 +## 查看 cron 进程 +# ps aux | grep "think cron:schedule" +# # 精确查找 +# pgrep -f "think cron:schedule" +# # 查看进程树 +# pstree -p | grep -i cron + +## 启动 cron 任务 +# 守护进程模式 +# nohup php think cron:schedule > /dev/null 2>&1 & +####################################### + # 启动Supervisor CMD ["/usr/bin/supervisord", "-c", "/etc/supervisor/conf.d/supervisord.conf"] \ No newline at end of file diff --git a/docs/common/cron.md b/docs/common/cron.md index 47d2835d9..cde5af71b 100644 --- a/docs/common/cron.md +++ b/docs/common/cron.md @@ -4,6 +4,22 @@ 本项目使用的 是 `yunwuxin/think-cron` 包,该包提供了丰富的功能,包括任务调度、任务执行、任务状态管理等。该包支持多种任务类型,如固定任务、循环任务、一次性任务等,同时支持多种执行周期,如分钟、小时、天、周、月等。 +## 手动启动计划任务 + +``` bash +############ 查看 cron 进程 +## 查看 cron 进程 +ps aux | grep "think cron:schedule" +# # 精确查找 +pgrep -f "think cron:schedule" +# # 查看进程树 +pstree -p | grep -i cron + +## 启动 cron 任务 +# 守护进程模式 +nohup php think cron:schedule > /dev/null 2>&1 & +####################################### +``` ## 计划任务管理 diff --git a/docs/deploy.md b/docs/deploy.md new file mode 100644 index 000000000..0f9a128ff --- /dev/null +++ b/docs/deploy.md @@ -0,0 +1,37 @@ +# 部署说明 + + +## 自动部署 + +### 创建部署包 + +```bash +# 代码示例 +cd /d/projects/shop-projects/backend +/d/phpstudy_pro/Extensions/php/php7.4.3nts/php.exe ./scripts/generate_deploy_package.php --source ./src --target /d/projects/zips/php后端源码/2025-11-10 --output update.zip + +# Linux +php ./scripts/generate_deploy_package.php --source ./src --target ./ftp-src --output update.zip +``` + +## 部署流程 + +``` bash +# 部署流程 +cd /var/www/all_source +# 测试本地部署 +php ./scripts/deploy.php --zip update.zip --target ./ftp-src --dry-run --verbose + +# 实际部署 +php ./scripts/deploy.php --zip update.zip --target ./ftp-src --verbose +``` + + + +### 真实环境部署 + + +``` base +cd /www/tools +php ./deploy.php --zip update.zip --target /www/wwwroot/myweb/newfwq --dry-run --verbose +``` \ No newline at end of file diff --git a/scripts/deploy.php b/scripts/deploy.php new file mode 100644 index 000000000..1d9ed1922 --- /dev/null +++ b/scripts/deploy.php @@ -0,0 +1,533 @@ +zipPath = $zipPath; + $this->targetDir = rtrim($targetDir, DIRECTORY_SEPARATOR); + $this->dryRun = $options['dryRun'] ?? false; + $this->verbose = $options['verbose'] ?? false; + $this->backup = $options['backup'] ?? false; + + $this->validatePaths(); + } + + /** + * 验证路径有效性 + */ + private function validatePaths(): void + { + if (!file_exists($this->zipPath)) { + throw new InvalidArgumentException("ZIP文件不存在: {$this->zipPath}"); + } + + if (!is_readable($this->zipPath)) { + throw new InvalidArgumentException("ZIP文件不可读: {$this->zipPath}"); + } + + if (!is_dir($this->targetDir)) { + throw new InvalidArgumentException("目标目录不存在: {$this->targetDir}"); + } + + if (!is_writable($this->targetDir)) { + throw new InvalidArgumentException("目标目录不可写: {$this->targetDir}"); + } + } + + /** + * 执行部署 + */ + public function deploy(): array + { + $this->logMessage('INFO', '开始自动部署流程'); + $this->logMessage('INFO', "ZIP文件: {$this->zipPath}"); + $this->logMessage('INFO', "目标目录: {$this->targetDir}"); + + try { + // 打开ZIP文件 + $zip = new ZipArchive(); + $openResult = $zip->open($this->zipPath); + + if ($openResult !== true) { + throw new RuntimeException("无法打开ZIP文件,错误代码: {$openResult}"); + } + + $this->logMessage('INFO', 'ZIP文件打开成功'); + + // 分析ZIP文件结构 + $zipFiles = $this->analyzeZipStructure($zip); + $this->logMessage('INFO', "ZIP包中包含 {$zipFiles['fileCount']} 个文件"); + + // 分析目标目录结构 + $targetFiles = $this->analyzeTargetDirectory(); + $this->logMessage('INFO', "目标目录中包含 {$targetFiles['fileCount']} 个文件"); + + // 比较差异并部署 + $result = $this->compareAndDeploy($zip, $zipFiles, $targetFiles); + + // 关闭ZIP文件 + $zip->close(); + + $this->logMessage('SUCCESS', '部署完成'); + return $result; + + } catch (Exception $e) { + $this->logMessage('ERROR', "部署失败: " . $e->getMessage()); + throw $e; + } + } + + /** + * 分析ZIP文件结构 + */ + private function analyzeZipStructure(ZipArchive $zip): array + { + $files = []; + $fileCount = 0; + $totalSize = 0; + + for ($i = 0; $i < $zip->numFiles; $i++) { + $fileInfo = $zip->statIndex($i); + + if ($fileInfo === false) { + continue; + } + + $fileName = $fileInfo['name']; + + // 跳过目录条目 + if (substr($fileName, -1) === '/') { + continue; + } + + $files[$fileName] = [ + 'size' => $fileInfo['size'], + 'compressed_size' => $fileInfo['comp_size'], + 'modified' => $fileInfo['mtime'], + 'crc32' => $fileInfo['crc'] + ]; + + $fileCount++; + $totalSize += $fileInfo['size']; + } + + return [ + 'files' => $files, + 'fileCount' => $fileCount, + 'totalSize' => $totalSize + ]; + } + + /** + * 分析目标目录结构 + */ + private function analyzeTargetDirectory(): array + { + $files = []; + $fileCount = 0; + $totalSize = 0; + + $iterator = new RecursiveIteratorIterator( + new RecursiveDirectoryIterator( + $this->targetDir, + RecursiveDirectoryIterator::SKIP_DOTS + ), + RecursiveIteratorIterator::SELF_FIRST + ); + + foreach ($iterator as $file) { + if ($file->isDir()) { + continue; + } + + $relativePath = $this->getRelativePath($file->getPathname(), $this->targetDir); + $files[$relativePath] = [ + 'size' => $file->getSize(), + 'modified' => $file->getMTime(), + 'permissions' => $file->getPerms() + ]; + + $fileCount++; + $totalSize += $file->getSize(); + } + + return [ + 'files' => $files, + 'fileCount' => $fileCount, + 'totalSize' => $totalSize + ]; + } + + /** + * 比较差异并部署文件 + */ + private function compareAndDeploy(ZipArchive $zip, array $zipFiles, array $targetFiles): array + { + $results = [ + 'added' => 0, + 'updated' => 0, + 'skipped' => 0, + 'errors' => 0, + 'details' => [] + ]; + + foreach ($zipFiles['files'] as $filePath => $zipFileInfo) { + $targetFilePath = $this->targetDir . DIRECTORY_SEPARATOR . $filePath; + $targetFileExists = isset($targetFiles['files'][$filePath]); + + try { + if (!$targetFileExists) { + // 新文件 - 需要创建目录并复制文件 + $result = $this->deployNewFile($zip, $filePath, $targetFilePath); + $results['added']++; + $results['details'][] = [ + 'file' => $filePath, + 'action' => 'added', + 'success' => true + ]; + } else { + // 文件已存在 - 检查是否需要更新 + $needsUpdate = $this->needsUpdate($zip, $filePath, $targetFiles['files'][$filePath]); + + if ($needsUpdate) { + $result = $this->deployUpdatedFile($zip, $filePath, $targetFilePath); + $results['updated']++; + $results['details'][] = [ + 'file' => $filePath, + 'action' => 'updated', + 'success' => true + ]; + } else { + $results['skipped']++; + $results['details'][] = [ + 'file' => $filePath, + 'action' => 'skipped', + 'success' => true + ]; + $this->logMessage('INFO', "文件无需更新: {$filePath}"); + } + } + } catch (Exception $e) { + $results['errors']++; + $results['details'][] = [ + 'file' => $filePath, + 'action' => 'error', + 'success' => false, + 'error' => $e->getMessage() + ]; + $this->logMessage('ERROR', "处理文件失败 {$filePath}: " . $e->getMessage()); + } + } + + return $results; + } + + /** + * 部署新文件 + */ + private function deployNewFile(ZipArchive $zip, string $filePath, string $targetFilePath): bool + { + $this->logMessage('INFO', "添加新文件: {$filePath}"); + + if ($this->dryRun) { + $this->logMessage('DRY_RUN', "将创建文件: {$targetFilePath}"); + return true; + } + + // 创建目录(如果需要) + $targetDir = dirname($targetFilePath); + if (!is_dir($targetDir)) { + if (!mkdir($targetDir, 0755, true)) { + throw new RuntimeException("无法创建目录: {$targetDir}"); + } + $this->logMessage('INFO', "创建目录: {$targetDir}"); + } + + // 从ZIP中提取文件 + $fileContent = $zip->getFromName($filePath); + if ($fileContent === false) { + throw new RuntimeException("无法从ZIP中读取文件: {$filePath}"); + } + + // 写入文件 + if (file_put_contents($targetFilePath, $fileContent) === false) { + throw new RuntimeException("无法写入文件: {$targetFilePath}"); + } + + $this->logMessage('SUCCESS', "文件创建成功: {$filePath}"); + return true; + } + + /** + * 部署更新文件 + */ + private function deployUpdatedFile(ZipArchive $zip, string $filePath, string $targetFilePath): bool + { + $this->logMessage('INFO', "更新文件: {$filePath}"); + + if ($this->dryRun) { + $this->logMessage('DRY_RUN', "将更新文件: {$targetFilePath}"); + return true; + } + + // 备份原文件(可选) + if ($this->backup && file_exists($targetFilePath)) { + $backupPath = $targetFilePath . '.backup.' . date('YmdHis'); + if (copy($targetFilePath, $backupPath)) { + $this->logMessage('INFO', "文件已备份: {$backupPath}"); + } + } + + // 从ZIP中提取文件内容 + $fileContent = $zip->getFromName($filePath); + if ($fileContent === false) { + throw new RuntimeException("无法从ZIP中读取文件: {$filePath}"); + } + + // 写入文件 + if (file_put_contents($targetFilePath, $fileContent) === false) { + throw new RuntimeException("无法写入文件: {$targetFilePath}"); + } + + $this->logMessage('SUCCESS', "文件更新成功: {$filePath}"); + return true; + } + + /** + * 检查文件是否需要更新 + */ + private function needsUpdate(ZipArchive $zip, string $filePath, array $targetFileInfo): bool + { + $zipFileInfo = $zip->statName($filePath); + if ($zipFileInfo === false) { + return false; + } + + // 比较文件大小 + if ($zipFileInfo['size'] != $targetFileInfo['size']) { + $this->logMessage('DEBUG', "文件大小不同: {$filePath} (ZIP: {$zipFileInfo['size']}, 目标: {$targetFileInfo['size']})"); + return true; + } + + // 比较修改时间(ZIP时间可能不准确,作为次要判断) + if ($zipFileInfo['mtime'] > $targetFileInfo['modified']) { + $this->logMessage('DEBUG', "ZIP文件较新: {$filePath}"); + return true; + } + + // 比较CRC32校验(最可靠的方法) + $targetCrc = hash_file('crc32b', $this->targetDir . DIRECTORY_SEPARATOR . $filePath); + $zipCrc = sprintf('%08x', $zipFileInfo['crc']); + + if (strtolower($targetCrc) !== strtolower($zipCrc)) { + $this->logMessage('DEBUG', "文件内容不同: {$filePath}"); + return true; + } + + return false; + } + + /** + * 获取相对路径 + */ + private function getRelativePath(string $path, string $basePath): string + { + $basePath = rtrim($basePath, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR; + return str_replace($basePath, '', $path); + } + + /** + * 记录日志 + */ + private function logMessage(string $level, string $message): void + { + $timestamp = date('Y-m-d H:i:s'); + $logEntry = "[{$timestamp}] [{$level}] {$message}"; + + $this->log[] = $logEntry; + + if ($this->verbose || in_array($level, ['ERROR', 'SUCCESS'])) { + echo $logEntry . PHP_EOL; + } + } + + /** + * 获取部署日志 + */ + public function getLog(): array + { + return $this->log; + } +} + +// 命令行接口 +class CommandLineInterface +{ + private array $options; + + public function __construct(array $argv) + { + $this->options = $this->parseOptions($argv); + } + + public function run(): void + { + try { + $this->showBanner(); + + if ($this->options['help'] ?? false) { + $this->showHelp(); + exit(0); + } + + // 验证参数 + $zipPath = $this->options['zip'] ?? null; + $targetDir = $this->options['target'] ?? null; + + if (!$zipPath || !$targetDir) { + echo "错误: 必须指定 --zip 和 --target 参数\n"; + $this->showHelp(); + exit(1); + } + + // 创建部署器实例 + $deployer = new AutoDeployer($zipPath, $targetDir, [ + 'dryRun' => $this->options['dry-run'] ?? false, + 'verbose' => $this->options['verbose'] ?? false, + 'backup' => $this->options['backup'] ?? false + ]); + + // 执行部署 + $result = $deployer->deploy(); + + // 显示结果摘要 + $this->showSummary($result); + + } catch (Exception $e) { + echo "错误: " . $e->getMessage() . PHP_EOL; + exit(1); + } + } + + private function parseOptions(array $argv): array + { + $options = [ + 'help' => false, + 'verbose' => false, + 'dry-run' => false + ]; + + for ($i = 1; $i < count($argv); $i++) { + $arg = $argv[$i]; + + switch ($arg) { + case '--help': + case '-h': + $options['help'] = true; + break; + + case '--verbose': + case '-v': + $options['verbose'] = true; + break; + + case '--dry-run': + case '-d': + $options['dry-run'] = true; + break; + + case '--backup': + case '-b': + $options['backup'] = true; + break; + + case '--zip': + $options['zip'] = $argv[++$i] ?? null; + break; + + case '--target': + $options['target'] = $argv[++$i] ?? null; + break; + + default: + if (substr($arg, 0, 1) === '-') { + echo "警告: 未知选项 {$arg}\n"; + } + break; + } + } + + return $options; + } + + private function showBanner(): void + { + echo "========================================\n"; + echo " 文件自动部署工具 v1.0\n"; + echo " PHP 7.4 兼容版本\n"; + echo "========================================\n\n"; + } + + private function showHelp(): void + { + echo "用法: php deploy.php [选项]\n\n"; + echo "选项:\n"; + echo " --zip ZIP文件路径 (必需)\n"; + echo " --target 目标目录路径 (必需)\n"; + echo " --dry-run, -d 试运行,不实际修改文件\n"; + echo " --backup, -b 部署前备份目标文件或目录\n"; + echo " --verbose, -v 显示详细输出\n"; + echo " --help, -h 显示此帮助信息\n\n"; + echo "示例:\n"; + echo " php deploy.php --zip update.zip --target /var/www/html\n"; + echo " php deploy.php --zip patch.zip --target ./app --dry-run\n"; + echo " php deploy.php --zip release.zip --target /var/www --verbose\n"; + } + + private function showSummary(array $result): void + { + echo "\n部署摘要:\n"; + echo "========================================\n"; + echo "新增文件: {$result['added']}\n"; + echo "更新文件: {$result['updated']}\n"; + echo "跳过文件: {$result['skipped']}\n"; + echo "错误文件: {$result['errors']}\n"; + echo "========================================\n"; + + if ($result['errors'] > 0) { + echo "\n错误详情:\n"; + foreach ($result['details'] as $detail) { + if (!$detail['success']) { + echo " - {$detail['file']}: {$detail['error']}\n"; + } + } + } + } +} + +// 主程序入口 +if (PHP_SAPI === 'cli') { + $cli = new CommandLineInterface($argv); + $cli->run(); +} else { + echo "此脚本只能在命令行模式下运行\n"; + exit(1); +} +?> \ No newline at end of file diff --git a/scripts/enhanced_deploy.php b/scripts/enhanced_deploy.php new file mode 100644 index 000000000..9b2c2148b --- /dev/null +++ b/scripts/enhanced_deploy.php @@ -0,0 +1,679 @@ +zipPath = $zipPath; + $this->targetDir = rtrim($targetDir, DIRECTORY_SEPARATOR); + + $this->config = array_merge([ + 'dry_run' => false, + 'verbose' => false, + 'backup_enabled' => true, + 'backup_dir' => $this->targetDir . '/backups/deploy', + 'delete_enabled' => true, + 'delete_list_file' => '_DELETE_LIST.txt', + 'delete_manifest_file' => '_DELETE_MANIFEST.json', + 'safe_delete' => true, + 'max_backups' => 5, + 'confirmation_required' => false + ], $config); + + $this->dryRun = $this->config['dry_run']; + $this->verbose = $this->config['verbose']; + + $this->validatePaths(); + } + + /** + * 获取相对路径 + */ + private function getRelativePath(string $path, string $basePath): string + { + $basePath = rtrim($basePath, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR; + return str_replace($basePath, '', $path); + } + + /** + * 执行部署(包含删除功能) + */ + public function deploy(): array + { + $this->logMessage('INFO', '开始增强版自动部署流程'); + $this->logMessage('INFO', "ZIP文件: {$this->zipPath}"); + $this->logMessage('INFO', "目标目录: {$this->targetDir}"); + + try { + // 打开ZIP文件 + $zip = new ZipArchive(); + if ($zip->open($this->zipPath) !== true) { + throw new RuntimeException("无法打开ZIP文件: {$this->zipPath}"); + } + + $this->logMessage('INFO', 'ZIP文件打开成功'); + + // 步骤1:读取删除清单 + $this->loadDeleteList($zip); + + // 步骤2:分析ZIP文件结构 + $zipFiles = $this->analyzeZipStructure($zip); + $this->logMessage('INFO', "ZIP包中包含 {$zipFiles['file_count']} 个文件"); + + // 步骤3:分析目标目录结构 + $targetFiles = $this->analyzeTargetDirectory(); + $this->logMessage('INFO', "目标目录中包含 {$targetFiles['file_count']} 个文件"); + + // 步骤4:确认删除操作(如果需要) + if ($this->config['confirmation_required'] && !empty($this->deleteList)) { + if (!$this->confirmDeletion()) { + $zip->close(); + throw new RuntimeException("用户取消部署操作"); + } + } + + // 步骤5:执行删除操作 + $deleteResult = $this->executeDeletion(); + + // 步骤6:部署新文件和更新文件 + $deployResult = $this->deployFiles($zip, $zipFiles, $targetFiles); + + // 关闭ZIP文件 + $zip->close(); + + // 合并结果 + $result = array_merge($deployResult, [ + 'deletion' => $deleteResult + ]); + + $this->logMessage('SUCCESS', '部署完成'); + return $result; + + } catch (Exception $e) { + $this->logMessage('ERROR', "部署失败: " . $e->getMessage()); + throw $e; + } + } + + /** + * 分析ZIP文件结构 + */ + private function analyzeZipStructure(ZipArchive $zip): array + { + $files = []; + $fileCount = 0; + $totalSize = 0; + + for ($i = 0; $i < $zip->numFiles; $i++) { + $fileInfo = $zip->statIndex($i); + + if ($fileInfo === false) { + continue; + } + + $fileName = $fileInfo['name']; + + // 跳过目录条目 + if (substr($fileName, -1) === '/') { + continue; + } + + $files[$fileName] = [ + 'size' => $fileInfo['size'], + 'compressed_size' => $fileInfo['comp_size'], + 'modified' => $fileInfo['mtime'], + 'crc32' => $fileInfo['crc'] + ]; + + $fileCount++; + $totalSize += $fileInfo['size']; + } + + return [ + 'files' => $files, + 'fileCount' => $fileCount, + 'totalSize' => $totalSize + ]; + } + + /** + * 分析目标目录结构 + */ + private function analyzeTargetDirectory(): array + { + $files = []; + $fileCount = 0; + $totalSize = 0; + + $iterator = new RecursiveIteratorIterator( + new RecursiveDirectoryIterator( + $this->targetDir, + RecursiveDirectoryIterator::SKIP_DOTS + ), + RecursiveIteratorIterator::SELF_FIRST + ); + + foreach ($iterator as $file) { + if ($file->isDir()) { + continue; + } + + $relativePath = $this->getRelativePath($file->getPathname(), $this->targetDir); + $files[$relativePath] = [ + 'size' => $file->getSize(), + 'modified' => $file->getMTime(), + 'permissions' => $file->getPerms() + ]; + + $fileCount++; + $totalSize += $file->getSize(); + } + + return [ + 'files' => $files, + 'fileCount' => $fileCount, + 'totalSize' => $totalSize + ]; + } + + + /** + * 加载删除清单 + */ + private function loadDeleteList(ZipArchive $zip): void + { + $this->deleteList = []; + + // 尝试从多个可能的文件中读取删除清单 + $possibleFiles = [ + $this->config['delete_list_file'], + 'DELETE_LIST.txt', + 'delete.list', + 'manifest.json', + $this->config['delete_manifest_file'] + ]; + + foreach ($possibleFiles as $listFile) { + $content = $zip->getFromName($listFile); + if ($content !== false) { + $this->parseDeleteList($content, $listFile); + break; + } + } + + if (empty($this->deleteList)) { + $this->logMessage('INFO', '未找到删除清单文件,跳过删除操作'); + } else { + $this->logMessage('INFO', "找到删除清单,包含 " . count($this->deleteList) . " 个待删除文件"); + } + } + + /** + * 解析删除清单 + */ + private function parseDeleteList(string $content, string $sourceFile): void + { + $this->logMessage('DEBUG', "从文件 {$sourceFile} 解析删除清单"); + + // 尝试解析JSON格式 + if (strpos($sourceFile, '.json') !== false) { + $data = json_decode($content, true); + if ($data && isset($data['files_to_delete'])) { + $this->deleteList = $data['files_to_delete']; + return; + } + } + + // 解析文本格式(每行一个文件路径) + $lines = explode("\n", $content); + foreach ($lines as $line) { + $line = trim($line); + + // 跳过空行和注释 + if (empty($line) || $line[0] === '#' || $line[0] === ';') { + continue; + } + + $this->deleteList[] = $line; + } + + // 去重 + $this->deleteList = array_unique($this->deleteList); + } + + /** + * 执行删除操作 + */ + private function executeDeletion(): array + { + if (!$this->config['delete_enabled'] || empty($this->deleteList)) { + return [ + 'total' => 0, + 'deleted' => 0, + 'skipped' => 0, + 'errors' => 0, + 'details' => [] + ]; + } + + $result = [ + 'total' => count($this->deleteList), + 'deleted' => 0, + 'skipped' => 0, + 'errors' => 0, + 'details' => [] + ]; + + $this->logMessage('INFO', "开始执行删除操作,共 {$result['total']} 个文件"); + + foreach ($this->deleteList as $fileToDelete) { + try { + $fullPath = $this->targetDir . DIRECTORY_SEPARATOR . $fileToDelete; + + if (!$this->shouldDeleteFile($fullPath, $fileToDelete)) { + $result['skipped']++; + $result['details'][] = [ + 'file' => $fileToDelete, + 'action' => 'skipped', + 'reason' => '安全检查未通过', + 'success' => true + ]; + continue; + } + + $deleteResult = $this->deleteFile($fullPath, $fileToDelete); + + if ($deleteResult) { + $result['deleted']++; + $result['details'][] = [ + 'file' => $fileToDelete, + 'action' => 'deleted', + 'success' => true + ]; + } else { + $result['skipped']++; + $result['details'][] = [ + 'file' => $fileToDelete, + 'action' => 'skipped', + 'reason' => '文件不存在或无法删除', + 'success' => true + ]; + } + + } catch (Exception $e) { + $result['errors']++; + $result['details'][] = [ + 'file' => $fileToDelete, + 'action' => 'error', + 'error' => $e->getMessage(), + 'success' => false + ]; + $this->logMessage('ERROR', "删除文件失败 {$fileToDelete}: " . $e->getMessage()); + } + } + + $this->logMessage('SUCCESS', "删除操作完成: 成功 {$result['deleted']} 个, 跳过 {$result['skipped']} 个, 错误 {$result['errors']} 个"); + + return $result; + } + + /** + * 检查是否应该删除文件 + */ + private function shouldDeleteFile(string $fullPath, string $relativePath): bool + { + // 安全检查:确保路径在目标目录内 + $realTarget = realpath($this->targetDir); + $realFile = realpath($fullPath); + + if ($realFile === false || strpos($realFile, $realTarget) !== 0) { + $this->logMessage('WARNING', "安全警告: 跳过删除外部文件 {$relativePath}"); + return false; + } + + // 检查重要文件保护 + $protectedPatterns = [ + '/\.env$/', + '/config\.php$/', + '/database\.php$/', + '/\.htaccess$/', + '/web\.config$/', + '/index\.php$/', + '/composer\.json$/', + '/composer\.lock$/', + '/package\.json$/', + '/yarn\.lock$/', + '/\.gitignore$/' + ]; + + foreach ($protectedPatterns as $pattern) { + if (preg_match($pattern, $relativePath)) { + $this->logMessage('WARNING', "保护文件: 跳过删除 {$relativePath}"); + return false; + } + } + + // 检查文件是否存在 + if (!file_exists($fullPath)) { + $this->logMessage('DEBUG', "文件不存在,跳过删除: {$relativePath}"); + return false; + } + + // 检查是否为目录 + if (is_dir($fullPath)) { + $this->logMessage('WARNING', "跳过删除目录: {$relativePath}"); + return false; + } + + return true; + } + + /** + * 删除单个文件 + */ + private function deleteFile(string $fullPath, string $relativePath): bool + { + if ($this->dryRun) { + $this->logMessage('DRY_RUN', "将删除文件: {$relativePath}"); + return true; + } + + // 备份文件(如果启用) + if ($this->config['backup_enabled']) { + $this->backupFile($fullPath, $relativePath); + } + + // 删除文件 + if (unlink($fullPath)) { + $this->logMessage('INFO', "文件已删除: {$relativePath}"); + + // 尝试删除空目录 + $this->cleanupEmptyDirectories(dirname($fullPath)); + + return true; + } else { + $this->logMessage('ERROR', "无法删除文件: {$relativePath}"); + return false; + } + } + + /** + * 备份文件 + */ + private function backupFile(string $filePath, string $relativePath): void + { + if (!file_exists($filePath)) { + return; + } + + $backupDir = $this->config['backup_dir']; + if (!is_dir($backupDir)) { + mkdir($backupDir, 0755, true); + } + + $backupPath = $backupDir . DIRECTORY_SEPARATOR . + date('Ymd_His') . '_' . + str_replace('/', '_', $relativePath); + + $backupDir = dirname($backupPath); + if (!is_dir($backupDir)) { + mkdir($backupDir, 0755, true); + } + + if (copy($filePath, $backupPath)) { + $this->logMessage('INFO', "文件已备份: {$backupPath}"); + } else { + $this->logMessage('WARNING', "文件备份失败: {$relativePath}"); + } + } + + /** + * 清理空目录 + */ + private function cleanupEmptyDirectories(string $directory): void + { + $currentDir = $directory; + $baseDir = realpath($this->targetDir); + + while ($currentDir !== $baseDir && is_dir($currentDir)) { + // 检查目录是否为空 + $files = scandir($currentDir); + if (count($files) === 2) { // 只有 . 和 .. + if ($this->dryRun) { + $this->logMessage('DRY_RUN', "将删除空目录: {$currentDir}"); + } else { + if (rmdir($currentDir)) { + $this->logMessage('INFO', "空目录已删除: {$currentDir}"); + } + } + + $currentDir = dirname($currentDir); + } else { + break; + } + } + } + + /** + * 确认删除操作 + */ + private function confirmDeletion(): bool + { + echo "\n⚠️ 警告: 以下文件将被删除:\n"; + echo "========================================\n"; + + foreach ($this->deleteList as $index => $file) { + echo sprintf("%3d. %s\n", $index + 1, $file); + } + + echo "========================================\n"; + echo "总共 " . count($this->deleteList) . " 个文件将被删除\n\n"; + + echo "是否继续?(y/N): "; + $handle = fopen('php://stdin', 'r'); + $input = trim(fgets($handle)); + fclose($handle); + + return strtolower($input) === 'y'; + } + + /** + * 部署文件(从父类继承或实现) + */ + private function deployFiles(ZipArchive $zip, array $zipFiles, array $targetFiles): array + { + // 这里实现文件部署逻辑,与之前的AutoDeployer类似 + // 包括添加新文件和更新现有文件 + $result = [ + 'added' => 0, + 'updated' => 0, + 'skipped' => 0, + 'errors' => 0, + 'details' => [] + ]; + + // 实现文件部署逻辑... + + return $result; + } + + /** + * 记录日志 + */ + private function logMessage(string $level, string $message): void + { + $timestamp = date('Y-m-d H:i:s'); + $logEntry = "[{$timestamp}] [{$level}] {$message}"; + + $this->log[] = $logEntry; + + if ($this->verbose || in_array($level, ['ERROR', 'SUCCESS', 'WARNING'])) { + echo $logEntry . PHP_EOL; + } + } + + /** + * 验证路径 + */ + private function validatePaths(): void + { + if (!file_exists($this->zipPath)) { + throw new InvalidArgumentException("ZIP文件不存在: {$this->zipPath}"); + } + + if (!is_dir($this->targetDir)) { + throw new InvalidArgumentException("目标目录不存在: {$this->targetDir}"); + } + } + + /** + * 获取日志 + */ + public function getLog(): array + { + return $this->log; + } +} + +// 命令行接口 +class EnhancedDeployerCLI +{ + public function run(array $argv): void + { + try { + $options = $this->parseOptions($argv); + + if ($options['help']) { + $this->showHelp(); + exit(0); + } + + if (!$options['zip'] || !$options['target']) { + echo "错误: 必须指定 --zip 和 --target 参数\n"; + $this->showHelp(); + exit(1); + } + + $deployer = new EnhancedAutoDeployer($options['zip'], $options['target'], [ + 'dry_run' => $options['dry-run'], + 'verbose' => $options['verbose'], + 'confirmation_required' => $options['confirm'] + ]); + + $result = $deployer->deploy(); + $this->showResults($result); + + } catch (Exception $e) { + echo "错误: " . $e->getMessage() . PHP_EOL; + exit(1); + } + } + + private function parseOptions(array $argv): array + { + $options = [ + 'help' => false, + 'dry-run' => false, + 'verbose' => false, + 'confirm' => false, + 'zip' => null, + 'target' => null + ]; + + for ($i = 1; $i < count($argv); $i++) { + switch ($argv[$i]) { + case '--help': + case '-h': + $options['help'] = true; + break; + + case '--dry-run': + case '-d': + $options['dry-run'] = true; + break; + + case '--verbose': + case '-v': + $options['verbose'] = true; + break; + + case '--confirm': + case '-c': + $options['confirm'] = true; + break; + + case '--zip': + $options['zip'] = $argv[++$i] ?? null; + break; + + case '--target': + $options['target'] = $argv[++$i] ?? null; + break; + } + } + + return $options; + } + + private function showHelp(): void + { + echo "增强版自动部署工具\n\n"; + echo "用法: php enhanced_deploy.php [选项]\n\n"; + echo "选项:\n"; + echo " --zip ZIP文件路径 (必需)\n"; + echo " --target 目标目录路径 (必需)\n"; + echo " --dry-run, -d 试运行,不实际修改文件\n"; + echo " --verbose, -v 显示详细输出\n"; + echo " --confirm, -c 确认删除操作\n"; + echo " --help, -h 显示此帮助信息\n\n"; + echo "示例:\n"; + echo " php enhanced_deploy.php --zip update.zip --target /var/www/html\n"; + echo " php enhanced_deploy.php --zip patch.zip --target ./app --dry-run\n"; + echo " php enhanced_deploy.php --zip deploy.zip --target /var/www --confirm\n"; + } + + private function showResults(array $result): void + { + echo "\n部署结果:\n"; + echo "========================================\n"; + echo "文件部署:\n"; + echo " 新增: {$result['added']} 个文件\n"; + echo " 更新: {$result['updated']} 个文件\n"; + echo " 跳过: {$result['skipped']} 个文件\n"; + echo " 错误: {$result['errors']} 个文件\n\n"; + + echo "文件删除:\n"; + echo " 总计: {$result['deletion']['total']} 个文件\n"; + echo " 删除: {$result['deletion']['deleted']} 个文件\n"; + echo " 跳过: {$result['deletion']['skipped']} 个文件\n"; + echo " 错误: {$result['deletion']['errors']} 个文件\n"; + echo "========================================\n"; + } +} + +// 主程序入口 +if (PHP_SAPI === 'cli') { + $cli = new EnhancedDeployerCLI(); + $cli->run($argv); +} else { + echo "此脚本只能在命令行模式下运行\n"; + exit(1); +} +?> \ No newline at end of file diff --git a/scripts/generate_deploy_package.php b/scripts/generate_deploy_package.php new file mode 100644 index 000000000..37619256c --- /dev/null +++ b/scripts/generate_deploy_package.php @@ -0,0 +1,801 @@ +sourceDir = rtrim($sourceDir, DIRECTORY_SEPARATOR); + $this->targetDir = rtrim($targetDir, DIRECTORY_SEPARATOR); + $this->outputPath = $outputPath; + + $this->config = array_merge([ + 'exclude_patterns' => [ + '/\.git/', // Git目录 + '/\.svn/', // SVN目录 + '/node_modules/', // Node模块 + '/vendor/', // Composer依赖 + '/cache/', // 缓存目录 + '/logs/', // 日志目录 + '/temp/', // 临时目录 + '/tmp/', // 临时目录 + '/backup/', // 备份目录 + '/runtime/', // 运行时目录 + '/upload/', // 上传目录 + '/\.DS_Store$/', // Mac系统文件 + '/Thumbs\.db$/', // Windows缩略图 + '/\.log$/', // 日志文件 + '/\.tmp$/', // 临时文件 + '/\.env$/', // 环境变量 + '/composer\.lock$/', // Composer依赖锁文件 + ], + 'include_patterns' => [], // 包含模式(优先于排除) + 'compare_method' => 'content', // content, size, mtime, hash + 'hash_algorithm' => 'md5', // md5, sha1, crc32 + 'min_file_size' => 0, // 最小文件大小(字节) + 'max_file_size' => 10485760, // 最大文件大小(10MB) + 'dry_run' => false, // 试运行 + 'verbose' => false, // 详细输出 + 'backup_old' => true, // 备份旧版本 + 'compression_level' => 9, // ZIP压缩级别 0-9 + ], $config); + + $this->validateDirectories(); + } + + /** + * 验证目录有效性 + */ + private function validateDirectories(): void + { + if (!is_dir($this->sourceDir)) { + throw new InvalidArgumentException("源目录不存在: {$this->sourceDir}"); + } + + if (!is_readable($this->sourceDir)) { + throw new InvalidArgumentException("源目录不可读: {$this->sourceDir}"); + } + + if (!is_dir($this->targetDir)) { + $this->logMessage('WARNING', "目标目录不存在,将创建完整部署包: {$this->targetDir}"); + } elseif (!is_readable($this->targetDir)) { + throw new InvalidArgumentException("目标目录不可读: {$this->targetDir}"); + } + + $outputDir = dirname($this->outputPath); + if (!is_dir($outputDir)) { + throw new InvalidArgumentException("输出目录不存在: {$outputDir}"); + } + + if (!is_writable($outputDir)) { + throw new InvalidArgumentException("输出目录不可写: {$outputDir}"); + } + } + + /** + * 生成部署包 + */ + public function generate(): array + { + $this->logMessage('INFO', '开始生成部署包'); + $this->logMessage('INFO', "源目录: {$this->sourceDir}"); + $this->logMessage('INFO', "目标目录: {$this->targetDir}"); + $this->logMessage('INFO', "输出文件: {$this->outputPath}"); + + try { + // 步骤1:分析目录结构 + $this->logMessage('INFO', '分析目录结构...'); + $sourceFiles = $this->analyzeDirectory($this->sourceDir); + $targetFiles = is_dir($this->targetDir) ? $this->analyzeDirectory($this->targetDir) : []; + + $this->logMessage('INFO', "源目录文件数: " . count($sourceFiles)); + $this->logMessage('INFO', "目标目录文件数: " . count($targetFiles)); + + // 步骤2:比较文件差异 + $this->logMessage('INFO', '比较文件差异...'); + $diffFiles = $this->compareFiles($sourceFiles, $targetFiles); + + // 步骤3:生成部署包 + $this->logMessage('INFO', '生成部署ZIP包...'); + $result = $this->createDeployPackage($diffFiles); + + $this->logMessage('SUCCESS', '部署包生成完成'); + return array_merge($result, [ + 'comparison' => $this->comparisonResult + ]); + + } catch (Exception $e) { + $this->logMessage('ERROR', "生成失败: " . $e->getMessage()); + throw $e; + } + } + + /** + * 分析目录结构 + */ + private function analyzeDirectory(string $directory): array + { + $files = []; + $basePath = rtrim($directory, DIRECTORY_SEPARATOR); + + $iterator = new RecursiveIteratorIterator( + new RecursiveDirectoryIterator( + $directory, + FilesystemIterator::SKIP_DOTS | FilesystemIterator::FOLLOW_SYMLINKS + ), + RecursiveIteratorIterator::SELF_FIRST + ); + + foreach ($iterator as $file) { + if ($file->isDir()) { + continue; + } + + $filePath = $file->getPathname(); + $relativePath = $this->getRelativePath($filePath, $basePath); + + // 检查是否应该排除 + if ($this->shouldExclude($relativePath)) { + continue; + } + + // 检查文件大小限制 + $fileSize = $file->getSize(); + if ($fileSize < $this->config['min_file_size'] || $fileSize > $this->config['max_file_size']) { + $this->logMessage('DEBUG', "跳过文件(大小限制): {$relativePath} ({$fileSize} bytes)"); + continue; + } + + $files[$relativePath] = [ + 'full_path' => $filePath, + 'size' => $fileSize, + 'modified' => $file->getMTime(), + 'permissions' => $file->getPerms(), + 'hash' => $this->calculateFileHash($filePath) + ]; + } + + return $files; + } + + /** + * 比较文件差异 + */ + private function compareFiles(array $sourceFiles, array $targetFiles): array + { + $diffFiles = [ + 'added' => [], // 新增文件 + 'modified' => [], // 修改文件 + 'deleted' => [], // 删除文件(记录以便清理) + 'unchanged' => [] // 未改变文件 + ]; + + $this->comparisonResult = [ + 'total_source_files' => count($sourceFiles), + 'total_target_files' => count($targetFiles), + 'files_analyzed' => 0 + ]; + + // 检查新增和修改的文件 + foreach ($sourceFiles as $relativePath => $sourceFileInfo) { + $this->comparisonResult['files_analyzed']++; + + if (!isset($targetFiles[$relativePath])) { + // 新增文件 + $diffFiles['added'][$relativePath] = $sourceFileInfo; + $this->logMessage('INFO', "新增文件: {$relativePath}"); + } else { + // 文件存在,检查是否需要更新 + $targetFileInfo = $targetFiles[$relativePath]; + + if ($this->needsUpdate($sourceFileInfo, $targetFileInfo)) { + $diffFiles['modified'][$relativePath] = $sourceFileInfo; + $this->logMessage('INFO', "修改文件: {$relativePath}"); + } else { + $diffFiles['unchanged'][$relativePath] = $sourceFileInfo; + $this->logMessage('DEBUG', "未改变: {$relativePath}"); + } + } + } + + // 检查需要删除的文件 + foreach ($targetFiles as $relativePath => $targetFileInfo) { + if (!isset($sourceFiles[$relativePath])) { + $diffFiles['deleted'][$relativePath] = $targetFileInfo; + $this->logMessage('INFO', "待删除文件: {$relativePath}"); + } + } + + $this->comparisonResult = array_merge($this->comparisonResult, [ + 'added_files' => count($diffFiles['added']), + 'modified_files' => count($diffFiles['modified']), + 'deleted_files' => count($diffFiles['deleted']), + 'unchanged_files' => count($diffFiles['unchanged']) + ]); + + $this->logMessage('INFO', "比较完成: 新增{$this->comparisonResult['added_files']}个, 修改{$this->comparisonResult['modified_files']}个, 删除{$this->comparisonResult['deleted_files']}个"); + + return $diffFiles; + } + + /** + * 检查文件是否需要更新 + */ + private function needsUpdate(array $sourceFile, array $targetFile): bool + { + $method = $this->config['compare_method']; + + switch ($method) { + case 'content': + // 内容比较(最准确) + return $sourceFile['hash'] !== $targetFile['hash']; + + case 'size': + // 大小比较 + return $sourceFile['size'] !== $targetFile['size']; + + case 'mtime': + // 修改时间比较 + return $sourceFile['modified'] > $targetFile['modified']; + + case 'hash': + default: + // 哈希比较(默认) + return $sourceFile['hash'] !== $targetFile['hash']; + } + } + + /** + * 创建部署包 + */ + protected function createDeployPackage(array $diffFiles): array + { + if ($this->config['dry_run']) { + $this->logMessage('DRY_RUN', "试运行模式,将创建包含 " . + (count($diffFiles['added']) + count($diffFiles['modified'])) . " 个文件的ZIP包"); + return $this->simulatePackageCreation($diffFiles); + } + + $zip = new ZipArchive(); + $openResult = $zip->open($this->outputPath, ZipArchive::CREATE | ZipArchive::OVERWRITE); + + if ($openResult !== true) { + throw new RuntimeException("无法创建ZIP文件,错误代码: {$openResult}"); + } + + $addedCount = 0; + $modifiedCount = 0; + + // 添加新增和修改的文件 + $filesToAdd = array_merge($diffFiles['added'], $diffFiles['modified']); + + + $fileIndex = 0; + foreach ($filesToAdd as $relativePath => $fileInfo) { + try { + // 确保目录结构被正确保留 + // 首先尝试直接添加文件 + if ($zip->addFile($fileInfo['full_path'], $relativePath)) { + $zip->setCompressionName($relativePath, ZipArchive::CM_DEFLATE); + $zip->setCompressionIndex($fileIndex, ZipArchive::CM_DEFLATE, $this->config['compression_level']); + $fileIndex++; + + if (isset($diffFiles['added'][$relativePath])) { + $addedCount++; + } else { + $modifiedCount++; + } + + $this->logMessage('DEBUG', "添加到ZIP: {$relativePath}"); + } else { + // 如果直接添加失败,尝试先创建目录结构 + $dir = dirname($relativePath); + if ($dir !== '.' && $dir !== '') { + // 确保所有父目录都被创建 + $parts = explode(DIRECTORY_SEPARATOR, $dir); + $currentPath = ''; + foreach ($parts as $part) { + $currentPath .= $part . DIRECTORY_SEPARATOR; + $currentPath = rtrim($currentPath, DIRECTORY_SEPARATOR); + if ($currentPath !== '' && !$zip->addEmptyDir($currentPath)) { + // 添加空目录失败可能是因为目录已存在,继续尝试 + continue; + } + } + } + + // 再次尝试添加文件 + if ($zip->addFile($fileInfo['full_path'], $relativePath)) { + $zip->setCompressionName($relativePath, ZipArchive::CM_DEFLATE); + $zip->setCompressionIndex($fileIndex, ZipArchive::CM_DEFLATE, $this->config['compression_level']); + $fileIndex++; + + if (isset($diffFiles['added'][$relativePath])) { + $addedCount++; + } else { + $modifiedCount++; + } + + $this->logMessage('DEBUG', "添加到ZIP (创建目录后): {$relativePath}"); + } else { + $this->logMessage('WARNING', "添加文件失败: {$relativePath}"); + } + } + } catch (Exception $e) { + $this->logMessage('ERROR', "处理文件失败 {$relativePath}: " . $e->getMessage()); + } + } + + // 创建删除列表文件 + if (!empty($diffFiles['deleted'])) { + $deleteListContent = "# 需要删除的文件列表\n# 生成时间: " . date('Y-m-d H:i:s') . "\n\n"; + foreach (array_keys($diffFiles['deleted']) as $fileToDelete) { + $deleteListContent .= $fileToDelete . "\n"; + } + + $zip->addFromString('_DELETE_LIST.txt', $deleteListContent); + $this->logMessage('INFO', "创建删除列表,包含 " . count($diffFiles['deleted']) . " 个文件"); + } + + // 创建部署清单 + $manifest = $this->createManifest($diffFiles); + $zip->addFromString('_DEPLOY_MANIFEST.json', json_encode($manifest, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE)); + + // 关闭ZIP文件 + if (!$zip->close()) { + throw new RuntimeException("ZIP文件保存失败"); + } + + // 验证ZIP文件 + $zipSize = filesize($this->outputPath); + $fileCount = $zip->numFiles; + + $result = [ + 'output_path' => $this->outputPath, + 'file_size' => $zipSize, + 'file_size_human' => $this->formatBytes($zipSize), + 'files_added' => $addedCount, + 'files_modified' => $modifiedCount, + 'files_deleted' => count($diffFiles['deleted']), + 'total_files' => $addedCount + $modifiedCount, + 'compression_level' => $this->config['compression_level'], + 'created_at' => date('Y-m-d H:i:s') + ]; + + $this->logMessage('SUCCESS', "部署包创建成功: {$result['file_size_human']}, 包含 {$result['total_files']} 个文件"); + + return $result; + } + + /** + * 创建部署清单 + */ + private function createManifest(array $diffFiles): array + { + return [ + 'metadata' => [ + 'generator' => 'DeployPackageGenerator', + 'version' => '1.0', + 'created_at' => date('c'), + 'source_dir' => $this->sourceDir, + 'target_dir' => $this->targetDir, + 'compare_method' => $this->config['compare_method'] + ], + 'statistics' => $this->comparisonResult, + 'files' => [ + 'added' => array_keys($diffFiles['added']), + 'modified' => array_keys($diffFiles['modified']), + 'deleted' => array_keys($diffFiles['deleted']) + ], + 'file_details' => [ + 'added' => $diffFiles['added'], + 'modified' => $diffFiles['modified'] + ] + ]; + } + + /** + * 试运行模式 + */ + private function simulatePackageCreation(array $diffFiles): array + { + $totalSize = 0; + + foreach (array_merge($diffFiles['added'], $diffFiles['modified']) as $fileInfo) { + $totalSize += $fileInfo['size']; + } + + return [ + 'output_path' => $this->outputPath, + 'file_size' => $totalSize, + 'file_size_human' => $this->formatBytes($totalSize), + 'files_added' => count($diffFiles['added']), + 'files_modified' => count($diffFiles['modified']), + 'files_deleted' => count($diffFiles['deleted']), + 'total_files' => count($diffFiles['added']) + count($diffFiles['modified']), + 'dry_run' => true, + 'message' => '试运行模式 - 未实际创建文件' + ]; + } + + /** + * 检查是否应该排除文件 + */ + private function shouldExclude(string $relativePath): bool + { + // 首先检查包含模式 + foreach ($this->config['include_patterns'] as $pattern) { + if (preg_match($pattern, $relativePath)) { + return false; + } + } + + // 然后检查排除模式 + foreach ($this->config['exclude_patterns'] as $pattern) { + if (preg_match($pattern, $relativePath)) { + $this->logMessage('DEBUG', "排除文件: {$relativePath} (匹配模式: {$pattern})"); + return true; + } + } + + return false; + } + + /** + * 计算文件哈希 + */ + private function calculateFileHash(string $filePath): string + { + $algorithm = $this->config['hash_algorithm']; + + switch ($algorithm) { + case 'md5': + return md5_file($filePath); + case 'sha1': + return sha1_file($filePath); + case 'crc32': + return hash_file('crc32b', $filePath); + default: + return md5_file($filePath); + } + } + + /** + * 获取相对路径 + */ + private function getRelativePath(string $path, string $basePath): string + { + $basePath = rtrim($basePath, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR; + return str_replace($basePath, '', $path); + } + + /** + * 格式化字节大小 + */ + private function formatBytes(int $bytes, int $precision = 2): string + { + $units = ['B', 'KB', 'MB', 'GB', 'TB']; + $bytes = max($bytes, 0); + $pow = floor(($bytes ? log($bytes) : 0) / log(1024)); + $pow = min($pow, count($units) - 1); + $bytes /= pow(1024, $pow); + + return round($bytes, $precision) . ' ' . $units[$pow]; + } + + /** + * 记录日志 + */ + private function logMessage(string $level, string $message): void + { + $timestamp = date('Y-m-d H:i:s'); + $logEntry = "[{$timestamp}] [{$level}] {$message}"; + + $this->log[] = $logEntry; + + if ($this->config['verbose'] || in_array($level, ['ERROR', 'SUCCESS', 'WARNING'])) { + echo $logEntry . PHP_EOL; + } + } + + /** + * 获取日志 + */ + public function getLog(): array + { + return $this->log; + } +} + +/** + * 增强版部署包生成器 + * 生成包含删除清单的部署包 + */ +class EnhancedPackageGenerator extends DeployPackageGenerator +{ + /** + * 创建部署包(增强版) + */ + protected function createDeployPackage(array $diffFiles): array + { + $result = parent::createDeployPackage($diffFiles); + + // 添加删除清单 + if (!empty($diffFiles['deleted'])) { + $this->addDeleteManifest($diffFiles['deleted']); + } + + return $result; + } + + /** + * 添加删除清单文件 + */ + private function addDeleteManifest(array $deletedFiles): void + { + if ($this->config['dry_run']) { + $this->logMessage('DRY_RUN', "将创建删除清单,包含 " . count($deletedFiles) . " 个文件"); + return; + } + + $zip = new ZipArchive(); + if ($zip->open($this->outputPath) !== true) { + throw new RuntimeException("无法打开ZIP文件添加删除清单"); + } + + // 文本格式删除清单 + $textContent = $this->createTextDeleteList($deletedFiles); + $zip->addFromString('DELETE_LIST.txt', $textContent); + + // JSON格式删除清单 + $jsonContent = $this->createJsonDeleteList($deletedFiles); + $zip->addFromString('delete_manifest.json', $jsonContent); + + $zip->close(); + + $this->logMessage('INFO', "删除清单已添加: " . count($deletedFiles) . " 个待删除文件"); + } + + /** + * 创建文本格式删除清单 + */ + private function createTextDeleteList(array $deletedFiles): string + { + $content = "# 自动部署删除清单\n"; + $content .= "# 生成时间: " . date('Y-m-d H:i:s') . "\n"; + $content .= "# 此文件列出了需要删除的文件列表\n\n"; + + foreach (array_keys($deletedFiles) as $filePath) { + $content .= $filePath . "\n"; + } + + $content .= "\n# 注释以 # 开头\n"; + $content .= "# 空行和注释行将被忽略\n"; + + return $content; + } + + /** + * 创建JSON格式删除清单 + */ + private function createJsonDeleteList(array $deletedFiles): string + { + $manifest = [ + 'version' => '1.0', + 'generated_at' => date('c'), + 'description' => '自动部署删除清单', + 'files_to_delete' => array_keys($deletedFiles), + 'total_files' => count($deletedFiles), + 'metadata' => [ + 'safe_deletion' => true, + 'backup_recommended' => true, + 'generator' => 'EnhancedPackageGenerator' + ] + ]; + + return json_encode($manifest, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE); + } +} + +// 命令行接口 +class PackageGeneratorCLI +{ + private array $options; + + public function __construct(array $argv) + { + $this->options = $this->parseOptions($argv); + } + + public function run(): void + { + try { + $this->showBanner(); + + if ($this->options['help'] ?? false) { + $this->showHelp(); + exit(0); + } + + // 验证参数 + $sourceDir = $this->options['source'] ?? null; + $targetDir = $this->options['target'] ?? null; + $outputFile = $this->options['output'] ?? null; + + if (!$sourceDir || !$targetDir || !$outputFile) { + echo "错误: 必须指定 --source, --target 和 --output 参数\n"; + $this->showHelp(); + exit(1); + } + + // 构建配置 + $config = [ + 'dry_run' => $this->options['dry-run'] ?? false, + 'verbose' => $this->options['verbose'] ?? false, + 'compare_method' => $this->options['compare'] ?? 'content' + ]; + + // 加载配置文件 + if (isset($this->options['config'])) { + $config = array_merge($config, $this->loadConfigFile($this->options['config'])); + } + + // 创建生成器实例 + $generator = new DeployPackageGenerator($sourceDir, $targetDir, $outputFile, $config); + + // 生成部署包 + $result = $generator->generate(); + + // 显示结果 + $this->showResults($result); + + } catch (Exception $e) { + echo "错误: " . $e->getMessage() . PHP_EOL; + exit(1); + } + } + + private function parseOptions(array $argv): array + { + $options = [ + 'help' => false, + 'verbose' => false, + 'dry-run' => false + ]; + + for ($i = 1; $i < count($argv); $i++) { + $arg = $argv[$i]; + + switch ($arg) { + case '--help': + case '-h': + $options['help'] = true; + break; + + case '--verbose': + case '-v': + $options['verbose'] = true; + break; + + case '--dry-run': + case '-d': + $options['dry-run'] = true; + break; + + case '--source': + case '-s': + $options['source'] = $argv[++$i] ?? null; + break; + + case '--target': + case '-t': + $options['target'] = $argv[++$i] ?? null; + break; + + case '--output': + case '-o': + $options['output'] = $argv[++$i] ?? null; + break; + + case '--config': + case '-c': + $options['config'] = $argv[++$i] ?? null; + break; + + case '--compare': + $options['compare'] = $argv[++$i] ?? 'content'; + break; + + default: + if (substr($arg, 0, 1) === '-') { + echo "警告: 未知选项 {$arg}\n"; + } + break; + } + } + + return $options; + } + + private function loadConfigFile(string $configPath): array + { + if (!file_exists($configPath)) { + throw new InvalidArgumentException("配置文件不存在: {$configPath}"); + } + + $config = include $configPath; + + if (!is_array($config)) { + throw new InvalidArgumentException("无效的配置文件: {$configPath}"); + } + + return $config; + } + + private function showBanner(): void + { + echo "========================================\n"; + echo " 部署包生成工具 v1.0\n"; + echo " PHP 7.4 兼容版本\n"; + echo "========================================\n\n"; + } + + private function showHelp(): void + { + echo "用法: php generate_deploy_package.php [选项]\n\n"; + echo "选项:\n"; + echo " --source, -s 源目录(新版本)\n"; + echo " --target, -t 目标目录(旧版本)\n"; + echo " --output, -o 输出ZIP文件路径\n"; + echo " --config, -c 配置文件路径\n"; + echo " --compare 比较方法 (content, size, mtime, hash)\n"; + echo " --dry-run, -d 试运行,不实际创建文件\n"; + echo " --verbose, -v 显示详细输出\n"; + echo " --help, -h 显示此帮助信息\n\n"; + echo "示例:\n"; + echo " php generate_deploy_package.php --source ./new_version --target ./old_version --output update.zip\n"; + echo " php generate_deploy_package.php -s /var/www/new -t /var/www/old -o patch.zip --dry-run\n"; + echo " php generate_deploy_package.php --source ./v2.0 --target ./v1.0 --output deploy.zip --config deploy.conf.php\n"; + } + + private function showResults(array $result): void + { + echo "\n生成结果:\n"; + echo "========================================\n"; + echo "输出文件: {$result['output_path']}\n"; + echo "文件大小: {$result['file_size_human']}\n"; + echo "包含文件: {$result['total_files']} 个\n"; + echo " - 新增: {$result['files_added']} 个\n"; + echo " - 修改: {$result['files_modified']} 个\n"; + echo " - 删除: {$result['files_deleted']} 个\n"; + + if (isset($result['dry_run'])) { + echo "模式: 试运行(未实际创建文件)\n"; + } + + echo "========================================\n"; + } +} + +// 主程序入口 +if (PHP_SAPI === 'cli') { + $cli = new PackageGeneratorCLI($argv); + $cli->run(); +} else { + echo "此脚本只能在命令行模式下运行\n"; + exit(1); +} +?> \ No newline at end of file diff --git a/scripts/security_config.php b/scripts/security_config.php new file mode 100644 index 000000000..55515aadb --- /dev/null +++ b/scripts/security_config.php @@ -0,0 +1,72 @@ +#!/usr/bin/env php +// security_config.php + true, + + // 安全删除模式 + 'safe_delete' => true, + + // 受保护的文件模式(不会被删除) + 'protected_patterns' => [ + '/\.env$/', + '/\.env\./', + '/config\.php$/', + '/config\/.*\.php$/', + '/database\.php$/', + '/composer\.json$/', + '/composer\.lock$/', + '/package\.json$/', + '/yarn\.lock$/', + '/\.htaccess$/', + '/web\.config$/', + '/index\.php$/', + '/app\.php$/', + '/bootstrap\.php$/', + '/\.gitignore$/', + '/README\.md$/', + '/LICENSE$/', + '/CHANGELOG$/', + '/robots\.txt$/', + '/sitemap\.xml$/', + '/favicon\.ico$/' + ], + + // 受保护的目录(不会被删除) + 'protected_directories' => [ + '/\.git/', + '/vendor/', + '/node_modules/', + '/storage/', + '/logs/', + '/uploads/', + '/backups/', + '/cache/', + '/temp/', + '/tmp/' + ], + + // 需要确认的删除操作 + 'confirmation_required' => [ + '\.php$' => true, // PHP文件需要确认 + '\.js$' => true, // JS文件需要确认 + '\.css$' => false, // CSS文件不需要确认 + 'config/' => true, // 配置目录需要确认 + ], + + // 备份设置 + 'backup_enabled' => true, + 'backup_dir' => '/var/backups/deploy', + 'max_backups' => 10, + + // 日志设置 + 'log_deletions' => true, + 'log_file' => '/var/log/deploy_deletions.log' +]; +?> \ No newline at end of file diff --git a/src/.env b/src/.env index 699b706d4..afec3dd73 100644 --- a/src/.env +++ b/src/.env @@ -1,20 +1,22 @@ -APP_DEBUG = true -APP_TRACE = true -[APP] -DEFAULT_TIMEZONE = Asia/Shanghai -[LANG] -default_lang = zh-cn -[DATABASE] -TYPE = mysql -HOSTNAME = newshop_mysql -DATABASE = shop_mallnew -USERNAME = shop_mallnew -PASSWORD = shop_mallnew -HOSTPORT = 3306 -CHARSET = utf8 -DEBUG = true -[redis] -HOST = newshop_redis -PORT = 6379 -PASSWORD = 'luckyshop123!@#' +APP_DEBUG = true +APP_TRACE = true +[APP] +DEFAULT_TIMEZONE = Asia/Shanghai +[LANG] +default_lang = zh-cn +[DATABASE] +TYPE = mysql +HOSTNAME = newshop_mysql +DATABASE = shop_mallnew +USERNAME = shop_mallnew +PASSWORD = shop_mallnew +HOSTPORT = 3306 +CHARSET = utf8 +DEBUG = true +[RRDATABASE] +HOSTNAME = 192.168.2.64 +[redis] +HOST = newshop_redis +PORT = 6379 +PASSWORD = 'luckyshop123!@#' EXPIRY = 604800 \ No newline at end of file diff --git a/src/addons/ewei_shopv2/cert/license.php b/src/addons/ewei_shopv2/cert/license.php index f7dd4698e..1cae8b6b8 100644 --- a/src/addons/ewei_shopv2/cert/license.php +++ b/src/addons/ewei_shopv2/cert/license.php @@ -1,21 +1,21 @@ -

// +---------------------------------------------------------------------- -

-

// | Program Name: ӭվԴpnp8.comwww. pnp8 .com ѧϰͷ

-

// +----------------------------------------------------------------------

-

// | Copyright: (c) 2013-2019 http://www.www.pnp8.com! All rights reserved.

-

// +----------------------------------------------------------------------

-

// | Developer: Դpnp8.comwww .pnp8 .com Դ̳

-

// +----------------------------------------------------------------------

-

* л֧֣֧ĶرվԴ

-

* ӭû¸µԴVIPɫԴݴ

-

* https://www-pnp8-com

-

* https://www.pnp8_com/ղرã - -

- ݶһһʮա2޶ڣ涨 - Ϊѧϰоں˼ԭͨװʾߴ - ȷʽʹģԲȨɣ֧! - ڴˣҲϣҰ˵о! - - վԴ붼Դռ޸Ļ߽! - ַȨ,뼰ʱ֪,Ǽɾ!

+

// +---------------------------------------------------------------------- +

+

// | Program Name: ӭվԴpnp8.comwww. pnp8 .com ѧϰͷ

+

// +----------------------------------------------------------------------

+

// | Copyright: (c) 2013-2019 http://www.www.pnp8.com! All rights reserved.

+

// +----------------------------------------------------------------------

+

// | Developer: Դpnp8.comwww .pnp8 .com Դ̳

+

// +----------------------------------------------------------------------

+

* л֧֣֧ĶرվԴ

+

* ӭû¸µԴVIPɫԴݴ

+

* https://www-pnp8-com

+

* https://www.pnp8_com/ղرã + +

+ ݶһһʮա2޶ڣ涨 + Ϊѧϰоں˼ԭͨװʾߴ + ȷʽʹģԲȨɣ֧! + ڴˣҲϣҰ˵о! + + վԴ붼Դռ޸Ļ߽! + ַȨ,뼰ʱ֪,Ǽɾ!

diff --git a/src/addons/ewei_shopv2/core/license.php b/src/addons/ewei_shopv2/core/license.php index f7dd4698e..1cae8b6b8 100644 --- a/src/addons/ewei_shopv2/core/license.php +++ b/src/addons/ewei_shopv2/core/license.php @@ -1,21 +1,21 @@ -

// +---------------------------------------------------------------------- -

-

// | Program Name: ӭվԴpnp8.comwww. pnp8 .com ѧϰͷ

-

// +----------------------------------------------------------------------

-

// | Copyright: (c) 2013-2019 http://www.www.pnp8.com! All rights reserved.

-

// +----------------------------------------------------------------------

-

// | Developer: Դpnp8.comwww .pnp8 .com Դ̳

-

// +----------------------------------------------------------------------

-

* л֧֣֧ĶرվԴ

-

* ӭû¸µԴVIPɫԴݴ

-

* https://www-pnp8-com

-

* https://www.pnp8_com/ղرã - -

- ݶһһʮա2޶ڣ涨 - Ϊѧϰоں˼ԭͨװʾߴ - ȷʽʹģԲȨɣ֧! - ڴˣҲϣҰ˵о! - - վԴ붼Դռ޸Ļ߽! - ַȨ,뼰ʱ֪,Ǽɾ!

+

// +---------------------------------------------------------------------- +

+

// | Program Name: ӭվԴpnp8.comwww. pnp8 .com ѧϰͷ

+

// +----------------------------------------------------------------------

+

// | Copyright: (c) 2013-2019 http://www.www.pnp8.com! All rights reserved.

+

// +----------------------------------------------------------------------

+

// | Developer: Դpnp8.comwww .pnp8 .com Դ̳

+

// +----------------------------------------------------------------------

+

* л֧֣֧ĶرվԴ

+

* ӭû¸µԴVIPɫԴݴ

+

* https://www-pnp8-com

+

* https://www.pnp8_com/ղرã + +

+ ݶһһʮա2޶ڣ涨 + Ϊѧϰоں˼ԭͨװʾߴ + ȷʽʹģԲȨɣ֧! + ڴˣҲϣҰ˵о! + + վԴ붼Դռ޸Ļ߽! + ַȨ,뼰ʱ֪,Ǽɾ!

diff --git a/src/addons/ewei_shopv2/data/license.php b/src/addons/ewei_shopv2/data/license.php index f7dd4698e..1cae8b6b8 100644 --- a/src/addons/ewei_shopv2/data/license.php +++ b/src/addons/ewei_shopv2/data/license.php @@ -1,21 +1,21 @@ -

// +---------------------------------------------------------------------- -

-

// | Program Name: ӭվԴpnp8.comwww. pnp8 .com ѧϰͷ

-

// +----------------------------------------------------------------------

-

// | Copyright: (c) 2013-2019 http://www.www.pnp8.com! All rights reserved.

-

// +----------------------------------------------------------------------

-

// | Developer: Դpnp8.comwww .pnp8 .com Դ̳

-

// +----------------------------------------------------------------------

-

* л֧֣֧ĶرվԴ

-

* ӭû¸µԴVIPɫԴݴ

-

* https://www-pnp8-com

-

* https://www.pnp8_com/ղرã - -

- ݶһһʮա2޶ڣ涨 - Ϊѧϰоں˼ԭͨװʾߴ - ȷʽʹģԲȨɣ֧! - ڴˣҲϣҰ˵о! - - վԴ붼Դռ޸Ļ߽! - ַȨ,뼰ʱ֪,Ǽɾ!

+

// +---------------------------------------------------------------------- +

+

// | Program Name: ӭվԴpnp8.comwww. pnp8 .com ѧϰͷ

+

// +----------------------------------------------------------------------

+

// | Copyright: (c) 2013-2019 http://www.www.pnp8.com! All rights reserved.

+

// +----------------------------------------------------------------------

+

// | Developer: Դpnp8.comwww .pnp8 .com Դ̳

+

// +----------------------------------------------------------------------

+

* л֧֣֧ĶرվԴ

+

* ӭû¸µԴVIPɫԴݴ

+

* https://www-pnp8-com

+

* https://www.pnp8_com/ղرã + +

+ ݶһһʮա2޶ڣ涨 + Ϊѧϰоں˼ԭͨװʾߴ + ȷʽʹģԲȨɣ֧! + ڴˣҲϣҰ˵о! + + վԴ붼Դռ޸Ļ߽! + ַȨ,뼰ʱ֪,Ǽɾ!

diff --git a/src/addons/ewei_shopv2/license.php b/src/addons/ewei_shopv2/license.php index 2e53652b4..ad5eb9873 100644 --- a/src/addons/ewei_shopv2/license.php +++ b/src/addons/ewei_shopv2/license.php @@ -1,21 +1,21 @@ -

// +---------------------------------------------------------------------- -

-

// | Program Name: ӭվԴpnp8.comwww. pnp8.com .com ѧϰͷ

-

// +----------------------------------------------------------------------

-

// | Copyright: (c) 2013-2019 http://www.www.pnp8.com! All rights reserved.

-

// +----------------------------------------------------------------------

-

// | Developer: Դpnp8.comwww .pnp8.com .com Դ̳

-

// +----------------------------------------------------------------------

-

* л֧֣֧ĶرվԴ

-

* ӭû¸µԴVIPɫԴݴ

-

* https://www-pnp8.com-com

-

* https://www.pnp8.com_com/ղرã - -

- ݶһһʮա2޶ڣ涨 - Ϊѧϰоں˼ԭͨװʾߴ - ȷʽʹģԲȨɣ֧! - ڴˣҲϣҰ˵о! - - վԴ붼Դռ޸Ļ߽! - ַȨ,뼰ʱ֪,Ǽɾ!

+

// +---------------------------------------------------------------------- +

+

// | Program Name: ӭվԴpnp8.comwww. pnp8.com .com ѧϰͷ

+

// +----------------------------------------------------------------------

+

// | Copyright: (c) 2013-2019 http://www.www.pnp8.com! All rights reserved.

+

// +----------------------------------------------------------------------

+

// | Developer: Դpnp8.comwww .pnp8.com .com Դ̳

+

// +----------------------------------------------------------------------

+

* л֧֣֧ĶرվԴ

+

* ӭû¸µԴVIPɫԴݴ

+

* https://www-pnp8.com-com

+

* https://www.pnp8.com_com/ղرã + +

+ ݶһһʮա2޶ڣ涨 + Ϊѧϰоں˼ԭͨװʾߴ + ȷʽʹģԲȨɣ֧! + ڴˣҲϣҰ˵о! + + վԴ붼Դռ޸Ļ߽! + ַȨ,뼰ʱ֪,Ǽɾ!

diff --git a/src/addons/ewei_shopv2/payment/license.php b/src/addons/ewei_shopv2/payment/license.php index f7dd4698e..1cae8b6b8 100644 --- a/src/addons/ewei_shopv2/payment/license.php +++ b/src/addons/ewei_shopv2/payment/license.php @@ -1,21 +1,21 @@ -

// +---------------------------------------------------------------------- -

-

// | Program Name: ӭվԴpnp8.comwww. pnp8 .com ѧϰͷ

-

// +----------------------------------------------------------------------

-

// | Copyright: (c) 2013-2019 http://www.www.pnp8.com! All rights reserved.

-

// +----------------------------------------------------------------------

-

// | Developer: Դpnp8.comwww .pnp8 .com Դ̳

-

// +----------------------------------------------------------------------

-

* л֧֣֧ĶرվԴ

-

* ӭû¸µԴVIPɫԴݴ

-

* https://www-pnp8-com

-

* https://www.pnp8_com/ղرã - -

- ݶһһʮա2޶ڣ涨 - Ϊѧϰоں˼ԭͨװʾߴ - ȷʽʹģԲȨɣ֧! - ڴˣҲϣҰ˵о! - - վԴ붼Դռ޸Ļ߽! - ַȨ,뼰ʱ֪,Ǽɾ!

+

// +---------------------------------------------------------------------- +

+

// | Program Name: ӭվԴpnp8.comwww. pnp8 .com ѧϰͷ

+

// +----------------------------------------------------------------------

+

// | Copyright: (c) 2013-2019 http://www.www.pnp8.com! All rights reserved.

+

// +----------------------------------------------------------------------

+

// | Developer: Դpnp8.comwww .pnp8 .com Դ̳

+

// +----------------------------------------------------------------------

+

* л֧֣֧ĶرվԴ

+

* ӭû¸µԴVIPɫԴݴ

+

* https://www-pnp8-com

+

* https://www.pnp8_com/ղرã + +

+ ݶһһʮա2޶ڣ涨 + Ϊѧϰоں˼ԭͨװʾߴ + ȷʽʹģԲȨɣ֧! + ڴˣҲϣҰ˵о! + + վԴ붼Դռ޸Ļ߽! + ַȨ,뼰ʱ֪,Ǽɾ!

diff --git a/src/addons/ewei_shopv2/pcsite/license.php b/src/addons/ewei_shopv2/pcsite/license.php index f7dd4698e..1cae8b6b8 100644 --- a/src/addons/ewei_shopv2/pcsite/license.php +++ b/src/addons/ewei_shopv2/pcsite/license.php @@ -1,21 +1,21 @@ -

// +---------------------------------------------------------------------- -

-

// | Program Name: ӭվԴpnp8.comwww. pnp8 .com ѧϰͷ

-

// +----------------------------------------------------------------------

-

// | Copyright: (c) 2013-2019 http://www.www.pnp8.com! All rights reserved.

-

// +----------------------------------------------------------------------

-

// | Developer: Դpnp8.comwww .pnp8 .com Դ̳

-

// +----------------------------------------------------------------------

-

* л֧֣֧ĶرվԴ

-

* ӭû¸µԴVIPɫԴݴ

-

* https://www-pnp8-com

-

* https://www.pnp8_com/ղرã - -

- ݶһһʮա2޶ڣ涨 - Ϊѧϰоں˼ԭͨװʾߴ - ȷʽʹģԲȨɣ֧! - ڴˣҲϣҰ˵о! - - վԴ붼Դռ޸Ļ߽! - ַȨ,뼰ʱ֪,Ǽɾ!

+

// +---------------------------------------------------------------------- +

+

// | Program Name: ӭվԴpnp8.comwww. pnp8 .com ѧϰͷ

+

// +----------------------------------------------------------------------

+

// | Copyright: (c) 2013-2019 http://www.www.pnp8.com! All rights reserved.

+

// +----------------------------------------------------------------------

+

// | Developer: Դpnp8.comwww .pnp8 .com Դ̳

+

// +----------------------------------------------------------------------

+

* л֧֣֧ĶرվԴ

+

* ӭû¸µԴVIPɫԴݴ

+

* https://www-pnp8-com

+

* https://www.pnp8_com/ղرã + +

+ ݶһһʮա2޶ڣ涨 + Ϊѧϰоں˼ԭͨװʾߴ + ȷʽʹģԲȨɣ֧! + ڴˣҲϣҰ˵о! + + վԴ붼Դռ޸Ļ߽! + ַȨ,뼰ʱ֪,Ǽɾ!

diff --git a/src/addons/ewei_shopv2/plugin/exchange/static/js/swipe.js b/src/addons/ewei_shopv2/plugin/exchange/static/js/swipe.js index 3b3b1a67f..8b1304018 100644 --- a/src/addons/ewei_shopv2/plugin/exchange/static/js/swipe.js +++ b/src/addons/ewei_shopv2/plugin/exchange/static/js/swipe.js @@ -1,525 +1,525 @@ -/* - * Swipe 2.0 - * - * Brad Birdsall - * Copyright 2012, Licensed GPL & MIT - * -*/ - -window.Swipe = function(element, options) { - - var _this = this; - - // return immediately if element doesn't exist - if (!element) return; - - // reference dom elements - this.container = element; - this.element = this.container.children[0]; - - // simple feature detection - this.browser = { - touch: (function() { - return ('ontouchstart' in window) || window.DocumentTouch && document instanceof DocumentTouch; - })(), - transitions: (function() { - var temp = document.createElement('swipe'), - props = ['perspectiveProperty', 'WebkitPerspective', 'MozPerspective', 'OPerspective', 'msPerspective']; - for ( var i in props ) { - if (temp.style[ props[i] ] !== undefined) return true; - } - return false; - })() - }; - - // retreive options - options = options || {}; - this.index = options.startSlide || 0; - this.speed = options.speed || 200; - this.callback = options.callback || function() {}; - this.transitionEnd = options.transitionEnd || function() {}; - this.delay = options.auto || 0; - this.cont = (options.continuous != undefined) ? !!options.continuous : true; - this.disableScroll = !!options.disableScroll; - - // verify index is a number not string - this.index = parseInt(this.index,10); - - // trigger slider initialization - this.setup(); - - // begin auto slideshow - this.begin(); - - // add event listeners - if (this.element.addEventListener) { - if (!!this.browser.touch) { - this.element.addEventListener('touchstart', this, false); - this.element.addEventListener('touchmove', this, false); - this.element.addEventListener('touchend', this, false); - } - if (!!this.browser.transitions) { - this.element.addEventListener('webkitTransitionEnd', this, false); - this.element.addEventListener('msTransitionEnd', this, false); - this.element.addEventListener('oTransitionEnd', this, false); - this.element.addEventListener('transitionend', this, false); - } - window.addEventListener('resize', this, false); - } - - // to play nice with old IE - else { - window.onresize = function () { - _this.setup(); - }; - } - -}; - -Swipe.prototype = { - createElem:function(){ - this.length = this.slides.length; - if (this.length < 2) return; - - var last=$(this.slides[this.length-1]).clone(); - var first=$(this.slides[0]).clone(); - - $(this.element).append(first); - $(this.element).prepend(last); - this.length = this.slides.length; - $(this.slides[0]).attr('id','') - $(this.slides[this.length-1]).attr('id','') - }, - setup: function() { - - // get and measure amt of slides - this.slides = this.element.children; - this.length = this.slides.length; - this.cache = new Array(this.length); - - // return immediately if their are less than two slides - - - // determine width of each slide - this.width = this.container.getBoundingClientRect().width || this.container.offsetWidth; - - // return immediately if measurement fails - if (!this.width) return; - - // store array of slides before, current, and after - var refArray = [[],[],[]]; - - this.element.style.width = (this.slides.length * this.width) + 'px'; - // stack elements - - for (var index = this.length - 1; index > -1; index--) { - - var elem = this.slides[index]; - - elem.style.width = this.width + 'px'; - elem.setAttribute('data-index', index); - - if (this.browser.transitions) { - elem.style.left = (index * -this.width) + 'px'; - } - - // add this index to the reference array 0:before 1:equal 2:after - refArray[this.index > index ? 0 : (this.index < index ? 2 : 1)].push(index); - - } - //console.log(refArray) - - if (this.browser.transitions) { - - // stack left, current, and right slides - this._stack(refArray[0],-1); - this._stack(refArray[1],0); - this._stack(refArray[2],1); - - } else { - // move "viewport" to put current slide into view - this.element.style.left = (this.index * -this.width)+"px"; - } - - this.container.style.visibility = 'visible'; - - }, - - kill: function() { - - // cancel slideshow - this.delay = 0; - clearTimeout(this.interval); - - // clear all translations - var slideArray = []; - for (var i = this.slides.length - 1; i >= 0; i--) { - this.slides[i].style.width = ''; - slideArray.push(i); - } - this._stack(slideArray,0); - - var elem = this.element; - elem.className = elem.className.replace('swipe-active',''); - - // remove event listeners - if (this.element.removeEventListener) { - if (!!this.browser.touch) { - this.element.removeEventListener('touchstart', this, false); - this.element.removeEventListener('touchmove', this, false); - this.element.removeEventListener('touchend', this, false); - } - if (!!this.browser.transitions) { - this.element.removeEventListener('webkitTransitionEnd', this, false); - this.element.removeEventListener('msTransitionEnd', this, false); - this.element.removeEventListener('oTransitionEnd', this, false); - this.element.removeEventListener('transitionend', this, false); - } - window.removeEventListener('resize', this, false); - } - - // kill old IE! you can quote me on that ;) - else { - window.onresize = null; - } - - }, - - getPos: function() { - - // return current index position - return this.index; - - }, - - prev: function(delay) { - - // cancel slideshow - this.delay = delay || 0; - clearTimeout(this.interval); - - // if not at first slide - if (this.index) this.slide(this.index-1, this.speed); - else if (this.cont) this.slide(this.length-1, this.speed); - - }, - - next: function(delay) { - - // cancel slideshow - this.delay = delay || 0; - clearTimeout(this.interval); - - if (this.index < this.length - 1) this.slide(this.index+1, this.speed); // if not last slide - else if (this.cont) this.slide(0, this.speed); //if last slide return to start - - }, - showelem: function(index) { - clearTimeout(this.interval); - this.slide(index, this.speed); - }, - begin: function() { - - var _this = this; - - this.interval = (this.delay) - ? setTimeout(function() { - _this.next(_this.delay); - }, this.delay) - : 0; - - }, - - handleEvent: function(e) { - switch (e.type) { - case 'touchstart': - this.onTouchStart(e); - break; - case 'touchmove': - this.onTouchMove(e); - break; - case 'touchend': - this.onTouchEnd(e); - break; - case 'webkitTransitionEnd': - case 'msTransitionEnd': - case 'oTransitionEnd': // opera 11 and below - case 'otransitionend': // opera 12 (and above?) - case 'transitionend': - this.onTransitionEnd(e); - break; - case 'resize': - this.setup(); - break; - } - - e.stopPropagation(); - }, - - onTouchStart: function(e) { - - var _this = this; - - _this.start = { - - // get touch coordinates for delta calculations in onTouchMove - pageX: e.touches[0].pageX, - pageY: e.touches[0].pageY, - - // set initial timestamp of touch sequence - time: Number( new Date() ) - - }; - - // used for testing first onTouchMove event - _this.isScrolling = undefined; - - // reset deltaX - _this.deltaX = 0; - - }, - - onTouchMove: function(e) { - - var _this = this; - - // ensure swiping with one touch and not pinching - if(e.touches.length > 1 || e.scale && e.scale !== 1) return; - - _this.deltaX = e.touches[0].pageX - _this.start.pageX; - - // determine if scrolling test has run - one time test - if ( typeof _this.isScrolling == 'undefined') { - _this.isScrolling = !!( _this.isScrolling || Math.abs(_this.deltaX) < Math.abs(e.touches[0].pageY - _this.start.pageY) ); - } - - // if user is not trying to scroll vertically - if (!_this.isScrolling) { - - // prevent native scrolling - e.preventDefault(); - - // cancel slideshow - _this.delay = 0; - clearTimeout(_this.interval); - - // increase resistance if first or last slide - _this.deltaX = - _this.deltaX / - ( (!_this.index && _this.deltaX > 0 // if first slide and sliding left - || _this.index == _this.length - 1 // or if last slide and sliding right - && _this.deltaX < 0 // and if sliding at all - ) ? - ( Math.abs(_this.deltaX) / _this.width + 1 ) // determine resistance level - : 1 ); // no resistance if false - - // translate immediately 1:1 - _this._move([_this.index-1,_this.index,_this.index+1],_this.deltaX); - - } else if (_this.disableScroll) { - - // prevent native scrolling - e.preventDefault(); - - } - - }, - - onTouchEnd: function(e) { - - var _this = this; - - // determine if slide attempt triggers next/prev slide - var isValidSlide = - Number(new Date()) - _this.start.time < 250 // if slide duration is less than 250ms - && Math.abs(_this.deltaX) > 20 // and if slide amt is greater than 20px - || Math.abs(_this.deltaX) > _this.width/6, // or if slide amt is greater than half the width - - // determine if slide attempt is past start and end - isPastBounds = - !_this.index && _this.deltaX > 0 // if first slide and slide amt is greater than 0 - || _this.index == _this.length - 1 && _this.deltaX < 0, // or if last slide and slide amt is less than 0 - - direction = _this.deltaX < 0; // true:right false:left - - // if not scrolling vertically - if (!_this.isScrolling) { - - if (isValidSlide && !isPastBounds) { - if (direction) { - _this._stack([_this.index-1],-1); - _this._slide([_this.index,_this.index+1],-_this.width,_this.speed); - _this.index += 1; - } else { - _this._stack([_this.index+1],1); - _this._slide([_this.index-1,_this.index],_this.width,_this.speed); - _this.index += -1; - } - _this.callback(_this.index, _this.slides[_this.index]); - } else { - _this._slide([_this.index-1,_this.index,_this.index+1],0,_this.speed); - } - } - - }, - - onTransitionEnd: function(e) { - - if (this._getElemIndex(e.target) == this.index) { // only call transition end on the main slide item - - if (this.delay) this.begin(); - - this.transitionEnd(this.index, this.slides[this.index]); - - } - - }, - - slide: function(to, speed) { - - var from = this.index; - - if (from == to) return; // do nothing if already on requested slide - - var speed = (typeof speed === "Undefined") ? this.speed : speed; - - if (this.browser.transitions) { - var toStack = Math.abs(from-to) - 1, - direction = Math.abs(from-to) / (from-to), // 1:right -1:left - inBetween = []; - - while (toStack--) inBetween.push( (to > from ? to : from) - toStack - 1 ); - - // stack em - this._stack(inBetween,direction); - - // now slide from and to in the proper direction - this._slide([from,to],this.width * direction,speed); - } - else { - this._animate(from*-this.width, to * -this.width, speed) - } - - this.index = to; - - this.callback(this.index, this.slides[this.index]); - - }, - - _slide: function(nums, dist, speed) { - - var _slides = this.slides, - l = nums.length; - while(l--) { - - this._translate(_slides[nums[l]], dist + this.cache[nums[l]], speed ? speed : 0); - - this.cache[nums[l]] += dist; - - } - - }, - - _stack: function(nums, pos) { // pos: -1:left 0:center 1:right - - var _slides = this.slides, - l = nums.length, - dist = this.width * pos; - - while(l--) { - - this._translate(_slides[nums[l]], dist, 0); - - this.cache[nums[l]] = dist; - - } - - }, - - _move: function(nums, dist) { // 1:1 scrolling - - var _slides = this.slides, - l = nums.length; - while(l--) this._translate(_slides[nums[l]], dist + this.cache[nums[l]], 0); - - }, - - _translate: function(elem, xval, speed) { - - if (!elem) return; - - var style = elem.style; - - // set duration speed to 0 - style.webkitTransitionDuration = - style.MozTransitionDuration = - style.msTransitionDuration = - style.OTransitionDuration = - style.transitionDuration = speed + 'ms'; - - // translate to given position - style.webkitTransform = 'translate(' + xval + 'px,0)' + 'translateZ(0)'; - style.msTransform = - style.MozTransform = - style.OTransform = 'translateX(' + xval + 'px)'; - - }, - - _animate: function(from, to, speed) { - - var elem = this.element; - - if (!speed) { // if not an animation, just reposition - - elem.style.left = to + 'px'; - - return; - - } - - var _this = this, - start = new Date(), - timer = setInterval(function() { - - var timeElap = new Date() - start; - - if (timeElap > speed) { - - elem.style.left = to + 'px'; // callback after this line - - if (_this.delay) _this.begin(); - - _this.transitionEnd(_this.index, _this.slides[_this.index]); - - - clearInterval(timer); - - return; - - } - - elem.style.left = (( (to - from) * (Math.floor((timeElap / speed) * 100) / 100) ) + from) + 'px'; - - }, 4); - - }, - - _getElemIndex: function(elem) { - - return parseInt(elem.getAttribute('data-index'),10); - - } - -}; - - -if ( window.jQuery || window.Zepto ) { - (function($) { - $.fn.Swipe = function(params) { - return this.each(function() { - var _this = $(this); - _this.data('Swipe', new Swipe(_this[0], params)); - }); - } - })( window.jQuery || window.Zepto ) +/* + * Swipe 2.0 + * + * Brad Birdsall + * Copyright 2012, Licensed GPL & MIT + * +*/ + +window.Swipe = function(element, options) { + + var _this = this; + + // return immediately if element doesn't exist + if (!element) return; + + // reference dom elements + this.container = element; + this.element = this.container.children[0]; + + // simple feature detection + this.browser = { + touch: (function() { + return ('ontouchstart' in window) || window.DocumentTouch && document instanceof DocumentTouch; + })(), + transitions: (function() { + var temp = document.createElement('swipe'), + props = ['perspectiveProperty', 'WebkitPerspective', 'MozPerspective', 'OPerspective', 'msPerspective']; + for ( var i in props ) { + if (temp.style[ props[i] ] !== undefined) return true; + } + return false; + })() + }; + + // retreive options + options = options || {}; + this.index = options.startSlide || 0; + this.speed = options.speed || 200; + this.callback = options.callback || function() {}; + this.transitionEnd = options.transitionEnd || function() {}; + this.delay = options.auto || 0; + this.cont = (options.continuous != undefined) ? !!options.continuous : true; + this.disableScroll = !!options.disableScroll; + + // verify index is a number not string + this.index = parseInt(this.index,10); + + // trigger slider initialization + this.setup(); + + // begin auto slideshow + this.begin(); + + // add event listeners + if (this.element.addEventListener) { + if (!!this.browser.touch) { + this.element.addEventListener('touchstart', this, false); + this.element.addEventListener('touchmove', this, false); + this.element.addEventListener('touchend', this, false); + } + if (!!this.browser.transitions) { + this.element.addEventListener('webkitTransitionEnd', this, false); + this.element.addEventListener('msTransitionEnd', this, false); + this.element.addEventListener('oTransitionEnd', this, false); + this.element.addEventListener('transitionend', this, false); + } + window.addEventListener('resize', this, false); + } + + // to play nice with old IE + else { + window.onresize = function () { + _this.setup(); + }; + } + +}; + +Swipe.prototype = { + createElem:function(){ + this.length = this.slides.length; + if (this.length < 2) return; + + var last=$(this.slides[this.length-1]).clone(); + var first=$(this.slides[0]).clone(); + + $(this.element).append(first); + $(this.element).prepend(last); + this.length = this.slides.length; + $(this.slides[0]).attr('id','') + $(this.slides[this.length-1]).attr('id','') + }, + setup: function() { + + // get and measure amt of slides + this.slides = this.element.children; + this.length = this.slides.length; + this.cache = new Array(this.length); + + // return immediately if their are less than two slides + + + // determine width of each slide + this.width = this.container.getBoundingClientRect().width || this.container.offsetWidth; + + // return immediately if measurement fails + if (!this.width) return; + + // store array of slides before, current, and after + var refArray = [[],[],[]]; + + this.element.style.width = (this.slides.length * this.width) + 'px'; + // stack elements + + for (var index = this.length - 1; index > -1; index--) { + + var elem = this.slides[index]; + + elem.style.width = this.width + 'px'; + elem.setAttribute('data-index', index); + + if (this.browser.transitions) { + elem.style.left = (index * -this.width) + 'px'; + } + + // add this index to the reference array 0:before 1:equal 2:after + refArray[this.index > index ? 0 : (this.index < index ? 2 : 1)].push(index); + + } + //console.log(refArray) + + if (this.browser.transitions) { + + // stack left, current, and right slides + this._stack(refArray[0],-1); + this._stack(refArray[1],0); + this._stack(refArray[2],1); + + } else { + // move "viewport" to put current slide into view + this.element.style.left = (this.index * -this.width)+"px"; + } + + this.container.style.visibility = 'visible'; + + }, + + kill: function() { + + // cancel slideshow + this.delay = 0; + clearTimeout(this.interval); + + // clear all translations + var slideArray = []; + for (var i = this.slides.length - 1; i >= 0; i--) { + this.slides[i].style.width = ''; + slideArray.push(i); + } + this._stack(slideArray,0); + + var elem = this.element; + elem.className = elem.className.replace('swipe-active',''); + + // remove event listeners + if (this.element.removeEventListener) { + if (!!this.browser.touch) { + this.element.removeEventListener('touchstart', this, false); + this.element.removeEventListener('touchmove', this, false); + this.element.removeEventListener('touchend', this, false); + } + if (!!this.browser.transitions) { + this.element.removeEventListener('webkitTransitionEnd', this, false); + this.element.removeEventListener('msTransitionEnd', this, false); + this.element.removeEventListener('oTransitionEnd', this, false); + this.element.removeEventListener('transitionend', this, false); + } + window.removeEventListener('resize', this, false); + } + + // kill old IE! you can quote me on that ;) + else { + window.onresize = null; + } + + }, + + getPos: function() { + + // return current index position + return this.index; + + }, + + prev: function(delay) { + + // cancel slideshow + this.delay = delay || 0; + clearTimeout(this.interval); + + // if not at first slide + if (this.index) this.slide(this.index-1, this.speed); + else if (this.cont) this.slide(this.length-1, this.speed); + + }, + + next: function(delay) { + + // cancel slideshow + this.delay = delay || 0; + clearTimeout(this.interval); + + if (this.index < this.length - 1) this.slide(this.index+1, this.speed); // if not last slide + else if (this.cont) this.slide(0, this.speed); //if last slide return to start + + }, + showelem: function(index) { + clearTimeout(this.interval); + this.slide(index, this.speed); + }, + begin: function() { + + var _this = this; + + this.interval = (this.delay) + ? setTimeout(function() { + _this.next(_this.delay); + }, this.delay) + : 0; + + }, + + handleEvent: function(e) { + switch (e.type) { + case 'touchstart': + this.onTouchStart(e); + break; + case 'touchmove': + this.onTouchMove(e); + break; + case 'touchend': + this.onTouchEnd(e); + break; + case 'webkitTransitionEnd': + case 'msTransitionEnd': + case 'oTransitionEnd': // opera 11 and below + case 'otransitionend': // opera 12 (and above?) + case 'transitionend': + this.onTransitionEnd(e); + break; + case 'resize': + this.setup(); + break; + } + + e.stopPropagation(); + }, + + onTouchStart: function(e) { + + var _this = this; + + _this.start = { + + // get touch coordinates for delta calculations in onTouchMove + pageX: e.touches[0].pageX, + pageY: e.touches[0].pageY, + + // set initial timestamp of touch sequence + time: Number( new Date() ) + + }; + + // used for testing first onTouchMove event + _this.isScrolling = undefined; + + // reset deltaX + _this.deltaX = 0; + + }, + + onTouchMove: function(e) { + + var _this = this; + + // ensure swiping with one touch and not pinching + if(e.touches.length > 1 || e.scale && e.scale !== 1) return; + + _this.deltaX = e.touches[0].pageX - _this.start.pageX; + + // determine if scrolling test has run - one time test + if ( typeof _this.isScrolling == 'undefined') { + _this.isScrolling = !!( _this.isScrolling || Math.abs(_this.deltaX) < Math.abs(e.touches[0].pageY - _this.start.pageY) ); + } + + // if user is not trying to scroll vertically + if (!_this.isScrolling) { + + // prevent native scrolling + e.preventDefault(); + + // cancel slideshow + _this.delay = 0; + clearTimeout(_this.interval); + + // increase resistance if first or last slide + _this.deltaX = + _this.deltaX / + ( (!_this.index && _this.deltaX > 0 // if first slide and sliding left + || _this.index == _this.length - 1 // or if last slide and sliding right + && _this.deltaX < 0 // and if sliding at all + ) ? + ( Math.abs(_this.deltaX) / _this.width + 1 ) // determine resistance level + : 1 ); // no resistance if false + + // translate immediately 1:1 + _this._move([_this.index-1,_this.index,_this.index+1],_this.deltaX); + + } else if (_this.disableScroll) { + + // prevent native scrolling + e.preventDefault(); + + } + + }, + + onTouchEnd: function(e) { + + var _this = this; + + // determine if slide attempt triggers next/prev slide + var isValidSlide = + Number(new Date()) - _this.start.time < 250 // if slide duration is less than 250ms + && Math.abs(_this.deltaX) > 20 // and if slide amt is greater than 20px + || Math.abs(_this.deltaX) > _this.width/6, // or if slide amt is greater than half the width + + // determine if slide attempt is past start and end + isPastBounds = + !_this.index && _this.deltaX > 0 // if first slide and slide amt is greater than 0 + || _this.index == _this.length - 1 && _this.deltaX < 0, // or if last slide and slide amt is less than 0 + + direction = _this.deltaX < 0; // true:right false:left + + // if not scrolling vertically + if (!_this.isScrolling) { + + if (isValidSlide && !isPastBounds) { + if (direction) { + _this._stack([_this.index-1],-1); + _this._slide([_this.index,_this.index+1],-_this.width,_this.speed); + _this.index += 1; + } else { + _this._stack([_this.index+1],1); + _this._slide([_this.index-1,_this.index],_this.width,_this.speed); + _this.index += -1; + } + _this.callback(_this.index, _this.slides[_this.index]); + } else { + _this._slide([_this.index-1,_this.index,_this.index+1],0,_this.speed); + } + } + + }, + + onTransitionEnd: function(e) { + + if (this._getElemIndex(e.target) == this.index) { // only call transition end on the main slide item + + if (this.delay) this.begin(); + + this.transitionEnd(this.index, this.slides[this.index]); + + } + + }, + + slide: function(to, speed) { + + var from = this.index; + + if (from == to) return; // do nothing if already on requested slide + + var speed = (typeof speed === "Undefined") ? this.speed : speed; + + if (this.browser.transitions) { + var toStack = Math.abs(from-to) - 1, + direction = Math.abs(from-to) / (from-to), // 1:right -1:left + inBetween = []; + + while (toStack--) inBetween.push( (to > from ? to : from) - toStack - 1 ); + + // stack em + this._stack(inBetween,direction); + + // now slide from and to in the proper direction + this._slide([from,to],this.width * direction,speed); + } + else { + this._animate(from*-this.width, to * -this.width, speed) + } + + this.index = to; + + this.callback(this.index, this.slides[this.index]); + + }, + + _slide: function(nums, dist, speed) { + + var _slides = this.slides, + l = nums.length; + while(l--) { + + this._translate(_slides[nums[l]], dist + this.cache[nums[l]], speed ? speed : 0); + + this.cache[nums[l]] += dist; + + } + + }, + + _stack: function(nums, pos) { // pos: -1:left 0:center 1:right + + var _slides = this.slides, + l = nums.length, + dist = this.width * pos; + + while(l--) { + + this._translate(_slides[nums[l]], dist, 0); + + this.cache[nums[l]] = dist; + + } + + }, + + _move: function(nums, dist) { // 1:1 scrolling + + var _slides = this.slides, + l = nums.length; + while(l--) this._translate(_slides[nums[l]], dist + this.cache[nums[l]], 0); + + }, + + _translate: function(elem, xval, speed) { + + if (!elem) return; + + var style = elem.style; + + // set duration speed to 0 + style.webkitTransitionDuration = + style.MozTransitionDuration = + style.msTransitionDuration = + style.OTransitionDuration = + style.transitionDuration = speed + 'ms'; + + // translate to given position + style.webkitTransform = 'translate(' + xval + 'px,0)' + 'translateZ(0)'; + style.msTransform = + style.MozTransform = + style.OTransform = 'translateX(' + xval + 'px)'; + + }, + + _animate: function(from, to, speed) { + + var elem = this.element; + + if (!speed) { // if not an animation, just reposition + + elem.style.left = to + 'px'; + + return; + + } + + var _this = this, + start = new Date(), + timer = setInterval(function() { + + var timeElap = new Date() - start; + + if (timeElap > speed) { + + elem.style.left = to + 'px'; // callback after this line + + if (_this.delay) _this.begin(); + + _this.transitionEnd(_this.index, _this.slides[_this.index]); + + + clearInterval(timer); + + return; + + } + + elem.style.left = (( (to - from) * (Math.floor((timeElap / speed) * 100) / 100) ) + from) + 'px'; + + }, 4); + + }, + + _getElemIndex: function(elem) { + + return parseInt(elem.getAttribute('data-index'),10); + + } + +}; + + +if ( window.jQuery || window.Zepto ) { + (function($) { + $.fn.Swipe = function(params) { + return this.each(function() { + var _this = $(this); + _this.data('Swipe', new Swipe(_this[0], params)); + }); + } + })( window.jQuery || window.Zepto ) } \ No newline at end of file diff --git a/src/addons/ewei_shopv2/plugin/license.php b/src/addons/ewei_shopv2/plugin/license.php index f7dd4698e..1cae8b6b8 100644 --- a/src/addons/ewei_shopv2/plugin/license.php +++ b/src/addons/ewei_shopv2/plugin/license.php @@ -1,21 +1,21 @@ -

// +---------------------------------------------------------------------- -

-

// | Program Name: ӭվԴpnp8.comwww. pnp8 .com ѧϰͷ

-

// +----------------------------------------------------------------------

-

// | Copyright: (c) 2013-2019 http://www.www.pnp8.com! All rights reserved.

-

// +----------------------------------------------------------------------

-

// | Developer: Դpnp8.comwww .pnp8 .com Դ̳

-

// +----------------------------------------------------------------------

-

* л֧֣֧ĶرվԴ

-

* ӭû¸µԴVIPɫԴݴ

-

* https://www-pnp8-com

-

* https://www.pnp8_com/ղرã - -

- ݶһһʮա2޶ڣ涨 - Ϊѧϰоں˼ԭͨװʾߴ - ȷʽʹģԲȨɣ֧! - ڴˣҲϣҰ˵о! - - վԴ붼Դռ޸Ļ߽! - ַȨ,뼰ʱ֪,Ǽɾ!

+

// +---------------------------------------------------------------------- +

+

// | Program Name: ӭվԴpnp8.comwww. pnp8 .com ѧϰͷ

+

// +----------------------------------------------------------------------

+

// | Copyright: (c) 2013-2019 http://www.www.pnp8.com! All rights reserved.

+

// +----------------------------------------------------------------------

+

// | Developer: Դpnp8.comwww .pnp8 .com Դ̳

+

// +----------------------------------------------------------------------

+

* л֧֣֧ĶرվԴ

+

* ӭû¸µԴVIPɫԴݴ

+

* https://www-pnp8-com

+

* https://www.pnp8_com/ղرã + +

+ ݶһһʮա2޶ڣ涨 + Ϊѧϰоں˼ԭͨװʾߴ + ȷʽʹģԲȨɣ֧! + ڴˣҲϣҰ˵о! + + վԴ붼Դռ޸Ļ߽! + ַȨ,뼰ʱ֪,Ǽɾ!

diff --git a/src/addons/ewei_shopv2/plugin/pc/template/mobile/default/order/pay/peerpay.html b/src/addons/ewei_shopv2/plugin/pc/template/mobile/default/order/pay/peerpay.html index b55e2dc01..be61fd000 100644 --- a/src/addons/ewei_shopv2/plugin/pc/template/mobile/default/order/pay/peerpay.html +++ b/src/addons/ewei_shopv2/plugin/pc/template/mobile/default/order/pay/peerpay.html @@ -1,117 +1,117 @@ -{template '_header'} - -
-
-
来自{$orderMember['nickname']}的代付订单
-
-
- {$goods['title']} -
-
- {$goods['title']} -
-
- {$address['realname']}  {php echo substr_replace($address['mobile'],'****',-4)} - {$address['province']} {$address['city']} {$address['area']} **** - ¥{$order['price']} -
- -
- -
-
- -
-
-
- - 1 -
留言并分享
-
- - 2 -
参与并付款
-
- - 3 -
代付成功
-
-
-
-
-
- - 1 -
留言并分享
-
- - 2 -
多人参与并付款
-
- - 3 -
筹集完金额
-
- - 4 -
代付成功
-
-
-
-
-
-
- -
-
-
-
- - - - - - -
-
- - +{template '_header'} + +
+
+
来自{$orderMember['nickname']}的代付订单
+
+
+ {$goods['title']} +
+
+ {$goods['title']} +
+
+ {$address['realname']}  {php echo substr_replace($address['mobile'],'****',-4)} + {$address['province']} {$address['city']} {$address['area']} **** + ¥{$order['price']} +
+ +
+ +
+
+ +
+
+
+ + 1 +
留言并分享
+
+ + 2 +
参与并付款
+
+ + 3 +
代付成功
+
+
+
+
+
+ + 1 +
留言并分享
+
+ + 2 +
多人参与并付款
+
+ + 3 +
筹集完金额
+
+ + 4 +
代付成功
+
+
+
+
+
+
+ +
+
+
+
+ + + + + + +
+
+ + {template '_footer'} \ No newline at end of file diff --git a/src/addons/ewei_shopv2/plugin/pc/template/mobile/default/order/pay/peerpaydetail.html b/src/addons/ewei_shopv2/plugin/pc/template/mobile/default/order/pay/peerpaydetail.html index 14f72043e..b5ed36e81 100644 --- a/src/addons/ewei_shopv2/plugin/pc/template/mobile/default/order/pay/peerpaydetail.html +++ b/src/addons/ewei_shopv2/plugin/pc/template/mobile/default/order/pay/peerpaydetail.html @@ -1,115 +1,115 @@ -{template '_header'} - -
-
-
- - - -
等待真爱路过...
- -
-
- -
-
已完成:{$rate}%
-
还差:{$rate_price}元
-
- -
- -
-
-
- -
来自{$member['nickname']}的代付订单
-
-
- {$goods['title']} -
-
- {$goods['title']} -
-
- {$address['realname']}  {php echo substr_replace($address['mobile'],'****',-4)} - {$address['province']} {$address['city']} {$address['area']} **** - ¥{$peerpay['peerpay_realprice']} -
- -
- -
-
-{if $peerpay['peerpay_type'] == 1} - {loop $message $mk $mv} -
-
-
- -
-
{$mv['uname']}帮他付了 ¥{$mv['price']}
-
{php echo $mk+1;}F
-
{php echo date('m-d H:i');}
-
-
{$mv['usay']}
-
- {/loop} - {if !empty($message)} -
- {/if} -
- {if $rate_price > 0} -
-
- - -
-
- {/if} -
-{/if} - - {if $rate_price > 0}
-
-
付款金额
-
¥  - -
-
-
- 立即支付{/if} - 我也要去玩 - 找小伙伴帮TA付款 -
-
- - +{template '_header'} + +
+
+
+ + + +
等待真爱路过...
+ +
+
+ +
+
已完成:{$rate}%
+
还差:{$rate_price}元
+
+ +
+ +
+
+
+ +
来自{$member['nickname']}的代付订单
+
+
+ {$goods['title']} +
+
+ {$goods['title']} +
+
+ {$address['realname']}  {php echo substr_replace($address['mobile'],'****',-4)} + {$address['province']} {$address['city']} {$address['area']} **** + ¥{$peerpay['peerpay_realprice']} +
+ +
+ +
+
+{if $peerpay['peerpay_type'] == 1} + {loop $message $mk $mv} +
+
+
+ +
+
{$mv['uname']}帮他付了 ¥{$mv['price']}
+
{php echo $mk+1;}F
+
{php echo date('m-d H:i');}
+
+
{$mv['usay']}
+
+ {/loop} + {if !empty($message)} +
+ {/if} +
+ {if $rate_price > 0} +
+
+ + +
+
+ {/if} +
+{/if} + + {if $rate_price > 0}
+
+
付款金额
+
¥  + +
+
+
+ 立即支付{/if} + 我也要去玩 + 找小伙伴帮TA付款 +
+
+ + {template '_footer'} \ No newline at end of file diff --git a/src/addons/ewei_shopv2/plugin/pc/template/mobile/default/order/pay/peerpayshare.html b/src/addons/ewei_shopv2/plugin/pc/template/mobile/default/order/pay/peerpayshare.html index 093076899..a83276870 100644 --- a/src/addons/ewei_shopv2/plugin/pc/template/mobile/default/order/pay/peerpayshare.html +++ b/src/addons/ewei_shopv2/plugin/pc/template/mobile/default/order/pay/peerpayshare.html @@ -1,33 +1,33 @@ -{template '_header'} - - -
-
-
- - - -
{$member['nickname']}
-
-
- -
-
已完成:{$rate}%
-
还差:{$rate_price}元
-
-
- -
-
- 找小伙伴帮忙付款 -
-
- - - +{template '_header'} + + +
+
+
+ + + +
{$member['nickname']}
+
+
+ +
+
已完成:{$rate}%
+
还差:{$rate_price}元
+
+
+ +
+
+ 找小伙伴帮忙付款 +
+
+ + + {template '_footer'} \ No newline at end of file diff --git a/src/addons/ewei_shopv2/plugin/pc/template/mobile/default/order/pay/success.html b/src/addons/ewei_shopv2/plugin/pc/template/mobile/default/order/pay/success.html index 0fd785e20..032e248c2 100644 --- a/src/addons/ewei_shopv2/plugin/pc/template/mobile/default/order/pay/success.html +++ b/src/addons/ewei_shopv2/plugin/pc/template/mobile/default/order/pay/success.html @@ -1,259 +1,259 @@ -{template '_header'} - - -
- -
-
- -
-
- {if $_GPC['result']=='seckill_refund'} - 支付失败 - {else} - 支付成功 - {/if} - -
-
 
-
- -
- -
-
-
- {if $_GPC['result']=='seckill_refund'} - - {else} - {if !empty($address)}{/if} - - {if !empty($order['dispatchtype']) && empty($order['isverify'])}{/if} - - {if !empty($order['isverify'])}{/if} - - {if !empty($order['virtual'])}{/if} - - {if !empty($order['isvirtual']) && empty($order['virtual'])} - {if !empty($order['isvirtualsend'])} - - {else} - - {/if} - {/if} - {/if} - -
-
-
- {if $_GPC['result']=='seckill_refund'} - 订单支付失败 - {else} - {if $order['paytype']==3} - 订单提交支付 - {else} - 订单支付成功 - {/if} - {/if} - -
-
- - {if $_GPC['result']=='seckill_refund'} - 支付超时,秒杀失败,系统会自动退款,如果未收到退款,请联系客服! - {else} - - - {if !empty($address)}您的包裹整装待发{/if} - - {if !empty($order['dispatchtype']) && empty($order['isverify'])}您可以到您选择的自提点取货了{/if} - - {if !empty($order['isverify'])}您可以到适用门店去使用了{/if} - - {if !empty($order['virtual'])}您购买的商品已自动发货{/if} - - {if !empty($order['isvirtual']) && empty($order['virtual'])} - {if !empty($order['isvirtualsend'])} - 您购买的商品已自动发货 - {else} - 您已经支付成功 - - {if p('lottery')} - - - {/if} - - - {/if} - {/if} - {/if} - -
-
-
-
- - {if !empty($stores)} - -
-
-
- - 适用门店 -
{php echo count($stores)}
-
-
- {loop $stores $item} -
-
- -
-
-
{$item['storename']}
-
- {$item['realname']} {$item['mobile']} -
-
- {$item['address']} -
- -
-
- {if !empty($item['tel'])}{/if} - -
-
- {/loop} -
- - -
- {/if} - {if !empty($address)} - -
-
-
-
-
{$address['realname']} {$address['mobile']}
-
{$address['province']}{$address['city']}{$address['area']} {$address['address']}
-
-
-
- {/if} - - {if !empty($carrier) || !empty($store)} - -
- {if !empty($carrier)} -
-
-
-
{$carrier['carrier_realname']} {$carrier['carrier_mobile']}
-
-
- {/if} - - {if !empty($store)} -
-
- -
-
-
{$store['storename']}
-
- {$store['realname']} {$store['mobile']} -
-
- {$store['address']} -
-
-
- {if !empty($store['tel'])}{/if} - -
-
- {/if} -
- {/if} - - -
-
-
{if $order['paytype']==3}需到付{else}实付金额{/if}
-
-
¥{if empty($peerprice)}{php echo number_format($order['price'],2)}{else}{$peerprice['price']}{/if}
-
- {if $_GPC['result']!='seckill_refund'} - {if !empty($order['virtual']) || !empty($order['isvirtualsend'])} - -
- -
请到订单详情中查看详细信息
- -
- - {/if} - {/if} -
- -
- - -
- -
- - {if p('lottery')} - - {/if} - -
+{template '_header'} + + +
+ +
+
+ +
+
+ {if $_GPC['result']=='seckill_refund'} + 支付失败 + {else} + 支付成功 + {/if} + +
+
 
+
+ +
+ +
+
+
+ {if $_GPC['result']=='seckill_refund'} + + {else} + {if !empty($address)}{/if} + + {if !empty($order['dispatchtype']) && empty($order['isverify'])}{/if} + + {if !empty($order['isverify'])}{/if} + + {if !empty($order['virtual'])}{/if} + + {if !empty($order['isvirtual']) && empty($order['virtual'])} + {if !empty($order['isvirtualsend'])} + + {else} + + {/if} + {/if} + {/if} + +
+
+
+ {if $_GPC['result']=='seckill_refund'} + 订单支付失败 + {else} + {if $order['paytype']==3} + 订单提交支付 + {else} + 订单支付成功 + {/if} + {/if} + +
+
+ + {if $_GPC['result']=='seckill_refund'} + 支付超时,秒杀失败,系统会自动退款,如果未收到退款,请联系客服! + {else} + + + {if !empty($address)}您的包裹整装待发{/if} + + {if !empty($order['dispatchtype']) && empty($order['isverify'])}您可以到您选择的自提点取货了{/if} + + {if !empty($order['isverify'])}您可以到适用门店去使用了{/if} + + {if !empty($order['virtual'])}您购买的商品已自动发货{/if} + + {if !empty($order['isvirtual']) && empty($order['virtual'])} + {if !empty($order['isvirtualsend'])} + 您购买的商品已自动发货 + {else} + 您已经支付成功 + + {if p('lottery')} + + + {/if} + + + {/if} + {/if} + {/if} + +
+
+
+
+ + {if !empty($stores)} + +
+
+
+ + 适用门店 +
{php echo count($stores)}
+
+
+ {loop $stores $item} +
+
+ +
+
+
{$item['storename']}
+
+ {$item['realname']} {$item['mobile']} +
+
+ {$item['address']} +
+ +
+
+ {if !empty($item['tel'])}{/if} + +
+
+ {/loop} +
+ + +
+ {/if} + {if !empty($address)} + +
+
+
+
+
{$address['realname']} {$address['mobile']}
+
{$address['province']}{$address['city']}{$address['area']} {$address['address']}
+
+
+
+ {/if} + + {if !empty($carrier) || !empty($store)} + +
+ {if !empty($carrier)} +
+
+
+
{$carrier['carrier_realname']} {$carrier['carrier_mobile']}
+
+
+ {/if} + + {if !empty($store)} +
+
+ +
+
+
{$store['storename']}
+
+ {$store['realname']} {$store['mobile']} +
+
+ {$store['address']} +
+
+
+ {if !empty($store['tel'])}{/if} + +
+
+ {/if} +
+ {/if} + + +
+
+
{if $order['paytype']==3}需到付{else}实付金额{/if}
+
+
¥{if empty($peerprice)}{php echo number_format($order['price'],2)}{else}{$peerprice['price']}{/if}
+
+ {if $_GPC['result']!='seckill_refund'} + {if !empty($order['virtual']) || !empty($order['isvirtualsend'])} + +
+ +
请到订单详情中查看详细信息
+ +
+ + {/if} + {/if} +
+ +
+ + +
+ +
+ + {if p('lottery')} + + {/if} + +
{template '_footer'} \ No newline at end of file diff --git a/src/addons/ewei_shopv2/plugin/pc/template/mobile/default/order/pay/wechat_jie.html b/src/addons/ewei_shopv2/plugin/pc/template/mobile/default/order/pay/wechat_jie.html index f6ccc935e..b18637717 100644 --- a/src/addons/ewei_shopv2/plugin/pc/template/mobile/default/order/pay/wechat_jie.html +++ b/src/addons/ewei_shopv2/plugin/pc/template/mobile/default/order/pay/wechat_jie.html @@ -1,22 +1,22 @@ -