Files
modstagram-back/models/UserModel.js
payacom 90c5f94b57 check
2026-09-03 16:06:02 +03:30

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