Skip to content
项目
群组
代码片段
帮助
正在加载...
帮助
为 GitLab 提交贡献
登录
切换导航
U
ubains-module-test
项目
项目
详情
活动
周期分析
仓库
仓库
文件
提交
分支
标签
贡献者
分枝图
比较
统计图
议题
1
议题
1
列表
看板
标记
里程碑
合并请求
0
合并请求
0
CI / CD
CI / CD
流水线
作业
计划
统计图
Wiki
Wiki
代码片段
代码片段
成员
成员
折叠边栏
关闭边栏
活动
分枝图
统计图
创建新议题
作业
提交
议题看板
打开侧边栏
郑晓兵
ubains-module-test
Commits
5758f3c5
提交
5758f3c5
authored
9月 14, 2026
作者:
陈泽健
浏览文件
操作
浏览文件
下载
差异文件
Merge remote-tracking branch 'origin/platform-auto-test' (
10c8a1a2
另一窗口: 用例JSON修复)
上级
d8b67a54
10c8a1a2
展开全部
隐藏空白字符变更
内嵌
并排
正在显示
12 个修改的文件
包含
355 行增加
和
40 行删除
+355
-40
_计划执行_5.44定时任务失败用例逐条修复.md
Docs/PRD/问题处理/定时任务失败用例/_计划执行_5.44定时任务失败用例逐条修复.md
+0
-0
_执行计划_表格选择器类号不稳定导致回放失败.md
Docs/PRD/问题处理/录制器/_执行计划_表格选择器类号不稳定导致回放失败.md
+27
-23
_问题处理_表格选择器类号不稳定导致回放失败.md
Docs/PRD/问题处理/录制器/_问题处理_表格选择器类号不稳定导致回放失败.md
+0
-0
_执行计划_5.44定时执行被取消导致钉钉报告通过率15.25%.md
Docs/PRD/问题处理/执行中心/_执行计划_5.44定时执行被取消导致钉钉报告通过率15.25%.md
+11
-4
_执行计划_修复执行中心用例JSON双重编码报错.md
Docs/PRD/问题处理/执行中心/_执行计划_修复执行中心用例JSON双重编码报错.md
+176
-0
_问题处理_5.44定时执行被取消导致钉钉报告通过率15.25%.md
Docs/PRD/问题处理/执行中心/_问题处理_5.44定时执行被取消导致钉钉报告通过率15.25%.md
+5
-3
_问题处理_5.60执行中心用例JSON双重编码报错.md
Docs/PRD/问题处理/执行中心/_问题处理_5.60执行中心用例JSON双重编码报错.md
+0
-0
HANDOFF_UI自动化.md
HANDOFF_UI自动化.md
+86
-2
HANDOFF_UI自动化_用例修复.md
HANDOFF_UI自动化_用例修复.md
+0
-0
batch.py
backend/app/routers/batch.py
+3
-1
cases.py
backend/app/routers/cases.py
+37
-5
docker-compose.yml
deploy/docker-compose.yml
+10
-2
没有找到文件。
Docs/PRD/问题处理/定时任务失败用例/_计划执行_5.44定时任务失败用例逐条修复.md
浏览文件 @
5758f3c5
此差异已折叠。
点击以展开。
Docs/PRD/问题处理/录制器/_执行计划_表格选择器类号不稳定导致回放失败.md
浏览文件 @
5758f3c5
...
...
@@ -4,7 +4,7 @@
> **创建日期**: 2026-09-09
> **作者**: czj
> **关联问题**: [_问题处理_表格选择器类号不稳定导致回放失败.md](_问题处理_表格选择器类号不稳定导致回放失败.md)
> **状态**:
执行中
> **状态**:
已完成
> **关联执行**: exec_5f866bda62c0466a9db881f8ca501375(手动执行-2026/9/9 14:39:52,5.60 环境)
> **关联用例**: case_aafbdfb50fa5401a92ba3f8145b8bf07「完整预约流程验证」(VNC 录制)
...
...
@@ -22,32 +22,33 @@
### 阶段 1:代码修改(recorder_engine.py + playwright_executor.py)
-
[
]
1.1
`backend/app/services/recorder_engine.py`
-
[
x
]
1.1
`backend/app/services/recorder_engine.py`
-
`stableClasses()`
过滤
`el-table_\d+(_column_\d+)?$`
(生成类)
-
`buildCandidates()`
新增表格作用域候选(
`.el-table >> nth=<表序>`
+ 结构位)
-
**新增 pickerHost 逻辑**
:文本输入若位于
`.el-date-editor`
/
`.el-select`
等容器内,不再忽略聚焦点击(记录容器 click 触发面板)
-
[
]
1.2
`backend/app/executors/playwright_executor.py`
-
[
x
]
1.2
`backend/app/executors/playwright_executor.py`
-
`L2685 _selector_exists_fast()`
失败后 2s 重试一次再判死
### 阶段 2:用例数据修复
-
[
]
2.1 写脚本
`backend/scripts/fix_case_5f86.py`
更新 case_aafbdfb50fa5401a92ba3f8145b8bf07 8 个表格步骤为稳定选择器
-
[
x
]
2.1 写脚本
`backend/scripts/fix_case_5f86_step17.py`
更新 case_aafbdfb50fa5401a92ba3f8145b8bf07 表格步骤为稳定选择器 + 插入时间面板触发步骤
### 阶段 3:部署 5.60
-
[
]
3.1 本地修改后
`npm run build`
前端
-
[
]
3.2 scp dist + backend 服务到 5.60
-
[
]
3.3
`docker compose restart app`
-
[
x
]
3.1 本地修改后
`npm run build`
前端
-
[
x
]
3.2 scp dist + backend 服务到 5.60
-
[
x
]
3.3
`docker compose restart app`
### 阶段 4:重新执行验证
-
[
]
4.1 重跑 case_aafbdfb50fa5401a92ba3f8145b8bf07(手动或脚本)
-
[
]
4.2 验证两表勾选通过、无黑边
-
[
x
]
4.1 重跑 case_aafbdfb50fa5401a92ba3f8145b8bf07(手动或脚本)
-
[
x
]
4.2 验证两表勾选通过、时间面板步骤通过、后续步骤全部通过
### 阶段 5:归档
-
[
]
5.1 两份文档状态更新为「已完成」
-
[
]
5.2 HANDOFF_UI自动化.md 增补会话 66 章节
-
[
x
]
5.1 两份文档状态更新为「已完成」
-
[
x
]
5.2 HANDOFF_UI自动化.md 增补会话 66 章节
---
...
...
@@ -55,30 +56,33 @@
### 阶段 1-2(2026-09-09 本地实施)
-
recorder_engine.py
两处修改
-
recorder_engine.py
三处修改(stableClasses 过滤 + 表格作用域候选 + pickerHost 面板触发)
-
playwright_executor.py 两处修改
-
fix_case_5f86.py 编写完成(待部署后运行)
-
fix_case_5f86
_step17
.py 编写完成(待部署后运行)
### 阶段 3-4(2026-09-09 部署 5.60)
-
dist 部署 + 容器重启
-
重新执行用例通过(表格步骤全部通过)
-
fix_case_5f86_step17.py 在容器内运行(36→37 步,插入「点击会议开始时间」+ 时间项稳定选择器)
-
重新执行用例通过(37/37 全部通过,含时间面板打开 + 时间选择步骤)
### 阶段 5(归档)
-
两份文档状态更新
-
归档文档
[
_归档_5f866bda62c0466a9db881f8ca501375.md
](
_归档_5f866bda62c0466a9db881f8ca501375.md
)
已生成
-
HANDOFF_UI自动化.md 增补
---
## 四、验收标准
| 测试项 | 预期结果 |
|--------|----------|
| 录制器 | 新录制的表格勾选步骤候选中不含
`el-table_\d+_column_\d+`
|
| 执行器 | 表格选择器在异步渲染后仍能被命中 |
| 用例 | case_aafbdfb50fa5401a92ba3f8145b8bf07 重新执行通过,步骤 13-18 全部通过 |
| 回归 | 非 table 元素的录制候选行为不变 |
| 测试项 | 预期结果 | 实际结果 |
|--------|----------|----------|
| 录制器 | 新录制的表格勾选步骤候选中不含
`el-table_\d+_column_\d+`
| ✅ 已过滤 |
| 录制器 | 时间面板触发点击不再被忽略 | ✅ pickerHost 逻辑 + 单测通过 |
| 执行器 | 表格选择器在异步渲染后仍能被命中 | ✅ 2s 重试缓冲 |
| 用例 | case_aafbdfb50fa5401a92ba3f8145b8bf07 重新执行通过,步骤 13-18 全部通过 | ✅ exec_cf9a0c1ff55b47219af3ee01491ff2de 37/37 通过 |
| 回归 | 非 table 元素的录制候选行为不变 | ✅ tests/test_recorder_e2e.py 15/15 通过 |
---
...
...
@@ -86,9 +90,9 @@
| 文件 | 角色 |
|------|------|
|
`backend/app/services/recorder_engine.py`
| 录制器候选生成(本次
唯一
改动点) |
|
`backend/app/executors/playwright_executor.py`
| 执行器快速探测(本次
唯一
改动点) |
|
`backend/scripts/fix_case_5f86.py`
| 用例数据修复脚本 |
|
`backend/app/services/recorder_engine.py`
| 录制器候选生成(本次改动点) |
|
`backend/app/executors/playwright_executor.py`
| 执行器快速探测(本次改动点) |
|
`backend/scripts/fix_case_5f86
_step17
.py`
| 用例数据修复脚本 |
|
`backend/scripts/probe_table_classes.py`
| 排查脚本(已完成) |
|
`backend/scripts/probe_replay_exec5f86.py`
| 回放复现脚本(已完成) |
...
...
Docs/PRD/问题处理/录制器/_问题处理_表格选择器类号不稳定导致回放失败.md
浏览文件 @
5758f3c5
此差异已折叠。
点击以展开。
Docs/PRD/问题处理/执行中心/_执行计划_5.44定时执行被取消导致钉钉报告通过率15.25%.md
浏览文件 @
5758f3c5
# 执行计划 — 5.44 定时执行被取消导致钉钉报告通过率仅 15.25%
> **版本**: 2026-09-09(v
2,按用户确认方案更新:停止通知替代静默跳过
)
> **版本**: 2026-09-09(v
3:链接定稿为直达 UI 自动化执行中心 `/execution/ui`
)
> **模块**: 执行中心 / 定时任务 / 钉钉通知
> **优先级**: P0(立即修复,误导性告警)
> **状态**: 已完成(代码 + 单测 483/483 通过
,待三台
部署)
> **状态**: 已完成(代码 + 单测 483/483 通过
;**5.60 已部署 + 实测**,5.44/5.202 待
部署)
---
...
...
@@ -92,13 +92,14 @@ INFO: 192.168.9.51:54221 - "POST /api/executions/exec_0043d9cc.../cancel HTTP/1.
⏰ 停止时间:2026-09-09 11:54:02
📎
查看本次执行报告(超链接,直达 `{PLATFORM_BASE_URL}/api/reports/generate/{exec_id}`)
📎
[访问测试管理平台-执行中心]({PLATFORM_BASE_URL}/execution/ui)
```
设计要点:
-
**无通过率行**
——中断执行的数据不完整,给通过率必然误导(本次 15.25% 事件的根源);仅计数。
-
**标题独立**
(「UI自动化执行已停止」),与常规报告「UI自动化定时报告」区分,群成员一眼分辨。
-
**@策略**
:手动取消不@人(操作方本人已知);异常中断(看门狗/进程重启/引擎异常)按配置@负责人。
-
**链接定稿(v3,用户 3 连纠正)**
:v1 直达报告生成接口
`/api/reports/generate/{exec_id}`
→ 用户否决「怎么会是跳转到报告界面?」;v2 平台根路径 → 用户否决「应该是跳转到UI自动化模块下的执行中心才对」;
**v3 定稿 `{PLATFORM_BASE_URL}/execution/ui`**
(前端路由「自动化测试 → 执行中心」,见
`frontend/src/router/index.ts`
)。
-
中断判定:
`cancelled`
→ 手动取消;
`failed`
+ error_message 命中中断关键词(看门狗/进程重启/被中断等)→ 异常中断;
`failed`
且无用例结果 → 引擎异常。有完整结果统计的
`failed`
(真实失败)仍走常规 ⚠️ 报告。
## 四、执行计划(实际落地)
...
...
@@ -135,10 +136,16 @@ INFO: 192.168.9.51:54221 - "POST /api/executions/exec_0043d9cc.../cancel HTTP/1.
## 五、验收标准
-
[
x
]
手动取消一条运行中执行 → 钉钉群收到「🛑 执行已停止」通知(非通过率报告),报告中心带 cancelled 标识(单测覆盖)
-
[
x
]
重启容器打断执行 → 恢复后被中断用例状态为
`skipped`
,通过率统计不含假失败(单测覆盖)
## 五、验收标准
-
[
x
]
手动取消一条运行中执行 → 钉钉群收到「🛑 执行已停止」通知(非通过率报告),报告中心带 cancelled 标识(单测覆盖)
-
[
x
]
重启容器打断执行 → 恢复后被中断用例状态为
`skipped`
,通过率统计不含假失败(单测覆盖)
-
[
x
]
后端全量 pytest 通过(483/483)
-
[
]
三台同步部署(5.44 / 5.202 / 5.60)——
**待执行**
-
[
x
]
**5.60 部署完成**
:
`deploy_to_60.py`
同步核心服务
`dingtalk_notify_service.py`
+
`execution_service.py`
(MD5 一致)→ 重启 →
`/health`
healthy → 容器内 md5sum 复核一致
-
[
x
]
**5.60 停止通知实测**
:
`send_test_stop_notice_60.py`
推送 🛑 停止通知成功(
`{'errcode': 0, 'errmsg': 'ok'}`
),末尾链接
`[访问测试管理平台-执行中心](http://192.168.5.60/execution/ui)`
直达执行中心
-
[
]
**5.44 / 5.202 部署**
——待执行
-
[
x
]
前端取消按钮提示文案生效(本地构建验证)
## 六、风险与缓解
...
...
Docs/PRD/问题处理/执行中心/_执行计划_修复执行中心用例JSON双重编码报错.md
0 → 100644
浏览文件 @
5758f3c5
# 执行计划文档 - 修复执行中心用例JSON字段双重编码报错
> **文档类型**: 计划执行文档
> **创建日期**: 2026-09-10
> **作者**: czj
> **优先级**: P0
> **关联问题**: `_问题处理_5.60执行中心用例JSON双重编码报错.md`
---
## 一、改动总览
| 文件 | 改动类型 | 改动量 | 说明 |
|------|---------|--------|------|
|
`backend/app/routers/cases.py`
| 修改 | ~40 行 | 新增
`_decode_json_field()`
,
`_case_to_response`
四个 JSON 字段容错解码 |
|
`backend/app/routers/batch.py`
| 修改 | ~10 行 | 批量复制改走
`_case_to_response`
,消除
`model_validate`
直通风险 |
|
`backend/scripts/repair_double_encoded_json.py`
| 新增 | ~180 行 | MySQL 双重编码 JSON 列检测+修复脚本(默认 dry-run,
`--apply`
执行) |
| 5.60 服务器 MySQL | 数据修复 | 67 行 × 3 列 |
`tags`
/
`steps`
/
`config`
单层解码还原(先备份) |
---
## 二、详细执行步骤
### Step 1:cases.py 响应构造防御
**文件**
:
`backend/app/routers/cases.py`
1.
顶部补充
`import json`
2.
在
`_case_to_response`
上方新增模块级工具函数:
```
python
def
_decode_json_field
(
value
,
expected_type
,
default
):
"""
容错解码 JSON 字段(防御 MySQL 中双重编码的 JSON 字符串)
背景:部分历史数据在 MySQL JSON 列中被写成"JSON 字符串再包一层 JSON 文本"
(如 '"[
\\
"a
\\
"]"'),ORM 读出为 str,直接传给 Pydantic 强类型字段会
校验失败并导致整个列表接口 500。此处统一做一次解码兜底。
Args:
value: ORM 读出的原始值(list/dict/str/None)
expected_type: 期望类型(list 或 dict),用于校验解码结果
default: 解码失败时的回退默认值
Returns:
解码后的 list/dict,或 default
"""
if
value
is
None
:
return
default
if
isinstance
(
value
,
(
list
,
dict
)):
return
value
if
isinstance
(
value
,
str
):
try
:
parsed
=
json
.
loads
(
value
)
if
isinstance
(
parsed
,
expected_type
):
return
parsed
except
(
ValueError
,
TypeError
):
pass
return
default
```
3.
`_case_to_response`
中四个字段改写:
```
python
tags
=
_decode_json_field
(
c
.
tags
,
list
,
[]),
steps
=
_decode_json_field
(
c
.
steps
,
(
list
,
dict
),
[]),
config
=
_decode_json_field
(
c
.
config
,
dict
,
{}),
parameters
=
_decode_json_field
(
getattr
(
c
,
"parameters"
,
None
),
dict
,
{}),
```
> 说明:steps 期望类型为 `(list, dict)`,因为 UI 用例存步骤列表、安全用例存配置字典。
### Step 2:batch.py 批量复制路径同源修复
**文件**
:
`backend/app/routers/batch.py`
`batch_copy_cases`
中:
```
python
# 修改前
return
[
TestCaseResponse
.
model_validate
(
c
)
for
c
in
new_cases
]
# 修改后(复用 cases.py 的防御构造,并把响应转回 camelCase 模型)
from
app.routers.cases
import
_case_to_response
items
=
[
_case_to_response
(
c
)
for
c
in
new_cases
]
return
[
TestCaseResponse
.
model_validate
(
i
.
model_dump
(
by_alias
=
True
))
for
i
in
items
]
```
> 说明:`_case_to_response` 已完成字段解码,再经 `model_dump(by_alias=True)` + `model_validate` 保持原返回结构(camelCase)不变。
### Step 3:数据修复脚本
**文件**
:
`backend/scripts/repair_double_encoded_json.py`
(新增)
功能设计:
1.
连接 MySQL(默认
`192.168.5.60:3307/plat_auto_test`
,支持环境变量覆盖)
2.
检测阶段:对
`test_cases`
的
`tags`
/
`steps`
/
`config`
/
`parameters`
四列扫描
```sql
SELECT id, name FROM test_cases
WHERE JSON_TYPE(<col>)='STRING' AND JSON_VALID(JSON_UNQUOTE(<col>))
```
3.
修复阶段(
`--apply`
时):
-
先建备份表
`test_cases_bak_YYYYMMDD`
(仅含受影响行)
-
逐列执行
`UPDATE test_cases SET <col> = CAST(JSON_UNQUOTE(<col>) AS JSON) WHERE ...`
-
复检:再跑一次检测 SQL,确认归零
4.
默认 dry-run,只打印将受影响的行;
`--apply`
才落库
### Step 4:本地验证
1.
`cd backend && python -c`
单测脚本:构造
`tags='["a"]'`
、
`steps='{"k":1}'`
、
`config='{}'`
的假 ORM 对象,调
`_case_to_response`
断言输出类型正确
2.
`pytest tests/ -v -k "case"`
(若存在用例相关测试)确认无回归
### Step 5:部署 5.60
1.
scp 上传改动文件到
`/data/third_party/plat-auto-test/backend/app/routers/`
与
`backend/scripts/`
2.
`cd /data/third_party/plat-auto-test/deploy && docker compose restart app`
3.
数据修复(脚本在容器内或宿主机直连 3307 执行,先 dry-run 后
`--apply`
)
4.
回归验证:
```bash
curl -s 'http://127.0.0.1/api/cases?limit=500' | python3 -c "
import json,sys; d=json.load(sys.stdin)
sec=[c for c in d['items'] if c.get('caseType')=='security']
print('total:', d['total'], '| security:', len(sec))
assert all(isinstance(c['tags'], list) for c in d['items'])
assert all(isinstance(c['config'], dict) for c in d['items'])
print('✅ 全部字段类型正确')
"
```
5.
浏览器打开执行中心/用例管理页面确认可用
---
## 三、验证步骤
| # | 验证项 | 方法 | 通过标准 |
|---|--------|------|---------|
| 1 | 列表接口不再 500 |
`curl /api/cases?limit=500`
| HTTP 200 |
| 2 | 字段类型正确 | 解析响应 JSON | 所有 items 的 tags 为 list、config 为 dict |
| 3 | 详情接口 |
`curl /api/cases/sec_2_1_1`
| 200,steps 为 dict |
| 4 | 批量复制 | 用例管理页对 sec 用例执行复制 | 201,不报 500 |
| 5 | 数据库归零 | 修复脚本再跑检测 | 受影响行数 = 0 |
| 6 | 本地回归 |
`pytest tests/`
| 通过 |
---
## 四、风险评估
| 风险 | 等级 | 缓解措施 |
|------|------|---------|
| 数据修复误伤正常 STRING 类型 JSON 值 | 低 | 检测条件限定
`JSON_VALID(JSON_UNQUOTE(col))`
(内容必须是合法 JSON 文本才修);修复前自动备份 |
| 修复脚本重复执行 | 无 | 修复后检测归零,脚本幂等 |
| 代码防御掩盖未来写入侧 bug | 低 | 解码仅在 str 场景触发,正常数据零开销;问题处理文档已记录污染链路供后续排查写入侧 |
| 5.60 重启容器造成短暂不可用 | 低 | 与日常发版一致,重启耗时约 30s |
| batch.py 返回结构变化 | 无 | 经
`by_alias=True`
dump 后再 validate,camelCase 输出不变 |
## 五、实际执行记录(2026-09-10)
计划与实际的偏差及结果:
| Step | 计划 | 实际 |
|------|------|------|
| 1 | cases.py 加
`_decode_json_field`
| 完成;pytest 过程中发现类型分派缺陷(
`parameters='[]'`
解码为 list ≠ dict 需回退默认值),已改为统一 expected_type 校验 |
| 2 | batch.py 走
`_case_to_response`
| 完成 |
| 3 | 修复脚本 dry-run 后
`--apply`
| dry-run 检测
**0 条**
——67 行脏数据在取证到部署的窗口内已被其他途径恢复(无备份表佐证),无需
`--apply`
;脚本留作幂等巡检 |
| 4 | 本地验证 | 单测断言全过 +
`pytest tests/`
**487 passed**
|
| 5 | scp cases.py/batch.py/脚本 → restart → 验证 | 首次部署后 /api/cases 仍 500,
**新根因**
:5.60 代码基线为旧版(case_service/schemas 无 project_type),HEAD 版 cases.py 透传
`project_type`
撞签名不匹配。改推
**适配版**
`backend/_hotfix_cases_deploy.py`
(父基线+防御、剥离 project_type 透传)后通过 |
| 6 | 回归验证 | 列表 200/total=448/类型全对、详情 200 steps=dict、批量复制 200+清理 204、日志无 ERROR —— 全部通过 |
### 5.60 服务器基线结论(重要)
部署目录非 git 仓库,基线为混合版本:大部分文件 = 522ab2f6^(project_type 特性之前),但
`database.py`
/
`models/test_case.py`
已被单独热修(含 project_type),另有服务器特有裁剪/新增(scheduler_service 缺钉钉块、module.py 多 project_id)。
**严禁整目录覆盖**
;project_type 特性上线须 13 文件整体同步+回归。
---
*本文档由 Claude Code 生成,遵循项目计划执行文档规范。*
Docs/PRD/问题处理/执行中心/_问题处理_5.44定时执行被取消导致钉钉报告通过率15.25%.md
浏览文件 @
5758f3c5
...
...
@@ -3,7 +3,7 @@
> **日期**: 2026-09-09
> **模块**: 执行中心 / 定时任务 / 钉钉通知
> **级别**: P1(数据误导,非功能损坏)
> **状态**: 代码修复完成(单测 483/483 通过)
,待三台部署 + 实弹验证
> **状态**: 代码修复完成(单测 483/483 通过)
;**5.60 已部署 + 实测停止通知发送成功**;5.44 / 5.202 待部署
---
...
...
@@ -73,7 +73,7 @@ INFO: 192.168.9.51:54221 - "POST /api/executions/exec_0043d9cc.../cancel HTTP/1.
⏰ 停止时间:2026-09-09 11:54:02
📎
查看本次执行报告 ← 钉钉内可点击,直达该执行的报告页
📎
[访问测试管理平台-执行中心](http://192.168.5.44:8081/execution/ui) ← 钉钉内可点击,直达「自动化测试 → 执行中心」页面
```
@策略:手动取消不@人;异常中断(看门狗/进程重启/引擎异常)按配置@负责人。
...
...
@@ -83,7 +83,9 @@ INFO: 192.168.9.51:54221 - "POST /api/executions/exec_0043d9cc.../cancel HTTP/1.
-
[
x
]
后端全量 pytest 通过(483/483,含取消→停止通知、看门狗→停止通知、真实失败→常规报告用例)
-
[
x
]
中断用例标
`skipped`
不再假失败(启动恢复 + 看门狗路径单测覆盖)
-
[
x
]
前端取消按钮提示文案生效(本地构建验证)
-
[
]
三台同步部署(5.44 / 5.202 / 5.60)——
**待执行**
(部署前按会话 63 流程预检 running 执行)
-
[
x
]
**5.60 部署完成**
:
`deploy_to_60.py`
同步
`dingtalk_notify_service.py`
+
`execution_service.py`
(MD5
`fb594a6c…`
一致)→ 重启 →
`/health`
healthy → 容器内 md5sum 复核一致
-
[
x
]
**5.60 停止通知实测**
:
`send_test_stop_notice_60.py`
推送 🛑 停止通知成功(
`{'errcode': 0, 'errmsg': 'ok'}`
),末尾链接
`[访问测试管理平台-执行中心](http://192.168.5.60/execution/ui)`
直达执行中心
-
[
]
**5.44 / 5.202 部署**
——待执行(部署前按会话 63 流程预检 running 执行,5.44 有每日定时 4h+ 任务注意撞车)
-
[
]
实弹验证:下一次定时执行被取消/中断后,钉钉群收到的是停止通知而非 15.25% 式通过率报告
---
...
...
Docs/PRD/问题处理/执行中心/_问题处理_5.60执行中心用例JSON双重编码报错.md
0 → 100644
浏览文件 @
5758f3c5
此差异已折叠。
点击以展开。
HANDOFF_UI自动化.md
浏览文件 @
5758f3c5
...
...
@@ -3,11 +3,95 @@
> **生成时间**: 2026-09-10
> **当前分支**: `platform-auto-test`
> **最近提交**: `42506263` feat(ui-recorder): 有头模式放大操作 + 流畅度优化(Phase 1-4 闭环)
> **状态**: 🟢 **会话
70 完成(2026-09-10):用例录制器全屏放大 + 菜单迁移/改名 + requestFullscreen 修复——修复版已重部署 5.60 并实机复验通过(Phase 1-4 闭环);Phase 5(5.44/5.202 同步)待用户安排
**(历史会话见下章节)
> **状态**: 🟢 **会话
72 完成(2026-09-11):5.60 UI用例管理 500 修复(legacy step_number → StepDefinition 迁移 68 条)+ 5.44/5.202 同步修复(两库各 113 条步骤缺 order/name 规范化完毕),三台全量分页验证全部 HTTP 200
**(历史会话见下章节)
---
## 📊 当前状态(会话 70,2026-09-10 · 用例录制器全屏放大 + 菜单迁移/改名)
## 📊 当前状态(会话 72,2026-09-11 · 5.60/5.44/5.202 UI用例步骤格式 500 修复与三台验证)
**背景**
:5.60 UI 用例管理先出现
`TestCaseResponse`
校验失败(旧步骤使用
`step_number`
、
`target_url`
、
`assert_type`
顶层字段),随后同步核查 5.44 / 5.202 时发现两台也存在一批录制/导入历史步骤缺少
`order`
/
`name`
,导致特定分页返回 HTTP 500。
### ✅ 根因
-
**5.60**
:68 条历史用例使用 legacy flat step 格式,缺少
`order`
,且 URL/断言字段未嵌入
`params`
;Pydantic
`StepDefinition`
严格校验时触发
`Field required`
。
-
**5.44 / 5.202**
:两库各有 113 条 steps 数组步骤缺少
`order`
,部分还使用
`description`
代替
`name`
;数据库统计「无
`step_number`
」无法覆盖这类新旧混合格式,因此最初误判为已清洁。
-
API
`/api/cases`
将整批 ORM 数据直接构造为
`TestCaseResponse`
,单条脏步骤即可使对应分页整体 500。
### ✅ 修复动作
1.
**5.60 已完成的数据迁移**
:在 app 容器内执行
`_migrate_legacy_steps_560.py`
,将 68 条转换为
`order / name / action / params / expected`
;
`step_number → order`
、
`target_url → params.url`
、
`assert_type → params.type`
,复核
`remaining legacy = 0`
。
2.
**5.44 / 5.202 数据规范化**
:对两台全量扫描
`test_cases.steps`
,发现各 113 条缺少
`order`
的历史步骤;补齐递增
`order`
、将
`description`
回填为
`name`
、确保
`params`
为字典,并保留原有 action/params 内容。实际发生字段变化的用例各 30 条(其余 83 条为混合结构或已含必需字段)。
3.
**临时脚本清理**
:远端
`/tmp`
中的迁移/规范化脚本已删除;本地临时迁移脚本也已删除。此次生产数据修复未提交 Git 代码或数据库文件。
### ✅ 三台验证结果
| 环境 | API 地址 | 修复前问题 | 修复后验证 |
|---|---|---|---|
| 5.60 |
`http://192.168.5.60`
| legacy 步骤导致 UI 用例列表 500 |
`GET /api/cases?case_type=ui&skip=0&limit=20`
HTTP 200;legacy=0 |
| 5.44 |
`http://192.168.5.44:8081`
| 某些分页(skip 60~90)HTTP 500 | 全量 UI 分页 HTTP 200;total=385;首条步骤含
`order=1`
|
| 5.202 |
`http://192.168.5.202:8081`
| 某些分页(skip 60~90)HTTP 500 | 全量 UI 分页 HTTP 200;total=388;首条步骤含
`order=1`
|
> 注意:5.44 / 5.202 的平台容器映射为 `8081:80`,访问宿主机 80 会命中其他 nginx/管理平台并返回 404;必须使用 `:8081`。容器内 API 访问 `http://localhost/api/...`。
### ⚠️ 遗留与后续建议
1.
**建议补代码防御**
:当前本地
`backend/app/routers/cases.py`
的
`_decode_json_field`
只能解 JSON 外层类型,尚未自动把 legacy step 对象规范化;后续应增加
`_normalize_steps`
,在
`_case_to_response`
中将
`step_number/description/target_url/assert_type`
统一转换,防止未来导入脏数据再次导致整页 500。
2.
**5.44 / 5.202 各仍有 83 条未自动改写的历史混合结构**
:本次 API 全分页已经通过,但建议后续抽样核查这些用例的
`name/action/params`
是否满足完整
`StepDefinition`
语义,不要直接全量覆盖未知结构。
3.
本次未部署前端/后端代码,仅修复生产 MySQL 中的 steps JSON;如后续上线代码防御,需按三台环境分别部署并避开正在执行的任务。
4.
不要把远程数据修复临时脚本复制回仓库提交;如需保留迁移能力,应整理为正式、可 dry-run/备份/回滚的运维脚本后再走评审。
---
## 📊 历史状态(会话 71,2026-09-10 · 5.60 执行中心用例 JSON 双重编码报错修复)
**背景**
:5.60 生产服务器(192.168.5.60)访问执行中心/用例管理页面报「服务器内部错误」——
`GET /api/cases`
返回 500,Pydantic 校验失败:67 条安全测试用例(
`sec_*`
)的 tags/steps/config 在 MySQL JSON 列中被写成「JSON 字符串再包一层 JSON 文本」(
**双重编码**
),ORM 读出为 str,
`_case_to_response`
的
`or []`
只兜 None 兜不住「值存在但类型错误」→ 一条脏数据炸整个列表接口。
### ✅ 根因(5.60 MySQL 实测实锤)
-
`test_cases`
448 行中 67 行(全部
`case_type='security'`
)的 tags/steps/config 三列
`JSON_TYPE='STRING'`
且
`JSON_VALID(JSON_UNQUOTE(col))`
;parameters 与 executions.config(1049 行)等干净
-
逐层解码证明:
`json.loads`
第 1 次仍为 str,第 2 次才是真数据(
`'"[\\"a\\"]"'`
→ str → list)
-
污染来源推断:某同步/迁移脚本以
**ORM 方式**
写入「已是 JSON 文本的字符串」→ SQLAlchemy 对 JSON 列 str 值再
`json.dumps`
一次形成双编码;raw SQL 写入的
`sync_to_560_full.py`
安全(MySQL 自动解析合法 JSON 文本)
### ✅ 修复(双管齐下,代码防御为主)
1.
**代码防御(治本,防再发)**
:
-
`backend/app/routers/cases.py`
:新增模块级
`_decode_json_field(value, expected_type, default)`
——list/dict 透传、str 尝试
`json.loads`
且类型符合预期才返回、否则回退默认值;
`_case_to_response`
四个 JSON 字段(tags/steps/config/parameters)全部改走该函数
-
`backend/app/routers/batch.py`
:批量复制
`TestCaseResponse.model_validate(c)`
→ 复用
`_case_to_response`
,消除同源隐患
-
修复过程中本地 pytest 抓出
**类型分派缺陷**
:
`parameters='[]'`
解码为 list ≠ dict 需回退
`{}`
(统一 expected_type 校验后修复)
2.
**数据修复(治标,清现有脏数据)**
:新增
`backend/scripts/repair_double_encoded_json.py`
(dry-run 默认;
`--apply`
先备份
`test_cases_bak_YYYYMMDD`
再
`CAST(JSON_UNQUOTE(col) AS JSON)`
,复检归零,幂等)
### 🔍 两个重要发现
1.
**脏数据已自愈**
:部署时 dry-run 检测
**0 条**
——67 行在取证到部署的窗口内被其他途径恢复(无备份表佐证,脚本
`--apply`
未执行过),数据修复步骤无需执行,脚本留作幂等巡检工具
2.
**5.60 代码基线 ≠ git HEAD(重要约束,勿踩)**
:5.60 backend 部署目录非 git 仓库,大部分文件停留在
`522ab2f6^`
(project_type 特性
**之前**
):
-
直接部署 HEAD 版 cases.py(含 project_type 透传)→ 新 500
`CaseService.list() got an unexpected keyword argument 'project_type'`
-
已生成
**适配版**
`backend/_hotfix_cases_deploy.py`
(父基线 +
`_decode_json_field`
,剥离 project_type 透传)部署生效(gitignore 已忽略
`backend/_*.py`
)
-
服务器另有特有热修:module.py 多 project_id、scheduler_service 缺钉钉通知块 →
**严禁整目录覆盖部署**
;project_type 特性(522ab2f6)如需上线 5.60 须 13 文件整体同步 + 回归
### ✅ 验证(全部通过)
| # | 验证项 | 结果 |
|---|--------|------|
| 1 | 本地 pytest |
**487 passed**
(含
`_decode_json_field`
类型分派回归修复) |
| 2 |
`GET /api/cases?limit=500`
(5.60) | HTTP 200,total=448,全部 items tags=list / config=dict / steps=list
\|
dict |
| 3 | 详情
`GET /api/cases/sec_2_1_1`
| 200,steps=dict(test_type=api_security),tags=
`['OWASP-API1','水平越权','IDOR']`
|
| 4 | 批量复制
`POST /api/batch/copy`
| 200(副本字段类型正确,已删除清理 204) |
| 5 | 容器 health / 错误日志 | health 200,无 ERROR |
### 📁 文档沉淀
-
`Docs/PRD/问题处理/执行中心/_问题处理_5.60执行中心用例JSON双重编码报错.md`
(已补「五、实施与验证记录」章节)
-
`Docs/PRD/问题处理/执行中心/_执行计划_修复执行中心用例JSON双重编码报错.md`
(已补「五、实际执行记录」章节)
### ⚠️ 待办(后续跟进)
1.
**本地 cases.py / batch.py 改动未提交 git**
(连同本次修复文档),待用户确认后提交
2.
**project_type 特性(522ab2f6)如需同步 5.60**
:13 文件整体同步 + 回归,不能单推 router 单文件
3.
脏数据恢复来源待查(疑似
`sync_to_560_full.py`
某次同步),修复脚本留作巡检
---
## 📊 历史状态(会话 70,2026-09-10 · 用例录制器全屏放大 + 菜单迁移/改名)
**背景**
:用户反馈录制器两个新需求:① noVNC 录制画面能否
**放大到整个浏览器窗口**
显示(便于精细观察/操作小元素);② 录制器从「辅助工具」菜单迁移到「自动化测试」菜单下,名字改为「
**用例录制器**
」。
**关键约束(用户明确)**
:全屏放大必须在不降低定位精度的前提下实现——如影响定位准确度则放弃该功能。
...
...
HANDOFF_UI自动化_用例修复.md
0 → 100644
浏览文件 @
5758f3c5
此差异已折叠。
点击以展开。
backend/app/routers/batch.py
浏览文件 @
5758f3c5
...
...
@@ -25,6 +25,7 @@ from app.schemas.batch import (
BatchOperationResponse
,
)
from
app.schemas.test_case
import
TestCaseResponse
from
app.routers.cases
import
_case_to_response
logger
=
logging
.
getLogger
(
__name__
)
...
...
@@ -113,7 +114,8 @@ async def batch_copy_cases(
request
.
target_module_id
)
logger
.
info
(
f
"批量复制完成: {stats}"
)
return
[
TestCaseResponse
.
model_validate
(
c
)
for
c
in
new_cases
]
# 走 _case_to_response 统一构造,附带 JSON 字段容错解码(防御双重编码脏数据)
return
[
_case_to_response
(
c
)
for
c
in
new_cases
]
except
Exception
as
e
:
logger
.
error
(
f
"批量复制失败: {str(e)}"
)
raise
HTTPException
(
status_code
=
500
,
detail
=
f
"批量复制失败: {str(e)}"
)
...
...
backend/app/routers/cases.py
浏览文件 @
5758f3c5
...
...
@@ -9,8 +9,9 @@
最后修改:2026-07-09
"""
import
json
import
logging
from
typing
import
Optional
,
List
from
typing
import
Optional
,
List
,
Union
from
fastapi
import
APIRouter
,
Depends
,
HTTPException
,
Query
from
sqlalchemy.ext.asyncio
import
AsyncSession
...
...
@@ -30,6 +31,37 @@ logger = logging.getLogger(__name__)
router
=
APIRouter
()
def
_decode_json_field
(
value
,
expected_type
:
tuple
,
default
):
"""
容错解码 JSON 字段(防御 MySQL 中双重编码的 JSON 字符串)
背景:5.60 生产库部分历史数据在 MySQL JSON 列中被写成"JSON 字符串
再包一层 JSON 文本"(如 '"[
\\
"a
\\
"]"'),ORM 读出为 str,直接传给
Pydantic 强类型字段会校验失败并导致整个列表接口 500。
此处统一做一次解码兜底。
Args:
value: ORM 读出的原始值(list/dict/str/None)
expected_type: 期望类型元组(如 (list,) 或 (list, dict)),用于校验解码结果
default: 解码失败时的回退默认值
Returns:
解码后的 list/dict,或 default
"""
if
value
is
None
:
return
default
if
isinstance
(
value
,
expected_type
):
return
value
if
isinstance
(
value
,
str
):
try
:
parsed
=
json
.
loads
(
value
)
if
isinstance
(
parsed
,
expected_type
):
return
parsed
except
(
ValueError
,
TypeError
):
pass
return
default
def
_case_to_response
(
c
)
->
TestCaseResponse
:
"""
统一构造用例响应对象(包含 case_type 等所有字段)
...
...
@@ -47,12 +79,12 @@ def _case_to_response(c) -> TestCaseResponse:
description
=
c
.
description
,
status
=
c
.
status
,
priority
=
c
.
priority
,
tags
=
c
.
tags
or
[]
,
steps
=
c
.
steps
or
[]
,
config
=
c
.
config
or
{}
,
tags
=
_decode_json_field
(
c
.
tags
,
(
list
,),
[])
,
steps
=
_decode_json_field
(
c
.
steps
,
(
list
,
dict
),
[])
,
config
=
_decode_json_field
(
c
.
config
,
(
dict
,),
{})
,
case_type
=
getattr
(
c
,
"case_type"
,
None
)
or
"ui"
,
project_type
=
getattr
(
c
,
"project_type"
,
"standard"
)
or
"standard"
,
parameters
=
c
.
parameters
or
{}
,
parameters
=
_decode_json_field
(
getattr
(
c
,
"parameters"
,
None
),
(
dict
,),
{})
,
created_at
=
c
.
created_at
,
updated_at
=
c
.
updated_at
,
)
...
...
deploy/docker-compose.yml
浏览文件 @
5758f3c5
...
...
@@ -14,7 +14,7 @@ services:
TZ
:
Asia/Shanghai
volumes
:
# MySQL 数据持久化
-
/data/third_party/plat-auto-test/mysql/data:/var/lib/mysql
-
/data/third_party/plat-auto-test/mysql/data:/var/lib/mysql
:Z
# 初始化脚本(仅首次启动执行)
-
./init.sql:/docker-entrypoint-initdb.d/init.sql:ro
ports
:
...
...
@@ -32,6 +32,10 @@ services:
-
--collation-server=utf8mb4_unicode_ci
-
--default-authentication-plugin=mysql_native_password
-
--max_connections=200
# Docker 20.10.7 默认 seccomp 白名单不含 clone3(EL9 镜像 glibc 用其建线程),
# MySQL 8.0.46 初始化报 "Can't create thread (errno: 1)"。放开 seccomp 修复。
security_opt
:
-
seccomp:unconfined
# ==================== 应用容器(前后端合一) ====================
app
:
...
...
@@ -55,10 +59,14 @@ services:
# 目标机资源监控(SSH 采集被测系统 192.168.5.44)。采样已走线程池不阻塞事件循环,
# 关闭会导致压测全程无目标机资源数据(2026-09-02 混合场景无监控数据问题根因)
-
TARGET_MONITOR_ENABLED=true
# Xvfb 虚拟桌面(有头模式录制用)
-
OPENBLAS_NUM_THREADS=1
-
OMP_NUM_THREADS=1
-
MKL_NUM_THREADS=1
-
XVFB_DISPLAY=:99
-
XVFB_SCREEN=1920x1080x24
-
NOVNC_PORT=6080
security_opt
:
-
seccomp:unconfined
volumes
:
# 后端源码(更新后端只需重启容器,无需重建镜像)
-
/data/third_party/plat-auto-test/backend:/app
...
...
编写
预览
Markdown
格式
0%
重试
或
添加新文件
添加附件
取消
您添加了
0
人
到此讨论。请谨慎行事。
请先完成此评论的编辑!
取消
请
注册
或者
登录
后发表评论