This commit is contained in:
hy001 2026-04-24 14:05:02 +08:00
parent 28ae34e2fa
commit b69c58bddf
15 changed files with 1024 additions and 10 deletions

View File

@ -1,7 +1,10 @@
package com.red.circle.console.adapter.app.user;
import com.red.circle.common.business.core.enums.SysOriginPlatformEnum;
import com.red.circle.console.app.service.app.user.DatavService;
import com.red.circle.console.app.dto.clientobject.datav.CountryDashboardCO;
import com.red.circle.console.app.dto.cmd.datav.CountryDashboardQryCmd;
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.web.controller.BaseController;
import com.red.circle.other.inner.model.cmd.user.OnlineCountQryCmd;
import com.red.circle.other.inner.model.dto.user.ActiveUserCountryCodeDTO;
@ -24,7 +27,8 @@ import org.springframework.web.bind.annotation.RestController;
@RequestMapping(value = "/datav", produces = MediaType.APPLICATION_JSON_VALUE)
public class DatavRestController extends BaseController {
private final DatavService userExpandService;
private final DatavService userExpandService;
private final CountryDashboardService countryDashboardService;
@GetMapping("/active/user-country-code")
public List<ActiveUserCountryCodeDTO> latestActiveUserCountryCode() {
@ -37,8 +41,13 @@ public class DatavRestController extends BaseController {
}
@GetMapping("/online/room/count")
public String onlineRoomCount(@RequestParam("sysOrigin") String sysOrigin) {
return Objects.toString(userExpandService.count(sysOrigin));
}
}
public String onlineRoomCount(@RequestParam("sysOrigin") String sysOrigin) {
return Objects.toString(userExpandService.count(sysOrigin));
}
@GetMapping("/country-dashboard")
public CountryDashboardCO countryDashboard(CountryDashboardQryCmd cmd) {
return countryDashboardService.query(cmd);
}
}

View File

@ -0,0 +1,454 @@
package com.red.circle.console.app.service.app.datav;
import com.red.circle.console.app.dto.clientobject.datav.CountryDashboardCO;
import com.red.circle.console.app.dto.clientobject.datav.CountryDashboardMetricCO;
import com.red.circle.console.app.dto.cmd.datav.CountryDashboardQryCmd;
import com.red.circle.console.infra.database.rds.dao.datav.CountryDashboardDAO;
import com.red.circle.console.infra.database.rds.dto.datav.CountryDashboardMetricDTO;
import com.red.circle.console.infra.database.rds.dto.datav.CountryDashboardUserCountryDTO;
import java.math.BigDecimal;
import java.sql.Timestamp;
import java.time.DayOfWeek;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.YearMonth;
import java.time.format.DateTimeFormatter;
import java.time.temporal.WeekFields;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Comparator;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.function.BiConsumer;
import lombok.RequiredArgsConstructor;
import org.bson.Document;
import org.bson.types.Decimal128;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.core.aggregation.Aggregation;
import org.springframework.data.mongodb.core.aggregation.AggregationOperation;
import org.springframework.data.mongodb.core.query.Criteria;
import org.springframework.stereotype.Service;
/**
* 国家数据大屏.
*/
@Service
@RequiredArgsConstructor
public class CountryDashboardServiceImpl implements CountryDashboardService {
private static final String COLLECTION_GIFT_GIVE_RUNNING_WATER = "gift_give_running_water";
private static final String PERIOD_ALL = "ALL";
private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd");
private static final DateTimeFormatter MONTH_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM");
private static final int USER_COUNTRY_BATCH_SIZE = 1000;
private final CountryDashboardDAO countryDashboardDAO;
private final MongoTemplate mongoTemplate;
@Override
public CountryDashboardCO query(CountryDashboardQryCmd cmd) {
QueryCondition condition = QueryCondition.of(cmd);
Map<String, CountryDashboardMetricCO> rows = new LinkedHashMap<>();
mergeSql(rows, countryDashboardDAO.listNewUsers(
condition.periodType, condition.startTime, condition.endTime,
condition.countryKeyword, condition.sysOrigin));
mergeSql(rows, countryDashboardDAO.listNewUserRecharge(
condition.periodType, condition.startTime, condition.endTime,
condition.countryKeyword, condition.sysOrigin));
mergeSql(rows, countryDashboardDAO.listOfficialRecharge(
condition.periodType, condition.startTime, condition.endTime,
condition.countryKeyword, condition.sysOrigin));
mergeSql(rows, countryDashboardDAO.listDealerRecharge(
condition.periodType, condition.startTime, condition.endTime,
condition.countryKeyword, condition.sysOrigin));
mergeSql(rows, countryDashboardDAO.listSalaryExchange(
condition.periodType, condition.startTime, condition.endTime,
condition.countryKeyword, condition.sysOrigin));
mergeSql(rows, countryDashboardDAO.listLuckyGift(
condition.periodType, condition.startTime, condition.endTime,
condition.countryKeyword, condition.sysOrigin));
mergeSql(rows, countryDashboardDAO.listWalletGame(
condition.periodType, condition.startTime, condition.endTime,
condition.countryKeyword, condition.sysOrigin));
mergeSql(rows, countryDashboardDAO.listLuckyBoxGame(
condition.periodType, condition.startTime, condition.endTime,
condition.countryKeyword, condition.sysOrigin));
mergeMongo(rows, condition, listGiftConsume(condition),
(row, amount) -> row.setGiftConsume(add(row.getGiftConsume(), amount)));
mergeMongo(rows, condition, listLuckyGiftAnchorShare(condition),
(row, amount) -> row.setLuckyGiftAnchorShare(add(row.getLuckyGiftAnchorShare(), amount)));
List<CountryDashboardMetricCO> records = rows.values().stream()
.peek(this::calculateFormulaFields)
.sorted(Comparator.comparing(CountryDashboardMetricCO::getPeriodKey,
Comparator.nullsLast(Comparator.reverseOrder()))
.thenComparing(CountryDashboardMetricCO::getCountryCode,
Comparator.nullsLast(String::compareTo)))
.toList();
CountryDashboardMetricCO total = buildTotal(records, condition.periodType);
return new CountryDashboardCO()
.setPeriodType(condition.periodType)
.setStartDate(condition.startDate)
.setEndDate(condition.endDate)
.setComputedAt(LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")))
.setTotal(total)
.setRecords(records);
}
private void mergeSql(Map<String, CountryDashboardMetricCO> rows,
List<CountryDashboardMetricDTO> metrics) {
if (metrics == null || metrics.isEmpty()) {
return;
}
for (CountryDashboardMetricDTO metric : metrics) {
CountryDashboardMetricCO row = getRow(rows, metric.getCountryCode(), metric.getCountryName(),
metric.getPeriodKey(), metric.getPeriodName());
row.setCountryNewUser(row.getCountryNewUser() + safeLong(metric.getCountryNewUser()));
row.setNewUserRecharge(add(row.getNewUserRecharge(), metric.getNewUserRecharge()));
row.setOfficialRecharge(add(row.getOfficialRecharge(), metric.getOfficialRecharge()));
row.setDealerRecharge(add(row.getDealerRecharge(), metric.getDealerRecharge()));
row.setSalaryExchange(add(row.getSalaryExchange(), metric.getSalaryExchange()));
row.setLuckyGiftTotalFlow(add(row.getLuckyGiftTotalFlow(), metric.getLuckyGiftTotalFlow()));
row.setLuckyGiftPayout(add(row.getLuckyGiftPayout(), metric.getLuckyGiftPayout()));
row.setGameTotalFlow(add(row.getGameTotalFlow(), metric.getGameTotalFlow()));
row.setGamePayout(add(row.getGamePayout(), metric.getGamePayout()));
}
}
private void mergeMongo(Map<String, CountryDashboardMetricCO> rows, QueryCondition condition,
List<MongoMetric> metrics, BiConsumer<CountryDashboardMetricCO, BigDecimal> merger) {
if (metrics == null || metrics.isEmpty()) {
return;
}
Map<Long, CountryDashboardUserCountryDTO> countryMap = mapUserCountries(
metrics.stream().map(MongoMetric::userId).filter(Objects::nonNull).distinct().toList());
for (MongoMetric metric : metrics) {
CountryDashboardUserCountryDTO country = countryMap.get(metric.userId());
if (country == null || !matchesCountry(country, condition.countryKeyword)) {
continue;
}
PeriodBucket bucket = condition.toPeriodBucket(metric.day());
CountryDashboardMetricCO row = getRow(rows, country.getCountryCode(), country.getCountryName(),
bucket.key(), bucket.name());
merger.accept(row, metric.amount());
}
}
private List<MongoMetric> listGiftConsume(QueryCondition condition) {
List<Criteria> criteria = baseGiftCriteria(condition);
criteria.add(Criteria.where("luckyGift").ne(Boolean.TRUE));
criteria.add(Criteria.where("originId").regex("^\\d+$"));
criteria.add(new Criteria().orOperator(
Criteria.where("dynamicContentId").exists(false),
Criteria.where("dynamicContentId").is(null),
Criteria.where("dynamicContentId").is("")
));
return aggregateGiftAmount(criteria, "$giftValue.actualAmount", false);
}
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);
}
private List<Criteria> baseGiftCriteria(QueryCondition condition) {
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.endTime != null) {
criteria.add(Criteria.where("createTime").lt(Timestamp.valueOf(condition.endTime)));
}
if (hasText(condition.sysOrigin)) {
criteria.add(Criteria.where("sysOrigin").is(condition.sysOrigin));
}
return criteria;
}
private List<MongoMetric> aggregateGiftAmount(List<Criteria> criteria, String amountPath,
boolean unwindAcceptUsers) {
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(Aggregation.group("userId", "day").sum("amount").as("amount"));
return mongoTemplate.aggregate(Aggregation.newAggregation(operations),
COLLECTION_GIFT_GIVE_RUNNING_WATER, Document.class)
.getMappedResults()
.stream()
.map(this::toMongoMetric)
.filter(Objects::nonNull)
.toList();
}
private AggregationOperation projectGiftDayAmount(String amountPath) {
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"))));
}
private MongoMetric toMongoMetric(Document document) {
Object id = document.get("_id");
if (!(id instanceof Document idDoc)) {
return null;
}
Long userId = toLong(idDoc.get("userId"));
String day = Objects.toString(idDoc.get("day"), null);
BigDecimal amount = toBigDecimal(document.get("amount"));
if (userId == null || !hasText(day) || amount == null) {
return null;
}
return new MongoMetric(userId, day, amount);
}
private Map<Long, CountryDashboardUserCountryDTO> mapUserCountries(Collection<Long> userIds) {
Map<Long, CountryDashboardUserCountryDTO> result = new HashMap<>();
if (userIds == null || userIds.isEmpty()) {
return result;
}
List<Long> ids = new ArrayList<>(userIds);
for (int start = 0; start < ids.size(); start += USER_COUNTRY_BATCH_SIZE) {
int end = Math.min(start + USER_COUNTRY_BATCH_SIZE, ids.size());
List<CountryDashboardUserCountryDTO> batch = countryDashboardDAO.listUserCountryByIds(
ids.subList(start, end));
if (batch == null) {
continue;
}
for (CountryDashboardUserCountryDTO item : batch) {
result.put(item.getUserId(), item);
}
}
return result;
}
private boolean matchesCountry(CountryDashboardUserCountryDTO country, String keyword) {
if (!hasText(keyword)) {
return true;
}
String countryCode = Objects.toString(country.getCountryCode(), "");
String countryName = Objects.toString(country.getCountryName(), "");
return countryCode.equals(keyword) || countryName.contains(keyword);
}
private CountryDashboardMetricCO getRow(Map<String, CountryDashboardMetricCO> rows,
String countryCode, String countryName, String periodKey, String periodName) {
String fixedCountryCode = hasText(countryCode) ? countryCode : "UNKNOWN";
String fixedCountryName = hasText(countryName) ? countryName : fixedCountryCode;
String fixedPeriodKey = hasText(periodKey) ? periodKey : PERIOD_ALL;
String fixedPeriodName = hasText(periodName) ? periodName : "全部";
return rows.computeIfAbsent(fixedCountryCode + "|" + fixedPeriodKey,
key -> new CountryDashboardMetricCO()
.setCountryCode(fixedCountryCode)
.setCountryName(fixedCountryName)
.setPeriodKey(fixedPeriodKey)
.setPeriodName(fixedPeriodName));
}
private void calculateFormulaFields(CountryDashboardMetricCO row) {
row.setTotalRecharge(add(row.getOfficialRecharge(), row.getDealerRecharge()));
row.setLuckyGiftProfit(safe(row.getLuckyGiftTotalFlow())
.subtract(safe(row.getLuckyGiftPayout()))
.subtract(safe(row.getLuckyGiftAnchorShare())));
row.setGameProfit(safe(row.getGameTotalFlow()).subtract(safe(row.getGamePayout())));
}
private CountryDashboardMetricCO buildTotal(List<CountryDashboardMetricCO> records,
String periodType) {
CountryDashboardMetricCO total = new CountryDashboardMetricCO()
.setCountryCode("ALL")
.setCountryName("全部国家")
.setPeriodKey(PERIOD_ALL.equals(periodType) ? PERIOD_ALL : "TOTAL")
.setPeriodName("合计");
for (CountryDashboardMetricCO row : records) {
total.setCountryNewUser(total.getCountryNewUser() + safeLong(row.getCountryNewUser()));
total.setNewUserRecharge(add(total.getNewUserRecharge(), row.getNewUserRecharge()));
total.setOfficialRecharge(add(total.getOfficialRecharge(), row.getOfficialRecharge()));
total.setDealerRecharge(add(total.getDealerRecharge(), row.getDealerRecharge()));
total.setSalaryExchange(add(total.getSalaryExchange(), row.getSalaryExchange()));
total.setGiftConsume(add(total.getGiftConsume(), row.getGiftConsume()));
total.setLuckyGiftTotalFlow(add(total.getLuckyGiftTotalFlow(), row.getLuckyGiftTotalFlow()));
total.setLuckyGiftPayout(add(total.getLuckyGiftPayout(), row.getLuckyGiftPayout()));
total.setLuckyGiftAnchorShare(add(total.getLuckyGiftAnchorShare(), row.getLuckyGiftAnchorShare()));
total.setGameTotalFlow(add(total.getGameTotalFlow(), row.getGameTotalFlow()));
total.setGamePayout(add(total.getGamePayout(), row.getGamePayout()));
}
calculateFormulaFields(total);
return total;
}
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 long safeLong(Long value) {
return value == null ? 0L : value;
}
private static BigDecimal toBigDecimal(Object value) {
if (value == null) {
return BigDecimal.ZERO;
}
if (value instanceof BigDecimal bigDecimal) {
return bigDecimal;
}
if (value instanceof Decimal128 decimal128) {
return decimal128.bigDecimalValue();
}
if (value instanceof Number number) {
return new BigDecimal(number.toString());
}
return new BigDecimal(Objects.toString(value, "0"));
}
private static Long toLong(Object value) {
if (value instanceof Number number) {
return number.longValue();
}
if (value == null) {
return null;
}
return Long.valueOf(Objects.toString(value));
}
private static boolean hasText(String value) {
return value != null && !value.trim().isEmpty();
}
private record MongoMetric(Long userId, String day, BigDecimal amount) {
}
private record PeriodBucket(String key, String name) {
}
private static final class QueryCondition {
private final String periodType;
private final String startDate;
private final String endDate;
private final LocalDateTime startTime;
private final LocalDateTime endTime;
private final String countryKeyword;
private final String sysOrigin;
private QueryCondition(String periodType, String startDate, String endDate,
LocalDateTime startTime, LocalDateTime endTime, String countryKeyword,
String sysOrigin) {
this.periodType = periodType;
this.startDate = startDate;
this.endDate = endDate;
this.startTime = startTime;
this.endTime = endTime;
this.countryKeyword = countryKeyword;
this.sysOrigin = sysOrigin;
}
private static QueryCondition of(CountryDashboardQryCmd cmd) {
String periodType = normalizePeriodType(cmd == null ? null : cmd.getPeriodType());
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();
switch (periodType) {
case "WEEK" -> {
start = today.with(DayOfWeek.MONDAY);
end = today.with(DayOfWeek.SUNDAY);
}
case "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.");
}
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(),
trimToNull(cmd == null ? null : cmd.getCountryKeyword()),
trimToNull(cmd == null ? null : cmd.getSysOrigin())
);
}
private PeriodBucket toPeriodBucket(String day) {
if (PERIOD_ALL.equals(periodType)) {
return new PeriodBucket(PERIOD_ALL, "全部");
}
LocalDate date = LocalDate.parse(day, DATE_FORMATTER);
if ("MONTH".equals(periodType)) {
return new PeriodBucket(date.format(MONTH_FORMATTER), date.format(MONTH_FORMATTER));
}
if ("WEEK".equals(periodType)) {
WeekFields weekFields = WeekFields.ISO;
int weekYear = date.get(weekFields.weekBasedYear());
int week = date.get(weekFields.weekOfWeekBasedYear());
String key = String.format(Locale.ROOT, "%d-W%02d", weekYear, week);
return new PeriodBucket(key, key);
}
return new PeriodBucket(day, day);
}
private static String normalizePeriodType(String periodType) {
String value = trimToNull(periodType);
if (value == null) {
return "DAY";
}
value = value.toUpperCase(Locale.ROOT);
return switch (value) {
case "DAY", "WEEK", "MONTH", "ALL" -> value;
default -> "DAY";
};
}
private static LocalDate parseDate(String value) {
String fixed = trimToNull(value);
return fixed == null ? null : LocalDate.parse(fixed, DATE_FORMATTER);
}
private static String trimToNull(String value) {
if (value == null) {
return null;
}
String trimmed = value.trim();
return trimmed.isEmpty() ? null : trimmed;
}
}
}

View File

@ -0,0 +1,30 @@
package com.red.circle.console.app.dto.clientobject.datav;
import java.io.Serial;
import java.io.Serializable;
import java.util.List;
import lombok.Data;
import lombok.experimental.Accessors;
/**
* 国家数据大屏结果.
*/
@Data
@Accessors(chain = true)
public class CountryDashboardCO implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
private String periodType;
private String startDate;
private String endDate;
private String computedAt;
private CountryDashboardMetricCO total;
private List<CountryDashboardMetricCO> records;
}

View File

@ -0,0 +1,57 @@
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 CountryDashboardMetricCO implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
private String countryCode;
private String countryName;
private String periodKey;
private String periodName;
private Long countryNewUser = 0L;
private BigDecimal newUserRecharge = BigDecimal.ZERO;
private BigDecimal dealerRecharge = BigDecimal.ZERO;
private BigDecimal salaryExchange = BigDecimal.ZERO;
private BigDecimal totalRecharge = BigDecimal.ZERO;
private BigDecimal giftConsume = BigDecimal.ZERO;
private BigDecimal luckyGiftTotalFlow = BigDecimal.ZERO;
private BigDecimal luckyGiftPayout = BigDecimal.ZERO;
private BigDecimal luckyGiftAnchorShare = BigDecimal.ZERO;
private BigDecimal luckyGiftProfit = BigDecimal.ZERO;
private BigDecimal gameTotalFlow = BigDecimal.ZERO;
private BigDecimal gamePayout = BigDecimal.ZERO;
private BigDecimal gameProfit = BigDecimal.ZERO;
/**
* 官方直充总额服务端用于计算总充值.
*/
private BigDecimal officialRecharge = BigDecimal.ZERO;
}

View File

@ -0,0 +1,42 @@
package com.red.circle.console.app.dto.cmd.datav;
import java.io.Serial;
import java.io.Serializable;
import lombok.Data;
import lombok.experimental.Accessors;
/**
* 国家数据大屏查询.
*/
@Data
@Accessors(chain = true)
public class CountryDashboardQryCmd implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* DAY / WEEK / MONTH / ALL.
*/
private String periodType;
/**
* yyyy-MM-dd.
*/
private String startDate;
/**
* yyyy-MM-dd.
*/
private String endDate;
/**
* 国家编码或国家名称关键字.
*/
private String countryKeyword;
/**
* 系统来源.
*/
private String sysOrigin;
}

View File

@ -0,0 +1,12 @@
package com.red.circle.console.app.service.app.datav;
import com.red.circle.console.app.dto.clientobject.datav.CountryDashboardCO;
import com.red.circle.console.app.dto.cmd.datav.CountryDashboardQryCmd;
/**
* 国家数据大屏.
*/
public interface CountryDashboardService {
CountryDashboardCO query(CountryDashboardQryCmd cmd);
}

View File

@ -0,0 +1,74 @@
package com.red.circle.console.infra.database.rds.dao.datav;
import com.red.circle.console.infra.database.rds.dto.datav.CountryDashboardMetricDTO;
import com.red.circle.console.infra.database.rds.dto.datav.CountryDashboardUserCountryDTO;
import com.red.circle.console.infra.database.rds.entity.app.IUserBaseInfo;
import com.red.circle.framework.mybatis.dao.BaseDAO;
import java.time.LocalDateTime;
import java.util.Collection;
import java.util.List;
import org.apache.ibatis.annotations.Param;
/**
* 国家数据大屏聚合.
*/
public interface CountryDashboardDAO extends BaseDAO<IUserBaseInfo> {
List<CountryDashboardMetricDTO> listNewUsers(
@Param("periodType") String periodType,
@Param("startTime") LocalDateTime startTime,
@Param("endTime") LocalDateTime endTime,
@Param("countryKeyword") String countryKeyword,
@Param("sysOrigin") String sysOrigin);
List<CountryDashboardMetricDTO> listNewUserRecharge(
@Param("periodType") String periodType,
@Param("startTime") LocalDateTime startTime,
@Param("endTime") LocalDateTime endTime,
@Param("countryKeyword") String countryKeyword,
@Param("sysOrigin") String sysOrigin);
List<CountryDashboardMetricDTO> listOfficialRecharge(
@Param("periodType") String periodType,
@Param("startTime") LocalDateTime startTime,
@Param("endTime") LocalDateTime endTime,
@Param("countryKeyword") String countryKeyword,
@Param("sysOrigin") String sysOrigin);
List<CountryDashboardMetricDTO> listDealerRecharge(
@Param("periodType") String periodType,
@Param("startTime") LocalDateTime startTime,
@Param("endTime") LocalDateTime endTime,
@Param("countryKeyword") String countryKeyword,
@Param("sysOrigin") String sysOrigin);
List<CountryDashboardMetricDTO> listSalaryExchange(
@Param("periodType") String periodType,
@Param("startTime") LocalDateTime startTime,
@Param("endTime") LocalDateTime endTime,
@Param("countryKeyword") String countryKeyword,
@Param("sysOrigin") String sysOrigin);
List<CountryDashboardMetricDTO> listLuckyGift(
@Param("periodType") String periodType,
@Param("startTime") LocalDateTime startTime,
@Param("endTime") LocalDateTime endTime,
@Param("countryKeyword") String countryKeyword,
@Param("sysOrigin") String sysOrigin);
List<CountryDashboardMetricDTO> listWalletGame(
@Param("periodType") String periodType,
@Param("startTime") LocalDateTime startTime,
@Param("endTime") LocalDateTime endTime,
@Param("countryKeyword") String countryKeyword,
@Param("sysOrigin") String sysOrigin);
List<CountryDashboardMetricDTO> listLuckyBoxGame(
@Param("periodType") String periodType,
@Param("startTime") LocalDateTime startTime,
@Param("endTime") LocalDateTime endTime,
@Param("countryKeyword") String countryKeyword,
@Param("sysOrigin") String sysOrigin);
List<CountryDashboardUserCountryDTO> listUserCountryByIds(@Param("userIds") Collection<Long> userIds);
}

View File

@ -0,0 +1,39 @@
package com.red.circle.console.infra.database.rds.dto.datav;
import java.math.BigDecimal;
import lombok.Data;
import lombok.experimental.Accessors;
/**
* 国家数据大屏 SQL 聚合结果.
*/
@Data
@Accessors(chain = true)
public class CountryDashboardMetricDTO {
private String countryCode;
private String countryName;
private String periodKey;
private String periodName;
private Long countryNewUser;
private BigDecimal newUserRecharge;
private BigDecimal officialRecharge;
private BigDecimal dealerRecharge;
private BigDecimal salaryExchange;
private BigDecimal luckyGiftTotalFlow;
private BigDecimal luckyGiftPayout;
private BigDecimal gameTotalFlow;
private BigDecimal gamePayout;
}

View File

@ -0,0 +1,18 @@
package com.red.circle.console.infra.database.rds.dto.datav;
import lombok.Data;
import lombok.experimental.Accessors;
/**
* 用户国家归属.
*/
@Data
@Accessors(chain = true)
public class CountryDashboardUserCountryDTO {
private Long userId;
private String countryCode;
private String countryName;
}

View File

@ -0,0 +1,266 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.red.circle.console.infra.database.rds.dao.datav.CountryDashboardDAO">
<sql id="CountryColumns">
NULLIF(u.country_code, '') AS countryCode,
NULLIF(u.country_name, '') AS countryName
</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>
<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>
<otherwise>'全部'</otherwise>
</choose>
AS periodName
</sql>
<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>
<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>
<otherwise>'全部'</otherwise>
</choose>
AS periodName
</sql>
<sql id="UserWhere">
AND (u.is_del = 0 OR u.is_del IS NULL)
<if test="countryKeyword != null and countryKeyword != ''">
AND (
u.country_code = #{countryKeyword}
OR u.country_name LIKE CONCAT('%', #{countryKeyword}, '%')
)
</if>
<if test="sysOrigin != null and sysOrigin != ''">
AND u.origin_sys = #{sysOrigin}
</if>
</sql>
<sql id="SourceTimeWhere">
<if test="startTime != null">
AND t.create_time &gt;= #{startTime}
</if>
<if test="endTime != null">
AND t.create_time &lt; #{endTime}
</if>
</sql>
<sql id="UserTimeWhere">
<if test="startTime != null">
AND u.create_time &gt;= #{startTime}
</if>
<if test="endTime != null">
AND u.create_time &lt; #{endTime}
</if>
</sql>
<sql id="SourceSysOriginWhere">
<if test="sysOrigin != null and sysOrigin != ''">
AND t.sys_origin = #{sysOrigin}
</if>
</sql>
<select id="listNewUsers"
resultType="com.red.circle.console.infra.database.rds.dto.datav.CountryDashboardMetricDTO">
SELECT
<include refid="CountryColumns"/>,
<include refid="PeriodColumnsU"/>,
COUNT(1) AS countryNewUser
FROM user_base_info u
WHERE 1 = 1
<include refid="UserWhere"/>
<include refid="UserTimeWhere"/>
GROUP BY countryCode, countryName, periodKey, periodName
</select>
<select id="listNewUserRecharge"
resultType="com.red.circle.console.infra.database.rds.dto.datav.CountryDashboardMetricDTO">
SELECT
<include refid="CountryColumns"/>,
<include refid="PeriodColumnsT"/>,
IFNULL(SUM(t.unit_price), 0) AS newUserRecharge
FROM order_purchase_history t
INNER JOIN user_base_info u ON u.id = t.user_id
WHERE t.evn = 'PROD'
AND t.is_trial_period = 0
AND t.status = 'COMPLETE'
<include refid="UserWhere"/>
<include refid="SourceTimeWhere"/>
<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>
</choose>
GROUP BY countryCode, countryName, periodKey, periodName
</select>
<select id="listOfficialRecharge"
resultType="com.red.circle.console.infra.database.rds.dto.datav.CountryDashboardMetricDTO">
SELECT
<include refid="CountryColumns"/>,
<include refid="PeriodColumnsT"/>,
IFNULL(SUM(t.unit_price), 0) AS officialRecharge
FROM order_purchase_history t
INNER JOIN user_base_info u ON u.id = t.user_id
WHERE t.evn = 'PROD'
AND t.is_trial_period = 0
AND t.status = 'COMPLETE'
<include refid="UserWhere"/>
<include refid="SourceTimeWhere"/>
<include refid="SourceSysOriginWhere"/>
GROUP BY countryCode, countryName, periodKey, periodName
</select>
<select id="listDealerRecharge"
resultType="com.red.circle.console.infra.database.rds.dto.datav.CountryDashboardMetricDTO">
SELECT
<include refid="CountryColumns"/>,
<include refid="PeriodColumnsT"/>,
IFNULL(SUM(t.recharge_amount), 0) AS dealerRecharge
FROM user_gold_coin_recharge t
INNER JOIN user_base_info u ON u.id = t.user_id
WHERE t.gold_coin_source IN (2, 3)
<include refid="UserWhere"/>
<include refid="SourceTimeWhere"/>
<include refid="SourceSysOriginWhere"/>
GROUP BY countryCode, countryName, periodKey, periodName
</select>
<select id="listSalaryExchange"
resultType="com.red.circle.console.infra.database.rds.dto.datav.CountryDashboardMetricDTO">
SELECT
<include refid="CountryColumns"/>,
<include refid="PeriodColumnsT"/>,
IFNULL(SUM(t.amount), 0) AS salaryExchange
FROM (
SELECT user_id, sys_origin, amount, create_time
FROM user_salary_account_running_water
WHERE type = 1 AND salary_event = 'SALARY_EXCHANGE'
UNION ALL
SELECT user_id, sys_origin, amount, create_time
FROM user_salary_diamond_running_water
WHERE type = 1 AND salary_event = 'SALARY_EXCHANGE_GOLD_COINS'
) t
INNER JOIN user_base_info u ON u.id = t.user_id
WHERE 1 = 1
<include refid="UserWhere"/>
<include refid="SourceTimeWhere"/>
<include refid="SourceSysOriginWhere"/>
GROUP BY countryCode, countryName, periodKey, periodName
</select>
<select id="listLuckyGift"
resultType="com.red.circle.console.infra.database.rds.dto.datav.CountryDashboardMetricDTO">
SELECT
<include refid="CountryColumns"/>,
<include refid="PeriodColumnsT"/>,
IFNULL(SUM(t.pay_amount), 0) AS luckyGiftTotalFlow,
IFNULL(SUM(t.award_amount), 0) AS luckyGiftPayout
FROM game_lucky_gift_count t
INNER JOIN user_base_info u ON u.id = t.user_id
WHERE 1 = 1
<include refid="UserWhere"/>
<include refid="SourceTimeWhere"/>
<include refid="SourceSysOriginWhere"/>
GROUP BY countryCode, countryName, periodKey, periodName
</select>
<select id="listWalletGame"
resultType="com.red.circle.console.infra.database.rds.dto.datav.CountryDashboardMetricDTO">
SELECT
<include refid="CountryColumns"/>,
<include refid="PeriodColumnsT"/>,
IFNULL(SUM(CASE WHEN t.type = 1 THEN t.penny_amount ELSE 0 END) / 100.00, 0) AS gameTotalFlow,
IFNULL(SUM(CASE WHEN t.type = 0 THEN t.penny_amount ELSE 0 END) / 100.00, 0) AS gamePayout
FROM wallet_gold_asset_record t
INNER JOIN user_base_info u ON u.id = t.user_id
WHERE (
t.event_type IN (
'GAME_SLOT_MACHINE',
'GAME_BARBECUE',
'TURNTABLE_GAME',
'LUDO_GAME',
'GAME_BURST_CRYSTAL',
'EGG',
'FRUIT_GAME',
'DOUBLE_LAYER_FRUIT',
'DOUBLE_LAYER_BARBECUE',
'GAME_FRUIT_BET',
'GAME_FRUIT_AWARD',
'GAME_BACK',
'GAME_WINNING',
'LUDO_VICTORY',
'LUDO_REFUND',
'GAME_LUDO_REFUND',
'USD_GAME_WIN',
'USD_GAME_REFUND',
'USD_GAME_REFUND_DRAW',
'TEEN_PATTI',
'BET_TEEN_PATTI',
'LOTTERY_NUMBER_BET',
'LOTTERY_REWARD'
)
OR t.event_type LIKE 'BAISHUN_GAME%'
OR t.event_type LIKE 'YOMI_GAME%'
OR t.event_type LIKE 'HKYS_GAME%'
OR t.event_type LIKE 'HOT_GAME%'
)
AND t.event_type NOT IN ('LUCKY_GIFT', 'LUCKY_GIFT_GOLD_REWARD', 'LUCKY_BOX_GAME', 'NEW_LUCKY_BOX_GAME', 'GAME_RAKE')
AND t.event_type NOT LIKE 'GIVE_GIFT%'
AND t.event_type NOT LIKE 'ACCEPT_GIFT%'
<include refid="UserWhere"/>
<include refid="SourceTimeWhere"/>
<include refid="SourceSysOriginWhere"/>
GROUP BY countryCode, countryName, periodKey, periodName
</select>
<select id="listLuckyBoxGame"
resultType="com.red.circle.console.infra.database.rds.dto.datav.CountryDashboardMetricDTO">
SELECT
<include refid="CountryColumns"/>,
<include refid="PeriodColumnsT"/>,
IFNULL(SUM(t.pay_amount), 0) AS gameTotalFlow,
IFNULL(SUM(t.gift_amount), 0) AS gamePayout
FROM game_lucky_box_count t
INNER JOIN user_base_info u ON u.id = t.user_id
WHERE 1 = 1
<include refid="UserWhere"/>
<include refid="SourceTimeWhere"/>
<include refid="SourceSysOriginWhere"/>
GROUP BY countryCode, countryName, periodKey, periodName
</select>
<select id="listUserCountryByIds"
resultType="com.red.circle.console.infra.database.rds.dto.datav.CountryDashboardUserCountryDTO">
SELECT
id AS userId,
NULLIF(country_code, '') AS countryCode,
NULLIF(country_name, '') AS countryName
FROM user_base_info
WHERE id IN
<foreach collection="userIds" item="userId" open="(" separator="," close=")">
#{userId}
</foreach>
</select>
</mapper>

View File

@ -72,4 +72,5 @@ red-circle:
- /console/datav/active/user-country-code
- /console/datav/online/user/count
- /console/datav/online/room/count
- /console/datav/country-dashboard
- /console/user/base/info/im/sig

View File

@ -12,6 +12,7 @@ public class BaishunRuntimeConfig {
String sysOrigin;
String platformBaseUrl;
String luckyGiftUrl;
String appId;
String appName;
String appChannel;

View File

@ -52,7 +52,9 @@ public class BaishunRuntimeConfigResolver {
ApiAccount resolved = new ApiAccount();
resolved.setStandardId(base.getStandardId());
resolved.setUrl(defaultIfBlank(resolveLuckyGiftApiUrl(config.getPlatformBaseUrl()), base.getUrl()));
resolved.setUrl(defaultIfBlank(
resolveLuckyGiftApiUrl(config.getLuckyGiftUrl(), config.getPlatformBaseUrl()),
base.getUrl()));
resolved.setAppId(defaultIfBlank(config.getAppId(), base.getAppId()));
resolved.setAppChannel(defaultIfBlank(config.getAppChannel(), base.getAppChannel()));
resolved.setAppKey(defaultIfBlank(config.getAppKey(), base.getAppKey()));
@ -72,6 +74,7 @@ public class BaishunRuntimeConfigResolver {
return base.toBuilder()
.sysOrigin(normalizeSysOrigin(defaultIfBlank(row.getSysOrigin(), base.getSysOrigin())))
.platformBaseUrl(defaultIfBlank(row.getPlatformBaseUrl(), base.getPlatformBaseUrl()))
.luckyGiftUrl(defaultIfBlank(row.getLuckyGiftUrl(), base.getLuckyGiftUrl()))
.appId(row.getAppId() != null && row.getAppId() > 0 ? String.valueOf(row.getAppId()) : base.getAppId())
.appName(defaultIfBlank(row.getAppName(), base.getAppName()))
.appChannel(defaultIfBlank(row.getAppChannel(), base.getAppChannel()))
@ -84,7 +87,11 @@ public class BaishunRuntimeConfigResolver {
.build();
}
private String resolveLuckyGiftApiUrl(String platformBaseUrl) {
private String resolveLuckyGiftApiUrl(String luckyGiftUrl, String platformBaseUrl) {
String configuredUrl = trim(luckyGiftUrl);
if (StringUtils.isNotBlank(configuredUrl)) {
return trimTrailingSlash(configuredUrl);
}
String baseUrl = trim(platformBaseUrl);
if (StringUtils.isBlank(baseUrl)) {
return null;

View File

@ -35,6 +35,7 @@ class BaishunRuntimeConfigResolverTest {
when(providerConfigService.getBySysOrigin("LIKEI")).thenReturn(new BaishunProviderConfig()
.setSysOrigin("LIKEI")
.setPlatformBaseUrl("https://provider.example.com/")
.setLuckyGiftUrl("https://lucky.example.com/lucky_gift/start_game")
.setAppId(4581045902L)
.setAppChannel("yumiparty")
.setAppKey("provider-key"));
@ -42,7 +43,7 @@ class BaishunRuntimeConfigResolverTest {
ApiAccount resolved = resolver.resolveLuckyGiftApiAccount("LIKEI", 99L);
assertEquals(99L, resolved.getStandardId());
assertEquals("https://provider.example.com/lucky_gift/start_game", resolved.getUrl());
assertEquals("https://lucky.example.com/lucky_gift/start_game", resolved.getUrl());
assertEquals("4581045902", resolved.getAppId());
assertEquals("yumiparty", resolved.getAppChannel());
assertEquals("provider-key", resolved.getAppKey());

View File

@ -37,6 +37,9 @@ public class BaishunProviderConfig extends TimestampBaseEntity {
@TableField("platform_base_url")
private String platformBaseUrl;
@TableField("lucky_gift_url")
private String luckyGiftUrl;
@TableField("app_id")
private Long appId;