package dashboard import ( "context" "fmt" "strings" "time" ) // CoinSellerRechargeBillQuery 是 finance legacy 列表需要的币商进货明细筛选。 // 它只服务只读展示;金额口径必须与 StatisticsOverview 的 coin_seller_recharge_usd_minor 保持一致。 type CoinSellerRechargeBillQuery struct { Keyword string RegionID int64 StatTZ string StartMS int64 EndMS int64 Page int PageSize int } type CoinSellerRechargeBill struct { TransactionID string Source string UserID int64 CountryCode string USDMinorAmount int64 CreatedAtMS int64 } type CoinSellerRechargeBillSource interface { ListCoinSellerRechargeBills(ctx context.Context, query CoinSellerRechargeBillQuery) ([]CoinSellerRechargeBill, int64, error) } func (s *AslanMongoDashboardSource) ListCoinSellerRechargeBills(ctx context.Context, query CoinSellerRechargeBillQuery) ([]CoinSellerRechargeBill, int64, error) { if s == nil || s.legacyDB == nil { return nil, 0, fmt.Errorf("aslan legacy mysql is not configured") } countryFilter, err := s.countryFilter(ctx, StatisticsQuery{RegionID: query.RegionID}) if err != nil { return nil, 0, err } if countryFilter.Applied && len(countryFilter.Codes) == 0 { return []CoinSellerRechargeBill{}, 0, nil } page := query.Page if page < 1 { page = 1 } pageSize := query.PageSize if pageSize < 1 { pageSize = 10 } unionSQL, args := s.coinSellerRechargeBillUnionSQL(query, countryFilter) queryCtx, cancel := context.WithTimeout(ctx, s.requestTimeout) defer cancel() var total int64 if err := s.legacyDB.QueryRowContext(queryCtx, `SELECT COUNT(*) FROM (`+unionSQL+`) bills`, args...).Scan(&total); err != nil { return nil, 0, fmt.Errorf("count aslan coin seller recharge bills: %w", err) } if total == 0 { return []CoinSellerRechargeBill{}, 0, nil } rows, err := s.legacyDB.QueryContext(queryCtx, ` SELECT transaction_id, source, user_id, country_code, amount, created_at FROM (`+unionSQL+`) bills ORDER BY created_at DESC, transaction_id DESC LIMIT ? OFFSET ?`, append(args, pageSize, (page-1)*pageSize)...) if err != nil { return nil, 0, fmt.Errorf("query aslan coin seller recharge bills: %w", err) } defer rows.Close() items := make([]CoinSellerRechargeBill, 0, pageSize) for rows.Next() { var item CoinSellerRechargeBill var amountText string var createdAt time.Time if err := rows.Scan(&item.TransactionID, &item.Source, &item.UserID, &item.CountryCode, &amountText, &createdAt); err != nil { return nil, 0, fmt.Errorf("scan aslan coin seller recharge bill: %w", err) } amount, err := decimalTextToMinor(amountText) if err != nil { return nil, 0, fmt.Errorf("parse aslan coin seller recharge bill %s amount: %w", item.TransactionID, err) } item.CountryCode = aslanMongoCountryCode(item.CountryCode) item.USDMinorAmount = amount item.CreatedAtMS = createdAt.UnixMilli() items = append(items, item) } if err := rows.Err(); err != nil { return nil, 0, fmt.Errorf("iterate aslan coin seller recharge bills: %w", err) } return items, total, nil } func (s *AslanMongoDashboardSource) coinSellerRechargeBillUnionSQL(query CoinSellerRechargeBillQuery, countryFilter externalDashboardCountryFilter) (string, []any) { goldWhere := []string{ "ugcr.sys_origin = ?", "ugcr.gold_coin_source = 2", "ugcr.recharge_amount > 0", "ubi.country_code IS NOT NULL", "ubi.country_code != ''", } goldArgs := []any{s.sysOrigin} freightWhere := []string{ "fbrw.sys_origin = ?", "fbrw.origin = 'PURCHASE'", "fbrw.type = 0", "(COALESCE(fbrw.amount, 0) <> 0 OR COALESCE(fbrw.usd_quantity, 0) <> 0)", "ubi.country_code IS NOT NULL", "ubi.country_code != ''", } freightArgs := []any{s.sysOrigin} if query.StartMS > 0 { start := time.UnixMilli(query.StartMS) goldWhere = append(goldWhere, "ugcr.create_time >= ?") goldArgs = append(goldArgs, start) freightWhere = append(freightWhere, "fbrw.create_time >= ?") freightArgs = append(freightArgs, start) } if query.EndMS > 0 { end := time.UnixMilli(query.EndMS) goldWhere = append(goldWhere, "ugcr.create_time < ?") goldArgs = append(goldArgs, end) freightWhere = append(freightWhere, "fbrw.create_time < ?") freightArgs = append(freightArgs, end) } if keyword := strings.TrimSpace(query.Keyword); keyword != "" { goldWhere = append(goldWhere, "(CAST(ugcr.id AS CHAR) = ? OR CONCAT('aslan-gold-coin-', ugcr.id) = ? OR CAST(ugcr.user_id AS CHAR) = ?)") goldArgs = append(goldArgs, keyword, keyword, keyword) freightWhere = append(freightWhere, "(CAST(fbrw.id AS CHAR) = ? OR CONCAT('aslan-freight-purchase-', fbrw.id) = ? OR CAST(fbrw.user_id AS CHAR) = ?)") freightArgs = append(freightArgs, keyword, keyword, keyword) } if countryFilter.Applied { placeholders := make([]string, 0, len(countryFilter.Codes)) for _, code := range countryFilter.Codes { normalized := aslanMongoCountryCode(code) if normalized == "" { continue } placeholders = append(placeholders, "?") goldArgs = append(goldArgs, normalized) freightArgs = append(freightArgs, normalized) } if len(placeholders) == 0 { placeholders = append(placeholders, "?") goldArgs = append(goldArgs, "__NO_COUNTRY__") freightArgs = append(freightArgs, "__NO_COUNTRY__") } condition := "UPPER(ubi.country_code) IN (" + strings.Join(placeholders, ",") + ")" goldWhere = append(goldWhere, condition) freightWhere = append(freightWhere, condition) } walletDatabase := strings.TrimSpace(s.legacyWalletDatabase) if walletDatabase == "" { walletDatabase = "atyou_wallet" } unionSQL := ` SELECT CONCAT('aslan-gold-coin-', ugcr.id) AS transaction_id, 'gold_coin_recharge' AS source, ugcr.user_id, UPPER(ubi.country_code) AS country_code, CAST(ugcr.recharge_amount AS CHAR) AS amount, ugcr.create_time AS created_at FROM user_gold_coin_recharge ugcr INNER JOIN user_base_info ubi ON ubi.id = ugcr.user_id WHERE ` + strings.Join(goldWhere, " AND ") + ` UNION ALL SELECT CONCAT('aslan-freight-purchase-', fbrw.id) AS transaction_id, 'freight_purchase' AS source, fbrw.user_id, UPPER(ubi.country_code) AS country_code, CAST(CASE WHEN fbrw.amount IS NOT NULL AND fbrw.amount <> 0 THEN fbrw.amount ELSE COALESCE(fbrw.usd_quantity, 0) END AS CHAR) AS amount, fbrw.create_time AS created_at FROM ` + quoteMySQLIdentifier(walletDatabase) + `.user_freight_balance_running_water fbrw INNER JOIN user_base_info ubi ON ubi.id = fbrw.user_id WHERE ` + strings.Join(freightWhere, " AND ") return unionSQL, append(goldArgs, freightArgs...) }