## 阻断性缺陷 - list.vue 是 0 字节空文件、slist.vue 与 search/search.vue 从未存在, 而前者是 tabBar 首页、后者是 tabBar「搜索」页 —— 开屏即白屏。 按 .nvue 原型与详情页契约重建三页(CSS 渐变主视觉、分类筛选、搜索历史/热搜/联想)。 - parse-image-url.js 对空封面调 undefined.startsWith 直接抛错,列表页整页崩。 - 云函数目录缺 uni-cms-articles / uni-cms-categories / uni-cms-unlock-record schema 与 schema.ext.js,线上内容渲染与解锁逻辑无配置可用。 ## 越权与数据一致性 - uni-cms-articles:del/update/add 全部无鉴权,未登录即可删任意文章、 改他人文章作者与阅读量。补 login + 作者归属校验,作者与计数改为服务端取值。 - comments.likeComment/relikeComment:直接采信客户端传入的 user_id, 可冒名点赞刷计数。改为以令牌为准,并纳入事务。 - comments.updateComment:对数组取 .author_id,权限判断恒失败; 字段名 updateTime 与 ip_location 类型与 schema 不符。 - comments.deleteComment:`!root_id === 0` 优先级错误导致计数恒不减; 且误更新 uni-cms-articles、按不存在的 type 字段删点赞明细产生孤儿数据。 - cms-articles-like/collect:查重条件混入本次请求时间戳,防重永远失效, 可无限重复刷计数;补唯一索引并事务化。 - cms-vote:读-改-写票数导致并发丢票,记录与统计非原子;改为事务 + 原子自增。 - cms-articles-log:忽略传入 user_id 直接返回全表,泄露全站浏览记录。 - article_info:get() 使用未定义变量必崩;读接口全部无鉴权。 - user-info:公开资料接口可查任意用户 last_login_ip。 ## 资源与数据 - 全项目清空失效的签名外链(expire_at 均为 2025-03,必然 403), 改为本地生成资源:6 套文章模板、8 个编辑器图标、2 张文章配图。 - 新增分类 / 模板 / 礼物 / 热搜词种子数据,并在 db_init.json 登记, 同时补上点赞、收藏、投票、浏览日志的唯一索引。 ## 功能 - 草稿箱:预览页拆出「发布」与「存为草稿」,作品列表按状态筛选并显示徽标。 原实现有 4 个 tab 但只有 1 个有内容,且 article_status 在 UI 上无体现。 - 编辑中断恢复:接上原本空实现的「编辑草稿」回调,区分新建与编辑已有文章。 ## 工具 - tools/audit-project.js:编码 / 页面路由 / 云调用 / 云函数鉴权 / 敏感信息检查 - tools/check-vue.js:SFC 脚本语法(词法扫描处理 import·export 与条件编译) - tools/verify.js:一键验证;两个检查器各带自测,防止"永远通过" - tools/gen-*.py:模板与图标资源生成脚本
884 lines
22 KiB
JavaScript
884 lines
22 KiB
JavaScript
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 };
|
||
}
|
||
},
|
||
|
||
} |