李冠威 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__':
# 在启动应用前初始化数据库
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
)
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
:: 检查Docker是否安装
docker --version > nul 2>&1
if %errorlevel% neq 0 (
echo 错误: 未检测到Docker. 请安装Docker后再运行此脚本.
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
REM Switch to frontend directory
cd frontend
echo Current directory: %cd%
REM Check if npm is installed
where npm >nul 2>nul
if %ERRORLEVEL% NEQ 0 (
echo Error: npm not found. Please install Node.js
:: 检查Docker Compose是否安装
docker compose version > nul 2>&1
if %errorlevel% neq 0 (
echo 错误: 未检测到Docker Compose. 请安装Docker Compose后再运行此脚本.
pause
goto menu
exit /b
)
REM Install dependencies
echo Installing frontend dependencies...
call npm install
call npm install vue-router@4 axios --save
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 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
:: 构建并启动容器
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
if "%1"=="" goto menu
exit /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 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
:: 获取端口号
set PORT=8080
for /f "tokens=1,2 delims==" %%a in (.env) do (
if "%%a"=="PORT" set PORT=%%b
)
:end
echo Thank you for using Gold Miner Game Launcher!
timeout /t 2 >nul
exit /b 0
echo ==== 黄金矿工游戏系统已成功部署! ====
echo 前端访问地址: http://localhost:%PORT%
echo 管理员初始账号: admin
echo 管理员初始密码: admin
echo.
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