优化每日推送和积分转化逻辑
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
]);
|
||||
|
||||
@@ -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) */
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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.',
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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导出',
|
||||
|
||||
@@ -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: '积分折算现金比例',
|
||||
|
||||
@@ -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',
|
||||
}
|
||||
|
||||
@@ -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、用户名、手机号',
|
||||
|
||||
@@ -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') },
|
||||
])
|
||||
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -14,6 +14,11 @@
|
||||
@submit.prevent=""
|
||||
@keyup.enter="onSubmit()"
|
||||
>
|
||||
<el-divider content-position="left">{{ t('mall.playxConfig.section_daily_source') }}</el-divider>
|
||||
<el-form-item :label="t('mall.playxConfig.daily_points_enabled')" prop="daily_points_enabled">
|
||||
<el-switch v-model="form.daily_points_enabled" />
|
||||
<div class="form-tip">{{ t('mall.playxConfig.daily_points_enabled_tip') }}</div>
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('mall.playxConfig.return_ratio')" prop="return_ratio">
|
||||
<el-input-number
|
||||
v-model="form.return_ratio"
|
||||
@@ -23,9 +28,57 @@
|
||||
:precision="2"
|
||||
controls-position="right"
|
||||
class="w100"
|
||||
:disabled="!form.daily_points_enabled"
|
||||
/>
|
||||
<div class="form-tip">{{ t('mall.playxConfig.return_ratio_tip') }}</div>
|
||||
</el-form-item>
|
||||
|
||||
<el-divider content-position="left">{{ t('mall.playxConfig.section_lifetime_source') }}</el-divider>
|
||||
<el-form-item :label="t('mall.playxConfig.lifetime_points_enabled')" prop="lifetime_points_enabled">
|
||||
<el-switch v-model="form.lifetime_points_enabled" />
|
||||
<div class="form-tip">{{ t('mall.playxConfig.lifetime_points_enabled_tip') }}</div>
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('mall.playxConfig.lifetime_points_ratio')" prop="lifetime_points_ratio">
|
||||
<el-input-number
|
||||
v-model="form.lifetime_points_ratio"
|
||||
:min="0"
|
||||
:max="100"
|
||||
:step="0.01"
|
||||
:precision="2"
|
||||
controls-position="right"
|
||||
class="w100"
|
||||
:disabled="!form.lifetime_points_enabled"
|
||||
/>
|
||||
<div class="form-tip">{{ t('mall.playxConfig.lifetime_points_ratio_tip') }}</div>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button
|
||||
type="warning"
|
||||
:loading="calculating"
|
||||
:disabled="!form.lifetime_points_enabled"
|
||||
@click="onCalculateLifetime()"
|
||||
>
|
||||
{{ t('mall.playxConfig.calculate_lifetime_btn') }}
|
||||
</el-button>
|
||||
<div class="form-tip">{{ t('mall.playxConfig.calculate_lifetime_tip') }}</div>
|
||||
</el-form-item>
|
||||
|
||||
<el-divider content-position="left">{{ t('mall.playxConfig.section_mall_open') }}</el-divider>
|
||||
<el-form-item :label="t('mall.playxConfig.mall_open_date')" prop="mall_open_date">
|
||||
<el-date-picker
|
||||
v-model="form.mall_open_date"
|
||||
type="date"
|
||||
value-format="YYYY-MM-DD"
|
||||
class="w100"
|
||||
:placeholder="t('Please select field', { field: t('mall.playxConfig.mall_open_date') })"
|
||||
/>
|
||||
<div class="form-tip">{{ t('mall.playxConfig.mall_open_date_tip') }}</div>
|
||||
<div v-if="activeMallOpenDate" class="form-tip">
|
||||
{{ t('mall.playxConfig.active_mall_open_date') }}:{{ activeMallOpenDate }}
|
||||
</div>
|
||||
</el-form-item>
|
||||
|
||||
<el-divider content-position="left">{{ t('mall.playxConfig.section_other') }}</el-divider>
|
||||
<el-form-item :label="t('mall.playxConfig.unlock_ratio')" prop="unlock_ratio">
|
||||
<el-input-number
|
||||
v-model="form.unlock_ratio"
|
||||
@@ -60,9 +113,10 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
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<FormInstance>('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()
|
||||
})
|
||||
|
||||
@@ -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],
|
||||
|
||||
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user