This commit is contained in:
payacom
2026-10-06 17:08:44 +03:30
parent fade8543e2
commit 367416d419
14 changed files with 6087 additions and 5937 deletions

View File

@@ -1,3 +1,4 @@
const UserModel = require('../models/UserModel')
const FREE_LIFETIME_CREATION_LIMIT = 100
const canUseFreeCreation = (user) => {
@@ -18,8 +19,20 @@ const consumeFreeCreation = async (user) => {
}
}
// updateOne + $inc (اتمیک، فقط همین فیلد) به‌جای user.save() — save() کل سند رو
// ری‌ولیدیت می‌کنه و برای کاربرهایی با مقدار نامعتبر در فیلدهای دیگه (مثلاً
// income_range) با ValidationError شکست می‌خوره
const res = await UserModel.updateOne(
{ _id: user._id, lifetime_free_creations: { $lt: FREE_LIFETIME_CREATION_LIMIT } },
{ $inc: { lifetime_free_creations: 1 } }
)
if (!res.modifiedCount) {
return {
ok: false,
message: `سقف ${FREE_LIFETIME_CREATION_LIMIT} ثبت رایگان (پروژه، بیلبورد و درخواست) به پایان رسیده است.`,
}
}
user.lifetime_free_creations = (user.lifetime_free_creations || 0) + 1
await user.save()
return { ok: true }
}

View File

@@ -57,3 +57,42 @@ async function optimizeImageFile(filePath, { maxDimension = DEFAULT_MAX_DIMENSIO
}
module.exports = { optimizeImageFile, DEFAULT_MAX_DIMENSION, DEFAULT_JPEG_QUALITY }
const VARIANT_WIDTHS = [480, 800]
/**
* نسخه‌های کوچک‌تر (WebP، با حفظ نسبت تصویر) کنار فایل اصلی می‌سازد و یک placeholder
* خیلی کم‌حجم هم می‌سازد. فایل اصلی دست‌نخورده می‌ماند و نسخه‌ای که از اصل بزرگ‌تر
* نباشد ساخته نمی‌شود.
*/
async function generateImageVariants(filePath, { widths = VARIANT_WIDTHS } = {}) {
if (!filePath) return
try {
if (!(await fs.pathExists(filePath))) return
const metadata = await sharp(filePath, { failOn: 'none' }).metadata()
if (!metadata.width || !metadata.height) return
const dir = path.dirname(filePath)
const base = path.basename(filePath, path.extname(filePath))
const source = sharp(filePath, { failOn: 'none' }).rotate()
for (const width of widths) {
if (width >= metadata.width) continue
await source
.clone()
.resize({ width, fit: 'inside', withoutEnlargement: true })
.webp({ quality: 80 })
.toFile(path.join(dir, `${base}.w${width}.webp`))
}
await source
.clone()
.resize({ width: 24, fit: 'inside', withoutEnlargement: true })
.webp({ quality: 40 })
.toFile(path.join(dir, `${base}.lqip.webp`))
} catch (error) {
console.error('[imageOptimize] variants failed:', error.message)
}
}
module.exports.generateImageVariants = generateImageVariants

View File

@@ -0,0 +1,18 @@
const ZARINPAL_START_PAY = 'https://www.zarinpal.com/pg/StartPay'
function getPaymentPageBase () {
return (process.env.APP_SITE_CallBack || process.env.APP_URL || '').replace(/\/$/, '')
}
// زرین‌پال صفحه‌ی پرداخت رو فقط وقتی باز می‌کنه که Referer درخواست یه دامنه‌ی مجاز باشه؛
// لینکِ مستقیمِ StartPay از مرورگرِ خارجی (مثلاً اندروید) Referer نداره و رد می‌شه. این
// آدرس روی دامنه‌ی خودمون یه صفحه‌ی redirect می‌زنه تا Referer درست ارسال بشه
function buildPaymentPageUrl (authority) {
return `${getPaymentPageBase()}/pay/${encodeURIComponent(String(authority))}`
}
function buildZarinpalStartPayUrl (authority) {
return `${ZARINPAL_START_PAY}/${encodeURIComponent(String(authority))}`
}
module.exports = { buildPaymentPageUrl, buildZarinpalStartPayUrl }