diff --git a/CLAUDE.md b/CLAUDE.md index 4dd963d..6f5e577 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,81 +1,89 @@ # CLAUDE.md -This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. +此文件为 Claude Code (claude.ai/code) 在此代码库中工作时提供指导。 -## Common Development Commands +## 常用开发命令 (Common Development Commands) -### Building and Running -- `make run` - Run the server with wire dependency injection -- `make build` - Build the binary to `bin/eagle-service` with version info and race detection -- `make wire` - Generate dependency injection code using Google Wire (generates `wire_gen.go`) +### 构建与运行 (Building and Running) +- `make run` - 运行带有 wire 依赖注入的服务器 +- `make build` - 构建二进制文件到 `bin/eagle-service`,包含版本信息和竞态检测 +- `make wire` - 使用 Google Wire 生成依赖注入代码(生成 `wire_gen.go`) -### Code Generation -- `make grpc` - Generate gRPC and Protocol Buffer code from `.proto` files -- `make proto` - Generate protocol buffer structs with validation -- `make gorm-gen` - Generate GORM model files using `cmd/gen/generate.go` +### 代码生成 (Code Generation) +- `make grpc` - 从 `.proto` 文件生成 gRPC 和 Protocol Buffer 代码 +- `make proto` - 生成带有验证的协议缓冲区结构体 +- `make gorm-gen` - 使用 `cmd/gen/generate.go` 生成 GORM 模型文件 -### Testing and Quality -- `make test` - Run tests with race detection -- `make lint` - Run golangci-lint for code quality checks -- `make cover` - Generate test coverage report to `coverage.txt` -- `make view-cover` - Generate and view HTML coverage report +### 测试与质量 (Testing and Quality) +- `make test` - 运行带有竞态检测的测试 +- `make lint` - 运行 golangci-lint 进行代码质量检查 +- `make cover` - 生成测试覆盖率报告到 `coverage.txt` +- `make view-cover` - 生成并查看 HTML 覆盖率报告 -### Documentation -- `make docs` - Generate Swagger documentation (accessible at http://localhost:8080/swagger/index.html) +### 文档 (Documentation) +- `make docs` - 生成 Swagger 文档(可通过 http://localhost:8080/swagger/index.html 访问) -## Architecture Overview +## 架构概述 (Architecture Overview) -This is a Go microservice built with the Eagle framework using Clean Architecture principles: +这是使用 Clean Architecture 原则构建的 Go 微服务,基于 Eagle 框架: -### Layer Structure +### 层级结构 (Layer Structure) ``` -Service Layer (business logic) +业务逻辑层 (Service Layer) ↓ -Repository Layer (data access abstraction) +数据访问抽象层 (Repository Layer) ↓ -DAL (Data Access Layer) - ├── DB (database operations) - ├── Cache (Redis caching) - └── RPC (external service calls) +数据访问层 (DAL) + ├── 数据库操作 (DB) + ├── Redis 缓存 (Cache) + └── 外部服务调用 (RPC) ``` -### Key Components - -**Dependency Injection**: Uses Google Wire for compile-time dependency injection -- `cmd/server/wire.go` - Wire provider definitions -- `cmd/server/wire_gen.go` - Generated dependency injection code (run `make wire` to regenerate) - -**Service Layer** (`internal/service/`): -- `*_svc.go` files - HTTP service implementations -- `*_grpc.go` files - gRPC service implementations - -**Repository Layer** (`internal/repository/`): -- Provides unified data access interface -- Abstracts underlying data storage (database, cache, RPC) - -**DAL Layer** (`internal/dal/`): -- `db/` - Database operations using GORM -- `cache/` - Redis caching operations -- `rpc/` - External service communications - -**Configuration**: Uses Eagle framework's config system -- Config files in `config/` directory organized by environment (`dev/`, `prod/`, `test/`) -- Loaded via `config.New()` in main.go - -### Protocol Buffers and gRPC -- Proto definitions in `api/` directory -- Generated files include validation, gRPC, and HTTP gateway bindings -- Use `make grpc` after modifying `.proto` files - -### Entry Points -- `cmd/server/main.go` - Main HTTP/gRPC server -- `cmd/consumer/main.go` - Background job consumer -- Both use Wire for dependency injection - -### Development Workflow -1. Modify proto files if needed → `make grpc` -2. Run server → `make run` -3. Add business logic in service/repository layers -4. Generate wire code → `make wire` (if dependencies change) -5. Test → `make test` -6. Build → `make build` \ No newline at end of file +### 关键组件 (Key Components) + +**依赖注入 (Dependency Injection)**: 使用 Google Wire 进行编译时依赖注入 +- `cmd/server/wire.go` - Wire 提供程序定义 +- `cmd/server/wire_gen.go` - 生成的依赖注入代码(运行 `make wire` 重新生成) + +**服务层 (Service Layer)** (`internal/service/`): +- `*_svc.go` 文件 - 业务逻辑处理 +- `*_grpc.go` 文件 - gRPC 协议转换 + +**仓储层 (Repository Layer)** (`internal/repository/`): +- 提供统一的数据访问接口 +- 抽象底层数据存储(数据库、缓存、RPC) + +**数据访问层 (DAL Layer)** (`internal/dal/`): +- `db/` - 使用 GORM 的数据库操作 +- `cache/` - Redis 缓存操作 +- `rpc/` - 外部服务通信 + +**配置 (Configuration)**: 使用 Eagle 框架的配置系统 +- `config/` 目录中的配置文件,按环境组织(`dev/`、`prod/`、`test/`) +- 通过 main.go 中的 `config.New()` 加载 + +### 协议缓冲区和 gRPC (Protocol Buffers and gRPC) +- `api/` 目录中的 Proto 定义 +- 生成的文件包含验证、gRPC 和 HTTP 网关绑定 +- 修改 `.proto` 文件后使用 `make grpc` + +### 入口点 (Entry Points) +- `cmd/server/main.go` - 主要 HTTP/gRPC 服务器 +- `cmd/consumer/main.go` - 后台作业消费者 +- 两者都使用 Wire 进行依赖注入 + +### 开发工作流 (Development Workflow) +1. 根据需要修改 proto 文件 → `make grpc` +2. 运行服务器 → `make run` +3. 在 service/repository 层添加业务逻辑 +4. 生成 wire 代码 → `make wire`(如果依赖关系发生变化) +5. 测试 → `make test` +6. 构建 → `make build` + +## 注意事项(system prompt) + +- Always respond in 中文,但注释一律用英文 +- 不要过度设计,保证代码简洁易懂,简单实用 +- 写代码时,要注意圈复杂度,代码尽可能复用 +- 写代码时,注意模块设计,尽量使用设计模式 +- 改动时最小化修改,尽量不修改到其他模块代码 \ No newline at end of file diff --git a/internal/ecode/error.go b/internal/ecode/error.go index 884adb9..7226e85 100644 --- a/internal/ecode/error.go +++ b/internal/ecode/error.go @@ -2,31 +2,49 @@ package ecode import ( "github.com/go-eagle/eagle/pkg/errcode" - "google.golang.org/grpc/codes" ) +// ============================================ +// Eagle 框架标准错误码(统一通过 ecode 包使用) +// ============================================ + // nolint: golint var ( - // common errors - ErrInternalError = errcode.New(10000, "Internal error") - ErrInvalidArgument = errcode.New(10001, "Invalid argument") - ErrNotFound = errcode.New(10003, "Not found") - ErrAccessDenied = errcode.New(10006, "Access denied") - ErrCanceled = errcode.New(codes.Canceled, "RPC request is canceled") + // 通用错误 + // Common errors + ErrInternalServer = errcode.ErrInternalServer + ErrInvalidArgument = errcode.ErrInvalidParam + ErrUnauthorized = errcode.ErrUnauthorized + ErrNotFound = errcode.ErrNotFound + ErrUnknown = errcode.ErrUnknown + ErrDeadlineExceeded = errcode.ErrDeadlineExceeded + ErrAccessDenied = errcode.ErrAccessDenied + ErrLimitExceed = errcode.ErrLimitExceed + ErrMethodNotAllowed = errcode.ErrMethodNotAllowed + ErrSignParam = errcode.ErrSignParam + ErrValidation = errcode.ErrValidation + ErrDatabase = errcode.ErrDatabase + ErrToken = errcode.ErrToken + ErrInvalidToken = errcode.ErrInvalidToken + ErrTokenTimeout = errcode.ErrTokenTimeout + ErrTooManyRequests = errcode.ErrTooManyRequests + ErrInvalidTransaction = errcode.ErrInvalidTransaction + ErrEncrypt = errcode.ErrEncrypt + ErrServiceUnavailable = errcode.ErrServiceUnavailable - // user grpc errors - ErrUserIsExist = errcode.New(20100, "The user already exists.") - ErrUserNotFound = errcode.New(20101, "The user was not found.") - ErrPasswordIncorrect = errcode.New(20102, "账号或密码错误") - ErrAreaCodeEmpty = errcode.New(20103, "手机区号不能为空") - ErrPhoneEmpty = errcode.New(20104, "手机号不能为空") - ErrGenVCode = errcode.New(20105, "生成验证码错误") - ErrSendSMS = errcode.New(20106, "发送短信错误") - ErrSendSMSTooMany = errcode.New(20107, "已超出当日限制,请明天再试") - ErrVerifyCode = errcode.New(20108, "验证码错误") - ErrEmailOrPassword = errcode.New(20109, "邮箱或密码错误") - ErrTwicePasswordNotMatch = errcode.New(20110, "两次密码输入不一致") - ErrRegisterFailed = errcode.New(20111, "注册失败") - ErrToken = errcode.New(20112, "Gen token error") - ErrEncrypt = errcode.New(20113, "Encrypting the user password error") + // user 业务错误码(项目特定) + // User business errors (project-specific) + ErrUserIsExist = errcode.NewError(20100, "The user already exists.") + ErrUserNotFound = errcode.NewError(20101, "The user was not found.") + ErrPasswordIncorrect = errcode.NewError(20102, "账号或密码错误") + ErrAreaCodeEmpty = errcode.NewError(20103, "手机区号不能为空") + ErrPhoneEmpty = errcode.NewError(20104, "手机号不能为空") + ErrGenVCode = errcode.NewError(20105, "生成验证码错误") + ErrSendSMS = errcode.NewError(20106, "发送短信错误") + ErrSendSMSTooMany = errcode.NewError(20107, "已超出当日限制,请明天再试") + ErrVerifyCode = errcode.NewError(20108, "验证码错误") + ErrEmailOrPassword = errcode.NewError(20109, "邮箱或密码错误") + ErrTwicePasswordNotMatch = errcode.NewError(20110, "两次密码输入不一致") + ErrRegisterFailed = errcode.NewError(20111, "注册失败") + ErrInternalError = errcode.ErrInternalServer ) diff --git a/internal/handler/v1/login.go b/internal/handler/v1/login.go index 3f86575..d8c428e 100644 --- a/internal/handler/v1/login.go +++ b/internal/handler/v1/login.go @@ -3,17 +3,18 @@ package v1 import ( "github.com/gin-gonic/gin" "github.com/go-eagle/eagle-layout/internal/service" + "github.com/go-eagle/eagle-layout/internal/types" "github.com/go-eagle/eagle/pkg/app" "github.com/go-eagle/eagle/pkg/errcode" ) // LoginHandler 包含 UserService type LoginHandler struct { - UserService service.UserService + UserService *service.UserService } // NewLoginHandler 创建一个新的 LoginHandler -func NewLoginHandler(userService service.UserService) *LoginHandler { +func NewLoginHandler(userService *service.UserService) *LoginHandler { return &LoginHandler{UserService: userService} } @@ -27,6 +28,7 @@ func NewLoginHandler(userService service.UserService) *LoginHandler { func (h *LoginHandler) LoginHandler(c *gin.Context) { // 从请求中提取参数(例如 JSON 或表单参数) var loginRequest struct { + Email string `json:"email"` Username string `json:"username"` Password string `json:"password"` } @@ -37,7 +39,12 @@ func (h *LoginHandler) LoginHandler(c *gin.Context) { } // 调用 UserService 的逻辑 - user, err := h.UserService.Login(c.Request.Context(), loginRequest.Username, loginRequest.Password) + input := types.LoginInput{ + Email: loginRequest.Email, + Username: loginRequest.Username, + Password: loginRequest.Password, + } + user, err := h.UserService.Login(c.Request.Context(), input) if err != nil { app.Error(c, errcode.ErrUnauthorized.WithDetails(err.Error())) return diff --git a/internal/handler/v1/register.go b/internal/handler/v1/register.go index 4a739ab..ddaf1ef 100644 --- a/internal/handler/v1/register.go +++ b/internal/handler/v1/register.go @@ -3,17 +3,18 @@ package v1 import ( "github.com/gin-gonic/gin" "github.com/go-eagle/eagle-layout/internal/service" + "github.com/go-eagle/eagle-layout/internal/types" "github.com/go-eagle/eagle/pkg/app" "github.com/go-eagle/eagle/pkg/errcode" ) // RegisterHandler 包含 UserService type RegisterHandler struct { - UserService service.UserService + UserService *service.UserService } // NewRegisterHandler 创建一个新的 RegisterHandler -func NewRegisterHandler(userService service.UserService) *RegisterHandler { +func NewRegisterHandler(userService *service.UserService) *RegisterHandler { return &RegisterHandler{UserService: userService} } @@ -37,7 +38,11 @@ func (h *RegisterHandler) RegisterHandler(c *gin.Context) { } // 调用 UserService 的逻辑 - user, err := h.UserService.Register(c.Request.Context(), RegisterRequest.Username, RegisterRequest.Password) + input := types.RegisterInput{ + Username: RegisterRequest.Username, + Password: RegisterRequest.Password, + } + user, err := h.UserService.Register(c.Request.Context(), input) if err != nil { app.Error(c, errcode.ErrUnauthorized.WithDetails(err.Error())) return diff --git a/internal/service/convert.go b/internal/service/convert.go new file mode 100644 index 0000000..d56f1ac --- /dev/null +++ b/internal/service/convert.go @@ -0,0 +1,27 @@ +package service + +import ( + "github.com/go-eagle/eagle/pkg/errcode" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + "gorm.io/gorm" +) + +// convertToGrpcError 转换错误为 gRPC 错误 +func convertToGrpcError(err error) error { + // 检查是否是 gorm.ErrRecordNotFound + if err == gorm.ErrRecordNotFound { + return status.Errorf(codes.NotFound, "user not found") + } + + // 获取错误码 + e, ok := err.(*errcode.Error) + if !ok { + // 非标准错误,包装为 Internal 错误 + return status.Errorf(codes.Internal, "internal error: %v", err) + } + + // 转换错误码 + code := errcode.ToRPCCode(e.Code()) + return status.Errorf(code, e.Msg()) +} diff --git a/internal/service/user_grpc.go b/internal/service/user_grpc.go new file mode 100644 index 0000000..1d918a4 --- /dev/null +++ b/internal/service/user_grpc.go @@ -0,0 +1,229 @@ +package service + +import ( + "context" + "strconv" + + "github.com/jinzhu/copier" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + + pb "github.com/go-eagle/eagle-layout/api/user/v1" + "github.com/go-eagle/eagle-layout/internal/repository" + "github.com/go-eagle/eagle-layout/internal/types" +) + +var ( + _ pb.UserServiceServer = (*UserServiceServer)(nil) +) + +// UserServiceServer gRPC 服务端 +type UserServiceServer struct { + pb.UnimplementedUserServiceServer + + userSvc *UserService +} + +// NewUserServiceServer 创建 gRPC 服务端 +func NewUserServiceServer(repo repository.UserRepo) *UserServiceServer { + return &UserServiceServer{ + userSvc: NewUserService(repo), + } +} + +// Register 注册 +func (s *UserServiceServer) Register(ctx context.Context, req *pb.RegisterRequest) (*pb.RegisterReply, error) { + // 协议转换:pb.RegisterRequest → types.RegisterInput + input := types.RegisterInput{ + Username: req.Username, + Email: req.Email, + Password: req.Password, + } + + // 调用业务逻辑 + result, err := s.userSvc.Register(ctx, input) + if err != nil { + return nil, convertToGrpcError(err) + } + + // 协议转换:types.RegisterOutput → pb.RegisterReply + return &pb.RegisterReply{ + Id: result.ID, + Username: result.Username, + }, nil +} + +// Login 登录 +func (s *UserServiceServer) Login(ctx context.Context, req *pb.LoginRequest) (*pb.LoginReply, error) { + // 协议转换 + input := types.LoginInput{ + Email: req.Email, + Username: req.Username, + Password: req.Password, + } + + // 调用业务逻辑 + result, err := s.userSvc.Login(ctx, input) + if err != nil { + return nil, convertToGrpcError(err) + } + + return &pb.LoginReply{ + Id: result.ID, + AccessToken: result.AccessToken, + }, nil +} + +// Logout 登出 +func (s *UserServiceServer) Logout(ctx context.Context, req *pb.LogoutRequest) (*pb.LogoutReply, error) { + // 协议转换 + input := types.LogoutInput{ + ID: req.Id, + AccessToken: req.AccessToken, + } + + // 调用业务逻辑 + _, err := s.userSvc.Logout(ctx, input) + if err != nil { + return nil, convertToGrpcError(err) + } + + return &pb.LogoutReply{}, nil +} + +// CreateUser 创建用户 +func (s *UserServiceServer) CreateUser(ctx context.Context, req *pb.CreateUserRequest) (*pb.CreateUserReply, error) { + // 协议转换 + input := types.CreateUserInput{ + Username: req.Username, + Email: req.Email, + Password: req.Password, + } + + // 调用业务逻辑 + result, err := s.userSvc.CreateUser(ctx, input) + if err != nil { + return nil, convertToGrpcError(err) + } + + return &pb.CreateUserReply{ + Id: result.ID, + Username: result.Username, + }, nil +} + +// UpdateUser 更新用户 +func (s *UserServiceServer) UpdateUser(ctx context.Context, req *pb.UpdateUserRequest) (*pb.UpdateUserReply, error) { + // 协议转换 + input := types.UpdateUserInput{ + UserId: req.UserId, + Nickname: req.Nickname, + Phone: req.Phone, + Email: req.Email, + Avatar: req.Avatar, + Gender: int32(req.Gender), + Birthday: req.Birthday, + Bio: req.Bio, + Status: int32(req.Status), + } + + // 调用业务逻辑 + result, err := s.userSvc.UpdateUser(ctx, input) + if err != nil { + return nil, convertToGrpcError(err) + } + + return &pb.UpdateUserReply{ + UserId: result.UserId, + Nickname: result.Nickname, + Phone: result.Phone, + Email: result.Email, + Avatar: result.Avatar, + Gender: pb.GenderType(result.Gender), + Birthday: result.Birthday, + Bio: result.Bio, + Status: pb.StatusType(result.Status), + UpdatedAt: result.UpdatedAt, + }, nil +} + +// UpdatePassword 更新密码 +func (s *UserServiceServer) UpdatePassword(ctx context.Context, req *pb.UpdatePasswordRequest) (*pb.UpdatePasswordReply, error) { + // 协议转换:string ID -> int64 ID + id, err := strconv.ParseInt(req.Id, 10, 64) + if err != nil { + return nil, status.Errorf(codes.InvalidArgument, "invalid id format") + } + + input := types.UpdatePasswordInput{ + ID: id, + Password: req.Password, + NewPassword: req.NewPassword, + ConfirmPassword: req.ConfirmPassword, + } + + // 调用业务逻辑 + _, err = s.userSvc.UpdatePassword(ctx, input) + if err != nil { + return nil, convertToGrpcError(err) + } + + return &pb.UpdatePasswordReply{}, nil +} + +// GetUser 获取用户 +func (s *UserServiceServer) GetUser(ctx context.Context, req *pb.GetUserRequest) (*pb.GetUserReply, error) { + // 协议转换 + input := types.GetUserInput{ + ID: req.Id, + } + + // 调用业务逻辑 + result, err := s.userSvc.GetUser(ctx, input) + if err != nil { + return nil, convertToGrpcError(err) + } + + return &pb.GetUserReply{ + User: s.convertUser(result.User), + }, nil +} + +// BatchGetUsers 批量获取用户 +func (s *UserServiceServer) BatchGetUsers(ctx context.Context, req *pb.BatchGetUsersRequest) (*pb.BatchGetUsersReply, error) { + // 检查 RPC 请求是否取消(gRPC 特定处理) + if ctx.Err() == context.Canceled { + return nil, status.Error(codes.Canceled, "request canceled") + } + + // 协议转换 + input := types.BatchGetUsersInput{ + IDs: req.GetIds(), + } + + // 调用业务逻辑 + result, err := s.userSvc.BatchGetUsers(ctx, input) + if err != nil { + return nil, convertToGrpcError(err) + } + + // 转换为 pb.User 列表 + var pbUsers []*pb.User + for _, user := range result.Users { + pbUsers = append(pbUsers, s.convertUser(user)) + } + + return &pb.BatchGetUsersReply{ + Users: pbUsers, + }, nil +} + +// convertUser 转换用户模型 +func (s *UserServiceServer) convertUser(u *types.User) *pb.User { + if u == nil { + return nil + } + pbUser := &pb.User{} + _ = copier.Copy(pbUser, u) + return pbUser +} diff --git a/internal/service/user_grpc_svc.go b/internal/service/user_grpc_svc.go deleted file mode 100644 index 9e4aabd..0000000 --- a/internal/service/user_grpc_svc.go +++ /dev/null @@ -1,405 +0,0 @@ -package service - -import ( - "context" - "errors" - "time" - - "github.com/jinzhu/copier" - "github.com/spf13/cast" - "gorm.io/gorm" - - pb "github.com/go-eagle/eagle-layout/api/user/v1" - "github.com/go-eagle/eagle-layout/internal/dal/cache" - "github.com/go-eagle/eagle-layout/internal/dal/db/model" - "github.com/go-eagle/eagle-layout/internal/ecode" - "github.com/go-eagle/eagle-layout/internal/repository" - "github.com/go-eagle/eagle-layout/internal/tasks" - "github.com/go-eagle/eagle-layout/internal/types" - "github.com/go-eagle/eagle/pkg/app" - "github.com/go-eagle/eagle/pkg/auth" - "github.com/go-eagle/eagle/pkg/errcode" - "github.com/go-eagle/eagle/pkg/log" -) - -var ( - _ pb.UserServiceServer = (*UserServiceServer)(nil) -) - -type UserServiceServer struct { - pb.UnimplementedUserServiceServer - - repo repository.UserRepo -} - -func NewUserServiceServer(repo repository.UserRepo) *UserServiceServer { - return &UserServiceServer{ - repo: repo, - } -} - -func (s *UserServiceServer) Register(ctx context.Context, req *pb.RegisterRequest) (*pb.RegisterReply, error) { - err := req.Validate() - if err != nil { - return nil, ecode.ErrInvalidArgument.WithDetails(errcode.NewDetails(map[string]interface{}{ - "msg": err.Error(), - })).Status(req).Err() - } - - var userBase *model.UserInfoModel - - // check user is existed by email - userBase, err = s.repo.GetUserByEmail(ctx, req.Email) - if err != nil { - return nil, ecode.ErrInternalError.WithDetails(errcode.NewDetails(map[string]interface{}{ - "msg": err.Error(), - })).Status(req).Err() - } - if userBase != nil && userBase.ID > 0 { - return nil, ecode.ErrUserIsExist.Status(req).Err() - } - - // check user is existed by username - userBase, err = s.repo.GetUserByUsername(ctx, req.Username) - if err != nil { - return nil, ecode.ErrInternalError.WithDetails(errcode.NewDetails(map[string]interface{}{ - "msg": err.Error(), - })).Status(req).Err() - } - if userBase != nil && userBase.ID > 0 { - return nil, ecode.ErrUserIsExist.Status(req).Err() - } - - // gen a hash password - pwd, err := auth.HashAndSalt(req.Password) - if err != nil { - return nil, errcode.ErrEncrypt - } - - // create a new user - user, err := newUser(req.Username, req.Email, pwd) - if err != nil { - return nil, ecode.ErrInternalError.WithDetails(errcode.NewDetails(map[string]interface{}{ - "msg": err.Error(), - })).Status(req).Err() - } - uid, err := s.repo.CreateUser(ctx, user) - if err != nil { - return nil, ecode.ErrInternalError.WithDetails(errcode.NewDetails(map[string]interface{}{ - "msg": err.Error(), - })).Status(req).Err() - } - - // send welcome email - err = tasks.NewEmailWelcomeTask(tasks.EmailWelcomePayload{UserID: uid}) - if err != nil { - return nil, ecode.ErrInternalError.WithDetails(errcode.NewDetails(map[string]interface{}{ - "msg": err.Error(), - })).Status(req).Err() - } - - return &pb.RegisterReply{ - Id: uid, - Username: req.Username, - }, nil -} - -func newUser(username, email, password string) (model.UserInfoModel, error) { - return model.UserInfoModel{ - Username: username, - Email: email, - Password: password, - Status: int32(pb.StatusType_NORMAL), - CreatedAt: time.Now().Unix(), - }, nil -} - -func (s *UserServiceServer) Login(ctx context.Context, req *pb.LoginRequest) (*pb.LoginReply, error) { - if len(req.Email) == 0 && len(req.Username) == 0 { - return nil, ecode.ErrInvalidArgument.Status(req).Err() - } - - // get user base info - var ( - user *model.UserInfoModel - err error - ) - - // try to get user by email first - if req.Email != "" { - user, err = s.repo.GetUserByEmail(ctx, req.Email) - if err != nil { - return nil, ecode.ErrInternalError.WithDetails(errcode.NewDetails(map[string]interface{}{ - "msg": err.Error(), - })).Status(req).Err() - } - } - - // if not found by email, try username - if user == nil && len(req.Username) > 0 { - user, err = s.repo.GetUserByUsername(ctx, req.Username) - if err != nil { - return nil, ecode.ErrInternalError.WithDetails(errcode.NewDetails(map[string]interface{}{ - "msg": err.Error(), - })).Status(req).Err() - } - } - - // user not found, return password incorrect error (security best practice) - if user == nil || user.ID == 0 { - return nil, ecode.ErrPasswordIncorrect.Status(req).Err() - } - - if !auth.ComparePasswords(user.Password, req.Password) { - return nil, ecode.ErrPasswordIncorrect.Status(req).Err() - } - - // Sign the json web token. - payload := map[string]interface{}{"user_id": user.ID, "username": user.Username} - token, err := app.Sign(ctx, payload, app.Conf.JwtSecret, int64(cache.UserTokenExpireTime)) - if err != nil { - return nil, ecode.ErrToken.Status(req).Err() - } - - // record token to redis - err = cache.NewUserTokenCache().SetUserTokenCache(ctx, user.ID, token, cache.UserTokenExpireTime) - if err != nil { - return nil, ecode.ErrToken.Status(req).Err() - } - - return &pb.LoginReply{ - Id: user.ID, - AccessToken: token, - }, nil -} - -func (s *UserServiceServer) Logout(ctx context.Context, req *pb.LogoutRequest) (*pb.LogoutReply, error) { - c := cache.NewUserTokenCache() - // check token - token, err := c.GetUserTokenCache(ctx, req.Id) - if err != nil { - return nil, ecode.ErrToken.Status(req).Err() - } - if token != req.AccessToken { - return nil, ecode.ErrAccessDenied.Status(req).Err() - } - - // delete token from cache - err = c.DelUserTokenCache(ctx, req.GetId()) - if err != nil { - return nil, ecode.ErrInternalError.WithDetails(errcode.NewDetails(map[string]interface{}{ - "msg": err.Error(), - })).Status(req).Err() - } - - return &pb.LogoutReply{}, nil -} - -func (s *UserServiceServer) CreateUser(ctx context.Context, req *pb.CreateUserRequest) (*pb.CreateUserReply, error) { - // gen a hash password - pwd, err := auth.HashAndSalt(req.Password) - if err != nil { - return nil, errcode.ErrEncrypt - } - - // create a new user - user, err := newUser(req.Username, req.Email, pwd) - if err != nil { - return nil, ecode.ErrInternalError.WithDetails(errcode.NewDetails(map[string]interface{}{ - "msg": err.Error(), - })).Status(req).Err() - } - id, err := s.repo.CreateUser(ctx, user) - if err != nil { - return nil, ecode.ErrInternalError.WithDetails(errcode.NewDetails(map[string]interface{}{ - "msg": err.Error(), - })).Status(req).Err() - } - - return &pb.CreateUserReply{ - Id: id, - Username: req.Username, - }, nil -} - -func (s *UserServiceServer) UpdateUser(ctx context.Context, req *pb.UpdateUserRequest) (*pb.UpdateUserReply, error) { - if req.UserId == 0 { - return nil, ecode.ErrInvalidArgument.Status(req).Err() - } - - user := model.UserInfoModel{ - Nickname: req.Nickname, - //Phone: req.Phone, - Email: req.Email, - Avatar: req.Avatar, - //Gender: cast.ToString(req.Gender), - Birthday: req.Birthday, - Bio: req.Bio, - Status: cast.ToInt32(req.Status), - UpdatedAt: time.Now().Unix(), - } - err := s.repo.UpdateUser(ctx, req.UserId, user) - if err != nil { - return nil, ecode.ErrInternalError.WithDetails(errcode.NewDetails(map[string]interface{}{ - "msg": err.Error(), - })).Status(req).Err() - } - - return &pb.UpdateUserReply{ - UserId: req.UserId, - Nickname: req.Nickname, - Phone: req.Phone, - Email: req.Email, - Avatar: req.Avatar, - Gender: req.Gender, - Birthday: req.Birthday, - Bio: req.Bio, - Status: req.Status, - UpdatedAt: time.Now().Unix(), - }, nil -} - -func (s *UserServiceServer) UpdatePassword(ctx context.Context, req *pb.UpdatePasswordRequest) (*pb.UpdatePasswordReply, error) { - if len(req.Id) == 0 { - return nil, ecode.ErrInvalidArgument.Status(req).Err() - } - if len(req.Password) == 0 || len(req.NewPassword) == 0 || len(req.ConfirmPassword) == 0 { - return nil, ecode.ErrInvalidArgument.Status(req).Err() - } - if req.NewPassword != req.ConfirmPassword { - return nil, ecode.ErrTwicePasswordNotMatch.Status(req).Err() - } - - // get user base info - var ( - user *model.UserInfoModel - err error - ) - user, err = s.repo.GetUser(ctx, cast.ToInt64(req.Id)) - if err != nil { - return nil, ecode.ErrInternalError.WithDetails(errcode.NewDetails(map[string]interface{}{ - "msg": err.Error(), - })).Status(req).Err() - } - if user == nil || user.ID == 0 { - return nil, ecode.ErrUserNotFound.Status(req).Err() - } - - if !auth.ComparePasswords(user.Password, req.Password) { - return nil, ecode.ErrPasswordIncorrect.Status(req).Err() - } - - newPwd, err := auth.HashAndSalt(req.NewPassword) - if err != nil { - return nil, ecode.ErrEncrypt.Status(req).Err() - } - - data := model.UserInfoModel{ - Password: newPwd, - UpdatedAt: time.Now().Unix(), - } - err = s.repo.UpdateUser(ctx, user.ID, data) - if err != nil { - return nil, ecode.ErrInternalError.WithDetails(errcode.NewDetails(map[string]interface{}{ - "msg": err.Error(), - })).Status(req).Err() - } - - return &pb.UpdatePasswordReply{}, nil -} - -func (s *UserServiceServer) GetUser(ctx context.Context, req *pb.GetUserRequest) (*pb.GetUserReply, error) { - user, err := s.repo.GetUser(ctx, req.Id) - if err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return nil, ecode.ErrUserNotFound.Status(req).Err() - } - return nil, ecode.ErrInternalError.WithDetails(errcode.NewDetails(map[string]interface{}{ - "msg": err.Error(), - })).Status(req).Err() - } - - u, err := convertUser(user) - if err != nil { - return nil, ecode.ErrInternalError.WithDetails(errcode.NewDetails(map[string]interface{}{ - "msg": err.Error(), - })).Status(req).Err() - } - - return &pb.GetUserReply{ - User: u, - }, nil -} - -func (s *UserServiceServer) BatchGetUsers(ctx context.Context, req *pb.BatchGetUsersRequest) (*pb.BatchGetUsersReply, error) { - // check rpc request if canceled - if ctx.Err() == context.Canceled { - return nil, ecode.ErrCanceled.Status(req).Err() - } - - if len(req.GetIds()) == 0 { - return nil, errors.New("ids is empty") - } - var ( - ids []int64 - users []*pb.User - ) - ids = req.GetIds() - - // user base - userBases, err := s.repo.BatchGetUsers(ctx, ids) - if err != nil { - return nil, ecode.ErrInternalError.Status(req).Err() - } - userMap := make(map[int64]*model.UserInfoModel, 0) - for _, val := range userBases { - userMap[val.ID] = val - } - - // compose data - for _, id := range ids { - user, ok := userMap[id] - if !ok { - continue - } - u, err := convertUser(user) - if err != nil { - log.Errorf("[BatchGetUsers] convertUser error: %v", err) - continue - } - users = append(users, u) - } - - return &pb.BatchGetUsersReply{ - Users: users, - }, nil -} - -func convertUser(u *model.UserInfoModel) (*pb.User, error) { - if u == nil { - return nil, nil - } - user := &types.User{ - Id: u.ID, - Username: u.Username, - Phone: u.Phone, - Email: u.Email, - LoginAt: u.LoginAt, - Status: u.Status, - Nickname: u.Nickname, - Avatar: u.Avatar, - Gender: u.Gender, - Birthday: u.Birthday, - Bio: u.Bio, - CreatedAt: u.CreatedAt, - UpdatedAt: u.UpdatedAt, - } - - // copy to pb.user - pbUser := &pb.User{} - err := copier.Copy(pbUser, &user) - if err != nil { - return nil, err - } - return pbUser, nil -} diff --git a/internal/service/user_svc.go b/internal/service/user_svc.go index 5b3de0e..779dac4 100644 --- a/internal/service/user_svc.go +++ b/internal/service/user_svc.go @@ -2,35 +2,332 @@ package service import ( "context" + "fmt" + "time" + "github.com/go-eagle/eagle-layout/internal/dal/cache" + "github.com/go-eagle/eagle-layout/internal/dal/db/model" + "github.com/go-eagle/eagle-layout/internal/ecode" "github.com/go-eagle/eagle-layout/internal/repository" + "github.com/go-eagle/eagle-layout/internal/tasks" + "github.com/go-eagle/eagle-layout/internal/types" + "github.com/go-eagle/eagle/pkg/app" + "github.com/go-eagle/eagle/pkg/auth" + "github.com/go-eagle/eagle/pkg/errcode" + + pb "github.com/go-eagle/eagle-layout/api/user/v1" ) -// UserService define a interface -type UserService interface { - Login(ctx context.Context, username, password string) (string, error) - Register(ctx context.Context, username, password string) (string, error) +// UserService 用户业务服务 +type UserService struct { + repo repository.UserRepo } -// greeterService define a struct -type userService struct { - repo repository.UserRepo +// NewUserService 创建用户业务服务 +func NewUserService(repo repository.UserRepo) *UserService { + return &UserService{ + repo: repo, + } } -var _ UserService = (*userService)(nil) +// Register 用户注册 +func (s *UserService) Register(ctx context.Context, input types.RegisterInput) (*types.RegisterOutput, error) { + // 检查邮箱是否存在 + userBase, err := s.repo.GetUserByEmail(ctx, input.Email) + if err != nil { + return nil, fmt.Errorf("[UserService] Register GetUserByEmail error: %w", err) + } + if userBase != nil && userBase.ID > 0 { + return nil, ecode.ErrUserIsExist + } -// NewUserService create a service -func NewUserService(repo repository.UserRepo) *userService { - return &userService{ - repo: repo, + // 检查用户名是否存在 + userBase, err = s.repo.GetUserByUsername(ctx, input.Username) + if err != nil { + return nil, ecode.ErrInternalError + } + if userBase != nil && userBase.ID > 0 { + return nil, ecode.ErrUserIsExist + } + + // 生成密码哈希 + pwdHashed, err := auth.HashAndSalt(input.Password) + if err != nil { + return nil, errcode.ErrEncrypt } + + // 创建用户 + user, err := s.newUser(input.Username, input.Email, pwdHashed) + if err != nil { + return nil, ecode.ErrInternalError + } + uid, err := s.repo.CreateUser(ctx, user) + if err != nil { + return nil, fmt.Errorf("[UserService] Register CreateUser error: %w", err) + } + + // 发送欢迎邮件 + err = tasks.NewEmailWelcomeTask(tasks.EmailWelcomePayload{UserID: uid}) + if err != nil { + return nil, fmt.Errorf("[UserService] Register NewEmailWelcomeTask error: %w", err) + } + + return &types.RegisterOutput{ + ID: uid, + Username: input.Username, + }, nil } -// Hello . -func (s *userService) Login(ctx context.Context, username, password string) (string, error) { - return "hello " + username, nil +// Login 用户登录 +func (s *UserService) Login(ctx context.Context, input types.LoginInput) (*types.LoginOutput, error) { + var user *model.UserInfoModel + var err error + + // 尝试用邮箱登录 + if input.Email != "" { + user, err = s.repo.GetUserByEmail(ctx, input.Email) + if err != nil { + return nil, fmt.Errorf("[UserService] Login GetUserByEmail error: %w", err) + } + } + + // 如果没找到,尝试用户名 + if user == nil && input.Username != "" { + user, err = s.repo.GetUserByUsername(ctx, input.Username) + if err != nil { + return nil, fmt.Errorf("[UserService] Login GetUserByUsername error: %w", err) + } + } + + // 用户不存在或密码错误 + if user == nil || user.ID == 0 || !auth.ComparePasswords(user.Password, input.Password) { + return nil, ecode.ErrPasswordIncorrect + } + + // 生成 token + payload := map[string]interface{}{"user_id": user.ID, "username": user.Username} + token, err := app.Sign(ctx, payload, app.Conf.JwtSecret, int64(cache.UserTokenExpireTime)) + if err != nil { + return nil, ecode.ErrToken + } + + // 记录 token 到 redis + err = cache.NewUserTokenCache().SetUserTokenCache(ctx, user.ID, token, cache.UserTokenExpireTime) + if err != nil { + return nil, ecode.ErrToken + } + + return &types.LoginOutput{ + ID: user.ID, + AccessToken: token, + }, nil +} + +// Logout 用户登出 +func (s *UserService) Logout(ctx context.Context, input types.LogoutInput) (*types.LogoutOutput, error) { + c := cache.NewUserTokenCache() + + // 检查 token + token, err := c.GetUserTokenCache(ctx, input.ID) + if err != nil { + return nil, ecode.ErrToken + } + if token != input.AccessToken { + return nil, ecode.ErrAccessDenied + } + + // 从缓存中删除 token + err = c.DelUserTokenCache(ctx, input.ID) + if err != nil { + return nil, ecode.ErrInternalError + } + + return &types.LogoutOutput{}, nil +} + +// CreateUser 创建用户 +func (s *UserService) CreateUser(ctx context.Context, input types.CreateUserInput) (*types.CreateUserOutput, error) { + // 生成密码哈希 + pwd, err := auth.HashAndSalt(input.Password) + if err != nil { + return nil, errcode.ErrEncrypt + } + + // 创建用户 + user, err := s.newUser(input.Username, input.Email, pwd) + if err != nil { + return nil, fmt.Errorf("[UserService] CreateUser newUser error: %w", err) + } + id, err := s.repo.CreateUser(ctx, user) + if err != nil { + return nil, fmt.Errorf("[UserService] CreateUser CreateUser error: %w", err) + } + + return &types.CreateUserOutput{ + ID: id, + Username: input.Username, + }, nil } -func (s *userService) Register(ctx context.Context, username, password string) (string, error) { - return "register success, username: " + username, nil +// UpdateUser 更新用户 +func (s *UserService) UpdateUser(ctx context.Context, input types.UpdateUserInput) (*types.UpdateUserOutput, error) { + if input.UserId == 0 { + return nil, ecode.ErrInvalidArgument + } + + user := model.UserInfoModel{ + Nickname: input.Nickname, + Email: input.Email, + Avatar: input.Avatar, + Birthday: input.Birthday, + Bio: input.Bio, + Status: input.Status, + UpdatedAt: time.Now().Unix(), + } + err := s.repo.UpdateUser(ctx, input.UserId, user) + if err != nil { + return nil, fmt.Errorf("[UserService] UpdateUser UpdateUser error: %w", err) + } + + return &types.UpdateUserOutput{ + UserId: input.UserId, + Nickname: input.Nickname, + Email: input.Email, + Avatar: input.Avatar, + Gender: input.Gender, + Birthday: input.Birthday, + Bio: input.Bio, + Status: input.Status, + UpdatedAt: time.Now().Unix(), + }, nil +} + +// UpdatePassword 更新密码 +func (s *UserService) UpdatePassword(ctx context.Context, input types.UpdatePasswordInput) (*types.UpdatePasswordOutput, error) { + if input.ID == 0 { + return nil, ecode.ErrInvalidArgument + } + if input.Password == "" || input.ConfirmPassword == "" || input.NewPassword == "" { + return nil, ecode.ErrInvalidArgument + } + if input.Password != input.ConfirmPassword { + return nil, ecode.ErrTwicePasswordNotMatch + } + + // 获取用户信息 + user, err := s.repo.GetUser(ctx, input.ID) + if err != nil { + return nil, fmt.Errorf("[UserService] UpdatePassword GetUser error: %w", err) + } + if user == nil || user.ID == 0 { + return nil, ecode.ErrUserNotFound + } + + // 验证旧密码 + if !auth.ComparePasswords(user.Password, input.Password) { + return nil, ecode.ErrPasswordIncorrect + } + + // 生成新密码哈希 + newPwd, err := auth.HashAndSalt(input.NewPassword) + if err != nil { + return nil, errcode.ErrEncrypt + } + + // 更新密码 + data := model.UserInfoModel{ + Password: newPwd, + UpdatedAt: time.Now().Unix(), + } + err = s.repo.UpdateUser(ctx, user.ID, data) + if err != nil { + return nil, fmt.Errorf("[UserService] UpdatePassword UpdateUser error: %w", err) + } + + return &types.UpdatePasswordOutput{}, nil +} + +// GetUser 获取用户 +func (s *UserService) GetUser(ctx context.Context, input types.GetUserInput) (*types.GetUserOutput, error) { + user, err := s.repo.GetUser(ctx, input.ID) + if err != nil { + return nil, fmt.Errorf("[UserService] GetUser GetUser error: %w", err) + } + + u, err := s.convertUser(user) + if err != nil { + return nil, fmt.Errorf("[UserService] GetUser convertUser error: %w", err) + } + + return &types.GetUserOutput{ + User: u, + }, nil +} + +// BatchGetUsers 批量获取用户 +func (s *UserService) BatchGetUsers(ctx context.Context, input types.BatchGetUsersInput) (*types.BatchGetUsersOutput, error) { + if len(input.IDs) == 0 { + return nil, ecode.ErrInvalidArgument + } + + // 获取用户列表 + userBases, err := s.repo.BatchGetUsers(ctx, input.IDs) + if err != nil { + return nil, ecode.ErrInternalError + } + + userMap := make(map[int64]*model.UserInfoModel, 0) + for _, val := range userBases { + userMap[val.ID] = val + } + + var users []*types.User + for _, id := range input.IDs { + user, ok := userMap[id] + if !ok { + continue + } + u, err := s.convertUser(user) + if err != nil { + continue + } + users = append(users, u) + } + + return &types.BatchGetUsersOutput{ + Users: users, + }, nil +} + +// newUser 创建用户模型 +func (s *UserService) newUser(username, email, password string) (model.UserInfoModel, error) { + return model.UserInfoModel{ + Username: username, + Email: email, + Password: password, + Status: int32(pb.StatusType_NORMAL), + CreatedAt: time.Now().Unix(), + }, nil +} + +// convertUser 转换用户模型 +func (s *UserService) convertUser(u *model.UserInfoModel) (*types.User, error) { + if u == nil { + return nil, nil + } + return &types.User{ + Id: u.ID, + Username: u.Username, + Phone: u.Phone, + Email: u.Email, + LoginAt: u.LoginAt, + Status: u.Status, + Nickname: u.Nickname, + Avatar: u.Avatar, + Gender: u.Gender, + Birthday: u.Birthday, + Bio: u.Bio, + CreatedAt: u.CreatedAt, + UpdatedAt: u.UpdatedAt, + }, nil } diff --git a/internal/types/user.go b/internal/types/user.go index 2a419ae..0972b97 100644 --- a/internal/types/user.go +++ b/internal/types/user.go @@ -17,3 +17,109 @@ type User struct { CreatedAt int64 `json:"created_at"` UpdatedAt int64 `json:"updated_at"` } + +// RegisterInput 注册输入 +type RegisterInput struct { + Username string + Email string + Password string +} + +// RegisterOutput 注册输出 +type RegisterOutput struct { + ID int64 + Username string +} + +// LoginInput 登录输入 +type LoginInput struct { + Email string + Username string + Password string +} + +// LoginOutput 登录输出 +type LoginOutput struct { + ID int64 + AccessToken string +} + +// CreateUserInput 创建用户输入 +type CreateUserInput struct { + Username string + Email string + Password string +} + +// CreateUserOutput 创建用户输出 +type CreateUserOutput struct { + ID int64 + Username string +} + +// UpdateUserInput 更新用户输入 +type UpdateUserInput struct { + UserId int64 + Nickname string + Phone string + Email string + Avatar string + Gender int32 + Birthday string + Bio string + Status int32 +} + +// UpdateUserOutput 更新用户输出 +type UpdateUserOutput struct { + UserId int64 + Nickname string + Phone string + Email string + Avatar string + Gender int32 + Birthday string + Bio string + Status int32 + UpdatedAt int64 +} + +// UpdatePasswordInput 更新密码输入 +type UpdatePasswordInput struct { + ID int64 + Password string + ConfirmPassword string + NewPassword string +} + +// UpdatePasswordOutput 更新密码输出 +type UpdatePasswordOutput struct{} + +// GetUserInput 获取用户输入 +type GetUserInput struct { + ID int64 +} + +// GetUserOutput 获取用户输出 +type GetUserOutput struct { + User *User +} + +// BatchGetUsersInput 批量获取用户输入 +type BatchGetUsersInput struct { + IDs []int64 +} + +// BatchGetUsersOutput 批量获取用户输出 +type BatchGetUsersOutput struct { + Users []*User +} + +// LogoutInput 登出输入 +type LogoutInput struct { + ID int64 + AccessToken string +} + +// LogoutOutput 登出输出 +type LogoutOutput struct{}