package dashboard import ( "context" "database/sql" "fmt" "strings" "time" ) // ListCoinSellerRechargeBills 把 dashboard-cdc-worker 的 dealer_recharge 聚合事实暴露给 finance 充值流水。 // Yumi 当前只有按天/国家沉淀的币商充值金额,没有单笔订单号和币商用户 ID;这里生成稳定 synthetic id, // 让列表、导出和区域筛选至少与总览/Databi 的金额口径完全一致。 func (s *MySQLExternalDashboardSource) ListCoinSellerRechargeBills(ctx context.Context, query CoinSellerRechargeBillQuery) ([]CoinSellerRechargeBill, int64, error) { if s == nil || s.db == nil { return nil, 0, fmt.Errorf("external dashboard source 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 } requestTimezone := strings.TrimSpace(query.StatTZ) if requestTimezone == "" { requestTimezone = s.statTimezone } requestLocation, err := time.LoadLocation(requestTimezone) if err != nil { return nil, 0, fmt.Errorf("load external dashboard bill request timezone %s: %w", requestTimezone, err) } storageTimezone := s.statTimezone if _, err := time.LoadLocation(storageTimezone); err != nil { return nil, 0, fmt.Errorf("load external dashboard bill storage timezone %s: %w", storageTimezone, err) } startDate, endDate, _, _ := externalDashboardDateRange(StatisticsQuery{StartMS: query.StartMS, EndMS: query.EndMS}, requestLocation) whereSQL, args := s.periodMetricWhere(storageTimezone, startDate, endDate, countryFilter) whereSQL, args = s.appendCoinSellerBillKeyword(whereSQL, args, query.Keyword) page := query.Page if page < 1 { page = 1 } pageSize := query.PageSize if pageSize < 1 { pageSize = 10 } queryCtx, cancel := context.WithTimeout(ctx, s.requestTimeout) defer cancel() var total int64 if err := s.db.QueryRowContext(queryCtx, ` SELECT COUNT(*) FROM ( SELECT t.period_key, t.country_code FROM country_dashboard_period_metric t WHERE `+whereSQL+` GROUP BY t.period_key, t.country_code HAVING COALESCE(SUM(t.dealer_recharge), 0) <> 0 ) bills`, args...).Scan(&total); err != nil { return nil, 0, fmt.Errorf("count external coin seller recharge bills for %s: %w", s.appCode, err) } if total == 0 { return []CoinSellerRechargeBill{}, 0, nil } rows, err := s.db.QueryContext(queryCtx, ` SELECT t.period_key, UPPER(t.country_code), MAX(t.country_name), CAST(COALESCE(SUM(t.dealer_recharge), 0) AS CHAR) FROM country_dashboard_period_metric t WHERE `+whereSQL+` GROUP BY t.period_key, t.country_code HAVING COALESCE(SUM(t.dealer_recharge), 0) <> 0 ORDER BY t.period_key DESC, t.country_code LIMIT ? OFFSET ?`, append(args, pageSize, (page-1)*pageSize)...) if err != nil { return nil, 0, fmt.Errorf("query external coin seller recharge bills for %s: %w", s.appCode, err) } defer rows.Close() items := make([]CoinSellerRechargeBill, 0, pageSize) for rows.Next() { var periodKey string var countryCode string var countryName sql.NullString var amountText string if err := rows.Scan(&periodKey, &countryCode, &countryName, &amountText); err != nil { return nil, 0, fmt.Errorf("scan external coin seller recharge bill for %s: %w", s.appCode, err) } amount, err := decimalTextToMinor(amountText) if err != nil { return nil, 0, fmt.Errorf("parse external coin seller recharge amount for %s/%s/%s: %w", s.appCode, periodKey, countryCode, err) } createdAt, err := time.ParseInLocation("2006-01-02", periodKey, requestLocation) if err != nil { return nil, 0, fmt.Errorf("parse external coin seller recharge day for %s/%s/%s: %w", s.appCode, periodKey, countryCode, err) } countryCode = strings.ToUpper(strings.TrimSpace(countryCode)) items = append(items, CoinSellerRechargeBill{ TransactionID: fmt.Sprintf("%s-dealer-recharge-%s-%s", s.appCode, periodKey, countryCode), Source: "dealer_recharge", CountryCode: countryCode, USDMinorAmount: amount, CreatedAtMS: createdAt.UnixMilli(), }) } if err := rows.Err(); err != nil { return nil, 0, fmt.Errorf("iterate external coin seller recharge bills for %s: %w", s.appCode, err) } return items, total, nil } func (s *MySQLExternalDashboardSource) appendCoinSellerBillKeyword(whereSQL string, args []any, rawKeyword string) (string, []any) { keyword := strings.TrimSpace(rawKeyword) if keyword == "" { return whereSQL, args } upperKeyword := strings.ToUpper(keyword) return whereSQL + ` AND (CONCAT(?, '-dealer-recharge-', t.period_key, '-', UPPER(t.country_code)) = ? OR UPPER(t.country_code) = ? OR t.country_name LIKE ?)`, append(args, s.appCode, keyword, upperKeyword, "%"+keyword+"%") }