490 lines
19 KiB
PHP
490 lines
19 KiB
PHP
<?php
|
||
|
||
declare(strict_types=1);
|
||
|
||
namespace app\models\connection;
|
||
|
||
use yii;
|
||
use yii\base\Model;
|
||
|
||
use app\models\Dellin as DellinModel;
|
||
use app\models\Product;
|
||
use app\models\Account;
|
||
use app\models\Settings;
|
||
|
||
use GuzzleHttp\Client as Guzzle;
|
||
use GuzzleHttp\Exception\ClientException as GuzzleException;
|
||
|
||
use DateTime;
|
||
use DateTimeZone;
|
||
use Exception;
|
||
|
||
class Dellin extends Model
|
||
{
|
||
/**
|
||
* Инстанция браузера
|
||
*/
|
||
public static Guzzle $browser;
|
||
|
||
/**
|
||
* Сессия аккаунта
|
||
*/
|
||
public static string $session;
|
||
|
||
public function __construct($config = [])
|
||
{
|
||
parent::__construct($config);
|
||
|
||
self::$browser = new Guzzle([
|
||
'base_uri' => 'https://api.dellin.ru/'
|
||
]);
|
||
|
||
self::authorization();
|
||
}
|
||
|
||
// /**
|
||
// * Поиск городов
|
||
// *
|
||
// * @return array|null Найденные города
|
||
// */
|
||
// public function searchCities(): ?array
|
||
// {
|
||
// $this->onReady(function () {
|
||
// // Запрос городов
|
||
// $request = $this->browser->post('/v2/public/kladr.json', [
|
||
// 'json' => [
|
||
// 'appkey' => yii::$app->params['dellin']['key'],
|
||
|
||
// ]
|
||
// ])
|
||
// });
|
||
// }
|
||
/**
|
||
* Рассчет доставки (расширенный)
|
||
*
|
||
* Рассчет нескольких товаров идет через простое перемножение результатов доставки одного товара
|
||
* В API всегда идет рассчет для одного товара, так было решено
|
||
*
|
||
* @param int $from Идентификатор терминала Dellin
|
||
* @param int $to Идентификатор терминала Dellin
|
||
* @param int $weight Вес (кг)
|
||
* @param int $x Ширина (cм)
|
||
* @param int $y Высота (cм)
|
||
* @param int $z Длинна (cм)
|
||
* @param int $amount Количество
|
||
* @param Account|int|null $account Аккаунт
|
||
*
|
||
* @return string
|
||
*
|
||
* @todo Загружать помимо терминалов ещё и адреса, чтобы доделать доставку малогабаритных грузов
|
||
* Разрабраться с параметрами 0,54м * 0,39м * 0,39м (0.082134м) и 0.1 куб метр в чем разница
|
||
*/
|
||
public static function calcDeliveryAdvanced(int $from, int $to, int $weight, int $x, int $y, int $z, int $amount = 1, bool $avia = false, Account|int|null $account = null): array
|
||
{
|
||
return self::handle(function () use ($from, $to, $weight, $x, $y, $z, $amount, $avia, $account) {
|
||
// Всё готово к работе
|
||
|
||
$account = Account::initAccount($account);
|
||
|
||
// Инициализация
|
||
$from = DellinModel::searchByTerminalId($from, terminal_data_only: true);
|
||
$to = DellinModel::searchByTerminalId($to, terminal_data_only: true);
|
||
|
||
// Значения по умолчанию, если указан 0
|
||
if (empty($x) || $x === 0) $x = 25;
|
||
if (empty($y) || $y === 0) $y = 40;
|
||
if (empty($z) || $z === 0) $z = 25;
|
||
if (empty($weight) || $weight === 0) $weight = 300;
|
||
|
||
// Конвертация из сантиметров в метры
|
||
$x /= 100;
|
||
$y /= 100;
|
||
$z /= 100;
|
||
|
||
// Вычисление самой крупной стороны, так как ДеловыеЛинии имеют ограничения на все три поля и у длинны оно больше всех
|
||
if ($x > $z && $x > $y) {
|
||
// "X" больше всех
|
||
|
||
// Инициализация
|
||
$width = $z;
|
||
$height = $y;
|
||
$length = $x;
|
||
} else if ($y > $x && $y > $z) {
|
||
// "Y" больше всех
|
||
|
||
// Инициализация
|
||
$width = $x;
|
||
$height = $z;
|
||
$length = $y;
|
||
} else {
|
||
// "Z" больше всех
|
||
|
||
// Инициализация
|
||
$width = $x;
|
||
$height = $y;
|
||
$length = $z;
|
||
}
|
||
|
||
// Инициализация
|
||
$query = [];
|
||
|
||
// Рассчёт типа доставки
|
||
if (
|
||
!$avia
|
||
&& $weight <= 30
|
||
&& ($length <= 0.54 && $width <= 0.39 && $height <= 0.39)
|
||
&& $length * $width * $height <= 0.1
|
||
) {
|
||
// Доставка категории "small"
|
||
|
||
$query['delivery']['deliveryType']['type'] = 'small';
|
||
$query['delivery']['derival']['variant'] = 'address';
|
||
$query['delivery']['derival']['address']['search'] = $from->fullAddress;
|
||
$query['delivery']['derival']['time']['worktimeStart'] = '08:00';
|
||
$query['delivery']['derival']['time']['worktimeEnd'] = '20:00';
|
||
$query['delivery']['arrival']['variant'] = 'address';
|
||
$query['delivery']['arrival']['address']['search'] = $to->fullAddress;
|
||
$query['delivery']['arrival']['time']['worktimeStart'] = '08:00';
|
||
$query['delivery']['arrival']['time']['worktimeEnd'] = '20:00';
|
||
} else {
|
||
// Доставка категории "auto"
|
||
|
||
if ($avia) {
|
||
// Рассчет для доставки по воздуху
|
||
|
||
// Ограничение на минимальный вес
|
||
$weight <= 0.5 and $weight = 0.5;
|
||
|
||
$query['delivery']['deliveryType']['type'] = 'avia';
|
||
} else {
|
||
// Рассчет для доставки по земле
|
||
|
||
$query['delivery']['deliveryType']['type'] = 'auto';
|
||
}
|
||
|
||
$query['delivery']['derival']['variant'] = 'terminal';
|
||
$query['delivery']['derival']['terminalID'] = $from->id;
|
||
$query['delivery']['arrival']['variant'] = 'terminal';
|
||
$query['delivery']['arrival']['terminalID'] = $to->id;
|
||
}
|
||
|
||
// Инициализация часового пояса
|
||
preg_match_all('/UTC([\+\-0-9:]*)/', $account->zone ?? Settings::searchActive()['timezone_default'] ?? 'UTC+3', $timezone);
|
||
$timezone = $timezone[1][0];
|
||
|
||
// Инициализация
|
||
$query = array_merge_recursive(
|
||
$query,
|
||
[
|
||
'appkey' => yii::$app->params['dellin']['key'],
|
||
'sessionID' => self::$session,
|
||
'delivery' => [
|
||
'derival' => [
|
||
'produceDate' => (new DateTime())->setTimestamp(time() + 86400 * 3)->setTimezone(new DateTimeZone($timezone))->format('Y-m-d')
|
||
]
|
||
],
|
||
'members' => [
|
||
'requester' => [
|
||
'role' => 'sender'
|
||
]
|
||
],
|
||
'cargo' => [
|
||
'quantity' => 1,
|
||
'width' => $width,
|
||
'height' => $height,
|
||
'length' => $length,
|
||
'totalVolume' => $width * $height * $length,
|
||
'totalWeight' => $weight,
|
||
'oversizedWeight' => $weight,
|
||
'oversizedVolume' => $width * $height * $length
|
||
]
|
||
]
|
||
);
|
||
|
||
// Запрос
|
||
$request = self::$browser->post('/v2/calculator.json', [
|
||
'json' => $query
|
||
]);
|
||
|
||
if ($request->getStatusCode() === 200) {
|
||
// Запрос прошел успешно
|
||
|
||
// Инициализация
|
||
$response = json_decode((string) $request->getBody(), true);
|
||
|
||
if ($response['metadata']['status'] === 200) {
|
||
// Со стороны ДеловыеЛинии ошибок нет
|
||
|
||
$response['data']['price'] = [
|
||
'one' => $response['data']['price'],
|
||
'all' => $response['data']['price'] * $amount
|
||
];
|
||
|
||
return $response['data'];
|
||
}
|
||
|
||
throw new Exception('На стороне сервера ДеловыеЛинии какие-то проблемы, либо отправлен неверный запрос', 500);
|
||
}
|
||
|
||
throw new Exception('Не удалось запросить рассчёт доставки у ДеловыеЛинии', 500);
|
||
});
|
||
}
|
||
|
||
/**
|
||
* Рассчет доставки
|
||
*
|
||
* @param string $from Идентификатор терминала Dellin
|
||
* @param string $to Идентификатор терминала Dellin
|
||
*
|
||
* @return string
|
||
*
|
||
* @deprecated
|
||
*/
|
||
public static function calcDelivery(string $from, string $to): array
|
||
{
|
||
return self::handle(function () use ($from, $to) {
|
||
// Всё готово к работе
|
||
|
||
// Запрос
|
||
$request = self::$browser->post('/v1/micro_calc.json', [
|
||
'json' => [
|
||
'appkey' => yii::$app->params['dellin']['key'],
|
||
'sessionID' => self::$session,
|
||
'derival' => [
|
||
'city' => $from
|
||
],
|
||
'arrival' => [
|
||
'city' => $to
|
||
]
|
||
]
|
||
]);
|
||
|
||
|
||
if ($request->getStatusCode() === 200) {
|
||
// Запрос прошел успешно
|
||
|
||
// Инициализация
|
||
$response = json_decode((string) $request->getBody(), true);
|
||
|
||
if ($response['metadata']['status'] === 200) {
|
||
// Со стороны ДеловыеЛинии ошибок нет
|
||
|
||
return $response['data'];
|
||
}
|
||
|
||
throw new Exception('На стороне сервера ДеловыеЛинии какие-то проблемы, либо отправлен неверный запрос', 500);
|
||
}
|
||
|
||
throw new Exception('Не удалось запросить рассчёт доставки у ДеловыеЛинии', 500);
|
||
});
|
||
}
|
||
|
||
/**
|
||
* Импорт терминалов
|
||
*
|
||
* @return array|null Сохранённые терминалы
|
||
*/
|
||
public static function importTerminals(Account|int|null $account = null): ?int
|
||
{
|
||
return self::handle(function () use ($account) {
|
||
// Всё готово к работе
|
||
|
||
if (is_null($account)) {
|
||
// Данные аккаунта не переданы
|
||
|
||
if (isset(yii::$app->user)) {
|
||
if (yii::$app->user->isGuest) {
|
||
// Аккаунт не аутентифицирован
|
||
|
||
return 0;
|
||
} else {
|
||
// Аккаунт аутентифицирован
|
||
|
||
// Инициализация
|
||
$account = yii::$app->user->identity;
|
||
}
|
||
} else {
|
||
return 0;
|
||
}
|
||
} else {
|
||
if (is_int($account)) {
|
||
// Передан идентификатор (_key) аккаунта (подразумевается)
|
||
|
||
// Инициализация (поиск в базе данных)
|
||
if (!$account = Account::searchById(Account::collectionName() . "/$account")) {
|
||
// Не удалось инициализировать аккаунт
|
||
|
||
return 0;
|
||
}
|
||
}
|
||
}
|
||
|
||
// Запрос ссылки на файл с городами, возвращает ['hash' => string, 'url' => string]
|
||
$request = self::$browser->post('/v3/public/terminals.json', [
|
||
'json' => [
|
||
'appkey' => yii::$app->params['dellin']['key'],
|
||
]
|
||
]);
|
||
|
||
if ($request->getStatusCode() === 200) {
|
||
// Запрос прошел успешно
|
||
|
||
// Инициализация часового пояса
|
||
preg_match_all('/UTC([\+\-0-9:]*)/', $account->zone ?? Settings::searchActive()['timezone_default'] ?? 'UTC+3', $timezone);
|
||
$timezone = $timezone[1][0];
|
||
|
||
// Инициализация параметров
|
||
$response = json_decode((string) $request->getBody(), true);
|
||
$dir = YII_PATH_PUBLIC . '/../assets/import/' . (new DateTime('now', new DateTimeZone($timezone)))->format('Y-m-d') . '/dellin/terminals/' . (yii::$app->user->identity->_key ?? 'system') . '/';
|
||
$amount = 0;
|
||
|
||
if (!file_exists($dir)) {
|
||
// Директории не существует
|
||
|
||
mkdir($dir, 0775, true);
|
||
}
|
||
|
||
$request = self::$browser->get($response['url'], [
|
||
'sink' => $file = $dir . time() . '.json'
|
||
]);
|
||
|
||
// Инициализация
|
||
$terminals = json_decode(fread(fopen($file, "r"), filesize($file)), true);
|
||
|
||
foreach ($terminals['city'] as $terminal) {
|
||
// Перебор городов
|
||
|
||
if ($model = DellinModel::searchByCityId($terminal['id'])) {
|
||
// Удалось найти город в базе данных
|
||
|
||
$after_import_log = function () use ($model): void {
|
||
// Запись в журнал
|
||
$model->journal('update');
|
||
|
||
if (yii::$app->getRequest()->isConsoleRequest) {
|
||
// Вызов из терминала
|
||
|
||
echo 'Удалось перезаписать терминалы города: ' . ($model->data['name'] ?? 'Неизвестно') . PHP_EOL;
|
||
}
|
||
};
|
||
} else {
|
||
// Не удалось найти город в базе данных
|
||
|
||
$model = new DellinModel();
|
||
|
||
$after_import_log = function () use ($model): void {
|
||
if (yii::$app->getRequest()->isConsoleRequest) {
|
||
// Вызов из терминала
|
||
|
||
echo 'Удалось записать терминалы города: ' . ($model->data['name'] ?? 'Неизвестно') . PHP_EOL;
|
||
}
|
||
};
|
||
}
|
||
|
||
// Запись
|
||
$model->data = $terminal;
|
||
|
||
// Отправка в базу данных
|
||
if ($model->save()) {
|
||
// Удалось сохранить в базе данных
|
||
|
||
// Запись в журнал
|
||
$after_import_log();
|
||
|
||
// Постинкрементация счётчика
|
||
$amount++;
|
||
|
||
continue;
|
||
} else {
|
||
// Не удалось сохранить в базе данных
|
||
|
||
throw new Exception('Не удалось сохранить терминалы города "' . ($model->data['name'] ?? 'Неизвестно') . '" в базу данных', 500);
|
||
}
|
||
}
|
||
|
||
return $amount;
|
||
}
|
||
|
||
throw new Exception('Не удалось синхронизировать данные городов с ДеловыеЛинии', 500);
|
||
});
|
||
}
|
||
|
||
/**
|
||
* Аутентификация и авторизация
|
||
*/
|
||
protected static function authorization(): bool
|
||
{
|
||
// Аутентификация и авторизация
|
||
$request = self::browser()->post('/v3/auth/login.json', [
|
||
'json' => [
|
||
'appkey' => yii::$app->params['dellin']['key'],
|
||
'login' => yii::$app->params['dellin']['nickname'],
|
||
'password' => yii::$app->params['dellin']['password']
|
||
]
|
||
]);
|
||
|
||
if ($request->getStatusCode() === 200) {
|
||
// Запрос прошел успешно
|
||
|
||
// Инициализация
|
||
$response = json_decode((string) $request->getBody(), true);
|
||
|
||
if ($response['metadata']['status'] === 200) {
|
||
// Аутентификация и авторизация пройдены успешно
|
||
|
||
// Запись сессии
|
||
self::$session = $response['data']['sessionID'];
|
||
|
||
return true;
|
||
}
|
||
|
||
throw new Exception('Не удалось авторизироваться в ДеловыеЛинии', $response['metadata']['status']);
|
||
}
|
||
|
||
throw new Exception('Не удалось авторизироваться в ДеловыеЛинии', $request->getStatusCode);
|
||
}
|
||
|
||
/**
|
||
* Инициализация и выполнение
|
||
*
|
||
* @param callable|null $function Код к выполнению
|
||
* @param [mixed] ...$vars Параметры к нему
|
||
*
|
||
* @return mixed Возврат из функции
|
||
*/
|
||
protected static function handle(callable $function = null, mixed ...$vars): mixed
|
||
{
|
||
try {
|
||
if (self::browser() instanceof Guzzle) {
|
||
// Браузер инициализирован
|
||
|
||
if (self::authorization() && isset(self::$session)) {
|
||
// Аутентифицирован и авторизован
|
||
|
||
return $function(...$vars);
|
||
} else {
|
||
throw new Exception('Аккаунт не авторизирован', 401);
|
||
}
|
||
} else {
|
||
throw new Exception('Браузер не инициализирован', 500);
|
||
}
|
||
} catch (GuzzleException $e) {
|
||
throw new Exception($e->getResponse()->getBody()->getContents() ?? 'Не удалось инициализировать инстанцию для работы с API ДеловыеЛинии', 500, $e->getPrevious());
|
||
} catch (Exception $e) {
|
||
throw new Exception($e->getMessage() ?? 'Не удалось инициализировать инстанцию для работы с API ДеловыеЛинии', 500, $e->getPrevious());
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Чтение или инициализация браузера
|
||
*
|
||
* @return Guzzle Инстанция
|
||
*/
|
||
protected static function browser(): Guzzle
|
||
{
|
||
return self::$browser ?? self::$browser = new Guzzle([
|
||
'base_uri' => 'https://api.dellin.ru/'
|
||
]);
|
||
}
|
||
}
|