Commit 0191d305 by Евгений

Улучшение #19452

Учет баллов при работе с АПИ
1 parent 0cea829e
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
class refreshLimits extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'command:name';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Command description';
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*
* @return int
*/
public function handle()
{
return 0;
}
}
...@@ -24,7 +24,7 @@ class Kernel extends ConsoleKernel ...@@ -24,7 +24,7 @@ class Kernel extends ConsoleKernel
*/ */
protected function schedule(Schedule $schedule) protected function schedule(Schedule $schedule)
{ {
// $schedule->command('inspire')->hourly(); $schedule->command('refreshLimits')->hourly();
} }
/** /**
......
<?php
namespace App\Jobs;
use App\Service\AdsHandler;
use App\Service\API;
use App\Service\Contract\APIRequest;
use App\Service\HeaderLimits;
use App\Service\Limits;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
class ProcessCallAPI implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
private $api;
private $limitId;
/**
* Create a new job instance.
*
* @return void
*/
public function __construct(int $limitId, APIRequest $api)
{
$this->limitId = $limitId;
$this->api = $api;
}
/**
* Execute the job.
*
* @return void
*/
public function handle()
{
$limits = Limits::getInstance($this->api->getToken());
try{
$api = API::getInstance($this->api);
$response = $api->execute();
$limits->acceptRezerv($this->limitId, new HeaderLimits($response->headers()) );
//TODO: обработать результат
AdsHandler::getInstance($this->api)->handle($response);
}catch(\Exception $e ){
//TODO: надо отдельно выделить ошибки вызовов, за которые списываются баллы
//https://yandex.ru/dev/direct/doc/dg/concepts/errors.html
$limits->removeRezerv($this->limitId);
}
}
}
<?php
namespace App\Jobs;
use App\Service\Contract\APIRequest;
use App\Service\Limits;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldBeUnique;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
class ProcessCallLimitedAPI implements ShouldQueue, ShouldBeUnique
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
private $api;
/**
* Create a new job instance.
*
* @return void
*/
public function __construct(APIRequest $api)
{
$this->api = $api;
}
/**
* Execute the job.
*
* @return void
*/
public function handle()
{
$limits = Limits::getInstance($this->api->getToken());
//получаем сколько объектов можем обработать
$objects = $limits->countObjectsLimit($this->api);
if (!$objects){
//нет свободных баллов, замораживаем до следующего часа
$this->reRunHour();
return;
}
try{
//резервируем на это количетсво
$limitId = $limits->doRezerv($this->api, $objects);
}catch(\Exception $e){
//нет свободных баллов, замораживаем до следующего часа
$this->reRunHour();
return;
}
//вызов АПИ разбиваем на несколько, если коненчо не хватает баллов одним запросом все получить
if ($api = $this->api->chunk($objects)){
//вызываем для остальных объектов вызов АПИ
self::dispatch( new ProcessCallLimitedAPI($this->api->getToken(), $api));
}
//вызываем очередь получения данных от АПИ
//туда передаем сохраненный лимит
//там уже либо будет удален, если не удастся выполнить запрос
//либо обновятся данные
self::dispatch(new ProcessCallAPI($limitId, $this->api));
}
/**
* The unique ID of the job.
*
* @return string
*/
public function uniqueId()
{
return $this->api->getToken()->id;
}
private function reRunHour(){
self::dispatch( new ProcessCallLimitedAPI($this->api))->delay(now()->addMinutes(60-date("i")));
}
}
...@@ -22,4 +22,9 @@ class Tokens extends Model ...@@ -22,4 +22,9 @@ class Tokens extends Model
$query->where('api', $api); $query->where('api', $api);
}); });
} }
public function limits()
{
return $this->hasMany(Limits::class, 'token', 'id')->orderBy('updated_at', 'DESC');
}
} }
<?php <?php
namespace App\Service; namespace App\Service;
use GuzzleHttp\Client; use App\Service\Contract\APIRequest;
use Illuminate\Http\Client\Response;
class API implements \App\Service\Contract\API { class API implements \App\Service\Contract\API {
protected $client; protected $request;
protected function __construct(){ protected function __construct(APIRequest $request = null){
$this->client = new Client(); if ($request){
$this->request = $request;
}
} }
static function getInstance($api){ static function getInstance(APIRequest $request = null){
switch($api){ switch($request->getApi()){
case 'yd': case 'yd':
return new YandexDirect(); return new YandexDirect($request);
break; break;
} }
} }
...@@ -35,4 +38,14 @@ class API implements \App\Service\Contract\API { ...@@ -35,4 +38,14 @@ class API implements \App\Service\Contract\API {
function extractToken($data){ function extractToken($data){
return ''; return '';
} }
function setRequest(APIRequest $request)
{
$this->request = $request;
}
function execute(): Response
{
}
} }
<?php
namespace App\Service;
use App\Models\Tokens;
class APIRequest implements \App\Service\Contract\APIRequest {
function setService(string $service)
{
// TODO: Implement setService() method.
}
function getService()
{
// TODO: Implement getService() method.
}
function setMethod(string $method)
{
// TODO: Implement setMethod() method.
}
function getMethod()
{
// TODO: Implement getMethod() method.
}
function setParams(array $params)
{
// TODO: Implement setParams() method.
}
function getParams()
{
// TODO: Implement getParams() method.
}
function setToken(int $token)
{
// TODO: Implement setToken() method.
}
function getToken(): Tokens
{
// TODO: Implement getToken() method.
}
function setApi(string $api)
{
// TODO: Implement setApi() method.
}
function getApi(): string
{
// TODO: Implement getApi() method.
}
function chunk($objects): \App\Service\Contract\APIRequest
{
// TODO: Implement chunk() method.
}
}
<?php
namespace App\Service;
use App\Service\Contract\APIRequest;
class AdsHandler{
protected static $_instance;
protected function __constructor(APIRequest $request = null){
}
public static function getInstance(APIRequest $request = null){
self::$_instance = new self($request);
}
}
<?php <?php
namespace App\Service\Contract; namespace App\Service\Contract;
use Illuminate\Http\Client\Response;
interface API{ interface API{
function getAuthLink(); function getAuthLink();
function getToken($code); function getToken($code);
function setRequest(APIRequest $request);
function execute(): Response;
} }
<?php
namespace App\Service\Contract;
use App\Models\Tokens;
interface APIRequest{
function setService(string $service);
function getService();
function setMethod(string $method);
function getMethod();
function setParams(array $params);
function getParams();
function setToken(int $token);
function getToken(): Tokens;
function setApi(string $api);
function getApi(): string;
function chunk($objects): APIRequest;
}
...@@ -2,7 +2,6 @@ ...@@ -2,7 +2,6 @@
namespace App\Service\Contract; namespace App\Service\Contract;
interface HeaderLimits{ interface HeaderLimits{
function handleHeader(Array $headers);
function getDayLimit(): int; function getDayLimit(): int;
function getCurrentLimit(): int; function getCurrentLimit(): int;
function getSpentLimit(): int; function getSpentLimit(): int;
......
...@@ -4,8 +4,9 @@ namespace App\Service\Contract; ...@@ -4,8 +4,9 @@ namespace App\Service\Contract;
interface Limits{ interface Limits{
function current(): int; function current(): int;
function DayLimit(): int; function DayLimit(): int;
function countObjectsLimit(String $service, String $method): int; function countObjectsLimit(APIRequest $request): int;
function doRezerv(String $service, String $method, int $limit): int; function doRezerv(APIRequest $request, int $limit): int;
function removeRezerv(int $id): int; function removeRezerv(int $id): int;
function updateLimits(HeaderLimits $limits); function updateLimits(HeaderLimits $limits);
function acceptRezerv($id, HeaderLimits $limits);
} }
<?php
namespace App\Service;
class Costs{
private $costs = [
'AdExtensions' => [
'add' => [
5,1
],
'delete' => [
10,1
],
'get' => [
5,1
],
],
'AdGroups' => [
'add' => [
20,20
],
'delete' => [
10,0
],
'get' => [
15,1
],
'update' => [
20,20
],
],
'AdImages' => [
'add' => [
20,20
],
'delete' => [
10,0
],
'get' => [
15,1
],
],
'Ads' => [
'add' => [
20,20
],
'archive' => [
15,0
],
'delete' => [
10,0
],
'get' => [
15,1
],
'moderate' => [
15,0
],
'resume' => [
15,0
],
'suspend' => [
15,0
],
'unarchive' => [
40,0
],
'update' => [
20,20
],
],
'AgencyClients' => [
'add' => [
10,1
],
'get' => [
10,1
],
'update' => [
10,1
],
],
'AudienceTargets' => [
'add' => [
10,2
],
'delete' => [
10,2
],
'get' => [
1,1
],
'resume' => [
10,2
],
'setBids' => [
10,2
],
'suspend' => [
10,2
],
],
'Bids' => [
'get' => [
15,3/2000
],
'set' => [
25,0
],
'setAuto' => [
25,0
],
],
'BidModifiers' => [
'add' => [
15,1
],
'delete' => [
15,0
],
'get' => [
1,0
],
'set' => [
2,0
],
'toggle' => [
15,0
],
],
'Businesses' => [
'get' => [
10,1
],
],
'Campaigns' => [
'add' => [
10,5
],
'archive' => [
10,5
],
'delete' => [
10,2
],
'get' => [
10,1
],
'resume' => [
10,5
],
'suspend' => [
10,5
],
'unarchive' => [
10,5
],
'update' => [
10,3
],
],
'Changes' => [
'check' => [
10,0
],
'checkCampaigns' => [
10,0
],
'checkDictionaries' => [
10,0
],
],
'Clients' => [
'get' => [
10,0
],
'update' => [
10,1
],
],
'Creatives' => [
'get' => [
15,1
],
],
'Dictionaries' => [
'get' => [
1,0
],
],
'DynamicTextAdTargets' => [
'add' => [
20,5
],
'delete' => [
10,1
],
'get' => [
15,1
],
'resume' => [
10,1
],
'setBids' => [
25,0
],
'suspend' => [
10,1
],
],
'Feeds' => [
'add' => [
20,20
],
'delete' => [
10,0
],
'get' => [
15,1
],
'update' => [
20,20
],
],
'KeywordBids' => [
'get' => [
15,3/2000
],
'set' => [
25,0
],
'setAuto' => [
25,0
],
],
'Keywords' => [
'add' => [
20,2
],
'delete' => [
10,1
],
'get' => [
15,3,1
],
'resume' => [
15,0
],
'suspend' => [
15,0
],
'update' => [
20,2
],
],
'KeywordsResearch' => [
'deduplicate' => [
10,0
],
'hasSearchVolume' => [
1,0
],
],
'Leads' => [
'get' => [
1,1
],
],
'NegativeKeywordSharedSets' => [
'add' => [
20,20
],
'delete' => [
10,0
],
'get' => [
15,1
],
'update' => [
20,20
],
],
'RetargetingLists' => [
'add' => [
10,2
],
'delete' => [
10,2
],
'get' => [
1,1
],
'update' => [
10,2
],
],
'Sitelinks' => [
'add' => [
20,20
],
'delete' => [
10,0
],
'get' => [
15,1
],
],
'SmartAdTargets' => [
'add' => [
20,5
],
'delete' => [
10,1
],
'get' => [
15,1
],
'resume' => [
10,1
],
'setBids' => [
10,0
],
'suspend' => [
10,1
],
'update' => [
10,1
],
],
'TurboPages' => [
'get' => [
15,1
],
],
'VCards' => [
'add' => [
20,20
],
'delete' => [
10,0
],
'get' => [
15,1
],
],
];
function getCostCall(\App\Service\Contract\APIRequest $request){
return $this->costs[$request->getService()][$request->getMethod()][0] ?? 0;
}
function getCostObject(\App\Service\Contract\APIRequest $request){
if (method_exists($this, $request->getService().ucfirst($request->getMethod()).'Logic')){
return $this->{$request->getService().ucfirst($request->getMethod()).'Logic'}($request->getParams());
}
return $this->costs[$request->getService()][$request->getMethod()][1] ?? 0;
}
private function KeywordsGetLogic($params){
if (
in_array('Productivity', $params['FieldNames']) ||
in_array('StatisticsSearch', $params['FieldNames']) ||
in_array('StatisticsNetwork', $params['FieldNames'])
){
return $this->costs['Keywords']['get'][1]/2000;
}
return $this->costs['Keywords']['get'][2]/2000;
}
}
...@@ -7,7 +7,7 @@ class HeaderLimits implements \App\Service\Contract\HeaderLimits{ ...@@ -7,7 +7,7 @@ class HeaderLimits implements \App\Service\Contract\HeaderLimits{
private $currentLimit = 0; private $currentLimit = 0;
private $spentLimit = 0; private $spentLimit = 0;
function handleHeader(Array $headers) function __constructor(Array $headers)
{ {
if (!isset($headers['Units'])){ if (!isset($headers['Units'])){
throw new Exception('Не найден заголовок с баллами'); throw new Exception('Не найден заголовок с баллами');
......
...@@ -3,18 +3,22 @@ namespace App\Service; ...@@ -3,18 +3,22 @@ namespace App\Service;
use App\Models\Tokens; use App\Models\Tokens;
use App\Service\Contract\HeaderLimits; use App\Service\Contract\HeaderLimits;
use Illuminate\Support\Facades\DB;
class Limits implements \App\Service\Contract\Limits { class Limits implements \App\Service\Contract\Limits {
private $_instance; private static $_instance;
private $token; private $token;
private $limitCosts;
private function __constructor(Tokens $token){ private function __constructor(Tokens $token){
$this->token = $token; $this->token = $token;
$this->limitCosts = new Costs();
} }
public function getInstance($token){ public static function getInstance($token): Limits
$this->_instance = new self($token); {
self::$_instance = new self($token);
} }
function current(): int function current(): int
...@@ -27,29 +31,117 @@ class Limits implements \App\Service\Contract\Limits { ...@@ -27,29 +31,117 @@ class Limits implements \App\Service\Contract\Limits {
$this->token->limits->first()->day; $this->token->limits->first()->day;
} }
function countObjectsLimit(string $service, string $method): int function countObjectsLimit(\App\Service\Contract\APIRequest $request): int
{ {
$cost = $this->getCost($service, $method); $cost = $this->limitCosts->getCostObject($request);
return floor($this->current() / $cost); if ($this->limitCosts->getCostCall($request) > $this->current()){
return 0;
}
return $cost > 0 ? floor(($this->current() - $this->limitCosts->getCostCall($request)) / $cost) : -1;
} }
function doRezerv(string $service, string $method, int $limit): int /**
* @param string $service
* @param string $method
* @param int $limit
* @return int
* @throws \Exception
* предполагается что класс работает в очереди.
* Иначе может быть что одновременно будет два резервирования с одним и тем же остатком.
*/
function doRezerv(\App\Service\Contract\APIRequest $request, int $objects): int
{ {
// TODO: Implement doRezerv() method. $limit = $this->getSpent($objects, $request);
if ($this->token->limit<$limit){
throw new \Exception('Недостаточно баллов');
}
DB::beginTransaction();
try{
$rezerv = new \App\Models\Limits();
$rezerv->token = $this->token->id;
$rezerv->service = $request->getService();
$rezerv->method = $request->getMethod();
$rezerv->spent = $limit;
$rezerv->current = $this->token->limit;
$rezerv->reserved = 1;
$rezerv->save();
$this->token->limit -= $limit;
$this->token->save();
DB::commit();
}catch(\Exception $e){
DB::rollBack();
throw $e;
}
return $rezerv->id;
} }
function removeRezerv(int $id): int function removeRezerv(int $id): int
{ {
// TODO: Implement removeRezerv() method. DB::beginTransaction();
try{
$limit = \App\Models\Limits::findOrFail($id);
$this->token->limit += $limit->spent;
$this->token->save();
$limit->delete();
DB::commit();
}catch(\Exception $e){
DB::rollBack();
throw $e;
}
}
function acceptRezerv($id, HeaderLimits $limits){
DB::beginTransaction();
try{
$this->token->limit = $limits->getCurrentLimit();
$this->token->save();
$limit = \App\Models\Limits::findOrFail($id);
$limit->spent = $limits->getSpentLimit();
$limit->current = $limits->getCurrentLimit();
$limit->day = $limits->getDayLimit();
$limit->reserved = 0;
$limit->save();
DB::commit();
}catch(\Exception $e){
DB::rollBack();
throw $e;
}
} }
function updateLimits(HeaderLimits $limits) function updateLimits(HeaderLimits $limits)
{ {
// TODO: Implement updateLimits() method. DB::beginTransaction();
} try{
$this->token->limit = $limits->getCurrentLimit();
$this->token->save();
private function getCost(){ $limit = new \App\Models\Limits();
$limit->token = $this->token->id;
$limit->spent = $limits->getSpentLimit();
$limit->current = $limits->getCurrentLimit();
$limit->day = $limits->getDayLimit();
$limit->reserved = 0;
$limit->save();
DB::commit();
}catch(\Exception $e){
DB::rollBack();
throw $e;
}
}
private function getSpent($objects, \App\Service\Contract\APIRequest $request): int
{
$cost = $this->limitCosts->getCostObject($request);
return $objects * $cost + $this->limitCosts->getCostCall($request);
} }
} }
<?php <?php
namespace App\Service; namespace App\Service;
use Illuminate\Support\Facades\Http;
class YandexDirect extends API{ class YandexDirect extends API{
private $url = 'https://oauth.yandex.ru/token'; private $url = 'https://oauth.yandex.ru/token';
...@@ -11,13 +12,11 @@ class YandexDirect extends API{ ...@@ -11,13 +12,11 @@ class YandexDirect extends API{
} }
function getToken($code) { function getToken($code) {
$data = $this->client->post($this->getTokenUrl(), [ $data = Http::post($this->getTokenUrl(), [
'form_params' => [ 'grant_type' => 'authorization_code',
'grant_type' => 'authorization_code', 'code' => $code,
'code' => $code, 'client_id' => config('api.yandex.id'),
'client_id' => config('api.yandex.id'), 'client_secret' => config('api.yandex.password'),
'client_secret' => config('api.yandex.password'),
]
]); ]);
return $this->extractToken($data); return $this->extractToken($data);
} }
...@@ -27,14 +26,14 @@ class YandexDirect extends API{ ...@@ -27,14 +26,14 @@ class YandexDirect extends API{
} }
function extractToken($data){ function extractToken($data){
$token = json_decode($data->getBody())->access_token; $token = $data->json()->access_token;
$login = $this->getLoginByToken($token); $login = $this->getLoginByToken($token);
return ['token' => $token, 'login' => $login]; return ['token' => $token, 'login' => $login];
} }
public function getLoginByToken($token) { public function getLoginByToken($token) {
$url = "https://login.yandex.ru/info?format=json&oauth_token={$token}"; $url = "https://login.yandex.ru/info?format=json&oauth_token={$token}";
$data = json_decode($this->client->get($url)->getBody()); $data = Http::get($url)->json();
return isset($data->login) ? $data->login : false; return isset($data->login) ? $data->login : false;
} }
......
...@@ -64,7 +64,7 @@ return [ ...@@ -64,7 +64,7 @@ return [
'connection' => 'default', 'connection' => 'default',
'queue' => env('REDIS_QUEUE', 'default'), 'queue' => env('REDIS_QUEUE', 'default'),
'retry_after' => 90, 'retry_after' => 90,
'block_for' => null, 'block_for' => 5,
], ],
], ],
......
...@@ -39,8 +39,10 @@ class CreateLimitsTable extends Migration ...@@ -39,8 +39,10 @@ class CreateLimitsTable extends Migration
*/ */
public function down() public function down()
{ {
Schema::table('tokens', function (Blueprint $table) { Schema::table('limits', function (Blueprint $table) {
$table->dropForeign('limits_token_foreign'); $table->dropForeign('limits_token_foreign');
});
Schema::table('tokens', function (Blueprint $table) {
$table->dropColumn('limit'); $table->dropColumn('limit');
}); });
Schema::dropIfExists('limits'); Schema::dropIfExists('limits');
......
php artisan queue:work &
php artisan queue:work &
php artisan queue:work &
php artisan queue:work &
php artisan queue:work &
php artisan schedule:work &
Markdown is supported
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!