const express = require('express') const router = express.Router() const { User, Order, Gallery } = require('../models') const ApiResponse = require('../utils/response') const { userAuthMiddleware } = require('../middleware/auth') const logger = require('../utils/logger') /** * 获取排行榜(公开接口,无需登录) * GET /api/user/leaderboard */ router.get('/leaderboard', async (req, res) => { try { const { limit = 50, dim } = req.query const users = await User.getLeaderboard(dim ? parseInt(dim) : null, parseInt(limit)) const data = users.map((u, i) => ({ rank: i + 1, openid: u.openid, nickname: u.nickName || '星辰旅者', avatar: u.avatarUrl || '', totalScore: u.totalScore || 0, dim1Score: u.dim1Score || 0, dim2Score: u.dim2Score || 0, dim3Score: u.dim3Score || 0, dim4Score: u.dim4Score || 0, dim5Score: u.dim5Score || 0, unlockedDims: u.unlockedDims || [1] })) return ApiResponse.success(res, data) } catch (error) { logger.error('获取排行榜失败:', error) return ApiResponse.serverError(res, error.message) } }) // 应用用户认证中间件到所有路由(排行榜已在上方公开) router.use(userAuthMiddleware) /** * 获取用户进度 * GET /api/user/progress */ router.get('/progress', async (req, res) => { try { const user = req.user if (!user) { // 新用户,返回默认进度 return ApiResponse.success(res, { isNewUser: true, data: { unlockedDims: [1], dim1Score: 0, dim2Score: 0, dim3Score: 0, dim4Score: 0, dim5Score: 0, totalScore: 0, ownedSkins: [], isAdFree: false, isSubscriber: false, subscribeExpiry: null, totalPlayTime: 0 } }) } return ApiResponse.success(res, { isNewUser: false, data: user }) } catch (error) { logger.error('获取进度失败:', error) return ApiResponse.serverError(res, error.message) } }) /** * 保存用户进度 * POST /api/user/progress */ router.post('/progress', async (req, res) => { try { const user = req.user const openid = user.openid const { dim, data, dim1Score, dim2Score, dim3Score, dim4Score, dim5Score, unlockedDims, ownedSkins, isAdFree, isSubscriber, subscribeExpiry, totalPlayTime } = req.body // 查找或创建用户(理论上已通过中间件找到) let dbUser = user // 新版接口:按维度更新 if (dim && data) { const dimData = dbUser.exploreData[dim] || {} let updatedDim = { ...dimData } if (dim === 'dim1') { updatedDim.completed = data.completed || dimData.completed || false updatedDim.bestScore = Math.max(data.bestScore || 0, dimData.bestScore || 0) updatedDim.collectedEggs = (dimData.collectedEggs || 0) + (data.newEggs || 0) updatedDim.totalLength = Math.max(data.totalLength || 0, dimData.totalLength || 0) } else if (dim === 'dim2') { updatedDim.completed = data.completed || dimData.completed || false updatedDim.bestArea = Math.max(data.bestArea || 0, dimData.bestArea || 0) updatedDim.createdShapes = (dimData.createdShapes || 0) + (data.newShapes || 0) } else if (dim === 'dim3') { updatedDim.completed = data.completed || dimData.completed || false updatedDim.exploredFaces = Math.max(data.exploredFaces || 0, dimData.exploredFaces || 0) updatedDim.rotationTime = (dimData.rotationTime || 0) + (data.rotationTime || 0) updatedDim.bestScore = Math.max(data.bestScore || 0, dimData.bestScore || 0) } else if (dim === 'dim4') { updatedDim.completed = data.completed || dimData.completed || false updatedDim.bestScore = Math.max(data.bestScore || 0, dimData.bestScore || 0) updatedDim.exploredEvents = (dimData.exploredEvents || 0) + (data.newEvents || 0) } else if (dim === 'dim5') { updatedDim.completed = data.completed || dimData.completed || false updatedDim.bestScore = Math.max(data.bestScore || 0, dimData.bestScore || 0) updatedDim.exploredThoughts = (dimData.exploredThoughts || 0) + (data.newThoughts || 0) } dbUser.exploreData[dim] = updatedDim // 同步更新已解锁维度 if (data.completed && !dbUser.unlockedDims.includes(parseInt(dim.replace('dim', '')) + 1)) { const dimNum = parseInt(dim.replace('dim', '')) if (!dbUser.unlockedDims.includes(dimNum + 1)) { dbUser.unlockedDims.push(dimNum + 1) } } if (totalPlayTime !== null && totalPlayTime !== undefined) { dbUser.totalPlayTime += totalPlayTime } } // 兼容旧版接口:直接传各维度分数 if (dim1Score !== null && dim1Score !== undefined) { dbUser.exploreData.dim1.bestScore = Math.max(dim1Score, dbUser.exploreData.dim1?.bestScore || 0) } if (dim2Score !== null && dim2Score !== undefined) { dbUser.exploreData.dim2.bestArea = Math.max(dim2Score, dbUser.exploreData.dim2?.bestArea || 0) } if (dim3Score !== null && dim3Score !== undefined) { dbUser.exploreData.dim3.bestScore = Math.max(dim3Score, dbUser.exploreData.dim3?.bestScore || 0) } if (dim4Score !== null && dim4Score !== undefined) { dbUser.exploreData.dim4 = dbUser.exploreData.dim4 || {} dbUser.exploreData.dim4.bestScore = Math.max(dim4Score, dbUser.exploreData.dim4?.bestScore || 0) } if (dim5Score !== null && dim5Score !== undefined) { dbUser.exploreData.dim5 = dbUser.exploreData.dim5 || {} dbUser.exploreData.dim5.bestScore = Math.max(dim5Score, dbUser.exploreData.dim5?.bestScore || 0) } // 更新其他字段 if (ownedSkins !== null && ownedSkins !== undefined) dbUser.ownedSkins = ownedSkins if (isAdFree !== null && isAdFree !== undefined) dbUser.isAdFree = isAdFree if (isSubscriber !== null && isSubscriber !== undefined) dbUser.isSubscriber = isSubscriber if (subscribeExpiry !== null && subscribeExpiry !== undefined) dbUser.subscribeExpiry = subscribeExpiry await dbUser.save() // 计算总分 const totalScore = dbUser.calculateTotalScore() return ApiResponse.success(res, { success: true, totalScore, exploreData: user.exploreData }) } catch (error) { logger.error('保存进度失败:', error) return ApiResponse.serverError(res, error.message) } }) /** * 获取用户成就 * GET /api/user/achievements */ router.get('/achievements', async (req, res) => { try { const user = req.user // 生成成就数据(AI 维度主题) const achievements = [ { name: '知识探索', achievements: [ { id: 'dim1_read', name: 'AI起源探索者', description: '完成 AI 起源维度的知识阅读', icon: '📜', unlocked: user.exploreData.dim1?.completed || false, date: user.exploreData.dim1?.completed ? user.updatedAt.toISOString().split('T')[0] : null }, { id: 'dim2_read', name: 'AI发展探索者', description: '完成 AI 发展维度的知识阅读', icon: '🏛', unlocked: user.exploreData.dim2?.completed || false, date: user.exploreData.dim2?.completed ? user.updatedAt.toISOString().split('T')[0] : null }, { id: 'dim3_read', name: 'AI当前探索者', description: '完成 AI 当前格局维度的知识阅读', icon: '♟', unlocked: user.exploreData.dim3?.completed || false, date: user.exploreData.dim3?.completed ? user.updatedAt.toISOString().split('T')[0] : null }, { id: 'dim4_read', name: 'AI学习探索者', description: '完成 AI 学习与工具维度的知识阅读', icon: '🧠', unlocked: user.exploreData.dim4?.completed || false, date: user.exploreData.dim4?.completed ? user.updatedAt.toISOString().split('T')[0] : null }, { id: 'dim5_read', name: 'AI趋势探索者', description: '完成 AI 趋势追踪维度的知识阅读', icon: '⚡', unlocked: user.exploreData.dim5?.completed || false, date: user.exploreData.dim5?.completed ? user.updatedAt.toISOString().split('T')[0] : null } ] }, { name: '收藏成就', achievements: [ { id: 'first_collection', name: '初次收藏', description: '收藏你的第一个 AI 知识', icon: '❤️', unlocked: user.collectedKnowledge?.length > 0, date: user.collectedKnowledge?.length > 0 ? user.updatedAt.toISOString().split('T')[0] : null }, { id: 'five_collections', name: '收藏爱好者', description: '收藏5个 AI 知识', icon: '🧡', unlocked: user.collectedKnowledge?.length >= 5, date: user.collectedKnowledge?.length >= 5 ? user.updatedAt.toISOString().split('T')[0] : null }, { id: 'ten_collections', name: '收藏大师', description: '收藏10个 AI 知识', icon: '💛', unlocked: user.collectedKnowledge?.length >= 10, date: user.collectedKnowledge?.length >= 10 ? user.updatedAt.toISOString().split('T')[0] : null } ] }, { name: 'AI 问答', achievements: [ { id: 'first_ask', name: '初次提问', description: '向 AI 助手提出第一个问题', icon: '🤖', unlocked: user.achievements?.includes('first_ask') || false, date: user.achievements?.includes('first_ask') ? user.updatedAt.toISOString().split('T')[0] : null }, { id: 'ten_asks', name: '勤学好问', description: '向 AI 助手提出 10 个问题', icon: '💡', unlocked: user.achievements?.includes('ten_asks') || false, date: user.achievements?.includes('ten_asks') ? user.updatedAt.toISOString().split('T')[0] : null }, { id: 'fifty_asks', name: 'AI 达人', description: '向 AI 助手提出 50 个问题', icon: '🧠', unlocked: user.achievements?.includes('fifty_asks') || false, date: user.achievements?.includes('fifty_asks') ? user.updatedAt.toISOString().split('T')[0] : null } ] }, { name: '社交成就', achievements: [ { id: 'first_share', name: '初次分享', description: '分享你的第一个知识给朋友', icon: '📤', unlocked: user.achievements?.includes('first_share') || false, date: user.achievements?.includes('first_share') ? user.updatedAt.toISOString().split('T')[0] : null }, { id: 'five_shares', name: '分享达人', description: '分享 5 次知识给朋友', icon: '📣', unlocked: user.achievements?.includes('five_shares') || false, date: user.achievements?.includes('five_shares') ? user.updatedAt.toISOString().split('T')[0] : null } ] } ] return ApiResponse.success(res, achievements) } catch (error) { logger.error('获取成就失败:', error) return ApiResponse.serverError(res, error.message) } }) /** * 获取用户统计数据 * GET /api/user/stats */ router.get('/stats', async (req, res) => { try { const user = req.user const { Gallery } = require('../models') // 获取用户作品数 const worksCount = await Gallery.countDocuments({ userId: user._id }) // 获取等级信息 const levelInfo = user.getLevelInfo() // 计算探索时长(分钟转小时,保留1位小数) const playTimeHours = Math.round((user.totalPlayTime || 0) / 60 * 10) / 10 return ApiResponse.success(res, { level: levelInfo.level, levelName: levelInfo.name, levelPoints: levelInfo.points, levelProgress: levelInfo.progress, totalProgress: levelInfo.totalProgress, nextLevelPoints: levelInfo.nextLevelPoints, totalPlayTime: playTimeHours, totalScore: user.totalScore || 0, worksCount: worksCount, collectionCount: user.collectedKnowledge?.length || 0, achievementCount: user.achievements?.length || 0, unlockedDims: user.unlockedDims || [1] }) } catch (error) { logger.error('获取用户统计失败:', error) return ApiResponse.serverError(res, error.message) } }) /** * 获取用户作品列表 * GET /api/user/works */ router.get('/works', async (req, res) => { try { const user = req.user const { page = 1, limit = 20 } = req.query const { Gallery } = require('../models') const query = { userId: user._id } const total = await Gallery.countDocuments(query) const works = await Gallery.find(query) .sort({ createdAt: -1 }) .skip((parseInt(page) - 1) * parseInt(limit)) .limit(parseInt(limit)) .lean() // 格式化作品数据 const formattedWorks = works.map(work => ({ id: work._id, title: work.title, description: work.description, imageUrl: work.imageUrl, dimension: work.dimension, status: work.status, likes: work.likes || 0, views: work.views || 0, createdAt: work.createdAt })) return ApiResponse.paginated(res, formattedWorks, { page: parseInt(page), limit: parseInt(limit), total }) } catch (error) { logger.error('获取用户作品失败:', error) return ApiResponse.serverError(res, error.message) } }) module.exports = router