fix: 解决访问没有埋点的问题

This commit is contained in:
2025-11-10 17:28:52 +08:00
parent e440631275
commit 57dea2ca87
38 changed files with 4172 additions and 1624 deletions

2
.gitignore vendored
View File

@@ -1,2 +1,4 @@
src/runtime src/runtime
src/upload src/upload
ftp-src
update.zip

View File

@@ -28,6 +28,7 @@ services:
- "${PHP_FPM_PORT:-9000}:9000" # PHP-FPM - "${PHP_FPM_PORT:-9000}:9000" # PHP-FPM
- "${XDEBUG_POST:-9003}:9003" # Xdebug - "${XDEBUG_POST:-9003}:9003" # Xdebug
volumes: volumes:
- ./:/var/www/all_source
- ./src:/var/www/html - ./src:/var/www/html
# 更新下载源列表以加速apt-get # 更新下载源列表以加速apt-get
- ./docker/debian/sources.list:/etc/apt/sources.list:ro - ./docker/debian/sources.list:/etc/apt/sources.list:ro

View File

@@ -77,5 +77,19 @@ RUN echo "zend_extension=xdebug.so" > /usr/local/etc/php/conf.d/xdebug.ini
# 暴露端口 # 暴露端口
EXPOSE 9000 9003 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 # 启动Supervisor
CMD ["/usr/bin/supervisord", "-c", "/etc/supervisor/conf.d/supervisord.conf"] CMD ["/usr/bin/supervisord", "-c", "/etc/supervisor/conf.d/supervisord.conf"]

View File

@@ -4,6 +4,22 @@
本项目使用的 是 `yunwuxin/think-cron` 包,该包提供了丰富的功能,包括任务调度、任务执行、任务状态管理等。该包支持多种任务类型,如固定任务、循环任务、一次性任务等,同时支持多种执行周期,如分钟、小时、天、周、月等。 本项目使用的 是 `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 &
#######################################
```
## 计划任务管理 ## 计划任务管理

37
docs/deploy.md Normal file
View File

@@ -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
```

533
scripts/deploy.php Normal file
View File

@@ -0,0 +1,533 @@
<?php
// deploy.php
#!/usr/bin/env php
/**
* 文件自动部署脚本
* 功能比较ZIP包与目标目录的差异自动部署文件
* PHP 7.4 兼容版本
*/
declare(strict_types=1);
class AutoDeployer
{
private string $zipPath;
private string $targetDir;
private array $log = [];
private bool $dryRun = false;
private bool $verbose = false;
private bool $backup = false;
public function __construct(string $zipPath, string $targetDir, array $options = [])
{
$this->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 <path> ZIP文件路径 (必需)\n";
echo " --target <path> 目标目录路径 (必需)\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);
}
?>

679
scripts/enhanced_deploy.php Normal file
View File

@@ -0,0 +1,679 @@
<?php
// enhanced_deploy.php
#!/usr/bin/env php
/**
* 增强版自动部署脚本
* 支持根据补丁ZIP文件删除不需要的文件
* PHP 7.4 兼容版本
*/
declare(strict_types=1);
class EnhancedAutoDeployer
{
private string $zipPath;
private string $targetDir;
private array $config;
private array $log = [];
private bool $dryRun;
private bool $verbose;
private array $deleteList = [];
public function __construct(string $zipPath, string $targetDir, array $config = [])
{
$this->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 <path> ZIP文件路径 (必需)\n";
echo " --target <path> 目标目录路径 (必需)\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);
}
?>

View File

@@ -0,0 +1,801 @@
<?php
// enhanced_package_generator.php
#!/usr/bin/env php
/**
* 部署包生成器
* 生成部署包, 只包含新增、修改的文件
*/
class DeployPackageGenerator
{
private string $sourceDir; // 目录A新版本
private string $targetDir; // 目录B旧版本
private string $outputPath; // 输出ZIP文件路径
private array $config; // 配置选项
private array $log = []; // 日志记录
private array $comparisonResult = []; // 比较结果
public function __construct(string $sourceDir, string $targetDir, string $outputPath, array $config = [])
{
$this->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 <path> 源目录(新版本)\n";
echo " --target, -t <path> 目标目录(旧版本)\n";
echo " --output, -o <path> 输出ZIP文件路径\n";
echo " --config, -c <path> 配置文件路径\n";
echo " --compare <method> 比较方法 (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);
}
?>

View File

@@ -0,0 +1,72 @@
#!/usr/bin/env php
// security_config.php
<?php
/**
* 安全策略配置文件
* 用于控制删除操作的安全性
*/
return [
// 启用/禁用删除功能
'delete_enabled' => 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'
];
?>

View File

@@ -13,6 +13,8 @@ PASSWORD = shop_mallnew
HOSTPORT = 3306 HOSTPORT = 3306
CHARSET = utf8 CHARSET = utf8
DEBUG = true DEBUG = true
[RRDATABASE]
HOSTNAME = 192.168.2.64
[redis] [redis]
HOST = newshop_redis HOST = newshop_redis
PORT = 6379 PORT = 6379

View File

@@ -1,4 +1,4 @@
/* /*
* Swipe 2.0 * Swipe 2.0

View File

@@ -1885,8 +1885,10 @@ function paramFilter($param)
* @param string $level 日志级别 debug、info、warning、error * @param string $level 日志级别 debug、info、warning、error
* @param string $file 日志文件名(不含路径) * @param string $file 日志文件名(不含路径)
*/ */
function log_write(string $message, string $level = 'info', string $filename = '', int $maxFileSize = 5242880): void function log_write(string $message, string $level = 'info', string $filename = ''): void
{ {
// 日志文件最大大小(字节)
$maxFileSize = 10485760; // 10MB
$callerInfo = CallerInfo::getCallerInfo(1); $callerInfo = CallerInfo::getCallerInfo(1);
// 格式化日志内容 // 格式化日志内容
@@ -1911,41 +1913,36 @@ function log_write(string $message, string $level = 'info', string $filename = '
$filename = date('Y-m-d') . '.log'; $filename = date('Y-m-d') . '.log';
} }
// 获取基础日志文件名(不含扩展名)和扩展名
$fileInfo = pathinfo($filename);
$baseName = $fileInfo['filename'];
$extension = isset($fileInfo['extension']) ? '.' . $fileInfo['extension'] : '.log';
// 日志文件路径 // 日志文件路径
$logFile = $logPath . $filename; $logFile = $logPath . $filename;
// 检查文件大小并处理日志分割 // 检查文件大小,如果超过限制则直接重新写入(不创建新文件)
if (file_exists($logFile) && filesize($logFile) >= $maxFileSize) { $flags = FILE_APPEND | LOCK_EX;
// 查找现有的带序号的日志文件,确定下一个序号 if (file_exists($logFile) && is_readable($logFile)) {
$nextIndex = 1; try {
$pattern = $logPath . $baseName . '-???' . $extension; // 方法1使用 filesize 函数(基本方法)
$matchingFiles = glob($pattern); $fileSize = filesize($logFile);
if (!empty($matchingFiles)) { // 如果 filesize 失败或返回 false尝试使用 stat 函数
// 提取最大序号 if ($fileSize === false) {
$maxIndex = 0; $stat = stat($logFile);
foreach ($matchingFiles as $file) { if ($stat && isset($stat['size'])) {
$fileBase = pathinfo($file, PATHINFO_FILENAME); $fileSize = $stat['size'];
if (preg_match('/-([0-9]{3})$/', $fileBase, $matches)) {
$index = (int)$matches[1];
$maxIndex = max($maxIndex, $index);
} }
} }
$nextIndex = $maxIndex + 1;
}
// 生成带序号的新文件名 // 如果获取到的文件大小超过限制,使用覆盖模式
$newFilename = $baseName . '-' . sprintf('%03d', $nextIndex) . $extension; if (isset($fileSize) && $fileSize >= $maxFileSize) {
$logFile = $logPath . $newFilename; $flags = 0 | LOCK_EX;
}
} catch (Exception $e) {
// 如果发生异常,为了安全起见,继续使用追加模式
// 可以根据需要记录异常信息
}
} }
// 写入文件(追加模式) // 写入文件
file_put_contents($logFile, $content, FILE_APPEND | LOCK_EX); file_put_contents($logFile, $content, $flags);
} }

View File

@@ -30,25 +30,26 @@ class StatShop extends BaseModel
*/ */
public function addShopStat($data) public function addShopStat($data)
{ {
log_write('店铺按天新统计数据开始添加', 'debug'); $site_id = $data['site_id'] ?? 1;
log_write('店铺按天新统计数据开始添加site_id'.$site_id, 'debug');
try{ try{
$carbon = Carbon::now(); $carbon = Carbon::now();
$dir = __UPLOAD__.'/stat/stat_shop/'; $dir = __UPLOAD__.'/stat/stat_shop/';
if (!is_dir($dir) && !mkdir($dir, 0777, true) && !is_dir($dir)) { if (!is_dir($dir) && !mkdir($dir, 0777, true) && !is_dir($dir)) {
return $this->error(sprintf('Directory "%s" was not created', $dir)); return $this->error(sprintf('Directory "%s" was not created', $dir));
} }
$filename = $dir.$carbon->year.'_'.$carbon->month.'_'.$carbon->day.'_'.$carbon->second.'_'.unique_random().'.json'; $filename = $dir.$carbon->year.'_'.$carbon->month.'_'.$carbon->day.'_'.$carbon->second.'_s'.$site_id.'_'.unique_random().'.json';
$stat_extend = new Stat($filename, 'stat_shop',$data['site_id']); $stat_extend = new Stat($filename, 'stat_shop', $site_id);
$stat_extend->handleData($data);//写入文件 $stat_extend->handleData($data);//写入文件
//增加当天时统计 //增加当天时统计
$this->addShopHourStat($data, $carbon); $this->addShopHourStat($data, $carbon);
}catch (\Exception $e){ }catch (\Exception $e){
log_write('店铺按天新统计数据添加失败:' . $e->getMessage(), 'error'); log_write('店铺按天新统计数据添加失败site_id'.$site_id . ',错误信息' . $e->getMessage(), 'error');
return $this->error('店铺按天新统计数据添加失败'); return $this->error('店铺按天新统计数据添加失败');
} }
log_write('店铺按天新统计数据已添加', 'debug'); log_write('店铺按天新统计数据已添加site_id'.$site_id, 'debug');
return $this->success(); return $this->success();
} }
@@ -100,6 +101,7 @@ class StatShop extends BaseModel
Log::write(time().'stat_shop_'.json_encode($data_array)); Log::write(time().'stat_shop_'.json_encode($data_array));
$system_stat = new \app\model\system\Stat(); $system_stat = new \app\model\system\Stat();
foreach ($data_array as $json_k => $json_v){ foreach ($data_array as $json_k => $json_v){
$json_v = $this->removeExtraFields($json_v);
$system_stat->addStatShopModel($json_v); $system_stat->addStatShopModel($json_v);
} }
log_write('店铺按天统计数据处理成功', 'debug'); log_write('店铺按天统计数据处理成功', 'debug');
@@ -118,18 +120,19 @@ class StatShop extends BaseModel
*/ */
public function addShopHourStat($data, $carbon) public function addShopHourStat($data, $carbon)
{ {
log_write('店铺按小时新统计数据开始添加', 'debug'); $site_id = $data['site_id'] ?? 1;
log_write('店铺按小时新统计数据开始添加site_id'.$site_id, 'debug');
try{ try{
$dir = __UPLOAD__.'/stat/stat_shop_hour/'; $dir = __UPLOAD__.'/stat/stat_shop_hour/';
if (!is_dir($dir) && !mkdir($dir, 0777, true) && !is_dir($dir)) { if (!is_dir($dir) && !mkdir($dir, 0777, true) && !is_dir($dir)) {
return $this->error(sprintf('Directory "%s" was not created', $dir)); return $this->error(sprintf('Directory "%s" was not created', $dir));
} }
$filename = $dir.$carbon->year.'_'.$carbon->month.'_'.$carbon->day.'_'.$carbon->hour.'_'.$carbon->second.'_'.unique_random().'.json'; $filename = $dir.$carbon->year.'_'.$carbon->month.'_'.$carbon->day.'_'.$carbon->hour.'_'.$carbon->second.'_s'.$site_id.'_'.unique_random().'.json';
$stat_extend = new Stat($filename, 'stat_shop_hour',$data['site_id']); $stat_extend = new Stat($filename, 'stat_shop_hour', $site_id);
$stat_extend->handleData($data);//写入文件 $stat_extend->handleData($data);//写入文件
log_write('店铺按小时新统计数据已添加', 'debug'); log_write('店铺按小时新统计数据已添加site_id'.$site_id, 'debug');
}catch (\Exception $e){ }catch (\Exception $e){
log_write('店铺按小时新统计数据添加失败:' . $e->getMessage(), 'error'); log_write('店铺按小时新统计数据添加失败site_id'.$site_id . ',错误信息' . $e->getMessage(), 'error');
return $this->error('店铺按小时新统计数据添加失败'); return $this->error('店铺按小时新统计数据添加失败');
} }
return $this->success(); return $this->success();
@@ -178,6 +181,7 @@ class StatShop extends BaseModel
Log::write(time().'stat_shop_hour_'.json_encode($data_array)); Log::write(time().'stat_shop_hour_'.json_encode($data_array));
$system_stat = new \app\model\system\Stat(); $system_stat = new \app\model\system\Stat();
foreach ($json_array as $json_k => $json_v){ foreach ($json_array as $json_k => $json_v){
$json_v = $this->removeExtraFields($json_v);
$system_stat->addStatShopHourModel($json_v); $system_stat->addStatShopHourModel($json_v);
} }
log_write('系统计划任务执行店铺按小时统计完成'); log_write('系统计划任务执行店铺按小时统计完成');
@@ -186,6 +190,19 @@ class StatShop extends BaseModel
} }
} }
/**
* 将不需要的字段从json_v中移除并返回处理后的数组
* @param array $json_v 原始json数组
* @return array 处理后的json数组
*/
public function removeExtraFields($json_v)
{
try {
// 从json_v中移除多余字段
unset($json_v['store_id']); // 移除store_id字段
} catch (\Exception $e) {}
return $json_v;
}
public function scanFile($path) { public function scanFile($path) {
$result = []; $result = [];

View File

@@ -31,13 +31,16 @@ class StatStore extends BaseModel
*/ */
public function addStoreStat($data) public function addStoreStat($data)
{ {
$site_id = $data['site_id'] ?? 1;
log_write('门店按天新统计数据开始添加site_id'.$site_id, 'debug');
try {
$dir = __UPLOAD__.'/stat/stat_store/'; $dir = __UPLOAD__.'/stat/stat_store/';
if (!is_dir($dir) && !mkdir($dir, 0777, true) && !is_dir($dir)) { if (!is_dir($dir) && !mkdir($dir, 0777, true) && !is_dir($dir)) {
return $this->error(sprintf('Directory "%s" was not created', $dir)); return $this->error(sprintf('Directory "%s" was not created', $dir));
} }
$carbon = Carbon::now(); $carbon = Carbon::now();
$filename = $dir.$carbon->year.'_'.$carbon->month.'_'.$carbon->day.'_'.$carbon->second.'_'.unique_random().'.json'; $filename = $dir.$carbon->year.'_'.$carbon->month.'_'.$carbon->day.'_s'.$site_id.'_'.$carbon->second.'_'.unique_random().'.json';
$stat_extend = new Stat($filename, 'stat_store'); $stat_extend = new Stat($filename, 'stat_store', $site_id);
$stat_extend->handleData($data);//写入文件 $stat_extend->handleData($data);//写入文件
//增加当天时统计 //增加当天时统计
@@ -50,6 +53,13 @@ class StatStore extends BaseModel
} }
$stat_shop_model = new StatShop(); $stat_shop_model = new StatShop();
$stat_shop_model->addShopStat($stat_shop); $stat_shop_model->addShopStat($stat_shop);
} catch (\Exception $e) {
log_write('门店按天新统计数据添加失败site_id'.$site_id . ',错误信息:' . $e->getMessage(), 'error');
return $this->error('门店按天新统计数据添加失败');
}
log_write('门店按天新统计数据已添加site_id'.$site_id, 'debug');
return $this->success(); return $this->success();
} }
@@ -59,11 +69,18 @@ class StatStore extends BaseModel
*/ */
public function cronStatStore() public function cronStatStore()
{ {
log_write('门店按天统计数据开始处理', 'debug');
$path = __UPLOAD__.'/stat/stat_store'; $path = __UPLOAD__.'/stat/stat_store';
if(!is_dir($path)) return; if(!is_dir($path)) {
log_write('门店按天统计数据处理失败:目录不存在:'.$path, 'error');
return;
}
$result = $this->scanFile($path); $result = $this->scanFile($path);
if(empty($result)) return; if(empty($result)) {
log_write('门店按天统计数据处理失败:目录下无文件:'.$path, 'error');
return;
}
try { try {
$json_array = []; $json_array = [];
@@ -95,8 +112,9 @@ class StatStore extends BaseModel
foreach ($json_array as $json_k => $json_v){ foreach ($json_array as $json_k => $json_v){
$store_stat->addStatStoreModel($json_v); $store_stat->addStatStoreModel($json_v);
} }
log_write('门店按天统计数据处理成功', 'debug');
} catch (\Exception $e) { } catch (\Exception $e) {
log_write('门店按天统计数据处理失败:'.$e->getMessage(), 'error');
} }
} }
@@ -108,13 +126,22 @@ class StatStore extends BaseModel
*/ */
public function addStoreHourStat($data, $carbon) public function addStoreHourStat($data, $carbon)
{ {
$site_id = $data['site_id'] ?? 1;
log_write('门店按小时新统计数据开始添加site_id'.$site_id, 'debug'); //增加site_id
try {
$dir = __UPLOAD__.'/stat/stat_store_hour/'; $dir = __UPLOAD__.'/stat/stat_store_hour/';
if (!is_dir($dir) && !mkdir($dir, 0777, true) && !is_dir($dir)) { if (!is_dir($dir) && !mkdir($dir, 0777, true) && !is_dir($dir)) {
return $this->error(sprintf('Directory "%s" was not created', $dir)); return $this->error(sprintf('Directory "%s" was not created', $dir));
} }
$filename = $dir.$carbon->year.'_'.$carbon->month.'_'.$carbon->day.'_'.$carbon->hour.'_'.$carbon->second.'_'.unique_random().'.json'; $filename = $dir.$carbon->year.'_'.$carbon->month.'_'.$carbon->day.'_s'.$site_id.'_'.$carbon->hour.'_'.$carbon->second.'_'.unique_random().'.json';
$stat_extend = new Stat($filename, 'stat_store_hour'); $stat_extend = new Stat($filename, 'stat_store_hour', $site_id);
$stat_extend->handleData($data);//写入文件 $stat_extend->handleData($data);//写入文件
log_write('门店按小时新统计数据已添加site_id'.$site_id, 'debug'); //增加site_id
} catch (\Exception $e) {
log_write('门店按小时新统计数据添加失败site_id'.$site_id . ',错误信息:' . $e->getMessage(), 'error'); //增加site_id
return $this->error('门店按小时新统计数据添加失败');
}
return $this->success(); return $this->success();
} }
@@ -123,11 +150,18 @@ class StatStore extends BaseModel
*/ */
public function cronStatStoreHour() public function cronStatStoreHour()
{ {
log_write('门店按小时统计数据开始处理', 'debug');
$path = __UPLOAD__.'/stat/stat_store_hour'; $path = __UPLOAD__.'/stat/stat_store_hour';
if(!is_dir($path)) return; if(!is_dir($path)) {
log_write('门店按小时统计数据处理失败:目录不存在:'.$path, 'error');
return;
}
$result = $this->scanFile($path); $result = $this->scanFile($path);
if(empty($result)) return; if(empty($result)) {
log_write('门店按小时统计数据处理失败:目录下无文件:'.$path, 'error');
return;
}
try { try {
$json_array = []; $json_array = [];
@@ -154,13 +188,14 @@ class StatStore extends BaseModel
$data_array[$k] = $json_v; $data_array[$k] = $json_v;
} }
} }
Log::write(time().'stat_store_hour'.json_encode($data_array)); Log::write(time().'stat_store_hour_'.json_encode($data_array));
$store_stat = new \app\model\store\Stat(); $store_stat = new \app\model\store\Stat();
foreach ($json_array as $json_k => $json_v){ foreach ($json_array as $json_k => $json_v){
$store_stat->addStatStoreHourModel($json_v); $store_stat->addStatStoreHourModel($json_v);
} }
log_write('门店按小时统计数据处理成功', 'debug');
} catch (\Exception $e) { } catch (\Exception $e) {
log_write('门店按小时统计数据处理失败:'.$e->getMessage(), 'error');
} }
} }

View File

@@ -22,7 +22,7 @@
<link rel="stylesheet" type="text/css" href="SHOP_CSS/style2/common.css?v={$version}" /> <link rel="stylesheet" type="text/css" href="SHOP_CSS/style2/common.css?v={$version}" />
<script src="__STATIC__/js/jquery-3.1.1.js"></script> <script src="__STATIC__/js/jquery-3.1.1.js"></script>
<script src="__STATIC__/js/jquery.cookie.js"></script> <script src="__STATIC__/js/jquery.cookie.js"></script>
<script src="__STATIC__/js/deep-proxy-1.0.js?t={$version}5"></script> <script src="__STATIC__/js/deep-proxy-1.0.js?t={$version}"></script>
<script src="__STATIC__/ext/layui/layui.js"></script> <script src="__STATIC__/ext/layui/layui.js"></script>
<script> <script>
layui.use(['layer', 'upload', 'element'], function() {}); layui.use(['layer', 'upload', 'element'], function() {});

View File

@@ -11,7 +11,7 @@ return [
// 应用的命名空间 // 应用的命名空间
'app_namespace' => '', 'app_namespace' => '',
// 是否启用路由 // 是否启用路由
'with_route' => false, 'with_route' => true,
//url模式 //url模式
'url_model' => 3, 'url_model' => 3,
// 是否启用事件 // 是否启用事件

View File

@@ -2509,17 +2509,6 @@ return [
], ],
], ],
], ],
[
'name'=> 'CONFIG_AI_PLATFORM',
'title' => 'AI智能客服配置',
'url'=> 'shop/config/ai',
'is_show' => 1,
'is_control' => 1,
'is_icon' => 0,
'picture' => '',
'picture_selected' => '',
'sort' => 1,
],
[ [
'name' => 'CONFIG_BASE_ORDER', 'name' => 'CONFIG_BASE_ORDER',
'title' => '交易设置', 'title' => '交易设置',

View File

@@ -43,6 +43,7 @@ class InitRoute extends Service {
$data = str_replace($time, '', $decrypt_data); $data = str_replace($time, '', $decrypt_data);
$json_data = decrypt($data, $key); $json_data = decrypt($data, $key);
$array = json_decode($json_data, true); $array = json_decode($json_data, true);
return $array;
} }
private function addonsAuth() { private function addonsAuth() {
$cache = Cache::get('auth_addon'); $cache = Cache::get('auth_addon');

352
src/extend/debug.txt Normal file
View File

@@ -0,0 +1,352 @@
460
460
460
460
460
460
460
460
460
460
460
460
460
460
460
460
460
460
460
460
460
460
460
460
460
460
460
460
460
460
460
460
460
460
460
460
460
460
460
460
460
460
460
460
460
460
460
460
460
460
460
460
460
460
460
460
460
460
460
460
460
460
460
460
460
460
460
460
460
460
460
460
460
460
460
460
460
460
460
460
460
460
460
460
1
1
1
1
1
1
1
1
1
1
1
1
1
1
1
1
1
1
1
1
1
1
1
1
1
1
1
1
460
460
460
460
1
1
1
1
460
460
460
460
460
460
460
460
1
1
1
1
1
1
1
1
460
460
460
460
1
1
1
1
460
460
460
460
1
1
1
1
460
460
460
460
1
1
1
1
460
460
460
460
460
460
1
1
1
1
1
1
460
460
460
460
1
1
1
1
460
460
460
460
460
460
1
1
1
1
1
1
460
460
460
460
1
1
1
1
460
460
460
460
460
460
460
460
460
460
460
460
460
460
1
1
1
1
1
1
1
1
1
1
1
1
1
1
460
460
460
460
460
460
1
1
1
1
1
1
460
460
460
460
1
1
1
1
460
460
460
460
460
460
460
460
460
460
460
460
460
460
460
460
460
460
460
460
460
460
460
460
460
460
460
460
460
460
460
460
460
460
460
460
460
460
460
460
460
460
460
460
1
1
1
1
1
1
1
1
1
1
1
1
1
1
1
1
1
1
1
1
1
1
1
1
1
1
1
1
1
1
1
1
1
1
1
1
1
1
1
1
1
1
1
1
460
460
460
460
1
1
1
1
460
460
460
460
1
1
1
1

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 115 KiB