diff --git a/.env-example b/.env-example index cf21271..009255a 100644 --- a/.env-example +++ b/.env-example @@ -22,13 +22,7 @@ DATABASE_CHARSET = utf8mb4 DATABASE_PREFIX = # PlayX 配置 -# 以下三项比例以数据库表 mall_business_config 为准(后台 积分商城 → 商城参数配置);无记录时回退为下列 env 默认值 -# 提现折算:积分 -> 现金(如 10 分 = 1 元,则 points_to_cash_ratio=0.1) -PLAYX_POINTS_TO_CASH_RATIO=0.1 -# 返还比例:新增保障金 = ABS(yesterday_win_loss_net) * return_ratio(仅亏损时) -PLAYX_RETURN_RATIO=0.1 -# 解锁比例:今日可领取上限 = yesterday_total_deposit * unlock_ratio -PLAYX_UNLOCK_RATIO=0.1 +# 积分折算、昨日净输赢/终身总输赢兑换比例等均在后台「积分商城 → 商城参数配置」(mall_business_config),不再使用下方 env 比例项 # Daily Push 签名校验密钥(HMAC,建议从部署系统注入,避免写入代码/仓库) PLAYX_DAILY_PUSH_SECRET= # 第三方每日推送原始日志保留天数(runtime/logs/daily_push_raw,默认 30) diff --git a/app/admin/controller/mall/DailyPush.php b/app/admin/controller/mall/DailyPush.php index c832ad8..a0c6803 100644 --- a/app/admin/controller/mall/DailyPush.php +++ b/app/admin/controller/mall/DailyPush.php @@ -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', ]; diff --git a/app/admin/controller/mall/PlayxConfig.php b/app/admin/controller/mall/PlayxConfig.php index 9aaa8fa..8ae7aee 100644 --- a/app/admin/controller/mall/PlayxConfig.php +++ b/app/admin/controller/mall/PlayxConfig.php @@ -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); + } } diff --git a/app/admin/controller/mall/UserAsset.php b/app/admin/controller/mall/UserAsset.php index 7d7f156..0b5dd59 100644 --- a/app/admin/controller/mall/UserAsset.php +++ b/app/admin/controller/mall/UserAsset.php @@ -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', ]; diff --git a/app/admin/lang/en.php b/app/admin/lang/en.php index db7cab1..d1dfe68 100644 --- a/app/admin/lang/en.php +++ b/app/admin/lang/en.php @@ -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.', diff --git a/app/admin/lang/zh-cn.php b/app/admin/lang/zh-cn.php index 5f8de9f..9f797dc 100644 --- a/app/admin/lang/zh-cn.php +++ b/app/admin/lang/zh-cn.php @@ -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' => '没有文件被上传', diff --git a/app/api/controller/v1/Playx.php b/app/api/controller/v1/Playx.php index 53c507e..4948a78 100644 --- a/app/api/controller/v1/Playx.php +++ b/app/api/controller/v1/Playx.php @@ -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 + */ + 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(); diff --git a/app/common/library/MallDailyPushExport.php b/app/common/library/MallDailyPushExport.php index 2040089..5672721 100644 --- a/app/common/library/MallDailyPushExport.php +++ b/app/common/library/MallDailyPushExport.php @@ -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', ]; diff --git a/app/common/library/MallDailyPushLogReplay.php b/app/common/library/MallDailyPushLogReplay.php index 647e89d..3cbde21 100644 --- a/app/common/library/MallDailyPushLogReplay.php +++ b/app/common/library/MallDailyPushLogReplay.php @@ -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', diff --git a/app/common/library/MallPlayxRatios.php b/app/common/library/MallPlayxRatios.php index 1598ef0..47cbc35 100644 --- a/app/common/library/MallPlayxRatios.php +++ b/app/common/library/MallPlayxRatios.php @@ -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); diff --git a/app/common/library/MallPointsConversion.php b/app/common/library/MallPointsConversion.php new file mode 100644 index 0000000..a14440d --- /dev/null +++ b/app/common/library/MallPointsConversion.php @@ -0,0 +1,266 @@ + 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 $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 $config + * @deprecated 使用 dailyConvertedPoints + */ + public static function dailyLockedIncrement(string $businessDate, float $yesterdayWinLossNet, array $config): int + { + return self::dailyConvertedPoints($businessDate, $yesterdayWinLossNet, $config); + } + + /** + * @return array + */ + 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); + } +} diff --git a/app/common/model/MallBusinessConfig.php b/app/common/model/MallBusinessConfig.php index f5a4f60..f5c29a9 100644 --- a/app/common/model/MallBusinessConfig.php +++ b/app/common/model/MallBusinessConfig.php @@ -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; + } } diff --git a/app/common/model/MallDailyPush.php b/app/common/model/MallDailyPush.php index 1fd33af..5b7d990 100644 --- a/app/common/model/MallDailyPush.php +++ b/app/common/model/MallDailyPush.php @@ -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', ]; } diff --git a/app/common/model/MallUserAsset.php b/app/common/model/MallUserAsset.php index bbbafa7..c521379 100644 --- a/app/common/model/MallUserAsset.php +++ b/app/common/model/MallUserAsset.php @@ -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, ]); diff --git a/config/playx.php b/config/playx.php index cecd36a..5cbdf8b 100644 --- a/config/playx.php +++ b/config/playx.php @@ -4,13 +4,6 @@ * PlayX 积分商城对接配置 */ return [ - /** - * 以下三项比例以数据表 mall_business_config 为准(后台「商城参数配置」)。 - * 此处保留 env 仅作兼容/文档默认值;业务代码请使用 MallPlayxRatios::get()。 - */ - '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')), // Daily Push 签名校验(PlayX 调用商城时使用) 'daily_push_secret' => strval(env('PLAYX_DAILY_PUSH_SECRET', '')), /** 第三方每日推送原始日志保留天数(runtime/logs/daily_push_raw) */ diff --git a/web/src/api/backend/mall/playxConfig.ts b/web/src/api/backend/mall/playxConfig.ts index 6b18ae9..e6ed998 100644 --- a/web/src/api/backend/mall/playxConfig.ts +++ b/web/src/api/backend/mall/playxConfig.ts @@ -5,6 +5,7 @@ export const url = '/admin/mall.PlayxConfig/' export const actionUrl = new Map([ ['index', url + 'index'], ['save', url + 'save'], + ['calculateLifetimePoints', url + 'calculateLifetimePoints'], ]) export function index() { @@ -26,3 +27,15 @@ export function save(data: anyObj) { } ) } + +export function calculateLifetimePoints() { + return createAxios( + { + url: actionUrl.get('calculateLifetimePoints'), + method: 'post', + }, + { + showSuccessMessage: true, + } + ) +} diff --git a/web/src/lang/backend/en/mall/dailyPush.ts b/web/src/lang/backend/en/mall/dailyPush.ts index e0c69f5..5e3e423 100644 --- a/web/src/lang/backend/en/mall/dailyPush.ts +++ b/web/src/lang/backend/en/mall/dailyPush.ts @@ -4,9 +4,11 @@ export default { 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', 'quick Search Fields': 'ID', export_excel: 'Export Excel', diff --git a/web/src/lang/backend/en/mall/playxConfig.ts b/web/src/lang/backend/en/mall/playxConfig.ts index 6307a6f..c0401d8 100644 --- a/web/src/lang/backend/en/mall/playxConfig.ts +++ b/web/src/lang/backend/en/mall/playxConfig.ts @@ -1,8 +1,24 @@ export default { - return_ratio: 'Return ratio (return_ratio)', - return_ratio_tip: 'In daily push, when yesterday net win/loss is negative: locked increment = |net| × this ratio.', + section_daily_source: 'Source 1: yesterday net win/loss', + daily_points_enabled: 'Enable daily loss to points', + daily_points_enabled_tip: 'Convert yesterday net win/loss from daily push to locked points; only losses (negative net) count.', + return_ratio: 'Yesterday loss conversion ratio', + return_ratio_tip: 'On daily push: converted points = floor(|yesterday net|) × ratio (losses only, non-negative integer). Requires mall open date and enabled source.', + section_lifetime_source: 'Source 2: lifetime net win/loss', + lifetime_points_enabled: 'Enable lifetime loss to points', + lifetime_points_enabled_tip: 'Uses the latest daily push row per member; only losses. Skips when lifetime field is missing.', + lifetime_points_ratio: 'Lifetime loss conversion ratio', + lifetime_points_ratio_tip: 'One-click job: locked points from |lifetime net| × ratio. Re-run reverses prior lifetime conversion then applies the new ratio.', + calculate_lifetime_btn: 'Calculate lifetime loss points', + calculate_lifetime_tip: 'Batch update from latest daily push lifetime field and ratio above.', + calculate_lifetime_confirm: 'Recalculate all members with the current lifetime ratio (reverses previous lifetime conversion first). Continue?', + section_mall_open: 'Mall open date', + mall_open_date: 'Mall open date', + mall_open_date_tip: 'Takes effect the next calendar day. E.g. set Aug 1: from Aug 2, daily push rows with business date on/after Aug 1 convert yesterday loss to points.', + active_mall_open_date: 'Active open date', + section_other: 'Other settings', unlock_ratio: 'Unlock ratio (unlock_ratio)', - unlock_ratio_tip: 'In daily push: daily claim cap = yesterday total deposit × this ratio.', + unlock_ratio_tip: 'Daily push: daily claim cap = yesterday total deposit × this ratio.', points_to_cash_ratio: 'Points to cash ratio', points_to_cash_ratio_tip: 'For withdrawable cash display: cash ≈ available points × this ratio.', } diff --git a/web/src/lang/backend/en/mall/playxDailyPush.ts b/web/src/lang/backend/en/mall/playxDailyPush.ts index 277fcc5..13fff88 100644 --- a/web/src/lang/backend/en/mall/playxDailyPush.ts +++ b/web/src/lang/backend/en/mall/playxDailyPush.ts @@ -4,9 +4,11 @@ export default { date: 'date', username: 'username', yesterday_win_loss_net: 'yesterday_win_loss_net', + 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_win_loss_net', create_time: 'create_time', 'quick Search Fields': 'id', } diff --git a/web/src/lang/backend/en/mall/userAsset.ts b/web/src/lang/backend/en/mall/userAsset.ts index 49b3482..3908987 100644 --- a/web/src/lang/backend/en/mall/userAsset.ts +++ b/web/src/lang/backend/en/mall/userAsset.ts @@ -9,6 +9,8 @@ export default { today_limit: 'today_limit', today_claimed: 'today_claimed', today_limit_date: 'today_limit_date', + points_claim_cap: 'Points claim cap', + total_points_claimed: 'Total points claimed', create_time: 'create_time', update_time: 'update_time', 'quick Search Fields': 'id, playX_user_id, username, phone', diff --git a/web/src/lang/backend/zh-cn/mall/dailyPush.ts b/web/src/lang/backend/zh-cn/mall/dailyPush.ts index 7d2ee36..e14abf1 100644 --- a/web/src/lang/backend/zh-cn/mall/dailyPush.ts +++ b/web/src/lang/backend/zh-cn/mall/dailyPush.ts @@ -4,9 +4,11 @@ export default { 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: '创建时间', 'quick Search Fields': 'ID', export_excel: 'Excel导出', diff --git a/web/src/lang/backend/zh-cn/mall/playxConfig.ts b/web/src/lang/backend/zh-cn/mall/playxConfig.ts index 6233f26..fc6fc38 100644 --- a/web/src/lang/backend/zh-cn/mall/playxConfig.ts +++ b/web/src/lang/backend/zh-cn/mall/playxConfig.ts @@ -1,6 +1,22 @@ export default { - return_ratio: '返还比例(return_ratio)', - return_ratio_tip: '每日推送中,仅当昨日净输赢为负时:待领取增量 = |净输赢| × 本比例。', + section_daily_source: '积分来源一:昨日净输赢', + daily_points_enabled: '启用昨日净输赢转积分', + daily_points_enabled_tip: '根据每日推送中的「昨日净输赢」折算待领取积分;仅统计会员输的钱(净输赢为负),赢的不计。', + return_ratio: '昨日净输赢兑换比例', + return_ratio_tip: '每日推送入库时:转换积分 = floor(|昨日净输赢|) × 本比例(仅负值,结果为非负整数)。需满足商城开放日规则且本来源已启用。', + section_lifetime_source: '积分来源二:终身总输赢', + lifetime_points_enabled: '启用终身总输赢转积分', + lifetime_points_enabled_tip: '使用每日推送中每位会员最新一条记录的「终身总输赢」字段;仅统计输的钱。第三方未传该字段时跳过。', + lifetime_points_ratio: '终身总输赢兑换比例', + lifetime_points_ratio_tip: '一键计算时:待领取增量按 |终身总输赢| × 本比例折算。重复执行会先按已记入的终身折算额扣回,再按新比例增加。', + calculate_lifetime_btn: '一键计算终身总输赢', + calculate_lifetime_tip: '按各会员最新每日推送中的终身总输赢与上方比例,批量更新待领取积分与领取上限。无终身总输赢数据的会员将跳过。', + calculate_lifetime_confirm: '将按当前终身总输赢兑换比例重新折算全部会员积分,已折算部分会先扣回再按新比例增加。是否继续?', + section_mall_open: '商城开放日', + mall_open_date: '商城开放日', + mall_open_date_tip: '设置后隔日生效。例如设为 8 月 1 日,则从 8 月 2 日起对业务日期为 8 月 1 日及之后的每日推送执行「昨日净输赢」转积分(T+1:开放日前一日产生的输赢在开放日次日开始折算)。', + active_mall_open_date: '当前生效的开放日', + section_other: '其他参数', unlock_ratio: '解锁比例(unlock_ratio)', unlock_ratio_tip: '每日推送中:今日可领取上限 = 昨日总充值 × 本比例。', points_to_cash_ratio: '积分折算现金比例', diff --git a/web/src/lang/backend/zh-cn/mall/playxDailyPush.ts b/web/src/lang/backend/zh-cn/mall/playxDailyPush.ts index f5c6cfb..a029f3e 100644 --- a/web/src/lang/backend/zh-cn/mall/playxDailyPush.ts +++ b/web/src/lang/backend/zh-cn/mall/playxDailyPush.ts @@ -4,9 +4,11 @@ export default { 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: '创建时间', 'quick Search Fields': 'ID', } diff --git a/web/src/lang/backend/zh-cn/mall/userAsset.ts b/web/src/lang/backend/zh-cn/mall/userAsset.ts index b0772a8..46d4c10 100644 --- a/web/src/lang/backend/zh-cn/mall/userAsset.ts +++ b/web/src/lang/backend/zh-cn/mall/userAsset.ts @@ -9,6 +9,8 @@ export default { today_limit: '今日可领取上限', today_claimed: '今日已领取', today_limit_date: '今日上限日期', + points_claim_cap: '领取积分上限', + total_points_claimed: '累计已领取积分', create_time: '创建时间', update_time: '修改时间', 'quick Search Fields': 'ID、playX用户ID、用户名、手机号', diff --git a/web/src/views/backend/mall/dailyPush/exportDialog.vue b/web/src/views/backend/mall/dailyPush/exportDialog.vue index 12179e4..5a68376 100644 --- a/web/src/views/backend/mall/dailyPush/exportDialog.vue +++ b/web/src/views/backend/mall/dailyPush/exportDialog.vue @@ -92,9 +92,11 @@ const fieldOptions = computed(() => [ { value: 'date', label: t('mall.dailyPush.date') }, { value: 'username', label: t('mall.dailyPush.username') }, { value: 'yesterday_win_loss_net', label: t('mall.dailyPush.yesterday_win_loss_net') }, + { value: 'converted_points', label: t('mall.dailyPush.converted_points') }, { value: 'yesterday_total_deposit', label: t('mall.dailyPush.yesterday_total_deposit') }, { value: 'lifetime_total_deposit', label: t('mall.dailyPush.lifetime_total_deposit') }, { value: 'lifetime_total_withdraw', label: t('mall.dailyPush.lifetime_total_withdraw') }, + { value: 'lifetime_win_loss_net', label: t('mall.dailyPush.lifetime_win_loss_net') }, { value: 'create_time', label: t('mall.dailyPush.create_time') }, ]) diff --git a/web/src/views/backend/mall/dailyPush/index.vue b/web/src/views/backend/mall/dailyPush/index.vue index 382ff6f..983d077 100644 --- a/web/src/views/backend/mall/dailyPush/index.vue +++ b/web/src/views/backend/mall/dailyPush/index.vue @@ -81,6 +81,14 @@ const baTable = new baTableClass( operator: 'RANGE', sortable: 'custom', }, + { + label: t('mall.dailyPush.converted_points'), + prop: 'converted_points', + align: 'center', + operator: 'RANGE', + sortable: 'custom', + width: 100, + }, { label: t('mall.dailyPush.yesterday_total_deposit'), prop: 'yesterday_total_deposit', @@ -103,6 +111,14 @@ const baTable = new baTableClass( operator: 'RANGE', sortable: 'custom', }, + { + label: t('mall.dailyPush.lifetime_win_loss_net'), + prop: 'lifetime_win_loss_net', + align: 'center', + minWidth: 95, + operator: 'RANGE', + sortable: 'custom', + }, { label: t('mall.dailyPush.create_time'), prop: 'create_time', diff --git a/web/src/views/backend/mall/playxConfig/index.vue b/web/src/views/backend/mall/playxConfig/index.vue index dd1cbf3..ae95aa5 100644 --- a/web/src/views/backend/mall/playxConfig/index.vue +++ b/web/src/views/backend/mall/playxConfig/index.vue @@ -14,6 +14,11 @@ @submit.prevent="" @keyup.enter="onSubmit()" > + {{ t('mall.playxConfig.section_daily_source') }} + + +
{{ t('mall.playxConfig.daily_points_enabled_tip') }}
+
{{ t('mall.playxConfig.return_ratio_tip') }}
+ + {{ t('mall.playxConfig.section_lifetime_source') }} + + +
{{ t('mall.playxConfig.lifetime_points_enabled_tip') }}
+
+ + +
{{ t('mall.playxConfig.lifetime_points_ratio_tip') }}
+
+ + + {{ t('mall.playxConfig.calculate_lifetime_btn') }} + +
{{ t('mall.playxConfig.calculate_lifetime_tip') }}
+
+ + {{ t('mall.playxConfig.section_mall_open') }} + + +
{{ t('mall.playxConfig.mall_open_date_tip') }}
+
+ {{ t('mall.playxConfig.active_mall_open_date') }}:{{ activeMallOpenDate }} +
+
+ + {{ t('mall.playxConfig.section_other') }} import type { FormInstance, FormRules } from 'element-plus' +import { ElMessageBox } from 'element-plus' import { onMounted, reactive, ref, useTemplateRef } from 'vue' import { useI18n } from 'vue-i18n' -import { index, save } from '/@/api/backend/mall/playxConfig' +import { calculateLifetimePoints, index, save } from '/@/api/backend/mall/playxConfig' import MallPageIntro from '/@/components/mall/MallPageIntro.vue' defineOptions({ @@ -73,18 +127,25 @@ const { t } = useI18n() const formRef = useTemplateRef('formRef') const loading = ref(true) const submitting = ref(false) +const calculating = ref(false) const remark = ref('') +const activeMallOpenDate = ref('') const form = reactive({ return_ratio: 0.1, unlock_ratio: 0.1, points_to_cash_ratio: 0.1, + daily_points_enabled: true, + lifetime_points_enabled: false, + lifetime_points_ratio: 0.1, + mall_open_date: '' as string | null, }) const rules: FormRules = { return_ratio: [{ required: true, message: t('Please input field', { field: t('mall.playxConfig.return_ratio') }) }], unlock_ratio: [{ required: true, message: t('Please input field', { field: t('mall.playxConfig.unlock_ratio') }) }], points_to_cash_ratio: [{ required: true, message: t('Please input field', { field: t('mall.playxConfig.points_to_cash_ratio') }) }], + lifetime_points_ratio: [{ required: true, message: t('Please input field', { field: t('mall.playxConfig.lifetime_points_ratio') }) }], } const loadData = () => { @@ -93,6 +154,7 @@ const loadData = () => { .then((res) => { remark.value = res.data.remark ?? '' const row = res.data.row ?? {} + activeMallOpenDate.value = row.active_mall_open_date ?? '' if (row.return_ratio !== undefined && row.return_ratio !== null) { form.return_ratio = parseFloat(String(row.return_ratio)) } @@ -102,6 +164,12 @@ const loadData = () => { if (row.points_to_cash_ratio !== undefined && row.points_to_cash_ratio !== null) { form.points_to_cash_ratio = parseFloat(String(row.points_to_cash_ratio)) } + form.daily_points_enabled = Number(row.daily_points_enabled ?? 1) === 1 + form.lifetime_points_enabled = Number(row.lifetime_points_enabled ?? 0) === 1 + if (row.lifetime_points_ratio !== undefined && row.lifetime_points_ratio !== null) { + form.lifetime_points_ratio = parseFloat(String(row.lifetime_points_ratio)) + } + form.mall_open_date = row.mall_open_date ? String(row.mall_open_date).slice(0, 10) : null }) .finally(() => { loading.value = false @@ -116,6 +184,10 @@ const onSubmit = () => { return_ratio: form.return_ratio, unlock_ratio: form.unlock_ratio, points_to_cash_ratio: form.points_to_cash_ratio, + daily_points_enabled: form.daily_points_enabled ? 1 : 0, + lifetime_points_enabled: form.lifetime_points_enabled ? 1 : 0, + lifetime_points_ratio: form.lifetime_points_ratio, + mall_open_date: form.mall_open_date || null, }) .then(() => { loadData() @@ -126,6 +198,21 @@ const onSubmit = () => { }) } +const onCalculateLifetime = () => { + ElMessageBox.confirm(t('mall.playxConfig.calculate_lifetime_confirm'), t('Reminder'), { + type: 'warning', + }).then(() => { + calculating.value = true + calculateLifetimePoints() + .then(() => { + loadData() + }) + .finally(() => { + calculating.value = false + }) + }) +} + onMounted(() => { loadData() }) diff --git a/web/src/views/backend/mall/playxDailyPush/index.vue b/web/src/views/backend/mall/playxDailyPush/index.vue index f7e188d..1843619 100644 --- a/web/src/views/backend/mall/playxDailyPush/index.vue +++ b/web/src/views/backend/mall/playxDailyPush/index.vue @@ -39,9 +39,11 @@ const baTable = new baTableClass( { label: t('mall.playxDailyPush.date'), prop: 'date', align: 'center', render: 'date', operator: 'RANGE', comSearchRender: 'date', sortable: 'custom', width: 120, operatorPlaceholder: t('Fuzzy query') }, { label: t('mall.playxDailyPush.username'), prop: 'username', align: 'center', operatorPlaceholder: t('Fuzzy query'), sortable: false, operator: 'LIKE' }, { label: t('mall.playxDailyPush.yesterday_win_loss_net'), prop: 'yesterday_win_loss_net', align: 'center', operator: 'RANGE', sortable: false }, + { label: t('mall.playxDailyPush.converted_points'), prop: 'converted_points', align: 'center', operator: 'RANGE', sortable: false, width: 100 }, { label: t('mall.playxDailyPush.yesterday_total_deposit'), prop: 'yesterday_total_deposit', align: 'center', operator: 'RANGE', sortable: false }, { label: t('mall.playxDailyPush.lifetime_total_deposit'), prop: 'lifetime_total_deposit', align: 'center', operator: 'RANGE', sortable: false }, { label: t('mall.playxDailyPush.lifetime_total_withdraw'), prop: 'lifetime_total_withdraw', align: 'center', operator: 'RANGE', sortable: false }, + { label: t('mall.playxDailyPush.lifetime_win_loss_net'), prop: 'lifetime_win_loss_net', align: 'center', operator: 'RANGE', sortable: false }, { label: t('mall.playxDailyPush.create_time'), prop: 'create_time', align: 'center', render: 'datetime', operator: 'RANGE', comSearchRender: 'datetime', sortable: 'custom', width: 160, timeFormat: 'yyyy-mm-dd hh:MM:ss' }, ], dblClickNotEditColumn: [undefined], diff --git a/web/src/views/backend/mall/userAsset/index.vue b/web/src/views/backend/mall/userAsset/index.vue index eb943dc..2d033a3 100644 --- a/web/src/views/backend/mall/userAsset/index.vue +++ b/web/src/views/backend/mall/userAsset/index.vue @@ -68,6 +68,8 @@ const baTable = new baTableClass( { label: t('mall.userAsset.available_points'), prop: 'available_points', align: 'center', operator: 'RANGE', sortable: false }, { label: t('mall.userAsset.today_limit'), prop: 'today_limit', align: 'center', operator: 'RANGE', sortable: false }, { label: t('mall.userAsset.today_claimed'), prop: 'today_claimed', align: 'center', operator: 'RANGE', sortable: false }, + { label: t('mall.userAsset.points_claim_cap'), prop: 'points_claim_cap', align: 'center', operator: 'RANGE', sortable: false }, + { label: t('mall.userAsset.total_points_claimed'), prop: 'total_points_claimed', align: 'center', operator: 'RANGE', sortable: false }, { label: t('mall.userAsset.today_limit_date'), prop: 'today_limit_date',