1.优化实时对局页面样式以及自动创建下一局和作废本局的记录
2.新增派彩达到game_config.jackpot_max_amount必须审核才能发放 3.新增游戏对局记录-查看游玩记录btn 3.备份MySQL数据库
This commit is contained in:
@@ -139,7 +139,7 @@ class Live extends Backend
|
||||
}
|
||||
$raw = $request->post('enabled');
|
||||
$enabled = $raw === true || $raw === '1' || $raw === 1;
|
||||
GameRecordService::setLiveRuntimeEnabled($enabled);
|
||||
GameRecordService::setAutoCreateEnabled($enabled);
|
||||
if ($enabled) {
|
||||
GameRecordService::bootstrapPeriodWhenRuntimeEnabled();
|
||||
}
|
||||
@@ -168,6 +168,15 @@ class Live extends Backend
|
||||
return $this->error(is_string($errMsg) ? $errMsg : __('Void failed'));
|
||||
}
|
||||
$okMsg = $res['msg'] ?? '';
|
||||
return $this->success(is_string($okMsg) ? $okMsg : '', GameLiveService::buildSnapshot(null));
|
||||
$snapshot = GameLiveService::buildSnapshot(null);
|
||||
// 作废本局后:必须关闭自动创建下一局开关(允许管理员后续手动重新开启)
|
||||
$snapshot['runtime_enabled'] = false;
|
||||
// 作废后一般不存在进行中的局,直接进入维护态(用于前端展示“维护中”倒计时)
|
||||
$snapshot['maintenance_ui'] = true;
|
||||
$refund = $res['refund'] ?? null;
|
||||
if (is_array($refund)) {
|
||||
$snapshot['void_refund'] = $refund;
|
||||
}
|
||||
return $this->success(is_string($okMsg) ? $okMsg : '', $snapshot);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
namespace app\admin\controller\game;
|
||||
|
||||
use app\common\controller\Backend;
|
||||
use app\common\service\GameBetSettleService;
|
||||
use support\think\Db;
|
||||
use support\Response;
|
||||
use Webman\Http\Request as WebmanRequest;
|
||||
|
||||
@@ -78,26 +80,191 @@ class PlayRecord extends Backend
|
||||
$where[] = ['user.admin_id', 'in', $this->scopedAdminIds()];
|
||||
}
|
||||
|
||||
$res = $this->model
|
||||
->withJoin($this->withJoinTable, $this->withJoinType)
|
||||
->with($this->withJoinTable)
|
||||
->visible([
|
||||
'user' => ['username', 'phone'],
|
||||
'channel' => ['name'],
|
||||
'gameRecord' => ['period_no', 'status'],
|
||||
// 避免 ThinkORM withJoin 对 game_record 的字段缓存导致 select 出已删除列(如 preset_number)
|
||||
// 这里改为手写 join + 明确 field 列表,保证数据库字段变更后不受 schema 缓存影响。
|
||||
$query = Db::name($table)->alias($mainShort !== '' ? $mainShort : 'play_record')
|
||||
->leftJoin('user user', 'user.id = ' . ($mainShort !== '' ? $mainShort : 'play_record') . '.user_id')
|
||||
->leftJoin('channel channel', 'channel.id = ' . ($mainShort !== '' ? $mainShort : 'play_record') . '.channel_id')
|
||||
->leftJoin('game_record game_record', 'game_record.id = ' . ($mainShort !== '' ? $mainShort : 'play_record') . '.period_id')
|
||||
->where($where);
|
||||
|
||||
$res = $query
|
||||
->field([
|
||||
($mainShort !== '' ? $mainShort : 'play_record') . '.*',
|
||||
'user.username as user__username',
|
||||
'user.phone as user__phone',
|
||||
'channel.name as channel__name',
|
||||
'game_record.period_no as gameRecord__period_no',
|
||||
'game_record.status as gameRecord__status',
|
||||
])
|
||||
->alias($alias)
|
||||
->where($where)
|
||||
->order($order)
|
||||
->paginate($limit);
|
||||
|
||||
$list = $res->items();
|
||||
$total = $res->total();
|
||||
|
||||
// 将 join 扁平字段还原为原页面所需结构:user/channel/gameRecord
|
||||
foreach ($list as $idx => $row) {
|
||||
if (!is_array($row)) {
|
||||
continue;
|
||||
}
|
||||
$row['user'] = [
|
||||
'username' => isset($row['user__username']) ? (string) $row['user__username'] : '',
|
||||
'phone' => isset($row['user__phone']) ? (string) $row['user__phone'] : '',
|
||||
];
|
||||
$row['channel'] = [
|
||||
'name' => isset($row['channel__name']) ? (string) $row['channel__name'] : '',
|
||||
];
|
||||
$row['gameRecord'] = [
|
||||
'period_no' => isset($row['gameRecord__period_no']) ? (string) $row['gameRecord__period_no'] : '',
|
||||
'status' => isset($row['gameRecord__status']) && is_numeric((string) $row['gameRecord__status'])
|
||||
? (int) $row['gameRecord__status']
|
||||
: null,
|
||||
];
|
||||
unset(
|
||||
$row['user__username'],
|
||||
$row['user__phone'],
|
||||
$row['channel__name'],
|
||||
$row['gameRecord__period_no'],
|
||||
$row['gameRecord__status'],
|
||||
);
|
||||
$list[$idx] = $row;
|
||||
}
|
||||
|
||||
$threshold = $this->jackpotMaxAmount();
|
||||
foreach ($list as $idx => $row) {
|
||||
if (!is_array($row)) {
|
||||
continue;
|
||||
}
|
||||
$status = isset($row['status']) && is_numeric($row['status']) ? (int) $row['status'] : 0;
|
||||
$win = bcadd(strval($row['win_amount'] ?? '0'), '0', 2);
|
||||
$needReview = bccomp($threshold, '0', 2) > 0 && bccomp($win, $threshold, 2) >= 0;
|
||||
$canApprove = $needReview && $status === GameBetSettleService::PLAY_STATUS_PENDING_REVIEW;
|
||||
$row['jackpot_need_review'] = $needReview ? 1 : 0;
|
||||
$row['can_jackpot_approve'] = $canApprove ? 1 : 0;
|
||||
$list[$idx] = $row;
|
||||
}
|
||||
|
||||
return $this->success('', [
|
||||
'list' => $res->items(),
|
||||
'total' => $res->total(),
|
||||
'list' => $list,
|
||||
'total' => $total,
|
||||
'remark' => get_route_remark(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 大奖审核通过并派彩(仅 win_amount >= game_config.jackpot_max_amount 且 status=待审核 才可操作)
|
||||
*/
|
||||
public function approveJackpot(WebmanRequest $request): Response
|
||||
{
|
||||
$response = $this->initializeBackend($request);
|
||||
if ($response !== null) {
|
||||
return $response;
|
||||
}
|
||||
if ($request->method() !== 'POST') {
|
||||
return $this->error(__('Parameter error'));
|
||||
}
|
||||
|
||||
$idRaw = $request->post('id');
|
||||
if ($idRaw === null || $idRaw === '' || !is_numeric(strval($idRaw))) {
|
||||
return $this->error(__('Parameter error'));
|
||||
}
|
||||
$id = (int) $idRaw;
|
||||
if ($id <= 0) {
|
||||
return $this->error(__('Parameter error'));
|
||||
}
|
||||
$remarkRaw = $request->post('remark');
|
||||
$remark = is_string($remarkRaw) ? trim($remarkRaw) : '';
|
||||
|
||||
// 权限范围校验:复用列表逻辑(非超管只能操作其下辖用户)
|
||||
if ($this->auth && !$this->auth->isSuperAdmin()) {
|
||||
$uidRaw = Db::name('game_play_record')->where('id', $id)->value('user_id');
|
||||
$uid = ($uidRaw === null || $uidRaw === '' || !is_numeric(strval($uidRaw))) ? 0 : (int) $uidRaw;
|
||||
if ($uid <= 0) {
|
||||
return $this->error(__('Record not found'));
|
||||
}
|
||||
$ownerAdminId = Db::name('user')->where('id', $uid)->value('admin_id');
|
||||
$aid = ($ownerAdminId === null || $ownerAdminId === '' || !is_numeric(strval($ownerAdminId))) ? 0 : (int) $ownerAdminId;
|
||||
if ($aid <= 0 || !in_array($aid, $this->scopedAdminIds(), true)) {
|
||||
return $this->error(__('You have no permission'));
|
||||
}
|
||||
}
|
||||
|
||||
$adminId = $this->auth ? (int) ($this->auth->id ?? 0) : 0;
|
||||
|
||||
Db::startTrans();
|
||||
try {
|
||||
$result = GameBetSettleService::approveJackpotPlayRecord($id, $adminId, $remark);
|
||||
if (($result['ok'] ?? false) !== true) {
|
||||
Db::rollback();
|
||||
$msg = is_string($result['msg'] ?? null) ? $result['msg'] : __('Parameter error');
|
||||
return $this->error($msg);
|
||||
}
|
||||
Db::commit();
|
||||
} catch (\Throwable $e) {
|
||||
Db::rollback();
|
||||
return $this->error($e->getMessage());
|
||||
}
|
||||
|
||||
return $this->success(__('Approved'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 大奖审核拒绝(remark 必填)
|
||||
*/
|
||||
public function rejectJackpot(WebmanRequest $request): Response
|
||||
{
|
||||
$response = $this->initializeBackend($request);
|
||||
if ($response !== null) {
|
||||
return $response;
|
||||
}
|
||||
if ($request->method() !== 'POST') {
|
||||
return $this->error(__('Parameter error'));
|
||||
}
|
||||
$idRaw = $request->post('id');
|
||||
if ($idRaw === null || $idRaw === '' || !is_numeric(strval($idRaw))) {
|
||||
return $this->error(__('Parameter error'));
|
||||
}
|
||||
$id = (int) $idRaw;
|
||||
if ($id <= 0) {
|
||||
return $this->error(__('Parameter error'));
|
||||
}
|
||||
$remarkRaw = $request->post('remark');
|
||||
$remark = is_string($remarkRaw) ? trim($remarkRaw) : '';
|
||||
if ($remark === '') {
|
||||
return $this->error(__('Please provide reject reason'));
|
||||
}
|
||||
|
||||
if ($this->auth && !$this->auth->isSuperAdmin()) {
|
||||
$uidRaw = Db::name('game_play_record')->where('id', $id)->value('user_id');
|
||||
$uid = ($uidRaw === null || $uidRaw === '' || !is_numeric(strval($uidRaw))) ? 0 : (int) $uidRaw;
|
||||
if ($uid <= 0) {
|
||||
return $this->error(__('Record not found'));
|
||||
}
|
||||
$ownerAdminId = Db::name('user')->where('id', $uid)->value('admin_id');
|
||||
$aid = ($ownerAdminId === null || $ownerAdminId === '' || !is_numeric(strval($ownerAdminId))) ? 0 : (int) $ownerAdminId;
|
||||
if ($aid <= 0 || !in_array($aid, $this->scopedAdminIds(), true)) {
|
||||
return $this->error(__('You have no permission'));
|
||||
}
|
||||
}
|
||||
|
||||
$adminId = $this->auth ? (int) ($this->auth->id ?? 0) : 0;
|
||||
Db::startTrans();
|
||||
try {
|
||||
$result = GameBetSettleService::rejectJackpotPlayRecord($id, $adminId, $remark);
|
||||
if (($result['ok'] ?? false) !== true) {
|
||||
Db::rollback();
|
||||
$msg = is_string($result['msg'] ?? null) ? $result['msg'] : __('Parameter error');
|
||||
return $this->error($msg);
|
||||
}
|
||||
Db::commit();
|
||||
} catch (\Throwable $e) {
|
||||
Db::rollback();
|
||||
return $this->error($e->getMessage());
|
||||
}
|
||||
return $this->success(__('Rejected'));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int[]
|
||||
*/
|
||||
@@ -116,5 +283,26 @@ class PlayRecord extends Backend
|
||||
$adminIds = array_values(array_unique(array_filter($adminIds, static fn($id) => $id > 0)));
|
||||
return $adminIds === [] ? [0] : $adminIds;
|
||||
}
|
||||
|
||||
private function jackpotMaxAmount(): string
|
||||
{
|
||||
$row = Db::name('game_config')->where('config_key', GameBetSettleService::CONFIG_KEY_JACKPOT_MAX_AMOUNT)->find();
|
||||
if (!is_array($row)) {
|
||||
return '0.00';
|
||||
}
|
||||
$raw = $row['config_value'] ?? null;
|
||||
if ($raw === null || $raw === '') {
|
||||
return '0.00';
|
||||
}
|
||||
$v = is_string($raw) ? trim($raw) : (is_numeric($raw) ? strval($raw) : '');
|
||||
if ($v === '' || !is_numeric($v)) {
|
||||
return '0.00';
|
||||
}
|
||||
$normalized = bcadd($v, '0', 2);
|
||||
if (bccomp($normalized, '0', 2) <= 0) {
|
||||
return '0.00';
|
||||
}
|
||||
return $normalized;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -32,6 +32,44 @@ class Record extends Backend
|
||||
return null;
|
||||
}
|
||||
|
||||
protected function _index(): Response
|
||||
{
|
||||
if ($this->request && $this->request->get('select')) {
|
||||
return $this->select($this->request);
|
||||
}
|
||||
|
||||
list($where, $alias, $limit, $order) = $this->queryBuilder();
|
||||
|
||||
$res = $this->model
|
||||
->field($this->indexField)
|
||||
->withJoin($this->withJoinTable, $this->withJoinType)
|
||||
->with($this->withJoinTable)
|
||||
->alias($alias)
|
||||
->where($where)
|
||||
->order($order)
|
||||
->paginate($limit);
|
||||
|
||||
$list = $res->items();
|
||||
foreach ($list as $idx => $row) {
|
||||
if (!is_array($row)) {
|
||||
continue;
|
||||
}
|
||||
$status = isset($row['status']) && is_numeric((string) $row['status']) ? (int) $row['status'] : 0;
|
||||
$reason = isset($row['void_reason']) && is_string($row['void_reason']) ? $row['void_reason'] : '';
|
||||
// 将“系统自愈作废”的对局在列表中标记为【异常】(展示态,不改库中 status=5 的事实)
|
||||
if ($status === 5 && $reason !== '' && str_starts_with($reason, 'system_recover:')) {
|
||||
$row['status'] = 6;
|
||||
$list[$idx] = $row;
|
||||
}
|
||||
}
|
||||
|
||||
return $this->success('', [
|
||||
'list' => $list,
|
||||
'total' => $res->total(),
|
||||
'remark' => get_route_remark(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function add(WebmanRequest $request): Response
|
||||
{
|
||||
$response = $this->initializeBackend($request);
|
||||
@@ -86,24 +124,95 @@ class Record extends Backend
|
||||
|
||||
$rows = Db::name('game_record')
|
||||
->where('status', 5)
|
||||
// 仅展示服务重启自愈导致的异常作废,排除管理员手动作废
|
||||
->whereLike('void_reason', 'system_recover:%')
|
||||
->field(['id', 'period_no', 'void_reason', 'update_time'])
|
||||
->order('id', 'desc')
|
||||
->limit($limit)
|
||||
->select()
|
||||
->toArray();
|
||||
|
||||
$periodIds = [];
|
||||
foreach ($rows as $row) {
|
||||
$pid = (int) ($row['id'] ?? 0);
|
||||
if ($pid > 0) {
|
||||
$periodIds[] = $pid;
|
||||
}
|
||||
}
|
||||
$refundAggByPeriod = [];
|
||||
$refundIdsByPeriod = [];
|
||||
if ($periodIds !== []) {
|
||||
$aggRows = Db::name('bet_order')
|
||||
->whereIn('period_id', $periodIds)
|
||||
->where('status', 3)
|
||||
->fieldRaw('period_id as pid, COUNT(*) as cnt, COUNT(DISTINCT user_id) as users, COALESCE(SUM(total_amount), 0) as amt')
|
||||
->group('period_id')
|
||||
->select()
|
||||
->toArray();
|
||||
foreach ($aggRows as $a) {
|
||||
$pid = (int) ($a['pid'] ?? 0);
|
||||
if ($pid <= 0) {
|
||||
continue;
|
||||
}
|
||||
$refundAggByPeriod[$pid] = [
|
||||
'orders' => (int) ($a['cnt'] ?? 0),
|
||||
'users' => (int) ($a['users'] ?? 0),
|
||||
'amount' => (string) ($a['amt'] ?? '0.00'),
|
||||
];
|
||||
}
|
||||
|
||||
$idRows = Db::name('bet_order')
|
||||
->whereIn('period_id', $periodIds)
|
||||
->where('status', 3)
|
||||
->field(['period_id', 'id'])
|
||||
->order('id', 'desc')
|
||||
->limit(2000)
|
||||
->select()
|
||||
->toArray();
|
||||
foreach ($idRows as $r) {
|
||||
$pid = (int) ($r['period_id'] ?? 0);
|
||||
$bid = (int) ($r['id'] ?? 0);
|
||||
if ($pid <= 0 || $bid <= 0) {
|
||||
continue;
|
||||
}
|
||||
if (!isset($refundIdsByPeriod[$pid])) {
|
||||
$refundIdsByPeriod[$pid] = [];
|
||||
}
|
||||
if (count($refundIdsByPeriod[$pid]) < 50) {
|
||||
$refundIdsByPeriod[$pid][] = $bid;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$list = [];
|
||||
foreach ($rows as $row) {
|
||||
$meta = $this->parseRecoverVoidReason(is_string($row['void_reason'] ?? null) ? $row['void_reason'] : '');
|
||||
$reason = is_string($row['void_reason'] ?? null) ? $row['void_reason'] : '';
|
||||
$isAutoRecover = $this->isSystemRecoverReason($reason);
|
||||
$pid = (int) ($row['id'] ?? 0);
|
||||
$agg = $refundAggByPeriod[$pid] ?? null;
|
||||
$users = (int) ($meta['users'] ?? 0);
|
||||
$orders = (int) ($meta['orders'] ?? 0);
|
||||
$amount = is_string($meta['amount'] ?? null) ? $meta['amount'] : '0.00';
|
||||
if (is_array($agg)) {
|
||||
if ($orders <= 0 && ($agg['orders'] ?? 0) > 0) {
|
||||
$orders = (int) ($agg['orders'] ?? 0);
|
||||
}
|
||||
if ($users <= 0 && ($agg['users'] ?? 0) > 0) {
|
||||
$users = (int) ($agg['users'] ?? 0);
|
||||
}
|
||||
if (bccomp($amount, '0', 2) <= 0 && is_string($agg['amount'] ?? null)) {
|
||||
$amount = (string) $agg['amount'];
|
||||
}
|
||||
}
|
||||
$list[] = [
|
||||
'id' => (int) ($row['id'] ?? 0),
|
||||
'period_no' => (string) ($row['period_no'] ?? ''),
|
||||
'abnormal_from_status' => $meta['from_status'],
|
||||
'refunded_user_count' => $meta['users'],
|
||||
'refunded_order_count' => $meta['orders'],
|
||||
'refunded_total_amount' => $meta['amount'],
|
||||
'refunded_user_count' => $users,
|
||||
'refunded_order_count' => $orders,
|
||||
'refunded_total_amount' => $amount,
|
||||
'refunded_order_ids' => $refundIdsByPeriod[$pid] ?? [],
|
||||
'recovered_at' => (int) ($row['update_time'] ?? 0),
|
||||
'void_reason' => $reason,
|
||||
'is_auto_recover' => $isAutoRecover ? 1 : 0,
|
||||
@@ -116,6 +225,77 @@ class Record extends Backend
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 某期对局的游玩(压注)记录列表(用于对局记录页弹窗查看)。
|
||||
*/
|
||||
public function playRecordList(WebmanRequest $request): Response
|
||||
{
|
||||
$response = $this->initializeBackend($request);
|
||||
if ($response !== null) {
|
||||
return $response;
|
||||
}
|
||||
|
||||
$periodIdRaw = $request->get('period_id');
|
||||
if (!is_numeric((string) $periodIdRaw)) {
|
||||
return $this->error(__('Parameter error'));
|
||||
}
|
||||
$periodId = (int) $periodIdRaw;
|
||||
if ($periodId <= 0) {
|
||||
return $this->error(__('Parameter error'));
|
||||
}
|
||||
|
||||
$pageRaw = $request->get('page', 1);
|
||||
$page = is_numeric((string) $pageRaw) ? (int) $pageRaw : 1;
|
||||
if ($page < 1) {
|
||||
$page = 1;
|
||||
}
|
||||
|
||||
$limitRaw = $request->get('limit', 30);
|
||||
$limit = is_numeric((string) $limitRaw) ? (int) $limitRaw : 30;
|
||||
if ($limit < 1) {
|
||||
$limit = 1;
|
||||
}
|
||||
if ($limit > 200) {
|
||||
$limit = 200;
|
||||
}
|
||||
|
||||
$query = Db::name('game_play_record')
|
||||
->alias('pr')
|
||||
->leftJoin('user u', 'u.id = pr.user_id')
|
||||
->leftJoin('channel c', 'c.id = pr.channel_id')
|
||||
->where('pr.period_id', $periodId);
|
||||
|
||||
$total = (int) $query->count('pr.id');
|
||||
$list = $query
|
||||
->field([
|
||||
'pr.id',
|
||||
'pr.period_id',
|
||||
'pr.user_id',
|
||||
'pr.channel_id',
|
||||
'pr.pick_numbers',
|
||||
'pr.total_amount',
|
||||
'pr.streak_at_bet',
|
||||
'pr.is_auto',
|
||||
'pr.win_amount',
|
||||
'pr.jackpot_extra_amount',
|
||||
'pr.status',
|
||||
'pr.idempotency_key',
|
||||
'pr.create_time',
|
||||
'pr.update_time',
|
||||
'u.username as user_username',
|
||||
'c.name as channel_name',
|
||||
])
|
||||
->order('pr.id', 'desc')
|
||||
->page($page, $limit)
|
||||
->select()
|
||||
->toArray();
|
||||
|
||||
return $this->success('', [
|
||||
'list' => $list,
|
||||
'total' => $total,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{from_status:int,users:int,orders:int,amount:string}
|
||||
*/
|
||||
|
||||
@@ -49,7 +49,7 @@ class Game extends MobileBase
|
||||
$user = $this->auth->getUser();
|
||||
return $this->mobileSuccess([
|
||||
'server_time' => $now,
|
||||
'runtime_enabled' => GameRecordService::isLiveRuntimeEnabled(),
|
||||
'runtime_enabled' => GameRecordService::getConfigBool(GameRecordService::KEY_AUTO_CREATE),
|
||||
'period' => [
|
||||
'period_no' => (string) ($periodRow['period_no'] ?? ''),
|
||||
'status' => $this->mapPeriodStatus($periodRow['status'] ?? null),
|
||||
@@ -107,7 +107,7 @@ class Game extends MobileBase
|
||||
$now = time();
|
||||
$startAt = $this->intValue($periodRow['period_start_at'] ?? 0);
|
||||
return $this->mobileSuccess([
|
||||
'runtime_enabled' => GameRecordService::isLiveRuntimeEnabled(),
|
||||
'runtime_enabled' => GameRecordService::getConfigBool(GameRecordService::KEY_AUTO_CREATE),
|
||||
'period_id' => $periodRow['id'],
|
||||
'period_no' => $periodRow['period_no'],
|
||||
'status' => $this->mapPeriodStatus($periodRow['status'] ?? null),
|
||||
@@ -169,7 +169,7 @@ class Game extends MobileBase
|
||||
$numberCount = (string) count($numbers);
|
||||
$totalAmount = bcmul($singleAmount, $numberCount, 2);
|
||||
|
||||
if (!GameRecordService::isLiveRuntimeEnabled()) {
|
||||
if (!GameRecordService::getConfigBool(GameRecordService::KEY_AUTO_CREATE)) {
|
||||
return $this->mobileError(3001, 'Game is paused');
|
||||
}
|
||||
$period = GameRecord::where('period_no', $periodNo)->find();
|
||||
|
||||
@@ -16,7 +16,6 @@ class GamePeriod extends Model
|
||||
'period_start_at' => 'integer',
|
||||
'status' => 'integer',
|
||||
'draw_mode' => 'integer',
|
||||
'preset_number' => 'integer',
|
||||
'result_number' => 'integer',
|
||||
];
|
||||
|
||||
|
||||
@@ -17,7 +17,6 @@ class GameRecord extends Model
|
||||
'period_start_at' => 'integer',
|
||||
'status' => 'integer',
|
||||
'draw_mode' => 'integer',
|
||||
'preset_number' => 'integer',
|
||||
'result_number' => 'integer',
|
||||
'ai_locked_number' => 'integer',
|
||||
'pending_draw_number' => 'integer',
|
||||
|
||||
@@ -14,6 +14,14 @@ use Throwable;
|
||||
*/
|
||||
final class GameBetSettleService
|
||||
{
|
||||
public const PLAY_STATUS_PENDING_DRAW = 1;
|
||||
public const PLAY_STATUS_SETTLED = 2;
|
||||
public const PLAY_STATUS_REFUNDED = 3;
|
||||
public const PLAY_STATUS_RETURNED = 4;
|
||||
public const PLAY_STATUS_PENDING_REVIEW = 5;
|
||||
|
||||
public const CONFIG_KEY_JACKPOT_MAX_AMOUNT = 'jackpot_max_amount';
|
||||
|
||||
/**
|
||||
* 对指定期次按开奖号码结算所有「待开奖」注单;同一注单幂等(仅 status=1 会更新)。
|
||||
*
|
||||
@@ -28,9 +36,10 @@ final class GameBetSettleService
|
||||
}
|
||||
|
||||
$now = time();
|
||||
$jackpotMaxAmount = self::jackpotMaxAmount();
|
||||
$bets = Db::name('bet_order')
|
||||
->where('period_id', $recordId)
|
||||
->where('status', 1)
|
||||
->where('status', self::PLAY_STATUS_PENDING_DRAW)
|
||||
->order('id', 'asc')
|
||||
->select()
|
||||
->toArray();
|
||||
@@ -60,14 +69,16 @@ final class GameBetSettleService
|
||||
|
||||
$win = self::computeWinAmount($bet, $resultNumber);
|
||||
$jackpot = '0.00';
|
||||
$needReview = self::shouldRequireJackpotReview($win, $jackpotMaxAmount);
|
||||
$nextStatus = $needReview ? self::PLAY_STATUS_PENDING_REVIEW : self::PLAY_STATUS_SETTLED;
|
||||
|
||||
$affected = Db::name('bet_order')
|
||||
->where('id', $betId)
|
||||
->where('status', 1)
|
||||
->where('status', self::PLAY_STATUS_PENDING_DRAW)
|
||||
->update([
|
||||
'win_amount' => $win,
|
||||
'jackpot_extra_amount' => $jackpot,
|
||||
'status' => 2,
|
||||
'status' => $nextStatus,
|
||||
'update_time' => $now,
|
||||
]);
|
||||
|
||||
@@ -91,8 +102,8 @@ final class GameBetSettleService
|
||||
}
|
||||
|
||||
$balanceAfter = (string) (Db::name('user')->where('id', $userId)->value('coin') ?? '0');
|
||||
if (bccomp($win, '0', 2) > 0) {
|
||||
$paid = self::creditUserPayout($bet, $betId, $win, $now);
|
||||
if (!$needReview && bccomp($win, '0', 2) > 0) {
|
||||
$paid = self::creditUserPayout($bet, $betId, $win, $now, null, '压注派彩');
|
||||
if ($paid !== null) {
|
||||
$balanceAfter = $paid;
|
||||
}
|
||||
@@ -154,6 +165,109 @@ final class GameBetSettleService
|
||||
return ['jackpot_hits' => $jackpotHits];
|
||||
}
|
||||
|
||||
/**
|
||||
* 大奖审核通过后派彩(幂等):仅当 play_record.status=待审核 且 win_amount>=阈值时执行。
|
||||
*
|
||||
* @return array{ok: bool, msg: string, balance_after?: string}
|
||||
*/
|
||||
public static function approveJackpotPlayRecord(int $playRecordId, int $operatorAdminId, string $remark = ''): array
|
||||
{
|
||||
if ($playRecordId <= 0) {
|
||||
return ['ok' => false, 'msg' => __('Parameter error')];
|
||||
}
|
||||
// 兼容:bet_order 可能是 VIEW,且 * 列表会固化;审核字段始终以 game_play_record 为准
|
||||
$row = Db::name('game_play_record')->where('id', $playRecordId)->find();
|
||||
if (!is_array($row)) {
|
||||
$row = Db::name('bet_order')->where('id', $playRecordId)->find();
|
||||
}
|
||||
if (!is_array($row)) {
|
||||
return ['ok' => false, 'msg' => __('Record not found')];
|
||||
}
|
||||
$status = isset($row['status']) && is_numeric($row['status']) ? (int) $row['status'] : 0;
|
||||
if ($status !== self::PLAY_STATUS_PENDING_REVIEW) {
|
||||
return ['ok' => false, 'msg' => __('This record does not require review')];
|
||||
}
|
||||
$winAmount = bcadd((string) ($row['win_amount'] ?? '0'), '0', 2);
|
||||
$threshold = self::jackpotMaxAmount();
|
||||
if (!self::shouldRequireJackpotReview($winAmount, $threshold)) {
|
||||
return ['ok' => false, 'msg' => __('This record does not meet jackpot review threshold')];
|
||||
}
|
||||
$userId = isset($row['user_id']) && is_numeric($row['user_id']) ? (int) $row['user_id'] : 0;
|
||||
if ($userId <= 0) {
|
||||
return ['ok' => false, 'msg' => __('Order is missing user info')];
|
||||
}
|
||||
$now = time();
|
||||
$balanceAfter = null;
|
||||
if (bccomp($winAmount, '0', 2) > 0) {
|
||||
$balanceAfter = self::creditUserPayout($row, $playRecordId, $winAmount, $now, $operatorAdminId > 0 ? $operatorAdminId : null, '大奖审核通过派彩');
|
||||
}
|
||||
$reviewRemark = trim($remark);
|
||||
if ($reviewRemark === '') {
|
||||
$reviewRemark = 'approved';
|
||||
}
|
||||
$update = [
|
||||
'status' => self::PLAY_STATUS_SETTLED,
|
||||
'review_admin_id' => $operatorAdminId > 0 ? $operatorAdminId : null,
|
||||
'review_time' => $now,
|
||||
'review_remark' => substr($reviewRemark, 0, 255),
|
||||
'update_time' => $now,
|
||||
];
|
||||
// 优先写主表
|
||||
Db::name('game_play_record')->where('id', $playRecordId)->where('status', self::PLAY_STATUS_PENDING_REVIEW)->update($update);
|
||||
// 兼容写 view 场景(若存在且可写)
|
||||
try {
|
||||
Db::name('bet_order')->where('id', $playRecordId)->where('status', self::PLAY_STATUS_PENDING_REVIEW)->update($update);
|
||||
} catch (\Throwable) {
|
||||
}
|
||||
|
||||
$out = ['ok' => true, 'msg' => __('Approved')];
|
||||
if (is_string($balanceAfter)) {
|
||||
$out['balance_after'] = $balanceAfter;
|
||||
}
|
||||
return $out;
|
||||
}
|
||||
|
||||
/**
|
||||
* 大奖审核拒绝:仅当 status=待审核 才可操作;拒绝后不派彩,标记为已退回(status=4)。
|
||||
*
|
||||
* @return array{ok: bool, msg: string}
|
||||
*/
|
||||
public static function rejectJackpotPlayRecord(int $playRecordId, int $operatorAdminId, string $remark): array
|
||||
{
|
||||
if ($playRecordId <= 0) {
|
||||
return ['ok' => false, 'msg' => __('Parameter error')];
|
||||
}
|
||||
$reason = trim($remark);
|
||||
if ($reason === '') {
|
||||
return ['ok' => false, 'msg' => __('Please provide reject reason')];
|
||||
}
|
||||
$row = Db::name('game_play_record')->where('id', $playRecordId)->find();
|
||||
if (!is_array($row)) {
|
||||
$row = Db::name('bet_order')->where('id', $playRecordId)->find();
|
||||
}
|
||||
if (!is_array($row)) {
|
||||
return ['ok' => false, 'msg' => __('Record not found')];
|
||||
}
|
||||
$status = isset($row['status']) && is_numeric($row['status']) ? (int) $row['status'] : 0;
|
||||
if ($status !== self::PLAY_STATUS_PENDING_REVIEW) {
|
||||
return ['ok' => false, 'msg' => __('This record does not require review')];
|
||||
}
|
||||
$now = time();
|
||||
$update = [
|
||||
'status' => self::PLAY_STATUS_RETURNED,
|
||||
'review_admin_id' => $operatorAdminId > 0 ? $operatorAdminId : null,
|
||||
'review_time' => $now,
|
||||
'review_remark' => substr($reason, 0, 255),
|
||||
'update_time' => $now,
|
||||
];
|
||||
Db::name('game_play_record')->where('id', $playRecordId)->where('status', self::PLAY_STATUS_PENDING_REVIEW)->update($update);
|
||||
try {
|
||||
Db::name('bet_order')->where('id', $playRecordId)->where('status', self::PLAY_STATUS_PENDING_REVIEW)->update($update);
|
||||
} catch (\Throwable) {
|
||||
}
|
||||
return ['ok' => true, 'msg' => __('Rejected')];
|
||||
}
|
||||
|
||||
/**
|
||||
* 补偿:库中已结束局次但注单仍为待开奖的,可重复调用(幂等)。
|
||||
*/
|
||||
@@ -176,7 +290,7 @@ final class GameBetSettleService
|
||||
}
|
||||
$pending = Db::name('bet_order')
|
||||
->where('period_id', $rid)
|
||||
->where('status', 1)
|
||||
->where('status', self::PLAY_STATUS_PENDING_DRAW)
|
||||
->count();
|
||||
if ($pending === 0) {
|
||||
continue;
|
||||
@@ -251,7 +365,7 @@ final class GameBetSettleService
|
||||
/**
|
||||
* @return string|null 派彩后余额;已幂等入账过时返回当前余额;失败或未执行派彩返回 null
|
||||
*/
|
||||
private static function creditUserPayout(array $bet, int $betId, string $winAmount, int $now): ?string
|
||||
private static function creditUserPayout(array $bet, int $betId, string $winAmount, int $now, ?int $operatorAdminId, string $remark): ?string
|
||||
{
|
||||
$userId = (int) ($bet['user_id'] ?? 0);
|
||||
if ($userId <= 0) {
|
||||
@@ -284,8 +398,8 @@ final class GameBetSettleService
|
||||
'ref_type' => 'bet_order',
|
||||
'ref_id' => $betId,
|
||||
'idempotency_key' => $idem,
|
||||
'operator_admin_id' => null,
|
||||
'remark' => '压注派彩',
|
||||
'operator_admin_id' => $operatorAdminId,
|
||||
'remark' => $remark !== '' ? $remark : '压注派彩',
|
||||
'create_time' => $now,
|
||||
]);
|
||||
|
||||
@@ -304,4 +418,35 @@ final class GameBetSettleService
|
||||
|
||||
return $after;
|
||||
}
|
||||
|
||||
private static function jackpotMaxAmount(): string
|
||||
{
|
||||
// 结算属于高频长驻进程逻辑:为避免 GameHotDataRedis::$gcLocal 进程内静态缓存导致阈值更新不生效,
|
||||
// 这里直接读库拿最新值(本方法在 settleBetsForDraw 中仅调用一次)。
|
||||
$row = Db::name('game_config')->where('config_key', self::CONFIG_KEY_JACKPOT_MAX_AMOUNT)->find();
|
||||
if (!is_array($row)) {
|
||||
return '0.00';
|
||||
}
|
||||
$raw = $row['config_value'] ?? null;
|
||||
if ($raw === null || $raw === '') {
|
||||
return '0.00';
|
||||
}
|
||||
$v = is_string($raw) ? trim($raw) : (is_numeric($raw) ? strval($raw) : '');
|
||||
if ($v === '' || !is_numeric($v)) {
|
||||
return '0.00';
|
||||
}
|
||||
$normalized = bcadd($v, '0', 2);
|
||||
if (bccomp($normalized, '0', 2) <= 0) {
|
||||
return '0.00';
|
||||
}
|
||||
return $normalized;
|
||||
}
|
||||
|
||||
private static function shouldRequireJackpotReview(string $winAmount, string $threshold): bool
|
||||
{
|
||||
if (bccomp($threshold, '0', 2) <= 0) {
|
||||
return false;
|
||||
}
|
||||
return bccomp($winAmount, $threshold, 2) >= 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,30 +47,25 @@ final class GameHotDataRedis
|
||||
if ($configKey === '') {
|
||||
return null;
|
||||
}
|
||||
if (array_key_exists($configKey, self::$gcLocal)) {
|
||||
$cachedLocal = self::$gcLocal[$configKey];
|
||||
return is_array($cachedLocal) ? $cachedLocal : null;
|
||||
}
|
||||
// game_config 为全局配置,多进程/多 worker 间必须强一致;
|
||||
// 因此不使用进程内本地缓存(gcLocal),避免某个进程读到旧值导致前端回弹/行为冲突。
|
||||
if (self::enabled()) {
|
||||
$cached = self::redisGet(self::KEY_GC . $configKey);
|
||||
if ($cached !== null && $cached !== '') {
|
||||
$decoded = json_decode($cached, true);
|
||||
if (is_array($decoded)) {
|
||||
self::$gcLocal[$configKey] = $decoded;
|
||||
return $decoded;
|
||||
}
|
||||
}
|
||||
}
|
||||
$row = Db::name('game_config')->where('config_key', $configKey)->find();
|
||||
if (!$row) {
|
||||
self::$gcLocal[$configKey] = null;
|
||||
return null;
|
||||
}
|
||||
if (self::enabled()) {
|
||||
$ttl = self::intConfig('ttl_game_config', 86400);
|
||||
self::redisSetEx(self::KEY_GC . $configKey, $ttl, json_encode($row, JSON_UNESCAPED_UNICODE));
|
||||
}
|
||||
self::$gcLocal[$configKey] = $row;
|
||||
return $row;
|
||||
}
|
||||
|
||||
@@ -88,17 +83,24 @@ final class GameHotDataRedis
|
||||
*/
|
||||
public static function gameConfigReplaceFromDb(string $configKey): void
|
||||
{
|
||||
if ($configKey === '' || !self::enabled()) {
|
||||
if ($configKey === '') {
|
||||
return;
|
||||
}
|
||||
// 无论是否启用 Redis 热点缓存,都要刷新进程内缓存,避免同一 worker 读到旧值
|
||||
$row = Db::name('game_config')->where('config_key', $configKey)->find();
|
||||
if (!$row) {
|
||||
self::gameConfigForget($configKey);
|
||||
self::$gcLocal[$configKey] = null;
|
||||
if (self::enabled()) {
|
||||
self::redisDel(self::KEY_GC . $configKey);
|
||||
}
|
||||
return;
|
||||
}
|
||||
$ttl = self::intConfig('ttl_game_config', 86400);
|
||||
self::redisSetEx(self::KEY_GC . $configKey, $ttl, json_encode($row, JSON_UNESCAPED_UNICODE));
|
||||
|
||||
self::$gcLocal[$configKey] = $row;
|
||||
if (self::enabled()) {
|
||||
$ttl = self::intConfig('ttl_game_config', 86400);
|
||||
self::redisSetEx(self::KEY_GC . $configKey, $ttl, json_encode($row, JSON_UNESCAPED_UNICODE));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -202,7 +202,9 @@ final class GameLiveService
|
||||
GameHotDataCoordinator::afterUserCommitted($uid);
|
||||
}
|
||||
}
|
||||
GameRecordService::bootstrapPeriodWhenRuntimeEnabled();
|
||||
// 异常对局作废后:自动暂停游戏,不自动创建新一期;需管理员手动开启「游戏运行」才会重新开局
|
||||
GameRecordService::setAutoCreateEnabled(false);
|
||||
GameHotDataCoordinator::afterGameConfigKeyCommitted(GameRecordService::KEY_AUTO_CREATE);
|
||||
self::publishSnapshot(null);
|
||||
Log::info('game live startup marked abnormal and refunded', [
|
||||
'record_id' => $recordId,
|
||||
@@ -733,6 +735,7 @@ final class GameLiveService
|
||||
$refundedUserIds = [];
|
||||
try {
|
||||
$now = time();
|
||||
$refund = ['user_ids' => [], 'order_count' => 0, 'total_amount' => '0.00', 'order_ids' => []];
|
||||
Db::startTrans();
|
||||
try {
|
||||
$refund = self::refundPendingBetsSummaryForPeriodLocked($rid, $now);
|
||||
@@ -750,9 +753,9 @@ final class GameLiveService
|
||||
Db::rollback();
|
||||
return ['ok' => false, 'msg' => __('Void failed') . ': ' . $e->getMessage()];
|
||||
}
|
||||
GameRecordService::setLiveRuntimeEnabled(false);
|
||||
GameRecordService::setAutoCreateEnabled(false);
|
||||
GameHotDataCoordinator::afterGameRecordCommitted($rid);
|
||||
GameHotDataCoordinator::afterGameConfigKeyCommitted(GameRecordService::KEY_LIVE_RUNTIME);
|
||||
GameHotDataCoordinator::afterGameConfigKeyCommitted(GameRecordService::KEY_AUTO_CREATE);
|
||||
foreach ($refundedUserIds as $uid) {
|
||||
if ($uid > 0) {
|
||||
GameHotDataCoordinator::afterUserCommitted($uid);
|
||||
@@ -764,6 +767,7 @@ final class GameLiveService
|
||||
'ok' => true,
|
||||
'msg' => __('Period voided'),
|
||||
'record' => self::reloadRecord($rid),
|
||||
'refund' => $refund,
|
||||
];
|
||||
} finally {
|
||||
GameHotDataLock::release(GameHotDataLock::TYPE_GAME_RECORD, (string) $rid, $lock['token'], $lock['redis_lock']);
|
||||
@@ -780,13 +784,14 @@ final class GameLiveService
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{user_ids:list<int>,order_count:int,total_amount:string}
|
||||
* @return array{user_ids:list<int>,order_count:int,total_amount:string,order_ids:list<int>}
|
||||
*/
|
||||
private static function refundPendingBetsSummaryForPeriodLocked(int $periodId, int $now): array
|
||||
{
|
||||
$userIdSet = [];
|
||||
$orderCount = 0;
|
||||
$totalAmount = '0.00';
|
||||
$orderIds = [];
|
||||
$bets = Db::name('bet_order')
|
||||
->where('period_id', $periodId)
|
||||
->where('status', 1)
|
||||
@@ -806,6 +811,8 @@ final class GameLiveService
|
||||
'status' => 3,
|
||||
'update_time' => $now,
|
||||
]);
|
||||
$orderCount++;
|
||||
$orderIds[] = $betId;
|
||||
continue;
|
||||
}
|
||||
$before = (string) (Db::name('user')->where('id', $userId)->value('coin') ?? '0');
|
||||
@@ -832,7 +839,7 @@ final class GameLiveService
|
||||
UserWalletRecord::create([
|
||||
'user_id' => $userId,
|
||||
'channel_id' => $channelId,
|
||||
'biz_type' => 'bet_void',
|
||||
'biz_type' => 'void_refund',
|
||||
'direction' => 1,
|
||||
'amount' => $total,
|
||||
'balance_before' => $before,
|
||||
@@ -844,6 +851,7 @@ final class GameLiveService
|
||||
$userIdSet[$userId] = true;
|
||||
$orderCount++;
|
||||
$totalAmount = bcadd($totalAmount, $total, 2);
|
||||
$orderIds[] = $betId;
|
||||
}
|
||||
|
||||
$out = [];
|
||||
@@ -855,6 +863,7 @@ final class GameLiveService
|
||||
'user_ids' => $out,
|
||||
'order_count' => $orderCount,
|
||||
'total_amount' => $totalAmount,
|
||||
'order_ids' => $orderIds,
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -13,9 +13,6 @@ final class GameRecordService
|
||||
|
||||
public const KEY_MANUAL_CREATE = 'period_manual_create_enabled';
|
||||
|
||||
/** 后台「游戏实时对局」运行开关:0=暂停自动开奖与派彩后自动创建下一期;1=运行 */
|
||||
public const KEY_LIVE_RUNTIME = 'game_live_runtime_enabled';
|
||||
|
||||
private const ACTIVE_STATUSES = [0, 1, 2, 3];
|
||||
|
||||
public static function getConfigBool(string $key): bool
|
||||
@@ -53,9 +50,6 @@ final class GameRecordService
|
||||
|
||||
public static function tickAutoCreate(): void
|
||||
{
|
||||
if (!self::isLiveRuntimeEnabled()) {
|
||||
return;
|
||||
}
|
||||
if (!self::getConfigBool(self::KEY_AUTO_CREATE)) {
|
||||
return;
|
||||
}
|
||||
@@ -86,7 +80,8 @@ final class GameRecordService
|
||||
|
||||
public static function createNextRecordAfterDraw(): ?string
|
||||
{
|
||||
if (!self::isLiveRuntimeEnabled()) {
|
||||
// 派彩结束后是否自动开新局:由 period_auto_create_enabled 控制
|
||||
if (!self::getConfigBool(self::KEY_AUTO_CREATE)) {
|
||||
return null;
|
||||
}
|
||||
if (self::hasActiveRecord()) {
|
||||
@@ -96,29 +91,16 @@ final class GameRecordService
|
||||
}
|
||||
|
||||
/**
|
||||
* 未配置键时视为开启(兼容旧库未跑迁移)。
|
||||
* 实时对局页「自动创建下一局」开关(兼容旧命名 runtime)。
|
||||
*/
|
||||
public static function isLiveRuntimeEnabled(): bool
|
||||
{
|
||||
$row = GameHotDataRedis::gameConfigRow(self::KEY_LIVE_RUNTIME);
|
||||
if ($row === null) {
|
||||
return true;
|
||||
}
|
||||
$v = $row['config_value'] ?? '';
|
||||
return $v === '1' || $v === 1;
|
||||
return self::getConfigBool(self::KEY_AUTO_CREATE);
|
||||
}
|
||||
|
||||
public static function setLiveRuntimeEnabled(bool $enabled): void
|
||||
{
|
||||
$now = time();
|
||||
$v = $enabled ? '1' : '0';
|
||||
self::upsertConfig(
|
||||
self::KEY_LIVE_RUNTIME,
|
||||
$v,
|
||||
'int',
|
||||
'后台「游戏实时对局」运行开关:0=维护(禁止下注、结束后不自动开新期,当局仍自动开奖并结算);1=运行',
|
||||
$now
|
||||
);
|
||||
self::setAutoCreateEnabled($enabled);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -126,6 +108,9 @@ final class GameRecordService
|
||||
*/
|
||||
public static function bootstrapPeriodWhenRuntimeEnabled(): void
|
||||
{
|
||||
if (!self::getConfigBool(self::KEY_AUTO_CREATE)) {
|
||||
return;
|
||||
}
|
||||
if (self::hasActiveRecord()) {
|
||||
return;
|
||||
}
|
||||
@@ -135,6 +120,13 @@ final class GameRecordService
|
||||
}
|
||||
}
|
||||
|
||||
public static function setAutoCreateEnabled(bool $enabled): void
|
||||
{
|
||||
$now = time();
|
||||
$v = $enabled ? '1' : '0';
|
||||
self::upsertConfig(self::KEY_AUTO_CREATE, $v, 'int', '是否允许自动创建下一局(全局仅一局)', $now);
|
||||
}
|
||||
|
||||
private static function createNextRecordRow(): string
|
||||
{
|
||||
$periodNo = self::generatePeriodNo();
|
||||
|
||||
@@ -15,12 +15,11 @@ class GamePeriod extends Validate
|
||||
'period_start_at' => 'integer',
|
||||
'status' => 'require|in:0,1,2,3,4,5',
|
||||
'draw_mode' => 'in:0,1',
|
||||
'preset_number' => 'between:1,36',
|
||||
'result_number' => 'between:1,36',
|
||||
];
|
||||
|
||||
protected $scene = [
|
||||
'add' => ['period_no', 'period_start_at', 'status', 'draw_mode', 'preset_number', 'result_number'],
|
||||
'edit' => ['period_start_at', 'status', 'draw_mode', 'preset_number', 'result_number'],
|
||||
'add' => ['period_no', 'period_start_at', 'status', 'draw_mode', 'result_number'],
|
||||
'edit' => ['period_start_at', 'status', 'draw_mode', 'result_number'],
|
||||
];
|
||||
}
|
||||
|
||||
@@ -15,12 +15,11 @@ class GameRecord extends Validate
|
||||
'period_start_at' => 'integer',
|
||||
'status' => 'require|in:0,1,2,3,4,5',
|
||||
'draw_mode' => 'in:0,1',
|
||||
'preset_number' => 'between:1,36',
|
||||
'result_number' => 'between:1,36',
|
||||
];
|
||||
|
||||
protected $scene = [
|
||||
'add' => ['period_no', 'period_start_at', 'status', 'draw_mode', 'preset_number', 'result_number'],
|
||||
'edit' => ['period_start_at', 'status', 'draw_mode', 'preset_number', 'result_number'],
|
||||
'add' => ['period_no', 'period_start_at', 'status', 'draw_mode', 'result_number'],
|
||||
'edit' => ['period_start_at', 'status', 'draw_mode', 'result_number'],
|
||||
];
|
||||
}
|
||||
|
||||
@@ -35,12 +35,12 @@ export default {
|
||||
pick_numbers: 'Pick numbers',
|
||||
total_amount: 'Total bet amount',
|
||||
streak_at_bet: 'Streak at bet',
|
||||
runtime_switch: 'Game runtime',
|
||||
runtime_switch: 'Auto-create next round',
|
||||
countdown_maintenance: 'Maintenance',
|
||||
runtime_draining_banner:
|
||||
'Game stopped: the current round will run through draw, settlement and payout. Full maintenance UI appears after payout completes.',
|
||||
runtime_maintenance_banner: 'Maintenance: player betting is disabled. Turn runtime on to resume; a new round is created when idle.',
|
||||
runtime_off_tip: 'When turning runtime on with no active round, a new period is created immediately.',
|
||||
runtime_maintenance_banner: 'Maintenance: player betting is disabled. Turn on auto-create to resume; a new round is created when idle.',
|
||||
runtime_off_tip: 'When enabling auto-create with no active round, a new period is created immediately.',
|
||||
void_btn: 'Void round',
|
||||
void_dialog_title: 'Void current round',
|
||||
void_reason_label: 'Reason',
|
||||
|
||||
@@ -17,6 +17,18 @@ export default {
|
||||
'status 1': 'Pending draw',
|
||||
'status 2': 'Settled',
|
||||
'status 3': 'Refunded',
|
||||
'status 4': 'Returned',
|
||||
'status 5': 'Pending review',
|
||||
review_title: 'Win review',
|
||||
review_open: 'Review',
|
||||
review_approve: 'Approve',
|
||||
review_reject: 'Reject',
|
||||
review_remark: 'Reject remark',
|
||||
review_remark_placeholder: 'Please enter reject reason',
|
||||
review_remark_required: 'Remark is required when rejecting',
|
||||
review_approve_confirm: 'Confirm approve and pay out?',
|
||||
review_approve_success: 'Approved and paid out',
|
||||
review_reject_success: 'Rejected',
|
||||
idempotency_key: 'Idempotency key',
|
||||
create_time: 'Created',
|
||||
update_time: 'Updated',
|
||||
|
||||
@@ -10,10 +10,10 @@ export default {
|
||||
'status 3': 'Paying',
|
||||
'status 4': 'Ended',
|
||||
'status 5': 'Void',
|
||||
'status 6': 'Abnormal',
|
||||
draw_mode: 'Draw mode',
|
||||
'draw_mode 0': 'Auto AI',
|
||||
'draw_mode 1': 'Manual preset',
|
||||
preset_number: 'Preset number',
|
||||
result_number: 'Result number',
|
||||
platform_profit_amount: 'Round P/L (platform)',
|
||||
winner_user_count: 'Winners',
|
||||
@@ -26,13 +26,23 @@ export default {
|
||||
manual_create_label: 'Allow manual create next round',
|
||||
manual_create_tip: 'When enabled, button below can create next round manually',
|
||||
btn_create_next: 'Create next round (manual)',
|
||||
view_abnormal_rounds: 'View abnormal rounds',
|
||||
abnormal_dialog_title: 'Abnormal round recovery logs',
|
||||
abnormal_dialog_tip: 'Shows rounds auto-recovered after service restart (auto-void + refund).',
|
||||
abnormal_from_status: 'Status before recovery',
|
||||
refunded_user_count: 'Refunded users',
|
||||
refunded_order_count: 'Refunded orders',
|
||||
refunded_total_amount: 'Total refunded amount',
|
||||
recovered_at: 'Recovered at',
|
||||
load_abnormal_failed: 'Failed to load abnormal rounds',
|
||||
view_play_records: 'View play records',
|
||||
play_record_dialog_title: 'Play (bet) records',
|
||||
play_record_col_id: 'ID',
|
||||
play_record_col_user: 'Username',
|
||||
play_record_col_channel: 'Channel',
|
||||
play_record_col_pick_numbers: 'Picks',
|
||||
play_record_col_total_amount: 'Play amount',
|
||||
play_record_col_is_auto: 'Auto',
|
||||
play_record_col_win_amount: 'Payout',
|
||||
play_record_col_jackpot_extra_amount: 'Jackpot extra',
|
||||
play_record_col_status: 'Status',
|
||||
play_record_col_create_time: 'Created',
|
||||
play_record_is_auto_0: 'Manual',
|
||||
play_record_is_auto_1: 'Auto bet',
|
||||
play_record_status_1: 'Pending draw',
|
||||
play_record_status_2: 'Settled',
|
||||
play_record_status_3: 'Refunded',
|
||||
play_record_status_4: 'Returned',
|
||||
load_play_record_failed: 'Failed to load play records',
|
||||
}
|
||||
|
||||
@@ -35,11 +35,11 @@ export default {
|
||||
pick_numbers: '下注号码',
|
||||
total_amount: '下注总额',
|
||||
streak_at_bet: '下注时连胜',
|
||||
runtime_switch: '游戏运行',
|
||||
runtime_switch: '自动创建下一局',
|
||||
countdown_maintenance: '维护中',
|
||||
runtime_draining_banner: '已关闭游戏:当前局将正常进行至开奖、结算并完成派彩;全部结束后进入维护模式(倒计时与操作区将切换为维护中)。',
|
||||
runtime_maintenance_banner: '维护中:玩家端已禁止下注。请开启「游戏运行」恢复;若无进行中的局将自动创建新一期。',
|
||||
runtime_off_tip: '开启「游戏运行」后,若无进行中的局将立即创建新一期。',
|
||||
runtime_maintenance_banner: '维护中:玩家端已禁止下注。请开启「自动创建下一局」恢复;若无进行中的局将自动创建新一期。',
|
||||
runtime_off_tip: '开启「自动创建下一局」后,若无进行中的局将立即创建新一期。',
|
||||
void_btn: '作废本局',
|
||||
void_dialog_title: '作废本局',
|
||||
void_reason_label: '作废原因',
|
||||
|
||||
@@ -17,6 +17,18 @@ export default {
|
||||
'status 1': '待开奖',
|
||||
'status 2': '已结算',
|
||||
'status 3': '已退款',
|
||||
'status 4': '已退回',
|
||||
'status 5': '待审核',
|
||||
review_title: '中奖审核',
|
||||
review_open: '审核',
|
||||
review_approve: '通过',
|
||||
review_reject: '拒绝',
|
||||
review_remark: '拒绝备注',
|
||||
review_remark_placeholder: '请输入拒绝原因',
|
||||
review_remark_required: '拒绝时必须填写备注',
|
||||
review_approve_confirm: '确认审核通过并派彩提现吗?',
|
||||
review_approve_success: '审核通过,已派彩',
|
||||
review_reject_success: '已拒绝派彩',
|
||||
idempotency_key: '幂等键',
|
||||
create_time: '创建时间',
|
||||
update_time: '更新时间',
|
||||
|
||||
@@ -10,10 +10,10 @@ export default {
|
||||
'status 3': '派彩中',
|
||||
'status 4': '已结束',
|
||||
'status 5': '已作废',
|
||||
'status 6': '异常',
|
||||
draw_mode: '开奖方式',
|
||||
'draw_mode 0': '自动AI',
|
||||
'draw_mode 1': '手动预设',
|
||||
preset_number: '预设号码',
|
||||
result_number: '开奖号码',
|
||||
platform_profit_amount: '对局盈亏(平台)',
|
||||
winner_user_count: '中奖人数',
|
||||
@@ -26,13 +26,23 @@ export default {
|
||||
manual_create_label: '允许手动创建下一局',
|
||||
manual_create_tip: '开启后可在本页使用「手动创建下一局」按钮',
|
||||
btn_create_next: '手动创建下一局',
|
||||
view_abnormal_rounds: '查看异常对局',
|
||||
abnormal_dialog_title: '异常对局恢复记录',
|
||||
abnormal_dialog_tip: '展示服务重启后自动恢复的异常对局(自动作废并退款)。',
|
||||
abnormal_from_status: '异常前状态',
|
||||
refunded_user_count: '退款用户数',
|
||||
refunded_order_count: '退款注单数',
|
||||
refunded_total_amount: '退款总金额',
|
||||
recovered_at: '恢复时间',
|
||||
load_abnormal_failed: '加载异常对局失败',
|
||||
view_play_records: '查看游玩记录',
|
||||
play_record_dialog_title: '游玩(压注)记录',
|
||||
play_record_col_id: 'ID',
|
||||
play_record_col_user: '用户名',
|
||||
play_record_col_channel: '渠道',
|
||||
play_record_col_pick_numbers: '选号',
|
||||
play_record_col_total_amount: '游玩金额',
|
||||
play_record_col_is_auto: '托管',
|
||||
play_record_col_win_amount: '派彩',
|
||||
play_record_col_jackpot_extra_amount: 'Jackpot',
|
||||
play_record_col_status: '状态',
|
||||
play_record_col_create_time: '创建时间',
|
||||
play_record_is_auto_0: '手动',
|
||||
play_record_is_auto_1: '托管',
|
||||
play_record_status_1: '待开奖',
|
||||
play_record_status_2: '已结算',
|
||||
play_record_status_3: '已退款',
|
||||
play_record_status_4: '已退回',
|
||||
load_play_record_failed: '加载游玩记录失败',
|
||||
}
|
||||
|
||||
@@ -256,6 +256,7 @@ const calcLoading = ref(false)
|
||||
const drawLoading = ref(false)
|
||||
const pendingSwitchNumber = ref<number | null>(null)
|
||||
const runtimeSwitchLoading = ref(false)
|
||||
const pendingRuntimeTarget = ref<boolean | null>(null)
|
||||
const voidDialogVisible = ref(false)
|
||||
const voidReason = ref('')
|
||||
const voidSubmitting = ref(false)
|
||||
@@ -534,6 +535,23 @@ const canVoidPeriod = computed(() => {
|
||||
/** 派彩结束后的完整维护态:操作区除顶部开关外全部锁定 */
|
||||
const asideOperationLocked = computed(() => snapshot.maintenance_ui === true)
|
||||
|
||||
function toBool(v: unknown): boolean | null {
|
||||
if (typeof v === 'boolean') {
|
||||
return v
|
||||
}
|
||||
if (typeof v === 'number' && Number.isFinite(v)) {
|
||||
if (v === 1) return true
|
||||
if (v === 0) return false
|
||||
return null
|
||||
}
|
||||
if (typeof v === 'string') {
|
||||
const s = v.trim().toLowerCase()
|
||||
if (s === '1' || s === 'true') return true
|
||||
if (s === '0' || s === 'false') return false
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
function mergeLiveSnapshot(data: anyObj): void {
|
||||
if (data.record !== undefined) {
|
||||
snapshot.record = data.record
|
||||
@@ -553,11 +571,20 @@ function mergeLiveSnapshot(data: anyObj): void {
|
||||
snapshot.can_calculate = !!data.can_calculate
|
||||
snapshot.can_draw = !!data.can_draw
|
||||
snapshot.can_schedule_draw = !!(data.can_schedule_draw || data.can_draw)
|
||||
if (typeof data.runtime_enabled === 'boolean') {
|
||||
snapshot.runtime_enabled = data.runtime_enabled
|
||||
const runtimeEnabled = toBool(data.runtime_enabled)
|
||||
if (pendingRuntimeTarget.value !== null) {
|
||||
// 开关请求进行中:避免 ws 的旧快照覆盖用户刚切换的状态,导致 UI 回弹
|
||||
snapshot.runtime_enabled = pendingRuntimeTarget.value
|
||||
} else if (runtimeEnabled !== null) {
|
||||
snapshot.runtime_enabled = runtimeEnabled
|
||||
}
|
||||
if (typeof data.maintenance_ui === 'boolean') {
|
||||
snapshot.maintenance_ui = data.maintenance_ui
|
||||
if (pendingRuntimeTarget.value !== null) {
|
||||
// 开启时不应显示维护中;关闭后的维护中应由服务端在对局完全结束后下发
|
||||
snapshot.maintenance_ui = pendingRuntimeTarget.value ? false : snapshot.maintenance_ui
|
||||
} else {
|
||||
snapshot.maintenance_ui = data.maintenance_ui
|
||||
}
|
||||
}
|
||||
syncServerClock(data.server_time)
|
||||
}
|
||||
@@ -615,11 +642,18 @@ async function loadSnapshot() {
|
||||
}
|
||||
|
||||
async function onRuntimeSwitch(val: boolean | string | number): void {
|
||||
const on = val === true || val === 'true' || val === 1
|
||||
const on = toBool(val) === true
|
||||
// 防止某些场景下 model-value 变化触发重复 change 事件,造成 runtime 接口循环调用
|
||||
if (on === !!snapshot.runtime_enabled) {
|
||||
const current = toBool(snapshot.runtime_enabled) === true
|
||||
if (on === current) {
|
||||
return
|
||||
}
|
||||
// el-switch 为受控组件(model-value 来自 snapshot),接口返回前先乐观更新,避免点击后立刻回弹
|
||||
snapshot.runtime_enabled = on
|
||||
if (on) {
|
||||
snapshot.maintenance_ui = false
|
||||
}
|
||||
pendingRuntimeTarget.value = on
|
||||
runtimeSwitchLoading.value = true
|
||||
try {
|
||||
const res = await createAxios({
|
||||
@@ -637,6 +671,7 @@ async function onRuntimeSwitch(val: boolean | string | number): void {
|
||||
await loadSnapshot()
|
||||
} finally {
|
||||
runtimeSwitchLoading.value = false
|
||||
pendingRuntimeTarget.value = null
|
||||
}
|
||||
}
|
||||
|
||||
@@ -667,6 +702,14 @@ async function submitVoidPeriod(): Promise<void> {
|
||||
})
|
||||
if (res.code === 1 && res.data) {
|
||||
mergeLiveSnapshot(res.data as anyObj)
|
||||
const refund = (res.data as anyObj).void_refund
|
||||
if (refund && typeof refund === 'object') {
|
||||
const orderCount = Number(refund.order_count ?? 0)
|
||||
const totalAmount = String(refund.total_amount ?? '0.00')
|
||||
if (orderCount > 0) {
|
||||
ElMessage.success(`已退款 ${orderCount} 笔注单,合计 ${totalAmount}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
voidDialogVisible.value = false
|
||||
} finally {
|
||||
|
||||
@@ -8,16 +8,49 @@
|
||||
></TableHeader>
|
||||
|
||||
<Table ref="tableRef"></Table>
|
||||
|
||||
<el-dialog class="ba-operate-dialog" :title="t('game.playRecord.review_title')" :model-value="reviewDialog.visible" width="620px" :close-on-click-modal="false" @close="closeReviewDialog">
|
||||
<div v-loading="reviewDialog.loading">
|
||||
<el-descriptions :column="2" border class="mb-12">
|
||||
<el-descriptions-item :label="t('game.playRecord.id')">{{ reviewDialog.row?.id ?? '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item :label="t('game.playRecord.gameRecord_period_no')">{{ reviewDialog.row?.gameRecord?.period_no ?? '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item :label="t('game.playRecord.user_username')">{{ reviewDialog.row?.user?.username ?? '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item :label="t('game.playRecord.channel_name')">{{ reviewDialog.row?.channel?.name ?? '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item :label="t('game.playRecord.pick_numbers')">{{ formatPickNumbers({}, {}, reviewDialog.row?.pick_numbers) }}</el-descriptions-item>
|
||||
<el-descriptions-item :label="t('game.playRecord.total_amount')">{{ formatAmount({}, {}, reviewDialog.row?.total_amount) }}</el-descriptions-item>
|
||||
<el-descriptions-item :label="t('game.playRecord.win_amount')">{{ formatAmount({}, {}, reviewDialog.row?.win_amount) }}</el-descriptions-item>
|
||||
<el-descriptions-item :label="t('game.playRecord.status')">{{ reviewDialog.row?.status ? t(`game.playRecord.status ${reviewDialog.row.status}`) : '-' }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
<el-form label-width="90px">
|
||||
<el-form-item :label="t('game.playRecord.review_remark')">
|
||||
<el-input
|
||||
v-model="reviewDialog.remark"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
:placeholder="t('game.playRecord.review_remark_placeholder')"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
<template #footer>
|
||||
<el-button @click="closeReviewDialog">{{ t('Cancel') }}</el-button>
|
||||
<el-button type="danger" :loading="reviewDialog.loading" @click="submitReject">{{ t('game.playRecord.review_reject') }}</el-button>
|
||||
<el-button type="primary" :loading="reviewDialog.loading" @click="submitApprove">{{ t('game.playRecord.review_approve') }}</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, provide, useTemplateRef } from 'vue'
|
||||
import { onMounted, provide, reactive, useTemplateRef } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { baTableApi } from '/@/api/common'
|
||||
import { defaultOptButtons } from '/@/components/table'
|
||||
import TableHeader from '/@/components/table/header/index.vue'
|
||||
import Table from '/@/components/table/index.vue'
|
||||
import baTableClass from '/@/utils/baTable'
|
||||
import createAxios from '/@/utils/axios'
|
||||
|
||||
defineOptions({
|
||||
name: 'game/playRecord',
|
||||
@@ -25,6 +58,14 @@ defineOptions({
|
||||
|
||||
const { t } = useI18n()
|
||||
const tableRef = useTemplateRef('tableRef')
|
||||
const optButtons: OptButton[] = defaultOptButtons([])
|
||||
const reviewDialog = reactive({
|
||||
visible: false,
|
||||
loading: false,
|
||||
id: 0,
|
||||
remark: '',
|
||||
row: null as any,
|
||||
})
|
||||
|
||||
function formatPickNumbers(_row: anyObj, _column: any, cellValue: unknown) {
|
||||
if (cellValue === null || cellValue === undefined) {
|
||||
@@ -119,7 +160,6 @@ const baTable = new baTableClass(
|
||||
replaceValue: { '0': t('game.playRecord.is_auto 0'), '1': t('game.playRecord.is_auto 1') },
|
||||
},
|
||||
{ label: t('game.playRecord.win_amount'), prop: 'win_amount', align: 'center', minWidth: 110, operator: 'RANGE', formatter: formatAmount },
|
||||
{ label: t('game.playRecord.jackpot_extra_amount'), prop: 'jackpot_extra_amount', align: 'center', minWidth: 120, operator: 'RANGE', formatter: formatAmount },
|
||||
{
|
||||
label: t('game.playRecord.status'),
|
||||
prop: 'status',
|
||||
@@ -128,11 +168,13 @@ const baTable = new baTableClass(
|
||||
operator: 'eq',
|
||||
render: 'tag',
|
||||
effect: 'dark',
|
||||
custom: { '1': 'warning', '2': 'success', '3': 'danger' },
|
||||
custom: { '1': 'warning', '2': 'success', '3': 'danger', '4': 'info', '5': 'warning' },
|
||||
replaceValue: {
|
||||
'1': t('game.playRecord.status 1'),
|
||||
'2': t('game.playRecord.status 2'),
|
||||
'3': t('game.playRecord.status 3'),
|
||||
'4': t('game.playRecord.status 4'),
|
||||
'5': t('game.playRecord.status 5'),
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -166,12 +208,110 @@ const baTable = new baTableClass(
|
||||
width: 170,
|
||||
timeFormat: 'yyyy-mm-dd hh:MM:ss',
|
||||
},
|
||||
{ label: t('Operate'), align: 'center', width: 90, render: 'buttons', buttons: optButtons, operator: false, fixed: 'right' },
|
||||
],
|
||||
dblClickNotEditColumn: [undefined],
|
||||
},
|
||||
{}
|
||||
)
|
||||
|
||||
const openReviewDialog = (row: any) => {
|
||||
const idRaw = row?.id
|
||||
const id = typeof idRaw === 'number' ? idRaw : Number(idRaw)
|
||||
if (!Number.isFinite(id) || id <= 0) {
|
||||
ElMessage.error(t('Parameter error'))
|
||||
return
|
||||
}
|
||||
reviewDialog.visible = true
|
||||
reviewDialog.id = id
|
||||
reviewDialog.remark = ''
|
||||
reviewDialog.row = row ?? null
|
||||
}
|
||||
|
||||
const closeReviewDialog = (force = false) => {
|
||||
if (reviewDialog.loading && !force) return
|
||||
reviewDialog.visible = false
|
||||
reviewDialog.id = 0
|
||||
reviewDialog.remark = ''
|
||||
reviewDialog.row = null
|
||||
}
|
||||
|
||||
const submitApprove = async () => {
|
||||
if (!reviewDialog.id) return
|
||||
reviewDialog.loading = true
|
||||
try {
|
||||
await createAxios(
|
||||
{
|
||||
url: '/admin/game.PlayRecord/approveJackpot',
|
||||
method: 'post',
|
||||
data: { id: reviewDialog.id, remark: reviewDialog.remark },
|
||||
},
|
||||
{ showSuccessMessage: false }
|
||||
)
|
||||
ElMessage.success(t('game.playRecord.review_approve_success'))
|
||||
if (reviewDialog.row) {
|
||||
reviewDialog.row.status = 2
|
||||
reviewDialog.row.can_jackpot_approve = 0
|
||||
}
|
||||
closeReviewDialog(true)
|
||||
baTable.onTableHeaderAction('refresh', { event: 'review-approve' })
|
||||
} catch (error: any) {
|
||||
const message = typeof error?.message === 'string' && error.message !== '' ? error.message : t('Operation failed')
|
||||
ElMessage.error(message)
|
||||
} finally {
|
||||
reviewDialog.loading = false
|
||||
}
|
||||
}
|
||||
|
||||
const submitReject = async () => {
|
||||
if (!reviewDialog.id) return
|
||||
const remark = String(reviewDialog.remark ?? '').trim()
|
||||
if (!remark) {
|
||||
ElMessage.error(t('game.playRecord.review_remark_required'))
|
||||
return
|
||||
}
|
||||
reviewDialog.loading = true
|
||||
try {
|
||||
await createAxios(
|
||||
{
|
||||
url: '/admin/game.PlayRecord/rejectJackpot',
|
||||
method: 'post',
|
||||
data: { id: reviewDialog.id, remark },
|
||||
},
|
||||
{ showSuccessMessage: false }
|
||||
)
|
||||
ElMessage.success(t('game.playRecord.review_reject_success'))
|
||||
if (reviewDialog.row) {
|
||||
reviewDialog.row.status = 4
|
||||
reviewDialog.row.can_jackpot_approve = 0
|
||||
}
|
||||
closeReviewDialog(true)
|
||||
baTable.onTableHeaderAction('refresh', { event: 'review-reject' })
|
||||
} catch (error: any) {
|
||||
const message = typeof error?.message === 'string' && error.message !== '' ? error.message : t('Operation failed')
|
||||
ElMessage.error(message)
|
||||
} finally {
|
||||
reviewDialog.loading = false
|
||||
}
|
||||
}
|
||||
|
||||
optButtons.unshift({
|
||||
render: 'tipButton',
|
||||
name: 'approve-jackpot',
|
||||
title: 'game.playRecord.review_open',
|
||||
text: '',
|
||||
type: 'warning',
|
||||
icon: 'fa fa-check',
|
||||
disabledTip: true,
|
||||
display: (row: any) => {
|
||||
const v = row?.can_jackpot_approve
|
||||
return String(v ?? '') === '1'
|
||||
},
|
||||
click: (row: any) => {
|
||||
openReviewDialog(row)
|
||||
},
|
||||
})
|
||||
|
||||
provide('baTable', baTable)
|
||||
|
||||
onMounted(() => {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<template>
|
||||
<template>
|
||||
<div class="default-main ba-table-box">
|
||||
<el-alert class="ba-table-alert" v-if="baTable.table.remark" :title="baTable.table.remark" type="info" show-icon />
|
||||
|
||||
@@ -6,50 +6,16 @@
|
||||
:buttons="['refresh', 'comSearch', 'quickSearch', 'columnDisplay']"
|
||||
:quick-search-placeholder="t('Quick search placeholder', { fields: t('game.record.quick Search Fields') })"
|
||||
>
|
||||
<el-button v-blur class="table-header-operate btns-ml-12" type="warning" @click="openAbnormalDialog">
|
||||
<Icon name="fa fa-exclamation-triangle" />
|
||||
<span class="table-header-operate-text">{{ t('game.record.view_abnormal_rounds') }}</span>
|
||||
</el-button>
|
||||
</TableHeader>
|
||||
|
||||
<Table ref="tableRef"></Table>
|
||||
|
||||
<PopupForm />
|
||||
|
||||
<el-dialog
|
||||
class="ba-operate-dialog"
|
||||
:title="t('game.record.abnormal_dialog_title')"
|
||||
:model-value="abnormalDialog.visible"
|
||||
width="900px"
|
||||
:close-on-click-modal="false"
|
||||
@close="closeAbnormalDialog"
|
||||
>
|
||||
<div v-loading="abnormalDialog.loading">
|
||||
<el-alert type="info" :closable="false" show-icon class="mb-12">
|
||||
{{ t('game.record.abnormal_dialog_tip') }}
|
||||
</el-alert>
|
||||
<el-table :data="abnormalDialog.list" border size="small" max-height="420">
|
||||
<el-table-column prop="period_no" :label="t('game.record.period_no')" min-width="180" />
|
||||
<el-table-column :label="t('game.record.abnormal_from_status')" min-width="120">
|
||||
<template #default="scope">{{ formatStatusLabel(scope.row.abnormal_from_status) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="refunded_user_count" :label="t('game.record.refunded_user_count')" min-width="120" />
|
||||
<el-table-column prop="refunded_order_count" :label="t('game.record.refunded_order_count')" min-width="120" />
|
||||
<el-table-column prop="refunded_total_amount" :label="t('game.record.refunded_total_amount')" min-width="140" />
|
||||
<el-table-column prop="recovered_at" :label="t('game.record.recovered_at')" min-width="170">
|
||||
<template #default="scope">{{ formatRecoveredTime(scope.row.recovered_at) }}</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
<template #footer>
|
||||
<el-button @click="closeAbnormalDialog">{{ t('Cancel') }}</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, provide, reactive, useTemplateRef } from 'vue'
|
||||
import { onMounted, provide, useTemplateRef } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import PopupForm from './popupForm.vue'
|
||||
@@ -58,9 +24,7 @@ import { defaultOptButtons } from '/@/components/table'
|
||||
import TableHeader from '/@/components/table/header/index.vue'
|
||||
import Table from '/@/components/table/index.vue'
|
||||
import baTableClass from '/@/utils/baTable'
|
||||
import createAxios from '/@/utils/axios'
|
||||
import { timeFormat } from '/@/utils/common'
|
||||
|
||||
import { routePush } from '/@/utils/router'
|
||||
defineOptions({
|
||||
name: 'game/record',
|
||||
})
|
||||
@@ -68,19 +32,37 @@ defineOptions({
|
||||
const { t } = useI18n()
|
||||
const tableRef = useTemplateRef('tableRef')
|
||||
const optButtons: OptButton[] = defaultOptButtons(['edit'])
|
||||
const abnormalDialog = reactive({
|
||||
visible: false,
|
||||
loading: false,
|
||||
list: [] as any[],
|
||||
})
|
||||
|
||||
const formatCoin = (_row: any, _column: any, cellValue: number | string | null | undefined) => {
|
||||
if (cellValue === null || cellValue === undefined || cellValue === '') return '—'
|
||||
if (cellValue === null || cellValue === undefined || cellValue === '') return '-'
|
||||
const n = Number(cellValue)
|
||||
if (Number.isNaN(n)) return '—'
|
||||
if (Number.isNaN(n)) return '-'
|
||||
return n.toFixed(2)
|
||||
}
|
||||
|
||||
optButtons.unshift({
|
||||
render: 'tipButton',
|
||||
name: 'view-play-record',
|
||||
title: 'game.record.view_play_records',
|
||||
text: '',
|
||||
type: 'success',
|
||||
icon: 'fa fa-list',
|
||||
class: 'table-row-view-play-record',
|
||||
disabledTip: false,
|
||||
click: (row: any) => {
|
||||
const pidRaw = row?.id
|
||||
const pid = typeof pidRaw === 'number' ? pidRaw : Number(pidRaw)
|
||||
if (!Number.isFinite(pid) || pid <= 0) {
|
||||
ElMessage.error(t('Parameter error'))
|
||||
return
|
||||
}
|
||||
void routePush({
|
||||
path: '/admin/game/playRecord',
|
||||
query: { period_id: String(pid) },
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
const baTable = new baTableClass(
|
||||
new baTableApi('/admin/game.Record/'),
|
||||
{
|
||||
@@ -88,8 +70,25 @@ const baTable = new baTableClass(
|
||||
column: [
|
||||
{ type: 'selection', align: 'center', operator: false },
|
||||
{ label: t('game.record.id'), prop: 'id', align: 'center', width: 100, operator: 'RANGE', sortable: 'custom' },
|
||||
{ label: t('game.record.period_no'), prop: 'period_no', align: 'center', minWidth: 180, operatorPlaceholder: t('Fuzzy query'), operator: 'LIKE' },
|
||||
{ label: t('game.record.period_start_at'), prop: 'period_start_at', align: 'center', width: 170, render: 'datetime', operator: 'RANGE', comSearchRender: 'datetime', sortable: 'custom', timeFormat: 'yyyy-mm-dd hh:MM:ss' },
|
||||
{
|
||||
label: t('game.record.period_no'),
|
||||
prop: 'period_no',
|
||||
align: 'center',
|
||||
minWidth: 180,
|
||||
operatorPlaceholder: t('Fuzzy query'),
|
||||
operator: 'LIKE',
|
||||
},
|
||||
{
|
||||
label: t('game.record.period_start_at'),
|
||||
prop: 'period_start_at',
|
||||
align: 'center',
|
||||
width: 170,
|
||||
render: 'datetime',
|
||||
operator: 'RANGE',
|
||||
comSearchRender: 'datetime',
|
||||
sortable: 'custom',
|
||||
timeFormat: 'yyyy-mm-dd hh:MM:ss',
|
||||
},
|
||||
{
|
||||
label: t('game.record.status'),
|
||||
prop: 'status',
|
||||
@@ -98,8 +97,16 @@ const baTable = new baTableClass(
|
||||
operator: 'eq',
|
||||
render: 'tag',
|
||||
effect: 'dark',
|
||||
custom: { '0': 'success', '1': 'warning', '2': 'info', '3': 'primary', '4': 'warning', '5': 'danger' },
|
||||
replaceValue: { '0': t('game.record.status 0'), '1': t('game.record.status 1'), '2': t('game.record.status 2'), '3': t('game.record.status 3'), '4': t('game.record.status 4'), '5': t('game.record.status 5') },
|
||||
custom: { '0': 'success', '1': 'warning', '2': 'info', '3': 'primary', '4': 'warning', '5': 'danger', '6': 'danger' },
|
||||
replaceValue: {
|
||||
'0': t('game.record.status 0'),
|
||||
'1': t('game.record.status 1'),
|
||||
'2': t('game.record.status 2'),
|
||||
'3': t('game.record.status 3'),
|
||||
'4': t('game.record.status 4'),
|
||||
'5': t('game.record.status 5'),
|
||||
'6': t('game.record.status 6'),
|
||||
},
|
||||
},
|
||||
{
|
||||
label: t('game.record.draw_mode'),
|
||||
@@ -111,7 +118,6 @@ const baTable = new baTableClass(
|
||||
custom: { '0': 'info', '1': 'warning' },
|
||||
replaceValue: { '0': t('game.record.draw_mode 0'), '1': t('game.record.draw_mode 1') },
|
||||
},
|
||||
{ label: t('game.record.preset_number'), prop: 'preset_number', align: 'center', width: 100, operator: 'RANGE' },
|
||||
{ label: t('game.record.result_number'), prop: 'result_number', align: 'center', width: 100, operator: 'RANGE' },
|
||||
{
|
||||
label: t('game.record.platform_profit_amount'),
|
||||
@@ -130,9 +136,37 @@ const baTable = new baTableClass(
|
||||
operator: 'RANGE',
|
||||
sortable: false,
|
||||
},
|
||||
{ label: t('game.record.void_reason'), prop: 'void_reason', align: 'center', minWidth: 140, operatorPlaceholder: t('Fuzzy query'), operator: 'LIKE', showOverflowTooltip: true },
|
||||
{ label: t('game.record.create_time'), prop: 'create_time', align: 'center', render: 'datetime', operator: 'RANGE', comSearchRender: 'datetime', sortable: 'custom', width: 170, timeFormat: 'yyyy-mm-dd hh:MM:ss' },
|
||||
{ label: t('game.record.update_time'), prop: 'update_time', align: 'center', render: 'datetime', operator: 'RANGE', comSearchRender: 'datetime', sortable: 'custom', width: 170, timeFormat: 'yyyy-mm-dd hh:MM:ss' },
|
||||
{
|
||||
label: t('game.record.void_reason'),
|
||||
prop: 'void_reason',
|
||||
align: 'center',
|
||||
minWidth: 140,
|
||||
operatorPlaceholder: t('Fuzzy query'),
|
||||
operator: 'LIKE',
|
||||
showOverflowTooltip: true,
|
||||
},
|
||||
{
|
||||
label: t('game.record.create_time'),
|
||||
prop: 'create_time',
|
||||
align: 'center',
|
||||
render: 'datetime',
|
||||
operator: 'RANGE',
|
||||
comSearchRender: 'datetime',
|
||||
sortable: 'custom',
|
||||
width: 170,
|
||||
timeFormat: 'yyyy-mm-dd hh:MM:ss',
|
||||
},
|
||||
{
|
||||
label: t('game.record.update_time'),
|
||||
prop: 'update_time',
|
||||
align: 'center',
|
||||
render: 'datetime',
|
||||
operator: 'RANGE',
|
||||
comSearchRender: 'datetime',
|
||||
sortable: 'custom',
|
||||
width: 170,
|
||||
timeFormat: 'yyyy-mm-dd hh:MM:ss',
|
||||
},
|
||||
{ label: t('Operate'), align: 'center', width: 100, render: 'buttons', buttons: optButtons, operator: false, fixed: 'right' },
|
||||
],
|
||||
dblClickNotEditColumn: [undefined],
|
||||
@@ -142,50 +176,6 @@ const baTable = new baTableClass(
|
||||
}
|
||||
)
|
||||
|
||||
const formatStatusLabel = (status: number) => {
|
||||
const key = String(status)
|
||||
if (!['0', '1', '2', '3', '4', '5'].includes(key)) {
|
||||
return '-'
|
||||
}
|
||||
return t(`game.record.status ${key}`)
|
||||
}
|
||||
|
||||
const openAbnormalDialog = async () => {
|
||||
abnormalDialog.visible = true
|
||||
abnormalDialog.loading = true
|
||||
try {
|
||||
const response = await createAxios(
|
||||
{
|
||||
url: '/admin/game.Record/abnormalList',
|
||||
method: 'get',
|
||||
params: { limit: 100 },
|
||||
},
|
||||
{
|
||||
showSuccessMessage: false,
|
||||
}
|
||||
)
|
||||
const data = response?.data ?? {}
|
||||
abnormalDialog.list = Array.isArray(data.list) ? data.list : []
|
||||
} catch (error: any) {
|
||||
const message = typeof error?.message === 'string' && error.message !== '' ? error.message : t('game.record.load_abnormal_failed')
|
||||
ElMessage.error(message)
|
||||
abnormalDialog.list = []
|
||||
} finally {
|
||||
abnormalDialog.loading = false
|
||||
}
|
||||
}
|
||||
|
||||
const closeAbnormalDialog = () => {
|
||||
abnormalDialog.visible = false
|
||||
}
|
||||
|
||||
const formatRecoveredTime = (timestamp: number) => {
|
||||
if (!timestamp) {
|
||||
return '-'
|
||||
}
|
||||
return timeFormat(timestamp)
|
||||
}
|
||||
|
||||
provide('baTable', baTable)
|
||||
|
||||
onMounted(() => {
|
||||
|
||||
@@ -24,7 +24,6 @@
|
||||
prop="draw_mode"
|
||||
:input-attr="{ content: { '0': t('game.record.draw_mode 0'), '1': t('game.record.draw_mode 1') } }"
|
||||
/>
|
||||
<FormItem :label="t('game.record.preset_number')" type="number" v-model="baTable.form.items!.preset_number" prop="preset_number" :input-attr="{ step: 1, min: 1, max: 36 }" />
|
||||
<FormItem :label="t('game.record.result_number')" type="number" v-model="baTable.form.items!.result_number" prop="result_number" :input-attr="{ step: 1, min: 1, max: 36 }" />
|
||||
<FormItem :label="t('game.record.platform_profit_amount')" type="string" v-model="baTable.form.items!.platform_profit_amount" prop="platform_profit_amount" />
|
||||
<FormItem :label="t('game.record.winner_user_count')" type="number" v-model="baTable.form.items!.winner_user_count" prop="winner_user_count" :input-attr="{ step: 1, min: 0 }" />
|
||||
|
||||
Reference in New Issue
Block a user