From 9f61e4fe86dee07abe6d097d9cca0f2bf1497a81 Mon Sep 17 00:00:00 2001 From: yokowu <18836617@qq.com> Date: Wed, 23 Jul 2025 16:38:59 +0800 Subject: [PATCH] =?UTF-8?q?chore:=20=E4=BB=A3=E7=A0=81=E8=B0=83=E6=95=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/cmd/server/wire_gen.go | 4 +- backend/config/config.go | 2 + backend/docs/swagger.json | 145 ++++++++++++++++++++++- backend/domain/dashboard.go | 9 +- backend/errcode/errcode.go | 1 + backend/errcode/locale.zh.toml | 5 +- backend/internal/user/handler/v1/user.go | 21 ++-- backend/internal/user/repo/user.go | 55 +++++++-- 8 files changed, 211 insertions(+), 31 deletions(-) diff --git a/backend/cmd/server/wire_gen.go b/backend/cmd/server/wire_gen.go index 2bd3e03..42af4ba 100644 --- a/backend/cmd/server/wire_gen.go +++ b/backend/cmd/server/wire_gen.go @@ -1,6 +1,6 @@ // Code generated by Wire. DO NOT EDIT. -//go:generate go run -mod=mod github.com/google/wire/cmd/wire +//go:generate go run github.com/google/wire/cmd/wire //go:build !wireinject // +build !wireinject @@ -78,7 +78,7 @@ func newServer() (*Server, error) { if err != nil { return nil, err } - userRepo := repo5.NewUserRepo(client, ipdbIPDB, redisClient) + userRepo := repo5.NewUserRepo(client, ipdbIPDB, redisClient, configConfig) userUsecase := usecase4.NewUserUsecase(configConfig, redisClient, userRepo, slogLogger, sessionSession) dashboardRepo := repo6.NewDashboardRepo(client) dashboardUsecase := usecase5.NewDashboardUsecase(dashboardRepo) diff --git a/backend/config/config.go b/backend/config/config.go index f73a4f1..8805e41 100644 --- a/backend/config/config.go +++ b/backend/config/config.go @@ -24,6 +24,7 @@ type Config struct { Admin struct { User string `mapstructure:"user"` Password string `mapstructure:"password"` + Limit int `mapstructure:"limit"` } `mapstructure:"admin"` Session struct { @@ -82,6 +83,7 @@ func Init() (*Config, error) { v.SetDefault("server.addr", ":8888") v.SetDefault("admin.user", "admin") v.SetDefault("admin.password", "") + v.SetDefault("admin.limit", 100) v.SetDefault("session.expire_day", 30) v.SetDefault("database.master", "") v.SetDefault("database.slave", "") diff --git a/backend/docs/swagger.json b/backend/docs/swagger.json index 076c68a..26f9c5c 100644 --- a/backend/docs/swagger.json +++ b/backend/docs/swagger.json @@ -99,6 +99,46 @@ } } }, + "/api/v1/admin/export-completion-data": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "管理员导出所有补全相关数据", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "admin" + ], + "summary": "导出补全数据", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/domain.ExportCompletionDataResp" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/web.Resp" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/web.Resp" + } + } + } + } + }, "/api/v1/admin/list": { "get": { "description": "获取管理员用户列表", @@ -3118,6 +3158,92 @@ } } }, + "domain.CompletionData": { + "type": "object", + "properties": { + "code_lines": { + "description": "代码行数", + "type": "integer" + }, + "completion": { + "description": "LLM生成的补全代码", + "type": "string" + }, + "created_at": { + "description": "创建时间戳", + "type": "integer" + }, + "cursor_position": { + "description": "光标位置 {\"line\": 10, \"column\": 5}", + "type": "object", + "additionalProperties": {} + }, + "input_tokens": { + "description": "输入token数", + "type": "integer" + }, + "is_accept": { + "description": "用户是否接受补全", + "type": "boolean" + }, + "is_suggested": { + "description": "是否为建议模式", + "type": "boolean" + }, + "model_id": { + "description": "模型ID", + "type": "string" + }, + "model_name": { + "description": "模型名称", + "type": "string" + }, + "model_type": { + "description": "模型类型", + "type": "string" + }, + "output_tokens": { + "description": "输出token数", + "type": "integer" + }, + "program_language": { + "description": "编程语言", + "type": "string" + }, + "prompt": { + "description": "用户输入的提示", + "type": "string" + }, + "request_id": { + "description": "请求ID", + "type": "string" + }, + "source_code": { + "description": "当前文件原文", + "type": "string" + }, + "task_id": { + "description": "任务ID", + "type": "string" + }, + "updated_at": { + "description": "更新时间戳", + "type": "integer" + }, + "user_id": { + "description": "用户ID", + "type": "string" + }, + "user_input": { + "description": "用户最终输入的内容", + "type": "string" + }, + "work_mode": { + "description": "工作模式", + "type": "string" + } + } + }, "domain.CompletionInfo": { "type": "object", "properties": { @@ -3384,6 +3510,22 @@ } } }, + "domain.ExportCompletionDataResp": { + "type": "object", + "properties": { + "data": { + "description": "补全数据列表", + "type": "array", + "items": { + "$ref": "#/definitions/domain.CompletionData" + } + }, + "total_count": { + "description": "总记录数", + "type": "integer" + } + } + }, "domain.GetProviderModelListResp": { "type": "object", "properties": { @@ -3895,7 +4037,8 @@ }, "cursor_position": { "description": "光标位置(用于reject action)", - "type": "integer" + "type": "object", + "additionalProperties": {} }, "id": { "description": "task_id or resp_id", diff --git a/backend/domain/dashboard.go b/backend/domain/dashboard.go index b96b1ba..170d6ee 100644 --- a/backend/domain/dashboard.go +++ b/backend/domain/dashboard.go @@ -40,14 +40,7 @@ type StatisticsFilter struct { } func (s StatisticsFilter) StartTime() time.Time { - switch s.Precision { - case "hour": - return time.Now().Add(-time.Duration(s.Duration) * time.Hour) - case "day": - return time.Now().AddDate(0, 0, -int(s.Duration)) - default: - return time.Now().AddDate(0, 0, -90) - } + return time.Now().Add(-24 * time.Hour) } type UserHeatmapResp struct { diff --git a/backend/errcode/errcode.go b/backend/errcode/errcode.go index 23f0c3d..9d168b7 100644 --- a/backend/errcode/errcode.go +++ b/backend/errcode/errcode.go @@ -20,4 +20,5 @@ var ( ErrNotInvited = web.NewBadRequestErr("err-not-invited") ErrDingtalkNotEnabled = web.NewBadRequestErr("err-dingtalk-not-enabled") ErrCustomNotEnabled = web.NewBadRequestErr("err-custom-not-enabled") + ErrUserLimit = web.NewBadRequestErr("err-user-limit") ) diff --git a/backend/errcode/locale.zh.toml b/backend/errcode/locale.zh.toml index 6000dd0..536e37b 100644 --- a/backend/errcode/locale.zh.toml +++ b/backend/errcode/locale.zh.toml @@ -26,4 +26,7 @@ other = "未被邀请" other = "钉钉未启用" [err-custom-not-enabled] -other = "OAuth未启用" \ No newline at end of file +other = "OAuth未启用" + +[err-user-limit] +other = "用户数量已达上限" \ No newline at end of file diff --git a/backend/internal/user/handler/v1/user.go b/backend/internal/user/handler/v1/user.go index f64b7ee..78a0b2f 100644 --- a/backend/internal/user/handler/v1/user.go +++ b/backend/internal/user/handler/v1/user.go @@ -623,16 +623,17 @@ func (h *UserHandler) InitAdmin() error { } // ExportCompletionData godoc -// @Summary 导出补全数据 -// @Description 管理员导出所有补全相关数据 -// @Tags admin -// @Accept json -// @Produce json -// @Security ApiKeyAuth -// @Success 200 {object} domain.ExportCompletionDataResp -// @Failure 401 {object} errcode.Error -// @Failure 500 {object} errcode.Error -// @Router /api/v1/admin/export-completion-data [get] +// +// @Summary 导出补全数据 +// @Description 管理员导出所有补全相关数据 +// @Tags admin +// @Accept json +// @Produce json +// @Security ApiKeyAuth +// @Success 200 {object} domain.ExportCompletionDataResp +// @Failure 401 {object} web.Resp{} +// @Failure 500 {object} web.Resp{} +// @Router /api/v1/admin/export-completion-data [get] func (h *UserHandler) ExportCompletionData(c *web.Context) error { data, err := h.usecase.ExportCompletionData(c.Request().Context()) if err != nil { diff --git a/backend/internal/user/repo/user.go b/backend/internal/user/repo/user.go index ca303f3..1c5d3dd 100644 --- a/backend/internal/user/repo/user.go +++ b/backend/internal/user/repo/user.go @@ -12,6 +12,7 @@ import ( "github.com/GoYoko/web" + "github.com/chaitin/MonkeyCode/backend/config" "github.com/chaitin/MonkeyCode/backend/consts" "github.com/chaitin/MonkeyCode/backend/db" "github.com/chaitin/MonkeyCode/backend/db/admin" @@ -32,10 +33,16 @@ type UserRepo struct { db *db.Client ipdb *ipdb.IPDB redis *redis.Client + cfg *config.Config } -func NewUserRepo(db *db.Client, ipdb *ipdb.IPDB, redis *redis.Client) domain.UserRepo { - return &UserRepo{db: db, ipdb: ipdb, redis: redis} +func NewUserRepo( + db *db.Client, + ipdb *ipdb.IPDB, + redis *redis.Client, + cfg *config.Config, +) domain.UserRepo { + return &UserRepo{db: db, ipdb: ipdb, redis: redis, cfg: cfg} } func (r *UserRepo) InitAdmin(ctx context.Context, username, password string) error { @@ -113,13 +120,25 @@ func (r *UserRepo) innerValidateInviteCode(ctx context.Context, tx *db.Tx, code } func (r *UserRepo) CreateUser(ctx context.Context, user *db.User) (*db.User, error) { - return r.db.User.Create(). - SetUsername(user.Username). - SetEmail(user.Email). - SetPassword(user.Password). - SetStatus(user.Status). - SetPlatform(user.Platform). - Save(ctx) + var res *db.User + err := entx.WithTx(ctx, r.db, func(tx *db.Tx) error { + if err := r.checkLimit(ctx, tx); err != nil { + return err + } + u, err := tx.User.Create(). + SetUsername(user.Username). + SetEmail(user.Email). + SetPassword(user.Password). + SetStatus(user.Status). + SetPlatform(user.Platform). + Save(ctx) + if err != nil { + return err + } + res = u + return nil + }) + return res, err } func (r *UserRepo) UserLoginHistory(ctx context.Context, page *web.Pagination) ([]*db.UserLoginHistory, *db.PageInfo, error) { @@ -297,6 +316,10 @@ func (r *UserRepo) DeleteAdmin(ctx context.Context, id string) error { func (r *UserRepo) OAuthRegister(ctx context.Context, platform consts.UserPlatform, inviteCode string, req *domain.OAuthUserInfo) (*db.User, error) { var u *db.User err := entx.WithTx(ctx, r.db, func(tx *db.Tx) error { + if err := r.checkLimit(ctx, tx); err != nil { + return err + } + if _, err := r.innerValidateInviteCode(ctx, tx, inviteCode); err != nil { return errcode.ErrInviteCodeInvalid.Wrap(err) } @@ -365,6 +388,17 @@ func (r *UserRepo) updateAvatar(ctx context.Context, tx *db.Tx, ui *db.UserIdent return tx.User.UpdateOneID(ui.UserID).SetAvatarURL(avatar).Exec(ctx) } +func (r *UserRepo) checkLimit(ctx context.Context, tx *db.Tx) error { + count, err := tx.User.Query().Count(ctx) + if err != nil { + return err + } + if count >= r.cfg.Admin.Limit { + return errcode.ErrUserLimit.Wrap(err) + } + return nil +} + func (r *UserRepo) SignUpOrIn(ctx context.Context, platform consts.UserPlatform, req *domain.OAuthUserInfo) (*db.User, error) { var u *db.User err := entx.WithTx(ctx, r.db, func(tx *db.Tx) error { @@ -384,6 +418,9 @@ func (r *UserRepo) SignUpOrIn(ctx context.Context, platform consts.UserPlatform, if !db.IsNotFound(err) { return err } + if err = r.checkLimit(ctx, tx); err != nil { + return err + } user, err := tx.User.Create(). SetUsername(req.Name). SetEmail(req.Email).