李冠威 2 months ago
commit 11b8a4aed5

@ -0,0 +1,21 @@
.git
.github
.gitignore
.DS_Store
.env
.env.*
__pycache__
*.pyc
*.pyo
*.pyd
instance/
.pytest_cache
.coverage
htmlcov/
.vscode
*.log
logs/
*.db
tmp/
node_modules/
dist/

@ -0,0 +1,17 @@
# ????
FLASK_ENV=production
PORT=8080
# ?????
DB_HOST=mysql2.sqlpub.com
DB_PORT=3307
DB_USER=goldminer
DB_PASSWORD=nBAWq9DDwJ14Fugq
DB_NAME=goldminer
# ????
SECRET_KEY=dev_key_for_goldminer
ADMIN_SETUP_KEY=goldminer_admin_setup_key
# ??
TZ=Asia/Shanghai

@ -1,203 +1,124 @@
# 黄金矿工游戏
# 黄金矿工游戏系统
一个基于Vue和Flask的在线黄金矿工游戏支持实时排行榜和玩家互动功能
黄金矿工是一个基于Web的多人在线游戏系统使用Vue.js和Flask开发支持Docker一键部署
## 项目结构
## 项目特点
- `frontend/`: Vue.js前端代码
- `backend/`: Flask后端代码
- `docker-compose.yml`: Docker编排配置文件
- 经典黄金矿工游戏玩法
- 实时排行榜和聊天系统
- 用户账户系统
- 完整的管理员后台
- Docker化部署便于安装和维护
## 本地开发
## 系统架构
### 前端开发
- **前端**Vue.js、Socket.IO-client
- **后端**Flask、Flask-SocketIO、SQLAlchemy
- **数据库**MySQL
- **部署**Docker、Nginx
```bash
cd frontend
npm install
npm run serve
```
## 快速开始 (Docker)
前端服务将运行在 http://localhost:8080
### 环境要求
- Docker
- Docker Compose
### 后端开发
### 部署步骤
1. 克隆仓库
```bash
cd backend
pip install -r requirements.txt
python run_server.py
git clone https://github.com/yourusername/goldminer.git
cd goldminer
```
后端服务将运行在 http://localhost:5000
## Docker部署
2. 启动服务 (Linux/Mac)
```bash
chmod +x start.sh
./start.sh
```
项目已配置Docker支持可以使用Docker Compose进行一键部署。
Windows系统:
```
start.bat
```
### 前置条件
3. 访问游戏
- 打开浏览器访问: http://localhost:8080
- 管理员初始账号: admin
- 管理员初始密码: admin
- 安装 [Docker](https://www.docker.com/get-started)
- 安装 [Docker Compose](https://docs.docker.com/compose/install/)
### 环境变量配置
### 运行步骤
系统使用`.env`文件管理环境变量,首次运行会自动创建。主要配置项:
1. 在项目根目录创建`.env`文件,配置环境变量(可选,已提供默认值):
| 变量名 | 描述 | 默认值 |
|--------|------|--------|
| PORT | 前端服务端口 | 8080 |
| DB_HOST | 数据库主机名 | mysql2.sqlpub.com |
| DB_PORT | 数据库端口 | 3307 |
| DB_USER | 数据库用户名 | goldminer |
| DB_PASSWORD | 数据库密码 | nBAWq9DDwJ14Fugq |
| SECRET_KEY | Flask会话密钥 | dev_key_for_goldminer |
| ADMIN_SETUP_KEY | 管理员初始化密钥 | goldminer_admin_setup_key |
```
# 数据库配置
DB_HOST=mysql2.sqlpub.com
DB_PORT=3307
DB_USER=goldminer
DB_PASSWORD=nBAWq9DDwJ14Fugq
DB_NAME=goldminer
# 应用配置
SECRET_KEY=dev_key_for_goldminer
```
## 手动开发环境搭建
2. 使用Docker Compose构建和启动服务:
### 后端开发
1. 安装Python依赖
```bash
docker-compose up -d
cd backend
pip install -r requirements.txt
python run_server.py
```
3. 访问应用:
- 前端: http://localhost:8080
2. 后端服务将在 http://localhost:5000 运行
### 查看日志
### 前端开发
1. 安装依赖
```bash
# 查看所有服务日志
docker-compose logs
# 查看特定服务日志
docker-compose logs backend
docker-compose logs frontend
cd frontend
npm install
```
### 停止服务
2. 启动开发服务器
```bash
docker-compose down
npm run serve
```
3. 前端开发服务器将在 http://localhost:8080 运行
## 游戏功能
- 登录和注册用户
- 经典的黄金矿工游戏玩法
- 用户注册与登录
- 黄金矿工经典游戏玩法
- 实时排行榜
- 在线玩家状态显示
- 游戏历史记录
- 聊天室功能
## 技术栈
- 前端: Vue 3, Socket.io-client
- 后端: Flask, Flask-SocketIO
- 数据库: MySQL
- 容器化: Docker, Docker Compose
## 系统架构
下图展示了黄金矿工游戏的系统架构:
![系统架构图](https://mermaid.ink/img/eyJjb2RlIjoiZ3JhcGggVEQ7XG4gICAgQVtcIueUqOaIt+a1j-imvuWZqFwiXSAtLT58orWpl3wgQltcIlZ1ZSDliY3nq69cIl1cbiAgICBCIC0tPnxBUEkg6K-35rGCfCBDW1wiRmxhc2sg5ZCO56uvXCJdXG4gICAgQyAtLT586ZSZ5bqmfCBCXG4gICAgXG4gICAgc3ViZ3JhcGggXCLliY3nq68gbG9jYWxob3N0OjgwODBcIlxuICAgIEIgLS0-fOa4suafk3wgRFtcIkFwcC52dWVcIl1cbiAgICBEIC0tPnzljIXlkKt8IEVbXCJHYW1lLnZ1ZVwiXVxuICAgIEUgLS0-fOa4uOaIj-mAu-i-kXwgRltcIua4uOaIj-W-queOr1wiXVxuICAgIEYgLS0-fOabtOaWsHwgR1tcIkNhbnZhcyDmuLLmn5NcIl1cbiAgICBlbmRcbiAgICBcbiAgICBzdWJncmFwaCBcIuWQjuerryBsb2NhbGhvc3Q6NTAwMFwiXG4gICAgQyAtLT585bGP5oCn5paH5Lu26LWE5rqQfCBIW1wiaW5kZXguaHRtbFwiXVxuICAgIEMgLS0-fEFQSXwgSVtcIi9hcGkvaGVhbHRoXCJdXG4gICAgQyAtLT586ZO+5o6lIENsb3VkfCBKW1wiTXlTUUwg5LqR5pWw5o2u5bqTXCJdXG4gICAgZW5kIiwibWVybWFpZCI6e30sInVwZGF0ZUVkaXRvciI6ZmFsc2UsImF1dG9TeW5jIjp0cnVlLCJ1cGRhdGVEaWFncmFtIjpmYWxzZX0)
## 数据库配置
- 实时聊天室
- 管理后台
- 用户管理
- 排行榜管理
- 游戏历史管理
本项目使用云端MySQL数据库存储用户数据、游戏记录和聊天信息。
## 管理员功能
数据库配置信息:
- 数据库类型MySQL 8.4.3
- 服务提供商SQLPub.com
- 数据库名称goldminer
- 连接地址mysql2.sqlpub.com:3307
- 用户管理:查看、编辑、删除用户
- 排行榜管理:查看、重置排行榜
- 游戏历史管理:查看、删除游戏记录
## 游戏说明
## 生产环境部署
- 使用鼠标点击或空格键发射绳索
- 松开鼠标或空格键收回绳索
- 按P键暂停游戏
- 收集金块和钻石以获得分数
- 达到目标分数进入下一关
- 寻找特殊道具提升能力
## 特殊道具
- 速度提升:增加绳索伸缩速度
- 磁力钩:增加抓取范围
## 安装和运行
### 快速启动(推荐)
直接双击 `start.bat` 文件即可一键启动游戏。
这个脚本会自动:
1. 检查并安装所需的后端依赖
2. 连接到云端数据库并创建必要的表(如果不存在)
3. 同时启动后端和前端服务器
4. 自动在浏览器中打开游戏
### 手动启动
如果自动脚本出现问题,可以按照以下步骤手动启动:
#### 后端
1. 进入后端目录
```
cd goldminer/backend
```
2. 安装依赖
```
pip install -r requirements.txt
```
3. 运行后端服务器
```
python app.py
```
服务器将在 http://localhost:5000 运行。
#### 前端
1. 进入前端目录
```
cd goldminer/frontend
```
2. 安装依赖
```
npm install
```
3. 开发模式运行
```
npm run serve
1. 修改 `.env` 文件中的生产环境配置
2. 启动Docker容器
```bash
docker compose up -d
```
前端开发服务器将在 http://localhost:8080 运行。
## 游戏说明
- 使用鼠标点击或空格键发射绳索
- 松开鼠标或空格键收回绳索
- 按P键暂停游戏
- 收集金块和钻石以获得分数
- 达到目标分数进入下一关
- 寻找特殊道具提升能力
## 特殊道具
## 开发者
- 速度提升:增加绳索伸缩速度
- 磁力钩:增加抓取范围
- [Your Name]
## 技术栈
## 许可证
- 后端Flask, SQLAlchemy, PyMySQL
- 前端Vue 3
- 数据库MySQL 8.4.3 (云端)
- 通信Axios, Socket.IO
MIT

@ -2,6 +2,13 @@ FROM python:3.9-slim
WORKDIR /app
# 安装系统依赖
RUN apt-get update && apt-get install -y --no-install-recommends \
gcc \
python3-dev \
default-libmysqlclient-dev \
&& rm -rf /var/lib/apt/lists/*
# 复制依赖文件
COPY requirements.txt .
@ -14,6 +21,11 @@ COPY . .
# 设置环境变量
ENV PYTHONUNBUFFERED=1
ENV FLASK_APP=app.py
ENV FLASK_ENV=production
# 健康检查
HEALTHCHECK --interval=30s --timeout=30s --start-period=5s --retries=3 \
CMD curl -f http://localhost:5000/api/health || exit 1
# 暴露端口
EXPOSE 5000

@ -1,7 +1,11 @@
flask
flask-cors
flask-sqlalchemy
flask-socketio
werkzeug
sqlalchemy<2.0.0
pymysql
flask==2.2.3
flask-cors==3.0.10
flask-sqlalchemy==2.5.1
flask-socketio==5.3.2
werkzeug==2.2.3
sqlalchemy==1.4.46
pymysql==1.0.3
eventlet==0.33.3
gunicorn==20.1.0
python-dotenv==1.0.0
mysqlclient==2.1.1

@ -1,23 +1,41 @@
from app import app, socketio, init_db
import os
import logging
from dotenv import load_dotenv
from app import app, socketio, init_db
# 加载环境变量
load_dotenv()
# 配置日志
logging.basicConfig(level=logging.INFO)
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger(__name__)
if __name__ == '__main__':
# 在启动应用前初始化数据库
init_db()
try:
init_db()
logger.info("数据库初始化成功")
except Exception as e:
logger.error(f"数据库初始化失败: {e}")
# 继续执行,因为可能会在应用启动后自动重试
# 从环境变量获取主机和端口,如果没有则使用默认值
host = os.environ.get('HOST', '0.0.0.0')
port = int(os.environ.get('PORT', 5000))
debug = os.environ.get('FLASK_ENV') == 'development'
logger.info(f"环境: {os.environ.get('FLASK_ENV', 'production')}")
logger.info(f"启动服务器,监听 {host}:{port}")
logger.info(f"SECRET_KEY设置: {app.config['SECRET_KEY'][:5]}...")
logger.info(f"SESSION_COOKIE_SECURE: {app.config['SESSION_COOKIE_SECURE']}")
logger.info(f"CORS supports_credentials: True")
logger.info(f"SECRET_KEY设置: {app.config['SECRET_KEY'][:3]}..." if 'SECRET_KEY' in app.config else "SECRET_KEY未设置")
logger.info(f"SESSION_COOKIE_SECURE: {app.config.get('SESSION_COOKIE_SECURE', False)}")
logger.info(f"CORS supports_credentials: {app.config.get('CORS_SUPPORTS_CREDENTIALS', False)}")
# 启动应用,确保支持跨域会话
socketio.run(app, host=host, port=port, allow_unsafe_werkzeug=True)
if debug:
logger.info("使用开发模式启动")
socketio.run(app, host=host, port=port, debug=True)
else:
logger.info("使用生产模式启动")
socketio.run(app, host=host, port=port, debug=False)

@ -5,12 +5,23 @@ services:
container_name: goldminer-backend
restart: always
environment:
- FLASK_ENV=${FLASK_ENV:-production}
- DB_HOST=${DB_HOST:-mysql2.sqlpub.com}
- DB_PORT=${DB_PORT:-3307}
- DB_USER=${DB_USER:-goldminer}
- DB_PASSWORD=${DB_PASSWORD:-nBAWq9DDwJ14Fugq}
- DB_NAME=${DB_NAME:-goldminer}
- SECRET_KEY=${SECRET_KEY:-dev_key_for_goldminer}
- ADMIN_SETUP_KEY=${ADMIN_SETUP_KEY:-goldminer_admin_setup_key}
- TZ=Asia/Shanghai
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:5000/api/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 40s
volumes:
- backend_logs:/app/logs
networks:
- app-network
@ -25,10 +36,19 @@ services:
depends_on:
- backend
ports:
- "8080:80"
- "${PORT:-8080}:80"
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:80/"]
interval: 30s
timeout: 10s
retries: 3
start_period: 40s
networks:
- app-network
volumes:
backend_logs:
networks:
app-network:
driver: bridge

@ -5,22 +5,22 @@ WORKDIR /app
# 显示npm版本和node版本
RUN node -v && npm -v
# 复制依赖文件
COPY package*.json ./
# 首先只复制包管理文件以利用缓存
COPY package.json package-lock.json ./
# 安装Vue CLI和项目依赖
RUN npm install -g @vue/cli && npm install
# 安装项目依赖
RUN npm ci --quiet
# 复制所有前端源码
COPY . .
# 设置生产环境构建
ENV NODE_ENV=production
# 构建生产环境代码
RUN echo "Building with vue-cli-service..." && \
npx vue-cli-service build || \
./node_modules/.bin/vue-cli-service build || \
vue-cli-service build
RUN npm run build
# 确保dist目录存在
# 检查构建结果
RUN ls -la dist || exit 1
# 第二阶段使用nginx提供静态文件
@ -32,6 +32,11 @@ COPY --from=build-stage /app/dist /usr/share/nginx/html
# 复制自定义nginx配置
COPY nginx.conf /etc/nginx/conf.d/default.conf
# 健康检查
HEALTHCHECK --interval=30s --timeout=5s --start-period=5s --retries=3 \
CMD curl -f http://localhost:80/ || exit 1
# 暴露端口
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

@ -2,12 +2,27 @@ server {
listen 80;
server_name localhost;
# 安全设置
server_tokens off;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
root /usr/share/nginx/html;
index index.html;
# 设置文件缓存
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg)$ {
expires max;
add_header Cache-Control "public, max-age=31536000";
access_log off;
}
# 静态资源请求直接访问
location / {
try_files $uri $uri/ /index.html;
add_header Cache-Control "no-cache, no-store, must-revalidate";
}
# API请求代理到后端服务
@ -17,6 +32,9 @@ server {
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_read_timeout 90s;
proxy_connect_timeout 90s;
proxy_buffering off;
}
# Socket.IO请求代理到后端服务
@ -29,8 +47,15 @@ server {
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_cache_bypass $http_upgrade;
proxy_read_timeout 86400s; # 设置更长的读取超时适合WebSocket连接
proxy_buffering off;
}
# 压缩设置
gzip on;
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;
gzip_min_length 1000;
# 错误页面
error_page 404 /index.html;
error_page 500 502 503 504 /50x.html;

File diff suppressed because one or more lines are too long

@ -0,0 +1,816 @@
<template>
<div class="admin-container">
<h1>管理控制台</h1>
<div v-if="!isAdmin" class="admin-error">
<p>您没有权限访问此页面</p>
<router-link to="/" class="back-home">返回首页</router-link>
</div>
<div v-else class="admin-panel">
<div class="tabs">
<button
v-for="tab in tabs"
:key="tab.id"
@click="activeTab = tab.id"
:class="{ 'active': activeTab === tab.id }">
{{ tab.name }}
</button>
</div>
<!-- 用户管理 -->
<div v-if="activeTab === 'users'" class="tab-content">
<h2>用户管理</h2>
<div class="search-bar">
<input
type="text"
v-model="userSearchTerm"
placeholder="搜索用户..."
@input="filterUsers"
/>
<button @click="refreshUsers" class="refresh-btn">刷新</button>
</div>
<div class="table-wrapper">
<table class="users-table">
<thead>
<tr>
<th>ID</th>
<th>用户名</th>
<th>最高分</th>
<th>注册时间</th>
<th>最后登录</th>
<th>管理员</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<tr v-for="user in filteredUsers" :key="user.id">
<td>{{ user.id }}</td>
<td>{{ user.username }}</td>
<td>{{ user.high_score }}</td>
<td>{{ formatDate(user.created_at) }}</td>
<td>{{ formatDate(user.last_login) }}</td>
<td>
<input
type="checkbox"
:checked="user.is_admin"
@change="toggleAdminStatus(user)"
:disabled="currentUser && currentUser.id === user.id"
/>
</td>
<td class="actions">
<button @click="editUser(user)" class="edit-btn">编辑</button>
<button
@click="deleteUser(user)"
class="delete-btn"
:disabled="currentUser && currentUser.id === user.id">
删除
</button>
<button @click="resetUserScore(user.id)" class="reset-btn">
重置分数
</button>
</td>
</tr>
</tbody>
</table>
</div>
<!-- 用户编辑模态框 -->
<div v-if="showEditModal" class="modal-backdrop">
<div class="modal">
<h3>编辑用户</h3>
<form @submit.prevent="saveUserEdit">
<div class="form-group">
<label>用户名:</label>
<input type="text" v-model="editingUser.username" required />
</div>
<div class="form-group">
<label>最高分:</label>
<input type="number" v-model="editingUser.high_score" min="0" />
</div>
<div class="form-group">
<label>管理员权限:</label>
<input
type="checkbox"
v-model="editingUser.is_admin"
:disabled="currentUser && currentUser.id === editingUser.id"
/>
</div>
<div class="form-group">
<label>重设密码 (留空则不修改):</label>
<input type="password" v-model="editingUser.password" />
</div>
<div class="modal-actions">
<button type="submit" class="save-btn">保存</button>
<button type="button" @click="cancelEdit" class="cancel-btn">取消</button>
</div>
</form>
</div>
</div>
</div>
<!-- 排行榜管理 -->
<div v-if="activeTab === 'leaderboard'" class="tab-content">
<h2>排行榜管理</h2>
<div class="leaderboard-actions">
<button @click="confirmResetAllScores" class="danger-btn">
重置所有分数
</button>
</div>
<div class="table-wrapper">
<table class="leaderboard-table">
<thead>
<tr>
<th>排名</th>
<th>用户名</th>
<th>最高分</th>
<th>最后游戏</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<tr v-for="(user, index) in leaderboard" :key="user.id">
<td>{{ index + 1 }}</td>
<td>{{ user.username }}</td>
<td>{{ user.high_score }}</td>
<td>{{ formatDate(user.last_login) }}</td>
<td class="actions">
<button @click="editUser(user)" class="edit-btn">编辑</button>
<button @click="resetUserScore(user.id)" class="reset-btn">
重置分数
</button>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<!-- 游戏历史管理 -->
<div v-if="activeTab === 'history'" class="tab-content">
<h2>游戏历史管理</h2>
<div class="search-bar">
<input
type="text"
v-model="historySearchTerm"
placeholder="搜索用户..."
@input="filterGameHistory"
/>
<button @click="loadGameHistory" class="refresh-btn">刷新</button>
</div>
<div class="table-wrapper">
<table class="history-table">
<thead>
<tr>
<th>ID</th>
<th>用户</th>
<th>分数</th>
<th>等级</th>
<th>时长()</th>
<th>金币</th>
<th>游戏时间</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<tr v-for="record in filteredGameHistory" :key="record.id">
<td>{{ record.id }}</td>
<td>{{ record.username }}</td>
<td>{{ record.score }}</td>
<td>{{ record.level_reached }}</td>
<td>{{ record.duration }}</td>
<td>{{ record.gold_earned }}</td>
<td>{{ formatDate(record.created_at) }}</td>
<td class="actions">
<button @click="deleteGameRecord(record.id)" class="delete-btn">
删除
</button>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<!-- 确认对话框 -->
<div v-if="showConfirmDialog" class="modal-backdrop">
<div class="modal confirm-dialog">
<h3>{{ confirmDialogTitle }}</h3>
<p>{{ confirmDialogMessage }}</p>
<div class="modal-actions">
<button @click="confirmDialogAction" class="danger-btn">确认</button>
<button @click="cancelConfirmDialog" class="cancel-btn">取消</button>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
import axios from 'axios'
export default {
name: 'AdminPanel',
data() {
return {
isAdmin: false,
currentUser: null,
activeTab: 'users',
tabs: [
{ id: 'users', name: '用户管理' },
{ id: 'leaderboard', name: '排行榜管理' },
{ id: 'history', name: '游戏历史' }
],
//
users: [],
filteredUsers: [],
userSearchTerm: '',
//
leaderboard: [],
//
gameHistory: [],
filteredGameHistory: [],
historySearchTerm: '',
//
showEditModal: false,
editingUser: {
id: null,
username: '',
high_score: 0,
is_admin: false,
password: ''
},
//
showConfirmDialog: false,
confirmDialogTitle: '',
confirmDialogMessage: '',
confirmDialogAction: () => {}
}
},
created() {
this.checkAdminStatus()
.then(() => {
if (this.isAdmin) {
this.loadData()
}
})
},
methods: {
async checkAdminStatus() {
try {
const userData = JSON.parse(localStorage.getItem('user'))
const token = localStorage.getItem('auth_token')
console.log('检查管理员状态,当前用户数据:', userData)
console.log('当前令牌:', token ? token.substring(0, 8) + '...' : 'no token')
if (!userData) {
this.isAdmin = false
console.log('未登录,跳转到登录页面')
this.$router.push('/login')
return Promise.resolve(false)
}
//
this.isAdmin = userData.is_admin === true // true
console.log('本地存储显示用户管理员状态:', this.isAdmin)
//
console.log('开始向服务器验证管理员状态')
const response = await axios.get('/api/test/admin_status', {
withCredentials: true,
headers: {
'X-Requested-With': 'XMLHttpRequest',
'Authorization': token ? `Bearer ${token}` : ''
}
})
console.log('服务器返回的验证数据:', response.data)
if (response.data.logged_in && response.data.is_admin === true) {
console.log('服务器验证结果: 是管理员,允许访问管理页面')
this.isAdmin = true
this.currentUser = response.data.user_data
// localStorageis_admin
localStorage.setItem('user', JSON.stringify(response.data.user_data))
return Promise.resolve(true)
} else {
console.log('服务器验证结果: 不是管理员或未登录,跳转回首页')
this.isAdmin = false
//
if (!response.data.logged_in && userData) {
console.log('服务器会话可能已过期,尝试重新登录...')
this.$router.push('/login')
} else {
this.$router.push('/')
}
return Promise.resolve(false)
}
} catch (error) {
console.error('验证管理员状态失败:', error)
this.isAdmin = false
this.$router.push('/')
return Promise.resolve(false)
}
},
async loadData() {
if (!this.isAdmin) return
await Promise.all([
this.loadUsers(),
this.loadLeaderboard(),
this.loadGameHistory()
])
},
//
async loadUsers() {
try {
const response = await axios.get('/api/admin/users')
this.users = response.data.users
this.filterUsers()
} catch (error) {
console.error('加载用户列表失败:', error)
}
},
filterUsers() {
if (!this.userSearchTerm) {
this.filteredUsers = [...this.users]
return
}
const term = this.userSearchTerm.toLowerCase()
this.filteredUsers = this.users.filter(user =>
user.username.toLowerCase().includes(term) ||
user.id.toString().includes(term)
)
},
refreshUsers() {
this.loadUsers()
},
//
async loadLeaderboard() {
try {
const response = await axios.get('/api/leaderboard')
this.leaderboard = response.data.leaderboard
} catch (error) {
console.error('加载排行榜失败:', error)
}
},
async confirmResetAllScores() {
this.showConfirmDialog = true
this.confirmDialogTitle = '重置所有分数'
this.confirmDialogMessage = '确定要重置所有用户的分数吗?此操作不可撤销!'
this.confirmDialogAction = this.resetAllScores
},
async resetAllScores() {
try {
await axios.post('/api/admin/leaderboard/reset')
this.showConfirmDialog = false
//
await this.loadUsers()
await this.loadLeaderboard()
await this.loadGameHistory()
alert('所有分数已重置')
} catch (error) {
console.error('重置分数失败:', error)
alert('重置分数失败: ' + (error.response?.data?.error || '未知错误'))
}
},
async resetUserScore(userId) {
this.showConfirmDialog = true
this.confirmDialogTitle = '重置用户分数'
this.confirmDialogMessage = '确定要重置此用户的分数吗?此操作不可撤销!'
this.confirmDialogAction = async () => {
try {
await axios.post('/api/admin/leaderboard/reset', { user_id: userId })
this.showConfirmDialog = false
//
await this.loadUsers()
await this.loadLeaderboard()
await this.loadGameHistory()
alert('用户分数已重置')
} catch (error) {
console.error('重置分数失败:', error)
alert('重置分数失败: ' + (error.response?.data?.error || '未知错误'))
}
}
},
//
async loadGameHistory() {
try {
const response = await axios.get('/api/admin/game_history')
this.gameHistory = response.data.history
this.filterGameHistory()
} catch (error) {
console.error('加载游戏历史失败:', error)
}
},
filterGameHistory() {
if (!this.historySearchTerm) {
this.filteredGameHistory = [...this.gameHistory]
return
}
const term = this.historySearchTerm.toLowerCase()
this.filteredGameHistory = this.gameHistory.filter(record =>
record.username.toLowerCase().includes(term) ||
record.id.toString().includes(term)
)
},
async deleteGameRecord(recordId) {
this.showConfirmDialog = true
this.confirmDialogTitle = '删除游戏记录'
this.confirmDialogMessage = '确定要删除此游戏记录吗?此操作不可撤销!'
this.confirmDialogAction = async () => {
try {
await axios.delete(`/api/admin/game_history/${recordId}`)
this.showConfirmDialog = false
await this.loadGameHistory()
alert('记录已删除')
} catch (error) {
console.error('删除记录失败:', error)
alert('删除记录失败: ' + (error.response?.data?.error || '未知错误'))
}
}
},
//
editUser(user) {
this.editingUser = {
id: user.id,
username: user.username,
high_score: user.high_score,
is_admin: user.is_admin,
password: '' //
}
this.showEditModal = true
},
async saveUserEdit() {
try {
const payload = {
username: this.editingUser.username,
high_score: parseInt(this.editingUser.high_score),
is_admin: this.editingUser.is_admin
}
//
if (this.editingUser.password) {
payload.password = this.editingUser.password
}
await axios.put(`/api/admin/users/${this.editingUser.id}`, payload)
//
this.showEditModal = false
//
await this.loadUsers()
await this.loadLeaderboard()
// localStorage
const userData = JSON.parse(localStorage.getItem('user'))
if (userData && userData.id === this.editingUser.id) {
userData.username = this.editingUser.username
userData.high_score = this.editingUser.high_score
userData.is_admin = this.editingUser.is_admin
localStorage.setItem('user', JSON.stringify(userData))
}
alert('用户信息已更新')
} catch (error) {
console.error('更新用户失败:', error)
alert('更新用户失败: ' + (error.response?.data?.error || '未知错误'))
}
},
cancelEdit() {
this.showEditModal = false
this.editingUser = {
id: null,
username: '',
high_score: 0,
is_admin: false,
password: ''
}
},
async toggleAdminStatus(user) {
//
if (this.currentUser && this.currentUser.id === user.id) {
return
}
try {
await axios.put(`/api/admin/users/${user.id}`, {
is_admin: !user.is_admin
})
//
await this.loadUsers()
} catch (error) {
console.error('更新管理员状态失败:', error)
alert('更新管理员状态失败: ' + (error.response?.data?.error || '未知错误'))
}
},
async deleteUser(user) {
//
if (this.currentUser && this.currentUser.id === user.id) {
return
}
this.showConfirmDialog = true
this.confirmDialogTitle = '删除用户'
this.confirmDialogMessage = `确定要删除用户 "${user.username}" 吗?此操作将永久删除该用户及其所有数据,且不可撤销!`
this.confirmDialogAction = async () => {
try {
await axios.delete(`/api/admin/users/${user.id}`)
this.showConfirmDialog = false
//
await this.loadUsers()
await this.loadLeaderboard()
await this.loadGameHistory()
alert('用户已删除')
} catch (error) {
console.error('删除用户失败:', error)
alert('删除用户失败: ' + (error.response?.data?.error || '未知错误'))
}
}
},
//
formatDate(dateString) {
if (!dateString) return '未知'
const date = new Date(dateString)
return date.toLocaleDateString('zh-CN', {
year: 'numeric',
month: 'numeric',
day: 'numeric',
hour: '2-digit',
minute: '2-digit'
})
},
cancelConfirmDialog() {
this.showConfirmDialog = false
this.confirmDialogTitle = ''
this.confirmDialogMessage = ''
this.confirmDialogAction = () => {}
}
}
}
</script>
<style scoped>
.admin-container {
max-width: 1200px;
margin: 0 auto;
padding: 20px;
}
h1 {
color: #8B4513;
text-align: center;
margin-bottom: 30px;
}
.admin-error {
text-align: center;
padding: 50px 0;
}
.back-home {
display: inline-block;
margin-top: 20px;
padding: 10px 20px;
background-color: #8B4513;
color: white;
text-decoration: none;
border-radius: 4px;
}
.tabs {
display: flex;
border-bottom: 1px solid #ddd;
margin-bottom: 20px;
}
.tabs button {
padding: 10px 20px;
background: none;
border: none;
cursor: pointer;
font-size: 16px;
color: #555;
}
.tabs button.active {
color: #8B4513;
border-bottom: 3px solid #8B4513;
font-weight: bold;
}
.tab-content {
background: white;
padding: 20px;
border-radius: 4px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.tab-content h2 {
color: #8B4513;
margin-top: 0;
margin-bottom: 20px;
}
/* 表格样式 */
.table-wrapper {
overflow-x: auto;
margin-bottom: 20px;
}
table {
width: 100%;
border-collapse: collapse;
}
th, td {
padding: 10px;
text-align: left;
border-bottom: 1px solid #ddd;
}
th {
background-color: #f5f5f5;
color: #333;
}
tr:hover {
background-color: #f9f9f9;
}
/* 按钮样式 */
.actions {
display: flex;
gap: 5px;
}
.edit-btn, .delete-btn, .reset-btn, .refresh-btn {
padding: 5px 8px;
border: none;
border-radius: 3px;
font-size: 12px;
cursor: pointer;
}
.edit-btn {
background-color: #4CAF50;
color: white;
}
.delete-btn {
background-color: #f44336;
color: white;
}
.reset-btn {
background-color: #ff9800;
color: white;
}
.refresh-btn {
background-color: #2196F3;
color: white;
margin-left: 10px;
}
button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
/* 搜索栏 */
.search-bar {
display: flex;
margin-bottom: 20px;
}
.search-bar input {
flex-grow: 1;
padding: 8px;
border: 1px solid #ddd;
border-radius: 4px;
}
/* 模态框 */
.modal-backdrop {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
}
.modal {
background: white;
padding: 20px;
border-radius: 5px;
width: 400px;
max-width: 90%;
}
.modal h3 {
margin-top: 0;
color: #8B4513;
}
.form-group {
margin-bottom: 15px;
}
.form-group label {
display: block;
margin-bottom: 5px;
font-weight: bold;
}
.form-group input[type="text"],
.form-group input[type="number"],
.form-group input[type="password"] {
width: 100%;
padding: 8px;
border: 1px solid #ddd;
border-radius: 4px;
}
.modal-actions {
display: flex;
justify-content: flex-end;
gap: 10px;
margin-top: 20px;
}
.save-btn {
background-color: #4CAF50;
color: white;
border: none;
padding: 8px 16px;
border-radius: 4px;
cursor: pointer;
}
.cancel-btn {
background-color: #9e9e9e;
color: white;
border: none;
padding: 8px 16px;
border-radius: 4px;
cursor: pointer;
}
.danger-btn {
background-color: #f44336;
color: white;
border: none;
padding: 8px 16px;
border-radius: 4px;
cursor: pointer;
}
/* 确认对话框 */
.confirm-dialog {
width: 350px;
text-align: center;
}
.confirm-dialog p {
margin: 15px 0;
}
/* 排行榜操作 */
.leaderboard-actions {
margin-bottom: 20px;
}
</style>

@ -0,0 +1,226 @@
<template>
<div class="admin-setup-container">
<div class="setup-form">
<h1>管理员初始化</h1>
<p class="description">请设置系统的第一个管理员账户此页面只能被访问一次</p>
<div v-if="error" class="error-message">
{{ error }}
</div>
<div v-if="success" class="success-message">
{{ success }}
<div class="redirect-info">
即将跳转到登录页面... <span>{{ countdown }}</span>
</div>
</div>
<form @submit.prevent="setupAdmin" v-if="!success">
<div class="form-group">
<label for="username">用户名</label>
<input
type="text"
id="username"
v-model="username"
required
:disabled="loading"
placeholder="请输入用户名"
/>
</div>
<div class="form-group">
<label for="password">密码</label>
<input
type="password"
id="password"
v-model="password"
required
:disabled="loading"
placeholder="请输入密码"
/>
</div>
<div class="form-group">
<label for="setupKey">安装密钥</label>
<input
type="password"
id="setupKey"
v-model="setupKey"
required
:disabled="loading"
placeholder="请输入安装密钥"
/>
<small>此密钥用于确认您有权设置管理员账户</small>
</div>
<div class="form-actions">
<button type="submit" :disabled="loading">
{{ loading ? '处理中...' : '创建管理员账户' }}
</button>
</div>
</form>
</div>
</div>
</template>
<script>
import axios from 'axios'
export default {
name: 'AdminSetup',
data() {
return {
username: '',
password: '',
setupKey: '',
loading: false,
error: '',
success: '',
countdown: 5
}
},
methods: {
async setupAdmin() {
this.loading = true
this.error = ''
try {
const response = await axios.post('/api/admin/setup', {
username: this.username,
password: this.password,
setup_key: this.setupKey
})
//
this.success = response.data.message
//
this.startRedirectCountdown()
} catch (error) {
console.error('设置管理员失败:', error)
this.error = error.response?.data?.error || '设置管理员失败,请稍后重试'
} finally {
this.loading = false
}
},
startRedirectCountdown() {
const timer = setInterval(() => {
this.countdown--
if (this.countdown <= 0) {
clearInterval(timer)
this.$router.push('/login')
}
}, 1000)
}
}
}
</script>
<style scoped>
.admin-setup-container {
display: flex;
justify-content: center;
align-items: center;
min-height: 80vh;
padding: 20px;
}
.setup-form {
background-color: white;
padding: 30px;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
width: 100%;
max-width: 500px;
}
h1 {
color: #8B4513;
text-align: center;
margin-bottom: 10px;
}
.description {
text-align: center;
color: #666;
margin-bottom: 30px;
}
.form-group {
margin-bottom: 20px;
}
label {
display: block;
margin-bottom: 8px;
font-weight: bold;
color: #333;
}
input {
width: 100%;
padding: 10px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 16px;
}
small {
display: block;
margin-top: 5px;
color: #777;
font-size: 12px;
}
.form-actions {
text-align: center;
margin-top: 30px;
}
button {
background-color: #8B4513;
color: white;
border: none;
padding: 12px 24px;
border-radius: 4px;
font-size: 16px;
cursor: pointer;
transition: background-color 0.2s;
}
button:hover {
background-color: #6d370f;
}
button:disabled {
background-color: #ccc;
cursor: not-allowed;
}
.error-message {
background-color: #ffebee;
color: #c62828;
padding: 10px;
border-radius: 4px;
margin-bottom: 20px;
}
.success-message {
background-color: #e8f5e9;
color: #2e7d32;
padding: 15px;
border-radius: 4px;
margin-bottom: 20px;
text-align: center;
}
.redirect-info {
margin-top: 15px;
font-size: 14px;
}
.redirect-info span {
font-weight: bold;
}
</style>

@ -2,9 +2,6 @@
<div class="login-container">
<div class="auth-form">
<h2>登录</h2>
<div class="admin-tip">
提示可使用管理员账户用户名: admin, 密码: admin
</div>
<div class="form-group">
<label for="username">用户名</label>
<input

@ -1,142 +1,81 @@
@echo off
setlocal enabledelayedexpansion
title Gold Miner Game Launcher
REM Process command line arguments
if "%1"=="1" goto backend_direct
if "%1"=="2" goto frontend_direct
echo ==== 黄金矿工游戏系统部署脚本 ====
echo 正在检查环境...
:menu
cls
echo ===================================
echo Gold Miner Game Launcher
echo ===================================
echo.
echo Current computer IP address:
ipconfig | find "IPv4"
echo.
echo Please note the IP address shown above. Other computers can access the game using this address.
echo Other computers should use http://[your-IP-address]:8080 to access the game.
echo.
echo Please select an option:
echo [1] Start Backend Server
echo [2] Start Frontend Server
echo [3] Start Both Frontend and Backend (Two Windows)
echo [4] Exit
echo.
set /p choice=Enter your choice (1-4):
if "%choice%"=="1" goto backend
if "%choice%"=="2" goto frontend
if "%choice%"=="3" goto both
if "%choice%"=="4" goto end
echo Invalid option, please try again.
timeout /t 2 >nul
goto menu
:backend_direct
REM Start backend directly from command line
title Gold Miner - Backend Server
goto backend_start
:backend
cls
echo ===================================
echo Starting Backend Server
echo ===================================
echo.
:backend_start
cd /d %~dp0
REM Activate virtual environment if it exists
if exist "..\..\.venv\Scripts\activate.bat" (
call "..\..\.venv\Scripts\activate.bat"
echo Virtual environment activated
:: 检查Docker是否安装
docker --version > nul 2>&1
if %errorlevel% neq 0 (
echo 错误: 未检测到Docker. 请安装Docker后再运行此脚本.
pause
exit /b
)
REM Check backend dependencies
echo Installing backend dependencies...
cd backend
python -m pip install -r requirements.txt
REM Run the backend server with host=0.0.0.0
echo ===================================
echo Starting Backend Server...
echo Backend will listen on all network interfaces (0.0.0.0:5000)
echo Connecting to cloud database: mysql2.sqlpub.com:3307
echo ===================================
python app.py
pause
if "%1"=="" goto menu
exit /b
:frontend_direct
REM Start frontend directly from command line
title Gold Miner - Frontend Server
goto frontend_start
:frontend
cls
echo ===================================
echo Starting Frontend Server
echo ===================================
echo.
:frontend_start
cd /d %~dp0
:: 检查Docker Compose是否安装
docker compose version > nul 2>&1
if %errorlevel% neq 0 (
echo 错误: 未检测到Docker Compose. 请安装Docker Compose后再运行此脚本.
pause
exit /b
)
REM Switch to frontend directory
cd frontend
echo Current directory: %cd%
echo Docker环境检查通过!
:: 检查.env文件是否存在不存在则从示例文件创建
if not exist ".env" (
echo 未发现.env文件, 从示例创建...
if exist ".env.example" (
copy .env.example .env
echo 已创建.env文件. 请检查并根据需要修改配置.
) else (
echo 错误: 未找到.env.example文件. 正在创建基本配置...
echo # 应用配置> .env
echo FLASK_ENV=production>> .env
echo PORT=8080>> .env
echo.>> .env
echo # 数据库配置>> .env
echo DB_HOST=mysql2.sqlpub.com>> .env
echo DB_PORT=3307>> .env
echo DB_USER=goldminer>> .env
echo DB_PASSWORD=nBAWq9DDwJ14Fugq>> .env
echo DB_NAME=goldminer>> .env
echo.>> .env
echo # 安全配置>> .env
echo SECRET_KEY=dev_key_for_goldminer>> .env
echo ADMIN_SETUP_KEY=goldminer_admin_setup_key>> .env
echo.>> .env
echo # 时区>> .env
echo TZ=Asia/Shanghai>> .env
)
)
REM Check if npm is installed
where npm >nul 2>nul
if %ERRORLEVEL% NEQ 0 (
echo Error: npm not found. Please install Node.js
:: 构建并启动容器
echo 正在构建并启动容器...
docker compose down
docker compose build --no-cache
docker compose up -d
:: 检查容器是否成功启动
timeout /t 5 /nobreak > nul
docker compose ps | find "goldminer-backend" > nul
if %errorlevel% neq 0 (
echo 错误: 容器未能成功启动. 请查看日志排查问题:
docker compose logs
pause
goto menu
exit /b
)
REM Install dependencies
echo Installing frontend dependencies...
call npm install
call npm install vue-router@4 axios --save
REM Run the frontend server with host=0.0.0.0
echo ===================================
echo Starting Frontend Server...
echo Frontend will listen on all network interfaces (0.0.0.0:8080)
echo ===================================
echo Local access URL: http://localhost:8080
echo LAN access URL: http://[your-IP-address]:8080
echo ===================================
call npm run serve -- --host 0.0.0.0
pause
if "%1"=="" goto menu
exit /b
:: 获取端口号
set PORT=8080
for /f "tokens=1,2 delims==" %%a in (.env) do (
if "%%a"=="PORT" set PORT=%%b
)
:both
cls
echo ===================================
echo Starting Both Frontend and Backend Servers
echo ===================================
echo.
echo Two windows will open to run frontend and backend servers separately.
echo Do not close either window until you want to stop the game.
echo ==== 黄金矿工游戏系统已成功部署! ====
echo 前端访问地址: http://localhost:%PORT%
echo 管理员初始账号: admin
echo 管理员初始密码: admin
echo.
echo Press any key to continue...
pause >nul
REM Start backend in a new window
start cmd /k "%~f0" 1
REM Start frontend in a new window
start cmd /k "%~f0" 2
goto menu
:end
echo Thank you for using Gold Miner Game Launcher!
timeout /t 2 >nul
exit /b 0
echo 提示: 按任意键退出,系统将继续在后台运行
pause

@ -0,0 +1,55 @@
#!/bin/bash
# 设置颜色变量
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
RED='\033[0;31m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
echo -e "${GREEN}==== 黄金矿工游戏系统部署脚本 ====${NC}"
echo -e "${YELLOW}正在检查环境...${NC}"
# 检查Docker是否安装
if ! [ -x "$(command -v docker)" ]; then
echo -e "${RED}错误: 未检测到Docker. 请安装Docker后再运行此脚本.${NC}" >&2
exit 1
fi
# 检查Docker Compose是否安装
if ! [ -x "$(command -v docker compose)" ]; then
echo -e "${RED}错误: 未检测到Docker Compose. 请安装Docker Compose后再运行此脚本.${NC}" >&2
exit 1
fi
echo -e "${GREEN}Docker环境检查通过!${NC}"
# 检查.env文件是否存在不存在则从示例文件创建
if [ ! -f ".env" ]; then
echo -e "${YELLOW}未发现.env文件, 从示例创建...${NC}"
if [ -f ".env.example" ]; then
cp .env.example .env
echo -e "${BLUE}已创建.env文件. 请检查并根据需要修改配置.${NC}"
else
echo -e "${RED}错误: 未找到.env.example文件. 无法创建配置.${NC}" >&2
exit 1
fi
fi
# 构建并启动容器
echo -e "${GREEN}正在构建并启动容器...${NC}"
docker compose down
docker compose build --no-cache
docker compose up -d
# 检查容器是否成功启动
sleep 5
if [ "$(docker compose ps -q | wc -l)" -eq 2 ]; then
echo -e "${GREEN}==== 黄金矿工游戏系统已成功部署! ====${NC}"
echo -e "${BLUE}前端访问地址: http://localhost:$(grep PORT .env | cut -d= -f2 || echo 8080)${NC}"
echo -e "${BLUE}管理员初始账号: admin${NC}"
echo -e "${BLUE}管理员初始密码: admin${NC}"
else
echo -e "${RED}错误: 容器未能成功启动. 请查看日志排查问题:${NC}"
docker compose logs
fi
Loading…
Cancel
Save