初始提交: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:
@@ -0,0 +1,93 @@
|
||||
"""配置加载。
|
||||
|
||||
凭据解析优先级:环境变量 QT_PASSWORD > Windows DPAPI 文件 > 配置文件明文 > 交互输入。
|
||||
配置文件默认 config/config.json(已在 .gitignore 中)。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import getpass
|
||||
import json
|
||||
import os
|
||||
from dataclasses import dataclass, field
|
||||
from pathlib import Path
|
||||
|
||||
DEFAULT_CONFIG = "config/config.json"
|
||||
|
||||
|
||||
@dataclass
|
||||
class Config:
|
||||
# 连接
|
||||
base_url: str
|
||||
username: str
|
||||
password: str = field(default="", repr=False)
|
||||
timeout: int = 60
|
||||
# 业务
|
||||
team_name: str = "当前项目"
|
||||
work_dir: str = ""
|
||||
sprint: str = ""
|
||||
start_time: str = ""
|
||||
end_time: str = ""
|
||||
content_desc: str = "需求开发测试"
|
||||
service_type: str = "DevelopService"
|
||||
belong_team: str = ""
|
||||
# 三个责任人;留空则用登录用户的真实姓名
|
||||
evaluator: str = ""
|
||||
technical_leader: str = ""
|
||||
customer_leader: str = ""
|
||||
# TFS
|
||||
tfs_project: str = "Tools_Dev"
|
||||
tfs_team: str = "DataTechnologies"
|
||||
area_path: str = ""
|
||||
iteration_path: str = ""
|
||||
# 运行
|
||||
workers: int = 2
|
||||
db_path: str = "state/qtimport.db"
|
||||
|
||||
def resolved_roles(self, operator: str) -> tuple[str, str, str]:
|
||||
"""三个责任人,缺省都落到操作人(与现有脚本一致)。"""
|
||||
return (
|
||||
self.evaluator or operator,
|
||||
self.technical_leader or operator,
|
||||
self.customer_leader or operator,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def load(cls, path: str | os.PathLike = DEFAULT_CONFIG, *, need_password: bool = True) -> "Config":
|
||||
file_path = Path(path)
|
||||
if not file_path.exists():
|
||||
raise SystemExit(f"缺少配置文件:{file_path}(可从 config/config.example.json 复制)")
|
||||
with open(file_path, encoding="utf-8-sig") as handle:
|
||||
raw = json.load(handle)
|
||||
|
||||
allowed = {f for f in cls.__dataclass_fields__}
|
||||
unknown = set(raw) - allowed
|
||||
if unknown:
|
||||
raise SystemExit(f"配置文件含未知字段:{sorted(unknown)}")
|
||||
cfg = cls(**raw)
|
||||
|
||||
cfg.password = resolve_password(cfg.password, need=need_password)
|
||||
if not cfg.base_url or not cfg.username:
|
||||
raise SystemExit("配置缺少 base_url 或 username")
|
||||
if need_password and not cfg.password:
|
||||
raise SystemExit("未提供密码:设置 QT_PASSWORD 环境变量或在配置中填写")
|
||||
return cfg
|
||||
|
||||
|
||||
def resolve_password(configured: str, *, need: bool = True) -> str:
|
||||
env = os.environ.get("QT_PASSWORD")
|
||||
if env:
|
||||
return env
|
||||
if configured and configured != "REPLACE_ME":
|
||||
return configured
|
||||
if not need:
|
||||
return ""
|
||||
if not os.isatty(0): # 非交互(计划任务/CI)
|
||||
return ""
|
||||
return getpass.getpass("QuickTeam 密码:")
|
||||
|
||||
|
||||
def load_raw(path: str | os.PathLike = DEFAULT_CONFIG) -> dict:
|
||||
"""读原始 JSON(plan 阶段不需要密码时用)。"""
|
||||
with open(Path(path), encoding="utf-8-sig") as handle:
|
||||
return json.load(handle)
|
||||
Reference in New Issue
Block a user