feat: 管理后台全面升级——修复菜单无数据根因(items字段不匹配+trends接口缺失),新增知识管理(审核/检测题编辑)/访问日志/看板v2(最近访问+点亮分布+全量指标),trends CRUD,用户/反馈列表修正
This commit is contained in:
+122
-1
@@ -1,7 +1,7 @@
|
||||
const express = require('express')
|
||||
const router = express.Router()
|
||||
const mongoose = require('mongoose')
|
||||
const { User, Order, Knowledge, ShopItem, AdminLog, AIChatQuota, Feedback, LoginLog } = require('../models')
|
||||
const { User, Order, Knowledge, ShopItem, AdminLog, AIChatQuota, Feedback, LoginLog, Trend, KnowledgeLit } = require('../models')
|
||||
const { authMiddleware, requirePermission } = require('../middleware/auth')
|
||||
const ApiResponse = require('../utils/response')
|
||||
const logger = require('../utils/logger')
|
||||
@@ -1077,4 +1077,125 @@ router.get('/login-logs', requirePermission('user:read'), async (req, res) => {
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
/**
|
||||
* ===== 趋势资讯管理 CRUD =====
|
||||
*/
|
||||
router.get('/trends', requirePermission('trend:read'), async (req, res) => {
|
||||
try {
|
||||
const { page = 1, limit = 20, keyword = '' } = req.query
|
||||
const query = keyword ? { title: { $regex: keyword, $options: 'i' } } : {}
|
||||
const total = await Trend.countDocuments(query)
|
||||
const items = await Trend.find(query).sort({ newsDate: -1, createdAt: -1 })
|
||||
.skip((parseInt(page) - 1) * parseInt(limit)).limit(parseInt(limit)).lean()
|
||||
return ApiResponse.paginated(res, items, { page: parseInt(page), limit: parseInt(limit), total })
|
||||
} catch (error) {
|
||||
logger.error('获取趋势列表失败:', error)
|
||||
return ApiResponse.serverError(res, '获取趋势列表失败')
|
||||
}
|
||||
})
|
||||
|
||||
router.post('/trends', requirePermission('trend:write'), async (req, res) => {
|
||||
try {
|
||||
const { title, summary, source, sourceUrl, category, tags, hot, picked, newsDate } = req.body
|
||||
if (!title) return ApiResponse.error(res, '标题不能为空', 400)
|
||||
const item = new Trend({ title, summary, source, sourceUrl, category, tags, hot: !!hot, picked: !!picked, newsDate: newsDate || new Date() })
|
||||
await item.save()
|
||||
await logAction(req, 'create', 'trend', item._id, { title })
|
||||
return ApiResponse.success(res, item, '创建成功')
|
||||
} catch (error) {
|
||||
logger.error('创建趋势失败:', error)
|
||||
return ApiResponse.serverError(res, '创建失败')
|
||||
}
|
||||
})
|
||||
|
||||
router.put('/trends/:id', requirePermission('trend:write'), async (req, res) => {
|
||||
try {
|
||||
const { title, summary, source, sourceUrl, category, tags, hot, picked, newsDate } = req.body
|
||||
const item = await Trend.findByIdAndUpdate(req.params.id,
|
||||
{ title, summary, source, sourceUrl, category, tags, hot: !!hot, picked: !!picked, ...(newsDate ? { newsDate } : {}) },
|
||||
{ new: true })
|
||||
if (!item) return ApiResponse.notFound(res, '趋势不存在')
|
||||
await logAction(req, 'update', 'trend', item._id, { title })
|
||||
return ApiResponse.success(res, item, '更新成功')
|
||||
} catch (error) {
|
||||
logger.error('更新趋势失败:', error)
|
||||
return ApiResponse.serverError(res, '更新失败')
|
||||
}
|
||||
})
|
||||
|
||||
router.delete('/trends/:id', requirePermission('trend:write'), async (req, res) => {
|
||||
try {
|
||||
const item = await Trend.findByIdAndDelete(req.params.id)
|
||||
if (!item) return ApiResponse.notFound(res, '趋势不存在')
|
||||
await logAction(req, 'delete', 'trend', item._id, { title: item.title })
|
||||
return ApiResponse.success(res, {}, '删除成功')
|
||||
} catch (error) {
|
||||
logger.error('删除趋势失败:', error)
|
||||
return ApiResponse.serverError(res, '删除失败')
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* ===== 增强看板:产品全量指标 + 最近访问记录 =====
|
||||
* GET /api/admin/dashboard-v2
|
||||
*/
|
||||
router.get('/dashboard-v2', requirePermission('dashboard:read'), async (req, res) => {
|
||||
try {
|
||||
const [
|
||||
totalUsers, newUsers7d, totalKnowledge, approvedKnowledge, withQuiz,
|
||||
totalTrends, totalFeedbacks, pendingFeedbacks, totalOrders, paidOrders,
|
||||
totalLits, litUsers, recentLogins, litByDimAgg, aiStatsRaw
|
||||
] = await Promise.all([
|
||||
User.countDocuments({}),
|
||||
User.countDocuments({ createdAt: { $gte: new Date(Date.now() - 7 * 86400000) } }),
|
||||
Knowledge.countDocuments({}),
|
||||
Knowledge.countDocuments({ status: 'approved' }),
|
||||
Knowledge.countDocuments({ 'options.0': { $exists: true } }),
|
||||
Trend.countDocuments({}),
|
||||
Feedback.countDocuments({}),
|
||||
Feedback.countDocuments({ status: 'pending' }),
|
||||
Order.countDocuments({}),
|
||||
Order.countDocuments({ status: 'paid' }),
|
||||
KnowledgeLit.countDocuments({}),
|
||||
KnowledgeLit.distinct('openid'),
|
||||
LoginLog.find({}).sort({ createdAt: -1 }).limit(10).lean(),
|
||||
KnowledgeLit.aggregate([{ $group: { _id: '$dim', count: { $sum: 1 } } }]),
|
||||
AIChatQuota.aggregate([{ $group: { _id: null, used: { $sum: '$usedQuota' }, users: { $sum: 1 } } }]),
|
||||
])
|
||||
|
||||
const litByDim = {}
|
||||
for (let i = 1; i <= 5; i++) litByDim[i] = 0
|
||||
litByDimAgg.forEach(r => { if (r._id >= 1 && r._id <= 5) litByDim[r._id] = r.count })
|
||||
|
||||
const DIM_NAMES = { 1: 'AI 起源', 2: 'AI 发展', 3: 'AI 当前', 4: 'AI 学习', 5: 'AI 趋势' }
|
||||
const ai = aiStatsRaw[0] || { used: 0, users: 0 }
|
||||
|
||||
return ApiResponse.success(res, {
|
||||
stats: {
|
||||
totalUsers, newUsers7d,
|
||||
totalKnowledge, approvedKnowledge, withQuiz,
|
||||
totalTrends, totalFeedbacks, pendingFeedbacks,
|
||||
totalOrders, paidOrders,
|
||||
totalLits, litUsers: litUsers.length,
|
||||
aiChatUsed: ai.used, aiChatUsers: ai.users,
|
||||
},
|
||||
litByDim: Object.entries(litByDim).map(([dim, count]) => ({ dim: Number(dim), name: DIM_NAMES[dim], count })),
|
||||
recentLogins: recentLogins.map(l => ({
|
||||
openid: l.openid,
|
||||
type: l.type,
|
||||
method: l.method,
|
||||
ip: l.ip,
|
||||
success: l.success,
|
||||
failReason: l.failReason,
|
||||
userAgent: (l.userAgent || '').slice(0, 80),
|
||||
createdAt: l.createdAt,
|
||||
})),
|
||||
})
|
||||
} catch (error) {
|
||||
logger.error('获取增强看板失败:', error)
|
||||
return ApiResponse.serverError(res, '获取看板失败')
|
||||
}
|
||||
})
|
||||
|
||||
module.exports = router
|
||||
|
||||
Reference in New Issue
Block a user