1.优化实时对局页面样式以及自动创建下一局和作废本局的记录

2.新增派彩达到game_config.jackpot_max_amount必须审核才能发放
3.新增游戏对局记录-查看游玩记录btn
3.备份MySQL数据库
This commit is contained in:
2026-04-28 18:25:44 +08:00
parent 4ea83d2818
commit 687257adaa
22 changed files with 945 additions and 208 deletions

View File

@@ -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);
}
}

View File

@@ -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;
}
}

View File

@@ -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}
*/