初始化足球投注平台 MVP Monorepo
包含 NestJS 后端、三端前端、Prisma 数据模型、结算引擎测试与 PRD 文档。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
52
apps/admin/src/views/Agents.vue
Normal file
52
apps/admin/src/views/Agents.vue
Normal file
@@ -0,0 +1,52 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue';
|
||||
import api from '../api';
|
||||
import { ElMessage } from 'element-plus';
|
||||
|
||||
const agents = ref<unknown[]>([]);
|
||||
const form = ref({ username: '', password: 'Agent@123', creditLimit: 50000 });
|
||||
|
||||
onMounted(load);
|
||||
|
||||
async function load() {
|
||||
const { data } = await api.get('/admin/agents');
|
||||
agents.value = data.data;
|
||||
}
|
||||
|
||||
async function create() {
|
||||
await api.post('/admin/agents', form.value);
|
||||
ElMessage.success('创建成功');
|
||||
load();
|
||||
}
|
||||
|
||||
async function adjustCredit(agent: { userId: string }, amount: number) {
|
||||
await api.post(`/admin/agents/${agent.userId}/credit`, {
|
||||
amount,
|
||||
requestId: `credit-${Date.now()}`,
|
||||
});
|
||||
ElMessage.success('额度已调整');
|
||||
load();
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<h2>代理管理</h2>
|
||||
<el-form inline style="margin: 16px 0">
|
||||
<el-input v-model="form.username" placeholder="用户名" style="width: 120px" />
|
||||
<el-input-number v-model="form.creditLimit" placeholder="额度" />
|
||||
<el-button type="primary" @click="create">创建一级代理</el-button>
|
||||
</el-form>
|
||||
<el-table :data="agents">
|
||||
<el-table-column label="用户名">
|
||||
<template #default="{ row }">{{ (row as { user?: { username: string } }).user?.username }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="level" label="层级" />
|
||||
<el-table-column prop="creditLimit" label="授信额度" />
|
||||
<el-table-column prop="usedCredit" label="已用额度" />
|
||||
<el-table-column label="操作">
|
||||
<template #default="{ row }">
|
||||
<el-button size="small" @click="adjustCredit(row as { userId: string }, 10000)">+10000</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</template>
|
||||
23
apps/admin/src/views/Audit.vue
Normal file
23
apps/admin/src/views/Audit.vue
Normal file
@@ -0,0 +1,23 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue';
|
||||
import api from '../api';
|
||||
|
||||
const logs = ref<unknown[]>([]);
|
||||
|
||||
onMounted(async () => {
|
||||
const { data } = await api.get('/admin/audit-logs');
|
||||
logs.value = data.data.items;
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<h2>操作日志</h2>
|
||||
<el-table :data="logs">
|
||||
<el-table-column prop="action" label="操作" />
|
||||
<el-table-column prop="module" label="模块" />
|
||||
<el-table-column prop="targetId" label="目标" />
|
||||
<el-table-column label="时间">
|
||||
<template #default="{ row }">{{ new Date((row as { createdAt: string }).createdAt).toLocaleString() }}</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</template>
|
||||
24
apps/admin/src/views/Bets.vue
Normal file
24
apps/admin/src/views/Bets.vue
Normal file
@@ -0,0 +1,24 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue';
|
||||
import api from '../api';
|
||||
|
||||
const bets = ref<unknown[]>([]);
|
||||
|
||||
onMounted(async () => {
|
||||
const { data } = await api.get('/admin/bets');
|
||||
bets.value = data.data.items;
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<h2>注单管理</h2>
|
||||
<el-table :data="bets">
|
||||
<el-table-column prop="betNo" label="注单号" />
|
||||
<el-table-column prop="betType" label="类型" />
|
||||
<el-table-column prop="stake" label="投注额" />
|
||||
<el-table-column prop="status" label="状态" />
|
||||
<el-table-column label="时间">
|
||||
<template #default="{ row }">{{ new Date((row as { placedAt: string }).placedAt).toLocaleString() }}</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</template>
|
||||
39
apps/admin/src/views/Cashback.vue
Normal file
39
apps/admin/src/views/Cashback.vue
Normal file
@@ -0,0 +1,39 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
import api from '../api';
|
||||
import { ElMessage } from 'element-plus';
|
||||
|
||||
const preview = ref<Record<string, unknown> | null>(null);
|
||||
const period = ref({
|
||||
start: new Date(Date.now() - 7 * 86400000).toISOString().slice(0, 10),
|
||||
end: new Date().toISOString().slice(0, 10),
|
||||
});
|
||||
|
||||
async function generatePreview() {
|
||||
const { data } = await api.post('/admin/cashbacks/preview', {
|
||||
periodStart: period.value.start,
|
||||
periodEnd: period.value.end,
|
||||
});
|
||||
preview.value = data.data;
|
||||
}
|
||||
|
||||
async function confirm() {
|
||||
if (!preview.value?.batch) return;
|
||||
await api.post(`/admin/cashbacks/${(preview.value.batch as { id: string }).id}/confirm`);
|
||||
ElMessage.success('返水已发放');
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<h2>返水管理</h2>
|
||||
<el-form inline>
|
||||
<el-date-picker v-model="period.start" type="date" value-format="YYYY-MM-DD" />
|
||||
<el-date-picker v-model="period.end" type="date" value-format="YYYY-MM-DD" />
|
||||
<el-button @click="generatePreview">生成预览</el-button>
|
||||
</el-form>
|
||||
<el-card v-if="preview" style="margin-top: 16px">
|
||||
<p>玩家数: {{ (preview.batch as { playerCount: number })?.playerCount }}</p>
|
||||
<p>总金额: {{ preview.totalAmount }}</p>
|
||||
<el-button type="success" @click="confirm">确认发放</el-button>
|
||||
</el-card>
|
||||
</template>
|
||||
21
apps/admin/src/views/Dashboard.vue
Normal file
21
apps/admin/src/views/Dashboard.vue
Normal file
@@ -0,0 +1,21 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue';
|
||||
import api from '../api';
|
||||
|
||||
const stats = ref<Record<string, unknown>>({});
|
||||
|
||||
onMounted(async () => {
|
||||
const { data } = await api.get('/admin/dashboard');
|
||||
stats.value = data.data;
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<h2>控制台</h2>
|
||||
<el-row :gutter="16" style="margin-top: 16px">
|
||||
<el-col :span="6"><el-statistic title="今日投注笔数" :value="(stats.todayBetCount as number) || 0" /></el-col>
|
||||
<el-col :span="6"><el-statistic title="今日投注额" :value="Number(stats.todayStake) || 0" :precision="2" /></el-col>
|
||||
<el-col :span="6"><el-statistic title="今日派彩" :value="Number(stats.todayPayout) || 0" :precision="2" /></el-col>
|
||||
<el-col :span="6"><el-statistic title="待结算赛事" :value="(stats.pendingSettlement as number) || 0" /></el-col>
|
||||
</el-row>
|
||||
</template>
|
||||
40
apps/admin/src/views/Login.vue
Normal file
40
apps/admin/src/views/Login.vue
Normal file
@@ -0,0 +1,40 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import api from '../api';
|
||||
import { ElMessage } from 'element-plus';
|
||||
|
||||
const router = useRouter();
|
||||
const form = ref({ username: 'admin', password: 'Admin@123' });
|
||||
const loading = ref(false);
|
||||
|
||||
async function login() {
|
||||
loading.value = true;
|
||||
try {
|
||||
const { data } = await api.post('/admin/auth/login', form.value);
|
||||
localStorage.setItem('admin_token', data.data.token);
|
||||
router.push('/');
|
||||
} catch {
|
||||
ElMessage.error('登录失败');
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div style="min-height: 100vh; display: flex; align-items: center; justify-content: center">
|
||||
<el-card style="width: 400px">
|
||||
<h2 style="text-align: center; margin-bottom: 24px">平台后台登录</h2>
|
||||
<el-form @submit.prevent="login">
|
||||
<el-form-item label="用户名">
|
||||
<el-input v-model="form.username" />
|
||||
</el-form-item>
|
||||
<el-form-item label="密码">
|
||||
<el-input v-model="form.password" type="password" />
|
||||
</el-form-item>
|
||||
<el-button type="primary" native-type="submit" :loading="loading" style="width: 100%">登录</el-button>
|
||||
</el-form>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
74
apps/admin/src/views/Matches.vue
Normal file
74
apps/admin/src/views/Matches.vue
Normal file
@@ -0,0 +1,74 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import api from '../api';
|
||||
import { ElMessage } from 'element-plus';
|
||||
|
||||
const router = useRouter();
|
||||
const matches = ref<unknown[]>([]);
|
||||
const form = ref({
|
||||
leagueId: '',
|
||||
homeTeamId: '',
|
||||
awayTeamId: '',
|
||||
startTime: '',
|
||||
});
|
||||
|
||||
onMounted(load);
|
||||
|
||||
async function load() {
|
||||
const { data } = await api.get('/admin/matches');
|
||||
matches.value = data.data;
|
||||
}
|
||||
|
||||
async function create() {
|
||||
await api.post('/admin/matches', form.value);
|
||||
ElMessage.success('赛事已创建');
|
||||
load();
|
||||
}
|
||||
|
||||
async function publish(id: string) {
|
||||
await api.post(`/admin/matches/${id}/publish`);
|
||||
await api.post(`/admin/matches/${id}/markets/templates`, {
|
||||
marketTypes: ['FT_1X2', 'FT_HANDICAP', 'FT_OVER_UNDER', 'FT_ODD_EVEN'],
|
||||
});
|
||||
ElMessage.success('已发布并生成盘口');
|
||||
load();
|
||||
}
|
||||
|
||||
async function close(id: string) {
|
||||
await api.post(`/admin/matches/${id}/close`);
|
||||
ElMessage.success('已封盘');
|
||||
load();
|
||||
}
|
||||
|
||||
function settle(id: string) {
|
||||
router.push(`/settlement/${id}`);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<h2>赛事管理</h2>
|
||||
<el-card style="margin-bottom: 16px">
|
||||
<el-form inline>
|
||||
<el-input v-model="form.leagueId" placeholder="联赛ID" style="width: 100px" />
|
||||
<el-input v-model="form.homeTeamId" placeholder="主队ID" style="width: 100px" />
|
||||
<el-input v-model="form.awayTeamId" placeholder="客队ID" style="width: 100px" />
|
||||
<el-input v-model="form.startTime" placeholder="开赛时间 ISO" style="width: 200px" />
|
||||
<el-button type="primary" @click="create">创建赛事</el-button>
|
||||
</el-form>
|
||||
</el-card>
|
||||
<el-table :data="matches">
|
||||
<el-table-column prop="id" label="ID" width="80" />
|
||||
<el-table-column prop="status" label="状态" />
|
||||
<el-table-column label="开赛时间">
|
||||
<template #default="{ row }">{{ new Date((row as { startTime: string }).startTime).toLocaleString() }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="300">
|
||||
<template #default="{ row }">
|
||||
<el-button v-if="(row as { status: string }).status === 'DRAFT'" size="small" @click="publish((row as { id: string }).id)">发布</el-button>
|
||||
<el-button v-if="(row as { status: string }).status === 'PUBLISHED'" size="small" @click="close((row as { id: string }).id)">封盘</el-button>
|
||||
<el-button size="small" type="warning" @click="settle((row as { id: string }).id)">结算</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</template>
|
||||
46
apps/admin/src/views/Settlement.vue
Normal file
46
apps/admin/src/views/Settlement.vue
Normal file
@@ -0,0 +1,46 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
import api from '../api';
|
||||
import { ElMessage } from 'element-plus';
|
||||
|
||||
const route = useRoute();
|
||||
const score = ref({ htHome: 0, htAway: 0, ftHome: 0, ftAway: 0 });
|
||||
const preview = ref<Record<string, unknown> | null>(null);
|
||||
|
||||
async function recordScore() {
|
||||
await api.post(`/admin/matches/${route.params.id}/settlement/score`, score.value);
|
||||
ElMessage.success('比分已录入');
|
||||
}
|
||||
|
||||
async function previewSettlement() {
|
||||
const { data } = await api.post(`/admin/matches/${route.params.id}/settlement/preview`);
|
||||
preview.value = data.data;
|
||||
}
|
||||
|
||||
async function confirm() {
|
||||
if (!preview.value?.batch) return;
|
||||
await api.post(`/admin/settlement/${(preview.value.batch as { id: string }).id}/confirm`);
|
||||
ElMessage.success('结算已确认');
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<h2>赛事结算 #{{ route.params.id }}</h2>
|
||||
<el-form inline style="margin: 16px 0">
|
||||
<el-input-number v-model="score.htHome" :min="0" />
|
||||
<el-input-number v-model="score.htAway" :min="0" />
|
||||
<span>半场</span>
|
||||
<el-input-number v-model="score.ftHome" :min="0" />
|
||||
<el-input-number v-model="score.ftAway" :min="0" />
|
||||
<span>全场</span>
|
||||
<el-button @click="recordScore">录入比分</el-button>
|
||||
<el-button type="primary" @click="previewSettlement">生成预览</el-button>
|
||||
</el-form>
|
||||
<el-card v-if="preview">
|
||||
<p>单关注单: {{ preview.singleBetCount }}</p>
|
||||
<p>预计派彩: {{ preview.totalPayout }}</p>
|
||||
<p>退款: {{ preview.totalRefund }}</p>
|
||||
<el-button type="success" @click="confirm">确认结算</el-button>
|
||||
</el-card>
|
||||
</template>
|
||||
38
apps/admin/src/views/Users.vue
Normal file
38
apps/admin/src/views/Users.vue
Normal file
@@ -0,0 +1,38 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue';
|
||||
import api from '../api';
|
||||
import { ElMessage } from 'element-plus';
|
||||
|
||||
const users = ref<unknown[]>([]);
|
||||
const form = ref({ username: '', password: 'Player@123', parentId: '' });
|
||||
|
||||
onMounted(load);
|
||||
|
||||
async function load() {
|
||||
const { data } = await api.get('/admin/users');
|
||||
users.value = data.data.items;
|
||||
}
|
||||
|
||||
async function create() {
|
||||
await api.post('/admin/users', form.value);
|
||||
ElMessage.success('创建成功');
|
||||
form.value.username = '';
|
||||
load();
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<h2>玩家管理</h2>
|
||||
<el-form inline style="margin: 16px 0">
|
||||
<el-input v-model="form.username" placeholder="用户名" style="width: 150px" />
|
||||
<el-input v-model="form.password" placeholder="密码" style="width: 150px" />
|
||||
<el-button type="primary" @click="create">创建玩家</el-button>
|
||||
</el-form>
|
||||
<el-table :data="users">
|
||||
<el-table-column prop="username" label="用户名" />
|
||||
<el-table-column prop="status" label="状态" />
|
||||
<el-table-column label="余额">
|
||||
<template #default="{ row }">{{ (row as { wallet?: { availableBalance: string } }).wallet?.availableBalance }}</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</template>
|
||||
Reference in New Issue
Block a user