229 lines
7.1 KiB
JavaScript
229 lines
7.1 KiB
JavaScript
const { getRedisClient } = require("./client")
|
|
const { getSellersByIds, getSellerById } = require("./sellerCache.service")
|
|
const { getUsersPublicByIds, setUsersPublicInRedis } = require("./userPublicCache.service")
|
|
const userDB = require("../../db/user.db")
|
|
const { getMyVotesForDeals } = require("./dealVote.service")
|
|
|
|
function createRedisClient() {
|
|
return getRedisClient()
|
|
}
|
|
|
|
const USER_SAVED_HASH_PREFIX = "users:savedmap:"
|
|
|
|
async function getUserSavedMap(redis, dealIds = [], viewerId = null) {
|
|
const uid = Number(viewerId)
|
|
if (!Number.isInteger(uid) || !dealIds.length) return new Map()
|
|
try {
|
|
const results = await redis.hmget(`${USER_SAVED_HASH_PREFIX}${uid}`, dealIds.map(String))
|
|
const map = new Map()
|
|
results.forEach((raw, idx) => {
|
|
const value = raw == null ? 0 : Number(raw)
|
|
if (value) map.set(Number(dealIds[idx]), true)
|
|
})
|
|
return map
|
|
} catch {
|
|
return new Map()
|
|
}
|
|
}
|
|
|
|
async function getHotDealListId(redis, hotListId) {
|
|
if (hotListId) return String(hotListId)
|
|
const latest = await redis.get("deals:lists:hot:latest")
|
|
return latest ? String(latest) : null
|
|
}
|
|
|
|
async function getListId(redis, listKeyPrefix, listId) {
|
|
if (listId) return String(listId)
|
|
const latest = await redis.get(`${listKeyPrefix}:latest`)
|
|
return latest ? String(latest) : null
|
|
}
|
|
|
|
async function getHotDealIds({ hotListId } = {}) {
|
|
const redis = createRedisClient()
|
|
|
|
try {
|
|
const listId = await getHotDealListId(redis, hotListId)
|
|
if (!listId) return { hotListId: null, dealIds: [] }
|
|
|
|
const key = `deals:lists:hot:${listId}`
|
|
const raw = await redis.call("JSON.GET", key, "$.dealIds")
|
|
if (!raw) return { hotListId: listId, dealIds: [] }
|
|
|
|
const parsed = JSON.parse(raw)
|
|
const dealIds = Array.isArray(parsed) ? parsed[0] : []
|
|
|
|
return {
|
|
hotListId: listId,
|
|
dealIds: Array.isArray(dealIds) ? dealIds.map((id) => Number(id)) : [],
|
|
}
|
|
} catch {
|
|
return { hotListId: null, dealIds: [] }
|
|
} finally {}
|
|
}
|
|
|
|
async function getDealsByIdsFromRedis(ids = [], viewerId = null) {
|
|
if (!ids.length) return []
|
|
const redis = createRedisClient()
|
|
|
|
try {
|
|
const pipeline = redis.pipeline()
|
|
ids.forEach((id) => {
|
|
pipeline.call("JSON.GET", `deals:cache:${id}`)
|
|
})
|
|
|
|
const results = await pipeline.exec()
|
|
const deals = []
|
|
|
|
results.forEach(([, raw], idx) => {
|
|
if (!raw) return
|
|
try {
|
|
const deal = JSON.parse(raw)
|
|
if (deal && deal.id) {
|
|
deals.push({ deal, index: idx })
|
|
}
|
|
} catch {
|
|
return
|
|
}
|
|
})
|
|
|
|
// Preserve original order (ids array order)
|
|
const ordered = deals
|
|
.sort((a, b) => a.index - b.index)
|
|
.map((item) => item.deal)
|
|
|
|
const sellerIds = ordered
|
|
.map((deal) => Number(deal?.sellerId))
|
|
.filter((id) => Number.isInteger(id) && id > 0)
|
|
const sellerMap = sellerIds.length ? await getSellersByIds(sellerIds) : new Map()
|
|
const voteMap = viewerId ? await getMyVotesForDeals(ordered.map((d) => d.id), viewerId) : new Map()
|
|
const savedMap = viewerId ? await getUserSavedMap(redis, ordered.map((d) => d.id), viewerId) : new Map()
|
|
|
|
const userIds = ordered
|
|
.map((deal) => Number(deal?.userId))
|
|
.filter((id) => Number.isInteger(id) && id > 0)
|
|
const userMap = userIds.length ? await getUsersPublicByIds(userIds) : new Map()
|
|
const missingUserIds = Array.from(
|
|
new Set(userIds.filter((id) => !userMap.has(id)))
|
|
)
|
|
|
|
if (missingUserIds.length) {
|
|
const missingSet = new Set(missingUserIds)
|
|
const ttlPipeline = redis.pipeline()
|
|
ordered.forEach((deal) => {
|
|
ttlPipeline.ttl(`deals:cache:${deal.id}`)
|
|
})
|
|
const ttlResults = await ttlPipeline.exec()
|
|
const ttlByDealId = new Map()
|
|
ttlResults.forEach(([, ttl], idx) => {
|
|
const dealId = ordered[idx]?.id
|
|
if (dealId) ttlByDealId.set(Number(dealId), Number(ttl))
|
|
})
|
|
|
|
const users = await userDB.findUsersByIds(missingUserIds, {
|
|
select: {
|
|
id: true,
|
|
username: true,
|
|
avatarUrl: true,
|
|
userBadges: {
|
|
orderBy: { earnedAt: "desc" },
|
|
select: {
|
|
earnedAt: true,
|
|
badge: { select: { id: true, name: true, iconUrl: true, description: true } },
|
|
},
|
|
},
|
|
},
|
|
})
|
|
|
|
const ttlByUserId = {}
|
|
ordered.forEach((deal) => {
|
|
const uid = Number(deal?.userId)
|
|
if (!missingSet.has(uid)) return
|
|
const ttl = ttlByDealId.get(Number(deal?.id))
|
|
if (ttl == null || ttl <= 0) return
|
|
ttlByUserId[uid] = Math.max(ttlByUserId[uid] || 0, ttl)
|
|
})
|
|
|
|
if (users.length) {
|
|
await setUsersPublicInRedis(users, { ttlSecondsById: ttlByUserId })
|
|
users.forEach((u) => userMap.set(u.id, u))
|
|
}
|
|
}
|
|
|
|
const enriched = ordered.map((deal) => {
|
|
let next = deal
|
|
if (!next?.user && next?.userId) {
|
|
const user = userMap.get(Number(next.userId)) || null
|
|
if (user) next = { ...next, user }
|
|
}
|
|
if (!next?.seller && next?.sellerId) {
|
|
const seller = sellerMap.get(Number(next.sellerId)) || null
|
|
if (seller) next = { ...next, seller }
|
|
}
|
|
const myVote = viewerId ? Number(voteMap.get(Number(next.id)) ?? 0) : 0
|
|
const isSaved = viewerId ? savedMap.get(Number(next.id)) === true : false
|
|
return { ...next, myVote, isSaved }
|
|
})
|
|
|
|
return enriched
|
|
} catch {
|
|
return []
|
|
} finally {}
|
|
}
|
|
|
|
async function getDealByIdFromRedis(id, viewerId = null) {
|
|
const redis = createRedisClient()
|
|
try {
|
|
const raw = await redis.call("JSON.GET", `deals:cache:${id}`)
|
|
if (!raw) return null
|
|
let deal = JSON.parse(raw)
|
|
if (deal?.sellerId && !deal?.seller) {
|
|
const seller = await getSellerById(Number(deal.sellerId))
|
|
if (seller) deal = { ...deal, seller }
|
|
}
|
|
if (viewerId) {
|
|
const voteMap = await getMyVotesForDeals([deal.id], viewerId)
|
|
const savedMap = await getUserSavedMap(redis, [deal.id], viewerId)
|
|
const isSaved = savedMap.get(Number(deal.id)) === true
|
|
deal = { ...deal, myVote: Number(voteMap.get(Number(deal.id)) ?? 0), isSaved }
|
|
}
|
|
return deal
|
|
} catch {
|
|
return null
|
|
} finally {}
|
|
}
|
|
|
|
async function getHotRangeDealIds({ range, listId } = {}) {
|
|
const redis = createRedisClient()
|
|
|
|
try {
|
|
const prefix =
|
|
range === "day"
|
|
? "deals:lists:hot_day"
|
|
: range === "week"
|
|
? "deals:lists:hot_week"
|
|
: range === "month"
|
|
? "deals:lists:hot_month"
|
|
: null
|
|
if (!prefix) return { listId: null, dealIds: [] }
|
|
|
|
const resolvedId = await getListId(redis, prefix, listId)
|
|
if (!resolvedId) return { listId: null, dealIds: [] }
|
|
|
|
const key = `${prefix}:${resolvedId}`
|
|
const raw = await redis.call("JSON.GET", key, "$.dealIds")
|
|
if (!raw) return { listId: resolvedId, dealIds: [] }
|
|
|
|
const parsed = JSON.parse(raw)
|
|
const dealIds = Array.isArray(parsed) ? parsed[0] : []
|
|
|
|
return {
|
|
listId: resolvedId,
|
|
dealIds: Array.isArray(dealIds) ? dealIds.map((id) => Number(id)) : [],
|
|
}
|
|
} catch {
|
|
return { listId: null, dealIds: [] }
|
|
} finally {}
|
|
}
|
|
|
|
module.exports = { getHotDealIds, getHotRangeDealIds, getDealsByIdsFromRedis, getDealByIdFromRedis }
|