security+fix: 评审修复——下线webview-token任意账号接管;progress接口付费字段只读;检测题答案脱敏;读接口不记日志;share-gain每日限3次防刷;页面标题/分享标题搜索优化
This commit is contained in:
@@ -1,6 +1,8 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="zh-CN">
|
<html lang="zh-CN">
|
||||||
<head>
|
<head>
|
||||||
|
<meta name="sogou_site_verification" content="bZ8oAfjvQm" />
|
||||||
|
<meta name="360-site-verification" content="0cc596886cce77073d8fcba9d5ec7d9c" />
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, user-scalable=no, viewport-fit=cover" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, user-scalable=no, viewport-fit=cover" />
|
||||||
<link rel="icon" type="image/svg+xml" href="/static/favicon.svg" />
|
<link rel="icon" type="image/svg+xml" href="/static/favicon.svg" />
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
{
|
{
|
||||||
"path": "pages/index/index",
|
"path": "pages/index/index",
|
||||||
"style": {
|
"style": {
|
||||||
"navigationBarTitleText": "AI 维度",
|
"navigationBarTitleText": "宇之然AI维度-AI知识学习与人工智能资讯",
|
||||||
"navigationStyle": "custom",
|
"navigationStyle": "custom",
|
||||||
"enablePullDownRefresh": true,
|
"enablePullDownRefresh": true,
|
||||||
"backgroundTextStyle": "dark"
|
"backgroundTextStyle": "dark"
|
||||||
@@ -19,7 +19,7 @@
|
|||||||
{
|
{
|
||||||
"path": "pages/detail/detail",
|
"path": "pages/detail/detail",
|
||||||
"style": {
|
"style": {
|
||||||
"navigationBarTitleText": "AI 知识",
|
"navigationBarTitleText": "AI知识详情-人工智能知识点",
|
||||||
"navigationStyle": "custom"
|
"navigationStyle": "custom"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -33,21 +33,21 @@
|
|||||||
{
|
{
|
||||||
"path": "pages/favorites/favorites",
|
"path": "pages/favorites/favorites",
|
||||||
"style": {
|
"style": {
|
||||||
"navigationBarTitleText": "我的收藏",
|
"navigationBarTitleText": "我的AI知识收藏",
|
||||||
"navigationStyle": "custom"
|
"navigationStyle": "custom"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "pages/achievements/achievements",
|
"path": "pages/achievements/achievements",
|
||||||
"style": {
|
"style": {
|
||||||
"navigationBarTitleText": "成就",
|
"navigationBarTitleText": "我的AI学习成就",
|
||||||
"navigationStyle": "custom"
|
"navigationStyle": "custom"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "pages/leaderboard/leaderboard",
|
"path": "pages/leaderboard/leaderboard",
|
||||||
"style": {
|
"style": {
|
||||||
"navigationBarTitleText": "排行榜",
|
"navigationBarTitleText": "AI学习排行榜",
|
||||||
"navigationStyle": "custom"
|
"navigationStyle": "custom"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -82,7 +82,7 @@
|
|||||||
{
|
{
|
||||||
"path": "pages/news/news",
|
"path": "pages/news/news",
|
||||||
"style": {
|
"style": {
|
||||||
"navigationBarTitleText": "AI 资讯",
|
"navigationBarTitleText": "AI资讯速递-人工智能热点解读",
|
||||||
"navigationStyle": "custom",
|
"navigationStyle": "custom",
|
||||||
"enablePullDownRefresh": true,
|
"enablePullDownRefresh": true,
|
||||||
"backgroundTextStyle": "dark"
|
"backgroundTextStyle": "dark"
|
||||||
@@ -91,21 +91,21 @@
|
|||||||
{
|
{
|
||||||
"path": "pages/dimension/dimension",
|
"path": "pages/dimension/dimension",
|
||||||
"style": {
|
"style": {
|
||||||
"navigationBarTitleText": "",
|
"navigationBarTitleText": "AI知识星域-五大维度学人工智能",
|
||||||
"navigationStyle": "custom"
|
"navigationStyle": "custom"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "pages/chat/chat",
|
"path": "pages/chat/chat",
|
||||||
"style": {
|
"style": {
|
||||||
"navigationBarTitleText": "AI 问答",
|
"navigationBarTitleText": "AI问答-人工智能知识解答",
|
||||||
"navigationStyle": "custom"
|
"navigationStyle": "custom"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "pages/trend/trend",
|
"path": "pages/trend/trend",
|
||||||
"style": {
|
"style": {
|
||||||
"navigationBarTitleText": "AI 趋势",
|
"navigationBarTitleText": "AI趋势-每日人工智能行业动态",
|
||||||
"navigationStyle": "custom",
|
"navigationStyle": "custom",
|
||||||
"enablePullDownRefresh": true,
|
"enablePullDownRefresh": true,
|
||||||
"backgroundTextStyle": "dark"
|
"backgroundTextStyle": "dark"
|
||||||
|
|||||||
@@ -118,7 +118,7 @@ function onLoad(fn) {
|
|||||||
// 微信小程序分享
|
// 微信小程序分享
|
||||||
// #ifdef MP-WEIXIN
|
// #ifdef MP-WEIXIN
|
||||||
onShareAppMessage(() => ({
|
onShareAppMessage(() => ({
|
||||||
title: 'AI 维度 - 你的AI知识助手',
|
title: 'AI知识社区 · 宇之然AI维度',
|
||||||
path: '/pages/index/index'
|
path: '/pages/index/index'
|
||||||
}))
|
}))
|
||||||
// #endif
|
// #endif
|
||||||
|
|||||||
@@ -232,7 +232,7 @@ function goBack() {
|
|||||||
// 微信小程序分享
|
// 微信小程序分享
|
||||||
// #ifdef MP-WEIXIN
|
// #ifdef MP-WEIXIN
|
||||||
onShareAppMessage(() => ({
|
onShareAppMessage(() => ({
|
||||||
title: 'AI 维度 - 你的AI知识助手',
|
title: '帮AI维度变得更好 · 意见反馈',
|
||||||
path: '/pages/index/index'
|
path: '/pages/index/index'
|
||||||
}))
|
}))
|
||||||
// #endif
|
// #endif
|
||||||
|
|||||||
@@ -138,7 +138,7 @@ function dimDotColor(id) {
|
|||||||
// 微信小程序分享
|
// 微信小程序分享
|
||||||
// #ifdef MP-WEIXIN
|
// #ifdef MP-WEIXIN
|
||||||
onShareAppMessage(() => ({
|
onShareAppMessage(() => ({
|
||||||
title: 'AI 维度 - 你的AI知识助手',
|
title: 'AI学习排行榜 · 宇之然AI维度',
|
||||||
path: '/pages/index/index'
|
path: '/pages/index/index'
|
||||||
}))
|
}))
|
||||||
// #endif
|
// #endif
|
||||||
|
|||||||
@@ -324,7 +324,7 @@ function goForgotPassword() {
|
|||||||
// 微信小程序分享
|
// 微信小程序分享
|
||||||
// #ifdef MP-WEIXIN
|
// #ifdef MP-WEIXIN
|
||||||
onShareAppMessage(() => ({
|
onShareAppMessage(() => ({
|
||||||
title: 'AI 维度 - 你的AI知识助手',
|
title: '登录解锁AI学习完整功能 · 宇之然AI维度',
|
||||||
path: '/pages/index/index'
|
path: '/pages/index/index'
|
||||||
}))
|
}))
|
||||||
// #endif
|
// #endif
|
||||||
|
|||||||
@@ -291,7 +291,7 @@ function goBack() {
|
|||||||
// 微信小程序分享
|
// 微信小程序分享
|
||||||
// #ifdef MP-WEIXIN
|
// #ifdef MP-WEIXIN
|
||||||
onShareAppMessage(() => ({
|
onShareAppMessage(() => ({
|
||||||
title: 'AI 维度 - 你的AI知识助手',
|
title: 'AI知识社区 · 宇之然AI维度',
|
||||||
path: '/pages/index/index'
|
path: '/pages/index/index'
|
||||||
}))
|
}))
|
||||||
// #endif
|
// #endif
|
||||||
|
|||||||
@@ -225,7 +225,7 @@ function goBack() {
|
|||||||
// 微信小程序分享
|
// 微信小程序分享
|
||||||
// #ifdef MP-WEIXIN
|
// #ifdef MP-WEIXIN
|
||||||
onShareAppMessage(() => ({
|
onShareAppMessage(() => ({
|
||||||
title: 'AI 维度 - 你的AI知识助手',
|
title: '我的AI学习进度 · 宇之然AI维度',
|
||||||
path: '/pages/index/index'
|
path: '/pages/index/index'
|
||||||
}))
|
}))
|
||||||
// #endif
|
// #endif
|
||||||
|
|||||||
@@ -192,7 +192,7 @@ onUnmounted(() => {
|
|||||||
// 微信小程序分享
|
// 微信小程序分享
|
||||||
// #ifdef MP-WEIXIN
|
// #ifdef MP-WEIXIN
|
||||||
onShareAppMessage(() => ({
|
onShareAppMessage(() => ({
|
||||||
title: 'AI 维度 - 你的AI知识助手',
|
title: '搜一搜想了解的AI知识 · 宇之然AI维度',
|
||||||
path: '/pages/index/index'
|
path: '/pages/index/index'
|
||||||
}))
|
}))
|
||||||
// #endif
|
// #endif
|
||||||
|
|||||||
@@ -148,7 +148,7 @@ function goBack() {
|
|||||||
// 微信小程序分享
|
// 微信小程序分享
|
||||||
// #ifdef MP-WEIXIN
|
// #ifdef MP-WEIXIN
|
||||||
onShareAppMessage(() => ({
|
onShareAppMessage(() => ({
|
||||||
title: 'AI 维度 - 你的AI知识助手',
|
title: 'AI知识社区 · 宇之然AI维度',
|
||||||
path: '/pages/index/index'
|
path: '/pages/index/index'
|
||||||
}))
|
}))
|
||||||
// #endif
|
// #endif
|
||||||
|
|||||||
@@ -106,7 +106,6 @@ router.get('/dashboard', requirePermission('dashboard:read'), async (req, res) =
|
|||||||
Order.getWeekTrend()
|
Order.getWeekTrend()
|
||||||
])
|
])
|
||||||
|
|
||||||
await logAction(req, 'read', 'dashboard')
|
|
||||||
|
|
||||||
return ApiResponse.success(res, {
|
return ApiResponse.success(res, {
|
||||||
today: {
|
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, {
|
return ApiResponse.paginated(res, formattedUsers, {
|
||||||
page: parseInt(page),
|
page: parseInt(page),
|
||||||
@@ -263,7 +261,6 @@ router.get('/users/:id', requirePermission('users:read'), async (req, res) => {
|
|||||||
|
|
||||||
// 画廊(Gallery)功能已移除,作品数为 0
|
// 画廊(Gallery)功能已移除,作品数为 0
|
||||||
|
|
||||||
await logAction(req, 'read', 'users', req.params.id)
|
|
||||||
|
|
||||||
return ApiResponse.success(res, {
|
return ApiResponse.success(res, {
|
||||||
...user,
|
...user,
|
||||||
@@ -302,7 +299,6 @@ router.get('/knowledge', requirePermission('knowledge:read'), async (req, res) =
|
|||||||
.limit(parseInt(limit))
|
.limit(parseInt(limit))
|
||||||
.lean()
|
.lean()
|
||||||
|
|
||||||
await logAction(req, 'read', 'knowledge', null, { page, limit, dim, status })
|
|
||||||
|
|
||||||
return ApiResponse.paginated(res, knowledge, {
|
return ApiResponse.paginated(res, knowledge, {
|
||||||
page: parseInt(page),
|
page: parseInt(page),
|
||||||
@@ -435,7 +431,6 @@ router.get('/orders', requirePermission('orders:read'), async (req, res) => {
|
|||||||
.limit(parseInt(limit))
|
.limit(parseInt(limit))
|
||||||
.lean()
|
.lean()
|
||||||
|
|
||||||
await logAction(req, 'read', 'orders', null, { page, limit, status })
|
|
||||||
|
|
||||||
return ApiResponse.paginated(res, orders, {
|
return ApiResponse.paginated(res, orders, {
|
||||||
page: parseInt(page),
|
page: parseInt(page),
|
||||||
@@ -464,7 +459,6 @@ router.get('/shop', requirePermission('shop:read'), async (req, res) => {
|
|||||||
.limit(parseInt(limit))
|
.limit(parseInt(limit))
|
||||||
.lean()
|
.lean()
|
||||||
|
|
||||||
await logAction(req, 'read', 'shop', null, { page, limit, status })
|
|
||||||
|
|
||||||
return ApiResponse.paginated(res, items, {
|
return ApiResponse.paginated(res, items, {
|
||||||
page: parseInt(page),
|
page: parseInt(page),
|
||||||
@@ -555,7 +549,6 @@ router.get('/admins', requirePermission('*'), async (req, res) => {
|
|||||||
.sort({ createdAt: -1 })
|
.sort({ createdAt: -1 })
|
||||||
.lean()
|
.lean()
|
||||||
|
|
||||||
await logAction(req, 'read', 'admins')
|
|
||||||
|
|
||||||
return ApiResponse.success(res, admins)
|
return ApiResponse.success(res, admins)
|
||||||
|
|
||||||
@@ -580,7 +573,6 @@ router.get('/shop-items', requirePermission('shop:read'), async (req, res) => {
|
|||||||
.limit(parseInt(limit))
|
.limit(parseInt(limit))
|
||||||
.lean()
|
.lean()
|
||||||
|
|
||||||
await logAction(req, 'read', 'shop', null, { page, limit, status })
|
|
||||||
|
|
||||||
return ApiResponse.paginated(res, items, {
|
return ApiResponse.paginated(res, items, {
|
||||||
page: parseInt(page),
|
page: parseInt(page),
|
||||||
@@ -890,7 +882,6 @@ router.get('/feedback', requirePermission('feedback:read'), async (req, res) =>
|
|||||||
hasImages: fb.images?.length > 0
|
hasImages: fb.images?.length > 0
|
||||||
}))
|
}))
|
||||||
|
|
||||||
await logAction(req, 'read', 'feedback', null, { page, limit, status, type })
|
|
||||||
|
|
||||||
return ApiResponse.paginated(res, formattedFeedbacks, {
|
return ApiResponse.paginated(res, formattedFeedbacks, {
|
||||||
page: parseInt(page),
|
page: parseInt(page),
|
||||||
@@ -918,7 +909,6 @@ router.get('/feedback/:id', requirePermission('feedback:read'), async (req, res)
|
|||||||
return ApiResponse.notFound(res, '反馈不存在')
|
return ApiResponse.notFound(res, '反馈不存在')
|
||||||
}
|
}
|
||||||
|
|
||||||
await logAction(req, 'read', 'feedback', req.params.id)
|
|
||||||
|
|
||||||
return ApiResponse.success(res, {
|
return ApiResponse.success(res, {
|
||||||
id: feedback._id,
|
id: feedback._id,
|
||||||
@@ -1114,7 +1104,6 @@ router.get('/login-logs', requirePermission('user:read'), async (req, res) => {
|
|||||||
createdAt: log.createdAt
|
createdAt: log.createdAt
|
||||||
}))
|
}))
|
||||||
|
|
||||||
await logAction(req, 'read', 'login-logs', null, { page, limit })
|
|
||||||
|
|
||||||
return ApiResponse.paginated(res, formattedLogs, {
|
return ApiResponse.paginated(res, formattedLogs, {
|
||||||
page: parseInt(page),
|
page: parseInt(page),
|
||||||
|
|||||||
@@ -227,6 +227,17 @@ router.post('/share-gain', userAuthMiddleware, async (req, res) => {
|
|||||||
quota = new AIChatQuota({ openid })
|
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)
|
const remainingQuota = await quota.addQuotaByShare(5)
|
||||||
|
|
||||||
return ApiResponse.success(res, {
|
return ApiResponse.success(res, {
|
||||||
|
|||||||
+56
-53
@@ -582,58 +582,61 @@ router.put('/nickname', async (req, res) => {
|
|||||||
* 用于小程序 WebView 内嵌 H5 时,通过 openid 获取登录 token
|
* 用于小程序 WebView 内嵌 H5 时,通过 openid 获取登录 token
|
||||||
* 小程序在 URL 中传递 openid,H5 用这个接口换取 token
|
* 小程序在 URL 中传递 openid,H5 用这个接口换取 token
|
||||||
*/
|
*/
|
||||||
router.post('/webview-token', async (req, res) => {
|
// [安全下线 2026-09-19] 该接口仅凭 openid 即可换任意用户 token(配合公开 openid 列表=账号接管)。客户端已确认无调用,如需 H5 登录用 code 换 token 方案。
|
||||||
try {
|
/* 已下线:router.post('/webview-token', async (req, res) => {
|
||||||
const { openid } = req.body
|
// try {
|
||||||
|
// const { openid } = req.body
|
||||||
if (!openid) {
|
//
|
||||||
return ApiResponse.error(res, '缺少用户标识', 400)
|
// if (!openid) {
|
||||||
}
|
// return ApiResponse.error(res, '缺少用户标识', 400)
|
||||||
|
// }
|
||||||
// 查找用户
|
//
|
||||||
const user = await User.findOne({ openid })
|
// // 查找用户
|
||||||
|
// const user = await User.findOne({ openid })
|
||||||
if (!user) {
|
//
|
||||||
return ApiResponse.error(res, '用户不存在', 404)
|
// if (!user) {
|
||||||
}
|
// return ApiResponse.error(res, '用户不存在', 404)
|
||||||
|
// }
|
||||||
// 生成新的 JWT token
|
//
|
||||||
const token = jwt.sign(
|
// // 生成新的 JWT token
|
||||||
{ openid, userId: user._id },
|
// const token = jwt.sign(
|
||||||
process.env.JWT_SECRET,
|
// { openid, userId: user._id },
|
||||||
{ expiresIn: process.env.JWT_EXPIRE }
|
// process.env.JWT_SECRET,
|
||||||
)
|
// { expiresIn: process.env.JWT_EXPIRE }
|
||||||
|
// )
|
||||||
// 更新最后登录时间
|
//
|
||||||
user.lastLoginAt = new Date()
|
// // 更新最后登录时间
|
||||||
await user.save()
|
// user.lastLoginAt = new Date()
|
||||||
|
// await user.save()
|
||||||
// 记录登录日志
|
//
|
||||||
await recordLoginLog(
|
// // 记录登录日志
|
||||||
user._id,
|
// await recordLoginLog(
|
||||||
openid,
|
// user._id,
|
||||||
'auto_login',
|
// openid,
|
||||||
'wechat_webview',
|
// 'auto_login',
|
||||||
'webview_h5',
|
// 'wechat_webview',
|
||||||
req,
|
// 'webview_h5',
|
||||||
true
|
// req,
|
||||||
)
|
// true
|
||||||
|
// )
|
||||||
logger.info(`WebView Token交换成功: ${openid}`)
|
//
|
||||||
|
// logger.info(`WebView Token交换成功: ${openid}`)
|
||||||
return ApiResponse.success(res, {
|
//
|
||||||
token,
|
// return ApiResponse.success(res, {
|
||||||
openid,
|
// token,
|
||||||
userInfo: {
|
// openid,
|
||||||
nickName: user.nickName,
|
// userInfo: {
|
||||||
avatarUrl: user.avatarUrl
|
// nickName: user.nickName,
|
||||||
}
|
// avatarUrl: user.avatarUrl
|
||||||
}, '登录成功')
|
// }
|
||||||
|
// }, '登录成功')
|
||||||
} catch (error) {
|
//
|
||||||
logger.error('WebView Token交换失败:', error)
|
// } catch (error) {
|
||||||
return ApiResponse.serverError(res, error.message)
|
// logger.error('WebView Token交换失败:', error)
|
||||||
}
|
// return ApiResponse.serverError(res, error.message)
|
||||||
})
|
// }
|
||||||
|
// })
|
||||||
|
//
|
||||||
|
*/
|
||||||
|
|
||||||
module.exports = router
|
module.exports = router
|
||||||
|
|||||||
@@ -330,7 +330,12 @@ router.get('/:dim/questions', async (req, res) => {
|
|||||||
.limit(parseInt(limit))
|
.limit(parseInt(limit))
|
||||||
.lean()
|
.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) {
|
} catch (error) {
|
||||||
logger.error('获取问答失败:', error)
|
logger.error('获取问答失败:', error)
|
||||||
return ApiResponse.serverError(res, '获取问答失败')
|
return ApiResponse.serverError(res, '获取问答失败')
|
||||||
|
|||||||
+394
-398
@@ -1,398 +1,394 @@
|
|||||||
const express = require('express')
|
const express = require('express')
|
||||||
const router = express.Router()
|
const router = express.Router()
|
||||||
const { User } = require('../models')
|
const { User } = require('../models')
|
||||||
const ApiResponse = require('../utils/response')
|
const ApiResponse = require('../utils/response')
|
||||||
const { userAuthMiddleware } = require('../middleware/auth')
|
const { userAuthMiddleware } = require('../middleware/auth')
|
||||||
const logger = require('../utils/logger')
|
const logger = require('../utils/logger')
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取排行榜(公开接口,无需登录)
|
* 获取排行榜(公开接口,无需登录)
|
||||||
* GET /api/user/leaderboard
|
* GET /api/user/leaderboard
|
||||||
*/
|
*/
|
||||||
router.get('/leaderboard', async (req, res) => {
|
router.get('/leaderboard', async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const { limit = 50, dim } = req.query
|
const { limit = 50, dim } = req.query
|
||||||
|
|
||||||
const users = await User.getLeaderboard(dim ? parseInt(dim) : null, parseInt(limit))
|
const users = await User.getLeaderboard(dim ? parseInt(dim) : null, parseInt(limit))
|
||||||
|
|
||||||
const data = users.map((u, i) => ({
|
const data = users.map((u, i) => ({
|
||||||
rank: i + 1,
|
rank: i + 1,
|
||||||
openid: u.openid,
|
openid: u.openid,
|
||||||
nickname: u.nickName || '星辰旅者',
|
nickname: u.nickName || '星辰旅者',
|
||||||
avatar: u.avatarUrl || '',
|
avatar: u.avatarUrl || '',
|
||||||
totalScore: u.totalScore || 0,
|
totalScore: u.totalScore || 0,
|
||||||
dim1Score: u.dim1Score || 0,
|
dim1Score: u.dim1Score || 0,
|
||||||
dim2Score: u.dim2Score || 0,
|
dim2Score: u.dim2Score || 0,
|
||||||
dim3Score: u.dim3Score || 0,
|
dim3Score: u.dim3Score || 0,
|
||||||
dim4Score: u.dim4Score || 0,
|
dim4Score: u.dim4Score || 0,
|
||||||
dim5Score: u.dim5Score || 0,
|
dim5Score: u.dim5Score || 0,
|
||||||
unlockedDims: u.unlockedDims || [1]
|
unlockedDims: u.unlockedDims || [1]
|
||||||
}))
|
}))
|
||||||
|
|
||||||
return ApiResponse.success(res, data)
|
return ApiResponse.success(res, data)
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('获取排行榜失败:', error)
|
logger.error('获取排行榜失败:', error)
|
||||||
return ApiResponse.serverError(res, error.message)
|
return ApiResponse.serverError(res, error.message)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// 应用用户认证中间件到所有路由(排行榜已在上方公开)
|
// 应用用户认证中间件到所有路由(排行榜已在上方公开)
|
||||||
router.use(userAuthMiddleware)
|
router.use(userAuthMiddleware)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取用户进度
|
* 获取用户进度
|
||||||
* GET /api/user/progress
|
* GET /api/user/progress
|
||||||
*/
|
*/
|
||||||
router.get('/progress', async (req, res) => {
|
router.get('/progress', async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const user = req.user
|
const user = req.user
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
// 新用户,返回默认进度
|
// 新用户,返回默认进度
|
||||||
return ApiResponse.success(res, {
|
return ApiResponse.success(res, {
|
||||||
isNewUser: true,
|
isNewUser: true,
|
||||||
data: {
|
data: {
|
||||||
unlockedDims: [1],
|
unlockedDims: [1],
|
||||||
dim1Score: 0,
|
dim1Score: 0,
|
||||||
dim2Score: 0,
|
dim2Score: 0,
|
||||||
dim3Score: 0,
|
dim3Score: 0,
|
||||||
dim4Score: 0,
|
dim4Score: 0,
|
||||||
dim5Score: 0,
|
dim5Score: 0,
|
||||||
totalScore: 0,
|
totalScore: 0,
|
||||||
ownedSkins: [],
|
ownedSkins: [],
|
||||||
isAdFree: false,
|
isAdFree: false,
|
||||||
isSubscriber: false,
|
isSubscriber: false,
|
||||||
subscribeExpiry: null,
|
subscribeExpiry: null,
|
||||||
totalPlayTime: 0
|
totalPlayTime: 0
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
return ApiResponse.success(res, {
|
return ApiResponse.success(res, {
|
||||||
isNewUser: false,
|
isNewUser: false,
|
||||||
data: user
|
data: user
|
||||||
})
|
})
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('获取进度失败:', error)
|
logger.error('获取进度失败:', error)
|
||||||
return ApiResponse.serverError(res, error.message)
|
return ApiResponse.serverError(res, error.message)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 保存用户进度
|
* 保存用户进度
|
||||||
* POST /api/user/progress
|
* POST /api/user/progress
|
||||||
*/
|
*/
|
||||||
router.post('/progress', async (req, res) => {
|
router.post('/progress', async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const user = req.user
|
const user = req.user
|
||||||
const openid = user.openid
|
const openid = user.openid
|
||||||
|
|
||||||
const {
|
const {
|
||||||
dim,
|
dim,
|
||||||
data,
|
data,
|
||||||
dim1Score,
|
dim1Score,
|
||||||
dim2Score,
|
dim2Score,
|
||||||
dim3Score,
|
dim3Score,
|
||||||
dim4Score,
|
dim4Score,
|
||||||
dim5Score,
|
dim5Score,
|
||||||
unlockedDims,
|
unlockedDims,
|
||||||
ownedSkins,
|
ownedSkins,
|
||||||
isAdFree,
|
isAdFree,
|
||||||
isSubscriber,
|
isSubscriber,
|
||||||
subscribeExpiry,
|
subscribeExpiry,
|
||||||
totalPlayTime
|
totalPlayTime
|
||||||
} = req.body
|
} = req.body
|
||||||
|
|
||||||
// 查找或创建用户(理论上已通过中间件找到)
|
// 查找或创建用户(理论上已通过中间件找到)
|
||||||
let dbUser = user
|
let dbUser = user
|
||||||
|
|
||||||
// 新版接口:按维度更新
|
// 新版接口:按维度更新
|
||||||
if (dim && data) {
|
if (dim && data) {
|
||||||
const dimData = dbUser.exploreData[dim] || {}
|
const dimData = dbUser.exploreData[dim] || {}
|
||||||
let updatedDim = { ...dimData }
|
let updatedDim = { ...dimData }
|
||||||
|
|
||||||
if (dim === 'dim1') {
|
if (dim === 'dim1') {
|
||||||
updatedDim.completed = data.completed || dimData.completed || false
|
updatedDim.completed = data.completed || dimData.completed || false
|
||||||
updatedDim.bestScore = Math.max(data.bestScore || 0, dimData.bestScore || 0)
|
updatedDim.bestScore = Math.max(data.bestScore || 0, dimData.bestScore || 0)
|
||||||
updatedDim.collectedEggs = (dimData.collectedEggs || 0) + (data.newEggs || 0)
|
updatedDim.collectedEggs = (dimData.collectedEggs || 0) + (data.newEggs || 0)
|
||||||
updatedDim.totalLength = Math.max(data.totalLength || 0, dimData.totalLength || 0)
|
updatedDim.totalLength = Math.max(data.totalLength || 0, dimData.totalLength || 0)
|
||||||
} else if (dim === 'dim2') {
|
} else if (dim === 'dim2') {
|
||||||
updatedDim.completed = data.completed || dimData.completed || false
|
updatedDim.completed = data.completed || dimData.completed || false
|
||||||
updatedDim.bestArea = Math.max(data.bestArea || 0, dimData.bestArea || 0)
|
updatedDim.bestArea = Math.max(data.bestArea || 0, dimData.bestArea || 0)
|
||||||
updatedDim.createdShapes = (dimData.createdShapes || 0) + (data.newShapes || 0)
|
updatedDim.createdShapes = (dimData.createdShapes || 0) + (data.newShapes || 0)
|
||||||
} else if (dim === 'dim3') {
|
} else if (dim === 'dim3') {
|
||||||
updatedDim.completed = data.completed || dimData.completed || false
|
updatedDim.completed = data.completed || dimData.completed || false
|
||||||
updatedDim.exploredFaces = Math.max(data.exploredFaces || 0, dimData.exploredFaces || 0)
|
updatedDim.exploredFaces = Math.max(data.exploredFaces || 0, dimData.exploredFaces || 0)
|
||||||
updatedDim.rotationTime = (dimData.rotationTime || 0) + (data.rotationTime || 0)
|
updatedDim.rotationTime = (dimData.rotationTime || 0) + (data.rotationTime || 0)
|
||||||
updatedDim.bestScore = Math.max(data.bestScore || 0, dimData.bestScore || 0)
|
updatedDim.bestScore = Math.max(data.bestScore || 0, dimData.bestScore || 0)
|
||||||
} else if (dim === 'dim4') {
|
} else if (dim === 'dim4') {
|
||||||
updatedDim.completed = data.completed || dimData.completed || false
|
updatedDim.completed = data.completed || dimData.completed || false
|
||||||
updatedDim.bestScore = Math.max(data.bestScore || 0, dimData.bestScore || 0)
|
updatedDim.bestScore = Math.max(data.bestScore || 0, dimData.bestScore || 0)
|
||||||
updatedDim.exploredEvents = (dimData.exploredEvents || 0) + (data.newEvents || 0)
|
updatedDim.exploredEvents = (dimData.exploredEvents || 0) + (data.newEvents || 0)
|
||||||
} else if (dim === 'dim5') {
|
} else if (dim === 'dim5') {
|
||||||
updatedDim.completed = data.completed || dimData.completed || false
|
updatedDim.completed = data.completed || dimData.completed || false
|
||||||
updatedDim.bestScore = Math.max(data.bestScore || 0, dimData.bestScore || 0)
|
updatedDim.bestScore = Math.max(data.bestScore || 0, dimData.bestScore || 0)
|
||||||
updatedDim.exploredThoughts = (dimData.exploredThoughts || 0) + (data.newThoughts || 0)
|
updatedDim.exploredThoughts = (dimData.exploredThoughts || 0) + (data.newThoughts || 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
dbUser.exploreData[dim] = updatedDim
|
dbUser.exploreData[dim] = updatedDim
|
||||||
|
|
||||||
// 同步更新已解锁维度
|
// 同步更新已解锁维度
|
||||||
if (data.completed && !dbUser.unlockedDims.includes(parseInt(dim.replace('dim', '')) + 1)) {
|
if (data.completed && !dbUser.unlockedDims.includes(parseInt(dim.replace('dim', '')) + 1)) {
|
||||||
const dimNum = parseInt(dim.replace('dim', ''))
|
const dimNum = parseInt(dim.replace('dim', ''))
|
||||||
if (!dbUser.unlockedDims.includes(dimNum + 1)) {
|
if (!dbUser.unlockedDims.includes(dimNum + 1)) {
|
||||||
dbUser.unlockedDims.push(dimNum + 1)
|
dbUser.unlockedDims.push(dimNum + 1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (totalPlayTime !== null && totalPlayTime !== undefined) {
|
if (totalPlayTime !== null && totalPlayTime !== undefined) {
|
||||||
dbUser.totalPlayTime += totalPlayTime
|
dbUser.totalPlayTime += totalPlayTime
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 兼容旧版接口:直接传各维度分数
|
// 兼容旧版接口:直接传各维度分数
|
||||||
if (dim1Score !== null && dim1Score !== undefined) {
|
if (dim1Score !== null && dim1Score !== undefined) {
|
||||||
dbUser.exploreData.dim1.bestScore = Math.max(dim1Score, dbUser.exploreData.dim1?.bestScore || 0)
|
dbUser.exploreData.dim1.bestScore = Math.max(dim1Score, dbUser.exploreData.dim1?.bestScore || 0)
|
||||||
}
|
}
|
||||||
if (dim2Score !== null && dim2Score !== undefined) {
|
if (dim2Score !== null && dim2Score !== undefined) {
|
||||||
dbUser.exploreData.dim2.bestArea = Math.max(dim2Score, dbUser.exploreData.dim2?.bestArea || 0)
|
dbUser.exploreData.dim2.bestArea = Math.max(dim2Score, dbUser.exploreData.dim2?.bestArea || 0)
|
||||||
}
|
}
|
||||||
if (dim3Score !== null && dim3Score !== undefined) {
|
if (dim3Score !== null && dim3Score !== undefined) {
|
||||||
dbUser.exploreData.dim3.bestScore = Math.max(dim3Score, dbUser.exploreData.dim3?.bestScore || 0)
|
dbUser.exploreData.dim3.bestScore = Math.max(dim3Score, dbUser.exploreData.dim3?.bestScore || 0)
|
||||||
}
|
}
|
||||||
if (dim4Score !== null && dim4Score !== undefined) {
|
if (dim4Score !== null && dim4Score !== undefined) {
|
||||||
dbUser.exploreData.dim4 = dbUser.exploreData.dim4 || {}
|
dbUser.exploreData.dim4 = dbUser.exploreData.dim4 || {}
|
||||||
dbUser.exploreData.dim4.bestScore = Math.max(dim4Score, dbUser.exploreData.dim4?.bestScore || 0)
|
dbUser.exploreData.dim4.bestScore = Math.max(dim4Score, dbUser.exploreData.dim4?.bestScore || 0)
|
||||||
}
|
}
|
||||||
if (dim5Score !== null && dim5Score !== undefined) {
|
if (dim5Score !== null && dim5Score !== undefined) {
|
||||||
dbUser.exploreData.dim5 = dbUser.exploreData.dim5 || {}
|
dbUser.exploreData.dim5 = dbUser.exploreData.dim5 || {}
|
||||||
dbUser.exploreData.dim5.bestScore = Math.max(dim5Score, dbUser.exploreData.dim5?.bestScore || 0)
|
dbUser.exploreData.dim5.bestScore = Math.max(dim5Score, dbUser.exploreData.dim5?.bestScore || 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 更新其他字段
|
// [安全] 付费权益字段(ownedSkins/isAdFree/isSubscriber/subscribeExpiry)不接受客户端写入,仅支付回调/管理员可改
|
||||||
if (ownedSkins !== null && ownedSkins !== undefined) dbUser.ownedSkins = ownedSkins
|
|
||||||
if (isAdFree !== null && isAdFree !== undefined) dbUser.isAdFree = isAdFree
|
await dbUser.save()
|
||||||
if (isSubscriber !== null && isSubscriber !== undefined) dbUser.isSubscriber = isSubscriber
|
|
||||||
if (subscribeExpiry !== null && subscribeExpiry !== undefined) dbUser.subscribeExpiry = subscribeExpiry
|
// 计算总分
|
||||||
|
const totalScore = dbUser.calculateTotalScore()
|
||||||
await dbUser.save()
|
|
||||||
|
return ApiResponse.success(res, {
|
||||||
// 计算总分
|
success: true,
|
||||||
const totalScore = dbUser.calculateTotalScore()
|
totalScore,
|
||||||
|
exploreData: user.exploreData
|
||||||
return ApiResponse.success(res, {
|
})
|
||||||
success: true,
|
|
||||||
totalScore,
|
} catch (error) {
|
||||||
exploreData: user.exploreData
|
logger.error('保存进度失败:', error)
|
||||||
})
|
return ApiResponse.serverError(res, error.message)
|
||||||
|
}
|
||||||
} catch (error) {
|
})
|
||||||
logger.error('保存进度失败:', error)
|
|
||||||
return ApiResponse.serverError(res, error.message)
|
/**
|
||||||
}
|
* 获取用户成就
|
||||||
})
|
* GET /api/user/achievements
|
||||||
|
*/
|
||||||
/**
|
router.get('/achievements', async (req, res) => {
|
||||||
* 获取用户成就
|
try {
|
||||||
* GET /api/user/achievements
|
const user = req.user
|
||||||
*/
|
|
||||||
router.get('/achievements', async (req, res) => {
|
// 生成成就数据(AI 维度主题)
|
||||||
try {
|
const achievements = [
|
||||||
const user = req.user
|
{
|
||||||
|
name: '知识探索',
|
||||||
// 生成成就数据(AI 维度主题)
|
achievements: [
|
||||||
const achievements = [
|
{
|
||||||
{
|
id: 'dim1_read',
|
||||||
name: '知识探索',
|
name: 'AI起源探索者',
|
||||||
achievements: [
|
description: '完成 AI 起源维度的知识阅读',
|
||||||
{
|
icon: '📜',
|
||||||
id: 'dim1_read',
|
unlocked: user.exploreData.dim1?.completed || false,
|
||||||
name: 'AI起源探索者',
|
date: user.exploreData.dim1?.completed ? user.updatedAt.toISOString().split('T')[0] : null
|
||||||
description: '完成 AI 起源维度的知识阅读',
|
},
|
||||||
icon: '📜',
|
{
|
||||||
unlocked: user.exploreData.dim1?.completed || false,
|
id: 'dim2_read',
|
||||||
date: user.exploreData.dim1?.completed ? user.updatedAt.toISOString().split('T')[0] : null
|
name: 'AI发展探索者',
|
||||||
},
|
description: '完成 AI 发展维度的知识阅读',
|
||||||
{
|
icon: '🏛',
|
||||||
id: 'dim2_read',
|
unlocked: user.exploreData.dim2?.completed || false,
|
||||||
name: 'AI发展探索者',
|
date: user.exploreData.dim2?.completed ? user.updatedAt.toISOString().split('T')[0] : null
|
||||||
description: '完成 AI 发展维度的知识阅读',
|
},
|
||||||
icon: '🏛',
|
{
|
||||||
unlocked: user.exploreData.dim2?.completed || false,
|
id: 'dim3_read',
|
||||||
date: user.exploreData.dim2?.completed ? user.updatedAt.toISOString().split('T')[0] : null
|
name: 'AI当前探索者',
|
||||||
},
|
description: '完成 AI 当前格局维度的知识阅读',
|
||||||
{
|
icon: '♟',
|
||||||
id: 'dim3_read',
|
unlocked: user.exploreData.dim3?.completed || false,
|
||||||
name: 'AI当前探索者',
|
date: user.exploreData.dim3?.completed ? user.updatedAt.toISOString().split('T')[0] : null
|
||||||
description: '完成 AI 当前格局维度的知识阅读',
|
},
|
||||||
icon: '♟',
|
{
|
||||||
unlocked: user.exploreData.dim3?.completed || false,
|
id: 'dim4_read',
|
||||||
date: user.exploreData.dim3?.completed ? user.updatedAt.toISOString().split('T')[0] : null
|
name: 'AI学习探索者',
|
||||||
},
|
description: '完成 AI 学习与工具维度的知识阅读',
|
||||||
{
|
icon: '🧠',
|
||||||
id: 'dim4_read',
|
unlocked: user.exploreData.dim4?.completed || false,
|
||||||
name: 'AI学习探索者',
|
date: user.exploreData.dim4?.completed ? user.updatedAt.toISOString().split('T')[0] : null
|
||||||
description: '完成 AI 学习与工具维度的知识阅读',
|
},
|
||||||
icon: '🧠',
|
{
|
||||||
unlocked: user.exploreData.dim4?.completed || false,
|
id: 'dim5_read',
|
||||||
date: user.exploreData.dim4?.completed ? user.updatedAt.toISOString().split('T')[0] : null
|
name: 'AI趋势探索者',
|
||||||
},
|
description: '完成 AI 趋势追踪维度的知识阅读',
|
||||||
{
|
icon: '⚡',
|
||||||
id: 'dim5_read',
|
unlocked: user.exploreData.dim5?.completed || false,
|
||||||
name: 'AI趋势探索者',
|
date: user.exploreData.dim5?.completed ? user.updatedAt.toISOString().split('T')[0] : null
|
||||||
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: '收藏成就',
|
name: '初次收藏',
|
||||||
achievements: [
|
description: '收藏你的第一个 AI 知识',
|
||||||
{
|
icon: '❤️',
|
||||||
id: 'first_collection',
|
unlocked: user.collectedKnowledge?.length > 0,
|
||||||
name: '初次收藏',
|
date: user.collectedKnowledge?.length > 0 ? user.updatedAt.toISOString().split('T')[0] : null
|
||||||
description: '收藏你的第一个 AI 知识',
|
},
|
||||||
icon: '❤️',
|
{
|
||||||
unlocked: user.collectedKnowledge?.length > 0,
|
id: 'five_collections',
|
||||||
date: user.collectedKnowledge?.length > 0 ? user.updatedAt.toISOString().split('T')[0] : null
|
name: '收藏爱好者',
|
||||||
},
|
description: '收藏5个 AI 知识',
|
||||||
{
|
icon: '🧡',
|
||||||
id: 'five_collections',
|
unlocked: user.collectedKnowledge?.length >= 5,
|
||||||
name: '收藏爱好者',
|
date: user.collectedKnowledge?.length >= 5 ? user.updatedAt.toISOString().split('T')[0] : null
|
||||||
description: '收藏5个 AI 知识',
|
},
|
||||||
icon: '🧡',
|
{
|
||||||
unlocked: user.collectedKnowledge?.length >= 5,
|
id: 'ten_collections',
|
||||||
date: user.collectedKnowledge?.length >= 5 ? user.updatedAt.toISOString().split('T')[0] : null
|
name: '收藏大师',
|
||||||
},
|
description: '收藏10个 AI 知识',
|
||||||
{
|
icon: '💛',
|
||||||
id: 'ten_collections',
|
unlocked: user.collectedKnowledge?.length >= 10,
|
||||||
name: '收藏大师',
|
date: user.collectedKnowledge?.length >= 10 ? user.updatedAt.toISOString().split('T')[0] : null
|
||||||
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: 'AI 问答',
|
name: '初次提问',
|
||||||
achievements: [
|
description: '向 AI 助手提出第一个问题',
|
||||||
{
|
icon: '🤖',
|
||||||
id: 'first_ask',
|
unlocked: user.achievements?.includes('first_ask') || false,
|
||||||
name: '初次提问',
|
date: user.achievements?.includes('first_ask') ? user.updatedAt.toISOString().split('T')[0] : null
|
||||||
description: '向 AI 助手提出第一个问题',
|
},
|
||||||
icon: '🤖',
|
{
|
||||||
unlocked: user.achievements?.includes('first_ask') || false,
|
id: 'ten_asks',
|
||||||
date: user.achievements?.includes('first_ask') ? user.updatedAt.toISOString().split('T')[0] : null
|
name: '勤学好问',
|
||||||
},
|
description: '向 AI 助手提出 10 个问题',
|
||||||
{
|
icon: '💡',
|
||||||
id: 'ten_asks',
|
unlocked: user.achievements?.includes('ten_asks') || false,
|
||||||
name: '勤学好问',
|
date: user.achievements?.includes('ten_asks') ? user.updatedAt.toISOString().split('T')[0] : null
|
||||||
description: '向 AI 助手提出 10 个问题',
|
},
|
||||||
icon: '💡',
|
{
|
||||||
unlocked: user.achievements?.includes('ten_asks') || false,
|
id: 'fifty_asks',
|
||||||
date: user.achievements?.includes('ten_asks') ? user.updatedAt.toISOString().split('T')[0] : null
|
name: 'AI 达人',
|
||||||
},
|
description: '向 AI 助手提出 50 个问题',
|
||||||
{
|
icon: '🧠',
|
||||||
id: 'fifty_asks',
|
unlocked: user.achievements?.includes('fifty_asks') || false,
|
||||||
name: 'AI 达人',
|
date: user.achievements?.includes('fifty_asks') ? user.updatedAt.toISOString().split('T')[0] : null
|
||||||
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: '社交成就',
|
name: '初次分享',
|
||||||
achievements: [
|
description: '分享你的第一个知识给朋友',
|
||||||
{
|
icon: '📤',
|
||||||
id: 'first_share',
|
unlocked: user.achievements?.includes('first_share') || false,
|
||||||
name: '初次分享',
|
date: user.achievements?.includes('first_share') ? user.updatedAt.toISOString().split('T')[0] : null
|
||||||
description: '分享你的第一个知识给朋友',
|
},
|
||||||
icon: '📤',
|
{
|
||||||
unlocked: user.achievements?.includes('first_share') || false,
|
id: 'five_shares',
|
||||||
date: user.achievements?.includes('first_share') ? user.updatedAt.toISOString().split('T')[0] : null
|
name: '分享达人',
|
||||||
},
|
description: '分享 5 次知识给朋友',
|
||||||
{
|
icon: '📣',
|
||||||
id: 'five_shares',
|
unlocked: user.achievements?.includes('five_shares') || false,
|
||||||
name: '分享达人',
|
date: user.achievements?.includes('five_shares') ? user.updatedAt.toISOString().split('T')[0] : null
|
||||||
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.success(res, achievements)
|
return ApiResponse.serverError(res, error.message)
|
||||||
|
}
|
||||||
} catch (error) {
|
})
|
||||||
logger.error('获取成就失败:', error)
|
|
||||||
return ApiResponse.serverError(res, error.message)
|
/**
|
||||||
}
|
* 获取用户统计数据
|
||||||
})
|
* GET /api/user/stats
|
||||||
|
*/
|
||||||
/**
|
router.get('/stats', async (req, res) => {
|
||||||
* 获取用户统计数据
|
try {
|
||||||
* GET /api/user/stats
|
const user = req.user
|
||||||
*/
|
|
||||||
router.get('/stats', async (req, res) => {
|
// 获取等级信息
|
||||||
try {
|
const levelInfo = user.getLevelInfo()
|
||||||
const user = req.user
|
|
||||||
|
// 计算探索时长(分钟转小时,保留1位小数)
|
||||||
// 获取等级信息
|
const playTimeHours = Math.round((user.totalPlayTime || 0) / 60 * 10) / 10
|
||||||
const levelInfo = user.getLevelInfo()
|
|
||||||
|
return ApiResponse.success(res, {
|
||||||
// 计算探索时长(分钟转小时,保留1位小数)
|
level: levelInfo.level,
|
||||||
const playTimeHours = Math.round((user.totalPlayTime || 0) / 60 * 10) / 10
|
levelName: levelInfo.name,
|
||||||
|
levelPoints: levelInfo.points,
|
||||||
return ApiResponse.success(res, {
|
levelProgress: levelInfo.progress,
|
||||||
level: levelInfo.level,
|
totalProgress: levelInfo.totalProgress,
|
||||||
levelName: levelInfo.name,
|
nextLevelPoints: levelInfo.nextLevelPoints,
|
||||||
levelPoints: levelInfo.points,
|
totalPlayTime: playTimeHours,
|
||||||
levelProgress: levelInfo.progress,
|
totalScore: user.totalScore || 0,
|
||||||
totalProgress: levelInfo.totalProgress,
|
worksCount: 0,
|
||||||
nextLevelPoints: levelInfo.nextLevelPoints,
|
collectionCount: user.collectedKnowledge?.length || 0,
|
||||||
totalPlayTime: playTimeHours,
|
achievementCount: user.achievements?.length || 0,
|
||||||
totalScore: user.totalScore || 0,
|
unlockedDims: user.unlockedDims || [1]
|
||||||
worksCount: 0,
|
})
|
||||||
collectionCount: user.collectedKnowledge?.length || 0,
|
|
||||||
achievementCount: user.achievements?.length || 0,
|
} catch (error) {
|
||||||
unlockedDims: user.unlockedDims || [1]
|
logger.error('获取用户统计失败:', error)
|
||||||
})
|
return ApiResponse.serverError(res, error.message)
|
||||||
|
}
|
||||||
} catch (error) {
|
})
|
||||||
logger.error('获取用户统计失败:', error)
|
|
||||||
return ApiResponse.serverError(res, error.message)
|
/**
|
||||||
}
|
* 获取用户作品列表
|
||||||
})
|
* GET /api/user/works
|
||||||
|
* 说明:画廊(Gallery)功能已移除,统一返回空列表
|
||||||
/**
|
*/
|
||||||
* 获取用户作品列表
|
router.get('/works', async (req, res) => {
|
||||||
* GET /api/user/works
|
try {
|
||||||
* 说明:画廊(Gallery)功能已移除,统一返回空列表
|
const user = req.user
|
||||||
*/
|
const { page = 1, limit = 20 } = req.query
|
||||||
router.get('/works', async (req, res) => {
|
|
||||||
try {
|
return ApiResponse.paginated(res, [], {
|
||||||
const user = req.user
|
page: parseInt(page),
|
||||||
const { page = 1, limit = 20 } = req.query
|
limit: parseInt(limit),
|
||||||
|
total: 0
|
||||||
return ApiResponse.paginated(res, [], {
|
})
|
||||||
page: parseInt(page),
|
|
||||||
limit: parseInt(limit),
|
} catch (error) {
|
||||||
total: 0
|
logger.error('获取用户作品失败:', error)
|
||||||
})
|
return ApiResponse.serverError(res, error.message)
|
||||||
|
}
|
||||||
} catch (error) {
|
})
|
||||||
logger.error('获取用户作品失败:', error)
|
|
||||||
return ApiResponse.serverError(res, error.message)
|
module.exports = router
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
module.exports = router
|
|
||||||
|
|||||||
Reference in New Issue
Block a user