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); } }