效能度量 专栏文章 质量保障系统的落地实践 (二) 项目管理设计 - 基础信息与缺陷信息设计

亦攸 · 2024年04月29日 · 最后由 亦攸 回复于 2024年05月10日 · 4476 次阅读

前言

大家好,上一篇文章中介绍了整个项目的概况,那么从这篇文章开始,我们聚焦在项目管理板块的设计和实现上,本文涉及到的三方平台为 Jira,通过分析三方平台能够提供的能力以及获取数据的条件来一步步完善系统设计。

成员管理设计

开始前先补充上一节中组织结构、成员管理的表设计:

# 组织结构管理-组织结构信息
class DepartmentInfo(BaseModel):
    name = models.CharField(max_length=50, verbose_name=u"节点名称")

    class Meta:
        db_table = 'Department_Info'
        verbose_name = "组织结构管理-组织结构信息"
        verbose_name_plural = verbose_name

class MemberInfo(BaseModel):
    department_id = models.IntegerField(verbose_name=u"归属组织节点")
    name = models.CharField(max_length=30, verbose_name=u"成员姓名")
    phone = models.CharField(max_length=11, verbose_name=u"手机号")
    role = models.IntegerField(verbose_name=u"成员角色")
    end = models.IntegerField(verbose_name=u"成员技术端(前端/后端)")
    email = models.CharField(max_length=100, verbose_name=u"成员邮箱")
    is_available = models.BooleanField(default=True, verbose_name=u"是否有效")

    class Meta:
        db_table = "Member_Info"
        verbose_name = "成员管理-成员信息"
        verbose_name_plural = verbose_name

项目管理设计

首先,我们抛开具体代码实现逻辑,仅从需求层面考虑项目数据应该包含哪些要素:
1、项目基础信息,比如项目名称,项目归属组织节点,预计发布时间,冒烟次数,TC 数量,测试人日,研发人日,是否延期,延期次数,延期原因等等,这类数据与项目信息是 1 比 1 的关系,所以数据表设计中可保存在同一张表内:

# 项目管理-项目基础信息
class ProjectInfo(BaseModel):
    department_id = models.IntegerField(verbose_name=u"归属组织节点")
    name = models.CharField(max_length=50, verbose_name=u"项目名称")
    smoke_times = models.IntegerField(default=0, verbose_name=u"冒烟次数")
    test_periods = models.IntegerField(default=0, verbose_name=u"测试总人日")
    develop_periods = models.IntegerField(default=0, verbose_name=u"研发总人日")
    tc_count = models.IntegerField(default=0, verbose_name=u"总TC数")
    release_date = models.CharField(null=True, blank=True, max_length=30, verbose_name=u"发布日期")
    is_delayed = models.BooleanField(default=False, verbose_name=u"是否延期")
    delayed_times = models.IntegerField(null=True, blank=True, verbose_name=u"延期次数")
    delayed_periods = models.FloatField(null=True, blank=True, verbose_name=u"延期天数")
    delayed_reason = models.CharField(null=True, blank=True, max_length=30, verbose_name=u"延期原因")
    has_released = models.BooleanField(null=True, blank=True, default=False, verbose_name=u"是否已发布")

    class Meta:
        db_table = "Project_Info"
        verbose_name = "项目管理-项目基础信息"
        verbose_name_plural = verbose_name

2、项目参与人员信息,比如 QA 人员,前端研发,后端研发,产品经理,设计师等,这类数据与项目信息是 1 比 n 的关系,所以设计时考虑独立一张表:

# 成员管理-项目人员管理
class ProjectMemberInfo(BaseModel):
    department_id = models.IntegerField(verbose_name=u"归属组织节点")
    project_id = models.IntegerField(verbose_name=u"项目id")
    member_id = models.IntegerField(verbose_name=u"成员id")
    member_phone = models.CharField(max_length=11, verbose_name=u"成员手机号")
    member_name = models.CharField(max_length=20, verbose_name=u"成员姓名")
    member_email = models.CharField(max_length=100, verbose_name=u"用户邮箱")
    member_role = models.IntegerField(verbose_name=u"成员角色")
    member_end = models.IntegerField(verbose_name=u"成员端")
    is_available = models.BooleanField(default=True, verbose_name=u"是否有效")

    class Meta:
        db_table = "Project_Member_Info"
        verbose_name = "项目人员管理"
        verbose_name_plural = verbose_name

3、项目内测缺陷,上一篇文章中提到,Jira 系统依据 jquery 语句进行数据查询,一般来说一个项目对应一个 jquery,即使真的出现一个项目多个 jquery 的情况,也可以通过 jquery 语句的合并使其保持与项目产生 1 比 1 的关系,所以此处可以考虑将 jquery 语句与项目进行绑定,即放入 ProjectInfo 类中。不过在上篇文章中也提到了,由于项目内测缺陷与线上缺陷或其他业务定义的缺陷对于 Jira 系统来说是一样的处理逻辑,差别只在于 jquery 语句的构造。那么也可以考虑独立项目与缺陷 jquery 的对应关系:

# 缺陷管理-缺陷配置
class IssueConfigInfo(BaseModel):
    department_id = models.IntegerField(verbose_name=u"归属组织节点ID")
    project_id = models.IntegerField(null=True, blank=True, verbose_name=u"项目ID,可为空")
    name = models.CharField(null=True, blank=True, max_length=255, verbose_name=u"配置名称")
    query = models.CharField(max_length=255, verbose_name=u"查询条件")
    built_in = models.BooleanField(default=False, verbose_name=u"是否内置")
    is_available = models.BooleanField(default=True, verbose_name=u"是否启用")
    biz_type = models.IntegerField(verbose_name=u"缺陷类型配置标识")

    class Meta:
        db_table = "Issue_Config_Info"
        verbose_name = "缺陷管理-缺陷配置"
        verbose_name_plural = verbose_name

为什么采用这种方案设计,是因为独立这张表后,线上缺陷及其他业务类型缺陷的 jquery 均可以放在这张表中,通过 biz_type 与 built_in 字段进行区分,那么通过这张表的设计可以将缺陷场景基本囊括,后续拓展缺陷基础信息、缺陷生命周期等数据表时,只需通过 IssueConfigInfo 的主字段 config_id 进行关联即可:

# 缺陷管理-缺陷信息
class IssueInfo(BaseModel):
    department_id = models.IntegerField(verbose_name=u"归属组织节点ID")
    config_id = models.IntegerField(verbose_name=u"缺陷配置ID")
    issue_key = models.CharField(max_length=50, verbose_name=u"缺陷key")
    issue_type = models.CharField(max_length=10, verbose_name=u"缺陷类型")
    issue_url = models.CharField(max_length=150, verbose_name="u缺陷链接")
    issue_priority = models.CharField(max_length=10, verbose_name=u"缺陷优先级")
    issue_status = models.CharField(max_length=15, verbose_name=u"缺陷当前状态")
    issue_reason = models.CharField(max_length=50, verbose_name=u"缺陷原因")
    issue_reopen = models.IntegerField(verbose_name=u"缺陷重开次数")
    issue_valid = models.CharField(max_length=50, verbose_name=u"缺陷有效性")
    issue_assignee = models.CharField(max_length=30, verbose_name=u"指派人姓名")
    issue_assignee_email = models.CharField(max_length=50, verbose_name=u"指派人邮箱")
    issue_reporter = models.CharField(max_length=30, verbose_name=u"报告人姓名")
    issue_reporter_email = models.CharField(max_length=50, verbose_name=u"报告人邮箱")
    issue_create_time = models.CharField(max_length=30, verbose_name=u"缺陷创建时间")
    biz_type = models.IntegerField(verbose_name=u"缺陷类型配置标识")

    class Meta:
        db_table = "Issue_Info"
        verbose_name = "缺陷管理-缺陷信息"
        verbose_name_plural = verbose_name

# 缺陷管理-缺陷生命周期信息
class IssueLifePeriodInfo(BaseModel):
    department_id = models.IntegerField(verbose_name=u"归属组织节点ID")
    config_id = models.IntegerField(verbose_name=u"缺陷配置ID")
    issue_key = models.CharField(max_length=50, verbose_name=u"缺陷key")
    issue_url = models.CharField(max_length=150, verbose_name=u"缺陷链接")
    issue_create_time = models.CharField(max_length=30, verbose_name=u"缺陷创建时间")
    seconds = models.IntegerField(verbose_name=u"生命周期(s)")
    member_id = models.IntegerField(verbose_name=u"归属人ID")
    member_name = models.CharField(max_length=50, verbose_name=u"归属人姓名")
    member_phone = models.CharField(max_length=11, verbose_name=u"手机号")
    member_role = models.IntegerField(verbose_name=u"归属人角色")
    member_end = models.IntegerField(verbose_name=u"归属人端")
    biz_type = models.IntegerField(verbose_name=u"缺陷类型配置标识")

    class Meta:
        db_table = "Issue_Life_Period_Info"
        verbose_name = "缺陷管理-缺陷生命周期"
        verbose_name_plural = verbose_name

以上的设计方案,从目前的实践结果来看,能够简化与缺陷相关的很多问题,所以在这里做一个推荐。
回过头我们再来看 Jira 系统能够提供的数据结构是怎样的,以下为 Jira 返回的缺陷数据 (精简过):

{
    'expand': 'operations,versionedRepresentations,editmeta,changelog,renderedFields',
    'id': '481371',
    'self': 'https://jira.demo-inc.com/rest/api/2/issue/481371',
    'key': 'DEMOKEY-16022',
    'fields': {
        'resolution': {
            'self': 'https://jira.demo-inc.com/rest/api/2/resolution/10103',
            'id': '10103',
            'description': '关闭需求、任务或者缺陷',
            'name': 'CLOSED'
        },
        'assignee': {
            'self': 'https://jira.demo-inc.com/rest/api/2/user?username=zhangsan',
            'name': 'zhangsan',
            'key': 'zhangsan',
            'emailAddress': 'zhangsan@demo.com',
            'avatarUrls': {
                '48x48': 'https://jira.demo-inc.com/secure/useravatar?ownerId=zhangsan&avatarId=15414',
                '24x24': 'https://jira.demo-inc.com/secure/useravatar?size=small&ownerId=zhangsan&avatarId=15414',
                '16x16': 'https://jira.demo-inc.com/secure/useravatar?size=xsmall&ownerId=zhangsan&avatarId=15414',
                '32x32': 'https://jira.demo-inc.com/secure/useravatar?size=medium&ownerId=zhangsan&avatarId=15414'
            },
            'displayName': '张三',
            'active': True,
            'timeZone': 'Asia/Shanghai'
        },
        'issuetype': {
            'self': 'https://jira.demo-inc.com/rest/api/2/issuetype/11200',
            'id': '11200',
            'description': 'Bug,缺陷,影响使用的故障等',
            'iconUrl': 'https://jira.demo-inc.com/secure/viewavatar?size=xsmall&avatarId=10303&avatarType=issuetype',
            'name': 'BUG',
            'subtask': False,
            'avatarId': 10303
        },
        'status': {
            'self': 'https://jira.demo-inc.com/rest/api/2/status/10126',
            'description': '当某问题被验证已经被修复后,测试同学置该Bug为close状态。',
            'iconUrl': 'https://jira.demo-inc.com/images/icons/statuses/generic.png',
            'name': 'Close',
            'id': '10126',
            'statusCategory': {
                'self': 'https://jira.demo-inc.com/rest/api/2/statuscategory/3',
                'id': 3,
                'key': 'done',
                'colorName': 'green',
                'name': '完成'
            }
        },
        'creator': {
            'self': 'https://jira.demo-inc.com/rest/api/2/user?username=zhangsan',
            'name': 'zhangsan',
            'key': 'zhangsan',
            'emailAddress': 'zhangsan@demo.com',
            'avatarUrls': {
                '48x48': 'https://jira.demo-inc.com/secure/useravatar?ownerId=zhangsan&avatarId=15414',
                '24x24': 'https://jira.demo-inc.com/secure/useravatar?size=small&ownerId=zhangsan&avatarId=15414',
                '16x16': 'https://jira.demo-inc.com/secure/useravatar?size=xsmall&ownerId=zhangsan&avatarId=15414',
                '32x32': 'https://jira.demo-inc.com/secure/useravatar?size=medium&ownerId=zhangsan&avatarId=15414'
            },
            'displayName': '张三',
            'active': True,
            'timeZone': 'Asia/Shanghai'
        },
        'reporter': {
            'self': 'https://jira.demo-inc.com/rest/api/2/user?username=zhangsan',
            'name': 'zhangsan',
            'key': 'zhangsan',
            'emailAddress': 'zhangsan@demo.com',
            'avatarUrls': {
                '48x48': 'https://jira.demo-inc.com/secure/useravatar?ownerId=zhangsan&avatarId=15414',
                '24x24': 'https://jira.demo-inc.com/secure/useravatar?size=small&ownerId=zhangsan&avatarId=15414',
                '16x16': 'https://jira.demo-inc.com/secure/useravatar?size=xsmall&ownerId=zhangsan&avatarId=15414',
                '32x32': 'https://jira.demo-inc.com/secure/useravatar?size=medium&ownerId=zhangsan&avatarId=15414'
            },
            'displayName': '张三',
            'active': True,
            'timeZone': 'Asia/Shanghai'
        },
        'summary': '【后端】demo缺陷描述',
        'priority': {
            'self': 'https://jira.demo-inc.com/rest/api/2/priority/10001',
            'iconUrl': 'https://jira.demo-inc.com/images/icons/priorities/critical.svg',
            'name': 'P1',
            'id': '10001'
        },
        'created': '2024-04-15T11:41:04.000+0800',
    },
    'changelog': {
        'startAt': 0,
        'maxResults': 4,
        'total': 4,
        'histories': [{
            'id': '2775730',
            'created': '2024-04-15T14:07:07.000+0800',
            'items': [{
                'field': 'priority',
                'fieldtype': 'jira',
                'from': '10002',
                'fromString': 'P2',
                'to': '10001',
                'toString': 'P1'
            }]
        }, {
            'id': '2775984',
            'author': {
                'self': 'https://jira.demo-inc.com/rest/api/2/user?username=lisi',
                'name': 'lisi',
                'key': 'lisi',
                'emailAddress': 'lisi@demo.com',
                'avatarUrls': {
                    '48x48': 'https://jira.demo-inc.com/secure/useravatar?avatarId=10122',
                    '24x24': 'https://jira.demo-inc.com/secure/useravatar?size=small&avatarId=10122',
                    '16x16': 'https://jira.demo-inc.com/secure/useravatar?size=xsmall&avatarId=10122',
                    '32x32': 'https://jira.demo-inc.com/secure/useravatar?size=medium&avatarId=10122'
                },
                'displayName': 'lisi',
                'active': True,
                'timeZone': 'Asia/Shanghai'
            },
            'created': '2024-04-15T16:08:08.000+0800',
            'items': [{
                'field': 'FixTime',
                'fieldtype': 'custom',
                'from': None,
                'fromString': None,
                'to': '2024-04-15T16:08:08+0800',
                'toString': '2024-04-15 16:08'
            }, {
                'field': 'FixTime',
                'fieldtype': 'custom',
                'from': '2024-04-15T16:08:08+0800',
                'fromString': '2024-04-15 16:08',
                'to': '2024-04-15T16:08:08+0800',
                'toString': '2024-04-15 16:08'
            }, {
                'field': 'status',
                'fieldtype': 'jira',
                'from': '10113',
                'fromString': 'new',
                'to': '10123',
                'toString': 'Fixed'
            }]
        }, {
            'id': '2775985',
            'author': {
                'self': 'https://jira.demo-inc.com/rest/api/2/user?username=lisi',
                'name': 'lisi',
                'key': 'lisi',
                'emailAddress': 'lisi@demo.com',
                'avatarUrls': {
                    '48x48': 'https://jira.demo-inc.com/secure/useravatar?avatarId=10122',
                    '24x24': 'https://jira.demo-inc.com/secure/useravatar?size=small&avatarId=10122',
                    '16x16': 'https://jira.demo-inc.com/secure/useravatar?size=xsmall&avatarId=10122',
                    '32x32': 'https://jira.demo-inc.com/secure/useravatar?size=medium&avatarId=10122'
                },
                'displayName': 'lisi',
                'active': True,
                'timeZone': 'Asia/Shanghai'
            },
            'created': '2024-04-15T16:08:15.000+0800',
            'items': [{
                'field': 'assignee',
                'fieldtype': 'jira',
                'from': 'lisi',
                'fromString': 'lisi',
                'to': 'zhangsan',
                'toString': '张三'
            }]
        }, {
            'id': '2775989',
            'author': {
                'self': 'https://jira.demo-inc.com/rest/api/2/user?username=zhangsan',
                'name': 'zhangsan',
                'key': 'zhangsan',
                'emailAddress': 'zhangsan@demo.com',
                'avatarUrls': {
                    '48x48': 'https://jira.demo-inc.com/secure/useravatar?ownerId=zhangsan&avatarId=15414',
                    '24x24': 'https://jira.demo-inc.com/secure/useravatar?size=small&ownerId=zhangsan&avatarId=15414',
                    '16x16': 'https://jira.demo-inc.com/secure/useravatar?size=xsmall&ownerId=zhangsan&avatarId=15414',
                    '32x32': 'https://jira.demo-inc.com/secure/useravatar?size=medium&ownerId=zhangsan&avatarId=15414'
                },
                'displayName': '张三',
                'active': True,
                'timeZone': 'Asia/Shanghai'
            },
            'created': '2024-04-15T16:08:41.000+0800',
            'items': [{
                'field': 'resolution',
                'fieldtype': 'jira',
                'from': None,
                'fromString': None,
                'to': '10103',
                'toString': 'CLOSED'
            }, {
                'field': 'status',
                'fieldtype': 'jira',
                'from': '10123',
                'fromString': 'Fixed',
                'to': '10126',
                'toString': 'Close'
            }]
        }]
    }
}

以上的缺陷数据可分为 fields、changlog 两大部分,fields 中可以提取出缺陷的基础信息,可将这些信息存入 IssueInfo 表内;changlog 中可以提取出缺陷的变更记录,如缺陷状态变更,缺陷指派人变更,通过这些数据的计算可以得出缺陷指派人跟进该缺陷的时间周期,存入 IssueLifePeriodInfo 内。

缺陷生命周期

通过 fields 提取字段存入 IssueInfo 表内相对比较简单,只需按层级提取参数落表即可:


接下来我们主要谈谈缺陷跟进生命周期该如何计算与实现,Jira 的操作界面如下:

一般来说缺陷管理系统中,缺陷状态变更与缺陷指派人变更不会同步操作,我们举一个具体的例子,内测缺陷由测试 A 建立,指派给研发 B,研发修复完成后,需要将缺陷状态置为 Fixed,而此时缺陷的指派人依旧为研发 B,随后研发 B 将 Fixed 的缺陷指派回测试 A,此时缺陷状态为 Fixed。测试 A 验证通过后,关闭缺陷,这个时候缺陷的指派人为测试 A:

每一次指派人变更或状态变更时,系统都会记录一个时间节点,利用两次时间节点的差值,可以得出各个操作步骤的时间差值,累计可得出当前指派人处理缺陷的时间:

然后我们将业务需求转换为代码逻辑,当指派人不变的时候,处理周期需要累加,当指派人发生变更的时候,需要新建一份初始数据进行统计:

# 缺陷管理-统计缺陷生命周期
def fetchIssueLifePeriodFunc(department_id, issue_create_time, issue_assignee, change_log_list):
    """
        传参:
            department_id: 组织结构id
            issue_create_time: 缺陷创建时间
            issue_assignee: 缺陷初始指派人,,从fetchQueryIssueMain获取
            change_log_list: 缺陷changelog数据列表,从fetchQueryIssueMain获取
        返回:
            member_period_map: 缺陷指派人涉及缺陷生命周期相关信息
    """
    # 指针信息记录缺陷指派人、状态
    assignee_pointer = issue_assignee
    time_pointer = issue_create_time

    # 定义汇总数据
    member_period_map = defaultdict(lambda: {
        "seconds": 0,
        "member_phone": None,
        "member_role": None,
        "member_end": None,
        "member_name": None,
        "member_id": None
    })

    for change_log_info in change_log_list:
        # 提取时间节点
        history_time_point = change_log_info["time_point"]
        # 计算耗时
        seconds = secondsDelta(time_pointer, history_time_point)

        # 略过异常数据
        if not assignee_pointer:
            continue

        # 若当前指派人不在数据集合内,才进行数据查询,否则直接累加数据
        if assignee_pointer not in member_period_map:
            author_info = fetchEmailMemberOrNotify(department_id, assignee_pointer)
            if author_info:
                assignee_role = author_info.role
                assignee_end = author_info.end
                assignee_name = author_info.name
                assignee_id = author_info.id
                assignee_phone = author_info.phone
            else:
                assignee_role = MemberRoleEnum.UNKNOWN.value
                assignee_end = MemberEndEnum.UNKNOWN.value
                assignee_name = assignee_pointer
                assignee_id = -1
                assignee_phone = ""

            # 便于后续数据统计,冗余一些字段
            member_period_map[assignee_pointer]["seconds"] += seconds
            member_period_map[assignee_pointer]["member_phone"] = assignee_phone
            member_period_map[assignee_pointer]["member_role"] = assignee_role
            member_period_map[assignee_pointer]["member_end"] = assignee_end
            member_period_map[assignee_pointer]["member_name"] = assignee_name
            member_period_map[assignee_pointer]["member_id"] = assignee_id
        else:
            member_period_map[assignee_pointer]["seconds"] += seconds

        # 处理指针更新
        tag = change_log_info["tag"]
        if tag == "assignee":
            assignee_pointer = change_log_info["to"]
        time_pointer = history_time_point

    return member_period_map

以上是缺陷流转处于理想状态的情况下,可以进行有效统计,并且可以分出不同用户角色进行统计,从而得出 QA 处理周期、前端处理周期、后端处理周期:



但是这些数据能够统计出来的前提是缺陷需要依照既定的生命周期进行运转,而实际项目过程中往往不会严格按照上面生命周期流转的步骤进行,所以此处需要有对应的推送提醒,规范缺陷行为。

缺陷提醒

例如当研发修复或 Rejected 缺陷后,不进行指派改动,那么缺陷指派人一直是研发,那么测试人员进行缺陷验证关闭的处理周期无法统计到对应的测试头上,所以这种情况下需要提示研发将 Fixed 的缺陷指派给测试:

# 缺陷管理-FixedOrRejected状态缺陷指派人信息变更提醒(消息推送给研发)
def issueFixedOrRejectedAssigneeNotifyFunc(department_id, issue_status, issue_list, member_notify_map, webhook):
    issue_assignee_phones = []
    msg = f"以下处于{issue_status}状态的缺陷,请及时完成指派人更新.\n"
    for issue_info in issue_list:
        issue_status = issue_info["issue_status"]
        if issue_status not in ("FIXED", "REJECTED"):
            continue

        issue_url = issue_info["issue_url"]
        issue_assignee_email = issue_info["issue_assignee_email"]
        if issue_assignee_email not in member_notify_map:
            continue

        member_notify_phone = member_notify_map[issue_assignee_email]

        issue_reporter_email = issue_info["issue_reporter_email"]
        # 当前缺陷指派人并非缺陷创建人时进行指派
        if issue_assignee_email != issue_reporter_email:
            reporter_info = fetchEmailMemberOrNotify(department_id, issue_reporter_email)
            if reporter_info:
                reporter_name = reporter_info.name
                msg += f"{issue_url} 指派至{reporter_name}. @{member_notify_phone}\n"
                issue_assignee_phones.append(member_notify_phone)

    if issue_assignee_phones:
        dingTalkNotify(webhook, msg, issue_assignee_phones)

同样的当缺陷处于 Reopen 状态的时候,也需要提醒测试将缺陷指派回研发:

# 缺陷管理-Reopen状态缺陷指派人变更提醒(消息推送给QA)
def issueReopenAssigneeNotifyFunc(department_id, issue_list, issue_assignee_map, member_notify_map, webhook):
    issue_reporter_phones = []
    msg = f"以下处于REOPEN状态的缺陷,请及时完成指派人更新.\n"
    for issue_info in issue_list:
        issue_status = issue_info["issue_status"]
        if issue_status != "REOPEN":
            continue

        issue_key = issue_info["issue_key"]
        issue_url = issue_info["issue_url"]
        issue_reporter_email = issue_info["issue_reporter_email"]
        issue_current_assignee_email = issue_info["issue_assignee_email"]
        issue_assignee_list = issue_assignee_map[issue_key]

        if issue_reporter_email not in member_notify_map:
            continue
        issue_reporter_phone = member_notify_map[issue_reporter_email]

        # 缺陷当前指派人为研发的情况,则不进行提醒;否则通过指派列表找出最近的指派研发
        assignee_info = fetchEmailMemberOrNotify(department_id, issue_current_assignee_email)
        if assignee_info:
            assignee_role = assignee_info.role
            if assignee_role == MemberRoleEnum.DEVELOPER.value:
                continue

        # 将缺陷指派人信息倒序,找出最后一位研发指派人
        issue_assignee_list = issue_assignee_list[::-1]
        assignee_name = None
        for issue_assignee_email in issue_assignee_list:
            assignee_info = fetchEmailMemberOrNotify(department_id, issue_assignee_email)
            if assignee_info:
                assignee_role = assignee_info.role
                if assignee_role == MemberRoleEnum.DEVELOPER.value:
                    assignee_name = assignee_info.name
                    break

        if assignee_name:
            msg += f"{issue_url} 指派至{assignee_name}.\n"
        else:
            msg = f"{issue_url} 指派人列表:{issue_assignee_list},均未能匹配到有效研发信息,请联系管理员."

        issue_reporter_phones.append(issue_reporter_phone)

    if issue_reporter_phones:
        dingTalkNotify(webhook, msg, issue_reporter_phones)

结合了推送提醒工具后,缺陷的生命周期统计才能更贴近实际情况,这样才能为后续的调整动作提供数据支撑,例如研发 Fixed 缺陷周期过长,期望有所降低,那么可以通过缺陷跟进提醒工具的配合,提醒研发及时跟进处于 New 状态的缺陷。同理若测试完成 Fixed 缺陷验收的时间过长,也可以使用类似的提醒机制,总而言之,需要有数据沉淀,才有后续的提高策略,系统设计的第一步是沉淀数据。

缺陷信息与账户体系改造 (补充)

通过 Jira 系统接口返回值的解析,可以拿到缺陷的 reporter、assignee 信息,一般来说这两个字段均是邮箱类型,如 zhangsan@demo.com、lisi@demo.com,那么只需要与用户体系的 MemberInfo 内的邮箱一一对应,就可以打通缺陷与用户体系的关联关系。当然这是理想状态,而实际情况往往不如人意。
介绍不如意的问题之前,我们先来看一下 Jira 系统的账户体系,下图是一张 Jira 账户的详情:

我们可以看到,JIra 的账户体系内有几个元素:用户名全名邮箱,反馈到 Jira 的响应值中,这些元素各有用处。如缺陷的 reporter 及 assignee 信息,取的是账户体系的邮箱信息;而在 changlog 中则用到了用户名全名

那我们举个实际例子:
首先看缺陷的 report、assignee:

fields:{
      'assignee': {
            'self': 'https://jira.demo-inc.com/rest/api/2/user?username=zhangsan',
            'name': 'zhangsan',
            'key': 'zhangsan',
            'emailAddress': 'zhangsan@demo.com',
            'avatarUrls': {
                '48x48': 'https://jira.demo-inc.com/secure/useravatar?ownerId=zhangsan&avatarId=15414',
                '24x24': 'https://jira.demo-inc.com/secure/useravatar?size=small&ownerId=zhangsan&avatarId=15414',
                '16x16': 'https://jira.demo-inc.com/secure/useravatar?size=xsmall&ownerId=zhangsan&avatarId=15414',
                '32x32': 'https://jira.demo-inc.com/secure/useravatar?size=medium&ownerId=zhangsan&avatarId=15414'
            },
            'displayName': '张三',
            'active': True,
            'timeZone': 'Asia/Shanghai'
        },
      'reporter': {
            'self': 'https://jira.demo-inc.com/rest/api/2/user?username=zhangsan',
            'name': 'zhangsan',
            'key': 'zhangsan',
            'emailAddress': 'zhangsan@demo.com',
            'avatarUrls': {
                '48x48': 'https://jira.demo-inc.com/secure/useravatar?ownerId=zhangsan&avatarId=15414',
                '24x24': 'https://jira.demo-inc.com/secure/useravatar?size=small&ownerId=zhangsan&avatarId=15414',
                '16x16': 'https://jira.demo-inc.com/secure/useravatar?size=xsmall&ownerId=zhangsan&avatarId=15414',
                '32x32': 'https://jira.demo-inc.com/secure/useravatar?size=medium&ownerId=zhangsan&avatarId=15414'
            },
            'displayName': '张三',
            'active': True,
            'timeZone': 'Asia/Shanghai'
        },

通过上面的 JSON 结构体可以看出,report 以及 assignee 的 emailAddress 字段,对应是邮箱地址。

接下来我们再来看 changlog:

{
            'id': '2775985',
            'author': {
                'self': 'https://jira.demo-inc.com/rest/api/2/user?username=lisi',
                'name': 'lisi',
                'key': 'lisi',
                'emailAddress': 'lisi@demo.com',
                'avatarUrls': {
                    '48x48': 'https://jira.demo-inc.com/secure/useravatar?avatarId=10122',
                    '24x24': 'https://jira.demo-inc.com/secure/useravatar?size=small&avatarId=10122',
                    '16x16': 'https://jira.demo-inc.com/secure/useravatar?size=xsmall&avatarId=10122',
                    '32x32': 'https://jira.demo-inc.com/secure/useravatar?size=medium&avatarId=10122'
                },
                'displayName': 'lisi',
                'active': True,
                'timeZone': 'Asia/Shanghai'
            },
            'created': '2024-04-15T16:08:15.000+0800',
            'items': [{
                'field': 'assignee',
                'fieldtype': 'jira',
                'from': 'lisi',
                'fromString': 'lisi',
                'to': 'zhangsan',
                'toString': '张三'
            }]
        },

items 中记录了缺陷从 lisi 指派给了张三,从这组数据我们可以看到,fromString 和 toString 是 Jira 账户全名,而 from 和 code 记录的是用户名
那么就带来一个问题:如果我们通过 report、assignee 的 emailAddress 字段与用户体系内的 email 字段进行关联,那么 changelog 中的数据该如何与账户体系进行关联?换个说法,Jira 账户体系下的用户名与 emailAddress 有没有直接关联可以使用?因为我已经实践过了,所以就直接说答案了,没有稳固的关联,因为这几个字段均支持编辑,同时他们的初始值是有一定关系的,比如张三加入公司后,公司默认开通 Jira 账户,此时他的信息为:全名:张三,邮箱:zhangsan@demo.com,用户名:zhangsan,我们可以看到用户名与邮箱之间关系:用户名+统一邮箱后缀=邮箱,也就是说大多数人的情况是这样。可是这些字段又可以编辑,那就意味着有小部分的信息是不符合上述规律的,那我们该如何处理呢?
答案是维护映射关系,用这种映射关系完成 Jira 用户名邮箱 之间的关联关系,甚至包括 Jira 邮箱用户体系邮箱的关系:

总结

本篇文章除了分析如何采集项目需要的缺陷数据外,最核心的部分在于如何将三方平台的信息与质量保障系统进行对接,这是接入三方系统几乎绕不开的问题,每家公司能提供的基础组件能力不同,如何将这些能力结合到质量保障系统中,将价值数据沉淀到系统中,是构建系统时都要去思考的问题。而通过实践,我觉得这种映射关系的方案有比较好的效果。而后续文章介绍项目代码信息采集的时候,也会用到同样的方法,在此先做个铺垫。
以上内容为我所在公司关于项目缺陷管理的实践内容,篇幅原因关于项目代码信息的内容将放在后续的文章中说明,也请各位批评指正,谢谢。

共收到 6 条回复 时间 点赞

有用吧确实有点小用,嗯之前整过这玩意 一哥们看数据能看出来用例批量同一时间点了,被噶了。

框架开源了吗?

disable 回复

被噶了是啥意思呀

Forkey 回复

目前不会开源

千行缺陷率之前我们也统计,现在不统计了,因为考核什么就会得到什么,考核千行缺陷率会变向促进重复代码和冗余代码的行数的增多,这种增多不好统计,但是会让千行缺陷率降低,也就没了意义,反而影响代码质量,我们现在缺陷和需求做绑定算缺陷率,比和代码行数合理,大家认为呢?

如果过度追求这个数字,确实会导致这个问题。这是一个侧面反馈指标,不能作为绝对指标。
其次代码统计的数据很难做得十分精确,这要求了分支的开启与结束时间需要十分准确,而这些信息由 QA 维护会存在一定的偏差
另外来说有些公司有代码编写规范,code review 一定程度上能够限制这种情况。我们更关注的是缺陷的情况,缺陷的相应周期、超时情况等

需要 登录 后方可回复, 如果你还没有账号请点击这里 注册