// 管理后台API处理 const { getDatabase } = require('./sql'); const fs = require('fs'); const path = require('path'); const { promisify } = require('util'); const { formidable } = require('formidable'); const RECHARGE_FILE = path.join(__dirname, 'recharge.json'); const PRODUCT_PRICING_FILE = path.join(__dirname, 'product-pricing.json'); const mkdir = promisify(fs.mkdir); const access = promisify(fs.access); const readdir = promisify(fs.readdir); const stat = promisify(fs.stat); const unlink = promisify(fs.unlink); const rmdir = promisify(fs.rmdir); // 商店资源根目录(使用 market_data 目录) const STORE_DIR = path.join(__dirname, 'market_data'); // 确保目录存在 async function ensureDir(dirPath) { try { await access(dirPath); } catch (error) { await mkdir(dirPath, { recursive: true }); } } // 递归删除目录 async function deleteDirectory(dirPath) { try { const files = await readdir(dirPath); for (const file of files) { const filePath = path.join(dirPath, file); const stats = await stat(filePath); if (stats.isDirectory()) { await deleteDirectory(filePath); } else { await unlink(filePath); } } await rmdir(dirPath); } catch (error) { console.error('[Admin] 删除目录失败:', error); throw error; } } // 获取所有用户列表 async function handleGetAllUsers(req, res) { try { console.log('[Admin] 收到获取用户列表请求'); const db = await getDatabase(); const users = db.getAllUsers(); console.log('[Admin] 从数据库获取到用户数量:', users ? users.length : 0); console.log('[Admin] 用户数据:', users); res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8', 'Access-Control-Allow-Origin': '*' // 允许跨域访问 }); res.end(JSON.stringify({ success: true, users: users || [] })); } catch (error) { console.error('[Admin] 获取用户列表失败:', error); res.writeHead(500, { 'Content-Type': 'application/json; charset=utf-8', 'Access-Control-Allow-Origin': '*' }); res.end(JSON.stringify({ success: false, message: '获取用户列表失败: ' + error.message })); } } // 更新用户信息(管理员) async function handleAdminUpdateUser(req, res) { let body = ''; req.on('data', chunk => { body += chunk.toString(); }); req.on('end', async () => { try { const data = JSON.parse(body); const { id, username, phone, points } = data; if (!id) { res.writeHead(400, { 'Content-Type': 'application/json; charset=utf-8' }); res.end(JSON.stringify({ success: false, message: '缺少用户ID参数' })); return; } const db = await getDatabase(); const user = db.findUserById(id); if (!user) { res.writeHead(404, { 'Content-Type': 'application/json; charset=utf-8' }); res.end(JSON.stringify({ success: false, message: '用户不存在' })); return; } // 构建更新对象 const updates = {}; if (username !== undefined) { updates.username = username; } if (phone !== undefined) { updates.phone = phone; } // 更新用户信息 if (Object.keys(updates).length > 0) { db.updateUser(id, updates); } // 更新点数(如果提供) if (points !== undefined && points !== null) { const currentPoints = db.getUserPoints(user.username); const diff = points - currentPoints; if (diff > 0) { db.addPoints(user.username, diff); } else if (diff < 0) { db.deductPoints(user.username, Math.abs(diff)); } } res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' }); res.end(JSON.stringify({ success: true, message: '更新成功' })); } catch (error) { console.error('[Admin] 更新用户失败:', error); res.writeHead(500, { 'Content-Type': 'application/json; charset=utf-8' }); res.end(JSON.stringify({ success: false, message: '更新失败: ' + error.message })); } }); } // 上传商店素材 async function handleAdminUploadStore(req, res) { console.log('[Admin] ========== 收到上传请求 =========='); console.log('[Admin] 请求方法:', req.method); console.log('[Admin] 请求URL:', req.url); console.log('[Admin] Content-Type:', req.headers['content-type']); const form = formidable({ uploadDir: path.join(__dirname, 'temp'), keepExtensions: true, multiples: true }); form.parse(req, async (err, fields, files) => { if (err) { console.error('[Admin] ❌ 解析上传文件失败:', err); res.writeHead(500, { 'Content-Type': 'application/json; charset=utf-8' }); res.end(JSON.stringify({ success: false, message: '上传失败: ' + err.message })); return; } console.log('[Admin] ✅ 文件解析成功'); console.log('[Admin] fields:', JSON.stringify(fields, null, 2)); console.log('[Admin] files keys:', Object.keys(files)); try { const category = Array.isArray(fields.category) ? fields.category[0] : fields.category; const name = Array.isArray(fields.name) ? fields.name[0] : fields.name; const price = parseInt(Array.isArray(fields.price) ? fields.price[0] : fields.price) || 0; console.log('[Admin] 📦 上传参数:'); console.log('[Admin] - 分类(category):', category); console.log('[Admin] - 文件夹名(name):', name); console.log('[Admin] - 价格(price):', price); if (!category || !name) { res.writeHead(400, { 'Content-Type': 'application/json; charset=utf-8' }); res.end(JSON.stringify({ success: false, message: '缺少分类或名称参数' })); return; } // 获取上传的文件 let fileList = []; if (files.files) { fileList = Array.isArray(files.files) ? files.files : [files.files]; } console.log('[Admin] 📁 收到文件数量:', fileList.length); if (fileList.length === 0) { console.log('[Admin] ❌ 文件列表为空'); res.writeHead(400, { 'Content-Type': 'application/json; charset=utf-8' }); res.end(JSON.stringify({ success: false, message: '请选择要上传的文件' })); return; } // 创建目标目录 const targetDir = path.join(STORE_DIR, category, name); console.log('[Admin] 📂 目标目录:', targetDir); console.log('[Admin] 📂 STORE_DIR:', STORE_DIR); await ensureDir(targetDir); console.log('[Admin] ✅ 目标目录已创建/确认存在'); // 移动文件到目标目录,保持目录结构 console.log('[Admin] 🔄 开始处理', fileList.length, '个文件...'); let processedCount = 0; let skippedCount = 0; for (let i = 0; i < fileList.length; i++) { const file = fileList[i]; const fileObj = Array.isArray(file) ? file[0] : file; if (!fileObj || !fileObj.filepath) { console.log(`[Admin] ⚠️ [${i+1}/${fileList.length}] 跳过无效文件对象`); skippedCount++; continue; } // 获取原始文件名(包含相对路径) const originalName = fileObj.originalFilename || path.basename(fileObj.filepath); console.log(`[Admin] 📄 [${i+1}/${fileList.length}] 处理文件:`, originalName); // 处理路径分隔符(统一使用 /) const normalizedName = originalName.replace(/\\/g, '/'); // 如果 originalName 包含文件夹名(如 player_0001/image.png),需要去掉文件夹名前缀 // 因为 targetDir 已经是 market_data/category/name 了 let relativePath = normalizedName; // 如果路径以文件夹名开头(如 player_0001/image.png),去掉文件夹名 if (normalizedName.startsWith(name + '/')) { relativePath = normalizedName.substring(name.length + 1); console.log(`[Admin] └─ 去掉文件夹前缀 "${name}/",相对路径:`, relativePath); } const targetPath = path.join(targetDir, relativePath); console.log(`[Admin] └─ 目标路径:`, targetPath); // 确保目标目录存在 await ensureDir(path.dirname(targetPath)); // 复制文件 await fs.promises.copyFile(fileObj.filepath, targetPath); console.log(`[Admin] └─ ✅ 文件已复制成功`); processedCount++; // 删除临时文件 try { await fs.promises.unlink(fileObj.filepath); } catch (err) { console.warn(`[Admin] └─ ⚠️ 删除临时文件失败:`, err.message); } } console.log('[Admin] ========== 上传处理完成 =========='); console.log('[Admin] ✅ 成功处理:', processedCount, '个文件'); if (skippedCount > 0) { console.log('[Admin] ⚠️ 跳过:', skippedCount, '个无效文件'); } console.log('[Admin] 📂 最终保存位置:', targetDir); // 更新商店资源配置(如果需要) // 这里可以添加更新商店配置文件的逻辑 res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' }); res.end(JSON.stringify({ success: true, message: '上传成功', path: `${category}/${name}`, fileCount: processedCount })); } catch (error) { console.error('[Admin] ❌ ========== 上传失败 =========='); console.error('[Admin] 错误信息:', error.message); console.error('[Admin] 错误堆栈:', error.stack); res.writeHead(500, { 'Content-Type': 'application/json; charset=utf-8' }); res.end(JSON.stringify({ success: false, message: '上传失败: ' + error.message })); } }); } // 删除商店素材 async function handleAdminDeleteStore(req, res) { let body = ''; req.on('data', chunk => { body += chunk.toString(); }); req.on('end', async () => { try { const data = JSON.parse(body); const { resourcePath } = data; console.log('[Admin] 删除请求 - resourcePath:', resourcePath); console.log('[Admin] STORE_DIR:', STORE_DIR); if (!resourcePath) { res.writeHead(400, { 'Content-Type': 'application/json; charset=utf-8' }); res.end(JSON.stringify({ success: false, message: '缺少资源路径参数' })); return; } // 构建完整路径 const fullPath = path.join(STORE_DIR, resourcePath); console.log('[Admin] 完整路径:', fullPath); // 安全检查 const normalizedPath = path.normalize(fullPath); const normalizedRoot = path.normalize(STORE_DIR); console.log('[Admin] normalizedPath:', normalizedPath); console.log('[Admin] normalizedRoot:', normalizedRoot); if (!normalizedPath.startsWith(normalizedRoot)) { console.log('[Admin] 安全检查失败'); res.writeHead(403, { 'Content-Type': 'application/json; charset=utf-8' }); res.end(JSON.stringify({ success: false, message: '访问被拒绝' })); return; } // 检查路径是否存在 try { const stats = await stat(fullPath); console.log('[Admin] 路径存在,是目录:', stats.isDirectory()); if (stats.isDirectory()) { await deleteDirectory(fullPath); } else { await unlink(fullPath); } console.log('[Admin] 删除成功'); res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' }); res.end(JSON.stringify({ success: true, message: '删除成功' })); } catch (error) { console.log('[Admin] stat 错误:', error.code, error.message); if (error.code === 'ENOENT') { res.writeHead(404, { 'Content-Type': 'application/json; charset=utf-8' }); res.end(JSON.stringify({ success: false, message: '资源不存在' })); } else { throw error; } } } catch (error) { console.error('[Admin] 删除素材失败:', error); res.writeHead(500, { 'Content-Type': 'application/json; charset=utf-8' }); res.end(JSON.stringify({ success: false, message: '删除失败: ' + error.message })); } }); } // 创建分类文件夹 async function handleAdminCreateFolder(req, res) { let body = ''; req.on('data', chunk => { body += chunk.toString(); }); req.on('end', async () => { try { const data = JSON.parse(body); const { name } = data; if (!name || !name.trim()) { res.writeHead(400, { 'Content-Type': 'application/json; charset=utf-8' }); res.end(JSON.stringify({ success: false, message: '缺少文件夹名称参数' })); return; } const folderName = name.trim(); // 验证文件夹名称 if (/[\\/:*?"<>|]/.test(folderName)) { res.writeHead(400, { 'Content-Type': 'application/json; charset=utf-8' }); res.end(JSON.stringify({ success: false, message: '文件夹名称包含非法字符' })); return; } // 创建分类文件夹(在 STORE_DIR 根目录下) const targetDir = path.join(STORE_DIR, folderName); // 检查文件夹是否已存在 try { await access(targetDir); res.writeHead(400, { 'Content-Type': 'application/json; charset=utf-8' }); res.end(JSON.stringify({ success: false, message: '文件夹已存在' })); return; } catch (error) { if (error.code !== 'ENOENT') { throw error; } // 文件夹不存在,创建它 await mkdir(targetDir, { recursive: true }); } res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' }); res.end(JSON.stringify({ success: true, message: '创建文件夹成功', path: folderName })); } catch (error) { console.error('[Admin] 创建文件夹失败:', error); res.writeHead(500, { 'Content-Type': 'application/json; charset=utf-8' }); res.end(JSON.stringify({ success: false, message: '创建文件夹失败: ' + error.message })); } }); } // 重命名商店素材 async function handleAdminRenameStore(req, res) { let body = ''; req.on('data', chunk => { body += chunk.toString(); }); req.on('end', async () => { try { const data = JSON.parse(body); const { resourcePath, newName } = data; if (!resourcePath || !newName) { res.writeHead(400, { 'Content-Type': 'application/json; charset=utf-8' }); res.end(JSON.stringify({ success: false, message: '缺少资源路径或新名称参数' })); return; } // 验证新名称 if (/[\\/:*?"<>|]/.test(newName)) { res.writeHead(400, { 'Content-Type': 'application/json; charset=utf-8' }); res.end(JSON.stringify({ success: false, message: '名称包含非法字符' })); return; } // 构建完整路径 const fullPath = path.join(STORE_DIR, resourcePath); // 安全检查 const normalizedPath = path.normalize(fullPath); const normalizedRoot = path.normalize(STORE_DIR); if (!normalizedPath.startsWith(normalizedRoot)) { res.writeHead(403, { 'Content-Type': 'application/json; charset=utf-8' }); res.end(JSON.stringify({ success: false, message: '访问被拒绝' })); return; } // 检查路径是否存在 try { await access(fullPath); } catch (error) { if (error.code === 'ENOENT') { res.writeHead(404, { 'Content-Type': 'application/json; charset=utf-8' }); res.end(JSON.stringify({ success: false, message: '资源不存在' })); return; } else { throw error; } } // 构建新路径 const parentDir = path.dirname(fullPath); const newFullPath = path.join(parentDir, newName); // 检查新名称是否已存在 try { await access(newFullPath); res.writeHead(400, { 'Content-Type': 'application/json; charset=utf-8' }); res.end(JSON.stringify({ success: false, message: '新名称已存在' })); return; } catch (error) { if (error.code !== 'ENOENT') { throw error; } } // 重命名文件或文件夹 await fs.promises.rename(fullPath, newFullPath); const newPathKey = path.relative(STORE_DIR, newFullPath).replace(/\\/g, '/'); const oldPathKey = resourcePath; // 检查是否是分类文件夹(在根目录下) const stats = await stat(newFullPath); const isCategoryFolder = stats.isDirectory() && !resourcePath.includes('/'); // 更新价格文件中的路径(如果存在) try { const pricesFilePath = path.join(STORE_DIR, 'prices.json'); const pricesData = await fs.promises.readFile(pricesFilePath, 'utf-8'); const prices = JSON.parse(pricesData); // 更新所有相关的价格路径 // 如果是分类文件夹,需要更新所有子资源的价格路径 if (isCategoryFolder) { const updatedPrices = {}; for (const [key, value] of Object.entries(prices)) { if (key.startsWith(oldPathKey + '/')) { // 更新子资源的路径 const newKey = key.replace(oldPathKey + '/', newPathKey + '/'); updatedPrices[newKey] = value; } else if (key === oldPathKey) { // 更新分类文件夹本身的价格(如果有) updatedPrices[newPathKey] = value; } else { // 保留其他路径 updatedPrices[key] = value; } } await fs.promises.writeFile(pricesFilePath, JSON.stringify(updatedPrices, null, 2), 'utf-8'); } else { // 只更新当前资源的价格路径 if (prices[oldPathKey] !== undefined) { prices[newPathKey] = prices[oldPathKey]; delete prices[oldPathKey]; await fs.promises.writeFile(pricesFilePath, JSON.stringify(prices, null, 2), 'utf-8'); } } } catch (error) { // 价格文件不存在或更新失败,不影响重命名操作 console.warn('[Admin] 更新价格文件失败:', error); } res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8', 'Access-Control-Allow-Origin': '*' }); res.end(JSON.stringify({ success: true, message: '重命名成功', newPath: newPathKey, isCategoryFolder: isCategoryFolder })); } catch (error) { console.error('[Admin] 重命名素材失败:', error); res.writeHead(500, { 'Content-Type': 'application/json; charset=utf-8' }); res.end(JSON.stringify({ success: false, message: '重命名失败: ' + error.message })); } }); } // 更新资源价格 async function handleAdminUpdatePrice(req, res) { let body = ''; req.on('data', chunk => { body += chunk.toString(); }); req.on('end', async () => { try { const data = JSON.parse(body); const { resourcePath, price } = data; if (!resourcePath || price === undefined) { res.writeHead(400, { 'Content-Type': 'application/json; charset=utf-8' }); res.end(JSON.stringify({ success: false, message: '缺少资源路径或价格参数' })); return; } // 价格数据存储在JSON文件中 const pricesFilePath = path.join(STORE_DIR, 'prices.json'); let prices = {}; try { const pricesData = await fs.promises.readFile(pricesFilePath, 'utf-8'); prices = JSON.parse(pricesData); } catch (error) { // 文件不存在,创建新的 prices = {}; } // 更新价格 prices[resourcePath] = price; // 保存到文件 await fs.promises.writeFile(pricesFilePath, JSON.stringify(prices, null, 2), 'utf-8'); // 通知 StoreManager 清除价格缓存(通过事件或直接调用) // 注意:这里 StoreManager 的缓存会在下次请求时自动清除 res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' }); res.end(JSON.stringify({ success: true, message: '价格更新成功' })); } catch (error) { console.error('[Admin] 更新价格失败:', error); res.writeHead(500, { 'Content-Type': 'application/json; charset=utf-8' }); res.end(JSON.stringify({ success: false, message: '更新失败: ' + error.message })); } }); } // 货币设置文件路径 const CURRENCY_SETTINGS_FILE = path.join(__dirname, 'currency-settings.json'); // 获取货币/充值套餐设置 async function handleGetCurrencySettings(req, res) { try { let packages = [ { points: 100, bonus: 20, price: 5 }, { points: 1000, bonus: 200, price: 50 }, { points: 10000, bonus: 800, price: 500 } ]; try { const data = await fs.promises.readFile(CURRENCY_SETTINGS_FILE, 'utf-8'); const parsed = JSON.parse(data); packages = parsed.packages || packages; } catch (err) { // 文件不存在,使用默认值 } res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' }); res.end(JSON.stringify({ success: true, packages })); } catch (error) { console.error('[Admin] 获取充值套餐失败:', error); res.writeHead(500, { 'Content-Type': 'application/json; charset=utf-8' }); res.end(JSON.stringify({ success: false, message: '获取设置失败: ' + error.message })); } } // 保存货币/充值套餐设置 async function handleSaveCurrencySettings(req, res) { let body = ''; req.on('data', chunk => { body += chunk.toString(); }); req.on('end', async () => { try { const data = JSON.parse(body); const { packages } = data; await fs.promises.writeFile(CURRENCY_SETTINGS_FILE, JSON.stringify({ packages }, null, 2), 'utf-8'); res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' }); res.end(JSON.stringify({ success: true, message: '充值套餐保存成功' })); } catch (error) { console.error('[Admin] 保存充值套餐失败:', error); res.writeHead(500, { 'Content-Type': 'application/json; charset=utf-8' }); res.end(JSON.stringify({ success: false, message: '保存失败: ' + error.message })); } }); } // 获取商品定价设置 async function handleGetProductPricingSettings(req, res) { try { let products = [ { id: 'vip-matting', name: 'VIP抠图', desc: '使用VIP服务进行图片抠图', price: 0 }, { id: 'ai-generate', name: 'AI生图', desc: '使用AI生成图片', price: 0 } ]; try { const data = await fs.promises.readFile(PRODUCT_PRICING_FILE, 'utf-8'); const parsed = JSON.parse(data); if (parsed.products && Array.isArray(parsed.products)) { // 合并服务器保存的价格 parsed.products.forEach(serverProduct => { const localProduct = products.find(p => p.id === serverProduct.id); if (localProduct) { localProduct.price = serverProduct.price || 0; } }); } } catch (err) { // 文件不存在,使用默认值 } res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' }); res.end(JSON.stringify({ success: true, products })); } catch (error) { console.error('[Admin] 获取商品定价失败:', error); res.writeHead(500, { 'Content-Type': 'application/json; charset=utf-8' }); res.end(JSON.stringify({ success: false, message: '获取设置失败: ' + error.message })); } } // 保存商品定价设置 async function handleSaveProductPricingSettings(req, res) { let body = ''; req.on('data', chunk => { body += chunk.toString(); }); req.on('end', async () => { try { const data = JSON.parse(body); const { productId, price } = data; if (!productId || price === undefined) { res.writeHead(400, { 'Content-Type': 'application/json; charset=utf-8' }); res.end(JSON.stringify({ success: false, message: '缺少必要参数' })); return; } // 读取现有设置 let products = [ { id: 'vip-matting', name: 'VIP抠图', desc: '使用VIP服务进行图片抠图', price: 0 }, { id: 'ai-generate', name: 'AI生图', desc: '使用AI生成图片', price: 0 } ]; try { const fileData = await fs.promises.readFile(PRODUCT_PRICING_FILE, 'utf-8'); const parsed = JSON.parse(fileData); if (parsed.products && Array.isArray(parsed.products)) { products = parsed.products; } } catch (err) { // 文件不存在,使用默认值 } // 更新指定商品的价格 const product = products.find(p => p.id === productId); if (product) { product.price = parseFloat(price) || 0; } else { res.writeHead(404, { 'Content-Type': 'application/json; charset=utf-8' }); res.end(JSON.stringify({ success: false, message: '商品不存在' })); return; } await fs.promises.writeFile(PRODUCT_PRICING_FILE, JSON.stringify({ products }, null, 2), 'utf-8'); res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' }); res.end(JSON.stringify({ success: true, message: '价格保存成功' })); } catch (error) { console.error('[Admin] 保存商品定价失败:', error); res.writeHead(500, { 'Content-Type': 'application/json; charset=utf-8' }); res.end(JSON.stringify({ success: false, message: '保存失败: ' + error.message })); } }); } module.exports = { handleGetAllUsers, handleAdminUpdateUser, handleAdminUploadStore, handleAdminDeleteStore, handleAdminCreateFolder, handleAdminRenameStore, handleAdminUpdatePrice, handleGetCurrencySettings, handleSaveCurrencySettings, handleGetProductPricingSettings, handleSaveProductPricingSettings };