refactor(项目设置): 检查三方平台关联项目有效性
--bug=1010673 --user=李玉号 【项目设置】-zentao/tapd 编辑项目 写入无效的项目id ,进入缺陷管理,创建缺陷会报错 https://www.tapd.cn/55049933/s/1115973
This commit is contained in:
parent
6c0dcb5e7d
commit
fd51593dc0
|
@ -152,4 +152,9 @@ public class ProjectController {
|
||||||
public boolean isVersionEnable(@PathVariable String projectId) {
|
public boolean isVersionEnable(@PathVariable String projectId) {
|
||||||
return projectService.isVersionEnable(projectId);
|
return projectService.isVersionEnable(projectId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@PostMapping("/check/third/project")
|
||||||
|
public void checkThirdProjectExist(@RequestBody Project project) {
|
||||||
|
projectService.checkThirdProjectExist(project);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -36,6 +36,11 @@ import io.metersphere.performance.request.DeleteTestPlanRequest;
|
||||||
import io.metersphere.performance.request.QueryProjectFileRequest;
|
import io.metersphere.performance.request.QueryProjectFileRequest;
|
||||||
import io.metersphere.performance.service.PerformanceReportService;
|
import io.metersphere.performance.service.PerformanceReportService;
|
||||||
import io.metersphere.performance.service.PerformanceTestService;
|
import io.metersphere.performance.service.PerformanceTestService;
|
||||||
|
import io.metersphere.track.issue.AbstractIssuePlatform;
|
||||||
|
import io.metersphere.track.issue.JiraPlatform;
|
||||||
|
import io.metersphere.track.issue.TapdPlatform;
|
||||||
|
import io.metersphere.track.issue.ZentaoPlatform;
|
||||||
|
import io.metersphere.track.request.testcase.IssuesRequest;
|
||||||
import io.metersphere.track.service.TestCaseService;
|
import io.metersphere.track.service.TestCaseService;
|
||||||
import io.metersphere.track.service.TestPlanProjectService;
|
import io.metersphere.track.service.TestPlanProjectService;
|
||||||
import io.metersphere.track.service.TestPlanReportService;
|
import io.metersphere.track.service.TestPlanReportService;
|
||||||
|
@ -188,6 +193,35 @@ public class ProjectService {
|
||||||
return project;
|
return project;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void checkThirdProjectExist(Project project) {
|
||||||
|
IssuesRequest issuesRequest = new IssuesRequest();
|
||||||
|
if (StringUtils.isBlank(project.getId())) {
|
||||||
|
MSException.throwException("project ID cannot be empty");
|
||||||
|
}
|
||||||
|
issuesRequest.setProjectId(project.getId());
|
||||||
|
issuesRequest.setWorkspaceId(project.getWorkspaceId());
|
||||||
|
if (StringUtils.equalsIgnoreCase(project.getPlatform(), IssuesManagePlatform.Tapd.name())) {
|
||||||
|
TapdPlatform tapd = new TapdPlatform(issuesRequest);
|
||||||
|
this.doCheckThirdProjectExist(tapd, project.getTapdId());
|
||||||
|
} else if (StringUtils.equalsIgnoreCase(project.getPlatform(), IssuesManagePlatform.Jira.name())) {
|
||||||
|
JiraPlatform jira = new JiraPlatform(issuesRequest);
|
||||||
|
this.doCheckThirdProjectExist(jira, project.getJiraKey());
|
||||||
|
} else if (StringUtils.equalsIgnoreCase(project.getPlatform(), IssuesManagePlatform.Zentao.name())) {
|
||||||
|
ZentaoPlatform zentao = new ZentaoPlatform(issuesRequest);
|
||||||
|
this.doCheckThirdProjectExist(zentao, project.getZentaoId());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void doCheckThirdProjectExist(AbstractIssuePlatform platform, String relateId) {
|
||||||
|
if (StringUtils.isBlank(relateId)) {
|
||||||
|
MSException.throwException(Translator.get("issue_project_not_exist"));
|
||||||
|
}
|
||||||
|
Boolean exist = platform.checkProjectExist(relateId);
|
||||||
|
if (BooleanUtils.isFalse(exist)) {
|
||||||
|
MSException.throwException(Translator.get("issue_project_not_exist"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private String genSystemId() {
|
private String genSystemId() {
|
||||||
String maxSystemIdInDb = extProjectMapper.getMaxSystemId();
|
String maxSystemIdInDb = extProjectMapper.getMaxSystemId();
|
||||||
String systemId = "10001";
|
String systemId = "10001";
|
||||||
|
|
|
@ -522,4 +522,9 @@ public abstract class AbstractIssuePlatform implements IssuesPlatform {
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Boolean checkProjectExist(String relateId) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -78,4 +78,11 @@ public interface IssuesPlatform {
|
||||||
* @return
|
* @return
|
||||||
*/
|
*/
|
||||||
IssueTemplateDao getThirdPartTemplate();
|
IssueTemplateDao getThirdPartTemplate();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查其它平台关联的ID是否存在
|
||||||
|
* @param relateId 其它平台在MS项目上关联的相关ID
|
||||||
|
* @return Boolean
|
||||||
|
*/
|
||||||
|
Boolean checkProjectExist(String relateId);
|
||||||
}
|
}
|
||||||
|
|
|
@ -133,7 +133,11 @@ public class JiraPlatform extends AbstractIssuePlatform {
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<JiraIssueType> getIssueTypes(String jiraKey) {
|
public List<JiraIssueType> getIssueTypes(String jiraKey) {
|
||||||
|
try {
|
||||||
return jiraClientV2.getIssueType(jiraKey);
|
return jiraClientV2.getIssueType(jiraKey);
|
||||||
|
} catch (Exception e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -648,4 +652,17 @@ public class JiraPlatform extends AbstractIssuePlatform {
|
||||||
});
|
});
|
||||||
return options.toJSONString();
|
return options.toJSONString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Boolean checkProjectExist(String relateId) {
|
||||||
|
try {
|
||||||
|
JiraIssueProject project = jiraClientV2.getProject(relateId);
|
||||||
|
if (project != null && StringUtils.isNotBlank(project.getId())) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -264,4 +264,13 @@ public class TapdPlatform extends AbstractIssuePlatform {
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Boolean checkProjectExist(String relateId) {
|
||||||
|
try {
|
||||||
|
return tapdClient.checkProjectExist(relateId);
|
||||||
|
} catch (Exception e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -409,4 +409,9 @@ public class ZentaoPlatform extends AbstractIssuePlatform {
|
||||||
String imgRegex = "<img src.*?/>";
|
String imgRegex = "<img src.*?/>";
|
||||||
return ztDescription.replaceAll(imgRegex, "");
|
return ztDescription.replaceAll(imgRegex, "");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Boolean checkProjectExist(String relateId) {
|
||||||
|
return zentaoClient.checkProjectExist(relateId);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -117,4 +117,11 @@ public class TapdClient extends BaseClient {
|
||||||
USER_NAME = config.getAccount();
|
USER_NAME = config.getAccount();
|
||||||
PASSWD = config.getPassword();
|
PASSWD = config.getPassword();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean checkProjectExist(String relateId) {
|
||||||
|
String url = getBaseUrl() + "/roles?workspace_id={1}";
|
||||||
|
ResponseEntity<String> response = restTemplate.exchange(url, HttpMethod.GET, getAuthHttpEntity(), String.class, relateId);
|
||||||
|
TapdGetIssueResponse res = (TapdGetIssueResponse) getResultForObject(TapdGetIssueResponse.class, response);
|
||||||
|
return res == null || res.getStatus() != 404;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -188,4 +188,19 @@ public abstract class ZentaoClient extends BaseClient {
|
||||||
}
|
}
|
||||||
return String.format(replaceImgUrl, suffix);
|
return String.format(replaceImgUrl, suffix);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean checkProjectExist(String relateId) {
|
||||||
|
String sessionId = login();
|
||||||
|
ResponseEntity<String> response = restTemplate.exchange(requestUrl.getProductGet(),
|
||||||
|
HttpMethod.GET, null, String.class, relateId, sessionId);
|
||||||
|
try {
|
||||||
|
Object data = JSONObject.parseObject(response.getBody()).get("data");
|
||||||
|
if (!StringUtils.equals((String) data, "false")) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
LogUtil.info("query zentao product info error. product id: " + relateId);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,6 +19,7 @@ public class ZentaoGetClient extends ZentaoClient {
|
||||||
private static final String CREATE_META_DATA="?m=bug&f=create&productID={0}&t=json&zentaosid={1}";
|
private static final String CREATE_META_DATA="?m=bug&f=create&productID={0}&t=json&zentaosid={1}";
|
||||||
private static final String REPLACE_IMG_URL="<img src=\"%s/index.php?m=file&f=read&fileID=$1\"/>";
|
private static final String REPLACE_IMG_URL="<img src=\"%s/index.php?m=file&f=read&fileID=$1\"/>";
|
||||||
private static final Pattern IMG_PATTERN = Pattern.compile("m=file&f=read&fileID=(.*?)\"/>");
|
private static final Pattern IMG_PATTERN = Pattern.compile("m=file&f=read&fileID=(.*?)\"/>");
|
||||||
|
private static final String PRODUCT_GET = "&module=product&methodName=getById¶ms=productID={0}&zentaosid={1}";
|
||||||
// 注意 recTotal={1}&recPerPage={2}&pageID={3} 顺序不能调换,有点恶心
|
// 注意 recTotal={1}&recPerPage={2}&pageID={3} 顺序不能调换,有点恶心
|
||||||
private static final String BUG_LIST_URL = "/?m=bug&f=browse&productID={0}&branch=&browseType=¶m=0&orderBy=&recTotal={1}&recPerPage={2}&pageID={3}&t=json&zentaosid={4}";
|
private static final String BUG_LIST_URL = "/?m=bug&f=browse&productID={0}&branch=&browseType=¶m=0&orderBy=&recTotal={1}&recPerPage={2}&pageID={3}&t=json&zentaosid={4}";
|
||||||
|
|
||||||
|
@ -43,6 +44,7 @@ public class ZentaoGetClient extends ZentaoClient {
|
||||||
request.setBugDelete(getNotSuperModelUrl(BUG_DELETE));
|
request.setBugDelete(getNotSuperModelUrl(BUG_DELETE));
|
||||||
request.setBugList(getNotSuperModelUrl(BUG_LIST_URL));
|
request.setBugList(getNotSuperModelUrl(BUG_LIST_URL));
|
||||||
request.setCreateMetaData(getNotSuperModelUrl(CREATE_META_DATA));
|
request.setCreateMetaData(getNotSuperModelUrl(CREATE_META_DATA));
|
||||||
|
request.setProductGet(getUrl(PRODUCT_GET));
|
||||||
requestUrl = request;
|
requestUrl = request;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -20,6 +20,7 @@ public class ZentaoPathInfoClient extends ZentaoClient {
|
||||||
private static final String FILE_UPLOAD = "/api-getModel-file-saveUpload.json?zentaosid=";
|
private static final String FILE_UPLOAD = "/api-getModel-file-saveUpload.json?zentaosid=";
|
||||||
private static final String REPLACE_IMG_URL = "<img src=\"%s/file-read-$1\"/>";
|
private static final String REPLACE_IMG_URL = "<img src=\"%s/file-read-$1\"/>";
|
||||||
private static final Pattern IMG_PATTERN = Pattern.compile("file-read-(.*?)\"/>");
|
private static final Pattern IMG_PATTERN = Pattern.compile("file-read-(.*?)\"/>");
|
||||||
|
private static final String PRODUCT_GET = "/api-getModel-product-getById-productID={0}?zentaosid={1}";
|
||||||
private static final String BUG_LIST_URL = "/bug-browse-{1}---0--{2}-{3}-{4}.json?zentaosid={5}";
|
private static final String BUG_LIST_URL = "/bug-browse-{1}---0--{2}-{3}-{4}.json?zentaosid={5}";
|
||||||
|
|
||||||
|
|
||||||
|
@ -44,6 +45,7 @@ public class ZentaoPathInfoClient extends ZentaoClient {
|
||||||
request.setBugDelete(getUrl(BUG_DELETE));
|
request.setBugDelete(getUrl(BUG_DELETE));
|
||||||
request.setBugList(getUrl(BUG_LIST_URL));
|
request.setBugList(getUrl(BUG_LIST_URL));
|
||||||
request.setCreateMetaData(getUrl(CREATE_META_DATA));
|
request.setCreateMetaData(getUrl(CREATE_META_DATA));
|
||||||
|
request.setProductGet(getUrl(PRODUCT_GET));
|
||||||
requestUrl = request;
|
requestUrl = request;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -21,5 +21,6 @@ public class RequestUrl {
|
||||||
private String buildsGet;
|
private String buildsGet;
|
||||||
private String fileUpload;
|
private String fileUpload;
|
||||||
private String replaceImgUrl;
|
private String replaceImgUrl;
|
||||||
|
private String productGet;
|
||||||
private Pattern imgPattern;
|
private Pattern imgPattern;
|
||||||
}
|
}
|
||||||
|
|
|
@ -163,6 +163,7 @@ id_required=ID required
|
||||||
id_repeat_in_table=ID is repeat in table
|
id_repeat_in_table=ID is repeat in table
|
||||||
step_model_tip=Step description fill in STEP, text description please fill in TEXT (not required)
|
step_model_tip=Step description fill in STEP, text description please fill in TEXT (not required)
|
||||||
case_status_not_exist=The use case status must be Prepare, Underway way and Completed
|
case_status_not_exist=The use case status must be Prepare, Underway way and Completed
|
||||||
|
issue_project_not_exist=ID does not exist or other errors
|
||||||
#ldap
|
#ldap
|
||||||
ldap_url_is_null=LDAP address is empty
|
ldap_url_is_null=LDAP address is empty
|
||||||
ldap_dn_is_null=LDAP binding DN is empty
|
ldap_dn_is_null=LDAP binding DN is empty
|
||||||
|
|
|
@ -163,6 +163,7 @@ id_required=ID必填
|
||||||
id_repeat_in_table=表格内ID重复
|
id_repeat_in_table=表格内ID重复
|
||||||
step_model_tip=步骤描述填写 STEP,文本描述请填写 TEXT (非必填)
|
step_model_tip=步骤描述填写 STEP,文本描述请填写 TEXT (非必填)
|
||||||
case_status_not_exist=用例状态必须为未开始(Prepare)、进行中(Underway)、已完成(Completed)
|
case_status_not_exist=用例状态必须为未开始(Prepare)、进行中(Underway)、已完成(Completed)
|
||||||
|
issue_project_not_exist=ID不存在或其它错误
|
||||||
#ldap
|
#ldap
|
||||||
ldap_url_is_null=LDAP地址为空
|
ldap_url_is_null=LDAP地址为空
|
||||||
ldap_dn_is_null=LDAP绑定DN为空
|
ldap_dn_is_null=LDAP绑定DN为空
|
||||||
|
|
|
@ -163,6 +163,7 @@ id_required=ID必填
|
||||||
id_repeat_in_table=表格內ID重復
|
id_repeat_in_table=表格內ID重復
|
||||||
step_model_tip=步驟描述填寫 STEP,文本描述請填寫 TEXT (非必填)
|
step_model_tip=步驟描述填寫 STEP,文本描述請填寫 TEXT (非必填)
|
||||||
case_status_not_exist=用例狀態必須為未開始(Prepare)、進行中(Underway)、已完成(Completed)
|
case_status_not_exist=用例狀態必須為未開始(Prepare)、進行中(Underway)、已完成(Completed)
|
||||||
|
issue_project_not_exist=ID不存在或其它錯誤
|
||||||
#ldap
|
#ldap
|
||||||
ldap_url_is_null=LDAP地址為空
|
ldap_url_is_null=LDAP地址為空
|
||||||
ldap_dn_is_null=LDAP綁定DN為空
|
ldap_dn_is_null=LDAP綁定DN為空
|
||||||
|
|
|
@ -39,12 +39,17 @@
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item :label-width="labelWidth" :label="$t('project.tapd_id')" v-if="tapd">
|
<el-form-item :label-width="labelWidth" :label="$t('project.tapd_id')" v-if="tapd">
|
||||||
<el-input v-model="form.tapdId" autocomplete="off"></el-input>
|
<el-input v-model="form.tapdId" autocomplete="off"></el-input>
|
||||||
|
<el-button @click="check" type="primary" class="checkButton">{{ $t('test_track.issue.check_id_exist') }}</el-button>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
|
||||||
<project-jira-config v-if="jira" :label-width="labelWidth" :form="form"/>
|
<project-jira-config v-if="jira" :label-width="labelWidth" :form="form">
|
||||||
|
<template #checkBtn>
|
||||||
|
<el-button @click="check" type="primary" class="checkButton">{{ $t('test_track.issue.check_id_exist') }}</el-button>
|
||||||
|
</template>
|
||||||
|
</project-jira-config>
|
||||||
<el-form-item :label-width="labelWidth" :label="$t('project.zentao_id')" v-if="zentao">
|
<el-form-item :label-width="labelWidth" :label="$t('project.zentao_id')" v-if="zentao">
|
||||||
<el-input v-model="form.zentaoId" autocomplete="off"></el-input>
|
<el-input v-model="form.zentaoId" autocomplete="off"></el-input>
|
||||||
|
<el-button @click="check" type="primary" class="checkButton">{{ $t('test_track.issue.check_id_exist') }}</el-button>
|
||||||
<ms-instructions-icon effect="light">
|
<ms-instructions-icon effect="light">
|
||||||
<template>
|
<template>
|
||||||
禅道流程:产品-项目 | 产品-迭代 | 产品-冲刺 | 项目-迭代 | 项目-冲刺 <br/><br/>
|
禅道流程:产品-项目 | 产品-迭代 | 产品-冲刺 | 项目-迭代 | 项目-冲刺 <br/><br/>
|
||||||
|
@ -186,6 +191,15 @@ export default {
|
||||||
this.createVisible = false;
|
this.createVisible = false;
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
check() {
|
||||||
|
if (!this.form.id) {
|
||||||
|
this.$warning(this.$t("test_track.issue.save_project_first"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.$post("/project/check/third/project", this.form, () => {
|
||||||
|
this.$success("OK");
|
||||||
|
});
|
||||||
|
},
|
||||||
getOptions() {
|
getOptions() {
|
||||||
if (this.$refs.issueTemplate) {
|
if (this.$refs.issueTemplate) {
|
||||||
this.$refs.issueTemplate.getTemplateOptions();
|
this.$refs.issueTemplate.getTemplateOptions();
|
||||||
|
@ -309,4 +323,8 @@ pre {
|
||||||
.el-input, .el-textarea {
|
.el-input, .el-textarea {
|
||||||
width: 80%;
|
width: 80%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.checkButton {
|
||||||
|
margin-left: 5px;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
<div>
|
<div>
|
||||||
<el-form-item :label-width="labelWidth" :label="$t('project.jira_key')">
|
<el-form-item :label-width="labelWidth" :label="$t('project.jira_key')">
|
||||||
<el-input v-model="form.jiraKey" autocomplete="off" @blur="getIssueTypeOption"/>
|
<el-input v-model="form.jiraKey" autocomplete="off" @blur="getIssueTypeOption"/>
|
||||||
|
<slot name="checkBtn"></slot>
|
||||||
<ms-instructions-icon effect="light">
|
<ms-instructions-icon effect="light">
|
||||||
<template>
|
<template>
|
||||||
<img class="jira-image" src="@/assets/jira-key.png"/>
|
<img class="jira-image" src="@/assets/jira-key.png"/>
|
||||||
|
|
|
@ -2362,7 +2362,9 @@ export default {
|
||||||
update_third_party_bugs: "Update the defects of third-party platforms",
|
update_third_party_bugs: "Update the defects of third-party platforms",
|
||||||
sync_bugs: "Synchronization Issue",
|
sync_bugs: "Synchronization Issue",
|
||||||
save_before_open_comment: "Please save issue before comment",
|
save_before_open_comment: "Please save issue before comment",
|
||||||
delete_tip: "Confirm Delete Issue:"
|
delete_tip: "Confirm Delete Issue:",
|
||||||
|
check_id_exist: "Check",
|
||||||
|
save_project_first: "Please save the project first"
|
||||||
},
|
},
|
||||||
report: {
|
report: {
|
||||||
name: "Test Plan Report",
|
name: "Test Plan Report",
|
||||||
|
|
|
@ -2367,7 +2367,9 @@ export default {
|
||||||
update_third_party_bugs: "更新第三方平台的缺陷",
|
update_third_party_bugs: "更新第三方平台的缺陷",
|
||||||
sync_bugs: "同步缺陷",
|
sync_bugs: "同步缺陷",
|
||||||
save_before_open_comment: "请先保存缺陷再添加评论",
|
save_before_open_comment: "请先保存缺陷再添加评论",
|
||||||
delete_tip: "确认删除缺陷:"
|
delete_tip: "确认删除缺陷:",
|
||||||
|
check_id_exist: "检查",
|
||||||
|
save_project_first: "请先保存项目"
|
||||||
},
|
},
|
||||||
report: {
|
report: {
|
||||||
name: "测试计划报告",
|
name: "测试计划报告",
|
||||||
|
|
|
@ -2366,7 +2366,9 @@ export default {
|
||||||
update_third_party_bugs: "更新第三方平臺的缺陷",
|
update_third_party_bugs: "更新第三方平臺的缺陷",
|
||||||
sync_bugs: "同步缺陷",
|
sync_bugs: "同步缺陷",
|
||||||
save_before_open_comment: "請先保存缺陷再添加評論",
|
save_before_open_comment: "請先保存缺陷再添加評論",
|
||||||
delete_tip: "確認刪除缺陷:"
|
delete_tip: "確認刪除缺陷:",
|
||||||
|
check_id_exist: "檢查",
|
||||||
|
save_project_first: "請先保存項目"
|
||||||
},
|
},
|
||||||
report: {
|
report: {
|
||||||
name: "測試計劃報告",
|
name: "測試計劃報告",
|
||||||
|
|
Loading…
Reference in New Issue