1、业务背景
财务这边大部分系统都是供应商项目,由于供应商的研发人员没有飞书项目的权限,涉及到供应商系统需求 财务这边都是通过多维表格进行bug的生命周期管理如图:
但多维表格没有跟飞书项目直接关联,测试组做bug统计的时候无法计入供应商bug,对测试人员的bug数量造成一些影响。
解决方案:
读取表格数据,调用飞书项目接口,将bug导入到对应飞书项目需求里
2、结果展示
1、点击桌面"供应商bug导入"应用图标
2、输入文件路径和飞书项目ID
3、结果展示
导入结果:
原始数据:
备注:代码逻辑里对bug优先级做了映射,高级-- C类、中级-- B类、低级和建议-- C类
3、实现方式
官方文档: 飞书项目开发者手册
1、创建插件
插件入口
点击个人头像,从「开发者后台」进入插件开发者后台;
创建完成后,对插件进行数据权限管理和发布
插件凭证
插件凭证是插件开发阶段、运行阶段用于身份鉴权的唯一凭证。
2、名词解释
|----------|----------------|-----------------------------------|
| 中文名词 | API 名词 | 描述 |
| 空间域名 | simple_name | |
| 字段ID | field_key | |
| 工作项实例 | instance | 工作项实例是一个具体的事例,例如,一个已经创建的需求、缺陷、项目等 |
3、接口调用
通过基础名词解释可得,我们创建bug就是新增一个工作实例,找到官方文档中的创建工作项的接口信息
1、创建工作项接口
|--------------|----------|--------------------|----------|-----------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| 请求方式 | POST |||||
| 请求地址 | /open_api/:project_key/work_item/create |||||
| 请求header | 详见 请求header |||||
| 请求参数 | 参数类型 | 参数名 | 是否必填 | 值类型 | 说明 |
| 请求参数 | 路径参数 | project_key | 是 | string | 空间id(project_key),或者空间域名(simple_name) |
| 请求参数 | 请求体参数 | work_item_type_key | 是 | string | 工作项类型,自定义工作项可通过获取空间下工作项类型获取 |
| 请求参数 | 请求体参数 | name | 是 | string | 工作项名称 |
| 请求参数 | 请求体参数 | field_value_pairs | 否 | list<FieldValuePair> FieldValuePair | 创建工作项的具体字段可以从获取创建工作项元数据 接口中获取,字段格式可查看字段与属性解析格式 |
| 请求参数 | 请求体参数 | template_id | 是 | int64 | 模板ID,可以从以下途径获取: 1. 获取创建工作项元数据接口中的template字段的options中获取,选择对应的value 2. 获取工作项下的流程模板列表接口的template_id 3. 获取字段信息接口中的template字段的options中获取,选择对应的value |
| 请求体格式 | bash { "work_item_type_key": "story",//工作项类型 "template_id": 123123,//流程模板id "name":"2333",//工作项名称 "field_value_pairs": [ { "field_key": "description",//字段key,作为请求参数和field_alias二选一必填,选择范围为工作项元数据中获取的字段全集 "field_value": "2333"//字段值,作为请求参数必填;value填写规则取决于字段类型,不同类型的规则可见字段与属性解析格式 } ] }
|||||
| 返回格式 | { "data": 19781, // 工作项id "err": {}, "err_msg": "", "err_code": 0 } |||||
根据接口文档描述,我们需要获得 请求header 信息。需要查看header文档
通过请求header文档,得知我们需要获取访问凭证
1、获取访问凭证接口
调用 获取插件访问凭证 接口,通过插件凭证 Plugin ID 和 Plugin Secret 获取 plugin_access_token (或者virtual_plugin_access_token)
|------------|---------------|--------|--------|----------------------------------------------------------------------------|
| 请求方式 | POST ||||
| 请求地址 | https://{平台域名}/open_api/authen/plugin_token ||||
| 请求头参数 | 参数名 | 类型 | 必填 | 说明 |
| | Content-Type | string | 是 | 固定值:"application/json" |
| 请求体参数 | 参数名 | 类型 | 必填 | 说明 |
| | plugin_id | string | 是 | 插件唯一标识,Plugin ID |
| | plugin_secret | string | 是 | 插件密钥,Plugin Secret |
| | type | int | 否 | 插件访问凭证类型,可选值:0、1。默认为plugin_access_token,值为1时将返回virtual_plugin_access_token |
| cURL示例 | curl --location 'https://{平台域名}/open_api/authen/plugin_token' \ --header 'Content-Type: application/json' \ --data '{ "plugin_id": "MII_63E9D49B8C820014", "plugin_secret": "D01B5F1A191C8620D133CDC371C0C7CB", "type": 0 }' ||||
| 响应体参数 | { "data": { "expire_time": 7200, // token失效时间 "token": "p-49257489-f7d7-4cd6-b34f-98c6b81db375" // 插件访问凭证 plugin_access_token } } ||||
"plugin_id" 和 "plugin_secret" 我们创建插件时已经获得,由接口文档,可以轻松获取到请求header的凭证信息
2、工作项类型获取
根据创建工作项的接口文档 work_item_type_key 需要通过 获取空间下工作项类型 接口获取
为了方便获取到我们需要的参数,只需要用postman获取即可
即: "work_item_type_key": "issue"
3、获取 template_id
同理我们可以调用获取字段信息接口,拿到缺陷的 template_id
即: "template_id": 34673,
4、其余自定义字段获取
可以通过获取工作项详情查询
最终的请求数据
{ "work_item_type_key": "issue", "name": data[i][0], "template_id": 34673, "field_value_pairs": [ { "field_alias": "bug_priority", "field_value": {"label": priority_map.get(data[i][6], {"label": "B类", "value": "53cnaxoz_"}).get("label"), "value": priority_map.get(data[i][6], {"label": "B类", "value": "53cnaxoz_"}).get("value") }, }, {"field_alias": "_field_linked_story", "field_value": linked_story}, {"field_alias": "owner", "field_value": reporter_map.get(data[i][1], "7230980664668045340")}, {"field_alias": "issue_reporter", "field_value": reporter_map.get(data[i][1], "7230980664668045340")}, {"field_alias": "issue_operator", "field_value": ["7413123527076806659"]}, {"field_alias": "description", "field_value": data[i][0]}, ] }
2、状态流转接口
状态流转接口新
|--------------|----------|--------------------|----------|-----------------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------|
| 请求方式 | POST |||||
| 请求地址 | /open_api/:project_key/workflow/:work_item_type_key/:work_item_id/node/state_change |||||
| 请求header | 详见 请求header |||||
| 请求参数 | 参数类型 | 参数名 | 是否必填 | 值类型 | 说明 |
| 请求参数 | 路径参数 | work_item_id | 是 | int64 | 工作项ID |
| 请求参数 | 路径参数 | work_item_type_key | 是 | string | 工作项类型可以,从获取空间下工作项类型接口获取 |
| 请求参数 | 路径参数 | project_key | 是 | string | 空间id(project_key),或者空间域名(simple_name) |
| 请求参数 | 请求体参数 | transition_id | 是 | int64 | 流转到下一状态的id,从获取工作流详情接口查询状态流获取 |
| 请求参数 | 请求体参数 | role_owners | 否 | list<RoleOwner> RoleOwner | 角色及负责人 |
| 请求参数 | 请求体参数 | fields | 否 | list<FieldValuePair> FieldValuePair | 要更新的字段数组(只能更新状态表单) |
| 请求体格式 | { "transition_id":12345//状态id,仅状态流返回 "fields":[ { "field_alias": "sentry_link",//字段对接标识 "field_key": "field_658c22",//字段key "field_type_key": "text",//字段类型 "field_value": "23333"//字段值 } ] } |||||
根据接口文档,我们需要获取header、work_item_id、work_item_type_key、project_key、transition_id等信息
其中header、work_item_type_key、project_key可以参考 "创建工作项" 接口里的获取方式
1、work_item_id的取值
work_item_id 我们可以从创建工作项接口的返回值里取到
2、transition_id 获取
transition_id 可以通过调用 获取工作流详情 接口获取
从接口返回信息可得,bug状态 :
由 OPEN --> RESOLVED 的 transition_id 是 983309
由 RESOLVED --> CLOSED 的 transition_id 是 983314
状态流转的请求json信息
{ "transition_id": 983309, # 状态改成 RESOLVED "fields": [ {"field_alias": "bug_remark", "field_value": "供应商bug"}, {"field_alias": "bug_reason", "field_type_key": "select", "field_key": "field_21fcfb", "field_value": {"label": "自测不仔细产生bug", "value": "b39yvbscm"}} ] }
4、代码示例
python
import pandas as pd
import json
import requests
from tkinter_test import imp_confirm
def get_plugin_token():
"""
获取插件的 token并返回
:return: 插件的token信息
"""
url = 'https://project.feishu.cn/open_api/authen/plugin_token'
payload = json.dumps({
"plugin_id": plugin_id,
"plugin_secret": plugin_secret,
"type": 0
})
headers = {'Content-Type': 'application/json'}
response = requests.request("POST", url, headers=headers, data=payload)
return response.json()['data']['token']
def add_bug_datas(linked_story, file_path):
"""
把bug导入到对应的飞书项目里
:param linked_story: 飞书项目ID
:param file_path: bug文件路径
"""
df = pd.read_excel(file_path)
data = df.iloc[0:, :]
data = data.to_numpy()
url = "https://project.feishu.cn/open_api/hdltech/work_item/create"
for i in range(len(data)):
user_data = {
"work_item_type_key": "issue",
"name": data[i][0],
"template_id": 34673,
"field_value_pairs": [
{
"field_alias": "bug_priority",
"field_value": {"label": priority_map.get(data[i][6], {"label": "B类", "value": "53cnaxoz_"}).get("label"),
"value": priority_map.get(data[i][6], {"label": "B类", "value": "53cnaxoz_"}).get("value")
},
},
{"field_alias": "_field_linked_story", "field_value": linked_story},
{"field_alias": "owner", "field_value": reporter_map.get(
data[i][1], "7230980664668045340")},
{"field_alias": "issue_reporter", "field_value": reporter_map.get(
data[i][1], "7230980664668045340")},
{"field_alias": "issue_operator",
"field_value": ["7413123527076806659"]},
{"field_alias": "description", "field_value": data[i][0]},
]
}
data_to_send = json.dumps(user_data).encode("utf-8")
header = {"content-type": "application/json",
"X-User-Key": user_key,
"X-PLUGIN-TOKEN": get_plugin_token()
}
r = requests.post(url, data=data_to_send, headers=header)
data_list.append(r.json()["data"])
print(r.json())
def close_bug():
"""
更改bug状态
"""
headers = {
"Content-Type": "application/json",
"X-PLUGIN-TOKEN": get_plugin_token(),
"X-USER-KEY": user_key,
}
for bug_id in data_list:
bug_id = bug_id
url = f"https://project.feishu.cn/open_api/hdltech/workflow/issue/{bug_id}/node/state_change"
data = {
"transition_id": 983309, # 状态改成 RESOLVED
"fields": [
{"field_alias": "bug_remark", "field_value": "供应商bug"},
{"field_alias": "bug_reason",
"field_type_key": "select",
"field_key": "field_21fcfb",
"field_value": {"label": "自测不仔细产生bug", "value": "b39yvbscm"}}
]
}
requests.post(url, json=data, headers=headers).json()
data = {
"transition_id": 983314, # 状态改成close
"fields": [
{"field_alias": "bug_remark", "field_value": "供应商bug"},
]
}
result = requests.post(url, json=data, headers=headers).json()
print(result)
file_path, linked_story = imp_confirm()
add_bug_datas(linked_story, file_path)
close_bug()
#print(file_path, linked_story)
4、弹窗信息的实现
Python GUI编程(Tkinter) :https://docs.python.org/zh-cn/3/library/tk.html
python
import tkinter as tk
root = tk.Tk()
root.geometry('400x230+500+260')
root.title('导入bug操作')
page = tk.Frame(root)
page.pack()
file_path = tk.StringVar()
story_id = tk.StringVar()
tk.Label(page).grid(row=0, column=0)
tk.Label(page, text='请输入文件路径').grid(row=1, column=1)
tk.Entry(page, textvariable=file_path).grid(row=1, column=2, pady=10)
tk.Label(page, text='请输入项目ID').grid(row=2, column=1, pady=10)
tk.Entry(page, textvariable=story_id).grid(row=2, column=2, pady=10)
def imp_confirm():
filepath = file_path.get().replace(" ", "")
storyid = story_id.get().replace(" ", "")
if len(filepath) == 0:
messagebox.showwarning("警告", "文件路径不能为空")
elif len(storyid) == 0:
messagebox.showwarning("警告", "项目ID不能为空")
elif not storyid.isdigit():
messagebox.showwarning('警告', '项目ID为整数请确认后输入')
else:
page.quit()
#messagebox.showinfo("提示", "开始导入请稍后")
return filepath, int(storyid)
tk.Button(page, text='确认', command=imp_confirm).grid(row=3, column=1, pady=10)
tk.Button(page, text='取消', command=page.quit).grid(row=3, column=2)
root.mainloop()
5、打包操作
使用 Pyinstaller 进行打包
pyinstaller -w -i im.icns -n 供应商bug导入 opexcel.py