缺陷生命周期与高质量缺陷报告¶
一、一条被退回 5 次的缺陷¶
这是 2026-09 从 JIRA 导出的真实流转历史(时间已脱敏):
09-08 10:12 tester 新建 "订单页有bug",描述 21 字,无环境无版本
09-08 14:30 dev 已拒绝 "无法复现"
09-08 15:02 tester 重新打开 "还是不行"
09-09 09:40 dev 需要信息 "哪个环境?哪个账号?"
09-09 11:15 tester 重新打开 "测试环境,账号 test01"
09-10 10:00 dev 已修复 "修复了"
09-10 10:05 tester 重新打开 "没修好"
09-11 16:20 dev 已修复 commit 7ac1f02,构建 v2.3.2-rc3
周期时间 3 天 6 小时,开发实际改代码 40 分钟,来回评论 9 条。根因不是沟通态度,而是报告缺少构建号:09-10 开发在 v2.3.2-rc2 上修完,测试仍在 v2.3.1 的部署上验证。
这类成本可以量化:一个来回平均 30 分钟上下文切换,9 条评论 ≈ 4.5 人时,而补全报告只需要 3 分钟。
二、状态机与每次流转的准入条件¶
缺陷状态不是装饰,每个箭头都要有准入条件,缺一条就退回。
| 当前状态 | 事件 | 目标状态 | 准入条件(缺一条即退回) | 责任人 | 时限 |
|---|---|---|---|---|---|
| 新建 | 确认 | 已确认 | 步骤可执行、构建号、实际/期望、证据、复现率 | 开发 owner | 1 个工作日 |
| 新建 | 拒绝 | 已拒绝 | 理由码(重复/设计如此/无法复现/环境)+ 对照证据 | 开发 owner | 1 个工作日 |
| 已拒绝 | 关闭 | 已关闭 | 报告人同意,或 3 个工作日无异议 | 测试 | 3 个工作日 |
| 已确认 | 开始 | 修复中 | 指派 owner、填写计划修复版本 | 开发 owner | 当日 |
| 修复中 | 提测 | 待验证 | commit SHA、构建号/镜像 tag、影响范围、自测结论 | 开发 owner | 按 SLA |
| 待验证 | 通过 | 已关闭 | 在指定构建上按原步骤验证 + 回归范围执行 | 测试 | 1 个工作日 |
| 待验证 | 失败 | 重新打开 | 新证据:新构建号、新日志、新录屏 | 测试 | - |
| 任意 | 挂起 | 挂起 | 原因、解除条件、复查日期 | 提交人 | - |
「待验证 → 已关闭」最容易漏。只按原步骤验一遍不够,要加一条回归断言:这次修的是空指针,就要验证「券不存在」「券已过期」「券已使用」三个相邻分支,否则修 A 破 B。
用 JQL 找卡在状态里的缺陷,每周一跑:
project = QALAB AND status = "待验证" AND updated <= -3d
project = QALAB AND status = "修复中" AND created <= -7d
project = QALAB AND resolution = "Cannot Reproduce" AND created >= -90d
第一条超过 5 条,说明验证环节积压;第二条说明修复排队没人管;第三条用来算「复现失败率」。
三、四类非正常关闭怎么处理¶
| 类型 | 判定标准 | 必填字段 | 关闭后动作 |
|---|---|---|---|
| 重复 | 同一根因、同一现象,已有主单 | 主单 key + duplicates 链接 |
只保留主单计数,重复单不计入缺陷密度 |
| 无法复现 | 报告人 3 环境 × 5 次尝试均未复现,或日志无对应记录 | 尝试矩阵、时间窗 ±5 分钟、traceId | 转「待信息」7 天,仍无回应则关闭并标记原因 |
| 延期 | 本版本不修,但确认是缺陷 | 影响面、规避方案、目标版本、责任人 | 进 DDR(延期缺陷清单),版本发布评审逐条过 |
| 不修复 | 产品决策接受现状 | 决策人、替代方案、用户影响 | 在发布说明中登记,季度复盘重评 |
三个可执行细节:
- 查重不能靠肉眼。 建单前跑
summary ~ "关键词",或按 traceId 搜:project = QALAB AND text ~ "9f2c41ab7e"。 - 「无法复现」不是垃圾桶。 关闭必须写清「在哪些环境、试了几次、日志时间窗」。季度复现失败率 = 无法复现单 / 总缺陷,超过 15% 说明日志、环境或数据构造能力有缺口,不是测试手气差。
- 延期必须落在版本上。
labels = "deferred" AND "目标版本" is EMPTY应恒为 0 条。
四、差报告与好报告的逐项对比¶
差报告(原文照抄):
好报告:
标题:【订单】优惠券满 100 减 10,订单金额 100.00 时未抵扣,应付仍为 100.00(P1)
版本:order-service v2.3.1(镜像 tag 20260908-1a2b3c4),web 前端 2.18.0
环境:test 集群 / Chrome 128.0.6613.120 / Windows 11 23H2
账号:test01(uid=10086,已实名),券 C20260801001(面额 10,门槛 100,状态 AVAILABLE)
前置:购物车已有 SKU 1001 × 1,金额 100.00
复现步骤(必现,5/5):
1. 登录 test01,进入结算页
2. 选择券 C20260801001
3. 点击「提交订单」,停留在确认弹窗
实际:弹窗金额 orderAmount=100.00,payAmount=100.00,couponDiscount=0.00
期望:payAmount=90.00,couponDiscount=10.00
证据:
- HAR:order-preview-20260908.har(请求 #37)
- 请求:POST /api/v1/orders/preview,X-Request-Id: 9f2c41ab7e
- 响应:{"orderAmount":"100.00","payAmount":"100.00","couponDiscount":"0.00"}
- 服务端日志见附件 order-service-1402.log(traceId=9f2c41ab7e)
影响:所有门槛恰好等于订单金额的券,资金类,用户可感知
初步定位:OrderAmountCalculator.java:88 coupon 为 null,疑似券查询条件带了 status=AVAILABLE 但传了 lock_order_id
逐项对比:
| 维度 | 差报告 | 好报告 |
|---|---|---|
| 标题 | 「有 bug」 | 模块 + 触发条件 + 现象 + 金额 + 等级 |
| 版本 | 无 | 服务版本 + 镜像 tag + 前端版本 |
| 环境 | 「测试环境」 | 集群、浏览器完整版本、操作系统版本 |
| 数据 | 无 | 账号 uid、券 ID 及其全部关键字段 |
| 步骤 | 「下单的时候」 | 3 步,标注必现 5/5 |
| 实际/期望 | 「没反应」 | 三个金额字段的具体值与期望值 |
| 证据 | 无 | HAR + 请求 ID + 响应体 + 服务端日志行号 |
| 影响面 | 无 | 说明影响所有等门槛订单,资金类 |
| 定位线索 | 无 | 精确到类名与行号 |
五、可直接复制的报告模板¶
## 标题
【模块】在[场景]下[操作],出现[现象],[量化影响](P?)
## 环境
- 服务/版本:xxx v2.3.1,镜像 tag 20260908-1a2b3c4
- 前端:2.18.0 / Chrome 128.0.6613.120 / Windows 11 23H2 / 集群名
- 账号:test01(uid=10086,角色/实名状态)/ 数据 ID 及关键字段
## 前置条件
1. ...
## 复现步骤(必现/偶现 5/10)
1. ...
## 实际结果 / 期望结果
实际:字段级数值;期望:字段级数值(引用需求条目 AC-xx)
## 证据
- HAR / 录屏 / curl 命令 / 响应体(code、message、traceId)
- 服务端日志(时间窗 + traceId + 堆栈)/ DB 查询结果
## 影响面
影响用户范围、是否资金/数据类、是否有绕过方案
## 初步定位
类名:行号 / 接口 / 可疑 SQL / 最近变更 commit
命名规范统一成 {模块}-{场景}-{日期}-{traceId前8位}.har,否则三个月后没人找得到证据。
六、P0~P3 分级标准与判定示例¶
| 等级 | 业务影响 | 范围 | 绕过方案 | 响应/修复 | 发布影响 |
|---|---|---|---|---|---|
| P0 | 资金错误、数据丢失、核心链路不可用 | 全量或大比例用户 | 无 | 2 小时内响应,当日修复 | 阻塞发布,已上线则回滚 |
| P1 | 主要功能受损 | 特定场景/部分用户 | 无或需人工介入 | 当日响应,本迭代必修 | 阻塞发布 |
| P2 | 次要功能异常、体验明显受损 | 小范围 | 有明确绕过 | 本迭代或下迭代 | 不阻塞,需处理计划 |
| P3 | 文案、样式、低频易用性问题 | 个别 | 有 | 排期 | 不阻塞 |
三个判定示例:
- 支付回调成功但订单仍为待支付 → P0。资金已扣、状态未变,用户重复支付概率高,无绕过。
- 100.00 订单券不抵扣,用户可手动取消重下 → P1。资金类但存在用户可自助绕过,影响面是「等门槛订单」这一特定集合。
- 券列表第 2 页重复展示 1 条 → P2。数据展示错误、不影响领取与核销,刷新可绕过。
- 导出报表接口 500,仅 3 个内部运营使用 → 按业务影响定 P2,而不是因为「500 就是严重」。技术严重度不等于优先级。
分级争议的裁决口径写死:先看是否资金/数据错误,再看影响用户比例,最后看有无绕过。 三者冲突时以资金优先。
七、取证:五类可复制的命令¶
1. curl 重放¶
curl -sS -X POST 'https://test.qalab.cn/api/v1/orders/preview' \
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer eyJhbGciOiJIUzI1NiJ9...' \
-H 'X-Request-Id: 9f2c41ab7e' \
-d '{"skuId":1001,"count":1,"couponId":"C20260801001"}' \
-D headers.txt -o resp.json --trace-ascii trace.log \
-w 'http=%{http_code} total=%{time_total}s\n'
报告里贴这条命令,开发可以直接复制执行,省掉「你是怎么调的」这一轮。
2. 浏览器 HAR¶
DevTools → Network → 勾选 Preserve log → 复现 → 右键 Save all as HAR。分析失败与慢请求:
# 所有 5xx
jq -r '.log.entries[] | select(.response.status >= 500)
| [.request.method, .response.status, .request.url] | @tsv' order.har
# 耗时 Top5
jq -r '.log.entries[] | "\(.time|floor)ms\t\(.request.url)"' order.har | sort -rn | head -5
注意 HAR 默认包含 Authorization 与 Cookie,外发前脱敏:jq 'del(.log.entries[].request.headers[] | select(.name=="Authorization"))'。
3. Android 日志¶
adb shell pidof com.qalab.app # 输出 12345
adb logcat -c # 先清缓冲,避免捞到旧日志
adb logcat -v threadtime --pid=12345 > app.log
Select-String -Path app.log -Pattern "FATAL|AndroidRuntime|9f2c41ab7e"
09-08 14:02:11.382 12345 12390 E AndroidRuntime: FATAL EXCEPTION: main
09-08 14:02:11.382 12345 12390 E AndroidRuntime: Process: com.qalab.app, PID: 12345
09-08 14:02:11.382 12345 12390 E AndroidRuntime: java.lang.IllegalStateException: coupon already used
09-08 14:02:11.382 12345 12390 E AndroidRuntime: at com.qalab.coupon.CouponRepo.lock(CouponRepo.kt:57)
--pid 比 grep 包名 可靠,进程重启后 PID 变了要重新取。
4. 服务端 traceId¶
2026-09-08T14:02:11.382+08:00 ERROR 1 --- [http-nio-8080-exec-7] c.q.o.OrderController : traceId=9f2c41ab7e userId=10086 orderNo=SO20260908001
java.lang.NullPointerException: Cannot invoke "com.qalab.coupon.dto.CouponDTO.getDiscountAmount()" because "coupon" is null
at com.qalab.order.service.OrderAmountCalculator.calc(OrderAmountCalculator.java:88)
注意这条 NPE 是 Java 14+ 的 helpful message,JDK 8 只会输出 java.lang.NullPointerException 加行号。如果日志里只有后者,必须靠 traceId 关联上下游,不能靠堆栈定位。
grep -n '9f2c41ab7e' /var/log/qalab/order-service.log
# Grafana Loki(logcli)
logcli query '{app="order-service"} |= "9f2c41ab7e"' --since=2h --limit=200
5. 时间与数据对齐¶
- 客户端、网关、容器、DB 四个时钟都要对:
date -u与date差 8 小时是常态,报告里统一写+08:00。 - 客户端时间不可信,用服务端接收时间做锚点。
- 数据类缺陷必须附 DB 查询,而不是截图:
SELECT id, user_id, status, lock_order_id, used_time, expire_time, version
FROM coupon WHERE id = 'C20260801001';
八、缺陷密度:口径先定,数字才有意义¶
口径必须写进模板,否则跨模块不可比:
| 口径项 | 规则 |
|---|---|
| 统计周期 | 按版本发布日切分,不按自然月(否则跨版本混算) |
| 去重 | duplicate 只算主单;父子任务只算叶子;同根因多入口算 1 条 |
| 级别 | 用结案时的最终级别,不在提单时冻结 |
| 排除 | 环境问题、需求变更废弃、测试数据错误、重复单不计入 |
| 分母 | 只统计手写业务代码,排除生成代码、单测、历史遗留目录 |
分母用 cloc 取,不要凭感觉:
cloc --json --exclude-dir=target,node_modules,generated,test ./src > cloc.json
jq '[.sum.code] | .[0]' cloc.json # 手写代码行数,除以 1000 得 KLOC
示例:订单模块 12.4 KLOC,本版本 P0 1 条、P1 3 条、P2 9 条、P3 11 条,加权缺陷 5+9+9+3.3=26.3,密度 2.12 /KLOC。同团队上季度基线 1.8,上升 18%,需要下钻到具体模块,而不是直接说「质量下降」。跨公司数字没有可比性,因为分母口径几乎一定不同。
九、开发不认账时,用证据链说话¶
真实场景:开发回复「我本地好的,你环境问题」。三步走。
第一步,先自证环境。 贴构建号与部署时间,证明测的就是他的代码:
kubectl -n test get deploy order-service -o jsonpath='{.spec.template.spec.containers[0].image}'
# harbor.qalab.cn/qalab/order-service:20260908-1a2b3c4
kubectl -n test get pod -l app=order-service -o jsonpath='{.items[0].status.containerStatuses[0].imageID}'
第二步,给数据不一致的硬证据。 不是「页面显示不对」,而是库里两个字段互相矛盾:
SELECT id, status, lock_order_id, used_time FROM coupon WHERE id='C20260801001';
-- status=USED, lock_order_id=NULL, used_time=NULL
status=USED 但 used_time 为空,只有一种解释:状态更新与时间写入不在同一事务里,与「本地好的」无关。
第三步,三段式回复,不评价人只描述事实。
事实:在镜像 20260908-1a2b3c4 上必现 5/5,curl 可重放(附命令),
服务端 traceId=9f2c41ab7e 在 OrderAmountCalculator.java:88 抛 NPE。
影响:所有订单金额恰好等于券门槛的订单都不抵扣,资金类,今日已有 3 笔客诉。
请求:请确认 7ac1f02 是否包含该场景的单测;若已包含,我这边提供复现环境供联调。
升级路径也要提前约定:技术负责人 → 质量门禁(P0/P1 未关闭则阻塞发布)→ 迭代复盘。不要用「你不修」这种表达,用「该缺陷满足 P1 准出阻塞条件,需要你确认排期或走延期评审」。
十、检查清单¶
- 标题含模块、触发条件、现象、量化影响
- 构建号/镜像 tag 与验证环境一致
- 账号、数据 ID 及关键字段齐全,可独立构造
- 步骤 ≤5 步,标注必现/偶现及次数
- 实际与期望分字段写,期望引用需求条目
- 证据含请求 ID 与可重放的 curl
- 服务端日志带 traceId 与时间窗
- 影响面写明用户范围、是否资金/数据、有无绕过
- 级别按「资金 → 范围 → 绕过」顺序判定
- 关闭前执行相邻分支回归,不只验原步骤
- 非正常关闭有对应字段与审批人
- 周度跑一次「待验证超 3 天」JQL