From f1e5e750bd92512a2e4a7cdeb8e7f403dac00a49 Mon Sep 17 00:00:00 2001 From: hy001 Date: Wed, 13 May 2026 13:51:39 +0800 Subject: [PATCH] feat(console): add country dashboard game metrics --- .../adapter/app/user/DatavRestController.java | 28 + .../CountryDashboardDailyMetricTask.java | 4 + ...CountryDashboardGameMetricServiceImpl.java | 646 ++++++++++++++++++ .../datav/CountryDashboardGameMetricCO.java | 50 ++ .../CountryDashboardGameMetricQryCmd.java | 53 ++ .../CountryDashboardGameMetricService.java | 20 + .../rds/dao/datav/CountryDashboardDAO.java | 103 +++ .../datav/CountryDashboardGameMetricDTO.java | 51 ++ .../dao/datav/CountryDashboardDAO.xml | 525 +++++++++++++- .../src/main/resources/application.yml | 1 + ...3_country_dashboard_game_period_metric.sql | 52 ++ 11 files changed, 1521 insertions(+), 12 deletions(-) create mode 100644 rc-service/rc-service-console/console-application/src/main/java/com/red/circle/console/app/service/app/datav/CountryDashboardGameMetricServiceImpl.java create mode 100644 rc-service/rc-service-console/console-client/src/main/java/com/red/circle/console/app/dto/clientobject/datav/CountryDashboardGameMetricCO.java create mode 100644 rc-service/rc-service-console/console-client/src/main/java/com/red/circle/console/app/dto/cmd/datav/CountryDashboardGameMetricQryCmd.java create mode 100644 rc-service/rc-service-console/console-client/src/main/java/com/red/circle/console/app/service/app/datav/CountryDashboardGameMetricService.java create mode 100644 rc-service/rc-service-console/console-infrastructure/src/main/java/com/red/circle/console/infra/database/rds/dto/datav/CountryDashboardGameMetricDTO.java create mode 100644 sql/20260513_country_dashboard_game_period_metric.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 59d0613d..2fd70cb2 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 @@ -1,10 +1,13 @@ package com.red.circle.console.adapter.app.user; import com.red.circle.console.app.dto.clientobject.datav.CountryDashboardCO; +import com.red.circle.console.app.dto.clientobject.datav.CountryDashboardGameMetricCO; import com.red.circle.console.app.dto.clientobject.datav.CountryDashboardRechargeDetailCO; +import com.red.circle.console.app.dto.cmd.datav.CountryDashboardGameMetricQryCmd; import com.red.circle.console.app.dto.cmd.datav.CountryDashboardQryCmd; import com.red.circle.console.app.dto.cmd.datav.CountryDashboardRechargeDetailQryCmd; import com.red.circle.console.app.service.app.datav.CountryDashboardDailyMetricService; +import com.red.circle.console.app.service.app.datav.CountryDashboardGameMetricService; import com.red.circle.console.app.service.app.datav.CountryDashboardService; import com.red.circle.console.app.service.app.user.DatavService; import com.red.circle.framework.dto.PageResult; @@ -36,6 +39,7 @@ public class DatavRestController extends BaseController { private final DatavService userExpandService; private final CountryDashboardService countryDashboardService; private final CountryDashboardDailyMetricService countryDashboardDailyMetricService; + private final CountryDashboardGameMetricService countryDashboardGameMetricService; @GetMapping("/active/user-country-code") public List latestActiveUserCountryCode() { @@ -63,6 +67,12 @@ public class DatavRestController extends BaseController { return countryDashboardService.pageRechargeDetails(cmd); } + @GetMapping("/country-dashboard/game-metrics") + public PageResult countryDashboardGameMetrics( + CountryDashboardGameMetricQryCmd cmd) { + return countryDashboardGameMetricService.pageGameMetrics(cmd); + } + @PostMapping("/country-dashboard/backfill") public Map backfillCountryDashboard( @RequestParam String startDate, @@ -81,4 +91,22 @@ public class DatavRestController extends BaseController { "statTimezones", refreshedTimezones); } + @PostMapping("/country-dashboard/game-metrics/backfill") + public Map backfillCountryDashboardGameMetrics( + @RequestParam String startDate, + @RequestParam String endDate, + @RequestParam(value = "sysOrigin", defaultValue = "LIKEI") String sysOrigin, + @RequestParam(value = "statTimezones", required = false) String statTimezones) { + LocalDate parsedStartDate = LocalDate.parse(startDate); + LocalDate parsedEndDate = LocalDate.parse(endDate); + List refreshedTimezones = countryDashboardGameMetricService.refreshRange( + parsedStartDate, parsedEndDate, sysOrigin, statTimezones); + return Map.of( + "status", "success", + "startDate", parsedStartDate.toString(), + "endDate", parsedEndDate.toString(), + "sysOrigin", sysOrigin, + "statTimezones", refreshedTimezones); + } + } diff --git a/rc-service/rc-service-console/console-application/src/main/java/com/red/circle/console/app/scheduler/CountryDashboardDailyMetricTask.java b/rc-service/rc-service-console/console-application/src/main/java/com/red/circle/console/app/scheduler/CountryDashboardDailyMetricTask.java index 0026bbc9..1a3f88f3 100644 --- a/rc-service/rc-service-console/console-application/src/main/java/com/red/circle/console/app/scheduler/CountryDashboardDailyMetricTask.java +++ b/rc-service/rc-service-console/console-application/src/main/java/com/red/circle/console/app/scheduler/CountryDashboardDailyMetricTask.java @@ -2,6 +2,7 @@ package com.red.circle.console.app.scheduler; import com.red.circle.component.redis.annotation.TaskCacheLock; import com.red.circle.console.app.service.app.datav.CountryDashboardDailyMetricService; +import com.red.circle.console.app.service.app.datav.CountryDashboardGameMetricService; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; @@ -17,6 +18,7 @@ import org.springframework.stereotype.Component; public class CountryDashboardDailyMetricTask { private final CountryDashboardDailyMetricService countryDashboardDailyMetricService; + private final CountryDashboardGameMetricService countryDashboardGameMetricService; @Value("${red-circle.country-dashboard.refresh-today-days:1}") private int refreshTodayDays; @@ -30,6 +32,7 @@ public class CountryDashboardDailyMetricTask { public void refreshToday() { long start = System.currentTimeMillis(); countryDashboardDailyMetricService.refreshRecentDays(refreshTodayDays); + countryDashboardGameMetricService.refreshRecentDays(refreshTodayDays); log.info("country dashboard refresh today task finished, days={}, costMs={}", refreshTodayDays, System.currentTimeMillis() - start); } @@ -40,6 +43,7 @@ public class CountryDashboardDailyMetricTask { public void refreshRecent() { long start = System.currentTimeMillis(); countryDashboardDailyMetricService.refreshRecentDays(refreshRecentDays); + countryDashboardGameMetricService.refreshRecentDays(refreshRecentDays); log.info("country dashboard refresh recent task finished, days={}, costMs={}", refreshRecentDays, System.currentTimeMillis() - start); } diff --git a/rc-service/rc-service-console/console-application/src/main/java/com/red/circle/console/app/service/app/datav/CountryDashboardGameMetricServiceImpl.java b/rc-service/rc-service-console/console-application/src/main/java/com/red/circle/console/app/service/app/datav/CountryDashboardGameMetricServiceImpl.java new file mode 100644 index 00000000..f34cc637 --- /dev/null +++ b/rc-service/rc-service-console/console-application/src/main/java/com/red/circle/console/app/service/app/datav/CountryDashboardGameMetricServiceImpl.java @@ -0,0 +1,646 @@ +package com.red.circle.console.app.service.app.datav; + +import com.red.circle.common.business.core.enums.SysOriginPlatformEnum; +import com.red.circle.console.app.dto.clientobject.datav.CountryDashboardGameMetricCO; +import com.red.circle.console.app.dto.cmd.datav.CountryDashboardGameMetricQryCmd; +import com.red.circle.console.infra.database.rds.dao.datav.CountryDashboardDAO; +import com.red.circle.console.infra.database.rds.dto.datav.CountryDashboardGameMetricDTO; +import com.red.circle.framework.dto.PageResult; +import java.math.BigDecimal; +import java.math.RoundingMode; +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; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import org.springframework.transaction.support.TransactionTemplate; + +/** + * 国家数据大屏游戏维度预聚合. + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class CountryDashboardGameMetricServiceImpl implements CountryDashboardGameMetricService { + + private static final String PERIOD_DAY = "DAY"; + private static final String PERIOD_WEEK = "WEEK"; + private static final String PERIOD_MONTH = "MONTH"; + private static final String PERIOD_ALL = "ALL"; + private static final ZoneId STORAGE_ZONE_ID = ZoneId.of("Asia/Riyadh"); + 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 WeekFields WEEK_FIELDS = WeekFields.ISO; + + private final CountryDashboardDAO countryDashboardDAO; + private final TransactionTemplate transactionTemplate; + + @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; + + @Override + public PageResult pageGameMetrics( + CountryDashboardGameMetricQryCmd cmd) { + ensureMetricSchema(); + QueryCondition condition = QueryCondition.of(cmd); + int cursor = Math.max(1, cmd == null || cmd.getCursor() == null ? 1 : cmd.getCursor()); + int limit = Math.max(1, cmd == null || cmd.getLimit() == null ? 50 : cmd.getLimit()); + limit = Math.min(200, limit); + int offset = (cursor - 1) * limit; + + PageResult result = new PageResult<>(); + result.setCurrent(cursor); + result.setSize(limit); + if (!hasText(condition.sysOrigin)) { + result.setTotal(0L); + result.setRecords(List.of()); + return result; + } + + SortSpec sortSpec = normalizeSort(cmd == null ? null : cmd.getSortField(), + cmd == null ? null : cmd.getSortOrder()); + String countryCodes = normalizeCsv(cmd == null ? null : cmd.getCountryCodes()); + String gameProvider = trimToNull(cmd == null ? null : cmd.getGameProvider()); + if (gameProvider != null) { + gameProvider = gameProvider.toUpperCase(Locale.ROOT); + } + String gameKeyword = trimToNull(cmd == null ? null : cmd.getGameKeyword()); + + Long total = countryDashboardDAO.countGamePeriodMetrics( + condition.periodType, condition.startTime, condition.endTime, + PERIOD_ALL.equals(condition.periodType) ? PERIOD_ALL : null, + condition.statTimezone, condition.countryKeyword, countryCodes, gameProvider, gameKeyword, + condition.sysOrigin); + List records = countryDashboardDAO.listGamePeriodMetrics( + condition.periodType, condition.startTime, condition.endTime, + PERIOD_ALL.equals(condition.periodType) ? PERIOD_ALL : null, + condition.statTimezone, condition.countryKeyword, countryCodes, gameProvider, gameKeyword, + condition.sysOrigin, sortSpec.column(), sortSpec.direction(), offset, limit) + .stream() + .map(this::toCO) + .toList(); + + result.setTotal(total == null ? 0L : total); + result.setRecords(records); + return result; + } + + @Override + public void refreshRecentDays(int days) { + ensureMetricSchema(); + int safeDays = Math.max(days, 1); + List sysOrigins = resolveSysOrigins(); + List statTimezones = resolveStatTimezones(null); + for (String sysOrigin : sysOrigins) { + 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 game day 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 game all metric failed, sysOrigin={}, statTimezone={}", + sysOrigin, statTimezone.id(), ex); + } + } + } + } + + @Override + 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"); + } + if (endDate.isBefore(startDate)) { + throw new IllegalArgumentException("endDate must be greater than or equal to startDate"); + } + long dayCount = ChronoUnit.DAYS.between(startDate, endDate) + 1; + if (dayCount > 366) { + throw new IllegalArgumentException("backfill range can not exceed 366 days"); + } + String safeSysOrigin = trimToNull(sysOrigin); + if (safeSysOrigin == null) { + safeSysOrigin = SysOriginPlatformEnum.LIKEI.name(); + } + + List statDates = new ArrayList<>(); + for (long i = 0; i < dayCount; i++) { + statDates.add(startDate.plusDays(i)); + } + List statTimezones = resolveStatTimezones(statTimezonesValue); + long start = System.currentTimeMillis(); + log.info("refresh country dashboard game range start, startDate={}, endDate={}, days={}, sysOrigin={}, statTimezones={}", + startDate, endDate, dayCount, safeSysOrigin, + statTimezones.stream().map(StatTimezone::id).toList()); + 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 game 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() { + if (metricSchemaEnsured) { + return; + } + synchronized (this) { + if (metricSchemaEnsured) { + return; + } + countryDashboardDAO.createGamePeriodMetricTable(); + countryDashboardDAO.createGamePeriodUserMetricTable(); + metricSchemaEnsured = true; + log.info("country dashboard game metric tables ensured"); + } + } + + private void refreshAffectedPeriods(List statDates, String sysOrigin, + StatTimezone statTimezone, boolean failFast) { + Set windows = new LinkedHashSet<>(); + for (LocalDate statDate : statDates) { + windows.add(toWeekWindow(statDate, statTimezone.zoneId())); + windows.add(toMonthWindow(statDate, statTimezone.zoneId())); + } + for (PeriodWindow window : windows) { + try { + refreshPeriod(window, sysOrigin, statTimezone); + } catch (Exception ex) { + if (failFast) { + throw ex; + } + log.error("refresh country dashboard game period metric failed, periodType={}, periodKey={}, sysOrigin={}, statTimezone={}", + window.periodType(), window.periodKey(), sysOrigin, statTimezone.id(), ex); + } + } + } + + private void refreshPeriod(PeriodWindow window, String sysOrigin, StatTimezone statTimezone) { + long start = System.currentTimeMillis(); + TimeWindow timeWindow = toStorageTimeWindow(window, statTimezone); + Map rows = new LinkedHashMap<>(); + mergeAmounts(rows, countryDashboardDAO.listWalletGameMetricAmounts( + window.periodType(), timeWindow.startTime(), timeWindow.endTime(), + timeWindow.startTimeMillis(), timeWindow.endTimeMillis(), sysOrigin, + zoneOffset(STORAGE_ZONE_ID), statTimezone.offset()), window, sysOrigin, statTimezone); + mergeAmounts(rows, countryDashboardDAO.listLuckyBoxGameMetricAmounts( + window.periodType(), timeWindow.startTime(), timeWindow.endTime(), sysOrigin, + zoneOffset(STORAGE_ZONE_ID), statTimezone.offset()), window, sysOrigin, statTimezone); + + RefreshWriteResult writeResult = transactionTemplate.execute(status -> { + countryDashboardDAO.deleteGamePeriodUserMetrics(window.periodType(), window.periodKey(), + statTimezone.id(), sysOrigin); + int walletUsers = countryDashboardDAO.insertWalletGamePeriodUserMetrics( + window.periodType(), window.periodKey(), window.periodName(), + window.periodStartDate(), window.periodEndDate(), statTimezone.id(), + timeWindow.startTime(), timeWindow.endTime(), timeWindow.startTimeMillis(), + timeWindow.endTimeMillis(), sysOrigin); + int luckyBoxUsers = countryDashboardDAO.insertLuckyBoxGamePeriodUserMetrics( + window.periodType(), window.periodKey(), window.periodName(), + window.periodStartDate(), window.periodEndDate(), statTimezone.id(), + timeWindow.startTime(), timeWindow.endTime(), sysOrigin); + mergeUserCounts(rows, countryDashboardDAO.listGamePeriodUserMetricCounts( + window.periodType(), window.periodKey(), statTimezone.id(), sysOrigin), + window, sysOrigin, statTimezone); + List metrics = new ArrayList<>(rows.values()); + int deleted = countryDashboardDAO.deleteGamePeriodMetrics(window.periodType(), + window.periodKey(), statTimezone.id(), sysOrigin); + int inserted = metrics.isEmpty() ? 0 : countryDashboardDAO.upsertGamePeriodMetrics(metrics); + return new RefreshWriteResult(deleted, inserted, walletUsers + luckyBoxUsers); + }); + + log.info("refresh country dashboard game period metric success, periodType={}, periodKey={}, sysOrigin={}, statTimezone={}, rows={}, 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, StatTimezone statTimezone) { + long start = System.currentTimeMillis(); + PeriodWindow window = new PeriodWindow(PERIOD_ALL, PERIOD_ALL, "全部", null, null); + Map rows = new LinkedHashMap<>(); + mergeAmounts(rows, countryDashboardDAO.listAllGamePeriodAmountsFromPeriodDays( + statTimezone.id(), sysOrigin), window, sysOrigin, statTimezone); + + RefreshWriteResult writeResult = transactionTemplate.execute(status -> { + countryDashboardDAO.deleteGamePeriodUserMetrics(PERIOD_ALL, PERIOD_ALL, + statTimezone.id(), sysOrigin); + int userRows = countryDashboardDAO.insertAllGamePeriodUserMetricsFromDaily( + statTimezone.id(), sysOrigin); + mergeUserCounts(rows, countryDashboardDAO.listGamePeriodUserMetricCounts( + PERIOD_ALL, PERIOD_ALL, statTimezone.id(), sysOrigin), window, sysOrigin, statTimezone); + List metrics = new ArrayList<>(rows.values()); + int deleted = countryDashboardDAO.deleteGamePeriodMetrics(PERIOD_ALL, PERIOD_ALL, + statTimezone.id(), sysOrigin); + int inserted = metrics.isEmpty() ? 0 : countryDashboardDAO.upsertGamePeriodMetrics(metrics); + return new RefreshWriteResult(deleted, inserted, userRows); + }); + + log.info("refresh country dashboard game all metric success, sysOrigin={}, statTimezone={}, rows={}, deleted={}, inserted={}, userRows={}, costMs={}", + sysOrigin, statTimezone.id(), rows.size(), writeResult.deleted(), writeResult.inserted(), + writeResult.userRows(), System.currentTimeMillis() - start); + } + + private void mergeAmounts(Map rows, + List metrics, PeriodWindow window, String sysOrigin, + StatTimezone statTimezone) { + if (metrics == null || metrics.isEmpty()) { + return; + } + for (CountryDashboardGameMetricDTO metric : metrics) { + CountryDashboardGameMetricDTO row = getRow(rows, metric, window, sysOrigin, statTimezone); + row.setConsumeAmount(add(row.getConsumeAmount(), metric.getConsumeAmount())); + row.setPayoutAmount(add(row.getPayoutAmount(), metric.getPayoutAmount())); + row.setProfitAmount(add(row.getProfitAmount(), metric.getProfitAmount())); + row.setOrderCount(safeLong(row.getOrderCount()) + safeLong(metric.getOrderCount())); + } + } + + private void mergeUserCounts(Map rows, + List metrics, PeriodWindow window, String sysOrigin, + StatTimezone statTimezone) { + if (metrics == null || metrics.isEmpty()) { + return; + } + for (CountryDashboardGameMetricDTO metric : metrics) { + CountryDashboardGameMetricDTO row = getRow(rows, metric, window, sysOrigin, statTimezone); + row.setUserCount(safeLong(metric.getUserCount())); + } + } + + private CountryDashboardGameMetricDTO getRow( + Map rows, CountryDashboardGameMetricDTO metric, + PeriodWindow window, String sysOrigin, StatTimezone statTimezone) { + String countryCode = hasText(metric.getCountryCode()) ? metric.getCountryCode() : "UNKNOWN"; + String countryName = hasText(metric.getCountryName()) ? metric.getCountryName() : countryCode; + String gameProvider = hasText(metric.getGameProvider()) ? metric.getGameProvider() : "UNKNOWN"; + String gameId = hasText(metric.getGameId()) ? metric.getGameId() : "UNKNOWN"; + String gameName = hasText(metric.getGameName()) ? metric.getGameName() : gameId; + return rows.computeIfAbsent(countryCode + "|" + gameProvider + "|" + gameId, + key -> new CountryDashboardGameMetricDTO() + .setPeriodType(window.periodType()) + .setPeriodKey(window.periodKey()) + .setStatTimezone(statTimezone.id()) + .setPeriodName(window.periodName()) + .setPeriodStartDate(window.periodStartDate()) + .setPeriodEndDate(window.periodEndDate()) + .setSysOrigin(sysOrigin) + .setCountryCode(countryCode) + .setCountryName(countryName) + .setGameProvider(gameProvider) + .setGameId(gameId) + .setGameName(gameName) + .setConsumeAmount(BigDecimal.ZERO) + .setPayoutAmount(BigDecimal.ZERO) + .setProfitAmount(BigDecimal.ZERO) + .setUserCount(0L) + .setOrderCount(0L)); + } + + private CountryDashboardGameMetricCO toCO(CountryDashboardGameMetricDTO dto) { + BigDecimal consume = safe(dto.getConsumeAmount()); + BigDecimal payout = safe(dto.getPayoutAmount()); + BigDecimal profit = safe(dto.getProfitAmount()); + return new CountryDashboardGameMetricCO() + .setPeriodKey(dto.getPeriodKey()) + .setPeriodName(dto.getPeriodName()) + .setStatTimezone(dto.getStatTimezone()) + .setCountryCode(dto.getCountryCode()) + .setCountryName(dto.getCountryName()) + .setGameProvider(dto.getGameProvider()) + .setGameId(dto.getGameId()) + .setGameName(dto.getGameName()) + .setConsumeAmount(consume) + .setPayoutAmount(payout) + .setProfitAmount(profit) + .setPayoutRate(percent(payout, consume)) + .setProfitRate(percent(profit, consume)) + .setUserCount(safeLong(dto.getUserCount())) + .setOrderCount(safeLong(dto.getOrderCount())) + .setRefreshedAt(dto.getRefreshedAt() == null ? null : dto.getRefreshedAt().format(DATETIME_FORMATTER)); + } + + private PeriodWindow toDayWindow(LocalDate statDate) { + String key = statDate.format(DATE_FORMATTER); + return new PeriodWindow(PERIOD_DAY, key, key, statDate, statDate.plusDays(1)); + } + + private PeriodWindow toWeekWindow(LocalDate statDate, ZoneId statZoneId) { + LocalDate start = statDate.with(DayOfWeek.MONDAY); + 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, ZoneId statZoneId) { + LocalDate start = statDate.withDayOfMonth(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, ZoneId statZoneId) { + LocalDate tomorrow = LocalDate.now(statZoneId).plusDays(1); + return naturalEndExclusive.isAfter(tomorrow) ? tomorrow : naturalEndExclusive; + } + + 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 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 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(CountryDashboardGameMetricServiceImpl::normalizeStatTimezone) + .filter(Objects::nonNull) + .forEach(zone -> zones.putIfAbsent(zone.id(), zone)); + if (zones.isEmpty()) { + zones.put(STORAGE_ZONE_ID.getId(), new StatTimezone(STORAGE_ZONE_ID.getId(), STORAGE_ZONE_ID, + zoneOffset(STORAGE_ZONE_ID))); + } + return new ArrayList<>(zones.values()); + } + + private List resolveSysOrigins() { + String config = trimToNull(refreshSysOrigins); + if (config == null) { + return List.of(SysOriginPlatformEnum.LIKEI.name()); + } + if ("ALL".equalsIgnoreCase(config)) { + return SysOriginPlatformEnum.getVoiceSystems().stream() + .map(SysOriginPlatformEnum::name) + .filter(Objects::nonNull) + .distinct() + .toList(); + } + List sysOrigins = Arrays.stream(config.split(",")) + .map(String::trim) + .filter(CountryDashboardGameMetricServiceImpl::hasText) + .distinct() + .toList(); + return sysOrigins.isEmpty() ? List.of(SysOriginPlatformEnum.LIKEI.name()) : sysOrigins; + } + + private static SortSpec normalizeSort(String sortField, String sortOrder) { + String column = switch (Objects.toString(sortField, "")) { + case "payoutAmount" -> "t.payout_amount"; + case "profitAmount" -> "t.profit_amount"; + case "userCount" -> "t.user_count"; + case "orderCount" -> "t.order_count"; + case "consumeAmount" -> "t.consume_amount"; + default -> null; + }; + String direction = "ascend".equalsIgnoreCase(sortOrder) ? "ASC" : "DESC"; + return new SortSpec(column, direction); + } + + private static 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) { + return null; + } + return new StatTimezone(zoneId.getId(), zoneId, zoneOffset(zoneId)); + } + + private static String normalizeCsv(String value) { + if (!hasText(value)) { + return null; + } + String normalized = Arrays.stream(value.split(",")) + .map(String::trim) + .filter(CountryDashboardGameMetricServiceImpl::hasText) + .distinct() + .reduce((left, right) -> left + "," + right) + .orElse(null); + return hasText(normalized) ? normalized : null; + } + + private static BigDecimal add(BigDecimal left, BigDecimal right) { + return safe(left).add(safe(right)); + } + + private static BigDecimal safe(BigDecimal value) { + return value == null ? BigDecimal.ZERO : value; + } + + private static BigDecimal percent(BigDecimal numerator, BigDecimal denominator) { + BigDecimal safeDenominator = safe(denominator); + if (BigDecimal.ZERO.compareTo(safeDenominator) == 0) { + return BigDecimal.ZERO; + } + return safe(numerator) + .multiply(BigDecimal.valueOf(100)) + .divide(safeDenominator, 4, RoundingMode.HALF_UP); + } + + private static long safeLong(Long value) { + return value == null ? 0L : value; + } + + private static boolean hasText(String value) { + return value != null && !value.trim().isEmpty(); + } + + private static String trimToNull(String value) { + if (value == null) { + return null; + } + String trimmed = value.trim(); + return trimmed.isEmpty() ? null : trimmed; + } + + 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 record PeriodWindow(String periodType, String periodKey, String periodName, + 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) { + } + + private record SortSpec(String column, String direction) { + } + + private static final class QueryCondition { + + private final String periodType; + private final String statTimezone; + private final ZoneId statZoneId; + private final LocalDate startDateValue; + private final LocalDate endDateValue; + private final LocalDateTime startTime; + private final LocalDateTime endTime; + private final String countryKeyword; + private final String sysOrigin; + + private QueryCondition(String periodType, String statTimezone, ZoneId statZoneId, + LocalDate startDateValue, LocalDate endDateValue, LocalDateTime startTime, + LocalDateTime endTime, String countryKeyword, String sysOrigin) { + this.periodType = periodType; + this.statTimezone = statTimezone; + this.statZoneId = statZoneId; + this.startDateValue = startDateValue; + this.endDateValue = endDateValue; + this.startTime = startTime; + this.endTime = endTime; + this.countryKeyword = countryKeyword; + this.sysOrigin = sysOrigin; + } + + private static QueryCondition of(CountryDashboardGameMetricQryCmd 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(statZoneId); + switch (periodType) { + case PERIOD_WEEK -> { + start = today.with(DayOfWeek.MONDAY); + end = today.with(DayOfWeek.SUNDAY); + } + case PERIOD_MONTH -> { + start = today.withDayOfMonth(1); + end = today.withDayOfMonth(today.lengthOfMonth()); + } + default -> { + start = today; + end = today; + } + } + } + if (start != null && end == null) { + end = start; + } + if (start == null && end != null) { + start = end; + } + if (start != null && end != null && end.isBefore(start)) { + 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, + statZoneId.getId(), + statZoneId, + start, + end, + storageStartTime, + storageEndTime, + trimToNull(cmd == null ? null : cmd.getCountryKeyword()), + trimToNull(cmd == null ? null : cmd.getSysOrigin()) + ); + } + + private static String normalizePeriodType(String periodType) { + String value = trimToNull(periodType); + if (value == null) { + return PERIOD_DAY; + } + value = value.toUpperCase(Locale.ROOT); + return switch (value) { + case PERIOD_DAY, PERIOD_WEEK, PERIOD_MONTH, PERIOD_ALL -> value; + default -> PERIOD_DAY; + }; + } + + private static ZoneId normalizeStatZoneId(String statTimezone) { + StatTimezone normalized = normalizeStatTimezone(statTimezone); + return normalized == null ? STORAGE_ZONE_ID : normalized.zoneId(); + } + + 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/CountryDashboardGameMetricCO.java b/rc-service/rc-service-console/console-client/src/main/java/com/red/circle/console/app/dto/clientobject/datav/CountryDashboardGameMetricCO.java new file mode 100644 index 00000000..e360060d --- /dev/null +++ b/rc-service/rc-service-console/console-client/src/main/java/com/red/circle/console/app/dto/clientobject/datav/CountryDashboardGameMetricCO.java @@ -0,0 +1,50 @@ +package com.red.circle.console.app.dto.clientobject.datav; + +import java.io.Serial; +import java.io.Serializable; +import java.math.BigDecimal; +import lombok.Data; +import lombok.experimental.Accessors; + +/** + * 国家数据大屏游戏维度指标. + */ +@Data +@Accessors(chain = true) +public class CountryDashboardGameMetricCO implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + private String periodKey; + + private String periodName; + + private String statTimezone; + + private String countryCode; + + private String countryName; + + private String gameProvider; + + private String gameId; + + private String gameName; + + private BigDecimal consumeAmount = BigDecimal.ZERO; + + private BigDecimal payoutAmount = BigDecimal.ZERO; + + private BigDecimal profitAmount = BigDecimal.ZERO; + + private BigDecimal payoutRate = BigDecimal.ZERO; + + private BigDecimal profitRate = BigDecimal.ZERO; + + private Long userCount = 0L; + + private Long orderCount = 0L; + + private String refreshedAt; +} diff --git a/rc-service/rc-service-console/console-client/src/main/java/com/red/circle/console/app/dto/cmd/datav/CountryDashboardGameMetricQryCmd.java b/rc-service/rc-service-console/console-client/src/main/java/com/red/circle/console/app/dto/cmd/datav/CountryDashboardGameMetricQryCmd.java new file mode 100644 index 00000000..c89511e8 --- /dev/null +++ b/rc-service/rc-service-console/console-client/src/main/java/com/red/circle/console/app/dto/cmd/datav/CountryDashboardGameMetricQryCmd.java @@ -0,0 +1,53 @@ +package com.red.circle.console.app.dto.cmd.datav; + +import java.io.Serial; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.experimental.Accessors; + +/** + * 国家数据大屏游戏数据查询. + */ +@Data +@Accessors(chain = true) +@EqualsAndHashCode(callSuper = true) +public class CountryDashboardGameMetricQryCmd extends CountryDashboardQryCmd { + + @Serial + private static final long serialVersionUID = 1L; + + /** + * 页码,从 1 开始. + */ + private Integer cursor; + + /** + * 每页条数. + */ + private Integer limit; + + /** + * 多个国家编码,逗号分隔. + */ + private String countryCodes; + + /** + * 游戏厂商. + */ + private String gameProvider; + + /** + * 游戏 ID 或名称关键字. + */ + private String gameKeyword; + + /** + * 排序字段. + */ + private String sortField; + + /** + * ascend / descend. + */ + private String sortOrder; +} diff --git a/rc-service/rc-service-console/console-client/src/main/java/com/red/circle/console/app/service/app/datav/CountryDashboardGameMetricService.java b/rc-service/rc-service-console/console-client/src/main/java/com/red/circle/console/app/service/app/datav/CountryDashboardGameMetricService.java new file mode 100644 index 00000000..8dd5f347 --- /dev/null +++ b/rc-service/rc-service-console/console-client/src/main/java/com/red/circle/console/app/service/app/datav/CountryDashboardGameMetricService.java @@ -0,0 +1,20 @@ +package com.red.circle.console.app.service.app.datav; + +import com.red.circle.console.app.dto.clientobject.datav.CountryDashboardGameMetricCO; +import com.red.circle.console.app.dto.cmd.datav.CountryDashboardGameMetricQryCmd; +import com.red.circle.framework.dto.PageResult; +import java.time.LocalDate; +import java.util.List; + +/** + * 国家数据大屏游戏维度指标. + */ +public interface CountryDashboardGameMetricService { + + PageResult pageGameMetrics(CountryDashboardGameMetricQryCmd cmd); + + void refreshRecentDays(int days); + + List refreshRange(LocalDate startDate, LocalDate endDate, String sysOrigin, + String statTimezones); +} 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 ff2903b9..21c69675 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 @@ -1,6 +1,7 @@ package com.red.circle.console.infra.database.rds.dao.datav; import com.red.circle.console.infra.database.rds.dto.datav.CountryDashboardDailyMetricDTO; +import com.red.circle.console.infra.database.rds.dto.datav.CountryDashboardGameMetricDTO; import com.red.circle.console.infra.database.rds.dto.datav.CountryDashboardMetricDTO; import com.red.circle.console.infra.database.rds.dto.datav.CountryDashboardPeriodMetricDTO; import com.red.circle.console.infra.database.rds.dto.datav.CountryDashboardRechargeDetailDTO; @@ -25,6 +26,10 @@ public interface CountryDashboardDAO extends BaseDAO { int createPeriodUserMetricTable(); + int createGamePeriodMetricTable(); + + int createGamePeriodUserMetricTable(); + int ensurePeriodMetricTimezoneSchema(); List listNewUsers( @@ -259,4 +264,102 @@ public interface CountryDashboardDAO extends BaseDAO { @Param("statTimezoneOffset") String statTimezoneOffset, @Param("offset") int offset, @Param("limit") int limit); + + List listWalletGameMetricAmounts( + @Param("periodType") String periodType, + @Param("startTime") LocalDateTime startTime, + @Param("endTime") LocalDateTime endTime, + @Param("startTimeMillis") Long startTimeMillis, + @Param("endTimeMillis") Long endTimeMillis, + @Param("sysOrigin") String sysOrigin, + @Param("storageTimezoneOffset") String storageTimezoneOffset, + @Param("statTimezoneOffset") String statTimezoneOffset); + + List listLuckyBoxGameMetricAmounts( + @Param("periodType") String periodType, + @Param("startTime") LocalDateTime startTime, + @Param("endTime") LocalDateTime endTime, + @Param("sysOrigin") String sysOrigin, + @Param("storageTimezoneOffset") String storageTimezoneOffset, + @Param("statTimezoneOffset") String statTimezoneOffset); + + int deleteGamePeriodUserMetrics( + @Param("periodType") String periodType, + @Param("periodKey") String periodKey, + @Param("statTimezone") String statTimezone, + @Param("sysOrigin") String sysOrigin); + + int insertWalletGamePeriodUserMetrics( + @Param("periodType") String periodType, + @Param("periodKey") String periodKey, + @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 insertLuckyBoxGamePeriodUserMetrics( + @Param("periodType") String periodType, + @Param("periodKey") String periodKey, + @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); + + int insertAllGamePeriodUserMetricsFromDaily( + @Param("statTimezone") String statTimezone, + @Param("sysOrigin") String sysOrigin); + + List listGamePeriodUserMetricCounts( + @Param("periodType") String periodType, + @Param("periodKey") String periodKey, + @Param("statTimezone") String statTimezone, + @Param("sysOrigin") String sysOrigin); + + List listAllGamePeriodAmountsFromPeriodDays( + @Param("statTimezone") String statTimezone, + @Param("sysOrigin") String sysOrigin); + + int deleteGamePeriodMetrics( + @Param("periodType") String periodType, + @Param("periodKey") String periodKey, + @Param("statTimezone") String statTimezone, + @Param("sysOrigin") String sysOrigin); + + int upsertGamePeriodMetrics(@Param("metrics") List metrics); + + Long countGamePeriodMetrics( + @Param("periodType") String periodType, + @Param("startTime") LocalDateTime startTime, + @Param("endTime") LocalDateTime endTime, + @Param("periodKey") String periodKey, + @Param("statTimezone") String statTimezone, + @Param("countryKeyword") String countryKeyword, + @Param("countryCodes") String countryCodes, + @Param("gameProvider") String gameProvider, + @Param("gameKeyword") String gameKeyword, + @Param("sysOrigin") String sysOrigin); + + List listGamePeriodMetrics( + @Param("periodType") String periodType, + @Param("startTime") LocalDateTime startTime, + @Param("endTime") LocalDateTime endTime, + @Param("periodKey") String periodKey, + @Param("statTimezone") String statTimezone, + @Param("countryKeyword") String countryKeyword, + @Param("countryCodes") String countryCodes, + @Param("gameProvider") String gameProvider, + @Param("gameKeyword") String gameKeyword, + @Param("sysOrigin") String sysOrigin, + @Param("sortColumn") String sortColumn, + @Param("sortDirection") String sortDirection, + @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/CountryDashboardGameMetricDTO.java b/rc-service/rc-service-console/console-infrastructure/src/main/java/com/red/circle/console/infra/database/rds/dto/datav/CountryDashboardGameMetricDTO.java new file mode 100644 index 00000000..feed6369 --- /dev/null +++ b/rc-service/rc-service-console/console-infrastructure/src/main/java/com/red/circle/console/infra/database/rds/dto/datav/CountryDashboardGameMetricDTO.java @@ -0,0 +1,51 @@ +package com.red.circle.console.infra.database.rds.dto.datav; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.time.LocalDateTime; +import lombok.Data; +import lombok.experimental.Accessors; + +/** + * 国家数据大屏游戏维度聚合结果. + */ +@Data +@Accessors(chain = true) +public class CountryDashboardGameMetricDTO { + + private String periodType; + + private String periodKey; + + private String statTimezone; + + private String periodName; + + private LocalDate periodStartDate; + + private LocalDate periodEndDate; + + private String sysOrigin; + + private String countryCode; + + private String countryName; + + private String gameProvider; + + private String gameId; + + private String gameName; + + private BigDecimal consumeAmount; + + private BigDecimal payoutAmount; + + private BigDecimal profitAmount; + + private Long userCount; + + private Long orderCount; + + private LocalDateTime refreshedAt; +} 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 2dc02e32..53b8c390 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 @@ -91,6 +91,61 @@ ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='国家数据大屏周期用户去重明细' + + CREATE TABLE IF NOT EXISTS `country_dashboard_game_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 '周期结束日期,开区间', + `sys_origin` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '系统来源', + `country_code` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'UNKNOWN' COMMENT '国家码', + `country_name` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'UNKNOWN' COMMENT '国家名称', + `game_provider` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'UNKNOWN' COMMENT '游戏厂商', + `game_id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'UNKNOWN' COMMENT '游戏ID', + `game_name` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'UNKNOWN' COMMENT '游戏名称', + `consume_amount` decimal(20,2) NOT NULL DEFAULT 0.00 COMMENT '用户消耗', + `payout_amount` decimal(20,2) NOT NULL DEFAULT 0.00 COMMENT '用户返奖', + `profit_amount` decimal(20,2) NOT NULL DEFAULT 0.00 COMMENT '平台收益', + `user_count` bigint NOT NULL DEFAULT 0 COMMENT '去重游戏用户数', + `order_count` bigint NOT NULL DEFAULT 0 COMMENT '流水笔数', + `refreshed_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '最近刷新时间', + `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_game_period` (`sys_origin`, `stat_timezone`, `period_type`, `period_key`, `country_code`, `game_provider`, `game_id`), + KEY `idx_game_period_query` (`sys_origin`, `stat_timezone`, `period_type`, `period_key`, `country_code`), + KEY `idx_game_period_date` (`sys_origin`, `stat_timezone`, `period_type`, `period_start_date`, `period_end_date`), + KEY `idx_game_lookup` (`sys_origin`, `stat_timezone`, `game_provider`, `game_id`, `period_type`, `period_key`), + KEY `idx_game_period_sort` (`sys_origin`, `stat_timezone`, `period_type`, `period_key`, `consume_amount`) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='国家数据大屏游戏维度周期预聚合' + + + + CREATE TABLE IF NOT EXISTS `country_dashboard_game_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 '周期结束日期,开区间', + `sys_origin` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '系统来源', + `country_code` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'UNKNOWN' COMMENT '国家码', + `country_name` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'UNKNOWN' COMMENT '国家名称', + `game_provider` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'UNKNOWN' COMMENT '游戏厂商', + `game_id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'UNKNOWN' COMMENT '游戏ID', + `user_id` bigint NOT NULL COMMENT '用户ID', + `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + PRIMARY KEY (`id`), + UNIQUE KEY `uk_game_period_user` (`period_type`, `period_key`, `stat_timezone`, `sys_origin`, `country_code`, `game_provider`, `game_id`, `user_id`), + KEY `idx_game_user_count` (`period_type`, `period_key`, `stat_timezone`, `sys_origin`, `game_provider`, `game_id`, `country_code`), + KEY `idx_game_user_lookup` (`sys_origin`, `stat_timezone`, `user_id`, `game_provider`, `game_id`) + ) 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(); @@ -519,51 +574,51 @@ - SELECT user_id, sys_origin, type, penny_amount, event_type, FROM_UNIXTIME(create_time / 1000) AS create_time + SELECT user_id, sys_origin, type, penny_amount, event_type, event_id, FROM_UNIXTIME(create_time / 1000) AS create_time FROM likei_wallet.wallet_gold_asset_record_1 UNION ALL - SELECT user_id, sys_origin, type, penny_amount, event_type, FROM_UNIXTIME(create_time / 1000) AS create_time + SELECT user_id, sys_origin, type, penny_amount, event_type, event_id, FROM_UNIXTIME(create_time / 1000) AS create_time FROM likei_wallet.wallet_gold_asset_record_2 UNION ALL - SELECT user_id, sys_origin, type, penny_amount, event_type, FROM_UNIXTIME(create_time / 1000) AS create_time + SELECT user_id, sys_origin, type, penny_amount, event_type, event_id, FROM_UNIXTIME(create_time / 1000) AS create_time FROM likei_wallet.wallet_gold_asset_record_3 UNION ALL - SELECT user_id, sys_origin, type, penny_amount, event_type, FROM_UNIXTIME(create_time / 1000) AS create_time + SELECT user_id, sys_origin, type, penny_amount, event_type, event_id, FROM_UNIXTIME(create_time / 1000) AS create_time FROM likei_wallet.wallet_gold_asset_record_4 UNION ALL - SELECT user_id, sys_origin, type, penny_amount, event_type, FROM_UNIXTIME(create_time / 1000) AS create_time + SELECT user_id, sys_origin, type, penny_amount, event_type, event_id, FROM_UNIXTIME(create_time / 1000) AS create_time FROM likei_wallet.wallet_gold_asset_record_5 UNION ALL - SELECT user_id, sys_origin, type, penny_amount, event_type, FROM_UNIXTIME(create_time / 1000) AS create_time + SELECT user_id, sys_origin, type, penny_amount, event_type, event_id, FROM_UNIXTIME(create_time / 1000) AS create_time FROM likei_wallet.wallet_gold_asset_record_6 UNION ALL - SELECT user_id, sys_origin, type, penny_amount, event_type, FROM_UNIXTIME(create_time / 1000) AS create_time + SELECT user_id, sys_origin, type, penny_amount, event_type, event_id, FROM_UNIXTIME(create_time / 1000) AS create_time FROM likei_wallet.wallet_gold_asset_record_7 UNION ALL - SELECT user_id, sys_origin, type, penny_amount, event_type, FROM_UNIXTIME(create_time / 1000) AS create_time + SELECT user_id, sys_origin, type, penny_amount, event_type, event_id, FROM_UNIXTIME(create_time / 1000) AS create_time FROM likei_wallet.wallet_gold_asset_record_8 UNION ALL - SELECT user_id, sys_origin, type, penny_amount, event_type, FROM_UNIXTIME(create_time / 1000) AS create_time + SELECT user_id, sys_origin, type, penny_amount, event_type, event_id, FROM_UNIXTIME(create_time / 1000) AS create_time FROM likei_wallet.wallet_gold_asset_record_9 UNION ALL - SELECT user_id, sys_origin, type, penny_amount, event_type, FROM_UNIXTIME(create_time / 1000) AS create_time + SELECT user_id, sys_origin, type, penny_amount, event_type, event_id, FROM_UNIXTIME(create_time / 1000) AS create_time FROM likei_wallet.wallet_gold_asset_record_10 UNION ALL - SELECT user_id, sys_origin, type, penny_amount, event_type, FROM_UNIXTIME(create_time / 1000) AS create_time + SELECT user_id, sys_origin, type, penny_amount, event_type, event_id, FROM_UNIXTIME(create_time / 1000) AS create_time FROM likei_wallet.wallet_gold_asset_record_11 UNION ALL - SELECT user_id, sys_origin, type, penny_amount, event_type, FROM_UNIXTIME(create_time / 1000) AS create_time + SELECT user_id, sys_origin, type, penny_amount, event_type, event_id, FROM_UNIXTIME(create_time / 1000) AS create_time FROM likei_wallet.wallet_gold_asset_record_12 @@ -596,6 +651,7 @@ 'LOTTERY_REWARD' ) OR t.event_type LIKE 'BAISHUN_GAME%' + OR t.event_type LIKE 'GAME_OPEN_%' OR t.event_type LIKE 'YOMI_GAME%' OR t.event_type LIKE 'HKYS_GAME%' OR t.event_type LIKE 'HOT_GAME%' @@ -605,6 +661,131 @@ AND t.event_type NOT LIKE 'ACCEPT_GIFT%' + + CASE + WHEN bo.vendor_game_id IS NOT NULL THEN 'BAISHUN' + WHEN go.game_id IS NOT NULL THEN 'GAME_OPEN' + WHEN t.event_type LIKE 'BAISHUN_GAME%' THEN 'BAISHUN' + WHEN t.event_type LIKE 'GAME_OPEN_%' THEN 'GAME_OPEN' + WHEN t.event_type LIKE 'YOMI_GAME%' THEN 'YOMI' + WHEN t.event_type LIKE 'HKYS_GAME%' THEN 'HKYS' + WHEN t.event_type LIKE 'HOT_GAME%' THEN 'HOT' + ELSE 'INTERNAL' + END + + + + CASE + WHEN bo.vendor_game_id IS NOT NULL THEN CAST(bo.vendor_game_id AS CHAR) + WHEN go.game_id IS NOT NULL THEN go.game_id + WHEN t.event_type LIKE 'BAISHUN_GAME_%' THEN COALESCE(NULLIF(SUBSTRING(t.event_type, 14), ''), t.event_type) + WHEN t.event_type LIKE 'GAME_OPEN_%' THEN COALESCE(NULLIF(SUBSTRING(t.event_type, 11), ''), t.event_type) + WHEN t.event_type LIKE 'YOMI_GAME_%' THEN COALESCE(NULLIF(SUBSTRING(t.event_type, 11), ''), t.event_type) + WHEN t.event_type LIKE 'HKYS_GAME_%' THEN COALESCE(NULLIF(SUBSTRING(t.event_type, 11), ''), t.event_type) + WHEN t.event_type LIKE 'HOT_GAME_%' THEN COALESCE(NULLIF(SUBSTRING(t.event_type, 10), ''), t.event_type) + WHEN t.event_type IN ('FRUIT_GAME', 'DOUBLE_LAYER_FRUIT', 'GAME_FRUIT_BET', 'GAME_FRUIT_AWARD') THEN 'FRUIT_GAME' + WHEN t.event_type IN ('LUDO_GAME', 'LUDO_VICTORY', 'LUDO_REFUND', 'GAME_LUDO_REFUND') THEN 'LUDO_GAME' + WHEN t.event_type IN ('GAME_BARBECUE', 'DOUBLE_LAYER_BARBECUE') THEN 'BARBECUE_GAME' + WHEN t.event_type IN ('GAME_SLOT_MACHINE') THEN 'SLOT_MACHINE' + WHEN t.event_type IN ('TURNTABLE_GAME') THEN 'TURNTABLE_GAME' + WHEN t.event_type IN ('GAME_BURST_CRYSTAL') THEN 'BURST_CRYSTAL' + WHEN t.event_type IN ('TEEN_PATTI', 'BET_TEEN_PATTI') THEN 'TEEN_PATTI' + WHEN t.event_type IN ('LOTTERY_NUMBER_BET', 'LOTTERY_REWARD') THEN 'LOTTERY_NUMBER' + ELSE t.event_type + END + + + + CASE + WHEN t.event_type IN ('FRUIT_GAME', 'DOUBLE_LAYER_FRUIT', 'GAME_FRUIT_BET', 'GAME_FRUIT_AWARD') THEN 'Fruit Game' + WHEN t.event_type IN ('LUDO_GAME', 'LUDO_VICTORY', 'LUDO_REFUND', 'GAME_LUDO_REFUND') THEN 'Ludo Game' + WHEN t.event_type IN ('GAME_BARBECUE', 'DOUBLE_LAYER_BARBECUE') THEN 'Barbecue Game' + WHEN t.event_type = 'GAME_SLOT_MACHINE' THEN 'Slot Machine' + WHEN t.event_type = 'TURNTABLE_GAME' THEN 'Turntable Game' + WHEN t.event_type = 'GAME_BURST_CRYSTAL' THEN 'Burst Crystal' + WHEN t.event_type IN ('TEEN_PATTI', 'BET_TEEN_PATTI') THEN 'Teen Patti' + WHEN t.event_type IN ('LOTTERY_NUMBER_BET', 'LOTTERY_REWARD') THEN 'Lottery Number' + ELSE t.event_type + END + + + + LEFT JOIN baishun_order_idempotency bo + ON bo.sys_origin = t.sys_origin COLLATE utf8mb4_0900_ai_ci + AND bo.wallet_event_id = t.event_id COLLATE utf8mb4_0900_ai_ci + LEFT JOIN game_open_callback_log go + ON go.sys_origin = t.sys_origin COLLATE utf8mb4_0900_ai_ci + AND t.event_id COLLATE utf8mb4_0900_ai_ci = CONCAT('GAME_OPEN:', go.order_id) + LEFT JOIN baishun_game_catalog bc + ON (bo.vendor_game_id IS NOT NULL OR t.event_type LIKE 'BAISHUN_GAME_%') + AND bc.sys_origin = t.sys_origin COLLATE utf8mb4_0900_ai_ci + AND bc.profile = 'PROD' + AND bc.vendor_game_id = CAST( AS UNSIGNED) + LEFT JOIN lingxian_game_catalog lc + ON ( + go.game_id IS NOT NULL + OR + t.event_type LIKE 'GAME_OPEN_%' + OR t.event_type LIKE 'YOMI_GAME_%' + OR t.event_type LIKE 'HKYS_GAME_%' + OR t.event_type LIKE 'HOT_GAME_%' + ) + AND lc.sys_origin = t.sys_origin COLLATE utf8mb4_0900_ai_ci + AND lc.profile = 'PROD' + AND lc.vendor_game_id = COLLATE utf8mb4_0900_ai_ci + LEFT JOIN sys_game_list_config sg + ON sg.sys_origin = t.sys_origin COLLATE utf8mb4_0900_ai_ci + AND sg.game_id = COLLATE utf8mb4_0900_ai_ci + + + + COALESCE( + NULLIF(bc.name, ''), + NULLIF(lc.name, ''), + NULLIF(sg.name, ''), + + ) + + + + WHERE t.period_type = #{periodType} + AND t.stat_timezone = #{statTimezone} + + + AND t.period_key = 'ALL' + + + + AND t.period_start_date >= DATE(#{startTime}) + + + AND t.period_start_date < DATE(#{endTime}) + + + + + AND ( + t.country_code = #{countryKeyword} + OR t.country_name LIKE CONCAT('%', #{countryKeyword}, '%') + ) + + + AND FIND_IN_SET(t.country_code, #{countryCodes}) + + + AND t.game_provider = #{gameProvider} + + + AND ( + t.game_id = #{gameKeyword} + OR t.game_name LIKE CONCAT('%', #{gameKeyword}, '%') + ) + + + AND t.sys_origin = #{sysOrigin} + + + + + + + + + DELETE FROM country_dashboard_game_period_user_metric + WHERE period_type = #{periodType} + AND period_key = #{periodKey} + AND stat_timezone = #{statTimezone} + + AND sys_origin = #{sysOrigin} + + + + + INSERT IGNORE INTO country_dashboard_game_period_user_metric ( + period_type, + period_key, + stat_timezone, + period_name, + period_start_date, + period_end_date, + sys_origin, + country_code, + country_name, + game_provider, + game_id, + user_id + ) + SELECT DISTINCT + #{periodType}, + #{periodKey}, + #{statTimezone}, + #{periodName}, + #{periodStartDate}, + #{periodEndDate}, + #{sysOrigin}, + IFNULL(NULLIF(u.country_code, ''), 'UNKNOWN') AS countryCode, + IFNULL(NULLIF(u.country_name, ''), IFNULL(NULLIF(u.country_code, ''), 'UNKNOWN')) AS countryName, + AS gameProvider, + AS gameId, + t.user_id AS userId + FROM ( + + ) t + INNER JOIN user_base_info u ON u.id = t.user_id + + WHERE + AND (u.is_del = 0 OR u.is_del IS NULL) + + + + AND u.origin_sys = #{sysOrigin} + + + + + INSERT IGNORE INTO country_dashboard_game_period_user_metric ( + period_type, + period_key, + stat_timezone, + period_name, + period_start_date, + period_end_date, + sys_origin, + country_code, + country_name, + game_provider, + game_id, + user_id + ) + SELECT DISTINCT + #{periodType}, + #{periodKey}, + #{statTimezone}, + #{periodName}, + #{periodStartDate}, + #{periodEndDate}, + #{sysOrigin}, + IFNULL(NULLIF(u.country_code, ''), 'UNKNOWN') AS countryCode, + IFNULL(NULLIF(u.country_name, ''), IFNULL(NULLIF(u.country_code, ''), 'UNKNOWN')) AS countryName, + 'INTERNAL' AS gameProvider, + 'LUCKY_BOX' AS gameId, + t.user_id AS userId + FROM game_lucky_box_count t + INNER JOIN user_base_info u ON u.id = t.user_id + WHERE 1 = 1 + AND (u.is_del = 0 OR u.is_del IS NULL) + + + + AND u.origin_sys = #{sysOrigin} + + + + + INSERT IGNORE INTO country_dashboard_game_period_user_metric ( + period_type, + period_key, + stat_timezone, + period_name, + period_start_date, + period_end_date, + sys_origin, + country_code, + country_name, + game_provider, + game_id, + user_id + ) + SELECT DISTINCT + 'ALL', + 'ALL', + #{statTimezone}, + '全部', + NULL, + NULL, + t.sys_origin, + t.country_code, + t.country_name, + t.game_provider, + t.game_id, + t.user_id + FROM country_dashboard_game_period_user_metric t + WHERE t.period_type = 'DAY' + AND t.stat_timezone = #{statTimezone} + + AND t.sys_origin = #{sysOrigin} + + + + + + + + + DELETE FROM country_dashboard_game_period_metric + WHERE period_type = #{periodType} + AND period_key = #{periodKey} + AND stat_timezone = #{statTimezone} + + AND sys_origin = #{sysOrigin} + + + + + INSERT INTO country_dashboard_game_period_metric ( + period_type, + period_key, + stat_timezone, + period_name, + period_start_date, + period_end_date, + sys_origin, + country_code, + country_name, + game_provider, + game_id, + game_name, + consume_amount, + payout_amount, + profit_amount, + user_count, + order_count, + refreshed_at + ) + VALUES + + ( + #{metric.periodType}, + #{metric.periodKey}, + #{metric.statTimezone}, + #{metric.periodName}, + #{metric.periodStartDate}, + #{metric.periodEndDate}, + #{metric.sysOrigin}, + #{metric.countryCode}, + #{metric.countryName}, + #{metric.gameProvider}, + #{metric.gameId}, + #{metric.gameName}, + #{metric.consumeAmount}, + #{metric.payoutAmount}, + #{metric.profitAmount}, + #{metric.userCount}, + #{metric.orderCount}, + NOW() + ) + + ON DUPLICATE KEY UPDATE + period_name = VALUES(period_name), + period_start_date = VALUES(period_start_date), + period_end_date = VALUES(period_end_date), + country_name = VALUES(country_name), + game_name = VALUES(game_name), + consume_amount = VALUES(consume_amount), + payout_amount = VALUES(payout_amount), + profit_amount = VALUES(profit_amount), + user_count = VALUES(user_count), + order_count = VALUES(order_count), + refreshed_at = VALUES(refreshed_at) + + + + + +