From c2bee924ceb4a0916795606ac3f40bf93ad0ddd3 Mon Sep 17 00:00:00 2001 From: hy001 Date: Fri, 8 May 2026 12:20:12 +0800 Subject: [PATCH] feat(console): add dashboard timezone preaggregation --- .../adapter/app/user/DatavRestController.java | 9 +- .../CountryDashboardDailyMetricService.java | 317 ++++++++++++----- .../datav/CountryDashboardServiceImpl.java | 178 +++++++--- .../datav/CountryDashboardCO.java | 2 + .../dto/cmd/datav/CountryDashboardQryCmd.java | 5 + .../rds/dao/datav/CountryDashboardDAO.java | 76 +++- .../CountryDashboardPeriodMetricDTO.java | 2 + .../dao/datav/CountryDashboardDAO.xml | 336 ++++++++++++++++-- .../docs/country-dashboard-data-query.md | 43 ++- ...260507_country_dashboard_period_metric.sql | 14 +- ...untry_dashboard_period_metric_timezone.sql | 141 ++++++++ 11 files changed, 928 insertions(+), 195 deletions(-) create mode 100644 sql/20260508_country_dashboard_period_metric_timezone.sql diff --git a/rc-service/rc-service-console/console-adapter/src/main/java/com/red/circle/console/adapter/app/user/DatavRestController.java b/rc-service/rc-service-console/console-adapter/src/main/java/com/red/circle/console/adapter/app/user/DatavRestController.java index 40f25a6d..59d0613d 100644 --- a/rc-service/rc-service-console/console-adapter/src/main/java/com/red/circle/console/adapter/app/user/DatavRestController.java +++ b/rc-service/rc-service-console/console-adapter/src/main/java/com/red/circle/console/adapter/app/user/DatavRestController.java @@ -67,15 +67,18 @@ public class DatavRestController extends BaseController { public Map backfillCountryDashboard( @RequestParam String startDate, @RequestParam String endDate, - @RequestParam(value = "sysOrigin", defaultValue = "LIKEI") String sysOrigin) { + @RequestParam(value = "sysOrigin", defaultValue = "LIKEI") String sysOrigin, + @RequestParam(value = "statTimezones", required = false) String statTimezones) { LocalDate parsedStartDate = LocalDate.parse(startDate); LocalDate parsedEndDate = LocalDate.parse(endDate); - countryDashboardDailyMetricService.refreshRange(parsedStartDate, parsedEndDate, sysOrigin); + List refreshedTimezones = countryDashboardDailyMetricService.refreshRange( + parsedStartDate, parsedEndDate, sysOrigin, statTimezones); return Map.of( "status", "success", "startDate", parsedStartDate.toString(), "endDate", parsedEndDate.toString(), - "sysOrigin", sysOrigin); + "sysOrigin", sysOrigin, + "statTimezones", refreshedTimezones); } } diff --git a/rc-service/rc-service-console/console-application/src/main/java/com/red/circle/console/app/service/app/datav/CountryDashboardDailyMetricService.java b/rc-service/rc-service-console/console-application/src/main/java/com/red/circle/console/app/service/app/datav/CountryDashboardDailyMetricService.java index be4d677d..672b0fc7 100644 --- a/rc-service/rc-service-console/console-application/src/main/java/com/red/circle/console/app/service/app/datav/CountryDashboardDailyMetricService.java +++ b/rc-service/rc-service-console/console-application/src/main/java/com/red/circle/console/app/service/app/datav/CountryDashboardDailyMetricService.java @@ -6,10 +6,12 @@ import com.red.circle.console.infra.database.rds.dto.datav.CountryDashboardDaily import com.red.circle.console.infra.database.rds.dto.datav.CountryDashboardMetricDTO; import com.red.circle.console.infra.database.rds.dto.datav.CountryDashboardPeriodMetricDTO; import java.math.BigDecimal; -import java.sql.Timestamp; import java.time.DayOfWeek; +import java.time.Instant; import java.time.LocalDate; import java.time.LocalDateTime; +import java.time.ZoneId; +import java.time.ZoneOffset; import java.time.format.DateTimeFormatter; import java.time.temporal.ChronoUnit; import java.time.temporal.WeekFields; @@ -44,6 +46,9 @@ public class CountryDashboardDailyMetricService { private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd"); private static final DateTimeFormatter MONTH_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM"); private static final WeekFields WEEK_FIELDS = WeekFields.ISO; + private static final ZoneId STORAGE_ZONE_ID = ZoneId.of("Asia/Riyadh"); + private static final String STORAGE_TIMEZONE = "Asia/Riyadh"; + private static final String STORAGE_TIMEZONE_OFFSET = "+03:00"; private final CountryDashboardDAO countryDashboardDAO; private final TransactionTemplate transactionTemplate; @@ -51,37 +56,54 @@ public class CountryDashboardDailyMetricService { @Value("${red-circle.country-dashboard.refresh-sys-origins:LIKEI}") private String refreshSysOrigins; + @Value("${red-circle.country-dashboard.refresh-timezones:Asia/Riyadh,UTC,Asia/Shanghai}") + private String refreshTimezones; + private volatile boolean metricSchemaEnsured; public void refreshRecentDays(int days) { ensureMetricSchema(); int safeDays = Math.max(days, 1); - LocalDate today = LocalDate.now(); - List statDates = new ArrayList<>(); - for (int i = safeDays - 1; i >= 0; i--) { - statDates.add(today.minusDays(i)); - } + List storageStatDates = recentDates(safeDays, STORAGE_ZONE_ID); + List statTimezones = resolveStatTimezones(null); List sysOrigins = resolveSysOrigins(); for (String sysOrigin : sysOrigins) { - for (LocalDate statDate : statDates) { + for (LocalDate statDate : storageStatDates) { try { - refreshDay(statDate, sysOrigin); + refreshDailyMetric(statDate, sysOrigin); } catch (Exception ex) { log.error("refresh country dashboard daily metric failed, statDate={}, sysOrigin={}", statDate, sysOrigin, ex); } } - refreshAffectedPeriods(statDates, sysOrigin, false); - try { - refreshAll(sysOrigin); - } catch (Exception ex) { - log.error("refresh country dashboard all metric failed, sysOrigin={}", sysOrigin, ex); + for (StatTimezone statTimezone : statTimezones) { + List statDates = recentDates(safeDays, statTimezone.zoneId()); + for (LocalDate statDate : statDates) { + try { + refreshPeriod(toDayWindow(statDate), sysOrigin, statTimezone); + } catch (Exception ex) { + log.error("refresh country dashboard day period metric failed, statDate={}, sysOrigin={}, statTimezone={}", + statDate, sysOrigin, statTimezone.id(), ex); + } + } + refreshAffectedPeriods(statDates, sysOrigin, statTimezone, false); + try { + refreshAll(sysOrigin, statTimezone); + } catch (Exception ex) { + log.error("refresh country dashboard all metric failed, sysOrigin={}, statTimezone={}", + sysOrigin, statTimezone.id(), ex); + } } } } public void refreshRange(LocalDate startDate, LocalDate endDate, String sysOrigin) { + refreshRange(startDate, endDate, sysOrigin, null); + } + + public List refreshRange(LocalDate startDate, LocalDate endDate, String sysOrigin, + String statTimezonesValue) { ensureMetricSchema(); if (startDate == null || endDate == null) { throw new IllegalArgumentException("startDate and endDate are required"); @@ -104,16 +126,25 @@ public class CountryDashboardDailyMetricService { for (long i = 0; i < dayCount; i++) { statDates.add(startDate.plusDays(i)); } + List statTimezones = resolveStatTimezones(statTimezonesValue); - log.info("refresh country dashboard range start, startDate={}, endDate={}, days={}, sysOrigin={}", - startDate, endDate, dayCount, safeSysOrigin); + log.info("refresh country dashboard range start, startDate={}, endDate={}, days={}, sysOrigin={}, statTimezones={}", + startDate, endDate, dayCount, safeSysOrigin, + statTimezones.stream().map(StatTimezone::id).toList()); for (LocalDate statDate : statDates) { - refreshDay(statDate, safeSysOrigin); + refreshDailyMetric(statDate, safeSysOrigin); } - refreshAffectedPeriods(statDates, safeSysOrigin, true); - refreshAll(safeSysOrigin); - log.info("refresh country dashboard range success, startDate={}, endDate={}, days={}, sysOrigin={}, costMs={}", - startDate, endDate, dayCount, safeSysOrigin, System.currentTimeMillis() - start); + for (StatTimezone statTimezone : statTimezones) { + for (LocalDate statDate : statDates) { + refreshPeriod(toDayWindow(statDate), safeSysOrigin, statTimezone); + } + refreshAffectedPeriods(statDates, safeSysOrigin, statTimezone, true); + refreshAll(safeSysOrigin, statTimezone); + } + log.info("refresh country dashboard range success, startDate={}, endDate={}, days={}, sysOrigin={}, statTimezones={}, costMs={}", + startDate, endDate, dayCount, safeSysOrigin, + statTimezones.stream().map(StatTimezone::id).toList(), System.currentTimeMillis() - start); + return statTimezones.stream().map(StatTimezone::id).toList(); } private void ensureMetricSchema() { @@ -127,42 +158,62 @@ public class CountryDashboardDailyMetricService { countryDashboardDAO.createDailyMetricTable(); countryDashboardDAO.createPeriodMetricTable(); countryDashboardDAO.createPeriodUserMetricTable(); + countryDashboardDAO.ensurePeriodMetricTimezoneSchema(); metricSchemaEnsured = true; log.info("country dashboard metric tables ensured"); } } public void refreshDay(LocalDate statDate, String sysOrigin) { + ensureMetricSchema(); + refreshDailyMetric(statDate, sysOrigin); + for (StatTimezone statTimezone : resolveStatTimezones(null)) { + refreshPeriod(toDayWindow(statDate), sysOrigin, statTimezone); + } + } + + private void refreshDailyMetric(LocalDate statDate, String sysOrigin) { long start = System.currentTimeMillis(); LocalDateTime startTime = statDate.atStartOfDay(); LocalDateTime endTime = statDate.plusDays(1).atStartOfDay(); - Long startTimeMillis = Timestamp.valueOf(startTime).getTime(); - Long endTimeMillis = Timestamp.valueOf(endTime).getTime(); + Long startTimeMillis = statDate.atStartOfDay(STORAGE_ZONE_ID).toInstant().toEpochMilli(); + Long endTimeMillis = statDate.plusDays(1).atStartOfDay(STORAGE_ZONE_ID).toInstant().toEpochMilli(); Map rows = new LinkedHashMap<>(); mergeSql(rows, countryDashboardDAO.listNewUsers( - PERIOD_DAY, startTime, endTime, null, sysOrigin), statDate, sysOrigin); + PERIOD_DAY, startTime, endTime, null, sysOrigin, + STORAGE_TIMEZONE_OFFSET, STORAGE_TIMEZONE_OFFSET), statDate, sysOrigin); mergeSql(rows, countryDashboardDAO.listNewUserRecharge( - PERIOD_DAY, startTime, endTime, null, sysOrigin), statDate, sysOrigin); + PERIOD_DAY, startTime, endTime, null, sysOrigin, + STORAGE_TIMEZONE_OFFSET, STORAGE_TIMEZONE_OFFSET), statDate, sysOrigin); mergeSql(rows, countryDashboardDAO.listOfficialRecharge( - PERIOD_DAY, startTime, endTime, null, sysOrigin), statDate, sysOrigin); + PERIOD_DAY, startTime, endTime, null, sysOrigin, + STORAGE_TIMEZONE_OFFSET, STORAGE_TIMEZONE_OFFSET), statDate, sysOrigin); mergeSql(rows, countryDashboardDAO.listMifapayRecharge( - PERIOD_DAY, startTime, endTime, null, sysOrigin), statDate, sysOrigin); + PERIOD_DAY, startTime, endTime, null, sysOrigin, + STORAGE_TIMEZONE_OFFSET, STORAGE_TIMEZONE_OFFSET), statDate, sysOrigin); mergeSql(rows, countryDashboardDAO.listGoogleRecharge( - PERIOD_DAY, startTime, endTime, null, sysOrigin), statDate, sysOrigin); + PERIOD_DAY, startTime, endTime, null, sysOrigin, + STORAGE_TIMEZONE_OFFSET, STORAGE_TIMEZONE_OFFSET), statDate, sysOrigin); mergeSql(rows, countryDashboardDAO.listDealerRecharge( - PERIOD_DAY, startTime, endTime, null, sysOrigin), statDate, sysOrigin); + PERIOD_DAY, startTime, endTime, null, sysOrigin, + STORAGE_TIMEZONE_OFFSET, STORAGE_TIMEZONE_OFFSET), statDate, sysOrigin); mergeSql(rows, countryDashboardDAO.listSalaryExchange( - PERIOD_DAY, startTime, endTime, null, sysOrigin), statDate, sysOrigin); + PERIOD_DAY, startTime, endTime, null, sysOrigin, + STORAGE_TIMEZONE_OFFSET, STORAGE_TIMEZONE_OFFSET), statDate, sysOrigin); mergeSql(rows, countryDashboardDAO.listLuckyGift( - PERIOD_DAY, startTime, endTime, null, sysOrigin), statDate, sysOrigin); + PERIOD_DAY, startTime, endTime, null, sysOrigin, + STORAGE_TIMEZONE_OFFSET, STORAGE_TIMEZONE_OFFSET), statDate, sysOrigin); mergeSql(rows, countryDashboardDAO.listWalletGame( - PERIOD_DAY, startTime, endTime, startTimeMillis, endTimeMillis, null, sysOrigin), + PERIOD_DAY, startTime, endTime, startTimeMillis, endTimeMillis, null, sysOrigin, + STORAGE_TIMEZONE_OFFSET, STORAGE_TIMEZONE_OFFSET), statDate, sysOrigin); mergeSql(rows, countryDashboardDAO.listLuckyBoxGame( - PERIOD_DAY, startTime, endTime, null, sysOrigin), statDate, sysOrigin); + PERIOD_DAY, startTime, endTime, null, sysOrigin, + STORAGE_TIMEZONE_OFFSET, STORAGE_TIMEZONE_OFFSET), statDate, sysOrigin); mergeSql(rows, countryDashboardDAO.listGameUser( - PERIOD_DAY, startTime, endTime, startTimeMillis, endTimeMillis, null, sysOrigin), + PERIOD_DAY, startTime, endTime, startTimeMillis, endTimeMillis, null, sysOrigin, + STORAGE_TIMEZONE_OFFSET, STORAGE_TIMEZONE_OFFSET), statDate, sysOrigin); List metrics = new ArrayList<>(rows.values()); @@ -174,103 +225,122 @@ public class CountryDashboardDailyMetricService { log.info("refresh country dashboard daily metric success, statDate={}, sysOrigin={}, countries={}, deleted={}, inserted={}, costMs={}", statDate, sysOrigin, metrics.size(), writeResult.deleted(), writeResult.inserted(), System.currentTimeMillis() - start); - refreshPeriod(toDayWindow(statDate), sysOrigin); } private void refreshAffectedPeriods(List statDates, String sysOrigin, - boolean failFast) { + StatTimezone statTimezone, boolean failFast) { Set windows = new LinkedHashSet<>(); for (LocalDate statDate : statDates) { - windows.add(toWeekWindow(statDate)); - windows.add(toMonthWindow(statDate)); + windows.add(toWeekWindow(statDate, statTimezone.zoneId())); + windows.add(toMonthWindow(statDate, statTimezone.zoneId())); } for (PeriodWindow window : windows) { try { - refreshPeriod(window, sysOrigin); + refreshPeriod(window, sysOrigin, statTimezone); } catch (Exception ex) { if (failFast) { throw ex; } - log.error("refresh country dashboard period metric failed, periodType={}, periodKey={}, sysOrigin={}", - window.periodType(), window.periodKey(), sysOrigin, ex); + log.error("refresh country dashboard period metric failed, periodType={}, periodKey={}, sysOrigin={}, statTimezone={}", + window.periodType(), window.periodKey(), sysOrigin, statTimezone.id(), ex); } } } - private void refreshPeriod(PeriodWindow window, String sysOrigin) { + private void refreshPeriod(PeriodWindow window, String sysOrigin, StatTimezone statTimezone) { long start = System.currentTimeMillis(); - LocalDateTime startTime = window.periodStartDate().atStartOfDay(); - LocalDateTime endTime = window.periodEndDate().atStartOfDay(); - Long startTimeMillis = Timestamp.valueOf(startTime).getTime(); - Long endTimeMillis = Timestamp.valueOf(endTime).getTime(); + TimeWindow timeWindow = toStorageTimeWindow(window, statTimezone); + LocalDateTime startTime = timeWindow.startTime(); + LocalDateTime endTime = timeWindow.endTime(); + Long startTimeMillis = timeWindow.startTimeMillis(); + Long endTimeMillis = timeWindow.endTimeMillis(); Map rows = new LinkedHashMap<>(); mergePeriodAmounts(rows, countryDashboardDAO.listNewUserRecharge( - window.periodType(), startTime, endTime, null, sysOrigin), window, sysOrigin); + window.periodType(), startTime, endTime, null, sysOrigin, + STORAGE_TIMEZONE_OFFSET, statTimezone.offset()), window, sysOrigin, statTimezone); mergePeriodAmounts(rows, countryDashboardDAO.listOfficialRecharge( - window.periodType(), startTime, endTime, null, sysOrigin), window, sysOrigin); + window.periodType(), startTime, endTime, null, sysOrigin, + STORAGE_TIMEZONE_OFFSET, statTimezone.offset()), window, sysOrigin, statTimezone); mergePeriodAmounts(rows, countryDashboardDAO.listMifapayRecharge( - window.periodType(), startTime, endTime, null, sysOrigin), window, sysOrigin); + window.periodType(), startTime, endTime, null, sysOrigin, + STORAGE_TIMEZONE_OFFSET, statTimezone.offset()), window, sysOrigin, statTimezone); mergePeriodAmounts(rows, countryDashboardDAO.listGoogleRecharge( - window.periodType(), startTime, endTime, null, sysOrigin), window, sysOrigin); + window.periodType(), startTime, endTime, null, sysOrigin, + STORAGE_TIMEZONE_OFFSET, statTimezone.offset()), window, sysOrigin, statTimezone); mergePeriodAmounts(rows, countryDashboardDAO.listDealerRecharge( - window.periodType(), startTime, endTime, null, sysOrigin), window, sysOrigin); + window.periodType(), startTime, endTime, null, sysOrigin, + STORAGE_TIMEZONE_OFFSET, statTimezone.offset()), window, sysOrigin, statTimezone); mergePeriodAmounts(rows, countryDashboardDAO.listSalaryExchange( - window.periodType(), startTime, endTime, null, sysOrigin), window, sysOrigin); + window.periodType(), startTime, endTime, null, sysOrigin, + STORAGE_TIMEZONE_OFFSET, statTimezone.offset()), window, sysOrigin, statTimezone); mergePeriodAmounts(rows, countryDashboardDAO.listLuckyGift( - window.periodType(), startTime, endTime, null, sysOrigin), window, sysOrigin); + window.periodType(), startTime, endTime, null, sysOrigin, + STORAGE_TIMEZONE_OFFSET, statTimezone.offset()), window, sysOrigin, statTimezone); mergePeriodAmounts(rows, countryDashboardDAO.listWalletGame( - window.periodType(), startTime, endTime, startTimeMillis, endTimeMillis, null, sysOrigin), - window, sysOrigin); + window.periodType(), startTime, endTime, startTimeMillis, endTimeMillis, null, sysOrigin, + STORAGE_TIMEZONE_OFFSET, statTimezone.offset()), + window, sysOrigin, statTimezone); mergePeriodAmounts(rows, countryDashboardDAO.listLuckyBoxGame( - window.periodType(), startTime, endTime, null, sysOrigin), window, sysOrigin); + window.periodType(), startTime, endTime, null, sysOrigin, + STORAGE_TIMEZONE_OFFSET, statTimezone.offset()), window, sysOrigin, statTimezone); RefreshWriteResult writeResult = transactionTemplate.execute(status -> { - countryDashboardDAO.deletePeriodUserMetrics(window.periodType(), window.periodKey(), sysOrigin); + countryDashboardDAO.deletePeriodUserMetrics(window.periodType(), window.periodKey(), + statTimezone.id(), sysOrigin); int newUsers = countryDashboardDAO.insertPeriodNewUserMetrics( window.periodType(), window.periodKey(), window.periodName(), - window.periodStartDate(), window.periodEndDate(), startTime, endTime, sysOrigin); + window.periodStartDate(), window.periodEndDate(), statTimezone.id(), + startTime, endTime, sysOrigin); int luckyGiftUsers = countryDashboardDAO.insertPeriodLuckyGiftUserMetrics( window.periodType(), window.periodKey(), window.periodName(), - window.periodStartDate(), window.periodEndDate(), startTime, endTime, sysOrigin); + window.periodStartDate(), window.periodEndDate(), statTimezone.id(), + startTime, endTime, sysOrigin); int gameUsers = countryDashboardDAO.insertPeriodGameUserMetrics( window.periodType(), window.periodKey(), window.periodName(), - window.periodStartDate(), window.periodEndDate(), startTime, endTime, + window.periodStartDate(), window.periodEndDate(), statTimezone.id(), startTime, endTime, startTimeMillis, endTimeMillis, sysOrigin); mergePeriodUserCounts(rows, countryDashboardDAO.listPeriodUserMetricCounts( - window.periodType(), window.periodKey(), sysOrigin), window, sysOrigin); + window.periodType(), window.periodKey(), statTimezone.id(), sysOrigin), + window, sysOrigin, statTimezone); List metrics = new ArrayList<>(rows.values()); - int deleted = countryDashboardDAO.deletePeriodMetrics(window.periodType(), window.periodKey(), sysOrigin); + int deleted = countryDashboardDAO.deletePeriodMetrics(window.periodType(), window.periodKey(), + statTimezone.id(), sysOrigin); int inserted = metrics.isEmpty() ? 0 : countryDashboardDAO.upsertPeriodMetrics(metrics); return new RefreshWriteResult(deleted, inserted, newUsers + luckyGiftUsers + gameUsers); }); - log.info("refresh country dashboard period metric success, periodType={}, periodKey={}, sysOrigin={}, countries={}, deleted={}, inserted={}, userRows={}, costMs={}", - window.periodType(), window.periodKey(), sysOrigin, rows.size(), writeResult.deleted(), - writeResult.inserted(), writeResult.userRows(), System.currentTimeMillis() - start); + log.info("refresh country dashboard period metric success, periodType={}, periodKey={}, sysOrigin={}, statTimezone={}, countries={}, deleted={}, inserted={}, userRows={}, costMs={}", + window.periodType(), window.periodKey(), sysOrigin, statTimezone.id(), rows.size(), + writeResult.deleted(), writeResult.inserted(), writeResult.userRows(), + System.currentTimeMillis() - start); } - private void refreshAll(String sysOrigin) { + private void refreshAll(String sysOrigin, StatTimezone statTimezone) { long start = System.currentTimeMillis(); PeriodWindow window = new PeriodWindow(PERIOD_ALL, PERIOD_ALL, "全部", null, null); Map rows = new LinkedHashMap<>(); - mergePeriodAmounts(rows, countryDashboardDAO.listAllPeriodAmountsFromDaily(sysOrigin), - window, sysOrigin); + mergePeriodAmounts(rows, countryDashboardDAO.listAllPeriodAmountsFromPeriodDays( + statTimezone.id(), sysOrigin), window, sysOrigin, statTimezone); RefreshWriteResult writeResult = transactionTemplate.execute(status -> { - countryDashboardDAO.deletePeriodUserMetrics(PERIOD_ALL, PERIOD_ALL, sysOrigin); - int userRows = countryDashboardDAO.insertAllPeriodUserMetricsFromDaily(sysOrigin); + countryDashboardDAO.deletePeriodUserMetrics(PERIOD_ALL, PERIOD_ALL, + statTimezone.id(), sysOrigin); + int userRows = countryDashboardDAO.insertAllPeriodUserMetricsFromDaily( + statTimezone.id(), sysOrigin); mergePeriodUserCounts(rows, countryDashboardDAO.listPeriodUserMetricCounts( - PERIOD_ALL, PERIOD_ALL, sysOrigin), window, sysOrigin); + PERIOD_ALL, PERIOD_ALL, statTimezone.id(), sysOrigin), window, sysOrigin, + statTimezone); List metrics = new ArrayList<>(rows.values()); - int deleted = countryDashboardDAO.deletePeriodMetrics(PERIOD_ALL, PERIOD_ALL, sysOrigin); + int deleted = countryDashboardDAO.deletePeriodMetrics(PERIOD_ALL, PERIOD_ALL, + statTimezone.id(), sysOrigin); int inserted = metrics.isEmpty() ? 0 : countryDashboardDAO.upsertPeriodMetrics(metrics); return new RefreshWriteResult(deleted, inserted, userRows); }); - log.info("refresh country dashboard all metric success, sysOrigin={}, countries={}, deleted={}, inserted={}, userRows={}, costMs={}", - sysOrigin, rows.size(), writeResult.deleted(), writeResult.inserted(), + log.info("refresh country dashboard all metric success, sysOrigin={}, statTimezone={}, countries={}, deleted={}, inserted={}, userRows={}, costMs={}", + sysOrigin, statTimezone.id(), rows.size(), writeResult.deleted(), writeResult.inserted(), writeResult.userRows(), System.currentTimeMillis() - start); } @@ -298,12 +368,14 @@ public class CountryDashboardDailyMetricService { } private void mergePeriodAmounts(Map rows, - List metrics, PeriodWindow window, String sysOrigin) { + List metrics, PeriodWindow window, String sysOrigin, + StatTimezone statTimezone) { if (metrics == null || metrics.isEmpty()) { return; } for (CountryDashboardMetricDTO metric : metrics) { - CountryDashboardPeriodMetricDTO row = getPeriodRow(rows, metric, window, sysOrigin); + CountryDashboardPeriodMetricDTO row = getPeriodRow(rows, metric, window, sysOrigin, + statTimezone); row.setNewUserRecharge(add(row.getNewUserRecharge(), metric.getNewUserRecharge())); row.setOfficialRecharge(add(row.getOfficialRecharge(), metric.getOfficialRecharge())); row.setMifapayRecharge(add(row.getMifapayRecharge(), metric.getMifapayRecharge())); @@ -318,12 +390,14 @@ public class CountryDashboardDailyMetricService { } private void mergePeriodUserCounts(Map rows, - List metrics, PeriodWindow window, String sysOrigin) { + List metrics, PeriodWindow window, String sysOrigin, + StatTimezone statTimezone) { if (metrics == null || metrics.isEmpty()) { return; } for (CountryDashboardMetricDTO metric : metrics) { - CountryDashboardPeriodMetricDTO row = getPeriodRow(rows, metric, window, sysOrigin); + CountryDashboardPeriodMetricDTO row = getPeriodRow(rows, metric, window, sysOrigin, + statTimezone); row.setCountryNewUser(safeLong(metric.getCountryNewUser())); row.setLuckyGiftUser(safeLong(metric.getLuckyGiftUser())); row.setGameUser(safeLong(metric.getGameUser())); @@ -344,13 +418,15 @@ public class CountryDashboardDailyMetricService { } private CountryDashboardPeriodMetricDTO getPeriodRow(Map rows, - CountryDashboardMetricDTO metric, PeriodWindow window, String sysOrigin) { + CountryDashboardMetricDTO metric, PeriodWindow window, String sysOrigin, + StatTimezone statTimezone) { String countryCode = hasText(metric.getCountryCode()) ? metric.getCountryCode() : "UNKNOWN"; String countryName = hasText(metric.getCountryName()) ? metric.getCountryName() : countryCode; return rows.computeIfAbsent(countryCode, key -> new CountryDashboardPeriodMetricDTO() .setPeriodType(window.periodType()) .setPeriodKey(window.periodKey()) + .setStatTimezone(statTimezone.id()) .setPeriodName(window.periodName()) .setPeriodStartDate(window.periodStartDate()) .setPeriodEndDate(window.periodEndDate()) @@ -364,27 +440,93 @@ public class CountryDashboardDailyMetricService { return new PeriodWindow(PERIOD_DAY, key, key, statDate, statDate.plusDays(1)); } - private PeriodWindow toWeekWindow(LocalDate statDate) { + private PeriodWindow toWeekWindow(LocalDate statDate, ZoneId statZoneId) { LocalDate start = statDate.with(DayOfWeek.MONDAY); - LocalDate end = capCurrentPeriodEnd(start.plusDays(7)); + LocalDate end = capCurrentPeriodEnd(start.plusDays(7), statZoneId); int weekYear = statDate.get(WEEK_FIELDS.weekBasedYear()); int week = statDate.get(WEEK_FIELDS.weekOfWeekBasedYear()); String key = String.format(Locale.ROOT, "%d-W%02d", weekYear, week); return new PeriodWindow(PERIOD_WEEK, key, key, start, end); } - private PeriodWindow toMonthWindow(LocalDate statDate) { + private PeriodWindow toMonthWindow(LocalDate statDate, ZoneId statZoneId) { LocalDate start = statDate.withDayOfMonth(1); - LocalDate end = capCurrentPeriodEnd(start.plusMonths(1)); + LocalDate end = capCurrentPeriodEnd(start.plusMonths(1), statZoneId); String key = statDate.format(MONTH_FORMATTER); return new PeriodWindow(PERIOD_MONTH, key, key, start, end); } - private LocalDate capCurrentPeriodEnd(LocalDate naturalEndExclusive) { - LocalDate tomorrow = LocalDate.now().plusDays(1); + private LocalDate capCurrentPeriodEnd(LocalDate naturalEndExclusive, ZoneId statZoneId) { + LocalDate tomorrow = LocalDate.now(statZoneId).plusDays(1); return naturalEndExclusive.isAfter(tomorrow) ? tomorrow : naturalEndExclusive; } + private List recentDates(int days, ZoneId zoneId) { + LocalDate today = LocalDate.now(zoneId); + List statDates = new ArrayList<>(); + for (int i = days - 1; i >= 0; i--) { + statDates.add(today.minusDays(i)); + } + return statDates; + } + + private TimeWindow toStorageTimeWindow(PeriodWindow window, StatTimezone statTimezone) { + Instant startInstant = window.periodStartDate().atStartOfDay(statTimezone.zoneId()).toInstant(); + Instant endInstant = window.periodEndDate().atStartOfDay(statTimezone.zoneId()).toInstant(); + return new TimeWindow( + LocalDateTime.ofInstant(startInstant, STORAGE_ZONE_ID), + LocalDateTime.ofInstant(endInstant, STORAGE_ZONE_ID), + startInstant.toEpochMilli(), + endInstant.toEpochMilli()); + } + + private List resolveStatTimezones(String statTimezonesValue) { + String config = trimToNull(statTimezonesValue); + if (config == null) { + config = trimToNull(refreshTimezones); + } + if (config == null) { + config = "Asia/Riyadh,UTC,Asia/Shanghai"; + } + + Map zones = new LinkedHashMap<>(); + Arrays.stream(config.split(",")) + .map(String::trim) + .map(this::normalizeStatTimezone) + .filter(Objects::nonNull) + .forEach(zone -> zones.putIfAbsent(zone.id(), zone)); + if (zones.isEmpty()) { + zones.put(STORAGE_TIMEZONE, new StatTimezone(STORAGE_TIMEZONE, STORAGE_ZONE_ID, + STORAGE_TIMEZONE_OFFSET)); + } + return new ArrayList<>(zones.values()); + } + + private StatTimezone normalizeStatTimezone(String statTimezone) { + String value = trimToNull(statTimezone); + if (value == null) { + return null; + } + String upperValue = value.toUpperCase(Locale.ROOT); + ZoneId zoneId = switch (upperValue) { + case "UTC", "Z", "+00:00" -> ZoneId.of("UTC"); + case "ASIA/SHANGHAI", "BEIJING", "CHINA", "+08:00" -> ZoneId.of("Asia/Shanghai"); + case "ASIA/RIYADH", "RIYADH", "SERVER", "+03:00" -> STORAGE_ZONE_ID; + default -> null; + }; + if (zoneId == null) { + log.warn("ignore unsupported country dashboard stat timezone, statTimezone={}", statTimezone); + return null; + } + return new StatTimezone(zoneId.getId(), zoneId, zoneOffset(zoneId)); + } + + private static String zoneOffset(ZoneId zoneId) { + ZoneOffset offset = zoneId.getRules().getOffset(Instant.now()); + String id = offset.getId(); + return "Z".equals(id) ? "+00:00" : id; + } + private List resolveSysOrigins() { String config = trimToNull(refreshSysOrigins); if (config == null) { @@ -433,6 +575,13 @@ public class CountryDashboardDailyMetricService { LocalDate periodStartDate, LocalDate periodEndDate) { } + private record StatTimezone(String id, ZoneId zoneId, String offset) { + } + + private record TimeWindow(LocalDateTime startTime, LocalDateTime endTime, + Long startTimeMillis, Long endTimeMillis) { + } + private record RefreshWriteResult(int deleted, int inserted, int userRows) { } } diff --git a/rc-service/rc-service-console/console-application/src/main/java/com/red/circle/console/app/service/app/datav/CountryDashboardServiceImpl.java b/rc-service/rc-service-console/console-application/src/main/java/com/red/circle/console/app/service/app/datav/CountryDashboardServiceImpl.java index 643560e1..8f34b514 100644 --- a/rc-service/rc-service-console/console-application/src/main/java/com/red/circle/console/app/service/app/datav/CountryDashboardServiceImpl.java +++ b/rc-service/rc-service-console/console-application/src/main/java/com/red/circle/console/app/service/app/datav/CountryDashboardServiceImpl.java @@ -15,9 +15,13 @@ import java.math.BigDecimal; import java.math.RoundingMode; import java.sql.Timestamp; import java.time.DayOfWeek; +import java.time.Instant; import java.time.LocalDate; import java.time.LocalDateTime; import java.time.YearMonth; +import java.time.ZoneId; +import java.time.ZoneOffset; +import java.time.ZonedDateTime; import java.time.format.DateTimeFormatter; import java.time.temporal.WeekFields; import java.util.ArrayList; @@ -54,8 +58,11 @@ public class CountryDashboardServiceImpl implements CountryDashboardService { private static final String COLLECTION_GIFT_GIVE_RUNNING_WATER = "gift_give_running_water"; private static final String COLLECTION_USER_DAILY_ACTIVE_LOG = "user_daily_active_log"; private static final String PERIOD_ALL = "ALL"; + private static final ZoneId STORAGE_ZONE_ID = ZoneId.of("Asia/Riyadh"); + private static final ZoneId DEFAULT_STAT_ZONE_ID = STORAGE_ZONE_ID; private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd"); private static final DateTimeFormatter MONTH_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM"); + private static final DateTimeFormatter DATETIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); private static final int USER_COUNTRY_BATCH_SIZE = 1000; private final CountryDashboardDAO countryDashboardDAO; @@ -69,7 +76,7 @@ public class CountryDashboardServiceImpl implements CountryDashboardService { if (shouldUsePrecomputedMetrics(condition)) { mergeSql(rows, countryDashboardDAO.listPrecomputedPeriodMetrics( condition.periodType, condition.startTime, condition.endTime, - condition.countryKeyword, condition.sysOrigin)); + condition.countryKeyword, condition.statTimezone, condition.sysOrigin)); } else { mergeLiveSql(rows, condition); } @@ -95,7 +102,8 @@ public class CountryDashboardServiceImpl implements CountryDashboardService { .setPeriodType(condition.periodType) .setStartDate(condition.startDate) .setEndDate(condition.endDate) - .setComputedAt(LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))) + .setStatTimezone(condition.statTimezone) + .setComputedAt(ZonedDateTime.now(condition.statZoneId).format(DATETIME_FORMATTER)) .setTotal(total) .setRecords(records); } @@ -103,39 +111,50 @@ public class CountryDashboardServiceImpl implements CountryDashboardService { private void mergeLiveSql(Map rows, QueryCondition condition) { mergeSql(rows, countryDashboardDAO.listNewUsers( condition.periodType, condition.startTime, condition.endTime, - condition.countryKeyword, condition.sysOrigin)); + condition.countryKeyword, condition.sysOrigin, + condition.storageTimezoneOffset, condition.statTimezoneOffset)); mergeSql(rows, countryDashboardDAO.listNewUserRecharge( condition.periodType, condition.startTime, condition.endTime, - condition.countryKeyword, condition.sysOrigin)); + condition.countryKeyword, condition.sysOrigin, + condition.storageTimezoneOffset, condition.statTimezoneOffset)); mergeSql(rows, countryDashboardDAO.listOfficialRecharge( condition.periodType, condition.startTime, condition.endTime, - condition.countryKeyword, condition.sysOrigin)); + condition.countryKeyword, condition.sysOrigin, + condition.storageTimezoneOffset, condition.statTimezoneOffset)); mergeSql(rows, countryDashboardDAO.listMifapayRecharge( condition.periodType, condition.startTime, condition.endTime, - condition.countryKeyword, condition.sysOrigin)); + condition.countryKeyword, condition.sysOrigin, + condition.storageTimezoneOffset, condition.statTimezoneOffset)); mergeSql(rows, countryDashboardDAO.listGoogleRecharge( condition.periodType, condition.startTime, condition.endTime, - condition.countryKeyword, condition.sysOrigin)); + condition.countryKeyword, condition.sysOrigin, + condition.storageTimezoneOffset, condition.statTimezoneOffset)); mergeSql(rows, countryDashboardDAO.listDealerRecharge( condition.periodType, condition.startTime, condition.endTime, - condition.countryKeyword, condition.sysOrigin)); + condition.countryKeyword, condition.sysOrigin, + condition.storageTimezoneOffset, condition.statTimezoneOffset)); mergeSql(rows, countryDashboardDAO.listSalaryExchange( condition.periodType, condition.startTime, condition.endTime, - condition.countryKeyword, condition.sysOrigin)); + condition.countryKeyword, condition.sysOrigin, + condition.storageTimezoneOffset, condition.statTimezoneOffset)); mergeSql(rows, countryDashboardDAO.listLuckyGift( condition.periodType, condition.startTime, condition.endTime, - condition.countryKeyword, condition.sysOrigin)); + condition.countryKeyword, condition.sysOrigin, + condition.storageTimezoneOffset, condition.statTimezoneOffset)); mergeSql(rows, countryDashboardDAO.listWalletGame( condition.periodType, condition.startTime, condition.endTime, condition.startTimeMillis(), condition.endTimeMillis(), - condition.countryKeyword, condition.sysOrigin)); + condition.countryKeyword, condition.sysOrigin, + condition.storageTimezoneOffset, condition.statTimezoneOffset)); mergeSql(rows, countryDashboardDAO.listLuckyBoxGame( condition.periodType, condition.startTime, condition.endTime, - condition.countryKeyword, condition.sysOrigin)); + condition.countryKeyword, condition.sysOrigin, + condition.storageTimezoneOffset, condition.statTimezoneOffset)); mergeSql(rows, countryDashboardDAO.listGameUser( condition.periodType, condition.startTime, condition.endTime, condition.startTimeMillis(), condition.endTimeMillis(), - condition.countryKeyword, condition.sysOrigin)); + condition.countryKeyword, condition.sysOrigin, + condition.storageTimezoneOffset, condition.statTimezoneOffset)); } private boolean shouldUsePrecomputedMetrics(QueryCondition condition) { @@ -146,10 +165,11 @@ public class CountryDashboardServiceImpl implements CountryDashboardService { if (condition.startTime != null || condition.endTime != null) { return false; } - long count = safeLong(countryDashboardDAO.countAllPrecomputedPeriodMetrics(condition.sysOrigin)); + long count = safeLong(countryDashboardDAO.countAllPrecomputedPeriodMetrics( + condition.statTimezone, condition.sysOrigin)); if (count <= 0) { - log.warn("country dashboard all precomputed metric missing, skip live all-history scan, sysOrigin={}", - condition.sysOrigin); + log.warn("country dashboard all precomputed metric missing, skip live all-history scan, sysOrigin={}, statTimezone={}", + condition.sysOrigin, condition.statTimezone); } return true; } @@ -159,26 +179,27 @@ public class CountryDashboardServiceImpl implements CountryDashboardService { return false; } long actualPeriods = safeLong(countryDashboardDAO.countPrecomputedPeriods( - condition.periodType, periodKeys, condition.sysOrigin)); + condition.periodType, periodKeys, condition.statTimezone, condition.sysOrigin)); boolean complete = actualPeriods >= periodKeys.size(); if (!complete) { - log.info("country dashboard use live sql, period precomputed metric incomplete, periodType={}, sysOrigin={}, start={}, end={}, expectedPeriods={}, actualPeriods={}", - condition.periodType, condition.sysOrigin, condition.startDate, condition.endDate, + log.info("country dashboard use live sql, period precomputed metric incomplete, periodType={}, sysOrigin={}, statTimezone={}, start={}, end={}, expectedPeriods={}, actualPeriods={}", + condition.periodType, condition.sysOrigin, condition.statTimezone, condition.startDate, condition.endDate, periodKeys.size(), actualPeriods); } return complete; } private List expectedPrecomputedPeriodKeys(QueryCondition condition) { - if (condition.startTime == null || condition.endTime == null - || !condition.startTime.isBefore(condition.endTime)) { + if (condition.startDateValue == null || condition.endDateValue == null + || condition.endDateValue.isBefore(condition.startDateValue)) { return List.of(); } - LocalDate start = condition.startTime.toLocalDate(); - LocalDate endExclusive = condition.endTime.toLocalDate(); + LocalDate start = condition.startDateValue; + LocalDate endExclusive = condition.endDateValue.plusDays(1); return switch (condition.periodType) { case "DAY" -> expectedDayKeys(start, endExclusive); - case "WEEK", "MONTH" -> expectedAlignedPeriodKeys(condition.periodType, start, endExclusive); + case "WEEK", "MONTH" -> expectedAlignedPeriodKeys(condition.periodType, start, + endExclusive, condition.statZoneId); default -> List.of(); }; } @@ -192,10 +213,10 @@ public class CountryDashboardServiceImpl implements CountryDashboardService { } private List expectedAlignedPeriodKeys(String periodType, LocalDate start, - LocalDate endExclusive) { + LocalDate endExclusive, ZoneId statZoneId) { List keys = new ArrayList<>(); LocalDate cursor = start; - LocalDate tomorrow = LocalDate.now().plusDays(1); + LocalDate tomorrow = LocalDate.now(statZoneId).plusDays(1); while (cursor.isBefore(endExclusive)) { LocalDate periodStart = periodStart(periodType, cursor); if (!cursor.equals(periodStart)) { @@ -251,10 +272,11 @@ public class CountryDashboardServiceImpl implements CountryDashboardService { Long total = countryDashboardDAO.countRechargeDetails( condition.startTime, condition.endTime, condition.countryKeyword, countryCodes, - condition.sysOrigin); + condition.sysOrigin, condition.storageTimezoneOffset, condition.statTimezoneOffset); List records = countryDashboardDAO.listRechargeDetails( condition.startTime, condition.endTime, condition.countryKeyword, countryCodes, - condition.sysOrigin, offset, limit) + condition.sysOrigin, condition.storageTimezoneOffset, condition.statTimezoneOffset, + offset, limit) .stream() .map(this::toRechargeDetailCO) .toList(); @@ -358,12 +380,13 @@ public class CountryDashboardServiceImpl implements CountryDashboardService { private void mergeRetention(Map rows, QueryCondition condition) { List cohorts = countryDashboardDAO.listNewUserCohorts( condition.periodType, condition.startTime, condition.endTime, - condition.countryKeyword, condition.sysOrigin); + condition.countryKeyword, condition.sysOrigin, + condition.storageTimezoneOffset, condition.statTimezoneOffset); if (cohorts == null || cohorts.isEmpty()) { return; } - LocalDate today = LocalDate.now(); + LocalDate today = LocalDate.now(condition.statZoneId); Set userIds = new HashSet<>(); Set activeDates = new HashSet<>(); for (CountryDashboardRetentionUserDTO cohort : cohorts) { @@ -398,13 +421,13 @@ public class CountryDashboardServiceImpl implements CountryDashboardService { Criteria.where("dynamicContentId").is(null), Criteria.where("dynamicContentId").is("") )); - return aggregateGiftAmount(criteria, "$giftValue.actualAmount", false); + return aggregateGiftAmount(criteria, "$giftValue.actualAmount", false, condition.statTimezone); } private List listLuckyGiftAnchorShare(QueryCondition condition) { List criteria = baseGiftCriteria(condition); criteria.add(Criteria.where("luckyGift").is(Boolean.TRUE)); - return aggregateGiftAmount(criteria, "$acceptUsers.targetAmount", true); + return aggregateGiftAmount(criteria, "$acceptUsers.targetAmount", true, condition.statTimezone); } private List listDailyActive(QueryCondition condition) { @@ -428,11 +451,11 @@ public class CountryDashboardServiceImpl implements CountryDashboardService { List criteria = new ArrayList<>(); criteria.add(Criteria.where("refunded").ne(Boolean.TRUE)); criteria.add(Criteria.where("userId").ne(null)); - if (condition.startTime != null) { - criteria.add(Criteria.where("createTime").gte(Timestamp.valueOf(condition.startTime))); + if (condition.startInstant != null) { + criteria.add(Criteria.where("createTime").gte(Timestamp.from(condition.startInstant))); } - if (condition.endTime != null) { - criteria.add(Criteria.where("createTime").lt(Timestamp.valueOf(condition.endTime))); + if (condition.endInstant != null) { + criteria.add(Criteria.where("createTime").lt(Timestamp.from(condition.endInstant))); } if (hasText(condition.sysOrigin)) { criteria.add(Criteria.where("sysOrigin").is(condition.sysOrigin)); @@ -462,13 +485,13 @@ public class CountryDashboardServiceImpl implements CountryDashboardService { } private List aggregateGiftAmount(List criteria, String amountPath, - boolean unwindAcceptUsers) { + boolean unwindAcceptUsers, String statTimezone) { List operations = new ArrayList<>(); operations.add(Aggregation.match(new Criteria().andOperator(criteria.toArray(new Criteria[0])))); if (unwindAcceptUsers) { operations.add(Aggregation.unwind("acceptUsers")); } - operations.add(projectGiftDayAmount(amountPath)); + operations.add(projectGiftDayAmount(amountPath, statTimezone)); operations.add(Aggregation.group("userId", "day").sum("amount").as("amount")); return mongoTemplate.aggregate(Aggregation.newAggregation(operations), @@ -480,12 +503,14 @@ public class CountryDashboardServiceImpl implements CountryDashboardService { .toList(); } - private AggregationOperation projectGiftDayAmount(String amountPath) { + private AggregationOperation projectGiftDayAmount(String amountPath, String statTimezone) { return context -> new Document("$project", new Document("userId", "$userId") .append("amount", amountPath) .append("day", new Document("$dateToString", - new Document("format", "%Y-%m-%d").append("date", "$createTime")))); + new Document("format", "%Y-%m-%d") + .append("date", "$createTime") + .append("timezone", statTimezone)))); } private MongoMetric toMongoMetric(Document document) { @@ -752,30 +777,49 @@ public class CountryDashboardServiceImpl implements CountryDashboardService { private final String periodType; private final String startDate; private final String endDate; + private final String statTimezone; + private final ZoneId statZoneId; + private final String storageTimezoneOffset; + private final String statTimezoneOffset; + private final LocalDate startDateValue; + private final LocalDate endDateValue; private final LocalDateTime startTime; private final LocalDateTime endTime; + private final Instant startInstant; + private final Instant endInstant; private final String countryKeyword; private final String sysOrigin; private QueryCondition(String periodType, String startDate, String endDate, - LocalDateTime startTime, LocalDateTime endTime, String countryKeyword, - String sysOrigin) { + String statTimezone, ZoneId statZoneId, String storageTimezoneOffset, + String statTimezoneOffset, LocalDate startDateValue, LocalDate endDateValue, + LocalDateTime startTime, LocalDateTime endTime, Instant startInstant, + Instant endInstant, String countryKeyword, String sysOrigin) { this.periodType = periodType; this.startDate = startDate; this.endDate = endDate; + this.statTimezone = statTimezone; + this.statZoneId = statZoneId; + this.storageTimezoneOffset = storageTimezoneOffset; + this.statTimezoneOffset = statTimezoneOffset; + this.startDateValue = startDateValue; + this.endDateValue = endDateValue; this.startTime = startTime; this.endTime = endTime; + this.startInstant = startInstant; + this.endInstant = endInstant; this.countryKeyword = countryKeyword; this.sysOrigin = sysOrigin; } private static QueryCondition of(CountryDashboardQryCmd cmd) { String periodType = normalizePeriodType(cmd == null ? null : cmd.getPeriodType()); + ZoneId statZoneId = normalizeStatZoneId(cmd == null ? null : cmd.getStatTimezone()); LocalDate start = parseDate(cmd == null ? null : cmd.getStartDate()); LocalDate end = parseDate(cmd == null ? null : cmd.getEndDate()); if (!PERIOD_ALL.equals(periodType) && start == null && end == null) { - LocalDate today = LocalDate.now(); + LocalDate today = LocalDate.now(statZoneId); switch (periodType) { case "WEEK" -> { start = today.with(DayOfWeek.MONDAY); @@ -802,12 +846,27 @@ public class CountryDashboardServiceImpl implements CountryDashboardService { throw new IllegalArgumentException("endDate must not be before startDate."); } + Instant startInstant = start == null ? null : start.atStartOfDay(statZoneId).toInstant(); + Instant endInstant = end == null ? null : end.plusDays(1).atStartOfDay(statZoneId).toInstant(); + LocalDateTime storageStartTime = startInstant == null ? null + : LocalDateTime.ofInstant(startInstant, STORAGE_ZONE_ID); + LocalDateTime storageEndTime = endInstant == null ? null + : LocalDateTime.ofInstant(endInstant, STORAGE_ZONE_ID); + return new QueryCondition( periodType, start == null ? null : DATE_FORMATTER.format(start), end == null ? null : DATE_FORMATTER.format(end), - start == null ? null : start.atStartOfDay(), - end == null ? null : end.plusDays(1).atStartOfDay(), + statZoneId.getId(), + statZoneId, + zoneOffset(STORAGE_ZONE_ID), + zoneOffset(statZoneId), + start, + end, + storageStartTime, + storageEndTime, + startInstant, + endInstant, trimToNull(cmd == null ? null : cmd.getCountryKeyword()), trimToNull(cmd == null ? null : cmd.getSysOrigin()) ); @@ -832,11 +891,15 @@ public class CountryDashboardServiceImpl implements CountryDashboardService { } private Long startTimeMillis() { - return startTime == null ? null : Timestamp.valueOf(startTime).getTime(); + return startInstant == null ? null : startInstant.toEpochMilli(); } private Long endTimeMillis() { - return endTime == null ? null : Timestamp.valueOf(endTime).getTime(); + return endInstant == null ? null : endInstant.toEpochMilli(); + } + + private boolean isStorageTimezone() { + return STORAGE_ZONE_ID.equals(statZoneId); } private static String normalizePeriodType(String periodType) { @@ -851,6 +914,27 @@ public class CountryDashboardServiceImpl implements CountryDashboardService { }; } + private static ZoneId normalizeStatZoneId(String statTimezone) { + String value = trimToNull(statTimezone); + if (value == null) { + return DEFAULT_STAT_ZONE_ID; + } + value = value.trim(); + String upperValue = value.toUpperCase(Locale.ROOT); + return switch (upperValue) { + case "UTC", "Z", "+00:00" -> ZoneId.of("UTC"); + case "ASIA/SHANGHAI", "BEIJING", "CHINA", "+08:00" -> ZoneId.of("Asia/Shanghai"); + case "ASIA/RIYADH", "RIYADH", "SERVER", "+03:00" -> STORAGE_ZONE_ID; + default -> DEFAULT_STAT_ZONE_ID; + }; + } + + private static String zoneOffset(ZoneId zoneId) { + ZoneOffset offset = zoneId.getRules().getOffset(Instant.now()); + String id = offset.getId(); + return "Z".equals(id) ? "+00:00" : id; + } + private static LocalDate parseDate(String value) { String fixed = trimToNull(value); return fixed == null ? null : LocalDate.parse(fixed, DATE_FORMATTER); diff --git a/rc-service/rc-service-console/console-client/src/main/java/com/red/circle/console/app/dto/clientobject/datav/CountryDashboardCO.java b/rc-service/rc-service-console/console-client/src/main/java/com/red/circle/console/app/dto/clientobject/datav/CountryDashboardCO.java index 73a16c81..067d5066 100644 --- a/rc-service/rc-service-console/console-client/src/main/java/com/red/circle/console/app/dto/clientobject/datav/CountryDashboardCO.java +++ b/rc-service/rc-service-console/console-client/src/main/java/com/red/circle/console/app/dto/clientobject/datav/CountryDashboardCO.java @@ -22,6 +22,8 @@ public class CountryDashboardCO implements Serializable { private String endDate; + private String statTimezone; + private String computedAt; private CountryDashboardMetricCO total; diff --git a/rc-service/rc-service-console/console-client/src/main/java/com/red/circle/console/app/dto/cmd/datav/CountryDashboardQryCmd.java b/rc-service/rc-service-console/console-client/src/main/java/com/red/circle/console/app/dto/cmd/datav/CountryDashboardQryCmd.java index 03a25e22..50175df3 100644 --- a/rc-service/rc-service-console/console-client/src/main/java/com/red/circle/console/app/dto/cmd/datav/CountryDashboardQryCmd.java +++ b/rc-service/rc-service-console/console-client/src/main/java/com/red/circle/console/app/dto/cmd/datav/CountryDashboardQryCmd.java @@ -30,6 +30,11 @@ public class CountryDashboardQryCmd implements Serializable { */ private String endDate; + /** + * 统计时区,默认 Asia/Riyadh. + */ + private String statTimezone; + /** * 国家编码或国家名称关键字. */ diff --git a/rc-service/rc-service-console/console-infrastructure/src/main/java/com/red/circle/console/infra/database/rds/dao/datav/CountryDashboardDAO.java b/rc-service/rc-service-console/console-infrastructure/src/main/java/com/red/circle/console/infra/database/rds/dao/datav/CountryDashboardDAO.java index aad0b8c3..ff2903b9 100644 --- a/rc-service/rc-service-console/console-infrastructure/src/main/java/com/red/circle/console/infra/database/rds/dao/datav/CountryDashboardDAO.java +++ b/rc-service/rc-service-console/console-infrastructure/src/main/java/com/red/circle/console/infra/database/rds/dao/datav/CountryDashboardDAO.java @@ -25,61 +25,79 @@ public interface CountryDashboardDAO extends BaseDAO { int createPeriodUserMetricTable(); + int ensurePeriodMetricTimezoneSchema(); + List listNewUsers( @Param("periodType") String periodType, @Param("startTime") LocalDateTime startTime, @Param("endTime") LocalDateTime endTime, @Param("countryKeyword") String countryKeyword, - @Param("sysOrigin") String sysOrigin); + @Param("sysOrigin") String sysOrigin, + @Param("storageTimezoneOffset") String storageTimezoneOffset, + @Param("statTimezoneOffset") String statTimezoneOffset); List listNewUserRecharge( @Param("periodType") String periodType, @Param("startTime") LocalDateTime startTime, @Param("endTime") LocalDateTime endTime, @Param("countryKeyword") String countryKeyword, - @Param("sysOrigin") String sysOrigin); + @Param("sysOrigin") String sysOrigin, + @Param("storageTimezoneOffset") String storageTimezoneOffset, + @Param("statTimezoneOffset") String statTimezoneOffset); List listOfficialRecharge( @Param("periodType") String periodType, @Param("startTime") LocalDateTime startTime, @Param("endTime") LocalDateTime endTime, @Param("countryKeyword") String countryKeyword, - @Param("sysOrigin") String sysOrigin); + @Param("sysOrigin") String sysOrigin, + @Param("storageTimezoneOffset") String storageTimezoneOffset, + @Param("statTimezoneOffset") String statTimezoneOffset); List listMifapayRecharge( @Param("periodType") String periodType, @Param("startTime") LocalDateTime startTime, @Param("endTime") LocalDateTime endTime, @Param("countryKeyword") String countryKeyword, - @Param("sysOrigin") String sysOrigin); + @Param("sysOrigin") String sysOrigin, + @Param("storageTimezoneOffset") String storageTimezoneOffset, + @Param("statTimezoneOffset") String statTimezoneOffset); List listGoogleRecharge( @Param("periodType") String periodType, @Param("startTime") LocalDateTime startTime, @Param("endTime") LocalDateTime endTime, @Param("countryKeyword") String countryKeyword, - @Param("sysOrigin") String sysOrigin); + @Param("sysOrigin") String sysOrigin, + @Param("storageTimezoneOffset") String storageTimezoneOffset, + @Param("statTimezoneOffset") String statTimezoneOffset); List listDealerRecharge( @Param("periodType") String periodType, @Param("startTime") LocalDateTime startTime, @Param("endTime") LocalDateTime endTime, @Param("countryKeyword") String countryKeyword, - @Param("sysOrigin") String sysOrigin); + @Param("sysOrigin") String sysOrigin, + @Param("storageTimezoneOffset") String storageTimezoneOffset, + @Param("statTimezoneOffset") String statTimezoneOffset); List listSalaryExchange( @Param("periodType") String periodType, @Param("startTime") LocalDateTime startTime, @Param("endTime") LocalDateTime endTime, @Param("countryKeyword") String countryKeyword, - @Param("sysOrigin") String sysOrigin); + @Param("sysOrigin") String sysOrigin, + @Param("storageTimezoneOffset") String storageTimezoneOffset, + @Param("statTimezoneOffset") String statTimezoneOffset); List listLuckyGift( @Param("periodType") String periodType, @Param("startTime") LocalDateTime startTime, @Param("endTime") LocalDateTime endTime, @Param("countryKeyword") String countryKeyword, - @Param("sysOrigin") String sysOrigin); + @Param("sysOrigin") String sysOrigin, + @Param("storageTimezoneOffset") String storageTimezoneOffset, + @Param("statTimezoneOffset") String statTimezoneOffset); List listWalletGame( @Param("periodType") String periodType, @@ -88,14 +106,18 @@ public interface CountryDashboardDAO extends BaseDAO { @Param("startTimeMillis") Long startTimeMillis, @Param("endTimeMillis") Long endTimeMillis, @Param("countryKeyword") String countryKeyword, - @Param("sysOrigin") String sysOrigin); + @Param("sysOrigin") String sysOrigin, + @Param("storageTimezoneOffset") String storageTimezoneOffset, + @Param("statTimezoneOffset") String statTimezoneOffset); List listLuckyBoxGame( @Param("periodType") String periodType, @Param("startTime") LocalDateTime startTime, @Param("endTime") LocalDateTime endTime, @Param("countryKeyword") String countryKeyword, - @Param("sysOrigin") String sysOrigin); + @Param("sysOrigin") String sysOrigin, + @Param("storageTimezoneOffset") String storageTimezoneOffset, + @Param("statTimezoneOffset") String statTimezoneOffset); List listGameUser( @Param("periodType") String periodType, @@ -104,7 +126,9 @@ public interface CountryDashboardDAO extends BaseDAO { @Param("startTimeMillis") Long startTimeMillis, @Param("endTimeMillis") Long endTimeMillis, @Param("countryKeyword") String countryKeyword, - @Param("sysOrigin") String sysOrigin); + @Param("sysOrigin") String sysOrigin, + @Param("storageTimezoneOffset") String storageTimezoneOffset, + @Param("statTimezoneOffset") String statTimezoneOffset); List listPrecomputedMetrics( @Param("periodType") String periodType, @@ -118,6 +142,7 @@ public interface CountryDashboardDAO extends BaseDAO { @Param("startTime") LocalDateTime startTime, @Param("endTime") LocalDateTime endTime, @Param("countryKeyword") String countryKeyword, + @Param("statTimezone") String statTimezone, @Param("sysOrigin") String sysOrigin); Long countPrecomputedMetricDates( @@ -128,9 +153,12 @@ public interface CountryDashboardDAO extends BaseDAO { Long countPrecomputedPeriods( @Param("periodType") String periodType, @Param("periodKeys") Collection periodKeys, + @Param("statTimezone") String statTimezone, @Param("sysOrigin") String sysOrigin); - Long countAllPrecomputedPeriodMetrics(@Param("sysOrigin") String sysOrigin); + Long countAllPrecomputedPeriodMetrics( + @Param("statTimezone") String statTimezone, + @Param("sysOrigin") String sysOrigin); int deleteDailyMetrics( @Param("statDate") LocalDate statDate, @@ -141,6 +169,7 @@ public interface CountryDashboardDAO extends BaseDAO { int deletePeriodMetrics( @Param("periodType") String periodType, @Param("periodKey") String periodKey, + @Param("statTimezone") String statTimezone, @Param("sysOrigin") String sysOrigin); int upsertPeriodMetrics(@Param("metrics") List metrics); @@ -148,6 +177,7 @@ public interface CountryDashboardDAO extends BaseDAO { int deletePeriodUserMetrics( @Param("periodType") String periodType, @Param("periodKey") String periodKey, + @Param("statTimezone") String statTimezone, @Param("sysOrigin") String sysOrigin); int insertPeriodNewUserMetrics( @@ -156,6 +186,7 @@ public interface CountryDashboardDAO extends BaseDAO { @Param("periodName") String periodName, @Param("periodStartDate") LocalDate periodStartDate, @Param("periodEndDate") LocalDate periodEndDate, + @Param("statTimezone") String statTimezone, @Param("startTime") LocalDateTime startTime, @Param("endTime") LocalDateTime endTime, @Param("sysOrigin") String sysOrigin); @@ -166,6 +197,7 @@ public interface CountryDashboardDAO extends BaseDAO { @Param("periodName") String periodName, @Param("periodStartDate") LocalDate periodStartDate, @Param("periodEndDate") LocalDate periodEndDate, + @Param("statTimezone") String statTimezone, @Param("startTime") LocalDateTime startTime, @Param("endTime") LocalDateTime endTime, @Param("sysOrigin") String sysOrigin); @@ -176,27 +208,35 @@ public interface CountryDashboardDAO extends BaseDAO { @Param("periodName") String periodName, @Param("periodStartDate") LocalDate periodStartDate, @Param("periodEndDate") LocalDate periodEndDate, + @Param("statTimezone") String statTimezone, @Param("startTime") LocalDateTime startTime, @Param("endTime") LocalDateTime endTime, @Param("startTimeMillis") Long startTimeMillis, @Param("endTimeMillis") Long endTimeMillis, @Param("sysOrigin") String sysOrigin); - int insertAllPeriodUserMetricsFromDaily(@Param("sysOrigin") String sysOrigin); + int insertAllPeriodUserMetricsFromDaily( + @Param("statTimezone") String statTimezone, + @Param("sysOrigin") String sysOrigin); List listPeriodUserMetricCounts( @Param("periodType") String periodType, @Param("periodKey") String periodKey, + @Param("statTimezone") String statTimezone, @Param("sysOrigin") String sysOrigin); - List listAllPeriodAmountsFromDaily(@Param("sysOrigin") String sysOrigin); + List listAllPeriodAmountsFromPeriodDays( + @Param("statTimezone") String statTimezone, + @Param("sysOrigin") String sysOrigin); List listNewUserCohorts( @Param("periodType") String periodType, @Param("startTime") LocalDateTime startTime, @Param("endTime") LocalDateTime endTime, @Param("countryKeyword") String countryKeyword, - @Param("sysOrigin") String sysOrigin); + @Param("sysOrigin") String sysOrigin, + @Param("storageTimezoneOffset") String storageTimezoneOffset, + @Param("statTimezoneOffset") String statTimezoneOffset); List listUserCountryByIds(@Param("userIds") Collection userIds); @@ -205,7 +245,9 @@ public interface CountryDashboardDAO extends BaseDAO { @Param("endTime") LocalDateTime endTime, @Param("countryKeyword") String countryKeyword, @Param("countryCodes") String countryCodes, - @Param("sysOrigin") String sysOrigin); + @Param("sysOrigin") String sysOrigin, + @Param("storageTimezoneOffset") String storageTimezoneOffset, + @Param("statTimezoneOffset") String statTimezoneOffset); List listRechargeDetails( @Param("startTime") LocalDateTime startTime, @@ -213,6 +255,8 @@ public interface CountryDashboardDAO extends BaseDAO { @Param("countryKeyword") String countryKeyword, @Param("countryCodes") String countryCodes, @Param("sysOrigin") String sysOrigin, + @Param("storageTimezoneOffset") String storageTimezoneOffset, + @Param("statTimezoneOffset") String statTimezoneOffset, @Param("offset") int offset, @Param("limit") int limit); } diff --git a/rc-service/rc-service-console/console-infrastructure/src/main/java/com/red/circle/console/infra/database/rds/dto/datav/CountryDashboardPeriodMetricDTO.java b/rc-service/rc-service-console/console-infrastructure/src/main/java/com/red/circle/console/infra/database/rds/dto/datav/CountryDashboardPeriodMetricDTO.java index f9be711f..823ab80f 100644 --- a/rc-service/rc-service-console/console-infrastructure/src/main/java/com/red/circle/console/infra/database/rds/dto/datav/CountryDashboardPeriodMetricDTO.java +++ b/rc-service/rc-service-console/console-infrastructure/src/main/java/com/red/circle/console/infra/database/rds/dto/datav/CountryDashboardPeriodMetricDTO.java @@ -16,6 +16,8 @@ public class CountryDashboardPeriodMetricDTO { private String periodKey; + private String statTimezone; + private String periodName; private LocalDate periodStartDate; diff --git a/rc-service/rc-service-console/console-infrastructure/src/main/resources/dao/datav/CountryDashboardDAO.xml b/rc-service/rc-service-console/console-infrastructure/src/main/resources/dao/datav/CountryDashboardDAO.xml index a8438e14..2dc02e32 100644 --- a/rc-service/rc-service-console/console-infrastructure/src/main/resources/dao/datav/CountryDashboardDAO.xml +++ b/rc-service/rc-service-console/console-infrastructure/src/main/resources/dao/datav/CountryDashboardDAO.xml @@ -39,6 +39,7 @@ `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键', `period_type` varchar(16) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '周期类型:DAY/WEEK/MONTH/ALL', `period_key` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '周期键', + `stat_timezone` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'Asia/Riyadh' COMMENT '统计时区', `period_name` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '周期名称', `period_start_date` date DEFAULT NULL COMMENT '周期开始日期', `period_end_date` date DEFAULT NULL COMMENT '周期结束日期,开区间', @@ -62,9 +63,9 @@ `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', PRIMARY KEY (`id`), - UNIQUE KEY `uk_period_origin_country` (`period_type`, `period_key`, `sys_origin`, `country_code`), - KEY `idx_origin_period` (`sys_origin`, `period_type`, `period_key`), - KEY `idx_period_date` (`period_type`, `period_start_date`, `period_end_date`) + UNIQUE KEY `uk_period_origin_country` (`period_type`, `period_key`, `stat_timezone`, `sys_origin`, `country_code`), + KEY `idx_origin_period` (`sys_origin`, `stat_timezone`, `period_type`, `period_key`), + KEY `idx_period_date` (`stat_timezone`, `period_type`, `period_start_date`, `period_end_date`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='国家数据大屏周期预聚合' @@ -73,6 +74,7 @@ `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键', `period_type` varchar(16) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '周期类型:DAY/WEEK/MONTH/ALL', `period_key` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '周期键', + `stat_timezone` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'Asia/Riyadh' COMMENT '统计时区', `period_name` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '周期名称', `period_start_date` date DEFAULT NULL COMMENT '周期开始日期', `period_end_date` date DEFAULT NULL COMMENT '周期结束日期,开区间', @@ -83,29 +85,290 @@ `metric_type` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '人数指标类型', `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', PRIMARY KEY (`id`), - UNIQUE KEY `uk_period_user_metric` (`period_type`, `period_key`, `sys_origin`, `country_code`, `user_id`, `metric_type`), - KEY `idx_period_metric_country` (`period_type`, `period_key`, `sys_origin`, `metric_type`, `country_code`), - KEY `idx_origin_user_metric` (`sys_origin`, `user_id`, `metric_type`) + UNIQUE KEY `uk_period_user_metric` (`period_type`, `period_key`, `stat_timezone`, `sys_origin`, `country_code`, `user_id`, `metric_type`), + KEY `idx_period_metric_country` (`period_type`, `period_key`, `stat_timezone`, `sys_origin`, `metric_type`, `country_code`), + KEY `idx_origin_user_metric` (`sys_origin`, `stat_timezone`, `user_id`, `metric_type`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='国家数据大屏周期用户去重明细' + + SET @country_dashboard_schema_lock = GET_LOCK('country_dashboard_period_timezone_schema', 60); + SET @current_schema = DATABASE(); + + SET @ddl = IF( + @country_dashboard_schema_lock = 1 + AND NOT EXISTS ( + SELECT 1 + FROM INFORMATION_SCHEMA.COLUMNS + WHERE TABLE_SCHEMA = @current_schema + AND TABLE_NAME = 'country_dashboard_period_metric' + AND COLUMN_NAME = 'stat_timezone' + ), + 'ALTER TABLE `country_dashboard_period_metric` ADD COLUMN `stat_timezone` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT ''Asia/Riyadh'' COMMENT ''统计时区'' AFTER `period_key`', + 'SELECT 1' + ); + PREPARE stmt FROM @ddl; + EXECUTE stmt; + DEALLOCATE PREPARE stmt; + + SET @ddl = IF( + @country_dashboard_schema_lock = 1 + AND NOT EXISTS ( + SELECT 1 + FROM INFORMATION_SCHEMA.COLUMNS + WHERE TABLE_SCHEMA = @current_schema + AND TABLE_NAME = 'country_dashboard_period_user_metric' + AND COLUMN_NAME = 'stat_timezone' + ), + 'ALTER TABLE `country_dashboard_period_user_metric` ADD COLUMN `stat_timezone` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT ''Asia/Riyadh'' COMMENT ''统计时区'' AFTER `period_key`', + 'SELECT 1' + ); + PREPARE stmt FROM @ddl; + EXECUTE stmt; + DEALLOCATE PREPARE stmt; + + SET @index_columns = ( + SELECT GROUP_CONCAT(COLUMN_NAME ORDER BY SEQ_IN_INDEX) + FROM INFORMATION_SCHEMA.STATISTICS + WHERE TABLE_SCHEMA = @current_schema + AND TABLE_NAME = 'country_dashboard_period_metric' + AND INDEX_NAME = 'uk_period_origin_country' + ); + SET @ddl = IF( + @country_dashboard_schema_lock = 1 + AND @index_columns IS NOT NULL + AND @index_columns <> 'period_type,period_key,stat_timezone,sys_origin,country_code', + 'ALTER TABLE `country_dashboard_period_metric` DROP INDEX `uk_period_origin_country`', + 'SELECT 1' + ); + PREPARE stmt FROM @ddl; + EXECUTE stmt; + DEALLOCATE PREPARE stmt; + SET @index_columns = ( + SELECT GROUP_CONCAT(COLUMN_NAME ORDER BY SEQ_IN_INDEX) + FROM INFORMATION_SCHEMA.STATISTICS + WHERE TABLE_SCHEMA = @current_schema + AND TABLE_NAME = 'country_dashboard_period_metric' + AND INDEX_NAME = 'uk_period_origin_country' + ); + SET @ddl = IF( + @country_dashboard_schema_lock = 1 + AND @index_columns IS NULL, + 'ALTER TABLE `country_dashboard_period_metric` ADD UNIQUE KEY `uk_period_origin_country` (`period_type`, `period_key`, `stat_timezone`, `sys_origin`, `country_code`)', + 'SELECT 1' + ); + PREPARE stmt FROM @ddl; + EXECUTE stmt; + DEALLOCATE PREPARE stmt; + + SET @index_columns = ( + SELECT GROUP_CONCAT(COLUMN_NAME ORDER BY SEQ_IN_INDEX) + FROM INFORMATION_SCHEMA.STATISTICS + WHERE TABLE_SCHEMA = @current_schema + AND TABLE_NAME = 'country_dashboard_period_metric' + AND INDEX_NAME = 'idx_origin_period' + ); + SET @ddl = IF( + @country_dashboard_schema_lock = 1 + AND @index_columns IS NOT NULL + AND @index_columns <> 'sys_origin,stat_timezone,period_type,period_key', + 'ALTER TABLE `country_dashboard_period_metric` DROP INDEX `idx_origin_period`', + 'SELECT 1' + ); + PREPARE stmt FROM @ddl; + EXECUTE stmt; + DEALLOCATE PREPARE stmt; + SET @index_columns = ( + SELECT GROUP_CONCAT(COLUMN_NAME ORDER BY SEQ_IN_INDEX) + FROM INFORMATION_SCHEMA.STATISTICS + WHERE TABLE_SCHEMA = @current_schema + AND TABLE_NAME = 'country_dashboard_period_metric' + AND INDEX_NAME = 'idx_origin_period' + ); + SET @ddl = IF( + @country_dashboard_schema_lock = 1 + AND @index_columns IS NULL, + 'ALTER TABLE `country_dashboard_period_metric` ADD KEY `idx_origin_period` (`sys_origin`, `stat_timezone`, `period_type`, `period_key`)', + 'SELECT 1' + ); + PREPARE stmt FROM @ddl; + EXECUTE stmt; + DEALLOCATE PREPARE stmt; + + SET @index_columns = ( + SELECT GROUP_CONCAT(COLUMN_NAME ORDER BY SEQ_IN_INDEX) + FROM INFORMATION_SCHEMA.STATISTICS + WHERE TABLE_SCHEMA = @current_schema + AND TABLE_NAME = 'country_dashboard_period_metric' + AND INDEX_NAME = 'idx_period_date' + ); + SET @ddl = IF( + @country_dashboard_schema_lock = 1 + AND @index_columns IS NOT NULL + AND @index_columns <> 'stat_timezone,period_type,period_start_date,period_end_date', + 'ALTER TABLE `country_dashboard_period_metric` DROP INDEX `idx_period_date`', + 'SELECT 1' + ); + PREPARE stmt FROM @ddl; + EXECUTE stmt; + DEALLOCATE PREPARE stmt; + SET @index_columns = ( + SELECT GROUP_CONCAT(COLUMN_NAME ORDER BY SEQ_IN_INDEX) + FROM INFORMATION_SCHEMA.STATISTICS + WHERE TABLE_SCHEMA = @current_schema + AND TABLE_NAME = 'country_dashboard_period_metric' + AND INDEX_NAME = 'idx_period_date' + ); + SET @ddl = IF( + @country_dashboard_schema_lock = 1 + AND @index_columns IS NULL, + 'ALTER TABLE `country_dashboard_period_metric` ADD KEY `idx_period_date` (`stat_timezone`, `period_type`, `period_start_date`, `period_end_date`)', + 'SELECT 1' + ); + PREPARE stmt FROM @ddl; + EXECUTE stmt; + DEALLOCATE PREPARE stmt; + + SET @index_columns = ( + SELECT GROUP_CONCAT(COLUMN_NAME ORDER BY SEQ_IN_INDEX) + FROM INFORMATION_SCHEMA.STATISTICS + WHERE TABLE_SCHEMA = @current_schema + AND TABLE_NAME = 'country_dashboard_period_user_metric' + AND INDEX_NAME = 'uk_period_user_metric' + ); + SET @ddl = IF( + @country_dashboard_schema_lock = 1 + AND @index_columns IS NOT NULL + AND @index_columns <> 'period_type,period_key,stat_timezone,sys_origin,country_code,user_id,metric_type', + 'ALTER TABLE `country_dashboard_period_user_metric` DROP INDEX `uk_period_user_metric`', + 'SELECT 1' + ); + PREPARE stmt FROM @ddl; + EXECUTE stmt; + DEALLOCATE PREPARE stmt; + SET @index_columns = ( + SELECT GROUP_CONCAT(COLUMN_NAME ORDER BY SEQ_IN_INDEX) + FROM INFORMATION_SCHEMA.STATISTICS + WHERE TABLE_SCHEMA = @current_schema + AND TABLE_NAME = 'country_dashboard_period_user_metric' + AND INDEX_NAME = 'uk_period_user_metric' + ); + SET @ddl = IF( + @country_dashboard_schema_lock = 1 + AND @index_columns IS NULL, + 'ALTER TABLE `country_dashboard_period_user_metric` ADD UNIQUE KEY `uk_period_user_metric` (`period_type`, `period_key`, `stat_timezone`, `sys_origin`, `country_code`, `user_id`, `metric_type`)', + 'SELECT 1' + ); + PREPARE stmt FROM @ddl; + EXECUTE stmt; + DEALLOCATE PREPARE stmt; + + SET @index_columns = ( + SELECT GROUP_CONCAT(COLUMN_NAME ORDER BY SEQ_IN_INDEX) + FROM INFORMATION_SCHEMA.STATISTICS + WHERE TABLE_SCHEMA = @current_schema + AND TABLE_NAME = 'country_dashboard_period_user_metric' + AND INDEX_NAME = 'idx_period_metric_country' + ); + SET @ddl = IF( + @country_dashboard_schema_lock = 1 + AND @index_columns IS NOT NULL + AND @index_columns <> 'period_type,period_key,stat_timezone,sys_origin,metric_type,country_code', + 'ALTER TABLE `country_dashboard_period_user_metric` DROP INDEX `idx_period_metric_country`', + 'SELECT 1' + ); + PREPARE stmt FROM @ddl; + EXECUTE stmt; + DEALLOCATE PREPARE stmt; + SET @index_columns = ( + SELECT GROUP_CONCAT(COLUMN_NAME ORDER BY SEQ_IN_INDEX) + FROM INFORMATION_SCHEMA.STATISTICS + WHERE TABLE_SCHEMA = @current_schema + AND TABLE_NAME = 'country_dashboard_period_user_metric' + AND INDEX_NAME = 'idx_period_metric_country' + ); + SET @ddl = IF( + @country_dashboard_schema_lock = 1 + AND @index_columns IS NULL, + 'ALTER TABLE `country_dashboard_period_user_metric` ADD KEY `idx_period_metric_country` (`period_type`, `period_key`, `stat_timezone`, `sys_origin`, `metric_type`, `country_code`)', + 'SELECT 1' + ); + PREPARE stmt FROM @ddl; + EXECUTE stmt; + DEALLOCATE PREPARE stmt; + + SET @index_columns = ( + SELECT GROUP_CONCAT(COLUMN_NAME ORDER BY SEQ_IN_INDEX) + FROM INFORMATION_SCHEMA.STATISTICS + WHERE TABLE_SCHEMA = @current_schema + AND TABLE_NAME = 'country_dashboard_period_user_metric' + AND INDEX_NAME = 'idx_origin_user_metric' + ); + SET @ddl = IF( + @country_dashboard_schema_lock = 1 + AND @index_columns IS NOT NULL + AND @index_columns <> 'sys_origin,stat_timezone,user_id,metric_type', + 'ALTER TABLE `country_dashboard_period_user_metric` DROP INDEX `idx_origin_user_metric`', + 'SELECT 1' + ); + PREPARE stmt FROM @ddl; + EXECUTE stmt; + DEALLOCATE PREPARE stmt; + SET @index_columns = ( + SELECT GROUP_CONCAT(COLUMN_NAME ORDER BY SEQ_IN_INDEX) + FROM INFORMATION_SCHEMA.STATISTICS + WHERE TABLE_SCHEMA = @current_schema + AND TABLE_NAME = 'country_dashboard_period_user_metric' + AND INDEX_NAME = 'idx_origin_user_metric' + ); + SET @ddl = IF( + @country_dashboard_schema_lock = 1 + AND @index_columns IS NULL, + 'ALTER TABLE `country_dashboard_period_user_metric` ADD KEY `idx_origin_user_metric` (`sys_origin`, `stat_timezone`, `user_id`, `metric_type`)', + 'SELECT 1' + ); + PREPARE stmt FROM @ddl; + EXECUTE stmt; + DEALLOCATE PREPARE stmt; + + DO IF(@country_dashboard_schema_lock = 1, RELEASE_LOCK('country_dashboard_period_timezone_schema'), 0); + + NULLIF(u.country_code, '') AS countryCode, NULLIF(u.country_name, '') AS countryName + + + + CONVERT_TZ(t.create_time, #{storageTimezoneOffset}, #{statTimezoneOffset}) + + t.create_time + + + + + + + CONVERT_TZ(u.create_time, #{storageTimezoneOffset}, #{statTimezoneOffset}) + + u.create_time + + + - DATE_FORMAT(t.create_time, '%Y-%m-%d') - DATE_FORMAT(t.create_time, '%x-W%v') - DATE_FORMAT(t.create_time, '%Y-%m') + DATE_FORMAT(, '%Y-%m-%d') + DATE_FORMAT(, '%x-W%v') + DATE_FORMAT(, '%Y-%m') 'ALL' AS periodKey, - DATE_FORMAT(t.create_time, '%Y-%m-%d') - DATE_FORMAT(t.create_time, '%x-W%v') - DATE_FORMAT(t.create_time, '%Y-%m') + DATE_FORMAT(, '%Y-%m-%d') + DATE_FORMAT(, '%x-W%v') + DATE_FORMAT(, '%Y-%m') '全部' AS periodName @@ -113,16 +376,16 @@ - DATE_FORMAT(u.create_time, '%Y-%m-%d') - DATE_FORMAT(u.create_time, '%x-W%v') - DATE_FORMAT(u.create_time, '%Y-%m') + DATE_FORMAT(, '%Y-%m-%d') + DATE_FORMAT(, '%x-W%v') + DATE_FORMAT(, '%Y-%m') 'ALL' AS periodKey, - DATE_FORMAT(u.create_time, '%Y-%m-%d') - DATE_FORMAT(u.create_time, '%x-W%v') - DATE_FORMAT(u.create_time, '%Y-%m') + DATE_FORMAT(, '%Y-%m-%d') + DATE_FORMAT(, '%x-W%v') + DATE_FORMAT(, '%Y-%m') '全部' AS periodName @@ -418,6 +681,7 @@ IFNULL(SUM(t.game_payout), 0) AS gamePayout FROM country_dashboard_period_metric t WHERE t.period_type = #{periodType} + AND t.stat_timezone = #{statTimezone} AND t.period_start_date >= DATE(#{startTime}) @@ -440,6 +704,7 @@ SELECT COUNT(DISTINCT t.period_key) FROM country_dashboard_period_metric t WHERE t.period_type = #{periodType} + AND t.stat_timezone = #{statTimezone} AND t.period_key IN @@ -456,6 +721,7 @@ FROM country_dashboard_period_metric t WHERE t.period_type = 'ALL' AND t.period_key = 'ALL' + AND t.stat_timezone = #{statTimezone} AND t.sys_origin = #{sysOrigin} @@ -537,6 +803,7 @@ DELETE FROM country_dashboard_period_metric WHERE period_type = #{periodType} AND period_key = #{periodKey} + AND stat_timezone = #{statTimezone} AND sys_origin = #{sysOrigin} @@ -546,6 +813,7 @@ INSERT INTO country_dashboard_period_metric ( period_type, period_key, + stat_timezone, period_name, period_start_date, period_end_date, @@ -572,6 +840,7 @@ ( #{item.periodType}, #{item.periodKey}, + #{item.statTimezone}, #{item.periodName}, #{item.periodStartDate}, #{item.periodEndDate}, @@ -619,6 +888,7 @@ DELETE FROM country_dashboard_period_user_metric WHERE period_type = #{periodType} AND period_key = #{periodKey} + AND stat_timezone = #{statTimezone} AND sys_origin = #{sysOrigin} @@ -628,6 +898,7 @@ INSERT IGNORE INTO country_dashboard_period_user_metric ( period_type, period_key, + stat_timezone, period_name, period_start_date, period_end_date, @@ -640,6 +911,7 @@ SELECT DISTINCT #{periodType}, #{periodKey}, + #{statTimezone}, #{periodName}, #{periodStartDate}, #{periodEndDate}, @@ -661,6 +933,7 @@ INSERT IGNORE INTO country_dashboard_period_user_metric ( period_type, period_key, + stat_timezone, period_name, period_start_date, period_end_date, @@ -673,6 +946,7 @@ SELECT DISTINCT #{periodType}, #{periodKey}, + #{statTimezone}, #{periodName}, #{periodStartDate}, #{periodEndDate}, @@ -696,6 +970,7 @@ INSERT IGNORE INTO country_dashboard_period_user_metric ( period_type, period_key, + stat_timezone, period_name, period_start_date, period_end_date, @@ -708,6 +983,7 @@ SELECT DISTINCT #{periodType}, #{periodKey}, + #{statTimezone}, #{periodName}, #{periodStartDate}, #{periodEndDate}, @@ -741,6 +1017,7 @@ INSERT IGNORE INTO country_dashboard_period_user_metric ( period_type, period_key, + stat_timezone, period_name, period_start_date, period_end_date, @@ -753,6 +1030,7 @@ SELECT DISTINCT 'ALL', 'ALL', + #{statTimezone}, '全部', NULL, NULL, @@ -763,6 +1041,7 @@ t.metric_type FROM country_dashboard_period_user_metric t WHERE t.period_type = 'DAY' + AND t.stat_timezone = #{statTimezone} AND t.sys_origin = #{sysOrigin} @@ -781,13 +1060,14 @@ FROM country_dashboard_period_user_metric t WHERE t.period_type = #{periodType} AND t.period_key = #{periodKey} + AND t.stat_timezone = #{statTimezone} AND t.sys_origin = #{sysOrigin} GROUP BY countryCode, periodKey - @@ -945,7 +1226,10 @@ t.amount, t.coinQuantity, t.remark, - DATE_FORMAT(t.createTime, '%Y-%m-%d %H:%i:%s') AS createTime + DATE_FORMAT( + CONVERT_TZ(t.createTime, #{storageTimezoneOffset}, #{statTimezoneOffset}), + '%Y-%m-%d %H:%i:%s' + ) AS createTime FROM ( ) t @@ -1067,7 +1351,7 @@ u.id AS userId, , , - DATE_FORMAT(u.create_time, '%Y-%m-%d') AS registerDate + DATE_FORMAT(, '%Y-%m-%d') AS registerDate FROM user_base_info u WHERE 1 = 1 diff --git a/rc-service/rc-service-console/docs/country-dashboard-data-query.md b/rc-service/rc-service-console/docs/country-dashboard-data-query.md index 9482d4f3..eb63a7ae 100644 --- a/rc-service/rc-service-console/docs/country-dashboard-data-query.md +++ b/rc-service/rc-service-console/docs/country-dashboard-data-query.md @@ -15,6 +15,7 @@ - `periodType`: `DAY` / `WEEK` / `MONTH` / `ALL` - `startDate`, `endDate`: 闭区间日期,后端转为 `[startDate 00:00:00, endDate + 1 day 00:00:00)` +- `statTimezone`: 统计时区,默认 `Asia/Riyadh`,支持 `Asia/Riyadh` / `UTC` / `Asia/Shanghai` - `countryKeyword`: 国家码或国家名模糊匹配 - `sysOrigin`: 系统来源,预聚合查询必须传 @@ -32,7 +33,7 @@ ### `country_dashboard_period_metric` -周期汇总表,按 `period_type + period_key + sys_origin + country_code` 唯一。 +周期汇总表,按 `period_type + period_key + stat_timezone + sys_origin + country_code` 唯一。 周期键示例: @@ -45,7 +46,7 @@ ### `country_dashboard_period_user_metric` -周期用户去重明细表,按 `period_type + period_key + sys_origin + country_code + user_id + metric_type` 唯一。 +周期用户去重明细表,按 `period_type + period_key + stat_timezone + sys_origin + country_code + user_id + metric_type` 唯一。 当前维护的 `metric_type`: @@ -62,6 +63,7 @@ 走预聚合的条件: - `sysOrigin` 不为空。 +- 对应 `statTimezone` 的周期预聚合已经存在。 - `DAY`: 查询范围内每一天都有 `country_dashboard_period_metric` 数据。 - `WEEK/MONTH`: 查询范围必须按周期边界对齐,当前未结束的周/月允许到今天。 - `ALL`: 无 `startDate/endDate` 时固定走 `period_type=ALL, period_key=ALL`。如果汇总表未回填,返回空汇总,不再回退全历史实时扫描。 @@ -95,10 +97,18 @@ red-circle: 刷新链路: -1. 刷新日表 `country_dashboard_daily_metric`。 -2. 刷新对应 `DAY` 周期表和用户去重明细。 -3. 根据最近日期刷新受影响的 `WEEK`、`MONTH`。 -4. 基于已存在的日表和日级用户明细刷新 `ALL`。 +1. 按利雅得时区刷新日表 `country_dashboard_daily_metric`。 +2. 按配置的统计时区刷新对应 `DAY` 周期表和用户去重明细。 +3. 按配置的统计时区刷新受影响的 `WEEK`、`MONTH`。 +4. 基于同一 `stat_timezone` 下已存在的 `DAY` 周期表和日级用户明细刷新 `ALL`。 + +默认刷新时区: + +```yaml +red-circle: + country-dashboard: + refresh-timezones: Asia/Riyadh,UTC,Asia/Shanghai +``` ## 指标来源 @@ -126,6 +136,7 @@ SQL 预聚合指标: - `sql/20260507_country_dashboard_daily_metric.sql` - `sql/20260507_country_dashboard_period_metric.sql` +- `sql/20260508_country_dashboard_period_metric_timezone.sql` 为避免遗漏,console 的刷新和手动回填入口会先执行同等的 `CREATE TABLE IF NOT EXISTS` 检查,再写入预聚合数据。 @@ -139,7 +150,13 @@ SQL 预聚合指标: curl -X POST 'http://127.0.0.1:{console_port}/console/datav/country-dashboard/backfill?startDate=2026-04-08&endDate=2026-05-07&sysOrigin=LIKEI' ``` -该入口会按天重算日表和 `DAY` 周期,再刷新受影响的 `WEEK`、`MONTH` 和 `ALL`。 +该入口默认回填 `Asia/Riyadh`、`UTC`、`Asia/Shanghai` 三个统计时区。需要指定时区时可以传: + +```bash +curl -X POST 'http://127.0.0.1:{console_port}/console/datav/country-dashboard/backfill?startDate=2026-04-08&endDate=2026-05-07&sysOrigin=LIKEI&statTimezones=Asia/Riyadh,UTC,Asia/Shanghai' +``` + +该入口会按天重算日表,并按每个 `stat_timezone` 重算 `DAY` 周期,再刷新受影响的 `WEEK`、`MONTH` 和 `ALL`。 ## 验证 SQL @@ -156,19 +173,19 @@ ORDER BY stat_date DESC; 检查周期汇总: ```sql -SELECT period_type, period_key, sys_origin, COUNT(1) +SELECT stat_timezone, period_type, period_key, sys_origin, COUNT(1) FROM country_dashboard_period_metric WHERE sys_origin = 'LIKEI' -GROUP BY period_type, period_key, sys_origin -ORDER BY period_type, period_key DESC; +GROUP BY stat_timezone, period_type, period_key, sys_origin +ORDER BY stat_timezone, period_type, period_key DESC; ``` 检查用户去重明细: ```sql -SELECT period_type, period_key, metric_type, COUNT(1) +SELECT stat_timezone, period_type, period_key, metric_type, COUNT(1) FROM country_dashboard_period_user_metric WHERE sys_origin = 'LIKEI' -GROUP BY period_type, period_key, metric_type -ORDER BY period_type, period_key DESC, metric_type; +GROUP BY stat_timezone, period_type, period_key, metric_type +ORDER BY stat_timezone, period_type, period_key DESC, metric_type; ``` diff --git a/sql/20260507_country_dashboard_period_metric.sql b/sql/20260507_country_dashboard_period_metric.sql index da20803a..d6008707 100644 --- a/sql/20260507_country_dashboard_period_metric.sql +++ b/sql/20260507_country_dashboard_period_metric.sql @@ -4,6 +4,7 @@ CREATE TABLE IF NOT EXISTS `country_dashboard_period_metric` ( `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键', `period_type` varchar(16) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '周期类型:DAY/WEEK/MONTH/ALL', `period_key` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '周期键', + `stat_timezone` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'Asia/Riyadh' COMMENT '统计时区', `period_name` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '周期名称', `period_start_date` date DEFAULT NULL COMMENT '周期开始日期', `period_end_date` date DEFAULT NULL COMMENT '周期结束日期,开区间', @@ -27,15 +28,16 @@ CREATE TABLE IF NOT EXISTS `country_dashboard_period_metric` ( `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', PRIMARY KEY (`id`), - UNIQUE KEY `uk_period_origin_country` (`period_type`, `period_key`, `sys_origin`, `country_code`), - KEY `idx_origin_period` (`sys_origin`, `period_type`, `period_key`), - KEY `idx_period_date` (`period_type`, `period_start_date`, `period_end_date`) + UNIQUE KEY `uk_period_origin_country` (`period_type`, `period_key`, `stat_timezone`, `sys_origin`, `country_code`), + KEY `idx_origin_period` (`sys_origin`, `stat_timezone`, `period_type`, `period_key`), + KEY `idx_period_date` (`stat_timezone`, `period_type`, `period_start_date`, `period_end_date`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='国家数据大屏周期预聚合'; CREATE TABLE IF NOT EXISTS `country_dashboard_period_user_metric` ( `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键', `period_type` varchar(16) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '周期类型:DAY/WEEK/MONTH/ALL', `period_key` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '周期键', + `stat_timezone` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'Asia/Riyadh' COMMENT '统计时区', `period_name` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '周期名称', `period_start_date` date DEFAULT NULL COMMENT '周期开始日期', `period_end_date` date DEFAULT NULL COMMENT '周期结束日期,开区间', @@ -46,7 +48,7 @@ CREATE TABLE IF NOT EXISTS `country_dashboard_period_user_metric` ( `metric_type` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '人数指标类型', `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', PRIMARY KEY (`id`), - UNIQUE KEY `uk_period_user_metric` (`period_type`, `period_key`, `sys_origin`, `country_code`, `user_id`, `metric_type`), - KEY `idx_period_metric_country` (`period_type`, `period_key`, `sys_origin`, `metric_type`, `country_code`), - KEY `idx_origin_user_metric` (`sys_origin`, `user_id`, `metric_type`) + UNIQUE KEY `uk_period_user_metric` (`period_type`, `period_key`, `stat_timezone`, `sys_origin`, `country_code`, `user_id`, `metric_type`), + KEY `idx_period_metric_country` (`period_type`, `period_key`, `stat_timezone`, `sys_origin`, `metric_type`, `country_code`), + KEY `idx_origin_user_metric` (`sys_origin`, `stat_timezone`, `user_id`, `metric_type`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='国家数据大屏周期用户去重明细'; diff --git a/sql/20260508_country_dashboard_period_metric_timezone.sql b/sql/20260508_country_dashboard_period_metric_timezone.sql new file mode 100644 index 00000000..9e9c285b --- /dev/null +++ b/sql/20260508_country_dashboard_period_metric_timezone.sql @@ -0,0 +1,141 @@ +SET NAMES utf8mb4; + +SET @current_schema = DATABASE(); + +SET @ddl = IF( + EXISTS ( + SELECT 1 + FROM INFORMATION_SCHEMA.COLUMNS + WHERE TABLE_SCHEMA = @current_schema + AND TABLE_NAME = 'country_dashboard_period_metric' + AND COLUMN_NAME = 'stat_timezone' + ), + 'SELECT 1', + 'ALTER TABLE `country_dashboard_period_metric` ADD COLUMN `stat_timezone` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT ''Asia/Riyadh'' COMMENT ''统计时区'' AFTER `period_key`' +); +PREPARE stmt FROM @ddl; +EXECUTE stmt; +DEALLOCATE PREPARE stmt; + +SET @ddl = IF( + EXISTS ( + SELECT 1 + FROM INFORMATION_SCHEMA.COLUMNS + WHERE TABLE_SCHEMA = @current_schema + AND TABLE_NAME = 'country_dashboard_period_user_metric' + AND COLUMN_NAME = 'stat_timezone' + ), + 'SELECT 1', + 'ALTER TABLE `country_dashboard_period_user_metric` ADD COLUMN `stat_timezone` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT ''Asia/Riyadh'' COMMENT ''统计时区'' AFTER `period_key`' +); +PREPARE stmt FROM @ddl; +EXECUTE stmt; +DEALLOCATE PREPARE stmt; + +SET @ddl = IF( + EXISTS ( + SELECT 1 + FROM INFORMATION_SCHEMA.STATISTICS + WHERE TABLE_SCHEMA = @current_schema + AND TABLE_NAME = 'country_dashboard_period_metric' + AND INDEX_NAME = 'uk_period_origin_country' + ), + 'ALTER TABLE `country_dashboard_period_metric` DROP INDEX `uk_period_origin_country`', + 'SELECT 1' +); +PREPARE stmt FROM @ddl; +EXECUTE stmt; +DEALLOCATE PREPARE stmt; + +ALTER TABLE `country_dashboard_period_metric` + ADD UNIQUE KEY `uk_period_origin_country` (`period_type`, `period_key`, `stat_timezone`, `sys_origin`, `country_code`); + +SET @ddl = IF( + EXISTS ( + SELECT 1 + FROM INFORMATION_SCHEMA.STATISTICS + WHERE TABLE_SCHEMA = @current_schema + AND TABLE_NAME = 'country_dashboard_period_metric' + AND INDEX_NAME = 'idx_origin_period' + ), + 'ALTER TABLE `country_dashboard_period_metric` DROP INDEX `idx_origin_period`', + 'SELECT 1' +); +PREPARE stmt FROM @ddl; +EXECUTE stmt; +DEALLOCATE PREPARE stmt; + +ALTER TABLE `country_dashboard_period_metric` + ADD KEY `idx_origin_period` (`sys_origin`, `stat_timezone`, `period_type`, `period_key`); + +SET @ddl = IF( + EXISTS ( + SELECT 1 + FROM INFORMATION_SCHEMA.STATISTICS + WHERE TABLE_SCHEMA = @current_schema + AND TABLE_NAME = 'country_dashboard_period_metric' + AND INDEX_NAME = 'idx_period_date' + ), + 'ALTER TABLE `country_dashboard_period_metric` DROP INDEX `idx_period_date`', + 'SELECT 1' +); +PREPARE stmt FROM @ddl; +EXECUTE stmt; +DEALLOCATE PREPARE stmt; + +ALTER TABLE `country_dashboard_period_metric` + ADD KEY `idx_period_date` (`stat_timezone`, `period_type`, `period_start_date`, `period_end_date`); + +SET @ddl = IF( + EXISTS ( + SELECT 1 + FROM INFORMATION_SCHEMA.STATISTICS + WHERE TABLE_SCHEMA = @current_schema + AND TABLE_NAME = 'country_dashboard_period_user_metric' + AND INDEX_NAME = 'uk_period_user_metric' + ), + 'ALTER TABLE `country_dashboard_period_user_metric` DROP INDEX `uk_period_user_metric`', + 'SELECT 1' +); +PREPARE stmt FROM @ddl; +EXECUTE stmt; +DEALLOCATE PREPARE stmt; + +ALTER TABLE `country_dashboard_period_user_metric` + ADD UNIQUE KEY `uk_period_user_metric` (`period_type`, `period_key`, `stat_timezone`, `sys_origin`, `country_code`, `user_id`, `metric_type`); + +SET @ddl = IF( + EXISTS ( + SELECT 1 + FROM INFORMATION_SCHEMA.STATISTICS + WHERE TABLE_SCHEMA = @current_schema + AND TABLE_NAME = 'country_dashboard_period_user_metric' + AND INDEX_NAME = 'idx_period_metric_country' + ), + 'ALTER TABLE `country_dashboard_period_user_metric` DROP INDEX `idx_period_metric_country`', + 'SELECT 1' +); +PREPARE stmt FROM @ddl; +EXECUTE stmt; +DEALLOCATE PREPARE stmt; + +ALTER TABLE `country_dashboard_period_user_metric` + ADD KEY `idx_period_metric_country` (`period_type`, `period_key`, `stat_timezone`, `sys_origin`, `metric_type`, `country_code`); + +SET @ddl = IF( + EXISTS ( + SELECT 1 + FROM INFORMATION_SCHEMA.STATISTICS + WHERE TABLE_SCHEMA = @current_schema + AND TABLE_NAME = 'country_dashboard_period_user_metric' + AND INDEX_NAME = 'idx_origin_user_metric' + ), + 'ALTER TABLE `country_dashboard_period_user_metric` DROP INDEX `idx_origin_user_metric`', + 'SELECT 1' +); +PREPARE stmt FROM @ddl; +EXECUTE stmt; +DEALLOCATE PREPARE stmt; + +ALTER TABLE `country_dashboard_period_user_metric` + ADD KEY `idx_origin_user_metric` (`sys_origin`, `stat_timezone`, `user_id`, `metric_type`);