diff --git a/app/api/controller/v1/Playx.php b/app/api/controller/v1/Playx.php index 79bf216..53c507e 100644 --- a/app/api/controller/v1/Playx.php +++ b/app/api/controller/v1/Playx.php @@ -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')); } diff --git a/app/command/MallDailyPushReplayLog.php b/app/command/MallDailyPushReplayLog.php new file mode 100644 index 0000000..8e66fe7 --- /dev/null +++ b/app/command/MallDailyPushReplayLog.php @@ -0,0 +1,76 @@ +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('必须提供 --file 日志文件路径'); + return self::FAILURE; + } + + $dryRun = boolval($input->getOption('dry-run')); + try { + $result = (new MallDailyPushLogReplay())->replay($file, $dryRun); + } catch (Throwable $e) { + $output->writeln('' . $e->getMessage() . ''); + return self::FAILURE; + } + + $output->writeln('DailyPush 日志回放' . ($dryRun ? '预检' : '完成') . ''); + 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('错误明细(最多显示 100 条):'); + 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; + } +} diff --git a/app/common/library/MallDailyPushBackfill.php b/app/common/library/MallDailyPushBackfill.php index 4bc9c55..040aefb 100644 --- a/app/common/library/MallDailyPushBackfill.php +++ b/app/common/library/MallDailyPushBackfill.php @@ -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; - } } diff --git a/app/common/library/MallDailyPushLogReplay.php b/app/common/library/MallDailyPushLogReplay.php new file mode 100644 index 0000000..647e89d --- /dev/null +++ b/app/common/library/MallDailyPushLogReplay.php @@ -0,0 +1,481 @@ + + */ + 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 + */ + 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 $stats + * @return array> + */ + 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> $records + * @return array{0:array,1:array,2:array} + */ + 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 $record + * @param resource|null $backupHandle + * @param array $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 $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 $member + * @param array $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 $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 $stats + */ + private function appendError(array &$stats, string $message): void + { + if (count($stats['errors']) < 100) { + $stats['errors'][] = $message; + } + } +} diff --git a/docs/PlayX-接口文档.md b/docs/PlayX-接口文档.md index 55f65eb..135a251 100644 --- a/docs/PlayX-接口文档.md +++ b/docs/PlayX-接口文档.md @@ -56,9 +56,9 @@ { "member_id": "123456", "login": "john", - "lty_deposit": 15230.75, - "lty_withdrawal": 12400.50, - "yesterday_total_w": -320.25, + "ltv_deposit": 15230.75, + "ltv_withdrawal": 12400.50, + "yesterday_total_wl": -320.25, "yesterday_total_deposit": 500.00 } ] @@ -69,10 +69,10 @@ - `report_date` -> `date`(若为 Unix 秒则转为 `YYYY-MM-DD`) - `member[].member_id` -> `user_id` - `member[].login` -> `username` -- `member[].yesterday_total_w` -> `yesterday_win_loss_net` +- `member[].yesterday_total_wl` -> `yesterday_win_loss_net` - `member[].yesterday_total_deposit` -> `yesterday_total_deposit` -- `member[].lty_deposit` -> `lifetime_total_deposit` -- `member[].lty_withdrawal` -> `lifetime_total_withdraw` +- `member[].ltv_deposit` -> `lifetime_total_deposit` +- `member[].ltv_withdrawal` -> `lifetime_total_withdraw` 返回补充: - 批量模式会在 `data` 里增加 `results[]`,每个成员一条结果(是否 `deduped`)。 @@ -147,9 +147,9 @@ curl -X POST 'http://localhost:1818/api/v1/mall/dailyPush' \ { "member_id": "123456", "login": "john", - "lty_deposit": 15230.75, - "lty_withdrawal": 12400.50, - "yesterday_total_w": -320.25, + "ltv_deposit": 15230.75, + "ltv_withdrawal": 12400.50, + "yesterday_total_wl": -320.25, "yesterday_total_deposit": 500.00 } ] diff --git a/docs/PlayX-调用积分商城接口说明.md b/docs/PlayX-调用积分商城接口说明.md index bb63da5..f2117fd 100644 --- a/docs/PlayX-调用积分商城接口说明.md +++ b/docs/PlayX-调用积分商城接口说明.md @@ -171,14 +171,14 @@ expected = HMAC_SHA256( canonical , PLAYX_DAILY_PUSH_SECRET ) |------|------|------|------| | `member_id` | string | 是 | playX 用户 ID(幂等键之一) | | `login` | string | 否 | 用户展示名 | -| `yesterday_total_w` | number | 否 | 昨日净输赢;小于 0 才会累加到 `locked_points` | +| `yesterday_total_wl` | number | 否 | 昨日净输赢;小于 0 才会累加到 `locked_points` | | `yesterday_total_deposit` | number | 否 | 昨日总充值;用于计算 `today_limit` | -| `lty_deposit` | number | 否 | 历史总充值(冗余入库) | -| `lty_withdrawal` | number | 否 | 历史总提现(冗余入库) | +| `ltv_deposit` | number | 否 | 历史总充值(冗余入库) | +| `ltv_withdrawal` | number | 否 | 历史总提现(冗余入库) | #### Body 填写要求(批量模式) - **必须有**:`report_date`、`member`(数组且至少 1 个元素)、`member[].member_id`。 -- **允许缺省**:成员的 `login/yesterday_total_w/yesterday_total_deposit/lty_deposit/lty_withdrawal`;缺省时按 `0` 或空字符串处理。 +- **允许缺省**:成员的 `login/yesterday_total_wl/yesterday_total_deposit/ltv_deposit/ltv_withdrawal`;缺省时按 `0` 或空字符串处理。 - **日期**:`report_date` 传 Unix 秒会自动转换成 `YYYY-MM-DD`;如果直接传 `YYYY-MM-DD` 也支持。 ### 3.4 幂等 @@ -222,9 +222,9 @@ curl -X POST 'https://{商城域名}/api/v1/mall/dailyPush' \ { "member_id": "123456", "login": "john", - "lty_deposit": 15230.75, - "lty_withdrawal": 12400.50, - "yesterday_total_w": -320.25, + "ltv_deposit": 15230.75, + "ltv_withdrawal": 12400.50, + "yesterday_total_wl": -320.25, "yesterday_total_deposit": 500.00 } ] diff --git a/web/src/views/backend/mall/userAsset/popupForm.vue b/web/src/views/backend/mall/userAsset/popupForm.vue index eaa8332..e738067 100644 --- a/web/src/views/backend/mall/userAsset/popupForm.vue +++ b/web/src/views/backend/mall/userAsset/popupForm.vue @@ -117,7 +117,6 @@ const { t } = useI18n() const rules: Partial> = reactive({ username: [buildValidatorData({ name: 'required', title: t('mall.userAsset.username') })], - phone: [buildValidatorData({ name: 'required', title: t('mall.userAsset.phone') })], locked_points: [buildValidatorData({ name: 'required', title: t('mall.userAsset.locked_points') })], available_points: [buildValidatorData({ name: 'required', title: t('mall.userAsset.available_points') })], today_limit: [buildValidatorData({ name: 'required', title: t('mall.userAsset.today_limit') })],