Files
t/uniCloud-alipay/cloudfunctions/comments/index.obj.js
T
2025-06-13 20:46:10 +08:00

866 lines
18 KiB
JavaScript

const db = uniCloud.database();
const _ = db.command;
const $ = db.command.aggregate;
const {
log,
time
} = require('console');
const uniID = require('uni-id-common');
let obj = {
getDateTime() {
return new Date();
},
async getDeletedCount(commentId) {
const {
deleted
} = await db.collection('comment_like')
.where({
comment_id: commentId
})
.count();
return deleted;
},
// 高并发安全的计数方法
async incrementReadCount(query) {
const t = Date.now();
try {
await this.checkRequestFrequency(query.article_id, context.CLIENTIP);
const res = await db.collection('uni-cms-articles')
.doc(query.article_id.toString())
.update({
view_count: db.command.inc(1),
last_view_time: t
});
return {
code: 200,
data: {
view_count: res.updated ? res.data.view_count : 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)
const isRootNode = !parentId;
if (isRootNode) {
roots.push(node);
}
console.log('节点预处理:', {
id: node.id,
parentId,
isRootNode
});
});
// 2. 构建树结构
comments.forEach(comment => {
const currentId = comment._id.toString();
const current = map.get(currentId);
const parentId = current.parent_id;
if (parentId) {
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) {
return {
code: 400,
message: '评论内容不能为空'
};
}
console.log(query);
let {
article_id,
content,
parent_id,
root_id
} = query;
console.log(root_id, parent_id);
// 处理父评论逻辑
if (parent_id && parent_id !== 0) {
const parentRecord = await db.collection('comment')
.doc(parent_id)
.get();
if (!parentRecord.data[0]) {
return {
code: 404,
message: '父评论不存在'
};
}
}
const parent_comment = await db.collection("comment")
.doc(parent_id)
.get();
console.log(parent_comment);
// 构建评论数据
const commentData = {
article_id,
author_id: uid,
content,
parent_id,
root_id,
parent_author_id: parent_comment.data.length > 0 ? parent_comment.data[0].author_id : 0,
parent_author_content: parent_comment.data.length > 0 ? parent_comment.data[0].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);
await db.collection('comment').doc(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) {
return {
code: 400,
message: '评论内容不能为空'
};
}
let {
content,
parent_id = 0,
root_id = 0
} = query;
// 处理父评论逻辑
if (parent_id && parent_id !== 0) {
const parentRecord = await db.collection('comment')
.doc(parent_id)
.get();
if (!parentRecord.data[0]) {
return {
code: 404,
message: '父评论不存在'
};
}
// 继承父评论的root_id(如果父评论是根评论则使用父评论ID)
root_id = parentRecord.data[0].root_id === 0 ?
parent_id :
parentRecord.data[0].root_id;
}
// 构建评论数据
const commentData = {
article_id: query.article_id,
author_id: uid,
content,
parent_id,
root_id,
create_time: obj.getDateTime(),
update_time: obj.getDateTime(),
ip_location: {
clientIP: clientInfo.clientIP
} || {},
like_count: 0,
reply_count: 0
};
console.log(commentData);
// 插入数据库
const {
id
} = await db.collection('comment').add(commentData);
// 更新父评论的回复计数
if (parent_id !== 0) {
await db.collection('comment')
.doc(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();
if (!commentDoc.data) {
throw new Error('NOT_FOUND: 评论不存在');
}
console.log(commentDoc.data);
if (commentDoc.data[0].author_id !== uid) {
throw new Error('PERMISSION_DENIED: 无权限删除该评论');
}
transaction = await db.startTransaction();
await transaction.collection('comment').doc(id).remove();
await transaction.collection('comment_like')
.where({
comment_id: id,
type: 'comment' // 添加类型过滤提高准确性
})
.remove();
if (!root_id === 0) {
await transaction.collection('uni-cms-articles')
.doc(root_id)
.update({
reply_count: _.inc(-1)
});
}
await transaction.commit();
await db.collection('comment_operation_logs').add({
type: 'comment_delete',
uid,
comment_id: commentDoc.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
})
.get();
if (commentDoc.data.author_id !== uid) {
throw new Error('PERMISSION_DENIED: 无权修改该评论');
}
// ==== 数据处理 ====
const sanitizedContent = comment.content.trim();
const maskedIP = clientInfo.clientIP.replace(/\.\d+$/, '.*');
// ==== 构建更新数据 ====
const updateData = {
updateTime: obj.getDateTime(),
content: sanitizedContent,
ip_location: 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: commentDoc.data.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,
user: this.getClientInfo().uid
});
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({
root_id: 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) {
let datatime = new Date().getTime();
const transaction = await db.startTransaction();
try {
const exist = await transaction.collection('comment_like')
.where({
user_id: user_id,
comment_id: comment_id
}).count();
if (exist.total > 0) throw new Error('已点赞');
await transaction.collection('comment_like').add({
user_id: user_id,
comment_id: comment_id,
create_time: datatime
});
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) {
try {
const likeRes = await db.collection('comment_like')
.where({
user_id: user_id,
comment_id: comment_id
}).remove();
if (likeRes.deleted === 0) throw new Error('未找到点赞记录');
await db.collection('comment').doc(comment_id)
.update({
like_count: _.inc(-1)
});
return {
code: 200
};
} catch (e) {
return {
code: 500,
msg: e.message
};
}
},
}