diff --git a/client/index.html b/client/index.html index ce9167d..1d496ab 100644 --- a/client/index.html +++ b/client/index.html @@ -1,6 +1,8 @@
+ + diff --git a/client/src/pages.json b/client/src/pages.json index 7279d30..553a322 100644 --- a/client/src/pages.json +++ b/client/src/pages.json @@ -3,7 +3,7 @@ { "path": "pages/index/index", "style": { - "navigationBarTitleText": "AI 维度", + "navigationBarTitleText": "宇之然AI维度-AI知识学习与人工智能资讯", "navigationStyle": "custom", "enablePullDownRefresh": true, "backgroundTextStyle": "dark" @@ -19,7 +19,7 @@ { "path": "pages/detail/detail", "style": { - "navigationBarTitleText": "AI 知识", + "navigationBarTitleText": "AI知识详情-人工智能知识点", "navigationStyle": "custom" } }, @@ -33,21 +33,21 @@ { "path": "pages/favorites/favorites", "style": { - "navigationBarTitleText": "我的收藏", + "navigationBarTitleText": "我的AI知识收藏", "navigationStyle": "custom" } }, { "path": "pages/achievements/achievements", "style": { - "navigationBarTitleText": "成就", + "navigationBarTitleText": "我的AI学习成就", "navigationStyle": "custom" } }, { "path": "pages/leaderboard/leaderboard", "style": { - "navigationBarTitleText": "排行榜", + "navigationBarTitleText": "AI学习排行榜", "navigationStyle": "custom" } }, @@ -82,7 +82,7 @@ { "path": "pages/news/news", "style": { - "navigationBarTitleText": "AI 资讯", + "navigationBarTitleText": "AI资讯速递-人工智能热点解读", "navigationStyle": "custom", "enablePullDownRefresh": true, "backgroundTextStyle": "dark" @@ -91,21 +91,21 @@ { "path": "pages/dimension/dimension", "style": { - "navigationBarTitleText": "", + "navigationBarTitleText": "AI知识星域-五大维度学人工智能", "navigationStyle": "custom" } }, { "path": "pages/chat/chat", "style": { - "navigationBarTitleText": "AI 问答", + "navigationBarTitleText": "AI问答-人工智能知识解答", "navigationStyle": "custom" } }, { "path": "pages/trend/trend", "style": { - "navigationBarTitleText": "AI 趋势", + "navigationBarTitleText": "AI趋势-每日人工智能行业动态", "navigationStyle": "custom", "enablePullDownRefresh": true, "backgroundTextStyle": "dark" diff --git a/client/src/pages/agreement/agreement.vue b/client/src/pages/agreement/agreement.vue index 3fda98e..207820b 100644 --- a/client/src/pages/agreement/agreement.vue +++ b/client/src/pages/agreement/agreement.vue @@ -118,7 +118,7 @@ function onLoad(fn) { // 微信小程序分享 // #ifdef MP-WEIXIN onShareAppMessage(() => ({ - title: 'AI 维度 - 你的AI知识助手', + title: 'AI知识社区 · 宇之然AI维度', path: '/pages/index/index' })) // #endif diff --git a/client/src/pages/feedback/feedback.vue b/client/src/pages/feedback/feedback.vue index b62a225..03f2cb3 100644 --- a/client/src/pages/feedback/feedback.vue +++ b/client/src/pages/feedback/feedback.vue @@ -232,7 +232,7 @@ function goBack() { // 微信小程序分享 // #ifdef MP-WEIXIN onShareAppMessage(() => ({ - title: 'AI 维度 - 你的AI知识助手', + title: '帮AI维度变得更好 · 意见反馈', path: '/pages/index/index' })) // #endif diff --git a/client/src/pages/leaderboard/leaderboard.vue b/client/src/pages/leaderboard/leaderboard.vue index 2d67e04..5a32b37 100644 --- a/client/src/pages/leaderboard/leaderboard.vue +++ b/client/src/pages/leaderboard/leaderboard.vue @@ -138,7 +138,7 @@ function dimDotColor(id) { // 微信小程序分享 // #ifdef MP-WEIXIN onShareAppMessage(() => ({ - title: 'AI 维度 - 你的AI知识助手', + title: 'AI学习排行榜 · 宇之然AI维度', path: '/pages/index/index' })) // #endif diff --git a/client/src/pages/login/login.vue b/client/src/pages/login/login.vue index 291112e..405bace 100644 --- a/client/src/pages/login/login.vue +++ b/client/src/pages/login/login.vue @@ -324,7 +324,7 @@ function goForgotPassword() { // 微信小程序分享 // #ifdef MP-WEIXIN onShareAppMessage(() => ({ - title: 'AI 维度 - 你的AI知识助手', + title: '登录解锁AI学习完整功能 · 宇之然AI维度', path: '/pages/index/index' })) // #endif diff --git a/client/src/pages/payment/payment.vue b/client/src/pages/payment/payment.vue index 0e5e00e..88cd918 100644 --- a/client/src/pages/payment/payment.vue +++ b/client/src/pages/payment/payment.vue @@ -291,7 +291,7 @@ function goBack() { // 微信小程序分享 // #ifdef MP-WEIXIN onShareAppMessage(() => ({ - title: 'AI 维度 - 你的AI知识助手', + title: 'AI知识社区 · 宇之然AI维度', path: '/pages/index/index' })) // #endif diff --git a/client/src/pages/progress/progress.vue b/client/src/pages/progress/progress.vue index 87177b9..fe1f558 100644 --- a/client/src/pages/progress/progress.vue +++ b/client/src/pages/progress/progress.vue @@ -225,7 +225,7 @@ function goBack() { // 微信小程序分享 // #ifdef MP-WEIXIN onShareAppMessage(() => ({ - title: 'AI 维度 - 你的AI知识助手', + title: '我的AI学习进度 · 宇之然AI维度', path: '/pages/index/index' })) // #endif diff --git a/client/src/pages/search/search.vue b/client/src/pages/search/search.vue index 3627fdd..4245cdc 100644 --- a/client/src/pages/search/search.vue +++ b/client/src/pages/search/search.vue @@ -192,7 +192,7 @@ onUnmounted(() => { // 微信小程序分享 // #ifdef MP-WEIXIN onShareAppMessage(() => ({ - title: 'AI 维度 - 你的AI知识助手', + title: '搜一搜想了解的AI知识 · 宇之然AI维度', path: '/pages/index/index' })) // #endif diff --git a/client/src/pages/settings/settings.vue b/client/src/pages/settings/settings.vue index e1e78e5..4c7e091 100644 --- a/client/src/pages/settings/settings.vue +++ b/client/src/pages/settings/settings.vue @@ -148,7 +148,7 @@ function goBack() { // 微信小程序分享 // #ifdef MP-WEIXIN onShareAppMessage(() => ({ - title: 'AI 维度 - 你的AI知识助手', + title: 'AI知识社区 · 宇之然AI维度', path: '/pages/index/index' })) // #endif diff --git a/server/src/routes/admin.js b/server/src/routes/admin.js index 24612bc..c8f61db 100755 --- a/server/src/routes/admin.js +++ b/server/src/routes/admin.js @@ -106,7 +106,6 @@ router.get('/dashboard', requirePermission('dashboard:read'), async (req, res) = Order.getWeekTrend() ]) - await logAction(req, 'read', 'dashboard') return ApiResponse.success(res, { today: { @@ -201,7 +200,6 @@ router.get('/users', requirePermission('users:read'), async (req, res) => { } }) - await logAction(req, 'read', 'users', null, { page, limit, search }) return ApiResponse.paginated(res, formattedUsers, { page: parseInt(page), @@ -263,7 +261,6 @@ router.get('/users/:id', requirePermission('users:read'), async (req, res) => { // 画廊(Gallery)功能已移除,作品数为 0 - await logAction(req, 'read', 'users', req.params.id) return ApiResponse.success(res, { ...user, @@ -302,7 +299,6 @@ router.get('/knowledge', requirePermission('knowledge:read'), async (req, res) = .limit(parseInt(limit)) .lean() - await logAction(req, 'read', 'knowledge', null, { page, limit, dim, status }) return ApiResponse.paginated(res, knowledge, { page: parseInt(page), @@ -435,7 +431,6 @@ router.get('/orders', requirePermission('orders:read'), async (req, res) => { .limit(parseInt(limit)) .lean() - await logAction(req, 'read', 'orders', null, { page, limit, status }) return ApiResponse.paginated(res, orders, { page: parseInt(page), @@ -464,7 +459,6 @@ router.get('/shop', requirePermission('shop:read'), async (req, res) => { .limit(parseInt(limit)) .lean() - await logAction(req, 'read', 'shop', null, { page, limit, status }) return ApiResponse.paginated(res, items, { page: parseInt(page), @@ -555,7 +549,6 @@ router.get('/admins', requirePermission('*'), async (req, res) => { .sort({ createdAt: -1 }) .lean() - await logAction(req, 'read', 'admins') return ApiResponse.success(res, admins) @@ -580,7 +573,6 @@ router.get('/shop-items', requirePermission('shop:read'), async (req, res) => { .limit(parseInt(limit)) .lean() - await logAction(req, 'read', 'shop', null, { page, limit, status }) return ApiResponse.paginated(res, items, { page: parseInt(page), @@ -890,7 +882,6 @@ router.get('/feedback', requirePermission('feedback:read'), async (req, res) => hasImages: fb.images?.length > 0 })) - await logAction(req, 'read', 'feedback', null, { page, limit, status, type }) return ApiResponse.paginated(res, formattedFeedbacks, { page: parseInt(page), @@ -918,7 +909,6 @@ router.get('/feedback/:id', requirePermission('feedback:read'), async (req, res) return ApiResponse.notFound(res, '反馈不存在') } - await logAction(req, 'read', 'feedback', req.params.id) return ApiResponse.success(res, { id: feedback._id, @@ -1114,7 +1104,6 @@ router.get('/login-logs', requirePermission('user:read'), async (req, res) => { createdAt: log.createdAt })) - await logAction(req, 'read', 'login-logs', null, { page, limit }) return ApiResponse.paginated(res, formattedLogs, { page: parseInt(page), diff --git a/server/src/routes/aiChat.js b/server/src/routes/aiChat.js index c915368..f9a0562 100755 --- a/server/src/routes/aiChat.js +++ b/server/src/routes/aiChat.js @@ -227,6 +227,17 @@ router.post('/share-gain', userAuthMiddleware, async (req, res) => { quota = new AIChatQuota({ openid }) } + // [防刷] 每日分享奖励最多3次 + const today = new Date().toISOString().slice(0, 10) + const shareDate = (quota.shareDate || '').toString().slice(0, 10) + const shareCount = shareDate === today ? (quota.shareCount || 0) : 0 + if (shareCount >= 3) { + return ApiResponse.error(res, '今日分享奖励已领完,明天再来吧', 429) + } + quota.shareDate = new Date() + quota.shareCount = shareCount + 1 + await quota.save() + const remainingQuota = await quota.addQuotaByShare(5) return ApiResponse.success(res, { diff --git a/server/src/routes/auth.js b/server/src/routes/auth.js index 98c6c9d..0f13d8b 100755 --- a/server/src/routes/auth.js +++ b/server/src/routes/auth.js @@ -582,58 +582,61 @@ router.put('/nickname', async (req, res) => { * 用于小程序 WebView 内嵌 H5 时,通过 openid 获取登录 token * 小程序在 URL 中传递 openid,H5 用这个接口换取 token */ -router.post('/webview-token', async (req, res) => { - try { - const { openid } = req.body - - if (!openid) { - return ApiResponse.error(res, '缺少用户标识', 400) - } - - // 查找用户 - const user = await User.findOne({ openid }) - - if (!user) { - return ApiResponse.error(res, '用户不存在', 404) - } - - // 生成新的 JWT token - const token = jwt.sign( - { openid, userId: user._id }, - process.env.JWT_SECRET, - { expiresIn: process.env.JWT_EXPIRE } - ) - - // 更新最后登录时间 - user.lastLoginAt = new Date() - await user.save() - - // 记录登录日志 - await recordLoginLog( - user._id, - openid, - 'auto_login', - 'wechat_webview', - 'webview_h5', - req, - true - ) - - logger.info(`WebView Token交换成功: ${openid}`) - - return ApiResponse.success(res, { - token, - openid, - userInfo: { - nickName: user.nickName, - avatarUrl: user.avatarUrl - } - }, '登录成功') - - } catch (error) { - logger.error('WebView Token交换失败:', error) - return ApiResponse.serverError(res, error.message) - } -}) +// [安全下线 2026-09-19] 该接口仅凭 openid 即可换任意用户 token(配合公开 openid 列表=账号接管)。客户端已确认无调用,如需 H5 登录用 code 换 token 方案。 +/* 已下线:router.post('/webview-token', async (req, res) => { +// try { +// const { openid } = req.body +// +// if (!openid) { +// return ApiResponse.error(res, '缺少用户标识', 400) +// } +// +// // 查找用户 +// const user = await User.findOne({ openid }) +// +// if (!user) { +// return ApiResponse.error(res, '用户不存在', 404) +// } +// +// // 生成新的 JWT token +// const token = jwt.sign( +// { openid, userId: user._id }, +// process.env.JWT_SECRET, +// { expiresIn: process.env.JWT_EXPIRE } +// ) +// +// // 更新最后登录时间 +// user.lastLoginAt = new Date() +// await user.save() +// +// // 记录登录日志 +// await recordLoginLog( +// user._id, +// openid, +// 'auto_login', +// 'wechat_webview', +// 'webview_h5', +// req, +// true +// ) +// +// logger.info(`WebView Token交换成功: ${openid}`) +// +// return ApiResponse.success(res, { +// token, +// openid, +// userInfo: { +// nickName: user.nickName, +// avatarUrl: user.avatarUrl +// } +// }, '登录成功') +// +// } catch (error) { +// logger.error('WebView Token交换失败:', error) +// return ApiResponse.serverError(res, error.message) +// } +// }) +// +*/ module.exports = router diff --git a/server/src/routes/knowledge.js b/server/src/routes/knowledge.js index 1762ffa..38b8901 100755 --- a/server/src/routes/knowledge.js +++ b/server/src/routes/knowledge.js @@ -330,7 +330,12 @@ router.get('/:dim/questions', async (req, res) => { .limit(parseInt(limit)) .lean() - return ApiResponse.success(res, questions) + // 脱敏:不下发 isCorrect(答案) + return ApiResponse.success(res, questions.map(q => { + const o = (q.toObject ? q.toObject() : q) + if (Array.isArray(o.options)) o.options = o.options.map(op => ({ text: op.text, index: op.index })) + return o + })) } catch (error) { logger.error('获取问答失败:', error) return ApiResponse.serverError(res, '获取问答失败') diff --git a/server/src/routes/user.js b/server/src/routes/user.js index 947a8b7..7ca5d27 100755 --- a/server/src/routes/user.js +++ b/server/src/routes/user.js @@ -1,398 +1,394 @@ -const express = require('express') -const router = express.Router() -const { User } = 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 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: 0, - 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 - * 说明:画廊(Gallery)功能已移除,统一返回空列表 - */ -router.get('/works', async (req, res) => { - try { - const user = req.user - const { page = 1, limit = 20 } = req.query - - return ApiResponse.paginated(res, [], { - page: parseInt(page), - limit: parseInt(limit), - total: 0 - }) - - } catch (error) { - logger.error('获取用户作品失败:', error) - return ApiResponse.serverError(res, error.message) - } -}) - -module.exports = router +const express = require('express') +const router = express.Router() +const { User } = 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) + } + + // [安全] 付费权益字段(ownedSkins/isAdFree/isSubscriber/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 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: 0, + 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 + * 说明:画廊(Gallery)功能已移除,统一返回空列表 + */ +router.get('/works', async (req, res) => { + try { + const user = req.user + const { page = 1, limit = 20 } = req.query + + return ApiResponse.paginated(res, [], { + page: parseInt(page), + limit: parseInt(limit), + total: 0 + }) + + } catch (error) { + logger.error('获取用户作品失败:', error) + return ApiResponse.serverError(res, error.message) + } +}) + +module.exports = router