commit 0bbe2ace83b08e970300075da09f0ab24133d1f3 Author: daniel.w Date: Sun Jul 21 23:57:56 2024 +0800 feat:init project diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f145d53 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +.idea/ +go.sum +account/ +gen_result/ +etc/member.yaml \ No newline at end of file diff --git a/build/Dockerfile b/build/Dockerfile new file mode 100644 index 0000000..d8d6f1b --- /dev/null +++ b/build/Dockerfile @@ -0,0 +1,43 @@ +FROM golang:1.22 AS builder + +LABEL stage=gobuilder + +ENV CGO_ENABLED 0 +RUN apt-get update && \ + apt-get install git + +WORKDIR /build +# +## Download public key for yt.com +#RUN mkdir -p -m 0700 ~/.ssh && ssh-keyscan code.30cm.net >> ~/.ssh/known_hosts +# +## Forces the usage of git and ssh key fwded by ssh-agent for yt.com git repos +#RUN git config --global url."git@git.30cm.net:".insteadOf "https://code.30cm.net" +# +## private go packages +#ENV GOPRIVATE=code.30cm.net + +ADD ../go.mod . +ADD ../go.sum . +RUN --mount=type=ssh go mod download + +ENV FLAG="-s -w -X main.Version=${VERSION} -X main.Built=${BUILT} -X main.GitCommit=${GIT_COMMIT}" +COPY .. . +COPY ../etc /app/etc + +RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build \ + -ldflags "$FLAG" \ + -o /app/member member.go + + +FROM gcr.io/distroless/static-debian12 + +COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt +COPY --from=builder /usr/share/zoneinfo/Asia/Taipei /usr/share/zoneinfo/Asia/Taipei +ENV TZ Asia/Taipei + +WORKDIR /app +COPY --from=builder /app/member /app/member +COPY --from=builder /app/etc /app/etc + +CMD ["./member", "-f", "etc/member.yaml"] diff --git a/etc/member-example.yaml b/etc/member-example.yaml new file mode 100644 index 0000000..3692f16 --- /dev/null +++ b/etc/member-example.yaml @@ -0,0 +1,10 @@ +Name: member.rpc +ListenOn: 0.0.0.0:8080 +Etcd: + Hosts: + - 127.0.0.1:2379 + Key: member.rpc +DB: + DsnString: root:yytt@tcp(127.0.0.1:3306)/ark_member?charset=utf8mb4&parseTime=true&loc=Asia%2FTaipei +Cache: + - Host: 127.0.0.1:7001 \ No newline at end of file diff --git a/generate/database/migration.sh b/generate/database/migration.sh new file mode 100644 index 0000000..e69de29 diff --git a/generate/database/mysql/20230529020011_account_table.down.sql b/generate/database/mysql/20230529020011_account_table.down.sql new file mode 100644 index 0000000..e807a6d --- /dev/null +++ b/generate/database/mysql/20230529020011_account_table.down.sql @@ -0,0 +1 @@ +DROP TABLE IF EXISTS `account`; diff --git a/generate/database/mysql/20230529020011_account_table.up.sql b/generate/database/mysql/20230529020011_account_table.up.sql new file mode 100644 index 0000000..d113a59 --- /dev/null +++ b/generate/database/mysql/20230529020011_account_table.up.sql @@ -0,0 +1,10 @@ +CREATE TABLE `account` ( + `id` BIGINT NOT NULL AUTO_INCREMENT, + `account` VARCHAR(50) NOT NULL, + `token` VARCHAR(255) NOT NULL, + `platform` INT NOT NULL COMMENT '平台類型 1. ark 2. google', + `create_time` BIGINT NOT NULL DEFAULT 0, + `update_time` BIGINT NOT NULL DEFAULT 0, + PRIMARY KEY (`id`), + UNIQUE INDEX `uk_account` (`account` ASC) +)ENGINE=InnoDB COMMENT='帳號'; diff --git a/generate/database/mysql/20230529020011_account_uid_table.down.sql b/generate/database/mysql/20230529020011_account_uid_table.down.sql new file mode 100644 index 0000000..dd19471 --- /dev/null +++ b/generate/database/mysql/20230529020011_account_uid_table.down.sql @@ -0,0 +1 @@ +DROP TABLE IF EXISTS `account_to_uid`; diff --git a/generate/database/mysql/20230529020011_account_uid_table.up.sql b/generate/database/mysql/20230529020011_account_uid_table.up.sql new file mode 100644 index 0000000..bf48c07 --- /dev/null +++ b/generate/database/mysql/20230529020011_account_uid_table.up.sql @@ -0,0 +1,8 @@ +CREATE TABLE `account_to_uid` ( + `id` BIGINT NOT NULL AUTO_INCREMENT, + `account` VARCHAR(50) NOT NULL, + `uid` VARCHAR(255) NOT NULL, + PRIMARY KEY (`id`), + UNIQUE INDEX `uk_account` (`account` ASC), + INDEX `idx_uid` (`uid` ASC) +)ENGINE=InnoDB COMMENT='帳號轉換表'; diff --git a/generate/database/mysql/20230529020011_user_table.down.sql b/generate/database/mysql/20230529020011_user_table.down.sql new file mode 100644 index 0000000..45aaa85 --- /dev/null +++ b/generate/database/mysql/20230529020011_user_table.down.sql @@ -0,0 +1 @@ +DROP TABLE IF EXISTS `user_table`; diff --git a/generate/database/mysql/20230529020011_user_table.up.sql b/generate/database/mysql/20230529020011_user_table.up.sql new file mode 100644 index 0000000..86010e2 --- /dev/null +++ b/generate/database/mysql/20230529020011_user_table.up.sql @@ -0,0 +1,18 @@ +CREATE TABLE `user_table` ( + `id` BIGINT NOT NULL AUTO_INCREMENT, + `verify_type` tinyint DEFAULT 0 NOT NULL COMMENT '驗證類型 0. 異常 1.信箱 2.手機 3. GA 4.不驗證', + `alarm_type` tinyint DEFAULT 0 NOT NULL COMMENT '告警狀態 0. 異常 1. 正常(未告警) 2.系統告警中', + `status` tinyint DEFAULT 0 NOT NULL COMMENT '會員狀態 0. 異常 1. 尚未驗證 2. 啟用 3. 停權中 4. 信箱以驗證 5. 手機以驗證 6. GA 以驗證', + `uid` VARCHAR(255) NOT NULL, + `role_id` VARCHAR(255) NOT NULL DEFAULT '', + `language` VARCHAR(255) NOT NULL DEFAULT '', + `currency` VARCHAR(255) NOT NULL DEFAULT '', + `nick_name` VARCHAR(255) DEFAULT '', + `gender` tinyint DEFAULT 0 NOT NULL COMMENT '0. 不願透露, 1 男 2 女', + `birthday` INT NOT NULL DEFAULT 0, + `create_time` BIGINT NOT NULL DEFAULT 0, + `update_time` BIGINT NOT NULL DEFAULT 0, + PRIMARY KEY (`id`), + INDEX `idx_create_time` (`create_time` ASC), + UNIQUE INDEX `uk_uid` (`uid` ASC) +)ENGINE=InnoDB COMMENT='使用者資訊'; diff --git a/generate/database/mysql/20230719061241_machine_node.down.sql b/generate/database/mysql/20230719061241_machine_node.down.sql new file mode 100644 index 0000000..b299591 --- /dev/null +++ b/generate/database/mysql/20230719061241_machine_node.down.sql @@ -0,0 +1 @@ +DROP TABLE IF EXISTS `machine_node`; \ No newline at end of file diff --git a/generate/database/mysql/20230719061241_machine_node.up.sql b/generate/database/mysql/20230719061241_machine_node.up.sql new file mode 100644 index 0000000..855ccd8 --- /dev/null +++ b/generate/database/mysql/20230719061241_machine_node.up.sql @@ -0,0 +1,7 @@ +CREATE TABLE `machine_node` ( + `id` bigint AUTO_INCREMENT NOT NULL COMMENT '流水號', + `create_time` bigint DEFAULT 0 NOT NULL COMMENT '創建時間', + `update_time` bigint DEFAULT 0 NOT NULL COMMENT '更新時間', + `host_name` varchar(200) DEFAULT '' NOT NULL COMMENT 'host name', + PRIMARY KEY (`id`) +) ENGINE=InnoDB COMMENT='machineID Assigner for Generator'; diff --git a/generate/database/mysql/create/20230529020000_create_schema.down.sql b/generate/database/mysql/create/20230529020000_create_schema.down.sql new file mode 100644 index 0000000..e7727a5 --- /dev/null +++ b/generate/database/mysql/create/20230529020000_create_schema.down.sql @@ -0,0 +1 @@ +DROP DATABASE IF EXISTS `ark_member`; \ No newline at end of file diff --git a/generate/database/mysql/create/20230529020000_create_schema.up.sql b/generate/database/mysql/create/20230529020000_create_schema.up.sql new file mode 100644 index 0000000..d997e04 --- /dev/null +++ b/generate/database/mysql/create/20230529020000_create_schema.up.sql @@ -0,0 +1 @@ +CREATE DATABASE IF NOT EXISTS `ark_member`; \ No newline at end of file diff --git a/generate/protobuf/member.proto b/generate/protobuf/member.proto new file mode 100644 index 0000000..9e8f18d --- /dev/null +++ b/generate/protobuf/member.proto @@ -0,0 +1,216 @@ +syntax = "proto3"; + +package member; +option go_package="./member"; + +// ================ enum ================ +enum VerifyType { + VERIFY_NONE = 0; // 初始(異常) + VERIFY_EMAIL = 1; + VERIFY_PHONE = 2; + VERIFY_GOOGLE = 3; +} + +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; // 帳號停權中 + STATUS_EMAIL = 4; // 信箱以驗證 + STATUS_PHONE = 5; // 手機以驗證 + STATUS_GA = 6; // GA 已綁定 +} + +enum Gender { + GENDER_NONE = 0; // 初始(未提供) + GENDER_MALE = 1; // 男性 + GENDER_FEMALE = 2; // 女性 +} +// ================ enum ================ + + +// ================ common ================ +message Pager { + int64 total =1; + int64 size=2; + int64 index=3; +} + +message Response { + BaseResp status=1; +} + +message BaseResp { + string code = 1; + string message = 2; + string error = 3; +} + +// ================ common ================ + + +// ================ account ================ +message CreateLoginUserReq { + string login_id = 1; + int64 platform = 2; + string token = 3; +} + +message BindingUserReq { + string uid = 1; + string login_id = 2; +} + +message CreateUserInfoReq { + string uid = 1; + VerifyType verify_type = 2; + AlarmType alarm_type = 3; + MemberStatus status = 4; + string role_id = 5; + string language = 6; + string currency = 7; + optional string nick_name = 8; + optional uint32 gender = 9; + optional int64 birthday = 10; +} + +message GetAccountInfoResp { + BaseResp status = 1; + CreateLoginUserReq data = 2; +} + +// UpdateUserInfoReq 不處理邏輯給不給改,這裡只關新增修改刪除 +message UpdateUserInfoReq { + string uid = 1; + optional string language = 2; + optional string currency = 3; + optional string nick_name = 4; + optional uint32 gender = 5; + optional int64 birthday = 6; + optional VerifyType verify_type = 7; + optional AlarmType alarm_type = 8; +} + +message GetUIDByAccountReq { + string account = 1; +} + +message UID { + string uid = 1; +} + +message GetUidByAccountResp { + BaseResp status = 1; + UID data = 2; +} + +message UpdateTokenReq { + string account = 1; + string token = 2; +} + +message GenerateRefreshCodeReq { + string account = 1; + int32 code_type =2; +} + +message VerifyCode { + string verify_code = 1; +} + +message GenerateRefreshCodeResp { + BaseResp status = 1; + VerifyCode data = 2; +} + +message VerifyRefreshCodeReq { + string account = 1; + int32 code_type =2; + string verify_code = 3; +} + +message UpdateStatusReq { + string account = 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 role_id = 5; + string language = 6; + string currency = 7; + optional string nick_name = 8; + optional uint32 gender = 9; + optional int64 birthday = 10; +} + +message GetUserInfoResp { + BaseResp status = 1; + UserInfo data = 2; +} + +message ListUserInfoReq { + optional string role_id = 1; + optional VerifyType verify_type = 2; + optional AlarmType alarm_type = 3; + optional MemberStatus status = 4; + optional int64 create_start_time = 5; + optional int64 create_end_time = 6; + int64 page_size =7; + int64 page_index=8; +} + +message ListUserInfoResp { + BaseResp status = 1; + repeated UserInfo data = 2; + Pager page =3; +} + + +service Account { + // CreateUserAccount 建立帳號與密碼 -> 可登入,但可不可以做其他事情看業務流程,也可以只註冊就好 + rpc CreateUserAccount(CreateLoginUserReq) returns(Response); + // GetUserAccountInfo 取得帳號密碼資料 + rpc GetUserAccountInfo(GetUIDByAccountReq) returns(GetAccountInfoResp); + // UpdateUserToken 更新密碼 + rpc UpdateUserToken(UpdateTokenReq) returns(Response); + + + // GetUidByAccount 用帳號換取 UID + rpc GetUidByAccount(GetUIDByAccountReq) returns(GetUidByAccountResp); + // BindAccount 綁定帳號 -> account bind to UID + rpc BindAccount(BindingUserReq) returns(Response); + + + // BindUserInfo 初次,綁定 User Info + rpc BindUserInfo(CreateUserInfoReq) returns(Response); + // UpdateUserInfo 更新 User Info + rpc UpdateUserInfo(UpdateUserInfoReq) returns(Response); + // UpdateStatus 修改狀態 + rpc UpdateStatus(UpdateStatusReq) returns(Response); + // UpdateStatus 取得會員資訊 + rpc GetUserInfo(GetUserInfoReq) returns(GetUserInfoResp); + // ListMember 取得會員列表 + rpc ListMember(ListUserInfoReq) returns(ListUserInfoResp); + + // GenerateRefreshCode 這個帳號驗證碼(十分鐘),通用的 + rpc GenerateRefreshCode(GenerateRefreshCodeReq) returns(GenerateRefreshCodeResp); + // VerifyRefreshCode 驗證忘記密碼 token + rpc VerifyRefreshCode(VerifyRefreshCodeReq) returns(Response); + +} +// ================ account ================ \ No newline at end of file diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..82f437b --- /dev/null +++ b/go.mod @@ -0,0 +1,101 @@ +module member + +go 1.22.3 + +require ( + github.com/bwmarrin/snowflake v0.3.0 + github.com/go-playground/validator/v10 v10.22.0 + github.com/go-sql-driver/mysql v1.8.1 + github.com/golang/mock v1.6.0 + github.com/stretchr/testify v1.9.0 + github.com/zeromicro/go-zero v1.6.6 + go.uber.org/goleak v1.2.1 + golang.org/x/crypto v0.24.0 + google.golang.org/grpc v1.65.0 + google.golang.org/protobuf v1.34.2 +) + +require ( + filippo.io/edwards25519 v1.1.0 // indirect + github.com/beorn7/perks v1.0.1 // indirect + github.com/cenkalti/backoff/v4 v4.2.1 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/coreos/go-semver v0.3.1 // indirect + github.com/coreos/go-systemd/v22 v22.5.0 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect + github.com/emicklei/go-restful/v3 v3.11.0 // indirect + github.com/fatih/color v1.17.0 // indirect + github.com/gabriel-vasile/mimetype v1.4.3 // indirect + github.com/go-logr/logr v1.3.0 // indirect + github.com/go-logr/stdr v1.2.2 // 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/gogo/protobuf v1.3.2 // indirect + github.com/golang/protobuf v1.5.4 // indirect + github.com/google/gnostic-models v0.6.8 // indirect + github.com/google/go-cmp v0.6.0 // indirect + github.com/google/gofuzz v1.2.0 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.18.0 // indirect + github.com/josharian/intern v1.0.0 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/leodido/go-urn v1.4.0 // 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/matttproud/golang_protobuf_extensions/v2 v2.0.0 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/openzipkin/zipkin-go v0.4.2 // indirect + github.com/pelletier/go-toml/v2 v2.2.2 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/prometheus/client_golang v1.18.0 // indirect + github.com/prometheus/client_model v0.5.0 // indirect + github.com/prometheus/common v0.45.0 // indirect + github.com/prometheus/procfs v0.12.0 // indirect + github.com/redis/go-redis/v9 v9.5.3 // indirect + github.com/spaolacci/murmur3 v1.1.0 // indirect + go.etcd.io/etcd/api/v3 v3.5.14 // indirect + go.etcd.io/etcd/client/pkg/v3 v3.5.14 // indirect + go.etcd.io/etcd/client/v3 v3.5.14 // indirect + go.opentelemetry.io/otel v1.19.0 // indirect + go.opentelemetry.io/otel/exporters/jaeger v1.17.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.19.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.19.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0 // indirect + go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.19.0 // indirect + go.opentelemetry.io/otel/exporters/zipkin v1.19.0 // indirect + go.opentelemetry.io/otel/metric v1.19.0 // indirect + go.opentelemetry.io/otel/sdk v1.19.0 // indirect + go.opentelemetry.io/otel/trace v1.19.0 // indirect + go.opentelemetry.io/proto/otlp v1.0.0 // indirect + go.uber.org/atomic v1.10.0 // indirect + go.uber.org/automaxprocs v1.5.3 // indirect + go.uber.org/multierr v1.9.0 // indirect + go.uber.org/zap v1.24.0 // indirect + golang.org/x/net v0.26.0 // indirect + golang.org/x/oauth2 v0.20.0 // indirect + golang.org/x/sys v0.21.0 // indirect + golang.org/x/term v0.21.0 // indirect + golang.org/x/text v0.16.0 // indirect + golang.org/x/time v0.5.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20240528184218-531527333157 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20240528184218-531527333157 // 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-20230726121419-3b25d923346b // 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..c1927ff --- /dev/null +++ b/internal/config/config.go @@ -0,0 +1,19 @@ +package config + +import ( + "github.com/zeromicro/go-zero/core/stores/cache" + "github.com/zeromicro/go-zero/zrpc" +) + +type Config struct { + zrpc.RpcServerConf + // 加上DB結構體 + DB struct { + DsnString string + } + Cache cache.CacheConf + + Bcrypt struct { + Cost int + } +} diff --git a/internal/domain/const.go b/internal/domain/const.go new file mode 100644 index 0000000..c5725b7 --- /dev/null +++ b/internal/domain/const.go @@ -0,0 +1,5 @@ +package domain + +const ( + Scope = 10 +) diff --git a/internal/domain/sttatus.go b/internal/domain/sttatus.go new file mode 100644 index 0000000..2c72bb0 --- /dev/null +++ b/internal/domain/sttatus.go @@ -0,0 +1,16 @@ +package domain + +import "fmt" + +type Code int + +func (c Code) ToString() string { + return fmt.Sprintf("%d", c) +} + +const ( + CodeOk = Code(102000) + CodeParamInvalid = Code(304000) + CodeInternalError = Code(305000) + CodeAccountExists = Code(306000) +) diff --git a/internal/lib/error/code/define.go b/internal/lib/error/code/define.go new file mode 100644 index 0000000..49715a2 --- /dev/null +++ b/internal/lib/error/code/define.go @@ -0,0 +1,98 @@ +package code + +const ( + OK uint32 = 0 +) + +// Scope +const ( + Unset uint32 = iota + CloudEPPortalGW + CloudEPMember +) + +// Category for general operations: 100 - 4900 +const ( + _ = iota + CatInput uint32 = iota * 100 + CatDB + CatResource + CatGRPC + CatAuth + CatSystem + CatPubSub +) + +// CatArk Category for specific app/service: 5000 - 9900 +const ( + CatArk uint32 = (iota + 50) * 100 +) + +// Detail - Input 1xx +const ( + _ = iota + CatInput + InvalidFormat + NotValidImplementation + InvalidRange +) + +// Detail - Database 2xx +const ( + _ = iota + CatDB + DBError // general error + DBDataConvert + DBDuplicate +) + +// Detail - Resource 3xx +const ( + _ = iota + CatResource + ResourceNotFound + InvalidResourceFormat + ResourceAlreadyExist + ResourceInsufficient + InsufficientPermission + InvalidMeasurementID + ResourceExpired + ResourceMigrated + InvalidResourceState + InsufficientQuota + ResourceHasMultiOwner +) + +/* Detail - GRPC */ +// The GRPC detail code uses Go GRPC's built-in codes. +// Refer to "google.golang.org/grpc/codes" for more detail. + +// Detail - Auth 5xx +const ( + _ = iota + CatAuth + Unauthorized + AuthExpired + InvalidPosixTime + SigAndPayloadNotMatched + Forbidden +) + +// Detail - System 6xx +const ( + _ = iota + CatSystem + SystemInternalError + SystemMaintainError + SystemTimeoutError +) + +// Detail - PubSub 7xx +const ( + _ = iota + CatPubSub + Publish + Consume + MsgSizeTooLarge +) + +// Detail - Ark 5xxx +const ( + _ = iota + CatArk + ArkInternal + ArkHttp400 +) diff --git a/internal/lib/error/code/messsage.go b/internal/lib/error/code/messsage.go new file mode 100644 index 0000000..18a4d4f --- /dev/null +++ b/internal/lib/error/code/messsage.go @@ -0,0 +1,13 @@ +package code + +// CatToStr collects general error messages for each Category +// It is used to send back to API caller +var CatToStr = map[uint32]string{ + CatInput: "Invalid Input Data", + CatDB: "Database Error", + CatResource: "Resource Error", + CatGRPC: "Internal Service Communication Error", + CatAuth: "Authentication Error", + CatArk: "Internal Service Communication Error", + CatSystem: "System Error", +} diff --git a/internal/lib/error/easy_func.go b/internal/lib/error/easy_func.go new file mode 100644 index 0000000..a0f802b --- /dev/null +++ b/internal/lib/error/easy_func.go @@ -0,0 +1,441 @@ +package error + +import ( + "errors" + "fmt" + "github.com/zeromicro/go-zero/core/logx" + _ "github.com/zeromicro/go-zero/core/logx" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + "member/internal/lib/error/code" + "strings" +) + +func newErr(scope, detail uint32, msg string) *Err { + cat := detail / 100 * 100 + return &Err{ + category: cat, + code: detail, + scope: scope, + msg: msg, + } +} + +func newBuiltinGRPCErr(scope, detail uint32, msg string) *Err { + return &Err{ + category: code.CatGRPC, + code: detail, + scope: scope, + msg: msg, + } +} + +// FromError tries to let error as Err +// it supports to unwrap error that has Err +// return nil if failed to transfer +func FromError(err error) *Err { + if err == nil { + return nil + } + + var e *Err + if errors.As(err, &e) { + return e + } + + return nil +} + +// FromCode parses code as following +// Decimal: 120314 +// 12 represents Scope +// 03 represents Category +// 14 represents Detail error code +func FromCode(code uint32) *Err { + scope := code / 10000 + detail := code % 10000 + return &Err{ + category: detail / 100 * 100, + code: detail, + scope: scope, + msg: "", + } +} + +// FromGRPCError transfer error to Err +// useful for gRPC client +func FromGRPCError(err error) *Err { + s, _ := status.FromError(err) + e := FromCode(uint32(s.Code())) + e.msg = s.Message() + + // For GRPC built-in code + if e.Scope() == code.Unset && e.Category() == 0 && e.Code() != code.OK { + e = newBuiltinGRPCErr(Scope, e.Code(), s.Message()) + } + + return e +} + +// Deprecated: check GRPCStatus() in Errs struct +// ToGRPCError returns the status.Status +// Useful to return error in gRPC server +func ToGRPCError(e *Err) error { + return status.New(codes.Code(e.FullCode()), e.Error()).Err() +} + +/*** System ***/ + +// SystemTimeoutError returns Err +func SystemTimeoutError(s ...string) *Err { + return newErr(Scope, code.SystemTimeoutError, fmt.Sprintf("system timeout: %s", strings.Join(s, " "))) +} + +// SystemTimeoutErrorL logs error message and returns Err +func SystemTimeoutErrorL(l logx.Logger, s ...string) *Err { + e := SystemTimeoutError(s...) + l.WithCallerSkip(1).Error(e.Error()) + return e +} + +// SystemInternalError returns Err struct +func SystemInternalError(s ...string) *Err { + return newErr(Scope, code.SystemInternalError, fmt.Sprintf("internal error: %s", strings.Join(s, " "))) +} + +// SystemInternalErrorL logs error message and returns Err +func SystemInternalErrorL(l logx.Logger, s ...string) *Err { + e := SystemInternalError(s...) + l.WithCallerSkip(1).Error(e.Error()) + return e +} + +// SystemMaintainErrorL logs error message and returns Err +func SystemMaintainErrorL(l logx.Logger, s ...string) *Err { + e := SystemMaintainError(s...) + l.WithCallerSkip(1).Error(e.Error()) + return e +} + +// SystemMaintainError returns Err struct +func SystemMaintainError(s ...string) *Err { + return newErr(Scope, code.SystemMaintainError, fmt.Sprintf("service under maintenance: %s", strings.Join(s, " "))) +} + +/*** CatInput ***/ + +// InvalidFormat returns Err struct +func InvalidFormat(s ...string) *Err { + return newErr(Scope, code.InvalidFormat, fmt.Sprintf("invalid format: %s", strings.Join(s, " "))) +} + +// InvalidFormatL logs error message and returns Err +func InvalidFormatL(l logx.Logger, s ...string) *Err { + e := InvalidFormat(s...) + l.WithCallerSkip(1).Error(e.Error()) + return e +} + +// InvalidRange returns Err struct +func InvalidRange(s ...string) *Err { + return newErr(Scope, code.InvalidRange, fmt.Sprintf("invalid range: %s", strings.Join(s, " "))) +} + +// InvalidRangeL logs error message and returns Err +func InvalidRangeL(l logx.Logger, s ...string) *Err { + e := InvalidRange(s...) + l.WithCallerSkip(1).Error(e.Error()) + return e +} + +// NotValidImplementation returns Err struct +func NotValidImplementation(s ...string) *Err { + return newErr(Scope, code.NotValidImplementation, fmt.Sprintf("not valid implementation: %s", strings.Join(s, " "))) +} + +// NotValidImplementationL logs error message and returns Err +func NotValidImplementationL(l logx.Logger, s ...string) *Err { + e := NotValidImplementation(s...) + l.WithCallerSkip(1).Error(e.Error()) + return e +} + +/*** CatDB ***/ + +// DBError returns Err +func DBError(s ...string) *Err { + return newErr(Scope, code.DBError, fmt.Sprintf("db error: %s", strings.Join(s, " "))) +} + +// DBErrorL logs error message and returns Err +func DBErrorL(l logx.Logger, s ...string) *Err { + e := DBError(s...) + l.WithCallerSkip(1).Error(e.Error()) + return e +} + +// DBDataConvert returns Err +func DBDataConvert(s ...string) *Err { + return newErr(Scope, code.DBDataConvert, fmt.Sprintf("data from db convert error: %s", strings.Join(s, " "))) +} + +// DBDataConvertL logs error message and returns Err +func DBDataConvertL(l logx.Logger, s ...string) *Err { + e := DBDataConvert(s...) + l.WithCallerSkip(1).Error(e.Error()) + return e +} + +// DBDuplicate returns Err +func DBDuplicate(s ...string) *Err { + return newErr(Scope, code.DBDuplicate, fmt.Sprintf("data Duplicate key error: %s", strings.Join(s, " "))) +} + +// DBDuplicateL logs error message and returns Err +func DBDuplicateL(l logx.Logger, s ...string) *Err { + e := DBDuplicate(s...) + l.WithCallerSkip(1).Error(e.Error()) + return e +} + +/*** CatResource ***/ + +// ResourceNotFound returns Err and logging +func ResourceNotFound(s ...string) *Err { + return newErr(Scope, code.ResourceNotFound, fmt.Sprintf("resource not found: %s", strings.Join(s, " "))) +} + +// ResourceNotFoundL logs error message and returns Err +func ResourceNotFoundL(l logx.Logger, s ...string) *Err { + e := ResourceNotFound(s...) + l.WithCallerSkip(1).Error(e.Error()) + return e +} + +// InvalidResourceFormat returns Err +func InvalidResourceFormat(s ...string) *Err { + return newErr(Scope, code.InvalidResourceFormat, fmt.Sprintf("invalid resource format: %s", strings.Join(s, " "))) +} + +// InvalidResourceFormatL logs error message and returns Err +func InvalidResourceFormatL(l logx.Logger, s ...string) *Err { + e := InvalidResourceFormat(s...) + l.WithCallerSkip(1).Error(e.Error()) + return e +} + +// InvalidResourceState returns status not correct. +// for example: company should be destroy, agent should be no-sensor/fail-install ... +func InvalidResourceState(s ...string) *Err { + return newErr(Scope, code.InvalidResourceState, fmt.Sprintf("invalid resource state: %s", strings.Join(s, " "))) +} + +// InvalidResourceStateL logs error message and returns status not correct. +func InvalidResourceStateL(l logx.Logger, s ...string) *Err { + e := InvalidResourceState(s...) + l.WithCallerSkip(1).Error(e.Error()) + return e +} + +func ResourceInsufficient(s ...string) *Err { + return newErr(Scope, code.ResourceInsufficient, + fmt.Sprintf("insufficient resource: %s", strings.Join(s, " "))) +} + +func ResourceInsufficientL(l logx.Logger, s ...string) *Err { + e := ResourceInsufficient(s...) + l.WithCallerSkip(1).Error(e.Error()) + return e +} + +// InsufficientPermission returns Err +func InsufficientPermission(s ...string) *Err { + return newErr(Scope, code.InsufficientPermission, + fmt.Sprintf("insufficient permission: %s", strings.Join(s, " "))) +} + +// InsufficientPermissionL returns Err and log +func InsufficientPermissionL(l logx.Logger, s ...string) *Err { + e := InsufficientPermission(s...) + l.WithCallerSkip(1).Error(e.Error()) + return e +} + +// ResourceAlreadyExist returns Err +func ResourceAlreadyExist(s ...string) *Err { + return newErr(Scope, code.ResourceAlreadyExist, fmt.Sprintf("resource already exist: %s", strings.Join(s, " "))) +} + +// ResourceAlreadyExistL logs error message and returns Err +func ResourceAlreadyExistL(l logx.Logger, s ...string) *Err { + e := ResourceAlreadyExist(s...) + l.WithCallerSkip(1).Error(e.Error()) + return e +} + +// InvalidMeasurementID returns Err +func InvalidMeasurementID(s ...string) *Err { + return newErr(Scope, code.InvalidMeasurementID, fmt.Sprintf("missing measurement id: %s", strings.Join(s, " "))) +} + +// InvalidMeasurementIDL logs error message and returns Err +func InvalidMeasurementIDL(l logx.Logger, s ...string) *Err { + e := InvalidMeasurementID(s...) + l.WithCallerSkip(1).Error(e.Error()) + return e +} + +// ResourceExpired returns Err +func ResourceExpired(s ...string) *Err { + return newErr(Scope, code.ResourceExpired, fmt.Sprintf("resource expired: %s", strings.Join(s, " "))) +} + +// ResourceExpiredL logs error message and returns Err +func ResourceExpiredL(l logx.Logger, s ...string) *Err { + e := ResourceExpired(s...) + l.WithCallerSkip(1).Error(e.Error()) + return e +} + +// ResourceMigrated returns Err +func ResourceMigrated(s ...string) *Err { + return newErr(Scope, code.ResourceMigrated, fmt.Sprintf("resource migrated: %s", strings.Join(s, " "))) +} + +// ResourceMigratedL logs error message and returns Err +func ResourceMigratedL(l logx.Logger, s ...string) *Err { + e := ResourceMigrated(s...) + l.WithCallerSkip(1).Error(e.Error()) + return e +} + +// InsufficientQuota returns Err +func InsufficientQuota(s ...string) *Err { + return newErr(Scope, code.InsufficientQuota, fmt.Sprintf("insufficient quota: %s", strings.Join(s, " "))) +} + +// InsufficientQuotaL logs error message and returns Err +func InsufficientQuotaL(l logx.Logger, s ...string) *Err { + e := InsufficientQuota(s...) + l.WithCallerSkip(1).Error(e.Error()) + return e +} + +/*** CatAuth ***/ + +// Unauthorized returns Err +func Unauthorized(s ...string) *Err { + return newErr(Scope, code.Unauthorized, fmt.Sprintf("unauthorized: %s", strings.Join(s, " "))) +} + +// UnauthorizedL logs error message and returns Err +func UnauthorizedL(l logx.Logger, s ...string) *Err { + e := Unauthorized(s...) + l.WithCallerSkip(1).Error(e.Error()) + return e +} + +// AuthExpired returns Err +func AuthExpired(s ...string) *Err { + return newErr(Scope, code.AuthExpired, fmt.Sprintf("expired: %s", strings.Join(s, " "))) +} + +// AuthExpiredL logs error message and returns Err +func AuthExpiredL(l logx.Logger, s ...string) *Err { + e := AuthExpired(s...) + l.WithCallerSkip(1).Error(e.Error()) + return e +} + +// InvalidPosixTime returns Err +func InvalidPosixTime(s ...string) *Err { + return newErr(Scope, code.InvalidPosixTime, fmt.Sprintf("invalid posix time: %s", strings.Join(s, " "))) +} + +// InvalidPosixTimeL logs error message and returns Err +func InvalidPosixTimeL(l logx.Logger, s ...string) *Err { + e := InvalidPosixTime(s...) + l.WithCallerSkip(1).Error(e.Error()) + return e +} + +// SigAndPayloadNotMatched returns Err +func SigAndPayloadNotMatched(s ...string) *Err { + return newErr(Scope, code.SigAndPayloadNotMatched, fmt.Sprintf("signature and the payload are not match: %s", strings.Join(s, " "))) +} + +// SigAndPayloadNotMatchedL logs error message and returns Err +func SigAndPayloadNotMatchedL(l logx.Logger, s ...string) *Err { + e := SigAndPayloadNotMatched(s...) + l.WithCallerSkip(1).Error(e.Error()) + return e +} + +// Forbidden returns Err +func Forbidden(s ...string) *Err { + return newErr(Scope, code.Forbidden, fmt.Sprintf("forbidden: %s", strings.Join(s, " "))) +} + +// ForbiddenL logs error message and returns Err +func ForbiddenL(l logx.Logger, s ...string) *Err { + e := Forbidden(s...) + l.WithCallerSkip(1).Error(e.Error()) + return e +} + +// IsAuthUnauthorizedError check the err is unauthorized error +func IsAuthUnauthorizedError(err *Err) bool { + switch err.Code() { + case code.Unauthorized, code.AuthExpired, code.InvalidPosixTime, + code.SigAndPayloadNotMatched, code.Forbidden, + code.InvalidFormat, code.ResourceNotFound: + return true + default: + return false + } +} + +/*** CatXBC ***/ + +// ArkInternal returns Err +func ArkInternal(s ...string) *Err { + return newErr(Scope, code.ArkInternal, fmt.Sprintf("ark internal error: %s", strings.Join(s, " "))) +} + +// ArkInternalL logs error message and returns Err +func ArkInternalL(l logx.Logger, s ...string) *Err { + e := ArkInternal(s...) + l.WithCallerSkip(1).Error(e.Error()) + return e +} + +/*** CatPubSub ***/ + +// Publish returns Err +func Publish(s ...string) *Err { + return newErr(Scope, code.Publish, fmt.Sprintf("publish: %s", strings.Join(s, " "))) +} + +// PublishL logs error message and returns Err +func PublishL(l logx.Logger, s ...string) *Err { + e := Publish(s...) + l.WithCallerSkip(1).Error(e.Error()) + return e +} + +// Consume returns Err +func Consume(s ...string) *Err { + return newErr(Scope, code.Consume, fmt.Sprintf("consume: %s", strings.Join(s, " "))) +} + +// MsgSizeTooLarge returns Err +func MsgSizeTooLarge(s ...string) *Err { + return newErr(Scope, code.MsgSizeTooLarge, fmt.Sprintf("kafka error: %s", strings.Join(s, " "))) +} + +// MsgSizeTooLargeL logs error message and returns Err +func MsgSizeTooLargeL(l logx.Logger, s ...string) *Err { + e := MsgSizeTooLarge(s...) + l.WithCallerSkip(1).Error(e.Error()) + return e +} diff --git a/internal/lib/error/easy_func_test.go b/internal/lib/error/easy_func_test.go new file mode 100644 index 0000000..f92e680 --- /dev/null +++ b/internal/lib/error/easy_func_test.go @@ -0,0 +1,1009 @@ +package error + +import ( + "context" + "errors" + "fmt" + "github.com/golang/mock/gomock" + "github.com/stretchr/testify/assert" + "github.com/zeromicro/go-zero/core/logx" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + "member/internal/lib/error/code" + "reflect" + "strconv" + "testing" +) + +func TestFromGRPCError_GivenStatusWithCodeAndMessage_ShouldReturnErr(t *testing.T) { + // setup + s := status.Error(codes.Code(102399), "FAKE ERROR") + + // act + e := FromGRPCError(s) + + // assert + assert.Equal(t, uint32(10), e.Scope()) + assert.Equal(t, uint32(2300), e.Category()) + assert.Equal(t, uint32(2399), e.Code()) + assert.Equal(t, "FAKE ERROR", e.Error()) +} + +func TestFromGRPCError_GivenNilError_ShouldReturnErr_Scope0_Cat0_Detail0(t *testing.T) { + // setup + var nilError error = nil + + // act + e := FromGRPCError(nilError) + + // assert + assert.Equal(t, uint32(0), e.Scope()) + assert.Equal(t, uint32(0), e.Category()) + assert.Equal(t, uint32(0), e.Code()) + assert.Equal(t, "", e.Error()) +} + +func TestFromGRPCError_GivenGRPCNativeError_ShouldReturnErr_Scope0_CatGRPC_DetailGRPCUnavailable(t *testing.T) { + // setup + msg := "GRPC Unavailable ERROR" + s := status.Error(codes.Code(codes.Unavailable), msg) + + // act + e := FromGRPCError(s) + + // assert + assert.Equal(t, code.Unset, e.Scope()) + assert.Equal(t, code.CatGRPC, e.Category()) + assert.Equal(t, uint32(codes.Unavailable), e.Code()) + assert.Equal(t, msg, e.Error()) +} + +func TestFromGRPCError_GivenGeneralError_ShouldReturnErr_Scope0_CatGRPC_DetailGRPCUnknown(t *testing.T) { + // setup + generalErr := errors.New("general error") + + // act + e := FromGRPCError(generalErr) + + // assert + assert.Equal(t, code.Unset, e.Scope()) + assert.Equal(t, code.CatGRPC, e.Category()) + assert.Equal(t, uint32(codes.Unknown), e.Code()) +} + +func TestToGRPCError_GivenErr_StatusShouldHave_Code112233(t *testing.T) { + // setup + e := Err{scope: 11, code: 2233, msg: "FAKE MSG"} + + // act + err := ToGRPCError(&e) + s, _ := status.FromError(err) + + // assert + assert.Equal(t, 112233, int(s.Code())) + assert.Equal(t, "FAKE MSG", s.Message()) +} + +func TestInvalidFormat_WithStrings_ShouldHasCatInputAndDetailCode(t *testing.T) { + // setup + Scope = 99 + defer func() { + Scope = code.Unset + }() + + // act + e := InvalidFormat("field A", "Error description") + + // assert + assert.Equal(t, code.CatInput, e.Category()) + assert.Equal(t, code.InvalidFormat, e.Code()) + assert.Equal(t, uint32(99), e.Scope()) + assert.Equal(t, e.Error(), "invalid format: field A Error description") +} + +func TestInvalidFormatL_WithStrings_ShouldHasCatInputAndDetailCode(t *testing.T) { + // setup + Scope = 99 + defer func() { Scope = code.Unset }() + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + ctx := context.Background() + // act + e := InvalidFormatL(logx.WithContext(ctx), "field A", "Error description") + + // assert + assert.Equal(t, code.CatInput, e.Category()) + assert.Equal(t, code.InvalidFormat, e.Code()) + assert.Equal(t, uint32(99), e.Scope()) + assert.Contains(t, e.Error(), "field A") + assert.Contains(t, e.Error(), "Error description") +} + +func TestInvalidRange_WithStrings_ShouldHasCatInputAndDetailCode(t *testing.T) { + // setup + Scope = 99 + defer func() { + Scope = code.Unset + }() + + // act + e := InvalidRange("field A", "Error description") + + // assert + assert.Equal(t, code.CatInput, e.Category()) + assert.Equal(t, code.InvalidRange, e.Code()) + assert.Equal(t, uint32(99), e.Scope()) + assert.Equal(t, e.Error(), "invalid range: field A Error description") +} + +func TestInvalidRangeL_WithStrings_ShouldHasCatInputAndDetailCode(t *testing.T) { + // setup + Scope = 99 + defer func() { Scope = code.Unset }() + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + ctx := context.Background() + // act + e := InvalidRangeL(logx.WithContext(ctx), "field A", "Error description") + + // assert + assert.Equal(t, code.CatInput, e.Category()) + assert.Equal(t, code.InvalidRange, e.Code()) + assert.Equal(t, uint32(99), e.Scope()) + assert.Contains(t, e.Error(), "field A") + assert.Contains(t, e.Error(), "Error description") +} + +func TestNotValidImplementation_WithStrings_ShouldHasCatInputAndDetailCode(t *testing.T) { + // setup + Scope = 99 + defer func() { + Scope = code.Unset + }() + + // act + e := NotValidImplementation("field A", "Error description") + + // assert + assert.Equal(t, code.CatInput, e.Category()) + assert.Equal(t, code.NotValidImplementation, e.Code()) + assert.Equal(t, uint32(99), e.Scope()) + assert.Equal(t, e.Error(), "not valid implementation: field A Error description") +} + +func TestNotValidImplementationL_WithStrings_ShouldHasCatInputAndDetailCode(t *testing.T) { + // setup + Scope = 99 + defer func() { Scope = code.Unset }() + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + l := logx.WithContext(context.Background()) + // act + e := NotValidImplementationL(l, "field A", "Error description") + + // assert + assert.Equal(t, code.CatInput, e.Category()) + assert.Equal(t, code.NotValidImplementation, e.Code()) + assert.Equal(t, uint32(99), e.Scope()) + assert.Contains(t, e.Error(), "field A") + assert.Contains(t, e.Error(), "Error description") +} + +func TestDBError_WithStrings_ShouldHasCatDBAndDetailCodeDBError(t *testing.T) { + // setup + Scope = 99 + defer func() { + Scope = code.Unset + }() + + // act + e := DBError("field A", "Error description") + + // assert + assert.Equal(t, code.CatDB, e.Category()) + assert.Equal(t, code.DBError, e.Code()) + assert.Equal(t, uint32(99), e.Scope()) + assert.Contains(t, e.Error(), "field A") + assert.Contains(t, e.Error(), "Error description") +} + +func TestDBDataConvert_WithStrings_ShouldHasCatDBAndDetailCodeDBDataConvert(t *testing.T) { + // setup + Scope = 99 + defer func() { + Scope = code.Unset + }() + + // act + e := DBDataConvert("field A", "Error description") + + // assert + assert.Equal(t, code.CatDB, e.Category()) + assert.Equal(t, code.DBDataConvert, e.Code()) + assert.Equal(t, uint32(99), e.Scope()) + assert.Contains(t, e.Error(), "field A") + assert.Contains(t, e.Error(), "Error description") +} + +func TestResourceNotFound_WithStrings_ShouldHasCatResource_DetailCodeResourceNotFound(t *testing.T) { + // setup + Scope = 99 + defer func() { + Scope = code.Unset + }() + + // act + e := ResourceNotFound("field A", "Error description") + + // assert + assert.Equal(t, code.CatResource, e.Category()) + assert.Equal(t, code.ResourceNotFound, e.Code()) + assert.Equal(t, uint32(99), e.Scope()) + assert.Contains(t, e.Error(), "field A") + assert.Contains(t, e.Error(), "Error description") +} + +func TestInvalidResourceFormat_WithStrings_ShouldHasCatResource_DetailCodeInvalidResourceFormat(t *testing.T) { + // setup + Scope = 99 + defer func() { + Scope = code.Unset + }() + + // act + e := InvalidResourceFormat("field A", "Error description") + + // assert + assert.Equal(t, code.CatResource, e.Category()) + assert.Equal(t, code.InvalidResourceFormat, e.Code()) + assert.Equal(t, uint32(99), e.Scope()) + assert.Contains(t, e.Error(), "field A") + assert.Contains(t, e.Error(), "Error description") +} + +func TestInvalidResourceState_OK(t *testing.T) { + // setup + Scope = 99 + defer func() { + Scope = code.Unset + }() + + // act + e := InvalidResourceState("field A", "Error description") + + // assert + assert.Equal(t, code.CatResource, e.Category()) + assert.Equal(t, code.InvalidResourceState, e.Code()) + assert.Equal(t, uint32(99), e.Scope()) + assert.EqualError(t, e, "invalid resource state: field A Error description") +} + +func TestInvalidResourceStateL_LogError(t *testing.T) { + // setup + Scope = 99 + defer func() { Scope = code.Unset }() + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + l := logx.WithContext(context.Background()) + + // act + e := InvalidResourceStateL(l, "field A", "Error description") + + // assert + assert.Equal(t, code.CatResource, e.Category()) + assert.Equal(t, code.InvalidResourceState, e.Code()) + assert.Equal(t, uint32(99), e.Scope()) + assert.EqualError(t, e, "invalid resource state: field A Error description") +} + +func TestAuthExpired_OK(t *testing.T) { + // setup + Scope = 99 + defer func() { + Scope = code.Unset + }() + + // act + e := AuthExpired("field A", "Error description") + + // assert + assert.Equal(t, code.CatAuth, e.Category()) + assert.Equal(t, code.AuthExpired, e.Code()) + assert.Equal(t, uint32(99), e.Scope()) + assert.Contains(t, e.Error(), "field A") + assert.Contains(t, e.Error(), "Error description") +} + +func TestUnauthorized_WithStrings_ShouldHasCatAuth_DetailCodeUnauthorized(t *testing.T) { + // setup + Scope = 99 + defer func() { + Scope = code.Unset + }() + + // act + e := Unauthorized("field A", "Error description") + + // assert + assert.Equal(t, code.CatAuth, e.Category()) + assert.Equal(t, code.Unauthorized, e.Code()) + assert.Equal(t, uint32(99), e.Scope()) + assert.Contains(t, e.Error(), "field A") + assert.Contains(t, e.Error(), "Error description") +} + +func TestInvalidPosixTime_WithStrings_ShouldHasCatAuth_DetailCodeInvalidPosixTime(t *testing.T) { + // setup + Scope = 99 + defer func() { + Scope = code.Unset + }() + + // act + e := InvalidPosixTime("field A", "Error description") + + // assert + assert.Equal(t, code.CatAuth, e.Category()) + assert.Equal(t, code.InvalidPosixTime, e.Code()) + assert.Equal(t, uint32(99), e.Scope()) + assert.Contains(t, e.Error(), "field A") + assert.Contains(t, e.Error(), "Error description") +} + +func TestSigAndPayloadNotMatched_WithStrings_ShouldHasCatAuth_DetailCodeSigAndPayloadNotMatched(t *testing.T) { + // setup + Scope = 99 + defer func() { + Scope = code.Unset + }() + + // act + e := SigAndPayloadNotMatched("field A", "Error description") + + // assert + assert.Equal(t, code.CatAuth, e.Category()) + assert.Equal(t, code.SigAndPayloadNotMatched, e.Code()) + assert.Equal(t, uint32(99), e.Scope()) + assert.Contains(t, e.Error(), "field A") + assert.Contains(t, e.Error(), "Error description") +} + +func TestForbidden_WithStrings_ShouldHasCatAuth_DetailCodeForbidden(t *testing.T) { + // setup + Scope = 99 + defer func() { + Scope = code.Unset + }() + + // act + e := Forbidden("field A", "Error description") + + // assert + assert.Equal(t, code.CatAuth, e.Category()) + assert.Equal(t, code.Forbidden, e.Code()) + assert.Equal(t, uint32(99), e.Scope()) + assert.Contains(t, e.Error(), "field A") + assert.Contains(t, e.Error(), "Error description") +} + +func TestXBCInternal_WithStrings_ShouldHasCatResource_DetailCodeXBCInternal(t *testing.T) { + // setup + Scope = 99 + defer func() { + Scope = code.Unset + }() + + // act + e := XBCInternal("field A", "Error description") + + // assert + assert.Equal(t, code.CatArk, e.Category()) + assert.Equal(t, code.ArkInternal, e.Code()) + assert.Equal(t, uint32(99), e.Scope()) + assert.Contains(t, e.Error(), "field A") + assert.Contains(t, e.Error(), "Error description") +} + +func TestGeneralInternalError_WithStrings_DetailInternalError(t *testing.T) { + // setup + Scope = 99 + defer func() { + Scope = code.Unset + }() + + // act + e := SystemInternalError("field A", "Error description") + + // assert + assert.Equal(t, code.CatSystem, e.Category()) + assert.Equal(t, code.SystemInternalError, e.Code()) + assert.Equal(t, uint32(99), e.Scope()) + assert.Contains(t, e.Error(), "field A") + assert.Contains(t, e.Error(), "Error description") +} + +func TestGeneralInternalErrorL_WithStrings_DetailInternalError(t *testing.T) { + // setup + Scope = 99 + defer func() { Scope = code.Unset }() + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + l := logx.WithContext(context.Background()) + + // act + e := SystemInternalErrorL(l, "field A", "Error description") + + // assert + assert.Equal(t, code.CatSystem, e.Category()) + assert.Equal(t, code.SystemInternalError, e.Code()) + assert.Equal(t, uint32(99), e.Scope()) + assert.Contains(t, e.Error(), "field A") + assert.Contains(t, e.Error(), "Error description") +} + +func TestSystemMaintainError_WithStrings_DetailSystemMaintainError(t *testing.T) { + // setup + Scope = 99 + defer func() { Scope = code.Unset }() + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + l := logx.WithContext(context.Background()) + + // act + e := SystemMaintainErrorL(l, "field A", "Error description") + + // assert + assert.Equal(t, code.CatSystem, e.Category()) + assert.Equal(t, code.SystemMaintainError, e.Code()) + assert.Equal(t, uint32(99), e.Scope()) + assert.Contains(t, e.Error(), "field A") + assert.Contains(t, e.Error(), "Error description") +} + +func TestResourceAlreadyExist_WithStrings_DetailInternalError(t *testing.T) { + // setup + Scope = 99 + defer func() { + Scope = code.Unset + }() + + // act + e := ResourceAlreadyExist("field A", "Error description") + + // assert + assert.Equal(t, code.CatResource, e.Category()) + assert.Equal(t, code.ResourceAlreadyExist, e.Code()) + assert.Equal(t, uint32(99), e.Scope()) + assert.Contains(t, e.Error(), "field A") + assert.Contains(t, e.Error(), "Error description") +} + +func TestResourceAlreadyExistL_WithStrings_DetailInternalError(t *testing.T) { + // setup + Scope = 99 + defer func() { Scope = code.Unset }() + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + l := logx.WithContext(context.Background()) + + // act + e := ResourceAlreadyExistL(l, "field A", "Error description") + + // assert + assert.Equal(t, code.CatResource, e.Category()) + assert.Equal(t, code.ResourceAlreadyExist, e.Code()) + assert.Equal(t, uint32(99), e.Scope()) + assert.Contains(t, e.Error(), "field A") + assert.Contains(t, e.Error(), "Error description") +} + +func TestResourceInsufficient_WithStrings_DetailInternalError(t *testing.T) { + // setup + Scope = 99 + defer func() { + Scope = code.Unset + }() + + // act + e := ResourceInsufficient("field A", "Error description") + + // assert + assert.Equal(t, code.CatResource, e.Category()) + assert.Equal(t, code.ResourceInsufficient, e.Code()) + assert.Equal(t, uint32(99), e.Scope()) + assert.Contains(t, e.Error(), "field A") + assert.Contains(t, e.Error(), "Error description") +} + +func TestResourceInsufficientL_WithStrings_DetailInternalError(t *testing.T) { + // setup + Scope = 99 + defer func() { Scope = code.Unset }() + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + l := logx.WithContext(context.Background()) + + // act + e := ResourceInsufficientL(l, "field A", "Error description") + + // assert + assert.Equal(t, code.CatResource, e.Category()) + assert.Equal(t, code.ResourceInsufficient, e.Code()) + assert.Equal(t, uint32(99), e.Scope()) + assert.Contains(t, e.Error(), "field A") + assert.Contains(t, e.Error(), "Error description") +} + +func TestInsufficientPermission_WithStrings_DetailInternalError(t *testing.T) { + // setup + Scope = 99 + defer func() { + Scope = code.Unset + }() + + // act + e := InsufficientPermission("field A", "Error description") + + // assert + assert.Equal(t, code.CatResource, e.Category()) + assert.Equal(t, code.InsufficientPermission, e.Code()) + assert.Equal(t, uint32(99), e.Scope()) + assert.Contains(t, e.Error(), "field A") + assert.Contains(t, e.Error(), "Error description") +} + +func TestInsufficientPermissionL_WithStrings_DetailInternalError(t *testing.T) { + // setup + Scope = 99 + defer func() { Scope = code.Unset }() + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + l := logx.WithContext(context.Background()) + + // act + e := InsufficientPermissionL(l, "field A", "Error description") + + // assert + assert.Equal(t, code.CatResource, e.Category()) + assert.Equal(t, code.InsufficientPermission, e.Code()) + assert.Equal(t, uint32(99), e.Scope()) + assert.Contains(t, e.Error(), "field A") + assert.Contains(t, e.Error(), "Error description") +} + +func TestInvalidMeasurementID_WithErrorStrings_ShouldReturnCorrectCodeAndErrorString(t *testing.T) { + // setup + Scope = 99 + defer func() { + Scope = code.Unset + }() + + // act + e := InvalidMeasurementID("field A", "Error description") + + // assert + assert.Equal(t, code.CatResource, e.Category()) + assert.Equal(t, code.InvalidMeasurementID, e.Code()) + assert.Equal(t, uint32(99), e.Scope()) + assert.Contains(t, e.Error(), "field A") + assert.Contains(t, e.Error(), "Error description") +} + +func TestInvalidMeasurementIDL_WithErrorStrings_ShouldReturnCorrectCodeAndErrorStringAndCallLogger(t *testing.T) { + // setup + Scope = 99 + defer func() { Scope = code.Unset }() + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + l := logx.WithContext(context.Background()) + + // act + e := InvalidMeasurementIDL(l, "field A", "Error description") + + // assert + assert.Equal(t, code.CatResource, e.Category()) + assert.Equal(t, code.InvalidMeasurementID, e.Code()) + assert.Equal(t, uint32(99), e.Scope()) + assert.Contains(t, e.Error(), "field A") + assert.Contains(t, e.Error(), "Error description") +} + +func TestResourceExpired_OK(t *testing.T) { + // setup + Scope = 99 + defer func() { + Scope = code.Unset + }() + + // act + e := ResourceExpired("field A", "Error description") + + // assert + assert.Equal(t, code.CatResource, e.Category()) + assert.Equal(t, code.ResourceExpired, e.Code()) + assert.Equal(t, uint32(99), e.Scope()) + assert.Contains(t, e.Error(), "field A") + assert.Contains(t, e.Error(), "Error description") +} + +func TestResourceExpiredL_LogError(t *testing.T) { + // setup + Scope = 99 + defer func() { Scope = code.Unset }() + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + l := logx.WithContext(context.Background()) + + // act + e := ResourceExpiredL(l, "field A", "Error description") + + // assert + assert.Equal(t, code.CatResource, e.Category()) + assert.Equal(t, code.ResourceExpired, e.Code()) + assert.Equal(t, uint32(99), e.Scope()) + assert.Contains(t, e.Error(), "field A") + assert.Contains(t, e.Error(), "Error description") +} + +func TestResourceMigrated_OK(t *testing.T) { + // setup + Scope = 99 + defer func() { + Scope = code.Unset + }() + + // act + e := ResourceMigrated("field A", "Error description") + + // assert + assert.Equal(t, code.CatResource, e.Category()) + assert.Equal(t, code.ResourceMigrated, e.Code()) + assert.Equal(t, uint32(99), e.Scope()) + assert.Contains(t, e.Error(), "field A") + assert.Contains(t, e.Error(), "Error description") +} + +func TestResourceMigratedL_LogError(t *testing.T) { + // setup + Scope = 99 + defer func() { Scope = code.Unset }() + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + l := logx.WithContext(context.Background()) + + // act + e := ResourceMigratedL(l, "field A", "Error description") + + // assert + assert.Equal(t, code.CatResource, e.Category()) + assert.Equal(t, code.ResourceMigrated, e.Code()) + assert.Equal(t, uint32(99), e.Scope()) + assert.Contains(t, e.Error(), "field A") + assert.Contains(t, e.Error(), "Error description") +} + +func TestInsufficientQuota_OK(t *testing.T) { + // setup + Scope = 99 + defer func() { + Scope = code.Unset + }() + + // act + e := InsufficientQuota("field A", "Error description") + + // assert + assert.Equal(t, code.CatResource, e.Category()) + assert.Equal(t, code.InsufficientQuota, e.Code()) + assert.Equal(t, uint32(99), e.Scope()) + assert.Contains(t, e.Error(), "field A") + assert.Contains(t, e.Error(), "Error description") +} + +func TestInsufficientQuotaL_LogError(t *testing.T) { + // setup + Scope = 99 + defer func() { Scope = code.Unset }() + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + l := logx.WithContext(context.Background()) + + // act + e := InsufficientQuotaL(l, "field A", "Error description") + + // assert + assert.Equal(t, code.CatResource, e.Category()) + assert.Equal(t, code.InsufficientQuota, e.Code()) + assert.Equal(t, uint32(99), e.Scope()) + assert.Contains(t, e.Error(), "field A") + assert.Contains(t, e.Error(), "Error description") +} + +func TestPublish_WithErrorStrings_ShouldReturnCorrectCodeAndErrorString(t *testing.T) { + // setup + Scope = 99 + defer func() { + Scope = code.Unset + }() + + // act + e := Publish("field A", "Error description") + + // assert + assert.Equal(t, code.CatPubSub, e.Category()) + assert.Equal(t, code.Publish, e.Code()) + assert.Equal(t, uint32(99), e.Scope()) + assert.Contains(t, e.Error(), "field A") + assert.Contains(t, e.Error(), "Error description") +} + +func TestPublishL_WithErrorStrings_ShouldReturnCorrectCodeAndErrorStringAndCallLogger(t *testing.T) { + // setup + Scope = 99 + defer func() { Scope = code.Unset }() + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + l := logx.WithContext(context.Background()) + + // act + e := PublishL(l, "field A", "Error description") + + // assert + assert.Equal(t, code.CatPubSub, e.Category()) + assert.Equal(t, code.Publish, e.Code()) + assert.Equal(t, uint32(99), e.Scope()) + assert.Contains(t, e.Error(), "field A") + assert.Contains(t, e.Error(), "Error description") +} + +func TestMsgSizeTooLarge_WithErrorStrings_ShouldReturnCorrectCodeAndErrorString(t *testing.T) { + // setup + Scope = 99 + defer func() { + Scope = code.Unset + }() + + // act + e := MsgSizeTooLarge("Error description") + + // assert + assert.Equal(t, code.CatPubSub, e.Category()) + assert.Equal(t, code.MsgSizeTooLarge, e.Code()) + assert.Equal(t, uint32(99), e.Scope()) + assert.Contains(t, e.Error(), "kafka error: Error description") +} + +func TestMsgSizeTooLargeL_WithErrorStrings_ShouldReturnCorrectCodeAndErrorStringAndCallLogger(t *testing.T) { + // setup + Scope = 99 + defer func() { Scope = code.Unset }() + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + l := logx.WithContext(context.Background()) + + // act + e := MsgSizeTooLargeL(l, "Error description") + + // assert + assert.Equal(t, code.CatPubSub, e.Category()) + assert.Equal(t, code.MsgSizeTooLarge, e.Code()) + assert.Equal(t, uint32(99), e.Scope()) + assert.Contains(t, e.Error(), "kafka error: Error description") +} + +func TestStructErr_WithInternalErr_ShouldIsFuncReportCorrectly(t *testing.T) { + // setup + Scope = 99 + defer func() { Scope = code.Unset }() + // arrange 2 layers err + layer1Err := fmt.Errorf("layer 1 error") + layer2Err := fmt.Errorf("layer 2: %w", layer1Err) + + // act with error chain: InvalidFormat -> layer 2 err -> layer 1 err + e := InvalidFormat("field A", "Error description") + e.Wrap(layer2Err) + + // assert + assert.Equal(t, code.CatInput, e.Category()) + assert.Equal(t, code.InvalidFormat, e.Code()) + assert.Equal(t, uint32(99), e.Scope()) + assert.Contains(t, e.Error(), "field A") + assert.Contains(t, e.Error(), "Error description") + + // errors.Is should report correctly + assert.True(t, errors.Is(e, layer1Err)) + assert.True(t, errors.Is(e, layer2Err)) +} + +func TestStructErr_WithInternalErr_ShouldErrorOutputChainErrMessage(t *testing.T) { + // setup + Scope = 99 + defer func() { Scope = code.Unset }() + + // arrange 2 layers err + layer1Err := fmt.Errorf("layer 1 error") + // act with error chain: InvalidFormat -> layer 1 err + e := InvalidFormat("field A", "Error description") + e.Wrap(layer1Err) + + // assert + assert.Equal(t, "invalid format: field A Error description: layer 1 error", e.Error()) +} + +// arrange a specific err type just for UT +type testErr struct { + code int +} + +func (e *testErr) Error() string { + return strconv.Itoa(e.code) +} + +func TestStructErr_WithInternalErr_ShouldAsFuncReportCorrectly(t *testing.T) { + // setup + Scope = 99 + defer func() { Scope = code.Unset }() + + testE := &testErr{code: 123} + layer2Err := fmt.Errorf("layer 2: %w", testE) + + // act with error chain: InvalidFormat -> layer 2 err -> testErr + e := InvalidFormat("field A", "Error description") + e.Wrap(layer2Err) + + // assert + assert.Equal(t, code.CatInput, e.Category()) + assert.Equal(t, code.InvalidFormat, e.Code()) + assert.Equal(t, uint32(99), e.Scope()) + assert.Contains(t, e.Error(), "field A") + assert.Contains(t, e.Error(), "Error description") + + // errors.As should report correctly + var internalErr *testErr + assert.True(t, errors.As(e, &internalErr)) + assert.Equal(t, testE, internalErr) +} + +/* +benchmark run for 1 second: +Benchmark_ErrorsIs_OneLayerError-4 148281332 8.68 ns/op 0 B/op 0 allocs/op +Benchmark_ErrorsIs_TwoLayerError-4 35048202 32.4 ns/op 0 B/op 0 allocs/op +Benchmark_ErrorsIs_FourLayerError-4 15309349 81.7 ns/op 0 B/op 0 allocs/op + +Benchmark_ErrorsAs_OneLayerError-4 16893205 70.4 ns/op 0 B/op 0 allocs/op +Benchmark_ErrorsAs_TwoLayerError-4 10568083 112 ns/op 0 B/op 0 allocs/op +Benchmark_ErrorsAs_FourLayerError-4 6307729 188 ns/op 0 B/op 0 allocs/op +*/ +func Benchmark_ErrorsIs_OneLayerError(b *testing.B) { + layer1Err := &testErr{code: 123} + var err error = layer1Err + + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + errors.Is(err, layer1Err) + } +} + +func Benchmark_ErrorsIs_TwoLayerError(b *testing.B) { + layer1Err := &testErr{code: 123} + + // act with error chain: InvalidFormat(layer 2) -> testErr(layer 1) + layer2Err := InvalidFormat("field A", "Error description") + layer2Err.Wrap(layer1Err) + + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + errors.Is(layer2Err, layer1Err) + } +} + +func Benchmark_ErrorsIs_FourLayerError(b *testing.B) { + layer1Err := &testErr{code: 123} + layer2Err := fmt.Errorf("layer 2: %w", layer1Err) + layer3Err := fmt.Errorf("layer 3: %w", layer2Err) + // act with error chain: InvalidFormat(layer 4) -> Error(layer 3) -> Error(layer 2) -> testErr(layer 1) + layer4Err := InvalidFormat("field A", "Error description") + layer4Err.Wrap(layer3Err) + + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + errors.Is(layer4Err, layer1Err) + } +} + +func Benchmark_ErrorsAs_OneLayerError(b *testing.B) { + layer1Err := &testErr{code: 123} + var err error = layer1Err + + b.ReportAllocs() + b.ResetTimer() + var internalErr *testErr + for i := 0; i < b.N; i++ { + errors.As(err, &internalErr) + } +} + +func Benchmark_ErrorsAs_TwoLayerError(b *testing.B) { + layer1Err := &testErr{code: 123} + + // act with error chain: InvalidFormat(layer 2) -> testErr(layer 1) + layer2Err := InvalidFormat("field A", "Error description") + layer2Err.Wrap(layer1Err) + + b.ReportAllocs() + b.ResetTimer() + var internalErr *testErr + for i := 0; i < b.N; i++ { + errors.As(layer2Err, &internalErr) + } +} + +func Benchmark_ErrorsAs_FourLayerError(b *testing.B) { + layer1Err := &testErr{code: 123} + layer2Err := fmt.Errorf("layer 2: %w", layer1Err) + layer3Err := fmt.Errorf("layer 3: %w", layer2Err) + // act with error chain: InvalidFormat(layer 4) -> Error(layer 3) -> Error(layer 2) -> testErr(layer 1) + layer4Err := InvalidFormat("field A", "Error description") + layer4Err.Wrap(layer3Err) + + b.ReportAllocs() + b.ResetTimer() + var internalErr *testErr + for i := 0; i < b.N; i++ { + errors.As(layer4Err, &internalErr) + } +} + +func TestFromError(t *testing.T) { + tests := []struct { + name string + givenError error + want *Err + }{ + { + "given nil error should return nil", + nil, + nil, + }, + { + "given normal error should return nil", + errors.New("normal error"), + nil, + }, + { + "given Err should return Err", + ResourceNotFound("fake error"), + ResourceNotFound("fake error"), + }, + { + "given error wraps Err should return Err", + fmt.Errorf("outter error wraps %w", ResourceNotFound("fake error")), + ResourceNotFound("fake error"), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := FromError(tt.givenError); !reflect.DeepEqual(got, tt.want) { + t.Errorf("FromError() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/internal/lib/error/errors.go b/internal/lib/error/errors.go new file mode 100644 index 0000000..7db1f75 --- /dev/null +++ b/internal/lib/error/errors.go @@ -0,0 +1,197 @@ +package error + +import ( + "errors" + "fmt" + "member/internal/lib/error/code" + "net/http" + + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" +) + +// TODO Error要移到common 包 + +// Scope global variable should be set by service or module +var Scope = code.Unset + +type Err struct { + category uint32 + code uint32 + scope uint32 + msg string + internalErr error +} + +// Error is the interface of error +// Getter function of private property "msg" +func (e *Err) Error() string { + if e == nil { + return "" + } + + // chain the error string if the internal err exists + var internalErrStr string + if e.internalErr != nil { + internalErrStr = e.internalErr.Error() + } + + if e.msg != "" { + if internalErrStr != "" { + return fmt.Sprintf("%s: %s", e.msg, internalErrStr) + } + return e.msg + } + + generalErrStr := e.GeneralError() + if internalErrStr != "" { + return fmt.Sprintf("%s: %s", generalErrStr, internalErrStr) + } + return generalErrStr +} + +// Category getter function of private property "category" +func (e *Err) Category() uint32 { + if e == nil { + return 0 + } + return e.category +} + +// Scope getter function of private property "scope" +func (e *Err) Scope() uint32 { + if e == nil { + return code.Unset + } + + return e.scope +} + +// CodeStr returns the string of error code with zero padding +func (e *Err) CodeStr() string { + if e == nil { + return "00000" + } + + if e.Category() == code.CatGRPC { + return fmt.Sprintf("%d%04d", e.Scope(), e.Category()+e.Code()) + } + + return fmt.Sprintf("%d%04d", e.Scope(), e.Code()) +} + +// Code getter function of private property "code" +func (e *Err) Code() uint32 { + if e == nil { + return code.OK + } + + return e.code +} + +func (e *Err) FullCode() uint32 { + if e == nil { + return 0 + } + + if e.Category() == code.CatGRPC { + return e.Scope()*10000 + e.Category() + e.Code() + } + + return e.Scope()*10000 + e.Code() +} + +// HTTPStatus returns corresponding HTTP status code +func (e *Err) HTTPStatus() int { + if e == nil || e.Code() == code.OK { + return http.StatusOK + } + // determine status code by code + switch e.Code() { + case code.ResourceInsufficient: + // 400 + return http.StatusBadRequest + case code.Unauthorized, code.InsufficientPermission: + // 401 + return http.StatusUnauthorized + case code.InsufficientQuota: + // 402 + return http.StatusPaymentRequired + case code.InvalidPosixTime, code.Forbidden: + // 403 + return http.StatusForbidden + case code.ResourceNotFound: + // 404 + return http.StatusNotFound + case code.ResourceAlreadyExist, code.InvalidResourceState: + // 409 + return http.StatusConflict + case code.NotValidImplementation: + // 501 + return http.StatusNotImplemented + default: + } + + // determine status code by category + switch e.Category() { + case code.CatInput: + return http.StatusBadRequest + default: + // return status code 500 if none of the condition is met + return http.StatusInternalServerError + } +} + +// GeneralError transform category level error message +// It's the general error message for customer/API caller +func (e *Err) GeneralError() string { + if e == nil { + return "" + } + + errStr, ok := code.CatToStr[e.Category()] + if !ok { + return "" + } + + return errStr +} + +// Is called when performing errors.Is(). +// DO NOT USE THIS FUNCTION DIRECTLY unless you are very certain about what you're doing. +// Use errors.Is instead. +// This function compares if two error variables are both *Err, and have the same code (without checking the wrapped internal error) +func (e *Err) Is(f error) bool { + var err *Err + ok := errors.As(f, &err) + if !ok { + return false + } + return e.Code() == err.Code() +} + +// Unwrap returns the underlying error +// The result of unwrapping an error may itself have an Unwrap method; +// we call the sequence of errors produced by repeated unwrapping the error chain. +func (e *Err) Unwrap() error { + if e == nil { + return nil + } + return e.internalErr +} + +// Wrap sets the internal error to Err struct +func (e *Err) Wrap(internalErr error) *Err { + if e != nil { + e.internalErr = internalErr + } + return e +} + +func (e *Err) GRPCStatus() *status.Status { + if e == nil { + return status.New(codes.OK, "") + } + + return status.New(codes.Code(e.FullCode()), e.Error()) +} diff --git a/internal/lib/error/errors_test.go b/internal/lib/error/errors_test.go new file mode 100644 index 0000000..33c1625 --- /dev/null +++ b/internal/lib/error/errors_test.go @@ -0,0 +1,296 @@ +package error + +import ( + "errors" + "fmt" + "github.com/stretchr/testify/assert" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + "member/internal/lib/error/code" + "net/http" + "testing" +) + +func TestCode_GivenNilReceiver_CodeReturnOK_CodeStrReturns00000(t *testing.T) { + // setup + var e *Err = nil + + // act & assert + assert.Equal(t, code.OK, e.Code()) + assert.Equal(t, "00000", e.CodeStr()) + assert.Equal(t, "", e.Error()) +} + +func TestCode_GivenScope99DetailCode6687_ShouldReturn996687(t *testing.T) { + // setup + e := Err{scope: 99, code: 6687} + + // act & assert + assert.Equal(t, uint32(6687), e.Code()) + assert.Equal(t, "996687", e.CodeStr()) +} + +func TestCode_GivenScope0DetailCode87_ShouldReturn87(t *testing.T) { + // setup + e := Err{scope: 0, code: 87} + + // act & assert + assert.Equal(t, uint32(87), e.Code()) + assert.Equal(t, "00087", e.CodeStr()) +} + +func TestFromCode_Given870005_ShouldHasScope87_Cat0_Detail5(t *testing.T) { + // setup + e := FromCode(870005) + + // assert + assert.Equal(t, uint32(87), e.Scope()) + assert.Equal(t, uint32(0), e.Category()) + assert.Equal(t, uint32(5), e.Code()) + assert.Equal(t, "", e.Error()) +} + +func TestFromCode_Given0_ShouldHasScope0_Cat0_Detail0(t *testing.T) { + // setup + e := FromCode(0) + + // assert + assert.Equal(t, uint32(0), e.Scope()) + assert.Equal(t, uint32(0), e.Category()) + assert.Equal(t, uint32(0), e.Code()) + assert.Equal(t, "", e.Error()) +} + +func TestFromCode_Given9105_ShouldHasScope0_Cat9100_Detail9105(t *testing.T) { + // setup + e := FromCode(9105) + + // assert + assert.Equal(t, uint32(0), e.Scope()) + assert.Equal(t, uint32(9100), e.Category()) + assert.Equal(t, uint32(9105), e.Code()) + assert.Equal(t, "", e.Error()) +} + +func TestErr_ShouldImplementErrorFunction(t *testing.T) { + // setup a func return error + f := func() error { return InvalidFormat("fake field") } + + // act + err := f() + + // assert + assert.NotNil(t, err) + assert.Contains(t, fmt.Sprint(err), "fake field") // can be printed +} + +func TestGeneralError_GivenNilErr_ShouldReturnEmptyString(t *testing.T) { + // setup + var e *Err = nil + + // act & assert + assert.Equal(t, "", e.GeneralError()) +} + +func TestGeneralError_GivenNotExistCat_ShouldReturnEmptyString(t *testing.T) { + // setup + e := Err{category: 123456} + + // act & assert + assert.Equal(t, "", e.GeneralError()) +} + +func TestGeneralError_GivenCatDB_ShouldReturnDBError(t *testing.T) { + // setup + e := Err{category: code.CatDB} + catErrStr := code.CatToStr[code.CatDB] + + // act & assert + assert.Equal(t, catErrStr, e.GeneralError()) +} + +func TestError_GivenEmptyMsg_ShouldReturnCatGeneralErrorMessage(t *testing.T) { + // setup + e := Err{category: code.CatDB, msg: ""} + + // act + errMsg := e.Error() + + // assert + assert.Equal(t, code.CatToStr[code.CatDB], errMsg) +} + +func TestError_GivenMsg_ShouldReturnGiveMsg(t *testing.T) { + // setup + e := Err{msg: "FAKE"} + + // act + errMsg := e.Error() + + // assert + assert.Equal(t, "FAKE", errMsg) +} + +func TestIs_GivenNilErr_ShouldReturnFalse(t *testing.T) { + var nilErrs *Err + // act + result := errors.Is(nilErrs, DBError()) + result2 := errors.Is(DBError(), nilErrs) + + // assert + assert.False(t, result) + assert.False(t, result2) +} + +func TestIs_GivenNil_ShouldReturnFalse(t *testing.T) { + // act + result := errors.Is(nil, DBError()) + result2 := errors.Is(DBError(), nil) + + // assert + assert.False(t, result) + assert.False(t, result2) +} + +func TestIs_GivenNilReceiver_ShouldReturnCorrectResult(t *testing.T) { + var nilErr *Err = nil + + // test 1: nilErr != DBError + var dbErr error = DBError("fake db error") + assert.False(t, nilErr.Is(dbErr)) + + // test 2: nilErr != nil error + var nilError error + assert.False(t, nilErr.Is(nilError)) + + // test 3: nilErr == another nilErr + var nilErr2 *Err = nil + assert.True(t, nilErr.Is(nilErr2)) +} + +func TestIs_GivenDBError_ShouldReturnTrue(t *testing.T) { + // setup + dbErr := DBError("fake db error") + + // act + result := errors.Is(dbErr, DBError("not care")) + result2 := errors.Is(DBError(), dbErr) + + // assert + assert.True(t, result) + assert.True(t, result2) +} + +func TestIs_GivenDBErrorAssignToErrorType_ShouldReturnTrue(t *testing.T) { + // setup + var dbErr error = DBError("fake db error") + + // act + result := errors.Is(dbErr, DBError("not care")) + result2 := errors.Is(DBError(), dbErr) + + // assert + assert.True(t, result) + assert.True(t, result2) +} + +func TestWrap_GivenNilErr_ShouldNoPanic(t *testing.T) { + // act & assert + assert.NotPanics(t, func() { + var e *Err = nil + _ = e.Wrap(fmt.Errorf("test")) + }) +} + +func TestWrap_GivenErrorToWrap_ShouldReturnErrorWithWrappedError(t *testing.T) { + // act & assert + wrappedErr := fmt.Errorf("test") + wrappingErr := SystemInternalError("WrappingError").Wrap(wrappedErr) + unWrappedErr := wrappingErr.Unwrap() + + assert.Equal(t, wrappedErr, unWrappedErr) +} + +func TestUnwrap_GivenNilErr_ShouldReturnNil(t *testing.T) { + var e *Err = nil + internalErr := e.Unwrap() + assert.Nil(t, internalErr) +} + +func TestErrorsIs_GivenNilErr_ShouldReturnFalse(t *testing.T) { + var e *Err = nil + assert.False(t, errors.Is(e, fmt.Errorf("test"))) +} + +func TestErrorsAs_GivenNilErr_ShouldReturnFalse(t *testing.T) { + var internalErr *testErr + var e *Err = nil + assert.False(t, errors.As(e, &internalErr)) +} + +func TestGRPCStatus(t *testing.T) { + // setup table driven tests + tests := []struct { + name string + given *Err + expect *status.Status + expectConvert error + }{ + { + "nil errs.Err", + nil, + status.New(codes.OK, ""), + nil, + }, + { + "InvalidFormat Err", + InvalidFormat("fake"), + status.New(codes.Code(101), "invalid format: fake"), + status.New(codes.Code(101), "invalid format: fake").Err(), + }, + } + + // act & assert + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + s := test.given.GRPCStatus() + assert.Equal(t, test.expect.Code(), s.Code()) + assert.Equal(t, test.expect.Message(), s.Message()) + assert.Equal(t, test.expectConvert, status.Convert(test.given).Err()) + }) + } +} + +func TestErr_HTTPStatus(t *testing.T) { + tests := []struct { + name string + err *Err + want int + }{ + {name: "nil error", err: nil, want: http.StatusOK}, + {name: "invalid measurement id", err: &Err{category: code.CatResource, code: code.InvalidMeasurementID}, want: http.StatusInternalServerError}, + {name: "resource already exists", err: &Err{category: code.CatResource, code: code.ResourceAlreadyExist}, want: http.StatusConflict}, + {name: "invalid resource state", err: &Err{category: code.CatResource, code: code.InvalidResourceState}, want: http.StatusConflict}, + {name: "invalid posix time", err: &Err{category: code.CatAuth, code: code.InvalidPosixTime}, want: http.StatusForbidden}, + {name: "unauthorized", err: &Err{category: code.CatAuth, code: code.Unauthorized}, want: http.StatusUnauthorized}, + {name: "db error", err: &Err{category: code.CatDB, code: code.DBError}, want: http.StatusInternalServerError}, + {name: "insufficient permission", err: &Err{category: code.CatResource, code: code.InsufficientPermission}, want: http.StatusUnauthorized}, + {name: "resource insufficient", err: &Err{category: code.CatResource, code: code.ResourceInsufficient}, want: http.StatusBadRequest}, + {name: "invalid format", err: &Err{category: code.CatInput, code: code.InvalidFormat}, want: http.StatusBadRequest}, + {name: "resource not found", err: &Err{code: code.ResourceNotFound}, want: http.StatusNotFound}, + {name: "ok", err: &Err{code: code.OK}, want: http.StatusOK}, + {name: "not valid implementation", err: &Err{category: code.CatInput, code: code.NotValidImplementation}, want: http.StatusNotImplemented}, + {name: "forbidden", err: &Err{category: code.CatAuth, code: code.Forbidden}, want: http.StatusForbidden}, + {name: "insufficient quota", err: &Err{category: code.CatResource, code: code.InsufficientQuota}, want: http.StatusPaymentRequired}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + + // act + got := tt.err.HTTPStatus() + + // assert + assert.Equal(t, tt.want, got) + }) + } +} diff --git a/internal/lib/middleware/with_context.go b/internal/lib/middleware/with_context.go new file mode 100644 index 0000000..65f21b1 --- /dev/null +++ b/internal/lib/middleware/with_context.go @@ -0,0 +1,27 @@ +package middleware + +import ( + "context" + "errors" + "github.com/zeromicro/go-zero/core/logx" + "google.golang.org/grpc" + ers "member/internal/lib/error" + "time" +) + +const defaultTimeout = 30 * time.Second + +func TimeoutMiddleware(ctx context.Context, req any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp any, err error) { + + newCtx, cancelCtx := context.WithTimeout(ctx, defaultTimeout) + defer func() { + cancelCtx() + + if errors.Is(newCtx.Err(), context.DeadlineExceeded) { + err = ers.SystemTimeoutError(info.FullMethod) + logx.Errorf("Method: %s, request %v, timeout: %d", info.FullMethod, req, defaultTimeout) + } + }() + + return handler(ctx, req) +} diff --git a/internal/lib/required/validate.go b/internal/lib/required/validate.go new file mode 100644 index 0000000..f21031c --- /dev/null +++ b/internal/lib/required/validate.go @@ -0,0 +1,24 @@ +package required + +import "github.com/go-playground/validator/v10" + +// ValidateAll TODO 要移到common 包 +func ValidateAll(validate *validator.Validate, obj any) error { + err := validate.Struct(obj) + if err != nil { + return err + } + + return nil +} + +func MustValidator(option ...Option) *validator.Validate { + // TODO Validator 要抽出來 + v := validator.New() + err := BindToValidator(v, option...) + if err != nil { + // log + } + + return v +} diff --git a/internal/lib/required/validate_option.go b/internal/lib/required/validate_option.go new file mode 100644 index 0000000..24851ab --- /dev/null +++ b/internal/lib/required/validate_option.go @@ -0,0 +1,41 @@ +package required + +import ( + "fmt" + "regexp" + + "github.com/go-playground/validator/v10" +) + +type Option struct { + ValidatorName string + ValidatorFunc func(fl validator.FieldLevel) bool +} + +func BindToValidator(v *validator.Validate, opts ...Option) error { + for _, item := range opts { + err := v.RegisterValidation(item.ValidatorName, item.ValidatorFunc) + if err != nil { + return fmt.Errorf("failed to register validator : %w", err) + } + } + + return nil +} + +// WithAccount 創建一個新的 Option 結構,包含自定義的驗證函數,用於驗證 email 和台灣的手機號碼格式 +func WithAccount(tagName string) Option { + return Option{ + ValidatorName: tagName, + ValidatorFunc: func(fl validator.FieldLevel) bool { + value := fl.Field().String() + emailRegex := `^[a-z0-9._%+\-]+@[a-z0-9.\-]+\.[a-z]{2,}$` + phoneRegex := `^(\+886|0)?9\d{8}$` + + emailMatch, _ := regexp.MatchString(emailRegex, value) + phoneMatch, _ := regexp.MatchString(phoneRegex, value) + + return emailMatch || phoneMatch + }, + } +} diff --git a/internal/lib/snackflow/error.go b/internal/lib/snackflow/error.go new file mode 100644 index 0000000..45327f2 --- /dev/null +++ b/internal/lib/snackflow/error.go @@ -0,0 +1,18 @@ +package snowflake + +import ( + "fmt" + "time" +) + +type NewNodeError struct { + machineNodeID int64 + startTime time.Time + + Err error +} + +func (e *NewNodeError) Error() string { + return fmt.Sprintf("new node fail machineNodeID: %d, startTime: %s, err: %v", + e.machineNodeID, e.startTime, e.Err) +} diff --git a/internal/lib/snackflow/error_test.go b/internal/lib/snackflow/error_test.go new file mode 100644 index 0000000..48c64b4 --- /dev/null +++ b/internal/lib/snackflow/error_test.go @@ -0,0 +1,54 @@ +package snowflake + +import ( + "fmt" + "testing" + "time" +) + +func TestNewNodeError_Error(t *testing.T) { + startTime, err := time.Parse(time.DateOnly, "2023-07-20") + if err != nil { + t.Error(err) + } + + t.Parallel() + + type fields struct { + machineNodeID int64 + startTime time.Time + Err error + } + tests := []struct { + name string + fields fields + want string + }{ + { + name: "success", + fields: fields{ + machineNodeID: 1, + startTime: startTime, + Err: nil, + }, + want: fmt.Sprintf("new node fail machineNodeID: %d, startTime: %s, err: %v", + 1, "2023-07-20 00:00:00 +0000 UTC", nil), + }, + } + for _, tt := range tests { + tt := tt + + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + e := &NewNodeError{ + machineNodeID: tt.fields.machineNodeID, + startTime: tt.fields.startTime, + Err: tt.fields.Err, + } + if got := e.Error(); got != tt.want { + t.Errorf("Error() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/internal/lib/snackflow/readme.md b/internal/lib/snackflow/readme.md new file mode 100644 index 0000000..bdf2d8d --- /dev/null +++ b/internal/lib/snackflow/readme.md @@ -0,0 +1,8 @@ +### +# snowflake + +```go +import "yt.com/backend/common.git/snowflake" +``` + +### 量級超過1024 再來做解決 \ No newline at end of file diff --git a/internal/lib/snackflow/snowflake.go b/internal/lib/snackflow/snowflake.go new file mode 100644 index 0000000..09f3fb6 --- /dev/null +++ b/internal/lib/snackflow/snowflake.go @@ -0,0 +1,81 @@ +package snowflake + +import ( + "fmt" + "sync" + "time" + + "github.com/bwmarrin/snowflake" +) + +var mu sync.Mutex + +// Snowflake provides a way to NewNode for Generate UID. +type Snowflake struct { + machineNodeID int64 + + startTime time.Time +} + +// Option is the options type to configure Snowflake. +type Option func(*Snowflake) + +// New returns a new Snowflake instance with the provided options. +func New(opts ...Option) *Snowflake { + s := &Snowflake{ + // default machine 1 + machineNodeID: 1, + } + + for _, opt := range opts { + opt(s) + } + + return s +} + +// WithMachineNodeID adds machineID total 10bit = 1024 machine number. +func WithMachineNodeID(machineNodeID int64) Option { + return func(snowflake *Snowflake) { + snowflake.machineNodeID = machineNodeID + } +} + +// WithStartTime adds snowflake start timestamp in milliseconds. +func WithStartTime(startTime time.Time) Option { + return func(snowflake *Snowflake) { + snowflake.startTime = startTime + } +} + +// GetNowDate return nowTodayDate e.g. 2023-07-20 00:00:00 +0000 UTC. +func GetNowDate() (time.Time, error) { + startTime := time.Now().UTC().Format(time.DateOnly) + + st, err := time.Parse(time.DateOnly, startTime) + if err != nil { + return time.Time{}, fmt.Errorf("time.Parse failed :%w", err) + } + + return st, nil +} + +// NewNode return snowflake node use Generate UID. +func (s *Snowflake) NewNode() (*snowflake.Node, error) { + mu.Lock() + defer mu.Unlock() + + snowflake.Epoch = s.startTime.UnixMilli() + + node, err := snowflake.NewNode(s.machineNodeID) + if err != nil { + return nil, fmt.Errorf("snowflake.NewNode, failed :%w", + &NewNodeError{ + machineNodeID: s.machineNodeID, + startTime: s.startTime, + Err: err, + }) + } + + return node, nil +} diff --git a/internal/lib/snackflow/snowflake_test.go b/internal/lib/snackflow/snowflake_test.go new file mode 100644 index 0000000..2b477af --- /dev/null +++ b/internal/lib/snackflow/snowflake_test.go @@ -0,0 +1,148 @@ +package snowflake + +import ( + "flag" + "os" + "reflect" + "testing" + "time" + + "go.uber.org/goleak" +) + +func TestMain(m *testing.M) { + leak := flag.Bool("leak", false, "use leak detector") + flag.Parse() + + if *leak { + goleak.VerifyTestMain(m) + + return + } + + os.Exit(m.Run()) +} + +func TestSnowflake(t *testing.T) { + st, err := GetNowDate() + if err != nil { + t.Error(err) + } + + t.Parallel() + + type args struct { + machineNodeID int64 + startTime time.Time + } + tests := []struct { + name string + args args + want *Snowflake + + wantDeepEqualErr bool + wantNewNodeErr bool + }{ + { + name: "success", + args: args{ + machineNodeID: 10, + startTime: st, + }, + want: &Snowflake{ + machineNodeID: 10, + startTime: st, + }, + wantDeepEqualErr: false, + wantNewNodeErr: false, + }, + { + name: "failed machine node ID negative number", + args: args{ + machineNodeID: -1, + startTime: time.Time{}, + }, + want: &Snowflake{ + machineNodeID: -1, + startTime: time.Time{}, + }, + wantDeepEqualErr: false, + wantNewNodeErr: true, + }, + { + name: "failed snowflake struct field by machine node ID", + args: args{ + machineNodeID: 10, + startTime: st, + }, + want: &Snowflake{ + machineNodeID: 2, + startTime: st, + }, + wantDeepEqualErr: true, + wantNewNodeErr: false, + }, + { + name: "failed snowflake struct field by startTime", + args: args{ + machineNodeID: 2, + startTime: st, + }, + want: &Snowflake{ + machineNodeID: 2, + startTime: time.Time{}, + }, + wantDeepEqualErr: true, + wantNewNodeErr: false, + }, + } + for _, tt := range tests { + tt := tt + + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + got := New( + WithMachineNodeID(tt.args.machineNodeID), + WithStartTime(tt.args.startTime), + ) + + if !reflect.DeepEqual(got, tt.want) != tt.wantDeepEqualErr { + t.Errorf("Snowflake.New() = %v, want %v", got, tt.want) + } + + node, err := got.NewNode() + if (err != nil) != tt.wantNewNodeErr { + t.Errorf("NewNode() = %v, want %v", err != nil, tt.wantNewNodeErr) + } + + if err == nil { + id := node.Generate().Int64() + if id <= 0 { + t.Errorf("node.Generate().Int64() = %v, want %s", id, "id > 0") + } + } + }) + } +} + +func BenchmarkSnowflake(b *testing.B) { + st, err := GetNowDate() + if err != nil { + b.Error(err) + } + + snowflake := New( + WithMachineNodeID(1), + WithStartTime(st), + ) + + node, err := snowflake.NewNode() + if err != nil { + b.Error(err) + } + + for i := 0; i < b.N; i++ { + node.Generate().Int64() + } +} diff --git a/internal/logic/bind_account_logic.go b/internal/logic/bind_account_logic.go new file mode 100644 index 0000000..9eaff7e --- /dev/null +++ b/internal/logic/bind_account_logic.go @@ -0,0 +1,73 @@ +package logic + +import ( + "context" + "fmt" + "member/internal/domain" + ers "member/internal/lib/error" + "member/internal/lib/required" + "member/internal/model" + "strconv" + + "member/gen_result/pb/member" + "member/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" ` +} + +// BindAccount 綁定帳號 -> account bind to UID +func (l *BindAccountLogic) BindAccount(in *member.BindingUserReq) (*member.Response, error) { + // 驗證資料 + err := required.ValidateAll(l.svcCtx.Validate, &bindLoginUserReq{ + Account: in.GetLoginId(), + }) + if err != nil { + return nil, ers.InvalidFormat(err.Error()) + } + + uid := in.GetUid() + // 有UID 綁看看,沒帶UID 近來,確認沒重複就直接綁一個給他 + if in.GetUid() == "" { + uid = strconv.FormatInt(int64(l.svcCtx.SnackFlowGen.Generate()), 10) + } + // 先確定有這個Account + _, err = l.svcCtx.AccountModel.FindOneByAccount(l.ctx, in.GetLoginId()) + if err != nil { + return nil, ers.ResourceNotFound(fmt.Sprintf("failed to get account : %s ", in.GetLoginId())) + } + + _, err = l.svcCtx.AccountToUidModel.Insert(l.ctx, &model.AccountToUid{ + Account: in.LoginId, + Uid: uid, + }) + if err != nil { + return nil, ers.DBError(err.Error()) + } + + return &member.Response{ + Status: &member.BaseResp{ + Code: domain.CodeOk.ToString(), + Message: "success", + Error: "", + }, + }, nil +} diff --git a/internal/logic/bind_user_info_logic.go b/internal/logic/bind_user_info_logic.go new file mode 100644 index 0000000..ac5f39d --- /dev/null +++ b/internal/logic/bind_user_info_logic.go @@ -0,0 +1,31 @@ +package logic + +import ( + "context" + + "member/gen_result/pb/member" + "member/internal/svc" + + "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), + } +} + +// BindUserInfo 初次,綁定 User Info +func (l *BindUserInfoLogic) BindUserInfo(in *member.CreateUserInfoReq) (*member.Response, error) { + // todo: add your logic here and delete this line + + return &member.Response{}, nil +} diff --git a/internal/logic/create_user_account_logic.go b/internal/logic/create_user_account_logic.go new file mode 100644 index 0000000..0b91070 --- /dev/null +++ b/internal/logic/create_user_account_logic.go @@ -0,0 +1,76 @@ +package logic + +import ( + "context" + "errors" + "fmt" + "github.com/go-sql-driver/mysql" + "github.com/zeromicro/go-zero/core/logx" + "member/gen_result/pb/member" + ers "member/internal/lib/error" + "member/internal/lib/required" + "member/internal/model" + "member/internal/svc" + "member/internal/utils" + "time" +) + +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), + } +} + +type createLoginUserReq struct { + LoginId string `json:"login_id" validate:"account"` + Platform int64 `json:"platform" validate:"required,oneof=1 2 3"` + Token string `json:"token" validate:"required"` +} + +// CreateUserAccount 建立帳號與密碼 -> 可登入,但可不可以做其他事情看業務流程,也可以只註冊就好 +func (l *CreateUserAccountLogic) CreateUserAccount(in *member.CreateLoginUserReq) (*member.Response, error) { + // 驗證資料 + err := required.ValidateAll(l.svcCtx.Validate, &createLoginUserReq{ + LoginId: in.GetLoginId(), + Platform: in.GetPlatform(), + Token: in.GetToken(), + }) + if err != nil { + return nil, ers.InvalidFormat(err.Error()) + } + + token, err := utils.HashPassword(in.GetToken(), l.svcCtx.Config.Bcrypt.Cost) + if err != nil { + return nil, ers.ArkInternal(fmt.Sprintf("failed to encrypt err: %v", err.Error())) + } + + // 新增進去 + now := time.Now().UTC().Unix() + _, err = l.svcCtx.AccountModel.Insert(l.ctx, &model.Account{ + Account: in.LoginId, + Token: token, + Platform: in.Platform, + CreateTime: now, + UpdateTime: now, + }) + if err != nil { + // 新增進去 + var mysqlErr *mysql.MySQLError + if errors.As(err, &mysqlErr) && mysqlErr.Number == 1062 { + // 處理重複條目錯誤 + return nil, ers.DBDuplicate(in.LoginId) + } + + return nil, ers.DBError(err.Error()) + } + + return nil, nil +} diff --git a/internal/logic/generate_refresh_code_logic.go b/internal/logic/generate_refresh_code_logic.go new file mode 100644 index 0000000..2d263a2 --- /dev/null +++ b/internal/logic/generate_refresh_code_logic.go @@ -0,0 +1,31 @@ +package logic + +import ( + "context" + + "member/gen_result/pb/member" + "member/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/get_uid_by_account_logic.go b/internal/logic/get_uid_by_account_logic.go new file mode 100644 index 0000000..68eab23 --- /dev/null +++ b/internal/logic/get_uid_by_account_logic.go @@ -0,0 +1,31 @@ +package logic + +import ( + "context" + + "member/gen_result/pb/member" + "member/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/get_user_account_info_logic.go b/internal/logic/get_user_account_info_logic.go new file mode 100644 index 0000000..660db18 --- /dev/null +++ b/internal/logic/get_user_account_info_logic.go @@ -0,0 +1,58 @@ +package logic + +import ( + "context" + "member/gen_result/pb/member" + "member/internal/domain" + ers "member/internal/lib/error" + "member/internal/lib/required" + "member/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), + } +} + +type getUserAccountReq struct { + LoginId string `json:"login_id" validate:"account"` +} + +// GetUserAccountInfo 取得帳號密碼資料 +func (l *GetUserAccountInfoLogic) GetUserAccountInfo(in *member.GetUIDByAccountReq) (*member.GetAccountInfoResp, error) { + // 驗證輸入資料 + err := required.ValidateAll(l.svcCtx.Validate, &getUserAccountReq{ + LoginId: in.GetAccount(), + }) + if err != nil { + return nil, ers.InvalidFormat(err.Error()) + } + + account, err := l.svcCtx.AccountModel.FindOneByAccount(l.ctx, in.GetAccount()) + if err != nil { + + return nil, ers.DBError(err.Error()) + } + + return &member.GetAccountInfoResp{ + Status: &member.BaseResp{ + Code: domain.CodeOk.ToString(), + }, + Data: &member.CreateLoginUserReq{ + LoginId: account.Account, + Token: account.Token, + Platform: account.Platform, + }, + }, nil +} diff --git a/internal/logic/get_user_info_logic.go b/internal/logic/get_user_info_logic.go new file mode 100644 index 0000000..0f56cfc --- /dev/null +++ b/internal/logic/get_user_info_logic.go @@ -0,0 +1,31 @@ +package logic + +import ( + "context" + + "member/gen_result/pb/member" + "member/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), + } +} + +// UpdateStatus 取得會員資訊 +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/list_member_logic.go b/internal/logic/list_member_logic.go new file mode 100644 index 0000000..9cf98c3 --- /dev/null +++ b/internal/logic/list_member_logic.go @@ -0,0 +1,31 @@ +package logic + +import ( + "context" + + "member/gen_result/pb/member" + "member/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/update_status_logic.go b/internal/logic/update_status_logic.go new file mode 100644 index 0000000..c71d5c2 --- /dev/null +++ b/internal/logic/update_status_logic.go @@ -0,0 +1,31 @@ +package logic + +import ( + "context" + + "member/gen_result/pb/member" + "member/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.Response, error) { + // todo: add your logic here and delete this line + + return &member.Response{}, nil +} diff --git a/internal/logic/update_user_info_logic.go b/internal/logic/update_user_info_logic.go new file mode 100644 index 0000000..9d93691 --- /dev/null +++ b/internal/logic/update_user_info_logic.go @@ -0,0 +1,31 @@ +package logic + +import ( + "context" + + "member/gen_result/pb/member" + "member/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.Response, error) { + // todo: add your logic here and delete this line + + return &member.Response{}, nil +} diff --git a/internal/logic/update_user_token_logic.go b/internal/logic/update_user_token_logic.go new file mode 100644 index 0000000..d6c6635 --- /dev/null +++ b/internal/logic/update_user_token_logic.go @@ -0,0 +1,31 @@ +package logic + +import ( + "context" + + "member/gen_result/pb/member" + "member/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.Response, error) { + // todo: add your logic here and delete this line + + return &member.Response{}, nil +} diff --git a/internal/logic/verify_refresh_code_logic.go b/internal/logic/verify_refresh_code_logic.go new file mode 100644 index 0000000..a316a82 --- /dev/null +++ b/internal/logic/verify_refresh_code_logic.go @@ -0,0 +1,31 @@ +package logic + +import ( + "context" + + "member/gen_result/pb/member" + "member/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.Response, error) { + // todo: add your logic here and delete this line + + return &member.Response{}, nil +} diff --git a/internal/model/account_model.go b/internal/model/account_model.go new file mode 100755 index 0000000..958d6dc --- /dev/null +++ b/internal/model/account_model.go @@ -0,0 +1,27 @@ +package model + +import ( + "github.com/zeromicro/go-zero/core/stores/cache" + "github.com/zeromicro/go-zero/core/stores/sqlx" +) + +var _ AccountModel = (*customAccountModel)(nil) + +type ( + // AccountModel is an interface to be customized, add more methods here, + // and implement the added methods in customAccountModel. + AccountModel interface { + accountModel + } + + customAccountModel struct { + *defaultAccountModel + } +) + +// NewAccountModel returns a model for the database table. +func NewAccountModel(conn sqlx.SqlConn, c cache.CacheConf, opts ...cache.Option) AccountModel { + return &customAccountModel{ + defaultAccountModel: newAccountModel(conn, c, opts...), + } +} diff --git a/internal/model/account_model_gen.go b/internal/model/account_model_gen.go new file mode 100755 index 0000000..97c22bf --- /dev/null +++ b/internal/model/account_model_gen.go @@ -0,0 +1,154 @@ +// Code generated by goctl. DO NOT EDIT. + +package model + +import ( + "context" + "database/sql" + "fmt" + "strings" + + "github.com/zeromicro/go-zero/core/stores/builder" + "github.com/zeromicro/go-zero/core/stores/cache" + "github.com/zeromicro/go-zero/core/stores/sqlc" + "github.com/zeromicro/go-zero/core/stores/sqlx" + "github.com/zeromicro/go-zero/core/stringx" +) + +var ( + accountFieldNames = builder.RawFieldNames(&Account{}) + accountRows = strings.Join(accountFieldNames, ",") + accountRowsExpectAutoSet = strings.Join(stringx.Remove(accountFieldNames, "`id`"), ",") + accountRowsWithPlaceHolder = strings.Join(stringx.Remove(accountFieldNames, "`id`"), "=?,") + "=?" + + cacheAccountIdPrefix = "cache:account:id:" + cacheAccountAccountPrefix = "cache:account:account:" +) + +type ( + accountModel interface { + Insert(ctx context.Context, data *Account) (sql.Result, error) + FindOne(ctx context.Context, id int64) (*Account, error) + FindOneByAccount(ctx context.Context, account string) (*Account, error) + Update(ctx context.Context, data *Account) error + Delete(ctx context.Context, id int64) error + } + + defaultAccountModel struct { + sqlc.CachedConn + table string + } + + Account struct { + Id int64 `db:"id"` + Account string `db:"account"` + Token string `db:"token"` + Platform int64 `db:"platform"` // 平台類型 1. ark 2. google + CreateTime int64 `db:"create_time"` + UpdateTime int64 `db:"update_time"` + } +) + +func newAccountModel(conn sqlx.SqlConn, c cache.CacheConf, opts ...cache.Option) *defaultAccountModel { + return &defaultAccountModel{ + CachedConn: sqlc.NewConn(conn, c, opts...), + table: "`account`", + } +} + +func (m *defaultAccountModel) withSession(session sqlx.Session) *defaultAccountModel { + return &defaultAccountModel{ + CachedConn: m.CachedConn.WithSession(session), + table: "`account`", + } +} + +func (m *defaultAccountModel) Delete(ctx context.Context, id int64) error { + data, err := m.FindOne(ctx, id) + if err != nil { + return err + } + + accountAccountKey := fmt.Sprintf("%s%v", cacheAccountAccountPrefix, data.Account) + accountIdKey := fmt.Sprintf("%s%v", cacheAccountIdPrefix, id) + _, err = m.ExecCtx(ctx, func(ctx context.Context, conn sqlx.SqlConn) (result sql.Result, err error) { + query := fmt.Sprintf("delete from %s where `id` = ?", m.table) + return conn.ExecCtx(ctx, query, id) + }, accountAccountKey, accountIdKey) + return err +} + +func (m *defaultAccountModel) FindOne(ctx context.Context, id int64) (*Account, error) { + accountIdKey := fmt.Sprintf("%s%v", cacheAccountIdPrefix, id) + var resp Account + err := m.QueryRowCtx(ctx, &resp, accountIdKey, func(ctx context.Context, conn sqlx.SqlConn, v any) error { + query := fmt.Sprintf("select %s from %s where `id` = ? limit 1", accountRows, m.table) + return conn.QueryRowCtx(ctx, v, query, id) + }) + switch err { + case nil: + return &resp, nil + case sqlc.ErrNotFound: + return nil, ErrNotFound + default: + return nil, err + } +} + +func (m *defaultAccountModel) FindOneByAccount(ctx context.Context, account string) (*Account, error) { + accountAccountKey := fmt.Sprintf("%s%v", cacheAccountAccountPrefix, account) + var resp Account + err := m.QueryRowIndexCtx(ctx, &resp, accountAccountKey, m.formatPrimary, func(ctx context.Context, conn sqlx.SqlConn, v any) (i any, e error) { + query := fmt.Sprintf("select %s from %s where `account` = ? limit 1", accountRows, m.table) + if err := conn.QueryRowCtx(ctx, &resp, query, account); err != nil { + return nil, err + } + return resp.Id, nil + }, m.queryPrimary) + switch err { + case nil: + return &resp, nil + case sqlc.ErrNotFound: + return nil, ErrNotFound + default: + return nil, err + } +} + +func (m *defaultAccountModel) Insert(ctx context.Context, data *Account) (sql.Result, error) { + accountAccountKey := fmt.Sprintf("%s%v", cacheAccountAccountPrefix, data.Account) + accountIdKey := fmt.Sprintf("%s%v", cacheAccountIdPrefix, data.Id) + ret, err := m.ExecCtx(ctx, func(ctx context.Context, conn sqlx.SqlConn) (result sql.Result, err error) { + query := fmt.Sprintf("insert into %s (%s) values (?, ?, ?, ?, ?)", m.table, accountRowsExpectAutoSet) + return conn.ExecCtx(ctx, query, data.Account, data.Token, data.Platform, data.CreateTime, data.UpdateTime) + }, accountAccountKey, accountIdKey) + return ret, err +} + +func (m *defaultAccountModel) Update(ctx context.Context, newData *Account) error { + data, err := m.FindOne(ctx, newData.Id) + if err != nil { + return err + } + + accountAccountKey := fmt.Sprintf("%s%v", cacheAccountAccountPrefix, data.Account) + accountIdKey := fmt.Sprintf("%s%v", cacheAccountIdPrefix, data.Id) + _, err = m.ExecCtx(ctx, func(ctx context.Context, conn sqlx.SqlConn) (result sql.Result, err error) { + query := fmt.Sprintf("update %s set %s where `id` = ?", m.table, accountRowsWithPlaceHolder) + return conn.ExecCtx(ctx, query, newData.Account, newData.Token, newData.Platform, newData.CreateTime, newData.UpdateTime, newData.Id) + }, accountAccountKey, accountIdKey) + return err +} + +func (m *defaultAccountModel) formatPrimary(primary any) string { + return fmt.Sprintf("%s%v", cacheAccountIdPrefix, primary) +} + +func (m *defaultAccountModel) queryPrimary(ctx context.Context, conn sqlx.SqlConn, v, primary any) error { + query := fmt.Sprintf("select %s from %s where `id` = ? limit 1", accountRows, m.table) + return conn.QueryRowCtx(ctx, v, query, primary) +} + +func (m *defaultAccountModel) tableName() string { + return m.table +} diff --git a/internal/model/account_to_uid_model.go b/internal/model/account_to_uid_model.go new file mode 100755 index 0000000..694526e --- /dev/null +++ b/internal/model/account_to_uid_model.go @@ -0,0 +1,31 @@ +package model + +import ( + "github.com/zeromicro/go-zero/core/stores/cache" + "github.com/zeromicro/go-zero/core/stores/sqlx" +) + +var _ AccountToUidModel = (*customAccountToUidModel)(nil) + +var ( + cacheAccountPrefix = "cache:accountToUid:account:" +) + +type ( + // AccountToUidModel is an interface to be customized, add more methods here, + // and implement the added methods in customAccountToUidModel. + AccountToUidModel interface { + accountToUidModel + } + + customAccountToUidModel struct { + *defaultAccountToUidModel + } +) + +// NewAccountToUidModel returns a model for the database table. +func NewAccountToUidModel(conn sqlx.SqlConn, c cache.CacheConf, opts ...cache.Option) AccountToUidModel { + return &customAccountToUidModel{ + defaultAccountToUidModel: newAccountToUidModel(conn, c, opts...), + } +} diff --git a/internal/model/account_to_uid_model_gen.go b/internal/model/account_to_uid_model_gen.go new file mode 100755 index 0000000..2bf4551 --- /dev/null +++ b/internal/model/account_to_uid_model_gen.go @@ -0,0 +1,151 @@ +// Code generated by goctl. DO NOT EDIT. + +package model + +import ( + "context" + "database/sql" + "fmt" + "strings" + + "github.com/zeromicro/go-zero/core/stores/builder" + "github.com/zeromicro/go-zero/core/stores/cache" + "github.com/zeromicro/go-zero/core/stores/sqlc" + "github.com/zeromicro/go-zero/core/stores/sqlx" + "github.com/zeromicro/go-zero/core/stringx" +) + +var ( + accountToUidFieldNames = builder.RawFieldNames(&AccountToUid{}) + accountToUidRows = strings.Join(accountToUidFieldNames, ",") + accountToUidRowsExpectAutoSet = strings.Join(stringx.Remove(accountToUidFieldNames, "`id`"), ",") + accountToUidRowsWithPlaceHolder = strings.Join(stringx.Remove(accountToUidFieldNames, "`id`"), "=?,") + "=?" + + cacheAccountToUidIdPrefix = "cache:accountToUid:id:" + cacheAccountToUidAccountPrefix = "cache:accountToUid:account:" +) + +type ( + accountToUidModel interface { + Insert(ctx context.Context, data *AccountToUid) (sql.Result, error) + FindOne(ctx context.Context, id int64) (*AccountToUid, error) + FindOneByAccount(ctx context.Context, account string) (*AccountToUid, error) + Update(ctx context.Context, data *AccountToUid) error + Delete(ctx context.Context, id int64) error + } + + defaultAccountToUidModel struct { + sqlc.CachedConn + table string + } + + AccountToUid struct { + Id int64 `db:"id"` + Account string `db:"account"` + Uid string `db:"uid"` + } +) + +func newAccountToUidModel(conn sqlx.SqlConn, c cache.CacheConf, opts ...cache.Option) *defaultAccountToUidModel { + return &defaultAccountToUidModel{ + CachedConn: sqlc.NewConn(conn, c, opts...), + table: "`account_to_uid`", + } +} + +func (m *defaultAccountToUidModel) withSession(session sqlx.Session) *defaultAccountToUidModel { + return &defaultAccountToUidModel{ + CachedConn: m.CachedConn.WithSession(session), + table: "`account_to_uid`", + } +} + +func (m *defaultAccountToUidModel) Delete(ctx context.Context, id int64) error { + data, err := m.FindOne(ctx, id) + if err != nil { + return err + } + + accountToUidAccountKey := fmt.Sprintf("%s%v", cacheAccountToUidAccountPrefix, data.Account) + accountToUidIdKey := fmt.Sprintf("%s%v", cacheAccountToUidIdPrefix, id) + _, err = m.ExecCtx(ctx, func(ctx context.Context, conn sqlx.SqlConn) (result sql.Result, err error) { + query := fmt.Sprintf("delete from %s where `id` = ?", m.table) + return conn.ExecCtx(ctx, query, id) + }, accountToUidAccountKey, accountToUidIdKey) + return err +} + +func (m *defaultAccountToUidModel) FindOne(ctx context.Context, id int64) (*AccountToUid, error) { + accountToUidIdKey := fmt.Sprintf("%s%v", cacheAccountToUidIdPrefix, id) + var resp AccountToUid + err := m.QueryRowCtx(ctx, &resp, accountToUidIdKey, func(ctx context.Context, conn sqlx.SqlConn, v any) error { + query := fmt.Sprintf("select %s from %s where `id` = ? limit 1", accountToUidRows, m.table) + return conn.QueryRowCtx(ctx, v, query, id) + }) + switch err { + case nil: + return &resp, nil + case sqlc.ErrNotFound: + return nil, ErrNotFound + default: + return nil, err + } +} + +func (m *defaultAccountToUidModel) FindOneByAccount(ctx context.Context, account string) (*AccountToUid, error) { + accountToUidAccountKey := fmt.Sprintf("%s%v", cacheAccountToUidAccountPrefix, account) + var resp AccountToUid + err := m.QueryRowIndexCtx(ctx, &resp, accountToUidAccountKey, m.formatPrimary, func(ctx context.Context, conn sqlx.SqlConn, v any) (i any, e error) { + query := fmt.Sprintf("select %s from %s where `account` = ? limit 1", accountToUidRows, m.table) + if err := conn.QueryRowCtx(ctx, &resp, query, account); err != nil { + return nil, err + } + return resp.Id, nil + }, m.queryPrimary) + switch err { + case nil: + return &resp, nil + case sqlc.ErrNotFound: + return nil, ErrNotFound + default: + return nil, err + } +} + +func (m *defaultAccountToUidModel) Insert(ctx context.Context, data *AccountToUid) (sql.Result, error) { + accountToUidAccountKey := fmt.Sprintf("%s%v", cacheAccountToUidAccountPrefix, data.Account) + accountToUidIdKey := fmt.Sprintf("%s%v", cacheAccountToUidIdPrefix, data.Id) + ret, err := m.ExecCtx(ctx, func(ctx context.Context, conn sqlx.SqlConn) (result sql.Result, err error) { + query := fmt.Sprintf("insert into %s (%s) values (?, ?)", m.table, accountToUidRowsExpectAutoSet) + return conn.ExecCtx(ctx, query, data.Account, data.Uid) + }, accountToUidAccountKey, accountToUidIdKey) + return ret, err +} + +func (m *defaultAccountToUidModel) Update(ctx context.Context, newData *AccountToUid) error { + data, err := m.FindOne(ctx, newData.Id) + if err != nil { + return err + } + + accountToUidAccountKey := fmt.Sprintf("%s%v", cacheAccountToUidAccountPrefix, data.Account) + accountToUidIdKey := fmt.Sprintf("%s%v", cacheAccountToUidIdPrefix, data.Id) + _, err = m.ExecCtx(ctx, func(ctx context.Context, conn sqlx.SqlConn) (result sql.Result, err error) { + query := fmt.Sprintf("update %s set %s where `id` = ?", m.table, accountToUidRowsWithPlaceHolder) + return conn.ExecCtx(ctx, query, newData.Account, newData.Uid, newData.Id) + }, accountToUidAccountKey, accountToUidIdKey) + return err +} + +func (m *defaultAccountToUidModel) formatPrimary(primary any) string { + return fmt.Sprintf("%s%v", cacheAccountToUidIdPrefix, primary) +} + +func (m *defaultAccountToUidModel) queryPrimary(ctx context.Context, conn sqlx.SqlConn, v, primary any) error { + query := fmt.Sprintf("select %s from %s where `id` = ? limit 1", accountToUidRows, m.table) + return conn.QueryRowCtx(ctx, v, query, primary) +} + +func (m *defaultAccountToUidModel) tableName() string { + return m.table +} diff --git a/internal/model/machine_node_model.go b/internal/model/machine_node_model.go new file mode 100755 index 0000000..86b044e --- /dev/null +++ b/internal/model/machine_node_model.go @@ -0,0 +1,48 @@ +package model + +import ( + "context" + "fmt" + "github.com/zeromicro/go-zero/core/stores/cache" + "github.com/zeromicro/go-zero/core/stores/sqlc" + "github.com/zeromicro/go-zero/core/stores/sqlx" +) + +var _ MachineNodeModel = (*customMachineNodeModel)(nil) + +type ( + // MachineNodeModel is an interface to be customized, add more methods here, + // and implement the added methods in customMachineNodeModel. + MachineNodeModel interface { + machineNodeModel + FindOneByHostName(ctx context.Context, hostName string) (*MachineNode, error) + } + + customMachineNodeModel struct { + *defaultMachineNodeModel + } +) + +// NewMachineNodeModel returns a model for the database table. +func NewMachineNodeModel(conn sqlx.SqlConn, c cache.CacheConf, opts ...cache.Option) MachineNodeModel { + return &customMachineNodeModel{ + defaultMachineNodeModel: newMachineNodeModel(conn, c, opts...), + } +} + +func (m *defaultMachineNodeModel) FindOneByHostName(ctx context.Context, hostName string) (*MachineNode, error) { + machineNodeIdKey := fmt.Sprintf("%s%v", cacheMachineNodeIdPrefix, hostName) + var resp MachineNode + err := m.QueryRowCtx(ctx, &resp, machineNodeIdKey, func(ctx context.Context, conn sqlx.SqlConn, v any) error { + query := fmt.Sprintf("select %s from %s where `host_name` = ? limit 1", machineNodeRows, m.table) + return conn.QueryRowCtx(ctx, v, query, hostName) + }) + switch err { + case nil: + return &resp, nil + case sqlc.ErrNotFound: + return nil, ErrNotFound + default: + return nil, err + } +} diff --git a/internal/model/machine_node_model_gen.go b/internal/model/machine_node_model_gen.go new file mode 100755 index 0000000..37d29b5 --- /dev/null +++ b/internal/model/machine_node_model_gen.go @@ -0,0 +1,117 @@ +// Code generated by goctl. DO NOT EDIT. + +package model + +import ( + "context" + "database/sql" + "fmt" + "strings" + + "github.com/zeromicro/go-zero/core/stores/builder" + "github.com/zeromicro/go-zero/core/stores/cache" + "github.com/zeromicro/go-zero/core/stores/sqlc" + "github.com/zeromicro/go-zero/core/stores/sqlx" + "github.com/zeromicro/go-zero/core/stringx" +) + +var ( + machineNodeFieldNames = builder.RawFieldNames(&MachineNode{}) + machineNodeRows = strings.Join(machineNodeFieldNames, ",") + machineNodeRowsExpectAutoSet = strings.Join(stringx.Remove(machineNodeFieldNames, "`id`"), ",") + machineNodeRowsWithPlaceHolder = strings.Join(stringx.Remove(machineNodeFieldNames, "`id`"), "=?,") + "=?" + + cacheMachineNodeIdPrefix = "cache:machineNode:id:" +) + +type ( + machineNodeModel interface { + Insert(ctx context.Context, data *MachineNode) (sql.Result, error) + FindOne(ctx context.Context, id int64) (*MachineNode, error) + Update(ctx context.Context, data *MachineNode) error + Delete(ctx context.Context, id int64) error + } + + defaultMachineNodeModel struct { + sqlc.CachedConn + table string + } + + MachineNode struct { + Id int64 `db:"id"` // 流水號 + CreateTime int64 `db:"create_time"` // 創建時間 + UpdateTime int64 `db:"update_time"` // 更新時間 + HostName string `db:"host_name"` // host name + } +) + +func newMachineNodeModel(conn sqlx.SqlConn, c cache.CacheConf, opts ...cache.Option) *defaultMachineNodeModel { + return &defaultMachineNodeModel{ + CachedConn: sqlc.NewConn(conn, c, opts...), + table: "`machine_node`", + } +} + +func (m *defaultMachineNodeModel) withSession(session sqlx.Session) *defaultMachineNodeModel { + return &defaultMachineNodeModel{ + CachedConn: m.CachedConn.WithSession(session), + table: "`machine_node`", + } +} + +func (m *defaultMachineNodeModel) Delete(ctx context.Context, id int64) error { + machineNodeIdKey := fmt.Sprintf("%s%v", cacheMachineNodeIdPrefix, id) + _, err := m.ExecCtx(ctx, func(ctx context.Context, conn sqlx.SqlConn) (result sql.Result, err error) { + query := fmt.Sprintf("delete from %s where `id` = ?", m.table) + return conn.ExecCtx(ctx, query, id) + }, machineNodeIdKey) + return err +} + +func (m *defaultMachineNodeModel) FindOne(ctx context.Context, id int64) (*MachineNode, error) { + machineNodeIdKey := fmt.Sprintf("%s%v", cacheMachineNodeIdPrefix, id) + var resp MachineNode + err := m.QueryRowCtx(ctx, &resp, machineNodeIdKey, func(ctx context.Context, conn sqlx.SqlConn, v any) error { + query := fmt.Sprintf("select %s from %s where `id` = ? limit 1", machineNodeRows, m.table) + return conn.QueryRowCtx(ctx, v, query, id) + }) + switch err { + case nil: + return &resp, nil + case sqlc.ErrNotFound: + return nil, ErrNotFound + default: + return nil, err + } +} + +func (m *defaultMachineNodeModel) Insert(ctx context.Context, data *MachineNode) (sql.Result, error) { + machineNodeIdKey := fmt.Sprintf("%s%v", cacheMachineNodeIdPrefix, data.Id) + ret, err := m.ExecCtx(ctx, func(ctx context.Context, conn sqlx.SqlConn) (result sql.Result, err error) { + query := fmt.Sprintf("insert into %s (%s) values (?, ?, ?)", m.table, machineNodeRowsExpectAutoSet) + return conn.ExecCtx(ctx, query, data.CreateTime, data.UpdateTime, data.HostName) + }, machineNodeIdKey) + return ret, err +} + +func (m *defaultMachineNodeModel) Update(ctx context.Context, data *MachineNode) error { + machineNodeIdKey := fmt.Sprintf("%s%v", cacheMachineNodeIdPrefix, data.Id) + _, err := m.ExecCtx(ctx, func(ctx context.Context, conn sqlx.SqlConn) (result sql.Result, err error) { + query := fmt.Sprintf("update %s set %s where `id` = ?", m.table, machineNodeRowsWithPlaceHolder) + return conn.ExecCtx(ctx, query, data.CreateTime, data.UpdateTime, data.HostName, data.Id) + }, machineNodeIdKey) + return err +} + +func (m *defaultMachineNodeModel) formatPrimary(primary any) string { + return fmt.Sprintf("%s%v", cacheMachineNodeIdPrefix, primary) +} + +func (m *defaultMachineNodeModel) queryPrimary(ctx context.Context, conn sqlx.SqlConn, v, primary any) error { + query := fmt.Sprintf("select %s from %s where `id` = ? limit 1", machineNodeRows, m.table) + return conn.QueryRowCtx(ctx, v, query, primary) +} + +func (m *defaultMachineNodeModel) tableName() string { + return m.table +} diff --git a/internal/model/user_table_model.go b/internal/model/user_table_model.go new file mode 100755 index 0000000..13f2b1d --- /dev/null +++ b/internal/model/user_table_model.go @@ -0,0 +1,27 @@ +package model + +import ( + "github.com/zeromicro/go-zero/core/stores/cache" + "github.com/zeromicro/go-zero/core/stores/sqlx" +) + +var _ UserTableModel = (*customUserTableModel)(nil) + +type ( + // UserTableModel is an interface to be customized, add more methods here, + // and implement the added methods in customUserTableModel. + UserTableModel interface { + userTableModel + } + + customUserTableModel struct { + *defaultUserTableModel + } +) + +// NewUserTableModel returns a model for the database table. +func NewUserTableModel(conn sqlx.SqlConn, c cache.CacheConf, opts ...cache.Option) UserTableModel { + return &customUserTableModel{ + defaultUserTableModel: newUserTableModel(conn, c, opts...), + } +} diff --git a/internal/model/user_table_model_gen.go b/internal/model/user_table_model_gen.go new file mode 100755 index 0000000..7ee1f9b --- /dev/null +++ b/internal/model/user_table_model_gen.go @@ -0,0 +1,161 @@ +// Code generated by goctl. DO NOT EDIT. + +package model + +import ( + "context" + "database/sql" + "fmt" + "strings" + + "github.com/zeromicro/go-zero/core/stores/builder" + "github.com/zeromicro/go-zero/core/stores/cache" + "github.com/zeromicro/go-zero/core/stores/sqlc" + "github.com/zeromicro/go-zero/core/stores/sqlx" + "github.com/zeromicro/go-zero/core/stringx" +) + +var ( + userTableFieldNames = builder.RawFieldNames(&UserTable{}) + userTableRows = strings.Join(userTableFieldNames, ",") + userTableRowsExpectAutoSet = strings.Join(stringx.Remove(userTableFieldNames, "`id`", "`create_at`", "`create_time`", "`created_at`", "`update_at`", "`update_time`", "`updated_at`"), ",") + userTableRowsWithPlaceHolder = strings.Join(stringx.Remove(userTableFieldNames, "`id`", "`create_at`", "`create_time`", "`created_at`", "`update_at`", "`update_time`", "`updated_at`"), "=?,") + "=?" + + cacheUserTableIdPrefix = "cache:userTable:id:" + cacheUserTableUidPrefix = "cache:userTable:uid:" +) + +type ( + userTableModel interface { + Insert(ctx context.Context, data *UserTable) (sql.Result, error) + FindOne(ctx context.Context, id int64) (*UserTable, error) + FindOneByUid(ctx context.Context, uid string) (*UserTable, error) + Update(ctx context.Context, data *UserTable) error + Delete(ctx context.Context, id int64) error + } + + defaultUserTableModel struct { + sqlc.CachedConn + table string + } + + UserTable struct { + Id int64 `db:"id"` + VerifyType int64 `db:"verify_type"` // 驗證類型 0. 異常 1.信箱 2.手機 3. GA 4.不驗證 + AlarmType int64 `db:"alarm_type"` // 告警狀態 0. 異常 1. 正常(未告警) 2.系統告警中 + Status int64 `db:"status"` // 會員狀態 0. 異常 1. 尚未驗證 2. 啟用 3. 停權中 4. 信箱以驗證 5. 手機以驗證 6. GA 以驗證 + Uid string `db:"uid"` + RoleId string `db:"role_id"` + Language string `db:"language"` + Currency string `db:"currency"` + NickName string `db:"nick_name"` + Gender int64 `db:"gender"` // 0. 不願透露, 1 男 2 女 + Birthday int64 `db:"birthday"` + CreateTime int64 `db:"create_time"` + UpdateTime int64 `db:"update_time"` + } +) + +func newUserTableModel(conn sqlx.SqlConn, c cache.CacheConf, opts ...cache.Option) *defaultUserTableModel { + return &defaultUserTableModel{ + CachedConn: sqlc.NewConn(conn, c, opts...), + table: "`user_table`", + } +} + +func (m *defaultUserTableModel) withSession(session sqlx.Session) *defaultUserTableModel { + return &defaultUserTableModel{ + CachedConn: m.CachedConn.WithSession(session), + table: "`user_table`", + } +} + +func (m *defaultUserTableModel) Delete(ctx context.Context, id int64) error { + data, err := m.FindOne(ctx, id) + if err != nil { + return err + } + + userTableIdKey := fmt.Sprintf("%s%v", cacheUserTableIdPrefix, id) + userTableUidKey := fmt.Sprintf("%s%v", cacheUserTableUidPrefix, data.Uid) + _, err = m.ExecCtx(ctx, func(ctx context.Context, conn sqlx.SqlConn) (result sql.Result, err error) { + query := fmt.Sprintf("delete from %s where `id` = ?", m.table) + return conn.ExecCtx(ctx, query, id) + }, userTableIdKey, userTableUidKey) + return err +} + +func (m *defaultUserTableModel) FindOne(ctx context.Context, id int64) (*UserTable, error) { + userTableIdKey := fmt.Sprintf("%s%v", cacheUserTableIdPrefix, id) + var resp UserTable + err := m.QueryRowCtx(ctx, &resp, userTableIdKey, func(ctx context.Context, conn sqlx.SqlConn, v any) error { + query := fmt.Sprintf("select %s from %s where `id` = ? limit 1", userTableRows, m.table) + return conn.QueryRowCtx(ctx, v, query, id) + }) + switch err { + case nil: + return &resp, nil + case sqlc.ErrNotFound: + return nil, ErrNotFound + default: + return nil, err + } +} + +func (m *defaultUserTableModel) FindOneByUid(ctx context.Context, uid string) (*UserTable, error) { + userTableUidKey := fmt.Sprintf("%s%v", cacheUserTableUidPrefix, uid) + var resp UserTable + err := m.QueryRowIndexCtx(ctx, &resp, userTableUidKey, m.formatPrimary, func(ctx context.Context, conn sqlx.SqlConn, v any) (i any, e error) { + query := fmt.Sprintf("select %s from %s where `uid` = ? limit 1", userTableRows, m.table) + if err := conn.QueryRowCtx(ctx, &resp, query, uid); err != nil { + return nil, err + } + return resp.Id, nil + }, m.queryPrimary) + switch err { + case nil: + return &resp, nil + case sqlc.ErrNotFound: + return nil, ErrNotFound + default: + return nil, err + } +} + +func (m *defaultUserTableModel) Insert(ctx context.Context, data *UserTable) (sql.Result, error) { + userTableIdKey := fmt.Sprintf("%s%v", cacheUserTableIdPrefix, data.Id) + userTableUidKey := fmt.Sprintf("%s%v", cacheUserTableUidPrefix, data.Uid) + ret, err := m.ExecCtx(ctx, func(ctx context.Context, conn sqlx.SqlConn) (result sql.Result, err error) { + query := fmt.Sprintf("insert into %s (%s) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", m.table, userTableRowsExpectAutoSet) + return conn.ExecCtx(ctx, query, data.VerifyType, data.AlarmType, data.Status, data.Uid, data.RoleId, data.Language, data.Currency, data.NickName, data.Gender, data.Birthday) + }, userTableIdKey, userTableUidKey) + return ret, err +} + +func (m *defaultUserTableModel) Update(ctx context.Context, newData *UserTable) error { + data, err := m.FindOne(ctx, newData.Id) + if err != nil { + return err + } + + userTableIdKey := fmt.Sprintf("%s%v", cacheUserTableIdPrefix, data.Id) + userTableUidKey := fmt.Sprintf("%s%v", cacheUserTableUidPrefix, data.Uid) + _, err = m.ExecCtx(ctx, func(ctx context.Context, conn sqlx.SqlConn) (result sql.Result, err error) { + query := fmt.Sprintf("update %s set %s where `id` = ?", m.table, userTableRowsWithPlaceHolder) + return conn.ExecCtx(ctx, query, newData.VerifyType, newData.AlarmType, newData.Status, newData.Uid, newData.RoleId, newData.Language, newData.Currency, newData.NickName, newData.Gender, newData.Birthday, newData.Id) + }, userTableIdKey, userTableUidKey) + return err +} + +func (m *defaultUserTableModel) formatPrimary(primary any) string { + return fmt.Sprintf("%s%v", cacheUserTableIdPrefix, primary) +} + +func (m *defaultUserTableModel) queryPrimary(ctx context.Context, conn sqlx.SqlConn, v, primary any) error { + query := fmt.Sprintf("select %s from %s where `id` = ? limit 1", userTableRows, m.table) + return conn.QueryRowCtx(ctx, v, query, primary) +} + +func (m *defaultUserTableModel) tableName() string { + return m.table +} diff --git a/internal/model/vars.go b/internal/model/vars.go new file mode 100644 index 0000000..69ca814 --- /dev/null +++ b/internal/model/vars.go @@ -0,0 +1,5 @@ +package model + +import "github.com/zeromicro/go-zero/core/stores/sqlx" + +var ErrNotFound = sqlx.ErrNotFound diff --git a/internal/server/account_server.go b/internal/server/account_server.go new file mode 100644 index 0000000..13badd9 --- /dev/null +++ b/internal/server/account_server.go @@ -0,0 +1,95 @@ +// Code generated by goctl. DO NOT EDIT. +// Source: member.proto + +package server + +import ( + "context" + + "member/gen_result/pb/member" + "member/internal/logic" + "member/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.Response, error) { + l := logic.NewCreateUserAccountLogic(ctx, s.svcCtx) + return l.CreateUserAccount(in) +} + +// GetUserAccountInfo 取得帳號密碼資料 +func (s *AccountServer) GetUserAccountInfo(ctx context.Context, in *member.GetUIDByAccountReq) (*member.GetAccountInfoResp, error) { + l := logic.NewGetUserAccountInfoLogic(ctx, s.svcCtx) + return l.GetUserAccountInfo(in) +} + +// UpdateUserToken 更新密碼 +func (s *AccountServer) UpdateUserToken(ctx context.Context, in *member.UpdateTokenReq) (*member.Response, error) { + l := logic.NewUpdateUserTokenLogic(ctx, s.svcCtx) + return l.UpdateUserToken(in) +} + +// GetUidByAccount 用帳號換取 UID +func (s *AccountServer) GetUidByAccount(ctx context.Context, in *member.GetUIDByAccountReq) (*member.GetUidByAccountResp, error) { + l := logic.NewGetUidByAccountLogic(ctx, s.svcCtx) + return l.GetUidByAccount(in) +} + +// BindAccount 綁定帳號 -> account bind to UID +func (s *AccountServer) BindAccount(ctx context.Context, in *member.BindingUserReq) (*member.Response, error) { + l := logic.NewBindAccountLogic(ctx, s.svcCtx) + return l.BindAccount(in) +} + +// BindUserInfo 初次,綁定 User Info +func (s *AccountServer) BindUserInfo(ctx context.Context, in *member.CreateUserInfoReq) (*member.Response, error) { + l := logic.NewBindUserInfoLogic(ctx, s.svcCtx) + return l.BindUserInfo(in) +} + +// UpdateUserInfo 更新 User Info +func (s *AccountServer) UpdateUserInfo(ctx context.Context, in *member.UpdateUserInfoReq) (*member.Response, error) { + l := logic.NewUpdateUserInfoLogic(ctx, s.svcCtx) + return l.UpdateUserInfo(in) +} + +// UpdateStatus 修改狀態 +func (s *AccountServer) UpdateStatus(ctx context.Context, in *member.UpdateStatusReq) (*member.Response, error) { + l := logic.NewUpdateStatusLogic(ctx, s.svcCtx) + return l.UpdateStatus(in) +} + +// UpdateStatus 取得會員資訊 +func (s *AccountServer) GetUserInfo(ctx context.Context, in *member.GetUserInfoReq) (*member.GetUserInfoResp, error) { + l := logic.NewGetUserInfoLogic(ctx, s.svcCtx) + return l.GetUserInfo(in) +} + +// ListMember 取得會員列表 +func (s *AccountServer) ListMember(ctx context.Context, in *member.ListUserInfoReq) (*member.ListUserInfoResp, error) { + l := logic.NewListMemberLogic(ctx, s.svcCtx) + return l.ListMember(in) +} + +// GenerateRefreshCode 這個帳號驗證碼(十分鐘),通用的 +func (s *AccountServer) GenerateRefreshCode(ctx context.Context, in *member.GenerateRefreshCodeReq) (*member.GenerateRefreshCodeResp, error) { + l := logic.NewGenerateRefreshCodeLogic(ctx, s.svcCtx) + return l.GenerateRefreshCode(in) +} + +// VerifyRefreshCode 驗證忘記密碼 token +func (s *AccountServer) VerifyRefreshCode(ctx context.Context, in *member.VerifyRefreshCodeReq) (*member.Response, error) { + l := logic.NewVerifyRefreshCodeLogic(ctx, s.svcCtx) + return l.VerifyRefreshCode(in) +} diff --git a/internal/svc/machine_node.go b/internal/svc/machine_node.go new file mode 100644 index 0000000..a044bd7 --- /dev/null +++ b/internal/svc/machine_node.go @@ -0,0 +1,66 @@ +package svc + +import ( + "context" + "github.com/bwmarrin/snowflake" + sf "member/internal/lib/snackflow" + "member/internal/model" + "os" + "time" +) + +type machineNode struct { + MachineNodeID int64 `json:"machine_node_id"` +} + +type MachineNodeCreateParams struct { + HostName string +} + +func NewMachineNode(node model.MachineNodeModel) int64 { + ctx := context.Background() + nodeName := os.Getenv("POD_NAME") + if os.Getenv("POD_NAME") == "" { + nodeName = "default_node" + } + + machine, err := node.FindOneByHostName(ctx, nodeName) + if err != nil { + result, err := node.Insert(ctx, &model.MachineNode{ + CreateTime: time.Now().Unix(), + HostName: nodeName, + }) + if err != nil { + return 1 + } + id, err := result.LastInsertId() + if err != nil { + return 1 + } + + return id + } + + return machine.Id +} + +func GetMachineNodeID(machineNodeID int64) int64 { + // Snowflake 公式,工作機器 ID 佔用 10bit,最多容納 1024節點, + // 故用 % 1024 取餘數做 ring + const nodeMax = 1024 + + return machineNodeID % nodeMax +} + +func newSnackFlowNode(node model.MachineNodeModel) (*snowflake.Node, error) { + + nodeID := NewMachineNode(node) + ringNodeID := GetMachineNodeID(nodeID) + s := sf.New(sf.WithMachineNodeID(ringNodeID)) + n, err := s.NewNode() + if err != nil { + return nil, err + } + + return n, nil +} diff --git a/internal/svc/service_context.go b/internal/svc/service_context.go new file mode 100644 index 0000000..db92094 --- /dev/null +++ b/internal/svc/service_context.go @@ -0,0 +1,43 @@ +package svc + +import ( + "github.com/bwmarrin/snowflake" + "member/internal/config" + "member/internal/domain" + "member/internal/lib/required" + "member/internal/model" + + "github.com/go-playground/validator/v10" + "github.com/zeromicro/go-zero/core/stores/sqlx" + ers "member/internal/lib/error" +) + +type ServiceContext struct { + Config config.Config + + Validate *validator.Validate + AccountModel model.AccountModel + UserModel model.UserTableModel + AccountToUidModel model.AccountToUidModel + SnackFlowGen *snowflake.Node +} + +func NewServiceContext(c config.Config) *ServiceContext { + sqlConn := sqlx.NewMysql(c.DB.DsnString) + // 設置 + ers.Scope = domain.Scope + + n, err := newSnackFlowNode(model.NewMachineNodeModel(sqlConn, c.Cache)) + if err != nil { + panic(err) + } + + return &ServiceContext{ + Config: c, + Validate: required.MustValidator(required.WithAccount("account")), + UserModel: model.NewUserTableModel(sqlConn, c.Cache), + AccountToUidModel: model.NewAccountToUidModel(sqlConn, c.Cache), + AccountModel: model.NewAccountModel(sqlConn, c.Cache), + SnackFlowGen: n, + } +} diff --git a/internal/utils/password.go b/internal/utils/password.go new file mode 100644 index 0000000..5f10af9 --- /dev/null +++ b/internal/utils/password.go @@ -0,0 +1,20 @@ +package utils + +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/makefile b/makefile new file mode 100644 index 0000000..b47eb31 --- /dev/null +++ b/makefile @@ -0,0 +1,105 @@ +.PHONY: help test-race lint sec-scan gci-format db-mysql-init docker-image-build db-mysql-down generate + +help: ## show how tot use this tools + @awk 'BEGIN {FS = ":.*?## "} /^[a-zA-Z0-9_-]+:.*?## / {sub("\\\\n",sprintf("\n%22c"," "), $$2);printf "\033[36m%-25s\033[0m %s\n", $$1, $$2}' $(MAKEFILE_LIST) + +PROJECT_NAME?=member + +######## +# test # +######## + +test-race: ## launch all tests with race detection + go test ./... -cover -race + +######## +# lint # +######## + +lint: ## lints the entire codebase + @golangci-lint run ./... --config=./.golangci.yaml + +####### +# sec # +####### + +sec-scan: trivy-scan vuln-scan ## scan for security and vulnerability issues + +trivy-scan: ## scan for sec issues with trivy (trivy binary needed) + trivy fs --exit-code 1 --no-progress --severity CRITICAL ./ + +vuln-scan: ## scan for vulnerability issues with govulncheck (govulncheck binary needed) + govulncheck ./... + +###### +# db # +###### +MYSQL_SQL_PATH="./database/migrations/mysql" + +db-mysql-init: + @( \ + printf "Enter migrate name: "; read -r MIGRATE_NAME && \ + migrate create -ext sql -dir ${MYSQL_SQL_PATH} $${MIGRATE_NAME} \ + ) + +db-mysql-up: + @( \ + printf "Enter pass for db: \n"; read -rs DB_PASSWORD && \ + printf "Enter port(3306...): \n"; read -r DB_PORT &&\ + migrate --database "mysql://root:$${DB_PASSWORD}@tcp(localhost:$${DB_PORT})/$(PROJECT_NAME)?charset=utf8&parseTime=True&loc=Local" --path ${MYSQL_SQL_PATH} up \ + ) + +db-mysql-down: + @( \ + printf "Enter pass for db: \n"; read -s DB_PASSWORD && \ + printf "Enter port(3306...): \n"; read -r DB_PORT &&\ + migrate --database "mysql://root:$${DB_PASSWORD}@tcp(localhost:$${DB_PORT})/$(PROJECT_NAME)?charset=utf8&parseTime=True&loc=Local" --path ${MYSQL_SQL_PATH} down \ + ) + +SQL_FILE_TIMESTAMP=$(shell date '+%Y%m%d%H%M%S') + +gen-migrate-sql: + @( \ + printf "Enter file name: "; read -r FILE_NAME; \ + touch database/migrations/mysql/$(SQL_FILE_TIMESTAMP)_$$FILE_NAME.up.sql; \ + touch database/migrations/mysql/$(SQL_FILE_TIMESTAMP)_$$FILE_NAME.down.sql; \ + ) + +########### +# GCI # +########### +gci-format: + gci write --skip-generated -s standard -s default -s "prefix(yt.com/backend)" -s "prefix($(PROJECT_NAME))" ./ + +######### +# build # +######### + +GitCommit=$(shell git rev-parse HEAD) +Date=$(shell date -Iseconds) + +build: + @( \ + printf "Enter file name: "; read -r VERSION; \ + go build -ldflags "-s -w -X 'main.Version=$$VERSION' -X 'main.Built=$(Date)' -X 'main.GitCommit=$(GitCommit)'" -o ./bin/$(PROJECT_NAME) ./cmd/$(PROJECT_NAME) \ + ) + +docker-image-build: + docker build \ + -f ./build/Dockerfile \ + -t $(PROJECT_NAME) \ + --platform linux/amd64 \ + --build-arg BUILT=$(Date) \ + --build-arg GIT_COMMIT=$(GitCommit) \ + --ssh default=$$HOME/.ssh/id_rsa \ + ./ + +generate: + @( \ + printf "Generate protobuf .... "; \ +# goctl rpc protoc ./generate/protobuf/member.proto --style=go_zero --go_out=./gen_result/pb --go-grpc_out=./gen_result/pb --zrpc_out=. \ +# printf "Generate docker .... "; \ +# goctl docker --go member.go --exe member --version 1.22 --tz Asia/Taipei --remote code.30cm.net --base gcr.io/distroless/static-debian12 +# goctl model mysql ddl -c yes -s ./generate/database/mysql/20230529020011_account_uid_table.up.sql --style go_zero -d ./internal/model + + ) \ No newline at end of file diff --git a/member.go b/member.go new file mode 100644 index 0000000..e83bd6d --- /dev/null +++ b/member.go @@ -0,0 +1,43 @@ +package main + +import ( + "flag" + "fmt" + "member/internal/lib/middleware" + + "member/gen_result/pb/member" + "member/internal/config" + "member/internal/server" + "member/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, server.NewAccountServer(ctx)) + + if c.Mode == service.DevMode || c.Mode == service.TestMode { + reflection.Register(grpcServer) + } + }) + defer s.Stop() + + // 加入勿中間件 + s.AddUnaryInterceptors(middleware.TimeoutMiddleware) + + fmt.Printf("Starting rpc server at %s...\n", c.ListenOn) + s.Start() +} diff --git a/readme.md b/readme.md new file mode 100644 index 0000000..cd9f892 --- /dev/null +++ b/readme.md @@ -0,0 +1,14 @@ +### Install + +```bash + +goctl + +go get -d github.com/envoyproxy/protoc-gen-validate + +``` + + +## GRPC Validate +https://github.com/anoyah/go-zero-validate +https://github.com/bufbuild/protoc-gen-validate \ No newline at end of file