مستندات

قطعی اینترنت بعد از 18 و 19 دی 1404 و قطعی سراسری اینترنت بعد از شروع جنگ سوم در اسفند 1404، باعث شد خیلی‌ها مثل من که دانش کمی توی برنامه‌نویسی داشتند، برای جبران قطع دسترسی‌ها و متناسب نیازهاشون دست به کار بشن و با گسترش دانش و آزمون و خطا، کارهایی انجام بدند که شاید شبیه اختراع چرخ از ابتدا به‌نظر می‌رسیده اما حداقل نیاز اونها رو مرتفع می‌کرده. من هم از این قافله جدا نبودم و برای این وبسایت که بر پایۀ وردپرس ساخته شده، پلاگین‌های اختصاصی نوشتم که گزارش اونها رو در زیر می‌تونید بخونید؛ شاید برای آیندگان کمکی باشه:

فهرست مطالب

پلاگین شبیه‌ساز کانال تلگرام

سالیان سال فعالیت در شبکه‌های اجتماعی و اطلاع‌رسانی درخصوص کلاس‌ها، دوره‌ها، اشتراک‌گذاری منابع آموزشی و سرگرمی به‌زبان آلمانی، با قطع دسترسی از بین رفت. لاجرم من هم مجبور به کوچ به پیام‌رسان‌های بومی شدم اما پراکندگی این پیام‌رسان‌ها و عدم دسترسی به همۀ مخاطبین در یک پیام‌رسان واحد، باعث شد به این فکر بیوفتم که پلاگینی توسعه بدم تا با کمک اون، مطالب مندرج در تمام این پیام‌رسان‌ها و تلگرام، تجمیع شده و در قالب یک کانال، در صفحۀ اصلی نمایش داده بشه؛ تا با این کار بتونم پیام‌ها و اطلاعیه‌ها رو یکجا به تمام مخاطبین این وبسایت برسونم. انجام این کار، با توجه به رویکرد سنگین نکردن لود صفحه‌ای که شورت کد پلاگین درش قرار میگیره، چالش جدیدی برای من بود. موارد مهمی که در توسعۀ این پلاگین انجام/استفاده شدند رو در زیر می‌بینید.

1. Class Constants for Meta Keys – Encapsulation & DRY Principle

استفاده از class constants به جای hard-coded strings در تمام متدها:

const POST_TYPE = ‘tg_channel_post’;
const META_TYPE = ‘_tg_media_type’;
const META_URL = ‘_tg_media_url’;
const META_HASHTAGS = ‘_tg_hashtags_str’;

Benefit: Single point of truth.

تغییر یک constant در کلاس در همه‌جا propagate می‌شود. خطای تایپی در runtime به صفر می‌رسد.

Design Pattern: Constant Interface (refined).

2. Conditional Meta Box with WordPress Media API Integration

در متد render_meta_box، منطق شرطی برای نمایش فیلدهای وابسته به نوع رسانه، همراه با lazy initialization of wp.media frames:

var mediaFrame;
$(‘#tg_upload_btn’).on(‘click’, function(e) {
if (mediaFrame) { mediaFrame.open(); return; }
mediaFrame = wp.media({ title: ‘انتخاب رسانه’, … });
mediaFrame.on(‘select’, function() {
var attachment = mediaFrame.state().get(‘selection’).first().toJSON();
$(‘input[name=”tg_media_url”]’).val(attachment.url);
});
mediaFrame.open();
});

Technical note: Singleton pattern for media frames prevents multiple DOM listeners and redundant object instantiation.

UX: Dynamic toggling of poster and file options based on radio selection via jQuery toggle().

3. Optimized Hashtag Storage for Meta Query

استخراج هشتگ‌ها با regex و ذخیره به فرمت ,tag1,tag2, برای استفاده از LIKE در فیلتر:

$hashtags = $this->extract_hashtags($post->post_content);
$hashtags_str = ‘,’ . implode(‘,’, $hashtags) . ‘,’;
update_post_meta($post_id, self::META_HASHTAGS, $hashtags_str);

و در کوئری AJAX:

‘meta_query’ => [[
‘key’ => self::META_HASHTAGS,
‘value’ => ‘,’ . $hashtag . ‘,’,
‘compare’ => ‘LIKE’,
]];

Performance:

استفاده از LIKE با pattern ای که از leading/trailing comma استفاده می‌کند، ایندکس meta_value را (در حد امکان) کارآمد نگه می‌دارد.

Edge cases:

متد extract_hashtags با preg_match_all('/#([\w\-]+)/u', $text, $matches) تمامی هشتگ‌های فارسی و انگلیسی را پوشش می‌دهد.

4. Infinite Scroll with Debounced Search & URL State Management

در get_infinite_js، یک finite state machine برای مدیریت offset, hashtag filter, search query و loading flags پیاده شده:

var loading = false, hasMore = true, offset = 0, currentHashtag = ”, currentSearch = ”;

function performSearch() {
var searchVal = $.trim($(‘.tg-search-input’).val());
if (searchVal === currentSearch) return;
currentSearch = searchVal;
currentHashtag = ”;
resetAndLoad(); // offset = 0, container.empty(), fetch from start
}

$(‘.tg-search-input’).on(‘input’, function() {
clearTimeout(searchTimeout);
searchTimeout = setTimeout(performSearch, 500); // debounce
});

Debouncing:

جلوگیری از ارسال درخواست‌های AJAX به ازای هر کاراکتر.

URL hash handling:

خواندن پارامتر tg_hashtag از $_GET و سپس ست کردن فیلتر مربوطه قبل از اولین load.

Smooth scroll:

پس از reset، با scrollToContainer کاربر را به بالای لیست هدایت می‌کند.

5. Persian Date Conversion – Graceful Fallback Pattern

داخل متد gregorian_to_jalali:

if (class_exists(‘IntlDateFormatter’)) {
$formatter = new IntlDateFormatter(
‘fa_IR@calendar=persian’,
IntlDateFormatter::NONE,
IntlDateFormatter::NONE,
‘Asia/Tehran’,
IntlDateFormatter::TRADITIONAL,
‘yyyy/MM/dd – HH:mm’
);
return $formatter->format($timestamp);
}
// Manual algorithm fallback

Robustness:

روی سرورهایی که intl extension ندارند، الگوریتم محاسبات دستی (با پشتیبانی از leap years) جایگزین می‌شود.

Timezone:

استفاده از Asia/Tehran برای نمایش ساعت محلی.

6. Intelligent Video Handling – Internal vs External URLs

$is_internal = (strpos($url, home_url()) !== false);
if ($is_internal) {
echo ‘<video controls preload=”metadata” poster=”…”>…’;
} else {
echo ‘<div class=”video-cover” onclick=”window.open(…)”>…’;
}

Internal:

از تگ <video> native با preload="metadata" برای کاهش بار اولیه.

External:

شبیه‌سازی thumbnail با پخش‌کننده overlay که کاربر را در یک tab جدید به منبع اصلی می‌برد.

تجربه کاربری یکپارچه بدون breaking UX.

7. Unique Hashtag Cloud – Single Optimized SQL Query

داخل get_all_unique_hashtags:

global $wpdb;
$query = $wpdb->get_col($wpdb->prepare(
“SELECT DISTINCT pm.meta_value
FROM {$wpdb->postmeta} pm
INNER JOIN {$wpdb->posts} p ON p.ID = pm.post_id
WHERE pm.meta_key = %s
AND p.post_type = %s
AND p.post_status = ‘publish'”,
$meta_key, $post_type
));

One query برای واکشی تمام meta_value های مربوطه. سپس explode و array_unique در PHP.

پرهیز از N+1 queries و استفاده از DISTINCT در سطح دیتابیس.

8. Inline Asset Injection – Zero External Files

تمام استایل‌ها و اسکریپت‌های فرانت‌اند با wp_add_inline_style و wp_add_inline_script تزریق می‌شوند، اما ابتدا وابستگی‌ها (jQuery) و localized variables تنظیم می‌گردند:

wp_register_script(‘tg-infinite-scroll’, ”, [‘jquery’], null, true);
wp_enqueue_script(‘tg-infinite-scroll’);
wp_localize_script(‘tg-infinite-scroll’, ‘tg_ajax’, [
‘ajax_url’ => admin_url(‘admin-ajax.php’),
‘nonce’ => wp_create_nonce(‘tg_load_more’),
]);
wp_add_inline_script(‘tg-infinite-scroll’, $this->get_infinite_js());

No external HTTP requests for plugin assets.

Dependency management:

به درستی به jQuery وابسته شده است.

Localization before inline:

تضمین می‌کند که آبجکت tg_ajax در scope اسکریپت inline موجود باشد.

9. Settings Page for Target Page Selection – Decoupling Configuration

 

register_setting(‘tg_channel_settings_group’, $this->option_name, ‘intval’);
add_settings_field(…, [$this, ‘page_selector_callback’]);
// در callback:
wp_dropdown_pages([‘name’ => $this->option_name, ‘selected’ => $selected]);

سپس داخل link_hashtags:

$page_id = get_option($this->option_name, 0);
$base_url = $page_id ? get_permalink($page_id) : home_url(‘/’);
$url = add_query_arg(‘tg_hashtag’, $m[1], $base_url);

Loose coupling:

پلاگین به هیچ صفحه‌ی خاصی hard-code نشده است. کاربر از طریق UI صفحه‌ی حاوی شورت‌کد را انتخاب می‌کند.

Backward compatibility:

اگر صفحه‌ای انتخاب نشود، به home_url() fallback می‌کند.

10. Single Post Override – Proper Conditional Hooks

public function single_post_content($content) {
if (is_singular(self::POST_TYPE) && in_the_loop() && is_main_query()) {
ob_start();
$this->render_single_post($post);
return ob_get_clean();
}
return $content;
}
add_filter(‘the_content’, [$this, ‘single_post_content’], 20);

Priority 20:

بعد از اکثر پلاگین‌های دیگر اجرا می‌شود اما همچنان قابلیت override را دارد.

in_the_loop() && is_main_query():

از تداخل با خروجی‌های سایدبار یا secondary loops جلوگیری می‌کند.

11. Security Hardening at Every Layer

Nonce verification در متاباکس: wp_verify_nonce($_POST['tg_channel_meta_box_nonce'], 'tg_channel_meta_box')

AJAX nonce checkcheck_ajax_referer('tg_load_more', 'nonce')

Capability checkcurrent_user_can('edit_post', $post_id) قبل از ذخیره متا.

Data sanitization:

sanitize_text_field برای ورودی‌های متنی.

esc_url_raw برای آدرس‌های فایل.

wp_kses_post برای محتوای caption.

esc_js و json_encode در inline script ها.

12. Activation Hook for Permalink Flush

register_activation_hook(__FILE__, function() {
$tg = new TG_Channel_Pro();
$tg->register_post_type();
flush_rewrite_rules();
});

بدون نیاز به مراجعه به Settings > Permalinks بعد از فعالسازی.

فقط یک بار اجرا می‌شود و از flush_rewrite_rules به درستی استفاده می‌کند.

پلاگین و دیتابیس دیکشنری

با قطع دسترسی به اینترنت، سایت‌هایی مثل گوگل ترنزلیت، wort.ir و هوش مصنوعی‌های مختلف هم برای زبان‌آموزان از دسترس خارج شدند، گرچه باوجود اتصال داخلی چند پلتفرم هوش مصنوعی، امکان استفادۀ فوق‌العاده محدود از یک سری امکانات وجود داشته و داره اما به دلیل استفادۀ این پلتفرم‌ها از api، جواب‌ها اغلب نادرست و یا گیج‌کننده بودند. بنابراین بلافاصله با قطع شدن اینترنت بعد از اعتراضات 18 دی 1404، تصمیم گرفتم دیتابیس جامعی از کلمات آلمانی و معانی فارسی اونها، بااضافۀ جملات نمونه، کلمات ترکیبی و همینطور کلمات هم‌خانواده تهیه کنم و در مدت قطعی جنگ، کارهای کدنویسی افزونه رو مطابق استانداردهایی که این پایین توضیح میدم انجام بدم:

1. ساختار دیتابیس با ایندکس‌گذاری FULLTEXT و کلیدهای جداگانه

CREATE TABLE $table_entries (
id bigint(20) NOT NULL AUTO_INCREMENT,
word_type varchar(50) NOT NULL,
word_fa varchar(190) NOT NULL,
word_de varchar(190) NOT NULL,
grammar_info text,
examples longtext,
compounds longtext,
PRIMARY KEY (id),
KEY word_fa (word_fa),
KEY word_de (word_de),
FULLTEXT KEY ft_fa_de (word_fa, word_de)
) $charset_collate;

توضیح: استفاده از FULLTEXT index روی دو ستون word_fa, word_de امکان جستجوی پیشرفته با Boolean Mode را فراهم می‌کند. همچنین ایندکس‌های معمولی روی هر کلمه به صورت جداگانه برای جستجوهای LIKE فالبک تعبیه شده است.

نکته تخصصی: پشتیبانی از Multilingual Full-Text با کاراکترست UTF8mb4 که برای زبان‌های راست‌به‌چپ و چپ‌به‌راست بهینه است.

2. جدول Aliases برای عادی‌سازی داده‌ها (Normalization)

CREATE TABLE $table_aliases (
id bigint(20) NOT NULL AUTO_INCREMENT,
entry_id bigint(20) NOT NULL,
alias_word varchar(190) NOT NULL,
lang varchar(10) NOT NULL,
alias_type varchar(20) DEFAULT ‘alias’,
PRIMARY KEY (id),
KEY entry_id (entry_id),
KEY alias_word (alias_word)
);

مزیت: ذخیره‌سازی یک‌به‌چند برای هر مدخل، شامل مترادف‌ها (alias) و ترکیب‌ها (compound). این ساختار از تکرار اطلاعات جلوگیری کرده و جستجو روی aliasها را بسیار سریع می‌کند.

کاربرد هوشمندانه: در زمان بررسی تداخل مدخل‌ها (Conflict Resolution)، با استفاده از توابع sdict_merge_aliases_from_entry و sdict_merge_compounds_from_entry قابلیت ادغام بدون از دست دادن داده فراهم شده است.

3. الگوریتم اولویت‌بندی ۶ سطحی (6‑Level Priority Ranking)

در تابع smart_dict_ajax_handler_v6 یک Ranking Pipeline طراحی شده که نتایج را بر اساس دقت تطابق دسته‌بندی می‌کند:

$priority1 = []; // مدخل اصلی دقیق (Exact Main Entry)
$priority2 = []; // ترکیب دقیق (Exact Compound)
$priority3 = []; // alias دقیق (Exact Alias)
$priority4 = []; // مدخل اصلی شامل کلمه به صورت جدا (Whole Word in Main)
$priority5 = []; // ترکیب شامل کلمه به صورت جدا (Whole Word in Compound)
$priority6 = []; // alias شامل کلمه به صورت جدا (Whole Word in Alias)

نحوه کار: هر مدخل ابتدا در سطح بالاتر بررسی می‌شود. اگر تطابق داشته باشد، continue اجرا شده و وارد مراحل پایین‌تر نمی‌شود. این کار ضمن جلوگیری از Duplicate Results، ترتیب نمایش منطقی (دقیق‌ترین نتایج در بالا) را تضمین می‌کند.

قابلیت Whole Word Detection: تابع sdict_contains_whole_word با استفاده از Regex Boundary و نرمال‌سازی فاصله و نیم‌فاصله، اطمینان می‌دهد که مثلاً جستجوی «بر» فقط کلمه «بر» را بیاورد نه «برگ» را.

4. رفع دو باگ مهم کراس‌اولویتی (Cross‑Priority & Internal Duplicates)

$priority1_keys = array();
foreach ( $priority1 as $item ) {
$key = $item[‘entry’]->word_de . ‘|’ . $item[‘entry’]->word_fa;
$priority1_keys[$key] = true;
}
// Filter priority2 (exact compound matches)
$filtered_priority2 = array();
foreach ( $priority2 as $item ) {
$compound_key = $item[‘compound_de’] . ‘|’ . $item[‘compound_fa’];
if ( ! isset( $priority1_keys[$compound_key] ) ) {
$filtered_priority2[] = $item;
}
}

دلیل باگ: احتمال داشت یک ترکیب (compound) دقیقاً با نام مدخل اصلی یکی باشد، و در دو اولویت مجزا تکرار شود.

راه‌حل: ساخت Set از کلیدهای مدخل‌های اولویت ۱ و حذف آیتم‌های اولویت ۲ و ۵ که کلید ترکیبی آن‌ها در این Set وجود دارد.

همچنین: حذف تکراری‌های داخلی در هر اولویت با استفاده از Grouping by compound key و انتخاب تصادفی از گروه (چون همه یکسان هستند).

5. نرمال‌سازی پیشرفته زبان‌های فارسی و آلمانی با پشتیبانی از نیم‌فاصله و اعراب

function sdict_normalize_persian_exact( $str ) {
// BUG #2 FIX: Convert half-space (U+200C) to normal space
$str = str_replace( “\x{200C}”, ‘ ‘, $str );
$str = str_replace( array(‘ً’,’ٌ’,’ٍ’,’َ’,’ُ’,’ِ’,’ّ’,’ٓ’,’ٔ’,’ٕ’), ”, $str );
$str = str_replace( ‘غ’, ‘ق’, $str );
$str = preg_replace( ‘/[أإآؤئء]/u’, ‘ع’, $str );
// حذف کاراکترهای کنترلی فاصله‌دار
$str = preg_replace( ‘/[\x{2000}-\x{200F}\x{202F}\x{205F}\x{3000}\x{180E}]+/u’, ”, $str );
return mb_strtolower( $str, ‘UTF-8’ );
}

  • نرمال‌سازی آلمانی: تبدیل ä,ö,ü,ß به ae,oe,ue,ss که استاندارد تطابق متون آلمانی است.

  • نکته ظریف: تبدیل نیم‌فاصله (U+200C) به فاصله معمولی قبل از هر پردازش دیگر، تا کلماتی مثل «می‌شود» با «می شود» یکی در نظر گرفته شوند.

6. Conflict Resolution در زمان Import JSON با سه گزینه هوشمند

if ( $user_choice === ‘skip’ ) {
sdict_merge_aliases_from_entry( intval( $conflict[‘existing_id’] ), $conflict[‘new_data’] );
sdict_merge_compounds_from_entry( intval( $conflict[‘existing_id’] ), $conflict[‘new_data’] );
$resolved_count++;
}

گزینه‌ها:

Replace: حذف مدخل قدیم و درج جدید.

Duplicate: ثبت تکراری (برای موارد خاص).

Skip (ادغام): فقط مترادف‌ها و ترکیب‌های جدید را به مدخل موجود اضافه می‌کند بدون تغییر کلمات اصلی.

پیاده‌سازی: توابع sdict_merge_aliases_from_entry و sdict_merge_compounds_from_entry با بررسی array_diff و درج رکوردهای جدید در جدول aliases و به‌روزرسانی فیلد compounds از تکرار جلوگیری می‌کنند.

7. قابلیت «نمایش بیشتر» (Show More Overlay) در فرانت‌اند

در خروجی shortcode و AJAX، محدودیت ۵ نتیجه اول به صورت پیش‌فرض اعمال می‌شود و مابقی با یک لایه شیشه‌ای (blur overlay) پوشانده می‌شوند:

if ($items.length > 5) {
$items.slice(5).hide();
var overlay = $(‘<div class=”smart-dict-more-overlay”><button class=”smart-dict-show-more-btn”>نمایش بیشتر</button></div>’);
$resultList.css(‘position’, ‘relative’).append(overlay);
overlay.find(‘button’).on(‘click’, function() {
$items.slice(5).show();
overlay.remove();
$resultList.css(‘position’, ”);
});
}

UX پیشرفته: کاهش بار ذهنی کاربر با نمایش تنها ۵ نتیجه ابتدایی، اما امکان مشاهده همه با یک کلیک بدون رفرش صفحه.

استفاده از backdrop-filter: blur(8px) برای افکت مدرن glassmorphism.

8. تشخیص خودکار زبان جستجو (Language Detection)

function sdict_detect_language( $str ) {
$persian_pattern = ‘/[\x{0600}-\x{06FF}\x{FB50}-\x{FDFF}\x{FE70}-\x{FEFF}]/u’;
preg_match_all( $persian_pattern, $str, $matches );
$persian_count = count( $matches[0] );
$total_chars = mb_strlen( $str, ‘UTF-8’ );
if ( $total_chars == 0 ) return ‘de’;
$ratio = $persian_count / $total_chars;
return ( $ratio > 0.3 ) ? ‘fa’ : ‘de’;
}

نحوه کار: اگر بیش از ۳۰٪ کاراکترهای عبارت در بازه یونیکد فارسی باشند، زبان جستجو 'fa' در نظر گرفته می‌شود، در غیر این صورت 'de'.

کاربرد: تعیین اینکه کدام ستون (word_fa یا word_de) به عنوان مبنای تطابق اصلی در اولویت‌بندی استفاده شود.

9. دکمه دانلود لیست کلمات آلمانی با Nonce امنیتی

add_action( ‘admin_init’, ‘smart_dict_download_german_words’ );
function smart_dict_download_german_words() {
if ( isset( $_GET[‘sdict_download_german’] ) && isset( $_GET[‘_wpnonce’] ) && wp_verify_nonce( $_GET[‘_wpnonce’], ‘sdict_download_nonce’ ) ) {
global $wpdb;
$all_words = $wpdb->get_col( “SELECT word_de FROM {$wpdb->prefix}sdict_entries ORDER BY word_de ASC” );
if ( $all_words ) {
header( ‘Content-Type: text/plain; charset=utf-8’ );
header( ‘Content-Disposition: attachment; filename=”german-words-list.txt”‘ );
echo implode( ‘ / ‘, $all_words );
exit;
}
}
}

امنیت: بررسی nonce قبل از هر خروجی.

کاربردی: خروجی یک فایل متنی با لیست تمام کلمات آلمانی (جدا شده با /) برای استفاده در فرایندهای دیگر.

10. رندر شرطی موبایل و دسکتاپ با CSS Media Query

در استایل‌های فرانت‌اند، دو ساختار متفاوت برای نمایش هدر هر آیتم طراحی شده:

@media (max-width: 600px) {
.dict-header-desktop { display: none; }
.dict-header-mobile { display: flex; flex-direction: column; }
}

و در HTML:

<div class=”dict-header-desktop”>
<!– ترتیب چینش برای صفحه‌عریض –>
</div>
<div class=”dict-header-mobile”>
<!– ترتیب چینش عمودی برای موبایل –>
</div>

پاسخگو (Responsive) بدون نیاز به JavaScript

حفظ خوانایی با تغییر اندازه فونت و چیدمان بر اساس فضای موجود.

پلاگین رزرو کلاس

تا قبل از قطعی اینترنت، برای هندل مسائل مربوط به رزرو کلاس، از افزونۀ bookingly استفاده میشد که با قطع اتصال، سرعت کار با این افزونه به شدت پایین اومد و خیلی از قابلیت های مدنظر غیرفعال شد. بنابراین ساخت سازوکاری جدید برای رزرو کلاس، مطابق شرایط و نیازهای خودم، در دستور کار قرار گرفت و این بزرگترین چرخی بود که از اول اختراع شد:

1. لایه انتزاعی (Abstraction Layer) برای BigBlueButton با قابلیت تنظیمات پیشرفته

private function bbb_api($method, $params = []) {
$bbb_url = get_option(‘gtbp_bbb_url’, self::BBB_DEFAULT_URL);
$secret = get_option(‘gtbp_bbb_secret’, self::BBB_DEFAULT_SECRET);
$query = http_build_query($params, ”, ‘&’);
$checksum = sha1($method . $query . $secret);
$url = rtrim($bbb_url, ‘/’) . ‘/’ . $method . ‘?’ . $query . ‘&checksum=’ . $checksum;
$response = wp_remote_get($url, [‘timeout’ => 30]);
// parse XML response…
}

امضای درخواست (Checksum) طبق استاندارد BBB API.

تنظیمات پویا مانند presentationURL، meetingLayout و lockSettingsDisablePublicChat از طریق متد create_bbb_room:

$params = [
‘name’ => $room_name,
‘meetingID’ => $meeting_id,
‘record’ => ‘true’,
‘autoStartRecording’ => ‘false’,
];
if (!empty($presentation_url)) $params[‘presentationURL’] = $presentation_url;
if ($disable_public_chat === ‘1’) $params[‘lockSettingsDisablePublicChat’] = ‘true’;

جداسازی لینک دانش‌آموز (VIEWER) و لینک معلم (MODERATOR) با نام واقعی کاربران، که در متد get_bbb_join_url ساخته می‌شود.

Polling خودکار ضبط‌ها توسط gtbp_check_bbb_recordings (کرون جاب هر ساعت) که پس از اتمام جلسه، لینک ضبط را از BBB دریافت و به کاربر ایمیل می‌کند.

2. سیستم بدهی/طلب (Debt/Credit) با ذخیره در User Meta و اعمال در سبد خرید

private function get_user_adjustment($user_id) {
$adjustment = get_user_meta($user_id, ‘_gtbp_user_adjustment’, true);
return is_numeric($adjustment) ? intval($adjustment) : 0;
}

در محاسبه سبد نهایی:

$payable_total = $after_discount + $user_adjustment;
if ($payable_total < 0) $payable_total = 0;
if ($payable_total == 0) {
// ثبت مستقیم بدون پرداخت و به‌روزرسانی طلب
$new_adjustment = $user_adjustment + $after_discount;
$this->set_user_adjustment($user->ID, $new_adjustment);
}

عدد مثبت = بدهی (به مبلغ کل اضافه می‌شود) ، عدد منفی = طلب (از کل کسر می‌شود).

مدیریت تسویه: وقتی مبلغ نهایی صفر می‌شود، رزرو مستقیماً ثبت می‌شود و مقدار طلب/بدهی به‌روز می‌گردد.

در پنل ادمین، فرم مجزایی برای تنظیم این مقدار برای هر کاربر وجود دارد.

3. تقویم هوشمند با وضعیت تعطیلات دو کشور (ایران و آلمان) و ظرفیت ساعات

public function ajax_get_calendar_status() {
// دریافت تعطیلات ثابت ایران (مثلاً ۱۳ فروردین) و آلمان (مثلاً ۳ اکتبر)
$ir_fixed_holidays = [ ’01-01’=>’عید نوروز’, … ];
$de_fixed_holidays = [ ’01-01’=>’Neujahr’, … ];
// بررسی تعطیلات دستی ادمین، فعال بودن روز، پر بودن تمام ساعات
$is_full = ($booked >= $total_slots);
$result[$date_str] = [
‘past’ => $is_past, ‘holiday’ => $is_holiday, ‘active’ => $is_active,
‘full’ => $is_full, ‘ir_holiday’ => $ir_h_name, ‘de_holiday’ => $de_h_name
];
}

در فرانت‌اند، هر روز تقویم آیکن پرچم ایران یا آلمان را در صورت تعطیل رسمی نشان می‌دهد (با onclick نمایش پیام توضیحی).

فیلتر هوشمند ساعات در متد ajax_get_slots:

$diff_hours = ($slot_ts – $now_ts) / 3600;
$is_too_soon = $diff_hours < 24;

ساعاتی که کمتر از ۲۴ ساعت مانده باشند غیرقابل انتخاب می‌شوند (too_soon=true).

4. اولویت‌بندی ساعات (Priority Slot Selection) در فرانت‌اند

در render_booking_frontend، یک منطق فیلتر کردن ساعات وجود دارد:

const l1 = [’13:30-14:30′, ’15:00-16:00′];
const l2 = [’12:00-13:00′];
const l3 = [’10:30-11:30′];
const l4 = [’09:00-10:00′];
const hasAvailable = (level) => slots.some(s => level.includes(s.time) && !s.booked && !s.too_soon);
let activeLevel = [];
if (hasAvailable(l1)) activeLevel = l1;
else if (hasAvailable(l2)) activeLevel = l2;
else if (hasAvailable(l3)) activeLevel = l3;
else if (hasAvailable(l4)) activeLevel = l4;

اولویت با ساعات عصر (13:30-14:3015:00-16:00). در صورت نبود ظرفیت، ساعات قبلی نمایش داده می‌شوند.

این کار باعث هدایت کاربر به زمان‌های بهینه برای استاد می‌شود (یک نوع Business Logic Optimization).

5. مدیریت پیشرفته پرداخت: درگاه آنلاین (ووکامرس) + کارت به کارت با تایمر ۱۰ دقیقه‌ای

if ($payment_method === ‘online’) {
// ایجاد سفارش ووکامرس با اقلام و تخفیف و بدهی/طلب
$order = wc_create_order();
$order->add_product($product, 1);
// افزودن آیتم هزینه اضافی برای تعدیل
$adjustment_fee = new WC_Order_Item_Fee();
$adjustment_fee->set_amount($user_adjustment);
$order->add_item($adjustment_fee);
$order->update_meta_data(‘_gtbp_pending_cart’, $cart);
$order->update_meta_data(‘_gtbp_temp_booking_ids’, $inserted_ids);
$order->save();
// ارسال به درگاه
$redirect_url = $order->get_checkout_payment_url();
} else { // کارت به کارت
// ذخیره رزرو با وضعیت ‘temp_card’ و انقضای ۱۰ دقیقه
$wpdb->insert(…, [‘status’ => ‘temp_card’]);
wp_send_json_success([‘type’ => ‘bank’, ‘expires’ => time() + 600]);
}

پاکسازی خودکار رزروهای منقضی توسط check_pending_card_payments (کرون جاب هر ساعت) و ارسال ایمیل لغو.

یادآوری ۱۸ ساعته برای کاربران خارج از ایران (send_payment_reminders) با لینک تایید یکبار مصرف و Nonce.

دکمه تأیید پرداخت در فرانت (ajax_confirm_card_payment) که وضعیت را به confirmed تغییر می‌دهد و لینک BBB را ایمیل می‌کند.

6. سیستم کد تخفیف با سقف استفاده per‑user

public function validate_coupon() {
$usage = (int) get_user_meta($user_id, ‘_gtbp_coupon_usage_’ . $code, true);
if ($usage >= $limit) wp_send_json_error([‘message’ => ‘سقف مجاز استفاده شما از این کد تمام شده است.’]);
// …
}

در wc_payment_complete (بعد از پرداخت آنلاین):

$discount_code = $order->get_meta(‘_gtbp_discount_code’);
if (!empty($discount_code)) {
$usage = (int) get_user_meta($user_id, ‘_gtbp_coupon_usage_’ . $discount_code, true);
update_user_meta($user_id, ‘_gtbp_coupon_usage_’ . $discount_code, $usage + 1);
}

قابلیت تنظیم درصد و تعداد دفعات مجاز در پنل ادمین (زیرمنوی کدهای تخفیف).

7. صفحه ادمین «ساخت جلسه فوری» (Instant Session) بدون نیاز به رزرو کامل

public function admin_instant_session_page() {
if (isset($_POST[‘no_student’])) {
// فقط ساخت اتاق BBB و نمایش لینک‌ها
$room = $this->create_bbb_room($room_name, ‘بدون شاگرد’, ‘حمیدرضا سعادتی’);
$generated_links = [‘student_link’ => $room[‘student_link’], ‘teacher_link’ => $room[‘teacher_link’]];
} else {
// ثبت رزرو کامل برای یک کاربر + ارسال ایمیل و ذخیره در دیتابیس
}
}

کاربرد: اساتید می‌توانند بدون دخالت کاربر، جلسه فوری ایجاد کرده و لینک را مستقیماً در اختیار شاگرد قرار دهند.

امنیت: بررسی current_user_can('manage_options') و Nonce.

8. نمایش لینک کلاس فقط ۳ دقیقه قبل از شروع (در شورتکد)

$class_start_ts = (new DateTime($row->booking_date . ‘ ‘ . $start_time . ‘:00’, $tz))->getTimestamp();
$show_link = ($now_ts >= ($class_start_ts – 3*60));
if ($show_link) {
$output .= ‘<a href=”‘ . esc_url($row->roomeet_join_link) . ‘” target=”_blank”>لینک ورود</a>’;
} else {
$output .= ‘<span>⏳ لینک کلاس ۳ دقیقه قبل از شروع فعال می‌شود</span>’;
}

رفع مشکل امنیتی: لینک BBB تا لحظه آخر در معرض دید کاربر قرار نمی‌گیرد و امکان ورود زودهنگام وجود ندارد.

استفاده از DateTimeZone('Asia/Tehran') برای همگام‌سازی با ساعت ایران.

9. کرون جاب‌های داخلی (Scheduled Events) برای مدیریت خودکار

public function schedule_payment_checks() {
if (!wp_next_scheduled(‘gtbp_check_pending_payments’)) {
wp_schedule_event(time(), ‘hourly’, ‘gtbp_check_pending_payments’);
}
}

  • gtbp_check_pending_payments: لغو رزروهای pending_card بعد از ۲۴ ساعت.

  • gtbp_send_payment_reminders: ارسال یادآوری برای رزروهای حدود ۱۸ ساعت قبل.

  • gtbp_check_bbb_recordings: واکشی ضبط‌ها از BBB هر ساعت.

  • gtbp_cleanup_bbb_links: حذف لینک‌های منقضی شده از متای کاربران (کرون روزانه).

10. کش کردن آپشن‌ها با Transient API برای کاهش کوئری دیتابیس

private function get_classes() {
if ($this->cached_classes === null) {
$classes = get_transient(‘gtbp_cached_classes’);
if (false === $classes) {
$classes = get_option(‘gtbp_classes’, []);
set_transient(‘gtbp_cached_classes’, $classes, 15 * MINUTE_IN_SECONDS);
}
$this->cached_classes = $classes;
}
return $this->cached_classes;
}

تمامی متدهای getter (get_holidaysget_working_hoursget_bank_info) دارای کش سطح کلاس و ترنزینت هستند.

پاکسازی خودکار کش هنگام ذخیره تغییرات در ادمین ($this->cached_classes = null).

11. ساختار دیتابیس با ایندکس‌گذاری مناسب

CREATE TABLE $this->table_name (
id mediumint(9) NOT NULL AUTO_INCREMENT,
booking_date date NOT NULL,
booking_time varchar(50) NOT NULL,
status varchar(20) DEFAULT ‘confirmed’ NOT NULL,
outside_iran tinyint(1) DEFAULT 0 NOT NULL,
roomeet_room_id varchar(100) DEFAULT NULL,
roomeet_recording_link text DEFAULT NULL,
KEY idx_booking_date (booking_date),
KEY idx_status (status),
KEY idx_recording (roomeet_recording_link)
);

ایندکس روی booking_date برای فیلترهای تقویم و کوئری‌های روزانه.

ایندکس روی status برای جدا کردن رزروهای معلق.

ایندکس روی roomeet_recording_link (اختیاری) برای جستجوی ضبط‌ها.

12. یکپارچگی با ووکامرس (WooCommerce Integration)

همگام‌سازی محصولات: به ازای هر کلاس، یک محصول مجازی (virtual) با دسته‌بندی مخفی (catalog_visibility='hidden') در ووکامرس ساخته می‌شود.

هوک woocommerce_payment_complete: پس از موفقیت پرداخت، رزروهای موقت (temp_booking_ids) حذف شده و رزروهای اصلی با لینک BBB ثبت می‌شوند.

فیلتر woocommerce_get_return_url: کاربر بعد از پرداخت به صفحه اصلی رزرو بازگردانده می‌شود با پارامتر موفقیت.

13. لینک خودکار گوگل تقویم (Google Calendar Link) در ایمیل‌ها

private function generate_gcal_link($date, $time, $class_name, $jalali_date) {
$start_dt = new DateTime(“$date $start_time:00”, $tz);
$start_dt->setTimezone(new DateTimeZone(‘UTC’));
$start_format = $start_dt->format(‘Ymd\THis\Z’);
$text = urlencode(‘کلاس آلمانی – ‘ . $jalali_date);
return “https://www.google.com/calendar/render?action=TEMPLATE&text={$text}&dates={$start_format}/{$end_format}”;
}

کاربر با یک کلیک می‌تواند رویداد را به تقویم گوگل خود اضافه کند.

زمان‌ها به UTC تبدیل می‌شوند تا در هر منطقه زمانی درست نمایش داده شوند.

14. قابلیت ریسپانسیو (Responsive) با استایل‌های موبایل-فرندلی

در استایل‌های فرانت‌اند، از @media (max-width: 600px) برای تغییر چیدمان جدول سبد خرید و دکمه‌ها استفاده شده. همچنین Modal dialog برای تأیید افزودن به سبد خرید:

document.getElementById(‘gtbp-modal’).style.display = ‘flex’;
function handleModal(wantsMore) {
if(wantsMore) { /* ادامه انتخاب */ }
else { /* نمایش سبد */ }
}

15. مدیریت ایمیل با SMTP اختصاصی و قالب متنی غنی

public function setup_smtp($phpmailer) {
$phpmailer->isSMTP();
$phpmailer->Host = ‘mail.hamidrezasaadati.com’;
$phpmailer->SMTPAuth = true;
$phpmailer->Port = 465;
$phpmailer->SMTPSecure = ‘ssl’;
}

ایمیل‌های تایید، یادآوری، لغو و لینک ضبط همگی با نام دامنه اختصاصی ارسال می‌شوند.

کپی تمام ایمیل‌ها به آدرس ادمین (hrschemiker@gmail.com) برای پیگیری.

اگر از گوگل به اینجا کشیده شدید، امیدوارم این اسنیپت‌ها در توسعۀ محصولی که دارید روش کار می‌کنید براتون مفید باشن. اگر سوال یا نظری دارید میتونید این زیر توی کامنت‌ها ازم بپرسید.

امتیاز شما به این نوشته:
(میانگین: 0)

امتیاز شما به این نوشته:
(میانگین: 0)