更新每日推送数据的保存
This commit is contained in:
@@ -89,22 +89,15 @@ class Playx extends Api
|
||||
return null;
|
||||
}
|
||||
|
||||
private function buildTempPhone(): ?string
|
||||
{
|
||||
for ($i = 0; $i < 8; $i++) {
|
||||
$candidate = '13' . str_pad(strval(mt_rand(0, 999999999)), 9, '0', STR_PAD_LEFT);
|
||||
if (!MallUserAsset::where('phone', $candidate)->find()) {
|
||||
return $candidate;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private function ensureAssetForPlayx(string $playxUserId, string $username): ?MallUserAsset
|
||||
private function ensureAssetForPlayx(string $playxUserId, string $username, string $phone = ''): ?MallUserAsset
|
||||
{
|
||||
$phone = trim($phone);
|
||||
$asset = MallUserAsset::where('playx_user_id', $playxUserId)->find();
|
||||
if ($asset) {
|
||||
if ($phone !== '' && trim(strval($asset->phone ?? '')) === '') {
|
||||
$asset->phone = $phone;
|
||||
$asset->save();
|
||||
}
|
||||
return $asset;
|
||||
}
|
||||
|
||||
@@ -115,15 +108,14 @@ class Playx extends Api
|
||||
$byName = MallUserAsset::where('username', $effectiveUsername)->find();
|
||||
if ($byName) {
|
||||
$byName->playx_user_id = $playxUserId;
|
||||
if ($phone !== '' && trim(strval($byName->phone ?? '')) === '') {
|
||||
$byName->phone = $phone;
|
||||
}
|
||||
$byName->save();
|
||||
|
||||
return $byName;
|
||||
}
|
||||
|
||||
$phone = $this->buildTempPhone();
|
||||
if ($phone === null) {
|
||||
return null;
|
||||
}
|
||||
$pwd = hash_password(Random::build('alnum', 16));
|
||||
$now = time();
|
||||
|
||||
@@ -151,9 +143,17 @@ class Playx extends Api
|
||||
/**
|
||||
* 按每日推送记录同步用户资产主信息;applyAssetDelta=true 时才落资产增量。
|
||||
*/
|
||||
private function syncAssetByDailyPush(string $playxUserId, string $username, string $date, int $newLocked, int $todayLimit, bool $applyAssetDelta): ?MallUserAsset
|
||||
private function syncAssetByDailyPush(
|
||||
string $playxUserId,
|
||||
string $username,
|
||||
string $date,
|
||||
int $newLocked,
|
||||
int $todayLimit,
|
||||
bool $applyAssetDelta,
|
||||
string $phone = ''
|
||||
): ?MallUserAsset
|
||||
{
|
||||
$asset = $this->ensureAssetForPlayx($playxUserId, $username);
|
||||
$asset = $this->ensureAssetForPlayx($playxUserId, $username, $phone);
|
||||
if (!$asset) {
|
||||
return null;
|
||||
}
|
||||
@@ -215,7 +215,7 @@ class Playx extends Api
|
||||
}
|
||||
|
||||
// ===== 新版批量上报格式 =====
|
||||
// 兼容你们截图:{ report_date, member:[{member_id, login, lty_deposit, lty_withdrawal, yesterday_total_w, yesterday_total_deposit}, ...] }
|
||||
// 当前字段为 ltv_deposit、ltv_withdrawal、yesterday_total_wl;同时兼容早期错误字段名。
|
||||
if (isset($body['report_date']) && isset($body['member']) && is_array($body['member'])) {
|
||||
$reportDate = $body['report_date'];
|
||||
$date = '';
|
||||
@@ -249,10 +249,11 @@ class Playx extends Api
|
||||
}
|
||||
|
||||
$username = strval($m['login'] ?? '');
|
||||
$yesterdayWinLossNet = $m['yesterday_total_w'] ?? 0;
|
||||
$phone = trim(strval($m['phone'] ?? ($m['mobile'] ?? '')));
|
||||
$yesterdayWinLossNet = $m['yesterday_total_wl'] ?? ($m['yesterday_total_w'] ?? 0);
|
||||
$yesterdayTotalDeposit = $m['yesterday_total_deposit'] ?? 0;
|
||||
$lifetimeTotalDeposit = $m['lty_deposit'] ?? 0;
|
||||
$lifetimeTotalWithdraw = $m['lty_withdrawal'] ?? 0;
|
||||
$lifetimeTotalDeposit = $m['ltv_deposit'] ?? ($m['lty_deposit'] ?? 0);
|
||||
$lifetimeTotalWithdraw = $m['ltv_withdrawal'] ?? ($m['lty_withdrawal'] ?? 0);
|
||||
|
||||
$exists = MallDailyPush::where('user_id', $playxUserId)->where('date', $date)->find();
|
||||
if ($exists) {
|
||||
@@ -261,7 +262,7 @@ class Playx extends Api
|
||||
$newLocked = intval(round(abs(floatval($yesterdayWinLossNet)) * $returnRatio));
|
||||
}
|
||||
$todayLimit = intval(round(floatval($yesterdayTotalDeposit) * $unlockRatio));
|
||||
$asset = $this->syncAssetByDailyPush($playxUserId, $username, $date, $newLocked, $todayLimit, false);
|
||||
$asset = $this->syncAssetByDailyPush($playxUserId, $username, $date, $newLocked, $todayLimit, false, $phone);
|
||||
if (!$asset) {
|
||||
return $this->error(__('Failed to ensure PlayX user asset'));
|
||||
}
|
||||
@@ -294,7 +295,7 @@ class Playx extends Api
|
||||
}
|
||||
$todayLimit = intval(round(floatval($yesterdayTotalDeposit) * $unlockRatio));
|
||||
|
||||
$asset = $this->syncAssetByDailyPush($playxUserId, $username, $date, $newLocked, $todayLimit, true);
|
||||
$asset = $this->syncAssetByDailyPush($playxUserId, $username, $date, $newLocked, $todayLimit, true, $phone);
|
||||
if (!$asset) {
|
||||
throw new \RuntimeException(__('Failed to ensure PlayX user asset'));
|
||||
}
|
||||
@@ -327,6 +328,7 @@ class Playx extends Api
|
||||
$requestId = $body['request_id'] ?? '';
|
||||
$date = $body['date'] ?? '';
|
||||
$playxUserId = strval($body['user_id'] ?? '');
|
||||
$phone = trim(strval($body['phone'] ?? ($body['mobile'] ?? '')));
|
||||
$yesterdayWinLossNet = $body['yesterday_win_loss_net'] ?? 0;
|
||||
$yesterdayTotalDeposit = $body['yesterday_total_deposit'] ?? 0;
|
||||
|
||||
@@ -344,7 +346,15 @@ class Playx extends Api
|
||||
$newLocked = intval(round(abs(floatval($yesterdayWinLossNet)) * $returnRatio));
|
||||
}
|
||||
$todayLimit = intval(round(floatval($yesterdayTotalDeposit) * $unlockRatio));
|
||||
$asset = $this->syncAssetByDailyPush($playxUserId, strval($body['username'] ?? ''), $date, $newLocked, $todayLimit, false);
|
||||
$asset = $this->syncAssetByDailyPush(
|
||||
$playxUserId,
|
||||
strval($body['username'] ?? ''),
|
||||
$date,
|
||||
$newLocked,
|
||||
$todayLimit,
|
||||
false,
|
||||
$phone
|
||||
);
|
||||
if (!$asset) {
|
||||
return $this->error(__('Failed to ensure PlayX user asset'));
|
||||
}
|
||||
@@ -379,7 +389,15 @@ class Playx extends Api
|
||||
}
|
||||
$todayLimit = intval(round(floatval($yesterdayTotalDeposit) * $unlockRatio));
|
||||
|
||||
$asset = $this->syncAssetByDailyPush($playxUserId, strval($body['username'] ?? ''), $date, $newLocked, $todayLimit, true);
|
||||
$asset = $this->syncAssetByDailyPush(
|
||||
$playxUserId,
|
||||
strval($body['username'] ?? ''),
|
||||
$date,
|
||||
$newLocked,
|
||||
$todayLimit,
|
||||
true,
|
||||
$phone
|
||||
);
|
||||
if (!$asset) {
|
||||
throw new \RuntimeException(__('Failed to ensure PlayX user asset'));
|
||||
}
|
||||
@@ -543,6 +561,7 @@ class Playx extends Api
|
||||
|
||||
$userId = strval($data['user_id']);
|
||||
$username = strval($data['username'] ?? '');
|
||||
$phone = trim(strval($data['phone'] ?? ($data['mobile'] ?? '')));
|
||||
|
||||
$expireAt = time() + intval(config('playx.session_expire_seconds', 3600));
|
||||
if (!empty($data['token_expire_at'])) {
|
||||
@@ -552,7 +571,7 @@ class Playx extends Api
|
||||
}
|
||||
}
|
||||
|
||||
$asset = $this->ensureAssetForPlayx($userId, $username);
|
||||
$asset = $this->ensureAssetForPlayx($userId, $username, $phone);
|
||||
if ($asset === null) {
|
||||
return $this->error(__('Failed to ensure PlayX user asset'));
|
||||
}
|
||||
|
||||
76
app/command/MallDailyPushReplayLog.php
Normal file
76
app/command/MallDailyPushReplayLog.php
Normal file
@@ -0,0 +1,76 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace app\command;
|
||||
|
||||
use app\common\library\MallDailyPushLogReplay;
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Throwable;
|
||||
|
||||
#[AsCommand('mall:daily-push:replay-log', '从原始日志幂等回放每日推送数据')]
|
||||
class MallDailyPushReplayLog extends Command
|
||||
{
|
||||
protected function configure(): void
|
||||
{
|
||||
$this->addOption('file', null, InputOption::VALUE_REQUIRED, 'daily_push_*.log 文件路径');
|
||||
$this->addOption('dry-run', null, InputOption::VALUE_NONE, '仅预检,不修改数据库');
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$file = trim(strval($input->getOption('file') ?? ''));
|
||||
if ($file === '') {
|
||||
$output->writeln('<error>必须提供 --file 日志文件路径</error>');
|
||||
return self::FAILURE;
|
||||
}
|
||||
|
||||
$dryRun = boolval($input->getOption('dry-run'));
|
||||
try {
|
||||
$result = (new MallDailyPushLogReplay())->replay($file, $dryRun);
|
||||
} catch (Throwable $e) {
|
||||
$output->writeln('<error>' . $e->getMessage() . '</error>');
|
||||
return self::FAILURE;
|
||||
}
|
||||
|
||||
$output->writeln('<info>DailyPush 日志回放' . ($dryRun ? '预检' : '完成') . '</info>');
|
||||
foreach ([
|
||||
'dry_run',
|
||||
'log_file',
|
||||
'backup_file',
|
||||
'lines',
|
||||
'members',
|
||||
'unique_members',
|
||||
'daily_created',
|
||||
'daily_updated',
|
||||
'daily_unchanged',
|
||||
'assets_created',
|
||||
'assets_updated',
|
||||
'locked_points_delta',
|
||||
'invalid_lines',
|
||||
'failed_members',
|
||||
] as $key) {
|
||||
$value = $result[$key] ?? '';
|
||||
if (is_bool($value)) {
|
||||
$value = $value ? 'true' : 'false';
|
||||
}
|
||||
$output->writeln($key . ': ' . strval($value));
|
||||
}
|
||||
|
||||
$errors = $result['errors'] ?? [];
|
||||
if (is_array($errors) && $errors !== []) {
|
||||
$output->writeln('<comment>错误明细(最多显示 100 条):</comment>');
|
||||
foreach ($errors as $error) {
|
||||
$output->writeln('- ' . strval($error));
|
||||
}
|
||||
}
|
||||
|
||||
return intval($result['failed_members'] ?? 0) > 0 || intval($result['invalid_lines'] ?? 0) > 0
|
||||
? self::FAILURE
|
||||
: self::SUCCESS;
|
||||
}
|
||||
}
|
||||
@@ -119,16 +119,12 @@ class MallDailyPushBackfill
|
||||
return ['created' => true, 'updated' => false];
|
||||
}
|
||||
|
||||
$phone = $this->buildTempPhone();
|
||||
if ($phone === null) {
|
||||
throw new \RuntimeException('Failed to allocate phone for playx user');
|
||||
}
|
||||
$pwd = hash_password(Random::build('alnum', 16));
|
||||
$now = time();
|
||||
$created = MallUserAsset::create([
|
||||
'playx_user_id' => $playxUserId,
|
||||
'username' => $effectiveUsername,
|
||||
'phone' => $phone,
|
||||
'phone' => '',
|
||||
'password' => $pwd,
|
||||
'admin_id' => 0,
|
||||
'locked_points' => 0,
|
||||
@@ -145,16 +141,5 @@ class MallDailyPushBackfill
|
||||
}
|
||||
return ['created' => true, 'updated' => false];
|
||||
}
|
||||
|
||||
private function buildTempPhone(): ?string
|
||||
{
|
||||
for ($i = 0; $i < 8; $i++) {
|
||||
$candidate = '13' . str_pad(strval(mt_rand(0, 999999999)), 9, '0', STR_PAD_LEFT);
|
||||
if (!MallUserAsset::where('phone', $candidate)->find()) {
|
||||
return $candidate;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
481
app/common/library/MallDailyPushLogReplay.php
Normal file
481
app/common/library/MallDailyPushLogReplay.php
Normal file
@@ -0,0 +1,481 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace app\common\library;
|
||||
|
||||
use app\common\model\MallDailyPush;
|
||||
use app\common\model\MallUserAsset;
|
||||
use ba\Random;
|
||||
use RuntimeException;
|
||||
use support\think\Db;
|
||||
use Throwable;
|
||||
|
||||
/**
|
||||
* 将 daily_push_*.log 中保存的第三方原始推送幂等回放到业务表。
|
||||
*/
|
||||
class MallDailyPushLogReplay
|
||||
{
|
||||
private const QUERY_CHUNK_SIZE = 1000;
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function replay(string $logFile, bool $dryRun = false): array
|
||||
{
|
||||
$stats = $this->newStats($logFile, $dryRun);
|
||||
$records = $this->parseRecords($logFile, $stats);
|
||||
$stats['unique_members'] = count($records);
|
||||
|
||||
[$dailyMap, $assetByPlayxId, $assetByUsername] = $this->preloadExistingData($records);
|
||||
$ratios = MallPlayxRatios::get();
|
||||
$returnRatio = floatval($ratios['return_ratio'] ?? 0);
|
||||
$unlockRatio = floatval($ratios['unlock_ratio'] ?? 0);
|
||||
[$backupHandle, $backupPath] = $this->openBackup($dryRun);
|
||||
$stats['backup_file'] = $backupPath;
|
||||
|
||||
try {
|
||||
foreach ($records as $key => $record) {
|
||||
try {
|
||||
$daily = $dailyMap[$key] ?? null;
|
||||
$asset = $assetByPlayxId[$record['user_id']]
|
||||
?? ($record['username'] !== '' ? ($assetByUsername[$record['username']] ?? null) : null);
|
||||
|
||||
$result = $this->replayRecord(
|
||||
$record,
|
||||
$daily,
|
||||
$asset,
|
||||
$returnRatio,
|
||||
$unlockRatio,
|
||||
$dryRun,
|
||||
$backupHandle,
|
||||
$stats
|
||||
);
|
||||
if ($result['daily']) {
|
||||
$dailyMap[$key] = $result['daily'];
|
||||
}
|
||||
if ($result['asset']) {
|
||||
$assetByPlayxId[$record['user_id']] = $result['asset'];
|
||||
if ($record['username'] !== '') {
|
||||
$assetByUsername[$record['username']] = $result['asset'];
|
||||
}
|
||||
}
|
||||
} catch (Throwable $e) {
|
||||
$stats['failed_members']++;
|
||||
$this->appendError($stats, 'user_id=' . $record['user_id'] . ':' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
if (is_resource($backupHandle)) {
|
||||
fclose($backupHandle);
|
||||
}
|
||||
}
|
||||
|
||||
return $stats;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
private function newStats(string $logFile, bool $dryRun): array
|
||||
{
|
||||
return [
|
||||
'dry_run' => $dryRun,
|
||||
'log_file' => $logFile,
|
||||
'backup_file' => '',
|
||||
'lines' => 0,
|
||||
'members' => 0,
|
||||
'unique_members' => 0,
|
||||
'daily_created' => 0,
|
||||
'daily_updated' => 0,
|
||||
'daily_unchanged' => 0,
|
||||
'assets_created' => 0,
|
||||
'assets_updated' => 0,
|
||||
'locked_points_delta' => 0,
|
||||
'invalid_lines' => 0,
|
||||
'failed_members' => 0,
|
||||
'errors' => [],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $stats
|
||||
* @return array<string, array<string, mixed>>
|
||||
*/
|
||||
private function parseRecords(string $logFile, array &$stats): array
|
||||
{
|
||||
if (!is_file($logFile) || !is_readable($logFile)) {
|
||||
throw new RuntimeException('日志文件不存在或不可读:' . $logFile);
|
||||
}
|
||||
$handle = fopen($logFile, 'rb');
|
||||
if ($handle === false) {
|
||||
throw new RuntimeException('无法打开日志文件:' . $logFile);
|
||||
}
|
||||
|
||||
$records = [];
|
||||
try {
|
||||
while (($line = fgets($handle)) !== false) {
|
||||
$line = trim($line);
|
||||
if ($line === '') {
|
||||
continue;
|
||||
}
|
||||
$stats['lines']++;
|
||||
$entry = json_decode($line, true);
|
||||
$raw = is_array($entry) ? strval($entry['raw'] ?? '') : '';
|
||||
$payload = $raw !== '' ? json_decode($raw, true) : null;
|
||||
if (!is_array($payload) || !isset($payload['report_date'], $payload['member']) || !is_array($payload['member'])) {
|
||||
$stats['invalid_lines']++;
|
||||
$this->appendError($stats, '第 ' . strval($stats['lines']) . ' 行格式无效');
|
||||
continue;
|
||||
}
|
||||
|
||||
$date = $this->resolveDate($payload['report_date']);
|
||||
if ($date === '') {
|
||||
$stats['invalid_lines']++;
|
||||
$this->appendError($stats, '第 ' . strval($stats['lines']) . ' 行 report_date 无效');
|
||||
continue;
|
||||
}
|
||||
$entryTime = strtotime(strval($entry['time'] ?? ''));
|
||||
$createTime = $entryTime === false ? time() : $entryTime;
|
||||
|
||||
foreach ($payload['member'] as $member) {
|
||||
$stats['members']++;
|
||||
if (!is_array($member)) {
|
||||
$stats['failed_members']++;
|
||||
continue;
|
||||
}
|
||||
$userId = trim(strval($member['member_id'] ?? ''));
|
||||
if ($userId === '') {
|
||||
$stats['failed_members']++;
|
||||
$this->appendError($stats, '第 ' . strval($stats['lines']) . ' 行存在空 member_id');
|
||||
continue;
|
||||
}
|
||||
$key = $date . '|' . $userId;
|
||||
$records[$key] = [
|
||||
'user_id' => $userId,
|
||||
'date' => $date,
|
||||
'username' => trim(strval($member['login'] ?? '')),
|
||||
'phone' => trim(strval($member['phone'] ?? ($member['mobile'] ?? ''))),
|
||||
'yesterday_win_loss_net' => $this->numericValue($member, ['yesterday_total_wl', 'yesterday_total_w']),
|
||||
'yesterday_total_deposit' => $this->numericValue($member, ['yesterday_total_deposit']),
|
||||
'lifetime_total_deposit' => $this->numericValue($member, ['ltv_deposit', 'lty_deposit']),
|
||||
'lifetime_total_withdraw' => $this->numericValue($member, ['ltv_withdrawal', 'lty_withdrawal']),
|
||||
'create_time' => $createTime,
|
||||
];
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
fclose($handle);
|
||||
}
|
||||
return $records;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, array<string, mixed>> $records
|
||||
* @return array{0:array<string,MallDailyPush>,1:array<string,MallUserAsset>,2:array<string,MallUserAsset>}
|
||||
*/
|
||||
private function preloadExistingData(array $records): array
|
||||
{
|
||||
$idsByDate = [];
|
||||
$userIds = [];
|
||||
$usernames = [];
|
||||
foreach ($records as $record) {
|
||||
$date = $record['date'];
|
||||
$userId = $record['user_id'];
|
||||
$idsByDate[$date][$userId] = true;
|
||||
$userIds[$userId] = true;
|
||||
if ($record['username'] !== '') {
|
||||
$usernames[$record['username']] = true;
|
||||
}
|
||||
}
|
||||
|
||||
$dailyMap = [];
|
||||
foreach ($idsByDate as $date => $ids) {
|
||||
foreach (array_chunk(array_keys($ids), self::QUERY_CHUNK_SIZE) as $idChunk) {
|
||||
$rows = MallDailyPush::where('date', $date)->whereIn('user_id', $idChunk)->select();
|
||||
foreach ($rows as $row) {
|
||||
/** @var MallDailyPush $row */
|
||||
$dailyMap[$date . '|' . strval($row->user_id)] = $row;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$assetByPlayxId = [];
|
||||
foreach (array_chunk(array_keys($userIds), self::QUERY_CHUNK_SIZE) as $idChunk) {
|
||||
$rows = MallUserAsset::whereIn('playx_user_id', $idChunk)->select();
|
||||
foreach ($rows as $row) {
|
||||
$assetByPlayxId[strval($row->playx_user_id)] = $row;
|
||||
}
|
||||
}
|
||||
|
||||
$assetByUsername = [];
|
||||
foreach (array_chunk(array_keys($usernames), self::QUERY_CHUNK_SIZE) as $usernameChunk) {
|
||||
$rows = MallUserAsset::whereIn('username', $usernameChunk)->select();
|
||||
foreach ($rows as $row) {
|
||||
$assetByUsername[strval($row->username)] = $row;
|
||||
}
|
||||
}
|
||||
|
||||
return [$dailyMap, $assetByPlayxId, $assetByUsername];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{0:mixed,1:string}
|
||||
*/
|
||||
private function openBackup(bool $dryRun): array
|
||||
{
|
||||
if ($dryRun) {
|
||||
return [null, ''];
|
||||
}
|
||||
$backupDir = runtime_path('backup/daily_push_replay');
|
||||
if (!is_dir($backupDir) && !mkdir($backupDir, 0755, true) && !is_dir($backupDir)) {
|
||||
throw new RuntimeException('无法创建回放备份目录');
|
||||
}
|
||||
$path = $backupDir . DIRECTORY_SEPARATOR . 'before_replay_' . date('Ymd_His') . '.jsonl';
|
||||
$handle = fopen($path, 'wb');
|
||||
if ($handle === false) {
|
||||
throw new RuntimeException('无法创建回放备份文件');
|
||||
}
|
||||
return [$handle, $path];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $record
|
||||
* @param resource|null $backupHandle
|
||||
* @param array<string, mixed> $stats
|
||||
* @return array{daily:?MallDailyPush,asset:?MallUserAsset}
|
||||
*/
|
||||
private function replayRecord(
|
||||
array $record,
|
||||
?MallDailyPush $daily,
|
||||
?MallUserAsset $asset,
|
||||
float $returnRatio,
|
||||
float $unlockRatio,
|
||||
bool $dryRun,
|
||||
$backupHandle,
|
||||
array &$stats
|
||||
): array {
|
||||
$oldWinLossNet = $daily ? floatval($daily->yesterday_win_loss_net ?? 0) : 0.0;
|
||||
$lockedDelta = $this->lockedContribution($record['yesterday_win_loss_net'], $returnRatio)
|
||||
- $this->lockedContribution($oldWinLossNet, $returnRatio);
|
||||
$todayLimit = intval(round($record['yesterday_total_deposit'] * $unlockRatio));
|
||||
$dailyData = [
|
||||
'user_id' => $record['user_id'],
|
||||
'date' => $record['date'],
|
||||
'username' => $record['username'],
|
||||
'yesterday_win_loss_net' => $record['yesterday_win_loss_net'],
|
||||
'yesterday_total_deposit' => $record['yesterday_total_deposit'],
|
||||
'lifetime_total_deposit' => $record['lifetime_total_deposit'],
|
||||
'lifetime_total_withdraw' => $record['lifetime_total_withdraw'],
|
||||
];
|
||||
$dailyChanged = !$daily || $this->dailyHasChanges($daily, $dailyData);
|
||||
$this->countDailyChange($stats, $daily, $dailyChanged);
|
||||
|
||||
$assetWillCreate = !$asset;
|
||||
$assetWillUpdate = $assetWillCreate
|
||||
|| trim(strval($asset->playx_user_id ?? '')) !== $record['user_id']
|
||||
|| $lockedDelta !== 0
|
||||
|| ($record['username'] !== '' && trim(strval($asset->username ?? '')) !== $record['username'])
|
||||
|| ($record['phone'] !== '' && trim(strval($asset->phone ?? '')) === '')
|
||||
|| $this->shouldUpdateDailyLimit($asset, $record['date'], $todayLimit);
|
||||
if ($assetWillCreate) {
|
||||
$stats['assets_created']++;
|
||||
} elseif ($assetWillUpdate) {
|
||||
$stats['assets_updated']++;
|
||||
}
|
||||
$stats['locked_points_delta'] += $lockedDelta;
|
||||
|
||||
if ($dryRun) {
|
||||
return ['daily' => $daily, 'asset' => $asset];
|
||||
}
|
||||
if (!$dailyChanged && !$assetWillUpdate) {
|
||||
return ['daily' => $daily, 'asset' => $asset];
|
||||
}
|
||||
|
||||
$this->writeBackup($backupHandle, $record['date'] . '|' . $record['user_id'], $daily, $asset);
|
||||
Db::startTrans();
|
||||
try {
|
||||
if ($daily) {
|
||||
if ($dailyChanged) {
|
||||
$daily->save($dailyData);
|
||||
}
|
||||
} else {
|
||||
$dailyData['create_time'] = $record['create_time'];
|
||||
$daily = MallDailyPush::create($dailyData);
|
||||
}
|
||||
|
||||
if (!$asset) {
|
||||
$effectiveUsername = $record['username'] !== '' ? $record['username'] : 'playx_' . $record['user_id'];
|
||||
$asset = MallUserAsset::create([
|
||||
'playx_user_id' => $record['user_id'],
|
||||
'username' => $effectiveUsername,
|
||||
'phone' => $record['phone'],
|
||||
'password' => hash_password(Random::build('alnum', 16)),
|
||||
'admin_id' => 0,
|
||||
'locked_points' => 0,
|
||||
'available_points' => 0,
|
||||
'today_limit' => 0,
|
||||
'today_claimed' => 0,
|
||||
'today_limit_date' => null,
|
||||
'create_time' => $record['create_time'],
|
||||
'update_time' => time(),
|
||||
]);
|
||||
if (!$asset) {
|
||||
throw new RuntimeException('创建用户资产失败');
|
||||
}
|
||||
/** @var MallUserAsset $asset */
|
||||
} elseif ($assetWillUpdate) {
|
||||
$asset->playx_user_id = $record['user_id'];
|
||||
if ($record['username'] !== '') {
|
||||
$asset->username = $record['username'];
|
||||
}
|
||||
if ($record['phone'] !== '' && trim(strval($asset->phone ?? '')) === '') {
|
||||
$asset->phone = $record['phone'];
|
||||
}
|
||||
}
|
||||
|
||||
if ($assetWillUpdate) {
|
||||
if ($lockedDelta !== 0) {
|
||||
$asset->locked_points = max(0, intval($asset->locked_points ?? 0) + $lockedDelta);
|
||||
}
|
||||
$this->applyDailyLimit($asset, $record['date'], $todayLimit);
|
||||
$asset->save();
|
||||
}
|
||||
Db::commit();
|
||||
} catch (Throwable $e) {
|
||||
Db::rollback();
|
||||
throw $e;
|
||||
}
|
||||
|
||||
return ['daily' => $daily, 'asset' => $asset];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $stats
|
||||
*/
|
||||
private function countDailyChange(array &$stats, ?MallDailyPush $daily, bool $changed): void
|
||||
{
|
||||
if (!$daily) {
|
||||
$stats['daily_created']++;
|
||||
} elseif ($changed) {
|
||||
$stats['daily_updated']++;
|
||||
} else {
|
||||
$stats['daily_unchanged']++;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $member
|
||||
* @param array<int, string> $keys
|
||||
*/
|
||||
private function numericValue(array $member, array $keys): float
|
||||
{
|
||||
foreach ($keys as $key) {
|
||||
if (array_key_exists($key, $member) && is_numeric($member[$key])) {
|
||||
return round(floatval($member[$key]), 2);
|
||||
}
|
||||
}
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
private function resolveDate(mixed $reportDate): string
|
||||
{
|
||||
if (is_numeric($reportDate)) {
|
||||
$timestamp = intval($reportDate);
|
||||
return $timestamp > 0 ? date('Y-m-d', $timestamp) : '';
|
||||
}
|
||||
$date = trim(strval($reportDate));
|
||||
$parsed = \DateTime::createFromFormat('Y-m-d', $date);
|
||||
return $parsed && $parsed->format('Y-m-d') === $date ? $date : '';
|
||||
}
|
||||
|
||||
private function lockedContribution(float $winLossNet, float $returnRatio): int
|
||||
{
|
||||
return $winLossNet < 0 ? intval(round(abs($winLossNet) * $returnRatio)) : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $dailyData
|
||||
*/
|
||||
private function dailyHasChanges(MallDailyPush $daily, array $dailyData): bool
|
||||
{
|
||||
if (trim(strval($daily->username ?? '')) !== $dailyData['username']) {
|
||||
return true;
|
||||
}
|
||||
foreach ([
|
||||
'yesterday_win_loss_net',
|
||||
'yesterday_total_deposit',
|
||||
'lifetime_total_deposit',
|
||||
'lifetime_total_withdraw',
|
||||
] as $field) {
|
||||
if (abs(floatval($daily->$field ?? 0) - floatval($dailyData[$field])) > 0.000001) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private function shouldUpdateDailyLimit(?MallUserAsset $asset, string $date, int $todayLimit): bool
|
||||
{
|
||||
if (!$asset) {
|
||||
return true;
|
||||
}
|
||||
$currentDate = trim(strval($asset->today_limit_date ?? ''));
|
||||
if ($currentDate !== '' && $currentDate > $date) {
|
||||
return false;
|
||||
}
|
||||
return $currentDate !== $date || intval($asset->today_limit ?? 0) !== $todayLimit;
|
||||
}
|
||||
|
||||
private function applyDailyLimit(MallUserAsset $asset, string $date, int $todayLimit): void
|
||||
{
|
||||
$currentDate = trim(strval($asset->today_limit_date ?? ''));
|
||||
if ($currentDate !== '' && $currentDate > $date) {
|
||||
return;
|
||||
}
|
||||
if ($currentDate !== $date) {
|
||||
$asset->today_claimed = 0;
|
||||
$asset->today_limit_date = $date;
|
||||
}
|
||||
$asset->today_limit = $todayLimit;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param resource|null $handle
|
||||
*/
|
||||
private function writeBackup($handle, string $key, ?MallDailyPush $daily, ?MallUserAsset $asset): void
|
||||
{
|
||||
if (!is_resource($handle)) {
|
||||
return;
|
||||
}
|
||||
$assetData = null;
|
||||
if ($asset) {
|
||||
$assetData = [
|
||||
'id' => $asset->id,
|
||||
'playx_user_id' => $asset->playx_user_id,
|
||||
'username' => $asset->username,
|
||||
'phone' => $asset->phone,
|
||||
'locked_points' => $asset->locked_points,
|
||||
'today_limit' => $asset->today_limit,
|
||||
'today_claimed' => $asset->today_claimed,
|
||||
'today_limit_date' => $asset->today_limit_date,
|
||||
'update_time' => $asset->update_time,
|
||||
];
|
||||
}
|
||||
fwrite($handle, json_encode([
|
||||
'key' => $key,
|
||||
'daily_push' => $daily ? $daily->toArray() : null,
|
||||
'user_asset' => $assetData,
|
||||
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) . PHP_EOL);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $stats
|
||||
*/
|
||||
private function appendError(array &$stats, string $message): void
|
||||
{
|
||||
if (count($stats['errors']) < 100) {
|
||||
$stats['errors'][] = $message;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user