remove admin-dashboard
This commit is contained in:
Binary file not shown.
File diff suppressed because it is too large
Load Diff
@@ -1,39 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "cocos-ecs-hotupdate-admin",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"description": "Cocos ECS Extension Hot Update Management Backend",
|
|
||||||
"main": "dist/server.js",
|
|
||||||
"scripts": {
|
|
||||||
"dev": "ts-node-dev --respawn --transpile-only src/server.ts",
|
|
||||||
"build": "tsc",
|
|
||||||
"start": "node dist/server.js",
|
|
||||||
"migrate": "ts-node src/database/migrate.ts",
|
|
||||||
"simple": "node server.js"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"express": "^4.18.2",
|
|
||||||
"multer": "^1.4.5-lts.1",
|
|
||||||
"sqlite3": "^5.1.6",
|
|
||||||
"cors": "^2.8.5",
|
|
||||||
"bcryptjs": "^2.4.3",
|
|
||||||
"jsonwebtoken": "^9.0.2",
|
|
||||||
"adm-zip": "^0.5.10",
|
|
||||||
"crypto": "^1.0.1",
|
|
||||||
"fs-extra": "^11.1.1",
|
|
||||||
"moment": "^2.29.4",
|
|
||||||
"node-cron": "^3.0.3"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@types/express": "^4.17.17",
|
|
||||||
"@types/multer": "^1.4.7",
|
|
||||||
"@types/node": "^20.5.0",
|
|
||||||
"@types/cors": "^2.8.13",
|
|
||||||
"@types/bcryptjs": "^2.4.2",
|
|
||||||
"@types/jsonwebtoken": "^9.0.2",
|
|
||||||
"@types/adm-zip": "^0.5.0",
|
|
||||||
"@types/fs-extra": "^11.0.1",
|
|
||||||
"@types/node-cron": "^3.0.8",
|
|
||||||
"typescript": "^5.1.6",
|
|
||||||
"ts-node-dev": "^2.0.0"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,686 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="zh-CN">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<title>Cocos ECS Extension - 热更新管理后台</title>
|
|
||||||
<link href="https://cdn.jsdelivr.net/npm/element-plus@2.4.4/dist/index.css" rel="stylesheet">
|
|
||||||
<style>
|
|
||||||
body { margin: 0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; }
|
|
||||||
.header { background: #409eff; color: white; padding: 16px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
|
|
||||||
.container { max-width: 1200px; margin: 20px auto; padding: 0 20px; }
|
|
||||||
.card { background: white; border-radius: 8px; box-shadow: 0 2px 8px rgba(0,0,0,0.1); margin-bottom: 20px; }
|
|
||||||
.upload-area {
|
|
||||||
border: 2px dashed #dcdfe6;
|
|
||||||
border-radius: 8px;
|
|
||||||
padding: 40px;
|
|
||||||
text-align: center;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all 0.3s;
|
|
||||||
}
|
|
||||||
.upload-area:hover { border-color: #409eff; background: #f0f8ff; }
|
|
||||||
.version-list { max-height: 400px; overflow-y: auto; }
|
|
||||||
.version-item {
|
|
||||||
padding: 16px;
|
|
||||||
border-bottom: 1px solid #ebeef5;
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
.version-info h4 { margin: 0 0 8px 0; color: #303133; }
|
|
||||||
.version-info p { margin: 0; color: #909399; font-size: 14px; }
|
|
||||||
.status-tag {
|
|
||||||
padding: 4px 12px;
|
|
||||||
border-radius: 16px;
|
|
||||||
font-size: 12px;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
.status-active { background: #f0f9ff; color: #409eff; }
|
|
||||||
.status-draft { background: #fdf6ec; color: #e6a23c; }
|
|
||||||
.stats-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 20px; }
|
|
||||||
.stat-card {
|
|
||||||
padding: 24px;
|
|
||||||
text-align: center;
|
|
||||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
||||||
color: white;
|
|
||||||
border-radius: 12px;
|
|
||||||
}
|
|
||||||
.stat-number { font-size: 32px; font-weight: bold; margin-bottom: 8px; }
|
|
||||||
.stat-label { font-size: 14px; opacity: 0.9; }
|
|
||||||
.btn {
|
|
||||||
padding: 8px 16px;
|
|
||||||
border: none;
|
|
||||||
border-radius: 4px;
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 14px;
|
|
||||||
transition: all 0.3s;
|
|
||||||
}
|
|
||||||
.btn-primary { background: #409eff; color: white; }
|
|
||||||
.btn-primary:hover { background: #337ecc; }
|
|
||||||
.btn-danger { background: #f56c6c; color: white; }
|
|
||||||
.btn-danger:hover { background: #dd6161; }
|
|
||||||
.btn-success { background: #67c23a; color: white; }
|
|
||||||
.btn-success:hover { background: #5daf34; }
|
|
||||||
.plugin-card:hover { border-color: #409eff; background: #f0f8ff; }
|
|
||||||
.plugin-active { border-color: #409eff !important; background: #f0f8ff !important; }
|
|
||||||
.filtered-out { opacity: 0.3; }
|
|
||||||
|
|
||||||
/* 模态框样式 */
|
|
||||||
.modal-overlay {
|
|
||||||
position: fixed;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
background: rgba(0, 0, 0, 0.5);
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
z-index: 1000;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-content {
|
|
||||||
background: white;
|
|
||||||
border-radius: 8px;
|
|
||||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
|
||||||
width: 600px;
|
|
||||||
max-width: 90%;
|
|
||||||
max-height: 90%;
|
|
||||||
overflow-y: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-header {
|
|
||||||
padding: 20px 24px;
|
|
||||||
border-bottom: 1px solid #e4e7ed;
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-header h3 {
|
|
||||||
margin: 0;
|
|
||||||
color: #303133;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-close {
|
|
||||||
background: none;
|
|
||||||
border: none;
|
|
||||||
font-size: 24px;
|
|
||||||
cursor: pointer;
|
|
||||||
color: #909399;
|
|
||||||
padding: 0;
|
|
||||||
width: 32px;
|
|
||||||
height: 32px;
|
|
||||||
border-radius: 50%;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-close:hover {
|
|
||||||
background: #f5f7fa;
|
|
||||||
color: #606266;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-body {
|
|
||||||
padding: 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-footer {
|
|
||||||
padding: 20px 24px;
|
|
||||||
border-top: 1px solid #e4e7ed;
|
|
||||||
display: flex;
|
|
||||||
justify-content: flex-end;
|
|
||||||
gap: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-group {
|
|
||||||
margin-bottom: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-group label {
|
|
||||||
display: block;
|
|
||||||
margin-bottom: 8px;
|
|
||||||
color: #606266;
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-group small {
|
|
||||||
display: block;
|
|
||||||
margin-top: 4px;
|
|
||||||
color: #909399;
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn.btn-default {
|
|
||||||
background: #fff;
|
|
||||||
color: #606266;
|
|
||||||
border: 1px solid #dcdfe6;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn.btn-default:hover {
|
|
||||||
color: #409eff;
|
|
||||||
border-color: #c6e2ff;
|
|
||||||
background: #ecf5ff;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div id="app">
|
|
||||||
<div class="header">
|
|
||||||
<h1 style="margin: 0; font-size: 24px;">🚀 Cocos ECS Extension - 热更新管理后台</h1>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="container">
|
|
||||||
<!-- 插件选择器 -->
|
|
||||||
<div class="card">
|
|
||||||
<div style="padding: 20px;">
|
|
||||||
<h2>🔌 插件管理</h2>
|
|
||||||
<div style="display: flex; gap: 20px; flex-wrap: wrap;">
|
|
||||||
<div
|
|
||||||
v-for="plugin in plugins"
|
|
||||||
:key="plugin.id"
|
|
||||||
@click="selectPlugin(plugin.id)"
|
|
||||||
:class="'plugin-card ' + (currentPlugin === plugin.id ? 'plugin-active' : '')"
|
|
||||||
style="flex: 1; min-width: 250px; padding: 20px; border: 2px solid #e4e7ed; border-radius: 8px; cursor: pointer; transition: all 0.3s;"
|
|
||||||
>
|
|
||||||
<div style="display: flex; align-items: center; gap: 12px;">
|
|
||||||
<span style="font-size: 32px;">{{ plugin.icon }}</span>
|
|
||||||
<div style="flex: 1;">
|
|
||||||
<h3 style="margin: 0; color: #303133;">{{ plugin.displayName }}</h3>
|
|
||||||
<p style="margin: 4px 0 0 0; color: #909399; font-size: 14px;">{{ plugin.description || '暂无描述' }}</p>
|
|
||||||
<div style="margin: 8px 0 0 0; display: flex; gap: 16px; font-size: 12px;">
|
|
||||||
<span style="color: #67c23a;">{{ plugin.status === 'active' ? '✅ 活跃' : '⏸️ 暂停' }}</span>
|
|
||||||
<span style="color: #409eff;">📦 版本: {{ plugin.versionCount || 0 }}</span>
|
|
||||||
<span style="color: #e6a23c;">📥 下载: {{ plugin.totalDownloads || 0 }}</span>
|
|
||||||
</div>
|
|
||||||
<p style="margin: 4px 0 0 0; color: #c0c4cc; font-size: 12px;">{{ plugin.author || '未知作者' }}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 统计数据 -->
|
|
||||||
<div class="card">
|
|
||||||
<div style="padding: 20px;">
|
|
||||||
<h2>📊 系统统计</h2>
|
|
||||||
<div class="stats-grid">
|
|
||||||
<div class="stat-card">
|
|
||||||
<div class="stat-number">{{ stats.totalVersions }}</div>
|
|
||||||
<div class="stat-label">总版本数</div>
|
|
||||||
</div>
|
|
||||||
<div class="stat-card">
|
|
||||||
<div class="stat-number">{{ stats.activeUsers }}</div>
|
|
||||||
<div class="stat-label">活跃用户</div>
|
|
||||||
</div>
|
|
||||||
<div class="stat-card">
|
|
||||||
<div class="stat-number">{{ stats.todayUpdates }}</div>
|
|
||||||
<div class="stat-label">今日更新</div>
|
|
||||||
</div>
|
|
||||||
<div class="stat-card">
|
|
||||||
<div class="stat-number">{{ stats.successRate }}%</div>
|
|
||||||
<div class="stat-label">成功率</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 上传新版本 -->
|
|
||||||
<div class="card">
|
|
||||||
<div style="padding: 20px;">
|
|
||||||
<h2>📦 上传新版本</h2>
|
|
||||||
<div class="upload-area" @click="selectFile">
|
|
||||||
<div v-if="!uploading">
|
|
||||||
<i style="font-size: 48px; color: #c0c4cc;">📁</i>
|
|
||||||
<p style="margin: 16px 0 0 0; color: #606266;">点击或拖拽文件到此处上传插件包</p>
|
|
||||||
<p style="margin: 8px 0 0 0; color: #909399; font-size: 14px;">支持 .zip 格式,最大 100MB</p>
|
|
||||||
</div>
|
|
||||||
<div v-else>
|
|
||||||
<i style="font-size: 48px; color: #409eff;">⏳</i>
|
|
||||||
<p style="margin: 16px 0 0 0; color: #409eff;">上传中... {{ uploadProgress }}%</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<input type="file" ref="fileInput" @change="handleFileSelect" accept=".zip" style="display: none;">
|
|
||||||
|
|
||||||
<div v-if="selectedFile" style="margin-top: 20px; padding: 16px; background: #f5f7fa; border-radius: 4px;">
|
|
||||||
<h4 style="margin: 0 0 12px 0;">版本信息</h4>
|
|
||||||
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 16px;">
|
|
||||||
<div>
|
|
||||||
<label>目标插件:</label>
|
|
||||||
<select v-model="versionInfo.pluginId" style="width: 100%; padding: 8px; border: 1px solid #dcdfe6; border-radius: 4px;">
|
|
||||||
<option v-for="plugin in plugins" :key="plugin.id" :value="plugin.id">
|
|
||||||
{{ plugin.icon }} {{ plugin.displayName }}
|
|
||||||
</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label>版本号:</label>
|
|
||||||
<input v-model="versionInfo.version" style="width: 100%; padding: 8px; border: 1px solid #dcdfe6; border-radius: 4px;">
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label>发布渠道:</label>
|
|
||||||
<select v-model="versionInfo.channel" style="width: 100%; padding: 8px; border: 1px solid #dcdfe6; border-radius: 4px;">
|
|
||||||
<option value="stable">稳定版</option>
|
|
||||||
<option value="beta">测试版</option>
|
|
||||||
<option value="dev">开发版</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div style="margin-top: 16px;">
|
|
||||||
<label>更新说明:</label>
|
|
||||||
<textarea v-model="versionInfo.description" rows="3" style="width: 100%; padding: 8px; border: 1px solid #dcdfe6; border-radius: 4px;"></textarea>
|
|
||||||
</div>
|
|
||||||
<div style="margin-top: 16px;">
|
|
||||||
<label>
|
|
||||||
<input type="checkbox" v-model="versionInfo.mandatory"> 强制更新
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<div style="margin-top: 16px;">
|
|
||||||
<button class="btn btn-primary" @click="uploadVersion" :disabled="uploading">
|
|
||||||
{{ uploading ? '上传中...' : '发布版本' }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 版本管理 -->
|
|
||||||
<div class="card">
|
|
||||||
<div style="padding: 20px;">
|
|
||||||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px;">
|
|
||||||
<h2>📋 版本管理 - {{ getCurrentPluginName() }}</h2>
|
|
||||||
<div style="display: flex; gap: 12px; align-items: center;">
|
|
||||||
<button class="btn btn-primary" @click="showCreatePlugin = true">+ 创建新项目</button>
|
|
||||||
<select v-model="currentPlugin" @change="filterVersions" style="padding: 8px; border: 1px solid #dcdfe6; border-radius: 4px;">
|
|
||||||
<option value="">全部插件</option>
|
|
||||||
<option v-for="plugin in plugins" :key="plugin.id" :value="plugin.id">
|
|
||||||
{{ plugin.icon }} {{ plugin.displayName }}
|
|
||||||
</option>
|
|
||||||
</select>
|
|
||||||
<button class="btn btn-success" @click="refreshVersions">刷新列表</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="version-list">
|
|
||||||
<div v-for="version in filteredVersions" :key="version.id" class="version-item">
|
|
||||||
<div class="version-info">
|
|
||||||
<h4>
|
|
||||||
<span style="margin-right: 8px;">{{ getPluginIcon(version.pluginId) }}</span>
|
|
||||||
{{ version.version }} ({{ getChannelName(version.channel) }})
|
|
||||||
<span style="font-size: 12px; color: #909399; margin-left: 8px;">- {{ getPluginDisplayName(version.pluginId) }}</span>
|
|
||||||
</h4>
|
|
||||||
<p>{{ version.description }}</p>
|
|
||||||
<p>发布时间: {{ formatDate(version.release_date) }} | 文件大小: {{ formatSize(version.file_size) }}</p>
|
|
||||||
</div>
|
|
||||||
<div style="display: flex; align-items: center; gap: 12px;">
|
|
||||||
<span :class="'status-tag status-' + version.status">
|
|
||||||
{{ getStatusName(version.status) }}
|
|
||||||
</span>
|
|
||||||
<button class="btn btn-primary" @click="editVersion(version)">编辑</button>
|
|
||||||
<button class="btn btn-danger" @click="deleteVersion(version)">删除</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div v-if="filteredVersions.length === 0" style="text-align: center; padding: 40px; color: #909399;">
|
|
||||||
{{ currentPlugin ? '该插件暂无版本数据' : '暂无版本数据' }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 创建项目模态框 -->
|
|
||||||
<div v-if="showCreatePlugin" class="modal-overlay" @click="showCreatePlugin = false">
|
|
||||||
<div class="modal-content" @click.stop>
|
|
||||||
<div class="modal-header">
|
|
||||||
<h3>🎯 创建新项目</h3>
|
|
||||||
<button @click="showCreatePlugin = false" class="modal-close">×</button>
|
|
||||||
</div>
|
|
||||||
<div class="modal-body">
|
|
||||||
<div class="form-group">
|
|
||||||
<label>项目ID *</label>
|
|
||||||
<input v-model="newPlugin.id" placeholder="例如: my-awesome-plugin" style="width: 100%; padding: 12px; border: 1px solid #dcdfe6; border-radius: 4px;">
|
|
||||||
<small>项目ID必须是唯一的,建议使用小写字母和短横线</small>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label>项目名称 *</label>
|
|
||||||
<input v-model="newPlugin.name" placeholder="例如: my-awesome-plugin" style="width: 100%; padding: 12px; border: 1px solid #dcdfe6; border-radius: 4px;">
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label>显示名称 *</label>
|
|
||||||
<input v-model="newPlugin.displayName" placeholder="例如: My Awesome Plugin" style="width: 100%; padding: 12px; border: 1px solid #dcdfe6; border-radius: 4px;">
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label>项目描述</label>
|
|
||||||
<textarea v-model="newPlugin.description" rows="3" placeholder="描述你的项目功能和特点..." style="width: 100%; padding: 12px; border: 1px solid #dcdfe6; border-radius: 4px;"></textarea>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label>作者</label>
|
|
||||||
<input v-model="newPlugin.author" placeholder="例如: Your Name" style="width: 100%; padding: 12px; border: 1px solid #dcdfe6; border-radius: 4px;">
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label>仓库地址</label>
|
|
||||||
<input v-model="newPlugin.repository" placeholder="例如: https://github.com/user/repo" style="width: 100%; padding: 12px; border: 1px solid #dcdfe6; border-radius: 4px;">
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label>图标 (Emoji)</label>
|
|
||||||
<input v-model="newPlugin.icon" placeholder="📦" maxlength="2" style="width: 100%; padding: 12px; border: 1px solid #dcdfe6; border-radius: 4px;">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="modal-footer">
|
|
||||||
<button @click="showCreatePlugin = false" class="btn btn-default">取消</button>
|
|
||||||
<button @click="createPlugin" class="btn btn-primary" :disabled="creatingPlugin">
|
|
||||||
{{ creatingPlugin ? '创建中...' : '创建项目' }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/vue@3.3.8/dist/vue.global.js"></script>
|
|
||||||
<script>
|
|
||||||
const { createApp } = Vue;
|
|
||||||
|
|
||||||
createApp({
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
uploading: false,
|
|
||||||
uploadProgress: 0,
|
|
||||||
selectedFile: null,
|
|
||||||
currentPlugin: 'cocos-ecs-extension', // 当前选中的插件
|
|
||||||
showCreatePlugin: false,
|
|
||||||
creatingPlugin: false,
|
|
||||||
newPlugin: {
|
|
||||||
id: '',
|
|
||||||
name: '',
|
|
||||||
displayName: '',
|
|
||||||
description: '',
|
|
||||||
author: '',
|
|
||||||
repository: '',
|
|
||||||
icon: '📦'
|
|
||||||
},
|
|
||||||
plugins: [],
|
|
||||||
versionInfo: {
|
|
||||||
pluginId: 'cocos-ecs-extension',
|
|
||||||
version: '',
|
|
||||||
channel: 'stable',
|
|
||||||
description: '',
|
|
||||||
mandatory: false
|
|
||||||
},
|
|
||||||
stats: {
|
|
||||||
totalVersions: 0,
|
|
||||||
activeUsers: 0,
|
|
||||||
todayUpdates: 0,
|
|
||||||
successRate: 100.0
|
|
||||||
},
|
|
||||||
versions: []
|
|
||||||
}
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
filteredVersions() {
|
|
||||||
if (!this.currentPlugin) {
|
|
||||||
return this.versions;
|
|
||||||
}
|
|
||||||
return this.versions.filter(v => v.pluginId === this.currentPlugin);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
selectPlugin(pluginId) {
|
|
||||||
this.currentPlugin = pluginId;
|
|
||||||
this.versionInfo.pluginId = pluginId;
|
|
||||||
},
|
|
||||||
filterVersions() {
|
|
||||||
// 触发计算属性重新计算
|
|
||||||
},
|
|
||||||
getCurrentPluginName() {
|
|
||||||
if (!this.currentPlugin) return '全部插件';
|
|
||||||
const plugin = this.plugins.find(p => p.id === this.currentPlugin);
|
|
||||||
return plugin ? plugin.displayName : '未知插件';
|
|
||||||
},
|
|
||||||
getPluginIcon(pluginId) {
|
|
||||||
const plugin = this.plugins.find(p => p.id === pluginId);
|
|
||||||
return plugin ? plugin.icon : '📦';
|
|
||||||
},
|
|
||||||
getPluginDisplayName(pluginId) {
|
|
||||||
const plugin = this.plugins.find(p => p.id === pluginId);
|
|
||||||
return plugin ? plugin.displayName : pluginId;
|
|
||||||
},
|
|
||||||
selectFile() {
|
|
||||||
this.$refs.fileInput.click();
|
|
||||||
},
|
|
||||||
handleFileSelect(event) {
|
|
||||||
const file = event.target.files[0];
|
|
||||||
if (file) {
|
|
||||||
this.selectedFile = file;
|
|
||||||
// 自动提取版本号(如果文件名包含版本信息)
|
|
||||||
const match = file.name.match(/(\d+\.\d+\.\d+)/);
|
|
||||||
if (match) {
|
|
||||||
this.versionInfo.version = match[1];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
async uploadVersion() {
|
|
||||||
if (!this.selectedFile) return;
|
|
||||||
|
|
||||||
this.uploading = true;
|
|
||||||
this.uploadProgress = 0;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const formData = new FormData();
|
|
||||||
formData.append('package', this.selectedFile);
|
|
||||||
formData.append('pluginId', this.versionInfo.pluginId);
|
|
||||||
formData.append('version', this.versionInfo.version);
|
|
||||||
formData.append('channel', this.versionInfo.channel);
|
|
||||||
formData.append('description', this.versionInfo.description);
|
|
||||||
formData.append('mandatory', this.versionInfo.mandatory.toString());
|
|
||||||
|
|
||||||
// 模拟上传进度
|
|
||||||
const progressInterval = setInterval(() => {
|
|
||||||
this.uploadProgress = Math.min(this.uploadProgress + Math.random() * 15, 90);
|
|
||||||
}, 200);
|
|
||||||
|
|
||||||
// 调用真实的上传API
|
|
||||||
const response = await fetch('/api/upload/package', {
|
|
||||||
method: 'POST',
|
|
||||||
body: formData
|
|
||||||
});
|
|
||||||
|
|
||||||
clearInterval(progressInterval);
|
|
||||||
this.uploadProgress = 100;
|
|
||||||
|
|
||||||
const result = await response.json();
|
|
||||||
|
|
||||||
if (result.success) {
|
|
||||||
alert(`版本上传成功!\n插件: ${result.data.pluginId}\n版本: ${result.data.version}\n文件数: ${result.data.filesCount}`);
|
|
||||||
|
|
||||||
// 重置表单
|
|
||||||
this.selectedFile = null;
|
|
||||||
this.versionInfo = {
|
|
||||||
pluginId: this.currentPlugin,
|
|
||||||
version: '',
|
|
||||||
channel: 'stable',
|
|
||||||
description: '',
|
|
||||||
mandatory: false
|
|
||||||
};
|
|
||||||
this.$refs.fileInput.value = '';
|
|
||||||
|
|
||||||
// 刷新数据
|
|
||||||
await this.refreshVersions();
|
|
||||||
await this.refreshStats();
|
|
||||||
} else {
|
|
||||||
alert('上传失败: ' + result.error);
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
alert('上传失败: ' + error.message);
|
|
||||||
} finally {
|
|
||||||
this.uploading = false;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
async refreshVersions() {
|
|
||||||
try {
|
|
||||||
const params = new URLSearchParams();
|
|
||||||
if (this.currentPlugin) {
|
|
||||||
params.set('pluginId', this.currentPlugin);
|
|
||||||
}
|
|
||||||
|
|
||||||
const response = await fetch(`/api/versions?${params}`);
|
|
||||||
const result = await response.json();
|
|
||||||
|
|
||||||
if (result.success) {
|
|
||||||
this.versions = result.data.map(v => ({
|
|
||||||
id: v.id,
|
|
||||||
pluginId: v.pluginId,
|
|
||||||
version: v.version,
|
|
||||||
channel: v.channel,
|
|
||||||
description: v.description,
|
|
||||||
release_date: v.releaseDate,
|
|
||||||
file_size: v.fileSize,
|
|
||||||
status: v.status
|
|
||||||
}));
|
|
||||||
} else {
|
|
||||||
console.error('获取版本列表失败:', result.error);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('刷新版本列表失败:', error);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
async refreshStats() {
|
|
||||||
try {
|
|
||||||
const response = await fetch('/api/stats');
|
|
||||||
const result = await response.json();
|
|
||||||
|
|
||||||
if (result.success) {
|
|
||||||
this.stats = result.data;
|
|
||||||
} else {
|
|
||||||
console.error('获取统计数据失败:', result.error);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('刷新统计数据失败:', error);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
editVersion(version) {
|
|
||||||
// 编辑版本
|
|
||||||
console.log('编辑版本:', version);
|
|
||||||
},
|
|
||||||
deleteVersion(version) {
|
|
||||||
if (confirm('确定要删除版本 ' + version.version + ' 吗?')) {
|
|
||||||
// 删除版本
|
|
||||||
console.log('删除版本:', version);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
getChannelName(channel) {
|
|
||||||
const names = { stable: '稳定版', beta: '测试版', dev: '开发版' };
|
|
||||||
return names[channel] || channel;
|
|
||||||
},
|
|
||||||
getStatusName(status) {
|
|
||||||
const names = { active: '已发布', draft: '草稿' };
|
|
||||||
return names[status] || status;
|
|
||||||
},
|
|
||||||
formatDate(dateString) {
|
|
||||||
return new Date(dateString).toLocaleString('zh-CN');
|
|
||||||
},
|
|
||||||
formatSize(bytes) {
|
|
||||||
const units = ['B', 'KB', 'MB', 'GB'];
|
|
||||||
let size = bytes;
|
|
||||||
let unitIndex = 0;
|
|
||||||
while (size >= 1024 && unitIndex < units.length - 1) {
|
|
||||||
size /= 1024;
|
|
||||||
unitIndex++;
|
|
||||||
}
|
|
||||||
return size.toFixed(1) + ' ' + units[unitIndex];
|
|
||||||
}
|
|
||||||
},
|
|
||||||
async mounted() {
|
|
||||||
// 加载插件列表
|
|
||||||
await this.loadPlugins();
|
|
||||||
|
|
||||||
// 加载初始数据
|
|
||||||
await this.refreshStats();
|
|
||||||
await this.refreshVersions();
|
|
||||||
},
|
|
||||||
async loadPlugins() {
|
|
||||||
try {
|
|
||||||
const response = await fetch('/api/plugins');
|
|
||||||
const result = await response.json();
|
|
||||||
|
|
||||||
if (result.success) {
|
|
||||||
this.plugins = result.data.map(p => ({
|
|
||||||
id: p.id,
|
|
||||||
name: p.name,
|
|
||||||
displayName: p.displayName,
|
|
||||||
description: p.description,
|
|
||||||
author: p.author,
|
|
||||||
repository: p.repository,
|
|
||||||
icon: p.icon,
|
|
||||||
status: p.status,
|
|
||||||
versionCount: p.versionCount || 0,
|
|
||||||
totalDownloads: p.totalDownloads || 0
|
|
||||||
}));
|
|
||||||
|
|
||||||
// 如果没有选中插件且有插件数据,自动选中第一个
|
|
||||||
if (!this.currentPlugin && this.plugins.length > 0) {
|
|
||||||
this.currentPlugin = this.plugins[0].id;
|
|
||||||
this.versionInfo.pluginId = this.plugins[0].id;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
console.error('获取插件列表失败:', result.error);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('加载插件列表失败:', error);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
async createPlugin() {
|
|
||||||
if (!this.newPlugin.id || !this.newPlugin.name || !this.newPlugin.displayName) {
|
|
||||||
alert('请填写必要的字段:项目ID、项目名称、显示名称');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.creatingPlugin = true;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await fetch('/api/plugins', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json'
|
|
||||||
},
|
|
||||||
body: JSON.stringify(this.newPlugin)
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await response.json();
|
|
||||||
|
|
||||||
if (result.success) {
|
|
||||||
alert(`项目创建成功!\n项目名称: ${result.data.displayName}\n项目ID: ${result.data.id}`);
|
|
||||||
|
|
||||||
// 重置表单
|
|
||||||
this.newPlugin = {
|
|
||||||
id: '',
|
|
||||||
name: '',
|
|
||||||
displayName: '',
|
|
||||||
description: '',
|
|
||||||
author: '',
|
|
||||||
repository: '',
|
|
||||||
icon: '📦'
|
|
||||||
};
|
|
||||||
|
|
||||||
// 关闭模态框
|
|
||||||
this.showCreatePlugin = false;
|
|
||||||
|
|
||||||
// 刷新插件列表
|
|
||||||
await this.loadPlugins();
|
|
||||||
await this.refreshStats();
|
|
||||||
} else {
|
|
||||||
alert('创建失败: ' + result.error);
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
alert('创建失败: ' + error.message);
|
|
||||||
} finally {
|
|
||||||
this.creatingPlugin = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}).mount('#app');
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,283 +0,0 @@
|
|||||||
import * as sqlite3 from 'sqlite3';
|
|
||||||
import { promisify } from 'util';
|
|
||||||
import * as path from 'path';
|
|
||||||
import * as fs from 'fs-extra';
|
|
||||||
|
|
||||||
// 数据库实例
|
|
||||||
export let db: sqlite3.Database;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 插件信息接口
|
|
||||||
*/
|
|
||||||
export interface PluginInfo {
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
displayName: string;
|
|
||||||
description: string;
|
|
||||||
author: string;
|
|
||||||
repository: string;
|
|
||||||
icon: string;
|
|
||||||
status: 'active' | 'inactive';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 初始化数据库
|
|
||||||
*/
|
|
||||||
export async function initDatabase(): Promise<void> {
|
|
||||||
const dbPath = path.join(__dirname, '../../data/hotupdate.db');
|
|
||||||
|
|
||||||
// 确保数据目录存在
|
|
||||||
await fs.ensureDir(path.dirname(dbPath));
|
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
db = new sqlite3.Database(dbPath, (err: any) => {
|
|
||||||
if (err) {
|
|
||||||
console.error('数据库连接失败:', err);
|
|
||||||
reject(err);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('✅ 数据库连接成功');
|
|
||||||
createTables().then(resolve).catch(reject);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 创建数据表
|
|
||||||
*/
|
|
||||||
async function createTables(): Promise<void> {
|
|
||||||
const run = promisify(db.run.bind(db));
|
|
||||||
|
|
||||||
try {
|
|
||||||
// 插件信息表
|
|
||||||
await run(`
|
|
||||||
CREATE TABLE IF NOT EXISTS plugins (
|
|
||||||
id TEXT PRIMARY KEY,
|
|
||||||
name TEXT NOT NULL,
|
|
||||||
display_name TEXT NOT NULL,
|
|
||||||
description TEXT,
|
|
||||||
author TEXT,
|
|
||||||
repository TEXT,
|
|
||||||
icon TEXT,
|
|
||||||
status TEXT DEFAULT 'active',
|
|
||||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
||||||
)
|
|
||||||
`);
|
|
||||||
|
|
||||||
// 版本信息表 - 添加plugin_id字段
|
|
||||||
await run(`
|
|
||||||
CREATE TABLE IF NOT EXISTS versions (
|
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
||||||
plugin_id TEXT NOT NULL,
|
|
||||||
version TEXT NOT NULL,
|
|
||||||
channel TEXT NOT NULL DEFAULT 'stable',
|
|
||||||
release_date DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
description TEXT,
|
|
||||||
download_url TEXT NOT NULL,
|
|
||||||
file_size INTEGER NOT NULL,
|
|
||||||
checksum TEXT NOT NULL,
|
|
||||||
mandatory BOOLEAN DEFAULT 0,
|
|
||||||
status TEXT DEFAULT 'active',
|
|
||||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
FOREIGN KEY (plugin_id) REFERENCES plugins (id) ON DELETE CASCADE,
|
|
||||||
UNIQUE(plugin_id, version, channel)
|
|
||||||
)
|
|
||||||
`);
|
|
||||||
|
|
||||||
// 版本文件表
|
|
||||||
await run(`
|
|
||||||
CREATE TABLE IF NOT EXISTS version_files (
|
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
||||||
version_id INTEGER NOT NULL,
|
|
||||||
file_path TEXT NOT NULL,
|
|
||||||
file_hash TEXT NOT NULL,
|
|
||||||
file_size INTEGER NOT NULL,
|
|
||||||
action TEXT NOT NULL DEFAULT 'update',
|
|
||||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
FOREIGN KEY (version_id) REFERENCES versions (id) ON DELETE CASCADE
|
|
||||||
)
|
|
||||||
`);
|
|
||||||
|
|
||||||
// 用户表
|
|
||||||
await run(`
|
|
||||||
CREATE TABLE IF NOT EXISTS users (
|
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
||||||
username TEXT NOT NULL UNIQUE,
|
|
||||||
password_hash TEXT NOT NULL,
|
|
||||||
role TEXT DEFAULT 'admin',
|
|
||||||
last_login DATETIME,
|
|
||||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
||||||
)
|
|
||||||
`);
|
|
||||||
|
|
||||||
// 更新统计表 - 添加plugin_id字段
|
|
||||||
await run(`
|
|
||||||
CREATE TABLE IF NOT EXISTS update_stats (
|
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
||||||
plugin_id TEXT NOT NULL,
|
|
||||||
version TEXT NOT NULL,
|
|
||||||
user_count INTEGER DEFAULT 0,
|
|
||||||
success_count INTEGER DEFAULT 0,
|
|
||||||
failure_count INTEGER DEFAULT 0,
|
|
||||||
last_updated DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
FOREIGN KEY (plugin_id) REFERENCES plugins (id) ON DELETE CASCADE
|
|
||||||
)
|
|
||||||
`);
|
|
||||||
|
|
||||||
// 配置表
|
|
||||||
await run(`
|
|
||||||
CREATE TABLE IF NOT EXISTS config (
|
|
||||||
key TEXT PRIMARY KEY,
|
|
||||||
value TEXT NOT NULL,
|
|
||||||
description TEXT,
|
|
||||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
||||||
)
|
|
||||||
`);
|
|
||||||
|
|
||||||
console.log('✅ 数据表创建成功');
|
|
||||||
|
|
||||||
// 插入默认数据
|
|
||||||
await insertDefaultData();
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('❌ 数据表创建失败:', error);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 插入默认数据
|
|
||||||
*/
|
|
||||||
async function insertDefaultData(): Promise<void> {
|
|
||||||
const run = promisify(db.run.bind(db)) as (sql: string, params?: any[]) => Promise<any>;
|
|
||||||
|
|
||||||
// 插入默认插件
|
|
||||||
const defaultPlugins: PluginInfo[] = [
|
|
||||||
{
|
|
||||||
id: 'cocos-ecs-extension',
|
|
||||||
name: 'cocos-ecs-extension',
|
|
||||||
displayName: 'Cocos ECS Framework',
|
|
||||||
description: '为Cocos Creator提供高性能ECS框架支持',
|
|
||||||
author: 'esengine',
|
|
||||||
repository: 'https://github.com/esengine/ecs-framework',
|
|
||||||
icon: '🎮',
|
|
||||||
status: 'active'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'behaviour-tree-ai',
|
|
||||||
name: 'behaviour-tree-ai',
|
|
||||||
displayName: 'Behaviour Tree AI',
|
|
||||||
description: '智能行为树AI系统,支持可视化编辑',
|
|
||||||
author: 'esengine',
|
|
||||||
repository: 'https://github.com/esengine/ecs-framework/tree/master/thirdparty/BehaviourTree-ai',
|
|
||||||
icon: '🧠',
|
|
||||||
status: 'active'
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
for (const plugin of defaultPlugins) {
|
|
||||||
try {
|
|
||||||
await run(
|
|
||||||
`INSERT OR IGNORE INTO plugins
|
|
||||||
(id, name, display_name, description, author, repository, icon, status)
|
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
||||||
[plugin.id, plugin.name, plugin.displayName, plugin.description,
|
|
||||||
plugin.author, plugin.repository, plugin.icon, plugin.status]
|
|
||||||
);
|
|
||||||
} catch (error) {
|
|
||||||
console.warn('插件数据插入失败:', plugin.id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 插入默认配置
|
|
||||||
const defaultConfigs = [
|
|
||||||
{ key: 'server_url', value: 'http://localhost:3001', description: '服务器地址' },
|
|
||||||
{ key: 'cdn_url', value: 'https://cdn.earthonline-game.cn', description: 'CDN地址' },
|
|
||||||
{ key: 'auto_backup', value: 'true', description: '自动备份' },
|
|
||||||
{ key: 'max_backup_count', value: '5', description: '最大备份数量' },
|
|
||||||
{ key: 'upload_max_size', value: '100', description: '上传文件最大大小(MB)' }
|
|
||||||
];
|
|
||||||
|
|
||||||
for (const config of defaultConfigs) {
|
|
||||||
try {
|
|
||||||
await run(
|
|
||||||
'INSERT OR IGNORE INTO config (key, value, description) VALUES (?, ?, ?)',
|
|
||||||
[config.key, config.value, config.description]
|
|
||||||
);
|
|
||||||
} catch (error) {
|
|
||||||
console.warn('配置插入失败:', config.key);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 插入默认管理员用户
|
|
||||||
const bcrypt = require('bcryptjs');
|
|
||||||
const defaultPassword = 'admin123';
|
|
||||||
const hashedPassword = await bcrypt.hash(defaultPassword, 10);
|
|
||||||
|
|
||||||
try {
|
|
||||||
await run(
|
|
||||||
'INSERT OR IGNORE INTO users (username, password_hash, role) VALUES (?, ?, ?)',
|
|
||||||
['admin', hashedPassword, 'admin']
|
|
||||||
);
|
|
||||||
console.log('✅ 默认管理员账户创建成功 (用户名: admin, 密码: admin123)');
|
|
||||||
} catch (error) {
|
|
||||||
console.warn('默认用户创建失败:', error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 执行查询
|
|
||||||
*/
|
|
||||||
export function query(sql: string, params: any[] = []): Promise<any[]> {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
db.all(sql, params, (err: any, rows: any) => {
|
|
||||||
if (err) {
|
|
||||||
reject(err);
|
|
||||||
} else {
|
|
||||||
resolve(rows);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 执行单个查询
|
|
||||||
*/
|
|
||||||
export function queryOne(sql: string, params: any[] = []): Promise<any> {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
db.get(sql, params, (err: any, row: any) => {
|
|
||||||
if (err) {
|
|
||||||
reject(err);
|
|
||||||
} else {
|
|
||||||
resolve(row);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 执行更新/插入
|
|
||||||
*/
|
|
||||||
export function execute(sql: string, params: any[] = []): Promise<{ lastID: number; changes: number }> {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
db.run(sql, params, function(err: any) {
|
|
||||||
if (err) {
|
|
||||||
reject(err);
|
|
||||||
} else {
|
|
||||||
resolve({ lastID: (this as any).lastID, changes: (this as any).changes });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 关闭数据库连接
|
|
||||||
*/
|
|
||||||
export function closeDatabase(): void {
|
|
||||||
db.close();
|
|
||||||
}
|
|
||||||
@@ -1,221 +0,0 @@
|
|||||||
import { Router, Request, Response } from 'express';
|
|
||||||
import * as bcrypt from 'bcryptjs';
|
|
||||||
import * as jwt from 'jsonwebtoken';
|
|
||||||
import { query, execute } from '../database';
|
|
||||||
|
|
||||||
export const authRoutes = Router();
|
|
||||||
|
|
||||||
// JWT密钥 - 生产环境应该从环境变量读取
|
|
||||||
const JWT_SECRET = process.env.JWT_SECRET || 'your-secret-key-change-in-production';
|
|
||||||
const JWT_EXPIRES_IN = '24h';
|
|
||||||
|
|
||||||
interface LoginRequest {
|
|
||||||
username: string;
|
|
||||||
password: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface AuthRequest extends Request {
|
|
||||||
user?: {
|
|
||||||
id: number;
|
|
||||||
username: string;
|
|
||||||
role: string;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 登录
|
|
||||||
*/
|
|
||||||
authRoutes.post('/login', async (req: Request, res: Response) => {
|
|
||||||
try {
|
|
||||||
const { username, password }: LoginRequest = req.body;
|
|
||||||
|
|
||||||
if (!username || !password) {
|
|
||||||
return res.status(400).json({
|
|
||||||
success: false,
|
|
||||||
error: '用户名和密码不能为空'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 查找用户
|
|
||||||
const users = await query(`
|
|
||||||
SELECT id, username, password_hash, role
|
|
||||||
FROM users
|
|
||||||
WHERE username = ?
|
|
||||||
`, [username]);
|
|
||||||
|
|
||||||
if (users.length === 0) {
|
|
||||||
return res.status(401).json({
|
|
||||||
success: false,
|
|
||||||
error: '用户名或密码错误'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const user = users[0];
|
|
||||||
|
|
||||||
// 验证密码
|
|
||||||
const isPasswordValid = await bcrypt.compare(password, user.password_hash);
|
|
||||||
|
|
||||||
if (!isPasswordValid) {
|
|
||||||
return res.status(401).json({
|
|
||||||
success: false,
|
|
||||||
error: '用户名或密码错误'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 生成JWT token
|
|
||||||
const token = jwt.sign(
|
|
||||||
{
|
|
||||||
id: user.id,
|
|
||||||
username: user.username,
|
|
||||||
role: user.role
|
|
||||||
},
|
|
||||||
JWT_SECRET,
|
|
||||||
{ expiresIn: JWT_EXPIRES_IN }
|
|
||||||
);
|
|
||||||
|
|
||||||
// 更新最后登录时间
|
|
||||||
await execute(`
|
|
||||||
UPDATE users
|
|
||||||
SET last_login = CURRENT_TIMESTAMP
|
|
||||||
WHERE id = ?
|
|
||||||
`, [user.id]);
|
|
||||||
|
|
||||||
res.json({
|
|
||||||
success: true,
|
|
||||||
data: {
|
|
||||||
token,
|
|
||||||
user: {
|
|
||||||
id: user.id,
|
|
||||||
username: user.username,
|
|
||||||
role: user.role
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('登录失败:', error);
|
|
||||||
res.status(500).json({
|
|
||||||
success: false,
|
|
||||||
error: '登录失败',
|
|
||||||
message: error instanceof Error ? error.message : String(error)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 验证token
|
|
||||||
*/
|
|
||||||
authRoutes.get('/verify', authenticateToken, (req: AuthRequest, res: Response) => {
|
|
||||||
res.json({
|
|
||||||
success: true,
|
|
||||||
data: {
|
|
||||||
user: req.user
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 登出 (清除客户端token即可)
|
|
||||||
*/
|
|
||||||
authRoutes.post('/logout', (req: Request, res: Response) => {
|
|
||||||
res.json({
|
|
||||||
success: true,
|
|
||||||
message: '登出成功'
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 修改密码
|
|
||||||
*/
|
|
||||||
authRoutes.post('/change-password', authenticateToken, async (req: AuthRequest, res: Response) => {
|
|
||||||
try {
|
|
||||||
const { currentPassword, newPassword } = req.body;
|
|
||||||
|
|
||||||
if (!currentPassword || !newPassword) {
|
|
||||||
return res.status(400).json({
|
|
||||||
success: false,
|
|
||||||
error: '当前密码和新密码不能为空'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (newPassword.length < 6) {
|
|
||||||
return res.status(400).json({
|
|
||||||
success: false,
|
|
||||||
error: '新密码长度不能少于6位'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取当前用户信息
|
|
||||||
const users = await query(`
|
|
||||||
SELECT password_hash FROM users WHERE id = ?
|
|
||||||
`, [req.user!.id]);
|
|
||||||
|
|
||||||
if (users.length === 0) {
|
|
||||||
return res.status(404).json({
|
|
||||||
success: false,
|
|
||||||
error: '用户不存在'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 验证当前密码
|
|
||||||
const isCurrentPasswordValid = await bcrypt.compare(currentPassword, users[0].password_hash);
|
|
||||||
|
|
||||||
if (!isCurrentPasswordValid) {
|
|
||||||
return res.status(401).json({
|
|
||||||
success: false,
|
|
||||||
error: '当前密码错误'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 加密新密码
|
|
||||||
const saltRounds = 10;
|
|
||||||
const hashedNewPassword = await bcrypt.hash(newPassword, saltRounds);
|
|
||||||
|
|
||||||
// 更新密码
|
|
||||||
await execute(`
|
|
||||||
UPDATE users
|
|
||||||
SET password_hash = ?
|
|
||||||
WHERE id = ?
|
|
||||||
`, [hashedNewPassword, req.user!.id]);
|
|
||||||
|
|
||||||
res.json({
|
|
||||||
success: true,
|
|
||||||
message: '密码修改成功'
|
|
||||||
});
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('修改密码失败:', error);
|
|
||||||
res.status(500).json({
|
|
||||||
success: false,
|
|
||||||
error: '修改密码失败',
|
|
||||||
message: error instanceof Error ? error.message : String(error)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* JWT Token验证中间件
|
|
||||||
*/
|
|
||||||
export function authenticateToken(req: AuthRequest, res: Response, next: any) {
|
|
||||||
const authHeader = req.headers['authorization'];
|
|
||||||
const token = authHeader && authHeader.split(' ')[1]; // Bearer TOKEN
|
|
||||||
|
|
||||||
if (!token) {
|
|
||||||
return res.status(401).json({
|
|
||||||
success: false,
|
|
||||||
error: '访问令牌缺失'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
jwt.verify(token, JWT_SECRET, (err: any, user: any) => {
|
|
||||||
if (err) {
|
|
||||||
return res.status(403).json({
|
|
||||||
success: false,
|
|
||||||
error: '访问令牌无效或已过期'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
req.user = user;
|
|
||||||
next();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -1,190 +0,0 @@
|
|||||||
import { Router, Request, Response } from 'express';
|
|
||||||
import { query, execute } from '../database';
|
|
||||||
|
|
||||||
export const configRoutes = Router();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取所有配置
|
|
||||||
*/
|
|
||||||
configRoutes.get('/', async (req: Request, res: Response) => {
|
|
||||||
try {
|
|
||||||
const configs = await query(`
|
|
||||||
SELECT key, value, description, updated_at
|
|
||||||
FROM config
|
|
||||||
ORDER BY key
|
|
||||||
`);
|
|
||||||
|
|
||||||
res.json({
|
|
||||||
success: true,
|
|
||||||
data: configs
|
|
||||||
});
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('获取配置失败:', error);
|
|
||||||
res.status(500).json({
|
|
||||||
success: false,
|
|
||||||
error: '获取配置失败',
|
|
||||||
message: error instanceof Error ? error.message : String(error)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取特定配置
|
|
||||||
*/
|
|
||||||
configRoutes.get('/:key', async (req: Request, res: Response) => {
|
|
||||||
try {
|
|
||||||
const { key } = req.params;
|
|
||||||
|
|
||||||
const config = await query(`
|
|
||||||
SELECT key, value, description, updated_at
|
|
||||||
FROM config
|
|
||||||
WHERE key = ?
|
|
||||||
`, [key]);
|
|
||||||
|
|
||||||
if (config.length === 0) {
|
|
||||||
return res.status(404).json({
|
|
||||||
success: false,
|
|
||||||
error: '配置不存在'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
res.json({
|
|
||||||
success: true,
|
|
||||||
data: config[0]
|
|
||||||
});
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('获取配置失败:', error);
|
|
||||||
res.status(500).json({
|
|
||||||
success: false,
|
|
||||||
error: '获取配置失败',
|
|
||||||
message: error instanceof Error ? error.message : String(error)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 更新配置
|
|
||||||
*/
|
|
||||||
configRoutes.put('/:key', async (req: Request, res: Response) => {
|
|
||||||
try {
|
|
||||||
const { key } = req.params;
|
|
||||||
const { value, description } = req.body;
|
|
||||||
|
|
||||||
if (value === undefined) {
|
|
||||||
return res.status(400).json({
|
|
||||||
success: false,
|
|
||||||
error: '缺少配置值'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查配置是否存在
|
|
||||||
const existing = await query('SELECT key FROM config WHERE key = ?', [key]);
|
|
||||||
|
|
||||||
if (existing.length === 0) {
|
|
||||||
// 创建新配置
|
|
||||||
await execute(`
|
|
||||||
INSERT INTO config (key, value, description, updated_at)
|
|
||||||
VALUES (?, ?, ?, CURRENT_TIMESTAMP)
|
|
||||||
`, [key, value, description || '']);
|
|
||||||
} else {
|
|
||||||
// 更新现有配置
|
|
||||||
await execute(`
|
|
||||||
UPDATE config
|
|
||||||
SET value = ?, description = ?, updated_at = CURRENT_TIMESTAMP
|
|
||||||
WHERE key = ?
|
|
||||||
`, [value, description || '', key]);
|
|
||||||
}
|
|
||||||
|
|
||||||
res.json({
|
|
||||||
success: true,
|
|
||||||
message: '配置更新成功'
|
|
||||||
});
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('更新配置失败:', error);
|
|
||||||
res.status(500).json({
|
|
||||||
success: false,
|
|
||||||
error: '更新配置失败',
|
|
||||||
message: error instanceof Error ? error.message : String(error)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 删除配置
|
|
||||||
*/
|
|
||||||
configRoutes.delete('/:key', async (req: Request, res: Response) => {
|
|
||||||
try {
|
|
||||||
const { key } = req.params;
|
|
||||||
|
|
||||||
await execute('DELETE FROM config WHERE key = ?', [key]);
|
|
||||||
|
|
||||||
res.json({
|
|
||||||
success: true,
|
|
||||||
message: '配置删除成功'
|
|
||||||
});
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('删除配置失败:', error);
|
|
||||||
res.status(500).json({
|
|
||||||
success: false,
|
|
||||||
error: '删除配置失败',
|
|
||||||
message: error instanceof Error ? error.message : String(error)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 批量更新配置
|
|
||||||
*/
|
|
||||||
configRoutes.post('/batch', async (req: Request, res: Response) => {
|
|
||||||
try {
|
|
||||||
const { configs } = req.body;
|
|
||||||
|
|
||||||
if (!Array.isArray(configs)) {
|
|
||||||
return res.status(400).json({
|
|
||||||
success: false,
|
|
||||||
error: '配置数据格式错误'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const config of configs) {
|
|
||||||
const { key, value, description } = config;
|
|
||||||
|
|
||||||
if (!key || value === undefined) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查配置是否存在
|
|
||||||
const existing = await query('SELECT key FROM config WHERE key = ?', [key]);
|
|
||||||
|
|
||||||
if (existing.length === 0) {
|
|
||||||
await execute(`
|
|
||||||
INSERT INTO config (key, value, description, updated_at)
|
|
||||||
VALUES (?, ?, ?, CURRENT_TIMESTAMP)
|
|
||||||
`, [key, value, description || '']);
|
|
||||||
} else {
|
|
||||||
await execute(`
|
|
||||||
UPDATE config
|
|
||||||
SET value = ?, description = ?, updated_at = CURRENT_TIMESTAMP
|
|
||||||
WHERE key = ?
|
|
||||||
`, [value, description || '', key]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
res.json({
|
|
||||||
success: true,
|
|
||||||
message: '批量配置更新成功'
|
|
||||||
});
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('批量更新配置失败:', error);
|
|
||||||
res.status(500).json({
|
|
||||||
success: false,
|
|
||||||
error: '批量更新配置失败',
|
|
||||||
message: error instanceof Error ? error.message : String(error)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
@@ -1,302 +0,0 @@
|
|||||||
import { Router, Request, Response } from 'express';
|
|
||||||
import { query, queryOne, execute } from '../database';
|
|
||||||
|
|
||||||
export const pluginRoutes = Router();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取插件列表
|
|
||||||
*/
|
|
||||||
pluginRoutes.get('/', async (req: Request, res: Response) => {
|
|
||||||
try {
|
|
||||||
const plugins = await query(`
|
|
||||||
SELECT
|
|
||||||
p.*,
|
|
||||||
COUNT(DISTINCT v.id) as version_count,
|
|
||||||
COALESCE(SUM(us.user_count), 0) as total_downloads,
|
|
||||||
MAX(v.created_at) as latest_version_date
|
|
||||||
FROM plugins p
|
|
||||||
LEFT JOIN versions v ON p.id = v.plugin_id
|
|
||||||
LEFT JOIN update_stats us ON p.id = us.plugin_id
|
|
||||||
GROUP BY p.id
|
|
||||||
ORDER BY p.display_name
|
|
||||||
`);
|
|
||||||
|
|
||||||
res.json({
|
|
||||||
success: true,
|
|
||||||
data: plugins.map(p => ({
|
|
||||||
id: p.id,
|
|
||||||
name: p.name,
|
|
||||||
displayName: p.display_name,
|
|
||||||
description: p.description,
|
|
||||||
author: p.author,
|
|
||||||
repository: p.repository,
|
|
||||||
icon: p.icon,
|
|
||||||
status: p.status,
|
|
||||||
versionCount: p.version_count,
|
|
||||||
totalDownloads: p.total_downloads,
|
|
||||||
latestVersionDate: p.latest_version_date,
|
|
||||||
createdAt: p.created_at
|
|
||||||
}))
|
|
||||||
});
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('获取插件列表失败:', error);
|
|
||||||
res.status(500).json({
|
|
||||||
success: false,
|
|
||||||
error: '获取插件列表失败',
|
|
||||||
message: error instanceof Error ? error.message : String(error)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取特定插件信息
|
|
||||||
*/
|
|
||||||
pluginRoutes.get('/:id', async (req: Request, res: Response) => {
|
|
||||||
try {
|
|
||||||
const { id } = req.params;
|
|
||||||
|
|
||||||
const plugin = await queryOne(`
|
|
||||||
SELECT
|
|
||||||
p.*,
|
|
||||||
COUNT(DISTINCT v.id) as version_count,
|
|
||||||
COALESCE(SUM(us.user_count), 0) as total_downloads,
|
|
||||||
MAX(v.created_at) as latest_version_date
|
|
||||||
FROM plugins p
|
|
||||||
LEFT JOIN versions v ON p.id = v.plugin_id
|
|
||||||
LEFT JOIN update_stats us ON p.id = us.plugin_id
|
|
||||||
WHERE p.id = ?
|
|
||||||
GROUP BY p.id
|
|
||||||
`, [id]);
|
|
||||||
|
|
||||||
if (!plugin) {
|
|
||||||
return res.status(404).json({
|
|
||||||
success: false,
|
|
||||||
error: '插件不存在'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
res.json({
|
|
||||||
success: true,
|
|
||||||
data: {
|
|
||||||
id: plugin.id,
|
|
||||||
name: plugin.name,
|
|
||||||
displayName: plugin.display_name,
|
|
||||||
description: plugin.description,
|
|
||||||
author: plugin.author,
|
|
||||||
repository: plugin.repository,
|
|
||||||
icon: plugin.icon,
|
|
||||||
status: plugin.status,
|
|
||||||
versionCount: plugin.version_count,
|
|
||||||
totalDownloads: plugin.total_downloads,
|
|
||||||
latestVersionDate: plugin.latest_version_date,
|
|
||||||
createdAt: plugin.created_at
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('获取插件信息失败:', error);
|
|
||||||
res.status(500).json({
|
|
||||||
success: false,
|
|
||||||
error: '获取插件信息失败',
|
|
||||||
message: error instanceof Error ? error.message : String(error)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 创建新插件
|
|
||||||
*/
|
|
||||||
pluginRoutes.post('/', async (req: Request, res: Response) => {
|
|
||||||
try {
|
|
||||||
const { id, name, displayName, description, author, repository, icon } = req.body;
|
|
||||||
|
|
||||||
if (!id || !name || !displayName) {
|
|
||||||
return res.status(400).json({
|
|
||||||
success: false,
|
|
||||||
error: '缺少必要参数: id, name, displayName'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查插件ID是否已存在
|
|
||||||
const existingPlugin = await queryOne('SELECT id FROM plugins WHERE id = ?', [id]);
|
|
||||||
|
|
||||||
if (existingPlugin) {
|
|
||||||
return res.status(400).json({
|
|
||||||
success: false,
|
|
||||||
error: '插件ID已存在'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 创建插件记录
|
|
||||||
await execute(`
|
|
||||||
INSERT INTO plugins
|
|
||||||
(id, name, display_name, description, author, repository, icon)
|
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
||||||
`, [id, name, displayName, description || '', author || '', repository || '', icon || '📦']);
|
|
||||||
|
|
||||||
console.log(`✅ 新插件创建成功: ${displayName} (${id})`);
|
|
||||||
|
|
||||||
res.json({
|
|
||||||
success: true,
|
|
||||||
data: {
|
|
||||||
id,
|
|
||||||
name,
|
|
||||||
displayName,
|
|
||||||
description: description || '',
|
|
||||||
author: author || '',
|
|
||||||
repository: repository || '',
|
|
||||||
icon: icon || '📦',
|
|
||||||
status: 'active'
|
|
||||||
},
|
|
||||||
message: '插件创建成功'
|
|
||||||
});
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('创建插件失败:', error);
|
|
||||||
res.status(500).json({
|
|
||||||
success: false,
|
|
||||||
error: '创建插件失败',
|
|
||||||
message: error instanceof Error ? error.message : String(error)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 更新插件信息
|
|
||||||
*/
|
|
||||||
pluginRoutes.put('/:id', async (req: Request, res: Response) => {
|
|
||||||
try {
|
|
||||||
const { id } = req.params;
|
|
||||||
const { displayName, description, author, repository, icon, status } = req.body;
|
|
||||||
|
|
||||||
// 检查插件是否存在
|
|
||||||
const existingPlugin = await queryOne('SELECT id FROM plugins WHERE id = ?', [id]);
|
|
||||||
|
|
||||||
if (!existingPlugin) {
|
|
||||||
return res.status(404).json({
|
|
||||||
success: false,
|
|
||||||
error: '插件不存在'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 更新插件信息
|
|
||||||
await execute(`
|
|
||||||
UPDATE plugins
|
|
||||||
SET display_name = ?, description = ?, author = ?, repository = ?, icon = ?, status = ?, updated_at = ?
|
|
||||||
WHERE id = ?
|
|
||||||
`, [displayName, description, author, repository, icon, status, new Date().toISOString(), id]);
|
|
||||||
|
|
||||||
res.json({
|
|
||||||
success: true,
|
|
||||||
message: '插件信息更新成功'
|
|
||||||
});
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('更新插件失败:', error);
|
|
||||||
res.status(500).json({
|
|
||||||
success: false,
|
|
||||||
error: '更新插件失败',
|
|
||||||
message: error instanceof Error ? error.message : String(error)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 删除插件(软删除)
|
|
||||||
*/
|
|
||||||
pluginRoutes.delete('/:id', async (req: Request, res: Response) => {
|
|
||||||
try {
|
|
||||||
const { id } = req.params;
|
|
||||||
|
|
||||||
// 检查插件是否存在
|
|
||||||
const existingPlugin = await queryOne('SELECT id FROM plugins WHERE id = ?', [id]);
|
|
||||||
|
|
||||||
if (!existingPlugin) {
|
|
||||||
return res.status(404).json({
|
|
||||||
success: false,
|
|
||||||
error: '插件不存在'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查是否有版本记录
|
|
||||||
const versionCount = await queryOne('SELECT COUNT(*) as count FROM versions WHERE plugin_id = ?', [id]);
|
|
||||||
|
|
||||||
if (versionCount && versionCount.count > 0) {
|
|
||||||
// 如果有版本记录,只进行软删除
|
|
||||||
await execute(`
|
|
||||||
UPDATE plugins
|
|
||||||
SET status = 'inactive', updated_at = ?
|
|
||||||
WHERE id = ?
|
|
||||||
`, [new Date().toISOString(), id]);
|
|
||||||
|
|
||||||
res.json({
|
|
||||||
success: true,
|
|
||||||
message: '插件已停用(因为存在版本记录)'
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
// 如果没有版本记录,可以直接删除
|
|
||||||
await execute('DELETE FROM plugins WHERE id = ?', [id]);
|
|
||||||
|
|
||||||
res.json({
|
|
||||||
success: true,
|
|
||||||
message: '插件删除成功'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('删除插件失败:', error);
|
|
||||||
res.status(500).json({
|
|
||||||
success: false,
|
|
||||||
error: '删除插件失败',
|
|
||||||
message: error instanceof Error ? error.message : String(error)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 激活/停用插件
|
|
||||||
*/
|
|
||||||
pluginRoutes.patch('/:id/status', async (req: Request, res: Response) => {
|
|
||||||
try {
|
|
||||||
const { id } = req.params;
|
|
||||||
const { status } = req.body;
|
|
||||||
|
|
||||||
if (!['active', 'inactive'].includes(status)) {
|
|
||||||
return res.status(400).json({
|
|
||||||
success: false,
|
|
||||||
error: '无效的状态值,必须是 active 或 inactive'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查插件是否存在
|
|
||||||
const existingPlugin = await queryOne('SELECT id FROM plugins WHERE id = ?', [id]);
|
|
||||||
|
|
||||||
if (!existingPlugin) {
|
|
||||||
return res.status(404).json({
|
|
||||||
success: false,
|
|
||||||
error: '插件不存在'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 更新状态
|
|
||||||
await execute(`
|
|
||||||
UPDATE plugins
|
|
||||||
SET status = ?, updated_at = ?
|
|
||||||
WHERE id = ?
|
|
||||||
`, [status, new Date().toISOString(), id]);
|
|
||||||
|
|
||||||
res.json({
|
|
||||||
success: true,
|
|
||||||
message: `插件已${status === 'active' ? '激活' : '停用'}`
|
|
||||||
});
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('更新插件状态失败:', error);
|
|
||||||
res.status(500).json({
|
|
||||||
success: false,
|
|
||||||
error: '更新插件状态失败',
|
|
||||||
message: error instanceof Error ? error.message : String(error)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
@@ -1,299 +0,0 @@
|
|||||||
import { Router, Request, Response } from 'express';
|
|
||||||
import { query, queryOne, execute } from '../database';
|
|
||||||
|
|
||||||
export const statsRoutes = Router();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取整体统计信息
|
|
||||||
*/
|
|
||||||
statsRoutes.get('/', async (req: Request, res: Response) => {
|
|
||||||
try {
|
|
||||||
// 插件数量统计
|
|
||||||
const pluginStats = await query(`
|
|
||||||
SELECT
|
|
||||||
COUNT(*) as total_plugins,
|
|
||||||
COUNT(CASE WHEN status = 'active' THEN 1 END) as active_plugins,
|
|
||||||
COUNT(CASE WHEN status = 'inactive' THEN 1 END) as inactive_plugins
|
|
||||||
FROM plugins
|
|
||||||
`);
|
|
||||||
|
|
||||||
// 版本数量统计
|
|
||||||
const versionStats = await query(`
|
|
||||||
SELECT
|
|
||||||
COUNT(*) as total_versions,
|
|
||||||
COUNT(CASE WHEN status = 'active' THEN 1 END) as active_versions,
|
|
||||||
COUNT(CASE WHEN channel = 'stable' THEN 1 END) as stable_versions,
|
|
||||||
COUNT(CASE WHEN channel = 'beta' THEN 1 END) as beta_versions,
|
|
||||||
COUNT(CASE WHEN mandatory = 1 THEN 1 END) as mandatory_versions
|
|
||||||
FROM versions
|
|
||||||
`);
|
|
||||||
|
|
||||||
// 文件数量统计
|
|
||||||
const fileStats = await query(`
|
|
||||||
SELECT
|
|
||||||
COUNT(*) as total_files,
|
|
||||||
SUM(file_size) as total_size,
|
|
||||||
AVG(file_size) as avg_file_size
|
|
||||||
FROM version_files
|
|
||||||
`);
|
|
||||||
|
|
||||||
// 更新统计
|
|
||||||
const updateStats = await query(`
|
|
||||||
SELECT
|
|
||||||
SUM(user_count) as total_users,
|
|
||||||
SUM(success_count) as total_success,
|
|
||||||
SUM(failure_count) as total_failure
|
|
||||||
FROM update_stats
|
|
||||||
`);
|
|
||||||
|
|
||||||
res.json({
|
|
||||||
success: true,
|
|
||||||
data: {
|
|
||||||
plugins: pluginStats[0] || {},
|
|
||||||
versions: versionStats[0] || {},
|
|
||||||
files: fileStats[0] || {},
|
|
||||||
updates: updateStats[0] || {}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('获取统计信息失败:', error);
|
|
||||||
res.status(500).json({
|
|
||||||
success: false,
|
|
||||||
error: '获取统计信息失败',
|
|
||||||
message: error instanceof Error ? error.message : String(error)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取插件使用统计
|
|
||||||
*/
|
|
||||||
statsRoutes.get('/plugins', async (req: Request, res: Response) => {
|
|
||||||
try {
|
|
||||||
const { limit = 10 } = req.query;
|
|
||||||
|
|
||||||
const pluginStats = await query(`
|
|
||||||
SELECT
|
|
||||||
p.id,
|
|
||||||
p.display_name,
|
|
||||||
p.icon,
|
|
||||||
COUNT(v.id) as version_count,
|
|
||||||
COALESCE(SUM(us.user_count), 0) as total_users,
|
|
||||||
COALESCE(SUM(us.success_count), 0) as total_success,
|
|
||||||
COALESCE(SUM(us.failure_count), 0) as total_failure,
|
|
||||||
MAX(v.created_at) as latest_version_date
|
|
||||||
FROM plugins p
|
|
||||||
LEFT JOIN versions v ON p.id = v.plugin_id AND v.status = 'active'
|
|
||||||
LEFT JOIN update_stats us ON p.id = us.plugin_id
|
|
||||||
WHERE p.status = 'active'
|
|
||||||
GROUP BY p.id, p.display_name, p.icon
|
|
||||||
ORDER BY total_users DESC
|
|
||||||
LIMIT ?
|
|
||||||
`, [Number(limit)]);
|
|
||||||
|
|
||||||
res.json({
|
|
||||||
success: true,
|
|
||||||
data: pluginStats
|
|
||||||
});
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('获取插件统计失败:', error);
|
|
||||||
res.status(500).json({
|
|
||||||
success: false,
|
|
||||||
error: '获取插件统计失败',
|
|
||||||
message: error instanceof Error ? error.message : String(error)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取版本使用统计
|
|
||||||
*/
|
|
||||||
statsRoutes.get('/versions', async (req: Request, res: Response) => {
|
|
||||||
try {
|
|
||||||
const { pluginId, days = 30 } = req.query;
|
|
||||||
|
|
||||||
let sql = `
|
|
||||||
SELECT
|
|
||||||
v.id,
|
|
||||||
v.version,
|
|
||||||
v.channel,
|
|
||||||
v.release_date,
|
|
||||||
p.display_name as plugin_name,
|
|
||||||
COALESCE(us.user_count, 0) as user_count,
|
|
||||||
COALESCE(us.success_count, 0) as success_count,
|
|
||||||
COALESCE(us.failure_count, 0) as failure_count,
|
|
||||||
CASE
|
|
||||||
WHEN us.user_count > 0
|
|
||||||
THEN ROUND((us.success_count * 1.0 / us.user_count) * 100, 2)
|
|
||||||
ELSE 0
|
|
||||||
END as success_rate
|
|
||||||
FROM versions v
|
|
||||||
LEFT JOIN plugins p ON v.plugin_id = p.id
|
|
||||||
LEFT JOIN update_stats us ON v.plugin_id = us.plugin_id AND v.version = us.version
|
|
||||||
WHERE v.status = 'active'
|
|
||||||
AND v.release_date >= date('now', '-' || ? || ' days')
|
|
||||||
`;
|
|
||||||
const params: any[] = [Number(days)];
|
|
||||||
|
|
||||||
if (pluginId) {
|
|
||||||
sql += ' AND v.plugin_id = ?';
|
|
||||||
params.push(pluginId);
|
|
||||||
}
|
|
||||||
|
|
||||||
sql += ' ORDER BY v.release_date DESC';
|
|
||||||
|
|
||||||
const versionStats = await query(sql, params);
|
|
||||||
|
|
||||||
res.json({
|
|
||||||
success: true,
|
|
||||||
data: versionStats
|
|
||||||
});
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('获取版本统计失败:', error);
|
|
||||||
res.status(500).json({
|
|
||||||
success: false,
|
|
||||||
error: '获取版本统计失败',
|
|
||||||
message: error instanceof Error ? error.message : String(error)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取趋势统计
|
|
||||||
*/
|
|
||||||
statsRoutes.get('/trends', async (req: Request, res: Response) => {
|
|
||||||
try {
|
|
||||||
const { period = 'week' } = req.query;
|
|
||||||
|
|
||||||
let dateFormat = '%Y-%m-%d';
|
|
||||||
let dateInterval = '7 days';
|
|
||||||
|
|
||||||
if (period === 'month') {
|
|
||||||
dateFormat = '%Y-%m';
|
|
||||||
dateInterval = '30 days';
|
|
||||||
} else if (period === 'year') {
|
|
||||||
dateFormat = '%Y';
|
|
||||||
dateInterval = '365 days';
|
|
||||||
}
|
|
||||||
|
|
||||||
// 版本发布趋势
|
|
||||||
const versionTrends = await query(`
|
|
||||||
SELECT
|
|
||||||
strftime(?, release_date) as period,
|
|
||||||
COUNT(*) as version_count,
|
|
||||||
COUNT(CASE WHEN channel = 'stable' THEN 1 END) as stable_count,
|
|
||||||
COUNT(CASE WHEN channel = 'beta' THEN 1 END) as beta_count
|
|
||||||
FROM versions
|
|
||||||
WHERE release_date >= date('now', '-' || ? || '')
|
|
||||||
AND status = 'active'
|
|
||||||
GROUP BY strftime(?, release_date)
|
|
||||||
ORDER BY period DESC
|
|
||||||
LIMIT 20
|
|
||||||
`, [dateFormat, dateInterval, dateFormat]);
|
|
||||||
|
|
||||||
// 更新使用趋势
|
|
||||||
const updateTrends = await query(`
|
|
||||||
SELECT
|
|
||||||
strftime(?, last_updated) as period,
|
|
||||||
SUM(user_count) as total_users,
|
|
||||||
SUM(success_count) as total_success,
|
|
||||||
SUM(failure_count) as total_failure
|
|
||||||
FROM update_stats
|
|
||||||
WHERE last_updated >= date('now', '-' || ? || '')
|
|
||||||
GROUP BY strftime(?, last_updated)
|
|
||||||
ORDER BY period DESC
|
|
||||||
LIMIT 20
|
|
||||||
`, [dateFormat, dateInterval, dateFormat]);
|
|
||||||
|
|
||||||
res.json({
|
|
||||||
success: true,
|
|
||||||
data: {
|
|
||||||
versions: versionTrends,
|
|
||||||
updates: updateTrends
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('获取趋势统计失败:', error);
|
|
||||||
res.status(500).json({
|
|
||||||
success: false,
|
|
||||||
error: '获取趋势统计失败',
|
|
||||||
message: error instanceof Error ? error.message : String(error)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 记录更新成功
|
|
||||||
*/
|
|
||||||
statsRoutes.post('/success', async (req: Request, res: Response) => {
|
|
||||||
try {
|
|
||||||
const { pluginId, version } = req.body;
|
|
||||||
|
|
||||||
if (!pluginId || !version) {
|
|
||||||
return res.status(400).json({
|
|
||||||
success: false,
|
|
||||||
error: '缺少必要参数: pluginId, version'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
await execute(`
|
|
||||||
UPDATE update_stats
|
|
||||||
SET success_count = success_count + 1, last_updated = ?
|
|
||||||
WHERE plugin_id = ? AND version = ?
|
|
||||||
`, [new Date().toISOString(), pluginId, version]);
|
|
||||||
|
|
||||||
res.json({
|
|
||||||
success: true,
|
|
||||||
message: '更新成功记录已保存'
|
|
||||||
});
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('记录更新成功失败:', error);
|
|
||||||
res.status(500).json({
|
|
||||||
success: false,
|
|
||||||
error: '记录更新成功失败',
|
|
||||||
message: error instanceof Error ? error.message : String(error)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 记录更新失败
|
|
||||||
*/
|
|
||||||
statsRoutes.post('/failure', async (req: Request, res: Response) => {
|
|
||||||
try {
|
|
||||||
const { pluginId, version, error: errorMessage } = req.body;
|
|
||||||
|
|
||||||
if (!pluginId || !version) {
|
|
||||||
return res.status(400).json({
|
|
||||||
success: false,
|
|
||||||
error: '缺少必要参数: pluginId, version'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
await execute(`
|
|
||||||
UPDATE update_stats
|
|
||||||
SET failure_count = failure_count + 1, last_updated = ?
|
|
||||||
WHERE plugin_id = ? AND version = ?
|
|
||||||
`, [new Date().toISOString(), pluginId, version]);
|
|
||||||
|
|
||||||
console.log(`❌ 更新失败记录: ${pluginId} v${version} - ${errorMessage}`);
|
|
||||||
|
|
||||||
res.json({
|
|
||||||
success: true,
|
|
||||||
message: '更新失败记录已保存'
|
|
||||||
});
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('记录更新失败失败:', error);
|
|
||||||
res.status(500).json({
|
|
||||||
success: false,
|
|
||||||
error: '记录更新失败失败',
|
|
||||||
message: error instanceof Error ? error.message : String(error)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
@@ -1,251 +0,0 @@
|
|||||||
import { Router, Request, Response } from 'express';
|
|
||||||
import * as multer from 'multer';
|
|
||||||
import * as path from 'path';
|
|
||||||
import * as fs from 'fs-extra';
|
|
||||||
import * as crypto from 'crypto';
|
|
||||||
import * as AdmZip from 'adm-zip';
|
|
||||||
import { execute, query } from '../database';
|
|
||||||
|
|
||||||
interface MulterRequest extends Request {
|
|
||||||
file?: any;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const uploadRoutes = Router();
|
|
||||||
|
|
||||||
// 配置multer用于文件上传
|
|
||||||
const storage = multer.diskStorage({
|
|
||||||
destination: async (req, file, cb) => {
|
|
||||||
const uploadDir = path.join(__dirname, '../../uploads/packages');
|
|
||||||
await fs.ensureDir(uploadDir);
|
|
||||||
cb(null, uploadDir);
|
|
||||||
},
|
|
||||||
filename: (req, file, cb) => {
|
|
||||||
// 生成唯一文件名
|
|
||||||
const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9);
|
|
||||||
const ext = path.extname(file.originalname);
|
|
||||||
cb(null, `${file.fieldname}-${uniqueSuffix}${ext}`);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const upload = multer({
|
|
||||||
storage: storage,
|
|
||||||
limits: {
|
|
||||||
fileSize: 100 * 1024 * 1024 // 100MB限制
|
|
||||||
},
|
|
||||||
fileFilter: (req, file, cb) => {
|
|
||||||
if (file.mimetype === 'application/zip' || path.extname(file.originalname).toLowerCase() === '.zip') {
|
|
||||||
cb(null, true);
|
|
||||||
} else {
|
|
||||||
cb(new Error('只允许上传ZIP文件'));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 上传插件包
|
|
||||||
*/
|
|
||||||
uploadRoutes.post('/package', upload.single('package'), async (req: MulterRequest, res: Response) => {
|
|
||||||
try {
|
|
||||||
if (!req.file) {
|
|
||||||
return res.status(400).json({
|
|
||||||
success: false,
|
|
||||||
error: '未选择文件'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const { pluginId, version, channel, description, mandatory } = req.body;
|
|
||||||
|
|
||||||
if (!pluginId || !version || !channel) {
|
|
||||||
// 删除已上传的文件
|
|
||||||
await fs.remove(req.file.path);
|
|
||||||
return res.status(400).json({
|
|
||||||
success: false,
|
|
||||||
error: '缺少必要参数: pluginId, version, channel'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`📦 上传插件包: ${pluginId} v${version} (${channel})`);
|
|
||||||
|
|
||||||
// 计算文件哈希
|
|
||||||
const fileHash = await calculateFileHash(req.file.path);
|
|
||||||
|
|
||||||
// 分析ZIP文件内容
|
|
||||||
const zipAnalysis = await analyzeZipFile(req.file.path);
|
|
||||||
|
|
||||||
// 生成下载URL
|
|
||||||
const downloadUrl = `/uploads/packages/${req.file.filename}`;
|
|
||||||
|
|
||||||
// 创建版本记录
|
|
||||||
const result = await execute(`
|
|
||||||
INSERT INTO versions
|
|
||||||
(plugin_id, version, channel, description, download_url, file_size, checksum, mandatory, release_date)
|
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
||||||
`, [
|
|
||||||
pluginId,
|
|
||||||
version,
|
|
||||||
channel,
|
|
||||||
description || '',
|
|
||||||
downloadUrl,
|
|
||||||
req.file.size,
|
|
||||||
fileHash,
|
|
||||||
mandatory === 'true' ? 1 : 0,
|
|
||||||
new Date().toISOString()
|
|
||||||
]);
|
|
||||||
|
|
||||||
const versionId = result.lastID;
|
|
||||||
|
|
||||||
// 添加文件记录
|
|
||||||
for (const file of zipAnalysis.files) {
|
|
||||||
await execute(`
|
|
||||||
INSERT INTO version_files (version_id, file_path, file_hash, file_size, action)
|
|
||||||
VALUES (?, ?, ?, ?, ?)
|
|
||||||
`, [versionId, file.path, file.hash, file.size, 'update']);
|
|
||||||
}
|
|
||||||
|
|
||||||
res.json({
|
|
||||||
success: true,
|
|
||||||
data: {
|
|
||||||
id: versionId,
|
|
||||||
pluginId,
|
|
||||||
version,
|
|
||||||
channel,
|
|
||||||
description: description || '',
|
|
||||||
downloadUrl,
|
|
||||||
fileSize: req.file.size,
|
|
||||||
checksum: fileHash,
|
|
||||||
mandatory: mandatory === 'true',
|
|
||||||
filesCount: zipAnalysis.files.length,
|
|
||||||
uploadedAt: new Date().toISOString()
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
// 清理文件
|
|
||||||
if (req.file) {
|
|
||||||
await fs.remove(req.file.path).catch(() => {});
|
|
||||||
}
|
|
||||||
|
|
||||||
console.error('上传失败:', error);
|
|
||||||
res.status(500).json({
|
|
||||||
success: false,
|
|
||||||
error: '上传失败',
|
|
||||||
message: error instanceof Error ? error.message : String(error)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 上传进度查询
|
|
||||||
*/
|
|
||||||
uploadRoutes.get('/progress/:id', async (req: Request, res: Response) => {
|
|
||||||
try {
|
|
||||||
const { id } = req.params;
|
|
||||||
|
|
||||||
// 这里可以实现真正的进度追踪
|
|
||||||
// 目前返回完成状态
|
|
||||||
res.json({
|
|
||||||
success: true,
|
|
||||||
data: {
|
|
||||||
id,
|
|
||||||
progress: 100,
|
|
||||||
status: 'completed'
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('查询上传进度失败:', error);
|
|
||||||
res.status(500).json({
|
|
||||||
success: false,
|
|
||||||
error: '查询上传进度失败',
|
|
||||||
message: error instanceof Error ? error.message : String(error)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取上传历史
|
|
||||||
*/
|
|
||||||
uploadRoutes.get('/history', async (req: Request, res: Response) => {
|
|
||||||
try {
|
|
||||||
const { pluginId, limit = 10 } = req.query;
|
|
||||||
|
|
||||||
let sql = `
|
|
||||||
SELECT v.*, p.display_name as plugin_display_name, p.icon as plugin_icon
|
|
||||||
FROM versions v
|
|
||||||
LEFT JOIN plugins p ON v.plugin_id = p.id
|
|
||||||
WHERE 1=1
|
|
||||||
`;
|
|
||||||
const params: any[] = [];
|
|
||||||
|
|
||||||
if (pluginId) {
|
|
||||||
sql += ' AND v.plugin_id = ?';
|
|
||||||
params.push(pluginId);
|
|
||||||
}
|
|
||||||
|
|
||||||
sql += ` ORDER BY v.created_at DESC LIMIT ?`;
|
|
||||||
params.push(Number(limit));
|
|
||||||
|
|
||||||
const uploads = await query(sql, params);
|
|
||||||
|
|
||||||
res.json({
|
|
||||||
success: true,
|
|
||||||
data: uploads.map(u => ({
|
|
||||||
id: u.id,
|
|
||||||
pluginId: u.plugin_id,
|
|
||||||
version: u.version,
|
|
||||||
channel: u.channel,
|
|
||||||
fileSize: u.file_size,
|
|
||||||
uploadedAt: u.created_at,
|
|
||||||
pluginDisplayName: u.plugin_display_name,
|
|
||||||
pluginIcon: u.plugin_icon
|
|
||||||
}))
|
|
||||||
});
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('获取上传历史失败:', error);
|
|
||||||
res.status(500).json({
|
|
||||||
success: false,
|
|
||||||
error: '获取上传历史失败',
|
|
||||||
message: error instanceof Error ? error.message : String(error)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 计算文件哈希值
|
|
||||||
*/
|
|
||||||
async function calculateFileHash(filePath: string): Promise<string> {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const hash = crypto.createHash('sha256');
|
|
||||||
const stream = fs.createReadStream(filePath);
|
|
||||||
|
|
||||||
stream.on('data', data => hash.update(data));
|
|
||||||
stream.on('end', () => resolve(hash.digest('hex')));
|
|
||||||
stream.on('error', reject);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 分析ZIP文件内容
|
|
||||||
*/
|
|
||||||
async function analyzeZipFile(filePath: string): Promise<{ files: Array<{ path: string; hash: string; size: number }> }> {
|
|
||||||
const zip = new AdmZip(filePath);
|
|
||||||
const entries = zip.getEntries();
|
|
||||||
|
|
||||||
const files: Array<{ path: string; hash: string; size: number }> = [];
|
|
||||||
|
|
||||||
for (const entry of entries) {
|
|
||||||
if (!entry.isDirectory) {
|
|
||||||
const content = entry.getData();
|
|
||||||
const hash = crypto.createHash('sha256').update(content).digest('hex');
|
|
||||||
|
|
||||||
files.push({
|
|
||||||
path: entry.entryName,
|
|
||||||
hash: hash,
|
|
||||||
size: entry.header.size
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return { files };
|
|
||||||
}
|
|
||||||
@@ -1,348 +0,0 @@
|
|||||||
import { Router, Request, Response } from 'express';
|
|
||||||
import { query, queryOne, execute } from '../database';
|
|
||||||
|
|
||||||
export const versionRoutes = Router();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取所有版本列表
|
|
||||||
*/
|
|
||||||
versionRoutes.get('/', async (req: Request, res: Response) => {
|
|
||||||
try {
|
|
||||||
const { pluginId, channel, limit = 50 } = req.query;
|
|
||||||
|
|
||||||
let sql = `
|
|
||||||
SELECT v.*, p.display_name as plugin_display_name, p.icon as plugin_icon
|
|
||||||
FROM versions v
|
|
||||||
LEFT JOIN plugins p ON v.plugin_id = p.id
|
|
||||||
WHERE v.status = 'active'
|
|
||||||
`;
|
|
||||||
const params: any[] = [];
|
|
||||||
|
|
||||||
if (pluginId) {
|
|
||||||
sql += ' AND v.plugin_id = ?';
|
|
||||||
params.push(pluginId);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (channel) {
|
|
||||||
sql += ' AND v.channel = ?';
|
|
||||||
params.push(channel);
|
|
||||||
}
|
|
||||||
|
|
||||||
sql += ` ORDER BY v.created_at DESC LIMIT ?`;
|
|
||||||
params.push(Number(limit));
|
|
||||||
|
|
||||||
const versions = await query(sql, params);
|
|
||||||
|
|
||||||
res.json({
|
|
||||||
success: true,
|
|
||||||
data: versions
|
|
||||||
});
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('获取版本列表失败:', error);
|
|
||||||
res.status(500).json({
|
|
||||||
success: false,
|
|
||||||
error: '获取版本列表失败',
|
|
||||||
message: error instanceof Error ? error.message : String(error)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取特定版本详情
|
|
||||||
*/
|
|
||||||
versionRoutes.get('/:id', async (req: Request, res: Response) => {
|
|
||||||
try {
|
|
||||||
const { id } = req.params;
|
|
||||||
|
|
||||||
const version = await query(`
|
|
||||||
SELECT v.*, p.display_name as plugin_display_name, p.icon as plugin_icon
|
|
||||||
FROM versions v
|
|
||||||
LEFT JOIN plugins p ON v.plugin_id = p.id
|
|
||||||
WHERE v.id = ? AND v.status = 'active'
|
|
||||||
`, [id]);
|
|
||||||
|
|
||||||
if (version.length === 0) {
|
|
||||||
return res.status(404).json({
|
|
||||||
success: false,
|
|
||||||
error: '版本不存在'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取版本文件列表
|
|
||||||
const files = await query(`
|
|
||||||
SELECT * FROM version_files WHERE version_id = ?
|
|
||||||
`, [id]);
|
|
||||||
|
|
||||||
res.json({
|
|
||||||
success: true,
|
|
||||||
data: {
|
|
||||||
...version[0],
|
|
||||||
files
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('获取版本详情失败:', error);
|
|
||||||
res.status(500).json({
|
|
||||||
success: false,
|
|
||||||
error: '获取版本详情失败',
|
|
||||||
message: error instanceof Error ? error.message : String(error)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 删除版本
|
|
||||||
*/
|
|
||||||
versionRoutes.delete('/:id', async (req: Request, res: Response) => {
|
|
||||||
try {
|
|
||||||
const { id } = req.params;
|
|
||||||
|
|
||||||
// 软删除
|
|
||||||
await execute(`
|
|
||||||
UPDATE versions SET status = 'deleted', updated_at = CURRENT_TIMESTAMP
|
|
||||||
WHERE id = ?
|
|
||||||
`, [id]);
|
|
||||||
|
|
||||||
res.json({
|
|
||||||
success: true,
|
|
||||||
message: '版本删除成功'
|
|
||||||
});
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('删除版本失败:', error);
|
|
||||||
res.status(500).json({
|
|
||||||
success: false,
|
|
||||||
error: '删除版本失败',
|
|
||||||
message: error instanceof Error ? error.message : String(error)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 更新版本状态
|
|
||||||
*/
|
|
||||||
versionRoutes.patch('/:id/status', async (req: Request, res: Response) => {
|
|
||||||
try {
|
|
||||||
const { id } = req.params;
|
|
||||||
const { status } = req.body;
|
|
||||||
|
|
||||||
if (!['active', 'inactive', 'deleted'].includes(status)) {
|
|
||||||
return res.status(400).json({
|
|
||||||
success: false,
|
|
||||||
error: '无效的状态值'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
await execute(`
|
|
||||||
UPDATE versions SET status = ?, updated_at = CURRENT_TIMESTAMP
|
|
||||||
WHERE id = ?
|
|
||||||
`, [status, id]);
|
|
||||||
|
|
||||||
res.json({
|
|
||||||
success: true,
|
|
||||||
message: '状态更新成功'
|
|
||||||
});
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('更新版本状态失败:', error);
|
|
||||||
res.status(500).json({
|
|
||||||
success: false,
|
|
||||||
error: '更新版本状态失败',
|
|
||||||
message: error instanceof Error ? error.message : String(error)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 检查更新 (供客户端使用)
|
|
||||||
*/
|
|
||||||
versionRoutes.post('/check', async (req: Request, res: Response) => {
|
|
||||||
try {
|
|
||||||
const { currentVersion, pluginId = 'cocos-ecs-extension', channel = 'stable', platform, editorVersion } = req.body;
|
|
||||||
|
|
||||||
console.log(`📱 插件更新检查: ${pluginId} v${currentVersion} (${channel}) - ${platform}`);
|
|
||||||
|
|
||||||
// 查找最新版本
|
|
||||||
const latestVersion = await queryOne(`
|
|
||||||
SELECT * FROM versions
|
|
||||||
WHERE plugin_id = ? AND channel = ? AND status = 'active'
|
|
||||||
ORDER BY created_at DESC
|
|
||||||
LIMIT 1
|
|
||||||
`, [pluginId, channel]);
|
|
||||||
|
|
||||||
if (!latestVersion) {
|
|
||||||
return res.json({
|
|
||||||
hasUpdate: false,
|
|
||||||
message: `暂无 ${pluginId} 的 ${channel} 版本`,
|
|
||||||
currentVersion
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 比较版本号
|
|
||||||
const hasUpdate = isNewerVersion(latestVersion.version, currentVersion);
|
|
||||||
|
|
||||||
if (hasUpdate) {
|
|
||||||
// 记录更新检查
|
|
||||||
await execute(`
|
|
||||||
INSERT OR REPLACE INTO update_stats (plugin_id, version, user_count, last_updated)
|
|
||||||
VALUES (?, ?, COALESCE((SELECT user_count FROM update_stats WHERE plugin_id = ? AND version = ?), 0) + 1, ?)
|
|
||||||
`, [pluginId, latestVersion.version, pluginId, latestVersion.version, new Date().toISOString()]);
|
|
||||||
|
|
||||||
// 获取文件列表
|
|
||||||
const files = await query(`
|
|
||||||
SELECT file_path, file_hash, file_size, action
|
|
||||||
FROM version_files
|
|
||||||
WHERE version_id = ?
|
|
||||||
`, [latestVersion.id]);
|
|
||||||
|
|
||||||
res.json({
|
|
||||||
hasUpdate: true,
|
|
||||||
version: latestVersion.version,
|
|
||||||
releaseDate: latestVersion.release_date,
|
|
||||||
description: latestVersion.description,
|
|
||||||
downloadUrl: latestVersion.download_url,
|
|
||||||
fileSize: latestVersion.file_size,
|
|
||||||
checksum: latestVersion.checksum,
|
|
||||||
mandatory: latestVersion.mandatory === 1,
|
|
||||||
files: files.map(f => ({
|
|
||||||
path: f.file_path,
|
|
||||||
hash: f.file_hash,
|
|
||||||
size: f.file_size,
|
|
||||||
action: f.action
|
|
||||||
}))
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
res.json({
|
|
||||||
hasUpdate: false,
|
|
||||||
message: `${pluginId} 当前已是最新版本`,
|
|
||||||
currentVersion
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('检查更新失败:', error);
|
|
||||||
res.status(500).json({
|
|
||||||
hasUpdate: false,
|
|
||||||
error: '检查更新失败',
|
|
||||||
message: error instanceof Error ? error.message : String(error)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 创建新版本
|
|
||||||
*/
|
|
||||||
versionRoutes.post('/', async (req: Request, res: Response) => {
|
|
||||||
try {
|
|
||||||
const { pluginId, version, channel, description, downloadUrl, fileSize, checksum, mandatory, files } = req.body;
|
|
||||||
|
|
||||||
// 检查版本是否已存在
|
|
||||||
const existingVersion = await queryOne(`
|
|
||||||
SELECT id FROM versions
|
|
||||||
WHERE plugin_id = ? AND version = ? AND channel = ?
|
|
||||||
`, [pluginId, version, channel]);
|
|
||||||
|
|
||||||
if (existingVersion) {
|
|
||||||
return res.status(400).json({
|
|
||||||
success: false,
|
|
||||||
error: '该版本已存在'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 创建版本记录
|
|
||||||
const result = await execute(`
|
|
||||||
INSERT INTO versions
|
|
||||||
(plugin_id, version, channel, description, download_url, file_size, checksum, mandatory, release_date)
|
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
||||||
`, [pluginId, version, channel, description, downloadUrl, fileSize, checksum, mandatory ? 1 : 0, new Date().toISOString()]);
|
|
||||||
|
|
||||||
const versionId = result.lastID;
|
|
||||||
|
|
||||||
// 添加文件记录
|
|
||||||
if (files && files.length > 0) {
|
|
||||||
for (const file of files) {
|
|
||||||
await execute(`
|
|
||||||
INSERT INTO version_files (version_id, file_path, file_hash, file_size, action)
|
|
||||||
VALUES (?, ?, ?, ?, ?)
|
|
||||||
`, [versionId, file.path, file.hash, file.size, file.action || 'update']);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
res.json({
|
|
||||||
success: true,
|
|
||||||
data: {
|
|
||||||
id: versionId,
|
|
||||||
pluginId,
|
|
||||||
version,
|
|
||||||
channel,
|
|
||||||
description,
|
|
||||||
downloadUrl,
|
|
||||||
fileSize,
|
|
||||||
checksum,
|
|
||||||
mandatory
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('创建版本失败:', error);
|
|
||||||
res.status(500).json({
|
|
||||||
success: false,
|
|
||||||
error: '创建版本失败',
|
|
||||||
message: error instanceof Error ? error.message : String(error)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 更新版本信息
|
|
||||||
*/
|
|
||||||
versionRoutes.put('/:id', async (req: Request, res: Response) => {
|
|
||||||
try {
|
|
||||||
const { id } = req.params;
|
|
||||||
const { description, status, mandatory } = req.body;
|
|
||||||
|
|
||||||
await execute(`
|
|
||||||
UPDATE versions
|
|
||||||
SET description = ?, status = ?, mandatory = ?, updated_at = ?
|
|
||||||
WHERE id = ?
|
|
||||||
`, [description, status, mandatory ? 1 : 0, new Date().toISOString(), id]);
|
|
||||||
|
|
||||||
res.json({
|
|
||||||
success: true,
|
|
||||||
message: '版本信息更新成功'
|
|
||||||
});
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('更新版本失败:', error);
|
|
||||||
res.status(500).json({
|
|
||||||
success: false,
|
|
||||||
error: '更新版本失败',
|
|
||||||
message: error instanceof Error ? error.message : String(error)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 比较版本号
|
|
||||||
*/
|
|
||||||
function isNewerVersion(newVersion: string, currentVersion: string): boolean {
|
|
||||||
const parseVersion = (version: string) => {
|
|
||||||
return version.split('.').map(Number);
|
|
||||||
};
|
|
||||||
|
|
||||||
const newParts = parseVersion(newVersion);
|
|
||||||
const currentParts = parseVersion(currentVersion);
|
|
||||||
const maxLength = Math.max(newParts.length, currentParts.length);
|
|
||||||
|
|
||||||
for (let i = 0; i < maxLength; i++) {
|
|
||||||
const newPart = newParts[i] || 0;
|
|
||||||
const currentPart = currentParts[i] || 0;
|
|
||||||
|
|
||||||
if (newPart > currentPart) return true;
|
|
||||||
if (newPart < currentPart) return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
@@ -1,91 +0,0 @@
|
|||||||
import * as express from 'express';
|
|
||||||
import { Request, Response, NextFunction } from 'express';
|
|
||||||
import * as cors from 'cors';
|
|
||||||
import * as path from 'path';
|
|
||||||
import { authRoutes } from './routes/auth';
|
|
||||||
import { uploadRoutes } from './routes/upload';
|
|
||||||
import { versionRoutes } from './routes/versions';
|
|
||||||
import { configRoutes } from './routes/config';
|
|
||||||
import { statsRoutes } from './routes/stats';
|
|
||||||
import { initDatabase } from './database';
|
|
||||||
|
|
||||||
const app = express();
|
|
||||||
const PORT = process.env.PORT || 3001;
|
|
||||||
|
|
||||||
// 中间件
|
|
||||||
app.use(cors());
|
|
||||||
app.use(express.json({ limit: '100mb' }));
|
|
||||||
app.use(express.urlencoded({ extended: true, limit: '100mb' }));
|
|
||||||
|
|
||||||
// 静态文件服务
|
|
||||||
app.use('/uploads', express.static(path.join(__dirname, '../uploads')));
|
|
||||||
app.use('/assets', express.static(path.join(__dirname, '../public')));
|
|
||||||
|
|
||||||
// API路由
|
|
||||||
app.use('/api/auth', authRoutes);
|
|
||||||
app.use('/api/upload', uploadRoutes);
|
|
||||||
app.use('/api/config', configRoutes);
|
|
||||||
app.use('/api/stats', statsRoutes);
|
|
||||||
|
|
||||||
// 热更新客户端API (供Cocos Creator插件使用)
|
|
||||||
app.post('/api/plugin-updates/check', (req: Request, res: Response) => {
|
|
||||||
// 检查插件更新
|
|
||||||
res.json({
|
|
||||||
message: '当前已是最新版本',
|
|
||||||
hasUpdate: false,
|
|
||||||
currentVersion: req.body?.currentVersion || '1.0.0'
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// 管理界面路由
|
|
||||||
app.get('/', (req: Request, res: Response) => {
|
|
||||||
res.sendFile(path.join(__dirname, '../public/index.html'));
|
|
||||||
});
|
|
||||||
|
|
||||||
app.get('/admin', (req: Request, res: Response) => {
|
|
||||||
res.sendFile(path.join(__dirname, '../public/index.html'));
|
|
||||||
});
|
|
||||||
|
|
||||||
// 健康检查
|
|
||||||
app.get('/health', (req: Request, res: Response) => {
|
|
||||||
res.json({
|
|
||||||
status: 'ok',
|
|
||||||
timestamp: new Date().toISOString(),
|
|
||||||
version: '1.0.0'
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// 错误处理中间件
|
|
||||||
app.use((err: any, req: Request, res: Response, next: NextFunction) => {
|
|
||||||
console.error('Error:', err);
|
|
||||||
res.status(500).json({
|
|
||||||
error: '服务器内部错误',
|
|
||||||
message: process.env.NODE_ENV === 'development' ? err.message : '请联系管理员'
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// 404处理
|
|
||||||
app.use('*', (req: Request, res: Response) => {
|
|
||||||
res.status(404).json({ error: '接口不存在' });
|
|
||||||
});
|
|
||||||
|
|
||||||
// 启动服务器
|
|
||||||
async function startServer() {
|
|
||||||
try {
|
|
||||||
// 初始化数据库
|
|
||||||
await initDatabase();
|
|
||||||
|
|
||||||
app.listen(PORT, () => {
|
|
||||||
console.log(`🚀 热更新管理后台启动成功!`);
|
|
||||||
console.log(`📍 服务地址: http://localhost:${PORT}`);
|
|
||||||
console.log(`📱 管理界面: http://localhost:${PORT}`);
|
|
||||||
console.log(`📱 管理界面(admin): http://localhost:${PORT}/admin`);
|
|
||||||
console.log(`🔗 健康检查: http://localhost:${PORT}/health`);
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
console.error('❌ 服务器启动失败:', error);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
startServer();
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
{
|
|
||||||
"compilerOptions": {
|
|
||||||
"target": "ES2020",
|
|
||||||
"module": "commonjs",
|
|
||||||
"lib": ["ES2020"],
|
|
||||||
"moduleResolution": "node",
|
|
||||||
"outDir": "./dist",
|
|
||||||
"rootDir": "./src",
|
|
||||||
"strict": true,
|
|
||||||
"esModuleInterop": true,
|
|
||||||
"allowSyntheticDefaultImports": true,
|
|
||||||
"skipLibCheck": true,
|
|
||||||
"forceConsistentCasingInFileNames": true,
|
|
||||||
"resolveJsonModule": true,
|
|
||||||
"declaration": true,
|
|
||||||
"declarationMap": true,
|
|
||||||
"sourceMap": true
|
|
||||||
},
|
|
||||||
"include": [
|
|
||||||
"src/**/*"
|
|
||||||
],
|
|
||||||
"exclude": [
|
|
||||||
"node_modules",
|
|
||||||
"dist",
|
|
||||||
"uploads"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user