feat(sys_user_login_log): 新增用户登录日志查询及详情接口

- 新增 sys_user_login_log 相关的 API 接口定义及请求响应结构体
- 新增登录日志查询和详情的控制器逻辑实现
- 新增登录日志查询和详情的服务接口及实现
- 增加对登录日志的分页查询,支持用户ID、登录名、状态、时间范围等过滤条件
- 支持根据ID查询登录日志详情
- 更新依赖包至 gf v2.9.5 版本
- 配置文件更新 Redis 连接地址和密码
This commit is contained in:
danial
2025-11-12 20:27:02 +08:00
parent 0e92cf4eca
commit 107d2a7e47
15 changed files with 322 additions and 25 deletions

View File

@@ -0,0 +1,16 @@
// =================================================================================
// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT.
// =================================================================================
package sys_user_login_log
import (
"context"
"kami/api/sys_user_login_log/v1"
)
type ISysUserLoginLogV1 interface {
LoginLogQuery(ctx context.Context, req *v1.LoginLogQueryReq) (res *v1.LoginLogQueryRes, err error)
LoginLogDetail(ctx context.Context, req *v1.LoginLogDetailReq) (res *v1.LoginLogDetailRes, err error)
}

View File

@@ -0,0 +1,37 @@
package v1
import (
"kami/api/commonApi"
"kami/internal/model/entity"
"github.com/gogf/gf/v2/frame/g"
)
// LoginLogQueryReq 登录日志查询请求
type LoginLogQueryReq struct {
g.Meta `path:"/sys-user-login-log/list" tags:"用户登录日志管理" method:"get" summary:"查询登录日志列表"`
UserId string `p:"userId" dc:"用户ID可选"`
LoginName string `p:"loginName" dc:"登录名(可选)"`
Status int `p:"status" dc:"登录状态1登录成功 2登录失败可选"`
StartTime string `p:"startTime" dc:"开始时间格式为2006-01-02可选"`
EndTime string `p:"endTime" dc:"结束时间格式为2006-01-02可选"`
commonApi.CommonPageReq
}
// LoginLogQueryRes 登录日志查询响应
type LoginLogQueryRes struct {
g.Meta `mime:"application/json"`
commonApi.CommonPageRes[*entity.V1SysUserLoginLog]
}
// LoginLogDetailReq 登录日志详情查询请求
type LoginLogDetailReq struct {
g.Meta `path:"/sys-user-login-log/detail" tags:"用户登录日志管理" method:"get" summary:"查询登录日志详情"`
commonApi.CommonIntId
}
// LoginLogDetailRes 登录日志详情响应
type LoginLogDetailRes struct {
g.Meta `mime:"application/json"`
Data *entity.V1SysUserLoginLog `json:"data"`
}

10
go.mod
View File

@@ -7,10 +7,10 @@ require (
github.com/bytedance/sonic v1.14.2
github.com/casbin/casbin/v2 v2.132.0
github.com/duke-git/lancet/v2 v2.3.8
github.com/gogf/gf/contrib/drivers/mysql/v2 v2.9.4
github.com/gogf/gf/contrib/metric/otelmetric/v2 v2.9.4
github.com/gogf/gf/contrib/nosql/redis/v2 v2.9.4
github.com/gogf/gf/v2 v2.9.4
github.com/gogf/gf/contrib/drivers/mysql/v2 v2.9.5
github.com/gogf/gf/contrib/metric/otelmetric/v2 v2.9.5
github.com/gogf/gf/contrib/nosql/redis/v2 v2.9.5
github.com/gogf/gf/v2 v2.9.5
github.com/golang-jwt/jwt/v5 v5.3.0
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0
github.com/google/uuid v1.6.0
@@ -24,6 +24,7 @@ require (
github.com/xuri/excelize/v2 v2.10.0
go.opentelemetry.io/otel v1.38.0
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.14.0
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0
go.opentelemetry.io/otel/log v0.14.0
go.opentelemetry.io/otel/sdk v1.38.0
@@ -83,7 +84,6 @@ require (
github.com/xuri/nfp v0.0.2-0.20250530014748-2ddeb826f9a9 // indirect
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
go.opentelemetry.io/contrib/instrumentation/runtime v0.63.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 // indirect
go.opentelemetry.io/otel/metric v1.38.0 // indirect
go.opentelemetry.io/otel/sdk/metric v1.38.0 // indirect
go.opentelemetry.io/proto/otlp v1.9.0 // indirect

16
go.sum
View File

@@ -56,14 +56,14 @@ github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo=
github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=
github.com/gogf/gf/contrib/drivers/mysql/v2 v2.9.4 h1:ntAPahCjQwQ79CC6tI67QDgj17NTWp+lMd1SaL2jJhs=
github.com/gogf/gf/contrib/drivers/mysql/v2 v2.9.4/go.mod h1:/350+9clTW5ktUvF+hePMN9yDknB2ipslqcx3Y2rLDQ=
github.com/gogf/gf/contrib/metric/otelmetric/v2 v2.9.4 h1:kHcX00bUpLxFihMJPRAfEEo2c2URnQ+3TnIM7fTyLrk=
github.com/gogf/gf/contrib/metric/otelmetric/v2 v2.9.4/go.mod h1:sLFI9YoufXjUtnaDp8btbjWJfoDG9p6NpJHgBnk3qCc=
github.com/gogf/gf/contrib/nosql/redis/v2 v2.9.4 h1:iKXUQ+8TklSriAqOQjfwioI36zlByqrDqz4ISaRFvm8=
github.com/gogf/gf/contrib/nosql/redis/v2 v2.9.4/go.mod h1:PYVwyQ0gN+w3wL7zKAoeUpy2WFs4/V8+Ls+eNsy7Uo0=
github.com/gogf/gf/v2 v2.9.4 h1:6vleEWypot9WBPncP2GjbpgAUeG6Mzb1YESb9nPMkjY=
github.com/gogf/gf/v2 v2.9.4/go.mod h1:Ukl+5HUH9S7puBmNLR4L1zUqeRwi0nrW4OigOknEztU=
github.com/gogf/gf/contrib/drivers/mysql/v2 v2.9.5 h1:0+ZBYhi4sqwxXwL+hIBpp06a7G4m5nmjskQ3NNb8qYc=
github.com/gogf/gf/contrib/drivers/mysql/v2 v2.9.5/go.mod h1:vyB7J/uJcLCrHD5lfFBzxhEEMkePIRzfhd33EcsuLa0=
github.com/gogf/gf/contrib/metric/otelmetric/v2 v2.9.5 h1:WHED8lQqYcI/Cu3wF1nFzYTzKAAlPI3JcYrySk0BngE=
github.com/gogf/gf/contrib/metric/otelmetric/v2 v2.9.5/go.mod h1:HLK7KunmfInknvzhtVbwJGbQJda7Q3GOvF1eYTMBqT0=
github.com/gogf/gf/contrib/nosql/redis/v2 v2.9.5 h1:Ku7p3CvGchxC7zPSgArf/tZs2w9Yb8tS/gH5ADN+p9g=
github.com/gogf/gf/contrib/nosql/redis/v2 v2.9.5/go.mod h1:cjy18NsSLZQf5zaLAzuo7B2gr8GGjCTWDTEPY7T+6FI=
github.com/gogf/gf/v2 v2.9.5 h1:1scfOdHbMP854oQaiLejl+eL+c4xfuvtWmmZiDJxbKs=
github.com/gogf/gf/v2 v2.9.5/go.mod h1:VUb5eyJKpvW77O/dXsbbLNO/Kjrg0UycIiq0lRiBjjo=
github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=
github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g=

View File

@@ -15,6 +15,7 @@ import (
"kami/internal/controller/sysUser"
"kami/internal/controller/sys_payment"
"kami/internal/controller/sys_user_login"
"kami/internal/controller/sys_user_login_log"
"kami/internal/controller/user_center"
"kami/utility/cron"
"kami/utility/monitor"
@@ -69,6 +70,7 @@ var Main = gcmd.Command{
group.Bind(card_info_original_jd.NewV1())
group.Bind(card_info_c_trip.NewV1())
group.Bind(jd_cookie.NewV1())
group.Bind(sys_user_login_log.New())
})
monitor.Register(ctx) // 注册监控任务
cron.Register(ctx) // 注册轮询任务

View File

@@ -0,0 +1,5 @@
// =================================================================================
// This is auto-generated by GoFrame CLI tool only once. Fill this file as you wish.
// =================================================================================
package sys_user_login_log

View File

@@ -0,0 +1,13 @@
package sys_user_login_log
import (
"kami/api/sys_user_login_log"
)
type ControllerV1 struct{}
var _ sys_user_login_log.ISysUserLoginLogV1 = (*ControllerV1)(nil)
func New() *ControllerV1 {
return &ControllerV1{}
}

View File

@@ -0,0 +1,64 @@
package sys_user_login_log
import (
"context"
"kami/api/commonApi"
v1 "kami/api/sys_user_login_log/v1"
"kami/internal/errHandler"
"kami/internal/model"
"kami/internal/model/entity"
"kami/internal/service"
"github.com/gogf/gf/v2/errors/gcode"
)
// LoginLogQuery 查询登录日志列表
func (c *ControllerV1) LoginLogQuery(ctx context.Context, req *v1.LoginLogQueryReq) (res *v1.LoginLogQueryRes, err error) {
if req.Current <= 0 {
req.Current = 1
}
if req.PageSize <= 0 {
req.PageSize = 10
}
// 转换为查询模型
input := &model.LoginLogQueryInput{
LoginLogQueryReq: *req,
}
// 调用服务查询
total, list, err := service.SysLoginLog().QueryLoginLogs(ctx, input)
if err != nil {
err = errHandler.WrapError(ctx, gcode.CodeInternalError, err, "查询登录日志失败")
return
}
if list == nil {
list = make([]*entity.V1SysUserLoginLog, 0)
}
res = &v1.LoginLogQueryRes{
CommonPageRes: commonApi.CommonPageRes[*entity.V1SysUserLoginLog]{
Total: total,
CommonDataRes: commonApi.CommonDataRes[*entity.V1SysUserLoginLog]{
List: list,
},
},
}
return
}
// LoginLogDetail 查询登录日志详情
func (c *ControllerV1) LoginLogDetail(ctx context.Context, req *v1.LoginLogDetailReq) (res *v1.LoginLogDetailRes, err error) {
detail, err := service.SysLoginLog().GetLoginLogDetail(ctx, req.ID)
if err != nil {
err = errHandler.WrapError(ctx, gcode.CodeInternalError, err, "查询登录日志详情失败")
return
}
res = &v1.LoginLogDetailRes{
Data: detail,
}
return
}

View File

@@ -3,10 +3,15 @@ package sys_login_log
import (
"context"
"kami/internal/dao"
"kami/internal/model"
"kami/internal/model/entity"
"kami/internal/service"
"kami/utility/config"
"kami/utility/utils"
"github.com/gogf/gf/v2/os/grpool"
"github.com/gogf/gf/v2/os/gtime"
)
func init() {
@@ -32,3 +37,112 @@ func (s *sSysLoginLog) Invoke(ctx context.Context, data *model.LoginLogParams) {
},
)
}
// QueryLoginLogs 查询登录日志列表
func (s *sSysLoginLog) QueryLoginLogs(ctx context.Context, input *model.LoginLogQueryInput) (total int, list []*entity.V1SysUserLoginLog, err error) {
m := dao.V1SysUserLoginLog.Ctx(ctx).DB(config.GetDatabaseV1())
// 条件过滤
if input.UserId != "" {
m = m.Where(dao.V1SysUserLoginLog.Columns().UserId, input.UserId)
}
if input.LoginName != "" {
m = m.Where(dao.V1SysUserLoginLog.Columns().LoginName, input.LoginName)
}
if input.Status > 0 {
m = m.Where(dao.V1SysUserLoginLog.Columns().Status, input.Status)
}
// 时间范围查询
if input.StartTime != "" {
startTime, err2 := gtime.StrToTime(input.StartTime)
if err2 == nil {
m = m.WhereGTE(dao.V1SysUserLoginLog.Columns().CreatedAt, startTime)
}
}
if input.EndTime != "" {
endTime, err2 := gtime.StrToTime(input.EndTime)
if err2 == nil {
// 时间范围需要到当天的结束时刻
m = m.WhereLTE(dao.V1SysUserLoginLog.Columns().CreatedAt, endTime.AddDate(0, 0, 1).StartOfDay())
}
}
// 排序:按创建时间倒序
m = m.Order(dao.V1SysUserLoginLog.Columns().CreatedAt + " DESC")
// 分页查询
list = make([]*entity.V1SysUserLoginLog, 0)
err = m.Page(input.Current, input.PageSize).ScanAndCount(&list, &total, true)
err = utils.HandleNoRowsError(err)
return
}
// GetLoginLogDetail 获取登录日志详情
func (s *sSysLoginLog) GetLoginLogDetail(ctx context.Context, id uint) (res *entity.V1SysUserLoginLog, err error) {
m := dao.V1SysUserLoginLog.Ctx(ctx).DB(config.GetDatabaseV1())
err = m.WherePri(id).Scan(&res)
err = utils.HandleNoRowsError(err)
return
}
//func (s *sSysLoginLog) List(ctx context.Context, req *systemV1.LoginLogSearchReq) (res *systemV1.LoginLogSearchRes, err error) {
// res = new(systemV1.LoginLogSearchRes)
// if req.PageNum == 0 {
// req.PageNum = 1
// }
// res.CurrentPage = req.PageNum
// if req.PageSize == 0 {
// req.PageSize = consts.PageSize
// }
// m := dao.SysLoginLog.Ctx(ctx)
// order := "info_id DESC"
// if req.LoginName != "" {
// m = m.Where("login_name like ?", "%"+req.LoginName+"%")
// }
// if req.Status != "" {
// m = m.Where("status", gconv.Int(req.Status))
// }
// if req.Ipaddr != "" {
// m = m.Where("ipaddr like ?", "%"+req.Ipaddr+"%")
// }
// if req.LoginLocation != "" {
// m = m.Where("login_location like ?", "%"+req.LoginLocation+"%")
// }
// if len(req.DateRange) != 0 {
// m = m.Where("login_time >=? AND login_time <=?", req.DateRange[0], req.DateRange[1])
// }
// if req.SortName != "" {
// if req.SortOrder != "" {
// order = req.SortName + " " + req.SortOrder
// } else {
// order = req.SortName + " DESC"
// }
// }
// err = g.Try(ctx, func(ctx context.Context) {
// res.Total, err = m.Count()
// liberr.ErrIsNil(ctx, err, "获取日志失败")
// err = m.Page(req.PageNum, req.PageSize).Order(order).Scan(&res.List)
// liberr.ErrIsNil(ctx, err, "获取日志数据失败")
// })
// return
//}
//
//func (s *sSysLoginLog) DeleteLoginLogByIds(ctx context.Context, ids []int) (err error) {
// err = g.Try(ctx, func(ctx context.Context) {
// _, err = dao.SysLoginLog.Ctx(ctx).Delete("info_id in (?)", ids)
// liberr.ErrIsNil(ctx, err, "删除失败")
// })
// return
//}
//
//func (s *sSysLoginLog) ClearLoginLog(ctx context.Context) (err error) {
// err = g.Try(ctx, func(ctx context.Context) {
// _, err = g.DB().Ctx(ctx).Exec(ctx, "truncate "+dao.SysLoginLog.Table())
// liberr.ErrIsNil(ctx, err, "清除失败")
// })
// return
//}

View File

@@ -117,17 +117,6 @@ func (s *sSysUser) LoginLog(ctx context.Context, params *model.LoginLogParams) {
}
}
//func (s *sSysUser) UpdateLoginInfo(ctx context.Context, id uint64, ip string) (err error) {
// err = g.Try(ctx, func(ctx context.Context) {
// _, err = dao.V1SysUser.Ctx(ctx).WherePri(id).Unscoped().Update(g.Map{
// dao.V1SysUser.Columns().LastLoginIp: ip,
// dao.V1SysUser.Columns().LastLoginTime: gtime.Now(),
// })
// liberr.ErrIsNil(ctx, err, "更新用户登录信息失败")
// })
// return
//}
// GetAdminRules 获取用户菜单数据
func (s *sSysUser) GetAdminRules(ctx context.Context, userId string) (menuList []*model.UserMenus, permissions []string, err error) {
isSuperAdmin := false

View File

@@ -1,7 +1,9 @@
package model
import (
"kami/api/commonApi"
v1 "kami/api/sys_user_login/v1"
loginlogv1 "kami/api/sys_user_login_log/v1"
)
// LoginLogParams 登录日志写入参数
@@ -14,6 +16,11 @@ type LoginLogParams struct {
Module string
}
// LoginLogQueryInput 登录日志查询输入
type LoginLogQueryInput struct {
loginlogv1.LoginLogQueryReq
}
type UserLoginInput struct {
Username string `p:"username" v:"required#用户名不能为空"`
Password string `p:"password" v:"required#密码不能为空"`
@@ -31,3 +38,9 @@ type UserMenus struct {
*UserMenu `json:""`
Children []*UserMenus `json:"children"`
}
// LoginLogQueryInputWithCommon 包含分页信息的登录日志查询输入
type LoginLogQueryInputWithCommon struct {
loginLog *loginlogv1.LoginLogQueryReq
PageReq commonApi.CommonPageReq
}

View File

@@ -48,6 +48,8 @@ type (
CheckIsNormal(ctx context.Context, accountId string)
// HandleTmpStoppedList 处理临时暂停账号
HandleTmpStoppedList(ctx context.Context) (err error)
// GetDecryptedPassword 获取解密后的密码
GetDecryptedPassword(encryptedPassword string) (string, error)
// GetNextUser 从sysUser表中查询所有用户获取排序后的当前用户后面下一个Id
GetNextUser(ctx context.Context, originalUserId string, amount decimal.Decimal) (targetId string, err error)
SetCurrentTargetAccount(ctx context.Context, machineId string, m *model.AccountIdInfo) (err error)

View File

@@ -8,11 +8,16 @@ package service
import (
"context"
"kami/internal/model"
"kami/internal/model/entity"
)
type (
ISysLoginLog interface {
Invoke(ctx context.Context, data *model.LoginLogParams)
// QueryLoginLogs 查询登录日志列表
QueryLoginLogs(ctx context.Context, input *model.LoginLogQueryInput) (total int, list []*entity.V1SysUserLoginLog, err error)
// GetLoginLogDetail 获取登录日志详情
GetLoginLogDetail(ctx context.Context, id uint) (res *entity.V1SysUserLoginLog, err error)
}
)

View File

@@ -0,0 +1,36 @@
// ================================================================================
// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT.
// You can delete these comments if you wish manually maintain this interface file.
// ================================================================================
package service
import (
"context"
"kami/internal/model"
"kami/internal/model/entity"
)
type (
ISysUserLoginLogQuery interface {
// QueryLoginLogs 查询登录日志列表
QueryLoginLogs(ctx context.Context, input *model.LoginLogQueryInput) (total int, list []*entity.V1SysUserLoginLog, err error)
// GetLoginLogDetail 获取登录日志详情
GetLoginLogDetail(ctx context.Context, id uint) (res *entity.V1SysUserLoginLog, err error)
}
)
var (
localSysUserLoginLogQuery ISysUserLoginLogQuery
)
func SysUserLoginLogQuery() ISysUserLoginLogQuery {
if localSysUserLoginLogQuery == nil {
panic("implement not found for interface ISysUserLoginLogQuery, forgot register?")
}
return localSysUserLoginLogQuery
}
func RegisterSysUserLoginLogQuery(i ISysUserLoginLogQuery) {
localSysUserLoginLogQuery = i
}

View File

@@ -48,7 +48,8 @@ database:
redis:
# 单实例配置
default:
address: 127.0.0.1:6379
address: 127.0.0.1:30567
pass: redis123
db: 4
idleTimeout: "60s" #连接最大空闲时间使用时间字符串例如30s/1m/1d
maxConnLifetime: "90s" #连接最长存活时间使用时间字符串例如30s/1m/1d