From 1282690e37aed8bf3451efa2c3da3a5b1fcbac11 Mon Sep 17 00:00:00 2001 From: yokowu <18836617@qq.com> Date: Mon, 21 Jul 2025 11:53:26 +0800 Subject: [PATCH 1/3] =?UTF-8?q?feat:=20=E5=A2=9E=E5=8A=A0=E7=94=A8?= =?UTF-8?q?=E6=88=B7=E7=99=BB=E5=BD=95=E6=9D=A5=E6=BA=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/cmd/server/wire_gen.go | 2 +- backend/consts/user.go | 10 +- backend/docs/swagger.json | 43 ++++++++- backend/domain/oauth.go | 4 +- backend/domain/user.go | 14 +-- backend/internal/user/handler/v1/user.go | 12 +-- backend/internal/user/repo/user.go | 21 ++-- backend/internal/user/usecase/user.go | 118 +++++++++++++++-------- 8 files changed, 160 insertions(+), 64 deletions(-) diff --git a/backend/cmd/server/wire_gen.go b/backend/cmd/server/wire_gen.go index ab0016a..d779603 100644 --- a/backend/cmd/server/wire_gen.go +++ b/backend/cmd/server/wire_gen.go @@ -74,7 +74,7 @@ func newServer() (*Server, error) { return nil, err } userRepo := repo5.NewUserRepo(client, ipdbIPDB, redisClient) - userUsecase := usecase4.NewUserUsecase(configConfig, redisClient, userRepo, slogLogger) + userUsecase := usecase4.NewUserUsecase(configConfig, redisClient, userRepo, slogLogger, sessionSession) userHandler := v1_3.NewUserHandler(web, userUsecase, extensionUsecase, authMiddleware, activeMiddleware, sessionSession, slogLogger, configConfig) dashboardRepo := repo6.NewDashboardRepo(client) dashboardUsecase := usecase5.NewDashboardUsecase(dashboardRepo) diff --git a/backend/consts/user.go b/backend/consts/user.go index 8e6bb3a..dfd3502 100644 --- a/backend/consts/user.go +++ b/backend/consts/user.go @@ -14,7 +14,8 @@ const ( ) const ( - SessionName = "monkeycode_session" + SessionName = "monkeycode_session" + UserSessionName = "monkeycode_user_session" ) type UserPlatform string @@ -38,3 +39,10 @@ const ( InviteCodeStatusPending InviteCodeStatus = "pending" InviteCodeStatusUsed InviteCodeStatus = "used" ) + +type LoginSource string + +const ( + LoginSourcePlugin LoginSource = "plugin" + LoginSourceBrowser LoginSource = "browser" +) diff --git a/backend/docs/swagger.json b/backend/docs/swagger.json index eac0ad2..2271db4 100644 --- a/backend/docs/swagger.json +++ b/backend/docs/swagger.json @@ -1785,6 +1785,20 @@ "description": "会话ID", "name": "session_id", "in": "query" + }, + { + "enum": [ + "plugin", + "browser" + ], + "type": "string", + "x-enum-varnames": [ + "LoginSourcePlugin", + "LoginSourceBrowser" + ], + "description": "登录来源 plugin: 插件 browser: 浏览器; 默认为 plugin", + "name": "source", + "in": "query" } ], "responses": { @@ -2107,6 +2121,17 @@ "ChatRoleSystem" ] }, + "consts.LoginSource": { + "type": "string", + "enum": [ + "plugin", + "browser" + ], + "x-enum-varnames": [ + "LoginSourcePlugin", + "LoginSourceBrowser" + ] + }, "consts.ModelProvider": { "type": "string", "enum": [ @@ -2894,9 +2919,17 @@ "type": "string" }, "session_id": { - "description": "会话Id", + "description": "会话Id插件登录时必填", "type": "string" }, + "source": { + "description": "登录来源 plugin: 插件 browser: 浏览器; 默认为 plugin", + "allOf": [ + { + "$ref": "#/definitions/consts.LoginSource" + } + ] + }, "username": { "description": "用户名", "type": "string" @@ -2909,6 +2942,14 @@ "redirect_url": { "description": "重定向URL", "type": "string" + }, + "user": { + "description": "用户信息", + "allOf": [ + { + "$ref": "#/definitions/domain.User" + } + ] } } }, diff --git a/backend/domain/oauth.go b/backend/domain/oauth.go index 91df152..cb921bd 100644 --- a/backend/domain/oauth.go +++ b/backend/domain/oauth.go @@ -32,6 +32,7 @@ type OAuthUserInfo struct { } type OAuthSignUpOrInReq struct { + Source consts.LoginSource `json:"source"` // 登录来源 plugin: 插件 browser: 浏览器; 默认为 plugin Platform consts.UserPlatform `json:"platform" query:"platform" validate:"required"` // 第三方平台 dingtalk SessionID string `json:"session_id" query:"session_id"` // 会话ID RedirectURL string `json:"redirect_url" query:"redirect_url"` // 登录成功后跳转的 URL @@ -56,7 +57,8 @@ type OAuthURLResp struct { } type OAuthState struct { - Kind consts.OAuthKind `json:"kind" query:"kind" validate:"required"` // 注册或登录 + Source consts.LoginSource `json:"source"` // 登录来源 plugin: 插件 browser: 浏览器; 默认为 plugin + Kind consts.OAuthKind `json:"kind" query:"kind" validate:"required"` // invite: 邀请登录 login: 登录 SessionID string `json:"session_id"` // 会话ID Platform consts.UserPlatform `json:"platform" query:"platform" validate:"required"` // 第三方平台 dingtalk RedirectURL string `json:"redirect_url" query:"redirect_url"` // 登录成功后跳转的 URL diff --git a/backend/domain/user.go b/backend/domain/user.go index 7709307..8eaff63 100644 --- a/backend/domain/user.go +++ b/backend/domain/user.go @@ -29,7 +29,7 @@ type UserUsecase interface { GetSetting(ctx context.Context) (*Setting, error) UpdateSetting(ctx context.Context, req *UpdateSettingReq) (*Setting, error) OAuthSignUpOrIn(ctx context.Context, req *OAuthSignUpOrInReq) (*OAuthURLResp, error) - OAuthCallback(ctx context.Context, req *OAuthCallbackReq) (string, error) + OAuthCallback(ctx *web.Context, req *OAuthCallbackReq) error } type UserRepo interface { @@ -83,10 +83,11 @@ type VSCodeAuthInitResp struct { } type LoginReq struct { - SessionID string `json:"session_id"` // 会话Id - Username string `json:"username"` // 用户名 - Password string `json:"password"` // 密码 - IP string `json:"-"` // IP地址 + Source consts.LoginSource `json:"source"` // 登录来源 plugin: 插件 browser: 浏览器; 默认为 plugin + SessionID string `json:"session_id"` // 会话Id插件登录时必填 + Username string `json:"username"` // 用户名 + Password string `json:"password"` // 密码 + IP string `json:"-"` // IP地址 } type AdminLoginReq struct { @@ -95,7 +96,8 @@ type AdminLoginReq struct { } type LoginResp struct { - RedirectURL string `json:"redirect_url"` // 重定向URL + RedirectURL string `json:"redirect_url"` // 重定向URL + User *User `json:"user,omitempty"` // 用户信息 } type ListReq struct { diff --git a/backend/internal/user/handler/v1/user.go b/backend/internal/user/handler/v1/user.go index 019c894..4f14000 100644 --- a/backend/internal/user/handler/v1/user.go +++ b/backend/internal/user/handler/v1/user.go @@ -199,6 +199,11 @@ func (h *UserHandler) Login(c *web.Context, req domain.LoginReq) error { if err != nil { return err } + if req.Source == consts.LoginSourceBrowser { + if _, err := h.session.Save(c, consts.UserSessionName, c.Request().Host, resp.User); err != nil { + return err + } + } return c.Success(resp) } @@ -491,12 +496,7 @@ func (h *UserHandler) OAuthSignUpOrIn(ctx *web.Context, req domain.OAuthSignUpOr // @Router /api/v1/user/oauth/callback [get] func (h *UserHandler) OAuthCallback(ctx *web.Context, req domain.OAuthCallbackReq) error { req.IP = ctx.RealIP() - resp, err := h.usecase.OAuthCallback(ctx.Request().Context(), &req) - if err != nil { - return err - } - ctx.Redirect(http.StatusFound, resp) - return nil + return h.usecase.OAuthCallback(ctx, &req) } func (h *UserHandler) InitAdmin() error { diff --git a/backend/internal/user/repo/user.go b/backend/internal/user/repo/user.go index 08a57d6..8806f87 100644 --- a/backend/internal/user/repo/user.go +++ b/backend/internal/user/repo/user.go @@ -420,17 +420,20 @@ func (r *UserRepo) SaveUserLoginHistory(ctx context.Context, userID string, ip s if err != nil { return err } - _, err = r.db.UserLoginHistory.Create(). + c := r.db.UserLoginHistory.Create(). SetUserID(uid). SetIP(ip). SetCity(addr.City). SetCountry(addr.Country). - SetProvince(addr.Province). - SetClientVersion(session.Version). - SetOsType(session.OSType). - SetOsRelease(session.OSRelease). - SetClientID(session.ClientID). - SetHostname(session.Hostname). - Save(ctx) - return err + SetProvince(addr.Province) + + if session != nil { + c.SetClientVersion(session.Version). + SetOsType(session.OSType). + SetOsRelease(session.OSRelease). + SetClientID(session.ClientID). + SetHostname(session.Hostname) + } + + return c.Exec(ctx) } diff --git a/backend/internal/user/usecase/user.go b/backend/internal/user/usecase/user.go index 52c9db1..a9f12fa 100644 --- a/backend/internal/user/usecase/user.go +++ b/backend/internal/user/usecase/user.go @@ -6,6 +6,7 @@ import ( "errors" "fmt" "log/slog" + "net/http" "net/url" "time" @@ -23,13 +24,15 @@ import ( "github.com/chaitin/MonkeyCode/backend/errcode" "github.com/chaitin/MonkeyCode/backend/pkg/cvt" "github.com/chaitin/MonkeyCode/backend/pkg/oauth" + "github.com/chaitin/MonkeyCode/backend/pkg/session" ) type UserUsecase struct { - cfg *config.Config - redis *redis.Client - repo domain.UserRepo - logger *slog.Logger + cfg *config.Config + redis *redis.Client + repo domain.UserRepo + logger *slog.Logger + session *session.Session } func NewUserUsecase( @@ -37,12 +40,14 @@ func NewUserUsecase( redis *redis.Client, repo domain.UserRepo, logger *slog.Logger, + session *session.Session, ) domain.UserUsecase { u := &UserUsecase{ - cfg: cfg, - redis: redis, - repo: repo, - logger: logger, + cfg: cfg, + redis: redis, + repo: repo, + logger: logger, + session: session, } return u } @@ -205,22 +210,36 @@ func (u *UserUsecase) Login(ctx context.Context, req *domain.LoginReq) (*domain. return nil, errcode.ErrPassword.Wrap(err) } - apiKey, err := u.repo.GetOrCreateApiKey(ctx, user.ID.String()) - if err != nil { - return nil, err + switch req.Source { + case consts.LoginSourcePlugin: + apiKey, err := u.repo.GetOrCreateApiKey(ctx, user.ID.String()) + if err != nil { + return nil, err + } + + r, session, err := u.getVSCodeURL(ctx, req.SessionID, apiKey.Key, user.Username) + if err != nil { + return nil, err + } + + if err := u.repo.SaveUserLoginHistory(ctx, user.ID.String(), req.IP, session); err != nil { + u.logger.With("error", err).Error("save user login history") + } + return &domain.LoginResp{ + RedirectURL: r, + }, nil + + case consts.LoginSourceBrowser: + if err := u.repo.SaveUserLoginHistory(ctx, user.ID.String(), req.IP, nil); err != nil { + u.logger.With("error", err).Error("save user login history") + } + return &domain.LoginResp{ + RedirectURL: "", + User: cvt.From(user, &domain.User{}), + }, nil } - r, session, err := u.getVSCodeURL(ctx, req.SessionID, apiKey.Key, user.Username) - if err != nil { - return nil, err - } - - if err := u.repo.SaveUserLoginHistory(ctx, user.ID.String(), req.IP, session); err != nil { - u.logger.With("error", err).Error("save user login history") - } - return &domain.LoginResp{ - RedirectURL: r, - }, nil + return nil, fmt.Errorf("invalid login kind") } func (u *UserUsecase) getVSCodeURL(ctx context.Context, sessionID, apiKey, username string) (string, *domain.VSCodeSession, error) { @@ -455,6 +474,7 @@ func (u *UserUsecase) OAuthSignUpOrIn(ctx context.Context, req *domain.OAuthSign state, url := oauth.GetAuthorizeURL() session := &domain.OAuthState{ + Source: req.Source, SessionID: req.SessionID, Kind: req.OAuthKind(), Platform: req.Platform, @@ -474,35 +494,56 @@ func (u *UserUsecase) OAuthSignUpOrIn(ctx context.Context, req *domain.OAuthSign }, nil } -func (u *UserUsecase) OAuthCallback(ctx context.Context, req *domain.OAuthCallbackReq) (string, error) { +func (u *UserUsecase) OAuthCallback(c *web.Context, req *domain.OAuthCallbackReq) error { + ctx := c.Request().Context() b, err := u.redis.Get(ctx, fmt.Sprintf("oauth:state:%s", req.State)).Result() if err != nil { - return "", err + return err } var session domain.OAuthState if err := json.Unmarshal([]byte(b), &session); err != nil { - return "", err + return err } switch session.Kind { case consts.OAuthKindInvite: - return u.WithOAuthCallback(ctx, req, &session, func(ctx context.Context, s *domain.OAuthState, oui *domain.OAuthUserInfo) (*db.User, error) { + _, redirect, err := u.WithOAuthCallback(ctx, req, &session, func(ctx context.Context, s *domain.OAuthState, oui *domain.OAuthUserInfo) (*db.User, error) { return u.repo.OAuthRegister(ctx, s.Platform, s.InviteCode, oui) }) + if err != nil { + return err + } + c.Redirect(http.StatusFound, redirect) + return nil case consts.OAuthKindLogin: setting, err := u.repo.GetSetting(ctx) if err != nil { - return "", err + return err } - return u.WithOAuthCallback(ctx, req, &session, func(ctx context.Context, s *domain.OAuthState, oui *domain.OAuthUserInfo) (*db.User, error) { + user, redirect, err := u.WithOAuthCallback(ctx, req, &session, func(ctx context.Context, s *domain.OAuthState, oui *domain.OAuthUserInfo) (*db.User, error) { if setting.EnableAutoLogin { return u.repo.SignUpOrIn(ctx, s.Platform, oui) } return u.repo.OAuthLogin(ctx, s.Platform, oui) }) + if err != nil { + return err + } + + if session.Source == consts.LoginSourceBrowser { + resUser := cvt.From(user, &domain.User{}) + if _, err := u.session.Save(c, consts.UserSessionName, c.Request().Host, resUser); err != nil { + return err + } + return c.Success(resUser) + } + + c.Redirect(http.StatusFound, redirect) + return nil + default: - return "", errcode.ErrOAuthStateInvalid + return errcode.ErrOAuthStateInvalid } } @@ -530,28 +571,27 @@ func (u *UserUsecase) FetchUserInfo(ctx context.Context, req *domain.OAuthCallba type OAuthUserRepoHandle func(context.Context, *domain.OAuthState, *domain.OAuthUserInfo) (*db.User, error) -func (u *UserUsecase) WithOAuthCallback(ctx context.Context, req *domain.OAuthCallbackReq, session *domain.OAuthState, handle OAuthUserRepoHandle) (string, error) { +func (u *UserUsecase) WithOAuthCallback(ctx context.Context, req *domain.OAuthCallbackReq, session *domain.OAuthState, handle OAuthUserRepoHandle) (*db.User, string, error) { info, err := u.FetchUserInfo(ctx, req, session) if err != nil { - return "", err + return nil, "", err } user, err := handle(ctx, session, info) if err != nil { - return "", err - } - - apiKey, err := u.repo.GetOrCreateApiKey(ctx, user.ID.String()) - if err != nil { - return "", err + return nil, "", err } redirect := session.RedirectURL if session.SessionID != "" { + apiKey, err := u.repo.GetOrCreateApiKey(ctx, user.ID.String()) + if err != nil { + return nil, "", err + } r, vsess, err := u.getVSCodeURL(ctx, session.SessionID, apiKey.Key, user.Username) if err != nil { - return "", err + return nil, "", err } redirect = fmt.Sprintf("%s?redirect_url=%s", redirect, url.QueryEscape(r)) if err := u.repo.SaveUserLoginHistory(ctx, user.ID.String(), req.IP, vsess); err != nil { @@ -560,5 +600,5 @@ func (u *UserUsecase) WithOAuthCallback(ctx context.Context, req *domain.OAuthCa } u.logger.Debug("oauth callback", "redirect", redirect) - return redirect, nil + return user, redirect, nil } From 8ebaf582a1a76a269a53fbe427766fb7107dfee2 Mon Sep 17 00:00:00 2001 From: yokowu <18836617@qq.com> Date: Mon, 21 Jul 2025 12:24:00 +0800 Subject: [PATCH 2/3] =?UTF-8?q?feat:=20=E7=94=A8=E6=88=B7=E7=BB=9F?= =?UTF-8?q?=E8=AE=A1=E6=8E=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/cmd/server/wire_gen.go | 2 +- backend/docs/swagger.json | 279 +++++++++++++++++- backend/domain/dashboard.go | 8 +- backend/domain/user.go | 11 +- backend/internal/middleware/active.go | 2 +- backend/internal/middleware/auth.go | 29 +- .../internal/model/handler/http/v1/model.go | 4 +- backend/internal/user/handler/v1/dashboard.go | 70 +++++ backend/internal/user/handler/v1/user.go | 53 +++- backend/internal/user/repo/user.go | 4 +- backend/internal/user/usecase/user.go | 32 +- 11 files changed, 470 insertions(+), 24 deletions(-) create mode 100644 backend/internal/user/handler/v1/dashboard.go diff --git a/backend/cmd/server/wire_gen.go b/backend/cmd/server/wire_gen.go index d779603..7b7578e 100644 --- a/backend/cmd/server/wire_gen.go +++ b/backend/cmd/server/wire_gen.go @@ -75,9 +75,9 @@ func newServer() (*Server, error) { } userRepo := repo5.NewUserRepo(client, ipdbIPDB, redisClient) userUsecase := usecase4.NewUserUsecase(configConfig, redisClient, userRepo, slogLogger, sessionSession) - userHandler := v1_3.NewUserHandler(web, userUsecase, extensionUsecase, authMiddleware, activeMiddleware, sessionSession, slogLogger, configConfig) dashboardRepo := repo6.NewDashboardRepo(client) dashboardUsecase := usecase5.NewDashboardUsecase(dashboardRepo) + userHandler := v1_3.NewUserHandler(web, userUsecase, extensionUsecase, dashboardUsecase, authMiddleware, activeMiddleware, sessionSession, slogLogger, configConfig) dashboardHandler := v1_4.NewDashboardHandler(web, dashboardUsecase, authMiddleware, activeMiddleware) billingRepo := repo7.NewBillingRepo(client) billingUsecase := usecase6.NewBillingUsecase(billingRepo) diff --git a/backend/docs/swagger.json b/backend/docs/swagger.json index 2271db4..2b90143 100644 --- a/backend/docs/swagger.json +++ b/backend/docs/swagger.json @@ -1446,6 +1446,175 @@ "responses": {} } }, + "/api/v1/user/dashboard/events": { + "get": { + "description": "获取用户事件", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Dashboard" + ], + "summary": "获取用户事件", + "operationId": "user-dashboard-events", + "parameters": [ + { + "maximum": 90, + "minimum": 24, + "type": "integer", + "default": 90, + "description": "持续时间 (小时或天数)`", + "name": "duration", + "in": "query" + }, + { + "enum": [ + "hour", + "day" + ], + "type": "string", + "default": "day", + "description": "精度: \"hour\", \"day\"", + "name": "precision", + "in": "query", + "required": true + }, + { + "type": "string", + "description": "用户ID,可选参数", + "name": "user_id", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/web.Resp" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/domain.UserEvent" + } + } + } + } + ] + } + } + } + } + }, + "/api/v1/user/dashboard/heatmap": { + "get": { + "description": "用户热力图", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Dashboard" + ], + "summary": "用户热力图", + "operationId": "user-dashboard-heatmap", + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/web.Resp" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/domain.UserHeatmapResp" + } + } + } + ] + } + } + } + } + }, + "/api/v1/user/dashboard/stat": { + "get": { + "description": "获取用户统计信息", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Dashboard" + ], + "summary": "获取用户统计信息", + "operationId": "user-dashboard-stat", + "parameters": [ + { + "maximum": 90, + "minimum": 24, + "type": "integer", + "default": 90, + "description": "持续时间 (小时或天数)`", + "name": "duration", + "in": "query" + }, + { + "enum": [ + "hour", + "day" + ], + "type": "string", + "default": "day", + "description": "精度: \"hour\", \"day\"", + "name": "precision", + "in": "query", + "required": true + }, + { + "type": "string", + "description": "用户ID,可选参数", + "name": "user_id", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/web.Resp" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/domain.UserStat" + } + } + } + ] + } + } + } + } + }, "/api/v1/user/delete": { "delete": { "description": "删除用户", @@ -1823,6 +1992,87 @@ } } }, + "/api/v1/user/profile": { + "get": { + "description": "获取用户信息", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "User" + ], + "summary": "获取用户信息", + "operationId": "user-profile", + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/web.Resp" + }, + { + "type": "object", + "properties": { + "data": { + "type": "string" + } + } + } + ] + } + } + } + }, + "put": { + "description": "更新用户信息", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "User" + ], + "summary": "更新用户信息", + "operationId": "user-profile", + "parameters": [ + { + "description": "param", + "name": "req", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/domain.ProfileUpdateReq" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/web.Resp" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/domain.User" + } + } + } + ] + } + } + } + } + }, "/api/v1/user/register": { "post": { "description": "注册用户", @@ -3165,6 +3415,27 @@ } } }, + "domain.ProfileUpdateReq": { + "type": "object", + "properties": { + "avatar": { + "description": "头像", + "type": "string" + }, + "old_password": { + "description": "旧密码", + "type": "string" + }, + "password": { + "description": "密码", + "type": "string" + }, + "username": { + "description": "用户名", + "type": "string" + } + } + }, "domain.ProviderModel": { "type": "object", "properties": { @@ -3769,19 +4040,19 @@ } }, "total_accepted_per": { - "description": "近90天总接受率", + "description": "总接受率", "type": "number" }, "total_chats": { - "description": "近90天总对话任务数", + "description": "总对话任务数", "type": "integer" }, "total_completions": { - "description": "近90天总补全任务数", + "description": "总补全任务数", "type": "integer" }, "total_lines_of_code": { - "description": "近90天总代码行数", + "description": "总代码行数", "type": "integer" }, "work_mode": { diff --git a/backend/domain/dashboard.go b/backend/domain/dashboard.go index 99e1358..b96b1ba 100644 --- a/backend/domain/dashboard.go +++ b/backend/domain/dashboard.go @@ -77,10 +77,10 @@ func (u *UserCodeRank) From(d *db.Task) *UserCodeRank { } type UserStat struct { - TotalChats int64 `json:"total_chats"` // 近90天总对话任务数 - TotalCompletions int64 `json:"total_completions"` // 近90天总补全任务数 - TotalLinesOfCode int64 `json:"total_lines_of_code"` // 近90天总代码行数 - TotalAcceptedPer float64 `json:"total_accepted_per"` // 近90天总接受率 + TotalChats int64 `json:"total_chats"` // 总对话任务数 + TotalCompletions int64 `json:"total_completions"` // 总补全任务数 + TotalLinesOfCode int64 `json:"total_lines_of_code"` // 总代码行数 + TotalAcceptedPer float64 `json:"total_accepted_per"` // 总接受率 Chats []TimePoint[int64] `json:"chats"` // 对话任务数统计 Completions []TimePoint[int64] `json:"code_completions"` // 补全任务数统计 LinesOfCode []TimePoint[int64] `json:"lines_of_code"` // 代码行数统计 diff --git a/backend/domain/user.go b/backend/domain/user.go index 8eaff63..eb60e01 100644 --- a/backend/domain/user.go +++ b/backend/domain/user.go @@ -14,6 +14,7 @@ import ( type UserUsecase interface { Login(ctx context.Context, req *LoginReq) (*LoginResp, error) Update(ctx context.Context, req *UpdateUserReq) (*User, error) + ProfileUpdate(ctx context.Context, req *ProfileUpdateReq) (*User, error) Delete(ctx context.Context, id string) error InitAdmin(ctx context.Context) error AdminLogin(ctx context.Context, req *LoginReq) (*AdminUser, error) @@ -34,7 +35,7 @@ type UserUsecase interface { type UserRepo interface { List(ctx context.Context, page *web.Pagination) ([]*db.User, *db.PageInfo, error) - Update(ctx context.Context, id string, fn func(*db.UserUpdateOne) error) (*db.User, error) + Update(ctx context.Context, id string, fn func(*db.User, *db.UserUpdateOne) error) (*db.User, error) Delete(ctx context.Context, id string) error InitAdmin(ctx context.Context, username, password string) error CreateUser(ctx context.Context, user *db.User) (*db.User, error) @@ -57,6 +58,14 @@ type UserRepo interface { SaveUserLoginHistory(ctx context.Context, userID, ip string, session *VSCodeSession) error } +type ProfileUpdateReq struct { + UID string `json:"-"` + Username *string `json:"username"` // 用户名 + Password *string `json:"password"` // 密码 + OldPassword *string `json:"old_password"` // 旧密码 + Avatar *string `json:"avatar"` // 头像 +} + type UpdateUserReq struct { ID string `json:"id" validate:"required"` // 用户ID Status *consts.UserStatus `json:"status"` // 用户状态 active: 正常 locked: 锁定 inactive: 禁用 diff --git a/backend/internal/middleware/active.go b/backend/internal/middleware/active.go index e8365df..7717681 100644 --- a/backend/internal/middleware/active.go +++ b/backend/internal/middleware/active.go @@ -29,7 +29,7 @@ func (a *ActiveMiddleware) Active(scope string) echo.MiddlewareFunc { return func(c echo.Context) error { switch scope { case "admin": - if user := GetUser(c); user != nil { + if user := GetAdmin(c); user != nil { if err := a.redis.Set(context.Background(), fmt.Sprintf(consts.AdminActiveKeyFmt, user.ID), time.Now().Unix(), 0).Err(); err != nil { a.logger.With("error", err).ErrorContext(c.Request().Context(), "failed to set admin active status in Redis") } diff --git a/backend/internal/middleware/auth.go b/backend/internal/middleware/auth.go index c96b73d..45c9e3f 100644 --- a/backend/internal/middleware/auth.go +++ b/backend/internal/middleware/auth.go @@ -12,7 +12,8 @@ import ( ) const ( - userKey = "session:user" + adminKey = "session:admin" + userKey = "session:user" ) type AuthMiddleware struct { @@ -27,10 +28,10 @@ func NewAuthMiddleware(session *session.Session, logger *slog.Logger) *AuthMiddl } } -func (m *AuthMiddleware) Auth() echo.MiddlewareFunc { +func (m *AuthMiddleware) UserAuth() echo.MiddlewareFunc { return func(next echo.HandlerFunc) echo.HandlerFunc { return func(c echo.Context) error { - user, err := session.Get[domain.AdminUser](m.session, c, consts.SessionName) + user, err := session.Get[domain.User](m.session, c, consts.UserSessionName) if err != nil { m.logger.Error("auth failed", "error", err) return c.String(http.StatusUnauthorized, "Unauthorized") @@ -41,6 +42,24 @@ func (m *AuthMiddleware) Auth() echo.MiddlewareFunc { } } -func GetUser(c echo.Context) *domain.AdminUser { - return c.Get(userKey).(*domain.AdminUser) +func (m *AuthMiddleware) Auth() echo.MiddlewareFunc { + return func(next echo.HandlerFunc) echo.HandlerFunc { + return func(c echo.Context) error { + user, err := session.Get[domain.AdminUser](m.session, c, consts.SessionName) + if err != nil { + m.logger.Error("auth failed", "error", err) + return c.String(http.StatusUnauthorized, "Unauthorized") + } + c.Set(adminKey, &user) + return next(c) + } + } +} + +func GetAdmin(c echo.Context) *domain.AdminUser { + return c.Get(adminKey).(*domain.AdminUser) +} + +func GetUser(c echo.Context) *domain.User { + return c.Get(userKey).(*domain.User) } diff --git a/backend/internal/model/handler/http/v1/model.go b/backend/internal/model/handler/http/v1/model.go index 8a67b06..e8fb9ef 100644 --- a/backend/internal/model/handler/http/v1/model.go +++ b/backend/internal/model/handler/http/v1/model.go @@ -88,7 +88,7 @@ func (h *ModelHandler) List(c *web.Context) error { // @Success 200 {object} web.Resp{data=[]domain.Model} // @Router /api/v1/model/my [get] func (h *ModelHandler) MyModelList(c *web.Context, req domain.MyModelListReq) error { - user := middleware.GetUser(c) + user := middleware.GetAdmin(c) req.UserID = user.ID models, err := h.usecase.MyModelList(c.Request().Context(), &req) if err != nil { @@ -109,7 +109,7 @@ func (h *ModelHandler) MyModelList(c *web.Context, req domain.MyModelListReq) er // @Success 200 {object} web.Resp{data=domain.Model} // @Router /api/v1/model [post] func (h *ModelHandler) Create(c *web.Context, req domain.CreateModelReq) error { - user := middleware.GetUser(c) + user := middleware.GetAdmin(c) req.UserID = user.ID m, err := h.usecase.Create(c.Request().Context(), &req) if err != nil { diff --git a/backend/internal/user/handler/v1/dashboard.go b/backend/internal/user/handler/v1/dashboard.go new file mode 100644 index 0000000..1dce5a8 --- /dev/null +++ b/backend/internal/user/handler/v1/dashboard.go @@ -0,0 +1,70 @@ +package v1 + +import ( + "github.com/GoYoko/web" + + "github.com/chaitin/MonkeyCode/backend/domain" + "github.com/chaitin/MonkeyCode/backend/internal/middleware" +) + +// UserStat 获取用户统计信息 +// +// @Tags Dashboard +// @Summary 获取用户统计信息 +// @Description 获取用户统计信息 +// @ID user-dashboard-stat +// @Accept json +// @Produce json +// @Param filter query domain.StatisticsFilter true "筛选参数" +// @Success 200 {object} web.Resp{data=domain.UserStat} +// @Router /api/v1/user/dashboard/stat [get] +func (h *UserHandler) UserStat(c *web.Context, req domain.StatisticsFilter) error { + req.UserID = middleware.GetUser(c).ID + userStat, err := h.duse.UserStat(c.Request().Context(), req) + if err != nil { + return err + } + return c.Success(userStat) +} + +// UserEvents 获取用户事件 +// +// @Tags Dashboard +// @Summary 获取用户事件 +// @Description 获取用户事件 +// @ID user-dashboard-events +// @Accept json +// @Produce json +// @Param filter query domain.StatisticsFilter true "筛选参数" +// @Success 200 {object} web.Resp{data=[]domain.UserEvent} +// @Router /api/v1/user/dashboard/events [get] +func (h *UserHandler) UserEvents(c *web.Context) error { + userEvents, err := h.duse.UserEvents(c.Request().Context(), domain.StatisticsFilter{ + Precision: "day", + Duration: 90, + UserID: middleware.GetUser(c).ID, + }) + if err != nil { + return err + } + return c.Success(userEvents) +} + +// UserHeatmap 用户热力图 +// +// @Tags Dashboard +// @Summary 用户热力图 +// @Description 用户热力图 +// @ID user-dashboard-heatmap +// @Accept json +// @Produce json +// @Success 200 {object} web.Resp{data=domain.UserHeatmapResp} +// @Router /api/v1/user/dashboard/heatmap [get] +func (h *UserHandler) UserHeatmap(c *web.Context) error { + userID := middleware.GetUser(c).ID + userHeatmap, err := h.duse.UserHeatmap(c.Request().Context(), userID) + if err != nil { + return err + } + return c.Success(userHeatmap) +} diff --git a/backend/internal/user/handler/v1/user.go b/backend/internal/user/handler/v1/user.go index 4f14000..3148e0c 100644 --- a/backend/internal/user/handler/v1/user.go +++ b/backend/internal/user/handler/v1/user.go @@ -31,6 +31,7 @@ type CacheEntry struct { type UserHandler struct { usecase domain.UserUsecase euse domain.ExtensionUsecase + duse domain.DashboardUsecase session *session.Session logger *slog.Logger cfg *config.Config @@ -43,6 +44,7 @@ func NewUserHandler( w *web.Web, usecase domain.UserUsecase, euse domain.ExtensionUsecase, + duse domain.DashboardUsecase, auth *middleware.AuthMiddleware, active *middleware.ActiveMiddleware, session *session.Session, @@ -51,10 +53,11 @@ func NewUserHandler( ) *UserHandler { u := &UserHandler{ usecase: usecase, + euse: euse, + duse: duse, session: session, logger: logger, cfg: cfg, - euse: euse, vsixCache: make(map[string]*CacheEntry), limiter: rate.NewLimiter(rate.Every(time.Duration(cfg.Extension.LimitSecond)*time.Second), cfg.Extension.Limit), } @@ -82,6 +85,9 @@ func NewUserHandler( g.POST("/register", web.BindHandler(u.Register)) g.POST("/login", web.BindHandler(u.Login)) + g.GET("/profile", web.BaseHandler(u.Profile), auth.UserAuth()) + g.PUT("/profile", web.BindHandler(u.UpdateProfile), auth.UserAuth()) + g.Use(auth.Auth(), active.Active("admin")) g.PUT("/update", web.BindHandler(u.Update)) @@ -90,6 +96,13 @@ func NewUserHandler( g.GET("/list", web.BindHandler(u.List, web.WithPage())) g.GET("/login-history", web.BaseHandler(u.LoginHistory, web.WithPage())) + // user dashboard + d := w.Group("/api/v1/user/dashboard") + d.Use(auth.UserAuth(), active.Active("user")) + d.GET("/stat", web.BindHandler(u.UserStat)) + d.GET("/events", web.BaseHandler(u.UserEvents)) + d.GET("/heatmap", web.BaseHandler(u.UserHeatmap)) + return u } @@ -338,7 +351,7 @@ func (h *UserHandler) LoginHistory(c *web.Context) error { // @Success 200 {object} web.Resp{data=domain.InviteResp} // @Router /api/v1/user/invite [get] func (h *UserHandler) Invite(c *web.Context) error { - user := middleware.GetUser(c) + user := middleware.GetAdmin(c) resp, err := h.usecase.Invite(c.Request().Context(), user.ID) if err != nil { return err @@ -378,7 +391,7 @@ func (h *UserHandler) Register(c *web.Context, req domain.RegisterReq) error { // @Success 200 {object} web.Resp{data=domain.AdminUser} // @Router /api/v1/admin/create [post] func (h *UserHandler) CreateAdmin(c *web.Context, req domain.CreateAdminReq) error { - user := middleware.GetUser(c) + user := middleware.GetAdmin(c) if user.Username != "admin" { return errcode.ErrPermission } @@ -499,6 +512,40 @@ func (h *UserHandler) OAuthCallback(ctx *web.Context, req domain.OAuthCallbackRe return h.usecase.OAuthCallback(ctx, &req) } +// Profile 获取用户信息 +// +// @Tags User +// @Summary 获取用户信息 +// @Description 获取用户信息 +// @ID user-profile +// @Accept json +// @Produce json +// @Success 200 {object} web.Resp{data=string} +// @Router /api/v1/user/profile [get] +func (h *UserHandler) Profile(ctx *web.Context) error { + return ctx.Success(middleware.GetUser(ctx)) +} + +// UpdateProfile 更新用户信息 +// +// @Tags User +// @Summary 更新用户信息 +// @Description 更新用户信息 +// @ID user-profile +// @Accept json +// @Produce json +// @Param req body domain.ProfileUpdateReq true "param" +// @Success 200 {object} web.Resp{data=domain.User} +// @Router /api/v1/user/profile [put] +func (h *UserHandler) UpdateProfile(ctx *web.Context, req domain.ProfileUpdateReq) error { + req.UID = middleware.GetUser(ctx).ID + user, err := h.usecase.ProfileUpdate(ctx.Request().Context(), &req) + if err != nil { + return err + } + return ctx.Success(user) +} + func (h *UserHandler) InitAdmin() error { return h.usecase.InitAdmin(context.Background()) } diff --git a/backend/internal/user/repo/user.go b/backend/internal/user/repo/user.go index 8806f87..908c0db 100644 --- a/backend/internal/user/repo/user.go +++ b/backend/internal/user/repo/user.go @@ -221,7 +221,7 @@ func (r *UserRepo) UpdateSetting(ctx context.Context, fn func(*db.Setting, *db.S return res, err } -func (r *UserRepo) Update(ctx context.Context, id string, fn func(*db.UserUpdateOne) error) (*db.User, error) { +func (r *UserRepo) Update(ctx context.Context, id string, fn func(*db.User, *db.UserUpdateOne) error) (*db.User, error) { uid, err := uuid.Parse(id) if err != nil { return nil, err @@ -233,7 +233,7 @@ func (r *UserRepo) Update(ctx context.Context, id string, fn func(*db.UserUpdate if err != nil { return err } - if err := fn(u.Update()); err != nil { + if err := fn(u, u.Update()); err != nil { return err } return u.Update().Exec(ctx) diff --git a/backend/internal/user/usecase/user.go b/backend/internal/user/usecase/user.go index a9f12fa..4577788 100644 --- a/backend/internal/user/usecase/user.go +++ b/backend/internal/user/usecase/user.go @@ -395,7 +395,7 @@ func (u *UserUsecase) UpdateSetting(ctx context.Context, req *domain.UpdateSetti } func (u *UserUsecase) Update(ctx context.Context, req *domain.UpdateUserReq) (*domain.User, error) { - user, err := u.repo.Update(ctx, req.ID, func(u *db.UserUpdateOne) error { + user, err := u.repo.Update(ctx, req.ID, func(_ *db.User, u *db.UserUpdateOne) error { if req.Status != nil { u.SetStatus(*req.Status) } @@ -602,3 +602,33 @@ func (u *UserUsecase) WithOAuthCallback(ctx context.Context, req *domain.OAuthCa u.logger.Debug("oauth callback", "redirect", redirect) return user, redirect, nil } + +func (u *UserUsecase) ProfileUpdate(ctx context.Context, req *domain.ProfileUpdateReq) (*domain.User, error) { + user, err := u.repo.Update(ctx, req.UID, func(old *db.User, uuo *db.UserUpdateOne) error { + if req.Avatar != nil { + uuo.SetAvatarURL(*req.Avatar) + } + + if req.Username != nil { + uuo.SetUsername(*req.Username) + } + + if req.Password != nil && req.OldPassword != nil { + if err := bcrypt.CompareHashAndPassword([]byte(old.Password), []byte(*req.OldPassword)); err != nil { + return errcode.ErrPassword.Wrap(err) + } + + hash, err := bcrypt.GenerateFromPassword([]byte(*req.Password), bcrypt.DefaultCost) + if err != nil { + return errcode.ErrPassword.Wrap(err) + } + uuo.SetPassword(string(hash)) + } + + return nil + }) + if err != nil { + return nil, err + } + return cvt.From(user, &domain.User{}), nil +} From 20c0da6fb03acd19edfeedc8815aef12c94233ee Mon Sep 17 00:00:00 2001 From: yokowu <18836617@qq.com> Date: Mon, 21 Jul 2025 15:04:03 +0800 Subject: [PATCH 3/3] =?UTF-8?q?feat:=20=E7=94=A8=E6=88=B7=E4=BD=BF?= =?UTF-8?q?=E7=94=A8=E8=AE=B0=E5=BD=95=E6=8E=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/cmd/server/wire_gen.go | 4 +- backend/docs/swagger.json | 326 +++++++++++++++++- backend/domain/billing.go | 9 +- backend/domain/oauth.go | 10 +- backend/domain/user.go | 10 +- backend/go.mod | 2 +- backend/go.sum | 4 +- .../billing/handler/http/v1/billing.go | 4 +- backend/internal/billing/repo/billing.go | 37 +- backend/internal/billing/usecase/billing.go | 8 +- backend/internal/middleware/active.go | 6 + backend/internal/middleware/auth.go | 12 +- backend/internal/middleware/proxy.go | 6 +- backend/internal/openai/handler/v1/v1.go | 10 +- backend/internal/user/handler/v1/dashboard.go | 9 +- backend/internal/user/handler/v1/record.go | 91 +++++ backend/internal/user/handler/v1/user.go | 25 +- backend/internal/user/usecase/user.go | 6 +- 18 files changed, 520 insertions(+), 59 deletions(-) create mode 100644 backend/internal/user/handler/v1/record.go diff --git a/backend/cmd/server/wire_gen.go b/backend/cmd/server/wire_gen.go index 7b7578e..f3355bd 100644 --- a/backend/cmd/server/wire_gen.go +++ b/backend/cmd/server/wire_gen.go @@ -77,10 +77,10 @@ func newServer() (*Server, error) { userUsecase := usecase4.NewUserUsecase(configConfig, redisClient, userRepo, slogLogger, sessionSession) dashboardRepo := repo6.NewDashboardRepo(client) dashboardUsecase := usecase5.NewDashboardUsecase(dashboardRepo) - userHandler := v1_3.NewUserHandler(web, userUsecase, extensionUsecase, dashboardUsecase, authMiddleware, activeMiddleware, sessionSession, slogLogger, configConfig) - dashboardHandler := v1_4.NewDashboardHandler(web, dashboardUsecase, authMiddleware, activeMiddleware) billingRepo := repo7.NewBillingRepo(client) billingUsecase := usecase6.NewBillingUsecase(billingRepo) + userHandler := v1_3.NewUserHandler(web, userUsecase, extensionUsecase, dashboardUsecase, billingUsecase, authMiddleware, activeMiddleware, sessionSession, slogLogger, configConfig) + dashboardHandler := v1_4.NewDashboardHandler(web, dashboardUsecase, authMiddleware, activeMiddleware) billingHandler := v1_5.NewBillingHandler(web, billingUsecase, authMiddleware, activeMiddleware) server := &Server{ config: configConfig, diff --git a/backend/docs/swagger.json b/backend/docs/swagger.json index 2b90143..4a047b5 100644 --- a/backend/docs/swagger.json +++ b/backend/docs/swagger.json @@ -1446,6 +1446,280 @@ "responses": {} } }, + "/api/v1/user/chat/info": { + "get": { + "description": "获取对话内容", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "User Record" + ], + "summary": "获取对话内容", + "operationId": "user-chat-info", + "parameters": [ + { + "type": "string", + "description": "对话记录ID", + "name": "id", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/web.Resp" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/domain.ChatInfo" + } + } + } + ] + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "type": "string" + } + } + } + } + }, + "/api/v1/user/chat/record": { + "get": { + "description": "获取用户对话记录", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "User Record" + ], + "summary": "获取用户对话记录", + "operationId": "user-list-chat-record", + "parameters": [ + { + "type": "string", + "description": "作者", + "name": "author", + "in": "query" + }, + { + "type": "boolean", + "description": "是否接受筛选", + "name": "is_accept", + "in": "query" + }, + { + "type": "string", + "description": "语言", + "name": "language", + "in": "query" + }, + { + "type": "string", + "description": "下一页标识", + "name": "next_token", + "in": "query" + }, + { + "type": "integer", + "description": "分页", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "每页多少条记录", + "name": "size", + "in": "query" + }, + { + "type": "string", + "description": "工作模式", + "name": "work_mode", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/web.Resp" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/domain.ListChatRecordResp" + } + } + } + ] + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "type": "string" + } + } + } + } + }, + "/api/v1/user/completion/info": { + "get": { + "description": "获取补全内容", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "User Record" + ], + "summary": "获取补全内容", + "operationId": "user-completion-info", + "parameters": [ + { + "type": "string", + "description": "补全记录ID", + "name": "id", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/web.Resp" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/domain.CompletionInfo" + } + } + } + ] + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "type": "string" + } + } + } + } + }, + "/api/v1/user/completion/record": { + "get": { + "description": "获取补全记录", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "User Record" + ], + "summary": "获取补全记录", + "operationId": "user-list-completion-record", + "parameters": [ + { + "type": "string", + "description": "作者", + "name": "author", + "in": "query" + }, + { + "type": "boolean", + "description": "是否接受筛选", + "name": "is_accept", + "in": "query" + }, + { + "type": "string", + "description": "语言", + "name": "language", + "in": "query" + }, + { + "type": "string", + "description": "下一页标识", + "name": "next_token", + "in": "query" + }, + { + "type": "integer", + "description": "分页", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "每页多少条记录", + "name": "size", + "in": "query" + }, + { + "type": "string", + "description": "工作模式", + "name": "work_mode", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/web.Resp" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/domain.ListCompletionRecordResp" + } + } + } + ] + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "type": "string" + } + } + } + } + }, "/api/v1/user/dashboard/events": { "get": { "description": "获取用户事件", @@ -1456,7 +1730,7 @@ "application/json" ], "tags": [ - "Dashboard" + "User Dashboard" ], "summary": "获取用户事件", "operationId": "user-dashboard-events", @@ -1510,6 +1784,12 @@ } ] } + }, + "401": { + "description": "Unauthorized", + "schema": { + "type": "string" + } } } } @@ -1524,7 +1804,7 @@ "application/json" ], "tags": [ - "Dashboard" + "User Dashboard" ], "summary": "用户热力图", "operationId": "user-dashboard-heatmap", @@ -1546,6 +1826,12 @@ } ] } + }, + "401": { + "description": "Unauthorized", + "schema": { + "type": "string" + } } } } @@ -1560,7 +1846,7 @@ "application/json" ], "tags": [ - "Dashboard" + "User Dashboard" ], "summary": "获取用户统计信息", "operationId": "user-dashboard-stat", @@ -1611,6 +1897,12 @@ } ] } + }, + "401": { + "description": "Unauthorized", + "schema": { + "type": "string" + } } } } @@ -1961,13 +2253,15 @@ "browser" ], "type": "string", + "default": "plugin", "x-enum-varnames": [ "LoginSourcePlugin", "LoginSourceBrowser" ], "description": "登录来源 plugin: 插件 browser: 浏览器; 默认为 plugin", "name": "source", - "in": "query" + "in": "query", + "required": true } ], "responses": { @@ -2002,7 +2296,7 @@ "application/json" ], "tags": [ - "User" + "User Manage" ], "summary": "获取用户信息", "operationId": "user-profile", @@ -2018,12 +2312,18 @@ "type": "object", "properties": { "data": { - "type": "string" + "$ref": "#/definitions/domain.User" } } } ] } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/web.Resp" + } } } }, @@ -2036,10 +2336,10 @@ "application/json" ], "tags": [ - "User" + "User Manage" ], "summary": "更新用户信息", - "operationId": "user-profile", + "operationId": "user-update-profile", "parameters": [ { "description": "param", @@ -2069,6 +2369,12 @@ } ] } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/web.Resp" + } } } } @@ -3163,6 +3469,9 @@ }, "domain.LoginReq": { "type": "object", + "required": [ + "source" + ], "properties": { "password": { "description": "密码", @@ -3174,6 +3483,7 @@ }, "source": { "description": "登录来源 plugin: 插件 browser: 浏览器; 默认为 plugin", + "default": "plugin", "allOf": [ { "$ref": "#/definitions/consts.LoginSource" diff --git a/backend/domain/billing.go b/backend/domain/billing.go index a3346df..c412b1b 100644 --- a/backend/domain/billing.go +++ b/backend/domain/billing.go @@ -13,19 +13,20 @@ import ( type BillingUsecase interface { ListChatRecord(ctx context.Context, req ListRecordReq) (*ListChatRecordResp, error) ListCompletionRecord(ctx context.Context, req ListRecordReq) (*ListCompletionRecordResp, error) - CompletionInfo(ctx context.Context, id string) (*CompletionInfo, error) - ChatInfo(ctx context.Context, id string) (*ChatInfo, error) + CompletionInfo(ctx context.Context, id, userID string) (*CompletionInfo, error) + ChatInfo(ctx context.Context, id, userID string) (*ChatInfo, error) } type BillingRepo interface { ListChatRecord(ctx context.Context, req ListRecordReq) (*ListChatRecordResp, error) ListCompletionRecord(ctx context.Context, req ListRecordReq) (*ListCompletionRecordResp, error) - CompletionInfo(ctx context.Context, id string) (*CompletionInfo, error) - ChatInfo(ctx context.Context, id string) (*ChatInfo, error) + CompletionInfo(ctx context.Context, id, userID string) (*CompletionInfo, error) + ChatInfo(ctx context.Context, id, userID string) (*ChatInfo, error) } type ListRecordReq struct { *web.Pagination + UserID string `json:"-"` Author string `json:"author" query:"author"` // 作者 Language string `json:"language" query:"language"` // 语言 WorkMode string `json:"work_mode" query:"work_mode"` // 工作模式 diff --git a/backend/domain/oauth.go b/backend/domain/oauth.go index cb921bd..8502d28 100644 --- a/backend/domain/oauth.go +++ b/backend/domain/oauth.go @@ -32,11 +32,11 @@ type OAuthUserInfo struct { } type OAuthSignUpOrInReq struct { - Source consts.LoginSource `json:"source"` // 登录来源 plugin: 插件 browser: 浏览器; 默认为 plugin - Platform consts.UserPlatform `json:"platform" query:"platform" validate:"required"` // 第三方平台 dingtalk - SessionID string `json:"session_id" query:"session_id"` // 会话ID - RedirectURL string `json:"redirect_url" query:"redirect_url"` // 登录成功后跳转的 URL - InviteCode string `json:"inviate_code" query:"inviate_code"` // 邀请码 + Source consts.LoginSource `json:"source" query:"source" validate:"required" default:"plugin"` // 登录来源 plugin: 插件 browser: 浏览器; 默认为 plugin + Platform consts.UserPlatform `json:"platform" query:"platform" validate:"required"` // 第三方平台 dingtalk + SessionID string `json:"session_id" query:"session_id"` // 会话ID + RedirectURL string `json:"redirect_url" query:"redirect_url"` // 登录成功后跳转的 URL + InviteCode string `json:"inviate_code" query:"inviate_code"` // 邀请码 } func (o OAuthSignUpOrInReq) OAuthKind() consts.OAuthKind { diff --git a/backend/domain/user.go b/backend/domain/user.go index eb60e01..3d3686e 100644 --- a/backend/domain/user.go +++ b/backend/domain/user.go @@ -92,11 +92,11 @@ type VSCodeAuthInitResp struct { } type LoginReq struct { - Source consts.LoginSource `json:"source"` // 登录来源 plugin: 插件 browser: 浏览器; 默认为 plugin - SessionID string `json:"session_id"` // 会话Id插件登录时必填 - Username string `json:"username"` // 用户名 - Password string `json:"password"` // 密码 - IP string `json:"-"` // IP地址 + Source consts.LoginSource `json:"source" validate:"required" default:"plugin"` // 登录来源 plugin: 插件 browser: 浏览器; 默认为 plugin + SessionID string `json:"session_id"` // 会话Id插件登录时必填 + Username string `json:"username"` // 用户名 + Password string `json:"password"` // 密码 + IP string `json:"-"` // IP地址 } type AdminLoginReq struct { diff --git a/backend/go.mod b/backend/go.mod index 118ffea..0cc5469 100644 --- a/backend/go.mod +++ b/backend/go.mod @@ -4,7 +4,7 @@ go 1.23.7 require ( entgo.io/ent v0.14.4 - github.com/GoYoko/web v1.0.0 + github.com/GoYoko/web v1.1.0 github.com/cloudwego/eino v0.3.51 github.com/cloudwego/eino-ext/components/model/openai v0.0.0-20250710065240-482d48888f25 github.com/golang-migrate/migrate/v4 v4.18.3 diff --git a/backend/go.sum b/backend/go.sum index 4b55356..3fbe0f2 100644 --- a/backend/go.sum +++ b/backend/go.sum @@ -8,8 +8,8 @@ github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= github.com/DATA-DOG/go-sqlmock v1.5.0 h1:Shsta01QNfFxHCfpW6YH2STWB0MudeXXEWMr20OEh60= github.com/DATA-DOG/go-sqlmock v1.5.0/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM= -github.com/GoYoko/web v1.0.0 h1:kcNxz8BvpKavE0/iqatOmUeCXVghaoD5xYDiHDulVaE= -github.com/GoYoko/web v1.0.0/go.mod h1:DL9/gvuUG2jcBE1XUIY+9QBrrhdshzPEdxMCzR9jUHo= +github.com/GoYoko/web v1.1.0 h1:nIbtol5z0Y03d0nHsvGjv+W0fgmFRGUL8fzPN3kmrOY= +github.com/GoYoko/web v1.1.0/go.mod h1:DL9/gvuUG2jcBE1XUIY+9QBrrhdshzPEdxMCzR9jUHo= github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= github.com/agext/levenshtein v1.2.3 h1:YB2fHEn0UJagG8T1rrWknE3ZQzWM06O8AMAatNn7lmo= diff --git a/backend/internal/billing/handler/http/v1/billing.go b/backend/internal/billing/handler/http/v1/billing.go index 1ab76ae..ce27028 100644 --- a/backend/internal/billing/handler/http/v1/billing.go +++ b/backend/internal/billing/handler/http/v1/billing.go @@ -84,7 +84,7 @@ func (h *BillingHandler) ListCompletionRecord(c *web.Context, req domain.ListRec // @Success 200 {object} web.Resp{data=domain.CompletionInfo} // @Router /api/v1/billing/completion/info [get] func (h *BillingHandler) CompletionInfo(c *web.Context) error { - info, err := h.usecase.CompletionInfo(c.Request().Context(), c.QueryParam("id")) + info, err := h.usecase.CompletionInfo(c.Request().Context(), c.QueryParam("id"), "") if err != nil { return err } @@ -103,7 +103,7 @@ func (h *BillingHandler) CompletionInfo(c *web.Context) error { // @Success 200 {object} web.Resp{data=domain.ChatInfo} // @Router /api/v1/billing/chat/info [get] func (h *BillingHandler) ChatInfo(c *web.Context) error { - info, err := h.usecase.ChatInfo(c.Request().Context(), c.QueryParam("id")) + info, err := h.usecase.ChatInfo(c.Request().Context(), c.QueryParam("id"), "") if err != nil { return err } diff --git a/backend/internal/billing/repo/billing.go b/backend/internal/billing/repo/billing.go index 0ea936f..8e7c7c9 100644 --- a/backend/internal/billing/repo/billing.go +++ b/backend/internal/billing/repo/billing.go @@ -13,6 +13,7 @@ import ( "github.com/chaitin/MonkeyCode/backend/domain" "github.com/chaitin/MonkeyCode/backend/pkg/cvt" "github.com/chaitin/MonkeyCode/backend/pkg/entx" + "github.com/google/uuid" ) type BillingRepo struct { @@ -24,14 +25,21 @@ func NewBillingRepo(db *db.Client) domain.BillingRepo { } // ChatInfo implements domain.BillingRepo. -func (b *BillingRepo) ChatInfo(ctx context.Context, id string) (*domain.ChatInfo, error) { - record, err := b.db.Task.Query(). +func (b *BillingRepo) ChatInfo(ctx context.Context, id, userID string) (*domain.ChatInfo, error) { + q := b.db.Task.Query(). WithTaskRecords(func(trq *db.TaskRecordQuery) { trq.Order(taskrecord.ByCreatedAt(sql.OrderAsc())) trq.Where(taskrecord.RoleNEQ(consts.ChatRoleSystem)) }). - Where(task.TaskID(id)). - First(ctx) + Where(task.TaskID(id)) + if userID != "" { + uid, err := uuid.Parse(userID) + if err != nil { + return nil, err + } + q.Where(task.UserID(uid)) + } + record, err := q.First(ctx) if err != nil { return nil, err } @@ -40,13 +48,20 @@ func (b *BillingRepo) ChatInfo(ctx context.Context, id string) (*domain.ChatInfo } // CompletionInfo implements domain.BillingRepo. -func (b *BillingRepo) CompletionInfo(ctx context.Context, id string) (*domain.CompletionInfo, error) { - record, err := b.db.Task.Query(). +func (b *BillingRepo) CompletionInfo(ctx context.Context, id, userID string) (*domain.CompletionInfo, error) { + q := b.db.Task.Query(). WithTaskRecords(func(trq *db.TaskRecordQuery) { trq.Order(taskrecord.ByCreatedAt(sql.OrderAsc())) }). - Where(task.TaskID(id)). - First(ctx) + Where(task.TaskID(id)) + if userID != "" { + uid, err := uuid.Parse(userID) + if err != nil { + return nil, err + } + q.Where(task.UserID(uid)) + } + record, err := q.First(ctx) if err != nil { return nil, err } @@ -86,6 +101,12 @@ func filterTask(q *db.TaskQuery, req domain.ListRecordReq) { q.Where(task.IsAccept(*req.IsAccept)) } + if req.UserID != "" { + if uid, err := uuid.Parse(req.UserID); err == nil { + q.Where(task.UserID(uid)) + } + } + if req.Author != "" { q.Where(task.HasUserWith(func(s *sql.Selector) { s.Where(sql.Like(s.C(user.FieldUsername), "%"+req.Author+"%")) diff --git a/backend/internal/billing/usecase/billing.go b/backend/internal/billing/usecase/billing.go index 7aa7599..7d6b75c 100644 --- a/backend/internal/billing/usecase/billing.go +++ b/backend/internal/billing/usecase/billing.go @@ -25,11 +25,11 @@ func (b *BillingUsecase) ListCompletionRecord(ctx context.Context, req domain.Li } // CompletionInfo implements domain.BillingUsecase. -func (b *BillingUsecase) CompletionInfo(ctx context.Context, id string) (*domain.CompletionInfo, error) { - return b.repo.CompletionInfo(ctx, id) +func (b *BillingUsecase) CompletionInfo(ctx context.Context, id, userID string) (*domain.CompletionInfo, error) { + return b.repo.CompletionInfo(ctx, id, userID) } // ChatInfo implements domain.BillingUsecase. -func (b *BillingUsecase) ChatInfo(ctx context.Context, id string) (*domain.ChatInfo, error) { - return b.repo.ChatInfo(ctx, id) +func (b *BillingUsecase) ChatInfo(ctx context.Context, id, userID string) (*domain.ChatInfo, error) { + return b.repo.ChatInfo(ctx, id, userID) } diff --git a/backend/internal/middleware/active.go b/backend/internal/middleware/active.go index 7717681..ba58bdb 100644 --- a/backend/internal/middleware/active.go +++ b/backend/internal/middleware/active.go @@ -35,6 +35,12 @@ func (a *ActiveMiddleware) Active(scope string) echo.MiddlewareFunc { } } case "user": + if user := GetUser((c)); user != nil { + if err := a.redis.Set(context.Background(), fmt.Sprintf(consts.UserActiveKeyFmt, user.ID), time.Now().Unix(), 0).Err(); err != nil { + a.logger.With("error", err).ErrorContext(c.Request().Context(), "failed to set user active status in Redis") + } + } + case "apikey": if apikey := GetApiKey(c); apikey != nil { if err := a.redis.Set(context.Background(), fmt.Sprintf(consts.UserActiveKeyFmt, apikey.UserID), time.Now().Unix(), 0).Err(); err != nil { a.logger.With("error", err).ErrorContext(c.Request().Context(), "failed to set user active status in Redis") diff --git a/backend/internal/middleware/auth.go b/backend/internal/middleware/auth.go index 45c9e3f..61fc56b 100644 --- a/backend/internal/middleware/auth.go +++ b/backend/internal/middleware/auth.go @@ -57,9 +57,17 @@ func (m *AuthMiddleware) Auth() echo.MiddlewareFunc { } func GetAdmin(c echo.Context) *domain.AdminUser { - return c.Get(adminKey).(*domain.AdminUser) + i := c.Get(adminKey) + if i == nil { + return nil + } + return i.(*domain.AdminUser) } func GetUser(c echo.Context) *domain.User { - return c.Get(userKey).(*domain.User) + i := c.Get(userKey) + if i == nil { + return nil + } + return i.(*domain.User) } diff --git a/backend/internal/middleware/proxy.go b/backend/internal/middleware/proxy.go index f8a42a0..ee800df 100644 --- a/backend/internal/middleware/proxy.go +++ b/backend/internal/middleware/proxy.go @@ -53,5 +53,9 @@ func (p *ProxyMiddleware) Auth() echo.MiddlewareFunc { } func GetApiKey(c echo.Context) *domain.ApiKey { - return c.Get(ApiContextKey).(*domain.ApiKey) + i := c.Get(ApiContextKey) + if i == nil { + return nil + } + return i.(*domain.ApiKey) } diff --git a/backend/internal/openai/handler/v1/v1.go b/backend/internal/openai/handler/v1/v1.go index 46208bf..6a40cb1 100644 --- a/backend/internal/openai/handler/v1/v1.go +++ b/backend/internal/openai/handler/v1/v1.go @@ -49,11 +49,11 @@ func NewV1Handler( g := w.Group("/v1", middleware.Auth()) g.GET("/models", web.BaseHandler(h.ModelList)) - g.POST("/completion/accept", web.BindHandler(h.AcceptCompletion), active.Active("user")) - g.POST("/report", web.BindHandler(h.Report), active.Active("user")) - g.POST("/chat/completions", web.BaseHandler(h.ChatCompletion), active.Active("user")) - g.POST("/completions", web.BaseHandler(h.Completions), active.Active("user")) - g.POST("/embeddings", web.BaseHandler(h.Embeddings), active.Active("user")) + g.POST("/completion/accept", web.BindHandler(h.AcceptCompletion), active.Active("apikey")) + g.POST("/report", web.BindHandler(h.Report), active.Active("apikey")) + g.POST("/chat/completions", web.BaseHandler(h.ChatCompletion), active.Active("apikey")) + g.POST("/completions", web.BaseHandler(h.Completions), active.Active("apikey")) + g.POST("/embeddings", web.BaseHandler(h.Embeddings), active.Active("apikey")) return h } diff --git a/backend/internal/user/handler/v1/dashboard.go b/backend/internal/user/handler/v1/dashboard.go index 1dce5a8..456f591 100644 --- a/backend/internal/user/handler/v1/dashboard.go +++ b/backend/internal/user/handler/v1/dashboard.go @@ -9,7 +9,7 @@ import ( // UserStat 获取用户统计信息 // -// @Tags Dashboard +// @Tags User Dashboard // @Summary 获取用户统计信息 // @Description 获取用户统计信息 // @ID user-dashboard-stat @@ -17,6 +17,7 @@ import ( // @Produce json // @Param filter query domain.StatisticsFilter true "筛选参数" // @Success 200 {object} web.Resp{data=domain.UserStat} +// @Failure 401 {object} string // @Router /api/v1/user/dashboard/stat [get] func (h *UserHandler) UserStat(c *web.Context, req domain.StatisticsFilter) error { req.UserID = middleware.GetUser(c).ID @@ -29,7 +30,7 @@ func (h *UserHandler) UserStat(c *web.Context, req domain.StatisticsFilter) erro // UserEvents 获取用户事件 // -// @Tags Dashboard +// @Tags User Dashboard // @Summary 获取用户事件 // @Description 获取用户事件 // @ID user-dashboard-events @@ -37,6 +38,7 @@ func (h *UserHandler) UserStat(c *web.Context, req domain.StatisticsFilter) erro // @Produce json // @Param filter query domain.StatisticsFilter true "筛选参数" // @Success 200 {object} web.Resp{data=[]domain.UserEvent} +// @Failure 401 {object} string // @Router /api/v1/user/dashboard/events [get] func (h *UserHandler) UserEvents(c *web.Context) error { userEvents, err := h.duse.UserEvents(c.Request().Context(), domain.StatisticsFilter{ @@ -52,13 +54,14 @@ func (h *UserHandler) UserEvents(c *web.Context) error { // UserHeatmap 用户热力图 // -// @Tags Dashboard +// @Tags User Dashboard // @Summary 用户热力图 // @Description 用户热力图 // @ID user-dashboard-heatmap // @Accept json // @Produce json // @Success 200 {object} web.Resp{data=domain.UserHeatmapResp} +// @Failure 401 {object} string // @Router /api/v1/user/dashboard/heatmap [get] func (h *UserHandler) UserHeatmap(c *web.Context) error { userID := middleware.GetUser(c).ID diff --git a/backend/internal/user/handler/v1/record.go b/backend/internal/user/handler/v1/record.go new file mode 100644 index 0000000..8a11476 --- /dev/null +++ b/backend/internal/user/handler/v1/record.go @@ -0,0 +1,91 @@ +package v1 + +import ( + "github.com/GoYoko/web" + "github.com/chaitin/MonkeyCode/backend/domain" + "github.com/chaitin/MonkeyCode/backend/internal/middleware" +) + +// ListChatRecord 获取用户对话记录 +// +// @Tags User Record +// @Summary 获取用户对话记录 +// @Description 获取用户对话记录 +// @ID user-list-chat-record +// @Accept json +// @Produce json +// @Param page query domain.ListRecordReq true "参数" +// @Success 200 {object} web.Resp{data=domain.ListChatRecordResp} +// @Failure 401 {object} string +// @Router /api/v1/user/chat/record [get] +func (h *UserHandler) ListChatRecord(c *web.Context, req domain.ListRecordReq) error { + req.Pagination = c.Page() + req.UserID = middleware.GetUser(c).ID + records, err := h.buse.ListChatRecord(c.Request().Context(), req) + if err != nil { + return err + } + return c.Success(records) +} + +// ListCompletionRecord 获取补全记录 +// +// @Tags User Record +// @Summary 获取补全记录 +// @Description 获取补全记录 +// @ID user-list-completion-record +// @Accept json +// @Produce json +// @Param page query domain.ListRecordReq true "参数" +// @Success 200 {object} web.Resp{data=domain.ListCompletionRecordResp} +// @Failure 401 {object} string +// @Router /api/v1/user/completion/record [get] +func (h *UserHandler) ListCompletionRecord(c *web.Context, req domain.ListRecordReq) error { + req.Pagination = c.Page() + req.UserID = middleware.GetUser(c).ID + records, err := h.buse.ListCompletionRecord(c.Request().Context(), req) + if err != nil { + return err + } + return c.Success(records) +} + +// CompletionInfo 获取补全内容 +// +// @Tags User Record +// @Summary 获取补全内容 +// @Description 获取补全内容 +// @ID user-completion-info +// @Accept json +// @Produce json +// @Param id query string true "补全记录ID" +// @Success 200 {object} web.Resp{data=domain.CompletionInfo} +// @Failure 401 {object} string +// @Router /api/v1/user/completion/info [get] +func (h *UserHandler) CompletionInfo(c *web.Context) error { + info, err := h.buse.CompletionInfo(c.Request().Context(), c.QueryParam("id"), middleware.GetUser(c).ID) + if err != nil { + return err + } + return c.Success(info) +} + +// ChatInfo 获取对话内容 +// +// @Tags User Record +// @Summary 获取对话内容 +// @Description 获取对话内容 +// @ID user-chat-info +// @Accept json +// @Produce json +// @Param id query string true "对话记录ID" +// @Success 200 {object} web.Resp{data=domain.ChatInfo} +// @Failure 401 {object} string +// @Router /api/v1/user/chat/info [get] +func (h *UserHandler) ChatInfo(c *web.Context) error { + info, err := h.buse.ChatInfo(c.Request().Context(), c.QueryParam("id"), middleware.GetUser(c).ID) + if err != nil { + return err + } + return c.Success(info) +} diff --git a/backend/internal/user/handler/v1/user.go b/backend/internal/user/handler/v1/user.go index 3148e0c..17a349e 100644 --- a/backend/internal/user/handler/v1/user.go +++ b/backend/internal/user/handler/v1/user.go @@ -32,6 +32,7 @@ type UserHandler struct { usecase domain.UserUsecase euse domain.ExtensionUsecase duse domain.DashboardUsecase + buse domain.BillingUsecase session *session.Session logger *slog.Logger cfg *config.Config @@ -45,6 +46,7 @@ func NewUserHandler( usecase domain.UserUsecase, euse domain.ExtensionUsecase, duse domain.DashboardUsecase, + buse domain.BillingUsecase, auth *middleware.AuthMiddleware, active *middleware.ActiveMiddleware, session *session.Session, @@ -55,6 +57,7 @@ func NewUserHandler( usecase: usecase, euse: euse, duse: duse, + buse: buse, session: session, logger: logger, cfg: cfg, @@ -103,6 +106,17 @@ func NewUserHandler( d.GET("/events", web.BaseHandler(u.UserEvents)) d.GET("/heatmap", web.BaseHandler(u.UserHeatmap)) + // user record + uc := w.Group("/api/v1/user/chat") + uc.Use(auth.UserAuth(), active.Active("user")) + uc.GET("/record", web.BindHandler(u.ListChatRecord, web.WithPage())) + uc.GET("/info", web.BaseHandler(u.ChatInfo)) + + cplt := w.Group("/api/v1/user/completion") + cplt.Use(auth.UserAuth(), active.Active("user")) + cplt.GET("/record", web.BindHandler(u.ListCompletionRecord, web.WithPage())) + cplt.GET("/info", web.BaseHandler(u.CompletionInfo)) + return u } @@ -489,6 +503,7 @@ func (h *UserHandler) UpdateSetting(c *web.Context, req domain.UpdateSettingReq) // @Success 200 {object} web.Resp{data=domain.OAuthURLResp} // @Router /api/v1/user/oauth/signup-or-in [get] func (h *UserHandler) OAuthSignUpOrIn(ctx *web.Context, req domain.OAuthSignUpOrInReq) error { + h.logger.With("req", req).DebugContext(ctx.Request().Context(), "OAuthSignUpOrIn") resp, err := h.usecase.OAuthSignUpOrIn(ctx.Request().Context(), &req) if err != nil { return err @@ -514,13 +529,14 @@ func (h *UserHandler) OAuthCallback(ctx *web.Context, req domain.OAuthCallbackRe // Profile 获取用户信息 // -// @Tags User +// @Tags User Manage // @Summary 获取用户信息 // @Description 获取用户信息 // @ID user-profile // @Accept json // @Produce json -// @Success 200 {object} web.Resp{data=string} +// @Success 200 {object} web.Resp{data=domain.User} +// @Failure 401 {object} web.Resp{} // @Router /api/v1/user/profile [get] func (h *UserHandler) Profile(ctx *web.Context) error { return ctx.Success(middleware.GetUser(ctx)) @@ -528,14 +544,15 @@ func (h *UserHandler) Profile(ctx *web.Context) error { // UpdateProfile 更新用户信息 // -// @Tags User +// @Tags User Manage // @Summary 更新用户信息 // @Description 更新用户信息 -// @ID user-profile +// @ID user-update-profile // @Accept json // @Produce json // @Param req body domain.ProfileUpdateReq true "param" // @Success 200 {object} web.Resp{data=domain.User} +// @Failure 401 {object} web.Resp{} // @Router /api/v1/user/profile [put] func (h *UserHandler) UpdateProfile(ctx *web.Context, req domain.ProfileUpdateReq) error { req.UID = middleware.GetUser(ctx).ID diff --git a/backend/internal/user/usecase/user.go b/backend/internal/user/usecase/user.go index 4577788..1d80caf 100644 --- a/backend/internal/user/usecase/user.go +++ b/backend/internal/user/usecase/user.go @@ -239,7 +239,7 @@ func (u *UserUsecase) Login(ctx context.Context, req *domain.LoginReq) (*domain. }, nil } - return nil, fmt.Errorf("invalid login kind") + return nil, fmt.Errorf("invalid login source") } func (u *UserUsecase) getVSCodeURL(ctx context.Context, sessionID, apiKey, username string) (string, *domain.VSCodeSession, error) { @@ -533,10 +533,10 @@ func (u *UserUsecase) OAuthCallback(c *web.Context, req *domain.OAuthCallbackReq if session.Source == consts.LoginSourceBrowser { resUser := cvt.From(user, &domain.User{}) + u.logger.With("user", resUser).With("host", c.Request().Host).DebugContext(ctx, "save user session") if _, err := u.session.Save(c, consts.UserSessionName, c.Request().Host, resUser); err != nil { return err } - return c.Success(resUser) } c.Redirect(http.StatusFound, redirect) @@ -599,7 +599,7 @@ func (u *UserUsecase) WithOAuthCallback(ctx context.Context, req *domain.OAuthCa } } - u.logger.Debug("oauth callback", "redirect", redirect) + u.logger.With("session", session).Debug("oauth callback", "redirect", redirect) return user, redirect, nil }