refactor(core): 重构Google Play订阅与商品接口逻辑

This commit is contained in:
2026-04-08 17:33:36 +08:00
parent da3ee94924
commit b83957e0bc
9 changed files with 36 additions and 31 deletions

View File

@@ -89,7 +89,8 @@ public class SaTokenConfigure implements WebMvcConfigurer {
"/google-play/rtdn", "/google-play/rtdn",
"/appVersions/checkUpdate", "/appVersions/checkUpdate",
"/appVersions/checkUpdate", "/appVersions/checkUpdate",
"/character/detailWithNotLogin" "/character/detailWithNotLogin",
"/apple/validate-receipt"
}; };
} }
@Bean @Bean

View File

@@ -11,6 +11,7 @@ import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource; import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
@@ -34,41 +35,42 @@ public class ProductsController {
@Operation(summary = "查询商品明细", description = "根据商品ID或productId查询商品详情通过platform区分平台") @Operation(summary = "查询商品明细", description = "根据商品ID或productId查询商品详情通过platform区分平台")
public BaseResponse<KeyboardProductItemRespVO> getProductDetail( public BaseResponse<KeyboardProductItemRespVO> getProductDetail(
@RequestParam(value = "id", required = false) Long id, @RequestParam(value = "id", required = false) Long id,
@RequestParam(value = "productId", required = false) String productId, @RequestParam(value = "productId", required = false) String productId) {
@RequestParam(value = "platform", required = false) String platform
) {
if (id == null && (productId == null || productId.isBlank())) { if (id == null && (productId == null || productId.isBlank())) {
throw new BusinessException(ErrorCode.PARAMS_ERROR, "id 或 productId 至少传一个"); throw new BusinessException(ErrorCode.PARAMS_ERROR, "id 或 productId 至少传一个");
} }
// 判断平台如果是android返回安卓商品否则默认返回苹果商品 // 判断平台如果是android返回安卓商品否则默认返回苹果商品
String resolvedPlatform = "android".equalsIgnoreCase(platform) ? "android" : "apple";
KeyboardProductItemRespVO result = (id != null) KeyboardProductItemRespVO result = (id != null)
? productItemsService.getProductDetailById(id, resolvedPlatform) ? productItemsService.getProductDetailById(id)
: productItemsService.getProductDetailByProductId(productId, resolvedPlatform); : productItemsService.getProductDetailByProductId(productId);
return ResultUtils.success(result); return ResultUtils.success(result);
} }
@GetMapping("/listByType") @GetMapping("/listByType")
@Operation(summary = "按类型查询商品列表", description = "根据商品类型查询商品列表type=all 返回全部") @Operation(summary = "按类型查询商品列表", description = "根据商品类型查询商品列表type=all 返回全部")
public BaseResponse<List<KeyboardProductItemRespVO>> listByType(@RequestParam("type") String type) { public BaseResponse<List<KeyboardProductItemRespVO>> listByType(
@RequestParam("type") String type,
@RequestHeader(value = "platform", required = false) String platform) {
if (type == null || type.isBlank()) { if (type == null || type.isBlank()) {
throw new BusinessException(ErrorCode.PARAMS_ERROR, "type 不能为空"); throw new BusinessException(ErrorCode.PARAMS_ERROR, "type 不能为空");
} }
List<KeyboardProductItemRespVO> result = productItemsService.listProductsByType(type); List<KeyboardProductItemRespVO> result = productItemsService.listProductsByType(type, platform);
return ResultUtils.success(result); return ResultUtils.success(result);
} }
@GetMapping("/inApp/list") @GetMapping("/inApp/list")
@Operation(summary = "查询内购商品列表", description = "查询 type=in-app-purchase 的商品列表") @Operation(summary = "查询内购商品列表", description = "查询 type=in-app-purchase 的商品列表")
public BaseResponse<List<KeyboardProductItemRespVO>> listInAppPurchases() { public BaseResponse<List<KeyboardProductItemRespVO>> listInAppPurchases(
List<KeyboardProductItemRespVO> result = productItemsService.listProductsByType("in-app-purchase"); @RequestHeader(value = "platform", required = false) String platform) {
List<KeyboardProductItemRespVO> result = productItemsService.listProductsByType("in-app-purchase", platform);
return ResultUtils.success(result); return ResultUtils.success(result);
} }
@GetMapping("/subscription/list") @GetMapping("/subscription/list")
@Operation(summary = "查询订阅商品列表", description = "查询 type=subscription 的商品列表") @Operation(summary = "查询订阅商品列表", description = "查询 type=subscription 的商品列表")
public BaseResponse<List<KeyboardProductItemRespVO>> listSubscriptions() { public BaseResponse<List<KeyboardProductItemRespVO>> listSubscriptions(
List<KeyboardProductItemRespVO> result = productItemsService.listProductsByType("subscription"); @RequestHeader(value = "platform", required = false) String platform) {
List<KeyboardProductItemRespVO> result = productItemsService.listProductsByType("subscription", platform);
return ResultUtils.success(result); return ResultUtils.success(result);
} }
} }

View File

@@ -115,6 +115,7 @@ public class GooglePlayApiClient {
.packageName(packageName) .packageName(packageName)
.productId(text(lineItem, "productId")) .productId(text(lineItem, "productId"))
.productType(GooglePlayConstants.PRODUCT_TYPE_SUBSCRIPTION) .productType(GooglePlayConstants.PRODUCT_TYPE_SUBSCRIPTION)
.basePlanId(text(lineItem.path("offerDetails"), "basePlanId"))
.purchaseToken(purchaseToken) .purchaseToken(purchaseToken)
.orderKey(resolveOrderKey(googleOrderId, purchaseToken)) .orderKey(resolveOrderKey(googleOrderId, purchaseToken))
.googleOrderId(googleOrderId) .googleOrderId(googleOrderId)

View File

@@ -17,7 +17,7 @@ public class GooglePlayPubSubAuthService {
private final GooglePlayApiClient apiClient; private final GooglePlayApiClient apiClient;
public void verify(HttpServletRequest request, GooglePlayPubSubPushRequest pushRequest) { public void verify(HttpServletRequest request, GooglePlayPubSubPushRequest pushRequest) {
verifyTopic(request); // verifyTopic(request);
verifySubscription(pushRequest); verifySubscription(pushRequest);
if (!properties.isValidatePubsubJwt()) { if (!properties.isValidatePubsubJwt()) {
return; return;
@@ -34,7 +34,7 @@ public class GooglePlayPubSubAuthService {
if (expectedTopic == null || expectedTopic.isBlank()) { if (expectedTopic == null || expectedTopic.isBlank()) {
return; return;
} }
String currentTopic = request.getHeader("X-Goog-Topic"); String currentTopic = request.getHeader("projects/keyboard-490601/topics/keyboard_topic");
if (!expectedTopic.equals(currentTopic)) { if (!expectedTopic.equals(currentTopic)) {
throw new BusinessException(ErrorCode.GOOGLE_PLAY_WEBHOOK_UNAUTHORIZED, "Pub/Sub topic 不匹配"); throw new BusinessException(ErrorCode.GOOGLE_PLAY_WEBHOOK_UNAUTHORIZED, "Pub/Sub topic 不匹配");
} }

View File

@@ -32,7 +32,7 @@ public class GooglePlayStateService {
@Transactional(rollbackFor = Exception.class) @Transactional(rollbackFor = Exception.class)
public GooglePlaySyncResult sync(GooglePlaySyncCommand command, GooglePlayPurchaseSnapshot snapshot) { public GooglePlaySyncResult sync(GooglePlaySyncCommand command, GooglePlayPurchaseSnapshot snapshot) {
KeyboardProductItems product = loadProduct(snapshot.getProductId()); KeyboardProductItems product = loadProduct(snapshot.getBasePlanId());
GooglePlayOrder order = buildOrder(command, snapshot); GooglePlayOrder order = buildOrder(command, snapshot);
GooglePlayPurchaseToken token = buildToken(command, snapshot); GooglePlayPurchaseToken token = buildToken(command, snapshot);
// 先保存订单以确保 order.id 已生成,钱包充值等权益分发依赖 order.id 写入交易流水 // 先保存订单以确保 order.id 已生成,钱包充值等权益分发依赖 order.id 写入交易流水

View File

@@ -15,6 +15,8 @@ public class GooglePlayPurchaseSnapshot {
private String productType; private String productType;
private String basePlanId;
private String purchaseToken; private String purchaseToken;
private String orderKey; private String orderKey;

View File

@@ -15,19 +15,17 @@ public interface KeyboardProductItemsService extends IService<KeyboardProductIte
* 根据主键ID和平台查询商品明细 * 根据主键ID和平台查询商品明细
* *
* @param id 商品主键ID * @param id 商品主键ID
* @param platform 平台标识android / apple
* @return 商品明细(不存在返回 null * @return 商品明细(不存在返回 null
*/ */
KeyboardProductItemRespVO getProductDetailById(Long id, String platform); KeyboardProductItemRespVO getProductDetailById(Long id);
/** /**
* 根据 productId 和平台查询商品明细 * 根据 productId 和平台查询商品明细
* *
* @param productId 商品 productId * @param productId 商品 productId
* @param platform 平台标识android / apple
* @return 商品明细(不存在返回 null * @return 商品明细(不存在返回 null
*/ */
KeyboardProductItemRespVO getProductDetailByProductId(String productId, String platform); KeyboardProductItemRespVO getProductDetailByProductId(String productId);
/** /**
* 根据 productId 获取商品实体 * 根据 productId 获取商品实体
@@ -41,6 +39,6 @@ public interface KeyboardProductItemsService extends IService<KeyboardProductIte
* @param type 商品类型subscription / in-app-purchase / all * @param type 商品类型subscription / in-app-purchase / all
* @return 商品列表 * @return 商品列表
*/ */
List<KeyboardProductItemRespVO> listProductsByType(String type); List<KeyboardProductItemRespVO> listProductsByType(String type, String platform);
} }

View File

@@ -46,7 +46,7 @@ public class GooglePlayBillingServiceImpl implements GooglePlayBillingService {
String packageName = resolvePackageName(req.getPackageName()); String packageName = resolvePackageName(req.getPackageName());
String productType = normalizeProductType(req.getProductType()); String productType = normalizeProductType(req.getProductType());
GooglePlayPurchaseSnapshot snapshot = fetchSnapshot(packageName, productType, req.getPurchaseToken()); GooglePlayPurchaseSnapshot snapshot = fetchSnapshot(packageName, productType, req.getPurchaseToken());
validateProduct(snapshot, req.getProductId()); // validateProduct(snapshot, req.getProductId());
verifyExternalAccount(userId, snapshot); verifyExternalAccount(userId, snapshot);
GooglePlaySyncCommand command = GooglePlaySyncCommand.builder() GooglePlaySyncCommand command = GooglePlaySyncCommand.builder()
.userId(userId) .userId(userId)
@@ -163,7 +163,7 @@ public class GooglePlayBillingServiceImpl implements GooglePlayBillingService {
if (requestProductId == null || requestProductId.isBlank()) { if (requestProductId == null || requestProductId.isBlank()) {
return; return;
} }
if (!requestProductId.equals(snapshot.getProductId())) { if (!requestProductId.equals(snapshot.getBasePlanId())) {
throw new BusinessException(ErrorCode.GOOGLE_PLAY_PURCHASE_MISMATCH, "productId 与 Google 返回不一致"); throw new BusinessException(ErrorCode.GOOGLE_PLAY_PURCHASE_MISMATCH, "productId 与 Google 返回不一致");
} }
} }

View File

@@ -20,18 +20,16 @@ public class KeyboardProductItemsServiceImpl extends ServiceImpl<KeyboardProduct
* 根据ID和平台获取产品详情 * 根据ID和平台获取产品详情
* *
* @param id 产品ID * @param id 产品ID
* @param platform 平台标识android / apple
* @return 产品详情响应对象如果未找到产品则返回null * @return 产品详情响应对象如果未找到产品则返回null
*/ */
@Override @Override
public KeyboardProductItemRespVO getProductDetailById(Long id, String platform) { public KeyboardProductItemRespVO getProductDetailById(Long id) {
if (id == null) { if (id == null) {
return null; return null;
} }
KeyboardProductItems item = this.lambdaQuery() KeyboardProductItems item = this.lambdaQuery()
.eq(KeyboardProductItems::getId, id) .eq(KeyboardProductItems::getId, id)
.eq(KeyboardProductItems::getPlatform, platform)
.one(); .one();
return item == null ? null : BeanUtil.copyProperties(item, KeyboardProductItemRespVO.class); return item == null ? null : BeanUtil.copyProperties(item, KeyboardProductItemRespVO.class);
@@ -42,18 +40,16 @@ public class KeyboardProductItemsServiceImpl extends ServiceImpl<KeyboardProduct
* 根据产品ID和平台获取产品详情 * 根据产品ID和平台获取产品详情
* *
* @param productId 产品ID * @param productId 产品ID
* @param platform 平台标识android / apple
* @return 产品详情响应对象如果未找到产品则返回null * @return 产品详情响应对象如果未找到产品则返回null
*/ */
@Override @Override
public KeyboardProductItemRespVO getProductDetailByProductId(String productId, String platform) { public KeyboardProductItemRespVO getProductDetailByProductId(String productId) {
if (productId == null || productId.isBlank()) { if (productId == null || productId.isBlank()) {
return null; return null;
} }
KeyboardProductItems item = this.lambdaQuery() KeyboardProductItems item = this.lambdaQuery()
.eq(KeyboardProductItems::getProductId, productId) .eq(KeyboardProductItems::getProductId, productId)
.eq(KeyboardProductItems::getPlatform, platform)
.one(); .one();
return item == null ? null : BeanUtil.copyProperties(item, KeyboardProductItemRespVO.class); return item == null ? null : BeanUtil.copyProperties(item, KeyboardProductItemRespVO.class);
@@ -77,7 +73,7 @@ public class KeyboardProductItemsServiceImpl extends ServiceImpl<KeyboardProduct
* @return 产品详情响应列表按ID升序排列 * @return 产品详情响应列表按ID升序排列
*/ */
@Override @Override
public java.util.List<KeyboardProductItemRespVO> listProductsByType(String type) { public java.util.List<KeyboardProductItemRespVO> listProductsByType(String type, String platform) {
// 创建Lambda查询构造器 // 创建Lambda查询构造器
var query = this.lambdaQuery(); var query = this.lambdaQuery();
@@ -86,6 +82,11 @@ public class KeyboardProductItemsServiceImpl extends ServiceImpl<KeyboardProduct
query.eq(KeyboardProductItems::getType, type); query.eq(KeyboardProductItems::getType, type);
} }
// 根据平台过滤商品
if (platform != null && !platform.isBlank()) {
query.eq(KeyboardProductItems::getPlatform, platform);
}
// 执行查询按ID升序排列 // 执行查询按ID升序排列
java.util.List<KeyboardProductItems> items = query java.util.List<KeyboardProductItems> items = query
.orderByAsc(KeyboardProductItems::getId) .orderByAsc(KeyboardProductItems::getId)