"""诊断工具:定位 BatchImportFeature 空引用异常。 只操作一张临时草稿工单,结束时删除,不影响业务数据。 用法: python tools/diag_import.py # 建单→填单→读字段→试导入→删单 python tools/diag_import.py --keep # 保留草稿(便于在页面上比对) """ from __future__ import annotations import argparse import json import sys sys.path.insert(0, ".") from qtimport.api import CONN_QUICKTEAM, SP_IMPORT_EXCEL, SP_WORKSHEET, QuickTeamApi # noqa: E402 from qtimport.config import Config # noqa: E402 from qtimport.transport import Transport # noqa: E402 def dump(title: str, rows) -> dict: print(f"\n=== {title} ===") if isinstance(rows, list) and rows: print(json.dumps(rows[0], ensure_ascii=False, indent=2)) return rows[0] print(json.dumps(rows, ensure_ascii=False, indent=2)) return {} def main() -> int: parser = argparse.ArgumentParser() parser.add_argument("--config", default="config/config.json") parser.add_argument("--project", default="资管 - 华泰保险资管") parser.add_argument("--excel", default=r"D:\Work\QuickTeam\2026 Sprint 10\资管 - 华泰保险资管.xlsx") parser.add_argument("--keep", action="store_true") args = parser.parse_args() cfg = Config.load(args.config) t = Transport(cfg.base_url, timeout=cfg.timeout, retries=0) api = QuickTeamApi(t) user = api.login(cfg.username, cfg.password) team = api.resolve_team(api.teams(user.user_id), cfg.team_name) project = next(p for p in api.projects(team.team_id) if p.project_name == args.project) print(f"登录:{user.real_name};项目:{project.project_name}") worksheet_id = api.create_worksheet() print(f"草稿工单:{worksheet_id}") try: dump("建单后", t.exec_table(CONN_QUICKTEAM, SP_WORKSHEET, [("WorkSheetId", worksheet_id, "int")], op="raw")) evaluator, technical, customer = cfg.resolved_roles(user.real_name) print(f"角色:评估={evaluator} 技术={technical} 客户={customer}") result = api.fill_worksheet( worksheet_id, project, system_name=cfg.sprint, start_time=cfg.start_time, end_time=cfg.end_time, evaluator=evaluator, technical_leader=technical, customer_leader=customer, belong_team=cfg.belong_team, content=cfg.content_desc, operator=user.real_name, service_type=cfg.service_type, ) print(f"填单 Result={result}") after = dump("填单后", t.exec_table(CONN_QUICKTEAM, SP_WORKSHEET, [("WorkSheetId", worksheet_id, "int")], op="raw")) print(f"CurrentVersionId={after.get('CurrentVersionId')!r} VersionCode={after.get('VersionCode')!r}") remote = api.upload_excel(args.excel) print(f"\n上传成功:{remote}") # 逐个尝试参数组合,看哪种不报空引用 variants = [ ("VersionId=0", [("WorkSheetId", worksheet_id, "int"), ("VersionId", 0, "int")]), ("VersionId=-1", [("WorkSheetId", worksheet_id, "int"), ("VersionId", -1, "int")]), ("VersionId 省略", [("WorkSheetId", worksheet_id, "int")]), ("VersionId=空串", [("WorkSheetId", worksheet_id, "int"), ("VersionId", "", "string")]), ] for label, params in variants: print(f"\n--- 尝试 {label} ---") try: out = t.batch_import(SP_IMPORT_EXCEL, params, remote) print("返回:", json.dumps(out, ensure_ascii=False)) if isinstance(out, list) and out and isinstance(out[0], dict) and int(out[0].get("Column1", -1)) == 0: print(f"✔ {label} 成功") break except Exception as error: # noqa: BLE001 print(f"✘ {type(error).__name__}: {error}") dump("导入后", t.exec_table(CONN_QUICKTEAM, SP_WORKSHEET, [("WorkSheetId", worksheet_id, "int")], op="raw")) feats = api.features(worksheet_id) print(f"功能模块数量:{len(feats)}") for f in feats[:3]: print(" -", f.feature_id, f.code, f.name) finally: if args.keep: print(f"\n草稿保留:{worksheet_id}(请手工核对后删除)") else: try: api.delete_worksheet(worksheet_id) print(f"\n已删除草稿 {worksheet_id}") except Exception as error: # noqa: BLE001 print(f"\n删除失败:{error}(请手工删除 {worksheet_id})") return 0 if __name__ == "__main__": raise SystemExit(main())