Merge pull request #51 from yokowu/feat-user-active

feat(user): 记录用户活跃时间
This commit is contained in:
Yoko
2025-07-04 11:00:07 +08:00
committed by GitHub
9 changed files with 77 additions and 11 deletions

View File

@@ -57,13 +57,14 @@ func newServer(dir string) (*Server, error) {
openAIRepo := repo2.NewOpenAIRepo(client)
openAIUsecase := openai.NewOpenAIUsecase(configConfig, openAIRepo, slogLogger)
proxyMiddleware := middleware.NewProxyMiddleware(proxyUsecase)
v1Handler := v1.NewV1Handler(slogLogger, web, domainProxy, openAIUsecase, proxyMiddleware)
redisClient := store.NewRedisCli(configConfig)
activeMiddleware := middleware.NewActiveMiddleware(redisClient, slogLogger)
v1Handler := v1.NewV1Handler(slogLogger, web, domainProxy, openAIUsecase, proxyMiddleware, activeMiddleware)
modelRepo := repo3.NewModelRepo(client)
modelUsecase := usecase2.NewModelUsecase(slogLogger, modelRepo, configConfig)
sessionSession := session.NewSession(configConfig)
authMiddleware := middleware.NewAuthMiddleware(sessionSession, slogLogger)
modelHandler := v1_2.NewModelHandler(web, modelUsecase, authMiddleware, slogLogger)
redisClient := store.NewRedisCli(configConfig)
userRepo := repo4.NewUserRepo(client)
userUsecase := usecase3.NewUserUsecase(configConfig, redisClient, userRepo, slogLogger)
userHandler := v1_3.NewUserHandler(web, userUsecase, authMiddleware, sessionSession, slogLogger, configConfig)

View File

@@ -1,5 +1,9 @@
package consts
const (
UserActiveKeyFmt = "user:active:%s"
)
type UserStatus string
const (

View File

@@ -4,7 +4,7 @@ go 1.23.7
require (
entgo.io/ent v0.14.4
github.com/GoYoko/web v1.1.0
github.com/GoYoko/web v1.0.0
github.com/golang-migrate/migrate/v4 v4.18.3
github.com/google/uuid v1.6.0
github.com/google/wire v0.6.0

View File

@@ -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.1.0 h1:nIbtol5z0Y03d0nHsvGjv+W0fgmFRGUL8fzPN3kmrOY=
github.com/GoYoko/web v1.1.0/go.mod h1:DL9/gvuUG2jcBE1XUIY+9QBrrhdshzPEdxMCzR9jUHo=
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/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=

View File

@@ -0,0 +1,37 @@
package middleware
import (
"context"
"fmt"
"log/slog"
"time"
"github.com/chaitin/MonkeyCode/backend/consts"
"github.com/labstack/echo/v4"
"github.com/redis/go-redis/v9"
)
type ActiveMiddleware struct {
redis *redis.Client
logger *slog.Logger
}
func NewActiveMiddleware(redis *redis.Client, logger *slog.Logger) *ActiveMiddleware {
return &ActiveMiddleware{
redis: redis,
logger: logger,
}
}
func (a *ActiveMiddleware) Active() echo.MiddlewareFunc {
return func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
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")
}
}
return next(c)
}
}
}

View File

@@ -25,6 +25,7 @@ func NewV1Handler(
proxy domain.Proxy,
usecase domain.OpenAIUsecase,
middleware *middleware.ProxyMiddleware,
active *middleware.ActiveMiddleware,
) *V1Handler {
h := &V1Handler{
logger: logger.With(slog.String("handler", "openai")),
@@ -35,10 +36,10 @@ func NewV1Handler(
g := w.Group("/v1", middleware.Auth())
g.GET("/models", web.BaseHandler(h.ModelList))
g.POST("/completion/accept", web.BindHandler(h.AcceptCompletion))
g.POST("/chat/completions", web.BindHandler(h.ChatCompletion))
g.POST("/completions", web.BindHandler(h.Completions))
g.POST("/embeddings", web.BindHandler(h.Embeddings))
g.POST("/completion/accept", web.BindHandler(h.AcceptCompletion), active.Active())
g.POST("/chat/completions", web.BindHandler(h.ChatCompletion), active.Active())
g.POST("/completions", web.BindHandler(h.Completions), active.Active())
g.POST("/embeddings", web.BindHandler(h.Embeddings), active.Active())
return h
}

View File

@@ -39,6 +39,7 @@ var Provider = wire.NewSet(
dashrepo.NewDashboardRepo,
middleware.NewProxyMiddleware,
middleware.NewAuthMiddleware,
middleware.NewActiveMiddleware,
userV1.NewUserHandler,
userrepo.NewUserRepo,
userusecase.NewUserUsecase,

View File

@@ -629,7 +629,7 @@ func (p *LLMProxy) handleChatCompletionStream(ctx context.Context, w http.Respon
"apiBase", m.APIBase,
"work_mode", mode,
"requestHeader", newReq.Header,
"requestBody", newReq,
"requestBody", req,
"taskID", taskID,
"messages", cvt.Filter(req.Messages, func(i int, v openai.ChatCompletionMessage) (openai.ChatCompletionMessage, bool) {
return v, v.Role != "system"

View File

@@ -60,14 +60,36 @@ func (u *UserUsecase) List(ctx context.Context, req domain.ListReq) (*domain.Lis
return nil, err
}
ids := cvt.Iter(users, func(_ int, u *db.User) string { return u.ID.String() })
m, err := u.getUserActive(ctx, ids)
if err != nil {
return nil, err
}
return &domain.ListUserResp{
PageInfo: p,
Users: cvt.Iter(users, func(_ int, e *db.User) *domain.User {
return cvt.From(e, &domain.User{}).From(e)
return cvt.From(e, &domain.User{
LastActiveAt: m[e.ID.String()],
})
}),
}, nil
}
func (u *UserUsecase) getUserActive(ctx context.Context, ids []string) (map[string]int64, error) {
m := make(map[string]int64)
for _, id := range ids {
key := fmt.Sprintf(consts.UserActiveKeyFmt, id)
if t, err := u.redis.Get(ctx, key).Int64(); err != nil {
u.logger.With("key", key).With("error", err).Warn("get user active time failed")
} else {
m[id] = t
}
}
return m, nil
}
// AdminList implements domain.UserUsecase.
func (u *UserUsecase) AdminList(ctx context.Context, page *web.Pagination) (*domain.ListAdminUserResp, error) {
admins, p, err := u.repo.AdminList(ctx, page)