修复幸运礼物和系统推送

This commit is contained in:
hy001 2026-04-23 12:24:48 +08:00
parent 4396f34b3b
commit 1fd196094d
17 changed files with 1247 additions and 427 deletions

View File

@ -83,10 +83,19 @@ public class SysPushRecordLogQryCmd extends PageCommand {
*/
private Integer failQuantity;
/**
* 成功数量
*/
private Integer successQuantity;
}
/**
* 成功数量
*/
private Integer successQuantity;
/**
* 开始时间.
*/
private Long startTime;
/**
* 结束时间.
*/
private Long endTime;
}

View File

@ -1,24 +1,26 @@
package com.red.circle.other.inner.model.cmd.sys;
import java.io.Serial;
import java.io.Serializable;
import lombok.Data;
import lombok.experimental.Accessors;
/**
* <p>
* 推送任务
package com.red.circle.other.inner.model.cmd.sys;
import com.red.circle.framework.core.dto.PageCommand;
import java.io.Serial;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
/**
* <p>
* 推送任务
* </p>
*
* @author pengshigang
* @since 2022-10-20
*/
@Data
@Accessors(chain = true)
public class SysPushTaskQryCmd implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
*/
@Data
@EqualsAndHashCode(callSuper = true)
@Accessors(chain = true)
public class SysPushTaskQryCmd extends PageCommand {
@Serial
private static final long serialVersionUID = 1L;
/**
* 0.下架 1.上架.

View File

@ -1,65 +1,75 @@
package com.red.circle.console.adapter.app.sys;
import com.red.circle.console.app.service.app.sys.SysPushService;
import com.red.circle.framework.web.controller.BaseController;
import lombok.RequiredArgsConstructor;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
package com.red.circle.console.adapter.app.sys;
import com.red.circle.console.app.dto.clienobject.sys.SysPushRecordLogCO;
import com.red.circle.console.app.dto.clienobject.sys.SysPushTaskCO;
import com.red.circle.console.app.service.app.sys.SysPushService;
import com.red.circle.console.infra.annotations.OpsOperationReqLog;
import com.red.circle.framework.dto.PageResult;
import com.red.circle.framework.web.controller.BaseController;
import com.red.circle.other.inner.model.cmd.sys.CreateRecordCmd;
import com.red.circle.other.inner.model.cmd.sys.SysPushRecordLogQryCmd;
import com.red.circle.other.inner.model.cmd.sys.SysPushTaskCmd;
import com.red.circle.other.inner.model.cmd.sys.SysPushTaskQryCmd;
import lombok.RequiredArgsConstructor;
import org.springframework.http.MediaType;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
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;
/**
* 后台推送 前端控制器
*
* @author pengliang
* @since 2020-02-07
*/
@RequiredArgsConstructor
@RestController
@RequestMapping(value = "/push", produces = MediaType.APPLICATION_JSON_VALUE)
public class PushRestController extends BaseController {
private final SysPushService sysPushService;
/*
*//**
* 后台推送分页列表.
*//*
@GetMapping("/page")
public PageResult<SysPushRecordLogCO> pageSysPushBack(SysPushRecordLogQryCmd query) {
return sysPushService.pageLogs(query);
}
@OpsOperationReqLog("发送推送信息")
@PostMapping
public void saveAndPush(@RequestBody CreateRecordCmd param) {
param.setCreateUser(param.getReqUserId());
// 设置代理上线去除
// System.setProperty("proxyHost", "localhost");
// System.setProperty("proxyPort", "10809");
sysPushService.create(param);
}
@OpsOperationReqLog("保存定时消息推送任务")
@PostMapping("/task/save")
public void saveTask(@RequestBody @Validated SysPushTaskCmd param) {
param.setCreateUser(param.getReqUserId());
sysPushService.saveTask(param);
}
*//**
* 定时推送任务列表.
*//*
@GetMapping("/task/page")
public PageResult<SysPushTaskCO> pageTask(SysPushTaskQryCmd query) {
return sysPushService.pageTask(query);
}
*//**
* 定时推送任务列表.
*//*
@GetMapping("/task/delete/{id}")
public void deletePushTask(@PathVariable("id") Long id) {
sysPushService.deletePushTask(id);
}*/
}
*/
@Validated
@RequiredArgsConstructor
@RestController
@RequestMapping(value = "/push", produces = MediaType.APPLICATION_JSON_VALUE)
public class PushRestController extends BaseController {
private final SysPushService sysPushService;
/**
* 后台推送分页列表.
*/
@GetMapping("/page")
public PageResult<SysPushRecordLogCO> pageSysPushBack(SysPushRecordLogQryCmd query) {
return sysPushService.pageLogs(query);
}
@OpsOperationReqLog("发送推送信息")
@PostMapping
public void saveAndPush(@RequestBody CreateRecordCmd param) {
param.setCreateUser(param.getReqUserId());
sysPushService.create(param);
}
@OpsOperationReqLog("保存定时消息推送任务")
@PostMapping("/task/save")
public void saveTask(@RequestBody @Validated SysPushTaskCmd param) {
param.setCreateUser(param.getReqUserId());
sysPushService.saveTask(param);
}
/**
* 定时推送任务列表.
*/
@GetMapping("/task/page")
public PageResult<SysPushTaskCO> pageTask(SysPushTaskQryCmd query) {
return sysPushService.pageTask(query);
}
/**
* 删除定时推送任务.
*/
@GetMapping("/task/delete/{id}")
public void deletePushTask(@PathVariable("id") Long id) {
sysPushService.deletePushTask(id);
}
}

View File

@ -1,255 +1,419 @@
package com.red.circle.console.app.service.app.sys;
import com.red.circle.console.infra.database.rds.service.admin.UserService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
/**
* @author pengliang
* @since 2020/3/6 15:53
*/
@Slf4j
@RequiredArgsConstructor
@Service
public class SysPushServiceImpl implements SysPushService {
private final UserService userService;
/*private final ISysPushTaskService sysPushTaskService;
private final IUserBaseInfoService userBaseInfoService;
private final ISysPushRecordLogService sysPushRecordLogService;
private final GoogleFirebasePushService googleFirebasePushService;
private final ISysPushUserRecordLogService sysPushUserRecordLogService;
private final IUserLatestMobileDeviceService userLatestMobileDeviceService;
@Override
public void create(CreateRecordCmd createRecordParam) {
if (StringUtils.isBlank(createRecordParam.getSysOrigin())) {
return;
}
if (createRecordParam.isGoogleDevice()) {
pushGoogle(createRecordParam);
}
}
@Override
public PageResult<SysPushRecordLogCO> pageLogs(SysPushRecordLogQryCmd query) {
IPage<SysPushRecordLog> sysPushRecordLogPage = sysPushRecordLogService.query()
.eq(StringUtils.isNotBlank(query.getBusinessScene()), SysPushRecordLog::getBusinessScene,
query.getBusinessScene())
.eq(StringUtils.isNotBlank(query.getPlatform()), SysPushRecordLog::getPlatform,
query.getPlatform())
.eq(StringUtils.isNotBlank(query.getDeviceType()), SysPushRecordLog::getDeviceType,
query.getDeviceType())
.eq(StringUtils.isNotBlank(query.getSysOrigin()), SysPushRecordLog::getSysOrigin,
query.getSysOrigin())
.ge(Objects.nonNull(query.getStartCreateDate()), SysPushRecordLog::getCreateTime,
query.getStartCreateDate())
.le(Objects.nonNull(query.getEndCreateDate()), SysPushRecordLog::getCreateTime,
query.getEndCreateDate())
.eq(Objects.nonNull(query.getPushStatus()) && query.isSuccess(),
SysPushRecordLog::getPushStatus, ResultStatusEnum.SUCCESS)
.in(Objects.nonNull(query.getPushStatus()) && !query.isSuccess(),
SysPushRecordLog::getPushStatus, ResultStatusEnum.getFailKeys())
.orderByDesc(SysPushRecordLog::getId)
.page(query);
if (CollectionUtil.isEmpty(sysPushRecordLogPage.getRecords())) {
return sysPushRecordLogPage
.convert(sysPushRecordLog -> sysPushRecordLog.convert(SysPushRecordLogCO.class));
}
Map<Long, User> userMap = userService.mapBackUser(getBackUserIds(sysPushRecordLogPage));
Map<Long, UserBaseInfo> userBaseInfoMap = userBaseInfoService
.mapByIds(getAppUserIds(sysPushRecordLogPage));
return sysPushRecordLogPage.convert(sysPushRecordLog -> {
SysPushRecordLogCO sysPushRecordLogVO = sysPushRecordLog.convert(SysPushRecordLogCO.class);
sysPushRecordLogVO
.setBusinessScene(MessageBusinessSceneEnum.getDesc(sysPushRecordLog.getBusinessScene()));
sysPushRecordLogVO.setPushStatus(ResultStatusEnum.getDesc(sysPushRecordLog.getPushStatus()));
if (PushOriginEnum.isBack(sysPushRecordLog.getOrigin())) {
User user = userMap.get(sysPushRecordLog.getCreateUser());
if (Objects.nonNull(user)) {
sysPushRecordLogVO.setSendUserName(user.getNickname());
}
return sysPushRecordLogVO;
}
UserBaseInfo userBaseInfo = userBaseInfoMap.get(sysPushRecordLog.getCreateUser());
if (Objects.nonNull(userBaseInfo)) {
sysPushRecordLogVO.setSendUserName(userBaseInfo.getUserNickname());
}
return sysPushRecordLogVO;
});
}
@Override
public PageResult<SysPushTaskCO> pageTask(SysPushTaskQryCmd query) {
return sysPushTaskService.pageTask(query);
}
@Override
public void saveTask(SysPushTaskCmd param) {
sysPushTaskService.saveTask(param);
}
@Override
public void deletePushTask(Long id) {
sysPushTaskService.deletePushTask(id);
}
private List<Long> getBackUserIds(PageResult<SysPushRecordLogCO> sysPushRecordLogPage) {
return sysPushRecordLogPage.getRecords().stream().filter(sysPushRecordLog ->
PushOriginEnum.isBack(sysPushRecordLog.getOrigin()) && sysPushRecordLog.getCreateUser() > 0
).map(SysPushRecordLog::getCreateUser).collect(Collectors.toList());
}
private Set<Long> getAppUserIds(IPage<SysPushRecordLog> sysPushRecordLogPage) {
return sysPushRecordLogPage.getRecords().stream().filter(sysPushRecordLog ->
PushOriginEnum.isApp(sysPushRecordLog.getOrigin()) && sysPushRecordLog.getCreateUser() > 0
).map(SysPushRecordLog::getCreateUser).collect(Collectors.toSet());
}
*//**
* 推送谷歌
*//*
private void pushGoogle(CreateRecordCmd createRecordParam) {
List<UserLatestMobileDevicePushDTO> users = userLatestMobileDeviceService
.listPushUserByCondition(createRecordParam);
if (CollectionUtils.isEmpty(users)) {
return;
}
ThreadPoolManager.getInstance().execute("pushGoogle", () -> {
List<String> deviceIds = users.stream().map(UserLatestMobileDevicePushDTO::getDeviceId)
.collect(Collectors.toList());
//推送并处理结果
processPush(createRecordParam, users, googleFirebasePushService.sendMulticast(
PushParam.builder()
.sysOrigin(SysOriginPlatformEnum.valueOf(createRecordParam.getSysOrigin()))
.androidDevices(deviceIds)
.title(createRecordParam.getTitle())
.body(createRecordParam.getContent())
.build()
));
});
}
private void processPush(CreateRecordCmd createRecordParam,
List<UserLatestMobileDevicePushDTO> users,
FirebaseResponses responses) {
if (Objects.isNull(responses) || Objects.equals(responses.getFailCount(), users.size())) {
//全部失败
allPushFail(createRecordParam, users);
return;
}
createRecordParam.setPushStatus(ResultStatusEnum.SUCCESS.name());
createRecordParam.setFailQuantity(responses.getFailCount());
createRecordParam.setSuccessQuantity(responses.getSuccessCount());
addPushLog(createRecordParam);
//处理成功或失败
processPushResult(createRecordParam.getId(), users, responses);
}
private void processPushResult(Long pushId, List<UserLatestMobileDevicePushDTO> users,
FirebaseResponses responses) {
List<String> successTokens = responses.getSuccessDevicesString();
List<String> failTokens = responses.getFailDevicesString();
List<SysPushUserRecordLog> saveData = Lists.newArrayList();
Timestamp date = TimestampUtils.now();
for (UserLatestMobileDevicePushDTO user : users) {
SysPushUserRecordLog pushUserRecord = new SysPushUserRecordLog();
pushUserRecord.setId(IdWorkerUtils.getId());
pushUserRecord.setPushId(pushId);
pushUserRecord.setPushToken(user.getDeviceId());
pushUserRecord.setUserId(user.getUserId());
pushUserRecord.setCreateTime(date);
pushUserRecord.setUpdateTime(date);
Boolean isSuccess = isSuccess(successTokens, failTokens, user);
if (Objects.isNull(isSuccess)) {
continue;
}
pushUserRecord.setSuccess(isSuccess);
saveData.add(pushUserRecord);
}
List<List<SysPushUserRecordLog>> logList = Lists.partition(saveData, 200);
logList.forEach(sysPushUserRecordLogService::saveBatch);
}
private Boolean isSuccess(List<String> successTokens, List<String> failTokens,
UserLatestMobileDevicePushDTO user) {
for (String successToken : successTokens) {
if (Objects.equals(user.getDeviceId(), successToken)) {
return Boolean.TRUE;
}
}
for (String failToken : failTokens) {
if (Objects.equals(user.getDeviceId(), failToken)) {
return Boolean.FALSE;
}
}
return null;
}
private void allPushFail(CreateRecordCmd createRecordParam,
List<UserLatestMobileDevicePushDTO> users) {
createRecordParam.setPushStatus(ResultStatusEnum.FAIL.name());
createRecordParam.setFailQuantity(users.size());
createRecordParam.setSuccessQuantity(0);
addPushLog(createRecordParam);
Timestamp date = TimestampUtils.now();
List<SysPushUserRecordLog> logs = users.stream().map(user -> {
SysPushUserRecordLog pushUserRecord = new SysPushUserRecordLog();
pushUserRecord.setPushId(createRecordParam.getId());
pushUserRecord.setPushToken(user.getDeviceId());
pushUserRecord.setUserId(user.getUserId());
pushUserRecord.setSuccess(Boolean.FALSE);
pushUserRecord.setCreateTime(date);
pushUserRecord.setUpdateTime(date);
pushUserRecord.setCreateUser(createRecordParam.getCreateUser());
return pushUserRecord;
}).collect(Collectors.toList());
List<List<SysPushUserRecordLog>> logList = Lists.partition(logs, 200);
logList.forEach(sysPushUserRecordLogService::saveBatch);
}
private void addPushLog(CreateRecordCmd newPushDTO) {
SysPushRecordLog sysPushRecordLog = newPushDTO.convert(SysPushRecordLog.class);
sysPushRecordLog.setOrigin(PushOriginEnum.BACK.name());
sysPushRecordLogService.save(sysPushRecordLog);
newPushDTO.setId(sysPushRecordLog.getId());
}
*/
}
package com.red.circle.console.app.service.app.sys;
import com.red.circle.common.business.core.enums.SysOriginPlatformEnum;
import com.red.circle.console.app.dto.clienobject.sys.SysPushRecordLogCO;
import com.red.circle.console.app.dto.clienobject.sys.SysPushTaskCO;
import com.red.circle.console.infra.database.rds.entity.sys.SysPushRecordLog;
import com.red.circle.console.infra.database.rds.service.admin.UserService;
import com.red.circle.console.infra.database.rds.service.sys.SysPushRecordLogService;
import com.red.circle.external.inner.endpoint.push.GoogleFirebasePushClient;
import com.red.circle.external.inner.model.cmd.message.google.GooglePushCmd;
import com.red.circle.framework.core.asserts.ResponseAssert;
import com.red.circle.framework.core.dto.PageCommand;
import com.red.circle.framework.core.response.CommonErrorCode;
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.endpoint.user.device.RegisterDeviceClient;
import com.red.circle.other.inner.enums.sys.MessageBusinessSceneEnum;
import com.red.circle.other.inner.model.cmd.sys.CreateRecordCmd;
import com.red.circle.other.inner.model.cmd.sys.SysPushRecordLogQryCmd;
import com.red.circle.other.inner.model.cmd.sys.SysPushTaskCmd;
import com.red.circle.other.inner.model.cmd.sys.SysPushTaskQryCmd;
import com.red.circle.other.inner.model.cmd.user.device.PageLatestMobileDeviceQryCmd;
import com.red.circle.other.inner.model.dto.user.UserLatestMobileDeviceHistoryDTO;
import com.red.circle.tool.core.collection.CollectionUtils;
import com.red.circle.tool.core.date.TimestampUtils;
import com.red.circle.tool.core.text.StringUtils;
import java.sql.Timestamp;
import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
/**
* 后台 Push 服务.
*
* <p>旧的推送实现已经失效这里按当前可用的设备查询与推送客户端重建最小闭环
* 发送 Push记录 Push 日志为未迁移的定时任务返回明确的不可用状态</p>
*/
@Slf4j
@RequiredArgsConstructor
@Service
public class SysPushServiceImpl implements SysPushService {
private static final int DEFAULT_PAGE_LIMIT = 20;
private static final int DEVICE_PAGE_LIMIT = 500;
private static final String PUSH_ORIGIN_BACK = "BACK";
private static final String PUSH_STATUS_SUCCESS = "SUCCESS";
private static final String PUSH_STATUS_FAIL = "FAIL";
private static final Set<String> PUSH_FAIL_STATUSES = Set.of(
"UNKNOWN",
PUSH_STATUS_FAIL,
"IOS_FAIL",
"ANDROID_FAIL");
private final UserService userService;
private final RegisterDeviceClient registerDeviceClient;
private final GoogleFirebasePushClient googleFirebasePushClient;
private final SysPushRecordLogService sysPushRecordLogService;
@Override
public void create(CreateRecordCmd createRecordParam) {
validateCreateParam(createRecordParam);
List<UserLatestMobileDeviceHistoryDTO> targetDevices = listTargetDevices(createRecordParam);
if (CollectionUtils.isEmpty(targetDevices)) {
ResponseAssert.failure(ResponseErrorCode.REQUEST_PARAMETER_ERROR,
"No matching push devices.");
}
SysPushRecordLog pushLog = buildPushLog(createRecordParam);
try {
pushGoogle(createRecordParam, targetDevices);
pushLog.setPushStatus(PUSH_STATUS_SUCCESS);
pushLog.setSuccessQuantity(targetDevices.size());
pushLog.setFailQuantity(0);
} catch (RuntimeException ex) {
pushLog.setPushStatus(PUSH_STATUS_FAIL);
pushLog.setSuccessQuantity(0);
pushLog.setFailQuantity(targetDevices.size());
log.warn("Push broadcast failed, sysOrigin={}, title={}", createRecordParam.getSysOrigin(),
createRecordParam.getTitle(), ex);
throw ex;
} finally {
ResponseAssert.isTrue(CommonErrorCode.OPERATING_FAILURE, !sysPushRecordLogService.save(pushLog));
createRecordParam.setId(pushLog.getId());
}
}
@Override
public PageResult<SysPushRecordLogCO> pageLogs(SysPushRecordLogQryCmd query) {
PageResult<SysPushRecordLog> page = sysPushRecordLogService.query()
.eq(StringUtils.isNotBlank(query.getBusinessScene()), SysPushRecordLog::getBusinessScene,
query.getBusinessScene())
.eq(StringUtils.isNotBlank(query.getPlatform()), SysPushRecordLog::getPlatform,
query.getPlatform())
.eq(StringUtils.isNotBlank(query.getDeviceType()), SysPushRecordLog::getDeviceType,
query.getDeviceType())
.eq(StringUtils.isNotBlank(query.getSysOrigin()), SysPushRecordLog::getSysOrigin,
query.getSysOrigin())
.ge(Objects.nonNull(query.getStartTime()), SysPushRecordLog::getCreateTime,
new Timestamp(query.getStartTime()))
.le(Objects.nonNull(query.getEndTime()), SysPushRecordLog::getCreateTime,
new Timestamp(query.getEndTime()))
.eq(isSuccessStatusQuery(query.getPushStatus()), SysPushRecordLog::getPushStatus,
PUSH_STATUS_SUCCESS)
.in(isFailStatusQuery(query.getPushStatus()), SysPushRecordLog::getPushStatus,
PUSH_FAIL_STATUSES)
.orderByDesc(SysPushRecordLog::getId)
.page(ensurePageQuery(query));
if (CollectionUtils.isEmpty(page.getRecords())) {
return page.convert(this::toPushLogCO);
}
Map<Long, String> sendUserNameMap = userService.mapBackUserNickname(
page.getRecords().stream()
.map(SysPushRecordLog::getCreateUser)
.filter(Objects::nonNull)
.collect(Collectors.toSet()));
return page.convert(record -> toPushLogCO(record, sendUserNameMap));
}
@Override
public PageResult<SysPushTaskCO> pageTask(SysPushTaskQryCmd query) {
return PageResult.newPageResult(resolvePageLimit(ensurePageQuery(query)));
}
@Override
public void saveTask(SysPushTaskCmd param) {
ResponseAssert.failure(CommonErrorCode.API_DEPRECATED,
"Push task is not available in the migrated admin yet.");
}
@Override
public void deletePushTask(Long id) {
ResponseAssert.failure(CommonErrorCode.API_DEPRECATED,
"Push task is not available in the migrated admin yet.");
}
private void validateCreateParam(CreateRecordCmd createRecordParam) {
ResponseAssert.notBlank(ResponseErrorCode.REQUEST_PARAMETER_ERROR, createRecordParam.getTitle());
ResponseAssert.notBlank(ResponseErrorCode.REQUEST_PARAMETER_ERROR,
createRecordParam.getContent());
ResponseAssert.notBlank(ResponseErrorCode.REQUEST_PARAMETER_ERROR,
createRecordParam.getSysOrigin());
ResponseAssert.notBlank(ResponseErrorCode.REQUEST_PARAMETER_ERROR,
createRecordParam.getDeviceType());
ResponseAssert.notNull(ResponseErrorCode.REQUEST_PARAMETER_ERROR,
SysOriginPlatformEnum.toEnum(createRecordParam.getSysOrigin()));
ResponseAssert.isTrue(ResponseErrorCode.REQUEST_PARAMETER_ERROR,
!createRecordParam.isGoogleDevice());
}
private void pushGoogle(CreateRecordCmd createRecordParam,
List<UserLatestMobileDeviceHistoryDTO> targetDevices) {
GooglePushCmd pushCmd = new GooglePushCmd()
.setSysOrigin(SysOriginPlatformEnum.toEnum(createRecordParam.getSysOrigin()))
.setTitle(createRecordParam.getTitle())
.setBody(createRecordParam.getContent());
for (UserLatestMobileDeviceHistoryDTO device : targetDevices) {
if (isIosPlatform(device.getRequestClient())) {
pushCmd.addIosDevice(device.getDeviceId());
continue;
}
pushCmd.addAndroidDevice(device.getDeviceId());
}
ResponseAssert.isTrue(ResponseErrorCode.REQUEST_PARAMETER_ERROR,
!pushCmd.isNotEmptyAndroidDevices() && !pushCmd.isNotEmptyIosDevices());
ResponseAssert.checkSuccess(googleFirebasePushClient.broadcast(pushCmd));
}
private List<UserLatestMobileDeviceHistoryDTO> listTargetDevices(CreateRecordCmd createRecordParam) {
List<UserLatestMobileDeviceHistoryDTO> devices = createRecordParam.isFixedUserIdSend()
? listFixedTargetDevices(createRecordParam)
: listBroadcastTargetDevices(createRecordParam);
return distinctByDeviceId(devices);
}
private List<UserLatestMobileDeviceHistoryDTO> listFixedTargetDevices(
CreateRecordCmd createRecordParam) {
List<Long> fixedUserIds = createRecordParam.getQualifiedFixedUserIdList();
if (CollectionUtils.isEmpty(fixedUserIds)) {
return CollectionUtils.newArrayList();
}
List<UserLatestMobileDeviceHistoryDTO> devices = new ArrayList<>(fixedUserIds.size());
for (Long userId : fixedUserIds) {
PageLatestMobileDeviceQryCmd query = buildDeviceQuery(createRecordParam);
query.setUserId(userId);
PageResult<UserLatestMobileDeviceHistoryDTO> page = ResponseAssert.requiredSuccess(
registerDeviceClient.pageLatestMobileDevice(query));
if (CollectionUtils.isEmpty(page.getRecords())) {
continue;
}
UserLatestMobileDeviceHistoryDTO latestDevice = page.getRecords().iterator().next();
if (matchesDevice(latestDevice, createRecordParam, true)) {
devices.add(latestDevice);
}
}
return devices;
}
private List<UserLatestMobileDeviceHistoryDTO> listBroadcastTargetDevices(
CreateRecordCmd createRecordParam) {
List<UserLatestMobileDeviceHistoryDTO> devices = new ArrayList<>();
PageLatestMobileDeviceQryCmd query = buildDeviceQuery(createRecordParam);
PageQuery pageQuery = query.getPageQuery();
while (true) {
PageResult<UserLatestMobileDeviceHistoryDTO> page = ResponseAssert.requiredSuccess(
registerDeviceClient.pageLatestMobileDevice(query));
Collection<UserLatestMobileDeviceHistoryDTO> records = page.getRecords();
if (CollectionUtils.isEmpty(records)) {
break;
}
records.stream()
.filter(device -> matchesDevice(device, createRecordParam, false))
.forEach(devices::add);
if (records.size() < resolvePageLimit(pageQuery)) {
break;
}
pageQuery.setCursor(pageQuery.getCursor() + 1);
}
return devices;
}
private PageLatestMobileDeviceQryCmd buildDeviceQuery(CreateRecordCmd createRecordParam) {
PageLatestMobileDeviceQryCmd query = new PageLatestMobileDeviceQryCmd();
query.setSysOrigin(createRecordParam.getSysOrigin());
PageQuery pageQuery = new PageQuery();
pageQuery.setCursor(1);
pageQuery.setLimit(DEVICE_PAGE_LIMIT);
query.setPageQuery(pageQuery);
return query;
}
private List<UserLatestMobileDeviceHistoryDTO> distinctByDeviceId(
List<UserLatestMobileDeviceHistoryDTO> devices) {
if (CollectionUtils.isEmpty(devices)) {
return CollectionUtils.newArrayList();
}
Map<String, UserLatestMobileDeviceHistoryDTO> distinctMap = new LinkedHashMap<>();
for (UserLatestMobileDeviceHistoryDTO device : devices) {
if (Objects.isNull(device) || StringUtils.isBlank(device.getDeviceId())) {
continue;
}
distinctMap.putIfAbsent(device.getDeviceId(), device);
}
return new ArrayList<>(distinctMap.values());
}
private boolean matchesDevice(UserLatestMobileDeviceHistoryDTO device,
CreateRecordCmd createRecordParam, boolean ignoreLanguage) {
if (Objects.isNull(device) || StringUtils.isBlank(device.getDeviceId())) {
return false;
}
if (StringUtils.isNotBlank(createRecordParam.getDeviceType())
&& !equalsIgnoreCase(createRecordParam.getDeviceType(), device.getDeviceType())) {
return false;
}
if (StringUtils.isNotBlank(createRecordParam.getPlatform())
&& !isSamePlatform(createRecordParam.getPlatform(), device.getRequestClient())) {
return false;
}
return ignoreLanguage || StringUtils.isBlank(createRecordParam.getLanguage())
|| equalsIgnoreCase(createRecordParam.getLanguage(), device.getLanguage());
}
private boolean isSuccessStatusQuery(String pushStatus) {
String normalized = normalizeStatusQuery(pushStatus);
return Objects.equals(normalized, "0") || Objects.equals(normalized,
PUSH_STATUS_SUCCESS);
}
private boolean isFailStatusQuery(String pushStatus) {
String normalized = normalizeStatusQuery(pushStatus);
return Objects.equals(normalized, "1")
|| (StringUtils.isNotBlank(normalized) && !Objects.equals(normalized,
PUSH_STATUS_SUCCESS));
}
private String normalizeStatusQuery(String pushStatus) {
if (StringUtils.isBlank(pushStatus)) {
return "";
}
String normalized = pushStatus.trim();
if (Objects.equals(normalized, "0")) {
return "0";
}
if (Objects.equals(normalized, "1")) {
return "1";
}
return normalized.toUpperCase();
}
private boolean isSamePlatform(String expected, String actual) {
return Objects.equals(normalizePlatform(expected), normalizePlatform(actual));
}
private boolean isIosPlatform(String platform) {
return Objects.equals(normalizePlatform(platform), "ios");
}
private String normalizePlatform(String platform) {
if (StringUtils.isBlank(platform)) {
return "";
}
String normalized = platform.trim().toLowerCase();
if (Objects.equals(normalized, "ios")) {
return "ios";
}
if (Objects.equals(normalized, "android")) {
return "android";
}
return normalized;
}
private boolean equalsIgnoreCase(String left, String right) {
if (StringUtils.isBlank(left) || StringUtils.isBlank(right)) {
return false;
}
return Objects.equals(left.trim().toUpperCase(), right.trim().toUpperCase());
}
private SysPushRecordLog buildPushLog(CreateRecordCmd createRecordParam) {
Timestamp now = TimestampUtils.now();
SysPushRecordLog pushLog = new SysPushRecordLog();
pushLog.setTitle(createRecordParam.getTitle());
pushLog.setContent(createRecordParam.getContent());
pushLog.setPlatform(createRecordParam.getPlatform());
pushLog.setSysOrigin(createRecordParam.getSysOrigin());
pushLog.setBusinessScene(createRecordParam.getBusinessScene());
pushLog.setDeviceType(createRecordParam.getDeviceType());
pushLog.setFixedUserIds(createRecordParam.getFixedUserIds());
pushLog.setLink(createRecordParam.getLink());
pushLog.setOrigin(PUSH_ORIGIN_BACK);
pushLog.setCreateUser(createRecordParam.getCreateUser());
pushLog.setUpdateUser(createRecordParam.getCreateUser());
pushLog.setCreateTime(now);
pushLog.setUpdateTime(now);
return pushLog;
}
private SysPushRecordLogCO toPushLogCO(SysPushRecordLog record) {
return toPushLogCO(record, CollectionUtils.newHashMap());
}
private SysPushRecordLogCO toPushLogCO(SysPushRecordLog record, Map<Long, String> sendUserNameMap) {
SysPushRecordLogCO result = new SysPushRecordLogCO();
result.setId(record.getId());
result.setTitle(record.getTitle());
result.setContent(record.getContent());
result.setSysOrigin(record.getSysOrigin());
result.setPlatform(record.getPlatform());
result.setBusinessScene(getBusinessSceneDesc(record.getBusinessScene()));
result.setFixedUserIds(record.getFixedUserIds());
result.setLink(record.getLink());
result.setOrigin(record.getOrigin());
result.setPushStatus(getPushStatusDesc(record.getPushStatus()));
result.setDeviceType(record.getDeviceType());
result.setFailQuantity(record.getFailQuantity());
result.setSuccessQuantity(record.getSuccessQuantity());
result.setSendUserName(sendUserNameMap.get(record.getCreateUser()));
result.setCreateTime(record.getCreateTime());
return result;
}
private String getBusinessSceneDesc(String businessScene) {
String desc = MessageBusinessSceneEnum.getDesc(businessScene);
return StringUtils.isBlank(desc) ? businessScene : desc;
}
private String getPushStatusDesc(String pushStatus) {
if (Objects.equals(PUSH_STATUS_SUCCESS, pushStatus)) {
return "成功";
}
if (PUSH_FAIL_STATUSES.contains(pushStatus)) {
return "失败";
}
return pushStatus;
}
private PageQuery ensurePageQuery(PageCommand pageCommand) {
PageQuery pageQuery = pageCommand.getPageQuery();
if (Objects.isNull(pageQuery)) {
pageQuery = new PageQuery();
pageCommand.setPageQuery(pageQuery);
}
if (Objects.isNull(pageQuery.getCursor()) || pageQuery.getCursor() < 1) {
pageQuery.setCursor(1);
}
if (Objects.isNull(pageQuery.getLimit()) || pageQuery.getLimit() < 1) {
pageQuery.setLimit(DEFAULT_PAGE_LIMIT);
}
return pageQuery;
}
private int resolvePageLimit(PageQuery pageQuery) {
if (Objects.isNull(pageQuery) || Objects.isNull(pageQuery.getLimit())
|| pageQuery.getLimit() < 1) {
return DEFAULT_PAGE_LIMIT;
}
return pageQuery.getLimit();
}
}

View File

@ -1,11 +1,12 @@
package com.red.circle.console.app.dto.clienobject.sys;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
import java.io.Serial;
import java.io.Serializable;
import lombok.Data;
import lombok.experimental.Accessors;
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.sql.Timestamp;
import lombok.Data;
import lombok.experimental.Accessors;
/**
* <p>
@ -89,9 +90,14 @@ public class SysPushRecordLogCO implements Serializable {
*/
private Integer successQuantity;
/**
* 发送用户.
*/
private String sendUserName;
}
/**
* 发送用户.
*/
private String sendUserName;
/**
* 创建时间.
*/
private Timestamp createTime;
}

View File

@ -1,36 +1,43 @@
package com.red.circle.console.app.service.app.sys;
/**
* 系统推送服务.
*
* @author pengliang
* @since 2020/3/6 15:53
*/
public interface SysPushService {
/*
*//**
* 创建推送.
*//*
void create(CreateRecordCmd createRecordParam);
*//**
* 获取push日志分页列表
*//*
PageResult<SysPushRecordLogCO> pageLogs(SysPushRecordLogQryCmd query);
*//**
* 定时推送任务分页列表
*//*
PageResult<SysPushTaskCO> pageTask(SysPushTaskQryCmd query);
*//**
* 保存推送任务
*//*
void saveTask(SysPushTaskCmd param);
*//**
* 删除任务
*//*
void deletePushTask(Long id);*/
}
package com.red.circle.console.app.service.app.sys;
import com.red.circle.console.app.dto.clienobject.sys.SysPushRecordLogCO;
import com.red.circle.console.app.dto.clienobject.sys.SysPushTaskCO;
import com.red.circle.framework.dto.PageResult;
import com.red.circle.other.inner.model.cmd.sys.CreateRecordCmd;
import com.red.circle.other.inner.model.cmd.sys.SysPushRecordLogQryCmd;
import com.red.circle.other.inner.model.cmd.sys.SysPushTaskCmd;
import com.red.circle.other.inner.model.cmd.sys.SysPushTaskQryCmd;
/**
* 系统推送服务.
*
* @author pengliang
* @since 2020/3/6 15:53
*/
public interface SysPushService {
/**
* 创建推送.
*/
void create(CreateRecordCmd createRecordParam);
/**
* 获取push日志分页列表.
*/
PageResult<SysPushRecordLogCO> pageLogs(SysPushRecordLogQryCmd query);
/**
* 定时推送任务分页列表.
*/
PageResult<SysPushTaskCO> pageTask(SysPushTaskQryCmd query);
/**
* 保存推送任务.
*/
void saveTask(SysPushTaskCmd param);
/**
* 删除任务.
*/
void deletePushTask(Long id);
}

View File

@ -0,0 +1,11 @@
package com.red.circle.console.infra.database.rds.dao.sys;
import com.red.circle.console.infra.database.rds.entity.sys.SysPushRecordLog;
import com.red.circle.framework.mybatis.dao.BaseDAO;
/**
* Push 日志 DAO.
*/
public interface SysPushRecordLogDAO extends BaseDAO<SysPushRecordLog> {
}

View File

@ -0,0 +1,64 @@
package com.red.circle.console.infra.database.rds.entity.sys;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.red.circle.framework.mybatis.entity.TimestampBaseEntity;
import java.io.Serial;
import java.io.Serializable;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
/**
* Push 日志.
*/
@Data
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
@TableName("sys_push_record_log")
public class SysPushRecordLog extends TimestampBaseEntity implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
@TableId(value = "id", type = IdType.ASSIGN_ID)
private Long id;
@TableField("title")
private String title;
@TableField("content")
private String content;
@TableField("sys_origin")
private String sysOrigin;
@TableField("platform")
private String platform;
@TableField("business_scene")
private String businessScene;
@TableField("fixed_user_ids")
private String fixedUserIds;
@TableField("link")
private String link;
@TableField("origin")
private String origin;
@TableField("push_status")
private String pushStatus;
@TableField("device_type")
private String deviceType;
@TableField("fail_quantity")
private Integer failQuantity;
@TableField("success_quantity")
private Integer successQuantity;
}

View File

@ -0,0 +1,11 @@
package com.red.circle.console.infra.database.rds.service.sys;
import com.red.circle.console.infra.database.rds.entity.sys.SysPushRecordLog;
import com.red.circle.framework.mybatis.service.BaseService;
/**
* Push 日志服务.
*/
public interface SysPushRecordLogService extends BaseService<SysPushRecordLog> {
}

View File

@ -0,0 +1,16 @@
package com.red.circle.console.infra.database.rds.service.sys.impl;
import com.red.circle.console.infra.database.rds.dao.sys.SysPushRecordLogDAO;
import com.red.circle.console.infra.database.rds.entity.sys.SysPushRecordLog;
import com.red.circle.console.infra.database.rds.service.sys.SysPushRecordLogService;
import com.red.circle.framework.mybatis.service.impl.BaseServiceImpl;
import org.springframework.stereotype.Service;
/**
* Push 日志服务实现.
*/
@Service
public class SysPushRecordLogServiceImpl extends
BaseServiceImpl<SysPushRecordLogDAO, SysPushRecordLog> implements SysPushRecordLogService {
}

View File

@ -1,23 +1,28 @@
package com.red.circle;
import com.red.circle.component.redis.RedisAutoConfiguration;
import com.red.circle.framework.cloud.annotation.RedCircleCloudApplication;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.scheduling.annotation.EnableScheduling;
package com.red.circle;
import com.red.circle.component.redis.RedisAutoConfiguration;
import com.red.circle.framework.cloud.annotation.RedCircleCloudApplication;
import com.red.circle.live.adapter.app.LiveMicRestController;
import com.red.circle.live.adapter.app.LiveRoomRestController;
import com.red.circle.live.adapter.app.LiveUserRestController;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.context.annotation.Import;
import org.springframework.scheduling.annotation.EnableScheduling;
/**
* @author pengliang on 2023/12/7
*/
@Slf4j
@EnableScheduling
@EnableDiscoveryClient
@EnableFeignClients
@SpringBootApplication(exclude = RedisAutoConfiguration.class)
public class LiveServiceApplication {
@EnableScheduling
@EnableDiscoveryClient
@EnableFeignClients
@SpringBootApplication(exclude = RedisAutoConfiguration.class)
@Import({LiveMicRestController.class, LiveRoomRestController.class, LiveUserRestController.class})
public class LiveServiceApplication {
public static void main(String[] args) {
log.info("Begin to start Spring Boot Application");

View File

@ -126,11 +126,12 @@ public class GameLuckyGiftCommon {
private final RankingActivityService rankingActivityService;
private final GiftMqMessage giftMqMessage;
private final GiftAppConvertor giftAppConvertor;
private final ActivityRechargeTicketService activityRechargeTicketService;
private final RoomDailyTaskProgressService roomDailyTaskProgressService;
private final RoomMemberService roomMemberService;
private final RedisService redisService;
private final RoomProfileManagerService roomProfileManagerService;
private final ActivityRechargeTicketService activityRechargeTicketService;
private final RoomDailyTaskProgressService roomDailyTaskProgressService;
private final RoomMemberService roomMemberService;
private final RedisService redisService;
private final RoomProfileManagerService roomProfileManagerService;
private final LuckyGiftSendMetricsService luckyGiftSendMetricsService;
/**
* 发送幸运礼物抽奖mq.
@ -290,26 +291,35 @@ public class GameLuckyGiftCommon {
Integer totalAwardAmount = apiResult.getOrderResults().stream().mapToInt(OrderResult::getRewardNum).sum();
// 保存幸运礼物流水
Long rewardAmount = Long.valueOf(String.valueOf(totalAwardAmount));
saveLuckGiftCount(param, rewardAmount);
if (totalAwardAmount <= 0) {
return;
}
// 发送中奖金币
BigDecimal balance = sendGold(param.getUserId(), param.getGiftId(), param.getSysOrigin(), rewardAmount);
// 发送中奖通知
int multiple = getRewardMultiple(param, rewardAmount);
sendMsg(param, multiple, balance, rewardAmount);
// 魔法礼物中奖后处理
GiftConfigDTO giftConfigDTO = getGiftConfigDTO(cmd.getGiftId());
if (GiftTabEnum.MAGIC.name().equals(giftConfigDTO.getGiftTab())) {
String timeId = IdWorker.getIdStr();
GiveAwayGiftBatchEvent giftBatchEvent = buildGiftBatchEvent(cmd, timeId, param, giftConfigDTO);
GiftConfigDTO giftConfigDTO = getGiftConfigDTO(cmd.getGiftId());
luckyGiftSendMetricsService.settle(
"legacy:" + cmd.getId(),
cmd.getRoomId(),
cmd.getUserId(),
giftConfigDTO.getGiftCandy(),
cmd.getQuantity(),
Objects.isNull(cmd.getAcceptUserId()) ? Collections.emptyList() : List.of(cmd.getAcceptUserId())
);
// 保存幸运礼物流水
Long rewardAmount = Long.valueOf(String.valueOf(totalAwardAmount));
saveLuckGiftCount(param, rewardAmount);
if (totalAwardAmount <= 0) {
return;
}
// 发送中奖金币
BigDecimal balance = sendGold(param.getUserId(), param.getGiftId(), param.getSysOrigin(), rewardAmount);
// 发送中奖通知
int multiple = getRewardMultiple(param, rewardAmount);
sendMsg(param, multiple, balance, rewardAmount);
// 魔法礼物中奖后处理
if (GiftTabEnum.MAGIC.name().equals(giftConfigDTO.getGiftTab())) {
String timeId = IdWorker.getIdStr();
GiveAwayGiftBatchEvent giftBatchEvent = buildGiftBatchEvent(cmd, timeId, param, giftConfigDTO);
giftMqMessage.sendGift(timeId, giftBatchEvent);

View File

@ -58,6 +58,7 @@ public class LuckyGiftResultProcessor {
private final RedisService redisService;
private final RoomProfileManagerService roomProfileManagerService;
private final GameLuckyGiftCommon gameLuckyGiftCommon;
private final LuckyGiftSendMetricsService luckyGiftSendMetricsService;
public void process(GameLuckyGiftBusinessEvent event, LuckyGiftGoClient.LuckyGiftGoDrawResponse response) {
GiftConfigDTO giftConfig = giftCacheService.getById(event.getGiftId());
@ -85,6 +86,18 @@ public class LuckyGiftResultProcessor {
long payAmount = resolvePayAmount(event, giftUnitPrice);
BigDecimal balanceAfter = BigDecimal.valueOf(Objects.requireNonNullElse(response.getBalanceAfter(), 0L));
luckyGiftSendMetricsService.settle(
event.getBusinessId(),
event.getRoomId(),
event.getUserId(),
giftUnitPrice,
event.getQuantity(),
Optional.ofNullable(event.getUsers()).orElse(Collections.emptyList()).stream()
.map(GameLuckyGiftUser::getAcceptUserId)
.filter(Objects::nonNull)
.toList()
);
for (LuckyGiftGoClient.LuckyGiftGoDrawResult result : response.getResults()) {
GameLuckyGiftUser user = userMap.get(result.getId());
if (user == null) {

View File

@ -0,0 +1,149 @@
package com.red.circle.other.app.common.gift;
import com.red.circle.component.redis.service.RedisService;
import com.red.circle.other.domain.enums.VipBenefitType;
import com.red.circle.other.domain.gateway.user.UserProfileGateway;
import com.red.circle.other.domain.gateway.user.ability.UserRegionGateway;
import com.red.circle.other.domain.model.user.ability.RegionConfig;
import com.red.circle.other.infra.common.team.TeamTargetCommon;
import com.red.circle.other.infra.database.mongo.entity.live.RoomProfile;
import com.red.circle.other.infra.database.mongo.service.live.RoomProfileManagerService;
import com.red.circle.other.infra.database.mongo.service.team.team.TeamMemberService;
import com.red.circle.other.infra.database.rds.service.user.user.ConsumptionLevelService;
import com.red.circle.tool.core.collection.CollectionUtils;
import com.red.circle.tool.core.num.ArithmeticUtils;
import com.red.circle.tool.core.text.StringUtils;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
/**
* 幸运礼物送出后的统计补偿.
* 只补房间流水魅力值主播收礼积分固定按 10% 结算
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class LuckyGiftSendMetricsService {
private static final String PROCESSED_KEY_PREFIX = "lucky_gift:send:metrics:processed:";
private static final BigDecimal LUCKY_GIFT_METRIC_RATIO = new BigDecimal("0.1");
private final RedisService redisService;
private final TeamTargetCommon teamTargetCommon;
private final TeamMemberService teamMemberService;
private final UserRegionGateway userRegionGateway;
private final UserProfileGateway userProfileGateway;
private final ConsumptionLevelService consumptionLevelService;
private final RoomProfileManagerService roomProfileManagerService;
public void settle(
String idempotencyKey,
Long roomId,
Long sendUserId,
BigDecimal giftUnitPrice,
Integer quantity,
List<Long> acceptUserIds
) {
if (StringUtils.isBlank(idempotencyKey) || sendUserId == null || giftUnitPrice == null
|| quantity == null || quantity <= 0 || CollectionUtils.isEmpty(acceptUserIds)) {
return;
}
if (!redisService.setIfAbsent(PROCESSED_KEY_PREFIX + idempotencyKey, "1", 7,
TimeUnit.DAYS)) {
return;
}
BigDecimal singleGiftAmount = giftUnitPrice.multiply(BigDecimal.valueOf(quantity));
BigDecimal singleMetricAmount = calculateMetricAmount(singleGiftAmount);
if (ArithmeticUtils.lteZero(singleMetricAmount)) {
return;
}
settleRoomValue(roomId, sendUserId, singleGiftAmount, acceptUserIds.size());
settleCharm(acceptUserIds, singleMetricAmount);
settleHostPoints(sendUserId, acceptUserIds, singleMetricAmount);
}
private void settleRoomValue(Long roomId, Long sendUserId, BigDecimal singleGiftAmount,
int acceptSize) {
if (roomId == null || acceptSize <= 0) {
return;
}
RoomProfile roomProfile = roomProfileManagerService.getProfileById(roomId);
if (roomProfile == null || !Objects.equals(roomProfile.getUserId(), sendUserId)
|| teamMemberService.getByMemberId(sendUserId) == null) {
return;
}
BigDecimal roomMetricAmount = calculateMetricAmount(
singleGiftAmount.multiply(BigDecimal.valueOf(acceptSize)));
if (ArithmeticUtils.lteZero(roomMetricAmount)) {
return;
}
teamTargetCommon.incrementRoomValue(sendUserId, roomMetricAmount);
}
private void settleCharm(List<Long> acceptUserIds, BigDecimal singleMetricAmount) {
for (Long acceptUserId : acceptUserIds) {
if (acceptUserId == null) {
continue;
}
BigDecimal vipBenefit = Objects.requireNonNullElse(
userProfileGateway.getUserVipBenefit(acceptUserId, VipBenefitType.XP_RATE),
VipBenefitType.XP_RATE.getDefaultValue()
);
BigDecimal charmAmount = singleMetricAmount.multiply(vipBenefit)
.setScale(0, RoundingMode.DOWN);
if (ArithmeticUtils.lteZero(charmAmount)) {
continue;
}
consumptionLevelService.incrConsumptionDiamond(acceptUserId, charmAmount);
}
}
private void settleHostPoints(Long sendUserId, List<Long> acceptUserIds,
BigDecimal singleMetricAmount) {
RegionConfig sendRegion = userRegionGateway.getRegionConfigByUserId(sendUserId);
if (sendRegion == null || StringUtils.isBlank(sendRegion.getRegionCode())) {
return;
}
Set<Long> anchorUserIds = new HashSet<>(
teamMemberService.listMemberIds(new HashSet<>(acceptUserIds)));
for (Long acceptUserId : acceptUserIds) {
if (acceptUserId == null || !anchorUserIds.contains(acceptUserId)) {
continue;
}
RegionConfig acceptRegion = userRegionGateway.getRegionConfigByUserId(acceptUserId);
if (acceptRegion == null || StringUtils.isBlank(acceptRegion.getRegionCode())) {
continue;
}
if (!userRegionGateway.checkBusinessRegionCode(sendRegion.getRegionCode(),
acceptRegion.getRegionCode())) {
continue;
}
teamTargetCommon.incrementGiftValue(acceptUserId, singleMetricAmount);
}
}
private BigDecimal calculateMetricAmount(BigDecimal amount) {
if (ArithmeticUtils.lteZero(amount)) {
return BigDecimal.ZERO;
}
return amount.multiply(LUCKY_GIFT_METRIC_RATIO).setScale(0, RoundingMode.DOWN);
}
}

View File

@ -0,0 +1,205 @@
package com.red.circle.other.app.common.gift;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import com.red.circle.component.redis.service.RedisService;
import com.red.circle.external.inner.endpoint.message.ImGroupClient;
import com.red.circle.mq.rocket.business.producer.GiftMqMessage;
import com.red.circle.mq.rocket.business.service.MessageSenderService;
import com.red.circle.other.app.convertor.material.GiftAppConvertor;
import com.red.circle.other.app.convertor.user.UserProfileAppConvertor;
import com.red.circle.other.app.service.activity.ActivityRechargeTicketService;
import com.red.circle.other.app.service.activity.RankingActivityService;
import com.red.circle.other.app.service.game.BaishunRuntimeConfigResolver;
import com.red.circle.other.app.service.task.RoomDailyTaskProgressService;
import com.red.circle.other.domain.gateway.user.UserProfileGateway;
import com.red.circle.other.domain.model.user.UserProfile;
import com.red.circle.other.infra.config.LuckyGiftApiConfig;
import com.red.circle.other.infra.database.cache.entity.game.luckgift.GameLuckyGiftInventoryCache;
import com.red.circle.other.infra.database.cache.entity.game.luckgift.LuckyGiftInventoryTemplateCache;
import com.red.circle.other.infra.database.cache.service.other.EnumConfigCacheService;
import com.red.circle.other.infra.database.cache.service.other.GameLuckyGiftCacheService;
import com.red.circle.other.infra.database.cache.service.other.GiftCacheService;
import com.red.circle.other.infra.database.mongo.service.live.RoomProfileManagerService;
import com.red.circle.other.infra.database.rds.entity.game.GameLuckyGiftCount;
import com.red.circle.other.infra.database.rds.service.game.GameLuckyGiftCountService;
import com.red.circle.other.infra.database.rds.service.game.GameLuckyGiftRuleConfigService;
import com.red.circle.other.infra.database.rds.service.game.LuckyGiftProbabilityDetailsService;
import com.red.circle.other.infra.database.rds.service.game.LuckyGiftProbabilityService;
import com.red.circle.other.infra.database.rds.service.live.RoomMemberService;
import com.red.circle.other.infra.database.rds.service.sys.EnumConfigService;
import com.red.circle.other.inner.model.dto.material.GiftConfigDTO;
import com.red.circle.other.inner.model.dto.user.UserProfileDTO;
import com.red.circle.wallet.inner.endpoint.wallet.WalletGoldClient;
import java.math.BigDecimal;
import java.util.Collections;
import java.util.List;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
class GameLuckyGiftCommonTest {
@Test
void run_shouldSettleLegacyMetricsWhenAwardIsZero() {
TestFixture fixture = new TestFixture();
fixture.mockGiftAndSender();
GameLuckyGiftParamCmd cmd = fixture.newCmd().setAcceptUserId(3001L);
fixture.common.run(cmd);
verify(fixture.luckyGiftSendMetricsService).settle(
"legacy:9001",
1001L,
2001L,
BigDecimal.valueOf(100),
2,
List.of(3001L)
);
ArgumentCaptor<GameLuckyGiftCount> countCaptor = ArgumentCaptor.forClass(GameLuckyGiftCount.class);
verify(fixture.gameLuckyGiftCountService).save(countCaptor.capture());
assertEquals(Long.valueOf(0), countCaptor.getValue().getAwardAmount());
assertEquals(Long.valueOf(200), countCaptor.getValue().getPayAmount());
}
@Test
void run_shouldHandleNullAcceptUserIdWhenAwardIsZero() {
TestFixture fixture = new TestFixture();
fixture.mockGiftAndSender();
GameLuckyGiftParamCmd cmd = fixture.newCmd().setAcceptUserId(null);
assertDoesNotThrow(() -> fixture.common.run(cmd));
verify(fixture.luckyGiftSendMetricsService).settle(
"legacy:9001",
1001L,
2001L,
BigDecimal.valueOf(100),
2,
Collections.emptyList()
);
}
private static final class TestFixture {
private final MessageSenderService senderService = mock(MessageSenderService.class);
private final ImGroupClient imGroupClient = mock(ImGroupClient.class);
private final WalletGoldClient walletGoldClient = mock(WalletGoldClient.class);
private final GiftCacheService giftCacheService = mock(GiftCacheService.class);
private final EnumConfigService enumConfigService = mock(EnumConfigService.class);
private final UserProfileGateway userProfileGateway = mock(UserProfileGateway.class);
private final EnumConfigCacheService enumConfigCacheService = mock(EnumConfigCacheService.class);
private final UserProfileAppConvertor userProfileAppConvertor = mock(UserProfileAppConvertor.class);
private final GameLuckyGiftCountService gameLuckyGiftCountService = mock(GameLuckyGiftCountService.class);
private final GameLuckyGiftCacheService gameLuckyGiftCacheService = mock(GameLuckyGiftCacheService.class);
private final LuckyGiftProbabilityService luckyGiftProbabilityService = mock(
LuckyGiftProbabilityService.class);
private final GameLuckyGiftRuleConfigService gameLuckyGiftRuleConfigService = mock(
GameLuckyGiftRuleConfigService.class);
private final LuckyGiftProbabilityDetailsService luckyGiftProbabilityDetailsService = mock(
LuckyGiftProbabilityDetailsService.class);
private final LuckyGiftApiConfig luckyGiftApiConfig = mock(LuckyGiftApiConfig.class);
private final BaishunRuntimeConfigResolver baishunRuntimeConfigResolver = mock(
BaishunRuntimeConfigResolver.class);
private final RankingActivityService rankingActivityService = mock(RankingActivityService.class);
private final GiftMqMessage giftMqMessage = mock(GiftMqMessage.class);
private final GiftAppConvertor giftAppConvertor = mock(GiftAppConvertor.class);
private final ActivityRechargeTicketService activityRechargeTicketService = mock(
ActivityRechargeTicketService.class);
private final RoomDailyTaskProgressService roomDailyTaskProgressService = mock(
RoomDailyTaskProgressService.class);
private final RoomMemberService roomMemberService = mock(RoomMemberService.class);
private final RedisService redisService = mock(RedisService.class);
private final RoomProfileManagerService roomProfileManagerService = mock(
RoomProfileManagerService.class);
private final LuckyGiftSendMetricsService luckyGiftSendMetricsService = mock(
LuckyGiftSendMetricsService.class);
private final GameLuckyGiftCommon common = new GameLuckyGiftCommon(
senderService,
imGroupClient,
walletGoldClient,
giftCacheService,
enumConfigService,
userProfileGateway,
enumConfigCacheService,
userProfileAppConvertor,
gameLuckyGiftCountService,
gameLuckyGiftCacheService,
luckyGiftProbabilityService,
gameLuckyGiftRuleConfigService,
luckyGiftProbabilityDetailsService,
luckyGiftApiConfig,
baishunRuntimeConfigResolver,
rankingActivityService,
giftMqMessage,
giftAppConvertor,
activityRechargeTicketService,
roomDailyTaskProgressService,
roomMemberService,
redisService,
roomProfileManagerService,
luckyGiftSendMetricsService
);
private void mockGiftAndSender() {
GiftConfigDTO giftConfig = new GiftConfigDTO()
.setId(11L)
.setStandardId(99L)
.setGiftCandy(BigDecimal.valueOf(100))
.setGiftPhoto("gift.png");
when(giftCacheService.getById(11L)).thenReturn(giftConfig);
LuckyGiftInventoryTemplateCache template = new LuckyGiftInventoryTemplateCache()
.setGiftGiveTotal(0)
.setComboLoss(0)
.setCaches(List.of(new GameLuckyGiftInventoryCache().setMultiple(1).setQuantity(1)));
when(gameLuckyGiftCacheService.getInventoryTemplate(99L, 2)).thenReturn(template);
UserProfile senderProfile = new UserProfile();
senderProfile.setId(2001L);
senderProfile.setAccount("sender001");
when(userProfileGateway.getByUserId(2001L)).thenReturn(senderProfile);
UserProfileDTO senderProfileDTO = new UserProfileDTO();
senderProfileDTO.setId(2001L);
senderProfileDTO.setAccount("sender001");
senderProfileDTO.setUserNickname("sender");
senderProfileDTO.setUserAvatar("sender.png");
when(userProfileAppConvertor.toUserProfileDTO(senderProfile)).thenReturn(senderProfileDTO);
LuckyGiftApiConfig.ApiAccount apiAccount = new LuckyGiftApiConfig.ApiAccount();
apiAccount.setUrl("http://127.0.0.1:1");
apiAccount.setAppId("test-app");
apiAccount.setAppChannel("test-channel");
apiAccount.setAppKey("test-key");
when(baishunRuntimeConfigResolver.resolveLuckyGiftApiAccount("LIKEI", 99L))
.thenReturn(apiAccount);
}
private GameLuckyGiftParamCmd newCmd() {
return new GameLuckyGiftParamCmd()
.setId(9001L)
.setUserId(2001L)
.setRoomId(1001L)
.setRoomAccount("room-account")
.setSysOrigin("LIKEI")
.setGiftId(11L)
.setQuantity(2)
.setGiftCombos(0)
.setRegionCode("SA")
.setGiftsCount(0)
.setComboLossCount(0)
.setOpenComboLoss(false)
.setSysComboLoss(0)
.setOpenGiftGiveTotal(false)
.setSysGiftGiveTotal(0)
.setStandardId(99L);
}
}
}

View File

@ -41,6 +41,7 @@ class LuckyGiftResultProcessorTest {
RedisService redisService = mock(RedisService.class);
RoomProfileManagerService roomProfileManagerService = mock(RoomProfileManagerService.class);
GameLuckyGiftCommon gameLuckyGiftCommon = mock(GameLuckyGiftCommon.class);
LuckyGiftSendMetricsService luckyGiftSendMetricsService = mock(LuckyGiftSendMetricsService.class);
LuckyGiftResultProcessor processor = new LuckyGiftResultProcessor(
gameLuckyGiftCountService,
giftCacheService,
@ -51,7 +52,8 @@ class LuckyGiftResultProcessorTest {
roomMemberService,
redisService,
roomProfileManagerService,
gameLuckyGiftCommon
gameLuckyGiftCommon,
luckyGiftSendMetricsService
);
GiftConfigDTO giftConfig = new GiftConfigDTO()
@ -110,6 +112,15 @@ class LuckyGiftResultProcessorTest {
processor.process(event, response);
verify(luckyGiftSendMetricsService).settle(
"biz-001",
3003L,
1001L,
BigDecimal.valueOf(173),
4,
List.of(2002L)
);
ArgumentCaptor<GameLuckyGiftCount> countCaptor = ArgumentCaptor.forClass(GameLuckyGiftCount.class);
verify(gameLuckyGiftCountService).saveOrUpdate(countCaptor.capture());
GameLuckyGiftCount saved = countCaptor.getValue();

View File

@ -0,0 +1,127 @@
package com.red.circle.other.app.common.gift;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import com.red.circle.component.redis.service.RedisService;
import com.red.circle.other.domain.enums.VipBenefitType;
import com.red.circle.other.domain.gateway.user.UserProfileGateway;
import com.red.circle.other.domain.gateway.user.ability.UserRegionGateway;
import com.red.circle.other.domain.model.user.ability.RegionConfig;
import com.red.circle.other.infra.common.team.TeamTargetCommon;
import com.red.circle.other.infra.database.mongo.entity.live.RoomProfile;
import com.red.circle.other.infra.database.mongo.entity.team.team.TeamMember;
import com.red.circle.other.infra.database.mongo.service.live.RoomProfileManagerService;
import com.red.circle.other.infra.database.mongo.service.team.team.TeamMemberService;
import com.red.circle.other.infra.database.rds.service.user.user.ConsumptionLevelService;
import java.math.BigDecimal;
import java.util.List;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
class LuckyGiftSendMetricsServiceTest {
private static final long ROOM_ID = 1001L;
private static final long SEND_USER_ID = 2001L;
private static final long ACCEPT_ANCHOR_ID = 3001L;
private static final long ACCEPT_OTHER_ANCHOR_ID = 3002L;
private RedisService redisService;
private TeamTargetCommon teamTargetCommon;
private TeamMemberService teamMemberService;
private UserRegionGateway userRegionGateway;
private UserProfileGateway userProfileGateway;
private ConsumptionLevelService consumptionLevelService;
private RoomProfileManagerService roomProfileManagerService;
private LuckyGiftSendMetricsService service;
@BeforeEach
void setUp() {
redisService = mock(RedisService.class);
teamTargetCommon = mock(TeamTargetCommon.class);
teamMemberService = mock(TeamMemberService.class);
userRegionGateway = mock(UserRegionGateway.class);
userProfileGateway = mock(UserProfileGateway.class);
consumptionLevelService = mock(ConsumptionLevelService.class);
roomProfileManagerService = mock(RoomProfileManagerService.class);
service = new LuckyGiftSendMetricsService(
redisService,
teamTargetCommon,
teamMemberService,
userRegionGateway,
userProfileGateway,
consumptionLevelService,
roomProfileManagerService
);
}
@Test
void settle_shouldAddRoomValueCharmAndHostPointsWithTenPercent() {
when(redisService.setIfAbsent("lucky_gift:send:metrics:processed:biz-1", "1", 7,
TimeUnit.DAYS)).thenReturn(true);
RoomProfile roomProfile = new RoomProfile();
roomProfile.setId(ROOM_ID);
roomProfile.setUserId(SEND_USER_ID);
when(roomProfileManagerService.getProfileById(ROOM_ID)).thenReturn(roomProfile);
when(teamMemberService.getByMemberId(SEND_USER_ID)).thenReturn(new TeamMember());
when(teamMemberService.listMemberIds(Set.of(ACCEPT_ANCHOR_ID, ACCEPT_OTHER_ANCHOR_ID)))
.thenReturn(List.of(ACCEPT_ANCHOR_ID, ACCEPT_OTHER_ANCHOR_ID));
when(userProfileGateway.getUserVipBenefit(ACCEPT_ANCHOR_ID, VipBenefitType.XP_RATE))
.thenReturn(new BigDecimal("1.25"));
when(userProfileGateway.getUserVipBenefit(ACCEPT_OTHER_ANCHOR_ID, VipBenefitType.XP_RATE))
.thenReturn(BigDecimal.ONE);
RegionConfig sendRegion = new RegionConfig().setRegionCode("SA");
RegionConfig sameRegion = new RegionConfig().setRegionCode("SA");
RegionConfig diffRegion = new RegionConfig().setRegionCode("PK");
when(userRegionGateway.getRegionConfigByUserId(SEND_USER_ID)).thenReturn(sendRegion);
when(userRegionGateway.getRegionConfigByUserId(ACCEPT_ANCHOR_ID)).thenReturn(sameRegion);
when(userRegionGateway.getRegionConfigByUserId(ACCEPT_OTHER_ANCHOR_ID)).thenReturn(diffRegion);
when(userRegionGateway.checkBusinessRegionCode("SA", "SA")).thenReturn(true);
when(userRegionGateway.checkBusinessRegionCode("SA", "PK")).thenReturn(false);
service.settle(
"biz-1",
ROOM_ID,
SEND_USER_ID,
BigDecimal.valueOf(100),
2,
List.of(ACCEPT_ANCHOR_ID, ACCEPT_OTHER_ANCHOR_ID)
);
verify(teamTargetCommon).incrementRoomValue(SEND_USER_ID, BigDecimal.valueOf(40));
verify(consumptionLevelService).incrConsumptionDiamond(ACCEPT_ANCHOR_ID,
BigDecimal.valueOf(25));
verify(consumptionLevelService).incrConsumptionDiamond(ACCEPT_OTHER_ANCHOR_ID,
BigDecimal.valueOf(20));
verify(teamTargetCommon).incrementGiftValue(ACCEPT_ANCHOR_ID, BigDecimal.valueOf(20));
verify(teamTargetCommon, never()).incrementGiftValue(ACCEPT_OTHER_ANCHOR_ID,
BigDecimal.valueOf(20));
}
@Test
void settle_shouldSkipWhenAlreadyProcessed() {
when(redisService.setIfAbsent("lucky_gift:send:metrics:processed:biz-2", "1", 7,
TimeUnit.DAYS)).thenReturn(false);
service.settle(
"biz-2",
ROOM_ID,
SEND_USER_ID,
BigDecimal.valueOf(100),
1,
List.of(ACCEPT_ANCHOR_ID)
);
verify(teamTargetCommon, never()).incrementRoomValue(SEND_USER_ID, BigDecimal.TEN);
verify(consumptionLevelService, never()).incrConsumptionDiamond(ACCEPT_ANCHOR_ID,
BigDecimal.TEN);
verify(teamTargetCommon, never()).incrementGiftValue(ACCEPT_ANCHOR_ID, BigDecimal.TEN);
}
}