feat(console): add dashboard timezone preaggregation

This commit is contained in:
hy001 2026-05-08 12:20:12 +08:00
parent 45cb56b45a
commit c2bee924ce
11 changed files with 928 additions and 195 deletions

View File

@ -67,15 +67,18 @@ public class DatavRestController extends BaseController {
public Map<String, Object> 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<String> refreshedTimezones = countryDashboardDailyMetricService.refreshRange(
parsedStartDate, parsedEndDate, sysOrigin, statTimezones);
return Map.of(
"status", "success",
"startDate", parsedStartDate.toString(),
"endDate", parsedEndDate.toString(),
"sysOrigin", sysOrigin);
"sysOrigin", sysOrigin,
"statTimezones", refreshedTimezones);
}
}

View File

@ -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<LocalDate> statDates = new ArrayList<>();
for (int i = safeDays - 1; i >= 0; i--) {
statDates.add(today.minusDays(i));
}
List<LocalDate> storageStatDates = recentDates(safeDays, STORAGE_ZONE_ID);
List<StatTimezone> statTimezones = resolveStatTimezones(null);
List<String> 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<LocalDate> 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<String> 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<StatTimezone> 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<String, CountryDashboardDailyMetricDTO> 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<CountryDashboardDailyMetricDTO> 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<LocalDate> statDates, String sysOrigin,
boolean failFast) {
StatTimezone statTimezone, boolean failFast) {
Set<PeriodWindow> 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<String, CountryDashboardPeriodMetricDTO> 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<CountryDashboardPeriodMetricDTO> 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<String, CountryDashboardPeriodMetricDTO> 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<CountryDashboardPeriodMetricDTO> 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<String, CountryDashboardPeriodMetricDTO> rows,
List<CountryDashboardMetricDTO> metrics, PeriodWindow window, String sysOrigin) {
List<CountryDashboardMetricDTO> 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<String, CountryDashboardPeriodMetricDTO> rows,
List<CountryDashboardMetricDTO> metrics, PeriodWindow window, String sysOrigin) {
List<CountryDashboardMetricDTO> 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<String, CountryDashboardPeriodMetricDTO> 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<LocalDate> recentDates(int days, ZoneId zoneId) {
LocalDate today = LocalDate.now(zoneId);
List<LocalDate> 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<StatTimezone> resolveStatTimezones(String statTimezonesValue) {
String config = trimToNull(statTimezonesValue);
if (config == null) {
config = trimToNull(refreshTimezones);
}
if (config == null) {
config = "Asia/Riyadh,UTC,Asia/Shanghai";
}
Map<String, StatTimezone> 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<String> 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) {
}
}

View File

@ -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<String, CountryDashboardMetricCO> 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<String> 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<String> expectedAlignedPeriodKeys(String periodType, LocalDate start,
LocalDate endExclusive) {
LocalDate endExclusive, ZoneId statZoneId) {
List<String> 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<CountryDashboardRechargeDetailCO> 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<String, CountryDashboardMetricCO> rows, QueryCondition condition) {
List<CountryDashboardRetentionUserDTO> 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<Long> userIds = new HashSet<>();
Set<String> 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<MongoMetric> listLuckyGiftAnchorShare(QueryCondition condition) {
List<Criteria> 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<MongoUserDayMetric> listDailyActive(QueryCondition condition) {
@ -428,11 +451,11 @@ public class CountryDashboardServiceImpl implements CountryDashboardService {
List<Criteria> 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<MongoMetric> aggregateGiftAmount(List<Criteria> criteria, String amountPath,
boolean unwindAcceptUsers) {
boolean unwindAcceptUsers, String statTimezone) {
List<AggregationOperation> 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);

View File

@ -22,6 +22,8 @@ public class CountryDashboardCO implements Serializable {
private String endDate;
private String statTimezone;
private String computedAt;
private CountryDashboardMetricCO total;

View File

@ -30,6 +30,11 @@ public class CountryDashboardQryCmd implements Serializable {
*/
private String endDate;
/**
* 统计时区默认 Asia/Riyadh.
*/
private String statTimezone;
/**
* 国家编码或国家名称关键字.
*/

View File

@ -25,61 +25,79 @@ public interface CountryDashboardDAO extends BaseDAO<IUserBaseInfo> {
int createPeriodUserMetricTable();
int ensurePeriodMetricTimezoneSchema();
List<CountryDashboardMetricDTO> 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<CountryDashboardMetricDTO> 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<CountryDashboardMetricDTO> 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<CountryDashboardMetricDTO> 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<CountryDashboardMetricDTO> 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<CountryDashboardMetricDTO> 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<CountryDashboardMetricDTO> 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<CountryDashboardMetricDTO> 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<CountryDashboardMetricDTO> listWalletGame(
@Param("periodType") String periodType,
@ -88,14 +106,18 @@ public interface CountryDashboardDAO extends BaseDAO<IUserBaseInfo> {
@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<CountryDashboardMetricDTO> 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<CountryDashboardMetricDTO> listGameUser(
@Param("periodType") String periodType,
@ -104,7 +126,9 @@ public interface CountryDashboardDAO extends BaseDAO<IUserBaseInfo> {
@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<CountryDashboardMetricDTO> listPrecomputedMetrics(
@Param("periodType") String periodType,
@ -118,6 +142,7 @@ public interface CountryDashboardDAO extends BaseDAO<IUserBaseInfo> {
@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<IUserBaseInfo> {
Long countPrecomputedPeriods(
@Param("periodType") String periodType,
@Param("periodKeys") Collection<String> 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<IUserBaseInfo> {
int deletePeriodMetrics(
@Param("periodType") String periodType,
@Param("periodKey") String periodKey,
@Param("statTimezone") String statTimezone,
@Param("sysOrigin") String sysOrigin);
int upsertPeriodMetrics(@Param("metrics") List<CountryDashboardPeriodMetricDTO> metrics);
@ -148,6 +177,7 @@ public interface CountryDashboardDAO extends BaseDAO<IUserBaseInfo> {
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<IUserBaseInfo> {
@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<IUserBaseInfo> {
@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<IUserBaseInfo> {
@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<CountryDashboardMetricDTO> listPeriodUserMetricCounts(
@Param("periodType") String periodType,
@Param("periodKey") String periodKey,
@Param("statTimezone") String statTimezone,
@Param("sysOrigin") String sysOrigin);
List<CountryDashboardMetricDTO> listAllPeriodAmountsFromDaily(@Param("sysOrigin") String sysOrigin);
List<CountryDashboardMetricDTO> listAllPeriodAmountsFromPeriodDays(
@Param("statTimezone") String statTimezone,
@Param("sysOrigin") String sysOrigin);
List<CountryDashboardRetentionUserDTO> 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<CountryDashboardUserCountryDTO> listUserCountryByIds(@Param("userIds") Collection<Long> userIds);
@ -205,7 +245,9 @@ public interface CountryDashboardDAO extends BaseDAO<IUserBaseInfo> {
@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<CountryDashboardRechargeDetailDTO> listRechargeDetails(
@Param("startTime") LocalDateTime startTime,
@ -213,6 +255,8 @@ public interface CountryDashboardDAO extends BaseDAO<IUserBaseInfo> {
@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);
}

View File

@ -16,6 +16,8 @@ public class CountryDashboardPeriodMetricDTO {
private String periodKey;
private String statTimezone;
private String periodName;
private LocalDate periodStartDate;

View File

@ -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='国家数据大屏周期预聚合'
</update>
@ -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='国家数据大屏周期用户去重明细'
</update>
<update id="ensurePeriodMetricTimezoneSchema">
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 &lt;&gt; '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 &lt;&gt; '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 &lt;&gt; '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 &lt;&gt; '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 &lt;&gt; '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 &lt;&gt; '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);
</update>
<sql id="CountryColumns">
NULLIF(u.country_code, '') AS countryCode,
NULLIF(u.country_name, '') AS countryName
</sql>
<sql id="StatTimeExprT">
<choose>
<when test="storageTimezoneOffset != null and storageTimezoneOffset != '' and statTimezoneOffset != null and statTimezoneOffset != ''">
CONVERT_TZ(t.create_time, #{storageTimezoneOffset}, #{statTimezoneOffset})
</when>
<otherwise>t.create_time</otherwise>
</choose>
</sql>
<sql id="StatTimeExprU">
<choose>
<when test="storageTimezoneOffset != null and storageTimezoneOffset != '' and statTimezoneOffset != null and statTimezoneOffset != ''">
CONVERT_TZ(u.create_time, #{storageTimezoneOffset}, #{statTimezoneOffset})
</when>
<otherwise>u.create_time</otherwise>
</choose>
</sql>
<sql id="PeriodColumnsT">
<choose>
<when test="periodType == 'DAY'">DATE_FORMAT(t.create_time, '%Y-%m-%d')</when>
<when test="periodType == 'WEEK'">DATE_FORMAT(t.create_time, '%x-W%v')</when>
<when test="periodType == 'MONTH'">DATE_FORMAT(t.create_time, '%Y-%m')</when>
<when test="periodType == 'DAY'">DATE_FORMAT(<include refid="StatTimeExprT"/>, '%Y-%m-%d')</when>
<when test="periodType == 'WEEK'">DATE_FORMAT(<include refid="StatTimeExprT"/>, '%x-W%v')</when>
<when test="periodType == 'MONTH'">DATE_FORMAT(<include refid="StatTimeExprT"/>, '%Y-%m')</when>
<otherwise>'ALL'</otherwise>
</choose>
AS periodKey,
<choose>
<when test="periodType == 'DAY'">DATE_FORMAT(t.create_time, '%Y-%m-%d')</when>
<when test="periodType == 'WEEK'">DATE_FORMAT(t.create_time, '%x-W%v')</when>
<when test="periodType == 'MONTH'">DATE_FORMAT(t.create_time, '%Y-%m')</when>
<when test="periodType == 'DAY'">DATE_FORMAT(<include refid="StatTimeExprT"/>, '%Y-%m-%d')</when>
<when test="periodType == 'WEEK'">DATE_FORMAT(<include refid="StatTimeExprT"/>, '%x-W%v')</when>
<when test="periodType == 'MONTH'">DATE_FORMAT(<include refid="StatTimeExprT"/>, '%Y-%m')</when>
<otherwise>'全部'</otherwise>
</choose>
AS periodName
@ -113,16 +376,16 @@
<sql id="PeriodColumnsU">
<choose>
<when test="periodType == 'DAY'">DATE_FORMAT(u.create_time, '%Y-%m-%d')</when>
<when test="periodType == 'WEEK'">DATE_FORMAT(u.create_time, '%x-W%v')</when>
<when test="periodType == 'MONTH'">DATE_FORMAT(u.create_time, '%Y-%m')</when>
<when test="periodType == 'DAY'">DATE_FORMAT(<include refid="StatTimeExprU"/>, '%Y-%m-%d')</when>
<when test="periodType == 'WEEK'">DATE_FORMAT(<include refid="StatTimeExprU"/>, '%x-W%v')</when>
<when test="periodType == 'MONTH'">DATE_FORMAT(<include refid="StatTimeExprU"/>, '%Y-%m')</when>
<otherwise>'ALL'</otherwise>
</choose>
AS periodKey,
<choose>
<when test="periodType == 'DAY'">DATE_FORMAT(u.create_time, '%Y-%m-%d')</when>
<when test="periodType == 'WEEK'">DATE_FORMAT(u.create_time, '%x-W%v')</when>
<when test="periodType == 'MONTH'">DATE_FORMAT(u.create_time, '%Y-%m')</when>
<when test="periodType == 'DAY'">DATE_FORMAT(<include refid="StatTimeExprU"/>, '%Y-%m-%d')</when>
<when test="periodType == 'WEEK'">DATE_FORMAT(<include refid="StatTimeExprU"/>, '%x-W%v')</when>
<when test="periodType == 'MONTH'">DATE_FORMAT(<include refid="StatTimeExprU"/>, '%Y-%m')</when>
<otherwise>'全部'</otherwise>
</choose>
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}
<if test="periodType != 'ALL' and startTime != null">
AND t.period_start_date &gt;= DATE(#{startTime})
</if>
@ -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}
<if test="periodKeys != null and periodKeys.size() &gt; 0">
AND t.period_key IN
<foreach collection="periodKeys" item="periodKey" open="(" separator="," close=")">
@ -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}
<if test="sysOrigin != null and sysOrigin != ''">
AND t.sys_origin = #{sysOrigin}
</if>
@ -537,6 +803,7 @@
DELETE FROM country_dashboard_period_metric
WHERE period_type = #{periodType}
AND period_key = #{periodKey}
AND stat_timezone = #{statTimezone}
<if test="sysOrigin != null and sysOrigin != ''">
AND sys_origin = #{sysOrigin}
</if>
@ -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}
<if test="sysOrigin != null and sysOrigin != ''">
AND sys_origin = #{sysOrigin}
</if>
@ -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}
<if test="sysOrigin != null and sysOrigin != ''">
AND t.sys_origin = #{sysOrigin}
</if>
@ -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}
<if test="sysOrigin != null and sysOrigin != ''">
AND t.sys_origin = #{sysOrigin}
</if>
GROUP BY countryCode, periodKey
</select>
<select id="listAllPeriodAmountsFromDaily"
<select id="listAllPeriodAmountsFromPeriodDays"
resultType="com.red.circle.console.infra.database.rds.dto.datav.CountryDashboardMetricDTO">
SELECT
t.country_code AS countryCode,
@ -804,8 +1084,9 @@
IFNULL(SUM(t.lucky_gift_payout), 0) AS luckyGiftPayout,
IFNULL(SUM(t.game_total_flow), 0) AS gameTotalFlow,
IFNULL(SUM(t.game_payout), 0) AS gamePayout
FROM country_dashboard_daily_metric t
WHERE 1 = 1
FROM country_dashboard_period_metric t
WHERE t.period_type = 'DAY'
AND t.stat_timezone = #{statTimezone}
<if test="sysOrigin != null and sysOrigin != ''">
AND t.sys_origin = #{sysOrigin}
</if>
@ -841,9 +1122,9 @@
<include refid="UserTimeWhere"/>
<include refid="SourceSysOriginWhere"/>
<choose>
<when test="periodType == 'DAY'">AND DATE(u.create_time) = DATE(t.create_time)</when>
<when test="periodType == 'WEEK'">AND DATE_FORMAT(u.create_time, '%x-W%v') = DATE_FORMAT(t.create_time, '%x-W%v')</when>
<when test="periodType == 'MONTH'">AND DATE_FORMAT(u.create_time, '%Y-%m') = DATE_FORMAT(t.create_time, '%Y-%m')</when>
<when test="periodType == 'DAY'">AND DATE(<include refid="StatTimeExprU"/>) = DATE(<include refid="StatTimeExprT"/>)</when>
<when test="periodType == 'WEEK'">AND DATE_FORMAT(<include refid="StatTimeExprU"/>, '%x-W%v') = DATE_FORMAT(<include refid="StatTimeExprT"/>, '%x-W%v')</when>
<when test="periodType == 'MONTH'">AND DATE_FORMAT(<include refid="StatTimeExprU"/>, '%Y-%m') = DATE_FORMAT(<include refid="StatTimeExprT"/>, '%Y-%m')</when>
</choose>
GROUP BY countryCode, countryName, periodKey, periodName
</select>
@ -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 (
<include refid="RechargeDetailSource"/>
) t
@ -1067,7 +1351,7 @@
u.id AS userId,
<include refid="CountryColumns"/>,
<include refid="PeriodColumnsU"/>,
DATE_FORMAT(u.create_time, '%Y-%m-%d') AS registerDate
DATE_FORMAT(<include refid="StatTimeExprU"/>, '%Y-%m-%d') AS registerDate
FROM user_base_info u
WHERE 1 = 1
<include refid="UserWhere"/>

View File

@ -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;
```

View File

@ -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='国家数据大屏周期用户去重明细';

View File

@ -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`);