getSettings('auto_enabled')); } public function getFreePeriod() { $period = (int)$this->getSettings('free_period'); return $period > 0 ? $period : 0; } public function noMoneyOff() { return $this->config('listings.svc.upauto.nomoney.off', false, TYPE_BOOL); } public static function autoPeriods() { $plugin = bff::plugin('services\listings'); return bff::filter('listings.svc.upauto.periods', [ 1 => ['id' => 1, 't' => $plugin->lang('Everyday')], 3 => ['id' => 3, 't' => $plugin->lang('Every 3 days')], 7 => ['id' => 7, 't' => $plugin->lang('Once a week')], -1 => ['id' => -1, 't' => $plugin->lang('Every weekday')], ]); } public function getKey(): string { return 'up'; } /** * Service can be in a pack * @return bool */ public function isPackable(): bool { return true; } public function formParams(array & $params = []) { if ($this->isAutoEnabled()) { $data = []; $fields = [ 'up_on' => TYPE_UINT, 'up_p' => TYPE_UINT, 'up_t' => TYPE_UINT, 'up_h' => TYPE_UINT, 'up_m' => TYPE_UINT, 'up_fr_h' => TYPE_UINT, 'up_fr_m' => TYPE_UINT, 'up_to_h' => TYPE_UINT, 'up_to_m' => TYPE_UINT, 'up_int' => TYPE_UINT, ]; if ($this->isPOST() && $this->input->hasPost('up_p')) { $data = $this->input->postm($fields); } elseif ($this->input->has('up_p')) { $data = $this->input->getm($fields); } $this->activationSettings = array_merge($this->activationSettings ?? [], $data); foreach ($fields as $k => $v) { if (! empty($this->activationSettings[$k])) { $params[$k] = $this->activationSettings[$k]; } } } } /** * Service cost * @return float */ public function getCost(): float { $this->formParams(); return parent::getCost(); } public function calculatePrice(): float { $price = parent::calculatePrice(); if (($this->activationSettings['up_activate'] ?? 0) > 0) { $price = 0; } return $price; } /** * Get service expiration date or 0 - unlimited * @return int */ public function getExpires(): int { return 0; } public function onActivateAfter() { do { if (! $this->itemID) { break; } $itemData = Listings::model()->itemData($this->itemID, ['cat_id1', 'publicated_to']); if (empty($itemData)) { break; } $exist = $this->getItemServiceData($this->getKey(), $this->itemID); if (empty($exist)) { break; } $payed = $exist['settings']['up_activate'] ?? 0; $this->activationSettings['up_activate'] = $payed; $save = false; if ($this->activationPack) { $cnt = $this->activationSettings['up_cnt'] ?? 0; if ($cnt > 0) { $payed += $cnt; $save = true; } } $position = Listings::model()->itemPositionInCategory($this->itemID, $itemData['cat_id1']); if ($position === 1) { $this->updateBalance = false; }else if (! $this->activationPack && $payed > 0) { $this->updateBalance = false; $payed--; $save = true; } $update = [ 'publicated_order' => $this->db->now(), ]; $to = Listings::itemPublicationPeriod()->refreshTo(false, ''); if (strtotime($itemData['publicated_to']) < $to) { $update['publicated_to'] = date('Y-m-d H:i:s', $to); } Listings::model()->itemSave($this->itemID, $update); if (! empty($this->activationSettings['up_on'])) { $this->activationSettings['up_next'] = $this->calcNext(); $save = true; } if ($save) { $this->activationSettings['up_activate'] = $payed; $exist->fill(['settings' => $this->activationSettings])->save(); } else { if ($payed <= 0) { $this->deactivate(); } } } while(false); } /** * Deactivate service * @param array $opts * @return bool */ public function deactivate(array $opts = []): bool { if ( empty($this->activationSettings['up_on']) && empty($this->activationSettings['up_activate']) ) { $exist = $this->getItemServiceData($this->getKey(), $this->itemID); if ($exist) { if (! empty($exist['settings']['up_on']) || ! empty($exist['settings']['up_activate'])) { $exist->fill(['settings' => $this->activationSettings])->save(); } } return parent::deactivate($opts); } return false; } /** * Returns service bill description * @return string */ public function getBillDescription(): string { $item = $this->manager->getItemData($this->itemID, ['link', 'title']); if (! $item) { return ''; } return $this->plugin()->lang('Raising of listing in the list
[title]', [ 'link' => $item['link'], 'title' => $item['title'], ]); } /** * Admin settings form * @param Form $form * @return mixed|void */ public function onAdminSettings($form) { parent::onAdminSettings($form); $form->to('settings'); $form->checkbox('auto_enabled', $this->plugin()->langAdmin('Auto UP'), true) ->label($this->plugin()->langAdmin('Available')) ->before('icon_b'); $form->number('free_period', $this->plugin()->langAdmin('Free UP'), 0) ->tip($this->plugin()->langAdmin('The number of days after which the free raising of the listings becomes available, [num] - the function is disabled.', ['num' => 0])) ->beforeFieldRender(function($html) { return $html; return $this->plugin()->langAdmin('every').' '.$html.''; # todo: ? }) ->label($this->plugin()->langAdmin('days')) ->after('auto_enabled'); $form->endUnion(); $this->addAdminSettingsFormPrices($form); } /** * Admin pack settings form * @param Form $form * @return void */ public function onAdminSettingsPackServices($form) { $form ->number('up_cnt', '', 0) ->stretch('mini') ->htmlAfter(' '.$this->plugin()->langAdmin('- number of raises')) ->together($this->getKey()) ; } public function getInstallSettings(): array { return [ 'title' => [ 'en' => 'Up', 'ru' => 'Поднятие', ], 'description' => [ 'ru' => 'Повышая приоритет вашего объявления вы поднимаете его в начало списка объявлений схожей тематики. ' . PHP_EOL . 'Это удобный способ для привлечения внимания посетителей портала к вашему предложению. ' . PHP_EOL . 'Станьте первыми в списке и будете лидером по числу просмотров.', ], 'description_view' => [ 'ru' => '

Повышая приоритет вашего объявления вы поднимаете его в начало списка объявлений схожей тематики. 

'. '

Это удобный способ для привлечения внимания посетителей портала к вашему предложению. 

'. '

Станьте первыми в списке и будете лидером по числу просмотров.

', ], 'price' => 1, 'settings' => [ 'auto_enabled' => 1, 'free_period' => 0, 'add_form' => 0, ], ]; } public function onInstall(array $opts = []) { $opts['icons'] = [ 'icon_b' => $this->plugin()->path('static/up.svg'), 'icon_s' => $this->plugin()->path('static/up.svg'), ]; return parent::onInstall($opts); } /** * Prepare activation form data * @param array $data @ref * @param array $opts * @return void */ public function onActivationForm(array & $data, array $opts = []) { parent::onActivationForm($data); if ($this->isAutoEnabled()) { $this->formParams(); $opts = array_merge($opts, $this->activationSettings ?? []); $opts['formData'] = & $data; $opts['price'] = $this->getPrice(); $opts['id'] = $this->itemID; $data['form'] = $this->plugin()->template('tpl/form.up', $opts); } } /** * Активация услуги бесплатного поднятия объявления * @param int $itemID ID объявления * @param bool $silent тихий режим, только поднять (если доступно) * @param string $nextDate дата последнего бесплатного поднятия (если известна) * @return string сообщение об успешной активации */ public function upFree(int $itemID, bool $silent = true, string $nextDate = '') { do { $days = $this->getFreePeriod(); if (! $days) { if (! $silent) { $this->errors->reloadPage(); } else { return false; } break; } $upTo = strtotime('-' . $days . ' days'); if (empty($nextDate)) { $itemData = Listings::model()->itemData($itemID, ['user_id', 'svc_up_free', 'created']); if (empty($itemData)) { if (! $silent) { $this->errors->reloadPage(); } else { return false; } break; } $created = strtotime($itemData['created']); $upFree = strtotime($itemData['svc_up_free']); $nextDate = $upFree > $created ? $upFree : $created; } if ($nextDate > $upTo) { if (! $silent) { $allow = strtotime('+' . $days . ' days', $nextDate); $this->errors->set($this->plugin()->lang('The possibility of a free raising for this announcement will be available [date]', [ 'date' => tpl::date($allow), ])); } else { return false; } break; } $now = $this->db->now(); $res = Listings::model()->itemSave($itemID, [ 'svc_up_free' => $now, 'publicated_order' => $now, ]); if (empty($res)) { if (! $silent) { $this->errors->reloadPage(); } else { return false; } } else { return ($silent ? true : $this->plugin()->lang('The listing was successfully raised')); } } while (false); if ($silent) { return false; } return ''; } public function upFreeMass(array $itemsIDs = [], & $response = []) { do { if (empty($itemsIDs)) { break; } $days = $this->getFreePeriod(); if (! $days) { break; } $response['cnt'] = $this->itemsUpFree([ 'id' => $itemsIDs, 'user_id' => User::id(), 'svc_up_free <= :date', ], [ 'bind' => [':date' => date('Y-m-d', strtotime('-' . $days . ' days'))], ]); if (! empty($response['cnt'])) { $response['message'] = $this->plugin()->lang('Selected listings were successfully raised'); } return; } while (false); $this->errors->reloadPage(); } public function autoSave() { do { if (! $this->isAutoEnabled()) { break; } $itemID = $this->input->post('id', TYPE_UINT); if (! $itemID || ! $this->security->validateToken()) { break; } if (! Listings::isItemOwner($itemID)) { break; } $this->setActivationSettings($itemID, User::id()); $this->getCost(); if (! empty($this->activationSettings['up_on'])) { $this->activationSettings['up_next'] = $this->calcNext(); $success = $this->activateItemService( $this->getKey(), $this->itemID, $this->userID, $this->activationSettings ); if ($success) { return ['message' => $this->plugin()->lang('Automatic raise activated'), 'active' => 1]; } break; } else { $this->activationSettings['up_on'] = 0; $this->deactivate(); return ['message' => $this->plugin()->lang('Automatic raise deactivated'), 'active' => 0]; } } while (false); $this->errors->reloadPage(); } public function cronUpAuto() { if (! $this->isCron()) { return; } if (! $this->isAutoEnabled()) { return; } $this->getServiceStatusModel()->tap(function ($query) { $freeDays = $this->getFreePeriod(); /** @var $query ServiceStatus */ $query ->where('group_key', $this->getGroupKey()) ->where('service_key', $this->getKey()) ->where('service_status', static::STATUS_ACTIVE) ->chunkById(100, function ($items) use ($freeDays) { $time = time(); foreach ($items as $item) { if (empty($item['item_id'])) { continue; } $itemID = $item['item_id']; if (empty($item['settings']['up_on'])) { continue; } if (! empty($item['settings']['up_next'])) { if ($item['settings']['up_next'] > $time) { # еще не наступило время следующего поднятия continue; } } do { $itemData = Listings::model()->itemData($itemID, ['user_id', 'cat_id1', 'status']); if (empty($itemData) || $itemData['status'] != Listings::STATUS_PUBLICATED) { break; } $this->setActivationSettings($itemID, $itemData['user_id'], $item['settings']); # проверяем позицию объявления в главной категории if (Listings::model()->itemPositionInCategory($itemID, $itemData['cat_id1']) == 1) { break; } # проверка бесплатного поднятия if ($freeDays) { if ($this->upFree($itemID)) { # удалось поднять бесплатно break; } } $price = $this->getPrice(); # проверяем достаточно ли средств на счету для активации услуги $balance = Users::model()->userBalance($itemData['user_id']); if ($balance < $price) { if ($this->noMoneyOff()) { # деактивируем услугу $this->activationSettings['up_on'] = 0; $this->deactivate(); continue 2; } break; } # активируем услугу $this->activate(); continue 2; } while (false); # рассчитаем время следующего запуска $this->activationSettings['up_next'] = $this->calcNext(); $item ->fill(['settings' => $this->activationSettings]) ->save(); } }) ; }); } /** * Расчет времени следующего запуска для услуги автоматического поднятия в соответствии с настройками * @param int $time время с которого начинать расчет в Unix формате, 0 - текущее время * @return int time */ protected function calcNext(int $time = 0) { $sett = $this->activationSettings; if (! $time) { $time = time(); } $result = strtotime('+1 month', $time); do { if (empty($sett['up_p']) || empty($sett['up_t'])) { break; } if ($sett['up_t'] == static::AUTO_SPECIFIED) { # Точно указанное время: $hour = (empty($sett['up_h']) ? 0 : $sett['up_h']); if ($hour < 0 || $hour > 24) { $hour = 0; } $hz = (string)$hour; $hz = (strlen($hz) == 1 ? '0' . $hz : $hz); $min = (empty($sett['up_m']) ? 0 : $sett['up_m']); $min = round($min / 10) * 10; if ($min < 0 || $min > 59) { $min = 0; } $mz = (string)$min; $mz = (strlen($mz) == 1 ? '0' . $mz : $mz); # указанное время для сегодня $result = strtotime(date('Y-m-d ' . $hz . ':' . $mz . ':00', $time)); switch ($sett['up_p']) { case 1: # каждый день case 3: # Раз в 3 дня case 7: # Раз в неделю if ($result <= $time) { # уже прошло, рассчитаем для запуска через 'up_p' дней $result = strtotime('+' . $sett['up_p'] . ' day', $result); } break 2; case -1: # Каждый будний день $dw = date('w', $result); if ($dw == 0 || $dw == 6 || $result <= $time) { # уже прошло, или сегодня выходной do { # найдем след. рабочий день $result = strtotime('+1 day', $result); $dw = date('w', $result); } while ($dw == 0 || $dw == 6); } break 2; } } elseif ($sett['up_t'] == static::AUTO_INTERVAL) { # Промежуток времени с-до, с указанием интервала: $fr_h = (empty($sett['up_fr_h']) ? 0 : $sett['up_fr_h']); if ($fr_h < 0 || $fr_h > 24) { $fr_h = 0; } $fr_hz = (string)$fr_h; $fr_hz = (strlen($fr_hz) == 1 ? '0' . $fr_hz : $fr_hz); $fr_m = (empty($sett['up_fr_m']) ? 0 : $sett['up_fr_m']); $fr_m = round($fr_m / 10) * 10; if ($fr_m < 0 || $fr_m > 59) { $fr_m = 0; } $fr_mz = (string)$fr_m; $fr_mz = (strlen($fr_mz) == 1 ? '0' . $fr_mz : $fr_mz); $to_h = (empty($sett['up_to_h']) ? 0 : $sett['up_to_h']); if ($to_h < 0 || $to_h > 24) { $to_h = 0; } $to_hz = (string)$to_h; $to_hz = (strlen($to_hz) == 1 ? '0' . $to_hz : $to_hz); $to_m = (empty($sett['up_to_m']) ? 0 : $sett['up_to_m']); $to_m = round($to_m / 10) * 10; if ($to_m < 0 || $to_m > 59) { $to_m = 0; } $to_mz = (string)$to_m; $to_mz = (strlen($to_mz) == 1 ? '0' . $to_mz : $to_mz); $int = empty($sett['up_int']) ? 60 : $sett['up_int']; if ($int < 30) { $int = 30; } # указанное время для сегодня "c" $result_fr = strtotime(date('Y-m-d ' . $fr_hz . ':' . $fr_mz . ':00', $time)); # указанное время для сегодня "до" $result_to = strtotime(date('Y-m-d ' . $to_hz . ':' . $to_mz . ':00', $time)); switch ($sett['up_p']) { case 1: # каждый день case 3: # Раз в 3 дня case 7: # Раз в неделю if ($result_to < $time) { # уже прошло время "до", рассчитаем для запуска через 'up_p' дней $result = strtotime('+' . $sett['up_p'] . ' day', $result_fr); } elseif ($result_fr <= $time) { # уже прошло время "c" но еще не наступило "до" $result = $result_fr; do { $result = strtotime('+' . $int . ' minutes', $result); if ($time < $result && $result <= $result_to) { break 3; } } while ($result < $result_to); # с учетом указанного интервала сегодня до времени "до" не попадаем, запуска через 'up_p' дней $result = strtotime('+' . $sett['up_p'] . ' day', $result_fr); } else { $result = $result_fr; # еще не наступило сегодня "с" } break 2; case -1: # Каждый будний день $dw = date('w', $result_fr); if ($dw == 0 || $dw == 6) { # если сегодня выходной $result = $result_fr; do { # найдем след. рабочий день $result = strtotime('+1 day', $result); $dw = date('w', $result); } while ($dw == 0 || $dw == 6); break 2; } do { if ($result_to < $time) { # уже прошло время "до", рассчитаем для запуска на завтра $result = strtotime('+1 day', $result_fr); } elseif ($result_fr <= $time) { # уже прошло время "c" но еще не наступило "до" $result = $result_fr; do { $result = strtotime('+' . $int . ' minutes', $result); if ($time < $result && $result <= $result_to) { break 2; } } while ($result < $result_to); # с учетом указанного интервала сегодня до времени "до" не попадаем, рассчитаем для запуска на завтра $result = strtotime('+1 day', $result_fr); } else { $result = $result_fr; # еще не наступило сегодня "с" } } while (false); $dw = date('w', $result); if ($dw == 0 || $dw == 6) { # если день запуска выходной => след. рабочий день do { $result = strtotime('+1 day', $result); $dw = date('w', $result); } while ($dw == 0 || $dw == 6); } break 2; } } else { break; } } while (false); return $result; } /** * Отправка почтовых уведомлений о возможности бесплатного поднятия объявлений */ public function cronUpFreeEnable() { if (! $this->isCron() || $this->app->demo()) { return; } $days = $this->getFreePeriod(); if (! $days) { return; # бесплатные поднятия отключены } # кол-во отправляемых объявлений за подход $limit = 100; $now = date('Y-m-d'); # очистка списка отправленных за предыдущие дни $last = $this->config('bbs_item_up_free_enable_last_enotify', '', TYPE_STR); if ($last != $now) { config::save('bbs_item_up_free_enable_last_enotify', $now); Listings::model()->itemsEnotifyClear(static::ENOTIFY_UP_FREE_ENABLE, $last); $this->cronUpFillEmpty(); } # получаем пользователей у которых есть одно+ объявление с доступным бесплатным поднятием, $users = $this->cronUpFreeEnableUsers($days, $limit); if (empty($users)) { return; } foreach ($users as &$v) { $this->locale->setCurrentLanguage($v['lang'], true); $loginAuto = Users::loginAutoHash($v); if ($v['cnt'] == 1) { # у пользователя всего одно объявление # помечаем в таблице отправленных за сегодня (если еще нет) if ($this->cronUpFreeEnableSended([$v['item_id']], $now)) { continue; } $v['item_link'] = Url::dynamic($v['item_link']); $v['item_link_up'] = $v['item_link'] . '?up_free=1&alogin=' . $loginAuto; $this->app->sendMailTemplate($v, 'listings_item_upfree', $v['email'], false, '', '', $v['lang']); } else { $v['items'] = explode(',', $v['items']); if ($this->cronUpFreeEnableSended($v['items'], $now)) { continue; } $v['count'] = $v['cnt']; $v['count_items'] = tpl::declension($v['cnt'], _t('listings', 'listing;listings;listings')); $v['items_link_up'] = Listings::url('my.items', [ 'act' => 'email-up-free', 'alogin' => $loginAuto, ]); $this->app->sendMailTemplate($v, 'listings_item_upfree_group', $v['email'], false, '', '', $v['lang']); } } unset($v); } /** * Данные об объявлениях для "Уведомления о возможности бесплатного поднятия объявлений" (cron) * @param int $days кол-во дней через которое поднятие становится вновь доступным * @param int $limit ограничение на выборку * @return array */ protected function cronUpFreeEnableUsers(int $days, int $limit = 100) { if (empty($limit) || $limit < 0) { $limit = 100; } $filter = [ 'I.is_publicated = 1', 'I.status = ' . Listings::STATUS_PUBLICATED, 'I.svc_up_free = :date', 'E.item_id IS NULL', 'U.user_id = I.user_id', 'US.user_id = I.user_id', 'U.blocked = 0', 'U.activated = 1', 'U.enotify & ' . Users::ENOTIFY_NEWS, ]; $filter = Listings::model()->prepareFilter($filter, '', [ ':type' => static::ENOTIFY_UP_FREE_ENABLE, ':now' => date('Y-m-d'), ':date' => date('Y-m-d', strtotime('-' . $days . ' days')), ]); $data = $this->db->select_key('SELECT I.id as item_id, I.title as item_title, I.link as item_link, U.email, U.name, I.user_id, U.user_id_ex, US.last_login, U.lang, COUNT(I.id) AS cnt, GROUP_CONCAT(I.id) AS items FROM ' . Listings::TABLE_ITEMS . ' AS I LEFT JOIN ' . Listings::TABLE_ITEMS_ENOTIFY . ' E ON E.item_id = I.id AND E.sended = :now AND E.message_type = :type, ' . Users::TABLE_USERS . ' AS U, ' . Users::TABLE_USERS_STAT . ' AS US ' . $filter['where'] . ' GROUP BY I.user_id ' . $this->db->prepareLimit(0, $limit), 'user_id', $filter['bind']); if (empty($data)) { $data = []; } return $data; } /** * Уведомления о возможности бесплатного поднятия объявлений: * Работа со списком объявлений отправленных уведомлений о возможности бесплатного поднятия * @param array $itemsID ID объявлений * @param string $date дата в формаре "Y-m-d" * @return bool */ protected function cronUpFreeEnableSended(array $itemsID, string $date) { $data = $this->db->select_rows_column(Listings::TABLE_ITEMS_ENOTIFY, 'item_id', [ 'message_type' => static::ENOTIFY_UP_FREE_ENABLE, 'item_id' => $itemsID, 'sended' => $date, ]); if (empty($data)) { $data = false; } $insert = []; foreach ($itemsID as $v) { if ($data && in_array($v, $data)) { continue; } $insert[] = [ 'message_type' => static::ENOTIFY_UP_FREE_ENABLE, 'item_id' => $v, 'sended' => $date, ]; } if (! empty($insert)) { $this->db->multiInsert(Listings::TABLE_ITEMS_ENOTIFY, $insert); return false; } return true; } protected function cronUpFillEmpty() { $this->db->exec('UPDATE '.Listings::TABLE_ITEMS.' SET svc_up_free = DATE(created) WHERE svc_up_free = 0'); } public function emailUpFree(& $messages) { $days = $this->getFreePeriod(); if (! $days) { return; } $userID = User::id(); if (! $userID) { return; } $itemsID = $this->userUpFreeEnable($userID, $days); $cnt = $this->itemsUpFree(['id' => $itemsID]); if ($cnt) { $messages[] = $this->plugin()->lang('Successfully raised [cnt].', ['cnt' => tpl::declension($cnt, _t('listings', 'listing;listings;listings'))]); } } /** * Бесплатное поднятие: * Получаем список ID объявлений пользователя, подходящих по дате, отправленной в рассылке * @param int $userID ID пользователя * @param int $days кол-во дней * @return mixed */ public function userUpFreeEnable($userID, int $days) { return $this->db->select_one_column(' SELECT I.id FROM ' . Listings::TABLE_ITEMS . ' I WHERE I.user_id = :user AND I.company_id >= 0 AND I.is_publicated = 1 AND I.status = :publicated AND I.svc_up_free <= :date ', [ ':user' => $userID, ':publicated' => Listings::STATUS_PUBLICATED, ':date' => date('Y-m-d', strtotime('-' . $days . ' days')), ]); } /** * Бесплатное поднятие нескольких объявлений по фильтру * @param array $filter фильтр требуемых объявлений * @param array $opts доп. параметры * @return int кол-во затронутых объявлений */ public function itemsUpFree(array $filter, array $opts = []) { if (empty($filter)) { return 0; } if (! isset($opts['context'])) { $opts['context'] = 'items-upfree'; } $now = $this->db->now(); return Listings::model()->itemsUpdateByFilter([ 'svc_up_free' => $now, 'publicated_order' => $now, ], $filter, $opts); } public function adminItemServices(array $data, array $itemData) { $data['itemData'] = $itemData; return $this->plugin()->template('tpl/admin.item.up', $data); } }