用「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 个单测通过。
94 lines
3.0 KiB
Python
94 lines
3.0 KiB
Python
"""配置加载。
|
||
|
||
凭据解析优先级:环境变量 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)
|