Files
chenshoulin 58850314cf 初始提交: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 个单测通过。
2026-11-05 19:14:25 +08:00

94 lines
3.0 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""配置加载。
凭据解析优先级:环境变量 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)