package sys import ( "context" "fmt" "net" "strings" "hotgo/internal/consts" "hotgo/internal/dao" "hotgo/internal/library/hgorm/handler" "hotgo/internal/library/location" "hotgo/internal/library/queue" "hotgo/internal/model/entity" "hotgo/internal/model/input/form" "hotgo/internal/model/input/sysin" "hotgo/internal/service" "hotgo/utility/convert" "hotgo/utility/encrypt" "hotgo/utility/excel" "hotgo/utility/useragent" "github.com/gogf/gf/v2/database/gdb" "github.com/gogf/gf/v2/errors/gerror" "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/net/ghttp" "github.com/gogf/gf/v2/os/gctx" "github.com/gogf/gf/v2/os/gtime" "github.com/gogf/gf/v2/util/gconv" ) type sSysAppMemberLoginLog struct{} func NewSysAppMemberLoginLog() *sSysAppMemberLoginLog { return &sSysAppMemberLoginLog{} } func init() { service.RegisterSysAppMemberLoginLog(NewSysAppMemberLoginLog()) } func (s *sSysAppMemberLoginLog) Model(ctx context.Context) *gdb.Model { return handler.Model(dao.AppMemberLoginLog.Ctx(ctx), &handler.Option{FilterTenant: true}) } func (s *sSysAppMemberLoginLog) List(ctx context.Context, in *sysin.AppMemberLoginLogListInp) (list []*sysin.AppMemberLoginLogListModel, totalCount int, err error) { lc, mc := dao.AppMemberLoginLog.Columns(), dao.AppMember.Columns() mod := s.Model(ctx).As("l").LeftJoin(dao.AppMember.Table()+" m", "m."+mc.Id+"=l."+lc.MemberId) if in.Keyword != "" { keyword := strings.TrimSpace(in.Keyword) like := "%" + keyword + "%" mod = mod.Where(fmt.Sprintf("(m.%s LIKE ? OR l.%s LIKE ? OR l.%s LIKE ? OR l.%s LIKE ?)", mc.MemberNo, lc.Username, lc.MobileMask, lc.MemberId), like, like, like, like) } if in.LoginType > 0 { mod = mod.Where("l."+lc.LoginType, in.LoginType) } if in.Status > 0 { mod = mod.Where("l."+lc.Status, in.Status) } if in.LoginDomain != "" { mod = mod.WhereLike("l."+lc.LoginDomain, "%"+strings.ToLower(strings.TrimSpace(in.LoginDomain))+"%") } if in.LoginIp != "" { mod = mod.WhereLike("l."+lc.LoginIpMask, "%"+strings.TrimSpace(in.LoginIp)+"%") } if len(in.LoginAt) == 2 { mod = mod.WhereBetween("l."+lc.LoginAt, in.LoginAt[0], in.LoginAt[1]) } totalCount, err = mod.Clone().Count() if err != nil || totalCount == 0 { return } mod = mod.Fields( "l."+lc.Id, "l."+lc.ReqId, "l."+lc.MemberId, fmt.Sprintf("COALESCE(m.%s,'') member_no", mc.MemberNo), "l."+lc.Username, "l."+lc.MobileMask, "l."+lc.LoginType, "l."+lc.Status, "l."+lc.ErrMsg, "l."+lc.LoginAt, "l."+lc.LoginIpMask, "l."+lc.LoginRegion, "l."+lc.LoginDomain, "l."+lc.UserAgent, ) err = mod.Page(in.Page, in.PerPage).OrderDesc("l." + lc.LoginAt).OrderDesc("l." + lc.Id).Scan(&list) if err != nil { return nil, 0, gerror.Wrap(err, "查询会员登录日志失败") } for _, row := range list { row.Os = useragent.GetOs(row.UserAgent) row.Browser = useragent.GetBrowser(row.UserAgent) } return } func (s *sSysAppMemberLoginLog) View(ctx context.Context, in *sysin.AppMemberLoginLogViewInp) (res *sysin.AppMemberLoginLogViewModel, err error) { lc, mc := dao.AppMemberLoginLog.Columns(), dao.AppMember.Columns() res = new(sysin.AppMemberLoginLogViewModel) err = s.Model(ctx).As("l").LeftJoin(dao.AppMember.Table()+" m", "m."+mc.Id+"=l."+lc.MemberId).Fields( "l."+lc.Id, "l."+lc.ReqId, "l."+lc.MemberId, fmt.Sprintf("COALESCE(m.%s,'') member_no", mc.MemberNo), "l."+lc.Username, "l."+lc.MobileMask, "l."+lc.LoginType, "l."+lc.Status, "l."+lc.ErrMsg, "l."+lc.LoginAt, "l."+lc.LoginIpMask, "l."+lc.LoginRegion, "l."+lc.LoginDomain, "l."+lc.UserAgent, ).Where("l."+lc.Id, in.Id).Scan(res) if err != nil { return nil, gerror.Wrap(err, "查询会员登录日志详情失败") } if res.Id == 0 { return nil, gerror.New("会员登录日志不存在") } res.Os = useragent.GetOs(res.UserAgent) res.Browser = useragent.GetBrowser(res.UserAgent) return } func (s *sSysAppMemberLoginLog) Export(ctx context.Context, in *sysin.AppMemberLoginLogListInp) error { list, totalCount, err := s.List(ctx, in) if err != nil { return err } var exports []sysin.AppMemberLoginLogExportModel if err = gconv.Scan(list, &exports); err != nil { return err } tags, err := convert.GetEntityDescTags(sysin.AppMemberLoginLogExportModel{}) if err != nil { return err } fileName := "导出会员登录日志-" + gctx.CtxId(ctx) sheetName := fmt.Sprintf("共%v行,共%v页,当前第%v页,本页%v行", totalCount, form.CalPageCount(totalCount, in.PerPage), in.Page, len(exports)) return excel.ExportByStructs(ctx, tags, exports, fileName, sheetName) } func (s *sSysAppMemberLoginLog) Push(ctx context.Context, in *sysin.AppMemberLoginLogPushInp) { r := ghttp.RequestFromCtx(ctx) if r == nil { g.Log().Warningf(ctx, "会员登录日志缺少HTTP请求上下文") return } model := entity.AppMemberLoginLog{ ReqId: gctx.CtxId(ctx), LoginType: in.LoginType, Status: consts.StatusEnabled, LoginAt: gtime.Now(), UserAgent: r.UserAgent(), } if in.Member != nil { model.TenantId = in.Member.TenantId model.MemberId = in.Member.Id model.Username = in.Member.Username model.MobileMask = encrypt.MaskMobile(in.Member.Mobile) } else if in.LoginType == consts.AppMemberLoginTypeSms { model.MobileMask = encrypt.MaskMobile(in.Account) } else { model.Username = strings.TrimSpace(in.Account) } if in.Err != nil { model.Status = consts.StatusDisable model.ErrMsg = normalizeLoginError(in.Err) } clientIP := location.GetClientIp(r) model.LoginIpMask = maskIP(clientIP) if cipher, err := encrypt.EncryptField(ctx, clientIP); err == nil { model.LoginIpCipher = cipher } else { g.Log().Warningf(ctx, "加密会员登录IP失败 memberId:%d err:%+v", model.MemberId, err) } if ipData, err := location.GetLocation(ctx, clientIP); err == nil && ipData != nil { model.LoginRegion = formatLoginRegion(ipData) } else if err != nil { g.Log().Warningf(ctx, "解析会员登录地区失败 memberId:%d ip:%s err:%+v", model.MemberId, model.LoginIpMask, err) } origin := r.Header.Get("Origin") if domain, _, _ := resolveRequestDomain(origin, r.Host); domain != "" { model.LoginDomain = domain } if err := queue.Push(consts.QueueAppMemberLoginLogTopic, model); err != nil { g.Log().Warningf(ctx, "推送会员登录日志失败,改为同步写入 memberId:%d err:%+v", model.MemberId, err) if writeErr := s.RealWrite(ctx, model); writeErr != nil { g.Log().Warningf(ctx, "同步写入会员登录日志失败 memberId:%d err:%+v", model.MemberId, writeErr) } } } func (s *sSysAppMemberLoginLog) RealWrite(ctx context.Context, model entity.AppMemberLoginLog) error { _, err := dao.AppMemberLoginLog.Ctx(ctx).Data(model).OmitEmptyData().Insert() if err != nil { return gerror.Wrap(err, "写入会员登录日志失败") } return nil } func maskIP(value string) string { ip := net.ParseIP(strings.TrimSpace(value)) if ip == nil { return "" } if v4 := ip.To4(); v4 != nil { return fmt.Sprintf("%d.%d.*.*", v4[0], v4[1]) } parts := strings.Split(ip.String(), ":") if len(parts) > 4 { return strings.Join(parts[:4], ":") + ":****" } return ip.String() } func formatLoginRegion(data *location.IpLocationData) string { parts := []string{data.Country, data.Province, data.City, data.Area} seen := make(map[string]struct{}) result := make([]string, 0, len(parts)) for _, part := range parts { part = strings.TrimSpace(part) if part == "" { continue } if _, ok := seen[part]; ok { continue } seen[part] = struct{}{} result = append(result, part) } if len(result) == 0 { return strings.TrimSpace(data.Region) } return strings.Join(result, " ") } func normalizeLoginError(err error) string { if err == nil { return "" } message := err.Error() allowed := []string{"账号或密码错误", "验证码错误或已过期", "会员不存在", "会员已禁用", "风控限制登录", "登录失败,请稍后重试"} for _, item := range allowed { if strings.Contains(message, item) { return item } } return "系统异常" }