Files
webman-buildadmin-mall/app/common/library/MallPointsConversion.php

267 lines
8.9 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
* }
*/
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);
}
}