优化每日推送和积分转化逻辑
This commit is contained in:
@@ -30,9 +30,11 @@ class DailyPush extends Backend
|
||||
'date',
|
||||
'username',
|
||||
'yesterday_win_loss_net',
|
||||
'converted_points',
|
||||
'yesterday_total_deposit',
|
||||
'lifetime_total_deposit',
|
||||
'lifetime_total_withdraw',
|
||||
'lifetime_win_loss_net',
|
||||
'create_time',
|
||||
];
|
||||
|
||||
|
||||
@@ -6,8 +6,10 @@ namespace app\admin\controller\mall;
|
||||
|
||||
use app\common\controller\Backend;
|
||||
use app\common\library\MallPlayxRatios;
|
||||
use app\common\library\MallPointsConversion;
|
||||
use app\common\model\MallBusinessConfig;
|
||||
use support\Response;
|
||||
use support\think\Db;
|
||||
use Webman\Http\Request;
|
||||
|
||||
/**
|
||||
@@ -22,25 +24,15 @@ class PlayxConfig extends Backend
|
||||
return $response;
|
||||
}
|
||||
|
||||
$row = MallBusinessConfig::order('id', 'asc')->find();
|
||||
if (!$row) {
|
||||
$now = time();
|
||||
MallBusinessConfig::create([
|
||||
'return_ratio' => floatval(env('PLAYX_RETURN_RATIO', '0.1')),
|
||||
'unlock_ratio' => floatval(env('PLAYX_UNLOCK_RATIO', '0.1')),
|
||||
'points_to_cash_ratio' => floatval(env('PLAYX_POINTS_TO_CASH_RATIO', '0.1')),
|
||||
'create_time' => $now,
|
||||
'update_time' => $now,
|
||||
]);
|
||||
$row = MallBusinessConfig::order('id', 'asc')->find();
|
||||
}
|
||||
$row = MallBusinessConfig::ensureRow();
|
||||
|
||||
if ($row) {
|
||||
MallPlayxRatios::syncFromRow($row);
|
||||
}
|
||||
MallPlayxRatios::syncFromRow($row);
|
||||
|
||||
$payload = $row->toArray();
|
||||
$payload['active_mall_open_date'] = MallPointsConversion::resolveActiveMallOpenDate($row);
|
||||
|
||||
return $this->success('', [
|
||||
'row' => $row ? $row->toArray() : [],
|
||||
'row' => $payload,
|
||||
'remark' => get_route_remark(),
|
||||
]);
|
||||
}
|
||||
@@ -71,20 +63,54 @@ class PlayxConfig extends Backend
|
||||
return $this->error(__('Parameter error'));
|
||||
}
|
||||
|
||||
$dailyEnabled = !empty($data['daily_points_enabled']) ? 1 : 0;
|
||||
$lifetimeEnabled = !empty($data['lifetime_points_enabled']) ? 1 : 0;
|
||||
$lifetimeRatio = $data['lifetime_points_ratio'] ?? null;
|
||||
if (!is_numeric($lifetimeRatio)) {
|
||||
return $this->error(__('Parameter error'));
|
||||
}
|
||||
$lifetimeRatioF = floatval($lifetimeRatio);
|
||||
if ($lifetimeRatioF < 0) {
|
||||
return $this->error(__('Parameter error'));
|
||||
}
|
||||
|
||||
$mallOpenDate = MallPointsConversion::normalizeDate($data['mall_open_date'] ?? null);
|
||||
|
||||
$row = MallBusinessConfig::order('id', 'asc')->find();
|
||||
$now = time();
|
||||
if (!$row) {
|
||||
$now = time();
|
||||
$effectiveOn = $mallOpenDate !== null ? MallPointsConversion::nextCalendarDate() : null;
|
||||
MallBusinessConfig::create([
|
||||
'return_ratio' => $returnF,
|
||||
'unlock_ratio' => $unlockF,
|
||||
'points_to_cash_ratio' => $cashF,
|
||||
'create_time' => $now,
|
||||
'update_time' => $now,
|
||||
'return_ratio' => $returnF,
|
||||
'unlock_ratio' => $unlockF,
|
||||
'points_to_cash_ratio' => $cashF,
|
||||
'daily_points_enabled' => $dailyEnabled,
|
||||
'lifetime_points_enabled' => $lifetimeEnabled,
|
||||
'lifetime_points_ratio' => $lifetimeRatioF,
|
||||
'mall_open_date' => $mallOpenDate,
|
||||
'mall_open_date_previous' => null,
|
||||
'mall_open_date_effective_on' => $effectiveOn,
|
||||
'create_time' => $now,
|
||||
'update_time' => $now,
|
||||
]);
|
||||
} else {
|
||||
$previousOpen = MallPointsConversion::resolveActiveMallOpenDate($row);
|
||||
$currentPending = MallPointsConversion::normalizeDate($row->mall_open_date ?? null);
|
||||
$openDateChanged = $mallOpenDate !== $currentPending;
|
||||
|
||||
$row->return_ratio = $returnF;
|
||||
$row->unlock_ratio = $unlockF;
|
||||
$row->points_to_cash_ratio = $cashF;
|
||||
$row->daily_points_enabled = $dailyEnabled;
|
||||
$row->lifetime_points_enabled = $lifetimeEnabled;
|
||||
$row->lifetime_points_ratio = $lifetimeRatioF;
|
||||
|
||||
if ($openDateChanged) {
|
||||
$row->mall_open_date_previous = $previousOpen;
|
||||
$row->mall_open_date = $mallOpenDate;
|
||||
$row->mall_open_date_effective_on = $mallOpenDate !== null ? MallPointsConversion::nextCalendarDate() : null;
|
||||
}
|
||||
|
||||
$row->save();
|
||||
}
|
||||
|
||||
@@ -95,4 +121,32 @@ class PlayxConfig extends Backend
|
||||
|
||||
return $this->success(__('The current page configuration item was updated successfully'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 一键按最新终身总输赢与比例折算积分(重复执行会先扣旧比例再加新比例)
|
||||
*/
|
||||
public function calculateLifetimePoints(Request $request): Response
|
||||
{
|
||||
$response = $this->initializeBackend($request);
|
||||
if ($response !== null) {
|
||||
return $response;
|
||||
}
|
||||
|
||||
$row = MallBusinessConfig::order('id', 'asc')->find();
|
||||
if (!$row) {
|
||||
return $this->error(__('Parameter error'));
|
||||
}
|
||||
|
||||
Db::startTrans();
|
||||
try {
|
||||
$stats = MallPointsConversion::recalculateAllLifetimePoints($row);
|
||||
Db::commit();
|
||||
} catch (\Throwable $e) {
|
||||
Db::rollback();
|
||||
|
||||
return $this->error($e->getMessage());
|
||||
}
|
||||
|
||||
return $this->success(__('Operation completed'), $stats);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,6 +42,8 @@ class UserAsset extends Backend
|
||||
'today_limit',
|
||||
'today_claimed',
|
||||
'today_limit_date',
|
||||
'points_claim_cap',
|
||||
'total_points_claimed',
|
||||
'create_time',
|
||||
'update_time',
|
||||
];
|
||||
|
||||
@@ -27,6 +27,7 @@ return [
|
||||
'Added successfully' => 'Added successfully!',
|
||||
'Deleted successfully' => 'Deleted successfully!',
|
||||
'Parameter error' => 'Parameter error!',
|
||||
'Lifetime points source is disabled' => 'Lifetime points source is disabled',
|
||||
'File uploaded successfully' => 'File uploaded successfully',
|
||||
'No files were uploaded' => 'No files were uploaded',
|
||||
'The uploaded file format is not allowed' => 'The uploaded file format is no allowance.',
|
||||
|
||||
@@ -27,6 +27,7 @@ return [
|
||||
'Added successfully' => '添加成功!',
|
||||
'Deleted successfully' => '删除成功!',
|
||||
'Parameter error' => '参数错误!',
|
||||
'Lifetime points source is disabled' => '终身总输赢积分来源未启用',
|
||||
'Please use the %s field to sort before operating' => '请使用 %s 字段排序后再操作',
|
||||
'File uploaded successfully' => '文件上传成功!',
|
||||
'No files were uploaded' => '没有文件被上传',
|
||||
|
||||
@@ -15,6 +15,7 @@ use app\common\model\MallSession;
|
||||
use app\common\model\MallOrder;
|
||||
use app\common\model\MallUserAsset;
|
||||
use app\common\library\MallPlayxRatios;
|
||||
use app\common\library\MallPointsConversion;
|
||||
use app\common\library\MallDailyPushRawLogger;
|
||||
use app\common\model\MallAddress;
|
||||
use support\think\Db;
|
||||
@@ -143,11 +144,19 @@ class Playx extends Api
|
||||
/**
|
||||
* 按每日推送记录同步用户资产主信息;applyAssetDelta=true 时才落资产增量。
|
||||
*/
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
private function getPointsConversionConfig(): array
|
||||
{
|
||||
return MallPointsConversion::configForConversion();
|
||||
}
|
||||
|
||||
private function syncAssetByDailyPush(
|
||||
string $playxUserId,
|
||||
string $username,
|
||||
string $date,
|
||||
int $newLocked,
|
||||
int $convertedPoints,
|
||||
int $todayLimit,
|
||||
bool $applyAssetDelta,
|
||||
string $phone = ''
|
||||
@@ -169,7 +178,9 @@ class Playx extends Api
|
||||
$asset->today_claimed = 0;
|
||||
$asset->today_limit_date = $date;
|
||||
}
|
||||
$asset->locked_points = intval($asset->locked_points ?? 0) + $newLocked;
|
||||
if ($convertedPoints !== 0) {
|
||||
MallPointsConversion::applyDailyConversionDelta($asset, $convertedPoints);
|
||||
}
|
||||
$asset->today_limit = $todayLimit;
|
||||
}
|
||||
|
||||
@@ -235,9 +246,8 @@ class Playx extends Api
|
||||
$requestId = 'report_' . $date;
|
||||
}
|
||||
|
||||
$ratios = MallPlayxRatios::get();
|
||||
$returnRatio = $ratios['return_ratio'];
|
||||
$unlockRatio = $ratios['unlock_ratio'];
|
||||
$pointsConfig = $this->getPointsConversionConfig();
|
||||
$unlockRatio = floatval($pointsConfig['unlock_ratio'] ?? 0);
|
||||
|
||||
$results = [];
|
||||
$allDeduped = true;
|
||||
@@ -254,15 +264,13 @@ class Playx extends Api
|
||||
$yesterdayTotalDeposit = $m['yesterday_total_deposit'] ?? 0;
|
||||
$lifetimeTotalDeposit = $m['ltv_deposit'] ?? ($m['lty_deposit'] ?? 0);
|
||||
$lifetimeTotalWithdraw = $m['ltv_withdrawal'] ?? ($m['lty_withdrawal'] ?? 0);
|
||||
$lifetimeWinLossNet = MallPointsConversion::parseLifetimeWinLossFromMember($m);
|
||||
|
||||
$exists = MallDailyPush::where('user_id', $playxUserId)->where('date', $date)->find();
|
||||
if ($exists) {
|
||||
$newLocked = 0;
|
||||
if ($yesterdayWinLossNet < 0) {
|
||||
$newLocked = intval(round(abs(floatval($yesterdayWinLossNet)) * $returnRatio));
|
||||
}
|
||||
$convertedPoints = MallPointsConversion::dailyConvertedPoints($date, floatval($yesterdayWinLossNet), $pointsConfig);
|
||||
$todayLimit = intval(round(floatval($yesterdayTotalDeposit) * $unlockRatio));
|
||||
$asset = $this->syncAssetByDailyPush($playxUserId, $username, $date, $newLocked, $todayLimit, false, $phone);
|
||||
$asset = $this->syncAssetByDailyPush($playxUserId, $username, $date, $convertedPoints, $todayLimit, false, $phone);
|
||||
if (!$asset) {
|
||||
return $this->error(__('Failed to ensure PlayX user asset'));
|
||||
}
|
||||
@@ -278,7 +286,7 @@ class Playx extends Api
|
||||
|
||||
Db::startTrans();
|
||||
try {
|
||||
MallDailyPush::create([
|
||||
$pushData = [
|
||||
'user_id' => $playxUserId,
|
||||
'date' => $date,
|
||||
'username' => $username,
|
||||
@@ -287,15 +295,17 @@ class Playx extends Api
|
||||
'lifetime_total_deposit' => $lifetimeTotalDeposit,
|
||||
'lifetime_total_withdraw' => $lifetimeTotalWithdraw,
|
||||
'create_time' => time(),
|
||||
]);
|
||||
|
||||
$newLocked = 0;
|
||||
if ($yesterdayWinLossNet < 0) {
|
||||
$newLocked = intval(round(abs(floatval($yesterdayWinLossNet)) * $returnRatio));
|
||||
];
|
||||
$convertedPoints = MallPointsConversion::dailyConvertedPoints($date, floatval($yesterdayWinLossNet), $pointsConfig);
|
||||
$pushData['converted_points'] = $convertedPoints;
|
||||
if ($lifetimeWinLossNet !== null) {
|
||||
$pushData['lifetime_win_loss_net'] = $lifetimeWinLossNet;
|
||||
}
|
||||
MallDailyPush::create($pushData);
|
||||
|
||||
$todayLimit = intval(round(floatval($yesterdayTotalDeposit) * $unlockRatio));
|
||||
|
||||
$asset = $this->syncAssetByDailyPush($playxUserId, $username, $date, $newLocked, $todayLimit, true, $phone);
|
||||
$asset = $this->syncAssetByDailyPush($playxUserId, $username, $date, $convertedPoints, $todayLimit, true, $phone);
|
||||
if (!$asset) {
|
||||
throw new \RuntimeException(__('Failed to ensure PlayX user asset'));
|
||||
}
|
||||
@@ -338,19 +348,15 @@ class Playx extends Api
|
||||
|
||||
$exists = MallDailyPush::where('user_id', $playxUserId)->where('date', $date)->find();
|
||||
if ($exists) {
|
||||
$newLocked = 0;
|
||||
$ratios = MallPlayxRatios::get();
|
||||
$returnRatio = $ratios['return_ratio'];
|
||||
$unlockRatio = $ratios['unlock_ratio'];
|
||||
if ($yesterdayWinLossNet < 0) {
|
||||
$newLocked = intval(round(abs(floatval($yesterdayWinLossNet)) * $returnRatio));
|
||||
}
|
||||
$pointsConfig = $this->getPointsConversionConfig();
|
||||
$unlockRatio = floatval($pointsConfig['unlock_ratio'] ?? 0);
|
||||
$convertedPoints = MallPointsConversion::dailyConvertedPoints($date, floatval($yesterdayWinLossNet), $pointsConfig);
|
||||
$todayLimit = intval(round(floatval($yesterdayTotalDeposit) * $unlockRatio));
|
||||
$asset = $this->syncAssetByDailyPush(
|
||||
$playxUserId,
|
||||
strval($body['username'] ?? ''),
|
||||
$date,
|
||||
$newLocked,
|
||||
$convertedPoints,
|
||||
$todayLimit,
|
||||
false,
|
||||
$phone
|
||||
@@ -369,7 +375,12 @@ class Playx extends Api
|
||||
|
||||
Db::startTrans();
|
||||
try {
|
||||
MallDailyPush::create([
|
||||
$pointsConfig = $this->getPointsConversionConfig();
|
||||
$lifetimeWinLossNet = null;
|
||||
if (isset($body['lifetime_win_loss_net']) && is_numeric($body['lifetime_win_loss_net'])) {
|
||||
$lifetimeWinLossNet = floatval($body['lifetime_win_loss_net']);
|
||||
}
|
||||
$pushData = [
|
||||
'user_id' => $playxUserId,
|
||||
'date' => $date,
|
||||
'username' => $body['username'] ?? '',
|
||||
@@ -378,22 +389,22 @@ class Playx extends Api
|
||||
'lifetime_total_deposit' => $body['lifetime_total_deposit'] ?? 0,
|
||||
'lifetime_total_withdraw' => $body['lifetime_total_withdraw'] ?? 0,
|
||||
'create_time' => time(),
|
||||
]);
|
||||
|
||||
$newLocked = 0;
|
||||
$ratios = MallPlayxRatios::get();
|
||||
$returnRatio = $ratios['return_ratio'];
|
||||
$unlockRatio = $ratios['unlock_ratio'];
|
||||
if ($yesterdayWinLossNet < 0) {
|
||||
$newLocked = intval(round(abs(floatval($yesterdayWinLossNet)) * $returnRatio));
|
||||
];
|
||||
$convertedPoints = MallPointsConversion::dailyConvertedPoints($date, floatval($yesterdayWinLossNet), $pointsConfig);
|
||||
$pushData['converted_points'] = $convertedPoints;
|
||||
if ($lifetimeWinLossNet !== null) {
|
||||
$pushData['lifetime_win_loss_net'] = $lifetimeWinLossNet;
|
||||
}
|
||||
MallDailyPush::create($pushData);
|
||||
|
||||
$unlockRatio = floatval($pointsConfig['unlock_ratio'] ?? 0);
|
||||
$todayLimit = intval(round(floatval($yesterdayTotalDeposit) * $unlockRatio));
|
||||
|
||||
$asset = $this->syncAssetByDailyPush(
|
||||
$playxUserId,
|
||||
strval($body['username'] ?? ''),
|
||||
$date,
|
||||
$newLocked,
|
||||
$convertedPoints,
|
||||
$todayLimit,
|
||||
true,
|
||||
$phone
|
||||
@@ -744,11 +755,12 @@ class Playx extends Api
|
||||
}
|
||||
|
||||
$remain = $asset->today_limit - $asset->today_claimed;
|
||||
if ($asset->locked_points <= 0 || $remain <= 0) {
|
||||
$lifetimeRemain = MallPointsConversion::remainingClaimable($asset);
|
||||
if ($asset->locked_points <= 0 || $remain <= 0 || $lifetimeRemain <= 0) {
|
||||
return $this->error(__('No points to claim or limit reached'));
|
||||
}
|
||||
|
||||
$canClaim = min($asset->locked_points, $remain);
|
||||
$canClaim = min($asset->locked_points, $remain, $lifetimeRemain);
|
||||
|
||||
Db::startTrans();
|
||||
try {
|
||||
@@ -762,6 +774,7 @@ class Playx extends Api
|
||||
$asset->locked_points -= $canClaim;
|
||||
$asset->available_points += $canClaim;
|
||||
$asset->today_claimed += $canClaim;
|
||||
$asset->total_points_claimed = max(0, intval($asset->total_points_claimed ?? 0) + $canClaim);
|
||||
$asset->save();
|
||||
|
||||
Db::commit();
|
||||
|
||||
@@ -30,9 +30,11 @@ class MallDailyPushExport
|
||||
'date' => '业务日期',
|
||||
'username' => '用户名',
|
||||
'yesterday_win_loss_net' => '昨日净输赢',
|
||||
'converted_points' => '转换积分',
|
||||
'yesterday_total_deposit' => '昨日总充值',
|
||||
'lifetime_total_deposit' => '历史总充值',
|
||||
'lifetime_total_withdraw' => '历史总提现',
|
||||
'lifetime_total_deposit' => '终身总充值',
|
||||
'lifetime_total_withdraw' => '终身总提现',
|
||||
'lifetime_win_loss_net' => '终身总输赢',
|
||||
'create_time' => '创建时间',
|
||||
];
|
||||
|
||||
@@ -45,9 +47,11 @@ class MallDailyPushExport
|
||||
'date' => 'Business date',
|
||||
'username' => 'Username',
|
||||
'yesterday_win_loss_net' => 'Yesterday net win/loss',
|
||||
'converted_points' => 'Converted points',
|
||||
'yesterday_total_deposit' => 'Yesterday total deposit',
|
||||
'lifetime_total_deposit' => 'Lifetime total deposit',
|
||||
'lifetime_total_withdraw' => 'Lifetime total withdraw',
|
||||
'lifetime_win_loss_net' => 'Lifetime net win/loss',
|
||||
'create_time' => 'Created at',
|
||||
];
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace app\common\library;
|
||||
|
||||
use app\common\model\MallBusinessConfig;
|
||||
use app\common\model\MallDailyPush;
|
||||
use app\common\model\MallUserAsset;
|
||||
use ba\Random;
|
||||
@@ -28,9 +29,9 @@ class MallDailyPushLogReplay
|
||||
$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);
|
||||
$configRow = MallBusinessConfig::ensureRow();
|
||||
$pointsConfig = MallPointsConversion::configFromRow($configRow);
|
||||
$unlockRatio = floatval($pointsConfig['unlock_ratio'] ?? 0);
|
||||
[$backupHandle, $backupPath] = $this->openBackup($dryRun);
|
||||
$stats['backup_file'] = $backupPath;
|
||||
|
||||
@@ -45,7 +46,7 @@ class MallDailyPushLogReplay
|
||||
$record,
|
||||
$daily,
|
||||
$asset,
|
||||
$returnRatio,
|
||||
$pointsConfig,
|
||||
$unlockRatio,
|
||||
$dryRun,
|
||||
$backupHandle,
|
||||
@@ -249,21 +250,29 @@ class MallDailyPushLogReplay
|
||||
array $record,
|
||||
?MallDailyPush $daily,
|
||||
?MallUserAsset $asset,
|
||||
float $returnRatio,
|
||||
array $pointsConfig,
|
||||
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);
|
||||
$newLocked = MallPointsConversion::dailyConvertedPoints(
|
||||
$record['date'],
|
||||
floatval($record['yesterday_win_loss_net']),
|
||||
$pointsConfig
|
||||
);
|
||||
$oldLocked = $daily
|
||||
? MallPointsConversion::dailyConvertedPoints($record['date'], $oldWinLossNet, $pointsConfig)
|
||||
: 0;
|
||||
$lockedDelta = $newLocked - $oldLocked;
|
||||
$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'],
|
||||
'converted_points' => $newLocked,
|
||||
'yesterday_total_deposit' => $record['yesterday_total_deposit'],
|
||||
'lifetime_total_deposit' => $record['lifetime_total_deposit'],
|
||||
'lifetime_total_withdraw' => $record['lifetime_total_withdraw'],
|
||||
@@ -317,6 +326,10 @@ class MallDailyPushLogReplay
|
||||
'today_limit' => 0,
|
||||
'today_claimed' => 0,
|
||||
'today_limit_date' => null,
|
||||
'points_claim_cap' => 0,
|
||||
'total_points_claimed' => 0,
|
||||
'lifetime_converted_points' => 0,
|
||||
'daily_converted_points' => 0,
|
||||
'create_time' => $record['create_time'],
|
||||
'update_time' => time(),
|
||||
]);
|
||||
@@ -336,7 +349,7 @@ class MallDailyPushLogReplay
|
||||
|
||||
if ($assetWillUpdate) {
|
||||
if ($lockedDelta !== 0) {
|
||||
$asset->locked_points = max(0, intval($asset->locked_points ?? 0) + $lockedDelta);
|
||||
MallPointsConversion::applyDailyConversionDelta($asset, $lockedDelta);
|
||||
}
|
||||
$this->applyDailyLimit($asset, $record['date'], $todayLimit);
|
||||
$asset->save();
|
||||
@@ -404,6 +417,7 @@ class MallDailyPushLogReplay
|
||||
}
|
||||
foreach ([
|
||||
'yesterday_win_loss_net',
|
||||
'converted_points',
|
||||
'yesterday_total_deposit',
|
||||
'lifetime_total_deposit',
|
||||
'lifetime_total_withdraw',
|
||||
|
||||
@@ -9,7 +9,7 @@ use support\Redis as RedisSupport;
|
||||
use Throwable;
|
||||
|
||||
/**
|
||||
* PlayX 与积分商城相关的比例配置:优先读 Redis,未命中则读 mall_business_config 并回写 Redis;无表记录时回退 .env 并写入 Redis。
|
||||
* PlayX 与积分商城相关的比例配置:优先读 Redis,未命中则读 mall_business_config 并回写 Redis。
|
||||
*/
|
||||
final class MallPlayxRatios
|
||||
{
|
||||
@@ -124,9 +124,9 @@ final class MallPlayxRatios
|
||||
}
|
||||
|
||||
$out = [
|
||||
'return_ratio' => floatval(env('PLAYX_RETURN_RATIO', '0.1')),
|
||||
'unlock_ratio' => floatval(env('PLAYX_UNLOCK_RATIO', '0.1')),
|
||||
'points_to_cash_ratio' => floatval(env('PLAYX_POINTS_TO_CASH_RATIO', '0.1')),
|
||||
'return_ratio' => MallBusinessConfig::DEFAULT_RETURN_RATIO,
|
||||
'unlock_ratio' => MallBusinessConfig::DEFAULT_UNLOCK_RATIO,
|
||||
'points_to_cash_ratio' => MallBusinessConfig::DEFAULT_POINTS_TO_CASH_RATIO,
|
||||
];
|
||||
self::writeRedis($conn, $out);
|
||||
|
||||
|
||||
266
app/common/library/MallPointsConversion.php
Normal file
266
app/common/library/MallPointsConversion.php
Normal file
@@ -0,0 +1,266 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace app\common\library;
|
||||
|
||||
use app\common\model\MallBusinessConfig;
|
||||
use app\common\model\MallDailyPush;
|
||||
use app\common\model\MallUserAsset;
|
||||
use support\think\Db;
|
||||
|
||||
/**
|
||||
* 积分来源:昨日净输赢 / 终身总输赢 折算与领取上限
|
||||
*/
|
||||
final class MallPointsConversion
|
||||
{
|
||||
/**
|
||||
* @return array{
|
||||
* return_ratio: float,
|
||||
* unlock_ratio: float,
|
||||
* points_to_cash_ratio: float,
|
||||
* daily_points_enabled: bool,
|
||||
* lifetime_points_enabled: bool,
|
||||
* lifetime_points_ratio: float,
|
||||
* mall_open_date: ?string,
|
||||
* mall_open_date_previous: ?string,
|
||||
* mall_open_date_effective_on: ?string,
|
||||
* active_mall_open_date: ?string
|
||||
* }
|
||||
*/
|
||||
public static function configFromRow(MallBusinessConfig $row): array
|
||||
{
|
||||
return [
|
||||
'return_ratio' => floatval($row->return_ratio ?? 0),
|
||||
'unlock_ratio' => floatval($row->unlock_ratio ?? 0),
|
||||
'points_to_cash_ratio' => floatval($row->points_to_cash_ratio ?? 0),
|
||||
'daily_points_enabled' => intval($row->daily_points_enabled ?? 1) === 1,
|
||||
'lifetime_points_enabled' => intval($row->lifetime_points_enabled ?? 0) === 1,
|
||||
'lifetime_points_ratio' => floatval($row->lifetime_points_ratio ?? 0),
|
||||
'mall_open_date' => self::normalizeDate($row->mall_open_date ?? null),
|
||||
'mall_open_date_previous' => self::normalizeDate($row->mall_open_date_previous ?? null),
|
||||
'mall_open_date_effective_on' => self::normalizeDate($row->mall_open_date_effective_on ?? null),
|
||||
'active_mall_open_date' => self::resolveActiveMallOpenDate($row),
|
||||
];
|
||||
}
|
||||
|
||||
public static function lossToPoints(float $winLossNet, float $ratio): int
|
||||
{
|
||||
if ($winLossNet >= 0 || $ratio <= 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return self::lossIntegerToPoints($winLossNet, $ratio);
|
||||
}
|
||||
|
||||
/**
|
||||
* 仅统计输钱:取净输赢绝对值的整数部分,再按比例折算为正整数积分
|
||||
*/
|
||||
public static function lossIntegerToPoints(float $winLossNet, float $ratio): int
|
||||
{
|
||||
if ($winLossNet >= 0 || $ratio <= 0) {
|
||||
return 0;
|
||||
}
|
||||
$lossInteger = intval(floor(abs($winLossNet)));
|
||||
if ($lossInteger <= 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return intval(round($lossInteger * $ratio));
|
||||
}
|
||||
|
||||
/**
|
||||
* 每日推送:昨日净输赢折算积分(受开关、商城开放日约束)
|
||||
*
|
||||
* @param array<string, mixed> $config
|
||||
*/
|
||||
public static function dailyConvertedPoints(string $businessDate, float $yesterdayWinLossNet, array $config): int
|
||||
{
|
||||
if (empty($config['daily_points_enabled'])) {
|
||||
return 0;
|
||||
}
|
||||
$openDate = $config['active_mall_open_date'] ?? null;
|
||||
if ($openDate !== null && $openDate !== '' && $businessDate < $openDate) {
|
||||
return 0;
|
||||
}
|
||||
$ratio = floatval($config['return_ratio'] ?? 0);
|
||||
|
||||
return self::lossIntegerToPoints($yesterdayWinLossNet, $ratio);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $config
|
||||
* @deprecated 使用 dailyConvertedPoints
|
||||
*/
|
||||
public static function dailyLockedIncrement(string $businessDate, float $yesterdayWinLossNet, array $config): int
|
||||
{
|
||||
return self::dailyConvertedPoints($businessDate, $yesterdayWinLossNet, $config);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public static function configForConversion(): array
|
||||
{
|
||||
return self::configFromRow(MallBusinessConfig::ensureRow());
|
||||
}
|
||||
|
||||
public static function syncClaimCap(MallUserAsset $asset): void
|
||||
{
|
||||
$lifetime = max(0, intval($asset->lifetime_converted_points ?? 0));
|
||||
$daily = max(0, intval($asset->daily_converted_points ?? 0));
|
||||
$asset->points_claim_cap = $lifetime + $daily;
|
||||
}
|
||||
|
||||
public static function applyDailyConversionDelta(MallUserAsset $asset, int $delta): void
|
||||
{
|
||||
if ($delta === 0) {
|
||||
return;
|
||||
}
|
||||
$asset->daily_converted_points = max(0, intval($asset->daily_converted_points ?? 0) + $delta);
|
||||
self::syncClaimCap($asset);
|
||||
$newLocked = intval($asset->locked_points ?? 0) + $delta;
|
||||
$asset->locked_points = max(0, $newLocked);
|
||||
}
|
||||
|
||||
public static function applyLifetimeConversion(MallUserAsset $asset, int $targetLifetimePoints): void
|
||||
{
|
||||
$current = max(0, intval($asset->lifetime_converted_points ?? 0));
|
||||
$delta = $targetLifetimePoints - $current;
|
||||
if ($delta === 0) {
|
||||
return;
|
||||
}
|
||||
$asset->lifetime_converted_points = $targetLifetimePoints;
|
||||
self::syncClaimCap($asset);
|
||||
$newLocked = intval($asset->locked_points ?? 0) + $delta;
|
||||
$asset->locked_points = max(0, $newLocked);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{processed: int, updated: int, skipped: int}
|
||||
*/
|
||||
public static function recalculateAllLifetimePoints(MallBusinessConfig $row): array
|
||||
{
|
||||
if (intval($row->lifetime_points_enabled ?? 0) !== 1) {
|
||||
throw new \RuntimeException(__('Lifetime points source is disabled'));
|
||||
}
|
||||
|
||||
$config = self::configFromRow($row);
|
||||
$ratio = floatval($config['lifetime_points_ratio'] ?? 0);
|
||||
if ($ratio < 0) {
|
||||
throw new \RuntimeException(__('Parameter error'));
|
||||
}
|
||||
|
||||
$stats = ['processed' => 0, 'updated' => 0, 'skipped' => 0];
|
||||
|
||||
$rows = Db::query(
|
||||
'SELECT d.* FROM mall_daily_push d INNER JOIN (
|
||||
SELECT user_id, MAX(`date`) AS max_date FROM mall_daily_push GROUP BY user_id
|
||||
) latest ON d.user_id = latest.user_id AND d.date = latest.max_date'
|
||||
);
|
||||
|
||||
foreach ($rows as $pushRow) {
|
||||
$stats['processed']++;
|
||||
$playxUserId = trim(strval($pushRow['user_id'] ?? ''));
|
||||
if ($playxUserId === '') {
|
||||
$stats['skipped']++;
|
||||
continue;
|
||||
}
|
||||
|
||||
$lifetimeWl = $pushRow['lifetime_win_loss_net'] ?? null;
|
||||
if ($lifetimeWl === null || $lifetimeWl === '') {
|
||||
$stats['skipped']++;
|
||||
continue;
|
||||
}
|
||||
|
||||
$target = self::lossToPoints(floatval($lifetimeWl), $ratio);
|
||||
$asset = MallUserAsset::where('playx_user_id', $playxUserId)->lock(true)->find();
|
||||
if (!$asset) {
|
||||
$stats['skipped']++;
|
||||
continue;
|
||||
}
|
||||
|
||||
$before = intval($asset->lifetime_converted_points ?? 0);
|
||||
if ($before === $target) {
|
||||
$stats['skipped']++;
|
||||
continue;
|
||||
}
|
||||
|
||||
self::applyLifetimeConversion($asset, $target);
|
||||
$asset->save();
|
||||
$stats['updated']++;
|
||||
}
|
||||
|
||||
return $stats;
|
||||
}
|
||||
|
||||
public static function remainingClaimable(MallUserAsset $asset): int
|
||||
{
|
||||
$cap = max(0, intval($asset->points_claim_cap ?? 0));
|
||||
$claimed = max(0, intval($asset->total_points_claimed ?? 0));
|
||||
|
||||
return max(0, $cap - $claimed);
|
||||
}
|
||||
|
||||
public static function resolveActiveMallOpenDate(MallBusinessConfig $row): ?string
|
||||
{
|
||||
$pending = self::normalizeDate($row->mall_open_date ?? null);
|
||||
$previous = self::normalizeDate($row->mall_open_date_previous ?? null);
|
||||
$effectiveOn = self::normalizeDate($row->mall_open_date_effective_on ?? null);
|
||||
$today = date('Y-m-d');
|
||||
|
||||
if ($pending === null) {
|
||||
return $previous;
|
||||
}
|
||||
if ($effectiveOn === null || $today >= $effectiveOn) {
|
||||
return $pending;
|
||||
}
|
||||
|
||||
return $previous;
|
||||
}
|
||||
|
||||
public static function normalizeDate(mixed $value): ?string
|
||||
{
|
||||
if ($value === null || $value === '') {
|
||||
return null;
|
||||
}
|
||||
$str = trim(strval($value));
|
||||
if ($str === '') {
|
||||
return null;
|
||||
}
|
||||
if (strlen($str) >= 10) {
|
||||
$str = substr($str, 0, 10);
|
||||
}
|
||||
$parsed = \DateTime::createFromFormat('Y-m-d', $str);
|
||||
|
||||
return $parsed && $parsed->format('Y-m-d') === $str ? $str : null;
|
||||
}
|
||||
|
||||
public static function nextCalendarDate(string $fromDate = ''): string
|
||||
{
|
||||
$base = $fromDate !== '' ? $fromDate : date('Y-m-d');
|
||||
$dt = \DateTime::createFromFormat('Y-m-d', $base);
|
||||
if (!$dt) {
|
||||
$dt = new \DateTime();
|
||||
}
|
||||
$dt->modify('+1 day');
|
||||
|
||||
return $dt->format('Y-m-d');
|
||||
}
|
||||
|
||||
public static function parseLifetimeWinLossFromMember(array $member): ?float
|
||||
{
|
||||
if (!array_key_exists('ltv_wl', $member) && !array_key_exists('lifetime_total_wl', $member) && !array_key_exists('ltv_total_wl', $member)) {
|
||||
return null;
|
||||
}
|
||||
$raw = $member['ltv_wl'] ?? ($member['lifetime_total_wl'] ?? ($member['ltv_total_wl'] ?? null));
|
||||
if ($raw === null || $raw === '') {
|
||||
return null;
|
||||
}
|
||||
if (!is_numeric($raw)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return floatval($raw);
|
||||
}
|
||||
}
|
||||
@@ -15,11 +15,50 @@ class MallBusinessConfig extends Model
|
||||
|
||||
protected bool $autoWriteTimestamp = true;
|
||||
|
||||
public const DEFAULT_RETURN_RATIO = 0.1;
|
||||
public const DEFAULT_UNLOCK_RATIO = 0.1;
|
||||
public const DEFAULT_POINTS_TO_CASH_RATIO = 0.1;
|
||||
public const DEFAULT_LIFETIME_POINTS_RATIO = 0.1;
|
||||
|
||||
protected array $type = [
|
||||
'create_time' => 'integer',
|
||||
'update_time' => 'integer',
|
||||
'return_ratio' => 'float',
|
||||
'unlock_ratio' => 'float',
|
||||
'points_to_cash_ratio' => 'float',
|
||||
'create_time' => 'integer',
|
||||
'update_time' => 'integer',
|
||||
'return_ratio' => 'float',
|
||||
'unlock_ratio' => 'float',
|
||||
'points_to_cash_ratio' => 'float',
|
||||
'daily_points_enabled' => 'integer',
|
||||
'lifetime_points_enabled' => 'integer',
|
||||
'lifetime_points_ratio' => 'float',
|
||||
'mall_open_date' => 'string',
|
||||
'mall_open_date_previous' => 'string',
|
||||
'mall_open_date_effective_on' => 'string',
|
||||
];
|
||||
|
||||
/**
|
||||
* 保证存在一条商城参数配置(比例仅来自后台 mall_business_config,不再读 .env)
|
||||
*/
|
||||
public static function ensureRow(): self
|
||||
{
|
||||
$row = self::order('id', 'asc')->find();
|
||||
if ($row) {
|
||||
return $row;
|
||||
}
|
||||
|
||||
$now = time();
|
||||
$created = self::create([
|
||||
'return_ratio' => self::DEFAULT_RETURN_RATIO,
|
||||
'unlock_ratio' => self::DEFAULT_UNLOCK_RATIO,
|
||||
'points_to_cash_ratio' => self::DEFAULT_POINTS_TO_CASH_RATIO,
|
||||
'daily_points_enabled' => 1,
|
||||
'lifetime_points_enabled' => 0,
|
||||
'lifetime_points_ratio' => self::DEFAULT_LIFETIME_POINTS_RATIO,
|
||||
'create_time' => $now,
|
||||
'update_time' => $now,
|
||||
]);
|
||||
if (!$created) {
|
||||
throw new \RuntimeException('Failed to create mall_business_config');
|
||||
}
|
||||
|
||||
return $created;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,9 +15,11 @@ class MallDailyPush extends Model
|
||||
|
||||
protected array $type = [
|
||||
'yesterday_win_loss_net' => 'float',
|
||||
'converted_points' => 'integer',
|
||||
'yesterday_total_deposit' => 'float',
|
||||
'lifetime_total_deposit' => 'float',
|
||||
'lifetime_total_withdraw' => 'float',
|
||||
'lifetime_win_loss_net' => 'float',
|
||||
'create_time' => 'integer',
|
||||
];
|
||||
}
|
||||
|
||||
@@ -24,6 +24,10 @@ class MallUserAsset extends Model
|
||||
'today_limit' => 'integer',
|
||||
'today_claimed' => 'integer',
|
||||
'admin_id' => 'integer',
|
||||
'points_claim_cap' => 'integer',
|
||||
'total_points_claimed' => 'integer',
|
||||
'lifetime_converted_points' => 'integer',
|
||||
'daily_converted_points' => 'integer',
|
||||
];
|
||||
|
||||
/**
|
||||
@@ -62,6 +66,10 @@ class MallUserAsset extends Model
|
||||
'today_limit' => 0,
|
||||
'today_claimed' => 0,
|
||||
'today_limit_date' => null,
|
||||
'points_claim_cap' => 0,
|
||||
'total_points_claimed' => 0,
|
||||
'lifetime_converted_points' => 0,
|
||||
'daily_converted_points' => 0,
|
||||
'create_time' => $now,
|
||||
'update_time' => $now,
|
||||
]);
|
||||
|
||||
Reference in New Issue
Block a user