378 lines
13 KiB
PHP
378 lines
13 KiB
PHP
<?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,
|
|
* mall_event_end_date: ?string,
|
|
* mall_event_end_date_previous: ?string,
|
|
* mall_event_end_date_effective_on: ?string,
|
|
* active_event_end_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),
|
|
'mall_event_end_date' => self::normalizeDate($row->mall_event_end_date ?? null),
|
|
'mall_event_end_date_previous' => self::normalizeDate($row->mall_event_end_date_previous ?? null),
|
|
'mall_event_end_date_effective_on' => self::normalizeDate($row->mall_event_end_date_effective_on ?? null),
|
|
'active_event_end_date' => self::resolveActiveEventEndDate($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,
|
|
?string $settlementCalendarDate = null
|
|
): int {
|
|
if (empty($config['daily_points_enabled'])) {
|
|
return 0;
|
|
}
|
|
if (!self::isDailyPushSettlementAllowed($businessDate, $config, $settlementCalendarDate)) {
|
|
return 0;
|
|
}
|
|
$ratio = floatval($config['return_ratio'] ?? 0);
|
|
|
|
return self::lossIntegerToPoints($yesterdayWinLossNet, $ratio);
|
|
}
|
|
|
|
/**
|
|
* 每日推送是否允许折算昨日净输赢、昨日充值解锁上限(活动开始/结束日规则)
|
|
*/
|
|
public static function isDailyPushSettlementAllowed(
|
|
string $businessDate,
|
|
array $config,
|
|
?string $settlementCalendarDate = null
|
|
): bool {
|
|
$settlementDay = self::normalizeDate($settlementCalendarDate ?? date('Y-m-d')) ?? date('Y-m-d');
|
|
$start = self::normalizeDate($config['active_mall_open_date'] ?? null);
|
|
if ($start !== null && $businessDate < $start) {
|
|
return false;
|
|
}
|
|
|
|
$end = self::normalizeDate($config['active_event_end_date'] ?? null);
|
|
if ($end === null) {
|
|
return true;
|
|
}
|
|
if ($businessDate > $end) {
|
|
return false;
|
|
}
|
|
|
|
$endPlusOne = self::nextCalendarDate($end);
|
|
if ($settlementDay === $endPlusOne && $businessDate >= $end) {
|
|
return false;
|
|
}
|
|
if ($settlementDay > $endPlusOne) {
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* 活动开放期内按推送累加净输赢;关闭期间不累加,再开启后接着累加(不重置)。
|
|
*
|
|
* @param float|null $previousWinLossNet 更新已有每日推送时传旧「昨日净输赢」;新建推送传 null
|
|
*/
|
|
public static function applyEventPeriodWinLossForDailyPush(
|
|
MallUserAsset $asset,
|
|
string $businessDate,
|
|
float $yesterdayWinLossNet,
|
|
array $config,
|
|
?string $settlementCalendarDate = null,
|
|
?float $previousWinLossNet = null
|
|
): void {
|
|
if (!self::isDailyPushSettlementAllowed($businessDate, $config, $settlementCalendarDate)) {
|
|
return;
|
|
}
|
|
|
|
$current = floatval($asset->event_period_win_loss_net ?? 0);
|
|
if ($previousWinLossNet === null) {
|
|
$asset->event_period_win_loss_net = round($current + $yesterdayWinLossNet, 2);
|
|
|
|
return;
|
|
}
|
|
|
|
$delta = $yesterdayWinLossNet - $previousWinLossNet;
|
|
if ($delta === 0.0) {
|
|
return;
|
|
}
|
|
|
|
$asset->event_period_win_loss_net = round($current + $delta, 2);
|
|
}
|
|
|
|
public static function dailyPushTodayLimit(
|
|
string $businessDate,
|
|
float $yesterdayTotalDeposit,
|
|
array $config,
|
|
?string $settlementCalendarDate = null
|
|
): int {
|
|
if (!self::isDailyPushSettlementAllowed($businessDate, $config, $settlementCalendarDate)) {
|
|
return 0;
|
|
}
|
|
$unlockRatio = floatval($config['unlock_ratio'] ?? 0);
|
|
|
|
return intval(round($yesterdayTotalDeposit * $unlockRatio));
|
|
}
|
|
|
|
/**
|
|
* @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 resolveActiveEventEndDate(MallBusinessConfig $row): ?string
|
|
{
|
|
$pending = self::normalizeDate($row->mall_event_end_date ?? null);
|
|
$previous = self::normalizeDate($row->mall_event_end_date_previous ?? null);
|
|
$effectiveOn = self::normalizeDate($row->mall_event_end_date_effective_on ?? null);
|
|
$today = date('Y-m-d');
|
|
|
|
if ($pending === null) {
|
|
if ($effectiveOn !== null && $today >= $effectiveOn) {
|
|
return 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);
|
|
}
|
|
}
|