const db = uniCloud.database(); const _ = db.command; const $ = db.command.aggregate; const uniID = require('uni-id-common'); let obj = { getDateTime() { return new Date(); }, async getDeletedCount(commentId) { // count() 返回的是 total,不是 deleted const { total } = await db.collection('comment_like') .where({ comment_id: commentId }) .count(); return total; }, // 高并发安全的计数方法 async incrementReadCount(query) { try { if (!query || !query.article_id) { return { code: 400, message: '缺少 article_id' }; } // context 在原实现里未定义,必然抛 ReferenceError 并被吞掉,阅读量永远不增长 const clientInfo = this.getClientInfo(); await this.checkRequestFrequency(query.article_id, clientInfo.clientIP); const res = await db.collection('uni-cms-articles') .doc(query.article_id.toString()) .update({ view_count: db.command.inc(1), last_view_time: Date.now() }); return { code: 200, data: { view_count: res.updated ? 1 : 0 } }; } catch (e) { return this.handleCountError(e); } }, // 频率控制核心方法 async checkRequestFrequency(articleId, ip) { const key = `req:${articleId}:${ip}`; const now = Date.now(); const record = await db.collection('request_logs') .where({ key }) .orderBy('time', 'desc') .limit(1) .get(); if (record.data.length > 0 && (now - record.data[0].time) < 1000) { throw new Error('REQUEST_LIMIT'); } await db.collection('request_logs').add({ key, time: now, articleId, ip }); }, // 统一错误处理器 handleCountError(e) { const errorConfig = { 'REQUEST_LIMIT': { code: 429, message: '访问过于频繁' }, 'DATABASE_BUSY': { code: 503, message: '系统繁忙请重试' } }; return errorConfig[e.message] || { code: 500, message: '服务内部错误' }; }, async getUserInfoMap(userIds) { const res = await db.collection('uni-id-users') .where({ _id: db.command.in(userIds) }) .field({ _id: true, nickname: true, avatar_file: true }) .get(); return new Map(res.data.map(u => [u._id, u])); }, buildCommentTree(comments) { if (!Array.isArray(comments) || comments.length === 0) { console.error('无效的评论数据'); return []; } const map = new Map(); const roots = []; // 1. 数据预处理 comments.forEach(comment => { // 强制校验 _id 字段 if (!comment._id) { console.error('评论缺少 _id 字段:', comment); return; } const safeId = comment._id.toString(); const parentId = comment.parent_id; const rootId = comment.root_id; const node = { ...comment, id: safeId, replies: [], reply_count: Number(comment.reply_count) || 0, update_time: Number(comment.update_time) || Date.now(), create_time: new Date(comment.create_time).getTime() || Date.now() }; map.set(node.id, node); // 根节点判断(仅依赖 parent_id)。 // 哨兵值可能是数字 0 或字符串 "0",字符串 "0" 是 truthy,必须显式比较。 const isRootNode = !parentId || parentId === 0 || parentId === '0'; if (isRootNode) { roots.push(node); } }); // 2. 构建树结构 comments.forEach(comment => { const currentId = comment._id.toString(); const current = map.get(currentId); const parentId = current.parent_id; if (parentId && parentId !== 0 && parentId !== '0') { const parent = map.get(parentId); if (parent) { parent.replies.push(current); parent.reply_count = parent.replies.length; } else { console.warn(`父节点 ${parentId} 不存在,将当前节点 ${currentId} 升级为根节点`); roots.push(current); } } }); // 3. 排序 return roots.sort((a, b) => { return b.update_time - a.update_time; }); }, } module.exports = { _before() { this.uniID = uniID.createInstance({ context: this.getClientInfo() }); if (!this.uniID) throw new Error('uniID 初始化失败'); }, async getReplyComment(query) { try { const clientInfo = this.getClientInfo(); const { uid } = await this.uniID.checkToken(clientInfo.uniIdToken) let user_id = uid; // 1. 优化数据库查询(仅获取根评论) const commentsRes = await db.collection('comment') .where({ root_id: query.root_id }) .field({ _id: true, content: true, author_id: true, parent_id: true, root_id: true, create_time: true, update_time: true, ip_location: true, like_count: true, reply_count: true }) .orderBy('reply_count', 'desc') // 新增按时间倒序[9](@ref) .get(); // 2. 数据预处理 const rawComments = Object.freeze(commentsRes.data); // 3. 批量获取用户信息 const userIds = [...new Set(rawComments.map(c => c.author_id))]; const userMap = await obj.getUserInfoMap(userIds); // 4. 批量查询点赞状态 const commentIds = rawComments.map(c => c._id); const likeRes = await db.collection('comment_like') .where({ comment_id: db.command.in(commentIds), user_id: user_id }) .get(); const likeSet = new Set(likeRes.data.map(l => l.comment_id)); // 5. 构建扁平化数据结构 const commentList = rawComments.map(comment => ({ id: comment._id, _id: comment._id, pl_user_id: comment.author_id, owner: comment.author_id === user_id, hasLike: likeSet.has(comment._id), likeNum: comment.like_count || 0, root_id: comment.root_id, parent_id: comment.parent_id, avatarUrl: userMap.get(comment.author_id)?.avatar_file || {}, nickName: userMap.get(comment.author_id)?.nickname || '已注销用户', content: comment.content, createTime: comment.create_time, ip_location: comment.ip_location, hasShowMore: false })); return { code: 200, data: { commentList: commentList } }; } catch (e) { console.error('获取评论失败:', e); return { code: 500, message: '查询服务异常' }; } }, async replyComment(query) { try { const clientInfo = this.getClientInfo(); const { uid } = await this.uniID.checkToken(clientInfo.uniIdToken); // 参数校验 if (!query.content || !query.content.trim()) { return { code: 400, message: '评论内容不能为空' }; } if (query.content.trim().length > 500) { return { code: 400, message: '评论内容不能超过500字符' }; } let { article_id, content, parent_id, root_id } = query; // 取父评论信息。原实现无条件对 parent_id 调 doc(), // 顶层评论未传 parent_id 时会以 doc(undefined) 报错。 let parentComment = null; if (parent_id && parent_id !== 0 && parent_id !== '0') { const parentRecord = await db.collection('comment') .doc(parent_id) .get(); parentComment = parentRecord.data && parentRecord.data[0]; if (!parentComment) { return { code: 404, message: '父评论不存在' }; } // 回复应挂到父评论所属的根评论下,不接受客户端随意指定 root_id = (!parentComment.root_id || parentComment.root_id === 0 || parentComment.root_id === '0') ? parent_id : parentComment.root_id; } // 构建评论数据。schema 把 parent_id / root_id 定义为 string, // 真实 id 也是字符串,因此哨兵统一写成 "0"。 const commentData = { article_id, author_id: uid, content: content.trim(), parent_id: parent_id ? String(parent_id) : '0', root_id: root_id ? String(root_id) : '0', parent_author_id: parentComment ? parentComment.author_id : '', parent_author_content: parentComment ? parentComment.content : '', create_time: obj.getDateTime(), update_time: obj.getDateTime(), ip_location: { clientIP: clientInfo.clientIP || '' }, children: [], like_count: 0, reply_count: 0 }; // 插入数据库 const { id } = await db.collection('comment').add(commentData); // 只在确实存在根评论时累加回复数,避免对客户端传入的任意 id 刷计数 if (commentData.root_id && commentData.root_id !== 0 && commentData.root_id !== '0') { await db.collection('comment').doc(commentData.root_id) .update({ reply_count: _.inc(1) }); } // 获取用户信息 const userMap = await obj.getUserInfoMap([uid]); const userInfo = userMap.get(uid) || {}; // 构建返回数据 return { code: 200, data: { id, _id: id, pl_user_id: uid, owner: true, hasLike: false, likeNum: 0, root_id: commentData.root_id, parent_id: commentData.parent_id, avatarUrl: userInfo.avatar_file || { url: '', name: '', extname: '' }, nickName: userInfo.nickname || '注销用户', content: commentData.content, createTime: commentData.create_time, ip_location: commentData.ip_location, replyCount: 0 } }; } catch (e) { console.error('添加评论失败:', e); return { code: 500, message: '添加评论失败' }; } }, async addComment(query) { try { const clientInfo = this.getClientInfo(); const { uid } = await this.uniID.checkToken(clientInfo.uniIdToken); // 参数校验 if (!query.content || !query.content.trim()) { return { code: 400, message: '评论内容不能为空' }; } if (query.content.trim().length > 500) { return { code: 400, message: '评论内容不能超过500字符' }; } let { content, parent_id = 0, root_id = 0 } = query; // 处理父评论逻辑 if (parent_id && parent_id !== 0 && parent_id !== '0') { const parentRecord = await db.collection('comment') .doc(parent_id) .get(); const parent = parentRecord.data && parentRecord.data[0]; if (!parent) { return { code: 404, message: '父评论不存在' }; } // 继承父评论的root_id(如果父评论是根评论则使用父评论ID) root_id = (!parent.root_id || parent.root_id === 0 || parent.root_id === '0') ? parent_id : parent.root_id; } // 构建评论数据。schema 把 parent_id / root_id 定义为 string, // 真实 id 也是字符串,因此哨兵统一写成 "0"。 const commentData = { article_id: query.article_id, author_id: uid, content: content.trim(), parent_id: parent_id ? String(parent_id) : '0', root_id: root_id ? String(root_id) : '0', create_time: obj.getDateTime(), update_time: obj.getDateTime(), ip_location: { clientIP: clientInfo.clientIP || '' }, like_count: 0, reply_count: 0 }; // 插入数据库 const { id } = await db.collection('comment').add(commentData); // 更新父评论的回复计数(原判断 parent_id !== 0 对字符串 "0" 不成立,会误加) if (commentData.parent_id && commentData.parent_id !== 0 && commentData.parent_id !== '0') { await db.collection('comment') .doc(commentData.parent_id) .update({ reply_count: db.command.inc(1) }); } // 获取用户信息 const userMap = await obj.getUserInfoMap([uid]); const userInfo = userMap.get(uid) || {}; // 构建返回数据 return { code: 200, data: { id, _id: id, pl_user_id: uid, owner: true, hasLike: false, likeNum: 0, root_id: commentData.root_id, parent_id: commentData.parent_id, avatarUrl: userInfo.avatar_file || { url: '', name: '', extname: '' }, children: [], nickName: userInfo.nickname || '注销用户', content: commentData.content, createTime: commentData.create_time, ip_location: commentData.ip_location, replyCount: 0 } }; } catch (e) { console.error('添加评论失败:', e); return { code: 500, message: '添加评论失败' }; } }, async deleteComment(id, root_id) { let transaction = null; try { if (!id || typeof id !== 'string' || id.length !== 24) { throw new Error('INVALID_ID: 无效的评论ID'); } const clientInfo = this.getClientInfo(); const { uid } = await this.uniID.checkToken(clientInfo.uniIdToken); if (!uid) throw new Error('PERMISSION_DENIED: 用户未登录'); const commentDoc = await db.collection('comment') .doc(id) .field({ author_id: 1, article_id: 1 }) .get(); // data 为空时是空数组,必须取下标判断,否则后面的 .author_id 会抛 TypeError const target = commentDoc.data && commentDoc.data[0]; if (!target) { throw new Error('NOT_FOUND: 评论不存在'); } if (target.author_id !== uid) { throw new Error('PERMISSION_DENIED: 无权限删除该评论'); } transaction = await db.startTransaction(); await transaction.collection('comment').doc(id).remove(); // comment_like 没有 type 字段,带上该条件会一条都匹配不到,点赞明细变成孤儿 await transaction.collection('comment_like') .where({ comment_id: id }) .remove(); // 父评论的回复计数要减在 comment 表的父评论上。 // 原写法 `!root_id === 0` 恒为 false,且误更新了 uni-cms-articles 表,导致计数只增不减。 if (root_id && root_id !== 0 && root_id !== '0' && root_id !== id) { await transaction.collection('comment') .doc(root_id) .update({ reply_count: _.inc(-1) }); } await transaction.commit(); await db.collection('comment_operation_logs').add({ type: 'comment_delete', uid, comment_id: id, timestamp: obj.getDateTime() }); console.log(1); return { code: 200, message: '评论及关联数据删除成功', data: { deletedComment: 1, del_id: id } }; } catch (e) { // ================= 11. 事务回滚 ================= if (transaction) await transaction.rollback(); // ================= 12. 错误分类处理 ================= console.error(`[DELETE_COMMENT_ERROR] ${e.message}`, { comment_id: id, error: e.stack }); const errorMap = { 'INVALID_ID': 400, 'PERMISSION_DENIED': 403, 'NOT_FOUND': 404 }; return { code: errorMap[e.message.split(':')[0]] || 500, message: e.message.split(':')[1]?.trim() || '系统错误', errorType: e.message.split(':')[0] || 'UNKNOWN_ERROR' }; } }, // ================= 辅助方法 ================= async updateComment(id, comment) { try { // ==== 参数校验 ==== if (!id || typeof id !== 'string' || id.length !== 24) { throw new Error('INVALID_ID: 评论ID格式错误'); } if (!comment?.content?.trim() || comment.content.trim().length > 500) { throw new Error('INVALID_CONTENT: 评论内容不能为空且不超过500字符'); } // ==== 身份验证 ==== const clientInfo = this.getClientInfo(); const { uid } = await this.uniID.checkToken(clientInfo.uniIdToken); // ==== 权限验证 ==== const commentDoc = await db.collection('comment') .doc(id) .field({ author_id: 1, content: 1 }) .get(); // get() 返回的 data 是数组,取值必须带下标 const target = commentDoc.data && commentDoc.data[0]; if (!target) { throw new Error('NOT_FOUND: 评论不存在'); } if (target.author_id !== uid) { throw new Error('PERMISSION_DENIED: 无权修改该评论'); } // ==== 数据处理 ==== const sanitizedContent = comment.content.trim(); const maskedIP = (clientInfo.clientIP || '').replace(/\.\d+$/, '.*'); // ==== 构建更新数据 ==== // 字段名与类型要与 comment.schema.json 一致:update_time 为时间戳,ip_location 为对象 const updateData = { update_time: obj.getDateTime(), content: sanitizedContent, ip_location: { clientIP: maskedIP } }; if (comment.title) { updateData.title = comment.title.substring(0, 50); } // ==== 执行更新 ==== const res = await db.collection('comment') .doc(id) .update(updateData); // ==== 记录操作日志 ==== await db.collection('comment_operation_logs').add({ type: 'comment_update', operator: uid, target_id: id, before_content: target.content, after_content: sanitizedContent, timestamp: obj.getDateTime() }); return { code: 200, message: '评论更新成功', data: { updatedId: id, newContent: sanitizedContent, timestamp: Date.now() } }; } catch (error) { console.error(`[UPDATE_COMMENT_ERROR] ${error.message}`, { id }); return { code: error.code || 500, message: error.message.split(':')[1]?.trim() || '系统错误', errorType: error.message.split(':')[0] }; } }, async getComments(query) { try { const clientInfo = this.getClientInfo(); const { uid } = await this.uniID.checkToken(clientInfo.uniIdToken) let user_id = uid; // 1. 优化数据库查询(仅获取根评论) console.log(query); const commentsRes = await db.collection('comment') .where({ // 历史数据的哨兵值有数字 0 与字符串 "0" 两种,都要能命中 root_id: _.in([0, '0']), article_id: query.article_id }) .field({ _id: true, content: true, author_id: true, parent_id: true, root_id: true, create_time: true, update_time: true, ip_location: true, like_count: true, reply_count: true }) .orderBy('reply_count', 'desc') .get(); // 2. 数据预处理 const rawComments = Object.freeze(commentsRes.data); // 3. 批量获取用户信息 const userIds = [...new Set(rawComments.map(c => c.author_id))]; const userMap = await obj.getUserInfoMap(userIds); // 4. 批量查询点赞状态 const commentIds = rawComments.map(c => c._id); const likeRes = await db.collection('comment_like') .where({ comment_id: db.command.in(commentIds), user_id: user_id }) .get(); const likeSet = new Set(likeRes.data.map(l => l.comment_id)); // 5. 构建扁平化数据结构 const commentList = rawComments.map(comment => ({ id: comment._id, _id: comment._id, pl_user_id: comment.author_id, owner: comment.author_id === user_id, hasLike: likeSet.has(comment._id), likeNum: comment.like_count || 0, root_id: comment.root_id, parent_id: comment.parent_id, avatarUrl: userMap.get(comment.author_id)?.avatar_file || {}, nickName: userMap.get(comment.author_id)?.nickname || '已注销用户', content: comment.content, createTime: comment.create_time, ip_location: comment.ip_location, replyCount: comment.reply_count || 0, children: [], hasShowMore: false })); return { code: 200, data: { commentList: commentList // 直接返回无需构建树形结构[8](@ref) } }; } catch (e) { console.error('获取评论失败:', e); return { code: 500, message: '查询服务异常' }; } }, async getReadCount(query) { try { if (!query || !query.article_id) { throw new Error('INVALID_PARAM'); } const res = await db.collection('uni-cms-articles') .doc(query.article_id.toString()) .field({ view_count: 1, _id: 0 }) .get(); return { code: 200, data: { view_count: res.data[0]?.view_count || 0 } }; } catch (e) { const errorMap = { 'INVALID_PARAM': { code: 400, message: '参数异常' }, 'DATABASE_ERROR': { code: 503, message: '服务暂不可用' } }; return errorMap[e.message] || { code: 500, data: { view_count: 0 }, message: '统计服务异常' }; } }, async likeComment(user_id, comment_id) { // 以服务端令牌为准。原实现直接采信客户端传入的 user_id, // 可以冒用他人身份点赞并刷高 like_count。 const { uid } = await this.uniID.checkToken(this.getUniIdToken()); if (!uid) return { code: 401, msg: '请先登录' }; if (!comment_id) return { code: 400, msg: '缺少 comment_id' }; const transaction = await db.startTransaction(); try { const exist = await transaction.collection('comment_like') .where({ user_id: uid, comment_id: comment_id }).count(); if (exist.total > 0) { await transaction.rollback(); return { code: 201, msg: '已点赞' }; } await transaction.collection('comment_like').add({ user_id: uid, comment_id: comment_id, create_time: Date.now() }); await transaction.collection('comment').doc(comment_id) .update({ like_count: _.inc(1) }); await transaction.commit(); return { code: 200 }; } catch (e) { await transaction.rollback(); return { code: 500, msg: e.message }; } }, async relikeComment(user_id, comment_id) { const { uid } = await this.uniID.checkToken(this.getUniIdToken()); if (!uid) return { code: 401, msg: '请先登录' }; if (!comment_id) return { code: 400, msg: '缺少 comment_id' }; // 删除明细与减计数必须同事务,否则删成功而减失败会让计数永久偏高 const transaction = await db.startTransaction(); try { const likeRes = await transaction.collection('comment_like') .where({ user_id: uid, comment_id: comment_id }).remove(); if (likeRes.deleted === 0) { await transaction.rollback(); return { code: 201, msg: '未找到点赞记录' }; } await transaction.collection('comment').doc(comment_id) .update({ like_count: _.inc(-1) }); await transaction.commit(); return { code: 200 }; } catch (e) { await transaction.rollback(); return { code: 500, msg: e.message }; } }, }