+ * 负责验证苹果应用内购买的收据,包括一次性购买和订阅的验证逻辑 + * 使用 Apple App Store Server API 进行收据验证和交易信息获取 + *
+ */ +@Slf4j +@Service +public class AppleReceiptServiceImpl implements AppleReceiptService { + + /** + * App Store 服务器 API 客户端 + *+ * 用于调用 Apple App Store Server API 获取交易信息 + *
+ */ + private final AppStoreServerAPIClient client; + + /** + * 签名数据验证器 + *+ * 用于验证和解码 Apple 返回的 JWS 签名数据 + *
+ */ + private final SignedDataVerifier signedDataVerifier; + + /** + * 收据工具类 + *+ * 用于解析应用收据内容,提取交易 ID 等信息 + *
+ */ + private final ReceiptUtility receiptUtility; + + /** + * 构造函数 + *+ * 通过构造函数注入所需的依赖组件 + *
+ * @param client App Store 服务器 API 客户端 + * @param signedDataVerifier 签名数据验证器 + * @param receiptUtility 收据工具类 + */ + public AppleReceiptServiceImpl(AppStoreServerAPIClient client, + SignedDataVerifier signedDataVerifier, + ReceiptUtility receiptUtility) { + this.client = client; + this.signedDataVerifier = signedDataVerifier; + this.receiptUtility = receiptUtility; + } + + /** + * 验证 Apple 应用内购买收据 + *+ * 执行完整的收据验证流程,包括解析收据、获取交易信息、验证签名和业务逻辑校验 + *
+ * @param appReceipt Base64 编码的应用内购买收据 + * @return 验证结果对象,包含验证状态和交易信息 + */ + @Override + public AppleReceiptValidationResult validateReceipt(String appReceipt) { + // 检查收据是否为空 + if (appReceipt == null || appReceipt.isBlank()) { + return invalid("empty_receipt"); + } + + try { + // 1. 从收据里解析出 transactionId(不做验证,只是解析 ASN.1) + String transactionId = receiptUtility.extractTransactionIdFromAppReceipt(appReceipt); + if (transactionId == null) { + return invalid("no_transaction_id_in_receipt"); + } + + // 2. 调用 App Store Server API 获取单笔交易信息 + TransactionInfoResponse infoResponse = client.getTransactionInfo(transactionId); + + String signedTransactionInfo = infoResponse.getSignedTransactionInfo(); + if (signedTransactionInfo == null) { + return invalid("no_signed_transaction_info"); + } + + // 3. 使用 SignedDataVerifier 验证 JWS 并解码 payload + JWSTransactionDecodedPayload payload = + signedDataVerifier.verifyAndDecodeTransaction(signedTransactionInfo); + + // 4. 执行业务校验:检查交易是否仍然有效 + boolean stillActive = isTransactionActive(payload); + + // 构建并返回验证结果 + return getAppleReceiptValidationResult(stillActive, payload); + } catch (VerificationException e) { + // 验证异常处理 + log.warn("Apple receipt verification failed", e); + return invalid("verification_exception:" + e.getStatus()); + } catch (APIException e) { + // API 异常处理,可以根据错误码进一步处理(如环境切换) + // 4040010 表示订单不存在,可能是环境不匹配导致 + long code = e.getApiError().errorCode(); + log.warn("App Store API error, code={}", code, e); + return invalid("api_exception:" + code); + } catch (Exception e) { + // 其他未预期异常处理 + log.error("Unexpected error when validating Apple receipt", e); + return invalid("unexpected_error"); + } + } + + /** + * 构建苹果收据验证结果对象 + *+ * 根据交易有效性和交易负载构建详细的验证结果 + *
+ * @param stillActive 交易是否仍然有效 + * @param payload 解码后的交易负载 + * @return 包含详细信息的验证结果对象 + */ + private static AppleReceiptValidationResult getAppleReceiptValidationResult(boolean stillActive, JWSTransactionDecodedPayload payload) { + AppleReceiptValidationResult result = new AppleReceiptValidationResult(); + // 设置验证状态和原因 + result.setValid(stillActive); + result.setReason(stillActive ? "ok" : "expired_or_revoked"); + + // 设置交易相关信息 + result.setTransactionId(payload.getTransactionId()); + result.setOriginalTransactionId(payload.getOriginalTransactionId()); + result.setProductIds(List.of(payload.getProductId())); + + // 设置时间信息 + if (payload.getPurchaseDate() != null) { + result.setPurchaseDate(Instant.ofEpochMilli(payload.getPurchaseDate())); + } + if (payload.getExpiresDate() != null) { + result.setExpiresDate(Instant.ofEpochMilli(payload.getExpiresDate())); + } + + // 设置环境信息 + Environment env = payload.getEnvironment(); + result.setEnvironment(env); + return result; + } + + /** + * 创建无效的验证结果 + *+ * 快速创建一个表示验证失败的结果对象 + *
+ * @param reason 失败原因 + * @return 标记为无效的验证结果对象 + */ + private AppleReceiptValidationResult invalid(String reason) { + AppleReceiptValidationResult r = new AppleReceiptValidationResult(); + r.setValid(false); + r.setReason(reason); + return r; + } + + /** + * 检查交易是否仍然有效 + *+ * 根据交易负载中的信息判断交易是否处于有效状态 + * 1. 已撤销/退款的交易无效 + * 2. 有过期时间的订阅类交易检查是否已过期 + * 3. 一次性购买交易只要未撤销则认为有效 + *
+ * @param payload 解码后的交易负载 + * @return 交易是否有效 + */ + private boolean isTransactionActive(JWSTransactionDecodedPayload payload) { + // 已撤销/退款的可以直接认为无效 + if (payload.getRevocationDate() != null) { + return false; + } + + // 有 expiresDate 的一般是订阅(自动续订等),过期就无效 + if (payload.getExpiresDate() != null) { + long now = System.currentTimeMillis(); + return now < payload.getExpiresDate(); + } + + // 没有 expiresDate 的一次性内购,只要能通过验签 + 没撤销,就认为是有效购买 + return true; + } +} \ No newline at end of file diff --git a/src/main/java/com/yolo/keyborad/service/impl/QdrantVectorService.java b/src/main/java/com/yolo/keyborad/service/impl/QdrantVectorService.java index 9c353a2..1a575eb 100644 --- a/src/main/java/com/yolo/keyborad/service/impl/QdrantVectorService.java +++ b/src/main/java/com/yolo/keyborad/service/impl/QdrantVectorService.java @@ -1,15 +1,18 @@ package com.yolo.keyborad.service.impl; -import com.yolo.keyborad.utils.ProtoUtils; +import com.yolo.keyborad.common.ErrorCode; +import com.yolo.keyborad.exception.BusinessException; import io.qdrant.client.QdrantClient; import io.qdrant.client.grpc.Collections; import io.qdrant.client.grpc.JsonWithInt; import io.qdrant.client.grpc.Points; import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import java.util.List; import java.util.Map; +import java.util.concurrent.ExecutionException; import static io.qdrant.client.PointIdFactory.id; import static io.qdrant.client.ValueFactory.value; @@ -17,6 +20,7 @@ import static io.qdrant.client.VectorsFactory.vectors; @Service +@Slf4j public class QdrantVectorService { @Resource @@ -31,30 +35,37 @@ public class QdrantVectorService { * @param vector 向量(和 collection 中定义的 size 一致) * @param payload 额外信息,例如原文、标题、userId 等 */ - public void upsertPoint(long id, List