Compare commits
7 Commits
main
...
feat/walle
Author | SHA1 | Date |
---|---|---|
|
47fb3139c2 | |
|
4f6262d489 | |
|
45fab245d9 | |
|
5214ea2283 | |
|
59064b0a69 | |
|
4cb6faefdc | |
|
2f5b04de29 |
|
@ -0,0 +1 @@
|
||||||
|
DROP TABLE IF EXISTS `wallet`;
|
|
@ -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='錢包';
|
|
@ -0,0 +1 @@
|
||||||
|
DROP TABLE IF EXISTS `transaction`;
|
|
@ -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='交易紀錄表';
|
|
@ -0,0 +1 @@
|
||||||
|
DROP TABLE IF EXISTS `transaction`;
|
|
@ -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 = '錢包資金異動紀錄(每一次交易行為的快照記錄)';
|
|
@ -0,0 +1 @@
|
||||||
|
DROP DATABASE IF EXISTS `digimon_wallet`;
|
|
@ -0,0 +1 @@
|
||||||
|
CREATE DATABASE IF NOT EXISTS `digimon_wallet`;
|
56
go.mod
56
go.mod
|
@ -3,73 +3,117 @@ module code.30cm.net/digimon/app-cloudep-wallet-service
|
||||||
go 1.24.2
|
go 1.24.2
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
code.30cm.net/digimon/library-go/errs v1.2.14
|
||||||
|
github.com/google/uuid v1.6.0
|
||||||
|
github.com/shopspring/decimal v1.4.0
|
||||||
|
github.com/stretchr/testify v1.10.0
|
||||||
|
github.com/testcontainers/testcontainers-go v0.36.0
|
||||||
github.com/zeromicro/go-zero v1.8.2
|
github.com/zeromicro/go-zero v1.8.2
|
||||||
google.golang.org/grpc v1.71.1
|
google.golang.org/grpc v1.71.1
|
||||||
google.golang.org/protobuf v1.36.6
|
google.golang.org/protobuf v1.36.6
|
||||||
|
gorm.io/driver/mysql v1.5.7
|
||||||
|
gorm.io/gorm v1.25.12
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
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/beorn7/perks v1.0.1 // indirect
|
||||||
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
|
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
|
||||||
github.com/cespare/xxhash/v2 v2.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-semver v0.3.1 // indirect
|
||||||
github.com/coreos/go-systemd/v22 v22.5.0 // 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/davecgh/go-spew v1.1.1 // indirect
|
||||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // 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/emicklei/go-restful/v3 v3.11.0 // indirect
|
||||||
github.com/fatih/color v1.18.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/logr v1.4.2 // indirect
|
||||||
github.com/go-logr/stdr v1.2.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/jsonpointer v0.19.6 // indirect
|
||||||
github.com/go-openapi/jsonreference v0.20.2 // indirect
|
github.com/go-openapi/jsonreference v0.20.2 // indirect
|
||||||
github.com/go-openapi/swag v0.22.4 // 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/gogo/protobuf v1.3.2 // indirect
|
||||||
github.com/golang/mock v1.6.0 // indirect
|
github.com/golang/mock v1.6.0 // indirect
|
||||||
github.com/golang/protobuf v1.5.4 // indirect
|
github.com/golang/protobuf v1.5.4 // indirect
|
||||||
github.com/google/gnostic-models v0.6.8 // 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/gofuzz v1.2.0 // indirect
|
||||||
github.com/google/uuid v1.6.0 // indirect
|
|
||||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 // indirect
|
github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 // indirect
|
||||||
|
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||||
|
github.com/jinzhu/now v1.1.5 // indirect
|
||||||
github.com/josharian/intern v1.0.0 // indirect
|
github.com/josharian/intern v1.0.0 // indirect
|
||||||
github.com/json-iterator/go v1.1.12 // indirect
|
github.com/json-iterator/go v1.1.12 // indirect
|
||||||
github.com/klauspost/compress v1.17.11 // 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/mailru/easyjson v0.7.7 // indirect
|
||||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.20 // 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/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||||
github.com/modern-go/reflect2 v1.0.2 // 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/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/openzipkin/zipkin-go v0.4.3 // indirect
|
||||||
github.com/pelletier/go-toml/v2 v2.2.2 // 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_golang v1.21.1 // indirect
|
||||||
github.com/prometheus/client_model v0.6.1 // indirect
|
github.com/prometheus/client_model v0.6.1 // indirect
|
||||||
github.com/prometheus/common v0.62.0 // indirect
|
github.com/prometheus/common v0.62.0 // indirect
|
||||||
github.com/prometheus/procfs v0.15.1 // indirect
|
github.com/prometheus/procfs v0.15.1 // indirect
|
||||||
github.com/redis/go-redis/v9 v9.7.3 // 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/spaolacci/murmur3 v1.1.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/api/v3 v3.5.15 // indirect
|
||||||
go.etcd.io/etcd/client/pkg/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.etcd.io/etcd/client/v3 v3.5.15 // indirect
|
||||||
go.opentelemetry.io/auto/sdk v1.1.0 // 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/jaeger v1.17.0 // indirect
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.24.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/otlptracegrpc v1.24.0 // indirect
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp 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/stdout/stdouttrace v1.24.0 // indirect
|
||||||
go.opentelemetry.io/otel/exporters/zipkin 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/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.opentelemetry.io/proto/otlp v1.3.1 // indirect
|
||||||
go.uber.org/atomic v1.10.0 // indirect
|
go.uber.org/atomic v1.10.0 // indirect
|
||||||
go.uber.org/automaxprocs v1.6.0 // indirect
|
go.uber.org/automaxprocs v1.6.0 // indirect
|
||||||
go.uber.org/multierr v1.9.0 // indirect
|
go.uber.org/multierr v1.9.0 // indirect
|
||||||
go.uber.org/zap v1.24.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/net v0.35.0 // indirect
|
||||||
golang.org/x/oauth2 v0.25.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/term v0.29.0 // indirect
|
||||||
golang.org/x/text v0.22.0 // indirect
|
golang.org/x/text v0.22.0 // indirect
|
||||||
golang.org/x/time v0.10.0 // indirect
|
golang.org/x/time v0.10.0 // indirect
|
||||||
|
|
118
go.sum
118
go.sum
|
@ -1,3 +1,15 @@
|
||||||
|
code.30cm.net/digimon/library-go/errs v1.2.14 h1:Un9wcIIjjJW8D2i0ISf8ibzp9oNT4OqLsaSKW0T4RJU=
|
||||||
|
code.30cm.net/digimon/library-go/errs v1.2.14/go.mod h1:Hs4v7SbXNggDVBGXSYsFMjkii1qLF+rugrIpWePN4/o=
|
||||||
|
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 h1:uvdUDbHQHO85qeSydJtItA4T55Pw6BtAejd0APRJOCE=
|
||||||
github.com/alicebob/gopher-json v0.0.0-20230218143504-906a9b012302/go.mod h1:SGnFV6hVsYE877CKEZ6tDNTjaSXYUk6QqoIK6PrAtcc=
|
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=
|
github.com/alicebob/miniredis/v2 v2.34.0 h1:mBFWMaJSNL9RwdGRyEDoAAv8OQc5UlEhLDQggTglU/0=
|
||||||
|
@ -14,26 +26,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/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 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
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 h1:yi21YpKnrx1gt5R+la8n5WgS0kCrsPp33dmEyHReZr4=
|
||||||
github.com/coreos/go-semver v0.3.1/go.mod h1:irMmmIw/7yzSRPWryHsK7EYSg09caPQL03VsM8rvUec=
|
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 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs=
|
||||||
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
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.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.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 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
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 h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
|
||||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
|
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 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g=
|
||||||
github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
|
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 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
|
||||||
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
|
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.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.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 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
|
||||||
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
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 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
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 h1:eCs3fxoIi3Wh6vtgmLTOjdhSpiqphQ+DaPn38N2ZdrE=
|
||||||
github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs=
|
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=
|
github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE=
|
||||||
|
@ -41,6 +75,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.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 h1:QLMzNJnMGPRNDCbySlcj1x01tzU8/9LTTL9hZZZogBU=
|
||||||
github.com/go-openapi/swag v0.22.4/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14=
|
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 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI=
|
||||||
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls=
|
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=
|
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||||
|
@ -52,9 +89,10 @@ github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek
|
||||||
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
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 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I=
|
||||||
github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U=
|
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.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
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.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 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
|
||||||
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||||
|
@ -66,6 +104,10 @@ github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 h1:bkypFPDjIYGfCYD5mRBvpqxfYX1
|
||||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0/go.mod h1:P+Lt/0by1T8bfcF3z737NnSbmxQAppXMRziHUxPOC8k=
|
github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0/go.mod h1:P+Lt/0by1T8bfcF3z737NnSbmxQAppXMRziHUxPOC8k=
|
||||||
github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 h1:2VTzZjLZBgl62/EtslCrtky5vbi9dd7HrQPQIx6wqiw=
|
github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 h1:2VTzZjLZBgl62/EtslCrtky5vbi9dd7HrQPQIx6wqiw=
|
||||||
github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542/go.mod h1:Ow0tF8D4Kplbc8s8sSb3V2oUCygFHVp8gC3Dn6U4MNI=
|
github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542/go.mod h1:Ow0tF8D4Kplbc8s8sSb3V2oUCygFHVp8gC3Dn6U4MNI=
|
||||||
|
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
|
||||||
|
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
||||||
|
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
|
||||||
|
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||||
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
|
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
|
||||||
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
||||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||||
|
@ -83,6 +125,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/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 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
|
||||||
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
|
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 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
|
||||||
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
|
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
|
||||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||||
|
@ -90,17 +136,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.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 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
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-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 h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
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 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
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 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
|
||||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
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 h1:0jY9lJquiL8fcf3M4LAXN5aMlS/b2BV86HFFPCPMgE4=
|
||||||
github.com/onsi/ginkgo/v2 v2.13.0/go.mod h1:TE309ZR8s5FsKKpuB1YAQYBzCaAfUgatB/xlT/ETL/o=
|
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 h1:KIA/t2t5UBzoirT4H9tsML45GEbo3ouUnBHsCfD2tVg=
|
||||||
github.com/onsi/gomega v1.29.0/go.mod h1:9sxs+SwGrKI0+PWe4Fxa9tFQQBG5xSsSbMXOI8PPpoQ=
|
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 h1:9EGwpqkgnwdEIJ+Od7QVSEIH+ocmm5nPat0G7sjsSdg=
|
||||||
github.com/openzipkin/zipkin-go v0.4.3/go.mod h1:M9wCJZFWCo2RiY+o1eBCEMe0Dp2S5LDHcMZmk3RmK7c=
|
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=
|
github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM=
|
||||||
|
@ -109,6 +173,8 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
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 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
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 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g=
|
||||||
github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U=
|
github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U=
|
||||||
github.com/prometheus/client_golang v1.21.1 h1:DOvXXTqVzvkIewV/CDPFdejpMCGeMcbGCQ8YOmu+Ibk=
|
github.com/prometheus/client_golang v1.21.1 h1:DOvXXTqVzvkIewV/CDPFdejpMCGeMcbGCQ8YOmu+Ibk=
|
||||||
|
@ -123,6 +189,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/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 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
|
||||||
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
|
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 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI=
|
||||||
github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
|
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=
|
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
||||||
|
@ -133,6 +205,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 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
|
||||||
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
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.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.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.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||||
|
@ -140,11 +213,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.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 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
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.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||||
github.com/yuin/goldmark v1.2.1/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/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 h1:kYKnWBjvbNP4XLT3+bPEwAXJx262OhaHDWDVOPjL46M=
|
||||||
github.com/yuin/gopher-lua v1.1.1/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw=
|
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 h1:AbJckBoojbr1lqCN1dkvURTIHOau7yvKReEd7ZmjuCk=
|
||||||
github.com/zeromicro/go-zero v1.8.2/go.mod h1:G5dF+jzCEuq0t1j8qdrtVAy30QMgctGcKSfqFIGsvSg=
|
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=
|
go.etcd.io/etcd/api/v3 v3.5.15 h1:3KpLJir1ZEBrYuV2v+Twaa/e2MdDCEZ/70H+lzEiwsk=
|
||||||
|
@ -155,8 +236,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.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 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
|
||||||
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
|
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/contrib/instrumentation/net/http/otelhttp v0.49.0 h1:jq9TW8u3so/bN+JPT166wjOI6/vQPF6Xe7nMNIltagk=
|
||||||
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/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 h1:D7UpUy2Xc2wsi1Ras6V40q806WM07rqoCWzXu7Sqy+4=
|
||||||
go.opentelemetry.io/otel/exporters/jaeger v1.17.0/go.mod h1:nPCqOnEH9rNLKqH/+rrUjiMzHJdV1BlpKcTwRTyKkKI=
|
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=
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.24.0 h1:t6wl9SPayj+c7lEIFgm4ooDBZVb01IhLB4InpomhRw8=
|
||||||
|
@ -169,14 +252,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/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 h1:3evrL5poBuh1KF51D9gO/S+N/1msnm4DaBqs/rpXUqY=
|
||||||
go.opentelemetry.io/otel/exporters/zipkin v1.24.0/go.mod h1:0EHgD8R0+8yRhUYJOGR8Hfg2dpiJQxDOszd5smVO9wM=
|
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.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M=
|
||||||
go.opentelemetry.io/otel/metric v1.34.0/go.mod h1:CEDrp0fy2D0MvkXE+dPV7cMi8tWZwX3dmaIhwPOaqHE=
|
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 h1:95zS4k/2GOy069d321O8jWgYsW3MzVV+KuSPKp7Wr1A=
|
||||||
go.opentelemetry.io/otel/sdk v1.34.0/go.mod h1:0e/pNiaMAqaykJGKbi+tSjWfNNHMTxoC9qANsCzbyxU=
|
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 h1:5CeK9ujjbFVL5c1PhLuStg1wxA7vQv7ce1EK0Gyvahk=
|
||||||
go.opentelemetry.io/otel/sdk/metric v1.34.0/go.mod h1:jQ/r8Ze28zRKoNRdkjCZxfs6YvBTG1+YIqyFVFYec5w=
|
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.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs=
|
||||||
go.opentelemetry.io/otel/trace v1.34.0/go.mod h1:Svm7lSjQD7kG7KJ/MUHPVXSDGz2OX4h0M2jHBhmSfRE=
|
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 h1:TrMUixzpM0yuc/znrFTP9MMRh8trP93mkCiDVeXrui0=
|
||||||
go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8=
|
go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8=
|
||||||
go.uber.org/atomic v1.10.0 h1:9qC72Qh0+3MqyJbAn8YU5xVq1frD8bn3JtD2oXtafVQ=
|
go.uber.org/atomic v1.10.0 h1:9qC72Qh0+3MqyJbAn8YU5xVq1frD8bn3JtD2oXtafVQ=
|
||||||
|
@ -192,6 +275,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-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-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
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.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||||
golang.org/x/mod v0.3.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=
|
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||||
|
@ -210,14 +295,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/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-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-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-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-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-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-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.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.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
|
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
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.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 h1:L6pJp37ocefwRRtYPKSWOWzOtWSxVajvz2ldH/xi3iU=
|
||||||
golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s=
|
golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s=
|
||||||
|
@ -259,6 +350,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.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 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
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 h1:2ORfZ7+bGC3YJqGpV0KSDDEVf8hdGQ6A03/50vj8pmw=
|
||||||
k8s.io/api v0.29.3/go.mod h1:y2yg2NTyHUUkIoTC+phinTnEa3KFM6RZ3szxt014a80=
|
k8s.io/api v0.29.3/go.mod h1:y2yg2NTyHUUkIoTC+phinTnEa3KFM6RZ3szxt014a80=
|
||||||
k8s.io/apimachinery v0.29.4 h1:RaFdJiDmuKs/8cm1M6Dh1Kvyh59YQFDcFuFTSmXes6Q=
|
k8s.io/apimachinery v0.29.4 h1:RaFdJiDmuKs/8cm1M6Dh1Kvyh59YQFDcFuFTSmXes6Q=
|
||||||
|
|
|
@ -1,7 +1,22 @@
|
||||||
package config
|
package config
|
||||||
|
|
||||||
import "github.com/zeromicro/go-zero/zrpc"
|
import (
|
||||||
|
"github.com/zeromicro/go-zero/zrpc"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
zrpc.RpcServerConf
|
zrpc.RpcServerConf
|
||||||
|
|
||||||
|
MySQL struct {
|
||||||
|
UserName string
|
||||||
|
Password string
|
||||||
|
Host string
|
||||||
|
Port string
|
||||||
|
Database string
|
||||||
|
MaxIdleConns int
|
||||||
|
MaxOpenConns int
|
||||||
|
ConnMaxLifetime time.Duration
|
||||||
|
LogLevel string
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
package domain
|
|
@ -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(如為轉帳場景)
|
||||||
|
TxType wallet.TxType `gorm:"column:type"` // 交易類型(如轉帳、入金、出金等,自定義列舉)
|
||||||
|
BusinessType int8 `gorm:"column:business_type"` // 業務類型(如合約、模擬、一般用途等,數字代碼)
|
||||||
|
Asset string `gorm:"column:asset"` // 幣種(如 BTC、ETH、USD、TWD 等)
|
||||||
|
Amount decimal.Decimal `gorm:"column:amount"` // 本次變動金額(正數為增加,負數為扣減)
|
||||||
|
PostTransferBalance decimal.Decimal `gorm:"column:post_transfer_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"
|
||||||
|
}
|
|
@ -0,0 +1,34 @@
|
||||||
|
package entity
|
||||||
|
|
||||||
|
import (
|
||||||
|
"code.30cm.net/digimon/app-cloudep-wallet-service/pkg/domain/wallet"
|
||||||
|
"github.com/shopspring/decimal"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 🔐 重點設計理念:
|
||||||
|
// 避免競爭與資金衝突
|
||||||
|
// 將錢包用途拆分成不同 type,避免一個錢包同時處理可用資金與凍結資金,降低邏輯錯誤風險。
|
||||||
|
//
|
||||||
|
// 資金安全性與追蹤性強
|
||||||
|
// 分別記錄「凍結」「未確認」「可用」等不同狀態,方便核對資產總額與可提資金。
|
||||||
|
//
|
||||||
|
// 合約與模擬隔離
|
||||||
|
// 保留合約交易與模擬交易專屬錢包,實現清楚的邊界控制與風險隔離。
|
||||||
|
//
|
||||||
|
// 多品牌支援
|
||||||
|
// 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"` // 錢包類型
|
||||||
|
CreateAt int64 `gorm:"column:create_at"` // 建立時間(UnixNano timestamp)
|
||||||
|
UpdateAt int64 `gorm:"column:update_at"` // 更新時間(UnixNano timestamp)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Wallet) TableName() string {
|
||||||
|
return "wallet" // 對應的資料表名稱
|
||||||
|
}
|
|
@ -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"
|
||||||
|
}
|
|
@ -0,0 +1 @@
|
||||||
|
package domain
|
|
@ -0,0 +1,6 @@
|
||||||
|
package repository
|
||||||
|
|
||||||
|
import "errors"
|
||||||
|
|
||||||
|
var ErrRecordNotFound = errors.New("query record not found")
|
||||||
|
var ErrBalanceInsufficient = errors.New("balance insufficient") // 餘額不足
|
|
@ -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
|
||||||
|
}
|
|
@ -0,0 +1,89 @@
|
||||||
|
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"
|
||||||
|
"github.com/shopspring/decimal"
|
||||||
|
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Wallet struct {
|
||||||
|
Brand string // 品牌/平台(區分多租戶、多品牌情境)
|
||||||
|
UID string // 使用者 UID
|
||||||
|
Asset string // 資產代號,可為 BTC、ETH、GEM_RED、GEM_BLUE、POINTS ,USD, TWD 等....
|
||||||
|
Balance decimal.Decimal // 餘額(使用高精度 decimal 避免浮點誤差)
|
||||||
|
Type wallet.Types // 錢包類型
|
||||||
|
}
|
||||||
|
|
||||||
|
// WalletRepository 是錢包的總入口,負責查詢、初始化與跨帳戶查詢邏輯
|
||||||
|
type WalletRepository interface {
|
||||||
|
// NewDB 建立新的 DB 實例(提供給需要操作 tx 的場景)
|
||||||
|
NewDB() *gorm.DB
|
||||||
|
// Session 取得單一使用者資產的錢包服務(非交易模式)
|
||||||
|
//📌 使用場景:
|
||||||
|
// 用在 不需要交易機制的場景,例如:
|
||||||
|
// 純查詢錢包餘額,查詢快取、Log、統計報表,非敏感資料更新,失敗可以重試的情境
|
||||||
|
// ✅ 優點:
|
||||||
|
// 簡單快速、使用預設的資料庫連線
|
||||||
|
// 不用包 Transaction ,沒有 Rollback 負擔
|
||||||
|
Session(uid, asset string) UserWalletService
|
||||||
|
// SessionWithTx 在資料庫交易內取得錢包服務
|
||||||
|
// 📌 使用場景:
|
||||||
|
// 用在 資料需要一致性與原子性保證的邏輯中,例如:
|
||||||
|
// 加值與扣款(同時操作多個錢包)檢查餘額後立刻寫入交易記錄,綁定訂單與錢包扣款的行為
|
||||||
|
// 所有與 Add/Commit 有關的處理,與其他模組(訂單、KYC)共用一個 transaction
|
||||||
|
// ✅ 優點:
|
||||||
|
// 保障操作過程中不被其他並發操作影響
|
||||||
|
// 可控制 rollback 行為避免中間失敗導致不一致
|
||||||
|
// 可組合複雜操作(如:更新錢包同時寫入交易紀錄)
|
||||||
|
SessionWithTx(db *gorm.DB, uid, asset string) UserWalletService
|
||||||
|
// Transaction 資料庫交易包裝器(確保交易一致性)
|
||||||
|
Transaction(fn func(db *gorm.DB) error) error
|
||||||
|
// InitWallets 初始化使用者的所有錢包類型(如可用、凍結等)
|
||||||
|
InitWallets(ctx context.Context, param []Wallet) error
|
||||||
|
// QueryBalances 查詢特定資產的錢包餘額
|
||||||
|
QueryBalances(ctx context.Context, req BalanceQuery) ([]entity.Wallet, error)
|
||||||
|
// QueryBalancesByUIDs 查詢多個使用者在特定資產下的錢包餘額
|
||||||
|
QueryBalancesByUIDs(ctx context.Context, uids []string, req BalanceQuery) ([]entity.Wallet, error)
|
||||||
|
//// GetDailyTxAmount 查詢使用者今日交易總金額(指定類型與業務)
|
||||||
|
//GetDailyTxAmount(ctx context.Context, uid string, txTypes []domain.TxType, business wallet.BusinessName) ([]entity.Wallet, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// BalanceQuery 是查詢餘額時的篩選條件
|
||||||
|
type BalanceQuery struct {
|
||||||
|
UID string // 使用者 ID
|
||||||
|
Asset string // 資產類型(Crypto、寶石等)
|
||||||
|
Kinds []wallet.Types // 錢包類型(如可用、凍結等)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UserWalletService 定義了一個「單一使用者、單一資產」的錢包操作合約
|
||||||
|
type UserWalletService interface {
|
||||||
|
// InitializeWallets 為新使用者初始化所有錢包類型並寫入資料庫
|
||||||
|
InitializeWallets(ctx context.Context, brand string) ([]entity.Wallet, error)
|
||||||
|
// GetAllBalances 查詢此使用者此資產下所有錢包類型的當前餘額
|
||||||
|
GetAllBalances(ctx context.Context) ([]entity.Wallet, error)
|
||||||
|
// GetBalancesForTypes 查詢指定錢包類型的一組餘額(不加鎖)
|
||||||
|
GetBalancesForTypes(ctx context.Context, kinds []wallet.Types) ([]entity.Wallet, error)
|
||||||
|
// GetBalancesForUpdate 查詢並鎖定指定錢包類型(FOR UPDATE)
|
||||||
|
GetBalancesForUpdate(ctx context.Context, kinds []wallet.Types) ([]entity.Wallet, error)
|
||||||
|
// CurrentBalance 從本地緩存取得某種錢包類型的餘額
|
||||||
|
CurrentBalance(kind wallet.Types) decimal.Decimal
|
||||||
|
// IncreaseBalance 增加指定錢包類型的餘額,並累積一筆交易紀錄
|
||||||
|
IncreaseBalance(kind wallet.Types, orderID string, amount decimal.Decimal) error
|
||||||
|
// DecreaseBalance 減少指定錢包類型的餘額(等同於 IncreaseBalance 的負數版本)
|
||||||
|
DecreaseBalance(kind wallet.Types, orderID string, amount decimal.Decimal) error
|
||||||
|
// PrepareTransactions 為所有暫存交易紀錄填上 TXID/OrderID/Brand/BusinessType,並回傳可落庫的切片
|
||||||
|
PrepareTransactions(
|
||||||
|
txID int64,
|
||||||
|
orderID, brand string,
|
||||||
|
businessType wallet.BusinessName,
|
||||||
|
) []entity.WalletTransaction
|
||||||
|
// PersistBalances 將本地緩存中所有錢包最終餘額批次寫入資料庫
|
||||||
|
PersistBalances(ctx context.Context) error
|
||||||
|
// PersistOrderBalances 將本地緩存中所有訂單相關餘額批次寫入 transaction 表
|
||||||
|
PersistOrderBalances(ctx context.Context) error
|
||||||
|
// HasAvailableBalance 確認此使用者此資產是否已有可用餘額錢包
|
||||||
|
HasAvailableBalance(ctx context.Context) (bool, error)
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -0,0 +1 @@
|
||||||
|
package usecase
|
|
@ -0,0 +1,91 @@
|
||||||
|
package usecase
|
||||||
|
|
||||||
|
import (
|
||||||
|
"code.30cm.net/digimon/app-cloudep-wallet-service/pkg/domain/wallet"
|
||||||
|
"context"
|
||||||
|
"github.com/shopspring/decimal"
|
||||||
|
)
|
||||||
|
|
||||||
|
// WalletTransferUseCase 處理錢包資產轉移的核心業務邏輯(支援多種錢包操作)
|
||||||
|
type WalletTransferUseCase interface {
|
||||||
|
// Process 處理一次完整的錢包轉帳(含內部轉帳、跨使用者轉帳)
|
||||||
|
Process(ctx context.Context, req WalletTransferRequest) error
|
||||||
|
// Withdraw 出金操作(從錢包扣款)
|
||||||
|
Withdraw(ctx context.Context, tx WalletTransferRequest) error
|
||||||
|
// Deposit 入金操作(錢包加值)
|
||||||
|
Deposit(ctx context.Context, tx WalletTransferRequest) error
|
||||||
|
// DepositUnconfirmed 入金至未確認餘額(如轉帳待確認、預約加值)
|
||||||
|
DepositUnconfirmed(ctx context.Context, tx WalletTransferRequest) error
|
||||||
|
// Freeze 將資產從可用餘額移動至凍結餘額(常用於下單等鎖倉需求)
|
||||||
|
Freeze(ctx context.Context, tx WalletTransferRequest) error
|
||||||
|
// AppendFreeze 追加凍結金額(已有凍結金額的情況下再次追加)
|
||||||
|
AppendFreeze(ctx context.Context, tx WalletTransferRequest) error
|
||||||
|
// UnFreeze 將凍結金額移回可用餘額(如訂單取消)
|
||||||
|
UnFreeze(ctx context.Context, tx WalletTransferRequest) error
|
||||||
|
// RollbackFreeze 將凍結金額退回,並作為取消交易的一部分
|
||||||
|
RollbackFreeze(ctx context.Context, tx WalletTransferRequest) error
|
||||||
|
// RollbackFreezeAddAvailable 退回凍結金額並補回可用金額(全額退款流程)
|
||||||
|
RollbackFreezeAddAvailable(ctx context.Context, tx WalletTransferRequest) error
|
||||||
|
// CancelFreeze 取消凍結操作(不轉移回可用餘額,只做作廢處理)
|
||||||
|
CancelFreeze(ctx context.Context, tx WalletTransferRequest) error
|
||||||
|
// Unconfirmed 將未確認金額移回可用餘額(例如:加值完成確認)
|
||||||
|
Unconfirmed(ctx context.Context, tx WalletTransferRequest) error
|
||||||
|
// Balance 查詢目前錢包餘額(含可用與不可用)
|
||||||
|
Balance(ctx context.Context, req BalanceReq) ([]Balance, error)
|
||||||
|
// HistoryBalance 查詢歷史錢包快照(指定時間前的餘額狀態)
|
||||||
|
HistoryBalance(ctx context.Context, req BalanceReq) ([]Balance, error)
|
||||||
|
// BalanceByAssets 查詢使用者特定資產的各錢包類型餘額
|
||||||
|
BalanceByAssets(ctx context.Context, uid, cryptoCode string, walletTypes []wallet.Types) (BalanceAssetsResp, error)
|
||||||
|
// CheckBalance 驗證可用餘額是否足夠(通常用於風控前置驗證)
|
||||||
|
CheckBalance(ctx context.Context, tx WalletTransferRequest) error
|
||||||
|
// GetTodayWithdraw 查詢使用者今日累積出金額
|
||||||
|
GetTodayWithdraw(ctx context.Context, uid, toCrypto string) (TodayWithdrawResp, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// WalletTransferRequest 表示一次錢包轉帳的請求資料
|
||||||
|
type WalletTransferRequest struct {
|
||||||
|
ReferenceOrderID string // 對應的訂單編號,可為空(非訂單觸發)
|
||||||
|
FromUID string // 付款方 UID
|
||||||
|
ToUID string // 收款方 UID,若為錢包內部轉帳可與 FromUID 相同
|
||||||
|
Asset string // 資產代號(例如 BTC、ETH、TWD 等)
|
||||||
|
Amount decimal.Decimal // 轉移金額(正數,系統控制正負方向)
|
||||||
|
PostTransferBalance decimal.Decimal // 轉帳後餘額(可選填,便於日誌追蹤與審計)
|
||||||
|
TxType wallet.TxType // 交易類型(如入金、出金、轉帳等)
|
||||||
|
Business wallet.BusinessName // 業務場景類型(如合約、模擬、一般用途等)
|
||||||
|
Brand string // 所屬品牌(支援多品牌架構)
|
||||||
|
FromWalletType wallet.Types // 扣款來源錢包類型
|
||||||
|
ToWalletType wallet.Types // 收款目標錢包類型
|
||||||
|
}
|
||||||
|
|
||||||
|
// BalanceReq 表示查詢錢包餘額的請求參數
|
||||||
|
type BalanceReq struct {
|
||||||
|
UID string // 使用者 UID
|
||||||
|
Asset string // 指定資產(如 BTC)
|
||||||
|
BeforeHour int // 幾小時前的快照(若查歷史快照)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Balance 表示錢包的當前或歷史餘額狀態
|
||||||
|
type Balance struct {
|
||||||
|
Asset string // 資產代號
|
||||||
|
Available decimal.Decimal // 可用餘額
|
||||||
|
Unavailable UnavailableBalance // 不可用餘額(凍結、未確認)
|
||||||
|
UpdateTime int64 // 更新時間(Unix 時間戳)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnavailableBalance 表示錢包中不可用的部分
|
||||||
|
type UnavailableBalance struct {
|
||||||
|
Freeze decimal.Decimal // 凍結金額(如掛單中)
|
||||||
|
Unconfirmed decimal.Decimal // 未確認金額(如等待轉帳)
|
||||||
|
}
|
||||||
|
|
||||||
|
// BalanceAssetsResp 表示使用者所有錢包類型在某資產的總和
|
||||||
|
type BalanceAssetsResp struct {
|
||||||
|
Balances map[string]decimal.Decimal // 各錢包類型對應的餘額
|
||||||
|
Asset string // 查詢的資產代號
|
||||||
|
}
|
||||||
|
|
||||||
|
// TodayWithdrawResp 表示使用者今天的累積出金結果
|
||||||
|
type TodayWithdrawResp struct {
|
||||||
|
Withdraw decimal.Decimal // 今日總出金金額
|
||||||
|
Asset string // 資產代號
|
||||||
|
}
|
|
@ -0,0 +1,7 @@
|
||||||
|
package wallet
|
||||||
|
|
||||||
|
type BusinessName string
|
||||||
|
|
||||||
|
func (b BusinessName) ToInt8() int8 {
|
||||||
|
return int8(0)
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
|
@ -0,0 +1,17 @@
|
||||||
|
package wallet
|
||||||
|
|
||||||
|
type Types int8
|
||||||
|
|
||||||
|
const (
|
||||||
|
TypeAvailable Types = iota + 1 // 可動用金額(使用者可以自由花用的餘額)
|
||||||
|
TypeFreeze // 被凍結金額(交易進行中或風控鎖住的金額)
|
||||||
|
TypeUnconfirmed // 未確認金額(交易已送出但區塊鏈尚未確認)
|
||||||
|
// 以下為進階用途:合約或模擬交易錢包
|
||||||
|
|
||||||
|
TypeContractAvailable // 合約系統的可用金額
|
||||||
|
TypeContractFreeze // 合約中被凍結的金額
|
||||||
|
TypeSimulationAvailable // 模擬交易可用金額(例如沙盒環境)
|
||||||
|
TypeSimulationFreeze // 模擬交易凍結金額
|
||||||
|
)
|
||||||
|
|
||||||
|
var AllTypes = []Types{TypeAvailable, TypeFreeze, TypeUnconfirmed}
|
|
@ -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
|
||||||
|
}
|
|
@ -0,0 +1,10 @@
|
||||||
|
package sql_client
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
const (
|
||||||
|
defaultMaxOpenConns = 25
|
||||||
|
defaultMaxIdleConns = 25
|
||||||
|
defaultMaxLifeTime = 5 * time.Minute
|
||||||
|
defaultSlowSQLThreshold = 200 * time.Millisecond
|
||||||
|
)
|
|
@ -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
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -0,0 +1,219 @@
|
||||||
|
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"
|
||||||
|
)
|
||||||
|
|
||||||
|
// WalletService 代表一個使用者在某資產上的錢包服務,
|
||||||
|
// 負責讀取/寫入資料庫並在記憶體暫存變動
|
||||||
|
type WalletService struct {
|
||||||
|
db *gorm.DB
|
||||||
|
uid string // 使用者識別碼
|
||||||
|
asset string // 資產代號 (如 BTC、ETH、TWD)
|
||||||
|
localBalances map[wallet.Types]entity.Wallet // 暫存各類型錢包當前餘額
|
||||||
|
localOrderBalances map[int64]decimal.Decimal // 暫存各訂單變動後的餘額
|
||||||
|
transactions []entity.WalletTransaction // 暫存所有尚未落庫的錢包交易紀錄
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewWalletService 建立一個 WalletService 實例
|
||||||
|
func NewWalletService(db *gorm.DB, uid, asset string) repository.UserWalletService {
|
||||||
|
return &WalletService{
|
||||||
|
db: db,
|
||||||
|
uid: uid,
|
||||||
|
asset: asset,
|
||||||
|
localBalances: make(map[wallet.Types]entity.Wallet, len(wallet.AllTypes)),
|
||||||
|
localOrderBalances: make(map[int64]decimal.Decimal, len(wallet.AllTypes)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// InitializeWallets 啟動時為新使用者初始化所有類型錢包,並寫入資料庫
|
||||||
|
func (s *WalletService) InitializeWallets(ctx context.Context, brand string) ([]entity.Wallet, error) {
|
||||||
|
var wallets []entity.Wallet
|
||||||
|
for _, t := range wallet.AllTypes {
|
||||||
|
wallets = append(wallets, entity.Wallet{
|
||||||
|
Brand: brand,
|
||||||
|
UID: s.uid,
|
||||||
|
Asset: s.asset,
|
||||||
|
Balance: decimal.Zero,
|
||||||
|
Type: t,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if err := s.db.WithContext(ctx).Create(&wallets).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
// 將初始化後的錢包資料寫入本地緩存
|
||||||
|
for _, w := range wallets {
|
||||||
|
s.localBalances[w.Type] = w
|
||||||
|
}
|
||||||
|
return wallets, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAllBalances 查詢該使用者某資產所有錢包類型當前餘額
|
||||||
|
func (s *WalletService) GetAllBalances(ctx context.Context) ([]entity.Wallet, error) {
|
||||||
|
var result []entity.Wallet
|
||||||
|
err := s.db.WithContext(ctx).
|
||||||
|
Where("uid = ? AND asset = ?", s.uid, s.asset).
|
||||||
|
Select("id, asset, balance, type").
|
||||||
|
Find(&result).Error
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
for _, w := range result {
|
||||||
|
s.localBalances[w.Type] = w
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetBalancesForTypes 查詢指定類型的錢包餘額,不上鎖
|
||||||
|
func (s *WalletService) GetBalancesForTypes(ctx context.Context, kinds []wallet.Types) ([]entity.Wallet, error) {
|
||||||
|
var result []entity.Wallet
|
||||||
|
err := s.db.WithContext(ctx).
|
||||||
|
Where("uid = ? AND asset = ?", s.uid, s.asset).
|
||||||
|
Where("type IN ?", kinds).
|
||||||
|
Select("id, asset, balance, type").
|
||||||
|
Find(&result).Error
|
||||||
|
if err != nil {
|
||||||
|
return nil, translateNotFound(err)
|
||||||
|
}
|
||||||
|
for _, w := range result {
|
||||||
|
s.localBalances[w.Type] = w
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetBalancesForUpdate 查詢並鎖定指定類型的錢包 (FOR UPDATE)
|
||||||
|
func (s *WalletService) GetBalancesForUpdate(ctx context.Context, kinds []wallet.Types) ([]entity.Wallet, error) {
|
||||||
|
var result []entity.Wallet
|
||||||
|
err := s.db.WithContext(ctx).
|
||||||
|
Where("uid = ? AND asset = ?", s.uid, s.asset).
|
||||||
|
Where("type IN ?", kinds).
|
||||||
|
Clauses(clause.Locking{Strength: "UPDATE"}).
|
||||||
|
Select("id, asset, balance, type").
|
||||||
|
Find(&result).Error
|
||||||
|
if err != nil {
|
||||||
|
return nil, translateNotFound(err)
|
||||||
|
}
|
||||||
|
for _, w := range result {
|
||||||
|
s.localBalances[w.Type] = w
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CurrentBalance 從緩存中取得某種類型錢包的當前餘額
|
||||||
|
func (s *WalletService) CurrentBalance(kind wallet.Types) decimal.Decimal {
|
||||||
|
if w, ok := s.localBalances[kind]; ok {
|
||||||
|
return w.Balance
|
||||||
|
}
|
||||||
|
return decimal.Zero
|
||||||
|
}
|
||||||
|
|
||||||
|
// IncreaseBalance 在本地緩存新增餘額,並記錄一筆 WalletTransaction
|
||||||
|
func (s *WalletService) IncreaseBalance(kind wallet.Types, orderID string, amount decimal.Decimal) error {
|
||||||
|
w, ok := s.localBalances[kind]
|
||||||
|
if !ok {
|
||||||
|
return repository.ErrRecordNotFound
|
||||||
|
}
|
||||||
|
w.Balance = w.Balance.Add(amount)
|
||||||
|
if w.Balance.LessThan(decimal.Zero) {
|
||||||
|
return repository.ErrBalanceInsufficient
|
||||||
|
}
|
||||||
|
s.transactions = append(s.transactions, entity.WalletTransaction{
|
||||||
|
OrderID: orderID,
|
||||||
|
UID: s.uid,
|
||||||
|
WalletType: kind,
|
||||||
|
Asset: s.asset,
|
||||||
|
Amount: amount,
|
||||||
|
Balance: w.Balance,
|
||||||
|
})
|
||||||
|
s.localBalances[kind] = w
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DecreaseBalance 本質上是 IncreaseBalance 的負數版本
|
||||||
|
func (s *WalletService) DecreaseBalance(kind wallet.Types, orderID string, amount decimal.Decimal) error {
|
||||||
|
return s.IncreaseBalance(kind, orderID, amount.Neg())
|
||||||
|
}
|
||||||
|
|
||||||
|
// PrepareTransactions 為每筆暫存的 WalletTransaction 填入共用欄位 (txID, brand, businessType),
|
||||||
|
// 並回傳完整可落庫的切片
|
||||||
|
func (s *WalletService) PrepareTransactions(
|
||||||
|
txID int64,
|
||||||
|
orderID, brand string,
|
||||||
|
businessType wallet.BusinessName,
|
||||||
|
) []entity.WalletTransaction {
|
||||||
|
for i := range s.transactions {
|
||||||
|
s.transactions[i].TransactionID = txID
|
||||||
|
s.transactions[i].OrderID = orderID
|
||||||
|
s.transactions[i].Brand = brand
|
||||||
|
s.transactions[i].BusinessType = businessType.ToInt8()
|
||||||
|
}
|
||||||
|
return s.transactions
|
||||||
|
}
|
||||||
|
|
||||||
|
// PersistBalances 寫入本地緩存中所有錢包的最終餘額到資料庫
|
||||||
|
func (s *WalletService) PersistBalances(ctx context.Context) error {
|
||||||
|
return s.db.Transaction(func(tx *gorm.DB) error {
|
||||||
|
for _, w := range s.localBalances {
|
||||||
|
if err := tx.WithContext(ctx).
|
||||||
|
Model(&entity.Wallet{}).
|
||||||
|
Where("id = ?", w.ID).
|
||||||
|
UpdateColumns(map[string]interface{}{
|
||||||
|
"balance": w.Balance,
|
||||||
|
"update_at": time.Now().Unix(),
|
||||||
|
}).Error; err != nil {
|
||||||
|
return fmt.Errorf("更新錢包餘額失敗 (id=%d): %w", w.ID, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}, &sql.TxOptions{Isolation: sql.LevelReadCommitted})
|
||||||
|
}
|
||||||
|
|
||||||
|
// PersistOrderBalances 寫入所有訂單錢包的最終餘額到 transaction 表
|
||||||
|
func (s *WalletService) PersistOrderBalances(ctx context.Context) error {
|
||||||
|
return s.db.Transaction(func(tx *gorm.DB) error {
|
||||||
|
for id, bal := range s.localOrderBalances {
|
||||||
|
if err := tx.WithContext(ctx).
|
||||||
|
Model(&entity.Transaction{}).
|
||||||
|
Where("id = ?", id).
|
||||||
|
Update("post_transfer_balance", bal).Error; err != nil {
|
||||||
|
return fmt.Errorf("更新訂單錢包餘額失敗 (id=%d): %w", id, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}, &sql.TxOptions{Isolation: sql.LevelReadCommitted})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *WalletService) HasAvailableBalance(ctx context.Context) (bool, error) {
|
||||||
|
var exists bool
|
||||||
|
err := s.db.WithContext(ctx).
|
||||||
|
Model(&entity.Wallet{}).
|
||||||
|
Select("1").
|
||||||
|
Where("uid = ? AND asset = ?", s.uid, s.asset).
|
||||||
|
Where("type = ?", wallet.TypeAvailable).
|
||||||
|
Limit(1).
|
||||||
|
Scan(&exists).Error
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return exists, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// translateNotFound 將 GORM 的 RecordNotFound 轉為自訂錯誤
|
||||||
|
func translateNotFound(err error) error {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return repository.ErrRecordNotFound
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,113 @@
|
||||||
|
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"
|
||||||
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type WalletRepositoryParam struct {
|
||||||
|
DB *gorm.DB `name:"dbM"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type WalletRepository struct {
|
||||||
|
WalletRepositoryParam
|
||||||
|
}
|
||||||
|
|
||||||
|
func MustCategoryRepository(param WalletRepositoryParam) repository.WalletRepository {
|
||||||
|
return &WalletRepository{
|
||||||
|
param,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (repo *WalletRepository) NewDB() *gorm.DB {
|
||||||
|
return repo.DB.Begin()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Transaction 指 DB 事務,非記錄一筆交易
|
||||||
|
func (repo *WalletRepository) Transaction(fn func(db *gorm.DB) error) error {
|
||||||
|
db := repo.DB.Begin(&sql.TxOptions{
|
||||||
|
Isolation: sql.LevelReadCommitted,
|
||||||
|
ReadOnly: false,
|
||||||
|
})
|
||||||
|
defer db.Rollback()
|
||||||
|
|
||||||
|
if err := fn(db); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := db.Commit().Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (repo *WalletRepository) Session(uid, asset string) repository.UserWalletService {
|
||||||
|
return NewWalletService(repo.DB, uid, asset)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (repo *WalletRepository) SessionWithTx(db *gorm.DB, uid, asset string) repository.UserWalletService {
|
||||||
|
return NewWalletService(db, uid, asset)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (repo *WalletRepository) InitWallets(ctx context.Context, param []repository.Wallet) error {
|
||||||
|
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) {
|
||||||
|
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) {
|
||||||
|
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
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
|
@ -0,0 +1,124 @@
|
||||||
|
package usecase
|
||||||
|
|
||||||
|
import (
|
||||||
|
"code.30cm.net/digimon/app-cloudep-wallet-service/pkg/domain/repository"
|
||||||
|
"code.30cm.net/digimon/app-cloudep-wallet-service/pkg/domain/usecase"
|
||||||
|
"code.30cm.net/digimon/app-cloudep-wallet-service/pkg/domain/wallet"
|
||||||
|
"code.30cm.net/digimon/library-go/errs"
|
||||||
|
"context"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
type WalletUseCaseParam struct {
|
||||||
|
WalletRepo repository.WalletRepository
|
||||||
|
TransactionRepo repository.TransactionRepository
|
||||||
|
WalletTransactionRepo repository.WalletTransactionRepo
|
||||||
|
}
|
||||||
|
|
||||||
|
type WalletUseCase struct {
|
||||||
|
WalletUseCaseParam
|
||||||
|
|
||||||
|
// 內存讀寫所記錄有哪些玩家已經確認有存在錢包過了,減少確認錢包是否存在頻率
|
||||||
|
sync.RWMutex
|
||||||
|
existUIDAsset map[string]struct{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (use *WalletUseCase) Process(ctx context.Context, req usecase.WalletTransferRequest) error {
|
||||||
|
//TODO implement me
|
||||||
|
panic("implement me")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Withdraw 提幣
|
||||||
|
// 1. 新增一筆提幣交易
|
||||||
|
// 2. 錢包減少可用餘額
|
||||||
|
// 3. 錢包變化新增一筆減少可用餘額資料
|
||||||
|
func (use *WalletUseCase) Withdraw(ctx context.Context, tx usecase.WalletTransferRequest) error {
|
||||||
|
if !tx.Amount.IsPositive() {
|
||||||
|
return errs.InvalidRange("failed to get correct amount")
|
||||||
|
}
|
||||||
|
tx.TxType = wallet.Withdraw
|
||||||
|
|
||||||
|
return use.ProcessTransaction(ctx, tx, userWalletFlow{
|
||||||
|
UID: tx.FromUID,
|
||||||
|
Asset: tx.Asset,
|
||||||
|
Actions: []walletActionOption{use.withLockAvailable(), use.withSubAvailable()}, //use.lockAvailable(), use.subAvailable()
|
||||||
|
})
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func (use *WalletUseCase) Deposit(ctx context.Context, tx usecase.WalletTransferRequest) error {
|
||||||
|
//TODO implement me
|
||||||
|
panic("implement me")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (use *WalletUseCase) DepositUnconfirmed(ctx context.Context, tx usecase.WalletTransferRequest) error {
|
||||||
|
//TODO implement me
|
||||||
|
panic("implement me")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (use *WalletUseCase) Freeze(ctx context.Context, tx usecase.WalletTransferRequest) error {
|
||||||
|
//TODO implement me
|
||||||
|
panic("implement me")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (use *WalletUseCase) AppendFreeze(ctx context.Context, tx usecase.WalletTransferRequest) error {
|
||||||
|
//TODO implement me
|
||||||
|
panic("implement me")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (use *WalletUseCase) UnFreeze(ctx context.Context, tx usecase.WalletTransferRequest) error {
|
||||||
|
//TODO implement me
|
||||||
|
panic("implement me")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (use *WalletUseCase) RollbackFreeze(ctx context.Context, tx usecase.WalletTransferRequest) error {
|
||||||
|
//TODO implement me
|
||||||
|
panic("implement me")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (use *WalletUseCase) RollbackFreezeAddAvailable(ctx context.Context, tx usecase.WalletTransferRequest) error {
|
||||||
|
//TODO implement me
|
||||||
|
panic("implement me")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (use *WalletUseCase) CancelFreeze(ctx context.Context, tx usecase.WalletTransferRequest) error {
|
||||||
|
//TODO implement me
|
||||||
|
panic("implement me")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (use *WalletUseCase) Unconfirmed(ctx context.Context, tx usecase.WalletTransferRequest) error {
|
||||||
|
//TODO implement me
|
||||||
|
panic("implement me")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (use *WalletUseCase) Balance(ctx context.Context, req usecase.BalanceReq) ([]usecase.Balance, error) {
|
||||||
|
//TODO implement me
|
||||||
|
panic("implement me")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (use *WalletUseCase) HistoryBalance(ctx context.Context, req usecase.BalanceReq) ([]usecase.Balance, error) {
|
||||||
|
//TODO implement me
|
||||||
|
panic("implement me")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (use *WalletUseCase) BalanceByAssets(ctx context.Context, uid, cryptoCode string, walletTypes []wallet.Types) (usecase.BalanceAssetsResp, error) {
|
||||||
|
//TODO implement me
|
||||||
|
panic("implement me")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (use *WalletUseCase) CheckBalance(ctx context.Context, tx usecase.WalletTransferRequest) error {
|
||||||
|
//TODO implement me
|
||||||
|
panic("implement me")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (use *WalletUseCase) GetTodayWithdraw(ctx context.Context, uid, toCrypto string) (usecase.TodayWithdrawResp, error) {
|
||||||
|
//TODO implement me
|
||||||
|
panic("implement me")
|
||||||
|
}
|
||||||
|
|
||||||
|
func MustWalletUseCase(param WalletUseCaseParam) usecase.WalletTransferUseCase {
|
||||||
|
return &WalletUseCase{
|
||||||
|
WalletUseCaseParam: param,
|
||||||
|
existUIDAsset: make(map[string]struct{}),
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,79 @@
|
||||||
|
package usecase
|
||||||
|
|
||||||
|
import (
|
||||||
|
"code.30cm.net/digimon/app-cloudep-wallet-service/pkg/domain/repository"
|
||||||
|
"code.30cm.net/digimon/app-cloudep-wallet-service/pkg/domain/usecase"
|
||||||
|
"code.30cm.net/digimon/app-cloudep-wallet-service/pkg/domain/wallet"
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
type uidAssetKey struct {
|
||||||
|
uid string
|
||||||
|
asset string
|
||||||
|
}
|
||||||
|
|
||||||
|
// walletActionOption 表示一個「錢包操作函式」,可在錢包流程中插入自定義動作(例如:扣款、加值、凍結)
|
||||||
|
type walletActionOption func(
|
||||||
|
ctx context.Context,
|
||||||
|
tx *usecase.WalletTransferRequest,
|
||||||
|
wallet repository.UserWalletService,
|
||||||
|
) error
|
||||||
|
|
||||||
|
// withLockAvailable 鎖定用戶可用餘額
|
||||||
|
func (use *WalletUseCase) withLockAvailable() walletActionOption {
|
||||||
|
return func(ctx context.Context, tx *usecase.WalletTransferRequest, w repository.UserWalletService) error {
|
||||||
|
uidAsset := uidAssetKey{
|
||||||
|
uid: tx.FromUID,
|
||||||
|
asset: tx.Asset,
|
||||||
|
}
|
||||||
|
|
||||||
|
if !use.checkWalletExistence(uidAsset) {
|
||||||
|
// 找不到錢包存不存在
|
||||||
|
wStatus, err := w.HasAvailableBalance(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to check wallet: %w", err)
|
||||||
|
}
|
||||||
|
// 錢包不存在要做新增
|
||||||
|
if !wStatus {
|
||||||
|
//// 是合約模擬交易或帳變且錢包不存在才建立錢包
|
||||||
|
//if !(tx.Business == wa.ContractSimulationBusinessTypeBusinessName || tx.BusinessType == domain.DistributionBusinessTypeBusinessName) {
|
||||||
|
// // 新增錢包有命中 UK 不需要額外上鎖
|
||||||
|
// return use.translateError(err)
|
||||||
|
//}
|
||||||
|
|
||||||
|
if _, err := w.InitializeWallets(ctx, tx.Brand); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
use.markWalletAsExisting(uidAsset)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := w.GetBalancesForUpdate(ctx, []wallet.Types{wallet.TypeAvailable})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// subAvailable 減少用戶可用餘額
|
||||||
|
func (use *WalletUseCase) withSubAvailable() walletActionOption {
|
||||||
|
return func(_ context.Context, tx *usecase.WalletTransferRequest, w repository.UserWalletService) error {
|
||||||
|
if err := w.DecreaseBalance(wallet.TypeAvailable, tx.ReferenceOrderID, tx.Amount); err != nil {
|
||||||
|
if errors.Is(err, repository.ErrBalanceInsufficient) {
|
||||||
|
// todo 錯誤要看怎麼給(餘額不足)
|
||||||
|
return fmt.Errorf("balance insufficient")
|
||||||
|
}
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,127 @@
|
||||||
|
package usecase
|
||||||
|
|
||||||
|
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/usecase"
|
||||||
|
"code.30cm.net/digimon/app-cloudep-wallet-service/pkg/domain/wallet"
|
||||||
|
repo "code.30cm.net/digimon/app-cloudep-wallet-service/pkg/repository"
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
// userWalletFlow 表示一組要對某使用者、某資產執行的錢包操作(動作串列)
|
||||||
|
// 這些操作通常會在轉帳流程中依序套用,例如:從主錢包扣款 → 加到對方凍結錢包
|
||||||
|
type userWalletFlow struct {
|
||||||
|
UID string // 目標使用者 UID
|
||||||
|
Asset string // 目標資產代號(如 BTC、ETH、TWD)
|
||||||
|
Actions []walletActionOption // 要依序執行的錢包操作
|
||||||
|
}
|
||||||
|
|
||||||
|
// ProcessTransaction 處理一次完整的「錢包 + 訂單」交易流程:
|
||||||
|
// 1. 透過 Repository 開啟 DB 事務
|
||||||
|
// 2. 依序對每個 userWalletFlow 建立對應的 UserWalletService 實例
|
||||||
|
// 3. 依序執行每個 flow.Actions(扣款、加值、凍結、解凍…)
|
||||||
|
// 4. 建立一條 Transaction 記錄並寫進 transactionRepository
|
||||||
|
// 5. 將所有 walletTransactions 寫進 walletTransactionRepository
|
||||||
|
// 6. 最後在同一事務中執行每個 wallet 的 Execute / ExecuteOrder
|
||||||
|
// 7. 特別注意 flow 會按照順序做,所以順序是重要的
|
||||||
|
func (use *WalletUseCase) ProcessTransaction(
|
||||||
|
ctx context.Context,
|
||||||
|
req usecase.WalletTransferRequest,
|
||||||
|
flows ...userWalletFlow,
|
||||||
|
) error {
|
||||||
|
return use.WalletRepo.Transaction(func(db *gorm.DB) error {
|
||||||
|
// 暫存所有建立好的 UserWalletService
|
||||||
|
wallets := make([]repository.UserWalletService, 0, len(flows))
|
||||||
|
|
||||||
|
// flows 會按照順序做.順序是重要的
|
||||||
|
for _, flow := range flows {
|
||||||
|
// 1️⃣ 建立針對該使用者+資產的 UserWalletService
|
||||||
|
wSvc := repo.NewWalletService(db, flow.UID, flow.Asset)
|
||||||
|
|
||||||
|
// 2️⃣ 依序執行所有定義好的錢包操作
|
||||||
|
for _, action := range flow.Actions {
|
||||||
|
if err := action(ctx, &req, wSvc); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
wallets = append(wallets, wSvc)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3️⃣ 準備寫入 Transaction 主檔
|
||||||
|
txRecord := &entity.Transaction{
|
||||||
|
OrderID: req.ReferenceOrderID,
|
||||||
|
TransactionID: uuid.New().String(),
|
||||||
|
UID: req.FromUID,
|
||||||
|
ToUID: req.ToUID,
|
||||||
|
Asset: req.Asset,
|
||||||
|
TxType: req.TxType,
|
||||||
|
Amount: req.Amount,
|
||||||
|
Brand: req.Brand,
|
||||||
|
PostTransferBalance: req.PostTransferBalance,
|
||||||
|
BusinessType: req.Business.ToInt8(),
|
||||||
|
Status: wallet.EnableTrue,
|
||||||
|
DueTime: 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4️⃣ TODO 計算 DueTime (T+N 結算時間)
|
||||||
|
|
||||||
|
// 5️⃣ 寫入 Transaction 主檔
|
||||||
|
if err := use.TransactionRepo.Insert(ctx, txRecord); err != nil {
|
||||||
|
return fmt.Errorf("TransactionRepo.Insert 失敗: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 6️⃣ 聚合所有 wallet 內的交易歷程
|
||||||
|
var walletTxs []entity.WalletTransaction
|
||||||
|
for _, w := range wallets {
|
||||||
|
walletTxs = append(
|
||||||
|
walletTxs,
|
||||||
|
w.PrepareTransactions(
|
||||||
|
txRecord.ID,
|
||||||
|
txRecord.OrderID,
|
||||||
|
req.Brand,
|
||||||
|
req.Business,
|
||||||
|
)...,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 7️⃣ 批次寫入所有 WalletTransaction
|
||||||
|
if err := use.WalletTransactionRepo.Create(ctx, db, walletTxs); err != nil {
|
||||||
|
return fmt.Errorf("WalletTransactionRepository.Create 失敗: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 8️⃣ 最後才真正把錢包的餘額更新到資料庫(同一事務)
|
||||||
|
for _, wSvc := range wallets {
|
||||||
|
if err := wSvc.PersistBalances(ctx); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := wSvc.PersistOrderBalances(ctx); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// checkWalletExistence 檢查錢包是否存在於內存中
|
||||||
|
func (use *WalletUseCase) checkWalletExistence(uidAsset uidAssetKey) bool {
|
||||||
|
use.RLock()
|
||||||
|
defer use.RUnlock()
|
||||||
|
rk := fmt.Sprintf("%s-%s", uidAsset.uid, uidAsset.asset)
|
||||||
|
_, exists := use.existUIDAsset[rk]
|
||||||
|
|
||||||
|
return exists
|
||||||
|
}
|
||||||
|
|
||||||
|
// markWalletAsExisting 標記錢包為存在
|
||||||
|
func (use *WalletUseCase) markWalletAsExisting(uidAsset uidAssetKey) {
|
||||||
|
use.Lock()
|
||||||
|
defer use.Unlock()
|
||||||
|
rk := fmt.Sprintf("%s-%s", uidAsset.uid, uidAsset.asset)
|
||||||
|
use.existUIDAsset[rk] = struct{}{}
|
||||||
|
}
|
|
@ -0,0 +1,54 @@
|
||||||
|
|
||||||
|
// 充值-增加可用餘額
|
||||||
|
v1.POST("deposit", wallet.Deposit)
|
||||||
|
|
||||||
|
// 充值-增加限制餘額
|
||||||
|
v1.POST("deposit/unconfirmed", wallet.DepositUnconfirmed)
|
||||||
|
|
||||||
|
// 提現-減少可用餘額
|
||||||
|
v1.POST("withdraw", wallet.Withdraw)
|
||||||
|
|
||||||
|
// 凍結-減少可用餘額,加在凍結餘額
|
||||||
|
v1.POST("freeze", wallet.Freeze)
|
||||||
|
|
||||||
|
// 追加凍結(原凍結金額上)-減少可用餘額,加在凍結餘額
|
||||||
|
v1.POST("freeze/append", wallet.AppendFreeze)
|
||||||
|
|
||||||
|
// 解凍-減少凍結餘額
|
||||||
|
v1.POST("unfreeze", wallet.UnFreeze)
|
||||||
|
|
||||||
|
// rollback凍結-減少凍結餘額,加回可用餘額,不可指定金額
|
||||||
|
v1.POST("freeze/rollback", wallet.RollbackFreeze)
|
||||||
|
|
||||||
|
// rollback凍結-rollback凍結餘額,指定金額加回可用餘額
|
||||||
|
v1.POST("freeze/rollback/add", wallet.RollbackFreezeAddAvailable)
|
||||||
|
|
||||||
|
// 取消凍結-減少凍結餘額,加回可用餘額,可指定金額
|
||||||
|
v1.POST("freeze/cancel", wallet.CancelFreeze)
|
||||||
|
|
||||||
|
// 限制-減少凍結餘額,加別人限制餘額
|
||||||
|
v1.POST("unconfirmed", wallet.Unconfirmed)
|
||||||
|
|
||||||
|
// 合約劃轉
|
||||||
|
v1.POST("contract/transfer", wallet.ContractTransfer)
|
||||||
|
|
||||||
|
// 系統劃轉
|
||||||
|
v1.POST("system-transfer", wallet.systemTransfer)
|
||||||
|
|
||||||
|
// 餘額
|
||||||
|
v1.GET("balance/:uid/user", wallet.Balance)
|
||||||
|
|
||||||
|
// 歷史餘額
|
||||||
|
v1.GET("balance/history/:uid/user", wallet.HistoryBalance)
|
||||||
|
|
||||||
|
// 資產
|
||||||
|
v1.GET("assets/balance/:uid/user", wallet.BalanceByAssets)
|
||||||
|
|
||||||
|
// 檢查餘額
|
||||||
|
v1.POST("balance", wallet.CheckBalance)
|
||||||
|
|
||||||
|
// 取得今日已提現金額
|
||||||
|
v1.GET("withdraw/today/user/:uid", wallet.GetTodayWithdraw)
|
||||||
|
|
||||||
|
// 合約平台餘額
|
||||||
|
v1.GET("balance/contract/system", wallet.ContractSystemBalance)
|
Loading…
Reference in New Issue