security+fix: 评审修复——下线webview-token任意账号接管;progress接口付费字段只读;检测题答案脱敏;读接口不记日志;share-gain每日限3次防刷;页面标题/分享标题搜索优化

This commit is contained in:
Hermes Agent
2026-09-19 09:25:29 +08:00
parent bcb239c51b
commit 15eb4727bb
15 changed files with 486 additions and 480 deletions
+2
View File
@@ -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" />
+9 -9
View File
@@ -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"
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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
-11
View File
@@ -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),
+11
View File
@@ -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
View File
@@ -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
+6 -1
View File
@@ -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, '获取问答失败')
+1 -5
View File
@@ -171,11 +171,7 @@ router.post('/progress', async (req, res) => {
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
if (isSubscriber !== null && isSubscriber !== undefined) dbUser.isSubscriber = isSubscriber
if (subscribeExpiry !== null && subscribeExpiry !== undefined) dbUser.subscribeExpiry = subscribeExpiry
await dbUser.save() await dbUser.save()