修复页面搜索功能无法使用
This commit is contained in:
parent
8d8c8a9aac
commit
95461a09ab
52
DOCUMENTATION_GUIDE.md
Normal file
52
DOCUMENTATION_GUIDE.md
Normal file
@ -0,0 +1,52 @@
|
||||
# 项目文档整理指南
|
||||
|
||||
本文档介绍了KaMiXiTong软件授权管理系统中各个文档的作用和使用方法,帮助用户快速找到所需的文档。
|
||||
|
||||
## 文档分类
|
||||
|
||||
### 1. 入门指南
|
||||
- [README.md](README.md) - 项目简介和快速开始指南
|
||||
- [PROJECT_SUMMARY.md](PROJECT_SUMMARY.md) - 项目详细总结和架构说明
|
||||
|
||||
### 2. 部署文档
|
||||
- [DEPLOYMENT.md](DEPLOYMENT.md) - MySQL数据库部署详细指南
|
||||
- [MYSQL_CONFIG_GUIDE.md](MYSQL_CONFIG_GUIDE.md) - MySQL配置与初始化指南
|
||||
|
||||
### 3. 集成文档
|
||||
- [docs/INTEGRATION.md](docs/INTEGRATION.md) - 软件授权系统集成指南
|
||||
- [docs/EXAMPLES.md](docs/EXAMPLES.md) - 使用示例和代码模板
|
||||
|
||||
### 4. API文档
|
||||
- [docs/FASTAPI.md](docs/FASTAPI.md) - FastAPI接口配置文档
|
||||
- [docs/DEPLOYMENT.md](docs/DEPLOYMENT.md) - API部署说明
|
||||
|
||||
## 文档使用建议
|
||||
|
||||
### 新用户入门
|
||||
1. 首先阅读 [README.md](README.md) 了解项目基本功能
|
||||
2. 查看 [PROJECT_SUMMARY.md](PROJECT_SUMMARY.md) 了解系统架构和技术栈
|
||||
3. 根据需要选择部署方式,参考相应的部署文档
|
||||
|
||||
### 开发者集成
|
||||
1. 阅读 [docs/INTEGRATION.md](docs/INTEGRATION.md) 了解如何将授权验证器集成到您的软件中
|
||||
2. 参考 [docs/EXAMPLES.md](docs/EXAMPLES.md) 中的代码示例进行实际开发
|
||||
3. 如需使用FastAPI接口,查看 [docs/FASTAPI.md](docs/FASTAPI.md)
|
||||
|
||||
### 系统管理员
|
||||
1. 根据数据库类型选择相应的部署文档:
|
||||
- MySQL数据库:[DEPLOYMENT.md](DEPLOYMENT.md) 和 [MYSQL_CONFIG_GUIDE.md](MYSQL_CONFIG_GUIDE.md)
|
||||
2. 参考 [docs/DEPLOYMENT.md](docs/DEPLOYMENT.md) 进行生产环境部署
|
||||
|
||||
## 已删除文档说明
|
||||
|
||||
以下文档已被删除,因为它们是过时的或与当前系统不相关:
|
||||
|
||||
- `DEPLOYMENT_GUIDE.md` - 账号管理系统重构部署指南(已过时)
|
||||
- `SERVER_DEPLOYMENT_GUIDE.md` - 服务器部署指南(内容已整合到其他文档中)
|
||||
- `REFACTOR_NOTES.md` - 账号管理系统重构说明(已过时)
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. 所有文档均保持最新状态,与当前代码版本兼容
|
||||
2. 如在使用过程中发现问题,请及时反馈
|
||||
3. 建议收藏本文档,方便日后查阅
|
||||
203
app/web/templates/license/export.html
Normal file
203
app/web/templates/license/export.html
Normal file
@ -0,0 +1,203 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}导出卡密 - 软件授权管理系统{% endblock %}
|
||||
|
||||
{% block page_title %}导出卡密{% endblock %}
|
||||
|
||||
{% block page_actions %}
|
||||
<a href="{{ url_for('web.licenses') }}" class="btn btn-outline-secondary">
|
||||
<i class="fas fa-arrow-left me-2"></i>
|
||||
返回列表
|
||||
</a>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="col-lg-8">
|
||||
<div class="card shadow">
|
||||
<div class="card-body">
|
||||
<form id="export-form">
|
||||
<div class="mb-3">
|
||||
<label for="product_id" class="form-label">选择产品</label>
|
||||
<select class="form-select" id="product_id" name="product_id">
|
||||
<option value="">全部产品</option>
|
||||
{% for product in products %}
|
||||
<option value="{{ product.product_id }}">{{ product.product_name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="status" class="form-label">卡密状态</label>
|
||||
<select class="form-select" id="status" name="status">
|
||||
<option value="">全部状态</option>
|
||||
<option value="0">未激活</option>
|
||||
<option value="1">已激活</option>
|
||||
<option value="2">已过期</option>
|
||||
<option value="3">已禁用</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="type" class="form-label">卡密类型</label>
|
||||
<select class="form-select" id="type" name="type">
|
||||
<option value="">全部类型</option>
|
||||
<option value="1">正式卡密</option>
|
||||
<option value="0">试用卡密</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="format" class="form-label">导出格式 *</label>
|
||||
<select class="form-select" id="format" name="format" required>
|
||||
<option value="excel">Excel (.xlsx)</option>
|
||||
<option value="csv">CSV (.csv)</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-success" id="submit-btn">
|
||||
<i class="fas fa-file-export me-2"></i>
|
||||
<span id="submit-text">导出卡密</span>
|
||||
</button>
|
||||
<a href="{{ url_for('web.licenses') }}" class="btn btn-secondary">取消</a>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-lg-4">
|
||||
<div class="card shadow">
|
||||
<div class="card-header">
|
||||
<h6 class="mb-0">导出说明</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<ul class="list-unstyled">
|
||||
<li class="mb-2">
|
||||
<i class="fas fa-info-circle text-primary me-2"></i>
|
||||
<small>可以选择特定产品、状态和类型的卡密进行导出</small>
|
||||
</li>
|
||||
<li class="mb-2">
|
||||
<i class="fas fa-info-circle text-primary me-2"></i>
|
||||
<small>导出格式支持Excel和CSV两种</small>
|
||||
</li>
|
||||
<li class="mb-2">
|
||||
<i class="fas fa-info-circle text-primary me-2"></i>
|
||||
<small>导出的文件包含卡密、产品、状态、有效期等详细信息</small>
|
||||
</li>
|
||||
<li class="mb-2">
|
||||
<i class="fas fa-info-circle text-primary me-2"></i>
|
||||
<small>导出操作可能需要一些时间,请耐心等待</small>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_js %}
|
||||
<script>
|
||||
// 页面加载完成后初始化
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
initEventListeners();
|
||||
});
|
||||
|
||||
// 初始化事件监听器
|
||||
function initEventListeners() {
|
||||
// 表单提交
|
||||
document.getElementById('export-form').addEventListener('submit', function(e) {
|
||||
e.preventDefault();
|
||||
exportLicenses();
|
||||
});
|
||||
}
|
||||
|
||||
// 导出卡密
|
||||
function exportLicenses() {
|
||||
const submitBtn = document.getElementById('submit-btn');
|
||||
const submitText = document.getElementById('submit-text');
|
||||
|
||||
// 获取表单数据
|
||||
const formData = {
|
||||
product_id: document.getElementById('product_id').value || null,
|
||||
status: document.getElementById('status').value ? parseInt(document.getElementById('status').value) : null,
|
||||
type: document.getElementById('type').value ? parseInt(document.getElementById('type').value) : null,
|
||||
format: document.getElementById('format').value
|
||||
};
|
||||
|
||||
// 显示加载状态
|
||||
submitBtn.disabled = true;
|
||||
submitText.textContent = '导出中...';
|
||||
|
||||
// 直接使用fetch API而不是apiRequest函数,因为我们需要处理文件下载
|
||||
const fullUrl = (window.FRONTEND_DOMAIN || window.location.origin) + '/api/v1/licenses/export';
|
||||
|
||||
fetch(fullUrl, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
credentials: 'same-origin',
|
||||
body: JSON.stringify(formData)
|
||||
})
|
||||
.then(response => {
|
||||
// 隐藏加载动画
|
||||
hideLoading();
|
||||
|
||||
if (!response.ok) {
|
||||
// 处理错误响应
|
||||
if (response.status === 401) {
|
||||
showNotification('会话已过期,请重新登录', 'warning');
|
||||
setTimeout(() => {
|
||||
window.location.href = '/login';
|
||||
}, 1500);
|
||||
throw new Error('未授权访问');
|
||||
} else if (response.status === 403) {
|
||||
return response.json().then(errorData => {
|
||||
showNotification(errorData.message || '权限不足,无法执行此操作', 'error');
|
||||
throw new Error(`403: ${errorData.message || '权限不足'}`);
|
||||
});
|
||||
} else {
|
||||
return response.json().then(errorData => {
|
||||
throw new Error(`${response.status}: ${errorData.message || response.statusText}`);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 获取文件名
|
||||
const contentDisposition = response.headers.get('Content-Disposition');
|
||||
let filename = 'licenses.xlsx';
|
||||
if (contentDisposition) {
|
||||
const filenameMatch = contentDisposition.match(/filename="?([^"]+)"?/);
|
||||
if (filenameMatch && filenameMatch.length === 2) {
|
||||
filename = filenameMatch[1];
|
||||
}
|
||||
}
|
||||
|
||||
// 下载文件
|
||||
return response.blob().then(blob => {
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = filename;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
a.remove();
|
||||
window.URL.revokeObjectURL(url);
|
||||
|
||||
showNotification('导出成功', 'success');
|
||||
});
|
||||
})
|
||||
.catch(error => {
|
||||
// 隐藏加载动画
|
||||
hideLoading();
|
||||
console.error('Failed to export licenses:', error);
|
||||
showNotification(error.message || '导出失败', 'error');
|
||||
})
|
||||
.finally(() => {
|
||||
// 恢复按钮状态
|
||||
submitBtn.disabled = false;
|
||||
submitText.textContent = '导出卡密';
|
||||
});
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
292
deploy.py
Normal file
292
deploy.py
Normal file
@ -0,0 +1,292 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
KaMiXiTong 部署脚本
|
||||
用于生产环境的一键部署
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import subprocess
|
||||
import argparse
|
||||
from pathlib import Path
|
||||
|
||||
def check_python_version():
|
||||
"""检查Python版本"""
|
||||
if sys.version_info < (3, 6):
|
||||
print("❌ 错误: 需要Python 3.6或更高版本")
|
||||
print(f"当前版本: {sys.version}")
|
||||
sys.exit(1)
|
||||
print(f"✅ Python版本检查通过: {sys.version}")
|
||||
|
||||
def create_directories():
|
||||
"""创建必要的目录"""
|
||||
directories = [
|
||||
'logs',
|
||||
'static/uploads',
|
||||
'static/css',
|
||||
'static/js'
|
||||
]
|
||||
|
||||
for directory in directories:
|
||||
Path(directory).mkdir(parents=True, exist_ok=True)
|
||||
print("✅ 目录结构创建完成")
|
||||
|
||||
def check_env_file():
|
||||
"""检查环境配置文件"""
|
||||
env_file = Path('.env')
|
||||
if not env_file.exists():
|
||||
print("⚠️ 未找到 .env 文件,正在创建默认配置...")
|
||||
try:
|
||||
with open('.env.example', 'r', encoding='utf-8') as src:
|
||||
with open('.env', 'w', encoding='utf-8') as dst:
|
||||
dst.write(src.read())
|
||||
print("✅ 已创建默认 .env 文件")
|
||||
except FileNotFoundError:
|
||||
print("❌ 未找到 .env.example 文件")
|
||||
return False
|
||||
else:
|
||||
print("✅ 环境配置文件已存在")
|
||||
return True
|
||||
|
||||
def install_dependencies():
|
||||
"""安装依赖"""
|
||||
print("📦 正在安装依赖包...")
|
||||
try:
|
||||
subprocess.check_call([sys.executable, '-m', 'pip', 'install', '-r', 'requirements.txt'])
|
||||
|
||||
# 检查并安装生产环境依赖
|
||||
try:
|
||||
subprocess.check_call([sys.executable, '-m', 'pip', 'install', 'gunicorn'])
|
||||
print("✅ Gunicorn安装完成")
|
||||
except subprocess.CalledProcessError:
|
||||
print("⚠️ Gunicorn安装失败")
|
||||
|
||||
print("✅ 依赖安装完成")
|
||||
return True
|
||||
except subprocess.CalledProcessError:
|
||||
print("❌ 依赖安装失败")
|
||||
return False
|
||||
|
||||
def init_database():
|
||||
"""初始化数据库"""
|
||||
print("🗄️ 正在初始化数据库...")
|
||||
|
||||
# 检查使用哪种数据库
|
||||
env_file = Path('.env')
|
||||
if env_file.exists():
|
||||
with open(env_file, 'r', encoding='utf-8') as f:
|
||||
content = f.read()
|
||||
if 'DATABASE_URL=sqlite' in content:
|
||||
init_script = 'init_db_sqlite.py'
|
||||
else:
|
||||
init_script = 'setup_mysql.py'
|
||||
else:
|
||||
# 默认使用SQLite
|
||||
init_script = 'init_db_sqlite.py'
|
||||
|
||||
try:
|
||||
if os.path.exists(init_script):
|
||||
subprocess.check_call([sys.executable, init_script])
|
||||
print("✅ 数据库初始化完成")
|
||||
return True
|
||||
else:
|
||||
print(f"⚠️ 数据库初始化脚本 {init_script} 不存在")
|
||||
return False
|
||||
except subprocess.CalledProcessError as e:
|
||||
print(f"❌ 数据库初始化失败: {e}")
|
||||
return False
|
||||
|
||||
def setup_systemd_service():
|
||||
"""配置Systemd服务"""
|
||||
print("⚙️ 正在配置Systemd服务...")
|
||||
|
||||
# 获取当前路径
|
||||
app_path = os.path.abspath('.')
|
||||
venv_path = os.path.join(app_path, 'venv', 'bin', 'gunicorn')
|
||||
|
||||
# 如果没有虚拟环境,使用系统gunicorn
|
||||
if not os.path.exists(venv_path):
|
||||
venv_path = 'gunicorn'
|
||||
|
||||
service_content = f"""[Unit]
|
||||
Description=KaMiXiTong Service
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
User=www-data
|
||||
Group=www-data
|
||||
WorkingDirectory={app_path}
|
||||
Environment="FLASK_ENV=production"
|
||||
ExecStart={venv_path} -w 4 -b 127.0.0.1:5000 run:app
|
||||
Restart=always
|
||||
RestartSec=10
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
"""
|
||||
|
||||
try:
|
||||
# 写入服务文件
|
||||
with open('/tmp/kamaxitong.service', 'w') as f:
|
||||
f.write(service_content)
|
||||
|
||||
print("✅ Systemd服务配置文件已生成")
|
||||
print("💡 请使用以下命令安装服务:")
|
||||
print(" sudo cp /tmp/kamaxitong.service /etc/systemd/system/")
|
||||
print(" sudo systemctl daemon-reload")
|
||||
print(" sudo systemctl enable kamaxitong.service")
|
||||
print(" sudo systemctl start kamaxitong.service")
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"❌ Systemd服务配置失败: {e}")
|
||||
return False
|
||||
|
||||
def setup_nginx_config():
|
||||
"""配置Nginx"""
|
||||
print("⚙️ 正在生成Nginx配置...")
|
||||
|
||||
# 从环境变量获取域名
|
||||
frontend_domain = os.environ.get('FRONTEND_DOMAIN', '')
|
||||
if frontend_domain:
|
||||
# 移除协议部分
|
||||
if '://' in frontend_domain:
|
||||
server_name = frontend_domain.split('://', 1)[1]
|
||||
else:
|
||||
server_name = frontend_domain
|
||||
else:
|
||||
server_name = 'your-domain.com'
|
||||
|
||||
nginx_config = f"""server {{
|
||||
listen 80;
|
||||
server_name {server_name};
|
||||
|
||||
# 重定向到HTTPS(可选)
|
||||
return 301 https://$server_name$request_uri;
|
||||
}}
|
||||
|
||||
server {{
|
||||
listen 443 ssl http2;
|
||||
server_name {server_name};
|
||||
|
||||
# SSL证书配置(请替换为实际路径)
|
||||
# ssl_certificate /path/to/your/certificate.crt;
|
||||
# ssl_certificate_key /path/to/your/private.key;
|
||||
|
||||
# 安全头配置
|
||||
add_header X-Frame-Options "SAMEORIGIN" always;
|
||||
add_header X-XSS-Protection "1; mode=block" always;
|
||||
add_header X-Content-Type-Options "nosniff" always;
|
||||
add_header Referrer-Policy "no-referrer-when-downgrade" always;
|
||||
add_header Content-Security-Policy "default-src 'self' http: https: data: blob: 'unsafe-inline'" always;
|
||||
|
||||
# Gzip压缩
|
||||
gzip on;
|
||||
gzip_vary on;
|
||||
gzip_min_length 1024;
|
||||
gzip_proxied expired no-cache no-store private must-revalidate auth;
|
||||
gzip_types text/plain text/css text/xml text/javascript application/x-javascript application/xml+rss;
|
||||
|
||||
# 代理到Flask应用
|
||||
location / {{
|
||||
proxy_pass http://127.0.0.1:5000;
|
||||
proxy_set_header Host $host;
|
||||
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_set_header X-Forwarded-Host $server_name;
|
||||
}}
|
||||
|
||||
# 静态文件直接由Nginx处理
|
||||
location /static/ {{
|
||||
alias {os.path.abspath('static')}/;
|
||||
expires 1y;
|
||||
add_header Cache-Control "public, immutable";
|
||||
}}
|
||||
|
||||
# 日志配置
|
||||
access_log /var/log/nginx/kamaxitong.access.log;
|
||||
error_log /var/log/nginx/kamaxitong.error.log;
|
||||
}}
|
||||
"""
|
||||
|
||||
try:
|
||||
# 写入配置文件
|
||||
with open('/tmp/kamaxitong_nginx.conf', 'w') as f:
|
||||
f.write(nginx_config)
|
||||
|
||||
print("✅ Nginx配置文件已生成")
|
||||
print("💡 请使用以下命令安装配置:")
|
||||
print(" sudo cp /tmp/kamaxitong_nginx.conf /etc/nginx/sites-available/kamaxitong")
|
||||
print(" sudo ln -s /etc/nginx/sites-available/kamaxitong /etc/nginx/sites-enabled/")
|
||||
print(" sudo nginx -t")
|
||||
print(" sudo systemctl restart nginx")
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"❌ Nginx配置生成失败: {e}")
|
||||
return False
|
||||
|
||||
def main():
|
||||
"""主函数"""
|
||||
parser = argparse.ArgumentParser(description='KaMiXiTong 部署脚本')
|
||||
parser.add_argument('--skip-install', action='store_true', help='跳过依赖安装')
|
||||
parser.add_argument('--skip-db', action='store_true', help='跳过数据库初始化')
|
||||
parser.add_argument('--setup-service', action='store_true', help='配置Systemd服务')
|
||||
parser.add_argument('--setup-nginx', action='store_true', help='生成Nginx配置')
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
print("=" * 50)
|
||||
print(" KaMiXiTong 生产环境部署脚本")
|
||||
print("=" * 50)
|
||||
|
||||
# 检查Python版本
|
||||
check_python_version()
|
||||
|
||||
# 创建目录
|
||||
create_directories()
|
||||
|
||||
# 检查环境配置
|
||||
if not check_env_file():
|
||||
sys.exit(1)
|
||||
|
||||
# 安装依赖
|
||||
if not args.skip_install:
|
||||
if not install_dependencies():
|
||||
sys.exit(1)
|
||||
else:
|
||||
print("⏭️ 跳过依赖安装")
|
||||
|
||||
# 初始化数据库
|
||||
if not args.skip_db:
|
||||
if not init_database():
|
||||
sys.exit(1)
|
||||
else:
|
||||
print("⏭️ 跳过数据库初始化")
|
||||
|
||||
# 配置Systemd服务
|
||||
if args.setup_service:
|
||||
setup_systemd_service()
|
||||
|
||||
# 配置Nginx
|
||||
if args.setup_nginx:
|
||||
setup_nginx_config()
|
||||
|
||||
print("\n" + "=" * 50)
|
||||
print("✅ 部署准备完成!")
|
||||
print("=" * 50)
|
||||
|
||||
if args.setup_service:
|
||||
print("💡 已生成Systemd服务配置文件,请按提示安装")
|
||||
|
||||
if args.setup_nginx:
|
||||
print("💡 已生成Nginx配置文件,请按提示安装")
|
||||
|
||||
print("\n📌 下一步操作:")
|
||||
print("1. 编辑 .env 文件,配置数据库和其他参数")
|
||||
print("2. 配置SSL证书(推荐)")
|
||||
print("3. 启动服务")
|
||||
print("4. 访问你的域名")
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
134
docs/DEPLOYMENT.md
Normal file
134
docs/DEPLOYMENT.md
Normal file
@ -0,0 +1,134 @@
|
||||
# 部署说明
|
||||
|
||||
本文档介绍如何将KaMiXiTong系统部署到生产环境并通过域名访问。
|
||||
|
||||
## 域名访问配置
|
||||
|
||||
### 问题描述
|
||||
|
||||
在本地开发环境中,系统默认通过 `http://localhost:5000` 访问。当部署到服务器并通过域名访问时,前端页面中的接口调用可能会仍然指向 `localhost:5000`,导致接口调用失败。
|
||||
|
||||
### 解决方案
|
||||
|
||||
系统已通过修改前端JavaScript中的 `apiRequest` 函数来自动检测当前访问的主机并构建正确的API调用地址。
|
||||
|
||||
前端JavaScript现在会使用 `window.location.origin` 来获取当前访问的完整域名,并自动构建完整的API URL。
|
||||
|
||||
### 配置Nginx反向代理
|
||||
|
||||
推荐使用Nginx作为反向代理服务器。以下是一个Nginx配置示例:
|
||||
|
||||
```nginx
|
||||
server {
|
||||
listen 80;
|
||||
server_name your-domain.com;
|
||||
|
||||
# 重定向到HTTPS(可选)
|
||||
return 301 https://$server_name$request_uri;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 443 ssl;
|
||||
server_name your-domain.com;
|
||||
|
||||
# SSL证书配置
|
||||
ssl_certificate /path/to/your/certificate.crt;
|
||||
ssl_certificate_key /path/to/your/private.key;
|
||||
|
||||
# 代理到Flask应用
|
||||
location / {
|
||||
proxy_pass http://127.0.0.1:5000;
|
||||
proxy_set_header Host $host;
|
||||
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;
|
||||
}
|
||||
|
||||
# 静态文件直接由Nginx处理
|
||||
location /static/ {
|
||||
alias /path/to/your/app/static/;
|
||||
expires 1y;
|
||||
add_header Cache-Control "public, immutable";
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 环境变量配置
|
||||
|
||||
在生产环境中,建议通过环境变量来配置应用:
|
||||
|
||||
```bash
|
||||
# 复制示例配置文件
|
||||
cp .env.example .env
|
||||
|
||||
# 编辑配置文件
|
||||
nano .env
|
||||
```
|
||||
|
||||
重要的环境变量包括:
|
||||
|
||||
- `FLASK_ENV`: 设置为 `production`
|
||||
- `SECRET_KEY`: 设置为强随机字符串
|
||||
- `DATABASE_URL`: 数据库连接URL
|
||||
- `HOST`: 监听地址,生产环境建议设置为 `0.0.0.0`
|
||||
|
||||
### 使用Gunicorn启动应用
|
||||
|
||||
在生产环境中,推荐使用Gunicorn作为WSGI服务器:
|
||||
|
||||
```bash
|
||||
# 安装Gunicorn
|
||||
pip install gunicorn
|
||||
|
||||
# 启动应用
|
||||
gunicorn -w 4 -b 127.0.0.1:5000 run:app
|
||||
```
|
||||
|
||||
### 使用Systemd管理服务(Linux)
|
||||
|
||||
创建Systemd服务文件:
|
||||
|
||||
```ini
|
||||
# /etc/systemd/system/kamaxitong.service
|
||||
[Unit]
|
||||
Description=KaMiXiTong Service
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
User=www-data
|
||||
Group=www-data
|
||||
WorkingDirectory=/path/to/your/app
|
||||
Environment="FLASK_ENV=production"
|
||||
ExecStart=/path/to/your/venv/bin/gunicorn -w 4 -b 127.0.0.1:5000 run:app
|
||||
Restart=always
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
```
|
||||
|
||||
启用并启动服务:
|
||||
|
||||
```bash
|
||||
sudo systemctl enable kamaxitong.service
|
||||
sudo systemctl start kamaxitong.service
|
||||
```
|
||||
|
||||
## 常见问题
|
||||
|
||||
### Q: 前端页面能正常访问,但API调用失败怎么办?
|
||||
|
||||
A: 检查以下几点:
|
||||
1. 确认Nginx配置正确,特别是 `proxy_set_header` 指令
|
||||
2. 确认Flask应用监听地址设置为 `0.0.0.0`
|
||||
3. 检查浏览器开发者工具中的网络请求,确认API请求的URL是否正确
|
||||
|
||||
### Q: 如何确认域名配置是否正确?
|
||||
|
||||
A: 可以通过以下方式检查:
|
||||
1. 在浏览器中打开应用,按F12打开开发者工具
|
||||
2. 切换到Network标签页
|
||||
3. 刷新页面,观察API请求的URL是否为域名地址而非localhost
|
||||
|
||||
### Q: 静态文件无法加载怎么办?
|
||||
|
||||
A: 确保Nginx配置中正确设置了静态文件的location块,或者让Flask应用处理静态文件。
|
||||
420
init_db_mysql.py
Normal file
420
init_db_mysql.py
Normal file
@ -0,0 +1,420 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
MySQL 数据库初始化脚本
|
||||
KaMiXiTong 软件授权管理系统
|
||||
|
||||
使用 MySQL 数据库的完整版本,支持多用户和高性能
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from werkzeug.security import generate_password_hash
|
||||
|
||||
# 添加项目根目录到Python路径
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
def load_env_config():
|
||||
"""从.env文件加载配置"""
|
||||
env_file = '.env'
|
||||
if os.path.exists(env_file):
|
||||
print(f"正在从 {env_file} 加载配置...")
|
||||
with open(env_file, 'r', encoding='utf-8') as f:
|
||||
for line in f:
|
||||
line = line.strip()
|
||||
# 跳过注释和空行
|
||||
if line and not line.startswith('#'):
|
||||
# 解析键值对
|
||||
if '=' in line:
|
||||
key, value = line.split('=', 1)
|
||||
key = key.strip()
|
||||
value = value.strip().strip('"').strip("'")
|
||||
os.environ[key] = value
|
||||
print("配置加载完成")
|
||||
else:
|
||||
print(f"警告: 未找到 {env_file} 文件")
|
||||
|
||||
def init_mysql_database():
|
||||
"""初始化 MySQL 数据库"""
|
||||
print("=" * 50)
|
||||
print("KaMiXiTong MySQL 数据库初始化开始...")
|
||||
print("=" * 50)
|
||||
|
||||
# 从.env文件加载配置
|
||||
load_env_config()
|
||||
|
||||
# 获取数据库URL
|
||||
database_url = os.environ.get('DATABASE_URL')
|
||||
if not database_url:
|
||||
print("❌ 未设置DATABASE_URL环境变量")
|
||||
print("💡 请在.env文件中配置DATABASE_URL")
|
||||
print("示例: DATABASE_URL=mysql+pymysql://用户名:密码@主机:端口/数据库名")
|
||||
return False
|
||||
|
||||
try:
|
||||
# 设置数据库URL到环境变量
|
||||
os.environ['DATABASE_URL'] = database_url
|
||||
|
||||
# 导入应用
|
||||
from app import create_app, db
|
||||
from app.models import Admin, Product, Version, License, Device, Ticket
|
||||
from app.models.audit_log import AuditLog
|
||||
|
||||
# 创建应用实例
|
||||
app = create_app()
|
||||
|
||||
print(f"数据库类型: MySQL")
|
||||
print(f"数据库URL: {database_url}")
|
||||
|
||||
with app.app_context():
|
||||
# 1. 删除所有表(如果存在)
|
||||
print("\n1. 清理现有数据库表...")
|
||||
db.drop_all()
|
||||
print(" ✓ 已删除所有现有表")
|
||||
|
||||
# 2. 创建所有表
|
||||
print("\n2. 创建数据库表结构...")
|
||||
db.create_all()
|
||||
print(" ✓ 已创建所有数据表")
|
||||
|
||||
# 显式提交事务以确保表被创建
|
||||
db.session.commit()
|
||||
|
||||
# 2.1 验证表创建
|
||||
print("\n2.1 验证表创建...")
|
||||
from sqlalchemy import inspect
|
||||
inspector = inspect(db.engine)
|
||||
tables = inspector.get_table_names()
|
||||
print(f" ✓ 成功创建 {len(tables)} 个表: {', '.join(tables)}")
|
||||
|
||||
if not tables:
|
||||
raise Exception("表创建失败,没有找到任何表")
|
||||
|
||||
# 3. 插入初始数据
|
||||
print("\n3. 插入初始数据...")
|
||||
insert_mysql_data()
|
||||
|
||||
# 显式提交并关闭会话
|
||||
db.session.commit()
|
||||
db.session.close()
|
||||
|
||||
# 4. 显示数据库信息
|
||||
print("\n4. MySQL 数据库初始化完成!")
|
||||
show_mysql_database_info()
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"\n❌ MySQL 数据库初始化失败: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return False
|
||||
|
||||
|
||||
def insert_mysql_data():
|
||||
"""插入 MySQL 初始数据"""
|
||||
from app import db
|
||||
from app.models import Admin, Product, Version, License, Device, Ticket
|
||||
from app.models.audit_log import AuditLog
|
||||
|
||||
# 1. 创建默认管理员账号
|
||||
print(" 创建默认管理员账号...")
|
||||
admin = Admin(
|
||||
username='admin',
|
||||
email='admin@kamaxitong.com',
|
||||
role=1, # 超级管理员
|
||||
status=1 # 正常
|
||||
)
|
||||
admin.set_password('admin123')
|
||||
db.session.add(admin)
|
||||
|
||||
# 创建测试管理员账号
|
||||
test_admin = Admin(
|
||||
username='test_admin',
|
||||
email='test@kamaxitong.com',
|
||||
role=0, # 普通管理员
|
||||
status=1 # 正常
|
||||
)
|
||||
test_admin.set_password('test123')
|
||||
db.session.add(test_admin)
|
||||
print(" ✓ 已创建管理员账号 (admin/admin123, test_admin/test123)")
|
||||
|
||||
# 2. 创建示例产品
|
||||
print(" 创建示例产品...")
|
||||
products = [
|
||||
Product(
|
||||
product_id='KMX001',
|
||||
product_name='KaMiXiTong 专业版',
|
||||
description='专业的软件授权管理系统,支持多种授权模式和完整的设备管理',
|
||||
status=1
|
||||
),
|
||||
Product(
|
||||
product_id='KMX002',
|
||||
product_name='KaMiXiTong 企业版',
|
||||
description='企业级软件授权管理解决方案,支持分布式部署和高并发访问',
|
||||
status=1
|
||||
)
|
||||
]
|
||||
|
||||
for product in products:
|
||||
db.session.add(product)
|
||||
print(" ✓ 已创建2个示例产品")
|
||||
|
||||
# 3. 创建版本信息
|
||||
print(" 创建版本信息...")
|
||||
versions = [
|
||||
Version(
|
||||
product_id='KMX001',
|
||||
version_num='1.0.0',
|
||||
update_log='初始版本发布',
|
||||
download_url='https://download.kamaxitong.com/v1.0.0/professional.exe',
|
||||
min_license_version='1.0.0',
|
||||
force_update=0,
|
||||
download_status=1,
|
||||
publish_status=1
|
||||
),
|
||||
Version(
|
||||
product_id='KMX001',
|
||||
version_num='1.1.0',
|
||||
update_log='功能增强版本',
|
||||
download_url='https://download.kamaxitong.com/v1.1.0/professional.exe',
|
||||
min_license_version='1.0.0',
|
||||
force_update=0,
|
||||
download_status=1,
|
||||
publish_status=1
|
||||
),
|
||||
Version(
|
||||
product_id='KMX002',
|
||||
version_num='2.0.0',
|
||||
update_log='企业版初始版本',
|
||||
download_url='https://download.kamaxitong.com/v2.0.0/enterprise.exe',
|
||||
min_license_version='2.0.0',
|
||||
force_update=0,
|
||||
download_status=1,
|
||||
publish_status=1
|
||||
)
|
||||
]
|
||||
|
||||
for version in versions:
|
||||
db.session.add(version)
|
||||
print(" ✓ 已创建3个版本信息")
|
||||
|
||||
# 4. 创建示例许可证
|
||||
print(" 创建示例许可证...")
|
||||
# 使用时区感知的 datetime 对象替换 utcnow()
|
||||
current_time = datetime.now(timezone.utc)
|
||||
|
||||
licenses = [
|
||||
# 永久许可证
|
||||
License(
|
||||
license_key=License.generate_license_key(),
|
||||
product_id='KMX001',
|
||||
type=1, # 正式版
|
||||
status=1, # 已激活
|
||||
valid_days=-1, # 永久
|
||||
bind_machine_code='DEMO-MACHINE-CODE-001',
|
||||
activate_time=current_time - timedelta(days=30),
|
||||
expire_time=None, # 永久不过期
|
||||
last_verify_time=current_time - timedelta(days=1),
|
||||
unbind_count=0
|
||||
),
|
||||
# 1年试用期许可证
|
||||
License(
|
||||
license_key=License.generate_license_key(),
|
||||
product_id='KMX001',
|
||||
type=0, # 试用版
|
||||
status=1, # 已激活
|
||||
valid_days=365, # 1年
|
||||
bind_machine_code='DEMO-MACHINE-CODE-002',
|
||||
activate_time=current_time - timedelta(days=15),
|
||||
expire_time=current_time + timedelta(days=350), # 还有350天过期
|
||||
last_verify_time=current_time - timedelta(hours=6),
|
||||
unbind_count=1
|
||||
),
|
||||
# 未激活许可证
|
||||
License(
|
||||
license_key=License.generate_license_key(),
|
||||
product_id='KMX002',
|
||||
type=1, # 正式版
|
||||
status=0, # 未激活
|
||||
valid_days=365, # 1年
|
||||
bind_machine_code=None,
|
||||
activate_time=None,
|
||||
expire_time=None,
|
||||
last_verify_time=None,
|
||||
unbind_count=0
|
||||
)
|
||||
]
|
||||
|
||||
for license_obj in licenses:
|
||||
db.session.add(license_obj)
|
||||
print(" ✓ 已创建3个示例许可证")
|
||||
|
||||
# 5. 创建示例设备
|
||||
print(" 创建示例设备...")
|
||||
devices = [
|
||||
Device(
|
||||
machine_code='DEMO-MACHINE-CODE-001',
|
||||
license_id=1, # 对应第一个许可证
|
||||
product_id='KMX001',
|
||||
software_version='1.1.0',
|
||||
status=1, # 正常
|
||||
activate_time=current_time - timedelta(days=30),
|
||||
last_verify_time=current_time - timedelta(days=1)
|
||||
),
|
||||
Device(
|
||||
machine_code='DEMO-MACHINE-CODE-002',
|
||||
license_id=2, # 对应第二个许可证
|
||||
product_id='KMX001',
|
||||
software_version='1.0.0',
|
||||
status=1, # 正常
|
||||
activate_time=current_time - timedelta(days=15),
|
||||
last_verify_time=current_time - timedelta(hours=6)
|
||||
)
|
||||
]
|
||||
|
||||
for device in devices:
|
||||
db.session.add(device)
|
||||
print(" ✓ 已创建2个示例设备")
|
||||
|
||||
# 6. 创建示例工单
|
||||
print(" 创建示例工单...")
|
||||
tickets = [
|
||||
Ticket(
|
||||
title='许可证激活失败',
|
||||
product_id='KMX001',
|
||||
software_version='1.0.0',
|
||||
machine_code='CUSTOMER-MACHINE-001',
|
||||
license_key='DEMO-LICENSE-KEY-001',
|
||||
description='客户反馈无法激活许可证',
|
||||
priority=2, # 高优先级
|
||||
status=2, # 已解决
|
||||
operator='admin',
|
||||
remark='问题已解决',
|
||||
create_time=current_time - timedelta(days=5),
|
||||
update_time=current_time - timedelta(days=4),
|
||||
resolve_time=current_time - timedelta(days=4)
|
||||
),
|
||||
Ticket(
|
||||
title='功能咨询',
|
||||
product_id='KMX002',
|
||||
software_version='2.0.0',
|
||||
machine_code=None,
|
||||
license_key=None,
|
||||
description='客户咨询批量购买事宜',
|
||||
priority=1, # 中优先级
|
||||
status=0, # 待处理
|
||||
operator=None,
|
||||
remark=None,
|
||||
create_time=current_time - timedelta(days=2),
|
||||
update_time=current_time - timedelta(days=2)
|
||||
)
|
||||
]
|
||||
|
||||
for ticket in tickets:
|
||||
db.session.add(ticket)
|
||||
print(" ✓ 已创建2个示例工单")
|
||||
|
||||
# 7. 创建审计日志示例数据
|
||||
print(" 创建示例审计日志...")
|
||||
audit_logs = [
|
||||
AuditLog(
|
||||
admin_id=1,
|
||||
action='login',
|
||||
target_type='system',
|
||||
target_id=None,
|
||||
details='管理员登录系统',
|
||||
ip_address='127.0.0.1',
|
||||
user_agent='Mozilla/5.0...',
|
||||
create_time=current_time - timedelta(days=30)
|
||||
),
|
||||
AuditLog(
|
||||
admin_id=1,
|
||||
action='create',
|
||||
target_type='product',
|
||||
target_id=1,
|
||||
details='创建产品 KaMiXiTong 专业版',
|
||||
ip_address='127.0.0.1',
|
||||
user_agent='Mozilla/5.0...',
|
||||
create_time=current_time - timedelta(days=29)
|
||||
)
|
||||
]
|
||||
|
||||
for log in audit_logs:
|
||||
db.session.add(log)
|
||||
print(" ✓ 已创建2条示例审计日志")
|
||||
|
||||
# 提交所有更改
|
||||
db.session.commit()
|
||||
print(" ✓ 所有初始数据插入完成")
|
||||
|
||||
|
||||
def show_mysql_database_info():
|
||||
"""显示 MySQL 数据库信息"""
|
||||
from app import db
|
||||
from app.models import Admin, Product, Version, License, Device, Ticket
|
||||
from app.models.audit_log import AuditLog
|
||||
|
||||
print("\n" + "=" * 50)
|
||||
print("MySQL 数据库初始化信息统计")
|
||||
print("=" * 50)
|
||||
|
||||
# 统计各表的记录数
|
||||
tables_info = [
|
||||
('管理员账号', Admin.query.count()),
|
||||
('产品', Product.query.count()),
|
||||
('版本', Version.query.count()),
|
||||
('许可证', License.query.count()),
|
||||
('设备', Device.query.count()),
|
||||
('工单', Ticket.query.count()),
|
||||
('审计日志', AuditLog.query.count())
|
||||
]
|
||||
|
||||
for table_name, count in tables_info:
|
||||
print(f" {table_name:8} : {count:3} 条记录")
|
||||
|
||||
print("\n" + "=" * 50)
|
||||
print("默认登录信息")
|
||||
print("=" * 50)
|
||||
print(" 超级管理员: admin / admin123")
|
||||
print(" 普通管理员: test_admin / test123")
|
||||
print("\n注意事项:")
|
||||
print(" 1. 请在生产环境中修改默认密码")
|
||||
print(" 2. 确保MySQL服务正常运行")
|
||||
print(" 3. 修改 .env 文件中的 DATABASE_URL 为正确的MySQL配置")
|
||||
print(" 4. 定期备份数据库")
|
||||
print("=" * 50)
|
||||
|
||||
|
||||
def main():
|
||||
"""主函数"""
|
||||
try:
|
||||
# 检查是否安装了PyMySQL
|
||||
try:
|
||||
import pymysql
|
||||
except ImportError:
|
||||
print("❌ 未安装PyMySQL,请运行: pip install PyMySQL")
|
||||
sys.exit(1)
|
||||
|
||||
# 初始化MySQL数据库
|
||||
success = init_mysql_database()
|
||||
|
||||
if success:
|
||||
print("\n🎉 MySQL 数据库初始化成功!")
|
||||
print("\n启动服务器:")
|
||||
print(" python run.py")
|
||||
print(" 或")
|
||||
print(" python start.py")
|
||||
print("\n访问: http://localhost:5000")
|
||||
else:
|
||||
print("\n❌ MySQL 数据库初始化失败!")
|
||||
sys.exit(1)
|
||||
|
||||
except Exception as e:
|
||||
print(f"\n❌ 初始化失败: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
Loading…
Reference in New Issue
Block a user