修复幸运礼物和系统推送

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

View File

@ -1,65 +1,75 @@
package com.red.circle.console.adapter.app.sys; package com.red.circle.console.adapter.app.sys;
import com.red.circle.console.app.service.app.sys.SysPushService; import com.red.circle.console.app.dto.clienobject.sys.SysPushRecordLogCO;
import com.red.circle.framework.web.controller.BaseController; import com.red.circle.console.app.dto.clienobject.sys.SysPushTaskCO;
import lombok.RequiredArgsConstructor; import com.red.circle.console.app.service.app.sys.SysPushService;
import org.springframework.http.MediaType; import com.red.circle.console.infra.annotations.OpsOperationReqLog;
import org.springframework.web.bind.annotation.RequestMapping; import com.red.circle.framework.dto.PageResult;
import org.springframework.web.bind.annotation.RestController; 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 * @author pengliang
* @since 2020-02-07 * @since 2020-02-07
*/ */
@RequiredArgsConstructor @Validated
@RestController @RequiredArgsConstructor
@RequestMapping(value = "/push", produces = MediaType.APPLICATION_JSON_VALUE) @RestController
public class PushRestController extends BaseController { @RequestMapping(value = "/push", produces = MediaType.APPLICATION_JSON_VALUE)
public class PushRestController extends BaseController {
private final SysPushService sysPushService;
/* private final SysPushService sysPushService;
*//**
* 后台推送分页列表. /**
*//* * 后台推送分页列表.
@GetMapping("/page") */
public PageResult<SysPushRecordLogCO> pageSysPushBack(SysPushRecordLogQryCmd query) { @GetMapping("/page")
return sysPushService.pageLogs(query); public PageResult<SysPushRecordLogCO> pageSysPushBack(SysPushRecordLogQryCmd query) {
} return sysPushService.pageLogs(query);
}
@OpsOperationReqLog("发送推送信息")
@PostMapping @OpsOperationReqLog("发送推送信息")
public void saveAndPush(@RequestBody CreateRecordCmd param) { @PostMapping
param.setCreateUser(param.getReqUserId()); public void saveAndPush(@RequestBody CreateRecordCmd param) {
// 设置代理上线去除 param.setCreateUser(param.getReqUserId());
// System.setProperty("proxyHost", "localhost"); sysPushService.create(param);
// System.setProperty("proxyPort", "10809"); }
sysPushService.create(param);
} @OpsOperationReqLog("保存定时消息推送任务")
@PostMapping("/task/save")
@OpsOperationReqLog("保存定时消息推送任务") public void saveTask(@RequestBody @Validated SysPushTaskCmd param) {
@PostMapping("/task/save") param.setCreateUser(param.getReqUserId());
public void saveTask(@RequestBody @Validated SysPushTaskCmd param) { sysPushService.saveTask(param);
param.setCreateUser(param.getReqUserId()); }
sysPushService.saveTask(param);
} /**
* 定时推送任务列表.
*//** */
* 定时推送任务列表. @GetMapping("/task/page")
*//* public PageResult<SysPushTaskCO> pageTask(SysPushTaskQryCmd query) {
@GetMapping("/task/page") return sysPushService.pageTask(query);
public PageResult<SysPushTaskCO> pageTask(SysPushTaskQryCmd query) { }
return sysPushService.pageTask(query);
} /**
* 删除定时推送任务.
*//** */
* 定时推送任务列表. @GetMapping("/task/delete/{id}")
*//* public void deletePushTask(@PathVariable("id") Long id) {
@GetMapping("/task/delete/{id}") sysPushService.deletePushTask(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; package com.red.circle.console.app.service.app.sys;
import com.red.circle.console.infra.database.rds.service.admin.UserService; import com.red.circle.common.business.core.enums.SysOriginPlatformEnum;
import lombok.RequiredArgsConstructor; import com.red.circle.console.app.dto.clienobject.sys.SysPushRecordLogCO;
import lombok.extern.slf4j.Slf4j; import com.red.circle.console.app.dto.clienobject.sys.SysPushTaskCO;
import org.springframework.stereotype.Service; 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;
* @author pengliang import com.red.circle.external.inner.endpoint.push.GoogleFirebasePushClient;
* @since 2020/3/6 15:53 import com.red.circle.external.inner.model.cmd.message.google.GooglePushCmd;
*/ import com.red.circle.framework.core.asserts.ResponseAssert;
@Slf4j import com.red.circle.framework.core.dto.PageCommand;
@RequiredArgsConstructor import com.red.circle.framework.core.response.CommonErrorCode;
@Service import com.red.circle.framework.core.response.ResponseErrorCode;
public class SysPushServiceImpl implements SysPushService { import com.red.circle.framework.dto.PageQuery;
import com.red.circle.framework.dto.PageResult;
private final UserService userService; import com.red.circle.other.inner.endpoint.user.device.RegisterDeviceClient;
/*private final ISysPushTaskService sysPushTaskService; import com.red.circle.other.inner.enums.sys.MessageBusinessSceneEnum;
private final IUserBaseInfoService userBaseInfoService; import com.red.circle.other.inner.model.cmd.sys.CreateRecordCmd;
private final ISysPushRecordLogService sysPushRecordLogService; import com.red.circle.other.inner.model.cmd.sys.SysPushRecordLogQryCmd;
private final GoogleFirebasePushService googleFirebasePushService; import com.red.circle.other.inner.model.cmd.sys.SysPushTaskCmd;
private final ISysPushUserRecordLogService sysPushUserRecordLogService; import com.red.circle.other.inner.model.cmd.sys.SysPushTaskQryCmd;
private final IUserLatestMobileDeviceService userLatestMobileDeviceService; import com.red.circle.other.inner.model.cmd.user.device.PageLatestMobileDeviceQryCmd;
import com.red.circle.other.inner.model.dto.user.UserLatestMobileDeviceHistoryDTO;
@Override import com.red.circle.tool.core.collection.CollectionUtils;
public void create(CreateRecordCmd createRecordParam) { import com.red.circle.tool.core.date.TimestampUtils;
import com.red.circle.tool.core.text.StringUtils;
if (StringUtils.isBlank(createRecordParam.getSysOrigin())) { import java.sql.Timestamp;
return; import java.util.ArrayList;
} import java.util.Collection;
import java.util.LinkedHashMap;
if (createRecordParam.isGoogleDevice()) { import java.util.List;
import java.util.Map;
pushGoogle(createRecordParam); import java.util.Objects;
} import java.util.Set;
} import java.util.stream.Collectors;
import lombok.RequiredArgsConstructor;
@Override import lombok.extern.slf4j.Slf4j;
public PageResult<SysPushRecordLogCO> pageLogs(SysPushRecordLogQryCmd query) { import org.springframework.stereotype.Service;
IPage<SysPushRecordLog> sysPushRecordLogPage = sysPushRecordLogService.query() /**
.eq(StringUtils.isNotBlank(query.getBusinessScene()), SysPushRecordLog::getBusinessScene, * 后台 Push 服务.
query.getBusinessScene()) *
.eq(StringUtils.isNotBlank(query.getPlatform()), SysPushRecordLog::getPlatform, * <p>旧的推送实现已经失效这里按当前可用的设备查询与推送客户端重建最小闭环
query.getPlatform()) * 发送 Push记录 Push 日志为未迁移的定时任务返回明确的不可用状态</p>
.eq(StringUtils.isNotBlank(query.getDeviceType()), SysPushRecordLog::getDeviceType, */
query.getDeviceType()) @Slf4j
.eq(StringUtils.isNotBlank(query.getSysOrigin()), SysPushRecordLog::getSysOrigin, @RequiredArgsConstructor
query.getSysOrigin()) @Service
.ge(Objects.nonNull(query.getStartCreateDate()), SysPushRecordLog::getCreateTime, public class SysPushServiceImpl implements SysPushService {
query.getStartCreateDate())
.le(Objects.nonNull(query.getEndCreateDate()), SysPushRecordLog::getCreateTime, private static final int DEFAULT_PAGE_LIMIT = 20;
query.getEndCreateDate()) private static final int DEVICE_PAGE_LIMIT = 500;
.eq(Objects.nonNull(query.getPushStatus()) && query.isSuccess(), private static final String PUSH_ORIGIN_BACK = "BACK";
SysPushRecordLog::getPushStatus, ResultStatusEnum.SUCCESS) private static final String PUSH_STATUS_SUCCESS = "SUCCESS";
.in(Objects.nonNull(query.getPushStatus()) && !query.isSuccess(), private static final String PUSH_STATUS_FAIL = "FAIL";
SysPushRecordLog::getPushStatus, ResultStatusEnum.getFailKeys()) private static final Set<String> PUSH_FAIL_STATUSES = Set.of(
.orderByDesc(SysPushRecordLog::getId) "UNKNOWN",
.page(query); PUSH_STATUS_FAIL,
"IOS_FAIL",
if (CollectionUtil.isEmpty(sysPushRecordLogPage.getRecords())) { "ANDROID_FAIL");
return sysPushRecordLogPage
.convert(sysPushRecordLog -> sysPushRecordLog.convert(SysPushRecordLogCO.class)); private final UserService userService;
} private final RegisterDeviceClient registerDeviceClient;
private final GoogleFirebasePushClient googleFirebasePushClient;
Map<Long, User> userMap = userService.mapBackUser(getBackUserIds(sysPushRecordLogPage)); private final SysPushRecordLogService sysPushRecordLogService;
Map<Long, UserBaseInfo> userBaseInfoMap = userBaseInfoService
.mapByIds(getAppUserIds(sysPushRecordLogPage)); @Override
public void create(CreateRecordCmd createRecordParam) {
return sysPushRecordLogPage.convert(sysPushRecordLog -> { validateCreateParam(createRecordParam);
SysPushRecordLogCO sysPushRecordLogVO = sysPushRecordLog.convert(SysPushRecordLogCO.class);
sysPushRecordLogVO List<UserLatestMobileDeviceHistoryDTO> targetDevices = listTargetDevices(createRecordParam);
.setBusinessScene(MessageBusinessSceneEnum.getDesc(sysPushRecordLog.getBusinessScene())); if (CollectionUtils.isEmpty(targetDevices)) {
sysPushRecordLogVO.setPushStatus(ResultStatusEnum.getDesc(sysPushRecordLog.getPushStatus())); ResponseAssert.failure(ResponseErrorCode.REQUEST_PARAMETER_ERROR,
"No matching push devices.");
if (PushOriginEnum.isBack(sysPushRecordLog.getOrigin())) { }
User user = userMap.get(sysPushRecordLog.getCreateUser());
if (Objects.nonNull(user)) { SysPushRecordLog pushLog = buildPushLog(createRecordParam);
sysPushRecordLogVO.setSendUserName(user.getNickname()); try {
} pushGoogle(createRecordParam, targetDevices);
return sysPushRecordLogVO; pushLog.setPushStatus(PUSH_STATUS_SUCCESS);
} pushLog.setSuccessQuantity(targetDevices.size());
pushLog.setFailQuantity(0);
UserBaseInfo userBaseInfo = userBaseInfoMap.get(sysPushRecordLog.getCreateUser()); } catch (RuntimeException ex) {
if (Objects.nonNull(userBaseInfo)) { pushLog.setPushStatus(PUSH_STATUS_FAIL);
sysPushRecordLogVO.setSendUserName(userBaseInfo.getUserNickname()); pushLog.setSuccessQuantity(0);
} pushLog.setFailQuantity(targetDevices.size());
return sysPushRecordLogVO; log.warn("Push broadcast failed, sysOrigin={}, title={}", createRecordParam.getSysOrigin(),
}); createRecordParam.getTitle(), ex);
} throw ex;
} finally {
@Override ResponseAssert.isTrue(CommonErrorCode.OPERATING_FAILURE, !sysPushRecordLogService.save(pushLog));
public PageResult<SysPushTaskCO> pageTask(SysPushTaskQryCmd query) { createRecordParam.setId(pushLog.getId());
return sysPushTaskService.pageTask(query); }
} }
@Override @Override
public void saveTask(SysPushTaskCmd param) { public PageResult<SysPushRecordLogCO> pageLogs(SysPushRecordLogQryCmd query) {
sysPushTaskService.saveTask(param); PageResult<SysPushRecordLog> page = sysPushRecordLogService.query()
} .eq(StringUtils.isNotBlank(query.getBusinessScene()), SysPushRecordLog::getBusinessScene,
query.getBusinessScene())
.eq(StringUtils.isNotBlank(query.getPlatform()), SysPushRecordLog::getPlatform,
@Override query.getPlatform())
public void deletePushTask(Long id) { .eq(StringUtils.isNotBlank(query.getDeviceType()), SysPushRecordLog::getDeviceType,
sysPushTaskService.deletePushTask(id); query.getDeviceType())
} .eq(StringUtils.isNotBlank(query.getSysOrigin()), SysPushRecordLog::getSysOrigin,
query.getSysOrigin())
private List<Long> getBackUserIds(PageResult<SysPushRecordLogCO> sysPushRecordLogPage) { .ge(Objects.nonNull(query.getStartTime()), SysPushRecordLog::getCreateTime,
return sysPushRecordLogPage.getRecords().stream().filter(sysPushRecordLog -> new Timestamp(query.getStartTime()))
PushOriginEnum.isBack(sysPushRecordLog.getOrigin()) && sysPushRecordLog.getCreateUser() > 0 .le(Objects.nonNull(query.getEndTime()), SysPushRecordLog::getCreateTime,
).map(SysPushRecordLog::getCreateUser).collect(Collectors.toList()); new Timestamp(query.getEndTime()))
} .eq(isSuccessStatusQuery(query.getPushStatus()), SysPushRecordLog::getPushStatus,
PUSH_STATUS_SUCCESS)
private Set<Long> getAppUserIds(IPage<SysPushRecordLog> sysPushRecordLogPage) { .in(isFailStatusQuery(query.getPushStatus()), SysPushRecordLog::getPushStatus,
return sysPushRecordLogPage.getRecords().stream().filter(sysPushRecordLog -> PUSH_FAIL_STATUSES)
PushOriginEnum.isApp(sysPushRecordLog.getOrigin()) && sysPushRecordLog.getCreateUser() > 0 .orderByDesc(SysPushRecordLog::getId)
).map(SysPushRecordLog::getCreateUser).collect(Collectors.toSet()); .page(ensurePageQuery(query));
}
if (CollectionUtils.isEmpty(page.getRecords())) {
return page.convert(this::toPushLogCO);
*//** }
* 推送谷歌
*//* Map<Long, String> sendUserNameMap = userService.mapBackUserNickname(
private void pushGoogle(CreateRecordCmd createRecordParam) { page.getRecords().stream()
.map(SysPushRecordLog::getCreateUser)
List<UserLatestMobileDevicePushDTO> users = userLatestMobileDeviceService .filter(Objects::nonNull)
.listPushUserByCondition(createRecordParam); .collect(Collectors.toSet()));
if (CollectionUtils.isEmpty(users)) { return page.convert(record -> toPushLogCO(record, sendUserNameMap));
return; }
}
@Override
ThreadPoolManager.getInstance().execute("pushGoogle", () -> { public PageResult<SysPushTaskCO> pageTask(SysPushTaskQryCmd query) {
return PageResult.newPageResult(resolvePageLimit(ensurePageQuery(query)));
List<String> deviceIds = users.stream().map(UserLatestMobileDevicePushDTO::getDeviceId) }
.collect(Collectors.toList());
@Override
//推送并处理结果 public void saveTask(SysPushTaskCmd param) {
processPush(createRecordParam, users, googleFirebasePushService.sendMulticast( ResponseAssert.failure(CommonErrorCode.API_DEPRECATED,
PushParam.builder() "Push task is not available in the migrated admin yet.");
.sysOrigin(SysOriginPlatformEnum.valueOf(createRecordParam.getSysOrigin())) }
.androidDevices(deviceIds)
.title(createRecordParam.getTitle()) @Override
.body(createRecordParam.getContent()) public void deletePushTask(Long id) {
.build() ResponseAssert.failure(CommonErrorCode.API_DEPRECATED,
)); "Push task is not available in the migrated admin yet.");
}); }
}
private void validateCreateParam(CreateRecordCmd createRecordParam) {
private void processPush(CreateRecordCmd createRecordParam, ResponseAssert.notBlank(ResponseErrorCode.REQUEST_PARAMETER_ERROR, createRecordParam.getTitle());
List<UserLatestMobileDevicePushDTO> users, ResponseAssert.notBlank(ResponseErrorCode.REQUEST_PARAMETER_ERROR,
FirebaseResponses responses) { createRecordParam.getContent());
ResponseAssert.notBlank(ResponseErrorCode.REQUEST_PARAMETER_ERROR,
if (Objects.isNull(responses) || Objects.equals(responses.getFailCount(), users.size())) { createRecordParam.getSysOrigin());
//全部失败 ResponseAssert.notBlank(ResponseErrorCode.REQUEST_PARAMETER_ERROR,
allPushFail(createRecordParam, users); createRecordParam.getDeviceType());
return; ResponseAssert.notNull(ResponseErrorCode.REQUEST_PARAMETER_ERROR,
} SysOriginPlatformEnum.toEnum(createRecordParam.getSysOrigin()));
ResponseAssert.isTrue(ResponseErrorCode.REQUEST_PARAMETER_ERROR,
createRecordParam.setPushStatus(ResultStatusEnum.SUCCESS.name()); !createRecordParam.isGoogleDevice());
createRecordParam.setFailQuantity(responses.getFailCount()); }
createRecordParam.setSuccessQuantity(responses.getSuccessCount());
addPushLog(createRecordParam); private void pushGoogle(CreateRecordCmd createRecordParam,
//处理成功或失败 List<UserLatestMobileDeviceHistoryDTO> targetDevices) {
processPushResult(createRecordParam.getId(), users, responses); GooglePushCmd pushCmd = new GooglePushCmd()
} .setSysOrigin(SysOriginPlatformEnum.toEnum(createRecordParam.getSysOrigin()))
.setTitle(createRecordParam.getTitle())
private void processPushResult(Long pushId, List<UserLatestMobileDevicePushDTO> users, .setBody(createRecordParam.getContent());
FirebaseResponses responses) {
for (UserLatestMobileDeviceHistoryDTO device : targetDevices) {
List<String> successTokens = responses.getSuccessDevicesString(); if (isIosPlatform(device.getRequestClient())) {
List<String> failTokens = responses.getFailDevicesString(); pushCmd.addIosDevice(device.getDeviceId());
continue;
List<SysPushUserRecordLog> saveData = Lists.newArrayList(); }
Timestamp date = TimestampUtils.now(); pushCmd.addAndroidDevice(device.getDeviceId());
}
for (UserLatestMobileDevicePushDTO user : users) {
ResponseAssert.isTrue(ResponseErrorCode.REQUEST_PARAMETER_ERROR,
SysPushUserRecordLog pushUserRecord = new SysPushUserRecordLog(); !pushCmd.isNotEmptyAndroidDevices() && !pushCmd.isNotEmptyIosDevices());
pushUserRecord.setId(IdWorkerUtils.getId()); ResponseAssert.checkSuccess(googleFirebasePushClient.broadcast(pushCmd));
pushUserRecord.setPushId(pushId); }
pushUserRecord.setPushToken(user.getDeviceId());
pushUserRecord.setUserId(user.getUserId()); private List<UserLatestMobileDeviceHistoryDTO> listTargetDevices(CreateRecordCmd createRecordParam) {
pushUserRecord.setCreateTime(date); List<UserLatestMobileDeviceHistoryDTO> devices = createRecordParam.isFixedUserIdSend()
pushUserRecord.setUpdateTime(date); ? listFixedTargetDevices(createRecordParam)
: listBroadcastTargetDevices(createRecordParam);
Boolean isSuccess = isSuccess(successTokens, failTokens, user); return distinctByDeviceId(devices);
if (Objects.isNull(isSuccess)) { }
continue;
} private List<UserLatestMobileDeviceHistoryDTO> listFixedTargetDevices(
CreateRecordCmd createRecordParam) {
pushUserRecord.setSuccess(isSuccess); List<Long> fixedUserIds = createRecordParam.getQualifiedFixedUserIdList();
saveData.add(pushUserRecord); if (CollectionUtils.isEmpty(fixedUserIds)) {
} return CollectionUtils.newArrayList();
}
List<List<SysPushUserRecordLog>> logList = Lists.partition(saveData, 200);
logList.forEach(sysPushUserRecordLogService::saveBatch); List<UserLatestMobileDeviceHistoryDTO> devices = new ArrayList<>(fixedUserIds.size());
} for (Long userId : fixedUserIds) {
PageLatestMobileDeviceQryCmd query = buildDeviceQuery(createRecordParam);
private Boolean isSuccess(List<String> successTokens, List<String> failTokens, query.setUserId(userId);
UserLatestMobileDevicePushDTO user) { PageResult<UserLatestMobileDeviceHistoryDTO> page = ResponseAssert.requiredSuccess(
registerDeviceClient.pageLatestMobileDevice(query));
for (String successToken : successTokens) { if (CollectionUtils.isEmpty(page.getRecords())) {
if (Objects.equals(user.getDeviceId(), successToken)) { continue;
return Boolean.TRUE; }
} UserLatestMobileDeviceHistoryDTO latestDevice = page.getRecords().iterator().next();
} if (matchesDevice(latestDevice, createRecordParam, true)) {
devices.add(latestDevice);
for (String failToken : failTokens) { }
if (Objects.equals(user.getDeviceId(), failToken)) { }
return Boolean.FALSE; return devices;
} }
}
private List<UserLatestMobileDeviceHistoryDTO> listBroadcastTargetDevices(
return null; CreateRecordCmd createRecordParam) {
} List<UserLatestMobileDeviceHistoryDTO> devices = new ArrayList<>();
PageLatestMobileDeviceQryCmd query = buildDeviceQuery(createRecordParam);
private void allPushFail(CreateRecordCmd createRecordParam, PageQuery pageQuery = query.getPageQuery();
List<UserLatestMobileDevicePushDTO> users) {
while (true) {
createRecordParam.setPushStatus(ResultStatusEnum.FAIL.name()); PageResult<UserLatestMobileDeviceHistoryDTO> page = ResponseAssert.requiredSuccess(
createRecordParam.setFailQuantity(users.size()); registerDeviceClient.pageLatestMobileDevice(query));
createRecordParam.setSuccessQuantity(0); Collection<UserLatestMobileDeviceHistoryDTO> records = page.getRecords();
addPushLog(createRecordParam); if (CollectionUtils.isEmpty(records)) {
break;
Timestamp date = TimestampUtils.now(); }
List<SysPushUserRecordLog> logs = users.stream().map(user -> { records.stream()
.filter(device -> matchesDevice(device, createRecordParam, false))
SysPushUserRecordLog pushUserRecord = new SysPushUserRecordLog(); .forEach(devices::add);
pushUserRecord.setPushId(createRecordParam.getId());
pushUserRecord.setPushToken(user.getDeviceId()); if (records.size() < resolvePageLimit(pageQuery)) {
pushUserRecord.setUserId(user.getUserId()); break;
pushUserRecord.setSuccess(Boolean.FALSE); }
pushUserRecord.setCreateTime(date); pageQuery.setCursor(pageQuery.getCursor() + 1);
pushUserRecord.setUpdateTime(date); }
pushUserRecord.setCreateUser(createRecordParam.getCreateUser()); return devices;
return pushUserRecord; }
}).collect(Collectors.toList()); private PageLatestMobileDeviceQryCmd buildDeviceQuery(CreateRecordCmd createRecordParam) {
PageLatestMobileDeviceQryCmd query = new PageLatestMobileDeviceQryCmd();
List<List<SysPushUserRecordLog>> logList = Lists.partition(logs, 200); query.setSysOrigin(createRecordParam.getSysOrigin());
logList.forEach(sysPushUserRecordLogService::saveBatch); PageQuery pageQuery = new PageQuery();
} pageQuery.setCursor(1);
pageQuery.setLimit(DEVICE_PAGE_LIMIT);
private void addPushLog(CreateRecordCmd newPushDTO) { query.setPageQuery(pageQuery);
SysPushRecordLog sysPushRecordLog = newPushDTO.convert(SysPushRecordLog.class); return query;
sysPushRecordLog.setOrigin(PushOriginEnum.BACK.name()); }
sysPushRecordLogService.save(sysPushRecordLog);
newPushDTO.setId(sysPushRecordLog.getId()); 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; package com.red.circle.console.app.dto.clienobject.sys;
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 java.io.Serial; import java.io.Serial;
import java.io.Serializable; import java.io.Serializable;
import lombok.Data; import java.sql.Timestamp;
import lombok.experimental.Accessors; import lombok.Data;
import lombok.experimental.Accessors;
/** /**
* <p> * <p>
@ -89,9 +90,14 @@ public class SysPushRecordLogCO implements Serializable {
*/ */
private Integer successQuantity; 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; 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;
* @author pengliang import com.red.circle.other.inner.model.cmd.sys.SysPushRecordLogQryCmd;
* @since 2020/3/6 15:53 import com.red.circle.other.inner.model.cmd.sys.SysPushTaskCmd;
*/ import com.red.circle.other.inner.model.cmd.sys.SysPushTaskQryCmd;
public interface SysPushService {
/* /**
*//** * 系统推送服务.
* 创建推送. *
*//* * @author pengliang
void create(CreateRecordCmd createRecordParam); * @since 2020/3/6 15:53
*/
*//** public interface SysPushService {
* 获取push日志分页列表
*//* /**
PageResult<SysPushRecordLogCO> pageLogs(SysPushRecordLogQryCmd query); * 创建推送.
*/
*//** void create(CreateRecordCmd createRecordParam);
* 定时推送任务分页列表
*//* /**
PageResult<SysPushTaskCO> pageTask(SysPushTaskQryCmd query); * 获取push日志分页列表.
*/
*//** PageResult<SysPushRecordLogCO> pageLogs(SysPushRecordLogQryCmd query);
* 保存推送任务
*//* /**
void saveTask(SysPushTaskCmd param); * 定时推送任务分页列表.
*/
*//** PageResult<SysPushTaskCO> pageTask(SysPushTaskQryCmd query);
* 删除任务
*//* /**
void deletePushTask(Long id);*/ * 保存推送任务.
} */
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; package com.red.circle;
import com.red.circle.component.redis.RedisAutoConfiguration; import com.red.circle.component.redis.RedisAutoConfiguration;
import com.red.circle.framework.cloud.annotation.RedCircleCloudApplication; import com.red.circle.framework.cloud.annotation.RedCircleCloudApplication;
import lombok.extern.slf4j.Slf4j; import com.red.circle.live.adapter.app.LiveMicRestController;
import org.springframework.boot.SpringApplication; import com.red.circle.live.adapter.app.LiveRoomRestController;
import org.springframework.boot.autoconfigure.SpringBootApplication; import com.red.circle.live.adapter.app.LiveUserRestController;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient; import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.openfeign.EnableFeignClients; import org.springframework.boot.SpringApplication;
import org.springframework.scheduling.annotation.EnableScheduling; 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 * @author pengliang on 2023/12/7
*/ */
@Slf4j @Slf4j
@EnableScheduling @EnableScheduling
@EnableDiscoveryClient @EnableDiscoveryClient
@EnableFeignClients @EnableFeignClients
@SpringBootApplication(exclude = RedisAutoConfiguration.class) @SpringBootApplication(exclude = RedisAutoConfiguration.class)
public class LiveServiceApplication { @Import({LiveMicRestController.class, LiveRoomRestController.class, LiveUserRestController.class})
public class LiveServiceApplication {
public static void main(String[] args) { public static void main(String[] args) {
log.info("Begin to start Spring Boot Application"); log.info("Begin to start Spring Boot Application");

View File

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

View File

@ -58,6 +58,7 @@ public class LuckyGiftResultProcessor {
private final RedisService redisService; private final RedisService redisService;
private final RoomProfileManagerService roomProfileManagerService; private final RoomProfileManagerService roomProfileManagerService;
private final GameLuckyGiftCommon gameLuckyGiftCommon; private final GameLuckyGiftCommon gameLuckyGiftCommon;
private final LuckyGiftSendMetricsService luckyGiftSendMetricsService;
public void process(GameLuckyGiftBusinessEvent event, LuckyGiftGoClient.LuckyGiftGoDrawResponse response) { public void process(GameLuckyGiftBusinessEvent event, LuckyGiftGoClient.LuckyGiftGoDrawResponse response) {
GiftConfigDTO giftConfig = giftCacheService.getById(event.getGiftId()); GiftConfigDTO giftConfig = giftCacheService.getById(event.getGiftId());
@ -85,6 +86,18 @@ public class LuckyGiftResultProcessor {
long payAmount = resolvePayAmount(event, giftUnitPrice); long payAmount = resolvePayAmount(event, giftUnitPrice);
BigDecimal balanceAfter = BigDecimal.valueOf(Objects.requireNonNullElse(response.getBalanceAfter(), 0L)); 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()) { for (LuckyGiftGoClient.LuckyGiftGoDrawResult result : response.getResults()) {
GameLuckyGiftUser user = userMap.get(result.getId()); GameLuckyGiftUser user = userMap.get(result.getId());
if (user == null) { 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); RedisService redisService = mock(RedisService.class);
RoomProfileManagerService roomProfileManagerService = mock(RoomProfileManagerService.class); RoomProfileManagerService roomProfileManagerService = mock(RoomProfileManagerService.class);
GameLuckyGiftCommon gameLuckyGiftCommon = mock(GameLuckyGiftCommon.class); GameLuckyGiftCommon gameLuckyGiftCommon = mock(GameLuckyGiftCommon.class);
LuckyGiftSendMetricsService luckyGiftSendMetricsService = mock(LuckyGiftSendMetricsService.class);
LuckyGiftResultProcessor processor = new LuckyGiftResultProcessor( LuckyGiftResultProcessor processor = new LuckyGiftResultProcessor(
gameLuckyGiftCountService, gameLuckyGiftCountService,
giftCacheService, giftCacheService,
@ -51,7 +52,8 @@ class LuckyGiftResultProcessorTest {
roomMemberService, roomMemberService,
redisService, redisService,
roomProfileManagerService, roomProfileManagerService,
gameLuckyGiftCommon gameLuckyGiftCommon,
luckyGiftSendMetricsService
); );
GiftConfigDTO giftConfig = new GiftConfigDTO() GiftConfigDTO giftConfig = new GiftConfigDTO()
@ -110,6 +112,15 @@ class LuckyGiftResultProcessorTest {
processor.process(event, response); 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); ArgumentCaptor<GameLuckyGiftCount> countCaptor = ArgumentCaptor.forClass(GameLuckyGiftCount.class);
verify(gameLuckyGiftCountService).saveOrUpdate(countCaptor.capture()); verify(gameLuckyGiftCountService).saveOrUpdate(countCaptor.capture());
GameLuckyGiftCount saved = countCaptor.getValue(); 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);
}
}