Compare commits
35 Commits
3e4251ba3b
...
3ec362be15
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3ec362be15 | ||
|
|
2ad23b068d | ||
|
|
769af267ea | ||
|
|
b7ac08be5d | ||
|
|
9a893d9cc0 | ||
|
|
b2be53f398 | ||
|
|
f3ef729ea8 | ||
|
|
f60a850ec8 | ||
|
|
403cff15c0 | ||
|
|
f7a08cdf09 | ||
|
|
bbc4a267be | ||
|
|
04097e530e | ||
|
|
f79d460f10 | ||
|
|
83b61e94dd | ||
|
|
b1e68f00e8 | ||
|
|
70029bb511 | ||
|
|
f1e5e750bd | ||
|
|
7238e8eb46 | ||
|
|
c9ea77f762 | ||
|
|
0ace2cc68f | ||
|
|
41305adb9d | ||
|
|
f735b0a9a3 | ||
|
|
e0c4b72d4e | ||
|
|
93c39f77cf | ||
|
|
3a01a490ab | ||
|
|
1ac5e6c62d | ||
|
|
2f45f23bd7 | ||
|
|
354f11150f | ||
|
|
5d98ed071c | ||
|
|
3a0a03d226 | ||
|
|
8d9b3b05d3 | ||
|
|
c82e4c91f7 | ||
|
|
938d1522de | ||
|
|
10d4df14cf | ||
|
|
835b6d5367 |
@ -32,6 +32,10 @@ red-circle:
|
||||
bucket-name: ${LIKEI_TENCENT_COS_BUCKET}
|
||||
region: ${LIKEI_TENCENT_COS_REGION}
|
||||
access-url: ${LIKEI_TENCENT_COS_ACCESS_URL}
|
||||
connection-timeout-ms: ${LIKEI_TENCENT_COS_CONNECTION_TIMEOUT_MS:15000}
|
||||
connection-request-timeout-ms: ${LIKEI_TENCENT_COS_CONNECTION_REQUEST_TIMEOUT_MS:15000}
|
||||
shutdown-timeout-ms: ${LIKEI_TENCENT_COS_SHUTDOWN_TIMEOUT_MS:10000}
|
||||
max-error-retry: ${LIKEI_TENCENT_COS_MAX_ERROR_RETRY:0}
|
||||
instant-message:
|
||||
broadcast-group:
|
||||
LOTFUN: "${LIKEI_IM_BROADCAST_GROUP_LOTFUN}"
|
||||
@ -40,6 +44,7 @@ red-circle:
|
||||
secretId: ${LIKEI_TRTC_SECRET_ID}
|
||||
secretKey: ${LIKEI_TRTC_SECRET_KEY}
|
||||
endpoint: ${LIKEI_TRTC_ENDPOINT}
|
||||
region: ${LIKEI_TRTC_REGION:ap-singapore}
|
||||
sdkAppId: ${LIKEI_TRTC_SDK_APP_ID}
|
||||
tencet-im:
|
||||
appId: ${LIKEI_IM_APP_ID}
|
||||
|
||||
@ -7,4 +7,9 @@ task:
|
||||
base-url: ${TASK_CENTER_GO_URL:${LIKEI_GATEWAY_ROUTE_GO_URI:http://golang:2900}}
|
||||
internal-token: ${GAME_INTERNAL_CALLBACK_SECRET:}
|
||||
timeout-millis: ${TASK_CENTER_GO_TIMEOUT_MILLIS:3000}
|
||||
event:
|
||||
mq:
|
||||
enabled: ${TASK_CENTER_EVENT_MQ_ENABLED:false}
|
||||
topic: ${TASK_CENTER_EVENT_MQ_TOPIC:RC_DEFAULT_APP_ORDINARY}
|
||||
tag: ${TASK_CENTER_EVENT_MQ_TAG:task_center_event}
|
||||
|
||||
|
||||
@ -79,3 +79,8 @@ task:
|
||||
base-url: ${TASK_CENTER_GO_URL:${LIKEI_GATEWAY_ROUTE_GO_URI:http://golang:2900}}
|
||||
internal-token: ${GAME_INTERNAL_CALLBACK_SECRET:}
|
||||
timeout-millis: ${TASK_CENTER_GO_TIMEOUT_MILLIS:3000}
|
||||
event:
|
||||
mq:
|
||||
enabled: ${TASK_CENTER_EVENT_MQ_ENABLED:false}
|
||||
topic: ${TASK_CENTER_EVENT_MQ_TOPIC:RC_DEFAULT_APP_ORDINARY}
|
||||
tag: ${TASK_CENTER_EVENT_MQ_TAG:task_center_event}
|
||||
|
||||
@ -122,6 +122,11 @@ task:
|
||||
base-url: ${TASK_CENTER_GO_URL:${LIKEI_GATEWAY_ROUTE_GO_URI:http://golang:2900}}
|
||||
internal-token: ${GAME_INTERNAL_CALLBACK_SECRET:}
|
||||
timeout-millis: ${TASK_CENTER_GO_TIMEOUT_MILLIS:3000}
|
||||
event:
|
||||
mq:
|
||||
enabled: ${TASK_CENTER_EVENT_MQ_ENABLED:false}
|
||||
topic: ${TASK_CENTER_EVENT_MQ_TOPIC:RC_DEFAULT_APP_ORDINARY}
|
||||
tag: ${TASK_CENTER_EVENT_MQ_TAG:task_center_event}
|
||||
|
||||
voice-room:
|
||||
region-broadcast:
|
||||
|
||||
@ -10,3 +10,16 @@ invite:
|
||||
base-url: ${INVITE_CAMPAIGN_GO_URL:}
|
||||
internal-token: ${GAME_INTERNAL_CALLBACK_SECRET:}
|
||||
timeout-millis: ${INVITE_CAMPAIGN_GO_TIMEOUT_MILLIS:10000}
|
||||
|
||||
task:
|
||||
center:
|
||||
go:
|
||||
enabled: ${TASK_CENTER_GO_ENABLED:true}
|
||||
base-url: ${TASK_CENTER_GO_URL:${LIKEI_GATEWAY_ROUTE_GO_URI:http://golang:2900}}
|
||||
internal-token: ${GAME_INTERNAL_CALLBACK_SECRET:}
|
||||
timeout-millis: ${TASK_CENTER_GO_TIMEOUT_MILLIS:3000}
|
||||
event:
|
||||
mq:
|
||||
enabled: ${TASK_CENTER_EVENT_MQ_ENABLED:false}
|
||||
topic: ${TASK_CENTER_EVENT_MQ_TOPIC:RC_DEFAULT_APP_ORDINARY}
|
||||
tag: ${TASK_CENTER_EVENT_MQ_TAG:task_center_event}
|
||||
|
||||
@ -38,6 +38,12 @@ public class LoginAccessValidationService {
|
||||
private static final List<String> ALLOWED_CHINA_REGIONS = List.of(
|
||||
"香港", "澳门", "澳門", "台湾", "台灣", "Hong Kong", "Macau", "Macao", "Taiwan"
|
||||
);
|
||||
private static final List<String> BLOCKED_COUNTRY_CODES = List.of(
|
||||
"MO", "SG", "TW"
|
||||
);
|
||||
private static final List<String> BLOCKED_COUNTRY_NAMES = List.of(
|
||||
"澳门", "澳門", "台湾", "台灣", "Macau", "Macao", "Taiwan", "新加坡", "Singapore"
|
||||
);
|
||||
private static final List<IpGeoProvider> IP_GEO_PROVIDERS = List.of(
|
||||
new IpGeoProvider("ip.sb", "https://api.ip.sb/geoip/%s"),
|
||||
new IpGeoProvider("freeipapi", "https://freeipapi.com/api/json/%s"),
|
||||
@ -144,7 +150,7 @@ public class LoginAccessValidationService {
|
||||
}
|
||||
|
||||
private boolean isIpValidationFailed(String ip) {
|
||||
return isMainlandChina(loadIpData(ip));
|
||||
return isBlockedIpRegion(loadIpData(ip));
|
||||
}
|
||||
|
||||
private boolean isZoneValidationFailed(String reqZone) {
|
||||
@ -235,6 +241,10 @@ public class LoginAccessValidationService {
|
||||
};
|
||||
}
|
||||
|
||||
static boolean isBlockedIpRegion(JSONObject dataObject) {
|
||||
return isMainlandChina(dataObject) || isBlockedCountryOrRegion(dataObject);
|
||||
}
|
||||
|
||||
static boolean isMainlandChina(JSONObject dataObject) {
|
||||
if (dataObject == null) {
|
||||
return false;
|
||||
@ -251,6 +261,20 @@ public class LoginAccessValidationService {
|
||||
return StringUtils.isBlank(region) || !ALLOWED_CHINA_REGIONS.contains(region);
|
||||
}
|
||||
|
||||
private static boolean isBlockedCountryOrRegion(JSONObject dataObject) {
|
||||
if (dataObject == null) {
|
||||
return false;
|
||||
}
|
||||
String countryCode = dataObject.getString("country_id");
|
||||
if (StringUtils.isNotBlank(countryCode)
|
||||
&& BLOCKED_COUNTRY_CODES.contains(countryCode.trim().toUpperCase(Locale.ROOT))) {
|
||||
return true;
|
||||
}
|
||||
String country = dataObject.getString("country");
|
||||
String region = dataObject.getString("region");
|
||||
return BLOCKED_COUNTRY_NAMES.contains(country) || BLOCKED_COUNTRY_NAMES.contains(region);
|
||||
}
|
||||
|
||||
private static JSONObject normalizeIpNcGy(String provider, JSONObject jsonObject) {
|
||||
JSONObject countryObject = jsonObject.getJSONObject("country");
|
||||
if (countryObject == null) {
|
||||
|
||||
@ -70,11 +70,58 @@ public class LoginAccessValidationServiceTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void allowChinaSpecialRegions() {
|
||||
public void allowHongKongRegion() {
|
||||
JSONObject data = JSON.parseObject("""
|
||||
{"country_id":"CN","country":"中国","region":"香港"}
|
||||
""");
|
||||
|
||||
assertFalse(LoginAccessValidationService.isMainlandChina(data));
|
||||
assertFalse(LoginAccessValidationService.isBlockedIpRegion(data));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void allowHongKongCountryCode() {
|
||||
JSONObject data = JSON.parseObject("""
|
||||
{"country_id":"HK","country":"Hong Kong","region":"Kwai Tsing District"}
|
||||
""");
|
||||
|
||||
assertFalse(LoginAccessValidationService.isBlockedIpRegion(data));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void blockMacauCountryCode() {
|
||||
JSONObject data = JSON.parseObject("""
|
||||
{"country_id":"MO","country":"Macao","region":""}
|
||||
""");
|
||||
|
||||
assertTrue(LoginAccessValidationService.isBlockedIpRegion(data));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void blockSingaporeCountryCode() {
|
||||
JSONObject data = JSON.parseObject("""
|
||||
{"country_id":"SG","country":"Singapore","region":""}
|
||||
""");
|
||||
|
||||
assertTrue(LoginAccessValidationService.isBlockedIpRegion(data));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void blockTaiwanRegion() {
|
||||
JSONObject data = JSON.parseObject("""
|
||||
{"country_id":"CN","country":"中国","region":"台湾"}
|
||||
""");
|
||||
|
||||
assertFalse(LoginAccessValidationService.isMainlandChina(data));
|
||||
assertTrue(LoginAccessValidationService.isBlockedIpRegion(data));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void blockTaiwanCountryCode() {
|
||||
JSONObject data = JSON.parseObject("""
|
||||
{"country_id":"TW","country":"Taiwan","region":""}
|
||||
""");
|
||||
|
||||
assertTrue(LoginAccessValidationService.isBlockedIpRegion(data));
|
||||
}
|
||||
}
|
||||
|
||||
@ -37,4 +37,7 @@ public interface UserHistoryIdentityClientApi {
|
||||
|
||||
@GetMapping("/saveManager")
|
||||
ResultResponse<Void> saveManager(@RequestParam("userId") Long userId);
|
||||
|
||||
@GetMapping("/saveYumiManager")
|
||||
ResultResponse<Void> saveYumiManager(@RequestParam("userId") Long userId);
|
||||
}
|
||||
|
||||
@ -19,6 +19,11 @@ public enum ConsolePropsTypeEnum {
|
||||
*/
|
||||
RIDE("RIDE", "座驾"),
|
||||
|
||||
/**
|
||||
* 徽章.
|
||||
*/
|
||||
BADGE("BADGE", "徽章"),
|
||||
|
||||
/**
|
||||
* 贵族.
|
||||
*/
|
||||
|
||||
@ -40,8 +40,13 @@ public class UserHistoryIdentityDTO implements Serializable {
|
||||
private Boolean host;
|
||||
|
||||
/**
|
||||
* 是否做过团队经理.
|
||||
* 是否做过旧团队经理.
|
||||
*/
|
||||
private Boolean manager;
|
||||
|
||||
/**
|
||||
* 是否做过 Yumi 团队经理.
|
||||
*/
|
||||
private Boolean yumiManager;
|
||||
|
||||
}
|
||||
|
||||
@ -13,7 +13,17 @@ RUN apt-get update \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
WORKDIR /application
|
||||
COPY console-start/target/rc-service-console-*.jar /application/service.jar
|
||||
COPY console-start/target/rc-service-console-*.jar /tmp/service-jars/
|
||||
RUN set -e; \
|
||||
jars="$(find /tmp/service-jars -maxdepth 1 -type f -name 'rc-service-console-*.jar' | sort)"; \
|
||||
count="$(printf '%s\n' "$jars" | grep -c . || true)"; \
|
||||
if [ "$count" -ne 1 ]; then \
|
||||
echo "Expected exactly one rc-service-console jar, found $count:"; \
|
||||
ls -la /tmp/service-jars; \
|
||||
exit 1; \
|
||||
fi; \
|
||||
cp "$jars" /application/service.jar \
|
||||
&& rm -rf /tmp/service-jars
|
||||
|
||||
CMD exec java -Djava.security.egd=file:/dev/./urandom \
|
||||
-Dnacos.remote.client.grpc.timeout=6000 \
|
||||
|
||||
@ -2,12 +2,13 @@ package com.red.circle.console.adapter.app.party3rd;//package com.sugartime.app.
|
||||
|
||||
import com.red.circle.external.inner.endpoint.oss.OssServiceClient;
|
||||
import com.red.circle.framework.core.asserts.ResponseAssert;
|
||||
import java.io.InputStream;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.core.io.ByteArrayResource;
|
||||
import org.springframework.core.io.InputStreamResource;
|
||||
import org.springframework.http.HttpEntity;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.MediaType;
|
||||
@ -57,23 +58,30 @@ public class AliYunRestController {
|
||||
fileHeaders.setContentType(MediaType.parseMediaType(file.getContentType()));
|
||||
}
|
||||
|
||||
ByteArrayResource resource = new ByteArrayResource(file.getBytes()) {
|
||||
@Override
|
||||
public String getFilename() {
|
||||
return Objects.toString(file.getOriginalFilename(), "upload.bin");
|
||||
}
|
||||
};
|
||||
|
||||
MultiValueMap<String, Object> body = new LinkedMultiValueMap<>();
|
||||
body.add("file", new HttpEntity<>(resource, fileHeaders));
|
||||
|
||||
String uploadUrl = externalBaseUrl.replaceAll("/+$", "")
|
||||
+ "/oss/upload/general?dir=" + encode(dir)
|
||||
+ "&filename=" + encode(filename);
|
||||
ResponseEntity<Map> response = new RestTemplate().postForEntity(
|
||||
uploadUrl,
|
||||
new HttpEntity<>(body, requestHeaders),
|
||||
Map.class);
|
||||
ResponseEntity<Map> response;
|
||||
try (InputStream inputStream = file.getInputStream()) {
|
||||
InputStreamResource resource = new InputStreamResource(inputStream) {
|
||||
@Override
|
||||
public String getFilename() {
|
||||
return Objects.toString(file.getOriginalFilename(), "upload.bin");
|
||||
}
|
||||
|
||||
@Override
|
||||
public long contentLength() {
|
||||
return file.getSize();
|
||||
}
|
||||
};
|
||||
|
||||
MultiValueMap<String, Object> body = new LinkedMultiValueMap<>();
|
||||
body.add("file", new HttpEntity<>(resource, fileHeaders));
|
||||
response = new RestTemplate().postForEntity(
|
||||
uploadUrl,
|
||||
new HttpEntity<>(body, requestHeaders),
|
||||
Map.class);
|
||||
}
|
||||
Map<?, ?> responseBody = response.getBody();
|
||||
if (responseBody == null || !Boolean.TRUE.equals(responseBody.get("status"))) {
|
||||
String errorMsg = responseBody == null ? "upload failed"
|
||||
|
||||
@ -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<ActiveUserCountryCodeDTO> latestActiveUserCountryCode() {
|
||||
@ -63,6 +67,24 @@ public class DatavRestController extends BaseController {
|
||||
return countryDashboardService.pageRechargeDetails(cmd);
|
||||
}
|
||||
|
||||
@GetMapping("/country-dashboard/game-metrics")
|
||||
public PageResult<CountryDashboardGameMetricCO> countryDashboardGameMetrics(
|
||||
CountryDashboardGameMetricQryCmd cmd) {
|
||||
return countryDashboardGameMetricService.pageGameMetrics(cmd);
|
||||
}
|
||||
|
||||
@GetMapping("/country-dashboard/game-metrics/games")
|
||||
public PageResult<CountryDashboardGameMetricCO> countryDashboardGameMetricGames(
|
||||
CountryDashboardGameMetricQryCmd cmd) {
|
||||
return countryDashboardGameMetricService.pageGameSummaries(cmd);
|
||||
}
|
||||
|
||||
@GetMapping("/country-dashboard/game-metrics/country-rank")
|
||||
public List<CountryDashboardGameMetricCO> countryDashboardGameMetricCountryRanks(
|
||||
CountryDashboardGameMetricQryCmd cmd) {
|
||||
return countryDashboardGameMetricService.listGameCountryRanks(cmd);
|
||||
}
|
||||
|
||||
@PostMapping("/country-dashboard/backfill")
|
||||
public Map<String, Object> backfillCountryDashboard(
|
||||
@RequestParam String startDate,
|
||||
@ -81,4 +103,22 @@ public class DatavRestController extends BaseController {
|
||||
"statTimezones", refreshedTimezones);
|
||||
}
|
||||
|
||||
@PostMapping("/country-dashboard/game-metrics/backfill")
|
||||
public Map<String, Object> 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<String> refreshedTimezones = countryDashboardGameMetricService.refreshRange(
|
||||
parsedStartDate, parsedEndDate, sysOrigin, statTimezones);
|
||||
return Map.of(
|
||||
"status", "success",
|
||||
"startDate", parsedStartDate.toString(),
|
||||
"endDate", parsedEndDate.toString(),
|
||||
"sysOrigin", sysOrigin,
|
||||
"statTimezones", refreshedTimezones);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -0,0 +1,745 @@
|
||||
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<CountryDashboardGameMetricCO> 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<CountryDashboardGameMetricCO> 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 = normalizeGameProvider(cmd == null ? null : cmd.getGameProvider());
|
||||
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.startPeriodKey, condition.endPeriodKey, condition.statTimezone,
|
||||
condition.countryKeyword, countryCodes, gameProvider, gameKeyword, condition.sysOrigin);
|
||||
List<CountryDashboardGameMetricCO> records = countryDashboardDAO.listGamePeriodMetrics(
|
||||
condition.periodType, condition.startTime, condition.endTime,
|
||||
PERIOD_ALL.equals(condition.periodType) ? PERIOD_ALL : null,
|
||||
condition.startPeriodKey, condition.endPeriodKey, 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 PageResult<CountryDashboardGameMetricCO> pageGameSummaries(
|
||||
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<CountryDashboardGameMetricCO> result = new PageResult<>();
|
||||
result.setCurrent(cursor);
|
||||
result.setSize(limit);
|
||||
if (!hasText(condition.sysOrigin)) {
|
||||
result.setTotal(0L);
|
||||
result.setRecords(List.of());
|
||||
return result;
|
||||
}
|
||||
|
||||
SortSpec sortSpec = normalizeSummarySort(cmd == null ? null : cmd.getSortField(),
|
||||
cmd == null ? null : cmd.getSortOrder());
|
||||
String countryCodes = normalizeCsv(cmd == null ? null : cmd.getCountryCodes());
|
||||
String gameProvider = normalizeGameProvider(cmd == null ? null : cmd.getGameProvider());
|
||||
String gameKeyword = trimToNull(cmd == null ? null : cmd.getGameKeyword());
|
||||
|
||||
Long total = countryDashboardDAO.countGameSummaryMetrics(
|
||||
condition.periodType, condition.startTime, condition.endTime,
|
||||
PERIOD_ALL.equals(condition.periodType) ? PERIOD_ALL : null,
|
||||
condition.startPeriodKey, condition.endPeriodKey, condition.statTimezone,
|
||||
condition.countryKeyword, countryCodes, gameProvider, gameKeyword, condition.sysOrigin);
|
||||
List<CountryDashboardGameMetricCO> records = countryDashboardDAO.listGameSummaryMetrics(
|
||||
condition.periodType, condition.startTime, condition.endTime,
|
||||
PERIOD_ALL.equals(condition.periodType) ? PERIOD_ALL : null,
|
||||
condition.startPeriodKey, condition.endPeriodKey, 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 List<CountryDashboardGameMetricCO> listGameCountryRanks(
|
||||
CountryDashboardGameMetricQryCmd cmd) {
|
||||
ensureMetricSchema();
|
||||
QueryCondition condition = QueryCondition.of(cmd);
|
||||
if (!hasText(condition.sysOrigin)) {
|
||||
return List.of();
|
||||
}
|
||||
String gameProvider = normalizeGameProvider(cmd == null ? null : cmd.getGameProvider());
|
||||
String gameId = trimToNull(cmd == null ? null : cmd.getGameId());
|
||||
if (gameProvider == null || gameId == null) {
|
||||
return List.of();
|
||||
}
|
||||
int limit = Math.max(1, cmd.getRankLimit() == null ? 100 : cmd.getRankLimit());
|
||||
limit = Math.min(500, limit);
|
||||
String countryCodes = normalizeCsv(cmd.getCountryCodes());
|
||||
return countryDashboardDAO.listGameCountryRankMetrics(
|
||||
condition.periodType, condition.startTime, condition.endTime,
|
||||
PERIOD_ALL.equals(condition.periodType) ? PERIOD_ALL : null,
|
||||
condition.startPeriodKey, condition.endPeriodKey, condition.statTimezone,
|
||||
condition.countryKeyword, countryCodes, gameProvider, null, gameId, condition.sysOrigin,
|
||||
limit)
|
||||
.stream()
|
||||
.map(this::toCO)
|
||||
.toList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void refreshRecentDays(int days) {
|
||||
ensureMetricSchema();
|
||||
int safeDays = Math.max(days, 1);
|
||||
List<String> sysOrigins = resolveSysOrigins();
|
||||
List<StatTimezone> statTimezones = resolveStatTimezones(null);
|
||||
for (String sysOrigin : sysOrigins) {
|
||||
for (StatTimezone statTimezone : statTimezones) {
|
||||
List<LocalDate> statDates = recentDates(safeDays, statTimezone.zoneId());
|
||||
for (LocalDate statDate : statDates) {
|
||||
try {
|
||||
refreshPeriod(toDayWindow(statDate), sysOrigin, statTimezone);
|
||||
} catch (Exception ex) {
|
||||
log.error("refresh country dashboard 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<String> 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<LocalDate> statDates = new ArrayList<>();
|
||||
for (long i = 0; i < dayCount; i++) {
|
||||
statDates.add(startDate.plusDays(i));
|
||||
}
|
||||
List<StatTimezone> 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<LocalDate> statDates, String sysOrigin,
|
||||
StatTimezone statTimezone, boolean failFast) {
|
||||
Set<PeriodWindow> 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<String, CountryDashboardGameMetricDTO> rows = new LinkedHashMap<>();
|
||||
mergeAmounts(rows, countryDashboardDAO.listWalletGameMetricAmounts(
|
||||
window.periodType(), timeWindow.startTime(), timeWindow.endTime(),
|
||||
timeWindow.startTimeMillis(), timeWindow.endTimeMillis(), null, sysOrigin,
|
||||
zoneOffset(STORAGE_ZONE_ID), statTimezone.offset()), window, sysOrigin, statTimezone);
|
||||
mergeAmounts(rows, countryDashboardDAO.listLuckyBoxGameMetricAmounts(
|
||||
window.periodType(), timeWindow.startTime(), timeWindow.endTime(), null, 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<CountryDashboardGameMetricDTO> 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<String, CountryDashboardGameMetricDTO> 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<CountryDashboardGameMetricDTO> 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<String, CountryDashboardGameMetricDTO> rows,
|
||||
List<CountryDashboardGameMetricDTO> 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<String, CountryDashboardGameMetricDTO> rows,
|
||||
List<CountryDashboardGameMetricDTO> 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<String, CountryDashboardGameMetricDTO> 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<LocalDate> recentDates(int days, ZoneId zoneId) {
|
||||
LocalDate today = LocalDate.now(zoneId);
|
||||
List<LocalDate> statDates = new ArrayList<>();
|
||||
for (int i = days - 1; i >= 0; i--) {
|
||||
statDates.add(today.minusDays(i));
|
||||
}
|
||||
return statDates;
|
||||
}
|
||||
|
||||
private List<StatTimezone> resolveStatTimezones(String statTimezonesValue) {
|
||||
String config = trimToNull(statTimezonesValue);
|
||||
if (config == null) {
|
||||
config = trimToNull(refreshTimezones);
|
||||
}
|
||||
if (config == null) {
|
||||
config = "Asia/Riyadh,UTC,Asia/Shanghai";
|
||||
}
|
||||
|
||||
Map<String, StatTimezone> zones = new LinkedHashMap<>();
|
||||
Arrays.stream(config.split(","))
|
||||
.map(String::trim)
|
||||
.map(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<String> 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<String> 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 SortSpec normalizeSummarySort(String sortField, String sortOrder) {
|
||||
String column = switch (Objects.toString(sortField, "")) {
|
||||
case "payoutAmount" -> "payoutAmount";
|
||||
case "profitAmount" -> "profitAmount";
|
||||
case "userCount" -> "userCount";
|
||||
case "orderCount" -> "orderCount";
|
||||
case "consumeAmount" -> "consumeAmount";
|
||||
default -> null;
|
||||
};
|
||||
String direction = "ascend".equalsIgnoreCase(sortOrder) ? "ASC" : "DESC";
|
||||
return new SortSpec(column, direction);
|
||||
}
|
||||
|
||||
private static String normalizeGameProvider(String gameProvider) {
|
||||
String value = trimToNull(gameProvider);
|
||||
return value == null ? null : value.toUpperCase(Locale.ROOT);
|
||||
}
|
||||
|
||||
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 startPeriodKey;
|
||||
private final String endPeriodKey;
|
||||
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 startPeriodKey, String endPeriodKey,
|
||||
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.startPeriodKey = startPeriodKey;
|
||||
this.endPeriodKey = endPeriodKey;
|
||||
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);
|
||||
String startPeriodKey = PERIOD_DAY.equals(periodType) && start != null
|
||||
? start.format(DATE_FORMATTER) : null;
|
||||
String endPeriodKey = PERIOD_DAY.equals(periodType) && end != null
|
||||
? end.format(DATE_FORMATTER) : null;
|
||||
return new QueryCondition(
|
||||
periodType,
|
||||
statZoneId.getId(),
|
||||
statZoneId,
|
||||
start,
|
||||
end,
|
||||
storageStartTime,
|
||||
storageEndTime,
|
||||
startPeriodKey,
|
||||
endPeriodKey,
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -9,11 +9,14 @@ import com.red.circle.console.inner.error.ConsoleErrorCode;
|
||||
import com.red.circle.framework.core.asserts.ResponseAssert;
|
||||
import com.red.circle.framework.core.response.CommonErrorCode;
|
||||
import com.red.circle.framework.dto.PageResult;
|
||||
import com.red.circle.other.inner.endpoint.sys.AdministratorClient;
|
||||
import com.red.circle.other.inner.endpoint.team.manager.TeamManagerClient;
|
||||
import com.red.circle.other.inner.endpoint.team.target.UserHistoryIdentityClient;
|
||||
import com.red.circle.other.inner.endpoint.user.region.RegionConfigClient;
|
||||
import com.red.circle.other.inner.endpoint.user.user.UserProfileClient;
|
||||
import com.red.circle.other.inner.enums.sys.appmanager.RoomRolesEnum;
|
||||
import com.red.circle.other.inner.model.cmd.team.manager.TeamManagerOpsPageQryCmd;
|
||||
import com.red.circle.other.inner.model.dto.sys.AdministratorDTO;
|
||||
import com.red.circle.other.inner.model.dto.agency.manager.TeamManagerDTO;
|
||||
import com.red.circle.other.inner.model.dto.user.UserProfileDTO;
|
||||
import com.red.circle.tool.core.sequence.IdWorkerUtils;
|
||||
@ -36,6 +39,7 @@ public class TeamManagerServiceImpl implements TeamManagerService {
|
||||
|
||||
private final UserService userService;
|
||||
private final TeamManagerClient teamManagerClient;
|
||||
private final AdministratorClient administratorClient;
|
||||
private final UserProfileClient userProfileClient;
|
||||
private final RegionConfigClient regionConfigClient;
|
||||
private final UserHistoryIdentityClient userHistoryIdentityClient;
|
||||
@ -111,7 +115,7 @@ public class TeamManagerServiceImpl implements TeamManagerService {
|
||||
.setCreateUser(cmd.getCreateUser());
|
||||
|
||||
ResponseAssert.requiredSuccess(teamManagerClient.add(addCmd));
|
||||
ResponseAssert.requiredSuccess(userHistoryIdentityClient.saveManager(cmd.getUserId()));
|
||||
ResponseAssert.requiredSuccess(userHistoryIdentityClient.saveYumiManager(cmd.getUserId()));
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -128,6 +132,16 @@ public class TeamManagerServiceImpl implements TeamManagerService {
|
||||
TeamManagerDTO db = ResponseAssert.requiredSuccess(teamManagerClient.getById(id));
|
||||
ResponseAssert.notNull(CommonErrorCode.DATA_ERROR, db);
|
||||
ResponseAssert.requiredSuccess(teamManagerClient.deleteById(id));
|
||||
removeAppManagerIfOnlyManagerRole(db.getUserId());
|
||||
}
|
||||
|
||||
private void removeAppManagerIfOnlyManagerRole(Long userId) {
|
||||
AdministratorDTO administrator = ResponseAssert.requiredSuccess(
|
||||
administratorClient.getByUserId(userId));
|
||||
if (Objects.isNull(administrator) || !RoomRolesEnum.MANAGER.eq(administrator.getRoles())) {
|
||||
return;
|
||||
}
|
||||
ResponseAssert.requiredSuccess(administratorClient.deleteSysAdministrator(administrator.getId()));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -287,9 +287,10 @@ public class UserBaseInfoServiceImpl implements
|
||||
.setAnchor(Objects.nonNull(teamMember))
|
||||
.setBd(ResponseAssert.requiredSuccess(bdTeamInfoClient.check(userId)))
|
||||
.setBdLeader(ResponseAssert.requiredSuccess(bdTeamLeaderClient.check(userId)))
|
||||
.setManager(ResponseAssert.requiredSuccess(teamManagerClient.check(userId)))
|
||||
.setManager(Boolean.FALSE)
|
||||
.setYumiManager(ResponseAssert.requiredSuccess(teamManagerClient.check(userId)))
|
||||
.setFreightAgent(
|
||||
ResponseAssert.requiredSuccess(freightGoldClient.checkFreightAgent(userId)))
|
||||
ResponseAssert.requiredSuccess(freightGoldClient.checkFreightAgent(userId)))
|
||||
.setHistoryIdentity(
|
||||
ResponseAssert.requiredSuccess(historyIdentityClient.getLabels(userId)));
|
||||
}
|
||||
|
||||
@ -69,6 +69,7 @@ public class UserDataViolationServiceImpl implements UserDataViolationService {
|
||||
}*/
|
||||
|
||||
public void processUpdateAccountStatus(ApprovalAccountCmd param) {
|
||||
boolean realtimeKicked = false;
|
||||
|
||||
// 警告
|
||||
if (ConsoleAccountStatusEnum.WARNING.eq(param.getAccountStatusEnum())) {
|
||||
@ -112,8 +113,8 @@ public class UserDataViolationServiceImpl implements UserDataViolationService {
|
||||
redisService
|
||||
.delete(SecurityConstant.getAccessTokenCacheKeyMd5(param.getBeApprovalUserId()));
|
||||
|
||||
imAccountClient.kick(param.getBeApprovalUserId());
|
||||
authClient.removeToken(param.getBeApprovalUserId());
|
||||
kickUserAndRemoveAuthToken(param.getBeApprovalUserId());
|
||||
realtimeKicked = true;
|
||||
}
|
||||
// 封禁+设备
|
||||
else if (ConsoleAccountStatusEnum.ARCHIVE_DEVICE.eq(param.getAccountStatusEnum())) {
|
||||
@ -137,9 +138,8 @@ public class UserDataViolationServiceImpl implements UserDataViolationService {
|
||||
redisService
|
||||
.delete(SecurityConstant.getAccessTokenCacheKeyMd5(relationUser.getUserId()));
|
||||
|
||||
imAccountClient.kick(relationUser.getUserId());
|
||||
authClient.removeToken(relationUser.getUserId());
|
||||
ResponseAssert.requiredSuccess(newsletterClient.send(
|
||||
kickUserAndRemoveAuthToken(relationUser.getUserId());
|
||||
ResponseAssert.requiredSuccess(newsletterClient.send(
|
||||
new NewsletterResultCmd()
|
||||
.setUserIds(List.of(relationUser.getUserId()))
|
||||
.setEvent(NewsletterEvent.ARCHIVE_DEVICE)
|
||||
@ -215,7 +215,9 @@ public class UserDataViolationServiceImpl implements UserDataViolationService {
|
||||
return;
|
||||
}
|
||||
|
||||
removeTrtcRoom(param);
|
||||
if (!realtimeKicked) {
|
||||
removeTrtcRoom(param.getBeApprovalUserId());
|
||||
}
|
||||
|
||||
addLog(param, param.getBeApprovalUserId());
|
||||
|
||||
@ -223,9 +225,15 @@ public class UserDataViolationServiceImpl implements UserDataViolationService {
|
||||
|
||||
}
|
||||
|
||||
private void removeTrtcRoom(ApprovalAccountCmd param) {
|
||||
private void kickUserAndRemoveAuthToken(Long userId) {
|
||||
imAccountClient.kick(userId);
|
||||
authClient.removeToken(userId);
|
||||
removeTrtcRoom(userId);
|
||||
}
|
||||
|
||||
private void removeTrtcRoom(Long userId) {
|
||||
String sessionId = ResponseAssert.requiredSuccess(
|
||||
userOnlineClient.getSessionId(param.getBeApprovalUserId()));
|
||||
userOnlineClient.getSessionId(userId));
|
||||
if (Objects.nonNull(sessionId)) {
|
||||
RoomProfileDTO roomProfile = ResponseAssert.requiredSuccess(
|
||||
roomManagerClient.getProfileById(Long.valueOf(sessionId)));
|
||||
@ -233,7 +241,7 @@ public class UserDataViolationServiceImpl implements UserDataViolationService {
|
||||
trtcClient.removeUser(
|
||||
new TrtcRemoveUserCmd()
|
||||
.setRoomId(Long.parseLong(roomProfile.getRoomAccount()))
|
||||
.setUserIds(List.of(param.getBeApprovalUserId()))
|
||||
.setUserIds(List.of(userId))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -180,19 +180,23 @@ public class UserWalletServiceImpl implements UserWalletService {
|
||||
|
||||
private UserGoldRunningWaterClsTmpCO getGoldRunningWaterDbFallback(
|
||||
UserGoldRunningWaterBackQryCmd query) {
|
||||
if (StringUtils.isNotBlank(query.getContext())) {
|
||||
if (StringUtils.isNotBlank(query.getContext()) && Objects.isNull(query.getLastId())) {
|
||||
return new UserGoldRunningWaterClsTmpCO()
|
||||
.setListOver(true)
|
||||
.setWaters(Lists.newArrayList());
|
||||
}
|
||||
|
||||
List<UserGoldRunningWaterCO> waters = listGoldRunningWater(query);
|
||||
int limit = Optional.ofNullable(query.getLimit()).filter(value -> value > 0)
|
||||
.orElse(PageConstant.DEFAULT_LIMIT_SIZE);
|
||||
if (CollectionUtils.isEmpty(waters)) {
|
||||
return null;
|
||||
return new UserGoldRunningWaterClsTmpCO()
|
||||
.setListOver(true)
|
||||
.setWaters(Lists.newArrayList());
|
||||
}
|
||||
|
||||
return new UserGoldRunningWaterClsTmpCO()
|
||||
.setListOver(true)
|
||||
.setListOver(waters.size() < limit)
|
||||
.setWaters(waters);
|
||||
}
|
||||
|
||||
|
||||
@ -37,13 +37,18 @@ public class UserIdentityCO implements Serializable {
|
||||
private Boolean bdLeader;
|
||||
|
||||
/**
|
||||
* true:团队经理,false:不是.
|
||||
* true:旧团队经理,false:不是.
|
||||
*/
|
||||
private Boolean manager;
|
||||
|
||||
/**
|
||||
* true:Yumi 团队经理,false:不是.
|
||||
*/
|
||||
private Boolean yumiManager;
|
||||
|
||||
/**
|
||||
* 货运代理: true:是 false:不是.
|
||||
*/
|
||||
*/
|
||||
private Boolean freightAgent;
|
||||
|
||||
/**
|
||||
|
||||
@ -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;
|
||||
}
|
||||
@ -0,0 +1,63 @@
|
||||
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 gameId;
|
||||
|
||||
/**
|
||||
* 游戏 ID 或名称关键字.
|
||||
*/
|
||||
private String gameKeyword;
|
||||
|
||||
/**
|
||||
* 国家排名返回条数.
|
||||
*/
|
||||
private Integer rankLimit;
|
||||
|
||||
/**
|
||||
* 排序字段.
|
||||
*/
|
||||
private String sortField;
|
||||
|
||||
/**
|
||||
* ascend / descend.
|
||||
*/
|
||||
private String sortOrder;
|
||||
}
|
||||
@ -0,0 +1,24 @@
|
||||
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<CountryDashboardGameMetricCO> pageGameMetrics(CountryDashboardGameMetricQryCmd cmd);
|
||||
|
||||
PageResult<CountryDashboardGameMetricCO> pageGameSummaries(CountryDashboardGameMetricQryCmd cmd);
|
||||
|
||||
List<CountryDashboardGameMetricCO> listGameCountryRanks(CountryDashboardGameMetricQryCmd cmd);
|
||||
|
||||
void refreshRecentDays(int days);
|
||||
|
||||
List<String> refreshRange(LocalDate startDate, LocalDate endDate, String sysOrigin,
|
||||
String statTimezones);
|
||||
}
|
||||
@ -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<IUserBaseInfo> {
|
||||
|
||||
int createPeriodUserMetricTable();
|
||||
|
||||
int createGamePeriodMetricTable();
|
||||
|
||||
int createGamePeriodUserMetricTable();
|
||||
|
||||
int ensurePeriodMetricTimezoneSchema();
|
||||
|
||||
List<CountryDashboardMetricDTO> listNewUsers(
|
||||
@ -259,4 +264,156 @@ public interface CountryDashboardDAO extends BaseDAO<IUserBaseInfo> {
|
||||
@Param("statTimezoneOffset") String statTimezoneOffset,
|
||||
@Param("offset") int offset,
|
||||
@Param("limit") int limit);
|
||||
|
||||
List<CountryDashboardGameMetricDTO> listWalletGameMetricAmounts(
|
||||
@Param("periodType") String periodType,
|
||||
@Param("startTime") LocalDateTime startTime,
|
||||
@Param("endTime") LocalDateTime endTime,
|
||||
@Param("startTimeMillis") Long startTimeMillis,
|
||||
@Param("endTimeMillis") Long endTimeMillis,
|
||||
@Param("countryKeyword") String countryKeyword,
|
||||
@Param("sysOrigin") String sysOrigin,
|
||||
@Param("storageTimezoneOffset") String storageTimezoneOffset,
|
||||
@Param("statTimezoneOffset") String statTimezoneOffset);
|
||||
|
||||
List<CountryDashboardGameMetricDTO> listLuckyBoxGameMetricAmounts(
|
||||
@Param("periodType") String periodType,
|
||||
@Param("startTime") LocalDateTime startTime,
|
||||
@Param("endTime") LocalDateTime endTime,
|
||||
@Param("countryKeyword") String countryKeyword,
|
||||
@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<CountryDashboardGameMetricDTO> listGamePeriodUserMetricCounts(
|
||||
@Param("periodType") String periodType,
|
||||
@Param("periodKey") String periodKey,
|
||||
@Param("statTimezone") String statTimezone,
|
||||
@Param("sysOrigin") String sysOrigin);
|
||||
|
||||
List<CountryDashboardGameMetricDTO> 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<CountryDashboardGameMetricDTO> metrics);
|
||||
|
||||
Long countGamePeriodMetrics(
|
||||
@Param("periodType") String periodType,
|
||||
@Param("startTime") LocalDateTime startTime,
|
||||
@Param("endTime") LocalDateTime endTime,
|
||||
@Param("periodKey") String periodKey,
|
||||
@Param("startPeriodKey") String startPeriodKey,
|
||||
@Param("endPeriodKey") String endPeriodKey,
|
||||
@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<CountryDashboardGameMetricDTO> listGamePeriodMetrics(
|
||||
@Param("periodType") String periodType,
|
||||
@Param("startTime") LocalDateTime startTime,
|
||||
@Param("endTime") LocalDateTime endTime,
|
||||
@Param("periodKey") String periodKey,
|
||||
@Param("startPeriodKey") String startPeriodKey,
|
||||
@Param("endPeriodKey") String endPeriodKey,
|
||||
@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);
|
||||
|
||||
Long countGameSummaryMetrics(
|
||||
@Param("periodType") String periodType,
|
||||
@Param("startTime") LocalDateTime startTime,
|
||||
@Param("endTime") LocalDateTime endTime,
|
||||
@Param("periodKey") String periodKey,
|
||||
@Param("startPeriodKey") String startPeriodKey,
|
||||
@Param("endPeriodKey") String endPeriodKey,
|
||||
@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<CountryDashboardGameMetricDTO> listGameSummaryMetrics(
|
||||
@Param("periodType") String periodType,
|
||||
@Param("startTime") LocalDateTime startTime,
|
||||
@Param("endTime") LocalDateTime endTime,
|
||||
@Param("periodKey") String periodKey,
|
||||
@Param("startPeriodKey") String startPeriodKey,
|
||||
@Param("endPeriodKey") String endPeriodKey,
|
||||
@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);
|
||||
|
||||
List<CountryDashboardGameMetricDTO> listGameCountryRankMetrics(
|
||||
@Param("periodType") String periodType,
|
||||
@Param("startTime") LocalDateTime startTime,
|
||||
@Param("endTime") LocalDateTime endTime,
|
||||
@Param("periodKey") String periodKey,
|
||||
@Param("startPeriodKey") String startPeriodKey,
|
||||
@Param("endPeriodKey") String endPeriodKey,
|
||||
@Param("statTimezone") String statTimezone,
|
||||
@Param("countryKeyword") String countryKeyword,
|
||||
@Param("countryCodes") String countryCodes,
|
||||
@Param("gameProvider") String gameProvider,
|
||||
@Param("gameKeyword") String gameKeyword,
|
||||
@Param("gameId") String gameId,
|
||||
@Param("sysOrigin") String sysOrigin,
|
||||
@Param("limit") int limit);
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
@ -91,6 +91,96 @@
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='国家数据大屏周期用户去重明细'
|
||||
</update>
|
||||
|
||||
<update id="createGamePeriodMetricTable">
|
||||
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='国家数据大屏游戏维度周期预聚合'
|
||||
;
|
||||
|
||||
SET @current_schema = DATABASE();
|
||||
|
||||
SET @index_columns = (
|
||||
SELECT GROUP_CONCAT(COLUMN_NAME ORDER BY SEQ_IN_INDEX)
|
||||
FROM INFORMATION_SCHEMA.STATISTICS
|
||||
WHERE TABLE_SCHEMA = @current_schema
|
||||
AND TABLE_NAME = 'country_dashboard_game_period_metric'
|
||||
AND INDEX_NAME = 'idx_game_period_game'
|
||||
);
|
||||
SET @ddl = IF(
|
||||
@index_columns IS NULL,
|
||||
'ALTER TABLE `country_dashboard_game_period_metric` ADD KEY `idx_game_period_game` (`sys_origin`, `stat_timezone`, `period_type`, `period_key`, `game_provider`, `game_id`, `country_code`)',
|
||||
'SELECT 1'
|
||||
);
|
||||
PREPARE stmt FROM @ddl;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
SET @index_columns = (
|
||||
SELECT GROUP_CONCAT(COLUMN_NAME ORDER BY SEQ_IN_INDEX)
|
||||
FROM INFORMATION_SCHEMA.STATISTICS
|
||||
WHERE TABLE_SCHEMA = @current_schema
|
||||
AND TABLE_NAME = 'country_dashboard_game_period_metric'
|
||||
AND INDEX_NAME = 'idx_game_rank_period'
|
||||
);
|
||||
SET @ddl = IF(
|
||||
@index_columns IS NULL,
|
||||
'ALTER TABLE `country_dashboard_game_period_metric` ADD KEY `idx_game_rank_period` (`sys_origin`, `stat_timezone`, `game_provider`, `game_id`, `period_type`, `period_key`, `country_code`)',
|
||||
'SELECT 1'
|
||||
);
|
||||
PREPARE stmt FROM @ddl;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
</update>
|
||||
|
||||
<update id="createGamePeriodUserMetricTable">
|
||||
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='国家数据大屏游戏维度周期用户去重明细'
|
||||
</update>
|
||||
|
||||
<update id="ensurePeriodMetricTimezoneSchema">
|
||||
SET @country_dashboard_schema_lock = GET_LOCK('country_dashboard_period_timezone_schema', 60);
|
||||
SET @current_schema = DATABASE();
|
||||
@ -519,51 +609,51 @@
|
||||
</sql>
|
||||
|
||||
<sql id="WalletGoldAssetRecordSource">
|
||||
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
|
||||
<include refid="WalletGoldAssetRecordShardWhere"/>
|
||||
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
|
||||
<include refid="WalletGoldAssetRecordShardWhere"/>
|
||||
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
|
||||
<include refid="WalletGoldAssetRecordShardWhere"/>
|
||||
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
|
||||
<include refid="WalletGoldAssetRecordShardWhere"/>
|
||||
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
|
||||
<include refid="WalletGoldAssetRecordShardWhere"/>
|
||||
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
|
||||
<include refid="WalletGoldAssetRecordShardWhere"/>
|
||||
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
|
||||
<include refid="WalletGoldAssetRecordShardWhere"/>
|
||||
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
|
||||
<include refid="WalletGoldAssetRecordShardWhere"/>
|
||||
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
|
||||
<include refid="WalletGoldAssetRecordShardWhere"/>
|
||||
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
|
||||
<include refid="WalletGoldAssetRecordShardWhere"/>
|
||||
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
|
||||
<include refid="WalletGoldAssetRecordShardWhere"/>
|
||||
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
|
||||
<include refid="WalletGoldAssetRecordShardWhere"/>
|
||||
</sql>
|
||||
@ -596,6 +686,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 +696,135 @@
|
||||
AND t.event_type NOT LIKE 'ACCEPT_GIFT%'
|
||||
</sql>
|
||||
|
||||
<sql id="WalletGameProviderExpr">
|
||||
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
|
||||
</sql>
|
||||
|
||||
<sql id="WalletGameIdExpr">
|
||||
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
|
||||
</sql>
|
||||
|
||||
<sql id="WalletGameNameFallbackExpr">
|
||||
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
|
||||
</sql>
|
||||
|
||||
<sql id="WalletGameJoinCatalogs">
|
||||
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(<include refid="WalletGameIdExpr"/> 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 = <include refid="WalletGameIdExpr"/> 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 = <include refid="WalletGameIdExpr"/> COLLATE utf8mb4_0900_ai_ci
|
||||
</sql>
|
||||
|
||||
<sql id="WalletGameNameExpr">
|
||||
COALESCE(
|
||||
NULLIF(CONVERT(bc.name USING utf8mb4) COLLATE utf8mb4_unicode_ci, CONVERT('' USING utf8mb4) COLLATE utf8mb4_unicode_ci),
|
||||
NULLIF(CONVERT(lc.name USING utf8mb4) COLLATE utf8mb4_unicode_ci, CONVERT('' USING utf8mb4) COLLATE utf8mb4_unicode_ci),
|
||||
NULLIF(CONVERT(sg.name USING utf8mb4) COLLATE utf8mb4_unicode_ci, CONVERT('' USING utf8mb4) COLLATE utf8mb4_unicode_ci),
|
||||
CONVERT(<include refid="WalletGameNameFallbackExpr"/> USING utf8mb4) COLLATE utf8mb4_unicode_ci
|
||||
)
|
||||
</sql>
|
||||
|
||||
<sql id="GameMetricFilterWhere">
|
||||
WHERE t.period_type = #{periodType}
|
||||
AND t.stat_timezone = #{statTimezone}
|
||||
<choose>
|
||||
<when test="periodType == 'ALL'">
|
||||
AND t.period_key = 'ALL'
|
||||
</when>
|
||||
<when test="periodType == 'DAY' and startPeriodKey != null and startPeriodKey != '' and endPeriodKey != null and endPeriodKey != ''">
|
||||
AND t.period_key >= #{startPeriodKey}
|
||||
AND t.period_key <= #{endPeriodKey}
|
||||
</when>
|
||||
<otherwise>
|
||||
<if test="startTime != null">
|
||||
AND t.period_start_date >= DATE(#{startTime})
|
||||
</if>
|
||||
<if test="endTime != null">
|
||||
AND t.period_start_date < DATE(#{endTime})
|
||||
</if>
|
||||
</otherwise>
|
||||
</choose>
|
||||
<if test="countryKeyword != null and countryKeyword != ''">
|
||||
AND (
|
||||
t.country_code = #{countryKeyword}
|
||||
OR t.country_name LIKE CONCAT('%', #{countryKeyword}, '%')
|
||||
)
|
||||
</if>
|
||||
<if test="countryCodes != null and countryCodes != ''">
|
||||
AND FIND_IN_SET(t.country_code, #{countryCodes})
|
||||
</if>
|
||||
<if test="gameProvider != null and gameProvider != ''">
|
||||
AND t.game_provider = #{gameProvider}
|
||||
</if>
|
||||
<if test="gameKeyword != null and gameKeyword != ''">
|
||||
AND (
|
||||
t.game_id = #{gameKeyword}
|
||||
OR t.game_name LIKE CONCAT('%', #{gameKeyword}, '%')
|
||||
)
|
||||
</if>
|
||||
<if test="sysOrigin != null and sysOrigin != ''">
|
||||
AND t.sys_origin = #{sysOrigin}
|
||||
</if>
|
||||
</sql>
|
||||
|
||||
<select id="listPrecomputedMetrics"
|
||||
resultType="com.red.circle.console.infra.database.rds.dto.datav.CountryDashboardMetricDTO">
|
||||
SELECT
|
||||
@ -1345,6 +1565,391 @@
|
||||
GROUP BY countryCode, countryName, periodKey, periodName
|
||||
</select>
|
||||
|
||||
<select id="listWalletGameMetricAmounts"
|
||||
resultType="com.red.circle.console.infra.database.rds.dto.datav.CountryDashboardGameMetricDTO">
|
||||
SELECT
|
||||
<include refid="CountryColumns"/>,
|
||||
<include refid="PeriodColumnsT"/>,
|
||||
<include refid="WalletGameProviderExpr"/> AS gameProvider,
|
||||
<include refid="WalletGameIdExpr"/> AS gameId,
|
||||
MAX(<include refid="WalletGameNameExpr"/>) AS gameName,
|
||||
IFNULL(SUM(CASE WHEN t.type = 1 THEN t.penny_amount ELSE 0 END) / 100.00, 0) AS consumeAmount,
|
||||
IFNULL(SUM(CASE WHEN t.type = 0 THEN t.penny_amount ELSE 0 END) / 100.00, 0) AS payoutAmount,
|
||||
IFNULL((SUM(CASE WHEN t.type = 1 THEN t.penny_amount ELSE 0 END)
|
||||
- SUM(CASE WHEN t.type = 0 THEN t.penny_amount ELSE 0 END)) / 100.00, 0) AS profitAmount,
|
||||
COUNT(1) AS orderCount
|
||||
FROM (
|
||||
<include refid="WalletGoldAssetRecordSource"/>
|
||||
) t
|
||||
INNER JOIN user_base_info u ON u.id = t.user_id
|
||||
<include refid="WalletGameJoinCatalogs"/>
|
||||
WHERE <include refid="WalletGameEventWhere"/>
|
||||
<include refid="UserWhere"/>
|
||||
<include refid="SourceTimeWhere"/>
|
||||
<include refid="SourceSysOriginWhere"/>
|
||||
GROUP BY countryCode, countryName, periodKey, periodName, gameProvider, gameId
|
||||
</select>
|
||||
|
||||
<select id="listLuckyBoxGameMetricAmounts"
|
||||
resultType="com.red.circle.console.infra.database.rds.dto.datav.CountryDashboardGameMetricDTO">
|
||||
SELECT
|
||||
<include refid="CountryColumns"/>,
|
||||
<include refid="PeriodColumnsT"/>,
|
||||
'INTERNAL' AS gameProvider,
|
||||
'LUCKY_BOX' AS gameId,
|
||||
'Lucky Box' AS gameName,
|
||||
IFNULL(SUM(t.pay_amount), 0) AS consumeAmount,
|
||||
IFNULL(SUM(t.gift_amount), 0) AS payoutAmount,
|
||||
IFNULL(SUM(t.pay_amount), 0) - IFNULL(SUM(t.gift_amount), 0) AS profitAmount,
|
||||
COUNT(1) AS orderCount
|
||||
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, gameProvider, gameId, gameName
|
||||
</select>
|
||||
|
||||
<delete id="deleteGamePeriodUserMetrics">
|
||||
DELETE FROM country_dashboard_game_period_user_metric
|
||||
WHERE period_type = #{periodType}
|
||||
AND period_key = #{periodKey}
|
||||
AND stat_timezone = #{statTimezone}
|
||||
<if test="sysOrigin != null and sysOrigin != ''">
|
||||
AND sys_origin = #{sysOrigin}
|
||||
</if>
|
||||
</delete>
|
||||
|
||||
<insert id="insertWalletGamePeriodUserMetrics">
|
||||
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,
|
||||
<include refid="WalletGameProviderExpr"/> AS gameProvider,
|
||||
<include refid="WalletGameIdExpr"/> AS gameId,
|
||||
t.user_id AS userId
|
||||
FROM (
|
||||
<include refid="WalletGoldAssetRecordSource"/>
|
||||
) t
|
||||
INNER JOIN user_base_info u ON u.id = t.user_id
|
||||
<include refid="WalletGameJoinCatalogs"/>
|
||||
WHERE <include refid="WalletGameEventWhere"/>
|
||||
AND (u.is_del = 0 OR u.is_del IS NULL)
|
||||
<include refid="SourceTimeWhere"/>
|
||||
<include refid="SourceSysOriginWhere"/>
|
||||
<if test="sysOrigin != null and sysOrigin != ''">
|
||||
AND u.origin_sys = #{sysOrigin}
|
||||
</if>
|
||||
</insert>
|
||||
|
||||
<insert id="insertLuckyBoxGamePeriodUserMetrics">
|
||||
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)
|
||||
<include refid="SourceTimeWhere"/>
|
||||
<include refid="SourceSysOriginWhere"/>
|
||||
<if test="sysOrigin != null and sysOrigin != ''">
|
||||
AND u.origin_sys = #{sysOrigin}
|
||||
</if>
|
||||
</insert>
|
||||
|
||||
<insert id="insertAllGamePeriodUserMetricsFromDaily">
|
||||
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}
|
||||
<if test="sysOrigin != null and sysOrigin != ''">
|
||||
AND t.sys_origin = #{sysOrigin}
|
||||
</if>
|
||||
</insert>
|
||||
|
||||
<select id="listGamePeriodUserMetricCounts"
|
||||
resultType="com.red.circle.console.infra.database.rds.dto.datav.CountryDashboardGameMetricDTO">
|
||||
SELECT
|
||||
t.country_code AS countryCode,
|
||||
MAX(t.country_name) AS countryName,
|
||||
t.period_key AS periodKey,
|
||||
MAX(t.period_name) AS periodName,
|
||||
t.game_provider AS gameProvider,
|
||||
t.game_id AS gameId,
|
||||
COUNT(1) AS userCount
|
||||
FROM country_dashboard_game_period_user_metric t
|
||||
WHERE t.period_type = #{periodType}
|
||||
AND t.period_key = #{periodKey}
|
||||
AND t.stat_timezone = #{statTimezone}
|
||||
<if test="sysOrigin != null and sysOrigin != ''">
|
||||
AND t.sys_origin = #{sysOrigin}
|
||||
</if>
|
||||
GROUP BY countryCode, periodKey, gameProvider, gameId
|
||||
</select>
|
||||
|
||||
<select id="listAllGamePeriodAmountsFromPeriodDays"
|
||||
resultType="com.red.circle.console.infra.database.rds.dto.datav.CountryDashboardGameMetricDTO">
|
||||
SELECT
|
||||
t.country_code AS countryCode,
|
||||
MAX(t.country_name) AS countryName,
|
||||
'ALL' AS periodKey,
|
||||
'全部' AS periodName,
|
||||
t.game_provider AS gameProvider,
|
||||
t.game_id AS gameId,
|
||||
MAX(t.game_name) AS gameName,
|
||||
IFNULL(SUM(t.consume_amount), 0) AS consumeAmount,
|
||||
IFNULL(SUM(t.payout_amount), 0) AS payoutAmount,
|
||||
IFNULL(SUM(t.profit_amount), 0) AS profitAmount,
|
||||
IFNULL(SUM(t.order_count), 0) AS orderCount
|
||||
FROM country_dashboard_game_period_metric t
|
||||
WHERE t.period_type = 'DAY'
|
||||
AND t.stat_timezone = #{statTimezone}
|
||||
<if test="sysOrigin != null and sysOrigin != ''">
|
||||
AND t.sys_origin = #{sysOrigin}
|
||||
</if>
|
||||
GROUP BY countryCode, gameProvider, gameId
|
||||
</select>
|
||||
|
||||
<delete id="deleteGamePeriodMetrics">
|
||||
DELETE FROM country_dashboard_game_period_metric
|
||||
WHERE period_type = #{periodType}
|
||||
AND period_key = #{periodKey}
|
||||
AND stat_timezone = #{statTimezone}
|
||||
<if test="sysOrigin != null and sysOrigin != ''">
|
||||
AND sys_origin = #{sysOrigin}
|
||||
</if>
|
||||
</delete>
|
||||
|
||||
<insert id="upsertGamePeriodMetrics">
|
||||
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
|
||||
<foreach collection="metrics" item="metric" separator=",">
|
||||
(
|
||||
#{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()
|
||||
)
|
||||
</foreach>
|
||||
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)
|
||||
</insert>
|
||||
|
||||
<select id="countGamePeriodMetrics" resultType="java.lang.Long">
|
||||
SELECT COUNT(1)
|
||||
FROM country_dashboard_game_period_metric t
|
||||
<include refid="GameMetricFilterWhere"/>
|
||||
</select>
|
||||
|
||||
<select id="listGamePeriodMetrics"
|
||||
resultType="com.red.circle.console.infra.database.rds.dto.datav.CountryDashboardGameMetricDTO">
|
||||
SELECT
|
||||
t.period_key AS periodKey,
|
||||
t.period_name AS periodName,
|
||||
t.stat_timezone AS statTimezone,
|
||||
t.country_code AS countryCode,
|
||||
t.country_name AS countryName,
|
||||
t.game_provider AS gameProvider,
|
||||
t.game_id AS gameId,
|
||||
t.game_name AS gameName,
|
||||
t.consume_amount AS consumeAmount,
|
||||
t.payout_amount AS payoutAmount,
|
||||
t.profit_amount AS profitAmount,
|
||||
t.user_count AS userCount,
|
||||
t.order_count AS orderCount,
|
||||
t.refreshed_at AS refreshedAt
|
||||
FROM country_dashboard_game_period_metric t
|
||||
<include refid="GameMetricFilterWhere"/>
|
||||
<choose>
|
||||
<when test="sortColumn != null and sortColumn != ''">
|
||||
ORDER BY ${sortColumn} ${sortDirection}, t.period_key DESC, t.country_code ASC, t.game_provider ASC, t.game_id ASC
|
||||
</when>
|
||||
<otherwise>
|
||||
ORDER BY t.period_key DESC, t.consume_amount DESC, t.country_code ASC, t.game_provider ASC, t.game_id ASC
|
||||
</otherwise>
|
||||
</choose>
|
||||
LIMIT #{limit} OFFSET #{offset}
|
||||
</select>
|
||||
|
||||
<select id="countGameSummaryMetrics" resultType="java.lang.Long">
|
||||
SELECT COUNT(1)
|
||||
FROM (
|
||||
SELECT 1
|
||||
FROM country_dashboard_game_period_metric t
|
||||
<include refid="GameMetricFilterWhere"/>
|
||||
GROUP BY t.game_provider, t.game_id
|
||||
) summary
|
||||
</select>
|
||||
|
||||
<select id="listGameSummaryMetrics"
|
||||
resultType="com.red.circle.console.infra.database.rds.dto.datav.CountryDashboardGameMetricDTO">
|
||||
SELECT
|
||||
MAX(t.period_key) AS periodKey,
|
||||
MAX(t.period_name) AS periodName,
|
||||
MAX(t.stat_timezone) AS statTimezone,
|
||||
MAX(t.game_provider) AS gameProvider,
|
||||
t.game_id AS gameId,
|
||||
MAX(t.game_name) AS gameName,
|
||||
COALESCE(SUM(t.consume_amount), 0) AS consumeAmount,
|
||||
COALESCE(SUM(t.payout_amount), 0) AS payoutAmount,
|
||||
COALESCE(SUM(t.profit_amount), 0) AS profitAmount,
|
||||
COALESCE(SUM(t.user_count), 0) AS userCount,
|
||||
COALESCE(SUM(t.order_count), 0) AS orderCount,
|
||||
MAX(t.refreshed_at) AS refreshedAt
|
||||
FROM country_dashboard_game_period_metric t
|
||||
<include refid="GameMetricFilterWhere"/>
|
||||
GROUP BY t.game_provider, t.game_id
|
||||
<choose>
|
||||
<when test="sortColumn != null and sortColumn != ''">
|
||||
ORDER BY ${sortColumn} ${sortDirection}, gameProvider ASC, gameId ASC
|
||||
</when>
|
||||
<otherwise>
|
||||
ORDER BY consumeAmount DESC, gameProvider ASC, gameId ASC
|
||||
</otherwise>
|
||||
</choose>
|
||||
LIMIT #{limit} OFFSET #{offset}
|
||||
</select>
|
||||
|
||||
<select id="listGameCountryRankMetrics"
|
||||
resultType="com.red.circle.console.infra.database.rds.dto.datav.CountryDashboardGameMetricDTO">
|
||||
SELECT
|
||||
MAX(t.period_key) AS periodKey,
|
||||
MAX(t.period_name) AS periodName,
|
||||
MAX(t.stat_timezone) AS statTimezone,
|
||||
t.country_code AS countryCode,
|
||||
MAX(t.country_name) AS countryName,
|
||||
MAX(t.game_provider) AS gameProvider,
|
||||
MAX(t.game_id) AS gameId,
|
||||
MAX(t.game_name) AS gameName,
|
||||
COALESCE(SUM(t.consume_amount), 0) AS consumeAmount,
|
||||
COALESCE(SUM(t.payout_amount), 0) AS payoutAmount,
|
||||
COALESCE(SUM(t.profit_amount), 0) AS profitAmount,
|
||||
COALESCE(SUM(t.user_count), 0) AS userCount,
|
||||
COALESCE(SUM(t.order_count), 0) AS orderCount,
|
||||
MAX(t.refreshed_at) AS refreshedAt
|
||||
FROM country_dashboard_game_period_metric t
|
||||
<include refid="GameMetricFilterWhere"/>
|
||||
AND t.game_provider = #{gameProvider}
|
||||
AND t.game_id = #{gameId}
|
||||
GROUP BY t.country_code
|
||||
ORDER BY consumeAmount DESC, countryCode ASC
|
||||
LIMIT #{limit}
|
||||
</select>
|
||||
|
||||
<select id="listNewUserCohorts"
|
||||
resultType="com.red.circle.console.infra.database.rds.dto.datav.CountryDashboardRetentionUserDTO">
|
||||
SELECT
|
||||
|
||||
@ -9,13 +9,13 @@ import org.springframework.context.annotation.Configuration;
|
||||
@Configuration
|
||||
public class UndertowUploadLimitConfiguration {
|
||||
|
||||
private static final long MAX_UPLOAD_SIZE_BYTES = 20L * 1024 * 1024;
|
||||
private static final long MAX_UPLOAD_REQUEST_SIZE_BYTES = 520L * 1024 * 1024;
|
||||
|
||||
@Bean
|
||||
public WebServerFactoryCustomizer<ConfigurableUndertowWebServerFactory> undertowUploadLimitCustomizer() {
|
||||
return factory -> factory.addBuilderCustomizers(builder -> {
|
||||
builder.setServerOption(UndertowOptions.MAX_ENTITY_SIZE, MAX_UPLOAD_SIZE_BYTES);
|
||||
builder.setServerOption(UndertowOptions.MULTIPART_MAX_ENTITY_SIZE, MAX_UPLOAD_SIZE_BYTES);
|
||||
builder.setServerOption(UndertowOptions.MAX_ENTITY_SIZE, MAX_UPLOAD_REQUEST_SIZE_BYTES);
|
||||
builder.setServerOption(UndertowOptions.MULTIPART_MAX_ENTITY_SIZE, MAX_UPLOAD_REQUEST_SIZE_BYTES);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@ -35,6 +35,18 @@ public class LocalDefaultRequestHeaderFilter extends OncePerRequestFilter {
|
||||
Map.entry(RequestHeaderConstant.REQ_LANG, "zh-CN"),
|
||||
Map.entry(RequestHeaderConstant.REQ_ZONE, "Asia/Shanghai")
|
||||
);
|
||||
private static final Set<String> SUPPRESSED_HEADERS = Set.of(
|
||||
"connection",
|
||||
"content-length",
|
||||
"host",
|
||||
"keep-alive",
|
||||
"proxy-authenticate",
|
||||
"proxy-authorization",
|
||||
"te",
|
||||
"trailer",
|
||||
"transfer-encoding",
|
||||
"upgrade"
|
||||
);
|
||||
|
||||
@Override
|
||||
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
|
||||
@ -45,13 +57,24 @@ public class LocalDefaultRequestHeaderFilter extends OncePerRequestFilter {
|
||||
missingHeaders.put(header.getKey(), header.getValue());
|
||||
}
|
||||
}
|
||||
if (missingHeaders.isEmpty()) {
|
||||
boolean hasSuppressedHeader = hasSuppressedHeader(request);
|
||||
if (missingHeaders.isEmpty() && !hasSuppressedHeader) {
|
||||
filterChain.doFilter(request, response);
|
||||
return;
|
||||
}
|
||||
filterChain.doFilter(new LocalHeaderRequestWrapper(request, missingHeaders), response);
|
||||
}
|
||||
|
||||
private boolean hasSuppressedHeader(HttpServletRequest request) {
|
||||
Enumeration<String> headerNames = request.getHeaderNames();
|
||||
while (headerNames.hasMoreElements()) {
|
||||
if (SUPPRESSED_HEADERS.contains(headerNames.nextElement().toLowerCase())) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static final class LocalHeaderRequestWrapper extends HttpServletRequestWrapper {
|
||||
|
||||
private final Map<String, String> headers;
|
||||
@ -63,12 +86,18 @@ public class LocalDefaultRequestHeaderFilter extends OncePerRequestFilter {
|
||||
|
||||
@Override
|
||||
public String getHeader(String name) {
|
||||
if (isSuppressedHeader(name)) {
|
||||
return null;
|
||||
}
|
||||
String header = headers.get(name);
|
||||
return header != null ? header : super.getHeader(name);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Enumeration<String> getHeaders(String name) {
|
||||
if (isSuppressedHeader(name)) {
|
||||
return Collections.emptyEnumeration();
|
||||
}
|
||||
String header = headers.get(name);
|
||||
if (header != null) {
|
||||
return Collections.enumeration(List.of(header));
|
||||
@ -81,10 +110,17 @@ public class LocalDefaultRequestHeaderFilter extends OncePerRequestFilter {
|
||||
Set<String> headerNames = new LinkedHashSet<>();
|
||||
Enumeration<String> originalHeaderNames = super.getHeaderNames();
|
||||
while (originalHeaderNames.hasMoreElements()) {
|
||||
headerNames.add(originalHeaderNames.nextElement());
|
||||
String headerName = originalHeaderNames.nextElement();
|
||||
if (!isSuppressedHeader(headerName)) {
|
||||
headerNames.add(headerName);
|
||||
}
|
||||
}
|
||||
headerNames.addAll(headers.keySet());
|
||||
return Collections.enumeration(headerNames);
|
||||
}
|
||||
|
||||
private static boolean isSuppressedHeader(String name) {
|
||||
return name != null && SUPPRESSED_HEADERS.contains(name.toLowerCase());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -4,7 +4,7 @@ server:
|
||||
servlet:
|
||||
context-path: /console
|
||||
undertow:
|
||||
max-http-post-size: 20MB
|
||||
max-http-post-size: 520MB
|
||||
no-request-timeout: 60000
|
||||
|
||||
spring:
|
||||
@ -19,9 +19,9 @@ spring:
|
||||
dev: dev,web-framework,openfeign,mybatis-plus
|
||||
prod: prod,web-framework,openfeign,mybatis-plus
|
||||
servlet:
|
||||
multipart:
|
||||
max-file-size: 20MB
|
||||
max-request-size: 20MB
|
||||
multipart:
|
||||
max-file-size: 500MB
|
||||
max-request-size: 520MB
|
||||
enabled: true
|
||||
---
|
||||
spring:
|
||||
@ -74,4 +74,7 @@ red-circle:
|
||||
- /console/datav/online/room/count
|
||||
- /console/datav/country-dashboard
|
||||
- /console/datav/country-dashboard/recharge-details
|
||||
- /console/datav/country-dashboard/game-metrics
|
||||
- /console/datav/country-dashboard/game-metrics/games
|
||||
- /console/datav/country-dashboard/game-metrics/country-rank
|
||||
- /console/user/base/info/im/sig
|
||||
|
||||
@ -0,0 +1,314 @@
|
||||
# Admin 数据大屏游戏数据表技术方案
|
||||
|
||||
## 目标
|
||||
|
||||
在新 admin 的 `统计管理 / 数据大屏` 中增加独立的“游戏数据”表,用于按日期、国家、游戏维度查看消耗和收益数据。
|
||||
|
||||
页面查询不能直接扫描 `likei_wallet.wallet_gold_asset_record_1..12`、第三方回调日志或游戏明细表。所有 admin 查询默认读取游戏维度预聚合表;只有后台刷新任务和手动回填任务允许按时间窗口扫描原始数据。
|
||||
|
||||
## 现状
|
||||
|
||||
当前国家数据大屏入口:
|
||||
|
||||
- 前端路由:`chatapp3-admin/apps/src/router/routes/modules/statistics.ts`
|
||||
- 前端页面:`chatapp3-admin/apps/src/views/statistics/datav.vue`
|
||||
- 前端接口:`chatapp3-admin/apps/src/api/legacy/datav.ts`
|
||||
- 后端 Controller:`DatavRestController`
|
||||
- 后端 Service:`CountryDashboardServiceImpl`
|
||||
- 后端 Mapper:`CountryDashboardDAO.xml`
|
||||
- 现有查询文档:`chatapp3-java/rc-service/rc-service-console/docs/country-dashboard-data-query.md`
|
||||
|
||||
现有大屏已经聚合了国家维度的 `game_total_flow`、`game_payout`、`game_user`,但没有保留游戏维度。新增功能不改动现有总览口径,新增一套游戏维度查询和预聚合表。
|
||||
|
||||
## 页面设计
|
||||
|
||||
在 `datav.vue` 的表格区域增加第三个 tab:
|
||||
|
||||
- `指标总览`
|
||||
- `充值相关数据`
|
||||
- `游戏数据`
|
||||
|
||||
“游戏数据”表复用页面已有筛选项:
|
||||
|
||||
- 系统:`sysOrigin`
|
||||
- 统计时区:`statTimezone`
|
||||
- 周期:`periodType`
|
||||
- 日期范围:`startDate/endDate`
|
||||
- 国家多选:`countryCodes`
|
||||
|
||||
新增游戏筛选项:
|
||||
|
||||
- `gameProvider`:游戏厂商,支持 `INTERNAL`、`BAISHUN`、`GAME_OPEN`、`LINGXIAN`,可为空。
|
||||
- `gameKeyword`:游戏 ID 或游戏名称模糊搜索。
|
||||
|
||||
表格字段:
|
||||
|
||||
| 字段 | 说明 |
|
||||
| --- | --- |
|
||||
| 日期/周期 | `periodKey`,DAY 显示日期,WEEK/MONTH/ALL 显示周期 |
|
||||
| 国家 | `countryCode / countryName` |
|
||||
| 游戏厂商 | `gameProvider` |
|
||||
| 游戏 ID | `gameId` |
|
||||
| 游戏名称 | `gameName` |
|
||||
| 消耗 | 用户玩游戏支出,现有钱包口径为 `type = 1` |
|
||||
| 返奖 | 游戏返还给用户,现有钱包口径为 `type = 0` |
|
||||
| 收益 | `consumeAmount - payoutAmount` |
|
||||
| 返奖率 | `payoutAmount / consumeAmount` |
|
||||
| 收益率 | `profitAmount / consumeAmount` |
|
||||
| 游戏用户数 | 周期内去重用户数 |
|
||||
| 流水笔数 | 周期内消耗和返奖记录数 |
|
||||
| 更新时间 | 预聚合刷新时间 |
|
||||
|
||||
表格必须服务端分页,默认 `limit=50`,最大 `limit=200`。
|
||||
|
||||
## 后端接口
|
||||
|
||||
新增接口:
|
||||
|
||||
```http
|
||||
GET /console/datav/country-dashboard/game-metrics
|
||||
```
|
||||
|
||||
请求参数:
|
||||
|
||||
| 参数 | 必填 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| `sysOrigin` | 是 | 系统来源 |
|
||||
| `periodType` | 是 | `DAY` / `WEEK` / `MONTH` / `ALL` |
|
||||
| `startDate` | DAY/WEEK/MONTH 必填 | 查询开始日期 |
|
||||
| `endDate` | DAY/WEEK/MONTH 必填 | 查询结束日期 |
|
||||
| `statTimezone` | 否 | 默认 `Asia/Riyadh` |
|
||||
| `countryCodes` | 否 | 多国家码,逗号分隔 |
|
||||
| `countryKeyword` | 否 | 国家码或国家名搜索 |
|
||||
| `gameProvider` | 否 | 游戏厂商 |
|
||||
| `gameKeyword` | 否 | 游戏 ID 或名称搜索 |
|
||||
| `cursor` | 否 | 页码,默认 1 |
|
||||
| `limit` | 否 | 页大小,默认 50,最大 200 |
|
||||
| `sortField` | 否 | 允许 `consumeAmount/payoutAmount/profitAmount/userCount/orderCount` |
|
||||
| `sortOrder` | 否 | `ascend/descend` |
|
||||
|
||||
响应结构:
|
||||
|
||||
```json
|
||||
{
|
||||
"current": 1,
|
||||
"size": 50,
|
||||
"total": 120,
|
||||
"records": [
|
||||
{
|
||||
"periodKey": "2026-05-12",
|
||||
"periodName": "2026-05-12",
|
||||
"statTimezone": "Asia/Riyadh",
|
||||
"countryCode": "SA",
|
||||
"countryName": "Saudi Arabia",
|
||||
"gameProvider": "BAISHUN",
|
||||
"gameId": "1067",
|
||||
"gameName": "example game",
|
||||
"consumeAmount": "1000.00",
|
||||
"payoutAmount": "720.00",
|
||||
"profitAmount": "280.00",
|
||||
"payoutRate": "72.00%",
|
||||
"profitRate": "28.00%",
|
||||
"userCount": 86,
|
||||
"orderCount": 320,
|
||||
"refreshedAt": "2026-05-12 10:20:00"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
后端新增对象建议:
|
||||
|
||||
- `CountryDashboardGameMetricQryCmd`
|
||||
- `CountryDashboardGameMetricCO`
|
||||
- `CountryDashboardGameMetricDTO`
|
||||
- `CountryDashboardGameMetricService`
|
||||
- `CountryDashboardGameMetricDAO`
|
||||
- `CountryDashboardGameMetricDAO.xml`
|
||||
|
||||
## 预聚合表
|
||||
|
||||
### `country_dashboard_game_period_metric`
|
||||
|
||||
页面直接查询这张表。
|
||||
|
||||
```sql
|
||||
CREATE TABLE IF NOT EXISTS `country_dashboard_game_period_metric` (
|
||||
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键',
|
||||
`period_type` varchar(16) NOT NULL COMMENT '周期类型:DAY/WEEK/MONTH/ALL',
|
||||
`period_key` varchar(32) NOT NULL COMMENT '周期键',
|
||||
`stat_timezone` varchar(64) NOT NULL DEFAULT 'Asia/Riyadh' COMMENT '统计时区',
|
||||
`period_name` varchar(64) NOT NULL DEFAULT '' COMMENT '周期名称',
|
||||
`period_start_date` date DEFAULT NULL COMMENT '周期开始日期',
|
||||
`period_end_date` date DEFAULT NULL COMMENT '周期结束日期,开区间',
|
||||
`sys_origin` varchar(32) NOT NULL DEFAULT '' COMMENT '系统来源',
|
||||
`country_code` varchar(64) NOT NULL DEFAULT 'UNKNOWN' COMMENT '国家码',
|
||||
`country_name` varchar(128) NOT NULL DEFAULT 'UNKNOWN' COMMENT '国家名称',
|
||||
`game_provider` varchar(32) NOT NULL DEFAULT 'UNKNOWN' COMMENT '游戏厂商',
|
||||
`game_id` varchar(64) NOT NULL DEFAULT 'UNKNOWN' COMMENT '游戏ID',
|
||||
`game_name` varchar(128) 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` (`period_type`, `period_key`, `stat_timezone`, `sys_origin`, `country_code`, `game_provider`, `game_id`),
|
||||
KEY `idx_game_period_query` (`sys_origin`, `stat_timezone`, `period_type`, `period_key`, `country_code`),
|
||||
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='国家数据大屏游戏维度周期预聚合';
|
||||
```
|
||||
|
||||
### `country_dashboard_game_period_user_metric`
|
||||
|
||||
用于 `WEEK/MONTH/ALL` 去重用户数,避免直接把日用户数相加导致重复。
|
||||
|
||||
```sql
|
||||
CREATE TABLE IF NOT EXISTS `country_dashboard_game_period_user_metric` (
|
||||
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键',
|
||||
`period_type` varchar(16) NOT NULL COMMENT '周期类型:DAY/WEEK/MONTH/ALL',
|
||||
`period_key` varchar(32) NOT NULL COMMENT '周期键',
|
||||
`stat_timezone` varchar(64) NOT NULL DEFAULT 'Asia/Riyadh' COMMENT '统计时区',
|
||||
`period_name` varchar(64) NOT NULL DEFAULT '' COMMENT '周期名称',
|
||||
`period_start_date` date DEFAULT NULL COMMENT '周期开始日期',
|
||||
`period_end_date` date DEFAULT NULL COMMENT '周期结束日期,开区间',
|
||||
`sys_origin` varchar(32) NOT NULL DEFAULT '' COMMENT '系统来源',
|
||||
`country_code` varchar(64) NOT NULL DEFAULT 'UNKNOWN' COMMENT '国家码',
|
||||
`country_name` varchar(128) NOT NULL DEFAULT 'UNKNOWN' COMMENT '国家名称',
|
||||
`game_provider` varchar(32) NOT NULL DEFAULT 'UNKNOWN' COMMENT '游戏厂商',
|
||||
`game_id` varchar(64) 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='国家数据大屏游戏维度周期用户去重明细';
|
||||
```
|
||||
|
||||
## 数据来源和游戏识别
|
||||
|
||||
以钱包流水作为财务主口径:
|
||||
|
||||
- `wallet_gold_asset_record_1..12.type = 1`:消耗
|
||||
- `wallet_gold_asset_record_1..12.type = 0`:返奖
|
||||
- 金额沿用现有大屏口径:`penny_amount / 100.00`
|
||||
- `LUCKY_GIFT`、`LUCKY_GIFT_GOLD_REWARD` 继续归属幸运礼物模块,不并入游戏数据表。
|
||||
|
||||
游戏维度来源:
|
||||
|
||||
| 来源 | 识别方式 | 名称补全 |
|
||||
| --- | --- | --- |
|
||||
| 百顺 | 钱包 `event_id = baishun_order_idempotency.wallet_event_id`,取 `vendor_game_id`;兜底解析 `event_type = BAISHUN_GAME_{gameId}` | `baishun_game_catalog.name` |
|
||||
| 统一三方游戏 | 钱包 `event_id = CONCAT('GAME_OPEN:', game_open_callback_log.order_id)`,取 `game_open_callback_log.game_id`;兜底解析 `event_type = GAME_OPEN_{gameId}` | `lingxian_game_catalog.name` 或 `sys_game_list_config.name` |
|
||||
| Lucky Box | `game_lucky_box_count` 固定为 `gameProvider=INTERNAL, gameId=LUCKY_BOX` | 固定名称 `Lucky Box` |
|
||||
| 其他站内游戏 | 按 `event_type` 映射稳定 `gameId`,例如 `FRUIT_GAME/GAME_FRUIT_* -> FRUIT_GAME`、`LUDO_* -> LUDO_GAME` | 代码内映射或后续配置表 |
|
||||
|
||||
如果某条流水无法识别具体游戏,落到:
|
||||
|
||||
- `gameProvider = UNKNOWN`
|
||||
- `gameId = event_type`
|
||||
- `gameName = event_type`
|
||||
|
||||
这样可以保证金额不丢失,同时便于后续补映射。
|
||||
|
||||
## 刷新和回填
|
||||
|
||||
新增刷新服务建议为 `CountryDashboardGameMetricService`。
|
||||
|
||||
默认任务:
|
||||
|
||||
- 每 10 分钟刷新最近 1 天。
|
||||
- 每天 00:30 刷新最近 8 天。
|
||||
- 支持手动回填,单次最大 366 天,和现有国家大屏保持一致。
|
||||
|
||||
刷新逻辑:
|
||||
|
||||
1. 按 `sysOrigin` 和 `statTimezone` 计算周期窗口,转成存储时区时间窗口。
|
||||
2. 扫描窗口内原始游戏流水,按 `period + country + gameProvider + gameId` 聚合金额和流水笔数。
|
||||
3. 删除对应周期的 `country_dashboard_game_period_user_metric`,写入去重用户明细。
|
||||
4. 从用户明细聚合 `user_count`。
|
||||
5. 删除并写入对应周期的 `country_dashboard_game_period_metric`。
|
||||
6. `ALL` 周期的金额从 `DAY` 周期表汇总,用户数从日级用户明细去重汇总。
|
||||
|
||||
刷新任务失败只影响游戏数据表,不影响现有国家大屏总览。
|
||||
|
||||
## 查询性能要求
|
||||
|
||||
页面查询只能访问 `country_dashboard_game_period_metric`,禁止在请求链路访问以下原始表:
|
||||
|
||||
- `likei_wallet.wallet_gold_asset_record_1..12`
|
||||
- `game_open_callback_log`
|
||||
- `baishun_order_idempotency`
|
||||
- `game_lucky_box_count`
|
||||
- 第三方游戏 catalog 表
|
||||
|
||||
分页查询必须满足:
|
||||
|
||||
- `sysOrigin` 必传。
|
||||
- `periodType`、`statTimezone` 必须落到联合索引前缀。
|
||||
- `DAY/WEEK/MONTH` 查询必须带日期范围,并转成 `period_key` 或 `period_start_date` 范围。
|
||||
- `ALL` 只查 `period_type='ALL' AND period_key='ALL'`,不回退全历史实时扫描。
|
||||
- `countryKeyword` 和 `gameKeyword` 只允许在已经命中周期范围之后过滤;大范围模糊搜索需要限制日期范围。
|
||||
- 只允许白名单排序字段,避免动态 SQL 注入和无法使用索引的任意排序。
|
||||
- `limit` 上限 200。
|
||||
|
||||
刷新任务扫描原始钱包分表时必须带:
|
||||
|
||||
- `sys_origin`
|
||||
- `create_time` 毫秒闭开区间
|
||||
- 游戏 `event_type` 白名单或前缀条件
|
||||
|
||||
现有 wallet 分表有 `idx_sorigin_etype_ctime(sys_origin,event_type,create_time)` 和 `idx_event(event_id)`,刷新 SQL 应优先利用这两个索引。不能对 `create_time` 列包函数做过滤,只能在 SELECT 或 GROUP BY 阶段转换时间。
|
||||
|
||||
## 实施步骤
|
||||
|
||||
1. 新增 SQL 迁移:创建 `country_dashboard_game_period_metric` 和 `country_dashboard_game_period_user_metric`。
|
||||
2. 新增后端 DTO/Cmd/CO/DAO/Service,并在 `DatavRestController` 暴露 `/country-dashboard/game-metrics`。
|
||||
3. 新增刷新任务和手动回填入口,复用现有 `red-circle.country-dashboard.refresh-sys-origins`、`refresh-timezones` 配置。
|
||||
4. 新 admin 新增接口方法 `countryDashboardGameMetrics`。
|
||||
5. `datav.vue` 增加“游戏数据”tab、游戏筛选项和分页表格。
|
||||
6. 首次上线后先回填最近 8 天,再回填历史需要展示的范围。
|
||||
|
||||
## 验证方案
|
||||
|
||||
SQL 验证:
|
||||
|
||||
```sql
|
||||
EXPLAIN
|
||||
SELECT *
|
||||
FROM country_dashboard_game_period_metric
|
||||
WHERE sys_origin = 'LIKEI'
|
||||
AND stat_timezone = 'Asia/Riyadh'
|
||||
AND period_type = 'DAY'
|
||||
AND period_key BETWEEN '2026-05-01' AND '2026-05-12'
|
||||
ORDER BY consume_amount DESC
|
||||
LIMIT 50;
|
||||
```
|
||||
|
||||
预期使用 `idx_game_period_query` 或 `idx_game_period_sort`,不能全表扫描。
|
||||
|
||||
数据口径验证:
|
||||
|
||||
```sql
|
||||
SELECT SUM(consume_amount), SUM(payout_amount)
|
||||
FROM country_dashboard_game_period_metric
|
||||
WHERE sys_origin = 'LIKEI'
|
||||
AND stat_timezone = 'Asia/Riyadh'
|
||||
AND period_type = 'DAY'
|
||||
AND period_key = '2026-05-12';
|
||||
```
|
||||
|
||||
对比现有 `country_dashboard_period_metric` 同日同国家汇总的 `game_total_flow/game_payout`,差异应只来自“无法识别但仍落 UNKNOWN”的映射归类,不应出现金额丢失。
|
||||
|
||||
功能验证:
|
||||
|
||||
- 查询 DAY 日期范围,表格按日期、国家、游戏拆分。
|
||||
- 查询 WEEK/MONTH,用户数为周期内去重,不是日用户数累加。
|
||||
- 查询 ALL,不触发实时原始流水扫描。
|
||||
- 国家筛选、游戏厂商筛选、游戏关键字筛选、分页、排序都能返回稳定结果。
|
||||
- 刷新任务失败时,现有数据大屏总览接口不受影响。
|
||||
@ -13,7 +13,17 @@ RUN apt-get update \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
WORKDIR /application
|
||||
COPY external-start/target/rc-service-external-*.jar /application/service.jar
|
||||
COPY external-start/target/rc-service-external-*.jar /tmp/service-jars/
|
||||
RUN set -e; \
|
||||
jars="$(find /tmp/service-jars -maxdepth 1 -type f -name 'rc-service-external-*.jar' | sort)"; \
|
||||
count="$(printf '%s\n' "$jars" | grep -c . || true)"; \
|
||||
if [ "$count" -ne 1 ]; then \
|
||||
echo "Expected exactly one rc-service-external jar, found $count:"; \
|
||||
ls -la /tmp/service-jars; \
|
||||
exit 1; \
|
||||
fi; \
|
||||
cp "$jars" /application/service.jar \
|
||||
&& rm -rf /tmp/service-jars
|
||||
|
||||
CMD exec java -Djava.security.egd=file:/dev/./urandom \
|
||||
-Dnacos.remote.client.grpc.timeout=6000 \
|
||||
|
||||
@ -31,7 +31,7 @@ import java.util.UUID;
|
||||
public class OssRestController {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(OssRestController.class);
|
||||
private static final long MAX_UPLOAD_SIZE_BYTES = 20L * 1024 * 1024;
|
||||
private static final long MAX_UPLOAD_SIZE_BYTES = 100L * 1024 * 1024;
|
||||
|
||||
private final AliYunStsService aliYunStsService;
|
||||
private final OssService ossService;
|
||||
@ -78,7 +78,7 @@ public class OssRestController {
|
||||
return ResultResponse.failure(OssErrorCode.FILE_IS_NULL);
|
||||
}
|
||||
if (file.getSize() > MAX_UPLOAD_SIZE_BYTES) {
|
||||
return ResultResponse.failure(OssErrorCode.FILE_SIZE_GREATER_THAN_20M);
|
||||
return ResultResponse.failure(OssErrorCode.FILE_SIZE_GREATER_THAN_100M);
|
||||
}
|
||||
String name = file.getOriginalFilename().toLowerCase();
|
||||
if(!FileUtils.fileContentType(name)) {
|
||||
@ -123,7 +123,7 @@ public class OssRestController {
|
||||
return ResultResponse.failure(OssErrorCode.FILE_IS_NULL);
|
||||
}
|
||||
if (file.getSize() > MAX_UPLOAD_SIZE_BYTES) {
|
||||
return ResultResponse.failure(OssErrorCode.FILE_SIZE_GREATER_THAN_20M);
|
||||
return ResultResponse.failure(OssErrorCode.FILE_SIZE_GREATER_THAN_100M);
|
||||
}
|
||||
String storageKey = buildStorageKey(dir, filename, file.getOriginalFilename());
|
||||
if (tencentCosStorageService.isEnabled()) {
|
||||
|
||||
@ -0,0 +1,16 @@
|
||||
package com.red.circle.external.adapter.app.oss;
|
||||
|
||||
import com.red.circle.external.response.OssErrorCode;
|
||||
import com.red.circle.framework.dto.ResultResponse;
|
||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
||||
import org.springframework.web.multipart.MaxUploadSizeExceededException;
|
||||
|
||||
@RestControllerAdvice
|
||||
public class OssUploadExceptionHandler {
|
||||
|
||||
@ExceptionHandler(MaxUploadSizeExceededException.class)
|
||||
public ResultResponse<Void> handleMaxUploadSizeExceeded(MaxUploadSizeExceededException e) {
|
||||
return ResultResponse.failure(OssErrorCode.FILE_SIZE_GREATER_THAN_100M);
|
||||
}
|
||||
}
|
||||
@ -14,8 +14,6 @@ import org.slf4j.LoggerFactory;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.URI;
|
||||
import java.util.UUID;
|
||||
@ -48,18 +46,14 @@ public class TencentCosStorageService {
|
||||
public void upload(String key, InputStream inputStream, long contentLength, String contentType) {
|
||||
COSClient client = buildClient();
|
||||
try {
|
||||
byte[] data = inputStream.readAllBytes();
|
||||
ObjectMetadata metadata = new ObjectMetadata();
|
||||
metadata.setContentLength(data.length);
|
||||
metadata.setContentLength(contentLength);
|
||||
if (StringUtils.hasText(contentType)) {
|
||||
metadata.setContentType(contentType);
|
||||
}
|
||||
client.putObject(
|
||||
new PutObjectRequest(
|
||||
properties.getBucketName(), key, new ByteArrayInputStream(data), metadata));
|
||||
} catch (IOException e) {
|
||||
log.error("tencent cos read upload stream failed key={}", key, e);
|
||||
throw new IllegalStateException("read upload stream failed", e);
|
||||
properties.getBucketName(), key, inputStream, metadata));
|
||||
} catch (Exception e) {
|
||||
log.error("tencent cos upload failed key={}", key, e);
|
||||
throw e;
|
||||
|
||||
@ -5,7 +5,7 @@ import com.red.circle.framework.dto.IResponseErrorCode;
|
||||
public enum OssErrorCode implements IResponseErrorCode {
|
||||
FILE_IS_NULL(20001, "file is null"),
|
||||
FILE_TYPE_NOT_SUPPORTED(20003, "File type not supported"),
|
||||
FILE_SIZE_GREATER_THAN_20M(20003, "File size greater than 20M"),
|
||||
FILE_SIZE_GREATER_THAN_100M(20003, "File size greater than 100M"),
|
||||
NETWORK_ANOMALY(20004, "network anomaly");
|
||||
|
||||
private final int code;
|
||||
|
||||
@ -0,0 +1,38 @@
|
||||
package com.red.circle.external.infra.config;
|
||||
|
||||
import com.red.circle.component.instant.msg.InstantMessageProperties;
|
||||
import com.red.circle.component.instant.msg.tencet.props.TrtcTencetProperties;
|
||||
import com.red.circle.component.instant.msg.tencet.service.endpoint.api.trtc.HyTrtcClient;
|
||||
import com.tencentcloudapi.common.Credential;
|
||||
import com.tencentcloudapi.common.profile.ClientProfile;
|
||||
import com.tencentcloudapi.common.profile.HttpProfile;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Primary;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
@Configuration
|
||||
public class TencentTrtcClientRegionConfiguration {
|
||||
|
||||
private static final String DEFAULT_TRTC_REGION = "ap-singapore";
|
||||
|
||||
@Bean
|
||||
@Primary
|
||||
public HyTrtcClient regionAwareHyTrtcClient(
|
||||
InstantMessageProperties instantMessageProperties,
|
||||
@Value("${red-circle.instant-message.tencet-trtc.region:" + DEFAULT_TRTC_REGION + "}")
|
||||
String region) {
|
||||
TrtcTencetProperties trtcProperties = instantMessageProperties.getTencetTrtc();
|
||||
Credential credential = new Credential(
|
||||
trtcProperties.getSecretId(),
|
||||
trtcProperties.getSecretKey()
|
||||
);
|
||||
HttpProfile httpProfile = new HttpProfile();
|
||||
httpProfile.setEndpoint(trtcProperties.getEndpoint());
|
||||
ClientProfile clientProfile = new ClientProfile();
|
||||
clientProfile.setHttpProfile(httpProfile);
|
||||
String resolvedRegion = StringUtils.hasText(region) ? region.trim() : DEFAULT_TRTC_REGION;
|
||||
return new HyTrtcClient(credential, resolvedRegion, clientProfile);
|
||||
}
|
||||
}
|
||||
@ -9,13 +9,13 @@ import org.springframework.context.annotation.Configuration;
|
||||
@Configuration
|
||||
public class UndertowUploadLimitConfiguration {
|
||||
|
||||
private static final long MAX_UPLOAD_SIZE_BYTES = 20L * 1024 * 1024;
|
||||
private static final long MAX_UPLOAD_REQUEST_SIZE_BYTES = 105L * 1024 * 1024;
|
||||
|
||||
@Bean
|
||||
public WebServerFactoryCustomizer<ConfigurableUndertowWebServerFactory> undertowUploadLimitCustomizer() {
|
||||
return factory -> factory.addBuilderCustomizers(builder -> {
|
||||
builder.setServerOption(UndertowOptions.MAX_ENTITY_SIZE, MAX_UPLOAD_SIZE_BYTES);
|
||||
builder.setServerOption(UndertowOptions.MULTIPART_MAX_ENTITY_SIZE, MAX_UPLOAD_SIZE_BYTES);
|
||||
builder.setServerOption(UndertowOptions.MAX_ENTITY_SIZE, MAX_UPLOAD_REQUEST_SIZE_BYTES);
|
||||
builder.setServerOption(UndertowOptions.MULTIPART_MAX_ENTITY_SIZE, MAX_UPLOAD_REQUEST_SIZE_BYTES);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@ -2,7 +2,7 @@ server:
|
||||
port: 3000
|
||||
shutdown: graceful
|
||||
undertow:
|
||||
max-http-post-size: 20MB
|
||||
max-http-post-size: 105MB
|
||||
no-request-timeout: 60000
|
||||
|
||||
spring:
|
||||
@ -12,8 +12,8 @@ spring:
|
||||
allow-bean-definition-overriding: true
|
||||
servlet:
|
||||
multipart:
|
||||
max-file-size: 20MB
|
||||
max-request-size: 20MB
|
||||
max-file-size: 100MB
|
||||
max-request-size: 105MB
|
||||
enabled: true
|
||||
profiles:
|
||||
active: @profiles.active@
|
||||
|
||||
@ -213,6 +213,9 @@ public class UserHeartbeatListener implements MessageListener {
|
||||
log.warn("没有找到房间ID: {}", event.getSessionId());
|
||||
return;
|
||||
}
|
||||
if (Objects.nonNull(event.getSysOrigin())) {
|
||||
roomProfile.setSysOrigin(event.getSysOrigin().name());
|
||||
}
|
||||
|
||||
UserProfileDTO actualUserProfile = ensureUserProfile(userProfile);
|
||||
AgoraChannelStateDTO channelState = agoraRoomStateService.getChannelState(roomId);
|
||||
|
||||
@ -1,7 +1,9 @@
|
||||
package com.red.circle.live.infra.client;
|
||||
|
||||
import com.alibaba.fastjson.JSON;
|
||||
import com.red.circle.component.mq.MessageEvent;
|
||||
import com.red.circle.live.domain.constant.ActiveSecondConstant;
|
||||
import com.red.circle.mq.rocket.business.service.MessageSenderService;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.net.HttpURLConnection;
|
||||
@ -14,6 +16,7 @@ import java.util.Objects;
|
||||
import lombok.Data;
|
||||
import lombok.experimental.Accessors;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.ObjectProvider;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@ -23,6 +26,8 @@ public class TaskCenterGoClient {
|
||||
|
||||
private static final String EVENT_TYPE_MIC_DURATION_SECONDS = "MIC_DURATION_SECONDS";
|
||||
|
||||
private final ObjectProvider<MessageSenderService> messageSenderServiceProvider;
|
||||
|
||||
@Value("${task.center.go.enabled:true}")
|
||||
private boolean enabled;
|
||||
|
||||
@ -35,6 +40,19 @@ public class TaskCenterGoClient {
|
||||
@Value("${task.center.go.timeout-millis:3000}")
|
||||
private Integer timeoutMillis;
|
||||
|
||||
@Value("${task.center.event.mq.enabled:false}")
|
||||
private boolean mqEnabled;
|
||||
|
||||
@Value("${task.center.event.mq.topic:RC_DEFAULT_APP_ORDINARY}")
|
||||
private String mqTopic;
|
||||
|
||||
@Value("${task.center.event.mq.tag:task_center_event}")
|
||||
private String mqTag;
|
||||
|
||||
public TaskCenterGoClient(ObjectProvider<MessageSenderService> messageSenderServiceProvider) {
|
||||
this.messageSenderServiceProvider = messageSenderServiceProvider;
|
||||
}
|
||||
|
||||
public void reportMicDuration(String sysOrigin,
|
||||
Long roomId,
|
||||
Integer micIndex,
|
||||
@ -81,6 +99,9 @@ public class TaskCenterGoClient {
|
||||
}
|
||||
|
||||
private void reportEvent(TaskCenterEventRequest request) {
|
||||
if (mqEnabled) {
|
||||
publishMqEvent(request);
|
||||
}
|
||||
if (!enabled) {
|
||||
return;
|
||||
}
|
||||
@ -109,9 +130,21 @@ public class TaskCenterGoClient {
|
||||
}
|
||||
|
||||
int status = connection.getResponseCode();
|
||||
String responseBody = readResponse(connection);
|
||||
if (status < 200 || status >= 300) {
|
||||
log.warn("Task center go report failed. status={}, body={}, eventId={}",
|
||||
status, readResponse(connection), request.getEventId());
|
||||
status, responseBody, request.getEventId());
|
||||
return;
|
||||
}
|
||||
TaskCenterEventResult result = parseEventResult(responseBody);
|
||||
if (Objects.nonNull(result) && !Boolean.TRUE.equals(result.getProcessed())) {
|
||||
log.warn("Task center go report ignored. eventId={}, eventType={}, userId={}, updated={}, reason={}, body={}",
|
||||
request.getEventId(),
|
||||
request.getEventType(),
|
||||
request.getUserId(),
|
||||
result.getUpdated(),
|
||||
result.getReason(),
|
||||
responseBody);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn("Task center go report error. eventId={}", request.getEventId(), e);
|
||||
@ -122,6 +155,24 @@ public class TaskCenterGoClient {
|
||||
}
|
||||
}
|
||||
|
||||
private void publishMqEvent(TaskCenterEventRequest request) {
|
||||
try {
|
||||
MessageSenderService senderService = messageSenderServiceProvider.getIfAvailable();
|
||||
if (Objects.isNull(senderService)) {
|
||||
log.warn("Task center mq sender missing, skip event report. eventId={}", request.getEventId());
|
||||
return;
|
||||
}
|
||||
senderService.sendEventMessage(MessageEvent.builder()
|
||||
.consumeId(request.getEventId())
|
||||
.topicString(defaultIfBlank(mqTopic, "RC_DEFAULT_APP_ORDINARY"))
|
||||
.tag(defaultIfBlank(mqTag, "task_center_event"))
|
||||
.body(request)
|
||||
.build());
|
||||
} catch (Exception e) {
|
||||
log.warn("Task center mq report error. eventId={}", request.getEventId(), e);
|
||||
}
|
||||
}
|
||||
|
||||
private String readResponse(HttpURLConnection connection) {
|
||||
try (InputStream inputStream = connection.getErrorStream() != null
|
||||
? connection.getErrorStream()
|
||||
@ -135,6 +186,18 @@ public class TaskCenterGoClient {
|
||||
}
|
||||
}
|
||||
|
||||
private TaskCenterEventResult parseEventResult(String responseBody) {
|
||||
try {
|
||||
TaskCenterResponse response = JSON.parseObject(responseBody, TaskCenterResponse.class);
|
||||
if (Objects.isNull(response)) {
|
||||
return null;
|
||||
}
|
||||
return Objects.nonNull(response.getBody()) ? response.getBody() : response.getData();
|
||||
} catch (Exception e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private String trimRightSlash(String value) {
|
||||
while (value.endsWith("/")) {
|
||||
value = value.substring(0, value.length() - 1);
|
||||
@ -142,6 +205,11 @@ public class TaskCenterGoClient {
|
||||
return value;
|
||||
}
|
||||
|
||||
private String defaultIfBlank(String value, String fallback) {
|
||||
String normalized = Objects.toString(value, "").trim();
|
||||
return normalized.isEmpty() ? fallback : normalized;
|
||||
}
|
||||
|
||||
@Data
|
||||
@Accessors(chain = true)
|
||||
private static class TaskCenterEventRequest {
|
||||
@ -153,4 +221,17 @@ public class TaskCenterGoClient {
|
||||
private String occurredAt;
|
||||
private Map<String, Object> payload;
|
||||
}
|
||||
|
||||
@Data
|
||||
private static class TaskCenterResponse {
|
||||
private TaskCenterEventResult body;
|
||||
private TaskCenterEventResult data;
|
||||
}
|
||||
|
||||
@Data
|
||||
private static class TaskCenterEventResult {
|
||||
private Boolean processed;
|
||||
private Integer updated;
|
||||
private String reason;
|
||||
}
|
||||
}
|
||||
|
||||
@ -82,11 +82,16 @@ public interface LiveMicCacheService {
|
||||
*/
|
||||
List<LiveMicrophone> listLiveMicrophone(Long roomId, Long userId);
|
||||
|
||||
/**
|
||||
* 刷新移除不活跃用户.
|
||||
*
|
||||
* @return true 有刷新掉用户
|
||||
*/
|
||||
boolean refreshNotActiveUser(Long roomId);
|
||||
|
||||
}
|
||||
/**
|
||||
* 刷新移除不活跃用户.
|
||||
*
|
||||
* @return true 有刷新掉用户
|
||||
*/
|
||||
boolean refreshNotActiveUser(Long roomId);
|
||||
|
||||
/**
|
||||
* 刷新移除不活跃用户, 返回被移除的麦位.
|
||||
*/
|
||||
List<LiveMicrophone> refreshNotActiveUserAndReturn(Long roomId);
|
||||
|
||||
}
|
||||
|
||||
@ -58,16 +58,15 @@ public class LiveMicCacheServiceImpl implements LiveMicCacheService {
|
||||
return !redisService.hashHasKey(LiveMicKey.SEAT.getKey(roomId), Objects.toString(index));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void goUp(Long roomId, Integer index, LiveMicrophone microphone) {
|
||||
// if (Objects.isNull(microphone.getRefreshTime())) {
|
||||
// microphone.updateLastActivityTime();
|
||||
// }
|
||||
microphone.updateLastActivityTime();
|
||||
log.info("用户上麦 index = {} microphone = {} ", index, JSON.toJSONString(microphone));
|
||||
redisService.hashPut(LiveMicKey.SEAT.getKey(roomId),
|
||||
Objects.toString(index),
|
||||
microphone);
|
||||
@Override
|
||||
public void goUp(Long roomId, Integer index, LiveMicrophone microphone) {
|
||||
if (Objects.isNull(microphone.getRefreshTime())) {
|
||||
microphone.updateLastActivityTime();
|
||||
}
|
||||
log.info("用户上麦 index = {} microphone = {} ", index, JSON.toJSONString(microphone));
|
||||
redisService.hashPut(LiveMicKey.SEAT.getKey(roomId),
|
||||
Objects.toString(index),
|
||||
microphone);
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -120,27 +119,40 @@ public class LiveMicCacheServiceImpl implements LiveMicCacheService {
|
||||
.toList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean refreshNotActiveUser(Long roomId) {
|
||||
setExpire(roomId);
|
||||
|
||||
List<LiveMicrophone> liveMicrophone = listLiveMicrophone(roomId);
|
||||
if (CollectionUtils.isEmpty(liveMicrophone)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
String[] index =
|
||||
liveMicrophone.stream().filter(LiveMicrophone::checkInactiveExpire)
|
||||
.map(LiveMicrophone::getMicIndex)
|
||||
.map(Objects::toString)
|
||||
.toArray(String[]::new);
|
||||
|
||||
log.info("清除不活跃的用户入参 = {} ", JSON.toJSONString(index));
|
||||
Long size = redisService.hashDelete(LiveMicKey.SEAT.getKey(roomId), index);
|
||||
log.info("清除不活跃的用户结果 = {} ", size);
|
||||
|
||||
return Objects.nonNull(size) && size > 0;
|
||||
}
|
||||
@Override
|
||||
public boolean refreshNotActiveUser(Long roomId) {
|
||||
return CollectionUtils.isNotEmpty(refreshNotActiveUserAndReturn(roomId));
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<LiveMicrophone> refreshNotActiveUserAndReturn(Long roomId) {
|
||||
setExpire(roomId);
|
||||
|
||||
List<LiveMicrophone> liveMicrophone = listLiveMicrophone(roomId);
|
||||
if (CollectionUtils.isEmpty(liveMicrophone)) {
|
||||
return CollectionUtils.newArrayList();
|
||||
}
|
||||
|
||||
List<LiveMicrophone> inactiveMicrophones =
|
||||
liveMicrophone.stream().filter(LiveMicrophone::checkInactiveExpire).toList();
|
||||
String[] index =
|
||||
inactiveMicrophones.stream()
|
||||
.map(LiveMicrophone::getMicIndex)
|
||||
.map(Objects::toString)
|
||||
.toArray(String[]::new);
|
||||
if (index.length == 0) {
|
||||
return CollectionUtils.newArrayList();
|
||||
}
|
||||
|
||||
log.info("清除不活跃的用户入参 = {} ", JSON.toJSONString(index));
|
||||
Long size = redisService.hashDelete(LiveMicKey.SEAT.getKey(roomId), index);
|
||||
log.info("清除不活跃的用户结果 = {} ", size);
|
||||
|
||||
if (Objects.isNull(size) || size <= 0) {
|
||||
return CollectionUtils.newArrayList();
|
||||
}
|
||||
return inactiveMicrophones;
|
||||
}
|
||||
|
||||
|
||||
private void setExpire(Long roomId) {
|
||||
|
||||
@ -181,7 +181,7 @@ public class LiveMicrophoneGatewayImpl implements LiveMicrophoneGateway {
|
||||
ResponseAssert.notNull(UserErrorCode.USER_INFO_NOT_FOUND, liveMicrophone.getUser());
|
||||
|
||||
if (liveMicCacheService.checkEqSelf(roomId, micIndex, userId)) {
|
||||
liveMicCacheService.refreshNotActiveUser(roomId);
|
||||
refreshNotActiveUserAndReport(roomProfile);
|
||||
return micUserChangeNotice(roomProfile);
|
||||
}
|
||||
|
||||
@ -213,7 +213,7 @@ public class LiveMicrophoneGatewayImpl implements LiveMicrophoneGateway {
|
||||
micUpdated = true;
|
||||
|
||||
// 清理活跃用户
|
||||
liveMicCacheService.refreshNotActiveUser(roomId);
|
||||
refreshNotActiveUserAndReport(roomProfile);
|
||||
|
||||
// 发送通知
|
||||
return micUserChangeNotice(roomProfile);
|
||||
@ -307,8 +307,8 @@ public class LiveMicrophoneGatewayImpl implements LiveMicrophoneGateway {
|
||||
// 下麦克风
|
||||
liveMicCacheService.goDown(roomId, micIndex);
|
||||
// 清理活跃用户
|
||||
liveMicCacheService.refreshNotActiveUser(roomId);
|
||||
// 发送通知
|
||||
refreshNotActiveUserAndReport(roomProfile);
|
||||
// 发送通知
|
||||
micUserChangeNotice(roomProfile);
|
||||
}
|
||||
|
||||
@ -379,9 +379,9 @@ public class LiveMicrophoneGatewayImpl implements LiveMicrophoneGateway {
|
||||
liveMicCacheService.goDown(roomId, mickIndex);
|
||||
reportMicDuration(roomProfile, roomId, liveMicrophone, "MIC_LOCK");
|
||||
// 清理活跃用户
|
||||
liveMicCacheService.refreshNotActiveUser(roomId);
|
||||
// 发送通知
|
||||
micUserChangeNotice(roomProfile, settingSeat);
|
||||
refreshNotActiveUserAndReport(roomProfile);
|
||||
// 发送通知
|
||||
micUserChangeNotice(roomProfile, settingSeat);
|
||||
} finally {
|
||||
micOpsUnlock(roomId, mickIndex);
|
||||
}
|
||||
@ -595,13 +595,14 @@ public class LiveMicrophoneGatewayImpl implements LiveMicrophoneGateway {
|
||||
List<LiveMicSettingSeat> liveMicSettingSeats = initMicSetting(roomId);
|
||||
if (CollectionUtils.isEmpty(liveMicSettingSeats)) {
|
||||
return CollectionUtils.newArrayList();
|
||||
}
|
||||
|
||||
Map<Integer, LiveMicrophone> liveMicrophoneMap = mapLiveMicrophone(roomId);
|
||||
log.info("在线取麦克风列表信息 -Map: {}", liveMicrophoneMap);
|
||||
return liveMicSettingSeats.stream().map(seat -> new LiveMicrophoneDTO()
|
||||
.setRoomId(roomId)
|
||||
.setMicIndex(seat.getMicIndex())
|
||||
}
|
||||
|
||||
Map<Integer, LiveMicrophone> liveMicrophoneMap = mapLiveMicrophone(roomId);
|
||||
log.debug("list live microphone map summary, roomId={}, micCount={}", roomId,
|
||||
liveMicrophoneMap.size());
|
||||
return liveMicSettingSeats.stream().map(seat -> new LiveMicrophoneDTO()
|
||||
.setRoomId(roomId)
|
||||
.setMicIndex(seat.getMicIndex())
|
||||
.setMicLock(seat.getMicLock())
|
||||
.setMicMute(seat.getMicMute())
|
||||
.setUser(liveMicInfraConvertor.toLiveMicUserDTO(
|
||||
@ -655,24 +656,26 @@ public class LiveMicrophoneGatewayImpl implements LiveMicrophoneGateway {
|
||||
public void initMicSetting(Long roomId, Integer micSize) {
|
||||
init(roomId, micSize);
|
||||
}
|
||||
|
||||
private Map<Integer, LiveMicrophone> mapLiveMicrophone(Long roomId) {
|
||||
List<LiveMicrophone> liveMicrophones = liveMicCacheService.listLiveMicrophone(roomId);
|
||||
log.info("在线取麦克风列表信息 -列表: {}", liveMicrophones);
|
||||
//获取房间用户心动值
|
||||
Map<Long, Long> liveHeartbeatMap = getLiveHeartbeatMap(liveHeartbeatClient.listLiveHeartbeat(roomId).getBody());
|
||||
if (CollectionUtils.isEmpty(liveMicrophones)) {
|
||||
|
||||
private Map<Integer, LiveMicrophone> mapLiveMicrophone(Long roomId) {
|
||||
List<LiveMicrophone> liveMicrophones = liveMicCacheService.listLiveMicrophone(roomId);
|
||||
log.debug("list live microphone cache summary, roomId={}, micCount={}", roomId,
|
||||
liveMicrophones.size());
|
||||
//获取房间用户心动值
|
||||
Map<Long, Long> liveHeartbeatMap = getLiveHeartbeatMap(liveHeartbeatClient.listLiveHeartbeat(roomId).getBody());
|
||||
if (CollectionUtils.isEmpty(liveMicrophones)) {
|
||||
return CollectionUtils.newHashMap();
|
||||
}
|
||||
|
||||
List<LiveMicUser> liveMicUsers = liveMicrophones.stream().map(LiveMicrophone::getUser).toList();
|
||||
List<Long> userIds = liveMicUsers.stream().map(LiveMicUser::getId).toList();
|
||||
updateLiveMicrophoneUserInfo(liveMicrophones, roomId, userIds);
|
||||
Map<Long, Long> cpUserIdMap = userCpClient.getCpUserIdByUserIds(userIds).getBody();
|
||||
log.info("用户ID集合={}, 结果集cpUserIdMap = {}", userIds, cpUserIdMap);
|
||||
return liveMicrophones.stream()
|
||||
.peek(liveMicrophone -> {
|
||||
LiveMicUser user = liveMicrophone.getUser();
|
||||
List<Long> userIds = liveMicUsers.stream().map(LiveMicUser::getId).toList();
|
||||
updateLiveMicrophoneUserInfo(liveMicrophones, roomId, userIds);
|
||||
Map<Long, Long> cpUserIdMap = userCpClient.getCpUserIdByUserIds(userIds).getBody();
|
||||
log.debug("list live microphone cp user summary, roomId={}, userCount={}, cpUserCount={}",
|
||||
roomId, userIds.size(), Objects.nonNull(cpUserIdMap) ? cpUserIdMap.size() : 0);
|
||||
return liveMicrophones.stream()
|
||||
.peek(liveMicrophone -> {
|
||||
LiveMicUser user = liveMicrophone.getUser();
|
||||
user.setHeartbeatVal(Objects.nonNull(liveHeartbeatMap.get(user.getId()))? liveHeartbeatMap.get(user.getId()):0L);
|
||||
user.setCpUserId(Objects.nonNull(cpUserIdMap.get(user.getId()))? cpUserIdMap.get(user.getId()):0L);
|
||||
liveMicrophone.setUser(user);
|
||||
@ -760,12 +763,25 @@ public class LiveMicrophoneGatewayImpl implements LiveMicrophoneGateway {
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void refreshNotActiveUser(RoomProfileDTO roomProfile) {
|
||||
if (liveMicCacheService.refreshNotActiveUser(roomProfile.getId())) {
|
||||
micUserChangeNotice(roomProfile);
|
||||
}
|
||||
}
|
||||
@Override
|
||||
public void refreshNotActiveUser(RoomProfileDTO roomProfile) {
|
||||
if (refreshNotActiveUserAndReport(roomProfile)) {
|
||||
micUserChangeNotice(roomProfile);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean refreshNotActiveUserAndReport(RoomProfileDTO roomProfile) {
|
||||
if (Objects.isNull(roomProfile) || Objects.isNull(roomProfile.getId())) {
|
||||
return false;
|
||||
}
|
||||
List<LiveMicrophone> removedMicrophones =
|
||||
liveMicCacheService.refreshNotActiveUserAndReturn(roomProfile.getId());
|
||||
if (CollectionUtils.isEmpty(removedMicrophones)) {
|
||||
return false;
|
||||
}
|
||||
reportMicDuration(roomProfile, roomProfile.getId(), removedMicrophones, "INACTIVE_CLEANUP");
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取缓存房间roomAccount
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
package com.red.circle.order.app.common.task;
|
||||
|
||||
import com.alibaba.fastjson.JSON;
|
||||
import com.red.circle.component.mq.MessageEvent;
|
||||
import com.red.circle.mq.rocket.business.service.MessageSenderService;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.math.BigDecimal;
|
||||
@ -15,6 +17,7 @@ import java.util.Objects;
|
||||
import lombok.Data;
|
||||
import lombok.experimental.Accessors;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.ObjectProvider;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@ -24,6 +27,8 @@ public class TaskCenterGoClient {
|
||||
|
||||
private static final String EVENT_TYPE_RECHARGE_GOLD = "RECHARGE_GOLD";
|
||||
|
||||
private final ObjectProvider<MessageSenderService> messageSenderServiceProvider;
|
||||
|
||||
@Value("${task.center.go.enabled:true}")
|
||||
private boolean enabled;
|
||||
|
||||
@ -36,6 +41,19 @@ public class TaskCenterGoClient {
|
||||
@Value("${task.center.go.timeout-millis:3000}")
|
||||
private Integer timeoutMillis;
|
||||
|
||||
@Value("${task.center.event.mq.enabled:false}")
|
||||
private boolean mqEnabled;
|
||||
|
||||
@Value("${task.center.event.mq.topic:RC_DEFAULT_APP_ORDINARY}")
|
||||
private String mqTopic;
|
||||
|
||||
@Value("${task.center.event.mq.tag:task_center_event}")
|
||||
private String mqTag;
|
||||
|
||||
public TaskCenterGoClient(ObjectProvider<MessageSenderService> messageSenderServiceProvider) {
|
||||
this.messageSenderServiceProvider = messageSenderServiceProvider;
|
||||
}
|
||||
|
||||
public void reportRechargeGold(String sysOrigin,
|
||||
Long userId,
|
||||
Long purchaseHistoryId,
|
||||
@ -68,6 +86,9 @@ public class TaskCenterGoClient {
|
||||
}
|
||||
|
||||
private void reportEvent(TaskCenterEventRequest request) {
|
||||
if (mqEnabled) {
|
||||
publishMqEvent(request);
|
||||
}
|
||||
if (!enabled) {
|
||||
return;
|
||||
}
|
||||
@ -109,6 +130,24 @@ public class TaskCenterGoClient {
|
||||
}
|
||||
}
|
||||
|
||||
private void publishMqEvent(TaskCenterEventRequest request) {
|
||||
try {
|
||||
MessageSenderService senderService = messageSenderServiceProvider.getIfAvailable();
|
||||
if (Objects.isNull(senderService)) {
|
||||
log.warn("Task center mq sender missing, skip event report. eventId={}", request.getEventId());
|
||||
return;
|
||||
}
|
||||
senderService.sendEventMessage(MessageEvent.builder()
|
||||
.consumeId(request.getEventId())
|
||||
.topicString(defaultIfBlank(mqTopic, "RC_DEFAULT_APP_ORDINARY"))
|
||||
.tag(defaultIfBlank(mqTag, "task_center_event"))
|
||||
.body(request)
|
||||
.build());
|
||||
} catch (Exception e) {
|
||||
log.warn("Task center mq report error. eventId={}", request.getEventId(), e);
|
||||
}
|
||||
}
|
||||
|
||||
private String readResponse(HttpURLConnection connection) {
|
||||
try (InputStream inputStream = connection.getErrorStream() != null
|
||||
? connection.getErrorStream()
|
||||
@ -129,6 +168,11 @@ public class TaskCenterGoClient {
|
||||
return value;
|
||||
}
|
||||
|
||||
private String defaultIfBlank(String value, String fallback) {
|
||||
String normalized = Objects.toString(value, "").trim();
|
||||
return normalized.isEmpty() ? fallback : normalized;
|
||||
}
|
||||
|
||||
@Data
|
||||
@Accessors(chain = true)
|
||||
private static class TaskCenterEventRequest {
|
||||
|
||||
@ -51,12 +51,14 @@ import com.red.circle.tool.core.text.StringUtils;
|
||||
import com.red.circle.tool.crypto.SecurityUtils;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
@ -102,10 +104,12 @@ public class GameHkysRestController {
|
||||
return new HkysResponse<HkysUserProfileResponse>()
|
||||
.setErrorCode(HyksErrorEnum.PARAM_ERROR.getErrorCode());
|
||||
}
|
||||
String sign = SecurityUtils.md5(Stream.of(cmd.getGameId(), cmd.getUid(), cmd.getToken(), cmd.getRoomId(), gameHkysProperties.getSignKey()).filter(Objects::nonNull).map(String::valueOf).collect(Collectors.joining("")));
|
||||
if (!sign.equals(cmd.getSign())) {
|
||||
return new HkysResponse<HkysUserProfileResponse>().setErrorCode(HyksErrorEnum.SIGN_VERIFY_FAIL.getErrorCode());
|
||||
}
|
||||
if (!verifyLingxianUserInfoSign(cmd)) {
|
||||
log.warn("lingxian getUserInfo sign verify failed, uid={}, gameId={}, roomId={}, sysOrigin={}, tokenLength={}",
|
||||
cmd.getUid(), cmd.getGameId(), cmd.getRoomId(), resolveSysOriginQuietly(cmd.getToken()),
|
||||
cmd.getToken() == null ? 0 : cmd.getToken().length());
|
||||
return new HkysResponse<HkysUserProfileResponse>().setErrorCode(HyksErrorEnum.SIGN_VERIFY_FAIL.getErrorCode());
|
||||
}
|
||||
|
||||
HkysUserProfileQuery query = new HkysUserProfileQuery();
|
||||
query.setGameId(cmd.getGameId());
|
||||
@ -123,12 +127,12 @@ public class GameHkysRestController {
|
||||
|| Objects.isNull(cmd.getType()) || Objects.isNull(cmd.getCoin())) {
|
||||
return new HkysResponse<JSONObject>().setErrorCode(HyksErrorEnum.PARAM_ERROR.getErrorCode());
|
||||
}
|
||||
String sign = SecurityUtils.md5(Stream.of(cmd.getOrderId(), cmd.getGameId(), cmd.getRoundId(), cmd.getUid(),
|
||||
cmd.getCoin(), cmd.getType(), cmd.getToken(), cmd.getWinId(), cmd.getRoomId(), gameHkysProperties.getSignKey())
|
||||
.filter(Objects::nonNull).map(String::valueOf).collect(Collectors.joining("")));
|
||||
if (!sign.equals(cmd.getSign())) {
|
||||
return new HkysResponse<JSONObject>().setErrorCode(HyksErrorEnum.SIGN_VERIFY_FAIL.getErrorCode());
|
||||
}
|
||||
if (!verifyLingxianUpdateBalanceSign(cmd)) {
|
||||
log.warn("lingxian updateBalance sign verify failed, uid={}, gameId={}, orderId={}, roomId={}, sysOrigin={}, tokenLength={}",
|
||||
cmd.getUid(), cmd.getGameId(), cmd.getOrderId(), cmd.getRoomId(), resolveSysOriginQuietly(cmd.getToken()),
|
||||
cmd.getToken() == null ? 0 : cmd.getToken().length());
|
||||
return new HkysResponse<JSONObject>().setErrorCode(HyksErrorEnum.SIGN_VERIFY_FAIL.getErrorCode());
|
||||
}
|
||||
|
||||
HkysUserCoinUpdate param = new HkysUserCoinUpdate();
|
||||
param.setOrderId(cmd.getOrderId());
|
||||
@ -150,7 +154,7 @@ public class GameHkysRestController {
|
||||
//游玩一句游戏结束 每日任务 灵仙
|
||||
Long userId = DataTypeUtils.toLong(cmd.getUid());
|
||||
|
||||
String sysOrigin = UserCredential.parseToken(cmd.getToken()).getSysOrigin();
|
||||
String sysOrigin = parseSysOrigin(cmd.getToken());
|
||||
|
||||
BigDecimal cost = enumConfigCacheService
|
||||
.getValueBigDecimal(EnumConfigKey.GAME_BROADCAST_AMOUNT, sysOrigin) ;
|
||||
@ -223,6 +227,88 @@ public class GameHkysRestController {
|
||||
);
|
||||
}
|
||||
|
||||
private String resolveLingxianSignKey(String token) {
|
||||
List<String> keys = lingxianSignKeys(token);
|
||||
return keys.isEmpty() ? "" : keys.get(0);
|
||||
}
|
||||
|
||||
private boolean verifyLingxianUserInfoSign(GameLxwlUserInfoCmd cmd) {
|
||||
for (String token : lingxianTokenCandidates(cmd.getToken())) {
|
||||
for (String key : lingxianSignKeys(token)) {
|
||||
String sign = SecurityUtils.md5(Stream.of(cmd.getGameId(), cmd.getUid(), token, cmd.getRoomId(), key)
|
||||
.filter(Objects::nonNull).map(String::valueOf).collect(Collectors.joining("")));
|
||||
if (sign.equalsIgnoreCase(cmd.getSign())) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean verifyLingxianUpdateBalanceSign(GameLxwlUpdateBalanceCmd cmd) {
|
||||
for (String token : lingxianTokenCandidates(cmd.getToken())) {
|
||||
for (String key : lingxianSignKeys(token)) {
|
||||
String sign = SecurityUtils.md5(Stream.of(cmd.getOrderId(), cmd.getGameId(), cmd.getRoundId(), cmd.getUid(),
|
||||
cmd.getCoin(), cmd.getType(), token, cmd.getWinId(), cmd.getRoomId(), key)
|
||||
.filter(Objects::nonNull).map(String::valueOf).collect(Collectors.joining("")));
|
||||
if (sign.equalsIgnoreCase(cmd.getSign())) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private List<String> lingxianSignKeys(String token) {
|
||||
List<String> keys = new ArrayList<>();
|
||||
addIfNotBlank(keys, gameListConfigService.getActiveLingxianAppKey(resolveSysOriginQuietly(token)));
|
||||
addIfNotBlank(keys, gameHkysProperties.getSignKey());
|
||||
return keys;
|
||||
}
|
||||
|
||||
private List<String> lingxianTokenCandidates(String token) {
|
||||
List<String> tokens = new ArrayList<>();
|
||||
if (StringUtils.isBlank(token)) {
|
||||
tokens.add(null);
|
||||
return tokens;
|
||||
}
|
||||
addTokenCandidate(tokens, token);
|
||||
addTokenCandidate(tokens, SecurityUtils.urlDecode(token));
|
||||
return tokens;
|
||||
}
|
||||
|
||||
private void addTokenCandidate(List<String> tokens, String token) {
|
||||
if (StringUtils.isBlank(token)) {
|
||||
return;
|
||||
}
|
||||
addIfNotBlank(tokens, token);
|
||||
addIfNotBlank(tokens, trimTokenPadding(token));
|
||||
}
|
||||
|
||||
private String trimTokenPadding(String token) {
|
||||
return token == null ? "" : token.trim().replaceAll("=+$", "");
|
||||
}
|
||||
|
||||
private void addIfNotBlank(List<String> values, String value) {
|
||||
if (StringUtils.isBlank(value) || values.contains(value)) {
|
||||
return;
|
||||
}
|
||||
values.add(value);
|
||||
}
|
||||
|
||||
private String resolveSysOriginQuietly(String token) {
|
||||
try {
|
||||
return parseSysOrigin(token);
|
||||
} catch (Exception e) {
|
||||
log.warn("resolve lingxian sysOrigin failed");
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
private String parseSysOrigin(String token) {
|
||||
return UserCredential.parseToken(SecurityUtils.urlDecode(token)).getSysOrigin();
|
||||
}
|
||||
|
||||
private void reportTaskCenterGameEvents(String sysOrigin,
|
||||
Long userId,
|
||||
String provider,
|
||||
|
||||
@ -164,6 +164,7 @@ public class TeamBdMemberBillListQryExe {
|
||||
TeamBdMemberCO result = new TeamBdMemberCO();
|
||||
result.setMemberBillList(memberBillList);
|
||||
result.setMemberCount(memberBillList.size());
|
||||
result.setAgencyNumber(memberBillList.size());
|
||||
result.setTotalSalary(totalSalary);
|
||||
result.setTotalRecharge(totalRecharge);
|
||||
result.setBillTitle(TeamBillCycleUtils.parseBillBelongToDateRangeStr(currentBillBelong));
|
||||
@ -222,6 +223,8 @@ public class TeamBdMemberBillListQryExe {
|
||||
TeamBdMemberCO result = new TeamBdMemberCO();
|
||||
result.setMemberBillList(memberBillList);
|
||||
result.setMemberCount(memberBillList.size());
|
||||
result.setBdNumber(memberBillList.size());
|
||||
result.setAgencyNumber(totalAgentCount);
|
||||
result.setTotalSalary(totalSalary);
|
||||
result.setTotalRecharge(totalRecharge);
|
||||
result.setBillTitle(TeamBillCycleUtils.parseBillBelongToDateRangeStr(currentBillBelong));
|
||||
@ -321,6 +324,8 @@ public class TeamBdMemberBillListQryExe {
|
||||
TeamBdMemberCO result = new TeamBdMemberCO();
|
||||
result.setMemberBillList(Collections.emptyList());
|
||||
result.setMemberCount(0);
|
||||
result.setAgencyNumber(0);
|
||||
result.setBdNumber(0);
|
||||
result.setTotalSalary(BigDecimal.ZERO);
|
||||
result.setTotalRecharge(BigDecimal.ZERO);
|
||||
result.setBillTitle(TeamBillCycleUtils.parseBillBelongToDateRangeStr(currentBillBelong));
|
||||
@ -335,24 +340,45 @@ public class TeamBdMemberBillListQryExe {
|
||||
|
||||
@NotNull
|
||||
private Map<Long, BigDecimal> getRechargeMap(Integer currentBillBelong, Set<Long> teamIds) {
|
||||
if (CollectionUtils.isEmpty(teamIds)) {
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
Map<Long, List<TeamMember>> mapByTeamIds = teamMemberService.mapByTeamIds(teamIds);
|
||||
if (Objects.isNull(mapByTeamIds) || mapByTeamIds.isEmpty()) {
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
List<Long> memberIds = mapByTeamIds.values().stream()
|
||||
.flatMap(List::stream)
|
||||
.map(TeamMember::getMemberId)
|
||||
.toList();
|
||||
if (CollectionUtils.isEmpty(memberIds)) {
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
|
||||
LocalDateTime startTime = TeamBillCycleUtils.getBillBelongStartTime(currentBillBelong);
|
||||
LocalDateTime endTime = TeamBillCycleUtils.getBillBelongEndTime(currentBillBelong);
|
||||
ResultResponse<List<BatchUserRechargeStatisticsDTO>> response =
|
||||
inAppPurchaseDetailsClient.batchUserRechargeStatistics(
|
||||
memberIds,
|
||||
SysOriginPlatformEnum.LIKEI.name(),
|
||||
startTime,
|
||||
endTime
|
||||
);
|
||||
List<BatchUserRechargeStatisticsDTO> body = response.getBody();
|
||||
Map<Long, BigDecimal> rechargeMap = body.stream().collect(Collectors.toMap(BatchUserRechargeStatisticsDTO::getUserId, BatchUserRechargeStatisticsDTO::getTotalAmount, BigDecimal::add));
|
||||
return rechargeMap;
|
||||
try {
|
||||
ResultResponse<List<BatchUserRechargeStatisticsDTO>> response =
|
||||
inAppPurchaseDetailsClient.batchUserRechargeStatistics(
|
||||
memberIds,
|
||||
SysOriginPlatformEnum.LIKEI.name(),
|
||||
startTime,
|
||||
endTime
|
||||
);
|
||||
List<BatchUserRechargeStatisticsDTO> body = Optional.ofNullable(response)
|
||||
.map(ResultResponse::getBody)
|
||||
.orElse(Collections.emptyList());
|
||||
if (CollectionUtils.isEmpty(body)) {
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
return body.stream().collect(Collectors.toMap(
|
||||
BatchUserRechargeStatisticsDTO::getUserId,
|
||||
BatchUserRechargeStatisticsDTO::getTotalAmount,
|
||||
BigDecimal::add));
|
||||
} catch (Exception ex) {
|
||||
log.warn("Failed to query BD team recharge statistics, teamIds={}", teamIds, ex);
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
}
|
||||
|
||||
private Collection<? extends BusinessDevelopmentTeam> buildBdTeamVO(List<TeamMember> teamMembers) {
|
||||
|
||||
@ -7,11 +7,12 @@ import com.red.circle.framework.core.asserts.ResponseAssert;
|
||||
import com.red.circle.mq.business.model.event.gift.GiveAwayGiftBatchEvent;
|
||||
import com.red.circle.mq.business.model.event.gift.GiveAwayGiftRoomAccepts;
|
||||
import com.red.circle.mq.business.model.event.gift.GiveGiftConfig;
|
||||
import com.red.circle.mq.business.model.event.task.TaskApprovalEvent;
|
||||
import com.red.circle.mq.rocket.business.producer.GiftMqMessage;
|
||||
import com.red.circle.mq.rocket.business.producer.TaskMqMessage;
|
||||
import com.red.circle.other.app.dto.cmd.gift.GiveAwayGiftBatchCmd;
|
||||
import com.red.circle.other.infra.database.cache.service.other.GiftCacheService;
|
||||
import com.red.circle.mq.business.model.event.task.TaskApprovalEvent;
|
||||
import com.red.circle.mq.rocket.business.producer.GiftMqMessage;
|
||||
import com.red.circle.mq.rocket.business.producer.TaskMqMessage;
|
||||
import com.red.circle.other.app.common.task.TaskCenterGoClient;
|
||||
import com.red.circle.other.app.dto.cmd.gift.GiveAwayGiftBatchCmd;
|
||||
import com.red.circle.other.infra.database.cache.service.other.GiftCacheService;
|
||||
import com.red.circle.other.infra.database.rds.service.user.user.CpRelationshipService;
|
||||
import com.red.circle.other.inner.enums.material.GiftCurrencyType;
|
||||
import com.red.circle.other.inner.enums.material.GiftTabEnum;
|
||||
@ -45,10 +46,11 @@ public class BlessingsGiftGiveCmdExe {
|
||||
|
||||
private final GiftCacheService giftCacheService;
|
||||
private final GiftMqMessage giftMqMessage;
|
||||
private final TaskMqMessage taskMqMessage;
|
||||
private final WalletGoldClient walletGoldClient;
|
||||
private final CpRelationshipService cpRelationshipService;
|
||||
public BigDecimal execute(GiveAwayGiftBatchCmd cmd) {
|
||||
private final TaskMqMessage taskMqMessage;
|
||||
private final WalletGoldClient walletGoldClient;
|
||||
private final CpRelationshipService cpRelationshipService;
|
||||
private final TaskCenterGoClient taskCenterGoClient;
|
||||
public BigDecimal execute(GiveAwayGiftBatchCmd cmd) {
|
||||
|
||||
// 过滤赠送人非法值
|
||||
ResponseAssert.notEmpty(BaseErrorEnum.REQUEST_PARAMETER_ERROR, cmd.getAccepts());
|
||||
@ -80,8 +82,9 @@ public class BlessingsGiftGiveCmdExe {
|
||||
GiveAwayGiftBatchEvent giftBatchEvent = toGiveAwayGiftBatchEvent(cmd, giftConfig,
|
||||
receipt.getAssetRecordId());
|
||||
// log.warn("cptesttest execute:{}", JacksonUtils.toJson(giftBatchEvent));
|
||||
giftBatchEvent.setCheckBlessingsGift(Boolean.TRUE);
|
||||
giftMqMessage.sendGift(receipt.getAssetRecordId().toString(), giftBatchEvent);
|
||||
giftBatchEvent.setCheckBlessingsGift(Boolean.TRUE);
|
||||
giftMqMessage.sendGift(receipt.getAssetRecordId().toString(), giftBatchEvent);
|
||||
reportTaskCenterGiftEvent(cmd, giftConfig, receipt);
|
||||
|
||||
|
||||
|
||||
@ -91,8 +94,8 @@ public class BlessingsGiftGiveCmdExe {
|
||||
.day(LocalDateTimeUtils.nowFormat("yyyy-MM-dd"))
|
||||
.build());*/
|
||||
|
||||
return receipt.getBalance().getDollarAmount();
|
||||
}
|
||||
return receipt.getBalance().getDollarAmount();
|
||||
}
|
||||
|
||||
/**
|
||||
* 付钱.
|
||||
@ -113,11 +116,31 @@ public class BlessingsGiftGiveCmdExe {
|
||||
|
||||
// 1006 类型不在范围
|
||||
ResponseAssert.failure(CommonErrorEnum.TYPE_IS_NOT_IN_SCOPE);
|
||||
throw new IllegalArgumentException("param error.");
|
||||
|
||||
}
|
||||
|
||||
private GiveAwayGiftBatchEvent toGiveAwayGiftBatchEvent(GiveAwayGiftBatchCmd cmd,
|
||||
throw new IllegalArgumentException("param error.");
|
||||
|
||||
}
|
||||
|
||||
private void reportTaskCenterGiftEvent(GiveAwayGiftBatchCmd cmd, GiftConfigDTO giftConfig,
|
||||
WalletReceiptResDTO receipt) {
|
||||
if (Objects.isNull(receipt)) {
|
||||
return;
|
||||
}
|
||||
taskCenterGoClient.reportGiftConsumeGold(cmd.requireReqSysOrigin(),
|
||||
cmd.requiredReqUserId(),
|
||||
receipt.getAssetRecordId(),
|
||||
giftConfig.getId(),
|
||||
giftConfig.getGiftName(),
|
||||
giftConfig.getGiftTab(),
|
||||
giftConfig.getGiftCandy(),
|
||||
cmd.getQuantity(),
|
||||
cmd.filterAcceptUserId().size(),
|
||||
receipt.getOpAmount().getDollarAmount(),
|
||||
cmd.getRoomId(),
|
||||
cmd.getDynamicContentId(),
|
||||
"BLESSINGS_GIFT");
|
||||
}
|
||||
|
||||
private GiveAwayGiftBatchEvent toGiveAwayGiftBatchEvent(GiveAwayGiftBatchCmd cmd,
|
||||
GiftConfigDTO giftConfig, Long trackId) {
|
||||
|
||||
GiveAwayGiftBatchEvent giveAwayGiftBatchEvent = new GiveAwayGiftBatchEvent();
|
||||
|
||||
@ -5,12 +5,13 @@ import com.red.circle.common.business.enums.DiamondOrigin;
|
||||
import com.red.circle.framework.core.asserts.ResponseAssert;
|
||||
import com.red.circle.framework.core.response.CommonErrorCode;
|
||||
import com.red.circle.framework.core.response.ResponseErrorCode;
|
||||
import com.red.circle.mq.business.model.event.task.TaskApprovalEvent;
|
||||
import com.red.circle.mq.rocket.business.producer.GiftMqMessage;
|
||||
import com.red.circle.mq.rocket.business.producer.TaskMqMessage;
|
||||
import com.red.circle.other.app.convertor.material.GiftAppConvertor;
|
||||
import com.red.circle.other.app.dto.cmd.gift.GiveAwayGiftBatchCmd;
|
||||
import com.red.circle.other.app.service.task.TaskService;
|
||||
import com.red.circle.mq.business.model.event.task.TaskApprovalEvent;
|
||||
import com.red.circle.mq.rocket.business.producer.GiftMqMessage;
|
||||
import com.red.circle.mq.rocket.business.producer.TaskMqMessage;
|
||||
import com.red.circle.other.app.common.task.TaskCenterGoClient;
|
||||
import com.red.circle.other.app.convertor.material.GiftAppConvertor;
|
||||
import com.red.circle.other.app.dto.cmd.gift.GiveAwayGiftBatchCmd;
|
||||
import com.red.circle.other.app.service.task.TaskService;
|
||||
import com.red.circle.other.domain.gateway.user.UserProfileGateway;
|
||||
import com.red.circle.other.domain.gateway.user.ability.UserRegionGateway;
|
||||
import com.red.circle.other.infra.database.cache.service.other.GiftCacheService;
|
||||
@ -52,9 +53,10 @@ public class GiftGiveAwayBatchCmdExe {
|
||||
private final GiftCacheService giftCacheService;
|
||||
private final TaskMqMessage taskMqMessage;
|
||||
private final UserProfileGateway userProfileGateway;
|
||||
private final WalletDiamondClient walletDiamondClient;
|
||||
private final TaskService taskService;
|
||||
private final UserRegionGateway userRegionGateway;
|
||||
private final WalletDiamondClient walletDiamondClient;
|
||||
private final TaskService taskService;
|
||||
private final UserRegionGateway userRegionGateway;
|
||||
private final TaskCenterGoClient taskCenterGoClient;
|
||||
|
||||
public BigDecimal execute(GiveAwayGiftBatchCmd cmd) {
|
||||
|
||||
@ -83,10 +85,11 @@ public class GiftGiveAwayBatchCmdExe {
|
||||
// 付钱
|
||||
WalletReceiptDTO receipt = consumeCandy(cmd, giftConfig);
|
||||
log.warn("GiftGiveAwayBatchCmdExe{}", receipt);
|
||||
|
||||
giftMqMessage.sendGift(receipt.getConsumeId().toString(),
|
||||
giftAppConvertor.toGiveAwayGiftBatchEvent(cmd, giftConfig, receipt.getConsumeId()));
|
||||
userProfileGateway.removeCache(cmd.requiredReqUserId());
|
||||
|
||||
giftMqMessage.sendGift(receipt.getConsumeId().toString(),
|
||||
giftAppConvertor.toGiveAwayGiftBatchEvent(cmd, giftConfig, receipt.getConsumeId()));
|
||||
reportTaskCenterGiftEvent(cmd, giftConfig, receipt);
|
||||
userProfileGateway.removeCache(cmd.requiredReqUserId());
|
||||
|
||||
// 检查是否未完成任务
|
||||
Boolean taskStatus = taskService.checkTaskStatus(cmd.getReqUserId(), 2L, LocalDateTimeUtils.nowFormat("yyyy-MM-dd"));
|
||||
@ -159,11 +162,31 @@ public class GiftGiveAwayBatchCmdExe {
|
||||
}
|
||||
|
||||
// 类型不在范围
|
||||
ResponseAssert.failure(CommonErrorCode.TYPE_IS_NOT_IN_SCOPE);
|
||||
throw new IllegalArgumentException("param error.");
|
||||
}
|
||||
|
||||
private boolean isGiftTypeEqDiamond(GiftConfigDTO giftConfigInfo) {
|
||||
ResponseAssert.failure(CommonErrorCode.TYPE_IS_NOT_IN_SCOPE);
|
||||
throw new IllegalArgumentException("param error.");
|
||||
}
|
||||
|
||||
private void reportTaskCenterGiftEvent(GiveAwayGiftBatchCmd cmd, GiftConfigDTO giftConfig,
|
||||
WalletReceiptDTO receipt) {
|
||||
if (!isGiftTypeEqGold(giftConfig) || Objects.isNull(receipt)) {
|
||||
return;
|
||||
}
|
||||
taskCenterGoClient.reportGiftConsumeGold(cmd.requireReqSysOrigin(),
|
||||
cmd.requiredReqUserId(),
|
||||
receipt.getConsumeId(),
|
||||
giftConfig.getId(),
|
||||
giftConfig.getGiftName(),
|
||||
giftConfig.getGiftTab(),
|
||||
giftConfig.getGiftCandy(),
|
||||
cmd.getQuantity(),
|
||||
cmd.filterAcceptUserId().size(),
|
||||
receipt.getProcessAmount(),
|
||||
cmd.getRoomId(),
|
||||
cmd.getDynamicContentId(),
|
||||
"NORMAL_GIFT");
|
||||
}
|
||||
|
||||
private boolean isGiftTypeEqDiamond(GiftConfigDTO giftConfigInfo) {
|
||||
return Objects.equals(giftConfigInfo.getType(), GiftCurrencyType.DIAMOND.name());
|
||||
}
|
||||
|
||||
|
||||
@ -4,12 +4,13 @@ import com.red.circle.framework.core.asserts.ResponseAssert;
|
||||
import com.red.circle.framework.core.response.CommonErrorCode;
|
||||
import com.red.circle.framework.core.response.ResponseErrorCode;
|
||||
import com.red.circle.mq.business.model.event.task.TaskApprovalEvent;
|
||||
import com.red.circle.mq.rocket.business.producer.GiftMqMessage;
|
||||
import com.red.circle.mq.rocket.business.producer.TaskMqMessage;
|
||||
import com.red.circle.other.app.common.gift.GameLuckyGiftCommon;
|
||||
import com.red.circle.other.app.convertor.material.GiftAppConvertor;
|
||||
import com.red.circle.other.app.dto.cmd.gift.GiveAwayGiftBatchCmd;
|
||||
import com.red.circle.other.app.service.task.TaskService;
|
||||
import com.red.circle.mq.rocket.business.producer.GiftMqMessage;
|
||||
import com.red.circle.mq.rocket.business.producer.TaskMqMessage;
|
||||
import com.red.circle.other.app.common.gift.GameLuckyGiftCommon;
|
||||
import com.red.circle.other.app.common.task.TaskCenterGoClient;
|
||||
import com.red.circle.other.app.convertor.material.GiftAppConvertor;
|
||||
import com.red.circle.other.app.dto.cmd.gift.GiveAwayGiftBatchCmd;
|
||||
import com.red.circle.other.app.service.task.TaskService;
|
||||
import com.red.circle.other.domain.gateway.user.UserProfileGateway;
|
||||
import com.red.circle.other.infra.database.cache.service.other.GiftCacheService;
|
||||
import com.red.circle.other.inner.asserts.GiftErrorCode;
|
||||
@ -46,9 +47,10 @@ public class LuckyGiftGiveCmdExe {
|
||||
private final WalletGoldClient walletGoldClient;
|
||||
private final GiftCacheService giftCacheService;
|
||||
private final UserProfileGateway userProfileGateway;
|
||||
private final GameLuckyGiftCommon gameLuckyGiftCommon;
|
||||
private final TaskMqMessage taskMqMessage;
|
||||
private final TaskService taskService;
|
||||
private final GameLuckyGiftCommon gameLuckyGiftCommon;
|
||||
private final TaskMqMessage taskMqMessage;
|
||||
private final TaskService taskService;
|
||||
private final TaskCenterGoClient taskCenterGoClient;
|
||||
|
||||
public BigDecimal execute(GiveAwayGiftBatchCmd cmd) {
|
||||
|
||||
@ -98,6 +100,7 @@ public class LuckyGiftGiveCmdExe {
|
||||
giftAppConvertor.toGiveAwayGiftBatchEvent(cmd, giftConfig, trackId)
|
||||
);
|
||||
}
|
||||
reportTaskCenterGiftEvent(cmd, giftConfig, receipt);
|
||||
|
||||
// 抽奖mq
|
||||
if (luckyGift) {
|
||||
@ -149,8 +152,31 @@ public class LuckyGiftGiveCmdExe {
|
||||
|
||||
// 类型不在范围
|
||||
ResponseAssert.failure(CommonErrorCode.TYPE_IS_NOT_IN_SCOPE);
|
||||
throw new IllegalArgumentException("param error.");
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
throw new IllegalArgumentException("param error.");
|
||||
|
||||
}
|
||||
|
||||
private void reportTaskCenterGiftEvent(GiveAwayGiftBatchCmd cmd, GiftConfigDTO giftConfig,
|
||||
WalletReceiptResDTO receipt) {
|
||||
if (Objects.isNull(receipt)) {
|
||||
return;
|
||||
}
|
||||
String source = Objects.equals(giftConfig.getGiftTab(), GiftTabEnum.MAGIC.name())
|
||||
? "MAGIC_GIFT"
|
||||
: "LUCKY_GIFT";
|
||||
taskCenterGoClient.reportGiftConsumeGold(cmd.requireReqSysOrigin(),
|
||||
cmd.requiredReqUserId(),
|
||||
receipt.getAssetRecordId(),
|
||||
giftConfig.getId(),
|
||||
giftConfig.getGiftName(),
|
||||
giftConfig.getGiftTab(),
|
||||
giftConfig.getGiftCandy(),
|
||||
cmd.getQuantity(),
|
||||
cmd.filterAcceptUserId().size(),
|
||||
receipt.getOpAmount().getDollarAmount(),
|
||||
cmd.getRoomId(),
|
||||
cmd.getDynamicContentId(),
|
||||
source);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -19,11 +19,11 @@ import org.springframework.stereotype.Component;
|
||||
*/
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class LatestAndReviewVersionManageGetQryExe {
|
||||
|
||||
private final VersionManageService versionManageService;
|
||||
private final SysAppVersionManageAppConvertor sysAppVersionManageAppConvertor;
|
||||
private final VersionUpdateDescriptionService versionUpdateDescriptionService;
|
||||
public class LatestAndReviewVersionManageGetQryExe {
|
||||
|
||||
private final VersionManageService versionManageService;
|
||||
private final SysAppVersionManageAppConvertor sysAppVersionManageAppConvertor;
|
||||
private final VersionUpdateDescriptionService versionUpdateDescriptionService;
|
||||
|
||||
public VersionManageLatestAndReviewCO execute(AppExtCommand cmd) {
|
||||
|
||||
@ -37,14 +37,14 @@ public class LatestAndReviewVersionManageGetQryExe {
|
||||
.setReview(builderCO(cmd.getReqLanguage(),
|
||||
sysAppVersionManageAppConvertor.toVersionManageCO(review)));
|
||||
}
|
||||
|
||||
private VersionManage getVersion(AppExtCommand cmd, Boolean bool) {
|
||||
return versionManageService
|
||||
.getVersion(cmd.requireReqSysOrigin()
|
||||
, cmd.getReqClient().getClientName()
|
||||
, cmd.getReqAppIntel().getChannel()
|
||||
, bool);
|
||||
}
|
||||
|
||||
private VersionManage getVersion(AppExtCommand cmd, Boolean bool) {
|
||||
return versionManageService
|
||||
.getVersion(cmd.requireReqSysOrigin()
|
||||
, cmd.getReqClient().getClientName()
|
||||
, null
|
||||
, bool);
|
||||
}
|
||||
|
||||
|
||||
private VersionManageCO builderCO(String language, VersionManageCO versionCO) {
|
||||
|
||||
@ -18,11 +18,11 @@ import org.springframework.stereotype.Component;
|
||||
*/
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class LatestVersionManageGetQryExe {
|
||||
|
||||
private final VersionManageService versionManageService;
|
||||
private final SysAppVersionManageAppConvertor sysAppVersionManageAppConvertor;
|
||||
private final VersionUpdateDescriptionService versionUpdateDescriptionService;
|
||||
public class LatestVersionManageGetQryExe {
|
||||
|
||||
private final VersionManageService versionManageService;
|
||||
private final SysAppVersionManageAppConvertor sysAppVersionManageAppConvertor;
|
||||
private final VersionUpdateDescriptionService versionUpdateDescriptionService;
|
||||
|
||||
public VersionManageCO execute(AppExtCommand cmd) {
|
||||
|
||||
@ -45,12 +45,12 @@ public class LatestVersionManageGetQryExe {
|
||||
.getUpdateDescription(versionCO.getId(), cmd.getReqLanguage()))
|
||||
.orElse(versionCO.getUpdateDescribe());
|
||||
}
|
||||
|
||||
private VersionManage getVersion(AppExtCommand cmd) {
|
||||
return versionManageService
|
||||
.getVersion(cmd.requireReqSysOrigin()
|
||||
, cmd.getReqClient().getClientName()
|
||||
, cmd.getReqAppIntel().getChannel()
|
||||
, Boolean.FALSE);
|
||||
}
|
||||
}
|
||||
|
||||
private VersionManage getVersion(AppExtCommand cmd) {
|
||||
return versionManageService
|
||||
.getVersion(cmd.requireReqSysOrigin()
|
||||
, cmd.getReqClient().getClientName()
|
||||
, null
|
||||
, Boolean.FALSE);
|
||||
}
|
||||
}
|
||||
|
||||
@ -23,8 +23,9 @@ import org.springframework.stereotype.Service;
|
||||
@RequiredArgsConstructor
|
||||
public class RoomRegionFilterCommon {
|
||||
|
||||
// account 1001041, 1001545
|
||||
// account 1001035, 1001041, 1001545
|
||||
private static final Set<Long> ALL_REGION_ROOM_WHITELIST_USER_IDS = Set.of(
|
||||
2044358825183604738L,
|
||||
2044700023064686594L,
|
||||
2047206645188063233L
|
||||
);
|
||||
|
||||
@ -4,9 +4,10 @@ import com.google.common.collect.Lists;
|
||||
import com.google.common.collect.Maps;
|
||||
import com.red.circle.common.business.core.enums.SysOriginPlatformEnum;
|
||||
import com.red.circle.common.business.enums.SVIPLevelEnum;
|
||||
import com.red.circle.other.app.convertor.live.RoomProfileAppConvertor;
|
||||
import com.red.circle.other.app.convertor.user.UserProfileAppConvertor;
|
||||
import com.red.circle.other.app.dto.clientobject.room.RoomVoiceProfileCO;
|
||||
import com.red.circle.other.app.convertor.live.RoomProfileAppConvertor;
|
||||
import com.red.circle.other.app.convertor.user.UserProfileAppConvertor;
|
||||
import com.red.circle.other.app.dto.clientobject.family.RoomSettingCO;
|
||||
import com.red.circle.other.app.dto.clientobject.room.RoomVoiceProfileCO;
|
||||
import com.red.circle.other.domain.gateway.user.UserProfileGateway;
|
||||
import com.red.circle.other.infra.database.mongo.entity.live.ActiveVoiceRoom;
|
||||
import com.red.circle.other.infra.database.mongo.entity.live.RoomProfileManager;
|
||||
@ -166,12 +167,16 @@ public class RoomVoiceProfileCommon {
|
||||
return null;
|
||||
}
|
||||
|
||||
RoomVoiceProfileCO roomVoiceProfile = roomProfileAppConvertor.toRoomVoiceProfileCO(manager);
|
||||
roomVoiceProfile.setUserProfile(userProfile);
|
||||
roomVoiceProfile
|
||||
.putExistsPassword(StringUtils.isNotBlank(manager.getSetting().getPassword()))
|
||||
.putRoomDefaultSetting(roomProfileAppConvertor.toRoomSettingCO(manager.getSetting()))
|
||||
.putRoomMemberQuantity(manager.getCounter().getMemberCount());
|
||||
RoomVoiceProfileCO roomVoiceProfile = roomProfileAppConvertor.toRoomVoiceProfileCO(manager);
|
||||
roomVoiceProfile.setUserProfile(userProfile);
|
||||
RoomSettingCO roomSetting = roomProfileAppConvertor.toRoomSettingCO(manager.getSetting());
|
||||
if (Objects.nonNull(roomSetting)) {
|
||||
roomSetting.setPassword("");
|
||||
}
|
||||
roomVoiceProfile
|
||||
.putExistsPassword(StringUtils.isNotBlank(manager.getSetting().getPassword()))
|
||||
.putRoomDefaultSetting(roomSetting)
|
||||
.putRoomMemberQuantity(manager.getCounter().getMemberCount());
|
||||
roomVoiceProfile.setRoomBadgeIcons(Optional.ofNullable(manager.getUseBadges()).map(
|
||||
badges -> badges.stream().map(UseBadgeDTO::getSelectUrl)
|
||||
.toList()).orElse(Lists.newArrayList())
|
||||
|
||||
@ -3,12 +3,17 @@ package com.red.circle.other.app.common.task;
|
||||
import cn.hutool.http.HttpResponse;
|
||||
import cn.hutool.http.HttpUtil;
|
||||
import com.alibaba.fastjson.JSON;
|
||||
import com.red.circle.component.mq.MessageEvent;
|
||||
import com.red.circle.mq.rocket.business.service.MessageSenderService;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.Instant;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import lombok.Data;
|
||||
import lombok.experimental.Accessors;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.ObjectProvider;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@ -17,6 +22,9 @@ import org.springframework.stereotype.Component;
|
||||
public class TaskCenterGoClient {
|
||||
|
||||
private static final String EVENT_TYPE_GAME_CONSUME_GOLD = "GAME_CONSUME_GOLD";
|
||||
private static final String EVENT_TYPE_GIFT_CONSUME_GOLD = "GIFT_CONSUME_GOLD";
|
||||
|
||||
private final ObjectProvider<MessageSenderService> messageSenderServiceProvider;
|
||||
|
||||
@Value("${task.center.go.enabled:true}")
|
||||
private boolean enabled;
|
||||
@ -30,6 +38,19 @@ public class TaskCenterGoClient {
|
||||
@Value("${task.center.go.timeout-millis:3000}")
|
||||
private Integer timeoutMillis;
|
||||
|
||||
@Value("${task.center.event.mq.enabled:false}")
|
||||
private boolean mqEnabled;
|
||||
|
||||
@Value("${task.center.event.mq.topic:RC_DEFAULT_APP_ORDINARY}")
|
||||
private String mqTopic;
|
||||
|
||||
@Value("${task.center.event.mq.tag:task_center_event}")
|
||||
private String mqTag;
|
||||
|
||||
public TaskCenterGoClient(ObjectProvider<MessageSenderService> messageSenderServiceProvider) {
|
||||
this.messageSenderServiceProvider = messageSenderServiceProvider;
|
||||
}
|
||||
|
||||
public void reportGameConsumeGold(String sysOrigin,
|
||||
Long userId,
|
||||
String provider,
|
||||
@ -51,7 +72,56 @@ public class TaskCenterGoClient {
|
||||
.setPayload(payload));
|
||||
}
|
||||
|
||||
public void reportGiftConsumeGold(String sysOrigin,
|
||||
Long userId,
|
||||
Long trackId,
|
||||
Long giftId,
|
||||
String giftName,
|
||||
String giftTab,
|
||||
BigDecimal giftCandy,
|
||||
Integer quantity,
|
||||
Integer acceptUserSize,
|
||||
BigDecimal consumeGold,
|
||||
Long roomId,
|
||||
Long dynamicContentId,
|
||||
String source) {
|
||||
if (Objects.isNull(userId) || Objects.isNull(trackId) || Objects.isNull(consumeGold)
|
||||
|| consumeGold.compareTo(BigDecimal.ZERO) <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
long deltaGold = consumeGold.longValue();
|
||||
if (deltaGold <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
Map<String, Object> payload = new HashMap<>();
|
||||
payload.put("trackId", trackId);
|
||||
payload.put("giftId", giftId);
|
||||
payload.put("giftName", giftName);
|
||||
payload.put("giftTab", giftTab);
|
||||
payload.put("giftCandy", giftCandy);
|
||||
payload.put("quantity", quantity);
|
||||
payload.put("acceptUserSize", acceptUserSize);
|
||||
payload.put("actualAmount", consumeGold);
|
||||
payload.put("roomId", roomId);
|
||||
payload.put("dynamicContentId", dynamicContentId);
|
||||
payload.put("source", source);
|
||||
|
||||
reportEvent(new TaskCenterEventRequest()
|
||||
.setSysOrigin(sysOrigin)
|
||||
.setEventId("GIFT_CONSUME:" + trackId)
|
||||
.setEventType(EVENT_TYPE_GIFT_CONSUME_GOLD)
|
||||
.setUserId(userId)
|
||||
.setDeltaValue(deltaGold)
|
||||
.setOccurredAt(Instant.now().toString())
|
||||
.setPayload(payload));
|
||||
}
|
||||
|
||||
private void reportEvent(TaskCenterEventRequest request) {
|
||||
if (mqEnabled) {
|
||||
publishMqEvent(request);
|
||||
}
|
||||
if (!enabled) {
|
||||
return;
|
||||
}
|
||||
@ -78,6 +148,24 @@ public class TaskCenterGoClient {
|
||||
}
|
||||
}
|
||||
|
||||
private void publishMqEvent(TaskCenterEventRequest request) {
|
||||
try {
|
||||
MessageSenderService senderService = messageSenderServiceProvider.getIfAvailable();
|
||||
if (Objects.isNull(senderService)) {
|
||||
log.warn("Task center mq sender missing, skip event report. eventId={}", request.getEventId());
|
||||
return;
|
||||
}
|
||||
senderService.sendEventMessage(MessageEvent.builder()
|
||||
.consumeId(request.getEventId())
|
||||
.topicString(defaultIfBlank(mqTopic, "RC_DEFAULT_APP_ORDINARY"))
|
||||
.tag(defaultIfBlank(mqTag, "task_center_event"))
|
||||
.body(request)
|
||||
.build());
|
||||
} catch (Exception e) {
|
||||
log.warn("Task center mq report error. eventId={}", request.getEventId(), e);
|
||||
}
|
||||
}
|
||||
|
||||
private String trimRightSlash(String value) {
|
||||
while (value.endsWith("/")) {
|
||||
value = value.substring(0, value.length() - 1);
|
||||
@ -85,6 +173,11 @@ public class TaskCenterGoClient {
|
||||
return value;
|
||||
}
|
||||
|
||||
private String defaultIfBlank(String value, String fallback) {
|
||||
String normalized = Objects.toString(value, "").trim();
|
||||
return normalized.isEmpty() ? fallback : normalized;
|
||||
}
|
||||
|
||||
@Data
|
||||
@Accessors(chain = true)
|
||||
private static class TaskCenterEventRequest {
|
||||
|
||||
@ -336,8 +336,10 @@ public class LoginLoggerListener implements MessageListener {
|
||||
return LoginBlockResult.notBlocked();
|
||||
}
|
||||
List<String> reasons = new ArrayList<>();
|
||||
if (IpCountryUtils.isMainlandChina(ipGeoData)) {
|
||||
reasons.add("中国大陆IP");
|
||||
if (IpCountryUtils.isBlockedIpRegion(ipGeoData)) {
|
||||
reasons.add(IpCountryUtils.isMainlandChina(ipGeoData)
|
||||
? "中国大陆IP"
|
||||
: "受限IP地区:" + blockedRegionName(ipGeoData));
|
||||
}
|
||||
if (Objects.equals("Asia/Shanghai", event.getZoneId())) {
|
||||
reasons.add("中国时区");
|
||||
@ -347,6 +349,22 @@ public class LoginLoggerListener implements MessageListener {
|
||||
: new LoginBlockResult(true, String.join(",", reasons));
|
||||
}
|
||||
|
||||
private String blockedRegionName(IpCountryUtils.IpGeoData ipGeoData) {
|
||||
if (Objects.isNull(ipGeoData) || ipGeoData.isEmpty()) {
|
||||
return "";
|
||||
}
|
||||
if (StringUtils.isNotBlank(ipGeoData.region())) {
|
||||
return ipGeoData.region();
|
||||
}
|
||||
if (StringUtils.isNotBlank(ipGeoData.countryName())) {
|
||||
return ipGeoData.countryName();
|
||||
}
|
||||
if (StringUtils.isNotBlank(ipGeoData.countryCode())) {
|
||||
return ipGeoData.countryCode();
|
||||
}
|
||||
return ipGeoData.region();
|
||||
}
|
||||
|
||||
private boolean isLoginWhiteAccount(LoginLoggerEvent event, UserProfile userProfile) {
|
||||
if (Objects.isNull(userProfile) || StringUtils.isBlank(userProfile.getAccount())) {
|
||||
return false;
|
||||
|
||||
@ -62,10 +62,11 @@ public class UserIdentityServiceImpl implements UserIdentityService {
|
||||
.setSuperAdmin(isSuperAdmin)
|
||||
.setAdmin(isValidAdmin)
|
||||
.setBdLeader(ResponseAssert.requiredSuccess(bdTeamLeaderClient.check(userId)))
|
||||
.setManager(isTeamManager)
|
||||
.setManager(Boolean.FALSE)
|
||||
.setYumiManager(isTeamManager)
|
||||
.setAppManager(isManager)
|
||||
;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Boolean hasPermission(PropsStoreTypeQryCmd cmd) {
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
package com.red.circle.other.app.common.room;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@ -14,6 +16,16 @@ import org.junit.jupiter.api.Test;
|
||||
|
||||
class RoomRegionFilterCommonTest {
|
||||
|
||||
@Test
|
||||
void canViewAllRegions_shouldAllowConfiguredWhiteListUser() {
|
||||
UserRegionGateway userRegionGateway = mock(UserRegionGateway.class);
|
||||
RoomRegionFilterCommon roomRegionFilterCommon = new RoomRegionFilterCommon(userRegionGateway);
|
||||
|
||||
assertTrue(roomRegionFilterCommon.canViewAllRegions(2044358825183604738L));
|
||||
assertFalse(roomRegionFilterCommon.canViewAllRegions(1001035L));
|
||||
assertFalse(roomRegionFilterCommon.canViewAllRegions(null));
|
||||
}
|
||||
|
||||
@Test
|
||||
void filterActiveVoiceRoomsByRegion_shouldKeepOnlyCurrentRegionRooms() {
|
||||
UserRegionGateway userRegionGateway = mock(UserRegionGateway.class);
|
||||
|
||||
@ -17,6 +17,10 @@ public class TeamBdMemberCO {
|
||||
|
||||
private Integer memberCount;
|
||||
|
||||
private Integer agencyNumber;
|
||||
|
||||
private Integer bdNumber;
|
||||
|
||||
private List<TeamBdMemberBillCO> memberBillList;
|
||||
|
||||
private BdHistoryCO bdHistoryCO;
|
||||
|
||||
@ -55,10 +55,16 @@ public class UserIdentityVO implements Serializable {
|
||||
private Boolean superAdmin;
|
||||
|
||||
/**
|
||||
* 是否是经理
|
||||
* 旧经理字段,Yumi 经理身份不再使用.
|
||||
*/
|
||||
private Boolean manager;
|
||||
|
||||
/**
|
||||
* 是否是 Yumi 经理.
|
||||
*/
|
||||
@JsonProperty("yumi_manager")
|
||||
private Boolean yumiManager;
|
||||
|
||||
/**
|
||||
* 是否是App管理员角色里的经理.
|
||||
*/
|
||||
|
||||
@ -51,9 +51,15 @@ public class UserHistoryIdentity extends TimestampBaseEntity {
|
||||
private Boolean host;
|
||||
|
||||
/**
|
||||
* 是否做过团队经理.
|
||||
* 是否做过旧团队经理.
|
||||
*/
|
||||
@TableField("is_manager")
|
||||
private Boolean manager;
|
||||
|
||||
/**
|
||||
* 是否做过 Yumi 团队经理.
|
||||
*/
|
||||
@TableField("is_yumi_manager")
|
||||
private Boolean yumiManager;
|
||||
|
||||
}
|
||||
|
||||
@ -46,10 +46,15 @@ public interface GameListConfigService extends BaseService<GameListConfig> {
|
||||
* 获取灵仙当前启用的配置档案.
|
||||
*/
|
||||
String getActiveLingxianProfile(String sysOrigin);
|
||||
|
||||
/**
|
||||
* 查询游戏信息
|
||||
*/
|
||||
|
||||
/**
|
||||
* 获取灵仙当前启用配置的 appKey.
|
||||
*/
|
||||
String getActiveLingxianAppKey(String sysOrigin);
|
||||
|
||||
/**
|
||||
* 查询游戏信息
|
||||
*/
|
||||
GameListConfig getByGameCode(String gameCode, String sysOrigin);
|
||||
|
||||
/**
|
||||
|
||||
@ -213,6 +213,27 @@ public class GameListConfigServiceImpl extends
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getActiveLingxianAppKey(String sysOrigin) {
|
||||
if (StringUtils.isBlank(sysOrigin)) {
|
||||
return "";
|
||||
}
|
||||
try {
|
||||
return Optional.ofNullable(jdbcTemplate.queryForObject("""
|
||||
SELECT app_key
|
||||
FROM lingxian_provider_config
|
||||
WHERE sys_origin = ?
|
||||
AND active = 1
|
||||
ORDER BY update_time DESC
|
||||
LIMIT 1
|
||||
""", String.class, sysOrigin))
|
||||
.map(String::trim)
|
||||
.orElse("");
|
||||
} catch (Exception e) {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public GameListConfig getByGameCode(String gameCode, String sysOrigin) {
|
||||
return query()
|
||||
|
||||
@ -22,6 +22,11 @@ public interface UserHistoryIdentityService extends BaseService<UserHistoryIdent
|
||||
* 保存身份=manager.
|
||||
*/
|
||||
void saveManager(Long userId);
|
||||
|
||||
/**
|
||||
* 保存身份=yumi manager.
|
||||
*/
|
||||
void saveYumiManager(Long userId);
|
||||
|
||||
/**
|
||||
* 保存身份=host.
|
||||
|
||||
@ -43,6 +43,7 @@ public class UserHistoryIdentityServiceImpl extends
|
||||
.setAgent(Boolean.FALSE)
|
||||
.setHost(Boolean.FALSE)
|
||||
.setManager(Boolean.FALSE)
|
||||
.setYumiManager(Boolean.FALSE)
|
||||
);
|
||||
} catch (DuplicateKeyException ex) {
|
||||
// ignore
|
||||
@ -66,6 +67,31 @@ public class UserHistoryIdentityServiceImpl extends
|
||||
.setAgent(Boolean.FALSE)
|
||||
.setHost(Boolean.FALSE)
|
||||
.setManager(Boolean.TRUE)
|
||||
.setYumiManager(Boolean.FALSE)
|
||||
);
|
||||
} catch (DuplicateKeyException ex) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void saveYumiManager(Long userId) {
|
||||
if (exists(userId)) {
|
||||
update()
|
||||
.set(UserHistoryIdentity::getYumiManager, Boolean.TRUE)
|
||||
.eq(UserHistoryIdentity::getId, userId)
|
||||
.execute();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
save(new UserHistoryIdentity()
|
||||
.setId(userId)
|
||||
.setBd(Boolean.FALSE)
|
||||
.setAgent(Boolean.FALSE)
|
||||
.setHost(Boolean.FALSE)
|
||||
.setManager(Boolean.FALSE)
|
||||
.setYumiManager(Boolean.TRUE)
|
||||
);
|
||||
} catch (DuplicateKeyException ex) {
|
||||
// ignore
|
||||
@ -88,6 +114,7 @@ public class UserHistoryIdentityServiceImpl extends
|
||||
.setAgent(Boolean.FALSE)
|
||||
.setHost(Boolean.TRUE)
|
||||
.setManager(Boolean.FALSE)
|
||||
.setYumiManager(Boolean.FALSE)
|
||||
);
|
||||
} catch (DuplicateKeyException ex) {
|
||||
// ignore
|
||||
@ -112,6 +139,7 @@ public class UserHistoryIdentityServiceImpl extends
|
||||
.setAgent(Boolean.TRUE)
|
||||
.setHost(Boolean.TRUE)
|
||||
.setManager(Boolean.FALSE)
|
||||
.setYumiManager(Boolean.FALSE)
|
||||
);
|
||||
} catch (DuplicateKeyException ex) {
|
||||
// ignore
|
||||
@ -174,6 +202,9 @@ public class UserHistoryIdentityServiceImpl extends
|
||||
if (Objects.equals(identity.getManager(), Boolean.TRUE)) {
|
||||
res.add("Manager");
|
||||
}
|
||||
if (Objects.equals(identity.getYumiManager(), Boolean.TRUE)) {
|
||||
res.add("YumiManager");
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
|
||||
@ -31,6 +31,12 @@ public class IpCountryUtils {
|
||||
private static final List<String> ALLOWED_CHINA_REGIONS = List.of(
|
||||
"香港", "澳门", "澳門", "台湾", "台灣", "Hong Kong", "Macau", "Macao", "Taiwan"
|
||||
);
|
||||
private static final List<String> BLOCKED_COUNTRY_CODES = List.of(
|
||||
"MO", "SG", "TW"
|
||||
);
|
||||
private static final List<String> BLOCKED_COUNTRY_NAMES = List.of(
|
||||
"澳门", "澳門", "台湾", "台灣", "Macau", "Macao", "Taiwan", "新加坡", "Singapore"
|
||||
);
|
||||
private static final List<IpGeoProvider> IP_GEO_PROVIDERS = List.of(
|
||||
new IpGeoProvider("ip.sb", "https://api.ip.sb/geoip/%s"),
|
||||
new IpGeoProvider("freeipapi", "https://freeipapi.com/api/json/%s"),
|
||||
@ -95,6 +101,10 @@ public class IpCountryUtils {
|
||||
return isMainlandChina(getGeoData(ip));
|
||||
}
|
||||
|
||||
public static boolean isBlockedIpRegion(IpGeoData data) {
|
||||
return isMainlandChina(data) || isBlockedCountryOrRegion(data);
|
||||
}
|
||||
|
||||
public static boolean isMainlandChina(IpGeoData data) {
|
||||
if (data == null || data.isEmpty()) {
|
||||
return false;
|
||||
@ -108,6 +118,18 @@ public class IpCountryUtils {
|
||||
return StringUtils.isBlank(data.region()) || !ALLOWED_CHINA_REGIONS.contains(data.region());
|
||||
}
|
||||
|
||||
private static boolean isBlockedCountryOrRegion(IpGeoData data) {
|
||||
if (data == null || data.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
if (StringUtils.isNotBlank(data.countryCode())
|
||||
&& BLOCKED_COUNTRY_CODES.contains(data.countryCode().trim().toUpperCase(Locale.ROOT))) {
|
||||
return true;
|
||||
}
|
||||
return BLOCKED_COUNTRY_NAMES.contains(data.countryName())
|
||||
|| BLOCKED_COUNTRY_NAMES.contains(data.region());
|
||||
}
|
||||
|
||||
private IpGeoData requestIpGeoData(CloseableHttpClient httpClient, IpGeoProvider provider, String ip) {
|
||||
String requestUrl = provider.buildUrl(ip);
|
||||
HttpGet httpGet = new HttpGet(requestUrl);
|
||||
|
||||
@ -69,9 +69,47 @@ public class IpCountryUtilsTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void allowChinaSpecialRegions() {
|
||||
public void allowHongKongRegion() {
|
||||
IpCountryUtils.IpGeoData data = new IpCountryUtils.IpGeoData("CN", "中国", "香港", "test");
|
||||
|
||||
assertFalse(IpCountryUtils.isMainlandChina(data));
|
||||
assertFalse(IpCountryUtils.isBlockedIpRegion(data));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void allowHongKongCountryCode() {
|
||||
IpCountryUtils.IpGeoData data = new IpCountryUtils.IpGeoData("HK", "Hong Kong", "", "test");
|
||||
|
||||
assertFalse(IpCountryUtils.isMainlandChina(data));
|
||||
assertFalse(IpCountryUtils.isBlockedIpRegion(data));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void blockMacauCountryCode() {
|
||||
IpCountryUtils.IpGeoData data = new IpCountryUtils.IpGeoData("MO", "Macao", "", "test");
|
||||
|
||||
assertTrue(IpCountryUtils.isBlockedIpRegion(data));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void blockSingaporeCountryCode() {
|
||||
IpCountryUtils.IpGeoData data = new IpCountryUtils.IpGeoData("SG", "Singapore", "", "test");
|
||||
|
||||
assertTrue(IpCountryUtils.isBlockedIpRegion(data));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void blockTaiwanRegion() {
|
||||
IpCountryUtils.IpGeoData data = new IpCountryUtils.IpGeoData("CN", "中国", "台湾", "test");
|
||||
|
||||
assertFalse(IpCountryUtils.isMainlandChina(data));
|
||||
assertTrue(IpCountryUtils.isBlockedIpRegion(data));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void blockTaiwanCountryCode() {
|
||||
IpCountryUtils.IpGeoData data = new IpCountryUtils.IpGeoData("TW", "Taiwan", "", "test");
|
||||
|
||||
assertTrue(IpCountryUtils.isBlockedIpRegion(data));
|
||||
}
|
||||
}
|
||||
|
||||
@ -62,4 +62,10 @@ public class UserHistoryIdentityEndpoint implements UserHistoryIdentityClientApi
|
||||
historyIdentityClientService.saveManager(userId);
|
||||
return ResultResponse.success();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResultResponse<Void> saveYumiManager(Long userId) {
|
||||
historyIdentityClientService.saveYumiManager(userId);
|
||||
return ResultResponse.success();
|
||||
}
|
||||
}
|
||||
|
||||
@ -23,4 +23,6 @@ public interface UserHistoryIdentityClientService {
|
||||
void saveBd(Long userId);
|
||||
|
||||
void saveManager(Long userId);
|
||||
|
||||
void saveYumiManager(Long userId);
|
||||
}
|
||||
|
||||
@ -55,4 +55,9 @@ public class UserHistoryIdentityClientServiceImpl implements UserHistoryIdentity
|
||||
public void saveManager(Long userId) {
|
||||
historyIdentityService.saveManager(userId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void saveYumiManager(Long userId) {
|
||||
historyIdentityService.saveYumiManager(userId);
|
||||
}
|
||||
}
|
||||
|
||||
@ -29,6 +29,7 @@ import com.red.circle.other.inner.asserts.user.UserErrorCode;
|
||||
import com.red.circle.other.inner.endpoint.user.user.UserSvipClient;
|
||||
import com.red.circle.wallet.app.dto.cmd.FreightDealerToUserShipCmd;
|
||||
import com.red.circle.wallet.app.common.invite.InviteCampaignGoClient;
|
||||
import com.red.circle.wallet.app.common.task.TaskCenterGoClient;
|
||||
import com.red.circle.wallet.domain.gateway.FreightShipGateway;
|
||||
import com.red.circle.wallet.domain.gateway.WalletGoldGateway;
|
||||
import com.red.circle.wallet.domain.wallet.WalletReceipt;
|
||||
@ -84,6 +85,7 @@ public class DealerToUserShipCmdExe {
|
||||
private final UserProfileClient userProfileClient;
|
||||
private final InviteUserClient inviteUserClient;
|
||||
private final InviteCampaignGoClient inviteCampaignGoClient;
|
||||
private final TaskCenterGoClient taskCenterGoClient;
|
||||
|
||||
public BigDecimal execute(FreightDealerToUserShipCmd cmd) {
|
||||
controlSwitch.execute(cmd.requireReqSysOriginChildEnum());
|
||||
@ -169,6 +171,15 @@ public class DealerToUserShipCmdExe {
|
||||
|
||||
// 累计充值
|
||||
incrRechargeQuantity(cmd);
|
||||
taskCenterGoClient.reportSellerAgentRechargeGold(cmd.requireReqSysOrigin(),
|
||||
cmd.getAcceptUserId(),
|
||||
runningWater.getId(),
|
||||
cmd.getQuantity(),
|
||||
getGoldToSUD(cmd),
|
||||
cmd.requiredReqUserId(),
|
||||
freightBalance.getUserId(),
|
||||
freightSeller.getId(),
|
||||
cmd.getReqTraceId());
|
||||
notifyInviteCampaignRecharge(cmd, runningWater);
|
||||
|
||||
//累计今日充值,每日累计充值抽奖活动
|
||||
|
||||
@ -30,12 +30,13 @@ import com.red.circle.other.inner.model.dto.user.UserOneTimeTaskDTO;
|
||||
import com.red.circle.other.inner.model.dto.user.UserProfileDTO;
|
||||
import com.red.circle.tool.core.date.LocalDateTimeUtils;
|
||||
import com.red.circle.tool.core.json.JacksonUtils;
|
||||
import com.red.circle.tool.core.num.ArithmeticUtils;
|
||||
import com.red.circle.tool.core.num.ArithmeticUtils;
|
||||
import com.red.circle.tool.core.tuple.PennyAmount;
|
||||
import com.red.circle.other.inner.asserts.user.UserErrorCode;
|
||||
import com.red.circle.wallet.app.common.invite.InviteCampaignGoClient;
|
||||
import com.red.circle.wallet.app.common.task.TaskCenterGoClient;
|
||||
import com.red.circle.wallet.app.dto.cmd.FreightShipSendCmd;
|
||||
import com.red.circle.wallet.app.dto.cmd.SalaryWithdrawCmd;
|
||||
import com.red.circle.wallet.app.dto.cmd.SalaryWithdrawCmd;
|
||||
import com.red.circle.wallet.domain.gateway.FreightShipGateway;
|
||||
import com.red.circle.wallet.domain.gateway.WalletGoldGateway;
|
||||
import com.red.circle.wallet.domain.wallet.WalletReceipt;
|
||||
@ -96,6 +97,7 @@ public class SendFreightShipCmdExe {
|
||||
private final InviteCampaignGoClient inviteCampaignGoClient;
|
||||
private final UserRegionClient userRegionClient;
|
||||
private final OfficialNoticeClient officialNoticeClient;
|
||||
private final TaskCenterGoClient taskCenterGoClient;
|
||||
|
||||
public BigDecimal execute(FreightShipSendCmd cmd) {
|
||||
|
||||
@ -190,6 +192,13 @@ public class SendFreightShipCmdExe {
|
||||
|
||||
// 累计充值
|
||||
incrRechargeQuantity(cmd);
|
||||
taskCenterGoClient.reportShippingAgentRechargeGold(cmd.requireReqSysOrigin(),
|
||||
cmd.getAcceptUserId(),
|
||||
runningWater.getId(),
|
||||
cmd.getQuantity(),
|
||||
getGoldToSUD(cmd),
|
||||
cmd.requiredReqUserId(),
|
||||
cmd.getReqTraceId());
|
||||
notifyInviteCampaignRecharge(cmd, runningWater);
|
||||
|
||||
//累计今日充值,每日累计充值抽奖活动
|
||||
|
||||
@ -0,0 +1,218 @@
|
||||
package com.red.circle.wallet.app.common.task;
|
||||
|
||||
import com.alibaba.fastjson.JSON;
|
||||
import com.red.circle.component.mq.MessageEvent;
|
||||
import com.red.circle.mq.rocket.business.service.MessageSenderService;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.math.BigDecimal;
|
||||
import java.math.RoundingMode;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.URL;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.Instant;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import lombok.Data;
|
||||
import lombok.experimental.Accessors;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.ObjectProvider;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Slf4j
|
||||
@Component
|
||||
public class TaskCenterGoClient {
|
||||
|
||||
private static final String EVENT_TYPE_RECHARGE_GOLD = "RECHARGE_GOLD";
|
||||
|
||||
private final ObjectProvider<MessageSenderService> messageSenderServiceProvider;
|
||||
|
||||
@Value("${task.center.go.enabled:true}")
|
||||
private boolean enabled;
|
||||
|
||||
@Value("${task.center.go.base-url:}")
|
||||
private String baseUrl;
|
||||
|
||||
@Value("${task.center.go.internal-token:}")
|
||||
private String internalToken;
|
||||
|
||||
@Value("${task.center.go.timeout-millis:3000}")
|
||||
private Integer timeoutMillis;
|
||||
|
||||
@Value("${task.center.event.mq.enabled:false}")
|
||||
private boolean mqEnabled;
|
||||
|
||||
@Value("${task.center.event.mq.topic:RC_DEFAULT_APP_ORDINARY}")
|
||||
private String mqTopic;
|
||||
|
||||
@Value("${task.center.event.mq.tag:task_center_event}")
|
||||
private String mqTag;
|
||||
|
||||
public TaskCenterGoClient(ObjectProvider<MessageSenderService> messageSenderServiceProvider) {
|
||||
this.messageSenderServiceProvider = messageSenderServiceProvider;
|
||||
}
|
||||
|
||||
public void reportShippingAgentRechargeGold(String sysOrigin,
|
||||
Long userId,
|
||||
Long runningWaterId,
|
||||
BigDecimal goldQuantity,
|
||||
BigDecimal usdQuantity,
|
||||
Long freightUserId,
|
||||
String traceId) {
|
||||
Map<String, Object> payload = new HashMap<>();
|
||||
payload.put("channel", "SHIPPING_AGENT");
|
||||
payload.put("runningWaterId", runningWaterId);
|
||||
payload.put("goldQuantity", goldQuantity);
|
||||
payload.put("usdQuantity", usdQuantity);
|
||||
payload.put("freightUserId", freightUserId);
|
||||
payload.put("traceId", traceId);
|
||||
reportRechargeGold(sysOrigin, userId, runningWaterId, goldQuantity, "SHIPPING_AGENT", payload);
|
||||
}
|
||||
|
||||
public void reportSellerAgentRechargeGold(String sysOrigin,
|
||||
Long userId,
|
||||
Long runningWaterId,
|
||||
BigDecimal goldQuantity,
|
||||
BigDecimal usdQuantity,
|
||||
Long sellerUserId,
|
||||
Long freightUserId,
|
||||
Long sellerId,
|
||||
String traceId) {
|
||||
Map<String, Object> payload = new HashMap<>();
|
||||
payload.put("channel", "SELLER_AGENT");
|
||||
payload.put("runningWaterId", runningWaterId);
|
||||
payload.put("goldQuantity", goldQuantity);
|
||||
payload.put("usdQuantity", usdQuantity);
|
||||
payload.put("sellerUserId", sellerUserId);
|
||||
payload.put("freightUserId", freightUserId);
|
||||
payload.put("sellerId", sellerId);
|
||||
payload.put("traceId", traceId);
|
||||
reportRechargeGold(sysOrigin, userId, runningWaterId, goldQuantity, "SELLER_AGENT", payload);
|
||||
}
|
||||
|
||||
private void reportRechargeGold(String sysOrigin,
|
||||
Long userId,
|
||||
Long runningWaterId,
|
||||
BigDecimal goldQuantity,
|
||||
String channel,
|
||||
Map<String, Object> payload) {
|
||||
if (Objects.isNull(userId) || Objects.isNull(runningWaterId) || Objects.isNull(goldQuantity)) {
|
||||
return;
|
||||
}
|
||||
long deltaValue = goldQuantity.setScale(0, RoundingMode.DOWN).longValue();
|
||||
if (deltaValue <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
reportEvent(new TaskCenterEventRequest()
|
||||
.setSysOrigin(sysOrigin)
|
||||
.setEventId("RECHARGE_GOLD:" + channel + ":" + runningWaterId)
|
||||
.setEventType(EVENT_TYPE_RECHARGE_GOLD)
|
||||
.setUserId(userId)
|
||||
.setDeltaValue(deltaValue)
|
||||
.setOccurredAt(Instant.now().toString())
|
||||
.setPayload(payload));
|
||||
}
|
||||
|
||||
private void reportEvent(TaskCenterEventRequest request) {
|
||||
if (mqEnabled) {
|
||||
publishMqEvent(request);
|
||||
}
|
||||
if (!enabled) {
|
||||
return;
|
||||
}
|
||||
String normalizedBaseUrl = trimRightSlash(Objects.toString(baseUrl, "").trim());
|
||||
if (normalizedBaseUrl.isEmpty()) {
|
||||
log.warn("Task center go baseUrl is blank, skip event report. eventId={}", request.getEventId());
|
||||
return;
|
||||
}
|
||||
|
||||
HttpURLConnection connection = null;
|
||||
try {
|
||||
URL url = new URL(normalizedBaseUrl + "/internal/task-center/event");
|
||||
connection = (HttpURLConnection) url.openConnection();
|
||||
int timeout = Objects.requireNonNullElse(timeoutMillis, 3000);
|
||||
connection.setConnectTimeout(timeout);
|
||||
connection.setReadTimeout(timeout);
|
||||
connection.setRequestMethod("POST");
|
||||
connection.setDoOutput(true);
|
||||
connection.setRequestProperty("Content-Type", "application/json");
|
||||
connection.setRequestProperty("Accept", "application/json");
|
||||
connection.setRequestProperty("X-Internal-Token", Objects.toString(internalToken, ""));
|
||||
|
||||
byte[] body = JSON.toJSONString(request).getBytes(StandardCharsets.UTF_8);
|
||||
try (OutputStream outputStream = connection.getOutputStream()) {
|
||||
outputStream.write(body);
|
||||
}
|
||||
|
||||
int status = connection.getResponseCode();
|
||||
if (status < 200 || status >= 300) {
|
||||
log.warn("Task center go report failed. status={}, body={}, eventId={}",
|
||||
status, readResponse(connection), request.getEventId());
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn("Task center go report error. eventId={}", request.getEventId(), e);
|
||||
} finally {
|
||||
if (Objects.nonNull(connection)) {
|
||||
connection.disconnect();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void publishMqEvent(TaskCenterEventRequest request) {
|
||||
try {
|
||||
MessageSenderService senderService = messageSenderServiceProvider.getIfAvailable();
|
||||
if (Objects.isNull(senderService)) {
|
||||
log.warn("Task center mq sender missing, skip event report. eventId={}", request.getEventId());
|
||||
return;
|
||||
}
|
||||
senderService.sendEventMessage(MessageEvent.builder()
|
||||
.consumeId(request.getEventId())
|
||||
.topicString(defaultIfBlank(mqTopic, "RC_DEFAULT_APP_ORDINARY"))
|
||||
.tag(defaultIfBlank(mqTag, "task_center_event"))
|
||||
.body(request)
|
||||
.build());
|
||||
} catch (Exception e) {
|
||||
log.warn("Task center mq report error. eventId={}", request.getEventId(), e);
|
||||
}
|
||||
}
|
||||
|
||||
private String readResponse(HttpURLConnection connection) {
|
||||
try (InputStream inputStream = connection.getErrorStream() != null
|
||||
? connection.getErrorStream()
|
||||
: connection.getInputStream()) {
|
||||
if (Objects.isNull(inputStream)) {
|
||||
return "";
|
||||
}
|
||||
return new String(inputStream.readAllBytes(), StandardCharsets.UTF_8);
|
||||
} catch (Exception e) {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
private String trimRightSlash(String value) {
|
||||
while (value.endsWith("/")) {
|
||||
value = value.substring(0, value.length() - 1);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
private String defaultIfBlank(String value, String fallback) {
|
||||
String normalized = Objects.toString(value, "").trim();
|
||||
return normalized.isEmpty() ? fallback : normalized;
|
||||
}
|
||||
|
||||
@Data
|
||||
@Accessors(chain = true)
|
||||
private static class TaskCenterEventRequest {
|
||||
private String sysOrigin;
|
||||
private String eventId;
|
||||
private String eventType;
|
||||
private Long userId;
|
||||
private Long deltaValue;
|
||||
private String occurredAt;
|
||||
private Map<String, Object> payload;
|
||||
}
|
||||
}
|
||||
@ -15,10 +15,10 @@ public interface WalletGoldBalanceService extends BaseService<WalletGoldBalance>
|
||||
*/
|
||||
boolean exists(Long userId);
|
||||
|
||||
/**
|
||||
* 初始化钱包,不存在创建.
|
||||
*/
|
||||
void init(String sysOrigin, Long userId, Long income, Long expenditure);
|
||||
/**
|
||||
* 初始化钱包,不存在创建.
|
||||
*/
|
||||
boolean init(String sysOrigin, Long userId, Long income, Long expenditure);
|
||||
|
||||
/**
|
||||
* 获取用户余额(注意可能出现负数).
|
||||
@ -27,15 +27,15 @@ public interface WalletGoldBalanceService extends BaseService<WalletGoldBalance>
|
||||
|
||||
Long getBalanceTotal(Long userId);
|
||||
|
||||
/**
|
||||
* 增加收入.
|
||||
*/
|
||||
void incrIncome(Long userId, Long amount);
|
||||
/**
|
||||
* 增加收入.
|
||||
*/
|
||||
boolean incrIncome(Long userId, Long amount);
|
||||
|
||||
/**
|
||||
* 增加支出.
|
||||
*/
|
||||
void incrExpenditure(Long userId, Long amount);
|
||||
/**
|
||||
* 增加支出.
|
||||
*/
|
||||
boolean incrExpenditure(Long userId, Long amount);
|
||||
|
||||
/**
|
||||
* 增加支出.
|
||||
|
||||
@ -31,19 +31,20 @@ public class WalletGoldBalanceServiceImpl extends
|
||||
.orElse(Boolean.FALSE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init(String sysOrigin, Long userId, Long income, Long expenditure) {
|
||||
try {
|
||||
save(new WalletGoldBalance()
|
||||
.setId(userId)
|
||||
.setSysOrigin(sysOrigin)
|
||||
.setIncome(income)
|
||||
.setExpenditure(expenditure)
|
||||
);
|
||||
} catch (DuplicateKeyException ex) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
@Override
|
||||
public boolean init(String sysOrigin, Long userId, Long income, Long expenditure) {
|
||||
try {
|
||||
return save(new WalletGoldBalance()
|
||||
.setId(userId)
|
||||
.setSysOrigin(sysOrigin)
|
||||
.setIncome(income)
|
||||
.setExpenditure(expenditure)
|
||||
);
|
||||
} catch (DuplicateKeyException ex) {
|
||||
// ignore
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public Long getBalance(Long userId) {
|
||||
Long balance = Optional.ofNullable(getById(userId))
|
||||
@ -60,21 +61,21 @@ public class WalletGoldBalanceServiceImpl extends
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void incrIncome(Long userId, Long amount) {
|
||||
update().set(WalletGoldBalance::getUpdateTime, DateUtils.now())
|
||||
.setSql(String.format("income=income+%s", amount))
|
||||
.eq(WalletGoldBalance::getId, userId)
|
||||
.last(PageConstant.LIMIT_ONE)
|
||||
.execute();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void incrExpenditure(Long userId, Long amount) {
|
||||
update().set(WalletGoldBalance::getUpdateTime, DateUtils.now())
|
||||
.setSql(String.format("expenditure=expenditure+%s", amount))
|
||||
.eq(WalletGoldBalance::getId, userId)
|
||||
.last(PageConstant.LIMIT_ONE)
|
||||
@Override
|
||||
public boolean incrIncome(Long userId, Long amount) {
|
||||
return update().set(WalletGoldBalance::getUpdateTime, DateUtils.now())
|
||||
.setSql(String.format("income=income+%s", amount))
|
||||
.eq(WalletGoldBalance::getId, userId)
|
||||
.last(PageConstant.LIMIT_ONE)
|
||||
.execute();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean incrExpenditure(Long userId, Long amount) {
|
||||
return update().set(WalletGoldBalance::getUpdateTime, DateUtils.now())
|
||||
.setSql(String.format("expenditure=expenditure+%s", amount))
|
||||
.eq(WalletGoldBalance::getId, userId)
|
||||
.last(PageConstant.LIMIT_ONE)
|
||||
.execute();
|
||||
}
|
||||
|
||||
|
||||
@ -4,11 +4,11 @@ import com.alibaba.nacos.shaded.com.google.common.base.Throwables;
|
||||
import com.red.circle.component.tencent.cls.service.ClsService;
|
||||
import com.red.circle.framework.web.props.EnvProperties;
|
||||
import com.red.circle.mq.business.model.event.wallet.WalletReceiptSyncEvent;
|
||||
import com.red.circle.tool.core.date.DateUtils;
|
||||
import com.red.circle.tool.core.sequence.IdWorkerUtils;
|
||||
import com.red.circle.tool.core.text.StringUtils;
|
||||
import com.red.circle.other.inner.endpoint.user.user.UserProfileClient;
|
||||
import com.red.circle.other.inner.model.dto.user.UserProfileDTO;
|
||||
import com.red.circle.tool.core.date.DateUtils;
|
||||
import com.red.circle.tool.core.sequence.IdWorkerUtils;
|
||||
import com.red.circle.tool.core.text.StringUtils;
|
||||
import com.red.circle.other.inner.endpoint.user.user.UserProfileClient;
|
||||
import com.red.circle.other.inner.model.dto.user.UserProfileDTO;
|
||||
import com.red.circle.wallet.domain.gateway.WalletExceptionLogGateway;
|
||||
import com.red.circle.wallet.domain.wallet.WalletBizType;
|
||||
import com.red.circle.wallet.domain.wallet.WalletErrorLogReason;
|
||||
@ -145,28 +145,58 @@ public class WalletAssetSyncTool {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 刷新金币同步.
|
||||
*/
|
||||
public void syncGold(WalletReceiptSyncEvent receipt) {
|
||||
try {
|
||||
if (receipt.checkTypeIncome()) {
|
||||
walletGoldBalanceService.incrIncome(receipt.getUserId(),
|
||||
receipt.getAmount().getPennyAmount());
|
||||
}
|
||||
|
||||
if (receipt.checkExpenditure()) {
|
||||
walletGoldBalanceService.incrExpenditure(receipt.getUserId(),
|
||||
receipt.getAmount().getPennyAmount());
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
saveErrorLog(receipt, WalletErrorLogReason.REST_BALANCE);
|
||||
ex.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private WalletRemarks toWalletRemarks(WalletReceiptSyncEvent receipt) {
|
||||
/**
|
||||
* 刷新金币同步.
|
||||
*/
|
||||
public void syncGold(WalletReceiptSyncEvent receipt) {
|
||||
try {
|
||||
if (!syncGoldBalanceChange(receipt)) {
|
||||
initMissingGoldBalance(receipt);
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
saveErrorLog(receipt, WalletErrorLogReason.REST_BALANCE);
|
||||
throw new IllegalStateException("sync gold balance failed", ex);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean syncGoldBalanceChange(WalletReceiptSyncEvent receipt) {
|
||||
if (receipt.checkTypeIncome()) {
|
||||
return walletGoldBalanceService.incrIncome(receipt.getUserId(),
|
||||
receipt.getAmount().getPennyAmount());
|
||||
}
|
||||
|
||||
if (receipt.checkExpenditure()) {
|
||||
return walletGoldBalanceService.incrExpenditure(receipt.getUserId(),
|
||||
receipt.getAmount().getPennyAmount());
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private void initMissingGoldBalance(WalletReceiptSyncEvent receipt) {
|
||||
long amount = receipt.getAmount().getPennyAmount();
|
||||
long balance = receipt.getBalance().getPennyAmount();
|
||||
long income = Math.max(balance, 0L);
|
||||
long expenditure = 0L;
|
||||
|
||||
if (receipt.checkExpenditure()) {
|
||||
income = Math.max(balance + amount, 0L);
|
||||
expenditure = Math.max(amount, 0L);
|
||||
}
|
||||
|
||||
if (walletGoldBalanceService.init(receipt.getSysOrigin(), receipt.getUserId(), income,
|
||||
expenditure)) {
|
||||
log.warn("初始化缺失金币余额记录: userId={}, eventType={}, eventId={}",
|
||||
receipt.getUserId(), receipt.getEventType(), receipt.getEventId());
|
||||
return;
|
||||
}
|
||||
if (!syncGoldBalanceChange(receipt)) {
|
||||
throw new IllegalStateException(
|
||||
"sync gold balance no rows after init: userId=" + receipt.getUserId());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private WalletRemarks toWalletRemarks(WalletReceiptSyncEvent receipt) {
|
||||
WalletRemarks remarks = new WalletRemarks()
|
||||
.setId(IdWorkerUtils.getId())
|
||||
.setBizId(Objects.toString(receipt.getAssetRecordId()))
|
||||
|
||||
@ -39,23 +39,25 @@ public class WalletGoldClientEndpoint implements WalletGoldClientApi {
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResultResponse<WalletReceiptResDTO> changeBalance(GoldReceiptCmd cmd) {
|
||||
public ResultResponse<WalletReceiptResDTO> changeBalance(
|
||||
@RequestBody @Validated GoldReceiptCmd cmd) {
|
||||
return ResultResponse.success(walletGoldClientService.changeBalance(cmd));
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResultResponse<WalletReceiptResDTO> changeBalanceTest(TestWalletReceiptCmd cmd) {
|
||||
public ResultResponse<WalletReceiptResDTO> changeBalanceTest(
|
||||
@RequestBody @Validated TestWalletReceiptCmd cmd) {
|
||||
return ResultResponse.success(walletGoldClientService.changeBalanceTest(cmd));
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResultResponse<Void> refundBack(RefundBackReceiptCmd cmd) {
|
||||
public ResultResponse<Void> refundBack(@RequestBody @Validated RefundBackReceiptCmd cmd) {
|
||||
walletGoldClientService.refundBack(cmd);
|
||||
return ResultResponse.success();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResultResponse<Void> recovery(RecoveryGoldReceiptCmd cmd) {
|
||||
public ResultResponse<Void> recovery(@RequestBody @Validated RecoveryGoldReceiptCmd cmd) {
|
||||
walletGoldClientService.recovery(cmd);
|
||||
return ResultResponse.success();
|
||||
}
|
||||
@ -74,7 +76,7 @@ public class WalletGoldClientEndpoint implements WalletGoldClientApi {
|
||||
|
||||
@Override
|
||||
public ResultResponse<List<UserGoldRunningWaterHistoryDTO>> listWater(
|
||||
UserGoldRunningWaterBackQryCmd cmd) {
|
||||
@RequestBody @Validated UserGoldRunningWaterBackQryCmd cmd) {
|
||||
return ResultResponse.success(walletGoldClientService.listWater(cmd));
|
||||
}
|
||||
|
||||
@ -90,7 +92,7 @@ public class WalletGoldClientEndpoint implements WalletGoldClientApi {
|
||||
|
||||
@Override
|
||||
public ResultResponse<UserGoldRunningWaterClsHistoryDTO> getClsSearchWater(
|
||||
UserGoldRunningWaterBackQryCmd query) {
|
||||
@RequestBody @Validated UserGoldRunningWaterBackQryCmd query) {
|
||||
return ResultResponse.success(walletGoldClientService.getClsSearchWater(query));
|
||||
}
|
||||
|
||||
|
||||
@ -0,0 +1,70 @@
|
||||
package com.red.circle.wallet.infra.tool;
|
||||
|
||||
import static org.mockito.Mockito.inOrder;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import com.red.circle.common.business.core.enums.ReceiptType;
|
||||
import com.red.circle.mq.business.model.event.wallet.WalletReceiptSyncEvent;
|
||||
import com.red.circle.tool.core.tuple.PennyAmount;
|
||||
import com.red.circle.wallet.infra.database.rds.service.WalletGoldBalanceService;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.InOrder;
|
||||
|
||||
class WalletAssetSyncToolTest {
|
||||
|
||||
@Test
|
||||
void syncGoldInitializesMissingBalanceWhenExpenditureUpdateMissesRow() {
|
||||
WalletGoldBalanceService balanceService = mock(WalletGoldBalanceService.class);
|
||||
when(balanceService.incrExpenditure(1001L, 1_500_000L)).thenReturn(false);
|
||||
when(balanceService.init("LIKEI", 1001L, 10_000_000L, 1_500_000L)).thenReturn(true);
|
||||
WalletAssetSyncTool tool = newTool(balanceService);
|
||||
|
||||
WalletReceiptSyncEvent event = new WalletReceiptSyncEvent();
|
||||
event.setReceiptType(ReceiptType.EXPENDITURE);
|
||||
event.setSysOrigin("LIKEI");
|
||||
event.setUserId(1001L);
|
||||
event.setEventType("VOICE_ROOM_RED_PACKET_SEND");
|
||||
event.setEventId("VRPS:123");
|
||||
event.setAmount(PennyAmount.ofPenny(1_500_000L));
|
||||
event.setBalance(PennyAmount.ofPenny(8_500_000L));
|
||||
|
||||
tool.syncGold(event);
|
||||
|
||||
InOrder ordered = inOrder(balanceService);
|
||||
ordered.verify(balanceService).incrExpenditure(1001L, 1_500_000L);
|
||||
ordered.verify(balanceService).init("LIKEI", 1001L, 10_000_000L, 1_500_000L);
|
||||
verify(balanceService, never()).incrIncome(1001L, 1_500_000L);
|
||||
}
|
||||
|
||||
@Test
|
||||
void syncGoldInitializesMissingBalanceWhenIncomeUpdateMissesRow() {
|
||||
WalletGoldBalanceService balanceService = mock(WalletGoldBalanceService.class);
|
||||
when(balanceService.incrIncome(1002L, 1_500_000L)).thenReturn(false);
|
||||
when(balanceService.init("LIKEI", 1002L, 11_500_000L, 0L)).thenReturn(true);
|
||||
WalletAssetSyncTool tool = newTool(balanceService);
|
||||
|
||||
WalletReceiptSyncEvent event = new WalletReceiptSyncEvent();
|
||||
event.setReceiptType(ReceiptType.INCOME);
|
||||
event.setSysOrigin("LIKEI");
|
||||
event.setUserId(1002L);
|
||||
event.setEventType("VOICE_ROOM_RED_PACKET_CLAIM");
|
||||
event.setEventId("VRPC:123");
|
||||
event.setAmount(PennyAmount.ofPenny(1_500_000L));
|
||||
event.setBalance(PennyAmount.ofPenny(11_500_000L));
|
||||
|
||||
tool.syncGold(event);
|
||||
|
||||
InOrder ordered = inOrder(balanceService);
|
||||
ordered.verify(balanceService).incrIncome(1002L, 1_500_000L);
|
||||
ordered.verify(balanceService).init("LIKEI", 1002L, 11_500_000L, 0L);
|
||||
verify(balanceService, never()).incrExpenditure(1002L, 1_500_000L);
|
||||
}
|
||||
|
||||
private WalletAssetSyncTool newTool(WalletGoldBalanceService balanceService) {
|
||||
return new WalletAssetSyncTool(null, null, null, null, null, balanceService, null, null, null,
|
||||
null);
|
||||
}
|
||||
}
|
||||
@ -44,7 +44,11 @@ VALUES
|
||||
('PK', 'PKR', 'Ewallet_Easypaisa', 'Easypaisa', 5.5000, 0.0000, '', 170),
|
||||
('PK', 'PKR', 'Ewallet_JazzCash', 'JazzCash', 5.5000, 0.0000, '', 180),
|
||||
('VN', 'VND', 'QR_VietQR', 'VietQR', 4.5000, 0.0000, '', 190),
|
||||
('VN', 'VND', 'BankTransfer_VN_BankTransfer', 'VN_BankTransfer', 4.5000, 0.0000, '', 200);
|
||||
('VN', 'VND', 'BankTransfer_VN_BankTransfer', 'VN_BankTransfer', 4.5000, 0.0000, '', 200),
|
||||
('PH', 'PHP', 'Ewallet_Gcash', 'Gcash', 0.0000, 0.0000, '', 50),
|
||||
('PH', 'PHP', 'Ewallet_PayMaya', 'PayMaya', 0.0000, 0.0000, '', 100),
|
||||
('PH', 'PHP', 'QR_QRPH', 'QRPH', 0.0000, 0.0000, '', 1),
|
||||
('BD', 'BDT', 'Ewallet_bKash', 'bKash', 0.0000, 0.0000, '', 50);
|
||||
|
||||
-- Hide unsupported MiFaPay country/channel combinations.
|
||||
UPDATE `sys_pay_country_channel_details` d
|
||||
|
||||
140
sql/20260512_mifapay_ph_bd_supported_channels.sql
Normal file
140
sql/20260512_mifapay_ph_bd_supported_channels.sql
Normal file
@ -0,0 +1,140 @@
|
||||
-- Enable newly configured MiFaPay methods for LIKEI.
|
||||
-- Requires the base channel rows from 20260430_mifapay_full_payment_country_channels.sql.
|
||||
|
||||
START TRANSACTION;
|
||||
|
||||
SET @factory_code := 'MIFA_PAY';
|
||||
SET @sys_origin := 'LIKEI';
|
||||
SET @region_relation_group := 'OPEN_PAY_COUNTRY';
|
||||
|
||||
DROP TEMPORARY TABLE IF EXISTS `tmp_mifapay_new_supported_channel`;
|
||||
CREATE TEMPORARY TABLE `tmp_mifapay_new_supported_channel` (
|
||||
`country_code` VARCHAR(2) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NOT NULL,
|
||||
`currency` VARCHAR(3) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
|
||||
`channel_code` VARCHAR(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
|
||||
`factory_channel` VARCHAR(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
|
||||
`suggest_score` INT NOT NULL,
|
||||
PRIMARY KEY (`country_code`, `currency`, `channel_code`, `factory_channel`)
|
||||
) ENGINE=Memory;
|
||||
|
||||
INSERT INTO `tmp_mifapay_new_supported_channel`
|
||||
(`country_code`, `currency`, `channel_code`, `factory_channel`, `suggest_score`)
|
||||
VALUES
|
||||
('PH', 'PHP', 'Ewallet_Gcash', 'Gcash', 95),
|
||||
('PH', 'PHP', 'Ewallet_PayMaya', 'PayMaya', 90),
|
||||
('PH', 'PHP', 'QR_QRPH', 'QRPH', 100),
|
||||
('BD', 'BDT', 'Ewallet_bKash', 'bKash', 95);
|
||||
|
||||
-- Enable the specific MiFaPay country/channel details.
|
||||
UPDATE `sys_pay_country_channel_details` d
|
||||
JOIN `sys_pay_country` pc ON pc.`id` = d.`pay_country_id`
|
||||
JOIN `sys_country_code` cc ON cc.`id` = pc.`country_id`
|
||||
JOIN `tmp_mifapay_new_supported_channel` s
|
||||
ON s.`country_code` = cc.`alpha_two`
|
||||
AND s.`currency` = pc.`currency`
|
||||
AND s.`channel_code` = d.`channel_code`
|
||||
AND s.`factory_channel` = d.`factory_channel`
|
||||
SET d.`is_shelf` = 1,
|
||||
d.`factory_min_limit` = 0.01,
|
||||
d.`factory_max_limit` = 99999999.00,
|
||||
d.`factory_daily_limit` = 99999999.00,
|
||||
d.`suggest_score` = s.`suggest_score`,
|
||||
d.`update_time` = CURRENT_TIMESTAMP
|
||||
WHERE d.`factory_code` = @factory_code;
|
||||
|
||||
-- Enable country/channel rows that now have at least one supported MiFaPay detail.
|
||||
UPDATE `sys_pay_country_channel` cch
|
||||
JOIN `sys_pay_country` pc ON pc.`id` = cch.`pay_country_id`
|
||||
JOIN `sys_country_code` cc ON cc.`id` = pc.`country_id`
|
||||
JOIN `tmp_mifapay_new_supported_channel` s
|
||||
ON s.`country_code` = cc.`alpha_two`
|
||||
AND s.`currency` = pc.`currency`
|
||||
AND s.`channel_code` = cch.`channel_code`
|
||||
SET cch.`is_shelf` = 1,
|
||||
cch.`update_time` = CURRENT_TIMESTAMP
|
||||
WHERE EXISTS (
|
||||
SELECT 1
|
||||
FROM `sys_pay_country_channel_details` d
|
||||
WHERE d.`pay_country_id` = cch.`pay_country_id`
|
||||
AND d.`channel_code` = cch.`channel_code`
|
||||
AND d.`factory_code` = @factory_code
|
||||
AND d.`factory_channel` = s.`factory_channel`
|
||||
AND d.`is_shelf` = 1
|
||||
);
|
||||
|
||||
-- Expose the related pay countries and LIKEI V2 region relations.
|
||||
UPDATE `sys_pay_country` pc
|
||||
JOIN `sys_country_code` cc ON cc.`id` = pc.`country_id`
|
||||
JOIN (SELECT DISTINCT `country_code`, `currency` FROM `tmp_mifapay_new_supported_channel`) s
|
||||
ON s.`country_code` = cc.`alpha_two`
|
||||
AND s.`currency` = pc.`currency`
|
||||
SET pc.`is_shelf` = 1,
|
||||
pc.`update_time` = CURRENT_TIMESTAMP
|
||||
WHERE EXISTS (
|
||||
SELECT 1
|
||||
FROM `sys_pay_country_channel_details` d
|
||||
WHERE d.`pay_country_id` = pc.`id`
|
||||
AND d.`factory_code` = @factory_code
|
||||
AND d.`is_shelf` = 1
|
||||
);
|
||||
|
||||
UPDATE `sys_region_relation` rr
|
||||
JOIN `sys_pay_country` pc ON pc.`id` = rr.`relation_id`
|
||||
JOIN `sys_country_code` cc ON cc.`id` = pc.`country_id`
|
||||
JOIN (SELECT DISTINCT `country_code`, `currency` FROM `tmp_mifapay_new_supported_channel`) s
|
||||
ON s.`country_code` = cc.`alpha_two`
|
||||
AND s.`currency` = pc.`currency`
|
||||
SET rr.`is_showcase` = 1,
|
||||
rr.`update_time` = CURRENT_TIMESTAMP
|
||||
WHERE rr.`sys_origin` = @sys_origin
|
||||
AND rr.`group_type` = @region_relation_group
|
||||
AND pc.`is_shelf` = 1;
|
||||
|
||||
COMMIT;
|
||||
|
||||
-- Verification queries. Expected: 4, 0, 2, 2.
|
||||
SELECT 'enabled_new_mifapay_details' AS `metric`, COUNT(*) AS `value`
|
||||
FROM `sys_pay_country_channel_details` d
|
||||
JOIN `sys_pay_country` pc ON pc.`id` = d.`pay_country_id`
|
||||
JOIN `sys_country_code` cc ON cc.`id` = pc.`country_id`
|
||||
JOIN `tmp_mifapay_new_supported_channel` s
|
||||
ON s.`country_code` = cc.`alpha_two`
|
||||
AND s.`currency` = pc.`currency`
|
||||
AND s.`channel_code` = d.`channel_code`
|
||||
AND s.`factory_channel` = d.`factory_channel`
|
||||
WHERE d.`factory_code` = @factory_code
|
||||
AND d.`is_shelf` = 1;
|
||||
|
||||
SELECT 'missing_new_mifapay_details' AS `metric`, COUNT(*) AS `value`
|
||||
FROM `tmp_mifapay_new_supported_channel` s
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM `sys_pay_country_channel_details` d
|
||||
JOIN `sys_pay_country` pc ON pc.`id` = d.`pay_country_id`
|
||||
JOIN `sys_country_code` cc ON cc.`id` = pc.`country_id`
|
||||
WHERE cc.`alpha_two` = s.`country_code`
|
||||
AND pc.`currency` = s.`currency`
|
||||
AND d.`channel_code` = s.`channel_code`
|
||||
AND d.`factory_code` = @factory_code
|
||||
AND d.`factory_channel` = s.`factory_channel`
|
||||
AND d.`is_shelf` = 1
|
||||
);
|
||||
|
||||
SELECT 'enabled_new_mifapay_countries' AS `metric`, COUNT(DISTINCT pc.`id`) AS `value`
|
||||
FROM `sys_pay_country` pc
|
||||
JOIN `sys_country_code` cc ON cc.`id` = pc.`country_id`
|
||||
JOIN (SELECT DISTINCT `country_code`, `currency` FROM `tmp_mifapay_new_supported_channel`) s
|
||||
ON s.`country_code` = cc.`alpha_two`
|
||||
AND s.`currency` = pc.`currency`
|
||||
WHERE pc.`is_shelf` = 1;
|
||||
|
||||
SELECT 'enabled_new_mifapay_region_relations' AS `metric`, COUNT(DISTINCT rr.`id`) AS `value`
|
||||
FROM `sys_region_relation` rr
|
||||
JOIN `sys_pay_country` pc ON pc.`id` = rr.`relation_id`
|
||||
JOIN `sys_country_code` cc ON cc.`id` = pc.`country_id`
|
||||
JOIN (SELECT DISTINCT `country_code`, `currency` FROM `tmp_mifapay_new_supported_channel`) s
|
||||
ON s.`country_code` = cc.`alpha_two`
|
||||
AND s.`currency` = pc.`currency`
|
||||
WHERE rr.`sys_origin` = @sys_origin
|
||||
AND rr.`group_type` = @region_relation_group
|
||||
AND rr.`is_showcase` = 1;
|
||||
16
sql/20260512_team_yumi_manager_identity.sql
Normal file
16
sql/20260512_team_yumi_manager_identity.sql
Normal file
@ -0,0 +1,16 @@
|
||||
SET @table_schema = DATABASE();
|
||||
|
||||
SET @ddl = IF(
|
||||
EXISTS(
|
||||
SELECT 1
|
||||
FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA = @table_schema
|
||||
AND TABLE_NAME = 'user_history_identity'
|
||||
AND COLUMN_NAME = 'is_yumi_manager'
|
||||
),
|
||||
'SELECT 1',
|
||||
"ALTER TABLE `user_history_identity` ADD COLUMN `is_yumi_manager` tinyint(1) NOT NULL DEFAULT 0 COMMENT '是否做过Yumi团队经理' AFTER `is_manager`"
|
||||
);
|
||||
PREPARE stmt FROM @ddl;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
52
sql/20260513_country_dashboard_game_period_metric.sql
Normal file
52
sql/20260513_country_dashboard_game_period_metric.sql
Normal file
@ -0,0 +1,52 @@
|
||||
SET NAMES utf8mb4;
|
||||
|
||||
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='国家数据大屏游戏维度周期用户去重明细';
|
||||
Loading…
x
Reference in New Issue
Block a user