🎉 feat: initialize 宇之然AI维度 project

- uni-app Vue3 frontend with dark glassmorphism theme
- 5 AI dimensions: Origin / Development / Current / Learning / Trend
- AI Chat system prompt updated from geometry to AI
- 23 AI knowledge articles initialized in DB
- Trend API + cron job for daily news
- Product pricing (Pro ¥19.9, VIP ¥39.9)
- Pinia stores + API utilities
- AGENTS.md documentation
This commit is contained in:
Yuzhiran Dev
2026-07-10 13:26:04 +08:00
commit 24cd9cef53
107 changed files with 20665 additions and 0 deletions
+424
View File
@@ -0,0 +1,424 @@
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')
// 应用用户认证中间件到所有路由
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/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)
}
})
/**
* 获取用户成就
* GET /api/user/achievements
*/
router.get('/achievements', async (req, res) => {
try {
const user = req.user
// 生成成就数据
const achievements = [
{
name: '探索成就',
achievements: [
{
id: 'dim1_complete',
name: '一维探索者',
description: '完成一维空间的探索',
icon: '⭐',
unlocked: user.exploreData.dim1?.completed || false,
date: user.exploreData.dim1?.completed ? user.updatedAt.toISOString().split('T')[0] : null
},
{
id: 'dim2_complete',
name: '二维探索者',
description: '完成二维空间的探索',
icon: '✨',
unlocked: user.exploreData.dim2?.completed || false,
date: user.exploreData.dim2?.completed ? user.updatedAt.toISOString().split('T')[0] : null
},
{
id: 'dim3_complete',
name: '三维探索者',
description: '完成三维空间的探索',
icon: '🌟',
unlocked: user.exploreData.dim3?.completed || false,
date: user.exploreData.dim3?.completed ? user.updatedAt.toISOString().split('T')[0] : null
},
{
id: 'dim4_complete',
name: '四维探索者',
description: '完成四维空间的探索',
icon: '💫',
unlocked: user.exploreData.dim4?.completed || false,
date: user.exploreData.dim4?.completed ? user.updatedAt.toISOString().split('T')[0] : null
},
{
id: 'dim5_complete',
name: '五维探索者',
description: '完成五维空间的探索',
icon: '🌌',
unlocked: user.exploreData.dim5?.completed || false,
date: user.exploreData.dim5?.completed ? user.updatedAt.toISOString().split('T')[0] : null
}
]
},
{
name: '创作成就',
achievements: [
{
id: 'first_work',
name: '初次创作',
description: '创作你的第一个作品',
icon: '🎨',
unlocked: user.achievements?.includes('first_work') || false,
date: user.achievements?.includes('first_work') ? user.updatedAt.toISOString().split('T')[0] : null
},
{
id: 'five_works',
name: '创作达人',
description: '创作5个作品',
icon: '🏆',
unlocked: user.achievements?.includes('five_works') || false,
date: user.achievements?.includes('five_works') ? user.updatedAt.toISOString().split('T')[0] : null
},
{
id: 'ten_works',
name: '创作大师',
description: '创作10个作品',
icon: '👑',
unlocked: user.achievements?.includes('ten_works') || false,
date: user.achievements?.includes('ten_works') ? user.updatedAt.toISOString().split('T')[0] : null
}
]
},
{
name: '收藏成就',
achievements: [
{
id: 'first_collection',
name: '初次收藏',
description: '收藏你的第一个知识',
icon: '❤️',
unlocked: user.collectedKnowledge?.length > 0,
date: user.collectedKnowledge?.length > 0 ? user.updatedAt.toISOString().split('T')[0] : null
},
{
id: 'five_collections',
name: '收藏爱好者',
description: '收藏5个知识',
icon: '🧡',
unlocked: user.collectedKnowledge?.length >= 5,
date: user.collectedKnowledge?.length >= 5 ? user.updatedAt.toISOString().split('T')[0] : null
},
{
id: 'ten_collections',
name: '收藏大师',
description: '收藏10个知识',
icon: '💛',
unlocked: user.collectedKnowledge?.length >= 10,
date: user.collectedKnowledge?.length >= 10 ? 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