Merge branch 'main' into test

This commit is contained in:
hy001 2026-05-23 12:20:10 +08:00
commit dfb71f55a8
17 changed files with 359 additions and 66 deletions

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.CountryDashboardRechargeDetailQryCmd;
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.CountryDashboardService;
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 CountryDashboardService countryDashboardService;
private final CountryDashboardDailyMetricService countryDashboardDailyMetricService;
private final CountryDashboardBackfillLockService countryDashboardBackfillLockService;
private final CountryDashboardGameMetricService countryDashboardGameMetricService;
@Value("${red-circle.country-dashboard.query-enabled:true}")
@ -116,14 +118,21 @@ public class DatavRestController extends BaseController {
}
LocalDate parsedStartDate = LocalDate.parse(startDate);
LocalDate parsedEndDate = LocalDate.parse(endDate);
List<String> refreshedTimezones = countryDashboardDailyMetricService.refreshRange(
parsedStartDate, parsedEndDate, sysOrigin, statTimezones);
Map<String, Object> refreshed = countryDashboardBackfillLockService.call(() -> {
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(
"status", "success",
"startDate", parsedStartDate.toString(),
"endDate", parsedEndDate.toString(),
"sysOrigin", sysOrigin,
"statTimezones", refreshedTimezones);
"statTimezones", refreshed);
}
@PostMapping("/country-dashboard/game-metrics/backfill")
@ -137,8 +146,9 @@ public class DatavRestController extends BaseController {
}
LocalDate parsedStartDate = LocalDate.parse(startDate);
LocalDate parsedEndDate = LocalDate.parse(endDate);
List<String> refreshedTimezones = countryDashboardGameMetricService.refreshRange(
parsedStartDate, parsedEndDate, sysOrigin, statTimezones);
List<String> refreshedTimezones = countryDashboardBackfillLockService.call(
() -> countryDashboardGameMetricService.refreshRange(
parsedStartDate, parsedEndDate, sysOrigin, statTimezones));
return Map.of(
"status", "success",
"startDate", parsedStartDate.toString(),

View File

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

View File

@ -6,6 +6,7 @@ import com.red.circle.console.app.service.app.datav.CountryDashboardGameMetricSe
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;
@ -14,6 +15,7 @@ import org.springframework.stereotype.Component;
*/
@Slf4j
@Component
@ConditionalOnProperty(name = "red-circle.country-dashboard.refresh-enabled", havingValue = "true")
@RequiredArgsConstructor
public class CountryDashboardDailyMetricTask {

View File

@ -37,7 +37,8 @@ import org.springframework.stereotype.Service;
@RequiredArgsConstructor
public class ResidentCpActivityServiceImpl implements ResidentCpActivityService {
private static final int CP_LEVEL_COUNT = 5;
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";
@ -53,8 +54,8 @@ public class ResidentCpActivityServiceImpl implements ResidentCpActivityService
String normalizedSysOrigin = normalizeSysOrigin(sysOrigin);
List<SysCpCabinConfigDTO> configs = listCpCabinConfigs(normalizedSysOrigin);
List<ResidentCpLevelConfigCO> levels = new ArrayList<>(CP_LEVEL_COUNT);
for (int level = 1; level <= CP_LEVEL_COUNT; level++) {
SysCpCabinConfigDTO config = level <= configs.size() ? configs.get(level - 1) : null;
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()
@ -76,8 +77,8 @@ public class ResidentCpActivityServiceImpl implements ResidentCpActivityService
for (ResidentCpLevelConfigSaveCmd levelCmd : sortedLevels(cmd.getLevels())) {
SysCpCabinConfigDTO config = Optional.ofNullable(levelCmd.getCabinConfigId())
.map(configById::get)
.orElseGet(() -> levelCmd.getLevel() <= configs.size()
? configs.get(levelCmd.getLevel() - 1)
.orElseGet(() -> levelCmd.getLevel() < configs.size()
? configs.get(levelCmd.getLevel())
: null);
boolean exists = Objects.nonNull(config) && Objects.nonNull(config.getId());
if (!exists) {
@ -133,14 +134,14 @@ public class ResidentCpActivityServiceImpl implements ResidentCpActivityService
ResponseAssert.notBlank(ResponseErrorCode.REQUEST_PARAMETER_ERROR, cmd.getSysOrigin());
List<ResidentCpLevelConfigSaveCmd> levels = sortedLevels(cmd.getLevels());
ResponseAssert.isTrue(ResponseErrorCode.REQUEST_PARAMETER_ERROR,
"CP levels must contain exactly 5 items.", levels.size() == CP_LEVEL_COUNT);
"CP levels must contain exactly 6 items.", levels.size() == CP_LEVEL_COUNT);
Set<Integer> levelSet = new HashSet<>();
long previousRequiredValue = -1L;
for (ResidentCpLevelConfigSaveCmd level : levels) {
ResponseAssert.isTrue(ResponseErrorCode.REQUEST_PARAMETER_ERROR,
"CP level must be between 1 and 5.",
Objects.nonNull(level.getLevel()) && level.getLevel() >= 1
&& level.getLevel() <= CP_LEVEL_COUNT && levelSet.add(level.getLevel()));
"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);

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

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

View File

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

View File

@ -72,6 +72,7 @@ public class CountryDashboardServiceImpl implements CountryDashboardService {
private final CountryDashboardDAO countryDashboardDAO;
private final MongoTemplate mongoTemplate;
private volatile boolean rechargeDetailSchemaEnsured;
@Override
public CountryDashboardCO query(CountryDashboardQryCmd cmd) {
@ -82,15 +83,6 @@ public class CountryDashboardServiceImpl implements CountryDashboardService {
condition.periodType, condition.startTime, condition.endTime,
condition.countryKeyword, condition.statTimezone, condition.sysOrigin));
mergeMongo(rows, condition, listGiftConsume(condition),
(row, amount) -> row.setGiftConsume(add(row.getGiftConsume(), amount)));
mergeMongo(rows, condition, listLuckyGiftAnchorShare(condition),
(row, amount) -> row.setLuckyGiftAnchorShare(add(row.getLuckyGiftAnchorShare(), amount)));
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()
.peek(this::calculateFormulaFields)
.sorted(Comparator.comparing(CountryDashboardMetricCO::getPeriodKey,
@ -294,18 +286,45 @@ public class CountryDashboardServiceImpl implements CountryDashboardService {
@Override
public PageResult<CountryDashboardRechargeDetailCO> pageRechargeDetails(
CountryDashboardRechargeDetailQryCmd cmd) {
ensureRechargeDetailSchema();
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());
limit = Math.min(100, limit);
QueryCondition condition = QueryCondition.of(cmd);
String countryCodes = trimToNull(cmd == null ? null : cmd.getCountryCodes());
long total = safeLong(countryDashboardDAO.countRechargeDetails(
condition.startTime, condition.endTime, condition.countryKeyword, countryCodes,
condition.sysOrigin, condition.storageTimezoneOffset, condition.statTimezoneOffset));
List<CountryDashboardRechargeDetailCO> records = total <= 0 ? List.of()
: countryDashboardDAO.listRechargeDetails(
condition.startTime, condition.endTime, condition.countryKeyword, countryCodes,
condition.sysOrigin, condition.storageTimezoneOffset, condition.statTimezoneOffset,
(cursor - 1) * limit, limit)
.stream()
.map(this::toRechargeDetailCO)
.toList();
PageResult<CountryDashboardRechargeDetailCO> result = new PageResult<>();
result.setCurrent(cursor);
result.setSize(limit);
result.setTotal(0L);
result.setRecords(List.of());
result.setTotal(total);
result.setRecords(records);
return result;
}
private void ensureRechargeDetailSchema() {
if (rechargeDetailSchemaEnsured) {
return;
}
synchronized (this) {
if (rechargeDetailSchemaEnsured) {
return;
}
countryDashboardDAO.createRechargeDetailTable();
rechargeDetailSchemaEnsured = true;
}
}
private CountryDashboardRechargeDetailCO toRechargeDetailCO(
CountryDashboardRechargeDetailDTO dto) {
return new CountryDashboardRechargeDetailCO()

View File

@ -0,0 +1,51 @@
package com.red.circle.console.app.service.app.activity;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
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.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());
}
private static SysCpCabinConfigDTO config(Long id, Long sort, Long unlockValue) {
return new SysCpCabinConfigDTO()
.setId(id)
.setName("CP " + sort + "")
.setCabinUnlockValue(unlockValue)
.setSort(sort);
}
}

View File

@ -30,8 +30,16 @@ public interface CountryDashboardDAO extends BaseDAO<IUserBaseInfo> {
int createGamePeriodUserMetricTable();
int createRechargeDetailTable();
int ensurePeriodMetricTimezoneSchema();
Integer getDashboardBackfillLock(
@Param("lockName") String lockName,
@Param("timeoutSeconds") Integer timeoutSeconds);
Integer releaseDashboardBackfillLock(@Param("lockName") String lockName);
List<CountryDashboardMetricDTO> listNewUsers(
@Param("periodType") String periodType,
@Param("startTime") LocalDateTime startTime,

View File

@ -3,6 +3,14 @@
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<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">
CREATE TABLE IF NOT EXISTS `country_dashboard_daily_metric` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键',
@ -181,6 +189,28 @@
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='国家数据大屏游戏维度周期用户去重明细'
</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">
SET @country_dashboard_schema_lock = GET_LOCK('country_dashboard_period_timezone_schema', 60);
SET @current_schema = DATABASE();
@ -1423,41 +1453,68 @@
<select id="countRechargeDetails" resultType="java.lang.Long">
SELECT COUNT(1)
FROM (
<include refid="RechargeDetailSource"/>
) t
INNER JOIN user_base_info u ON u.id = t.userId
FROM country_dashboard_recharge_detail t
WHERE 1 = 1
<include refid="UserWhere"/>
<include refid="CountryCodesWhere"/>
<if test="startTime != null">
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 id="listRechargeDetails"
resultType="com.red.circle.console.infra.database.rds.dto.datav.CountryDashboardRechargeDetailDTO">
SELECT
t.recordId,
t.sourceType,
t.sourceChannel,
t.userId,
t.record_id AS recordId,
t.source_type AS sourceType,
t.source_channel AS sourceChannel,
t.user_id AS userId,
u.account AS userAccount,
u.user_nickname AS userNickname,
NULLIF(u.country_code, '') AS countryCode,
NULLIF(u.country_name, '') AS countryName,
t.country_code AS countryCode,
t.country_name AS countryName,
t.amount,
t.coinQuantity,
t.coin_quantity AS coinQuantity,
t.remark,
DATE_FORMAT(
CONVERT_TZ(t.createTime, #{storageTimezoneOffset}, #{statTimezoneOffset}),
CONVERT_TZ(t.create_time, #{storageTimezoneOffset}, #{statTimezoneOffset}),
'%Y-%m-%d %H:%i:%s'
) AS createTime
FROM (
<include refid="RechargeDetailSource"/>
) t
INNER JOIN user_base_info u ON u.id = t.userId
FROM country_dashboard_recharge_detail t
LEFT JOIN user_base_info u ON u.id = t.user_id
WHERE 1 = 1
<include refid="UserWhere"/>
<include refid="CountryCodesWhere"/>
ORDER BY t.createTime DESC, t.recordId DESC
<if test="startTime != null">
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>
ORDER BY t.create_time DESC, t.record_id DESC
LIMIT #{limit} OFFSET #{offset}
</select>

View File

@ -29,7 +29,7 @@ import org.springframework.stereotype.Component;
public class UserCpRightsConfigQueryExe {
private static final String RELATIONSHIP_STATUS_NONE = "NONE";
private static final int CP_RIGHTS_LEVEL_COUNT = 5;
private static final int CP_RIGHTS_LEVEL_COUNT = 6;
private final CpCabinConfigService cpCabinConfigService;
private final CpRelationshipService cpRelationshipService;
@ -106,7 +106,7 @@ public class UserCpRightsConfigQueryExe {
: configs;
return IntStream.range(0, CP_RIGHTS_LEVEL_COUNT)
.mapToObj(index -> toLevel(relationType, getLevelConfig(resolvedConfigs, index),
index + 1, hasRelationship,
index, hasRelationship,
intimacyValue))
.toList();
}

View File

@ -0,0 +1,92 @@
package com.red.circle.other.app.command.user.query;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import com.red.circle.framework.core.dto.ReqSysOrigin;
import com.red.circle.other.app.dto.clientobject.user.relation.cp.CpRightsConfigCO;
import com.red.circle.other.app.dto.cmd.user.relation.cp.CpRightsConfigQueryCmd;
import com.red.circle.other.infra.database.rds.entity.user.user.CpCabinConfig;
import com.red.circle.other.infra.database.rds.entity.user.user.CpRelationship;
import com.red.circle.other.infra.database.rds.entity.user.user.CpValue;
import com.red.circle.other.infra.database.rds.service.user.user.CpCabinConfigService;
import com.red.circle.other.infra.database.rds.service.user.user.CpRelationshipService;
import com.red.circle.other.infra.database.rds.service.user.user.CpValueService;
import com.red.circle.other.infra.enums.user.user.CpRelationshipStatus;
import java.math.BigDecimal;
import java.util.List;
import org.junit.jupiter.api.Test;
class UserCpRightsConfigQueryExeTest {
@Test
void formedRelationRightsLevelShouldStartFromZero() {
Fixture fixture = new Fixture();
for (String relationType : List.of("CP", "BROTHER", "SISTERS")) {
CpRightsConfigCO result = fixture.exe.execute(cmd(relationType));
assertEquals(relationType, result.getRelationType());
assertEquals("NORMAL", result.getRelationshipStatus());
assertEquals(0L, result.getIntimacyValue());
assertEquals(6, result.getLevels().size());
assertEquals(List.of(0, 1, 2, 3, 4, 5),
result.getLevels().stream().map(CpRightsConfigCO.LevelCO::getLevel).toList());
assertEquals(relationType + "_CABIN_LEVEL_0",
result.getLevels().get(0).getRights().get(0).getRightKey());
assertTrue(result.getLevels().get(0).getUnlocked());
assertTrue(result.getLevels().get(0).getRights().get(0).getUnlocked());
}
}
private static CpRightsConfigQueryCmd cmd(String relationType) {
CpRightsConfigQueryCmd cmd = new CpRightsConfigQueryCmd();
cmd.setReqUserId(1001L);
cmd.setReqSysOrigin(ReqSysOrigin.of("LIKEI"));
cmd.setRelationType(relationType);
return cmd;
}
private static class Fixture {
private final CpCabinConfigService cpCabinConfigService = mock(CpCabinConfigService.class);
private final CpRelationshipService cpRelationshipService = mock(CpRelationshipService.class);
private final CpValueService cpValueService = mock(CpValueService.class);
private final UserCpRightsConfigQueryExe exe = new UserCpRightsConfigQueryExe(
cpCabinConfigService, cpRelationshipService, cpValueService);
private Fixture() {
for (String relationType : List.of("CP", "BROTHER", "SISTERS")) {
when(cpRelationshipService.getByUserIdOrCpUserId(1001L, relationType))
.thenReturn(relationship(relationType));
}
when(cpValueService.getById(3001L)).thenReturn(new CpValue().setCpVal(BigDecimal.ZERO));
when(cpCabinConfigService.getCpRightsLevelConfig("LIKEI")).thenReturn(List.of(
levelConfig("Level Zero", 0L),
levelConfig("Level One", 100L),
levelConfig("Level Two", 200L),
levelConfig("Level Three", 300L),
levelConfig("Level Four", 400L),
levelConfig("Level Five", 500L)
));
}
private static CpRelationship relationship(String relationType) {
return new CpRelationship()
.setUserId(1001L)
.setCpUserId(2001L)
.setCpValId(3001L)
.setRelationType(relationType)
.setStatus(CpRelationshipStatus.NORMAL.name());
}
private static CpCabinConfig levelConfig(String name, Long cabinUnlockValue) {
return new CpCabinConfig()
.setName(name)
.setCabinUnlockValue(cabinUnlockValue)
.setCover("https://example.com/" + name + ".png");
}
}
}

View File

@ -62,7 +62,7 @@ public class CpRightsConfigCO extends ClientObject {
public static class LevelCO {
/**
* Level number.
* Level number, starts from 0.
*/
private Integer level;

View File

@ -24,7 +24,7 @@ public class CpCabinConfigServiceImpl extends
BaseServiceImpl<CpCabinConfigDAO, CpCabinConfig> implements
CpCabinConfigService {
private static final int CP_RIGHTS_LEVEL_COUNT = 5;
private static final int CP_RIGHTS_LEVEL_COUNT = 6;
@Override
public List<CpCabinConfig> getCpCabinConfig(String sysOriginName) {

View File

@ -414,8 +414,8 @@ public class ActivitySourceGroupGatewayImpl implements ActivitySourceGroupGatewa
private Long firstVipResourceId(VipLevelConfig vipConfig) {
return Stream.of(
vipConfig.getBadgeResourceId(),
vipConfig.getShortBadgeResourceId(),
vipConfig.getBadgeResourceId(),
vipConfig.getAvatarFrameResourceId(),
vipConfig.getEntryEffectResourceId(),
vipConfig.getChatBubbleResourceId(),
@ -431,12 +431,17 @@ public class ActivitySourceGroupGatewayImpl implements ActivitySourceGroupGatewa
private String resolveVipCover(VipLevelConfig vipConfig,
Map<Long, PropsSourceRecord> vipCoverMap) {
PropsSourceRecord sourceRecord = vipCoverMap.get(firstVipResourceId(vipConfig));
if (Objects.nonNull(sourceRecord) && Objects.nonNull(sourceRecord.getCover())) {
if (Objects.nonNull(sourceRecord) && Objects.nonNull(sourceRecord.getCover())
&& !sourceRecord.getCover().isBlank()) {
return sourceRecord.getCover();
}
if (Objects.nonNull(sourceRecord) && Objects.nonNull(sourceRecord.getSourceUrl())
&& !sourceRecord.getSourceUrl().isBlank()) {
return sourceRecord.getSourceUrl();
}
return Stream.of(
vipConfig.getBadgeUrl(),
vipConfig.getShortBadgeUrl(),
vipConfig.getBadgeUrl(),
vipConfig.getAvatarFrameUrl(),
vipConfig.getEntryEffectUrl(),
vipConfig.getChatBubbleUrl(),

View File

@ -392,8 +392,8 @@ public class PropsActivityRewardGroupClientServiceImpl implements
private Long firstVipResourceId(VipLevelConfig vipConfig) {
return Stream.of(
vipConfig.getBadgeResourceId(),
vipConfig.getShortBadgeResourceId(),
vipConfig.getBadgeResourceId(),
vipConfig.getAvatarFrameResourceId(),
vipConfig.getEntryEffectResourceId(),
vipConfig.getChatBubbleResourceId(),
@ -422,8 +422,8 @@ public class PropsActivityRewardGroupClientServiceImpl implements
return sourceRecord.getSourceUrl();
}
return Stream.of(
vipConfig.getBadgeUrl(),
vipConfig.getShortBadgeUrl(),
vipConfig.getBadgeUrl(),
vipConfig.getAvatarFrameUrl(),
vipConfig.getEntryEffectUrl(),
vipConfig.getChatBubbleUrl(),