feat: add mysql lib

This commit is contained in:
王性驊 2025-04-16 17:24:54 +08:00
parent 4cb6faefdc
commit 5214ea2283
31 changed files with 1347 additions and 50 deletions

View File

@ -0,0 +1 @@
DROP TABLE IF EXISTS `wallet`;

View File

@ -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='錢包';

View File

@ -0,0 +1 @@
DROP TABLE IF EXISTS `transaction`;

View File

@ -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='交易紀錄表';

View File

@ -0,0 +1 @@
DROP TABLE IF EXISTS `transaction`;

View File

@ -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 = '錢包資金異動紀錄(每一次交易行為的快照記錄)';

View File

@ -0,0 +1 @@
DROP DATABASE IF EXISTS `digimon_wallet`;

View File

@ -0,0 +1 @@
CREATE DATABASE IF NOT EXISTS `digimon_wallet`;

49
go.mod
View File

@ -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

108
go.sum
View File

@ -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=

View File

@ -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
}
}

View File

@ -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"
}

View File

@ -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 {

View File

@ -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"
}

View File

@ -0,0 +1,6 @@
package repository
import "errors"
var ErrRecordNotFound = errors.New("query record not found")
var ErrBalanceInsufficient = errors.New("balance insufficient")

View File

@ -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
}

View File

@ -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 中一次提交)

View File

@ -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
}

View File

@ -1,3 +1,7 @@
package wallet
type BusinessName string
func (b BusinessName) ToInt8() int8 {
return int8(0)
}

View File

@ -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
}

View File

@ -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)
}

View File

@ -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}

View File

@ -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
}

View File

@ -0,0 +1,10 @@
package sql_client
import "time"
const (
defaultMaxOpenConns = 25
defaultMaxIdleConns = 25
defaultMaxLifeTime = 5 * time.Minute
defaultSlowSQLThreshold = 200 * time.Millisecond
)

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}
}

View File

@ -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
}
// 組成 DSNData 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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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 '資產代碼 BTCETHGEM_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 '資產代碼 BTCETHGEM_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 '資產代碼 BTCETHGEM_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)
}