Repository: dtm-labs/dtm Branch: main Commit: 18146ee53baf Files: 239 Total size: 1.0 MB Directory structure: gitextract_qlsxhq70/ ├── .github/ │ └── workflows/ │ ├── codeql-analysis.yml │ ├── docker.yml │ ├── release.yml │ └── tests.yml ├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── admin/ │ ├── .eslintrc.js │ ├── .gitignore │ ├── README.md │ ├── index.html │ ├── package.json │ ├── postcss.config.js │ ├── src/ │ │ ├── App.vue │ │ ├── api/ │ │ │ └── api_dtm.ts │ │ ├── assets/ │ │ │ └── css/ │ │ │ └── index.css │ │ ├── components/ │ │ │ ├── Screenfull/ │ │ │ │ └── index.vue │ │ │ └── SvgIcon/ │ │ │ └── index.vue │ │ ├── components.d.ts │ │ ├── icons/ │ │ │ └── readme.md │ │ ├── layout/ │ │ │ ├── aside.vue │ │ │ ├── components/ │ │ │ │ ├── content.vue │ │ │ │ ├── header.vue │ │ │ │ └── sidebar.vue │ │ │ └── index.vue │ │ ├── main.ts │ │ ├── permission.ts │ │ ├── router/ │ │ │ ├── asyncRouter.ts │ │ │ └── index.ts │ │ ├── store/ │ │ │ ├── index.ts │ │ │ └── modules/ │ │ │ └── layout.ts │ │ ├── type/ │ │ │ ├── index.d.ts │ │ │ ├── shim.vue.d.ts │ │ │ └── store/ │ │ │ └── layout.ts │ │ ├── utils/ │ │ │ ├── request.ts │ │ │ └── util.ts │ │ └── views/ │ │ └── Dashboard/ │ │ ├── GlobalTransactions/ │ │ │ ├── AllTransactions.vue │ │ │ ├── DialogTransactionDetail.vue │ │ │ └── UnfinishedTransactions.vue │ │ ├── KVPairs/ │ │ │ ├── Topics.vue │ │ │ └── _Components/ │ │ │ ├── DialogTopicDetail.vue │ │ │ └── DialogTopicSubscribe.vue │ │ └── Nodes/ │ │ └── LivingNodes.vue │ ├── tailwind.config.js │ ├── tsconfig.json │ └── vite.config.ts ├── charts/ │ ├── .helmignore │ ├── Chart.yaml │ ├── README.md │ ├── templates/ │ │ ├── NOTES.txt │ │ ├── _helpers.tpl │ │ ├── configmap.yaml │ │ ├── deployment.yaml │ │ ├── hpa.yaml │ │ ├── ingress.yaml │ │ ├── service.yaml │ │ └── tests/ │ │ └── test-connection.yaml │ └── values.yaml ├── client/ │ ├── README.md │ ├── dtmcli/ │ │ ├── barrier.go │ │ ├── barrier_mongo.go │ │ ├── barrier_redis.go │ │ ├── consts.go │ │ ├── cover_test.go │ │ ├── dtmimp/ │ │ │ ├── README-cn.md │ │ │ ├── README.md │ │ │ ├── consts.go │ │ │ ├── db_special.go │ │ │ ├── db_special_test.go │ │ │ ├── trans_base.go │ │ │ ├── trans_xa_base.go │ │ │ ├── types.go │ │ │ ├── types_test.go │ │ │ ├── utils.go │ │ │ ├── utils_test.go │ │ │ └── vars.go │ │ ├── logger/ │ │ │ └── logger.go │ │ ├── trans_msg.go │ │ ├── trans_saga.go │ │ ├── trans_tcc.go │ │ ├── types.go │ │ ├── types_test.go │ │ ├── utils.go │ │ └── xa.go │ ├── dtmgrpc/ │ │ ├── barrier.go │ │ ├── dtmgimp/ │ │ │ ├── README-cn.md │ │ │ ├── README.md │ │ │ ├── grpc_clients.go │ │ │ ├── types.go │ │ │ └── utils.go │ │ ├── dtmgpb/ │ │ │ ├── dtmgimp.pb.go │ │ │ ├── dtmgimp.proto │ │ │ └── dtmgimp_grpc.pb.go │ │ ├── msg.go │ │ ├── options.go │ │ ├── options_test.go │ │ ├── saga.go │ │ ├── tcc.go │ │ ├── type.go │ │ ├── type_test.go │ │ └── xa.go │ └── workflow/ │ ├── dummyReadCloser.go │ ├── factory.go │ ├── imp.go │ ├── rpc.go │ ├── server.go │ ├── utils.go │ ├── wfpb/ │ │ ├── wf.pb.go │ │ ├── wf.proto │ │ └── wf_grpc.pb.go │ ├── workflow.go │ └── workflow_test.go ├── conf.sample.yml ├── dtmsvr/ │ ├── api.go │ ├── api_grpc.go │ ├── api_http.go │ ├── api_json_rpc.go │ ├── config/ │ │ ├── config.go │ │ ├── config_test.go │ │ └── config_utils.go │ ├── cron.go │ ├── entry/ │ │ └── main.go │ ├── metrics.go │ ├── microservices/ │ │ └── drivers.go │ ├── storage/ │ │ ├── boltdb/ │ │ │ ├── boltdb.go │ │ │ └── boltdb_test.go │ │ ├── redis/ │ │ │ └── redis.go │ │ ├── registry/ │ │ │ ├── factory.go │ │ │ └── registry.go │ │ ├── sql/ │ │ │ └── sql.go │ │ ├── store.go │ │ └── trans.go │ ├── svr.go │ ├── topics.go │ ├── trans_class.go │ ├── trans_process.go │ ├── trans_status.go │ ├── trans_type_msg.go │ ├── trans_type_saga.go │ ├── trans_type_tcc.go │ ├── trans_type_workflow.go │ ├── trans_type_xa.go │ ├── utils.go │ └── utils_test.go ├── dtmutil/ │ ├── consts.go │ ├── db.go │ ├── utils.go │ └── utils_test.go ├── go.mod ├── go.sum ├── helper/ │ ├── .goreleaser.yml │ ├── Dockerfile-release │ ├── README-cn.md │ ├── README-en.md │ ├── bench/ │ │ ├── Makefile │ │ ├── main.go │ │ ├── prepare.sh │ │ ├── setup-redis6.sh │ │ ├── setup.sh │ │ ├── svr/ │ │ │ └── http.go │ │ ├── test-boltdb.sh │ │ ├── test-flash-sales.sh │ │ ├── test-mysql.sh │ │ └── test-redis.sh │ ├── compose.store.yml │ ├── golint.sh │ ├── sync-client.sh │ └── test-cover.sh ├── main.go ├── qs/ │ └── main.go ├── revive.toml ├── sqls/ │ ├── busi.mongo.js │ ├── busi.mysql.sql │ ├── busi.postgres.sql │ ├── dtmcli.barrier.mongo.js │ ├── dtmcli.barrier.mysql.sql │ ├── dtmcli.barrier.postgres.sql │ ├── dtmsvr.storage.mysql.sql │ ├── dtmsvr.storage.postgres.sql │ ├── dtmsvr.storage.sqlserver.sql │ └── dtmsvr.storage.tdsql.sql └── test/ ├── api_test.go ├── base_test.go ├── busi/ │ ├── barrier.go │ ├── base_grpc.go │ ├── base_http.go │ ├── base_jrpc.go │ ├── base_types.go │ ├── busi.pb.go │ ├── busi.proto │ ├── busi_grpc.pb.go │ ├── data.go │ ├── quick_start.go │ ├── startup.go │ └── utils.go ├── common_test.go ├── dtmsvr_test.go ├── main_test.go ├── msg_barrier_mongo_test.go ├── msg_barrier_redis_test.go ├── msg_barrier_test.go ├── msg_delay_test.go ├── msg_grpc_barrier_redis_test.go ├── msg_grpc_barrier_test.go ├── msg_grpc_test.go ├── msg_jrpc_test.go ├── msg_options_test.go ├── msg_test.go ├── msg_webhook_test.go ├── saga_barrier_mongo_test.go ├── saga_barrier_redis_test.go ├── saga_barrier_test.go ├── saga_compatible_test.go ├── saga_concurrent_test.go ├── saga_grpc_barrier_test.go ├── saga_grpc_test.go ├── saga_options_test.go ├── saga_test.go ├── store_test.go ├── tcc_barrier_test.go ├── tcc_cover_test.go ├── tcc_grpc_cover_test.go ├── tcc_grpc_test.go ├── tcc_jrpc_test.go ├── tcc_old_test.go ├── tcc_test.go ├── topic_test.go ├── types.go ├── workflow_base_test.go ├── workflow_grpc_test.go ├── workflow_http_ret_test.go ├── workflow_http_test.go ├── workflow_interceptor_test.go ├── workflow_ongoing_test.go ├── workflow_xa_test.go ├── xa_cover_test.go ├── xa_grpc_test.go └── xa_test.go ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/workflows/codeql-analysis.yml ================================================ # For most projects, this workflow file will not need changing; you simply need # to commit it to your repository. # # You may wish to alter this file to override the set of languages analyzed, # or to provide custom queries or build logic. # # ******** NOTE ******** # We have attempted to detect the languages in your repository. Please check # the `language` matrix defined below to confirm you have the correct set of # supported CodeQL languages. # name: "CodeQL" on: push: branches: [ main ] pull_request: # The branches below must be a subset of the branches above branches: [ main ] schedule: - cron: '25 19 * * 2' jobs: analyze: name: Analyze runs-on: ubuntu-latest permissions: actions: read contents: read security-events: write strategy: fail-fast: false matrix: language: [ 'go' ] # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] # Learn more about CodeQL language support at https://git.io/codeql-language-support steps: - name: Checkout repository uses: actions/checkout@v2 # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL uses: github/codeql-action/init@v1 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. # By default, queries listed here will override any specified in a config file. # Prefix the list here with "+" to use these queries and those in the config file. # queries: ./path/to/local/query, your-org/your-repo/queries@main # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild uses: github/codeql-action/autobuild@v1 # ℹ️ Command-line programs to run using the OS shell. # 📚 https://git.io/JvXDl # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines # and modify them (or add more) to build your code if your project # uses a compiled language #- run: | # make bootstrap # make release - name: Perform CodeQL Analysis uses: github/codeql-action/analyze@v1 ================================================ FILE: .github/workflows/docker.yml ================================================ name: Build Docker on: push: tags: - 'v*.*.*' jobs: docker: name: Build docker runs-on: ubuntu-latest steps: - name: Get Release Version run: | export RELEASE_VERSION=${GITHUB_REF#refs/*/} echo RELEASE_VERSION: ${RELEASE_VERSION} echo "RELEASE_VERSION=${RELEASE_VERSION}" >> $GITHUB_ENV - name: Check out code uses: actions/checkout@v3 - name: Create Docker id: meta uses: docker/metadata-action@v3 with: images: | yedf/dtm tags: | type=semver,pattern={{version}} type=semver,pattern={{major}}.{{minor}} type=semver,pattern={{major}} type=sha - name: Set up QEMU uses: docker/setup-qemu-action@v1 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v1 - name: Login to DockerHub uses: docker/login-action@v1 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Build and push uses: docker/build-push-action@v2 with: context: . file: ./helper/Dockerfile-release push: true platforms: linux/amd64,linux/arm64 tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} build-args: | RELEASE_VERSION=${{ env.RELEASE_VERSION }} ================================================ FILE: .github/workflows/release.yml ================================================ name: Release on: push: tags: - 'v*.*.*' jobs: release: name: Release on GitHub runs-on: ubuntu-latest steps: - name: Get Release Version run: | export RELEASE_VERSION=${GITHUB_REF#refs/*/} echo RELEASE_VERSION: ${RELEASE_VERSION} echo "RELEASE_VERSION=${RELEASE_VERSION}" >> $GITHUB_ENV - name: Check out code uses: actions/checkout@v3 - name: Validates GO releaser config uses: docker://goreleaser/goreleaser:v1.7.0 with: args: check - name: Create release on GitHub uses: docker://goreleaser/goreleaser:v1.7.0 with: args: release -f helper/.goreleaser.yml --rm-dist env: GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} - name: Setup node uses: actions/setup-node@v3 with: node-version: 14 - name: Build admin run: | cd admin npm install -g yarn yarn VITE_ADMIN_VERSION=${{ env.RELEASE_VERSION }} yarn build cd .. - name: Scp admin env: host: 'ubuntu@en.dtm.pub' host2: 'ubuntu@dtm.pub' dest: '/data/dtm-admin/' run: | cd admin echo "${{secrets.DEPLOY_KEY}}" > deploy_key chmod 600 ./deploy_key tar -cvzf dist.tar.gz dist scp -i deploy_key -o StrictHostKeyChecking=no dist.tar.gz ${{env.host}}:${{env.dest}} ssh -i deploy_key -o StrictHostKeyChecking=no ${{env.host}} 'cd ${{env.dest}} && tar -zvxf dist.tar.gz' scp -i deploy_key -o StrictHostKeyChecking=no dist.tar.gz ${{env.host2}}:${{env.dest}} ssh -i deploy_key -o StrictHostKeyChecking=no ${{env.host2}} 'cd ${{env.dest}} && tar -zvxf dist.tar.gz' rm deploy_key dist.tar.gz echo > dist/placeholder cd .. ================================================ FILE: .github/workflows/tests.yml ================================================ name: Tests on: push: branches-ignore: - 'tmp-*' pull_request: branches-ignore: - 'tmp-*' jobs: tests: name: CI runs-on: ubuntu-latest services: mysql: image: 'mysql:8' env: MYSQL_ALLOW_EMPTY_PASSWORD: 1 volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro ports: - 3306:3306 redis: image: 'redis' volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro ports: - 6379:6379 postgres: image: 'yedf/postgres-xa' volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro env: POSTGRES_PASSWORD: mysecretpassword POSTGRES_DB: dtm ports: - '5432:5432' mongo: image: 'yedf/mongo-rs' volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro ports: - 27017:27017 sqlserver: image: mcr.microsoft.com/mssql/server:2019-latest volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro env: ACCEPT_EULA: Y MSSQL_SA_PASSWORD: p@ssw0rd ports: - '1433:1433' steps: - name: Set up Go 1.23 uses: actions/setup-go@v2 with: go-version: '1.23.3' - name: Check out code uses: actions/checkout@v2 - name: Install dependencies run: | go mod download - name: Run CI lint run: sh helper/golint.sh - name: Run test cover run: sh helper/test-cover.sh - name: Upload coverage to Codecov run: bash <(curl -s https://codecov.io/bash) env: CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} ================================================ FILE: .gitignore ================================================ conf.yml *.out *.log # dist .idea/** .vscode default.etcd */**/*.bolt bench/bench helper/bench/bench helper/qs/qs # Output file of unit test coverage coverage.* profile.* test.sh dtm dtm-* dtm.* cache ================================================ FILE: LICENSE ================================================ BSD 3-Clause License Copyright (c) 2021, yedf All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ================================================ FILE: Makefile ================================================ # dev env https://www.dtm.pub/other/develop.html all: fmt lint test_redis .PHONY: all fmt: @gofmt -s -w ./ lint: revive -config revive.toml ./... .PHONY: test test: @go test ./... test_redis: TEST_STORE=redis go test ./... test_all: TEST_STORE=redis go test ./... TEST_STORE=boltdb go test ./... TEST_STORE=mysql go test ./... TEST_STORE=postgres go test ./... cover_test: ./helper/test-cover.sh ================================================ FILE: README.md ================================================ ![license](https://img.shields.io/github/license/dtm-labs/dtm) ![Build Status](https://github.com/dtm-labs/dtm/actions/workflows/tests.yml/badge.svg?branch=main) [![codecov](https://codecov.io/gh/dtm-labs/dtm/branch/main/graph/badge.svg?token=UKKEYQLP3F)](https://codecov.io/gh/dtm-labs/dtm) [![Go Report Card](https://goreportcard.com/badge/github.com/dtm-labs/dtm)](https://goreportcard.com/report/github.com/dtm-labs/dtm) [![Go Reference](https://pkg.go.dev/badge/github.com/dtm-labs/dtm.svg)](https://pkg.go.dev/github.com/dtm-labs/dtm) [![Mentioned in Awesome Go](https://awesome.re/mentioned-badge-flat.svg)](https://github.com/avelino/awesome-go#database) English | [简体中文](https://github.com/dtm-labs/dtm/blob/main/helper/README-cn.md) # Distributed Transactions Manager ## What is DTM DTM is a distributed transaction framework which provides cross-service eventual data consistency. It provides saga, tcc, xa, 2-phase message, outbox, workflow patterns for a variety of application scenarios. It also supports multiple languages and multiple store engine to form up a transaction as following: function-picture ## Who's using DTM (partial) [Tencent](https://en.dtm.pub/other/using.html#tencent) [Bytedance](https://en.dtm.pub/other/using.html#bytedance) [Ivydad](https://en.dtm.pub/other/using.html#ivydad) [More](https://en.dtm.pub/other/using.html) ## Features * Support for multiple transaction modes: SAGA, TCC, XA, Workflow, Outbox * Multiple languages support: SDK for Go, Java, PHP, C#, Python, Nodejs * Better Outbox: 2-phase messages, a more elegant solution than Outbox, support multi-databases * Multiple database transaction support: MySQL/MariaDB, Redis, MongoDB, Postgres, TDSQL, etc. * Support for multiple storage engines: MySQL/MariaDB (common), Redis (high performance), BoltDB (dev&test), MongoDB (under planning) * Support for multiple microservices architectures: [go-zero](https://github.com/zeromicro/go-zero), go-kratos/kratos, polarismesh/polaris * Support for high availability and easy horizontal scaling ## Application scenarios. DTM can be applied to data consistency issues in a large number of scenarios, here are a few common ones * [cache management](https://en.dtm.pub/app/cache.html): thoroughly guarantee the cache final consistency and strong consistency * [flash-sales to deduct inventory](https://en.dtm.pub/app/flash.html): in extreme cases, it is also possible to ensure that the precise inventory in Redis is exactly the same as the final order created, without the need for manual adjustment * [Non-monolithic order system](https://en.dtm.pub/app/order.html): Dramatically simplifies the architecture * [Event publishing/subscription](https://en.dtm.pub/practice/msg.html): better outbox pattern ## [Cook Book](https://en.dtm.pub) ## Quick start ### run dtm ``` bash git clone https://github.com/dtm-labs/dtm && cd dtm go run main.go ``` ### Start an example Suppose we want to perform an inter-bank transfer. The operations of transfer out (TransOut) and transfer in (TransIn) are coded in separate micro-services. Here is an example to illustrate a solution of dtm to this problem: ``` bash git clone https://github.com/dtm-labs/quick-start-sample.git && cd quick-start-sample/workflow-grpc go run main.go ``` ## Code ### Usage ``` go wfName := "workflow-grpc" err = workflow.Register(wfName, func(wf *workflow.Workflow, data []byte) error { // ... // Define a transaction branch for TransOut wf.NewBranch().OnRollback(func(bb *dtmcli.BranchBarrier) error { // compensation for TransOut _, err := busiCli.TransOutRevert(wf.Context, &req) return err }) _, err = busiCli.TransOut(wf.Context, &req) // check error // Define another transaction branch for TransIn wf.NewBranch().OnRollback(func(bb *dtmcli.BranchBarrier) error { _, err := busiCli.TransInRevert(wf.Context, &req) return err }) _, err = busiCli.TransIn(wf.Context, &req) return err } // ... req := busi.BusiReq{Amount: 30, TransInResult: ""} data, err := proto.Marshal(&req) // Execute workflow _, err = workflow.ExecuteCtx(wfName, shortuuid.New(), data) logger.Infof("result of workflow.Execute is: %v", err) ``` When the above code runs, we can see in the console that services `TransOut`, `TransIn` has been called. #### Rollback upon failure If any forward operation fails, DTM invokes the corresponding compensating operation of each sub-transaction to roll back, after which the transaction is successfully rolled back. Let's purposely trigger the failure of the second sub-transaction and watch what happens ``` go // req := busi.BusiReq{Amount: 30, TransInResult: ""} req := busi.BusiReq{Amount: 30, TransInResult: "FAILURE"} }) ``` we can see in the console that services `TransOut`, `TransIn`, `TransOutRevert` has been called ## More examples If you want more quick start examples, please refer to [dtm-labs/quick-start-sample](https://github.com/dtm-labs/quick-start-sample) The above example mainly demonstrates the flow of a distributed transaction. More on this, including practical examples of how to interact with an actual database, how to do compensation, how to do rollback, etc. please refer to [dtm-examples](https://github.com/dtm-labs/dtm-examples) for more examples. ## Give a star! ⭐ If you think this project is interesting, or helpful to you, please give a star! ================================================ FILE: admin/.eslintrc.js ================================================ module.exports = { parser: 'vue-eslint-parser', env: { browser: true, es2021: true }, extends: [ 'plugin:@typescript-eslint/recommended', 'plugin:vue/vue3-recommended' ], parserOptions: { parser: '@typescript-eslint/parser', sourceType: 'module', ecmaFeature: { jsx: true, tsx: true } }, plugins: [ '@typescript-eslint' ], rules: { 'vue/max-attributes-per-line': ['error', { singleline: 10, multiline: { max: 1 } }], 'vue/multi-word-component-names': 0, 'vue/singleline-html-element-content-newline': 'off', 'vue/multiline-html-element-content-newline': 'off', 'vue/html-indent': ['error', 4], indent: ['error', 4], // 4行缩进 'vue/script-indent': ['error', 4], quotes: ['error', 'single'], // 单引号 // 'vue/html-quotes': ['error', 'single'], semi: ['error', 'never'], // 禁止使用分号 'space-infix-ops': ['error', { int32Hint: false }], // 要求操作符周围有空格 'no-multi-spaces': 'error', // 禁止多个空格 'no-whitespace-before-property': 'error', // 禁止在属性前使用空格 'space-before-blocks': 'error', // 在块之前强制保持一致的间距 'space-before-function-paren': ['error', 'never'], // 在“ function”定义打开括号之前强制不加空格 'space-in-parens': ['error', 'never'], // 强制括号左右的不加空格 'spaced-comment': ['error', 'always'], // 注释间隔 'template-tag-spacing': ['error', 'always'], // 在模板标签及其文字之间需要空格 'no-var': 'error', 'prefer-destructuring': ['error', { // 优先使用数组和对象解构 array: true, object: true }, { enforceForRenamedProperties: false }], 'comma-dangle': ['error', 'never'], // 最后一个属性不允许有逗号 'arrow-spacing': 'error', // 箭头函数空格 'prefer-template': 'error', 'template-curly-spacing': 'error', 'quote-props': ['error', 'as-needed'], // 对象字面量属性名称使用引号 'object-curly-spacing': ['error', 'always'], // 强制在花括号中使用一致的空格 'no-unneeded-ternary': 'error', // 禁止可以表达为更简单结构的三元操作符 'no-restricted-syntax': ['error', 'WithStatement', 'BinaryExpression[operator="in"]'], // 禁止with/in语句 'no-lonely-if': 'error', // 禁止 if 语句作为唯一语句出现在 else 语句块中 'newline-per-chained-call': ['error', { ignoreChainWithDepth: 2 }], // 要求方法链中每个调用都有一个换行符 // 路径别名设置 'no-submodule-imports': ['off', '/@'], 'no-implicit-dependencies': ['off', ['/@']], '@typescript-eslint/no-explicit-any': 'off' // 类型可以使用any } } ================================================ FILE: admin/.gitignore ================================================ # Logs logs *.log npm-debug.log* yarn-debug.log* yarn-error.log* pnpm-debug.log* lerna-debug.log* node_modules dist dist-ssr *.local # Editor directories and files .vscode/* !.vscode/extensions.json .idea .DS_Store *.suo *.ntvs* *.njsproj *.sln *.sw? ================================================ FILE: admin/README.md ================================================ # DTM-Admin ================================================ FILE: admin/index.html ================================================ Dtm
================================================ FILE: admin/package.json ================================================ { "name": "dtm-admin", "private": true, "version": "0.0.0", "scripts": { "dev": "vite", "build": "vite build && echo > dist/placeholder", "preview": "vite preview", "lint": "eslint --ext .tsx,.ts,vue src/", "lint:fix": "eslint --ext .tsx,.ts,vue src/ --fix" }, "dependencies": { "ant-design-vue": "^3.1.1", "vue": "^3.2.25", "vue-demi": "^0.13.11", "vue-request": "^1.2.4" }, "devDependencies": { "@types/node": "^17.0.23", "@types/nprogress": "^0.2.0", "@typescript-eslint/eslint-plugin": "^5.18.0", "@typescript-eslint/parser": "^5.18.0", "@vitejs/plugin-vue": "^4.0.0", "autoprefixer": "^10.4.4", "axios": "^1.7.4", "eslint": "^8.33.0", "eslint-plugin-vue": "^8.6.0", "fast-glob": "^3.2.11", "nprogress": "^0.2.0", "pinia": "^2.0.0-rc.10", "postcss": "^8.4.31", "postcss-import": "^14.1.0", "postcss-nested": "^5.0.6", "postcss-simple-vars": "^6.0.3", "prettier": "^2.8.3", "prettier-eslint": "^15.0.1", "screenfull": "^6.0.1", "tailwindcss": "^3.0.24", "typescript": "^4.5.4", "unplugin-vue-components": "^0.25.1", "vite": "^3.2.11", "vite-plugin-ejs": "^1.6.4", "vite-plugin-svg-icons": "^2.0.1", "vue-router": "^4.0.13", "vue-tsc": "^0.29.8" } } ================================================ FILE: admin/postcss.config.js ================================================ module.exports = { plugins: { tailwindcss: {}, autoprefixer: {} } } ================================================ FILE: admin/src/App.vue ================================================ ================================================ FILE: admin/src/api/api_dtm.ts ================================================ import { AxiosResponse } from 'axios' import request from '/@/utils/request' export interface IListAllTransactionsReq { gid?: string; limit: number; position?: string; } export interface IListAllKVReq { cat: string; limit: number; position?: string; } export function listAllTransactions( payload: IListAllTransactionsReq ): Promise> { return request({ url: '/api/dtmsvr/all', method: 'get', params: payload }) } export function forceStopTransaction(gid: string): Promise { return request({ url: '/api/dtmsvr/forceStop', method: 'post', data: { gid } }) } export function queryKVPair(payload: { cat: string; key: string; }): Promise> { return request({ url: '/api/dtmsvr/queryKV', method: 'get', params: payload }) } export function listKVPairs( payload: IListAllKVReq ): Promise> { return request({ url: '/api/dtmsvr/scanKV', method: 'get', params: payload }) } export function deleteTopic(topicName: string): Promise> { return request({ url: `/api/dtmsvr/topic/${topicName}`, method: 'delete' }) } export function subscribe(payload: { topic: string; url: string; remark: string; }): Promise> { return request({ url: '/api/dtmsvr/subscribe', method: 'get', params: payload }) } export function unsubscribe(payload: { topic: string; url: string; }): Promise { return request({ url: '/api/dtmsvr/unsubscribe', method: 'get', params: payload }) } export function getTransaction(payload: { gid: string; }): Promise> { return request({ url: '/api/dtmsvr/query', method: 'get', params: payload }) } export function resetNextCronTime(gid: string): Promise { return request({ url: '/api/dtmsvr/resetNextCronTime', method: 'post', data: { gid } }) } export function getDtmVersion(): Promise> { return request({ url: '/api/dtmsvr/version', method: 'get' }) } ================================================ FILE: admin/src/assets/css/index.css ================================================ /* @tailwind base; */ @tailwind components; @tailwind utilities; ================================================ FILE: admin/src/components/Screenfull/index.vue ================================================ ================================================ FILE: admin/src/components/SvgIcon/index.vue ================================================ ================================================ FILE: admin/src/components.d.ts ================================================ /* eslint-disable */ /* prettier-ignore */ // @ts-nocheck // Generated by unplugin-vue-components // Read more: https://github.com/vuejs/core/pull/3399 export {} declare module 'vue' { export interface GlobalComponents { AAlert: typeof import('ant-design-vue/es')['Alert'] ABreadcrumb: typeof import('ant-design-vue/es')['Breadcrumb'] ABreadcrumbItem: typeof import('ant-design-vue/es')['BreadcrumbItem'] AButton: typeof import('ant-design-vue/es')['Button'] ADescriptions: typeof import('ant-design-vue/es')['Descriptions'] ADescriptionsItem: typeof import('ant-design-vue/es')['DescriptionsItem'] ADivider: typeof import('ant-design-vue/es')['Divider'] AForm: typeof import('ant-design-vue/es')['Form'] AFormItem: typeof import('ant-design-vue/es')['FormItem'] AInput: typeof import('ant-design-vue/es')['Input'] ALayout: typeof import('ant-design-vue/es')['Layout'] ALayoutContent: typeof import('ant-design-vue/es')['LayoutContent'] ALayoutHeader: typeof import('ant-design-vue/es')['LayoutHeader'] ALayoutSider: typeof import('ant-design-vue/es')['LayoutSider'] AMenu: typeof import('ant-design-vue/es')['Menu'] AMenuItem: typeof import('ant-design-vue/es')['MenuItem'] AModal: typeof import('ant-design-vue/es')['Modal'] APopconfirm: typeof import('ant-design-vue/es')['Popconfirm'] ARangePicker: typeof import('ant-design-vue/es')['RangePicker'] ASelect: typeof import('ant-design-vue/es')['Select'] ASelectOption: typeof import('ant-design-vue/es')['SelectOption'] ASubMenu: typeof import('ant-design-vue/es')['SubMenu'] ATable: typeof import('ant-design-vue/es')['Table'] ATag: typeof import('ant-design-vue/es')['Tag'] ATextarea: typeof import('ant-design-vue/es')['Textarea'] RouterLink: typeof import('vue-router')['RouterLink'] RouterView: typeof import('vue-router')['RouterView'] Screenfull: typeof import('./components/Screenfull/index.vue')['default'] SvgIcon: typeof import('./components/SvgIcon/index.vue')['default'] } } ================================================ FILE: admin/src/icons/readme.md ================================================ # ICon Component ================================================ FILE: admin/src/layout/aside.vue ================================================ ================================================ FILE: admin/src/layout/components/content.vue ================================================ ================================================ FILE: admin/src/layout/components/header.vue ================================================ ================================================ FILE: admin/src/layout/components/sidebar.vue ================================================ ================================================ FILE: admin/src/layout/index.vue ================================================ ================================================ FILE: admin/src/main.ts ================================================ import { createApp } from 'vue' import App from './App.vue' import router from '/@/router/index' import { pinia } from '/@/store' import { useLayoutStore } from '/@/store/modules/layout' import '/@/permission' import 'ant-design-vue/dist/antd.css' import '/@/assets/css/index.css' import 'virtual:svg-icons-register' const app = createApp(App) app.use(router) app.use(pinia) app.mount('#app') window.onunhandledrejection = (ev: PromiseRejectionEvent) => { showAlert(ev.reason.stack || ev.reason.message) } window.onerror = err => { if (typeof err === 'string') { return showAlert(err) } showAlert(JSON.stringify(err)) } function showAlert(msg: string) { const layout = useLayoutStore() if (!layout.globalError) { layout.setGlobalError(msg) } } ================================================ FILE: admin/src/permission.ts ================================================ import router from '/@/router' import { configure, start, done } from 'nprogress' import { useLayoutStore } from './store/modules/layout' configure({ showSpinner: false }) // eslint-disable-next-line @typescript-eslint/no-unused-vars const defaultRoutePath = '/' router.beforeEach((to) => { start() const { getMenubar, concatAllowRoutes } = useLayoutStore() if (getMenubar.menuList.length === 0) { concatAllowRoutes() return to.fullPath } }) router.afterEach(() => { done() }) ================================================ FILE: admin/src/router/asyncRouter.ts ================================================ const modules = import.meta.glob('../views/**/**.vue') const components: IObject<() => Promise> = { LayoutHeader: (() => import('/@/layout/index.vue')) as unknown as () => Promise } Object.keys(modules).forEach(key => { const nameMatch = key.match(/^\.\.\/views\/(.+)\.vue/) if (!nameMatch) return if (nameMatch[1].includes('_Components')) return const indexMatch = nameMatch[1].match(/(.*)\/Index$/i) let name = indexMatch ? indexMatch[1] : nameMatch[1]; [name] = name.split('/').splice(-1) components[name] = modules[key] as () => Promise }) export { components } ================================================ FILE: admin/src/router/index.ts ================================================ import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router' import { IMenubarList } from '../type/store/layout' import { components } from './asyncRouter' const Components: IObject<() => Promise> = Object.assign({}, components, { LayoutHeader: (() => import('/@/layout/index.vue')) as unknown as () => Promise< typeof import('*.vue') >, LayoutMain: (() => import('/@/layout/aside.vue')) as unknown as () => Promise< typeof import('*.vue') > }) export const allowRouter: Array = [ { name: 'Admin', path: '/', redirect: '/admin/global-transactions/all', component: Components['LayoutHeader'], meta: { title: 'Admin', activeMenu: '/admin' }, children: [ { // name: 'Nodes', // path: '/admin/nodes', // component: Components['LayoutMain'], // meta: { title: 'Nodes' }, // children: [ // { // name: 'LivingNodes', // path: '/admin/nodes/living', // component: Components['LivingNodes'], // meta: { title: 'Living Nodes' }, // } // ] // }, { name: 'GlobalTransactions', path: '/admin/global-transactions', component: Components['LayoutMain'], meta: { title: 'Global Transactions' }, children: [ { name: 'AllTransactions', path: '/admin/global-transactions/all', component: Components['AllTransactions'], meta: { title: 'All Transactions' } // }, { // name: 'UnfinishedTransactions', // path: '/admin/global-transactions/unfinished', // component: Components['UnfinishedTransactions'], // meta: { title: 'Unfinished Transactions' }, }, { name: 'TransactionDetail', path: '/admin/global-transactions/detail/:gid', component: Components['DialogTransactionDetail'], meta: { title: 'Transaction Detail' } }, ] }, { name: 'KVPairs', path: '/admin/kv', component: Components['LayoutMain'], meta: { title: 'Key-Value Pairs' }, children: [ { name: 'Topics', path: '/admin/kv/topics', component: Components['Topics'], meta: { title: 'Topics' } } ] } ] } ] const router = createRouter({ history: createWebHistory(window.basePath || undefined), routes: allowRouter as RouteRecordRaw[] }) export default router ================================================ FILE: admin/src/store/index.ts ================================================ import { createPinia } from 'pinia' export const pinia = createPinia() ================================================ FILE: admin/src/store/modules/layout.ts ================================================ import { defineStore } from 'pinia' import { allowRouter } from '/@/router' import { ILayout, IMenubar, IMenubarList, IStatus } from '/@/type/store/layout' import { getDtmVersion } from '/@/api/api_dtm' export const useLayoutStore = defineStore({ id: 'layout', state: (): ILayout => ({ menubar: { menuList: [] }, status: { isLoading: false }, dtmVersion: '', globalError: '' }), getters: { getMenubar(): IMenubar { return this.menubar }, getStatus(): IStatus { return this.status } }, actions: { setRoutes(data: Array): void { this.menubar.menuList = data }, setGlobalError(err: string) { this.globalError = err }, concatAllowRoutes(): void { allowRouter.reverse().forEach(v => this.menubar.menuList.unshift(v)) }, async loadDtmVersion(): Promise { const { data: { version } } = await getDtmVersion() this.dtmVersion = version console.log('dtm version: ', this.dtmVersion) } } }) ================================================ FILE: admin/src/type/index.d.ts ================================================ export { } declare global { interface IObject { [index: string]: T } interface ImportMetaEnv { VITE_APP_TITLE: string VITE_PORT: number VITE_PROXY: string VITE_ADMIN_VERSION: string } interface ITable { data: Array next_position: number, size: number } interface Window { basePath: string; } } ================================================ FILE: admin/src/type/shim.vue.d.ts ================================================ declare module '*.vue' { import { defineComponent } from 'vue' const Component: ReturnType export default Component } ================================================ FILE: admin/src/type/store/layout.ts ================================================ export interface IMenubar { menuList: Array } export interface ILayout { menubar: IMenubar status: IStatus dtmVersion: string globalError: string } export interface IStatus { isLoading: boolean } export interface IMenubarList { parentId?: number | string id?: number | string name: string path: string redirect?: string meta: { icon?: string title: string permission?: string[] activeMenu?: string hidden?: boolean alwaysShow?: boolean } component: (() => Promise) | string children?: Array } ================================================ FILE: admin/src/utils/request.ts ================================================ import axios from 'axios' const request = axios.create({ timeout: 60000 }) export default request ================================================ FILE: admin/src/utils/util.ts ================================================ import { useRoute } from 'vue-router' import { IMenubarList } from '../type/store/layout' export const findCurrentMenubar = (menuList: IMenubarList[], root?: boolean) => { const route = useRoute() let arr: IMenubarList[] | IMenubarList = [] for (let i = 0; i < menuList.length; i++) { const v = menuList[i] const usePath = v.meta.activeMenu || v.redirect || v.path const pos = usePath.lastIndexOf('/') const rootPath = pos == 0 ? usePath : usePath.substring(0, pos) if (route.path.indexOf(rootPath) !== -1) { if (!root) { arr = v.children as IMenubarList[] } else { arr = v } break } } return arr } export const sleep = async(ms: number) => { return new Promise(resolve => setTimeout(resolve, ms)) } ================================================ FILE: admin/src/views/Dashboard/GlobalTransactions/AllTransactions.vue ================================================ ================================================ FILE: admin/src/views/Dashboard/GlobalTransactions/DialogTransactionDetail.vue ================================================