diff --git a/generate/database/20230626063528_create_wallet_table.down.sql b/generate/database/20230626063528_create_wallet_table.down.sql new file mode 100755 index 0000000..c6061d5 --- /dev/null +++ b/generate/database/20230626063528_create_wallet_table.down.sql @@ -0,0 +1 @@ +DROP TABLE IF EXISTS `wallet`; diff --git a/generate/database/20230626063528_create_wallet_table.up.sql b/generate/database/20230626063528_create_wallet_table.up.sql new file mode 100755 index 0000000..86e6edd --- /dev/null +++ b/generate/database/20230626063528_create_wallet_table.up.sql @@ -0,0 +1,14 @@ +CREATE TABLE `wallet` ( + `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主鍵 ID(錢包唯一識別)', + `brand` VARCHAR(50) NOT NULL COMMENT '品牌/平台(多租戶識別)', + `uid` VARCHAR(64) NOT NULL COMMENT '使用者 UID', + `asset` VARCHAR(32) NOT NULL COMMENT '資產代碼(如 BTC、ETH、GEM_RED 等)', + `balance` DECIMAL(30, 18) UNSIGNED NOT NULL DEFAULT 0 COMMENT '資產餘額', + `type` TINYINT NOT NULL COMMENT '錢包類型', + `create_at` INTEGER NOT NULL DEFAULT 0 COMMENT '建立時間(Unix)', + `update_at` INTEGER NOT NULL DEFAULT 0 COMMENT '更新時間(Unix)', + PRIMARY KEY (`id`), + UNIQUE KEY `uq_brand_uid_asset_type` (`brand`, `uid`, `asset`, `type`), + KEY `idx_uid` (`uid`), + KEY `idx_brand` (`brand`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='錢包'; \ No newline at end of file diff --git a/generate/database/202504160353001_transaction.down.sql b/generate/database/202504160353001_transaction.down.sql new file mode 100755 index 0000000..7285aba --- /dev/null +++ b/generate/database/202504160353001_transaction.down.sql @@ -0,0 +1 @@ +DROP TABLE IF EXISTS `transaction`; diff --git a/generate/database/202504160353001_transaction.up.sql b/generate/database/202504160353001_transaction.up.sql new file mode 100755 index 0000000..14e55cc --- /dev/null +++ b/generate/database/202504160353001_transaction.up.sql @@ -0,0 +1,22 @@ +CREATE TABLE `transaction` ( + `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '交易主鍵 ID,自動遞增', + `order_id` VARCHAR(64) DEFAULT NULL COMMENT '關聯的訂單 ID,可為空(若不是由訂單觸發)', + `transaction_id` VARCHAR(64) NOT NULL COMMENT '此筆交易的唯一識別碼(系統內部使用,可為 UUID)', + `brand` VARCHAR(50) NOT NULL COMMENT '所屬品牌(支援多品牌場景)', + `uid` VARCHAR(64) NOT NULL COMMENT '交易發起者的 UID', + `to_uid` VARCHAR(64) DEFAULT NULL COMMENT '交易對象的 UID(如為轉帳場景)', + `type` TINYINT NOT NULL COMMENT '交易類型(如轉帳、入金、出金等,自定義列舉)', + `business_type` TINYINT NOT NULL COMMENT '業務類型(如合約、模擬、一般用途等,數字代碼)', + `crypto` VARCHAR(32) NOT NULL COMMENT '幣種(如 BTC、ETH、USD、TWD 等)', + `amount` DECIMAL(30, 18) NOT NULL COMMENT '本次變動金額(正數為增加,負數為扣減)', + `balance` DECIMAL(30, 18) NOT NULL COMMENT '交易完成後的錢包餘額', + `before_balance` DECIMAL(30, 18) NOT NULL COMMENT '交易前的錢包餘額(方便審計與對帳)', + `status` TINYINT NOT NULL DEFAULT 1 COMMENT '狀態(1: 有效、0: 無效/已取消)', + `create_time` BIGINT NOT NULL DEFAULT 0 COMMENT '建立時間(Unix 秒數)', + `due_time` BIGINT NOT NULL DEFAULT 0 COMMENT '到期時間(適用於凍結或延後入帳等場景)', + PRIMARY KEY (`id`), + UNIQUE KEY `uq_transaction_id` (`transaction_id`), + KEY `idx_uid` (`uid`), + KEY `idx_order_id` (`order_id`), + KEY `idx_create_time` (`create_time`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='交易紀錄表'; \ No newline at end of file diff --git a/generate/database/202504160353002_wallet_transaction.down.sql b/generate/database/202504160353002_wallet_transaction.down.sql new file mode 100755 index 0000000..7285aba --- /dev/null +++ b/generate/database/202504160353002_wallet_transaction.down.sql @@ -0,0 +1 @@ +DROP TABLE IF EXISTS `transaction`; diff --git a/generate/database/202504160353002_wallet_transaction.up.sql b/generate/database/202504160353002_wallet_transaction.up.sql new file mode 100755 index 0000000..29e6fdf --- /dev/null +++ b/generate/database/202504160353002_wallet_transaction.up.sql @@ -0,0 +1,22 @@ +CREATE TABLE `wallet_transaction` ( + `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主鍵 ID,自動遞增', + `transaction_id` BIGINT NOT NULL COMMENT '交易流水號(可對應某次業務操作,例如同一訂單的多筆變化)', + `order_id` VARCHAR(64) NOT NULL COMMENT '訂單編號(對應實際訂單或業務事件)', + `brand` VARCHAR(50) NOT NULL COMMENT '品牌(多租戶或多平台識別)', + `uid` VARCHAR(64) NOT NULL COMMENT '使用者 UID', + `wallet_type` TINYINT NOT NULL COMMENT '錢包類型(如主錢包、獎勵錢包、凍結錢包等)', + `business_type` TINYINT NOT NULL COMMENT '業務類型(如購物、退款、加值等)', + `asset` VARCHAR(32) NOT NULL COMMENT '資產代號(如 BTC、ETH、GEM_RED、USD 等)', + `amount` DECIMAL(30, 18) NOT NULL COMMENT '變動金額(正數為收入,負數為支出)', + `balance` DECIMAL(30, 18) NOT NULL COMMENT '當前錢包餘額(這筆交易後的餘額快照)', + `create_at` BIGINT NOT NULL DEFAULT 0 COMMENT '建立時間(UnixNano,紀錄交易發生時間)', + PRIMARY KEY (`id`), + KET `idx_uid` (`uid`), + KEY `idx_transaction_id` (`transaction_id`), + KEY `idx_order_id` (`order_id`), + KEY `idx_brand` (`brand`), + KEY `idx_wallet_type` (`wallet_type`) +) ENGINE = InnoDB + DEFAULT CHARSET = utf8mb4 + COLLATE = utf8mb4_unicode_ci + COMMENT = '錢包資金異動紀錄(每一次交易行為的快照記錄)'; \ No newline at end of file diff --git a/generate/database/digimon-wallet/20230527074947_create_schema.down.sql b/generate/database/digimon-wallet/20230527074947_create_schema.down.sql new file mode 100755 index 0000000..9b4cb1d --- /dev/null +++ b/generate/database/digimon-wallet/20230527074947_create_schema.down.sql @@ -0,0 +1 @@ +DROP DATABASE IF EXISTS `digimon_wallet`; \ No newline at end of file diff --git a/generate/database/digimon-wallet/20230527074947_create_schema.up.sql b/generate/database/digimon-wallet/20230527074947_create_schema.up.sql new file mode 100755 index 0000000..0fb707a --- /dev/null +++ b/generate/database/digimon-wallet/20230527074947_create_schema.up.sql @@ -0,0 +1 @@ +CREATE DATABASE IF NOT EXISTS `digimon_wallet`; \ No newline at end of file diff --git a/go.mod b/go.mod index fdc58d5..66a4a43 100644 --- a/go.mod +++ b/go.mod @@ -4,32 +4,49 @@ go 1.24.2 require ( github.com/shopspring/decimal v1.4.0 + github.com/testcontainers/testcontainers-go v0.36.0 github.com/zeromicro/go-zero v1.8.2 google.golang.org/grpc v1.71.1 google.golang.org/protobuf v1.36.6 + gorm.io/driver/mysql v1.5.7 gorm.io/gorm v1.25.12 ) require ( + dario.cat/mergo v1.0.1 // indirect + filippo.io/edwards25519 v1.1.0 // indirect + github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect + github.com/Microsoft/go-winio v0.6.2 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/cenkalti/backoff/v4 v4.3.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/containerd/log v0.1.0 // indirect + github.com/containerd/platforms v0.2.1 // indirect github.com/coreos/go-semver v0.3.1 // indirect github.com/coreos/go-systemd/v22 v22.5.0 // indirect + github.com/cpuguy83/dockercfg v0.3.2 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect + github.com/distribution/reference v0.6.0 // indirect + github.com/docker/docker v28.0.1+incompatible // indirect + github.com/docker/go-connections v0.5.0 // indirect + github.com/docker/go-units v0.5.0 // indirect + github.com/ebitengine/purego v0.8.2 // indirect github.com/emicklei/go-restful/v3 v3.11.0 // indirect github.com/fatih/color v1.18.0 // indirect + github.com/felixge/httpsnoop v1.0.4 // indirect github.com/go-logr/logr v1.4.2 // indirect github.com/go-logr/stdr v1.2.2 // indirect + github.com/go-ole/go-ole v1.2.6 // indirect github.com/go-openapi/jsonpointer v0.19.6 // indirect github.com/go-openapi/jsonreference v0.20.2 // indirect github.com/go-openapi/swag v0.22.4 // indirect + github.com/go-sql-driver/mysql v1.9.0 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/mock v1.6.0 // indirect github.com/golang/protobuf v1.5.4 // indirect github.com/google/gnostic-models v0.6.8 // indirect - github.com/google/go-cmp v0.6.0 // indirect + github.com/google/go-cmp v0.7.0 // indirect github.com/google/gofuzz v1.2.0 // indirect github.com/google/uuid v1.6.0 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 // indirect @@ -38,42 +55,64 @@ require ( github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/compress v1.17.11 // indirect + github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect + github.com/magiconair/properties v1.8.9 // indirect github.com/mailru/easyjson v0.7.7 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect + github.com/moby/docker-image-spec v1.3.1 // indirect + github.com/moby/patternmatcher v0.6.0 // indirect + github.com/moby/sys/sequential v0.5.0 // indirect + github.com/moby/sys/user v0.1.0 // indirect + github.com/moby/sys/userns v0.1.0 // indirect + github.com/moby/term v0.5.0 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/morikuni/aec v1.0.0 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/opencontainers/go-digest v1.0.0 // indirect + github.com/opencontainers/image-spec v1.1.1 // indirect github.com/openzipkin/zipkin-go v0.4.3 // indirect github.com/pelletier/go-toml/v2 v2.2.2 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect github.com/prometheus/client_golang v1.21.1 // indirect github.com/prometheus/client_model v0.6.1 // indirect github.com/prometheus/common v0.62.0 // indirect github.com/prometheus/procfs v0.15.1 // indirect github.com/redis/go-redis/v9 v9.7.3 // indirect + github.com/shirou/gopsutil/v4 v4.25.1 // indirect + github.com/sirupsen/logrus v1.9.3 // indirect github.com/spaolacci/murmur3 v1.1.0 // indirect + github.com/stretchr/testify v1.10.0 // indirect + github.com/tklauser/go-sysconf v0.3.12 // indirect + github.com/tklauser/numcpus v0.6.1 // indirect + github.com/yusufpapurcu/wmi v1.2.4 // indirect go.etcd.io/etcd/api/v3 v3.5.15 // indirect go.etcd.io/etcd/client/pkg/v3 v3.5.15 // indirect go.etcd.io/etcd/client/v3 v3.5.15 // indirect go.opentelemetry.io/auto/sdk v1.1.0 // indirect - go.opentelemetry.io/otel v1.34.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 // indirect + go.opentelemetry.io/otel v1.35.0 // indirect go.opentelemetry.io/otel/exporters/jaeger v1.17.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.24.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.24.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.24.0 // indirect go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.24.0 // indirect go.opentelemetry.io/otel/exporters/zipkin v1.24.0 // indirect - go.opentelemetry.io/otel/metric v1.34.0 // indirect + go.opentelemetry.io/otel/metric v1.35.0 // indirect go.opentelemetry.io/otel/sdk v1.34.0 // indirect - go.opentelemetry.io/otel/trace v1.34.0 // indirect + go.opentelemetry.io/otel/trace v1.35.0 // indirect go.opentelemetry.io/proto/otlp v1.3.1 // indirect go.uber.org/atomic v1.10.0 // indirect go.uber.org/automaxprocs v1.6.0 // indirect go.uber.org/multierr v1.9.0 // indirect go.uber.org/zap v1.24.0 // indirect + golang.org/x/crypto v0.33.0 // indirect golang.org/x/net v0.35.0 // indirect golang.org/x/oauth2 v0.25.0 // indirect - golang.org/x/sys v0.30.0 // indirect + golang.org/x/sys v0.31.0 // indirect golang.org/x/term v0.29.0 // indirect golang.org/x/text v0.22.0 // indirect golang.org/x/time v0.10.0 // indirect diff --git a/go.sum b/go.sum index 71c3838..a1b89a4 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,13 @@ +dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s= +dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= +filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= +filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= +github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 h1:bvDV9vkmnHYOMsOr4WLk+Vo07yKIzd94sVoIqshQ4bU= +github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8= +github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8= +github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= +github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= +github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= github.com/alicebob/gopher-json v0.0.0-20230218143504-906a9b012302 h1:uvdUDbHQHO85qeSydJtItA4T55Pw6BtAejd0APRJOCE= github.com/alicebob/gopher-json v0.0.0-20230218143504-906a9b012302/go.mod h1:SGnFV6hVsYE877CKEZ6tDNTjaSXYUk6QqoIK6PrAtcc= github.com/alicebob/miniredis/v2 v2.34.0 h1:mBFWMaJSNL9RwdGRyEDoAAv8OQc5UlEhLDQggTglU/0= @@ -14,26 +24,48 @@ github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK3 github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= +github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= +github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A= +github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw= github.com/coreos/go-semver v0.3.1 h1:yi21YpKnrx1gt5R+la8n5WgS0kCrsPp33dmEyHReZr4= github.com/coreos/go-semver v0.3.1/go.mod h1:irMmmIw/7yzSRPWryHsK7EYSg09caPQL03VsM8rvUec= github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs= github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +github.com/cpuguy83/dockercfg v0.3.2 h1:DlJTyZGBDlXqUZ2Dk2Q3xHs/FtnooJJVaad2S9GKorA= +github.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= +github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= +github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= +github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= +github.com/docker/docker v28.0.1+incompatible h1:FCHjSRdXhNRFjlHMTv4jUNlIBbTeRjrWfeFuJp7jpo0= +github.com/docker/docker v28.0.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c= +github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc= +github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= +github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/ebitengine/purego v0.8.2 h1:jPPGWs2sZ1UgOSgD2bClL0MJIqu58nOmIcBuXr62z1I= +github.com/ebitengine/purego v0.8.2/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g= github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.3.0/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= +github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= github.com/go-openapi/jsonpointer v0.19.6 h1:eCs3fxoIi3Wh6vtgmLTOjdhSpiqphQ+DaPn38N2ZdrE= github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs= github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE= @@ -41,6 +73,9 @@ github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= github.com/go-openapi/swag v0.22.4 h1:QLMzNJnMGPRNDCbySlcj1x01tzU8/9LTTL9hZZZogBU= github.com/go-openapi/swag v0.22.4/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= +github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= +github.com/go-sql-driver/mysql v1.9.0 h1:Y0zIbQXhQKmQgTp44Y1dp3wTXcn804QoTptLZT1vtvo= +github.com/go-sql-driver/mysql v1.9.0/go.mod h1:pDetrLJeA3oMujJuvXc8RJoasr589B6A9fwzD3QMrqw= github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= @@ -52,9 +87,10 @@ github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I= github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U= +github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= -github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= @@ -87,6 +123,10 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4= +github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I= +github.com/magiconair/properties v1.8.9 h1:nWcCbLq1N2v/cpNsy5WvQ37Fb+YElfq20WJ/a8RkpQM= +github.com/magiconair/properties v1.8.9/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= @@ -94,17 +134,35 @@ github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovk github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= +github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= +github.com/moby/patternmatcher v0.6.0 h1:GmP9lR19aU5GqSSFko+5pRqHi+Ohk1O69aFiKkVGiPk= +github.com/moby/patternmatcher v0.6.0/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc= +github.com/moby/sys/sequential v0.5.0 h1:OPvI35Lzn9K04PBbCLW0g4LcFAJgHsvXsRyewg5lXtc= +github.com/moby/sys/sequential v0.5.0/go.mod h1:tH2cOOs5V9MlPiXcQzRC+eEyab644PWKGRYaaV5ZZlo= +github.com/moby/sys/user v0.1.0 h1:WmZ93f5Ux6het5iituh9x2zAG7NFY9Aqi49jjE1PaQg= +github.com/moby/sys/user v0.1.0/go.mod h1:fKJhFOnsCN6xZ5gSfbM6zaHGgDJMrqt9/reuj4T7MmU= +github.com/moby/sys/userns v0.1.0 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g= +github.com/moby/sys/userns v0.1.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28= +github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= +github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= +github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/onsi/ginkgo/v2 v2.13.0 h1:0jY9lJquiL8fcf3M4LAXN5aMlS/b2BV86HFFPCPMgE4= github.com/onsi/ginkgo/v2 v2.13.0/go.mod h1:TE309ZR8s5FsKKpuB1YAQYBzCaAfUgatB/xlT/ETL/o= github.com/onsi/gomega v1.29.0 h1:KIA/t2t5UBzoirT4H9tsML45GEbo3ouUnBHsCfD2tVg= github.com/onsi/gomega v1.29.0/go.mod h1:9sxs+SwGrKI0+PWe4Fxa9tFQQBG5xSsSbMXOI8PPpoQ= +github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= +github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= +github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= +github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M= github.com/openzipkin/zipkin-go v0.4.3 h1:9EGwpqkgnwdEIJ+Od7QVSEIH+ocmm5nPat0G7sjsSdg= github.com/openzipkin/zipkin-go v0.4.3/go.mod h1:M9wCJZFWCo2RiY+o1eBCEMe0Dp2S5LDHcMZmk3RmK7c= github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= @@ -113,6 +171,8 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw= +github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g= github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U= github.com/prometheus/client_golang v1.21.1 h1:DOvXXTqVzvkIewV/CDPFdejpMCGeMcbGCQ8YOmu+Ibk= @@ -127,8 +187,12 @@ github.com/redis/go-redis/v9 v9.7.3 h1:YpPyAayJV+XErNsatSElgRZZVCwXX9QzkKYNvO7x0 github.com/redis/go-redis/v9 v9.7.3/go.mod h1:bGUrSggJ9X9GUmZpZNEOQKaANxSGgOEBRltRTZHSvrA= github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= +github.com/shirou/gopsutil/v4 v4.25.1 h1:QSWkTc+fu9LTAWfkZwZ6j8MSUk4A2LV7rbH0ZqmLjXs= +github.com/shirou/gopsutil/v4 v4.25.1/go.mod h1:RoUCUpndaJFtT+2zsZzzmhvbfGoDCJ7nFXKJf8GqJbI= github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k= github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI= github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= @@ -139,6 +203,7 @@ github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpE github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= @@ -146,11 +211,19 @@ github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXl github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/testcontainers/testcontainers-go v0.36.0 h1:YpffyLuHtdp5EUsI5mT4sRw8GZhO/5ozyDT1xWGXt00= +github.com/testcontainers/testcontainers-go v0.36.0/go.mod h1:yk73GVJ0KUZIHUtFna6MO7QS144qYpoY8lEEtU9Hed0= +github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU= +github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI= +github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk= +github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yuin/gopher-lua v1.1.1 h1:kYKnWBjvbNP4XLT3+bPEwAXJx262OhaHDWDVOPjL46M= github.com/yuin/gopher-lua v1.1.1/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw= +github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= +github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= github.com/zeromicro/go-zero v1.8.2 h1:AbJckBoojbr1lqCN1dkvURTIHOau7yvKReEd7ZmjuCk= github.com/zeromicro/go-zero v1.8.2/go.mod h1:G5dF+jzCEuq0t1j8qdrtVAy30QMgctGcKSfqFIGsvSg= go.etcd.io/etcd/api/v3 v3.5.15 h1:3KpLJir1ZEBrYuV2v+Twaa/e2MdDCEZ/70H+lzEiwsk= @@ -161,8 +234,10 @@ go.etcd.io/etcd/client/v3 v3.5.15 h1:23M0eY4Fd/inNv1ZfU3AxrbbOdW79r9V9Rl62Nm6ip4 go.etcd.io/etcd/client/v3 v3.5.15/go.mod h1:CLSJxrYjvLtHsrPKsy7LmZEE+DK2ktfd2bN4RhBMwlU= go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= -go.opentelemetry.io/otel v1.34.0 h1:zRLXxLCgL1WyKsPVrgbSdMN4c0FMkDAskSTQP+0hdUY= -go.opentelemetry.io/otel v1.34.0/go.mod h1:OWFPOQ+h4G8xpyjgqo4SxJYdDQ/qmRH+wivy7zzx9oI= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 h1:jq9TW8u3so/bN+JPT166wjOI6/vQPF6Xe7nMNIltagk= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0/go.mod h1:p8pYQP+m5XfbZm9fxtSKAbM6oIllS7s2AfxrChvc7iw= +go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ= +go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y= go.opentelemetry.io/otel/exporters/jaeger v1.17.0 h1:D7UpUy2Xc2wsi1Ras6V40q806WM07rqoCWzXu7Sqy+4= go.opentelemetry.io/otel/exporters/jaeger v1.17.0/go.mod h1:nPCqOnEH9rNLKqH/+rrUjiMzHJdV1BlpKcTwRTyKkKI= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.24.0 h1:t6wl9SPayj+c7lEIFgm4ooDBZVb01IhLB4InpomhRw8= @@ -175,14 +250,14 @@ go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.24.0 h1:s0PHtIkN+3xrbDO go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.24.0/go.mod h1:hZlFbDbRt++MMPCCfSJfmhkGIWnX1h3XjkfxZUjLrIA= go.opentelemetry.io/otel/exporters/zipkin v1.24.0 h1:3evrL5poBuh1KF51D9gO/S+N/1msnm4DaBqs/rpXUqY= go.opentelemetry.io/otel/exporters/zipkin v1.24.0/go.mod h1:0EHgD8R0+8yRhUYJOGR8Hfg2dpiJQxDOszd5smVO9wM= -go.opentelemetry.io/otel/metric v1.34.0 h1:+eTR3U0MyfWjRDhmFMxe2SsW64QrZ84AOhvqS7Y+PoQ= -go.opentelemetry.io/otel/metric v1.34.0/go.mod h1:CEDrp0fy2D0MvkXE+dPV7cMi8tWZwX3dmaIhwPOaqHE= +go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M= +go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE= go.opentelemetry.io/otel/sdk v1.34.0 h1:95zS4k/2GOy069d321O8jWgYsW3MzVV+KuSPKp7Wr1A= go.opentelemetry.io/otel/sdk v1.34.0/go.mod h1:0e/pNiaMAqaykJGKbi+tSjWfNNHMTxoC9qANsCzbyxU= go.opentelemetry.io/otel/sdk/metric v1.34.0 h1:5CeK9ujjbFVL5c1PhLuStg1wxA7vQv7ce1EK0Gyvahk= go.opentelemetry.io/otel/sdk/metric v1.34.0/go.mod h1:jQ/r8Ze28zRKoNRdkjCZxfs6YvBTG1+YIqyFVFYec5w= -go.opentelemetry.io/otel/trace v1.34.0 h1:+ouXS2V8Rd4hp4580a8q23bg0azF2nI8cqLYnC8mh/k= -go.opentelemetry.io/otel/trace v1.34.0/go.mod h1:Svm7lSjQD7kG7KJ/MUHPVXSDGz2OX4h0M2jHBhmSfRE= +go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs= +go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc= go.opentelemetry.io/proto/otlp v1.3.1 h1:TrMUixzpM0yuc/znrFTP9MMRh8trP93mkCiDVeXrui0= go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8= go.uber.org/atomic v1.10.0 h1:9qC72Qh0+3MqyJbAn8YU5xVq1frD8bn3JtD2oXtafVQ= @@ -198,6 +273,8 @@ go.uber.org/zap v1.24.0/go.mod h1:2kMP+WWQ8aoFoedH3T2sq6iJ2yDWpHbP0f6MQbS9Gkg= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus= +golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= @@ -216,14 +293,20 @@ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= -golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= +golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.29.0 h1:L6pJp37ocefwRRtYPKSWOWzOtWSxVajvz2ldH/xi3iU= golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s= @@ -265,8 +348,13 @@ gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gorm.io/driver/mysql v1.5.7 h1:MndhOPYOfEp2rHKgkZIhJ16eVUIRf2HmzgoPmh7FCWo= +gorm.io/driver/mysql v1.5.7/go.mod h1:sEtPWMiqiN1N1cMXoXmBbd8C6/l+TESwriotuRRpkDM= +gorm.io/gorm v1.25.7/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8= gorm.io/gorm v1.25.12 h1:I0u8i2hWQItBq1WfE0o2+WuL9+8L21K9e2HHSTE/0f8= gorm.io/gorm v1.25.12/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ= +gotest.tools/v3 v3.5.1 h1:EENdUnS3pdur5nybKYIh2Vfgc8IUNBjxDPSjtiJcOzU= +gotest.tools/v3 v3.5.1/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU= k8s.io/api v0.29.3 h1:2ORfZ7+bGC3YJqGpV0KSDDEVf8hdGQ6A03/50vj8pmw= k8s.io/api v0.29.3/go.mod h1:y2yg2NTyHUUkIoTC+phinTnEa3KFM6RZ3szxt014a80= k8s.io/apimachinery v0.29.4 h1:RaFdJiDmuKs/8cm1M6Dh1Kvyh59YQFDcFuFTSmXes6Q= diff --git a/internal/config/config.go b/internal/config/config.go index c1f85b9..7078439 100755 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -1,7 +1,22 @@ package config -import "github.com/zeromicro/go-zero/zrpc" +import ( + "github.com/zeromicro/go-zero/zrpc" + "time" +) type Config struct { zrpc.RpcServerConf + + MySQL struct { + UserName string + Password string + Host string + Port string + Database string + MaxIdleConns int + MaxOpenConns int + ConnMaxLifetime time.Duration + LogLevel string + } } diff --git a/pkg/domain/entity/transaction.go b/pkg/domain/entity/transaction.go new file mode 100644 index 0000000..930fa3c --- /dev/null +++ b/pkg/domain/entity/transaction.go @@ -0,0 +1,31 @@ +package entity + +import ( + "code.30cm.net/digimon/app-cloudep-wallet-service/pkg/domain/wallet" + "github.com/shopspring/decimal" +) + +// Transaction 代表一筆錢包交易紀錄(例如充值、扣款、轉帳等) +// 此表記錄所有交易的詳細資訊,包括金額、對象、餘額狀態與交易型態等 +type Transaction struct { + ID int64 `gorm:"column:id"` // 交易主鍵 ID,自動遞增 + OrderID string `gorm:"column:order_id"` // 關聯的訂單 ID,可為空(若不是由訂單觸發) + TransactionID string `gorm:"column:transaction_id"` // 此筆交易的唯一識別碼(系統內部使用,可為 UUID) + Brand string `gorm:"column:brand"` // 所屬品牌(支援多品牌場景) + UID string `gorm:"column:uid"` // 交易發起者的 UID + ToUID string `gorm:"column:to_uid"` // 交易對象的 UID(如為轉帳場景) + Type wallet.TxType `gorm:"column:type"` // 交易類型(如轉帳、入金、出金等,自定義列舉) + BusinessType int8 `gorm:"column:business_type"` // 業務類型(如合約、模擬、一般用途等,數字代碼) + Crypto string `gorm:"column:crypto"` // 幣種(如 BTC、ETH、USD、TWD 等) + Amount decimal.Decimal `gorm:"column:amount"` // 本次變動金額(正數為增加,負數為扣減) + Balance decimal.Decimal `gorm:"column:balance"` // 交易完成後的錢包餘額 + BeforeBalance decimal.Decimal `gorm:"column:before_balance"` // 交易前的錢包餘額(方便審計與對帳) + Status wallet.Enable `gorm:"column:status"` // 狀態(1: 有效、0: 無效/已取消) + CreateAt int64 `gorm:"column:create_time;autoCreateTime"` // 建立時間(Unix 秒數) + DueTime int64 `gorm:"column:due_time"` // 到期時間(適用於凍結或延後入帳等場景) +} + +// TableName 指定 GORM 對應的資料表名稱 +func (t *Transaction) TableName() string { + return "transaction" +} diff --git a/pkg/domain/entity/wallet.go b/pkg/domain/entity/wallet.go index 500d690..bd040c0 100644 --- a/pkg/domain/entity/wallet.go +++ b/pkg/domain/entity/wallet.go @@ -19,14 +19,14 @@ import ( // Brand 欄位讓平台可以支援多租戶架構(不同品牌有不同的錢包群組)。 type Wallet struct { - ID int64 `gorm:"column:id"` // 主鍵 ID(錢包的唯一識別) - Brand string `gorm:"column:brand"` // 品牌/平台(區分多租戶、多品牌情境) - UID string `gorm:"column:uid"` // 使用者 UID - Asset string `gorm:"column:asset"` // 資產代號,可為 BTC、ETH、GEM_RED、GEM_BLUE、POINTS ,USD, TWD 等.... - Balance decimal.Decimal `gorm:"column:balance"` // 餘額(使用高精度 decimal 避免浮點誤差) - Type wallet.Types `gorm:"column:type"` // 錢包類型 - CreateTime int64 `gorm:"column:create_time;autoCreateTime"` // 建立時間(UnixNano timestamp) - UpdateTime int64 `gorm:"column:update_time;autoUpdateTime"` // 更新時間(UnixNano timestamp) + ID int64 `gorm:"column:id"` // 主鍵 ID(錢包的唯一識別) + Brand string `gorm:"column:brand"` // 品牌/平台(區分多租戶、多品牌情境) + UID string `gorm:"column:uid"` // 使用者 UID + Asset string `gorm:"column:asset"` // 資產代號,可為 BTC、ETH、GEM_RED、GEM_BLUE、POINTS ,USD, TWD 等.... + Balance decimal.Decimal `gorm:"column:balance"` // 餘額(使用高精度 decimal 避免浮點誤差) + Type wallet.Types `gorm:"column:type"` // 錢包類型 + CreateAt int64 `gorm:"column:create_at"` // 建立時間(UnixNano timestamp) + UpdateAt int64 `gorm:"column:update_at"` // 更新時間(UnixNano timestamp) } func (c *Wallet) TableName() string { diff --git a/pkg/domain/entity/wallet_transaction.go b/pkg/domain/entity/wallet_transaction.go new file mode 100644 index 0000000..a03f782 --- /dev/null +++ b/pkg/domain/entity/wallet_transaction.go @@ -0,0 +1,26 @@ +package entity + +import ( + "code.30cm.net/digimon/app-cloudep-wallet-service/pkg/domain/wallet" + "github.com/shopspring/decimal" +) + +// WalletTransaction 表示錢包的交易紀錄(每一次扣款、加值、入帳、退費都會有一筆紀錄) +type WalletTransaction struct { + ID int64 `gorm:"column:id"` // 主鍵 ID(自動遞增) + TransactionID int64 `gorm:"column:transaction_id"` // 交易流水號(可對應某次業務操作,例如同一訂單的多筆變化) + OrderID string `gorm:"column:order_id"` // 訂單編號(對應實際訂單或業務事件) + Brand string `gorm:"column:brand"` // 品牌(多租戶或多平台識別) + UID string `gorm:"column:uid"` // 使用者 UID + WalletType wallet.Types `gorm:"column:wallet_type"` // 錢包類型(例如:主錢包、獎勵錢包、凍結錢包等) + BusinessType int8 `gorm:"column:business_type"` // 業務類型(定義本次交易是什麼用途,例如購物、退款、加值等) + Asset string `gorm:"column:asset"` // 資產代號,可為 BTC、ETH、GEM_RED、GEM_BLUE、POINTS ,USD, TWD 等.... + Amount decimal.Decimal `gorm:"column:amount"` // 變動金額(正數為收入,負數為支出) + Balance decimal.Decimal `gorm:"column:balance"` // 當前錢包餘額(這筆交易後的餘額快照) + CreateAt int64 `gorm:"column:create_at"` // 建立時間(UnixNano,紀錄交易發生時間) +} + +// TableName 指定 GORM 對應的資料表名稱 +func (t *WalletTransaction) TableName() string { + return "wallet_transaction" +} diff --git a/pkg/domain/repository/error.go b/pkg/domain/repository/error.go new file mode 100644 index 0000000..735b876 --- /dev/null +++ b/pkg/domain/repository/error.go @@ -0,0 +1,6 @@ +package repository + +import "errors" + +var ErrRecordNotFound = errors.New("query record not found") +var ErrBalanceInsufficient = errors.New("balance insufficient") diff --git a/pkg/domain/repository/transaction.go b/pkg/domain/repository/transaction.go new file mode 100644 index 0000000..35ad18f --- /dev/null +++ b/pkg/domain/repository/transaction.go @@ -0,0 +1,32 @@ +package repository + +import ( + "code.30cm.net/digimon/app-cloudep-wallet-service/pkg/domain/entity" + "code.30cm.net/digimon/app-cloudep-wallet-service/pkg/domain/wallet" + "context" + "time" +) + +type TransactionRepository interface { + FindByOrderID(ctx context.Context, orderID string) (entity.Transaction, error) + Insert(ctx context.Context, tx *entity.Transaction) error + BatchInsert(ctx context.Context, txs []*entity.Transaction) error + List(ctx context.Context, query TransactionQuery) ([]entity.Transaction, int64, error) + FindByDueTimeRange(ctx context.Context, start time.Time, txType []wallet.TxType) ([]entity.Transaction, error) + UpdateStatusByID(ctx context.Context, id int64, status int) error + ListWalletTransactions(ctx context.Context, uid string, orderIDs []string, walletType wallet.Types) ([]entity.WalletTransaction, error) +} + +type TransactionQuery struct { + UID *string + OrderID *string + Brand *string + Assets *string + WalletType *wallet.Types + TxTypes []wallet.TxType + BusinessType []int8 + StartTime *int64 + EndTime *int64 + PageIndex int64 + PageSize int64 +} diff --git a/pkg/domain/repository/wallet.go b/pkg/domain/repository/wallet.go index 977e646..860f2b7 100644 --- a/pkg/domain/repository/wallet.go +++ b/pkg/domain/repository/wallet.go @@ -10,11 +10,11 @@ import ( ) type Wallet struct { - Brand string `gorm:"column:brand"` // 品牌/平台(區分多租戶、多品牌情境) - UID string `gorm:"column:uid"` // 使用者 UID - Asset string `gorm:"column:asset"` // 資產代號,可為 BTC、ETH、GEM_RED、GEM_BLUE、POINTS ,USD, TWD 等.... - Balance decimal.Decimal `gorm:"column:balance"` // 餘額(使用高精度 decimal 避免浮點誤差) - Type wallet.Types `gorm:"column:type"` // 錢包類型 + Brand string // 品牌/平台(區分多租戶、多品牌情境) + UID string // 使用者 UID + Asset string // 資產代號,可為 BTC、ETH、GEM_RED、GEM_BLUE、POINTS ,USD, TWD 等.... + Balance decimal.Decimal // 餘額(使用高精度 decimal 避免浮點誤差) + Type wallet.Types // 錢包類型 } // WalletRepository 是錢包的總入口,負責查詢、初始化與跨帳戶查詢邏輯 @@ -75,8 +75,8 @@ type UserWalletService interface { // CheckReady 檢查錢包是否已經存在並準備好 CheckReady(ctx context.Context) (bool, error) // Add 加值與扣款邏輯(含業務類別) - Add(kind wallet.Types, business wallet.BusinessName, amount decimal.Decimal) error - Sub(kind wallet.Types, business wallet.BusinessName, amount decimal.Decimal) error + Add(kind wallet.Types, orderID string, amount decimal.Decimal) error + Sub(kind wallet.Types, orderID string, amount decimal.Decimal) error // AddTransaction 新增一筆交易紀錄(建立資料) AddTransaction(txID int64, orderID string, brand string, business wallet.BusinessName, kind wallet.Types, amount decimal.Decimal) //// PendingTransactions 查詢尚未執行的交易清單(會在 Execute 中一次提交) diff --git a/pkg/domain/repository/wallet_transaction.go b/pkg/domain/repository/wallet_transaction.go new file mode 100644 index 0000000..30cc757 --- /dev/null +++ b/pkg/domain/repository/wallet_transaction.go @@ -0,0 +1,17 @@ +package repository + +import ( + "code.30cm.net/digimon/app-cloudep-wallet-service/pkg/domain/entity" + "context" + "gorm.io/gorm" +) + +type WalletTransactionRepo interface { + Create(ctx context.Context, db *gorm.DB, tx []entity.WalletTransaction) error + HistoryBalance(ctx context.Context, req HistoryReq) ([]entity.WalletTransaction, error) +} + +type HistoryReq struct { + UID string + StartTime int64 +} diff --git a/pkg/domain/wallet/business_name.go b/pkg/domain/wallet/business_name.go index a9aaac0..408f798 100644 --- a/pkg/domain/wallet/business_name.go +++ b/pkg/domain/wallet/business_name.go @@ -1,3 +1,7 @@ package wallet type BusinessName string + +func (b BusinessName) ToInt8() int8 { + return int8(0) +} diff --git a/pkg/domain/wallet/status.go b/pkg/domain/wallet/status.go new file mode 100644 index 0000000..aeb5ef9 --- /dev/null +++ b/pkg/domain/wallet/status.go @@ -0,0 +1,16 @@ +package wallet + +type Enable int8 + +const ( + EnableTrue Enable = 1 + EnableFalse Enable = 2 +) + +func (e Enable) ToInt8() int8 { + return int8(e) +} + +func (e Enable) ToBool() bool { + return e == EnableTrue +} diff --git a/pkg/domain/wallet/tx_type.go b/pkg/domain/wallet/tx_type.go new file mode 100644 index 0000000..58e400e --- /dev/null +++ b/pkg/domain/wallet/tx_type.go @@ -0,0 +1,35 @@ +package wallet + +type TxType int8 + +// 交易類型 +const ( + // Deposit 充值(增加可用餘額) + Deposit TxType = iota + 1 + // Withdraw 提現(減少可用餘額) + Withdraw + // Freeze 凍結(減少可用餘額,加在凍結餘額) + Freeze + // UnFreeze 解凍(減少凍結餘額) + UnFreeze + // RollbackFreeze (減少凍結餘額,加回可用餘額,不可指定金額,根據訂單) + RollbackFreeze + // Unconfirmed 限制(減少凍結餘額,加別人限制餘額) + Unconfirmed + // CancelFreeze 取消凍結(減少凍結餘額,加回可用餘額,,可指定金額) + CancelFreeze + // DepositUnconfirmed 充值(增加限制餘額) + DepositUnconfirmed + // AppendFreeze 追加凍結(減少可用餘額,加在凍結餘額) + AppendFreeze + // RollbackFreezeAddAvailable (rollback凍結餘額,指定金額加回可用餘額) + RollbackFreezeAddAvailable + // Distribution 平台分發(活動送錢給玩家) + Distribution + // SystemTransfer 系統劃轉 + SystemTransfer +) + +func (t TxType) ToInt() int { + return int(t) +} diff --git a/pkg/domain/wallet/wallet_type.go b/pkg/domain/wallet/wallet_type.go index e455c25..18f6760 100644 --- a/pkg/domain/wallet/wallet_type.go +++ b/pkg/domain/wallet/wallet_type.go @@ -4,7 +4,7 @@ type Types int8 const ( TypeAvailable Types = iota + 1 // 可動用金額(使用者可以自由花用的餘額) - TypeFreezeType // 被凍結金額(交易進行中或風控鎖住的金額) + TypeFreeze // 被凍結金額(交易進行中或風控鎖住的金額) TypeUnconfirmed // 未確認金額(交易已送出但區塊鏈尚未確認) // 以下為進階用途:合約或模擬交易錢包 @@ -13,3 +13,5 @@ const ( TypeSimulationAvailable // 模擬交易可用金額(例如沙盒環境) TypeSimulationFreeze // 模擬交易凍結金額 ) + +var AllTypes = []Types{TypeAvailable, TypeFreeze, TypeUnconfirmed} diff --git a/pkg/lib/sql_client/config.go b/pkg/lib/sql_client/config.go new file mode 100644 index 0000000..c968854 --- /dev/null +++ b/pkg/lib/sql_client/config.go @@ -0,0 +1,16 @@ +package sql_client + +import "time" + +type Config struct { + User string + Password string + Host string + Port string + Database string + MaxIdleConns int + MaxOpenConns int + ConnMaxLifetime time.Duration + InterpolateParams bool + LogLevel string +} diff --git a/pkg/lib/sql_client/const.go b/pkg/lib/sql_client/const.go new file mode 100644 index 0000000..3b36f5e --- /dev/null +++ b/pkg/lib/sql_client/const.go @@ -0,0 +1,10 @@ +package sql_client + +import "time" + +const ( + defaultMaxOpenConns = 25 + defaultMaxIdleConns = 25 + defaultMaxLifeTime = 5 * time.Minute + defaultSlowSQLThreshold = 200 * time.Millisecond +) diff --git a/pkg/lib/sql_client/initMySQL.go b/pkg/lib/sql_client/initMySQL.go new file mode 100644 index 0000000..9da21c5 --- /dev/null +++ b/pkg/lib/sql_client/initMySQL.go @@ -0,0 +1,30 @@ +package sql_client + +import ( + "code.30cm.net/digimon/app-cloudep-wallet-service/internal/config" + "fmt" + "gorm.io/gorm" +) + +func NewMySQLClient(conf config.Config) (*gorm.DB, error) { + dbConf := &Config{ + User: conf.MySQL.UserName, + Password: conf.MySQL.Password, + Host: conf.MySQL.Host, + Port: conf.MySQL.Port, + Database: conf.MySQL.Database, + MaxIdleConns: conf.MySQL.MaxIdleConns, + MaxOpenConns: conf.MySQL.MaxOpenConns, + ConnMaxLifetime: conf.MySQL.ConnMaxLifetime, + InterpolateParams: true, + } + + dbInit := New(dbConf, WithLogLevel(conf.MySQL.LogLevel)) + + orm, err := dbInit.Conn() + if err != nil { + return nil, fmt.Errorf("failed to initiate db connection pool: %w", err) + } + + return orm, nil +} diff --git a/pkg/lib/sql_client/mysql.go b/pkg/lib/sql_client/mysql.go new file mode 100644 index 0000000..ea51cf0 --- /dev/null +++ b/pkg/lib/sql_client/mysql.go @@ -0,0 +1,84 @@ +package sql_client + +import ( + "fmt" + "gorm.io/driver/mysql" + "gorm.io/gorm" + "gorm.io/gorm/logger" + "gorm.io/gorm/schema" + "log" + "os" +) + +type MySQL struct { + mysqlConf gorm.Dialector + gormConf *gorm.Config + serverConf *Config +} + +var defaultConfig = logger.Config{ + SlowThreshold: defaultSlowSQLThreshold, + LogLevel: logger.Warn, + IgnoreRecordNotFoundError: false, + Colorful: true, +} + +// New initializes a MysqlInit using the provided Config and options. If +// opts is not provided it will initialize MysqlInit with default configuration. +func New(conf *Config, opts ...Option) *MySQL { + dsn := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?charset=utf8&parseTime=True&loc=Local&interpolateParams=%t", + conf.User, conf.Password, conf.Host, conf.Port, conf.Database, conf.InterpolateParams) + + loggerDefault := logger.New(log.New(os.Stdout, "\r\n", log.LstdFlags), defaultConfig) + + mysqli := &MySQL{ + mysqlConf: mysql.Open(dsn), + gormConf: &gorm.Config{ + NamingStrategy: schema.NamingStrategy{ + SingularTable: true, // use singular table name + }, + Logger: loggerDefault, + }, + serverConf: conf, + } + + for _, opt := range opts { + opt(mysqli) + } + + return mysqli +} + +// Conn initiates connection to database and return a gorm.DB. +func (mysqli *MySQL) Conn() (*gorm.DB, error) { + db, err := gorm.Open(mysqli.mysqlConf, mysqli.gormConf) + if err != nil { + return nil, fmt.Errorf("gorm open error: %w", err) + } + + sqlDB, err := db.DB() + if err != nil { + return nil, fmt.Errorf("get connect pool error :%w", err) + } + + maxIdleConns := defaultMaxIdleConns + if mysqli.serverConf.MaxIdleConns > 0 { + maxIdleConns = mysqli.serverConf.MaxIdleConns + } + + maxOpenConns := defaultMaxOpenConns + if mysqli.serverConf.MaxOpenConns > 0 { + maxOpenConns = mysqli.serverConf.MaxOpenConns + } + + maxLifeTime := defaultMaxLifeTime + if mysqli.serverConf.ConnMaxLifetime > 0 { + maxLifeTime = mysqli.serverConf.ConnMaxLifetime + } + + sqlDB.SetMaxIdleConns(maxIdleConns) + sqlDB.SetMaxOpenConns(maxOpenConns) + sqlDB.SetConnMaxLifetime(maxLifeTime) + + return db, nil +} diff --git a/pkg/lib/sql_client/options.go b/pkg/lib/sql_client/options.go new file mode 100644 index 0000000..bb6fd02 --- /dev/null +++ b/pkg/lib/sql_client/options.go @@ -0,0 +1,41 @@ +package sql_client + +import ( + "gorm.io/gorm/logger" + "strings" +) + +type Option func(*MySQL) + +// WithLogLevel set gorm log level. +func WithLogLevel(level string) Option { + return func(mysqli *MySQL) { + var gormLogLevel logger.LogLevel + + switch strings.ToLower(level) { + case "panic", "fatal", "error": + gormLogLevel = logger.Error + case "warn", "warning": + gormLogLevel = logger.Warn + case "info", "debug", "trace": + gormLogLevel = logger.Info + case "silent": + gormLogLevel = logger.Silent + default: + gormLogLevel = logger.Silent + } + + newLogger := mysqli.gormConf.Logger.LogMode(gormLogLevel) + + mysqli.gormConf.Logger = newLogger + } +} + +// WithPerformance disable default transaction and create a prepared statement +// related doc: https://gorm.io/docs/performance.html +func WithPerformance() Option { + return func(mysqli *MySQL) { + mysqli.gormConf.SkipDefaultTransaction = true + mysqli.gormConf.PrepareStmt = true + } +} diff --git a/pkg/repository/mysql_init_test.go b/pkg/repository/mysql_init_test.go new file mode 100755 index 0000000..c36a018 --- /dev/null +++ b/pkg/repository/mysql_init_test.go @@ -0,0 +1,65 @@ +package repository + +import ( + "context" + "fmt" + + "github.com/testcontainers/testcontainers-go" + "github.com/testcontainers/testcontainers-go/wait" +) + +const ( + MySQLUser = "root" + MySQLPassword = "password" + MySQLDatabase = "testdb" + MySQLPort = "3306" +) + +// 啟動 MySQL container +func startMySQLContainer() (host string, port string, dsn string, tearDown func(), err error) { + ctx := context.Background() + + req := testcontainers.ContainerRequest{ + Image: "mysql:8.0", + Env: map[string]string{ + "MYSQL_ROOT_PASSWORD": MySQLPassword, + "MYSQL_DATABASE": MySQLDatabase, + }, + ExposedPorts: []string{"3306/tcp"}, + WaitingFor: wait.ForListeningPort("3306/tcp"), + } + + mysqlC, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{ + ContainerRequest: req, + Started: true, + }) + if err != nil { + return "", "", "", nil, err + } + + mappedPort, err := mysqlC.MappedPort(ctx, "3306") + if err != nil { + return "", "", "", nil, err + } + + containerHost, err := mysqlC.Host(ctx) + if err != nil { + return "", "", "", nil, err + } + + // 組成 DSN(Data Source Name) + dsn = fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?parseTime=true", + MySQLUser, + MySQLPassword, + containerHost, + mappedPort.Port(), + MySQLDatabase, + ) + + tearDown = func() { + _ = mysqlC.Terminate(ctx) + } + + fmt.Printf("MySQL ready at: %s\n", dsn) + return containerHost, mappedPort.Port(), dsn, tearDown, nil +} diff --git a/pkg/repository/user_wallet.go b/pkg/repository/user_wallet.go index eefbd5e..67fce12 100644 --- a/pkg/repository/user_wallet.go +++ b/pkg/repository/user_wallet.go @@ -2,17 +2,297 @@ package repository import ( "code.30cm.net/digimon/app-cloudep-wallet-service/pkg/domain/entity" + "code.30cm.net/digimon/app-cloudep-wallet-service/pkg/domain/repository" + "code.30cm.net/digimon/app-cloudep-wallet-service/pkg/domain/wallet" + "context" + "database/sql" + "errors" + "fmt" "github.com/shopspring/decimal" "gorm.io/gorm" + "gorm.io/gorm/clause" + "time" ) -func NewUserWallet(db *gorm.DB, uid, crypto string) repository.UserWallet { - return &userWallet{ - db: db, - uid: uid, - crypto: crypto, +// 用戶某個幣種餘額 +type userWallet struct { + db *gorm.DB + uid string + asset string - localWalletBalance: make(map[domain.WalletType]entity.Wallet, len(domain.WalletAllType)), - localOrderBalance: make(map[int64]decimal.Decimal, len(domain.WalletAllType)), + // local wallet 相關計算的餘額存在這裡 + localWalletBalance map[wallet.Types]entity.Wallet + // local order wallet 相關計算的餘額存在這裡 + localOrderBalance map[int64]decimal.Decimal + // local wallet 內所有餘額變化紀錄 + transactions []entity.WalletTransaction +} + +func NewUserWallet(db *gorm.DB, uid, asset string) repository.UserWalletService { + return &userWallet{ + db: db, + uid: uid, + asset: asset, + localWalletBalance: make(map[wallet.Types]entity.Wallet, len(wallet.AllTypes)), + localOrderBalance: make(map[int64]decimal.Decimal, len(wallet.AllTypes)), } } + +func (repo *userWallet) Init(ctx context.Context, uid, asset, brand string) ([]entity.Wallet, error) { + wallets := make([]entity.Wallet, 0, len(wallet.AllTypes)) + for _, t := range wallet.AllTypes { + balance := decimal.Zero + + wallets = append(wallets, entity.Wallet{ + Brand: brand, + UID: uid, + Asset: asset, + Balance: balance, + Type: t, + }) + } + + if err := repo.db.WithContext(ctx).Create(&wallets).Error; err != nil { + return nil, err + } + + for _, v := range wallets { + repo.localWalletBalance[v.Type] = v + } + + return wallets, nil +} + +func (repo *userWallet) All(ctx context.Context) ([]entity.Wallet, error) { + var result []entity.Wallet + + err := repo.buildCommonWhereSQL(repo.uid, repo.asset). + WithContext(ctx). + Select("id, crypto, balance, type"). + Find(&result).Error + + if err != nil { + return []entity.Wallet{}, err + } + + for _, v := range result { + repo.localWalletBalance[v.Type] = v + } + + return result, nil +} + +func (repo *userWallet) Get(ctx context.Context, kinds []wallet.Types) ([]entity.Wallet, error) { + var wallets []entity.Wallet + + err := repo.buildCommonWhereSQL(repo.uid, repo.asset). + WithContext(ctx). + Model(&entity.Wallet{}). + Select("id, crypto, balance, type"). + Where("type IN ?", kinds). + Find(&wallets).Error + + if err != nil { + return []entity.Wallet{}, notFoundError(err) + } + + for _, w := range wallets { + repo.localWalletBalance[w.Type] = w + } + + return wallets, nil +} + +func (repo *userWallet) GetWithLock(ctx context.Context, kinds []wallet.Types) ([]entity.Wallet, error) { + var wallets []entity.Wallet + + err := repo.buildCommonWhereSQL(repo.uid, repo.asset). + WithContext(ctx). + Model(&entity.Wallet{}). + Select("id, crypto, balance, type"). + Where("type IN ?", kinds). + Clauses(clause.Locking{Strength: "UPDATE"}). + Find(&wallets).Error + + if err != nil { + return []entity.Wallet{}, notFoundError(err) + } + + for _, w := range wallets { + repo.localWalletBalance[w.Type] = w + } + + return wallets, nil +} + +func (repo *userWallet) LocalBalance(kind wallet.Types) decimal.Decimal { + w, ok := repo.localWalletBalance[kind] + if !ok { + return decimal.Zero + } + + return w.Balance +} + +func (repo *userWallet) LockByIDs(ctx context.Context, ids []int64) ([]entity.Wallet, error) { + var wallets []entity.Wallet + + err := repo.db.WithContext(ctx). + Model(&entity.Wallet{}). + Select("id, crypto, balance, type"). + Where("id IN ?", ids). + Clauses(clause.Locking{Strength: "UPDATE"}). + Find(&wallets).Error + + if err != nil { + return []entity.Wallet{}, notFoundError(err) + } + + for _, w := range wallets { + repo.localWalletBalance[w.Type] = w + } + + return wallets, nil +} + +func (repo *userWallet) CheckReady(ctx context.Context) (bool, error) { + var exists bool + err := repo.buildCommonWhereSQL(repo.uid, repo.asset).WithContext(ctx). + Model(&entity.Wallet{}). + Select("1"). + Where("type = ?", wallet.TypeAvailable). + Limit(1). + Scan(&exists).Error + + if err != nil { + return false, err + } + + return exists, nil +} + +// Add 新增某種餘額餘額 +// 使用前 localWalletBalance 必須有資料,所以必須執行過 GetWithLock / All 才會有資料 +func (repo *userWallet) Add(kind wallet.Types, orderID string, amount decimal.Decimal) error { + w, ok := repo.localWalletBalance[kind] + if !ok { + return repository.ErrRecordNotFound + } + + w.Balance = w.Balance.Add(amount) + if w.Balance.LessThan(decimal.Zero) { + return repository.ErrBalanceInsufficient + } + + repo.transactions = append(repo.transactions, entity.WalletTransaction{ + OrderID: orderID, + UID: repo.uid, + WalletType: kind, + Asset: repo.asset, + Amount: amount, + Balance: w.Balance, + }) + + repo.localWalletBalance[kind] = w + + return nil +} + +func (repo *userWallet) Sub(kind wallet.Types, orderID string, amount decimal.Decimal) error { + return repo.Add(kind, orderID, decimal.Zero.Sub(amount)) +} + +func (repo *userWallet) AddTransaction(txID int64, orderID string, brand string, business wallet.BusinessName, kind wallet.Types, amount decimal.Decimal) { + balance := repo.LocalBalance(kind).Add(amount) + repo.transactions = append(repo.transactions, entity.WalletTransaction{ + TransactionID: txID, + OrderID: orderID, + Brand: brand, + UID: repo.uid, + WalletType: kind, + BusinessType: business.ToInt8(), + Asset: repo.asset, + Amount: amount, + Balance: balance, + CreateAt: time.Now().UTC().UnixNano(), + }) +} + +func (repo *userWallet) Commit(ctx context.Context) error { + // 事務隔離等級設定 + rc := &sql.TxOptions{ + Isolation: sql.LevelReadCommitted, + ReadOnly: false, + } + + err := repo.db.Transaction(func(tx *gorm.DB) error { + for _, w := range repo.localWalletBalance { + err := tx.WithContext(ctx). + Model(&entity.Wallet{}). + Where("id = ?", w.ID). + UpdateColumns(map[string]any{ + "balance": w.Balance, + "update_time": time.Now().UTC().Unix(), + }).Error + + if err != nil { + return fmt.Errorf("failed to update wallet id %d: %w", w.ID, err) + } + } + + return nil // 所有更新成功才 return nil + }, rc) + + if err != nil { + return fmt.Errorf("update uid: %s asset: %s error: %w", repo.uid, repo.asset, err) + } + + return nil +} + +func (repo *userWallet) GetTransactions() []entity.WalletTransaction { + return repo.transactions +} + +func (repo *userWallet) CommitOrder(ctx context.Context) error { + rc := &sql.TxOptions{ + Isolation: sql.LevelReadCommitted, + ReadOnly: false, + } + err := repo.db.Transaction(func(tx *gorm.DB) error { + + for id, balance := range repo.localOrderBalance { + err := tx.WithContext(ctx). + Model(&entity.Transaction{}). + Where("id = ?", id). + Update("balance", balance).Error + + if err != nil { + return fmt.Errorf("failed to update order balance, id=%d, err=%w", id, err) + } + } + + return nil // 所有更新成功才 return nil + }, rc) + + if err != nil { + return fmt.Errorf("update uid: %s asset: %s error: %w", repo.uid, repo.asset, err) + } + + return nil +} + +// ============================================================================= + +func (repo *userWallet) buildCommonWhereSQL(uid, asset string) *gorm.DB { + return repo.db.Where("uid = ?", uid). + Where("asset = ?", asset) +} + +func notFoundError(err error) error { + if errors.Is(err, gorm.ErrRecordNotFound) { + return repository.ErrRecordNotFound + } + + return err +} diff --git a/pkg/repository/wallet.go b/pkg/repository/wallet.go index 500f656..a2db79b 100644 --- a/pkg/repository/wallet.go +++ b/pkg/repository/wallet.go @@ -23,7 +23,7 @@ func MustCategoryRepository(param WalletRepositoryParam) repository.WalletReposi } func (repo *WalletRepository) NewDB() *gorm.DB { - return repo.DB + return repo.DB.Begin() } func (repo *WalletRepository) Transaction(fn func(db *gorm.DB) error) error { @@ -45,26 +45,68 @@ func (repo *WalletRepository) Transaction(fn func(db *gorm.DB) error) error { } func (repo *WalletRepository) Session(uid, asset string) repository.UserWalletService { - //TODO implement me - panic("implement me") + return NewUserWallet(repo.DB, uid, asset) } func (repo *WalletRepository) SessionWithTx(db *gorm.DB, uid, asset string) repository.UserWalletService { - //TODO implement me - panic("implement me") + return NewUserWallet(db, uid, asset) } func (repo *WalletRepository) InitWallets(ctx context.Context, param []repository.Wallet) error { - //TODO implement me - panic("implement me") + wallets := make([]entity.Wallet, 0, len(param)) + for _, t := range param { + wallets = append(wallets, entity.Wallet{ + Brand: t.Brand, + UID: t.UID, + Asset: t.Asset, + Balance: t.Balance, + Type: t.Type, + }) + } + + if err := repo.DB.Create(&wallets).WithContext(ctx).Error; err != nil { + return err + } + + return nil } func (repo *WalletRepository) QueryBalances(ctx context.Context, req repository.BalanceQuery) ([]entity.Wallet, error) { - //TODO implement me - panic("implement me") + var data []entity.Wallet + s := repo.DB.WithContext(ctx). + Select("id, asset, balance, type, update_at"). + Where("uid = ?", req.UID) + + if req.Asset != "" { + s = s.Where("asset = ?", req.Asset) + } + + err := s.Find(&data).Error + if err != nil { + return nil, err + } + + return data, nil } func (repo *WalletRepository) QueryBalancesByUIDs(ctx context.Context, uids []string, req repository.BalanceQuery) ([]entity.Wallet, error) { - //TODO implement me - panic("implement me") + var data []entity.Wallet + query := repo.DB.WithContext(ctx). + Select("uid, asset, balance, type, update_at"). + Where("uid IN ?", uids) + + if req.Asset != "" { + query = query.Where("asset = ?", req.Asset) + } + + if len(req.Kinds) > 0 { + query = query.Where("type IN ?", req.Kinds) + } + + err := query.Find(&data).Error + if err != nil { + return nil, err + } + + return data, nil } diff --git a/pkg/repository/wallet_test.go b/pkg/repository/wallet_test.go new file mode 100644 index 0000000..e369483 --- /dev/null +++ b/pkg/repository/wallet_test.go @@ -0,0 +1,355 @@ +package repository + +import ( + "code.30cm.net/digimon/app-cloudep-wallet-service/internal/config" + "code.30cm.net/digimon/app-cloudep-wallet-service/pkg/domain/repository" + "code.30cm.net/digimon/app-cloudep-wallet-service/pkg/domain/wallet" + "code.30cm.net/digimon/app-cloudep-wallet-service/pkg/lib/sql_client" + "context" + "errors" + "fmt" + "github.com/shopspring/decimal" + "github.com/stretchr/testify/assert" + "gorm.io/gorm" + "testing" + "time" +) + +func SetupTestWalletRepository() (repository.WalletRepository, *gorm.DB, func(), error) { + host, port, _, tearDown, err := startMySQLContainer() + if err != nil { + return nil, nil, nil, fmt.Errorf("failed to start MySQL container: %w", err) + } + + conf := config.Config{ + MySQL: struct { + UserName string + Password string + Host string + Port string + Database string + MaxIdleConns int + MaxOpenConns int + ConnMaxLifetime time.Duration + LogLevel string + }{ + UserName: MySQLUser, + Password: MySQLPassword, + Host: host, + Port: port, + Database: MySQLDatabase, + MaxIdleConns: 10, + MaxOpenConns: 100, + ConnMaxLifetime: 300, + LogLevel: "info", + }, + } + + db, err := sql_client.NewMySQLClient(conf) + if err != nil { + tearDown() + return nil, nil, nil, fmt.Errorf("failed to create db client: %w", err) + } + + repo := MustCategoryRepository(WalletRepositoryParam{DB: db}) + return repo, db, tearDown, nil +} + +func TestWalletRepository_InitWallets(t *testing.T) { + repo, db, tearDown, err := SetupTestWalletRepository() + assert.NoError(t, err) + defer tearDown() + + // 🔽 這裡加上建表 SQL + createTableSQL := ` + CREATE TABLE IF NOT EXISTS wallet ( + id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主鍵 ID(錢包唯一識別)', + brand VARCHAR(50) NOT NULL COMMENT '品牌/平台(多租戶識別)', + uid VARCHAR(64) NOT NULL COMMENT '使用者 UID', + asset VARCHAR(32) NOT NULL COMMENT '資產代碼(如 BTC、ETH、GEM_RED 等)', + balance DECIMAL(30, 18) UNSIGNED NOT NULL DEFAULT 0 COMMENT '資產餘額', + type TINYINT NOT NULL COMMENT '錢包類型', + create_at INTEGER NOT NULL DEFAULT 0 COMMENT '建立時間(Unix)', + update_at INTEGER NOT NULL DEFAULT 0 COMMENT '更新時間(Unix)', + PRIMARY KEY (id), + UNIQUE KEY uq_brand_uid_asset_type (brand, uid, asset, type), + KEY idx_uid (uid), + KEY idx_brand (brand) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='錢包'; + ` + + err = db.Exec(createTableSQL).Error + assert.NoError(t, err) + + tests := []struct { + name string + param []repository.Wallet + wantErr bool + }{ + { + name: "insert single wallet", + param: []repository.Wallet{ + { + Brand: "test-brand", + UID: "user001", + Asset: "BTC", + Balance: decimal.NewFromFloat(10.5), + Type: wallet.TypeAvailable, + }, + }, + wantErr: false, + }, + { + name: "insert multiple wallets", + param: []repository.Wallet{ + { + Brand: "test-brand", + UID: "user002", + Asset: "ETH", + Balance: decimal.NewFromFloat(5), + Type: wallet.TypeAvailable, + }, + { + Brand: "test-brand", + UID: "user002", + Asset: "ETH", + Balance: decimal.NewFromFloat(1.2), + Type: wallet.TypeFreeze, + }, + }, + wantErr: false, + }, + { + name: "insert duplicate primary key (should fail if unique constraint)", + param: []repository.Wallet{ + { + Brand: "test-brand", + UID: "user001", + Asset: "BTC", + Balance: decimal.NewFromFloat(1), + Type: wallet.TypeAvailable, // 與第一筆測試資料相同 + }, + }, + wantErr: true, // 預期會違反 UNIQUE constraint + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := repo.InitWallets(context.Background(), tt.param) + if tt.wantErr { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + }) + } +} + +func TestWalletRepository_QueryBalances(t *testing.T) { + repo, db, tearDown, err := SetupTestWalletRepository() + assert.NoError(t, err) + defer tearDown() + // 🔽 這裡加上建表 SQL + createTableSQL := ` + CREATE TABLE IF NOT EXISTS wallet ( + id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主鍵 ID(錢包唯一識別)', + brand VARCHAR(50) NOT NULL COMMENT '品牌/平台(多租戶識別)', + uid VARCHAR(64) NOT NULL COMMENT '使用者 UID', + asset VARCHAR(32) NOT NULL COMMENT '資產代碼(如 BTC、ETH、GEM_RED 等)', + balance DECIMAL(30, 18) UNSIGNED NOT NULL DEFAULT 0 COMMENT '資產餘額', + type TINYINT NOT NULL COMMENT '錢包類型', + create_at INTEGER NOT NULL DEFAULT 0 COMMENT '建立時間(Unix)', + update_at INTEGER NOT NULL DEFAULT 0 COMMENT '更新時間(Unix)', + PRIMARY KEY (id), + UNIQUE KEY uq_brand_uid_asset_type (brand, uid, asset, type), + KEY idx_uid (uid), + KEY idx_brand (brand) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='錢包'; + ` + + err = db.Exec(createTableSQL).Error + assert.NoError(t, err) + + ctx := context.Background() + + // 建立初始化錢包資料 + wallets := []repository.Wallet{ + {Brand: "brand1", UID: "user1", Asset: "BTC", Balance: decimal.NewFromFloat(1.5), Type: wallet.TypeAvailable}, + {Brand: "brand1", UID: "user1", Asset: "ETH", Balance: decimal.NewFromFloat(2.5), Type: wallet.TypeFreeze}, + {Brand: "brand1", UID: "user2", Asset: "BTC", Balance: decimal.NewFromFloat(3.0), Type: wallet.TypeAvailable}, + } + + err = repo.InitWallets(ctx, wallets) + assert.NoError(t, err) + + tests := []struct { + name string + query repository.BalanceQuery + expected int + }{ + { + name: "Query all by UID", + query: repository.BalanceQuery{ + UID: "user1", + }, + expected: 2, + }, + { + name: "Query by UID and Asset", + query: repository.BalanceQuery{ + UID: "user1", + Asset: "BTC", + }, + expected: 1, + }, + { + name: "Query by UID with non-existing asset", + query: repository.BalanceQuery{ + UID: "user1", + Asset: "GEM_RED", + }, + expected: 0, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := repo.QueryBalances(ctx, tt.query) + assert.NoError(t, err) + assert.Len(t, got, tt.expected) + }) + } +} + +func TestWalletRepository_QueryBalancesByUIDs(t *testing.T) { + repo, db, tearDown, err := SetupTestWalletRepository() + assert.NoError(t, err) + defer tearDown() + + // 🔽 這裡加上建表 SQL + createTableSQL := ` + CREATE TABLE IF NOT EXISTS wallet ( + id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主鍵 ID(錢包唯一識別)', + brand VARCHAR(50) NOT NULL COMMENT '品牌/平台(多租戶識別)', + uid VARCHAR(64) NOT NULL COMMENT '使用者 UID', + asset VARCHAR(32) NOT NULL COMMENT '資產代碼(如 BTC、ETH、GEM_RED 等)', + balance DECIMAL(30, 18) UNSIGNED NOT NULL DEFAULT 0 COMMENT '資產餘額', + type TINYINT NOT NULL COMMENT '錢包類型', + create_at INTEGER NOT NULL DEFAULT 0 COMMENT '建立時間(Unix)', + update_at INTEGER NOT NULL DEFAULT 0 COMMENT '更新時間(Unix)', + PRIMARY KEY (id), + UNIQUE KEY uq_brand_uid_asset_type (brand, uid, asset, type), + KEY idx_uid (uid), + KEY idx_brand (brand) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='錢包'; + ` + + err = db.Exec(createTableSQL).Error + assert.NoError(t, err) + + ctx := context.Background() + + // 初始化錢包資料 + initData := []repository.Wallet{ + {Brand: "brand1", UID: "user1", Asset: "BTC", Balance: decimal.NewFromFloat(1.5), Type: wallet.TypeAvailable}, + {Brand: "brand1", UID: "user2", Asset: "BTC", Balance: decimal.NewFromFloat(2.0), Type: wallet.TypeAvailable}, + {Brand: "brand1", UID: "user2", Asset: "ETH", Balance: decimal.NewFromFloat(3.0), Type: wallet.TypeFreeze}, + {Brand: "brand1", UID: "user3", Asset: "BTC", Balance: decimal.NewFromFloat(4.5), Type: wallet.TypeAvailable}, + } + + err = repo.InitWallets(ctx, initData) + assert.NoError(t, err) + + tests := []struct { + name string + uids []string + query repository.BalanceQuery + expected int + }{ + { + name: "Query all users with BTC", + uids: []string{"user1", "user2", "user3"}, + query: repository.BalanceQuery{Asset: "BTC"}, + expected: 3, + }, + { + name: "Query specific users with filter by type", + uids: []string{"user2"}, + query: repository.BalanceQuery{Kinds: []wallet.Types{wallet.TypeAvailable}}, + expected: 1, + }, + { + name: "Query with no matches", + uids: []string{"user2"}, + query: repository.BalanceQuery{Asset: "DOGE"}, + expected: 0, + }, + { + name: "Query all for user2", + uids: []string{"user2"}, + query: repository.BalanceQuery{}, + expected: 2, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result, err := repo.QueryBalancesByUIDs(ctx, tt.uids, tt.query) + assert.NoError(t, err) + assert.Len(t, result, tt.expected) + }) + } +} + +func TestWalletRepository_NewDB(t *testing.T) { + repo, _, tearDown, err := SetupTestWalletRepository() + assert.NoError(t, err) + defer tearDown() + + tx := repo.NewDB() + assert.NotNil(t, tx) + assert.NoError(t, tx.Commit().Error) +} + +func TestWalletRepository_Transaction(t *testing.T) { + repo, _, tearDown, err := SetupTestWalletRepository() + assert.NoError(t, err) + defer tearDown() + + t.Run("commit success", func(t *testing.T) { + err := repo.Transaction(func(tx *gorm.DB) error { + return nil // 模擬成功流程 + }) + assert.NoError(t, err) + }) + + t.Run("rollback due to fn error", func(t *testing.T) { + customErr := errors.New("rollback me") + err := repo.Transaction(func(tx *gorm.DB) error { + return customErr + }) + assert.ErrorIs(t, err, customErr) + }) +} + +func TestWalletRepository_Session(t *testing.T) { + repo, _, tearDown, err := SetupTestWalletRepository() + assert.NoError(t, err) + defer tearDown() + + sess := repo.Session("userX", "BTC") + assert.NotNil(t, sess) +} + +func TestWalletRepository_SessionWithTx(t *testing.T) { + repo, _, tearDown, err := SetupTestWalletRepository() + assert.NoError(t, err) + defer tearDown() + + tx := repo.NewDB() + defer tx.Rollback() + + sess := repo.SessionWithTx(tx, "userY", "ETH") + assert.NotNil(t, sess) +}