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') })],