优化每日推送和积分转化逻辑
This commit is contained in:
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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user