'desc']; protected string|array $orderGuarantee = ['id' => 'desc']; protected bool $modelValidate = true; protected bool $modelSceneValidate = true; protected function initController(WebmanRequest $request): ?Response { $this->model = new \app\common\model\GameRecord(); 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); if ($response !== null) { return $response; } return $this->error(__('Game record is generated by system; manual creation is not allowed')); } public function edit(WebmanRequest $request): Response { $response = $this->initializeBackend($request); if ($response !== null) { return $response; } if ($request->method() === 'POST') { return $this->error(__('Game record cannot be edited')); } $pk = $this->model->getPk(); $id = $request->get($pk); $row = $this->model->find($id); if (!$row) { return $this->error(__('Record not found')); } return $this->success('', ['row' => $row]); } public function del(WebmanRequest $request): Response { $response = $this->initializeBackend($request); if ($response !== null) { return $response; } return $this->error(__('Game record cannot be deleted')); } public function abnormalList(WebmanRequest $request): Response { $response = $this->initializeBackend($request); if ($response !== null) { return $response; } $limitRaw = $request->get('limit', 30); $limit = is_numeric((string) $limitRaw) ? (int) $limitRaw : 30; if ($limit < 1) { $limit = 1; } if ($limit > 200) { $limit = 200; } $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' => $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, ]; } return $this->success('', [ 'list' => $list, 'total' => count($list), ]); } /** * 某期对局的游玩(压注)记录列表(用于对局记录页弹窗查看)。 */ 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} */ private function parseRecoverVoidReason(string $reason): array { $meta = [ 'from_status' => -1, 'users' => 0, 'orders' => 0, 'amount' => '0.00', ]; if (!$this->isSystemRecoverReason($reason)) { return $meta; } $payload = substr($reason, strlen('system_recover:')); if (!str_contains($payload, '=')) { return $meta; } $parts = explode('|', $payload); foreach ($parts as $part) { $item = trim($part); if ($item === '' || !str_contains($item, '=')) { continue; } [$key, $value] = explode('=', $item, 2); $key = trim($key); $value = trim($value); if ($key === 'from' && is_numeric($value)) { $meta['from_status'] = (int) $value; continue; } if ($key === 'users' && is_numeric($value)) { $meta['users'] = (int) $value; continue; } if ($key === 'orders' && is_numeric($value)) { $meta['orders'] = (int) $value; continue; } if ($key === 'amount' && $value !== '') { $meta['amount'] = $value; } } return $meta; } private function isSystemRecoverReason(string $reason): bool { return $reason !== '' && str_starts_with($reason, 'system_recover:'); } }