Compare commits
8 Commits
06e7828b85
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| e657a22b10 | |||
| d654777a02 | |||
| c8f8311cae | |||
| 20f8d9c152 | |||
| 52727dfd7c | |||
| 9b4819900d | |||
| cdfeace2f1 | |||
| 3665596c1f |
@@ -85,6 +85,7 @@ public enum ErrorCode {
|
||||
REPORT_TYPE_INVALID(40020, "举报类型无效"),
|
||||
REPORT_COMPANION_ID_EMPTY(40021, "被举报的AI角色ID不能为空"),
|
||||
REPORT_TYPE_EMPTY(40022, "举报类型不能为空"),
|
||||
ACCOUNT_RECENTLY_CANCELLED(50038, "账号注销未满7天,暂不允许注册"),
|
||||
VERSION_NOT_FOUND(40022, "未找到可用的版本配置");
|
||||
|
||||
/**
|
||||
|
||||
@@ -7,6 +7,7 @@ import com.yolo.keyborad.common.ResultUtils;
|
||||
import com.yolo.keyborad.exception.BusinessException;
|
||||
import com.yolo.keyborad.model.dto.AppleReceiptValidationResult;
|
||||
import com.yolo.keyborad.service.ApplePurchaseService;
|
||||
import com.yolo.keyborad.service.KeyboardUserPurchaseRecordsService;
|
||||
import com.yolo.keyborad.service.AppleReceiptService;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
@@ -23,11 +24,14 @@ public class AppleReceiptController {
|
||||
|
||||
private final AppleReceiptService appleReceiptService;
|
||||
private final ApplePurchaseService applePurchaseService;
|
||||
private final KeyboardUserPurchaseRecordsService purchaseRecordsService;
|
||||
|
||||
public AppleReceiptController(AppleReceiptService appleReceiptService,
|
||||
ApplePurchaseService applePurchaseService) {
|
||||
ApplePurchaseService applePurchaseService,
|
||||
KeyboardUserPurchaseRecordsService purchaseRecordsService) {
|
||||
this.appleReceiptService = appleReceiptService;
|
||||
this.applePurchaseService = applePurchaseService;
|
||||
this.purchaseRecordsService = purchaseRecordsService;
|
||||
}
|
||||
|
||||
@PostMapping("/receipt")
|
||||
@@ -85,4 +89,26 @@ public class AppleReceiptController {
|
||||
return ResultUtils.success(Boolean.TRUE);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查购买记录是否存在
|
||||
* 根据 transactionId 和 originalTransactionId 查询购买记录
|
||||
*
|
||||
* @param body 请求体,包含 transactionId 和 originalTransactionId
|
||||
* @return 存在返回 true,不存在返回 false
|
||||
*/
|
||||
@PostMapping("/check-purchase")
|
||||
public BaseResponse<Boolean> checkPurchaseExists(@RequestBody Map<String, String> body) {
|
||||
if (body == null) {
|
||||
throw new BusinessException(ErrorCode.PARAMS_ERROR, "body 不能为空");
|
||||
}
|
||||
String transactionId = body.get("transactionId");
|
||||
String originalTransactionId = body.get("originalTransactionId");
|
||||
if ((transactionId == null || transactionId.isBlank())
|
||||
&& (originalTransactionId == null || originalTransactionId.isBlank())) {
|
||||
throw new BusinessException(ErrorCode.PARAMS_ERROR, "transactionId 和 originalTransactionId 不能同时为空");
|
||||
}
|
||||
|
||||
boolean exists = purchaseRecordsService.checkPurchaseExists(transactionId, originalTransactionId);
|
||||
return ResultUtils.success(exists);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -146,7 +146,7 @@ public class GooglePlayApiClient {
|
||||
JsonNode offerDetails = firstLineItem.path("productOfferDetails");
|
||||
|
||||
String state = mapOneTimeState(text(root.path("purchaseStateContext"), "purchaseState"));
|
||||
|
||||
String purchaseOptionId = text(offerDetails, "purchaseOptionId");
|
||||
// 修正:一次性购买的订单号字段名为 "orderId"
|
||||
String googleOrderId = text(root, "orderId");
|
||||
|
||||
@@ -156,6 +156,7 @@ public class GooglePlayApiClient {
|
||||
.productId(text(firstLineItem, "productId"))
|
||||
.productType(GooglePlayConstants.PRODUCT_TYPE_ONE_TIME)
|
||||
.purchaseToken(purchaseToken)
|
||||
.purchaseOptionId(purchaseOptionId)
|
||||
.orderKey(resolveOrderKey(googleOrderId, purchaseToken))
|
||||
.googleOrderId(googleOrderId)
|
||||
.linkedPurchaseToken(null)
|
||||
|
||||
@@ -25,6 +25,7 @@ public final class GooglePlayConstants {
|
||||
public static final String CONSUMPTION_NOT_APPLICABLE = "NOT_APPLICABLE";
|
||||
|
||||
public static final String DELIVERY_PENDING = "PENDING";
|
||||
public static final String DELIVERY_PROCESSING = "PROCESSING";
|
||||
public static final String DELIVERY_DELIVERED = "DELIVERED";
|
||||
public static final String DELIVERY_REVOKED = "REVOKED";
|
||||
public static final String DELIVERY_NOT_REQUIRED = "NOT_REQUIRED";
|
||||
|
||||
@@ -22,6 +22,7 @@ public class GooglePlayEntitlementApplier {
|
||||
private final GooglePlayUserEntitlementMapper entitlementMapper;
|
||||
private final GooglePlayVipBenefitService vipBenefitService;
|
||||
private final GooglePlayWalletBenefitService walletBenefitService;
|
||||
private final GooglePlayOrderDeliveryGuard orderDeliveryGuard;
|
||||
|
||||
public GooglePlayUserEntitlement apply(Long userId,
|
||||
KeyboardProductItems product,
|
||||
@@ -29,6 +30,7 @@ public class GooglePlayEntitlementApplier {
|
||||
GooglePlayOrder order) {
|
||||
String benefitType = resolveBenefitType(product, snapshot);
|
||||
String entitlementKey = resolveEntitlementKey(benefitType, product.getProductId());
|
||||
boolean grantOwned = orderDeliveryGuard.prepareGrant(benefitType, snapshot, order);
|
||||
GooglePlayUserEntitlement entitlement = loadEntitlement(snapshot.getPurchaseToken(), entitlementKey);
|
||||
if (entitlement == null) {
|
||||
entitlement = new GooglePlayUserEntitlement();
|
||||
@@ -37,9 +39,11 @@ public class GooglePlayEntitlementApplier {
|
||||
fillCommonFields(entitlement, userId, product, snapshot, order, benefitType, entitlementKey);
|
||||
switch (benefitType) {
|
||||
case GooglePlayConstants.ENTITLEMENT_VIP_SUBSCRIPTION -> applySubscriptionVip(userId, product, snapshot, order, entitlement);
|
||||
case GooglePlayConstants.ENTITLEMENT_VIP_ONE_TIME -> applyOneTimeVip(userId, product, snapshot, order, entitlement);
|
||||
case GooglePlayConstants.ENTITLEMENT_WALLET_TOP_UP -> applyWalletTopUp(userId, product, snapshot, order, entitlement);
|
||||
default -> applyNonConsumable(snapshot, order, entitlement);
|
||||
case GooglePlayConstants.ENTITLEMENT_VIP_ONE_TIME ->
|
||||
applyOneTimeVip(userId, product, snapshot, order, entitlement, grantOwned);
|
||||
case GooglePlayConstants.ENTITLEMENT_WALLET_TOP_UP ->
|
||||
applyWalletTopUp(userId, product, snapshot, order, entitlement, grantOwned);
|
||||
default -> applyNonConsumable(snapshot, order, entitlement, grantOwned);
|
||||
}
|
||||
saveEntitlement(entitlement);
|
||||
return entitlement;
|
||||
@@ -94,9 +98,10 @@ public class GooglePlayEntitlementApplier {
|
||||
KeyboardProductItems product,
|
||||
GooglePlayPurchaseSnapshot snapshot,
|
||||
GooglePlayOrder order,
|
||||
GooglePlayUserEntitlement entitlement) {
|
||||
GooglePlayUserEntitlement entitlement,
|
||||
boolean grantOwned) {
|
||||
if (GooglePlayConstants.STATE_ACTIVE.equals(snapshot.getState())) {
|
||||
grantOneTimeVip(userId, product, order, entitlement);
|
||||
grantOneTimeVip(userId, product, order, entitlement, grantOwned);
|
||||
return;
|
||||
}
|
||||
revokeVipEntitlement(userId, order, entitlement);
|
||||
@@ -106,13 +111,14 @@ public class GooglePlayEntitlementApplier {
|
||||
KeyboardProductItems product,
|
||||
GooglePlayPurchaseSnapshot snapshot,
|
||||
GooglePlayOrder order,
|
||||
GooglePlayUserEntitlement entitlement) {
|
||||
GooglePlayUserEntitlement entitlement,
|
||||
boolean grantOwned) {
|
||||
BigDecimal amount = resolveWalletAmount(product);
|
||||
if (amount.compareTo(BigDecimal.ZERO) <= 0) {
|
||||
throw new BusinessException(ErrorCode.PRODUCT_QUOTA_NOT_SET);
|
||||
}
|
||||
if (GooglePlayConstants.STATE_ACTIVE.equals(snapshot.getState())) {
|
||||
grantWalletTopUp(userId, product, order, entitlement, amount);
|
||||
grantWalletTopUp(userId, product, order, entitlement, amount, grantOwned);
|
||||
return;
|
||||
}
|
||||
revokeWalletTopUp(userId, order, entitlement, amount);
|
||||
@@ -120,9 +126,14 @@ public class GooglePlayEntitlementApplier {
|
||||
|
||||
private void applyNonConsumable(GooglePlayPurchaseSnapshot snapshot,
|
||||
GooglePlayOrder order,
|
||||
GooglePlayUserEntitlement entitlement) {
|
||||
GooglePlayUserEntitlement entitlement,
|
||||
boolean grantOwned) {
|
||||
boolean active = GooglePlayConstants.STATE_ACTIVE.equals(snapshot.getState())
|
||||
|| GooglePlayConstants.STATE_CANCELED.equals(snapshot.getState());
|
||||
if (!grantOwned && active) {
|
||||
entitlement.setActive(GooglePlayConstants.DELIVERY_DELIVERED.equals(order.getDeliveryStatus()));
|
||||
return;
|
||||
}
|
||||
entitlement.setActive(active);
|
||||
entitlement.setStartTime(snapshot.getStartTime());
|
||||
entitlement.setEndTime(snapshot.getExpiryTime());
|
||||
@@ -139,9 +150,10 @@ public class GooglePlayEntitlementApplier {
|
||||
private void grantOneTimeVip(Long userId,
|
||||
KeyboardProductItems product,
|
||||
GooglePlayOrder order,
|
||||
GooglePlayUserEntitlement entitlement) {
|
||||
if (GooglePlayConstants.DELIVERY_DELIVERED.equals(order.getDeliveryStatus())) {
|
||||
entitlement.setActive(true);
|
||||
GooglePlayUserEntitlement entitlement,
|
||||
boolean grantOwned) {
|
||||
if (!grantOwned || GooglePlayConstants.DELIVERY_DELIVERED.equals(order.getDeliveryStatus())) {
|
||||
entitlement.setActive(GooglePlayConstants.DELIVERY_DELIVERED.equals(order.getDeliveryStatus()));
|
||||
return;
|
||||
}
|
||||
Date expiry = resolveOneTimeVipExpiry(product);
|
||||
@@ -172,12 +184,13 @@ public class GooglePlayEntitlementApplier {
|
||||
KeyboardProductItems product,
|
||||
GooglePlayOrder order,
|
||||
GooglePlayUserEntitlement entitlement,
|
||||
BigDecimal amount) {
|
||||
if (GooglePlayConstants.DELIVERY_DELIVERED.equals(order.getDeliveryStatus())) {
|
||||
entitlement.setActive(true);
|
||||
BigDecimal amount,
|
||||
boolean grantOwned) {
|
||||
if (!grantOwned || GooglePlayConstants.DELIVERY_DELIVERED.equals(order.getDeliveryStatus())) {
|
||||
entitlement.setActive(GooglePlayConstants.DELIVERY_DELIVERED.equals(order.getDeliveryStatus()));
|
||||
return;
|
||||
}
|
||||
walletBenefitService.grant(userId, order.getId(), product.getProductId(), amount);
|
||||
walletBenefitService.grant(userId, order.getId(), product.getName(), amount);
|
||||
entitlement.setActive(true);
|
||||
entitlement.setQuantity(amount);
|
||||
entitlement.setStartTime(new Date());
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
package com.yolo.keyborad.googleplay;
|
||||
|
||||
import com.yolo.keyborad.googleplay.model.GooglePlayPurchaseSnapshot;
|
||||
import com.yolo.keyborad.mapper.GooglePlayOrderMapper;
|
||||
import com.yolo.keyborad.model.entity.googleplay.GooglePlayOrder;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class GooglePlayOrderDeliveryGuard {
|
||||
|
||||
private final GooglePlayOrderMapper orderMapper;
|
||||
|
||||
/**
|
||||
* 一次性商品在真正发权益前先抢占处理资格,避免并发请求重复发货。
|
||||
*/
|
||||
public boolean prepareGrant(String benefitType, GooglePlayPurchaseSnapshot snapshot, GooglePlayOrder order) {
|
||||
if (!requiresGrantGuard(benefitType, snapshot) || order.getId() == null) {
|
||||
order.setDeliveryOwnershipGranted(true);
|
||||
return true;
|
||||
}
|
||||
if (GooglePlayConstants.DELIVERY_DELIVERED.equals(order.getDeliveryStatus())) {
|
||||
order.setDeliveryOwnershipGranted(false);
|
||||
return false;
|
||||
}
|
||||
Date now = new Date();
|
||||
int updated = orderMapper.updateDeliveryStatusIfMatch(
|
||||
order.getId(),
|
||||
GooglePlayConstants.DELIVERY_PENDING,
|
||||
GooglePlayConstants.DELIVERY_PROCESSING,
|
||||
now
|
||||
);
|
||||
if (updated == 1) {
|
||||
order.setDeliveryStatus(GooglePlayConstants.DELIVERY_PROCESSING);
|
||||
order.setDeliveryOwnershipGranted(true);
|
||||
order.setUpdatedAt(now);
|
||||
return true;
|
||||
}
|
||||
refreshDeliveryState(order);
|
||||
order.setDeliveryOwnershipGranted(false);
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean requiresGrantGuard(String benefitType, GooglePlayPurchaseSnapshot snapshot) {
|
||||
if (!GooglePlayConstants.PRODUCT_TYPE_ONE_TIME.equals(snapshot.getProductType())) {
|
||||
return false;
|
||||
}
|
||||
if (GooglePlayConstants.ENTITLEMENT_NON_CONSUMABLE.equals(benefitType)) {
|
||||
return GooglePlayConstants.STATE_ACTIVE.equals(snapshot.getState())
|
||||
|| GooglePlayConstants.STATE_CANCELED.equals(snapshot.getState());
|
||||
}
|
||||
return GooglePlayConstants.STATE_ACTIVE.equals(snapshot.getState());
|
||||
}
|
||||
|
||||
private void refreshDeliveryState(GooglePlayOrder order) {
|
||||
GooglePlayOrder latestOrder = orderMapper.selectById(order.getId());
|
||||
if (latestOrder == null) {
|
||||
return;
|
||||
}
|
||||
order.setDeliveryStatus(latestOrder.getDeliveryStatus());
|
||||
order.setGrantedQuantity(latestOrder.getGrantedQuantity());
|
||||
order.setUpdatedAt(latestOrder.getUpdatedAt());
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,7 @@ import com.yolo.keyborad.model.entity.googleplay.GooglePlayUserEntitlement;
|
||||
import com.yolo.keyborad.service.KeyboardProductItemsService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.dao.DuplicateKeyException;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
@@ -32,11 +33,13 @@ public class GooglePlayStateService {
|
||||
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public GooglePlaySyncResult sync(GooglePlaySyncCommand command, GooglePlayPurchaseSnapshot snapshot) {
|
||||
KeyboardProductItems product = loadProduct(snapshot.getBasePlanId());
|
||||
String productId = (snapshot.getBasePlanId() != null) ?
|
||||
snapshot.getBasePlanId() :
|
||||
snapshot.getPurchaseOptionId();
|
||||
KeyboardProductItems product = loadProduct(productId);
|
||||
GooglePlayOrder order = buildOrder(command, snapshot);
|
||||
GooglePlayPurchaseToken token = buildToken(command, snapshot);
|
||||
// 先保存订单以确保 order.id 已生成,钱包充值等权益分发依赖 order.id 写入交易流水
|
||||
saveOrder(order);
|
||||
persistOrderIfNew(order);
|
||||
GooglePlayUserEntitlement entitlement = null;
|
||||
if (command.getUserId() != null) {
|
||||
entitlement = entitlementApplier.apply(command.getUserId(), product, snapshot, order);
|
||||
@@ -51,8 +54,8 @@ public class GooglePlayStateService {
|
||||
.order(order)
|
||||
.token(token)
|
||||
.entitlement(entitlement)
|
||||
.acknowledgeRequired(requiresAcknowledge(snapshot, command.getUserId()))
|
||||
.consumeRequired(requiresConsume(snapshot, entitlement, command.getUserId()))
|
||||
.acknowledgeRequired(requiresAcknowledge(snapshot, command.getUserId(), order))
|
||||
.consumeRequired(requiresConsume(snapshot, entitlement, command.getUserId(), order))
|
||||
.linkedPurchaseTokenToSync(resolveLinkedToken(snapshot))
|
||||
.build();
|
||||
}
|
||||
@@ -144,15 +147,22 @@ public class GooglePlayStateService {
|
||||
return token;
|
||||
}
|
||||
|
||||
private boolean requiresAcknowledge(GooglePlayPurchaseSnapshot snapshot, Long userId) {
|
||||
return userId != null
|
||||
&& GooglePlayConstants.ACK_PENDING.equals(snapshot.getAcknowledgementState())
|
||||
&& GooglePlayConstants.STATE_ACTIVE.equals(snapshot.getState());
|
||||
private boolean requiresAcknowledge(GooglePlayPurchaseSnapshot snapshot, Long userId, GooglePlayOrder order) {
|
||||
if (userId == null
|
||||
|| !GooglePlayConstants.ACK_PENDING.equals(snapshot.getAcknowledgementState())
|
||||
|| !GooglePlayConstants.STATE_ACTIVE.equals(snapshot.getState())) {
|
||||
return false;
|
||||
}
|
||||
if (!GooglePlayConstants.PRODUCT_TYPE_ONE_TIME.equals(snapshot.getProductType())) {
|
||||
return true;
|
||||
}
|
||||
return Boolean.TRUE.equals(order.getDeliveryOwnershipGranted());
|
||||
}
|
||||
|
||||
private boolean requiresConsume(GooglePlayPurchaseSnapshot snapshot,
|
||||
GooglePlayUserEntitlement entitlement,
|
||||
Long userId) {
|
||||
Long userId,
|
||||
GooglePlayOrder order) {
|
||||
if (userId == null || entitlement == null) {
|
||||
return false;
|
||||
}
|
||||
@@ -160,7 +170,8 @@ public class GooglePlayStateService {
|
||||
boolean wallet = GooglePlayConstants.ENTITLEMENT_WALLET_TOP_UP.equals(entitlement.getBenefitType());
|
||||
boolean active = GooglePlayConstants.STATE_ACTIVE.equals(snapshot.getState());
|
||||
boolean pending = GooglePlayConstants.CONSUMPTION_PENDING.equals(snapshot.getConsumptionState());
|
||||
return oneTime && wallet && active && pending;
|
||||
boolean owned = Boolean.TRUE.equals(order.getDeliveryOwnershipGranted());
|
||||
return oneTime && wallet && active && pending && owned;
|
||||
}
|
||||
|
||||
private String resolveLinkedToken(GooglePlayPurchaseSnapshot snapshot) {
|
||||
@@ -172,20 +183,67 @@ public class GooglePlayStateService {
|
||||
|
||||
private void saveOrder(GooglePlayOrder order) {
|
||||
if (order.getId() == null) {
|
||||
orderMapper.insert(order);
|
||||
insertOrder(order);
|
||||
return;
|
||||
}
|
||||
orderMapper.updateById(order);
|
||||
}
|
||||
|
||||
/**
|
||||
* 仅在新订单场景预落库,避免并发请求用旧快照把发货状态回写成 PENDING。
|
||||
*/
|
||||
private void persistOrderIfNew(GooglePlayOrder order) {
|
||||
if (order.getId() == null) {
|
||||
saveOrder(order);
|
||||
}
|
||||
}
|
||||
|
||||
private void saveToken(GooglePlayPurchaseToken token) {
|
||||
if (token.getId() == null) {
|
||||
purchaseTokenMapper.insert(token);
|
||||
insertToken(token);
|
||||
return;
|
||||
}
|
||||
purchaseTokenMapper.updateById(token);
|
||||
}
|
||||
|
||||
/**
|
||||
* 并发首写同一订单时,唯一键冲突后回读已存在记录继续流程。
|
||||
*/
|
||||
private void insertOrder(GooglePlayOrder order) {
|
||||
try {
|
||||
orderMapper.insert(order);
|
||||
} catch (DuplicateKeyException e) {
|
||||
GooglePlayOrder existingOrder = orderMapper.selectOne(Wrappers.<GooglePlayOrder>lambdaQuery()
|
||||
.eq(GooglePlayOrder::getOrderKey, order.getOrderKey())
|
||||
.last("LIMIT 1"));
|
||||
if (existingOrder == null) {
|
||||
throw e;
|
||||
}
|
||||
order.setId(existingOrder.getId());
|
||||
order.setCreatedAt(existingOrder.getCreatedAt());
|
||||
order.setDeliveryStatus(existingOrder.getDeliveryStatus());
|
||||
order.setGrantedQuantity(existingOrder.getGrantedQuantity());
|
||||
orderMapper.updateById(order);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* purchaseToken 首次并发写入时回读已有记录,避免重复插入直接失败。
|
||||
*/
|
||||
private void insertToken(GooglePlayPurchaseToken token) {
|
||||
try {
|
||||
purchaseTokenMapper.insert(token);
|
||||
} catch (DuplicateKeyException e) {
|
||||
GooglePlayPurchaseToken existingToken = findToken(token.getPurchaseToken());
|
||||
if (existingToken == null) {
|
||||
throw e;
|
||||
}
|
||||
token.setId(existingToken.getId());
|
||||
token.setCreatedAt(existingToken.getCreatedAt());
|
||||
purchaseTokenMapper.updateById(token);
|
||||
}
|
||||
}
|
||||
|
||||
private void updateAckState(String purchaseToken, String orderKey, String state) {
|
||||
GooglePlayPurchaseToken token = findToken(purchaseToken);
|
||||
if (token != null) {
|
||||
|
||||
@@ -20,7 +20,7 @@ public class GooglePlayWalletBenefitService {
|
||||
private final KeyboardUserWalletService walletService;
|
||||
private final KeyboardWalletTransactionService walletTransactionService;
|
||||
|
||||
public void grant(Long userId, Long orderId, String productId, BigDecimal amount) {
|
||||
public void grant(Long userId, Long orderId, String productName, BigDecimal amount) {
|
||||
KeyboardUserWallet wallet = getOrCreateWallet(userId);
|
||||
BigDecimal before = defaultBalance(wallet.getBalance());
|
||||
BigDecimal after = before.add(amount);
|
||||
@@ -28,7 +28,7 @@ public class GooglePlayWalletBenefitService {
|
||||
wallet.setUpdatedAt(new Date());
|
||||
walletService.saveOrUpdate(wallet);
|
||||
walletTransactionService.createTransaction(userId, orderId, amount, GOOGLE_PLAY_WALLET_TX_TYPE,
|
||||
before, after, "Google Play 充值: " + productId);
|
||||
before, after, productName);
|
||||
}
|
||||
|
||||
public boolean revoke(Long userId, Long orderId, BigDecimal amount) {
|
||||
|
||||
@@ -56,4 +56,6 @@ public class GooglePlayPurchaseSnapshot {
|
||||
private Date lastSyncedAt;
|
||||
|
||||
private String rawResponse;
|
||||
|
||||
private String purchaseOptionId;
|
||||
}
|
||||
|
||||
@@ -2,6 +2,23 @@ package com.yolo.keyborad.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.yolo.keyborad.model.entity.googleplay.GooglePlayOrder;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import org.apache.ibatis.annotations.Update;
|
||||
|
||||
public interface GooglePlayOrderMapper extends BaseMapper<GooglePlayOrder> {
|
||||
|
||||
/**
|
||||
* 原子抢占发货资格,只有当前状态匹配时才允许进入处理中。
|
||||
*/
|
||||
@Update("""
|
||||
UPDATE google_play_order
|
||||
SET delivery_status = #{targetStatus},
|
||||
updated_at = #{updatedAt}
|
||||
WHERE id = #{orderId}
|
||||
AND delivery_status = #{expectedStatus}
|
||||
""")
|
||||
int updateDeliveryStatusIfMatch(@Param("orderId") Long orderId,
|
||||
@Param("expectedStatus") String expectedStatus,
|
||||
@Param("targetStatus") String targetStatus,
|
||||
@Param("updatedAt") java.util.Date updatedAt);
|
||||
}
|
||||
|
||||
@@ -132,4 +132,8 @@ public class KeyboardUser {
|
||||
@TableField(value = "vip_level")
|
||||
@Schema(description = "vip等级")
|
||||
private Integer vipLevel;
|
||||
|
||||
@TableField(value = "uuid")
|
||||
@Schema(description = "uuid")
|
||||
private String uuid;
|
||||
}
|
||||
|
||||
@@ -81,4 +81,10 @@ public class GooglePlayOrder {
|
||||
|
||||
@TableField("updated_at")
|
||||
private Date updatedAt;
|
||||
|
||||
/**
|
||||
* 当前线程是否拿到了本次发货资格,仅用于本次请求内控制幂等,不落库。
|
||||
*/
|
||||
@TableField(exist = false)
|
||||
private Boolean deliveryOwnershipGranted;
|
||||
}
|
||||
|
||||
@@ -57,4 +57,6 @@ public class KeyboardUserInfoRespVO {
|
||||
@Schema(description = "vip等级")
|
||||
private Integer vipLevel;
|
||||
|
||||
@Schema(description = "uuid")
|
||||
private String uuid;
|
||||
}
|
||||
@@ -59,4 +59,8 @@ public class KeyboardUserRespVO {
|
||||
@TableField(value = "vip_level")
|
||||
@Schema(description = "vip等级")
|
||||
private Integer vipLevel;
|
||||
|
||||
@TableField(value = "uuid")
|
||||
@Schema(description = "uuid")
|
||||
private String uuid;
|
||||
}
|
||||
@@ -43,4 +43,11 @@ public interface ApplePurchaseService {
|
||||
* @param notification 解码后的通知载荷
|
||||
*/
|
||||
void handleConsumptionRequest(ResponseBodyV2DecodedPayload notification);
|
||||
|
||||
/**
|
||||
* 处理一次性购买通知(ONE_TIME_CHARGE)
|
||||
*
|
||||
* @param notification 解码后的通知载荷
|
||||
*/
|
||||
void handleOneTimeChargeNotification(ResponseBodyV2DecodedPayload notification);
|
||||
}
|
||||
|
||||
@@ -8,4 +8,13 @@ import com.baomidou.mybatisplus.extension.service.IService;
|
||||
*/
|
||||
|
||||
public interface KeyboardUserPurchaseRecordsService extends IService<KeyboardUserPurchaseRecords>{
|
||||
|
||||
/**
|
||||
* 检查购买记录是否存在
|
||||
*
|
||||
* @param transactionId 交易 ID
|
||||
* @param originalTransactionId 原始交易 ID
|
||||
* @return 存在返回 true,不存在返回 false
|
||||
*/
|
||||
boolean checkPurchaseExists(String transactionId, String originalTransactionId);
|
||||
}
|
||||
|
||||
@@ -38,4 +38,5 @@ public interface UserService extends IService<KeyboardUser> {
|
||||
*/
|
||||
Boolean cancelAccount(long userId);
|
||||
|
||||
Long selectUserByUUid(String uuid);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.yolo.keyborad.service.impl;
|
||||
|
||||
import cn.dev33.satoken.stp.StpUtil;
|
||||
import com.apple.itunes.storekit.model.JWSRenewalInfoDecodedPayload;
|
||||
import com.apple.itunes.storekit.model.JWSTransactionDecodedPayload;
|
||||
import com.apple.itunes.storekit.model.NotificationTypeV2;
|
||||
@@ -34,6 +35,7 @@ import java.time.format.DateTimeParseException;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* 苹果购买后置处理:订阅续期 / 内购充值 + 记录落库
|
||||
@@ -152,23 +154,10 @@ public class ApplePurchaseServiceImpl implements ApplePurchaseService {
|
||||
JWSTransactionDecodedPayload transaction =
|
||||
signedDataVerifier.verifyAndDecodeTransaction(signedTransactionInfo);
|
||||
|
||||
String originalTransactionId = transaction.getOriginalTransactionId();
|
||||
String productId = transaction.getProductId();
|
||||
|
||||
// 根据原始交易ID查询用户购买记录
|
||||
List<KeyboardUserPurchaseRecords> records = purchaseRecordsService.lambdaQuery()
|
||||
.eq(KeyboardUserPurchaseRecords::getOriginalTransactionId, originalTransactionId)
|
||||
.orderByDesc(KeyboardUserPurchaseRecords::getId)
|
||||
.last("LIMIT 1")
|
||||
.list();
|
||||
|
||||
if (records == null || records.isEmpty()) {
|
||||
log.warn("No purchase record found for originalTransactionId={}", originalTransactionId);
|
||||
return;
|
||||
}
|
||||
|
||||
KeyboardUserPurchaseRecords existingRecord = records.get(0);
|
||||
Long userId = existingRecord.getUserId().longValue();
|
||||
UUID appAccountToken = transaction.getAppAccountToken();
|
||||
Long userId = userService.selectUserByUUid(appAccountToken.toString());
|
||||
|
||||
// 查询商品信息
|
||||
KeyboardProductItems product = productItemsService.getProductEntityByProductId(productId);
|
||||
@@ -354,6 +343,127 @@ public class ApplePurchaseServiceImpl implements ApplePurchaseService {
|
||||
// 提供用户消费状态、交付状态等信息,帮助 Apple 评估退款请求
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理一次性购买通知(ONE_TIME_CHARGE)
|
||||
* 流程参考 processPurchase:
|
||||
* 1. 验证并解码交易信息
|
||||
* 2. 幂等性检查,避免重复处理
|
||||
* 3. 通过 originalTransactionId 查找用户
|
||||
* 4. 查询商品信息
|
||||
* 5. 保存购买记录
|
||||
* 6. 根据商品类型执行对应逻辑(订阅延期或钱包充值)
|
||||
*
|
||||
* @param notification 解码后的通知载荷
|
||||
*/
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void handleOneTimeChargeNotification(ResponseBodyV2DecodedPayload notification) {
|
||||
if (notification == null || notification.getData() == null) {
|
||||
log.warn("ONE_TIME_CHARGE notification data is null");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// 1. 解码交易信息
|
||||
String signedTransactionInfo = notification.getData().getSignedTransactionInfo();
|
||||
if (signedTransactionInfo == null || signedTransactionInfo.isBlank()) {
|
||||
log.warn("No signed transaction info in ONE_TIME_CHARGE notification");
|
||||
return;
|
||||
}
|
||||
|
||||
JWSTransactionDecodedPayload transaction =
|
||||
signedDataVerifier.verifyAndDecodeTransaction(signedTransactionInfo);
|
||||
|
||||
String transactionId = transaction.getTransactionId();
|
||||
String originalTransactionId = transaction.getOriginalTransactionId();
|
||||
String productId = transaction.getProductId();
|
||||
UUID appAccountToken = transaction.getAppAccountToken();
|
||||
Long userId = userService.selectUserByUUid(appAccountToken.toString());
|
||||
|
||||
log.info("Processing ONE_TIME_CHARGE: transactionId={}, productId={}", transactionId, productId);
|
||||
|
||||
// 2. 幂等性检查:根据交易ID判断是否已处理
|
||||
boolean handled = purchaseRecordsService.lambdaQuery()
|
||||
.eq(KeyboardUserPurchaseRecords::getTransactionId, transactionId)
|
||||
.eq(KeyboardUserPurchaseRecords::getStatus, "PAID")
|
||||
.exists();
|
||||
if (handled) {
|
||||
log.info("ONE_TIME_CHARGE already handled, transactionId={}", transactionId);
|
||||
return;
|
||||
}
|
||||
|
||||
// // 3. 通过 originalTransactionId 查找关联的用户购买记录以获取 userId
|
||||
// List<KeyboardUserPurchaseRecords> records = purchaseRecordsService.lambdaQuery()
|
||||
// .eq(KeyboardUserPurchaseRecords::getOriginalTransactionId, originalTransactionId)
|
||||
// .orderByDesc(KeyboardUserPurchaseRecords::getId)
|
||||
// .last("LIMIT 1")
|
||||
// .list();
|
||||
//
|
||||
// if (records == null || records.isEmpty()) {
|
||||
// log.warn("No purchase record found for ONE_TIME_CHARGE, originalTransactionId={}", originalTransactionId);
|
||||
// return;
|
||||
// }
|
||||
|
||||
// Long userId = records.get(0).getUserId().longValue();
|
||||
|
||||
// 4. 查询商品信息
|
||||
KeyboardProductItems product = productItemsService.getProductEntityByProductId(productId);
|
||||
if (product == null) {
|
||||
log.error("Product not found for ONE_TIME_CHARGE, productId={}", productId);
|
||||
return;
|
||||
}
|
||||
// 5. 构建并保存购买记录
|
||||
KeyboardUserPurchaseRecords purchaseRecord = new KeyboardUserPurchaseRecords();
|
||||
purchaseRecord.setUserId(userId.intValue());
|
||||
purchaseRecord.setProductId(productId);
|
||||
purchaseRecord.setPurchaseQuantity(product.getDurationValue());
|
||||
purchaseRecord.setPrice(product.getPrice());
|
||||
purchaseRecord.setCurrency(product.getCurrency());
|
||||
purchaseRecord.setPurchaseType(product.getType());
|
||||
purchaseRecord.setStatus("PAID");
|
||||
purchaseRecord.setPaymentMethod("APPLE");
|
||||
purchaseRecord.setTransactionId(transactionId);
|
||||
purchaseRecord.setOriginalTransactionId(originalTransactionId);
|
||||
purchaseRecord.setProductIds(new String[]{productId});
|
||||
|
||||
if (transaction.getPurchaseDate() != null) {
|
||||
purchaseRecord.setPurchaseTime(Date.from(Instant.ofEpochMilli(transaction.getPurchaseDate())));
|
||||
purchaseRecord.setPurchaseDate(Date.from(Instant.ofEpochMilli(transaction.getPurchaseDate())));
|
||||
} else {
|
||||
purchaseRecord.setPurchaseTime(new Date());
|
||||
}
|
||||
|
||||
if (transaction.getExpiresDate() != null) {
|
||||
purchaseRecord.setExpiresDate(Date.from(Instant.ofEpochMilli(transaction.getExpiresDate())));
|
||||
}
|
||||
|
||||
if (transaction.getEnvironment() != null) {
|
||||
purchaseRecord.setEnvironment(transaction.getEnvironment().name());
|
||||
}
|
||||
|
||||
purchaseRecordsService.save(purchaseRecord);
|
||||
|
||||
// 6. 根据商品类型执行对应的业务逻辑
|
||||
if ("subscription".equalsIgnoreCase(product.getType())) {
|
||||
Instant expiresInstant = transaction.getExpiresDate() != null
|
||||
? Instant.ofEpochMilli(transaction.getExpiresDate()) : null;
|
||||
extendVip(userId, product, expiresInstant);
|
||||
} else if ("in-app-purchase".equalsIgnoreCase(product.getType())) {
|
||||
handleInAppPurchase(userId, product, purchaseRecord.getId());
|
||||
} else {
|
||||
log.warn("未知商品类型, type={}, productId={}", product.getType(), productId);
|
||||
}
|
||||
|
||||
log.info("ONE_TIME_CHARGE processed successfully: userId={}, transactionId={}", userId, transactionId);
|
||||
|
||||
} catch (VerificationException e) {
|
||||
log.error("Failed to verify transaction in ONE_TIME_CHARGE notification", e);
|
||||
} catch (Exception e) {
|
||||
log.error("Error processing ONE_TIME_CHARGE notification", e);
|
||||
throw new BusinessException(ErrorCode.OPERATION_ERROR, "处理一次性购买通知失败");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理成功的续订
|
||||
* 创建新的购买记录并延长VIP有效期
|
||||
@@ -673,7 +783,7 @@ public class ApplePurchaseServiceImpl implements ApplePurchaseService {
|
||||
(short) 2, // 交易类型:2-苹果内购充值
|
||||
before,
|
||||
after,
|
||||
"Apple 充值: " + product.getProductId()
|
||||
product.getName()
|
||||
);
|
||||
|
||||
// 8. 记录充值成功日志
|
||||
|
||||
@@ -155,6 +155,7 @@ public class AppleReceiptServiceImpl implements AppleReceiptService {
|
||||
|
||||
// 续订偏好变更通知
|
||||
case DID_CHANGE_RENEWAL_PREF:
|
||||
|
||||
case DID_CHANGE_RENEWAL_STATUS:
|
||||
case PRICE_INCREASE:
|
||||
applePurchaseService.handleRenewalPreferenceChange(notification);
|
||||
@@ -165,9 +166,14 @@ public class AppleReceiptServiceImpl implements AppleReceiptService {
|
||||
applePurchaseService.handleConsumptionRequest(notification);
|
||||
break;
|
||||
|
||||
// 一次性购买通知
|
||||
case ONE_TIME_CHARGE:
|
||||
applePurchaseService.handleOneTimeChargeNotification(notification);
|
||||
break;
|
||||
|
||||
// 其他通知类型(记录但不处理)
|
||||
case EXTERNAL_PURCHASE_TOKEN:
|
||||
case ONE_TIME_CHARGE:
|
||||
|
||||
case REVOKE:
|
||||
case TEST:
|
||||
log.info("Received notification type {} - no action required", type);
|
||||
|
||||
@@ -538,7 +538,7 @@ public class ChatServiceImpl implements ChatService {
|
||||
|
||||
// 1. TTS 转换
|
||||
long ttsStart = System.currentTimeMillis();
|
||||
TextToSpeechVO ttsResult = elevenLabsService.textToSpeechWithTimestamps(text, voiceId);
|
||||
TextToSpeechVO ttsResult = elevenLabsService.textToSpeechWithTimestamps(text.replaceAll("\\(.*?\\)", ""), voiceId);
|
||||
long ttsDuration = System.currentTimeMillis() - ttsStart;
|
||||
log.info("TTS 完成, audioId: {}, 耗时: {}ms", audioId, ttsDuration);
|
||||
|
||||
|
||||
@@ -133,7 +133,7 @@ public class KeyboardThemePurchaseServiceImpl extends ServiceImpl<KeyboardThemeP
|
||||
(short) 1, // 交易类型:1-购买主题
|
||||
beforeBalance, // 交易前余额
|
||||
afterBalance, // 交易后余额
|
||||
"购买主题: " + theme.getThemeName() // 交易备注
|
||||
theme.getThemeName() // 交易备注
|
||||
);
|
||||
|
||||
// 8. 更新购买记录的交易ID
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
package com.yolo.keyborad.service.impl;
|
||||
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import java.util.List;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import com.yolo.keyborad.mapper.KeyboardUserPurchaseRecordsMapper;
|
||||
import com.yolo.keyborad.model.entity.KeyboardUserPurchaseRecords;
|
||||
@@ -11,8 +10,19 @@ import com.yolo.keyborad.service.KeyboardUserPurchaseRecordsService;
|
||||
* @author: ziin
|
||||
* @date: 2025/12/12 15:16
|
||||
*/
|
||||
|
||||
|
||||
@Service
|
||||
public class KeyboardUserPurchaseRecordsServiceImpl extends ServiceImpl<KeyboardUserPurchaseRecordsMapper, KeyboardUserPurchaseRecords> implements KeyboardUserPurchaseRecordsService{
|
||||
|
||||
@Override
|
||||
public boolean checkPurchaseExists(String transactionId, String originalTransactionId) {
|
||||
LambdaQueryWrapper<KeyboardUserPurchaseRecords> queryWrapper = new LambdaQueryWrapper<>();
|
||||
if (transactionId != null && !transactionId.isBlank()) {
|
||||
queryWrapper.eq(KeyboardUserPurchaseRecords::getTransactionId, transactionId);
|
||||
}
|
||||
if (originalTransactionId != null && !originalTransactionId.isBlank()) {
|
||||
queryWrapper.eq(KeyboardUserPurchaseRecords::getOriginalTransactionId, originalTransactionId);
|
||||
}
|
||||
return exists(queryWrapper);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package com.yolo.keyborad.service.impl;
|
||||
|
||||
import cn.dev33.satoken.stp.StpUtil;
|
||||
import cn.hutool.core.bean.BeanUtil;
|
||||
import cn.hutool.core.lang.UUID;
|
||||
import cn.hutool.core.util.IdUtil;
|
||||
import cn.hutool.core.util.RandomUtil;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
@@ -26,6 +27,8 @@ import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.Instant;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.Date;
|
||||
import com.yolo.keyborad.service.impl.user.UserInviteCodeBinder;
|
||||
import com.yolo.keyborad.service.impl.user.UserMailVerificationHandler;
|
||||
@@ -89,8 +92,26 @@ public class UserServiceImpl extends ServiceImpl<KeyboardUserMapper, KeyboardUse
|
||||
.eq(KeyboardUser::getStatus, false));
|
||||
}
|
||||
|
||||
private static final int ACCOUNT_REUSE_COOLDOWN_DAYS = 7;
|
||||
|
||||
private void ensureSubjectIdNotRecentlyCancelled(String sub) {
|
||||
Date cooldownStart = Date.from(Instant.now().minus(ACCOUNT_REUSE_COOLDOWN_DAYS, ChronoUnit.DAYS));
|
||||
KeyboardUser recentlyDeleted = keyboardUserMapper.selectOne(
|
||||
new LambdaQueryWrapper<KeyboardUser>()
|
||||
.eq(KeyboardUser::getSubjectId, sub)
|
||||
.eq(KeyboardUser::getDeleted, true)
|
||||
.gt(KeyboardUser::getDeletedAt, cooldownStart)
|
||||
.last("LIMIT 1")
|
||||
);
|
||||
if (recentlyDeleted != null) {
|
||||
throw new BusinessException(ErrorCode.ACCOUNT_RECENTLY_CANCELLED);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public KeyboardUser createUserWithSubjectId(String sub) {
|
||||
ensureSubjectIdNotRecentlyCancelled(sub);
|
||||
|
||||
KeyboardUser keyboardUser = buildNewUserWithSubjectId(sub);
|
||||
keyboardUserMapper.insert(keyboardUser);
|
||||
keyboardCharacterService.addDefaultUserCharacter(keyboardUser.getId());
|
||||
@@ -198,11 +219,20 @@ public class UserServiceImpl extends ServiceImpl<KeyboardUserMapper, KeyboardUse
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Long selectUserByUUid(String uuid) {
|
||||
KeyboardUser keyboardUserDB = keyboardUserMapper.selectOne(
|
||||
new LambdaQueryWrapper<KeyboardUser>()
|
||||
.eq(KeyboardUser::getUuid, uuid));
|
||||
return keyboardUserDB.getId();
|
||||
}
|
||||
|
||||
private KeyboardUser buildNewUserWithSubjectId(String sub) {
|
||||
KeyboardUser keyboardUser = new KeyboardUser();
|
||||
keyboardUser.setSubjectId(sub);
|
||||
keyboardUser.setUid(IdUtil.getSnowflake().nextId());
|
||||
keyboardUser.setNickName("User_" + RandomUtil.randomString(6));
|
||||
keyboardUser.setUuid(IdUtil.randomUUID());
|
||||
return keyboardUser;
|
||||
}
|
||||
|
||||
|
||||
@@ -18,6 +18,8 @@ import com.yolo.keyborad.service.KeyboardUserQuotaTotalService;
|
||||
import com.yolo.keyborad.service.KeyboardUserWalletService;
|
||||
import com.yolo.keyborad.utils.RedisUtil;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.Instant;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.Date;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
@@ -33,6 +35,7 @@ import org.springframework.transaction.annotation.Transactional;
|
||||
public class UserRegistrationHandler {
|
||||
|
||||
private static final String USER_CODE_PREFIX = "user:";
|
||||
private static final int ACCOUNT_REUSE_COOLDOWN_DAYS = 7;
|
||||
|
||||
private final KeyboardUserMapper keyboardUserMapper;
|
||||
private final PasswordEncoder passwordEncoder;
|
||||
@@ -91,6 +94,24 @@ public class UserRegistrationHandler {
|
||||
if (userMail != null) {
|
||||
throw new BusinessException(ErrorCode.USER_HAS_EXISTED);
|
||||
}
|
||||
|
||||
ensureNotRecentlyCancelled(
|
||||
new LambdaQueryWrapper<KeyboardUser>()
|
||||
.eq(KeyboardUser::getEmail, mailAddress)
|
||||
);
|
||||
}
|
||||
|
||||
private void ensureNotRecentlyCancelled(LambdaQueryWrapper<KeyboardUser> baseQuery) {
|
||||
Date cooldownStart = Date.from(Instant.now().minus(ACCOUNT_REUSE_COOLDOWN_DAYS, ChronoUnit.DAYS));
|
||||
KeyboardUser recentlyDeleted = keyboardUserMapper.selectOne(
|
||||
baseQuery
|
||||
.eq(KeyboardUser::getDeleted, true)
|
||||
.gt(KeyboardUser::getDeletedAt, cooldownStart)
|
||||
.last("LIMIT 1")
|
||||
);
|
||||
if (recentlyDeleted != null) {
|
||||
throw new BusinessException(ErrorCode.ACCOUNT_RECENTLY_CANCELLED);
|
||||
}
|
||||
}
|
||||
|
||||
private void validatePasswords(UserRegisterDTO userRegisterDTO) {
|
||||
@@ -118,6 +139,7 @@ public class UserRegistrationHandler {
|
||||
keyboardUser.setEmail(userRegisterDTO.getMailAddress());
|
||||
keyboardUser.setGender(userRegisterDTO.getGender());
|
||||
keyboardUser.setEmailVerified(true);
|
||||
keyboardUser.setUuid(IdUtil.randomUUID());
|
||||
return keyboardUser;
|
||||
}
|
||||
|
||||
|
||||
@@ -119,7 +119,7 @@ nacos:
|
||||
elevenlabs:
|
||||
api-key: sk_25339d32bb14c91f460ed9fce83a1951672f07846a7a10ce
|
||||
voice-id: JBFqnCBsd6RMkjVDRZzb
|
||||
model-id: eleven_turbo_v2_5
|
||||
model-id: eleven_flash_v2_5
|
||||
output-format: mp3_44100_128
|
||||
|
||||
deepgram:
|
||||
|
||||
@@ -91,7 +91,7 @@ sa-token:
|
||||
elevenlabs:
|
||||
api-key: sk_25339d32bb14c91f460ed9fce83a1951672f07846a7a10ce
|
||||
voice-id: JBFqnCBsd6RMkjVDRZzb
|
||||
model-id: eleven_turbo_v2_5
|
||||
model-id: eleven_flash_v2_5
|
||||
output-format: mp3_44100_128
|
||||
|
||||
deepgram:
|
||||
|
||||
Reference in New Issue
Block a user