cherry-pic fix(zentao): update zentao plugin, do lots of fixes (#5963)

* fix(zentao): fix zentao's bug collector, add project and execution's bugs, remove product's bugs

* fix(zentao): fix ci error

* fix(zentao): add story_point when issue's type story

* fix(zentao): update project's url

* fix(zentao): fix project's url

* fix(zentao): fix e2e test

* fix(zentao): extract all task/bug/story ids from log comment
diff --git a/backend/plugins/zentao/e2e/snapshot_tables/issues_story.csv b/backend/plugins/zentao/e2e/snapshot_tables/issues_story.csv
index e3c1099..564f63f 100644
--- a/backend/plugins/zentao/e2e/snapshot_tables/issues_story.csv
+++ b/backend/plugins/zentao/e2e/snapshot_tables/issues_story.csv
@@ -1,10 +1,10 @@
 id,url,icon_url,issue_key,title,description,epic_key,type,original_type,status,original_status,story_point,resolution_date,created_date,updated_date,lead_time_minutes,parent_issue_id,priority,original_estimate_minutes,time_spent_minutes,time_remaining_minutes,creator_id,creator_name,assignee_id,assignee_name,severity,component
-zentao:ZentaoStory:1:1,http://iwater.red:8000/story-view-1.html,,1,首页设计和开发,,,REQUIREMENT,story,DONE,active,0,,2012-06-05T02:09:49.000+00:00,2012-06-05T02:25:19.000+00:00,0,,1,60,0,0,zentao:ZentaoAccount:1:2,产品经理,zentao:ZentaoAccount:1:2,产品经理,,
-zentao:ZentaoStory:1:2,http://iwater.red:8000/story-view-2.html,,2,新闻中心的设计和开发。,,,REQUIREMENT,story,DONE,active,0,,2012-06-05T02:16:37.000+00:00,2012-06-05T02:25:33.000+00:00,0,,1,60,0,0,zentao:ZentaoAccount:1:2,产品经理,zentao:ZentaoAccount:1:2,产品经理,,
+zentao:ZentaoStory:1:1,http://iwater.red:8000/story-view-1.html,,1,首页设计和开发,,,REQUIREMENT,story,DONE,active,1,,2012-06-05T02:09:49.000+00:00,2012-06-05T02:25:19.000+00:00,0,,1,60,0,0,zentao:ZentaoAccount:1:2,产品经理,zentao:ZentaoAccount:1:2,产品经理,,
+zentao:ZentaoStory:1:2,http://iwater.red:8000/story-view-2.html,,2,新闻中心的设计和开发。,,,REQUIREMENT,story,DONE,active,1,,2012-06-05T02:16:37.000+00:00,2012-06-05T02:25:33.000+00:00,0,,1,60,0,0,zentao:ZentaoAccount:1:2,产品经理,zentao:ZentaoAccount:1:2,产品经理,,
 zentao:ZentaoStory:1:3,http://iwater.red:8000/story-view-3.html,,3,成果展示的设计和开发,,,REQUIREMENT,story,DONE,active,0,,2012-06-05T02:18:10.000+00:00,2012-06-05T02:25:38.000+00:00,0,,1,0,0,0,zentao:ZentaoAccount:1:2,产品经理,zentao:ZentaoAccount:1:2,产品经理,,
-zentao:ZentaoStory:1:4,http://iwater.red:8000/story-view-4.html,,4,售后服务的设计和开发,,,REQUIREMENT,story,DONE,active,0,,2012-06-05T02:20:16.000+00:00,2012-06-05T02:25:42.000+00:00,0,,1,60,0,0,zentao:ZentaoAccount:1:2,产品经理,zentao:ZentaoAccount:1:2,产品经理,,
-zentao:ZentaoStory:1:5,http://iwater.red:8000/story-view-5.html,,5,诚聘英才的设计和开发,,,REQUIREMENT,story,reviewing,reviewing,0,,2012-06-05T02:21:39.000+00:00,,0,,1,60,0,0,zentao:ZentaoAccount:1:2,产品经理,zentao:ZentaoAccount:1:2,产品经理,,
-zentao:ZentaoStory:1:6,http://iwater.red:8000/story-view-6.html,,6,合作洽谈的设计和开发,,,REQUIREMENT,story,reviewing,reviewing,0,,2012-06-05T02:23:11.000+00:00,,0,,1,60,0,0,zentao:ZentaoAccount:1:2,产品经理,zentao:ZentaoAccount:1:2,产品经理,,
-zentao:ZentaoStory:1:7,http://iwater.red:8000/story-view-7.html,,7,关于我们的设计和开发,,,REQUIREMENT,story,reviewing,reviewing,0,,2012-06-05T02:24:19.000+00:00,,0,,1,60,0,0,zentao:ZentaoAccount:1:2,产品经理,zentao:ZentaoAccount:1:2,产品经理,,
-zentao:ZentaoStory:1:8,http://iwater.red:8000/story-view-8.html,,8,新闻中心的设计和开发。,,,REQUIREMENT,story,DONE,active,0,,2012-06-05T02:16:37.000+00:00,2012-06-05T02:25:33.000+00:00,0,,1,60,0,0,zentao:ZentaoAccount:1:2,产品经理,zentao:ZentaoAccount:1:2,产品经理,,
-zentao:ZentaoStory:1:9,http://iwater.red:8000/story-view-9.html,,9,首页设计和开发,,,REQUIREMENT,story,DONE,active,0,,2012-06-05T02:09:49.000+00:00,2012-06-05T02:25:19.000+00:00,0,,1,60,0,0,zentao:ZentaoAccount:1:2,产品经理,zentao:ZentaoAccount:1:2,产品经理,,
+zentao:ZentaoStory:1:4,http://iwater.red:8000/story-view-4.html,,4,售后服务的设计和开发,,,REQUIREMENT,story,DONE,active,1,,2012-06-05T02:20:16.000+00:00,2012-06-05T02:25:42.000+00:00,0,,1,60,0,0,zentao:ZentaoAccount:1:2,产品经理,zentao:ZentaoAccount:1:2,产品经理,,
+zentao:ZentaoStory:1:5,http://iwater.red:8000/story-view-5.html,,5,诚聘英才的设计和开发,,,REQUIREMENT,story,reviewing,reviewing,1,,2012-06-05T02:21:39.000+00:00,,0,,1,60,0,0,zentao:ZentaoAccount:1:2,产品经理,zentao:ZentaoAccount:1:2,产品经理,,
+zentao:ZentaoStory:1:6,http://iwater.red:8000/story-view-6.html,,6,合作洽谈的设计和开发,,,REQUIREMENT,story,reviewing,reviewing,1,,2012-06-05T02:23:11.000+00:00,,0,,1,60,0,0,zentao:ZentaoAccount:1:2,产品经理,zentao:ZentaoAccount:1:2,产品经理,,
+zentao:ZentaoStory:1:7,http://iwater.red:8000/story-view-7.html,,7,关于我们的设计和开发,,,REQUIREMENT,story,reviewing,reviewing,1,,2012-06-05T02:24:19.000+00:00,,0,,1,60,0,0,zentao:ZentaoAccount:1:2,产品经理,zentao:ZentaoAccount:1:2,产品经理,,
+zentao:ZentaoStory:1:8,http://iwater.red:8000/story-view-8.html,,8,新闻中心的设计和开发。,,,REQUIREMENT,story,DONE,active,1,,2012-06-05T02:16:37.000+00:00,2012-06-05T02:25:33.000+00:00,0,,1,60,0,0,zentao:ZentaoAccount:1:2,产品经理,zentao:ZentaoAccount:1:2,产品经理,,
+zentao:ZentaoStory:1:9,http://iwater.red:8000/story-view-9.html,,9,首页设计和开发,,,REQUIREMENT,story,DONE,active,1,,2012-06-05T02:09:49.000+00:00,2012-06-05T02:25:19.000+00:00,0,,1,60,0,0,zentao:ZentaoAccount:1:2,产品经理,zentao:ZentaoAccount:1:2,产品经理,,
diff --git a/backend/plugins/zentao/tasks/bug_collector.go b/backend/plugins/zentao/tasks/bug_collector.go
index 6ecd44b..17b5675 100644
--- a/backend/plugins/zentao/tasks/bug_collector.go
+++ b/backend/plugins/zentao/tasks/bug_collector.go
@@ -32,23 +32,53 @@
 
 var _ plugin.SubTaskEntryPoint = CollectBug
 
+var CollectBugMeta = plugin.SubTaskMeta{
+	Name:             "collectBug",
+	EntryPoint:       CollectBug,
+	EnabledByDefault: true,
+	Description:      "Collect Bug data from Zentao api",
+	DomainTypes:      []string{plugin.DOMAIN_TYPE_TICKET},
+}
+
+type collectBugInput struct {
+	Path        string
+	ProjectId   int64
+	ProductId   int64
+	ExecutionId int64
+}
+
 func CollectBug(taskCtx plugin.SubTaskContext) errors.Error {
 	data := taskCtx.GetData().(*ZentaoTaskData)
-	cursor, iterator, err := getProductIterator(taskCtx)
+	// project bug iterator
+	projectBugsIter := newIteratorFromSlice([]interface{}{
+		collectBugInput{
+			ProjectId: data.Options.ProjectId,
+			Path:      fmt.Sprintf("/projects/%d", data.Options.ProjectId),
+		},
+	})
+	// execution bug iterator
+	executionCursor, executionIterator, err := getExecutionIterator(taskCtx)
 	if err != nil {
 		return err
 	}
-	defer cursor.Close()
+	defer executionCursor.Close()
+	executionBugIter := newIteratorWrapper(executionIterator, func(arg interface{}) interface{} {
+		return &collectBugInput{
+			ExecutionId: arg.(*input).Id,
+			Path:        fmt.Sprintf("/executions/%d", arg.(*input).Id),
+		}
+	})
+
 	collector, err := api.NewApiCollector(api.ApiCollectorArgs{
 		RawDataSubTaskArgs: api.RawDataSubTaskArgs{
 			Ctx:     taskCtx,
 			Options: data.Options,
 			Table:   RAW_BUG_TABLE,
 		},
-		Input:       iterator,
+		Input:       newIteratorConcator(projectBugsIter, executionBugIter),
 		ApiClient:   data.ApiClient,
 		PageSize:    100,
-		UrlTemplate: "/products/{{ .Input.Id }}/bugs",
+		UrlTemplate: "{{ .Input.Path }}/bugs",
 		Query: func(reqData *api.RequestData) (url.Values, errors.Error) {
 			query := url.Values{}
 			query.Set("page", fmt.Sprintf("%v", reqData.Pager.Page))
@@ -77,11 +107,3 @@
 
 	return collector.Execute()
 }
-
-var CollectBugMeta = plugin.SubTaskMeta{
-	Name:             "collectBug",
-	EntryPoint:       CollectBug,
-	EnabledByDefault: true,
-	Description:      "Collect Bug data from Zentao api",
-	DomainTypes:      []string{plugin.DOMAIN_TYPE_TICKET},
-}
diff --git a/backend/plugins/zentao/tasks/bug_repo_commits_extractor.go b/backend/plugins/zentao/tasks/bug_repo_commits_extractor.go
index 53b9215..7afbb4d 100644
--- a/backend/plugins/zentao/tasks/bug_repo_commits_extractor.go
+++ b/backend/plugins/zentao/tasks/bug_repo_commits_extractor.go
@@ -19,8 +19,6 @@
 
 import (
 	"encoding/json"
-	"regexp"
-
 	"github.com/apache/incubator-devlake/core/errors"
 	"github.com/apache/incubator-devlake/core/plugin"
 	"github.com/apache/incubator-devlake/helpers/pluginhelper/api"
@@ -39,9 +37,6 @@
 
 func ExtractBugRepoCommits(taskCtx plugin.SubTaskContext) errors.Error {
 	data := taskCtx.GetData().(*ZentaoTaskData)
-
-	re := regexp.MustCompile(`(\d+)(?:,\s*(\d+))*`)
-
 	extractor, err := api.NewApiExtractor(api.ApiExtractorArgs{
 		RawDataSubTaskArgs: api.RawDataSubTaskArgs{
 			Ctx:     taskCtx,
@@ -60,19 +55,20 @@
 				return nil, errors.Default.WrapRaw(err)
 			}
 			results := make([]interface{}, 0)
-			match := re.FindStringSubmatch(res.Log.Comment)
-			for i := 1; i < len(match); i++ {
-				if match[i] != "" {
-					bugRepoCommits := &models.ZentaoBugRepoCommit{
-						ConnectionId: data.Options.ConnectionId,
-						Product:      input.Product,
-						Project:      data.Options.ProjectId,
-						RepoUrl:      res.Repo.CodePath,
-						CommitSha:    res.Revision,
-						IssueId:      match[i], // bug id
-					}
-					results = append(results, bugRepoCommits)
+			issueIds, err := extractIdFromLogComment("bug", res.Log.Comment)
+			if err != nil {
+				return nil, errors.Default.Wrap(err, "extractIdFromLogComment")
+			}
+			for _, issueId := range issueIds {
+				bugRepoCommits := &models.ZentaoBugRepoCommit{
+					ConnectionId: data.Options.ConnectionId,
+					Product:      input.Product,
+					Project:      data.Options.ProjectId,
+					RepoUrl:      res.Repo.CodePath,
+					CommitSha:    res.Revision,
+					IssueId:      issueId,
 				}
+				results = append(results, bugRepoCommits)
 			}
 
 			return results, nil
diff --git a/backend/plugins/zentao/tasks/project_convertor.go b/backend/plugins/zentao/tasks/project_convertor.go
index 3f217a9..2c39285 100644
--- a/backend/plugins/zentao/tasks/project_convertor.go
+++ b/backend/plugins/zentao/tasks/project_convertor.go
@@ -19,6 +19,7 @@
 
 import (
 	"fmt"
+	"net/url"
 	"reflect"
 
 	"github.com/apache/incubator-devlake/core/dal"
@@ -46,6 +47,7 @@
 func ConvertProjects(taskCtx plugin.SubTaskContext) errors.Error {
 	data := taskCtx.GetData().(*ZentaoTaskData)
 	db := taskCtx.GetDal()
+	logger := taskCtx.GetLogger()
 	boardIdGen := didgen.NewDomainIdGenerator(&models.ZentaoProject{})
 	cursor, err := db.Cursor(
 		dal.From(&models.ZentaoProject{}),
@@ -55,6 +57,17 @@
 		return err
 	}
 	defer cursor.Close()
+	var protocol, host string
+	endpoint := data.ApiClient.ApiClient.GetEndpoint()
+	if endpoint != "" {
+		endpointURL, err := url.Parse(endpoint)
+		if err != nil {
+			logger.Error(err, "parse: %s", endpoint)
+		} else {
+			protocol = endpointURL.Scheme
+			host = endpointURL.Host
+		}
+	}
 	convertor, err := api.NewDataConverter(api.DataConverterArgs{
 		InputRowType: reflect.TypeOf(models.ZentaoProject{}),
 		Input:        cursor,
@@ -74,7 +87,7 @@
 				Description: toolProject.Description,
 				CreatedDate: toolProject.OpenedDate.ToNullableTime(),
 				Type:        "scrum",
-				Url:         fmt.Sprintf("/project-index-%d.html", data.Options.ProjectId),
+				Url:         fmt.Sprintf("%s://%s/project-index-%d.html", protocol, host, data.Options.ProjectId),
 			}
 			results := make([]interface{}, 0)
 			results = append(results, domainBoard)
diff --git a/backend/plugins/zentao/tasks/shared.go b/backend/plugins/zentao/tasks/shared.go
index c4fd10d..f02c7f8 100644
--- a/backend/plugins/zentao/tasks/shared.go
+++ b/backend/plugins/zentao/tasks/shared.go
@@ -23,6 +23,7 @@
 	"net/url"
 	"path"
 	"reflect"
+	"regexp"
 	"strings"
 
 	"github.com/apache/incubator-devlake/core/dal"
@@ -179,29 +180,29 @@
 	return nil
 }
 
-func getProductIterator(taskCtx plugin.SubTaskContext) (dal.Rows, *api.DalCursorIterator, errors.Error) {
-	data := taskCtx.GetData().(*ZentaoTaskData)
-	db := taskCtx.GetDal()
-	clauses := []dal.Clause{
-		dal.Select("id"),
-		dal.From(&models.ZentaoProductSummary{}),
-		dal.Where(
-			"project_id = ? AND connection_id = ?",
-			data.Options.ProjectId, data.Options.ConnectionId,
-		),
-	}
-
-	cursor, err := db.Cursor(clauses...)
-	if err != nil {
-		return nil, nil, err
-	}
-	iterator, err := api.NewDalCursorIterator(db, cursor, reflect.TypeOf(input{}))
-	if err != nil {
-		cursor.Close()
-		return nil, nil, err
-	}
-	return cursor, iterator, nil
-}
+//func getProductIterator(taskCtx plugin.SubTaskContext) (dal.Rows, *api.DalCursorIterator, errors.Error) {
+//	data := taskCtx.GetData().(*ZentaoTaskData)
+//	db := taskCtx.GetDal()
+//	clauses := []dal.Clause{
+//		dal.Select("id"),
+//		dal.From(&models.ZentaoProductSummary{}),
+//		dal.Where(
+//			"project_id = ? AND connection_id = ?",
+//			data.Options.ProjectId, data.Options.ConnectionId,
+//		),
+//	}
+//
+//	cursor, err := db.Cursor(clauses...)
+//	if err != nil {
+//		return nil, nil, err
+//	}
+//	iterator, err := api.NewDalCursorIterator(db, cursor, reflect.TypeOf(input{}))
+//	if err != nil {
+//		cursor.Close()
+//		return nil, nil, err
+//	}
+//	return cursor, iterator, nil
+//}
 
 func getExecutionIterator(taskCtx plugin.SubTaskContext) (dal.Rows, *api.DalCursorIterator, errors.Error) {
 	data := taskCtx.GetData().(*ZentaoTaskData)
@@ -305,3 +306,36 @@
 	u.Path = path.Join(before, fmt.Sprintf("/%s-view-%d.html", issueType, id))
 	return u.String()
 }
+
+func extractIdFromLogComment(logCommentType string, comment string) ([]string, error) {
+	if logCommentType != "task" && logCommentType != "bug" && logCommentType != "story" {
+		return nil, errors.Default.New(fmt.Sprintf("unsupportted log comment type: %s", logCommentType))
+	}
+	regexpStr := fmt.Sprintf("(%s-view-\\d+\\.json)+", logCommentType)
+	re := regexp.MustCompile(regexpStr)
+	results := re.FindAllString(comment, -1)
+	var ret []string
+
+	convertMatchedString := func(s string) string {
+		if s == "" {
+			return s
+		}
+		s = strings.Replace(s, "-", " ", -1)
+		s = strings.Replace(s, ".", " ", -1)
+		return s
+	}
+
+	for _, matched := range results {
+		var id string
+		format := fmt.Sprintf("%s view %%s json", logCommentType)
+		n, err := fmt.Sscanf(convertMatchedString(matched), format, &id)
+		if err != nil {
+			return nil, err
+		}
+		if n < 1 {
+			return nil, errors.Default.New("unexpected comment")
+		}
+		ret = append(ret, id)
+	}
+	return ret, nil
+}
diff --git a/backend/plugins/zentao/tasks/shared_test.go b/backend/plugins/zentao/tasks/shared_test.go
index bd9aacc..3dfc5d8 100644
--- a/backend/plugins/zentao/tasks/shared_test.go
+++ b/backend/plugins/zentao/tasks/shared_test.go
@@ -17,7 +17,10 @@
 
 package tasks
 
-import "testing"
+import (
+	"reflect"
+	"testing"
+)
 
 func Test_convertIssueURL(t *testing.T) {
 	type args struct {
@@ -69,3 +72,83 @@
 		})
 	}
 }
+
+func Test_extractIdFromLogComment(t *testing.T) {
+	type args struct {
+		logCommentType string
+		comment        string
+	}
+	tests := []struct {
+		name    string
+		args    args
+		want    []string
+		wantErr bool
+	}{
+		{
+			name: "common-1",
+			args: args{
+				logCommentType: "something-wrong",
+				comment:        "random strings",
+			},
+			want:    nil,
+			wantErr: true,
+		},
+		{
+			name: "story-1",
+			args: args{
+				logCommentType: "story",
+				comment:        "story #<a href='\\/story-view-4590.json'  >4590<\\/a>\\n,<a href='\\/story-view-4572.json'  >4572<\\/a>\\n",
+			},
+			want:    []string{"4590", "4572"},
+			wantErr: false,
+		},
+		{
+			name: "story-2",
+			args: args{
+				logCommentType: "story",
+				comment:        "story #<a href='\\/story-view-4572.json'  >4572<\\/a>\\n,<a href='\\/story-view-4591.json'  >4591<\\/a>\\n \\u6d4b\\u8bd5\\u4e24\\u4e2a\\u5173\\u8054\\u5173\\u7cfb\\u662f\\u5426\\u90fd\\u5199\\u8fdbissuerepocommit\\u8868",
+			},
+			want:    []string{"4572", "4591"},
+			wantErr: false,
+		},
+		{
+			name: "story-3",
+			args: args{
+				logCommentType: "story",
+				comment:        "story #<a href='\\/story-view-4590.json'  >4590<\\/a>",
+			},
+			want:    []string{"4590"},
+			wantErr: false,
+		},
+		{
+			name: "bug-1",
+			args: args{
+				logCommentType: "bug",
+				comment:        "\"bug #<a href='\\/bug-view-6119.json'  >6119<\\/a>\\n,<a href='\\/bug-view-6118.json'  >6118<\\/a>\\n,<a href='\\/bug-view-6117.json'  >6117<\\/a>\\n,<a href='\\/bug-view-6121.json'  >6121<\\/a>\\n",
+			},
+			want:    []string{"6119", "6118", "6117", "6121"},
+			wantErr: false,
+		},
+		{
+			name: "task-1",
+			args: args{
+				logCommentType: "task",
+				comment:        "task #<a href='\\/task-view-004.json'  >004<\\/a>\\n\\uff0c\\u7985\\u9053\\u4efb\\u52a1\\u6d4b\\u8bd5",
+			},
+			want:    []string{"004"},
+			wantErr: false,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			got, err := extractIdFromLogComment(tt.args.logCommentType, tt.args.comment)
+			if (err != nil) != tt.wantErr {
+				t.Errorf("extractIdFromLogComment() error = %v, wantErr %v", err, tt.wantErr)
+				return
+			}
+			if !reflect.DeepEqual(got, tt.want) {
+				t.Errorf("extractIdFromLogComment() got = %v, want %v", got, tt.want)
+			}
+		})
+	}
+}
diff --git a/backend/plugins/zentao/tasks/story_convertor.go b/backend/plugins/zentao/tasks/story_convertor.go
index 3d32d91..d0d67e6 100644
--- a/backend/plugins/zentao/tasks/story_convertor.go
+++ b/backend/plugins/zentao/tasks/story_convertor.go
@@ -90,6 +90,7 @@
 				OriginalProject:         getOriginalProject(data),
 				Status:                  toolEntity.StdStatus,
 				OriginalEstimateMinutes: int64(toolEntity.Estimate) * 60,
+				StoryPoint:              toolEntity.Estimate,
 			}
 			if mappingType, ok := stdTypeMappings[domainEntity.OriginalType]; ok && mappingType != "" {
 				domainEntity.Type = mappingType
diff --git a/backend/plugins/zentao/tasks/story_repo_commits_extractor.go b/backend/plugins/zentao/tasks/story_repo_commits_extractor.go
index 3176d8e..7e1b69d 100644
--- a/backend/plugins/zentao/tasks/story_repo_commits_extractor.go
+++ b/backend/plugins/zentao/tasks/story_repo_commits_extractor.go
@@ -19,8 +19,6 @@
 
 import (
 	"encoding/json"
-	"regexp"
-
 	"github.com/apache/incubator-devlake/core/errors"
 	"github.com/apache/incubator-devlake/core/plugin"
 	"github.com/apache/incubator-devlake/helpers/pluginhelper/api"
@@ -39,8 +37,6 @@
 
 func ExtractStoryRepoCommits(taskCtx plugin.SubTaskContext) errors.Error {
 	data := taskCtx.GetData().(*ZentaoTaskData)
-
-	re := regexp.MustCompile(`(\d+)(?:,\s*(\d+))*`)
 	extractor, err := api.NewApiExtractor(api.ApiExtractorArgs{
 		RawDataSubTaskArgs: api.RawDataSubTaskArgs{
 			Ctx:     taskCtx,
@@ -55,18 +51,19 @@
 			}
 
 			results := make([]interface{}, 0)
-			match := re.FindStringSubmatch(res.Log.Comment)
-			for i := 1; i < len(match); i++ {
-				if match[i] != "" {
-					storyRepoCommits := &models.ZentaoStoryRepoCommit{
-						ConnectionId: data.Options.ConnectionId,
-						Project:      data.Options.ProjectId,
-						RepoUrl:      res.Repo.CodePath,
-						CommitSha:    res.Revision,
-						IssueId:      match[i], // story id
-					}
-					results = append(results, storyRepoCommits)
+			issueIds, err := extractIdFromLogComment("story", res.Log.Comment)
+			if err != nil {
+				return nil, errors.Default.Wrap(err, "extractIdFromLogComment")
+			}
+			for _, issueId := range issueIds {
+				storyRepoCommits := &models.ZentaoStoryRepoCommit{
+					ConnectionId: data.Options.ConnectionId,
+					Project:      data.Options.ProjectId,
+					RepoUrl:      res.Repo.CodePath,
+					CommitSha:    res.Revision,
+					IssueId:      issueId,
 				}
+				results = append(results, storyRepoCommits)
 			}
 
 			return results, nil
diff --git a/backend/plugins/zentao/tasks/task_repo_commits_extractor.go b/backend/plugins/zentao/tasks/task_repo_commits_extractor.go
index e601abe..9174239 100644
--- a/backend/plugins/zentao/tasks/task_repo_commits_extractor.go
+++ b/backend/plugins/zentao/tasks/task_repo_commits_extractor.go
@@ -19,8 +19,6 @@
 
 import (
 	"encoding/json"
-	"regexp"
-
 	"github.com/apache/incubator-devlake/core/errors"
 	"github.com/apache/incubator-devlake/core/plugin"
 	"github.com/apache/incubator-devlake/helpers/pluginhelper/api"
@@ -39,8 +37,6 @@
 
 func ExtractTaskRepoCommits(taskCtx plugin.SubTaskContext) errors.Error {
 	data := taskCtx.GetData().(*ZentaoTaskData)
-
-	re := regexp.MustCompile(`(\d+)(?:,\s*(\d+))*`)
 	extractor, err := api.NewApiExtractor(api.ApiExtractorArgs{
 		RawDataSubTaskArgs: api.RawDataSubTaskArgs{
 			Ctx:     taskCtx,
@@ -55,18 +51,19 @@
 			}
 
 			results := make([]interface{}, 0)
-			match := re.FindStringSubmatch(res.Log.Comment)
-			for i := 1; i < len(match); i++ {
-				if match[i] != "" {
-					taskRepoCommits := &models.ZentaoTaskRepoCommit{
-						ConnectionId: data.Options.ConnectionId,
-						Project:      data.Options.ProjectId,
-						RepoUrl:      res.Repo.CodePath,
-						CommitSha:    res.Revision,
-						IssueId:      match[i], // task id
-					}
-					results = append(results, taskRepoCommits)
+			issueIds, err := extractIdFromLogComment("task", res.Log.Comment)
+			if err != nil {
+				return nil, errors.Default.Wrap(err, "extractIdFromLogComment")
+			}
+			for _, issueId := range issueIds {
+				taskRepoCommits := &models.ZentaoTaskRepoCommit{
+					ConnectionId: data.Options.ConnectionId,
+					Project:      data.Options.ProjectId,
+					RepoUrl:      res.Repo.CodePath,
+					CommitSha:    res.Revision,
+					IssueId:      issueId,
 				}
+				results = append(results, taskRepoCommits)
 			}
 
 			return results, nil