You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@devlake.apache.org by wa...@apache.org on 2022/11/19 03:32:23 UTC

[incubator-devlake] branch feat-plugin-zentao updated: Feat update zentao (#3763)

This is an automated email from the ASF dual-hosted git repository.

warren pushed a commit to branch feat-plugin-zentao
in repository https://gitbox.apache.org/repos/asf/incubator-devlake.git


The following commit(s) were added to refs/heads/feat-plugin-zentao by this push:
     new 626bcb054 Feat update zentao (#3763)
626bcb054 is described below

commit 626bcb0547d64a04c071ccdbabfa429e9ef5b680
Author: Warren Chen <yi...@merico.dev>
AuthorDate: Sat Nov 19 11:32:20 2022 +0800

    Feat update zentao (#3763)
    
    * feat(zentao): create new plugin
    
    Relate to #2961
    
    * feat(zentao): create new plugin
    
    Relate to #2961
    
    * fix(zentao): update connection
    
    * feat(zentao): update project collector
    
    * feat(zentao):add zentao project extractor
    
    * fix(zentao): fix minor issues
    
    * feat(zentao): add execution
    
    * feat(zentao): fix execution time
    
    * feat(zentao): add zentao stories
    
    * fix(zentao):apiurl&model error
    
    * fix(zentao):restore code
    
    * feat(zentao):add zentao bugs
    
    * fix(zentao): rebase
    
    * feat(zentao):add zentao task and change stories to story
    
    * feat(zentao): finish the initial version
    
    * fix(zentao): update lint
    
    * feat(zentao): add config-ui support
    
    Co-authored-by: yuqiangabab <11...@qq.com>
---
 .../blueprints/ProviderTransformationSettings.jsx  |   2 +
 .../create-workflow/DataTransformations.jsx        |   2 +
 .../src/components/menus/PipelineConfigsMenu.jsx   |  10 +
 .../src/components/pipelines/StageTaskName.jsx     |   1 +
 config-ui/src/data/Providers.js                    |  29 ++-
 config-ui/src/data/availablePlugins.js             |   2 +-
 config-ui/src/data/integrations.jsx                |  22 ++
 .../zentao.js}                                     |  16 +-
 config-ui/src/hooks/useConnectionValidation.jsx    |   1 +
 config-ui/src/hooks/useIntegrations.jsx            |   2 +
 config-ui/src/images/integrations/zentao.svg       |  80 ++++++
 .../src/pages/blueprints/blueprint-settings.jsx    |   1 +
 .../configure/connections/ConfigureConnection.jsx  |   3 +-
 .../pages/configure/connections/ConnectionForm.jsx |   1 +
 config-ui/src/pages/configure/settings/zentao.jsx  |  57 +++++
 config-ui/src/registry/plugins/zentao.json         |  55 +++++
 plugins/helper/iso8601time.go                      |  31 +++
 plugins/zentao/api/blueprint.go                    |  70 ++++++
 plugins/zentao/api/connection.go                   | 148 +++++++++++
 plugins/zentao/api/init.go                         |  39 +++
 plugins/zentao/e2e/bug_test.go                     |  69 ++++++
 plugins/zentao/e2e/execution_test.go               |  63 +++++
 plugins/zentao/e2e/product_test.go                 |  63 +++++
 .../zentao/e2e/raw_tables/_raw_zentao_api_bugs.csv |   5 +
 .../e2e/raw_tables/_raw_zentao_api_executions.csv  |   2 +
 .../e2e/raw_tables/_raw_zentao_api_products.csv    |   2 +
 .../e2e/raw_tables/_raw_zentao_api_stories.csv     |   8 +
 .../e2e/raw_tables/_raw_zentao_api_tasks.csv       |   4 +
 .../e2e/snapshot_tables/_tool_zentao_bugs.csv      |  13 +
 .../snapshot_tables/_tool_zentao_executions.csv    |   2 +
 .../e2e/snapshot_tables/_tool_zentao_products.csv  |   2 +
 .../e2e/snapshot_tables/_tool_zentao_stories.csv   |   8 +
 .../e2e/snapshot_tables/_tool_zentao_tasks.csv     |   4 +
 .../e2e/snapshot_tables/board_issues_bug.csv       |   5 +
 .../e2e/snapshot_tables/board_issues_story.csv     |   8 +
 .../e2e/snapshot_tables/board_issues_task.csv      |   1 +
 .../e2e/snapshot_tables/boards_execution.csv       |   2 +
 .../zentao/e2e/snapshot_tables/boards_product.csv  |   2 +
 plugins/zentao/e2e/snapshot_tables/issues_bug.csv  |   5 +
 .../zentao/e2e/snapshot_tables/issues_story.csv    |   8 +
 plugins/zentao/e2e/snapshot_tables/issues_task.csv |   1 +
 plugins/zentao/e2e/story_test.go                   |  69 ++++++
 plugins/zentao/e2e/task_test.go                    |  68 +++++
 plugins/zentao/impl/impl.go                        | 136 ++++++++++
 plugins/zentao/models/access_token.go              |  27 ++
 plugins/zentao/models/archived/bug.go              |  99 ++++++++
 plugins/zentao/models/archived/connection.go       |  70 ++++++
 plugins/zentao/models/archived/execution.go        |  90 +++++++
 plugins/zentao/models/archived/product.go          |  62 +++++
 plugins/zentao/models/archived/project.go          | 113 +++++++++
 plugins/zentao/models/archived/story.go            |  89 +++++++
 plugins/zentao/models/archived/task.go             |  97 ++++++++
 plugins/zentao/models/bug.go                       | 192 +++++++++++++++
 plugins/zentao/models/connection.go                |  51 ++++
 plugins/zentao/models/execution.go                 | 274 +++++++++++++++++++++
 .../migrationscripts/20220906_add_init_tables.go   |  48 ++++
 plugins/zentao/models/migrationscripts/register.go |  29 +++
 plugins/zentao/models/product.go                   | 126 ++++++++++
 plugins/zentao/models/project.go                   | 113 +++++++++
 plugins/zentao/models/story.go                     | 169 +++++++++++++
 plugins/zentao/models/task.go                      | 201 +++++++++++++++
 plugins/zentao/tasks/api_client.go                 |  95 +++++++
 plugins/zentao/tasks/bug_collector.go              |  82 ++++++
 plugins/zentao/tasks/bug_convertor.go              | 116 +++++++++
 plugins/zentao/tasks/bug_extractor.go              | 138 +++++++++++
 plugins/zentao/tasks/execution_collector.go        |  81 ++++++
 plugins/zentao/tasks/execution_convertor.go        |  93 +++++++
 plugins/zentao/tasks/execution_extractor.go        | 128 ++++++++++
 plugins/zentao/tasks/product_collector.go          |  80 ++++++
 plugins/zentao/tasks/product_convertor.go          |  92 +++++++
 plugins/zentao/tasks/product_extractor.go          | 101 ++++++++
 plugins/zentao/tasks/project_collector.go          |  79 ++++++
 plugins/zentao/tasks/project_extractor.go          |  69 ++++++
 plugins/zentao/tasks/shared.go                     |  35 +++
 plugins/zentao/tasks/story_collector.go            |  83 +++++++
 plugins/zentao/tasks/story_convertor.go            | 115 +++++++++
 plugins/zentao/tasks/story_extractor.go            | 125 ++++++++++
 plugins/zentao/tasks/task_collector.go             |  82 ++++++
 plugins/zentao/tasks/task_convertor.go             | 117 +++++++++
 plugins/zentao/tasks/task_data.go                  |  62 +++++
 plugins/zentao/tasks/task_extractor.go             | 139 +++++++++++
 plugins/zentao/zentao.go                           |  47 ++++
 82 files changed, 4926 insertions(+), 5 deletions(-)

diff --git a/config-ui/src/components/blueprints/ProviderTransformationSettings.jsx b/config-ui/src/components/blueprints/ProviderTransformationSettings.jsx
index 22237cf1e..cefd7855c 100644
--- a/config-ui/src/components/blueprints/ProviderTransformationSettings.jsx
+++ b/config-ui/src/components/blueprints/ProviderTransformationSettings.jsx
@@ -25,6 +25,7 @@ import GithubSettings from '@/pages/configure/settings/github'
 import AzureSettings from '@/pages/configure/settings/azure'
 import BitbucketSettings from '@/pages/configure/settings/bitbucket'
 import GiteeSettings from '@/pages/configure/settings/gitee'
+import ZentaoSettings from '@/pages/configure/settings/zentao'
 
 // Transformation Higher-Order Component (HOC) Settings Loader
 const withTransformationSettings = (
@@ -70,6 +71,7 @@ const ProviderTransformationSettings = (props) => {
       [Providers.JIRA]: JiraSettings,
       [Providers.JENKINS]: JenkinsSettings,
       [Providers.TAPD]: TapdSettings,
+      [Providers.ZENTAO]: ZentaoSettings,
       [Providers.AZURE]: AzureSettings,
       [Providers.BITBUCKET]: BitbucketSettings,
       [Providers.GITEE]: GiteeSettings
diff --git a/config-ui/src/components/blueprints/create-workflow/DataTransformations.jsx b/config-ui/src/components/blueprints/create-workflow/DataTransformations.jsx
index be956819f..f00eae0d6 100644
--- a/config-ui/src/components/blueprints/create-workflow/DataTransformations.jsx
+++ b/config-ui/src/components/blueprints/create-workflow/DataTransformations.jsx
@@ -77,6 +77,7 @@ const DataTransformations = (props) => {
   const noTransformationsAvailable = useMemo(
     () =>
       [Providers.TAPD].includes(configuredConnection?.provider) ||
+      [Providers.ZENTAO].includes(configuredConnection?.provider) ||
       ([Providers.GITLAB].includes(configuredConnection?.provider) &&
         dataDomainsGroup[configuredConnection?.id].every(
           (e) => e.value !== DataDomainTypes.DEVOPS
@@ -86,6 +87,7 @@ const DataTransformations = (props) => {
       configuredConnection?.id,
       dataDomainsGroup,
       Providers.TAPD,
+      Providers.ZENTAO,
       Providers.GITLAB
     ]
   )
diff --git a/config-ui/src/components/menus/PipelineConfigsMenu.jsx b/config-ui/src/components/menus/PipelineConfigsMenu.jsx
index d45a4312e..930912b54 100644
--- a/config-ui/src/components/menus/PipelineConfigsMenu.jsx
+++ b/config-ui/src/components/menus/PipelineConfigsMenu.jsx
@@ -23,6 +23,7 @@ import { gitextractorConfig as sampleGitextractorPipelineConfig } from '@/data/p
 import { githubConfig as sampleGithubPipelineConfig } from '@/data/pipeline-config-samples/github'
 import { gitlabConfig as sampleGitlabPipelineConfig } from '@/data/pipeline-config-samples/gitlab'
 import { jiraConfig as sampleJiraPipelineConfig } from '@/data/pipeline-config-samples/jira'
+import { zentaoConfig as sampleZentaoPipelineConfig } from '@/data/pipeline-config-samples/zentao'
 import { jenkinsConfig as sampleJenkinsPipelineConfig } from '@/data/pipeline-config-samples/jenkins'
 import { feishuConfig as sampleFeishuPipelineConfig } from '@/data/pipeline-config-samples/feishu'
 import { dbtConfig as sampleDbtPipelineConfig } from '@/data/pipeline-config-samples/dbt'
@@ -137,6 +138,15 @@ const PipelineConfigsMenu = (props) => {
           )
         }
       />
+      <Menu.Item
+        icon='code'
+        text='Load Zentao Configuration'
+        onClick={() =>
+          setRawConfiguration(
+            JSON.stringify(sampleZentaoPipelineConfig, null, '  ')
+          )
+        }
+      />
     </Menu>
   )
 }
diff --git a/config-ui/src/components/pipelines/StageTaskName.jsx b/config-ui/src/components/pipelines/StageTaskName.jsx
index b4b044ce2..eeb7c4993 100644
--- a/config-ui/src/components/pipelines/StageTaskName.jsx
+++ b/config-ui/src/components/pipelines/StageTaskName.jsx
@@ -120,6 +120,7 @@ const StageTaskName = (props) => {
                     <>{ProviderLabels.JENKINS}</>
                   )}
                   {task.plugin === Providers.TAPD && <>{ProviderLabels.TAPD}</>}
+                  {task.plugin === Providers.ZENTAO && <>{ProviderLabels.ZENTAO}</>}
                   {task.plugin === Providers.JIRA && (
                     <>Board ID {task.options.boardId}</>
                   )}
diff --git a/config-ui/src/data/Providers.js b/config-ui/src/data/Providers.js
index 1a4d4f979..eb4c63134 100644
--- a/config-ui/src/data/Providers.js
+++ b/config-ui/src/data/Providers.js
@@ -22,6 +22,7 @@ import { ReactComponent as JenkinsProviderIcon } from '@/images/integrations/jen
 import { ReactComponent as JiraProviderIcon } from '@/images/integrations/jira.svg'
 import { ReactComponent as GitHubProviderIcon } from '@/images/integrations/github.svg'
 import { ReactComponent as TapdProviderIcon } from '@/images/integrations/tapd.svg'
+import { ReactComponent as ZentaoProviderIcon } from '@/images/integrations/zentao.svg'
 import { ReactComponent as AzureProviderIcon } from '@/images/integrations/azure.svg'
 import { ReactComponent as BitbucketProviderIcon } from '@/images/integrations/bitbucket.svg'
 import { ReactComponent as GiteeProviderIcon } from '@/images/integrations/gitee.svg'
@@ -50,6 +51,7 @@ const Providers = {
   DBT: 'dbt',
   STARROCKS: 'starrocks',
   TAPD: 'tapd',
+  ZENTAO: 'zentao',
   AZURE: 'azure',
   BITBUCKET: 'bitbucket',
   GITEE: 'gitee',
@@ -76,7 +78,8 @@ const ProviderLabels = {
   AE: 'Analysis Engine (AE)',
   DBT: 'Data Build Tool (DBT)',
   STARROCKS: 'StarRocks',
-  TAPD: 'TAPD',
+  TAPD: 'Tapd',
+  ZENTAO: 'Zentao',
   AZURE: 'Azure CI',
   BITBUCKET: 'BitBucket',
   GITEE: 'Gitee',
@@ -146,6 +149,19 @@ const ProviderFormLabels = {
       </>
     )
   },
+  zentao: {
+    name: 'Connection Name',
+    endpoint: 'Endpoint URL',
+    proxy: 'Proxy URL',
+    username: 'Username',
+    password: 'Password',
+    rateLimitPerHour: (
+      <>
+        Rate Limit <sup>(per hour)</sup>
+        <RateLimitTooltip />
+      </>
+    )
+  },
   jira: {
     name: 'Connection Name',
     endpoint: 'Endpoint URL',
@@ -308,6 +324,14 @@ const ProviderFormPlaceholders = {
     password: 'eg. ************',
     rateLimitPerHour: '1000'
   },
+  zentao: {
+    name: 'eg. Zentao',
+    endpoint: 'URL eg. http://subdomain.domain:port/api.php/v1/',
+    proxy: 'eg. http://proxy.localhost:8080',
+    username: 'eg. devlake',
+    password: 'eg. ************',
+    rateLimitPerHour: '1000'
+  },
   jira: {
     name: 'eg. JIRA',
     endpoint: 'eg. https://your-domain.atlassian.net/rest/',
@@ -366,6 +390,9 @@ const ProviderIcons = {
   [Providers.TAPD]: (w, h) => (
     <TapdProviderIcon width={w || 24} height={h || 24} />
   ),
+  [Providers.ZENTAO]: (w, h) => (
+    <ZentaoProviderIcon width={w || 24} height={h || 24} />
+  ),
   [Providers.JIRA]: (w, h) => (
     <JiraProviderIcon width={w || 24} height={h || 24} />
   ),
diff --git a/config-ui/src/data/availablePlugins.js b/config-ui/src/data/availablePlugins.js
index 4433f79cb..dc5f79d2d 100644
--- a/config-ui/src/data/availablePlugins.js
+++ b/config-ui/src/data/availablePlugins.js
@@ -15,6 +15,6 @@
  * limitations under the License.
  *
  */
-const AVAILABLE_PLUGINS = ['gitlab', 'jira', 'jenkins', 'github', 'tapd']
+const AVAILABLE_PLUGINS = ['gitlab', 'jira', 'jenkins', 'github', 'tapd', 'zentao']
 
 module.exports = AVAILABLE_PLUGINS
diff --git a/config-ui/src/data/integrations.jsx b/config-ui/src/data/integrations.jsx
index 1a10f7aa0..b5f0165c3 100644
--- a/config-ui/src/data/integrations.jsx
+++ b/config-ui/src/data/integrations.jsx
@@ -30,6 +30,7 @@ import { ReactComponent as JenkinsProvider } from '@/images/integrations/jenkins
 import { ReactComponent as JiraProvider } from '@/images/integrations/jira.svg'
 import { ReactComponent as GitHubProvider } from '@/images/integrations/github.svg'
 import { ReactComponent as TapdProvider } from '@/images/integrations/tapd.svg'
+import { ReactComponent as ZentaoProvider } from '@/images/integrations/zentao.svg'
 import { ReactComponent as AzureProvider } from '@/images/integrations/azure.svg'
 import { ReactComponent as BitbucketProvider } from '@/images/integrations/bitbucket.svg'
 import { ReactComponent as GiteeProvider } from '@/images/integrations/gitee.svg'
@@ -102,6 +103,27 @@ const integrationsData = [
     // relocated to ProviderTransformationSettings since v0.12.0
     settings: {}
   },
+  {
+    id: Providers.ZENTAO,
+    type: ProviderTypes.INTEGRATION,
+    enabled: true,
+    multiConnection: true,
+    isBeta: true,
+    name: ProviderLabels.ZENTAO,
+    icon: (
+      <ZentaoProvider
+        className='providerIconSvg'
+        width='30'
+        height='30'
+        style={{ float: 'left', marginTop: '5px' }}
+      />
+    ),
+    iconDashboard: (
+      <ZentaoProvider className='providerIconSvg' width='40' height='40' />
+    ),
+    // relocated to ProviderTransformationSettings since v0.12.0
+    settings: {}
+  },
   {
     id: Providers.JIRA,
     type: ProviderTypes.INTEGRATION,
diff --git a/config-ui/src/data/availablePlugins.js b/config-ui/src/data/pipeline-config-samples/zentao.js
similarity index 79%
copy from config-ui/src/data/availablePlugins.js
copy to config-ui/src/data/pipeline-config-samples/zentao.js
index 4433f79cb..22590ee72 100644
--- a/config-ui/src/data/availablePlugins.js
+++ b/config-ui/src/data/pipeline-config-samples/zentao.js
@@ -15,6 +15,18 @@
  * limitations under the License.
  *
  */
-const AVAILABLE_PLUGINS = ['gitlab', 'jira', 'jenkins', 'github', 'tapd']
+const zentaoConfig = [
+  [
+    {
+      plugin: 'zentao',
+      options: {
+        connectionId: 1,
+        productId: 1,
+        projectId: 1,
+        executionId: 1,
+      }
+    }
+  ]
+]
 
-module.exports = AVAILABLE_PLUGINS
+export { zentaoConfig }
diff --git a/config-ui/src/hooks/useConnectionValidation.jsx b/config-ui/src/hooks/useConnectionValidation.jsx
index fbf109a67..5596c6634 100644
--- a/config-ui/src/hooks/useConnectionValidation.jsx
+++ b/config-ui/src/hooks/useConnectionValidation.jsx
@@ -96,6 +96,7 @@ function useConnectionValidation({
         }
         break
       case Providers.JIRA:
+      case Providers.ZENTAO:
       case Providers.JENKINS:
         if (!username || username.length <= 2) {
           errs.push('Username is required')
diff --git a/config-ui/src/hooks/useIntegrations.jsx b/config-ui/src/hooks/useIntegrations.jsx
index a3c5f8840..19f95621a 100644
--- a/config-ui/src/hooks/useIntegrations.jsx
+++ b/config-ui/src/hooks/useIntegrations.jsx
@@ -26,6 +26,7 @@ import GitHubPlugin from '@/registry/plugins/github.json'
 import GitLabPlugin from '@/registry/plugins/gitlab.json'
 import JenkinsPlugin from '@/registry/plugins/jenkins.json'
 import TapdPlugin from '@/registry/plugins/tapd.json'
+import ZentaoPlugin from '@/registry/plugins/zentao.json'
 import AzurePlugin from '@/registry/plugins/azure.json'
 import BitbucketPlugin from '@/registry/plugins/bitbucket.json'
 import GiteePlugin from '@/registry/plugins/gitee.json'
@@ -49,6 +50,7 @@ function useIntegrations(
     GitLabPlugin,
     JenkinsPlugin,
     TapdPlugin,
+    ZentaoPlugin,
     AzurePlugin,
     BitbucketPlugin,
     GiteePlugin,
diff --git a/config-ui/src/images/integrations/zentao.svg b/config-ui/src/images/integrations/zentao.svg
new file mode 100644
index 000000000..9dab076a3
--- /dev/null
+++ b/config-ui/src/images/integrations/zentao.svg
@@ -0,0 +1,80 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Generator: Adobe Illustrator 24.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0)  -->
+
+<svg
+   version="1.1"
+   id="图层_1"
+   x="0px"
+   y="0px"
+   viewBox="0 0 189.83999 189.84"
+   xml:space="preserve"
+   sodipodi:docname="zentao.svg"
+   width="189.84"
+   height="189.84"
+   inkscape:version="1.2 (dc2aedaf03, 2022-05-15)"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:svg="http://www.w3.org/2000/svg"><defs
+   id="defs156" /><sodipodi:namedview
+   id="namedview154"
+   pagecolor="#ffffff"
+   bordercolor="#111111"
+   borderopacity="1"
+   inkscape:showpageshadow="0"
+   inkscape:pageopacity="0"
+   inkscape:pagecheckerboard="1"
+   inkscape:deskcolor="#d1d1d1"
+   showgrid="false"
+   inkscape:zoom="3.1447385"
+   inkscape:cx="289.69023"
+   inkscape:cy="95.079449"
+   inkscape:window-width="3840"
+   inkscape:window-height="2050"
+   inkscape:window-x="-12"
+   inkscape:window-y="-12"
+   inkscape:window-maximized="1"
+   inkscape:current-layer="g151" />
+<style
+   type="text/css"
+   id="style80">
+	.st0{fill:#888988;}
+	.st1{fill:#221714;}
+	.st2{fill-rule:evenodd;clip-rule:evenodd;fill:#221714;}
+	.st3{fill:url(#SVGID_1_);}
+	.st4{fill-rule:evenodd;clip-rule:evenodd;fill:#FFFFFF;}
+</style>
+<g
+   id="g151"
+   transform="translate(-7.92,-5.74)">
+	
+	<g
+   id="g149">
+		<linearGradient
+   id="SVGID_1_"
+   gradientUnits="userSpaceOnUse"
+   x1="102.8407"
+   y1="195.5705"
+   x2="102.8407"
+   y2="5.7350001">
+			<stop
+   offset="0"
+   style="stop-color:#2975FE"
+   id="stop140" />
+			<stop
+   offset="1"
+   style="stop-color:#0CD5FF"
+   id="stop142" />
+		</linearGradient>
+		<path
+   class="st3"
+   d="m 102.84,5.74 c -52.42,0 -94.92,42.5 -94.92,94.92 0,52.42 42.5,94.92 94.92,94.92 52.43,0 94.92,-42.49 94.92,-94.92 0,-52.43 -42.49,-94.92 -94.92,-94.92 z"
+   id="path145"
+   style="fill:url(#SVGID_1_)" />
+		<path
+   class="st4"
+   d="m 174.02,47.18 c 0,0 15.56,23.18 8.66,42.87 -4.63,13.22 -21.42,17.86 -34.85,13.59 C 126.97,97 101.09,74.82 72.82,95.21 c -23.9,17.25 -13.94,47.33 3.13,57.35 12.79,7.51 30.62,9.86 41.04,0.99 8.35,-7.12 12.9,-25.67 -1.17,-32.03 -4.8,-2.17 -14.24,-2.65 -19.44,3.41 -6.79,7.92 -4.21,18.99 5.19,22.5 0,0 -16.8,-0.84 -19.03,-18.22 -3.8,-29.57 34.05,-34.45 52,-23.36 32.28,19.94 20.83,65.12 -27.19,73.35 -24.33,4.16 -64.53,-4.21 -80.46,-36.76 -5.97,-12.2 -4.34,-10.11 -4.34,-10.11 0,0 2.12,2.3 [...]
+   id="path147" />
+	</g>
+</g>
+</svg>
diff --git a/config-ui/src/pages/blueprints/blueprint-settings.jsx b/config-ui/src/pages/blueprints/blueprint-settings.jsx
index 0f9e3e728..e3e322fb6 100644
--- a/config-ui/src/pages/blueprints/blueprint-settings.jsx
+++ b/config-ui/src/pages/blueprints/blueprint-settings.jsx
@@ -515,6 +515,7 @@ const BlueprintSettings = (props) => {
                 dataDomainsGroup[configuredConnection?.id]?.length > 0
               break
             case Providers.TAPD:
+            case Providers.ZENTAO:
               isValid = dataDomainsGroup[configuredConnection?.id]?.length > 0
               break
             default:
diff --git a/config-ui/src/pages/configure/connections/ConfigureConnection.jsx b/config-ui/src/pages/configure/connections/ConfigureConnection.jsx
index 197e04a88..73ca3ee53 100644
--- a/config-ui/src/pages/configure/connections/ConfigureConnection.jsx
+++ b/config-ui/src/pages/configure/connections/ConfigureConnection.jsx
@@ -273,7 +273,8 @@ export default function ConfigureConnection() {
                         {[
                           Providers.GITLAB,
                           Providers.JIRA,
-                          Providers.TAPD
+                          Providers.TAPD,
+                          Providers.ZENTAO
                         ].includes(activeProvider?.id) && (
                           <h2 style={{ margin: 0 }}>
                             #{activeConnection?.id} {activeConnection.name}
diff --git a/config-ui/src/pages/configure/connections/ConnectionForm.jsx b/config-ui/src/pages/configure/connections/ConnectionForm.jsx
index deb7eab04..fcd2dab72 100644
--- a/config-ui/src/pages/configure/connections/ConnectionForm.jsx
+++ b/config-ui/src/pages/configure/connections/ConnectionForm.jsx
@@ -803,6 +803,7 @@ export default function ConnectionForm(props) {
           Providers.JIRA,
           Providers.JENKINS,
           Providers.TAPD,
+          Providers.ZENTAO,
           Providers.AZURE,
           Providers.BITBUCKET,
           Providers.GITEE
diff --git a/config-ui/src/pages/configure/settings/zentao.jsx b/config-ui/src/pages/configure/settings/zentao.jsx
new file mode 100644
index 000000000..96de85a1f
--- /dev/null
+++ b/config-ui/src/pages/configure/settings/zentao.jsx
@@ -0,0 +1,57 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+import React, { useEffect, useState } from 'react'
+import { useParams, useHistory } from 'react-router-dom'
+import { DataDomainTypes } from '@/data/DataDomains'
+
+import '@/styles/integration.scss'
+import '@/styles/connections.scss'
+
+export default function ZentaoSettings(props) {
+  const {
+    provider,
+    connection,
+    dataDomains = [],
+    onSettingsChange = () => {}
+  } = props
+  const history = useHistory()
+
+  const cancel = () => {
+    history.push(`/integrations/${provider.id}`)
+  }
+
+  return (
+    <>
+      <div className='headlineContainer'>
+        <h3 className='headline'>No Additional Settings</h3>
+        <p className='description'>
+          This integration doesn’t require any configuration. You can continue
+          to&nbsp;
+          <a href='#' style={{ textDecoration: 'underline' }} onClick={cancel}>
+            add other data connections
+          </a>
+          &nbsp; or trigger collection at the{' '}
+          <a href='#' style={{ textDecoration: 'underline' }} onClick={cancel}>
+            previous page
+          </a>
+          .
+        </p>
+      </div>
+    </>
+  )
+}
diff --git a/config-ui/src/registry/plugins/zentao.json b/config-ui/src/registry/plugins/zentao.json
new file mode 100644
index 000000000..b89a893fb
--- /dev/null
+++ b/config-ui/src/registry/plugins/zentao.json
@@ -0,0 +1,55 @@
+{
+  "id": "zentao",
+  "name": "ZENTAO",
+  "type": "integration",
+  "enabled": true,
+  "multiConnection": true,
+  "connectionLimit": 0,
+  "isBeta": true,
+  "isProvider": true,
+  "icon": "src/images/integrations/zentao.svg",
+  "private": false,
+  "connection": {
+    "authentication": "plain",
+    "fields": {
+      "name": { "enable": true, "required": true, "readonly": false },
+      "endpoint": { },
+      "proxy": { },
+      "username": { },
+      "password": { },
+      "rateLimitPerHour": { }
+    },
+    "labels": {
+      "name": "Connection Name",
+      "endpoint": "Endpoint URL",
+      "proxy": "Proxy URL",
+      "username": "Username",
+      "password": "Password",
+      "rateLimitPerHour": "Rate Limit (per hour)"
+    },
+    "placeholders": {
+      "name": "eg. Zentao",
+      "endpoint": "URL eg. http://subdomain.domain:port/api.php/v1/",
+      "proxy": "eg. http://proxy.localhost:8080",
+      "username": "eg. admin",
+      "password": "eg. ************",
+      "rateLimit": "1000"
+    },
+    "tooltips": {
+    }
+  },
+  "availableDataDomains": ["TICKET"],
+  "transformations": {
+    "scopes": {
+      "options": {
+      }
+    },
+    "default": {
+      "issueTypeRequirement": "",
+      "issueTypeBug": "",
+      "issueTypeIncident": "",
+      "productionPattern": "",
+      "deploymentPattern": ""
+    }
+  }
+}
\ No newline at end of file
diff --git a/plugins/helper/iso8601time.go b/plugins/helper/iso8601time.go
index 8d4092626..3029adb19 100644
--- a/plugins/helper/iso8601time.go
+++ b/plugins/helper/iso8601time.go
@@ -18,6 +18,7 @@ limitations under the License.
 package helper
 
 import (
+	"database/sql/driver"
 	"fmt"
 	"regexp"
 	"strings"
@@ -63,6 +64,10 @@ func init() {
 			Matcher: regexp.MustCompile(`[+-][\d]{2}:[\d]{2}$`),
 			Format:  "2006-01-02T15:04:05-07:00",
 		},
+		{
+			Matcher: regexp.MustCompile(`[+-][\d]{2}-[\d]{2}$`),
+			Format:  "2006-01-02",
+		},
 	}
 }
 
@@ -131,3 +136,29 @@ func Iso8601TimeToTime(iso8601Time *Iso8601Time) *time.Time {
 	t := iso8601Time.ToTime()
 	return &t
 }
+
+// Value FIXME ...
+func (jt *Iso8601Time) Value() (driver.Value, error) {
+	if jt == nil {
+		return nil, nil
+	}
+	var zeroTime time.Time
+	t := jt.time
+	if t.UnixNano() == zeroTime.UnixNano() {
+		return nil, nil
+	}
+	return t, nil
+}
+
+// Scan FIXME ...
+func (jt *Iso8601Time) Scan(v interface{}) error {
+	value, ok := v.(time.Time)
+	if ok {
+		*jt = Iso8601Time{
+			time:   value,
+			format: time.RFC3339,
+		}
+		return nil
+	}
+	return fmt.Errorf("can not convert %v to timestamp", v)
+}
diff --git a/plugins/zentao/api/blueprint.go b/plugins/zentao/api/blueprint.go
new file mode 100644
index 000000000..942de5b69
--- /dev/null
+++ b/plugins/zentao/api/blueprint.go
@@ -0,0 +1,70 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License.  You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package api
+
+import (
+	"encoding/json"
+	"github.com/apache/incubator-devlake/errors"
+
+	"github.com/apache/incubator-devlake/plugins/core"
+	"github.com/apache/incubator-devlake/plugins/helper"
+	"github.com/apache/incubator-devlake/plugins/zentao/tasks"
+)
+
+func MakePipelinePlan(subtaskMetas []core.SubTaskMeta, connectionId uint64, scope []*core.BlueprintScopeV100) (core.PipelinePlan, errors.Error) {
+	var err error
+	plan := make(core.PipelinePlan, len(scope))
+	for i, scopeElem := range scope {
+		taskOptions := make(map[string]interface{})
+		err = json.Unmarshal(scopeElem.Options, &taskOptions)
+		if err != nil {
+			return nil, errors.Default.WrapRaw(err)
+		}
+		taskOptions["connectionId"] = connectionId
+
+		//TODO Add transformation rules to task options
+
+		/*
+		   var transformationRules tasks.TransformationRules
+		   if len(scopeElem.Transformation) > 0 {
+		       err = json.Unmarshal(scopeElem.Transformation, &transformationRules)
+		       if err != nil {
+		           return nil, err
+		       }
+		   }
+		*/
+		//taskOptions["transformationRules"] = transformationRules
+		_, err := tasks.DecodeAndValidateTaskOptions(taskOptions)
+		if err != nil {
+			return nil, errors.Default.WrapRaw(err)
+		}
+		// subtasks
+		subtasks, err := helper.MakePipelinePlanSubtasks(subtaskMetas, scopeElem.Entities)
+		if err != nil {
+			return nil, errors.Default.WrapRaw(err)
+		}
+		plan[i] = core.PipelineStage{
+			{
+				Plugin:   "zentao",
+				Subtasks: subtasks,
+				Options:  taskOptions,
+			},
+		}
+	}
+	return plan, nil
+}
diff --git a/plugins/zentao/api/connection.go b/plugins/zentao/api/connection.go
new file mode 100644
index 000000000..93244c5e1
--- /dev/null
+++ b/plugins/zentao/api/connection.go
@@ -0,0 +1,148 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License.  You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package api
+
+import (
+	"context"
+	"github.com/apache/incubator-devlake/errors"
+	"github.com/apache/incubator-devlake/plugins/core"
+	"github.com/apache/incubator-devlake/plugins/helper"
+	"github.com/apache/incubator-devlake/plugins/zentao/models"
+	"github.com/mitchellh/mapstructure"
+	"net/http"
+)
+
+// TODO Please modify the following code to fit your needs
+func TestConnection(input *core.ApiResourceInput) (*core.ApiResourceOutput, errors.Error) {
+	// process input
+	var params models.TestConnectionRequest
+	err := mapstructure.Decode(input.Body, &params)
+	if err != nil {
+		return nil, errors.BadInput.Wrap(err, "could not decode request parameters")
+	}
+	err = vld.Struct(params)
+	if err != nil {
+		return nil, errors.BadInput.Wrap(err, "could not validate request parameters")
+	}
+
+	authApiClient, err := helper.NewApiClient(context.TODO(), params.Endpoint, nil, 0, params.Proxy, basicRes)
+	if err != nil {
+		return nil, errors.Default.WrapRaw(err)
+	}
+
+	// request for access token
+	tokenReqBody := &models.ApiAccessTokenRequest{
+		Account:  params.Username,
+		Password: params.Password,
+	}
+	tokenRes, err := authApiClient.Post("/tokens", nil, tokenReqBody, nil)
+	if err != nil {
+		return nil, errors.Default.WrapRaw(err)
+	}
+	tokenResBody := &models.ApiAccessTokenResponse{}
+	err = helper.UnmarshalResponse(tokenRes, tokenResBody)
+	if err != nil {
+		return nil, errors.Default.WrapRaw(err)
+	}
+	if tokenResBody.Token == "" {
+		return nil, errors.Default.New("failed to request access token")
+	}
+
+	// output
+	return nil, nil
+}
+
+//TODO Please modify the folowing code to adapt to your plugin
+/*
+POST /plugins/Zentao/connections
+{
+	"name": "Zentao data connection name",
+	"endpoint": "Zentao api endpoint, i.e. https://example.com",
+	"username": "username, usually should be email address",
+	"password": "Zentao api access token"
+}
+*/
+func PostConnections(input *core.ApiResourceInput) (*core.ApiResourceOutput, errors.Error) {
+	// update from request and save to database
+	connection := &models.ZentaoConnection{}
+	err := connectionHelper.Create(connection, input)
+	if err != nil {
+		return nil, err
+	}
+	return &core.ApiResourceOutput{Body: connection, Status: http.StatusOK}, nil
+}
+
+//TODO Please modify the folowing code to adapt to your plugin
+/*
+PATCH /plugins/Zentao/connections/:connectionId
+{
+	"name": "Zentao data connection name",
+	"endpoint": "Zentao api endpoint, i.e. https://example.com",
+	"username": "username, usually should be email address",
+	"password": "Zentao api access token"
+}
+*/
+func PatchConnection(input *core.ApiResourceInput) (*core.ApiResourceOutput, errors.Error) {
+	connection := &models.ZentaoConnection{}
+	err := connectionHelper.Patch(connection, input)
+	if err != nil {
+		return nil, err
+	}
+	return &core.ApiResourceOutput{Body: connection}, nil
+}
+
+/*
+DELETE /plugins/Zentao/connections/:connectionId
+*/
+func DeleteConnection(input *core.ApiResourceInput) (*core.ApiResourceOutput, errors.Error) {
+	connection := &models.ZentaoConnection{}
+	err := connectionHelper.First(connection, input.Params)
+	if err != nil {
+		return nil, err
+	}
+	err = connectionHelper.Delete(connection)
+	return &core.ApiResourceOutput{Body: connection}, err
+}
+
+/*
+GET /plugins/Zentao/connections
+*/
+func ListConnections(input *core.ApiResourceInput) (*core.ApiResourceOutput, errors.Error) {
+	var connections []models.ZentaoConnection
+	err := connectionHelper.List(&connections)
+	if err != nil {
+		return nil, err
+	}
+	return &core.ApiResourceOutput{Body: connections, Status: http.StatusOK}, nil
+}
+
+//TODO Please modify the folowing code to adapt to your plugin
+/*
+GET /plugins/Zentao/connections/:connectionId
+{
+	"name": "Zentao data connection name",
+	"endpoint": "Zentao api endpoint, i.e. https://merico.atlassian.net/rest",
+	"username": "username, usually should be email address",
+	"password": "Zentao api access token"
+}
+*/
+func GetConnection(input *core.ApiResourceInput) (*core.ApiResourceOutput, errors.Error) {
+	connection := &models.ZentaoConnection{}
+	err := connectionHelper.First(connection, input.Params)
+	return &core.ApiResourceOutput{Body: connection}, err
+}
diff --git a/plugins/zentao/api/init.go b/plugins/zentao/api/init.go
new file mode 100644
index 000000000..6774e1482
--- /dev/null
+++ b/plugins/zentao/api/init.go
@@ -0,0 +1,39 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License.  You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package api
+
+import (
+	"github.com/apache/incubator-devlake/plugins/core"
+	"github.com/apache/incubator-devlake/plugins/helper"
+	"github.com/go-playground/validator/v10"
+	"github.com/spf13/viper"
+	"gorm.io/gorm"
+)
+
+var vld *validator.Validate
+var connectionHelper *helper.ConnectionApiHelper
+var basicRes core.BasicRes
+
+func Init(config *viper.Viper, logger core.Logger, database *gorm.DB) {
+	basicRes = helper.NewDefaultBasicRes(config, logger, database)
+	vld = validator.New()
+	connectionHelper = helper.NewConnectionHelper(
+		basicRes,
+		vld,
+	)
+}
diff --git a/plugins/zentao/e2e/bug_test.go b/plugins/zentao/e2e/bug_test.go
new file mode 100644
index 000000000..c7f9baadd
--- /dev/null
+++ b/plugins/zentao/e2e/bug_test.go
@@ -0,0 +1,69 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License.  You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package e2e
+
+import (
+	"github.com/apache/incubator-devlake/models/common"
+	"github.com/apache/incubator-devlake/models/domainlayer/ticket"
+	"testing"
+
+	"github.com/apache/incubator-devlake/helpers/e2ehelper"
+	"github.com/apache/incubator-devlake/plugins/zentao/impl"
+	"github.com/apache/incubator-devlake/plugins/zentao/models"
+	"github.com/apache/incubator-devlake/plugins/zentao/tasks"
+)
+
+func TestZentaoBugDataFlow(t *testing.T) {
+
+	var zentao impl.Zentao
+	dataflowTester := e2ehelper.NewDataFlowTester(t, "zentao", zentao)
+
+	taskData := &tasks.ZentaoTaskData{
+		Options: &tasks.ZentaoOptions{
+			ConnectionId: 1,
+			ProjectId:    1,
+			ProductId:    3,
+			ExecutionId:  1,
+		},
+	}
+
+	// import raw data table
+	dataflowTester.ImportCsvIntoRawTable("./raw_tables/_raw_zentao_api_bugs.csv",
+		"_raw_zentao_api_bugs")
+
+	// verify extraction
+	dataflowTester.FlushTabler(&models.ZentaoBug{})
+	dataflowTester.Subtask(tasks.ExtractBugMeta, taskData)
+	dataflowTester.VerifyTableWithOptions(&models.ZentaoBug{}, e2ehelper.TableOptions{
+		CSVRelPath:  "./snapshot_tables/_tool_zentao_bugs.csv",
+		IgnoreTypes: []interface{}{common.NoPKModel{}},
+	})
+
+	// verify conversion
+	dataflowTester.FlushTabler(&ticket.Issue{})
+	dataflowTester.FlushTabler(&ticket.BoardIssue{})
+	dataflowTester.Subtask(tasks.ConvertBugMeta, taskData)
+	dataflowTester.VerifyTableWithOptions(&ticket.Issue{}, e2ehelper.TableOptions{
+		CSVRelPath:  "./snapshot_tables/issues_bug.csv",
+		IgnoreTypes: []interface{}{common.NoPKModel{}},
+	})
+	dataflowTester.VerifyTableWithOptions(&ticket.BoardIssue{}, e2ehelper.TableOptions{
+		CSVRelPath:  "./snapshot_tables/board_issues_bug.csv",
+		IgnoreTypes: []interface{}{common.NoPKModel{}},
+	})
+}
diff --git a/plugins/zentao/e2e/execution_test.go b/plugins/zentao/e2e/execution_test.go
new file mode 100644
index 000000000..3b0371be5
--- /dev/null
+++ b/plugins/zentao/e2e/execution_test.go
@@ -0,0 +1,63 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License.  You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package e2e
+
+import (
+	"github.com/apache/incubator-devlake/models/common"
+	"github.com/apache/incubator-devlake/models/domainlayer/ticket"
+	"testing"
+
+	"github.com/apache/incubator-devlake/helpers/e2ehelper"
+	"github.com/apache/incubator-devlake/plugins/zentao/impl"
+	"github.com/apache/incubator-devlake/plugins/zentao/models"
+	"github.com/apache/incubator-devlake/plugins/zentao/tasks"
+)
+
+func TestZentaoExecutionDataFlow(t *testing.T) {
+
+	var zentao impl.Zentao
+	dataflowTester := e2ehelper.NewDataFlowTester(t, "zentao", zentao)
+
+	taskData := &tasks.ZentaoTaskData{
+		Options: &tasks.ZentaoOptions{
+			ConnectionId: 1,
+			ProjectId:    1,
+			ProductId:    3,
+			ExecutionId:  1,
+		},
+	}
+
+	// import raw data table
+	dataflowTester.ImportCsvIntoRawTable("./raw_tables/_raw_zentao_api_executions.csv",
+		"_raw_zentao_api_executions")
+
+	// verify extraction
+	dataflowTester.FlushTabler(&models.ZentaoExecution{})
+	dataflowTester.Subtask(tasks.ExtractExecutionMeta, taskData)
+	dataflowTester.VerifyTableWithOptions(&models.ZentaoExecution{}, e2ehelper.TableOptions{
+		CSVRelPath:  "./snapshot_tables/_tool_zentao_executions.csv",
+		IgnoreTypes: []interface{}{common.NoPKModel{}},
+	})
+
+	dataflowTester.FlushTabler(&ticket.Board{})
+	dataflowTester.Subtask(tasks.ConvertExecutionMeta, taskData)
+	dataflowTester.VerifyTableWithOptions(&ticket.Board{}, e2ehelper.TableOptions{
+		CSVRelPath:  "./snapshot_tables/boards_execution.csv",
+		IgnoreTypes: []interface{}{common.NoPKModel{}},
+	})
+}
diff --git a/plugins/zentao/e2e/product_test.go b/plugins/zentao/e2e/product_test.go
new file mode 100644
index 000000000..af7ad2887
--- /dev/null
+++ b/plugins/zentao/e2e/product_test.go
@@ -0,0 +1,63 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License.  You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package e2e
+
+import (
+	"github.com/apache/incubator-devlake/models/common"
+	"github.com/apache/incubator-devlake/models/domainlayer/ticket"
+	"testing"
+
+	"github.com/apache/incubator-devlake/helpers/e2ehelper"
+	"github.com/apache/incubator-devlake/plugins/zentao/impl"
+	"github.com/apache/incubator-devlake/plugins/zentao/models"
+	"github.com/apache/incubator-devlake/plugins/zentao/tasks"
+)
+
+func TestZentaoProductDataFlow(t *testing.T) {
+
+	var zentao impl.Zentao
+	dataflowTester := e2ehelper.NewDataFlowTester(t, "zentao", zentao)
+
+	taskData := &tasks.ZentaoTaskData{
+		Options: &tasks.ZentaoOptions{
+			ConnectionId: 1,
+			ProjectId:    1,
+			ProductId:    3,
+			ExecutionId:  9,
+		},
+	}
+
+	// import raw data table
+	dataflowTester.ImportCsvIntoRawTable("./raw_tables/_raw_zentao_api_products.csv",
+		"_raw_zentao_api_products")
+
+	// verify extraction
+	dataflowTester.FlushTabler(&models.ZentaoProduct{})
+	dataflowTester.Subtask(tasks.ExtractProductMeta, taskData)
+	dataflowTester.VerifyTableWithOptions(&models.ZentaoProduct{}, e2ehelper.TableOptions{
+		CSVRelPath:  "./snapshot_tables/_tool_zentao_products.csv",
+		IgnoreTypes: []interface{}{common.NoPKModel{}},
+	})
+
+	dataflowTester.FlushTabler(&ticket.Board{})
+	dataflowTester.Subtask(tasks.ConvertProductMeta, taskData)
+	dataflowTester.VerifyTableWithOptions(&ticket.Board{}, e2ehelper.TableOptions{
+		CSVRelPath:  "./snapshot_tables/boards_product.csv",
+		IgnoreTypes: []interface{}{common.NoPKModel{}},
+	})
+}
diff --git a/plugins/zentao/e2e/raw_tables/_raw_zentao_api_bugs.csv b/plugins/zentao/e2e/raw_tables/_raw_zentao_api_bugs.csv
new file mode 100644
index 000000000..8ff72a6bc
--- /dev/null
+++ b/plugins/zentao/e2e/raw_tables/_raw_zentao_api_bugs.csv
@@ -0,0 +1,5 @@
+id,params,data,url,input,created_at
+1,"{""ConnectionId"":1,""ProductId"":3,""ExecutionId"":1,""ProjectId"":1}","{""id"":4,""project"":7,""product"":3,""injection"":0,""identify"":0,""branch"":0,""module"":11,""execution"":1,""plan"":0,""story"":4,""storyVersion"":1,""task"":9,""toTask"":0,""toStory"":0,""title"":""\u552e\u540e\u670d\u52a1\u9875\u9762\u95ee\u9898"",""keywords"":"""",""severity"":3,""pri"":1,""type"":""codeerror"",""os"":"""",""browser"":"""",""hardware"":"""",""found"":"""",""steps"":""\u003Cp\u003E[\u6b65\ [...]
+2,"{""ConnectionId"":1,""ProductId"":3,""ExecutionId"":1,""ProjectId"":1}","{""id"":3,""project"":7,""product"":3,""injection"":0,""identify"":0,""branch"":0,""module"":10,""execution"":1,""plan"":0,""story"":3,""storyVersion"":2,""task"":6,""toTask"":0,""toStory"":0,""title"":""\u6210\u679c\u5c55\u793a\u9875\u9762\u95ee\u9898"",""keywords"":"""",""severity"":3,""pri"":1,""type"":""codeerror"",""os"":"""",""browser"":"""",""hardware"":"""",""found"":"""",""steps"":""\u003Cp\u003E[\u6b65\ [...]
+3,"{""ConnectionId"":1,""ProductId"":3,""ExecutionId"":1,""ProjectId"":1}","{""id"":2,""project"":7,""product"":3,""injection"":0,""identify"":0,""branch"":0,""module"":9,""execution"":1,""plan"":1,""story"":2,""storyVersion"":1,""task"":15,""toTask"":0,""toStory"":0,""title"":""\u65b0\u95fb\u4e2d\u5fc3\u9875\u9762\u95ee\u9898"",""keywords"":""hh"",""severity"":3,""pri"":2,""type"":""codeerror"",""os"":"",windows"",""browser"":"",chrome"",""hardware"":"""",""found"":"""",""steps"":""\u00 [...]
+4,"{""ConnectionId"":1,""ProductId"":3,""ExecutionId"":1,""ProjectId"":1}","{""id"":1,""project"":7,""product"":3,""injection"":0,""identify"":0,""branch"":0,""module"":8,""execution"":1,""plan"":0,""story"":1,""storyVersion"":1,""task"":1,""toTask"":0,""toStory"":0,""title"":""\u9996\u9875\u9875\u9762\u95ee\u9898"",""keywords"":"""",""severity"":3,""pri"":1,""type"":""codeerror"",""os"":"""",""browser"":"""",""hardware"":"""",""found"":"""",""steps"":""\u003Cp\u003E[\u6b65\u9aa4]\u8fdb\ [...]
diff --git a/plugins/zentao/e2e/raw_tables/_raw_zentao_api_executions.csv b/plugins/zentao/e2e/raw_tables/_raw_zentao_api_executions.csv
new file mode 100644
index 000000000..019b75cdb
--- /dev/null
+++ b/plugins/zentao/e2e/raw_tables/_raw_zentao_api_executions.csv
@@ -0,0 +1,2 @@
+id,params,data,url,input,created_at
+1,"{""ConnectionId"":1,""ProductId"":3,""ExecutionId"":1,""ProjectId"":1}","{""id"":1,""project"":7,""model"":"""",""type"":""sprint"",""lifetime"":""sprint"",""budget"":""0"",""budgetUnit"":""CNY"",""attribute"":"""",""percent"":0,""milestone"":""0"",""output"":"""",""auth"":"""",""parent"":7,""path"":"",7,1,"",""grade"":1,""name"":""\u4f01\u4e1a\u7f51\u7ad9\u7b2c\u4e00\u671f"",""code"":""coWeb1"",""begin"":""2020-06-05"",""end"":""2021-12-04"",""realBegan"":null,""realEnd"":null,""days [...]
diff --git a/plugins/zentao/e2e/raw_tables/_raw_zentao_api_products.csv b/plugins/zentao/e2e/raw_tables/_raw_zentao_api_products.csv
new file mode 100644
index 000000000..e58feada0
--- /dev/null
+++ b/plugins/zentao/e2e/raw_tables/_raw_zentao_api_products.csv
@@ -0,0 +1,2 @@
+id,params,data,url,input,created_at
+4,"{""ConnectionId"":1,""ProductId"":3,""ExecutionId"":9,""ProjectId"":1}","{""id"":3,""program"":10,""name"":""\u4ea7\u54c1\u540d\u79f01"",""code"":""\u4ea7\u54c1\u4ee3\u53f72"",""bind"":""0"",""line"":31,""type"":""normal"",""status"":""normal"",""subStatus"":"""",""desc"":""\u003Cspan style=\""background-color:#FFFFFF;\""\u003E\u4ea7\u54c1\u63cf\u8ff01\u003C\/span\u003E"",""PO"":{""id"":1,""account"":""devlake"",""avatar"":"""",""realname"":""devlake""},""QD"":{""id"":1,""account"":"" [...]
diff --git a/plugins/zentao/e2e/raw_tables/_raw_zentao_api_stories.csv b/plugins/zentao/e2e/raw_tables/_raw_zentao_api_stories.csv
new file mode 100644
index 000000000..dd79bc2f1
--- /dev/null
+++ b/plugins/zentao/e2e/raw_tables/_raw_zentao_api_stories.csv
@@ -0,0 +1,8 @@
+id,params,data,url,input,created_at
+1,"{""ConnectionId"":1,""ProductId"":3,""ExecutionId"":1,""ProjectId"":1}","{""id"":7,""vision"":""rnd"",""parent"":0,""product"":1,""branch"":0,""module"":7,""plan"":""1"",""source"":""po"",""sourceNote"":"""",""fromBug"":0,""feedback"":0,""title"":""\u5173\u4e8e\u6211\u4eec\u7684\u8bbe\u8ba1\u548c\u5f00\u53d1"",""keywords"":"""",""type"":""story"",""category"":""feature"",""pri"":1,""estimate"":1,""status"":""reviewing"",""subStatus"":"""",""color"":"""",""stage"":""planned"",""stagedB [...]
+2,"{""ConnectionId"":1,""ProductId"":3,""ExecutionId"":1,""ProjectId"":1}","{""id"":6,""vision"":""rnd"",""parent"":0,""product"":1,""branch"":0,""module"":6,""plan"":""1"",""source"":""po"",""sourceNote"":"""",""fromBug"":0,""feedback"":0,""title"":""\u5408\u4f5c\u6d3d\u8c08\u7684\u8bbe\u8ba1\u548c\u5f00\u53d1"",""keywords"":"""",""type"":""story"",""category"":""feature"",""pri"":1,""estimate"":1,""status"":""reviewing"",""subStatus"":"""",""color"":"""",""stage"":""planned"",""stagedB [...]
+3,"{""ConnectionId"":1,""ProductId"":3,""ExecutionId"":1,""ProjectId"":1}","{""id"":5,""vision"":""rnd"",""parent"":0,""product"":1,""branch"":0,""module"":5,""plan"":""1"",""source"":""po"",""sourceNote"":"""",""fromBug"":0,""feedback"":0,""title"":""\u8bda\u8058\u82f1\u624d\u7684\u8bbe\u8ba1\u548c\u5f00\u53d1"",""keywords"":"""",""type"":""story"",""category"":""feature"",""pri"":1,""estimate"":1,""status"":""reviewing"",""subStatus"":"""",""color"":"""",""stage"":""planned"",""stagedB [...]
+4,"{""ConnectionId"":1,""ProductId"":3,""ExecutionId"":1,""ProjectId"":1}","{""id"":4,""vision"":""rnd"",""parent"":0,""product"":1,""branch"":0,""module"":4,""plan"":""1"",""source"":""po"",""sourceNote"":"""",""fromBug"":0,""feedback"":0,""title"":""\u552e\u540e\u670d\u52a1\u7684\u8bbe\u8ba1\u548c\u5f00\u53d1"",""keywords"":"""",""type"":""story"",""category"":""feature"",""pri"":1,""estimate"":1,""status"":""active"",""subStatus"":"""",""color"":"""",""stage"":""developed"",""stagedBy [...]
+5,"{""ConnectionId"":1,""ProductId"":3,""ExecutionId"":1,""ProjectId"":1}","{""id"":3,""vision"":""rnd"",""parent"":0,""product"":1,""branch"":0,""module"":3,""plan"":""1"",""source"":""po"",""sourceNote"":"""",""fromBug"":0,""feedback"":0,""title"":""\u6210\u679c\u5c55\u793a\u7684\u8bbe\u8ba1\u548c\u5f00\u53d1"",""keywords"":"""",""type"":""story"",""category"":""feature"",""pri"":1,""estimate"":0,""status"":""active"",""subStatus"":"""",""color"":"""",""stage"":""developing"",""stagedB [...]
+6,"{""ConnectionId"":1,""ProductId"":3,""ExecutionId"":1,""ProjectId"":1}","{""id"":2,""vision"":""rnd"",""parent"":0,""product"":1,""branch"":0,""module"":2,""plan"":""1"",""source"":""po"",""sourceNote"":"""",""fromBug"":0,""feedback"":0,""title"":""\u65b0\u95fb\u4e2d\u5fc3\u7684\u8bbe\u8ba1\u548c\u5f00\u53d1\u3002"",""keywords"":"""",""type"":""story"",""category"":""feature"",""pri"":1,""estimate"":1,""status"":""active"",""subStatus"":"""",""color"":"""",""stage"":""projected"",""st [...]
+7,"{""ConnectionId"":1,""ProductId"":3,""ExecutionId"":1,""ProjectId"":1}","{""id"":1,""vision"":""rnd"",""parent"":0,""product"":1,""branch"":0,""module"":1,""plan"":""1"",""source"":""po"",""sourceNote"":"""",""fromBug"":0,""feedback"":0,""title"":""\u9996\u9875\u8bbe\u8ba1\u548c\u5f00\u53d1"",""keywords"":"""",""type"":""story"",""category"":""feature"",""pri"":1,""estimate"":1,""status"":""active"",""subStatus"":"""",""color"":"""",""stage"":""developing"",""stagedBy"":"""",""mailto" [...]
diff --git a/plugins/zentao/e2e/raw_tables/_raw_zentao_api_tasks.csv b/plugins/zentao/e2e/raw_tables/_raw_zentao_api_tasks.csv
new file mode 100644
index 000000000..8aa6a4489
--- /dev/null
+++ b/plugins/zentao/e2e/raw_tables/_raw_zentao_api_tasks.csv
@@ -0,0 +1,4 @@
+id,params,data,url,input,created_at
+1,"{""ConnectionId"":1,""ProductId"":3,""ExecutionId"":1,""ProjectId"":1}","{""id"":1,""project"":13,""parent"":0,""execution"":9,""module"":0,""design"":0,""story"":0,""storyVersion"":1,""designVersion"":0,""fromBug"":0,""feedback"":0,""fromIssue"":0,""name"":""\u4efb\u52a1\u540d\u79f0"",""type"":""devel"",""mode"":"""",""pri"":3,""estimate"":0,""consumed"":0,""left"":0,""deadline"":""2022-10-01"",""status"":""wait"",""subStatus"":"""",""color"":"""",""mailto"":[{""id"":2,""account"":"" [...]
+2,"{""ConnectionId"":1,""ProductId"":3,""ExecutionId"":1,""ProjectId"":1}","{""id"":2,""project"":13,""parent"":0,""execution"":9,""module"":0,""design"":0,""story"":0,""storyVersion"":1,""designVersion"":0,""fromBug"":0,""feedback"":0,""fromIssue"":0,""name"":""\u4efb\u52a1\u540d\u79f0"",""type"":""devel"",""mode"":"""",""pri"":3,""estimate"":0,""consumed"":0,""left"":0,""deadline"":""2022-10-01"",""status"":""wait"",""subStatus"":"""",""color"":"""",""mailto"":[{""id"":2,""account"":"" [...]
+4,"{""ConnectionId"":1,""ProductId"":3,""ExecutionId"":1,""ProjectId"":1}","{""id"":3,""project"":13,""parent"":0,""execution"":9,""module"":0,""design"":0,""story"":0,""storyVersion"":1,""designVersion"":0,""fromBug"":0,""feedback"":0,""fromIssue"":0,""name"":""\u4efb\u52a1\u540d\u79f0"",""type"":""devel"",""mode"":"""",""pri"":3,""estimate"":0,""consumed"":0,""left"":0,""deadline"":""2022-10-01"",""status"":""wait"",""subStatus"":"""",""color"":"""",""mailto"":[{""id"":2,""account"":"" [...]
diff --git a/plugins/zentao/e2e/snapshot_tables/_tool_zentao_bugs.csv b/plugins/zentao/e2e/snapshot_tables/_tool_zentao_bugs.csv
new file mode 100644
index 000000000..da82aa928
--- /dev/null
+++ b/plugins/zentao/e2e/snapshot_tables/_tool_zentao_bugs.csv
@@ -0,0 +1,13 @@
+connection_id,id,project,product,injection,identify,branch,module,execution,plan,story,story_version,task,to_task,to_story,title,keywords,severity,pri,type,os,browser,hardware,found,steps,status,sub_status,color,confirmed,activated_count,activated_date,feedback_by,notify_email,opened_by_id,opened_by_name,opened_date,opened_build,assigned_to_id,assigned_to_name,assigned_date,deadline,resolved_by_id,resolution,resolved_build,resolved_date,closed_by_id,closed_date,duplicate_bug,link_bug,fee [...]
+1,1,7,3,0,0,0,8,1,0,1,1,1,0,0,首页页面问题,,3,1,codeerror,,,,,"<p>[步骤]进入首页</p>
+<p>[结果]出现乱码&nbsp;&nbsp;&nbsp;&nbsp;</p>
+<p>[期望]正常显示</p>",active,,,0,0,,,,7,测试甲,2012-06-05T02:56:11.000+00:00,主干,4,开发甲,2012-06-05T02:56:11.000+00:00,,0,,,,0,,0,,0,0,0,0,,,,,,,0,0,2021-04-28T03:09:08.000+00:00,0,1,3,0,激活,normal
+1,2,7,3,0,0,0,9,1,1,2,1,15,0,0,新闻中心页面问题,hh,3,2,codeerror,",windows",",chrome",,,"<p>[步骤]进入新闻中心</p>
+<p>[结果]页面出现乱码</p>
+<p>[期望]正常显示rew</p>",delay,,,1,1,2022-10-05T04:16:44.000+00:00,,1114255335@qq.com,7,测试甲,2012-06-05T02:57:11.000+00:00,主干,0,,2022-10-05T04:19:22.000+00:00,2022-10-06,0,,,,0,,0,,0,0,0,0,,,,,,,1,1,2022-10-05T04:19:22.000+00:00,0,2,3,0,过期Bug,normal
+1,3,7,3,0,0,0,10,1,0,3,2,6,0,0,成果展示页面问题,,3,1,codeerror,,,,,"<p>[步骤]进入成果展示&nbsp;&nbsp;&nbsp;&nbsp;</p>
+<p>[结果]乱码</p>
+<p>[期望]正常显示</p>",active,,,0,0,,,,8,测试乙,2012-06-05T02:58:22.000+00:00,主干,4,开发甲,2012-06-05T02:58:22.000+00:00,,0,,,,0,,0,,0,0,0,0,,,,,,,0,0,2021-04-28T03:09:08.000+00:00,0,1,3,0,激活,normal
+1,4,7,3,0,0,0,11,1,0,4,1,9,0,0,售后服务页面问题,,3,1,codeerror,,,,,"<p>[步骤]进入售后服务</p>
+<p>[结果]乱码</p>
+<p>[期望]正常显示</p>",resolved,,,1,0,,,,9,测试丙,2012-06-05T03:00:19.000+00:00,主干,9,测试丙,2022-10-05T04:10:08.000+00:00,,1,fixed,主干,2022-10-05T04:09:59.000+00:00,0,,0,,0,0,0,0,,,,,,,0,1,2022-10-05T04:10:08.000+00:00,0,1,3,0,已解决,normal
diff --git a/plugins/zentao/e2e/snapshot_tables/_tool_zentao_executions.csv b/plugins/zentao/e2e/snapshot_tables/_tool_zentao_executions.csv
new file mode 100644
index 000000000..05762df6f
--- /dev/null
+++ b/plugins/zentao/e2e/snapshot_tables/_tool_zentao_executions.csv
@@ -0,0 +1,2 @@
+connection_id,id,project,model,type,lifetime,budget,budget_unit,attribute,percent,milestone,output,auth,parent,path,grade,name,code,plan_begin,plan_end,real_began,real_end,days,status,sub_status,pri,description,version,parent_version,plan_duration,real_duration,opened_by_id,opened_date,opened_version,last_edited_by_id,last_edited_date,closed_by_id,closed_date,canceled_by_id,canceled_date,suspended_date,po_id,pm_id,qd_id,rd_id,team,acl,order_in,vision,display_cards,fluid_board,deleted,tot [...]
+1,1,7,,sprint,sprint,0,CNY,,0,0,,,7,",7,1,",1,企业网站第一期,coWeb1,2020-06-05,2021-12-04,,,391,doing,,1,开发企业网站的基本雏形。<br />,0,0,0,0,0,,,0,,0,,0,,0000-00-00,2,3,10,2,公司开发团队,open,5,rnd,0,0,0,11753,52,40,29,0,58,0
diff --git a/plugins/zentao/e2e/snapshot_tables/_tool_zentao_products.csv b/plugins/zentao/e2e/snapshot_tables/_tool_zentao_products.csv
new file mode 100644
index 000000000..b0bd40c9c
--- /dev/null
+++ b/plugins/zentao/e2e/snapshot_tables/_tool_zentao_products.csv
@@ -0,0 +1,2 @@
+connection_id,id,program,name,code,bind,line,type,status,sub_status,description,po_id,qd_id,rd_id,acl,reviewer,created_by_id,created_date,created_version,order_in,deleted,plans,releases,builds,cases,projects,executions,bugs,docs,progress,case_review
+1,3,10,产品名称1,产品代号2,0,31,normal,normal,,"<span style=""background-color:#FFFFFF;"">产品描述1</span>",1,1,1,private,"devlake,dev1",1,2022-11-17T06:42:25.000+00:00,17.6,15,0,1,0,0,0,0,0,0,0,0,0
diff --git a/plugins/zentao/e2e/snapshot_tables/_tool_zentao_stories.csv b/plugins/zentao/e2e/snapshot_tables/_tool_zentao_stories.csv
new file mode 100644
index 000000000..c36663ef0
--- /dev/null
+++ b/plugins/zentao/e2e/snapshot_tables/_tool_zentao_stories.csv
@@ -0,0 +1,8 @@
+connection_id,id,product,branch,version,order_in,vision,parent,module,plan,source,source_note,from_bug,feedback,title,keywords,type,category,pri,estimate,status,sub_status,color,stage,lib,from_story,from_version,opened_by_id,opened_by_name,opened_date,assigned_to_id,assigned_to_name,assigned_date,approved_date,last_edited_id,last_edited_date,changed_date,reviewed_by_id,reviewed_date,closed_id,closed_date,closed_reason,activated_date,to_bug,child_stories,link_stories,link_requirements,dup [...]
+1,1,1,0,1,0,rnd,0,1,1,po,,0,0,首页设计和开发,,story,feature,1,1,active,,,developing,0,0,1,2,产品经理,2012-06-05T02:09:49.000+00:00,2,产品经理,,0000-00-00,2,2012-06-05T02:25:19.000+00:00,0000-00-00 00:00:00,0,2012-06-04T16:00:00.000+00:00,0,,,0000-00-00 00:00:00,0,,,,0,0,,,0,0,1,1.0版本 
+1,2,1,0,1,0,rnd,0,2,1,po,,0,0,新闻中心的设计和开发。,,story,feature,1,1,active,,,projected,0,0,1,2,产品经理,2012-06-05T02:16:37.000+00:00,2,产品经理,2012-06-05T02:16:37.000+00:00,0000-00-00,2,2012-06-05T02:25:33.000+00:00,0000-00-00 00:00:00,0,2012-06-04T16:00:00.000+00:00,0,,,0000-00-00 00:00:00,0,,,,0,0,,,0,0,1,1.0版本 
+1,3,1,0,2,0,rnd,0,3,1,po,,0,0,成果展示的设计和开发,,story,feature,1,0,active,,,developing,0,0,1,2,产品经理,2012-06-05T02:18:10.000+00:00,2,产品经理,2012-06-05T02:18:10.000+00:00,0000-00-00,2,2012-06-05T02:25:38.000+00:00,0000-00-00 00:00:00,0,2012-06-04T16:00:00.000+00:00,0,,,0000-00-00 00:00:00,0,,,,0,0,,,0,0,1,1.0版本 
+1,4,1,0,1,0,rnd,0,4,1,po,,0,0,售后服务的设计和开发,,story,feature,1,1,active,,,developed,0,0,1,2,产品经理,2012-06-05T02:20:16.000+00:00,2,产品经理,2012-06-05T02:20:16.000+00:00,0000-00-00,2,2012-06-05T02:25:42.000+00:00,0000-00-00 00:00:00,0,2012-06-04T16:00:00.000+00:00,0,,,0000-00-00 00:00:00,0,,,,0,0,,,0,0,1,1.0版本 
+1,5,1,0,1,0,rnd,0,5,1,po,,0,0,诚聘英才的设计和开发,,story,feature,1,1,reviewing,,,planned,0,0,1,2,产品经理,2012-06-05T02:21:39.000+00:00,2,产品经理,2012-06-05T02:21:39.000+00:00,0000-00-00,0,,0000-00-00 00:00:00,0,,0,,,0000-00-00 00:00:00,0,,,,0,0,,,0,0,1,1.0版本 
+1,6,1,0,1,0,rnd,0,6,1,po,,0,0,合作洽谈的设计和开发,,story,feature,1,1,reviewing,,,planned,0,0,1,2,产品经理,2012-06-05T02:23:11.000+00:00,2,产品经理,2012-06-05T02:23:11.000+00:00,0000-00-00,0,,0000-00-00 00:00:00,0,,0,,,0000-00-00 00:00:00,0,,,,0,0,,,0,0,1,1.0版本 
+1,7,1,0,1,0,rnd,0,7,1,po,,0,0,关于我们的设计和开发,,story,feature,1,1,reviewing,,,planned,0,0,1,2,产品经理,2012-06-05T02:24:19.000+00:00,2,产品经理,2012-06-05T02:24:19.000+00:00,0000-00-00,0,,0000-00-00 00:00:00,0,,0,,,0000-00-00 00:00:00,0,,,,0,0,,,0,0,1,1.0版本 
diff --git a/plugins/zentao/e2e/snapshot_tables/_tool_zentao_tasks.csv b/plugins/zentao/e2e/snapshot_tables/_tool_zentao_tasks.csv
new file mode 100644
index 000000000..276d53cda
--- /dev/null
+++ b/plugins/zentao/e2e/snapshot_tables/_tool_zentao_tasks.csv
@@ -0,0 +1,4 @@
+connection_id,id,execution_id,project,parent,execution,module,design,story,story_version,design_version,from_bug,feedback,from_issue,name,type,mode,pri,estimate,consumed,deadline,status,sub_status,color,description,version,opened_by_id,opened_by_name,opened_date,assigned_to_id,assigned_to_name,assigned_date,est_started,real_started,finished_id,finished_date,finished_list,canceled_id,canceled_date,closed_by_id,closed_date,plan_duration,real_duration,closed_reason,last_edited_id,last_edite [...]
+1,1,1,13,0,9,0,0,0,1,0,0,0,0,任务名称,devel,,3,0,0,2022-10-01,wait,,,任务描述<span> </span><br /><div><br /></div>,1,1,devlake,2022-09-19T01:50:37.000+00:00,5,开发乙,2022-09-19T01:50:37.000+00:00,2022-09-20,,0,,,0,,0,,0,0,,0,,0000-00-00 00:00:00,0,0,0,,,,,0,rnd,0,,0,0,,开发乙,3,0,0
+1,2,1,13,0,9,0,0,0,1,0,0,0,0,任务名称,devel,,3,0,0,2022-10-01,wait,,,任务描述<span> </span><br /><div><br /></div>,1,1,devlake,2022-09-19T01:50:37.000+00:00,5,开发乙,2022-09-19T01:50:37.000+00:00,2022-09-20,,0,,,0,,0,,0,0,,0,,0000-00-00 00:00:00,0,0,0,,,,,0,rnd,0,,0,0,,开发乙,3,0,0
+1,3,1,13,0,9,0,0,0,1,0,0,0,0,任务名称,devel,,3,0,0,2022-10-01,wait,,,任务描述<span> </span><br /><div><br /></div>,1,1,devlake,2022-09-19T01:50:37.000+00:00,5,开发乙,2022-09-19T01:50:37.000+00:00,2022-09-20,,0,,,0,,0,,0,0,,0,,0000-00-00 00:00:00,0,0,0,,,,,0,rnd,0,,0,0,,开发乙,3,0,0
diff --git a/plugins/zentao/e2e/snapshot_tables/board_issues_bug.csv b/plugins/zentao/e2e/snapshot_tables/board_issues_bug.csv
new file mode 100644
index 000000000..af59f0f72
--- /dev/null
+++ b/plugins/zentao/e2e/snapshot_tables/board_issues_bug.csv
@@ -0,0 +1,5 @@
+board_id,issue_id
+zentao:ZentaoProduct:1:3,zentao:ZentaoBug:1:1
+zentao:ZentaoProduct:1:3,zentao:ZentaoBug:1:2
+zentao:ZentaoProduct:1:3,zentao:ZentaoBug:1:3
+zentao:ZentaoProduct:1:3,zentao:ZentaoBug:1:4
diff --git a/plugins/zentao/e2e/snapshot_tables/board_issues_story.csv b/plugins/zentao/e2e/snapshot_tables/board_issues_story.csv
new file mode 100644
index 000000000..2bfddefa4
--- /dev/null
+++ b/plugins/zentao/e2e/snapshot_tables/board_issues_story.csv
@@ -0,0 +1,8 @@
+board_id,issue_id
+zentao:ZentaoProduct:1:3,zentao:ZentaoStory:1:1
+zentao:ZentaoProduct:1:3,zentao:ZentaoStory:1:2
+zentao:ZentaoProduct:1:3,zentao:ZentaoStory:1:3
+zentao:ZentaoProduct:1:3,zentao:ZentaoStory:1:4
+zentao:ZentaoProduct:1:3,zentao:ZentaoStory:1:5
+zentao:ZentaoProduct:1:3,zentao:ZentaoStory:1:6
+zentao:ZentaoProduct:1:3,zentao:ZentaoStory:1:7
diff --git a/plugins/zentao/e2e/snapshot_tables/board_issues_task.csv b/plugins/zentao/e2e/snapshot_tables/board_issues_task.csv
new file mode 100644
index 000000000..5500305c8
--- /dev/null
+++ b/plugins/zentao/e2e/snapshot_tables/board_issues_task.csv
@@ -0,0 +1 @@
+board_id,issue_id
diff --git a/plugins/zentao/e2e/snapshot_tables/boards_execution.csv b/plugins/zentao/e2e/snapshot_tables/boards_execution.csv
new file mode 100644
index 000000000..e596c62f9
--- /dev/null
+++ b/plugins/zentao/e2e/snapshot_tables/boards_execution.csv
@@ -0,0 +1,2 @@
+id,name,description,url,created_date,type
+zentao:ZentaoExecution:1:1,企业网站第一期,开发企业网站的基本雏形。<br />,",7,1,",,sprint
diff --git a/plugins/zentao/e2e/snapshot_tables/boards_product.csv b/plugins/zentao/e2e/snapshot_tables/boards_product.csv
new file mode 100644
index 000000000..1da72455a
--- /dev/null
+++ b/plugins/zentao/e2e/snapshot_tables/boards_product.csv
@@ -0,0 +1,2 @@
+id,name,description,url,created_date,type
+zentao:ZentaoProduct:1:3,产品名称1,"<span style=""background-color:#FFFFFF;"">产品描述1</span>",,2022-11-17T06:42:25.000+00:00,normal
diff --git a/plugins/zentao/e2e/snapshot_tables/issues_bug.csv b/plugins/zentao/e2e/snapshot_tables/issues_bug.csv
new file mode 100644
index 000000000..0bb589b18
--- /dev/null
+++ b/plugins/zentao/e2e/snapshot_tables/issues_bug.csv
@@ -0,0 +1,5 @@
+id,url,icon_url,issue_key,title,description,epic_key,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,deployment_id
+zentao:ZentaoBug:1:1,,,1,首页页面问题,,,codeerror,IN_PROGRESS,active,0,,2012-06-05T02:56:11.000+00:00,2021-04-28T03:09:08.000+00:00,0,zentao:ZentaoStory:1:1,,0,0,0,7,测试甲,4,开发甲,,,
+zentao:ZentaoBug:1:2,,,2,新闻中心页面问题,,,codeerror,IN_PROGRESS,delay,0,,2012-06-05T02:57:11.000+00:00,2022-10-05T04:19:22.000+00:00,0,zentao:ZentaoStory:1:2,,0,0,0,7,测试甲,0,,,,
+zentao:ZentaoBug:1:3,,,3,成果展示页面问题,,,codeerror,IN_PROGRESS,active,0,,2012-06-05T02:58:22.000+00:00,2021-04-28T03:09:08.000+00:00,0,zentao:ZentaoStory:1:3,,0,0,0,8,测试乙,4,开发甲,,,
+zentao:ZentaoBug:1:4,,,4,售后服务页面问题,,,codeerror,DONE,resolved,0,,2012-06-05T03:00:19.000+00:00,2022-10-05T04:10:08.000+00:00,0,zentao:ZentaoStory:1:4,,0,0,0,9,测试丙,9,测试丙,,,
diff --git a/plugins/zentao/e2e/snapshot_tables/issues_story.csv b/plugins/zentao/e2e/snapshot_tables/issues_story.csv
new file mode 100644
index 000000000..06752d1b5
--- /dev/null
+++ b/plugins/zentao/e2e/snapshot_tables/issues_story.csv
@@ -0,0 +1,8 @@
+id,url,icon_url,issue_key,title,description,epic_key,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,deployment_id
+zentao:ZentaoStory:1:1,,,1,首页设计和开发,,,story,IN_PROGRESS,developing,0,,2012-06-05T02:09:49.000+00:00,2012-06-05T02:25:19.000+00:00,0,zentao:ZentaoStory:1:0,,0,0,0,2,产品经理,2,产品经理,,,
+zentao:ZentaoStory:1:2,,,2,新闻中心的设计和开发。,,,story,IN_PROGRESS,projected,0,,2012-06-05T02:16:37.000+00:00,2012-06-05T02:25:33.000+00:00,0,zentao:ZentaoStory:1:0,,0,0,0,2,产品经理,2,产品经理,,,
+zentao:ZentaoStory:1:3,,,3,成果展示的设计和开发,,,story,IN_PROGRESS,developing,0,,2012-06-05T02:18:10.000+00:00,2012-06-05T02:25:38.000+00:00,0,zentao:ZentaoStory:1:0,,0,0,0,2,产品经理,2,产品经理,,,
+zentao:ZentaoStory:1:4,,,4,售后服务的设计和开发,,,story,IN_PROGRESS,developed,0,,2012-06-05T02:20:16.000+00:00,2012-06-05T02:25:42.000+00:00,0,zentao:ZentaoStory:1:0,,0,0,0,2,产品经理,2,产品经理,,,
+zentao:ZentaoStory:1:5,,,5,诚聘英才的设计和开发,,,story,IN_PROGRESS,planned,0,,2012-06-05T02:21:39.000+00:00,,0,zentao:ZentaoStory:1:0,,0,0,0,2,产品经理,2,产品经理,,,
+zentao:ZentaoStory:1:6,,,6,合作洽谈的设计和开发,,,story,IN_PROGRESS,planned,0,,2012-06-05T02:23:11.000+00:00,,0,zentao:ZentaoStory:1:0,,0,0,0,2,产品经理,2,产品经理,,,
+zentao:ZentaoStory:1:7,,,7,关于我们的设计和开发,,,story,IN_PROGRESS,planned,0,,2012-06-05T02:24:19.000+00:00,,0,zentao:ZentaoStory:1:0,,0,0,0,2,产品经理,2,产品经理,,,
diff --git a/plugins/zentao/e2e/snapshot_tables/issues_task.csv b/plugins/zentao/e2e/snapshot_tables/issues_task.csv
new file mode 100644
index 000000000..37eaa7ab1
--- /dev/null
+++ b/plugins/zentao/e2e/snapshot_tables/issues_task.csv
@@ -0,0 +1 @@
+id,url,icon_url,issue_key,title,description,epic_key,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,deployment_id
diff --git a/plugins/zentao/e2e/story_test.go b/plugins/zentao/e2e/story_test.go
new file mode 100644
index 000000000..c4bdf01d8
--- /dev/null
+++ b/plugins/zentao/e2e/story_test.go
@@ -0,0 +1,69 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License.  You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package e2e
+
+import (
+	"github.com/apache/incubator-devlake/models/common"
+	"github.com/apache/incubator-devlake/models/domainlayer/ticket"
+	"testing"
+
+	"github.com/apache/incubator-devlake/helpers/e2ehelper"
+	"github.com/apache/incubator-devlake/plugins/zentao/impl"
+	"github.com/apache/incubator-devlake/plugins/zentao/models"
+	"github.com/apache/incubator-devlake/plugins/zentao/tasks"
+)
+
+func TestZentaoStoryDataFlow(t *testing.T) {
+
+	var zentao impl.Zentao
+	dataflowTester := e2ehelper.NewDataFlowTester(t, "zentao", zentao)
+
+	taskData := &tasks.ZentaoTaskData{
+		Options: &tasks.ZentaoOptions{
+			ConnectionId: 1,
+			ProjectId:    1,
+			ProductId:    3,
+			ExecutionId:  1,
+		},
+	}
+
+	// import raw data table
+	dataflowTester.ImportCsvIntoRawTable("./raw_tables/_raw_zentao_api_stories.csv",
+		"_raw_zentao_api_stories")
+
+	// verify extraction
+	dataflowTester.FlushTabler(&models.ZentaoStory{})
+	dataflowTester.Subtask(tasks.ExtractStoryMeta, taskData)
+	dataflowTester.VerifyTableWithOptions(&models.ZentaoStory{}, e2ehelper.TableOptions{
+		CSVRelPath:  "./snapshot_tables/_tool_zentao_stories.csv",
+		IgnoreTypes: []interface{}{common.NoPKModel{}},
+	})
+
+	// verify conversion
+	dataflowTester.FlushTabler(&ticket.Issue{})
+	dataflowTester.FlushTabler(&ticket.BoardIssue{})
+	dataflowTester.Subtask(tasks.ConvertStoryMeta, taskData)
+	dataflowTester.VerifyTableWithOptions(&ticket.Issue{}, e2ehelper.TableOptions{
+		CSVRelPath:  "./snapshot_tables/issues_story.csv",
+		IgnoreTypes: []interface{}{common.NoPKModel{}},
+	})
+	dataflowTester.VerifyTableWithOptions(&ticket.BoardIssue{}, e2ehelper.TableOptions{
+		CSVRelPath:  "./snapshot_tables/board_issues_story.csv",
+		IgnoreTypes: []interface{}{common.NoPKModel{}},
+	})
+}
diff --git a/plugins/zentao/e2e/task_test.go b/plugins/zentao/e2e/task_test.go
new file mode 100644
index 000000000..355635259
--- /dev/null
+++ b/plugins/zentao/e2e/task_test.go
@@ -0,0 +1,68 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License.  You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package e2e
+
+import (
+	"github.com/apache/incubator-devlake/models/common"
+	"github.com/apache/incubator-devlake/models/domainlayer/ticket"
+	"testing"
+
+	"github.com/apache/incubator-devlake/helpers/e2ehelper"
+	"github.com/apache/incubator-devlake/plugins/zentao/impl"
+	"github.com/apache/incubator-devlake/plugins/zentao/models"
+	"github.com/apache/incubator-devlake/plugins/zentao/tasks"
+)
+
+func TestZentaoTaskDataFlow(t *testing.T) {
+
+	var zentao impl.Zentao
+	dataflowTester := e2ehelper.NewDataFlowTester(t, "zentao", zentao)
+
+	taskData := &tasks.ZentaoTaskData{
+		Options: &tasks.ZentaoOptions{
+			ConnectionId: 1,
+			ProjectId:    1,
+			ProductId:    3,
+			ExecutionId:  1,
+		},
+	}
+
+	// import raw data table
+	dataflowTester.ImportCsvIntoRawTable("./raw_tables/_raw_zentao_api_tasks.csv",
+		"_raw_zentao_api_tasks")
+
+	// verify extraction
+	dataflowTester.FlushTabler(&models.ZentaoTask{})
+	dataflowTester.Subtask(tasks.ExtractTaskMeta, taskData)
+	dataflowTester.VerifyTableWithOptions(&models.ZentaoTask{}, e2ehelper.TableOptions{
+		CSVRelPath:  "./snapshot_tables/_tool_zentao_tasks.csv",
+		IgnoreTypes: []interface{}{common.NoPKModel{}},
+	})
+
+	dataflowTester.FlushTabler(&ticket.Issue{})
+	dataflowTester.FlushTabler(&ticket.BoardIssue{})
+	dataflowTester.Subtask(tasks.ConvertTaskMeta, taskData)
+	dataflowTester.VerifyTableWithOptions(&ticket.Issue{}, e2ehelper.TableOptions{
+		CSVRelPath:  "./snapshot_tables/issues_task.csv",
+		IgnoreTypes: []interface{}{common.NoPKModel{}},
+	})
+	dataflowTester.VerifyTableWithOptions(&ticket.BoardIssue{}, e2ehelper.TableOptions{
+		CSVRelPath:  "./snapshot_tables/board_issues_task.csv",
+		IgnoreTypes: []interface{}{common.NoPKModel{}},
+	})
+}
diff --git a/plugins/zentao/impl/impl.go b/plugins/zentao/impl/impl.go
new file mode 100644
index 000000000..573b8eb16
--- /dev/null
+++ b/plugins/zentao/impl/impl.go
@@ -0,0 +1,136 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License.  You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package impl
+
+import (
+	"fmt"
+	"github.com/apache/incubator-devlake/errors"
+	"github.com/apache/incubator-devlake/plugins/core"
+	"github.com/apache/incubator-devlake/plugins/helper"
+	"github.com/apache/incubator-devlake/plugins/zentao/api"
+	"github.com/apache/incubator-devlake/plugins/zentao/models"
+	"github.com/apache/incubator-devlake/plugins/zentao/models/migrationscripts"
+	"github.com/apache/incubator-devlake/plugins/zentao/tasks"
+	"github.com/spf13/viper"
+	"gorm.io/gorm"
+)
+
+// make sure interface is implemented
+var _ core.PluginMeta = (*Zentao)(nil)
+var _ core.PluginInit = (*Zentao)(nil)
+var _ core.PluginTask = (*Zentao)(nil)
+var _ core.PluginApi = (*Zentao)(nil)
+var _ core.PluginBlueprintV100 = (*Zentao)(nil)
+var _ core.CloseablePluginTask = (*Zentao)(nil)
+
+type Zentao struct{}
+
+func (plugin Zentao) Description() string {
+	return "collect some Zentao data"
+}
+
+func (plugin Zentao) Init(config *viper.Viper, logger core.Logger, db *gorm.DB) errors.Error {
+	api.Init(config, logger, db)
+	return nil
+}
+
+func (plugin Zentao) SubTaskMetas() []core.SubTaskMeta {
+	// TODO add your sub task here
+	return []core.SubTaskMeta{
+		tasks.CollectProductMeta,
+		tasks.ExtractProductMeta,
+		tasks.ConvertProductMeta,
+		tasks.CollectExecutionMeta,
+		tasks.ExtractExecutionMeta,
+		tasks.ConvertExecutionMeta,
+		tasks.CollectStoryMeta,
+		tasks.ExtractStoryMeta,
+		tasks.ConvertStoryMeta,
+		tasks.CollectBugMeta,
+		tasks.ExtractBugMeta,
+		tasks.ConvertBugMeta,
+		tasks.CollectTaskMeta,
+		tasks.ExtractTaskMeta,
+		tasks.ConvertTaskMeta,
+	}
+}
+
+func (plugin Zentao) PrepareTaskData(taskCtx core.TaskContext, options map[string]interface{}) (interface{}, errors.Error) {
+	op, err := tasks.DecodeAndValidateTaskOptions(options)
+	if err != nil {
+		return nil, errors.Default.Wrap(err, "could not decode Zentao options")
+	}
+	connectionHelper := helper.NewConnectionHelper(
+		taskCtx,
+		nil,
+	)
+	connection := &models.ZentaoConnection{}
+	err = connectionHelper.FirstById(connection, op.ConnectionId)
+	if err != nil {
+		return nil, errors.Default.Wrap(err, "unable to get Zentao connection by the given connection ID: %v")
+	}
+
+	apiClient, err := tasks.NewZentaoApiClient(taskCtx, connection)
+	if err != nil {
+		return nil, errors.Default.Wrap(err, "unable to get Zentao API client instance: %v")
+	}
+
+	return &tasks.ZentaoTaskData{
+		Options:   op,
+		ApiClient: apiClient,
+	}, nil
+}
+
+// PkgPath information lost when compiled as plugin(.so)
+func (plugin Zentao) RootPkgPath() string {
+	return "github.com/apache/incubator-devlake/plugins/zentao"
+}
+
+func (plugin Zentao) MigrationScripts() []core.MigrationScript {
+	return migrationscripts.All()
+}
+
+func (plugin Zentao) ApiResources() map[string]map[string]core.ApiResourceHandler {
+	return map[string]map[string]core.ApiResourceHandler{
+		"test": {
+			"POST": api.TestConnection,
+		},
+		"connections": {
+			"POST": api.PostConnections,
+			"GET":  api.ListConnections,
+		},
+		"connections/:connectionId": {
+			"GET":    api.GetConnection,
+			"PATCH":  api.PatchConnection,
+			"DELETE": api.DeleteConnection,
+		},
+	}
+}
+
+func (plugin Zentao) MakePipelinePlan(connectionId uint64, scope []*core.BlueprintScopeV100) (core.PipelinePlan, errors.Error) {
+	return api.MakePipelinePlan(plugin.SubTaskMetas(), connectionId, scope)
+}
+
+func (plugin Zentao) Close(taskCtx core.TaskContext) errors.Error {
+	data, ok := taskCtx.GetData().(*tasks.ZentaoTaskData)
+	if !ok {
+		return errors.Default.New(fmt.Sprintf("GetData failed when try to close %+v", taskCtx))
+	}
+	data.ApiClient.Release()
+	return nil
+}
diff --git a/plugins/zentao/models/access_token.go b/plugins/zentao/models/access_token.go
new file mode 100644
index 000000000..60bf26178
--- /dev/null
+++ b/plugins/zentao/models/access_token.go
@@ -0,0 +1,27 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License.  You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package models
+
+type ApiAccessTokenRequest struct {
+	Account  string `json:"account"`
+	Password string `json:"password"`
+}
+
+type ApiAccessTokenResponse struct {
+	Token string `json:"token"`
+}
diff --git a/plugins/zentao/models/archived/bug.go b/plugins/zentao/models/archived/bug.go
new file mode 100644
index 000000000..a316cd809
--- /dev/null
+++ b/plugins/zentao/models/archived/bug.go
@@ -0,0 +1,99 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License.  You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package archived
+
+import (
+	"github.com/apache/incubator-devlake/models/migrationscripts/archived"
+	"time"
+)
+
+type ZentaoBug struct {
+	archived.NoPKModel
+	ConnectionId   uint64     `gorm:"primaryKey;type:BIGINT  NOT NULL"`
+	ID             uint64     `json:"id" gorm:"primaryKey;type:BIGINT  NOT NULL"`
+	Project        uint64     `json:"project"`
+	Product        uint64     `json:"product"`
+	Injection      int        `json:"injection"`
+	Identify       int        `json:"identify"`
+	Branch         int        `json:"branch"`
+	Module         int        `json:"module"`
+	Execution      uint64     `json:"execution"`
+	Plan           int        `json:"plan"`
+	Story          uint64     `json:"story"`
+	StoryVersion   int        `json:"storyVersion"`
+	Task           int        `json:"task"`
+	ToTask         int        `json:"toTask"`
+	ToStory        uint64     `json:"toStory"`
+	Title          string     `json:"title"`
+	Keywords       string     `json:"keywords"`
+	Severity       int        `json:"severity"`
+	Pri            int        `json:"pri"`
+	Type           string     `json:"type"`
+	Os             string     `json:"os"`
+	Browser        string     `json:"browser"`
+	Hardware       string     `json:"hardware"`
+	Found          string     `json:"found"`
+	Steps          string     `json:"steps"`
+	Status         string     `json:"status"`
+	SubStatus      string     `json:"subStatus"`
+	Color          string     `json:"color"`
+	Confirmed      int        `json:"confirmed"`
+	ActivatedCount int        `json:"activatedCount"`
+	ActivatedDate  *time.Time `json:"activatedDate"`
+	FeedbackBy     string     `json:"feedbackBy"`
+	NotifyEmail    string     `json:"notifyEmail"`
+	OpenedById     uint64
+	OpenedByName   string
+	OpenedDate     *time.Time `json:"openedDate"`
+	OpenedBuild    string     `json:"openedBuild"`
+	AssignedToId   uint64
+	AssignedToName string
+	AssignedDate   *time.Time `json:"assignedDate"`
+	Deadline       string     `json:"deadline"`
+	ResolvedById   uint64
+	Resolution     string     `json:"resolution"`
+	ResolvedBuild  string     `json:"resolvedBuild"`
+	ResolvedDate   *time.Time `json:"resolvedDate"`
+	ClosedById     uint64
+	ClosedDate     *time.Time `json:"closedDate"`
+	DuplicateBug   int        `json:"duplicateBug"`
+	LinkBug        string     `json:"linkBug"`
+	Feedback       int        `json:"feedback"`
+	Result         int        `json:"result"`
+	Repo           int        `json:"repo"`
+	Mr             int        `json:"mr"`
+	Entry          string     `json:"entry"`
+	Lines          string     `json:"lines"`
+	V1             string     `json:"v1"`
+	V2             string     `json:"v2"`
+	RepoType       string     `json:"repoType"`
+	IssueKey       string     `json:"issueKey"`
+	Testtask       int        `json:"testtask"`
+	LastEditedById uint64
+	LastEditedDate *time.Time `json:"lastEditedDate"`
+	Deleted        bool       `json:"deleted"`
+	PriOrder       string     `json:"priOrder"`
+	SeverityOrder  int        `json:"severityOrder"`
+	Needconfirm    bool       `json:"needconfirm"`
+	StatusName     string     `json:"statusName"`
+	ProductStatus  string     `json:"productStatus"`
+}
+
+func (ZentaoBug) TableName() string {
+	return "_tool_zentao_bugs"
+}
diff --git a/plugins/zentao/models/archived/connection.go b/plugins/zentao/models/archived/connection.go
new file mode 100644
index 000000000..5a89da666
--- /dev/null
+++ b/plugins/zentao/models/archived/connection.go
@@ -0,0 +1,70 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License.  You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package archived
+
+import (
+	"github.com/apache/incubator-devlake/models/migrationscripts/archived"
+)
+
+// TODO Please modify the following code to fit your needs
+// This object conforms to what the frontend currently sends.
+type ZentaoConnection struct {
+	RestConnection `mapstructure:",squash"`
+	//TODO you may need to use helper.BasicAuth instead of helper.AccessToken
+	BasicAuth `mapstructure:",squash"`
+}
+
+type TestConnectionRequest struct {
+	Endpoint  string `json:"endpoint"`
+	Proxy     string `json:"proxy"`
+	BasicAuth `mapstructure:",squash"`
+}
+
+// This object conforms to what the frontend currently expects.
+type ZentaoResponse struct {
+	Name string `json:"name"`
+	ID   uint64 `json:"id"`
+	ZentaoConnection
+}
+
+// Using User because it requires authentication.
+type ApiUserResponse struct {
+	Id   uint64
+	Name string `json:"name"`
+}
+
+func (ZentaoConnection) TableName() string {
+	return "_tool_zentao_connections"
+}
+
+type BasicAuth struct {
+	Username string `mapstructure:"username" validate:"required" json:"username"`
+	Password string `mapstructure:"password" validate:"required" json:"password"`
+}
+
+type RestConnection struct {
+	BaseConnection   `mapstructure:",squash"`
+	Endpoint         string `mapstructure:"endpoint" validate:"required" json:"endpoint"`
+	Proxy            string `mapstructure:"proxy" json:"proxy"`
+	RateLimitPerHour int    `comment:"api request rate limt per hour" json:"rateLimit"`
+}
+
+type BaseConnection struct {
+	Name string `gorm:"type:varchar(100);uniqueIndex" json:"name" validate:"required"`
+	archived.Model
+}
diff --git a/plugins/zentao/models/archived/execution.go b/plugins/zentao/models/archived/execution.go
new file mode 100644
index 000000000..cca92bd95
--- /dev/null
+++ b/plugins/zentao/models/archived/execution.go
@@ -0,0 +1,90 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License.  You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package archived
+
+import (
+	"github.com/apache/incubator-devlake/models/migrationscripts/archived"
+	"time"
+)
+
+type ZentaoExecution struct {
+	ConnectionId   uint64 `gorm:"primaryKey;type:BIGINT  NOT NULL"`
+	Id             uint64 `json:"id" gorm:"primaryKey;type:BIGINT  NOT NULL"`
+	Project        uint64 `json:"project"`
+	Model          string `json:"model"`
+	Type           string `json:"type"`
+	Lifetime       string `json:"lifetime"`
+	Budget         string `json:"budget"`
+	BudgetUnit     string `json:"budgetUnit"`
+	Attribute      string `json:"attribute"`
+	Percent        int    `json:"percent"`
+	Milestone      string `json:"milestone"`
+	Output         string `json:"output"`
+	Auth           string `json:"auth"`
+	Parent         uint64 `json:"parent"`
+	Path           string `json:"path"`
+	Grade          int    `json:"grade"`
+	Name           string `json:"name"`
+	Code           string `json:"code"`
+	PlanBegin      string `json:"begin"`
+	PlanEnd        string `json:"end"`
+	RealBegan      string `json:"realBegan"`
+	RealEnd        string `json:"realEnd"`
+	Days           int    `json:"days"`
+	Status         string `json:"status"`
+	SubStatus      string `json:"subStatus"`
+	Pri            string `json:"pri"`
+	Description    string `json:"desc"`
+	Version        int    `json:"version"`
+	ParentVersion  int    `json:"parentVersion"`
+	PlanDuration   int    `json:"planDuration"`
+	RealDuration   int    `json:"realDuration"`
+	OpenedById     uint64
+	OpenedDate     *time.Time `json:"openedDate"`
+	OpenedVersion  string     `json:"openedVersion"`
+	LastEditedById uint64
+	LastEditedDate *time.Time `json:"lastEditedDate"`
+	ClosedById     uint64
+	ClosedDate     *time.Time `json:"closedDate"`
+	CanceledById   uint64
+	CanceledDate   *time.Time `json:"canceledDate"`
+	SuspendedDate  string     `json:"suspendedDate"`
+	POId           uint64
+	PMId           uint64
+	QDId           uint64
+	RDId           uint64
+	Team           string `json:"team"`
+	Acl            string `json:"acl"`
+	OrderIn        int    `json:"order"`
+	Vision         string `json:"vision"`
+	DisplayCards   int    `json:"displayCards"`
+	FluidBoard     string `json:"fluidBoard"`
+	Deleted        bool   `json:"deleted"`
+	TotalHours     int    `json:"totalHours"`
+	TotalEstimate  int    `json:"totalEstimate"`
+	TotalConsumed  int    `json:"totalConsumed"`
+	TotalLeft      int    `json:"totalLeft"`
+	ProjectId      uint64
+	Progress       int  `json:"progress"`
+	CaseReview     bool `json:"caseReview"`
+	archived.NoPKModel
+}
+
+func (ZentaoExecution) TableName() string {
+	return "_tool_zentao_executions"
+}
diff --git a/plugins/zentao/models/archived/product.go b/plugins/zentao/models/archived/product.go
new file mode 100644
index 000000000..c6977a099
--- /dev/null
+++ b/plugins/zentao/models/archived/product.go
@@ -0,0 +1,62 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License.  You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package archived
+
+import (
+	"github.com/apache/incubator-devlake/models/migrationscripts/archived"
+	"time"
+)
+
+type ZentaoProduct struct {
+	ConnectionId   uint64 `gorm:"primaryKey;type:BIGINT  NOT NULL"`
+	Id             uint64 `json:"id" gorm:"primaryKey;type:BIGINT  NOT NULL"`
+	Program        int    `json:"program"`
+	Name           string `json:"name"`
+	Code           string `json:"code"`
+	Bind           string `json:"bind"`
+	Line           int    `json:"line"`
+	Type           string `json:"type"`
+	Status         string `json:"status"`
+	SubStatus      string `json:"subStatus"`
+	Description    string `json:"desc"`
+	POId           uint64
+	QDId           uint64
+	RDId           uint64
+	Acl            string `json:"acl"`
+	Reviewer       string `json:"reviewer"`
+	CreatedById    uint64
+	CreatedDate    *time.Time `json:"createdDate"`
+	CreatedVersion string     `json:"createdVersion"`
+	OrderIn        int        `json:"order"`
+	Deleted        string     `json:"deleted"`
+	Plans          int        `json:"plans"`
+	Releases       int        `json:"releases"`
+	Builds         int        `json:"builds"`
+	Cases          int        `json:"cases"`
+	Projects       int        `json:"projects"`
+	Executions     int        `json:"executions"`
+	Bugs           int        `json:"bugs"`
+	Docs           int        `json:"docs"`
+	Progress       int        `json:"progress"`
+	CaseReview     bool       `json:"caseReview"`
+	archived.NoPKModel
+}
+
+func (ZentaoProduct) TableName() string {
+	return "_tool_zentao_products"
+}
diff --git a/plugins/zentao/models/archived/project.go b/plugins/zentao/models/archived/project.go
new file mode 100644
index 000000000..8d884420b
--- /dev/null
+++ b/plugins/zentao/models/archived/project.go
@@ -0,0 +1,113 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License.  You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package archived
+
+import (
+	"github.com/apache/incubator-devlake/models/migrationscripts/archived"
+	"time"
+)
+
+type ZentaoProject struct {
+	archived.NoPKModel
+	ConnectionId  uint64 `gorm:"primaryKey;type:BIGINT  NOT NULL"`
+	ID            uint64 `json:"id" gorm:"primaryKey;type:BIGINT  NOT NULL"`
+	Project       uint64 `json:"project"`
+	Model         string `json:"model"`
+	Type          string `json:"type"`
+	Lifetime      string `json:"lifetime"`
+	Budget        string `json:"budget"`
+	BudgetUnit    string `json:"budgetUnit"`
+	Attribute     string `json:"attribute"`
+	Percent       int    `json:"percent"`
+	Milestone     string `json:"milestone"`
+	Output        string `json:"output"`
+	Auth          string `json:"auth"`
+	Parent        uint64 `json:"parent"`
+	Path          string `json:"path"`
+	Grade         int    `json:"grade"`
+	Name          string `json:"name"`
+	Code          string `json:"code"`
+	PlanBegin     string `json:"begin"`
+	PlanEnd       string `json:"end"`
+	RealBegan     string `json:"realBegan"`
+	RealEnd       string `json:"realEnd"`
+	Days          int    `json:"days"`
+	Status        string `json:"status"`
+	SubStatus     string `json:"subStatus"`
+	Pri           string `json:"pri"`
+	Description   string `json:"desc"`
+	Version       int    `json:"version"`
+	ParentVersion int    `json:"parentVersion"`
+	PlanDuration  int    `json:"planDuration"`
+	RealDuration  int    `json:"realDuration"`
+	//OpenedBy       string    `json:"openedBy"`
+	OpenedDate     *time.Time  `json:"openedDate"`
+	OpenedVersion  string      `json:"openedVersion"`
+	LastEditedBy   string      `json:"lastEditedBy"`
+	LastEditedDate **time.Time `json:"lastEditedDate"`
+	ClosedBy       string      `json:"closedBy"`
+	ClosedDate     **time.Time `json:"closedDate"`
+	CanceledBy     string      `json:"canceledBy"`
+	CanceledDate   **time.Time `json:"canceledDate"`
+	SuspendedDate  string      `json:"suspendedDate"`
+	PO             string      `json:"PO"`
+	PM             `json:"PM"`
+	QD             string `json:"QD"`
+	RD             string `json:"RD"`
+	Team           string `json:"team"`
+	Acl            string `json:"acl"`
+	Whitelist      `json:"whitelist" gorm:"-"`
+	OrderIn        int    `json:"order"`
+	Vision         string `json:"vision"`
+	DisplayCards   int    `json:"displayCards"`
+	FluidBoard     string `json:"fluidBoard"`
+	Deleted        bool   `json:"deleted"`
+	Delay          int    `json:"delay"`
+	Hours          `json:"hours"`
+	TeamCount      int    `json:"teamCount"`
+	LeftTasks      string `json:"leftTasks"`
+	//TeamMembers   []interface{} `json:"teamMembers" gorm:"-"`
+	TotalEstimate int `json:"totalEstimate"`
+	TotalConsumed int `json:"totalConsumed"`
+	TotalLeft     int `json:"totalLeft"`
+	Progress      int `json:"progress"`
+	TotalReal     int `json:"totalReal"`
+}
+type PM struct {
+	PmId       uint64 `json:"id"`
+	PmAccount  string `json:"account"`
+	PmAvatar   string `json:"avatar"`
+	PmRealname string `json:"realname"`
+}
+type Whitelist []struct {
+	WhitelistID       uint64 `json:"id"`
+	WhitelistAccount  string `json:"account"`
+	WhitelistAvatar   string `json:"avatar"`
+	WhitelistRealname string `json:"realname"`
+}
+type Hours struct {
+	HoursTotalEstimate int `json:"totalEstimate"`
+	HoursTotalConsumed int `json:"totalConsumed"`
+	HoursTotalLeft     int `json:"totalLeft"`
+	HoursProgress      int `json:"progress"`
+	HoursTotalReal     int `json:"totalReal"`
+}
+
+func (ZentaoProject) TableName() string {
+	return "_tool_zentao_projects"
+}
diff --git a/plugins/zentao/models/archived/story.go b/plugins/zentao/models/archived/story.go
new file mode 100644
index 000000000..770ecdfd8
--- /dev/null
+++ b/plugins/zentao/models/archived/story.go
@@ -0,0 +1,89 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License.  You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package archived
+
+import (
+	"github.com/apache/incubator-devlake/models/migrationscripts/archived"
+	"time"
+)
+
+type ZentaoStory struct {
+	archived.NoPKModel
+	ConnectionId uint64 `gorm:"primaryKey;type:BIGINT  NOT NULL"`
+	ID           uint64 `json:"id" gorm:"primaryKey;type:BIGINT  NOT NULL" `
+	Project      uint64 `json:"project"`
+	Product      uint64 `json:"product"`
+	Branch       int    `json:"branch"`
+	Version      int    `json:"version"`
+	OrderIn      int    `json:"order"`
+	Vision       string `json:"vision"`
+	Parent       uint64 `json:"parent"`
+	Module       int    `json:"module"`
+	Plan         string `json:"plan"`
+	Source       string `json:"source"`
+	SourceNote   string `json:"sourceNote"`
+	FromBug      int    `json:"fromBug"`
+	Feedback     int    `json:"feedback"`
+	FeedbackBy   string `json:"feedbackBy"`
+	Title        string `json:"title"`
+	Keywords     string `json:"keywords"`
+	Type         string `json:"type"`
+	Category     string `json:"category"`
+	Pri          int    `json:"pri"`
+	Estimate     int    `json:"estimate"`
+	Status       string `json:"status"`
+	SubStatus    string `json:"subStatus"`
+	Color        string `json:"color"`
+	Stage        string `json:"stage"`
+	StagedById   uint64 `json:"stagedBy"`
+	//Mailto           []interface{} `json:"mailto"`
+	Lib              int    `json:"lib"`
+	FromStory        uint64 `json:"fromStory"`
+	FromVersion      int    `json:"fromVersion"`
+	OpenedById       uint64
+	OpenedByName     string
+	OpenedDate       *time.Time `json:"openedDate"`
+	AssignedToId     uint64
+	AssignedToName   string
+	AssignedDate     *time.Time `json:"assignedDate"`
+	ApprovedDate     string     `json:"approvedDate"`
+	LastEditedId     uint64
+	LastEditedDate   *time.Time `json:"lastEditedDate"`
+	ChangedDate      string     `json:"changedDate"`
+	ReviewedById     uint64     `json:"reviewedBy"`
+	ReviewedDate     *time.Time `json:"reviewedDate"`
+	ClosedId         uint64
+	ClosedDate       *time.Time `json:"closedDate"`
+	ClosedReason     string     `json:"closedReason"`
+	ActivatedDate    string     `json:"activatedDate"`
+	ToBug            int        `json:"toBug"`
+	ChildStories     string     `json:"childStories"`
+	LinkStories      string     `json:"linkStories"`
+	LinkRequirements string     `json:"linkRequirements"`
+	DuplicateStory   uint64     `json:"duplicateStory"`
+	StoryChanged     string     `json:"storyChanged"`
+	NotifyEmail      string     `json:"notifyEmail"`
+	URChanged        string     `json:"URChanged"`
+	Deleted          bool       `json:"deleted"`
+	PriOrder         string     `json:"priOrder"`
+	PlanTitle        string     `json:"planTitle"`
+}
+
+func (ZentaoStory) TableName() string {
+	return "_tool_zentao_stories"
+}
diff --git a/plugins/zentao/models/archived/task.go b/plugins/zentao/models/archived/task.go
new file mode 100644
index 000000000..4fd3533d3
--- /dev/null
+++ b/plugins/zentao/models/archived/task.go
@@ -0,0 +1,97 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License.  You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package archived
+
+import (
+	"github.com/apache/incubator-devlake/models/migrationscripts/archived"
+	"time"
+)
+
+type ZentaoTask struct {
+	archived.NoPKModel
+	ConnectionId  uint64 `gorm:"primaryKey;type:BIGINT  NOT NULL"`
+	ExecutionId   uint64 `json:"execution_id"`
+	ID            uint64 `json:"id" gorm:"primaryKey;type:BIGINT  NOT NULL"`
+	Project       uint64 `json:"project"`
+	Parent        uint64 `json:"parent"`
+	Execution     uint64 `json:"execution"`
+	Module        int    `json:"module"`
+	Design        int    `json:"design"`
+	Story         uint64 `json:"story"`
+	StoryVersion  int    `json:"storyVersion"`
+	DesignVersion int    `json:"designVersion"`
+	FromBug       int    `json:"fromBug"`
+	Feedback      int    `json:"feedback"`
+	FromIssue     int    `json:"fromIssue"`
+	Name          string `json:"name"`
+	Type          string `json:"type"`
+	Mode          string `json:"mode"`
+	Pri           int    `json:"pri"`
+	Estimate      int    `json:"estimate"`
+	Consumed      int    `json:"consumed"`
+	Deadline      string `json:"deadline"`
+	Status        string `json:"status"`
+	SubStatus     string `json:"subStatus"`
+	Color         string `json:"color"`
+	//Mailto        interface{} `json:"mailto"`
+	Description        string `json:"desc"`
+	Version            int    `json:"version"`
+	OpenedById         uint64
+	OpenedByName       string
+	OpenedDate         *time.Time `json:"openedDate"`
+	AssignedToId       uint64
+	AssignedToName     string
+	AssignedDate       *time.Time `json:"assignedDate"`
+	EstStarted         string     `json:"estStarted"`
+	RealStarted        *time.Time `json:"realStarted"`
+	FinishedId         uint64
+	FinishedDate       *time.Time `json:"finishedDate"`
+	FinishedList       string     `json:"finishedList"`
+	CanceledId         uint64
+	CanceledDate       *time.Time `json:"canceledDate"`
+	ClosedById         uint64
+	ClosedDate         *time.Time `json:"closedDate"`
+	PlanDuration       int        `json:"planDuration"`
+	RealDuration       int        `json:"realDuration"`
+	ClosedReason       string     `json:"closedReason"`
+	LastEditedId       uint64
+	LastEditedDate     *time.Time `json:"lastEditedDate"`
+	ActivatedDate      string     `json:"activatedDate"`
+	OrderIn            int        `json:"order"`
+	Repo               int        `json:"repo"`
+	Mr                 int        `json:"mr"`
+	Entry              string     `json:"entry"`
+	Lines              string     `json:"lines"`
+	V1                 string     `json:"v1"`
+	V2                 string     `json:"v2"`
+	Deleted            bool       `json:"deleted"`
+	Vision             string     `json:"vision"`
+	StoryID            uint64     `json:"storyID"`
+	StoryTitle         string     `json:"storyTitle"`
+	Branch             int        `json:"branch"`
+	LatestStoryVersion int        `json:"latestStoryVersion"`
+	StoryStatus        string     `json:"storyStatus"`
+	AssignedToRealName string     `json:"assignedToRealName"`
+	PriOrder           string     `json:"priOrder"`
+	NeedConfirm        bool       `json:"needConfirm"`
+	Progress           int        `json:"progress"`
+}
+
+func (ZentaoTask) TableName() string {
+	return "_tool_zentao_tasks"
+}
diff --git a/plugins/zentao/models/bug.go b/plugins/zentao/models/bug.go
new file mode 100644
index 000000000..ed0d65599
--- /dev/null
+++ b/plugins/zentao/models/bug.go
@@ -0,0 +1,192 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License.  You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package models
+
+import (
+	"github.com/apache/incubator-devlake/models/common"
+	"time"
+)
+
+type ZentaoBugRes struct {
+	ID             uint64     `json:"id"`
+	Project        uint64     `json:"project"`
+	Product        uint64     `json:"product"`
+	Injection      int        `json:"injection"`
+	Identify       int        `json:"identify"`
+	Branch         int        `json:"branch"`
+	Module         int        `json:"module"`
+	Execution      uint64     `json:"execution"`
+	Plan           int        `json:"plan"`
+	Story          uint64     `json:"story"`
+	StoryVersion   int        `json:"storyVersion"`
+	Task           int        `json:"task"`
+	ToTask         int        `json:"toTask"`
+	ToStory        uint64     `json:"toStory"`
+	Title          string     `json:"title"`
+	Keywords       string     `json:"keywords"`
+	Severity       int        `json:"severity"`
+	Pri            int        `json:"pri"`
+	Type           string     `json:"type"`
+	Os             string     `json:"os"`
+	Browser        string     `json:"browser"`
+	Hardware       string     `json:"hardware"`
+	Found          string     `json:"found"`
+	Steps          string     `json:"steps"`
+	Status         string     `json:"status"`
+	SubStatus      string     `json:"subStatus"`
+	Color          string     `json:"color"`
+	Confirmed      int        `json:"confirmed"`
+	ActivatedCount int        `json:"activatedCount"`
+	ActivatedDate  *time.Time `json:"activatedDate"`
+	FeedbackBy     string     `json:"feedbackBy"`
+	NotifyEmail    string     `json:"notifyEmail"`
+	OpenedBy       struct {
+		ID       uint64 `json:"id"`
+		Account  string `json:"account"`
+		Avatar   string `json:"avatar"`
+		Realname string `json:"realname"`
+	} `json:"openedBy"`
+	OpenedDate  *time.Time `json:"openedDate"`
+	OpenedBuild string     `json:"openedBuild"`
+	AssignedTo  struct {
+		ID       uint64 `json:"id"`
+		Account  string `json:"account"`
+		Avatar   string `json:"avatar"`
+		Realname string `json:"realname"`
+	} `json:"assignedTo"`
+	AssignedDate *time.Time `json:"assignedDate"`
+	Deadline     string     `json:"deadline"`
+	ResolvedBy   struct {
+		ID       uint64 `json:"id"`
+		Account  string `json:"account"`
+		Avatar   string `json:"avatar"`
+		Realname string `json:"realname"`
+	} `json:"resolvedBy"`
+	Resolution    string     `json:"resolution"`
+	ResolvedBuild string     `json:"resolvedBuild"`
+	ResolvedDate  *time.Time `json:"resolvedDate"`
+	ClosedBy      struct {
+		ID       uint64 `json:"id"`
+		Account  string `json:"account"`
+		Avatar   string `json:"avatar"`
+		Realname string `json:"realname"`
+	} `json:"closedBy"`
+	ClosedDate   *time.Time `json:"closedDate"`
+	DuplicateBug int        `json:"duplicateBug"`
+	LinkBug      string     `json:"linkBug"`
+	Feedback     int        `json:"feedback"`
+	Result       int        `json:"result"`
+	Repo         int        `json:"repo"`
+	Mr           int        `json:"mr"`
+	Entry        string     `json:"entry"`
+	Lines        string     `json:"lines"`
+	V1           string     `json:"v1"`
+	V2           string     `json:"v2"`
+	RepoType     string     `json:"repoType"`
+	IssueKey     string     `json:"issueKey"`
+	Testtask     int        `json:"testtask"`
+	LastEditedBy struct {
+		ID       uint64 `json:"id"`
+		Account  string `json:"account"`
+		Avatar   string `json:"avatar"`
+		Realname string `json:"realname"`
+	} `json:"lastEditedBy"`
+	LastEditedDate *time.Time `json:"lastEditedDate"`
+	Deleted        bool       `json:"deleted"`
+	PriOrder       string     `json:"priOrder"`
+	SeverityOrder  int        `json:"severityOrder"`
+	Needconfirm    bool       `json:"needconfirm"`
+	StatusName     string     `json:"statusName"`
+	ProductStatus  string     `json:"productStatus"`
+}
+
+type ZentaoBug struct {
+	common.NoPKModel
+	ConnectionId   uint64     `gorm:"primaryKey;type:BIGINT  NOT NULL"`
+	ID             uint64     `json:"id" gorm:"primaryKey;type:BIGINT  NOT NULL"`
+	Project        uint64     `json:"project"`
+	Product        uint64     `json:"product"`
+	Injection      int        `json:"injection"`
+	Identify       int        `json:"identify"`
+	Branch         int        `json:"branch"`
+	Module         int        `json:"module"`
+	Execution      uint64     `json:"execution"`
+	Plan           int        `json:"plan"`
+	Story          uint64     `json:"story"`
+	StoryVersion   int        `json:"storyVersion"`
+	Task           int        `json:"task"`
+	ToTask         int        `json:"toTask"`
+	ToStory        uint64     `json:"toStory"`
+	Title          string     `json:"title"`
+	Keywords       string     `json:"keywords"`
+	Severity       int        `json:"severity"`
+	Pri            int        `json:"pri"`
+	Type           string     `json:"type"`
+	Os             string     `json:"os"`
+	Browser        string     `json:"browser"`
+	Hardware       string     `json:"hardware"`
+	Found          string     `json:"found"`
+	Steps          string     `json:"steps"`
+	Status         string     `json:"status"`
+	SubStatus      string     `json:"subStatus"`
+	Color          string     `json:"color"`
+	Confirmed      int        `json:"confirmed"`
+	ActivatedCount int        `json:"activatedCount"`
+	ActivatedDate  *time.Time `json:"activatedDate"`
+	FeedbackBy     string     `json:"feedbackBy"`
+	NotifyEmail    string     `json:"notifyEmail"`
+	OpenedById     uint64
+	OpenedByName   string
+	OpenedDate     *time.Time `json:"openedDate"`
+	OpenedBuild    string     `json:"openedBuild"`
+	AssignedToId   uint64
+	AssignedToName string
+	AssignedDate   *time.Time `json:"assignedDate"`
+	Deadline       string     `json:"deadline"`
+	ResolvedById   uint64
+	Resolution     string     `json:"resolution"`
+	ResolvedBuild  string     `json:"resolvedBuild"`
+	ResolvedDate   *time.Time `json:"resolvedDate"`
+	ClosedById     uint64
+	ClosedDate     *time.Time `json:"closedDate"`
+	DuplicateBug   int        `json:"duplicateBug"`
+	LinkBug        string     `json:"linkBug"`
+	Feedback       int        `json:"feedback"`
+	Result         int        `json:"result"`
+	Repo           int        `json:"repo"`
+	Mr             int        `json:"mr"`
+	Entry          string     `json:"entry"`
+	Lines          string     `json:"lines"`
+	V1             string     `json:"v1"`
+	V2             string     `json:"v2"`
+	RepoType       string     `json:"repoType"`
+	IssueKey       string     `json:"issueKey"`
+	Testtask       int        `json:"testtask"`
+	LastEditedById uint64
+	LastEditedDate *time.Time `json:"lastEditedDate"`
+	Deleted        bool       `json:"deleted"`
+	PriOrder       string     `json:"priOrder"`
+	SeverityOrder  int        `json:"severityOrder"`
+	Needconfirm    bool       `json:"needconfirm"`
+	StatusName     string     `json:"statusName"`
+	ProductStatus  string     `json:"productStatus"`
+}
+
+func (ZentaoBug) TableName() string {
+	return "_tool_zentao_bugs"
+}
diff --git a/plugins/zentao/models/connection.go b/plugins/zentao/models/connection.go
new file mode 100644
index 000000000..e25f0c3d5
--- /dev/null
+++ b/plugins/zentao/models/connection.go
@@ -0,0 +1,51 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License.  You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package models
+
+import "github.com/apache/incubator-devlake/plugins/helper"
+
+// TODO Please modify the following code to fit your needs
+// This object conforms to what the frontend currently sends.
+type ZentaoConnection struct {
+	helper.RestConnection `mapstructure:",squash"`
+	//TODO you may need to use helper.BasicAuth instead of helper.AccessToken
+	helper.BasicAuth `mapstructure:",squash"`
+}
+
+type TestConnectionRequest struct {
+	Endpoint         string `json:"endpoint"`
+	Proxy            string `json:"proxy"`
+	helper.BasicAuth `mapstructure:",squash"`
+}
+
+// This object conforms to what the frontend currently expects.
+type ZentaoResponse struct {
+	Name string `json:"name"`
+	ID   uint64 `json:"id"`
+	ZentaoConnection
+}
+
+// Using User because it requires authentication.
+type ApiUserResponse struct {
+	Id   uint64
+	Name string `json:"name"`
+}
+
+func (ZentaoConnection) TableName() string {
+	return "_tool_zentao_connections"
+}
diff --git a/plugins/zentao/models/execution.go b/plugins/zentao/models/execution.go
new file mode 100644
index 000000000..38768ae96
--- /dev/null
+++ b/plugins/zentao/models/execution.go
@@ -0,0 +1,274 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License.  You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package models
+
+import (
+	"github.com/apache/incubator-devlake/models/common"
+	"time"
+)
+
+type ZentaoExecutionRes struct {
+	ID            uint64     `json:"id"`
+	Project       uint64     `json:"project"`
+	Model         string     `json:"model"`
+	Type          string     `json:"type"`
+	Lifetime      string     `json:"lifetime"`
+	Budget        string     `json:"budget"`
+	BudgetUnit    string     `json:"budgetUnit"`
+	Attribute     string     `json:"attribute"`
+	Percent       int        `json:"percent"`
+	Milestone     string     `json:"milestone"`
+	Output        string     `json:"output"`
+	Auth          string     `json:"auth"`
+	Parent        uint64     `json:"parent"`
+	Path          string     `json:"path"`
+	Grade         int        `json:"grade"`
+	Name          string     `json:"name"`
+	Code          string     `json:"code"`
+	PlanBegin     string     `json:"begin"`
+	PlanEnd       string     `json:"end"`
+	RealBegan     *time.Time `json:"realBegan"`
+	RealEnd       *time.Time `json:"realEnd"`
+	Days          int        `json:"days"`
+	Status        string     `json:"status"`
+	SubStatus     string     `json:"subStatus"`
+	Pri           string     `json:"pri"`
+	Description   string     `json:"desc"`
+	Version       int        `json:"version"`
+	ParentVersion int        `json:"parentVersion"`
+	PlanDuration  int        `json:"planDuration"`
+	RealDuration  int        `json:"realDuration"`
+	OpenedBy      struct {
+		ID       uint64 `json:"id"`
+		Account  string `json:"account"`
+		Avatar   string `json:"avatar"`
+		Realname string `json:"realname"`
+	} `json:"openedBy"`
+	OpenedDate    *time.Time `json:"openedDate"`
+	OpenedVersion string     `json:"openedVersion"`
+	LastEditedBy  struct {
+		ID       uint64 `json:"id"`
+		Account  string `json:"account"`
+		Avatar   string `json:"avatar"`
+		Realname string `json:"realname"`
+	} `json:"lastEditedBy"`
+	LastEditedDate *time.Time `json:"lastEditedDate"`
+	ClosedBy       struct {
+		ID       uint64 `json:"id"`
+		Account  string `json:"account"`
+		Avatar   string `json:"avatar"`
+		Realname string `json:"realname"`
+	} `json:"closedBy"`
+	ClosedDate *time.Time `json:"closedDate"`
+	CanceledBy struct {
+		ID       uint64 `json:"id"`
+		Account  string `json:"account"`
+		Avatar   string `json:"avatar"`
+		Realname string `json:"realname"`
+	} `json:"canceledBy"`
+	CanceledDate  *time.Time `json:"canceledDate"`
+	SuspendedDate string     `json:"suspendedDate"`
+	PO            struct {
+		ID       uint64 `json:"id"`
+		Account  string `json:"account"`
+		Avatar   string `json:"avatar"`
+		Realname string `json:"realname"`
+	} `json:"PO"`
+	PM struct {
+		ID       uint64 `json:"id"`
+		Account  string `json:"account"`
+		Avatar   string `json:"avatar"`
+		Realname string `json:"realname"`
+	} `json:"PM"`
+	QD struct {
+		ID       uint64 `json:"id"`
+		Account  string `json:"account"`
+		Avatar   string `json:"avatar"`
+		Realname string `json:"realname"`
+	} `json:"QD"`
+	RD struct {
+		ID       uint64 `json:"id"`
+		Account  string `json:"account"`
+		Avatar   string `json:"avatar"`
+		Realname string `json:"realname"`
+	} `json:"RD"`
+	Team      string `json:"team"`
+	Acl       string `json:"acl"`
+	Whitelist []struct {
+		ID       uint64 `json:"id"`
+		Account  string `json:"account"`
+		Avatar   string `json:"avatar"`
+		Realname string `json:"realname"`
+	} `json:"whitelist"`
+	OrderIn       int    `json:"order"`
+	Vision        string `json:"vision"`
+	DisplayCards  int    `json:"displayCards"`
+	FluidBoard    string `json:"fluidBoard"`
+	Deleted       bool   `json:"deleted"`
+	TotalHours    int    `json:"totalHours"`
+	TotalEstimate int    `json:"totalEstimate"`
+	TotalConsumed int    `json:"totalConsumed"`
+	TotalLeft     int    `json:"totalLeft"`
+	ProjectInfo   struct {
+		ID             uint64 `json:"id"`
+		Project        uint64 `json:"project"`
+		Model          string `json:"model"`
+		Type           string `json:"type"`
+		Lifetime       string `json:"lifetime"`
+		Budget         string `json:"budget"`
+		BudgetUnit     string `json:"budgetUnit"`
+		Attribute      string `json:"attribute"`
+		Percent        int    `json:"percent"`
+		Milestone      string `json:"milestone"`
+		Output         string `json:"output"`
+		Auth           string `json:"auth"`
+		Parent         uint64 `json:"parent"`
+		Path           string `json:"path"`
+		Grade          int    `json:"grade"`
+		Name           string `json:"name"`
+		Code           string `json:"code"`
+		PlanBegin      string `json:"begin"`
+		PlanEnd        string `json:"end"`
+		RealBegan      string `json:"realBegan"`
+		RealEnd        string `json:"realEnd"`
+		Days           int    `json:"days"`
+		Status         string `json:"status"`
+		SubStatus      string `json:"subStatus"`
+		Pri            string `json:"pri"`
+		Description    string `json:"desc"`
+		Version        int    `json:"version"`
+		ParentVersion  int    `json:"parentVersion"`
+		PlanDuration   int    `json:"planDuration"`
+		RealDuration   int    `json:"realDuration"`
+		OpenedBy       string `json:"openedBy"`
+		OpenedDate     string `json:"openedDate"`
+		OpenedVersion  string `json:"openedVersion"`
+		LastEditedBy   string `json:"lastEditedBy"`
+		LastEditedDate string `json:"lastEditedDate"`
+		ClosedBy       string `json:"closedBy"`
+		ClosedDate     string `json:"closedDate"`
+		CanceledBy     string `json:"canceledBy"`
+		CanceledDate   string `json:"canceledDate"`
+		SuspendedDate  string `json:"suspendedDate"`
+		PO             string `json:"PO"`
+		PM             string `json:"PM"`
+		QD             string `json:"QD"`
+		RD             string `json:"RD"`
+		Team           string `json:"team"`
+		Acl            string `json:"acl"`
+		Whitelist      string `json:"whitelist"`
+		OrderIn        int    `json:"order"`
+		Vision         string `json:"vision"`
+		DisplayCards   int    `json:"displayCards"`
+		FluidBoard     string `json:"fluidBoard"`
+		Deleted        string `json:"deleted"`
+	} `json:"projectInfo"`
+	Progress    int `json:"progress"`
+	TeamMembers []struct {
+		ID         uint64 `json:"id"`
+		Root       int    `json:"root"`
+		Type       string `json:"type"`
+		Account    string `json:"account"`
+		Role       string `json:"role"`
+		Position   string `json:"position"`
+		Limited    string `json:"limited"`
+		Join       string `json:"join"`
+		Days       int    `json:"days"`
+		Hours      int    `json:"hours"`
+		Estimate   string `json:"estimate"`
+		Consumed   string `json:"consumed"`
+		Left       string `json:"left"`
+		OrderIn    int    `json:"order"`
+		TotalHours int    `json:"totalHours"`
+		UserID     uint64 `json:"userID"`
+		Realname   string `json:"realname"`
+	} `json:"teamMembers"`
+	Products []struct {
+		ID    uint64        `json:"id"`
+		Name  string        `json:"name"`
+		Plans []interface{} `json:"plans"`
+	} `json:"products"`
+	CaseReview bool `json:"caseReview"`
+}
+
+type ZentaoExecution struct {
+	ConnectionId   uint64     `gorm:"primaryKey"`
+	Id             uint64     `json:"id" gorm:"primaryKey"`
+	Project        uint64     `json:"project"`
+	Model          string     `json:"model"`
+	Type           string     `json:"type"`
+	Lifetime       string     `json:"lifetime"`
+	Budget         string     `json:"budget"`
+	BudgetUnit     string     `json:"budgetUnit"`
+	Attribute      string     `json:"attribute"`
+	Percent        int        `json:"percent"`
+	Milestone      string     `json:"milestone"`
+	Output         string     `json:"output"`
+	Auth           string     `json:"auth"`
+	Parent         uint64     `json:"parent"`
+	Path           string     `json:"path"`
+	Grade          int        `json:"grade"`
+	Name           string     `json:"name"`
+	Code           string     `json:"code"`
+	PlanBegin      string     `json:"begin"`
+	PlanEnd        string     `json:"end"`
+	RealBegan      *time.Time `json:"realBegan"`
+	RealEnd        *time.Time `json:"realEnd"`
+	Days           int        `json:"days"`
+	Status         string     `json:"status"`
+	SubStatus      string     `json:"subStatus"`
+	Pri            string     `json:"pri"`
+	Description    string     `json:"desc"`
+	Version        int        `json:"version"`
+	ParentVersion  int        `json:"parentVersion"`
+	PlanDuration   int        `json:"planDuration"`
+	RealDuration   int        `json:"realDuration"`
+	OpenedById     uint64
+	OpenedDate     *time.Time `json:"openedDate"`
+	OpenedVersion  string     `json:"openedVersion"`
+	LastEditedById uint64
+	LastEditedDate *time.Time `json:"lastEditedDate"`
+	ClosedById     uint64
+	ClosedDate     *time.Time `json:"closedDate"`
+	CanceledById   uint64
+	CanceledDate   *time.Time `json:"canceledDate"`
+	SuspendedDate  string     `json:"suspendedDate"`
+	POId           uint64
+	PMId           uint64
+	QDId           uint64
+	RDId           uint64
+	Team           string `json:"team"`
+	Acl            string `json:"acl"`
+	OrderIn        int    `json:"order"`
+	Vision         string `json:"vision"`
+	DisplayCards   int    `json:"displayCards"`
+	FluidBoard     string `json:"fluidBoard"`
+	Deleted        bool   `json:"deleted"`
+	TotalHours     int    `json:"totalHours"`
+	TotalEstimate  int    `json:"totalEstimate"`
+	TotalConsumed  int    `json:"totalConsumed"`
+	TotalLeft      int    `json:"totalLeft"`
+	ProjectId      uint64
+	Progress       int  `json:"progress"`
+	CaseReview     bool `json:"caseReview"`
+	common.NoPKModel
+}
+
+func (ZentaoExecution) TableName() string {
+	return "_tool_zentao_executions"
+}
diff --git a/plugins/zentao/models/migrationscripts/20220906_add_init_tables.go b/plugins/zentao/models/migrationscripts/20220906_add_init_tables.go
new file mode 100644
index 000000000..5d6e3cb23
--- /dev/null
+++ b/plugins/zentao/models/migrationscripts/20220906_add_init_tables.go
@@ -0,0 +1,48 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License.  You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package migrationscripts
+
+import (
+	"github.com/apache/incubator-devlake/errors"
+	"github.com/apache/incubator-devlake/helpers/migrationhelper"
+	"github.com/apache/incubator-devlake/plugins/core"
+	"github.com/apache/incubator-devlake/plugins/zentao/models/archived"
+)
+
+type addInitTables struct{}
+
+func (*addInitTables) Up(basicRes core.BasicRes) errors.Error {
+	return migrationhelper.AutoMigrateTables(
+		basicRes,
+		&archived.ZentaoConnection{},
+		&archived.ZentaoProject{},
+		&archived.ZentaoProduct{},
+		&archived.ZentaoExecution{},
+		&archived.ZentaoStory{},
+		&archived.ZentaoBug{},
+		&archived.ZentaoTask{},
+	)
+}
+
+func (*addInitTables) Version() uint64 {
+	return 20220910000001
+}
+
+func (*addInitTables) Name() string {
+	return "zentao init schemas"
+}
diff --git a/plugins/zentao/models/migrationscripts/register.go b/plugins/zentao/models/migrationscripts/register.go
new file mode 100644
index 000000000..c0c766cd6
--- /dev/null
+++ b/plugins/zentao/models/migrationscripts/register.go
@@ -0,0 +1,29 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License.  You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package migrationscripts
+
+import (
+	"github.com/apache/incubator-devlake/plugins/core"
+)
+
+// All return all the migration scripts
+func All() []core.MigrationScript {
+	return []core.MigrationScript{
+		new(addInitTables),
+	}
+}
diff --git a/plugins/zentao/models/product.go b/plugins/zentao/models/product.go
new file mode 100644
index 000000000..802e8668d
--- /dev/null
+++ b/plugins/zentao/models/product.go
@@ -0,0 +1,126 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License.  You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package models
+
+import (
+	"github.com/apache/incubator-devlake/models/common"
+	"time"
+)
+
+type ZentaoProductRes struct {
+	ID          uint64 `json:"id"`
+	Program     int    `json:"program"`
+	Name        string `json:"name"`
+	Code        string `json:"code"`
+	Bind        string `json:"bind"`
+	Line        int    `json:"line"`
+	Type        string `json:"type"`
+	Status      string `json:"status"`
+	SubStatus   string `json:"subStatus"`
+	Description string `json:"desc"`
+	PO          struct {
+		ID       uint64 `json:"id"`
+		Account  string `json:"account"`
+		Avatar   string `json:"avatar"`
+		Realname string `json:"realname"`
+	} `json:"PO"`
+	QD struct {
+		ID       uint64 `json:"id"`
+		Account  string `json:"account"`
+		Avatar   string `json:"avatar"`
+		Realname string `json:"realname"`
+	} `json:"QD"`
+	RD struct {
+		ID       uint64 `json:"id"`
+		Account  string `json:"account"`
+		Avatar   string `json:"avatar"`
+		Realname string `json:"realname"`
+	} `json:"RD"`
+	Feedback  interface{}   `json:"feedback"`
+	Acl       string        `json:"acl"`
+	Whitelist []interface{} `json:"whitelist"`
+	Reviewer  string        `json:"reviewer"`
+	CreatedBy struct {
+		ID       uint64 `json:"id"`
+		Account  string `json:"account"`
+		Avatar   string `json:"avatar"`
+		Realname string `json:"realname"`
+	} `json:"createdBy"`
+	CreatedDate    *time.Time `json:"createdDate"`
+	CreatedVersion string     `json:"createdVersion"`
+	OrderIn        int        `json:"order"`
+	Vision         string     `json:"vision"`
+	Deleted        string     `json:"deleted"`
+	Stories        struct {
+		Active    int `json:"active"`
+		Reviewing int `json:"reviewing"`
+		int       `json:""`
+		Draft     int `json:"draft"`
+		Closed    int `json:"closed"`
+		Changing  int `json:"changing"`
+	} `json:"stories"`
+	Plans      int  `json:"plans"`
+	Releases   int  `json:"releases"`
+	Builds     int  `json:"builds"`
+	Cases      int  `json:"cases"`
+	Projects   int  `json:"projects"`
+	Executions int  `json:"executions"`
+	Bugs       int  `json:"bugs"`
+	Docs       int  `json:"docs"`
+	Progress   int  `json:"progress"`
+	CaseReview bool `json:"caseReview"`
+}
+
+type ZentaoProduct struct {
+	ConnectionId   uint64 `gorm:"primaryKey"`
+	Id             uint64 `json:"id" gorm:"primaryKey"`
+	Program        int    `json:"program"`
+	Name           string `json:"name"`
+	Code           string `json:"code"`
+	Bind           string `json:"bind"`
+	Line           int    `json:"line"`
+	Type           string `json:"type"`
+	Status         string `json:"status"`
+	SubStatus      string `json:"subStatus"`
+	Description    string `json:"desc"`
+	POId           uint64
+	QDId           uint64
+	RDId           uint64
+	Acl            string `json:"acl"`
+	Reviewer       string `json:"reviewer"`
+	CreatedById    uint64
+	CreatedDate    *time.Time `json:"createdDate"`
+	CreatedVersion string     `json:"createdVersion"`
+	OrderIn        int        `json:"order"`
+	Deleted        string     `json:"deleted"`
+	Plans          int        `json:"plans"`
+	Releases       int        `json:"releases"`
+	Builds         int        `json:"builds"`
+	Cases          int        `json:"cases"`
+	Projects       int        `json:"projects"`
+	Executions     int        `json:"executions"`
+	Bugs           int        `json:"bugs"`
+	Docs           int        `json:"docs"`
+	Progress       int        `json:"progress"`
+	CaseReview     bool       `json:"caseReview"`
+	common.NoPKModel
+}
+
+func (ZentaoProduct) TableName() string {
+	return "_tool_zentao_products"
+}
diff --git a/plugins/zentao/models/project.go b/plugins/zentao/models/project.go
new file mode 100644
index 000000000..08238a8a5
--- /dev/null
+++ b/plugins/zentao/models/project.go
@@ -0,0 +1,113 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License.  You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package models
+
+import (
+	"github.com/apache/incubator-devlake/models/common"
+	"time"
+)
+
+type ZentaoProject struct {
+	common.NoPKModel
+	ConnectionId  uint64 `gorm:"primaryKey;type:BIGINT  NOT NULL"`
+	ID            uint64 `json:"id" gorm:"primaryKey;type:BIGINT  NOT NULL"`
+	Project       uint64 `json:"project"`
+	Model         string `json:"model"`
+	Type          string `json:"type"`
+	Lifetime      string `json:"lifetime"`
+	Budget        string `json:"budget"`
+	BudgetUnit    string `json:"budgetUnit"`
+	Attribute     string `json:"attribute"`
+	Percent       int    `json:"percent"`
+	Milestone     string `json:"milestone"`
+	Output        string `json:"output"`
+	Auth          string `json:"auth"`
+	Parent        uint64 `json:"parent"`
+	Path          string `json:"path"`
+	Grade         int    `json:"grade"`
+	Name          string `json:"name"`
+	Code          string `json:"code"`
+	PlanBegin     string `json:"begin"`
+	PlanEnd       string `json:"end"`
+	RealBegan     string `json:"realBegan"`
+	RealEnd       string `json:"realEnd"`
+	Days          int    `json:"days"`
+	Status        string `json:"status"`
+	SubStatus     string `json:"subStatus"`
+	Pri           string `json:"pri"`
+	Description   string `json:"desc"`
+	Version       int    `json:"version"`
+	ParentVersion int    `json:"parentVersion"`
+	PlanDuration  int    `json:"planDuration"`
+	RealDuration  int    `json:"realDuration"`
+	//OpenedBy       string    `json:"openedBy"`
+	OpenedDate     *time.Time  `json:"openedDate"`
+	OpenedVersion  string      `json:"openedVersion"`
+	LastEditedBy   string      `json:"lastEditedBy"`
+	LastEditedDate **time.Time `json:"lastEditedDate"`
+	ClosedBy       string      `json:"closedBy"`
+	ClosedDate     **time.Time `json:"closedDate"`
+	CanceledBy     string      `json:"canceledBy"`
+	CanceledDate   **time.Time `json:"canceledDate"`
+	SuspendedDate  string      `json:"suspendedDate"`
+	PO             string      `json:"PO"`
+	PM             `json:"PM"`
+	QD             string `json:"QD"`
+	RD             string `json:"RD"`
+	Team           string `json:"team"`
+	Acl            string `json:"acl"`
+	Whitelist      `json:"whitelist" gorm:"-"`
+	OrderIn        int    `json:"order"`
+	Vision         string `json:"vision"`
+	DisplayCards   int    `json:"displayCards"`
+	FluidBoard     string `json:"fluidBoard"`
+	Deleted        bool   `json:"deleted"`
+	Delay          int    `json:"delay"`
+	Hours          `json:"hours"`
+	TeamCount      int    `json:"teamCount"`
+	LeftTasks      string `json:"leftTasks"`
+	//TeamMembers   []interface{} `json:"teamMembers" gorm:"-"`
+	TotalEstimate int `json:"totalEstimate"`
+	TotalConsumed int `json:"totalConsumed"`
+	TotalLeft     int `json:"totalLeft"`
+	Progress      int `json:"progress"`
+	TotalReal     int `json:"totalReal"`
+}
+type PM struct {
+	PmId       uint64 `json:"id"`
+	PmAccount  string `json:"account"`
+	PmAvatar   string `json:"avatar"`
+	PmRealname string `json:"realname"`
+}
+type Whitelist []struct {
+	WhitelistID       uint64 `json:"id"`
+	WhitelistAccount  string `json:"account"`
+	WhitelistAvatar   string `json:"avatar"`
+	WhitelistRealname string `json:"realname"`
+}
+type Hours struct {
+	HoursTotalEstimate float64 `json:"totalEstimate"`
+	HoursTotalConsumed float64 `json:"totalConsumed"`
+	HoursTotalLeft     float64 `json:"totalLeft"`
+	HoursProgress      float64 `json:"progress"`
+	HoursTotalReal     float64 `json:"totalReal"`
+}
+
+func (ZentaoProject) TableName() string {
+	return "_tool_zentao_projects"
+}
diff --git a/plugins/zentao/models/story.go b/plugins/zentao/models/story.go
new file mode 100644
index 000000000..3b319d8b3
--- /dev/null
+++ b/plugins/zentao/models/story.go
@@ -0,0 +1,169 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License.  You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package models
+
+import (
+	"github.com/apache/incubator-devlake/models/common"
+	"time"
+)
+
+type ZentaoStoryRes struct {
+	ID          uint64        `json:"id"`
+	Vision      string        `json:"vision"`
+	Parent      uint64        `json:"parent"`
+	Product     uint64        `json:"product"`
+	Branch      int           `json:"branch"`
+	Module      int           `json:"module"`
+	Plan        string        `json:"plan"`
+	Source      string        `json:"source"`
+	SourceNote  string        `json:"sourceNote"`
+	FromBug     int           `json:"fromBug"`
+	Feedback    int           `json:"feedback"`
+	Title       string        `json:"title"`
+	Keywords    string        `json:"keywords"`
+	Type        string        `json:"type"`
+	Category    string        `json:"category"`
+	Pri         int           `json:"pri"`
+	Estimate    int           `json:"estimate"`
+	Status      string        `json:"status"`
+	SubStatus   string        `json:"subStatus"`
+	Color       string        `json:"color"`
+	Stage       string        `json:"stage"`
+	Mailto      []interface{} `json:"mailto"`
+	Lib         int           `json:"lib"`
+	FromStory   uint64        `json:"fromStory"`
+	FromVersion int           `json:"fromVersion"`
+	OpenedBy    struct {
+		ID       uint64 `json:"id"`
+		Account  string `json:"account"`
+		Avatar   string `json:"avatar"`
+		Realname string `json:"realname"`
+	} `json:"openedBy"`
+	OpenedDate *time.Time `json:"openedDate"`
+	AssignedTo struct {
+		ID       uint64 `json:"id"`
+		Account  string `json:"account"`
+		Avatar   string `json:"avatar"`
+		Realname string `json:"realname"`
+	} `json:"assignedTo"`
+	AssignedDate *time.Time `json:"assignedDate"`
+	ApprovedDate string     `json:"approvedDate"`
+	LastEditedBy struct {
+		ID       uint64 `json:"id"`
+		Account  string `json:"account"`
+		Avatar   string `json:"avatar"`
+		Realname string `json:"realname"`
+	} `json:"lastEditedBy"`
+	LastEditedDate *time.Time `json:"lastEditedDate"`
+	ChangedBy      string     `json:"changedBy"`
+	ChangedDate    string     `json:"changedDate"`
+	ReviewedBy     struct {
+		ID       uint64 `json:"id"`
+		Account  string `json:"account"`
+		Avatar   string `json:"avatar"`
+		Realname string `json:"realname"`
+	} `json:"reviewedBy"`
+	ReviewedDate *time.Time `json:"reviewedDate"`
+	ClosedBy     struct {
+		ID       uint64 `json:"id"`
+		Account  string `json:"account"`
+		Avatar   string `json:"avatar"`
+		Realname string `json:"realname"`
+	} `json:"closedBy"`
+	ClosedDate       *time.Time `json:"closedDate"`
+	ClosedReason     string     `json:"closedReason"`
+	ActivatedDate    string     `json:"activatedDate"`
+	ToBug            int        `json:"toBug"`
+	ChildStories     string     `json:"childStories"`
+	LinkStories      string     `json:"linkStories"`
+	LinkRequirements string     `json:"linkRequirements"`
+	DuplicateStory   uint64     `json:"duplicateStory"`
+	Version          int        `json:"version"`
+	StoryChanged     string     `json:"storyChanged"`
+	FeedbackBy       string     `json:"feedbackBy"`
+	NotifyEmail      string     `json:"notifyEmail"`
+	URChanged        string     `json:"URChanged"`
+	Deleted          bool       `json:"deleted"`
+	PriOrder         string     `json:"priOrder"`
+	PlanTitle        string     `json:"planTitle"`
+	ProductStatus    string     `json:"productStatus"`
+}
+
+type ZentaoStory struct {
+	common.NoPKModel
+	ConnectionId uint64 `gorm:"primaryKey;type:BIGINT  NOT NULL"`
+	ID           uint64 `json:"id" gorm:"primaryKey;type:BIGINT  NOT NULL" `
+	Product      uint64 `json:"product"`
+	Branch       int    `json:"branch"`
+	Version      int    `json:"version"`
+	OrderIn      int    `json:"order"`
+	Vision       string `json:"vision"`
+	Parent       uint64 `json:"parent"`
+	Module       int    `json:"module"`
+	Plan         string `json:"plan"`
+	Source       string `json:"source"`
+	SourceNote   string `json:"sourceNote"`
+	FromBug      int    `json:"fromBug"`
+	Feedback     int    `json:"feedback"`
+	Title        string `json:"title"`
+	Keywords     string `json:"keywords"`
+	Type         string `json:"type"`
+	Category     string `json:"category"`
+	Pri          int    `json:"pri"`
+	Estimate     int    `json:"estimate"`
+	Status       string `json:"status"`
+	SubStatus    string `json:"subStatus"`
+	Color        string `json:"color"`
+	Stage        string `json:"stage"`
+	//Mailto           []interface{} `json:"mailto"`
+	Lib              int    `json:"lib"`
+	FromStory        uint64 `json:"fromStory"`
+	FromVersion      int    `json:"fromVersion"`
+	OpenedById       uint64
+	OpenedByName     string
+	OpenedDate       *time.Time `json:"openedDate"`
+	AssignedToId     uint64
+	AssignedToName   string
+	AssignedDate     *time.Time `json:"assignedDate"`
+	ApprovedDate     string     `json:"approvedDate"`
+	LastEditedId     uint64
+	LastEditedDate   *time.Time `json:"lastEditedDate"`
+	ChangedDate      string     `json:"changedDate"`
+	ReviewedById     uint64     `json:"reviewedBy"`
+	ReviewedDate     *time.Time `json:"reviewedDate"`
+	ClosedId         uint64
+	ClosedDate       *time.Time `json:"closedDate"`
+	ClosedReason     string     `json:"closedReason"`
+	ActivatedDate    string     `json:"activatedDate"`
+	ToBug            int        `json:"toBug"`
+	ChildStories     string     `json:"childStories"`
+	LinkStories      string     `json:"linkStories"`
+	LinkRequirements string     `json:"linkRequirements"`
+	DuplicateStory   uint64     `json:"duplicateStory"`
+	StoryChanged     string     `json:"storyChanged"`
+	FeedbackBy       string     `json:"feedbackBy"`
+	NotifyEmail      string     `json:"notifyEmail"`
+	URChanged        string     `json:"URChanged"`
+	Deleted          bool       `json:"deleted"`
+	PriOrder         string     `json:"priOrder"`
+	PlanTitle        string     `json:"planTitle"`
+}
+
+func (ZentaoStory) TableName() string {
+	return "_tool_zentao_stories"
+}
diff --git a/plugins/zentao/models/task.go b/plugins/zentao/models/task.go
new file mode 100644
index 000000000..4ca8777c5
--- /dev/null
+++ b/plugins/zentao/models/task.go
@@ -0,0 +1,201 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License.  You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package models
+
+import (
+	"github.com/apache/incubator-devlake/models/common"
+	"time"
+)
+
+type ZentaoTaskRes struct {
+	Id            uint64 `json:"id"`
+	Project       uint64 `json:"project"`
+	Parent        uint64 `json:"parent"`
+	Execution     uint64 `json:"execution"`
+	Module        int    `json:"module"`
+	Design        int    `json:"design"`
+	Story         uint64 `json:"story"`
+	StoryVersion  int    `json:"storyVersion"`
+	DesignVersion int    `json:"designVersion"`
+	FromBug       int    `json:"fromBug"`
+	Feedback      int    `json:"feedback"`
+	FromIssue     int    `json:"fromIssue"`
+	Name          string `json:"name"`
+	Type          string `json:"type"`
+	Mode          string `json:"mode"`
+	Pri           int    `json:"pri"`
+	Estimate      int    `json:"estimate"`
+	Consumed      int    `json:"consumed"`
+	Deadline      string `json:"deadline"`
+	Status        string `json:"status"`
+	SubStatus     string `json:"subStatus"`
+	Color         string `json:"color"`
+	Mailto        []struct {
+		Id       uint64 `json:"id"`
+		Account  string `json:"account"`
+		Avatar   string `json:"avatar"`
+		Realname string `json:"realname"`
+	} `json:"mailto"`
+	Description string `json:"desc"`
+	Version     int    `json:"version"`
+	OpenedBy    struct {
+		Id       uint64 `json:"id"`
+		Account  string `json:"account"`
+		Avatar   string `json:"avatar"`
+		Realname string `json:"realname"`
+	} `json:"openedBy"`
+	OpenedDate *time.Time `json:"openedDate"`
+	AssignedTo struct {
+		Id       uint64 `json:"id"`
+		Account  string `json:"account"`
+		Avatar   string `json:"avatar"`
+		Realname string `json:"realname"`
+	} `json:"assignedTo"`
+	AssignedDate *time.Time `json:"assignedDate"`
+	EstStarted   string     `json:"estStarted"`
+	RealStarted  *time.Time `json:"realStarted"`
+	FinishedBy   struct {
+		Id       uint64 `json:"id"`
+		Account  string `json:"account"`
+		Avatar   string `json:"avatar"`
+		Realname string `json:"realname"`
+	} `json:"finishedBy"`
+	FinishedDate *time.Time `json:"finishedDate"`
+	FinishedList string     `json:"finishedList"`
+	CanceledBy   struct {
+		Id       uint64 `json:"id"`
+		Account  string `json:"account"`
+		Avatar   string `json:"avatar"`
+		Realname string `json:"realname"`
+	} `json:"canceledBy"`
+	CanceledDate *time.Time `json:"canceledDate"`
+	ClosedBy     struct {
+		Id       uint64 `json:"id"`
+		Account  string `json:"account"`
+		Avatar   string `json:"avatar"`
+		Realname string `json:"realname"`
+	} `json:"closedBy"`
+	ClosedDate   *time.Time `json:"closedDate"`
+	PlanDuration int        `json:"planDuration"`
+	RealDuration int        `json:"realDuration"`
+	ClosedReason string     `json:"closedReason"`
+	LastEditedBy struct {
+		Id       uint64 `json:"id"`
+		Account  string `json:"account"`
+		Avatar   string `json:"avatar"`
+		Realname string `json:"realname"`
+	} `json:"lastEditedBy"`
+	LastEditedDate *time.Time `json:"lastEditedDate"`
+	ActivatedDate  string     `json:"activatedDate"`
+	OrderIn        int        `json:"order"`
+	Repo           int        `json:"repo"`
+	Mr             int        `json:"mr"`
+	Entry          string     `json:"entry"`
+	Lines          string     `json:"lines"`
+	V1             string     `json:"v1"`
+	V2             string     `json:"v2"`
+	Deleted        bool       `json:"deleted"`
+	Vision         string     `json:"vision"`
+	StoryID        uint64     `json:"storyID"`
+	StoryTitle     string     `json:"storyTitle"`
+	Branch         interface {
+	} `json:"branch"`
+	LatestStoryVersion interface {
+	} `json:"latestStoryVersion"`
+	StoryStatus interface {
+	} `json:"storyStatus"`
+	AssignedToRealName string `json:"assignedToRealName"`
+	PriOrder           string `json:"priOrder"`
+	Delay              int    `json:"delay"`
+	NeedConfirm        bool   `json:"needConfirm"`
+	Progress           int    `json:"progress"`
+}
+
+type ZentaoTask struct {
+	common.NoPKModel
+	ConnectionId  uint64 `gorm:"primaryKey;type:BIGINT  NOT NULL"`
+	ExecutionId   uint64 `json:"execution_id"`
+	ID            uint64 `json:"id" gorm:"primaryKey;type:BIGINT  NOT NULL"`
+	Project       uint64 `json:"project"`
+	Parent        uint64 `json:"parent"`
+	Execution     uint64 `json:"execution"`
+	Module        int    `json:"module"`
+	Design        int    `json:"design"`
+	Story         uint64 `json:"story"`
+	StoryVersion  int    `json:"storyVersion"`
+	DesignVersion int    `json:"designVersion"`
+	FromBug       int    `json:"fromBug"`
+	Feedback      int    `json:"feedback"`
+	FromIssue     int    `json:"fromIssue"`
+	Name          string `json:"name"`
+	Type          string `json:"type"`
+	Mode          string `json:"mode"`
+	Pri           int    `json:"pri"`
+	Estimate      int    `json:"estimate"`
+	Consumed      int    `json:"consumed"`
+	Deadline      string `json:"deadline"`
+	Status        string `json:"status"`
+	SubStatus     string `json:"subStatus"`
+	Color         string `json:"color"`
+	//Mailto        interface{} `json:"mailto"`
+	Description        string `json:"desc"`
+	Version            int    `json:"version"`
+	OpenedById         uint64
+	OpenedByName       string
+	OpenedDate         *time.Time `json:"openedDate"`
+	AssignedToId       uint64
+	AssignedToName     string
+	AssignedDate       *time.Time `json:"assignedDate"`
+	EstStarted         string     `json:"estStarted"`
+	RealStarted        *time.Time `json:"realStarted"`
+	FinishedId         uint64
+	FinishedDate       *time.Time `json:"finishedDate"`
+	FinishedList       string     `json:"finishedList"`
+	CanceledId         uint64
+	CanceledDate       *time.Time `json:"canceledDate"`
+	ClosedById         uint64
+	ClosedDate         *time.Time `json:"closedDate"`
+	PlanDuration       int        `json:"planDuration"`
+	RealDuration       int        `json:"realDuration"`
+	ClosedReason       string     `json:"closedReason"`
+	LastEditedId       uint64
+	LastEditedDate     *time.Time `json:"lastEditedDate"`
+	ActivatedDate      string     `json:"activatedDate"`
+	OrderIn            int        `json:"order"`
+	Repo               int        `json:"repo"`
+	Mr                 int        `json:"mr"`
+	Entry              string     `json:"entry"`
+	Lines              string     `json:"lines"`
+	V1                 string     `json:"v1"`
+	V2                 string     `json:"v2"`
+	Deleted            bool       `json:"deleted"`
+	Vision             string     `json:"vision"`
+	StoryID            uint64     `json:"storyID"`
+	StoryTitle         string     `json:"storyTitle"`
+	Branch             int        `json:"branch"`
+	LatestStoryVersion int        `json:"latestStoryVersion"`
+	StoryStatus        string     `json:"storyStatus"`
+	AssignedToRealName string     `json:"assignedToRealName"`
+	PriOrder           string     `json:"priOrder"`
+	NeedConfirm        bool       `json:"needConfirm"`
+	Progress           int        `json:"progress"`
+}
+
+func (ZentaoTask) TableName() string {
+	return "_tool_zentao_tasks"
+}
diff --git a/plugins/zentao/tasks/api_client.go b/plugins/zentao/tasks/api_client.go
new file mode 100644
index 000000000..aff7b4d69
--- /dev/null
+++ b/plugins/zentao/tasks/api_client.go
@@ -0,0 +1,95 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License.  You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package tasks
+
+import (
+	"fmt"
+	"github.com/apache/incubator-devlake/errors"
+	"net/http"
+	"strconv"
+	"time"
+
+	"github.com/apache/incubator-devlake/plugins/core"
+	"github.com/apache/incubator-devlake/plugins/helper"
+	"github.com/apache/incubator-devlake/plugins/zentao/models"
+)
+
+func NewZentaoApiClient(taskCtx core.TaskContext, connection *models.ZentaoConnection) (*helper.ApiAsyncClient, error) {
+	authApiClient, err := helper.NewApiClient(taskCtx.GetContext(), connection.Endpoint, nil, 0, connection.Proxy, taskCtx)
+	if err != nil {
+		return nil, err
+	}
+
+	// request for access token
+	tokenReqBody := &models.ApiAccessTokenRequest{
+		Account:  connection.Username,
+		Password: connection.Password,
+	}
+	tokenRes, err := authApiClient.Post("/tokens", nil, tokenReqBody, nil)
+	if err != nil {
+		return nil, err
+	}
+	tokenResBody := &models.ApiAccessTokenResponse{}
+	err = helper.UnmarshalResponse(tokenRes, tokenResBody)
+	if err != nil {
+		return nil, err
+	}
+	if tokenResBody.Token == "" {
+		return nil, errors.Default.New("failed to request access token")
+	}
+	// real request apiClient
+	apiClient, err := helper.NewApiClient(taskCtx.GetContext(), connection.Endpoint, nil, 0, connection.Proxy, taskCtx)
+	if err != nil {
+		return nil, err
+	}
+	// set token
+	apiClient.SetHeaders(map[string]string{
+		"Token": fmt.Sprintf("%v", tokenResBody.Token),
+	})
+
+	// create rate limit calculator
+	rateLimiter := &helper.ApiRateLimitCalculator{
+		UserRateLimitPerHour: connection.RateLimitPerHour,
+		DynamicRateLimit: func(res *http.Response) (int, time.Duration, errors.Error) {
+			rateLimitHeader := res.Header.Get("RateLimit-Limit")
+			if rateLimitHeader == "" {
+				// use default
+				return 0, 0, nil
+			}
+			rateLimit, err := strconv.Atoi(rateLimitHeader)
+			if err != nil {
+				return 0, 0, errors.Default.Wrap(err, "failed to parse RateLimit-Limit header: %w")
+			}
+			// seems like {{ .plugin-ame }} rate limit is on minute basis
+			return rateLimit, 1 * time.Minute, nil
+		},
+	}
+	asyncApiClient, err := helper.CreateAsyncApiClient(
+		taskCtx,
+		apiClient,
+		rateLimiter,
+	)
+	if err != nil {
+		return nil, err
+	}
+	return asyncApiClient, nil
+}
+
+type ZentaoPagination struct {
+	Page int `json:"page"`
+}
diff --git a/plugins/zentao/tasks/bug_collector.go b/plugins/zentao/tasks/bug_collector.go
new file mode 100644
index 000000000..843438583
--- /dev/null
+++ b/plugins/zentao/tasks/bug_collector.go
@@ -0,0 +1,82 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License.  You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package tasks
+
+import (
+	"encoding/json"
+	"fmt"
+	"github.com/apache/incubator-devlake/errors"
+	"github.com/apache/incubator-devlake/plugins/core"
+	"github.com/apache/incubator-devlake/plugins/helper"
+	"net/http"
+	"net/url"
+)
+
+const RAW_BUG_TABLE = "zentao_api_bugs"
+
+var _ core.SubTaskEntryPoint = CollectBug
+
+func CollectBug(taskCtx core.SubTaskContext) errors.Error {
+	data := taskCtx.GetData().(*ZentaoTaskData)
+	collector, err := helper.NewApiCollector(helper.ApiCollectorArgs{
+		RawDataSubTaskArgs: helper.RawDataSubTaskArgs{
+			Ctx: taskCtx,
+			Params: ZentaoApiParams{
+				ConnectionId: data.Options.ConnectionId,
+				ProductId:    data.Options.ProductId,
+				ExecutionId:  data.Options.ExecutionId,
+				ProjectId:    data.Options.ProjectId,
+			},
+			Table: RAW_BUG_TABLE,
+		},
+		ApiClient: data.ApiClient,
+
+		PageSize: 100,
+		// TODO write which api would you want request
+		UrlTemplate: "/products/{{ .Params.ProductId }}/bugs",
+		Query: func(reqData *helper.RequestData) (url.Values, errors.Error) {
+			query := url.Values{}
+			query.Set("page", fmt.Sprintf("%v", reqData.Pager.Page))
+			query.Set("limit", fmt.Sprintf("%v", reqData.Pager.Size))
+			return query, nil
+		},
+		GetTotalPages: GetTotalPagesFromResponse,
+		ResponseParser: func(res *http.Response) ([]json.RawMessage, errors.Error) {
+			var data struct {
+				Bugs []json.RawMessage `json:"bugs"`
+			}
+			err := helper.UnmarshalResponse(res, &data)
+			if err != nil {
+				return nil, errors.Default.Wrap(err, "error reading endpoint response by Zentao bug collector")
+			}
+			return data.Bugs, nil
+		},
+	})
+	if err != nil {
+		return err
+	}
+
+	return collector.Execute()
+}
+
+var CollectBugMeta = core.SubTaskMeta{
+	Name:             "CollectBug",
+	EntryPoint:       CollectBug,
+	EnabledByDefault: true,
+	Description:      "Collect Bug data from Zentao api",
+}
diff --git a/plugins/zentao/tasks/bug_convertor.go b/plugins/zentao/tasks/bug_convertor.go
new file mode 100644
index 000000000..c58c332f1
--- /dev/null
+++ b/plugins/zentao/tasks/bug_convertor.go
@@ -0,0 +1,116 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License.  You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package tasks
+
+import (
+	"github.com/apache/incubator-devlake/errors"
+	"github.com/apache/incubator-devlake/models/domainlayer"
+	"github.com/apache/incubator-devlake/models/domainlayer/didgen"
+	"github.com/apache/incubator-devlake/models/domainlayer/ticket"
+	"github.com/apache/incubator-devlake/plugins/core"
+	"github.com/apache/incubator-devlake/plugins/core/dal"
+	"github.com/apache/incubator-devlake/plugins/helper"
+	"github.com/apache/incubator-devlake/plugins/zentao/models"
+	"reflect"
+	"strconv"
+)
+
+var _ core.SubTaskEntryPoint = ConvertBug
+
+var ConvertBugMeta = core.SubTaskMeta{
+	Name:             "convertBug",
+	EntryPoint:       ConvertBug,
+	EnabledByDefault: true,
+	Description:      "convert Zentao bug",
+	DomainTypes:      []string{core.DOMAIN_TYPE_TICKET},
+}
+
+func ConvertBug(taskCtx core.SubTaskContext) errors.Error {
+	data := taskCtx.GetData().(*ZentaoTaskData)
+	db := taskCtx.GetDal()
+	bugIdGen := didgen.NewDomainIdGenerator(&models.ZentaoBug{})
+	boardIdGen := didgen.NewDomainIdGenerator(&models.ZentaoProduct{})
+	storyIdGen := didgen.NewDomainIdGenerator(&models.ZentaoStory{})
+	cursor, err := db.Cursor(
+		dal.From(&models.ZentaoBug{}),
+		dal.Where(`_tool_zentao_bugs.product = ? and
+			_tool_zentao_bugs.connection_id = ?`, data.Options.ProductId, data.Options.ConnectionId),
+	)
+	if err != nil {
+		return err
+	}
+	defer cursor.Close()
+	convertor, err := helper.NewDataConverter(helper.DataConverterArgs{
+		InputRowType: reflect.TypeOf(models.ZentaoBug{}),
+		Input:        cursor,
+		RawDataSubTaskArgs: helper.RawDataSubTaskArgs{
+			Ctx: taskCtx,
+			Params: ZentaoApiParams{
+				ConnectionId: data.Options.ConnectionId,
+				ProductId:    data.Options.ProductId,
+				ExecutionId:  data.Options.ExecutionId,
+				ProjectId:    data.Options.ProjectId,
+			},
+			Table: RAW_BUG_TABLE,
+		},
+		Convert: func(inputRow interface{}) ([]interface{}, errors.Error) {
+			toolEntity := inputRow.(*models.ZentaoBug)
+			domainEntity := &ticket.Issue{
+				DomainEntity: domainlayer.DomainEntity{
+					Id: bugIdGen.Generate(toolEntity.ConnectionId, toolEntity.ID),
+				},
+				IssueKey:       strconv.FormatUint(toolEntity.ID, 10),
+				Title:          toolEntity.Title,
+				Type:           toolEntity.Type,
+				OriginalStatus: toolEntity.Status,
+				ResolutionDate: toolEntity.ClosedDate,
+				CreatedDate:    toolEntity.OpenedDate,
+				UpdatedDate:    toolEntity.LastEditedDate,
+				ParentIssueId:  storyIdGen.Generate(data.Options.ConnectionId, toolEntity.Story),
+				Priority:       string(rune(toolEntity.Pri)),
+				CreatorId:      strconv.FormatUint(toolEntity.OpenedById, 10),
+				CreatorName:    toolEntity.OpenedByName,
+				AssigneeId:     strconv.FormatUint(toolEntity.AssignedToId, 10),
+				AssigneeName:   toolEntity.AssignedToName,
+				Severity:       string(rune(toolEntity.Severity)),
+			}
+			switch toolEntity.Status {
+			case "resolved":
+				domainEntity.Status = "DONE"
+			default:
+				domainEntity.Status = "IN_PROGRESS"
+			}
+			if toolEntity.ClosedDate != nil {
+				domainEntity.LeadTimeMinutes = int64(toolEntity.ClosedDate.Sub(*toolEntity.OpenedDate).Minutes())
+			}
+			domainBoardIssue := &ticket.BoardIssue{
+				BoardId: boardIdGen.Generate(data.Options.ConnectionId, data.Options.ProductId),
+				IssueId: domainEntity.Id,
+			}
+			results := make([]interface{}, 0)
+			results = append(results, domainEntity, domainBoardIssue)
+			return results, nil
+		},
+	})
+
+	if err != nil {
+		return err
+	}
+
+	return convertor.Execute()
+}
diff --git a/plugins/zentao/tasks/bug_extractor.go b/plugins/zentao/tasks/bug_extractor.go
new file mode 100644
index 000000000..088c75602
--- /dev/null
+++ b/plugins/zentao/tasks/bug_extractor.go
@@ -0,0 +1,138 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License.  You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package tasks
+
+import (
+	"encoding/json"
+	"github.com/apache/incubator-devlake/errors"
+	"github.com/apache/incubator-devlake/plugins/core"
+	"github.com/apache/incubator-devlake/plugins/helper"
+	"github.com/apache/incubator-devlake/plugins/zentao/models"
+)
+
+var _ core.SubTaskEntryPoint = ExtractBug
+
+var ExtractBugMeta = core.SubTaskMeta{
+	Name:             "extractBug",
+	EntryPoint:       ExtractBug,
+	EnabledByDefault: true,
+	Description:      "extract Zentao bug",
+	DomainTypes:      []string{core.DOMAIN_TYPE_TICKET},
+}
+
+func ExtractBug(taskCtx core.SubTaskContext) errors.Error {
+	data := taskCtx.GetData().(*ZentaoTaskData)
+	extractor, err := helper.NewApiExtractor(helper.ApiExtractorArgs{
+		RawDataSubTaskArgs: helper.RawDataSubTaskArgs{
+			Ctx: taskCtx,
+			Params: ZentaoApiParams{
+				ConnectionId: data.Options.ConnectionId,
+				ProductId:    data.Options.ProductId,
+				ExecutionId:  data.Options.ExecutionId,
+				ProjectId:    data.Options.ProjectId,
+			},
+			Table: RAW_BUG_TABLE,
+		},
+		Extract: func(row *helper.RawData) ([]interface{}, errors.Error) {
+			res := &models.ZentaoBugRes{}
+			err := json.Unmarshal(row.Data, res)
+			if err != nil {
+				return nil, errors.Default.WrapRaw(err)
+			}
+			bug := &models.ZentaoBug{
+				ConnectionId:   data.Options.ConnectionId,
+				ID:             res.ID,
+				Project:        res.Project,
+				Product:        res.Product,
+				Injection:      res.Injection,
+				Identify:       res.Identify,
+				Branch:         res.Branch,
+				Module:         res.Module,
+				Execution:      res.Execution,
+				Plan:           res.Plan,
+				Story:          res.Story,
+				StoryVersion:   res.StoryVersion,
+				Task:           res.Task,
+				ToTask:         res.ToTask,
+				ToStory:        res.ToStory,
+				Title:          res.Title,
+				Keywords:       res.Keywords,
+				Severity:       res.Severity,
+				Pri:            res.Pri,
+				Type:           res.Type,
+				Os:             res.Os,
+				Browser:        res.Browser,
+				Hardware:       res.Hardware,
+				Found:          res.Found,
+				Steps:          res.Steps,
+				Status:         res.Status,
+				SubStatus:      res.SubStatus,
+				Color:          res.Color,
+				Confirmed:      res.Confirmed,
+				ActivatedCount: res.ActivatedCount,
+				ActivatedDate:  res.ActivatedDate,
+				FeedbackBy:     res.FeedbackBy,
+				NotifyEmail:    res.NotifyEmail,
+				OpenedById:     res.OpenedBy.ID,
+				OpenedByName:   res.OpenedBy.Realname,
+				OpenedDate:     res.OpenedDate,
+				OpenedBuild:    res.OpenedBuild,
+				AssignedToId:   res.AssignedTo.ID,
+				AssignedToName: res.AssignedTo.Realname,
+				AssignedDate:   res.AssignedDate,
+				Deadline:       res.Deadline,
+				ResolvedById:   res.ResolvedBy.ID,
+				Resolution:     res.Resolution,
+				ResolvedBuild:  res.ResolvedBuild,
+				ResolvedDate:   res.ResolvedDate,
+				ClosedById:     res.ClosedBy.ID,
+				ClosedDate:     res.ClosedDate,
+				DuplicateBug:   res.DuplicateBug,
+				LinkBug:        res.LinkBug,
+				Feedback:       res.Feedback,
+				Result:         res.Result,
+				Repo:           res.Repo,
+				Mr:             res.Mr,
+				Entry:          res.Entry,
+				Lines:          res.Lines,
+				V1:             res.V1,
+				V2:             res.V2,
+				RepoType:       res.RepoType,
+				IssueKey:       res.IssueKey,
+				Testtask:       res.Testtask,
+				LastEditedById: res.LastEditedBy.ID,
+				LastEditedDate: res.LastEditedDate,
+				Deleted:        res.Deleted,
+				PriOrder:       res.PriOrder,
+				SeverityOrder:  res.SeverityOrder,
+				Needconfirm:    res.Needconfirm,
+				StatusName:     res.StatusName,
+				ProductStatus:  res.ProductStatus,
+			}
+			results := make([]interface{}, 0)
+			results = append(results, bug)
+			return results, nil
+		},
+	})
+
+	if err != nil {
+		return err
+	}
+
+	return extractor.Execute()
+}
diff --git a/plugins/zentao/tasks/execution_collector.go b/plugins/zentao/tasks/execution_collector.go
new file mode 100644
index 000000000..8f6d2c6ae
--- /dev/null
+++ b/plugins/zentao/tasks/execution_collector.go
@@ -0,0 +1,81 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License.  You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package tasks
+
+import (
+	"encoding/json"
+	"fmt"
+	"github.com/apache/incubator-devlake/errors"
+	"github.com/apache/incubator-devlake/plugins/core"
+	"github.com/apache/incubator-devlake/plugins/helper"
+	"io"
+	"net/http"
+	"net/url"
+)
+
+const RAW_EXECUTION_TABLE = "zentao_api_executions"
+
+var _ core.SubTaskEntryPoint = CollectExecution
+
+func CollectExecution(taskCtx core.SubTaskContext) errors.Error {
+	data := taskCtx.GetData().(*ZentaoTaskData)
+	collector, err := helper.NewApiCollector(helper.ApiCollectorArgs{
+		RawDataSubTaskArgs: helper.RawDataSubTaskArgs{
+			Ctx: taskCtx,
+			Params: ZentaoApiParams{
+				ConnectionId: data.Options.ConnectionId,
+				ProductId:    data.Options.ProductId,
+				ExecutionId:  data.Options.ExecutionId,
+				ProjectId:    data.Options.ProjectId,
+			},
+			Table: RAW_EXECUTION_TABLE,
+		},
+		ApiClient: data.ApiClient,
+
+		PageSize: 100,
+		// TODO write which api would you want request
+		UrlTemplate: "executions/{{ .Params.ExecutionId }}",
+		Query: func(reqData *helper.RequestData) (url.Values, errors.Error) {
+			query := url.Values{}
+			query.Set("page", fmt.Sprintf("%v", reqData.Pager.Page))
+			query.Set("limit", fmt.Sprintf("%v", reqData.Pager.Size))
+			return query, nil
+		},
+		GetTotalPages: GetTotalPagesFromResponse,
+		ResponseParser: func(res *http.Response) ([]json.RawMessage, errors.Error) {
+			body, err := io.ReadAll(res.Body)
+			if err != nil {
+				return nil, errors.Default.Wrap(err, "error reading endpoint response by Zentao execution collector")
+			}
+			res.Body.Close()
+			return []json.RawMessage{body}, nil
+		},
+	})
+	if err != nil {
+		return err
+	}
+
+	return collector.Execute()
+}
+
+var CollectExecutionMeta = core.SubTaskMeta{
+	Name:             "CollectExecution",
+	EntryPoint:       CollectExecution,
+	EnabledByDefault: true,
+	Description:      "Collect Execution data from Zentao api",
+}
diff --git a/plugins/zentao/tasks/execution_convertor.go b/plugins/zentao/tasks/execution_convertor.go
new file mode 100644
index 000000000..a53ebb802
--- /dev/null
+++ b/plugins/zentao/tasks/execution_convertor.go
@@ -0,0 +1,93 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License.  You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package tasks
+
+import (
+	"github.com/apache/incubator-devlake/errors"
+	"github.com/apache/incubator-devlake/models/domainlayer"
+	"github.com/apache/incubator-devlake/models/domainlayer/didgen"
+	"github.com/apache/incubator-devlake/models/domainlayer/ticket"
+	"github.com/apache/incubator-devlake/plugins/core"
+	"github.com/apache/incubator-devlake/plugins/core/dal"
+	"github.com/apache/incubator-devlake/plugins/helper"
+	"github.com/apache/incubator-devlake/plugins/zentao/models"
+	"reflect"
+)
+
+var _ core.SubTaskEntryPoint = ConvertExecutions
+
+var ConvertExecutionMeta = core.SubTaskMeta{
+	Name:             "convertExecutions",
+	EntryPoint:       ConvertExecutions,
+	EnabledByDefault: true,
+	Description:      "convert Zentao executions",
+	DomainTypes:      []string{core.DOMAIN_TYPE_TICKET},
+}
+
+func ConvertExecutions(taskCtx core.SubTaskContext) errors.Error {
+	data := taskCtx.GetData().(*ZentaoTaskData)
+	db := taskCtx.GetDal()
+	boardIdGen := didgen.NewDomainIdGenerator(&models.ZentaoExecution{})
+	cursor, err := db.Cursor(
+		dal.From(&models.ZentaoExecution{}),
+		dal.Where(`_tool_zentao_executions.id = ? and 
+			_tool_zentao_executions.connection_id = ?`, data.Options.ExecutionId, data.Options.ConnectionId),
+	)
+	if err != nil {
+		return err
+	}
+	defer cursor.Close()
+	convertor, err := helper.NewDataConverter(helper.DataConverterArgs{
+		InputRowType: reflect.TypeOf(models.ZentaoExecution{}),
+		Input:        cursor,
+		RawDataSubTaskArgs: helper.RawDataSubTaskArgs{
+			Ctx: taskCtx,
+			Params: ZentaoApiParams{
+				ConnectionId: data.Options.ConnectionId,
+				ProductId:    data.Options.ProductId,
+				ExecutionId:  data.Options.ExecutionId,
+				ProjectId:    data.Options.ProjectId,
+			},
+			Table: RAW_EXECUTION_TABLE,
+		},
+		Convert: func(inputRow interface{}) ([]interface{}, errors.Error) {
+			toolExecution := inputRow.(*models.ZentaoExecution)
+
+			domainBoard := &ticket.Board{
+				DomainEntity: domainlayer.DomainEntity{
+					Id: boardIdGen.Generate(toolExecution.ConnectionId, toolExecution.Id),
+				},
+				Name:        toolExecution.Name,
+				Description: toolExecution.Description,
+				Url:         toolExecution.Path,
+				CreatedDate: toolExecution.OpenedDate,
+				Type:        toolExecution.Type,
+			}
+
+			results := make([]interface{}, 0)
+			results = append(results, domainBoard)
+			return results, nil
+		},
+	})
+
+	if err != nil {
+		return err
+	}
+
+	return convertor.Execute()
+}
diff --git a/plugins/zentao/tasks/execution_extractor.go b/plugins/zentao/tasks/execution_extractor.go
new file mode 100644
index 000000000..3e1094c6c
--- /dev/null
+++ b/plugins/zentao/tasks/execution_extractor.go
@@ -0,0 +1,128 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License.  You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package tasks
+
+import (
+	"encoding/json"
+	"github.com/apache/incubator-devlake/errors"
+	"github.com/apache/incubator-devlake/plugins/core"
+	"github.com/apache/incubator-devlake/plugins/helper"
+	"github.com/apache/incubator-devlake/plugins/zentao/models"
+)
+
+var _ core.SubTaskEntryPoint = ExtractExecutions
+
+var ExtractExecutionMeta = core.SubTaskMeta{
+	Name:             "extractExecutions",
+	EntryPoint:       ExtractExecutions,
+	EnabledByDefault: true,
+	Description:      "extract Zentao executions",
+	DomainTypes:      []string{core.DOMAIN_TYPE_TICKET},
+}
+
+func ExtractExecutions(taskCtx core.SubTaskContext) errors.Error {
+	data := taskCtx.GetData().(*ZentaoTaskData)
+	extractor, err := helper.NewApiExtractor(helper.ApiExtractorArgs{
+		RawDataSubTaskArgs: helper.RawDataSubTaskArgs{
+			Ctx: taskCtx,
+			Params: ZentaoApiParams{
+				ConnectionId: data.Options.ConnectionId,
+				ProductId:    data.Options.ProductId,
+				ExecutionId:  data.Options.ExecutionId,
+				ProjectId:    data.Options.ProjectId,
+			},
+			Table: RAW_EXECUTION_TABLE,
+		},
+		Extract: func(row *helper.RawData) ([]interface{}, errors.Error) {
+			res := &models.ZentaoExecutionRes{}
+			err := json.Unmarshal(row.Data, res)
+			if err != nil {
+				return nil, errors.Default.WrapRaw(err)
+			}
+			execution := &models.ZentaoExecution{
+				ConnectionId:   data.Options.ConnectionId,
+				Id:             res.ID,
+				Project:        res.Project,
+				Model:          res.Model,
+				Type:           res.Type,
+				Lifetime:       res.Lifetime,
+				Budget:         res.Budget,
+				BudgetUnit:     res.BudgetUnit,
+				Attribute:      res.Attribute,
+				Percent:        res.Percent,
+				Milestone:      res.Milestone,
+				Output:         res.Output,
+				Auth:           res.Auth,
+				Parent:         res.Parent,
+				Path:           res.Path,
+				Grade:          res.Grade,
+				Name:           res.Name,
+				Code:           res.Code,
+				PlanBegin:      res.PlanBegin,
+				PlanEnd:        res.PlanEnd,
+				RealBegan:      res.RealBegan,
+				RealEnd:        res.RealEnd,
+				Days:           res.Days,
+				Status:         res.Status,
+				SubStatus:      res.SubStatus,
+				Pri:            res.Pri,
+				Description:    res.Description,
+				Version:        res.Version,
+				ParentVersion:  res.ParentVersion,
+				PlanDuration:   res.PlanDuration,
+				RealDuration:   res.RealDuration,
+				OpenedById:     res.OpenedBy.ID,
+				OpenedDate:     res.OpenedDate,
+				OpenedVersion:  res.OpenedVersion,
+				LastEditedById: res.LastEditedBy.ID,
+				LastEditedDate: res.LastEditedDate,
+				ClosedById:     res.ClosedBy.ID,
+				ClosedDate:     res.ClosedDate,
+				CanceledById:   res.CanceledBy.ID,
+				CanceledDate:   res.CanceledDate,
+				SuspendedDate:  res.SuspendedDate,
+				POId:           res.PO.ID,
+				PMId:           res.PM.ID,
+				QDId:           res.QD.ID,
+				RDId:           res.RD.ID,
+				Team:           res.Team,
+				Acl:            res.Acl,
+				OrderIn:        res.OrderIn,
+				Vision:         res.Vision,
+				DisplayCards:   res.DisplayCards,
+				FluidBoard:     res.FluidBoard,
+				Deleted:        res.Deleted,
+				TotalHours:     res.TotalHours,
+				TotalEstimate:  res.TotalEstimate,
+				TotalConsumed:  res.TotalConsumed,
+				TotalLeft:      res.TotalLeft,
+				Progress:       res.Progress,
+				CaseReview:     res.CaseReview,
+			}
+			results := make([]interface{}, 0)
+			results = append(results, execution)
+			return results, nil
+		},
+	})
+
+	if err != nil {
+		return err
+	}
+
+	return extractor.Execute()
+}
diff --git a/plugins/zentao/tasks/product_collector.go b/plugins/zentao/tasks/product_collector.go
new file mode 100644
index 000000000..cc1e9d8cb
--- /dev/null
+++ b/plugins/zentao/tasks/product_collector.go
@@ -0,0 +1,80 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License.  You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package tasks
+
+import (
+	"encoding/json"
+	"fmt"
+	"github.com/apache/incubator-devlake/errors"
+	"github.com/apache/incubator-devlake/plugins/core"
+	"github.com/apache/incubator-devlake/plugins/helper"
+	"io"
+	"net/http"
+	"net/url"
+)
+
+const RAW_PRODUCT_TABLE = "zentao_api_products"
+
+var _ core.SubTaskEntryPoint = CollectProduct
+
+func CollectProduct(taskCtx core.SubTaskContext) errors.Error {
+	data := taskCtx.GetData().(*ZentaoTaskData)
+	collector, err := helper.NewApiCollector(helper.ApiCollectorArgs{
+		RawDataSubTaskArgs: helper.RawDataSubTaskArgs{
+			Ctx: taskCtx,
+			Params: ZentaoApiParams{
+				ConnectionId: data.Options.ConnectionId,
+				ExecutionId:  data.Options.ExecutionId,
+				ProductId:    data.Options.ProductId,
+				ProjectId:    data.Options.ProjectId,
+			},
+			Table: RAW_PRODUCT_TABLE,
+		},
+		ApiClient: data.ApiClient,
+		PageSize:  100,
+		// TODO write which api would you want request
+		UrlTemplate: "products/{{ .Params.ProductId }}",
+		Query: func(reqData *helper.RequestData) (url.Values, errors.Error) {
+			query := url.Values{}
+			query.Set("page", fmt.Sprintf("%v", reqData.Pager.Page))
+			query.Set("limit", fmt.Sprintf("%v", reqData.Pager.Size))
+			return query, nil
+		},
+		GetTotalPages: GetTotalPagesFromResponse,
+		ResponseParser: func(res *http.Response) ([]json.RawMessage, errors.Error) {
+			body, err := io.ReadAll(res.Body)
+			if err != nil {
+				return nil, errors.Default.Wrap(err, "error reading endpoint response by Zentao product collector")
+			}
+			res.Body.Close()
+			return []json.RawMessage{body}, nil
+		},
+	})
+	if err != nil {
+		return err
+	}
+
+	return collector.Execute()
+}
+
+var CollectProductMeta = core.SubTaskMeta{
+	Name:             "CollectProduct",
+	EntryPoint:       CollectProduct,
+	EnabledByDefault: true,
+	Description:      "Collect Product data from Zentao api",
+}
diff --git a/plugins/zentao/tasks/product_convertor.go b/plugins/zentao/tasks/product_convertor.go
new file mode 100644
index 000000000..83fa41154
--- /dev/null
+++ b/plugins/zentao/tasks/product_convertor.go
@@ -0,0 +1,92 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License.  You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package tasks
+
+import (
+	"github.com/apache/incubator-devlake/errors"
+	"github.com/apache/incubator-devlake/models/domainlayer"
+	"github.com/apache/incubator-devlake/models/domainlayer/didgen"
+	"github.com/apache/incubator-devlake/models/domainlayer/ticket"
+	"github.com/apache/incubator-devlake/plugins/core"
+	"github.com/apache/incubator-devlake/plugins/core/dal"
+	"github.com/apache/incubator-devlake/plugins/helper"
+	"github.com/apache/incubator-devlake/plugins/zentao/models"
+	"reflect"
+)
+
+var _ core.SubTaskEntryPoint = ConvertProducts
+
+var ConvertProductMeta = core.SubTaskMeta{
+	Name:             "convertProducts",
+	EntryPoint:       ConvertProducts,
+	EnabledByDefault: true,
+	Description:      "convert Zentao products",
+	DomainTypes:      []string{core.DOMAIN_TYPE_TICKET},
+}
+
+func ConvertProducts(taskCtx core.SubTaskContext) errors.Error {
+	data := taskCtx.GetData().(*ZentaoTaskData)
+	db := taskCtx.GetDal()
+	boardIdGen := didgen.NewDomainIdGenerator(&models.ZentaoProduct{})
+	cursor, err := db.Cursor(
+		dal.From(&models.ZentaoProduct{}),
+		dal.Where(`_tool_zentao_products.id = ? and 
+			_tool_zentao_products.connection_id = ?`, data.Options.ProductId, data.Options.ConnectionId),
+	)
+	if err != nil {
+		return err
+	}
+	defer cursor.Close()
+	convertor, err := helper.NewDataConverter(helper.DataConverterArgs{
+		InputRowType: reflect.TypeOf(models.ZentaoProduct{}),
+		Input:        cursor,
+		RawDataSubTaskArgs: helper.RawDataSubTaskArgs{
+			Ctx: taskCtx,
+			Params: ZentaoApiParams{
+				ConnectionId: data.Options.ConnectionId,
+				ExecutionId:  data.Options.ExecutionId,
+				ProductId:    data.Options.ProductId,
+				ProjectId:    data.Options.ProjectId,
+			},
+			Table: RAW_PRODUCT_TABLE,
+		},
+		Convert: func(inputRow interface{}) ([]interface{}, errors.Error) {
+			toolProduct := inputRow.(*models.ZentaoProduct)
+
+			domainBoard := &ticket.Board{
+				DomainEntity: domainlayer.DomainEntity{
+					Id: boardIdGen.Generate(toolProduct.ConnectionId, toolProduct.Id),
+				},
+				Name:        toolProduct.Name,
+				Description: toolProduct.Description,
+				CreatedDate: toolProduct.CreatedDate,
+				Type:        toolProduct.Type,
+			}
+
+			results := make([]interface{}, 0)
+			results = append(results, domainBoard)
+			return results, nil
+		},
+	})
+
+	if err != nil {
+		return err
+	}
+
+	return convertor.Execute()
+}
diff --git a/plugins/zentao/tasks/product_extractor.go b/plugins/zentao/tasks/product_extractor.go
new file mode 100644
index 000000000..21a624846
--- /dev/null
+++ b/plugins/zentao/tasks/product_extractor.go
@@ -0,0 +1,101 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License.  You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package tasks
+
+import (
+	"encoding/json"
+	"github.com/apache/incubator-devlake/errors"
+	"github.com/apache/incubator-devlake/plugins/core"
+	"github.com/apache/incubator-devlake/plugins/helper"
+	"github.com/apache/incubator-devlake/plugins/zentao/models"
+)
+
+var _ core.SubTaskEntryPoint = ExtractProducts
+
+var ExtractProductMeta = core.SubTaskMeta{
+	Name:             "extractProducts",
+	EntryPoint:       ExtractProducts,
+	EnabledByDefault: true,
+	Description:      "extract Zentao products",
+	DomainTypes:      []string{core.DOMAIN_TYPE_TICKET},
+}
+
+func ExtractProducts(taskCtx core.SubTaskContext) errors.Error {
+	data := taskCtx.GetData().(*ZentaoTaskData)
+	extractor, err := helper.NewApiExtractor(helper.ApiExtractorArgs{
+		RawDataSubTaskArgs: helper.RawDataSubTaskArgs{
+			Ctx: taskCtx,
+			Params: ZentaoApiParams{
+				ConnectionId: data.Options.ConnectionId,
+				ExecutionId:  data.Options.ExecutionId,
+				ProductId:    data.Options.ProductId,
+				ProjectId:    data.Options.ProjectId,
+			},
+			Table: RAW_PRODUCT_TABLE,
+		},
+		Extract: func(row *helper.RawData) ([]interface{}, errors.Error) {
+			res := &models.ZentaoProductRes{}
+			err := json.Unmarshal(row.Data, res)
+			if err != nil {
+				return nil, errors.Default.Wrap(err, "error reading endpoint response by Zentao product extractor")
+			}
+			product := &models.ZentaoProduct{
+				ConnectionId:   data.Options.ConnectionId,
+				Id:             uint64(res.ID),
+				Program:        res.Program,
+				Name:           res.Name,
+				Code:           res.Code,
+				Bind:           res.Bind,
+				Line:           res.Line,
+				Type:           res.Type,
+				Status:         res.Status,
+				SubStatus:      res.SubStatus,
+				Description:    res.Description,
+				POId:           res.PO.ID,
+				QDId:           res.QD.ID,
+				RDId:           res.RD.ID,
+				Acl:            res.Acl,
+				Reviewer:       res.Reviewer,
+				CreatedById:    res.CreatedBy.ID,
+				CreatedDate:    res.CreatedDate,
+				CreatedVersion: res.CreatedVersion,
+				OrderIn:        res.OrderIn,
+				Deleted:        res.Deleted,
+				Plans:          res.Plans,
+				Releases:       res.Releases,
+				Builds:         res.Builds,
+				Cases:          res.Cases,
+				Projects:       res.Projects,
+				Executions:     res.Executions,
+				Bugs:           res.Bugs,
+				Docs:           res.Docs,
+				Progress:       res.Progress,
+				CaseReview:     res.CaseReview,
+			}
+			results := make([]interface{}, 0)
+			results = append(results, product)
+			return results, nil
+		},
+	})
+
+	if err != nil {
+		return err
+	}
+
+	return extractor.Execute()
+}
diff --git a/plugins/zentao/tasks/project_collector.go b/plugins/zentao/tasks/project_collector.go
new file mode 100644
index 000000000..6d737a0b5
--- /dev/null
+++ b/plugins/zentao/tasks/project_collector.go
@@ -0,0 +1,79 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License.  You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package tasks
+
+import (
+	"encoding/json"
+	"fmt"
+	"github.com/apache/incubator-devlake/errors"
+	"github.com/apache/incubator-devlake/plugins/core"
+	"github.com/apache/incubator-devlake/plugins/helper"
+	"net/http"
+	"net/url"
+)
+
+const RAW_PROJECT_TABLE = "zentao_api_projects"
+
+var _ core.SubTaskEntryPoint = CollectProject
+
+func CollectProject(taskCtx core.SubTaskContext) errors.Error {
+	data := taskCtx.GetData().(*ZentaoTaskData)
+	collector, err := helper.NewApiCollector(helper.ApiCollectorArgs{
+		RawDataSubTaskArgs: helper.RawDataSubTaskArgs{
+			Ctx: taskCtx,
+			Params: ZentaoApiParams{
+				ConnectionId: data.Options.ConnectionId,
+				ExecutionId:  data.Options.ExecutionId,
+				ProductId:    data.Options.ProductId,
+				ProjectId:    data.Options.ProjectId,
+			},
+			Table: RAW_PROJECT_TABLE,
+		},
+		ApiClient: data.ApiClient,
+
+		PageSize: 100,
+		// TODO write which api would you want request
+		UrlTemplate: "projects",
+		Query: func(reqData *helper.RequestData) (url.Values, errors.Error) {
+			query := url.Values{}
+			query.Set("page", fmt.Sprintf("%v", reqData.Pager.Page))
+			query.Set("limit", fmt.Sprintf("%v", reqData.Pager.Size))
+			return query, nil
+		},
+		GetTotalPages: GetTotalPagesFromResponse,
+		ResponseParser: func(res *http.Response) ([]json.RawMessage, errors.Error) {
+			var data struct {
+				Projects []json.RawMessage `json:"projects"`
+			}
+			err := helper.UnmarshalResponse(res, &data)
+			return data.Projects, err
+		},
+	})
+	if err != nil {
+		return err
+	}
+
+	return collector.Execute()
+}
+
+var CollectProjectMeta = core.SubTaskMeta{
+	Name:             "CollectProject",
+	EntryPoint:       CollectProject,
+	EnabledByDefault: true,
+	Description:      "Collect Project data from Zentao api",
+}
diff --git a/plugins/zentao/tasks/project_extractor.go b/plugins/zentao/tasks/project_extractor.go
new file mode 100644
index 000000000..ce1463471
--- /dev/null
+++ b/plugins/zentao/tasks/project_extractor.go
@@ -0,0 +1,69 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License.  You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package tasks
+
+import (
+	"encoding/json"
+	"github.com/apache/incubator-devlake/errors"
+	"github.com/apache/incubator-devlake/plugins/core"
+	"github.com/apache/incubator-devlake/plugins/helper"
+	"github.com/apache/incubator-devlake/plugins/zentao/models"
+)
+
+var _ core.SubTaskEntryPoint = ExtractProjects
+
+var ExtractProjectMeta = core.SubTaskMeta{
+	Name:             "extractProjects",
+	EntryPoint:       ExtractProjects,
+	EnabledByDefault: true,
+	Description:      "extract Zentao projects",
+	DomainTypes:      []string{core.DOMAIN_TYPE_TICKET},
+}
+
+func ExtractProjects(taskCtx core.SubTaskContext) errors.Error {
+	data := taskCtx.GetData().(*ZentaoTaskData)
+	extractor, err := helper.NewApiExtractor(helper.ApiExtractorArgs{
+		RawDataSubTaskArgs: helper.RawDataSubTaskArgs{
+			Ctx: taskCtx,
+			Params: ZentaoApiParams{
+				ConnectionId: data.Options.ConnectionId,
+				ExecutionId:  data.Options.ExecutionId,
+				ProductId:    data.Options.ProductId,
+				ProjectId:    data.Options.ProjectId,
+			},
+			Table: RAW_PROJECT_TABLE,
+		},
+		Extract: func(row *helper.RawData) ([]interface{}, errors.Error) {
+			project := &models.ZentaoProject{}
+			err := json.Unmarshal(row.Data, project)
+			if err != nil {
+				return nil, errors.Default.Wrap(err, "error reading endpoint response by Zentao project executor")
+			}
+			project.ConnectionId = data.Options.ConnectionId
+			results := make([]interface{}, 0)
+			results = append(results, project)
+			return results, nil
+		},
+	})
+
+	if err != nil {
+		return err
+	}
+
+	return extractor.Execute()
+}
diff --git a/plugins/zentao/tasks/shared.go b/plugins/zentao/tasks/shared.go
new file mode 100644
index 000000000..2dc63d789
--- /dev/null
+++ b/plugins/zentao/tasks/shared.go
@@ -0,0 +1,35 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License.  You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package tasks
+
+import (
+	"github.com/apache/incubator-devlake/errors"
+	"net/http"
+
+	"github.com/apache/incubator-devlake/plugins/helper"
+)
+
+func GetTotalPagesFromResponse(res *http.Response, args *helper.ApiCollectorArgs) (int, errors.Error) {
+	body := &ZentaoPagination{}
+	err := helper.UnmarshalResponse(res, body)
+	if err != nil {
+		return 0, err
+	}
+	return body.Page, nil
+
+}
diff --git a/plugins/zentao/tasks/story_collector.go b/plugins/zentao/tasks/story_collector.go
new file mode 100644
index 000000000..d7db9c4f7
--- /dev/null
+++ b/plugins/zentao/tasks/story_collector.go
@@ -0,0 +1,83 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License.  You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package tasks
+
+import (
+	"encoding/json"
+	"fmt"
+	"github.com/apache/incubator-devlake/errors"
+	"github.com/apache/incubator-devlake/plugins/core"
+	"github.com/apache/incubator-devlake/plugins/helper"
+	"net/http"
+	"net/url"
+)
+
+const RAW_STORY_TABLE = "zentao_api_stories"
+
+var _ core.SubTaskEntryPoint = CollectStory
+
+func CollectStory(taskCtx core.SubTaskContext) errors.Error {
+	data := taskCtx.GetData().(*ZentaoTaskData)
+	collector, err := helper.NewApiCollector(helper.ApiCollectorArgs{
+		RawDataSubTaskArgs: helper.RawDataSubTaskArgs{
+			Ctx: taskCtx,
+			Params: ZentaoApiParams{
+				ConnectionId: data.Options.ConnectionId,
+				ProductId:    data.Options.ProductId,
+				ExecutionId:  data.Options.ExecutionId,
+				ProjectId:    data.Options.ProjectId,
+			},
+			Table: RAW_STORY_TABLE,
+		},
+		ApiClient: data.ApiClient,
+
+		PageSize: 100,
+		// TODO write which api would you want request
+		UrlTemplate: "/products/{{ .Params.ProjectId }}/stories",
+		Query: func(reqData *helper.RequestData) (url.Values, errors.Error) {
+			query := url.Values{}
+			query.Set("page", fmt.Sprintf("%v", reqData.Pager.Page))
+			query.Set("limit", fmt.Sprintf("%v", reqData.Pager.Size))
+			query.Set("status", "allstory")
+			return query, nil
+		},
+		GetTotalPages: GetTotalPagesFromResponse,
+		ResponseParser: func(res *http.Response) ([]json.RawMessage, errors.Error) {
+			var data struct {
+				Story []json.RawMessage `json:"stories"`
+			}
+			err := helper.UnmarshalResponse(res, &data)
+			if err != nil {
+				return nil, errors.Default.Wrap(err, "error reading endpoint response by Zentao bug collector")
+			}
+			return data.Story, nil
+		},
+	})
+	if err != nil {
+		return err
+	}
+
+	return collector.Execute()
+}
+
+var CollectStoryMeta = core.SubTaskMeta{
+	Name:             "CollectStory",
+	EntryPoint:       CollectStory,
+	EnabledByDefault: true,
+	Description:      "Collect Story data from Zentao api",
+}
diff --git a/plugins/zentao/tasks/story_convertor.go b/plugins/zentao/tasks/story_convertor.go
new file mode 100644
index 000000000..95b713535
--- /dev/null
+++ b/plugins/zentao/tasks/story_convertor.go
@@ -0,0 +1,115 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License.  You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package tasks
+
+import (
+	"github.com/apache/incubator-devlake/errors"
+	"github.com/apache/incubator-devlake/models/domainlayer"
+	"github.com/apache/incubator-devlake/models/domainlayer/didgen"
+	"github.com/apache/incubator-devlake/models/domainlayer/ticket"
+	"github.com/apache/incubator-devlake/plugins/core"
+	"github.com/apache/incubator-devlake/plugins/core/dal"
+	"github.com/apache/incubator-devlake/plugins/helper"
+	"github.com/apache/incubator-devlake/plugins/zentao/models"
+	"reflect"
+	"strconv"
+)
+
+var _ core.SubTaskEntryPoint = ConvertStory
+
+var ConvertStoryMeta = core.SubTaskMeta{
+	Name:             "convertStory",
+	EntryPoint:       ConvertStory,
+	EnabledByDefault: true,
+	Description:      "convert Zentao story",
+	DomainTypes:      []string{core.DOMAIN_TYPE_TICKET},
+}
+
+func ConvertStory(taskCtx core.SubTaskContext) errors.Error {
+	data := taskCtx.GetData().(*ZentaoTaskData)
+	db := taskCtx.GetDal()
+	storyIdGen := didgen.NewDomainIdGenerator(&models.ZentaoStory{})
+	boardIdGen := didgen.NewDomainIdGenerator(&models.ZentaoProduct{})
+	cursor, err := db.Cursor(
+		dal.From(&models.ZentaoStory{}),
+		dal.Where(`_tool_zentao_stories.product = ? and 
+			_tool_zentao_stories.connection_id = ?`, data.Options.ExecutionId, data.Options.ConnectionId),
+	)
+	if err != nil {
+		return err
+	}
+	defer cursor.Close()
+	convertor, err := helper.NewDataConverter(helper.DataConverterArgs{
+		InputRowType: reflect.TypeOf(models.ZentaoStory{}),
+		Input:        cursor,
+		RawDataSubTaskArgs: helper.RawDataSubTaskArgs{
+			Ctx: taskCtx,
+			Params: ZentaoApiParams{
+				ConnectionId: data.Options.ConnectionId,
+				ProductId:    data.Options.ProductId,
+				ExecutionId:  data.Options.ExecutionId,
+				ProjectId:    data.Options.ProjectId,
+			},
+			Table: RAW_STORY_TABLE,
+		},
+		Convert: func(inputRow interface{}) ([]interface{}, errors.Error) {
+			toolEntity := inputRow.(*models.ZentaoStory)
+
+			domainEntity := &ticket.Issue{
+				DomainEntity: domainlayer.DomainEntity{
+					Id: storyIdGen.Generate(toolEntity.ConnectionId, toolEntity.ID),
+				},
+				IssueKey:       strconv.FormatUint(toolEntity.ID, 10),
+				Title:          toolEntity.Title,
+				Type:           toolEntity.Type,
+				OriginalStatus: toolEntity.Stage,
+				ResolutionDate: toolEntity.ClosedDate,
+				CreatedDate:    toolEntity.OpenedDate,
+				UpdatedDate:    toolEntity.LastEditedDate,
+				ParentIssueId:  storyIdGen.Generate(data.Options.ConnectionId, toolEntity.Parent),
+				Priority:       string(rune(toolEntity.Pri)),
+				CreatorId:      strconv.FormatUint(toolEntity.OpenedById, 10),
+				CreatorName:    toolEntity.OpenedByName,
+				AssigneeId:     strconv.FormatUint(toolEntity.AssignedToId, 10),
+				AssigneeName:   toolEntity.AssignedToName,
+			}
+			switch toolEntity.Stage {
+			case "closed":
+				domainEntity.Status = "DONE"
+			default:
+				domainEntity.Status = "IN_PROGRESS"
+			}
+			if toolEntity.ClosedDate != nil {
+				domainEntity.LeadTimeMinutes = int64(toolEntity.ClosedDate.Sub(*toolEntity.OpenedDate).Minutes())
+			}
+			domainBoardIssue := &ticket.BoardIssue{
+				BoardId: boardIdGen.Generate(data.Options.ConnectionId, data.Options.ProductId),
+				IssueId: domainEntity.Id,
+			}
+			results := make([]interface{}, 0)
+			results = append(results, domainEntity, domainBoardIssue)
+			return results, nil
+		},
+	})
+
+	if err != nil {
+		return err
+	}
+
+	return convertor.Execute()
+}
diff --git a/plugins/zentao/tasks/story_extractor.go b/plugins/zentao/tasks/story_extractor.go
new file mode 100644
index 000000000..b144d738a
--- /dev/null
+++ b/plugins/zentao/tasks/story_extractor.go
@@ -0,0 +1,125 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License.  You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package tasks
+
+import (
+	"encoding/json"
+	"github.com/apache/incubator-devlake/errors"
+	"github.com/apache/incubator-devlake/plugins/core"
+	"github.com/apache/incubator-devlake/plugins/helper"
+	"github.com/apache/incubator-devlake/plugins/zentao/models"
+)
+
+var _ core.SubTaskEntryPoint = ExtractStory
+
+var ExtractStoryMeta = core.SubTaskMeta{
+	Name:             "extractStory",
+	EntryPoint:       ExtractStory,
+	EnabledByDefault: true,
+	Description:      "extract Zentao story",
+	DomainTypes:      []string{core.DOMAIN_TYPE_TICKET},
+}
+
+func ExtractStory(taskCtx core.SubTaskContext) errors.Error {
+	data := taskCtx.GetData().(*ZentaoTaskData)
+	extractor, err := helper.NewApiExtractor(helper.ApiExtractorArgs{
+		RawDataSubTaskArgs: helper.RawDataSubTaskArgs{
+			Ctx: taskCtx,
+			Params: ZentaoApiParams{
+				ConnectionId: data.Options.ConnectionId,
+				ProductId:    data.Options.ProductId,
+				ExecutionId:  data.Options.ExecutionId,
+				ProjectId:    data.Options.ProjectId,
+			},
+			Table: RAW_STORY_TABLE,
+		},
+		Extract: func(row *helper.RawData) ([]interface{}, errors.Error) {
+			res := &models.ZentaoStoryRes{}
+			err := json.Unmarshal(row.Data, res)
+			if err != nil {
+				return nil, errors.Default.WrapRaw(err)
+			}
+			story := &models.ZentaoStory{
+				ConnectionId:     data.Options.ConnectionId,
+				ID:               res.ID,
+				Product:          res.Product,
+				Branch:           res.Branch,
+				Version:          res.Version,
+				OrderIn:          0,
+				Vision:           res.Vision,
+				Parent:           res.Parent,
+				Module:           res.Module,
+				Plan:             res.Plan,
+				Source:           res.Source,
+				SourceNote:       res.SourceNote,
+				FromBug:          res.FromBug,
+				Feedback:         res.Feedback,
+				Title:            res.Title,
+				Keywords:         res.Keywords,
+				Type:             res.Type,
+				Category:         res.Category,
+				Pri:              res.Pri,
+				Estimate:         res.Estimate,
+				Status:           res.Status,
+				SubStatus:        res.SubStatus,
+				Color:            res.Color,
+				Stage:            res.Stage,
+				Lib:              res.Lib,
+				FromStory:        res.FromStory,
+				FromVersion:      res.FromVersion,
+				OpenedById:       res.OpenedBy.ID,
+				OpenedByName:     res.OpenedBy.Realname,
+				OpenedDate:       res.OpenedDate,
+				AssignedToId:     res.AssignedTo.ID,
+				AssignedToName:   res.AssignedTo.Realname,
+				AssignedDate:     res.AssignedDate,
+				ApprovedDate:     res.ApprovedDate,
+				LastEditedId:     res.LastEditedBy.ID,
+				LastEditedDate:   res.LastEditedDate,
+				ChangedDate:      res.ChangedDate,
+				ReviewedById:     res.ReviewedBy.ID,
+				ReviewedDate:     res.ReviewedDate,
+				ClosedId:         res.ClosedBy.ID,
+				ClosedDate:       res.ClosedDate,
+				ClosedReason:     res.ClosedReason,
+				ActivatedDate:    res.ActivatedDate,
+				ToBug:            res.ToBug,
+				ChildStories:     res.ChildStories,
+				LinkStories:      res.LinkStories,
+				LinkRequirements: res.LinkRequirements,
+				DuplicateStory:   res.DuplicateStory,
+				StoryChanged:     res.StoryChanged,
+				FeedbackBy:       res.FeedbackBy,
+				NotifyEmail:      res.NotifyEmail,
+				URChanged:        res.URChanged,
+				Deleted:          res.Deleted,
+				PriOrder:         res.PriOrder,
+				PlanTitle:        res.PlanTitle,
+			}
+			results := make([]interface{}, 0)
+			results = append(results, story)
+			return results, nil
+		},
+	})
+
+	if err != nil {
+		return err
+	}
+
+	return extractor.Execute()
+}
diff --git a/plugins/zentao/tasks/task_collector.go b/plugins/zentao/tasks/task_collector.go
new file mode 100644
index 000000000..8094870cf
--- /dev/null
+++ b/plugins/zentao/tasks/task_collector.go
@@ -0,0 +1,82 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License.  You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package tasks
+
+import (
+	"encoding/json"
+	"fmt"
+	"github.com/apache/incubator-devlake/errors"
+	"github.com/apache/incubator-devlake/plugins/core"
+	"github.com/apache/incubator-devlake/plugins/helper"
+	"net/http"
+	"net/url"
+)
+
+const RAW_TASK_TABLE = "zentao_api_tasks"
+
+var _ core.SubTaskEntryPoint = CollectTask
+
+func CollectTask(taskCtx core.SubTaskContext) errors.Error {
+	data := taskCtx.GetData().(*ZentaoTaskData)
+	collector, err := helper.NewApiCollector(helper.ApiCollectorArgs{
+		RawDataSubTaskArgs: helper.RawDataSubTaskArgs{
+			Ctx: taskCtx,
+			Params: ZentaoApiParams{
+				ConnectionId: data.Options.ConnectionId,
+				ProductId:    data.Options.ProductId,
+				ExecutionId:  data.Options.ExecutionId,
+				ProjectId:    data.Options.ProjectId,
+			},
+			Table: RAW_TASK_TABLE,
+		},
+		ApiClient: data.ApiClient,
+
+		PageSize: 100,
+		// TODO write which api would you want request
+		UrlTemplate: "/executions/{{ .Params.ExecutionId }}/tasks",
+		Query: func(reqData *helper.RequestData) (url.Values, errors.Error) {
+			query := url.Values{}
+			query.Set("page", fmt.Sprintf("%v", reqData.Pager.Page))
+			query.Set("limit", fmt.Sprintf("%v", reqData.Pager.Size))
+			return query, nil
+		},
+		GetTotalPages: GetTotalPagesFromResponse,
+		ResponseParser: func(res *http.Response) ([]json.RawMessage, errors.Error) {
+			var data struct {
+				Task []json.RawMessage `json:"tasks"`
+			}
+			err := helper.UnmarshalResponse(res, &data)
+			if err != nil {
+				return nil, errors.Default.Wrap(err, "error reading endpoint response by Zentao bug collector")
+			}
+			return data.Task, nil
+		},
+	})
+	if err != nil {
+		return err
+	}
+
+	return collector.Execute()
+}
+
+var CollectTaskMeta = core.SubTaskMeta{
+	Name:             "CollectTask",
+	EntryPoint:       CollectTask,
+	EnabledByDefault: true,
+	Description:      "Collect Task data from Zentao api",
+}
diff --git a/plugins/zentao/tasks/task_convertor.go b/plugins/zentao/tasks/task_convertor.go
new file mode 100644
index 000000000..9063289fe
--- /dev/null
+++ b/plugins/zentao/tasks/task_convertor.go
@@ -0,0 +1,117 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License.  You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package tasks
+
+import (
+	"github.com/apache/incubator-devlake/errors"
+	"github.com/apache/incubator-devlake/models/domainlayer"
+	"github.com/apache/incubator-devlake/models/domainlayer/didgen"
+	"github.com/apache/incubator-devlake/models/domainlayer/ticket"
+	"github.com/apache/incubator-devlake/plugins/core"
+	"github.com/apache/incubator-devlake/plugins/core/dal"
+	"github.com/apache/incubator-devlake/plugins/helper"
+	"github.com/apache/incubator-devlake/plugins/zentao/models"
+	"reflect"
+	"strconv"
+)
+
+var _ core.SubTaskEntryPoint = ConvertTask
+
+var ConvertTaskMeta = core.SubTaskMeta{
+	Name:             "convertTask",
+	EntryPoint:       ConvertTask,
+	EnabledByDefault: true,
+	Description:      "convert Zentao task",
+	DomainTypes:      []string{core.DOMAIN_TYPE_TICKET},
+}
+
+func ConvertTask(taskCtx core.SubTaskContext) errors.Error {
+	data := taskCtx.GetData().(*ZentaoTaskData)
+	db := taskCtx.GetDal()
+	storyIdGen := didgen.NewDomainIdGenerator(&models.ZentaoStory{})
+	boardIdGen := didgen.NewDomainIdGenerator(&models.ZentaoProduct{})
+	taskIdGen := didgen.NewDomainIdGenerator(&models.ZentaoTask{})
+	cursor, err := db.Cursor(
+		dal.From(&models.ZentaoTask{}),
+		dal.Where(`_tool_zentao_tasks.execution = ? and 
+			_tool_zentao_tasks.connection_id = ?`, data.Options.ExecutionId, data.Options.ConnectionId),
+	)
+	if err != nil {
+		return err
+	}
+	defer cursor.Close()
+	convertor, err := helper.NewDataConverter(helper.DataConverterArgs{
+		InputRowType: reflect.TypeOf(models.ZentaoTask{}),
+		Input:        cursor,
+		RawDataSubTaskArgs: helper.RawDataSubTaskArgs{
+			Ctx: taskCtx,
+			Params: ZentaoApiParams{
+				ConnectionId: data.Options.ConnectionId,
+				ProductId:    data.Options.ProductId,
+				ExecutionId:  data.Options.ExecutionId,
+				ProjectId:    data.Options.ProjectId,
+			},
+			Table: RAW_TASK_TABLE,
+		},
+		Convert: func(inputRow interface{}) ([]interface{}, errors.Error) {
+			toolEntity := inputRow.(*models.ZentaoTask)
+
+			domainEntity := &ticket.Issue{
+				DomainEntity: domainlayer.DomainEntity{
+					Id: taskIdGen.Generate(toolEntity.ConnectionId, toolEntity.ID),
+				},
+				IssueKey:       strconv.FormatUint(toolEntity.ID, 10),
+				Title:          toolEntity.Name,
+				Description:    toolEntity.Description,
+				Type:           toolEntity.Type,
+				OriginalStatus: toolEntity.Status,
+				ResolutionDate: toolEntity.ClosedDate,
+				CreatedDate:    toolEntity.OpenedDate,
+				UpdatedDate:    toolEntity.LastEditedDate,
+				ParentIssueId:  storyIdGen.Generate(data.Options.ConnectionId, toolEntity.Parent),
+				Priority:       string(rune(toolEntity.Pri)),
+				CreatorId:      strconv.FormatUint(toolEntity.OpenedById, 10),
+				CreatorName:    toolEntity.OpenedByName,
+				AssigneeId:     strconv.FormatUint(toolEntity.AssignedToId, 10),
+				AssigneeName:   toolEntity.AssignedToName,
+			}
+			switch toolEntity.Status {
+			case "done", "closed":
+				domainEntity.Status = "DONE"
+			default:
+				domainEntity.Status = "IN_PROGRESS"
+			}
+			if toolEntity.ClosedDate != nil {
+				domainEntity.LeadTimeMinutes = int64(toolEntity.ClosedDate.Sub(*toolEntity.OpenedDate).Minutes())
+			}
+			domainBoardIssue := &ticket.BoardIssue{
+				BoardId: boardIdGen.Generate(data.Options.ConnectionId, data.Options.ProductId),
+				IssueId: domainEntity.Id,
+			}
+			results := make([]interface{}, 0)
+			results = append(results, domainEntity, domainBoardIssue)
+			return results, nil
+		},
+	})
+
+	if err != nil {
+		return err
+	}
+
+	return convertor.Execute()
+}
diff --git a/plugins/zentao/tasks/task_data.go b/plugins/zentao/tasks/task_data.go
new file mode 100644
index 000000000..3a3d8ac74
--- /dev/null
+++ b/plugins/zentao/tasks/task_data.go
@@ -0,0 +1,62 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License.  You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package tasks
+
+import (
+	"fmt"
+	"github.com/apache/incubator-devlake/plugins/helper"
+	"github.com/mitchellh/mapstructure"
+)
+
+type ZentaoApiParams struct {
+	ConnectionId uint64
+	ProductId    uint64
+	ExecutionId  uint64
+	ProjectId    uint64
+}
+
+type ZentaoOptions struct {
+	// TODO add some custom options here if necessary
+	// options means some custom params required by plugin running.
+	// Such As How many rows do your want
+	// You can use it in sub tasks and you need pass it in main.go and pipelines.
+	ConnectionId uint64 `json:"connectionId"`
+	ProductId    uint64
+	ExecutionId  uint64
+	ProjectId    uint64
+	Tasks        []string `json:"tasks,omitempty"`
+	Since        string
+}
+
+type ZentaoTaskData struct {
+	Options   *ZentaoOptions
+	ApiClient *helper.ApiAsyncClient
+}
+
+func DecodeAndValidateTaskOptions(options map[string]interface{}) (*ZentaoOptions, error) {
+	var op ZentaoOptions
+	err := mapstructure.Decode(options, &op)
+	if err != nil {
+		return nil, err
+	}
+
+	if op.ConnectionId == 0 {
+		return nil, fmt.Errorf("connectionId is invalid")
+	}
+	return &op, nil
+}
diff --git a/plugins/zentao/tasks/task_extractor.go b/plugins/zentao/tasks/task_extractor.go
new file mode 100644
index 000000000..d736bc008
--- /dev/null
+++ b/plugins/zentao/tasks/task_extractor.go
@@ -0,0 +1,139 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License.  You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package tasks
+
+import (
+	"encoding/json"
+	"github.com/apache/incubator-devlake/errors"
+	"github.com/apache/incubator-devlake/plugins/core"
+	"github.com/apache/incubator-devlake/plugins/helper"
+	"github.com/apache/incubator-devlake/plugins/zentao/models"
+)
+
+var _ core.SubTaskEntryPoint = ExtractTask
+
+var ExtractTaskMeta = core.SubTaskMeta{
+	Name:             "extractTask",
+	EntryPoint:       ExtractTask,
+	EnabledByDefault: true,
+	Description:      "extract Zentao task",
+	DomainTypes:      []string{core.DOMAIN_TYPE_TICKET},
+}
+
+func ExtractTask(taskCtx core.SubTaskContext) errors.Error {
+	data := taskCtx.GetData().(*ZentaoTaskData)
+	extractor, err := helper.NewApiExtractor(helper.ApiExtractorArgs{
+		RawDataSubTaskArgs: helper.RawDataSubTaskArgs{
+			Ctx: taskCtx,
+			Params: ZentaoApiParams{
+				ConnectionId: data.Options.ConnectionId,
+				ProductId:    data.Options.ProductId,
+				ExecutionId:  data.Options.ExecutionId,
+				ProjectId:    data.Options.ProjectId,
+			},
+			Table: RAW_TASK_TABLE,
+		},
+		Extract: func(row *helper.RawData) ([]interface{}, errors.Error) {
+			res := &models.ZentaoTaskRes{}
+			err := json.Unmarshal(row.Data, res)
+			if err != nil {
+				return nil, errors.Default.WrapRaw(err)
+			}
+			task := &models.ZentaoTask{
+				ConnectionId:       data.Options.ConnectionId,
+				ExecutionId:        data.Options.ExecutionId,
+				ID:                 res.Id,
+				Project:            res.Project,
+				Parent:             res.Parent,
+				Execution:          res.Execution,
+				Module:             res.Module,
+				Design:             res.Design,
+				Story:              res.Story,
+				StoryVersion:       res.StoryVersion,
+				DesignVersion:      res.DesignVersion,
+				FromBug:            res.FromBug,
+				Feedback:           res.Feedback,
+				FromIssue:          res.FromIssue,
+				Name:               res.Name,
+				Type:               res.Type,
+				Mode:               res.Mode,
+				Pri:                res.Pri,
+				Estimate:           res.Estimate,
+				Consumed:           res.Consumed,
+				Deadline:           res.Deadline,
+				Status:             res.Status,
+				SubStatus:          res.SubStatus,
+				Color:              res.Color,
+				Description:        res.Description,
+				Version:            res.Version,
+				OpenedById:         res.OpenedBy.Id,
+				OpenedByName:       res.OpenedBy.Realname,
+				OpenedDate:         res.OpenedDate,
+				AssignedToId:       res.AssignedTo.Id,
+				AssignedToName:     res.AssignedTo.Realname,
+				AssignedDate:       res.AssignedDate,
+				EstStarted:         res.EstStarted,
+				RealStarted:        res.RealStarted,
+				FinishedId:         res.FinishedBy.Id,
+				FinishedDate:       res.FinishedDate,
+				FinishedList:       res.FinishedList,
+				CanceledId:         res.CanceledBy.Id,
+				CanceledDate:       res.CanceledDate,
+				ClosedById:         res.ClosedBy.Id,
+				ClosedDate:         res.ClosedDate,
+				PlanDuration:       res.PlanDuration,
+				RealDuration:       res.RealDuration,
+				ClosedReason:       res.ClosedReason,
+				LastEditedId:       res.LastEditedBy.Id,
+				LastEditedDate:     res.LastEditedDate,
+				ActivatedDate:      res.ActivatedDate,
+				OrderIn:            res.OrderIn,
+				Repo:               res.Repo,
+				Mr:                 res.Mr,
+				Entry:              res.Entry,
+				Lines:              res.Lines,
+				V1:                 res.V1,
+				V2:                 res.V2,
+				Deleted:            res.Deleted,
+				Vision:             res.Vision,
+				StoryID:            res.Story,
+				StoryTitle:         res.StoryTitle,
+				Branch:             0,
+				LatestStoryVersion: 0,
+				//Product:            res.Product.Id,
+				//Branch:             res.Branch,
+				//LatestStoryVersion: res.LatestStoryVersion,
+				//StoryStatus:        res.StoryStatus,
+				AssignedToRealName: res.AssignedToRealName,
+				PriOrder:           res.PriOrder,
+				NeedConfirm:        res.NeedConfirm,
+				//ProductType:        res.ProductType,
+				Progress: res.Progress,
+			}
+			results := make([]interface{}, 0)
+			results = append(results, task)
+			return results, nil
+		},
+	})
+
+	if err != nil {
+		return err
+	}
+
+	return extractor.Execute()
+}
diff --git a/plugins/zentao/zentao.go b/plugins/zentao/zentao.go
new file mode 100644
index 000000000..11d73050a
--- /dev/null
+++ b/plugins/zentao/zentao.go
@@ -0,0 +1,47 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License.  You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package main
+
+import (
+	"github.com/apache/incubator-devlake/plugins/zentao/impl"
+	"github.com/apache/incubator-devlake/runner"
+	"github.com/spf13/cobra"
+)
+
+// PluginEntry Export a variable named PluginEntry for Framework to search and load
+var PluginEntry impl.Zentao //nolint
+
+// standalone mode for debugging
+func main() {
+	cmd := &cobra.Command{Use: "zentao"}
+
+	connectionId := cmd.Flags().Uint64P("connectionId", "c", 0, "zentao connection id")
+	executionId := cmd.Flags().IntP("executionId", "e", 8, "execution id")
+	productId := cmd.Flags().IntP("productId", "o", 8, "product id")
+	projectId := cmd.Flags().IntP("projectId", "p", 8, "project id")
+
+	cmd.Run = func(cmd *cobra.Command, args []string) {
+		runner.DirectRun(cmd, args, PluginEntry, map[string]interface{}{
+			"connectionId": *connectionId,
+			"executionId":  *executionId,
+			"productId":    *productId,
+			"projectId":    *projectId,
+		})
+	}
+	runner.RunCmd(cmd)
+}