full app
This commit is contained in:
255
utils/adDelivery.js
Normal file
255
utils/adDelivery.js
Normal file
@@ -0,0 +1,255 @@
|
||||
const AdModel = require('../models/AdModel')
|
||||
const PostModel = require('../models/PostModel')
|
||||
const UserModel = require('../models/UserModel')
|
||||
const CommentModel = require('../models/CommentModel')
|
||||
const ProjectModel = require('../models/ProjectModel')
|
||||
const ShopProductListingModel = require('../models/ShopProductListingModel')
|
||||
const ShopModel = require('../models/ShopModel')
|
||||
const AdvertisingModel = require('../models/AdvertisingModel')
|
||||
const AdvertisingProfileModel = require('../models/AdvertisingProfile')
|
||||
const { sendPushToTokens } = require('./pushNotification')
|
||||
const { resolveShareableLinkDisplay } = require('./shareableLinkDisplay')
|
||||
|
||||
const LINKED_CONTENT_KIND = {
|
||||
project: { kind: 'project', field: 'content_project_id' },
|
||||
product: { kind: 'shopproduct', field: 'content_shop_product_id' },
|
||||
shop: { kind: 'shop', field: 'content_shop_id' },
|
||||
billboard: { kind: 'billboard', field: 'content_billboard_id' }
|
||||
}
|
||||
|
||||
/**
|
||||
* یه تبلیغِ فعالِ منطبق با پروفایلِ بیننده پیدا میکنه — هر فیلدِ هدفگذاری که تبلیغکننده
|
||||
* خالی گذاشته (null/آرایهی خالی) یعنی وایلدکارت (روی هر بینندهای صدق میکنه). خودِ
|
||||
* تبلیغکننده تبلیغِ خودش رو نمیبینه. اگه چند تبلیغِ منطبق بود، یکی رندوم انتخاب میشه
|
||||
* (چرخشِ ساده، بهجای همیشه یکی رو نشون دادن)
|
||||
*/
|
||||
async function findActiveAd (adType, viewer) {
|
||||
if (!viewer?._id) return null
|
||||
|
||||
const query = {
|
||||
status: 'active',
|
||||
ad_type: adType,
|
||||
expires_at: { $gt: new Date() },
|
||||
user_id: { $ne: viewer._id },
|
||||
$and: [
|
||||
{ $or: [{ target_country_id: null }, { target_country_id: viewer.country?.id ?? null }] },
|
||||
{ $or: [{ target_province_id: null }, { target_province_id: viewer.province?.id ?? null }] },
|
||||
{ $or: [{ target_city_id: null }, { target_city_id: viewer.city?.id ?? null }] },
|
||||
{ $or: [{ target_gender: 'any' }, { target_gender: viewer.gender || '__none__' }] },
|
||||
{ $or: [{ target_expertise: null }, { target_expertise: viewer.expertise || '__none__' }] },
|
||||
{
|
||||
$or: [
|
||||
{ target_sub_expertise: { $size: 0 } },
|
||||
{ target_sub_expertise: { $in: viewer.sub_expertise || [] } }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
const candidates = await AdModel.find(query).limit(20).lean()
|
||||
if (!candidates.length) return null
|
||||
return candidates[Math.floor(Math.random() * candidates.length)]
|
||||
}
|
||||
|
||||
/** عینِ shape خروجیِ mapPostsWithUsers (getUserController.js) — تا کارتِ پست توی فرانت
|
||||
* بدونِ نیاز به شرطِ جدا، پستِ تبلیغاتی رو هم درست رندر کنه */
|
||||
async function buildAdPostItem (ad) {
|
||||
if (!ad?.content_post_id) return null
|
||||
const post = await PostModel.findById(ad.content_post_id).lean()
|
||||
if (!post) return null
|
||||
const user = await UserModel.findById(post.user_id).lean()
|
||||
if (!user) return null
|
||||
|
||||
const commentsCount = await CommentModel.countDocuments({
|
||||
post: post._id,
|
||||
status: { $in: ['accepted', 'temp_accept'] }
|
||||
})
|
||||
|
||||
return {
|
||||
...post,
|
||||
user_name: user.user_name || '',
|
||||
first_name: user.first_name || '',
|
||||
last_name: user.last_name || '',
|
||||
expertise: user.expertise || '',
|
||||
sub_expertise: user.sub_expertise || [],
|
||||
province: user.show_location ? user.province || {} : {},
|
||||
city: user.show_location ? user.city || {} : {},
|
||||
show_location: user.show_location,
|
||||
user_level: user.user_level || '',
|
||||
profile_image: user.profile_image || '',
|
||||
is_verified: user.is_verified || '',
|
||||
verify_badge: user.verify_badge || '',
|
||||
user_score: user.user_score || 0,
|
||||
rate: user.rate || 0,
|
||||
likesCount: post.likes ? post.likes.length : 0,
|
||||
is_liked: false,
|
||||
commentsCount,
|
||||
is_ad: true,
|
||||
ad_id: String(ad._id)
|
||||
}
|
||||
}
|
||||
|
||||
function storyMediaUrl (storagePath) {
|
||||
if (!storagePath) return ''
|
||||
const normalized = storagePath.replace(/\\/g, '/')
|
||||
const idx = normalized.indexOf('/storage/')
|
||||
if (idx >= 0) return normalized.slice(idx)
|
||||
return normalized.replace('/root/modstagram-back/storage', '/storage')
|
||||
}
|
||||
|
||||
/** عینِ buildAdPostItem ولی برای استوری — getStoriesFeed هر گروه رو با user+stories
|
||||
* برمیگردونه. از مدیای اسنپشاتشدهی خودِ AdModel استفاده میکنه (نه StoryModel زنده) —
|
||||
* چون استوریِ اصلی معمولاً ۲۴ساعته منقضی میشه ولی کمپینِ تبلیغاتی چند روزهست */
|
||||
async function buildAdStoryGroup (ad) {
|
||||
if (!ad?.content_media_path) return null
|
||||
const user = await UserModel.findById(ad.user_id).lean()
|
||||
if (!user) return null
|
||||
|
||||
return {
|
||||
user: {
|
||||
_id: user._id,
|
||||
user_name: user.user_name,
|
||||
profile_image: user.profile_image,
|
||||
is_verified: user.is_verified,
|
||||
verify_badge: user.verify_badge
|
||||
},
|
||||
stories: [{
|
||||
_id: ad.content_story_id,
|
||||
media_path: storyMediaUrl(ad.content_media_path),
|
||||
media_type: ad.content_media_type,
|
||||
overlays: ad.content_overlays || [],
|
||||
linked_post_id: null,
|
||||
createdAt: ad.started_at,
|
||||
expires_at: ad.expires_at,
|
||||
viewed: false,
|
||||
liked: false,
|
||||
likes_count: 0,
|
||||
is_ad: true,
|
||||
ad_id: String(ad._id)
|
||||
}],
|
||||
has_unviewed: true,
|
||||
is_following: false,
|
||||
is_ad: true,
|
||||
ad_id: String(ad._id),
|
||||
latest_at: ad.started_at
|
||||
}
|
||||
}
|
||||
|
||||
/** برای چهار نوعِ «تبلیغِ محتوای موجود» (پروژه/کالا/فروشگاه/بیلبورد) — دقیقاً همون
|
||||
* resolveShareableLinkDisplay که برای لینکهای اشتراکیِ کپشن استفاده میشه (title+image) */
|
||||
async function buildAdLinkedContentItem (ad) {
|
||||
const entry = LINKED_CONTENT_KIND[ad?.ad_type]
|
||||
if (!entry) return null
|
||||
const contentId = ad[entry.field]
|
||||
if (!contentId) return null
|
||||
const display = await resolveShareableLinkDisplay(entry.kind, contentId)
|
||||
if (!display) return null
|
||||
return {
|
||||
ad_id: String(ad._id),
|
||||
ad_type: ad.ad_type,
|
||||
content_id: String(contentId),
|
||||
title: display.title,
|
||||
image: display.image,
|
||||
is_ad: true
|
||||
}
|
||||
}
|
||||
|
||||
/** عینِ shape خروجیِ getProjects (enrichProjectsWithRequests) — یه سندِ خامِ ProjectModel،
|
||||
* بدونِ projectRequests (پروژهی تبلیغاتی نیازی به درخواستها نداره) */
|
||||
async function buildAdProjectItem (ad) {
|
||||
if (!ad?.content_project_id) return null
|
||||
const project = await ProjectModel.findById(ad.content_project_id).lean()
|
||||
if (!project) return null
|
||||
return { ...project, is_ad: true, ad_id: String(ad._id) }
|
||||
}
|
||||
|
||||
/** عینِ shape خروجیِ getAdvertisingsWeb (لیستِ بیلبوردها) */
|
||||
async function buildAdBillboardItem (ad) {
|
||||
if (!ad?.content_billboard_id) return null
|
||||
const billboard = await AdvertisingModel.findById(ad.content_billboard_id).lean()
|
||||
if (!billboard) return null
|
||||
const creator = await AdvertisingProfileModel.findOne({ user: billboard.creator_id }).lean()
|
||||
return {
|
||||
_id: billboard._id,
|
||||
category: billboard.category,
|
||||
title: billboard.title,
|
||||
province: billboard.province,
|
||||
city: billboard.city,
|
||||
neighbourhood: billboard.neighbourhood,
|
||||
type: billboard.type,
|
||||
mostDiscountPercentage: billboard.mostDiscountPercentage,
|
||||
images: billboard.images,
|
||||
likedByUser: false,
|
||||
likesCount: billboard.likesCount,
|
||||
commentsCount: billboard.commentsCount,
|
||||
showDiscount: billboard.showDiscount,
|
||||
viewCount: billboard.viewCount,
|
||||
createdAt: billboard.createdAt,
|
||||
rate: { adTotalRatings: creator?.adTotalRatings, adAverageRating: creator?.adAverageRating },
|
||||
is_ad: true,
|
||||
ad_id: String(ad._id)
|
||||
}
|
||||
}
|
||||
|
||||
/** عینِ shape خروجیِ getDiscoverFeed (گریدِ اکسپلورِ فروشگاهها) — لیستینگ + shop پاپیولهشده */
|
||||
async function buildAdShopProductItem (ad) {
|
||||
if (!ad?.content_shop_product_id) return null
|
||||
const listing = await ShopProductListingModel.findById(ad.content_shop_product_id).lean()
|
||||
if (!listing || listing.status !== 'active') return null
|
||||
const shop = await ShopModel.findById(listing.shop).populate('owner', 'verify_badge').lean()
|
||||
if (!shop || shop.status !== 'active') return null
|
||||
return {
|
||||
...listing,
|
||||
shop,
|
||||
shop_count: 1,
|
||||
tileId: `ad-${ad._id}`,
|
||||
is_ad: true,
|
||||
ad_id: String(ad._id)
|
||||
}
|
||||
}
|
||||
|
||||
async function trackAdEvent (adId, type) {
|
||||
if (!adId || !['view', 'click'].includes(type)) return
|
||||
const field = type === 'view' ? 'views_count' : 'clicks_count'
|
||||
await AdModel.updateOne({ _id: adId }, { $inc: { [field]: 1 } })
|
||||
}
|
||||
|
||||
/** تبلیغِ نوع notification — بهجای تزریق توی فید/استوری، مستقیم پوش میفرسته به هر
|
||||
* کاربری که با هدفگذاریِ تبلیغ جور دربیاد؛ views_count رو با تعدادِ گیرندهها پر میکنه */
|
||||
async function deliverNotificationAd (ad) {
|
||||
if (ad.ad_type !== 'notification') return
|
||||
|
||||
const query = { _id: { $ne: ad.user_id } }
|
||||
if (ad.target_country_id != null) query['country.id'] = ad.target_country_id
|
||||
if (ad.target_province_id != null) query['province.id'] = ad.target_province_id
|
||||
if (ad.target_city_id != null) query['city.id'] = ad.target_city_id
|
||||
if (ad.target_gender && ad.target_gender !== 'any') query.gender = ad.target_gender
|
||||
if (ad.target_expertise) query.expertise = ad.target_expertise
|
||||
if (ad.target_sub_expertise?.length) query.sub_expertise = { $in: ad.target_sub_expertise }
|
||||
|
||||
const users = await UserModel.find(query).select('fcm_tokens fcm_web_tokens').lean()
|
||||
const tokens = users.flatMap((u) => [...(u.fcm_tokens || []), ...(u.fcm_web_tokens || [])])
|
||||
|
||||
if (tokens.length) {
|
||||
await sendPushToTokens(tokens, {
|
||||
title: ad.notification_title || 'تبلیغ',
|
||||
description: ad.notification_body || '',
|
||||
type: 'sponsored_notification',
|
||||
data: { ad_id: String(ad._id) }
|
||||
})
|
||||
}
|
||||
|
||||
await AdModel.updateOne({ _id: ad._id }, { $inc: { views_count: users.length } })
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
findActiveAd,
|
||||
buildAdPostItem,
|
||||
buildAdStoryGroup,
|
||||
buildAdLinkedContentItem,
|
||||
buildAdProjectItem,
|
||||
buildAdBillboardItem,
|
||||
buildAdShopProductItem,
|
||||
trackAdEvent,
|
||||
deliverNotificationAd
|
||||
}
|
||||
Reference in New Issue
Block a user