This commit is contained in:
payacom
2026-09-13 21:32:36 +03:30
parent 90c5f94b57
commit c302aea177
49 changed files with 4063 additions and 920 deletions

View File

@@ -1583,25 +1583,27 @@ const getPostsWeb = async (req, res, next) => {
})
}
// پروفایل یک کاربر خاص به‌صورت ریلز: همه‌ی پست‌های pool از یک نویسنده‌ن، پس رتبه‌بندی
// شخصی‌سازی‌شده (paginatePersonalizedExplore) بی‌معنیه — diversifyByAuthor توش با
// maxPerPage=1 فقط اولین پست رو نگه می‌داره و بقیه‌ی پست‌های همون یک نویسنده رو با
// ترتیبِ امتیازدهی‌شده (نه زمانی) می‌ریزه ته لیست، یعنی «پست بعدی» دیگه پست
// بعدیِ واقعیِ همون کاربر نیست. اینجا فقط seed post میره اول، بقیه دقیقاً با همون
// ترتیب زمانی/پین‌شده‌ی posts (که بالاتر مرتب شده) می‌مونن
if (feedMode === 'reels' && _id && seedPostId && posts.length > 1) {
const { posts: paginatedPosts, totalItems, isColdStart } =
await paginatePersonalizedExplore({
posts,
usersById,
page,
limit,
userId: decodedToken?.id,
seedPostId,
reelsMode: true,
blendFresh: false,
cacheScope: `reels-profile:${_id}`
})
const seedIndex = posts.findIndex((p) => String(p._id) === String(seedPostId))
const orderedPosts = seedIndex > 0
? [posts[seedIndex], ...posts.slice(0, seedIndex), ...posts.slice(seedIndex + 1)]
: posts
const startIndex = (page - 1) * limit
const endIndex = page * limit
const totalItems = orderedPosts.length
const paginatedPosts = orderedPosts.slice(startIndex, endIndex)
return res.status(200).json({
posts: paginatedPosts,
totalPages: Math.ceil(totalItems / limit) || 1,
totalItems,
feedMeta: { isColdStart: !!isColdStart }
totalItems
})
}
@@ -1919,7 +1921,9 @@ const getSingleUserWeb = async (req, res, next) => {
eye_color: user.eye_color,
hair_color: user.hair_color,
// «مشخصات فردی» فقط وقتی خودِ کاربر با تیک «نمایش مشخصات من برای عموم» موافقت کرده
// برای بقیه نمایش داده می‌شه — برای خودِ صاحب پروفایل صرف‌نظر از این تیک همیشه برمی‌گرده
// برای بقیه نمایش داده می‌شه — برای خودِ صاحب پروفایل صرف‌نظر از این تیک همیشه برمی‌گرده.
// فیلدهای ترجیحاتِ پارتنریابی (relationship_goal تا smoking_status) بعداً به فرمِ
// ویرایشِ مشخصاتِ فردی اضافه شدن ولی اینجا جا مونده بودن — الان همراهِ بقیه برمی‌گردن
...(user.personal_details_public === true || isSelf
? {
marital_status: user.marital_status,
@@ -1930,7 +1934,15 @@ const getSingleUserWeb = async (req, res, next) => {
personal_style: user.personal_style,
job: user.job,
income_range: user.income_range,
moral_traits: user.moral_traits
relationship_goal: user.relationship_goal,
personality_traits: user.personality_traits,
lifestyle: user.lifestyle,
interests: user.interests,
values: user.values,
relationship_view: user.relationship_view,
geo_preference: user.geo_preference,
children_status: user.children_status,
smoking_status: user.smoking_status
}
: {}),
personal_details_public: user.personal_details_public === true,

View File

@@ -83,16 +83,18 @@ const getNearbyUsers = async (req, res, next) => {
)
const blockedMe = new Set((blockerIds || []).map((id) => String(id)))
// فقط کاربرانی که «نمایش لوکیشن به دیگران» رو فعال کردن و دسترسی موقعیت مکانی
// رو به اپ داده‌ن (یعنی lat/lng واقعی دارن) باید توی رادار/لیست/نقشه دیده بشن
const candidates = await UserModel.find({
_id: { $ne: userId },
show_location: true,
account_status: { $ne: 'deactivated' },
show_location: true,
lat: { $nin: [null, ''] },
lng: { $nin: [null, ''] },
user_name: { $ne: null }
})
.select(
'user_name first_name last_name profile_image is_verified verify_badge is_Register lat lng blocked_users'
'user_name first_name last_name profile_image is_verified verify_badge is_Register show_location lat lng blocked_users'
)
.lean()
@@ -120,6 +122,9 @@ const getNearbyUsers = async (req, res, next) => {
is_verified: user.is_verified,
verify_badge: user.verify_badge,
is_Register: user.is_Register,
// فقط با تیکِ «نمایش لوکیشن به دیگران» باید روی نقشه‌ی واقعی قرار بگیرن — بدونش
// فقط توی رادار نشون داده می‌شن (تصمیم روی فرانت‌اند، اینجا فقط پرچمش رو می‌فرستیم)
show_location: user.show_location === true,
distanceMeters: Math.round(distanceMeters)
})
}

View File

@@ -13,6 +13,17 @@ function escapeRegex (value) {
return String(value).replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
}
/** Multi-select query params (relationship_goal, personality_traits, lifestyle,
* interests, values, children_status) are sent as a single comma-separated
* string, e.g. `?personality_traits=kind,loyal` — this is the one convention
* the RN/web clients must use for every array-type dating-profile filter.
* Empty/blank items are dropped; returns [] for an empty/absent param. */
function parseCsvParam (value) {
if (value === undefined || value === null || value === '') return []
const raw = Array.isArray(value) ? value.join(',') : String(value)
return raw.split(',').map((v) => v.trim()).filter((v) => v.length > 0)
}
/** $expr clauses for an age range, computed from `birthday` — mirrors
* utils/partnerMatch.js's ageFromBirthday so live search and later alert
* matching agree on the same age. */
@@ -31,7 +42,10 @@ function ageRangeExprClauses (ageMin, ageMax) {
/** Turns raw query params into the persisted alert-criteria shape consumed
* by utils/partnerMatch.js's matchesCriteria — used both for the immediate
* search and when a search is saved as an alert, so both always agree. */
* search and when a search is saved as an alert, so both always agree.
* Multi-select dating-profile filters (relationship_goal, personality_traits,
* lifestyle, interests, values) use a single comma-separated query param,
* e.g. `?personality_traits=kind,loyal` — see parseCsvParam. */
async function resolveCriteriaFromQuery (query) {
const criteria = {}
const {
@@ -39,7 +53,10 @@ async function resolveCriteriaFromQuery (query) {
heightMin, heightMax, weightMin, weightMax, sizeMin, sizeMax,
hair_color, eye_color,
marital_status, residence_type, vehicle_type, personal_style,
income_range, job, country, province, city
income_range, job, country, province, city,
relationship_goal, seeking_gender, personality_traits,
lifestyle, interests, values, relationship_view, geo_preference,
children_status, smoking_status
} = query || {}
if (gender) criteria.gender = gender
@@ -60,6 +77,26 @@ async function resolveCriteriaFromQuery (query) {
if (income_range) criteria.income_range = income_range
if (job) criteria.job = String(job).trim()
// desired_age_range is a stored preference field only (echoed back via
// PARTNER_RESULT_FIELDS) — it is NOT translated into a search filter here;
// clients bucket it into ageMin/ageMax themselves before calling search.
const relationshipGoalList = parseCsvParam(relationship_goal)
if (relationshipGoalList.length) criteria.relationship_goal = relationshipGoalList
if (seeking_gender) criteria.seeking_gender = seeking_gender
const personalityTraitsList = parseCsvParam(personality_traits)
if (personalityTraitsList.length) criteria.personality_traits = personalityTraitsList
const lifestyleList = parseCsvParam(lifestyle)
if (lifestyleList.length) criteria.lifestyle = lifestyleList
const interestsList = parseCsvParam(interests)
if (interestsList.length) criteria.interests = interestsList
const valuesList = parseCsvParam(values)
if (valuesList.length) criteria.values = valuesList
if (relationship_view) criteria.relationship_view = relationship_view
if (geo_preference) criteria.geo_preference = geo_preference
const childrenStatusList = parseCsvParam(children_status)
if (childrenStatusList.length) criteria.children_status = childrenStatusList
if (smoking_status) criteria.smoking_status = smoking_status
if (country) {
const countryFind = await CountryModel.findOne({ id: country }, { _id: 0 })
if (countryFind) criteria.country_id = countryFind.id
@@ -99,6 +136,22 @@ function criteriaToMongoFilter (criteria) {
if (criteria.personal_style) filter.personal_style = criteria.personal_style
if (criteria.income_range) filter.income_range = criteria.income_range
if (criteria.job) filter.job = { $regex: escapeRegex(criteria.job), $options: 'i' }
// Multi-select dating-profile fields: candidate matches if their stored
// array overlaps ANY of the searcher's selected values ($in semantics).
if (criteria.relationship_goal?.length) filter.relationship_goal = { $in: criteria.relationship_goal }
if (criteria.personality_traits?.length) filter.personality_traits = { $in: criteria.personality_traits }
if (criteria.lifestyle?.length) filter.lifestyle = { $in: criteria.lifestyle }
if (criteria.interests?.length) filter.interests = { $in: criteria.interests }
if (criteria.values?.length) filter.values = { $in: criteria.values }
// Single-select dating-profile fields: exact match.
if (criteria.seeking_gender) filter.seeking_gender = criteria.seeking_gender
if (criteria.relationship_view) filter.relationship_view = criteria.relationship_view
if (criteria.geo_preference) filter.geo_preference = criteria.geo_preference
if (criteria.children_status?.length) filter.children_status = { $in: criteria.children_status }
if (criteria.smoking_status) filter.smoking_status = criteria.smoking_status
if (criteria.country_id) filter['country.id'] = criteria.country_id
if (criteria.province_id) filter['province.id'] = criteria.province_id
if (criteria.city_id) filter['city.id'] = criteria.city_id
@@ -115,7 +168,9 @@ function criteriaToMongoFilter (criteria) {
const PARTNER_RESULT_FIELDS =
'_id user_name first_name last_name profile_image is_verified verify_badge is_Register ' +
'gender height weight size eye_color hair_color marital_status residence_type residence_area ' +
'vehicle_type vehicle_details personal_style job income_range moral_traits birthday country province city blocked_users'
'vehicle_type vehicle_details personal_style job income_range birthday country province city blocked_users ' +
'relationship_goal seeking_gender desired_age_range personality_traits lifestyle interests values ' +
'relationship_view geo_preference children_status smoking_status'
/** GET /users/partner-search — filters as query params (see resolveCriteriaFromQuery).
* Personal-details fields are included in the response ONLY here, by design