diff --git a/app/admin/controller/mall/PlayxConfig.php b/app/admin/controller/mall/PlayxConfig.php index 9f2663f..3ac046c 100644 --- a/app/admin/controller/mall/PlayxConfig.php +++ b/app/admin/controller/mall/PlayxConfig.php @@ -30,6 +30,7 @@ class PlayxConfig extends Backend $payload = $row->toArray(); $payload['active_mall_open_date'] = MallPointsConversion::resolveActiveMallOpenDate($row); + $payload['active_event_end_date'] = MallPointsConversion::resolveActiveEventEndDate($row); return $this->success('', [ 'row' => $payload, @@ -75,6 +76,10 @@ class PlayxConfig extends Backend } $mallOpenDate = MallPointsConversion::normalizeDate($data['mall_open_date'] ?? null); + $mallEventEndDate = MallPointsConversion::normalizeDate($data['mall_event_end_date'] ?? null); + if ($mallOpenDate !== null && $mallEventEndDate !== null && $mallEventEndDate < $mallOpenDate) { + return $this->error(__('Event end date cannot be earlier than start date')); + } $row = MallBusinessConfig::order('id', 'asc')->find(); $now = time(); @@ -90,6 +95,9 @@ class PlayxConfig extends Backend 'mall_open_date' => $mallOpenDate, 'mall_open_date_previous' => null, 'mall_open_date_effective_on' => $effectiveOn, + 'mall_event_end_date' => $mallEventEndDate, + 'mall_event_end_date_previous' => null, + 'mall_event_end_date_effective_on' => MallPointsConversion::nextCalendarDate(), 'create_time' => $now, 'update_time' => $now, ]); @@ -98,6 +106,10 @@ class PlayxConfig extends Backend $currentPending = MallPointsConversion::normalizeDate($row->mall_open_date ?? null); $openDateChanged = $mallOpenDate !== $currentPending; + $previousEnd = MallPointsConversion::resolveActiveEventEndDate($row); + $currentPendingEnd = MallPointsConversion::normalizeDate($row->mall_event_end_date ?? null); + $endDateChanged = $mallEventEndDate !== $currentPendingEnd; + $row->return_ratio = $returnF; $row->unlock_ratio = $unlockF; $row->points_to_cash_ratio = $cashF; @@ -111,6 +123,12 @@ class PlayxConfig extends Backend $row->mall_open_date_effective_on = $mallOpenDate !== null ? MallPointsConversion::nextCalendarDate() : null; } + if ($endDateChanged) { + $row->mall_event_end_date_previous = $previousEnd; + $row->mall_event_end_date = $mallEventEndDate; + $row->mall_event_end_date_effective_on = MallPointsConversion::nextCalendarDate(); + } + $row->save(); } diff --git a/app/admin/controller/mall/UserAsset.php b/app/admin/controller/mall/UserAsset.php index 0b5dd59..1dc49d9 100644 --- a/app/admin/controller/mall/UserAsset.php +++ b/app/admin/controller/mall/UserAsset.php @@ -44,6 +44,7 @@ class UserAsset extends Backend 'today_limit_date', 'points_claim_cap', 'total_points_claimed', + 'event_period_win_loss_net', 'create_time', 'update_time', ]; diff --git a/app/admin/lang/en.php b/app/admin/lang/en.php index f85d677..f31ee9d 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!', + 'Event end date cannot be earlier than start date' => 'Event end date cannot be earlier than start date', 'Lifetime points source is disabled' => 'Lifetime points source is disabled', 'Operation completed' => 'Operation completed!', 'Lifetime points calculation completed' => 'Lifetime conversion done: %d processed, %d updated, %d skipped', diff --git a/app/admin/lang/zh-cn.php b/app/admin/lang/zh-cn.php index a26b869..c54d085 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' => '参数错误!', + 'Event end date cannot be earlier than start date' => '活动结束日不能早于开始日', 'Lifetime points source is disabled' => '终身总输赢积分来源未启用', 'Operation completed' => '操作完成!', 'Lifetime points calculation completed' => '终身总输赢积分折算完成:共处理 %d 条,更新 %d 条,跳过 %d 条', diff --git a/app/api/controller/v1/Playx.php b/app/api/controller/v1/Playx.php index e87c9f8..b8db64a 100644 --- a/app/api/controller/v1/Playx.php +++ b/app/api/controller/v1/Playx.php @@ -139,6 +139,11 @@ class Playx extends Api return MallPointsConversion::configForConversion(); } + private function dailyPushSettlementDay(): string + { + return date('Y-m-d'); + } + private function syncAssetByDailyPush( string $playxUserId, string $username, @@ -146,7 +151,10 @@ class Playx extends Api int $convertedPoints, int $todayLimit, bool $applyAssetDelta, - string $phone = '' + string $phone = '', + ?array $pointsConfig = null, + ?float $yesterdayWinLossNet = null, + ?float $previousYesterdayWinLossNet = null ): ?MallUserAsset { $asset = $this->ensureAssetForPlayx($playxUserId, $username, $phone); @@ -169,6 +177,16 @@ class Playx extends Api MallPointsConversion::applyDailyConversionDelta($asset, $convertedPoints); } $asset->today_limit = $todayLimit; + if (is_array($pointsConfig) && $yesterdayWinLossNet !== null) { + MallPointsConversion::applyEventPeriodWinLossForDailyPush( + $asset, + $date, + $yesterdayWinLossNet, + $pointsConfig, + $this->dailyPushSettlementDay(), + $previousYesterdayWinLossNet + ); + } } $asset->save(); @@ -234,7 +252,7 @@ class Playx extends Api } $pointsConfig = $this->getPointsConversionConfig(); - $unlockRatio = floatval($pointsConfig['unlock_ratio'] ?? 0); + $settlementDay = $this->dailyPushSettlementDay(); $results = []; $allDeduped = true; @@ -255,8 +273,8 @@ class Playx extends Api $exists = MallDailyPush::where('user_id', $playxUserId)->where('date', $date)->find(); if ($exists) { - $convertedPoints = MallPointsConversion::dailyConvertedPoints($date, floatval($yesterdayWinLossNet), $pointsConfig); - $todayLimit = intval(round(floatval($yesterdayTotalDeposit) * $unlockRatio)); + $convertedPoints = MallPointsConversion::dailyConvertedPoints($date, floatval($yesterdayWinLossNet), $pointsConfig, $settlementDay); + $todayLimit = MallPointsConversion::dailyPushTodayLimit($date, floatval($yesterdayTotalDeposit), $pointsConfig, $settlementDay); $asset = $this->syncAssetByDailyPush($playxUserId, $username, $date, $convertedPoints, $todayLimit, false, $phone); if (!$asset) { return $this->error(__('Failed to ensure PlayX user asset')); @@ -283,16 +301,16 @@ class Playx extends Api 'lifetime_total_withdraw' => $lifetimeTotalWithdraw, 'create_time' => time(), ]; - $convertedPoints = MallPointsConversion::dailyConvertedPoints($date, floatval($yesterdayWinLossNet), $pointsConfig); + $convertedPoints = MallPointsConversion::dailyConvertedPoints($date, floatval($yesterdayWinLossNet), $pointsConfig, $settlementDay); $pushData['converted_points'] = $convertedPoints; if ($lifetimeWinLossNet !== null) { $pushData['lifetime_win_loss_net'] = $lifetimeWinLossNet; } MallDailyPush::create($pushData); - $todayLimit = intval(round(floatval($yesterdayTotalDeposit) * $unlockRatio)); + $todayLimit = MallPointsConversion::dailyPushTodayLimit($date, floatval($yesterdayTotalDeposit), $pointsConfig, $settlementDay); - $asset = $this->syncAssetByDailyPush($playxUserId, $username, $date, $convertedPoints, $todayLimit, true, $phone); + $asset = $this->syncAssetByDailyPush($playxUserId, $username, $date, $convertedPoints, $todayLimit, true, $phone, $pointsConfig, floatval($yesterdayWinLossNet)); if (!$asset) { throw new \RuntimeException(__('Failed to ensure PlayX user asset')); } @@ -336,9 +354,9 @@ class Playx extends Api $exists = MallDailyPush::where('user_id', $playxUserId)->where('date', $date)->find(); if ($exists) { $pointsConfig = $this->getPointsConversionConfig(); - $unlockRatio = floatval($pointsConfig['unlock_ratio'] ?? 0); - $convertedPoints = MallPointsConversion::dailyConvertedPoints($date, floatval($yesterdayWinLossNet), $pointsConfig); - $todayLimit = intval(round(floatval($yesterdayTotalDeposit) * $unlockRatio)); + $settlementDay = $this->dailyPushSettlementDay(); + $convertedPoints = MallPointsConversion::dailyConvertedPoints($date, floatval($yesterdayWinLossNet), $pointsConfig, $settlementDay); + $todayLimit = MallPointsConversion::dailyPushTodayLimit($date, floatval($yesterdayTotalDeposit), $pointsConfig, $settlementDay); $asset = $this->syncAssetByDailyPush( $playxUserId, strval($body['username'] ?? ''), @@ -363,6 +381,7 @@ class Playx extends Api Db::startTrans(); try { $pointsConfig = $this->getPointsConversionConfig(); + $settlementDay = $this->dailyPushSettlementDay(); $lifetimeWinLossNet = null; if (isset($body['lifetime_win_loss_net']) && is_numeric($body['lifetime_win_loss_net'])) { $lifetimeWinLossNet = floatval($body['lifetime_win_loss_net']); @@ -377,15 +396,14 @@ class Playx extends Api 'lifetime_total_withdraw' => $body['lifetime_total_withdraw'] ?? 0, 'create_time' => time(), ]; - $convertedPoints = MallPointsConversion::dailyConvertedPoints($date, floatval($yesterdayWinLossNet), $pointsConfig); + $convertedPoints = MallPointsConversion::dailyConvertedPoints($date, floatval($yesterdayWinLossNet), $pointsConfig, $settlementDay); $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)); + $todayLimit = MallPointsConversion::dailyPushTodayLimit($date, floatval($yesterdayTotalDeposit), $pointsConfig, $settlementDay); $asset = $this->syncAssetByDailyPush( $playxUserId, @@ -394,7 +412,9 @@ class Playx extends Api $convertedPoints, $todayLimit, true, - $phone + $phone, + $pointsConfig, + floatval($yesterdayWinLossNet) ); if (!$asset) { throw new \RuntimeException(__('Failed to ensure PlayX user asset')); diff --git a/app/common/library/MallDailyPushLogReplay.php b/app/common/library/MallDailyPushLogReplay.php index 3cbde21..a583c62 100644 --- a/app/common/library/MallDailyPushLogReplay.php +++ b/app/common/library/MallDailyPushLogReplay.php @@ -260,13 +260,24 @@ class MallDailyPushLogReplay $newLocked = MallPointsConversion::dailyConvertedPoints( $record['date'], floatval($record['yesterday_win_loss_net']), - $pointsConfig + $pointsConfig, + MallPointsConversion::nextCalendarDate($record['date']) ); $oldLocked = $daily - ? MallPointsConversion::dailyConvertedPoints($record['date'], $oldWinLossNet, $pointsConfig) + ? MallPointsConversion::dailyConvertedPoints( + $record['date'], + $oldWinLossNet, + $pointsConfig, + MallPointsConversion::nextCalendarDate($record['date']) + ) : 0; $lockedDelta = $newLocked - $oldLocked; - $todayLimit = intval(round($record['yesterday_total_deposit'] * $unlockRatio)); + $todayLimit = MallPointsConversion::dailyPushTodayLimit( + $record['date'], + floatval($record['yesterday_total_deposit']), + $pointsConfig, + MallPointsConversion::nextCalendarDate($record['date']) + ); $dailyData = [ 'user_id' => $record['user_id'], 'date' => $record['date'], @@ -352,6 +363,15 @@ class MallDailyPushLogReplay MallPointsConversion::applyDailyConversionDelta($asset, $lockedDelta); } $this->applyDailyLimit($asset, $record['date'], $todayLimit); + $settlementDay = MallPointsConversion::nextCalendarDate($record['date']); + MallPointsConversion::applyEventPeriodWinLossForDailyPush( + $asset, + $record['date'], + floatval($record['yesterday_win_loss_net']), + $pointsConfig, + $settlementDay, + $daily ? $oldWinLossNet : null + ); $asset->save(); } Db::commit(); diff --git a/app/common/library/MallPointsConversion.php b/app/common/library/MallPointsConversion.php index 662dff4..c77a2ac 100644 --- a/app/common/library/MallPointsConversion.php +++ b/app/common/library/MallPointsConversion.php @@ -25,7 +25,11 @@ final class MallPointsConversion * mall_open_date: ?string, * mall_open_date_previous: ?string, * mall_open_date_effective_on: ?string, - * active_mall_open_date: ?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 @@ -41,6 +45,10 @@ final class MallPointsConversion '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), ]; } @@ -74,13 +82,16 @@ final class MallPointsConversion * * @param array $config */ - public static function dailyConvertedPoints(string $businessDate, float $yesterdayWinLossNet, array $config): int - { + public static function dailyConvertedPoints( + string $businessDate, + float $yesterdayWinLossNet, + array $config, + ?string $settlementCalendarDate = null + ): int { if (empty($config['daily_points_enabled'])) { return 0; } - $openDate = $config['active_mall_open_date'] ?? null; - if ($openDate !== null && $openDate !== '' && $businessDate < $openDate) { + if (!self::isDailyPushSettlementAllowed($businessDate, $config, $settlementCalendarDate)) { return 0; } $ratio = floatval($config['return_ratio'] ?? 0); @@ -88,6 +99,85 @@ final class MallPointsConversion 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 $config * @deprecated 使用 dailyConvertedPoints @@ -219,6 +309,27 @@ final class MallPointsConversion 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 === '') { diff --git a/app/common/model/MallBusinessConfig.php b/app/common/model/MallBusinessConfig.php index f5c29a9..afde00d 100644 --- a/app/common/model/MallBusinessConfig.php +++ b/app/common/model/MallBusinessConfig.php @@ -32,6 +32,9 @@ class MallBusinessConfig extends Model 'mall_open_date' => 'string', 'mall_open_date_previous' => 'string', 'mall_open_date_effective_on' => 'string', + 'mall_event_end_date' => 'string', + 'mall_event_end_date_previous' => 'string', + 'mall_event_end_date_effective_on' => 'string', ]; /** diff --git a/app/common/model/MallUserAsset.php b/app/common/model/MallUserAsset.php index c521379..057d8ab 100644 --- a/app/common/model/MallUserAsset.php +++ b/app/common/model/MallUserAsset.php @@ -28,6 +28,7 @@ class MallUserAsset extends Model 'total_points_claimed' => 'integer', 'lifetime_converted_points' => 'integer', 'daily_converted_points' => 'integer', + 'event_period_win_loss_net' => 'float', ]; /** diff --git a/web/src/lang/backend/en/mall/pageIntro.ts b/web/src/lang/backend/en/mall/pageIntro.ts index 516c5ac..a580162 100644 --- a/web/src/lang/backend/en/mall/pageIntro.ts +++ b/web/src/lang/backend/en/mall/pageIntro.ts @@ -57,6 +57,6 @@ export default { }, playxConfig: { title: 'About this page', - desc: 'Configure daily and lifetime loss-to-points sources (switches and ratios), mall open date, unlock ratio, and points-to-cash ratio. Open date changes take effect the next calendar day.', + desc: 'Split into Event and General tabs: event start/end dates (effective next day; no conversion for end-day business data from the day after end), point sources and lifetime batch job; unlock and cash ratios under General.', }, } diff --git a/web/src/lang/backend/en/mall/playxConfig.ts b/web/src/lang/backend/en/mall/playxConfig.ts index 29fa759..9dfdbeb 100644 --- a/web/src/lang/backend/en/mall/playxConfig.ts +++ b/web/src/lang/backend/en/mall/playxConfig.ts @@ -1,9 +1,12 @@ export default { + tab_event: 'Event settings', + tab_regular: 'General settings', + section_event_period: 'Event period', 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.', + return_ratio_tip: 'On daily push: converted points = floor(|yesterday net|) × ratio (losses only). Requires event window 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.', @@ -14,12 +17,16 @@ export default { calculate_lifetime_confirm: 'Recalculate all members with the current lifetime ratio (reverses previous lifetime conversion first). Continue?', calculate_lifetime_success: 'Lifetime conversion done: {processed} processed, {updated} updated, {skipped} skipped', 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', + mall_open_date: 'Event start date', + mall_open_date_tip: 'Takes effect the next calendar day. From then, daily push rows with business date on/after start date convert yesterday loss.', + active_mall_open_date: 'Active start date', + mall_event_end_date: 'Event end date', + mall_event_end_date_tip: 'Takes effect the next day; leave empty for no end. On the day after end, rows for the end business date no longer convert points or unlock cap; later business dates are excluded too.', + active_event_end_date: 'Active end date', + event_end_before_start: 'End date cannot be earlier than start date', section_other: 'Other settings', unlock_ratio: 'Unlock ratio (unlock_ratio)', - unlock_ratio_tip: 'Daily push: daily claim cap = yesterday total deposit × this ratio.', + unlock_ratio_tip: 'Daily push: daily claim cap = yesterday total deposit × this ratio (within event window).', 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/userAsset.ts b/web/src/lang/backend/en/mall/userAsset.ts index 648d0ce..535a3ae 100644 --- a/web/src/lang/backend/en/mall/userAsset.ts +++ b/web/src/lang/backend/en/mall/userAsset.ts @@ -11,6 +11,7 @@ export default { today_limit_date: 'Daily cap date', points_claim_cap: 'Lifetime claim cap', total_points_claimed: 'Total points claimed', + event_period_win_loss_net: 'Event period net W/L (adds while open, pauses when closed, resumes on reopen)', create_time: 'Created at', update_time: 'Updated at', 'quick Search Fields': 'ID, PlayX user ID, username, phone', diff --git a/web/src/lang/backend/zh-cn/mall/pageIntro.ts b/web/src/lang/backend/zh-cn/mall/pageIntro.ts index c86dfea..29c2cc1 100644 --- a/web/src/lang/backend/zh-cn/mall/pageIntro.ts +++ b/web/src/lang/backend/zh-cn/mall/pageIntro.ts @@ -57,6 +57,6 @@ export default { }, playxConfig: { title: '页面说明', - desc: '配置昨日净输赢、终身总输赢两种积分来源(开关与兑换比例)、商城开放日,以及解锁比例与积分折算现金比例;保存后按规则生效(开放日隔日生效)。', + desc: '分为「活动配置」与「常规配置」:活动开始/结束日(隔日生效,结束日次日起不再折算该日业务数据的昨日净输赢与解锁上限)、两种积分来源及终身一键计算;常规项为解锁比例与积分折现比例。', }, } diff --git a/web/src/lang/backend/zh-cn/mall/playxConfig.ts b/web/src/lang/backend/zh-cn/mall/playxConfig.ts index ff5cbce..85681fe 100644 --- a/web/src/lang/backend/zh-cn/mall/playxConfig.ts +++ b/web/src/lang/backend/zh-cn/mall/playxConfig.ts @@ -1,9 +1,12 @@ export default { + tab_event: '活动配置', + tab_regular: '常规配置', + section_event_period: '活动时间', section_daily_source: '积分来源一:昨日净输赢', daily_points_enabled: '启用昨日净输赢转积分', daily_points_enabled_tip: '根据每日推送中的「昨日净输赢」折算待领取积分;仅统计会员输的钱(净输赢为负),赢的不计。', return_ratio: '昨日净输赢兑换比例', - return_ratio_tip: '每日推送入库时:转换积分 = floor(|昨日净输赢|) × 本比例(仅负值,结果为非负整数)。需满足商城开放日规则且本来源已启用。', + return_ratio_tip: '每日推送入库时:转换积分 = floor(|昨日净输赢|) × 本比例(仅负值,结果为非负整数)。需满足活动时间规则且本来源已启用。', section_lifetime_source: '积分来源二:终身总输赢', lifetime_points_enabled: '启用终身总输赢转积分', lifetime_points_enabled_tip: '使用每日推送中每位会员最新一条记录的「终身总输赢」字段;仅统计输的钱。第三方未传该字段时跳过。', @@ -14,12 +17,16 @@ export default { calculate_lifetime_confirm: '将按当前终身总输赢兑换比例重新折算全部会员积分,已折算部分会先扣回再按新比例增加。是否继续?', calculate_lifetime_success: '终身总输赢积分折算完成:共处理 {processed} 条,更新 {updated} 条,跳过 {skipped} 条', section_mall_open: '商城开放日', - mall_open_date: '商城开放日', - mall_open_date_tip: '设置后隔日生效。例如设为 8 月 1 日,则从 8 月 2 日起对业务日期为 8 月 1 日及之后的每日推送执行「昨日净输赢」转积分(T+1:开放日前一日产生的输赢在开放日次日开始折算)。', - active_mall_open_date: '当前生效的开放日', + mall_open_date: '活动开始日', + mall_open_date_tip: '设置后隔日生效。从生效日起,对业务日期不早于开始日的每日推送执行「昨日净输赢」转积分(T+1)。', + active_mall_open_date: '当前生效的开始日', + mall_event_end_date: '活动结束日', + mall_event_end_date_tip: '设置后隔日生效,可留空表示不限制结束。结束日次日不再对结束日当天业务数据的昨日净输赢折算积分与解锁上限;结束日之后的业务日期也不再折算。', + active_event_end_date: '当前生效的结束日', + event_end_before_start: '活动结束日不能早于开始日', section_other: '其他参数', unlock_ratio: '解锁比例(unlock_ratio)', - unlock_ratio_tip: '每日推送中:今日可领取上限 = 昨日总充值 × 本比例。', + unlock_ratio_tip: '每日推送中:今日可领取上限 = 昨日总充值 × 本比例(受活动时间规则约束)。', points_to_cash_ratio: '积分折算现金比例', points_to_cash_ratio_tip: '用于资产接口中「可提现现金」等展示:现金 ≈ 可用积分 × 本比例。', } diff --git a/web/src/lang/backend/zh-cn/mall/userAsset.ts b/web/src/lang/backend/zh-cn/mall/userAsset.ts index 46d4c10..ee2a53c 100644 --- a/web/src/lang/backend/zh-cn/mall/userAsset.ts +++ b/web/src/lang/backend/zh-cn/mall/userAsset.ts @@ -11,6 +11,7 @@ export default { today_limit_date: '今日上限日期', points_claim_cap: '领取积分上限', total_points_claimed: '累计已领取积分', + event_period_win_loss_net: '活动期累计净输赢(开放时累加,关闭暂停,再开续累)', create_time: '创建时间', update_time: '修改时间', 'quick Search Fields': 'ID、playX用户ID、用户名、手机号', diff --git a/web/src/views/backend/mall/order/index.vue b/web/src/views/backend/mall/order/index.vue index 7483532..6a2f5a5 100644 --- a/web/src/views/backend/mall/order/index.vue +++ b/web/src/views/backend/mall/order/index.vue @@ -93,7 +93,7 @@ const baTable = new baTableClass( prop: 'status', align: 'center', effect: 'dark', - custom: { PENDING: 'success', COMPLETED: 'primary', SHIPPED: 'info', REJECTED: 'loading' }, + custom: { PENDING: 'success', COMPLETED: 'primary', SHIPPED: 'info', REJECTED: 'danger' }, minWidth: 100, operator: 'eq', sortable: false, diff --git a/web/src/views/backend/mall/playxConfig/index.vue b/web/src/views/backend/mall/playxConfig/index.vue index 4838475..e383f17 100644 --- a/web/src/views/backend/mall/playxConfig/index.vue +++ b/web/src/views/backend/mall/playxConfig/index.vue @@ -14,96 +14,117 @@ @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_event_period') }} + + +
{{ t('mall.playxConfig.mall_open_date_tip') }}
+
+ {{ t('mall.playxConfig.active_mall_open_date') }}:{{ activeMallOpenDate }} +
+
+ + +
{{ t('mall.playxConfig.mall_event_end_date_tip') }}
+
+ {{ t('mall.playxConfig.active_event_end_date') }}:{{ activeEventEndDate }} +
+
- {{ 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_daily_source') }} + + +
{{ t('mall.playxConfig.daily_points_enabled_tip') }}
+
+ + +
{{ t('mall.playxConfig.return_ratio_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_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_other') }} - - -
{{ t('mall.playxConfig.unlock_ratio_tip') }}
-
- - -
{{ t('mall.playxConfig.points_to_cash_ratio_tip') }}
-
- + + {{ t('mall.playxConfig.section_other') }} + + +
{{ t('mall.playxConfig.unlock_ratio_tip') }}
+
+ + +
{{ t('mall.playxConfig.points_to_cash_ratio_tip') }}
+
+
+
+ + {{ t('Save') }} @@ -129,7 +150,9 @@ const loading = ref(true) const submitting = ref(false) const calculating = ref(false) const remark = ref('') +const activeTab = ref('event') const activeMallOpenDate = ref('') +const activeEventEndDate = ref('') const form = reactive({ return_ratio: 0.1, @@ -139,13 +162,26 @@ const form = reactive({ lifetime_points_enabled: false, lifetime_points_ratio: 0.1, mall_open_date: '' as string | null, + mall_event_end_date: '' as string | null, }) +const validateEventDates = (_rule: unknown, _value: unknown, callback: (err?: Error) => void) => { + const start = form.mall_open_date + const end = form.mall_event_end_date + if (start && end && end < start) { + callback(new Error(t('mall.playxConfig.event_end_before_start'))) + return + } + callback() +} + 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') }) }], + mall_event_end_date: [{ validator: validateEventDates, trigger: 'change' }], + mall_open_date: [{ validator: validateEventDates, trigger: 'change' }], } const loadData = () => { @@ -155,6 +191,7 @@ const loadData = () => { remark.value = res.data.remark ?? '' const row = res.data.row ?? {} activeMallOpenDate.value = row.active_mall_open_date ?? '' + activeEventEndDate.value = row.active_event_end_date ?? '' if (row.return_ratio !== undefined && row.return_ratio !== null) { form.return_ratio = parseFloat(String(row.return_ratio)) } @@ -170,6 +207,7 @@ const loadData = () => { 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 + form.mall_event_end_date = row.mall_event_end_date ? String(row.mall_event_end_date).slice(0, 10) : null }) .finally(() => { loading.value = false @@ -188,6 +226,7 @@ const onSubmit = () => { lifetime_points_enabled: form.lifetime_points_enabled ? 1 : 0, lifetime_points_ratio: form.lifetime_points_ratio, mall_open_date: form.mall_open_date || null, + mall_event_end_date: form.mall_event_end_date || null, }) .then(() => { loadData() @@ -236,4 +275,11 @@ onMounted(() => { color: var(--el-text-color-secondary); line-height: 1.5; } +.playx-config-tabs { + margin-bottom: 8px; +} +.playx-config-submit { + margin-top: 16px; + margin-bottom: 0; +} diff --git a/web/src/views/backend/mall/userAsset/index.vue b/web/src/views/backend/mall/userAsset/index.vue index 1c1bdf2..fe4e987 100644 --- a/web/src/views/backend/mall/userAsset/index.vue +++ b/web/src/views/backend/mall/userAsset/index.vue @@ -116,6 +116,14 @@ const baTable = new baTableClass( operator: 'RANGE', sortable: true, }, + { + label: t('mall.userAsset.event_period_win_loss_net'), + prop: 'event_period_win_loss_net', + align: 'center', + minWidth: 160, + operator: 'RANGE', + sortable: true, + }, { label: t('mall.userAsset.today_limit_date'), prop: 'today_limit_date', diff --git a/web/src/views/backend/mall/userAsset/popupForm.vue b/web/src/views/backend/mall/userAsset/popupForm.vue index e738067..596b864 100644 --- a/web/src/views/backend/mall/userAsset/popupForm.vue +++ b/web/src/views/backend/mall/userAsset/popupForm.vue @@ -77,6 +77,13 @@ :input-attr="{ step: 1, min: 0 }" :placeholder="t('Please input field', { field: t('mall.userAsset.today_limit') })" /> +