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

169 lines
4.6 KiB
JavaScript

const { randomUUID } = require("crypto")
const { getRedisClient } = require("./client")
const dealAnalyticsDb = require("../../db/dealAnalytics.db")
const { ensureMinDealTtl } = require("./dealCache.service")
const DEAL_EVENT_HASH_KEY = "dbsync:dealEvents"
const DEAL_ANALYTICS_TOTAL_PREFIX = "data: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)
}
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 redis = createRedisClient()
try {
const pipeline = redis.pipeline()
valid.forEach((event) => {
const field = `dealEvent:${randomUUID()}`
const payload = JSON.stringify({
dealId: Number(event.dealId),
type: String(event.type).toUpperCase(),
userId: event.userId ? Number(event.userId) : null,
ip: event.ip ? String(event.ip) : null,
createdAt: event.createdAt || new Date().toISOString(),
})
pipeline.hset(DEAL_EVENT_HASH_KEY, field, payload)
})
await pipeline.exec()
return valid.length
} 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
} finally {}
}
module.exports = {
seedDealAnalyticsTotals,
initDealAnalyticsTotal,
queueDealImpressions,
queueDealView,
queueDealClick,
incrementDealAnalyticsTotalsInRedis,
DEAL_EVENT_HASH_KEY,
}