package appuser import ( "context" "database/sql" "fmt" "strconv" "strings" "hyapp-admin-server/internal/appctx" ) type AppLoginLog struct { ID int64 `json:"id"` RequestID string `json:"requestId"` UserID string `json:"userId"` DisplayUserID string `json:"displayUserId"` Username string `json:"username"` Avatar string `json:"avatar"` Country string `json:"country"` RegionID int64 `json:"regionId"` RegionName string `json:"regionName"` LoginIP string `json:"loginIp"` IPCountryCode string `json:"ipCountryCode"` Channel string `json:"channel"` Platform string `json:"platform"` Result string `json:"result"` FailureCode string `json:"failureCode"` Blocked bool `json:"blocked"` BlockReason string `json:"blockReason"` RiskHighlighted bool `json:"riskHighlighted"` LoginType string `json:"loginType"` Provider string `json:"provider"` CreatedAtMs int64 `json:"createdAtMs"` } func (s *Service) ListLoginLogs(ctx context.Context, query loginLogQuery) ([]AppLoginLog, int64, error) { if s.userDB == nil { return nil, 0, fmt.Errorf("user mysql is not configured") } query = normalizeLoginLogQuery(query) whereSQL, args := loginLogWhereSQL(ctx, query) total, err := countRows(ctx, s.userDB, whereSQL, args...) if err != nil { return nil, 0, err } rows, err := s.userDB.QueryContext(ctx, fmt.Sprintf(` SELECT la.id, la.request_id, COALESCE(la.user_id, 0), COALESCE(u.current_display_user_id, ''), COALESCE(u.username, ''), COALESCE(u.avatar, ''), COALESCE(u.country, ''), COALESCE(u.region_id, 0), CASE WHEN COALESCE(u.region_id, 0) = 0 THEN 'GLOBAL' ELSE COALESCE((SELECT rg.name FROM regions rg WHERE rg.app_code = u.app_code AND rg.region_id = u.region_id LIMIT 1), '') END, COALESCE(la.client_ip, ''), COALESCE(la.ip_country_code, ''), COALESCE(NULLIF(la.channel, ''), CASE WHEN la.login_type = 'password' THEN 'account' ELSE COALESCE(la.provider, '') END), COALESCE(la.platform, ''), la.result, COALESCE(la.failure_code, ''), la.blocked, la.block_reason, la.login_type, COALESCE(la.provider, ''), la.created_at_ms %s ORDER BY la.created_at_ms DESC, la.id DESC LIMIT ? OFFSET ? `, whereSQL), append(args, query.PageSize, offset(query.Page, query.PageSize))...) if err != nil { return nil, 0, err } defer rows.Close() items := make([]AppLoginLog, 0, query.PageSize) for rows.Next() { item, err := scanAppLoginLog(rows) if err != nil { return nil, 0, err } items = append(items, item) } if err := rows.Err(); err != nil { return nil, 0, err } return items, total, nil } func loginLogWhereSQL(ctx context.Context, query loginLogQuery) (string, []any) { whereSQL := ` FROM login_audit la LEFT JOIN users u ON u.app_code = la.app_code AND u.user_id = la.user_id WHERE la.app_code = ?` args := []any{appctx.FromContext(ctx)} if query.UserID > 0 { whereSQL += " AND la.user_id = ?" args = append(args, query.UserID) } if query.DisplayUserID != "" { whereSQL += " AND u.current_display_user_id = ?" args = append(args, query.DisplayUserID) } if query.IP != "" { whereSQL += " AND la.client_ip = ?" args = append(args, query.IP) } if query.IPCountryCode != "" { whereSQL += " AND la.ip_country_code = ?" args = append(args, query.IPCountryCode) } if query.Channel != "" { whereSQL += " AND la.channel = ?" args = append(args, query.Channel) } if query.Platform != "" { whereSQL += " AND la.platform = ?" args = append(args, query.Platform) } if query.RegionIDSet { whereSQL += " AND COALESCE(u.region_id, 0) = ?" args = append(args, query.RegionID) } switch query.Blocked { case "blocked": whereSQL += " AND (la.blocked = 1 OR la.failure_code = 'AUTH_LOGIN_BLOCKED')" case "not_blocked": whereSQL += " AND la.blocked = 0 AND COALESCE(la.failure_code, '') <> 'AUTH_LOGIN_BLOCKED'" } if query.Result != "" { whereSQL += " AND la.result = ?" args = append(args, query.Result) } if query.StartMs > 0 { whereSQL += " AND la.created_at_ms >= ?" args = append(args, query.StartMs) } if query.EndMs > 0 { whereSQL += " AND la.created_at_ms <= ?" args = append(args, query.EndMs) } if query.Keyword != "" { like := "%" + query.Keyword + "%" whereSQL += " AND (CAST(la.user_id AS CHAR) LIKE ? OR u.current_display_user_id LIKE ? OR u.username LIKE ? OR la.client_ip LIKE ?)" args = append(args, like, like, like, like) } return whereSQL, args } func scanAppLoginLog(rows *sql.Rows) (AppLoginLog, error) { var item AppLoginLog var userID int64 if err := rows.Scan( &item.ID, &item.RequestID, &userID, &item.DisplayUserID, &item.Username, &item.Avatar, &item.Country, &item.RegionID, &item.RegionName, &item.LoginIP, &item.IPCountryCode, &item.Channel, &item.Platform, &item.Result, &item.FailureCode, &item.Blocked, &item.BlockReason, &item.LoginType, &item.Provider, &item.CreatedAtMs, ); err != nil { return AppLoginLog{}, err } if userID > 0 { item.UserID = strconv.FormatInt(userID, 10) } item.Result = normalizeLoginLogResult(item.Result, item.FailureCode, item.Blocked) item.RiskHighlighted = item.Blocked || item.FailureCode == "AUTH_LOGIN_BLOCKED" return item, nil } func normalizeLoginLogResult(result string, failureCode string, blocked bool) string { result = strings.TrimSpace(result) if result != "" { return result } if blocked || failureCode == "AUTH_LOGIN_BLOCKED" { return "blocked" } return "failed" } func normalizeLoginLogQuery(query loginLogQuery) loginLogQuery { if query.Page < 1 { query.Page = 1 } if query.PageSize < 1 { query.PageSize = 20 } if query.PageSize > 100 { query.PageSize = 100 } query.Keyword = strings.TrimSpace(query.Keyword) query.DisplayUserID = strings.TrimSpace(query.DisplayUserID) query.IP = strings.TrimSpace(query.IP) query.IPCountryCode = normalizeCountryCode(query.IPCountryCode) query.Channel = strings.ToLower(strings.TrimSpace(query.Channel)) query.Platform = strings.ToLower(strings.TrimSpace(query.Platform)) query.Blocked = strings.ToLower(strings.TrimSpace(query.Blocked)) query.Result = strings.ToLower(strings.TrimSpace(query.Result)) if query.Result == "blocked" { query.Blocked = "blocked" query.Result = "" } if query.Blocked != "blocked" && query.Blocked != "not_blocked" { query.Blocked = "all" } switch query.Result { case "", "failed", "revoked", "success": default: query.Result = "" } return query }