|
|
@@ -4,7 +4,9 @@ namespace App\Services;
|
|
|
|
|
|
use App\Models\ThirdGameOrder as ThirdGameOrderModel;
|
|
|
use App\Services\AdminRealtime\AdminRealtime;
|
|
|
+use App\Services\Sharing\EffectiveBet as Money;
|
|
|
use Carbon\Carbon;
|
|
|
+use Illuminate\Database\UniqueConstraintViolationException;
|
|
|
use Illuminate\Support\Facades\DB;
|
|
|
|
|
|
class ThirdGameOrderService
|
|
|
@@ -40,16 +42,16 @@ class ThirdGameOrderService
|
|
|
int $limit = 2000
|
|
|
): array {
|
|
|
return $this->persistProviderResult(
|
|
|
- $this->provider->historyOrders($startTime, $endTime, $page, $limit)
|
|
|
+ $this->provider->historyOrders($startTime, $endTime, $page, $limit), $page, $limit
|
|
|
);
|
|
|
}
|
|
|
|
|
|
public function syncRealtime(int $page = 1, int $limit = 2000): array
|
|
|
{
|
|
|
- return $this->persistProviderResult($this->provider->realtimeOrders($page, $limit));
|
|
|
+ return $this->persistProviderResult($this->provider->realtimeOrders($page, $limit), $page, $limit);
|
|
|
}
|
|
|
|
|
|
- private function persistProviderResult(array $result): array
|
|
|
+ private function persistProviderResult(array $result, int $page = 1, int $limit = 2000): array
|
|
|
{
|
|
|
if (empty($result['ok'])) {
|
|
|
return $result;
|
|
|
@@ -66,10 +68,8 @@ class ThirdGameOrderService
|
|
|
static fn(array $order): string => trim((string) ($order['player_id'] ?? '')),
|
|
|
$orders
|
|
|
)))));
|
|
|
- $existing = $this->existingSnapshots($orders);
|
|
|
$now = Carbon::now(config('app.timezone', 'Asia/Shanghai'))->format('Y-m-d H:i:s');
|
|
|
$rows = [];
|
|
|
- $matchedUserIds = [];
|
|
|
|
|
|
foreach ($orders as $order) {
|
|
|
$platform = trim((string) ($order['platform'] ?? ''));
|
|
|
@@ -80,19 +80,15 @@ class ThirdGameOrderService
|
|
|
|
|
|
$playerId = trim((string) ($order['player_id'] ?? ''));
|
|
|
$identity = $this->identity($platform, $playerId, $gameOrderId);
|
|
|
- $user = $users[$playerId] ?? null;
|
|
|
- $previous = $existing[$identity] ?? null;
|
|
|
- $userId = $user->id ?? $previous->user_id ?? null;
|
|
|
- if ($userId !== null) {
|
|
|
- $matchedUserIds[(int) $userId] = true;
|
|
|
+ $updated = $this->dateTime($order['last_update_time'] ?? null);
|
|
|
+ $status = min(255, max(0, (int) ($order['status'] ?? 0)));
|
|
|
+ if (isset($rows[$identity])
|
|
|
+ && $this->isStale($updated, $rows[$identity]['last_update_time'], $rows[$identity]['status'])) {
|
|
|
+ continue;
|
|
|
}
|
|
|
|
|
|
$rows[$identity] = [
|
|
|
'order_key' => $identity,
|
|
|
- 'user_id' => $userId,
|
|
|
- 'member_id' => $user->member_id ?? $previous->member_id ?? null,
|
|
|
- 'username' => (string) ($user->username ?? $previous->username ?? ''),
|
|
|
- 'first_name' => (string) ($user->first_name ?? $previous->first_name ?? ''),
|
|
|
'player_id' => $playerId,
|
|
|
'platform' => $platform,
|
|
|
'currency' => trim((string) ($order['currency'] ?? '')),
|
|
|
@@ -105,29 +101,42 @@ class ThirdGameOrderService
|
|
|
'valid_amount' => $this->decimal($order['valid_amount'] ?? null),
|
|
|
'settled_amount' => $this->decimal($order['settled_amount'] ?? null),
|
|
|
'bet_content' => $this->text($order['bet_content'] ?? ''),
|
|
|
- 'status' => min(255, max(0, (int) ($order['status'] ?? 0))),
|
|
|
+ 'status' => $status,
|
|
|
'game_order_id' => $gameOrderId,
|
|
|
'bet_time' => $this->dateTime($order['bet_time'] ?? null),
|
|
|
- 'last_update_time' => $this->dateTime($order['last_update_time'] ?? null),
|
|
|
- 'created_at' => $previous->created_at ?? $now,
|
|
|
+ 'last_update_time' => $updated,
|
|
|
+ 'created_at' => $now,
|
|
|
'updated_at' => $now,
|
|
|
];
|
|
|
}
|
|
|
|
|
|
+ $synced = 0;
|
|
|
+ $unmatched = 0;
|
|
|
+ $matchedUserIds = [];
|
|
|
if ($rows !== []) {
|
|
|
- $changedKeys = array_keys(array_filter($rows, fn(array $row, string $key): bool
|
|
|
- => $this->snapshotChanged($existing[$key] ?? null, $row), ARRAY_FILTER_USE_BOTH));
|
|
|
- ThirdGameOrderModel::query()->upsert(
|
|
|
- array_values($rows),
|
|
|
- ['order_key'],
|
|
|
- [
|
|
|
- 'user_id', 'member_id', 'username', 'first_name', 'player_id', 'currency',
|
|
|
- 'game_type', 'game_name', 'round_no', 'table_no', 'seat', 'bet_amount',
|
|
|
- 'valid_amount', 'settled_amount', 'bet_content', 'status', 'bet_time',
|
|
|
- 'last_update_time', 'updated_at',
|
|
|
- ]
|
|
|
- );
|
|
|
- $this->recordRealtime($changedKeys, $existing);
|
|
|
+ $commission = app(ThirdGameCommissionService::class);
|
|
|
+ $history = $commission->backfillState();
|
|
|
+ $rates = [];
|
|
|
+ foreach ($rows as $row) {
|
|
|
+ if (strtoupper($row['currency']) === $commission::currency()
|
|
|
+ && $commission::result($row['status'], $row['settled_amount']) === 'loss') {
|
|
|
+ $rates = $commission->rates()['ratios'];
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ ksort($rows);
|
|
|
+ foreach (array_chunk($rows, 200, true) as $batch) {
|
|
|
+ $written = $this->writeOrders($batch, $users, $commission, $rates, $history);
|
|
|
+ $synced += count($written);
|
|
|
+ foreach ($written as $row) {
|
|
|
+ if ($row['user_id'] === null) {
|
|
|
+ ++$unmatched;
|
|
|
+ } else {
|
|
|
+ $matchedUserIds[(int) $row['user_id']] = true;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
$pageNo = (int) ($result['page_no'] ?? $page);
|
|
|
@@ -137,9 +146,9 @@ class ThirdGameOrderService
|
|
|
return [
|
|
|
'ok' => true,
|
|
|
'received' => $received,
|
|
|
- 'synced' => count($rows),
|
|
|
+ 'synced' => $synced,
|
|
|
'matched_users' => count($matchedUserIds),
|
|
|
- 'unmatched_orders' => count(array_filter($rows, static fn(array $row): bool => $row['user_id'] === null)),
|
|
|
+ 'unmatched_orders' => $unmatched,
|
|
|
'total' => $total,
|
|
|
'page_no' => $pageNo,
|
|
|
'page_size' => $pageSize,
|
|
|
@@ -147,6 +156,100 @@ class ThirdGameOrderService
|
|
|
];
|
|
|
}
|
|
|
|
|
|
+ private function writeOrders(array $batch, array $users, ThirdGameCommissionService $commission, array $rates, array $history): array
|
|
|
+ {
|
|
|
+ for ($attempt = 1; ; ++$attempt) {
|
|
|
+ try {
|
|
|
+ [$written, $existing] = DB::transaction(function () use ($batch, $users, $commission, $rates, $history) {
|
|
|
+ $existing = ThirdGameOrderModel::query()->whereIn('order_key', array_keys($batch))
|
|
|
+ ->orderBy('order_key')->lockForUpdate()->get([
|
|
|
+ 'id', 'order_key', 'user_id', 'member_id', 'username', 'first_name', 'currency',
|
|
|
+ 'status', 'settled_amount', 'last_update_time', 'third_game_commission_ratio', 'third_game_commission',
|
|
|
+ ])->keyBy('order_key')->all();
|
|
|
+ $new = [];
|
|
|
+ $updates = [];
|
|
|
+ foreach ($batch as $key => $row) {
|
|
|
+ $previous = $existing[$key] ?? null;
|
|
|
+ if ($previous !== null && $this->isStale($row['last_update_time'],
|
|
|
+ $previous->getRawOriginal('last_update_time'), (int) $previous->getRawOriginal('status'))) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ $user = $users[$row['player_id']] ?? null;
|
|
|
+ $row['user_id'] = $user->id ?? $previous->user_id ?? null;
|
|
|
+ $row['member_id'] = $user->member_id ?? $previous->member_id ?? null;
|
|
|
+ $row['username'] = (string) ($user->username ?? $previous->username ?? '');
|
|
|
+ $row['first_name'] = (string) ($user->first_name ?? $previous->first_name ?? '');
|
|
|
+ $row = $this->applyCommission($row, $previous, $commission, $rates, $history);
|
|
|
+ if ($previous === null) {
|
|
|
+ $new[$key] = $row;
|
|
|
+ } else {
|
|
|
+ $updates[$key] = $row;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if ($new !== []) {
|
|
|
+ // A concurrent insert must be re-read before applying a fee snapshot.
|
|
|
+ ThirdGameOrderModel::query()->insert(array_values($new));
|
|
|
+ }
|
|
|
+ if ($updates !== []) {
|
|
|
+ ThirdGameOrderModel::query()->upsert(array_values($updates), ['order_key'], [
|
|
|
+ 'user_id', 'member_id', 'username', 'first_name', 'player_id', 'currency',
|
|
|
+ 'game_type', 'game_name', 'round_no', 'table_no', 'seat', 'bet_amount',
|
|
|
+ 'valid_amount', 'settled_amount', 'bet_content', 'status', 'bet_time',
|
|
|
+ 'last_update_time', 'updated_at', 'third_game_commission_ratio', 'third_game_commission',
|
|
|
+ ]);
|
|
|
+ }
|
|
|
+
|
|
|
+ return [$new + $updates, $existing];
|
|
|
+ }, 3);
|
|
|
+ break;
|
|
|
+ } catch (UniqueConstraintViolationException $e) {
|
|
|
+ if ($attempt === 3) {
|
|
|
+ throw $e;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ $changedKeys = array_keys(array_filter($written, fn (array $row, string $key): bool
|
|
|
+ => $this->snapshotChanged($existing[$key] ?? null, $row), ARRAY_FILTER_USE_BOTH));
|
|
|
+ $this->recordRealtime($changedKeys, $existing);
|
|
|
+
|
|
|
+ return $written;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Whether an incoming version is older than the stored one. Versions are ordered by the provider update time;
|
|
|
+ * a version without one cannot be ordered, so it may only replace an unfinished version, never a finished
|
|
|
+ * (settled/cancelled) one. A timed version always replaces an untimed one.
|
|
|
+ */
|
|
|
+ private function isStale(?string $candidateTime, ?string $currentTime, int $currentStatus): bool
|
|
|
+ {
|
|
|
+ if ($candidateTime === null) return $currentStatus !== 0;
|
|
|
+ return $currentTime !== null && $candidateTime < $currentTime;
|
|
|
+ }
|
|
|
+
|
|
|
+ private function applyCommission(array $row, ?ThirdGameOrderModel $previous, ThirdGameCommissionService $commission, array $rates, array $history): array
|
|
|
+ {
|
|
|
+ $currency = strtoupper($row['currency']);
|
|
|
+ $ratio = $previous !== null && $currency === strtoupper(trim((string) $previous->currency))
|
|
|
+ ? $previous->third_game_commission_ratio : null;
|
|
|
+ $result = $commission::result($row['status'], $row['settled_amount']);
|
|
|
+ if ($result === 'loss') {
|
|
|
+ if ($ratio === null && $currency === $commission::currency()) {
|
|
|
+ if ($commission->isHistorical($previous?->id, $row['last_update_time'], $history)) {
|
|
|
+ $rates = ($history['currency'] ?? null) === $currency ? $history['snapshot']['ratios'] : [];
|
|
|
+ }
|
|
|
+ $ratio = $rates[strtolower($row['platform'])] ?? null;
|
|
|
+ }
|
|
|
+ $amount = $ratio === null ? null : $commission::charge($row['settled_amount'], $ratio);
|
|
|
+ } else {
|
|
|
+ $amount = $row['status'] === 1 && $result === null ? null : Money::zero();
|
|
|
+ }
|
|
|
+
|
|
|
+ return $row + ['third_game_commission_ratio' => $ratio, 'third_game_commission' => $amount];
|
|
|
+ }
|
|
|
+
|
|
|
public function syncForList(array $params): array
|
|
|
{
|
|
|
if ((int) ($params['page'] ?? 1) > 1) {
|
|
|
@@ -374,7 +477,7 @@ class ThirdGameOrderService
|
|
|
/**
|
|
|
* @return array<string, ThirdGameOrderModel>
|
|
|
*/
|
|
|
- /** 同步每分钟全量 upsert,只有新订单或状态、结算、会员归属变化才推送后台 */
|
|
|
+ /** 只推送新订单及状态、结算、抽成、会员归属等有实质变化的订单。 */
|
|
|
private function snapshotChanged($previous, array $row): bool
|
|
|
{
|
|
|
if ($previous === null) {
|
|
|
@@ -386,6 +489,9 @@ class ThirdGameOrderService
|
|
|
: $settled !== $row['settled_amount'];
|
|
|
|
|
|
return $settledChanged
|
|
|
+ || $previous->third_game_commission_ratio !== $row['third_game_commission_ratio']
|
|
|
+ || $previous->third_game_commission !== $row['third_game_commission']
|
|
|
+ || $previous->currency !== $row['currency']
|
|
|
|| (int) $previous->getRawOriginal('status') !== (int) $row['status']
|
|
|
|| (string) $previous->getRawOriginal('last_update_time') !== (string) $row['last_update_time']
|
|
|
|| (string) $previous->getRawOriginal('user_id') !== (string) $row['user_id'];
|
|
|
@@ -406,36 +512,6 @@ class ThirdGameOrderService
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- private function existingSnapshots(array $orders): array
|
|
|
- {
|
|
|
- $orderKeys = [];
|
|
|
- foreach ($orders as $order) {
|
|
|
- $gameOrderId = trim((string) ($order['game_order_id'] ?? ''));
|
|
|
- if ($gameOrderId === '') {
|
|
|
- continue;
|
|
|
- }
|
|
|
- $orderKeys[] = $this->identity(
|
|
|
- trim((string) ($order['platform'] ?? '')),
|
|
|
- trim((string) ($order['player_id'] ?? '')),
|
|
|
- $gameOrderId
|
|
|
- );
|
|
|
- }
|
|
|
- $orderKeys = array_values(array_unique($orderKeys));
|
|
|
- if ($orderKeys === []) {
|
|
|
- return [];
|
|
|
- }
|
|
|
-
|
|
|
- $rows = ThirdGameOrderModel::query()
|
|
|
- ->whereIn('order_key', $orderKeys)
|
|
|
- ->get(['id', 'order_key', 'user_id', 'member_id', 'username', 'first_name', 'status', 'settled_amount', 'last_update_time', 'created_at']);
|
|
|
- $result = [];
|
|
|
- foreach ($rows as $row) {
|
|
|
- $result[(string) $row->order_key] = $row;
|
|
|
- }
|
|
|
-
|
|
|
- return $result;
|
|
|
- }
|
|
|
-
|
|
|
private function format(ThirdGameOrderModel $order): array
|
|
|
{
|
|
|
$userId = $order->current_user_id ?? $order->user_id;
|
|
|
@@ -444,6 +520,9 @@ class ThirdGameOrderService
|
|
|
$firstName = $order->current_first_name ?? $order->first_name;
|
|
|
$gameType = (int) $order->game_type;
|
|
|
$status = (int) $order->status;
|
|
|
+ $result = ThirdGameCommissionService::result($status, $order->settled_amount);
|
|
|
+ $commission = $result === 'loss' ? $order->third_game_commission
|
|
|
+ : ($status === 1 && $result === null ? null : Money::zero());
|
|
|
|
|
|
return [
|
|
|
'id' => (int) $order->id,
|
|
|
@@ -464,6 +543,9 @@ class ThirdGameOrderService
|
|
|
'bet_amount' => $this->number($order->bet_amount),
|
|
|
'valid_amount' => $this->number($order->valid_amount),
|
|
|
'settled_amount' => $this->number($order->settled_amount),
|
|
|
+ 'bet_result' => $result,
|
|
|
+ 'third_game_commission_ratio' => $result === 'loss' ? $order->third_game_commission_ratio : null,
|
|
|
+ 'third_game_commission' => $commission,
|
|
|
'bet_content' => (string) ($order->bet_content ?? ''),
|
|
|
'status' => $status,
|
|
|
'status_text' => self::STATUS_NAMES[$status] ?? '未知',
|