full
This commit is contained in:
146
utils/partnerMatch.js
Normal file
146
utils/partnerMatch.js
Normal file
@@ -0,0 +1,146 @@
|
||||
const PartnerSearchAlertModel = require('../models/PartnerSearchAlertModel')
|
||||
const NotificationModel = require('../models/NotificationModel')
|
||||
const { sendPushToTokens } = require('./pushNotification')
|
||||
|
||||
let ioInstance = null
|
||||
|
||||
/** یکبار از index.js صدا زده میشه — مثل الگوی notifyFollowers.js */
|
||||
function setIo (io) {
|
||||
ioInstance = io
|
||||
}
|
||||
|
||||
function ageFromBirthday (birthday) {
|
||||
if (!birthday) return null
|
||||
const diffMs = Date.now() - new Date(birthday).getTime()
|
||||
return Math.floor(diffMs / (365.25 * 24 * 60 * 60 * 1000))
|
||||
}
|
||||
|
||||
function inRange (value, min, max) {
|
||||
if (value === null || value === undefined || Number.isNaN(value)) return false
|
||||
if (min !== undefined && min !== null && value < min) return false
|
||||
if (max !== undefined && max !== null && value > max) return false
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* Compares one user document against one saved alert's criteria. Runs as a
|
||||
* plain JS predicate (not a Mongo query) since it always checks a single
|
||||
* already-loaded document, not a collection.
|
||||
*/
|
||||
function matchesCriteria (user, criteria = {}) {
|
||||
if (criteria.gender && user.gender !== criteria.gender) return false
|
||||
|
||||
if (criteria.heightMin !== undefined || criteria.heightMax !== undefined) {
|
||||
if (!inRange(Number(user.height), criteria.heightMin, criteria.heightMax)) return false
|
||||
}
|
||||
if (criteria.weightMin !== undefined || criteria.weightMax !== undefined) {
|
||||
if (!inRange(Number(user.weight), criteria.weightMin, criteria.weightMax)) return false
|
||||
}
|
||||
if (criteria.sizeMin !== undefined || criteria.sizeMax !== undefined) {
|
||||
if (!inRange(Number(user.size), criteria.sizeMin, criteria.sizeMax)) return false
|
||||
}
|
||||
if (criteria.ageMin !== undefined || criteria.ageMax !== undefined) {
|
||||
if (!inRange(ageFromBirthday(user.birthday), criteria.ageMin, criteria.ageMax)) return false
|
||||
}
|
||||
|
||||
if (criteria.hair_color && user.hair_color !== criteria.hair_color) return false
|
||||
if (criteria.eye_color && user.eye_color !== criteria.eye_color) return false
|
||||
if (criteria.marital_status && user.marital_status !== criteria.marital_status) return false
|
||||
if (criteria.residence_type && user.residence_type !== criteria.residence_type) return false
|
||||
if (criteria.vehicle_type && user.vehicle_type !== criteria.vehicle_type) return false
|
||||
if (criteria.personal_style && user.personal_style !== criteria.personal_style) return false
|
||||
if (criteria.income_range && user.income_range !== criteria.income_range) return false
|
||||
|
||||
if (criteria.job) {
|
||||
if (!user.job || !user.job.toLowerCase().includes(criteria.job.toLowerCase())) return false
|
||||
}
|
||||
|
||||
if (criteria.country_id && user.country?.id !== criteria.country_id) return false
|
||||
if (criteria.province_id && user.province?.id !== criteria.province_id) return false
|
||||
if (criteria.city_id && user.city?.id !== criteria.city_id) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
/** Only re-check when a field a saved search could plausibly filter on has
|
||||
* actually changed — avoids running on unrelated saves like `last_online`. */
|
||||
const MATCH_RELEVANT_FIELDS = [
|
||||
'gender', 'height', 'weight', 'size', 'birthday', 'hair_color', 'eye_color',
|
||||
'marital_status', 'residence_type', 'residence_area', 'vehicle_type',
|
||||
'vehicle_details', 'personal_style', 'job', 'income_range', 'moral_traits',
|
||||
'country', 'province', 'city', 'is_Register'
|
||||
]
|
||||
|
||||
function shouldCheck (user) {
|
||||
return MATCH_RELEVANT_FIELDS.some((field) => user.isModified(field))
|
||||
}
|
||||
|
||||
/** Finds every active alert this user now matches (that hasn't already been
|
||||
* notified about this exact user) and notifies each alert owner once.
|
||||
* Caller is responsible for the shouldCheck() gate — isModified() is only
|
||||
* meaningful in pre('save'), so it can't be re-checked here in a post('save')
|
||||
* context (Mongoose clears the modified-paths tracking before post hooks run). */
|
||||
async function checkPartnerAlerts (user) {
|
||||
try {
|
||||
const alerts = await PartnerSearchAlertModel.find({
|
||||
active: true,
|
||||
user_id: { $ne: user._id },
|
||||
notified_user_ids: { $ne: user._id }
|
||||
})
|
||||
if (alerts.length === 0) return
|
||||
|
||||
const matchedAlerts = alerts.filter((alert) => matchesCriteria(user, alert.criteria))
|
||||
if (matchedAlerts.length === 0) return
|
||||
|
||||
const title = 'همکار پیشنهادی جدید'
|
||||
const description = 'کاربری با مشخصات مورد نظر شما به مجموعه مدستاگرام پیوست'
|
||||
|
||||
const notifications = matchedAlerts.map((alert) => ({
|
||||
user_id: alert.user_id,
|
||||
type: 'partner_match',
|
||||
title,
|
||||
description,
|
||||
project_post_id: user._id,
|
||||
actor_id: user._id
|
||||
}))
|
||||
// create() (not insertMany) — insertMany skips document middleware, so the
|
||||
// mongoose-timestamp plugin never runs and createdAt/updatedAt are left
|
||||
// unset, which sorts these notifications to the very last page of the list
|
||||
await NotificationModel.create(notifications)
|
||||
|
||||
await Promise.all(
|
||||
matchedAlerts.map((alert) =>
|
||||
PartnerSearchAlertModel.updateOne(
|
||||
{ _id: alert._id },
|
||||
{ $addToSet: { notified_user_ids: user._id } }
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
if (ioInstance) {
|
||||
matchedAlerts.forEach((alert) => {
|
||||
try {
|
||||
ioInstance.to(`user:${String(alert.user_id)}`).emit('notification', { type: 'partner_match', title, description })
|
||||
} catch (_) {}
|
||||
})
|
||||
}
|
||||
|
||||
const UserModel = require('../models/UserModel')
|
||||
const alertOwners = await UserModel.find({ _id: { $in: matchedAlerts.map((a) => a.user_id) } })
|
||||
.select('fcm_tokens fcm_web_tokens')
|
||||
.lean()
|
||||
const tokens = alertOwners.flatMap((u) => [...(u.fcm_tokens || []), ...(u.fcm_web_tokens || [])])
|
||||
if (tokens.length > 0) {
|
||||
await sendPushToTokens(tokens, {
|
||||
title,
|
||||
description,
|
||||
type: 'partner_match',
|
||||
data: { project_post_id: String(user._id) }
|
||||
}).catch(() => {})
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[checkPartnerAlerts] خطا:', error.message)
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { matchesCriteria, checkPartnerAlerts, setIo, ageFromBirthday, shouldCheck }
|
||||
Reference in New Issue
Block a user