684 lines
14 KiB
JavaScript
684 lines
14 KiB
JavaScript
const mongoose = require('mongoose')
|
|
const mongoosePaginate = require('mongoose-paginate-v2')
|
|
const timestamp = require('mongoose-timestamp')
|
|
const userSchema = new mongoose.Schema({
|
|
mobile: {
|
|
type: String,
|
|
required: false,
|
|
trim: true,
|
|
unique: true,
|
|
sparse: true
|
|
},
|
|
otp: {
|
|
type: String,
|
|
required: false,
|
|
trim: true,
|
|
minLength: 6,
|
|
maxLength: 6,
|
|
default: null
|
|
},
|
|
otpSentAt: {
|
|
type: Date,
|
|
required: false,
|
|
default: null
|
|
},
|
|
email: {
|
|
type: String,
|
|
required: false,
|
|
trim: true,
|
|
lowercase: true,
|
|
unique: true,
|
|
sparse: true
|
|
},
|
|
google_id: {
|
|
type: String,
|
|
required: false,
|
|
trim: true,
|
|
unique: true,
|
|
sparse: true
|
|
},
|
|
auth_provider: {
|
|
type: String,
|
|
required: false,
|
|
enum: ['mobile', 'google', 'instagram'],
|
|
default: 'mobile'
|
|
},
|
|
instagram_id: {
|
|
type: String,
|
|
required: false,
|
|
trim: true,
|
|
unique: true,
|
|
sparse: true
|
|
},
|
|
instagram_username: {
|
|
type: String,
|
|
required: false,
|
|
trim: true,
|
|
default: null
|
|
},
|
|
instagram_account_type: {
|
|
type: String,
|
|
required: false,
|
|
enum: ['BUSINESS', 'MEDIA_CREATOR', null],
|
|
default: null
|
|
},
|
|
instagram_access_token_enc: {
|
|
type: String,
|
|
required: false,
|
|
default: null
|
|
},
|
|
instagram_token_expires_at: {
|
|
type: Date,
|
|
required: false,
|
|
default: null
|
|
},
|
|
instagram_connected_at: {
|
|
type: Date,
|
|
required: false,
|
|
default: null
|
|
},
|
|
instagram_last_synced_at: {
|
|
type: Date,
|
|
required: false,
|
|
default: null
|
|
},
|
|
instagram_sync_prefs: {
|
|
bio: { type: Boolean, default: false },
|
|
posts: { type: Boolean, default: false },
|
|
reels: { type: Boolean, default: false },
|
|
profile_picture: { type: Boolean, default: false },
|
|
auto_update: { type: Boolean, default: false }
|
|
},
|
|
email_verified: {
|
|
type: Boolean,
|
|
required: false,
|
|
default: false
|
|
},
|
|
totp_secret: {
|
|
type: String,
|
|
required: false,
|
|
default: null
|
|
},
|
|
totp_pending_secret: {
|
|
type: String,
|
|
required: false,
|
|
default: null
|
|
},
|
|
totp_enabled: {
|
|
type: Boolean,
|
|
required: false,
|
|
default: false
|
|
},
|
|
user_name: {
|
|
type: String,
|
|
required: false,
|
|
trim: true,
|
|
minLength: 1,
|
|
maxLength: 255,
|
|
default: null
|
|
},
|
|
password: {
|
|
type: String,
|
|
required: false,
|
|
trim: true,
|
|
minLength: 1,
|
|
maxLength: 255,
|
|
default: null
|
|
},
|
|
first_name: {
|
|
type: String,
|
|
required: false,
|
|
trim: true,
|
|
minLength: 1,
|
|
maxLength: 255,
|
|
default: null
|
|
},
|
|
last_name: {
|
|
type: String,
|
|
required: false,
|
|
trim: true,
|
|
minLength: 1,
|
|
maxLength: 255,
|
|
default: null
|
|
},
|
|
user_type: {
|
|
type: String,
|
|
required: false,
|
|
trim: true,
|
|
minLength: 1,
|
|
maxLength: 255,
|
|
default: null
|
|
},
|
|
is_verified: {
|
|
type: String,
|
|
required: false,
|
|
trim: true,
|
|
minLength: 1,
|
|
maxLength: 255,
|
|
default: 'none'
|
|
},
|
|
license_verified: {
|
|
type: Boolean,
|
|
required: false,
|
|
default: false
|
|
},
|
|
gold_badge: {
|
|
type: Boolean,
|
|
required: false,
|
|
default: false
|
|
},
|
|
verify_badge: {
|
|
type: String,
|
|
enum: ['none', 'gray', 'blue', 'green', 'gold'],
|
|
default: 'none'
|
|
},
|
|
profile_image: {
|
|
type: String,
|
|
required: false,
|
|
trim: true,
|
|
default: null
|
|
},
|
|
national_card_image: {
|
|
type: String,
|
|
required: false,
|
|
trim: true,
|
|
default: null
|
|
},
|
|
expertise: {
|
|
type: String,
|
|
required: false,
|
|
trim: true,
|
|
minLength: 1,
|
|
maxLength: 255,
|
|
default: null
|
|
},
|
|
sub_expertise: {
|
|
type: [String], // تغییر نوع به آرایه از استرینگ
|
|
required: false,
|
|
trim: true,
|
|
minLength: 1,
|
|
maxLength: 255,
|
|
default: null
|
|
},
|
|
display_sub_expertise: {
|
|
type: String,
|
|
required: false,
|
|
trim: true,
|
|
minLength: 1,
|
|
maxLength: 255,
|
|
default: null
|
|
},
|
|
gender: {
|
|
type: String,
|
|
required: false,
|
|
trim: true,
|
|
minLength: 1,
|
|
maxLength: 255,
|
|
default: null
|
|
},
|
|
height: {
|
|
type: String,
|
|
required: false,
|
|
trim: true,
|
|
minLength: 1,
|
|
maxLength: 255,
|
|
default: null
|
|
},
|
|
weight: {
|
|
type: String,
|
|
required: false,
|
|
trim: true,
|
|
minLength: 1,
|
|
maxLength: 255,
|
|
default: null
|
|
},
|
|
size: {
|
|
type: String,
|
|
required: false,
|
|
trim: true,
|
|
minLength: 1,
|
|
maxLength: 255,
|
|
default: null
|
|
},
|
|
eye_color: {
|
|
type: String,
|
|
required: false,
|
|
trim: true,
|
|
minLength: 1,
|
|
maxLength: 255,
|
|
default: null
|
|
},
|
|
hair_color: {
|
|
type: String,
|
|
required: false,
|
|
trim: true,
|
|
minLength: 1,
|
|
maxLength: 255,
|
|
default: null
|
|
},
|
|
marital_status: {
|
|
type: String,
|
|
enum: ['single', 'divorced'],
|
|
required: false,
|
|
default: null
|
|
},
|
|
residence_type: {
|
|
type: String,
|
|
enum: ['owner', 'tenant'],
|
|
required: false,
|
|
default: null
|
|
},
|
|
residence_area: {
|
|
type: String,
|
|
required: false,
|
|
trim: true,
|
|
minLength: 1,
|
|
maxLength: 255,
|
|
default: null
|
|
},
|
|
vehicle_type: {
|
|
type: String,
|
|
enum: ['none', 'car', 'motorcycle'],
|
|
required: false,
|
|
default: null
|
|
},
|
|
vehicle_details: {
|
|
type: String,
|
|
required: false,
|
|
trim: true,
|
|
minLength: 1,
|
|
maxLength: 255,
|
|
default: null
|
|
},
|
|
personal_style: {
|
|
type: String,
|
|
enum: ['formal', 'sporty'],
|
|
required: false,
|
|
default: null
|
|
},
|
|
job: {
|
|
type: String,
|
|
required: false,
|
|
trim: true,
|
|
minLength: 1,
|
|
maxLength: 255,
|
|
default: null
|
|
},
|
|
income_range: {
|
|
type: String,
|
|
enum: ['under_5m', '5m_10m', '10m_20m', 'over_20m'],
|
|
required: false,
|
|
default: null
|
|
},
|
|
moral_traits: {
|
|
type: String,
|
|
required: false,
|
|
trim: true,
|
|
minLength: 1,
|
|
maxLength: 1024,
|
|
default: null
|
|
},
|
|
personal_details_public: {
|
|
type: Boolean,
|
|
required: false,
|
|
default: false
|
|
},
|
|
cooperation_type: {
|
|
type: String,
|
|
required: false,
|
|
trim: true,
|
|
minLength: 1,
|
|
maxLength: 255,
|
|
default: null
|
|
},
|
|
cooperation_abroad: {
|
|
type: Boolean,
|
|
required: false,
|
|
default: null
|
|
},
|
|
conversation_projects: {
|
|
type: Boolean,
|
|
required: false,
|
|
default: null
|
|
},
|
|
bio: {
|
|
type: String,
|
|
required: false,
|
|
minLength: 1,
|
|
maxLength: 1024,
|
|
default: null
|
|
},
|
|
national_code: {
|
|
type: String,
|
|
required: false,
|
|
trim: true,
|
|
minLength: 1,
|
|
maxLength: 255,
|
|
default: null
|
|
},
|
|
birthday: {
|
|
type: Date,
|
|
required: false,
|
|
default: null
|
|
},
|
|
is_Register: {
|
|
type: String,
|
|
|
|
default: false
|
|
},
|
|
shaba: {
|
|
type: String,
|
|
required: false,
|
|
trim: true,
|
|
minLength: 1,
|
|
maxLength: 255,
|
|
default: null
|
|
},
|
|
address: {
|
|
type: String,
|
|
trim: true,
|
|
minLength: 1,
|
|
maxLength: 512,
|
|
default: null
|
|
},
|
|
country: {
|
|
type: Object,
|
|
default: null
|
|
},
|
|
province: {
|
|
type: Object,
|
|
default: null
|
|
},
|
|
city: {
|
|
type: Object,
|
|
default: null
|
|
},
|
|
lat: {
|
|
type: String,
|
|
trim: true,
|
|
minLength: 1,
|
|
maxLength: 512,
|
|
default: null
|
|
},
|
|
lng: {
|
|
type: String,
|
|
trim: true,
|
|
minLength: 1,
|
|
maxLength: 512,
|
|
default: null
|
|
},
|
|
show_location: {
|
|
type: Boolean,
|
|
required: false,
|
|
default: null
|
|
},
|
|
// وقتی «ریست الگوریتم» زده میشه ست میشه — buildUserExploreSignals لایک/کامنت/آفر
|
|
// قبل از این تاریخ رو نادیده میگیره تا کاربر واقعاً مثل تازهوارد رفتار ببینه، نه
|
|
// فقط کش رتبهبندی پاک بشه درحالیکه سیگنالهای شخصیسازی قدیمی هنوز اثر دارن
|
|
algorithm_reset_at: {
|
|
type: Date,
|
|
default: null
|
|
},
|
|
rate: {
|
|
type: String,
|
|
trim: true,
|
|
maxLength: 512,
|
|
default: null
|
|
},
|
|
referral_code: {
|
|
type: String,
|
|
trim: true,
|
|
minLength: 1,
|
|
maxLength: 512,
|
|
default: null
|
|
},
|
|
shop_id: {
|
|
type: mongoose.Schema.Types.ObjectId,
|
|
ref: 'Shop',
|
|
default: null
|
|
},
|
|
last_online: {
|
|
type: Date, // ذخیره تاریخ و زمان آخرین بازدید
|
|
default: Date.now // تنظیم تاریخ به تاریخ فعلی
|
|
},
|
|
business_license: {
|
|
type: String,
|
|
required: false,
|
|
trim: true,
|
|
default: null
|
|
},
|
|
user_level: {
|
|
type: String,
|
|
trim: true,
|
|
minLength: 1,
|
|
maxLength: 512,
|
|
// پیشفرض قبلاً null بود — یعنی تا اولین کامنت امتیازدار روی کاربر (recalculateUserRating
|
|
// در utils/commentRating.js)، هیچ سطحی نداشت. الان از همون رشتهای که getUserLevel با
|
|
// صفر ستاره برمیگردونه استفاده میکنیم، تا هر کاربر از ابتدا سطح واقعی داشته باشه
|
|
default: 'تازه وارد'
|
|
},
|
|
user_score: {
|
|
type: String,
|
|
trim: true,
|
|
minLength: 1,
|
|
maxLength: 512,
|
|
default: null
|
|
},
|
|
last_post: {
|
|
type: Object,
|
|
default: null
|
|
},
|
|
// user_ratings: [{
|
|
// project_id: {
|
|
// type: mongoose.Schema.Types.ObjectId,
|
|
// ref: 'Project',
|
|
// required: true
|
|
// },
|
|
// creator_id: {
|
|
// type: mongoose.Schema.Types.ObjectId,
|
|
// ref: 'User',
|
|
// required: true
|
|
// },
|
|
// rating: {
|
|
// type: Number,
|
|
// required: true
|
|
// },
|
|
// comment: {
|
|
// type: String,
|
|
// trim: true
|
|
// }
|
|
// }],
|
|
daily_free_request: {
|
|
type: Number,
|
|
default: 1
|
|
},
|
|
last_free_request_date: {
|
|
type: Date,
|
|
default: null
|
|
},
|
|
blocked_by: [{
|
|
type: mongoose.Schema.Types.ObjectId,
|
|
ref: 'User'
|
|
}],
|
|
blocked_users: [{
|
|
type: mongoose.Schema.Types.ObjectId,
|
|
ref: 'User'
|
|
}],
|
|
block_status: {
|
|
type: Boolean,
|
|
required: false,
|
|
default: null
|
|
},
|
|
blocked_until: {
|
|
type: Date,
|
|
required: false,
|
|
default: null
|
|
},
|
|
block_permanent: {
|
|
type: Boolean,
|
|
required: false,
|
|
default: false
|
|
},
|
|
lifetime_free_creations: {
|
|
type: Number,
|
|
default: 0
|
|
},
|
|
advertising_profiles: [{
|
|
type: mongoose.Schema.Types.ObjectId,
|
|
ref: 'AdvertisingProfile'
|
|
}],
|
|
services: [{
|
|
title: {
|
|
type: String,
|
|
required: true,
|
|
trim: true
|
|
},
|
|
originalPrice: {
|
|
type: String,
|
|
required: true
|
|
},
|
|
discountPrice: {
|
|
type: String,
|
|
required: false
|
|
},
|
|
discountPercentage: {
|
|
type: Number,
|
|
required: false
|
|
},
|
|
status: {
|
|
type: String,
|
|
default: "UnderReview",
|
|
required: false
|
|
},
|
|
image: {
|
|
type: String,
|
|
required: false,
|
|
trim: true
|
|
}
|
|
}],
|
|
monthly_free_offer: {
|
|
type: Number,
|
|
default: 1
|
|
},
|
|
last_free_offer_date: {
|
|
type: Date,
|
|
default: null
|
|
},
|
|
account_status: {
|
|
type: String,
|
|
enum: ['active', 'deactivated'],
|
|
default: 'active',
|
|
index: true
|
|
},
|
|
deactivated_at: {
|
|
type: Date,
|
|
default: null
|
|
},
|
|
reactivation_locked_until: {
|
|
type: Date,
|
|
default: null
|
|
},
|
|
scheduled_purge_at: {
|
|
type: Date,
|
|
default: null
|
|
},
|
|
allow_save_posts: {
|
|
type: Boolean,
|
|
default: true
|
|
},
|
|
ghost_mode: {
|
|
type: Boolean,
|
|
default: false
|
|
},
|
|
post_location_enabled: {
|
|
type: Boolean,
|
|
default: false
|
|
},
|
|
is_private: {
|
|
type: Boolean,
|
|
default: false
|
|
},
|
|
// تنظیمات کیف پول — امکان دریافت هدیه (پروفایل + چت) و اینکه آیا درخواست همکاری
|
|
// (offer) به این کاربر رایگانه یا طبق قیمت معمول پرداختی میشه
|
|
gift_enabled: {
|
|
type: Boolean,
|
|
default: true
|
|
},
|
|
offer_payment_enabled: {
|
|
type: Boolean,
|
|
default: true
|
|
},
|
|
preferred_language: {
|
|
type: String,
|
|
enum: require('../utils/appLanguages').APP_LANGUAGE_CODES,
|
|
default: 'fa'
|
|
},
|
|
e2ee_public_key: {
|
|
type: String,
|
|
required: false,
|
|
default: null
|
|
},
|
|
pinned_post_ids: {
|
|
type: [{
|
|
type: mongoose.Schema.Types.ObjectId,
|
|
ref: 'Post'
|
|
}],
|
|
default: []
|
|
},
|
|
// توکنهای FCM دستگاههای کاربر — آرایه چون یک کاربر میتونه چند دستگاه اندروید داشته باشه
|
|
fcm_tokens: {
|
|
type: [String],
|
|
default: []
|
|
},
|
|
// توکنهای FCM Web Push (مرورگر) — جدا از fcm_tokens چون توکنهای وب و اندروید فرمت/طول عمر متفاوتی دارن
|
|
fcm_web_tokens: {
|
|
type: [String],
|
|
default: []
|
|
},
|
|
// contactInfo: {
|
|
// phone: String,
|
|
// mobile: String,
|
|
// telegramLink: String,
|
|
// whatsappNumber: String,
|
|
// instagramLink: String,
|
|
// saveInfoForNextAds: { type: Boolean, default: false }
|
|
// },
|
|
// adTotalRatings: {
|
|
// type: Number,
|
|
// default: 0
|
|
// },
|
|
// adRatingsCount: {
|
|
// type: Number,
|
|
// default: 0
|
|
// },
|
|
// adAverageRating: {
|
|
// type: Number,
|
|
// default: 0
|
|
// }
|
|
})
|
|
|
|
userSchema.plugin(timestamp)
|
|
userSchema.plugin(mongoosePaginate)
|
|
|
|
// وقتی فیلدهای مرتبط با «یافتن همکار» تغییر میکنن (یا ثبتنام کامل میشه)، جستجوهای
|
|
// ذخیرهشدهی فعال رو چک میکنه — همهی کنترلرهای ویرایش پروفایل در نهایت save() صدا
|
|
// میزنن، پس اینجا تکنقطهایترین جاییه که بدون تکرار در هر کنترلر جدا کار میکنه.
|
|
//
|
|
// isModified() باید در pre('save') خونده بشه، نه post('save') — مونگوس وضعیت
|
|
// «تغییر کرده» رو بهعنوان بخشی از خود save() پاک میکنه، پس تا زمانی که به
|
|
// post('save') برسیم isModified() برای همهی فیلدها false برمیگرده. مقدار رو
|
|
// از طریق $locals بین pre و post منتقل میکنیم.
|
|
userSchema.pre('save', function (next) {
|
|
this.$locals.partnerAlertsRelevant = require('../utils/partnerMatch').shouldCheck(this)
|
|
next()
|
|
})
|
|
|
|
userSchema.post('save', function (doc) {
|
|
if (doc.$locals.partnerAlertsRelevant) {
|
|
require('../utils/partnerMatch').checkPartnerAlerts(doc).catch(() => {})
|
|
}
|
|
})
|
|
|
|
const UserModel = mongoose.model('User', userSchema)
|
|
module.exports = UserModel
|