Files
QTImport/qtimport/models.py
T
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

105 lines
2.5 KiB
Python
Raw 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.
"""数据结构。"""
from __future__ import annotations
import re
import time
from dataclasses import dataclass, field
@dataclass
class User:
user_id: str
real_name: str
user_name: str
@dataclass
class Team:
team_id: str
team_name: str
@dataclass
class Project:
project_id: str
project_name: str
@dataclass
class WorkSheet:
"""工单(每项目每 Sprint 一张)。"""
worksheet_id: int
code: str = ""
status: str = ""
version_id: int | None = None
version_code: str = ""
system_name: str = ""
start_time: str = ""
end_time: str = ""
@property
def done(self) -> bool:
"""03/04 = 甲方已确认,流程闭环。"""
return self.status in {"03", "04"}
@dataclass
class Feature:
feature_id: int
code: str = ""
name: str = ""
@dataclass
class WorkSheetVersion:
"""工单版本(审批链上的一环)。
`usp_UpdateWorkSheetById` 在第一次保存时插入「技术责任人确认版」(VersionStatus='N'),
但**不回填** `WorkSheet.CurrentVersionId` —— 那要等 `usp_VerifyWorkSheetById` 才写。
所以导入明细/确认工单都必须用「待确认版本」的 VersionId,不能用 CurrentVersionId。
"""
version_id: int
worksheet_id: int
version_code: str = ""
version_status: str = "" # N=待处理(当前),Y=已确认
next_operator: str = ""
@property
def pending(self) -> bool:
return self.version_status.strip().upper() == "N"
@dataclass
class TfsTeam:
team_name: str
area_path: str
iterations: list[dict] = field(default_factory=list)
def ms_to_date(value: object) -> str:
"""`/Date(1785081600000)/` → `2026-07-26`。"""
if not value:
return ""
if isinstance(value, str) and value.startswith("/Date("):
match = re.search(r"\((-?\d+)", value)
if match:
return time.strftime("%Y-%m-%d", time.localtime(int(match.group(1)) / 1000))
return str(value)
def normalize_name(name: str) -> str:
"""项目/迭代名归一化:去空白、统一破折号、忽略大小写。用于精确匹配,避免 contains 误命中。"""
text = re.sub(r"\s+", "", name or "")
for dash in ("-", "—", "–", "−"):
text = text.replace(dash, "-")
return text.casefold()
def normalize_iteration(name: str) -> str:
"""迭代名比对:在 normalize_name 基础上忽略数字前导零(`2026 Sprint 06` ≈ `2026 Sprint 6`)。"""
text = normalize_name(name)
return re.sub(r"(?<=\D)0+(?=\d)", "", text)