diff --git a/app/admin/controller/game/BetOrder.php b/app/admin/controller/game/BetOrder.php
new file mode 100644
index 0000000..254b505
--- /dev/null
+++ b/app/admin/controller/game/BetOrder.php
@@ -0,0 +1,129 @@
+ 'desc'];
+
+ protected string|array $orderGuarantee = ['id' => 'desc'];
+
+ protected array $withJoinTable = ['gameUser', 'channel', 'gamePeriod'];
+
+ protected function initController(WebmanRequest $request): ?Response
+ {
+ $this->model = new \app\common\model\GameBetOrder();
+ return null;
+ }
+
+ public function add(WebmanRequest $request): Response
+ {
+ $response = $this->initializeBackend($request);
+ if ($response !== null) {
+ return $response;
+ }
+ return $this->error('注单由游戏接口生成,禁止后台手工新增');
+ }
+
+ public function edit(WebmanRequest $request): Response
+ {
+ $response = $this->initializeBackend($request);
+ if ($response !== null) {
+ return $response;
+ }
+ return $this->error('注单不可编辑');
+ }
+
+ public function del(WebmanRequest $request): Response
+ {
+ $response = $this->initializeBackend($request);
+ if ($response !== null) {
+ return $response;
+ }
+ return $this->error('注单不可删除');
+ }
+
+ public function sortable(WebmanRequest $request): Response
+ {
+ $response = $this->initializeBackend($request);
+ if ($response !== null) {
+ return $response;
+ }
+ return $this->error('不支持排序');
+ }
+
+ /**
+ * 渠道管理员仅看本渠道注单
+ */
+ protected function _index(): Response
+ {
+ if ($this->request && $this->request->get('select')) {
+ return $this->select($this->request);
+ }
+
+ list($where, $alias, $limit, $order) = $this->queryBuilder();
+ $table = strtolower($this->model->getTable());
+ $mainShort = $alias[$table] ?? '';
+ if ($mainShort !== '' && $this->auth && !$this->auth->isSuperAdmin()) {
+ $channelIds = $this->getScopedChannelIdsForFilter();
+ $where[] = [$mainShort . '.channel_id', 'in', $channelIds !== [] ? $channelIds : [0]];
+ }
+
+ $res = $this->model
+ ->withJoin($this->withJoinTable, $this->withJoinType)
+ ->with($this->withJoinTable)
+ ->visible([
+ 'gameUser' => ['username', 'phone'],
+ 'channel' => ['name'],
+ 'gamePeriod' => ['period_no', 'status'],
+ ])
+ ->alias($alias)
+ ->where($where)
+ ->order($order)
+ ->paginate($limit);
+
+ return $this->success('', [
+ 'list' => $res->items(),
+ 'total' => $res->total(),
+ 'remark' => get_route_remark(),
+ ]);
+ }
+
+ /**
+ * @return int[]
+ */
+ private function getScopedChannelIdsForFilter(): array
+ {
+ if (!$this->auth) {
+ return [0];
+ }
+ if ($this->auth->isSuperAdmin()) {
+ return [];
+ }
+ $admin = Db::name('admin')
+ ->field(['id', 'channel_id'])
+ ->where('id', $this->auth->id)
+ ->find();
+ $ids = [];
+ if ($admin && !empty($admin['channel_id'])) {
+ $ids[] = $admin['channel_id'];
+ }
+ $owned = Db::name('channel')->where('top_admin_id', $this->auth->id)->column('id');
+ $created = Db::name('channel')->where('admin_id', $this->auth->id)->column('id');
+ return array_values(array_unique(array_merge($ids, $owned, $created)));
+ }
+}
diff --git a/app/common/model/GameBetOrder.php b/app/common/model/GameBetOrder.php
new file mode 100644
index 0000000..69de49f
--- /dev/null
+++ b/app/common/model/GameBetOrder.php
@@ -0,0 +1,41 @@
+ 'integer',
+ 'update_time' => 'integer',
+ 'pick_numbers' => 'json',
+ 'unit_amount' => 'string',
+ 'total_amount' => 'string',
+ 'win_amount' => 'string',
+ 'jackpot_extra_amount' => 'string',
+ 'status' => 'integer',
+ 'pick_count' => 'integer',
+ 'streak_at_bet' => 'integer',
+ 'is_auto' => 'integer',
+ ];
+
+ public function gameUser(): \think\model\relation\BelongsTo
+ {
+ return $this->belongsTo(GameUser::class, 'user_id', 'id');
+ }
+
+ public function channel(): \think\model\relation\BelongsTo
+ {
+ return $this->belongsTo(Channel::class, 'channel_id', 'id');
+ }
+
+ public function gamePeriod(): \think\model\relation\BelongsTo
+ {
+ return $this->belongsTo(GamePeriod::class, 'period_id', 'id');
+ }
+}
diff --git a/web/src/lang/backend/en/game/betOrder.ts b/web/src/lang/backend/en/game/betOrder.ts
new file mode 100644
index 0000000..a2cc4e6
--- /dev/null
+++ b/web/src/lang/backend/en/game/betOrder.ts
@@ -0,0 +1,35 @@
+export default {
+ 'quick Search Fields': 'ID / Period / Idempotency',
+ id: 'ID',
+ period_id: 'Period ID',
+ period_no: 'Period No.',
+ user_id: 'User ID',
+ channel_id: 'Channel ID',
+ pick_numbers: 'Picks',
+ unit_amount: 'Unit amount',
+ pick_count: 'Pick count',
+ total_amount: 'Total',
+ streak_at_bet: 'Streak at bet',
+ is_auto: 'Auto',
+ 'is_auto 0': 'Manual',
+ 'is_auto 1': 'Auto bet',
+ win_amount: 'Payout',
+ jackpot_extra_amount: 'Jackpot extra',
+ status: 'Status',
+ 'status 1': 'Pending draw',
+ 'status 2': 'Settled',
+ 'status 3': 'Refunded',
+ idempotency_key: 'Idempotency key',
+ create_time: 'Created',
+ update_time: 'Updated',
+ gamePeriod: {
+ period_no: 'Period (relation)',
+ status: 'Period status',
+ },
+ gameUser: {
+ username: 'Username',
+ },
+ channel: {
+ name: 'Channel',
+ },
+}
diff --git a/web/src/lang/backend/zh-cn/game/betOrder.ts b/web/src/lang/backend/zh-cn/game/betOrder.ts
new file mode 100644
index 0000000..4e92e2b
--- /dev/null
+++ b/web/src/lang/backend/zh-cn/game/betOrder.ts
@@ -0,0 +1,36 @@
+export default {
+ 'quick Search Fields': 'ID/期号/幂等键',
+ id: 'ID',
+ period_id: '期ID',
+ period_no: '期号',
+ user_id: '用户ID',
+ channel_id: '渠道ID',
+ pick_numbers: '选号',
+ unit_amount: '单号金额',
+ pick_count: '选号个数',
+ total_amount: '总金额',
+ streak_at_bet: '下注时连胜',
+ is_auto: '托管',
+ 'is_auto 0': '手动',
+ 'is_auto 1': '托管',
+ win_amount: '派彩',
+ jackpot_extra_amount: 'Jackpot',
+ status: '状态',
+ 'status 1': '待开奖',
+ 'status 2': '已结算',
+ 'status 3': '已退款',
+ idempotency_key: '幂等键',
+ create_time: '创建时间',
+ update_time: '更新时间',
+ /** 关联展示列(须嵌套,供 t('game.betOrder.gamePeriod.xxx')) */
+ gamePeriod: {
+ period_no: '对局期号',
+ status: '期状态',
+ },
+ gameUser: {
+ username: '用户名',
+ },
+ channel: {
+ name: '渠道',
+ },
+}
diff --git a/web/src/views/backend/game/betOrder/index.vue b/web/src/views/backend/game/betOrder/index.vue
new file mode 100644
index 0000000..7ae773b
--- /dev/null
+++ b/web/src/views/backend/game/betOrder/index.vue
@@ -0,0 +1,230 @@
+
+
+
+
+
+
+