用「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 个单测通过。
725 lines
40 KiB
Markdown
725 lines
40 KiB
Markdown
# QuickTeam 工单接口导入 —— 调研与设计
|
||
|
||
| 项 | 内容 |
|
||
| --- | --- |
|
||
| 版本 | v1.0(调研 + 设计,未进入编码) |
|
||
| 日期 | 2026-11-05 |
|
||
| 目标系统 | QuickTeam `http://192.168.1.213` |
|
||
| 现有实现 | `D:\Learning\Python\工单自动化\auto_quickteam.py`(Selenium) |
|
||
| 结论摘要 | **可行**。QuickTeam 无公开 REST API,但其前端全部业务数据都经一套「WCF + 通用存储过程网关」读写,该网关可被直接调用;已实测登录、项目、工单查询、文件上传四类接口,写入链路 5 个存储过程的参数契约已从前端产物完整还原。 |
|
||
|
||
---
|
||
|
||
## 1. 背景与现状
|
||
|
||
### 1.1 现有做法
|
||
|
||
`auto_quickteam.py` 用 Selenium 驱动 Chrome,按「项目」逐个完成 5 步动作:
|
||
|
||
| 步骤 | 函数 | 实际动作 |
|
||
| --- | --- | --- |
|
||
| 1 | `login` / `open_team` | 打开 `#/Login`,填账号密码,进「当前项目」 |
|
||
| 2 | `open_project` | 搜索项目名 → 点项目卡片 → 点「工单」页签(`tab-seven`) |
|
||
| 3 | `create_work_order` | 点「新增」→ 填系统名称/所属团队/起止时间/实施内容/三个责任人 → 保存 |
|
||
| 4 | `entry_work_order_info` | 点进工单 → 编辑 → 隐藏的 `input[type=file]` 上传 xlsx → 点「确认工单」→ 返回 |
|
||
| 5 | `push_work_order_to_tfs` | 点「推送功能模块」→ 选 TFS 项目 `Tools_Dev` → 选迭代 → 全选 → 批量推送 |
|
||
|
||
进度靠 `done.txt` 记录,只在整条流程成功后追加一行。
|
||
|
||
### 1.2 慢在哪
|
||
|
||
代码里的固定 `sleep` 合计约 **128 秒/项目**(`open_project` 17.5s + `create_work_order` 41s + `entry_work_order_info` 14s + `push_work_order_to_tfs` 56s),再加上元素等待、浏览器渲染与网络往返,单项目实际 **2~3 分钟**。一个 Sprint 12~13 个项目就是 **25~35 分钟**,全程需保持前台浏览器与稳定网络。
|
||
|
||
### 1.3 不稳在哪
|
||
|
||
| 根因 | 表现 |
|
||
| --- | --- |
|
||
| 依赖 DOM 结构 | XPath 写死 Element UI 结构,前端一改版即失效(旧版甚至用 `/html/body/section/.../input` 绝对路径) |
|
||
| 固定等待 vs 真实加载 | `sleep(10)` 等保存、`sleep(20)` 等推送,加载慢时 `TimeoutException`;快时又白等 |
|
||
| 弹层/遮罩时序 | `el-loading-mask`、`el-select-dropdown`(`x-placement`)出现与消失时机不定 |
|
||
| 上传控件不可见 | 必须 JS 强改 `display` 才能赋值,任何前端调整都会破坏 |
|
||
| 项目名用 `contains` 匹配 | `资管 - 华泰资管` 同时命中 `资管 - 华泰保险资管`,存在点错项目的风险(现有 `open_project_error_*.html` 已记录多起同名/近名项目失败) |
|
||
| 单实例线性执行 | 任一步失败整批中断,无重试、无断点续跑 |
|
||
| `done.txt` 语义粗糙 | 只在成功后写,中途失败重跑会**重复建工单**;也没有可核对的中间状态 |
|
||
|
||
---
|
||
|
||
## 2. 接口调研
|
||
|
||
### 2.1 核心发现:后端不是 REST,而是「通用存储过程网关」
|
||
|
||
QuickTeam 前端是 Vue 2 + Element UI 的 SPA(`/QuickTeam/`,hash 路由)。抓取并分析其构建产物后确认,前端所有业务读写都汇聚到少数几个 WCF 端点,其中数据层只有一个入口:
|
||
|
||
```
|
||
POST http://192.168.1.213/Services/CommonService.svc/ExecTable
|
||
Content-Type: application/json
|
||
|
||
{
|
||
"connectionName": "QuickTeam", // 数据库连接名:QuickTeam / UserCenter
|
||
"param": "%7B%22SPName%22%3A...%7D" // encodeURIComponent(JSON)
|
||
}
|
||
```
|
||
|
||
`param` 解码后为:
|
||
|
||
```json
|
||
{
|
||
"SPName": "QuickTeam.usp_GetProjectsByTeamId",
|
||
"SQLParams": [
|
||
{ "Name": "TeamId", "Value": "090F8E57-...", "DBType": "string" },
|
||
{ "Name": "IsTemplate", "Value": "1", "DBType": "string" }
|
||
]
|
||
}
|
||
```
|
||
|
||
配套还有一个 `ExecResult`(标量/返回值语义,如 `Result`、`Column1`),另外两个服务:
|
||
|
||
| 端点 | 用途 | 本项目是否使用 |
|
||
| --- | --- | --- |
|
||
| `/Services/CommonService.svc/` | `ExecTable` / `ExecResult`:查项目、查工单、建工单、确认工单 | 是(核心) |
|
||
| `/Services/FileOperationService.svc/` | `FileUpload`(上传 xlsx)、`BatchImportFeature`(服务器解析 Excel 写库) | 是(导入) |
|
||
| `/Services/TFSOperationService.svc/` | `GetTeamProjects`、`BatchImportWorkItemsToTfs`(推送 TFS) | 是(推送) |
|
||
| `/TaskProcessEngine/...` | 任务流程引擎,前端仅用它换 Token | 否 |
|
||
|
||
**这意味着:所谓「通过接口导入」并不需要官方开放 API —— 前端自己就是一个接口客户端,我们只要复刻它的调用序列,就能绕开浏览器与 DOM。**
|
||
|
||
### 2.2 实测结果(只读)
|
||
|
||
在 `192.168.1.213` 上以 `chenshoulin@goldenstand.cn` 实测,全部通过:
|
||
|
||
| 调用 | 结果 | 耗时 |
|
||
| --- | --- | --- |
|
||
| `UserCenter.dbo.usp_Authentication`(账号 + MD5 密码) | 返回 `GuidId/UserName/RealName/Token` | 0.50 s |
|
||
| `QuickTeam.usp_GetTeamsByUserId` | 返回 2 个团队(当前项目 / 归档项目) | 0.46 s |
|
||
| `QuickTeam.usp_GetProjectsByTeamId` | 返回 **215** 个项目 | 1.73 s |
|
||
| `QuickTeam.usp_GetWeeklyWorkSheetsByProjectId` | 单项目 3~4 条工单 | 0.28~1.35 s |
|
||
| `QuickTeam.usp_GetWorkSheetById` | 返回完整工单字段(见 §2.5) | ≈0.3 s |
|
||
| `QuickTeam.usp_GetProjectUsersByProjectId` | 单项目 14 名可指派人员 | 0.34 s |
|
||
| `FileOperationService.FileUpload` | 返回服务器路径 `E:\TSSWCFServices\FileBaseFoloder\WorkSheetImport\<ts>.xlsx` | 0.19 s |
|
||
| `TFSOperationService.GetTeamProjects` | 9 个 TFS 项目(`Tools_Dev` 等) | ≈1 s |
|
||
| `TFSOperationService.GetTeamByProject` | `Tools_Dev` 下 9 个团队,含 `AreaPath` 与 107 条迭代 | ≈2 s |
|
||
|
||
**没有使用 Cookie/令牌**:`ExecTable` 不校验身份,登录接口只是前端用来取 `UserId/RealName` 做界面门禁与 `Operator` 字段填充。这一点对实现是便利,对安全是隐患(见 §6 R6)。
|
||
|
||
配套只读探针已落在 `tools/api_probe.py`,一个 Sprint 的现状扫描(12 个项目、含 N+1 次工单详情)实测 **12.63 s,平均 1.05 s/项目**——这已经比 Selenium 完成一个项目的耗时低两个数量级。
|
||
|
||
### 2.3 调用链还原(与 Selenium 步骤一一对应)
|
||
|
||
前端产物中可直接读出以下序列,即本项目的目标行为:
|
||
|
||
```
|
||
登录 usp_Authentication(username, md5(password)) -> GuidId, RealName
|
||
建工单 usp_InsertWorkSheet() -> [{WorkSheetId}]
|
||
usp_UpdateWorkSheetById(...17 个字段, Operation="new") -> [{Result}]
|
||
导入明细 FileUpload(file) -> {FileUploadResult: 服务器路径}
|
||
BatchImportFeature(connectionName, param, filePath) -> [{Column1: 0}] (0=成功)
|
||
确认工单 usp_GetWorkSheetById(WorkSheetId) -> CurrentVersionId / VersionCode
|
||
usp_VerifyWorkSheetById(...) -> 0 (成功)
|
||
推送 TFS usp_GetWorkFeaturesByWorkSheetIds(WorkSheetIds) -> FeatureId 列表
|
||
BatchImportWorkItemsToTfs(projectName, areaPath, iterationPath)
|
||
```
|
||
|
||
### 2.4 接口契约清单
|
||
|
||
#### 2.4.1 `CommonService.svc/ExecTable`
|
||
|
||
| 存储过程 | 参数(Name / DBType) | 返回 |
|
||
| --- | --- | --- |
|
||
| `UserCenter.dbo.usp_Authentication` | `UserName`/string,`Password`/string(**MD5 摘要**) | 用户行 |
|
||
| `QuickTeam.usp_GetTeamsByUserId` | `UserId`/string | 团队行(`TeamId`、`TeamName`、`Role`) |
|
||
| `QuickTeam.usp_GetProjectsByTeamId` | `TeamId`/string,`IsTemplate`/string(固定 `"1"`) | 项目行(`ProjectId`、`ProjectName`) |
|
||
| `QuickTeam.usp_GetWeeklyWorkSheetsByProjectId` | `ProjectId`/string,`Monday`/string,`Sunday`/string | 工单行(`WorkSheetId`、`WorkSheetCode`、`Status`) |
|
||
| `QuickTeam.usp_GetWorkSheetById` | `WorkSheetId`/int | 工单详情(单行) |
|
||
| `QuickTeam.usp_GetProjectUsersByProjectId` | `ProjectId`/string | 项目人员(`UserId`、`RealName`) |
|
||
| `QuickTeam.usp_InsertWorkSheet` | **无参数** | `[{WorkSheetId}]` |
|
||
| `QuickTeam.usp_UpdateWorkSheetById` | 见下表 | `[{Result}]`,>0 表示成功 |
|
||
| `QuickTeam.usp_VerifyWorkSheetById` | `WorkSheetId`/int,`VersionId`/int,`NextOperator`/string,`NextVersionCode`/string,`Percent`/int,`Operator`/string | `0` = 成功(走 `ExecResult`) |
|
||
| `QuickTeam.usp_GetWorkFeaturesByWorkSheetIds` | `WorkSheetIds`/string(逗号分隔) | 功能模块行(含 `FeatureId`、`FeatureCode`) |
|
||
| `QuickTeam.usp_DeleteWorkSheetAllById` | `WorkSheetId`/int | 删除整单(**回滚用**) |
|
||
|
||
`usp_UpdateWorkSheetById` 的 17 个参数(前端 `chunk-0cadb1de` 中原样提取):
|
||
|
||
| 参数 | 类型 | 取值来源 |
|
||
| --- | --- | --- |
|
||
| `WorkSheetId` | string | 上一步返回值 |
|
||
| `ProjectId` | string | 项目 GUID |
|
||
| `ProjectName` | string | 项目名 |
|
||
| `SystemName` | string | **Sprint 名称**(如 `2026 Sprint 10`) |
|
||
| `Evaluator` | string | 工单评估人**真实姓名** |
|
||
| `TechnicalLeader` | string | 技术负责人真实姓名 |
|
||
| `CustomerLeader` | string | 客户负责人真实姓名 |
|
||
| `StartTime` / `EndTime` | string | `yyyy-MM-dd` |
|
||
| `Status` | string | `00`~`04`(见 §2.6) |
|
||
| `ServiceType` | string | 默认 `DevelopService` |
|
||
| `ImplementContent` | string | 实施内容,默认 `需求开发测试` |
|
||
| `OtherInstructions` | string | 其它说明(可空) |
|
||
| `Operator` | string | 操作人真实姓名 |
|
||
| `DeleteFlag` | string | 固定 `"0"` |
|
||
| `Operation` | string | 新建固定 `"new"`,修改 `"update"` |
|
||
| `BelongTeam` | string | 所属团队(如 `资管投资人事业部`) |
|
||
|
||
#### 2.4.2 `FileOperationService.svc`
|
||
|
||
| 操作 | 请求 | 返回 |
|
||
| --- | --- | --- |
|
||
| `FileUpload?fileName=<ts>.xlsx&fileFolder=WorkSheetImport` | `multipart/form-data`,文件字段名不限(实测 `file`/`upload` 均可) | `{"FileUploadResult":"E:\\TSSWCFServices\\FileBaseFoloder\\WorkSheetImport\\<ts>.xlsx"}` |
|
||
| `BatchImportFeature` | `{connectionName:"QuickTeam", param:<同 ExecTable>, filePath:"<上一步路径>"}`,其中 `SPName = QuickTeam.usp_BatchImportFeatureFromExcel`,`SQLParams = [WorkSheetId/int, VersionId/int]` | `[{Column1: 0}]`,**0 为成功** |
|
||
|
||
#### 2.4.3 `TFSOperationService.svc`
|
||
|
||
| 操作 | 请求 | 返回 |
|
||
| --- | --- | --- |
|
||
| `GetTeamProjects` | 空 body | TFS 项目列表(实测 9 个:`BeiJing_Dev`、`Tools_Dev`、`ShangHai_Dev`…,含 `ProjectId`) |
|
||
| `GetTeamByProject` | `{projectName:"Tools_Dev", projectId:"adbb5ee9-..."}` | 团队列表,每项含 `TeamName`、`AreaPath: ["Tools_Dev\\DataTechnologies"]`、`Iterations: [{Id, Name, Path}]` |
|
||
| `BatchImportWorkItemsToTfs` | `{connectionName:"QuickTeam", param:<SPName=[QuickTeam].[usp_BatchImportWorkFeaturesByFeatureIds], FeatureIds:"1,2,3">, projectName, areaPath, iterationPath}` | 推送结果 |
|
||
|
||
**推送所需的两个路径都能自动取到**(这点对稳定性很关键):
|
||
|
||
```
|
||
GetTeamProjects() -> Tools_Dev 的 ProjectId
|
||
GetTeamByProject("Tools_Dev", pid) -> teams[];team.AreaPath[0] = "Tools_Dev\DataTechnologies"
|
||
team.Iterations[].Path 形如 "Tools_Dev\2026 Sprint\2026 Sprint 10"
|
||
```
|
||
|
||
按 Sprint 名称(`2026 Sprint 10`)在 `Iterations` 里精确匹配即可得到 `iterationPath`,`areaPath` 取所选团队的 `AreaPath[0]`——**无需在配置里手工维护迭代路径**。前端 `GetIterationPaths` 在当前部署上返回 404(服务端未暴露该端点),但走 `GetTeamByProject` 已完全覆盖其用途。
|
||
|
||
### 2.5 工单数据模型(实测)
|
||
|
||
```json
|
||
{
|
||
"WorkSheetId": 225072,
|
||
"WorkSheetCode": "信托 - 上海信托_20260830_225072",
|
||
"ProjectId": "0f52024c-...",
|
||
"ProjectName": "信托 - 上海信托",
|
||
"SystemName": "2026 Sprint 8",
|
||
"Evaluator": "陈首林",
|
||
"TechnicalLeader": "陈首林",
|
||
"CustomerLeader": "陈首林",
|
||
"StartTime": "/Date(1785081600000)/",
|
||
"EndTime": "/Date(1788019200000)/",
|
||
"Status": "01",
|
||
"ServiceType": "DevelopService",
|
||
"ImplementContent": "需求开发测试",
|
||
"CurrentVersionId": 260113,
|
||
"VersionCode": "技术责任人确认版",
|
||
"BelongTeam": "资管投资人事业部",
|
||
"Finished": "00",
|
||
"DeleteFlag": "0"
|
||
}
|
||
```
|
||
|
||
要点:
|
||
- 日期是 **.NET `/Date(ms)/`** 格式,需转换;
|
||
- 工单标识 = `WorkSheetCode`(`项目名_日期_WorkSheetId`),业务唯一键实际是 **(ProjectId, SystemName)**;
|
||
- `SystemName` 就是 Sprint 名称,是幂等判定的关键字段。
|
||
|
||
### 2.6 工单状态与版本机
|
||
|
||
前端 `EffortListVersionInfo` 是硬编码常量,即审批链:
|
||
|
||
| 版本 | 版本名(VersionCode) | 确认后流转到 | NextOperator | NNextOperator | Percent |
|
||
| --- | --- | --- | --- | --- | --- |
|
||
| 01 | 技术责任人确认版 | 客户责任人确认版 | TechnicalLeader | CustomerLeader | 20 |
|
||
| 02 | 客户责任人确认版 | 甲方确认版 | CustomerLeader | CustomerLeader | — |
|
||
| 03 | 甲方确认版 | 流程结束 | CustomerLeader | — | — |
|
||
|
||
状态码:
|
||
|
||
| Status | 含义 |
|
||
| --- | --- |
|
||
| 00 | 等待技术责任人确认 |
|
||
| 01 | 等待客户责任人确认 |
|
||
| 02 | 等待甲方确认 |
|
||
| 03 / 04 | 甲方已确认 |
|
||
|
||
**「确认工单」= 推进版本**:调用 `usp_VerifyWorkSheetById` 时必须传当前版本的 `NNextOperator` 作为 `NextOperator`、`NextVersionName` 作为 `NextVersionCode`;技术责任人确认版这一步 `Percent=20`。这些值不要写死,应从 `usp_GetWorkSheetById` 返回的 `VersionCode` 映射得到(本表即映射依据)。
|
||
|
||
### 2.7 Excel 模板
|
||
|
||
上传的 xlsx 是「工作量评估表」模板(`Sheet1`),由**服务器端** `usp_BatchImportFeatureFromExcel` 按固定单元格位置解析——基本信息区(项目名称/起止时间/系统名称/三个责任人/实施内容)+ 明细表(服务模块代码、服务模块、服务内容、详细说明、需求/开发/测试/实施分项、小计)。
|
||
|
||
因此:
|
||
- **导入阶段必须沿用现有模板**,不能改结构;`D:\Work\QuickTeam\*` 下的文件已是该模板;
|
||
- 客户端**不需要**解析 Excel(这是原方案的一个隐含优点,本项目保留);
|
||
- 解析有副作用(直接写库),所以每次导入都必须是幂等的(见 §4.5)。
|
||
|
||
---
|
||
|
||
## 3. 可行性结论
|
||
|
||
### 3.1 技术可行(已验证部分)
|
||
|
||
| 验证项 | 状态 |
|
||
| --- | --- |
|
||
| 登录 / 取用户身份 | ✅ 实测通过 |
|
||
| 团队、项目、人员、工单查询 | ✅ 实测通过(215 项目 / 单项目 3~4 工单) |
|
||
| 文件上传通道 | ✅ 实测通过,返回服务器绝对路径 |
|
||
| 读取链路端到端 | ✅ `tools/api_probe.py` 已跑通一个 Sprint 的现状扫描 |
|
||
| 写链路(建单/导入/确认/推送) | ⚠️ 契约已完整还原,**尚未实写**(避免污染生产数据),需在测试项目上做一次小批量联调 |
|
||
|
||
### 3.2 收益
|
||
|
||
| 指标 | 现状(Selenium) | 接口方案(估算) | 依据 |
|
||
| --- | --- | --- | --- |
|
||
| 单项目耗时 | 2~3 min | 10~25 s | 实测读 1.05 s/项目;写操作 4~5 次 + 上传 0.19 s + TFS 推送(未知,按 5~15 s 估) |
|
||
| 一个 Sprint(12 项目) | 25~35 min | 3~6 min | 线性外推 |
|
||
| 人工介入 | 必须人在前台看着 | 可无人值守 + 微信/邮件告警 | 无浏览器 |
|
||
| 失败恢复 | 整批中断、可能重复建单 | 台账驱动,按阶段续跑 | §4.5 |
|
||
| 环境依赖 | Chrome + Driver + 分辨率 + DOM 版本 | 只要能访问 HTTP 服务 | — |
|
||
| 前端改版影响 | 直接失效 | 无影响(契约是存储过程) | — |
|
||
|
||
### 3.3 风险可控但需正视
|
||
|
||
接口方案把「UI 不确定性」换成了「服务端契约不确定性」:不动的是存储过程名与参数,动的是**我们无法从代码里看到的分支逻辑**(例如 `usp_InsertWorkSheet` 无参却要返回合法 WorkSheetId、Excel 解析的隐含校验、TFS 重复推送的行为)。因此设计上必须把「可重入 + 可回滚 + 可审计」做成第一等公民,而不是事后补。
|
||
|
||
### 3.4 被否决的方案
|
||
|
||
| 方案 | 否决原因 |
|
||
| --- | --- |
|
||
| 继续优化 Selenium(显式等待、并发多实例) | 只是把不稳定的边界往后推,前端一改版仍然全废;并发浏览器成本高 |
|
||
| 直连数据库改表 | 绕过业务逻辑与审批链,无审计,风险远高于收益 |
|
||
| 抓包录制重放(HAR → 固定请求) | 与直接调接口等价,但可维护性更差 |
|
||
|
||
---
|
||
|
||
## 4. 系统设计
|
||
|
||
### 4.1 目标与非目标
|
||
|
||
**目标**
|
||
- 用接口完成「建工单 → 导入 Excel 明细 → 确认工单 → 推送 TFS」全流程;
|
||
- 一个 Sprint 批量执行,支持 `plan`(只读预演)/ `apply`(执行)/ `resume`(续跑)/ `report`;
|
||
- 幂等:重复执行不产生重复工单、不重复推送;
|
||
- 可观测:每一步有结构化日志与报告,失败可定位到具体阶段与接口。
|
||
|
||
**非目标**
|
||
- 不替代 QuickTeam 的审批流程(只做「技术责任人」这一跳);
|
||
- 不改服务器端、不改 Excel 模板;
|
||
- 不做 Web 界面(CLI 优先,稳定后再考虑)。
|
||
|
||
### 4.2 技术选型
|
||
|
||
| 项 | 选择 | 理由 |
|
||
| --- | --- | --- |
|
||
| 语言 | Python 3.12 | 与现有脚本同栈,团队可直接接手;生产环境已有 3.12 |
|
||
| HTTP | 标准库 `urllib`(可选 `requests`) | 零依赖,便于离线部署;只有 3 个服务、8 个操作,不需要框架 |
|
||
| 状态存储 | SQLite(单文件) | 无需服务,天然支持事务与断点;可直接用 DB 工具排查 |
|
||
| 日志 | JSONL + 控制台摘要 | 机器可读,便于后续接告警 |
|
||
| 测试 | pytest + 录制的响应 fixtures | 契约回放可在无网环境跑 |
|
||
| 打包 | 单目录 + `python -m qtimport` | 内网机器直接拷贝运行 |
|
||
|
||
### 4.3 分层架构
|
||
|
||
```
|
||
┌──────────────────────────────────────────────────────┐
|
||
│ cli plan / apply / resume / report │
|
||
├──────────────────────────────────────────────────────┤
|
||
│ workflow 单项目编排、阶段推进、补偿、并发调度 │
|
||
├──────────────────────────────────────────────────────┤
|
||
│ domain 工单聚合:查重、建单、填写、确认、推送 │
|
||
├──────────────────────────────────────────────────────┤
|
||
│ api SP 语义封装(login/projects/worksheets…) │
|
||
├──────────────────────────────────────────────────────┤
|
||
│ transport WCF 网关:ExecTable/ExecResult/Upload │
|
||
│ 双层 JSON 解码、错误解析、重试、耗时埋点 │
|
||
├──────────────────────────────────────────────────────┤
|
||
│ store SQLite 台账 + 运行报告 │
|
||
└──────────────────────────────────────────────────────┘
|
||
```
|
||
|
||
分层原则:**transport 不含业务,domain 不含 HTTP**。这样存储过程改名、端点迁移只影响一层;而幂等判定全部集中在 workflow/domain,不散落在 HTTP 调用里。
|
||
|
||
### 4.4 单项目时序
|
||
|
||
```
|
||
plan ─┬─ 登录 → 取 TeamId → 取项目列表(1 次,缓存)
|
||
├─ 读 xlsx 目录 → 项目名规范化匹配(精确优先,模糊仅提示)
|
||
└─ 查该项目历史工单 → 比对 SystemName → 输出计划(create / skip / resume)
|
||
|
||
apply ─┬─ [create] InsertWorkSheet() → UpdateWorkSheetById(Operation="new")
|
||
├─ [detail] GetWorkSheetById → CurrentVersionId / VersionCode
|
||
├─ [import] FileUpload(xlsx) → BatchImportFeature(WorkSheetId, VersionId)
|
||
├─ [verify] VerifyWorkSheetById(NextOperator, NextVersionCode, Percent)
|
||
└─ [push] GetTeamProjects → GetTeamByProject(解析 area/iteration)
|
||
GetWorkFeaturesByWorkSheetIds → BatchImportWorkItemsToTfs
|
||
```
|
||
|
||
每一步落台账后才进入下一步(write-ahead),因此任何时刻中断都能从记录点续跑。
|
||
|
||
### 4.5 幂等与断点(核心设计)
|
||
|
||
**业务唯一键**:`(ProjectId, SystemName)` —— 同一项目同一 Sprint 只应有一张工单。
|
||
|
||
**台账表**:
|
||
|
||
```sql
|
||
CREATE TABLE import_job (
|
||
run_id TEXT NOT NULL,
|
||
project_id TEXT NOT NULL,
|
||
project_name TEXT NOT NULL,
|
||
system_name TEXT NOT NULL, -- Sprint
|
||
source_file TEXT NOT NULL,
|
||
worksheet_id INTEGER, -- 建单后回填
|
||
version_id INTEGER,
|
||
stage TEXT NOT NULL, -- 见下
|
||
tfs_project TEXT,
|
||
iteration TEXT,
|
||
error TEXT,
|
||
updated_at TEXT NOT NULL,
|
||
PRIMARY KEY (run_id, project_id)
|
||
);
|
||
|
||
CREATE TABLE import_event ( -- 追加型审计流水
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
run_id TEXT NOT NULL,
|
||
project_id TEXT,
|
||
stage TEXT NOT NULL,
|
||
action TEXT NOT NULL, -- call/ok/fail/skip
|
||
detail TEXT,
|
||
elapsed_ms INTEGER,
|
||
created_at TEXT NOT NULL
|
||
);
|
||
```
|
||
|
||
**阶段机**:
|
||
|
||
```
|
||
PLANNED → WS_CREATED → WS_FILLED → EXCEL_IMPORTED → WS_VERIFIED → TFS_PUSHED
|
||
└──────────┴────────────┴──────────────┴──→ FAILED(stage, error)
|
||
```
|
||
|
||
**重入规则**(`apply` 前先跑一次判定,而不是无脑建单):
|
||
|
||
| 现场状态 | 动作 |
|
||
| --- | --- |
|
||
| 无该 Sprint 的工单 | 新建(create) |
|
||
| 有工单,`Status` ∈ {00,01},且台账显示是本工具创建 | **resume**:从台账记录的下一阶段继续,不重复建单 |
|
||
| 有工单,`Status` ∈ {00,01},但台账无记录(人工建的/上次工具崩了) | 默认 **skip 并告警**;加 `--adopt` 才接管续跑(保守优先,避免误改别人的单) |
|
||
| 有工单,`Status` ∈ {03,04} | **skip**(已闭环) |
|
||
| 有工单且已推送 TFS | **skip**(推送不可逆) |
|
||
|
||
**补偿(回滚)**:仅当「本次创建 + 尚未推送 TFS + 在导入/确认阶段失败」时,`--rollback-on-fail` 会调 `usp_DeleteWorkSheetAllById` 清掉半成品。已推送的一律不删——TFS 侧工作项已产生,必须人工处理,这是硬边界。
|
||
|
||
**并发**:`--workers 2` 默认(写路径串行度受服务端 SP 与 TFS 限流约束),读路径可 4。并发下每个项目独立事务,互不影响;工单编号由服务端生成,不存在本地竞争。
|
||
|
||
### 4.6 配置与凭据
|
||
|
||
```json
|
||
{
|
||
"base_url": "http://192.168.1.213",
|
||
"username": "chenshoulin@goldenstand.cn",
|
||
"password": "(不入库,见下)",
|
||
"team_name": "当前项目",
|
||
"work_dir": "D:/Work/QuickTeam/2026 Sprint 10",
|
||
"sprint": "2026 Sprint 10",
|
||
"start_time": "2026-10-01",
|
||
"end_time": "2026-10-31",
|
||
"content_desc": "需求开发测试",
|
||
"tfs_project": "Tools_Dev",
|
||
"tfs_team": "DataTechnologies",
|
||
"area_path": "",
|
||
"iteration_path": ""
|
||
}
|
||
```
|
||
|
||
`area_path` / `iteration_path` 留空即自动解析(`GetTeamByProject` → 按 Sprint 名匹配),填了则作为覆盖值使用。`tfs_team` 决定 `AreaPath`,缺省取该 TFS 项目的第一个团队。
|
||
|
||
凭据优先级:`QT_PASSWORD` 环境变量 → Windows DPAPI 加密文件 → 交互式输入。**明文密码不进仓库、不进日志**(`config.json` 已在 `.gitignore`)。
|
||
|
||
### 4.7 CLI
|
||
|
||
```bash
|
||
# 只读预演:打印将要做什么,不写任何数据
|
||
qtimport plan --config config/config.json
|
||
|
||
# 执行(默认幂等,已完成的跳过)
|
||
qtimport apply --config config/config.json --workers 2
|
||
|
||
# 指定项目重跑(失败重试的常用姿势)
|
||
qtimport apply --config config/config.json --project "信托 - 上海信托" --from-stage EXCEL_IMPORTED
|
||
|
||
# 出报告
|
||
qtimport report --run-id 20261105-1030
|
||
```
|
||
|
||
### 4.8 错误处理
|
||
|
||
WCF 的错误是 HTML 页面,真正的信息在 `The exception message is '...'` 里,必须解析出来再分类,否则只会看到 `HTTP 400`:
|
||
|
||
| 分类 | 判定特征 | 策略 |
|
||
| --- | --- | --- |
|
||
| `NET` | `URLError` / timeout | 重试 2 次(指数退避),仍失败则标记该阶段 |
|
||
| `SP_MISSING` | `Could not find stored procedure` | 不重试(配置/版本问题),整批中止并告警 |
|
||
| `PARAM` | `missing parameter` / `conversion` | 不重试,记到项目级错误,继续下一个项目 |
|
||
| `EXCEL` | `BatchImportFeature` 返回 `Column1 != 0` | 不重试,标记该文件模板不符,继续 |
|
||
| `TFS` | 推送异常 | 不重试,标记并保留工单(可人工补推) |
|
||
| `AUTH` | 登录返回空 | 立即中止(避免锁号) |
|
||
|
||
另有两个必须处理的协议细节:
|
||
1. **双层 JSON**:返回值常是「JSON 字符串里再套一层 JSON」,需循环解码 ≤3 次;
|
||
2. **BOM**:错误页带 UTF-8 BOM,需 `utf-8-sig` 解码。
|
||
|
||
### 4.9 报告
|
||
|
||
每次 run 产出:
|
||
|
||
- `reports/<run_id>/summary.md`:项目 / 文件 / 工单号 / 阶段 / 耗时 / 结论(create|skip|resume|fail);
|
||
- `reports/<run_id>/detail.csv`:逐阶段明细,便于 Excel 复核;
|
||
- `logs/<run_id>.jsonl`:结构化调用日志(含每次 HTTP 的 `sp`、`elapsed_ms`、`result`)。
|
||
|
||
### 4.10 目录结构(拟)
|
||
|
||
```
|
||
QTImport/
|
||
├── README.md
|
||
├── config/
|
||
│ ├── config.example.json
|
||
│ └── config.json # gitignore
|
||
├── docs/
|
||
│ └── QuickTeam-API导入-调研与设计.md
|
||
├── tools/
|
||
│ └── api_probe.py # 只读探针(已完成,可独立交付)
|
||
├── qtimport/
|
||
│ ├── __main__.py # CLI 入口
|
||
│ ├── transport.py # WCF 网关
|
||
│ ├── api.py # SP 语义封装
|
||
│ ├── domain.py # 工单聚合与状态机
|
||
│ ├── workflow.py # 编排 / 并发 / 补偿
|
||
│ ├── store.py # SQLite 台账
|
||
│ ├── report.py # 报告
|
||
│ └── config.py
|
||
├── tests/
|
||
│ ├── fixtures/ # 录制的真实响应
|
||
│ └── test_*.py
|
||
└── pyproject.toml
|
||
```
|
||
|
||
### 4.11 测试策略
|
||
|
||
| 层级 | 做法 |
|
||
| --- | --- |
|
||
| 单元 | payload 构造(`encodeURIComponent`+双层 JSON)、`/Date(ms)/` 转换、错误页解析、项目名规范化、状态机流转 |
|
||
| 契约回放 | 把本次实测的真实响应存 `tests/fixtures/`,离线断言字段与解码层数(防止服务端升级后静默失败) |
|
||
| 集成 | 在一个**专用测试项目**上跑 1 张工单:建 → 导 → 确认,人工核对 UI 与 DB,再删单 |
|
||
| 灰度 | 一个真实 Sprint 中挑 2 个项目跑接口,其余照旧;逐字段 diff(明细行数、小计、三个责任人、TFS 工作项数) |
|
||
| 回归 | 全量切换后,用 `plan` 的只读结果与 UI 页面勾兑一次 |
|
||
|
||
### 4.12 实施计划
|
||
|
||
| 里程碑 | 内容 | 交付 | 预估 |
|
||
| --- | --- | --- | --- |
|
||
| M0 | 调研 + 只读探针(**已完成**) | 本文档 + `tools/api_probe.py` | — |
|
||
| M1 | 写链路代码:transport/api/domain + `plan`/`apply`(**已实现**,未真实写入) | `qtimport/` 包 | — |
|
||
| M2 | 幂等/断点/补偿/并发/报告(**已完成**) | SQLite 台账 + 报告 | — |
|
||
| M3 | 真实联调:一个测试项目跑通建单→导入→确认(**待执行**) | 联调记录 | 0.5 d |
|
||
| M4 | 灰度一个 Sprint(2 个项目)+ 全量切换 | 切换说明 | 0.5 d |
|
||
| M5 | 退役 Selenium 脚本,保留为应急兜底 | — | — |
|
||
|
||
M3 是唯一剩下的卡点:**写链路一旦真跑通,后面都是工程量**。
|
||
|
||
---
|
||
|
||
## 5. 与现有脚本的对应关系
|
||
|
||
| 现有能力 | 新实现 | 备注 |
|
||
| --- | --- | --- |
|
||
| `get_project_name_list` + `done.txt` | 读目录 + SQLite 台账 | 台账可查、可重入,替代文本文件 |
|
||
| `login` | `usp_Authentication` | 无浏览器 |
|
||
| `open_team` / `open_project` | `usp_GetTeamsByUserId` / `usp_GetProjectsByTeamId` | 项目名做规范化精确匹配,避免 `contains` 误命中 |
|
||
| `create_work_order` | `InsertWorkSheet` + `UpdateWorkSheetById` | 字段一一对应 |
|
||
| `entry_work_order_info`(上传 xlsx) | `FileUpload` + `BatchImportFeature` | 服务器解析逻辑不变 |
|
||
| 「确认工单」点击 | `VerifyWorkSheetById` | 需按版本表推导 NextOperator/Percent |
|
||
| `push_work_order_to_tfs` | `GetWorkFeaturesByWorkSheetIds` + `BatchImportWorkItemsToTfs` | 全量勾选 = 拼全部 FeatureId |
|
||
| 错误页 dump HTML | 解析 `exception message` + 结构化日志 | 从「事后看截图」变「当场有原因」 |
|
||
|
||
---
|
||
|
||
## 6. 风险与开放问题
|
||
|
||
| 编号 | 风险/问题 | 影响 | 对策 | 优先级 |
|
||
| --- | --- | --- | --- | --- |
|
||
| R1 | 写链路未实测(`InsertWorkSheet` 无参、`UpdateWorkSheetById` 的 `Result` 语义、Excel 解析副作用) | 阻塞实施 | M1 在测试项目上小批量验证;先建后删,验证补偿可用 | **P0** |
|
||
| R2 | `GetIterationPaths` 404(服务端未暴露) | 原本以为迭代路径不可取 | 已解决:改走 `GetTeamByProject`,返回团队 `AreaPath` 与 `Iterations[].Path`,按 Sprint 名精确匹配 | — |
|
||
| R3 | 迭代/区域路径匹配失败(Sprint 名与 TFS 迭代命名不一致,如 `2026 Sprint 06` 补零) | 推送会失败 | 匹配时做归一化(去空格、去前导零、忽略大小写);匹配不到则**拒绝推送并报错**,不猜 | P1 |
|
||
| R4 | TFS 重复推送可能产生重复工作项(服务端无幂等标记) | 数据脏 | 台账记录 `TFS_PUSHED`,推过即不再推;报告里显式列出 | P1 |
|
||
| R5 | 服务器端 Excel 解析耗时未知(明细多时可能数十秒) | 影响单项目耗时估算与超时设置 | 首次联调即埋点计时;超时按实测值 × 2 设定 | P2 |
|
||
| R6 | `ExecTable` 无鉴权(实测无 Cookie 可读写任意 SP);密码 MD5 明文传输 | 安全 | 仅在内网机器执行、不使用管理员账号;不新增对外暴露;建议后续接入 SSO / 网络层限制 | P2 |
|
||
| R7 | 服务器 `WorkSheetImport` 目录只增不删(`FileBaseFoloder`) | 磁盘堆积 | 上传文件名带 run_id 前缀;提供清理脚本;联系运维确认保留策略 | P3 |
|
||
| R8 | 存储过程改名/加参(前端同步改) | 契约失效 | 契约回放测试会在字段缺失时立刻报错;版本化 `api.py` 的 SP 映射表 | P3 |
|
||
| R9 | 凭据落盘 | 泄露 | `config.json` 已 gitignore;支持环境变量/DPAPI;日志脱敏 | P3 |
|
||
|
||
---
|
||
|
||
## 7. 附录
|
||
|
||
### A. 实测请求样例(脱敏)
|
||
|
||
```http
|
||
POST /Services/CommonService.svc/ExecTable HTTP/1.1
|
||
Content-Type: application/json
|
||
|
||
{"connectionName":"QuickTeam","param":"%7B%22SPName%22%3A%22QuickTeam.usp_GetProjectsByTeamId%22%2C%22SQLParams%22%3A%5B%7B%22Name%22%3A%22TeamId%22%2C%22Value%22%3A%22090F8E57-...%22%2C%22DBType%22%3A%22string%22%7D%2C%7B%22Name%22%3A%22IsTemplate%22%2C%22Value%22%3A%221%22%2C%22DBType%22%3A%22string%22%7D%5D%7D"}
|
||
```
|
||
|
||
响应(注意外层是字符串,需二次解码):
|
||
|
||
```
|
||
"[{\"ProjectId\":\"0f52024c-...\",\"ProjectName\":\"信托 - 上海信托\",...}, ...]"
|
||
```
|
||
|
||
### B. 前端产物中的存储过程清单
|
||
|
||
共 210 个(`QuickTeam.*` + `UserCenter.*`),本项目涉及 13 个存储过程与 6 个服务操作。完整清单可通过以下命令复现:
|
||
|
||
```bash
|
||
# 抓取并统计(在任一可访问 QuickTeam 的机器上)
|
||
curl -s http://192.168.1.213/QuickTeam/ -o index.html
|
||
grep -oE '/QuickTeam/js/[A-Za-z0-9._-]+\.js' index.html | sort -u | \
|
||
while read p; do curl -s "http://192.168.1.213$p"; done | \
|
||
grep -oh '[A-Za-z][A-Za-z0-9_]*\.usp_[A-Za-z0-9_]*' | sort -u
|
||
```
|
||
|
||
### C. Selenium 耗时拆解(按代码 sleep 累加)
|
||
|
||
| 函数 | 固定 sleep | 主要来源 |
|
||
| --- | --- | --- |
|
||
| `open_project` | 17.5 s | 搜索 0.5+1+1+2、点击 3、页签 10 |
|
||
| `create_work_order` | 41 s | 新增 10、保存 10、三个责任人各 3、其余 5 |
|
||
| `entry_work_order_info` | 14 s | 三次点击各 3、返回 5 |
|
||
| `push_work_order_to_tfs` | 56 s | 开头 20、选项目后 20、其余 16 |
|
||
| **合计** | **≈128 s** | 不含元素等待/渲染/网络 |
|
||
|
||
### D. 探针工具
|
||
|
||
```bash
|
||
# 现状扫描(只读)
|
||
python tools/api_probe.py --config config/config.json
|
||
|
||
# 额外验证上传通道(会在服务器留一个临时文件,不解析、不写库)
|
||
python tools/api_probe.py --config config/config.json --upload-test
|
||
|
||
# 输出 JSON 供后续程序消费
|
||
python tools/api_probe.py --config config/config.json --json-out reports/probe.json
|
||
```
|
||
|
||
实测输出(2026 Sprint 10,12 个文件):
|
||
|
||
```
|
||
[登录] 陈首林 <chenshoulin@goldenstand.cn> UserId=86334126-... 0.50s
|
||
[团队] 当前项目 (090F8E57-...) 0.46s
|
||
[项目] 共 215 个 1.73s
|
||
信托 - 上海信托 4 否 待创建
|
||
...
|
||
合计 12 个文件:待创建 12,已存在 0,异常 0
|
||
[耗时] 扫描 12.63s 平均 1.05s/项目
|
||
```
|
||
|
||
|
||
---
|
||
|
||
## 8. 实现说明(2026-11-05)
|
||
|
||
代码已落地,与设计的对应关系:
|
||
|
||
| 设计章节 | 实现 |
|
||
| --- | --- |
|
||
| §4.3 分层 | `qtimport/transport.py`(HTTP+协议)、`api.py`(SP 语义)、`domain.py`(纯规则)、`workflow.py`(编排)、`store.py`(台账)、`report.py`(报告)、`__main__.py`(CLI) |
|
||
| §4.4 时序 | `workflow.Importer._run_project`,阶段常量 `STAGES` |
|
||
| §4.5 幂等 | `store.latest_job` 跨 run 取最近阶段;dry-run 的 run 不参与判定(避免演练被当成已完成) |
|
||
| §4.5 补偿 | `--rollback-on-fail`,仅在「本次创建 + 未到确认阶段」时调 `usp_DeleteWorkSheetAllById` |
|
||
| §4.7 CLI | `plan` / `apply` / `report` / `status`,真实写入需 `--yes` |
|
||
| §4.8 错误 | `errors.py` 把 WCF 错误页分类为 NET/SP_MISSING/PARAM/EXCEL/TFS/AUTH,决定重试或中止 |
|
||
| §4.11 测试 | `tests/` 69 个用例(unittest,无需网络),覆盖解码、错误分类、项目匹配、动作判定、版本推进、参数构造、回滚、续跑、工单列表去重、待确认版本选择、TFS 状态码 |
|
||
|
||
实现期新增的两个设计约束(原方案没写细):
|
||
|
||
1. **查询窗口与业务窗口分离**:`StartTime/EndTime` 是写进工单的业务数据,必须精确;查「是否已有工单」时用前后各放宽 30 天的窗口,避免边界漏查。
|
||
2. **Excel 上传前预检**:读 `xl/sharedStrings.xml` 确认含「工作量评估表」「服务模块」,避免把一个错文件送到服务器解析(`Column1 != 0` 的失败代价更高)。
|
||
|
||
实测(`--dry-run`,12 个项目):19.2 s,0 失败,TFS 上下文自动解析为
|
||
`area=Tools_Dev\DataTechnologies`、`iteration=Tools_Dev\2026 Sprint\2026 Sprint 10`。
|
||
|
||
### 8.1 真实联调(2026-11-05,Sprint 2026 Sprint 10)
|
||
|
||
单项目「资管 - 华泰保险资管」跑通全链路:
|
||
|
||
| 阶段 | 结果 |
|
||
| --- | --- |
|
||
| 建单 + 填单 | `usp_InsertWorkSheet` → 225134,`usp_UpdateWorkSheetById` 后 `Status=00`、`WorkSheetCode=资管 - 华泰保险资管_20261031_225134` |
|
||
| 导入明细 | `FileUpload` → `usp_BatchImportFeatureFromExcel` → `Column1=0`,2 个功能模块(测试工时 12 / 10.5,与 Excel 一致) |
|
||
| 确认 | `usp_VerifyWorkSheetById` → `Status=01`、`CurrentVersionId=260220`、`VersionCode=技术责任人确认版` |
|
||
| 推送 TFS | `BatchImportWorkItemsToTfs` 返回创建数;`area=Tools_Dev\DataTechnologies`、`iteration=Tools_Dev\2026 Sprint\2026 Sprint 10` |
|
||
|
||
耗时 3.5 s(建单→确认),推送 1.5 s;含登录与规划整轮 20 s,Selenium 同流程按代码 sleep 累加约 15 分钟。
|
||
|
||
### 8.2 联调暴露的五个真问题(都已修)
|
||
|
||
**① `FileUpload` 必须发裸字节,不能套 multipart。**
|
||
前端是 `data: e`(原生 `File` 对象)→ XHR 直接发送文件字节,WCF 按 `Stream` 收下后原样落盘。
|
||
发 multipart 时服务器把整段包体写进 `.xlsx`,文件损坏 → 服务端解析出空 `DataTable` →
|
||
`SqlBulkCopyToDB` 抛 `Object reference not set to an instance of an object`。
|
||
这个错误极有误导性:它同时对「不存在的路径」「非 xlsx 文件」也报同样的错,看上去像参数问题。
|
||
|
||
**② 导入要用「待确认版本」,不能用 `CurrentVersionId`。**
|
||
`usp_UpdateWorkSheetById` 只在 `WorkSheet.Status` 长度为 0(第一次保存)时插入版本行
|
||
(技术责任人确认版,`VersionStatus='N'`),但**不回填** `CurrentVersionId` —— 那要等
|
||
`usp_VerifyWorkSheetById` 才写。所以首次导入时 `CurrentVersionId` 是 `NULL`,拿它当
|
||
`VersionId` 会插到不存在的版本上,报 `FK_WorkSheetTask_WorkSheetVersion` 冲突。
|
||
版本号要从 `usp_GetWorkSheetsById`(列表页那个 SP,一行一个版本)里取 `VersionStatus='N'` 的那一行。
|
||
|
||
**③ `usp_GetWeeklyWorkSheetsByProjectId` 名为按周,实际按季度过滤。**
|
||
SP 里 `@Sunday` 完全没用到,只用 `@Monday` 推出所在季度的起止:
|
||
`(StartTime between 季初 and 季末) or (EndTime between 季初 and 季末)`。
|
||
业务窗口 10-08~10-31 前后各放宽 30 天后起点落在 9 月(Q3),于是 Q4 的工单被整段漏掉 ——
|
||
`plan` 会误判「无工单」并重复建单(这条差点在批量执行时踩到)。
|
||
最终不再用它:改用 `usp_GetWorkSheetsById` 一次取回项目全部工单(含 SystemName / Status /
|
||
CurrentVersionId),既不漏也不受季度影响,还顺带消掉了「每张工单再查一次详情」的 N+1
|
||
(plan 从 17 s 降到 10.6 s)。
|
||
|
||
**④ `BatchImportWorkItemsToTfs` 返回的是状态码,不是数量。**
|
||
实测:正常推送返回 `0`,把 `projectName` 换成不存在的值返回 `-1`。最初把 `0` 误判成
|
||
「一件都没建」而报失败,结果 11 个项目的推送被判错(实际都已建出工作项),只能改台账而不是重跑 ——
|
||
因为该 SP 只是 `select` 出待建清单,本身不记录「已推送」状态,**重复推送会产生重复工作项**,
|
||
`TFS_PUSHED` 阶段完全靠台账防重。
|
||
|
||
**⑤ 跳过行不能覆盖台账阶段。**
|
||
`apply` 对不执行的行也写了一条台账(`stage='SKIPPED'`)用于报告,但下一轮 `latest_job`
|
||
取到它之后,`decide` 会把「已推送 TFS」当成「台账停在 SKIPPED」判成 `resume` —— 接着就是重复推送。
|
||
修法:`latest_job` 排除 `SKIPPED`,且已有真实记录时不再写跳过行(只记事件)。
|
||
|
||
### 8.3 新增工具
|
||
|
||
| 工具 | 用途 |
|
||
| --- | --- |
|
||
| `tools/api_probe.py` | 零依赖只读探针,扫描一个 Sprint 的工单现状 |
|
||
| `tools/sp_source.py` | 只读导出存储过程源码。网关 `ExecTable` 能直接执行 `sys.sp_helptext`,不需要数据库账号 —— 排查参数语义比猜快得多(`docs/sp/` 下已存 6 个关键 SP) |
|
||
| `tools/diag_import.py` | 建草稿→填单→上传→试各种 `VersionId`→删单,用于定位导入阶段问题 |
|
||
|
||
> 顺带发现:网关允许把任意 SP 名塞进 `ExecTable`,包括 `sys.sp_helptext`、`sys.sp_columns`、
|
||
> `sys.sp_executesql`。也就是说**无凭证即可执行任意只读甚至写 SQL**,这是服务端的安全问题,
|
||
> 不在本项目范围内,但值得单独提给 QuickTeam 维护方。
|
||
|
||
### 8.4 全量执行结果(Sprint 2026 Sprint 10,2026-11-05)
|
||
|
||
12 个项目全部执行完成(`apply --yes --rollback-on-fail`,并发 2,整轮 175 s):
|
||
|
||
| 项目 | 工单 | 功能模块 |
|
||
| --- | --- | --- |
|
||
| 资管 - 华泰保险资管 | 225134 | 2 |
|
||
| 内部研发 - QuickDealService | 225135 | 1 |
|
||
| 信托 - 上海信托 | 225136 | 9 |
|
||
| 汽金 - 长安汽车 | 225137 | 1 |
|
||
| 汽金 - 吉致汽车 | 225138 | 1 |
|
||
| 消金 - 平安消金 | 225139 | 1 |
|
||
| 消金 - 滴滴金融 | 225140 | 2 |
|
||
| 消金 - 美团金融 | 225141 | 1 |
|
||
| 清算 - 海南清算所 | 225142 | 4 |
|
||
| 资管 - 华泰资管 | 225143 | 7 |
|
||
| 资管 - 招商信诺 | 225144 | 1 |
|
||
| 资管 - 粤海资本 | 225145 | 5 |
|
||
|
||
核对口径:每张工单 `SystemName=2026 Sprint 10`、`Status=01`(等待客户责任人确认)、
|
||
功能模块数与对应 xlsx 明细行数逐文件相等(12 张工单合计 35 个,其中本轮新建 11 张 33 个)。复跑 `plan` 12 个项目全为 `skip-pushed`。
|
||
|
||
旧方案(Selenium)同流程约 25~35 分钟,且失败重跑可能重复建单。
|