初始提交:QuickTeam 工单接口导入工具

用「WCF + 通用存储过程网关」接口替代 Selenium(D://Learning//Python//工单自动化)导入工单:
单 Sprint 12 个项目 175s 跑完(旧方案 25~35 分钟),失败可断点续跑且不重复建单。

- 分层:transport(HTTP/协议)/ api(存储过程语义)/ domain(纯规则)/
  workflow(阶段编排)/ store(SQLite 台账)/ report / CLI
- 幂等:台账驱动断点续跑;dry-run 与跳过行不污染进度;--rollback-on-fail 清理半成品
- 命令:plan / apply / report / status,真实写入需 --yes
- tools:只读探针、存储过程源码导出(sp_helptext)、导入诊断
- docs:调研与设计文档(含联调踩到的 5 个坑)+ 6 个关键存储过程源码

实测:Sprint 2026 Sprint 10 的 12 个项目全部完成建单→导入→确认→推送 TFS,
工单 225134–225145,35 个功能模块,重跑 plan 全部 skip-pushed。69 个单测通过。
This commit is contained in:
chenshoulin
2026-11-05 19:14:25 +08:00
commit 58850314cf
32 changed files with 4590 additions and 0 deletions
+109
View File
@@ -0,0 +1,109 @@
"""诊断工具:定位 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())