feat: 管理后台全面升级——修复菜单无数据根因(items字段不匹配+trends接口缺失),新增知识管理(审核/检测题编辑)/访问日志/看板v2(最近访问+点亮分布+全量指标),trends CRUD,用户/反馈列表修正

This commit is contained in:
2026-09-13 17:45:29 +08:00
parent a4da3722ef
commit c5ade459a5
8 changed files with 555 additions and 84 deletions
+122 -1
View File
@@ -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