HotTRDealsBackend/services/redis/categoryDealIndex.service.js
2026-02-09 21:47:55 +00:00

150 lines
4.3 KiB
JavaScript

const { getRedisClient } = require("./client")
const CATEGORY_DEAL_INDEX_KEY_PREFIX = "index:category:"
const CATEGORY_DEAL_INDEX_KEY_SUFFIX = ":deals"
function normalizePositiveInt(value) {
const num = Number(value)
if (!Number.isInteger(num) || num <= 0) return null
return num
}
function normalizeEpochMs(value) {
const num = Number(value)
if (!Number.isFinite(num) || num <= 0) return null
return Math.floor(num)
}
function toEpochMs(value) {
if (value instanceof Date) return value.getTime()
const date = new Date(value)
if (Number.isNaN(date.getTime())) return null
return date.getTime()
}
function isActiveStatus(status) {
return String(status || "").toUpperCase() === "ACTIVE"
}
function getCategoryDealIndexKey(categoryId) {
const cid = normalizePositiveInt(categoryId)
if (!cid) return null
return `${CATEGORY_DEAL_INDEX_KEY_PREFIX}${cid}${CATEGORY_DEAL_INDEX_KEY_SUFFIX}`
}
function normalizeDealIndexPayload(payload = {}) {
const dealId = normalizePositiveInt(payload.dealId ?? payload.id)
const categoryId = normalizePositiveInt(payload.categoryId)
const createdAtTs =
normalizeEpochMs(payload.createdAtTs) ?? normalizeEpochMs(toEpochMs(payload.createdAt))
const status = String(payload.status || "").toUpperCase()
return { dealId, categoryId, createdAtTs, status }
}
function isIndexableDeal(payload = {}) {
const normalized = normalizeDealIndexPayload(payload)
return Boolean(
normalized.dealId &&
normalized.categoryId &&
normalized.createdAtTs &&
isActiveStatus(normalized.status)
)
}
function addDealToCategoryIndexInPipeline(pipeline, payload = {}) {
const normalized = normalizeDealIndexPayload(payload)
if (!normalized.dealId || !normalized.categoryId || !normalized.createdAtTs) return false
if (!isActiveStatus(normalized.status)) return false
const key = getCategoryDealIndexKey(normalized.categoryId)
if (!key) return false
pipeline.zadd(key, String(normalized.createdAtTs), String(normalized.dealId))
return true
}
function removeDealFromCategoryIndexInPipeline(pipeline, payload = {}) {
const normalized = normalizeDealIndexPayload(payload)
if (!normalized.dealId || !normalized.categoryId) return false
const key = getCategoryDealIndexKey(normalized.categoryId)
if (!key) return false
pipeline.zrem(key, String(normalized.dealId))
return true
}
async function reconcileDealCategoryIndex({ before = null, after = null } = {}) {
const prev = normalizeDealIndexPayload(before || {})
const next = normalizeDealIndexPayload(after || {})
const prevIndexable = isIndexableDeal(prev)
const nextIndexable = isIndexableDeal(next)
const redis = getRedisClient()
const pipeline = redis.pipeline()
let commandCount = 0
if (prevIndexable) {
const removedForStatus = !nextIndexable
const movedCategory = nextIndexable && prev.categoryId !== next.categoryId
if (removedForStatus || movedCategory) {
if (removeDealFromCategoryIndexInPipeline(pipeline, prev)) commandCount += 1
}
}
if (nextIndexable) {
const isNew = !prevIndexable
const movedCategory = prevIndexable && prev.categoryId !== next.categoryId
const scoreChanged =
prevIndexable &&
prev.categoryId === next.categoryId &&
Number(prev.createdAtTs) !== Number(next.createdAtTs)
if (isNew || movedCategory || scoreChanged) {
if (addDealToCategoryIndexInPipeline(pipeline, next)) commandCount += 1
}
}
if (!commandCount) return 0
try {
await pipeline.exec()
return commandCount
} catch {
return 0
}
}
async function getRecentDealIdsByCategory({
categoryId,
sinceTs,
limit = 30,
} = {}) {
const cid = normalizePositiveInt(categoryId)
if (!cid) return []
const key = getCategoryDealIndexKey(cid)
if (!key) return []
const minTs = normalizeEpochMs(sinceTs) || 0
const safeLimit = Math.max(1, Math.min(Number(limit) || 30, 300))
const redis = getRedisClient()
try {
const ids = await redis.zrevrangebyscore(
key,
"+inf",
String(minTs),
"LIMIT",
0,
safeLimit
)
return Array.isArray(ids)
? ids.map((id) => Number(id)).filter((id) => Number.isInteger(id) && id > 0)
: []
} catch {
return []
}
}
module.exports = {
getCategoryDealIndexKey,
addDealToCategoryIndexInPipeline,
removeDealFromCategoryIndexInPipeline,
reconcileDealCategoryIndex,
getRecentDealIdsByCategory,
}