HotTRDealsBackend/services/redis/commentCache.service.js
2026-02-04 06:39:10 +00:00

207 lines
7.0 KiB
JavaScript

const { getRedisClient } = require("./client")
const { getOrCacheDeal, getDealIdByCommentId, ensureMinDealTtl } = require("./dealCache.service")
const DEFAULT_TTL_SECONDS = 15 * 60
const DEAL_KEY_PREFIX = "data:deals:"
const COMMENT_LOOKUP_KEY = "data:comments:lookup"
const COMMENT_IDS_KEY = "data: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()
} 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))
} 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,
}