239 lines
7.7 KiB
PHP
239 lines
7.7 KiB
PHP
<?php
|
||
|
||
declare(strict_types=1);
|
||
|
||
namespace app\common\library;
|
||
|
||
use app\common\model\MallItem;
|
||
use app\common\model\MallOrder;
|
||
use app\common\model\MallUserAsset;
|
||
use GuzzleHttp\Client;
|
||
use support\Log;
|
||
|
||
/**
|
||
* 红利订单调用 PlayX bonus/grant(与定时任务、后台手动推送共用)
|
||
*/
|
||
final class MallBonusGrantPush
|
||
{
|
||
/**
|
||
* @return array{ok: bool, message: string}
|
||
*/
|
||
public static function push(MallOrder $order): array
|
||
{
|
||
$conf = config('playx.angpow_import');
|
||
if (!is_array($conf)) {
|
||
return [
|
||
'ok' => false,
|
||
'message' => 'playX angpow_import not configured',
|
||
];
|
||
}
|
||
|
||
$baseUrl = rtrim(strval($conf['base_url'] ?? ''), '/');
|
||
$path = strval($conf['path'] ?? '');
|
||
$merchantCode = strval($conf['merchant_code'] ?? '');
|
||
$authKey = strval($conf['auth_key'] ?? '');
|
||
if ($baseUrl === '' || $path === '' || $merchantCode === '' || $authKey === '') {
|
||
return [
|
||
'ok' => false,
|
||
'message' => 'playX Angpow Import API not configured',
|
||
];
|
||
}
|
||
|
||
$url = $baseUrl . $path;
|
||
|
||
$asset = MallUserAsset::where('playx_user_id', $order->user_id)->find();
|
||
if (!$asset || !is_string($asset->playx_user_id ?? null) || strval($asset->playx_user_id) === '') {
|
||
return [
|
||
'ok' => false,
|
||
'message' => 'User asset not found',
|
||
];
|
||
}
|
||
|
||
$item = MallItem::where('id', $order->mall_item_id)->find();
|
||
if (!$item) {
|
||
return [
|
||
'ok' => false,
|
||
'message' => 'Item not found',
|
||
];
|
||
}
|
||
|
||
$reportDate = strval(time());
|
||
$signatureInput = 'merchant_code=' . $merchantCode . '&report_date=' . $reportDate;
|
||
$signature = self::buildSignature($signatureInput, $authKey);
|
||
if ($signature === null) {
|
||
return [
|
||
'ok' => false,
|
||
'message' => 'Build signature failed',
|
||
];
|
||
}
|
||
|
||
$start = gmdate('Y-m-d\TH:i:s\Z', strtotime(strval($order->start_time)));
|
||
$end = gmdate('Y-m-d\TH:i:s\Z', strtotime(strval($order->end_time)));
|
||
$multiplier = intval($order->multiplier ?? 0);
|
||
if ($multiplier <= 0) {
|
||
$multiplier = 1;
|
||
}
|
||
|
||
$payload = [
|
||
'merchant_code' => $merchantCode,
|
||
'report_date' => $reportDate,
|
||
'angpow' => [
|
||
[
|
||
'member_login' => strval($asset->playx_user_id),
|
||
'start_time' => $start,
|
||
'end_time' => $end,
|
||
'amount' => $order->amount,
|
||
'reward_name' => strval($item->title ?? ''),
|
||
'description' => strval($item->description ?? ''),
|
||
'member_inbox_message' => 'Congratulations! You received an angpow.',
|
||
'category' => strval($item->category ?? ''),
|
||
'category_title' => strval($item->category_title ?? ''),
|
||
'one_time_turnover' => 'yes',
|
||
'multiplier' => $multiplier,
|
||
],
|
||
],
|
||
'currency_visual' => [
|
||
[
|
||
'currency' => strval($conf['currency'] ?? 'MYR'),
|
||
'visual_name' => strval($conf['visual_name'] ?? 'Angpow'),
|
||
],
|
||
],
|
||
];
|
||
|
||
$client = new Client([
|
||
'timeout' => 20,
|
||
'http_errors' => false,
|
||
]);
|
||
|
||
try {
|
||
$res = $client->post($url, [
|
||
'headers' => [
|
||
'Content-Type' => 'application/json',
|
||
'X-Request-Signature' => $signature,
|
||
],
|
||
'json' => $payload,
|
||
]);
|
||
|
||
$httpStatus = 0;
|
||
if (is_object($res) && method_exists($res, 'getStatusCode')) {
|
||
$sc = $res->getStatusCode();
|
||
if (is_int($sc)) {
|
||
$httpStatus = $sc;
|
||
}
|
||
}
|
||
|
||
$rawBody = strval($res->getBody());
|
||
$data = json_decode($rawBody, true);
|
||
if (!is_array($data)) {
|
||
$jsonDetail = 'root not JSON object';
|
||
if (json_last_error() !== JSON_ERROR_NONE) {
|
||
$jem = json_last_error_msg();
|
||
$jsonDetail = is_string($jem) && $jem !== '' ? $jem : 'JSON parse error';
|
||
}
|
||
Log::error(
|
||
'[MallBonusGrantPush] response not a JSON object | order_id=' . $order->id
|
||
. ' | http=' . $httpStatus . ' | ' . $jsonDetail . ' | body=' . self::truncateForLog($rawBody)
|
||
);
|
||
|
||
return [
|
||
'ok' => false,
|
||
'message' => 'Invalid response',
|
||
];
|
||
}
|
||
|
||
if (($data['code'] ?? null) === '0' || ($data['code'] ?? null) === 0) {
|
||
return [
|
||
'ok' => true,
|
||
'message' => '',
|
||
];
|
||
}
|
||
|
||
$remoteMsg = $data['message'] ?? $data['msg'] ?? '';
|
||
if (!is_string($remoteMsg)) {
|
||
$remoteMsg = '';
|
||
}
|
||
if ($remoteMsg === '') {
|
||
$remoteMsg = 'playX angpow import not accepted';
|
||
}
|
||
|
||
$flags = JSON_UNESCAPED_UNICODE;
|
||
if (defined('JSON_INVALID_UTF8_SUBSTITUTE')) {
|
||
$flags = $flags | JSON_INVALID_UTF8_SUBSTITUTE;
|
||
}
|
||
$encoded = json_encode($data, $flags);
|
||
if (!is_string($encoded)) {
|
||
$encoded = $rawBody;
|
||
}
|
||
Log::error(
|
||
'[MallBonusGrantPush] angpow import rejected | order_id=' . $order->id
|
||
. ' | http=' . $httpStatus . ' | parsed=' . self::truncateForLog($encoded)
|
||
);
|
||
|
||
return [
|
||
'ok' => false,
|
||
'message' => $remoteMsg,
|
||
];
|
||
} catch (\Throwable $e) {
|
||
Log::error('[MallBonusGrantPush] request exception | order_id=' . $order->id . ' | ' . $e->getMessage());
|
||
|
||
return [
|
||
'ok' => false,
|
||
'message' => $e->getMessage(),
|
||
];
|
||
}
|
||
}
|
||
|
||
private static function truncateForLog(string $text, int $maxLen = 8000): string
|
||
{
|
||
$s = trim($text);
|
||
if ($s === '') {
|
||
return '';
|
||
}
|
||
$oneLine = preg_replace('/\s+/', ' ', $s);
|
||
$snippet = is_string($oneLine) && $oneLine !== '' ? $oneLine : $s;
|
||
if (function_exists('mb_strlen') && function_exists('mb_substr')) {
|
||
if (mb_strlen($snippet, 'UTF-8') > $maxLen) {
|
||
return mb_substr($snippet, 0, $maxLen, 'UTF-8') . '…';
|
||
}
|
||
|
||
return $snippet;
|
||
}
|
||
if (strlen($snippet) > $maxLen) {
|
||
return substr($snippet, 0, $maxLen) . '…';
|
||
}
|
||
|
||
return $snippet;
|
||
}
|
||
|
||
private static function buildSignature(string $input, string $authKey): ?string
|
||
{
|
||
$keyBytes = null;
|
||
|
||
$maybeBase64 = base64_decode($authKey, true);
|
||
if ($maybeBase64 !== false && $maybeBase64 !== '') {
|
||
$keyBytes = $maybeBase64;
|
||
}
|
||
|
||
if ($keyBytes === null) {
|
||
$isHex = ctype_xdigit($authKey) && (strlen($authKey) % 2 === 0);
|
||
if ($isHex) {
|
||
$hex = hex2bin($authKey);
|
||
if ($hex !== false && $hex !== '') {
|
||
$keyBytes = $hex;
|
||
}
|
||
}
|
||
}
|
||
|
||
if ($keyBytes === null) {
|
||
$keyBytes = $authKey;
|
||
}
|
||
|
||
$raw = hash_hmac('sha1', $input, $keyBytes, true);
|
||
if (!is_string($raw) || $raw === '') {
|
||
return null;
|
||
}
|
||
|
||
return base64_encode($raw);
|
||
}
|
||
}
|