Hook_Unfinished

#include <windows.h>

// 假设这两个函数是存在的
void DoRD() {}
void 改堆栈cal1() {}
void 改回堆栈cal1() {}

__declspec(naked) void HOOKcall()
{
    __asm
    {
        pushad
        nop
    }
    __asm
    {
        popad
        mov eax, dword ptr [esi + 8]
        sub eax, ecx
        retn
    }
}

int main() {
    // 第一个 Hook 操作
    DWORD HookAddress1 = 0x00491C62;//原函数地址
    DWORD HookSubroutinePtr1 = (DWORD)HOOKcall;//跳转函数地址
    DWORD JumpValue1 = HookSubroutinePtr1 - HookAddress1 - 5;
    DWORD old1 = 0;

    // 修改页面属性为可执行、可读、可写
    VirtualProtect((PVOID)HookAddress1, 114, PAGE_EXECUTE_READWRITE, &old1);

    // 修改内存
    *(BYTE*)HookAddress1 = 0xE8;//先写第一个B, 0xE8=Call
    *(DWORD*)(HookAddress1 + 1) = JumpValue1;//HookAddress1后面4个字节填写跳转值
    *(BYTE*)(HookAddress1 + 5) = 0x90;//空余的一个B用NOP填充

    // 恢复页面属性
    VirtualProtect((PVOID)HookAddress1, 114, old1, &old1);

    // 第二个 Hook 操作
    DWORD HookAddress2 = 0x00492008;
    DWORD HookSubroutinePtr2 = (DWORD)改回堆栈cal1;
    DWORD JumpValue2 = HookSubroutinePtr2 - HookAddress2 - 5;
    DWORD old2 = 0;

    // 修改页面属性为可执行、可读、可写
    VirtualProtect((PVOID)HookAddress2, 114, PAGE_EXECUTE_READWRITE, &old2);

    // 修改内存
    *(BYTE*)HookAddress2 = 0xE9;
    *(DWORD*)(HookAddress2 + 1) = JumpValue2;
    *(BYTE*)(HookAddress2 + 5) = 0x90;

    // 恢复页面属性
    VirtualProtect((PVOID)HookAddress2, 114, old2, &old2);

    return 0;
}    

DbgView输出调试信息

#include <stdio.h>
#include <stdarg.h>
#include <windows.h>

void CallOutputDebugInfo(char* pszFormat, ...) {
#ifdef DEBUG
    char szbufFormat[0x1000];
    char szbufFormat_Game[0x1100] = "";
    va_list argList;

    // 参数列表初始化
    va_start(argList, pszFormat);
    // 使用 vsprintf_s 格式化字符串
    vsprintf_s(szbufFormat, sizeof(szbufFormat), pszFormat, argList);
    // 拼接字符串
    strcat_s(szbufFormat_Game, sizeof(szbufFormat_Game), szbufFormat);
    // 输出调试信息
    OutputDebugStringA(szbufFormat_Game);
    // 结束可变参数列表的使用
    va_end(argList);
#endif
}    

提权

#include <windows.h>
#include <stdio.h>

//OpenProcess失败情况下的提权代码
BOOL Call_ElevatePrivilege(BOOL bEnable) {
    // 初始化成功标志
    BOOL fOK = FALSE;
    HANDLE hToken;

    // 打开当前进程的访问令牌
    if (OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES, &hToken)) {
        TOKEN_PRIVILEGES tp;
        // 设置权限数量
        tp.PrivilegeCount = 1;
        // 查找调试权限的 LUID
        LookupPrivilegeValue(NULL, SE_DEBUG_NAME, &tp.Privileges[0].Luid);
        // 根据传入的参数设置权限属性
        if (bEnable) {
            tp.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED;
        } else {
            tp.Privileges[0].Attributes = 0;
        }
        // 调整令牌权限
        AdjustTokenPrivileges(hToken, FALSE, &tp, sizeof(tp), NULL, NULL);
        // 检查操作是否成功
        fOK = (GetLastError() == ERROR_SUCCESS);
        // 关闭令牌句柄
        CloseHandle(hToken);
    }
    return fOK;
}    

mTempBrightnessEvent.setTime(System.currentTimeMillis()); mTempBrightnessEvent.setBrightness(brightnessState); mTempBrightnessEvent.setPhysicalDisplayId(mUniqueDisplayId); mTempBrightnessEvent.setDisplayState(state); mTempBrightnessEvent.setDisplayPolicy(mPowerRequest.policy); mTempBrightnessEvent.setReason(mBrightnessReason); mTempBrightnessEvent.setHbmMax(mBrightnessRangeController.getCurrentBrightnessMax()); mTempBrightnessEvent.setHbmMode(mBrightnessRangeController.getHighBrightnessMode()); mTempBrightnessEvent.setFlags(mTempBrightnessEvent.getFlags() | (mIsRbcActive ? BrightnessEvent.FLAG_RBC : 0) | (mPowerRequest.lowPowerMode ? BrightnessEvent.FLAG_LOW_POWER_MODE : 0)); mTempBrightnessEvent.setRbcStrength(mCdsi != null ? mCdsi.getReduceBrightColorsStrength() : -1); mTempBrightnessEvent.setPowerFactor(mPowerRequest.screenLowPowerBrightnessFactor); mTempBrightnessEvent.setWasShortTermModelActive(wasShortTermModelActive); mTempBrightnessEvent.setDisplayBrightnessStrategyName(displayBrightnessState .getDisplayBrightnessStrategyName()); mTempBrightnessEvent.setAutomaticBrightnessEnabled( displayBrightnessState.getShouldUseAutoBrightness()); // Temporary is what we use during slider interactions. We avoid logging those so that // we don't spam logcat when the slider is being used. boolean tempToTempTransition = mTempBrightnessEvent.getReason().getReason() == BrightnessReason.REASON_TEMPORARY && mLastBrightnessEvent.getReason().getReason() == BrightnessReason.REASON_TEMPORARY; // Purely for dumpsys; final boolean isRbcEvent = mLastBrightnessEvent.isRbcEnabled() != mTempBrightnessEvent.isRbcEnabled(); if ((!mTempBrightnessEvent.equalsMainData(mLastBrightnessEvent) && !tempToTempTransition) || brightnessAdjustmentFlags != 0) { mTempBrightnessEvent.setInitialBrightness(mLastBrightnessEvent.getBrightness()); mLastBrightnessEvent.copyFrom(mTempBrightnessEvent); BrightnessEvent newEvent = new BrightnessEvent(mTempBrightnessEvent); // Adjustment flags (and user-set flag) only get added after the equality checks since // they are transient. newEvent.setAdjustmentFlags(brightnessAdjustmentFlags); newEvent.setFlags(newEvent.getFlags() | (userSetBrightnessChanged ? BrightnessEvent.FLAG_USER_SET : 0)); if (DEBUG_PANIC) { Slog.i(mTag, newEvent.toString(/* includeTime= */ false)); } if (mBrightnessEventRingBuffer != null) { mBrightnessEventRingBuffer.append(newEvent); } if (isRbcEvent) { mRbcEventRingBuffer.append(newEvent); } } // Update display white-balance. if (mDisplayWhiteBalanceController != null) { if (state == Display.STATE_ON && mDisplayWhiteBalanceSettings.isEnabled()) { mDisplayWhiteBalanceController.setEnabled(true); mDisplayWhiteBalanceController.updateDisplayColorTemperature(); } else { mDisplayWhiteBalanceController.setEnabled(false); } } final boolean ready = mPendingScreenOnUnblocker == null && mPendingScreenOnUnblockerByDisplayOffload == null && (!mColorFadeEnabled || (!mColorFadeOnAnimator.isStarted() && !mColorFadeOffAnimator.isStarted()) //#ifdef OPLUS_EXTENSION_HOOK //Zongjun.Li@ANDROID.BIOMETIRCS.1070880, 2016/01/11, Add for biometrics && ! mDpcExt.isBlockDisplayByBiometrics()) //#endif /* OPLUS_EXTENSION_HOOK */ && mPowerState.waitUntilClean(mCleanListener); final boolean finished = ready && !mScreenBrightnessRampAnimator.isAnimating(); if (ready && (state != Display.STATE_OFF) && (state != Display.STATE_DOZE) && (state != Display.STATE_DOZE_SUSPEND) && mReportedScreenStateToPolicy == REPORTED_TO_POLICY_SCREEN_TURNING_ON) { setReportedScreenState(REPORTED_TO_POLICY_SCREEN_ON); mWindowManagerPolicy.screenTurnedOn(mDisplayId); Slog.d(mTag, "screenTurnedOn state=" + state + " brightnessState=" + brightnessState + " mustNotify=" + mustNotify); //#ifdef OPLUS_FEATURE_BIOMETRICS // Chunbo.Gao@ANDROID.BIOMETRICS 2024/8/5, Add for biometrics unblock screenon } else if (!mDpcExt.hasRemapDisable() && mDpcExt.isBlockDisplayByBiometrics() && (oldState == Display.STATE_ON) && ((state == Display.STATE_OFF) || (state == Display.STATE_DOZE)) && !mDpcExt.onWakeUp()) { //Qianghuang.huang@ANDROID.BIOMETRICS 2024/11/20,Add for biometrics block screen when in wakeup. Slog.i(mTag, "unblock screenon: oldState: " + oldState + ", state: " + state + ", isBlockScreenOnByBiometrics: " + mDpcExt.isBlockScreenOnByBiometrics()); if (mDpcExt.isBlockScreenOnByBiometrics()) { mDpcExt.unblockScreenOnByBiometrics(UNBLOCK_REASON_GO_TO_SLEEP); sendUpdatePowerState(); } } //#endif OPLUS_FEATURE_BIOMETRICS //#endif /* OPLUS_FEATURE_AUTOBRIGHTNESS */ // Grab a wake lock if we have unfinished business. if (!finished) { mWakelockController.acquireWakelock(WakelockController.WAKE_LOCK_UNFINISHED_BUSINESS); } //#ifdef OPLUS_EXTENSION_DEBUG //GuoDong.Huang@ANDROID.POWER, 2022/04/12, add for debug if ((DEBUG_PANIC || DEBUG) && ready && mustNotify) { Slog.d(mTag, "updatePowerState: ready = " + ready + ", mustNotify = " + mustNotify + ", state = " + state); } //#endif /* OPLUS_EXTENSION_DEBUG */ // Notify the power manager when ready. if (ready && mustNotify) { // Send state change. //#ifdef OPLUS_FEATURE_BIOMETRICS_POWERBLOCK //Wenlong@ANDROID.BIOMETRICS.2055900, 2019/06/12, Add for Screen on/off state sync mDpcExt.onUpdatePowerState(state, mPowerRequest.policy, (int)brightnessState); //#endif /* OPLUS_FEATURE_BIOMETRICS_POWERBLOCK */ synchronized (mLock) { if (!mPendingRequestChangedLocked) { mDisplayReadyLocked = true; if (DEBUG) { Slog.d(mTag, "Display ready!"); } } } sendOnStateChangedWithWakelock(); } // Release the wake lock when we have no unfinished business. if (finished) { mWakelockController.releaseWakelock(WakelockController.WAKE_LOCK_UNFINISHED_BUSINESS); } // Record if dozing for future comparison. mDozing = state != Display.STATE_ON; if (previousPolicy != mPowerRequest.policy) { logDisplayPolicyChanged(mPowerRequest.policy); //#ifdef OPLUS_EDIT //ChenYongxing@MULTIMEDIA.DISPLAY.SERVICE, 2024/03/13, Add for autobrightness String log = String.format("policy:%s->%s,state:%s->%s", DisplayPowerRequest.policyToString(previousPolicy), DisplayPowerRequest.policyToString(mPowerRequest.policy), Display.stateToString(oldState), Display.stateToString(targetState)); Slog.d(mTag, "updatePowerStateInternal " + log + " " + mPowerRequest); //#endif OPLUS_EDIT } //#ifdef OPLUS_EXTENSION_HOOK // Xinyu.Cen@ANDROID.POWER 2024/12/26, add for power on ux if (OplusPlatformLevelUtils.IS_LIGHT_OS || IS_LIGHT_OS_BY_AMS) { mDpcExt.clearPowerOnUX(); } //#endif /* OPLUS_EXTENSION_HOOK */ }分析这段代码的作用并给出注释
09-16
c++: fatal error: Killed signal terminated program cc1plus compilation terminated. gmake[2]: *** [CMakeFiles/_tenseal_cpp.dir/build.make:225: CMakeFiles/_tenseal_cpp.dir/tenseal/cpp/tensors/bfvtensor.cpp.o] Error 1 gmake[2]: *** Waiting for unfinished jobs.... c++: fatal error: Killed signal terminated program cc1plus compilation terminated. gmake[2]: *** [CMakeFiles/_sealapi_cpp.dir/build.make:160: CMakeFiles/_sealapi_cpp.dir/tenseal/sealapi/sealapi_context.cpp.o] Error 1 gmake[2]: *** Waiting for unfinished jobs.... c++: fatal error: Killed signal terminated program cc1plus compilation terminated. gmake[2]: *** [CMakeFiles/_tenseal_cpp.dir/build.make:82: CMakeFiles/_tenseal_cpp.dir/tenseal/sealapi/sealapi.cpp.o] Error 1 c++: fatal error: Killed signal terminated program cc1plus compilation terminated. gmake[2]: *** [CMakeFiles/tenseal.dir/build.make:108: CMakeFiles/tenseal.dir/tenseal/cpp/tensors/bfvvector.cpp.o] Error 1 gmake[2]: *** Waiting for unfinished jobs.... c++: fatal error: Killed signal terminated program cc1plus compilation terminated. gmake[2]: *** [CMakeFiles/_tenseal_cpp.dir/build.make:108: CMakeFiles/_tenseal_cpp.dir/tenseal/sealapi/sealapi_encrypt.cpp.o] Error 1 c++: fatal error: Killed signal terminated program cc1plus compilation terminated. gmake[2]: *** [CMakeFiles/_tenseal_cpp.dir/build.make:212: CMakeFiles/_tenseal_cpp.dir/tenseal/cpp/tensors/bfvvector.cpp.o] Error 1 c++: fatal error: Killed signal terminated program cc1plus compilation terminated. gmake[2]: *** [CMakeFiles/_tenseal_cpp.dir/build.make:277: CMakeFiles/_tenseal_cpp.dir/tenseal/binding.cpp.o] Error 1 c++: fatal error: Killed signal terminated program cc1plus compilation terminated. gmake[2]: *** [CMakeFiles/_sealapi_cpp.dir/build.make:173: CMakeFiles/_sealapi_cpp.dir/tenseal/sealapi/sealapi_util_namespace.cpp.o] Error 1 gmake[1]: *** [CMakeFiles/Makefile2:452: CMakeFiles/_sealapi_cpp.dir/all] Error 2 gmake[1]: *** Waiting for unfinished jobs.... gmake[1]: *** [CMakeFiles/Makefile2:425: CMakeFiles/tenseal.dir/all] Error 2 gmake[1]: *** [CMakeFiles/Makefile2:397: CMakeFiles/_tenseal_cpp.dir/all] Error 2 gmake: *** [Makefile:149: all] Error 2 Traceback (most recent call last): File "/usr/local/lib/python3.11/site-packages/pip/_vendor/pyproject_hooks/_in_process/_in_process.py", line 389, in <module> main() File "/usr/local/lib/python3.11/site-packages/pip/_vendor/pyproject_hooks/_in_process/_in_process.py", line 373, in main json_out["return_val"] = hook(**hook_input["kwargs"]) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/usr/local/lib/python3.11/site-packages/pip/_vendor/pyproject_hooks/_in_process/_in_process.py", line 280, in build_wheel return _build_backend().build_wheel( ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/tmp/pip-build-env-rh2o3tra/overlay/lib/python3.11/site-packages/setuptools/build_meta.py", line 435, in build_wheel return _build(['bdist_wheel', '--dist-info-dir', str(metadata_directory)]) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/tmp/pip-build-env-rh2o3tra/overlay/lib/python3.11/site-packages/setuptools/build_meta.py", line 423, in _build return self._build_with_temp_dir( ^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/tmp/pip-build-env-rh2o3tra/overlay/lib/python3.11/site-packages/setuptools/build_meta.py", line 404, in _build_with_temp_dir self.run_setup() File "/tmp/pip-build-env-rh2o3tra/overlay/lib/python3.11/site-packages/setuptools/build_meta.py", line 512, in run_setup super().run_setup(setup_script=setup_script) File "/tmp/pip-build-env-rh2o3tra/overlay/lib/python3.11/site-packages/setuptools/build_meta.py", line 317, in run_setup exec(code, locals()) File "<string>", line 81, in <module> File "/tmp/pip-build-env-rh2o3tra/overlay/lib/python3.11/site-packages/setuptools/__init__.py", line 115, in setup return distutils.core.setup(**attrs) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/tmp/pip-build-env-rh2o3tra/overlay/lib/python3.11/site-packages/setuptools/_distutils/core.py", line 186, in setup return run_commands(dist) ^^^^^^^^^^^^^^^^^^ File "/tmp/pip-build-env-rh2o3tra/overlay/lib/python3.11/site-packages/setuptools/_distutils/core.py", line 202, in run_commands dist.run_commands() File "/tmp/pip-build-env-rh2o3tra/overlay/lib/python3.11/site-packages/setuptools/_distutils/dist.py", line 1002, in run_commands self.run_command(cmd) File "/tmp/pip-build-env-rh2o3tra/overlay/lib/python3.11/site-packages/setuptools/dist.py", line 1102, in run_command super().run_command(command) File "/tmp/pip-build-env-rh2o3tra/overlay/lib/python3.11/site-packages/setuptools/_distutils/dist.py", line 1021, in run_command cmd_obj.run() File "/tmp/pip-build-env-rh2o3tra/overlay/lib/python3.11/site-packages/setuptools/command/bdist_wheel.py", line 370, in run self.run_command("build") File "/tmp/pip-build-env-rh2o3tra/overlay/lib/python3.11/site-packages/setuptools/_distutils/cmd.py", line 357, in run_command self.distribution.run_command(command) File "/tmp/pip-build-env-rh2o3tra/overlay/lib/python3.11/site-packages/setuptools/dist.py", line 1102, in run_command super().run_command(command) File "/tmp/pip-build-env-rh2o3tra/overlay/lib/python3.11/site-packages/setuptools/_distutils/dist.py", line 1021, in run_command cmd_obj.run() File "/tmp/pip-build-env-rh2o3tra/overlay/lib/python3.11/site-packages/setuptools/_distutils/command/build.py", line 135, in run self.run_command(cmd_name) File "/tmp/pip-build-env-rh2o3tra/overlay/lib/python3.11/site-packages/setuptools/_distutils/cmd.py", line 357, in run_command self.distribution.run_command(command) File "/tmp/pip-build-env-rh2o3tra/overlay/lib/python3.11/site-packages/setuptools/dist.py", line 1102, in run_command super().run_command(command) File "/tmp/pip-build-env-rh2o3tra/overlay/lib/python3.11/site-packages/setuptools/_distutils/dist.py", line 1021, in run_command cmd_obj.run() File "<string>", line 46, in run File "<string>", line 78, in build_extension File "/usr/local/lib/python3.11/subprocess.py", line 413, in check_call raise CalledProcessError(retcode, cmd) subprocess.CalledProcessError: Command '['cmake', '--build', '.', '--config', 'Release', '--', '-j']' returned non-zero exit status 2. [end of output] note: This error originates from a subprocess, and is likely not a problem with pip. ERROR: Failed building wheel for tenseal error: failed-wheel-build-for-install × Failed to build installable wheels for some pyproject.toml based projects ╰─> tenseal
08-06
import json import pandas as pd from pandas import ExcelWriter import openpyxl from html import unescape import re from openpyxl.styles import Font, Alignment, Border, Side, PatternFill from openpyxl.utils import get_column_letter import requests import os import time import sys from datetime import datetime from PyQt5 import QtWidgets, QtCore, QtGui from PyQt5.QtWidgets import QApplication, QMainWindow, QFileDialog, QMessageBox, QStatusBar import logging from pptx import Presentation from pptx.util import Inches, Pt from pptx.enum.chart import XL_CHART_TYPE from pptx.dml.color import RGBColor from pptx.chart.data import ChartData import io import warnings from typing import List, Dict, Any, Optional # 配置日志 logging.basicConfig( level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', handlers=[ logging.FileHandler("ptl_tool.log", mode='w', encoding='utf-8'), logging.StreamHandler(sys.stdout) ] ) logger = logging.getLogger("PTL_Tool") # 加载配置文件 def load_config(): """加载配置文件""" config_path = "config.json" default_config = { "API": { "PROD_URL": "https://lims.se.com/STARLIMS11.prod/RESTServices.REST_API.Api_clab_testplan_v1.lims", "QUAL_URL": "https://qual.lims.se.com/STARLIMS11.qual/RESTServices.REST_API.Api_clab_testplan_v1.lims", "USER": "API_CN", "PASSWORD": "YLWT", "COOKIE": "ASP.NET_SessionId=nn30yrczlmvf31be5kvynwpq" }, "TEMPLATE_PATHS": [ os.path.join("api_responses", "Validation Plan Preface.xlsx"), "Validation Plan Preface.xlsx", os.path.join(os.path.dirname(__file__), "Validation Plan Preface.xlsx") ], "MAX_PROCESS_ROWS": 10000, "PPT_MAX_DISPLAY_ITEMS": 10 } if os.path.exists(config_path): try: with open(config_path, 'r', encoding='utf-8') as f: config = json.load(f) for key, value in default_config.items(): if key not in config: config[key] = value return config except Exception as e: logger.warning(f"加载配置文件失败,使用默认配置: {e}") return default_config # 全局配置 CONFIG = load_config() MAX_PROCESS_ROWS = CONFIG["MAX_PROCESS_ROWS"] def extract_text(html_content: Optional[str]) -> str: """从HTML内容中提取纯文本信息,保留换行符""" if not isinstance(html_content, str) or html_content.lower() == "null" or html_content.strip() == "": return "N/A" def process_line(line: str) -> str: line = line.replace('<br>', '\n').replace('<br />', '\n').replace('<br/>', '\n') line = re.sub(r'<[^>]*>', '', line) line = unescape(line) return re.sub(r'[ \t]+', ' ', line).strip() if len(html_content) > 10000: result = [] for chunk in [html_content[i:i + 1000] for i in range(0, len(html_content), 1000)]: result.append(process_line(chunk)) return ' '.join(result) else: return process_line(html_content) def process_comments(html_comments: str) -> dict: """处理特定格式的试验申请注释信息""" clean_text = extract_text(html_comments) lines = [line.strip() for line in clean_text.split('\n') if line.strip()] result = {} current_section = None max_lines = min(len(lines), 1000) for i in range(max_lines): line = lines[i] if ':' in line: parts = line.split(':', 1) if len(parts) == 2: key, value = parts key = key.strip() value = value.strip() if '(' in value and ')' in value: try: main_value, explanation = value.split('(', 1) result[key] = main_value.strip() result[f"{key}_说明"] = explanation.replace(')', '').strip() except ValueError: result[key] = value else: result[key] = value else: if current_section and line: result[current_section] += ' ' + line elif line: current_section = f"备注_{len([k for k in result if k.startswith('备注_')]) + 1}" result[current_section] = line return result def create_template_based_file(df: pd.DataFrame, output_file: str, test_plan_id: str) -> bool: """保留模板所有内容,在后面新增第三个工作表写入数据""" logger.info(f"开始基于模板创建文件:{output_file}") # 查找模板文件 template_path = None for path in CONFIG["TEMPLATE_PATHS"]: if os.path.exists(path): template_path = path logger.info(f"找到模板文件:{template_path}") break # 加载模板或创建新工作簿 if template_path and os.path.exists(template_path): try: workbook = openpyxl.load_workbook(template_path) logger.info(f"模板包含工作表:{workbook.sheetnames}") except Exception as e: logger.error(f"加载模板失败,创建新工作簿:{e}") workbook = openpyxl.Workbook() else: logger.warning("未找到Validation Plan模板,创建新工作簿") workbook = openpyxl.Workbook() try: sheet_name = "Validation Plan" # 删除已存在的同名工作表 if sheet_name in workbook.sheetnames: logger.warning(f"工作表 {sheet_name} 已存在,删除后重新创建") workbook.remove(workbook[sheet_name]) # 创建新工作表 worksheet = workbook.create_sheet(title=sheet_name) # 确保新工作表是第3个 while len(workbook.sheetnames) < 3: temp_sheet_name = f"Temp_Sheet_{len(workbook.sheetnames) + 1}" workbook.create_sheet(title=temp_sheet_name) logger.info(f"补充空工作表 {temp_sheet_name}") # 重新排序 sheet_names = workbook.sheetnames if sheet_name in sheet_names: sheet_names.remove(sheet_name) sheet_names.insert(2, sheet_name) workbook._sheets = [workbook[sheet] for sheet in sheet_names] logger.info(f"最终工作表顺序:{workbook.sheetnames}") # 写入表头 for col_num, header in enumerate(df.columns, 1): cell = worksheet.cell(row=1, column=col_num, value=header) cell.font = Font(name='Arial', size=10, bold=True) cell.fill = PatternFill(start_color='D7E4BC', end_color='D7E4BC', fill_type='solid') cell.alignment = Alignment(horizontal='left', vertical='center', wrap_text=True) # 分批写入数据,优化内存 batch_size = 1000 total_rows = len(df) for start_idx in range(0, total_rows, batch_size): end_idx = min(start_idx + batch_size, total_rows) batch_df = df.iloc[start_idx:end_idx] for row_num, (_, row) in enumerate(batch_df.iterrows(), start_idx + 2): for col_num, value in enumerate(row, 1): # 处理长文本限制 if isinstance(value, str) and len(value) > 32767: value = value[:32767] cell = worksheet.cell(row=row_num, column=col_num, value=value) cell.font = Font(name='Arial', size=10) cell.alignment = Alignment(horizontal='left', vertical='center', wrap_text=True) logger.info(f"已写入 {end_idx}/{total_rows} 行数据") # 设置列宽和边框 thin_border = Border( left=Side(style='thin'), right=Side(style='thin'), top=Side(style='thin'), bottom=Side(style='thin') ) for col_num in range(1, len(df.columns) + 1): column_letter = get_column_letter(col_num) col_name = df.columns[col_num - 1] # 计算列宽 max_length = max(df[col_name].astype(str).map(len).max(), len(col_name)) + 2 if col_name in ['Test Details', 'Sample Description', 'Referential']: max_length = max(max_length, 30) elif col_name == 'Comment': max_length = max(max_length, 25) elif col_name == 'Sample ID': max_length = max(max_length, 20) elif col_name == 'Title': max_length = max(max_length, 25) max_length = min(max_length, 50) worksheet.column_dimensions[column_letter].width = max_length # 设置边框 for row_num in range(1, total_rows + 2): worksheet.cell(row=row_num, column=col_num).border = thin_border # 调整行高 for row_num in range(2, total_rows + 2): worksheet.row_dimensions[row_num].height = 30 # 保存文件 workbook.save(output_file) logger.info(f"成功生成文件:{output_file}") return True except Exception as e: logger.error(f"生成文件失败:{str(e)}", exc_info=True) raise def api_to_excel(api_response: Any, output_file: Optional[str] = None, test_plan_id: Optional[str] = None) -> bool: """将API响应数据转换为Excel文件(修复批量JSON解析和TESTSPECS字段查找)""" logger.info("开始转换数据到Excel...") # 解析API响应 if isinstance(api_response, str): try: data = json.loads(api_response) except json.JSONDecodeError as e: logger.error(f"JSON解析错误: {e}") QMessageBox.critical(None, "错误", f"无法解析API数据:{str(e)}\n请确保数据是有效的JSON格式。") return False else: data = api_response # 修复:重新定义批量数据判断逻辑 # 批量数据格式:[{test_plan_id, data, timestamp}, ...] is_batch_format = isinstance(data, list) and len(data) > 0 and all( isinstance(item, dict) and 'data' in item and 'test_plan_id' in item for item in data ) merged_data = [] used_test_plan_id = "Combined" if is_batch_format: logger.info(f"检测到批量合并数据,共 {len(data)} 个Test Plan") for item_idx, item in enumerate(data): try: plan_id = item.get('test_plan_id', f'Unknown_{item_idx + 1}') plan_data = item.get('data', {}) # 查找TESTSPECS的可能位置 specs = None if 'TESTSPECS' in plan_data: specs = plan_data['TESTSPECS'] elif 'TestSpecs' in plan_data: # 兼容大小写 specs = plan_data['TestSpecs'] elif 'testspecs' in plan_data: specs = plan_data['testspecs'] elif isinstance(plan_data, dict) and len(plan_data) == 1: # 处理可能的嵌套结构 nested_key = next(iter(plan_data.keys())) nested_data = plan_data[nested_key] if isinstance(nested_data, dict) and 'TESTSPECS' in nested_data: specs = nested_data['TESTSPECS'] if not specs or not isinstance(specs, list): logger.warning(f"Test Plan {plan_id} 中未找到有效的TESTSPECS列表,跳过该数据") continue max_specs = min(len(specs), MAX_PROCESS_ROWS) logger.info(f"处理Test Plan: {plan_id}, 测试规格数: {max_specs}") for spec_idx, spec in enumerate(specs[:max_specs]): if not isinstance(spec, dict): logger.warning(f"跳过无效的测试规格数据(索引{spec_idx})") continue test_spec_id = spec.get('TESTSPEC_ID', 'N/A') samples = spec.get('SAMPLES', []) standards = [std.get('REFERENCE', 'N/A') for std in spec.get('STANDARDS', [])] referential = "\n".join(standards) if standards else "N/A" sample_count = spec.get('SAMPLE_COUNT', len(samples)) sample_ids = [] sample_descriptions = [] sample_maturities = [] if samples: for sample in samples: sample_ids.append(sample.get('SAMPLEID', 'N/A')) sample_descriptions.append(extract_text(sample.get('DESCRIPTION', 'N/A'))) sample_maturities.append(sample.get('MATURITY', 'N/A')) sample_id_text = "\n".join(sample_ids) if sample_ids else "N/A" sample_description_text = "\n".join(sample_descriptions) if sample_descriptions else "N/A" sample_maturity_text = "\n".join(sample_maturities) if sample_maturities else "N/A" test_details = spec.get('TEST_DETAILS', 'N/A') test_details = test_details.replace('<br>', '\n').replace('-->', '->') test_details = extract_text(test_details) comments = spec.get('COMMENTS', 'N/A') if comments and comments.lower() != "null": comments = extract_text(comments) row = { 'Test Plan': plan_id, 'Test Spec ID': test_spec_id, 'Status': spec.get('STATUS', 'N/A'), 'Global Status': spec.get('GLOBAL_STATUS', 'N/A'), 'Title': spec.get('TITLE', 'N/A'), 'Referential': referential, 'Acceptance Criteria': spec.get('ACCEPTANCE_CRITERIA', 'N/A'), 'Test Details': test_details, 'Comment': comments, 'Sample Number': sample_count, 'Sample ID': sample_id_text, 'Sample Description': sample_description_text, 'Sample Maturity': sample_maturity_text, 'Workload': spec.get('WORKLOAD', 'N/A') } merged_data.append(row) except Exception as e: logger.error(f"处理Test Plan {item.get('test_plan_id', 'Unknown')} 时出错: {e}", exc_info=True) continue else: # 单个Test Plan数据处理 logger.info("检测到单个Test Plan数据") # 查找TESTSPECS的可能位置 specs = None if 'TESTSPECS' in data: specs = data['TESTSPECS'] elif 'TestSpecs' in data: specs = data['TestSpecs'] elif 'testspecs' in data: specs = data['testspecs'] elif isinstance(data, dict) and len(data) == 1: nested_key = next(iter(data.keys())) nested_data = data[nested_key] if isinstance(nested_data, dict) and 'TESTSPECS' in nested_data: specs = nested_data['TESTSPECS'] if not specs or not isinstance(specs, list): logger.error(f"API数据格式错误,在以下位置均未找到TESTSPECS字段:") logger.error(f"- 顶层数据: {list(data.keys())[:10]}") if isinstance(data, dict) and len(data) == 1: nested_key = next(iter(data.keys())) logger.error( f"- 嵌套数据[{nested_key}]: {list(data[nested_key].keys())[:10] if isinstance(data[nested_key], dict) else type(data[nested_key])}") QMessageBox.critical(None, "错误", "API数据格式错误,未找到TESTSPECS字段。\n" "请检查JSON文件结构,确保包含TESTSPECS列表。\n" "批量数据格式应为:[{test_plan_id, data: {TESTSPECS: [...]}, timestamp}, ...]") return False # 获取Test Plan ID if test_plan_id: used_test_plan_id = test_plan_id else: used_test_plan_id = data.get('TESTPLAN_ID', data.get('TestPlanId', 'Unknown_Test_Plan')) used_test_plan_id = re.sub(r'[\\/:*?"<>|]', '_', used_test_plan_id) max_specs = min(len(specs), MAX_PROCESS_ROWS) logger.info(f"共发现 {len(specs)} 个测试规格,将处理前 {max_specs} 个") for i in range(max_specs): spec = specs[i] if not isinstance(spec, dict): logger.warning(f"跳过无效的测试规格数据(索引{i})") continue if i % 100 == 0: logger.info(f"处理测试规格 {i + 1}/{max_specs}") test_spec_id = spec.get('TESTSPEC_ID', 'N/A') samples = spec.get('SAMPLES', []) standards = [std.get('REFERENCE', 'N/A') for std in spec.get('STANDARDS', [])] referential = "\n".join(standards) if standards else "N/A" sample_count = spec.get('SAMPLE_COUNT', len(samples)) sample_ids = [] sample_descriptions = [] sample_maturities = [] if samples: for sample in samples: sample_ids.append(sample.get('SAMPLEID', 'N/A')) sample_descriptions.append(extract_text(sample.get('DESCRIPTION', 'N/A'))) sample_maturities.append(sample.get('MATURITY', 'N/A')) sample_id_text = "\n".join(sample_ids) if sample_ids else "N/A" sample_description_text = "\n".join(sample_descriptions) if sample_descriptions else "N/A" sample_maturity_text = "\n".join(sample_maturities) if sample_maturities else "N/A" test_details = spec.get('TEST_DETAILS', 'N/A') test_details = test_details.replace('<br>', '\n').replace('-->', '->') test_details = extract_text(test_details) comments = spec.get('COMMENTS', 'N/A') if comments and comments.lower() != "null": comments = extract_text(comments) row = { 'Test Plan': used_test_plan_id, 'Test Spec ID': test_spec_id, 'Status': spec.get('STATUS', 'N/A'), 'Global Status': spec.get('GLOBAL_STATUS', 'N/A'), 'Title': spec.get('TITLE', 'N/A'), 'Referential': referential, 'Acceptance Criteria': spec.get('ACCEPTANCE_CRITERIA', 'N/A'), 'Test Details': test_details, 'Comment': comments, 'Sample Number': sample_count, 'Sample ID': sample_id_text, 'Sample Description': sample_description_text, 'Sample Maturity': sample_maturity_text, 'Workload': spec.get('WORKLOAD', 'N/A') } merged_data.append(row) # 检查是否有有效数据 if not merged_data: logger.error("未找到任何有效的测试规格数据") QMessageBox.warning(None, "警告", "JSON文件中未找到任何有效的测试规格数据,请检查数据质量。") return False # 创建DataFrame df = pd.DataFrame(merged_data) # 定义列顺序 column_order = [ 'Test Plan', 'Test Spec ID', 'Status', 'Global Status', 'Title', 'Referential', 'Acceptance Criteria', 'Test Details', 'Comment', 'Sample Number', 'Sample ID', 'Sample Description', 'Sample Maturity', 'Workload' ] # 确保所有列都存在 for col in column_order: if col not in df.columns: df[col] = 'N/A' df = df[column_order] # 生成输出文件名 if not output_file: output_file = f"{used_test_plan_id}_PTL03.xlsx" # 基于模板创建文件 try: if create_template_based_file(df, output_file, used_test_plan_id): logger.info(f"成功生成文件:{output_file}") QMessageBox.information(None, "成功", f"文件已保存:{os.path.basename(output_file)}\n\n" f"已保留模板所有内容,数据已写入第3个工作表\n" f"共处理 {len(df)} 条测试规格数据") return True else: return False except Exception as e: logger.error(f"写入Excel文件时发生异常:{str(e)}", exc_info=True) QMessageBox.critical(None, "错误", f"写入Excel文件时发生异常:{str(e)}") return False def load_api_data_from_file(file_path: str) -> Optional[Any]: """从JSON文件加载API数据(增强日志输出)""" try: logger.info(f"从文件加载数据: {file_path}") with open(file_path, 'r', encoding='utf-8') as file: # 读取文件内容 file_content = file.read().strip() if not file_content: logger.error("文件为空") QMessageBox.warning(None, "错误", "选择的JSON文件为空") return None # 尝试解析JSON data = json.loads(file_content) # 增强日志:输出数据结构详情 if isinstance(data, list): logger.info(f"加载到批量数据,共 {len(data)} 项") if len(data) > 0: first_item = data[0] logger.info(f"第一项数据包含字段: {list(first_item.keys())}") if 'data' in first_item: data_fields = list(first_item['data'].keys())[:10] # 只显示前10个字段 logger.info(f"第一项data字段包含: {data_fields}...") if 'TESTSPECS' in first_item['data']: logger.info(f"找到TESTSPECS字段,共 {len(first_item['data']['TESTSPECS'])} 个测试规格") else: logger.warning(f"第一项data字段中未找到TESTSPECS") elif isinstance(data, dict): logger.info(f"加载到单个数据,包含字段: {list(data.keys())[:10]}...") if 'TESTSPECS' in data: logger.info(f"找到TESTSPECS字段,共 {len(data['TESTSPECS'])} 个测试规格") else: logger.warning(f"未找到TESTSPECS字段") else: logger.warning(f"加载到非预期数据类型: {type(data)}") return data except FileNotFoundError as e: logger.error(f"文件未找到: {file_path}") QMessageBox.warning(None, "错误", f"找不到文件 '{os.path.basename(file_path)}'") return None except json.JSONDecodeError as e: logger.error(f"JSON解析错误: {file_path}, 位置: {e.pos}, 原因: {e.msg}") QMessageBox.warning(None, "错误", f"文件 '{os.path.basename(file_path)}' 不是有效的JSON格式\n错误位置: {e.pos}\n原因: {e.msg}") return None except Exception as e: logger.error(f"读取文件时发生异常:{str(e)}", exc_info=True) QMessageBox.warning(None, "错误", f"读取文件时发生异常:{str(e)}") return None def fetch_testplan_metadata(test_plan_id: str, version: int = 1, use_prod: bool = True) -> dict: """从STARLIMS API获取测试计划的元数据(移除tenacity重试,改用简单重试)""" logger.info(f"获取测试计划元数据: {test_plan_id}, 版本: {version}") base_url = CONFIG["API"]["PROD_URL"] if use_prod else CONFIG["API"]["QUAL_URL"] url = f"{base_url}?TestPlanId={test_plan_id}&Version={version}" headers = { 'Content-Type': 'application/json', 'STARLIMSUser': CONFIG["API"]["USER"], 'STARLIMSPass': CONFIG["API"]["PASSWORD"], 'Cookie': CONFIG["API"]["COOKIE"] } max_attempts = 3 attempt = 0 while attempt < max_attempts: try: warnings.filterwarnings('ignore', message='Unverified HTTPS request') response = requests.get(url, headers=headers, verify=False, timeout=30) response.raise_for_status() if len(response.content) > 10 * 1024 * 1024: logger.info(f"处理大型响应数据({len(response.content) / 1024 / 1024:.1f}MB)") return response.json() except (requests.exceptions.ConnectionError, requests.exceptions.Timeout) as e: attempt += 1 logger.warning(f"请求失败(尝试 {attempt}/{max_attempts}): {e}") if attempt < max_attempts: time.sleep(2 * attempt) # 指数退避等待 else: logger.error(f"多次请求失败: {e}", exc_info=True) raise except requests.exceptions.RequestException as e: logger.error(f"请求错误: {e}", exc_info=True) error_msg = f"获取 {test_plan_id} 数据失败:{str(e)}" if "401" in str(e) or "Unauthorized" in str(e): QMessageBox.critical(None, "认证错误", "API认证失败,请检查配置") raise QMessageBox.warning(None, "请求错误", error_msg) raise except ValueError as e: logger.error(f"JSON解析错误: {e}", exc_info=True) QMessageBox.warning(None, "解析错误", f"解析 {test_plan_id} 数据失败:{str(e)}") raise def create_json_from_test_plan_id(): """从三个下拉菜单+手动输入框获取Test Plan ID,创建合并JSON和Excel文件""" logger.info("开始从Test Plan ID创建合并JSON和Excel文件") try: test_plan_ids = [] # 从下拉菜单获取 for combo in [ui.comboBox_1, ui.comboBox_2, ui.comboBox_3]: plan_text = combo.currentText().strip() if plan_text: plan_id = plan_text.split('(')[0].strip() test_plan_ids.append(plan_id) # 从手动输入框获取 manual_plan_text = ui.textEdit_1.toPlainText().strip() if manual_plan_text: manual_ids = [id.strip() for id in manual_plan_text.split(',') if id.strip()] test_plan_ids.extend(manual_ids) # 去重和过滤 test_plan_ids = list(set([id for id in test_plan_ids if id])) if not test_plan_ids: QMessageBox.critical(None, "输入错误", "请至少选择一个Test Plan ID或手动输入有效ID") return # 禁用按钮 ui.pushButton_JSON.setEnabled(False) ui.statusbar.showMessage(f"正在获取 {len(test_plan_ids)} 个测试计划的数据...") all_test_plan_data = [] success_count = 0 error_count = 0 # 生成输出文件名 timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") combined_base_name = f"Combined_{'_'.join(test_plan_ids[:3])}_{timestamp}" # 限制文件名长度 output_dir = "api_responses" os.makedirs(output_dir, exist_ok=True) combined_json_file = os.path.join(output_dir, f"{combined_base_name}.json") combined_excel_file = os.path.join(output_dir, f"{combined_base_name}_PTL_03.xlsx") combined_df = pd.DataFrame() # 逐个处理 for idx, test_plan_id in enumerate(test_plan_ids): try: ui.statusbar.showMessage(f"正在获取 {test_plan_id} 的数据... ({idx + 1}/{len(test_plan_ids)})") data = fetch_testplan_metadata(test_plan_id) all_test_plan_data.append({ "test_plan_id": test_plan_id, "data": data, "timestamp": datetime.now().isoformat() }) success_count += 1 # 处理数据用于Excel specs = data.get('TESTSPECS', []) if not specs: logger.warning(f"Test Plan {test_plan_id} 中未找到TESTSPECS字段") continue max_specs = min(len(specs), MAX_PROCESS_ROWS) merged_data = [] for i in range(max_specs): spec = specs[i] test_spec_id = spec.get('TESTSPEC_ID', 'N/A') samples = spec.get('SAMPLES', []) standards = [std.get('REFERENCE', 'N/A') for std in spec.get('STANDARDS', [])] referential = "\n".join(standards) if standards else "N/A" sample_count = spec.get('SAMPLE_COUNT', len(samples)) sample_ids = [] sample_descriptions = [] sample_maturities = [] if samples: for sample in samples: sample_ids.append(sample.get('SAMPLEID', 'N/A')) sample_descriptions.append(extract_text(sample.get('DESCRIPTION', 'N/A'))) sample_maturities.append(sample.get('MATURITY', 'N/A')) sample_id_text = "\n".join(sample_ids) if sample_ids else "N/A" sample_description_text = "\n".join(sample_descriptions) if sample_descriptions else "N/A" sample_maturity_text = "\n".join(sample_maturities) if sample_maturities else "N/A" test_details = spec.get('TEST_DETAILS', 'N/A') test_details = test_details.replace('<br>', '\n').replace('-->', '->') test_details = extract_text(test_details) comments = spec.get('COMMENTS', 'N/A') if comments and comments.lower() != "null": comments = extract_text(comments) row = { 'Test Plan': test_plan_id, 'Test Spec ID': test_spec_id, 'Status': spec.get('STATUS', 'N/A'), 'Global Status': spec.get('GLOBAL_STATUS', 'N/A'), 'Title': spec.get('TITLE', 'N/A'), 'Referential': referential, 'Acceptance Criteria': spec.get('ACCEPTANCE_CRITERIA', 'N/A'), 'Test Details': test_details, 'Comment': comments, 'Sample Number': sample_count, 'Sample ID': sample_id_text, 'Sample Description': sample_description_text, 'Sample Maturity': sample_maturity_text, 'Workload': spec.get('WORKLOAD', 'N/A') } merged_data.append(row) single_df = pd.DataFrame(merged_data) combined_df = pd.concat([combined_df, single_df], ignore_index=True) except Exception as e: logger.error(f"获取 {test_plan_id} 数据时发生异常: {str(e)}", exc_info=True) error_count += 1 QMessageBox.warning( None, "获取数据失败", f"获取 {test_plan_id} 数据时发生错误: {str(e)}\n将继续处理其他Test Plan ID。" ) # 保存合并JSON try: with open(combined_json_file, 'w', encoding='utf-8') as f: json.dump(all_test_plan_data, f, ensure_ascii=False, indent=2) logger.info(f"合并的JSON文件已保存: {combined_json_file}") except Exception as e: logger.error(f"创建合并JSON文件时出错: {str(e)}", exc_info=True) QMessageBox.warning(None, "合并JSON失败", f"创建合并JSON文件时发生错误: {str(e)}") # 保存合并Excel try: if not combined_df.empty: combined_test_plan_id = f"Combined_{'_'.join(test_plan_ids[:3])}" if create_template_based_file(combined_df, combined_excel_file, combined_test_plan_id): logger.info(f"合并的Excel文件已保存: {combined_excel_file}") else: logger.warning("没有可保存的Excel数据") QMessageBox.warning(None, "警告", "没有找到有效的测试规格数据,无法生成Excel文件") except Exception as e: logger.error(f"创建合并Excel文件时出错: {str(e)}", exc_info=True) QMessageBox.warning(None, "合并Excel失败", f"创建合并Excel文件时发生错误: {str(e)}") # 显示结果 result_msg = (f"处理完成!\n" f"成功获取 {success_count} 个Test Plan ID的数据\n" f"获取失败 {error_count} 个Test Plan ID\n\n" f"合并的JSON文件:{os.path.basename(combined_json_file)}\n" f"合并的Excel文件:{os.path.basename(combined_excel_file) if os.path.exists(combined_excel_file) else '未生成'}\n" f"文件保存路径:{output_dir}") ui.statusbar.showMessage("处理完成") QMessageBox.information(None, "处理结果", result_msg) except Exception as e: logger.error(f"创建合并文件时发生异常: {str(e)}", exc_info=True) QMessageBox.critical(None, "错误", f"获取数据时发生异常: {str(e)}") ui.statusbar.showMessage("获取数据失败") finally: ui.pushButton_JSON.setEnabled(True) def create_excel_from_json(ptl_type="PTL_03"): """修复后的从JSON创建Excel函数""" logger.info(f"开始从JSON文件创建{ptl_type}格式Excel") try: # 选择文件 file_path, _ = QFileDialog.getOpenFileName( None, f"选择JSON文件 - {ptl_type}", "", "JSON文件 (*.json);;所有文件 (*)" ) if not file_path: logger.info("用户取消了文件选择") return # 禁用按钮 ui.pushButton_EXCEL03.setEnabled(False) ui.statusbar.showMessage(f"正在加载并处理{ptl_type}文件...") # 加载JSON数据(增强日志) data = load_api_data_from_file(file_path) if data is None: ui.statusbar.showMessage("加载JSON文件失败") return # 生成Excel文件名 excel_file = f"{os.path.splitext(file_path)[0]}_{ptl_type}.xlsx" # 转换为Excel(修复后的解析逻辑) if api_to_excel(data, excel_file): ui.statusbar.showMessage(f"{ptl_type}文件处理完成") logger.info(f"{ptl_type}文件已保存: {excel_file}") else: ui.statusbar.showMessage(f"{ptl_type}文件转换失败") logger.error(f"{ptl_type}文件转换失败") except Exception as e: logger.error(f"创建{ptl_type}文件时发生异常: {str(e)}", exc_info=True) QMessageBox.showerror("错误", f"处理{ptl_type}文件时发生异常: {str(e)}") ui.statusbar.showMessage(f"{ptl_type}文件处理失败") finally: ui.pushButton_EXCEL03.setEnabled(True) def create_slides_from_ptl03(): """生成包含验证结果的幻灯片""" logger.info("开始创建幻灯片") try: file_path, _ = QFileDialog.getOpenFileName( None, "选择PTL_03文件", "", "Excel文件 (*.xlsx);;所有文件 (*)" ) if not file_path: logger.info("用户取消了文件选择") return ui.pushButton_Slides.setEnabled(False) ui.statusbar.showMessage("正在创建幻灯片...") try: excel_file = pd.ExcelFile(file_path) sheet_names = excel_file.sheet_names logger.info(f"Excel文件包含工作表:{sheet_names}") if len(sheet_names) < 3: df = pd.read_excel(file_path, sheet_name=-1) logger.warning(f"工作表不足3个,读取最后一个工作表:{sheet_names[-1]}") QMessageBox.warning(None, "工作表兼容处理", f"Excel文件只有 {len(sheet_names)} 个工作表\n已自动读取最后一个工作表的数据") else: df = pd.read_excel(file_path, sheet_name=2) logger.info(f"成功读取第3个工作表:{sheet_names[2]},共 {len(df)} 行数据") except Exception as e: logger.error(f"读取Excel文件失败: {str(e)}", exc_info=True) QMessageBox.critical(None, "错误", f"读取Excel文件失败:{str(e)}") return # 数据验证 required_cols = ['Global Status', 'Title'] missing_cols = [col for col in required_cols if col not in df.columns] if missing_cols: raise ValueError(f"Excel文件缺少必要列:{', '.join(missing_cols)}") # 数据预处理 df['Global Status'] = df['Global Status'].fillna('Unfinished').astype(str).str.strip() total_test = len(df) passed_count = len(df[df['Global Status'] == 'Passed']) failed_count = len(df[df['Global Status'] == 'Non-Compliant']) ongoing_count = len(df[df['Global Status'] == 'Unfinished']) failed_titles = df[df['Global Status'] == 'Non-Compliant']['Title'].dropna().tolist() ongoing_titles = df[df['Global Status'] == 'Unfinished']['Title'].dropna().tolist() # 创建演示文稿 prs = Presentation() # 标题页 title_slide_layout = prs.slide_layouts[0] slide = prs.slides.add_slide(title_slide_layout) title = slide.shapes.title title.text = "Summary of Validation" title_para = title.text_frame.paragraphs[0] title_para.font.size = Pt(32) title_para.font.name = 'Arial' title_para.font.bold = True subtitle = slide.placeholders[1] subtitle.text = f"Based on {os.path.basename(file_path)}\n{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}" for para in subtitle.text_frame.paragraphs: para.font.size = Pt(14) para.font.name = 'Arial' # 统计+饼图页 slide_layout = prs.slide_layouts[5] slide = prs.slides.add_slide(slide_layout) slide_title = slide.shapes.title slide_title.text = "Validation Results" slide_title_para = slide_title.text_frame.paragraphs[0] slide_title_para.font.size = Pt(24) slide_title_para.font.name = 'Arial' slide_title_para.font.bold = True # 饼图 left = Inches(0.5) top = Inches(2) width = Inches(3.5) height = Inches(4.5) status_counts = df['Global Status'].value_counts().reset_index() status_counts.columns = ['Status', 'Count'] chart_data = ChartData() chart_data.categories = list(status_counts['Status']) chart_data.add_series('Test Status', tuple(status_counts['Count'])) chart = slide.shapes.add_chart( XL_CHART_TYPE.PIE, left, top, width, height, chart_data ).chart # 饼图样式 color_map = { 'Passed': RGBColor(155, 187, 89), 'Non-Compliant': RGBColor(192, 80, 77), 'Unfinished': RGBColor(255, 192, 0) } for i, status in enumerate(status_counts['Status']): point = chart.series[0].points[i] point.format.fill.solid() point.format.fill.fore_color.rgb = color_map.get(status, RGBColor(128, 100, 162)) label_text = f"{status}: {status_counts.iloc[i]['Count']} ({(status_counts.iloc[i]['Count'] / total_test) * 100:.1f}%)" font_size = max(4, min(8, 60 // len(label_text))) label = point.data_label.text_frame.paragraphs[0] label.text = label_text label.font.size = Pt(font_size) label.font.name = 'Arial' # 统计文字框 text_left = Inches(4.5) text_top = Inches(1.5) text_width = Inches(5.5) text_height = Inches(6.5) stats_text = f"""Total test item: {total_test} Passed test item: {passed_count} Failed test item: {failed_count} On-going test item: {ongoing_count} Failed test please see below: """ if failed_titles: for title in failed_titles[:CONFIG["PPT_MAX_DISPLAY_ITEMS"]]: if len(title) > 80: title = title[:80] + "..." stats_text += f"- {title}\n" if len(failed_titles) > CONFIG["PPT_MAX_DISPLAY_ITEMS"]: stats_text += f"- ... and {len(failed_titles) - CONFIG['PPT_MAX_DISPLAY_ITEMS']} more\n" else: stats_text += "No failed test\n" stats_text += "\nOn-going test please see below:\n" if ongoing_titles: for title in ongoing_titles[:CONFIG["PPT_MAX_DISPLAY_ITEMS"]]: if len(title) > 80: title = title[:80] + "..." stats_text += f"- {title}\n" if len(ongoing_titles) > CONFIG["PPT_MAX_DISPLAY_ITEMS"]: stats_text += f"- ... and {len(ongoing_titles) - CONFIG['PPT_MAX_DISPLAY_ITEMS']} more\n" else: stats_text += "No on-going test\n" # 添加文字框 text_box = slide.shapes.add_textbox(text_left, text_top, text_width, text_height) text_frame = text_box.text_frame text_frame.word_wrap = True paragraph = text_frame.add_paragraph() paragraph.text = stats_text paragraph.font.size = Pt(11) paragraph.font.name = 'Arial' paragraph.space_after = Pt(2) # 保存文件 base_path = os.path.splitext(file_path)[0] slides_file = f"{base_path}_Slides.pptx" if os.path.exists(slides_file): timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") slides_file = f"{base_path}_Slides_{timestamp}.pptx" try: prs.save(slides_file) logger.info(f"幻灯片已保存到: {slides_file}") QMessageBox.information(None, "成功", f"幻灯片已保存:\n{slides_file}") ui.statusbar.showMessage(f"幻灯片已保存: {os.path.basename(slides_file)}") except PermissionError: temp_dir = os.path.join(os.environ.get('TEMP', '/tmp'), 'ptl_slides') os.makedirs(temp_dir, exist_ok=True) slides_file = os.path.join(temp_dir, os.path.basename(slides_file)) prs.save(slides_file) logger.warning(f"原路径无权限,已保存到临时目录: {slides_file}") QMessageBox.information(None, "保存成功", f"幻灯片已保存到:\n{slides_file}\n(原路径无写入权限)") except Exception as e: logger.error(f"创建幻灯片时出错: {str(e)}", exc_info=True) QMessageBox.critical(None, "错误", f"创建幻灯片失败: {str(e)}") ui.statusbar.showMessage("创建幻灯片失败") finally: ui.pushButton_Slides.setEnabled(True) # 全局异常处理 def except_hook(cls, exception, traceback): logger.error(f"未处理的异常: {exception}", exc_info=(cls, exception, traceback)) sys.__excepthook__(cls, exception, traceback) QMessageBox.critical(None, "程序错误", f"发生未处理的异常:\n{str(exception)}") sys.excepthook = except_hook # 创建主窗口 app = QApplication(sys.argv) MainWindow = QMainWindow() # UI类定义 class Ui_MainWindow(object): def setupUi(self, MainWindow): MainWindow.setObjectName("MainWindow") MainWindow.resize(1500, 950) self.centralwidget = QtWidgets.QWidget(MainWindow) self.centralwidget.setObjectName("centralwidget") self.pushButton_JSON = QtWidgets.QPushButton(self.centralwidget) self.pushButton_JSON.setGeometry(QtCore.QRect(950, 90, 400, 50)) font = QtGui.QFont() font.setPointSize(16) self.pushButton_JSON.setFont(font) self.pushButton_JSON.setObjectName("pushButton_JSON") self.label_JSON = QtWidgets.QLabel(self.centralwidget) self.label_JSON.setGeometry(QtCore.QRect(150, 90, 290, 50)) font = QtGui.QFont() font.setPointSize(16) self.label_JSON.setFont(font) self.label_JSON.setObjectName("label_JSON") self.pushButton_EXCEL03 = QtWidgets.QPushButton(self.centralwidget) self.pushButton_EXCEL03.setGeometry(QtCore.QRect(950, 450, 400, 50)) font = QtGui.QFont() font.setPointSize(16) self.pushButton_EXCEL03.setFont(font) self.pushButton_EXCEL03.setObjectName("pushButton_EXCEL03") self.label_EXCEL03 = QtWidgets.QLabel(self.centralwidget) self.label_EXCEL03.setGeometry(QtCore.QRect(150, 450, 400, 50)) font = QtGui.QFont() font.setPointSize(16) self.label_EXCEL03.setFont(font) self.label_EXCEL03.setObjectName("label_EXCEL03") self.label_Slides = QtWidgets.QLabel(self.centralwidget) self.label_Slides.setGeometry(QtCore.QRect(150, 550, 400, 50)) font = QtGui.QFont() font.setPointSize(16) self.label_Slides.setFont(font) self.label_Slides.setToolTipDuration(-1) self.label_Slides.setObjectName("label_Slides") self.pushButton_Slides = QtWidgets.QPushButton(self.centralwidget) self.pushButton_Slides.setGeometry(QtCore.QRect(950, 550, 400, 50)) font = QtGui.QFont() font.setPointSize(16) self.pushButton_Slides.setFont(font) self.pushButton_Slides.setObjectName("pushButton_Slides") self.label_Instruction = QtWidgets.QLabel(self.centralwidget) self.label_Instruction.setGeometry(QtCore.QRect(140, 710, 1200, 150)) font = QtGui.QFont() font.setPointSize(16) self.label_Instruction.setFont(font) self.label_Instruction.setToolTipDuration(-1) self.label_Instruction.setObjectName("label_Instruction") self.comboBox_1 = QtWidgets.QComboBox(self.centralwidget) self.comboBox_1.setGeometry(QtCore.QRect(500, 150, 400, 50)) font = QtGui.QFont() font.setPointSize(12) self.comboBox_1.setFont(font) self.comboBox_1.setObjectName("comboBox_1") self.comboBox_1.addItem("") self.comboBox_1.setItemText(0, "") self.comboBox_1.addItem("") self.comboBox_1.addItem("") self.comboBox_1.addItem("") self.comboBox_1.addItem("") self.comboBox_1.addItem("") self.comboBox_1.addItem("") self.comboBox_1.addItem("") self.comboBox_1.addItem("") self.comboBox_1.addItem("") self.comboBox_1.addItem("") self.comboBox_1.addItem("") self.comboBox_1.addItem("") self.comboBox_2 = QtWidgets.QComboBox(self.centralwidget) self.comboBox_2.setGeometry(QtCore.QRect(500, 210, 400, 50)) font = QtGui.QFont() font.setPointSize(12) self.comboBox_2.setFont(font) self.comboBox_2.setObjectName("comboBox_2") self.comboBox_2.addItem("") self.comboBox_2.setItemText(0, "") self.comboBox_2.addItem("") self.comboBox_2.addItem("") self.comboBox_2.addItem("") self.comboBox_2.addItem("") self.comboBox_2.addItem("") self.comboBox_2.addItem("") self.comboBox_2.addItem("") self.comboBox_2.addItem("") self.comboBox_2.addItem("") self.comboBox_2.addItem("") self.comboBox_2.addItem("") self.comboBox_2.addItem("") self.comboBox_3 = QtWidgets.QComboBox(self.centralwidget) self.comboBox_3.setGeometry(QtCore.QRect(500, 270, 400, 50)) font = QtGui.QFont() font.setPointSize(12) self.comboBox_3.setFont(font) self.comboBox_3.setObjectName("comboBox_3") self.comboBox_3.addItem("") self.comboBox_3.setItemText(0, "") self.comboBox_3.addItem("") self.comboBox_3.addItem("") self.comboBox_3.addItem("") self.comboBox_3.addItem("") self.comboBox_3.addItem("") self.comboBox_3.addItem("") self.comboBox_3.addItem("") self.comboBox_3.addItem("") self.comboBox_3.addItem("") self.comboBox_3.addItem("") self.comboBox_3.addItem("") self.comboBox_3.addItem("") self.textEdit_1 = QtWidgets.QTextEdit(self.centralwidget) self.textEdit_1.setGeometry(QtCore.QRect(500, 90, 400, 50)) font = QtGui.QFont() font.setPointSize(12) self.textEdit_1.setFont(font) self.textEdit_1.setObjectName("textEdit_1") # 添加进度条 self.progress_bar = QtWidgets.QProgressBar(self.centralwidget) self.progress_bar.setGeometry(QtCore.QRect(150, 870, 1200, 30)) self.progress_bar.setObjectName("progress_bar") self.progress_bar.setRange(0, 100) self.progress_bar.setVisible(False) self.progress_bar.setStyleSheet(""" QProgressBar { border: 2px solid #bbb; border-radius: 5px; text-align: center; } QProgressBar::chunk { background-color: #87CEEB; width: 20px; } """) MainWindow.setCentralWidget(self.centralwidget) self.menubar = QtWidgets.QMenuBar(MainWindow) self.menubar.setGeometry(QtCore.QRect(0, 0, 1500, 22)) self.menubar.setObjectName("menubar") MainWindow.setMenuBar(self.menubar) self.statusbar = QtWidgets.QStatusBar(MainWindow) self.statusbar.setObjectName("statusbar") MainWindow.setStatusBar(self.statusbar) self.retranslateUi(MainWindow) QtCore.QMetaObject.connectSlotsByName(MainWindow) def retranslateUi(self, MainWindow): _translate = QtCore.QCoreApplication.translate MainWindow.setWindowTitle(_translate("MainWindow", "PTL_Tool")) self.pushButton_JSON.setText(_translate("MainWindow", "Create a JSON File")) self.label_JSON.setText(_translate("MainWindow", "Test Plan ID:")) self.pushButton_EXCEL03.setText(_translate("MainWindow", "Create Validation Plan")) self.label_EXCEL03.setText(_translate("MainWindow", "Choose a JSON File:")) self.label_Slides.setText(_translate("MainWindow", "Choose Validation Plan:")) self.pushButton_Slides.setText(_translate("MainWindow", "Create Slides")) self.label_Instruction.setText(_translate("MainWindow", "<html><head/><body><p>Instruction: </p><p>1. All functions have been launched.</p></body></html>")) # 下拉菜单选项 self.comboBox_1.setItemText(1, _translate("MainWindow", "PLAN25AA0120 (ACB)")) self.comboBox_1.setItemText(2, _translate("MainWindow", "PLAN25AA0029 (MCCB)")) self.comboBox_1.setItemText(3, _translate("MainWindow", "PLAN25AA0226 (MCB)")) self.comboBox_1.setItemText(4, _translate("MainWindow", "PLAN25AAxxxx (Switch)")) self.comboBox_1.setItemText(5, _translate("MainWindow", "PLAN25AA0103 (Contactor)")) self.comboBox_1.setItemText(6, _translate("MainWindow", "PLAN25AAxxxx (Relay)")) self.comboBox_1.setItemText(7, _translate("MainWindow", "PLAN25AAxxxx (GV)")) self.comboBox_1.setItemText(8, _translate("MainWindow", "PLAN25AAxxxx (ATS)")) self.comboBox_1.setItemText(9, _translate("MainWindow", "PLAN25AA0167 (RCBO IEC)")) self.comboBox_1.setItemText(10, _translate("MainWindow", "PLAN25AA0062 (SPD)")) self.comboBox_1.setItemText(11, _translate("MainWindow", "PLAN25AAxxxx (OUPA)")) self.comboBox_1.setItemText(12, _translate("MainWindow", "PLAN25AAxxxx (AFDD)")) self.comboBox_2.setItemText(1, _translate("MainWindow", "PLAN25AA0120 (ACB)")) self.comboBox_2.setItemText(2, _translate("MainWindow", "PLAN25AA0029 (MCCB)")) self.comboBox_2.setItemText(3, _translate("MainWindow", "PLAN25AA0226 (MCB)")) self.comboBox_2.setItemText(4, _translate("MainWindow", "PLAN25AAxxxx (Switch)")) self.comboBox_2.setItemText(5, _translate("MainWindow", "PLAN25AA0103 (Contactor)")) self.comboBox_2.setItemText(6, _translate("MainWindow", "PLAN25AAxxxx (Relay)")) self.comboBox_2.setItemText(7, _translate("MainWindow", "PLAN25AAxxxx (GV)")) self.comboBox_2.setItemText(8, _translate("MainWindow", "PLAN25AAxxxx (ATS)")) self.comboBox_2.setItemText(9, _translate("MainWindow", "PLAN25AA0167 (RCBO IEC)")) self.comboBox_2.setItemText(10, _translate("MainWindow", "PLAN25AA0062 (SPD)")) self.comboBox_2.setItemText(11, _translate("MainWindow", "PLAN25AAxxxx (OUPA)")) self.comboBox_2.setItemText(12, _translate("MainWindow", "PLAN25AAxxxx (AFDD)")) self.comboBox_3.setItemText(1, _translate("MainWindow", "PLAN25AA0120 (ACB)")) self.comboBox_3.setItemText(2, _translate("MainWindow", "PLAN25AA0029 (MCCB)")) self.comboBox_3.setItemText(3, _translate("MainWindow", "PLAN25AA0226 (MCB)")) self.comboBox_3.setItemText(4, _translate("MainWindow", "PLAN25AAxxxx (Switch)")) self.comboBox_3.setItemText(5, _translate("MainWindow", "PLAN25AA0103 (Contactor)")) self.comboBox_3.setItemText(6, _translate("MainWindow", "PLAN25AAxxxx (Relay)")) self.comboBox_3.setItemText(7, _translate("MainWindow", "PLAN25AAxxxx (GV)")) self.comboBox_3.setItemText(8, _translate("MainWindow", "PLAN25AAxxxx (ATS)")) self.comboBox_3.setItemText(9, _translate("MainWindow", "PLAN25AA0167 (RCBO IEC)")) self.comboBox_3.setItemText(10, _translate("MainWindow", "PLAN25AA0062 (SPD)")) self.comboBox_3.setItemText(11, _translate("MainWindow", "PLAN25AAxxxx (OUPA)")) self.comboBox_3.setItemText(12, _translate("MainWindow", "PLAN25AAxxxx (AFDD)")) self.textEdit_1.setPlaceholderText(_translate("MainWindow", "PLAN25AA****(多个ID用逗号分隔)")) # 初始化UI ui = Ui_MainWindow() ui.setupUi(MainWindow) # 连接信号和槽 ui.pushButton_JSON.clicked.connect(create_json_from_test_plan_id) ui.pushButton_EXCEL03.clicked.connect(lambda: create_excel_from_json("PTL_03")) ui.pushButton_Slides.clicked.connect(create_slides_from_ptl03) # 显示主窗口 MainWindow.show() # 运行主循环 if __name__ == "__main__": if len(sys.argv) == 1: sys.exit(app.exec_()) else: if len(sys.argv) < 2: print("错误:请提供JSON文件路径作为参数") sys.exit(1) json_file = sys.argv[1] if not json_file.lower().endswith('.json'): print(f"错误:文件 '{json_file}' 不是有效的JSON文件") sys.exit(1) api_data = load_api_data_from_file(json_file) if api_data is None: print("程序退出:无法加载数据") sys.exit(1) result = api_to_excel(api_data) if result: print("操作成功完成") sys.exit(0) else: print("程序退出:处理数据时发生错误") sys.exit(1)
12-03
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值