HotTRDealsBackend/services/redis/dealAnalytics.service.js
2026-02-07 22:42:02 +00:00

189 lines
5.3 KiB
JavaScript

const { randomUUID } = require("crypto")
const { getRedisClient } = require("./client")
const dealAnalyticsDb = require("../../db/dealAnalytics.db")
const { ensureMinDealTtl } = require("./dealCache.service")
const { DEAL_ANALYTICS_TOTAL_HASH_KEY } = require("./dbSync.service")
const DEAL_ANALYTICS_TOTAL_PREFIX = "deals:analytics:total:"
function createRedisClient() {
return getRedisClient()
}
function getTotalKey(dealId) {
return `${DEAL_ANALYTICS_TOTAL_PREFIX}${dealId}`
}
function normalizeIds(ids = []) {
return Array.from(
new Set(
(Array.isArray(ids) ? ids : [])
.map((id) => Number(id))
.filter((id) => Number.isInteger(id) && id > 0)
)
)
}
function isValidEventType(type) {
const normalized = String(type || "").toUpperCase()
return ["IMPRESSION", "VIEW", "CLICK"].includes(normalized)
}
function aggregateEventIncrements(events = []) {
const byDeal = new Map()
for (const event of events) {
const dealId = Number(event.dealId)
if (!Number.isInteger(dealId) || dealId <= 0) continue
const type = String(event.type || "").toUpperCase()
const entry = byDeal.get(dealId) || { dealId, impressions: 0, views: 0, clicks: 0 }
if (type === "IMPRESSION") entry.impressions += 1
else if (type === "VIEW") entry.views += 1
else if (type === "CLICK") entry.clicks += 1
byDeal.set(dealId, entry)
}
return Array.from(byDeal.values())
}
async function seedDealAnalyticsTotals({ dealIds = [] } = {}) {
const ids = normalizeIds(dealIds)
if (!ids.length) return 0
await dealAnalyticsDb.ensureTotalsForDealIds(ids)
const totals = await dealAnalyticsDb.getTotalsByDealIds(ids)
const totalsById = new Map(totals.map((t) => [t.dealId, t]))
const redis = createRedisClient()
try {
const pipeline = redis.pipeline()
ids.forEach((id) => {
const total = totalsById.get(id) || { impressions: 0, views: 0, clicks: 0 }
pipeline.hset(
getTotalKey(id),
"impressions",
String(total.impressions || 0),
"views",
String(total.views || 0),
"clicks",
String(total.clicks || 0)
)
})
await pipeline.exec()
return ids.length
} finally {}
}
async function initDealAnalyticsTotal(dealId) {
const id = Number(dealId)
if (!Number.isInteger(id) || id <= 0) return 0
await dealAnalyticsDb.ensureTotalsForDealIds([id])
await seedDealAnalyticsTotals({ dealIds: [id] })
return 1
}
async function queueDealEvents(events = []) {
const valid = (Array.isArray(events) ? events : []).filter(
(e) =>
e &&
Number.isInteger(Number(e.dealId)) &&
(e.userId || e.ip) &&
isValidEventType(e.type)
)
if (!valid.length) return 0
const increments = aggregateEventIncrements(valid)
if (!increments.length) return 0
const redis = createRedisClient()
try {
await incrementDealAnalyticsTotalsInRedis(increments)
const pipeline = redis.pipeline()
increments.forEach((entry) => {
const field = `dealTotals:${entry.dealId}:${randomUUID()}`
pipeline.hset(DEAL_ANALYTICS_TOTAL_HASH_KEY, field, JSON.stringify(entry))
})
await pipeline.exec()
return valid.length
} catch {
try {
await dealAnalyticsDb.applyDealTotalsBatch(increments)
return valid.length
} catch {
return 0
}
} finally {}
}
async function queueDealImpressions({ dealIds = [], userId = null, ip = null } = {}) {
if (!userId && !ip) return 0
const ids = normalizeIds(dealIds)
if (!ids.length) return 0
const events = ids.map((dealId) => ({
dealId,
type: "IMPRESSION",
userId,
ip,
}))
await Promise.all(ids.map((id) => ensureMinDealTtl(id, { minSeconds: 15 * 60 })))
return queueDealEvents(events)
}
async function queueDealView({ dealId, userId = null, ip = null } = {}) {
if (!userId && !ip) return 0
const id = Number(dealId)
if (!Number.isInteger(id) || id <= 0) return 0
await ensureMinDealTtl(id, { minSeconds: 15 * 60 })
return queueDealEvents([
{
dealId: id,
type: "VIEW",
userId,
ip,
},
])
}
async function queueDealClick({ dealId, userId = null, ip = null } = {}) {
if (!userId && !ip) return 0
const id = Number(dealId)
if (!Number.isInteger(id) || id <= 0) return 0
await ensureMinDealTtl(id, { minSeconds: 15 * 60 })
return queueDealEvents([
{
dealId: id,
type: "CLICK",
userId,
ip,
},
])
}
async function incrementDealAnalyticsTotalsInRedis(increments = []) {
const data = (Array.isArray(increments) ? increments : []).filter(
(item) => item && Number.isInteger(Number(item.dealId))
)
if (!data.length) return 0
const redis = createRedisClient()
try {
const pipeline = redis.pipeline()
data.forEach((item) => {
const key = getTotalKey(item.dealId)
if (item.impressions) pipeline.hincrby(key, "impressions", Number(item.impressions))
if (item.views) pipeline.hincrby(key, "views", Number(item.views))
if (item.clicks) pipeline.hincrby(key, "clicks", Number(item.clicks))
})
await pipeline.exec()
return data.length
} catch {
return 0
} finally {}
}
module.exports = {
seedDealAnalyticsTotals,
initDealAnalyticsTotal,
queueDealImpressions,
queueDealView,
queueDealClick,
incrementDealAnalyticsTotalsInRedis,
DEAL_ANALYTICS_TOTAL_HASH_KEY,
}