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
*/
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
$query->where('api', $api);
});
}
public function limits()
{
return $this->hasMany(Limits::class, 'token', 'id')->orderBy('updated_at', 'DESC');
}
}
<?php
namespace App\Service;
use GuzzleHttp\Client;
use App\Service\Contract\APIRequest;
use Illuminate\Http\Client\Response;
class API implements \App\Service\Contract\API {
protected $client;
protected $request;
protected function __construct(){
$this->client = new Client();
protected function __construct(APIRequest $request = null){
if ($request){
$this->request = $request;
}
}
static function getInstance($api){
switch($api){
static function getInstance(APIRequest $request = null){
switch($request->getApi()){
case 'yd':
return new YandexDirect();
return new YandexDirect($request);
break;
}
}
......@@ -35,4 +38,14 @@ class API implements \App\Service\Contract\API {
function extractToken($data){
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
namespace App\Service\Contract;
use Illuminate\Http\Client\Response;
interface API{
function getAuthLink();
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 @@
namespace App\Service\Contract;
interface HeaderLimits{
function handleHeader(Array $headers);
function getDayLimit(): int;
function getCurrentLimit(): int;
function getSpentLimit(): int;
......
......@@ -4,8 +4,9 @@ namespace App\Service\Contract;
interface Limits{
function current(): int;
function DayLimit(): int;
function countObjectsLimit(String $service, String $method): int;
function doRezerv(String $service, String $method, int $limit): int;
function countObjectsLimit(APIRequest $request): int;
function doRezerv(APIRequest $request, int $limit): int;
function removeRezerv(int $id): int;
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{
private $currentLimit = 0;
private $spentLimit = 0;
function handleHeader(Array $headers)
function __constructor(Array $headers)
{
if (!isset($headers['Units'])){
throw new Exception('Не найден заголовок с баллами');
......
......@@ -3,18 +3,22 @@ namespace App\Service;
use App\Models\Tokens;
use App\Service\Contract\HeaderLimits;
use Illuminate\Support\Facades\DB;
class Limits implements \App\Service\Contract\Limits {
private $_instance;
private static $_instance;
private $token;
private $limitCosts;
private function __constructor(Tokens $token){
$this->token = $token;
$this->limitCosts = new Costs();
}
public function getInstance($token){
$this->_instance = new self($token);
public static function getInstance($token): Limits
{
self::$_instance = new self($token);
}
function current(): int
......@@ -27,29 +31,117 @@ class Limits implements \App\Service\Contract\Limits {
$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);
return floor($this->current() / $cost);
$cost = $this->limitCosts->getCostObject($request);
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
{
// 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)
{
// 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
namespace App\Service;
use Illuminate\Support\Facades\Http;
class YandexDirect extends API{
private $url = 'https://oauth.yandex.ru/token';
......@@ -11,13 +12,11 @@ class YandexDirect extends API{
}
function getToken($code) {
$data = $this->client->post($this->getTokenUrl(), [
'form_params' => [
$data = Http::post($this->getTokenUrl(), [
'grant_type' => 'authorization_code',
'code' => $code,
'client_id' => config('api.yandex.id'),
'client_secret' => config('api.yandex.password'),
]
]);
return $this->extractToken($data);
}
......@@ -27,14 +26,14 @@ class YandexDirect extends API{
}
function extractToken($data){
$token = json_decode($data->getBody())->access_token;
$token = $data->json()->access_token;
$login = $this->getLoginByToken($token);
return ['token' => $token, 'login' => $login];
}
public function getLoginByToken($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;
}
......
......@@ -64,7 +64,7 @@ return [
'connection' => 'default',
'queue' => env('REDIS_QUEUE', 'default'),
'retry_after' => 90,
'block_for' => null,
'block_for' => 5,
],
],
......
......@@ -39,8 +39,10 @@ class CreateLimitsTable extends Migration
*/
public function down()
{
Schema::table('tokens', function (Blueprint $table) {
Schema::table('limits', function (Blueprint $table) {
$table->dropForeign('limits_token_foreign');
});
Schema::table('tokens', function (Blueprint $table) {
$table->dropColumn('limit');
});
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!