Files
webman-buildadmin-mall/app/process/PlayxJobs.php

281 lines
9.8 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
namespace app\process;
use app\common\model\MallItem;
use app\common\model\MallPlayxOrder;
use app\common\model\MallPlayxUserAsset;
use GuzzleHttp\Client;
use Workerman\Timer;
use Workerman\Worker;
/**
* PlayX 积分商城闭环任务
* - 轮询交易终态ACCEPTED -> COMPLETED/REJECTED
* - 对可重试失败进行重发(依赖 external_transaction_id 幂等)
*/
class PlayxJobs
{
protected Client $http;
public function __construct()
{
// 确保定时任务只在一个 worker 上运行
if (!Worker::getAllWorkers()) {
return;
}
$this->http = new Client([
'timeout' => 20,
'http_errors' => false,
]);
Timer::add(60, [$this, 'pollTransactionStatus']);
Timer::add(60, [$this, 'retryFailedGrants']);
}
/**
* 轮询:已 accepted 的订单,查询终态
*/
public function pollTransactionStatus(): void
{
$baseUrl = strval(config('playx.api.base_url', ''));
if ($baseUrl === '') {
return;
}
$path = strval(config('playx.api.transaction_status_url', '/api/v1/transaction/status'));
$url = rtrim($baseUrl, '/') . $path;
$list = MallPlayxOrder::where('grant_status', MallPlayxOrder::GRANT_ACCEPTED)
->where('status', MallPlayxOrder::STATUS_PENDING)
->order('id', 'desc')
->limit(50)
->select();
foreach ($list as $order) {
/** @var MallPlayxOrder $order */
try {
$res = $this->http->get($url, [
'query' => [
'externalTransactionId' => $order->external_transaction_id,
],
]);
$data = json_decode(strval($res->getBody()), true) ?? [];
$pxStatus = $data['status'] ?? '';
if ($pxStatus === MallPlayxOrder::STATUS_COMPLETED) {
$order->status = MallPlayxOrder::STATUS_COMPLETED;
$order->save();
continue;
}
if ($pxStatus === 'FAILED' || $pxStatus === MallPlayxOrder::STATUS_REJECTED) {
// 仅在从 PENDING 转 REJECTED 时退分,避免重复入账
$order->status = MallPlayxOrder::STATUS_REJECTED;
$order->grant_status = MallPlayxOrder::GRANT_FAILED_FINAL;
$order->fail_reason = strval($data['message'] ?? 'PlayX transaction failed');
$order->save();
$this->refundPoints($order);
continue;
}
// 其他情况视为仍在队列/PENDING保持不变
} catch (\Throwable $e) {
// 查询失败不影响状态,下一轮重试
}
}
}
/**
* 重试NOT_SENT / FAILED_RETRYABLE按 retry_count 间隔)
*/
public function retryFailedGrants(): void
{
$baseUrl = strval(config('playx.api.base_url', ''));
if ($baseUrl === '') {
return;
}
$bonusPath = strval(config('playx.api.bonus_grant_url', '/api/v1/bonus/grant'));
$withdrawPath = strval(config('playx.api.balance_credit_url', '/api/v1/balance/credit'));
$bonusUrl = rtrim($baseUrl, '/') . $bonusPath;
$withdrawUrl = rtrim($baseUrl, '/') . $withdrawPath;
$maxRetry = 3;
$list = MallPlayxOrder::whereIn('grant_status', [
MallPlayxOrder::GRANT_NOT_SENT,
MallPlayxOrder::GRANT_FAILED_RETRYABLE,
])
->where('status', MallPlayxOrder::STATUS_PENDING)
->where('retry_count', '<', $maxRetry)
->order('id', 'desc')
->limit(50)
->select();
foreach ($list as $order) {
/** @var MallPlayxOrder $order */
$allow = $this->allowRetryByInterval($order);
if (!$allow) {
continue;
}
$order->retry_count = intval($order->retry_count ?? 0) + 1;
try {
$this->sendGrantByOrder($order, $bonusUrl, $withdrawUrl, $maxRetry);
} catch (\Throwable $e) {
$order->fail_reason = $e->getMessage();
if (intval($order->retry_count) >= $maxRetry) {
$order->grant_status = MallPlayxOrder::GRANT_FAILED_FINAL;
$order->status = MallPlayxOrder::STATUS_REJECTED;
$order->save();
$this->refundPoints($order);
} else {
$order->grant_status = MallPlayxOrder::GRANT_FAILED_RETRYABLE;
$order->save();
}
}
}
}
private function allowRetryByInterval(MallPlayxOrder $order): bool
{
if ($order->grant_status === MallPlayxOrder::GRANT_NOT_SENT) {
return true;
}
$retryCount = intval($order->retry_count ?? 0);
$updatedAt = intval($order->update_time ?? 0);
$diff = time() - $updatedAt;
// retry_count: 已失败重试次数
// 0 -> 下次重试:等待 1min
// 1 -> 下次重试:等待 5min
// 2 -> 下次重试:等待 15min
if ($retryCount === 0 && $diff >= 60) {
return true;
}
if ($retryCount === 1 && $diff >= 300) {
return true;
}
if ($retryCount >= 2 && $diff >= 900) {
return true;
}
return false;
}
private function sendGrantByOrder(MallPlayxOrder $order, string $bonusUrl, string $withdrawUrl, int $maxRetry): void
{
$item = null;
if ($order->mallItem) {
$item = $order->mallItem;
} else {
$item = MallItem::where('id', $order->mall_item_id)->find();
}
if ($order->type === MallPlayxOrder::TYPE_BONUS) {
$rewardName = $item ? strval($item->title) : '';
$category = $item ? strval($item->category) : 'daily';
$categoryTitle = $item ? strval($item->category_title) : '';
$multiplier = intval($order->multiplier ?? 0);
if ($multiplier <= 0) {
$multiplier = 1;
}
$requestId = 'mall_retry_bonus_' . uniqid();
$res = $this->http->post($bonusUrl, [
'json' => [
'request_id' => $requestId,
'externalTransactionId' => $order->external_transaction_id,
'user_id' => $order->user_id,
'amount' => $order->amount,
'rewardName' => $rewardName,
'category' => $category,
'categoryTitle' => $categoryTitle,
'multiplier' => $multiplier,
],
]);
$data = json_decode(strval($res->getBody()), true) ?? [];
if ($res->getStatusCode() === 200 && ($data['status'] ?? '') === 'accepted') {
$order->grant_status = MallPlayxOrder::GRANT_ACCEPTED;
$order->playx_transaction_id = strval($data['playx_transaction_id'] ?? '');
$order->save();
return;
}
$order->fail_reason = strval($data['message'] ?? 'PlayX bonus grant not accepted');
if (intval($order->retry_count) >= $maxRetry) {
$order->grant_status = MallPlayxOrder::GRANT_FAILED_FINAL;
$order->status = MallPlayxOrder::STATUS_REJECTED;
$order->save();
$this->refundPoints($order);
return;
}
$order->grant_status = MallPlayxOrder::GRANT_FAILED_RETRYABLE;
$order->save();
return;
}
if ($order->type === MallPlayxOrder::TYPE_WITHDRAW) {
$multiplier = intval($order->multiplier ?? 0);
if ($multiplier <= 0) {
$multiplier = 1;
}
$requestId = 'mall_retry_withdraw_' . uniqid();
$res = $this->http->post($withdrawUrl, [
'json' => [
'request_id' => $requestId,
'externalTransactionId' => $order->external_transaction_id,
'user_id' => $order->user_id,
'amount' => $order->amount,
'multiplier' => $multiplier,
],
]);
$data = json_decode(strval($res->getBody()), true) ?? [];
if ($res->getStatusCode() === 200 && ($data['status'] ?? '') === 'accepted') {
$order->grant_status = MallPlayxOrder::GRANT_ACCEPTED;
$order->playx_transaction_id = strval($data['playx_transaction_id'] ?? '');
$order->save();
return;
}
$order->fail_reason = strval($data['message'] ?? 'PlayX balance credit not accepted');
if (intval($order->retry_count) >= $maxRetry) {
$order->grant_status = MallPlayxOrder::GRANT_FAILED_FINAL;
$order->status = MallPlayxOrder::STATUS_REJECTED;
$order->save();
$this->refundPoints($order);
return;
}
$order->grant_status = MallPlayxOrder::GRANT_FAILED_RETRYABLE;
$order->save();
return;
}
// PHYSICAL 目前由后台手工发货/驳回,不参与 PlayX 发放重试
}
private function refundPoints(MallPlayxOrder $order): void
{
if ($order->points_cost <= 0) {
return;
}
$asset = MallPlayxUserAsset::where('user_id', $order->user_id)->find();
if (!$asset) {
return;
}
// 已从用户可用积分中扣除,本次失败退回可用积分
$asset->available_points += intval($order->points_cost);
$asset->save();
}
}