提交 47541205 编写于 作者: 泰斯特Test's avatar 泰斯特Test

[feat](Cron)恢复钉钉提醒 & 新增企业微信提醒

上级 afde4d5b
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.6 (AutoTest-Platform) (1)" project-jdk-type="Python SDK" />
<component name="PyCharmProfessionalAdvertiser">
<option name="shown" value="true" />
</component>
</project>
\ No newline at end of file
此差异已折叠。
......@@ -95,11 +95,13 @@ def update_case_suite(project_id, case_suite_id):
filtered_data = CaseSuite.filter_field(request.get_json())
for key, value in filtered_data.items():
CaseSuite.update({"_id": ObjectId(case_suite_id)},
{'$set': {key: value}})
{'$set': {key: value}})
update_response = CaseSuite.update({"_id": ObjectId(case_suite_id)},
{'$set': {'lastUpdateTime': datetime.datetime.utcnow()}}, )
{'$set': {'lastUpdateTime': datetime.datetime.utcnow()}},)
if update_response["n"] == 0:
return jsonify({'status': 'failed', 'data': '未找到相应更新数据!'})
return jsonify({'status': 'ok', 'data': '更新成功'})
except BaseException as e:
return jsonify({'status': 'failed', 'data': '更新失败: %s' % e})
......@@ -49,6 +49,9 @@ def add_cron(project_id):
is_ding_ding_notify=filtered_data.get('isDingDingNotify'),
ding_ding_access_token=filtered_data.get('dingdingAccessToken'),
ding_ding_notify_strategy=filtered_data.get('dingdingNotifyStrategy'),
is_enterprise_wechat_notify=filtered_data.get('isEnterpriseWechatNotify'),
enterprise_wechat_access_token=filtered_data.get('enterpriseWechatAccessToken'),
enterprise_wechat_notify_strategy=filtered_data.get('enterpriseWechatNotifyStrategy'),
trigger_type=filtered_data.get('triggerType'),
test_case_id_list=filtered_data.get('testCaseIdList'),
is_execute_forbiddened_case=filtered_data.get('isExecuteForbiddenedCase'),
......@@ -60,6 +63,9 @@ def add_cron(project_id):
is_ding_ding_notify=filtered_data.get('isDingDingNotify'),
ding_ding_access_token=filtered_data.get('dingdingAccessToken'),
ding_ding_notify_strategy=filtered_data.get('dingdingNotifyStrategy'),
is_enterprise_wechat_notify=filtered_data.get('isEnterpriseWechatNotify'),
enterprise_wechat_access_token=filtered_data.get('enterpriseWechatAccessToken'),
enterprise_wechat_notify_strategy=filtered_data.get('enterpriseWechatNotifyStrategy'),
trigger_type=filtered_data.get('triggerType'),
test_case_id_list=filtered_data.get('testCaseIdList'),
is_execute_forbiddened_case=filtered_data.get('isExecuteForbiddenedCase'),
......
......@@ -39,9 +39,11 @@ def update_project(project_id):
Project.update({"_id": ObjectId(project_id)},
{'$set': {key: value}})
update_response = Project.update({"_id": ObjectId(project_id)},
{'$set': {'lastUpdateTime': datetime.datetime.utcnow()}}, )
{'$set': {'lastUpdateTime': datetime.datetime.utcnow()}},)
if update_response["n"] == 0:
return jsonify({'status': 'failed', 'data': '未找到相应更新数据!'})
return jsonify({'status': 'ok', 'data': '更新成功'})
except BaseException as e:
return jsonify({'status': 'failed', 'data': '更新失败: %s' % e})
......@@ -97,7 +97,7 @@ def update_case(project_id, case_suite_id, case_id):
TestingCase.update({"_id": ObjectId(case_id)},
{'$set': {key: value}})
update_response = TestingCase.update({"_id": ObjectId(case_id)},
{'$set': {'lastUpdateTime': datetime.datetime.utcnow()}})
{'$set': {'lastUpdateTime': datetime.datetime.utcnow()}})
if update_response["n"] == 0:
return jsonify({'status': 'failed', 'data': '未找到相应更新数据!'})
return jsonify({'status': 'ok', 'data': '更新成功'})
......@@ -163,8 +163,7 @@ def start_test():
return jsonify({'status': 'failed', 'data': '未在「测试用例」中找到任何「启用的」接口测试用例'})
testing_cases = copy.deepcopy(TestingCase.find({'isDeleted': {'$ne': True}, 'status': True}) # sort吃顺序
.sort(
[('caseSuiteId', pymongo.ASCENDING), ('createAt', pymongo.ASCENDING)])) # 再次初始化 Cursor object
.sort([('caseSuiteId', pymongo.ASCENDING), ('createAt', pymongo.ASCENDING)])) # 再次初始化 Cursor object
if case_id_list:
for testing_case in testing_cases:
if str(testing_case["_id"]) in case_id_list:
......@@ -195,8 +194,7 @@ def start_test():
if not is_single_test:
try:
tester.execute_all_test_and_send_report(TestingCase, TestReport, project_id, executor_nick_name,
execution_mode)
tester.execute_all_test_and_send_report(TestingCase, TestReport, project_id, executor_nick_name, execution_mode)
return jsonify({'status': 'ok', 'data': '测试已启动,稍后请留意自动化测试报告'})
except BaseException as e:
return jsonify({'status': 'failed', 'data': '测试启动失败: %s' % e})
......@@ -253,29 +251,29 @@ def single_test_result(case_id):
test_case_map = {
'caseSuiteId': '用例组_id',
'caseSuiteName': '用例组名称',
'_id': '用例_id',
'name': '用例名称',
'description': '用例描述',
'testCaseType': '用例类型',
'requestProtocol': '请求协议',
'requestMethod': '请求方法',
'domain': '请求域名',
'route': '请求路由',
'headers': '请求头部',
'presendParams': '请求参数',
'checkHttpCode': '状态码校验',
'checkResponseData': '正则校验',
'checkResponseSimilarity': '文本相似度校验',
'checkResponseNumber': '数值校验',
'setGlobalVars': '设置全局变量',
'isClearCookie': '请求前是否清除Cookie',
'createAt': '创建时间/UTC',
'creatorNickName': '创建人',
'lastUpdateTime': '最后更新时间/UTC',
'lastUpdatorNickName': '最后更新人',
}
'caseSuiteId': '用例组_id',
'caseSuiteName': '用例组名称',
'_id': '用例_id',
'name': '用例名称',
'description': '用例描述',
'testCaseType': '用例类型',
'requestProtocol': '请求协议',
'requestMethod': '请求方法',
'domain': '请求域名',
'route': '请求路由',
'headers': '请求头部',
'presendParams': '请求参数',
'checkHttpCode': '状态码校验',
'checkResponseData': '正则校验',
'checkResponseSimilarity': '文本相似度校验',
'checkResponseNumber': '数值校验',
'setGlobalVars': '设置全局变量',
'isClearCookie': '请求前是否清除Cookie',
'createAt': '创建时间/UTC',
'creatorNickName': '创建人',
'lastUpdateTime': '最后更新时间/UTC',
'lastUpdatorNickName': '最后更新人',
}
@app.route("/api/importTestCases", methods=['POST'])
......@@ -315,8 +313,7 @@ def import_test_cases():
missing_attributes = [nip for nip in non_intersection if nip in test_case_map.values()]
return jsonify({'status': 'failed', 'data': '「测试用例」Sheet 表头缺失字段: %s' % missing_attributes}) \
if missing_attributes else jsonify({'status': 'failed', 'data': '「测试用例」Sheet 表头存在多余字段: %s' %
[nip for nip in non_intersection if
nip not in test_case_map.values()]})
[nip for nip in non_intersection if nip not in test_case_map.values()]})
attributes_indexes = [test_case_attributes.index(v) for v in test_case_map.values()]
......@@ -334,8 +331,8 @@ def import_test_cases():
for j, v in enumerate(test_case_info.keys()):
test_case_info[v] = test_case_table.row_values(i + 1)[attributes_indexes[j]]
try:
is_case_exist, pre_import_case_info, is_case_suite_exist \
= get_pre_import_case_info(test_case_info, test_case_mapping=test_case_map, table_row_index=(i + 2))
is_case_exist, pre_import_case_info, is_case_suite_exist\
= get_pre_import_case_info(test_case_info, test_case_mapping=test_case_map, table_row_index=(i+2))
except BaseException as b_e:
return jsonify({'status': 'failed', 'data': '导入数据异常: %s' % b_e})
......@@ -346,7 +343,7 @@ def import_test_cases():
pre_import_case_info = TestingCase.filter_field(pre_import_case_info, use_set_default=False)
result = str(TestingCase.update({"_id": ObjectId(pre_import_case_info.get('_id'))},
{'$set': pre_import_case_info})) + \
' _id: {}'.format(pre_import_case_info.get('_id'))
' _id: {}'.format(pre_import_case_info.get('_id'))
update_count += 1
else:
try:
......@@ -364,13 +361,13 @@ def import_test_cases():
# 在用例列表内导入
else:
inserted_case_suite_id = None
case_suite_name = pre_import_case_info.get('caseSuiteName') \
case_suite_name = pre_import_case_info.get('caseSuiteName')\
if pre_import_case_info.get('caseSuiteName') else ''
if is_case_suite_exist:
if not case_suite_name == CaseSuite.find_one(
{"_id": ObjectId(pre_import_case_info.get('caseSuiteId'))})['name']:
CaseSuite.update({"_id": ObjectId(pre_import_case_info.get('caseSuiteId'))},
{'$set': {'name': case_suite_name}})
{'$set': {'name': case_suite_name}})
else:
pass
else:
......@@ -396,8 +393,8 @@ def import_test_cases():
if is_case_exist:
pre_import_case_info = TestingCase.filter_field(pre_import_case_info, use_set_default=False)
result = str(TestingCase.update({"_id": ObjectId(pre_import_case_info.get('_id'))},
{'$set': pre_import_case_info})) + ' _id: {}' \
.format(pre_import_case_info.get('_id'))
{'$set': pre_import_case_info})) + ' _id: {}'\
.format(pre_import_case_info.get('_id'))
update_count += 1
else:
......@@ -468,7 +465,7 @@ def export_test_cases():
elif isinstance(case.get(key), datetime.datetime):
case_data = str(case.get(key)).replace('.', ':', 1) \
if common.can_convert_to_str(case.get(key)) \
and str(case.get(key)).count('.') < 2 else str(case.get(key))
and str(case.get(key)).count('.') < 2 else str(case.get(key))
else:
case_data = str(case.get(key)) if case.get(key) is not None else ''
export_case.append(case_data)
......@@ -484,7 +481,7 @@ def export_test_cases():
print(e)
return _case_info
export_testing_cases = map(export_case_format, map(add_case_suite_name, TestingCase.find(query)))
export_testing_cases = map(export_case_format, map(add_case_suite_name, TestingCase.find(query)))
bytes_io = BytesIO()
workbook = xlsxwriter.Workbook(bytes_io, {'in_memory': True})
......@@ -501,3 +498,9 @@ def export_test_cases():
bytes_io.seek(0)
return send_file(bytes_io, mimetype='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet')
......@@ -20,19 +20,26 @@ def on_exec_test(data):
except BaseException:
raise TypeError('参数 「testDomain」 不合法')
try:
data['dingdingAccessToken'] = data['dingdingAccessToken'].strip()
except BaseException:
raise TypeError('参数 「dingdingAccessToken」 不合法')
data['isExecuteForbiddenedCase'] = True if common.can_convert_to_str(data.get('isExecuteForbiddenedCase'))\
data['isExecuteForbiddenedCase'] = True if data.get('isExecuteForbiddenedCase') \
and common.can_convert_to_str(data.get('isExecuteForbiddenedCase'))\
and data.get('isExecuteForbiddenedCase').upper() == 'TRUE' else False
data['isDingDingNotify'] = True if common.can_convert_to_str(data.get('isDingDingNotify'))\
and data.get('isDingDingNotify').upper() == 'TRUE' else False
data['isDingDingNotify'] = True if data.get('isDingDingNotify')\
and common.can_convert_to_str(data.get('isDingDingNotify'))\
and data.get('isDingDingNotify').upper() == 'TRUE' else False
data['isEnterpriseWechatNotify'] = True if data.get('isEnterpriseWechatNotify')\
and common.can_convert_to_str(data.get('isEnterpriseWechatNotify')) \
and data.get('isEnterpriseWechatNotify').upper() == 'TRUE' else False
data['dingdingAccessToken'] = data.get('dingdingAccessToken').strip() if data.get('dingdingAccessToken')\
and common.can_convert_to_str(data.get('dingdingAccessToken')) else None
data['enterpriseWechatAccessToken'] = data.get('enterpriseWechatAccessToken').strip()\
if data.get('enterpriseWechatAccessToken') and common.can_convert_to_str(data.get('enterpriseWechatAccessToken')) else None
try:
data['alarmMailList'] = data['alarmMailList'].strip().split(';')
data['alarmMailList'] = data['alarmMailList'].strip().split(';') if data.get('alarmMailList') else []
except BaseException:
raise TypeError('参数 「alarmMailList」 不合法')
......@@ -49,6 +56,8 @@ def on_exec_test(data):
run_date=filtered_data.get('runDate'),
is_ding_ding_notify=filtered_data.get('isDingDingNotify'),
ding_ding_access_token=filtered_data.get('dingdingAccessToken'),
is_enterprise_wechat_notify=filtered_data.get('isEnterpriseWechatNotify'),
enterprise_wechat_access_token=filtered_data.get('enterpriseWechatAccessToken'),
is_web_hook=True)
cron_manager.add_cron(cron)
......@@ -29,6 +29,28 @@ class CronTab(Model):
interval = FloatField()
runDate = DateField()
alarmMailList = ArrayField()
isDingDingNotify = BooleanField(field_name='isDingDingNotify', default=False)
dingdingAccessToken = StringField()
dingdingNotifyStrategy = DictField(field_name='dingdingNotifyStrategy',
default={'success': True, 'fail': True},
expected_structure={
'expectedTypeRange': [dict],
'expectedDict': {
'success': {'expectedTypeRange': [bool]},
'fail': {'expectedTypeRange': [bool]}
}
})
isEnterpriseWechatNotify = BooleanField(field_name='isEnterpriseWechatNotify', default=False)
enterpriseWechatAccessToken = StringField()
enterpriseWechatNotifyStrategy = DictField(field_name='enterpriseWechatNotifyStrategy',
default={'success': True, 'fail': True},
expected_structure={
'expectedTypeRange': [dict],
'expectedDict': {
'success': {'expectedTypeRange': [bool]},
'fail': {'expectedTypeRange': [bool]}
}
})
status = StringField(field_name='status', default='CREATED')
createAt = DateField()
creatorNickName = StringField()
......
......@@ -148,7 +148,7 @@ class tester:
for key, value in test_case['presendParams'].items():
if value is not None:
get_method_params_value = common.resolve_global_var(pre_resolve_var=value,
global_var_dic=self.global_vars)\
global_var_dic=self.global_vars) \
if isinstance(value, str) else value
url += '%s=%s&' % (key, get_method_params_value)
url = url[0:(len(url) - 1)]
......
......@@ -95,6 +95,9 @@ class CronManager:
is_ding_ding_notify = cron_info.get('isDingDingNotify')
ding_ding_access_token = cron_info.get('dingdingAccessToken')
ding_ding_notify_strategy = cron_info.get('dingdingNotifyStrategy')
is_enterprise_wechat_notify = cron_info.get('isEnterpriseWechatNotify')
enterprise_wechat_access_token = cron_info.get('enterpriseWechatAccessToken')
enterprise_wechat_notify_strategy = cron_info.get('enterpriseWechatNotifyStrategy')
try:
if trigger_type == 'interval' and int(interval) > 0:
......@@ -112,6 +115,9 @@ class CronManager:
is_ding_ding_notify=is_ding_ding_notify,
ding_ding_access_token=ding_ding_access_token,
ding_ding_notify_strategy=ding_ding_notify_strategy,
is_enterprise_wechat_notify=is_enterprise_wechat_notify,
enterprise_wechat_access_token=enterprise_wechat_access_token,
enterprise_wechat_notify_strategy=enterprise_wechat_notify_strategy,
trigger_type=trigger_type, # 更新定时器时,此参数并没有真正起到作用, 仅修改展示字段
test_case_id_list=test_case_id_list,
run_date=run_date) # 更新定时器时,此参数并没有起到作用, 仅修改展示字段
......@@ -123,6 +129,9 @@ class CronManager:
is_ding_ding_notify=is_ding_ding_notify,
ding_ding_access_token=ding_ding_access_token,
ding_ding_notify_strategy=ding_ding_notify_strategy,
is_enterprise_wechat_notify=is_enterprise_wechat_notify,
enterprise_wechat_access_token=enterprise_wechat_access_token,
enterprise_wechat_notify_strategy=enterprise_wechat_notify_strategy,
trigger_type=trigger_type, # 更新定时器时,此参数并没有起到作用, 仅修改展示字段
test_case_id_list=test_case_id_list,
seconds=interval) # 更新定时器时,此参数并没有起到作用, 仅修改展示字段
......
......@@ -13,7 +13,8 @@ import requests
class Cron:
def __init__(self, test_case_suite_id_list, test_domain, trigger_type, is_execute_forbiddened_case=False,
test_case_id_list=None, alarm_mail_list=None, is_ding_ding_notify=False, ding_ding_access_token=None,
ding_ding_notify_strategy=None, is_web_hook=False, **trigger_args):
ding_ding_notify_strategy=None, is_enterprise_wechat_notify=False, enterprise_wechat_access_token=None,
enterprise_wechat_notify_strategy=None, is_web_hook=False, **trigger_args):
if test_case_id_list is None:
test_case_id_list = []
......@@ -45,6 +46,14 @@ class Cron:
# print('self.ding_ding_access_token ----> %s' % self.ding_ding_access_token)
# print('self.ding_ding_notify_strategy ----> %s' % self.ding_ding_notify_strategy)
self.enterprise_wechat_access_token = enterprise_wechat_access_token if enterprise_wechat_access_token else None
self.enterprise_wechat_notify_strategy = {'success': True, 'fail': True} \
if is_enterprise_wechat_notify and enterprise_wechat_notify_strategy is None\
else enterprise_wechat_notify_strategy
print('self.enterprise_wechat_access_token ----> %s' % enterprise_wechat_access_token)
print('self.enterprise_wechat_notify_strategy ----> %s' % self.enterprise_wechat_notify_strategy)
self._id = str(common.get_object_id())
self.alarm_mail_list = []
......@@ -138,9 +147,13 @@ class Cron:
res = requests.post(url=hook_url, json=data, headers=headers)
return res
# TODO 当webhook模式触发cron时,可选测试结果发送微信群
def send_wechat_notify(self, hook_url, message):
pass
def send_enterprise_wechat_notify(self, title, content, headers=None):
if headers is None:
headers = {'Content-Type': 'application/json'}
hook_url = "https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key={}".format(self.enterprise_wechat_access_token)
data = {"msgtype": "markdown", "markdown": {"content": "{} \n >{}".format(title, content)}}
res = requests.post(url=hook_url, json=data, headers=headers)
return res
# TODO 发送报告具体链接至邮箱。 如interfaceTestProject/5ccfa182b144f831b04d7ca5/projectReport
def send_report_to_staff(self, project_id, mail_list, mail_title, mail_content):
......@@ -180,6 +193,26 @@ class Cron:
is_send_mail = self.failed_count > 0 and isinstance(self.alarm_mail_list, list)\
and len(self.alarm_mail_list) > 0
is_send_ding_ding = self.ding_ding_access_token if hasattr(self, 'ding_ding_access_token') else False
is_send_enterprise_wechat = self.enterprise_wechat_access_token if hasattr(self, 'enterprise_wechat_access_token')\
else False
if is_send_enterprise_wechat:
enterprise_wechat_title = '智能测试平台企业微信服务'
enterprise_wechat_content = '泰斯特平台 \n >⛔ 测试失败 \n > 生成报告id: {}'.format(self.report_id) \
if self.failed_count > 0 else '泰斯特平台 \n >👍️️️️ 测试通过 \n > 生成报告id: {}' \
.format(self.report_id)
if hasattr(self, 'enterprise_wechat_notify_strategy') and self.enterprise_wechat_notify_strategy.get('fail') \
and self.failed_count > 0:
enterprise_wechat_res = self.send_enterprise_wechat_notify(title=enterprise_wechat_title, content=enterprise_wechat_content)
if not enterprise_wechat_res.status_code == 200:
raise BaseException('企业微信发送异常: {}'.format(enterprise_wechat_res.text))
if hasattr(self, 'enterprise_wechat_notify_strategy') and self.enterprise_wechat_notify_strategy.get('success') \
and self.failed_count <= 0:
enterprise_wechat_res = self.send_enterprise_wechat_notify(title=enterprise_wechat_title, content=enterprise_wechat_content)
if not enterprise_wechat_res.status_code == 200:
raise BaseException('企业微信发送异常: {}'.format(enterprise_wechat_res.text))
if is_send_ding_ding:
dingding_title = '智能测试平台钉钉服务'
dingding_content = '### ⛔️ 泰斯特平台 \n >⛔ 测试失败 \n > 生成报告id: {}'.format(self.report_id)\
......@@ -195,6 +228,7 @@ class Cron:
dingding_res = self.send_ding_ding_notify(title=dingding_title, content=dingding_content)
if not dingding_res.status_code == 200:
raise BaseException('钉钉发送异常: {}'.format(dingding_res.text))
if is_send_mail:
mesg_title = '测试平台告警'
mesg_content = "Dears: \n\n 定时测试中存在用例未通过!,请登录平台查看详情 !\n\n 报告编号为:" \
......
<!DOCTYPE html><html><head><meta charset=utf-8><meta name=viewport content="width=device-width,initial-scale=1"><title>autotest-platform</title><link href=/static/css/app.89ecf87645f1fc7dd3cae5f78478cb6d.css rel=stylesheet></head><body><div id=app></div><script type=text/javascript src=/static/js/manifest.2ae2e69a05c33dfc65f8.js></script><script type=text/javascript src=/static/js/vendor.c08586561cc366a8a5ad.js></script><script type=text/javascript src=/static/js/app.cdaaaaeda95fb2a81629.js></script></body></html>
\ No newline at end of file
<!DOCTYPE html><html><head><meta charset=utf-8><meta name=viewport content="width=device-width,initial-scale=1"><title>autotest-platform</title><link href=/static/css/app.91462e20208bf2d6e7d58eeecc8fed8a.css rel=stylesheet></head><body><div id=app></div><script type=text/javascript src=/static/js/manifest.2ae2e69a05c33dfc65f8.js></script><script type=text/javascript src=/static/js/vendor.5dbfeeda77126c757cd8.js></script><script type=text/javascript src=/static/js/app.e560881528aedaa26b91.js></script></body></html>
\ No newline at end of file
因为 它太大了无法显示 source diff 。你可以改为 查看blob
因为 它太大了无法显示 source diff 。你可以改为 查看blob
此差异已折叠。
{"version":3,"sources":["webpack:///webpack/bootstrap 52a61c11dcf31b7b2d42"],"names":["parentJsonpFunction","window","chunkIds","moreModules","executeModules","moduleId","chunkId","result","i","resolves","length","installedChunks","push","Object","prototype","hasOwnProperty","call","modules","shift","__webpack_require__","s","installedModules","2","exports","module","l","m","c","d","name","getter","o","defineProperty","configurable","enumerable","get","n","__esModule","object","property","p","oe","err","console","error"],"mappings":"aACA,IAAAA,EAAAC,OAAA,aACAA,OAAA,sBAAAC,EAAAC,EAAAC,GAIA,IADA,IAAAC,EAAAC,EAAAC,EAAAC,EAAA,EAAAC,KACQD,EAAAN,EAAAQ,OAAoBF,IAC5BF,EAAAJ,EAAAM,GACAG,EAAAL,IACAG,EAAAG,KAAAD,EAAAL,GAAA,IAEAK,EAAAL,GAAA,EAEA,IAAAD,KAAAF,EACAU,OAAAC,UAAAC,eAAAC,KAAAb,EAAAE,KACAY,EAAAZ,GAAAF,EAAAE,IAIA,IADAL,KAAAE,EAAAC,EAAAC,GACAK,EAAAC,QACAD,EAAAS,OAAAT,GAEA,GAAAL,EACA,IAAAI,EAAA,EAAYA,EAAAJ,EAAAM,OAA2BF,IACvCD,EAAAY,IAAAC,EAAAhB,EAAAI,IAGA,OAAAD,GAIA,IAAAc,KAGAV,GACAW,EAAA,GAIA,SAAAH,EAAAd,GAGA,GAAAgB,EAAAhB,GACA,OAAAgB,EAAAhB,GAAAkB,QAGA,IAAAC,EAAAH,EAAAhB,IACAG,EAAAH,EACAoB,GAAA,EACAF,YAUA,OANAN,EAAAZ,GAAAW,KAAAQ,EAAAD,QAAAC,IAAAD,QAAAJ,GAGAK,EAAAC,GAAA,EAGAD,EAAAD,QAKAJ,EAAAO,EAAAT,EAGAE,EAAAQ,EAAAN,EAGAF,EAAAS,EAAA,SAAAL,EAAAM,EAAAC,GACAX,EAAAY,EAAAR,EAAAM,IACAhB,OAAAmB,eAAAT,EAAAM,GACAI,cAAA,EACAC,YAAA,EACAC,IAAAL,KAMAX,EAAAiB,EAAA,SAAAZ,GACA,IAAAM,EAAAN,KAAAa,WACA,WAA2B,OAAAb,EAAA,SAC3B,WAAiC,OAAAA,GAEjC,OADAL,EAAAS,EAAAE,EAAA,IAAAA,GACAA,GAIAX,EAAAY,EAAA,SAAAO,EAAAC,GAAsD,OAAA1B,OAAAC,UAAAC,eAAAC,KAAAsB,EAAAC,IAGtDpB,EAAAqB,EAAA,IAGArB,EAAAsB,GAAA,SAAAC,GAA8D,MAApBC,QAAAC,MAAAF,GAAoBA","file":"static/js/manifest.2ae2e69a05c33dfc65f8.js","sourcesContent":[" \t// install a JSONP callback for chunk loading\n \tvar parentJsonpFunction = window[\"webpackJsonp\"];\n \twindow[\"webpackJsonp\"] = function webpackJsonpCallback(chunkIds, moreModules, executeModules) {\n \t\t// add \"moreModules\" to the modules object,\n \t\t// then flag all \"chunkIds\" as loaded and fire callback\n \t\tvar moduleId, chunkId, i = 0, resolves = [], result;\n \t\tfor(;i < chunkIds.length; i++) {\n \t\t\tchunkId = chunkIds[i];\n \t\t\tif(installedChunks[chunkId]) {\n \t\t\t\tresolves.push(installedChunks[chunkId][0]);\n \t\t\t}\n \t\t\tinstalledChunks[chunkId] = 0;\n \t\t}\n \t\tfor(moduleId in moreModules) {\n \t\t\tif(Object.prototype.hasOwnProperty.call(moreModules, moduleId)) {\n \t\t\t\tmodules[moduleId] = moreModules[moduleId];\n \t\t\t}\n \t\t}\n \t\tif(parentJsonpFunction) parentJsonpFunction(chunkIds, moreModules, executeModules);\n \t\twhile(resolves.length) {\n \t\t\tresolves.shift()();\n \t\t}\n \t\tif(executeModules) {\n \t\t\tfor(i=0; i < executeModules.length; i++) {\n \t\t\t\tresult = __webpack_require__(__webpack_require__.s = executeModules[i]);\n \t\t\t}\n \t\t}\n \t\treturn result;\n \t};\n\n \t// The module cache\n \tvar installedModules = {};\n\n \t// objects to store loaded and loading chunks\n \tvar installedChunks = {\n \t\t2: 0\n \t};\n\n \t// The require function\n \tfunction __webpack_require__(moduleId) {\n\n \t\t// Check if module is in cache\n \t\tif(installedModules[moduleId]) {\n \t\t\treturn installedModules[moduleId].exports;\n \t\t}\n \t\t// Create a new module (and put it into the cache)\n \t\tvar module = installedModules[moduleId] = {\n \t\t\ti: moduleId,\n \t\t\tl: false,\n \t\t\texports: {}\n \t\t};\n\n \t\t// Execute the module function\n \t\tmodules[moduleId].call(module.exports, module, module.exports, __webpack_require__);\n\n \t\t// Flag the module as loaded\n \t\tmodule.l = true;\n\n \t\t// Return the exports of the module\n \t\treturn module.exports;\n \t}\n\n\n \t// expose the modules object (__webpack_modules__)\n \t__webpack_require__.m = modules;\n\n \t// expose the module cache\n \t__webpack_require__.c = installedModules;\n\n \t// define getter function for harmony exports\n \t__webpack_require__.d = function(exports, name, getter) {\n \t\tif(!__webpack_require__.o(exports, name)) {\n \t\t\tObject.defineProperty(exports, name, {\n \t\t\t\tconfigurable: false,\n \t\t\t\tenumerable: true,\n \t\t\t\tget: getter\n \t\t\t});\n \t\t}\n \t};\n\n \t// getDefaultExport function for compatibility with non-harmony modules\n \t__webpack_require__.n = function(module) {\n \t\tvar getter = module && module.__esModule ?\n \t\t\tfunction getDefault() { return module['default']; } :\n \t\t\tfunction getModuleExports() { return module; };\n \t\t__webpack_require__.d(getter, 'a', getter);\n \t\treturn getter;\n \t};\n\n \t// Object.prototype.hasOwnProperty.call\n \t__webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };\n\n \t// __webpack_public_path__\n \t__webpack_require__.p = \"/\";\n\n \t// on error function for async loading\n \t__webpack_require__.oe = function(err) { console.error(err); throw err; };\n\n\n\n// WEBPACK FOOTER //\n// webpack/bootstrap 52a61c11dcf31b7b2d42"],"sourceRoot":""}
\ No newline at end of file
{"version":3,"sources":["webpack:///webpack/bootstrap 62f616ec5ab76539a152"],"names":["parentJsonpFunction","window","chunkIds","moreModules","executeModules","moduleId","chunkId","result","i","resolves","length","installedChunks","push","Object","prototype","hasOwnProperty","call","modules","shift","__webpack_require__","s","installedModules","2","exports","module","l","m","c","d","name","getter","o","defineProperty","configurable","enumerable","get","n","__esModule","object","property","p","oe","err","console","error"],"mappings":"aACA,IAAAA,EAAAC,OAAA,aACAA,OAAA,sBAAAC,EAAAC,EAAAC,GAIA,IADA,IAAAC,EAAAC,EAAAC,EAAAC,EAAA,EAAAC,KACQD,EAAAN,EAAAQ,OAAoBF,IAC5BF,EAAAJ,EAAAM,GACAG,EAAAL,IACAG,EAAAG,KAAAD,EAAAL,GAAA,IAEAK,EAAAL,GAAA,EAEA,IAAAD,KAAAF,EACAU,OAAAC,UAAAC,eAAAC,KAAAb,EAAAE,KACAY,EAAAZ,GAAAF,EAAAE,IAIA,IADAL,KAAAE,EAAAC,EAAAC,GACAK,EAAAC,QACAD,EAAAS,OAAAT,GAEA,GAAAL,EACA,IAAAI,EAAA,EAAYA,EAAAJ,EAAAM,OAA2BF,IACvCD,EAAAY,IAAAC,EAAAhB,EAAAI,IAGA,OAAAD,GAIA,IAAAc,KAGAV,GACAW,EAAA,GAIA,SAAAH,EAAAd,GAGA,GAAAgB,EAAAhB,GACA,OAAAgB,EAAAhB,GAAAkB,QAGA,IAAAC,EAAAH,EAAAhB,IACAG,EAAAH,EACAoB,GAAA,EACAF,YAUA,OANAN,EAAAZ,GAAAW,KAAAQ,EAAAD,QAAAC,IAAAD,QAAAJ,GAGAK,EAAAC,GAAA,EAGAD,EAAAD,QAKAJ,EAAAO,EAAAT,EAGAE,EAAAQ,EAAAN,EAGAF,EAAAS,EAAA,SAAAL,EAAAM,EAAAC,GACAX,EAAAY,EAAAR,EAAAM,IACAhB,OAAAmB,eAAAT,EAAAM,GACAI,cAAA,EACAC,YAAA,EACAC,IAAAL,KAMAX,EAAAiB,EAAA,SAAAZ,GACA,IAAAM,EAAAN,KAAAa,WACA,WAA2B,OAAAb,EAAA,SAC3B,WAAiC,OAAAA,GAEjC,OADAL,EAAAS,EAAAE,EAAA,IAAAA,GACAA,GAIAX,EAAAY,EAAA,SAAAO,EAAAC,GAAsD,OAAA1B,OAAAC,UAAAC,eAAAC,KAAAsB,EAAAC,IAGtDpB,EAAAqB,EAAA,IAGArB,EAAAsB,GAAA,SAAAC,GAA8D,MAApBC,QAAAC,MAAAF,GAAoBA","file":"static/js/manifest.2ae2e69a05c33dfc65f8.js","sourcesContent":[" \t// install a JSONP callback for chunk loading\n \tvar parentJsonpFunction = window[\"webpackJsonp\"];\n \twindow[\"webpackJsonp\"] = function webpackJsonpCallback(chunkIds, moreModules, executeModules) {\n \t\t// add \"moreModules\" to the modules object,\n \t\t// then flag all \"chunkIds\" as loaded and fire callback\n \t\tvar moduleId, chunkId, i = 0, resolves = [], result;\n \t\tfor(;i < chunkIds.length; i++) {\n \t\t\tchunkId = chunkIds[i];\n \t\t\tif(installedChunks[chunkId]) {\n \t\t\t\tresolves.push(installedChunks[chunkId][0]);\n \t\t\t}\n \t\t\tinstalledChunks[chunkId] = 0;\n \t\t}\n \t\tfor(moduleId in moreModules) {\n \t\t\tif(Object.prototype.hasOwnProperty.call(moreModules, moduleId)) {\n \t\t\t\tmodules[moduleId] = moreModules[moduleId];\n \t\t\t}\n \t\t}\n \t\tif(parentJsonpFunction) parentJsonpFunction(chunkIds, moreModules, executeModules);\n \t\twhile(resolves.length) {\n \t\t\tresolves.shift()();\n \t\t}\n \t\tif(executeModules) {\n \t\t\tfor(i=0; i < executeModules.length; i++) {\n \t\t\t\tresult = __webpack_require__(__webpack_require__.s = executeModules[i]);\n \t\t\t}\n \t\t}\n \t\treturn result;\n \t};\n\n \t// The module cache\n \tvar installedModules = {};\n\n \t// objects to store loaded and loading chunks\n \tvar installedChunks = {\n \t\t2: 0\n \t};\n\n \t// The require function\n \tfunction __webpack_require__(moduleId) {\n\n \t\t// Check if module is in cache\n \t\tif(installedModules[moduleId]) {\n \t\t\treturn installedModules[moduleId].exports;\n \t\t}\n \t\t// Create a new module (and put it into the cache)\n \t\tvar module = installedModules[moduleId] = {\n \t\t\ti: moduleId,\n \t\t\tl: false,\n \t\t\texports: {}\n \t\t};\n\n \t\t// Execute the module function\n \t\tmodules[moduleId].call(module.exports, module, module.exports, __webpack_require__);\n\n \t\t// Flag the module as loaded\n \t\tmodule.l = true;\n\n \t\t// Return the exports of the module\n \t\treturn module.exports;\n \t}\n\n\n \t// expose the modules object (__webpack_modules__)\n \t__webpack_require__.m = modules;\n\n \t// expose the module cache\n \t__webpack_require__.c = installedModules;\n\n \t// define getter function for harmony exports\n \t__webpack_require__.d = function(exports, name, getter) {\n \t\tif(!__webpack_require__.o(exports, name)) {\n \t\t\tObject.defineProperty(exports, name, {\n \t\t\t\tconfigurable: false,\n \t\t\t\tenumerable: true,\n \t\t\t\tget: getter\n \t\t\t});\n \t\t}\n \t};\n\n \t// getDefaultExport function for compatibility with non-harmony modules\n \t__webpack_require__.n = function(module) {\n \t\tvar getter = module && module.__esModule ?\n \t\t\tfunction getDefault() { return module['default']; } :\n \t\t\tfunction getModuleExports() { return module; };\n \t\t__webpack_require__.d(getter, 'a', getter);\n \t\treturn getter;\n \t};\n\n \t// Object.prototype.hasOwnProperty.call\n \t__webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };\n\n \t// __webpack_public_path__\n \t__webpack_require__.p = \"/\";\n\n \t// on error function for async loading\n \t__webpack_require__.oe = function(err) { console.error(err); throw err; };\n\n\n\n// WEBPACK FOOTER //\n// webpack/bootstrap 62f616ec5ab76539a152"],"sourceRoot":""}
\ No newline at end of file
此差异已折叠。
此差异已折叠。
......@@ -20,7 +20,6 @@
"babel-loader": "^7.1.1",
"babel-plugin-istanbul": "^4.1.1",
"babel-plugin-syntax-jsx": "^6.18.0",
"babel-runtime": "^6.3.19",
"babel-plugin-transform-runtime": "^6.22.0",
"babel-plugin-transform-vue-jsx": "^3.5.0",
"babel-preset-env": "^1.3.2",
......@@ -98,7 +97,6 @@
"babel-plugin-istanbul": "^4.1.1",
"babel-plugin-syntax-jsx": "^6.18.0",
"babel-plugin-transform-runtime": "^6.22.0",
"babel-runtime": "^6.3.19",
"babel-plugin-transform-vue-jsx": "^3.5.0",
"babel-preset-env": "^1.3.2",
"babel-preset-es2015": "^6.24.1",
......
......@@ -9,41 +9,8 @@
</br>
</br>
</br>
<div align="center">
<img src="../assets/imgs/i-am-strong.jpg">
</img>
</div>
</br>
</br>
</br>
<div style="font-size:20px; text-align: center;">
若您想在自动化测试上更近一步,欢迎扫描下方海报二维码、泰斯特带你成长~
</div>
</br>
</br>
</br>
<div align="center">
<img src="../assets/imgs/planetPoster.jpg">
</img>
</div>
</br>
</br>
</br>
<div style="font-size:20px; text-align: center;">
若您对「泰斯特平台」有任何吐槽或想对平台有更深入的了解,欢迎扫描下方二维码关注我并了解最新资讯。
</div>
</br>
</br>
</br>
<div align="center">
<img src="../assets/imgs/publicAcount.jpg">
</img>
</div>
</br>
</br>
</br>
<div style="font-size:20px; text-align: center;">
若您觉得「泰斯特平台」使用起来非常顺心,舒服,那么也希望您能为开源项目的维护迭代尽一份绵薄之力~
若您觉得「泰斯特平台」使用起来非常顺心,舒服,希望他变得更好用,那么也希望您能为开源项目的维护迭代尽一份绵薄之力~
</div>
</br>
</br>
......
......@@ -19,10 +19,8 @@
<!--<el-dropdown-item>我的消息</el-dropdown-item>-->
<!--<el-dropdown-item>设置</el-dropdown-item>-->
<el-dropdown-item divided>
<a target="_blank" href="https://mp.weixin.qq.com/s/csjfg7EHocHEvdwohxTwaA">
使用教程
</a>
</el-dropdown-item>
<a target="_blank" href="https://shimo.im/docs/8TqxG3Ttjvj9yT8T" >使用教程</a>
</el-dropdown-item>
<el-dropdown-item ref="logoutBtn" divided @click.native="logout">退出登录</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
......
......@@ -11,10 +11,8 @@
<!--<el-dropdown-item>我的消息</el-dropdown-item>-->
<!--<el-dropdown-item>设置</el-dropdown-item>-->
<el-dropdown-item divided>
<a target="_blank" href="https://mp.weixin.qq.com/s/csjfg7EHocHEvdwohxTwaA">
使用教程
</a>
</el-dropdown-item>
<a target="_blank" href="https://shimo.im/docs/8TqxG3Ttjvj9yT8T" >使用教程</a>
</el-dropdown-item>
<el-dropdown-item divided @click.native="logout">退出登录</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
......
......@@ -116,6 +116,80 @@
</div>
</transition>
<el-form-item label="钉钉提醒">
<el-radio
v-model="editForm.isDingDingNotify"
:label="true">
</el-radio>
<el-radio
v-model="editForm.isDingDingNotify"
:label="false">
</el-radio>
</el-form-item>
<transition name="el-zoom-in-top">
<div
class="form-item-sub form-item-short"
v-if="editForm.isDingDingNotify && editForm.isDingDingNotify.toString()==='true'">
<el-form-item
style="width:90%"
label="钉钉Token"
prop="dingdingAccessToken">
<el-input v-model.trim="editForm.dingdingAccessToken" auto-complete="off"></el-input>
</el-form-item>
<el-form-item v-show="editForm.isDingDingNotify.toString()==='true'" label="提醒策略">
<el-checkbox
v-model="editForm.dingdingNotifyStrategy.success"
label="测试全通过时提醒">
</el-checkbox>
<el-checkbox
v-model="editForm.dingdingNotifyStrategy.fail"
label="测试存在失败时提醒">
</el-checkbox>
</el-form-item>
</div>
</transition>
<el-form-item label="企微提醒">
<el-radio
v-model="editForm.isEnterpriseWechatNotify"
:label="true">
</el-radio>
<el-radio
v-model="editForm.isEnterpriseWechatNotify"
:label="false">
</el-radio>
</el-form-item>
<transition name="el-zoom-in-top">
<div
class="form-item-sub form-item-short"
v-if="editForm.isEnterpriseWechatNotify && editForm.isEnterpriseWechatNotify.toString()==='true'">
<el-form-item
style="width:90%"
label="企微Token"
prop="enterpriseWechatAccessToken">
<el-input v-model.trim="editForm.enterpriseWechatAccessToken" auto-complete="off"></el-input>
</el-form-item>
<el-form-item v-show="editForm.isEnterpriseWechatNotify.toString()==='true'" label="提醒策略">
<el-checkbox
v-model="editForm.enterpriseWechatNotifyStrategy.success"
label="测试全通过时提醒">
</el-checkbox>
<el-checkbox
v-model="editForm.enterpriseWechatNotifyStrategy.fail"
label="测试存在失败时提醒">
</el-checkbox>
</el-form-item>
</div>
</transition>
<el-form-item label="告警邮箱" prop="alarmMailList">
<el-select
style="width: 60%;"
......@@ -184,6 +258,80 @@
</div>
</transition>
<el-form-item label="钉钉提醒">
<el-radio
v-model="addForm.isDingDingNotify"
:label="true">
</el-radio>
<el-radio
v-model="addForm.isDingDingNotify"
:label="false">
</el-radio>
</el-form-item>
<transition name="el-zoom-in-top">
<div
class="form-item-sub form-item-short"
v-if="addForm.isDingDingNotify && addForm.isDingDingNotify.toString()==='true'">
<el-form-item
style="width:90%"
label="钉钉Token"
prop="dingdingAccessToken">
<el-input v-model.trim="addForm.dingdingAccessToken" auto-complete="off"></el-input>
</el-form-item>
<el-form-item v-show="addForm.isDingDingNotify.toString()==='true'" label="提醒策略">
<el-checkbox
v-model="addForm.dingdingNotifyStrategy.success"
label="测试全通过时提醒">
</el-checkbox>
<el-checkbox
v-model="addForm.dingdingNotifyStrategy.fail"
label="测试存在失败时提醒">
</el-checkbox>
</el-form-item>
</div>
</transition>
<el-form-item label="企微提醒">
<el-radio
v-model="addForm.isEnterpriseWechatNotify"
:label="true">
</el-radio>
<el-radio
v-model="addForm.isEnterpriseWechatNotify"
:label="false">
</el-radio>
</el-form-item>
<transition name="el-zoom-in-top">
<div
class="form-item-sub form-item-short"
v-if="addForm.isEnterpriseWechatNotify && addForm.isEnterpriseWechatNotify.toString()==='true'">
<el-form-item
style="width:90%"
label="企微Token"
prop="enterpriseWechatAccessToken">
<el-input v-model.trim="addForm.enterpriseWechatAccessToken" auto-complete="off"></el-input>
</el-form-item>
<el-form-item v-show="addForm.isEnterpriseWechatNotify.toString()==='true'" label="提醒策略">
<el-checkbox
v-model="addForm.enterpriseWechatNotifyStrategy.success"
label="测试全通过时提醒">
</el-checkbox>
<el-checkbox
v-model="addForm.enterpriseWechatNotifyStrategy.fail"
label="测试存在失败时提醒">
</el-checkbox>
</el-form-item>
</div>
</transition>
<el-form-item label="告警邮箱" prop="alarmMailList">
<el-select clearable style="width: 60%;" v-model="addForm['alarmMailList']" @visible-change="checkActiveMailList" multiple placeholder="请选择告警报告接受者(可多选)">
<el-option v-for="(item,index) in alarmMailList" :key="index" :label="item.name" :value="item.mailAddress"></el-option>
......@@ -276,6 +424,12 @@
alarmMailList: [
{ required: false, message: '请选择告警邮箱', trigger: 'blur' }
],
isDingDingNotify: [
{ required: false, message: '请选择是否使用钉钉提醒', trigger: 'blur' }
],
dingdingAccessToken: [
{ required: false, message: '请输入钉钉AccessToken', trigger: 'blur' }
],
triggerType: [
{ required: true, message: '请选择触发类型', trigger: 'blur' }
],
......@@ -298,6 +452,12 @@
isExecuteForbiddenedCase: false,
testDomain: '',
alarmMailList: [],
isDingDingNotify: false,
dingdingNotifyStrategy: {success: false, fail: true},
dingdingAccessToken: '',
isEnterpriseWechatNotify: false,
enterpriseWechatNotifyStrategy: {success: false, fail: true},
enterpriseWechatAccessToken: '',
triggerType: '',
interval: 0,
runDate: '',
......@@ -319,6 +479,12 @@
alarmMailList: [
{ required: false, message: '请选择告警邮箱', trigger: 'blur' }
],
isDingDingNotify: [
{ required: false, message: '请选择是否使用钉钉提醒', trigger: 'blur' }
],
dingdingAccessToken: [
{ required: false, message: '请输入钉钉AccessToken', trigger: 'blur' }
],
triggerType: [
{ required: true, message: '请选择触发类型', trigger: 'blur' }
],
......@@ -341,6 +507,12 @@
isExecuteForbiddenedCase: false,
testDomain: '',
alarmMailList: [],
isDingDingNotify: false,
dingdingNotifyStrategy: {success: false, fail: true},
dingdingAccessToken: '',
isEnterpriseWechatNotify: false,
enterpriseWechatNotifyStrategy: {success: false, fail: true},
enterpriseWechatAccessToken: '',
triggerType: '',
interval: '',
runDate: '',
......@@ -550,6 +722,12 @@
triggerType: self.addForm.triggerType,
description: self.addForm.description,
alarmMailList: self.addForm.alarmMailList,
isDingDingNotify: self.addForm.isDingDingNotify,
dingdingAccessToken: self.addForm.dingdingAccessToken,
dingdingNotifyStrategy: self.addForm.dingdingNotifyStrategy,
isEnterpriseWechatNotify: self.addForm.isEnterpriseWechatNotify,
enterpriseWechatAccessToken: self.addForm.enterpriseWechatAccessToken,
enterpriseWechatNotifyStrategy: self.addForm.enterpriseWechatNotifyStrategy,
creatorNickName: unescape(getCookie('nickName').replace(/\\u/g, '%u')) || '未知用户',
lastUpdatorNickName: unescape(getCookie('nickName').replace(/\\u/g, '%u')) || '未知用户'
};
......@@ -624,6 +802,12 @@
next_run_time: self.editForm.next_run_time, // 用于判断是否要resume定时任务
description: self.editForm.description,
alarmMailList: self.editForm.alarmMailList,
isDingDingNotify: self.editForm.isDingDingNotify || false,
dingdingAccessToken: self.editForm.dingdingAccessToken || '',
dingdingNotifyStrategy: self.editForm.dingdingNotifyStrategy,
isEnterpriseWechatNotify: self.editForm.isEnterpriseWechatNotify || false,
enterpriseWechatAccessToken: self.editForm.enterpriseWechatAccessToken || '',
enterpriseWechatNotifyStrategy: self.editForm.enterpriseWechatNotifyStrategy,
lastUpdatorNickName: unescape(getCookie('nickName').replace(/\\u/g, '%u')) || '未知用户'
};
if (self.editForm.runDate && self.editForm.runDate.toString().trim() !== ''){
......
images/泰斯特平台LOGO.png

124.3 KB | W: | H:

images/泰斯特平台LOGO.png

110.6 KB | W: | H:

images/泰斯特平台LOGO.png
images/泰斯特平台LOGO.png
images/泰斯特平台LOGO.png
images/泰斯特平台LOGO.png
  • 2-up
  • Swipe
  • Onion skin
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册