修复接口访问问题

This commit is contained in:
wsb1224 2025-11-16 19:56:14 +08:00
parent 2a132259fb
commit 0e35be59f6
9 changed files with 134 additions and 657 deletions

2
.env
View File

@ -23,7 +23,7 @@ MAX_UNBIND_TIMES=3
LICENSE_KEY_LENGTH=32
LICENSE_KEY_PREFIX=
FRONTEND_DOMAIN=http://km.taisan.online
FRONTEND_DOMAIN=http://127.0.0.1:5000
# API配置

View File

@ -1,329 +0,0 @@
# 账号管理系统重构部署指南
## 快速开始
### 1. 备份现有系统
```bash
# 备份数据库
mysqldump -u root -p your_database > backup_$(date +%Y%m%d_%H%M%S).sql
# 备份应用文件
tar -czf app_backup_$(date +%Y%m%d_%H%M%S).tar.gz app/ migrations/
# 备份配置文件
cp config.py config.py.bak
```
### 2. 应用迁移
```bash
# 进入项目目录
cd /path/to/KaMiXiTong
# 应用数据库迁移
flask db upgrade
# 或手动执行
alembic upgrade head
```
### 3. 重启应用
```bash
# 如果使用systemd
sudo systemctl restart kamixitong
# 如果使用gunicorn
sudo killall gunicorn
# 然后重新启动
gunicorn -w 4 -b 0.0.0.0:5000 run:app &
# 如果直接运行
python run.py
```
### 4. 验证部署
```bash
# 运行测试脚本
python test_refactored_admin.py
```
## 详细部署步骤
### 步骤1: 准备工作
确认以下环境:
- Python 3.7+
- MySQL 5.7+ 或 PostgreSQL
- Flask-Migrate 已安装
```bash
pip list | grep -E "Flask|flask-migrate|sqlalchemy"
```
### 步骤2: 检查代码文件
确认以下文件已存在:
- `app/api/admin.py` (重构后)
- `app/models/audit_log.py` (新建)
- `app/web/templates/admin/list.html` (重构后)
- `migrations/versions/20241111_add_soft_delete_to_admin.py`
- `migrations/versions/20241111_create_audit_log.py`
### 步骤3: 停止应用
确保没有活跃的用户连接:
```bash
# 查看活跃连接
lsof -i :5000
# 停止应用
sudo systemctl stop kamixitong
# 或
pkill -f "gunicorn.*run:app"
```
### 步骤4: 应用迁移
```bash
# 初始化迁移仓库(如果还没有)
flask db init
# 生成迁移文件(可选,文件已存在)
# flask db migrate -m "Add soft delete fields to admin"
# flask db migrate -m "Create audit_log table"
# 升级数据库
flask db upgrade
# 验证迁移
flask db current
# 应该看到: add_soft_delete_admin 和 create_audit_log
```
### 步骤5: 验证表结构
```sql
-- 连接到数据库
mysql -u root -p
-- 检查admin表
DESCRIBE admin;
-- 应该看到: is_deleted, delete_time 字段
-- 检查audit_log表
DESCRIBE audit_log;
-- 应该存在audit_log表
```
### 步骤6: 启动应用
```bash
# 启动应用
python run.py
# 或
gunicorn -w 4 -b 0.0.0.0:5000 run:app &
# 检查日志
tail -f app.log
```
### 步骤7: 功能测试
#### 测试1: 登录系统
1. 访问登录页面
2. 使用超级管理员账号登录
3. 确认可以访问管理后台
#### 测试2: 账号管理
1. 进入"账号管理"页面
2. 创建新账号
3. 编辑账号
4. 启用/禁用账号
5. 删除账号(验证软删除)
6. 查看审计日志
#### 测试3: 验证新功能
```bash
# 访问审计日志表
mysql -u root -p -e "SELECT * FROM audit_log ORDER BY create_time DESC LIMIT 10;"
# 查看已删除的账号
mysql -u root -p -e "SELECT admin_id, username, is_deleted, delete_time FROM admin WHERE is_deleted = 1;"
```
## 回滚方案
如果遇到问题,可以回滚到原版本:
### 方案1: 回滚数据库
```bash
# 查看迁移历史
flask db history
# 回滚到特定版本
flask db downgrade add_soft_delete_admin
# 或完全回滚
flask db downgrade base
```
### 方案2: 恢复备份
```bash
# 恢复数据库
mysql -u root -p your_database < backup_YYYYMMDD_HHMMSS.sql
# 恢复应用文件
tar -xzf app_backup_YYYYMMDD_HHMMSS.tar.gz
# 重启应用
sudo systemctl restart kamixitong
```
### 方案3: 手动回滚
如果自动回滚失败,手动执行:
```sql
-- 删除audit_log表
DROP TABLE IF EXISTS audit_log;
-- 删除admin表字段
ALTER TABLE admin DROP COLUMN is_deleted;
ALTER TABLE admin DROP COLUMN delete_time;
```
## 性能优化建议
### 1. 审计日志归档
```sql
-- 归档6个月前的日志
CREATE TABLE audit_log_archive AS
SELECT * FROM audit_log
WHERE create_time < DATE_SUB(NOW(), INTERVAL 6 MONTH);
-- 清理旧日志
DELETE FROM audit_log
WHERE create_time < DATE_SUB(NOW(), INTERVAL 6 MONTH);
```
### 2. 添加索引
```sql
-- 为常用查询添加索引
CREATE INDEX idx_admin_status ON admin(status);
CREATE INDEX idx_admin_username ON admin(username);
```
### 3. 监控查询性能
```sql
-- 慢查询监控
SET long_query_time = 2;
SET slow_query_log = 1;
```
## 常见问题解决
### Q1: 迁移失败
**错误:** `Target database is not up to date`
**解决:**
```bash
# 标记当前版本
flask db stamp head
# 重新迁移
flask db upgrade
```
### Q2: 权限错误
**错误:** `Access denied for user`
**解决:**
```sql
-- 授权
GRANT ALL PRIVILEGES ON your_database.* TO 'your_user'@'localhost';
FLUSH PRIVILEGES;
```
### Q3: 审计日志记录失败
**错误:** `Table 'audit_log' doesn't exist`
**解决:**
```bash
# 手动应用迁移
alembic upgrade create_audit_log
```
### Q4: 登录失败
**错误:** `管理员不存在`
**解决:**
检查数据库中是否有超级管理员账号:
```sql
SELECT admin_id, username, role, status, is_deleted FROM admin WHERE role = 1 AND status = 1 AND is_deleted = 0;
```
如果没有,创建一个:
```sql
INSERT INTO admin (username, password_hash, email, role, status, is_deleted)
VALUES ('admin', '<hashed_password>', 'admin@example.com', 1, 1, 0);
```
## 监控和维护
### 日常检查
1. **查看应用日志**
```bash
tail -f app.log | grep -E "ERROR|CRITICAL"
```
2. **检查数据库状态**
```sql
SHOW PROCESSLIST;
SHOW TABLE STATUS;
```
3. **监控审计日志增长**
```sql
SELECT COUNT(*) FROM audit_log WHERE create_time > CURDATE();
```
### 定期维护
1. **每月归档审计日志**
2. **清理过期的已删除账号** (可选)
3. **优化数据库表** (`OPTIMIZE TABLE`)
4. **备份数据库**
## 联系支持
如果遇到问题:
1. 查看 `REFACTOR_NOTES.md` 了解详细变更
2. 运行 `test_refactored_admin.py` 进行诊断
3. 检查应用日志和数据库日志
4. 如需帮助,请提供:
- 错误日志
- 数据库版本
- Python版本
- 操作系统版本
---
**重要提示:**
- 部署前务必备份数据
- 在测试环境先进行验证
- 保留原有的admin_old.py和list_old.html作为备份
- 建议在低峰期进行部署

View File

@ -27,7 +27,6 @@ KaMiXiTong/
│ ├── test_api.py # API测试
│ └── test_models.py # 模型测试
├── docs/ # 文档
│ ├── API.md # API文档
│ ├── INTEGRATION.md # 集成文档
│ ├── EXAMPLES.md # 使用示例
│ └── FASTAPI.md # FastAPI接口文档
@ -136,7 +135,8 @@ gunicorn -w 4 -b 0.0.0.0:5000 run:app
## 文档
- [API文档](docs/API.md)
为了更好地管理和使用项目文档,请参考 [文档整理指南](DOCUMENTATION_GUIDE.md)。
- [集成指南](docs/INTEGRATION.md)
- [使用示例](docs/EXAMPLES.md)
- [FastAPI接口文档](docs/FASTAPI.md)

View File

@ -1,298 +0,0 @@
# 账号管理系统重构说明
## 概述
本次重构彻底改进了账号管理系统从后端API到前端页面都进行了全面优化。重构基于第一性原理思考采用更现代、更安全的架构。
## 重构前后对比
### 后端改进
| 方面 | 重构前 | 重构后 |
|------|--------|--------|
| 错误处理 | 基础try-catch | 装饰器统一异常处理,分类错误码 |
| 响应格式 | 不统一 | 统一响应结构: `{success, data, message, code}` |
| 数据验证 | 分散在多处 | 统一的验证函数,前后端一致 |
| 删除方式 | 硬删除(物理删除) | 软删除(标记删除) |
| 审计日志 | 无 | 完整的审计日志系统 |
| 日志记录 | 只记录错误 | 记录所有操作详情 |
| 安全性 | 基础验证 | 强验证+权限控制 |
| 代码复用 | 重复代码较多 | 模块化,函数复用 |
### 前端改进
| 方面 | 重构前 | 重构后 |
|------|--------|--------|
| 状态管理 | 散乱 | IIFE模式状态统一管理 |
| DOM操作 | 频繁使用innerHTML | DOM创建+事件委托 |
| 错误处理 | 简单alert | 优雅的通知系统 |
| 事件绑定 | 重复绑定 | 事件委托避免重复 |
| 代码结构 | 函数式 | 模块化(IIFE) |
| 用户体验 | 基础 | 加载状态、实时反馈 |
| 表单验证 | 基础 | 前后端双重验证 |
## 核心改进
### 1. 统一响应格式
```python
{
"success": bool, # 操作是否成功
"data": any, # 返回数据
"message": str, # 提示信息
"code": int # 状态码
}
```
**优点:**
- 前端处理更简单
- 错误信息更清晰
- 便于调试和日志分析
### 2. 软删除机制
- **添加字段:** `is_deleted`, `delete_time`
- **查询过滤:** 自动过滤已删除记录
- **安全保护:** 防止数据丢失
### 3. 审计日志
记录所有管理员操作:
- 操作类型: CREATE, UPDATE, DELETE, TOGGLE_STATUS
- 操作人: admin_id
- 目标: target_type, target_id
- 详情: details (JSON格式)
- 环境: IP地址, User-Agent
### 4. 统一验证
- `validate_username()`: 用户名验证
- `validate_password()`: 密码强度验证
- `validate_email()`: 邮箱格式验证
- `validate_admin_data()`: 管理员数据综合验证
### 5. 异常处理装饰器
```python
@handle_exceptions
def api_function():
# 异常自动处理
# 统一错误响应
# 自动回滚事务
```
### 6. 权限验证装饰器
```python
@require_admin
def protected_function():
# 自动检查登录状态
# 自动检查超级管理员权限
# 统一权限错误响应
```
## 文件变更
### 新增文件
1. **app/models/audit_log.py** - 审计日志模型
2. **app/api/admin_refactored.py** - 重构后的API(已替换原文件)
3. **app/web/templates/admin/list_refactored.html** - 重构后的前端页面(已替换原文件)
4. **migrations/versions/20241111_add_soft_delete_to_admin.py** - 添加软删除字段迁移
5. **migrations/versions/20241111_create_audit_log.py** - 创建审计日志表迁移
6. **test_refactored_admin.py** - 测试脚本
### 备份文件
1. `app/api/admin_old.py` - 原API文件备份
2. `app/web/templates/admin/list_old.html` - 原前端页面备份
## 数据库迁移
### 迁移1: 添加软删除字段
```bash
flask db upgrade
# 或
alembic upgrade head
```
**变更内容:**
- 添加 `is_deleted` 字段 (默认0)
- 添加 `delete_time` 字段
- 创建索引 `ix_admin_is_deleted`
### 迁移2: 创建审计日志表
```bash
# 迁移1完成后自动执行
# 或手动执行
flask db stamp add_soft_delete_admin
flask db upgrade
```
**变更内容:**
- 创建 `audit_log`
- 添加外键关联
- 创建索引
## API变更
### 响应结构统一
所有API现在返回统一格式:
```json
{
"success": true/false,
"data": {...}, // 成功时返回数据
"message": "提示信息",
"code": 0 // 状态码
}
```
### 状态码定义
```python
class ResponseCode:
SUCCESS = 0 # 成功
VALIDATION_ERROR = 1001 # 验证错误
AUTHENTICATION_FAILED = 1002 # 认证失败
PERMISSION_DENIED = 1003 # 权限不足
NOT_FOUND = 1004 # 资源不存在
DUPLICATE_USERNAME = 2001 # 用户名重复
INVALID_DATA = 2002 # 无效数据
CANNOT_DELETE_SELF = 2003 # 不能删除自己
CANNOT_DISABLE_SELF = 2004 # 不能禁用自己
SERVER_ERROR = 5000 # 服务器错误
```
## 前端改进
### 1. 模块化代码结构
使用IIFE模式封装:
```javascript
(function() {
'use strict';
// 状态管理
const state = {
currentPage: 1,
searchParams: {...},
isLoading: false
};
// 事件处理
function bindEvents() {...}
// 数据加载
function loadAdmins() {...}
// 初始化
document.addEventListener('DOMContentLoaded', init);
})();
```
### 2. 事件委托
使用事件委托减少内存泄漏:
```javascript
elements.adminList.addEventListener('click', function(e) {
const target = e.target.closest('button');
if (!target) return;
if (target.classList.contains('edit-btn')) {
// 编辑逻辑
} else if (target.classList.contains('delete-btn')) {
// 删除逻辑
}
});
```
### 3. DOM安全操作
使用createElement替代innerHTML:
```javascript
const tr = document.createElement('tr');
const td = document.createElement('td');
td.textContent = admin.username; // 防止XSS
tr.appendChild(td);
```
### 4. 用户体验优化
- 加载状态指示
- 优雅的错误通知
- 实时数据更新
- 表单验证反馈
## 测试
运行测试脚本:
```bash
python test_refactored_admin.py
```
测试覆盖:
1. Admin模型基本功能
2. 软删除功能
3. 审计日志记录
4. 数据验证函数
5. 统一响应格式
## 部署步骤
1. **备份数据库**
```bash
# 备份当前数据库
mysqldump -u user -p database > backup.sql
```
2. **应用迁移**
```bash
flask db upgrade
```
3. **重启应用**
```bash
# 如果使用gunicorn
sudo systemctl restart kamixitong
# 或直接运行
python run.py
```
4. **验证部署**
- 访问账号管理页面
- 创建测试账号
- 查看审计日志
## 已知问题和限制
1. **兼容性**: 现有已删除的硬删除记录需要手动处理
2. **性能**: 审计日志会逐渐增大,建议定期归档
3. **权限**: 当前只有超级管理员可以管理账号
## 未来改进计划
1. **批量操作**: 支持批量启用/禁用/删除
2. **角色细分**: 支持更多角色类型
3. **操作日志查看**: 在管理界面查看审计日志
4. **数据导出**: 支持导出账号数据
5. **API限流**: 添加API访问频率限制
## 总结
本次重构显著提升了系统的:
- **安全性**: 软删除+审计日志
- **可维护性**: 模块化+统一规范
- **可观测性**: 完整的日志记录
- **用户体验**: 实时反馈+优雅交互
- **代码质量**: 减少重复+提高复用
通过这次重构,账号管理系统达到了生产级别的标准,具备了更好的扩展性和维护性。

View File

@ -5,6 +5,7 @@ from flask_login import LoginManager
from flask_migrate import Migrate
from config import config
from flask_wtf.csrf import CSRFProtect
from flask_cors import CORS
import logging
from logging.handlers import RotatingFileHandler
@ -13,6 +14,7 @@ db = SQLAlchemy()
login_manager = LoginManager()
migrate = Migrate()
csrf = CSRFProtect()
cors = CORS()
def create_app(config_name='default'):
"""应用工厂函数"""
@ -48,6 +50,7 @@ def create_app(config_name='default'):
login_manager.init_app(app)
migrate.init_app(app, db)
csrf.init_app(app)
cors.init_app(app, resources={r"/api/*": {"origins": "*"}})
# 配置登录管理器
login_manager.login_view = 'web.login'

View File

@ -169,6 +169,18 @@ def create_version():
current_app.logger.warning(f"版本号已存在: product_id={product_id}, version_num={version_num}")
return jsonify({'success': False, 'message': '版本号已存在'}), 400
# 验证文件链接格式(如果提供了链接)
if download_url:
from urllib.parse import urlparse
try:
result = urlparse(download_url)
if not all([result.scheme, result.netloc]):
current_app.logger.warning(f"无效的文件链接格式: {download_url}")
return jsonify({'success': False, 'message': '文件链接格式无效'}), 400
except Exception as e:
current_app.logger.warning(f"文件链接验证失败: {str(e)}")
return jsonify({'success': False, 'message': '文件链接格式无效'}), 400
current_app.logger.info(f"创建版本对象: product_id={product_id}, version_num={version_num}")
version = Version(
product_id=product_id,
@ -198,7 +210,8 @@ def create_version():
log_operation('CREATE_VERSION', 'VERSION', version.version_id, {
'product_id': version.product_id,
'version_num': version.version_num,
'publish_now': publish_now
'publish_now': publish_now,
'has_download_url': bool(download_url)
})
return jsonify({
@ -336,7 +349,10 @@ def update_version_status(version_id):
except Exception as e:
current_app.logger.error(f"更新版本状态失败: {str(e)}")
return jsonify({'success': False, 'message': '服务器内部错误'}), 500
return jsonify({
'success': False,
'message': '服务器内部错误'
}), 500
@api_bp.route('/versions/upload', methods=['POST'])
@require_login
@ -630,3 +646,13 @@ def batch_update_version_status():
'message': '服务器内部错误'
}), 500

View File

@ -50,26 +50,50 @@
<textarea class="form-control" id="update_notes" name="update_notes" rows="5"></textarea>
</div>
<!-- 文件上传区域 -->
<!-- 文件提供方式选择 -->
<div class="mb-3">
<label class="form-label">版本文件上传</label>
<div class="border rounded p-3 mb-2">
<div class="d-flex justify-content-between align-items-center mb-2">
<button type="button" class="btn btn-outline-primary btn-sm" id="upload-file-btn">
<i class="fas fa-upload me-1"></i>选择文件
</button>
<input type="file" id="version_file" name="version_file" accept=".zip,.rar,.7z,.exe,.dmg,.pkg" class="d-none">
<div id="upload-status" class="text-muted">未选择文件</div>
<label class="form-label">文件提供方式 *</label>
<ul class="nav nav-tabs" id="fileSourceTabs" role="tablist">
<li class="nav-item" role="presentation">
<button class="nav-link active" id="upload-tab" data-bs-toggle="tab" data-bs-target="#upload-content" type="button" role="tab">上传文件</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="link-tab" data-bs-toggle="tab" data-bs-target="#link-content" type="button" role="tab">文件链接</button>
</li>
</ul>
<div class="tab-content border border-top-0 p-3" id="fileSourceTabsContent">
<!-- 文件上传区域 -->
<div class="tab-pane fade show active" id="upload-content" role="tabpanel">
<div class="d-flex justify-content-between align-items-center mb-2">
<button type="button" class="btn btn-outline-primary btn-sm" id="upload-file-btn">
<i class="fas fa-upload me-1"></i>选择文件
</button>
<input type="file" id="version_file" name="version_file" accept=".zip,.rar,.7z,.exe,.dmg,.pkg" class="d-none">
<div id="upload-status" class="text-muted">未选择文件</div>
</div>
<div id="file-info" class="small text-muted d-none">
<div>文件名: <span id="file-name"></span></div>
<div>文件大小: <span id="file-size"></span></div>
<div>文件哈希: <span id="file-hash"></span></div>
<input type="hidden" id="file_url" name="file_url">
<input type="hidden" id="file_hash" name="file_hash">
</div>
<div class="form-text">支持常见的压缩包和安装包格式</div>
</div>
<div id="file-info" class="small text-muted d-none">
<div>文件名: <span id="file-name"></span></div>
<div>文件大小: <span id="file-size"></span></div>
<div>文件哈希: <span id="file-hash"></span></div>
<input type="hidden" id="file_url" name="file_url">
<input type="hidden" id="file_hash" name="file_hash">
<!-- 文件链接区域 -->
<div class="tab-pane fade" id="link-content" role="tabpanel">
<div class="mb-3">
<label for="file_link" class="form-label">文件下载链接</label>
<input type="url" class="form-control" id="file_link" name="file_link" placeholder="https://example.com/software.zip">
<div class="form-text">请输入完整的文件下载链接</div>
</div>
<div class="mb-3">
<label for="link_file_name" class="form-label">文件名(可选)</label>
<input type="text" class="form-control" id="link_file_name" name="link_file_name" placeholder="software.zip">
</div>
</div>
</div>
<div class="form-text">支持常见的压缩包和安装包格式,文件将单独上传</div>
</div>
<div class="mb-3 form-check">
@ -241,15 +265,34 @@ function createVersion() {
formData.append('update_notes', updateNotes);
}
// 添加文件信息(如果已上传)
const fileUrl = document.getElementById('file_url').value;
if (fileUrl) {
formData.append('download_url', fileUrl);
}
// 检查当前选择的文件提供方式
const activeTab = document.querySelector('.nav-link.active').id;
const fileHash = document.getElementById('file_hash').value;
if (fileHash) {
formData.append('file_hash', fileHash);
if (activeTab === 'upload-tab') {
// 上传文件方式
const fileUrl = document.getElementById('file_url').value;
if (fileUrl) {
formData.append('download_url', fileUrl);
}
const fileHash = document.getElementById('file_hash').value;
if (fileHash) {
formData.append('file_hash', fileHash);
}
} else if (activeTab === 'link-tab') {
// 文件链接方式
const fileLink = document.getElementById('file_link').value.trim();
if (fileLink) {
formData.append('download_url', fileLink);
}
// 如果提供了文件名,则使用提供的文件名
const linkFileName = document.getElementById('link_file_name').value.trim();
if (linkFileName) {
// 生成一个虚拟的文件哈希(基于文件名)
const fileHash = generateSimpleHash(linkFileName);
formData.append('file_hash', fileHash);
}
}
const publishNow = document.getElementById('publish_now').checked;
@ -266,6 +309,17 @@ function createVersion() {
return;
}
// 验证文件提供方式
if (activeTab === 'upload-tab') {
// 上传方式不需要强制要求文件,但如果有文件则使用
} else if (activeTab === 'link-tab') {
const fileLink = document.getElementById('file_link').value.trim();
if (fileLink && !isValidUrl(fileLink)) {
showNotification('请输入有效的文件链接', 'warning');
return;
}
}
// 显示加载状态
submitBtn.disabled = true;
submitText.textContent = '创建中...';
@ -307,5 +361,26 @@ function createVersion() {
submitText.textContent = '创建版本';
});
}
// 简单的哈希生成函数
function generateSimpleHash(str) {
let hash = 0;
for (let i = 0; i < str.length; i++) {
const char = str.charCodeAt(i);
hash = ((hash << 5) - hash) + char;
hash = hash & hash; // 转换为32位整数
}
return Math.abs(hash).toString(16);
}
// 验证URL格式
function isValidUrl(string) {
try {
new URL(string);
return true;
} catch (_) {
return false;
}
}
</script>
{% endblock %}

Binary file not shown.

View File

@ -74,7 +74,7 @@ class TestAuthValidator(unittest.TestCase):
self.software_id = "TEST_SOFTWARE_ID"
self.validator = AuthValidator(
software_id=self.software_id,
api_url="http://km.taisan.online/api/v1",
api_url="http://127.0.0.1:5000/api/v1", # 使用本地测试URL
timeout=1 # 快速超时
)