2026-06-29 14:26:06 +08:00

245 lines
7.2 KiB
Go

package appuser
import (
"context"
"database/sql"
"fmt"
"strconv"
"strings"
"hyapp-admin-server/internal/appctx"
"hyapp-admin-server/internal/modules/shared"
)
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)}
nowMs := nowMillis()
if query.UserID > 0 {
whereSQL += " AND la.user_id = ?"
args = append(args, query.UserID)
}
if query.DisplayUserID != "" {
matchSQL, matchArgs := shared.UserDisplayIDSQL("u", query.DisplayUserID, nowMs)
whereSQL += " AND " + matchSQL
args = append(args, matchArgs...)
}
if !query.UserFilter.IsEmpty() {
matchSQL, matchArgs := shared.UserIdentityFieldsSQL("u", "la.user_id", query.UserFilter, nowMs)
whereSQL += " AND " + matchSQL
args = append(args, matchArgs...)
}
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 != "" && query.UserFilter.IsEmpty() {
like := "%" + query.Keyword + "%"
matchSQL, matchArgs := shared.UserIdentityKeywordSQL("u", "la.user_id", query.Keyword, nowMs)
whereSQL += " AND (" + matchSQL + " OR la.client_ip LIKE ?)"
args = append(args, matchArgs...)
args = append(args, 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.UserFilter.UserID = strings.TrimSpace(query.UserFilter.UserID)
query.UserFilter.DisplayUserID = strings.TrimSpace(query.UserFilter.DisplayUserID)
query.UserFilter.Username = strings.TrimSpace(query.UserFilter.Username)
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
}