跳转至

缺陷生命周期与高质量缺陷报告

一、一条被退回 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(延期缺陷清单),版本发布评审逐条过
不修复 产品决策接受现状 决策人、替代方案、用户影响 在发布说明中登记,季度复盘重评

三个可执行细节:

  1. 查重不能靠肉眼。 建单前跑 summary ~ "关键词",或按 traceId 搜:project = QALAB AND text ~ "9f2c41ab7e"。
  2. 「无法复现」不是垃圾桶。 关闭必须写清「在哪些环境、试了几次、日志时间窗」。季度复现失败率 = 无法复现单 / 总缺陷,超过 15% 说明日志、环境或数据构造能力有缺口,不是测试手气差。
  3. 延期必须落在版本上。 labels = "deferred" AND "目标版本" is EMPTY 应恒为 0 条。

四、差报告与好报告的逐项对比

差报告(原文照抄):

标题:订单页有bug
描述:下单的时候优惠券有时候不抵扣,点了没反应,麻烦看下。测试环境。

好报告:

标题:【订单】优惠券满 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 文案、样式、低频易用性问题 个别 有 排期 不阻塞

三个判定示例:

  1. 支付回调成功但订单仍为待支付 → P0。资金已扣、状态未变,用户重复支付概率高,无绕过。
  2. 100.00 订单券不抵扣,用户可手动取消重下 → P1。资金类但存在用户可自助绕过,影响面是「等门槛订单」这一特定集合。
  3. 券列表第 2 页重复展示 1 条 → P2。数据展示错误、不影响领取与核销,刷新可绕过。
  4. 导出报表接口 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'
{"code":0,"data":{"orderAmount":"100.00","payAmount":"100.00","couponDiscount":"0.00"}}

报告里贴这条命令,开发可以直接复制执行,省掉「你是怎么调的」这一轮。

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';

八、缺陷密度:口径先定,数字才有意义

加权缺陷密度 = Σ(缺陷数 × 级别权重) / KLOC(或 / 功能点数)
权重建议:P0=5,P1=3,P2=1,P3=0.3

口径必须写进模板,否则跨模块不可比:

口径项 规则
统计周期 按版本发布日切分,不按自然月(否则跨版本混算)
去重 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

相关阅读