Compare commits

..

73 Commits

Author SHA1 Message Date
hy001
0a29464a82 修复一下问题 2026-07-10 12:09:27 +08:00
hy001
7ead4fcc85 Merge branch 'main' of https://gitea.haiyihy.com/hy/chatapp3-java 2026-07-01 14:52:48 +08:00
hy001
c73d224e19 筛选 2026-07-01 14:52:47 +08:00
local
d7c3fbf721 fix account unblock login cache 2026-06-30 23:16:10 +08:00
hy001
97738f5b5e 充值 2026-06-25 23:37:58 +08:00
hy001
9de0155d7e 商品 2026-06-25 22:52:50 +08:00
hy001
18b49c9db4 三方支付 2026-06-25 22:37:00 +08:00
hy001
986f7507e7 切换支付国家汇率同步数据源 2026-06-25 17:21:30 +08:00
hy001
9957dc2a27 修复汇率同步非法国家码 2026-06-25 17:10:34 +08:00
hy001
c39dac772a order 和 console 2026-06-25 16:13:09 +08:00
hy001
1826aa10ab 经理中心 2026-06-24 15:39:54 +08:00
hy001
21bab83eea 相关支付 2026-06-24 12:57:33 +08:00
hy001
720c7cec50 Block HK MO TW SG login regions 2026-06-22 13:36:29 +08:00
hy001
de95193bda 经理中心 2026-06-12 19:36:53 +08:00
hy001
2736b25fbe 1 2026-06-10 19:00:45 +08:00
hy001
c56c9db06f 修复显示 2026-06-09 16:14:23 +08:00
hy001
c7016d5ae4 增加订单导出 2026-06-09 15:43:05 +08:00
hy001
c8e9c8c257 增加导出 2026-06-04 12:11:55 +08:00
hy001
7f7649d6bd Merge branch 'test'
# Conflicts:
#	rc-service/rc-service-other/other-application/src/main/java/com/red/circle/other/app/scheduler/TeamBillTask.java
#	rc-service/rc-service-other/other-inner-endpoint/src/main/java/com/red/circle/other/app/inner/service/team/impl/TeamManualSalaryPaymentClientServiceImpl.java
#	rc-service/rc-service-other/other-inner-endpoint/src/test/java/com/red/circle/other/app/inner/service/team/impl/TeamManualSalaryPaymentClientServiceImplTest.java
2026-06-01 19:58:08 +08:00
hy001
85f3ffbc10 自动结算 2026-06-01 19:46:48 +08:00
hy001
3c920ee208 fix: keep bill cycle switch while salary paused 2026-06-01 10:44:13 +08:00
hy001
e3be12cd8b 支持手动工资按月度账期发放 2026-05-29 20:56:57 +08:00
hy001
973f76a438 增加首冲 2026-05-28 15:07:52 +08:00
hy001
7541ba77a6 feat: expose dashboard recharge user metrics 2026-05-27 21:33:56 +08:00
hy001
8feecc423c Add country dashboard ARPU summary 2026-05-27 17:01:20 +08:00
hy001
0464935619 增加支付版本判断 2026-05-25 23:22:51 +08:00
hy001
d799d68946 修复工资发放 2026-05-25 20:34:00 +08:00
hy001
c2bd1554f9 Merge branch 'test' 2026-05-25 20:07:17 +08:00
hy001
6573219751 修复送礼报错 2026-05-25 17:07:13 +08:00
hy001
c2e28b5cb3 增加校验 2026-05-25 16:22:18 +08:00
hy001
0260498bd3 CP关系申请 问题修复 2026-05-25 14:32:15 +08:00
hy001
f837d75d42 Fix manual salary top-up settlement 2026-05-25 14:23:17 +08:00
hy001
7e70b3809c Expose salary transfer in country dashboard 2026-05-25 13:23:34 +08:00
hy001
e5eeebf84a cp功能修复 2026-05-25 11:16:53 +08:00
hy001
d53728e874 fix: refresh dashboard active retention metrics 2026-05-25 11:16:45 +08:00
hy001
1520362d9f Merge branch 'main' of gitea.haiyihy.com:hy/chatapp3-java 2026-05-25 10:40:04 +08:00
hy001
6405052215 vip时间设置 2026-05-25 10:40:03 +08:00
hy001
74fd42330e fix: complete country dashboard derived metrics 2026-05-24 14:08:31 +08:00
hy001
4112160656 Merge branch 'main' of gitea.haiyihy.com:hy/chatapp3-java 2026-05-23 21:26:46 +08:00
hy001
98d80c6732 修复竟态 2026-05-23 21:26:35 +08:00
hy001
b98d91a2fb 修复时间问题 2026-05-23 21:14:47 +08:00
hy001
b9361ac3ff 增加新的接口 2026-05-23 21:05:03 +08:00
hy001
aab443ce34 修复重复发消息 2026-05-23 20:09:54 +08:00
hy001
32543b958c cp 2026-05-23 19:07:47 +08:00
hy001
5359be337d 修复一些问题 2026-05-23 14:54:40 +08:00
hy001
fa6b9be148 修复边界 2026-05-23 14:02:23 +08:00
hy001
7299b1c088 fix: precompute country dashboard active metrics 2026-05-23 13:27:45 +08:00
hy001
3588d0aeee Merge branch 'main' into test 2026-05-23 13:18:30 +08:00
hy001
0bf810261f 修复首冲 2026-05-23 13:17:06 +08:00
hy001
35458a674e fix: make country dashboard backfill atomic 2026-05-23 12:27:44 +08:00
hy001
dfb71f55a8 Merge branch 'main' into test 2026-05-23 12:20:10 +08:00
hy001
c99542ebe9 修改CP相关 2026-05-23 12:19:57 +08:00
hy001
3168637944 已有关系拒绝其他关系 2026-05-22 21:23:19 +08:00
hy001
dca9f12cb5 新增发IM C2c 2026-05-22 20:11:20 +08:00
hy001
a0461ce68c CP关系解绑需要金币 2026-05-22 19:17:41 +08:00
hy001
40a8f90e01 默认topic 2026-05-22 18:12:46 +08:00
hy001
f540fc91bc use precomputed country dashboard metrics 2026-05-22 18:10:32 +08:00
hy001
505bdf6345 cp rocketmq 2026-05-22 15:59:09 +08:00
hy001
8f573739a2 cp系统完善 2026-05-22 15:57:48 +08:00
hy001
9e37b347a3 cp积分 2026-05-22 13:27:22 +08:00
hy001
e897d635ea 首冲奖励 2026-05-22 10:10:15 +08:00
hy001
8e37a3d333 CP关系冷静期解除 2026-05-21 16:19:53 +08:00
hy001
3f59e5b653 CP 2026-05-21 15:43:38 +08:00
hy001
c02050274b fix manual salary received total display 2026-05-21 15:06:03 +08:00
hy001
8460125d79 expose manual salary role breakdown 2026-05-21 14:05:38 +08:00
hy001
d0d48d71ac include agent salary in manual settlement 2026-05-21 13:59:36 +08:00
hy001
3cef4fc322 fix manual salary settlement net payable 2026-05-21 13:46:33 +08:00
hy001
a48fddb613 Merge branch 'release/redis-memory-fix-20260521' of gitea.haiyihy.com:hy/chatapp3-java 2026-05-21 11:19:21 +08:00
hy001
7bc8b44450 Merge branch 'main' of gitea.haiyihy.com:hy/chatapp3-java 2026-05-21 11:19:05 +08:00
hy001
a8424ad5e3 分国家发放工资 2026-05-21 11:19:04 +08:00
ZuoZuo
baaf171151 feat(manager): support badge gifts 2026-05-20 19:59:34 +08:00
hy001
a0cc63d784 fix: tolerate duplicate region configs 2026-05-20 14:11:54 +08:00
hy001
663affb734 Reopen salary exchange and user coin seller transfer 2026-05-20 12:32:45 +08:00
214 changed files with 11462 additions and 1752 deletions

22
.gitignore vendored
View File

@ -41,15 +41,15 @@ buildNumber.properties
.mtj.tmp/ .mtj.tmp/
# Package Files # # Package Files #
*.jar *.jar
*.war *.war
*.nar *.nar
*.ear *.ear
*.zip *.zip
*.tar.gz *.tar.gz
*.rar *.rar
*.iml *.iml
!maven_private/**/*.jar !maven_private/**/*.jar
# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
hs_err_pid* hs_err_pid*
@ -96,4 +96,6 @@ target/
devops-monitor-id_rsa.key devops-monitor-id_rsa.key
/claude/ /claude/
.local-run/ .local-run/
.env

View File

@ -175,6 +175,7 @@ gateway:
- /product/apple/config - /product/apple/config
- /game/sud/** - /game/sud/**
- /web/pay/** - /web/pay/**
- /order/web/pay/**
- /external/oss/upload - /external/oss/upload
- /telegram/webhook - /telegram/webhook
- /account/create/getRegion - /account/create/getRegion
@ -197,6 +198,12 @@ gateway:
- /go/resident-activity/specified-gift-weekly-rank/** - /go/resident-activity/specified-gift-weekly-rank/**
- /resident-activity/specified-gift-weekly-rank/** - /resident-activity/specified-gift-weekly-rank/**
- /go/resident-activity/week-star/** - /go/resident-activity/week-star/**
- /getUserInfo
- /go/getUserInfo
- /updateBalance
- /go/updateBalance
- /game/hotgame/**
- /go/game/hotgame/**
- /resident-activity/week-star/** - /resident-activity/week-star/**
- /resident-activity/sign-in-reward/** - /resident-activity/sign-in-reward/**
- /resident-activity/vip/** - /resident-activity/vip/**
@ -206,6 +213,7 @@ gateway:
- /resident-activity/voice-room-rocket/** - /resident-activity/voice-room-rocket/**
- /resident-activity/wheel/** - /resident-activity/wheel/**
- /resident-activity/smash-golden-egg/** - /resident-activity/smash-golden-egg/**
- /operate/hotgame-game/**
- /operate/baishun-game/** - /operate/baishun-game/**
- /operate/lingxian-game/** - /operate/lingxian-game/**
- /app-system/home-popups/** - /app-system/home-popups/**

View File

@ -128,6 +128,13 @@ task:
topic: ${TASK_CENTER_EVENT_MQ_TOPIC:RC_DEFAULT_APP_ORDINARY} topic: ${TASK_CENTER_EVENT_MQ_TOPIC:RC_DEFAULT_APP_ORDINARY}
tag: ${TASK_CENTER_EVENT_MQ_TAG:task_center_event} tag: ${TASK_CENTER_EVENT_MQ_TAG:task_center_event}
cp:
relation-broadcast:
mq:
enabled: ${CP_RELATION_BROADCAST_MQ_ENABLED:true}
topic: ${CP_RELATION_BROADCAST_MQ_TOPIC:RC_DEFAULT_APP_ORDINARY}
tag: ${CP_RELATION_BROADCAST_MQ_TAG:cp_relation_broadcast}
voice-room: voice-room:
region-broadcast: region-broadcast:
go: go:

View File

@ -39,10 +39,11 @@ public class LoginAccessValidationService {
"香港", "澳门", "澳門", "台湾", "台灣", "Hong Kong", "Macau", "Macao", "Taiwan" "香港", "澳门", "澳門", "台湾", "台灣", "Hong Kong", "Macau", "Macao", "Taiwan"
); );
private static final List<String> BLOCKED_COUNTRY_CODES = List.of( private static final List<String> BLOCKED_COUNTRY_CODES = List.of(
"MO" "HK", "MO", "TW", "SG"
); );
private static final List<String> BLOCKED_COUNTRY_NAMES = List.of( private static final List<String> BLOCKED_COUNTRY_NAMES = List.of(
"澳门", "澳門", "Macau", "Macao" "香港", "澳门", "澳門", "台湾", "台灣", "新加坡",
"Hong Kong", "Macau", "Macao", "Taiwan", "Singapore"
); );
private static final List<IpGeoProvider> IP_GEO_PROVIDERS = List.of( private static final List<IpGeoProvider> IP_GEO_PROVIDERS = List.of(
new IpGeoProvider("ip.sb", "https://api.ip.sb/geoip/%s"), new IpGeoProvider("ip.sb", "https://api.ip.sb/geoip/%s"),

View File

@ -70,22 +70,22 @@ public class LoginAccessValidationServiceTest {
} }
@Test @Test
public void allowHongKongRegion() { public void blockHongKongRegion() {
JSONObject data = JSON.parseObject(""" JSONObject data = JSON.parseObject("""
{"country_id":"CN","country":"中国","region":"香港"} {"country_id":"CN","country":"中国","region":"香港"}
"""); """);
assertFalse(LoginAccessValidationService.isMainlandChina(data)); assertFalse(LoginAccessValidationService.isMainlandChina(data));
assertFalse(LoginAccessValidationService.isBlockedIpRegion(data)); assertTrue(LoginAccessValidationService.isBlockedIpRegion(data));
} }
@Test @Test
public void allowHongKongCountryCode() { public void blockHongKongCountryCode() {
JSONObject data = JSON.parseObject(""" JSONObject data = JSON.parseObject("""
{"country_id":"HK","country":"Hong Kong","region":"Kwai Tsing District"} {"country_id":"HK","country":"Hong Kong","region":"Kwai Tsing District"}
"""); """);
assertFalse(LoginAccessValidationService.isBlockedIpRegion(data)); assertTrue(LoginAccessValidationService.isBlockedIpRegion(data));
} }
@Test @Test
@ -98,30 +98,30 @@ public class LoginAccessValidationServiceTest {
} }
@Test @Test
public void allowSingaporeCountryCode() { public void blockSingaporeCountryCode() {
JSONObject data = JSON.parseObject(""" JSONObject data = JSON.parseObject("""
{"country_id":"SG","country":"Singapore","region":""} {"country_id":"SG","country":"Singapore","region":""}
"""); """);
assertFalse(LoginAccessValidationService.isBlockedIpRegion(data)); assertTrue(LoginAccessValidationService.isBlockedIpRegion(data));
} }
@Test @Test
public void allowTaiwanRegion() { public void blockTaiwanRegion() {
JSONObject data = JSON.parseObject(""" JSONObject data = JSON.parseObject("""
{"country_id":"CN","country":"中国","region":"台湾"} {"country_id":"CN","country":"中国","region":"台湾"}
"""); """);
assertFalse(LoginAccessValidationService.isMainlandChina(data)); assertFalse(LoginAccessValidationService.isMainlandChina(data));
assertFalse(LoginAccessValidationService.isBlockedIpRegion(data)); assertTrue(LoginAccessValidationService.isBlockedIpRegion(data));
} }
@Test @Test
public void allowTaiwanCountryCode() { public void blockTaiwanCountryCode() {
JSONObject data = JSON.parseObject(""" JSONObject data = JSON.parseObject("""
{"country_id":"TW","country":"Taiwan","region":""} {"country_id":"TW","country":"Taiwan","region":""}
"""); """);
assertFalse(LoginAccessValidationService.isBlockedIpRegion(data)); assertTrue(LoginAccessValidationService.isBlockedIpRegion(data));
} }
} }

View File

@ -39,7 +39,7 @@ public enum ConsoleAccountStatusEnum {
/** /**
* 账号解封. * 账号解封.
*/ */
UNTIE_ACOOUNT("UNTIE_DEVICE", "账号解封"), UNTIE_ACOOUNT("UNTIE_ACOOUNT", "账号解封"),
/** /**
* 设备+账户解封. * 设备+账户解封.

View File

@ -46,9 +46,14 @@ public class GiveGiftConfig implements Serializable {
*/ */
private String type; private String type;
/** /**
* 礼物tab. * 礼物tab.
*/ */
private String giftTab; private String giftTab;
} /**
* CP礼物关系类型CP/BROTHER/SISTERS.
*/
private String cpRelationType;
}

View File

@ -56,9 +56,13 @@ public class BuySuccessEvent implements Serializable {
*/ */
String payPlatform; String payPlatform;
/** /**
* 来源. * 本次充值请求的App版本.
*/ */
String appVersion;
/**
* 来源.
*/
String origin; String origin;
/** /**

View File

@ -0,0 +1,39 @@
package com.red.circle.mq.business.model.event.pay;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
import java.io.Serializable;
import java.util.Map;
import lombok.Data;
import lombok.experimental.Accessors;
/**
* 通用充值成功事件
*/
@Data
@Accessors(chain = true)
public class RechargeSuccessEvent implements Serializable {
private static final long serialVersionUID = 1L;
private String eventId;
private String sysOrigin;
@JsonSerialize(using = ToStringSerializer.class)
private Long userId;
@JsonSerialize(using = ToStringSerializer.class)
private Long amountCents;
private String amountUsd;
private String currency;
private String payPlatform;
private String paymentMethod;
private String googleProductId;
private String productId;
private String appVersion;
private String sourceOrderId;
private String orderId;
private String occurredAt;
private Map<String, Object> payload;
}

View File

@ -17,13 +17,18 @@ public interface QueueNameConstant {
*/ */
String LOGIN_REGISTER = "LOGIN_REGISTER"; String LOGIN_REGISTER = "LOGIN_REGISTER";
/** /**
* 购买成功 * 购买成功
*/ */
String BUY_SUCCESS = "BUY_SUCCESS"; String BUY_SUCCESS = "BUY_SUCCESS";
/** /**
* 视频语音房间会话 * 通用充值成功
*/
String RECHARGE_SUCCESS = "RECHARGE_SUCCESS";
/**
* 视频语音房间会话
*/ */
String LIVE_VOICE_SESSION = "LIVE_VOICE_SESSION"; String LIVE_VOICE_SESSION = "LIVE_VOICE_SESSION";

View File

@ -0,0 +1,12 @@
package com.red.circle.mq.rocket.business.streams;
import com.red.circle.mq.rocket.business.constant.QueueNameConstant;
/**
* 通用充值成功事件
*/
public interface RechargeSuccessSink {
String INPUT = QueueNameConstant.RECHARGE_SUCCESS;
String TAG = "recharge_success_v1";
}

View File

@ -1,8 +1,12 @@
package com.red.circle.external.inner.endpoint.message.api; package com.red.circle.external.inner.endpoint.message.api;
import com.red.circle.framework.dto.ResultResponse; import com.red.circle.framework.dto.ResultResponse;
import org.springframework.web.bind.annotation.GetMapping; import com.red.circle.external.inner.model.cmd.message.CustomC2cMsgBodyCmd;
import org.springframework.web.bind.annotation.RequestParam; import jakarta.validation.Valid;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestParam;
/** /**
* im 消息服务. * im 消息服务.
@ -24,9 +28,15 @@ public interface ImMessageClientApi {
/** /**
* 发送文本消息. * 发送文本消息.
*/ */
@GetMapping("/sendMessageText/long") @GetMapping("/sendMessageText/long")
ResultResponse<Void> sendMessageText(@RequestParam("fromAccount") Long fromAccount, ResultResponse<Void> sendMessageText(@RequestParam("fromAccount") Long fromAccount,
@RequestParam("toAccount") Long toAccount, @RequestParam("toAccount") Long toAccount,
@RequestParam("text") String text); @RequestParam("text") String text);
} /**
* 发送 C2C 自定义消息.
*/
@PostMapping("/sendCustomMessage")
ResultResponse<Void> sendCustomMessage(@RequestBody @Valid CustomC2cMsgBodyCmd cmd);
}

View File

@ -0,0 +1,91 @@
package com.red.circle.external.inner.model.cmd.message;
import com.red.circle.framework.dto.Command;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import java.io.Serial;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* C2C custom IM message.
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class CustomC2cMsgBodyCmd extends Command {
@Serial
private static final long serialVersionUID = 1L;
@NotBlank(message = "fromAccount required.")
private String fromAccount;
@NotBlank(message = "toAccount required.")
private String toAccount;
@NotBlank(message = "desc required.")
private String desc;
@NotNull(message = "data required.")
private Object data;
private Integer syncOtherMachine;
public static CustomC2cMsgBodyBuilder builder() {
return new CustomC2cMsgBodyBuilder();
}
public static class CustomC2cMsgBodyBuilder {
private String fromAccount;
private String toAccount;
private String desc;
private Object data;
private Integer syncOtherMachine;
public CustomC2cMsgBodyBuilder fromAccount(String fromAccount) {
this.fromAccount = fromAccount;
return this;
}
public CustomC2cMsgBodyBuilder fromAccount(Long fromAccount) {
this.fromAccount = String.valueOf(fromAccount);
return this;
}
public CustomC2cMsgBodyBuilder toAccount(String toAccount) {
this.toAccount = toAccount;
return this;
}
public CustomC2cMsgBodyBuilder toAccount(Long toAccount) {
this.toAccount = String.valueOf(toAccount);
return this;
}
public CustomC2cMsgBodyBuilder desc(String desc) {
this.desc = desc;
return this;
}
public CustomC2cMsgBodyBuilder data(Object data) {
this.data = data;
return this;
}
public CustomC2cMsgBodyBuilder syncOtherMachine(Integer syncOtherMachine) {
this.syncOtherMachine = syncOtherMachine;
return this;
}
public CustomC2cMsgBodyCmd build() {
CustomC2cMsgBodyCmd cmd = new CustomC2cMsgBodyCmd();
cmd.setFromAccount(this.fromAccount);
cmd.setToAccount(this.toAccount);
cmd.setDesc(this.desc);
cmd.setData(this.data);
cmd.setSyncOtherMachine(this.syncOtherMachine);
return cmd;
}
}
}

View File

@ -3,11 +3,13 @@ package com.red.circle.order.inner.endpoint.api;
import com.red.circle.framework.dto.PageResult; import com.red.circle.framework.dto.PageResult;
import com.red.circle.framework.dto.ResultResponse; import com.red.circle.framework.dto.ResultResponse;
import com.red.circle.order.inner.model.cmd.SysPayCountryAddCmd; import com.red.circle.order.inner.model.cmd.SysPayCountryAddCmd;
import com.red.circle.order.inner.model.cmd.SysPayCountryChannelCmd; import com.red.circle.order.inner.model.cmd.SysPayCountryChannelCmd;
import com.red.circle.order.inner.model.cmd.SysPayCountryChannelDetailsCmd; import com.red.circle.order.inner.model.cmd.SysPayCountryChannelDetailsCmd;
import com.red.circle.order.inner.model.cmd.SysPayCountryQryCmd; import com.red.circle.order.inner.model.cmd.SysPayCountryCurrencyRateSyncCmd;
import com.red.circle.order.inner.model.dto.PayCountryDTO; import com.red.circle.order.inner.model.cmd.SysPayCountryQryCmd;
import com.red.circle.order.inner.model.dto.PayCountryCurrencyRateSyncDTO;
import com.red.circle.order.inner.model.dto.PayCountryDTO;
import com.red.circle.order.inner.model.dto.SysPayCountryChannelDTO; import com.red.circle.order.inner.model.dto.SysPayCountryChannelDTO;
import com.red.circle.order.inner.model.dto.SysPayCountryChannelDetailsDTO; import com.red.circle.order.inner.model.dto.SysPayCountryChannelDetailsDTO;
import java.math.BigDecimal; import java.math.BigDecimal;
@ -85,10 +87,14 @@ public interface PayCountryClientApi {
@GetMapping("/updateSort") @GetMapping("/updateSort")
ResultResponse<Void> updateSort(@RequestParam("id") Long id, @RequestParam("sort") Integer sort); ResultResponse<Void> updateSort(@RequestParam("id") Long id, @RequestParam("sort") Integer sort);
@GetMapping("/shelf") @GetMapping("/shelf")
ResultResponse<Void> shelf(@RequestParam("id") Long id, @RequestParam("shelf") Boolean shelf); ResultResponse<Void> shelf(@RequestParam("id") Long id, @RequestParam("shelf") Boolean shelf);
@GetMapping("/listPayCountryByRegionId") @PostMapping("/syncCurrencyRates")
ResultResponse<List<PayCountryDTO>> listPayCountryByRegionId( ResultResponse<PayCountryCurrencyRateSyncDTO> syncCurrencyRates(
@RequestParam("regionId") String regionId); @RequestBody SysPayCountryCurrencyRateSyncCmd cmd);
}
@GetMapping("/listPayCountryByRegionId")
ResultResponse<List<PayCountryDTO>> listPayCountryByRegionId(
@RequestParam("regionId") String regionId);
}

View File

@ -3,13 +3,14 @@ package com.red.circle.order.inner.model.cmd;
import com.fasterxml.jackson.databind.annotation.JsonSerialize; import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer; import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
import com.red.circle.framework.core.dto.CommonCommand; import com.red.circle.framework.core.dto.CommonCommand;
import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.NotNull;
import java.io.Serial; import java.io.Serial;
import java.math.BigDecimal; import java.math.BigDecimal;
import lombok.Data; import java.util.List;
import lombok.EqualsAndHashCode; import lombok.Data;
import lombok.experimental.Accessors; import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
/** /**
* <p> * <p>
@ -43,11 +44,17 @@ public class SysPayApplicationCommodityCmd extends CommonCommand {
/** /**
* 开通支付国家. * 开通支付国家.
*/ */
@JsonSerialize(using = ToStringSerializer.class) @JsonSerialize(using = ToStringSerializer.class)
private Long payCountryId; private Long payCountryId;
/** /**
* 区域ID. * 批量创建时的国家列表表结构仍是一条商品对应一个国家所以只在新增时拆成多条记录.
*/
@JsonSerialize(contentUsing = ToStringSerializer.class)
private List<Long> payCountryIds;
/**
* 区域ID.
*/ */
private String regionId; private String regionId;

View File

@ -0,0 +1,33 @@
package com.red.circle.order.inner.model.cmd;
import com.red.circle.framework.core.dto.CommonCommand;
import jakarta.validation.constraints.DecimalMax;
import jakarta.validation.constraints.DecimalMin;
import jakarta.validation.constraints.Digits;
import jakarta.validation.constraints.NotNull;
import java.math.BigDecimal;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
/**
* 支付国家货币汇率同步命令.
*
* @author system
*/
@Data
@EqualsAndHashCode(callSuper = true)
@Accessors(chain = true)
public class SysPayCountryCurrencyRateSyncCmd extends CommonCommand {
private static final long serialVersionUID = 1L;
/**
* 上浮比例5 表示在实时汇率基础上增加 5%.
*/
@NotNull
@DecimalMin("0")
@DecimalMax("100")
@Digits(integer = 3, fraction = 4)
private BigDecimal markupPercent;
}

View File

@ -98,8 +98,13 @@ public class InAppPurchaseDetailsQryCmd extends Command {
*/ */
private String lastId; private String lastId;
/** /**
* 条数. * 条数.
*/ */
private Integer limit; private Integer limit;
}
/**
* 是否导出全部筛选结果导出时不使用 lastId limit 分页.
*/
private Boolean exportAll;
}

View File

@ -0,0 +1,47 @@
package com.red.circle.order.inner.model.dto;
import com.red.circle.framework.dto.DTO;
import java.io.Serial;
import java.util.List;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
/**
* 支付国家货币汇率同步结果.
*
* @author system
*/
@Data
@EqualsAndHashCode(callSuper = true)
@Accessors(chain = true)
public class PayCountryCurrencyRateSyncDTO extends DTO {
@Serial
private static final long serialVersionUID = 1L;
/**
* 参与同步的支付国家数量.
*/
private Integer totalCount;
/**
* 已更新的支付国家数量.
*/
private Integer updatedCount;
/**
* 跳过的支付国家数量.
*/
private Integer skippedCount;
/**
* 汇率来源.
*/
private String sourceName;
/**
* 被跳过的国家说明.
*/
private List<String> skippedCountries;
}

View File

@ -9,16 +9,20 @@ import com.red.circle.other.inner.model.cmd.team.TeamApplicationProcessApprovalC
import com.red.circle.other.inner.model.cmd.team.TeamApplicationProcessApprovalQryCmd; import com.red.circle.other.inner.model.cmd.team.TeamApplicationProcessApprovalQryCmd;
import com.red.circle.other.inner.model.cmd.team.TeamApplicationProcessChangeCmd; import com.red.circle.other.inner.model.cmd.team.TeamApplicationProcessChangeCmd;
import com.red.circle.other.inner.model.cmd.team.TeamApplicationProcessQryCmd; import com.red.circle.other.inner.model.cmd.team.TeamApplicationProcessQryCmd;
import com.red.circle.other.inner.model.cmd.team.TeamChangeOwnerCmd; import com.red.circle.other.inner.model.cmd.team.TeamChangeOwnerCmd;
import com.red.circle.other.inner.model.cmd.team.TeamCreateCmd; import com.red.circle.other.inner.model.cmd.team.TeamCreateCmd;
import com.red.circle.other.inner.model.cmd.team.TeamSalaryBackQryCmd; import com.red.circle.other.inner.model.cmd.team.TeamManualSalaryPaymentCmd;
import com.red.circle.other.inner.model.cmd.team.TeamTableQryCmd; import com.red.circle.other.inner.model.cmd.team.TeamManualSalaryPaymentQryCmd;
import com.red.circle.other.inner.model.cmd.team.TeamUpdateCmd; import com.red.circle.other.inner.model.cmd.team.TeamSalaryBackQryCmd;
import com.red.circle.other.inner.model.dto.agency.agency.TeamApplicationProcessApprovalDTO; import com.red.circle.other.inner.model.cmd.team.TeamTableQryCmd;
import com.red.circle.other.inner.model.dto.agency.agency.TeamApplicationProcessDTO; import com.red.circle.other.inner.model.cmd.team.TeamUpdateCmd;
import com.red.circle.other.inner.model.dto.agency.agency.TeamMemberDTO; import com.red.circle.other.inner.model.dto.agency.agency.TeamApplicationProcessApprovalDTO;
import com.red.circle.other.inner.model.dto.agency.agency.TeamProfileDTO; import com.red.circle.other.inner.model.dto.agency.agency.TeamApplicationProcessDTO;
import com.red.circle.other.inner.model.dto.agency.agency.TeamSalaryPaymentDTO; import com.red.circle.other.inner.model.dto.agency.agency.TeamManualSalaryPaymentDTO;
import com.red.circle.other.inner.model.dto.agency.agency.TeamManualSalaryPaymentResultDTO;
import com.red.circle.other.inner.model.dto.agency.agency.TeamMemberDTO;
import com.red.circle.other.inner.model.dto.agency.agency.TeamProfileDTO;
import com.red.circle.other.inner.model.dto.agency.agency.TeamSalaryPaymentDTO;
import java.util.List; import java.util.List;
import java.util.Set; import java.util.Set;
@ -146,9 +150,23 @@ public interface TeamProfileClientApi {
/** /**
* 后台列表查询. * 后台列表查询.
*/ */
@PostMapping("/listByCondition") @PostMapping("/listByCondition")
ResultResponse<PageResult<TeamSalaryPaymentDTO>> listByCondition( ResultResponse<PageResult<TeamSalaryPaymentDTO>> listByCondition(
@RequestBody TeamSalaryBackQryCmd cmd); @RequestBody TeamSalaryBackQryCmd cmd);
/**
* 后台手动发放主播工资预览.
*/
@PostMapping("/manualSalary/page")
ResultResponse<PageResult<TeamManualSalaryPaymentDTO>> manualSalaryPage(
@RequestBody TeamManualSalaryPaymentQryCmd cmd);
/**
* 后台手动发放主播工资.
*/
@PostMapping("/manualSalary/pay")
ResultResponse<List<TeamManualSalaryPaymentResultDTO>> manualSalaryPay(
@RequestBody TeamManualSalaryPaymentCmd cmd);
/** /**
* 获得区域下的所有团队ID. * 获得区域下的所有团队ID.

View File

@ -217,15 +217,30 @@ public enum EnumConfigKey {
*/ */
YAHLLA_GIFT_GOLD_RATIO_SELF, YAHLLA_GIFT_GOLD_RATIO_SELF,
/** /**
* cp价格. * cp价格.
*/ */
CP_PRICE, CP_PRICE,
/** /**
* Aswat 超级管理员ID. * CP 关系解绑金币.
*/ */
ASWAT_ADMINS, CP_DISMISS_CONSUME_GOLD,
/**
* Brother 关系解绑金币.
*/
BROTHER_DISMISS_CONSUME_GOLD,
/**
* Sisters 关系解绑金币.
*/
SISTERS_DISMISS_CONSUME_GOLD,
/**
* Aswat 超级管理员ID.
*/
ASWAT_ADMINS,
/** /**
* boos位置起步金额. * boos位置起步金额.

View File

@ -0,0 +1,45 @@
package com.red.circle.other.inner.model.cmd.team;
import com.red.circle.framework.dto.Command;
import java.io.Serial;
import java.util.List;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
/**
* 后台手动发放主播工资.
*/
@EqualsAndHashCode(callSuper = true)
@Data
@Accessors(chain = true)
public class TeamManualSalaryPaymentCmd extends Command {
@Serial
private static final long serialVersionUID = 1L;
/**
* 归属系统.
*/
private String sysOrigin;
/**
* 国家 code.
*/
private String countryCode;
/**
* 工资账期为空时使用当前整月账期.
*/
private Integer billBelong;
/**
* 指定发放用户为空时按国家全量发放.
*/
private List<Long> userIds;
/**
* 后台操作人.
*/
private Long operationUserId;
}

View File

@ -0,0 +1,49 @@
package com.red.circle.other.inner.model.cmd.team;
import com.red.circle.framework.core.dto.PageCommand;
import java.io.Serial;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
/**
* 后台手动发放主播工资预览.
*/
@EqualsAndHashCode(callSuper = true)
@Data
@Accessors(chain = true)
public class TeamManualSalaryPaymentQryCmd extends PageCommand {
@Serial
private static final long serialVersionUID = 1L;
/**
* 归属系统.
*/
private String sysOrigin;
/**
* 国家 code.
*/
private String countryCode;
/**
* 工资账期为空时使用当前整月账期.
*/
private Integer billBelong;
/**
* 用户 id.
*/
private Long userId;
/**
* 排序字段.
*/
private String sortField;
/**
* 排序方向.
*/
private String sortOrder;
}

View File

@ -29,6 +29,16 @@ public class TeamManagerAddCmd extends Command {
@NotBlank(message = "regionId required.") @NotBlank(message = "regionId required.")
private String regionId; private String regionId;
private Boolean canAddBdLeader;
private Boolean canGiftProps;
private Boolean canBanUser;
private Boolean canUnbanUser;
private Boolean canChangeCountry;
@NotNull(message = "createUser required.") @NotNull(message = "createUser required.")
private Long createUser; private Long createUser;

View File

@ -0,0 +1,158 @@
package com.red.circle.other.inner.model.dto.agency.agency;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
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 TeamManualSalaryPaymentDTO implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* 成员目标记录 id.
*/
private String id;
/**
* 归属系统.
*/
private String sysOrigin;
/**
* 国家 code.
*/
private String countryCode;
/**
* 区域 id.
*/
private String region;
/**
* 团队 id.
*/
@JsonSerialize(using = ToStringSerializer.class)
private Long teamId;
/**
* 团队拥有者用户 id.
*/
@JsonSerialize(using = ToStringSerializer.class)
private Long teamOwnId;
/**
* 主播用户 id.
*/
@JsonSerialize(using = ToStringSerializer.class)
private Long userId;
/**
* 当前账期.
*/
private Integer billBelong;
/**
* 上次结算的积分.
*/
private Long lastSettlementTarget;
/**
* 本次积分.
*/
private Long currentTarget;
/**
* 本次积分档位.
*/
private Integer policyLevel;
/**
* 是否满足任意档位.
*/
private Boolean satisfyLevel;
/**
* 当前用户是否为代理.
*/
private Boolean agentMember;
/**
* 当前账期按积分档位计算出的工资.
*/
private BigDecimal expectedSalary;
/**
* 当前账期主播应发工资.
*/
private BigDecimal expectedMemberSalary;
/**
* 当前账期代理应发工资.
*/
private BigDecimal expectedAgentSalary;
/**
* 待结算工资 = 当前账期应发工资 - 当前账期实收工资.
*/
private BigDecimal payableSalary;
/**
* 主播待结算工资 = 主播应发工资 - 主播实收工资.
*/
private BigDecimal payableMemberSalary;
/**
* 代理待结算工资 = 代理应发工资 - 代理实收工资.
*/
private BigDecimal payableAgentSalary;
/**
* 当前账期总发放工资.
*/
private BigDecimal totalIssuedSalary;
/**
* 当前账期主播实收工资.
*/
private BigDecimal memberIssuedSalary;
/**
* 当前账期代理实收工资.
*/
private BigDecimal agentIssuedSalary;
/**
* 当前账期多发工资.
*/
private BigDecimal overIssuedSalary;
/**
* 当前账期主播多发工资.
*/
private BigDecimal memberOverIssuedSalary;
/**
* 当前账期代理多发工资.
*/
private BigDecimal agentOverIssuedSalary;
/**
* 当前工资余额.
*/
private BigDecimal currentSalaryBalance;
/**
* 当前档位是否已发放.
*/
private Boolean paid;
}

View File

@ -0,0 +1,88 @@
package com.red.circle.other.inner.model.dto.agency.agency;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
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 TeamManualSalaryPaymentResultDTO implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* 工资发放明细 id.
*/
private String id;
/**
* 归属系统.
*/
private String sysOrigin;
/**
* 国家 code.
*/
private String countryCode;
/**
* 区域 id.
*/
private String region;
/**
* 团队 id.
*/
@JsonSerialize(using = ToStringSerializer.class)
private Long teamId;
/**
* 团队拥有者用户 id.
*/
@JsonSerialize(using = ToStringSerializer.class)
private Long teamOwnId;
/**
* 主播用户 id.
*/
@JsonSerialize(using = ToStringSerializer.class)
private Long userId;
/**
* 当前账期.
*/
private Integer billBelong;
/**
* 发放前工资余额.
*/
private BigDecimal beforeSalaryBalance;
/**
* 发放后工资余额.
*/
private BigDecimal afterSalaryBalance;
/**
* 本次发放工资.
*/
private BigDecimal issuedSalary;
/**
* 本次积分.
*/
private Long currentTarget;
/**
* 发放档位.
*/
private Integer policyLevel;
}

View File

@ -32,6 +32,16 @@ public class TeamManagerDTO extends DTO {
private String regionId; private String regionId;
private Boolean canAddBdLeader;
private Boolean canGiftProps;
private Boolean canBanUser;
private Boolean canUnbanUser;
private Boolean canChangeCountry;
private Timestamp createTime; private Timestamp createTime;
private Timestamp updateTime; private Timestamp updateTime;

View File

@ -91,15 +91,20 @@ public class GiftConfigDTO implements Serializable {
*/ */
private String type; private String type;
/** /**
* 礼物tab. * 礼物tab.
*/ */
private String giftTab; private String giftTab;
/** /**
* 0.未删除 1.已删除. * CP礼物关系类型CP/BROTHER/SISTERS.
*/ */
private Boolean del; private String cpRelationType;
/**
* 0.未删除 1.已删除.
*/
private Boolean del;
/** /**
* 幸运礼物规格ID. * 幸运礼物规格ID.

View File

@ -1,6 +1,6 @@
package com.red.circle.wallet.inner.model.cmd; package com.red.circle.wallet.inner.model.cmd;
import com.red.circle.framework.core.dto.CommonCommand; import com.red.circle.common.business.dto.cmd.HistoryRangeTimeQryPageCmd;
import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotBlank;
import java.io.Serial; import java.io.Serial;
import lombok.Data; import lombok.Data;
@ -13,7 +13,7 @@ import lombok.experimental.Accessors;
@Data @Data
@EqualsAndHashCode(callSuper = true) @EqualsAndHashCode(callSuper = true)
@Accessors(chain = true) @Accessors(chain = true)
public class UserFreightBalanceRunningWaterExportQryCmd extends CommonCommand { public class UserFreightBalanceRunningWaterExportQryCmd extends HistoryRangeTimeQryPageCmd {
@Serial @Serial
private static final long serialVersionUID = 1L; private static final long serialVersionUID = 1L;
@ -27,7 +27,31 @@ public class UserFreightBalanceRunningWaterExportQryCmd extends CommonCommand {
/** /**
* 日期yyyyMM. * 日期yyyyMM.
*/ */
@NotBlank
private String monthDate; private String monthDate;
/**
* 发送人用户id.
*/
private Long userId;
/**
* 接收人用户id.
*/
private Long acceptUserId;
/**
* 来源.
*/
private String origin;
/**
* 查询后台操作流水.
*/
private Boolean queryBackOperationUser;
/**
* 操作用户ID多个用逗号分隔.
*/
private String createUsers;
} }

View File

@ -42,5 +42,9 @@ public class UserFreightBalanceRunningWaterPageQryCmd extends HistoryRangeTimeQr
*/ */
private Boolean queryBackOperationUser; private Boolean queryBackOperationUser;
/**
* 操作用户ID多个用逗号分隔.
*/
private String createUsers;
} }

View File

@ -50,6 +50,11 @@ public class UserGoldRunningWaterBackQryCmd extends Command {
@JsonSerialize(using = ToStringSerializer.class) @JsonSerialize(using = ToStringSerializer.class)
private Long backOperationUser; private Long backOperationUser;
/**
* 后台操作用户ID多个用逗号分隔.
*/
private String backOperationUsers;
/** /**
* 跟踪id. * 跟踪id.
*/ */

View File

@ -750,15 +750,20 @@ public enum GoldOrigin {
*/ */
USER_RED_PACKET("User red packet"), USER_RED_PACKET("User red packet"),
/** /**
* CP告白信封 * CP告白信封
*/ */
CP_LOVE_LETTER("CP love letter"), CP_LOVE_LETTER("CP love letter"),
/** /**
* cp榜单(周榜和季度榜) * 解除 CP/Brother/Sisters 关系.
*/ */
CP_RANKING_REWARD("CP ranking reward"), CP_DISMISS("CP dismiss"),
/**
* cp榜单(周榜和季度榜)
*/
CP_RANKING_REWARD("CP ranking reward"),
/** /**
* 签到付费 * 签到付费

View File

@ -0,0 +1,36 @@
package com.red.circle.console.adapter.app.activity;
import com.red.circle.console.app.dto.clienobject.app.activity.cp.ResidentCpConfigCO;
import com.red.circle.console.app.dto.cmd.app.activity.cp.ResidentCpConfigSaveCmd;
import com.red.circle.console.app.service.app.activity.ResidentCpActivityService;
import com.red.circle.console.infra.annotations.OpsOperationReqLog;
import lombok.RequiredArgsConstructor;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* 常驻 CP 后台接口.
*/
@Validated
@RestController
@RequiredArgsConstructor
@RequestMapping("/resident-activity/cp")
public class ResidentCpActivityRestController {
private final ResidentCpActivityService residentCpActivityService;
@GetMapping("/config")
public ResidentCpConfigCO getConfig(String sysOrigin) {
return residentCpActivityService.getConfig(sysOrigin);
}
@OpsOperationReqLog(value = "保存 CP 等级积分配置")
@PostMapping("/config/save")
public void saveConfig(@RequestBody @Validated ResidentCpConfigSaveCmd cmd) {
residentCpActivityService.saveConfig(cmd);
}
}

View File

@ -1,12 +1,15 @@
package com.red.circle.console.adapter.app.order; package com.red.circle.console.adapter.app.order;
import com.red.circle.console.app.dto.clienobject.order.InAppPurchaseDetailsCO; import com.red.circle.console.app.dto.clienobject.order.InAppPurchaseDetailsCO;
import com.red.circle.console.app.service.app.order.InAppPurchaseBackService; import com.red.circle.console.app.service.app.order.InAppPurchaseBackService;
import com.red.circle.framework.web.controller.BaseController; import com.red.circle.console.infra.annotations.OpsOperationReqLog;
import com.red.circle.order.inner.model.cmd.inapp.InAppPurchaseDetailsQryCmd; import com.red.circle.framework.web.controller.BaseController;
import java.util.List; import com.red.circle.order.inner.model.cmd.inapp.InAppPurchaseDetailsQryCmd;
import java.util.Map; import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor; import java.io.IOException;
import java.util.List;
import java.util.Map;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestBody;
@ -45,9 +48,19 @@ public class InAppPurchaseRestController extends BaseController {
/** /**
* 订单统计查询 - 返回金额总数和订单总数 * 订单统计查询 - 返回金额总数和订单总数
*/ */
@GetMapping("/statistics") @GetMapping("/statistics")
public Map<String, Object> statistics(InAppPurchaseDetailsQryCmd query) { public Map<String, Object> statistics(InAppPurchaseDetailsQryCmd query) {
return inAppPurchaseBackService.statistics(query); return inAppPurchaseBackService.statistics(query);
} }
} /**
* 订单详情导出.
*/
@OpsOperationReqLog(value = "导出-订单详情", ignoreRequestParam = true)
@GetMapping("/details/export")
public void exportDetails(HttpServletResponse response, InAppPurchaseDetailsQryCmd query)
throws IOException {
inAppPurchaseBackService.exportDetails(response, query);
}
}

View File

@ -4,10 +4,12 @@ package com.red.circle.console.adapter.app.sys.pay;
import com.red.circle.console.app.service.app.sys.pay.SysPayCountryService; import com.red.circle.console.app.service.app.sys.pay.SysPayCountryService;
import com.red.circle.console.infra.annotations.OpsOperationReqLog; import com.red.circle.console.infra.annotations.OpsOperationReqLog;
import com.red.circle.framework.dto.PageResult; import com.red.circle.framework.dto.PageResult;
import com.red.circle.framework.web.controller.BaseController; import com.red.circle.framework.web.controller.BaseController;
import com.red.circle.order.inner.model.cmd.SysPayCountryAddCmd; import com.red.circle.order.inner.model.cmd.SysPayCountryAddCmd;
import com.red.circle.order.inner.model.cmd.SysPayCountryQryCmd; import com.red.circle.order.inner.model.cmd.SysPayCountryCurrencyRateSyncCmd;
import com.red.circle.order.inner.model.dto.PayCountryDTO; import com.red.circle.order.inner.model.cmd.SysPayCountryQryCmd;
import com.red.circle.order.inner.model.dto.PayCountryCurrencyRateSyncDTO;
import com.red.circle.order.inner.model.dto.PayCountryDTO;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.util.HashSet; import java.util.HashSet;
import java.util.List; import java.util.List;
@ -111,10 +113,19 @@ public class SysPayOpenCountryController extends BaseController {
*/ */
@OpsOperationReqLog("修改国家上下架") @OpsOperationReqLog("修改国家上下架")
@GetMapping("/shelf") @GetMapping("/shelf")
public void shelf(@RequestParam Long id, public void shelf(@RequestParam Long id,
@RequestParam Boolean shelf) { @RequestParam Boolean shelf) {
sysPayCountryService.shelf(id, shelf); sysPayCountryService.shelf(id, shelf);
} }
/**
} * 同步全球支付国家货币和实时汇率.
*/
@OpsOperationReqLog("同步全球支付国家货币汇率")
@PostMapping("/sync-currency-rates")
public PayCountryCurrencyRateSyncDTO syncCurrencyRates(
@RequestBody @Validated SysPayCountryCurrencyRateSyncCmd cmd) {
return sysPayCountryService.syncCurrencyRates(cmd);
}
}

View File

@ -1,14 +1,21 @@
package com.red.circle.console.adapter.app.team; package com.red.circle.console.adapter.app.team;
import com.red.circle.console.app.dto.clienobject.team.TeamSalaryCO; import com.red.circle.console.app.dto.clienobject.team.TeamManualSalaryCO;
import com.red.circle.console.app.service.app.team.TeamSalaryBackService; import com.red.circle.console.app.dto.clienobject.team.TeamManualSalaryResultCO;
import com.red.circle.framework.dto.PageResult; import com.red.circle.console.app.dto.clienobject.team.TeamSalaryCO;
import com.red.circle.framework.web.controller.BaseController; import com.red.circle.console.app.service.app.team.TeamSalaryBackService;
import com.red.circle.other.inner.model.cmd.team.TeamSalaryBackQryCmd; import com.red.circle.framework.dto.PageResult;
import lombok.RequiredArgsConstructor; import com.red.circle.framework.web.controller.BaseController;
import org.springframework.web.bind.annotation.GetMapping; import com.red.circle.other.inner.model.cmd.team.TeamManualSalaryPaymentCmd;
import org.springframework.web.bind.annotation.RequestMapping; import com.red.circle.other.inner.model.cmd.team.TeamManualSalaryPaymentQryCmd;
import org.springframework.web.bind.annotation.RestController; import com.red.circle.other.inner.model.cmd.team.TeamSalaryBackQryCmd;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/** /**
* 团队自动发送工资凭证. * 团队自动发送工资凭证.
@ -26,8 +33,26 @@ public class TeamSalaryRestController extends BaseController {
* 工资凭证列表. * 工资凭证列表.
*/ */
@GetMapping("/list") @GetMapping("/list")
public PageResult<TeamSalaryCO> listByCondition(TeamSalaryBackQryCmd query) { public PageResult<TeamSalaryCO> listByCondition(TeamSalaryBackQryCmd query) {
return teamSalaryBackService.listByCondition(query); return teamSalaryBackService.listByCondition(query);
} }
} /**
* 手动工资发放预览.
*/
@GetMapping("/manual/page")
public PageResult<TeamManualSalaryCO> manualSalaryPage(TeamManualSalaryPaymentQryCmd query) {
return teamSalaryBackService.manualSalaryPage(query);
}
/**
* 确认手动发放工资.
*/
@PostMapping("/manual/pay")
public List<TeamManualSalaryResultCO> manualSalaryPay(
@RequestBody TeamManualSalaryPaymentCmd cmd) {
cmd.setOperationUserId(getReqUserId());
return teamSalaryBackService.manualSalaryPay(cmd);
}
}

View File

@ -7,6 +7,7 @@ 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.CountryDashboardQryCmd;
import com.red.circle.console.app.dto.cmd.datav.CountryDashboardRechargeDetailQryCmd; 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.CountryDashboardDailyMetricService;
import com.red.circle.console.app.service.app.datav.CountryDashboardBackfillLockService;
import com.red.circle.console.app.service.app.datav.CountryDashboardGameMetricService; 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.datav.CountryDashboardService;
import com.red.circle.console.app.service.app.user.DatavService; import com.red.circle.console.app.service.app.user.DatavService;
@ -41,6 +42,7 @@ public class DatavRestController extends BaseController {
private final DatavService userExpandService; private final DatavService userExpandService;
private final CountryDashboardService countryDashboardService; private final CountryDashboardService countryDashboardService;
private final CountryDashboardDailyMetricService countryDashboardDailyMetricService; private final CountryDashboardDailyMetricService countryDashboardDailyMetricService;
private final CountryDashboardBackfillLockService countryDashboardBackfillLockService;
private final CountryDashboardGameMetricService countryDashboardGameMetricService; private final CountryDashboardGameMetricService countryDashboardGameMetricService;
@Value("${red-circle.country-dashboard.query-enabled:true}") @Value("${red-circle.country-dashboard.query-enabled:true}")
@ -116,14 +118,21 @@ public class DatavRestController extends BaseController {
} }
LocalDate parsedStartDate = LocalDate.parse(startDate); LocalDate parsedStartDate = LocalDate.parse(startDate);
LocalDate parsedEndDate = LocalDate.parse(endDate); LocalDate parsedEndDate = LocalDate.parse(endDate);
List<String> refreshedTimezones = countryDashboardDailyMetricService.refreshRange( Map<String, Object> refreshed = countryDashboardBackfillLockService.call(() -> {
parsedStartDate, parsedEndDate, sysOrigin, statTimezones); List<String> countryTimezones = countryDashboardDailyMetricService.refreshRange(
parsedStartDate, parsedEndDate, sysOrigin, statTimezones);
List<String> gameTimezones = countryDashboardGameMetricService.refreshRange(
parsedStartDate, parsedEndDate, sysOrigin, statTimezones);
return Map.of(
"countryStatTimezones", countryTimezones,
"gameStatTimezones", gameTimezones);
});
return Map.of( return Map.of(
"status", "success", "status", "success",
"startDate", parsedStartDate.toString(), "startDate", parsedStartDate.toString(),
"endDate", parsedEndDate.toString(), "endDate", parsedEndDate.toString(),
"sysOrigin", sysOrigin, "sysOrigin", sysOrigin,
"statTimezones", refreshedTimezones); "statTimezones", refreshed);
} }
@PostMapping("/country-dashboard/game-metrics/backfill") @PostMapping("/country-dashboard/game-metrics/backfill")
@ -137,14 +146,21 @@ public class DatavRestController extends BaseController {
} }
LocalDate parsedStartDate = LocalDate.parse(startDate); LocalDate parsedStartDate = LocalDate.parse(startDate);
LocalDate parsedEndDate = LocalDate.parse(endDate); LocalDate parsedEndDate = LocalDate.parse(endDate);
List<String> refreshedTimezones = countryDashboardGameMetricService.refreshRange( Map<String, Object> refreshed = countryDashboardBackfillLockService.call(() -> {
parsedStartDate, parsedEndDate, sysOrigin, statTimezones); List<String> countryTimezones = countryDashboardDailyMetricService.refreshRange(
parsedStartDate, parsedEndDate, sysOrigin, statTimezones);
List<String> gameTimezones = countryDashboardGameMetricService.refreshRange(
parsedStartDate, parsedEndDate, sysOrigin, statTimezones);
return Map.of(
"countryStatTimezones", countryTimezones,
"gameStatTimezones", gameTimezones);
});
return Map.of( return Map.of(
"status", "success", "status", "success",
"startDate", parsedStartDate.toString(), "startDate", parsedStartDate.toString(),
"endDate", parsedEndDate.toString(), "endDate", parsedEndDate.toString(),
"sysOrigin", sysOrigin, "sysOrigin", sysOrigin,
"statTimezones", refreshedTimezones); "statTimezones", refreshed);
} }
private CountryDashboardCO emptyCountryDashboard(CountryDashboardQryCmd cmd) { private CountryDashboardCO emptyCountryDashboard(CountryDashboardQryCmd cmd) {

View File

@ -141,15 +141,22 @@ public class UserFreightRestController extends BaseController {
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"); response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
response.setCharacterEncoding("utf-8"); response.setCharacterEncoding("utf-8");
String fileName = URLEncoder.encode("Export Freight RunningWater " + query.getMonthDate(), String fileName = URLEncoder.encode("Export Freight RunningWater " + getExportNameSuffix(query),
StandardCharsets.UTF_8)
.replaceAll("\\+", "%20");
String sheetName = URLEncoder.encode("Export Freight RunningWater " + query.getMonthDate(),
StandardCharsets.UTF_8) StandardCharsets.UTF_8)
.replaceAll("\\+", "%20"); .replaceAll("\\+", "%20");
response.setHeader("Content-disposition", "attachment;filename*=utf-8''" + fileName + ".xlsx"); response.setHeader("Content-disposition", "attachment;filename*=utf-8''" + fileName + ".xlsx");
EasyExcel.write(response.getOutputStream(), UserFreightBalanceRunningWaterExportModel.class) EasyExcel.write(response.getOutputStream(), UserFreightBalanceRunningWaterExportModel.class)
.sheet(sheetName).doWrite(userFreightService.listExport(query)); .sheet("FreightRunningWater").doWrite(userFreightService.listExport(query));
}
private String getExportNameSuffix(UserFreightBalanceRunningWaterExportQryCmd query) {
if (query.getMonthDate() != null && !query.getMonthDate().isBlank()) {
return query.getMonthDate();
}
if (query.getStartTime() != null || query.getEndTime() != null) {
return query.getStartTime() + "-" + query.getEndTime();
}
return "Filtered";
} }
/** /**

View File

@ -9,11 +9,13 @@ import com.red.circle.console.infra.annotations.OpsOperationReqLog;
import com.red.circle.framework.web.controller.BaseController; import com.red.circle.framework.web.controller.BaseController;
import com.red.circle.wallet.inner.model.cmd.DeductGoldCmd; import com.red.circle.wallet.inner.model.cmd.DeductGoldCmd;
import com.red.circle.wallet.inner.model.cmd.DiamondCmd; import com.red.circle.wallet.inner.model.cmd.DiamondCmd;
import com.red.circle.wallet.inner.model.cmd.SendGoldCmd; import com.red.circle.wallet.inner.model.cmd.SendGoldCmd;
import com.red.circle.wallet.inner.model.cmd.UserGoldRunningWaterBackQryCmd; import com.red.circle.wallet.inner.model.cmd.UserGoldRunningWaterBackQryCmd;
import java.util.List; import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor; import java.io.IOException;
import org.springframework.validation.annotation.Validated; import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestBody;
@ -48,10 +50,20 @@ public class UserWalletRestController extends BaseController {
* 流水列表. * 流水列表.
*/ */
@GetMapping("/gold/running-water/cls") @GetMapping("/gold/running-water/cls")
public UserGoldRunningWaterClsTmpCO getGoldRunningWaterCls(UserGoldRunningWaterBackQryCmd query) { public UserGoldRunningWaterClsTmpCO getGoldRunningWaterCls(UserGoldRunningWaterBackQryCmd query) {
return userWalletService.getGoldRunningWaterCls(query); return userWalletService.getGoldRunningWaterCls(query);
} }
/**
* 流水导出.
*/
@GetMapping("/gold/running-water/cls/export")
public void exportGoldRunningWaterCls(
HttpServletResponse response,
@Validated UserGoldRunningWaterBackQryCmd query) throws IOException {
userWalletService.exportGoldRunningWater(response, query);
}
@OpsOperationReqLog("发送金币") @OpsOperationReqLog("发送金币")
@PostMapping("/send-gold") @PostMapping("/send-gold")
public void sendGold(@RequestBody @Validated SendGoldCmd param) { public void sendGold(@RequestBody @Validated SendGoldCmd param) {

View File

@ -23,13 +23,19 @@
<artifactId>business-rocketmq</artifactId> <artifactId>business-rocketmq</artifactId>
</dependency> </dependency>
<dependency> <dependency>
<artifactId>easyexcel</artifactId> <artifactId>easyexcel</artifactId>
<groupId>com.alibaba</groupId> <groupId>com.alibaba</groupId>
<version>${easyexcel.version}</version> <version>${easyexcel.version}</version>
</dependency> </dependency>
</dependencies> <dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<parent> <parent>
<artifactId>rc-service-console</artifactId> <artifactId>rc-service-console</artifactId>

View File

@ -94,6 +94,12 @@ public class UserFreightBalanceRunningWaterExportModel implements Serializable {
@ExcelProperty(value = "Origin Name") @ExcelProperty(value = "Origin Name")
private String originName; private String originName;
/**
* 操作人.
*/
@ExcelProperty(value = "Operator")
private String operationUserNickname;
/** /**
* 备注 * 备注
*/ */

View File

@ -0,0 +1,88 @@
package com.red.circle.console.app.excel.model.order;
import com.alibaba.excel.annotation.ExcelProperty;
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 InAppPurchaseDetailsExportModel implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
@ExcelProperty(value = "Env")
private String env;
@ExcelProperty(value = "Sys Origin")
private String sysOrigin;
@ExcelProperty(value = "Platform")
private String platform;
@ExcelProperty(value = "Factory")
private String factoryCode;
@ExcelProperty(value = "Channel")
private String channel;
@ExcelProperty(value = "Buyer User ID")
private String buyerUserId;
@ExcelProperty(value = "Buyer Account")
private String buyerAccount;
@ExcelProperty(value = "Buyer Nickname")
private String buyerNickname;
@ExcelProperty(value = "Accept User ID")
private String acceptUserId;
@ExcelProperty(value = "Accept Account")
private String acceptAccount;
@ExcelProperty(value = "Accept Nickname")
private String acceptNickname;
@ExcelProperty(value = "Amount(USD)")
private BigDecimal amountUsd;
@ExcelProperty(value = "Amount")
private BigDecimal amount;
@ExcelProperty(value = "Currency")
private String currency;
@ExcelProperty(value = "Description")
private String productDescriptor;
@ExcelProperty(value = "Status")
private String status;
@ExcelProperty(value = "Receipt Type")
private String receiptType;
@ExcelProperty(value = "Third Party Order ID")
private String orderId;
@ExcelProperty(value = "App Order ID")
private String appOrderId;
@ExcelProperty(value = "Track ID")
private String trackId;
@ExcelProperty(value = "Country Code")
private String countryCode;
@ExcelProperty(value = "Create Time")
private String createTime;
@ExcelProperty(value = "Update Time")
private String updateTime;
}

View File

@ -0,0 +1,91 @@
package com.red.circle.console.app.excel.model.wallet;
import com.alibaba.excel.annotation.ExcelProperty;
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 UserGoldRunningWaterExportModel implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* 流水ID.
*/
@ExcelProperty(value = "Record ID")
private String recordId;
/**
* 系统来源.
*/
@ExcelProperty(value = "Sys Origin")
private String sysOrigin;
/**
* 用户ID.
*/
@ExcelProperty(value = "User ID")
private String userId;
/**
* 用户账号.
*/
@ExcelProperty(value = "User Account")
private String userAccount;
/**
* 用户昵称.
*/
@ExcelProperty(value = "User Nickname")
private String userNickname;
/**
* 类型.
*/
@ExcelProperty(value = "Type")
private String type;
/**
* 金币金额.
*/
@ExcelProperty(value = "Amount")
private BigDecimal quantity;
/**
* 操作后余额.
*/
@ExcelProperty(value = "Balance")
private BigDecimal balance;
/**
* 原因.
*/
@ExcelProperty(value = "Reason")
private String originName;
/**
* 备注.
*/
@ExcelProperty(value = "Remark")
private String remark;
/**
* 操作人.
*/
@ExcelProperty(value = "Operator")
private String backOperationName;
/**
* 操作时间.
*/
@ExcelProperty(value = "Create Time")
private String createTime;
}

View File

@ -0,0 +1,38 @@
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 lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
/**
* 国家数据大屏活跃和留存增量聚合任务.
*/
@Slf4j
@Component
@ConditionalOnProperty(
name = "red-circle.country-dashboard.active-retention-refresh-enabled",
havingValue = "true",
matchIfMissing = true)
@RequiredArgsConstructor
public class CountryDashboardActiveRetentionMetricTask {
private final CountryDashboardDailyMetricService countryDashboardDailyMetricService;
@Value("${red-circle.country-dashboard.active-retention-refresh-days:32}")
private int refreshDays;
@Scheduled(cron = "${red-circle.country-dashboard.active-retention-refresh-cron:0 */10 * * * ?}",
zone = "Asia/Shanghai")
@TaskCacheLock(key = "COUNTRY_DASHBOARD_ACTIVE_RETENTION_REFRESH", expireSecond = 60 * 8)
public void refreshActiveRetention() {
long start = System.currentTimeMillis();
countryDashboardDailyMetricService.refreshActiveRetentionRecentDays(refreshDays);
log.info("country dashboard active retention refresh task finished, days={}, costMs={}",
refreshDays, System.currentTimeMillis() - start);
}
}

View File

@ -6,6 +6,7 @@ import com.red.circle.console.app.service.app.datav.CountryDashboardGameMetricSe
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.scheduling.annotation.Scheduled; import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
@ -14,6 +15,7 @@ import org.springframework.stereotype.Component;
*/ */
@Slf4j @Slf4j
@Component @Component
@ConditionalOnProperty(name = "red-circle.country-dashboard.refresh-enabled", havingValue = "true")
@RequiredArgsConstructor @RequiredArgsConstructor
public class CountryDashboardDailyMetricTask { public class CountryDashboardDailyMetricTask {

View File

@ -0,0 +1,286 @@
package com.red.circle.console.app.service.app.activity;
import com.red.circle.console.app.dto.clienobject.app.activity.cp.ResidentCpConfigCO;
import com.red.circle.console.app.dto.clienobject.app.activity.cp.ResidentCpLevelConfigCO;
import com.red.circle.console.app.dto.cmd.app.activity.cp.ResidentCpConfigSaveCmd;
import com.red.circle.console.app.dto.cmd.app.activity.cp.ResidentCpLevelConfigSaveCmd;
import com.red.circle.console.app.service.app.sys.CpCabinConfigService;
import com.red.circle.console.app.service.app.sys.SysEnumConfigService;
import com.red.circle.framework.core.asserts.ResponseAssert;
import com.red.circle.framework.core.response.ResponseErrorCode;
import com.red.circle.framework.dto.PageQuery;
import com.red.circle.framework.dto.PageResult;
import com.red.circle.other.inner.enums.config.EnumConfigKey;
import com.red.circle.other.inner.model.cmd.sys.SysCpCabinConfigQryCmd;
import com.red.circle.other.inner.model.dto.sys.EnumConfigDTO;
import com.red.circle.other.inner.model.dto.sys.SysCpCabinConfigDTO;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;
import lombok.RequiredArgsConstructor;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Service;
/**
* 常驻 CP 配置服务实现.
*/
@Service
@RequiredArgsConstructor
public class ResidentCpActivityServiceImpl implements ResidentCpActivityService {
private static final int CP_LEVEL_COUNT = 6;
private static final int CP_MIN_LEVEL = 0;
private static final int QUERY_LIMIT = 50;
private static final String CONFIG_GROUP_NAME = "CP";
private static final String CONFIG_DATA_TYPE = "int";
private static final String RELATION_TYPE_CP = "CP";
private static final String RELATION_TYPE_BROTHER = "BROTHER";
private static final String RELATION_TYPE_SISTERS = "SISTERS";
private final CpCabinConfigService cpCabinConfigService;
private final SysEnumConfigService sysEnumConfigService;
@Override
public ResidentCpConfigCO getConfig(String sysOrigin) {
String normalizedSysOrigin = normalizeSysOrigin(sysOrigin);
List<SysCpCabinConfigDTO> configs = listCpCabinConfigs(normalizedSysOrigin);
List<ResidentCpLevelConfigCO> levels = new ArrayList<>(CP_LEVEL_COUNT);
for (int level = CP_MIN_LEVEL; level < CP_LEVEL_COUNT; level++) {
SysCpCabinConfigDTO config = level < configs.size() ? configs.get(level) : null;
levels.add(toLevelCO(level, config));
}
return new ResidentCpConfigCO()
.setConfigured(!configs.isEmpty())
.setSysOrigin(normalizedSysOrigin)
.setDismissConsumeGolds(getDismissConsumeGolds(normalizedSysOrigin))
.setLevels(levels);
}
@Override
public void saveConfig(ResidentCpConfigSaveCmd cmd) {
validateConfig(cmd);
String normalizedSysOrigin = normalizeSysOrigin(cmd.getSysOrigin());
List<SysCpCabinConfigDTO> configs = listCpCabinConfigs(normalizedSysOrigin);
Map<Long, SysCpCabinConfigDTO> configById = configs.stream()
.filter(item -> Objects.nonNull(item.getId()))
.collect(Collectors.toMap(SysCpCabinConfigDTO::getId, Function.identity(), (left, right) -> left));
for (ResidentCpLevelConfigSaveCmd levelCmd : sortedLevels(cmd.getLevels())) {
SysCpCabinConfigDTO config = Optional.ofNullable(levelCmd.getCabinConfigId())
.map(configById::get)
.orElseGet(() -> levelCmd.getLevel() < configs.size()
? configs.get(levelCmd.getLevel())
: null);
boolean exists = Objects.nonNull(config) && Objects.nonNull(config.getId());
if (!exists) {
config = new SysCpCabinConfigDTO();
}
config.setSysOrigin(normalizedSysOrigin)
.setName(defaultLevelName(levelCmd.getLevel(), levelCmd.getLevelName()))
.setCabinUnlockValue(defaultRequiredValue(levelCmd.getRequiredIntimacyValue()))
.setSort(levelCmd.getLevel().longValue());
if (exists) {
cpCabinConfigService.updateCpCabin(config);
} else {
cpCabinConfigService.addCpCabin(config);
}
}
saveDismissConsumeGolds(normalizedSysOrigin, cmd.getDismissConsumeGolds());
}
private List<SysCpCabinConfigDTO> listCpCabinConfigs(String sysOrigin) {
SysCpCabinConfigQryCmd query = new SysCpCabinConfigQryCmd().setSysOrigin(sysOrigin);
PageQuery pageQuery = new PageQuery();
pageQuery.setCursor(1);
pageQuery.setLimit(QUERY_LIMIT);
pageQuery.setSearchCount(false);
query.setPageQuery(pageQuery);
PageResult<SysCpCabinConfigDTO> result = cpCabinConfigService.getCpCabin(query);
return Optional.ofNullable(result.getRecords())
.stream()
.flatMap(java.util.Collection::stream)
.sorted(Comparator
.comparing((SysCpCabinConfigDTO item) -> Optional.ofNullable(item.getSort())
.orElse(Long.MAX_VALUE))
.thenComparing(item -> Optional.ofNullable(item.getCabinUnlockValue())
.orElse(Long.MAX_VALUE))
.thenComparing(item -> Optional.ofNullable(item.getId()).orElse(Long.MAX_VALUE)))
.toList();
}
private ResidentCpLevelConfigCO toLevelCO(int level, SysCpCabinConfigDTO config) {
return new ResidentCpLevelConfigCO()
.setLevel(level)
.setLevelName(Objects.isNull(config)
? defaultLevelName(level, null)
: defaultLevelName(level, config.getName()))
.setRequiredIntimacyValue(Objects.isNull(config)
? 0L
: defaultRequiredValue(config.getCabinUnlockValue()))
.setCabinConfigId(Objects.isNull(config) ? null : config.getId());
}
private void validateConfig(ResidentCpConfigSaveCmd cmd) {
ResponseAssert.notBlank(ResponseErrorCode.REQUEST_PARAMETER_ERROR, cmd.getSysOrigin());
List<ResidentCpLevelConfigSaveCmd> levels = sortedLevels(cmd.getLevels());
Set<Integer> levelSet = new HashSet<>();
long previousRequiredValue = -1L;
for (ResidentCpLevelConfigSaveCmd level : levels) {
ResponseAssert.isTrue(ResponseErrorCode.REQUEST_PARAMETER_ERROR,
"CP level must be between 0 and 5.",
Objects.nonNull(level.getLevel()) && level.getLevel() >= CP_MIN_LEVEL
&& level.getLevel() < CP_LEVEL_COUNT && levelSet.add(level.getLevel()));
long requiredValue = defaultRequiredValue(level.getRequiredIntimacyValue());
ResponseAssert.isTrue(ResponseErrorCode.REQUEST_PARAMETER_ERROR,
"requiredIntimacyValue must be greater than or equal to zero.", requiredValue >= 0);
ResponseAssert.isTrue(ResponseErrorCode.REQUEST_PARAMETER_ERROR,
"requiredIntimacyValue must be ascending by level.",
requiredValue >= previousRequiredValue);
previousRequiredValue = requiredValue;
}
validateDismissConsumeGolds(cmd.getDismissConsumeGolds());
}
private Map<String, Long> getDismissConsumeGolds(String sysOrigin) {
Map<String, Long> result = defaultDismissConsumeGolds();
dismissConsumeGoldKeys().forEach((relationType, key) -> result.put(relationType,
enumConfigGoldValue(sysEnumConfigService.getByCode(key.name(), sysOrigin))));
return result;
}
private void saveDismissConsumeGolds(String sysOrigin, Map<String, Long> dismissConsumeGolds) {
if (Objects.isNull(dismissConsumeGolds)) {
return;
}
dismissConsumeGoldKeys().forEach((relationType, key) -> {
if (dismissConsumeGolds.containsKey(relationType)) {
saveDismissConsumeGold(sysOrigin, key, dismissConsumeGolds.get(relationType));
}
});
}
private void saveDismissConsumeGold(String sysOrigin, EnumConfigKey key, Long value) {
Long normalizedValue = defaultGold(value);
EnumConfigDTO enumConfig = sysEnumConfigService.getByCode(key.name(), sysOrigin);
boolean exists = Objects.nonNull(enumConfig) && Objects.nonNull(enumConfig.getId());
if (!exists) {
enumConfig = new EnumConfigDTO();
}
enumConfig.setSysOrigin(sysOrigin)
.setName(key.name())
.setTitle(dismissConsumeGoldTitle(key))
.setVal(String.valueOf(normalizedValue))
.setDescription(dismissConsumeGoldTitle(key))
.setDataType(CONFIG_DATA_TYPE)
.setReturnApp(Boolean.FALSE)
.setGroupName(CONFIG_GROUP_NAME);
if (exists) {
sysEnumConfigService.updateSysEnumConfig(enumConfig);
} else {
sysEnumConfigService.saveSysEnumConfig(enumConfig);
}
}
private void validateDismissConsumeGolds(Map<String, Long> dismissConsumeGolds) {
if (Objects.isNull(dismissConsumeGolds)) {
return;
}
dismissConsumeGolds.forEach((relationType, gold) -> {
ResponseAssert.isTrue(ResponseErrorCode.REQUEST_PARAMETER_ERROR,
"relationType is not supported.",
dismissConsumeGoldKeys().containsKey(normalizeRelationType(relationType)));
ResponseAssert.isTrue(ResponseErrorCode.REQUEST_PARAMETER_ERROR,
"dismissConsumeGold must be greater than or equal to zero.",
defaultGold(gold) >= 0);
});
}
private Long enumConfigGoldValue(EnumConfigDTO enumConfig) {
if (Objects.isNull(enumConfig) || StringUtils.isBlank(enumConfig.getVal())) {
return 0L;
}
try {
return new BigDecimal(enumConfig.getVal()).longValue();
} catch (NumberFormatException ex) {
return 0L;
}
}
private Map<String, Long> defaultDismissConsumeGolds() {
Map<String, Long> result = new LinkedHashMap<>();
dismissConsumeGoldKeys().keySet().forEach(relationType -> result.put(relationType, 0L));
return result;
}
private Map<String, EnumConfigKey> dismissConsumeGoldKeys() {
Map<String, EnumConfigKey> result = new LinkedHashMap<>();
result.put(RELATION_TYPE_CP, EnumConfigKey.CP_DISMISS_CONSUME_GOLD);
result.put(RELATION_TYPE_BROTHER, EnumConfigKey.BROTHER_DISMISS_CONSUME_GOLD);
result.put(RELATION_TYPE_SISTERS, EnumConfigKey.SISTERS_DISMISS_CONSUME_GOLD);
return result;
}
private String dismissConsumeGoldTitle(EnumConfigKey key) {
return switch (key) {
case CP_DISMISS_CONSUME_GOLD -> "CP 解绑金币";
case BROTHER_DISMISS_CONSUME_GOLD -> "Brother 解绑金币";
case SISTERS_DISMISS_CONSUME_GOLD -> "Sisters 解绑金币";
default -> "关系解绑金币";
};
}
private List<ResidentCpLevelConfigSaveCmd> sortedLevels(
List<ResidentCpLevelConfigSaveCmd> levels) {
if (Objects.isNull(levels)) {
return List.of();
}
return levels.stream()
.filter(Objects::nonNull)
.sorted(Comparator.comparing(ResidentCpLevelConfigSaveCmd::getLevel,
Comparator.nullsLast(Integer::compareTo)))
.toList();
}
private String normalizeSysOrigin(String sysOrigin) {
return Objects.toString(sysOrigin, "").trim().toUpperCase();
}
private Long defaultRequiredValue(Long requiredValue) {
return requiredValue == null ? 0L : requiredValue;
}
private Long defaultGold(Long gold) {
return gold == null ? 0L : gold;
}
private String normalizeRelationType(String relationType) {
if (StringUtils.isBlank(relationType)) {
return "";
}
String normalized = relationType.trim().toUpperCase();
if (Objects.equals(normalized, "BROTHERS")) {
return RELATION_TYPE_BROTHER;
}
if (Objects.equals(normalized, "SISTER")) {
return RELATION_TYPE_SISTERS;
}
return normalized;
}
private String defaultLevelName(Integer level, String levelName) {
if (StringUtils.isNotBlank(levelName)) {
return levelName.trim();
}
return "CP " + level + "";
}
}

View File

@ -0,0 +1,42 @@
package com.red.circle.console.app.service.app.datav;
import com.red.circle.console.infra.database.rds.dao.datav.CountryDashboardDAO;
import java.util.Objects;
import java.util.function.Supplier;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
/**
* Coordinates dashboard backfill and CDC worker writes through a MySQL named lock.
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class CountryDashboardBackfillLockService {
private static final String LOCK_NAME = "country_dashboard_backfill_lock";
private final CountryDashboardDAO countryDashboardDAO;
@Value("${red-circle.country-dashboard.backfill-lock-timeout-seconds:10}")
private Integer lockTimeoutSeconds;
public <T> T call(Supplier<T> supplier) {
Integer acquired = countryDashboardDAO.getDashboardBackfillLock(
LOCK_NAME, Math.max(Objects.requireNonNullElse(lockTimeoutSeconds, 10), 0));
if (!Objects.equals(acquired, 1)) {
throw new IllegalStateException("country dashboard backfill lock is held by CDC worker");
}
try {
return supplier.get();
} finally {
try {
countryDashboardDAO.releaseDashboardBackfillLock(LOCK_NAME);
} catch (Exception ex) {
log.warn("release country dashboard backfill lock failed", ex);
}
}
}
}

View File

@ -5,7 +5,10 @@ import com.red.circle.console.infra.database.rds.dao.datav.CountryDashboardDAO;
import com.red.circle.console.infra.database.rds.dto.datav.CountryDashboardDailyMetricDTO; import com.red.circle.console.infra.database.rds.dto.datav.CountryDashboardDailyMetricDTO;
import com.red.circle.console.infra.database.rds.dto.datav.CountryDashboardMetricDTO; 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.CountryDashboardPeriodMetricDTO;
import com.red.circle.console.infra.database.rds.dto.datav.CountryDashboardRetentionUserDTO;
import com.red.circle.console.infra.database.rds.dto.datav.CountryDashboardUserCountryDTO;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.sql.Timestamp;
import java.time.DayOfWeek; import java.time.DayOfWeek;
import java.time.Instant; import java.time.Instant;
import java.time.LocalDate; import java.time.LocalDate;
@ -17,6 +20,7 @@ import java.time.temporal.ChronoUnit;
import java.time.temporal.WeekFields; import java.time.temporal.WeekFields;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.HashSet;
import java.util.LinkedHashSet; import java.util.LinkedHashSet;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
import java.util.List; import java.util.List;
@ -24,9 +28,17 @@ import java.util.Locale;
import java.util.Map; import java.util.Map;
import java.util.Objects; import java.util.Objects;
import java.util.Set; import java.util.Set;
import java.util.function.BiConsumer;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.bson.Document;
import org.bson.types.Decimal128;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.core.aggregation.Aggregation;
import org.springframework.data.mongodb.core.aggregation.AggregationOperation;
import org.springframework.data.mongodb.core.query.Criteria;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.support.TransactionTemplate; import org.springframework.transaction.support.TransactionTemplate;
@ -49,14 +61,18 @@ public class CountryDashboardDailyMetricService {
private static final ZoneId STORAGE_ZONE_ID = ZoneId.of("Asia/Riyadh"); private static final ZoneId STORAGE_ZONE_ID = ZoneId.of("Asia/Riyadh");
private static final String STORAGE_TIMEZONE = "Asia/Riyadh"; private static final String STORAGE_TIMEZONE = "Asia/Riyadh";
private static final String STORAGE_TIMEZONE_OFFSET = "+03:00"; private static final String STORAGE_TIMEZONE_OFFSET = "+03:00";
private static final String COLLECTION_USER_DAILY_ACTIVE_LOG = "user_daily_active_log";
private static final String COLLECTION_GIFT_GIVE_RUNNING_WATER = "gift_give_running_water";
private static final int USER_BATCH_SIZE = 1000;
private final CountryDashboardDAO countryDashboardDAO; private final CountryDashboardDAO countryDashboardDAO;
private final TransactionTemplate transactionTemplate; private final TransactionTemplate transactionTemplate;
private final MongoTemplate mongoTemplate;
@Value("${red-circle.country-dashboard.refresh-sys-origins:LIKEI}") @Value("${red-circle.country-dashboard.refresh-sys-origins:LIKEI}")
private String refreshSysOrigins; private String refreshSysOrigins;
@Value("${red-circle.country-dashboard.refresh-timezones:Asia/Riyadh,UTC,Asia/Shanghai}") @Value("${red-circle.country-dashboard.refresh-timezones:Asia/Riyadh}")
private String refreshTimezones; private String refreshTimezones;
private volatile boolean metricSchemaEnsured; private volatile boolean metricSchemaEnsured;
@ -98,6 +114,39 @@ public class CountryDashboardDailyMetricService {
} }
} }
public void refreshActiveRetentionRecentDays(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 {
refreshActiveRetentionPeriod(toDayWindow(statDate), sysOrigin, statTimezone);
} catch (Exception ex) {
log.error("refresh country dashboard active retention day failed, statDate={}, sysOrigin={}, statTimezone={}",
statDate, sysOrigin, statTimezone.id(), ex);
}
}
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 {
refreshActiveRetentionPeriod(window, sysOrigin, statTimezone);
} catch (Exception ex) {
log.error("refresh country dashboard active retention period failed, periodType={}, periodKey={}, sysOrigin={}, statTimezone={}",
window.periodType(), window.periodKey(), sysOrigin, statTimezone.id(), ex);
}
}
}
}
}
public void refreshRange(LocalDate startDate, LocalDate endDate, String sysOrigin) { public void refreshRange(LocalDate startDate, LocalDate endDate, String sysOrigin) {
refreshRange(startDate, endDate, sysOrigin, null); refreshRange(startDate, endDate, sysOrigin, null);
} }
@ -159,6 +208,8 @@ public class CountryDashboardDailyMetricService {
countryDashboardDAO.createPeriodMetricTable(); countryDashboardDAO.createPeriodMetricTable();
countryDashboardDAO.createPeriodUserMetricTable(); countryDashboardDAO.createPeriodUserMetricTable();
countryDashboardDAO.ensurePeriodMetricTimezoneSchema(); countryDashboardDAO.ensurePeriodMetricTimezoneSchema();
countryDashboardDAO.ensurePeriodMetricUserColumnsSchema();
countryDashboardDAO.ensureMetricValueColumnsSchema();
metricSchemaEnsured = true; metricSchemaEnsured = true;
log.info("country dashboard metric tables ensured"); log.info("country dashboard metric tables ensured");
} }
@ -215,6 +266,14 @@ public class CountryDashboardDailyMetricService {
PERIOD_DAY, startTime, endTime, startTimeMillis, endTimeMillis, null, sysOrigin, PERIOD_DAY, startTime, endTime, startTimeMillis, endTimeMillis, null, sysOrigin,
STORAGE_TIMEZONE_OFFSET, STORAGE_TIMEZONE_OFFSET), STORAGE_TIMEZONE_OFFSET, STORAGE_TIMEZONE_OFFSET),
statDate, sysOrigin); statDate, sysOrigin);
TimeWindow timeWindow = new TimeWindow(startTime, endTime, startTimeMillis, endTimeMillis,
statDate.atStartOfDay(STORAGE_ZONE_ID).toInstant(),
statDate.plusDays(1).atStartOfDay(STORAGE_ZONE_ID).toInstant());
mergeDailyMongo(rows, listGiftConsume(timeWindow, sysOrigin, STORAGE_TIMEZONE),
(row, amount) -> row.setGiftConsume(add(row.getGiftConsume(), amount)), statDate, sysOrigin);
mergeDailyMongo(rows, listLuckyGiftAnchorShare(timeWindow, sysOrigin, STORAGE_TIMEZONE),
(row, amount) -> row.setLuckyGiftAnchorShare(add(row.getLuckyGiftAnchorShare(), amount)),
statDate, sysOrigin);
List<CountryDashboardDailyMetricDTO> metrics = new ArrayList<>(rows.values()); List<CountryDashboardDailyMetricDTO> metrics = new ArrayList<>(rows.values());
RefreshWriteResult writeResult = transactionTemplate.execute(status -> { RefreshWriteResult writeResult = transactionTemplate.execute(status -> {
@ -284,6 +343,15 @@ public class CountryDashboardDailyMetricService {
mergePeriodAmounts(rows, countryDashboardDAO.listLuckyBoxGame( mergePeriodAmounts(rows, countryDashboardDAO.listLuckyBoxGame(
window.periodType(), startTime, endTime, null, sysOrigin, window.periodType(), startTime, endTime, null, sysOrigin,
STORAGE_TIMEZONE_OFFSET, statTimezone.offset()), window, sysOrigin, statTimezone); STORAGE_TIMEZONE_OFFSET, statTimezone.offset()), window, sysOrigin, statTimezone);
mergePeriodMongo(rows, listGiftConsume(timeWindow, sysOrigin, statTimezone.id()),
(row, amount) -> row.setGiftConsume(add(row.getGiftConsume(), amount)),
window, sysOrigin, statTimezone);
mergePeriodMongo(rows, listLuckyGiftAnchorShare(timeWindow, sysOrigin, statTimezone.id()),
(row, amount) -> row.setLuckyGiftAnchorShare(add(row.getLuckyGiftAnchorShare(), amount)),
window, sysOrigin, statTimezone);
mergePeriodDailyActive(rows, listDailyActive(window, sysOrigin), window, sysOrigin,
statTimezone);
mergePeriodRetention(rows, window, sysOrigin, statTimezone, startTime, endTime);
RefreshWriteResult writeResult = transactionTemplate.execute(status -> { RefreshWriteResult writeResult = transactionTemplate.execute(status -> {
countryDashboardDAO.deletePeriodUserMetrics(window.periodType(), window.periodKey(), countryDashboardDAO.deletePeriodUserMetrics(window.periodType(), window.periodKey(),
@ -317,6 +385,24 @@ public class CountryDashboardDailyMetricService {
System.currentTimeMillis() - start); System.currentTimeMillis() - start);
} }
private void refreshActiveRetentionPeriod(PeriodWindow window, String sysOrigin,
StatTimezone statTimezone) {
long start = System.currentTimeMillis();
TimeWindow timeWindow = toStorageTimeWindow(window, statTimezone);
Map<String, CountryDashboardPeriodMetricDTO> rows = new LinkedHashMap<>();
mergePeriodDailyActive(rows, listDailyActive(window, sysOrigin), window, sysOrigin,
statTimezone);
mergePeriodRetention(rows, window, sysOrigin, statTimezone, timeWindow.startTime(),
timeWindow.endTime());
List<CountryDashboardPeriodMetricDTO> metrics = new ArrayList<>(rows.values());
int upserted = metrics.isEmpty() ? 0
: transactionTemplate.execute(status ->
countryDashboardDAO.upsertPeriodActiveRetentionMetrics(metrics));
log.info("refresh country dashboard active retention success, periodType={}, periodKey={}, sysOrigin={}, statTimezone={}, countries={}, upserted={}, costMs={}",
window.periodType(), window.periodKey(), sysOrigin, statTimezone.id(), metrics.size(),
upserted, System.currentTimeMillis() - start);
}
private void refreshAll(String sysOrigin, StatTimezone statTimezone) { private void refreshAll(String sysOrigin, StatTimezone statTimezone) {
long start = System.currentTimeMillis(); long start = System.currentTimeMillis();
PeriodWindow window = new PeriodWindow(PERIOD_ALL, PERIOD_ALL, "全部", null, null); PeriodWindow window = new PeriodWindow(PERIOD_ALL, PERIOD_ALL, "全部", null, null);
@ -352,15 +438,20 @@ public class CountryDashboardDailyMetricService {
for (CountryDashboardMetricDTO metric : metrics) { for (CountryDashboardMetricDTO metric : metrics) {
CountryDashboardDailyMetricDTO row = getRow(rows, metric, statDate, sysOrigin); CountryDashboardDailyMetricDTO row = getRow(rows, metric, statDate, sysOrigin);
row.setCountryNewUser(safeLong(row.getCountryNewUser()) + safeLong(metric.getCountryNewUser())); row.setCountryNewUser(safeLong(row.getCountryNewUser()) + safeLong(metric.getCountryNewUser()));
row.setDailyRechargeUser(safeLong(row.getDailyRechargeUser()) + safeLong(metric.getDailyRechargeUser()));
row.setNewUserRecharge(add(row.getNewUserRecharge(), metric.getNewUserRecharge())); row.setNewUserRecharge(add(row.getNewUserRecharge(), metric.getNewUserRecharge()));
row.setOfficialRecharge(add(row.getOfficialRecharge(), metric.getOfficialRecharge())); row.setOfficialRecharge(add(row.getOfficialRecharge(), metric.getOfficialRecharge()));
row.setMifapayRecharge(add(row.getMifapayRecharge(), metric.getMifapayRecharge())); row.setMifapayRecharge(add(row.getMifapayRecharge(), metric.getMifapayRecharge()));
row.setGoogleRecharge(add(row.getGoogleRecharge(), metric.getGoogleRecharge())); row.setGoogleRecharge(add(row.getGoogleRecharge(), metric.getGoogleRecharge()));
row.setDealerRecharge(add(row.getDealerRecharge(), metric.getDealerRecharge())); row.setDealerRecharge(add(row.getDealerRecharge(), metric.getDealerRecharge()));
row.setUserRecharge(add(row.getUserRecharge(), metric.getUserRecharge()));
row.setNewDealerUserRecharge(add(row.getNewDealerUserRecharge(), metric.getNewDealerUserRecharge()));
row.setSalaryExchange(add(row.getSalaryExchange(), metric.getSalaryExchange())); row.setSalaryExchange(add(row.getSalaryExchange(), metric.getSalaryExchange()));
row.setGiftConsume(add(row.getGiftConsume(), metric.getGiftConsume()));
row.setLuckyGiftTotalFlow(add(row.getLuckyGiftTotalFlow(), metric.getLuckyGiftTotalFlow())); row.setLuckyGiftTotalFlow(add(row.getLuckyGiftTotalFlow(), metric.getLuckyGiftTotalFlow()));
row.setLuckyGiftUser(safeLong(row.getLuckyGiftUser()) + safeLong(metric.getLuckyGiftUser())); row.setLuckyGiftUser(safeLong(row.getLuckyGiftUser()) + safeLong(metric.getLuckyGiftUser()));
row.setLuckyGiftPayout(add(row.getLuckyGiftPayout(), metric.getLuckyGiftPayout())); row.setLuckyGiftPayout(add(row.getLuckyGiftPayout(), metric.getLuckyGiftPayout()));
row.setLuckyGiftAnchorShare(add(row.getLuckyGiftAnchorShare(), metric.getLuckyGiftAnchorShare()));
row.setGameTotalFlow(add(row.getGameTotalFlow(), metric.getGameTotalFlow())); row.setGameTotalFlow(add(row.getGameTotalFlow(), metric.getGameTotalFlow()));
row.setGameUser(safeLong(row.getGameUser()) + safeLong(metric.getGameUser())); row.setGameUser(safeLong(row.getGameUser()) + safeLong(metric.getGameUser()));
row.setGamePayout(add(row.getGamePayout(), metric.getGamePayout())); row.setGamePayout(add(row.getGamePayout(), metric.getGamePayout()));
@ -381,9 +472,13 @@ public class CountryDashboardDailyMetricService {
row.setMifapayRecharge(add(row.getMifapayRecharge(), metric.getMifapayRecharge())); row.setMifapayRecharge(add(row.getMifapayRecharge(), metric.getMifapayRecharge()));
row.setGoogleRecharge(add(row.getGoogleRecharge(), metric.getGoogleRecharge())); row.setGoogleRecharge(add(row.getGoogleRecharge(), metric.getGoogleRecharge()));
row.setDealerRecharge(add(row.getDealerRecharge(), metric.getDealerRecharge())); row.setDealerRecharge(add(row.getDealerRecharge(), metric.getDealerRecharge()));
row.setUserRecharge(add(row.getUserRecharge(), metric.getUserRecharge()));
row.setNewDealerUserRecharge(add(row.getNewDealerUserRecharge(), metric.getNewDealerUserRecharge()));
row.setSalaryExchange(add(row.getSalaryExchange(), metric.getSalaryExchange())); row.setSalaryExchange(add(row.getSalaryExchange(), metric.getSalaryExchange()));
row.setGiftConsume(add(row.getGiftConsume(), metric.getGiftConsume()));
row.setLuckyGiftTotalFlow(add(row.getLuckyGiftTotalFlow(), metric.getLuckyGiftTotalFlow())); row.setLuckyGiftTotalFlow(add(row.getLuckyGiftTotalFlow(), metric.getLuckyGiftTotalFlow()));
row.setLuckyGiftPayout(add(row.getLuckyGiftPayout(), metric.getLuckyGiftPayout())); row.setLuckyGiftPayout(add(row.getLuckyGiftPayout(), metric.getLuckyGiftPayout()));
row.setLuckyGiftAnchorShare(add(row.getLuckyGiftAnchorShare(), metric.getLuckyGiftAnchorShare()));
row.setGameTotalFlow(add(row.getGameTotalFlow(), metric.getGameTotalFlow())); row.setGameTotalFlow(add(row.getGameTotalFlow(), metric.getGameTotalFlow()));
row.setGamePayout(add(row.getGamePayout(), metric.getGamePayout())); row.setGamePayout(add(row.getGamePayout(), metric.getGamePayout()));
} }
@ -399,11 +494,302 @@ public class CountryDashboardDailyMetricService {
CountryDashboardPeriodMetricDTO row = getPeriodRow(rows, metric, window, sysOrigin, CountryDashboardPeriodMetricDTO row = getPeriodRow(rows, metric, window, sysOrigin,
statTimezone); statTimezone);
row.setCountryNewUser(safeLong(metric.getCountryNewUser())); row.setCountryNewUser(safeLong(metric.getCountryNewUser()));
row.setDailyRechargeUser(safeLong(metric.getDailyRechargeUser()));
row.setLuckyGiftUser(safeLong(metric.getLuckyGiftUser())); row.setLuckyGiftUser(safeLong(metric.getLuckyGiftUser()));
row.setGameUser(safeLong(metric.getGameUser())); row.setGameUser(safeLong(metric.getGameUser()));
} }
} }
private void mergePeriodDailyActive(Map<String, CountryDashboardPeriodMetricDTO> rows,
List<CountryDashboardMetricDTO> metrics, PeriodWindow window, String sysOrigin,
StatTimezone statTimezone) {
if (metrics == null || metrics.isEmpty()) {
return;
}
for (CountryDashboardMetricDTO metric : metrics) {
CountryDashboardPeriodMetricDTO row = getPeriodRow(rows, metric, window, sysOrigin,
statTimezone);
row.setDailyActiveUser(safeLong(row.getDailyActiveUser()) + safeLong(metric.getDailyActiveUser()));
}
}
private void mergeDailyMongo(Map<String, CountryDashboardDailyMetricDTO> rows,
List<MongoMetric> metrics, BiConsumer<CountryDashboardDailyMetricDTO, BigDecimal> merger,
LocalDate statDate, String sysOrigin) {
if (metrics == null || metrics.isEmpty()) {
return;
}
Map<Long, CountryDashboardUserCountryDTO> countryMap = mapUserCountries(
metrics.stream().map(MongoMetric::userId).filter(Objects::nonNull).distinct().toList());
for (MongoMetric metric : metrics) {
CountryDashboardUserCountryDTO country = countryMap.get(metric.userId());
if (country == null) {
continue;
}
CountryDashboardMetricDTO rowKey = new CountryDashboardMetricDTO()
.setCountryCode(country.getCountryCode())
.setCountryName(country.getCountryName());
merger.accept(getRow(rows, rowKey, statDate, sysOrigin), metric.amount());
}
}
private void mergePeriodMongo(Map<String, CountryDashboardPeriodMetricDTO> rows,
List<MongoMetric> metrics, BiConsumer<CountryDashboardPeriodMetricDTO, BigDecimal> merger,
PeriodWindow window, String sysOrigin, StatTimezone statTimezone) {
if (metrics == null || metrics.isEmpty()) {
return;
}
Map<Long, CountryDashboardUserCountryDTO> countryMap = mapUserCountries(
metrics.stream().map(MongoMetric::userId).filter(Objects::nonNull).distinct().toList());
for (MongoMetric metric : metrics) {
CountryDashboardUserCountryDTO country = countryMap.get(metric.userId());
if (country == null) {
continue;
}
CountryDashboardMetricDTO rowKey = new CountryDashboardMetricDTO()
.setCountryCode(country.getCountryCode())
.setCountryName(country.getCountryName());
merger.accept(getPeriodRow(rows, rowKey, window, sysOrigin, statTimezone), metric.amount());
}
}
private void mergePeriodRetention(Map<String, CountryDashboardPeriodMetricDTO> rows,
PeriodWindow window, String sysOrigin, StatTimezone statTimezone, LocalDateTime startTime,
LocalDateTime endTime) {
mergePeriodRetentionDays(rows, window, sysOrigin, statTimezone, startTime.minusDays(1),
endTime.minusDays(1), 1);
mergePeriodRetentionDays(rows, window, sysOrigin, statTimezone, startTime.minusDays(7),
endTime.minusDays(7), 7);
mergePeriodRetentionDays(rows, window, sysOrigin, statTimezone, startTime.minusDays(30),
endTime.minusDays(30), 30);
}
private void mergePeriodRetentionDays(Map<String, CountryDashboardPeriodMetricDTO> rows,
PeriodWindow window, String sysOrigin, StatTimezone statTimezone, LocalDateTime cohortStartTime,
LocalDateTime cohortEndTime, int retentionDays) {
List<CountryDashboardRetentionUserDTO> cohorts = countryDashboardDAO.listNewUserCohorts(
window.periodType(), cohortStartTime, cohortEndTime, null, sysOrigin,
STORAGE_TIMEZONE_OFFSET, statTimezone.offset());
if (cohorts == null || cohorts.isEmpty()) {
return;
}
LocalDate today = LocalDate.now(statTimezone.zoneId());
Set<Long> userIds = new HashSet<>();
Set<String> activeDates = new HashSet<>();
for (CountryDashboardRetentionUserDTO cohort : cohorts) {
if (cohort.getUserId() == null || !hasText(cohort.getRegisterDate())) {
continue;
}
userIds.add(cohort.getUserId());
collectRetentionActiveDate(activeDates, cohort.getRegisterDate(), retentionDays, today);
}
Set<String> activeUserDates = listActiveUserDateSet(userIds, activeDates, sysOrigin);
for (CountryDashboardRetentionUserDTO cohort : cohorts) {
if (cohort.getUserId() == null || !hasText(cohort.getRegisterDate())) {
continue;
}
CountryDashboardMetricDTO metric = new CountryDashboardMetricDTO()
.setCountryCode(cohort.getCountryCode())
.setCountryName(cohort.getCountryName());
CountryDashboardPeriodMetricDTO row = getPeriodRow(rows, metric, window, sysOrigin,
statTimezone);
mergeRetentionDay(row, cohort.getUserId(), cohort.getRegisterDate(), retentionDays, today,
activeUserDates);
}
}
private List<CountryDashboardMetricDTO> listDailyActive(PeriodWindow window, String sysOrigin) {
if (window.periodStartDate() == null || window.periodEndDate() == null) {
return List.of();
}
List<Criteria> criteria = new ArrayList<>();
criteria.add(Criteria.where("activeDate")
.gte(window.periodStartDate().format(DATE_FORMATTER))
.lt(window.periodEndDate().format(DATE_FORMATTER)));
if (hasText(sysOrigin)) {
criteria.add(Criteria.where("sysOrigin").is(sysOrigin));
}
List<AggregationOperation> operations = new ArrayList<>();
operations.add(Aggregation.match(new Criteria().andOperator(criteria.toArray(new Criteria[0]))));
operations.add(Aggregation.group("registerCountryCode", "userId"));
operations.add(Aggregation.group("_id.registerCountryCode").count().as("dailyActiveUser"));
return mongoTemplate.aggregate(Aggregation.newAggregation(operations),
COLLECTION_USER_DAILY_ACTIVE_LOG, Document.class)
.getMappedResults()
.stream()
.map(this::toDailyActiveMetric)
.filter(Objects::nonNull)
.toList();
}
private CountryDashboardMetricDTO toDailyActiveMetric(Document document) {
String countryCode = normalizeCountryCode(document.get("_id"));
Long dailyActiveUser = toLong(document.get("dailyActiveUser"));
if (dailyActiveUser == null || dailyActiveUser <= 0) {
return null;
}
return new CountryDashboardMetricDTO()
.setCountryCode(countryCode)
.setCountryName(countryCode)
.setDailyActiveUser(dailyActiveUser);
}
private List<MongoMetric> listGiftConsume(TimeWindow timeWindow, String sysOrigin,
String statTimezone) {
List<Criteria> criteria = baseGiftCriteria(timeWindow, sysOrigin);
criteria.add(Criteria.where("luckyGift").ne(Boolean.TRUE));
criteria.add(Criteria.where("originId").regex("^\\d+$"));
criteria.add(new Criteria().orOperator(
Criteria.where("dynamicContentId").exists(false),
Criteria.where("dynamicContentId").is(null),
Criteria.where("dynamicContentId").is("")
));
return aggregateGiftAmount(criteria, "$giftValue.actualAmount", false, statTimezone);
}
private List<MongoMetric> listLuckyGiftAnchorShare(TimeWindow timeWindow, String sysOrigin,
String statTimezone) {
List<Criteria> criteria = baseGiftCriteria(timeWindow, sysOrigin);
criteria.add(Criteria.where("luckyGift").is(Boolean.TRUE));
return aggregateGiftAmount(criteria, "$acceptUsers.targetAmount", true, statTimezone);
}
private List<Criteria> baseGiftCriteria(TimeWindow timeWindow, String sysOrigin) {
List<Criteria> criteria = new ArrayList<>();
criteria.add(Criteria.where("refunded").ne(Boolean.TRUE));
criteria.add(Criteria.where("userId").ne(null));
criteria.add(Criteria.where("createTime").gte(Timestamp.from(timeWindow.startInstant())));
criteria.add(Criteria.where("createTime").lt(Timestamp.from(timeWindow.endInstant())));
if (hasText(sysOrigin)) {
criteria.add(Criteria.where("sysOrigin").is(sysOrigin));
}
return criteria;
}
private List<MongoMetric> aggregateGiftAmount(List<Criteria> criteria, String amountPath,
boolean unwindAcceptUsers, String statTimezone) {
List<AggregationOperation> operations = new ArrayList<>();
operations.add(Aggregation.match(new Criteria().andOperator(criteria.toArray(new Criteria[0]))));
if (unwindAcceptUsers) {
operations.add(Aggregation.unwind("acceptUsers"));
}
operations.add(context -> new Document("$project",
new Document("userId", "$userId")
.append("amount", amountPath)
.append("day", new Document("$dateToString",
new Document("format", "%Y-%m-%d")
.append("date", "$createTime")
.append("timezone", statTimezone)))));
operations.add(Aggregation.group("userId", "day").sum("amount").as("amount"));
return mongoTemplate.aggregate(Aggregation.newAggregation(operations),
COLLECTION_GIFT_GIVE_RUNNING_WATER, Document.class)
.getMappedResults()
.stream()
.map(this::toMongoMetric)
.filter(Objects::nonNull)
.toList();
}
private MongoMetric toMongoMetric(Document document) {
Object id = document.get("_id");
if (!(id instanceof Document idDoc)) {
return null;
}
Long userId = toLong(idDoc.get("userId"));
String day = Objects.toString(idDoc.get("day"), null);
BigDecimal amount = toBigDecimal(document.get("amount"));
if (userId == null || !hasText(day) || amount == null) {
return null;
}
return new MongoMetric(userId, day, amount);
}
private void collectRetentionActiveDate(Set<String> activeDates, String registerDate,
int retentionDays, LocalDate today) {
LocalDate targetDate = LocalDate.parse(registerDate, DATE_FORMATTER).plusDays(retentionDays);
if (!targetDate.isAfter(today)) {
activeDates.add(targetDate.format(DATE_FORMATTER));
}
}
private void mergeRetentionDay(CountryDashboardPeriodMetricDTO row, Long userId,
String registerDate, int retentionDays, LocalDate today, Set<String> activeUserDates) {
LocalDate targetDate = LocalDate.parse(registerDate, DATE_FORMATTER).plusDays(retentionDays);
if (targetDate.isAfter(today)) {
return;
}
String activeKey = userId + "|" + targetDate.format(DATE_FORMATTER);
boolean retained = activeUserDates.contains(activeKey);
if (retentionDays == 1) {
row.setD1RetentionBaseUser(safeLong(row.getD1RetentionBaseUser()) + 1);
row.setD1RetentionUser(safeLong(row.getD1RetentionUser()) + (retained ? 1 : 0));
return;
}
if (retentionDays == 7) {
row.setD7RetentionBaseUser(safeLong(row.getD7RetentionBaseUser()) + 1);
row.setD7RetentionUser(safeLong(row.getD7RetentionUser()) + (retained ? 1 : 0));
return;
}
row.setD30RetentionBaseUser(safeLong(row.getD30RetentionBaseUser()) + 1);
row.setD30RetentionUser(safeLong(row.getD30RetentionUser()) + (retained ? 1 : 0));
}
private Set<String> listActiveUserDateSet(Set<Long> userIds, Set<String> activeDates,
String sysOrigin) {
Set<String> result = new HashSet<>();
if (userIds == null || userIds.isEmpty() || activeDates == null || activeDates.isEmpty()) {
return result;
}
List<Long> ids = new ArrayList<>(userIds);
for (int start = 0; start < ids.size(); start += USER_BATCH_SIZE) {
int end = Math.min(start + USER_BATCH_SIZE, ids.size());
List<Criteria> criteria = new ArrayList<>();
criteria.add(Criteria.where("userId").in(ids.subList(start, end)));
criteria.add(Criteria.where("activeDate").in(activeDates));
if (hasText(sysOrigin)) {
criteria.add(Criteria.where("sysOrigin").is(sysOrigin));
}
Query query = Query.query(new Criteria().andOperator(criteria.toArray(new Criteria[0])));
query.fields().include("userId").include("activeDate");
List<Document> documents = mongoTemplate.find(query, Document.class,
COLLECTION_USER_DAILY_ACTIVE_LOG);
for (Document document : documents) {
Long userId = toLong(document.get("userId"));
String activeDate = Objects.toString(document.get("activeDate"), null);
if (userId != null && hasText(activeDate)) {
result.add(userId + "|" + activeDate);
}
}
}
return result;
}
private Map<Long, CountryDashboardUserCountryDTO> mapUserCountries(List<Long> userIds) {
Map<Long, CountryDashboardUserCountryDTO> result = new LinkedHashMap<>();
if (userIds == null || userIds.isEmpty()) {
return result;
}
for (int start = 0; start < userIds.size(); start += USER_BATCH_SIZE) {
int end = Math.min(start + USER_BATCH_SIZE, userIds.size());
List<CountryDashboardUserCountryDTO> batch = countryDashboardDAO.listUserCountryByIds(
userIds.subList(start, end));
if (batch == null) {
continue;
}
for (CountryDashboardUserCountryDTO item : batch) {
if (item.getUserId() != null) {
result.put(item.getUserId(), item);
}
}
}
return result;
}
private CountryDashboardDailyMetricDTO getRow(Map<String, CountryDashboardDailyMetricDTO> rows, private CountryDashboardDailyMetricDTO getRow(Map<String, CountryDashboardDailyMetricDTO> rows,
CountryDashboardMetricDTO metric, LocalDate statDate, String sysOrigin) { CountryDashboardMetricDTO metric, LocalDate statDate, String sysOrigin) {
String countryCode = hasText(metric.getCountryCode()) ? metric.getCountryCode() : "UNKNOWN"; String countryCode = hasText(metric.getCountryCode()) ? metric.getCountryCode() : "UNKNOWN";
@ -477,7 +863,9 @@ public class CountryDashboardDailyMetricService {
LocalDateTime.ofInstant(startInstant, STORAGE_ZONE_ID), LocalDateTime.ofInstant(startInstant, STORAGE_ZONE_ID),
LocalDateTime.ofInstant(endInstant, STORAGE_ZONE_ID), LocalDateTime.ofInstant(endInstant, STORAGE_ZONE_ID),
startInstant.toEpochMilli(), startInstant.toEpochMilli(),
endInstant.toEpochMilli()); endInstant.toEpochMilli(),
startInstant,
endInstant);
} }
private List<StatTimezone> resolveStatTimezones(String statTimezonesValue) { private List<StatTimezone> resolveStatTimezones(String statTimezonesValue) {
@ -486,7 +874,7 @@ public class CountryDashboardDailyMetricService {
config = trimToNull(refreshTimezones); config = trimToNull(refreshTimezones);
} }
if (config == null) { if (config == null) {
config = "Asia/Riyadh,UTC,Asia/Shanghai"; config = "Asia/Riyadh";
} }
Map<String, StatTimezone> zones = new LinkedHashMap<>(); Map<String, StatTimezone> zones = new LinkedHashMap<>();
@ -559,6 +947,37 @@ public class CountryDashboardDailyMetricService {
return value == null ? 0L : value; return value == null ? 0L : value;
} }
private static Long toLong(Object value) {
if (value instanceof Number number) {
return number.longValue();
}
if (value == null) {
return null;
}
return Long.valueOf(Objects.toString(value));
}
private static BigDecimal toBigDecimal(Object value) {
if (value == null) {
return BigDecimal.ZERO;
}
if (value instanceof BigDecimal bigDecimal) {
return bigDecimal;
}
if (value instanceof Decimal128 decimal128) {
return decimal128.bigDecimalValue();
}
if (value instanceof Number number) {
return new BigDecimal(number.toString());
}
return new BigDecimal(Objects.toString(value, "0"));
}
private static String normalizeCountryCode(Object value) {
String countryCode = Objects.toString(value, "").trim();
return countryCode.isEmpty() ? "UNKNOWN" : countryCode;
}
private static boolean hasText(String value) { private static boolean hasText(String value) {
return value != null && !value.trim().isEmpty(); return value != null && !value.trim().isEmpty();
} }
@ -579,9 +998,12 @@ public class CountryDashboardDailyMetricService {
} }
private record TimeWindow(LocalDateTime startTime, LocalDateTime endTime, private record TimeWindow(LocalDateTime startTime, LocalDateTime endTime,
Long startTimeMillis, Long endTimeMillis) { Long startTimeMillis, Long endTimeMillis, Instant startInstant, Instant endInstant) {
} }
private record RefreshWriteResult(int deleted, int inserted, int userRows) { private record RefreshWriteResult(int deleted, int inserted, int userRows) {
} }
private record MongoMetric(Long userId, String day, BigDecimal amount) {
}
} }

View File

@ -56,7 +56,7 @@ public class CountryDashboardGameMetricServiceImpl implements CountryDashboardGa
@Value("${red-circle.country-dashboard.refresh-sys-origins:LIKEI}") @Value("${red-circle.country-dashboard.refresh-sys-origins:LIKEI}")
private String refreshSysOrigins; private String refreshSysOrigins;
@Value("${red-circle.country-dashboard.refresh-timezones:Asia/Riyadh,UTC,Asia/Shanghai}") @Value("${red-circle.country-dashboard.refresh-timezones:Asia/Riyadh}")
private String refreshTimezones; private String refreshTimezones;
private volatile boolean metricSchemaEnsured; private volatile boolean metricSchemaEnsured;
@ -478,7 +478,7 @@ public class CountryDashboardGameMetricServiceImpl implements CountryDashboardGa
config = trimToNull(refreshTimezones); config = trimToNull(refreshTimezones);
} }
if (config == null) { if (config == null) {
config = "Asia/Riyadh,UTC,Asia/Shanghai"; config = "Asia/Riyadh";
} }
Map<String, StatTimezone> zones = new LinkedHashMap<>(); Map<String, StatTimezone> zones = new LinkedHashMap<>();

View File

@ -1,6 +1,7 @@
package com.red.circle.console.app.service.app.datav; package com.red.circle.console.app.service.app.datav;
import com.red.circle.console.app.dto.clientobject.datav.CountryDashboardCO; import com.red.circle.console.app.dto.clientobject.datav.CountryDashboardCO;
import com.red.circle.console.app.dto.clientobject.datav.CountryDashboardArpuSummaryCO;
import com.red.circle.console.app.dto.clientobject.datav.CountryDashboardMetricCO; import com.red.circle.console.app.dto.clientobject.datav.CountryDashboardMetricCO;
import com.red.circle.console.app.dto.clientobject.datav.CountryDashboardRechargeDetailCO; import com.red.circle.console.app.dto.clientobject.datav.CountryDashboardRechargeDetailCO;
import com.red.circle.console.app.dto.cmd.datav.CountryDashboardQryCmd; import com.red.circle.console.app.dto.cmd.datav.CountryDashboardQryCmd;
@ -72,33 +73,24 @@ public class CountryDashboardServiceImpl implements CountryDashboardService {
private final CountryDashboardDAO countryDashboardDAO; private final CountryDashboardDAO countryDashboardDAO;
private final MongoTemplate mongoTemplate; private final MongoTemplate mongoTemplate;
private volatile boolean rechargeDetailSchemaEnsured;
private volatile boolean periodMetricSchemaEnsured;
@Override @Override
public CountryDashboardCO query(CountryDashboardQryCmd cmd) { public CountryDashboardCO query(CountryDashboardQryCmd cmd) {
ensurePeriodMetricSchema();
QueryCondition condition = QueryCondition.of(cmd); QueryCondition condition = QueryCondition.of(cmd);
CountryDashboardCO dashboard = queryPrecomputed(condition);
dashboard.setArpuSummary(buildArpuSummary(condition));
return dashboard;
}
private CountryDashboardCO queryPrecomputed(QueryCondition condition) {
Map<String, CountryDashboardMetricCO> rows = new LinkedHashMap<>(); Map<String, CountryDashboardMetricCO> rows = new LinkedHashMap<>();
boolean usePrecomputedMetrics = shouldUsePrecomputedMetrics(condition); mergeSql(rows, countryDashboardDAO.listPrecomputedPeriodMetrics(
if (usePrecomputedMetrics) { condition.periodType, condition.startTime, condition.endTime,
mergeSql(rows, countryDashboardDAO.listPrecomputedPeriodMetrics( condition.countryKeyword, condition.statTimezone, condition.sysOrigin));
condition.periodType, condition.startTime, condition.endTime,
condition.countryKeyword, condition.statTimezone, condition.sysOrigin));
} else {
mergeLiveSql(rows, condition);
mergeBankSalaryExchange(rows, condition);
}
if (usePrecomputedMetrics && condition.shouldOverlayLiveSalaryExchange()) {
overlayLiveSalaryExchange(rows, condition);
}
mergeMongo(rows, condition, listGiftConsume(condition),
(row, amount) -> row.setGiftConsume(add(row.getGiftConsume(), amount)));
mergeMongo(rows, condition, listLuckyGiftAnchorShare(condition),
(row, amount) -> row.setLuckyGiftAnchorShare(add(row.getLuckyGiftAnchorShare(), amount)));
mergeMongo(rows, condition, listBankSalaryTransfer(condition),
(row, amount) -> row.setSalaryTransfer(add(row.getSalaryTransfer(), amount)));
mergeDailyActive(rows, condition, listDailyActive(condition));
mergeRetention(rows, condition);
List<CountryDashboardMetricCO> records = rows.values().stream() List<CountryDashboardMetricCO> records = rows.values().stream()
.peek(this::calculateFormulaFields) .peek(this::calculateFormulaFields)
@ -120,6 +112,13 @@ public class CountryDashboardServiceImpl implements CountryDashboardService {
.setRecords(records); .setRecords(records);
} }
private CountryDashboardArpuSummaryCO buildArpuSummary(QueryCondition condition) {
return new CountryDashboardArpuSummaryCO()
.setDay(queryPrecomputed(condition.recentDays(1)))
.setThreeDay(queryPrecomputed(condition.recentDays(3)))
.setWeek(queryPrecomputed(condition.recentDays(7)));
}
private void mergeLiveSql(Map<String, CountryDashboardMetricCO> rows, QueryCondition condition) { private void mergeLiveSql(Map<String, CountryDashboardMetricCO> rows, QueryCondition condition) {
mergeSql(rows, countryDashboardDAO.listNewUsers( mergeSql(rows, countryDashboardDAO.listNewUsers(
condition.periodType, condition.startTime, condition.endTime, condition.periodType, condition.startTime, condition.endTime,
@ -303,32 +302,60 @@ public class CountryDashboardServiceImpl implements CountryDashboardService {
@Override @Override
public PageResult<CountryDashboardRechargeDetailCO> pageRechargeDetails( public PageResult<CountryDashboardRechargeDetailCO> pageRechargeDetails(
CountryDashboardRechargeDetailQryCmd cmd) { CountryDashboardRechargeDetailQryCmd cmd) {
QueryCondition condition = QueryCondition.of(cmd); ensureRechargeDetailSchema();
int cursor = Math.max(1, cmd == null || cmd.getCursor() == null ? 1 : cmd.getCursor()); int cursor = Math.max(1, cmd == null || cmd.getCursor() == null ? 1 : cmd.getCursor());
int limit = Math.max(1, cmd == null || cmd.getLimit() == null ? 100 : cmd.getLimit()); int limit = Math.max(1, cmd == null || cmd.getLimit() == null ? 100 : cmd.getLimit());
limit = Math.min(100, limit); limit = Math.min(100, limit);
int offset = (cursor - 1) * limit;
String countryCodes = normalizeCountryCodes(cmd == null ? null : cmd.getCountryCodes());
Long total = countryDashboardDAO.countRechargeDetails( QueryCondition condition = QueryCondition.of(cmd);
String countryCodes = QueryCondition.trimToNull(cmd == null ? null : cmd.getCountryCodes());
long total = safeLong(countryDashboardDAO.countRechargeDetails(
condition.startTime, condition.endTime, condition.countryKeyword, countryCodes, condition.startTime, condition.endTime, condition.countryKeyword, countryCodes,
condition.sysOrigin, condition.storageTimezoneOffset, condition.statTimezoneOffset); condition.sysOrigin, condition.storageTimezoneOffset, condition.statTimezoneOffset));
List<CountryDashboardRechargeDetailCO> records = countryDashboardDAO.listRechargeDetails( List<CountryDashboardRechargeDetailCO> records = total <= 0 ? List.of()
condition.startTime, condition.endTime, condition.countryKeyword, countryCodes, : countryDashboardDAO.listRechargeDetails(
condition.sysOrigin, condition.storageTimezoneOffset, condition.statTimezoneOffset, condition.startTime, condition.endTime, condition.countryKeyword, countryCodes,
offset, limit) condition.sysOrigin, condition.storageTimezoneOffset, condition.statTimezoneOffset,
.stream() (cursor - 1) * limit, limit)
.map(this::toRechargeDetailCO) .stream()
.toList(); .map(this::toRechargeDetailCO)
.toList();
PageResult<CountryDashboardRechargeDetailCO> result = new PageResult<>(); PageResult<CountryDashboardRechargeDetailCO> result = new PageResult<>();
result.setCurrent(cursor); result.setCurrent(cursor);
result.setSize(limit); result.setSize(limit);
result.setTotal(total == null ? 0L : total); result.setTotal(total);
result.setRecords(records); result.setRecords(records);
return result; return result;
} }
private void ensureRechargeDetailSchema() {
if (rechargeDetailSchemaEnsured) {
return;
}
synchronized (this) {
if (rechargeDetailSchemaEnsured) {
return;
}
countryDashboardDAO.createRechargeDetailTable();
rechargeDetailSchemaEnsured = true;
}
}
private void ensurePeriodMetricSchema() {
if (periodMetricSchemaEnsured) {
return;
}
synchronized (this) {
if (periodMetricSchemaEnsured) {
return;
}
countryDashboardDAO.createPeriodMetricTable();
countryDashboardDAO.ensurePeriodMetricUserColumnsSchema();
countryDashboardDAO.ensureMetricValueColumnsSchema();
periodMetricSchemaEnsured = true;
}
}
private CountryDashboardRechargeDetailCO toRechargeDetailCO( private CountryDashboardRechargeDetailCO toRechargeDetailCO(
CountryDashboardRechargeDetailDTO dto) { CountryDashboardRechargeDetailDTO dto) {
return new CountryDashboardRechargeDetailCO() return new CountryDashboardRechargeDetailCO()
@ -355,18 +382,31 @@ public class CountryDashboardServiceImpl implements CountryDashboardService {
CountryDashboardMetricCO row = getRow(rows, metric.getCountryCode(), metric.getCountryName(), CountryDashboardMetricCO row = getRow(rows, metric.getCountryCode(), metric.getCountryName(),
metric.getPeriodKey(), metric.getPeriodName()); metric.getPeriodKey(), metric.getPeriodName());
row.setCountryNewUser(row.getCountryNewUser() + safeLong(metric.getCountryNewUser())); row.setCountryNewUser(row.getCountryNewUser() + safeLong(metric.getCountryNewUser()));
row.setDailyActiveUser(row.getDailyActiveUser() + safeLong(metric.getDailyActiveUser()));
row.setDailyRechargeUser(row.getDailyRechargeUser() + safeLong(metric.getDailyRechargeUser()));
row.setNewUserRecharge(add(row.getNewUserRecharge(), metric.getNewUserRecharge())); row.setNewUserRecharge(add(row.getNewUserRecharge(), metric.getNewUserRecharge()));
row.setOfficialRecharge(add(row.getOfficialRecharge(), metric.getOfficialRecharge())); row.setOfficialRecharge(add(row.getOfficialRecharge(), metric.getOfficialRecharge()));
row.setMifapayRecharge(add(row.getMifapayRecharge(), metric.getMifapayRecharge())); row.setMifapayRecharge(add(row.getMifapayRecharge(), metric.getMifapayRecharge()));
row.setGoogleRecharge(add(row.getGoogleRecharge(), metric.getGoogleRecharge())); row.setGoogleRecharge(add(row.getGoogleRecharge(), metric.getGoogleRecharge()));
row.setDealerRecharge(add(row.getDealerRecharge(), metric.getDealerRecharge())); row.setDealerRecharge(add(row.getDealerRecharge(), metric.getDealerRecharge()));
row.setUserRecharge(add(row.getUserRecharge(), metric.getUserRecharge()));
row.setNewDealerUserRecharge(add(row.getNewDealerUserRecharge(), metric.getNewDealerUserRecharge()));
row.setSalaryExchange(add(row.getSalaryExchange(), metric.getSalaryExchange())); row.setSalaryExchange(add(row.getSalaryExchange(), metric.getSalaryExchange()));
row.setSalaryTransfer(add(row.getSalaryTransfer(), metric.getSalaryTransfer()));
row.setGiftConsume(add(row.getGiftConsume(), metric.getGiftConsume()));
row.setLuckyGiftTotalFlow(add(row.getLuckyGiftTotalFlow(), metric.getLuckyGiftTotalFlow())); row.setLuckyGiftTotalFlow(add(row.getLuckyGiftTotalFlow(), metric.getLuckyGiftTotalFlow()));
row.setLuckyGiftUser(row.getLuckyGiftUser() + safeLong(metric.getLuckyGiftUser())); row.setLuckyGiftUser(row.getLuckyGiftUser() + safeLong(metric.getLuckyGiftUser()));
row.setLuckyGiftPayout(add(row.getLuckyGiftPayout(), metric.getLuckyGiftPayout())); row.setLuckyGiftPayout(add(row.getLuckyGiftPayout(), metric.getLuckyGiftPayout()));
row.setLuckyGiftAnchorShare(add(row.getLuckyGiftAnchorShare(), metric.getLuckyGiftAnchorShare()));
row.setGameTotalFlow(add(row.getGameTotalFlow(), metric.getGameTotalFlow())); row.setGameTotalFlow(add(row.getGameTotalFlow(), metric.getGameTotalFlow()));
row.setGameUser(row.getGameUser() + safeLong(metric.getGameUser())); row.setGameUser(row.getGameUser() + safeLong(metric.getGameUser()));
row.setGamePayout(add(row.getGamePayout(), metric.getGamePayout())); row.setGamePayout(add(row.getGamePayout(), metric.getGamePayout()));
row.setD1RetentionUser(row.getD1RetentionUser() + safeLong(metric.getD1RetentionUser()));
row.setD1RetentionBaseUser(row.getD1RetentionBaseUser() + safeLong(metric.getD1RetentionBaseUser()));
row.setD7RetentionUser(row.getD7RetentionUser() + safeLong(metric.getD7RetentionUser()));
row.setD7RetentionBaseUser(row.getD7RetentionBaseUser() + safeLong(metric.getD7RetentionBaseUser()));
row.setD30RetentionUser(row.getD30RetentionUser() + safeLong(metric.getD30RetentionUser()));
row.setD30RetentionBaseUser(row.getD30RetentionBaseUser() + safeLong(metric.getD30RetentionBaseUser()));
} }
} }
@ -747,11 +787,14 @@ public class CountryDashboardServiceImpl implements CountryDashboardService {
for (CountryDashboardMetricCO row : records) { for (CountryDashboardMetricCO row : records) {
total.setCountryNewUser(total.getCountryNewUser() + safeLong(row.getCountryNewUser())); total.setCountryNewUser(total.getCountryNewUser() + safeLong(row.getCountryNewUser()));
total.setDailyActiveUser(total.getDailyActiveUser() + safeLong(row.getDailyActiveUser())); total.setDailyActiveUser(total.getDailyActiveUser() + safeLong(row.getDailyActiveUser()));
total.setDailyRechargeUser(total.getDailyRechargeUser() + safeLong(row.getDailyRechargeUser()));
total.setNewUserRecharge(add(total.getNewUserRecharge(), row.getNewUserRecharge())); total.setNewUserRecharge(add(total.getNewUserRecharge(), row.getNewUserRecharge()));
total.setOfficialRecharge(add(total.getOfficialRecharge(), row.getOfficialRecharge())); total.setOfficialRecharge(add(total.getOfficialRecharge(), row.getOfficialRecharge()));
total.setMifapayRecharge(add(total.getMifapayRecharge(), row.getMifapayRecharge())); total.setMifapayRecharge(add(total.getMifapayRecharge(), row.getMifapayRecharge()));
total.setGoogleRecharge(add(total.getGoogleRecharge(), row.getGoogleRecharge())); total.setGoogleRecharge(add(total.getGoogleRecharge(), row.getGoogleRecharge()));
total.setDealerRecharge(add(total.getDealerRecharge(), row.getDealerRecharge())); total.setDealerRecharge(add(total.getDealerRecharge(), row.getDealerRecharge()));
total.setUserRecharge(add(total.getUserRecharge(), row.getUserRecharge()));
total.setNewDealerUserRecharge(add(total.getNewDealerUserRecharge(), row.getNewDealerUserRecharge()));
total.setSalaryExchange(add(total.getSalaryExchange(), row.getSalaryExchange())); total.setSalaryExchange(add(total.getSalaryExchange(), row.getSalaryExchange()));
total.setSalaryTransfer(add(total.getSalaryTransfer(), row.getSalaryTransfer())); total.setSalaryTransfer(add(total.getSalaryTransfer(), row.getSalaryTransfer()));
total.setGiftConsume(add(total.getGiftConsume(), row.getGiftConsume())); total.setGiftConsume(add(total.getGiftConsume(), row.getGiftConsume()));
@ -950,6 +993,34 @@ public class CountryDashboardServiceImpl implements CountryDashboardService {
); );
} }
private QueryCondition recentDays(int dayCount) {
LocalDate end = LocalDate.now(statZoneId);
LocalDate start = end.minusDays(Math.max(1, dayCount) - 1L);
Instant recentStartInstant = start.atStartOfDay(statZoneId).toInstant();
Instant recentEndInstant = end.plusDays(1).atStartOfDay(statZoneId).toInstant();
LocalDateTime recentStorageStartTime =
LocalDateTime.ofInstant(recentStartInstant, STORAGE_ZONE_ID);
LocalDateTime recentStorageEndTime =
LocalDateTime.ofInstant(recentEndInstant, STORAGE_ZONE_ID);
return new QueryCondition(
"DAY",
DATE_FORMATTER.format(start),
DATE_FORMATTER.format(end),
statTimezone,
statZoneId,
storageTimezoneOffset,
statTimezoneOffset,
start,
end,
recentStorageStartTime,
recentStorageEndTime,
recentStartInstant,
recentEndInstant,
null,
sysOrigin
);
}
private PeriodBucket toPeriodBucket(String day) { private PeriodBucket toPeriodBucket(String day) {
if (PERIOD_ALL.equals(periodType)) { if (PERIOD_ALL.equals(periodType)) {
return new PeriodBucket(PERIOD_ALL, "全部"); return new PeriodBucket(PERIOD_ALL, "全部");

View File

@ -1,21 +1,30 @@
package com.red.circle.console.app.service.app.order; package com.red.circle.console.app.service.app.order;
import com.red.circle.console.app.dto.clienobject.order.InAppPurchaseDetailsCO; import com.alibaba.excel.EasyExcel;
import com.red.circle.console.infra.database.rds.entity.admin.User; import com.red.circle.console.app.dto.clienobject.order.InAppPurchaseDetailsCO;
import com.red.circle.console.infra.database.rds.service.admin.UserService; import com.red.circle.console.app.excel.EasyExcelUtils;
import com.red.circle.framework.core.asserts.ResponseAssert; import com.red.circle.console.app.excel.model.order.InAppPurchaseDetailsExportModel;
import com.red.circle.order.inner.asserts.OrderErrorCode; import com.red.circle.console.infra.database.rds.entity.admin.User;
import com.red.circle.order.inner.endpoint.InAppPurchaseDetailsClient; import com.red.circle.console.infra.database.rds.service.admin.UserService;
import com.red.circle.order.inner.model.cmd.inapp.InAppPurchaseDetailsQryCmd; import com.red.circle.framework.core.asserts.ResponseAssert;
import com.red.circle.order.inner.model.dto.inapp.InAppPurchaseDetailsDTO; import com.red.circle.order.inner.asserts.OrderErrorCode;
import com.red.circle.tool.core.collection.CollectionUtils; import com.red.circle.order.inner.endpoint.InAppPurchaseDetailsClient;
import com.red.circle.other.inner.model.dto.user.UserProfileDTO; import com.red.circle.order.inner.model.cmd.inapp.InAppPurchaseDetailsQryCmd;
import com.red.circle.other.inner.endpoint.user.user.UserProfileClient; import com.red.circle.order.inner.model.dto.inapp.InAppPurchaseFactoryDTO;
import java.util.Collection; import com.red.circle.order.inner.model.dto.inapp.InAppPurchaseDetailsDTO;
import java.util.List; import com.red.circle.tool.core.collection.CollectionUtils;
import java.util.Map; import com.red.circle.tool.core.text.StringUtils;
import java.util.Objects; import com.red.circle.other.inner.model.dto.user.UserProfileDTO;
import java.util.Optional; import com.red.circle.other.inner.endpoint.user.user.UserProfileClient;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set; import java.util.Set;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import java.util.stream.Stream; import java.util.stream.Stream;
@ -126,11 +135,92 @@ public class InAppPurchaseBackServiceImpl implements InAppPurchaseBackService {
}).collect(Collectors.toList()); }).collect(Collectors.toList());
} }
@Override @Override
public Map<String, Object> statistics(InAppPurchaseDetailsQryCmd query) { public Map<String, Object> statistics(InAppPurchaseDetailsQryCmd query) {
return ResponseAssert.requiredSuccess( return ResponseAssert.requiredSuccess(
inAppPurchaseDetailsClient.statistics(query) inAppPurchaseDetailsClient.statistics(query)
); );
} }
} @Override
public void exportDetails(HttpServletResponse response, InAppPurchaseDetailsQryCmd query)
throws IOException {
query.setLastId(null);
query.setLimit(null);
query.setExportAll(Boolean.TRUE);
List<InAppPurchaseDetailsExportModel> exportRows = listDetails(query).stream()
.map(this::toExportModel)
.collect(Collectors.toList());
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
response.setCharacterEncoding(StandardCharsets.UTF_8.name());
String fileName = URLEncoder.encode("InAppPurchaseOrders", StandardCharsets.UTF_8)
.replaceAll("\\+", "%20");
response.setHeader("Content-disposition", "attachment;filename*=utf-8''" + fileName + ".xlsx");
EasyExcel.write(response.getOutputStream(), InAppPurchaseDetailsExportModel.class)
.registerWriteHandler(EasyExcelUtils.getStyleStrategy())
.sheet("Orders")
.doWrite(exportRows);
}
private InAppPurchaseDetailsExportModel toExportModel(InAppPurchaseDetailsCO co) {
InAppPurchaseDetailsDTO details = co.getDetails();
InAppPurchaseFactoryDTO factory = Objects.isNull(details) ? null : details.getFactory();
UserProfileDTO buyer = pickBuyerProfile(co, details);
UserProfileDTO accept = co.getAcceptUserProfile();
return new InAppPurchaseDetailsExportModel()
.setEnv(text(Objects.isNull(details) ? null : details.getEnv()))
.setSysOrigin(text(Objects.isNull(details) ? null : details.getSysOrigin()))
.setPlatform(text(Objects.isNull(factory) ? null : factory.getPlatform()))
.setFactoryCode(text(Objects.isNull(factory) ? null : factory.getFactoryCode()))
.setChannel(text(Objects.isNull(factory) ? null : factory.getFactoryChannelCode()))
.setBuyerUserId(userId(buyer, Objects.isNull(details) ? null : details.getCreateUser()))
.setBuyerAccount(text(Objects.isNull(buyer) ? null : buyer.getAccount()))
.setBuyerNickname(buyerNickname(co, buyer))
.setAcceptUserId(userId(accept, Objects.isNull(details) ? null : details.getAcceptUserId()))
.setAcceptAccount(text(Objects.isNull(accept) ? null : accept.getAccount()))
.setAcceptNickname(text(Objects.isNull(accept) ? null : accept.getUserNickname()))
.setAmountUsd(Objects.isNull(details) ? null : details.getAmountUsd())
.setAmount(Objects.isNull(details) ? null : details.getAmount())
.setCurrency(text(Objects.isNull(details) ? null : details.getCurrency()))
.setProductDescriptor(text(Objects.isNull(details) ? null : details.getProductDescriptor()))
.setStatus(text(Objects.isNull(details) ? null : details.getStatus()))
.setReceiptType(text(Objects.isNull(details) ? null : details.getReceiptType()))
.setOrderId(text(Objects.isNull(details) ? null : details.getOrderId()))
.setAppOrderId(text(Objects.isNull(details) ? null : details.getId()))
.setTrackId(text(Objects.isNull(details) ? null : details.getTrackId()))
.setCountryCode(text(Objects.isNull(details) ? null : details.getCountryCode()))
.setCreateTime(text(Objects.isNull(details) ? null : details.getCreateTime()))
.setUpdateTime(text(Objects.isNull(details) ? null : details.getUpdateTime()));
}
private UserProfileDTO pickBuyerProfile(InAppPurchaseDetailsCO co, InAppPurchaseDetailsDTO details) {
if (Objects.nonNull(co.getCreateUserProfile())) {
return co.getCreateUserProfile();
}
if (Objects.nonNull(details) && Objects.equals(details.getCreateUser(), details.getAcceptUserId())) {
return co.getAcceptUserProfile();
}
return null;
}
private String buyerNickname(InAppPurchaseDetailsCO co, UserProfileDTO buyer) {
if (StringUtils.isNotBlank(co.getCreateNickname())) {
return co.getCreateNickname();
}
return text(Objects.isNull(buyer) ? null : buyer.getUserNickname());
}
private String userId(UserProfileDTO profile, Long fallback) {
if (Objects.nonNull(profile) && Objects.nonNull(profile.getId())) {
return Objects.toString(profile.getId());
}
return text(fallback);
}
private String text(Object value) {
return Objects.isNull(value) ? "" : Objects.toString(value);
}
}

View File

@ -33,14 +33,19 @@ public class SysEnumConfigServiceImpl implements SysEnumConfigService {
} }
@Override @Override
public String getViolationAvatar(EnumConfigKey violationPicture) { public String getViolationAvatar(EnumConfigKey violationPicture) {
return ResponseAssert.requiredSuccess(enumConfigClient.getViolationAvatar(violationPicture)); return ResponseAssert.requiredSuccess(enumConfigClient.getViolationAvatar(violationPicture));
} }
@Override @Override
public void saveSysEnumConfig(EnumConfigDTO sysEnumConfig) { public EnumConfigDTO getByCode(String name, String sysOrigin) {
ResponseAssert.requiredSuccess(enumConfigClient.saveSysEnumConfig(sysEnumConfig)); return ResponseAssert.requiredSuccess(enumConfigClient.getByCode(name, sysOrigin));
} }
@Override
public void saveSysEnumConfig(EnumConfigDTO sysEnumConfig) {
ResponseAssert.requiredSuccess(enumConfigClient.saveSysEnumConfig(sysEnumConfig));
}
@Override @Override
public void updateSysEnumConfig(EnumConfigDTO sysEnumConfig) { public void updateSysEnumConfig(EnumConfigDTO sysEnumConfig) {

View File

@ -2,10 +2,12 @@ package com.red.circle.console.app.service.app.sys.pay;
import com.red.circle.framework.core.asserts.ResponseAssert; import com.red.circle.framework.core.asserts.ResponseAssert;
import com.red.circle.framework.dto.PageResult; import com.red.circle.framework.dto.PageResult;
import com.red.circle.order.inner.endpoint.PayCountryClient; import com.red.circle.order.inner.endpoint.PayCountryClient;
import com.red.circle.order.inner.model.cmd.SysPayCountryAddCmd; import com.red.circle.order.inner.model.cmd.SysPayCountryAddCmd;
import com.red.circle.order.inner.model.cmd.SysPayCountryQryCmd; import com.red.circle.order.inner.model.cmd.SysPayCountryCurrencyRateSyncCmd;
import com.red.circle.order.inner.model.dto.PayCountryDTO; import com.red.circle.order.inner.model.cmd.SysPayCountryQryCmd;
import com.red.circle.order.inner.model.dto.PayCountryCurrencyRateSyncDTO;
import com.red.circle.order.inner.model.dto.PayCountryDTO;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
@ -64,14 +66,19 @@ public class SysPayCountryServiceImpl implements SysPayCountryService {
} }
@Override @Override
public void shelf(Long id, Boolean shelf) { public void shelf(Long id, Boolean shelf) {
ResponseAssert.requiredSuccess(payCountryClient.shelf(id, shelf)); ResponseAssert.requiredSuccess(payCountryClient.shelf(id, shelf));
} }
@Override @Override
public Map<Long, PayCountryDTO> mapCountryByIds(Set<Long> ids) { public PayCountryCurrencyRateSyncDTO syncCurrencyRates(SysPayCountryCurrencyRateSyncCmd cmd) {
return ResponseAssert.requiredSuccess(payCountryClient.mapCountryByIds(ids)); return ResponseAssert.requiredSuccess(payCountryClient.syncCurrencyRates(cmd));
} }
@Override
public Map<Long, PayCountryDTO> mapCountryByIds(Set<Long> ids) {
return ResponseAssert.requiredSuccess(payCountryClient.mapCountryByIds(ids));
}
@Override @Override
public List<PayCountryDTO> listPayCountryByRegionId(String regionId) { public List<PayCountryDTO> listPayCountryByRegionId(String regionId) {

View File

@ -1,25 +1,33 @@
package com.red.circle.console.app.service.app.team; package com.red.circle.console.app.service.app.team;
import com.google.common.collect.Maps; import com.google.common.collect.Maps;
import com.google.common.collect.Sets; import com.google.common.collect.Sets;
import com.red.circle.console.app.convertor.team.TeamAppConvertor; import com.red.circle.console.app.dto.clienobject.team.TeamManualSalaryCO;
import com.red.circle.console.app.dto.clienobject.team.TeamSalaryCO; import com.red.circle.console.app.dto.clienobject.team.TeamManualSalaryResultCO;
import com.red.circle.console.app.dto.clienobject.team.TeamSalaryDetailsCO; import com.red.circle.console.app.convertor.team.TeamAppConvertor;
import com.red.circle.framework.core.asserts.ResponseAssert; import com.red.circle.console.app.dto.clienobject.team.TeamSalaryCO;
import com.red.circle.framework.dto.PageResult; import com.red.circle.console.app.dto.clienobject.team.TeamSalaryDetailsCO;
import com.red.circle.other.inner.endpoint.team.target.TeamProfileClient; import com.red.circle.framework.core.asserts.ResponseAssert;
import com.red.circle.other.inner.model.cmd.team.TeamSalaryBackQryCmd; import com.red.circle.framework.dto.PageResult;
import com.red.circle.other.inner.model.dto.agency.agency.TeamSalaryPaymentDTO; import com.red.circle.other.inner.endpoint.team.target.TeamProfileClient;
import com.red.circle.other.inner.model.dto.agency.agency.TeamSalaryPaymentDetailsDTO; import com.red.circle.other.inner.model.cmd.team.TeamManualSalaryPaymentCmd;
import com.red.circle.tool.core.collection.CollectionUtils; import com.red.circle.other.inner.model.cmd.team.TeamManualSalaryPaymentQryCmd;
import com.red.circle.tool.core.text.StringUtils; import com.red.circle.other.inner.model.cmd.team.TeamSalaryBackQryCmd;
import com.red.circle.other.inner.model.dto.user.UserProfileDTO; import com.red.circle.other.inner.model.dto.agency.agency.TeamManualSalaryPaymentDTO;
import com.red.circle.other.inner.endpoint.user.region.RegionConfigClient; import com.red.circle.other.inner.model.dto.agency.agency.TeamManualSalaryPaymentResultDTO;
import com.red.circle.other.inner.endpoint.user.user.UserProfileClient; import com.red.circle.other.inner.model.dto.agency.agency.TeamSalaryPaymentDTO;
import java.util.Comparator; import com.red.circle.other.inner.model.dto.agency.agency.TeamSalaryPaymentDetailsDTO;
import java.util.Map; import com.red.circle.tool.core.collection.CollectionUtils;
import java.util.Objects; import com.red.circle.tool.core.text.StringUtils;
import java.util.Set; import com.red.circle.other.inner.model.dto.user.UserProfileDTO;
import com.red.circle.other.inner.endpoint.user.region.RegionConfigClient;
import com.red.circle.other.inner.endpoint.user.user.UserProfileClient;
import java.util.Collection;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
@ -110,8 +118,41 @@ public class TeamSalaryBackServiceImpl implements TeamSalaryBackService {
return salaryVO; return salaryVO;
}).filter(Objects::nonNull).collect(Collectors.toList())); }).filter(Objects::nonNull).collect(Collectors.toList()));
return page; return page;
} }
@Override
public PageResult<TeamManualSalaryCO> manualSalaryPage(TeamManualSalaryPaymentQryCmd query) {
PageResult<TeamManualSalaryPaymentDTO> pageResult = ResponseAssert.requiredSuccess(
teamProfileClient.manualSalaryPage(query));
PageResult<TeamManualSalaryCO> page = pageResult.convert(this::toManualSalaryCO);
if (page.checkRecordsEmpty()) {
return page;
}
Map<Long, UserProfileDTO> userProfileMap = getUserProfileMap(page.getRecords().stream()
.map(TeamManualSalaryCO::getUserId)
.collect(Collectors.toSet()));
page.getRecords().forEach(record -> record.setUserProfile(userProfileMap.get(record.getUserId())));
return page;
}
@Override
public List<TeamManualSalaryResultCO> manualSalaryPay(TeamManualSalaryPaymentCmd cmd) {
List<TeamManualSalaryPaymentResultDTO> results = ResponseAssert.requiredSuccess(
teamProfileClient.manualSalaryPay(cmd));
if (CollectionUtils.isEmpty(results)) {
return List.of();
}
Map<Long, UserProfileDTO> userProfileMap = getUserProfileMap(results.stream()
.map(TeamManualSalaryPaymentResultDTO::getUserId)
.collect(Collectors.toSet()));
return results.stream()
.map(result -> toManualSalaryResultCO(result)
.setUserProfile(userProfileMap.get(result.getUserId())))
.collect(Collectors.toList());
}
private Map<String, String> getMapRegionName(TeamSalaryBackQryCmd query) { private Map<String, String> getMapRegionName(TeamSalaryBackQryCmd query) {
return ResponseAssert.requiredSuccess( return ResponseAssert.requiredSuccess(
@ -144,7 +185,70 @@ public class TeamSalaryBackServiceImpl implements TeamSalaryBackService {
return Maps.newHashMap(); return Maps.newHashMap();
} }
return ResponseAssert.requiredSuccess(userProfileClient.mapByUserIds(userIds)); return ResponseAssert.requiredSuccess(userProfileClient.mapByUserIds(userIds));
} }
} private Map<Long, UserProfileDTO> getUserProfileMap(Collection<Long> userIds) {
if (CollectionUtils.isEmpty(userIds)) {
return Maps.newHashMap();
}
Set<Long> userIdSet = userIds.stream()
.filter(Objects::nonNull)
.collect(Collectors.toSet());
if (CollectionUtils.isEmpty(userIdSet)) {
return Maps.newHashMap();
}
return ResponseAssert.requiredSuccess(userProfileClient.mapByUserIds(userIdSet));
}
private TeamManualSalaryCO toManualSalaryCO(TeamManualSalaryPaymentDTO dto) {
return new TeamManualSalaryCO()
.setId(dto.getId())
.setSysOrigin(dto.getSysOrigin())
.setCountryCode(dto.getCountryCode())
.setRegion(dto.getRegion())
.setTeamId(dto.getTeamId())
.setTeamOwnId(dto.getTeamOwnId())
.setUserId(dto.getUserId())
.setBillBelong(dto.getBillBelong())
.setLastSettlementTarget(dto.getLastSettlementTarget())
.setCurrentTarget(dto.getCurrentTarget())
.setPolicyLevel(dto.getPolicyLevel())
.setSatisfyLevel(dto.getSatisfyLevel())
.setAgentMember(dto.getAgentMember())
.setExpectedSalary(dto.getExpectedSalary())
.setExpectedMemberSalary(dto.getExpectedMemberSalary())
.setExpectedAgentSalary(dto.getExpectedAgentSalary())
.setPayableSalary(dto.getPayableSalary())
.setPayableMemberSalary(dto.getPayableMemberSalary())
.setPayableAgentSalary(dto.getPayableAgentSalary())
.setTotalIssuedSalary(dto.getTotalIssuedSalary())
.setMemberIssuedSalary(dto.getMemberIssuedSalary())
.setAgentIssuedSalary(dto.getAgentIssuedSalary())
.setOverIssuedSalary(dto.getOverIssuedSalary())
.setMemberOverIssuedSalary(dto.getMemberOverIssuedSalary())
.setAgentOverIssuedSalary(dto.getAgentOverIssuedSalary())
.setCurrentSalaryBalance(dto.getCurrentSalaryBalance())
.setAgentMember(dto.getAgentMember())
.setPaid(dto.getPaid());
}
private TeamManualSalaryResultCO toManualSalaryResultCO(
TeamManualSalaryPaymentResultDTO dto) {
return new TeamManualSalaryResultCO()
.setId(dto.getId())
.setSysOrigin(dto.getSysOrigin())
.setCountryCode(dto.getCountryCode())
.setRegion(dto.getRegion())
.setTeamId(dto.getTeamId())
.setTeamOwnId(dto.getTeamOwnId())
.setUserId(dto.getUserId())
.setBillBelong(dto.getBillBelong())
.setBeforeSalaryBalance(dto.getBeforeSalaryBalance())
.setAfterSalaryBalance(dto.getAfterSalaryBalance())
.setIssuedSalary(dto.getIssuedSalary())
.setCurrentTarget(dto.getCurrentTarget())
.setPolicyLevel(dto.getPolicyLevel());
}
}

View File

@ -92,6 +92,11 @@ public class TeamManagerServiceImpl implements TeamManagerService {
.orElse("")) .orElse(""))
.setRegionId(info.getRegionId()) .setRegionId(info.getRegionId())
.setRegionName(regionNameMap.get(info.getRegionId())) .setRegionName(regionNameMap.get(info.getRegionId()))
.setCanAddBdLeader(defaultEnabled(info.getCanAddBdLeader()))
.setCanGiftProps(defaultEnabled(info.getCanGiftProps()))
.setCanBanUser(defaultEnabled(info.getCanBanUser()))
.setCanUnbanUser(defaultEnabled(info.getCanUnbanUser()))
.setCanChangeCountry(defaultEnabled(info.getCanChangeCountry()))
.setCreateTime(info.getCreateTime()) .setCreateTime(info.getCreateTime())
.setUpdateTime(info.getUpdateTime())); .setUpdateTime(info.getUpdateTime()));
} }
@ -112,6 +117,11 @@ public class TeamManagerServiceImpl implements TeamManagerService {
.setSysOrigin(cmd.getSysOrigin()) .setSysOrigin(cmd.getSysOrigin())
.setContact(cmd.getContact()) .setContact(cmd.getContact())
.setRegionId(cmd.getRegionId()) .setRegionId(cmd.getRegionId())
.setCanAddBdLeader(defaultEnabled(cmd.getCanAddBdLeader()))
.setCanGiftProps(defaultEnabled(cmd.getCanGiftProps()))
.setCanBanUser(defaultEnabled(cmd.getCanBanUser()))
.setCanUnbanUser(defaultEnabled(cmd.getCanUnbanUser()))
.setCanChangeCountry(defaultEnabled(cmd.getCanChangeCountry()))
.setCreateUser(cmd.getCreateUser()); .setCreateUser(cmd.getCreateUser());
ResponseAssert.requiredSuccess(teamManagerClient.add(addCmd)); ResponseAssert.requiredSuccess(teamManagerClient.add(addCmd));
@ -124,6 +134,11 @@ public class TeamManagerServiceImpl implements TeamManagerService {
.setId(cmd.getId()) .setId(cmd.getId())
.setContact(cmd.getContact()) .setContact(cmd.getContact())
.setRegionId(cmd.getRegionId()) .setRegionId(cmd.getRegionId())
.setCanAddBdLeader(cmd.getCanAddBdLeader())
.setCanGiftProps(cmd.getCanGiftProps())
.setCanBanUser(cmd.getCanBanUser())
.setCanUnbanUser(cmd.getCanUnbanUser())
.setCanChangeCountry(cmd.getCanChangeCountry())
.setUpdateUser(cmd.getUpdateUser()))); .setUpdateUser(cmd.getUpdateUser())));
} }
@ -144,4 +159,8 @@ public class TeamManagerServiceImpl implements TeamManagerService {
ResponseAssert.requiredSuccess(administratorClient.deleteSysAdministrator(administrator.getId())); ResponseAssert.requiredSuccess(administratorClient.deleteSysAdministrator(administrator.getId()));
} }
private Boolean defaultEnabled(Boolean value) {
return !Boolean.FALSE.equals(value);
}
} }

View File

@ -456,6 +456,11 @@ public class UserFreightBalanceServiceImpl implements
Map<Long, RegionConfigDTO> regionConfigMap = ResponseAssert.requiredSuccess( Map<Long, RegionConfigDTO> regionConfigMap = ResponseAssert.requiredSuccess(
userRegionClient.mapUserRegionConfig( userRegionClient.mapUserRegionConfig(
new UserRegionConfigMapCmd().setUserIds(userIds))); new UserRegionConfigMapCmd().setUserIds(userIds)));
Map<Long, String> opsUserNicknameMap = userService.mapBackUserNickname(
waters.stream()
.map(UserFreightBalanceRunningWaterDTO::getCreateUser)
.filter(Objects::nonNull)
.collect(Collectors.toSet()));
return waters.stream().map(water -> { return waters.stream().map(water -> {
@ -472,6 +477,9 @@ public class UserFreightBalanceServiceImpl implements
.map(RegionConfigDTO::getRegionCode) .map(RegionConfigDTO::getRegionCode)
.orElse("未知")); .orElse("未知"));
exportModel.setOriginName(FreightBalanceOrigin.getDesc(water.getOrigin())); exportModel.setOriginName(FreightBalanceOrigin.getDesc(water.getOrigin()));
exportModel.setOperationUserNickname(Optional.ofNullable(water.getCreateUser())
.map(opsUserNicknameMap::get)
.orElse(""));
exportModel.setAccount(getAccountDescribe(sendUser)); exportModel.setAccount(getAccountDescribe(sendUser));
exportModel.setNickname(Optional.ofNullable(sendUser) exportModel.setNickname(Optional.ofNullable(sendUser)
.map(UserProfileDTO::getUserNickname).orElse("?")); .map(UserProfileDTO::getUserNickname).orElse("?"));

View File

@ -1,18 +1,23 @@
package com.red.circle.console.app.service.app.user; package com.red.circle.console.app.service.app.user;
import com.alibaba.nacos.shaded.com.google.common.collect.Lists; import com.alibaba.excel.EasyExcel;
import com.red.circle.common.business.core.enums.ReceiptType; import com.alibaba.excel.ExcelWriter;
import com.red.circle.common.business.core.enums.SysOriginPlatformEnum; import com.alibaba.excel.write.metadata.WriteSheet;
import com.red.circle.common.business.enums.DiamondOrigin; import com.alibaba.nacos.shaded.com.google.common.collect.Lists;
import com.red.circle.console.app.convertor.WalletAppConvertor; import com.red.circle.common.business.core.enums.ReceiptType;
import com.red.circle.console.app.dto.clienobject.user.UserGoldBalanceCO; import com.red.circle.common.business.core.enums.SysOriginPlatformEnum;
import com.red.circle.console.app.dto.clienobject.user.UserGoldRunningWaterCO; import com.red.circle.common.business.enums.DiamondOrigin;
import com.red.circle.console.app.dto.clienobject.user.UserGoldRunningWaterClsTmpCO; import com.red.circle.console.app.convertor.WalletAppConvertor;
import com.red.circle.console.infra.database.rds.service.admin.UserService; import com.red.circle.console.app.dto.clienobject.user.UserGoldBalanceCO;
import com.red.circle.console.inner.error.ConsoleErrorCode; import com.red.circle.console.app.dto.clienobject.user.UserGoldRunningWaterCO;
import com.red.circle.framework.core.asserts.ResponseAssert; import com.red.circle.console.app.dto.clienobject.user.UserGoldRunningWaterClsTmpCO;
import com.red.circle.framework.mybatis.constant.PageConstant; import com.red.circle.console.app.excel.EasyExcelUtils;
import com.red.circle.order.inner.endpoint.UserRechargeCountClient; import com.red.circle.console.app.excel.model.wallet.UserGoldRunningWaterExportModel;
import com.red.circle.console.infra.database.rds.service.admin.UserService;
import com.red.circle.console.inner.error.ConsoleErrorCode;
import com.red.circle.framework.core.asserts.ResponseAssert;
import com.red.circle.framework.mybatis.constant.PageConstant;
import com.red.circle.order.inner.endpoint.UserRechargeCountClient;
import com.red.circle.order.inner.model.enums.MonthlyRechargeType; import com.red.circle.order.inner.model.enums.MonthlyRechargeType;
import com.red.circle.other.inner.endpoint.activity.CumulativeRechargeClient; import com.red.circle.other.inner.endpoint.activity.CumulativeRechargeClient;
import com.red.circle.other.inner.endpoint.sys.AnchorSalaryExchangeGoldClient; import com.red.circle.other.inner.endpoint.sys.AnchorSalaryExchangeGoldClient;
@ -21,11 +26,11 @@ import com.red.circle.other.inner.model.cmd.sys.SysAnchorSalaryExchangeGoldCmd;
import com.red.circle.tool.core.collection.CollectionUtils; import com.red.circle.tool.core.collection.CollectionUtils;
import com.red.circle.tool.core.date.TimestampUtils; import com.red.circle.tool.core.date.TimestampUtils;
import com.red.circle.tool.core.num.ArithmeticUtils; import com.red.circle.tool.core.num.ArithmeticUtils;
import com.red.circle.tool.core.sequence.IdWorkerUtils; import com.red.circle.tool.core.sequence.IdWorkerUtils;
import com.red.circle.tool.core.text.StringUtils; import com.red.circle.tool.core.text.StringUtils;
import com.red.circle.other.inner.asserts.user.UserErrorCode; import com.red.circle.other.inner.asserts.user.UserErrorCode;
import com.red.circle.other.inner.endpoint.user.user.UserProfileClient; import com.red.circle.other.inner.endpoint.user.user.UserProfileClient;
import com.red.circle.other.inner.endpoint.user.user.UserSvipClient; import com.red.circle.other.inner.endpoint.user.user.UserSvipClient;
import com.red.circle.other.inner.model.dto.user.UserProfileDTO; import com.red.circle.other.inner.model.dto.user.UserProfileDTO;
import com.red.circle.wallet.inner.endpoint.wallet.WalletDiamondClient; import com.red.circle.wallet.inner.endpoint.wallet.WalletDiamondClient;
import com.red.circle.wallet.inner.endpoint.wallet.WalletGoldClient; import com.red.circle.wallet.inner.endpoint.wallet.WalletGoldClient;
@ -35,15 +40,23 @@ import com.red.circle.wallet.inner.model.cmd.DiamondCmd;
import com.red.circle.wallet.inner.model.cmd.DiamondReceiptCmd; import com.red.circle.wallet.inner.model.cmd.DiamondReceiptCmd;
import com.red.circle.wallet.inner.model.cmd.GoldReceiptCmd; import com.red.circle.wallet.inner.model.cmd.GoldReceiptCmd;
import com.red.circle.wallet.inner.model.cmd.SendGoldCmd; import com.red.circle.wallet.inner.model.cmd.SendGoldCmd;
import com.red.circle.wallet.inner.model.cmd.UserGoldRunningWaterBackQryCmd; import com.red.circle.wallet.inner.model.cmd.UserGoldRunningWaterBackQryCmd;
import com.red.circle.wallet.inner.model.dto.UserGoldRunningWaterClsHistoryDTO; import com.red.circle.wallet.inner.model.dto.UserGoldRunningWaterClsHistoryDTO;
import com.red.circle.wallet.inner.model.dto.UserGoldRunningWaterHistoryDTO; import com.red.circle.wallet.inner.model.dto.UserGoldRunningWaterDTO;
import com.red.circle.wallet.inner.model.enums.GoldOrigin; import com.red.circle.wallet.inner.model.dto.UserGoldRunningWaterHistoryDTO;
import java.math.BigDecimal; import com.red.circle.wallet.inner.model.enums.GoldOrigin;
import java.util.List; import jakarta.servlet.http.HttpServletResponse;
import java.util.Map; import java.io.IOException;
import java.util.Objects; import java.math.BigDecimal;
import java.util.Optional; import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.time.Instant;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
@ -54,12 +67,16 @@ import org.springframework.stereotype.Service;
* @author pengliang on 2022/7/21 * @author pengliang on 2022/7/21
*/ */
@Service @Service
@RequiredArgsConstructor @RequiredArgsConstructor
public class UserWalletServiceImpl implements UserWalletService { public class UserWalletServiceImpl implements UserWalletService {
private final UserService userService; private static final int EXPORT_BATCH_SIZE = 1000;
private final UserSvipClient userSvipClient; private static final DateTimeFormatter EXPORT_TIME_FORMATTER =
private final WalletGoldClient walletGoldClient; DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
private final UserService userService;
private final UserSvipClient userSvipClient;
private final WalletGoldClient walletGoldClient;
private final UserProfileClient userProfileClient; private final UserProfileClient userProfileClient;
private final UserLevelClient userLevelClient; private final UserLevelClient userLevelClient;
private final WalletAppConvertor walletAppConvertor; private final WalletAppConvertor walletAppConvertor;
@ -178,6 +195,133 @@ public class UserWalletServiceImpl implements UserWalletService {
); );
} }
@Override
public void exportGoldRunningWater(HttpServletResponse response, UserGoldRunningWaterBackQryCmd query)
throws IOException {
UserGoldRunningWaterBackQryCmd exportQuery = copyGoldRunningWaterQuery(query)
.setContext(null)
.setLastId(null)
.setLimit(EXPORT_BATCH_SIZE);
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
response.setCharacterEncoding(StandardCharsets.UTF_8.name());
String fileName = URLEncoder.encode("GmGoldRunningWater", StandardCharsets.UTF_8)
.replaceAll("\\+", "%20");
response.setHeader("Content-disposition", "attachment;filename*=utf-8''" + fileName + ".xlsx");
ExcelWriter excelWriter = null;
try {
excelWriter = EasyExcel.write(response.getOutputStream(), UserGoldRunningWaterExportModel.class)
.registerWriteHandler(EasyExcelUtils.getStyleStrategy())
.build();
WriteSheet sheet = EasyExcel.writerSheet("GmGoldRunningWater").build();
boolean wroteAnyData = false;
while (true) {
List<UserGoldRunningWaterCO> waters = listGoldRunningWater(exportQuery);
if (CollectionUtils.isEmpty(waters)) {
break;
}
excelWriter.write(toGoldRunningWaterExportModels(waters), sheet);
wroteAnyData = true;
Long nextLastId = getLastRunningWaterId(waters);
if (Objects.isNull(nextLastId) || Objects.equals(nextLastId, exportQuery.getLastId())) {
break;
}
exportQuery.setLastId(nextLastId);
}
if (!wroteAnyData) {
excelWriter.write(Lists.newArrayList(), sheet);
}
} finally {
if (Objects.nonNull(excelWriter)) {
excelWriter.finish();
}
}
}
private UserGoldRunningWaterBackQryCmd copyGoldRunningWaterQuery(
UserGoldRunningWaterBackQryCmd source) {
if (Objects.isNull(source)) {
return new UserGoldRunningWaterBackQryCmd();
}
return new UserGoldRunningWaterBackQryCmd()
.setId(source.getId())
.setSysOrigin(source.getSysOrigin())
.setUserId(source.getUserId())
.setQueryBackOperationUser(source.getQueryBackOperationUser())
.setBackOperationUser(source.getBackOperationUser())
.setBackOperationUsers(source.getBackOperationUsers())
.setTrackId(source.getTrackId())
.setType(source.getType())
.setOrigin(source.getOrigin())
.setStartTime(source.getStartTime())
.setEndTime(source.getEndTime());
}
private List<UserGoldRunningWaterExportModel> toGoldRunningWaterExportModels(
List<UserGoldRunningWaterCO> waters) {
return waters.stream()
.map(water -> {
UserGoldRunningWaterDTO runningWater = water.getRunningWater();
UserProfileDTO userProfile = water.getUserProfile();
return new UserGoldRunningWaterExportModel()
.setRecordId(Objects.toString(runningWater.getId(), ""))
.setSysOrigin(runningWater.getSysOrigin())
.setUserId(Objects.toString(runningWater.getUserId(), ""))
.setUserAccount(getAccountDescribe(userProfile))
.setUserNickname(Optional.ofNullable(userProfile)
.map(UserProfileDTO::getUserNickname).orElse(""))
.setType(getGoldRunningWaterTypeName(runningWater.getType()))
.setQuantity(runningWater.getQuantity())
.setBalance(runningWater.getBalance())
.setOriginName(runningWater.getOriginName())
.setRemark(runningWater.getRemark())
.setBackOperationName(StringUtils.isBlank(water.getBackOperationName())
? "-" : water.getBackOperationName())
.setCreateTime(formatExportTime(runningWater.getCreateTime()));
})
.toList();
}
private Long getLastRunningWaterId(List<UserGoldRunningWaterCO> waters) {
if (CollectionUtils.isEmpty(waters)) {
return null;
}
return Optional.ofNullable(waters.get(waters.size() - 1))
.map(UserGoldRunningWaterCO::getRunningWater)
.map(UserGoldRunningWaterDTO::getId)
.orElse(null);
}
private String getGoldRunningWaterTypeName(Integer type) {
if (Objects.equals(type, 0)) {
return "增加";
}
if (Objects.equals(type, 1)) {
return "减少";
}
return Objects.toString(type, "");
}
private String formatExportTime(Long time) {
if (Objects.isNull(time)) {
return "";
}
return EXPORT_TIME_FORMATTER.format(
Instant.ofEpochMilli(time).atZone(ZoneId.systemDefault()));
}
private String getAccountDescribe(UserProfileDTO userProfile) {
if (Objects.isNull(userProfile)) {
return "";
}
if (Objects.nonNull(userProfile.getOwnSpecialId())) {
return userProfile.getOwnSpecialId().getAccount() + "(" + userProfile.getAccount() + ")";
}
return userProfile.getAccount();
}
private UserGoldRunningWaterClsTmpCO getGoldRunningWaterDbFallback( private UserGoldRunningWaterClsTmpCO getGoldRunningWaterDbFallback(
UserGoldRunningWaterBackQryCmd query) { UserGoldRunningWaterBackQryCmd query) {
if (StringUtils.isNotBlank(query.getContext()) && Objects.isNull(query.getLastId())) { if (StringUtils.isNotBlank(query.getContext()) && Objects.isNull(query.getLastId())) {

View File

@ -30,6 +30,7 @@ import com.red.circle.tool.core.sequence.IdWorkerUtils;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.sql.Timestamp; import java.sql.Timestamp;
import java.time.Duration; import java.time.Duration;
import java.util.HashSet;
import java.util.Collection; import java.util.Collection;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
@ -96,7 +97,10 @@ public class GmVipGiftServiceImpl implements GmVipGiftService {
expireAt, now); expireAt, now);
vipOrderRecordService.save(order); vipOrderRecordService.save(order);
Set<Long> previousResourceIds = vipResourceIds(currentState);
upsertUserVipState(currentState, userProfile, config, startAt, expireAt, now); upsertUserVipState(currentState, userProfile, config, startAt, expireAt, now);
expireSupersededVipResources(userProfile.getOriginSys(), userProfile.getId(), config,
previousResourceIds, now);
giftVipResources(userProfile.getId(), config, now, cmd.getReqUserId()); giftVipResources(userProfile.getId(), config, now, cmd.getReqUserId());
GmVipGiftRecord record = buildGiftRecord(cmd, applicant, userProfile, fromLevel, config, GmVipGiftRecord record = buildGiftRecord(cmd, applicant, userProfile, fromLevel, config,
@ -272,6 +276,63 @@ public class GmVipGiftServiceImpl implements GmVipGiftService {
now, opUser); now, opUser);
} }
private void expireSupersededVipResources(String sysOrigin, Long userId,
VipLevelConfig targetConfig, Set<Long> previousResourceIds, Timestamp now) {
Set<Long> targetResourceIds = vipResourceIds(targetConfig);
Set<Long> allVipResourceIds = new HashSet<>();
vipLevelConfigService.query()
.eq(VipLevelConfig::getSysOrigin, sysOrigin)
.list()
.forEach(config -> allVipResourceIds.addAll(vipResourceIds(config)));
allVipResourceIds.addAll(previousResourceIds);
allVipResourceIds.removeAll(targetResourceIds);
if (CollectionUtils.isEmpty(allVipResourceIds)) {
return;
}
userPropsBackpackService.update()
.set(UserPropsBackpack::getExpireTime, now)
.set(UserPropsBackpack::getUseProps, Boolean.FALSE)
.set(UserPropsBackpack::getAllowGive, Boolean.FALSE)
.set(UserPropsBackpack::getUpdateTime, now)
.eq(UserPropsBackpack::getUserId, userId)
.in(UserPropsBackpack::getPropsId, allVipResourceIds)
.gt(UserPropsBackpack::getExpireTime, now)
.execute();
}
private Set<Long> vipResourceIds(VipLevelConfig config) {
Set<Long> ids = new HashSet<>();
if (Objects.isNull(config)) {
return ids;
}
addPositive(ids, config.getBadgeResourceId());
addPositive(ids, config.getAvatarFrameResourceId());
addPositive(ids, config.getEntryEffectResourceId());
addPositive(ids, config.getChatBubbleResourceId());
addPositive(ids, config.getFloatPictureResourceId());
return ids;
}
private Set<Long> vipResourceIds(UserVipState state) {
Set<Long> ids = new HashSet<>();
if (Objects.isNull(state)) {
return ids;
}
addPositive(ids, state.getBadgeResourceId());
addPositive(ids, state.getAvatarFrameResourceId());
addPositive(ids, state.getEntryEffectResourceId());
addPositive(ids, state.getChatBubbleResourceId());
addPositive(ids, state.getFloatPictureResourceId());
return ids;
}
private void addPositive(Set<Long> ids, Long id) {
if (Objects.nonNull(id) && id > 0) {
ids.add(id);
}
}
private void giveProp(Long userId, Long propsId, String type, Integer days, Timestamp now, private void giveProp(Long userId, Long propsId, String type, Integer days, Timestamp now,
Long opUser) { Long opUser) {
if (Objects.isNull(propsId) || propsId <= 0 || Objects.isNull(days) || days <= 0) { if (Objects.isNull(propsId) || propsId <= 0 || Objects.isNull(days) || days <= 0) {

View File

@ -0,0 +1,86 @@
package com.red.circle.console.app.service.app.activity;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import com.red.circle.console.app.dto.clienobject.app.activity.cp.ResidentCpConfigCO;
import com.red.circle.console.app.dto.clienobject.app.activity.cp.ResidentCpLevelConfigCO;
import com.red.circle.console.app.dto.cmd.app.activity.cp.ResidentCpConfigSaveCmd;
import com.red.circle.console.app.dto.cmd.app.activity.cp.ResidentCpLevelConfigSaveCmd;
import com.red.circle.console.app.service.app.sys.CpCabinConfigService;
import com.red.circle.framework.dto.PageResult;
import com.red.circle.other.inner.model.cmd.sys.SysCpCabinConfigQryCmd;
import com.red.circle.other.inner.model.dto.sys.SysCpCabinConfigDTO;
import java.util.List;
import org.junit.jupiter.api.Test;
class ResidentCpActivityServiceImplTest {
@Test
void getConfigShouldReturnCpLevelsFromZeroToFive() {
CpCabinConfigService cpCabinConfigService = mock(CpCabinConfigService.class);
ResidentCpActivityServiceImpl service = new ResidentCpActivityServiceImpl(
cpCabinConfigService);
PageResult<SysCpCabinConfigDTO> page = new PageResult<>();
page.setRecords(List.of(
config(100L, 1L, 100L),
config(101L, 2L, 200L),
config(102L, 3L, 300L),
config(103L, 4L, 400L),
config(104L, 5L, 500L)
));
when(cpCabinConfigService.getCpCabin(any(SysCpCabinConfigQryCmd.class))).thenReturn(page);
ResidentCpConfigCO result = service.getConfig("likei");
assertEquals("LIKEI", result.getSysOrigin());
assertEquals(List.of(0, 1, 2, 3, 4, 5),
result.getLevels().stream().map(ResidentCpLevelConfigCO::getLevel).toList());
assertEquals(100L, result.getLevels().get(0).getRequiredIntimacyValue());
assertEquals(100L, result.getLevels().get(0).getCabinConfigId());
assertEquals(0L, result.getLevels().get(5).getRequiredIntimacyValue());
}
@Test
void saveConfigShouldNotRequireExactLevelCount() {
CpCabinConfigService cpCabinConfigService = mock(CpCabinConfigService.class);
ResidentCpActivityServiceImpl service = new ResidentCpActivityServiceImpl(
cpCabinConfigService);
PageResult<SysCpCabinConfigDTO> page = new PageResult<>();
page.setRecords(List.of());
when(cpCabinConfigService.getCpCabin(any(SysCpCabinConfigQryCmd.class))).thenReturn(page);
ResidentCpConfigSaveCmd cmd = new ResidentCpConfigSaveCmd()
.setSysOrigin("likei")
.setLevels(List.of(
level(0, 0L),
level(1, 100L),
level(2, 200L),
level(3, 300L),
level(4, 400L)
));
assertDoesNotThrow(() -> service.saveConfig(cmd));
verify(cpCabinConfigService, times(5)).addCpCabin(any(SysCpCabinConfigDTO.class));
}
private static SysCpCabinConfigDTO config(Long id, Long sort, Long unlockValue) {
return new SysCpCabinConfigDTO()
.setId(id)
.setName("CP " + sort + "")
.setCabinUnlockValue(unlockValue)
.setSort(sort);
}
private static ResidentCpLevelConfigSaveCmd level(Integer level, Long requiredValue) {
return new ResidentCpLevelConfigSaveCmd()
.setLevel(level)
.setLevelName("CP " + level + "")
.setRequiredIntimacyValue(requiredValue);
}
}

View File

@ -0,0 +1,34 @@
package com.red.circle.console.app.dto.clienobject.app.activity.cp;
import com.red.circle.framework.dto.ClientObject;
import java.io.Serial;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
/**
* 常驻 CP 配置.
*/
@Data
@Accessors(chain = true)
@EqualsAndHashCode(callSuper = true)
public class ResidentCpConfigCO extends ClientObject {
@Serial
private static final long serialVersionUID = 1L;
private Boolean configured = Boolean.FALSE;
private String sysOrigin;
private List<ResidentCpLevelConfigCO> levels = new ArrayList<>();
/**
* Relation dismiss gold config. Key: CP/BROTHER/SISTERS.
*/
private Map<String, Long> dismissConsumeGolds = new LinkedHashMap<>();
}

View File

@ -0,0 +1,31 @@
package com.red.circle.console.app.dto.clienobject.app.activity.cp;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
import com.red.circle.framework.dto.ClientObject;
import java.io.Serial;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
/**
* 常驻 CP 等级配置.
*/
@Data
@Accessors(chain = true)
@EqualsAndHashCode(callSuper = true)
public class ResidentCpLevelConfigCO extends ClientObject {
@Serial
private static final long serialVersionUID = 1L;
private Integer level;
private String levelName;
@JsonSerialize(using = ToStringSerializer.class)
private Long requiredIntimacyValue;
@JsonSerialize(using = ToStringSerializer.class)
private Long cabinConfigId;
}

View File

@ -86,15 +86,20 @@ public class SysGiftConfigCO implements Serializable {
*/ */
private String type; private String type;
/** /**
* 礼物tab. * 礼物tab.
*/ */
private String giftTab; private String giftTab;
/** /**
* 0.未删除 1.已删除. * CP礼物关系类型CP/BROTHER/SISTERS.
*/ */
private Boolean del; private String cpRelationType;
/**
* 0.未删除 1.已删除.
*/
private Boolean del;
/** /**
* 幸运礼物规格ID. * 幸运礼物规格ID.

View File

@ -0,0 +1,57 @@
package com.red.circle.console.app.dto.clienobject.team;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
import com.red.circle.other.inner.model.dto.user.UserProfileDTO;
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 TeamManualSalaryCO implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
private String id;
private String sysOrigin;
private String countryCode;
private String region;
@JsonSerialize(using = ToStringSerializer.class)
private Long teamId;
@JsonSerialize(using = ToStringSerializer.class)
private Long teamOwnId;
@JsonSerialize(using = ToStringSerializer.class)
private Long userId;
private UserProfileDTO userProfile;
private Integer billBelong;
private Long lastSettlementTarget;
private Long currentTarget;
private Integer policyLevel;
private Boolean satisfyLevel;
private Boolean agentMember;
private BigDecimal expectedSalary;
private BigDecimal expectedMemberSalary;
private BigDecimal expectedAgentSalary;
private BigDecimal payableSalary;
private BigDecimal payableMemberSalary;
private BigDecimal payableAgentSalary;
private BigDecimal totalIssuedSalary;
private BigDecimal memberIssuedSalary;
private BigDecimal agentIssuedSalary;
private BigDecimal overIssuedSalary;
private BigDecimal memberOverIssuedSalary;
private BigDecimal agentOverIssuedSalary;
private BigDecimal currentSalaryBalance;
private Boolean paid;
}

View File

@ -0,0 +1,43 @@
package com.red.circle.console.app.dto.clienobject.team;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
import com.red.circle.other.inner.model.dto.user.UserProfileDTO;
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 TeamManualSalaryResultCO implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
private String id;
private String sysOrigin;
private String countryCode;
private String region;
@JsonSerialize(using = ToStringSerializer.class)
private Long teamId;
@JsonSerialize(using = ToStringSerializer.class)
private Long teamOwnId;
@JsonSerialize(using = ToStringSerializer.class)
private Long userId;
private UserProfileDTO userProfile;
private Integer billBelong;
private BigDecimal beforeSalaryBalance;
private BigDecimal afterSalaryBalance;
private BigDecimal issuedSalary;
private Long currentTarget;
private Integer policyLevel;
}

View File

@ -38,6 +38,16 @@ public class TeamManagerCO extends ClientObject {
private String regionName; private String regionName;
private Boolean canAddBdLeader;
private Boolean canGiftProps;
private Boolean canBanUser;
private Boolean canUnbanUser;
private Boolean canChangeCountry;
private Timestamp createTime; private Timestamp createTime;
private Timestamp updateTime; private Timestamp updateTime;

View File

@ -0,0 +1,23 @@
package com.red.circle.console.app.dto.clientobject.datav;
import java.io.Serial;
import java.io.Serializable;
import lombok.Data;
import lombok.experimental.Accessors;
/**
* 国家数据大屏 ARPU 汇总.
*/
@Data
@Accessors(chain = true)
public class CountryDashboardArpuSummaryCO implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
private CountryDashboardCO day;
private CountryDashboardCO threeDay;
private CountryDashboardCO week;
}

View File

@ -29,4 +29,6 @@ public class CountryDashboardCO implements Serializable {
private CountryDashboardMetricCO total; private CountryDashboardMetricCO total;
private List<CountryDashboardMetricCO> records; private List<CountryDashboardMetricCO> records;
private CountryDashboardArpuSummaryCO arpuSummary;
} }

View File

@ -28,10 +28,16 @@ public class CountryDashboardMetricCO implements Serializable {
private Long dailyActiveUser = 0L; private Long dailyActiveUser = 0L;
private Long dailyRechargeUser = 0L;
private BigDecimal newUserRecharge = BigDecimal.ZERO; private BigDecimal newUserRecharge = BigDecimal.ZERO;
private BigDecimal dealerRecharge = BigDecimal.ZERO; private BigDecimal dealerRecharge = BigDecimal.ZERO;
private BigDecimal userRecharge = BigDecimal.ZERO;
private BigDecimal newDealerUserRecharge = BigDecimal.ZERO;
private BigDecimal mifapayRecharge = BigDecimal.ZERO; private BigDecimal mifapayRecharge = BigDecimal.ZERO;
private BigDecimal googleRecharge = BigDecimal.ZERO; private BigDecimal googleRecharge = BigDecimal.ZERO;

View File

@ -0,0 +1,34 @@
package com.red.circle.console.app.dto.cmd.app.activity.cp;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotEmpty;
import java.io.Serial;
import java.io.Serializable;
import java.util.List;
import java.util.Map;
import lombok.Data;
import lombok.experimental.Accessors;
/**
* 常驻 CP 配置保存命令.
*/
@Data
@Accessors(chain = true)
public class ResidentCpConfigSaveCmd implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
@NotBlank(message = "sysOrigin required.")
private String sysOrigin;
@Valid
@NotEmpty(message = "levels required.")
private List<ResidentCpLevelConfigSaveCmd> levels;
/**
* Relation dismiss gold config. Key: CP/BROTHER/SISTERS.
*/
private Map<String, Long> dismissConsumeGolds;
}

View File

@ -0,0 +1,32 @@
package com.red.circle.console.app.dto.cmd.app.activity.cp;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
import jakarta.validation.constraints.NotNull;
import java.io.Serial;
import java.io.Serializable;
import lombok.Data;
import lombok.experimental.Accessors;
/**
* 常驻 CP 等级配置保存命令.
*/
@Data
@Accessors(chain = true)
public class ResidentCpLevelConfigSaveCmd implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
@NotNull(message = "level required.")
private Integer level;
private String levelName;
@NotNull(message = "requiredIntimacyValue required.")
@JsonSerialize(using = ToStringSerializer.class)
private Long requiredIntimacyValue;
@JsonSerialize(using = ToStringSerializer.class)
private Long cabinConfigId;
}

View File

@ -31,4 +31,14 @@ public class TeamManagerAddCmd extends Command {
@NotBlank(message = "regionId required.") @NotBlank(message = "regionId required.")
private String regionId; private String regionId;
private Boolean canAddBdLeader;
private Boolean canGiftProps;
private Boolean canBanUser;
private Boolean canUnbanUser;
private Boolean canChangeCountry;
} }

View File

@ -29,4 +29,14 @@ public class TeamManagerUpdateCmd extends Command {
@NotBlank(message = "regionId required.") @NotBlank(message = "regionId required.")
private String regionId; private String regionId;
private Boolean canAddBdLeader;
private Boolean canGiftProps;
private Boolean canBanUser;
private Boolean canUnbanUser;
private Boolean canChangeCountry;
} }

View File

@ -0,0 +1,14 @@
package com.red.circle.console.app.service.app.activity;
import com.red.circle.console.app.dto.clienobject.app.activity.cp.ResidentCpConfigCO;
import com.red.circle.console.app.dto.cmd.app.activity.cp.ResidentCpConfigSaveCmd;
/**
* 常驻 CP 配置管理.
*/
public interface ResidentCpActivityService {
ResidentCpConfigCO getConfig(String sysOrigin);
void saveConfig(ResidentCpConfigSaveCmd cmd);
}

View File

@ -1,9 +1,11 @@
package com.red.circle.console.app.service.app.order; package com.red.circle.console.app.service.app.order;
import com.red.circle.console.app.dto.clienobject.order.InAppPurchaseDetailsCO; import com.red.circle.console.app.dto.clienobject.order.InAppPurchaseDetailsCO;
import com.red.circle.order.inner.model.cmd.inapp.InAppPurchaseDetailsQryCmd; import com.red.circle.order.inner.model.cmd.inapp.InAppPurchaseDetailsQryCmd;
import java.util.List; import jakarta.servlet.http.HttpServletResponse;
import java.util.Map; import java.io.IOException;
import java.util.List;
import java.util.Map;
/** /**
* 内购服务. * 内购服务.
@ -25,6 +27,12 @@ public interface InAppPurchaseBackService extends IOrderService {
/** /**
* 订单统计查询 - 返回金额总数和订单总数 * 订单统计查询 - 返回金额总数和订单总数
*/ */
Map<String, Object> statistics(InAppPurchaseDetailsQryCmd query); Map<String, Object> statistics(InAppPurchaseDetailsQryCmd query);
} /**
* 按筛选条件导出订单详情不使用分页条件.
*/
void exportDetails(HttpServletResponse response, InAppPurchaseDetailsQryCmd query)
throws IOException;
}

View File

@ -18,9 +18,11 @@ public interface SysEnumConfigService {
List<EnumConfigDTO> sysEnumConfigList(String groupName); List<EnumConfigDTO> sysEnumConfigList(String groupName);
String getViolationAvatar(EnumConfigKey violationPicture); String getViolationAvatar(EnumConfigKey violationPicture);
void saveSysEnumConfig(EnumConfigDTO sysEnumConfig); EnumConfigDTO getByCode(String name, String sysOrigin);
void saveSysEnumConfig(EnumConfigDTO sysEnumConfig);
void updateSysEnumConfig(EnumConfigDTO sysEnumConfig); void updateSysEnumConfig(EnumConfigDTO sysEnumConfig);

View File

@ -1,9 +1,11 @@
package com.red.circle.console.app.service.app.sys.pay; package com.red.circle.console.app.service.app.sys.pay;
import com.red.circle.framework.dto.PageResult; import com.red.circle.framework.dto.PageResult;
import com.red.circle.order.inner.model.cmd.SysPayCountryAddCmd; import com.red.circle.order.inner.model.cmd.SysPayCountryAddCmd;
import com.red.circle.order.inner.model.cmd.SysPayCountryQryCmd; import com.red.circle.order.inner.model.cmd.SysPayCountryCurrencyRateSyncCmd;
import com.red.circle.order.inner.model.dto.PayCountryDTO; import com.red.circle.order.inner.model.cmd.SysPayCountryQryCmd;
import com.red.circle.order.inner.model.dto.PayCountryCurrencyRateSyncDTO;
import com.red.circle.order.inner.model.dto.PayCountryDTO;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
@ -57,12 +59,17 @@ public interface SysPayCountryService {
/** /**
* 修改国家上下架状态. * 修改国家上下架状态.
*/ */
void shelf(Long id, Boolean shelf); void shelf(Long id, Boolean shelf);
/** /**
* 根据id集合获得. * 同步支付国家默认货币和实时美元汇率.
*/ */
Map<Long, PayCountryDTO> mapCountryByIds(Set<Long> ids); PayCountryCurrencyRateSyncDTO syncCurrencyRates(SysPayCountryCurrencyRateSyncCmd cmd);
/**
* 根据id集合获得.
*/
Map<Long, PayCountryDTO> mapCountryByIds(Set<Long> ids);
/** /**
* 根据区域id集合获得支持支付的国家. * 根据区域id集合获得支持支付的国家.

View File

@ -1,9 +1,14 @@
package com.red.circle.console.app.service.app.team; package com.red.circle.console.app.service.app.team;
import com.red.circle.console.app.dto.clienobject.team.TeamSalaryCO; import com.red.circle.console.app.dto.clienobject.team.TeamSalaryCO;
import com.red.circle.framework.dto.PageResult; import com.red.circle.console.app.dto.clienobject.team.TeamManualSalaryCO;
import com.red.circle.other.inner.model.cmd.team.TeamSalaryBackQryCmd; import com.red.circle.console.app.dto.clienobject.team.TeamManualSalaryResultCO;
import com.red.circle.framework.dto.PageResult;
import com.red.circle.other.inner.model.cmd.team.TeamManualSalaryPaymentCmd;
import com.red.circle.other.inner.model.cmd.team.TeamManualSalaryPaymentQryCmd;
import com.red.circle.other.inner.model.cmd.team.TeamSalaryBackQryCmd;
import java.util.List;
/** /**
* 团队自动发送工资凭证. * 团队自动发送工资凭证.
@ -15,6 +20,16 @@ public interface TeamSalaryBackService {
/** /**
* 根据条件查询列表. * 根据条件查询列表.
*/ */
PageResult<TeamSalaryCO> listByCondition(TeamSalaryBackQryCmd query); PageResult<TeamSalaryCO> listByCondition(TeamSalaryBackQryCmd query);
} /**
* 手动工资发放预览.
*/
PageResult<TeamManualSalaryCO> manualSalaryPage(TeamManualSalaryPaymentQryCmd query);
/**
* 确认手动发放工资.
*/
List<TeamManualSalaryResultCO> manualSalaryPay(TeamManualSalaryPaymentCmd cmd);
}

View File

@ -4,11 +4,13 @@ import com.red.circle.console.app.dto.clienobject.user.UserGoldBalanceCO;
import com.red.circle.console.app.dto.clienobject.user.UserGoldRunningWaterCO; import com.red.circle.console.app.dto.clienobject.user.UserGoldRunningWaterCO;
import com.red.circle.console.app.dto.clienobject.user.UserGoldRunningWaterClsTmpCO; import com.red.circle.console.app.dto.clienobject.user.UserGoldRunningWaterClsTmpCO;
import com.red.circle.wallet.inner.model.cmd.DeductGoldCmd; import com.red.circle.wallet.inner.model.cmd.DeductGoldCmd;
import com.red.circle.wallet.inner.model.cmd.DiamondCmd; import com.red.circle.wallet.inner.model.cmd.DiamondCmd;
import com.red.circle.wallet.inner.model.cmd.SendGoldCmd; import com.red.circle.wallet.inner.model.cmd.SendGoldCmd;
import com.red.circle.wallet.inner.model.cmd.UserGoldRunningWaterBackQryCmd; import com.red.circle.wallet.inner.model.cmd.UserGoldRunningWaterBackQryCmd;
import java.math.BigDecimal; import jakarta.servlet.http.HttpServletResponse;
import java.util.List; import java.io.IOException;
import java.math.BigDecimal;
import java.util.List;
/** /**
* 用户钱包 服务. * 用户钱包 服务.
@ -29,15 +31,21 @@ public interface UserWalletService {
List<UserGoldRunningWaterCO> listGoldRunningWater(UserGoldRunningWaterBackQryCmd query); List<UserGoldRunningWaterCO> listGoldRunningWater(UserGoldRunningWaterBackQryCmd query);
/** /**
* 查询用户金币流水 cls. * 查询用户金币流水 cls.
*/ */
UserGoldRunningWaterClsTmpCO getGoldRunningWaterCls(UserGoldRunningWaterBackQryCmd query); UserGoldRunningWaterClsTmpCO getGoldRunningWaterCls(UserGoldRunningWaterBackQryCmd query);
/** /**
* 发送金币. * 导出用户金币流水.
*/ */
void sendGold(SendGoldCmd param); void exportGoldRunningWater(HttpServletResponse response, UserGoldRunningWaterBackQryCmd query)
throws IOException;
/**
* 发送金币.
*/
void sendGold(SendGoldCmd param);
/** /**
* 扣除金币. * 扣除金币.

View File

@ -30,8 +30,20 @@ public interface CountryDashboardDAO extends BaseDAO<IUserBaseInfo> {
int createGamePeriodUserMetricTable(); int createGamePeriodUserMetricTable();
int createRechargeDetailTable();
int ensurePeriodMetricTimezoneSchema(); int ensurePeriodMetricTimezoneSchema();
int ensurePeriodMetricUserColumnsSchema();
int ensureMetricValueColumnsSchema();
Integer getDashboardBackfillLock(
@Param("lockName") String lockName,
@Param("timeoutSeconds") Integer timeoutSeconds);
Integer releaseDashboardBackfillLock(@Param("lockName") String lockName);
List<CountryDashboardMetricDTO> listNewUsers( List<CountryDashboardMetricDTO> listNewUsers(
@Param("periodType") String periodType, @Param("periodType") String periodType,
@Param("startTime") LocalDateTime startTime, @Param("startTime") LocalDateTime startTime,
@ -179,6 +191,9 @@ public interface CountryDashboardDAO extends BaseDAO<IUserBaseInfo> {
int upsertPeriodMetrics(@Param("metrics") List<CountryDashboardPeriodMetricDTO> metrics); int upsertPeriodMetrics(@Param("metrics") List<CountryDashboardPeriodMetricDTO> metrics);
int upsertPeriodActiveRetentionMetrics(
@Param("metrics") List<CountryDashboardPeriodMetricDTO> metrics);
int deletePeriodUserMetrics( int deletePeriodUserMetrics(
@Param("periodType") String periodType, @Param("periodType") String periodType,
@Param("periodKey") String periodKey, @Param("periodKey") String periodKey,

View File

@ -24,6 +24,8 @@ public class CountryDashboardDailyMetricDTO {
private Long countryNewUser; private Long countryNewUser;
private Long dailyRechargeUser;
private BigDecimal newUserRecharge; private BigDecimal newUserRecharge;
private BigDecimal officialRecharge; private BigDecimal officialRecharge;
@ -34,14 +36,24 @@ public class CountryDashboardDailyMetricDTO {
private BigDecimal dealerRecharge; private BigDecimal dealerRecharge;
private BigDecimal userRecharge;
private BigDecimal newDealerUserRecharge;
private BigDecimal salaryExchange; private BigDecimal salaryExchange;
private BigDecimal salaryTransfer;
private BigDecimal giftConsume;
private BigDecimal luckyGiftTotalFlow; private BigDecimal luckyGiftTotalFlow;
private Long luckyGiftUser; private Long luckyGiftUser;
private BigDecimal luckyGiftPayout; private BigDecimal luckyGiftPayout;
private BigDecimal luckyGiftAnchorShare;
private BigDecimal gameTotalFlow; private BigDecimal gameTotalFlow;
private Long gameUser; private Long gameUser;

View File

@ -21,6 +21,10 @@ public class CountryDashboardMetricDTO {
private Long countryNewUser; private Long countryNewUser;
private Long dailyActiveUser;
private Long dailyRechargeUser;
private BigDecimal newUserRecharge; private BigDecimal newUserRecharge;
private BigDecimal officialRecharge; private BigDecimal officialRecharge;
@ -31,17 +35,39 @@ public class CountryDashboardMetricDTO {
private BigDecimal dealerRecharge; private BigDecimal dealerRecharge;
private BigDecimal userRecharge;
private BigDecimal newDealerUserRecharge;
private BigDecimal salaryExchange; private BigDecimal salaryExchange;
private BigDecimal salaryTransfer;
private BigDecimal giftConsume;
private BigDecimal luckyGiftTotalFlow; private BigDecimal luckyGiftTotalFlow;
private Long luckyGiftUser; private Long luckyGiftUser;
private BigDecimal luckyGiftPayout; private BigDecimal luckyGiftPayout;
private BigDecimal luckyGiftAnchorShare;
private BigDecimal gameTotalFlow; private BigDecimal gameTotalFlow;
private Long gameUser; private Long gameUser;
private BigDecimal gamePayout; private BigDecimal gamePayout;
private Long d1RetentionUser;
private Long d1RetentionBaseUser;
private Long d7RetentionUser;
private Long d7RetentionBaseUser;
private Long d30RetentionUser;
private Long d30RetentionBaseUser;
} }

View File

@ -32,6 +32,10 @@ public class CountryDashboardPeriodMetricDTO {
private Long countryNewUser; private Long countryNewUser;
private Long dailyActiveUser;
private Long dailyRechargeUser;
private BigDecimal newUserRecharge; private BigDecimal newUserRecharge;
private BigDecimal officialRecharge; private BigDecimal officialRecharge;
@ -42,17 +46,39 @@ public class CountryDashboardPeriodMetricDTO {
private BigDecimal dealerRecharge; private BigDecimal dealerRecharge;
private BigDecimal userRecharge;
private BigDecimal newDealerUserRecharge;
private BigDecimal salaryExchange; private BigDecimal salaryExchange;
private BigDecimal salaryTransfer;
private BigDecimal giftConsume;
private BigDecimal luckyGiftTotalFlow; private BigDecimal luckyGiftTotalFlow;
private Long luckyGiftUser; private Long luckyGiftUser;
private BigDecimal luckyGiftPayout; private BigDecimal luckyGiftPayout;
private BigDecimal luckyGiftAnchorShare;
private BigDecimal gameTotalFlow; private BigDecimal gameTotalFlow;
private Long gameUser; private Long gameUser;
private BigDecimal gamePayout; private BigDecimal gamePayout;
private Long d1RetentionUser;
private Long d1RetentionBaseUser;
private Long d7RetentionUser;
private Long d7RetentionBaseUser;
private Long d30RetentionUser;
private Long d30RetentionBaseUser;
} }

View File

@ -3,6 +3,14 @@
"http://mybatis.org/dtd/mybatis-3-mapper.dtd"> "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.red.circle.console.infra.database.rds.dao.datav.CountryDashboardDAO"> <mapper namespace="com.red.circle.console.infra.database.rds.dao.datav.CountryDashboardDAO">
<select id="getDashboardBackfillLock" resultType="java.lang.Integer">
SELECT GET_LOCK(#{lockName}, #{timeoutSeconds})
</select>
<select id="releaseDashboardBackfillLock" resultType="java.lang.Integer">
SELECT RELEASE_LOCK(#{lockName})
</select>
<update id="createDailyMetricTable"> <update id="createDailyMetricTable">
CREATE TABLE IF NOT EXISTS `country_dashboard_daily_metric` ( CREATE TABLE IF NOT EXISTS `country_dashboard_daily_metric` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键', `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键',
@ -12,15 +20,21 @@
`country_code` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'UNKNOWN' 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 '国家名称', `country_name` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'UNKNOWN' COMMENT '国家名称',
`country_new_user` bigint NOT NULL DEFAULT 0 COMMENT '新增用户数', `country_new_user` bigint NOT NULL DEFAULT 0 COMMENT '新增用户数',
`daily_recharge_user` bigint NOT NULL DEFAULT 0 COMMENT '当日充值用户数',
`new_user_recharge` decimal(20,2) NOT NULL DEFAULT 0.00 COMMENT '新增用户充值', `new_user_recharge` decimal(20,2) NOT NULL DEFAULT 0.00 COMMENT '新增用户充值',
`official_recharge` decimal(20,2) NOT NULL DEFAULT 0.00 COMMENT '官方充值', `official_recharge` decimal(20,2) NOT NULL DEFAULT 0.00 COMMENT '官方充值',
`mifapay_recharge` decimal(20,2) NOT NULL DEFAULT 0.00 COMMENT 'MifaPay充值', `mifapay_recharge` decimal(20,2) NOT NULL DEFAULT 0.00 COMMENT 'MifaPay充值',
`google_recharge` decimal(20,2) NOT NULL DEFAULT 0.00 COMMENT 'Google充值', `google_recharge` decimal(20,2) NOT NULL DEFAULT 0.00 COMMENT 'Google充值',
`dealer_recharge` decimal(20,2) NOT NULL DEFAULT 0.00 COMMENT '代理充值', `dealer_recharge` decimal(20,2) NOT NULL DEFAULT 0.00 COMMENT '代理充值',
`user_recharge` decimal(20,2) NOT NULL DEFAULT 0.00 COMMENT '用户充值',
`new_dealer_user_recharge` decimal(20,2) NOT NULL DEFAULT 0.00 COMMENT '新增币商用户充值',
`salary_exchange` decimal(20,2) NOT NULL DEFAULT 0.00 COMMENT '工资兑换', `salary_exchange` decimal(20,2) NOT NULL DEFAULT 0.00 COMMENT '工资兑换',
`salary_transfer` decimal(20,2) NOT NULL DEFAULT 0.00 COMMENT '工资转移',
`gift_consume` decimal(20,2) NOT NULL DEFAULT 0.00 COMMENT '礼物消耗',
`lucky_gift_total_flow` decimal(20,2) NOT NULL DEFAULT 0.00 COMMENT '幸运礼物流水', `lucky_gift_total_flow` decimal(20,2) NOT NULL DEFAULT 0.00 COMMENT '幸运礼物流水',
`lucky_gift_user` bigint NOT NULL DEFAULT 0 COMMENT '幸运礼物用户数', `lucky_gift_user` bigint NOT NULL DEFAULT 0 COMMENT '幸运礼物用户数',
`lucky_gift_payout` decimal(20,2) NOT NULL DEFAULT 0.00 COMMENT '幸运礼物返奖', `lucky_gift_payout` decimal(20,2) NOT NULL DEFAULT 0.00 COMMENT '幸运礼物返奖',
`lucky_gift_anchor_share` decimal(20,2) NOT NULL DEFAULT 0.00 COMMENT '幸运礼物主播分成',
`game_total_flow` decimal(20,2) NOT NULL DEFAULT 0.00 COMMENT '游戏流水', `game_total_flow` decimal(20,2) NOT NULL DEFAULT 0.00 COMMENT '游戏流水',
`game_user` bigint NOT NULL DEFAULT 0 COMMENT '游戏用户数', `game_user` bigint NOT NULL DEFAULT 0 COMMENT '游戏用户数',
`game_payout` decimal(20,2) NOT NULL DEFAULT 0.00 COMMENT '游戏返奖', `game_payout` decimal(20,2) NOT NULL DEFAULT 0.00 COMMENT '游戏返奖',
@ -47,18 +61,31 @@
`country_code` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'UNKNOWN' 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 '国家名称', `country_name` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'UNKNOWN' COMMENT '国家名称',
`country_new_user` bigint NOT NULL DEFAULT 0 COMMENT '新增用户数', `country_new_user` bigint NOT NULL DEFAULT 0 COMMENT '新增用户数',
`daily_active_user` bigint NOT NULL DEFAULT 0 COMMENT '当日活跃用户数',
`daily_recharge_user` bigint NOT NULL DEFAULT 0 COMMENT '当日充值用户数',
`new_user_recharge` decimal(20,2) NOT NULL DEFAULT 0.00 COMMENT '新增用户充值', `new_user_recharge` decimal(20,2) NOT NULL DEFAULT 0.00 COMMENT '新增用户充值',
`official_recharge` decimal(20,2) NOT NULL DEFAULT 0.00 COMMENT '官方充值', `official_recharge` decimal(20,2) NOT NULL DEFAULT 0.00 COMMENT '官方充值',
`mifapay_recharge` decimal(20,2) NOT NULL DEFAULT 0.00 COMMENT 'MifaPay充值', `mifapay_recharge` decimal(20,2) NOT NULL DEFAULT 0.00 COMMENT 'MifaPay充值',
`google_recharge` decimal(20,2) NOT NULL DEFAULT 0.00 COMMENT 'Google充值', `google_recharge` decimal(20,2) NOT NULL DEFAULT 0.00 COMMENT 'Google充值',
`dealer_recharge` decimal(20,2) NOT NULL DEFAULT 0.00 COMMENT '代理充值', `dealer_recharge` decimal(20,2) NOT NULL DEFAULT 0.00 COMMENT '代理充值',
`user_recharge` decimal(20,2) NOT NULL DEFAULT 0.00 COMMENT '用户充值',
`new_dealer_user_recharge` decimal(20,2) NOT NULL DEFAULT 0.00 COMMENT '新增币商用户充值',
`salary_exchange` decimal(20,2) NOT NULL DEFAULT 0.00 COMMENT '工资兑换', `salary_exchange` decimal(20,2) NOT NULL DEFAULT 0.00 COMMENT '工资兑换',
`salary_transfer` decimal(20,2) NOT NULL DEFAULT 0.00 COMMENT '工资转移',
`gift_consume` decimal(20,2) NOT NULL DEFAULT 0.00 COMMENT '礼物消耗',
`lucky_gift_total_flow` decimal(20,2) NOT NULL DEFAULT 0.00 COMMENT '幸运礼物流水', `lucky_gift_total_flow` decimal(20,2) NOT NULL DEFAULT 0.00 COMMENT '幸运礼物流水',
`lucky_gift_user` bigint NOT NULL DEFAULT 0 COMMENT '幸运礼物用户数', `lucky_gift_user` bigint NOT NULL DEFAULT 0 COMMENT '幸运礼物用户数',
`lucky_gift_payout` decimal(20,2) NOT NULL DEFAULT 0.00 COMMENT '幸运礼物返奖', `lucky_gift_payout` decimal(20,2) NOT NULL DEFAULT 0.00 COMMENT '幸运礼物返奖',
`lucky_gift_anchor_share` decimal(20,2) NOT NULL DEFAULT 0.00 COMMENT '幸运礼物主播分成',
`game_total_flow` decimal(20,2) NOT NULL DEFAULT 0.00 COMMENT '游戏流水', `game_total_flow` decimal(20,2) NOT NULL DEFAULT 0.00 COMMENT '游戏流水',
`game_user` bigint NOT NULL DEFAULT 0 COMMENT '游戏用户数', `game_user` bigint NOT NULL DEFAULT 0 COMMENT '游戏用户数',
`game_payout` decimal(20,2) NOT NULL DEFAULT 0.00 COMMENT '游戏返奖', `game_payout` decimal(20,2) NOT NULL DEFAULT 0.00 COMMENT '游戏返奖',
`d1_retention_user` bigint NOT NULL DEFAULT 0 COMMENT '次日留存用户数',
`d1_retention_base_user` bigint NOT NULL DEFAULT 0 COMMENT '次日留存基数',
`d7_retention_user` bigint NOT NULL DEFAULT 0 COMMENT '7日留存用户数',
`d7_retention_base_user` bigint NOT NULL DEFAULT 0 COMMENT '7日留存基数',
`d30_retention_user` bigint NOT NULL DEFAULT 0 COMMENT '30日留存用户数',
`d30_retention_base_user` bigint NOT NULL DEFAULT 0 COMMENT '30日留存基数',
`refreshed_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '最近刷新时间', `refreshed_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '最近刷新时间',
`create_time` 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 '更新时间', `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
@ -181,6 +208,28 @@
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='国家数据大屏游戏维度周期用户去重明细' ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='国家数据大屏游戏维度周期用户去重明细'
</update> </update>
<update id="createRechargeDetailTable">
CREATE TABLE IF NOT EXISTS `country_dashboard_recharge_detail` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键',
`source_type` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '充值来源',
`record_id` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '来源记录ID',
`source_channel` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '来源渠道',
`sys_origin` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '系统来源',
`user_id` bigint NOT NULL COMMENT '用户ID',
`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 '国家名称',
`amount` decimal(20,2) NOT NULL DEFAULT 0.00 COMMENT 'USD金额',
`coin_quantity` decimal(20,2) DEFAULT NULL COMMENT '金币数量',
`remark` varchar(512) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '备注',
`create_time` datetime NOT NULL COMMENT '业务创建时间',
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_recharge_source_record` (`source_type`, `record_id`),
KEY `idx_recharge_query` (`sys_origin`, `create_time`, `country_code`),
KEY `idx_recharge_user` (`user_id`, `create_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='国家数据大屏充值明细预聚合'
</update>
<update id="ensurePeriodMetricTimezoneSchema"> <update id="ensurePeriodMetricTimezoneSchema">
SET @country_dashboard_schema_lock = GET_LOCK('country_dashboard_period_timezone_schema', 60); SET @country_dashboard_schema_lock = GET_LOCK('country_dashboard_period_timezone_schema', 60);
SET @current_schema = DATABASE(); SET @current_schema = DATABASE();
@ -424,6 +473,305 @@
DO IF(@country_dashboard_schema_lock = 1, RELEASE_LOCK('country_dashboard_period_timezone_schema'), 0); DO IF(@country_dashboard_schema_lock = 1, RELEASE_LOCK('country_dashboard_period_timezone_schema'), 0);
</update> </update>
<update id="ensurePeriodMetricUserColumnsSchema">
SET @country_dashboard_user_columns_schema_lock = GET_LOCK('country_dashboard_period_user_columns_schema', 60);
SET @current_schema = DATABASE();
SET @ddl = IF(
@country_dashboard_user_columns_schema_lock = 1
AND NOT EXISTS (
SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_SCHEMA = @current_schema
AND TABLE_NAME = 'country_dashboard_period_metric'
AND COLUMN_NAME = 'daily_active_user'
),
'ALTER TABLE `country_dashboard_period_metric` ADD COLUMN `daily_active_user` bigint NOT NULL DEFAULT 0 COMMENT ''当日活跃用户数'' AFTER `country_new_user`',
'SELECT 1'
);
PREPARE stmt FROM @ddl;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
SET @ddl = IF(
@country_dashboard_user_columns_schema_lock = 1
AND NOT EXISTS (
SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_SCHEMA = @current_schema
AND TABLE_NAME = 'country_dashboard_period_metric'
AND COLUMN_NAME = 'daily_recharge_user'
),
'ALTER TABLE `country_dashboard_period_metric` ADD COLUMN `daily_recharge_user` bigint NOT NULL DEFAULT 0 COMMENT ''当日充值用户数'' AFTER `daily_active_user`',
'SELECT 1'
);
PREPARE stmt FROM @ddl;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
SET @ddl = IF(
@country_dashboard_user_columns_schema_lock = 1
AND NOT EXISTS (
SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_SCHEMA = @current_schema
AND TABLE_NAME = 'country_dashboard_period_metric'
AND COLUMN_NAME = 'd1_retention_user'
),
'ALTER TABLE `country_dashboard_period_metric` ADD COLUMN `d1_retention_user` bigint NOT NULL DEFAULT 0 COMMENT ''次日留存用户数'' AFTER `game_payout`',
'SELECT 1'
);
PREPARE stmt FROM @ddl;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
SET @ddl = IF(
@country_dashboard_user_columns_schema_lock = 1
AND NOT EXISTS (
SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_SCHEMA = @current_schema
AND TABLE_NAME = 'country_dashboard_period_metric'
AND COLUMN_NAME = 'd1_retention_base_user'
),
'ALTER TABLE `country_dashboard_period_metric` ADD COLUMN `d1_retention_base_user` bigint NOT NULL DEFAULT 0 COMMENT ''次日留存基数'' AFTER `d1_retention_user`',
'SELECT 1'
);
PREPARE stmt FROM @ddl;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
SET @ddl = IF(
@country_dashboard_user_columns_schema_lock = 1
AND NOT EXISTS (
SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_SCHEMA = @current_schema
AND TABLE_NAME = 'country_dashboard_period_metric'
AND COLUMN_NAME = 'd7_retention_user'
),
'ALTER TABLE `country_dashboard_period_metric` ADD COLUMN `d7_retention_user` bigint NOT NULL DEFAULT 0 COMMENT ''7日留存用户数'' AFTER `d1_retention_base_user`',
'SELECT 1'
);
PREPARE stmt FROM @ddl;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
SET @ddl = IF(
@country_dashboard_user_columns_schema_lock = 1
AND NOT EXISTS (
SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_SCHEMA = @current_schema
AND TABLE_NAME = 'country_dashboard_period_metric'
AND COLUMN_NAME = 'd7_retention_base_user'
),
'ALTER TABLE `country_dashboard_period_metric` ADD COLUMN `d7_retention_base_user` bigint NOT NULL DEFAULT 0 COMMENT ''7日留存基数'' AFTER `d7_retention_user`',
'SELECT 1'
);
PREPARE stmt FROM @ddl;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
SET @ddl = IF(
@country_dashboard_user_columns_schema_lock = 1
AND NOT EXISTS (
SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_SCHEMA = @current_schema
AND TABLE_NAME = 'country_dashboard_period_metric'
AND COLUMN_NAME = 'd30_retention_user'
),
'ALTER TABLE `country_dashboard_period_metric` ADD COLUMN `d30_retention_user` bigint NOT NULL DEFAULT 0 COMMENT ''30日留存用户数'' AFTER `d7_retention_base_user`',
'SELECT 1'
);
PREPARE stmt FROM @ddl;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
SET @ddl = IF(
@country_dashboard_user_columns_schema_lock = 1
AND NOT EXISTS (
SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_SCHEMA = @current_schema
AND TABLE_NAME = 'country_dashboard_period_metric'
AND COLUMN_NAME = 'd30_retention_base_user'
),
'ALTER TABLE `country_dashboard_period_metric` ADD COLUMN `d30_retention_base_user` bigint NOT NULL DEFAULT 0 COMMENT ''30日留存基数'' AFTER `d30_retention_user`',
'SELECT 1'
);
PREPARE stmt FROM @ddl;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
DO IF(@country_dashboard_user_columns_schema_lock = 1, RELEASE_LOCK('country_dashboard_period_user_columns_schema'), 0);
</update>
<update id="ensureMetricValueColumnsSchema">
SET @country_dashboard_value_columns_schema_lock = GET_LOCK('country_dashboard_value_columns_schema', 60);
SET @current_schema = DATABASE();
SET @ddl = IF(
@country_dashboard_value_columns_schema_lock = 1
AND NOT EXISTS (
SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_SCHEMA = @current_schema
AND TABLE_NAME = 'country_dashboard_daily_metric'
AND COLUMN_NAME = 'salary_transfer'
),
'ALTER TABLE `country_dashboard_daily_metric` ADD COLUMN `salary_transfer` decimal(20,2) NOT NULL DEFAULT 0.00 COMMENT ''工资转移'' AFTER `salary_exchange`',
'SELECT 1'
);
PREPARE stmt FROM @ddl;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
SET @ddl = IF(
@country_dashboard_value_columns_schema_lock = 1
AND NOT EXISTS (
SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_SCHEMA = @current_schema
AND TABLE_NAME = 'country_dashboard_daily_metric'
AND COLUMN_NAME = 'daily_recharge_user'
),
'ALTER TABLE `country_dashboard_daily_metric` ADD COLUMN `daily_recharge_user` bigint NOT NULL DEFAULT 0 COMMENT ''当日充值用户数'' AFTER `country_new_user`',
'SELECT 1'
);
PREPARE stmt FROM @ddl;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
SET @ddl = IF(
@country_dashboard_value_columns_schema_lock = 1
AND NOT EXISTS (
SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_SCHEMA = @current_schema
AND TABLE_NAME = 'country_dashboard_daily_metric'
AND COLUMN_NAME = 'user_recharge'
),
'ALTER TABLE `country_dashboard_daily_metric` ADD COLUMN `user_recharge` decimal(20,2) NOT NULL DEFAULT 0.00 COMMENT ''用户充值'' AFTER `dealer_recharge`',
'SELECT 1'
);
PREPARE stmt FROM @ddl;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
SET @ddl = IF(
@country_dashboard_value_columns_schema_lock = 1
AND NOT EXISTS (
SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_SCHEMA = @current_schema
AND TABLE_NAME = 'country_dashboard_period_metric'
AND COLUMN_NAME = 'user_recharge'
),
'ALTER TABLE `country_dashboard_period_metric` ADD COLUMN `user_recharge` decimal(20,2) NOT NULL DEFAULT 0.00 COMMENT ''用户充值'' AFTER `dealer_recharge`',
'SELECT 1'
);
PREPARE stmt FROM @ddl;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
SET @ddl = IF(
@country_dashboard_value_columns_schema_lock = 1
AND NOT EXISTS (
SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_SCHEMA = @current_schema
AND TABLE_NAME = 'country_dashboard_daily_metric'
AND COLUMN_NAME = 'new_dealer_user_recharge'
),
'ALTER TABLE `country_dashboard_daily_metric` ADD COLUMN `new_dealer_user_recharge` decimal(20,2) NOT NULL DEFAULT 0.00 COMMENT ''新增币商用户充值'' AFTER `user_recharge`',
'SELECT 1'
);
PREPARE stmt FROM @ddl;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
SET @ddl = IF(
@country_dashboard_value_columns_schema_lock = 1
AND NOT EXISTS (
SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_SCHEMA = @current_schema
AND TABLE_NAME = 'country_dashboard_period_metric'
AND COLUMN_NAME = 'new_dealer_user_recharge'
),
'ALTER TABLE `country_dashboard_period_metric` ADD COLUMN `new_dealer_user_recharge` decimal(20,2) NOT NULL DEFAULT 0.00 COMMENT ''新增币商用户充值'' AFTER `user_recharge`',
'SELECT 1'
);
PREPARE stmt FROM @ddl;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
SET @ddl = IF(
@country_dashboard_value_columns_schema_lock = 1
AND NOT EXISTS (
SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_SCHEMA = @current_schema
AND TABLE_NAME = 'country_dashboard_daily_metric'
AND COLUMN_NAME = 'gift_consume'
),
'ALTER TABLE `country_dashboard_daily_metric` ADD COLUMN `gift_consume` decimal(20,2) NOT NULL DEFAULT 0.00 COMMENT ''礼物消耗'' AFTER `salary_exchange`',
'SELECT 1'
);
PREPARE stmt FROM @ddl;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
SET @ddl = IF(
@country_dashboard_value_columns_schema_lock = 1
AND NOT EXISTS (
SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_SCHEMA = @current_schema
AND TABLE_NAME = 'country_dashboard_period_metric'
AND COLUMN_NAME = 'salary_transfer'
),
'ALTER TABLE `country_dashboard_period_metric` ADD COLUMN `salary_transfer` decimal(20,2) NOT NULL DEFAULT 0.00 COMMENT ''工资转移'' AFTER `salary_exchange`',
'SELECT 1'
);
PREPARE stmt FROM @ddl;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
SET @ddl = IF(
@country_dashboard_value_columns_schema_lock = 1
AND NOT EXISTS (
SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_SCHEMA = @current_schema
AND TABLE_NAME = 'country_dashboard_daily_metric'
AND COLUMN_NAME = 'lucky_gift_anchor_share'
),
'ALTER TABLE `country_dashboard_daily_metric` ADD COLUMN `lucky_gift_anchor_share` decimal(20,2) NOT NULL DEFAULT 0.00 COMMENT ''幸运礼物主播分成'' AFTER `lucky_gift_payout`',
'SELECT 1'
);
PREPARE stmt FROM @ddl;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
SET @ddl = IF(
@country_dashboard_value_columns_schema_lock = 1
AND NOT EXISTS (
SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_SCHEMA = @current_schema
AND TABLE_NAME = 'country_dashboard_period_metric'
AND COLUMN_NAME = 'gift_consume'
),
'ALTER TABLE `country_dashboard_period_metric` ADD COLUMN `gift_consume` decimal(20,2) NOT NULL DEFAULT 0.00 COMMENT ''礼物消耗'' AFTER `salary_exchange`',
'SELECT 1'
);
PREPARE stmt FROM @ddl;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
SET @ddl = IF(
@country_dashboard_value_columns_schema_lock = 1
AND NOT EXISTS (
SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_SCHEMA = @current_schema
AND TABLE_NAME = 'country_dashboard_period_metric'
AND COLUMN_NAME = 'lucky_gift_anchor_share'
),
'ALTER TABLE `country_dashboard_period_metric` ADD COLUMN `lucky_gift_anchor_share` decimal(20,2) NOT NULL DEFAULT 0.00 COMMENT ''幸运礼物主播分成'' AFTER `lucky_gift_payout`',
'SELECT 1'
);
PREPARE stmt FROM @ddl;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
DO IF(@country_dashboard_value_columns_schema_lock = 1, RELEASE_LOCK('country_dashboard_value_columns_schema'), 0);
</update>
<sql id="CountryColumns"> <sql id="CountryColumns">
NULLIF(u.country_code, '') AS countryCode, NULLIF(u.country_code, '') AS countryCode,
NULLIF(u.country_name, '') AS countryName NULLIF(u.country_name, '') AS countryName
@ -832,15 +1180,21 @@
MAX(t.country_name) AS countryName, MAX(t.country_name) AS countryName,
<include refid="PrecomputedPeriodColumns"/>, <include refid="PrecomputedPeriodColumns"/>,
IFNULL(SUM(t.country_new_user), 0) AS countryNewUser, IFNULL(SUM(t.country_new_user), 0) AS countryNewUser,
IFNULL(SUM(t.daily_recharge_user), 0) AS dailyRechargeUser,
IFNULL(SUM(t.new_user_recharge), 0) AS newUserRecharge, IFNULL(SUM(t.new_user_recharge), 0) AS newUserRecharge,
IFNULL(SUM(t.official_recharge), 0) AS officialRecharge, IFNULL(SUM(t.official_recharge), 0) AS officialRecharge,
IFNULL(SUM(t.mifapay_recharge), 0) AS mifapayRecharge, IFNULL(SUM(t.mifapay_recharge), 0) AS mifapayRecharge,
IFNULL(SUM(t.google_recharge), 0) AS googleRecharge, IFNULL(SUM(t.google_recharge), 0) AS googleRecharge,
IFNULL(SUM(t.dealer_recharge), 0) AS dealerRecharge, IFNULL(SUM(t.dealer_recharge), 0) AS dealerRecharge,
IFNULL(SUM(t.user_recharge), 0) AS userRecharge,
IFNULL(SUM(t.new_dealer_user_recharge), 0) AS newDealerUserRecharge,
IFNULL(SUM(t.salary_exchange), 0) AS salaryExchange, IFNULL(SUM(t.salary_exchange), 0) AS salaryExchange,
IFNULL(SUM(t.salary_transfer), 0) AS salaryTransfer,
IFNULL(SUM(t.gift_consume), 0) AS giftConsume,
IFNULL(SUM(t.lucky_gift_total_flow), 0) AS luckyGiftTotalFlow, IFNULL(SUM(t.lucky_gift_total_flow), 0) AS luckyGiftTotalFlow,
IFNULL(SUM(t.lucky_gift_user), 0) AS luckyGiftUser, IFNULL(SUM(t.lucky_gift_user), 0) AS luckyGiftUser,
IFNULL(SUM(t.lucky_gift_payout), 0) AS luckyGiftPayout, IFNULL(SUM(t.lucky_gift_payout), 0) AS luckyGiftPayout,
IFNULL(SUM(t.lucky_gift_anchor_share), 0) AS luckyGiftAnchorShare,
IFNULL(SUM(t.game_total_flow), 0) AS gameTotalFlow, IFNULL(SUM(t.game_total_flow), 0) AS gameTotalFlow,
IFNULL(SUM(t.game_user), 0) AS gameUser, IFNULL(SUM(t.game_user), 0) AS gameUser,
IFNULL(SUM(t.game_payout), 0) AS gamePayout IFNULL(SUM(t.game_payout), 0) AS gamePayout
@ -887,18 +1241,31 @@
t.period_key AS periodKey, t.period_key AS periodKey,
MAX(t.period_name) AS periodName, MAX(t.period_name) AS periodName,
IFNULL(SUM(t.country_new_user), 0) AS countryNewUser, IFNULL(SUM(t.country_new_user), 0) AS countryNewUser,
IFNULL(SUM(t.daily_active_user), 0) AS dailyActiveUser,
IFNULL(SUM(t.daily_recharge_user), 0) AS dailyRechargeUser,
IFNULL(SUM(t.new_user_recharge), 0) AS newUserRecharge, IFNULL(SUM(t.new_user_recharge), 0) AS newUserRecharge,
IFNULL(SUM(t.official_recharge), 0) AS officialRecharge, IFNULL(SUM(t.official_recharge), 0) AS officialRecharge,
IFNULL(SUM(t.mifapay_recharge), 0) AS mifapayRecharge, IFNULL(SUM(t.mifapay_recharge), 0) AS mifapayRecharge,
IFNULL(SUM(t.google_recharge), 0) AS googleRecharge, IFNULL(SUM(t.google_recharge), 0) AS googleRecharge,
IFNULL(SUM(t.dealer_recharge), 0) AS dealerRecharge, IFNULL(SUM(t.dealer_recharge), 0) AS dealerRecharge,
IFNULL(SUM(t.user_recharge), 0) AS userRecharge,
IFNULL(SUM(t.new_dealer_user_recharge), 0) AS newDealerUserRecharge,
IFNULL(SUM(t.salary_exchange), 0) AS salaryExchange, IFNULL(SUM(t.salary_exchange), 0) AS salaryExchange,
IFNULL(SUM(t.salary_transfer), 0) AS salaryTransfer,
IFNULL(SUM(t.gift_consume), 0) AS giftConsume,
IFNULL(SUM(t.lucky_gift_total_flow), 0) AS luckyGiftTotalFlow, IFNULL(SUM(t.lucky_gift_total_flow), 0) AS luckyGiftTotalFlow,
IFNULL(SUM(t.lucky_gift_user), 0) AS luckyGiftUser, IFNULL(SUM(t.lucky_gift_user), 0) AS luckyGiftUser,
IFNULL(SUM(t.lucky_gift_payout), 0) AS luckyGiftPayout, IFNULL(SUM(t.lucky_gift_payout), 0) AS luckyGiftPayout,
IFNULL(SUM(t.lucky_gift_anchor_share), 0) AS luckyGiftAnchorShare,
IFNULL(SUM(t.game_total_flow), 0) AS gameTotalFlow, IFNULL(SUM(t.game_total_flow), 0) AS gameTotalFlow,
IFNULL(SUM(t.game_user), 0) AS gameUser, IFNULL(SUM(t.game_user), 0) AS gameUser,
IFNULL(SUM(t.game_payout), 0) AS gamePayout IFNULL(SUM(t.game_payout), 0) AS gamePayout,
IFNULL(SUM(t.d1_retention_user), 0) AS d1RetentionUser,
IFNULL(SUM(t.d1_retention_base_user), 0) AS d1RetentionBaseUser,
IFNULL(SUM(t.d7_retention_user), 0) AS d7RetentionUser,
IFNULL(SUM(t.d7_retention_base_user), 0) AS d7RetentionBaseUser,
IFNULL(SUM(t.d30_retention_user), 0) AS d30RetentionUser,
IFNULL(SUM(t.d30_retention_base_user), 0) AS d30RetentionBaseUser
FROM country_dashboard_period_metric t FROM country_dashboard_period_metric t
WHERE t.period_type = #{periodType} WHERE t.period_type = #{periodType}
AND t.stat_timezone = #{statTimezone} AND t.stat_timezone = #{statTimezone}
@ -963,15 +1330,21 @@
country_code, country_code,
country_name, country_name,
country_new_user, country_new_user,
daily_recharge_user,
new_user_recharge, new_user_recharge,
official_recharge, official_recharge,
mifapay_recharge, mifapay_recharge,
google_recharge, google_recharge,
dealer_recharge, dealer_recharge,
user_recharge,
new_dealer_user_recharge,
salary_exchange, salary_exchange,
salary_transfer,
gift_consume,
lucky_gift_total_flow, lucky_gift_total_flow,
lucky_gift_user, lucky_gift_user,
lucky_gift_payout, lucky_gift_payout,
lucky_gift_anchor_share,
game_total_flow, game_total_flow,
game_user, game_user,
game_payout, game_payout,
@ -986,15 +1359,21 @@
#{item.countryCode}, #{item.countryCode},
#{item.countryName}, #{item.countryName},
IFNULL(#{item.countryNewUser}, 0), IFNULL(#{item.countryNewUser}, 0),
IFNULL(#{item.dailyRechargeUser}, 0),
IFNULL(#{item.newUserRecharge}, 0), IFNULL(#{item.newUserRecharge}, 0),
IFNULL(#{item.officialRecharge}, 0), IFNULL(#{item.officialRecharge}, 0),
IFNULL(#{item.mifapayRecharge}, 0), IFNULL(#{item.mifapayRecharge}, 0),
IFNULL(#{item.googleRecharge}, 0), IFNULL(#{item.googleRecharge}, 0),
IFNULL(#{item.dealerRecharge}, 0), IFNULL(#{item.dealerRecharge}, 0),
IFNULL(#{item.userRecharge}, 0),
IFNULL(#{item.newDealerUserRecharge}, 0),
IFNULL(#{item.salaryExchange}, 0), IFNULL(#{item.salaryExchange}, 0),
IFNULL(#{item.salaryTransfer}, 0),
IFNULL(#{item.giftConsume}, 0),
IFNULL(#{item.luckyGiftTotalFlow}, 0), IFNULL(#{item.luckyGiftTotalFlow}, 0),
IFNULL(#{item.luckyGiftUser}, 0), IFNULL(#{item.luckyGiftUser}, 0),
IFNULL(#{item.luckyGiftPayout}, 0), IFNULL(#{item.luckyGiftPayout}, 0),
IFNULL(#{item.luckyGiftAnchorShare}, 0),
IFNULL(#{item.gameTotalFlow}, 0), IFNULL(#{item.gameTotalFlow}, 0),
IFNULL(#{item.gameUser}, 0), IFNULL(#{item.gameUser}, 0),
IFNULL(#{item.gamePayout}, 0), IFNULL(#{item.gamePayout}, 0),
@ -1004,15 +1383,21 @@
ON DUPLICATE KEY UPDATE ON DUPLICATE KEY UPDATE
country_name = VALUES(country_name), country_name = VALUES(country_name),
country_new_user = VALUES(country_new_user), country_new_user = VALUES(country_new_user),
daily_recharge_user = VALUES(daily_recharge_user),
new_user_recharge = VALUES(new_user_recharge), new_user_recharge = VALUES(new_user_recharge),
official_recharge = VALUES(official_recharge), official_recharge = VALUES(official_recharge),
mifapay_recharge = VALUES(mifapay_recharge), mifapay_recharge = VALUES(mifapay_recharge),
google_recharge = VALUES(google_recharge), google_recharge = VALUES(google_recharge),
dealer_recharge = VALUES(dealer_recharge), dealer_recharge = VALUES(dealer_recharge),
user_recharge = VALUES(user_recharge),
new_dealer_user_recharge = VALUES(new_dealer_user_recharge),
salary_exchange = VALUES(salary_exchange), salary_exchange = VALUES(salary_exchange),
salary_transfer = VALUES(salary_transfer),
gift_consume = VALUES(gift_consume),
lucky_gift_total_flow = VALUES(lucky_gift_total_flow), lucky_gift_total_flow = VALUES(lucky_gift_total_flow),
lucky_gift_user = VALUES(lucky_gift_user), lucky_gift_user = VALUES(lucky_gift_user),
lucky_gift_payout = VALUES(lucky_gift_payout), lucky_gift_payout = VALUES(lucky_gift_payout),
lucky_gift_anchor_share = VALUES(lucky_gift_anchor_share),
game_total_flow = VALUES(game_total_flow), game_total_flow = VALUES(game_total_flow),
game_user = VALUES(game_user), game_user = VALUES(game_user),
game_payout = VALUES(game_payout), game_payout = VALUES(game_payout),
@ -1041,18 +1426,31 @@
country_code, country_code,
country_name, country_name,
country_new_user, country_new_user,
daily_active_user,
daily_recharge_user,
new_user_recharge, new_user_recharge,
official_recharge, official_recharge,
mifapay_recharge, mifapay_recharge,
google_recharge, google_recharge,
dealer_recharge, dealer_recharge,
user_recharge,
new_dealer_user_recharge,
salary_exchange, salary_exchange,
salary_transfer,
gift_consume,
lucky_gift_total_flow, lucky_gift_total_flow,
lucky_gift_user, lucky_gift_user,
lucky_gift_payout, lucky_gift_payout,
lucky_gift_anchor_share,
game_total_flow, game_total_flow,
game_user, game_user,
game_payout, game_payout,
d1_retention_user,
d1_retention_base_user,
d7_retention_user,
d7_retention_base_user,
d30_retention_user,
d30_retention_base_user,
refreshed_at refreshed_at
) )
VALUES VALUES
@ -1068,18 +1466,31 @@
#{item.countryCode}, #{item.countryCode},
#{item.countryName}, #{item.countryName},
IFNULL(#{item.countryNewUser}, 0), IFNULL(#{item.countryNewUser}, 0),
IFNULL(#{item.dailyActiveUser}, 0),
IFNULL(#{item.dailyRechargeUser}, 0),
IFNULL(#{item.newUserRecharge}, 0), IFNULL(#{item.newUserRecharge}, 0),
IFNULL(#{item.officialRecharge}, 0), IFNULL(#{item.officialRecharge}, 0),
IFNULL(#{item.mifapayRecharge}, 0), IFNULL(#{item.mifapayRecharge}, 0),
IFNULL(#{item.googleRecharge}, 0), IFNULL(#{item.googleRecharge}, 0),
IFNULL(#{item.dealerRecharge}, 0), IFNULL(#{item.dealerRecharge}, 0),
IFNULL(#{item.userRecharge}, 0),
IFNULL(#{item.newDealerUserRecharge}, 0),
IFNULL(#{item.salaryExchange}, 0), IFNULL(#{item.salaryExchange}, 0),
IFNULL(#{item.salaryTransfer}, 0),
IFNULL(#{item.giftConsume}, 0),
IFNULL(#{item.luckyGiftTotalFlow}, 0), IFNULL(#{item.luckyGiftTotalFlow}, 0),
IFNULL(#{item.luckyGiftUser}, 0), IFNULL(#{item.luckyGiftUser}, 0),
IFNULL(#{item.luckyGiftPayout}, 0), IFNULL(#{item.luckyGiftPayout}, 0),
IFNULL(#{item.luckyGiftAnchorShare}, 0),
IFNULL(#{item.gameTotalFlow}, 0), IFNULL(#{item.gameTotalFlow}, 0),
IFNULL(#{item.gameUser}, 0), IFNULL(#{item.gameUser}, 0),
IFNULL(#{item.gamePayout}, 0), IFNULL(#{item.gamePayout}, 0),
IFNULL(#{item.d1RetentionUser}, 0),
IFNULL(#{item.d1RetentionBaseUser}, 0),
IFNULL(#{item.d7RetentionUser}, 0),
IFNULL(#{item.d7RetentionBaseUser}, 0),
IFNULL(#{item.d30RetentionUser}, 0),
IFNULL(#{item.d30RetentionBaseUser}, 0),
NOW() NOW()
) )
</foreach> </foreach>
@ -1089,18 +1500,31 @@
period_end_date = VALUES(period_end_date), period_end_date = VALUES(period_end_date),
country_name = VALUES(country_name), country_name = VALUES(country_name),
country_new_user = VALUES(country_new_user), country_new_user = VALUES(country_new_user),
daily_active_user = VALUES(daily_active_user),
daily_recharge_user = VALUES(daily_recharge_user),
new_user_recharge = VALUES(new_user_recharge), new_user_recharge = VALUES(new_user_recharge),
official_recharge = VALUES(official_recharge), official_recharge = VALUES(official_recharge),
mifapay_recharge = VALUES(mifapay_recharge), mifapay_recharge = VALUES(mifapay_recharge),
google_recharge = VALUES(google_recharge), google_recharge = VALUES(google_recharge),
dealer_recharge = VALUES(dealer_recharge), dealer_recharge = VALUES(dealer_recharge),
user_recharge = VALUES(user_recharge),
new_dealer_user_recharge = VALUES(new_dealer_user_recharge),
salary_exchange = VALUES(salary_exchange), salary_exchange = VALUES(salary_exchange),
salary_transfer = VALUES(salary_transfer),
gift_consume = VALUES(gift_consume),
lucky_gift_total_flow = VALUES(lucky_gift_total_flow), lucky_gift_total_flow = VALUES(lucky_gift_total_flow),
lucky_gift_user = VALUES(lucky_gift_user), lucky_gift_user = VALUES(lucky_gift_user),
lucky_gift_payout = VALUES(lucky_gift_payout), lucky_gift_payout = VALUES(lucky_gift_payout),
lucky_gift_anchor_share = VALUES(lucky_gift_anchor_share),
game_total_flow = VALUES(game_total_flow), game_total_flow = VALUES(game_total_flow),
game_user = VALUES(game_user), game_user = VALUES(game_user),
game_payout = VALUES(game_payout), game_payout = VALUES(game_payout),
d1_retention_user = VALUES(d1_retention_user),
d1_retention_base_user = VALUES(d1_retention_base_user),
d7_retention_user = VALUES(d7_retention_user),
d7_retention_base_user = VALUES(d7_retention_base_user),
d30_retention_user = VALUES(d30_retention_user),
d30_retention_base_user = VALUES(d30_retention_base_user),
refreshed_at = NOW() refreshed_at = NOW()
</insert> </insert>
@ -1114,6 +1538,63 @@
</if> </if>
</delete> </delete>
<insert id="upsertPeriodActiveRetentionMetrics">
INSERT INTO country_dashboard_period_metric (
period_type,
period_key,
stat_timezone,
period_name,
period_start_date,
period_end_date,
sys_origin,
country_code,
country_name,
daily_active_user,
d1_retention_user,
d1_retention_base_user,
d7_retention_user,
d7_retention_base_user,
d30_retention_user,
d30_retention_base_user,
refreshed_at
)
VALUES
<foreach collection="metrics" item="item" separator=",">
(
#{item.periodType},
#{item.periodKey},
#{item.statTimezone},
#{item.periodName},
#{item.periodStartDate},
#{item.periodEndDate},
#{item.sysOrigin},
#{item.countryCode},
#{item.countryName},
IFNULL(#{item.dailyActiveUser}, 0),
IFNULL(#{item.d1RetentionUser}, 0),
IFNULL(#{item.d1RetentionBaseUser}, 0),
IFNULL(#{item.d7RetentionUser}, 0),
IFNULL(#{item.d7RetentionBaseUser}, 0),
IFNULL(#{item.d30RetentionUser}, 0),
IFNULL(#{item.d30RetentionBaseUser}, 0),
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),
daily_active_user = VALUES(daily_active_user),
d1_retention_user = VALUES(d1_retention_user),
d1_retention_base_user = VALUES(d1_retention_base_user),
d7_retention_user = VALUES(d7_retention_user),
d7_retention_base_user = VALUES(d7_retention_base_user),
d30_retention_user = VALUES(d30_retention_user),
d30_retention_base_user = VALUES(d30_retention_base_user),
refreshed_at = NOW()
</insert>
<insert id="insertPeriodNewUserMetrics"> <insert id="insertPeriodNewUserMetrics">
INSERT IGNORE INTO country_dashboard_period_user_metric ( INSERT IGNORE INTO country_dashboard_period_user_metric (
period_type, period_type,
@ -1275,6 +1756,7 @@
t.period_key AS periodKey, t.period_key AS periodKey,
MAX(t.period_name) AS periodName, MAX(t.period_name) AS periodName,
SUM(CASE WHEN t.metric_type = 'COUNTRY_NEW_USER' THEN 1 ELSE 0 END) AS countryNewUser, SUM(CASE WHEN t.metric_type = 'COUNTRY_NEW_USER' THEN 1 ELSE 0 END) AS countryNewUser,
SUM(CASE WHEN t.metric_type = 'RECHARGE_USER' THEN 1 ELSE 0 END) AS dailyRechargeUser,
SUM(CASE WHEN t.metric_type = 'LUCKY_GIFT_USER' THEN 1 ELSE 0 END) AS luckyGiftUser, SUM(CASE WHEN t.metric_type = 'LUCKY_GIFT_USER' THEN 1 ELSE 0 END) AS luckyGiftUser,
SUM(CASE WHEN t.metric_type = 'GAME_USER' THEN 1 ELSE 0 END) AS gameUser SUM(CASE WHEN t.metric_type = 'GAME_USER' THEN 1 ELSE 0 END) AS gameUser
FROM country_dashboard_period_user_metric t FROM country_dashboard_period_user_metric t
@ -1294,16 +1776,30 @@
MAX(t.country_name) AS countryName, MAX(t.country_name) AS countryName,
'ALL' AS periodKey, 'ALL' AS periodKey,
'全部' AS periodName, '全部' AS periodName,
IFNULL(SUM(t.country_new_user), 0) AS countryNewUser,
IFNULL(SUM(t.daily_active_user), 0) AS dailyActiveUser,
IFNULL(SUM(t.daily_recharge_user), 0) AS dailyRechargeUser,
IFNULL(SUM(t.new_user_recharge), 0) AS newUserRecharge, IFNULL(SUM(t.new_user_recharge), 0) AS newUserRecharge,
IFNULL(SUM(t.official_recharge), 0) AS officialRecharge, IFNULL(SUM(t.official_recharge), 0) AS officialRecharge,
IFNULL(SUM(t.mifapay_recharge), 0) AS mifapayRecharge, IFNULL(SUM(t.mifapay_recharge), 0) AS mifapayRecharge,
IFNULL(SUM(t.google_recharge), 0) AS googleRecharge, IFNULL(SUM(t.google_recharge), 0) AS googleRecharge,
IFNULL(SUM(t.dealer_recharge), 0) AS dealerRecharge, IFNULL(SUM(t.dealer_recharge), 0) AS dealerRecharge,
IFNULL(SUM(t.user_recharge), 0) AS userRecharge,
IFNULL(SUM(t.new_dealer_user_recharge), 0) AS newDealerUserRecharge,
IFNULL(SUM(t.salary_exchange), 0) AS salaryExchange, IFNULL(SUM(t.salary_exchange), 0) AS salaryExchange,
IFNULL(SUM(t.salary_transfer), 0) AS salaryTransfer,
IFNULL(SUM(t.gift_consume), 0) AS giftConsume,
IFNULL(SUM(t.lucky_gift_total_flow), 0) AS luckyGiftTotalFlow, IFNULL(SUM(t.lucky_gift_total_flow), 0) AS luckyGiftTotalFlow,
IFNULL(SUM(t.lucky_gift_payout), 0) AS luckyGiftPayout, IFNULL(SUM(t.lucky_gift_payout), 0) AS luckyGiftPayout,
IFNULL(SUM(t.lucky_gift_anchor_share), 0) AS luckyGiftAnchorShare,
IFNULL(SUM(t.game_total_flow), 0) AS gameTotalFlow, IFNULL(SUM(t.game_total_flow), 0) AS gameTotalFlow,
IFNULL(SUM(t.game_payout), 0) AS gamePayout IFNULL(SUM(t.game_payout), 0) AS gamePayout,
IFNULL(SUM(t.d1_retention_user), 0) AS d1RetentionUser,
IFNULL(SUM(t.d1_retention_base_user), 0) AS d1RetentionBaseUser,
IFNULL(SUM(t.d7_retention_user), 0) AS d7RetentionUser,
IFNULL(SUM(t.d7_retention_base_user), 0) AS d7RetentionBaseUser,
IFNULL(SUM(t.d30_retention_user), 0) AS d30RetentionUser,
IFNULL(SUM(t.d30_retention_base_user), 0) AS d30RetentionBaseUser
FROM country_dashboard_period_metric t FROM country_dashboard_period_metric t
WHERE t.period_type = 'DAY' WHERE t.period_type = 'DAY'
AND t.stat_timezone = #{statTimezone} AND t.stat_timezone = #{statTimezone}
@ -1423,41 +1919,68 @@
<select id="countRechargeDetails" resultType="java.lang.Long"> <select id="countRechargeDetails" resultType="java.lang.Long">
SELECT COUNT(1) SELECT COUNT(1)
FROM ( FROM country_dashboard_recharge_detail t
<include refid="RechargeDetailSource"/>
) t
INNER JOIN user_base_info u ON u.id = t.userId
WHERE 1 = 1 WHERE 1 = 1
<include refid="UserWhere"/> <if test="startTime != null">
<include refid="CountryCodesWhere"/> AND t.create_time &gt;= #{startTime}
</if>
<if test="endTime != null">
AND t.create_time &lt; #{endTime}
</if>
<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="sysOrigin != null and sysOrigin != ''">
AND t.sys_origin = #{sysOrigin}
</if>
</select> </select>
<select id="listRechargeDetails" <select id="listRechargeDetails"
resultType="com.red.circle.console.infra.database.rds.dto.datav.CountryDashboardRechargeDetailDTO"> resultType="com.red.circle.console.infra.database.rds.dto.datav.CountryDashboardRechargeDetailDTO">
SELECT SELECT
t.recordId, t.record_id AS recordId,
t.sourceType, t.source_type AS sourceType,
t.sourceChannel, t.source_channel AS sourceChannel,
t.userId, t.user_id AS userId,
u.account AS userAccount, u.account AS userAccount,
u.user_nickname AS userNickname, u.user_nickname AS userNickname,
NULLIF(u.country_code, '') AS countryCode, t.country_code AS countryCode,
NULLIF(u.country_name, '') AS countryName, t.country_name AS countryName,
t.amount, t.amount,
t.coinQuantity, t.coin_quantity AS coinQuantity,
t.remark, t.remark,
DATE_FORMAT( DATE_FORMAT(
CONVERT_TZ(t.createTime, #{storageTimezoneOffset}, #{statTimezoneOffset}), CONVERT_TZ(t.create_time, #{storageTimezoneOffset}, #{statTimezoneOffset}),
'%Y-%m-%d %H:%i:%s' '%Y-%m-%d %H:%i:%s'
) AS createTime ) AS createTime
FROM ( FROM country_dashboard_recharge_detail t
<include refid="RechargeDetailSource"/> LEFT JOIN user_base_info u ON u.id = t.user_id
) t
INNER JOIN user_base_info u ON u.id = t.userId
WHERE 1 = 1 WHERE 1 = 1
<include refid="UserWhere"/> <if test="startTime != null">
<include refid="CountryCodesWhere"/> AND t.create_time &gt;= #{startTime}
ORDER BY t.createTime DESC, t.recordId DESC </if>
<if test="endTime != null">
AND t.create_time &lt; #{endTime}
</if>
<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="sysOrigin != null and sysOrigin != ''">
AND t.sys_origin = #{sysOrigin}
</if>
ORDER BY t.create_time DESC, t.record_id DESC
LIMIT #{limit} OFFSET #{offset} LIMIT #{limit} OFFSET #{offset}
</select> </select>

View File

@ -0,0 +1,93 @@
# 国家数据大屏口径对账与修复计划
## 基准说明
- 对账日期:`2026-05-27`
- 统计时区:`Asia/Riyadh`
- 统计窗口:`2026-05-27 00:00:00``2026-05-28 00:00:00`
- 系统来源:`LIKEI`
- 对账方式:旧 Java 源表口径结果 vs 当前 `country_dashboard_*` 预聚合结果。
- 注意:日内数据会继续增长,金额和人数会随时间变化。本文中的“之前数据”来自本轮对账快照;后续修复验证必须重新跑同一窗口源表对账。
## 总体结论
当前大屏切到预聚合后,差异主要来自三类问题:
1. 旧 Java 是直接按 Mongo/SQL 源表字段聚合CDC worker 有些字段改成了用户国家表、wallet 事件或部分表事件,口径没有完全复刻。
2. 部分 Mongo 源表没有进入 CDC日活、礼物流水、幸运礼物、工资转移等需要单独的 reconcile/增量任务。
3. 留存基数与留存命中当前使用 worker 自己的用户国家表和活跃集合,和旧 Java 的注册 cohort + active log 口径不一致。
## 字段对账表
| 字段 | 旧 Java 口径 | 旧口径源表结果 | 当前预聚合结果 | 差异 | 差异原因 | 修复方案 | 状态 |
| --- | --- | ---: | ---: | ---: | --- | --- | --- |
| 新增 | 按 admin 用户列表展示口径统计 `user_base_info` 创建时间,展示日等于 `create_time + 8h` 后的日期;按用户国家归组。 | 之前用户列表显示约 57部署期间增长到 60 | 修复前明显少算 | 修复前不稳定 | worker 原先按数据库原始时间截日,和 admin 用户列表展示日不一致。 | 已改 worker 的新用户统计窗口为 `create_time + 8h` 展示日;回填目标日期并重建 `DAY/ALL`。 | 已修 |
| 日活 | Mongo `user_daily_active_log`,按 `activeDate + sysOrigin` 过滤;先按 `registerCountryCode,userId` 去重,再按 `registerCountryCode` 计数。 | 984 | 844 | -140 | worker 原先读取 active log 后,又强依赖 `dashboard_cdc_user_country` 做国家归类,用户国家缺失或删除标记会把活跃用户过滤掉;旧 Java 不查用户国家表。 | 已改 worker日活直接使用 active log 的 `sysOrigin/registerCountryCode`;只有 active log 国家为空才 fallback 用户国家表。 | 已修 |
| 新增充值 | 旧 Java 订单口径中,新注册用户在窗口内产生的充值。 | 2 | 2 | 0 | 当前已对齐。 | 保持现状;后续只做回归验证。 | 正常 |
| Google 充值 | Google 订单成功支付金额,按订单完成时间落入统计窗口。 | 63 | 63 | 0 | 当前已对齐。 | 保持现状;后续只做回归验证。 | 正常 |
| MifaPay 充值 | MifaPay 订单成功支付金额,按订单完成时间落入统计窗口。 | 0 | 0 | 0 | 当前已对齐。 | 保持现状;后续只做回归验证。 | 正常 |
| 币商充值 | 币商/代理充值流水,旧 Java 从币商充值相关 SQL 表聚合。 | 0 | 0 | 0 | 当前已对齐。 | 保持现状;后续只做回归验证。 | 正常 |
| 总充值 | Google + MifaPay + 币商等充值汇总。 | 63 | 63 | 0 | 当前已对齐。 | 保持现状;后续只做回归验证。 | 正常 |
| 工资兑换 | 旧 Java 包含 SQL 工资账户流水和 Mongo bank 流水中的工资兑换口径。 | 6.75 | 0 | -6.75 | worker 当前没有完整接入工资兑换相关 SQL/Mongo 流水。 | 增加工资兑换 reconcile读取旧 Java 使用的 SQL 表和 Mongo `user_bank_running_water` 条件,按用户国家归组写 `salary_exchange`;补回填。 | 待修 |
| 工资转移 | 旧 Java 来自 Mongo `user_bank_running_water` 的工资转移口径。 | 38 | 0 | -38 | worker 当前未接入 Mongo bank 转移流水。 | 增加 Mongo bank reconcile复刻旧 Java 的转移类型、状态、时间字段、金额单位,写 `salary_transfer`;补回填。 | 待修 |
| 礼物消耗 | Mongo `gift_give_running_water`;旧 Java 条件包含 `refunded != true``userId != null`、窗口内 `createTime`、指定 `sysOrigin``luckyGift != true``originId` 为数字、`dynamicContentId` 为空或不存在;金额取 `giftValue.actualAmount`。 | 7,276,368 | 3,327,288 | -3,949,080 | worker 当前没有完整复刻 Mongo `gift_give_running_water` 条件,可能只覆盖部分礼物事件或国家映射。 | 新增/修正 gift reconcile直接按旧 Java Mongo 条件聚合普通礼物消耗,写 `gift_consume`;不要从 wallet `LUCKY_GIFT` 反推普通礼物。 | 待修 |
| 幸运礼物流水 | 旧 Java 从 `game_lucky_gift_count` 聚合幸运礼物总流水。 | 2,279,050 | 0 | -2,279,050 | `game_lucky_gift_count` 有源数据,但 worker/预聚合没有写入 `lucky_gift_total_flow`。 | 新增 lucky gift reconcile按旧 Java 查询条件读取 `game_lucky_gift_count`,写 `lucky_gift_total_flow`,同时写用户去重表 `LUCKY_GIFT_USER`。 | 待修 |
| 幸运礼物人数 | 旧 Java 从 `game_lucky_gift_count` 按用户去重统计参与人数。 | 101 | 0 | -101 | 同幸运礼物流水,用户去重表未写。 | lucky gift reconcile 写 `country_dashboard_period_user_metric.metric_type='LUCKY_GIFT_USER'`,再重建汇总人数。 | 待修 |
| 幸运礼物返奖 | 旧 Java 从 `game_lucky_gift_count` 聚合返奖金额。 | 1,892,900 | 0 | -1,892,900 | 同幸运礼物流水,返奖字段未写。 | lucky gift reconcile 写 `lucky_gift_payout`。 | 待修 |
| 幸运礼物利润 | `幸运礼物流水 - 幸运礼物返奖`。 | 386,150 | 0 | -386,150 | 流水和返奖都缺失导致利润为 0。 | 修复幸运礼物流水和返奖后自动恢复;利润不应单独写源数据。 | 待修 |
| 游戏流水 | 旧 Java 从 `wallet_gold_asset_record_*` 和游戏统计表按游戏下注/消耗事件聚合。 | 109,688,935 | 74,509,547 | -35,179,388 | worker 当前游戏聚合少覆盖部分 `event_type` 或表来源,且用户去重也随之少算。 | 按旧 Java 的 game event_type 列表逐项核对,补齐 consume 事件映射;对每类事件做源表 vs 预聚合差异拆分。 | 待修 |
| 游戏人数 | 旧 Java 按游戏流水参与用户去重。 | 176 | 113 | -63 | 游戏流水事件少覆盖,导致用户去重同步少算。 | 游戏流水修复时同步写 `GAME_USER` 去重表;重建 `DAY/ALL`。 | 待修 |
| 游戏返奖 | 旧 Java 从游戏返奖/中奖事件聚合。 | 107,776,247 | 72,064,472 | -35,711,775 | worker 当前 payout 事件覆盖不完整,返奖少算幅度大于流水少算。 | 按旧 Java payout event_type 和 `game_lucky_box_count` 等来源补齐映射;验证 consume/payout 各自差值。 | 待修 |
| 游戏利润 | `游戏流水 - 游戏返奖`。 | 1,912,688 | 2,445,075 | +532,387 | 流水和返奖都少算,但返奖少算更多,所以利润虚高。 | 修复游戏流水和游戏返奖后重算;利润不单独写源数据。 | 待修 |
| 次日留存 | 旧 Java注册 cohort 来自用户注册表;命中来自 Mongo `user_daily_active_log` 的目标活跃日期。 | 0 / 0 | 84 / 127 | +84 / +127 | 当前留存基数和命中口径与旧 Java 不一致,使用了 worker 用户国家表和当前 active 集合。 | 重写 retention reconcile注册 cohort 复刻旧 Java `listNewUserCohorts`,活跃命中直接查 active log按国家分别写 base/user。 | 待修 |
| 七日留存 | 同上,目标日期为注册日 + 7。 | 0 / 0 | 25 / 60 | +25 / +60 | 同次日留存。 | 同 retention reconcile支持 D7。 | 待修 |
| 30 日留存 | 同上,目标日期为注册日 + 30。 | 0 / 0 | 5 / 7 | +5 / +7 | 同次日留存。 | 同 retention reconcile支持 D30。 | 待修 |
## 已完成修复记录
### 新增
- 修改点:`dashboard-cdc-worker` 新增用户统计使用 admin 用户列表展示日,即 `create_time + 8h` 后的日期。
- 验证结果:`2026-05-27` 修复后源数据和预聚合从 57 增长到 60增长来自部署期间新增的 3 个用户。
- 结论:新增已与用户列表显示口径对齐。
### 日活
- 修改点:`dashboard-cdc-worker/internal/storage/active_reconcile.go`
- 提交:`0b5e687 Align daily active metric with active log`
- 部署镜像:`dashboard-cdc-worker:daily-active-log-country-20260527-0b5e687`
- 验证结果:`2026-05-27`
- Mongo 旧 Java 口径:`1054`
- `country_dashboard_period_metric` 预聚合:`1054`
- 差值:`0`
- 结论:日活已与旧 Java active log 口径对齐。
## 后续修复顺序建议
1. 工资兑换、工资转移:影响字段少,数据源明确,先补 Mongo/SQL reconcile。
2. 礼物消耗:复刻 `gift_give_running_water` 条件,避免继续从 wallet 侧错误推导。
3. 幸运礼物:直接接 `game_lucky_gift_count`,同时写金额和人数去重。
4. 游戏流水、游戏返奖、游戏人数:按 event_type 拆分差异后逐类补齐。
5. 留存:最后统一重写,因为它依赖新增口径和日活口径都稳定。
## 每项修复后的验证模板
每修一个字段,都必须跑同一套对账:
```sql
-- 预聚合结果
SELECT
SUM(<metric_column>) AS precomputed_value
FROM country_dashboard_period_metric
WHERE period_type = 'DAY'
AND period_key = '2026-05-27'
AND stat_timezone = 'Asia/Riyadh'
AND sys_origin = 'LIKEI';
```
源表侧按旧 Java 条件单独查一遍,并输出:
| 字段 | 旧 Java 源表结果 | 预聚合结果 | 差值 | 是否通过 |
| --- | ---: | ---: | ---: | --- |
| 待验证字段 | 0 | 0 | 0 | 是/否 |

View File

@ -1,8 +1,9 @@
package com.red.circle.external.inner.endpoint.message; package com.red.circle.external.inner.endpoint.message;
import com.red.circle.external.inner.endpoint.message.api.ImMessageClientApi; import com.red.circle.external.inner.endpoint.message.api.ImMessageClientApi;
import com.red.circle.external.inner.service.message.ImMessageClientService; import com.red.circle.external.inner.model.cmd.message.CustomC2cMsgBodyCmd;
import com.red.circle.framework.dto.ResultResponse; import com.red.circle.external.inner.service.message.ImMessageClientService;
import com.red.circle.framework.dto.ResultResponse;
import com.red.circle.tool.core.parse.DataTypeUtils; import com.red.circle.tool.core.parse.DataTypeUtils;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
@ -30,10 +31,16 @@ public class ImMessageClientEndpoint implements ImMessageClientApi {
} }
@Override @Override
public ResultResponse<Void> sendMessageText(Long fromAccount, Long toAccount, String text) { public ResultResponse<Void> sendMessageText(Long fromAccount, Long toAccount, String text) {
imMessageClientService.sendMessageText(DataTypeUtils.toString(fromAccount), imMessageClientService.sendMessageText(DataTypeUtils.toString(fromAccount),
DataTypeUtils.toString(toAccount), text); DataTypeUtils.toString(toAccount), text);
return ResultResponse.success(); return ResultResponse.success();
} }
} @Override
public ResultResponse<Void> sendCustomMessage(CustomC2cMsgBodyCmd cmd) {
imMessageClientService.sendCustomMessage(cmd);
return ResultResponse.success();
}
}

View File

@ -1,7 +1,9 @@
package com.red.circle.external.inner.service.message; package com.red.circle.external.inner.service.message;
/** import com.red.circle.external.inner.model.cmd.message.CustomC2cMsgBodyCmd;
* im 消息服务.
/**
* im 消息服务.
* *
* @author pengliang on 2023/7/12 * @author pengliang on 2023/7/12
*/ */
@ -9,7 +11,12 @@ public interface ImMessageClientService {
/** /**
* 发送文本消息. * 发送文本消息.
*/ */
void sendMessageText(String fromAccount, String toAccount, String text); void sendMessageText(String fromAccount, String toAccount, String text);
} /**
* 发送 C2C 自定义消息.
*/
void sendCustomMessage(CustomC2cMsgBodyCmd cmd);
}

View File

@ -1,10 +1,16 @@
package com.red.circle.external.inner.service.message.impl; package com.red.circle.external.inner.service.message.impl;
import com.red.circle.component.instant.msg.tencet.service.endpoint.api.im.account.ImMessageManagerService; import com.red.circle.component.instant.msg.tencet.service.endpoint.api.im.account.ImMessageManagerService;
import com.red.circle.external.inner.service.message.ImMessageClientService; import com.red.circle.component.instant.msg.tencet.service.param.ImMessageParam;
import lombok.RequiredArgsConstructor; import com.red.circle.component.instant.msg.tencet.service.param.MessageBody;
import lombok.extern.log4j.Log4j2; import com.red.circle.component.instant.msg.tencet.service.param.OfflinePushInfo;
import org.springframework.stereotype.Service; import com.red.circle.external.inner.model.cmd.message.CustomC2cMsgBodyCmd;
import com.red.circle.external.inner.service.message.ImMessageClientService;
import com.red.circle.tool.core.num.NumUtils;
import com.red.circle.tool.core.parse.DataTypeUtils;
import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;
import org.springframework.stereotype.Service;
/** /**
* im 消息服务. * im 消息服务.
@ -19,8 +25,25 @@ public class ImMessageClientServiceImpl implements ImMessageClientService {
private final ImMessageManagerService imMessageManagerService; private final ImMessageManagerService imMessageManagerService;
@Override @Override
public void sendMessageText(String fromAccount, String toAccount, String text) { public void sendMessageText(String fromAccount, String toAccount, String text) {
imMessageManagerService.sendMessageText(fromAccount, toAccount, text).subscribe(); imMessageManagerService.sendMessageText(fromAccount, toAccount, text).subscribe();
} }
} @Override
public void sendCustomMessage(CustomC2cMsgBodyCmd cmd) {
ImMessageParam param = ImMessageParam.builder()
.syncOtherMachine(cmd.getSyncOtherMachine())
.fromAccount(cmd.getFromAccount())
.toAccount(cmd.getToAccount())
.offlinePushInfo(OfflinePushInfo.builder()
.pushFlag(0)
.build())
.msgRandom(DataTypeUtils.toInteger(NumUtils.getRandomNumberString(9)))
.build()
.addMsgBody(MessageBody.customBody(cmd.getDesc(), cmd.getData()));
imMessageManagerService.sendMessage(param)
.subscribe(res -> log.warn("sendCustomMessage response result:{}", res),
res -> log.error("sendCustomMessage error cmd={}, result={}", cmd, res));
}
}

View File

@ -109,6 +109,7 @@ public class AppInAppPurchaseCmdExe {
.setSysOrigin(history.getSysOrigin()) .setSysOrigin(history.getSysOrigin())
.setPlatform(history.getPlatform()) .setPlatform(history.getPlatform())
.setPayPlatform(history.getPayPlatform()) .setPayPlatform(history.getPayPlatform())
.setAppVersion(reqAppVersion(cmd))
.setOrigin(appCommodity.getProductConfigProductId()) .setOrigin(appCommodity.getProductConfigProductId())
.setOriginName(appCommodity.getOriginDescription()) .setOriginName(appCommodity.getOriginDescription())
.setExpireDateMs(history.getExpiresDateMs()) .setExpireDateMs(history.getExpiresDateMs())
@ -168,7 +169,12 @@ public class AppInAppPurchaseCmdExe {
return MonthlyRechargeType.UNDEFINED; return MonthlyRechargeType.UNDEFINED;
} }
private void saveFriendshipRecord(AppCommodity appCommodity) { private String reqAppVersion(AbstractPurchaseCmd cmd) {
return Objects.nonNull(cmd.getReqAppIntel())
? Objects.toString(cmd.getReqAppIntel().getVersion(), "")
: "";
}
private void saveFriendshipRecord(AppCommodity appCommodity) {
PurchaseHistory purchaseHistory = appCommodity.getPurchaseHistory(); PurchaseHistory purchaseHistory = appCommodity.getPurchaseHistory();
orderPurchaseFriendshipRecordService.save( orderPurchaseFriendshipRecordService.save(
new OrderPurchaseFriendshipRecord() new OrderPurchaseFriendshipRecord()

View File

@ -49,6 +49,14 @@ public class PayCountryAliasResolver {
return getMappedUsablePayCountryByCountryId(countryId); return getMappedUsablePayCountryByCountryId(countryId);
} }
public PayCountry resolvePayCountryByCountryId(Long countryId) {
PayCountry usablePayCountry = resolveUsablePayCountryByCountryId(countryId);
if (Objects.nonNull(usablePayCountry)) {
return usablePayCountry;
}
return getShelfPayCountryByCountryId(countryId);
}
private PayCountry getMappedUsablePayCountryByCountryId(Long countryId) { private PayCountry getMappedUsablePayCountryByCountryId(Long countryId) {
SysCountryCodeDTO countryCode = getCountryCodeById(countryId); SysCountryCodeDTO countryCode = getCountryCodeById(countryId);
if (Objects.isNull(countryCode)) { if (Objects.isNull(countryCode)) {
@ -114,6 +122,21 @@ public class PayCountryAliasResolver {
.orElse(null); .orElse(null);
} }
private PayCountry getShelfPayCountryByCountryId(Long countryId) {
if (Objects.isNull(countryId)) {
return null;
}
return payCountryService.query()
.eq(PayCountry::getShelf, Boolean.TRUE)
.eq(PayCountry::getCountryId, countryId)
.orderByDesc(PayCountry::getSort)
.list()
.stream()
.findFirst()
.orElse(null);
}
private boolean isUsablePayCountry(PayCountry payCountry) { private boolean isUsablePayCountry(PayCountry payCountry) {
return Objects.nonNull(payCountry) return Objects.nonNull(payCountry)
&& StringUtils.isNotBlank(payCountry.getCurrency()) && StringUtils.isNotBlank(payCountry.getCurrency())

View File

@ -1,12 +1,13 @@
package com.red.circle.order.app.command.pay.web; package com.red.circle.order.app.command.pay.web;
import com.google.api.client.util.Lists; import com.google.api.client.util.Lists;
import com.google.api.client.util.Maps; import com.google.api.client.util.Maps;
import com.google.api.client.util.Sets; import com.google.api.client.util.Sets;
import com.red.circle.common.business.core.amount.PayAmountArithmeticUtils; import com.red.circle.common.business.core.amount.PayAmountArithmeticUtils;
import com.red.circle.common.business.core.enums.SysOriginPlatformEnum; import com.red.circle.common.business.core.enums.SysOriginPlatformEnum;
import com.red.circle.framework.core.asserts.ResponseAssert; import com.red.circle.common.business.core.util.CountryCodeAliasUtils;
import com.red.circle.framework.core.response.CommonErrorCode; import com.red.circle.framework.core.asserts.ResponseAssert;
import com.red.circle.framework.core.response.CommonErrorCode;
import com.red.circle.order.app.convertor.PayAppConvertor; import com.red.circle.order.app.convertor.PayAppConvertor;
import com.red.circle.order.app.dto.clientobject.pay.ApplicationCommodityCardV2CO; import com.red.circle.order.app.dto.clientobject.pay.ApplicationCommodityCardV2CO;
import com.red.circle.order.app.dto.clientobject.pay.PayApplicationCO; import com.red.circle.order.app.dto.clientobject.pay.PayApplicationCO;
@ -23,6 +24,8 @@ import com.red.circle.order.infra.database.rds.service.pay.PayApplicationCommodi
import com.red.circle.order.infra.database.rds.service.pay.PayApplicationService; import com.red.circle.order.infra.database.rds.service.pay.PayApplicationService;
import com.red.circle.order.infra.database.rds.service.pay.PayChannelService; import com.red.circle.order.infra.database.rds.service.pay.PayChannelService;
import com.red.circle.order.infra.database.rds.service.pay.PayCountryChannelDetailsService; import com.red.circle.order.infra.database.rds.service.pay.PayCountryChannelDetailsService;
import com.red.circle.other.inner.endpoint.sys.SysCountryCodeClient;
import com.red.circle.other.inner.model.dto.sys.SysCountryCodeDTO;
import com.red.circle.tool.core.collection.CollectionUtils; import com.red.circle.tool.core.collection.CollectionUtils;
import com.red.circle.tool.core.num.ArithmeticUtils; import com.red.circle.tool.core.num.ArithmeticUtils;
import com.red.circle.tool.core.text.StringUtils; import com.red.circle.tool.core.text.StringUtils;
@ -54,6 +57,7 @@ public class PayWebApplicationCommodityV2QueryExe {
private final PayCountryAliasResolver payCountryAliasResolver; private final PayCountryAliasResolver payCountryAliasResolver;
private final PayApplicationCommodityService payApplicationCommodityService; private final PayApplicationCommodityService payApplicationCommodityService;
private final PayCountryChannelDetailsService payCountryChannelDetailsService; private final PayCountryChannelDetailsService payCountryChannelDetailsService;
private final SysCountryCodeClient sysCountryCodeClient;
public ApplicationCommodityCardV2CO execute(PayWebApplicationCommodityCmd cmd) { public ApplicationCommodityCardV2CO execute(PayWebApplicationCommodityCmd cmd) {
@ -65,33 +69,38 @@ public class PayWebApplicationCommodityV2QueryExe {
.toPayApplicationCO(payApplicationService.getById(cmd.getApplicationId())); .toPayApplicationCO(payApplicationService.getById(cmd.getApplicationId()));
ResponseAssert.notNull(CommonErrorCode.NOT_FOUND_RECORD_INFO, application); ResponseAssert.notNull(CommonErrorCode.NOT_FOUND_RECORD_INFO, application);
List<PayApplicationCommodity> payApplicationCommodities = getPayApplicationCommodities(cmd); List<PayApplicationCommodity> payApplicationCommodities = getPayApplicationCommodities(cmd);
if (CollectionUtils.isEmpty(payApplicationCommodities)) { if (CollectionUtils.isEmpty(payApplicationCommodities)) {
return new ApplicationCommodityCardV2CO() return new ApplicationCommodityCardV2CO()
.setApplication(application); .setApplication(application);
} }
// 所有支持的渠道 // LIKEI/Yumi H5 支付方式由本地 JSON 控制商品接口只负责按后台商品配置返回商品
List<PayCountryChannelDetails> countryChannelDetails = payCountryChannelDetailsService // 如果这里继续依赖支付国家渠道详情JSON 已配置但后端渠道未上架时会把商品误过滤为空
.listChannelCode(cmd.getPayCountryId()); List<PayApplicationCommodityCO> commodity = toCommodityCO(payCountry,
payApplicationCommodities);
if (CollectionUtils.isEmpty(commodity)) {
return new ApplicationCommodityCardV2CO()
.setApplication(application);
}
if (isJsonControlledPayment(cmd)) {
return new ApplicationCommodityCardV2CO()
.setApplication(application)
.setCommodity(commodity);
}
// 所有支持的渠道
List<PayCountryChannelDetails> countryChannelDetails = payCountryChannelDetailsService
.listChannelCode(cmd.getPayCountryId());
if (CollectionUtils.isEmpty(countryChannelDetails)) { if (CollectionUtils.isEmpty(countryChannelDetails)) {
return new ApplicationCommodityCardV2CO() return new ApplicationCommodityCardV2CO()
.setApplication(application); .setApplication(application);
} }
// 商品 // 过滤渠道限额范围
List<PayApplicationCommodityCO> commodity = toCommodityCO(payCountry, countryChannelDetails = filterCountryChannelLimit(countryChannelDetails, commodity);
payApplicationCommodities);
if (CollectionUtils.isEmpty(commodity)) {
return new ApplicationCommodityCardV2CO()
.setApplication(application);
}
// 过滤渠道限额范围
countryChannelDetails = filterCountryChannelLimit(countryChannelDetails, commodity);
if (CollectionUtils.isEmpty(countryChannelDetails)) { if (CollectionUtils.isEmpty(countryChannelDetails)) {
return new ApplicationCommodityCardV2CO() return new ApplicationCommodityCardV2CO()
@ -113,11 +122,13 @@ public class PayWebApplicationCommodityV2QueryExe {
.setChannels(getPayChannels(payChannels, countryChannelDetails)); .setChannels(getPayChannels(payChannels, countryChannelDetails));
} }
private List<PayApplicationCommodity> getPayApplicationCommodities( private List<PayApplicationCommodity> getPayApplicationCommodities(
PayWebApplicationCommodityCmd cmd) { PayWebApplicationCommodityCmd cmd) {
if (Objects.equals(cmd.requireReqSysOriginChildEnum(), SysOriginPlatformEnum.YOLO) // Yumi/LIKEI H5 已经先按用户资料拿到国家商品应按国家查regionId 只保留给旧的区域商品链路.
|| StringUtils.isNotBlank(cmd.getRegionId())) { if (Objects.equals(cmd.requireReqSysOriginChildEnum(), SysOriginPlatformEnum.YOLO)
|| (StringUtils.isNotBlank(cmd.getRegionId())
&& StringUtils.isBlank(cmd.getCountryCode()))) {
ResponseAssert ResponseAssert
.isTrue(CommonErrorCode.NOT_SUPPORTED_REGION, StringUtils.isNotBlank(cmd.getRegionId())); .isTrue(CommonErrorCode.NOT_SUPPORTED_REGION, StringUtils.isNotBlank(cmd.getRegionId()));
@ -127,12 +138,16 @@ public class PayWebApplicationCommodityV2QueryExe {
cmd.getType(), cmd.getRegionId()); cmd.getType(), cmd.getRegionId());
} }
return payApplicationCommodityService return payApplicationCommodityService
.listCommodityByApplicationCountry(cmd.getApplicationId(), cmd.getPayCountryId(), .listCommodityByApplicationCountry(cmd.getApplicationId(), cmd.getPayCountryId(),
cmd.getType(), null); cmd.getType(), null);
} }
private List<PayCountryChannelDetails> filterCountryChannelLimit( private boolean isJsonControlledPayment(PayWebApplicationCommodityCmd cmd) {
return Objects.equals(cmd.requireReqSysOriginChildEnum(), SysOriginPlatformEnum.LIKEI);
}
private List<PayCountryChannelDetails> filterCountryChannelLimit(
List<PayCountryChannelDetails> countryChannelDetails, List<PayCountryChannelDetails> countryChannelDetails,
List<PayApplicationCommodityCO> commodity) { List<PayApplicationCommodityCO> commodity) {
Map<Long, PayCountryChannelDetails> countryChannelMaps = Maps.newHashMap(); Map<Long, PayCountryChannelDetails> countryChannelMaps = Maps.newHashMap();
@ -165,7 +180,17 @@ public class PayWebApplicationCommodityV2QueryExe {
} }
private PayCountry getPayCountry(PayWebApplicationCommodityCmd cmd) { private PayCountry getPayCountry(PayWebApplicationCommodityCmd cmd) {
return payCountryAliasResolver.resolveUsablePayCountry(cmd.getPayCountryId()); if (Objects.nonNull(cmd.getPayCountryId()) || StringUtils.isBlank(cmd.getCountryCode())) {
return payCountryAliasResolver.resolveUsablePayCountry(cmd.getPayCountryId());
}
String normalizedCountryCode = CountryCodeAliasUtils.normalizePaymentCountryCode(
cmd.getCountryCode());
SysCountryCodeDTO countryCode = ResponseAssert.requiredSuccess(
sysCountryCodeClient.getByCode(normalizedCountryCode));
ResponseAssert.notNull(CommonErrorCode.NOT_SUPPORTED_REGION, countryCode);
// H5 只传用户国家码这里仅把真实国家转成商品表使用的支付国家ID是否开放支付国家由 H5 JSON 控制.
return payCountryAliasResolver.resolvePayCountryByCountryId(countryCode.getId());
} }
private List<PayChannelDetailsCO> getPayChannels(List<PayChannel> payChannels, private List<PayChannelDetailsCO> getPayChannels(List<PayChannel> payChannels,

View File

@ -26,8 +26,10 @@ public class PayWebOrderStatusQueryExe {
public PayOrderStatusCO execute(PayOrderStatusCmd cmd) { public PayOrderStatusCO execute(PayOrderStatusCmd cmd) {
OrderUserPurchasePay mysqlOrder = getMysqlOrder(cmd.getOrderId()); OrderUserPurchasePay mysqlOrder = getMysqlOrder(cmd.getOrderId());
ResponseAssert.notNull(OrderErrorCode.NO_PURCHASE_RECORD_FOUND, mysqlOrder); ResponseAssert.notNull(OrderErrorCode.NO_PURCHASE_RECORD_FOUND, mysqlOrder);
Long expectedUserId = Objects.nonNull(cmd.getReqUserId()) ? cmd.getReqUserId() : cmd.getUserId();
// 登录态请求继续使用网关注入的 reqUserId公开 H5 轮询没有登录态只允许用下单用户ID校验同一笔订单.
ResponseAssert.isTrue(OrderErrorCode.NO_PURCHASE_RECORD_FOUND, ResponseAssert.isTrue(OrderErrorCode.NO_PURCHASE_RECORD_FOUND,
Objects.equals(mysqlOrder.getUserId(), cmd.getReqUserId())); Objects.nonNull(expectedUserId) && Objects.equals(mysqlOrder.getUserId(), expectedUserId));
InAppPurchaseStatus status = MiFaPayMysqlOrderSupport InAppPurchaseStatus status = MiFaPayMysqlOrderSupport
.toInAppPurchaseStatus(mysqlOrder.getPayStatus()); .toInAppPurchaseStatus(mysqlOrder.getPayStatus());
return new PayOrderStatusCO() return new PayOrderStatusCO()

View File

@ -119,11 +119,12 @@ public class PayWebPlaceAnOrderCmdExe {
+ "&appId=" + application.getId() + "&appId=" + application.getId()
+ "&appStatus=0" + "&appStatus=0"
+ "&appType=" + commodity.getType()); + "&appType=" + commodity.getType());
details.setCancelRedirectUrl(details.getRechargeUrl()); details.setCancelRedirectUrl(details.getRechargeUrl());
details.setNewVersion(cmd.getNewVersion()); details.setNewVersion(cmd.getNewVersion());
details.setRequestIp(IpUtils.getIpAddr()); details.setAppVersion(cmd.getAppVersion());
details.setRequestIp(IpUtils.getIpAddr());
// 记录充值金额
// 记录充值金额
rechargeRecord(details); rechargeRecord(details);
return details; return details;
} }

View File

@ -14,17 +14,15 @@ import com.red.circle.other.inner.endpoint.sys.SysCountryCodeClient;
import com.red.circle.other.inner.model.dto.sys.SysCountryCodeDTO; import com.red.circle.other.inner.model.dto.sys.SysCountryCodeDTO;
import com.red.circle.tool.core.collection.CollectionUtils; import com.red.circle.tool.core.collection.CollectionUtils;
import com.red.circle.tool.core.parse.DataTypeUtils; import com.red.circle.tool.core.parse.DataTypeUtils;
import com.red.circle.tool.core.regex.RegexConstant; import com.red.circle.tool.core.regex.RegexConstant;
import com.red.circle.tool.core.text.StringUtils; import com.red.circle.tool.core.text.StringUtils;
import com.red.circle.other.inner.asserts.user.UserErrorCode; import com.red.circle.other.inner.asserts.user.UserErrorCode;
import com.red.circle.other.inner.model.dto.user.UserProfileDTO; import com.red.circle.other.inner.model.dto.user.UserProfileDTO;
import com.red.circle.other.inner.model.dto.user.reigon.RegionRelationDTO; import com.red.circle.other.inner.endpoint.user.region.UserRegionClient;
import com.red.circle.other.inner.enums.user.RegionRelationGroupEnum; import com.red.circle.other.inner.endpoint.user.user.UserProfileClient;
import com.red.circle.other.inner.endpoint.user.region.RegionRelationClient;
import com.red.circle.other.inner.endpoint.user.region.UserRegionClient;
import com.red.circle.other.inner.endpoint.user.user.UserProfileClient;
import com.red.circle.wallet.inner.endpoint.freight.FreightGoldClient; import com.red.circle.wallet.inner.endpoint.freight.FreightGoldClient;
import java.util.AbstractMap; import java.util.AbstractMap;
import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Objects; import java.util.Objects;
@ -47,9 +45,8 @@ public class UserProfileAndCountryQueryExe {
private final PayCountryAliasResolver payCountryAliasResolver; private final PayCountryAliasResolver payCountryAliasResolver;
private final UserProfileClient userProfileClient; private final UserProfileClient userProfileClient;
private final SysCountryCodeClient sysCountryCodeClient; private final SysCountryCodeClient sysCountryCodeClient;
private final RegionRelationClient regionRelationClient;
public UserProfileAndCountryCO execute(PayWebUserCmd cmd) {
public UserProfileAndCountryCO execute(PayWebUserCmd cmd) {
UserProfileDTO userProfile = getUserProfile(cmd); UserProfileDTO userProfile = getUserProfile(cmd);
@ -64,27 +61,17 @@ public class UserProfileAndCountryQueryExe {
.toWebSiteUseProfileCO(userProfile); .toWebSiteUseProfileCO(userProfile);
webSiteUseProfile.setAccount(userProfile.actualAccount()); webSiteUseProfile.setAccount(userProfile.actualAccount());
List<PayCountry> payCountryList; String regionId = ResponseAssert.requiredSuccess(
userRegionClient.getRegionId(userProfile.getId())
String regionId = ResponseAssert.requiredSuccess( );
userRegionClient.getRegionId(userProfile.getId())
); if (Objects.equals(cmd.getSysOrigin(), SysOriginPlatformEnum.LIKEI.name())) {
return toLikeiUserCountryResult(userProfile, webSiteUseProfile, regionId);
if (Objects.equals(cmd.getSysOrigin(), SysOriginPlatformEnum.LIKEI.name())) { }
ResponseAssert.isTrue(CommonErrorCode.NOT_SUPPORTED_REGION, StringUtils.isNotBlank(regionId));
List<PayCountry> payCountryList = payCountryService.listAllShelf();
List<RegionRelationDTO> regionRelationList = getRegionRelationList(userProfile, regionId); Map<Long, SysCountryCodeDTO> mapCountryCode = getMapCountryCode(payCountryList);
ResponseAssert.notEmpty(CommonErrorCode.NOT_SUPPORTED_REGION, regionRelationList); ResponseAssert.notEmpty(CommonErrorCode.NOT_SUPPORTED_REGION, mapCountryCode);
payCountryList = getPayCountryList(regionRelationList);
ResponseAssert.notEmpty(CommonErrorCode.NOT_SUPPORTED_REGION, payCountryList);
} else {
payCountryList = payCountryService.listAllShelf();
}
Map<Long, SysCountryCodeDTO> mapCountryCode = getMapCountryCode(payCountryList);
ResponseAssert.notEmpty(CommonErrorCode.NOT_SUPPORTED_REGION, mapCountryCode);
Map<Long, PayCountry> compatiblePayCountryMap = getCompatiblePayCountryMap(payCountryList); Map<Long, PayCountry> compatiblePayCountryMap = getCompatiblePayCountryMap(payCountryList);
return new UserProfileAndCountryCO() return new UserProfileAndCountryCO()
@ -105,9 +92,60 @@ public class UserProfileAndCountryQueryExe {
}).filter(Objects::nonNull).collect(Collectors.toList())) }).filter(Objects::nonNull).collect(Collectors.toList()))
.setUserProfile(webSiteUseProfile) .setUserProfile(webSiteUseProfile)
.setRegionId(regionId); .setRegionId(regionId);
} }
private UserProfileAndCountryCO toLikeiUserCountryResult(UserProfileDTO userProfile,
WebSiteUseProfileCO webSiteUseProfile, String regionId) {
SysCountryCodeDTO countryCode = getUserCountryCode(userProfile);
ResponseAssert.notNull(CommonErrorCode.NOT_SUPPORTED_REGION, countryCode);
PayCountry compatiblePayCountry = payCountryAliasResolver
.resolvePayCountryByCountryId(countryCode.getId());
boolean freightUser = existsFreightBalance(userProfile.getId());
webSiteUseProfile
.setFreightUser(freightUser)
// Yumi H5 先确认用户身份再用这里返回的商品类型查询后台商品避免币商误买普通金币商品后入普通金币账户.
.setCommodityType(freightUser ? PayApplicationCommodityType.FREIGHT_GOLD.name()
: PayApplicationCommodityType.GOLD.name());
// LIKEI/Yumi 的支付国家开放由 H5 JSON 控制这里仅返回用户真实国家商品接口再按国家码查该国商品.
PayCountryCO payCountryCO = new PayCountryCO()
.setId(Objects.nonNull(compatiblePayCountry) ? compatiblePayCountry.getId() : null)
.setPayCountryId(Objects.nonNull(compatiblePayCountry) ? compatiblePayCountry.getId() : null)
.setCountryId(countryCode.getId())
.setCurrency(Objects.nonNull(compatiblePayCountry) ? compatiblePayCountry.getCurrency() : null)
.setCountry(countryCode);
return new UserProfileAndCountryCO()
.setCountryList(Collections.singletonList(payCountryCO))
.setUserProfile(webSiteUseProfile)
.setRegionId(regionId);
}
private boolean existsFreightBalance(Long userId) {
return Boolean.TRUE.equals(ResponseAssert.requiredSuccess(
freightGoldClient.existsBalance(userId)
));
}
private SysCountryCodeDTO getUserCountryCode(UserProfileDTO userProfile) {
if (Objects.nonNull(userProfile.getCountryId())) {
return ResponseAssert.requiredSuccess(
sysCountryCodeClient.getById(userProfile.getCountryId())
);
}
if (StringUtils.isNotBlank(userProfile.getCountryCode())) {
return ResponseAssert.requiredSuccess(
sysCountryCodeClient.getByCode(userProfile.getCountryCode())
);
}
return null;
}
private UserProfileDTO getUserProfile(PayWebUserCmd cmd) { private UserProfileDTO getUserProfile(PayWebUserCmd cmd) {
if (StringUtils.isNotBlank(cmd.getAccount()) && StringUtils.isNotBlank(cmd.getSysOrigin())) { if (StringUtils.isNotBlank(cmd.getAccount()) && StringUtils.isNotBlank(cmd.getSysOrigin())) {
@ -146,18 +184,4 @@ public class UserProfileAndCountryQueryExe {
(left, right) -> left)); (left, right) -> left));
} }
private List<RegionRelationDTO> getRegionRelationList(UserProfileDTO userProfile, }
String regionId) {
return ResponseAssert.requiredSuccess(
regionRelationClient
.listByRegionId(regionId, userProfile.getOriginSys(),
RegionRelationGroupEnum.OPEN_PAY_COUNTRY)
);
}
private List<PayCountry> getPayCountryList(List<RegionRelationDTO> regionRelationList) {
return payCountryService
.listByIds(regionRelationList.stream().map(RegionRelationDTO::getRelationId).collect(
Collectors.toSet()));
}
}

View File

@ -115,9 +115,13 @@ public class PayPlaceAnOrderDetailsV2 implements Serializable {
*/ */
private Boolean newVersion; private Boolean newVersion;
/** /**
* 请求ip信息. * 本次充值请求的App版本.
*/ */
private String appVersion;
/**
* 请求ip信息.
*/
private String requestIp; private String requestIp;
public String getFactoryChannel() { public String getFactoryChannel() {
@ -156,6 +160,7 @@ public class PayPlaceAnOrderDetailsV2 implements Serializable {
.put("countryCode", this.getCountryCode()) .put("countryCode", this.getCountryCode())
.put("amount", this.getAmount().toString()) .put("amount", this.getAmount().toString())
.put("currency", this.getCurrency()) .put("currency", this.getCurrency())
.put("appVersion", Objects.toString(this.getAppVersion(), ""))
.build(); .build();
} }

View File

@ -1,7 +1,6 @@
package com.red.circle.order.app.command.pay.web.strategy; package com.red.circle.order.app.command.pay.web.strategy;
import com.google.api.client.util.Maps; import com.red.circle.component.pay.paymax.PayMaxUtils;
import com.red.circle.component.pay.paymax.PayMaxUtils;
import com.red.circle.framework.core.request.RequestClientEnum; import com.red.circle.framework.core.request.RequestClientEnum;
import com.red.circle.framework.web.props.EnvProperties; import com.red.circle.framework.web.props.EnvProperties;
import com.red.circle.order.app.common.MiFaPayMysqlOrderSupport; import com.red.circle.order.app.common.MiFaPayMysqlOrderSupport;
@ -19,13 +18,18 @@ import com.red.circle.order.infra.database.rds.service.order.OrderUserPurchasePa
import com.red.circle.order.inner.model.enums.inapp.InAppPurchaseStatus; import com.red.circle.order.inner.model.enums.inapp.InAppPurchaseStatus;
import com.red.circle.tool.core.date.TimestampUtils; import com.red.circle.tool.core.date.TimestampUtils;
import com.red.circle.tool.core.http.RcHttpClient; import com.red.circle.tool.core.http.RcHttpClient;
import com.red.circle.tool.core.json.JacksonUtils; import com.red.circle.tool.core.json.JacksonUtils;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.util.Arrays; import java.util.Collection;
import java.util.List; import java.util.HashMap;
import lombok.RequiredArgsConstructor; import java.util.Arrays;
import lombok.extern.slf4j.Slf4j; import java.util.List;
import org.springframework.stereotype.Service; import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
/** /**
* @author pengliang on 2022/11/2 * @author pengliang on 2022/11/2
@ -104,9 +108,9 @@ public class PayPlaceAnOrderService {
.setNoticeData(response.extra()) .setNoticeData(response.extra())
.setCreateTime(TimestampUtils.now()) .setCreateTime(TimestampUtils.now())
)) ))
.setMetadata(Maps.newHashMap()) .setMetadata(param.createMetadata())
.setVersion(0) .setVersion(0)
); );
return response; return response;
} }
@ -121,10 +125,31 @@ public class PayPlaceAnOrderService {
* @param targetCurrency 目标币种如CNY * @param targetCurrency 目标币种如CNY
* @return 汇率值 * @return 汇率值
*/ */
public BigDecimal getRealTimeExchangeRate(String baseCurrency, String targetCurrency) { public BigDecimal getRealTimeExchangeRate(String baseCurrency, String targetCurrency) {
try { return getRealTimeExchangeRates(baseCurrency, List.of(targetCurrency)).get(targetCurrency);
QuoteQueryParam param = new QuoteQueryParam(); }
QuoteQueryParam.QuoteQueryData data = new QuoteQueryParam.QuoteQueryData();
public Map<String, BigDecimal> getRealTimeExchangeRates(String baseCurrency,
Collection<String> targetCurrencies) {
Map<String, BigDecimal> result = new HashMap<>();
Optional.ofNullable(targetCurrencies).orElseGet(List::of).stream()
.filter(Objects::nonNull)
.map(String::trim)
.filter(currency -> !currency.isEmpty())
.distinct()
.forEach(targetCurrency -> {
BigDecimal rate = queryRealTimeExchangeRate(baseCurrency, targetCurrency);
if (Objects.nonNull(rate)) {
result.put(targetCurrency, rate);
}
});
return result;
}
private BigDecimal queryRealTimeExchangeRate(String baseCurrency, String targetCurrency) {
try {
QuoteQueryParam param = new QuoteQueryParam();
QuoteQueryParam.QuoteQueryData data = new QuoteQueryParam.QuoteQueryData();
// 币种对格式基础币种+目标币种如USDCNY // 币种对格式基础币种+目标币种如USDCNY
String ccyPair = baseCurrency + targetCurrency; String ccyPair = baseCurrency + targetCurrency;
@ -164,10 +189,10 @@ public class PayPlaceAnOrderService {
return null; return null;
} }
} catch (Exception e) { } catch (Exception e) {
log.error("Error occurred while querying real-time exchange rate from {} to {}", log.error("Error occurred while querying real-time exchange rate from {} to {}",
baseCurrency, targetCurrency, e); baseCurrency, targetCurrency, e);
return null; return null;
} }
} }
} }

View File

@ -8,8 +8,9 @@ import com.red.circle.framework.core.asserts.ResponseAssert;
import com.red.circle.framework.core.response.CommonErrorCode; import com.red.circle.framework.core.response.CommonErrorCode;
import com.red.circle.framework.dto.ResultResponse; import com.red.circle.framework.dto.ResultResponse;
import com.red.circle.framework.web.props.EnvProperties; import com.red.circle.framework.web.props.EnvProperties;
import com.red.circle.mq.business.model.event.pay.BuySuccessEvent; import com.red.circle.mq.business.model.event.pay.BuySuccessEvent;
import com.red.circle.order.app.command.pay.web.strategy.PayEnv; import com.red.circle.order.app.common.task.RechargeSuccessEventPublisher;
import com.red.circle.order.app.command.pay.web.strategy.PayEnv;
import com.red.circle.order.domain.gateway.UserRechargeCountGateway; import com.red.circle.order.domain.gateway.UserRechargeCountGateway;
import com.red.circle.order.infra.database.mongo.order.InAppPurchaseCollectionReceiptService; import com.red.circle.order.infra.database.mongo.order.InAppPurchaseCollectionReceiptService;
import com.red.circle.order.infra.database.mongo.order.InAppPurchaseDetailsService; import com.red.circle.order.infra.database.mongo.order.InAppPurchaseDetailsService;
@ -76,9 +77,10 @@ public class InAppPurchaseCommon {
private final OrderPurchaseHistoryService orderPurchaseHistoryService; private final OrderPurchaseHistoryService orderPurchaseHistoryService;
private final InAppPurchaseCollectionReceiptService inAppPurchaseCollectionReceiptService; private final InAppPurchaseCollectionReceiptService inAppPurchaseCollectionReceiptService;
private final UserLevelClient userLevelClient; private final UserLevelClient userLevelClient;
private final PropsActivityClient propsActivityClient; private final PropsActivityClient propsActivityClient;
private final UserOneTimeTaskClient userOneTimeTaskClient; private final UserOneTimeTaskClient userOneTimeTaskClient;
private final UserProfileClient userProfileClient; private final UserProfileClient userProfileClient;
private final RechargeSuccessEventPublisher rechargeSuccessEventPublisher;
public void inAppPurchaseReceipt(InAppPurchaseDetails order) { public void inAppPurchaseReceipt(InAppPurchaseDetails order) {
inAppPurchaseReceipt(order, inAppPurchaseReceipt(order,
@ -192,13 +194,15 @@ public class InAppPurchaseCommon {
userSvipClient.incrIntegral(order.getAcceptUserId(), userSvipClient.incrIntegral(order.getAcceptUserId(),
amount.longValue()); amount.longValue());
// 被邀请人充值邀请人获得佣金 // 被邀请人充值邀请人获得佣金
inviteUserClient.processRechargeCommission(amount, inviteUserClient.processRechargeCommission(amount,
order.getSysOrigin(), order.getAcceptUserId()); order.getSysOrigin(), order.getAcceptUserId());
// 发送首充奖励 rechargeSuccessEventPublisher.reportPaymentRecharge(order);
completedFirstCharge(order.getAcceptUserId(), order.getSysOrigin());
} // 发送首充奖励
completedFirstCharge(order.getAcceptUserId(), order.getSysOrigin());
}
private void completedFirstCharge(Long userId, String sysOrigin) { private void completedFirstCharge(Long userId, String sysOrigin) {
UserProfileDTO userProfileDTO = userProfileClient.getByUserId(userId).getBody(); UserProfileDTO userProfileDTO = userProfileClient.getByUserId(userId).getBody();

View File

@ -15,7 +15,9 @@ import java.math.BigDecimal;
import java.math.RoundingMode; import java.math.RoundingMode;
import java.sql.Timestamp; import java.sql.Timestamp;
import java.util.Collections; import java.util.Collections;
import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.Objects; import java.util.Objects;
/** /**
@ -61,6 +63,7 @@ public final class MiFaPayMysqlOrderSupport {
.setFactoryCode(param.getFactoryCode()) .setFactoryCode(param.getFactoryCode())
.setFactoryOrderId(Objects.toString(response.getTradeNo(), "")) .setFactoryOrderId(Objects.toString(response.getTradeNo(), ""))
.setReferenceId(Objects.toString(param.getApplicationId(), "")) .setReferenceId(Objects.toString(param.getApplicationId(), ""))
.setAppVersion(Objects.toString(param.getAppVersion(), ""))
.setReceiptType(Objects.toString(param.getReceiptType(), .setReceiptType(Objects.toString(param.getReceiptType(),
InAppPurchaseReceiptType.PAYMENT.name())) InAppPurchaseReceiptType.PAYMENT.name()))
.setPaymentChannel(Objects.toString(param.getChannelCode(), "")) .setPaymentChannel(Objects.toString(param.getChannelCode(), ""))
@ -121,7 +124,8 @@ public final class MiFaPayMysqlOrderSupport {
.setReason(order.getReason()) .setReason(order.getReason())
.setCreateTime(order.getCreateTime()) .setCreateTime(order.getCreateTime())
.setUpdateTime(order.getUpdateTime()) .setUpdateTime(order.getUpdateTime())
.setCreateUser(order.getCreateUser()); .setCreateUser(order.getCreateUser())
.setMetadata(metadata(order));
} }
public static InAppPurchaseStatus toInAppPurchaseStatus(String payStatus) { public static InAppPurchaseStatus toInAppPurchaseStatus(String payStatus) {
@ -162,6 +166,14 @@ public final class MiFaPayMysqlOrderSupport {
return InAppPurchaseReceiptType.PAYMENT; return InAppPurchaseReceiptType.PAYMENT;
} }
private static Map<String, String> metadata(OrderUserPurchasePay order) {
Map<String, String> metadata = new HashMap<>();
if (StringUtils.isNotBlank(order.getAppVersion())) {
metadata.put("appVersion", order.getAppVersion());
}
return metadata;
}
private static BigDecimal defaultDecimal(BigDecimal value) { private static BigDecimal defaultDecimal(BigDecimal value) {
return value != null ? value : BigDecimal.ZERO; return value != null ? value : BigDecimal.ZERO;
} }

View File

@ -0,0 +1,242 @@
package com.red.circle.order.app.common.task;
import com.red.circle.component.mq.MessageEvent;
import com.red.circle.mq.business.model.event.pay.BuySuccessEvent;
import com.red.circle.mq.business.model.event.pay.RechargeSuccessEvent;
import com.red.circle.mq.rocket.business.service.MessageSenderService;
import com.red.circle.mq.rocket.business.streams.RechargeSuccessSink;
import com.red.circle.order.infra.database.mongo.order.entity.InAppPurchaseDetails;
import com.red.circle.order.infra.database.mongo.order.entity.assist.InAppPurchaseProduct;
import com.red.circle.order.inner.model.enums.PayApplicationCommodityType;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.sql.Timestamp;
import java.time.Instant;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
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 RechargeSuccessEventPublisher {
private static final String GOOGLE = "GOOGLE";
private static final String APPLE = "APPLE";
private static final String THIRD_PARTY = "THIRD_PARTY";
private static final String UNKNOWN = "UNKNOWN";
private final ObjectProvider<MessageSenderService> messageSenderServiceProvider;
@Value("${recharge.success.mq.enabled:true}")
private boolean mqEnabled;
@Value("${recharge.success.mq.topic:RC_DEFAULT_APP_ORDINARY}")
private String mqTopic;
@Value("${recharge.success.mq.tag:" + RechargeSuccessSink.TAG + "}")
private String mqTag;
public RechargeSuccessEventPublisher(ObjectProvider<MessageSenderService> messageSenderServiceProvider) {
this.messageSenderServiceProvider = messageSenderServiceProvider;
}
public void reportAppPurchaseRecharge(BuySuccessEvent event) {
if (Objects.isNull(event)) {
return;
}
Long userId = event.getAcceptUserId();
Long purchaseHistoryId = event.getPurchaseHistoryId();
BigDecimal amountUsd = Objects.nonNull(event.getProductConfig())
? event.getProductConfig().getUnitPrice()
: null;
Long amountCents = toCents(amountUsd);
if (Objects.isNull(userId) || Objects.isNull(purchaseHistoryId) || !positive(amountCents)) {
return;
}
String sourceOrderId = String.valueOf(purchaseHistoryId);
String payPlatform = normalizePayPlatform(defaultIfBlank(event.getPayPlatform(), event.getPlatform()));
String paymentMethod = classifyPaymentMethod(payPlatform);
String appVersion = defaultIfBlank(event.getAppVersion(), "");
String productId = Objects.nonNull(event.getProductConfig())
? defaultIfBlank(event.getProductConfig().getProductId(), event.getOrigin())
: defaultIfBlank(event.getOrigin(), "");
String googleProductId = Objects.equals(GOOGLE, payPlatform) ? productId : "";
Map<String, Object> payload = new HashMap<>();
payload.put("payerUserId", event.getUserId());
payload.put("acceptUserId", userId);
payload.put("purchaseHistoryId", purchaseHistoryId);
payload.put("candyQuantity", event.getCandyQuantity());
payload.put("platform", event.getPlatform());
payload.put("payPlatform", event.getPayPlatform());
payload.put("appVersion", event.getAppVersion());
payload.put("productId", productId);
payload.put("googleProductId", googleProductId);
publish(new RechargeSuccessEvent()
.setEventId("RECHARGE_SUCCESS:" + paymentMethod + ":" + sourceOrderId)
.setSysOrigin(event.getSysOrigin())
.setUserId(userId)
.setAmountCents(amountCents)
.setAmountUsd(formatAmount(amountUsd))
.setCurrency("USD")
.setPayPlatform(payPlatform)
.setPaymentMethod(paymentMethod)
.setProductId(productId)
.setGoogleProductId(googleProductId)
.setAppVersion(appVersion)
.setSourceOrderId(sourceOrderId)
.setOrderId(sourceOrderId)
.setOccurredAt(toInstantString(event.getBuyDateTime()))
.setPayload(payload));
}
public void reportPaymentRecharge(InAppPurchaseDetails order) {
if (Objects.isNull(order) || !order.receiptTypeEqPayment()) {
return;
}
InAppPurchaseProduct product = order.firstProduct();
if (Objects.isNull(product) || !PayApplicationCommodityType.GOLD.eq(product.getCode())) {
return;
}
String payPlatform = normalizePayPlatform(Objects.nonNull(order.getFactory()) ? order.getFactory().getFactoryCode() : "");
String paymentMethod = classifyPaymentMethod(payPlatform);
BigDecimal amountUsd = firstPositive(order.getAmountUsd(), product.getAmountUsd());
Long amountCents = toCents(amountUsd);
if (Objects.isNull(order.getAcceptUserId()) || isBlank(order.getId()) || !positive(amountCents)) {
return;
}
Map<String, Object> payload = new HashMap<>();
payload.put("trackId", order.getTrackId());
payload.put("sourceOrderId", order.getId());
payload.put("orderId", order.getOrderId());
payload.put("factoryCode", payPlatform);
payload.put("factoryChannelCode", Objects.nonNull(order.getFactory()) ? order.getFactory().getFactoryChannelCode() : null);
payload.put("platform", Objects.nonNull(order.getFactory()) ? order.getFactory().getPlatform() : null);
payload.put("productCode", product.getCode());
payload.put("productContent", product.getContent());
String appVersion = appVersionFromMetadata(order);
payload.put("appVersion", appVersion);
String productId = defaultIfBlank(order.getTrackId(), "");
String googleProductId = Objects.equals(GOOGLE, payPlatform) ? productId : "";
payload.put("productId", productId);
payload.put("googleProductId", googleProductId);
publish(new RechargeSuccessEvent()
.setEventId("RECHARGE_SUCCESS:" + paymentMethod + ":" + payPlatform + ":" + order.getId())
.setSysOrigin(order.getSysOrigin())
.setUserId(order.getAcceptUserId())
.setAmountCents(amountCents)
.setAmountUsd(formatAmount(amountUsd))
.setCurrency(defaultIfBlank(order.getCurrency(), "USD"))
.setPayPlatform(payPlatform)
.setPaymentMethod(paymentMethod)
.setProductId(productId)
.setGoogleProductId(googleProductId)
.setAppVersion(appVersion)
.setSourceOrderId(order.getId())
.setOrderId(defaultIfBlank(order.getOrderId(), order.getId()))
.setOccurredAt(firstTime(order.getPurchaseDateMs(), order.getUpdateTime(), order.getCreateTime()))
.setPayload(payload));
}
private void publish(RechargeSuccessEvent event) {
if (!mqEnabled) {
return;
}
try {
MessageSenderService senderService = messageSenderServiceProvider.getIfAvailable();
if (Objects.isNull(senderService)) {
log.warn("Recharge success mq sender missing, skip event report. eventId={}", event.getEventId());
return;
}
senderService.sendEventMessage(MessageEvent.builder()
.consumeId(event.getEventId())
.topicString(defaultIfBlank(mqTopic, "RC_DEFAULT_APP_ORDINARY"))
.tag(defaultIfBlank(mqTag, RechargeSuccessSink.TAG))
.body(event)
.build());
} catch (Exception e) {
log.warn("Recharge success mq report error. eventId={}", event.getEventId(), e);
}
}
private String normalizePayPlatform(String value) {
String normalized = Objects.toString(value, "").trim().toUpperCase();
return normalized.isEmpty() ? UNKNOWN : normalized;
}
private String classifyPaymentMethod(String payPlatform) {
if (Objects.equals(GOOGLE, payPlatform)) {
return GOOGLE;
}
if (Objects.equals(APPLE, payPlatform)) {
return APPLE;
}
if (Objects.equals(UNKNOWN, payPlatform)) {
return UNKNOWN;
}
return THIRD_PARTY;
}
private String appVersionFromMetadata(InAppPurchaseDetails order) {
if (Objects.isNull(order) || Objects.isNull(order.getMetadata())) {
return "";
}
return defaultIfBlank(order.getMetadata().get("appVersion"), "");
}
private Long toCents(BigDecimal amount) {
if (Objects.isNull(amount) || amount.compareTo(BigDecimal.ZERO) <= 0) {
return null;
}
return amount.multiply(BigDecimal.valueOf(100)).setScale(0, RoundingMode.HALF_UP).longValue();
}
private BigDecimal firstPositive(BigDecimal first, BigDecimal second) {
if (Objects.nonNull(first) && first.compareTo(BigDecimal.ZERO) > 0) {
return first;
}
return second;
}
private boolean positive(Long value) {
return Objects.nonNull(value) && value > 0;
}
private String toInstantString(Timestamp timestamp) {
return Objects.nonNull(timestamp) ? timestamp.toInstant().toString() : Instant.now().toString();
}
private String firstTime(Timestamp first, Timestamp second, Timestamp third) {
if (Objects.nonNull(first)) {
return first.toInstant().toString();
}
if (Objects.nonNull(second)) {
return second.toInstant().toString();
}
if (Objects.nonNull(third)) {
return third.toInstant().toString();
}
return Instant.now().toString();
}
private String formatAmount(BigDecimal amount) {
return Objects.isNull(amount) ? null : amount.stripTrailingZeros().toPlainString();
}
private String defaultIfBlank(String value, String fallback) {
return isBlank(value) ? fallback : value.trim();
}
private boolean isBlank(String value) {
return Objects.toString(value, "").trim().isEmpty();
}
}

View File

@ -17,6 +17,7 @@ import com.red.circle.framework.dto.ResultResponse;
import com.red.circle.mq.business.model.event.pay.BuySuccessEvent; import com.red.circle.mq.business.model.event.pay.BuySuccessEvent;
import com.red.circle.mq.business.model.event.pay.PurchaseProductEvent; import com.red.circle.mq.business.model.event.pay.PurchaseProductEvent;
import com.red.circle.mq.rocket.business.streams.BuySuccessSink; import com.red.circle.mq.rocket.business.streams.BuySuccessSink;
import com.red.circle.order.app.common.task.RechargeSuccessEventPublisher;
import com.red.circle.order.app.common.task.TaskCenterGoClient; import com.red.circle.order.app.common.task.TaskCenterGoClient;
import com.red.circle.other.inner.endpoint.activity.CumulativeRechargeClient; import com.red.circle.other.inner.endpoint.activity.CumulativeRechargeClient;
import com.red.circle.other.inner.endpoint.activity.PropsActivityClient; import com.red.circle.other.inner.endpoint.activity.PropsActivityClient;
@ -71,6 +72,7 @@ public class BuySuccessListener implements MessageListener {
private final CumulativeRechargeClient cumulativeRechargeClient; private final CumulativeRechargeClient cumulativeRechargeClient;
private final RedisService redisService; private final RedisService redisService;
private final TaskCenterGoClient taskCenterGoClient; private final TaskCenterGoClient taskCenterGoClient;
private final RechargeSuccessEventPublisher rechargeSuccessEventPublisher;
@Override @Override
public Action consume(ConsumerMessage message) { public Action consume(ConsumerMessage message) {
@ -133,6 +135,7 @@ public class BuySuccessListener implements MessageListener {
eventBody.getPlatform(), eventBody.getPlatform(),
eventBody.getPayPlatform(), eventBody.getPayPlatform(),
Objects.nonNull(eventBody.getBuyDateTime()) ? eventBody.getBuyDateTime().toInstant() : null); Objects.nonNull(eventBody.getBuyDateTime()) ? eventBody.getBuyDateTime().toInstant() : null);
rechargeSuccessEventPublisher.reportAppPurchaseRecharge(eventBody);
userExpandClient.updatePurchasingToTrue(eventBody.getUserId()); userExpandClient.updatePurchasingToTrue(eventBody.getUserId());

View File

@ -44,12 +44,22 @@ public class WebSiteUseProfileCO extends ClientObject {
*/ */
private String account; private String account;
/** /**
* 归属平台. * 归属平台.
*/ */
private String sysOrigin; private String sysOrigin;
public static WebSiteUseProfileCO toWebSiteUseProfileCO(UserProfileDTO userProfile) { /**
* 是否存在货运金币账户Yumi H5 用这个身份决定展示普通金币还是货运金币商品.
*/
private Boolean freightUser;
/**
* 本次 H5 充值应该请求的商品类型普通用户是 GOLD货运/币商用户是 FREIGHT_GOLD.
*/
private String commodityType;
public static WebSiteUseProfileCO toWebSiteUseProfileCO(UserProfileDTO userProfile) {
if (userProfile == null) { if (userProfile == null) {
return null; return null;
} }

View File

@ -23,4 +23,9 @@ public class PayOrderStatusCmd extends AppExtCommand {
*/ */
@NotBlank(message = "orderId required.") @NotBlank(message = "orderId required.")
private String orderId; private String orderId;
/**
* H5公开支付页没有登录态轮询状态时必须用下单用户ID继续做订单归属校验.
*/
private Long userId;
} }

Some files were not shown because too many files have changed in this diff Show More