Compare commits

...

10 Commits

41 changed files with 1464 additions and 73 deletions

View File

@ -0,0 +1,301 @@
#!/usr/bin/env bash
set -euo pipefail
BASE_DIR="${BASE_DIR:-/opt/aslan-deploy}"
SRC_DIR="${SRC_DIR:-$BASE_DIR/source/likei-services}"
KUBECONFIG="${KUBECONFIG:-$BASE_DIR/kubeconfig}"
NAMESPACE="${NAMESPACE:-prod}"
MODE="${MODE:-preload}"
SKIP_BUILD="${SKIP_BUILD:-0}"
USE_GIT_SOURCE="${USE_GIT_SOURCE:-0}"
GIT_REMOTE_URL="${GIT_REMOTE_URL:-git@gitea.haiyihy.com:hy/aslan-server.git}"
GIT_REF="${GIT_REF:-atyou_prod}"
MAVEN_EXTRA_ARGS="${MAVEN_EXTRA_ARGS:-}"
BUILD_TS="${BUILD_TS:-$(date +%Y%m%dv%H%M%S)}"
BASE_IMAGE="tuokemi-con-registry.ap-southeast-1.cr.aliyuncs.com/public_mirror_images/eclipse-temurin:17-jdk-jammy"
FALLBACK_BASE_IMAGE="${FALLBACK_BASE_IMAGE:-eclipse-temurin:17-jdk-jammy}"
usage() {
cat <<'EOF'
Usage:
deploy-likei-services.sh [--mode preload|push|build-only|status] [--skip-build] [other|external|console ...]
Examples:
/opt/aslan-deploy/deploy-likei-services.sh status
/opt/aslan-deploy/deploy-likei-services.sh --mode preload other external console
USE_GIT_SOURCE=1 GIT_REF=atyou_prod /opt/aslan-deploy/deploy-likei-services.sh --mode preload other
MODE=push ALIYUN_USER=xxx ALIYUN_PASS=xxx /opt/aslan-deploy/deploy-likei-services.sh other
Notes:
- preload mode builds the image on this server, imports it into each TKE node's containerd, then updates the deployment.
- push mode follows the original Jenkins flow and requires registry credentials in ALIYUN_USER/ALIYUN_PASS.
- USE_GIT_SOURCE=1 clones or resets source from GIT_REMOTE_URL/GIT_REF before building.
EOF
}
log() {
printf '[%s] %s\n' "$(date '+%F %T')" "$*"
}
need_cmd() {
command -v "$1" >/dev/null 2>&1 || {
echo "missing command: $1" >&2
exit 2
}
}
ensure_base_image() {
if docker image inspect "$BASE_IMAGE" >/dev/null 2>&1; then
return
fi
log "base image not cached, pulling $FALLBACK_BASE_IMAGE"
docker pull "$FALLBACK_BASE_IMAGE"
docker tag "$FALLBACK_BASE_IMAGE" "$BASE_IMAGE"
}
update_source_from_git() {
if [[ "$USE_GIT_SOURCE" != "1" ]]; then
return
fi
need_cmd git
mkdir -p "$(dirname "$SRC_DIR")"
if [[ ! -d "$SRC_DIR/.git" ]]; then
rm -rf "$SRC_DIR"
log "git clone $GIT_REMOTE_URL ($GIT_REF) -> $SRC_DIR"
git clone --branch "$GIT_REF" "$GIT_REMOTE_URL" "$SRC_DIR"
return
fi
log "git reset source to origin/$GIT_REF from $GIT_REMOTE_URL"
git -C "$SRC_DIR" remote set-url origin "$GIT_REMOTE_URL"
git -C "$SRC_DIR" fetch origin "$GIT_REF"
git -C "$SRC_DIR" checkout "$GIT_REF"
git -C "$SRC_DIR" reset --hard "origin/$GIT_REF"
}
service_module() {
case "$1" in
other) echo "rc-service/rc-service-other/other-start" ;;
external) echo "rc-service/rc-service-external/external-start" ;;
console) echo "rc-service/rc-service-console/console-start" ;;
*) echo "unsupported service: $1" >&2; exit 2 ;;
esac
}
service_dir() {
case "$1" in
other) echo "rc-service/rc-service-other" ;;
external) echo "rc-service/rc-service-external" ;;
console) echo "rc-service/rc-service-console" ;;
*) echo "unsupported service: $1" >&2; exit 2 ;;
esac
}
image_repo() {
case "$1" in
other) echo "tuokemi-con-registry.ap-southeast-1.cr.aliyuncs.com/atyou-prod/other" ;;
external) echo "tuokemi-con-registry.ap-southeast-1.cr.aliyuncs.com/atyou-prod/external" ;;
console) echo "tuokemi-con-registry.ap-southeast-1.cr.aliyuncs.com/atyou-prod/console" ;;
*) echo "unsupported service: $1" >&2; exit 2 ;;
esac
}
build_service() {
local svc="$1"
local module
local dir
local repo
local tag
local image
module="$(service_module "$svc")"
dir="$(service_dir "$svc")"
repo="$(image_repo "$svc")"
tag="${svc}-${BUILD_TS}"
image="${repo}:${tag}"
cd "$SRC_DIR"
if [[ "$SKIP_BUILD" != "1" ]]; then
log "maven package $svc ($module)"
# shellcheck disable=SC2086
mvn clean package -pl "$module" -am -P prod -Dmaven.test.skip=true $MAVEN_EXTRA_ARGS
fi
ensure_base_image
log "docker build $image"
docker build \
--build-arg "SERVICE_VERSION=atyou-prod:${tag}" \
-f "$dir/Dockerfile" \
-t "$image" \
"$dir"
docker tag "$image" "${repo}:latest"
case "$MODE" in
build-only)
log "build-only completed for $svc: $image"
;;
push)
push_image "$svc" "$image" "$repo"
rollout "$svc" "$image"
;;
preload)
preload_image_to_nodes "$svc" "$image"
rollout "$svc" "$image"
;;
*)
echo "unsupported MODE: $MODE" >&2
exit 2
;;
esac
}
push_image() {
local svc="$1"
local image="$2"
local repo="$3"
if [[ -n "${ALIYUN_USER:-}" && -n "${ALIYUN_PASS:-}" ]]; then
log "docker login registry for $svc"
printf '%s' "$ALIYUN_PASS" | docker login --username "$ALIYUN_USER" --password-stdin tuokemi-con-registry.ap-southeast-1.cr.aliyuncs.com
fi
log "docker push $image"
docker push "$image"
docker push "${repo}:latest"
}
preload_image_to_nodes() {
local svc="$1"
local image="$2"
local safe_tag="${image//[^A-Za-z0-9_.-]/-}"
local tar_path="$BASE_DIR/artifacts/${safe_tag}.tar"
mkdir -p "$BASE_DIR/artifacts"
log "docker save $image -> $tar_path"
docker save "$image" -o "$tar_path"
mapfile -t nodes < <(kubectl --kubeconfig "$KUBECONFIG" get nodes -o jsonpath='{range .items[*]}{.metadata.name}{"\n"}{end}')
if [[ "${#nodes[@]}" -eq 0 ]]; then
echo "no kubernetes nodes found" >&2
exit 3
fi
for node in "${nodes[@]}"; do
preload_image_to_node "$svc" "$image" "$tar_path" "$node"
done
}
preload_image_to_node() {
local svc="$1"
local image="$2"
local tar_path="$3"
local node="$4"
local pod="image-preload-${svc}-${node//[^a-zA-Z0-9-]/-}-$(date +%s)"
local manifest
manifest="$(mktemp)"
cat >"$manifest" <<EOF
apiVersion: v1
kind: Pod
metadata:
name: ${pod}
namespace: ${NAMESPACE}
spec:
nodeName: "${node}"
restartPolicy: Never
hostPID: true
tolerations:
- operator: Exists
containers:
- name: preload
image: docker:26.1.3-dind
imagePullPolicy: IfNotPresent
command: ["sh", "-c", "sleep 3600"]
securityContext:
privileged: true
volumeMounts:
- name: containerd
mountPath: /run/containerd/containerd.sock
volumes:
- name: containerd
hostPath:
path: /run/containerd/containerd.sock
type: Socket
EOF
log "create preload pod $pod on node $node"
kubectl --kubeconfig "$KUBECONFIG" apply -f "$manifest" >/dev/null
rm -f "$manifest"
if ! kubectl --kubeconfig "$KUBECONFIG" -n "$NAMESPACE" wait --for=condition=Ready "pod/$pod" --timeout=180s >/dev/null; then
kubectl --kubeconfig "$KUBECONFIG" -n "$NAMESPACE" describe "pod/$pod" || true
kubectl --kubeconfig "$KUBECONFIG" -n "$NAMESPACE" delete pod "$pod" --ignore-not-found=true >/dev/null
return 1
fi
log "copy image tar to $pod"
if ! kubectl --kubeconfig "$KUBECONFIG" -n "$NAMESPACE" cp "$tar_path" "$pod:/tmp/image.tar" -c preload >/dev/null; then
kubectl --kubeconfig "$KUBECONFIG" -n "$NAMESPACE" delete pod "$pod" --ignore-not-found=true >/dev/null
return 1
fi
log "import $image into node $node"
if ! kubectl --kubeconfig "$KUBECONFIG" -n "$NAMESPACE" exec "$pod" -c preload -- \
ctr --address /run/containerd/containerd.sock -n k8s.io images import /tmp/image.tar >/dev/null; then
kubectl --kubeconfig "$KUBECONFIG" -n "$NAMESPACE" delete pod "$pod" --ignore-not-found=true >/dev/null
return 1
fi
kubectl --kubeconfig "$KUBECONFIG" -n "$NAMESPACE" delete pod "$pod" --ignore-not-found=true >/dev/null
}
rollout() {
local svc="$1"
local image="$2"
log "set image deployment/$svc $svc=$image"
kubectl --kubeconfig "$KUBECONFIG" -n "$NAMESPACE" set image "deployment/$svc" "$svc=$image"
kubectl --kubeconfig "$KUBECONFIG" -n "$NAMESPACE" rollout status "deployment/$svc" --timeout=600s
kubectl --kubeconfig "$KUBECONFIG" -n "$NAMESPACE" get deploy "$svc" -o wide
}
status() {
kubectl --kubeconfig "$KUBECONFIG" -n "$NAMESPACE" get deploy other external console -o wide
kubectl --kubeconfig "$KUBECONFIG" get nodes -o wide
}
main() {
while [[ "$#" -gt 0 ]]; do
case "$1" in
--mode)
MODE="$2"
shift 2
;;
--skip-build)
SKIP_BUILD=1
shift
;;
-h|--help)
usage
exit 0
;;
*)
break
;;
esac
done
need_cmd java
need_cmd mvn
need_cmd docker
need_cmd kubectl
[[ -f "$KUBECONFIG" ]] || { echo "missing kubeconfig: $KUBECONFIG" >&2; exit 2; }
if [[ "${1:-}" == "status" || "$MODE" == "status" ]]; then
status
exit 0
fi
update_source_from_git
local services=("$@")
if [[ "${#services[@]}" -eq 0 ]]; then
services=(other external console)
fi
for svc in "${services[@]}"; do
build_service "$svc"
done
}
main "$@"

View File

@ -0,0 +1,76 @@
#!/usr/bin/env bash
set -euo pipefail
DEPLOY_HOST="${DEPLOY_HOST:-43.160.245.220}"
DEPLOY_USER="${DEPLOY_USER:-ubuntu}"
SSH_KEY="${SSH_KEY:-$HOME/.ssh/aslan-deploy-sg-ed25519}"
REMOTE_BASE="${REMOTE_BASE:-/opt/aslan-deploy}"
REMOTE_SRC="$REMOTE_BASE/source/likei-services"
LOCAL_M2_ROOT="${LOCAL_M2_ROOT:-$HOME/.m2/repository}"
REMOTE_M2_ROOT="${REMOTE_M2_ROOT:-/home/$DEPLOY_USER/.m2/repository}"
M2_CACHE_GROUPS="${M2_CACHE_GROUPS:-com/red/circle com/github/sud}"
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
usage() {
cat <<'EOF'
Usage:
.deploy/aslan-deploy/sync-and-deploy.sh [remote deploy args...]
Examples:
.deploy/aslan-deploy/sync-and-deploy.sh status
.deploy/aslan-deploy/sync-and-deploy.sh --mode preload other
.deploy/aslan-deploy/sync-and-deploy.sh --mode preload other external console
Environment:
DEPLOY_HOST default 43.160.245.220
DEPLOY_USER default ubuntu
SSH_KEY default ~/.ssh/aslan-deploy-sg-ed25519
Git source on deploy host:
ssh -i ~/.ssh/aslan-deploy-sg-ed25519 ubuntu@43.160.245.220 \
'USE_GIT_SOURCE=1 GIT_REF=atyou_prod /opt/aslan-deploy/deploy-likei-services.sh --mode preload other'
EOF
}
if [[ "${1:-}" == "-h" || "${1:-}" == "--help" ]]; then
usage
exit 0
fi
ssh_base=(ssh -i "$SSH_KEY" -o StrictHostKeyChecking=accept-new "$DEPLOY_USER@$DEPLOY_HOST")
rsync_ssh="ssh -i $SSH_KEY -o StrictHostKeyChecking=accept-new"
"${ssh_base[@]}" "mkdir -p '$REMOTE_SRC' '$REMOTE_BASE'"
rsync -az --delete \
--exclude '.git/' \
--exclude 'target/' \
--exclude '**/target/' \
--exclude '.idea/' \
--exclude '.DS_Store' \
--exclude 'build.md' \
--exclude 'node_modules/' \
-e "$rsync_ssh" \
"$REPO_ROOT/" "$DEPLOY_USER@$DEPLOY_HOST:$REMOTE_SRC/"
rsync -az \
-e "$rsync_ssh" \
"$SCRIPT_DIR/deploy-likei-services.sh" "$DEPLOY_USER@$DEPLOY_HOST:$REMOTE_BASE/deploy-likei-services.sh"
for cache_group in $M2_CACHE_GROUPS; do
local_cache_path="$LOCAL_M2_ROOT/$cache_group"
remote_cache_parent="$REMOTE_M2_ROOT/$(dirname "$cache_group")"
if [[ ! -d "$local_cache_path" ]]; then
continue
fi
"${ssh_base[@]}" "mkdir -p '$remote_cache_parent'"
rsync -az --delete \
--exclude '*.lastUpdated' \
-e "$rsync_ssh" \
"$local_cache_path" "$DEPLOY_USER@$DEPLOY_HOST:$remote_cache_parent/"
done
printf -v remote_script_q '%q' "$REMOTE_BASE/deploy-likei-services.sh"
printf -v remote_args_q '%q ' "$@"
"${ssh_base[@]}" "chmod +x $remote_script_q && $remote_script_q $remote_args_q"

2
.gitignore vendored
View File

@ -96,3 +96,5 @@ devops-monitor-id_rsa.key
/.claude/
build.md
reports/

View File

@ -0,0 +1,9 @@
package com.red.circle.mq.business.model.event.gift;
/**
* 礼物发送场景.
*/
public enum GiftSendSceneEnum {
ROOM,
PRIVATE_CHAT
}

View File

@ -83,6 +83,11 @@ public class GiveAwayGiftBatchEvent implements Serializable {
*/
private Long roomId;
/**
* 发送场景.
*/
private GiftSendSceneEnum sendScene;
/**
* 动态id
*/
@ -270,6 +275,14 @@ public class GiveAwayGiftBatchEvent implements Serializable {
return Objects.nonNull(giftConfig) && Objects.equals(giftConfig.getGiftTab(), "MAGIC");
}
public GiftSendSceneEnum sendSceneOrRoom() {
return Objects.isNull(sendScene) ? GiftSendSceneEnum.ROOM : sendScene;
}
public boolean privateChatScene() {
return Objects.equals(sendScene, GiftSendSceneEnum.PRIVATE_CHAT);
}
/**
* 是否是专属礼物.

View File

@ -1,7 +1,11 @@
package com.red.circle.external.inner.endpoint.message.api;
import com.red.circle.external.inner.model.cmd.message.CustomC2CMsgBodyCmd;
import com.red.circle.framework.dto.ResultResponse;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestParam;
/**
@ -29,4 +33,10 @@ public interface ImMessageClientApi {
@RequestParam("toAccount") Long toAccount,
@RequestParam("text") String text);
/**
* 发送 C2C 自定义消息.
*/
@PostMapping("/sendCustomMessage")
ResultResponse<Void> sendCustomMessage(@RequestBody @Validated CustomC2CMsgBodyCmd cmd);
}

View File

@ -0,0 +1,70 @@
package com.red.circle.external.inner.model.cmd.message;
import com.red.circle.framework.dto.Command;
import jakarta.validation.constraints.NotBlank;
import java.io.Serial;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* C2C 自定义消息.
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class CustomC2CMsgBodyCmd extends Command {
@Serial
private static final long serialVersionUID = 1L;
@NotBlank(message = "fromAccount required.")
private String fromAccount;
@NotBlank(message = "toAccount required.")
private String toAccount;
@NotBlank(message = "desc required.")
private String desc;
private Object data;
public static CustomC2CMsgBodyBuilder builder() {
return new CustomC2CMsgBodyBuilder();
}
public static class CustomC2CMsgBodyBuilder {
private String fromAccount;
private String toAccount;
private String desc;
private Object data;
public CustomC2CMsgBodyBuilder fromAccount(String fromAccount) {
this.fromAccount = fromAccount;
return this;
}
public CustomC2CMsgBodyBuilder toAccount(String toAccount) {
this.toAccount = toAccount;
return this;
}
public CustomC2CMsgBodyBuilder desc(String desc) {
this.desc = desc;
return this;
}
public CustomC2CMsgBodyBuilder data(Object data) {
this.data = data;
return this;
}
public CustomC2CMsgBodyCmd build() {
CustomC2CMsgBodyCmd cmd = new CustomC2CMsgBodyCmd();
cmd.setFromAccount(fromAccount);
cmd.setToAccount(toAccount);
cmd.setDesc(desc);
cmd.setData(data);
return cmd;
}
}
}

View File

@ -1,6 +1,5 @@
package com.red.circle.console.adapter.app.user;
import com.red.circle.common.business.core.enums.SysOriginPlatformEnum;
import com.red.circle.console.app.dto.clienobject.count.aslan.AslanRegionCountryStatisticsCO;
import com.red.circle.console.app.service.app.count.AslanRegionCountryStatisticsService;
import com.red.circle.console.app.service.app.user.DatavService;

View File

@ -163,15 +163,21 @@ public class UserFreightBalanceServiceImpl implements
@Override
public void deduction(UserFreightBalanceCmd param) {
// 余额不足
// 扣款金币允许为0表示本次只登记USDT扣款负数会导致余额反向增加必须拦截
ResponseAssert.isTrue(WalletErrorCode.REQUIRED_GT_ZERO,
ArithmeticUtils.gte(param.getEarnPoints(), BigDecimal.ZERO));
// 操作失败
ResponseAssert.isTrue(CommonErrorCode.OPERATING_FAILURE,
decrCandyBalance(param.getSysOrigin(), param.getUserId(), param.getEarnPoints()));
// USDT扣款复用进货的amount字段必须大于0否则会产生没有金额意义的扣款流水
ResponseAssert.isTrue(WalletErrorCode.REQUIRED_GT_ZERO,
Objects.nonNull(param.getAmount()) && param.getAmount().compareTo(BigDecimal.ZERO) > 0);
// 插入流水
// 只有金币数量大于0时才真正扣减货运金币余额金币为0时保留原余额只记录USDT扣款流水
if (param.getEarnPoints().compareTo(BigDecimal.ZERO) > 0) {
ResponseAssert.isTrue(CommonErrorCode.OPERATING_FAILURE,
decrCandyBalance(param.getSysOrigin(), param.getUserId(), param.getEarnPoints()));
}
// 扣款流水同时记录金币数量和USDT金额amount用于后台列表导出和后续对账
BigDecimal balance = getAvailableBalance(param.getUserId());
ResponseAssert.requiredSuccess(
freightGoldClient.addRunningWater(new AddFreightBalanceRunningWaterCmd()
@ -182,13 +188,16 @@ public class UserFreightBalanceServiceImpl implements
.setQuantity(param.getEarnPoints())
.setBalance(balance)
.setUsdQuantity(BigDecimal.ZERO)
.setAmount(param.getAmount())
.setOrigin(FreightBalanceOrigin.DEDUCTION.name())
.setRemark(param.getRemark())
.setCreateUser(param.getOperationUser())
.setUpdateUser(param.getOperationUser())));
//余额如果少于1250则收回徽章
removeFreightShipBadge(param.getUserId(), balance);
// 实际扣减金币后才需要重新判断金币余额阈值USDT-only扣款不改变余额
if (param.getEarnPoints().compareTo(BigDecimal.ZERO) > 0) {
removeFreightShipBadge(param.getUserId(), balance);
}
}

View File

@ -65,6 +65,11 @@ public class UserFreightBalanceRunningWaterCO implements Serializable {
*/
private BigDecimal balance;
/**
* USDT金额.
*/
private BigDecimal amount;
/**
* 来源.
*/

View File

@ -14,11 +14,15 @@ public enum BannerDisplayPositionEnum {
ROOM("房间内"),
ROOM_LIST("房间列表页"),
EXPLORE_PAGE("发现页"),
WALLET("钱包"),
HOME_ALERT("首页弹出层");
HOME_ALERT("首页弹出层"),
GAME("游戏");
private final String desc;

View File

@ -1,6 +1,7 @@
package com.red.circle.external.inner.endpoint.message;
import com.red.circle.external.inner.endpoint.message.api.ImMessageClientApi;
import com.red.circle.external.inner.model.cmd.message.CustomC2CMsgBodyCmd;
import com.red.circle.external.inner.service.message.ImMessageClientService;
import com.red.circle.framework.dto.ResultResponse;
import com.red.circle.tool.core.parse.DataTypeUtils;
@ -36,4 +37,10 @@ public class ImMessageClientEndpoint implements ImMessageClientApi {
return ResultResponse.success();
}
@Override
public ResultResponse<Void> sendCustomMessage(CustomC2CMsgBodyCmd cmd) {
imMessageClientService.sendCustomMessage(cmd);
return ResultResponse.success();
}
}

View File

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

View File

@ -1,7 +1,12 @@
package com.red.circle.external.inner.service.message.impl;
import com.red.circle.component.instant.msg.tencet.service.endpoint.api.im.account.ImMessageManagerService;
import com.red.circle.component.instant.msg.tencet.service.param.ImMessageParam;
import com.red.circle.component.instant.msg.tencet.service.param.MessageBody;
import com.red.circle.component.instant.msg.tencet.service.param.OfflinePushInfo;
import com.red.circle.external.inner.model.cmd.message.CustomC2CMsgBodyCmd;
import com.red.circle.external.inner.service.message.ImMessageClientService;
import java.util.concurrent.ThreadLocalRandom;
import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;
import org.springframework.stereotype.Service;
@ -23,4 +28,20 @@ public class ImMessageClientServiceImpl implements ImMessageClientService {
imMessageManagerService.sendMessageText(fromAccount, toAccount, text).subscribe();
}
@Override
public void sendCustomMessage(CustomC2CMsgBodyCmd cmd) {
ImMessageParam param = ImMessageParam.builder()
.fromAccount(cmd.getFromAccount())
.toAccount(cmd.getToAccount())
.msgRandom(ThreadLocalRandom.current().nextInt(100_000_000, 1_000_000_000))
.offlinePushInfo(OfflinePushInfo.builder().pushFlag(0).build())
.build()
.addMsgBody(MessageBody.customBody(cmd.getDesc(), cmd.getData()));
imMessageManagerService.sendMessage(param)
.subscribe(resp -> log.info("send C2C custom message success: fromAccount={}, toAccount={}, desc={}, resp={}",
cmd.getFromAccount(), cmd.getToAccount(), cmd.getDesc(), resp),
throwable -> log.warn("send C2C custom message failed: fromAccount={}, toAccount={}, desc={}",
cmd.getFromAccount(), cmd.getToAccount(), cmd.getDesc(), throwable));
}
}

View File

@ -30,8 +30,8 @@ management:
show-details: always
rtc:
appId: f424387a480e41088239416e10030034
certificate: c96d8494f80542bb83c843ad4045df03
appId: 4b5e5cea3b86476caf7f7a57d05b82d1
certificate: a6feb209de6448148a926f59634807bb
red-circle:
oss:

View File

@ -56,12 +56,12 @@ public class LiveMicrophoneServiceImpl implements LiveMicrophoneService {
private final RedisService redisService;
private final RoomUserBlacklistClient roomUserBlacklistClient;
private final String authKey = "Basic N2Q0ZTRiZDYyODUyNDc3ZGI4Yzk3OGU1NTVmMDRiOGI6ODdkMjcxMTg2NzgxNGY4YjgzZTNkOWIyZmRjOTc3OWQ=";
private final String authKey = "Basic ZjFjNWIzMDZiMzA0NGU5Y2E5Yzk5YjYzNDNlZGJlZDM6OGQ2Njc0OGQ1NjQyNDVlZmI5MTc3NDFlYTllYWUyNWU=";
private final String killRoom = "https://api.sd-rtn.com/dev/v1/kicking-rule";
private final String queryUserInfo = "https://api.sd-rtn.com/dev/v1/channel/user/";
private final String appId = "535633ae846441b09547b68f8353aad3";
private final String appId = "4b5e5cea3b86476caf7f7a57d05b82d1";
private final UserProfileClient userProfileClient;

View File

@ -97,12 +97,12 @@ public class LiveMicrophoneGatewayImpl implements LiveMicrophoneGateway {
private final LiveRoomCacheService liveRoomCacheService;
// private final RedissonClient redissonClient;
//临时处理,后续房子啊Nacos配置处理
private final String authKey = "Basic MmIwZGFkOWQ0ZjEzNGQ2NmI5MmI2ZmY2MzljNmEwMTc6ZWRlMTQ1NTRjNTU0NDY2ZmE4OWNiYWFhNGNmZTM3YjA=";
private final String authKey = "Basic ZjFjNWIzMDZiMzA0NGU5Y2E5Yzk5YjYzNDNlZGJlZDM6OGQ2Njc0OGQ1NjQyNDVlZmI5MTc3NDFlYTllYWUyNWU=";
private final String killRoom = "https://api.sd-rtn.com/dev/v1/kicking-rule";
private final String queryUserInfo = "https://api.sd-rtn.com/dev/v1/channel/user/";
private final String appId = "660b61b534d04d44815ee855913a6efe";
private final String appId = "4b5e5cea3b86476caf7f7a57d05b82d1";
@Override

View File

@ -12,6 +12,7 @@ import com.red.circle.other.app.dto.clientobject.gift.PremiumGiftWallCO;
import com.red.circle.other.app.dto.clientobject.gift.UserGiftBackpackCO;
import com.red.circle.other.app.dto.cmd.gift.GiveAwayGiftBackpackCmd;
import com.red.circle.other.app.dto.cmd.gift.GiveAwayGiftBatchCmd;
import com.red.circle.other.app.dto.cmd.gift.GiveAwayPrivateGiftCmd;
import com.red.circle.other.app.service.gift.GiftService;
import com.red.circle.tool.core.json.JacksonUtils;
import java.math.BigDecimal;
@ -131,6 +132,20 @@ public class GiftRestController extends BaseController {
return giftService.giveAwayGiftBatch(cmd);
}
/**
* 私聊赠送-礼物.
*
* @eo.name 私聊赠送-礼物.
* @eo.url /private
* @eo.method post
* @eo.request-type json
*/
@PostMapping("/private")
public BigDecimal giveAwayPrivateGift(@RequestBody @Validated GiveAwayPrivateGiftCmd cmd) {
log.warn("giveAwayPrivateGift:{}", JacksonUtils.toJson(cmd));
return giftService.giveAwayPrivateGift(cmd);
}
/**
* 赠送-幸运礼物.
*
@ -181,4 +196,3 @@ public class GiftRestController extends BaseController {
}
}

View File

@ -6,6 +6,7 @@ import com.red.circle.common.business.core.enums.*;
import com.red.circle.framework.core.asserts.ResponseAssert;
import com.red.circle.mq.business.model.event.gift.GiveAwayGiftBatchEvent;
import com.red.circle.mq.business.model.event.gift.GiveAwayGiftRoomAccepts;
import com.red.circle.mq.business.model.event.gift.GiftSendSceneEnum;
import com.red.circle.mq.business.model.event.gift.GiveGiftConfig;
import com.red.circle.mq.business.model.event.task.TaskApprovalEvent;
import com.red.circle.mq.rocket.business.producer.GiftMqMessage;
@ -128,6 +129,7 @@ public class BlessingsGiftGiveCmdExe {
giveAwayGiftBatchEvent.setQuantity(cmd.getQuantity());
giveAwayGiftBatchEvent.setGiftConfig(toGiveGiftConfig(giftConfig));
giveAwayGiftBatchEvent.setRoomId(cmd.getRoomId());
giveAwayGiftBatchEvent.setSendScene(GiftSendSceneEnum.ROOM);
giveAwayGiftBatchEvent.setDynamicContentId(cmd.getDynamicContentId());
giveAwayGiftBatchEvent.setTrackId(trackId);
giveAwayGiftBatchEvent.setSongId(cmd.getSongId());

View File

@ -10,11 +10,14 @@ import com.red.circle.mq.rocket.business.producer.GiftMqMessage;
import com.red.circle.mq.rocket.business.producer.TaskMqMessage;
import com.red.circle.other.app.convertor.material.GiftAppConvertor;
import com.red.circle.other.app.dto.cmd.gift.GiveAwayGiftBatchCmd;
import com.red.circle.other.app.dto.cmd.gift.GiveAwayPrivateGiftCmd;
import com.red.circle.other.app.service.task.TaskService;
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.UserProfile;
import com.red.circle.other.infra.database.cache.service.other.GiftCacheService;
import com.red.circle.other.inner.asserts.GiftErrorCode;
import com.red.circle.other.inner.asserts.user.UserErrorCode;
import com.red.circle.other.inner.enums.material.GiftCurrencyType;
import com.red.circle.other.inner.enums.material.GiftTabEnum;
import com.red.circle.other.inner.model.dto.material.GiftConfigDTO;
@ -100,6 +103,49 @@ public class GiftGiveAwayBatchCmdExe {
return receipt.getBalance();
}
public BigDecimal executePrivate(GiveAwayPrivateGiftCmd cmd) {
GiftConfigDTO giftConfig = giftCacheService.getById(cmd.getGiftId());
log.warn("GiftGiveAwayPrivateCmdExe{}", giftConfig);
ResponseAssert.notNull(GiftErrorCode.GIFT_NOT_FOUND, giftConfig);
ResponseAssert.isTrue(ResponseErrorCode.REQUEST_PARAMETER_ERROR,
Objects.equals(cmd.requireReqSysOrigin(), giftConfig.getSysOrigin()));
ResponseAssert.isFalse(GiftErrorCode.NOT_SUPPORTED_GIFT_TYPE,
Objects.equals(giftConfig.getGiftTab(), GiftTabEnum.LUCKY_GIFT.name()));
checkPrivateAcceptUser(cmd);
if (isZeroGiftCandy(giftConfig)) {
BigDecimal dollarAmount = walletGoldClient.getBalance(cmd.requiredReqUserId()).getBody().getDollarAmount();
log.info("私聊赠送普通礼物金额为0,余额为 {}", dollarAmount);
return dollarAmount;
}
WalletReceiptDTO receipt = consumePrivateCandy(cmd, giftConfig);
log.warn("GiftGiveAwayPrivateCmdExe{}", receipt);
giftMqMessage.sendGift(receipt.getConsumeId().toString(),
giftAppConvertor.toGiveAwayPrivateGiftEvent(cmd, giftConfig, receipt.getConsumeId()));
userProfileGateway.removeCache(cmd.requiredReqUserId());
Boolean taskStatus = taskService.checkTaskStatus(cmd.getReqUserId(), 2L,
LocalDateTimeUtils.nowFormat("yyyy-MM-dd"));
if (taskStatus) {
taskMqMessage.sendTask(TaskApprovalEvent.builder()
.taskId(2)
.userId(cmd.getReqUserId())
.day(LocalDateTimeUtils.nowFormat("yyyy-MM-dd"))
.build());
}
return receipt.getBalance();
}
private void checkPrivateAcceptUser(GiveAwayPrivateGiftCmd cmd) {
UserProfile acceptUser = userProfileGateway.getByUserId(cmd.getAcceptUserId());
ResponseAssert.notNull(UserErrorCode.USER_INFO_NOT_FOUND, acceptUser);
ResponseAssert.isTrue(ResponseErrorCode.REQUEST_PARAMETER_ERROR,
Objects.equals(cmd.requireReqSysOrigin(), acceptUser.getOriginSys()));
}
private void checkDynamicRegion(GiveAwayGiftBatchCmd cmd) {
if (cmd.getDynamicContentId() == null) {
return;
@ -163,6 +209,60 @@ public class GiftGiveAwayBatchCmdExe {
throw new IllegalArgumentException("param error.");
}
/**
* 私聊付钱.
*/
private WalletReceiptDTO consumePrivateCandy(GiveAwayPrivateGiftCmd cmd, GiftConfigDTO giftConfig) {
if (isGiftTypeEqFree(giftConfig)) {
return new WalletReceiptDTO()
.setConsumeId(IdWorkerUtils.getId())
.setProcessAmount(BigDecimal.ZERO)
.setBalance(
ResponseAssert.requiredSuccess(walletGoldClient.getBalance(cmd.requiredReqUserId()))
.getDollarAmount());
}
if (isGiftTypeEqGold(giftConfig)) {
BigDecimal amount = cmd.calculateConsumeGolds(giftConfig.getGiftCandy());
WalletReceiptResDTO receiptRes = walletGoldClient.changeBalance(GoldReceiptCmd.builder()
.appExpenditure()
.userId(cmd.requiredReqUserId())
.eventId(giftConfig.getId())
.sysOrigin(cmd.requireReqSysOrigin())
.origin(GoldOrigin.GIVE_GIFT + "_" + giftConfig.getGiftTab())
.amount(amount)
.closeDelayAsset()
.build()).getBody();
return new WalletReceiptDTO()
.setConsumeId(receiptRes.getAssetRecordId())
.setBalance(receiptRes.getBalance().getDollarAmount())
.setProcessAmount(amount);
}
if (isGiftTypeEqDiamond(giftConfig)) {
return walletDiamondClient.changeBalance(new DiamondReceiptCmd()
.setConsumeId(Objects.toString(giftConfig.getId()))
.setType(ReceiptType.EXPENDITURE)
.setTrackId(giftConfig.getId())
.setUserId(cmd.requiredReqUserId())
.setSysOrigin(cmd.requireReqSysOriginEnum())
.setOrigin(DiamondOrigin.GIVE_GIFT)
.setAmount(cmd.calculateConsumeGolds(giftConfig.getGiftCandy()))
.setCreateTime(TimestampUtils.now())
).getBody();
}
ResponseAssert.failure(CommonErrorCode.TYPE_IS_NOT_IN_SCOPE);
throw new IllegalArgumentException("param error.");
}
private boolean isZeroGiftCandy(GiftConfigDTO giftConfig) {
return Objects.nonNull(giftConfig.getGiftCandy())
&& BigDecimal.ZERO.compareTo(giftConfig.getGiftCandy()) == 0;
}
private boolean isGiftTypeEqDiamond(GiftConfigDTO giftConfigInfo) {
return Objects.equals(giftConfigInfo.getType(), GiftCurrencyType.DIAMOND.name());
}

View File

@ -12,7 +12,6 @@ import com.red.circle.other.app.convertor.user.UserProfileAppConvertor;
import com.red.circle.other.app.dto.clientobject.RoomRedPacketCO;
import com.red.circle.other.app.dto.cmd.SendRoomRedPacketCmd;
import com.red.circle.other.app.dto.cmd.task.RoomDailyTaskProgressUpdateCmd;
import com.red.circle.other.app.enums.RoomRewardLevelEnum;
import com.red.circle.other.app.service.task.RoomDailyTaskProgressService;
import com.red.circle.other.app.util.OfficialNoticeUtils;
import com.red.circle.other.domain.gateway.RoomRedPacketGateway;
@ -23,15 +22,12 @@ import com.red.circle.other.domain.redpacket.RoomRedPacketSourceType;
import com.red.circle.other.domain.redpacket.RoomRedPacketStatus;
import com.red.circle.other.domain.redpacket.RoomRedPacketType;
import com.red.circle.other.infra.database.cache.service.other.RoomRedPacketCacheService;
import com.red.circle.other.infra.database.mongo.entity.activity.RoomContributionActivityCount;
import com.red.circle.other.infra.database.mongo.entity.live.RoomProfileManager;
import com.red.circle.other.infra.database.mongo.service.activity.RoomContributionActivityCountService;
import com.red.circle.other.infra.database.mongo.service.live.RoomProfileManagerService;
import com.red.circle.other.inner.asserts.OtherErrorCode;
import com.red.circle.other.inner.asserts.RoomErrorCode;
import com.red.circle.other.inner.enums.task.RoomDailyTaskCode;
import com.red.circle.other.inner.model.dto.user.UserProfileDTO;
import com.red.circle.tool.core.date.ZonedDateTimeAsiaRiyadhUtils;
import com.red.circle.tool.core.sequence.IdWorkerUtils;
import com.red.circle.tool.core.tuple.PennyAmount;
import com.red.circle.wallet.inner.endpoint.wallet.WalletGoldClient;
@ -42,10 +38,11 @@ import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.support.TransactionSynchronization;
import org.springframework.transaction.support.TransactionSynchronizationManager;
import java.time.Duration;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.concurrent.ThreadLocalRandom;
@ -159,26 +156,7 @@ public class SendRoomRedPacketCmdExe {
RoomRedPacket redPacket = buildRedPacket(cmd, systemUserId, packetId, expireTime, RoomRedPacketSourceType.PLATFORM);
roomRedPacketGateway.save(redPacket);
// 查询上周数据
deductRoomContribution(cmd.getRoomId(), cmd.getTotalAmount());
// 预生成随机金额列表拼手气红包
List<Long> amounts = generateRandomAmounts(cmd.getTotalAmount(), cmd.getTotalCount());
long expireSeconds = cmd.getExpireMinutes() * 60;
roomRedPacketCacheService.setRandomAmounts(packetId, amounts, expireSeconds);
// 写入Redis红包主数据
cacheRedPacketData(redPacket);
// 添加到房间红包列表
roomRedPacketCacheService.addToRoomList(
cmd.getRoomId(),
packetId,
System.currentTimeMillis()
);
// 发送飘窗通知平台红包
sendMessage(cmd, packetId);
publishPlatformRedPacket(cmd, redPacket, packetId);
log.info("发平台红包成功 packetId={} roomId={} amount={} count={} ",
packetId, cmd.getRoomId(), cmd.getTotalAmount(), cmd.getTotalCount());
@ -186,27 +164,101 @@ public class SendRoomRedPacketCmdExe {
return RoomRedPacketAppConvertor.toCO(redPacket);
}
private void deductRoomContribution(Long roomId, Long totalAmount) {
String lastWeekDateNumber = ZonedDateTimeAsiaRiyadhUtils.getLastWeekMonday().format(DateTimeFormatter.ofPattern("yyyyMMdd"));
RoomContributionActivityCount lastWeek = findByRoomIdAndDateNumber(roomId, Integer.parseInt(lastWeekDateNumber));
ResponseAssert.notNull(RoomErrorCode.ROOM_CONTRIBUTION_NOT_FOUND, lastWeek);
/**
* 执行房间周流水奖励平台红包.
*/
@Transactional(rollbackFor = Exception.class)
public RoomRedPacketCO executeRoomContributionReward(SendRoomRedPacketCmd cmd,
String activityCountId) {
cmd.setPacketType(2);
validateParams(cmd);
Long amount = lastWeek.getContributionValue().longValue();
RoomRewardLevelEnum level = RoomRewardLevelEnum.getLevelByAmount(amount);
ResponseAssert.notNull(RoomErrorCode.ROOM_CONTRIBUTION_NOT_COMPLETED, level);
ResponseAssert.isTrue(CommonErrorCode.DATA_ERROR, Objects.equals(totalAmount, level.getRewardCoins()));
String packetId = buildRoomContributionRewardPacketId(activityCountId);
boolean locked = roomContributionActivityCountService
.markRewardCoinsSending(activityCountId, packetId);
if (!locked) {
log.warn("房间周流水奖励已被处理 activityCountId={} roomId={}", activityCountId,
cmd.getRoomId());
return null;
}
boolean updated = roomContributionActivityCountService.updateRewardCoinsSent(lastWeek.getId());
ResponseAssert.isTrue(CommonErrorCode.OPERATING_FAILURE, updated);
try {
RoomRedPacket redPacket = roomRedPacketGateway.findByPacketId(packetId);
LocalDateTime expireTime = LocalDateTime.now().plusMinutes(cmd.getExpireMinutes());
if (Objects.isNull(redPacket)) {
Long systemUserId = 0L;
redPacket = buildRedPacket(cmd, systemUserId, packetId, expireTime,
RoomRedPacketSourceType.PLATFORM);
roomRedPacketGateway.save(redPacket);
} else if (isExpired(redPacket)) {
roomRedPacketGateway.resetPlatformPacket(packetId, cmd.getTotalAmount(),
cmd.getTotalCount(), cmd.getExpireMinutes(), expireTime);
redPacket = buildRedPacket(cmd, 0L, packetId, expireTime,
RoomRedPacketSourceType.PLATFORM);
}
RoomRedPacket finalRedPacket = redPacket;
TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
@Override
public void afterCommit() {
publishRoomContributionRewardAfterCommit(cmd, finalRedPacket, activityCountId,
packetId);
}
});
log.info("房间周流水奖励红包已提交 packetId={} roomId={} activityCountId={} amount={} count={}",
packetId, cmd.getRoomId(), activityCountId, cmd.getTotalAmount(),
cmd.getTotalCount());
return RoomRedPacketAppConvertor.toCO(redPacket);
} catch (Exception e) {
roomContributionActivityCountService
.resetRewardCoinsSending(activityCountId, packetId, e.getMessage());
throw e;
}
}
/**
* 根据房间ID和日期查询数据
*/
private RoomContributionActivityCount findByRoomIdAndDateNumber(Long roomId, Integer dateNumber) {
return roomContributionActivityCountService
.findByRoomIdAndDateNumber(roomId, dateNumber)
.orElse(null);
private void publishRoomContributionRewardAfterCommit(SendRoomRedPacketCmd cmd,
RoomRedPacket redPacket,
String activityCountId,
String packetId) {
try {
if (roomRedPacketCacheService.existsRedPacket(packetId)) {
roomRedPacketCacheService.addToRoomList(
cmd.getRoomId(),
packetId,
System.currentTimeMillis()
);
boolean sent = roomContributionActivityCountService
.markRewardCoinsSent(activityCountId, packetId);
if (!sent) {
log.warn("房间周流水奖励红包已存在但状态标记失败 packetId={} activityCountId={}",
packetId, activityCountId);
}
return;
}
publishPlatformRedPacket(cmd, redPacket, packetId);
boolean sent = roomContributionActivityCountService
.markRewardCoinsSent(activityCountId, packetId);
if (!sent) {
log.warn("房间周流水奖励红包发布后状态标记失败 packetId={} activityCountId={}",
packetId, activityCountId);
}
} catch (Exception e) {
roomContributionActivityCountService
.resetRewardCoinsSending(activityCountId, packetId, e.getMessage());
log.error("房间周流水奖励红包发布失败 packetId={} activityCountId={} roomId={}",
packetId, activityCountId, cmd.getRoomId(), e);
}
}
private String buildRoomContributionRewardPacketId(String activityCountId) {
return "RW" + activityCountId.replace("_", "");
}
private boolean isExpired(RoomRedPacket redPacket) {
return Objects.nonNull(redPacket.getExpireTime())
&& !redPacket.getExpireTime().isAfter(LocalDateTime.now());
}
/**
@ -222,9 +274,14 @@ public class SendRoomRedPacketCmdExe {
build.put("packetId", packetId);
if (cmd.getTotalAmount() >= 30000) {
SysOriginPlatformEnum sysOrigin = SysOriginPlatformEnum
.toEnum(cmd.getReqSysOrigin().getOrigin());
if (Objects.isNull(sysOrigin)) {
return;
}
imGroupClient.sendMessageBroadcast(
BroadcastGroupMsgBodyCmd.builder()
.toPlatform(SysOriginPlatformEnum.ATYOU)
.toPlatform(sysOrigin)
.type(GroupMessageTypeEnum.ROOM_RED_PACKET)
.data(build)
.build()
@ -271,6 +328,23 @@ public class SendRoomRedPacketCmdExe {
}
}
private void publishPlatformRedPacket(SendRoomRedPacketCmd cmd, RoomRedPacket redPacket,
String packetId) {
List<Long> amounts = generateRandomAmounts(cmd.getTotalAmount(), cmd.getTotalCount());
long expireSeconds = cmd.getExpireMinutes() * 60;
roomRedPacketCacheService.setRandomAmounts(packetId, amounts, expireSeconds);
cacheRedPacketData(redPacket);
roomRedPacketCacheService.addToRoomList(
cmd.getRoomId(),
packetId,
System.currentTimeMillis()
);
sendMessage(cmd, packetId);
}
/**
* 扣除用户金币
*/

View File

@ -23,6 +23,7 @@ import com.red.circle.mq.business.model.event.gift.GameLuckyGiftBusinessEvent;
import com.red.circle.mq.business.model.event.gift.GameLuckyGiftUser;
import com.red.circle.mq.business.model.event.gift.GiveAwayGiftBatchEvent;
import com.red.circle.mq.business.model.event.gift.GiveAwayGiftRoomAccepts;
import com.red.circle.mq.business.model.event.gift.GiftSendSceneEnum;
import com.red.circle.mq.rocket.business.producer.GiftMqMessage;
import com.red.circle.mq.rocket.business.service.MessageSenderService;
import com.red.circle.mq.rocket.business.streams.GameLuckyGiftBusinessSink;
@ -318,6 +319,7 @@ public class GameLuckyGiftCommon {
.setQuantity(cmd.getQuantity())
.setGiftConfig(giftAppConvertor.toGiveGiftConfig(giftConfigDTO))
.setRoomId(cmd.getRoomId())
.setSendScene(GiftSendSceneEnum.ROOM)
.setTrackId(Long.parseLong(timeId))
.setBagGift(Boolean.FALSE)
.setCreateTime(TimestampUtils.now());

View File

@ -4,11 +4,13 @@ import com.google.common.collect.Lists;
import com.red.circle.framework.core.convertor.ConvertorModel;
import com.red.circle.mq.business.model.event.gift.GiveAwayGiftBatchEvent;
import com.red.circle.mq.business.model.event.gift.GiveAwayGiftRoomAccepts;
import com.red.circle.mq.business.model.event.gift.GiftSendSceneEnum;
import com.red.circle.mq.business.model.event.gift.GiveGiftConfig;
import com.red.circle.other.app.dto.clientobject.gift.GiftBackpackLimitCO;
import com.red.circle.other.app.dto.clientobject.gift.GiftConfigCO;
import com.red.circle.other.app.dto.cmd.gift.GiveAwayGiftBackpackCmd;
import com.red.circle.other.app.dto.cmd.gift.GiveAwayGiftBatchCmd;
import com.red.circle.other.app.dto.cmd.gift.GiveAwayPrivateGiftCmd;
import com.red.circle.other.infra.database.rds.entity.gift.GiftBackpackLimit;
import com.red.circle.other.infra.database.rds.entity.gift.GiftConfig;
import com.red.circle.other.inner.model.dto.material.GiftConfigDTO;
@ -49,6 +51,7 @@ public interface GiftAppConvertor {
.setQuantity(cmd.getQuantity())
.setGiftConfig(toGiveGiftConfig(giftConfig))
.setRoomId(cmd.getRoomId())
.setSendScene(GiftSendSceneEnum.ROOM)
.setDynamicContentId(cmd.getDynamicContentId())
.setTrackId(trackId)
.setSongId(cmd.getSongId())
@ -103,10 +106,32 @@ public interface GiftAppConvertor {
.setQuantity(cmd.getQuantity())
.setGiftConfig(toGiveGiftConfig(giftConfig))
.setRoomId(cmd.getRoomId())
.setSendScene(GiftSendSceneEnum.ROOM)
.setTrackId(trackId)
.setBagGift(Boolean.TRUE)
.setCreateTime(Objects.isNull(cmd.getReqTime()) ? TimestampUtils.now()
: new Timestamp(cmd.getReqTime().getTime()));
}
default GiveAwayGiftBatchEvent toGiveAwayPrivateGiftEvent(GiveAwayPrivateGiftCmd cmd,
GiftConfigDTO giftConfig, Long trackId) {
return new GiveAwayGiftBatchEvent()
.setSysOrigin(cmd.requireReqSysOriginEnum())
.setRequestPlatform(Objects.toString(cmd.getReqClient()))
.setSendUserId(cmd.requiredReqUserId())
.setAccepts(List.of(
new GiveAwayGiftRoomAccepts()
.setSeatIndex(null)
.setAcceptUserId(cmd.getAcceptUserId())
))
.setQuantity(cmd.getQuantity())
.setGiftConfig(toGiveGiftConfig(giftConfig))
.setRoomId(null)
.setSendScene(GiftSendSceneEnum.PRIVATE_CHAT)
.setTrackId(trackId)
.setBagGift(Boolean.FALSE)
.setCreateTime(Objects.isNull(cmd.getReqTime()) ? TimestampUtils.now()
: new Timestamp(cmd.getReqTime().getTime()));
}
}

View File

@ -11,6 +11,8 @@ import com.red.circle.component.mq.service.Action;
import com.red.circle.component.mq.service.ConsumerMessage;
import com.red.circle.component.mq.service.MessageListener;
import com.red.circle.external.inner.endpoint.message.ImGroupClient;
import com.red.circle.external.inner.endpoint.message.ImMessageClient;
import com.red.circle.external.inner.model.cmd.message.CustomC2CMsgBodyCmd;
import com.red.circle.external.inner.model.cmd.message.CustomGroupMsgBodyCmd;
import com.red.circle.external.inner.model.enums.message.GroupMessageTypeEnum;
import com.red.circle.framework.core.dto.ReqSysOrigin;
@ -33,12 +35,14 @@ import com.red.circle.other.app.command.game.roompk.GameRoomPkUserCmdExe;
import com.red.circle.other.app.command.game.teampk.GameTeamPkBaseInfoCmdExe;
import com.red.circle.other.app.common.gift.GameLuckyGiftCommon;
import com.red.circle.other.app.dto.clientobject.game.*;
import com.red.circle.other.app.dto.clientobject.gift.PrivateGiftImPayloadCO;
import com.red.circle.other.app.dto.clientobject.gift.SendGiftNotifyCO;
import com.red.circle.other.app.dto.cmd.game.barrage.GameUserEffectsCmd;
import com.red.circle.other.domain.gateway.user.ability.UserRegionGateway;
import com.red.circle.other.domain.live.LiveHeartbeatCache;
import com.red.circle.other.domain.model.user.ability.RegionConfig;
import com.red.circle.other.domain.ranking.RankingActivityType;
import com.red.circle.other.infra.database.cache.service.other.GiftCacheService;
import com.red.circle.other.infra.database.cache.service.other.EnumConfigCacheService;
import com.red.circle.other.infra.database.cache.service.other.LiveHeartbeatCacheService;
import com.red.circle.other.infra.database.mongo.entity.gift.GiftAcceptUser;
@ -59,6 +63,7 @@ import com.red.circle.other.inner.enums.config.EnumConfigKey;
import com.red.circle.other.inner.enums.material.GiftSpecialEnum;
import com.red.circle.other.inner.enums.team.TeamPolicyTypeEnum;
import com.red.circle.other.inner.model.dto.activity.props.ActivityRewardPropsDTO;
import com.red.circle.other.inner.model.dto.material.GiftConfigDTO;
import com.red.circle.other.inner.model.dto.user.UserProfileDTO;
import com.red.circle.tool.core.collection.CollectionUtils;
import com.red.circle.tool.core.date.TimestampUtils;
@ -86,6 +91,7 @@ import org.jetbrains.annotations.NotNull;
public class GiveGiftsListener implements MessageListener {
private static final String TAG = "礼物消息";
private static final String PRIVATE_GIFT_IM_DESC = "PRIVATE_GIFT";
private final GameMqMessage gameMqMessage;
private final GiftMqMessage giftMqMessage;
private final TeamMemberService teamMemberService;
@ -98,6 +104,8 @@ public class GiveGiftsListener implements MessageListener {
private final LiveHeartbeatCacheService liveHeartbeatCacheService;
private final GiftGiveRunningWaterService giftGiveRunningWaterService;
private final ImGroupClient imGroupClient;
private final ImMessageClient imMessageClient;
private final GiftCacheService giftCacheService;
private final RoomProfileManagerService roomProfileManagerService;
private final GameTeamPkBaseInfoCmdExe gameTeamPkBaseInfoCmdExe;
private final GameRoomPkUserCmdExe gameRoomPkUserCmdExe;
@ -370,6 +378,10 @@ public class GiveGiftsListener implements MessageListener {
return;
}
if (event.privateChatScene()) {
sendPrivateGiftIM(event);
}
if (!SysOriginPlatformEnum.isVoiceSystem(runningWater.getSysOrigin())) {
return;
}
@ -393,8 +405,47 @@ public class GiveGiftsListener implements MessageListener {
// 排行榜相关
offlineProcessGiftEvent(OfflineStatisticsGiftEventType.RANK_COUNT, runningWater);
// 火箭统计相关
offlineProcessGiftEvent(OfflineStatisticsGiftEventType.ROCKET_COUNT, runningWater);
if (!event.privateChatScene()) {
// 火箭统计相关
offlineProcessGiftEvent(OfflineStatisticsGiftEventType.ROCKET_COUNT, runningWater);
}
}
private void sendPrivateGiftIM(GiveAwayGiftBatchEvent event) {
if (CollectionUtils.isEmpty(event.getAccepts()) || event.getAccepts().get(0).getAcceptUserId() == null) {
log.warn("私聊礼物IM发送失败,接收人为空:{}", JacksonUtils.toJson(event));
return;
}
Long acceptUserId = event.getAccepts().get(0).getAcceptUserId();
try {
GiftConfigDTO giftConfig = giftCacheService.getById(event.giftId());
PrivateGiftImPayloadCO payload = new PrivateGiftImPayloadCO()
.setType(PRIVATE_GIFT_IM_DESC)
.setScene(event.sendSceneOrRoom().name())
.setTrackId(event.getTrackId())
.setSendUserId(event.getSendUserId())
.setAcceptUserId(acceptUserId)
.setGiftId(event.giftId())
.setGiftName(Objects.nonNull(giftConfig) ? giftConfig.getGiftName() : null)
.setGiftPhoto(Objects.nonNull(giftConfig) ? giftConfig.getGiftPhoto() : null)
.setGiftSourceUrl(Objects.nonNull(giftConfig) ? giftConfig.getGiftSourceUrl() : null)
.setGiftType(event.giftType())
.setGiftTab(event.getGiftConfig().getGiftTab())
.setGiftCandy(event.getGiftConfig().getGiftCandy())
.setQuantity(event.getQuantity())
.setCreateTime(getCreateTime(event).getTime());
imMessageClient.sendCustomMessage(CustomC2CMsgBodyCmd.builder()
.fromAccount(Objects.toString(event.getSendUserId()))
.toAccount(Objects.toString(acceptUserId))
.desc(PRIVATE_GIFT_IM_DESC)
.data(payload)
.build());
log.info("私聊礼物IM发送成功,trackId={},sendUserId={},acceptUserId={}",
event.getTrackId(), event.getSendUserId(), acceptUserId);
} catch (Exception e) {
log.warn("私聊礼物IM发送失败,trackId={},sendUserId={},acceptUserId={}",
event.getTrackId(), event.getSendUserId(), acceptUserId, e);
}
}
private Pair<GiftGiveRunningWater,Boolean> saveRunningWater(GiveAwayGiftBatchEvent event) {
@ -444,6 +495,7 @@ public class GiveGiftsListener implements MessageListener {
.processed(Boolean.FALSE)
.trackId(event.getTrackId().toString())
.originId(Objects.nonNull(event.getRoomId()) ? event.getRoomId().toString() : "OTHER")
.sendScene(event.sendSceneOrRoom().name())
.dynamicContentId(Objects.nonNull(event.getDynamicContentId()) ? event.getDynamicContentId().toString() : null)
.sysOrigin(event.getSysOrigin().name())
.requestPlatform(event.getRequestPlatform())

View File

@ -42,6 +42,8 @@ import org.apache.commons.lang3.StringUtils;
@RequiredArgsConstructor
public class OnlineStatusUploadListener implements MessageListener {
private static final long ACTIVE_BITMAP_MAX_OFFSET = 50_000_000L;
private final ExpandService expandService;
private final UserSVipGateway userSVipGateway;
private final WalletGoldClient walletGoldClient;
@ -109,7 +111,7 @@ public class OnlineStatusUploadListener implements MessageListener {
}
private void countActiveUser(UserProfile userProfile) {
long account = Objects.hash(userProfile.getAccount());
long account = activeBitmapOffset(userProfile.getAccount());
if (userProfileGateway.checkTeamMember(userProfile.getId())) {
countUserActiveIndexService.countAnchorActiveUser(account);
countUserActiveIndexService.countAnchorActiveUser(userProfile.getOriginSys(), account);
@ -120,6 +122,20 @@ public class OnlineStatusUploadListener implements MessageListener {
countUserActiveIndexService.countOrdinaryActiveUser(userProfile.getOriginSys(), account);
}
private long activeBitmapOffset(String account) {
if (StringUtils.isNumeric(account)) {
try {
long offset = Long.parseLong(account);
if (offset >= 0 && offset <= ACTIVE_BITMAP_MAX_OFFSET) {
return offset;
}
} catch (NumberFormatException ignored) {
// Keep unusual account ids bounded instead of expanding Redis bitmaps.
}
}
return Math.floorMod(Objects.hashCode(account), ACTIVE_BITMAP_MAX_OFFSET);
}
private void addOnlineUser(UserOnlineStatusEvent event, UserProfile userProfile) {
if (StringUtils.isBlank(event.getSessionId())) {
return;

View File

@ -0,0 +1,202 @@
package com.red.circle.other.app.scheduler;
import com.red.circle.common.business.core.enums.SysOriginPlatformEnum;
import com.red.circle.component.redis.annotation.TaskCacheLock;
import com.red.circle.framework.core.dto.ReqSysOrigin;
import com.red.circle.other.app.command.redpacket.SendRoomRedPacketCmdExe;
import com.red.circle.other.app.dto.cmd.SendRoomRedPacketCmd;
import com.red.circle.other.app.enums.RoomRewardLevelEnum;
import com.red.circle.other.infra.database.mongo.entity.activity.RoomContributionActivityCount;
import com.red.circle.other.infra.database.mongo.entity.live.RoomProfileManager;
import com.red.circle.other.infra.database.mongo.service.activity.RoomContributionActivityCountService;
import com.red.circle.other.infra.database.mongo.service.live.RoomProfileManagerService;
import com.red.circle.tool.core.collection.CollectionUtils;
import java.math.BigDecimal;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
/**
* 自动发送房间周流水奖励.
*/
@Slf4j
@Component
@RequiredArgsConstructor
@ConditionalOnProperty(name = "scheduler.room-contribution-weekly-reward", havingValue = "true",
matchIfMissing = true)
public class RoomContributionWeeklyRewardTask {
private static final Integer DEFAULT_EXPIRE_MINUTES = 10;
private static final Integer DEFAULT_TOTAL_COUNT = 10;
private static final BigDecimal DEFAULT_MIN_CONTRIBUTION = BigDecimal.valueOf(100000);
private static final BigDecimal TARAB_MIN_CONTRIBUTION = BigDecimal.valueOf(150000);
private final SendRoomRedPacketCmdExe sendRoomRedPacketCmdExe;
private final RoomProfileManagerService roomProfileManagerService;
private final RoomContributionActivityCountService roomContributionActivityCountService;
@Value("${activity.room-contribution.weekly-reward.expire-minutes:10}")
private Integer expireMinutes;
@Value("${activity.room-contribution.weekly-reward.total-count:10}")
private Integer totalCount;
@Value("${activity.room-contribution.weekly-reward.repair-enabled:true}")
private Boolean repairEnabled;
@Value("${activity.room-contribution.weekly-reward.sys-origins:ATYOU}")
private String enabledSysOrigins;
/**
* 每周一沙特时间 00:08 发送上周房间周流水奖励.
*/
@Scheduled(cron = "0 8 0 ? * MON", zone = "Asia/Riyadh")
@TaskCacheLock(key = "ROOM_CONTRIBUTION_WEEKLY_REWARD_TASK", expireSecond = 600)
public void sendLastWeekRoomContributionRewards() {
sendLastWeekRoomContributionRewards("weekly");
}
/**
* 启动后和每分钟补偿上周未发奖励防止周一任务异常后永久漏发.
*/
@Scheduled(
initialDelayString = "${activity.room-contribution.weekly-reward.repair-initial-delay-ms:60000}",
fixedDelayString = "${activity.room-contribution.weekly-reward.repair-fixed-delay-ms:60000}")
@TaskCacheLock(key = "ROOM_CONTRIBUTION_WEEKLY_REWARD_TASK", expireSecond = 60)
public void repairLastWeekRoomContributionRewards() {
if (!Boolean.TRUE.equals(repairEnabled)) {
return;
}
sendLastWeekRoomContributionRewards("repair");
}
private void sendLastWeekRoomContributionRewards(String trigger) {
log.warn("[RoomContributionWeeklyReward] start trigger={}", trigger);
List<RoomContributionActivityCount> list =
roomContributionActivityCountService.listLastWeekRewardUnsentData();
if (CollectionUtils.isEmpty(list)) {
log.warn("[RoomContributionWeeklyReward] no unsent reward data trigger={}", trigger);
return;
}
int success = 0;
int fail = 0;
for (RoomContributionActivityCount data : list) {
try {
if (sendReward(data)) {
success++;
}
} catch (Exception e) {
fail++;
log.error("[RoomContributionWeeklyReward] send failed roomId={} dataId={} amount={}",
data.getRoomId(), data.getId(), data.getContributionValue(), e);
}
}
log.warn("[RoomContributionWeeklyReward] end trigger={} total={} success={} fail={}",
trigger, list.size(), success, fail);
}
private boolean sendReward(RoomContributionActivityCount data) {
if (Objects.isNull(data) || Objects.isNull(data.getRoomId())
|| Objects.isNull(data.getContributionValue())) {
return false;
}
RoomProfileManager room = roomProfileManagerService.getById(data.getRoomId());
SysOriginPlatformEnum sysOrigin = getSysOrigin(room);
if (Objects.isNull(room) || Objects.isNull(sysOrigin)) {
log.warn("[RoomContributionWeeklyReward] skip invalid room roomId={} dataId={}",
data.getRoomId(), data.getId());
return false;
}
if (!getEnabledSysOrigins().contains(sysOrigin)) {
log.warn("[RoomContributionWeeklyReward] skip disabled sysOrigin roomId={} dataId={} sysOrigin={}",
data.getRoomId(), data.getId(), sysOrigin);
return false;
}
if (!meetsPlatformThreshold(sysOrigin, data.getContributionValue())) {
log.warn("[RoomContributionWeeklyReward] skip threshold roomId={} dataId={} sysOrigin={} amount={}",
data.getRoomId(), data.getId(), sysOrigin, data.getContributionValue());
return false;
}
RoomRewardLevelEnum level = RoomRewardLevelEnum
.getLevelByAmount(data.getContributionValue().longValue());
if (Objects.isNull(level)) {
return false;
}
SendRoomRedPacketCmd cmd = new SendRoomRedPacketCmd();
cmd.setRoomId(data.getRoomId());
cmd.setTotalAmount(level.getRewardCoins());
cmd.setTotalCount(getTotalCount(level.getRewardCoins()));
cmd.setExpireMinutes(getExpireMinutes());
cmd.setReqUserId(room.getUserId());
cmd.setReqSysOrigin(ReqSysOrigin.of(sysOrigin.name()));
if (Objects.isNull(sendRoomRedPacketCmdExe.executeRoomContributionReward(cmd, data.getId()))) {
return false;
}
log.info("[RoomContributionWeeklyReward] sent roomId={} dataId={} level={} rewardCoins={}",
data.getRoomId(), data.getId(), level.getLevel(), level.getRewardCoins());
return true;
}
private SysOriginPlatformEnum getSysOrigin(RoomProfileManager room) {
if (Objects.isNull(room) || Objects.isNull(room.getSysOrigin())) {
return null;
}
try {
return SysOriginPlatformEnum.valueOf(room.getSysOrigin());
} catch (IllegalArgumentException e) {
return null;
}
}
private boolean meetsPlatformThreshold(SysOriginPlatformEnum sysOrigin, BigDecimal contribution) {
BigDecimal minContribution = Objects.equals(sysOrigin, SysOriginPlatformEnum.TARAB)
? TARAB_MIN_CONTRIBUTION : DEFAULT_MIN_CONTRIBUTION;
return contribution.compareTo(minContribution) >= 0;
}
private Set<SysOriginPlatformEnum> getEnabledSysOrigins() {
if (Objects.isNull(enabledSysOrigins) || enabledSysOrigins.isBlank()) {
return Collections.emptySet();
}
return Arrays.stream(enabledSysOrigins.split(","))
.map(String::trim)
.filter(item -> !item.isBlank())
.map(SysOriginPlatformEnum::toEnum)
.filter(Objects::nonNull)
.collect(Collectors.toSet());
}
private Integer getExpireMinutes() {
if (Objects.isNull(expireMinutes) || !List.of(1, 3, 5, 10).contains(expireMinutes)) {
return DEFAULT_EXPIRE_MINUTES;
}
return expireMinutes;
}
private Integer getTotalCount(Long rewardCoins) {
int count = Objects.isNull(totalCount) ? DEFAULT_TOTAL_COUNT : totalCount;
count = Math.max(1, Math.min(count, 100));
if (Objects.nonNull(rewardCoins)) {
count = Math.min(count, rewardCoins.intValue());
}
return count;
}
}

View File

@ -18,6 +18,7 @@ import com.red.circle.other.app.dto.clientobject.gift.PremiumGiftWallCO;
import com.red.circle.other.app.dto.clientobject.gift.UserGiftBackpackCO;
import com.red.circle.other.app.dto.cmd.gift.GiveAwayGiftBackpackCmd;
import com.red.circle.other.app.dto.cmd.gift.GiveAwayGiftBatchCmd;
import com.red.circle.other.app.dto.cmd.gift.GiveAwayPrivateGiftCmd;
import java.math.BigDecimal;
import java.util.List;
import lombok.RequiredArgsConstructor;
@ -60,6 +61,11 @@ public class GiftServiceImpl implements GiftService {
return giftGiveAwayBatchCmdExe.execute(cmd);
}
@Override
public BigDecimal giveAwayPrivateGift(GiveAwayPrivateGiftCmd cmd) {
return giftGiveAwayBatchCmdExe.executePrivate(cmd);
}
@Override
public BigDecimal giveLuckyGift(GiveAwayGiftBatchCmd cmd) {
return luckyGiftGiveCmdExe.execute(cmd);

View File

@ -0,0 +1,74 @@
package com.red.circle.other.app.convertor.material;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import com.red.circle.common.business.core.enums.SysOriginPlatformEnum;
import com.red.circle.framework.core.dto.ReqSysOrigin;
import com.red.circle.framework.core.request.RequestClientEnum;
import com.red.circle.mq.business.model.event.gift.GiftSendSceneEnum;
import com.red.circle.other.app.dto.cmd.gift.GiveAwayGiftBatchCmd;
import com.red.circle.other.app.dto.cmd.gift.GiveAwayPrivateGiftCmd;
import com.red.circle.other.inner.model.dto.material.GiftConfigDTO;
import java.math.BigDecimal;
import java.util.List;
import org.junit.jupiter.api.Test;
import org.mapstruct.factory.Mappers;
class GiftAppConvertorTest {
private final GiftAppConvertor convertor = Mappers.getMapper(GiftAppConvertor.class);
@Test
void shouldBuildPrivateGiftEventWithOneAcceptAndNoRoom() {
GiveAwayPrivateGiftCmd cmd = new GiveAwayPrivateGiftCmd();
cmd.setReqUserId(1L);
cmd.setReqSysOrigin(ReqSysOrigin.of(SysOriginPlatformEnum.ATYOU.name()));
cmd.setReqClient(RequestClientEnum.Android);
cmd.setAcceptUserId(2L);
cmd.setGiftId(3L);
cmd.setQuantity(4);
var event = convertor.toGiveAwayPrivateGiftEvent(cmd, giftConfig(), 99L);
assertEquals(GiftSendSceneEnum.PRIVATE_CHAT, event.getSendScene());
assertTrue(event.privateChatScene());
assertNull(event.getRoomId());
assertEquals(1L, event.getSendUserId());
assertEquals(1, event.getAccepts().size());
assertEquals(2L, event.getAccepts().get(0).getAcceptUserId());
assertEquals(4, event.getQuantity());
assertEquals(99L, event.getTrackId());
}
@Test
void shouldKeepBatchGiftEventAsRoomScene() {
GiveAwayGiftBatchCmd cmd = new GiveAwayGiftBatchCmd();
cmd.setReqUserId(1L);
cmd.setReqSysOrigin(ReqSysOrigin.of(SysOriginPlatformEnum.ATYOU.name()));
cmd.setReqClient(RequestClientEnum.Android);
cmd.setAcceptUserIds(List.of(2L, 2L));
cmd.setGiftId(3L);
cmd.setQuantity(4);
cmd.setRoomId(5L);
var event = convertor.toGiveAwayGiftBatchEvent(cmd, giftConfig(), 99L);
assertEquals(GiftSendSceneEnum.ROOM, event.getSendScene());
assertFalse(event.privateChatScene());
assertEquals(5L, event.getRoomId());
assertEquals(1, event.getAccepts().size());
assertEquals(2L, event.getAccepts().get(0).getAcceptUserId());
}
private GiftConfigDTO giftConfig() {
return new GiftConfigDTO()
.setId(3L)
.setGiftCandy(new BigDecimal("10"))
.setGiftIntegral(BigDecimal.ZERO)
.setType("GOLD")
.setGiftTab("NORMAL");
}
}

View File

@ -0,0 +1,44 @@
package com.red.circle.other.app.dto.clientobject.gift;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
import java.io.Serial;
import java.io.Serializable;
import java.math.BigDecimal;
import lombok.Data;
import lombok.experimental.Accessors;
/**
* 私聊礼物 IM 自定义消息内容.
*/
@Data
@Accessors(chain = true)
public class PrivateGiftImPayloadCO implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
private String type;
private String scene;
@JsonSerialize(using = ToStringSerializer.class)
private Long trackId;
@JsonSerialize(using = ToStringSerializer.class)
private Long sendUserId;
@JsonSerialize(using = ToStringSerializer.class)
private Long acceptUserId;
@JsonSerialize(using = ToStringSerializer.class)
private Long giftId;
private String giftName;
private String giftPhoto;
private String giftSourceUrl;
private String giftType;
private String giftTab;
private BigDecimal giftCandy;
private Integer quantity;
private Long createTime;
}

View File

@ -0,0 +1,39 @@
package com.red.circle.other.app.dto.cmd.gift;
import com.red.circle.common.business.dto.cmd.AppExtCommand;
import jakarta.validation.constraints.NotNull;
import java.math.BigDecimal;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.hibernate.validator.constraints.Range;
/**
* 私聊赠送礼物.
*/
@Data
@EqualsAndHashCode(callSuper = false)
public class GiveAwayPrivateGiftCmd extends AppExtCommand {
/**
* 接收人.
*/
@NotNull(message = "acceptUserId required.")
private Long acceptUserId;
/**
* 礼物编号.
*/
@NotNull(message = "giftId required.")
private Long giftId;
/**
* 礼物数量.
*/
@Range(min = 1, max = 7777, message = "quantity range 1~7777")
@NotNull(message = "quantity required.")
private Integer quantity;
public BigDecimal calculateConsumeGolds(BigDecimal giftGolds) {
return giftGolds.multiply(BigDecimal.valueOf(quantity));
}
}

View File

@ -9,6 +9,7 @@ import com.red.circle.other.app.dto.clientobject.gift.PremiumGiftWallCO;
import com.red.circle.other.app.dto.clientobject.gift.UserGiftBackpackCO;
import com.red.circle.other.app.dto.cmd.gift.GiveAwayGiftBackpackCmd;
import com.red.circle.other.app.dto.cmd.gift.GiveAwayGiftBatchCmd;
import com.red.circle.other.app.dto.cmd.gift.GiveAwayPrivateGiftCmd;
import java.math.BigDecimal;
import java.util.List;
@ -27,6 +28,8 @@ public interface GiftService {
BigDecimal giveAwayGiftBatch(GiveAwayGiftBatchCmd cmd);
BigDecimal giveAwayPrivateGift(GiveAwayPrivateGiftCmd cmd);
BigDecimal giveLuckyGift(GiveAwayGiftBatchCmd cmd);
List<GiftWallCO> listUserGiftWall(AppUserIdCmd cmd);

View File

@ -36,6 +36,12 @@ public interface RoomRedPacketGateway {
*/
void updateRefundStatus(String packetId, Integer refundStatus);
/**
* 重置平台红包发布参数.
*/
void resetPlatformPacket(String packetId, Long totalAmount, Integer totalCount,
Integer expireMinutes, LocalDateTime expireTime);
/**
* 查询过期且未退款的红包
*/

View File

@ -62,6 +62,31 @@ public class RoomContributionActivityCount extends Convert implements Serializab
*/
Boolean rewardCoinsSent;
/**
* 奖励金币是否发送中.
*/
Boolean rewardCoinsSending;
/**
* 奖励金币红包ID.
*/
String rewardCoinsPacketId;
/**
* 奖励金币发送时间.
*/
Timestamp rewardCoinsSentTime;
/**
* 奖励金币发送中时间.
*/
Timestamp rewardCoinsSendingTime;
/**
* 奖励金币发送失败原因.
*/
String rewardCoinsSendError;
/**
* 比例.
*/

View File

@ -47,6 +47,11 @@ public class GiftGiveRunningWater implements Serializable {
*/
private String originId;
/**
* 发送场景.
*/
private String sendScene;
/**
* 动态id
*/

View File

@ -92,11 +92,26 @@ public interface RoomContributionActivityCountService {
*/
List<RoomContributionActivityCount> listRoomContributionByPH(Long roomId,BigDecimal number);
/**
* 更新奖励金币状态为已发放
*/
/**
* 更新奖励金币状态为已发放
*/
boolean updateRewardCoinsSent(String id);
/**
* 抢占奖励金币发送记录.
*/
boolean markRewardCoinsSending(String id, String packetId);
/**
* 标记奖励金币已发送.
*/
boolean markRewardCoinsSent(String id, String packetId);
/**
* 释放奖励金币发送中状态等待重试.
*/
void resetRewardCoinsSending(String id, String packetId, String reason);
/**
* 获取所有房间上周贡献数据未领取状态
*
@ -104,6 +119,13 @@ public interface RoomContributionActivityCountService {
*/
List<RoomContributionActivityCount> getAllRoomsLastWeekData();
/**
* 获取所有房间上周贡献数据平台奖励未发放状态
*
* @return 所有房间上周贡献数据列表
*/
List<RoomContributionActivityCount> listLastWeekRewardUnsentData();
/**
* 根据房间ID和日期查询数据
*

View File

@ -14,6 +14,7 @@ import com.red.circle.tool.core.date.ZonedDateTimeAsiaRiyadhUtils;
import com.red.circle.tool.core.text.StringUtils;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.format.DateTimeFormatter;
import java.util.Comparator;
import java.util.List;
import java.util.Objects;
@ -45,6 +46,7 @@ public class RoomContributionActivityCountServiceImpl implements
private final MongoPageHelper mongoPageHelper;
private static final BigDecimal integralMin = BigDecimal.valueOf(100000);
private static final BigDecimal otherIntegralMin = BigDecimal.valueOf(150000);
private static final long REWARD_SENDING_EXPIRE_MILLIS = 2 * 60 * 1000L;
private Criteria getUniquenessCriteria(Long roomId, Integer dateNumber) {
return Criteria.where("id").is(getUniqueId(roomId, dateNumber));
@ -291,10 +293,53 @@ public class RoomContributionActivityCountServiceImpl implements
return result.getModifiedCount() > 0;
}
@Override
public boolean markRewardCoinsSending(String id, String packetId) {
Query query = Query.query(new Criteria().andOperator(
Criteria.where("_id").is(id),
unsentRewardCriteria(),
rewardNotSendingOrExpiredCriteria()
));
Update update = new Update()
.set("rewardCoinsSending", Boolean.TRUE)
.set("rewardCoinsPacketId", packetId)
.set("rewardCoinsSendingTime", TimestampUtils.now())
.unset("rewardCoinsSendError");
UpdateResult result = mongoTemplate.updateFirst(query, update,
RoomContributionActivityCount.class);
return result.getModifiedCount() > 0;
}
@Override
public boolean markRewardCoinsSent(String id, String packetId) {
Query query = Query.query(Criteria.where("_id").is(id)
.and("rewardCoinsPacketId").is(packetId));
Update update = new Update()
.set("rewardCoinsSent", Boolean.TRUE)
.set("rewardCoinsSending", Boolean.FALSE)
.set("rewardCoinsSentTime", TimestampUtils.now())
.unset("rewardCoinsSendError");
UpdateResult result = mongoTemplate.updateFirst(query, update,
RoomContributionActivityCount.class);
return result.getModifiedCount() > 0;
}
@Override
public void resetRewardCoinsSending(String id, String packetId, String reason) {
Query query = Query.query(new Criteria().andOperator(
Criteria.where("_id").is(id),
Criteria.where("rewardCoinsPacketId").is(packetId),
unsentRewardCriteria()
));
Update update = new Update()
.set("rewardCoinsSending", Boolean.FALSE)
.set("rewardCoinsSendError", limitErrorReason(reason));
mongoTemplate.updateFirst(query, update, RoomContributionActivityCount.class);
}
@Override
public List<RoomContributionActivityCount> getAllRoomsLastWeekData() {
// 获取上周的 dateNumber
Integer lastWeekDateNumber = ZonedDateTimeAsiaRiyadhUtils.getThisMonday() - 7;
Integer lastWeekDateNumber = getLastWeekDateNumber();
Criteria criteria = Criteria.where("dateNumber").is(lastWeekDateNumber)
.and("received").is(Boolean.FALSE);
@ -302,6 +347,18 @@ public class RoomContributionActivityCountServiceImpl implements
return mongoTemplate.find(Query.query(criteria), RoomContributionActivityCount.class);
}
@Override
public List<RoomContributionActivityCount> listLastWeekRewardUnsentData() {
Criteria criteria = new Criteria().andOperator(
Criteria.where("dateNumber").is(getLastWeekDateNumber()),
Criteria.where("contributionValue").gte(integralMin),
unsentRewardCriteria(),
rewardNotSendingOrExpiredCriteria()
);
return mongoTemplate.find(Query.query(criteria), RoomContributionActivityCount.class);
}
@Override
public Optional<RoomContributionActivityCount> findByRoomIdAndDateNumber(Long roomId, Integer dateNumber) {
Criteria criteria = Criteria.where("roomId").is(roomId)
@ -315,4 +372,32 @@ public class RoomContributionActivityCountServiceImpl implements
return Optional.ofNullable(result);
}
private Integer getLastWeekDateNumber() {
return Integer.valueOf(ZonedDateTimeAsiaRiyadhUtils.getLastWeekMonday()
.format(DateTimeFormatter.ofPattern("yyyyMMdd")));
}
private Criteria unsentRewardCriteria() {
return new Criteria().orOperator(
Criteria.where("rewardCoinsSent").exists(false),
Criteria.where("rewardCoinsSent").ne(Boolean.TRUE)
);
}
private Criteria rewardNotSendingOrExpiredCriteria() {
return new Criteria().orOperator(
Criteria.where("rewardCoinsSending").exists(false),
Criteria.where("rewardCoinsSending").ne(Boolean.TRUE),
Criteria.where("rewardCoinsSendingTime")
.lt(new java.sql.Timestamp(System.currentTimeMillis() - REWARD_SENDING_EXPIRE_MILLIS))
);
}
private String limitErrorReason(String reason) {
if (StringUtils.isBlank(reason) || reason.length() <= 500) {
return reason;
}
return reason.substring(0, 500);
}
}

View File

@ -89,6 +89,7 @@ public class GiftGiveRunningWaterServiceImpl implements GiftGiveRunningWaterServ
.songId(document.getLong("songId"))
.trackId(document.getString("trackId"))
.originId(document.getString("originId"))
.sendScene(document.getString("sendScene"))
.dynamicContentId(document.getString("dynamicContentId"))
.sysOrigin(document.getString("sysOrigin"))
.requestPlatform(document.getString("requestPlatform"))

View File

@ -70,7 +70,7 @@ public class TeamPolicyManagerServiceImpl implements TeamPolicyManagerService {
.find(Query.query(Criteria.where("region").is(region)
.and("release").is(Boolean.TRUE)
.and("sysOrigin").is(sysOrigin))
.with(Sort.by(Sort.Order.desc("createTime"))),
.with(newestFirst()),
TeamPolicyManager.class);
}
@ -81,7 +81,7 @@ public class TeamPolicyManagerServiceImpl implements TeamPolicyManagerService {
Criteria.where("region").is(region)
.and("release").is(Boolean.TRUE)
.and("sysOrigin").is(sysOrigin)
), TeamPolicyManager.class);
).with(newestFirst()), TeamPolicyManager.class);
}
@Override
@ -98,7 +98,7 @@ public class TeamPolicyManagerServiceImpl implements TeamPolicyManagerService {
}
criteria.and("countryCode").is(countryCode);
return mongoTemplate.findOne(Query.query(criteria), TeamPolicyManager.class);
return mongoTemplate.findOne(Query.query(criteria).with(newestFirst()), TeamPolicyManager.class);
}
@Override
@ -109,7 +109,8 @@ public class TeamPolicyManagerServiceImpl implements TeamPolicyManagerService {
}
List<TeamPolicyManager> teamPolicyManagers = mongoTemplate.find(
Query.query(Criteria.where("region").in(regions).and("release").is(Boolean.TRUE)),
Query.query(Criteria.where("region").in(regions).and("release").is(Boolean.TRUE))
.with(newestFirst()),
TeamPolicyManager.class);
if (CollectionUtils.isEmpty(teamPolicyManagers)) {
@ -117,7 +118,7 @@ public class TeamPolicyManagerServiceImpl implements TeamPolicyManagerService {
}
return teamPolicyManagers.stream()
.collect(Collectors.toMap(TeamPolicyManager::getRegion, v -> v));
.collect(Collectors.toMap(TeamPolicyManager::getRegion, v -> v, (existing, replacement) -> existing));
}
@Override
@ -130,7 +131,8 @@ public class TeamPolicyManagerServiceImpl implements TeamPolicyManagerService {
// 查询所有 region 的已发布政策所有 countryCode
List<TeamPolicyManager> policies = mongoTemplate.find(
Query.query(Criteria.where("region").in(regions)
.and("release").is(Boolean.TRUE)),
.and("release").is(Boolean.TRUE))
.with(newestFirst()),
TeamPolicyManager.class);
if (CollectionUtils.isEmpty(policies)) {
@ -170,6 +172,7 @@ public class TeamPolicyManagerServiceImpl implements TeamPolicyManagerService {
&& StringUtils.isNotBlank(policyManager.getSysOrigin())) {
policyManager.setHistoryRelease(Boolean.TRUE);
disableOtherReleasedPolicies(policyManager);
}
policyManager.setPolicy(policyManager.getPolicy().stream()
@ -178,6 +181,33 @@ public class TeamPolicyManagerServiceImpl implements TeamPolicyManagerService {
mongoTemplate.save(policyManager);
}
/**
* 发布政策时保持同一作用域只有一个正在使用的政策避免端内按国家或默认政策匹配到旧版本
*/
private void disableOtherReleasedPolicies(TeamPolicyManager policyManager) {
Criteria criteria = Criteria.where("sysOrigin").is(policyManager.getSysOrigin())
.and("region").is(policyManager.getRegion())
.and("release").is(Boolean.TRUE)
.and("id").ne(policyManager.getId());
if (Objects.equals(policyManager.getPolicyType(), TeamPolicyTypeEnum.SALARY_DIAMOND.name())) {
criteria.and("policyType").is(TeamPolicyTypeEnum.SALARY_DIAMOND.name());
} else {
criteria.and("policyType").ne(TeamPolicyTypeEnum.SALARY_DIAMOND.name());
}
if (StringUtils.isNotBlank(policyManager.getCountryCode())) {
criteria.and("countryCode").is(policyManager.getCountryCode());
}
mongoTemplate.updateMulti(Query.query(criteria),
Update.update("release", Boolean.FALSE), TeamPolicyManager.class);
}
private Sort newestFirst() {
return Sort.by(Sort.Order.desc("createTime"));
}
@Override
public void deleteById(Long id) {

View File

@ -36,6 +36,15 @@ public interface RoomRedPacketDAO {
*/
int updateRefundStatus(@Param("packetId") String packetId, @Param("refundStatus") Integer refundStatus);
/**
* 重置平台红包发布参数
*/
int resetPlatformPacket(@Param("packetId") String packetId,
@Param("totalAmount") Long totalAmount,
@Param("totalCount") Integer totalCount,
@Param("expireMinutes") Integer expireMinutes,
@Param("expireTime") LocalDateTime expireTime);
/**
* 查询过期且未退款的红包
*/

View File

@ -53,6 +53,13 @@ public class RoomRedPacketGatewayImpl implements RoomRedPacketGateway {
roomRedPacketDAO.updateRefundStatus(packetId, refundStatus);
}
@Override
public void resetPlatformPacket(String packetId, Long totalAmount, Integer totalCount,
Integer expireMinutes, LocalDateTime expireTime) {
roomRedPacketDAO.resetPlatformPacket(packetId, totalAmount, totalCount, expireMinutes,
expireTime);
}
@Override
public List<RoomRedPacket> findExpiredAndNotRefunded(LocalDateTime now) {
List<RoomRedPacketEntity> entities = roomRedPacketDAO.selectExpiredAndNotRefunded(now);

View File

@ -75,6 +75,21 @@
WHERE packet_id = #{packetId}
</update>
<update id="resetPlatformPacket">
UPDATE room_red_packet
SET total_amount = #{totalAmount},
total_count = #{totalCount},
remain_amount = #{totalAmount},
remain_count = #{totalCount},
expire_minutes = #{expireMinutes},
expire_time = #{expireTime},
status = 1,
refund_status = 0,
update_time = NOW()
WHERE packet_id = #{packetId}
AND source_type = 2
</update>
<select id="selectExpiredAndNotRefunded" resultMap="BaseResultMap">
SELECT
<include refid="Base_Column_List"/>