From b23be8bc55921e2c978ad49a9ca5815cf9c04528 Mon Sep 17 00:00:00 2001 From: Nixevol Date: Thu, 15 Jan 2026 17:26:12 +0800 Subject: [PATCH] v2.0.1 done --- app/main.py | 47 +++++++++----- app/processor.py | 37 ++++++++--- run.bat | 2 +- static/index.html | 8 +-- static/js/app.js | 154 +++++++++++++++++++++++++++++++++++++--------- 5 files changed, 190 insertions(+), 58 deletions(-) diff --git a/app/main.py b/app/main.py index 9d21901..592c526 100644 --- a/app/main.py +++ b/app/main.py @@ -30,7 +30,7 @@ from app.history import HistoryManager app = FastAPI( title="CapacityReport", description="容量报表数据处理系统", - version="2.0.0" + version="2.0.1" ) # CORS 配置 @@ -103,7 +103,7 @@ async def health_check(): return { "status": "healthy" if is_healthy else "unhealthy", "timestamp": datetime.now().isoformat(), - "version": "2.0.0", + "version": "2.0.1", "uptime_pid": os.getpid(), "checks": checks } @@ -396,13 +396,18 @@ async def start_processing(task_id: str = Body(..., embed=True)): logs: List[str] = [] def log_callback(msg: str): logs.append(msg) - processing_tasks[task_id] = {"logs": logs, "status": "processing"} + # 确保每次日志更新都同步到 processing_tasks + if task_id in processing_tasks: + processing_tasks[task_id]["logs"] = logs.copy() + processing_tasks[task_id]["status"] = "processing" + else: + processing_tasks[task_id] = {"logs": logs.copy(), "status": "processing"} logger = ProcessLogger(log_file=log_file, callback=log_callback) # 更新状态 history_manager.update(task_id, status="processing") - processing_tasks[task_id] = {"logs": logs, "status": "processing"} + processing_tasks[task_id] = {"logs": [], "status": "processing"} # 更新全局锁定状态为处理中 global_task_lock["locked"] = True @@ -453,22 +458,31 @@ async def start_processing(task_id: str = Body(..., embed=True)): @app.post("/api/process/status") async def get_processing_status(task_id: str = Body(..., embed=True)): """获取处理任务状态和日志(task_id 放在 POST body 中)""" - # 优先从文件读取日志(实时写入,保证最新) - logs = history_manager.get_logs(task_id) - - # 检查内存中的实时状态(用于获取状态) + # 检查内存中的实时状态(优先使用内存中的日志,实时更新) if task_id in processing_tasks: + task_info = processing_tasks[task_id] + # 如果内存中有日志,优先使用内存中的(实时更新) + if "logs" in task_info and task_info["logs"]: + return { + "task_id": task_id, + "status": task_info["status"], + "logs": task_info["logs"] # 使用内存中的实时日志 + } + # 如果内存中没有日志,尝试从文件读取 + logs = history_manager.get_logs(task_id) return { "task_id": task_id, - "status": processing_tasks[task_id]["status"], + "status": task_info["status"], "logs": logs # 使用文件中的日志 } - # 从历史记录获取 + # 从历史记录获取(任务已完成) record = history_manager.get(task_id) if not record: raise HTTPException(status_code=404, detail="任务不存在") + # 从文件读取日志 + logs = history_manager.get_logs(task_id) return { "task_id": task_id, "status": record.status, @@ -1118,7 +1132,7 @@ async def get_service_status(): """获取服务运行状态""" return { "status": "running", - "version": "2.0.0", + "version": "2.0.1", "platform": platform.system(), "supervisor": is_supervisor_running(), "pid": os.getpid(), @@ -1179,12 +1193,17 @@ async def execute_script(): logs: List[str] = [] def log_callback(msg: str): logs.append(msg) - processing_tasks[task_id] = {"logs": logs, "status": "processing"} + # 确保每次日志更新都同步到 processing_tasks + if task_id in processing_tasks: + processing_tasks[task_id]["logs"] = logs.copy() + processing_tasks[task_id]["status"] = "processing" + else: + processing_tasks[task_id] = {"logs": logs.copy(), "status": "processing"} logger = ProcessLogger(log_file=None, callback=log_callback) # 初始化任务状态 - processing_tasks[task_id] = {"logs": logs, "status": "processing"} + processing_tasks[task_id] = {"logs": [], "status": "processing"} # 在后台线程执行脚本 def run_script(): @@ -1256,6 +1275,6 @@ async def save_script_content(content: str = Body(..., embed=True)): if __name__ == "__main__": import uvicorn - print(f"CapacityReport v2.0.0") + print(f"CapacityReport v2.0.1") print(f"配置更新时间: {config.update}") uvicorn.run("app.main:app", host="0.0.0.0", port=9081, reload=False) diff --git a/app/processor.py b/app/processor.py index 982050f..f05c03f 100644 --- a/app/processor.py +++ b/app/processor.py @@ -687,27 +687,46 @@ class DataProcessor: with open(SQL_SCRIPT, 'r', encoding='utf-8') as f: sql_text = f.read() - sqls = sqlparse.split(sql_text) - total = len(sqls) + if not sql_text or not sql_text.strip(): + self.logger.warning("SQL 脚本文件为空,跳过执行") + return + sqls = sqlparse.split(sql_text) + # 过滤掉空语句和注释 + valid_sqls = [] + for sql in sqls: + sql = sql.strip() + if sql and not sql.startswith('#'): + valid_sqls.append(sql) + + if not valid_sqls: + self.logger.warning("SQL 脚本中没有有效的 SQL 语句(可能全是注释或空行)") + return + + total = len(valid_sqls) + self.logger.info(f"共找到 {total} 条有效的 SQL 语句") + + executed_count = 0 with self.db.get_connection() as conn: with conn.cursor() as cursor: - for i, sql in enumerate(sqls, 1): - sql = sql.strip() - if not sql or sql.startswith('#'): - continue - + for i, sql in enumerate(valid_sqls, 1): start_time = time.time() preview = sql[:80].replace('\n', ' ') self.logger.info(f"执行 SQL ({i}/{total}): {preview}...") try: cursor.execute(sql) + executed_count += 1 elapsed = round(time.time() - start_time, 2) - self.logger.info(f"完成,耗时 {elapsed} 秒") + affected_rows = cursor.rowcount if cursor.rowcount >= 0 else 0 + if affected_rows > 0: + self.logger.info(f"完成,耗时 {elapsed} 秒,影响 {affected_rows} 行") + else: + self.logger.info(f"完成,耗时 {elapsed} 秒") except Exception as e: self.logger.error(f"SQL 执行失败: {e}") + # 继续执行下一条 SQL,不中断 conn.commit() - self.logger.success("SQL 脚本执行完成") + self.logger.success(f"SQL 脚本执行完成,共执行 {executed_count}/{total} 条语句") diff --git a/run.bat b/run.bat index 891ac62..372aa2a 100644 --- a/run.bat +++ b/run.bat @@ -1,6 +1,6 @@ @echo off chcp 65001 >nul -title CapacityReport v2.0.0 [自动重启模式] +title CapacityReport v2.0.1 [自动重启模式] echo ======================================== echo CapacityReport - 容量报表处理程序 diff --git a/static/index.html b/static/index.html index 04edc1f..722305d 100644 --- a/static/index.html +++ b/static/index.html @@ -41,9 +41,9 @@ 系统设置 - -
- 上传中: - 0 -
已完成: 0 diff --git a/static/js/app.js b/static/js/app.js index fc7a1de..f11ab77 100644 --- a/static/js/app.js +++ b/static/js/app.js @@ -111,13 +111,24 @@ function showConfirm(title, message) { // API 调用 async function api(endpoint, options = {}) { - const response = await fetch(`/api${endpoint}`, { + // 如果没有指定 method 且没有 body,默认使用 GET + const method = options.method || (options.body ? 'POST' : 'GET'); + + const fetchOptions = { + method: method, headers: { 'Content-Type': 'application/json', ...options.headers }, ...options - }); + }; + + // GET 请求不应该有 body + if (method === 'GET' && fetchOptions.body) { + delete fetchOptions.body; + } + + const response = await fetch(`/api${endpoint}`, fetchOptions); if (!response.ok) { const error = await response.json().catch(() => ({ detail: response.statusText })); @@ -460,13 +471,11 @@ class FileUploader { updateUploadStats() { const stats = { total: this.files.length, - uploading: this.files.filter(f => f.status === 'uploading').length, success: this.files.filter(f => f.status === 'uploaded').length, error: this.files.filter(f => f.status === 'error').length }; $('#statsTotal').textContent = stats.total; - $('#statsUploading').textContent = stats.uploading; $('#statsSuccess').textContent = stats.success; $('#statsError').textContent = stats.error; @@ -536,15 +545,23 @@ class FileUploader { $('#totalProgress').style.display = 'none'; $('#uploadStats').style.display = 'none'; - // 显示处理区域(上传中状态) - $('#processSection').style.display = 'block'; - $('#processActions').style.display = 'none'; - $('#logContent').innerHTML = '
正在上传文件...
'; - $('#processStatus').className = 'process-status processing'; - $('#processStatus').textContent = '上传中...'; + // 隐藏处理区域(上传完成前不显示) + $('#processSection').style.display = 'none'; - // 强制浏览器重绘 - $('#processSection').offsetHeight; + // 显示文件列表和进度信息 + $('#fileList').style.display = 'block'; + $('#totalProgress').style.display = 'block'; + $('#uploadStats').style.display = 'flex'; + + // 初始化进度显示 + $('#totalPercent').textContent = '0%'; + $('#totalProgressBar').style.width = '0%'; + + // 保持所有文件为等待上传状态(不立即改为上传中) + this.files.forEach((fileData, index) => { + this.updateFileStatus(index, 'pending', 0); + }); + this.updateUploadStats(); this.isUploading = true; $('#startUpload').disabled = true; @@ -566,10 +583,40 @@ class FileUploader { // 上传进度事件 xhr.upload.addEventListener('progress', (e) => { if (e.lengthComputable) { - const progress = Math.round((e.loaded / e.total) * 100); - // 更新日志显示进度 - $('#logContent').innerHTML = `
正在上传文件... ${progress}%
`; - $('#processStatus').textContent = `上传中 ${progress}%`; + const totalProgress = Math.round((e.loaded / e.total) * 100); + const fileCount = this.files.length; + + // 更新总进度条 + $('#totalPercent').textContent = `${totalProgress}%`; + $('#totalProgressBar').style.width = `${totalProgress}%`; + + // 根据总进度计算当前应该处理到第几个文件 + // 每个文件占 100/fileCount 的进度 + const progressPerFile = 100 / fileCount; + const currentFileIndex = Math.floor(totalProgress / progressPerFile); + const currentFileProgress = (totalProgress % progressPerFile) / progressPerFile * 100; + + // 更新每个文件的状态 + this.files.forEach((fileData, index) => { + if (index < currentFileIndex) { + // 已完成的文件 + this.updateFileStatus(index, 'uploaded', 100); + } else if (index === currentFileIndex) { + // 当前正在上传的文件 + const fileProgress = Math.min(100, Math.max(0, Math.round(currentFileProgress))); + this.updateFileStatus(index, 'uploading', fileProgress); + + // 滚动到当前正在上传的文件 + const fileItem = $(`#file-item-${index}`); + if (fileItem) { + fileItem.scrollIntoView({ behavior: 'smooth', block: 'center' }); + } + } else { + // 还未开始的文件 + this.updateFileStatus(index, 'pending', 0); + } + }); + this.updateUploadStats(); } }); @@ -606,10 +653,25 @@ class FileUploader { this.taskId = taskId; console.log('上传完成,任务ID:', taskId); + // 将所有文件状态改为已完成 + this.files.forEach((fileData, index) => { + this.updateFileStatus(index, 'uploaded', 100); + }); + this.updateUploadStats(); + // 注意:锁定已在后端上传接口中完成,这里不需要再次锁定 - // 更新UI显示上传完成 + // 隐藏上传进度,显示处理区域 + $('#totalProgress').style.display = 'none'; + $('#uploadStats').style.display = 'none'; + $('#fileList').style.display = 'none'; + + // 显示处理区域(上传完成后才显示) + $('#processSection').style.display = 'block'; + const processActions = $('#processActions'); + if (processActions) processActions.style.display = 'none'; $('#logContent').innerHTML = `
上传完成: ${result.file_count || this.files.length} 个文件
`; + $('#processStatus').className = 'process-status processing'; $('#processStatus').textContent = '准备开始处理...'; showToast(`上传完成: ${result.file_count || this.files.length} 个文件`, 'success'); @@ -626,6 +688,13 @@ class FileUploader { } catch (error) { showToast(`上传失败: ${error.message}`, 'error'); console.error('上传失败:', error); + + // 将所有文件状态改为失败 + this.files.forEach((fileData, index) => { + this.updateFileStatus(index, 'error', 0); + }); + this.updateUploadStats(); + // 上传失败,解锁全局任务(后端会自动处理,但这里也尝试解锁) if (this.taskId) { try { @@ -669,7 +738,8 @@ class FileUploader { // 显示处理区域 $('#processSection').style.display = 'block'; - $('#processActions').style.display = 'none'; + const processActions = $('#processActions'); + if (processActions) processActions.style.display = 'none'; $('#processStatus').className = 'process-status processing'; $('#processStatus').textContent = '处理中...'; $('#logContent').innerHTML = '
处理任务已启动,等待日志...
'; @@ -771,6 +841,11 @@ class FileUploader { else if (log.includes('[WARN]')) level = 'warn'; return `
${log}
`; }).join(''); + } else if (status.status === 'processing') { + // 处理中但还没有日志,显示等待信息 + if (!logContent.innerHTML || logContent.innerHTML.trim() === '') { + logContent.innerHTML = '
等待处理开始...
'; + } } // 自动滚动到底部(仅在自动刷新开启时) @@ -813,13 +888,20 @@ class FileUploader { } catch (error) { console.error('轮询状态失败:', error); - // 如果请求失败(如 404),可能是任务ID错误 + // 如果请求失败(如 404),可能是任务ID错误或任务已完成 if (error.message && error.message.includes('404')) { - clearInterval(this.pollInterval); - showToast('任务状态获取失败', 'error'); - $('#processStatus').textContent = '状态获取失败'; + // 任务不存在,停止轮询并恢复界面 + if (this.pollInterval) { + clearInterval(this.pollInterval); + this.pollInterval = null; + } + showToast('任务不存在或已完成', 'warning'); + $('#processStatus').textContent = '任务不存在'; $('#processStatus').className = 'process-status failed'; this.restoreUploadUI(); + } else { + // 其他错误(如网络错误),继续轮询但显示错误提示 + console.warn('轮询状态失败,继续重试:', error); } } }; @@ -847,6 +929,16 @@ class FileUploader { else if (log.includes('[WARN]')) level = 'warn'; return `
${log}
`; }).join(''); + // 滚动到底部 + const container = $('#logContainer'); + if (container) { + container.scrollTop = container.scrollHeight; + } + } else if (status.status === 'processing') { + // 处理中但还没有日志,显示等待信息 + if (!logContent.innerHTML || logContent.innerHTML.trim() === '') { + logContent.innerHTML = '
等待处理开始...
'; + } } // 不自动滚动,让用户自己控制 @@ -881,11 +973,17 @@ class FileUploader { this.updateFileList(); // 隐藏处理区域 - $('#processSection').style.display = 'none'; - $('#processActions').style.display = 'none'; - $('#processStatus').className = 'process-status processing'; - $('#processStatus').textContent = '处理中...'; - $('#logContent').innerHTML = ''; + const processSection = $('#processSection'); + if (processSection) processSection.style.display = 'none'; + const processActions = $('#processActions'); + if (processActions) processActions.style.display = 'none'; + const processStatus = $('#processStatus'); + if (processStatus) { + processStatus.className = 'process-status processing'; + processStatus.textContent = '处理中...'; + } + const logContent = $('#logContent'); + if (logContent) logContent.innerHTML = ''; // 恢复显示上传区域 this.restoreUploadUI(); console.log('任务已重置,UI 已恢复'); @@ -2096,7 +2194,7 @@ document.addEventListener('DOMContentLoaded', () => { // 历史数据大小已在处理历史页面显示,不再在侧边栏显示 - console.log('CapacityReport v2.0.0 已加载'); + console.log('CapacityReport v2.0.1 已加载'); // 重启服务按钮事件(使用事件委托,支持所有页面的重启按钮) document.addEventListener('click', async (e) => {