commit 80f6caf86da52949afdbe7e604509fa49d1bbd52 Author: 王性驊 Date: Mon Dec 30 11:58:14 2024 +0800 feat: init project diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..040077b --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +.idea/ +go.sum +gen_result/ +etc/member.yaml +etc/member.dev.yaml +client/ +.DS_Store \ No newline at end of file diff --git a/.golangci.yaml b/.golangci.yaml new file mode 100644 index 0000000..36f79f3 --- /dev/null +++ b/.golangci.yaml @@ -0,0 +1,134 @@ +run: + timeout: 3m + # Exit code when at least one issue was found. + # Default: 1 + issues-exit-code: 2 + # Include test files or not. + # Default: true + tests: false + +# Reference URL: https://golangci-lint.run/usage/linters/ +linters: + # Disable everything by default so upgrades to not include new - default + # enabled- linters. + disable-all: true + # Specifically enable linters we want to use. + enable: + # - depguard + - errcheck + # - godot + - gofmt + - goimports + - gosimple + - govet + - ineffassign + - misspell + - revive + - typecheck + - unused + - asasalint + - asciicheck + - bidichk + - bodyclose + - contextcheck + - wastedassign + - whitespace + - thelper + - tparallel + - unconvert + - unparam + - usestdlibvars + - tenv + - testableexamples + - stylecheck + - sqlclosecheck + - nosprintfhostport + - paralleltest + - prealloc + - predeclared + - promlinter + - reassign + - rowserrcheck + - nakedret + - nestif + - nilerr + - nilnil + - nlreturn + - noctx + - nolintlint + - nonamedreturns + - decorder + - dogsled + - dupword + - durationcheck + - errchkjson + - errname + - errorlint + # - execinquery + - exhaustive + - exportloopref + - forbidigo + - forcetypeassert + - gochecknoinits + - gocognit + - goconst + - gocritic + - gocyclo + - goheader + - gomoddirectives + - goprintffuncname + - gosec + - grouper + - importas + - interfacebloat + - lll + - loggercheck + - maintidx + - makezero + +issues: + exclude-rules: + - path: _test\.go + linters: + - funlen + - goconst + - interfacer + - dupl + - lll + - goerr113 + - errcheck + - gocritic + - cyclop + - wrapcheck + - gocognit + - contextcheck + + exclude-dirs: + - internal/logic + + exclude-files: + - .*_test.go + + + +linters-settings: + gci: + sections: + - standard # Standard section: captures all standard packages. + - default # Default section: contains all imports that could not be matched to another section type. + gocognit: + # Minimal code complexity to report. + # Default: 30 (but we recommend 10-20) + min-complexity: 40 + nestif: + # Minimal complexity of if statements to report. + # Default: 5 + min-complexity: 10 + lll: + # Max line length, lines longer will be reported. + # '\t' is counted as 1 character by default, and can be changed with the tab-width option. + # Default: 120. + line-length: 200 + # Tab width in spaces. + # Default: 1 + tab-width: 1 \ No newline at end of file diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..daf4338 --- /dev/null +++ b/Makefile @@ -0,0 +1,53 @@ +# go-zero 生成風格 +GO_ZERO_STYLE=go_zero +GO ?= go +GOFMT ?= gofmt "-s" +GOFILES := $(shell find . -name "*.go") +LDFLAGS := -s -w +VERSION="v1.0.4" +DOCKER_REPO="igs170911/member" +GIT_COMMIT ?= $(shell git rev-parse --short HEAD) + +.PHONY: test +test: # 進行測試 + go test -v --cover ./... + +.PHONY: fmt +fmt: # 格式優化 + $(GOFMT) -w $(GOFILES) + goimports -w ./ + golangci-lint run + +.PHONY: gen-rpc +gen-rpc: # 建立 rpc code + goctl rpc protoc ./generate/protobuf/member.proto -m --style=$(GO_ZERO_STYLE) --go_out=./gen_result/pb --go-grpc_out=./gen_result/pb --zrpc_out=. + go mod tidy + @echo "Generate core-api files successfully" + +.PHONY: run-docker +run-docker: # 建立 rpc code + docker run --platform=linux/arm64/v8 -p 8080:8080 $(DOCKER_REPO):$(VERSION) + +.PHONY: build-docker +build-docker: + cp ./build/Dockerfile Dockerfile + docker buildx build \ + -t $(DOCKER_REPO):$(VERSION) \ + --build-arg VERSION=$(VERSION) \ + --build-arg GIT_COMMIT=$(GIT_COMMIT) \ + --secret id=ssh_key,src=./build/id_ed25519 \ + --progress=plain . + rm -rf Dockerfile + @echo "Generate core-api files successfully" + + +.PHONY: mock-gen +mock-gen: # 建立 mock 資料 + mockgen -source=./pkg/domain/repository/account.go -destination=./pkg/mock/repository/account.go -package=mock + mockgen -source=./pkg/domain/repository/account_uid.go -destination=./pkg/mock/repository/account_uid.go -package=mock + mockgen -source=./pkg/domain/repository/auto_id.go -destination=./pkg/mock/repository/auto_id.go -package=mock + mockgen -source=./pkg/domain/repository/user.go -destination=./pkg/mock/repository/user.go -package=mock + mockgen -source=./pkg/domain/repository/verify_code.go -destination=./pkg/mock/repository/verify_code.go -package=mock + mockgen -source=./pkg/domain/usecase/generate_uid.go -destination=./pkg/mock/usecase/generate_uid.go -package=mock + + @echo "Generate mock files successfully" diff --git a/build/Dockerfile b/build/Dockerfile new file mode 100644 index 0000000..076303a --- /dev/null +++ b/build/Dockerfile @@ -0,0 +1,42 @@ +########### +# BUILDER # +########### + +FROM golang:1.23.4 AS builder + +ARG VERSION +ARG BUILT +ARG GIT_COMMIT + +# private go packages +ENV GOPRIVATE=code.30cm.net +ENV FLAG="-s -w -X main.Version=${VERSION} -X main.Built=${BUILT} -X main.GitCommit=${GIT_COMMIT}" +WORKDIR /app +COPY . . + + +RUN apt-get update && \ + apt-get install -y git + +# Make the root foler for our ssh +RUN --mount=type=secret,id=ssh_key,dst=/root/.ssh/id_rsa \ + ssh-keyscan git.30cm.net >> /root/.ssh/known_hosts + + +RUN --mount=type=ssh go mod download + +RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build \ + -ldflags "$FLAG" \ + -o member + +########## +## FINAL # +########## +# +FROM gcr.io/distroless/static-debian11 +WORKDIR /app + +COPY --from=builder /app/member /app/member +COPY --from=builder /app/etc/member.yaml /app/etc/member.yaml +EXPOSE 8080 +CMD ["/app/member"] \ No newline at end of file diff --git a/build/id_ed25519 b/build/id_ed25519 new file mode 100644 index 0000000..e69de29 diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..7dcf0c3 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,71 @@ +version: "3.9" + +services: + app: + image: igs170911/member:v1.0.4 + container_name: app-service + ports: + - "8080:8080" # 替換為您的應用服務的公開端口 + depends_on: + - mongo + - etcd + - redis + environment: + MONGO_URI: mongodb://mongo:27017/appdb + ETCD_ENDPOINT: http://etcd:2379 + REDIS_HOST: redis + REDIS_PORT: 6379 + networks: + - app-network + + mongo: + image: mongo:8.0 + container_name: mongo + restart: always + ports: + - "27017:27017" + environment: + MONGO_INITDB_ROOT_USERNAME: root + MONGO_INITDB_ROOT_PASSWORD: example + networks: + - app-network + volumes: + - mongo-data:/data/db + + etcd: + image: quay.io/coreos/etcd:v3.5.5 + container_name: etcd + restart: always + command: > + /usr/local/bin/etcd + --data-dir=/etcd-data + --name=etcd + --listen-client-urls=http://0.0.0.0:2379 + --advertise-client-urls=http://etcd:2379 + ports: + - "2379:2379" + - "2380:2380" + networks: + - app-network + volumes: + - etcd-data:/etcd-data + + redis: + image: redis:7.0 + container_name: redis + restart: always + ports: + - "6379:6379" + networks: + - app-network + volumes: + - redis-data:/data + +networks: + app-network: + driver: bridge + +volumes: + mongo-data: + etcd-data: + redis-data: \ No newline at end of file diff --git a/etc/member.example.yaml b/etc/member.example.yaml new file mode 100644 index 0000000..f4fc756 --- /dev/null +++ b/etc/member.example.yaml @@ -0,0 +1,43 @@ +Name: member.rpc +ListenOn: 0.0.0.0:8080 +Etcd: + Hosts: + - 127.0.0.1:2379 + Key: member.rpc + +Cache: + - Host: 127.0.0.1:6379 + type: node +CacheExpireTime: 1s +CacheWithNotFoundExpiry: 1s + +Mongo: + Schema: mongodb + Host: 127.0.0.1 + User: "admin" + Password: "123" + Port: "27017" + Database: digimon_member + ReplicaName: "rs0" + MaxStaleness: 30m + MaxPoolSize: 30 + MinPoolSize: 10 + MaxConnIdleTime: 30m + Compressors: + - f + EnableStandardReadWriteSplitMode: true + ConnectTimeoutMs : 300 + +Bcrypt: + Cost: 10 + +GoogleAuth: + ClientID: + AuthURL: + +LineAuth: + ClientID : + ClientSecret : + RedirectURI : + +Host: 127.0.0.1 \ No newline at end of file diff --git a/generate/protobuf/member.proto b/generate/protobuf/member.proto new file mode 100644 index 0000000..9d39c8a --- /dev/null +++ b/generate/protobuf/member.proto @@ -0,0 +1,248 @@ +syntax = "proto3"; + +package member; +option go_package="./member"; + +// OKResp +message OKResp {} +// NoneReq +message NoneReq {} + +// ================ enum ================ +enum VerifyType { + VERIFY_NONE = 0; // 初始(異常) + VERIFY_OK = 1; + VERIFY_NOT = 2; // 尚未 +} + +enum AlarmType { + ALARM_NONE = 0; // 初始(異常) + ALARM_NOT = 1; // 未告警 + ALARM_SYSTEM = 2; // 系統告警中 +} + +enum MemberStatus { + STATUS_NONE = 0; // 初始(異常) + STATUS_VERIFY = 1; // 尚未驗證 + STATUS_COMPLETE = 2; // 帳號啟用中 + STATUS_DISABLE = 3; // 帳號停權中 +} + +// ================ enum ================ + + +// ================ common ================ +message Pager { + int64 total =1; + int64 size=2; + int64 index=3; +} +// ================ common ================ + + +// ================ account ================ +message CreateLoginUserReq { + string login_id = 1; + int64 platform = 2; + string token = 3; +} + +message BindingUserReq { + string uid = 1; + string login_id = 2; + int64 type = 3; +} + +message BindingUserResp { + string uid = 1; + string login_id = 2; + int64 type = 3; +} + +message CreateUserInfoReq { + string uid = 1; + VerifyType verify_type = 2; + AlarmType alarm_type = 3; + MemberStatus status = 4; + string language = 5; + string currency = 6; + optional string avatar= 7; + optional string nick_name = 8; + optional string full_name = 9; + optional int64 gender = 10; + optional int64 birthdate = 11; + optional string phone_number = 12; + optional string email = 13; + optional string address = 14; +} + +message GetAccountInfoResp { + CreateLoginUserReq data = 1; +} + +// UpdateUserInfoReq 不處理邏輯給不給改,這裡只關新增修改刪除 +message UpdateUserInfoReq { + string uid = 1; + optional string language = 2; + optional string currency = 3; + optional string nick_name = 4; + optional string avatar = 5; + optional VerifyType verify_type = 6; + optional AlarmType alarm_type = 7; + optional MemberStatus status = 8; + optional string full_name = 9; + optional int64 gender = 10; + optional int64 birthdate = 11; + optional string phone_number = 12; + optional string email = 13; + optional string address = 14; +} + +message GetUIDByAccountReq { + string account = 1; +} + +message GetUIDByAccountResp { + string uid = 1; + string account =2; +} + +message UpdateTokenReq { + string account = 1; + string token = 2; + int64 platform=3; +} + +message GenerateRefreshCodeReq { + string account = 1; + int32 code_type =2; +} + +message VerifyCode { + string verify_code = 1; +} + +message GenerateRefreshCodeResp { + VerifyCode data = 1; +} + +message VerifyRefreshCodeReq { + string account = 1; + int32 code_type =2; + string verify_code = 3; +} + +message UpdateStatusReq { + string uid = 1; + MemberStatus status = 2; +} + +message GetUserInfoReq { + string uid = 1; + optional string nick_name =2; +} + +message UserInfo { + string uid = 1; + VerifyType verify_type = 2; + AlarmType alarm_type = 3; + MemberStatus status = 4; + string language = 5; + string currency = 6; + string avatar = 7; + int64 create_time=8; + int64 update_time=9; + optional string nick_name = 10; +} + +message GetUserInfoResp { + UserInfo data = 1; +} + +message ListUserInfoReq { + optional VerifyType verify_type = 1; + optional AlarmType alarm_type = 2; + optional MemberStatus status = 3; + optional int64 create_start_time = 4; + optional int64 create_end_time = 5; + int64 page_size =6; + int64 page_index=7; +} + +message ListUserInfoResp { + repeated UserInfo data = 1; + Pager page =2; +} + +message VerifyAuthResultReq { + string token = 1; + optional string account = 2; +} + +message VerifyAuthResultResp { + bool status = 1; +} + +message TwitterAccessTokenResp { + string token = 1; +} +message BindVerifyEmailReq { + string uid = 1; + string email = 2; +} + +message BindVerifyPhoneReq { + string uid = 1; + string phone = 2; +} + +message LineAccessTokenResp { + string token = 1; +} + +message LineUserProfile { + string name = 1; + string email = 2; +} + +service Account { + // CreateUserAccount 建立帳號與密碼 -> 可登入,但可不可以做其他事情看業務流程,也可以只註冊就好 + rpc CreateUserAccount(CreateLoginUserReq) returns(OKResp); + // GetUserAccountInfo 取得帳號密碼資料 + rpc GetUserAccountInfo(GetUIDByAccountReq) returns(GetAccountInfoResp); + // UpdateUserToken 更新密碼 + rpc UpdateUserToken(UpdateTokenReq) returns(OKResp); + // GetUIDByAccount 用帳號換取 UID + rpc GetUIDByAccount(GetUIDByAccountReq) returns(GetUIDByAccountResp); + // BindAccount 綁定帳號 -> account bind to UID + rpc BindAccount(BindingUserReq) returns(BindingUserResp); + // BindUserInfo 初次,綁定 User Info + rpc BindUserInfo(CreateUserInfoReq) returns(OKResp); + // BindVerifyEmail 綁定 Email + rpc BindVerifyEmail(BindVerifyEmailReq) returns (OKResp); + // BindVerifyPhone 綁定 Phone + rpc BindVerifyPhone(BindVerifyPhoneReq) returns (OKResp); + // UpdateUserInfo 更新 User Info + rpc UpdateUserInfo(UpdateUserInfoReq) returns(OKResp); + // UpdateStatus 修改狀態 + rpc UpdateStatus(UpdateStatusReq) returns(OKResp); + // GetUserInfo 取得會員資訊 + rpc GetUserInfo(GetUserInfoReq) returns(GetUserInfoResp); + // ListMember 取得會員列表 + rpc ListMember(ListUserInfoReq) returns(ListUserInfoResp); + // GenerateRefreshCode 這個帳號驗證碼(十分鐘),通用的 + rpc GenerateRefreshCode(GenerateRefreshCodeReq) returns(GenerateRefreshCodeResp); + // VerifyRefreshCode 驗證忘記密碼 token + rpc VerifyRefreshCode(VerifyRefreshCodeReq) returns(OKResp); + // CheckRefreshCode 驗證忘記密碼 token 不刪除,只確認) + rpc CheckRefreshCode(VerifyRefreshCodeReq) returns(OKResp); + // VerifyGoogleAuthResult 驗證 google 登入是否有效 + rpc VerifyGoogleAuthResult(VerifyAuthResultReq)returns(VerifyAuthResultResp); + // VerifyPlatformAuthResult 驗證 google 登入是否有效 + rpc VerifyPlatformAuthResult(VerifyAuthResultReq)returns(VerifyAuthResultResp); + // LineCodeToAccessToken Line 驗證相關 + rpc LineCodeToAccessToken(NoneReq) returns (LineAccessTokenResp); + // LineGetProfileByAccessToken Line 驗證相關 + rpc LineGetProfileByAccessToken(NoneReq) returns (LineUserProfile); +} +// ================ account ================ \ No newline at end of file diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..2d03672 --- /dev/null +++ b/go.mod @@ -0,0 +1,147 @@ +module app-cloudep-member-server + +go 1.23.4 + +require ( + code.30cm.net/digimon/library-go/errs v1.2.12 + code.30cm.net/digimon/library-go/mongo v0.0.9 + code.30cm.net/digimon/library-go/utils/invited_code v1.2.5 + code.30cm.net/digimon/library-go/validator v1.0.0 + github.com/alicebob/miniredis/v2 v2.33.0 + github.com/stretchr/testify v1.9.0 + github.com/testcontainers/testcontainers-go v0.34.0 + github.com/zeromicro/go-zero v1.7.4 + go.mongodb.org/mongo-driver v1.17.1 + go.uber.org/mock v0.5.0 + golang.org/x/crypto v0.29.0 + google.golang.org/grpc v1.69.2 + google.golang.org/protobuf v1.36.1 +) + +require ( + dario.cat/mergo v1.0.0 // indirect + github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect + github.com/Microsoft/go-winio v0.6.2 // indirect + github.com/alicebob/gopher-json v0.0.0-20230218143504-906a9b012302 // indirect + github.com/beorn7/perks v1.0.1 // indirect + github.com/cenkalti/backoff/v4 v4.3.0 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/containerd/containerd v1.7.18 // indirect + github.com/containerd/log v0.1.0 // indirect + github.com/containerd/platforms v0.2.1 // indirect + github.com/coreos/go-semver v0.3.1 // indirect + github.com/coreos/go-systemd/v22 v22.5.0 // indirect + github.com/cpuguy83/dockercfg v0.3.2 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect + github.com/distribution/reference v0.6.0 // indirect + github.com/docker/docker v27.1.1+incompatible // indirect + github.com/docker/go-connections v0.5.0 // indirect + github.com/docker/go-units v0.5.0 // indirect + github.com/emicklei/go-restful/v3 v3.11.0 // indirect + github.com/fatih/color v1.18.0 // indirect + github.com/felixge/httpsnoop v1.0.4 // indirect + github.com/gabriel-vasile/mimetype v1.4.3 // indirect + github.com/go-logr/logr v1.4.2 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/go-ole/go-ole v1.2.6 // indirect + github.com/go-openapi/jsonpointer v0.19.6 // indirect + github.com/go-openapi/jsonreference v0.20.2 // indirect + github.com/go-openapi/swag v0.22.4 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/go-playground/validator/v10 v10.22.0 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/golang/mock v1.6.0 // indirect + github.com/golang/protobuf v1.5.4 // indirect + github.com/golang/snappy v0.0.4 // indirect + github.com/google/gnostic-models v0.6.8 // indirect + github.com/google/go-cmp v0.6.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/josharian/intern v1.0.0 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/compress v1.17.9 // indirect + github.com/leodido/go-urn v1.4.0 // indirect + github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect + github.com/magiconair/properties v1.8.7 // indirect + github.com/mailru/easyjson v0.7.7 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/moby/docker-image-spec v1.3.1 // indirect + github.com/moby/patternmatcher v0.6.0 // indirect + github.com/moby/sys/sequential v0.5.0 // indirect + github.com/moby/sys/user v0.1.0 // indirect + github.com/moby/term v0.5.0 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/montanaflynn/stats v0.7.1 // indirect + github.com/morikuni/aec v1.0.0 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/opencontainers/go-digest v1.0.0 // indirect + github.com/opencontainers/image-spec v1.1.0 // indirect + github.com/openzipkin/zipkin-go v0.4.3 // indirect + github.com/pelletier/go-toml/v2 v2.2.2 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect + github.com/prometheus/client_golang v1.20.5 // indirect + github.com/prometheus/client_model v0.6.1 // indirect + github.com/prometheus/common v0.55.0 // indirect + github.com/prometheus/procfs v0.15.1 // indirect + github.com/redis/go-redis/v9 v9.7.0 // indirect + github.com/shirou/gopsutil/v3 v3.23.12 // indirect + github.com/shoenig/go-m1cpu v0.1.6 // indirect + github.com/shopspring/decimal v1.4.0 // indirect + github.com/sirupsen/logrus v1.9.3 // 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/xdg-go/pbkdf2 v1.0.0 // indirect + github.com/xdg-go/scram v1.1.2 // indirect + github.com/xdg-go/stringprep v1.0.4 // indirect + github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect + github.com/yuin/gopher-lua v1.1.1 // indirect + github.com/yusufpapurcu/wmi v1.2.3 // indirect + go.etcd.io/etcd/api/v3 v3.5.15 // indirect + go.etcd.io/etcd/client/pkg/v3 v3.5.15 // indirect + go.etcd.io/etcd/client/v3 v3.5.15 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 // indirect + go.opentelemetry.io/otel v1.31.0 // indirect + go.opentelemetry.io/otel/exporters/jaeger v1.17.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.24.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.24.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.24.0 // indirect + go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.24.0 // indirect + go.opentelemetry.io/otel/exporters/zipkin v1.24.0 // indirect + go.opentelemetry.io/otel/metric v1.31.0 // indirect + go.opentelemetry.io/otel/sdk v1.31.0 // indirect + go.opentelemetry.io/otel/trace v1.31.0 // indirect + go.opentelemetry.io/proto/otlp v1.3.1 // indirect + go.uber.org/atomic v1.10.0 // indirect + go.uber.org/automaxprocs v1.6.0 // indirect + go.uber.org/multierr v1.9.0 // indirect + go.uber.org/zap v1.24.0 // indirect + golang.org/x/net v0.31.0 // indirect + golang.org/x/oauth2 v0.23.0 // indirect + golang.org/x/sync v0.9.0 // indirect + golang.org/x/sys v0.27.0 // indirect + golang.org/x/term v0.26.0 // indirect + golang.org/x/text v0.20.0 // indirect + golang.org/x/time v0.8.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20241015192408-796eee8c2d53 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20241015192408-796eee8c2d53 // indirect + gopkg.in/inf.v0 v0.9.1 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect + k8s.io/api v0.29.3 // indirect + k8s.io/apimachinery v0.29.4 // indirect + k8s.io/client-go v0.29.3 // indirect + k8s.io/klog/v2 v2.110.1 // indirect + k8s.io/kube-openapi v0.0.0-20231010175941-2dd684a91f00 // indirect + k8s.io/utils v0.0.0-20240711033017-18e509b52bc8 // indirect + sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect + sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect + sigs.k8s.io/yaml v1.3.0 // indirect +) diff --git a/internal/config/config.go b/internal/config/config.go new file mode 100755 index 0000000..8d436f1 --- /dev/null +++ b/internal/config/config.go @@ -0,0 +1,50 @@ +package config + +import ( + "github.com/zeromicro/go-zero/core/stores/cache" + "github.com/zeromicro/go-zero/core/stores/redis" + "github.com/zeromicro/go-zero/zrpc" + "time" +) + +type Config struct { + zrpc.RpcServerConf + redis.RedisConf + // Redis Cluster + Cache cache.CacheConf + CacheExpireTime time.Duration + CacheWithNotFoundExpiry time.Duration + + Mongo struct { + Schema string + User string + Password string + Host string + Port string + Database string + ReplicaName string + MaxStaleness time.Duration + MaxPoolSize uint64 + MinPoolSize uint64 + MaxConnIdleTime time.Duration + Compressors []string + EnableStandardReadWriteSplitMode bool + ConnectTimeoutMs int64 + } + + // 密碼加密層數 + Bcrypt struct { + Cost int + } + + GoogleAuth struct { + ClientID string + AuthURL string + } + + LineAuth struct { + ClientID string + ClientSecret string + RedirectURI string + } +} diff --git a/internal/logic/account/bind_account_logic.go b/internal/logic/account/bind_account_logic.go new file mode 100644 index 0000000..eaa3184 --- /dev/null +++ b/internal/logic/account/bind_account_logic.go @@ -0,0 +1,85 @@ +package accountlogic + +import ( + domain "app-cloudep-member-server/pkg/domain/member" + "app-cloudep-member-server/pkg/domain/usecase" + "code.30cm.net/digimon/library-go/errs" + "context" + "fmt" + "math" + + "app-cloudep-member-server/gen_result/pb/member" + "app-cloudep-member-server/internal/svc" + + "github.com/zeromicro/go-zero/core/logx" +) + +type BindAccountLogic struct { + ctx context.Context + svcCtx *svc.ServiceContext + logx.Logger +} + +func NewBindAccountLogic(ctx context.Context, svcCtx *svc.ServiceContext) *BindAccountLogic { + return &BindAccountLogic{ + ctx: ctx, + svcCtx: svcCtx, + Logger: logx.WithContext(ctx), + } +} + +type bindLoginUserReq struct { + Account string `json:"account" validate:"account"` + UID string `json:"uid" ` + Type int64 `json:"type" validate:"required,oneof=1 2 3"` +} + +// BindAccount 綁定帳號 -> account bind to UID +func (l *BindAccountLogic) BindAccount(in *member.BindingUserReq) (*member.BindingUserResp, error) { + // 驗證資料 + if err := l.svcCtx.Validate.ValidateAll(&bindLoginUserReq{ + Account: in.GetLoginId(), + Type: in.GetType(), + }); err != nil { + return nil, errs.InvalidFormat(err.Error()) + } + + // 先確定有這個Account + if _, err := l.svcCtx.AccountUseCase.GetUserAccountInfo(l.ctx, usecase.GetUIDByAccountRequest{Account: in.GetLoginId()}); err != nil { + return nil, err + } + + var err error + uid := in.GetUid() + // 有 UID 綁看看,沒帶UID 近來,確認沒重複就直接綁一個給他 + if in.GetUid() == "" { + uid, err = l.svcCtx.AccountUseCase.Generate(l.ctx) + if err != nil { + return nil, err + } + } + t, err := int64ToInt32Safe(in.GetType()) + if err != nil { + return nil, err + } + if _, err := l.svcCtx.AccountUseCase.BindAccount(l.ctx, usecase.BindingUser{ + LoginID: in.LoginId, + UID: uid, + Type: domain.AccountType(t), + }); err != nil { + return nil, err + } + + return &member.BindingUserResp{ + LoginId: in.LoginId, + Uid: uid, + Type: in.GetType(), + }, nil +} + +func int64ToInt32Safe(value int64) (int32, error) { + if value > math.MaxInt32 || value < math.MinInt32 { + return 0, fmt.Errorf("value %d is out of int32 range", value) + } + return int32(value), nil +} diff --git a/internal/logic/account/bind_user_info_logic.go b/internal/logic/account/bind_user_info_logic.go new file mode 100644 index 0000000..e1aed1c --- /dev/null +++ b/internal/logic/account/bind_user_info_logic.go @@ -0,0 +1,46 @@ +package accountlogic + +import ( + "app-cloudep-member-server/gen_result/pb/member" + "app-cloudep-member-server/internal/svc" + "context" + + "github.com/zeromicro/go-zero/core/logx" +) + +type BindUserInfoLogic struct { + ctx context.Context + svcCtx *svc.ServiceContext + logx.Logger +} + +func NewBindUserInfoLogic(ctx context.Context, svcCtx *svc.ServiceContext) *BindUserInfoLogic { + return &BindUserInfoLogic{ + ctx: ctx, + svcCtx: svcCtx, + Logger: logx.WithContext(ctx), + } +} + +type createUserInfo struct { + Uid string `validate:"required"` + VerifyType int32 `validate:"required,oneof=0 1 2 3"` + AlarmType int32 `validate:"required,oneof=0 1 2"` + Status int32 `validate:"required,oneof=1 2 3 4 5 6"` + RoleId string `validate:"required"` + Language string `validate:"required"` + Currency string `validate:"required"` + NickName string `validate:"required"` +} + +type createUserInfoReq struct { + Uid string `validate:"required"` // 唯一辨識碼 + Language string `validate:"required"` + Currency string `validate:"required"` +} + +// BindUserInfo 初次,綁定 User Info +func (l *BindUserInfoLogic) BindUserInfo(in *member.CreateUserInfoReq) (*member.OKResp, error) { + + return &member.OKResp{}, nil +} diff --git a/internal/logic/account/bind_verify_email_logic.go b/internal/logic/account/bind_verify_email_logic.go new file mode 100644 index 0000000..3b264a8 --- /dev/null +++ b/internal/logic/account/bind_verify_email_logic.go @@ -0,0 +1,31 @@ +package accountlogic + +import ( + "context" + + "app-cloudep-member-server/gen_result/pb/member" + "app-cloudep-member-server/internal/svc" + + "github.com/zeromicro/go-zero/core/logx" +) + +type BindVerifyEmailLogic struct { + ctx context.Context + svcCtx *svc.ServiceContext + logx.Logger +} + +func NewBindVerifyEmailLogic(ctx context.Context, svcCtx *svc.ServiceContext) *BindVerifyEmailLogic { + return &BindVerifyEmailLogic{ + ctx: ctx, + svcCtx: svcCtx, + Logger: logx.WithContext(ctx), + } +} + +// BindVerifyEmail 綁定 Email +func (l *BindVerifyEmailLogic) BindVerifyEmail(in *member.BindVerifyEmailReq) (*member.OKResp, error) { + // todo: add your logic here and delete this line + + return &member.OKResp{}, nil +} diff --git a/internal/logic/account/bind_verify_phone_logic.go b/internal/logic/account/bind_verify_phone_logic.go new file mode 100644 index 0000000..7ec68fb --- /dev/null +++ b/internal/logic/account/bind_verify_phone_logic.go @@ -0,0 +1,31 @@ +package accountlogic + +import ( + "context" + + "app-cloudep-member-server/gen_result/pb/member" + "app-cloudep-member-server/internal/svc" + + "github.com/zeromicro/go-zero/core/logx" +) + +type BindVerifyPhoneLogic struct { + ctx context.Context + svcCtx *svc.ServiceContext + logx.Logger +} + +func NewBindVerifyPhoneLogic(ctx context.Context, svcCtx *svc.ServiceContext) *BindVerifyPhoneLogic { + return &BindVerifyPhoneLogic{ + ctx: ctx, + svcCtx: svcCtx, + Logger: logx.WithContext(ctx), + } +} + +// BindVerifyPhone 綁定 Phone +func (l *BindVerifyPhoneLogic) BindVerifyPhone(in *member.BindVerifyPhoneReq) (*member.OKResp, error) { + // todo: add your logic here and delete this line + + return &member.OKResp{}, nil +} diff --git a/internal/logic/account/check_refresh_code_logic.go b/internal/logic/account/check_refresh_code_logic.go new file mode 100644 index 0000000..e4f3ce5 --- /dev/null +++ b/internal/logic/account/check_refresh_code_logic.go @@ -0,0 +1,31 @@ +package accountlogic + +import ( + "context" + + "app-cloudep-member-server/gen_result/pb/member" + "app-cloudep-member-server/internal/svc" + + "github.com/zeromicro/go-zero/core/logx" +) + +type CheckRefreshCodeLogic struct { + ctx context.Context + svcCtx *svc.ServiceContext + logx.Logger +} + +func NewCheckRefreshCodeLogic(ctx context.Context, svcCtx *svc.ServiceContext) *CheckRefreshCodeLogic { + return &CheckRefreshCodeLogic{ + ctx: ctx, + svcCtx: svcCtx, + Logger: logx.WithContext(ctx), + } +} + +// CheckRefreshCode 驗證忘記密碼 token 不刪除,只確認) +func (l *CheckRefreshCodeLogic) CheckRefreshCode(in *member.VerifyRefreshCodeReq) (*member.OKResp, error) { + // todo: add your logic here and delete this line + + return &member.OKResp{}, nil +} diff --git a/internal/logic/account/create_user_account_logic.go b/internal/logic/account/create_user_account_logic.go new file mode 100644 index 0000000..1665198 --- /dev/null +++ b/internal/logic/account/create_user_account_logic.go @@ -0,0 +1,31 @@ +package accountlogic + +import ( + "context" + + "app-cloudep-member-server/gen_result/pb/member" + "app-cloudep-member-server/internal/svc" + + "github.com/zeromicro/go-zero/core/logx" +) + +type CreateUserAccountLogic struct { + ctx context.Context + svcCtx *svc.ServiceContext + logx.Logger +} + +func NewCreateUserAccountLogic(ctx context.Context, svcCtx *svc.ServiceContext) *CreateUserAccountLogic { + return &CreateUserAccountLogic{ + ctx: ctx, + svcCtx: svcCtx, + Logger: logx.WithContext(ctx), + } +} + +// CreateUserAccount 建立帳號與密碼 -> 可登入,但可不可以做其他事情看業務流程,也可以只註冊就好 +func (l *CreateUserAccountLogic) CreateUserAccount(in *member.CreateLoginUserReq) (*member.OKResp, error) { + // todo: add your logic here and delete this line + + return &member.OKResp{}, nil +} diff --git a/internal/logic/account/generate_refresh_code_logic.go b/internal/logic/account/generate_refresh_code_logic.go new file mode 100644 index 0000000..593b86e --- /dev/null +++ b/internal/logic/account/generate_refresh_code_logic.go @@ -0,0 +1,31 @@ +package accountlogic + +import ( + "context" + + "app-cloudep-member-server/gen_result/pb/member" + "app-cloudep-member-server/internal/svc" + + "github.com/zeromicro/go-zero/core/logx" +) + +type GenerateRefreshCodeLogic struct { + ctx context.Context + svcCtx *svc.ServiceContext + logx.Logger +} + +func NewGenerateRefreshCodeLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GenerateRefreshCodeLogic { + return &GenerateRefreshCodeLogic{ + ctx: ctx, + svcCtx: svcCtx, + Logger: logx.WithContext(ctx), + } +} + +// GenerateRefreshCode 這個帳號驗證碼(十分鐘),通用的 +func (l *GenerateRefreshCodeLogic) GenerateRefreshCode(in *member.GenerateRefreshCodeReq) (*member.GenerateRefreshCodeResp, error) { + // todo: add your logic here and delete this line + + return &member.GenerateRefreshCodeResp{}, nil +} diff --git a/internal/logic/account/get_u_i_d_by_account_logic.go b/internal/logic/account/get_u_i_d_by_account_logic.go new file mode 100644 index 0000000..07aacdf --- /dev/null +++ b/internal/logic/account/get_u_i_d_by_account_logic.go @@ -0,0 +1,31 @@ +package accountlogic + +import ( + "context" + + "app-cloudep-member-server/gen_result/pb/member" + "app-cloudep-member-server/internal/svc" + + "github.com/zeromicro/go-zero/core/logx" +) + +type GetUIDByAccountLogic struct { + ctx context.Context + svcCtx *svc.ServiceContext + logx.Logger +} + +func NewGetUIDByAccountLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetUIDByAccountLogic { + return &GetUIDByAccountLogic{ + ctx: ctx, + svcCtx: svcCtx, + Logger: logx.WithContext(ctx), + } +} + +// GetUIDByAccount 用帳號換取 UID +func (l *GetUIDByAccountLogic) GetUIDByAccount(in *member.GetUIDByAccountReq) (*member.GetUIDByAccountResp, error) { + // todo: add your logic here and delete this line + + return &member.GetUIDByAccountResp{}, nil +} diff --git a/internal/logic/account/get_user_account_info_logic.go b/internal/logic/account/get_user_account_info_logic.go new file mode 100644 index 0000000..8fc5d17 --- /dev/null +++ b/internal/logic/account/get_user_account_info_logic.go @@ -0,0 +1,31 @@ +package accountlogic + +import ( + "context" + + "app-cloudep-member-server/gen_result/pb/member" + "app-cloudep-member-server/internal/svc" + + "github.com/zeromicro/go-zero/core/logx" +) + +type GetUserAccountInfoLogic struct { + ctx context.Context + svcCtx *svc.ServiceContext + logx.Logger +} + +func NewGetUserAccountInfoLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetUserAccountInfoLogic { + return &GetUserAccountInfoLogic{ + ctx: ctx, + svcCtx: svcCtx, + Logger: logx.WithContext(ctx), + } +} + +// GetUserAccountInfo 取得帳號密碼資料 +func (l *GetUserAccountInfoLogic) GetUserAccountInfo(in *member.GetUIDByAccountReq) (*member.GetAccountInfoResp, error) { + // todo: add your logic here and delete this line + + return &member.GetAccountInfoResp{}, nil +} diff --git a/internal/logic/account/get_user_info_logic.go b/internal/logic/account/get_user_info_logic.go new file mode 100644 index 0000000..3dfca9a --- /dev/null +++ b/internal/logic/account/get_user_info_logic.go @@ -0,0 +1,31 @@ +package accountlogic + +import ( + "context" + + "app-cloudep-member-server/gen_result/pb/member" + "app-cloudep-member-server/internal/svc" + + "github.com/zeromicro/go-zero/core/logx" +) + +type GetUserInfoLogic struct { + ctx context.Context + svcCtx *svc.ServiceContext + logx.Logger +} + +func NewGetUserInfoLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetUserInfoLogic { + return &GetUserInfoLogic{ + ctx: ctx, + svcCtx: svcCtx, + Logger: logx.WithContext(ctx), + } +} + +// GetUserInfo 取得會員資訊 +func (l *GetUserInfoLogic) GetUserInfo(in *member.GetUserInfoReq) (*member.GetUserInfoResp, error) { + // todo: add your logic here and delete this line + + return &member.GetUserInfoResp{}, nil +} diff --git a/internal/logic/account/line_code_to_access_token_logic.go b/internal/logic/account/line_code_to_access_token_logic.go new file mode 100644 index 0000000..a36721c --- /dev/null +++ b/internal/logic/account/line_code_to_access_token_logic.go @@ -0,0 +1,31 @@ +package accountlogic + +import ( + "context" + + "app-cloudep-member-server/gen_result/pb/member" + "app-cloudep-member-server/internal/svc" + + "github.com/zeromicro/go-zero/core/logx" +) + +type LineCodeToAccessTokenLogic struct { + ctx context.Context + svcCtx *svc.ServiceContext + logx.Logger +} + +func NewLineCodeToAccessTokenLogic(ctx context.Context, svcCtx *svc.ServiceContext) *LineCodeToAccessTokenLogic { + return &LineCodeToAccessTokenLogic{ + ctx: ctx, + svcCtx: svcCtx, + Logger: logx.WithContext(ctx), + } +} + +// LineCodeToAccessToken Line 驗證相關 +func (l *LineCodeToAccessTokenLogic) LineCodeToAccessToken(in *member.NoneReq) (*member.LineAccessTokenResp, error) { + // todo: add your logic here and delete this line + + return &member.LineAccessTokenResp{}, nil +} diff --git a/internal/logic/account/line_get_profile_by_access_token_logic.go b/internal/logic/account/line_get_profile_by_access_token_logic.go new file mode 100644 index 0000000..594b3fd --- /dev/null +++ b/internal/logic/account/line_get_profile_by_access_token_logic.go @@ -0,0 +1,31 @@ +package accountlogic + +import ( + "context" + + "app-cloudep-member-server/gen_result/pb/member" + "app-cloudep-member-server/internal/svc" + + "github.com/zeromicro/go-zero/core/logx" +) + +type LineGetProfileByAccessTokenLogic struct { + ctx context.Context + svcCtx *svc.ServiceContext + logx.Logger +} + +func NewLineGetProfileByAccessTokenLogic(ctx context.Context, svcCtx *svc.ServiceContext) *LineGetProfileByAccessTokenLogic { + return &LineGetProfileByAccessTokenLogic{ + ctx: ctx, + svcCtx: svcCtx, + Logger: logx.WithContext(ctx), + } +} + +// LineGetProfileByAccessToken Line 驗證相關 +func (l *LineGetProfileByAccessTokenLogic) LineGetProfileByAccessToken(in *member.NoneReq) (*member.LineUserProfile, error) { + // todo: add your logic here and delete this line + + return &member.LineUserProfile{}, nil +} diff --git a/internal/logic/account/list_member_logic.go b/internal/logic/account/list_member_logic.go new file mode 100644 index 0000000..fd11136 --- /dev/null +++ b/internal/logic/account/list_member_logic.go @@ -0,0 +1,31 @@ +package accountlogic + +import ( + "context" + + "app-cloudep-member-server/gen_result/pb/member" + "app-cloudep-member-server/internal/svc" + + "github.com/zeromicro/go-zero/core/logx" +) + +type ListMemberLogic struct { + ctx context.Context + svcCtx *svc.ServiceContext + logx.Logger +} + +func NewListMemberLogic(ctx context.Context, svcCtx *svc.ServiceContext) *ListMemberLogic { + return &ListMemberLogic{ + ctx: ctx, + svcCtx: svcCtx, + Logger: logx.WithContext(ctx), + } +} + +// ListMember 取得會員列表 +func (l *ListMemberLogic) ListMember(in *member.ListUserInfoReq) (*member.ListUserInfoResp, error) { + // todo: add your logic here and delete this line + + return &member.ListUserInfoResp{}, nil +} diff --git a/internal/logic/account/update_status_logic.go b/internal/logic/account/update_status_logic.go new file mode 100644 index 0000000..3bc6ced --- /dev/null +++ b/internal/logic/account/update_status_logic.go @@ -0,0 +1,31 @@ +package accountlogic + +import ( + "context" + + "app-cloudep-member-server/gen_result/pb/member" + "app-cloudep-member-server/internal/svc" + + "github.com/zeromicro/go-zero/core/logx" +) + +type UpdateStatusLogic struct { + ctx context.Context + svcCtx *svc.ServiceContext + logx.Logger +} + +func NewUpdateStatusLogic(ctx context.Context, svcCtx *svc.ServiceContext) *UpdateStatusLogic { + return &UpdateStatusLogic{ + ctx: ctx, + svcCtx: svcCtx, + Logger: logx.WithContext(ctx), + } +} + +// UpdateStatus 修改狀態 +func (l *UpdateStatusLogic) UpdateStatus(in *member.UpdateStatusReq) (*member.OKResp, error) { + // todo: add your logic here and delete this line + + return &member.OKResp{}, nil +} diff --git a/internal/logic/account/update_user_info_logic.go b/internal/logic/account/update_user_info_logic.go new file mode 100644 index 0000000..c1a3806 --- /dev/null +++ b/internal/logic/account/update_user_info_logic.go @@ -0,0 +1,31 @@ +package accountlogic + +import ( + "context" + + "app-cloudep-member-server/gen_result/pb/member" + "app-cloudep-member-server/internal/svc" + + "github.com/zeromicro/go-zero/core/logx" +) + +type UpdateUserInfoLogic struct { + ctx context.Context + svcCtx *svc.ServiceContext + logx.Logger +} + +func NewUpdateUserInfoLogic(ctx context.Context, svcCtx *svc.ServiceContext) *UpdateUserInfoLogic { + return &UpdateUserInfoLogic{ + ctx: ctx, + svcCtx: svcCtx, + Logger: logx.WithContext(ctx), + } +} + +// UpdateUserInfo 更新 User Info +func (l *UpdateUserInfoLogic) UpdateUserInfo(in *member.UpdateUserInfoReq) (*member.OKResp, error) { + // todo: add your logic here and delete this line + + return &member.OKResp{}, nil +} diff --git a/internal/logic/account/update_user_token_logic.go b/internal/logic/account/update_user_token_logic.go new file mode 100644 index 0000000..ad3b9be --- /dev/null +++ b/internal/logic/account/update_user_token_logic.go @@ -0,0 +1,31 @@ +package accountlogic + +import ( + "context" + + "app-cloudep-member-server/gen_result/pb/member" + "app-cloudep-member-server/internal/svc" + + "github.com/zeromicro/go-zero/core/logx" +) + +type UpdateUserTokenLogic struct { + ctx context.Context + svcCtx *svc.ServiceContext + logx.Logger +} + +func NewUpdateUserTokenLogic(ctx context.Context, svcCtx *svc.ServiceContext) *UpdateUserTokenLogic { + return &UpdateUserTokenLogic{ + ctx: ctx, + svcCtx: svcCtx, + Logger: logx.WithContext(ctx), + } +} + +// UpdateUserToken 更新密碼 +func (l *UpdateUserTokenLogic) UpdateUserToken(in *member.UpdateTokenReq) (*member.OKResp, error) { + // todo: add your logic here and delete this line + + return &member.OKResp{}, nil +} diff --git a/internal/logic/account/verify_google_auth_result_logic.go b/internal/logic/account/verify_google_auth_result_logic.go new file mode 100644 index 0000000..842fb04 --- /dev/null +++ b/internal/logic/account/verify_google_auth_result_logic.go @@ -0,0 +1,31 @@ +package accountlogic + +import ( + "context" + + "app-cloudep-member-server/gen_result/pb/member" + "app-cloudep-member-server/internal/svc" + + "github.com/zeromicro/go-zero/core/logx" +) + +type VerifyGoogleAuthResultLogic struct { + ctx context.Context + svcCtx *svc.ServiceContext + logx.Logger +} + +func NewVerifyGoogleAuthResultLogic(ctx context.Context, svcCtx *svc.ServiceContext) *VerifyGoogleAuthResultLogic { + return &VerifyGoogleAuthResultLogic{ + ctx: ctx, + svcCtx: svcCtx, + Logger: logx.WithContext(ctx), + } +} + +// VerifyGoogleAuthResult 驗證 google 登入是否有效 +func (l *VerifyGoogleAuthResultLogic) VerifyGoogleAuthResult(in *member.VerifyAuthResultReq) (*member.VerifyAuthResultResp, error) { + // todo: add your logic here and delete this line + + return &member.VerifyAuthResultResp{}, nil +} diff --git a/internal/logic/account/verify_platform_auth_result_logic.go b/internal/logic/account/verify_platform_auth_result_logic.go new file mode 100644 index 0000000..2007051 --- /dev/null +++ b/internal/logic/account/verify_platform_auth_result_logic.go @@ -0,0 +1,31 @@ +package accountlogic + +import ( + "context" + + "app-cloudep-member-server/gen_result/pb/member" + "app-cloudep-member-server/internal/svc" + + "github.com/zeromicro/go-zero/core/logx" +) + +type VerifyPlatformAuthResultLogic struct { + ctx context.Context + svcCtx *svc.ServiceContext + logx.Logger +} + +func NewVerifyPlatformAuthResultLogic(ctx context.Context, svcCtx *svc.ServiceContext) *VerifyPlatformAuthResultLogic { + return &VerifyPlatformAuthResultLogic{ + ctx: ctx, + svcCtx: svcCtx, + Logger: logx.WithContext(ctx), + } +} + +// VerifyPlatformAuthResult 驗證 google 登入是否有效 +func (l *VerifyPlatformAuthResultLogic) VerifyPlatformAuthResult(in *member.VerifyAuthResultReq) (*member.VerifyAuthResultResp, error) { + // todo: add your logic here and delete this line + + return &member.VerifyAuthResultResp{}, nil +} diff --git a/internal/logic/account/verify_refresh_code_logic.go b/internal/logic/account/verify_refresh_code_logic.go new file mode 100644 index 0000000..0cfdfa3 --- /dev/null +++ b/internal/logic/account/verify_refresh_code_logic.go @@ -0,0 +1,31 @@ +package accountlogic + +import ( + "context" + + "app-cloudep-member-server/gen_result/pb/member" + "app-cloudep-member-server/internal/svc" + + "github.com/zeromicro/go-zero/core/logx" +) + +type VerifyRefreshCodeLogic struct { + ctx context.Context + svcCtx *svc.ServiceContext + logx.Logger +} + +func NewVerifyRefreshCodeLogic(ctx context.Context, svcCtx *svc.ServiceContext) *VerifyRefreshCodeLogic { + return &VerifyRefreshCodeLogic{ + ctx: ctx, + svcCtx: svcCtx, + Logger: logx.WithContext(ctx), + } +} + +// VerifyRefreshCode 驗證忘記密碼 token +func (l *VerifyRefreshCodeLogic) VerifyRefreshCode(in *member.VerifyRefreshCodeReq) (*member.OKResp, error) { + // todo: add your logic here and delete this line + + return &member.OKResp{}, nil +} diff --git a/internal/server/account/account_server.go b/internal/server/account/account_server.go new file mode 100644 index 0000000..f08bc35 --- /dev/null +++ b/internal/server/account/account_server.go @@ -0,0 +1,138 @@ +// Code generated by goctl. DO NOT EDIT. +// goctl 1.7.3 +// Source: member.proto + +package server + +import ( + "context" + + "app-cloudep-member-server/gen_result/pb/member" + accountlogic "app-cloudep-member-server/internal/logic/account" + "app-cloudep-member-server/internal/svc" +) + +type AccountServer struct { + svcCtx *svc.ServiceContext + member.UnimplementedAccountServer +} + +func NewAccountServer(svcCtx *svc.ServiceContext) *AccountServer { + return &AccountServer{ + svcCtx: svcCtx, + } +} + +// CreateUserAccount 建立帳號與密碼 -> 可登入,但可不可以做其他事情看業務流程,也可以只註冊就好 +func (s *AccountServer) CreateUserAccount(ctx context.Context, in *member.CreateLoginUserReq) (*member.OKResp, error) { + l := accountlogic.NewCreateUserAccountLogic(ctx, s.svcCtx) + return l.CreateUserAccount(in) +} + +// GetUserAccountInfo 取得帳號密碼資料 +func (s *AccountServer) GetUserAccountInfo(ctx context.Context, in *member.GetUIDByAccountReq) (*member.GetAccountInfoResp, error) { + l := accountlogic.NewGetUserAccountInfoLogic(ctx, s.svcCtx) + return l.GetUserAccountInfo(in) +} + +// UpdateUserToken 更新密碼 +func (s *AccountServer) UpdateUserToken(ctx context.Context, in *member.UpdateTokenReq) (*member.OKResp, error) { + l := accountlogic.NewUpdateUserTokenLogic(ctx, s.svcCtx) + return l.UpdateUserToken(in) +} + +// GetUIDByAccount 用帳號換取 UID +func (s *AccountServer) GetUIDByAccount(ctx context.Context, in *member.GetUIDByAccountReq) (*member.GetUIDByAccountResp, error) { + l := accountlogic.NewGetUIDByAccountLogic(ctx, s.svcCtx) + return l.GetUIDByAccount(in) +} + +// BindAccount 綁定帳號 -> account bind to UID +func (s *AccountServer) BindAccount(ctx context.Context, in *member.BindingUserReq) (*member.BindingUserResp, error) { + l := accountlogic.NewBindAccountLogic(ctx, s.svcCtx) + return l.BindAccount(in) +} + +// BindUserInfo 初次,綁定 User Info +func (s *AccountServer) BindUserInfo(ctx context.Context, in *member.CreateUserInfoReq) (*member.OKResp, error) { + l := accountlogic.NewBindUserInfoLogic(ctx, s.svcCtx) + return l.BindUserInfo(in) +} + +// BindVerifyEmail 綁定 Email +func (s *AccountServer) BindVerifyEmail(ctx context.Context, in *member.BindVerifyEmailReq) (*member.OKResp, error) { + l := accountlogic.NewBindVerifyEmailLogic(ctx, s.svcCtx) + return l.BindVerifyEmail(in) +} + +// BindVerifyPhone 綁定 Phone +func (s *AccountServer) BindVerifyPhone(ctx context.Context, in *member.BindVerifyPhoneReq) (*member.OKResp, error) { + l := accountlogic.NewBindVerifyPhoneLogic(ctx, s.svcCtx) + return l.BindVerifyPhone(in) +} + +// UpdateUserInfo 更新 User Info +func (s *AccountServer) UpdateUserInfo(ctx context.Context, in *member.UpdateUserInfoReq) (*member.OKResp, error) { + l := accountlogic.NewUpdateUserInfoLogic(ctx, s.svcCtx) + return l.UpdateUserInfo(in) +} + +// UpdateStatus 修改狀態 +func (s *AccountServer) UpdateStatus(ctx context.Context, in *member.UpdateStatusReq) (*member.OKResp, error) { + l := accountlogic.NewUpdateStatusLogic(ctx, s.svcCtx) + return l.UpdateStatus(in) +} + +// GetUserInfo 取得會員資訊 +func (s *AccountServer) GetUserInfo(ctx context.Context, in *member.GetUserInfoReq) (*member.GetUserInfoResp, error) { + l := accountlogic.NewGetUserInfoLogic(ctx, s.svcCtx) + return l.GetUserInfo(in) +} + +// ListMember 取得會員列表 +func (s *AccountServer) ListMember(ctx context.Context, in *member.ListUserInfoReq) (*member.ListUserInfoResp, error) { + l := accountlogic.NewListMemberLogic(ctx, s.svcCtx) + return l.ListMember(in) +} + +// GenerateRefreshCode 這個帳號驗證碼(十分鐘),通用的 +func (s *AccountServer) GenerateRefreshCode(ctx context.Context, in *member.GenerateRefreshCodeReq) (*member.GenerateRefreshCodeResp, error) { + l := accountlogic.NewGenerateRefreshCodeLogic(ctx, s.svcCtx) + return l.GenerateRefreshCode(in) +} + +// VerifyRefreshCode 驗證忘記密碼 token +func (s *AccountServer) VerifyRefreshCode(ctx context.Context, in *member.VerifyRefreshCodeReq) (*member.OKResp, error) { + l := accountlogic.NewVerifyRefreshCodeLogic(ctx, s.svcCtx) + return l.VerifyRefreshCode(in) +} + +// CheckRefreshCode 驗證忘記密碼 token 不刪除,只確認) +func (s *AccountServer) CheckRefreshCode(ctx context.Context, in *member.VerifyRefreshCodeReq) (*member.OKResp, error) { + l := accountlogic.NewCheckRefreshCodeLogic(ctx, s.svcCtx) + return l.CheckRefreshCode(in) +} + +// VerifyGoogleAuthResult 驗證 google 登入是否有效 +func (s *AccountServer) VerifyGoogleAuthResult(ctx context.Context, in *member.VerifyAuthResultReq) (*member.VerifyAuthResultResp, error) { + l := accountlogic.NewVerifyGoogleAuthResultLogic(ctx, s.svcCtx) + return l.VerifyGoogleAuthResult(in) +} + +// VerifyPlatformAuthResult 驗證 google 登入是否有效 +func (s *AccountServer) VerifyPlatformAuthResult(ctx context.Context, in *member.VerifyAuthResultReq) (*member.VerifyAuthResultResp, error) { + l := accountlogic.NewVerifyPlatformAuthResultLogic(ctx, s.svcCtx) + return l.VerifyPlatformAuthResult(in) +} + +// LineCodeToAccessToken Line 驗證相關 +func (s *AccountServer) LineCodeToAccessToken(ctx context.Context, in *member.NoneReq) (*member.LineAccessTokenResp, error) { + l := accountlogic.NewLineCodeToAccessTokenLogic(ctx, s.svcCtx) + return l.LineCodeToAccessToken(in) +} + +// LineGetProfileByAccessToken Line 驗證相關 +func (s *AccountServer) LineGetProfileByAccessToken(ctx context.Context, in *member.NoneReq) (*member.LineUserProfile, error) { + l := accountlogic.NewLineGetProfileByAccessTokenLogic(ctx, s.svcCtx) + return l.LineGetProfileByAccessToken(in) +} diff --git a/internal/svc/service_context.go b/internal/svc/service_context.go new file mode 100644 index 0000000..427669b --- /dev/null +++ b/internal/svc/service_context.go @@ -0,0 +1,114 @@ +package svc + +import ( + "app-cloudep-member-server/internal/config" + cfg "app-cloudep-member-server/pkg/domain/config" + "app-cloudep-member-server/pkg/domain/usecase" + "app-cloudep-member-server/pkg/repository" + uc "app-cloudep-member-server/pkg/usecase" + "code.30cm.net/digimon/library-go/errs" + "code.30cm.net/digimon/library-go/errs/code" + mgo "code.30cm.net/digimon/library-go/mongo" + vi "code.30cm.net/digimon/library-go/validator" + "github.com/zeromicro/go-zero/core/stores/cache" + "github.com/zeromicro/go-zero/core/stores/mon" + "github.com/zeromicro/go-zero/core/stores/redis" +) + +type ServiceContext struct { + Config config.Config + Validate vi.Validate + AccountUseCase usecase.AccountUseCase +} + +func NewServiceContext(c config.Config) *ServiceContext { + // 設置 + errs.Scope = code.CloudEPMember + + return &ServiceContext{ + Config: c, + Validate: vi.MustValidator(vi.WithAccount("account")), + AccountUseCase: NewAccountUC(&c), + } +} + +func NewAccountUC(c *config.Config) usecase.AccountUseCase { + // 準備Mongo Config + conf := &mgo.Conf{ + Schema: c.Mongo.Schema, + Host: c.Mongo.Host, + Database: c.Mongo.Database, + MaxStaleness: c.Mongo.MaxStaleness, + MaxPoolSize: c.Mongo.MaxPoolSize, + MinPoolSize: c.Mongo.MinPoolSize, + MaxConnIdleTime: c.Mongo.MaxConnIdleTime, + Compressors: c.Mongo.Compressors, + EnableStandardReadWriteSplitMode: c.Mongo.EnableStandardReadWriteSplitMode, + ConnectTimeoutMs: c.Mongo.ConnectTimeoutMs, + } + + // 快取選項 + cacheOpts := []cache.Option{ + cache.WithExpiry(c.CacheExpireTime), + cache.WithNotFoundExpiry(c.CacheWithNotFoundExpiry), + } + dbOpts := []mon.Option{ + mgo.SetCustomDecimalType(), + mgo.InitMongoOptions(*conf), + } + + newRedis, err := redis.NewRedis(c.RedisConf, redis.Cluster()) + if err != nil { + panic(err) + } + + return uc.MustMemberUseCase(uc.MemberUseCaseParam{ + Account: repository.NewAccountRepository(repository.AccountRepositoryParam{ + Conf: conf, + CacheConf: c.Cache, + CacheOpts: cacheOpts, + DbOpts: dbOpts, + }), + User: repository.NewUserRepository(repository.UserRepositoryParam{ + Conf: conf, + CacheConf: c.Cache, + CacheOpts: cacheOpts, + DbOpts: dbOpts, + }), + AccountUID: repository.NewAccountUIDRepository(repository.AccountUIDRepositoryParam{ + Conf: conf, + CacheConf: c.Cache, + CacheOpts: cacheOpts, + DbOpts: dbOpts, + }), + VerifyCodeModel: repository.NewVerifyCodeRepository(newRedis), + GenerateUID: repository.NewAutoIDRepository(repository.AutoIDRepositoryParam{ + Conf: conf, + DbOpts: dbOpts, + }), + Config: prepareCfg(c), + }) +} + +func prepareCfg(c *config.Config) cfg.Config { + return cfg.Config{ + Bcrypt: struct{ Cost int }{Cost: c.Bcrypt.Cost}, + GoogleAuth: struct { + ClientID string + AuthURL string + }{ + ClientID: c.GoogleAuth.ClientID, + AuthURL: c.GoogleAuth.AuthURL, + }, + + LineAuth: struct { + ClientID string + ClientSecret string + RedirectURI string + }{ + ClientID: c.LineAuth.ClientID, + ClientSecret: c.LineAuth.ClientSecret, + RedirectURI: c.LineAuth.RedirectURI, + }, + } +} diff --git a/member.go b/member.go new file mode 100644 index 0000000..ccf5344 --- /dev/null +++ b/member.go @@ -0,0 +1,40 @@ +package main + +import ( + "flag" + + "github.com/zeromicro/go-zero/core/logx" + + "app-cloudep-member-server/gen_result/pb/member" + "app-cloudep-member-server/internal/config" + accountServer "app-cloudep-member-server/internal/server/account" + "app-cloudep-member-server/internal/svc" + + "github.com/zeromicro/go-zero/core/conf" + "github.com/zeromicro/go-zero/core/service" + "github.com/zeromicro/go-zero/zrpc" + "google.golang.org/grpc" + "google.golang.org/grpc/reflection" +) + +var configFile = flag.String("f", "etc/member.yaml", "the config file") + +func main() { + flag.Parse() + + var c config.Config + conf.MustLoad(*configFile, &c) + ctx := svc.NewServiceContext(c) + + s := zrpc.MustNewServer(c.RpcServerConf, func(grpcServer *grpc.Server) { + member.RegisterAccountServer(grpcServer, accountServer.NewAccountServer(ctx)) + + if c.Mode == service.DevMode || c.Mode == service.TestMode { + reflection.Register(grpcServer) + } + }) + defer s.Stop() + + logx.Infof("Starting rpc server at %s...\n", c.ListenOn) + s.Start() +} diff --git a/pkg/domain/config/config.go b/pkg/domain/config/config.go new file mode 100644 index 0000000..ee0cacf --- /dev/null +++ b/pkg/domain/config/config.go @@ -0,0 +1,19 @@ +package config + +type Config struct { + // 密碼加密層數 + Bcrypt struct { + Cost int + } + + GoogleAuth struct { + ClientID string + AuthURL string + } + + LineAuth struct { + ClientID string + ClientSecret string + RedirectURI string + } +} diff --git a/pkg/domain/const.go b/pkg/domain/const.go new file mode 100644 index 0000000..4188b5a --- /dev/null +++ b/pkg/domain/const.go @@ -0,0 +1 @@ +package domain diff --git a/pkg/domain/entity/account.go b/pkg/domain/entity/account.go new file mode 100644 index 0000000..d3b2fb1 --- /dev/null +++ b/pkg/domain/entity/account.go @@ -0,0 +1,20 @@ +package entity + +import ( + "app-cloudep-member-server/pkg/domain/member" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +type Account struct { + ID primitive.ObjectID `bson:"_id,omitempty" json:"id,omitempty"` + LoginID string `bson:"login_id"` + Token string `bson:"token"` + Platform member.Platform `bson:"platform"` // 平台類型 1. platform 2. google 3. line 4. apple + UpdateAt *int64 `bson:"update_at,omitempty" json:"update_at,omitempty"` + CreateAt *int64 `bson:"create_at,omitempty" json:"create_at,omitempty"` +} + +func (a *Account) CollectionName() string { + return "account" +} diff --git a/pkg/domain/entity/account_uid_table.go b/pkg/domain/entity/account_uid_table.go new file mode 100644 index 0000000..2183d99 --- /dev/null +++ b/pkg/domain/entity/account_uid_table.go @@ -0,0 +1,20 @@ +package entity + +import ( + "app-cloudep-member-server/pkg/domain/member" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +type AccountUID struct { + ID primitive.ObjectID `bson:"_id,omitempty" json:"id,omitempty"` + LoginID string `bson:"login_id"` + UID string `bson:"uid"` + Type member.AccountType `bson:"type"` + UpdateAt *int64 `bson:"update_at,omitempty" json:"update_at,omitempty"` + CreateAt *int64 `bson:"create_at,omitempty" json:"create_at,omitempty"` +} + +func (a *AccountUID) CollectionName() string { + return "account_uid_binding" +} diff --git a/pkg/domain/entity/auto_id.go b/pkg/domain/entity/auto_id.go new file mode 100644 index 0000000..f2d1c99 --- /dev/null +++ b/pkg/domain/entity/auto_id.go @@ -0,0 +1,15 @@ +package entity + +import "go.mongodb.org/mongo-driver/bson/primitive" + +type AutoID struct { + ID primitive.ObjectID `bson:"_id,omitempty" json:"id,omitempty"` + Name string `bson:"name,omitempty" json:"name,omitempty"` + Counter uint64 `bson:"counter" json:"counter"` + UpdateAt *int64 `bson:"update_at,omitempty" json:"update_at,omitempty"` + CreateAt *int64 `bson:"create_at,omitempty" json:"create_at,omitempty"` +} + +func (a *AutoID) CollectionName() string { + return "count" +} diff --git a/pkg/domain/entity/user.go b/pkg/domain/entity/user.go new file mode 100644 index 0000000..2e987a1 --- /dev/null +++ b/pkg/domain/entity/user.go @@ -0,0 +1,30 @@ +package entity + +import ( + "app-cloudep-member-server/pkg/domain/member" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +type User struct { + ID primitive.ObjectID `bson:"_id,omitempty" json:"id,omitempty"` + UID string `bson:"uid"` // 用戶 UID + AvatarURL *string `bson:"avatar_url,omitempty"` // 頭像 URL(可選) + FullName *string `bson:"full_name,omitempty"` // 用戶全名 + Nickname *string `bson:"nickname,omitempty"` // 暱稱(可選) + GenderCode *int64 `bson:"gender_code,omitempty"` // 性別代碼 + Birthdate *int64 `bson:"birthdate,omitempty"` // 生日 (格式: 19930417) + Address *string `bson:"address,omitempty"` // 地址 + AlarmCategory member.AlarmType `bson:"alarm_category"` // 告警狀態 + UserStatus member.Status `bson:"user_status"` // 用戶狀態 + PreferredLanguage string `bson:"preferred_language"` // 使用語言 + Currency string `bson:"currency"` // 使用幣種 + PhoneNumber *string `bson:"phone_number,omitempty"` // 電話 (驗證後才會出現) + Email *string `bson:"email,omitempty"` // 信箱 (驗證後才會出現) + UpdateAt *int64 `bson:"update_at,omitempty" json:"update_at,omitempty"` + CreateAt *int64 `bson:"create_at,omitempty" json:"create_at,omitempty"` +} + +func (u *User) CollectionName() string { + return "user_info" +} diff --git a/pkg/domain/errors.go b/pkg/domain/errors.go new file mode 100644 index 0000000..459ddcd --- /dev/null +++ b/pkg/domain/errors.go @@ -0,0 +1,33 @@ +package domain + +import "code.30cm.net/digimon/library-go/errs" + +// Verify Error Code +const ( + FailedToVerifyGoogle errs.ErrorCode = iota + 1 + FailedToVerifyGoogleTimeout + FailedToVerifyGoogleHTTPCode + FailedToVerifyGoogleTokenExpired + FailedToVerifyGoogleInvalidAudience + FailedToVerifyLine +) + +// PWS Error Code +const ( + HashPasswordErrorCode = 10 + iota + InsertAccountErrorCode + BindingUserTabletErrorCode + FailedToFindAccountErrorCode + FailedToBindAccountErrorCode + FailedToIncAccountErrorCode + FailedFindUIDByLoginIDErrorCode + FailedFindOneByAccountErrorCode + FailedToUpdatePasswordErrorCode + FailedToFindUserErrorCode + FailedToUpdateUserErrorCode + FailedToUpdateUserStatusErrorCode + FailedToGetUserInfoErrorCode + FailedToGetVerifyCodeErrorCode + FailedToGetCodeOnRedisErrorCode + FailedToGetCodeCorrectErrorCode +) diff --git a/pkg/domain/member/account_type.go b/pkg/domain/member/account_type.go new file mode 100644 index 0000000..8570b7c --- /dev/null +++ b/pkg/domain/member/account_type.go @@ -0,0 +1,25 @@ +package member + +type AccountType int32 + +const ( + AccountTypeNone AccountType = -1 + AccountTypePhone AccountType = 1 // 手機 + AccountTypeMail AccountType = 2 // 信箱 + AccountTypeDefine AccountType = 3 // 自定義帳號 +) + +var convAccountTypeCode = map[string]AccountType{ + "phone": AccountTypePhone, + "email": AccountTypeMail, + "platform": AccountTypeDefine, +} + +func GetAccountTypeByCode(code string) AccountType { + result, ok := convAccountTypeCode[code] + if !ok { + return AccountTypeNone + } + + return result +} diff --git a/pkg/domain/member/account_type_test.go b/pkg/domain/member/account_type_test.go new file mode 100644 index 0000000..baa6458 --- /dev/null +++ b/pkg/domain/member/account_type_test.go @@ -0,0 +1,26 @@ +package member + +import "testing" + +func TestGetAccountTypeByCode(t *testing.T) { + tests := []struct { + code string + expected AccountType + }{ + {"phone", AccountTypePhone}, // 測試有效值: 手機 + {"email", AccountTypeMail}, // 測試有效值: 信箱 + {"platform", AccountTypeDefine}, // 測試有效值: 自定義帳號 + {"unknown", AccountTypeNone}, // 測試無效值 + {"", AccountTypeNone}, // 測試空字串 + {"PHONE", AccountTypeNone}, // 測試大小寫不匹配 + } + + for _, tt := range tests { + t.Run(tt.code, func(t *testing.T) { + result := GetAccountTypeByCode(tt.code) + if result != tt.expected { + t.Errorf("GetAccountTypeByCode(%v) = %v, want %v", tt.code, result, tt.expected) + } + }) + } +} diff --git a/pkg/domain/member/alert_type.go b/pkg/domain/member/alert_type.go new file mode 100644 index 0000000..01c7ad1 --- /dev/null +++ b/pkg/domain/member/alert_type.go @@ -0,0 +1,25 @@ +package member + +// AlarmType 警報類型 +type AlarmType int32 + +func (a *AlarmType) CodeToString() string { + result, ok := verifyAlarmMap[*a] + if !ok { + return "" + } + + return result +} + +var verifyAlarmMap = map[AlarmType]string{ + AlarmUninitialized: "uninitialized", // 初始狀態(異常) + AlarmNoAlert: "no_alert", // 未告警 + AlarmSystem: "system_alert", // 系統告警中 +} + +const ( + AlarmUninitialized AlarmType = 0 // 初始狀態(異常) + AlarmNoAlert AlarmType = 1 // 未告警 + AlarmSystem AlarmType = 2 // 系統告警中 +) diff --git a/pkg/domain/member/alert_type_test.go b/pkg/domain/member/alert_type_test.go new file mode 100644 index 0000000..a8a463d --- /dev/null +++ b/pkg/domain/member/alert_type_test.go @@ -0,0 +1,27 @@ +package member + +import ( + "testing" +) + +func TestAlarmType_CodeToString(t *testing.T) { + tests := []struct { + alarmType AlarmType + expected string + }{ + {AlarmUninitialized, "uninitialized"}, // 測試初始狀態 + {AlarmNoAlert, "no_alert"}, // 測試未告警狀態 + {AlarmSystem, "system_alert"}, // 測試系統告警 + {AlarmType(999), ""}, // 測試不存在的狀態 + {AlarmType(-1), ""}, // 測試無效負數狀態 + } + + for _, tt := range tests { + t.Run(tt.expected, func(t *testing.T) { + result := tt.alarmType.CodeToString() + if result != tt.expected { + t.Errorf("CodeToString() = %v, want %v", result, tt.expected) + } + }) + } +} diff --git a/pkg/domain/member/generate_code_type.go b/pkg/domain/member/generate_code_type.go new file mode 100644 index 0000000..5453aaa --- /dev/null +++ b/pkg/domain/member/generate_code_type.go @@ -0,0 +1,40 @@ +package member + +type GenerateCodeType int64 + +const ( + GenerateCodeTypeNone GenerateCodeType = -1 + GenerateCodeTypeEmail GenerateCodeType = 1 // email 驗證碼 + GenerateCodeTypePhone GenerateCodeType = 2 // phone 驗證碼 + GenerateCodeTypeForgetPassword GenerateCodeType = 3 // 忘記密碼 +) + +var codeMap = map[GenerateCodeType]string{ + 1: "email", + 2: "phone", + 3: "forget_email", +} + +func GetCodeNameByCode(code GenerateCodeType) (string, bool) { + res, ok := codeMap[code] + if !ok { + return "", false + } + + return res, true +} + +var generateCodeTypeMap = map[string]GenerateCodeType{ + "email": GenerateCodeTypeEmail, + "phone": GenerateCodeTypePhone, + "forget_email": GenerateCodeTypeForgetPassword, +} + +func GetGetCodeNameByCode(code string) GenerateCodeType { + result, ok := generateCodeTypeMap[code] + if !ok { + return GenerateCodeTypeNone + } + + return result +} diff --git a/pkg/domain/member/generate_code_type_test.go b/pkg/domain/member/generate_code_type_test.go new file mode 100644 index 0000000..d9c80ba --- /dev/null +++ b/pkg/domain/member/generate_code_type_test.go @@ -0,0 +1,54 @@ +package member + +import ( + "testing" +) + +func TestGetCodeNameByCode(t *testing.T) { + tests := []struct { + code GenerateCodeType + expected string + ok bool + }{ + {GenerateCodeTypeEmail, "email", true}, + {GenerateCodeTypePhone, "phone", true}, + {GenerateCodeTypeForgetPassword, "forget_email", true}, + {GenerateCodeTypeNone, "", false}, // 無效代碼 + {GenerateCodeType(999), "", false}, // 無效代碼 + {GenerateCodeType(-999), "", false}, // 無效代碼 + } + + for _, tt := range tests { + t.Run(tt.expected, func(t *testing.T) { + result, ok := GetCodeNameByCode(tt.code) + if result != tt.expected || ok != tt.ok { + t.Errorf("GetCodeNameByCode(%v) = (%v, %v), want (%v, %v)", + tt.code, result, ok, tt.expected, tt.ok) + } + }) + } +} + +func TestGetGetCodeNameByCode(t *testing.T) { + tests := []struct { + name string + expected GenerateCodeType + }{ + {"email", GenerateCodeTypeEmail}, + {"phone", GenerateCodeTypePhone}, + {"forget_email", GenerateCodeTypeForgetPassword}, + {"unknown", GenerateCodeTypeNone}, // 無效名稱 + {"", GenerateCodeTypeNone}, // 空名稱 + {"123", GenerateCodeTypeNone}, // 無效名稱 + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := GetGetCodeNameByCode(tt.name) + if result != tt.expected { + t.Errorf("GetGetCodeNameByCode(%v) = %v, want %v", + tt.name, result, tt.expected) + } + }) + } +} diff --git a/pkg/domain/member/platform.go b/pkg/domain/member/platform.go new file mode 100644 index 0000000..483b76e --- /dev/null +++ b/pkg/domain/member/platform.go @@ -0,0 +1,57 @@ +package member + +const ( + Digimon Platform = 1 + iota + Google + Line + Apple +) + +const ( + PlatformNone Platform = -1 +) + +const ( + DigimonString = "platform" + GoogleString = "google" + LineString = "line" + AppleString = "apple" +) + +type Platform int8 + +func (p Platform) ToInt64() int64 { + return int64(p) +} + +// ToString - 將 Platform 轉為文字 +func (p Platform) ToString() string { + if result, ok := platformToString[p]; ok { + return result + } + + return "" +} + +var platformToString = map[Platform]string{ + Digimon: DigimonString, + Google: GoogleString, + Line: LineString, + Apple: AppleString, +} + +var stringToPlatform = map[string]Platform{ + DigimonString: Digimon, + GoogleString: Google, + LineString: Line, + AppleString: Apple, +} + +// GetPlatformByPlatformCode - 從文字轉為 Platform +func GetPlatformByPlatformCode(code string) Platform { + if result, ok := stringToPlatform[code]; ok { + return result + } + + return PlatformNone +} diff --git a/pkg/domain/member/platform_test.go b/pkg/domain/member/platform_test.go new file mode 100644 index 0000000..8c82f7b --- /dev/null +++ b/pkg/domain/member/platform_test.go @@ -0,0 +1,66 @@ +package member + +import "testing" + +func TestPlatform_ToInt64(t *testing.T) { + tests := []struct { + platform Platform + expected int64 + }{ + {Digimon, 1}, + {Google, 2}, + {Line, 3}, + {Apple, 4}, + {PlatformNone, -1}, + } + + for _, tt := range tests { + t.Run(tt.platform.ToString(), func(t *testing.T) { + if result := tt.platform.ToInt64(); result != tt.expected { + t.Errorf("ToInt64() = %v, want %v", result, tt.expected) + } + }) + } +} + +func TestPlatform_ToString(t *testing.T) { + tests := []struct { + platform Platform + expected string + }{ + {Digimon, DigimonString}, + {Google, GoogleString}, + {Line, LineString}, + {Apple, AppleString}, + {PlatformNone, ""}, + } + + for _, tt := range tests { + t.Run(tt.expected, func(t *testing.T) { + if result := tt.platform.ToString(); result != tt.expected { + t.Errorf("ToString() = %v, want %v", result, tt.expected) + } + }) + } +} + +func TestGetPlatformByPlatformCode(t *testing.T) { + tests := []struct { + code string + expected Platform + }{ + {DigimonString, Digimon}, + {GoogleString, Google}, + {LineString, Line}, + {AppleString, Apple}, + {"unknown", PlatformNone}, // 測試不存在的 Platform + } + + for _, tt := range tests { + t.Run(tt.code, func(t *testing.T) { + if result := GetPlatformByPlatformCode(tt.code); result != tt.expected { + t.Errorf("GetPlatformByPlatformCode(%v) = %v, want %v", tt.code, result, tt.expected) + } + }) + } +} diff --git a/pkg/domain/member/status.go b/pkg/domain/member/status.go new file mode 100644 index 0000000..9474bca --- /dev/null +++ b/pkg/domain/member/status.go @@ -0,0 +1,31 @@ +package member + +// Status 會員狀態 +type Status int32 + +func (s *Status) CodeToString() string { + result, ok := accountStatusMap[*s] + if !ok { + return "" + } + + return result +} + +var accountStatusMap = map[Status]string{ + AccountStatusUninitialized: "uninitialized", // 初始狀態(異常) + AccountStatusUnverified: "unverified", // 尚未完成驗證 + AccountStatusActive: "active", // 帳號啟用中 + AccountStatusSuspended: "suspended", // 帳號停權中 +} + +func (s *Status) ToInt32() int32 { + return int32(*s) +} + +const ( + AccountStatusUninitialized Status = 0 // 初始狀態(異常) + AccountStatusUnverified Status = 1 // 尚未驗證 + AccountStatusActive Status = 2 // 帳號啟用中 + AccountStatusSuspended Status = 3 // 帳號停權中 +) diff --git a/pkg/domain/member/status_test.go b/pkg/domain/member/status_test.go new file mode 100644 index 0000000..ae59907 --- /dev/null +++ b/pkg/domain/member/status_test.go @@ -0,0 +1,45 @@ +package member + +import "testing" + +func TestStatus_CodeToString(t *testing.T) { + tests := []struct { + status Status + expected string + }{ + {AccountStatusUninitialized, "uninitialized"}, + {AccountStatusUnverified, "unverified"}, + {AccountStatusActive, "active"}, + {AccountStatusSuspended, "suspended"}, + {Status(999), ""}, // 測試不存在的狀態 + } + + for _, tt := range tests { + t.Run(tt.expected, func(t *testing.T) { + if result := tt.status.CodeToString(); result != tt.expected { + t.Errorf("CodeToString() = %v, want %v", result, tt.expected) + } + }) + } +} + +func TestStatus_ToInt32(t *testing.T) { + tests := []struct { + status Status + expected int32 + }{ + {AccountStatusUninitialized, 0}, + {AccountStatusUnverified, 1}, + {AccountStatusActive, 2}, + {AccountStatusSuspended, 3}, + {Status(999), 999}, // 測試不存在的狀態 + } + + for _, tt := range tests { + t.Run(string(tt.expected), func(t *testing.T) { + if result := tt.status.ToInt32(); result != tt.expected { + t.Errorf("ToInt32() = %v, want %v", result, tt.expected) + } + }) + } +} diff --git a/pkg/domain/redis.go b/pkg/domain/redis.go new file mode 100644 index 0000000..10086ef --- /dev/null +++ b/pkg/domain/redis.go @@ -0,0 +1,46 @@ +package domain + +import ( + "fmt" + "strings" +) + +type RedisKey string + +const ( + AccountRedisKey RedisKey = "account" + AccountUIDRedisKey RedisKey = "account_uid" + UserRedisKey RedisKey = "user" + MemberPrefixRedisKey = "member" +) + +func (key RedisKey) ToString() string { + return "member:" + string(key) +} + +func (key RedisKey) With(s ...string) RedisKey { + parts := append([]string{string(key)}, s...) + + return RedisKey(strings.Join(parts, ":")) +} + +func GetAccountRedisKey(id string) string { + return AccountRedisKey.With(id).ToString() +} + +func GetAccountUIDRedisKey(id string) string { + return AccountUIDRedisKey.With(id).ToString() +} + +func GetUserRedisKey(id string) string { + return UserRedisKey.With(id).ToString() +} + +var ( + // checkVerifyKey 驗證碼驗證 + checkVerifyKey = fmt.Sprintf("%s:verify:", MemberPrefixRedisKey) +) + +func GetCheckVerifyKey(codeType, account string) string { + return fmt.Sprintf("%s%s:%s", checkVerifyKey, codeType, account) +} diff --git a/pkg/domain/repository/account.go b/pkg/domain/repository/account.go new file mode 100644 index 0000000..af04a17 --- /dev/null +++ b/pkg/domain/repository/account.go @@ -0,0 +1,26 @@ +package repository + +import ( + "app-cloudep-member-server/pkg/domain/entity" + "context" + + "go.mongodb.org/mongo-driver/mongo" +) + +type AccountRepository interface { + Insert(ctx context.Context, data *entity.Account) error + FindOne(ctx context.Context, id string) (*entity.Account, error) + Update(ctx context.Context, data *entity.Account) (*mongo.UpdateResult, error) + Delete(ctx context.Context, id string) (int64, error) + FindOneByAccount(ctx context.Context, loginID string) (*entity.Account, error) + UpdateTokenByLoginID(ctx context.Context, account string, token string) error + AccountIndexUP +} + +type AccountIndexUP interface { + Index20241226001UP(ctx context.Context) (*mongo.Cursor, error) +} + +// type AccountIndexDown interface { +// Index20241226001Down(ctx context.Context) +// } diff --git a/pkg/domain/repository/account_uid.go b/pkg/domain/repository/account_uid.go new file mode 100644 index 0000000..a75de31 --- /dev/null +++ b/pkg/domain/repository/account_uid.go @@ -0,0 +1,23 @@ +package repository + +import ( + "app-cloudep-member-server/pkg/domain/entity" + "context" + + "go.mongodb.org/mongo-driver/mongo" +) + +type AccountUIDRepository interface { + Insert(ctx context.Context, data *entity.AccountUID) error + FindOne(ctx context.Context, id string) (*entity.AccountUID, error) + Update(ctx context.Context, data *entity.AccountUID) (*mongo.UpdateResult, error) + Delete(ctx context.Context, id string) (int64, error) + FindUIDByLoginID(ctx context.Context, loginID string) (*entity.AccountUID, error) + AccountUIDIndexUP +} + +// Index20241226001Up 這樣方便管理,知道是 這張表 20241226 第一個新增的 index 之後有版本遷移也可以這樣寫,先快速這樣做,等之後找到更好的迭代工具,在用 + +type AccountUIDIndexUP interface { + Index20241226001UP(ctx context.Context) (*mongo.Cursor, error) +} diff --git a/pkg/domain/repository/auto_id.go b/pkg/domain/repository/auto_id.go new file mode 100644 index 0000000..407903a --- /dev/null +++ b/pkg/domain/repository/auto_id.go @@ -0,0 +1,23 @@ +package repository + +import ( + "app-cloudep-member-server/pkg/domain/entity" + "context" + + "go.mongodb.org/mongo-driver/mongo" +) + +type AutoIDRepository interface { + Insert(ctx context.Context, data *entity.AutoID) error + FindOne(ctx context.Context, id string) (*entity.AutoID, error) + Update(ctx context.Context, data *entity.AutoID) (*mongo.UpdateResult, error) + Delete(ctx context.Context, id string) (int64, error) + Inc(ctx context.Context, data *entity.AutoID) error + GetUIDFromNum(num int64) (string, error) + GetNumFromUID(uid string) (int64, error) + AutoIDIndexUP +} + +type AutoIDIndexUP interface { + Index20241226001UP(ctx context.Context) (*mongo.Cursor, error) +} diff --git a/pkg/domain/repository/user.go b/pkg/domain/repository/user.go new file mode 100644 index 0000000..7c31386 --- /dev/null +++ b/pkg/domain/repository/user.go @@ -0,0 +1,55 @@ +package repository + +import ( + "app-cloudep-member-server/pkg/domain/entity" + "app-cloudep-member-server/pkg/domain/member" + "context" + + "go.mongodb.org/mongo-driver/mongo" +) + +type UserRepository interface { + BaseUserRepository + UpdateUserDetailsByUID(ctx context.Context, data *UpdateUserInfoRequest) error + UpdateStatus(ctx context.Context, uid string, status int32) error + FindOneByUID(ctx context.Context, uid string) (*entity.User, error) + FindOneByNickName(ctx context.Context, nickName string) (*entity.User, error) + ListMembers(ctx context.Context, params *UserQueryParams) ([]*entity.User, int64, error) + UpdateEmailVerifyStatus(ctx context.Context, uid, email string) error + UpdatePhoneVerifyStatus(ctx context.Context, uid, phone string) error + UserIndexUP +} + +type UserIndexUP interface { + Index20241226001UP(ctx context.Context) (*mongo.Cursor, error) +} + +type BaseUserRepository interface { + Insert(ctx context.Context, data *entity.User) error + FindOne(ctx context.Context, id string) (*entity.User, error) + Update(ctx context.Context, data *entity.User) (*mongo.UpdateResult, error) + Delete(ctx context.Context, id string) (int64, error) +} + +type UserQueryParams struct { + AlarmCategory *member.AlarmType + UserStatus *member.Status + CreateStartTime *int64 + CreateEndTime *int64 + PageSize int64 + PageIndex int64 +} + +type UpdateUserInfoRequest struct { + UID string `json:"uid"` // 用戶 UID + AvatarURL *string `json:"avatar_url,omitempty"` // 頭像 URL(可選) + FullName *string `json:"full_name,omitempty"` // 用戶全名 + Nickname *string `json:"nickname,omitempty"` // 暱稱(可選) + GenderCode *int8 `json:"gender_code,omitempty"` // 性別代碼 + Birthdate *int64 `json:"birthdate,omitempty"` // 生日 (格式: 19930417) + Address *string `json:"address,omitempty"` // 地址 + AlarmCategory *member.AlarmType `json:"alarm_category,omitempty"` // 警報類型 + UserStatus *member.Status `json:"user_status,omitempty"` // 用戶狀態 + PreferredLanguage *string `json:"preferred_language,omitempty"` // 使用語言 + Currency *string `json:"currency,omitempty"` // 使用幣種 +} diff --git a/pkg/domain/repository/verify_code.go b/pkg/domain/repository/verify_code.go new file mode 100644 index 0000000..9ae8f02 --- /dev/null +++ b/pkg/domain/repository/verify_code.go @@ -0,0 +1,9 @@ +package repository + +import "context" + +type VerifyCodeRepository interface { + IsVerifyCodeExist(ctx context.Context, loginID, checkType string) (string, error) + SetVerifyCode(ctx context.Context, loginID, checkType, code string) error + DelVerifyCode(ctx context.Context, loginID, checkType string) error +} diff --git a/pkg/domain/usecase/account.go b/pkg/domain/usecase/account.go new file mode 100644 index 0000000..dc939ea --- /dev/null +++ b/pkg/domain/usecase/account.go @@ -0,0 +1,218 @@ +package usecase + +import ( + "app-cloudep-member-server/pkg/domain/member" + "context" +) + +// AccountUseCase 定義了帳號服務的操作方法 +type AccountUseCase interface { + MemberUseCase + BindingMemberUseCase + VerifyMemberUseCase + UIDGenerateUseCase +} + +type MemberUseCase interface { + // CreateUserAccount 創建用戶帳號 + CreateUserAccount(ctx context.Context, req CreateLoginUserRequest) error + // GetUIDByAccount 通過帳號取得 UID + GetUIDByAccount(ctx context.Context, req GetUIDByAccountRequest) (GetUIDByAccountResponse, error) + // GetUserAccountInfo 取得用戶帳號資訊 + GetUserAccountInfo(ctx context.Context, req GetUIDByAccountRequest) (GetAccountInfoResponse, error) + // UpdateUserToken 更新用戶 Token (密碼) + UpdateUserToken(ctx context.Context, req UpdateTokenRequest) error + // UpdateUserInfo 更新用戶資訊 + UpdateUserInfo(ctx context.Context, req *UpdateUserInfoRequest) error + // UpdateStatus 更新用戶狀態 + UpdateStatus(ctx context.Context, req UpdateStatusRequest) error + // GetUserInfo 取得用戶資訊 + GetUserInfo(ctx context.Context, req GetUserInfoRequest) (UserInfo, error) + // ListMember 取得會員列表 + ListMember(ctx context.Context, req ListUserInfoRequest) (ListUserInfoResponse, error) +} + +type BindingMemberUseCase interface { + // BindUserInfo 綁定用戶信息 + BindUserInfo(ctx context.Context, req CreateUserInfoRequest) error + // BindAccount 綁定帳號到 UID + BindAccount(ctx context.Context, req BindingUser) (BindingUser, error) + // BindVerifyEmail 驗證Email 後綁定到會員 + BindVerifyEmail(ctx context.Context, uid, email string) error + // BindVerifyPhone 驗證 Phone 後綁定到會員 + BindVerifyPhone(ctx context.Context, uid, phone string) error +} + +// CreateUserInfoRequest 用於創建用戶詳細信息 +type CreateUserInfoRequest struct { + UID string `json:"uid"` // 用戶 UID + AvatarURL *string `json:"avatar_url,omitempty"` // 頭像 URL(可選) + FullName *string `json:"full_name,omitempty"` // 用戶全名 + Nickname *string `json:"nickname,omitempty"` // 暱稱(可選) + GenderCode *int64 `json:"gender_code,omitempty"` // 性別代碼 + Birthdate *int64 `json:"birthdate,omitempty"` // 生日 (格式: 19930417) + PhoneNumber *string `json:"phone_number,omitempty"` // 電話 + Email *string `json:"email,omitempty"` // 電話 + Address *string `json:"address,omitempty"` // 地址 + AlarmCategory member.AlarmType `json:"alarm_category"` // 警報類型 + UserStatus member.Status `json:"user_status"` // 用戶狀態 + PreferredLanguage string `json:"preferred_language"` // 使用語言 + Currency string `json:"currency"` // 使用幣種 +} + +type UserInfo struct { + CreateUserInfoRequest + CreateTime int64 `json:"create_time"` // 創建時間 + UpdateTime int64 `json:"update_time"` // 更新時間 +} + +type UpdateUserInfoRequest struct { + UID string `json:"uid"` // 用戶 UID + AvatarURL *string `json:"avatar_url,omitempty"` // 頭像 URL(可選) + FullName *string `json:"full_name,omitempty"` // 用戶全名 + Nickname *string `json:"nickname,omitempty"` // 暱稱(可選) + GenderCode *int8 `json:"gender_code,omitempty"` // 性別代碼 + Birthdate *int64 `json:"birthdate,omitempty"` // 生日 (格式: 19930417) + Address *string `json:"address,omitempty"` // 地址 + AlarmCategory *member.AlarmType `json:"alarm_category,omitempty"` // 警報類型 + UserStatus *member.Status `json:"user_status,omitempty"` // 用戶狀態 + PreferredLanguage *string `json:"preferred_language,omitempty"` // 使用語言 + Currency *string `json:"currency,omitempty"` // 使用幣種 +} + +type CreateLoginUserRequest struct { + LoginID string `json:"login_id"` // 登錄 ID + Platform member.Platform `json:"platform"` // 平台類型 + Token string `json:"token"` // 驗證 Token +} + +// BindingUser 用於綁定用戶帳號 +type BindingUser struct { + UID string `json:"uid"` // 用戶 UID + LoginID string `json:"login_id"` // 登錄 ID + Type member.AccountType `json:"type"` // 綁定類型 +} + +// GetUIDByAccountRequest 用於通過帳號獲取用戶 UID +type GetUIDByAccountRequest struct { + Account string `json:"account"` // 帳號 +} + +// GetUIDByAccountResponse 用於返回帳號對應的 UID 信息 +type GetUIDByAccountResponse struct { + UID string `json:"uid"` // 用戶 UID + Account string `json:"account"` // 帳號 +} + +// GetAccountInfoResponse 用於返回用戶帳號信息 +type GetAccountInfoResponse struct { + Data CreateLoginUserRequest `json:"data"` // 登錄用戶信息 +} + +// UpdateTokenRequest 用於更新用戶 Token +type UpdateTokenRequest struct { + Account string `json:"account"` // 帳號 + Token string `json:"token"` // 新 Token + Platform int64 `json:"platform"` // 平台類型 +} + +// GenerateRefreshCodeRequest 用於請求產生刷新代碼 +type GenerateRefreshCodeRequest struct { + LoginID string `json:"login_id"` // 帳號 + CodeType member.GenerateCodeType `json:"code_type"` // 代碼類型 +} + +// VerifyCode 用於表示驗證代碼 +type VerifyCode struct { + VerifyCode string `json:"verify_code"` // 驗證碼 +} + +// GenerateRefreshCodeResponse 用於返回生成的驗證代碼 +type GenerateRefreshCodeResponse struct { + Data VerifyCode `json:"data"` // 驗證碼數據 +} + +// VerifyRefreshCodeRequest 用於驗證刷新代碼 +type VerifyRefreshCodeRequest struct { + LoginID string `json:"Login_id"` // 帳號 + CodeType member.GenerateCodeType `json:"code_type"` // 代碼類型 + VerifyCode string `json:"verify_code"` // 驗證碼 +} + +// UpdateStatusRequest 用於更新用戶狀態 +type UpdateStatusRequest struct { + UID string `json:"uid"` // 用戶 UID + Status member.Status `json:"status"` // 用戶狀態 +} + +// GetUserInfoRequest 用於請求取得用戶詳細信息 +type GetUserInfoRequest struct { + UID string `json:"uid,omitempty"` // 用戶 UID + NickName string `json:"nick_name,omitempty"` // 暱稱(可選) +} + +// GetUserInfoResponse 用於返回用戶詳細信息 +type GetUserInfoResponse struct { + Data UserInfo `json:"data"` // 用戶信息 +} + +// ListUserInfoRequest 用於查詢符合條件的用戶列表 +type ListUserInfoRequest struct { + VerificationType *member.AccountType `json:"verification_type,omitempty"` // 驗證類型(可選) + AlarmCategory *member.AlarmType `json:"alarm_category,omitempty"` // 警報類型(可選) + UserStatus *member.Status `json:"user_status,omitempty"` // 用戶狀態(可選) + CreateStartTime *int64 `json:"create_start_time,omitempty"` // 創建開始時間(可選) + CreateEndTime *int64 `json:"create_end_time,omitempty"` // 創建結束時間(可選) + PageSize int64 `json:"page_size"` // 每頁大小 + PageIndex int64 `json:"page_index"` // 當前頁索引 +} + +// ListUserInfoResponse 用於返回查詢的用戶列表及分頁信息 +type ListUserInfoResponse struct { + Data []UserInfo `json:"data"` // 用戶列表 + Page Pager `json:"page"` // 分頁信息 +} + +// VerifyAuthResultRequest 用於請求驗證授權結果 +type VerifyAuthResultRequest struct { + Token string `json:"token"` // 驗證 Token + Account string `json:"account"` // 帳號 +} + +// VerifyAuthResultResponse 用於返回授權驗證結果 +type VerifyAuthResultResponse struct { + Status bool `json:"status"` // 驗證結果狀態 +} + +// TwitterAccessTokenResponse 用於返回 Twitter 授權令牌 +type TwitterAccessTokenResponse struct { + Token string `json:"token"` // 授權 Token +} + +type GoogleTokenInfo struct { + Iss string `json:"iss"` // 發行者 (issuer) 通常為 "https://accounts.google.com" + Sub string `json:"sub"` // 使用者唯一 ID + Aud string `json:"aud"` // Audience,應該與你的 Client ID 匹配 + Exp string `json:"exp"` // 過期時間 (UNIX timestamp) + Iat string `json:"iat"` // 發行時間 (UNIX timestamp) + Email string `json:"email"` // 使用者的電子郵件 + EmailVerified string `json:"email_verified"` // 郵件是否已驗證 + Name string `json:"name"` // 使用者的名稱 + Picture string `json:"picture"` // 使用者的頭像 URL +} + +type LineAccessTokenResponse struct { + AccessToken string `json:"access_token"` + IDToken string `json:"id_token"` + ExpiresIn int `json:"expires_in"` + TokenType string `json:"token_type"` + Scope string `json:"scope"` + RefreshToken string `json:"refresh_token"` +} + +type LineUserProfile struct { + UserID string `json:"userId"` + DisplayName string `json:"displayName"` + PictureURL string `json:"pictureUrl"` + StatusMessage string `json:"statusMessage"` +} diff --git a/pkg/domain/usecase/common.go b/pkg/domain/usecase/common.go new file mode 100644 index 0000000..6ccd51f --- /dev/null +++ b/pkg/domain/usecase/common.go @@ -0,0 +1,7 @@ +package usecase + +type Pager struct { + Total int64 `json:"total"` // 總數量 + Size int64 `json:"size"` // 每頁大小 + Index int64 `json:"index"` // 當前頁索引 +} diff --git a/pkg/domain/usecase/generate_uid.go b/pkg/domain/usecase/generate_uid.go new file mode 100644 index 0000000..3f9bbb8 --- /dev/null +++ b/pkg/domain/usecase/generate_uid.go @@ -0,0 +1,7 @@ +package usecase + +import "context" + +type UIDGenerateUseCase interface { + Generate(ctx context.Context) (string, error) +} diff --git a/pkg/domain/usecase/verify.go b/pkg/domain/usecase/verify.go new file mode 100644 index 0000000..576c97c --- /dev/null +++ b/pkg/domain/usecase/verify.go @@ -0,0 +1,28 @@ +package usecase + +import "context" + +type VerifyMemberUseCase interface { + // GenerateRefreshCode 這個帳號驗證碼(十分鐘),通用的 + GenerateRefreshCode(ctx context.Context, req GenerateRefreshCodeRequest) (GenerateRefreshCodeResponse, error) + // VerifyRefreshCode 驗證驗證碼,驗證完會刪除 + VerifyRefreshCode(ctx context.Context, req VerifyRefreshCodeRequest) error + // CheckRefreshCode 驗證驗證碼,驗證完不會刪除 + CheckRefreshCode(ctx context.Context, req VerifyRefreshCodeRequest) error + // VerifyPlatformAuthResult 驗證平台授權結果 -> 看密碼對不對 + VerifyPlatformAuthResult(ctx context.Context, req VerifyAuthResultRequest) (VerifyAuthResultResponse, error) + GoogleVerify + LineVerify +} + +type GoogleVerify interface { + // VerifyGoogleAuthResult 驗證 Google 授權結果 + VerifyGoogleAuthResult(ctx context.Context, req VerifyAuthResultRequest) (GoogleTokenInfo, error) +} + +type LineVerify interface { + // LineCodeToAccessToken 換取 AccessToken + LineCodeToAccessToken(ctx context.Context, code string) (LineAccessTokenResponse, error) + // LineGetProfileByAccessToken 用 Access Token 換取使用者的基本資料 + LineGetProfileByAccessToken(ctx context.Context, accessToken string) (*LineUserProfile, error) +} diff --git a/pkg/mock/repository/account.go b/pkg/mock/repository/account.go new file mode 100644 index 0000000..38ac98e --- /dev/null +++ b/pkg/mock/repository/account.go @@ -0,0 +1,185 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: ./pkg/domain/repository/account.go +// +// Generated by this command: +// +// mockgen -source=./pkg/domain/repository/account.go -destination=./pkg/mock/repository/account.go -package=mock +// + +// Package mock is a generated GoMock package. +package mock + +import ( + entity "app-cloudep-member-server/pkg/domain/entity" + context "context" + reflect "reflect" + + mongo "go.mongodb.org/mongo-driver/mongo" + gomock "go.uber.org/mock/gomock" +) + +// MockAccountRepository is a mock of AccountRepository interface. +type MockAccountRepository struct { + ctrl *gomock.Controller + recorder *MockAccountRepositoryMockRecorder + isgomock struct{} +} + +// MockAccountRepositoryMockRecorder is the mock recorder for MockAccountRepository. +type MockAccountRepositoryMockRecorder struct { + mock *MockAccountRepository +} + +// NewMockAccountRepository creates a new mock instance. +func NewMockAccountRepository(ctrl *gomock.Controller) *MockAccountRepository { + mock := &MockAccountRepository{ctrl: ctrl} + mock.recorder = &MockAccountRepositoryMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockAccountRepository) EXPECT() *MockAccountRepositoryMockRecorder { + return m.recorder +} + +// Delete mocks base method. +func (m *MockAccountRepository) Delete(ctx context.Context, id string) (int64, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Delete", ctx, id) + ret0, _ := ret[0].(int64) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Delete indicates an expected call of Delete. +func (mr *MockAccountRepositoryMockRecorder) Delete(ctx, id any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockAccountRepository)(nil).Delete), ctx, id) +} + +// FindOne mocks base method. +func (m *MockAccountRepository) FindOne(ctx context.Context, id string) (*entity.Account, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "FindOne", ctx, id) + ret0, _ := ret[0].(*entity.Account) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// FindOne indicates an expected call of FindOne. +func (mr *MockAccountRepositoryMockRecorder) FindOne(ctx, id any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FindOne", reflect.TypeOf((*MockAccountRepository)(nil).FindOne), ctx, id) +} + +// FindOneByAccount mocks base method. +func (m *MockAccountRepository) FindOneByAccount(ctx context.Context, loginID string) (*entity.Account, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "FindOneByAccount", ctx, loginID) + ret0, _ := ret[0].(*entity.Account) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// FindOneByAccount indicates an expected call of FindOneByAccount. +func (mr *MockAccountRepositoryMockRecorder) FindOneByAccount(ctx, loginID any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FindOneByAccount", reflect.TypeOf((*MockAccountRepository)(nil).FindOneByAccount), ctx, loginID) +} + +// Index20241226001UP mocks base method. +func (m *MockAccountRepository) Index20241226001UP(ctx context.Context) (*mongo.Cursor, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Index20241226001UP", ctx) + ret0, _ := ret[0].(*mongo.Cursor) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Index20241226001UP indicates an expected call of Index20241226001UP. +func (mr *MockAccountRepositoryMockRecorder) Index20241226001UP(ctx any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Index20241226001UP", reflect.TypeOf((*MockAccountRepository)(nil).Index20241226001UP), ctx) +} + +// Insert mocks base method. +func (m *MockAccountRepository) Insert(ctx context.Context, data *entity.Account) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Insert", ctx, data) + ret0, _ := ret[0].(error) + return ret0 +} + +// Insert indicates an expected call of Insert. +func (mr *MockAccountRepositoryMockRecorder) Insert(ctx, data any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Insert", reflect.TypeOf((*MockAccountRepository)(nil).Insert), ctx, data) +} + +// Update mocks base method. +func (m *MockAccountRepository) Update(ctx context.Context, data *entity.Account) (*mongo.UpdateResult, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Update", ctx, data) + ret0, _ := ret[0].(*mongo.UpdateResult) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Update indicates an expected call of Update. +func (mr *MockAccountRepositoryMockRecorder) Update(ctx, data any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Update", reflect.TypeOf((*MockAccountRepository)(nil).Update), ctx, data) +} + +// UpdateTokenByLoginID mocks base method. +func (m *MockAccountRepository) UpdateTokenByLoginID(ctx context.Context, account, token string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "UpdateTokenByLoginID", ctx, account, token) + ret0, _ := ret[0].(error) + return ret0 +} + +// UpdateTokenByLoginID indicates an expected call of UpdateTokenByLoginID. +func (mr *MockAccountRepositoryMockRecorder) UpdateTokenByLoginID(ctx, account, token any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateTokenByLoginID", reflect.TypeOf((*MockAccountRepository)(nil).UpdateTokenByLoginID), ctx, account, token) +} + +// MockAccountIndexUP is a mock of AccountIndexUP interface. +type MockAccountIndexUP struct { + ctrl *gomock.Controller + recorder *MockAccountIndexUPMockRecorder + isgomock struct{} +} + +// MockAccountIndexUPMockRecorder is the mock recorder for MockAccountIndexUP. +type MockAccountIndexUPMockRecorder struct { + mock *MockAccountIndexUP +} + +// NewMockAccountIndexUP creates a new mock instance. +func NewMockAccountIndexUP(ctrl *gomock.Controller) *MockAccountIndexUP { + mock := &MockAccountIndexUP{ctrl: ctrl} + mock.recorder = &MockAccountIndexUPMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockAccountIndexUP) EXPECT() *MockAccountIndexUPMockRecorder { + return m.recorder +} + +// Index20241226001UP mocks base method. +func (m *MockAccountIndexUP) Index20241226001UP(ctx context.Context) (*mongo.Cursor, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Index20241226001UP", ctx) + ret0, _ := ret[0].(*mongo.Cursor) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Index20241226001UP indicates an expected call of Index20241226001UP. +func (mr *MockAccountIndexUPMockRecorder) Index20241226001UP(ctx any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Index20241226001UP", reflect.TypeOf((*MockAccountIndexUP)(nil).Index20241226001UP), ctx) +} diff --git a/pkg/mock/repository/account_uid.go b/pkg/mock/repository/account_uid.go new file mode 100644 index 0000000..5c5b434 --- /dev/null +++ b/pkg/mock/repository/account_uid.go @@ -0,0 +1,171 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: ./pkg/domain/repository/account_uid.go +// +// Generated by this command: +// +// mockgen -source=./pkg/domain/repository/account_uid.go -destination=./pkg/mock/repository/account_uid.go -package=mock +// + +// Package mock is a generated GoMock package. +package mock + +import ( + entity "app-cloudep-member-server/pkg/domain/entity" + context "context" + reflect "reflect" + + mongo "go.mongodb.org/mongo-driver/mongo" + gomock "go.uber.org/mock/gomock" +) + +// MockAccountUIDRepository is a mock of AccountUIDRepository interface. +type MockAccountUIDRepository struct { + ctrl *gomock.Controller + recorder *MockAccountUIDRepositoryMockRecorder + isgomock struct{} +} + +// MockAccountUIDRepositoryMockRecorder is the mock recorder for MockAccountUIDRepository. +type MockAccountUIDRepositoryMockRecorder struct { + mock *MockAccountUIDRepository +} + +// NewMockAccountUIDRepository creates a new mock instance. +func NewMockAccountUIDRepository(ctrl *gomock.Controller) *MockAccountUIDRepository { + mock := &MockAccountUIDRepository{ctrl: ctrl} + mock.recorder = &MockAccountUIDRepositoryMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockAccountUIDRepository) EXPECT() *MockAccountUIDRepositoryMockRecorder { + return m.recorder +} + +// Delete mocks base method. +func (m *MockAccountUIDRepository) Delete(ctx context.Context, id string) (int64, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Delete", ctx, id) + ret0, _ := ret[0].(int64) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Delete indicates an expected call of Delete. +func (mr *MockAccountUIDRepositoryMockRecorder) Delete(ctx, id any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockAccountUIDRepository)(nil).Delete), ctx, id) +} + +// FindOne mocks base method. +func (m *MockAccountUIDRepository) FindOne(ctx context.Context, id string) (*entity.AccountUID, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "FindOne", ctx, id) + ret0, _ := ret[0].(*entity.AccountUID) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// FindOne indicates an expected call of FindOne. +func (mr *MockAccountUIDRepositoryMockRecorder) FindOne(ctx, id any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FindOne", reflect.TypeOf((*MockAccountUIDRepository)(nil).FindOne), ctx, id) +} + +// FindUIDByLoginID mocks base method. +func (m *MockAccountUIDRepository) FindUIDByLoginID(ctx context.Context, loginID string) (*entity.AccountUID, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "FindUIDByLoginID", ctx, loginID) + ret0, _ := ret[0].(*entity.AccountUID) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// FindUIDByLoginID indicates an expected call of FindUIDByLoginID. +func (mr *MockAccountUIDRepositoryMockRecorder) FindUIDByLoginID(ctx, loginID any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FindUIDByLoginID", reflect.TypeOf((*MockAccountUIDRepository)(nil).FindUIDByLoginID), ctx, loginID) +} + +// Index20241226001UP mocks base method. +func (m *MockAccountUIDRepository) Index20241226001UP(ctx context.Context) (*mongo.Cursor, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Index20241226001UP", ctx) + ret0, _ := ret[0].(*mongo.Cursor) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Index20241226001UP indicates an expected call of Index20241226001UP. +func (mr *MockAccountUIDRepositoryMockRecorder) Index20241226001UP(ctx any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Index20241226001UP", reflect.TypeOf((*MockAccountUIDRepository)(nil).Index20241226001UP), ctx) +} + +// Insert mocks base method. +func (m *MockAccountUIDRepository) Insert(ctx context.Context, data *entity.AccountUID) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Insert", ctx, data) + ret0, _ := ret[0].(error) + return ret0 +} + +// Insert indicates an expected call of Insert. +func (mr *MockAccountUIDRepositoryMockRecorder) Insert(ctx, data any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Insert", reflect.TypeOf((*MockAccountUIDRepository)(nil).Insert), ctx, data) +} + +// Update mocks base method. +func (m *MockAccountUIDRepository) Update(ctx context.Context, data *entity.AccountUID) (*mongo.UpdateResult, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Update", ctx, data) + ret0, _ := ret[0].(*mongo.UpdateResult) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Update indicates an expected call of Update. +func (mr *MockAccountUIDRepositoryMockRecorder) Update(ctx, data any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Update", reflect.TypeOf((*MockAccountUIDRepository)(nil).Update), ctx, data) +} + +// MockAccountUIDIndexUP is a mock of AccountUIDIndexUP interface. +type MockAccountUIDIndexUP struct { + ctrl *gomock.Controller + recorder *MockAccountUIDIndexUPMockRecorder + isgomock struct{} +} + +// MockAccountUIDIndexUPMockRecorder is the mock recorder for MockAccountUIDIndexUP. +type MockAccountUIDIndexUPMockRecorder struct { + mock *MockAccountUIDIndexUP +} + +// NewMockAccountUIDIndexUP creates a new mock instance. +func NewMockAccountUIDIndexUP(ctrl *gomock.Controller) *MockAccountUIDIndexUP { + mock := &MockAccountUIDIndexUP{ctrl: ctrl} + mock.recorder = &MockAccountUIDIndexUPMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockAccountUIDIndexUP) EXPECT() *MockAccountUIDIndexUPMockRecorder { + return m.recorder +} + +// Index20241226001UP mocks base method. +func (m *MockAccountUIDIndexUP) Index20241226001UP(ctx context.Context) (*mongo.Cursor, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Index20241226001UP", ctx) + ret0, _ := ret[0].(*mongo.Cursor) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Index20241226001UP indicates an expected call of Index20241226001UP. +func (mr *MockAccountUIDIndexUPMockRecorder) Index20241226001UP(ctx any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Index20241226001UP", reflect.TypeOf((*MockAccountUIDIndexUP)(nil).Index20241226001UP), ctx) +} diff --git a/pkg/mock/repository/auto_id.go b/pkg/mock/repository/auto_id.go new file mode 100644 index 0000000..b722cfa --- /dev/null +++ b/pkg/mock/repository/auto_id.go @@ -0,0 +1,200 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: ./pkg/domain/repository/auto_id.go +// +// Generated by this command: +// +// mockgen -source=./pkg/domain/repository/auto_id.go -destination=./pkg/mock/repository/auto_id.go -package=mock +// + +// Package mock is a generated GoMock package. +package mock + +import ( + entity "app-cloudep-member-server/pkg/domain/entity" + context "context" + reflect "reflect" + + mongo "go.mongodb.org/mongo-driver/mongo" + gomock "go.uber.org/mock/gomock" +) + +// MockAutoIDRepository is a mock of AutoIDRepository interface. +type MockAutoIDRepository struct { + ctrl *gomock.Controller + recorder *MockAutoIDRepositoryMockRecorder + isgomock struct{} +} + +// MockAutoIDRepositoryMockRecorder is the mock recorder for MockAutoIDRepository. +type MockAutoIDRepositoryMockRecorder struct { + mock *MockAutoIDRepository +} + +// NewMockAutoIDRepository creates a new mock instance. +func NewMockAutoIDRepository(ctrl *gomock.Controller) *MockAutoIDRepository { + mock := &MockAutoIDRepository{ctrl: ctrl} + mock.recorder = &MockAutoIDRepositoryMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockAutoIDRepository) EXPECT() *MockAutoIDRepositoryMockRecorder { + return m.recorder +} + +// Delete mocks base method. +func (m *MockAutoIDRepository) Delete(ctx context.Context, id string) (int64, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Delete", ctx, id) + ret0, _ := ret[0].(int64) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Delete indicates an expected call of Delete. +func (mr *MockAutoIDRepositoryMockRecorder) Delete(ctx, id any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockAutoIDRepository)(nil).Delete), ctx, id) +} + +// FindOne mocks base method. +func (m *MockAutoIDRepository) FindOne(ctx context.Context, id string) (*entity.AutoID, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "FindOne", ctx, id) + ret0, _ := ret[0].(*entity.AutoID) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// FindOne indicates an expected call of FindOne. +func (mr *MockAutoIDRepositoryMockRecorder) FindOne(ctx, id any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FindOne", reflect.TypeOf((*MockAutoIDRepository)(nil).FindOne), ctx, id) +} + +// GetNumFromUID mocks base method. +func (m *MockAutoIDRepository) GetNumFromUID(uid string) (int64, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetNumFromUID", uid) + ret0, _ := ret[0].(int64) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetNumFromUID indicates an expected call of GetNumFromUID. +func (mr *MockAutoIDRepositoryMockRecorder) GetNumFromUID(uid any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetNumFromUID", reflect.TypeOf((*MockAutoIDRepository)(nil).GetNumFromUID), uid) +} + +// GetUIDFromNum mocks base method. +func (m *MockAutoIDRepository) GetUIDFromNum(num int64) (string, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetUIDFromNum", num) + ret0, _ := ret[0].(string) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetUIDFromNum indicates an expected call of GetUIDFromNum. +func (mr *MockAutoIDRepositoryMockRecorder) GetUIDFromNum(num any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUIDFromNum", reflect.TypeOf((*MockAutoIDRepository)(nil).GetUIDFromNum), num) +} + +// Inc mocks base method. +func (m *MockAutoIDRepository) Inc(ctx context.Context, data *entity.AutoID) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Inc", ctx, data) + ret0, _ := ret[0].(error) + return ret0 +} + +// Inc indicates an expected call of Inc. +func (mr *MockAutoIDRepositoryMockRecorder) Inc(ctx, data any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Inc", reflect.TypeOf((*MockAutoIDRepository)(nil).Inc), ctx, data) +} + +// Index20241226001UP mocks base method. +func (m *MockAutoIDRepository) Index20241226001UP(ctx context.Context) (*mongo.Cursor, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Index20241226001UP", ctx) + ret0, _ := ret[0].(*mongo.Cursor) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Index20241226001UP indicates an expected call of Index20241226001UP. +func (mr *MockAutoIDRepositoryMockRecorder) Index20241226001UP(ctx any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Index20241226001UP", reflect.TypeOf((*MockAutoIDRepository)(nil).Index20241226001UP), ctx) +} + +// Insert mocks base method. +func (m *MockAutoIDRepository) Insert(ctx context.Context, data *entity.AutoID) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Insert", ctx, data) + ret0, _ := ret[0].(error) + return ret0 +} + +// Insert indicates an expected call of Insert. +func (mr *MockAutoIDRepositoryMockRecorder) Insert(ctx, data any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Insert", reflect.TypeOf((*MockAutoIDRepository)(nil).Insert), ctx, data) +} + +// Update mocks base method. +func (m *MockAutoIDRepository) Update(ctx context.Context, data *entity.AutoID) (*mongo.UpdateResult, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Update", ctx, data) + ret0, _ := ret[0].(*mongo.UpdateResult) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Update indicates an expected call of Update. +func (mr *MockAutoIDRepositoryMockRecorder) Update(ctx, data any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Update", reflect.TypeOf((*MockAutoIDRepository)(nil).Update), ctx, data) +} + +// MockAutoIDIndexUP is a mock of AutoIDIndexUP interface. +type MockAutoIDIndexUP struct { + ctrl *gomock.Controller + recorder *MockAutoIDIndexUPMockRecorder + isgomock struct{} +} + +// MockAutoIDIndexUPMockRecorder is the mock recorder for MockAutoIDIndexUP. +type MockAutoIDIndexUPMockRecorder struct { + mock *MockAutoIDIndexUP +} + +// NewMockAutoIDIndexUP creates a new mock instance. +func NewMockAutoIDIndexUP(ctrl *gomock.Controller) *MockAutoIDIndexUP { + mock := &MockAutoIDIndexUP{ctrl: ctrl} + mock.recorder = &MockAutoIDIndexUPMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockAutoIDIndexUP) EXPECT() *MockAutoIDIndexUPMockRecorder { + return m.recorder +} + +// Index20241226001UP mocks base method. +func (m *MockAutoIDIndexUP) Index20241226001UP(ctx context.Context) (*mongo.Cursor, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Index20241226001UP", ctx) + ret0, _ := ret[0].(*mongo.Cursor) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Index20241226001UP indicates an expected call of Index20241226001UP. +func (mr *MockAutoIDIndexUPMockRecorder) Index20241226001UP(ctx any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Index20241226001UP", reflect.TypeOf((*MockAutoIDIndexUP)(nil).Index20241226001UP), ctx) +} diff --git a/pkg/mock/repository/user.go b/pkg/mock/repository/user.go new file mode 100644 index 0000000..95f9b21 --- /dev/null +++ b/pkg/mock/repository/user.go @@ -0,0 +1,342 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: ./pkg/domain/repository/user.go +// +// Generated by this command: +// +// mockgen -source=./pkg/domain/repository/user.go -destination=./pkg/mock/repository/user.go -package=mock +// + +// Package mock is a generated GoMock package. +package mock + +import ( + entity "app-cloudep-member-server/pkg/domain/entity" + repository "app-cloudep-member-server/pkg/domain/repository" + context "context" + reflect "reflect" + + mongo "go.mongodb.org/mongo-driver/mongo" + gomock "go.uber.org/mock/gomock" +) + +// MockUserRepository is a mock of UserRepository interface. +type MockUserRepository struct { + ctrl *gomock.Controller + recorder *MockUserRepositoryMockRecorder + isgomock struct{} +} + +// MockUserRepositoryMockRecorder is the mock recorder for MockUserRepository. +type MockUserRepositoryMockRecorder struct { + mock *MockUserRepository +} + +// NewMockUserRepository creates a new mock instance. +func NewMockUserRepository(ctrl *gomock.Controller) *MockUserRepository { + mock := &MockUserRepository{ctrl: ctrl} + mock.recorder = &MockUserRepositoryMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockUserRepository) EXPECT() *MockUserRepositoryMockRecorder { + return m.recorder +} + +// Delete mocks base method. +func (m *MockUserRepository) Delete(ctx context.Context, id string) (int64, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Delete", ctx, id) + ret0, _ := ret[0].(int64) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Delete indicates an expected call of Delete. +func (mr *MockUserRepositoryMockRecorder) Delete(ctx, id any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockUserRepository)(nil).Delete), ctx, id) +} + +// FindOne mocks base method. +func (m *MockUserRepository) FindOne(ctx context.Context, id string) (*entity.User, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "FindOne", ctx, id) + ret0, _ := ret[0].(*entity.User) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// FindOne indicates an expected call of FindOne. +func (mr *MockUserRepositoryMockRecorder) FindOne(ctx, id any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FindOne", reflect.TypeOf((*MockUserRepository)(nil).FindOne), ctx, id) +} + +// FindOneByNickName mocks base method. +func (m *MockUserRepository) FindOneByNickName(ctx context.Context, nickName string) (*entity.User, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "FindOneByNickName", ctx, nickName) + ret0, _ := ret[0].(*entity.User) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// FindOneByNickName indicates an expected call of FindOneByNickName. +func (mr *MockUserRepositoryMockRecorder) FindOneByNickName(ctx, nickName any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FindOneByNickName", reflect.TypeOf((*MockUserRepository)(nil).FindOneByNickName), ctx, nickName) +} + +// FindOneByUID mocks base method. +func (m *MockUserRepository) FindOneByUID(ctx context.Context, uid string) (*entity.User, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "FindOneByUID", ctx, uid) + ret0, _ := ret[0].(*entity.User) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// FindOneByUID indicates an expected call of FindOneByUID. +func (mr *MockUserRepositoryMockRecorder) FindOneByUID(ctx, uid any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FindOneByUID", reflect.TypeOf((*MockUserRepository)(nil).FindOneByUID), ctx, uid) +} + +// Index20241226001UP mocks base method. +func (m *MockUserRepository) Index20241226001UP(ctx context.Context) (*mongo.Cursor, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Index20241226001UP", ctx) + ret0, _ := ret[0].(*mongo.Cursor) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Index20241226001UP indicates an expected call of Index20241226001UP. +func (mr *MockUserRepositoryMockRecorder) Index20241226001UP(ctx any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Index20241226001UP", reflect.TypeOf((*MockUserRepository)(nil).Index20241226001UP), ctx) +} + +// Insert mocks base method. +func (m *MockUserRepository) Insert(ctx context.Context, data *entity.User) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Insert", ctx, data) + ret0, _ := ret[0].(error) + return ret0 +} + +// Insert indicates an expected call of Insert. +func (mr *MockUserRepositoryMockRecorder) Insert(ctx, data any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Insert", reflect.TypeOf((*MockUserRepository)(nil).Insert), ctx, data) +} + +// ListMembers mocks base method. +func (m *MockUserRepository) ListMembers(ctx context.Context, params *repository.UserQueryParams) ([]*entity.User, int64, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ListMembers", ctx, params) + ret0, _ := ret[0].([]*entity.User) + ret1, _ := ret[1].(int64) + ret2, _ := ret[2].(error) + return ret0, ret1, ret2 +} + +// ListMembers indicates an expected call of ListMembers. +func (mr *MockUserRepositoryMockRecorder) ListMembers(ctx, params any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListMembers", reflect.TypeOf((*MockUserRepository)(nil).ListMembers), ctx, params) +} + +// Update mocks base method. +func (m *MockUserRepository) Update(ctx context.Context, data *entity.User) (*mongo.UpdateResult, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Update", ctx, data) + ret0, _ := ret[0].(*mongo.UpdateResult) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Update indicates an expected call of Update. +func (mr *MockUserRepositoryMockRecorder) Update(ctx, data any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Update", reflect.TypeOf((*MockUserRepository)(nil).Update), ctx, data) +} + +// UpdateEmailVerifyStatus mocks base method. +func (m *MockUserRepository) UpdateEmailVerifyStatus(ctx context.Context, uid, email string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "UpdateEmailVerifyStatus", ctx, uid, email) + ret0, _ := ret[0].(error) + return ret0 +} + +// UpdateEmailVerifyStatus indicates an expected call of UpdateEmailVerifyStatus. +func (mr *MockUserRepositoryMockRecorder) UpdateEmailVerifyStatus(ctx, uid, email any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateEmailVerifyStatus", reflect.TypeOf((*MockUserRepository)(nil).UpdateEmailVerifyStatus), ctx, uid, email) +} + +// UpdatePhoneVerifyStatus mocks base method. +func (m *MockUserRepository) UpdatePhoneVerifyStatus(ctx context.Context, uid, phone string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "UpdatePhoneVerifyStatus", ctx, uid, phone) + ret0, _ := ret[0].(error) + return ret0 +} + +// UpdatePhoneVerifyStatus indicates an expected call of UpdatePhoneVerifyStatus. +func (mr *MockUserRepositoryMockRecorder) UpdatePhoneVerifyStatus(ctx, uid, phone any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdatePhoneVerifyStatus", reflect.TypeOf((*MockUserRepository)(nil).UpdatePhoneVerifyStatus), ctx, uid, phone) +} + +// UpdateStatus mocks base method. +func (m *MockUserRepository) UpdateStatus(ctx context.Context, uid string, status int32) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "UpdateStatus", ctx, uid, status) + ret0, _ := ret[0].(error) + return ret0 +} + +// UpdateStatus indicates an expected call of UpdateStatus. +func (mr *MockUserRepositoryMockRecorder) UpdateStatus(ctx, uid, status any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateStatus", reflect.TypeOf((*MockUserRepository)(nil).UpdateStatus), ctx, uid, status) +} + +// UpdateUserDetailsByUID mocks base method. +func (m *MockUserRepository) UpdateUserDetailsByUID(ctx context.Context, data *repository.UpdateUserInfoRequest) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "UpdateUserDetailsByUID", ctx, data) + ret0, _ := ret[0].(error) + return ret0 +} + +// UpdateUserDetailsByUID indicates an expected call of UpdateUserDetailsByUID. +func (mr *MockUserRepositoryMockRecorder) UpdateUserDetailsByUID(ctx, data any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateUserDetailsByUID", reflect.TypeOf((*MockUserRepository)(nil).UpdateUserDetailsByUID), ctx, data) +} + +// MockUserIndexUP is a mock of UserIndexUP interface. +type MockUserIndexUP struct { + ctrl *gomock.Controller + recorder *MockUserIndexUPMockRecorder + isgomock struct{} +} + +// MockUserIndexUPMockRecorder is the mock recorder for MockUserIndexUP. +type MockUserIndexUPMockRecorder struct { + mock *MockUserIndexUP +} + +// NewMockUserIndexUP creates a new mock instance. +func NewMockUserIndexUP(ctrl *gomock.Controller) *MockUserIndexUP { + mock := &MockUserIndexUP{ctrl: ctrl} + mock.recorder = &MockUserIndexUPMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockUserIndexUP) EXPECT() *MockUserIndexUPMockRecorder { + return m.recorder +} + +// Index20241226001UP mocks base method. +func (m *MockUserIndexUP) Index20241226001UP(ctx context.Context) (*mongo.Cursor, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Index20241226001UP", ctx) + ret0, _ := ret[0].(*mongo.Cursor) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Index20241226001UP indicates an expected call of Index20241226001UP. +func (mr *MockUserIndexUPMockRecorder) Index20241226001UP(ctx any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Index20241226001UP", reflect.TypeOf((*MockUserIndexUP)(nil).Index20241226001UP), ctx) +} + +// MockBaseUserRepository is a mock of BaseUserRepository interface. +type MockBaseUserRepository struct { + ctrl *gomock.Controller + recorder *MockBaseUserRepositoryMockRecorder + isgomock struct{} +} + +// MockBaseUserRepositoryMockRecorder is the mock recorder for MockBaseUserRepository. +type MockBaseUserRepositoryMockRecorder struct { + mock *MockBaseUserRepository +} + +// NewMockBaseUserRepository creates a new mock instance. +func NewMockBaseUserRepository(ctrl *gomock.Controller) *MockBaseUserRepository { + mock := &MockBaseUserRepository{ctrl: ctrl} + mock.recorder = &MockBaseUserRepositoryMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockBaseUserRepository) EXPECT() *MockBaseUserRepositoryMockRecorder { + return m.recorder +} + +// Delete mocks base method. +func (m *MockBaseUserRepository) Delete(ctx context.Context, id string) (int64, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Delete", ctx, id) + ret0, _ := ret[0].(int64) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Delete indicates an expected call of Delete. +func (mr *MockBaseUserRepositoryMockRecorder) Delete(ctx, id any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockBaseUserRepository)(nil).Delete), ctx, id) +} + +// FindOne mocks base method. +func (m *MockBaseUserRepository) FindOne(ctx context.Context, id string) (*entity.User, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "FindOne", ctx, id) + ret0, _ := ret[0].(*entity.User) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// FindOne indicates an expected call of FindOne. +func (mr *MockBaseUserRepositoryMockRecorder) FindOne(ctx, id any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FindOne", reflect.TypeOf((*MockBaseUserRepository)(nil).FindOne), ctx, id) +} + +// Insert mocks base method. +func (m *MockBaseUserRepository) Insert(ctx context.Context, data *entity.User) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Insert", ctx, data) + ret0, _ := ret[0].(error) + return ret0 +} + +// Insert indicates an expected call of Insert. +func (mr *MockBaseUserRepositoryMockRecorder) Insert(ctx, data any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Insert", reflect.TypeOf((*MockBaseUserRepository)(nil).Insert), ctx, data) +} + +// Update mocks base method. +func (m *MockBaseUserRepository) Update(ctx context.Context, data *entity.User) (*mongo.UpdateResult, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Update", ctx, data) + ret0, _ := ret[0].(*mongo.UpdateResult) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Update indicates an expected call of Update. +func (mr *MockBaseUserRepositoryMockRecorder) Update(ctx, data any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Update", reflect.TypeOf((*MockBaseUserRepository)(nil).Update), ctx, data) +} diff --git a/pkg/mock/repository/verify_code.go b/pkg/mock/repository/verify_code.go new file mode 100644 index 0000000..8f63be6 --- /dev/null +++ b/pkg/mock/repository/verify_code.go @@ -0,0 +1,84 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: ./pkg/domain/repository/verify_code.go +// +// Generated by this command: +// +// mockgen -source=./pkg/domain/repository/verify_code.go -destination=./pkg/mock/repository/verify_code.go -package=mock +// + +// Package mock is a generated GoMock package. +package mock + +import ( + context "context" + reflect "reflect" + + gomock "go.uber.org/mock/gomock" +) + +// MockVerifyCodeRepository is a mock of VerifyCodeRepository interface. +type MockVerifyCodeRepository struct { + ctrl *gomock.Controller + recorder *MockVerifyCodeRepositoryMockRecorder + isgomock struct{} +} + +// MockVerifyCodeRepositoryMockRecorder is the mock recorder for MockVerifyCodeRepository. +type MockVerifyCodeRepositoryMockRecorder struct { + mock *MockVerifyCodeRepository +} + +// NewMockVerifyCodeRepository creates a new mock instance. +func NewMockVerifyCodeRepository(ctrl *gomock.Controller) *MockVerifyCodeRepository { + mock := &MockVerifyCodeRepository{ctrl: ctrl} + mock.recorder = &MockVerifyCodeRepositoryMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockVerifyCodeRepository) EXPECT() *MockVerifyCodeRepositoryMockRecorder { + return m.recorder +} + +// DelVerifyCode mocks base method. +func (m *MockVerifyCodeRepository) DelVerifyCode(ctx context.Context, loginID, checkType string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DelVerifyCode", ctx, loginID, checkType) + ret0, _ := ret[0].(error) + return ret0 +} + +// DelVerifyCode indicates an expected call of DelVerifyCode. +func (mr *MockVerifyCodeRepositoryMockRecorder) DelVerifyCode(ctx, loginID, checkType any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DelVerifyCode", reflect.TypeOf((*MockVerifyCodeRepository)(nil).DelVerifyCode), ctx, loginID, checkType) +} + +// IsVerifyCodeExist mocks base method. +func (m *MockVerifyCodeRepository) IsVerifyCodeExist(ctx context.Context, loginID, checkType string) (string, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "IsVerifyCodeExist", ctx, loginID, checkType) + ret0, _ := ret[0].(string) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// IsVerifyCodeExist indicates an expected call of IsVerifyCodeExist. +func (mr *MockVerifyCodeRepositoryMockRecorder) IsVerifyCodeExist(ctx, loginID, checkType any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsVerifyCodeExist", reflect.TypeOf((*MockVerifyCodeRepository)(nil).IsVerifyCodeExist), ctx, loginID, checkType) +} + +// SetVerifyCode mocks base method. +func (m *MockVerifyCodeRepository) SetVerifyCode(ctx context.Context, loginID, checkType, code string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "SetVerifyCode", ctx, loginID, checkType, code) + ret0, _ := ret[0].(error) + return ret0 +} + +// SetVerifyCode indicates an expected call of SetVerifyCode. +func (mr *MockVerifyCodeRepositoryMockRecorder) SetVerifyCode(ctx, loginID, checkType, code any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetVerifyCode", reflect.TypeOf((*MockVerifyCodeRepository)(nil).SetVerifyCode), ctx, loginID, checkType, code) +} diff --git a/pkg/mock/usecase/generate_uid.go b/pkg/mock/usecase/generate_uid.go new file mode 100644 index 0000000..454eaf7 --- /dev/null +++ b/pkg/mock/usecase/generate_uid.go @@ -0,0 +1,56 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: ./pkg/domain/usecase/generate_uid.go +// +// Generated by this command: +// +// mockgen -source=./pkg/domain/usecase/generate_uid.go -destination=./pkg/mock/usecase/generate_uid.go -package=mock +// + +// Package mock is a generated GoMock package. +package mock + +import ( + context "context" + reflect "reflect" + + gomock "go.uber.org/mock/gomock" +) + +// MockUIDGenerateUseCase is a mock of UIDGenerateUseCase interface. +type MockUIDGenerateUseCase struct { + ctrl *gomock.Controller + recorder *MockUIDGenerateUseCaseMockRecorder + isgomock struct{} +} + +// MockUIDGenerateUseCaseMockRecorder is the mock recorder for MockUIDGenerateUseCase. +type MockUIDGenerateUseCaseMockRecorder struct { + mock *MockUIDGenerateUseCase +} + +// NewMockUIDGenerateUseCase creates a new mock instance. +func NewMockUIDGenerateUseCase(ctrl *gomock.Controller) *MockUIDGenerateUseCase { + mock := &MockUIDGenerateUseCase{ctrl: ctrl} + mock.recorder = &MockUIDGenerateUseCaseMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockUIDGenerateUseCase) EXPECT() *MockUIDGenerateUseCaseMockRecorder { + return m.recorder +} + +// Generate mocks base method. +func (m *MockUIDGenerateUseCase) Generate(ctx context.Context) (string, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Generate", ctx) + ret0, _ := ret[0].(string) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Generate indicates an expected call of Generate. +func (mr *MockUIDGenerateUseCaseMockRecorder) Generate(ctx any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Generate", reflect.TypeOf((*MockUIDGenerateUseCase)(nil).Generate), ctx) +} diff --git a/pkg/repository/account.go b/pkg/repository/account.go new file mode 100644 index 0000000..2eb9238 --- /dev/null +++ b/pkg/repository/account.go @@ -0,0 +1,155 @@ +package repository + +import ( + "app-cloudep-member-server/pkg/domain" + "app-cloudep-member-server/pkg/domain/entity" + "app-cloudep-member-server/pkg/domain/repository" + "context" + "errors" + "time" + + mgo "code.30cm.net/digimon/library-go/mongo" + "github.com/zeromicro/go-zero/core/stores/cache" + "github.com/zeromicro/go-zero/core/stores/mon" + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo" +) + +type AccountRepositoryParam struct { + Conf *mgo.Conf + CacheConf cache.CacheConf + DbOpts []mon.Option + CacheOpts []cache.Option +} + +type AccountRepository struct { + DB mgo.DocumentDBWithCacheUseCase +} + +func NewAccountRepository(param AccountRepositoryParam) repository.AccountRepository { + e := entity.Account{} + documentDB, err := mgo.MustDocumentDBWithCache( + param.Conf, + e.CollectionName(), + param.CacheConf, + param.DbOpts, + param.CacheOpts, + ) + if err != nil { + panic(err) + } + + return &AccountRepository{ + DB: documentDB, + } +} + +func (repo *AccountRepository) Insert(ctx context.Context, data *entity.Account) error { + if data.ID.IsZero() { + now := time.Now().UTC().UnixNano() + data.ID = primitive.NewObjectID() + data.CreateAt = &now + data.UpdateAt = &now + } + rk := domain.GetAccountRedisKey(data.ID.Hex()) + _, err := repo.DB.InsertOne(ctx, rk, data) + + return err +} + +func (repo *AccountRepository) FindOne(ctx context.Context, id string) (*entity.Account, error) { + oid, err := primitive.ObjectIDFromHex(id) + if err != nil { + return nil, ErrInvalidObjectID + } + + var data entity.Account + rk := domain.GetAccountRedisKey(id) + err = repo.DB.FindOne(ctx, rk, &data, bson.M{"_id": oid}) + + switch { + case err == nil: + return &data, nil + case errors.Is(err, mon.ErrNotFound): + return nil, ErrNotFound + default: + return nil, err + } +} + +func (repo *AccountRepository) Update(ctx context.Context, data *entity.Account) (*mongo.UpdateResult, error) { + now := time.Now().UTC().UnixNano() + data.UpdateAt = &now + + rk := domain.GetAccountRedisKey(data.ID.Hex()) + res, err := repo.DB.UpdateOne(ctx, rk, bson.M{"_id": data.ID}, bson.M{"$set": data}) + + return res, err +} + +func (repo *AccountRepository) Delete(ctx context.Context, id string) (int64, error) { + oid, err := primitive.ObjectIDFromHex(id) + if err != nil { + return 0, ErrInvalidObjectID + } + rk := domain.GetAccountRedisKey(id) + + return repo.DB.DeleteOne(ctx, rk, bson.M{"_id": oid}) +} + +func (repo *AccountRepository) FindOneByAccount(ctx context.Context, loginID string) (*entity.Account, error) { + // todo: 之後需要同步快取 + var data entity.Account + err := repo.DB.GetClient().FindOne(ctx, &data, bson.M{"login_id": loginID}) + switch { + case err == nil: + return &data, nil + case errors.Is(err, mon.ErrNotFound): + return nil, ErrNotFound + default: + return nil, err + } +} + +func (repo *AccountRepository) UpdateTokenByLoginID(ctx context.Context, account string, token string) error { + // todo: 之後需要同步快取 + filter := bson.M{"login_id": account} + update := bson.M{ + "$set": bson.M{ + "token": token, + "update_at": time.Now().UTC().UnixNano(), + }, + } + var data entity.Account + err := repo.DB.GetClient().FindOne(ctx, &data, filter) + if err != nil { + return err + } + + rk := domain.GetAccountRedisKey(data.ID.Hex()) + + modify, err := repo.DB.UpdateOne(ctx, rk, bson.M{"_id": data.ID}, update) + if err != nil { + return err + } + + if modify.MatchedCount == 0 { + return ErrNotFound // 自定義的錯誤表示未找到記錄 + } + + return nil +} + +func (repo *AccountRepository) Index20241226001UP(ctx context.Context) (*mongo.Cursor, error) { + // 等價於 db.account.createIndex({ "login_id": 1, "platform": 1}, {unique: true}) + repo.DB.PopulateMultiIndex(ctx, []string{ + "login_id", + "platform", + }, []int32{1, 1}, true) + + // 等價於 db.account.createIndex({"create_at": 1}) + repo.DB.PopulateIndex(ctx, "create_at", 1, false) + + return repo.DB.GetClient().Indexes().List(ctx) +} diff --git a/pkg/repository/account_test.go b/pkg/repository/account_test.go new file mode 100644 index 0000000..e27de71 --- /dev/null +++ b/pkg/repository/account_test.go @@ -0,0 +1,350 @@ +package repository + +import ( + "app-cloudep-member-server/pkg/domain/entity" + "app-cloudep-member-server/pkg/domain/repository" + "context" + "errors" + "testing" + "time" + + mgo "code.30cm.net/digimon/library-go/mongo" + "github.com/alicebob/miniredis/v2" + "github.com/stretchr/testify/assert" + "github.com/zeromicro/go-zero/core/stores/cache" + "go.mongodb.org/mongo-driver/bson/primitive" + + "github.com/zeromicro/go-zero/core/stores/redis" +) + +func SetupTestAccountRepository(db string) (repository.AccountRepository, func(), error) { + h, p, tearDown, err := startMongoContainer() + if err != nil { + return nil, nil, err + } + s, _ := miniredis.Run() + + conf := &mgo.Conf{ + Schema: Schema, + Host: h, + Port: p, + Database: db, + MaxStaleness: 300, + MaxPoolSize: 100, + MinPoolSize: 100, + MaxConnIdleTime: 300, + Compressors: []string{}, + EnableStandardReadWriteSplitMode: false, + ConnectTimeoutMs: 3000, + } + + cacheConf := cache.CacheConf{ + cache.NodeConf{ + RedisConf: redis.RedisConf{ + Host: s.Addr(), + Type: redis.NodeType, + }, + Weight: 100, + }, + } + + cacheOpts := []cache.Option{ + cache.WithExpiry(1000 * time.Microsecond), + cache.WithNotFoundExpiry(1000 * time.Microsecond), + } + + param := AccountRepositoryParam{ + Conf: conf, + CacheConf: cacheConf, + CacheOpts: cacheOpts, + } + repo := NewAccountRepository(param) + _, _ = repo.Index20241226001UP(context.Background()) + + return repo, tearDown, nil +} + +func TestAccountModel_Insert(t *testing.T) { + repo, tearDown, err := SetupTestAccountRepository("testDB") + defer tearDown() + assert.NoError(t, err) + + tests := []struct { + name string + account *entity.Account + expectError bool + }{ + { + name: "Valid account insert", + account: &entity.Account{ + LoginID: "testuser1", + Token: "testtoken1", + Platform: 1, + }, + expectError: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // 插入測試帳戶 + err := repo.Insert(context.Background(), tt.account) + if tt.expectError { + assert.Error(t, err) + } else { + assert.NoError(t, err, "插入操作應該成功") + + // 檢查是否生成了 ObjectID 和時間戳 + assert.NotZero(t, tt.account.ID, "ID 應該被生成") + assert.NotNil(t, tt.account.CreateAt, "CreateAt 應該被設置") + assert.NotNil(t, tt.account.UpdateAt, "UpdateAt 應該被設置") + + // 檢查插入的時間是否合理 + now := time.Now().UTC().UnixNano() + assert.LessOrEqual(t, *tt.account.CreateAt, now, "CreateAt 應在當前時間之前") + assert.LessOrEqual(t, *tt.account.UpdateAt, now, "UpdateAt 應在當前時間之前") + + // 確認插入的資料 + insertedAccount, err := repo.FindOne(context.Background(), tt.account.ID.Hex()) + assert.NoError(t, err, "應該可以找到插入的帳號資料") + assert.Equal(t, tt.account.LoginID, insertedAccount.LoginID, "LoginID 應相同") + assert.Equal(t, tt.account.Token, insertedAccount.Token, "Token 應相同") + assert.Equal(t, tt.account.Platform, insertedAccount.Platform, "Platform 應相同") + } + }) + } +} + +func TestAccountModel_FindOne(t *testing.T) { + repo, tearDown, err := SetupTestAccountRepository("testDB") + assert.NoError(t, err) + defer tearDown() + + // 插入測試帳戶 + account := &entity.Account{ + LoginID: "testuser1", + Token: "testtoken1", + Platform: 1, + } + err = repo.Insert(context.TODO(), account) + assert.NoError(t, err, "插入應成功") + nid := primitive.NewObjectID() + t.Logf("Inserted account ID: %s, nid:%s", account.ID.Hex(), nid.Hex()) + + tests := []struct { + name string + id string + expectError bool + expectedErr error + }{ + { + name: "Valid ID", + id: account.ID.Hex(), + expectError: false, + }, + { + name: "Non-existent ID", + id: nid.Hex(), + expectError: true, + expectedErr: ErrNotFound, + }, + { + name: "Invalid ID format", + id: "invalid-id", + expectError: true, + expectedErr: ErrInvalidObjectID, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result, err := repo.FindOne(context.TODO(), tt.id) + if tt.expectError { + assert.Error(t, err) + assert.Equal(t, tt.expectedErr, err) + } else { + assert.NoError(t, err) + assert.Equal(t, account.LoginID, result.LoginID) + assert.Equal(t, account.Token, result.Token) + } + }) + } +} + +func TestAccountModel_Update(t *testing.T) { + repo, tearDown, err := SetupTestAccountRepository("testDB") + assert.NoError(t, err) + defer tearDown() + + // 插入測試帳戶 + account := &entity.Account{ + LoginID: "testuser1", + Token: "testtoken1", + Platform: 1, + } + err = repo.Insert(context.Background(), account) + assert.NoError(t, err, "插入應成功") + + // 更新測試 + newToken := "updatedToken" + account.Token = newToken + + _, err = repo.Update(context.Background(), account) + assert.NoError(t, err, "更新應成功") + + // 驗證更新結果 + updatedAccount, err := repo.FindOne(context.Background(), account.ID.Hex()) + assert.NoError(t, err) + assert.Equal(t, newToken, updatedAccount.Token, "Token 應更新") +} + +func TestAccountModel_Delete(t *testing.T) { + repo, tearDown, err := SetupTestAccountRepository("testDB") + assert.NoError(t, err) + defer tearDown() + + // 插入測試帳戶 + account := &entity.Account{ + LoginID: "testuser1", + Token: "testtoken1", + Platform: 1, + } + err = repo.Insert(context.Background(), account) + assert.NoError(t, err, "插入應成功") + + tests := []struct { + name string + id string + expectError bool + expectedErr error + }{ + { + name: "Valid delete", + id: account.ID.Hex(), + expectError: false, + }, + { + name: "Invalid ID format", + id: "invalid-id", + expectError: true, + expectedErr: ErrInvalidObjectID, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + deletedCount, err := repo.Delete(context.Background(), tt.id) + if tt.expectError { + assert.Error(t, err) + assert.Equal(t, tt.expectedErr, err) + } else { + assert.NoError(t, err) + assert.Equal(t, int64(1), deletedCount, "刪除應成功") + } + }) + } +} + +func TestAccountModel_FindOneByAccount(t *testing.T) { + repo, tearDown, err := SetupTestAccountRepository("testDB") + assert.NoError(t, err) + defer tearDown() + + // 插入測試帳戶 + account := &entity.Account{ + LoginID: "testuser1", + Token: "testtoken1", + Platform: 1, + } + err = repo.Insert(context.Background(), account) + assert.NoError(t, err, "插入應成功") + + tests := []struct { + name string + loginID string + expectedErr error + expectFound bool + }{ + { + name: "Valid Account Found", + loginID: "testuser1", + expectedErr: nil, + expectFound: true, + }, + { + name: "Account Not Found", + loginID: "nonexistentuser", + expectedErr: ErrNotFound, + expectFound: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result, err := repo.FindOneByAccount(context.Background(), tt.loginID) + if tt.expectFound { + assert.NoError(t, err) + assert.Equal(t, account.LoginID, result.LoginID) + assert.Equal(t, account.Token, result.Token) + } else { + assert.Error(t, err) + assert.True(t, errors.Is(err, tt.expectedErr)) + } + }) + } +} + +func TestAccountModel_UpdateTokenByLoginID(t *testing.T) { + repo, tearDown, err := SetupTestAccountRepository("testDB") + assert.NoError(t, err) + defer tearDown() + + // 插入測試帳戶 + account := &entity.Account{ + LoginID: "testuser2", + Token: "initialtoken", + Platform: 1, + } + err = repo.Insert(context.Background(), account) + assert.NoError(t, err, "插入應成功") + + tests := []struct { + name string + loginID string + newToken string + expectedErr error + expectFound bool + }{ + { + name: "Valid Update Token", + loginID: "testuser2", + newToken: "newtoken123", + expectedErr: nil, + expectFound: true, + }, + { + name: "Account Not Found for Update", + loginID: "nonexistentuser", + newToken: "newtoken456", + expectedErr: ErrNotFound, + expectFound: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := repo.UpdateTokenByLoginID(context.Background(), tt.loginID, tt.newToken) + if tt.expectFound { + assert.NoError(t, err) + + // 驗證更新後的 token 值 + updatedAccount, findErr := repo.FindOneByAccount(context.Background(), tt.loginID) + assert.NoError(t, findErr) + assert.Equal(t, tt.newToken, updatedAccount.Token) + } else { + assert.Error(t, err) + assert.True(t, errors.Is(err, tt.expectedErr)) + } + }) + } +} diff --git a/pkg/repository/account_uid.go b/pkg/repository/account_uid.go new file mode 100644 index 0000000..795b216 --- /dev/null +++ b/pkg/repository/account_uid.go @@ -0,0 +1,125 @@ +package repository + +import ( + "app-cloudep-member-server/pkg/domain" + "app-cloudep-member-server/pkg/domain/entity" + "app-cloudep-member-server/pkg/domain/repository" + "context" + "errors" + "time" + + mgo "code.30cm.net/digimon/library-go/mongo" + "github.com/zeromicro/go-zero/core/stores/cache" + "github.com/zeromicro/go-zero/core/stores/mon" + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo" +) + +type AccountUIDRepositoryParam struct { + Conf *mgo.Conf + CacheConf cache.CacheConf + DbOpts []mon.Option + CacheOpts []cache.Option +} + +type AccountUIDRepository struct { + DB mgo.DocumentDBWithCacheUseCase +} + +func NewAccountUIDRepository(param AccountUIDRepositoryParam) repository.AccountUIDRepository { + e := entity.AccountUID{} + documentDB, err := mgo.MustDocumentDBWithCache( + param.Conf, + e.CollectionName(), + param.CacheConf, + param.DbOpts, + param.CacheOpts, + ) + if err != nil { + panic(err) + } + + return &AccountUIDRepository{ + DB: documentDB, + } +} + +func (repo *AccountUIDRepository) Insert(ctx context.Context, data *entity.AccountUID) error { + if data.ID.IsZero() { + now := time.Now().UTC().UnixNano() + data.ID = primitive.NewObjectID() + data.CreateAt = &now + data.UpdateAt = &now + } + rk := domain.GetAccountUIDRedisKey(data.ID.Hex()) + _, err := repo.DB.InsertOne(ctx, rk, data) + + return err +} + +func (repo *AccountUIDRepository) FindOne(ctx context.Context, id string) (*entity.AccountUID, error) { + oid, err := primitive.ObjectIDFromHex(id) + if err != nil { + return nil, ErrInvalidObjectID + } + + var data entity.AccountUID + rk := domain.GetAccountUIDRedisKey(id) + err = repo.DB.FindOne(ctx, rk, &data, bson.M{"_id": oid}) + switch { + case err == nil: + return &data, nil + case errors.Is(err, mon.ErrNotFound): + return nil, ErrNotFound + default: + return nil, err + } +} + +func (repo *AccountUIDRepository) Update(ctx context.Context, data *entity.AccountUID) (*mongo.UpdateResult, error) { + now := time.Now().UTC().UnixNano() + data.UpdateAt = &now + + rk := domain.GetAccountUIDRedisKey(data.ID.Hex()) + res, err := repo.DB.UpdateOne(ctx, rk, bson.M{"_id": data.ID}, bson.M{"$set": data}) + + return res, err +} + +func (repo *AccountUIDRepository) Delete(ctx context.Context, id string) (int64, error) { + oid, err := primitive.ObjectIDFromHex(id) + if err != nil { + return 0, ErrInvalidObjectID + } + rk := domain.GetAccountUIDRedisKey(id) + + return repo.DB.DeleteOne(ctx, rk, bson.M{"_id": oid}) +} + +func (repo *AccountUIDRepository) FindUIDByLoginID(ctx context.Context, loginID string) (*entity.AccountUID, error) { + var data entity.AccountUID + + err := repo.DB.GetClient().FindOne(ctx, &data, bson.M{"login_id": loginID}) + switch { + case err == nil: + return &data, nil + case errors.Is(err, mon.ErrNotFound): + return nil, ErrNotFound + default: + return nil, err + } +} + +func (repo *AccountUIDRepository) Index20241226001UP(ctx context.Context) (*mongo.Cursor, error) { + // 等價於 db.account_uid_binding.createIndex({"login_id": 1}, {unique: true}) + repo.DB.PopulateIndex(ctx, "login_id", 1, true) + + // 等價於 db.account_uid_binding.createIndex({"uid": 1}) + repo.DB.PopulateIndex(ctx, "uid", 1, false) + + // 等價於 db.account_uid_binding.createIndex({"create_at": 1}) + repo.DB.PopulateIndex(ctx, "create_at", 1, false) + + return repo.DB.GetClient().Indexes().List(ctx) +} diff --git a/pkg/repository/account_uid_test.go b/pkg/repository/account_uid_test.go new file mode 100644 index 0000000..29476b2 --- /dev/null +++ b/pkg/repository/account_uid_test.go @@ -0,0 +1,272 @@ +package repository + +import ( + "app-cloudep-member-server/pkg/domain/entity" + "app-cloudep-member-server/pkg/domain/repository" + "context" + "testing" + "time" + + mgo "code.30cm.net/digimon/library-go/mongo" + "github.com/alicebob/miniredis/v2" + "github.com/stretchr/testify/assert" + "github.com/zeromicro/go-zero/core/stores/cache" + "github.com/zeromicro/go-zero/core/stores/redis" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +func SetupTestAccountUIDRepository(db string) (repository.AccountUIDRepository, func(), error) { + h, p, tearDown, err := startMongoContainer() + if err != nil { + return nil, nil, err + } + s, _ := miniredis.Run() + + conf := &mgo.Conf{ + Schema: Schema, + Host: h, + Port: p, + Database: db, + MaxStaleness: 300, + MaxPoolSize: 100, + MinPoolSize: 100, + MaxConnIdleTime: 300, + Compressors: []string{}, + EnableStandardReadWriteSplitMode: false, + ConnectTimeoutMs: 3000, + } + + cacheConf := cache.CacheConf{ + cache.NodeConf{ + RedisConf: redis.RedisConf{ + Host: s.Addr(), + Type: redis.NodeType, + }, + Weight: 100, + }, + } + + cacheOpts := []cache.Option{ + cache.WithExpiry(1000 * time.Microsecond), + cache.WithNotFoundExpiry(1000 * time.Microsecond), + } + + param := AccountUIDRepositoryParam{ + Conf: conf, + CacheConf: cacheConf, + CacheOpts: cacheOpts, + } + + repo := NewAccountUIDRepository(param) + _, _ = repo.Index20241226001UP(context.Background()) + + return repo, tearDown, nil +} + +func TestDefaultAccountUidModel_Insert(t *testing.T) { + repo, tearDown, err := SetupTestAccountUIDRepository("testDB") + defer tearDown() + assert.NoError(t, err) + tests := []struct { + name string + accountUid *entity.AccountUID + expectError bool + }{ + { + name: "Valid AccountUid insert", + accountUid: &entity.AccountUID{ + LoginID: "testlogin1", + UID: "testuid1", + Type: 1, + }, + expectError: false, + }, + { + name: "Insert with missing UID", + accountUid: &entity.AccountUID{ + LoginID: "testlogin2", + Type: 2, + }, + expectError: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // 執行插入測試帳戶 UID + err := repo.Insert(context.Background(), tt.accountUid) + if tt.expectError { + assert.Error(t, err, "插入操作應該失敗") + } else { + assert.NoError(t, err, "插入操作應該成功") + + // 驗證 ObjectID 和時間戳是否生成 + assert.NotZero(t, tt.accountUid.ID, "應生成 ID") + assert.NotNil(t, tt.accountUid.CreateAt, "CreateAt 應被設置") + assert.NotNil(t, tt.accountUid.UpdateAt, "UpdateAt 應被設置") + + // 驗證插入的時間是否合理 + now := time.Now().UTC().UnixNano() + assert.LessOrEqual(t, *tt.accountUid.CreateAt, now, "CreateAt 應在當前時間之前") + assert.LessOrEqual(t, *tt.accountUid.UpdateAt, now, "UpdateAt 應在當前時間之前") + + // 驗證插入的資料是否正確 + insertedAccountUid, err := repo.FindOne(context.Background(), tt.accountUid.ID.Hex()) + assert.NoError(t, err, "應該可以找到插入的帳號 UID 資料") + assert.Equal(t, tt.accountUid.LoginID, insertedAccountUid.LoginID, "LoginID 應相同") + assert.Equal(t, tt.accountUid.UID, insertedAccountUid.UID, "UID 應相同") + assert.Equal(t, tt.accountUid.Type, insertedAccountUid.Type, "Type 應相同") + } + }) + } +} + +func TestDefaultAccountUidModel_FindOne(t *testing.T) { + repo, tearDown, err := SetupTestAccountUIDRepository("testDB") + defer tearDown() + assert.NoError(t, err) + + // 準備測試資料 + accountUid := &entity.AccountUID{ + LoginID: "testlogin", + UID: "testuid", + Type: 1, + } + err = repo.Insert(context.Background(), accountUid) + assert.NoError(t, err, "應成功插入測試資料") + + tests := []struct { + name string + id string + expectError bool + expectedUID string + }{ + { + name: "Valid FindOne", + id: accountUid.ID.Hex(), + expectError: false, + expectedUID: accountUid.UID, + }, + { + name: "Invalid ObjectID", + id: "invalid_id", + expectError: true, + }, + { + name: "Non-existent ObjectID", + id: primitive.NewObjectID().Hex(), + expectError: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result, err := repo.FindOne(context.Background(), tt.id) + if tt.expectError { + assert.Error(t, err) + } else { + assert.NoError(t, err) + assert.Equal(t, tt.expectedUID, result.UID, "找到的 UID 應符合預期") + } + }) + } +} + +func TestDefaultAccountUidModel_Update(t *testing.T) { + repo, tearDown, err := SetupTestAccountUIDRepository("testDB") + defer tearDown() + assert.NoError(t, err) + + // 準備測試資料 + accountUid := &entity.AccountUID{ + LoginID: "testlogin", + UID: "testuid", + Type: 1, + } + err = repo.Insert(context.Background(), accountUid) + assert.NoError(t, err) + + updatedUID := "updatedUID" + accountUid.UID = updatedUID + + // 執行更新操作 + _, err = repo.Update(context.Background(), accountUid) + assert.NoError(t, err, "應成功更新資料") + + // 確認更新結果 + updatedAccountUid, err := repo.FindOne(context.Background(), accountUid.ID.Hex()) + assert.NoError(t, err, "應能找到更新後的資料") + assert.Equal(t, updatedUID, updatedAccountUid.UID, "更新後的 UID 應符合預期") +} + +func TestDefaultAccountUidModel_Delete(t *testing.T) { + repo, tearDown, err := SetupTestAccountUIDRepository("testDB") + defer tearDown() + assert.NoError(t, err) + + // 準備測試資料 + accountUid := &entity.AccountUID{ + LoginID: "testlogin", + UID: "testuid", + Type: 1, + } + err = repo.Insert(context.Background(), accountUid) + assert.NoError(t, err) + + // 執行刪除操作 + count, err := repo.Delete(context.Background(), accountUid.ID.Hex()) + assert.NoError(t, err, "應成功刪除資料") + assert.Equal(t, int64(1), count, "刪除數量應為 1") + + // 確認資料已被刪除 + _, err = repo.FindOne(context.Background(), accountUid.ID.Hex()) + assert.Error(t, err, "應無法找到已刪除的資料") + assert.Equal(t, ErrNotFound, err, "應返回 ErrNotFound 錯誤") +} + +func TestCustomAccountUidModel_FindUIDByLoginID(t *testing.T) { + repo, tearDown, err := SetupTestAccountUIDRepository("testDB") + defer tearDown() + assert.NoError(t, err) + + // 準備測試資料 + accountUid := &entity.AccountUID{ + LoginID: "testloginid", + UID: "testuid", + Type: 1, + } + err = repo.Insert(context.Background(), accountUid) + assert.NoError(t, err, "應成功插入測試資料") + + tests := []struct { + name string + loginID string + expectError bool + expectedUID string + }{ + { + name: "Valid FindUIDByLoginID", + loginID: "testloginid", + expectError: false, + expectedUID: "testuid", + }, + { + name: "Non-existent LoginID", + loginID: "nonexistent", + expectError: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result, err := repo.FindUIDByLoginID(context.Background(), tt.loginID) + if tt.expectError { + assert.Error(t, err) + assert.Equal(t, ErrNotFound, err, "應返回 ErrNotFound 錯誤") + } else { + assert.NoError(t, err, "應成功找到符合的記錄") + assert.Equal(t, tt.expectedUID, result.UID, "找到的 UID 應符合預期") + } + }) + } +} diff --git a/pkg/repository/auto_id.go b/pkg/repository/auto_id.go new file mode 100644 index 0000000..9d48a70 --- /dev/null +++ b/pkg/repository/auto_id.go @@ -0,0 +1,124 @@ +package repository + +import ( + "app-cloudep-member-server/pkg/domain/entity" + "app-cloudep-member-server/pkg/domain/repository" + "context" + "errors" + "time" + + GIDLib "code.30cm.net/digimon/library-go/utils/invited_code" + + mgo "code.30cm.net/digimon/library-go/mongo" + "github.com/zeromicro/go-zero/core/stores/mon" + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" +) + +type AutoIDRepositoryParam struct { + Conf *mgo.Conf + DbOpts []mon.Option +} + +type AutoIDRepository struct { + DB mgo.DocumentDBUseCase + UIDTrans GIDLib.ConvertUseCase +} + +func NewAutoIDRepository(param AutoIDRepositoryParam) repository.AutoIDRepository { + e := entity.AutoID{} + documentDB, err := mgo.NewDocumentDB(param.Conf, e.CollectionName(), param.DbOpts...) + if err != nil { + panic(err) + } + + return &AutoIDRepository{ + DB: documentDB, + UIDTrans: GIDLib.MustConverter(10, 8, GIDLib.ConvertTable), + } +} + +func (repo *AutoIDRepository) Insert(ctx context.Context, data *entity.AutoID) error { + if data.ID.IsZero() { + now := time.Now().UTC().UnixNano() + data.ID = primitive.NewObjectID() + data.CreateAt = &now + data.UpdateAt = &now + } + _, err := repo.DB.GetClient().InsertOne(ctx, data) + + return err +} + +func (repo *AutoIDRepository) FindOne(ctx context.Context, id string) (*entity.AutoID, error) { + oid, err := primitive.ObjectIDFromHex(id) + if err != nil { + return nil, ErrInvalidObjectID + } + + var data entity.AutoID + + err = repo.DB.GetClient().FindOne(ctx, &data, bson.M{"_id": oid}) + switch { + case err == nil: + return &data, nil + case errors.Is(err, mon.ErrNotFound): + return nil, ErrNotFound + default: + return nil, err + } +} + +func (repo *AutoIDRepository) Update(ctx context.Context, data *entity.AutoID) (*mongo.UpdateResult, error) { + now := time.Now().UTC().UnixNano() + data.UpdateAt = &now + + res, err := repo.DB.GetClient().UpdateOne(ctx, bson.M{"_id": data.ID}, bson.M{"$set": data}) + + return res, err +} + +func (repo *AutoIDRepository) Delete(ctx context.Context, id string) (int64, error) { + oid, err := primitive.ObjectIDFromHex(id) + if err != nil { + return 0, ErrInvalidObjectID + } + + res, err := repo.DB.GetClient().DeleteOne(ctx, bson.M{"_id": oid}) + + return res, err +} + +func (repo *AutoIDRepository) Inc(ctx context.Context, data *entity.AutoID) error { + // 定義查詢的條件 + filter := bson.M{"name": "auto_id"} + + // 定義更新的操作,包括自增和更新時間 + update := bson.M{ + "$inc": bson.M{"counter": 1}, // 自增 counter + "$set": bson.M{"updateAt": time.Now().UTC().UnixNano()}, // 設置 updateAt 為當前時間 + } + // 使用 FindOneAndUpdate 並將結果解碼到 data 中 + err := repo.DB.GetClient().FindOneAndUpdate(ctx, &data, filter, update, + options.FindOneAndUpdate().SetUpsert(true), + options.FindOneAndUpdate().SetReturnDocument(options.After)) + + return err +} + +func (repo *AutoIDRepository) Index20241226001UP(ctx context.Context) (*mongo.Cursor, error) { + // db.count.createIndex({ "name": 1 }, { unique: true }); + repo.DB.PopulateIndex(ctx, "name", 1, true) + + return repo.DB.GetClient().Indexes().List(ctx) +} + +func (repo *AutoIDRepository) GetUIDFromNum(num int64) (string, error) { + return repo.UIDTrans.EncodeFromNum(num) +} + +func (repo *AutoIDRepository) GetNumFromUID(uid string) (int64, error) { + return repo.UIDTrans.DecodeFromCode(uid) +} diff --git a/pkg/repository/auto_id_test.go b/pkg/repository/auto_id_test.go new file mode 100644 index 0000000..c3b1f34 --- /dev/null +++ b/pkg/repository/auto_id_test.go @@ -0,0 +1,278 @@ +package repository + +import ( + "app-cloudep-member-server/pkg/domain/entity" + "app-cloudep-member-server/pkg/domain/repository" + "context" + "testing" + "time" + + mgo "code.30cm.net/digimon/library-go/mongo" + "github.com/stretchr/testify/assert" +) + +func SetupTestAutoIDRepository(db string) (repository.AutoIDRepository, func(), error) { + h, p, tearDown, err := startMongoContainer() + if err != nil { + return nil, nil, err + } + + conf := &mgo.Conf{ + Schema: Schema, + Host: h, + Port: p, + Database: db, + MaxStaleness: 300, + MaxPoolSize: 100, + MinPoolSize: 100, + MaxConnIdleTime: 300, + Compressors: []string{}, + EnableStandardReadWriteSplitMode: false, + ConnectTimeoutMs: 3000, + } + + param := AutoIDRepositoryParam{ + Conf: conf, + } + + repo := NewAutoIDRepository(param) + _, _ = repo.Index20241226001UP(context.Background()) + + return repo, tearDown, nil +} + +func TestDefaultAutoIDModel_Insert(t *testing.T) { + repo, tearDown, err := SetupTestAutoIDRepository("testDB") + defer tearDown() + assert.NoError(t, err) + + tests := []struct { + name string + data *entity.AutoID + expectError bool + }{ + { + name: "Valid AutoID insert", + data: &entity.AutoID{ + Name: "testCounter", + Counter: 100, + }, + expectError: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := repo.Insert(context.Background(), tt.data) + if tt.expectError { + assert.Error(t, err) + } else { + assert.NoError(t, err, "插入操作應該成功") + + // 檢查生成的 ID 和時間戳 + assert.NotZero(t, tt.data.ID, "ID 應該被生成") + assert.NotNil(t, tt.data.CreateAt, "CreateAt 應該被設置") + assert.NotNil(t, tt.data.UpdateAt, "UpdateAt 應該被設置") + + // 確認插入的資料是否正確 + now := time.Now().UTC().UnixNano() + assert.LessOrEqual(t, *tt.data.CreateAt, now, "CreateAt 應在當前時間之前") + assert.LessOrEqual(t, *tt.data.UpdateAt, now, "UpdateAt 應在當前時間之前") + + insertedAutoID, err := repo.FindOne(context.Background(), tt.data.ID.Hex()) + assert.NoError(t, err, "應該可以找到插入的資料") + assert.Equal(t, tt.data.Name, insertedAutoID.Name, "Name 應相同") + assert.Equal(t, tt.data.Counter, insertedAutoID.Counter, "Counter 應相同") + } + }) + } +} + +func TestDefaultAutoIDModel_FindOne(t *testing.T) { + repo, tearDown, err := SetupTestAutoIDRepository("testDB") + defer tearDown() + assert.NoError(t, err) + + inserted := &entity.AutoID{ + Name: "findTestCounter", + Counter: 200, + } + err = repo.Insert(context.Background(), inserted) + assert.NoError(t, err) + + tests := []struct { + name string + id string + expectError bool + expectedID string + }{ + { + name: "Valid FindOne by ID", + id: inserted.ID.Hex(), + expectError: false, + expectedID: inserted.ID.Hex(), + }, + { + name: "Invalid ObjectID", + id: "invalidObjectID", + expectError: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result, err := repo.FindOne(context.Background(), tt.id) + if tt.expectError { + assert.Error(t, err) + } else { + assert.NoError(t, err) + assert.Equal(t, tt.expectedID, result.ID.Hex(), "ID 應相同") + } + }) + } +} + +func TestDefaultAutoIDModel_Update(t *testing.T) { + repo, tearDown, err := SetupTestAutoIDRepository("testDB") + defer tearDown() + assert.NoError(t, err) + + inserted := &entity.AutoID{ + Name: "updateTestCounter", + Counter: 300, + } + err = repo.Insert(context.Background(), inserted) + assert.NoError(t, err) + + updatedData := *inserted // 創建副本進行更新 + updatedData.Counter = 400 + + tests := []struct { + name string + data *entity.AutoID + expectError bool + }{ + { + name: "Valid Update", + data: &updatedData, + expectError: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + _, err := repo.Update(context.Background(), tt.data) + if tt.expectError { + assert.Error(t, err) + } else { + assert.NoError(t, err) + // 驗證更新後的 Counter + updatedAutoID, err := repo.FindOne(context.Background(), tt.data.ID.Hex()) + assert.NoError(t, err) + assert.Equal(t, tt.data.Counter, updatedAutoID.Counter, "Counter 應相同") + } + }) + } +} + +func TestDefaultAutoIDModel_Delete(t *testing.T) { + repo, tearDown, err := SetupTestAutoIDRepository("testDB") + defer tearDown() + assert.NoError(t, err) + + inserted := &entity.AutoID{ + Name: "deleteTestCounter", + Counter: 500, + } + err = repo.Insert(context.Background(), inserted) + assert.NoError(t, err) + + tests := []struct { + name string + id string + expectError bool + }{ + { + name: "Valid Delete by ID", + id: inserted.ID.Hex(), + expectError: false, + }, + { + name: "Invalid ObjectID", + id: "invalidObjectID", + expectError: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + count, err := repo.Delete(context.Background(), tt.id) + if tt.expectError { + assert.Error(t, err) + } else { + assert.NoError(t, err) + assert.Equal(t, int64(1), count, "刪除應影響 1 條記錄") + + // 確認已刪除 + _, err = repo.FindOne(context.Background(), tt.id) + assert.ErrorIs(t, err, ErrNotFound, "記錄應該不存在") + } + }) + } +} + +func TestCustomAutoIDModel_Inc(t *testing.T) { + + tests := []struct { + name string + initialData *entity.AutoID + expectError bool + }{ + { + name: "Increment non-existing counter (upsert)", + initialData: nil, // 不提供初始數據,預期會自動插入新數據 + expectError: false, + }, + { + name: "Increment existing counter", + initialData: &entity.AutoID{ + Name: "auto_id", + Counter: 5, + }, + expectError: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + repo, tearDown, err := SetupTestAutoIDRepository("testDB") + defer tearDown() + assert.NoError(t, err) + // 插入初始數據(如果有) + if tt.initialData != nil { + err := repo.Insert(context.Background(), tt.initialData) + assert.NoError(t, err, "初始插入操作應該成功") + } + + // 創建一個 AutoID 結構來保存增量操作後的結果 + var result entity.AutoID + + // 執行 Inc 方法 + err = repo.Inc(context.Background(), &result) + if tt.expectError { + assert.Error(t, err, "應該返回錯誤") + } else { + assert.NoError(t, err, "Inc 操作應該成功") + + // 驗證結果中的 Counter 值是否正確增量 + expectedCounter := uint64(1) + if tt.initialData != nil { + expectedCounter = tt.initialData.Counter + 1 + } + + assert.Equal(t, expectedCounter, result.Counter, "Counter 值應該自增") + + } + }) + } +} diff --git a/pkg/repository/error.go b/pkg/repository/error.go new file mode 100755 index 0000000..c98cf79 --- /dev/null +++ b/pkg/repository/error.go @@ -0,0 +1,12 @@ +package repository + +import ( + "errors" + + "github.com/zeromicro/go-zero/core/stores/mon" +) + +var ( + ErrNotFound = mon.ErrNotFound + ErrInvalidObjectID = errors.New("invalid objectId") +) diff --git a/pkg/repository/start_mongo_container_test.go b/pkg/repository/start_mongo_container_test.go new file mode 100644 index 0000000..1bcfe1a --- /dev/null +++ b/pkg/repository/start_mongo_container_test.go @@ -0,0 +1,52 @@ +package repository + +import ( + "context" + "fmt" + + "github.com/testcontainers/testcontainers-go" + "github.com/testcontainers/testcontainers-go/wait" +) + +const ( + Host = "127.0.0.1" + Port = "27017" + Schema = "mongodb" +) + +func startMongoContainer() (string, string, func(), error) { + ctx := context.Background() + + req := testcontainers.ContainerRequest{ + Image: "mongo:latest", + ExposedPorts: []string{"27017/tcp"}, + WaitingFor: wait.ForListeningPort("27017/tcp"), + } + + mongoC, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{ + ContainerRequest: req, + Started: true, + }) + if err != nil { + return "", "", nil, err + } + + port, err := mongoC.MappedPort(ctx, Port) + if err != nil { + return "", "", nil, err + } + + host, err := mongoC.Host(ctx) + if err != nil { + return "", "", nil, err + } + + uri := fmt.Sprintf("mongodb://%s:%s", host, port.Port()) + tearDown := func() { + mongoC.Terminate(ctx) + } + + fmt.Printf("Connecting to %s\n", uri) + + return host, port.Port(), tearDown, nil +} diff --git a/pkg/repository/user.go b/pkg/repository/user.go new file mode 100644 index 0000000..8b393df --- /dev/null +++ b/pkg/repository/user.go @@ -0,0 +1,368 @@ +package repository + +import ( + "app-cloudep-member-server/pkg/domain" + "app-cloudep-member-server/pkg/domain/entity" + "app-cloudep-member-server/pkg/domain/repository" + "context" + "errors" + "fmt" + + mgo "code.30cm.net/digimon/library-go/mongo" + + "time" + + "github.com/zeromicro/go-zero/core/stores/cache" + "github.com/zeromicro/go-zero/core/stores/mon" + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" +) + +type UserRepositoryParam struct { + Conf *mgo.Conf + CacheConf cache.CacheConf + DbOpts []mon.Option + CacheOpts []cache.Option +} + +type UserRepository struct { + DB mgo.DocumentDBWithCacheUseCase +} + +func NewUserRepository(param UserRepositoryParam) repository.UserRepository { + e := entity.User{} + documentDB, err := mgo.MustDocumentDBWithCache( + param.Conf, + e.CollectionName(), + param.CacheConf, + param.DbOpts, + param.CacheOpts, + ) + if err != nil { + panic(err) + } + + return &UserRepository{ + DB: documentDB, + } +} + +func (repo *UserRepository) Insert(ctx context.Context, data *entity.User) error { + if data.ID.IsZero() { + now := time.Now().UTC().UnixNano() + data.ID = primitive.NewObjectID() + data.CreateAt = &now + data.UpdateAt = &now + } + + rk := domain.GetUserRedisKey(data.ID.Hex()) + _, err := repo.DB.InsertOne(ctx, rk, data) + + return err +} + +func (repo *UserRepository) FindOne(ctx context.Context, id string) (*entity.User, error) { + oid, err := primitive.ObjectIDFromHex(id) + if err != nil { + return nil, ErrInvalidObjectID + } + + var data entity.User + rk := domain.GetUserRedisKey(id) + err = repo.DB.FindOne(ctx, rk, &data, bson.M{"_id": oid}) + switch { + case err == nil: + return &data, nil + case errors.Is(err, mon.ErrNotFound): + return nil, ErrNotFound + default: + return nil, err + } +} + +func (repo *UserRepository) Update(ctx context.Context, data *entity.User) (*mongo.UpdateResult, error) { + now := time.Now().UTC().UnixNano() + data.UpdateAt = &now + + rk := domain.GetUserRedisKey(data.ID.Hex()) + res, err := repo.DB.UpdateOne(ctx, rk, bson.M{"_id": data.ID}, bson.M{"$set": data}) + + return res, err +} + +func (repo *UserRepository) Delete(ctx context.Context, id string) (int64, error) { + oid, err := primitive.ObjectIDFromHex(id) + if err != nil { + return 0, ErrInvalidObjectID + } + + rk := domain.GetUserRedisKey(id) + res, err := repo.DB.DeleteOne(ctx, rk, bson.M{"_id": oid}) + + return res, err +} + +func (repo *UserRepository) UpdateUserDetailsByUID(ctx context.Context, data *repository.UpdateUserInfoRequest) error { + updateFields := bson.M{} + + if data.AlarmCategory != nil { + updateFields["alarm_category"] = *data.AlarmCategory + } + if data.UserStatus != nil { + updateFields["user_status"] = *data.UserStatus + } + if data.PreferredLanguage != nil { + updateFields["preferred_language"] = *data.PreferredLanguage + } + if data.Currency != nil { + updateFields["currency"] = *data.Currency + } + if data.Nickname != nil { + updateFields["nickname"] = *data.Nickname + } + if data.AvatarURL != nil { + updateFields["avatar_url"] = *data.AvatarURL + } + if data.FullName != nil { + updateFields["full_name"] = *data.FullName + } + if data.GenderCode != nil { + updateFields["gender_code"] = *data.GenderCode + } + if data.Birthdate != nil { + updateFields["birthdate"] = *data.Birthdate + } + if data.Address != nil { + updateFields["address"] = *data.Address + } + + if len(updateFields) == 0 { + return nil + } + + updateFields["update_at"] = time.Now().UTC().UnixNano() + + filter := bson.M{"uid": data.UID} + update := bson.M{"$set": updateFields} + + // 不常寫,再找一次可接受 + id := repo.UIDToID(ctx, data.UID) + if id == "" { + return errors.New("invalid uid") + } + rk := domain.GetUserRedisKey(id) + result, err := repo.DB.UpdateOne(ctx, rk, filter, update, options.Update().SetUpsert(false)) + if err != nil { + return err + } + + if result.MatchedCount == 0 { + return ErrNotFound // 自定義的錯誤表示未找到記錄 + } + + return nil +} + +func (repo *UserRepository) UpdateStatus(ctx context.Context, uid string, status int32) error { + filter := bson.M{"uid": uid} + + // 構建更新內容,僅更新 status 字段並記錄 update_at 時間 + update := bson.M{ + "$set": bson.M{ + "user_status": status, + "update_at": time.Now().UTC().UnixNano(), + }, + } + + // 不常寫,再找一次可接受 + id := repo.UIDToID(ctx, uid) + if id == "" { + return errors.New("invalid uid") + } + rk := domain.GetUserRedisKey(id) + + // 執行更新操作 + result, err := repo.DB.UpdateOne(ctx, rk, filter, update, options.Update().SetUpsert(false)) + if err != nil { + return fmt.Errorf("failed to update status for uid %s: %w", uid, err) + } + + // 檢查更新結果,若沒有匹配的文檔,則返回錯誤 + if result.MatchedCount == 0 { + return ErrNotFound // 自定義的錯誤表示未找到記錄 + } + + return nil +} + +func (repo *UserRepository) FindOneByUID(ctx context.Context, uid string) (*entity.User, error) { + // 構建查找條件 + filter := bson.M{"uid": uid} + var data entity.User + + // 不常寫,再找一次可接受 + id := repo.UIDToID(ctx, uid) + if id == "" { + return nil, errors.New("invalid uid") + } + rk := domain.GetUserRedisKey(id) + + err := repo.DB.FindOne(ctx, rk, &data, filter) + switch { + case err == nil: + return &data, nil + case errors.Is(err, mon.ErrNotFound): + return nil, ErrNotFound + default: + return nil, err + } +} + +func (repo *UserRepository) FindOneByNickName(ctx context.Context, nickName string) (*entity.User, error) { + // 構建查找條件 + filter := bson.M{"nickname": nickName} + var data entity.User + + err := repo.DB.GetClient().FindOne(ctx, &data, filter) + switch { + case err == nil: + return &data, nil + case errors.Is(err, mon.ErrNotFound): + return nil, ErrNotFound + default: + return nil, err + } +} + +func (repo *UserRepository) ListMembers(ctx context.Context, params *repository.UserQueryParams) ([]*entity.User, int64, error) { + // 構建查詢條件 + filter := bson.M{} + + if params.AlarmCategory != nil { + filter["alarm_category"] = *params.AlarmCategory + } + if params.UserStatus != nil { + filter["user_status"] = *params.UserStatus + } + if params.CreateStartTime != nil || params.CreateEndTime != nil { + timeFilter := bson.M{} + if params.CreateStartTime != nil { + timeFilter["$gte"] = *params.CreateStartTime + } + if params.CreateEndTime != nil { + timeFilter["$lte"] = *params.CreateEndTime + } + filter["create_at"] = timeFilter + } + + // 計算符合條件的總數 + count, err := repo.DB.GetClient().CountDocuments(ctx, filter) + if err != nil { + return nil, 0, err + } + + // 構建查詢選項(分頁) + opts := options.Find(). + SetSkip(params.PageSize * (params.PageIndex - 1)). + SetLimit(params.PageSize) + + // 執行查詢 + var users = make([]*entity.User, 0, params.PageSize) + err = repo.DB.GetClient().Find(ctx, &users, filter, opts) + if err != nil { + return nil, 0, err + } + + return users, count, nil +} + +func (repo *UserRepository) UpdateEmailVerifyStatus(ctx context.Context, uid, email string) error { + // 構建查找條件 + filter := bson.M{"uid": uid} + + // 構建更新內容,僅更新 status 字段並記錄 update_at 時間 + update := bson.M{ + "$set": bson.M{ + "email": email, + "update_at": time.Now().UTC().UnixNano(), + }, + } + + // 不常寫,再找一次可接受 + id := repo.UIDToID(ctx, uid) + if id == "" { + return errors.New("invalid uid") + } + rk := domain.GetUserRedisKey(id) + + // 執行更新操作 + result, err := repo.DB.UpdateOne(ctx, rk, filter, update, options.Update().SetUpsert(false)) + if err != nil { + return fmt.Errorf("failed to update status for uid %s: %w", uid, err) + } + + // 檢查更新結果,若沒有匹配的文檔,則返回錯誤 + if result.MatchedCount == 0 { + return ErrNotFound // 自定義的錯誤表示未找到記錄 + } + + return nil +} + +func (repo *UserRepository) UpdatePhoneVerifyStatus(ctx context.Context, uid, phone string) error { + // 構建查找條件 + filter := bson.M{"uid": uid} + + // 構建更新內容,僅更新 status 字段並記錄 update_at 時間 + update := bson.M{ + "$set": bson.M{ + "phone_number": phone, + "update_at": time.Now().UTC().UnixNano(), + }, + } + + // 不常寫,再找一次可接受 + id := repo.UIDToID(ctx, uid) + if id == "" { + return errors.New("invalid uid") + } + rk := domain.GetUserRedisKey(id) + + // 執行更新操作 + result, err := repo.DB.UpdateOne(ctx, rk, filter, update, options.Update().SetUpsert(false)) + if err != nil { + return fmt.Errorf("failed to update status for uid %s: %w", uid, err) + } + + // 檢查更新結果,若沒有匹配的文檔,則返回錯誤 + if result.MatchedCount == 0 { + return ErrNotFound // 自定義的錯誤表示未找到記錄 + } + + return nil +} + +func (repo *UserRepository) UIDToID(ctx context.Context, uid string) string { + filter := bson.M{"uid": uid} + var user entity.User + opts := options.FindOne().SetProjection(bson.M{ + "_id": 1, + }) + err := repo.DB.GetClient().FindOne(ctx, &user, filter, opts) + if err != nil { + return "" + } + + return user.ID.Hex() +} + +func (repo *UserRepository) Index20241226001UP(ctx context.Context) (*mongo.Cursor, error) { + // db.user_info.createIndex({"uid": 1},{unique: true}) + repo.DB.PopulateIndex(ctx, "uid", 1, true) + // db.user_info.createIndex({"create_at": 1}) + repo.DB.PopulateIndex(ctx, "create_at", 1, false) + + return repo.DB.GetClient().Indexes().List(ctx) +} diff --git a/pkg/repository/user_test.go b/pkg/repository/user_test.go new file mode 100644 index 0000000..b179b29 --- /dev/null +++ b/pkg/repository/user_test.go @@ -0,0 +1,386 @@ +package repository + +import ( + "app-cloudep-member-server/pkg/domain/entity" + "app-cloudep-member-server/pkg/domain/member" + "app-cloudep-member-server/pkg/domain/repository" + "context" + "errors" + "testing" + "time" + + mgo "code.30cm.net/digimon/library-go/mongo" + "github.com/alicebob/miniredis/v2" + "github.com/stretchr/testify/assert" + "github.com/zeromicro/go-zero/core/stores/cache" + "github.com/zeromicro/go-zero/core/stores/redis" + "go.mongodb.org/mongo-driver/bson/primitive" + "google.golang.org/protobuf/proto" +) + +func SetupTestUserRepository(db string) (repository.UserRepository, func(), error) { + h, p, tearDown, err := startMongoContainer() + if err != nil { + return nil, nil, err + } + s, _ := miniredis.Run() + + conf := &mgo.Conf{ + Schema: Schema, + Host: h, + Port: p, + Database: db, + MaxStaleness: 300, + MaxPoolSize: 100, + MinPoolSize: 100, + MaxConnIdleTime: 300, + Compressors: []string{}, + EnableStandardReadWriteSplitMode: false, + ConnectTimeoutMs: 3000, + } + + cacheConf := cache.CacheConf{ + cache.NodeConf{ + RedisConf: redis.RedisConf{ + Host: s.Addr(), + Type: redis.NodeType, + }, + Weight: 100, + }, + } + + cacheOpts := []cache.Option{ + cache.WithExpiry(1000 * time.Microsecond), + cache.WithNotFoundExpiry(1000 * time.Microsecond), + } + + param := UserRepositoryParam{ + Conf: conf, + CacheConf: cacheConf, + CacheOpts: cacheOpts, + } + + repo := NewUserRepository(param) + _, _ = repo.Index20241226001UP(context.Background()) + + return repo, tearDown, nil +} + +func TestCustomUserModel_UpdateEmailVerifyStatus(t *testing.T) { + repo, tearDown, err := SetupTestUserRepository("testDB") + defer tearDown() + assert.NoError(t, err) + + initialUser := &entity.User{ + UID: "test_uid_email", + Email: proto.String("old_email@example.com"), + } + err = repo.Insert(context.Background(), initialUser) + assert.NoError(t, err) + + err = repo.UpdateEmailVerifyStatus(context.Background(), "test_uid_email", "new_email@example.com") + assert.NoError(t, err) + + updatedUser, err := repo.FindOneByUID(context.Background(), "test_uid_email") + assert.NoError(t, err) + assert.Equal(t, "new_email@example.com", *updatedUser.Email) +} + +func TestCustomUserModel_UpdatePhoneVerifyStatus(t *testing.T) { + repo, tearDown, err := SetupTestUserRepository("testDB") + defer tearDown() + assert.NoError(t, err) + + initialUser := &entity.User{ + UID: "test_uid_phone", + PhoneNumber: proto.String("123456789"), + } + err = repo.Insert(context.Background(), initialUser) + assert.NoError(t, err) + + err = repo.UpdatePhoneVerifyStatus(context.Background(), "test_uid_phone", "987654321") + assert.NoError(t, err) + + updatedUser, err := repo.FindOneByUID(context.Background(), "test_uid_phone") + assert.NoError(t, err) + assert.Equal(t, "987654321", *updatedUser.PhoneNumber) +} + +func TestCustomUserModel_UpdateUserDetailsByUID(t *testing.T) { + repo, tearDown, err := SetupTestUserRepository("testDB") + defer tearDown() + assert.NoError(t, err) + + tests := []struct { + name string + initialData *entity.User + updateData *repository.UpdateUserInfoRequest + expectError bool + }{ + { + name: "Update user details by UID", + initialData: &entity.User{ + UID: "test_uid_1", + Nickname: proto.String("old_nickname"), + FullName: proto.String("Old Full Name"), + }, + updateData: &repository.UpdateUserInfoRequest{ + UID: "test_uid_1", + Nickname: proto.String("new_nickname"), + FullName: proto.String("New Full Name"), + }, + expectError: false, + }, + { + name: "User not found", + initialData: nil, + updateData: &repository.UpdateUserInfoRequest{ + UID: "non_existent_uid", + Nickname: proto.String("nickname"), + }, + expectError: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.initialData != nil { + err := repo.Insert(context.Background(), tt.initialData) + assert.NoError(t, err) + } + + err := repo.UpdateUserDetailsByUID(context.Background(), tt.updateData) + if tt.expectError { + assert.Error(t, err) + } else { + assert.NoError(t, err) + + updatedUser, err := repo.FindOneByUID(context.Background(), tt.updateData.UID) + assert.NoError(t, err) + assert.Equal(t, *tt.updateData.Nickname, *updatedUser.Nickname) + assert.Equal(t, *tt.updateData.FullName, *updatedUser.FullName) + } + }) + } +} + +func TestCustomUserModel_FindOneByUID(t *testing.T) { + repo, tearDown, err := SetupTestUserRepository("testDB") + defer tearDown() + assert.NoError(t, err) + + initialUser := &entity.User{ + UID: "test_uid", + } + + err = repo.Insert(context.Background(), initialUser) + assert.NoError(t, err) + + user, err := repo.FindOneByUID(context.Background(), "test_uid") + assert.NoError(t, err) + assert.Equal(t, "test_uid", user.UID) +} + +func TestCustomUserModel_ListMembers(t *testing.T) { + repo, tearDown, err := SetupTestUserRepository("testDB") + defer tearDown() + assert.NoError(t, err) + + users := []*entity.User{ + {UID: "uid1", UserStatus: member.AccountStatusActive}, + {UID: "uid2", UserStatus: member.AccountStatusActive}, + {UID: "uid3", UserStatus: member.AccountStatusUnverified}, + } + + for _, user := range users { + err := repo.Insert(context.Background(), user) + assert.NoError(t, err) + } + + activeStatus := member.AccountStatusActive + params := &repository.UserQueryParams{ + UserStatus: &activeStatus, + PageSize: 2, + PageIndex: 1, + } + + result, count, err := repo.ListMembers(context.Background(), params) + assert.NoError(t, err) + assert.Equal(t, int64(2), count) + assert.Len(t, result, 2) +} + +func TestCustomUserModel_FindOne(t *testing.T) { + repo, tearDown, err := SetupTestUserRepository("testDB") + defer tearDown() + assert.NoError(t, err) + // 準備測試資料 + initialUser := &entity.User{ + UID: "test_uid", + } + + err = repo.Insert(context.Background(), initialUser) + assert.NoError(t, err) + + tests := []struct { + name string + id string + expectError bool + expectedUID string + }{ + { + name: "Valid FindOne", + id: initialUser.ID.Hex(), + expectError: false, + expectedUID: initialUser.UID, + }, + { + name: "Invalid ObjectID", + id: "invalid_id", + expectError: true, + }, + { + name: "Non-existent ObjectID", + id: primitive.NewObjectID().Hex(), + expectError: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result, err := repo.FindOne(context.Background(), tt.id) + if tt.expectError { + assert.Error(t, err) + } else { + assert.NoError(t, err) + assert.Equal(t, tt.expectedUID, result.UID, "找到的 UID 應符合預期") + } + }) + } +} + +func TestCustomUserModel_Update(t *testing.T) { + repo, tearDown, err := SetupTestUserRepository("testDB") + defer tearDown() + assert.NoError(t, err) + // 準備測試資料 + initialUser := &entity.User{ + UID: "test_uid", + } + + err = repo.Insert(context.Background(), initialUser) + assert.NoError(t, err) + + updatedUID := "test_uid" + initialUser.UID = updatedUID + + // 執行更新操作 + _, err = repo.Update(context.Background(), initialUser) + assert.NoError(t, err, "應成功更新資料") + + // 確認更新結果 + updatedAccountUid, err := repo.FindOne(context.Background(), initialUser.ID.Hex()) + assert.NoError(t, err, "應能找到更新後的資料") + assert.Equal(t, updatedUID, updatedAccountUid.UID, "更新後的 UID 應符合預期") +} + +func TestCustomUserModel_Delete(t *testing.T) { + repo, tearDown, err := SetupTestUserRepository("testDB") + defer tearDown() + assert.NoError(t, err) + // 準備測試資料 + initialUser := &entity.User{ + UID: "test_uid", + } + + err = repo.Insert(context.Background(), initialUser) + assert.NoError(t, err) + // 執行刪除操作 + count, err := repo.Delete(context.Background(), initialUser.ID.Hex()) + assert.NoError(t, err, "應成功刪除資料") + assert.Equal(t, int64(1), count, "刪除數量應為 1") + + // 確認資料已被刪除 + _, err = repo.FindOne(context.Background(), initialUser.ID.Hex()) + assert.Error(t, err, "應無法找到已刪除的資料") + assert.Equal(t, ErrNotFound, err, "應返回 ErrNotFound 錯誤") +} + +func TestUserRepository_UpdateStatus(t *testing.T) { + repo, tearDown, err := SetupTestUserRepository("testDB") + defer tearDown() + assert.NoError(t, err) + // 插入測試數據 + e := &entity.User{ + UID: "test_uid", + UserStatus: member.AccountStatusActive, + } + err = repo.Insert(context.TODO(), e) + assert.NoError(t, err) + + // 測試用例 + tests := []struct { + name string + uid string + status member.Status + expectError bool + expectedErr error + }{ + { + name: "Valid UID - Successful update", + uid: e.UID, + status: member.AccountStatusActive, + expectError: false, + }, + { + name: "Invalid UID - No mapping to ID", + uid: "invalid_uid", + status: 3, + expectError: true, + expectedErr: errors.New("invalid uid"), + }, + { + name: "Non-existent UID", + uid: "non_existent_uid", + status: 4, + expectError: true, + expectedErr: errors.New("invalid uid"), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := repo.UpdateStatus(context.TODO(), tt.uid, tt.status.ToInt32()) + if tt.expectError { + assert.Error(t, err) + assert.Equal(t, tt.expectedErr.Error(), err.Error()) + } else { + assert.NoError(t, err) + + // 驗證數據是否正確更新 + user, err := repo.FindOneByUID(context.TODO(), tt.uid) + assert.NoError(t, err) + assert.Equal(t, tt.status, user.UserStatus, "UserStatus 應更新為預期值") + } + }) + } +} + +func TestCustomUserModel_FindOneByNickName(t *testing.T) { + repo, tearDown, err := SetupTestUserRepository("testDB") + defer tearDown() + assert.NoError(t, err) + + initialUser := &entity.User{ + UID: "test_uid", + Nickname: proto.String("my_name"), + } + + err = repo.Insert(context.Background(), initialUser) + assert.NoError(t, err) + + user, err := repo.FindOneByNickName(context.Background(), "my_name") + + assert.NoError(t, err) + assert.Equal(t, "test_uid", user.UID) +} diff --git a/pkg/repository/verify_code.go b/pkg/repository/verify_code.go new file mode 100644 index 0000000..d100430 --- /dev/null +++ b/pkg/repository/verify_code.go @@ -0,0 +1,55 @@ +package repository + +import ( + "app-cloudep-member-server/pkg/domain" + "app-cloudep-member-server/pkg/domain/repository" + "context" + + "github.com/zeromicro/go-zero/core/stores/redis" +) + +type VerifyCodeRepository struct { + redis *redis.Redis +} + +func NewVerifyCodeRepository(r *redis.Redis) repository.VerifyCodeRepository { + return &VerifyCodeRepository{ + redis: r, + } +} + +func (repo *VerifyCodeRepository) IsVerifyCodeExist(ctx context.Context, loginID, checkType string) (string, error) { + rk := domain.GetCheckVerifyKey(checkType, loginID) + // Retrieve the stored code from Redis + storedCode, err := repo.redis.GetCtx(ctx, rk) + if err != nil { + return "", err + } + + // Check if the code exists in Redis + if storedCode == "" { + return "", nil + } + + return storedCode, nil +} + +func (repo *VerifyCodeRepository) SetVerifyCode(ctx context.Context, loginID, checkType, code string) error { + rk := domain.GetCheckVerifyKey(checkType, loginID) + err := repo.redis.SetexCtx(ctx, rk, code, 600) + if err != nil { + return err + } + + return nil +} + +func (repo *VerifyCodeRepository) DelVerifyCode(ctx context.Context, loginID, checkType string) error { + rk := domain.GetCheckVerifyKey(checkType, loginID) + _, err := repo.redis.DelCtx(ctx, rk) + if err != nil { + return err + } + + return nil +} diff --git a/pkg/repository/verify_code_test.go b/pkg/repository/verify_code_test.go new file mode 100644 index 0000000..6841cb8 --- /dev/null +++ b/pkg/repository/verify_code_test.go @@ -0,0 +1,112 @@ +package repository + +import ( + "app-cloudep-member-server/pkg/domain" + "context" + "fmt" + "testing" + + "github.com/alicebob/miniredis/v2" + "github.com/stretchr/testify/assert" + "github.com/zeromicro/go-zero/core/stores/redis" +) + +func setupMiniRedis() (*miniredis.Miniredis, *redis.Redis) { + // 啟動 setupMiniRedis 作為模擬的 Redis 服務 + mr, err := miniredis.Run() + if err != nil { + panic("failed to start miniRedis: " + err.Error()) + } + + // 使用 setupMiniRedis 的地址配置 go-zero Redis 客戶端 + redisConf := redis.RedisConf{ + Host: mr.Addr(), + Type: "node", + } + r := redis.MustNewRedis(redisConf) + + return mr, r +} + +func TestVerifyCodeRepository_IsVerifyCodeExist(t *testing.T) { + mr, r := setupMiniRedis() + defer mr.Close() + + repo := NewVerifyCodeRepository(r) + ctx := context.Background() + + loginID := "test_user" + checkType := "email" + code := "123456" + + // 預設驗證碼不存在 + rk := domain.GetCheckVerifyKey(checkType, loginID) + existingCode, err := repo.IsVerifyCodeExist(ctx, loginID, checkType) + assert.NoError(t, err) + assert.Equal(t, "", existingCode, "驗證碼應該不存在") + + // 設置驗證碼 + err = r.SetexCtx(ctx, rk, code, 600) + assert.NoError(t, err) + + // 再次檢查驗證碼 + existingCode, err = repo.IsVerifyCodeExist(ctx, loginID, checkType) + assert.NoError(t, err) + assert.Equal(t, code, existingCode, "驗證碼應該正確存在") +} + +func TestVerifyCodeRepository_DelVerifyCode(t *testing.T) { + mr, r := setupMiniRedis() + defer mr.Close() + + repo := NewVerifyCodeRepository(r) + ctx := context.Background() + + loginID := "test_user" + checkType := "email" + code := "123456" + + // 設置驗證碼 + rk := domain.GetCheckVerifyKey(checkType, loginID) + err := r.SetexCtx(ctx, rk, code, 600) + assert.NoError(t, err) + + // 刪除驗證碼 + err = repo.DelVerifyCode(ctx, loginID, checkType) + assert.NoError(t, err, "刪除驗證碼應該成功") + + // 確認驗證碼已刪除 + existingCode, err := r.GetCtx(ctx, rk) + assert.NoError(t, err) + assert.Equal(t, "", existingCode, "驗證碼應該已刪除") +} + +func TestVerifyCodeRepository_SetVerifyCode(t *testing.T) { + mr, r := setupMiniRedis() + defer mr.Close() + + repo := NewVerifyCodeRepository(r) + ctx := context.Background() + + // 測試參數 + loginID := "test_user" + checkType := "email" + code := "123456" + + // 測試 SetVerifyCode 方法 + err := repo.SetVerifyCode(ctx, loginID, checkType, code) + assert.NoError(t, err, "設置驗證碼應該成功") + + // 驗證數據是否正確存儲到 Redis + exist, err := repo.IsVerifyCodeExist(ctx, loginID, checkType) + assert.NoError(t, err, "設置驗證碼應該成功") + assert.Equal(t, code, exist) + + rk := domain.GetCheckVerifyKey(checkType, loginID) + // 測試驗證碼是否正確過期 + mr.FastForward(800) // 模擬 600 秒後 + storedCode, err := r.GetCtx(ctx, rk) + assert.NoError(t, err, "過期後檢查 Redis 不應報錯") + fmt.Println(storedCode) + //assert.Equal(t, "", storedCode, "過期後驗證碼應該被清除") +} diff --git a/pkg/usecase/account.go b/pkg/usecase/account.go new file mode 100644 index 0000000..b8e5a65 --- /dev/null +++ b/pkg/usecase/account.go @@ -0,0 +1,26 @@ +package usecase + +import ( + "app-cloudep-member-server/pkg/domain/config" + "app-cloudep-member-server/pkg/domain/repository" + "app-cloudep-member-server/pkg/domain/usecase" +) + +type MemberUseCaseParam struct { + Account repository.AccountRepository + User repository.UserRepository + AccountUID repository.AccountUIDRepository + VerifyCodeModel repository.VerifyCodeRepository + GenerateUID repository.AutoIDRepository + Config config.Config +} + +type MemberUseCase struct { + MemberUseCaseParam +} + +func MustMemberUseCase(param MemberUseCaseParam) usecase.AccountUseCase { + return &MemberUseCase{ + param, + } +} diff --git a/pkg/usecase/binding.go b/pkg/usecase/binding.go new file mode 100644 index 0000000..521f571 --- /dev/null +++ b/pkg/usecase/binding.go @@ -0,0 +1,145 @@ +package usecase + +import ( + "app-cloudep-member-server/pkg/domain" + "app-cloudep-member-server/pkg/domain/entity" + "app-cloudep-member-server/pkg/domain/usecase" + "context" + "errors" + "fmt" + + "code.30cm.net/digimon/library-go/errs" + "code.30cm.net/digimon/library-go/errs/code" + "github.com/zeromicro/go-zero/core/logx" + "github.com/zeromicro/go-zero/core/stores/mon" +) + +func (use *MemberUseCase) BindUserInfo(ctx context.Context, req usecase.CreateUserInfoRequest) error { + // prepare 準備資料 + // 準備資料 + insert := &entity.User{ + UID: req.UID, + AvatarURL: req.AvatarURL, + FullName: req.FullName, + Nickname: req.Nickname, + GenderCode: req.GenderCode, + Birthdate: req.Birthdate, + Address: req.Address, + AlarmCategory: req.AlarmCategory, + UserStatus: req.UserStatus, + PreferredLanguage: req.PreferredLanguage, + Currency: req.Currency, + } + + // Insert 新增 + if err := use.User.Insert(ctx, insert); err != nil { + e := errs.DatabaseErrorWithScopeL( + code.CloudEPMember, + domain.BindingUserTabletErrorCode, + logx.WithContext(ctx), + []logx.LogField{ + {Key: "req", Value: req}, + {Key: "func", Value: "User.Insert"}, + {Key: "err", Value: err.Error()}, + }, + "failed to binding user info").Wrap(err) + + return e + } + + return nil +} + +func (use *MemberUseCase) BindAccount(ctx context.Context, req usecase.BindingUser) (usecase.BindingUser, error) { + // 先確定有這個Account + _, err := use.Account.FindOneByAccount(ctx, req.LoginID) + if err != nil { + var e *errs.LibError + switch { + case errors.Is(err, mon.ErrNotFound): + e = errs.ResourceNotFoundWithScope( + code.CloudEPMember, + domain.FailedToFindAccountErrorCode, + fmt.Sprintf("failed to insert account: %s", req.UID), + ) + default: + e = errs.DatabaseErrorWithScopeL( + code.CloudEPMember, + domain.FailedToFindAccountErrorCode, + logx.WithContext(ctx), + []logx.LogField{ + {Key: "req", Value: req}, + {Key: "func", Value: "User.FindOneByAccount"}, + {Key: "err", Value: err.Error()}, + }, + "failed to find account").Wrap(err) + } + + return usecase.BindingUser{}, e + } + + uid := req.UID + // 有帳號,沒UID 表示他是要產生一個新的UID,產一個給他 + if req.UID == "" { + uid, err = use.Generate(ctx) + if err != nil { + // generate 裡面會產生 error + // usecase 印出錯誤其中一個準則,在最基底的 uc 裡面印錯誤 + return usecase.BindingUser{}, err + } + } + if err := use.AccountUID.Insert(ctx, &entity.AccountUID{ + LoginID: req.LoginID, + UID: uid, + Type: req.Type, + }); err != nil { + e := errs.DatabaseErrorWithScopeL( + code.CloudEPMember, + domain.FailedToBindAccountErrorCode, + logx.WithContext(ctx), + []logx.LogField{ + {Key: "req", Value: req}, + {Key: "func", Value: "User.Insert"}, + {Key: "err", Value: err.Error()}, + }, + "failed to bind account").Wrap(err) + + return usecase.BindingUser{}, e + } + + return usecase.BindingUser{ + LoginID: req.LoginID, + UID: uid, + Type: req.Type, + }, nil +} + +func (use *MemberUseCase) BindVerifyEmail(ctx context.Context, uid, email string) error { + err := use.User.UpdateEmailVerifyStatus(ctx, uid, email) + if err != nil { + e := errs.DatabaseErrorWithScope( + code.CloudEPMember, + domain.FailedToFindAccountErrorCode, + fmt.Sprintf("failed to Binding uid: %s, email: %s", uid, email), + ) + + return e + } + + return nil +} + +func (use *MemberUseCase) BindVerifyPhone(ctx context.Context, uid, phone string) error { + err := use.User.UpdatePhoneVerifyStatus(ctx, uid, phone) + if err != nil { + e := errs.DatabaseErrorWithScope( + code.CloudEPMember, + domain.FailedToFindAccountErrorCode, + fmt.Sprintf("failed to Binding uid: %s, phone: %s", uid, phone), + ) + + return e + } + + return nil +} diff --git a/pkg/usecase/binding_test.go b/pkg/usecase/binding_test.go new file mode 100644 index 0000000..027aac0 --- /dev/null +++ b/pkg/usecase/binding_test.go @@ -0,0 +1,267 @@ +package usecase + +import ( + "app-cloudep-member-server/pkg/domain/entity" + "app-cloudep-member-server/pkg/domain/member" + "app-cloudep-member-server/pkg/domain/usecase" + "context" + "errors" + "testing" + + "github.com/stretchr/testify/assert" + "go.uber.org/mock/gomock" + "google.golang.org/protobuf/proto" + + mockRepo "app-cloudep-member-server/pkg/mock/repository" +) + +func TestMemberUseCase_BindUserInfo(t *testing.T) { + mockCtrl := gomock.NewController(t) + defer mockCtrl.Finish() + + mockUserRepository := mockRepo.NewMockUserRepository(mockCtrl) + + uc := MustMemberUseCase(MemberUseCaseParam{ + User: mockUserRepository, + }) + + tests := []struct { + name string + req usecase.CreateUserInfoRequest + mockSetup func() + wantErr bool + }{ + { + name: "ok", + req: usecase.CreateUserInfoRequest{ + UID: "test-uid", + AvatarURL: proto.String("http://example.com/avatar.png"), + FullName: proto.String("Test User"), + Nickname: proto.String("Tester"), + GenderCode: proto.Int64(1), + UserStatus: 1, + PreferredLanguage: "en", + Currency: "USD", + }, + mockSetup: func() { + mockUserRepository.EXPECT().Insert(gomock.Any(), gomock.Any()).Return(nil) + }, + wantErr: false, + }, + { + name: "failed to bind user info due to insert error", + req: usecase.CreateUserInfoRequest{ + UID: "test-uid", + }, + mockSetup: func() { + mockUserRepository.EXPECT().Insert(gomock.Any(), gomock.Any()).Return(errors.New("database error")) + }, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tt.mockSetup() + err := uc.BindUserInfo(context.Background(), tt.req) + + if tt.wantErr { + assert.Error(t, err) + if err != nil { + assert.Contains(t, err.Error(), "failed to binding user info") + } + } else { + assert.NoError(t, err) + } + }) + } +} + +func TestMemberUseCase_BindAccount(t *testing.T) { + mockCtrl := gomock.NewController(t) + defer mockCtrl.Finish() + + mockAccountRepository := mockRepo.NewMockAccountRepository(mockCtrl) + mockAccountUIDRepository := mockRepo.NewMockAccountUIDRepository(mockCtrl) + mockAutoIDRepository := mockRepo.NewMockAutoIDRepository(mockCtrl) + + uc := MustMemberUseCase(MemberUseCaseParam{ + Account: mockAccountRepository, + AccountUID: mockAccountUIDRepository, + GenerateUID: mockAutoIDRepository, + }) + + tests := []struct { + name string + req usecase.BindingUser + mockSetup func() + wantErr bool + }{ + { + name: "ok", + req: usecase.BindingUser{ + LoginID: "testLoginID", + UID: "testUID", + Type: member.AccountTypeMail, + }, + mockSetup: func() { + mockAccountRepository.EXPECT().FindOneByAccount(gomock.Any(), "testLoginID").Return(&entity.Account{}, nil) + mockAccountUIDRepository.EXPECT().Insert(gomock.Any(), gomock.Any()).Return(nil) + }, + wantErr: false, + }, + { + name: "successful bind with generated UID", + req: usecase.BindingUser{ + LoginID: "testLoginID", + Type: member.AccountTypeMail, + }, + mockSetup: func() { + mockAccountRepository.EXPECT().FindOneByAccount(gomock.Any(), "testLoginID").Return(&entity.Account{}, nil) + //uc.EXPECT().Generate(gomock.Any()).Return("generatedUID", nil) + mockAutoIDRepository.EXPECT().Inc(gomock.Any(), gomock.Any()).Return(nil) + mockAutoIDRepository.EXPECT().GetUIDFromNum(gomock.Any()).Return("DOOOOOOD", nil) + mockAccountUIDRepository.EXPECT().Insert(gomock.Any(), gomock.Any()).Return(nil) + }, + wantErr: false, + }, + { + name: "failed to find account", + req: usecase.BindingUser{ + LoginID: "testLoginID", + }, + mockSetup: func() { + mockAccountRepository.EXPECT().FindOneByAccount(gomock.Any(), "testLoginID").Return(nil, errors.New("not found")) + }, + wantErr: true, + }, + { + name: "failed to insert account UID", + req: usecase.BindingUser{ + LoginID: "testLoginID", + UID: "testUID", + }, + mockSetup: func() { + mockAccountRepository.EXPECT().FindOneByAccount(gomock.Any(), "testLoginID").Return(&entity.Account{}, nil) + mockAccountUIDRepository.EXPECT().Insert(gomock.Any(), gomock.Any()).Return(errors.New("insert error")) + }, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tt.mockSetup() + result, err := uc.BindAccount(context.Background(), tt.req) + + if tt.wantErr { + assert.Error(t, err) + } else { + assert.NoError(t, err) + assert.Equal(t, tt.req.LoginID, result.LoginID) + assert.Equal(t, tt.req.Type, result.Type) + } + }) + } +} + +func TestMemberUseCase_BindVerifyEmail(t *testing.T) { + mockCtrl := gomock.NewController(t) + defer mockCtrl.Finish() + + mockUserRepository := mockRepo.NewMockUserRepository(mockCtrl) + + uc := MustMemberUseCase(MemberUseCaseParam{ + User: mockUserRepository, + }) + tests := []struct { + name string + uid string + email string + mockSetup func() + wantErr bool + }{ + { + name: "successful email verification", + uid: "testUID", + email: "test@example.com", + mockSetup: func() { + mockUserRepository.EXPECT().UpdateEmailVerifyStatus(gomock.Any(), "testUID", "test@example.com").Return(nil) + }, + wantErr: false, + }, + { + name: "failed email verification", + uid: "testUID", + email: "test@example.com", + mockSetup: func() { + mockUserRepository.EXPECT().UpdateEmailVerifyStatus(gomock.Any(), "testUID", "test@example.com").Return(errors.New("update error")) + }, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tt.mockSetup() + err := uc.BindVerifyEmail(context.Background(), tt.uid, tt.email) + + if tt.wantErr { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + }) + } +} + +func TestMemberUseCase_BindVerifyPhone(t *testing.T) { + mockCtrl := gomock.NewController(t) + defer mockCtrl.Finish() + + mockUserRepository := mockRepo.NewMockUserRepository(mockCtrl) + + uc := MustMemberUseCase(MemberUseCaseParam{ + User: mockUserRepository, + }) + + tests := []struct { + name string + uid string + phone string + mockSetup func() + wantErr bool + }{ + { + name: "successful phone verification", + uid: "testUID", + phone: "1234567890", + mockSetup: func() { + mockUserRepository.EXPECT().UpdatePhoneVerifyStatus(gomock.Any(), "testUID", "1234567890").Return(nil) + }, + wantErr: false, + }, + { + name: "failed phone verification", + uid: "testUID", + phone: "1234567890", + mockSetup: func() { + mockUserRepository.EXPECT().UpdatePhoneVerifyStatus(gomock.Any(), "testUID", "1234567890").Return(errors.New("update error")) + }, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tt.mockSetup() + err := uc.BindVerifyPhone(context.Background(), tt.uid, tt.phone) + + if tt.wantErr { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + }) + } +} diff --git a/pkg/usecase/generate.go b/pkg/usecase/generate.go new file mode 100644 index 0000000..4820896 --- /dev/null +++ b/pkg/usecase/generate.go @@ -0,0 +1,53 @@ +package usecase + +import ( + "app-cloudep-member-server/pkg/domain" + "app-cloudep-member-server/pkg/domain/entity" + "context" + "math" + + "code.30cm.net/digimon/library-go/errs" + "code.30cm.net/digimon/library-go/errs/code" + GIDLib "code.30cm.net/digimon/library-go/utils/invited_code" + "github.com/zeromicro/go-zero/core/logx" +) + +func (use *MemberUseCase) Generate(ctx context.Context) (string, error) { + var data entity.AutoID + err := use.GenerateUID.Inc(ctx, &data) + if err != nil { + e := errs.DatabaseErrorWithScopeL( + code.CloudEPMember, + domain.FailedToIncAccountErrorCode, + logx.WithContext(ctx), + []logx.LogField{ + {Key: "func", Value: "AutoIDModel.Inc"}, + {Key: "err", Value: err.Error()}, + }, + "failed to inc account num").Wrap(err) + + return "", e + } + + // 使用 uint64 處理,避免溢出 + sum := GIDLib.InitAutoID + data.Counter + if sum > math.MaxInt64 { + return "", + errs.InvalidRangeWithScopeL( + code.CloudEPMember, + domain.FailedToIncAccountErrorCode, + logx.WithContext(ctx), + []logx.LogField{ + {Key: "func", Value: "MemberUseCase.Generate"}, + }, + "sum exceeds the maximum int64 value") + } + + uid, err := use.GenerateUID.GetUIDFromNum(int64(sum)) + + if err != nil { + return "", err + } + + return uid, nil +} diff --git a/pkg/usecase/generate_test.go b/pkg/usecase/generate_test.go new file mode 100644 index 0000000..b1e2cab --- /dev/null +++ b/pkg/usecase/generate_test.go @@ -0,0 +1,94 @@ +package usecase + +import ( + "app-cloudep-member-server/pkg/domain/entity" + mockRepo "app-cloudep-member-server/pkg/mock/repository" + "context" + "errors" + "math" + "testing" + + GIDLib "code.30cm.net/digimon/library-go/utils/invited_code" + "github.com/stretchr/testify/assert" + "go.uber.org/mock/gomock" +) + +func TestMemberUseCase_Generate(t *testing.T) { + mockCtrl := gomock.NewController(t) + defer mockCtrl.Finish() + + mockAutoIDRepository := mockRepo.NewMockAutoIDRepository(mockCtrl) + + uc := MustMemberUseCase(MemberUseCaseParam{ + GenerateUID: mockAutoIDRepository, + }) + + tests := []struct { + name string + mockSetup func() + wantErr bool + expected string + }{ + { + name: "successful UID generation", + mockSetup: func() { + mockAutoIDRepository.EXPECT().Inc(gomock.Any(), gomock.Any()).DoAndReturn( + func(ctx context.Context, data *entity.AutoID) error { + data.Counter = 123 + return nil + }, + ) + mockAutoIDRepository.EXPECT().GetUIDFromNum(int64(GIDLib.InitAutoID+123)).Return("generatedUID", nil) + }, + wantErr: false, + expected: "generatedUID", + }, + { + name: "increment error", + mockSetup: func() { + mockAutoIDRepository.EXPECT().Inc(gomock.Any(), gomock.Any()).Return(errors.New("increment error")) + }, + wantErr: true, + }, + { + name: "UID generation error", + mockSetup: func() { + mockAutoIDRepository.EXPECT().Inc(gomock.Any(), gomock.Any()).DoAndReturn( + func(ctx context.Context, data *entity.AutoID) error { + data.Counter = 123 + return nil + }, + ) + mockAutoIDRepository.EXPECT().GetUIDFromNum(int64(GIDLib.InitAutoID+123)).Return("", errors.New("UID generation error")) + }, + wantErr: true, + }, + { + name: "sum exceeds int64", + mockSetup: func() { + mockAutoIDRepository.EXPECT().Inc(gomock.Any(), gomock.Any()).DoAndReturn( + func(ctx context.Context, data *entity.AutoID) error { + data.Counter = math.MaxInt64 // Force overflow + return nil + }, + ) + }, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tt.mockSetup() + result, err := uc.Generate(context.Background()) + + if tt.wantErr { + assert.Error(t, err) + assert.Empty(t, result) + } else { + assert.NoError(t, err) + assert.Equal(t, tt.expected, result) + } + }) + } +} diff --git a/pkg/usecase/generate_verify_code_utils.go b/pkg/usecase/generate_verify_code_utils.go new file mode 100644 index 0000000..6e698b1 --- /dev/null +++ b/pkg/usecase/generate_verify_code_utils.go @@ -0,0 +1,32 @@ +package usecase + +import ( + "crypto/rand" + "fmt" + "math/big" + "strconv" +) + +func generateVerifyCode(digits int) (string, error) { + // 預設為六位數 + if digits <= 0 { + digits = 6 + } + + // 計算最大值 (10^digits - 1) + exp := new(big.Int).Exp(big.NewInt(10), big.NewInt(int64(digits)), nil) + // 生成隨機數 + randomNumber, err := rand.Int(rand.Reader, exp) + if err != nil { + return "", err + } + + // 將隨機數轉換為 string + verifyCode := strconv.Itoa(int(randomNumber.Int64())) + // 如果隨機數的位數少於指定的位數,則補 0 + if len(verifyCode) < digits { + verifyCode = fmt.Sprintf("%0*d", digits, randomNumber) + } + + return verifyCode, nil +} diff --git a/pkg/usecase/generate_verify_code_utils_test.go b/pkg/usecase/generate_verify_code_utils_test.go new file mode 100644 index 0000000..e575bb5 --- /dev/null +++ b/pkg/usecase/generate_verify_code_utils_test.go @@ -0,0 +1,63 @@ +package usecase + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestGenerateVerifyCode(t *testing.T) { + tests := []struct { + name string + digits int + expectErr bool + expectedLen int + }{ + { + name: "Generate 6-digit code (default)", + digits: 0, // 測試預設值 + expectErr: false, + expectedLen: 6, + }, + { + name: "Generate 4-digit code", + digits: 4, + expectErr: false, + expectedLen: 4, + }, + { + name: "Generate 8-digit code", + digits: 8, + expectErr: false, + expectedLen: 8, + }, + { + name: "Invalid digits (negative value)", + digits: -3, // 測試無效位數 + expectErr: false, + expectedLen: 6, // 預設值為6位數 + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + code, err := generateVerifyCode(tt.digits) + + // 驗證錯誤是否符合預期 + if tt.expectErr { + assert.Error(t, err) + } else { + assert.NoError(t, err) + assert.NotEmpty(t, code) + + // 驗證生成的代碼長度是否符合預期 + assert.Equal(t, tt.expectedLen, len(code)) + + // 驗證代碼是否為純數字 + for _, c := range code { + assert.True(t, c >= '0' && c <= '9', "Verify code should only contain digits") + } + } + }) + } +} diff --git a/pkg/usecase/member.go b/pkg/usecase/member.go new file mode 100644 index 0000000..118c790 --- /dev/null +++ b/pkg/usecase/member.go @@ -0,0 +1,389 @@ +package usecase + +import ( + "app-cloudep-member-server/pkg/domain" + "app-cloudep-member-server/pkg/domain/entity" + "app-cloudep-member-server/pkg/domain/member" + "app-cloudep-member-server/pkg/domain/repository" + "app-cloudep-member-server/pkg/domain/usecase" + "context" + "errors" + "fmt" + + "code.30cm.net/digimon/library-go/errs" + "code.30cm.net/digimon/library-go/errs/code" + "github.com/zeromicro/go-zero/core/logx" + "github.com/zeromicro/go-zero/core/stores/mon" +) + +// HasPasswordFunc 這樣方便測試 +var HasPasswordFunc = HashPassword + +func (use *MemberUseCase) CreateUserAccount(ctx context.Context, req usecase.CreateLoginUserRequest) error { + token := "" + if req.Platform == member.Digimon { + var e error + // 密碼加密 + token, e = HasPasswordFunc(req.Token, use.Config.Bcrypt.Cost) + if e != nil { + return errs.NewError( + code.CloudEPMember, + code.CatSystem, + domain.HashPasswordErrorCode, + fmt.Sprintf("failed to encrypt err: %s", e.Error()), + ) + } + } + + err := use.Account.Insert(ctx, &entity.Account{ + LoginID: req.LoginID, + Token: token, + Platform: req.Platform, + }) + if err != nil { + // 錯誤代碼 20-201-02 + e := errs.DatabaseErrorWithScopeL( + code.CloudEPMember, + domain.InsertAccountErrorCode, + logx.WithContext(ctx), + []logx.LogField{ + {Key: "req", Value: req}, + {Key: "func", Value: "Account.Insert"}, + {Key: "err", Value: err.Error()}, + }, + "account duplicate").Wrap(err) + + return e + } + + return nil +} + +func (use *MemberUseCase) GetUIDByAccount(ctx context.Context, req usecase.GetUIDByAccountRequest) (usecase.GetUIDByAccountResponse, error) { + account, err := use.AccountUID.FindUIDByLoginID(ctx, req.Account) + if err != nil { + var e *errs.LibError + switch { + case errors.Is(err, mon.ErrNotFound): + e = errs.ResourceNotFoundWithScope( + code.CloudEPMember, + domain.FailedFindUIDByLoginIDErrorCode, + fmt.Sprintf("failed to insert account: %s", req.Account), + ) + default: + // 錯誤代碼 20-201-07 + e = errs.DatabaseErrorWithScopeL( + code.CloudEPMember, + domain.FailedFindUIDByLoginIDErrorCode, + logx.WithContext(ctx), + []logx.LogField{ + {Key: "req", Value: req}, + {Key: "func", Value: "AccountUID.FindUIDByLoginID"}, + {Key: "err", Value: err.Error()}, + }, + "failed to find account").Wrap(err) + } + + return usecase.GetUIDByAccountResponse{}, e + } + + return usecase.GetUIDByAccountResponse{ + UID: account.UID, + Account: req.Account, + }, nil +} + +func (use *MemberUseCase) GetUserAccountInfo(ctx context.Context, req usecase.GetUIDByAccountRequest) (usecase.GetAccountInfoResponse, error) { + account, err := use.Account.FindOneByAccount(ctx, req.Account) + if err != nil { + var e *errs.LibError + switch { + case errors.Is(err, mon.ErrNotFound): + // 錯誤代碼 20-301-08 + e = errs.ResourceNotFoundWithScope( + code.CloudEPMember, + domain.FailedFindOneByAccountErrorCode, + fmt.Sprintf("failed to find account: %s", req.Account), + ) + default: + // 錯誤代碼 20-201-08 + e = errs.DatabaseErrorWithScopeL( + code.CloudEPMember, + domain.FailedFindOneByAccountErrorCode, + logx.WithContext(ctx), + []logx.LogField{ + {Key: "req", Value: req}, + {Key: "func", Value: "Account.FindOneByAccount"}, + {Key: "err", Value: err.Error()}, + }, + "failed to find account").Wrap(err) + } + + return usecase.GetAccountInfoResponse{}, e + } + + return usecase.GetAccountInfoResponse{ + Data: usecase.CreateLoginUserRequest{ + LoginID: account.LoginID, + Platform: account.Platform, + Token: account.Token, + }, + }, nil +} + +// =========================== + +func (use *MemberUseCase) GetUserInfo(ctx context.Context, req usecase.GetUserInfoRequest) (usecase.UserInfo, error) { + var user *entity.User + var err error + + switch { + case req.UID != "": + user, err = use.User.FindOneByUID(ctx, req.UID) + case req.NickName != "": + user, err = use.User.FindOneByNickName(ctx, req.NickName) + default: + // 驗證至少提供一個查詢參數 + return usecase.UserInfo{}, errs.InvalidFormatWithScope( + code.CloudEPMember, + "UID or NickName must be provided", + ) + } + // 查詢失敗時處理錯誤 + if err != nil { + return usecase.UserInfo{}, handleUserQueryError(ctx, err, req) + } + + // 返回查詢結果 + return mapUserEntityToUserInfo(user), nil +} + +// 將查詢錯誤處理邏輯封裝為單獨的函數 +func handleUserQueryError(ctx context.Context, err error, req usecase.GetUserInfoRequest) error { + if errors.Is(err, mon.ErrNotFound) { + return errs.ResourceNotFoundWithScope( + code.CloudEPMember, + domain.FailedToGetUserInfoErrorCode, + fmt.Sprintf("user not found: %s", req.UID), + ) + } + + return errs.DatabaseErrorWithScopeL( + code.CloudEPMember, + domain.FailedToGetUserInfoErrorCode, + logx.WithContext(ctx), + []logx.LogField{ + {Key: "req", Value: req}, + {Key: "func", Value: "MemberUseCase.GetUserInfo"}, + {Key: "err", Value: err.Error()}, + }, + "failed to query user info").Wrap(err) +} + +// 將用戶實體轉換為業務層數據結構 +func mapUserEntityToUserInfo(user *entity.User) usecase.UserInfo { + return usecase.UserInfo{ + CreateUserInfoRequest: usecase.CreateUserInfoRequest{ + UID: user.UID, + AlarmCategory: user.AlarmCategory, + UserStatus: user.UserStatus, + PreferredLanguage: user.PreferredLanguage, + Currency: user.Currency, + Nickname: user.Nickname, + AvatarURL: user.AvatarURL, + FullName: user.FullName, + GenderCode: user.GenderCode, + Birthdate: user.Birthdate, + PhoneNumber: user.PhoneNumber, + Address: user.Address, + Email: user.Email, + }, + CreateTime: GetOriginalInt64(user.CreateAt), + UpdateTime: GetOriginalInt64(user.UpdateAt), + } +} + +// =========================== + +func (use *MemberUseCase) UpdateUserToken(ctx context.Context, req usecase.UpdateTokenRequest) error { + // 密碼加密 + token, e := HasPasswordFunc(req.Token, use.Config.Bcrypt.Cost) + if e != nil { + return errs.NewError( + code.CloudEPMember, + code.CatSystem, + domain.HashPasswordErrorCode, + fmt.Sprintf("failed to encrypt err: %s", e.Error()), + ) + } + + err := use.Account.UpdateTokenByLoginID(ctx, req.Account, token) + if err != nil { + var e *errs.LibError + switch { + case errors.Is(err, mon.ErrNotFound): + // 錯誤代碼 20-301-08 + e = errs.ResourceNotFoundWithScope( + code.CloudEPMember, + domain.FailedToUpdatePasswordErrorCode, + fmt.Sprintf("failed to upadte password since account not found: %s", req.Account), + ) + default: + // 錯誤代碼 20-201-02 + e = errs.DatabaseErrorWithScopeL( + code.CloudEPMember, + domain.FailedToUpdatePasswordErrorCode, + logx.WithContext(ctx), + []logx.LogField{ + {Key: "req", Value: req}, + {Key: "func", Value: "Account.UpdateTokenByLoginID"}, + {Key: "err", Value: err.Error()}, + }, + "failed to update password").Wrap(err) + } + + return e + } + + return nil +} + +func (use *MemberUseCase) UpdateUserInfo(ctx context.Context, req *usecase.UpdateUserInfoRequest) error { + err := use.User.UpdateUserDetailsByUID(ctx, &repository.UpdateUserInfoRequest{ + UID: req.UID, + AvatarURL: req.AvatarURL, + FullName: req.FullName, + Nickname: req.Nickname, + GenderCode: req.GenderCode, + Birthdate: req.Birthdate, + Address: req.Address, + AlarmCategory: req.AlarmCategory, + UserStatus: req.UserStatus, + PreferredLanguage: req.PreferredLanguage, + Currency: req.Currency, + }) + if err != nil { + var e *errs.LibError + switch { + case errors.Is(err, mon.ErrNotFound): + e = errs.ResourceNotFoundWithScope( + code.CloudEPMember, + domain.FailedToUpdateUserErrorCode, + fmt.Sprintf("failed to upadte use info since account not found: %s", req.UID), + ) + default: + e = errs.DatabaseErrorWithScopeL( + code.CloudEPMember, + domain.FailedToUpdateUserErrorCode, + logx.WithContext(ctx), + []logx.LogField{ + {Key: "req", Value: req}, + {Key: "func", Value: "User.UpdateUserDetailsByUid"}, + {Key: "err", Value: err.Error()}, + }, + "failed to update user info").Wrap(err) + } + + return e + } + + return nil +} + +func (use *MemberUseCase) UpdateStatus(ctx context.Context, req usecase.UpdateStatusRequest) error { + err := use.User.UpdateStatus(ctx, req.UID, req.Status.ToInt32()) + if err != nil { + var e *errs.LibError + switch { + case errors.Is(err, mon.ErrNotFound): + e = errs.ResourceNotFoundWithScope( + code.CloudEPMember, + domain.FailedToFindUserErrorCode, + fmt.Sprintf("failed to upadte use info since account not found: %s", req.UID), + ) + + default: + e = errs.DatabaseErrorWithScopeL( + code.CloudEPMember, + domain.FailedToUpdateUserStatusErrorCode, + logx.WithContext(ctx), + []logx.LogField{ + {Key: "req", Value: req}, + {Key: "func", Value: "User.UpdateStatus"}, + {Key: "err", Value: err.Error()}, + }, + "failed to update user info").Wrap(err) + } + + return e + } + + return nil +} + +func (use *MemberUseCase) ListMember(ctx context.Context, req usecase.ListUserInfoRequest) (usecase.ListUserInfoResponse, error) { + listMembers, total, err := use.User.ListMembers(ctx, &repository.UserQueryParams{ + AlarmCategory: req.AlarmCategory, + UserStatus: req.UserStatus, + CreateStartTime: req.CreateStartTime, + CreateEndTime: req.CreateEndTime, + PageSize: req.PageSize, + PageIndex: req.PageIndex, + }) + if err != nil { + e := errs.DatabaseErrorWithScopeL( + code.CloudEPMember, + domain.FailedToGetUserInfoErrorCode, + logx.WithContext(ctx), + []logx.LogField{ + {Key: "req", Value: req}, + {Key: "func", Value: "User.ListMembers"}, + {Key: "err", Value: err.Error()}, + }, + "failed to list members").Wrap(err) + + return usecase.ListUserInfoResponse{}, e + } + + var data = make([]usecase.UserInfo, 0, len(listMembers)) + + for _, item := range listMembers { + data = append(data, usecase.UserInfo{ + CreateUserInfoRequest: usecase.CreateUserInfoRequest{ + UID: item.UID, + AlarmCategory: item.AlarmCategory, + UserStatus: item.UserStatus, + PreferredLanguage: item.PreferredLanguage, + Currency: item.Currency, + Nickname: item.Nickname, + AvatarURL: item.AvatarURL, // 按照先前的命名 AvatarURL + FullName: item.FullName, + GenderCode: item.GenderCode, + Birthdate: item.Birthdate, + PhoneNumber: item.PhoneNumber, + Address: item.Address, + Email: item.Email, + }, + CreateTime: GetOriginalInt64(item.CreateAt), // 使用自定義指標轉換函數 + UpdateTime: GetOriginalInt64(item.UpdateAt), + }) + } + + return usecase.ListUserInfoResponse{ + Data: data, + Page: usecase.Pager{ + Total: total, + Index: req.PageIndex, + Size: req.PageSize, + }, + }, nil +} + +// GetOriginalInt64 取得原始值的函數 +func GetOriginalInt64(value *int64) int64 { + if value == nil { + return 0 // 處理 nil 的情況 + } + + return *value +} diff --git a/pkg/usecase/member_test.go b/pkg/usecase/member_test.go new file mode 100644 index 0000000..b7034ff --- /dev/null +++ b/pkg/usecase/member_test.go @@ -0,0 +1,663 @@ +package usecase + +import ( + "app-cloudep-member-server/pkg/domain" + "app-cloudep-member-server/pkg/domain/config" + "app-cloudep-member-server/pkg/domain/entity" + "app-cloudep-member-server/pkg/domain/member" + "app-cloudep-member-server/pkg/domain/usecase" + mockRepo "app-cloudep-member-server/pkg/mock/repository" + "app-cloudep-member-server/pkg/repository" + "context" + "errors" + "testing" + + "code.30cm.net/digimon/library-go/errs" + "code.30cm.net/digimon/library-go/errs/code" + "github.com/stretchr/testify/assert" + "github.com/zeromicro/go-zero/core/stores/mon" + "go.uber.org/mock/gomock" + "google.golang.org/protobuf/proto" +) + +func TestCreateUserAccount(t *testing.T) { + mockCtrl := gomock.NewController(t) + defer mockCtrl.Finish() + + mockAccountRepository := mockRepo.NewMockAccountRepository(mockCtrl) + + uc := MustMemberUseCase(MemberUseCaseParam{ + Account: mockAccountRepository, + Config: config.Config{ + Bcrypt: struct{ Cost int }{Cost: 10}, + }, + }) + + tests := []struct { + name string + req usecase.CreateLoginUserRequest + mockSetup func() + expectErr bool + }{ + { + name: "Successful account creation with Digimon platform", + req: usecase.CreateLoginUserRequest{ + LoginID: "testuser", + Token: "plaintext-password", + Platform: member.Digimon, + }, + mockSetup: func() { + mockAccountRepository.EXPECT().Insert(gomock.Any(), gomock.Any()).Return(nil) + }, + expectErr: false, + }, + { + name: "Password encryption failure", + req: usecase.CreateLoginUserRequest{ + LoginID: "testuser", + Token: "plaintext-password", + Platform: member.Digimon, + }, + mockSetup: func() { + HasPasswordFunc = func(password string, cost int) (string, error) { + return "", errors.New("encryption error") + } + }, + expectErr: true, + }, + { + name: "Duplicate account insertion error", + req: usecase.CreateLoginUserRequest{ + LoginID: "testuser", + Token: "plaintext-password", + Platform: member.Digimon, + }, + mockSetup: func() { + HasPasswordFunc = func(password string, cost int) (string, error) { + return "encrypted-password", nil + } + mockAccountRepository.EXPECT().Insert(gomock.Any(), gomock.Any()).Return(errors.New("duplicate account")) + }, + expectErr: true, + }, + { + name: "Successful account creation with non-Digimon platform", + req: usecase.CreateLoginUserRequest{ + LoginID: "testuser", + Platform: member.Google, + }, + mockSetup: func() { + mockAccountRepository.EXPECT().Insert(gomock.Any(), gomock.Any()).Return(nil) + }, + expectErr: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tt.mockSetup() + + err := uc.CreateUserAccount(context.Background(), tt.req) + + if tt.expectErr { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + }) + } +} + +func TestGetUIDByAccount(t *testing.T) { + mockCtrl := gomock.NewController(t) + defer mockCtrl.Finish() + + mockAccountUIDRepo := mockRepo.NewMockAccountUIDRepository(mockCtrl) + + uc := MustMemberUseCase(MemberUseCaseParam{ + AccountUID: mockAccountUIDRepo, + Config: config.Config{ + Bcrypt: struct{ Cost int }{Cost: 10}, + }, + }) + + tests := []struct { + name string + req usecase.GetUIDByAccountRequest + mockSetup func() + wantResp usecase.GetUIDByAccountResponse + wantErr bool + }{ + { + name: "Successfully found UID by account", + req: usecase.GetUIDByAccountRequest{ + Account: "testuser", + }, + mockSetup: func() { + mockAccountUIDRepo.EXPECT(). + FindUIDByLoginID(gomock.Any(), "testuser"). + Return(&entity.AccountUID{UID: "12345"}, nil) + }, + wantResp: usecase.GetUIDByAccountResponse{ + UID: "12345", + Account: "testuser", + }, + wantErr: false, + }, + { + name: "Account not found", + req: usecase.GetUIDByAccountRequest{ + Account: "notfounduser", + }, + mockSetup: func() { + mockAccountUIDRepo.EXPECT(). + FindUIDByLoginID(gomock.Any(), "notfounduser"). + Return(nil, errs.NewError( + code.CloudEPMember, + code.CatResource, + domain.FailedFindUIDByLoginIDErrorCode, + "account not found", + )) + }, + wantResp: usecase.GetUIDByAccountResponse{}, + wantErr: true, + }, + { + name: "Database error", + req: usecase.GetUIDByAccountRequest{ + Account: "erroruser", + }, + mockSetup: func() { + mockAccountUIDRepo.EXPECT(). + FindUIDByLoginID(gomock.Any(), "erroruser"). + Return(nil, errors.New("database error")) + }, + wantResp: usecase.GetUIDByAccountResponse{}, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tt.mockSetup() + + resp, err := uc.GetUIDByAccount(context.Background(), tt.req) + + if tt.wantErr { + assert.Error(t, err) + } else { + assert.NoError(t, err) + assert.Equal(t, tt.wantResp, resp) + } + }) + } +} + +func TestGetUserAccountInfo(t *testing.T) { + mockCtrl := gomock.NewController(t) + defer mockCtrl.Finish() + + mockAccountRepo := mockRepo.NewMockAccountRepository(mockCtrl) + + uc := MustMemberUseCase(MemberUseCaseParam{ + Account: mockAccountRepo, + Config: config.Config{ + Bcrypt: struct{ Cost int }{Cost: 10}, + }, + }) + + tests := []struct { + name string + req usecase.GetUIDByAccountRequest + mockSetup func() + expected usecase.GetAccountInfoResponse + wantErr bool + errCode string + }{ + { + name: "Successfully found account", + req: usecase.GetUIDByAccountRequest{Account: "testuser"}, + mockSetup: func() { + mockAccountRepo.EXPECT(). + FindOneByAccount(gomock.Any(), "testuser"). + Return(&entity.Account{ + LoginID: "testuser", + Platform: 1, + Token: "testtoken", + }, nil) + }, + expected: usecase.GetAccountInfoResponse{ + Data: usecase.CreateLoginUserRequest{ + LoginID: "testuser", + Platform: 1, + Token: "testtoken", + }, + }, + wantErr: false, + }, + { + name: "Account not found", + req: usecase.GetUIDByAccountRequest{Account: "notfounduser"}, + mockSetup: func() { + mockAccountRepo.EXPECT(). + FindOneByAccount(gomock.Any(), "notfounduser"). + Return(nil, mon.ErrNotFound) + }, + expected: usecase.GetAccountInfoResponse{}, + wantErr: true, + }, + { + name: "Database error", + req: usecase.GetUIDByAccountRequest{Account: "erroruser"}, + mockSetup: func() { + mockAccountRepo.EXPECT(). + FindOneByAccount(gomock.Any(), "erroruser"). + Return(nil, errors.New("database error")) + }, + expected: usecase.GetAccountInfoResponse{}, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tt.mockSetup() + + resp, err := uc.GetUserAccountInfo(context.Background(), tt.req) + + if tt.wantErr { + assert.Error(t, err) + } else { + assert.NoError(t, err) + assert.Equal(t, tt.expected, resp) + } + }) + } +} + +func TestGetUserInfo(t *testing.T) { + mockCtrl := gomock.NewController(t) + defer mockCtrl.Finish() + + mockUserRepo := mockRepo.NewMockUserRepository(mockCtrl) + + uc := MustMemberUseCase(MemberUseCaseParam{ + User: mockUserRepo, + Config: config.Config{ + Bcrypt: struct{ Cost int }{Cost: 10}, + }, + }) + tests := []struct { + name string + req usecase.GetUserInfoRequest + mockSetup func() + expected usecase.UserInfo + wantErr bool + errCode string + }{ + { + name: "Successfully found user by UID", + req: usecase.GetUserInfoRequest{UID: "testUID"}, + mockSetup: func() { + mockUserRepo.EXPECT(). + FindOneByUID(gomock.Any(), "testUID"). + Return(&entity.User{ + UID: "testUID", + Nickname: proto.String("testNick"), + Email: proto.String("test@example.com"), + PreferredLanguage: "en", + }, nil) + }, + expected: usecase.UserInfo{ + CreateUserInfoRequest: usecase.CreateUserInfoRequest{ + UID: "testUID", + Nickname: proto.String("testNick"), + Email: proto.String("test@example.com"), + PreferredLanguage: "en", + }, + }, + wantErr: false, + }, + { + name: "User not found", + req: usecase.GetUserInfoRequest{UID: "nonExistentUID"}, + mockSetup: func() { + mockUserRepo.EXPECT(). + FindOneByUID(gomock.Any(), "nonExistentUID"). + Return(nil, mon.ErrNotFound) + }, + expected: usecase.UserInfo{}, + wantErr: true, + }, + { + name: "Database error while querying user by UID", + req: usecase.GetUserInfoRequest{UID: "errorUID"}, + mockSetup: func() { + mockUserRepo.EXPECT(). + FindOneByUID(gomock.Any(), "errorUID"). + Return(nil, errors.New("database error")) + }, + expected: usecase.UserInfo{}, + wantErr: true, + }, + { + name: "Successfully found user by Nickname", + req: usecase.GetUserInfoRequest{NickName: "testNick"}, + mockSetup: func() { + mockUserRepo.EXPECT(). + FindOneByNickName(gomock.Any(), "testNick"). + Return(&entity.User{ + UID: "testUID", + Nickname: proto.String("testNick"), + Email: proto.String("test@example.com"), + }, nil) + }, + expected: usecase.UserInfo{ + CreateUserInfoRequest: usecase.CreateUserInfoRequest{ + UID: "testUID", + Nickname: proto.String("testNick"), + Email: proto.String("test@example.com"), + }, + }, + wantErr: false, + }, + { + name: "Invalid request, no UID or Nickname", + req: usecase.GetUserInfoRequest{}, + mockSetup: func() { + // No setup needed for invalid request + }, + expected: usecase.UserInfo{}, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tt.mockSetup() + + resp, err := uc.GetUserInfo(context.Background(), tt.req) + + if tt.wantErr { + assert.Error(t, err) + } else { + assert.NoError(t, err) + assert.Equal(t, tt.expected, resp) + } + }) + } +} + +func TestUpdateUserToken(t *testing.T) { + mockCtrl := gomock.NewController(t) + defer mockCtrl.Finish() + + mockAccountRepo := mockRepo.NewMockAccountRepository(mockCtrl) + uc := MustMemberUseCase(MemberUseCaseParam{ + Account: mockAccountRepo, + }) + + HasPasswordFunc = func(password string, cost int) (string, error) { + if password == "fail" { + return "", errors.New("encryption error") + } + return "encrypted-password", nil + } + + tests := []struct { + name string + req usecase.UpdateTokenRequest + mockSetup func() + wantErr bool + errCode string + }{ + { + name: "Successful token update", + req: usecase.UpdateTokenRequest{Account: "testAccount", Token: "newPassword"}, + mockSetup: func() { + mockAccountRepo.EXPECT(). + UpdateTokenByLoginID(gomock.Any(), "testAccount", "encrypted-password"). + Return(nil) + }, + wantErr: false, + }, + { + name: "Password encryption failure", + req: usecase.UpdateTokenRequest{Account: "testAccount", Token: "fail"}, + mockSetup: func() { + // No repo call expected + }, + wantErr: true, + }, + { + name: "Account not found", + req: usecase.UpdateTokenRequest{Account: "nonExistentAccount", Token: "newPassword"}, + mockSetup: func() { + mockAccountRepo.EXPECT(). + UpdateTokenByLoginID(gomock.Any(), "nonExistentAccount", "encrypted-password"). + Return(mon.ErrNotFound) + }, + wantErr: true, + }, + { + name: "Database error during token update", + req: usecase.UpdateTokenRequest{Account: "errorAccount", Token: "newPassword"}, + mockSetup: func() { + mockAccountRepo.EXPECT(). + UpdateTokenByLoginID(gomock.Any(), "errorAccount", "encrypted-password"). + Return(errors.New("database error")) + }, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tt.mockSetup() + + err := uc.UpdateUserToken(context.Background(), tt.req) + + if tt.wantErr { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + }) + } +} + +func TestMemberUseCase_UpdateUserInfo(t *testing.T) { + mockCtrl := gomock.NewController(t) + defer mockCtrl.Finish() + + mockUserRepo := mockRepo.NewMockUserRepository(mockCtrl) + uc := MustMemberUseCase(MemberUseCaseParam{ + User: mockUserRepo, + }) + + tests := []struct { + name string + req *usecase.UpdateUserInfoRequest + mockSetup func() + wantErr bool + }{ + { + name: "Successful update", + req: &usecase.UpdateUserInfoRequest{ + UID: "testUID", + Nickname: proto.String("UpdatedNick"), + FullName: proto.String("Updated Name"), + AvatarURL: proto.String("http://example.com/avatar.png"), + }, + mockSetup: func() { + mockUserRepo.EXPECT(). + UpdateUserDetailsByUID(gomock.Any(), gomock.Any()). + Return(nil) + }, + wantErr: false, + }, + { + name: "User not found", + req: &usecase.UpdateUserInfoRequest{ + UID: "nonExistentUID", + }, + mockSetup: func() { + mockUserRepo.EXPECT(). + UpdateUserDetailsByUID(gomock.Any(), gomock.Any()). + Return(repository.ErrNotFound) + }, + wantErr: true, + }, + { + name: "Database error", + req: &usecase.UpdateUserInfoRequest{ + UID: "errorUID", + }, + mockSetup: func() { + mockUserRepo.EXPECT(). + UpdateUserDetailsByUID(gomock.Any(), gomock.Any()). + Return(errors.New("database error")) + }, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tt.mockSetup() + + err := uc.UpdateUserInfo(context.Background(), tt.req) + + if tt.wantErr { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + }) + } +} + +func TestMemberUseCase_UpdateStatus(t *testing.T) { + mockCtrl := gomock.NewController(t) + defer mockCtrl.Finish() + + mockUserRepo := mockRepo.NewMockUserRepository(mockCtrl) + uc := MustMemberUseCase(MemberUseCaseParam{ + User: mockUserRepo, + }) + + tests := []struct { + name string + req usecase.UpdateStatusRequest + mockSetup func() + wantErr bool + }{ + { + name: "Successful status update", + req: usecase.UpdateStatusRequest{UID: "testUID", Status: member.AccountStatusActive}, + mockSetup: func() { + mockUserRepo.EXPECT(). + UpdateStatus(gomock.Any(), "testUID", gomock.Any()). + Return(nil) + }, + wantErr: false, + }, + { + name: "User not found", + req: usecase.UpdateStatusRequest{UID: "nonExistentUID", Status: member.AccountStatusActive}, + mockSetup: func() { + mockUserRepo.EXPECT(). + UpdateStatus(gomock.Any(), "nonExistentUID", gomock.Any()). + Return(repository.ErrNotFound) + }, + wantErr: true, + }, + { + name: "Database error", + req: usecase.UpdateStatusRequest{UID: "errorUID", Status: member.AccountStatusUninitialized}, + mockSetup: func() { + mockUserRepo.EXPECT(). + UpdateStatus(gomock.Any(), "errorUID", gomock.Any()). + Return(errors.New("database error")) + }, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tt.mockSetup() + + err := uc.UpdateStatus(context.Background(), tt.req) + + if tt.wantErr { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + }) + } +} + +func TestMemberUseCase_ListMember(t *testing.T) { + mockCtrl := gomock.NewController(t) + defer mockCtrl.Finish() + + mockUserRepo := mockRepo.NewMockUserRepository(mockCtrl) + uc := MustMemberUseCase(MemberUseCaseParam{ + User: mockUserRepo, + }) + + tests := []struct { + name string + req usecase.ListUserInfoRequest + mockSetup func() + wantErr bool + }{ + { + name: "Successful member listing", + req: usecase.ListUserInfoRequest{ + PageSize: 10, + PageIndex: 1, + }, + mockSetup: func() { + mockUserRepo.EXPECT(). + ListMembers(gomock.Any(), gomock.Any()). + Return([]*entity.User{ + {UID: "testUID1"}, + {UID: "testUID2"}, + }, int64(2), nil) + }, + wantErr: false, + }, + { + name: "Database error", + req: usecase.ListUserInfoRequest{ + PageSize: 10, + PageIndex: 1, + }, + mockSetup: func() { + mockUserRepo.EXPECT(). + ListMembers(gomock.Any(), gomock.Any()). + Return(nil, int64(0), errors.New("database error")) + }, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tt.mockSetup() + + resp, err := uc.ListMember(context.Background(), tt.req) + + if tt.wantErr { + assert.Error(t, err) + assert.Empty(t, resp.Data) + } else { + assert.NoError(t, err) + assert.NotEmpty(t, resp.Data) + } + }) + } +} diff --git a/pkg/usecase/password_utils.go b/pkg/usecase/password_utils.go new file mode 100644 index 0000000..969cab9 --- /dev/null +++ b/pkg/usecase/password_utils.go @@ -0,0 +1,21 @@ +package usecase + +import "golang.org/x/crypto/bcrypt" + +func HashPassword(password string, cost int) (string, error) { + bytes, err := bcrypt.GenerateFromPassword([]byte(password), cost) + + return string(bytes), err +} + +func CheckPasswordHash(password, hash string) bool { + err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password)) + + return err == nil +} + +func GetHashingCost(hashedPassword []byte) int { + cost, _ := bcrypt.Cost(hashedPassword) + + return cost +} diff --git a/pkg/usecase/password_utils_test.go b/pkg/usecase/password_utils_test.go new file mode 100644 index 0000000..0a14e6f --- /dev/null +++ b/pkg/usecase/password_utils_test.go @@ -0,0 +1,50 @@ +package usecase + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "golang.org/x/crypto/bcrypt" +) + +func TestHashPassword(t *testing.T) { + password := "securepassword123" + cost := bcrypt.DefaultCost + + hashedPassword, err := HashPassword(password, cost) + + assert.NoError(t, err, "生成哈希密碼應該成功") + assert.NotEmpty(t, hashedPassword, "生成的哈希密碼不應為空") + + // 確認哈希密碼長度符合預期 + assert.Greater(t, len(hashedPassword), 0, "哈希密碼長度應大於零") +} + +func TestCheckPasswordHash(t *testing.T) { + password := "securepassword123" + cost := bcrypt.DefaultCost + + // 生成哈希密碼 + hashedPassword, err := HashPassword(password, cost) + assert.NoError(t, err) + + // 驗證密碼與哈希是否匹配 + assert.True(t, CheckPasswordHash(password, hashedPassword), "密碼應與哈希匹配") + + // 測試不匹配的密碼 + wrongPassword := "wrongpassword" + assert.False(t, CheckPasswordHash(wrongPassword, hashedPassword), "錯誤密碼不應與哈希匹配") +} + +func TestGetHashingCost(t *testing.T) { + password := "securepassword123" + cost := bcrypt.DefaultCost + + // 生成哈希密碼 + hashedPassword, err := HashPassword(password, cost) + assert.NoError(t, err) + + // 驗證哈希成本 + actualCost := GetHashingCost([]byte(hashedPassword)) + assert.Equal(t, cost, actualCost, "哈希成本應與生成時的一致") +} diff --git a/pkg/usecase/verify.go b/pkg/usecase/verify.go new file mode 100644 index 0000000..2a08e64 --- /dev/null +++ b/pkg/usecase/verify.go @@ -0,0 +1,119 @@ +package usecase + +import ( + "app-cloudep-member-server/pkg/domain" + "app-cloudep-member-server/pkg/domain/member" + "app-cloudep-member-server/pkg/domain/usecase" + "context" + "fmt" + + "code.30cm.net/digimon/library-go/errs" + "code.30cm.net/digimon/library-go/errs/code" +) + +func (use *MemberUseCase) GenerateRefreshCode(ctx context.Context, param usecase.GenerateRefreshCodeRequest) (usecase.GenerateRefreshCodeResponse, error) { + checkType, status := member.GetCodeNameByCode(param.CodeType) + if !status { + e := errs.ResourceNotFoundWithScope( + code.CloudEPMember, + domain.FailedToGetVerifyCodeErrorCode, + fmt.Sprintf("failed to get verify code type: %d", param.CodeType), + ) + + return usecase.GenerateRefreshCodeResponse{}, e + } + + vc, err := use.VerifyCodeModel.IsVerifyCodeExist(ctx, param.LoginID, checkType) + if err != nil { + return usecase.GenerateRefreshCodeResponse{}, err + } + // 找不到,故要產生 + if vc == "" { + vc, err = generateVerifyCode(6) + if err != nil { + return usecase.GenerateRefreshCodeResponse{}, errs.SystemInternalError(err.Error()) + } + + err = use.VerifyCodeModel.SetVerifyCode(ctx, param.LoginID, checkType, vc) + if err != nil { + return usecase.GenerateRefreshCodeResponse{}, + errs.DatabaseErrorWithScope( + code.CloudEPMember, + domain.FailedToGetCodeOnRedisErrorCode, + "failed to set verify code", + ) + } + } + + return usecase.GenerateRefreshCodeResponse{ + Data: usecase.VerifyCode{ + VerifyCode: vc, + }, + }, nil +} + +func (use *MemberUseCase) VerifyRefreshCode(ctx context.Context, param usecase.VerifyRefreshCodeRequest) error { + err := use.CheckRefreshCode(ctx, param) + if err != nil { + return err + } + + // 因為 CheckRefreshCde 中也有驗證一次,這次就讓他不報錯直接過,因為如果有錯誤,上面那個就會報錯了 + checkType, _ := member.GetCodeNameByCode(param.CodeType) + // todo: 刪不掉看要用什麼方法補刪除,而不是報錯 + _ = use.VerifyCodeModel.DelVerifyCode(ctx, param.LoginID, checkType) + + return nil +} + +func (use *MemberUseCase) CheckRefreshCode(ctx context.Context, param usecase.VerifyRefreshCodeRequest) error { + checkType, status := member.GetCodeNameByCode(param.CodeType) + if !status { + return errs.ResourceNotFoundWithScope( + code.CloudEPMember, + domain.FailedToGetVerifyCodeErrorCode, + fmt.Sprintf("failed to get verify code type: %d", param.CodeType), + ) + } + + get, err := use.VerifyCodeModel.IsVerifyCodeExist(ctx, param.LoginID, checkType) + if err != nil { + return errs.DatabaseErrorWithScope( + code.CloudEPMember, + domain.FailedToGetCodeOnRedisErrorCode, + "failed to set verify code", + ) + } + if get == "" { + return errs.ResourceNotFoundWithScope( + code.CloudEPMember, + domain.FailedToGetCodeOnRedisErrorCode, + "failed to get data", + ) + } + + if get != param.VerifyCode { + return errs.ForbiddenWithScope( + code.CloudEPMember, + domain.FailedToGetCodeCorrectErrorCode, + "failed to verify code", + ) + } + + return nil +} + +func (use *MemberUseCase) VerifyPlatformAuthResult(ctx context.Context, param usecase.VerifyAuthResultRequest) (usecase.VerifyAuthResultResponse, error) { + account, err := use.Account.FindOneByAccount(ctx, param.Account) + if err != nil { + return usecase.VerifyAuthResultResponse{}, errs.ResourceNotFoundWithScope( + code.CloudEPMember, + domain.FailedToFindAccountErrorCode, + fmt.Sprintf("failed to find account: %s", param.Account), + ) + } + + return usecase.VerifyAuthResultResponse{ + Status: CheckPasswordHash(param.Token, account.Token), + }, nil +} diff --git a/pkg/usecase/verify_google.go b/pkg/usecase/verify_google.go new file mode 100644 index 0000000..0ef08ec --- /dev/null +++ b/pkg/usecase/verify_google.go @@ -0,0 +1,128 @@ +package usecase + +import ( + "app-cloudep-member-server/pkg/domain" + "app-cloudep-member-server/pkg/domain/usecase" + "context" + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "strconv" + "time" + + "code.30cm.net/digimon/library-go/errs" + "code.30cm.net/digimon/library-go/errs/code" +) + +func (use *MemberUseCase) VerifyGoogleAuthResult(ctx context.Context, req usecase.VerifyAuthResultRequest) (usecase.GoogleTokenInfo, error) { + var tokenInfo usecase.GoogleTokenInfo + // 發送 Google Token Info API 請求 + body, err := fetchGoogleTokenInfo(ctx, req.Token) + if err != nil { + return tokenInfo, err + } + + // 解析返回的 JSON 數據 + if err := json.Unmarshal(body, &tokenInfo); err != nil { + return tokenInfo, errs.ThirdPartyError( + code.CloudEPMember, + domain.FailedToVerifyGoogle, + "failed to parse token info", + ) + } + + // 驗證 Token 資訊 + if err := validateGoogleTokenInfo(tokenInfo, use.Config.GoogleAuth.ClientID); err != nil { + return tokenInfo, err + } + + return tokenInfo, nil +} + +// fetchGoogleTokenInfo 發送 Google TokenInfo API 請求並返回響應內容 +func fetchGoogleTokenInfo(ctx context.Context, token string) ([]byte, error) { + uri := fmt.Sprintf("https://oauth2.googleapis.com/tokeninfo?id_token=%s", token) + + // 發送請求 + r, err := http.NewRequestWithContext(ctx, http.MethodGet, uri, nil) + if err != nil { + return nil, errs.ThirdPartyError( + code.CloudEPMember, + domain.FailedToVerifyGoogle, + "failed to create request", err.Error()) + } + + resp, err := http.DefaultClient.Do(r) + if err != nil { + if errors.Is(ctx.Err(), context.DeadlineExceeded) { + return nil, errs.ThirdPartyError( + code.CloudEPMember, + domain.FailedToVerifyGoogleTimeout, + "request timeout", + ) + } + + return nil, errs.ThirdPartyError( + code.CloudEPMember, + domain.FailedToVerifyGoogleTimeout, + "failed to request Google TokenInfo API", + ) + } + defer resp.Body.Close() + + // 檢查返回的 HTTP 狀態碼 + if resp.StatusCode != http.StatusOK { + return nil, errs.ThirdPartyError( + code.CloudEPMember, + domain.FailedToVerifyGoogleHTTPCode, + fmt.Sprintf("unexpected status code: %d", resp.StatusCode), + ) + } + + // 讀取響應內容 + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, errs.ThirdPartyError( + code.CloudEPMember, + domain.FailedToVerifyGoogle, + "failed to read response body", + ) + } + + return body, nil +} + +// validateGoogleTokenInfo 驗證 Google Token 資訊 +func validateGoogleTokenInfo(tokenInfo usecase.GoogleTokenInfo, expectedClientID string) error { + // **驗證 1: Token 是否過期** + expiration, err := strconv.ParseInt(tokenInfo.Exp, 10, 64) + if err != nil || expiration <= time.Now().UTC().Unix() { + return errs.ThirdPartyError( + code.CloudEPMember, + domain.FailedToVerifyGoogleTokenExpired, + "token is expired", + ) + } + + // **驗證 2: Audience (aud) 是否與 Google Client ID 匹配** + if tokenInfo.Aud != expectedClientID { + return errs.ThirdPartyError( + code.CloudEPMember, + domain.FailedToVerifyGoogleInvalidAudience, + "invalid audience", + ) + } + + // **驗證 3: 是否 email 已驗證** + if tokenInfo.EmailVerified == "false" { + return errs.ThirdPartyError( + code.CloudEPMember, + domain.FailedToVerifyGoogle, + "email is not verified", + ) + } + + return nil +} diff --git a/pkg/usecase/verify_google_test.go b/pkg/usecase/verify_google_test.go new file mode 100644 index 0000000..54edbf6 --- /dev/null +++ b/pkg/usecase/verify_google_test.go @@ -0,0 +1,84 @@ +package usecase + +import ( + "app-cloudep-member-server/pkg/domain" + "app-cloudep-member-server/pkg/domain/usecase" + "code.30cm.net/digimon/library-go/errs" + "code.30cm.net/digimon/library-go/errs/code" + "github.com/stretchr/testify/assert" + "strconv" + "testing" + "time" +) + +func TestValidateGoogleTokenInfo(t *testing.T) { + expectedClientID := "test-client-id" + + tests := []struct { + name string + tokenInfo usecase.GoogleTokenInfo + expectedErr error + }{ + { + name: "Valid token", + tokenInfo: usecase.GoogleTokenInfo{ + Exp: strconv.FormatInt(time.Now().UTC().Add(10*time.Minute).Unix(), 10), + Aud: expectedClientID, + EmailVerified: "true", + }, + expectedErr: nil, + }, + { + name: "Token expired", + tokenInfo: usecase.GoogleTokenInfo{ + Exp: strconv.FormatInt(time.Now().UTC().Add(-10*time.Minute).Unix(), 10), + Aud: expectedClientID, + EmailVerified: "true", + }, + expectedErr: errs.ThirdPartyError( + code.CloudEPMember, + domain.FailedToVerifyGoogleTokenExpired, + "token is expired", + ), + }, + { + name: "Invalid audience", + tokenInfo: usecase.GoogleTokenInfo{ + Exp: strconv.FormatInt(time.Now().UTC().Add(10*time.Minute).Unix(), 10), + Aud: "invalid-client-id", + EmailVerified: "true", + }, + expectedErr: errs.ThirdPartyError( + code.CloudEPMember, + domain.FailedToVerifyGoogleInvalidAudience, + "invalid audience", + ), + }, + { + name: "Email not verified", + tokenInfo: usecase.GoogleTokenInfo{ + Exp: strconv.FormatInt(time.Now().UTC().Add(10*time.Minute).Unix(), 10), + Aud: expectedClientID, + EmailVerified: "false", + }, + expectedErr: errs.ThirdPartyError( + code.CloudEPMember, + domain.FailedToVerifyGoogle, + "email is not verified", + ), + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + err := validateGoogleTokenInfo(test.tokenInfo, expectedClientID) + + if test.expectedErr != nil { + assert.NotNil(t, err) + assert.EqualError(t, err, test.expectedErr.Error()) + } else { + assert.NoError(t, err) + } + }) + } +} diff --git a/pkg/usecase/verify_line.go b/pkg/usecase/verify_line.go new file mode 100644 index 0000000..67f88b9 --- /dev/null +++ b/pkg/usecase/verify_line.go @@ -0,0 +1,128 @@ +package usecase + +import ( + "app-cloudep-member-server/pkg/domain" + "app-cloudep-member-server/pkg/domain/usecase" + "bytes" + "context" + "encoding/json" + "io" + "net/http" + "net/url" + + "code.30cm.net/digimon/library-go/errs" + "code.30cm.net/digimon/library-go/errs/code" +) + +// LineCodeToAccessToken 透過 Line 授權碼換取 Access Token +func (use *MemberUseCase) LineCodeToAccessToken(ctx context.Context, code string) (usecase.LineAccessTokenResponse, error) { + data := url.Values{ + "grant_type": {"authorization_code"}, + "code": {code}, + "redirect_uri": {use.Config.LineAuth.RedirectURI}, + "client_id": {use.Config.LineAuth.ClientID}, + "client_secret": {use.Config.LineAuth.ClientSecret}, + } + + uri := "https://api.line.me/oauth2/v2.1/token" + headers := map[string]string{ + "Content-Type": "application/x-www-form-urlencoded", + } + + var tokenResponse usecase.LineAccessTokenResponse + if err := use.doPost(ctx, uri, headers, data.Encode(), &tokenResponse); err != nil { + return usecase.LineAccessTokenResponse{}, err + } + + return tokenResponse, nil +} + +// LineGetProfileByAccessToken 使用 Access Token 獲取 Line 用戶資料 +func (use *MemberUseCase) LineGetProfileByAccessToken(ctx context.Context, accessToken string) (*usecase.LineUserProfile, error) { + uri := "https://api.line.me/v2/profile" + headers := map[string]string{ + "Authorization": "Bearer " + accessToken, + } + + var profile usecase.LineUserProfile + if err := use.doGet(ctx, uri, headers, &profile); err != nil { + return nil, err + } + + return &profile, nil +} + +// doPost 發送 POST 請求並解析返回數據 +func (use *MemberUseCase) doPost(ctx context.Context, uri string, headers map[string]string, body string, result interface{}) error { + req, err := http.NewRequestWithContext(ctx, http.MethodPost, uri, bytes.NewBufferString(body)) + if err != nil { + return errs.ThirdPartyError( + code.CloudEPMember, + domain.FailedToVerifyLine, + "failed to create request", + ) + } + for key, value := range headers { + req.Header.Set(key, value) + } + + return use.doRequest(req, result) +} + +// doGet 發送 GET 請求並解析返回數據 +func (use *MemberUseCase) doGet(ctx context.Context, uri string, headers map[string]string, result interface{}) error { + req, err := http.NewRequestWithContext(ctx, http.MethodGet, uri, nil) + if err != nil { + return errs.ThirdPartyError( + code.CloudEPMember, + domain.FailedToVerifyLine, + "failed to create request", + ) + } + for key, value := range headers { + req.Header.Set(key, value) + } + + return use.doRequest(req, result) +} + +// doRequest 發送 HTTP 請求並處理響應 +func (use *MemberUseCase) doRequest(req *http.Request, result interface{}) error { + client := &http.Client{} + resp, err := client.Do(req) + if err != nil { + return errs.ThirdPartyError( + code.CloudEPMember, + domain.FailedToVerifyLine, + "failed to send request", + ) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return errs.ThirdPartyError( + code.CloudEPMember, + domain.FailedToVerifyLine, + "unexpected status code: "+http.StatusText(resp.StatusCode), + ) + } + + body, err := io.ReadAll(resp.Body) + if err != nil { + return errs.ThirdPartyError( + code.CloudEPMember, + domain.FailedToVerifyLine, + "failed to read response body", + ) + } + + if err := json.Unmarshal(body, result); err != nil { + return errs.ThirdPartyError( + code.CloudEPMember, + domain.FailedToVerifyLine, + "failed to parse response body", + ) + } + + return nil +} diff --git a/pkg/usecase/verify_test.go b/pkg/usecase/verify_test.go new file mode 100644 index 0000000..cb9a4c8 --- /dev/null +++ b/pkg/usecase/verify_test.go @@ -0,0 +1,276 @@ +package usecase + +import ( + "app-cloudep-member-server/pkg/domain/entity" + "app-cloudep-member-server/pkg/domain/member" + "app-cloudep-member-server/pkg/domain/usecase" + mockRepo "app-cloudep-member-server/pkg/mock/repository" + "context" + "errors" + "fmt" + "testing" + + "code.30cm.net/digimon/library-go/errs" + "github.com/stretchr/testify/assert" + "go.uber.org/mock/gomock" +) + +func TestGenerateRefreshCode(t *testing.T) { + mockCtrl := gomock.NewController(t) + defer mockCtrl.Finish() + + mockVerifyCodeModel := mockRepo.NewMockVerifyCodeRepository(mockCtrl) + uc := MustMemberUseCase(MemberUseCaseParam{ + VerifyCodeModel: mockVerifyCodeModel, + }) + + tests := []struct { + name string + param usecase.GenerateRefreshCodeRequest + mockSetup func() + wantErr bool + }{ + { + name: "Successful code generation", + param: usecase.GenerateRefreshCodeRequest{ + LoginID: "testLoginID", + CodeType: member.GenerateCodeTypeEmail, + }, + mockSetup: func() { + mockVerifyCodeModel.EXPECT().IsVerifyCodeExist(gomock.Any(), "testLoginID", "email").Return("", nil) + mockVerifyCodeModel.EXPECT().SetVerifyCode(gomock.Any(), "testLoginID", "email", gomock.Any()).Return(nil) + }, + wantErr: false, + }, + { + name: "Code type not found", + param: usecase.GenerateRefreshCodeRequest{ + LoginID: "testLoginID", + CodeType: -999, // Invalid code type + }, + mockSetup: func() {}, + wantErr: true, + }, + { + name: "Existing code retrieval", + param: usecase.GenerateRefreshCodeRequest{ + LoginID: "testLoginID", + CodeType: member.GenerateCodeTypeEmail, + }, + mockSetup: func() { + mockVerifyCodeModel.EXPECT().IsVerifyCodeExist(gomock.Any(), "testLoginID", "email").Return("123456", nil) + }, + wantErr: false, + }, + { + name: "Set verify code failure", + param: usecase.GenerateRefreshCodeRequest{ + LoginID: "testLoginID", + CodeType: member.GenerateCodeTypeEmail, + }, + mockSetup: func() { + mockVerifyCodeModel.EXPECT().IsVerifyCodeExist(gomock.Any(), "testLoginID", "email").Return("", nil) + mockVerifyCodeModel.EXPECT().SetVerifyCode(gomock.Any(), "testLoginID", "email", gomock.Any()).Return(errors.New("redis error")) + }, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tt.mockSetup() + + resp, err := uc.GenerateRefreshCode(context.Background(), tt.param) + + if tt.wantErr { + assert.Error(t, err) + assert.Empty(t, resp) + } else { + assert.NoError(t, err) + assert.NotEmpty(t, resp.Data.VerifyCode) + } + }) + } +} + +func TestCheckRefreshCode(t *testing.T) { + mockCtrl := gomock.NewController(t) + defer mockCtrl.Finish() + + mockVerifyCodeModel := mockRepo.NewMockVerifyCodeRepository(mockCtrl) + uc := MustMemberUseCase(MemberUseCaseParam{ + VerifyCodeModel: mockVerifyCodeModel, + }) + + tests := []struct { + name string + param usecase.VerifyRefreshCodeRequest + mockSetup func() + wantErr bool + }{ + { + name: "Successful verification", + param: usecase.VerifyRefreshCodeRequest{ + LoginID: "testLoginID", + CodeType: member.GenerateCodeTypeEmail, + VerifyCode: "123456", + }, + mockSetup: func() { + mockVerifyCodeModel.EXPECT().IsVerifyCodeExist(gomock.Any(), "testLoginID", "email").Return("123456", nil) + }, + wantErr: false, + }, + { + name: "Code type not found", + param: usecase.VerifyRefreshCodeRequest{ + LoginID: "testLoginID", + CodeType: -1, // Invalid CodeType + VerifyCode: "123456", + }, + mockSetup: func() {}, + wantErr: true, + }, + { + name: "Code not found in Redis", + param: usecase.VerifyRefreshCodeRequest{ + LoginID: "testLoginID", + CodeType: member.GenerateCodeTypeEmail, + VerifyCode: "123456", + }, + mockSetup: func() { + mockVerifyCodeModel.EXPECT().IsVerifyCodeExist(gomock.Any(), "testLoginID", "email").Return("", nil) + }, + wantErr: true, + }, + { + name: "Verification code mismatch", + param: usecase.VerifyRefreshCodeRequest{ + LoginID: "testLoginID", + CodeType: member.GenerateCodeTypeEmail, + VerifyCode: "654321", // Mismatch + }, + mockSetup: func() { + mockVerifyCodeModel.EXPECT().IsVerifyCodeExist(gomock.Any(), "testLoginID", "email").Return("123456", nil) + }, + wantErr: true, + }, + { + name: "Redis retrieval error", + param: usecase.VerifyRefreshCodeRequest{ + LoginID: "testLoginID", + CodeType: member.GenerateCodeTypeEmail, + VerifyCode: "123456", + }, + mockSetup: func() { + mockVerifyCodeModel.EXPECT().IsVerifyCodeExist(gomock.Any(), "testLoginID", "email").Return("", errors.New("redis error")) + }, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tt.mockSetup() + + err := uc.CheckRefreshCode(context.Background(), tt.param) + + if tt.wantErr { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + }) + } +} + +func TestVerifyPlatformAuthResult(t *testing.T) { + mockCtrl := gomock.NewController(t) + defer mockCtrl.Finish() + + mockAccountRepo := mockRepo.NewMockAccountRepository(mockCtrl) + uc := MustMemberUseCase(MemberUseCaseParam{ + Account: mockAccountRepo, + }) + token, err := HashPassword("password", 10) + assert.NoError(t, err) + fmt.Println(token) + tests := []struct { + name string + param usecase.VerifyAuthResultRequest + mockSetup func() + wantResp usecase.VerifyAuthResultResponse + wantErr bool + }{ + { + name: "Successful verification", + param: usecase.VerifyAuthResultRequest{ + Account: "testAccount", + Token: "password", + }, + mockSetup: func() { + mockAccountRepo.EXPECT().FindOneByAccount(gomock.Any(), "testAccount").Return(&entity.Account{ + Token: token, + }, nil) + }, + wantResp: usecase.VerifyAuthResultResponse{ + Status: true, + }, + wantErr: false, + }, + { + name: "Invalid token verification", + param: usecase.VerifyAuthResultRequest{ + Account: "testAccount", + Token: "invalidToken", + }, + mockSetup: func() { + mockAccountRepo.EXPECT().FindOneByAccount(gomock.Any(), "testAccount").Return(&entity.Account{ + Token: "validToken", + }, nil) + }, + wantResp: usecase.VerifyAuthResultResponse{ + Status: false, + }, + wantErr: false, + }, + { + name: "Account not found", + param: usecase.VerifyAuthResultRequest{ + Account: "nonExistentAccount", + Token: "someToken", + }, + mockSetup: func() { + mockAccountRepo.EXPECT().FindOneByAccount(gomock.Any(), "nonExistentAccount").Return(nil, errs.ResourceNotFound("account not found")) + }, + wantResp: usecase.VerifyAuthResultResponse{}, + wantErr: true, + }, + { + name: "Database error", + param: usecase.VerifyAuthResultRequest{ + Account: "testAccount", + Token: "someToken", + }, + mockSetup: func() { + mockAccountRepo.EXPECT().FindOneByAccount(gomock.Any(), "testAccount").Return(nil, errors.New("database error")) + }, + wantResp: usecase.VerifyAuthResultResponse{}, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tt.mockSetup() + + resp, err := uc.VerifyPlatformAuthResult(context.Background(), tt.param) + + if tt.wantErr { + assert.Error(t, err) + } else { + assert.NoError(t, err) + assert.Equal(t, tt.wantResp, resp) + } + }) + } +} diff --git a/readme.md b/readme.md new file mode 100644 index 0000000..e69de29