diff --git a/rc-service/rc-inner-api/other-inner/other-inner-model/src/main/java/com/red/circle/other/inner/model/cmd/sys/SysPushRecordLogQryCmd.java b/rc-service/rc-inner-api/other-inner/other-inner-model/src/main/java/com/red/circle/other/inner/model/cmd/sys/SysPushRecordLogQryCmd.java index e9118f63..c2e80d66 100644 --- a/rc-service/rc-inner-api/other-inner/other-inner-model/src/main/java/com/red/circle/other/inner/model/cmd/sys/SysPushRecordLogQryCmd.java +++ b/rc-service/rc-inner-api/other-inner/other-inner-model/src/main/java/com/red/circle/other/inner/model/cmd/sys/SysPushRecordLogQryCmd.java @@ -83,10 +83,19 @@ public class SysPushRecordLogQryCmd extends PageCommand { */ private Integer failQuantity; - /** - * 成功数量 - */ - private Integer successQuantity; - - -} + /** + * 成功数量 + */ + private Integer successQuantity; + + /** + * 开始时间. + */ + private Long startTime; + + /** + * 结束时间. + */ + private Long endTime; + +} diff --git a/rc-service/rc-inner-api/other-inner/other-inner-model/src/main/java/com/red/circle/other/inner/model/cmd/sys/SysPushTaskQryCmd.java b/rc-service/rc-inner-api/other-inner/other-inner-model/src/main/java/com/red/circle/other/inner/model/cmd/sys/SysPushTaskQryCmd.java index 3bf00052..bbfb48e7 100644 --- a/rc-service/rc-inner-api/other-inner/other-inner-model/src/main/java/com/red/circle/other/inner/model/cmd/sys/SysPushTaskQryCmd.java +++ b/rc-service/rc-inner-api/other-inner/other-inner-model/src/main/java/com/red/circle/other/inner/model/cmd/sys/SysPushTaskQryCmd.java @@ -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; - -/** - *

- * 推送任务 +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; + +/** + *

+ * 推送任务 *

* * @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.上架. diff --git a/rc-service/rc-service-console/console-adapter/src/main/java/com/red/circle/console/adapter/app/sys/PushRestController.java b/rc-service/rc-service-console/console-adapter/src/main/java/com/red/circle/console/adapter/app/sys/PushRestController.java index 696fd68f..9c8203d4 100644 --- a/rc-service/rc-service-console/console-adapter/src/main/java/com/red/circle/console/adapter/app/sys/PushRestController.java +++ b/rc-service/rc-service-console/console-adapter/src/main/java/com/red/circle/console/adapter/app/sys/PushRestController.java @@ -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 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 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 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 pageTask(SysPushTaskQryCmd query) { + return sysPushService.pageTask(query); + } + + /** + * 删除定时推送任务. + */ + @GetMapping("/task/delete/{id}") + public void deletePushTask(@PathVariable("id") Long id) { + sysPushService.deletePushTask(id); + } + +} diff --git a/rc-service/rc-service-console/console-application/src/main/java/com/red/circle/console/app/service/app/sys/SysPushServiceImpl.java b/rc-service/rc-service-console/console-application/src/main/java/com/red/circle/console/app/service/app/sys/SysPushServiceImpl.java index 4ae46c97..568218e0 100644 --- a/rc-service/rc-service-console/console-application/src/main/java/com/red/circle/console/app/service/app/sys/SysPushServiceImpl.java +++ b/rc-service/rc-service-console/console-application/src/main/java/com/red/circle/console/app/service/app/sys/SysPushServiceImpl.java @@ -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 pageLogs(SysPushRecordLogQryCmd query) { - - IPage 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 userMap = userService.mapBackUser(getBackUserIds(sysPushRecordLogPage)); - Map 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 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 getBackUserIds(PageResult sysPushRecordLogPage) { - return sysPushRecordLogPage.getRecords().stream().filter(sysPushRecordLog -> - PushOriginEnum.isBack(sysPushRecordLog.getOrigin()) && sysPushRecordLog.getCreateUser() > 0 - ).map(SysPushRecordLog::getCreateUser).collect(Collectors.toList()); - } - - private Set getAppUserIds(IPage 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 users = userLatestMobileDeviceService - .listPushUserByCondition(createRecordParam); - - if (CollectionUtils.isEmpty(users)) { - return; - } - - ThreadPoolManager.getInstance().execute("pushGoogle", () -> { - - List 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 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 users, - FirebaseResponses responses) { - - List successTokens = responses.getSuccessDevicesString(); - List failTokens = responses.getFailDevicesString(); - - List 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> logList = Lists.partition(saveData, 200); - logList.forEach(sysPushUserRecordLogService::saveBatch); - } - - private Boolean isSuccess(List successTokens, List 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 users) { - - createRecordParam.setPushStatus(ResultStatusEnum.FAIL.name()); - createRecordParam.setFailQuantity(users.size()); - createRecordParam.setSuccessQuantity(0); - addPushLog(createRecordParam); - - Timestamp date = TimestampUtils.now(); - - List 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> 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 服务. + * + *

旧的推送实现已经失效,这里按当前可用的设备查询与推送客户端重建最小闭环: + * 发送 Push、记录 Push 日志、为未迁移的定时任务返回明确的不可用状态。

+ */ +@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 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 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 pageLogs(SysPushRecordLogQryCmd query) { + PageResult 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 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 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 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 listTargetDevices(CreateRecordCmd createRecordParam) { + List devices = createRecordParam.isFixedUserIdSend() + ? listFixedTargetDevices(createRecordParam) + : listBroadcastTargetDevices(createRecordParam); + return distinctByDeviceId(devices); + } + + private List listFixedTargetDevices( + CreateRecordCmd createRecordParam) { + List fixedUserIds = createRecordParam.getQualifiedFixedUserIdList(); + if (CollectionUtils.isEmpty(fixedUserIds)) { + return CollectionUtils.newArrayList(); + } + + List devices = new ArrayList<>(fixedUserIds.size()); + for (Long userId : fixedUserIds) { + PageLatestMobileDeviceQryCmd query = buildDeviceQuery(createRecordParam); + query.setUserId(userId); + PageResult 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 listBroadcastTargetDevices( + CreateRecordCmd createRecordParam) { + List devices = new ArrayList<>(); + PageLatestMobileDeviceQryCmd query = buildDeviceQuery(createRecordParam); + PageQuery pageQuery = query.getPageQuery(); + + while (true) { + PageResult page = ResponseAssert.requiredSuccess( + registerDeviceClient.pageLatestMobileDevice(query)); + Collection 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 distinctByDeviceId( + List devices) { + if (CollectionUtils.isEmpty(devices)) { + return CollectionUtils.newArrayList(); + } + + Map 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 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(); + } +} diff --git a/rc-service/rc-service-console/console-client/src/main/java/com/red/circle/console/app/dto/clienobject/sys/SysPushRecordLogCO.java b/rc-service/rc-service-console/console-client/src/main/java/com/red/circle/console/app/dto/clienobject/sys/SysPushRecordLogCO.java index f7d08a50..f383a8de 100644 --- a/rc-service/rc-service-console/console-client/src/main/java/com/red/circle/console/app/dto/clienobject/sys/SysPushRecordLogCO.java +++ b/rc-service/rc-service-console/console-client/src/main/java/com/red/circle/console/app/dto/clienobject/sys/SysPushRecordLogCO.java @@ -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; /** *

@@ -89,9 +90,14 @@ public class SysPushRecordLogCO implements Serializable { */ private Integer successQuantity; - /** - * 发送用户. - */ - private String sendUserName; - -} + /** + * 发送用户. + */ + private String sendUserName; + + /** + * 创建时间. + */ + private Timestamp createTime; + +} diff --git a/rc-service/rc-service-console/console-client/src/main/java/com/red/circle/console/app/service/app/sys/SysPushService.java b/rc-service/rc-service-console/console-client/src/main/java/com/red/circle/console/app/service/app/sys/SysPushService.java index 8697fa4d..fec2aaf1 100644 --- a/rc-service/rc-service-console/console-client/src/main/java/com/red/circle/console/app/service/app/sys/SysPushService.java +++ b/rc-service/rc-service-console/console-client/src/main/java/com/red/circle/console/app/service/app/sys/SysPushService.java @@ -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 pageLogs(SysPushRecordLogQryCmd query); - - *//** - * 定时推送任务分页列表 - *//* - PageResult 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 pageLogs(SysPushRecordLogQryCmd query); + + /** + * 定时推送任务分页列表. + */ + PageResult pageTask(SysPushTaskQryCmd query); + + /** + * 保存推送任务. + */ + void saveTask(SysPushTaskCmd param); + + /** + * 删除任务. + */ + void deletePushTask(Long id); +} diff --git a/rc-service/rc-service-console/console-infrastructure/src/main/java/com/red/circle/console/infra/database/rds/dao/sys/SysPushRecordLogDAO.java b/rc-service/rc-service-console/console-infrastructure/src/main/java/com/red/circle/console/infra/database/rds/dao/sys/SysPushRecordLogDAO.java new file mode 100644 index 00000000..a2d82f62 --- /dev/null +++ b/rc-service/rc-service-console/console-infrastructure/src/main/java/com/red/circle/console/infra/database/rds/dao/sys/SysPushRecordLogDAO.java @@ -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 { + +} diff --git a/rc-service/rc-service-console/console-infrastructure/src/main/java/com/red/circle/console/infra/database/rds/entity/sys/SysPushRecordLog.java b/rc-service/rc-service-console/console-infrastructure/src/main/java/com/red/circle/console/infra/database/rds/entity/sys/SysPushRecordLog.java new file mode 100644 index 00000000..27888038 --- /dev/null +++ b/rc-service/rc-service-console/console-infrastructure/src/main/java/com/red/circle/console/infra/database/rds/entity/sys/SysPushRecordLog.java @@ -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; +} diff --git a/rc-service/rc-service-console/console-infrastructure/src/main/java/com/red/circle/console/infra/database/rds/service/sys/SysPushRecordLogService.java b/rc-service/rc-service-console/console-infrastructure/src/main/java/com/red/circle/console/infra/database/rds/service/sys/SysPushRecordLogService.java new file mode 100644 index 00000000..3bec5277 --- /dev/null +++ b/rc-service/rc-service-console/console-infrastructure/src/main/java/com/red/circle/console/infra/database/rds/service/sys/SysPushRecordLogService.java @@ -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 { + +} diff --git a/rc-service/rc-service-console/console-infrastructure/src/main/java/com/red/circle/console/infra/database/rds/service/sys/impl/SysPushRecordLogServiceImpl.java b/rc-service/rc-service-console/console-infrastructure/src/main/java/com/red/circle/console/infra/database/rds/service/sys/impl/SysPushRecordLogServiceImpl.java new file mode 100644 index 00000000..cb20383d --- /dev/null +++ b/rc-service/rc-service-console/console-infrastructure/src/main/java/com/red/circle/console/infra/database/rds/service/sys/impl/SysPushRecordLogServiceImpl.java @@ -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 implements SysPushRecordLogService { + +} diff --git a/rc-service/rc-service-live/live-start/src/main/java/com/red/circle/LiveServiceApplication.java b/rc-service/rc-service-live/live-start/src/main/java/com/red/circle/LiveServiceApplication.java index 0e4a6599..814b85e2 100644 --- a/rc-service/rc-service-live/live-start/src/main/java/com/red/circle/LiveServiceApplication.java +++ b/rc-service/rc-service-live/live-start/src/main/java/com/red/circle/LiveServiceApplication.java @@ -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"); diff --git a/rc-service/rc-service-other/other-application/src/main/java/com/red/circle/other/app/common/gift/GameLuckyGiftCommon.java b/rc-service/rc-service-other/other-application/src/main/java/com/red/circle/other/app/common/gift/GameLuckyGiftCommon.java index 236de5dd..2a1dd4ea 100644 --- a/rc-service/rc-service-other/other-application/src/main/java/com/red/circle/other/app/common/gift/GameLuckyGiftCommon.java +++ b/rc-service/rc-service-other/other-application/src/main/java/com/red/circle/other/app/common/gift/GameLuckyGiftCommon.java @@ -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); diff --git a/rc-service/rc-service-other/other-application/src/main/java/com/red/circle/other/app/common/gift/LuckyGiftResultProcessor.java b/rc-service/rc-service-other/other-application/src/main/java/com/red/circle/other/app/common/gift/LuckyGiftResultProcessor.java index 68759e74..0f7a0e93 100644 --- a/rc-service/rc-service-other/other-application/src/main/java/com/red/circle/other/app/common/gift/LuckyGiftResultProcessor.java +++ b/rc-service/rc-service-other/other-application/src/main/java/com/red/circle/other/app/common/gift/LuckyGiftResultProcessor.java @@ -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) { diff --git a/rc-service/rc-service-other/other-application/src/main/java/com/red/circle/other/app/common/gift/LuckyGiftSendMetricsService.java b/rc-service/rc-service-other/other-application/src/main/java/com/red/circle/other/app/common/gift/LuckyGiftSendMetricsService.java new file mode 100644 index 00000000..8ffbd519 --- /dev/null +++ b/rc-service/rc-service-other/other-application/src/main/java/com/red/circle/other/app/common/gift/LuckyGiftSendMetricsService.java @@ -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 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 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 acceptUserIds, + BigDecimal singleMetricAmount) { + RegionConfig sendRegion = userRegionGateway.getRegionConfigByUserId(sendUserId); + if (sendRegion == null || StringUtils.isBlank(sendRegion.getRegionCode())) { + return; + } + + Set 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); + } +} diff --git a/rc-service/rc-service-other/other-application/src/test/java/com/red/circle/other/app/common/gift/GameLuckyGiftCommonTest.java b/rc-service/rc-service-other/other-application/src/test/java/com/red/circle/other/app/common/gift/GameLuckyGiftCommonTest.java new file mode 100644 index 00000000..a4a76109 --- /dev/null +++ b/rc-service/rc-service-other/other-application/src/test/java/com/red/circle/other/app/common/gift/GameLuckyGiftCommonTest.java @@ -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 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); + } + } +} diff --git a/rc-service/rc-service-other/other-application/src/test/java/com/red/circle/other/app/common/gift/LuckyGiftResultProcessorTest.java b/rc-service/rc-service-other/other-application/src/test/java/com/red/circle/other/app/common/gift/LuckyGiftResultProcessorTest.java index 744498fa..c0c781f5 100644 --- a/rc-service/rc-service-other/other-application/src/test/java/com/red/circle/other/app/common/gift/LuckyGiftResultProcessorTest.java +++ b/rc-service/rc-service-other/other-application/src/test/java/com/red/circle/other/app/common/gift/LuckyGiftResultProcessorTest.java @@ -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 countCaptor = ArgumentCaptor.forClass(GameLuckyGiftCount.class); verify(gameLuckyGiftCountService).saveOrUpdate(countCaptor.capture()); GameLuckyGiftCount saved = countCaptor.getValue(); diff --git a/rc-service/rc-service-other/other-application/src/test/java/com/red/circle/other/app/common/gift/LuckyGiftSendMetricsServiceTest.java b/rc-service/rc-service-other/other-application/src/test/java/com/red/circle/other/app/common/gift/LuckyGiftSendMetricsServiceTest.java new file mode 100644 index 00000000..688eb1ed --- /dev/null +++ b/rc-service/rc-service-other/other-application/src/test/java/com/red/circle/other/app/common/gift/LuckyGiftSendMetricsServiceTest.java @@ -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); + } +}