This commit is contained in:
payacom
2026-09-24 21:03:27 +03:30
parent 7f0ddfde70
commit 806471241e
43 changed files with 2166 additions and 144 deletions

View File

@@ -14,6 +14,9 @@ const modelSchema = new mongoose.Schema({
payment_authority: { type: String },
payment_ref_id: { type: String },
status: { type: String, enum: ["pending", "success", "failed", "settled"], default: "success" },
// فقط «قصدِ» استفاده از کیف پول رو نگه می‌داره تا لحظه‌ی تاییدِ verify — کسرِ واقعی
// اونجا انجام می‌شه (utils/walletSplit.js)
walletAmountUsed: { type: Number, default: 0 },
});

72
models/AdModel.js Normal file
View File

@@ -0,0 +1,72 @@
const mongoose = require('mongoose')
const timestamp = require('mongoose-timestamp')
const mongoosePaginate = require('mongoose-paginate-v2')
// تبلیغِ کاربری — نوعش تعیین می‌کنه محتواش کجا تزریق می‌شه: first_post (اول فید)،
// first_story (اول نوار استوری)، notification (پوش مستقیم به کاربرهای هدف)، project/
// product/shop/billboard (اول لیستِ مربوطه، از utils/adDelivery.js:findActiveAd با همون
// ad_type resolve می‌شه). هدف‌گذاری (لوکیشن/تخصص/جنسیت) روی همون فیلدهایی که
// User/Post/exploreAlgorithm.js از قبل استفاده می‌کنن سوار شده (province.id عددیه، نه
// ObjectId — utils/adPricing.js همین رو resolve می‌کنه)
const AD_TYPES = ['first_post', 'first_story', 'notification', 'project', 'product', 'shop', 'billboard']
const adSchema = new mongoose.Schema({
user_id: { type: mongoose.Schema.Types.ObjectId, ref: 'User', required: true },
ad_type: { type: String, enum: AD_TYPES, required: true },
// محتوا — بسته به ad_type فقط یکی از این پر می‌شه
content_post_id: { type: mongoose.Schema.Types.ObjectId, ref: 'Post', default: null },
content_project_id: { type: mongoose.Schema.Types.ObjectId, ref: 'Project', default: null },
content_shop_product_id: { type: mongoose.Schema.Types.ObjectId, ref: 'ShopProductListing', default: null },
content_shop_id: { type: mongoose.Schema.Types.ObjectId, ref: 'Shop', default: null },
content_billboard_id: { type: mongoose.Schema.Types.ObjectId, ref: 'Advertising', default: null },
// استوری‌های واقعی معمولاً ۲۴ساعته منقضی می‌شن (StoryModel: expires_at) و از فیدِ
// استوریِ معمولی حذف می‌شن، ولی یه کمپینِ تبلیغاتی می‌تونه چند روز فعال بمونه — پس
// مدیا/اورلی‌ها رو موقعِ ساختِ تبلیغ اینجا کپی/اسنپ‌شات می‌کنیم تا مستقل از عمرِ
// طبیعیِ خودِ استوری باشه (content_story_id فقط برای رفرنسِ منبع نگه داشته می‌شه)
content_story_id: { type: mongoose.Schema.Types.ObjectId, ref: 'Story', default: null },
content_media_path: { type: String, default: null },
content_media_type: { type: String, default: null },
content_overlays: { type: mongoose.Schema.Types.Mixed, default: null },
notification_title: { type: String, trim: true, default: null },
notification_body: { type: String, trim: true, default: null },
// هدف‌گذاری
target_country_id: { type: Number, default: null },
target_province_id: { type: Number, default: null },
target_city_id: { type: Number, default: null },
target_expertise: { type: String, trim: true, default: null },
target_sub_expertise: { type: [String], default: [] },
target_gender: { type: String, enum: ['female', 'male', 'any'], default: 'any' },
// مالی
price: { type: Number, required: true },
walletAmountUsed: { type: Number, default: 0 },
payment_authority: { type: String, default: null },
status: {
type: String,
enum: ['pending_payment', 'active', 'expired', 'failed'],
default: 'pending_payment'
},
started_at: { type: Date, default: null },
// عین بقیه‌ی کمپین‌های اپ، یه مدت‌زمانِ ثابت (AD_CAMPAIGN_DURATION_DAYS توی
// adsController.js) بعد از فعال‌شدن — بعدش دیگه توی تزریقِ فید/استوری/نوتیف لحاظ نمی‌شه
expires_at: { type: Date, default: null },
views_count: { type: Number, default: 0 },
clicks_count: { type: Number, default: 0 }
})
// تزریق (سرچِ یه تبلیغِ فعالِ منطبق روی فید/استوری) و لیستِ «تبلیغ‌های من» دو مصرفِ
// اصلیِ ایندکسن
adSchema.index({ status: 1, ad_type: 1, expires_at: 1 })
adSchema.index({ user_id: 1, createdAt: -1 })
adSchema.plugin(timestamp)
adSchema.plugin(mongoosePaginate)
const AdModel = mongoose.model('Ad', adSchema)
// روی خودِ مدل به‌عنوان استاتیک — تا adsController.js/adsPanelController.js بدونِ
// require جدا به لیستِ معتبرِ ad_type دسترسی داشته باشن (module.exports همون مدله، نه یه آبجکت)
AdModel.AD_TYPES = AD_TYPES
module.exports = AdModel

View File

@@ -0,0 +1,24 @@
const mongoose = require('mongoose')
const timestamp = require('mongoose-timestamp')
// قانونِ قیمت‌گذاریِ ادمین برای یه «سناریو»ی تبلیغ — هر فیلدِ null یعنی وایلدکارت
// (روی هر مقداری از اون بعد صدق می‌کنه)؛ utils/adPricing.js دقیق‌ترین قانونِ منطبق
// (بیشترین فیلدِ غیرِnull) رو انتخاب می‌کنه
const adPricingRuleSchema = new mongoose.Schema({
ad_type: {
type: String,
enum: ['first_post', 'first_story', 'notification', 'project', 'product', 'shop', 'billboard'],
required: true
},
province_id: { type: Number, default: null },
gender: { type: String, enum: ['female', 'male'], default: null },
expertise: { type: String, trim: true, default: null },
sub_expertise: { type: String, trim: true, default: null },
price: { type: Number, required: true }
})
adPricingRuleSchema.index({ ad_type: 1 })
adPricingRuleSchema.plugin(timestamp)
const AdPricingRuleModel = mongoose.model('AdPricingRule', adPricingRuleSchema)
module.exports = AdPricingRuleModel

View File

@@ -19,6 +19,11 @@ const adminBroadcastSchema = new mongoose.Schema({
type: Number,
default: 0
},
image: {
type: String,
required: false,
default: null
},
sentBy: {
type: mongoose.Schema.Types.ObjectId,
ref: 'Admin',

View File

@@ -18,6 +18,11 @@ const advertisingCommentSchema = new mongoose.Schema({
required: true,
trim: true
},
rate: {
type: Number,
required: false,
default: null
},
status: {
type: String,
enum: ['pending', 'temp_accept', 'accepted', 'rejected'],

View File

@@ -0,0 +1,24 @@
const mongoose = require('mongoose')
const timestamp = require('mongoose-timestamp')
const autoDirectRuleSchema = new mongoose.Schema({
user_id: {
type: mongoose.Schema.Types.ObjectId,
ref: 'User',
required: true
},
trigger_text: { type: String, required: true, trim: true },
direct_message: { type: String, required: true, trim: true },
reply_caption: { type: String, required: true, trim: true },
is_active: { type: Boolean, default: true },
// هر بار این قانون واقعاً اجرا بشه (دایرکت ارسال + کامنتِ پاسخ ثبت بشه) یکی زیاد می‌شه —
// برای نمایشِ «تعداد کامنت‌های ارسالی» توی لیستِ AutoDirectScreen.tsx
sent_count: { type: Number, default: 0 }
})
// autoDirectTrigger.js هر کامنتِ تازه رو در برابرِ همه‌ی قانون‌های فعالِ صاحبِ پست چک می‌کنه
autoDirectRuleSchema.index({ user_id: 1, is_active: 1 })
autoDirectRuleSchema.plugin(timestamp)
const AutoDirectRuleModel = mongoose.model('AutoDirectRule', autoDirectRuleSchema)
module.exports = AutoDirectRuleModel

View File

@@ -33,6 +33,13 @@ const notificationSchema = new mongoose.Schema({
ref: 'User',
required: false
},
// آواتار فرستنده (برای نوتیف‌های مربوط به یه کاربر خاص) یا بنرِ دستیِ ادمین
// (برای admin_broadcast) — هم توی لیستِ داخلِ اپ و هم توی خودِ پوش FCM استفاده می‌شه
image: {
type: String,
required: false,
default: null
},
// فقط برای اعلان‌های همگانی پنل — جدا از read (که یعنی صفحه اعلان‌ها باز شده)،
// seen یعنی پوش واقعاً به دستگاه رسیده و clicked یعنی کاربر روش ضربه زده
broadcast_id: {

View File

@@ -0,0 +1,28 @@
const mongoose = require('mongoose')
const timestamp = require('mongoose-timestamp')
// اشتراک‌گذاری لینک: کاربر مشخص می‌کنه کدوم محتوای خودش (پکیج/پروژه/بیلبورد/فروشگاه/
// کالا/رزرو) وقتی یکی دیگه توی ثبت پست تگش می‌کنه، توی لیست لینک‌های قابل‌انتخاب اون
// کاربر (بلاگر) نشون داده بشه — shareableLinksController.js/for-users همین رکوردها رو می‌خونه
const shareableLinkSchema = new mongoose.Schema({
user_id: {
type: mongoose.Schema.Types.ObjectId,
ref: 'User',
required: true
},
kind: {
type: String,
enum: ['pkg', 'project', 'billboard', 'shop', 'shopproduct', 'booking'],
required: true
},
ref_id: {
type: String,
required: true
}
})
shareableLinkSchema.index({ user_id: 1, kind: 1, ref_id: 1 }, { unique: true })
shareableLinkSchema.plugin(timestamp)
const ShareableLinkModel = mongoose.model('ShareableLink', shareableLinkSchema)
module.exports = ShareableLinkModel