🎉 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
File diff suppressed because it is too large Load Diff
+240
View File
@@ -0,0 +1,240 @@
const express = require('express')
const router = express.Router()
const { AIChatQuota, AIModel } = require('../models')
const ApiResponse = require('../utils/response')
const { userAuthMiddleware } = require('../middleware/auth')
const logger = require('../utils/logger')
/**
* 获取用户AI问答配额
* GET /api/ai-chat/quota
*/
router.get('/quota', userAuthMiddleware, async (req, res) => {
try {
const openid = req.openid || req.user?.openid
if (!openid) {
return ApiResponse.error(res, '无法获取用户标识', 400)
}
let quota = await AIChatQuota.findOne({ openid })
if (!quota) {
// 创建新的配额记录,默认5次免费
quota = new AIChatQuota({ openid })
await quota.save()
}
// 检查是否需要重置每日使用次数
quota.resetDailyUsed()
await quota.save()
return ApiResponse.success(res, {
freeQuota: quota.freeQuota,
sharedQuota: quota.sharedQuota,
usedQuota: quota.usedQuota,
remainingQuota: quota.getRemainingQuota(),
dailyUsed: quota.dailyUsed
})
} catch (error) {
logger.error('获取AI问答配额失败:', error)
return ApiResponse.serverError(res, error.message)
}
})
/**
* AI问答接口
* POST /api/ai-chat/ask
*/
router.post('/ask', userAuthMiddleware, async (req, res) => {
try {
const openid = req.openid || req.user?.openid
const { question, dimension = 'general' } = req.body
if (!openid) {
return ApiResponse.error(res, '无法获取用户标识', 400)
}
if (!question || question.trim().length === 0) {
return ApiResponse.error(res, '请输入问题', 400)
}
// 检查用户配额
let quota = await AIChatQuota.findOne({ openid })
if (!quota) {
quota = new AIChatQuota({ openid })
await quota.save()
}
if (!quota.hasQuota()) {
return ApiResponse.error(res, '问答次数已用完,分享好友可获得更多次数', 403)
}
// 获取当前使用的AI模型配置
const aiModel = await AIModel.getCurrentModel()
let apiUrl, apiKey, modelId, modelConfig
if (aiModel) {
// 使用数据库配置的模型
apiUrl = aiModel.apiUrl
apiKey = aiModel.apiKey
modelId = aiModel.modelId
modelConfig = aiModel.config
} else {
// 使用环境变量配置的模型
apiUrl = process.env.OPENAI_API_URL || 'https://api.openai.com/v1/chat/completions'
apiKey = process.env.OPENAI_API_KEY
modelId = process.env.OPENAI_MODEL || 'gpt-3.5-turbo'
modelConfig = { temperature: 0.7, maxTokens: 800, topP: 1 }
}
// 检查API配置
if (!apiKey) {
logger.error('AI API Key未配置')
return ApiResponse.error(res, 'AI服务暂时不可用', 500)
}
// 构建系统提示词
const systemPrompt = getSystemPromptByDimension(dimension)
// 调用AI API
const response = await fetch(apiUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${apiKey}`
},
body: JSON.stringify({
model: modelId,
messages: [
{ role: 'system', content: systemPrompt },
{ role: 'user', content: question }
],
temperature: modelConfig.temperature || 0.7,
max_tokens: modelConfig.maxTokens || 800,
top_p: modelConfig.topP || 1
})
})
if (!response.ok) {
const errorData = await response.json()
logger.error('OpenAI API错误:', errorData)
return ApiResponse.error(res, 'AI服务响应异常', 500)
}
const data = await response.json()
const answer = data.choices[0].message.content
// 扣除一次使用次数
await quota.useQuota()
return ApiResponse.success(res, {
answer,
remainingQuota: quota.getRemainingQuota()
})
} catch (error) {
logger.error('AI问答失败:', error)
return ApiResponse.serverError(res, error.message)
}
})
/**
* 分享获得问答次数
* POST /api/ai-chat/share-gain
*/
router.post('/share-gain', userAuthMiddleware, async (req, res) => {
try {
const openid = req.openid || req.user?.openid
if (!openid) {
return ApiResponse.error(res, '无法获取用户标识', 400)
}
let quota = await AIChatQuota.findOne({ openid })
if (!quota) {
quota = new AIChatQuota({ openid })
}
// 增加5次分享获得的次数
const remainingQuota = await quota.addQuotaByShare(5)
return ApiResponse.success(res, {
message: '分享成功,获得5次问答机会',
gainedQuota: 5,
remainingQuota
})
} catch (error) {
logger.error('分享获得次数失败:', error)
return ApiResponse.serverError(res, error.message)
}
})
/**
* 根据维度获取系统提示词
*/
function getSystemPromptByDimension(dimension) {
const prompts = {
dim1: `你是"宇之然AI维度"的AI助手,专门解答关于 AI 起源和历史的问题。
请用通俗易懂的语言解释人工智能发展史上的重要事件、人物和里程碑,如:
- 图灵测试(1950)与艾伦·图灵的贡献
- 达特茅斯会议(1956)——AI 的诞生
- 深蓝 vs 卡斯帕罗夫(1997)
- AlphaGo 与深度学习革命(2016
- GPT 系列与大模型时代的开启
回答要简洁有趣,适合普通用户理解。`,
dim2: `你是"宇之然AI维度"的AI助手,专门解答关于 AI 技术发展的问题。
请用通俗易懂的语言解释人工智能核心技术演进,如:
- 机器学习与深度学习的基本原理
- Transformer 架构与自注意力机制
- 大规模预训练模型(GPT、BERT等)
- 多模态 AI、MoE 架构
- Agent 智能体技术
回答要简洁有趣,适合普通用户理解。`,
dim3: `你是"宇之然AI维度"的AI助手,专门解答关于当前 AI 行业格局的问题。
请用通俗易懂的语言解释当前 AI 领域的最新动态,如:
- 各大模型公司(OpenAI、Google、DeepSeek、Anthropic、Meta)的竞争格局
- 开源 vs 闭源模型的生态对比
- 具身智能与世界模型的最新进展
- AI 应用在各行业的落地情况
- Agent 智能体的发展现状
回答要简洁有趣,适合普通用户理解。`,
dim4: `你是"宇之然AI维度"的AI助手,专门解答关于 AI 学习和工具使用的问题。
请用通俗易懂的语言解释 AI 学习路线和工具使用方法,如:
- 提示词工程(Prompt Engineering)入门
- 检索增强生成(RAG)的原理与实践
- 模型微调(Fine-tuning)基础
- 主流 AI 开发框架(LangChain、Dify 等)
- 常用 AI 工具推荐与使用技巧
回答要简洁有趣,适合普通用户理解。`,
dim5: `你是"宇之然AI维度"的AI助手,专门解答关于 AI 趋势和前沿动态的问题。
请用通俗易懂的语言介绍 AI 领域的最新趋势,如:
- 最新 AI 产品发布动态
- 前沿论文与研究方向
- 行业趋势分析与预测
- AI 政策法规与伦理讨论
回答要简洁有趣,适合普通用户理解。`,
general: `你是"宇之然AI维度"的AI助手,致力于帮助用户系统化理解人工智能。
你可以回答关于 AI 的任何问题,包括但不限于:
- AI 历史与起源
- 核心技术原理
- 当前行业格局
- 学习路线与工具使用
- 最新趋势与前沿动态
你的回答要简洁、通俗易懂,适合普通用户理解。如果用户问的是某个具体维度的知识,可以引导用户去对应的维度模块深入学习。`
}
return prompts[dimension] || prompts.general
}
module.exports = router
+351
View File
@@ -0,0 +1,351 @@
const express = require('express')
const router = express.Router()
const { AIModel } = require('../models')
const { authMiddleware, requirePermission } = require('../middleware/auth')
const ApiResponse = require('../utils/response')
const logger = require('../utils/logger')
// 所有路由都需要认证
router.use(authMiddleware)
/**
* 获取当前使用的模型(公开接口,用于AI问答)
* GET /api/ai-model/current
*/
router.get('/current', async (req, res) => {
try {
const model = await AIModel.getCurrentModel()
if (!model) {
// 如果没有配置模型,返回环境变量中的默认配置
return ApiResponse.success(res, {
name: '默认模型',
modelId: process.env.OPENAI_MODEL || 'gpt-3.5-turbo',
apiUrl: process.env.OPENAI_API_URL || 'https://api.openai.com/v1/chat/completions',
apiKey: process.env.OPENAI_API_KEY || '',
config: {
temperature: 0.7,
maxTokens: 800,
topP: 1
}
})
}
// 返回模型信息(不返回API Key)
return ApiResponse.success(res, {
name: model.name,
modelId: model.modelId,
config: model.config
})
} catch (error) {
logger.error('获取当前模型失败:', error)
return ApiResponse.serverError(res, error.message)
}
})
/**
* 获取模型列表(管理后台)
* GET /api/ai-model/list
*/
router.get('/list', requirePermission('dashboard:read'), async (req, res) => {
try {
const { page = 1, pageSize = 10, keyword = '' } = req.query
const query = keyword
? { $or: [
{ name: { $regex: keyword, $options: 'i' } },
{ modelId: { $regex: keyword, $options: 'i' } }
]}
: {}
const [list, total] = await Promise.all([
AIModel.find(query)
.sort({ isDefault: -1, priority: 1, createdAt: -1 })
.skip((page - 1) * pageSize)
.limit(parseInt(pageSize)),
AIModel.countDocuments(query)
])
return ApiResponse.success(res, {
list,
total,
page: parseInt(page),
pageSize: parseInt(pageSize)
})
} catch (error) {
logger.error('获取模型列表失败:', error)
return ApiResponse.serverError(res, error.message)
}
})
/**
* 获取所有启用的模型(下拉选择用)
* GET /api/ai-model/active
*/
router.get('/active', requirePermission('dashboard:read'), async (req, res) => {
try {
const models = await AIModel.getActiveModels()
return ApiResponse.success(res, {
list: models.map(m => ({
_id: m._id,
name: m.name,
modelId: m.modelId,
isDefault: m.isDefault
}))
})
} catch (error) {
logger.error('获取启用模型失败:', error)
return ApiResponse.serverError(res, error.message)
}
})
/**
* 获取模型详情
* GET /api/ai-model/:id
*/
router.get('/:id', requirePermission('dashboard:read'), async (req, res) => {
try {
const { id } = req.params
const model = await AIModel.findById(id)
if (!model) {
return ApiResponse.notFound(res, '模型不存在')
}
return ApiResponse.success(res, { model })
} catch (error) {
logger.error('获取模型详情失败:', error)
return ApiResponse.serverError(res, error.message)
}
})
/**
* 创建模型
* POST /api/ai-model
*/
router.post('/', requirePermission('settings:write'), async (req, res) => {
try {
const { name, modelId, apiUrl, apiKey, description, config, priority } = req.body
// 验证必填字段
if (!name || !modelId || !apiUrl || !apiKey) {
return ApiResponse.error(res, '请填写所有必填字段', 400)
}
// 检查modelId是否已存在
const existingModel = await AIModel.findOne({ modelId })
if (existingModel) {
return ApiResponse.error(res, '该模型ID已存在', 400)
}
const model = new AIModel({
name,
modelId,
apiUrl,
apiKey,
description,
config,
priority
})
await model.save()
logger.info('创建AI模型:', { name, modelId, admin: req.admin.username })
return ApiResponse.success(res, {
message: '创建成功',
model: {
_id: model._id,
name: model.name,
modelId: model.modelId,
isDefault: model.isDefault,
isActive: model.isActive
}
})
} catch (error) {
logger.error('创建模型失败:', error)
return ApiResponse.serverError(res, error.message)
}
})
/**
* 更新模型
* PUT /api/ai-model/:id
*/
router.put('/:id', requirePermission('settings:write'), async (req, res) => {
try {
const { id } = req.params
const { name, modelId, apiUrl, apiKey, description, isActive, isDefault, config, priority } = req.body
const model = await AIModel.findById(id)
if (!model) {
return ApiResponse.notFound(res, '模型不存在')
}
// 如果修改了modelId,检查是否与其他模型冲突
if (modelId && modelId !== model.modelId) {
const existingModel = await AIModel.findOne({ modelId, _id: { $ne: id } })
if (existingModel) {
return ApiResponse.error(res, '该模型ID已存在', 400)
}
}
// 更新字段
if (name) model.name = name
if (modelId) model.modelId = modelId
if (apiUrl) model.apiUrl = apiUrl
if (apiKey) model.apiKey = apiKey
if (description !== undefined) model.description = description
if (isActive !== undefined) model.isActive = isActive
if (isDefault !== undefined) model.isDefault = isDefault
if (config) model.config = { ...model.config, ...config }
if (priority !== undefined) model.priority = priority
await model.save()
logger.info('更新AI模型:', { id, name: model.name, admin: req.admin.username })
return ApiResponse.success(res, {
message: '更新成功',
model: {
_id: model._id,
name: model.name,
modelId: model.modelId,
isDefault: model.isDefault,
isActive: model.isActive
}
})
} catch (error) {
logger.error('更新模型失败:', error)
return ApiResponse.serverError(res, error.message)
}
})
/**
* 删除模型
* DELETE /api/ai-model/:id
*/
router.delete('/:id', requirePermission('settings:write'), async (req, res) => {
try {
const { id } = req.params
const model = await AIModel.findById(id)
if (!model) {
return ApiResponse.notFound(res, '模型不存在')
}
// 检查是否是唯一的默认模型
if (model.isDefault) {
const defaultCount = await AIModel.countDocuments({ isDefault: true })
if (defaultCount <= 1) {
return ApiResponse.error(res, '不能删除唯一的默认模型,请先设置其他默认模型', 400)
}
}
await AIModel.findByIdAndDelete(id)
logger.info('删除AI模型:', { id, name: model.name, admin: req.admin.username })
return ApiResponse.success(res, { message: '删除成功' })
} catch (error) {
logger.error('删除模型失败:', error)
return ApiResponse.serverError(res, error.message)
}
})
/**
* 设置默认模型
* POST /api/ai-model/:id/set-default
*/
router.post('/:id/set-default', requirePermission('settings:write'), async (req, res) => {
try {
const { id } = req.params
const model = await AIModel.findById(id)
if (!model) {
return ApiResponse.notFound(res, '模型不存在')
}
if (!model.isActive) {
return ApiResponse.error(res, '不能将禁用模型设为默认', 400)
}
model.isDefault = true
await model.save()
logger.info('设置默认AI模型:', { id, name: model.name, admin: req.admin.username })
return ApiResponse.success(res, {
message: '设置默认模型成功',
model: {
_id: model._id,
name: model.name,
modelId: model.modelId,
isDefault: true
}
})
} catch (error) {
logger.error('设置默认模型失败:', error)
return ApiResponse.serverError(res, error.message)
}
})
/**
* 初始化默认模型(从环境变量)
* POST /api/ai-model/init-default
*/
router.post('/init-default', requirePermission('settings:write'), async (req, res) => {
try {
// 检查是否已有模型
const existingCount = await AIModel.countDocuments()
if (existingCount > 0) {
return ApiResponse.error(res, '已有模型配置,如需重新初始化请先删除现有模型', 400)
}
// 从环境变量创建默认模型
const defaultModel = new AIModel({
name: '默认模型',
modelId: process.env.OPENAI_MODEL || 'gpt-3.5-turbo',
apiUrl: process.env.OPENAI_API_URL || 'https://api.openai.com/v1/chat/completions',
apiKey: process.env.OPENAI_API_KEY || '',
description: '从环境变量初始化的默认模型',
isDefault: true,
isActive: true,
priority: 0
})
await defaultModel.save()
logger.info('初始化默认AI模型:', { admin: req.admin.username })
return ApiResponse.success(res, {
message: '初始化默认模型成功',
model: {
_id: defaultModel._id,
name: defaultModel.name,
modelId: defaultModel.modelId,
isDefault: true
}
})
} catch (error) {
logger.error('初始化默认模型失败:', error)
return ApiResponse.serverError(res, error.message)
}
})
module.exports = router
+492
View File
@@ -0,0 +1,492 @@
const express = require('express')
const router = express.Router()
const jwt = require('jsonwebtoken')
const { User, Admin, AdminLog, LoginLog } = require('../models')
const ApiResponse = require('../utils/response')
const weChatService = require('../utils/weixin')
const logger = require('../utils/logger')
const axios = require('axios')
/**
* 获取客户端IP
*/
const getClientIp = (req) => {
return req.headers['x-forwarded-for'] ||
req.headers['x-real-ip'] ||
req.connection.remoteAddress ||
req.socket.remoteAddress ||
'0.0.0.0'
}
/**
* 解析User-Agent
*/
const parseUserAgent = (userAgent) => {
if (!userAgent) return { device: '', os: '', browser: '' }
const device = /Mobile|Android|iPhone|iPad|iPod/i.test(userAgent) ? 'mobile' : 'desktop'
let os = ''
if (/Windows/i.test(userAgent)) os = 'Windows'
else if (/Mac OS X/i.test(userAgent)) os = 'MacOS'
else if (/Android/i.test(userAgent)) os = 'Android'
else if (/iOS|iPhone|iPad|iPod/i.test(userAgent)) os = 'iOS'
else if (/Linux/i.test(userAgent)) os = 'Linux'
else os = 'Unknown'
let browser = ''
if (/MicroMessenger/i.test(userAgent)) browser = 'WeChat'
else if (/Chrome/i.test(userAgent)) browser = 'Chrome'
else if (/Safari/i.test(userAgent)) browser = 'Safari'
else if (/Firefox/i.test(userAgent)) browser = 'Firefox'
else if (/Edge/i.test(userAgent)) browser = 'Edge'
else browser = 'Unknown'
return { device, os, browser }
}
/**
* 记录登录日志
*/
const recordLoginLog = async (userId, openid, type, method, source, req, success = true, failReason = '') => {
try {
const ip = getClientIp(req)
const userAgent = req.headers['user-agent'] || ''
const { device, os, browser } = parseUserAgent(userAgent)
await LoginLog.create({
userId,
openid,
type,
method,
source,
ip,
userAgent,
device,
os,
browser,
success,
failReason
})
} catch (error) {
logger.error('记录登录日志失败:', error)
}
}
/**
* 小程序登录
* POST /api/auth/login
*/
router.post('/login', async (req, res) => {
try {
const { code, userInfo, source = 'miniapp' } = req.body
// 方式1: 通过 code 登录(推荐)
let openid = null
if (code) {
const sessionData = await weChatService.code2Session(code)
openid = sessionData.openid
}
// 方式2: 开发环境直接使用 openid (已废弃,仅用于本地调试)
if (!openid && process.env.NODE_ENV === 'development' && req.body.openid) {
openid = req.body.openid
}
if (!openid) {
return ApiResponse.error(res, '无法获取用户标识', 400)
}
// 查找或创建用户
let user = await User.findOne({ openid })
let isNewUser = false
if (!user) {
// 新用户
user = new User({
openid,
nickName: userInfo?.nickName || '星辰旅行者',
avatarUrl: userInfo?.avatarUrl || '',
unlockedDims: [1]
})
isNewUser = true
logger.info(`新用户注册: ${openid}`)
} else {
// 老用户,更新登录时间
user.lastLoginAt = new Date()
if (userInfo?.nickName) user.nickName = userInfo.nickName
if (userInfo?.avatarUrl) user.avatarUrl = userInfo.avatarUrl
}
await user.save()
// 记录登录日志
await recordLoginLog(
user._id,
openid,
isNewUser ? 'register' : 'login',
'wechat_miniapp',
source,
req,
true
)
// 生成 JWT token(可选,小程序通常不需要)
const token = jwt.sign(
{ openid, userId: user._id },
process.env.JWT_SECRET,
{ expiresIn: process.env.JWT_EXPIRE }
)
return ApiResponse.success(res, {
token,
openid,
isNewUser,
userInfo: {
nickName: user.nickName,
avatarUrl: user.avatarUrl
},
firstPlayDate: user.createdAt
}, isNewUser ? '注册成功' : '登录成功')
} catch (error) {
logger.error('登录失败:', error)
return ApiResponse.serverError(res, error.message)
}
})
/**
* WebView 静默登录
* POST /api/auth/webview-login
* 用于微信小程序 webview 内嵌 H5 的自动登录
*/
router.post('/webview-login', async (req, res) => {
try {
const { code, userInfo } = req.body
if (!code) {
return ApiResponse.error(res, '缺少登录凭证', 400)
}
// 通过 code 获取 openid
let openid = null
try {
const sessionData = await weChatService.code2Session(code)
openid = sessionData.openid
} catch (error) {
logger.error('WebView登录获取session失败:', error)
return ApiResponse.error(res, '登录凭证无效', 401)
}
if (!openid) {
return ApiResponse.error(res, '无法获取用户标识', 400)
}
// 查找或创建用户
let user = await User.findOne({ openid })
let isNewUser = false
if (!user) {
// 新用户 - 使用微信提供的信息创建账户
user = new User({
openid,
nickName: userInfo?.nickName || '星辰旅行者',
avatarUrl: userInfo?.avatarUrl || '',
unlockedDims: [1]
})
isNewUser = true
logger.info(`WebView新用户注册: ${openid}`)
} else {
// 老用户,更新登录时间和信息
user.lastLoginAt = new Date()
if (userInfo?.nickName) user.nickName = userInfo.nickName
if (userInfo?.avatarUrl) user.avatarUrl = userInfo.avatarUrl
logger.info(`WebView老用户登录: ${openid}`)
}
await user.save()
// 记录登录日志
await recordLoginLog(
user._id,
openid,
isNewUser ? 'register' : 'auto_login',
'wechat_webview',
'webview',
req,
true
)
// 生成 JWT token
const token = jwt.sign(
{ openid, userId: user._id },
process.env.JWT_SECRET,
{ expiresIn: process.env.JWT_EXPIRE }
)
return ApiResponse.success(res, {
token,
openid,
isNewUser,
userInfo: {
nickName: user.nickName,
avatarUrl: user.avatarUrl
},
firstPlayDate: user.createdAt
}, isNewUser ? '注册成功' : '自动登录成功')
} catch (error) {
logger.error('WebView登录失败:', error)
return ApiResponse.serverError(res, error.message)
}
})
/**
* 管理员登录
* POST /api/auth/admin/login
*/
router.post('/admin/login', async (req, res) => {
try {
const { username, password } = req.body
// 查找管理员
const admin = await Admin.findOne({ username })
if (!admin) {
return ApiResponse.error(res, '用户名或密码错误', 401)
}
// 验证密码
const isMatch = await admin.validatePassword(password)
if (!isMatch) {
return ApiResponse.error(res, '用户名或密码错误', 401)
}
// 检查状态
if (admin.status !== 'active') {
return ApiResponse.error(res, '账户已禁用', 403)
}
// 更新登录信息
admin.lastLogin = new Date()
admin.lastLoginIp = req.ip
admin.loginCount += 1
await admin.save()
// 记录日志
await AdminLog.create({
adminId: admin._id,
adminName: admin.username,
action: 'login',
resource: 'system',
ip: req.ip,
userAgent: req.get('User-Agent')
})
// 生成 JWT token
const token = jwt.sign(
{ adminId: admin._id, role: admin.role },
process.env.JWT_SECRET,
{ expiresIn: process.env.JWT_EXPIRE }
)
// 获取权限
const permissions = Admin.getPermissionsByRole(admin.role)
return ApiResponse.success(res, {
token,
adminId: admin._id,
username: admin.username,
role: admin.role,
permissions
}, '登录成功')
} catch (error) {
logger.error('管理员登录失败:', error)
return ApiResponse.serverError(res, error.message)
}
})
/**
* H5环境用户名密码登录
* POST /api/auth/user/login
*/
router.post('/user/login', async (req, res) => {
try {
const { username, password } = req.body
// 查找用户(显式包含密码字段)
const user = await User.findOne({ username }).select('+password')
if (!user) {
return ApiResponse.error(res, '用户名或密码错误', 401)
}
// 验证密码
const isMatch = await user.validatePassword(password)
if (!isMatch) {
return ApiResponse.error(res, '用户名或密码错误', 401)
}
// 更新登录时间
user.lastLoginAt = new Date()
await user.save()
// 生成 JWT token
const token = jwt.sign(
{ userId: user._id, openid: user.openid },
process.env.JWT_SECRET,
{ expiresIn: process.env.JWT_EXPIRE }
)
return ApiResponse.success(res, {
token,
userId: user._id,
openid: user.openid,
userInfo: {
nickName: user.nickName,
avatarUrl: user.avatarUrl
},
firstPlayDate: user.createdAt
}, '登录成功')
} catch (error) {
logger.error('用户登录失败:', error)
return ApiResponse.serverError(res, error.message)
}
})
/**
* H5环境用户注册
* POST /api/auth/user/register
*/
router.post('/user/register', async (req, res) => {
try {
const { username, password, nickName, avatarUrl } = req.body
// 检查用户名是否已存在
const existingUser = await User.findOne({ username })
if (existingUser) {
return ApiResponse.error(res, '用户名已存在', 400)
}
// 创建新用户
const user = new User({
username,
password,
nickName: nickName || '星辰旅行者',
avatarUrl: avatarUrl || '',
unlockedDims: [1]
})
await user.save()
logger.info(`新用户注册: ${username}`)
// 生成 JWT token
const token = jwt.sign(
{ userId: user._id },
process.env.JWT_SECRET,
{ expiresIn: process.env.JWT_EXPIRE }
)
return ApiResponse.success(res, {
token,
userId: user._id,
userInfo: {
nickName: user.nickName,
avatarUrl: user.avatarUrl
}
}, '注册成功')
} catch (error) {
logger.error('用户注册失败:', error)
return ApiResponse.serverError(res, error.message)
}
})
/**
* 获取当前用户信息
* GET /api/auth/me
*/
router.get('/me', async (req, res) => {
try {
const token = req.header('Authorization')?.replace('Bearer ', '')
if (!token) {
return ApiResponse.unauthorized(res)
}
const decoded = jwt.verify(token, process.env.JWT_SECRET)
const user = await User.findById(decoded.userId)
if (!user) {
return ApiResponse.notFound(res, '用户不存在')
}
return ApiResponse.success(res, user)
} catch (error) {
logger.error('获取用户信息失败:', error)
return ApiResponse.serverError(res, error.message)
}
})
/**
* WebView Token 交换接口
* POST /api/auth/webview-token
* 用于小程序 WebView 内嵌 H5 时,通过 openid 获取登录 token
* 小程序在 URL 中传递 openidH5 用这个接口换取 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,
'webview_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
+201
View File
@@ -0,0 +1,201 @@
const express = require('express')
const router = express.Router()
const { BGM } = require('../models')
const ApiResponse = require('../utils/response')
const { authMiddleware } = require('../middleware/auth')
const logger = require('../utils/logger')
/**
* 获取指定维度的随机BGM(公开接口,移动端调用)
* GET /api/bgm/random/:dimension
*/
router.get('/random/:dimension', async (req, res) => {
try {
const dimension = parseInt(req.params.dimension)
if (![1, 2, 3, 4, 5].includes(dimension)) {
return ApiResponse.badRequest(res, '无效的维度参数')
}
// 查询该维度下所有激活的BGM
const bgms = await BGM.find({
dimension,
isActive: true
}).sort({ sortOrder: 1 })
if (!bgms || bgms.length === 0) {
return ApiResponse.success(res, null)
}
// 随机选择一个
const randomIndex = Math.floor(Math.random() * bgms.length)
const selectedBGM = bgms[randomIndex]
return ApiResponse.success(res, {
id: selectedBGM._id,
name: selectedBGM.name,
url: selectedBGM.url,
duration: selectedBGM.duration,
loop: selectedBGM.loop,
volume: selectedBGM.volume,
dimension: selectedBGM.dimension
})
} catch (error) {
logger.error('获取随机BGM失败:', error)
return ApiResponse.serverError(res, error.message)
}
})
/**
* 获取所有BGM列表(管理员)
* GET /api/admin/bgm/list
*/
router.get('/list', authMiddleware, async (req, res) => {
try {
const { dimension, page = 1, limit = 20 } = req.query
const query = {}
if (dimension && [1, 2, 3, 4, 5].includes(parseInt(dimension))) {
query.dimension = parseInt(dimension)
}
const pageNum = parseInt(page)
const limitNum = parseInt(limit)
const skip = (pageNum - 1) * limitNum
const [bgms, total] = await Promise.all([
BGM.find(query)
.sort({ dimension: 1, sortOrder: 1, createdAt: -1 })
.skip(skip)
.limit(limitNum),
BGM.countDocuments(query)
])
return ApiResponse.success(res, {
list: bgms,
total,
page: pageNum,
limit: limitNum,
totalPages: Math.ceil(total / limitNum)
})
} catch (error) {
logger.error('获取BGM列表失败:', error)
return ApiResponse.serverError(res, error.message)
}
})
/**
* 创建BGM(管理员)
* POST /api/admin/bgm/create
*/
router.post('/create', authMiddleware, async (req, res) => {
try {
const { name, dimension, url, duration, loop, volume, sortOrder, description } = req.body
if (!name || !dimension || !url) {
return ApiResponse.badRequest(res, '名称、维度和URL为必填项')
}
if (![1, 2, 3, 4, 5].includes(parseInt(dimension))) {
return ApiResponse.badRequest(res, '无效的维度参数')
}
const bgm = new BGM({
name,
dimension: parseInt(dimension),
url,
duration: duration || 0,
loop: loop !== undefined ? loop : true,
volume: volume !== undefined ? volume : 0.5,
sortOrder: sortOrder || 0,
description: description || ''
})
await bgm.save()
logger.info(`管理员创建了BGM: ${name} (维度${dimension})`)
return ApiResponse.success(res, { message: 'BGM创建成功', data: bgm })
} catch (error) {
logger.error('创建BGM失败:', error)
return ApiResponse.serverError(res, error.message)
}
})
/**
* 更新BGM(管理员)
* PUT /api/admin/bgm/update/:id
*/
router.put('/update/:id', authMiddleware, async (req, res) => {
try {
const { id } = req.params
const updates = req.body
const bgm = await BGM.findByIdAndUpdate(
id,
{ $set: updates },
{ new: true, runValidators: true }
)
if (!bgm) {
return ApiResponse.notFound(res, 'BGM不存在')
}
logger.info(`管理员更新了BGM: ${bgm.name}`)
return ApiResponse.success(res, { message: 'BGM更新成功', data: bgm })
} catch (error) {
logger.error('更新BGM失败:', error)
return ApiResponse.serverError(res, error.message)
}
})
/**
* 删除BGM(管理员)
* DELETE /api/admin/bgm/delete/:id
*/
router.delete('/delete/:id', authMiddleware, async (req, res) => {
try {
const { id } = req.params
const bgm = await BGM.findByIdAndDelete(id)
if (!bgm) {
return ApiResponse.notFound(res, 'BGM不存在')
}
logger.info(`管理员删除了BGM: ${bgm.name}`)
return ApiResponse.success(res, { message: 'BGM删除成功' })
} catch (error) {
logger.error('删除BGM失败:', error)
return ApiResponse.serverError(res, error.message)
}
})
/**
* 切换BGM激活状态(管理员)
* PUT /api/admin/bgm/toggle/:id
*/
router.put('/toggle/:id', authMiddleware, async (req, res) => {
try {
const { id } = req.params
const bgm = await BGM.findById(id)
if (!bgm) {
return ApiResponse.notFound(res, 'BGM不存在')
}
bgm.isActive = !bgm.isActive
await bgm.save()
logger.info(`管理员${bgm.isActive ? '激活' : '禁用'}了BGM: ${bgm.name}`)
return ApiResponse.success(res, {
message: `BGM已${bgm.isActive ? '激活' : '禁用'}`,
data: bgm
})
} catch (error) {
logger.error('切换BGM状态失败:', error)
return ApiResponse.serverError(res, error.message)
}
})
module.exports = router
+228
View File
@@ -0,0 +1,228 @@
const express = require('express')
const router = express.Router()
const { Feedback, User } = require('../models')
const ApiResponse = require('../utils/response')
const { userAuthMiddleware } = require('../middleware/auth')
const logger = require('../utils/logger')
/**
* 获取客户端IP
*/
const getClientIp = (req) => {
return req.headers['x-forwarded-for'] ||
req.headers['x-real-ip'] ||
req.connection.remoteAddress ||
req.socket.remoteAddress ||
'0.0.0.0'
}
/**
* 提交反馈
* POST /api/feedback
*/
router.post('/', async (req, res) => {
try {
const {
type,
title,
content,
images,
relatedPage,
deviceInfo,
userContact
} = req.body
// 验证必填字段
if (!type || !title || !content) {
return ApiResponse.error(res, '请填写完整的反馈信息', 400)
}
// 获取用户信息(如果已登录)
let userId = null
let openid = null
let userName = ''
try {
const token = req.headers.authorization?.replace('Bearer ', '')
if (token) {
const jwt = require('jsonwebtoken')
const decoded = jwt.verify(token, process.env.JWT_SECRET)
userId = decoded.userId
openid = decoded.openid
// 获取用户信息
const user = await User.findById(userId)
if (user) {
userName = user.nickName || ''
}
}
} catch (error) {
// Token 验证失败,继续以匿名方式提交
logger.warn('反馈提交时Token验证失败:', error.message)
}
// 创建反馈记录
const feedback = new Feedback({
userId,
openid,
userName,
userContact: userContact || '',
type,
title,
content,
images: images || [],
relatedPage: relatedPage || '',
deviceInfo: deviceInfo || {},
networkInfo: {
ip: getClientIp(req)
},
status: 'pending',
priority: 'normal'
})
await feedback.save()
logger.info(`新反馈提交: ${feedback._id}, 类型: ${type}`)
return ApiResponse.success(res, {
feedbackId: feedback._id
}, '反馈提交成功,我们会尽快处理')
} catch (error) {
logger.error('提交反馈失败:', error)
return ApiResponse.serverError(res, error.message)
}
})
/**
* 获取我的反馈列表
* GET /api/feedback/my
*/
router.get('/my', userAuthMiddleware, async (req, res) => {
try {
const user = req.user
const { page = 1, limit = 20 } = req.query
const query = { userId: user._id }
const total = await Feedback.countDocuments(query)
const feedbacks = await Feedback.find(query)
.sort({ createdAt: -1 })
.skip((parseInt(page) - 1) * parseInt(limit))
.limit(parseInt(limit))
.select('-deviceInfo -networkInfo')
.lean()
// 格式化返回数据
const formattedFeedbacks = feedbacks.map(fb => ({
id: fb._id,
type: fb.type,
title: fb.title,
content: fb.content.substring(0, 100) + (fb.content.length > 100 ? '...' : ''),
images: fb.images,
status: fb.status,
priority: fb.priority,
result: fb.result,
createdAt: fb.createdAt,
updatedAt: fb.updatedAt
}))
return ApiResponse.paginated(res, formattedFeedbacks, {
page: parseInt(page),
limit: parseInt(limit),
total
})
} catch (error) {
logger.error('获取我的反馈失败:', error)
return ApiResponse.serverError(res, error.message)
}
})
/**
* 获取反馈详情
* GET /api/feedback/:id
*/
router.get('/:id', userAuthMiddleware, async (req, res) => {
try {
const user = req.user
const feedback = await Feedback.findById(req.params.id).lean()
if (!feedback) {
return ApiResponse.notFound(res, '反馈不存在')
}
// 检查权限(只能查看自己的反馈)
if (feedback.userId?.toString() !== user._id.toString()) {
return ApiResponse.error(res, '无权查看此反馈', 403)
}
return ApiResponse.success(res, {
id: feedback._id,
type: feedback.type,
title: feedback.title,
content: feedback.content,
images: feedback.images,
relatedPage: feedback.relatedPage,
status: feedback.status,
priority: feedback.priority,
processLog: feedback.processLog,
result: feedback.result,
handler: feedback.handler,
handledAt: feedback.handledAt,
rating: feedback.rating,
createdAt: feedback.createdAt,
updatedAt: feedback.updatedAt
})
} catch (error) {
logger.error('获取反馈详情失败:', error)
return ApiResponse.serverError(res, error.message)
}
})
/**
* 对处理结果评分
* POST /api/feedback/:id/rate
*/
router.post('/:id/rate', userAuthMiddleware, async (req, res) => {
try {
const user = req.user
const { rating, remark } = req.body
if (!rating || rating < 1 || rating > 5) {
return ApiResponse.error(res, '评分必须在1-5之间', 400)
}
const feedback = await Feedback.findById(req.params.id)
if (!feedback) {
return ApiResponse.notFound(res, '反馈不存在')
}
// 检查权限
if (feedback.userId?.toString() !== user._id.toString()) {
return ApiResponse.error(res, '无权操作此反馈', 403)
}
// 只能对已解决的反馈评分
if (feedback.status !== 'resolved' && feedback.status !== 'closed') {
return ApiResponse.error(res, '只能对已处理的反馈进行评分', 400)
}
feedback.rating = rating
if (remark) {
feedback.userRemark = remark
}
await feedback.save()
return ApiResponse.success(res, {}, '评分成功')
} catch (error) {
logger.error('反馈评分失败:', error)
return ApiResponse.serverError(res, error.message)
}
})
module.exports = router
+209
View File
@@ -0,0 +1,209 @@
const express = require('express')
const router = express.Router()
const { Gallery } = require('../models')
const ApiResponse = require('../utils/response')
const { userAuthMiddleware, optionalAuthMiddleware } = require('../middleware/auth')
const logger = require('../utils/logger')
// 应用用户认证中间件到需要登录的路由
// 注意:POST /api/gallery 需要认证
router.post('/', userAuthMiddleware)
router.use('/like', userAuthMiddleware)
// 作品列表接口使用可选认证,这样未登录用户也能访问
router.get('/works', optionalAuthMiddleware)
/**
* 获取作品列表(兼容旧版API
* GET /api/gallery/works
*/
router.get('/works', async (req, res) => {
try {
const { page = 1, limit = 12, filter = 'all' } = req.query
const query = { status: 'approved' }
// 处理筛选条件
if (filter === 'dim2') {
query.dim = 2
} else if (filter === 'dim3') {
query.dim = 3
} else if (filter === 'my') {
// 显示当前用户的作品,需要认证
if (!req.user) {
return ApiResponse.error(res, '需要登录才能查看我的作品', 401)
}
query.openid = req.user.openid
}
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,
fileURL: work.imageUrl || work.imageData,
authorAvatar: work.authorAvatar || '/static/images/default_avatar.png',
authorName: work.authorName || '星辰旅者',
likes: work.likeCount || 0,
views: work.viewCount || 0,
dim: work.dim || 2,
type: work.dim === 2 ? 'dim2' : 'dim3',
description: work.description || '',
createdAt: work.createdAt
}))
return ApiResponse.success(res, {
list: formattedWorks,
total,
page: parseInt(page),
limit: parseInt(limit)
})
} catch (error) {
logger.error('获取作品列表失败:', error)
return ApiResponse.serverError(res, error.message)
}
})
/**
* 获取作品列表
* GET /api/gallery
*/
router.get('/', async (req, res) => {
try {
const { page = 1, limit = 20, status = 'approved', dim } = req.query
const query = { status }
if (dim) query.dim = parseInt(dim)
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()
return ApiResponse.paginated(res, works, {
page: parseInt(page),
limit: parseInt(limit),
total
})
} catch (error) {
logger.error('获取作品列表失败:', error)
return ApiResponse.serverError(res, error.message)
}
})
/**
* 上传作品
* POST /api/gallery
*/
router.post('/', async (req, res) => {
try {
const user = req.user
const { title, description, imageData, imageUrl, tags, dim } = req.body
if (!title || !dim) {
return ApiResponse.error(res, '标题和维度为必填项', 400)
}
const work = new Gallery({
openid: user.openid,
userId: user._id,
authorName: user.nickName,
authorAvatar: user.avatarUrl,
title,
description,
imageData,
imageUrl,
tags: tags || [],
dim,
status: 'approved' // 自动审核通过,用户可以立即看到自己的作品
})
await work.save()
return ApiResponse.success(res, work, '作品上传成功,等待审核')
} catch (error) {
logger.error('上传作品失败:', error)
return ApiResponse.serverError(res, error.message)
}
})
/**
* 获取作品详情
* GET /api/gallery/:id
*/
router.get('/:id', async (req, res) => {
try {
const work = await Gallery.findById(req.params.id)
if (!work) {
return ApiResponse.notFound(res, '作品不存在')
}
// 增加浏览量
work.viewCount += 1
await work.save()
return ApiResponse.success(res, work)
} catch (error) {
logger.error('获取作品详情失败:', error)
return ApiResponse.serverError(res, error.message)
}
})
/**
* 点赞作品(兼容移动端API
* POST /api/gallery/like/:id
*/
router.post('/like/:id', async (req, res) => {
try {
const work = await Gallery.findById(req.params.id)
if (!work) {
return ApiResponse.notFound(res, '作品不存在')
}
work.likeCount += 1
await work.save()
return ApiResponse.success(res, { likeCount: work.likeCount }, '点赞成功')
} catch (error) {
logger.error('点赞失败:', error)
return ApiResponse.serverError(res, error.message)
}
})
/**
* 点赞作品
* POST /api/gallery/:id/like
*/
router.post('/:id/like', async (req, res) => {
try {
const work = await Gallery.findById(req.params.id)
if (!work) {
return ApiResponse.notFound(res, '作品不存在')
}
work.likeCount += 1
await work.save()
return ApiResponse.success(res, { likeCount: work.likeCount }, '点赞成功')
} catch (error) {
logger.error('点赞失败:', error)
return ApiResponse.serverError(res, error.message)
}
})
module.exports = router
+57
View File
@@ -0,0 +1,57 @@
const authRoutes = require('./auth')
const userRoutes = require('./user')
const paymentRoutes = require('./payment')
const galleryRoutes = require('./gallery')
const knowledgeRoutes = require('./knowledge')
const adminRoutes = require('./admin')
const shareRoutes = require('./share')
const bgmRoutes = require('./bgm')
const aiChatRoutes = require('./aiChat')
const aiModelRoutes = require('./aiModel')
const feedbackRoutes = require('./feedback')
const pinyinRoutes = require('./pinyin')
const trendRoutes = require('./trend')
module.exports = (app) => {
// 认证路由
app.use('/api/auth', authRoutes)
// 用户路由
app.use('/api/user', userRoutes)
// 支付路由
app.use('/api/payment', paymentRoutes)
// 画廊路由
app.use('/api/gallery', galleryRoutes)
// 知识库路由
app.use('/api/knowledge', knowledgeRoutes)
// 反馈路由
app.use('/api/feedback', feedbackRoutes)
// 管理后台路由
app.use('/api/admin', adminRoutes)
// 分享记录路由
app.use('/api/share', shareRoutes)
// BGM音效路由(公开接口 + 管理员接口)
app.use('/api/bgm', bgmRoutes)
// AI问答路由
app.use('/api/ai-chat', aiChatRoutes)
// AI模型配置路由
app.use('/api/ai-model', aiModelRoutes)
// 拼音探索路由
app.use('/api/pinyin/contents', pinyinRoutes.contents)
app.use('/api/pinyin/progress', pinyinRoutes.progress)
app.use('/api/pinyin/games', pinyinRoutes.games)
app.use('/api/pinyin/achievements', pinyinRoutes.achievements)
// AI趋势路由
app.use('/api/trend', trendRoutes)
}
+186
View File
@@ -0,0 +1,186 @@
const express = require('express')
const router = express.Router()
const { Knowledge } = require('../models')
const ApiResponse = require('../utils/response')
const { userAuthMiddleware } = require('../middleware/auth')
const logger = require('../utils/logger')
/**
* 获取知识列表
* GET /api/knowledge
*/
router.get('/', async (req, res) => {
try {
const { page = 1, limit = 20, dim, category, status = 'approved' } = req.query
const query = { status }
if (dim) query.dim = parseInt(dim)
if (category) query.category = category
const total = await Knowledge.countDocuments(query)
const knowledge = await Knowledge.find(query)
.sort({ sortOrder: 1, createdAt: -1 })
.skip((parseInt(page) - 1) * parseInt(limit))
.limit(parseInt(limit))
.lean()
return ApiResponse.paginated(res, knowledge, {
page: parseInt(page),
limit: parseInt(limit),
total
})
} catch (error) {
logger.error('获取知识列表失败:', error)
return ApiResponse.serverError(res, error.message)
}
})
/**
* 获取用户收藏的知识列表
* GET /api/knowledge/collected
*/
router.get('/collected', userAuthMiddleware, async (req, res) => {
try {
const user = req.user
// 查找用户收藏的知识
const collectedKnowledge = await Knowledge.find({ _id: { $in: user.collectedKnowledge } })
.lean()
// 格式化返回数据
const formattedKnowledge = collectedKnowledge.map(item => ({
id: item._id,
dimension: item.dim,
title: item.title,
description: item.content || item.description || '',
knowledgeId: item._id
}))
return ApiResponse.success(res, formattedKnowledge)
} catch (error) {
logger.error('获取收藏列表失败:', error)
return ApiResponse.serverError(res, error.message)
}
})
/**
* 删除收藏
* DELETE /api/knowledge/collect/:id
*/
router.delete('/collect/:id', userAuthMiddleware, async (req, res) => {
try {
const knowledgeId = req.params.id
const user = req.user
// 检查是否已经收藏
if (!user.collectedKnowledge.includes(knowledgeId)) {
return ApiResponse.error(res, '未收藏该知识', 400)
}
// 从用户的 collectedKnowledge 数组中移除
user.collectedKnowledge = user.collectedKnowledge.filter(id => id.toString() !== knowledgeId.toString())
await user.save()
// 减少收藏计数
const knowledge = await Knowledge.findById(knowledgeId)
if (knowledge) {
knowledge.collectionCount = Math.max(0, knowledge.collectionCount - 1)
await knowledge.save()
}
return ApiResponse.success(res, {}, '删除收藏成功')
} catch (error) {
logger.error('删除收藏失败:', error)
return ApiResponse.serverError(res, error.message)
}
})
/**
* 获取知识详情
* GET /api/knowledge/:id
*/
router.get('/:id', async (req, res) => {
try {
const knowledge = await Knowledge.findById(req.params.id)
if (!knowledge) {
return ApiResponse.notFound(res, '知识不存在')
}
// 增加浏览量
knowledge.viewCount += 1
await knowledge.save()
return ApiResponse.success(res, knowledge)
} catch (error) {
logger.error('获取知识详情失败:', error)
return ApiResponse.serverError(res, error.message)
}
})
/**
* 收藏知识
* POST /api/knowledge/:id/collect
*/
router.post('/:id/collect', userAuthMiddleware, async (req, res) => {
try {
const knowledge = await Knowledge.findById(req.params.id)
if (!knowledge) {
return ApiResponse.notFound(res, '知识不存在')
}
const user = req.user
// 检查是否已经收藏
if (user.collectedKnowledge.includes(knowledge._id)) {
return ApiResponse.error(res, '已经收藏过了', 400)
}
// 将知识ID加入用户的 collectedKnowledge 数组
user.collectedKnowledge.push(knowledge._id)
await user.save()
// 增加收藏计数
knowledge.collectionCount += 1
await knowledge.save()
return ApiResponse.success(res, { collectionCount: knowledge.collectionCount }, '收藏成功')
} catch (error) {
logger.error('收藏失败:', error)
return ApiResponse.serverError(res, error.message)
}
})
/**
* 获取问答
* GET /api/knowledge/:dim/questions
*/
router.get('/:dim/questions', async (req, res) => {
try {
const { dim } = req.params
const { limit = 10 } = req.query
const questions = await Knowledge.find({
dim: parseInt(dim),
category: 'question',
status: 'approved'
})
.select('question options')
.limit(parseInt(limit))
.lean()
return ApiResponse.success(res, questions)
} catch (error) {
logger.error('获取问答失败:', error)
return ApiResponse.serverError(res, error.message)
}
})
module.exports = router
+266
View File
@@ -0,0 +1,266 @@
const express = require('express')
const router = express.Router()
const { User, Order, ShopItem } = require('../models')
const ApiResponse = require('../utils/response')
const weChatService = require('../utils/weixin')
const { userAuthMiddleware } = require('../middleware/auth')
const logger = require('../utils/logger')
/**
* 获取商品列表(从数据库)- 公开接口
* GET /api/payment/products
*/
router.get('/products', async (req, res) => {
try {
const items = await ShopItem.find({ status: 'active' }).sort({ sort: 1 })
return ApiResponse.success(res, items)
} catch (error) {
logger.error('获取商品列表失败:', error)
return ApiResponse.serverError(res, error.message)
}
})
// 应用用户认证中间件到需要保护的路由
router.use(userAuthMiddleware)
/**
* 创建订单
* POST /api/payment/create-order
*/
router.post('/create-order', async (req, res) => {
try {
const user = req.user
const { productId } = req.body
// 从数据库获取商品信息
const product = await ShopItem.findOne({ itemId: productId, status: 'active' })
if (!product) {
return ApiResponse.error(res, '无效商品 ID 或商品已下架', 400)
}
// 检查是否已购买(针对皮肤类一次性商品)
if (product.type === 'skin' && user.ownedSkins?.includes(productId)) {
return ApiResponse.error(res, '已拥有该商品', 400)
}
// 生成订单号
const orderNo = weChatService.generateOrderNo('WD')
// 创建订单记录
const order = new Order({
orderNo,
openid: user.openid,
userId: user._id,
productId,
productName: product.name,
amount: product.price,
status: 'pending'
})
await order.save()
// 调用微信支付统一下单
const payParams = await weChatService.createPaymentOrder({
openid: user.openid,
orderNo,
body: `宇之然-${product.name}`,
amount: product.price
})
return ApiResponse.success(res, {
orderNo,
productId,
productName: product.name,
amount: product.price,
payParams
}, '订单创建成功')
} catch (error) {
logger.error('创建订单失败:', error)
return ApiResponse.serverError(res, error.message)
}
})
/**
* 支付回调
* POST /api/payment/callback
*/
router.post('/callback', async (req, res) => {
try {
const data = req.body
// 验证签名
if (!weChatService.verifyCallbackSignature(data, data.sign)) {
logger.warn('支付签名验证失败')
return res.status(400).send('FAIL')
}
const orderNo = data.out_trade_no
const transactionId = data.transaction_id
// 查询订单
const order = await Order.findOne({ orderNo })
if (!order) {
logger.error(`订单不存在: ${orderNo}`)
return res.status(400).send('FAIL')
}
// 已处理过
if (order.status === 'paid' || order.status === 'completed') {
return res.status(200).send('SUCCESS')
}
// 更新订单状态
order.status = 'paid'
order.transactionId = transactionId
order.paidAt = new Date()
await order.save()
// 解锁商品
const user = await User.findOne({ openid: order.openid })
if (user) {
const product = await ShopItem.findOne({ itemId: order.productId })
if (product) {
if (product.type === 'skin') {
if (!user.ownedSkins.includes(order.productId)) {
user.ownedSkins.push(order.productId)
}
} else if (product.type === 'noad') {
user.isAdFree = true
} else if (product.type === 'subscription') {
const expiry = new Date()
expiry.setDate(expiry.getDate() + (product.duration || 30))
user.isSubscriber = true
user.subscribeExpiry = expiry
}
await user.save()
}
}
logger.info(`支付成功: ${orderNo}`)
return res.status(200).send('SUCCESS')
} catch (error) {
logger.error('支付回调处理失败:', error)
return res.status(500).send('FAIL')
}
})
/**
* 验证支付结果
* POST /api/payment/verify
*/
router.post('/verify', async (req, res) => {
try {
const user = req.user
const { productId, orderNo } = req.body
// 验证订单是否已处理
const order = await Order.findOne({
orderNo,
openid: user.openid,
status: { $in: ['paid', 'completed'] }
})
if (order) {
return ApiResponse.success(res, {
alreadyProcessed: true,
message: '订单已处理'
})
}
// 获取商品信息
const product = await ShopItem.findOne({ itemId: productId, status: 'active' })
if (!product) {
return ApiResponse.error(res, `未知商品: ${productId}`, 400)
}
// 更新用户商品
let updateData = {}
if (product.type === 'skin') {
if (!user.ownedSkins.includes(productId)) {
user.ownedSkins.push(productId)
}
} else if (product.type === 'noad') {
user.isAdFree = true
} else if (product.type === 'subscription') {
const expiry = new Date()
expiry.setDate(expiry.getDate() + (product.duration || 30))
user.isSubscriber = true
user.subscribeExpiry = expiry
}
await user.save()
// 记录订单
const newOrder = new Order({
orderNo,
openid: user.openid,
userId: user._id,
productId,
productName: product.name,
amount: product.price,
status: 'completed',
completedAt: new Date()
})
await newOrder.save()
return ApiResponse.success(res, {
product,
message: `${product.name} 已解锁!`
})
} catch (error) {
logger.error('验证支付失败:', error)
return ApiResponse.serverError(res, error.message)
}
})
/**
* 申请退款
* POST /api/payment/refund
*/
router.post('/refund', async (req, res) => {
try {
const { orderNo, reason } = req.body
// 查询订单
const order = await Order.findOne({ orderNo })
if (!order) {
return ApiResponse.error(res, '订单不存在', 404)
}
if (order.status !== 'paid' && order.status !== 'completed') {
return ApiResponse.error(res, '订单状态不支持退款', 400)
}
// 调用微信退款接口
const result = await weChatService.refundOrder(
order.transactionId,
`REF_${order.orderNo}`,
order.amount / 100,
order.amount / 100,
reason
)
if (result.success) {
// 更新订单状态
order.status = 'refunded'
order.refundReason = reason
order.refundedAt = new Date()
await order.save()
return ApiResponse.success(res, result, '退款申请已提交')
} else {
return ApiResponse.error(res, '退款申请失败', 500)
}
} catch (error) {
logger.error('申请退款失败:', error)
return ApiResponse.serverError(res, error.message)
}
})
module.exports = router
+15
View File
@@ -0,0 +1,15 @@
const express = require('express');
const router = express.Router();
const { authMiddleware } = require('../../middleware/auth');
const achievementController = require('../../controllers/pinyin/achievementController');
// 获取成就列表(公开)
router.get('/', achievementController.getAchievements);
// 获取我的成就(需要登录)
router.get('/my', authMiddleware, achievementController.getMyAchievements);
// 检查成就达成(需要登录)
router.post('/check', authMiddleware, achievementController.checkAchievements);
module.exports = router;
+16
View File
@@ -0,0 +1,16 @@
const express = require('express');
const router = express.Router();
const { authMiddleware } = require('../../middleware/auth');
const contentController = require('../../controllers/pinyin/contentController');
// 公开接口
router.get('/', contentController.getContents);
router.get('/:symbol', contentController.getContentBySymbol);
router.get('/:symbol/audio', contentController.getContentAudio);
// 管理员接口
router.post('/', authMiddleware, contentController.createContent);
router.put('/:id', authMiddleware, contentController.updateContent);
router.delete('/:id', authMiddleware, contentController.deleteContent);
module.exports = router;
+24
View File
@@ -0,0 +1,24 @@
const express = require('express');
const router = express.Router();
const { userAuthMiddleware } = require('../../middleware/auth');
const gameController = require('../../controllers/pinyin/gameController');
// 获取游戏配置
router.get('/config', gameController.getGameConfig);
// 获取游戏配置(按类型)
router.get('/config/:type', gameController.getGameConfigByType);
// 获取用户游戏统计(需要登录)
router.get('/stats', userAuthMiddleware, gameController.getGameStats);
// 获取用户游戏记录(需要登录)
router.get('/records', userAuthMiddleware, gameController.getGameRecords);
// 记录游戏结果(需要登录)
router.post('/record', userAuthMiddleware, gameController.recordGame);
// 获取游戏排行榜
router.get('/leaderboard/:type', gameController.getLeaderboard);
module.exports = router;
+14
View File
@@ -0,0 +1,14 @@
/**
* 拼音探索模块路由导出
*/
const contentsRouter = require('./contents');
const progressRouter = require('./progress');
const gamesRouter = require('./games');
const achievementsRouter = require('./achievements');
module.exports = {
contents: contentsRouter,
progress: progressRouter,
games: gamesRouter,
achievements: achievementsRouter
};
+9
View File
@@ -0,0 +1,9 @@
const express = require('express');
const router = express.Router();
const { userAuthMiddleware } = require('../../middleware/auth');
const progressController = require('../../controllers/pinyin/progressController');
// 获取用户探索进度
router.get('/', userAuthMiddleware, progressController.getProgress);
module.exports = router;
+67
View File
@@ -0,0 +1,67 @@
const express = require('express')
const router = express.Router()
const { ShareRecord, User } = require('../models')
const ApiResponse = require('../utils/response')
const { userAuthMiddleware, optionalAuthMiddleware, authMiddleware } = require('../middleware/auth')
const logger = require('../utils/logger')
/**
* 记录分享行为(支持匿名)
* POST /api/share/record
*/
router.post('/record', optionalAuthMiddleware, async (req, res) => {
try {
const { shareType, score } = req.body
const openid = req.user ? req.user.openid : 'anonymous'
const userId = req.user ? req.user._id : null
const record = new ShareRecord({
openid,
userId,
shareType: shareType || 'app',
score: score || 0
})
await record.save()
return ApiResponse.success(res, { message: '分享已记录' })
} catch (error) {
logger.error('记录分享失败:', error)
return ApiResponse.serverError(res, error.message)
}
})
/**
* 获取分享记录列表(管理员)
* GET /api/share/list
*/
router.get('/list', authMiddleware, async (req, res) => {
try {
const { page = 1, limit = 20, openid } = req.query
const skip = (page - 1) * limit
const query = {}
if (openid && openid !== 'all') {
query.openid = openid
}
const records = await ShareRecord.find(query)
.sort({ sharedAt: -1 })
.skip(skip)
.limit(parseInt(limit))
.populate('userId', 'nickname avatar')
const total = await ShareRecord.countDocuments(query)
return ApiResponse.success(res, {
list: records,
total,
page: parseInt(page),
limit: parseInt(limit)
})
} catch (error) {
logger.error('获取分享记录失败:', error)
return ApiResponse.serverError(res, error.message)
}
})
module.exports = router
+83
View File
@@ -0,0 +1,83 @@
const express = require('express');
const router = express.Router();
const Trend = require('../models/Trend');
/**
* 获取趋势列表
* GET /api/trend?category=产品&page=1&limit=20
*/
router.get('/', async (req, res) => {
try {
const { category, page = 1, limit = 20 } = req.query;
const query = { status: 'published' };
if (category) query.category = category;
const skip = (parseInt(page) - 1) * parseInt(limit);
const [list, total] = await Promise.all([
Trend.find(query)
.sort({ newsDate: -1 })
.skip(skip)
.limit(parseInt(limit))
.select('-__v'),
Trend.countDocuments(query)
]);
res.json({
success: true,
data: { list, total, page: parseInt(page), totalPages: Math.ceil(total / parseInt(limit)) }
});
} catch (err) {
res.status(500).json({ success: false, error: err.message });
}
});
/**
* 获取趋势详情
* GET /api/trend/:id
*/
router.get('/:id', async (req, res) => {
try {
const trend = await Trend.findByIdAndUpdate(
req.params.id,
{ $inc: { viewCount: 1 } },
{ new: true }
);
if (!trend) return res.status(404).json({ success: false, error: '趋势不存在' });
res.json({ success: true, data: trend });
} catch (err) {
res.status(500).json({ success: false, error: err.message });
}
});
/**
* 管理员:创建趋势
* POST /api/trend/admin
*/
router.post('/admin', async (req, res) => {
try {
const trend = new Trend(req.body);
await trend.save();
res.json({ success: true, data: trend });
} catch (err) {
res.status(500).json({ success: false, error: err.message });
}
});
/**
* 管理员:批量创建趋势(给 cron 用)
* POST /api/trend/admin/batch
*/
router.post('/admin/batch', async (req, res) => {
try {
const { items } = req.body;
if (!Array.isArray(items) || items.length === 0) {
return res.status(400).json({ success: false, error: 'items 不能为空' });
}
const result = await Trend.insertMany(items);
res.json({ success: true, data: { inserted: result.length } });
} catch (err) {
res.status(500).json({ success: false, error: err.message });
}
});
module.exports = router;
+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