1.优化开奖和推送

2.新增控制连续开奖赔率
This commit is contained in:
2026-04-20 10:02:27 +08:00
parent c184fa8a46
commit 24aab111b5
18 changed files with 749 additions and 37 deletions

View File

@@ -3,6 +3,8 @@
namespace app\admin\controller\config;
use app\common\controller\Backend;
use app\common\library\game\DepositTier;
use app\common\library\game\StreakWinReward;
use app\common\library\game\ZiHuaDictionary as ZiHuaDictionaryLib;
use support\Response;
use Webman\Http\Request as WebmanRequest;
@@ -33,7 +35,19 @@ class GameConfig extends Backend
}
/**
* 列表:排除独立表单维护的 36 字花字典
* @return list<string>
*/
protected function excludedConfigKeys(): array
{
return [
ZiHuaDictionaryLib::CONFIG_KEY,
DepositTier::CONFIG_KEY,
StreakWinReward::CONFIG_KEY,
];
}
/**
* 列表:排除独立表单维护的配置键
*/
protected function _index(): Response
{
@@ -45,7 +59,7 @@ class GameConfig extends Backend
$table = strtolower($this->model->getTable());
$mainShort = $alias[$table] ?? '';
if ($mainShort !== '') {
$where[] = [$mainShort . '.config_key', '<>', ZiHuaDictionaryLib::CONFIG_KEY];
$where[] = [$mainShort . '.config_key', 'not in', $this->excludedConfigKeys()];
}
$res = $this->model
@@ -63,4 +77,51 @@ class GameConfig extends Backend
'remark' => get_route_remark(),
]);
}
/**
* 远程下拉:排除独立维护的配置键
*/
protected function _select(): Response
{
if (empty($this->model)) {
return $this->success('', [
'list' => [],
'total' => 0,
]);
}
$pk = $this->model->getPk();
$fields = [$pk];
$quickSearchArr = is_array($this->quickSearchField) ? $this->quickSearchField : explode(',', (string) $this->quickSearchField);
foreach ($quickSearchArr as $f) {
$f = trim((string) $f);
if ($f === '') {
continue;
}
$f = str_contains($f, '.') ? substr($f, strrpos($f, '.') + 1) : $f;
if ($f !== '' && !in_array($f, $fields, true)) {
$fields[] = $f;
}
}
list($where, $alias, $limit, $order) = $this->queryBuilder();
$table = strtolower($this->model->getTable());
$mainShort = $alias[$table] ?? '';
if ($mainShort !== '') {
$where[] = [$mainShort . '.config_key', 'not in', $this->excludedConfigKeys()];
}
$res = $this->model
->field($fields)
->alias($alias)
->where($where)
->order($order)
->paginate($limit);
return $this->success('', [
'list' => $res->items(),
'total' => $res->total(),
]);
}
}

View File

@@ -0,0 +1,121 @@
<?php
declare(strict_types=1);
namespace app\admin\controller\config;
use app\common\controller\Backend;
use app\common\library\game\StreakWinReward as StreakWinRewardLib;
use support\think\Db;
use support\Response;
use Throwable;
use Webman\Http\Request as WebmanRequest;
/**
* 连胜奖励game_config.streak_win_reward
*/
class StreakWinReward extends Backend
{
protected bool $modelValidate = false;
protected array $noNeedPermission = ['index', 'save'];
private function hasNodePermission(WebmanRequest $request, string $action): bool
{
if (!$this->auth) {
return false;
}
$controllerPath = get_controller_path($request);
if (!$controllerPath) {
return false;
}
$paths = [];
$paths[] = $controllerPath . '/' . $action;
$parts = explode('/', $controllerPath);
foreach ($parts as &$part) {
if (str_contains($part, '_')) {
$part = lcfirst(str_replace(' ', '', ucwords(str_replace('_', ' ', $part))));
}
}
$paths[] = implode('/', $parts) . '/' . $action;
foreach (array_values(array_unique($paths)) as $path) {
if ($this->auth->check($path)) {
return true;
}
}
return false;
}
protected function initController(WebmanRequest $request): ?Response
{
return null;
}
public function index(WebmanRequest $request): Response
{
$response = $this->initializeBackend($request);
if ($response !== null) {
return $response;
}
if (!$this->hasNodePermission($request, 'index')) {
return $this->error(__('You have no permission'), [], 401);
}
if ($request->method() !== 'GET') {
return $this->error(__('Parameter error'));
}
$row = Db::name('game_config')->where('config_key', StreakWinRewardLib::CONFIG_KEY)->find();
$rows = StreakWinRewardLib::parseFromConfigValue($row['config_value'] ?? null);
return $this->success('', [
'rows' => $rows,
]);
}
public function save(WebmanRequest $request): Response
{
$response = $this->initializeBackend($request);
if ($response !== null) {
return $response;
}
if (!$this->hasNodePermission($request, 'save')) {
return $this->error(__('You have no permission'), [], 401);
}
if ($request->method() !== 'POST') {
return $this->error(__('Parameter error'));
}
$payload = $request->post('rows');
if (!is_array($payload)) {
return $this->error('参数错误');
}
$encoded = StreakWinRewardLib::encodeForDb($payload);
$now = time();
Db::startTrans();
try {
$exists = Db::name('game_config')->where('config_key', StreakWinRewardLib::CONFIG_KEY)->find();
if ($exists) {
Db::name('game_config')->where('config_key', StreakWinRewardLib::CONFIG_KEY)->update([
'config_value' => $encoded,
'update_time' => $now,
]);
} else {
Db::name('game_config')->insert([
'config_key' => StreakWinRewardLib::CONFIG_KEY,
'config_value' => $encoded,
'value_type' => 'json',
'remark' => '连胜奖励',
'create_time' => $now,
'update_time' => $now,
]);
}
Db::commit();
} catch (Throwable $e) {
Db::rollback();
return $this->error($e->getMessage());
}
StreakWinRewardLib::clearCache();
return $this->success('保存成功');
}
}

View File

@@ -138,7 +138,7 @@ class Game extends MobileBase
* 提交下注入参极简——period_no + numbers + bet_amount整笔总金额 + idempotency_key。
*
* 下注判定:开奖号码 ∈ pick_numbers 即算中奖,赔付按整笔 total_amount × odds 计算
* odds 定义见 GameBetSettleService::BASE_ODDS 与 streak_at_bet
* 派彩 = 压注总额 × 连胜奖励表 odds_factorstreak_at_bet 为下注时快照)。
*/
public function betPlace(Request $request): Response
{

View File

@@ -0,0 +1,205 @@
<?php
declare(strict_types=1);
namespace app\common\library\game;
use support\think\Db;
/**
* 连胜奖励game_config.streak_win_reward按「连胜档位」110 配置赔率系数与是否大奖。
*
* 派彩total_amount × odds_factor与后台「连胜奖励」表一致不再额外 ×33
*/
final class StreakWinReward
{
public const CONFIG_KEY = 'streak_win_reward';
/** @var list<array{streak: int, odds_factor: int, is_jackpot: bool}>|null */
private static ?array $cache = null;
public static function clearCache(): void
{
self::$cache = null;
}
/**
* @return list<array{streak: int, odds_factor: int, is_jackpot: bool}>
*/
public static function defaultRows(): array
{
$out = [];
for ($s = 1; $s <= 10; $s++) {
$out[] = [
'streak' => $s,
'odds_factor' => $s,
'is_jackpot' => $s === 10,
];
}
return $out;
}
/**
* @param mixed $raw
*
* @return list<array{streak: int, odds_factor: int, is_jackpot: bool}>
*/
public static function parseFromConfigValue($raw): array
{
if (!is_string($raw) || trim($raw) === '') {
return self::defaultRows();
}
$decoded = json_decode($raw, true);
if (!is_array($decoded)) {
return self::defaultRows();
}
$list = $decoded['rows'] ?? $decoded;
if (!is_array($list)) {
return self::defaultRows();
}
$byStreak = [];
foreach ($list as $row) {
if (!is_array($row)) {
continue;
}
$streak = isset($row['streak']) && is_numeric($row['streak']) ? (int) $row['streak'] : 0;
if ($streak < 1 || $streak > 10) {
continue;
}
$factor = isset($row['odds_factor']) && is_numeric($row['odds_factor']) ? (int) $row['odds_factor'] : $streak;
if ($factor < 1) {
$factor = 1;
}
$jack = !empty($row['is_jackpot']);
$byStreak[$streak] = [
'streak' => $streak,
'odds_factor' => $factor,
'is_jackpot' => $jack,
];
}
$out = [];
for ($s = 1; $s <= 10; $s++) {
$out[] = $byStreak[$s] ?? [
'streak' => $s,
'odds_factor' => $s,
'is_jackpot' => $s === 10,
];
}
return $out;
}
/**
* 从库加载并缓存
*
* @return list<array{streak: int, odds_factor: int, is_jackpot: bool}>
*/
public static function loadRows(): array
{
if (self::$cache !== null) {
return self::$cache;
}
$row = Db::name('game_config')->where('config_key', self::CONFIG_KEY)->find();
self::$cache = self::parseFromConfigValue($row['config_value'] ?? null);
return self::$cache;
}
/**
* streak_at_bet 为下注时快照0 表示尚未连胜);档位取 min(streak_at_bet+1, 10)。
*/
public static function levelFromStreakAtBet(int $streakAtBet): int
{
$level = $streakAtBet + 1;
if ($level < 1) {
$level = 1;
}
if ($level > 10) {
$level = 10;
}
return $level;
}
/**
* @return array{streak: int, odds_factor: int, is_jackpot: bool}
*/
public static function rowForStreakAtBet(int $streakAtBet): array
{
$level = self::levelFromStreakAtBet($streakAtBet);
foreach (self::loadRows() as $row) {
if ((int) ($row['streak'] ?? 0) === $level) {
return $row;
}
}
return [
'streak' => $level,
'odds_factor' => $level,
'is_jackpot' => $level === 10,
];
}
public static function isJackpotForStreakAtBet(int $streakAtBet): bool
{
return self::rowForStreakAtBet($streakAtBet)['is_jackpot'] === true;
}
/**
* 返回该注单适用的「赔率乘数」字符串(= 配置档位的 odds_factor供 bcmul(total_amount, ..., 4)。
*/
public static function totalOddsMultiplierForStreakAtBet(int $streakAtBet): string
{
$factor = (int) self::rowForStreakAtBet($streakAtBet)['odds_factor'];
if ($factor < 1) {
$factor = 1;
}
return bcadd((string) $factor, '0', 4);
}
/**
* @param list<array{streak: int, odds_factor: int, is_jackpot: bool}> $rows
*/
public static function encodeForDb(array $rows): string
{
$normalized = [];
for ($s = 1; $s <= 10; $s++) {
$found = null;
foreach ($rows as $r) {
if (!is_array($r)) {
continue;
}
$st = isset($r['streak']) && is_numeric($r['streak']) ? (int) $r['streak'] : 0;
if ($st === $s) {
$found = $r;
break;
}
}
if ($found === null) {
$normalized[] = [
'streak' => $s,
'odds_factor' => $s,
'is_jackpot' => $s === 10,
];
} else {
$f = isset($found['odds_factor']) && is_numeric($found['odds_factor']) ? (int) $found['odds_factor'] : $s;
if ($f < 1) {
$f = 1;
}
$normalized[] = [
'streak' => $s,
'odds_factor' => $f,
'is_jackpot' => !empty($found['is_jackpot']),
];
}
}
$json = json_encode(['rows' => $normalized], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
if ($json === false) {
return '{"rows":[]}';
}
return $json;
}
}

View File

@@ -4,25 +4,27 @@ declare(strict_types=1);
namespace app\common\service;
use app\common\library\game\StreakWinReward;
use support\think\Db;
use Throwable;
/**
* 开奖后结算注单:写入 win_amount、status=已结算;中奖时入账并记 user_wallet_recordbiz_type=payout
* 连胜赔率来自 game_config.streak_win_reward结算后更新 user.current_streak未中奖则连胜归 0
*/
final class GameBetSettleService
{
private const BASE_ODDS = 33;
/**
* 对指定期次按开奖号码结算所有「待开奖」注单;同一注单幂等(仅 status=1 会更新)。
*
* @return array{jackpot_hits: list<array{user_id: int, period_no: string, total_win: string, result_number: int}>}
*
* @throws Throwable
*/
public static function settleBetsForDraw(int $recordId, int $resultNumber): void
public static function settleBetsForDraw(int $recordId, int $resultNumber): array
{
if ($recordId <= 0 || $resultNumber < 1) {
return;
return ['jackpot_hits' => []];
}
$now = time();
@@ -36,12 +38,26 @@ final class GameBetSettleService
/** @var array<int, array{period_no: string, total_win: string, balance_after: string, orders: list<array{order_no: string, win_amount: string, hit: bool}>}> */
$aggregateByUser = [];
/** @var array<int, array{streak_at: int, had_win: bool}> */
$userOutcome = [];
/** @var array<int, true> */
$jackpotNotify = [];
foreach ($bets as $bet) {
$betId = (int) ($bet['id'] ?? 0);
if ($betId <= 0) {
continue;
}
$userId = (int) ($bet['user_id'] ?? 0);
if ($userId > 0 && !isset($userOutcome[$userId])) {
$userOutcome[$userId] = [
'streak_at' => (int) ($bet['streak_at_bet'] ?? 0),
'had_win' => false,
];
}
$win = self::computeWinAmount($bet, $resultNumber);
$jackpot = '0.0000';
@@ -59,10 +75,17 @@ final class GameBetSettleService
continue;
}
// 结算刚刚成功status 1 → 2把本单下注总额 1:1 累加到用户打码量
self::creditUserBetFlow($bet, $now);
$userId = (int) ($bet['user_id'] ?? 0);
if ($userId > 0) {
if (bccomp($win, '0', 4) > 0) {
$userOutcome[$userId]['had_win'] = true;
}
if (bccomp($win, '0', 4) > 0 && StreakWinReward::isJackpotForStreakAtBet((int) ($bet['streak_at_bet'] ?? 0))) {
$jackpotNotify[$userId] = true;
}
}
if ($userId <= 0) {
continue;
}
@@ -93,6 +116,23 @@ final class GameBetSettleService
];
}
foreach ($userOutcome as $userId => $info) {
$streakAt = (int) ($info['streak_at'] ?? 0);
$hadWin = (bool) ($info['had_win'] ?? false);
if ($hadWin) {
$next = $streakAt + 1;
if ($next > 10) {
$next = 10;
}
} else {
$next = 0;
}
Db::name('user')->where('id', $userId)->update([
'current_streak' => $next,
'update_time' => $now,
]);
}
foreach ($aggregateByUser as $userId => $agg) {
$hitOrderCount = 0;
foreach ($agg['orders'] as $o) {
@@ -119,6 +159,25 @@ final class GameBetSettleService
]);
}
}
$jackpotHits = [];
foreach ($jackpotNotify as $uid => $_) {
if (!isset($aggregateByUser[$uid])) {
continue;
}
$agg = $aggregateByUser[$uid];
if (bccomp($agg['total_win'], '0', 4) <= 0) {
continue;
}
$jackpotHits[] = [
'user_id' => (int) $uid,
'period_no' => (string) ($agg['period_no'] ?? ''),
'total_win' => (string) $agg['total_win'],
'result_number' => $resultNumber,
];
}
return ['jackpot_hits' => $jackpotHits];
}
/**
@@ -150,8 +209,9 @@ final class GameBetSettleService
}
Db::startTrans();
try {
self::settleBetsForDraw($rid, $rn);
$out = self::settleBetsForDraw($rid, $rn);
Db::commit();
JackpotPushService::publishHits($out['jackpot_hits'] ?? []);
$count++;
} catch (Throwable $e) {
Db::rollback();
@@ -163,7 +223,7 @@ final class GameBetSettleService
}
/**
* 应付派彩:开奖号码 ∈ pick_numbers 即中奖;整笔 total_amount × (连胜+1) × 33与 GameLiveService 一致)。
* 应付派彩:开奖号码 ∈ pick_numbers 即中奖;整笔 total_amount × odds_factorodds_factor 来自连胜奖励表对应档位)。
*/
public static function computeWinAmount(array $bet, int $resultNumber): string
{
@@ -180,7 +240,7 @@ final class GameBetSettleService
}
$total = (string) ($bet['total_amount'] ?? '0');
$streak = (int) ($bet['streak_at_bet'] ?? 0);
$odds = (string) (($streak + 1) * self::BASE_ODDS);
$odds = StreakWinReward::totalOddsMultiplierForStreakAtBet($streak);
return bcmul($total, $odds, 4);
}
@@ -206,7 +266,6 @@ final class GameBetSettleService
if (bccomp($flow, '0', 4) <= 0) {
return;
}
// 原子加法:避免读-改-写导致的并发覆盖;$flow 已由 bcadd 归一化为纯数字字符串,不存在 SQL 注入
Db::name('user')
->where('id', $userId)
->update([

View File

@@ -4,13 +4,13 @@ declare(strict_types=1);
namespace app\common\service;
use app\common\library\game\StreakWinReward;
use support\think\Db;
use Throwable;
use Webman\Push\Api;
final class GameLiveService
{
private const BASE_ODDS = 33;
private const CHANNEL = 'game-live';
private const EVENT = 'bet-updated';
@@ -327,6 +327,7 @@ final class GameLiveService
$now = time();
$payoutUntil = $now + self::PAYOUT_GRACE_SECONDS;
$settleOut = ['jackpot_hits' => []];
Db::startTrans();
try {
Db::name('game_record')->where('id', (int) $record['id'])->update([
@@ -337,14 +338,19 @@ final class GameLiveService
'payout_until' => $payoutUntil,
'update_time' => $now,
]);
GameBetSettleService::settleBetsForDraw((int) $record['id'], $finalNumber);
$settleOut = GameBetSettleService::settleBetsForDraw((int) $record['id'], $finalNumber);
Db::commit();
GameRecordStatService::refreshForRecordId((int) $record['id']);
} catch (Throwable $e) {
Db::rollback();
return ['ok' => false, 'msg' => $e->getMessage()];
}
try {
GameRecordStatService::refreshForRecordId((int) $record['id']);
} catch (Throwable) {
}
JackpotPushService::publishHits($settleOut['jackpot_hits'] ?? []);
self::publishPublicPeriodOpened((string) $record['period_no'], $finalNumber, $now);
self::publishPublicPeriodPayout((string) $record['period_no'], $finalNumber, $payoutUntil);
self::publishSnapshot(null);
@@ -690,7 +696,7 @@ final class GameLiveService
}
$total = (string) ($bet['total_amount'] ?? '0');
$streak = (int) ($bet['streak_at_bet'] ?? 0);
$odds = (string) (($streak + 1) * self::BASE_ODDS);
$odds = StreakWinReward::totalOddsMultiplierForStreakAtBet($streak);
$orderPayout = bcmul($total, $odds, 4);
$payout = bcadd($payout, $orderPayout, 4);
}

View File

@@ -4,6 +4,7 @@ declare(strict_types=1);
namespace app\common\service;
use app\common\library\game\StreakWinReward;
use support\think\Db;
/**
@@ -11,8 +12,6 @@ use support\think\Db;
*/
final class GameRecordStatService
{
private const BASE_ODDS = 33;
/**
* 根据注单与开奖号码回写 game_record 统计字段(已结束对局)。
*/
@@ -82,7 +81,7 @@ final class GameRecordStatService
}
/**
* 与 GameLiveService::estimateLossForNumber 中派彩一致:命中号码时 total_amount × (streak+1) × 33
* 与 GameLiveService::estimateLossForNumber 一致:命中号码时 total_amount × odds_factor
*/
private static function estimatePayoutForBet(array $bet, int $resultNumber): string
{
@@ -99,7 +98,7 @@ final class GameRecordStatService
}
$total = (string) ($bet['total_amount'] ?? '0');
$streak = (int) ($bet['streak_at_bet'] ?? 0);
$odds = (string) (($streak + 1) * self::BASE_ODDS);
$odds = StreakWinReward::totalOddsMultiplierForStreakAtBet($streak);
return bcmul($total, $odds, 4);
}

View File

@@ -0,0 +1,72 @@
<?php
declare(strict_types=1);
namespace app\common\service;
use Throwable;
use Webman\Push\Api;
/**
* 大奖派彩:玩家私有频道 + 公共频道(对局频道 + 公告频道,便于大厅与公告测试页均能收到)
*/
final class JackpotPushService
{
private const CHANNEL_GAME_PERIOD = 'public-game-period';
private const CHANNEL_OPERATION_NOTICE = 'public-operation-notice';
private const EVT_JACKPOT_HIT = 'jackpot.hit';
/**
* @param list<array{user_id: int, period_no: string, total_win: string, result_number: int}> $hits
*/
public static function publishHits(array $hits): void
{
foreach ($hits as $h) {
$uid = (int) ($h['user_id'] ?? 0);
if ($uid <= 0) {
continue;
}
$periodNo = (string) ($h['period_no'] ?? '');
$totalWin = (string) ($h['total_win'] ?? '0');
$rn = (int) ($h['result_number'] ?? 0);
UserPushService::publish($uid, UserPushService::EVT_JACKPOT_HIT, [
'period_no' => $periodNo,
'total_win_amount' => $totalWin,
'result_number' => $rn,
'is_jackpot' => true,
]);
self::publishPublicChannels($periodNo, $uid, $totalWin, $rn);
}
}
/**
* @param array<string, mixed> $payload
*/
private static function triggerChannel(Api $api, string $channel, array $payload): void
{
$api->trigger($channel, self::EVT_JACKPOT_HIT, $payload);
}
private static function publishPublicChannels(string $periodNo, int $userId, string $totalWin, int $resultNumber): void
{
try {
$api = new Api(
str_replace('0.0.0.0', '127.0.0.1', (string) config('plugin.webman.push.app.api')),
(string) config('plugin.webman.push.app.app_key'),
(string) config('plugin.webman.push.app.app_secret')
);
$payload = [
'period_no' => $periodNo,
'user_id' => $userId,
'total_win_amount' => $totalWin,
'result_number' => $resultNumber,
'message' => '恭喜玩家命中大奖派彩',
];
self::triggerChannel($api, self::CHANNEL_GAME_PERIOD, $payload);
self::triggerChannel($api, self::CHANNEL_OPERATION_NOTICE, $payload);
} catch (Throwable) {
}
}
}

View File

@@ -20,6 +20,9 @@ final class UserPushService
public const EVT_WALLET_CHANGED = 'wallet.changed';
/** 命中配置为「大奖」的连胜档派彩(私有频道) */
public const EVT_JACKPOT_HIT = 'jackpot.hit';
private static function channelName(string $uuid): string
{
return 'private-user-' . $uuid;

View File

@@ -0,0 +1,8 @@
export default {
desc: 'Streak levels 110: payout = bet total × odds_factor. Jackpot rows trigger jackpot.hit on the user private channel, public-game-period, and public-operation-notice when won.',
btn_save: 'Save',
streak: 'Streak (rounds)',
odds_factor: 'Odds factor',
is_jackpot: 'Jackpot',
err_factor: 'Row {no}: odds factor must be ≥ 1',
}

View File

@@ -1,3 +1,3 @@
export default {
tip: 'Subscribe to public-game-period (global period channel) for period.tick / period.locked / period.opened / period.payout. The server must publish to this channel.',
tip: 'Subscribe to public-game-period (global period channel) for period.tick / period.locked / period.opened / period.payout / jackpot.hit. The server must publish to this channel.',
}

View File

@@ -0,0 +1,8 @@
export default {
desc: '110 档连胜:派彩 = 压注总额 × 赔率系数odds_factor。勾选「大奖」的档位在中奖时会对玩家私有频道、public-game-period 与 public-operation-notice 推送 jackpot.hit。',
btn_save: '保存',
streak: '连胜档(局)',
odds_factor: '赔率系数',
is_jackpot: '是否大奖',
err_factor: '第 {no} 行赔率系数须 ≥1',
}

View File

@@ -1,3 +1,3 @@
export default {
tip: '订阅文档中的「全局对局频道」public-game-period用于验证 period.tick / period.locked / period.opened / period.payout 等公共事件(需服务端向该频道推送)。',
tip: '订阅文档中的「全局对局频道」public-game-period用于验证 period.tick / period.locked / period.opened / period.payout / jackpot.hit 等公共事件(需服务端向该频道推送)。',
}

View File

@@ -1,3 +1,3 @@
export default {
tip: '订阅文档中的「公告广播频道」public-operation-notice,用于验证 notice.popout 等全站公告类推送(需服务端向该频道推送。',
tip: '订阅「公告广播频道」public-operation-notice:全站公告 notice.popout 与本游戏 jackpot.hit连胜大奖广播均会发往此频道需服务端推送。',
}

View File

@@ -7,6 +7,7 @@ const DOC_EVENTS = [
'period.locked',
'period.opened',
'period.payout',
'jackpot.hit',
'bet.accepted',
'bet.settled',
'wallet.changed',

View File

@@ -14,32 +14,45 @@
</el-button>
</div>
<el-table v-loading="loading" border stripe :data="items" row-key="_rowKey" max-height="720">
<el-table-column prop="sort" :label="t('config.depositTier.sort')" width="100" align="center">
<el-table
v-loading="loading"
border
stripe
:data="items"
row-key="_rowKey"
max-height="720"
class="deposit-tier-table"
header-align="center"
>
<el-table-column prop="sort" :label="t('config.depositTier.sort')" width="100" align="center" header-align="center">
<template #default="{ row }">
<el-input-number v-model="row.sort" :min="0" :max="9999" :controls="false" style="width: 100%" />
<div class="cell-center">
<el-input-number v-model="row.sort" :min="0" :max="9999" :controls="false" style="width: 100%; max-width: 160px" />
</div>
</template>
</el-table-column>
<el-table-column :label="t('config.depositTier.status')" width="100" align="center">
<el-table-column :label="t('config.depositTier.status')" width="100" align="center" header-align="center">
<template #default="{ row }">
<el-switch v-model="row.status" :active-value="1" :inactive-value="0" />
<div class="cell-center">
<el-switch v-model="row.status" :active-value="1" :inactive-value="0" />
</div>
</template>
</el-table-column>
<el-table-column :label="t('config.depositTier.title_col')" min-width="180">
<el-table-column :label="t('config.depositTier.title_col')" min-width="180" align="center" header-align="center">
<template #default="{ row }">
<el-input v-model="row.title" maxlength="64" :placeholder="t('config.depositTier.title_ph')" />
</template>
</el-table-column>
<el-table-column :label="t('config.depositTier.title_en_col')" min-width="180">
<el-table-column :label="t('config.depositTier.title_en_col')" min-width="180" align="center" header-align="center">
<template #default="{ row }">
<el-input v-model="row.title_en" maxlength="64" :placeholder="t('config.depositTier.title_en_ph')" />
</template>
</el-table-column>
<el-table-column :label="t('config.depositTier.amount')" min-width="140">
<el-table-column :label="t('config.depositTier.amount')" min-width="140" align="center" header-align="center">
<template #default="{ row }">
<el-input v-model="row.amount" :placeholder="t('config.depositTier.amount_ph')">
<template #suffix>
@@ -49,7 +62,7 @@
</template>
</el-table-column>
<el-table-column :label="t('config.depositTier.bonus_amount')" min-width="140">
<el-table-column :label="t('config.depositTier.bonus_amount')" min-width="140" align="center" header-align="center">
<template #default="{ row }">
<el-input v-model="row.bonus_amount" :placeholder="t('config.depositTier.bonus_ph')">
<template #suffix>
@@ -59,25 +72,25 @@
</template>
</el-table-column>
<el-table-column :label="t('config.depositTier.desc_col')" min-width="220">
<el-table-column :label="t('config.depositTier.desc_col')" min-width="220" align="center" header-align="center">
<template #default="{ row }">
<el-input v-model="row.desc" maxlength="255" :autosize="{ minRows: 1, maxRows: 3 }" type="textarea" :placeholder="t('config.depositTier.desc_ph')" />
</template>
</el-table-column>
<el-table-column :label="t('config.depositTier.desc_en_col')" min-width="220">
<el-table-column :label="t('config.depositTier.desc_en_col')" min-width="220" align="center" header-align="center">
<template #default="{ row }">
<el-input v-model="row.desc_en" maxlength="255" :autosize="{ minRows: 1, maxRows: 3 }" type="textarea" :placeholder="t('config.depositTier.desc_en_ph')" />
</template>
</el-table-column>
<el-table-column :label="t('config.depositTier.tier_id')" width="140">
<el-table-column :label="t('config.depositTier.tier_id')" width="140" align="center" header-align="center">
<template #default="{ row }">
<el-text class="tier-id" truncated>{{ row.id || t('config.depositTier.auto_id') }}</el-text>
</template>
</el-table-column>
<el-table-column :label="t('config.depositTier.operate')" width="90" align="center" fixed="right">
<el-table-column :label="t('config.depositTier.operate')" width="90" align="center" header-align="center" fixed="right">
<template #default="{ $index }">
<el-button type="danger" link @click="onRemove($index)">
{{ t('config.depositTier.btn_remove') }}
@@ -248,4 +261,18 @@ onMounted(() => {
font-size: 12px;
}
}
.deposit-tier-table {
:deep(.el-table__header th),
:deep(.el-table__body td) {
text-align: center;
}
}
.cell-center {
display: flex;
justify-content: center;
align-items: center;
width: 100%;
}
</style>

View File

@@ -35,6 +35,7 @@ const baTable = new baTableClass(
new baTableApi('/admin/config.GameConfig/'),
{
pk: 'id',
filter: { page: 1, limit: 50 },
column: [
{ type: 'selection', align: 'center', operator: false },
{ label: t('config.gameConfig.id'), prop: 'id', align: 'center', width: 80, operator: 'RANGE', sortable: 'custom' },

View File

@@ -0,0 +1,141 @@
<template>
<div class="default-main ba-table-box streak-reward-page">
<el-alert type="info" :closable="false" show-icon>
{{ t('config.streakWinReward.desc') }}
</el-alert>
<div class="toolbar">
<el-button type="success" :loading="saving" :disabled="loading" @click="onSave">
{{ t('config.streakWinReward.btn_save') }}
</el-button>
</div>
<el-table
v-loading="loading"
border
stripe
:data="rows"
max-height="720"
class="streak-reward-table"
header-align="center"
>
<el-table-column prop="streak" :label="t('config.streakWinReward.streak')" width="110" align="center" header-align="center" />
<el-table-column :label="t('config.streakWinReward.odds_factor')" min-width="200" align="center" header-align="center">
<template #default="{ row }">
<div class="cell-center">
<el-input-number v-model="row.odds_factor" :min="1" :max="999" :step="1" :controls="true" />
</div>
</template>
</el-table-column>
<el-table-column :label="t('config.streakWinReward.is_jackpot')" min-width="90" align="center" header-align="center">
<template #default="{ row }">
<div class="cell-center">
<el-switch v-model="row.is_jackpot" />
</div>
</template>
</el-table-column>
</el-table>
</div>
</template>
<script setup lang="ts">
import { onMounted, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import { ElMessage } from 'element-plus'
import createAxios from '/@/utils/axios'
import { auth } from '/@/utils/common'
defineOptions({
name: 'config/streakWinReward',
})
const { t } = useI18n()
type Row = {
streak: number
odds_factor: number
is_jackpot: boolean
}
const loading = ref(false)
const saving = ref(false)
const rows = ref<Row[]>([])
async function load() {
loading.value = true
try {
const res = await createAxios({
url: '/admin/config.StreakWinReward/index',
method: 'get',
})
if (res.code === 1 && res.data && Array.isArray(res.data.rows)) {
rows.value = res.data.rows.map((r: Row) => ({
streak: typeof r.streak === 'number' ? r.streak : 1,
odds_factor: typeof r.odds_factor === 'number' ? r.odds_factor : 1,
is_jackpot: !!r.is_jackpot,
}))
}
} finally {
loading.value = false
}
}
async function onSave() {
if (!auth('save')) {
return
}
for (let i = 0; i < rows.value.length; i++) {
const row = rows.value[i]
if (row.odds_factor < 1) {
ElMessage.warning(t('config.streakWinReward.err_factor', { no: i + 1 }))
return
}
}
saving.value = true
try {
await createAxios({
url: '/admin/config.StreakWinReward/save',
method: 'post',
data: {
rows: rows.value.map((r) => ({
streak: r.streak,
odds_factor: r.odds_factor,
is_jackpot: r.is_jackpot,
})),
},
showSuccessMessage: true,
})
await load()
} finally {
saving.value = false
}
}
onMounted(() => {
void load()
})
</script>
<style scoped lang="scss">
.streak-reward-page {
.toolbar {
margin: 12px 0;
display: flex;
gap: 12px;
}
}
.streak-reward-table {
:deep(.el-table__header th),
:deep(.el-table__body td) {
text-align: center;
}
}
.cell-center {
display: flex;
justify-content: center;
align-items: center;
width: 100%;
}
</style>