169 lines
4.6 KiB
JavaScript
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,
|
|
}
|