feat: add member URL pool and login tracking
This commit is contained in:
@@ -0,0 +1,233 @@
|
||||
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 "系统异常"
|
||||
}
|
||||
Reference in New Issue
Block a user