remove admin-dashboard

This commit is contained in:
YHH
2025-06-19 20:49:09 +08:00
parent d29c9a96f4
commit 0107f1f58a
13 changed files with 0 additions and 5979 deletions

View File

@@ -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"
}
}

View File

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

View File

@@ -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();
}

View File

@@ -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();
});
}

View File

@@ -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)
});
}
});

View File

@@ -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)
});
}
});

View File

@@ -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)
});
}
});

View File

@@ -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 };
}

View File

@@ -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;
}

View File

@@ -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();

View File

@@ -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"
]
}