211 lines
7.1 KiB
JavaScript
211 lines
7.1 KiB
JavaScript
const { getRedisClient } = require("./client")
|
|
const { getOrCacheDeal, getDealIdByCommentId, ensureMinDealTtl } = require("./dealCache.service")
|
|
|
|
const DEFAULT_TTL_SECONDS = 15 * 60
|
|
const DEAL_KEY_PREFIX = "deals:cache:"
|
|
const COMMENT_LOOKUP_KEY = "comments:lookup"
|
|
const COMMENT_IDS_KEY = "comments:ids"
|
|
|
|
function createRedisClient() {
|
|
return getRedisClient()
|
|
}
|
|
|
|
function normalizeParentId(value) {
|
|
if (value === undefined || value === null || value === "" || value === "null") return null
|
|
const n = Number(value)
|
|
return Number.isInteger(n) && n > 0 ? n : null
|
|
}
|
|
|
|
function pickComments(deal, parentId) {
|
|
const list = Array.isArray(deal?.comments) ? deal.comments : []
|
|
return list.filter(
|
|
(c) =>
|
|
(normalizeParentId(c.parentId) ?? null) === (normalizeParentId(parentId) ?? null) &&
|
|
!c.deletedAt
|
|
)
|
|
}
|
|
|
|
function sortComments(list, sort) {
|
|
const mode = String(sort || "NEW").toUpperCase()
|
|
if (mode === "TOP") {
|
|
return list.sort((a, b) => {
|
|
const diff = Number(b.likeCount || 0) - Number(a.likeCount || 0)
|
|
if (diff !== 0) return diff
|
|
return new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()
|
|
})
|
|
}
|
|
return list.sort(
|
|
(a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()
|
|
)
|
|
}
|
|
|
|
|
|
async function updateDealCommentsInRedis(dealId, comments, commentCount) {
|
|
const redis = createRedisClient()
|
|
try {
|
|
const pipeline = redis.pipeline()
|
|
pipeline.call("JSON.SET", `${DEAL_KEY_PREFIX}${dealId}`, "$.comments", JSON.stringify(comments))
|
|
if (typeof commentCount === "number") {
|
|
pipeline.call("JSON.SET", `${DEAL_KEY_PREFIX}${dealId}`, "$.commentCount", Number(commentCount))
|
|
}
|
|
await pipeline.exec()
|
|
} catch {
|
|
// ignore cache failures
|
|
} finally {}
|
|
}
|
|
|
|
async function getCommentsForDeal({
|
|
dealId,
|
|
deal = null,
|
|
parentId = null,
|
|
page = 1,
|
|
limit = 10,
|
|
sort = "NEW",
|
|
viewerId = null,
|
|
} = {}) {
|
|
const resolvedDeal =
|
|
deal || (await getOrCacheDeal(dealId, { ttlSeconds: DEFAULT_TTL_SECONDS }))
|
|
if (!resolvedDeal) return { page: 1, total: 0, totalPages: 0, results: [] }
|
|
|
|
const normalizedLimit = Math.max(1, Math.min(Number(limit) || 10, 50))
|
|
const normalizedPage = Math.max(1, Number(page) || 1)
|
|
|
|
let comments = pickComments(resolvedDeal, parentId)
|
|
comments = sortComments(comments, sort)
|
|
|
|
const total = comments.length
|
|
const totalPages = Math.ceil(total / normalizedLimit)
|
|
const start = (normalizedPage - 1) * normalizedLimit
|
|
const results = comments.slice(start, start + normalizedLimit).map((comment) => {
|
|
const likes = Array.isArray(comment.likes) ? comment.likes : []
|
|
const myLike = viewerId
|
|
? likes.some((l) => Number(l.userId) === Number(viewerId))
|
|
: false
|
|
return {
|
|
...comment,
|
|
myLike,
|
|
}
|
|
})
|
|
|
|
return { page: normalizedPage, total, totalPages, results }
|
|
}
|
|
|
|
async function addCommentToRedis(comment, { ttlSeconds = DEFAULT_TTL_SECONDS } = {}) {
|
|
const deal = await getOrCacheDeal(comment.dealId, { ttlSeconds })
|
|
if (!deal) return { added: false }
|
|
|
|
const comments = Array.isArray(deal.comments) ? deal.comments : []
|
|
const newComment = {
|
|
id: comment.id,
|
|
dealId: comment.dealId,
|
|
userId: comment.userId,
|
|
text: comment.text,
|
|
createdAt: comment.createdAt instanceof Date ? comment.createdAt.toISOString() : comment.createdAt,
|
|
parentId: normalizeParentId(comment.parentId),
|
|
likeCount: Number.isFinite(comment.likeCount) ? comment.likeCount : 0,
|
|
repliesCount: Number.isFinite(comment.repliesCount) ? comment.repliesCount : 0,
|
|
deletedAt: null,
|
|
user: comment.user
|
|
? {
|
|
id: comment.user.id,
|
|
username: comment.user.username,
|
|
avatarUrl: comment.user.avatarUrl ?? null,
|
|
}
|
|
: null,
|
|
likes: [],
|
|
}
|
|
|
|
comments.unshift(newComment)
|
|
|
|
if (newComment.parentId) {
|
|
const parent = comments.find((c) => Number(c.id) === Number(newComment.parentId))
|
|
if (parent) {
|
|
parent.repliesCount = Number.isFinite(parent.repliesCount) ? parent.repliesCount + 1 : 1
|
|
}
|
|
}
|
|
|
|
const commentCount = Number.isFinite(deal.commentCount) ? deal.commentCount + 1 : 1
|
|
await updateDealCommentsInRedis(comment.dealId, comments, commentCount)
|
|
await ensureMinDealTtl(comment.dealId, { minSeconds: DEFAULT_TTL_SECONDS })
|
|
const redis = createRedisClient()
|
|
try {
|
|
await redis.hset(COMMENT_LOOKUP_KEY, String(comment.id), String(comment.dealId))
|
|
await redis.sadd(COMMENT_IDS_KEY, String(comment.id))
|
|
} catch {
|
|
// ignore cache failures
|
|
} finally {}
|
|
return { added: true }
|
|
}
|
|
|
|
async function removeCommentFromRedis({ commentId, dealId }) {
|
|
const deal = await getOrCacheDeal(dealId, { ttlSeconds: DEFAULT_TTL_SECONDS })
|
|
if (!deal) return { removed: false }
|
|
|
|
const comments = Array.isArray(deal.comments) ? deal.comments : []
|
|
const target = comments.find((c) => Number(c.id) === Number(commentId))
|
|
if (!target) return { removed: false }
|
|
if (target.deletedAt) return { removed: false, alreadyDeleted: true }
|
|
|
|
target.deletedAt = new Date().toISOString()
|
|
const commentCount = Math.max(0, Number(deal.commentCount || 0) - 1)
|
|
|
|
if (target.parentId) {
|
|
const parent = comments.find((c) => Number(c.id) === Number(target.parentId))
|
|
if (parent && Number.isFinite(parent.repliesCount)) {
|
|
parent.repliesCount = Math.max(0, parent.repliesCount - 1)
|
|
}
|
|
}
|
|
|
|
await updateDealCommentsInRedis(dealId, comments, commentCount)
|
|
await ensureMinDealTtl(dealId, { minSeconds: DEFAULT_TTL_SECONDS })
|
|
return { removed: true }
|
|
}
|
|
|
|
async function updateCommentLikeInRedisByDeal({ dealId, commentId, userId, like }) {
|
|
const deal = await getOrCacheDeal(dealId, { ttlSeconds: DEFAULT_TTL_SECONDS })
|
|
if (!deal) return { liked: Boolean(like), delta: 0, likeCount: 0 }
|
|
|
|
const comments = Array.isArray(deal.comments) ? deal.comments : []
|
|
const target = comments.find((c) => Number(c.id) === Number(commentId))
|
|
if (!target || target.deletedAt) return { liked: Boolean(like), delta: 0, likeCount: 0 }
|
|
|
|
let likes = Array.isArray(target.likes) ? target.likes : []
|
|
const exists = likes.some((l) => Number(l.userId) === Number(userId))
|
|
|
|
let delta = 0
|
|
if (like) {
|
|
if (!exists) {
|
|
likes = [...likes, { userId: Number(userId) }]
|
|
delta = 1
|
|
}
|
|
} else if (exists) {
|
|
likes = likes.filter((l) => Number(l.userId) !== Number(userId))
|
|
delta = -1
|
|
}
|
|
|
|
target.likes = likes
|
|
if (delta !== 0) {
|
|
target.likeCount = Math.max(0, Number(target.likeCount || 0) + delta)
|
|
} else {
|
|
target.likeCount = Number.isFinite(target.likeCount) ? target.likeCount : likes.length
|
|
}
|
|
|
|
await updateDealCommentsInRedis(dealId, comments)
|
|
await ensureMinDealTtl(dealId, { minSeconds: DEFAULT_TTL_SECONDS })
|
|
return { liked: Boolean(like), delta, likeCount: Number(target.likeCount || 0) }
|
|
}
|
|
|
|
async function updateCommentLikeInRedis({ commentId, userId, like }) {
|
|
const dealId = await getDealIdByCommentId(commentId)
|
|
if (!dealId) return { liked: Boolean(like), delta: 0, likeCount: 0 }
|
|
return updateCommentLikeInRedisByDeal({ dealId, commentId, userId, like })
|
|
}
|
|
|
|
module.exports = {
|
|
getCommentsForDeal,
|
|
addCommentToRedis,
|
|
removeCommentFromRedis,
|
|
updateCommentLikeInRedis,
|
|
updateCommentLikeInRedisByDeal,
|
|
}
|