feat(core): 新增苹果 App Store 订阅票据校验与向量存储结构升级
- 引入 Apple App Store Server Library,完成票据验证、续订、退款通知全套流程 - 新增 AppleReceiptController / AppleReceiptService 及相关配置类,支持沙箱与生产环境双端点 - 向量存储接口升级:EmbedSaveReq 封装向量与业务实体,QdrantVectorService 改为 JSON 字符串载荷并补全异常处理 - 补充 Apple 根证书与订阅密钥资源文件,pom 与 yml 增加对应依赖与配置
This commit is contained in:
6
pom.xml
6
pom.xml
@@ -97,6 +97,12 @@
|
|||||||
<artifactId>grpc-netty-shaded</artifactId>
|
<artifactId>grpc-netty-shaded</artifactId>
|
||||||
<version>1.65.1</version>
|
<version>1.65.1</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<!-- 苹果 IAP sdk -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.apple.itunes.storekit</groupId>
|
||||||
|
<artifactId>app-store-server-library</artifactId>
|
||||||
|
<version>3.6.0</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
<!-- <dependency>-->
|
<!-- <dependency>-->
|
||||||
<!-- <groupId>com.alibaba.cloud.ai</groupId>-->
|
<!-- <groupId>com.alibaba.cloud.ai</groupId>-->
|
||||||
|
|||||||
@@ -1,14 +1,17 @@
|
|||||||
package com.yolo.keyborad;
|
package com.yolo.keyborad;
|
||||||
|
|
||||||
|
import com.yolo.keyborad.config.AppleAppStoreProperties;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.mybatis.spring.annotation.MapperScan;
|
import org.mybatis.spring.annotation.MapperScan;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.boot.SpringApplication;
|
import org.springframework.boot.SpringApplication;
|
||||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||||
|
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||||
|
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@SpringBootApplication
|
@SpringBootApplication
|
||||||
@MapperScan("com.yolo.keyborad.mapper")
|
@MapperScan("com.yolo.keyborad.mapper")
|
||||||
|
@EnableConfigurationProperties(AppleAppStoreProperties.class)
|
||||||
public class MyApplication {
|
public class MyApplication {
|
||||||
public static void main(String[] args) {
|
public static void main(String[] args) {
|
||||||
SpringApplication.run(MyApplication.class, args);
|
SpringApplication.run(MyApplication.class, args);
|
||||||
|
|||||||
107
src/main/java/com/yolo/keyborad/config/AppleAppStoreConfig.java
Normal file
107
src/main/java/com/yolo/keyborad/config/AppleAppStoreConfig.java
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
package com.yolo.keyborad.config;
|
||||||
|
|
||||||
|
import com.apple.itunes.storekit.client.AppStoreServerAPIClient;
|
||||||
|
import com.apple.itunes.storekit.migration.ReceiptUtility;
|
||||||
|
import com.apple.itunes.storekit.model.Environment;
|
||||||
|
import com.apple.itunes.storekit.verification.SignedDataVerifier;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.core.io.Resource;
|
||||||
|
import org.springframework.core.io.ResourceLoader;
|
||||||
|
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Apple App Store 配置类
|
||||||
|
* 用于配置与Apple App Store服务器API交互所需的各种组件
|
||||||
|
*/
|
||||||
|
@Configuration
|
||||||
|
public class AppleAppStoreConfig {
|
||||||
|
|
||||||
|
// Apple App Store 属性配置
|
||||||
|
private final AppleAppStoreProperties properties;
|
||||||
|
// 资源加载器,用于加载证书和密钥文件
|
||||||
|
private final ResourceLoader resourceLoader;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构造函数
|
||||||
|
* @param properties Apple App Store属性配置对象
|
||||||
|
* @param resourceLoader Spring资源加载器
|
||||||
|
*/
|
||||||
|
public AppleAppStoreConfig(AppleAppStoreProperties properties,
|
||||||
|
ResourceLoader resourceLoader) {
|
||||||
|
this.properties = properties;
|
||||||
|
this.resourceLoader = resourceLoader;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建App Store服务器API客户端
|
||||||
|
* 用于与Apple App Store服务器进行API交互
|
||||||
|
*
|
||||||
|
* @return AppStoreServerAPIClient实例
|
||||||
|
* @throws Exception 当加载密钥文件或创建客户端失败时抛出
|
||||||
|
*/
|
||||||
|
@Bean
|
||||||
|
public AppStoreServerAPIClient appStoreServerAPIClient() throws Exception {
|
||||||
|
// 加载私钥文件
|
||||||
|
Resource keyResource = resourceLoader.getResource(properties.getPrivateKeyPath());
|
||||||
|
Path keyPath = keyResource.getFile().toPath();
|
||||||
|
String encodedKey = Files.readString(keyPath);
|
||||||
|
|
||||||
|
// 获取环境配置(沙盒或生产)
|
||||||
|
Environment env = Environment.valueOf(properties.getEnvironment());
|
||||||
|
|
||||||
|
// 创建并返回API客户端
|
||||||
|
return new AppStoreServerAPIClient(
|
||||||
|
encodedKey, // 私钥内容
|
||||||
|
properties.getKeyId(), // 密钥ID
|
||||||
|
properties.getIssuerId(), // 颁发者ID
|
||||||
|
properties.getBundleId(), // 应用包ID
|
||||||
|
env // 环境
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建签名数据验证器
|
||||||
|
* 用于验证来自App Store的签名收据和通知
|
||||||
|
*
|
||||||
|
* @return SignedDataVerifier实例
|
||||||
|
* @throws Exception 当加载根证书或创建验证器失败时抛出
|
||||||
|
*/
|
||||||
|
@Bean
|
||||||
|
public SignedDataVerifier signedDataVerifier() throws Exception {
|
||||||
|
// 加载所有根证书
|
||||||
|
Set<InputStream> rootCAs = new HashSet<>();
|
||||||
|
for (String path : properties.getRootCertificates()) {
|
||||||
|
Resource resource = resourceLoader.getResource(path);
|
||||||
|
rootCAs.add(resource.getInputStream());
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取环境配置(沙盒或生产)
|
||||||
|
Environment env = Environment.valueOf(properties.getEnvironment());
|
||||||
|
|
||||||
|
// 创建并返回签名数据验证器
|
||||||
|
return new SignedDataVerifier(
|
||||||
|
rootCAs, // 根证书集合
|
||||||
|
properties.getBundleId(), // 应用包ID
|
||||||
|
properties.getAppAppleId(), // 应用Apple ID(生产环境必须填写)
|
||||||
|
env, // 环境
|
||||||
|
true // 启用在线检查
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建收据工具类实例
|
||||||
|
* 用于处理和解析App Store收据数据
|
||||||
|
*
|
||||||
|
* @return ReceiptUtility实例
|
||||||
|
*/
|
||||||
|
@Bean
|
||||||
|
public ReceiptUtility receiptUtility() {
|
||||||
|
return new ReceiptUtility();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package com.yolo.keyborad.config;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@ConfigurationProperties(prefix = "apple.appstore")
|
||||||
|
@Data
|
||||||
|
public class AppleAppStoreProperties {
|
||||||
|
|
||||||
|
private String issuerId;
|
||||||
|
private String keyId;
|
||||||
|
private String bundleId;
|
||||||
|
private Long appAppleId;
|
||||||
|
private String privateKeyPath;
|
||||||
|
private String environment; // SANDBOX / PRODUCTION
|
||||||
|
private List<String> rootCertificates;
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
package com.yolo.keyborad.controller;
|
||||||
|
|
||||||
|
import com.yolo.keyborad.model.dto.AppleReceiptValidationResult;
|
||||||
|
import com.yolo.keyborad.service.AppleReceiptService;
|
||||||
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestBody;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/apple")
|
||||||
|
public class AppleReceiptController {
|
||||||
|
|
||||||
|
private final AppleReceiptService appleReceiptService;
|
||||||
|
|
||||||
|
public AppleReceiptController(AppleReceiptService appleReceiptService) {
|
||||||
|
this.appleReceiptService = appleReceiptService;
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/validate-receipt")
|
||||||
|
public AppleReceiptValidationResult validateReceipt(@RequestBody Map<String, String> body) {
|
||||||
|
String receipt = body.get("receipt");
|
||||||
|
return appleReceiptService.validateReceipt(receipt);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,8 +1,11 @@
|
|||||||
package com.yolo.keyborad.controller;
|
package com.yolo.keyborad.controller;
|
||||||
|
|
||||||
|
import cn.hutool.json.JSON;
|
||||||
|
import cn.hutool.json.JSONUtil;
|
||||||
import com.yolo.keyborad.common.BaseResponse;
|
import com.yolo.keyborad.common.BaseResponse;
|
||||||
import com.yolo.keyborad.common.ResultUtils;
|
import com.yolo.keyborad.common.ResultUtils;
|
||||||
|
|
||||||
|
import com.yolo.keyborad.model.dto.EmbedSaveReq;
|
||||||
import com.yolo.keyborad.model.dto.IosPayVerifyReq;
|
import com.yolo.keyborad.model.dto.IosPayVerifyReq;
|
||||||
import com.yolo.keyborad.service.impl.QdrantVectorService;
|
import com.yolo.keyborad.service.impl.QdrantVectorService;
|
||||||
import io.qdrant.client.QdrantClient;
|
import io.qdrant.client.QdrantClient;
|
||||||
@@ -82,8 +85,10 @@ public class DemoController {
|
|||||||
@PostMapping("/testSaveEmbed")
|
@PostMapping("/testSaveEmbed")
|
||||||
@Operation(summary = "测试存储向量接口", description = "测试存储向量接口")
|
@Operation(summary = "测试存储向量接口", description = "测试存储向量接口")
|
||||||
@Parameter(name = "userInput",required = true,description = "测试存储向量接口")
|
@Parameter(name = "userInput",required = true,description = "测试存储向量接口")
|
||||||
public BaseResponse<Boolean> testSaveEmbed( @RequestBody List<Float> userInput) throws Exception {
|
public BaseResponse<Boolean> testSaveEmbed(@RequestBody EmbedSaveReq embedSaveReq) {
|
||||||
qdrantVectorService.upsertPoint(1L, userInput, null);
|
qdrantVectorService.upsertPoint(embedSaveReq.getRecordItem().getId()
|
||||||
|
, embedSaveReq.getVector()
|
||||||
|
, JSONUtil.toJsonStr(embedSaveReq.getRecordItem()));
|
||||||
return ResultUtils.success(true);
|
return ResultUtils.success(true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
package com.yolo.keyborad.model.dto;
|
||||||
|
|
||||||
|
import com.apple.itunes.storekit.model.Environment;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@Builder
|
||||||
|
@AllArgsConstructor
|
||||||
|
@NoArgsConstructor
|
||||||
|
public class AppleReceiptValidationResult {
|
||||||
|
|
||||||
|
private boolean valid;
|
||||||
|
private String reason;
|
||||||
|
|
||||||
|
private String transactionId;
|
||||||
|
private String originalTransactionId;
|
||||||
|
private List<String> productIds;
|
||||||
|
|
||||||
|
private Instant purchaseDate;
|
||||||
|
private Instant expiresDate;
|
||||||
|
|
||||||
|
private Environment environment;
|
||||||
|
|
||||||
|
}
|
||||||
20
src/main/java/com/yolo/keyborad/model/dto/EmbedSaveReq.java
Normal file
20
src/main/java/com/yolo/keyborad/model/dto/EmbedSaveReq.java
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
package com.yolo.keyborad.model.dto;
|
||||||
|
|
||||||
|
import com.yolo.keyborad.model.entity.RecordItem;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* @author: ziin
|
||||||
|
* @date: 2025/11/14 13:21
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class EmbedSaveReq {
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
private List<Float> vector;
|
||||||
|
|
||||||
|
private RecordItem recordItem;
|
||||||
|
}
|
||||||
19
src/main/java/com/yolo/keyborad/model/entity/RecordItem.java
Normal file
19
src/main/java/com/yolo/keyborad/model/entity/RecordItem.java
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
package com.yolo.keyborad.model.entity;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* @author: ziin
|
||||||
|
* @date: 2025/11/14 13:22
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class RecordItem {
|
||||||
|
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
private String document;
|
||||||
|
|
||||||
|
private String content;
|
||||||
|
|
||||||
|
private Integer frequency;
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
package com.yolo.keyborad.service;
|
||||||
|
|
||||||
|
import com.yolo.keyborad.model.dto.AppleReceiptValidationResult;
|
||||||
|
|
||||||
|
public interface AppleReceiptService {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证 base64 app receipt 是否有效,并返回解析结果。
|
||||||
|
*
|
||||||
|
* @param appReceipt Base64 的 app receipt(以 MI... 开头那串)
|
||||||
|
*/
|
||||||
|
AppleReceiptValidationResult validateReceipt(String appReceipt);
|
||||||
|
}
|
||||||
@@ -0,0 +1,202 @@
|
|||||||
|
package com.yolo.keyborad.service.impl;
|
||||||
|
|
||||||
|
import com.apple.itunes.storekit.client.APIException;
|
||||||
|
import com.apple.itunes.storekit.client.AppStoreServerAPIClient;
|
||||||
|
import com.apple.itunes.storekit.migration.ReceiptUtility;
|
||||||
|
import com.apple.itunes.storekit.model.Environment;
|
||||||
|
import com.apple.itunes.storekit.model.JWSTransactionDecodedPayload;
|
||||||
|
import com.apple.itunes.storekit.model.TransactionInfoResponse;
|
||||||
|
import com.apple.itunes.storekit.verification.SignedDataVerifier;
|
||||||
|
import com.apple.itunes.storekit.verification.VerificationException;
|
||||||
|
import com.yolo.keyborad.model.dto.AppleReceiptValidationResult;
|
||||||
|
import com.yolo.keyborad.service.AppleReceiptService;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Apple 应用内购买收据验证服务实现类
|
||||||
|
* <p>
|
||||||
|
* 负责验证苹果应用内购买的收据,包括一次性购买和订阅的验证逻辑
|
||||||
|
* 使用 Apple App Store Server API 进行收据验证和交易信息获取
|
||||||
|
* </p>
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Service
|
||||||
|
public class AppleReceiptServiceImpl implements AppleReceiptService {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* App Store 服务器 API 客户端
|
||||||
|
* <p>
|
||||||
|
* 用于调用 Apple App Store Server API 获取交易信息
|
||||||
|
* </p>
|
||||||
|
*/
|
||||||
|
private final AppStoreServerAPIClient client;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 签名数据验证器
|
||||||
|
* <p>
|
||||||
|
* 用于验证和解码 Apple 返回的 JWS 签名数据
|
||||||
|
* </p>
|
||||||
|
*/
|
||||||
|
private final SignedDataVerifier signedDataVerifier;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 收据工具类
|
||||||
|
* <p>
|
||||||
|
* 用于解析应用收据内容,提取交易 ID 等信息
|
||||||
|
* </p>
|
||||||
|
*/
|
||||||
|
private final ReceiptUtility receiptUtility;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构造函数
|
||||||
|
* <p>
|
||||||
|
* 通过构造函数注入所需的依赖组件
|
||||||
|
* </p>
|
||||||
|
* @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 应用内购买收据
|
||||||
|
* <p>
|
||||||
|
* 执行完整的收据验证流程,包括解析收据、获取交易信息、验证签名和业务逻辑校验
|
||||||
|
* </p>
|
||||||
|
* @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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构建苹果收据验证结果对象
|
||||||
|
* <p>
|
||||||
|
* 根据交易有效性和交易负载构建详细的验证结果
|
||||||
|
* </p>
|
||||||
|
* @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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建无效的验证结果
|
||||||
|
* <p>
|
||||||
|
* 快速创建一个表示验证失败的结果对象
|
||||||
|
* </p>
|
||||||
|
* @param reason 失败原因
|
||||||
|
* @return 标记为无效的验证结果对象
|
||||||
|
*/
|
||||||
|
private AppleReceiptValidationResult invalid(String reason) {
|
||||||
|
AppleReceiptValidationResult r = new AppleReceiptValidationResult();
|
||||||
|
r.setValid(false);
|
||||||
|
r.setReason(reason);
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查交易是否仍然有效
|
||||||
|
* <p>
|
||||||
|
* 根据交易负载中的信息判断交易是否处于有效状态
|
||||||
|
* 1. 已撤销/退款的交易无效
|
||||||
|
* 2. 有过期时间的订阅类交易检查是否已过期
|
||||||
|
* 3. 一次性购买交易只要未撤销则认为有效
|
||||||
|
* </p>
|
||||||
|
* @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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,15 +1,18 @@
|
|||||||
package com.yolo.keyborad.service.impl;
|
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.QdrantClient;
|
||||||
import io.qdrant.client.grpc.Collections;
|
import io.qdrant.client.grpc.Collections;
|
||||||
import io.qdrant.client.grpc.JsonWithInt;
|
import io.qdrant.client.grpc.JsonWithInt;
|
||||||
import io.qdrant.client.grpc.Points;
|
import io.qdrant.client.grpc.Points;
|
||||||
import jakarta.annotation.Resource;
|
import jakarta.annotation.Resource;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.concurrent.ExecutionException;
|
||||||
|
|
||||||
import static io.qdrant.client.PointIdFactory.id;
|
import static io.qdrant.client.PointIdFactory.id;
|
||||||
import static io.qdrant.client.ValueFactory.value;
|
import static io.qdrant.client.ValueFactory.value;
|
||||||
@@ -17,6 +20,7 @@ import static io.qdrant.client.VectorsFactory.vectors;
|
|||||||
|
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
|
@Slf4j
|
||||||
public class QdrantVectorService {
|
public class QdrantVectorService {
|
||||||
|
|
||||||
@Resource
|
@Resource
|
||||||
@@ -31,30 +35,37 @@ public class QdrantVectorService {
|
|||||||
* @param vector 向量(和 collection 中定义的 size 一致)
|
* @param vector 向量(和 collection 中定义的 size 一致)
|
||||||
* @param payload 额外信息,例如原文、标题、userId 等
|
* @param payload 额外信息,例如原文、标题、userId 等
|
||||||
*/
|
*/
|
||||||
public void upsertPoint(long id, List<Float> vector, Map<String, JsonWithInt.Value> payload) throws Exception {
|
public void upsertPoint(long id, List<Float> vector,String payload){
|
||||||
|
|
||||||
|
// // 1. 确保 collection 存在(没有就创建一次即可)
|
||||||
|
// try {
|
||||||
|
// qdrantClient.createCollectionAsync(
|
||||||
|
// COLLECTION_NAME,
|
||||||
|
// Collections.VectorParams.newBuilder()
|
||||||
|
// .setSize(vector.size()) // 向量维度
|
||||||
|
// .setDistance(Collections.Distance.Cosine) // 相似度度量
|
||||||
|
// .build()
|
||||||
|
// ).get(); // 简单起见直接 get(),生产建议在启动时提前创建好
|
||||||
|
// } catch (InterruptedException | ExecutionException e) {
|
||||||
|
// log.error("创建 collection 失败", e);
|
||||||
|
// throw new BusinessException(ErrorCode.OPERATION_ERROR);
|
||||||
|
// }
|
||||||
|
|
||||||
|
try {
|
||||||
// 1. 确保 collection 存在(没有就创建一次即可)
|
qdrantClient.upsertAsync(
|
||||||
qdrantClient.createCollectionAsync(
|
COLLECTION_NAME,
|
||||||
COLLECTION_NAME,
|
List.of(
|
||||||
Collections.VectorParams.newBuilder()
|
Points.PointStruct.newBuilder()
|
||||||
.setSize(vector.size()) // 向量维度
|
.setId(id(id))
|
||||||
.setDistance(Collections.Distance.Cosine) // 相似度度量
|
.setVectors(vectors(vector))
|
||||||
.build()
|
.putAllPayload(Map.of("payload",value(payload)))
|
||||||
).get(); // 简单起见直接 get(),生产建议在启动时提前创建好
|
.build()
|
||||||
|
)
|
||||||
|
).get();
|
||||||
qdrantClient.upsertAsync(
|
} catch (InterruptedException | ExecutionException e) {
|
||||||
COLLECTION_NAME,
|
log.error("upsert point 失败", e);
|
||||||
List.of(
|
throw new BusinessException(ErrorCode.OPERATION_ERROR);
|
||||||
Points.PointStruct.newBuilder()
|
}
|
||||||
.setId(id(id))
|
|
||||||
.setVectors(vectors(vector))
|
|
||||||
.putAllPayload(Map.of("payload",value("testInfo")))
|
|
||||||
.build()
|
|
||||||
)
|
|
||||||
).get();
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
BIN
src/main/resources/AppleRootCA-G2.cer
Normal file
BIN
src/main/resources/AppleRootCA-G2.cer
Normal file
Binary file not shown.
BIN
src/main/resources/AppleRootCA-G3.cer
Normal file
BIN
src/main/resources/AppleRootCA-G3.cer
Normal file
Binary file not shown.
6
src/main/resources/SubscriptionKey_Y7TF7BV74G.p8
Normal file
6
src/main/resources/SubscriptionKey_Y7TF7BV74G.p8
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
-----BEGIN PRIVATE KEY-----
|
||||||
|
MIGTAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBHkwdwIBAQQg3YPU71xkC9jamBjy
|
||||||
|
HhI7dvQrwmK5MstIOEiwdPVSuvqgCgYIKoZIzj0DAQehRANCAATKNsGPJmrKtAda
|
||||||
|
byQwaaV6ODuiV1zX6JW9eNDS/1WJRqDEHCUG44kTaWyczCid4pQdzGumnbBoOxA8
|
||||||
|
0SaFtsTR
|
||||||
|
-----END PRIVATE KEY-----
|
||||||
@@ -15,4 +15,23 @@ knife4j:
|
|||||||
default:
|
default:
|
||||||
api-rule: package
|
api-rule: package
|
||||||
api-rule-resources:
|
api-rule-resources:
|
||||||
- com.yolo.keyborad.controller
|
- com.yolo.keyborad.controller
|
||||||
|
|
||||||
|
apple:
|
||||||
|
appstore:
|
||||||
|
issuer-id: "178b442e-b7be-4526-bd13-ab293d019df0"
|
||||||
|
key-id: "Y7TF7BV74G"
|
||||||
|
bundle-id: "com.loveKey.nyx"
|
||||||
|
# app 在 App Store 的 Apple ID(数值),生产环境必填
|
||||||
|
app-apple-id: 1234567890
|
||||||
|
|
||||||
|
# p8 私钥文件路径(你可以放在 resources 下)
|
||||||
|
private-key-path: "classpath:SubscriptionKey_Y7TF7BV74G.p8"
|
||||||
|
|
||||||
|
# SANDBOX 或 PRODUCTION
|
||||||
|
environment: "SANDBOX"
|
||||||
|
|
||||||
|
# 根证书路径(从 Apple PKI 下载)
|
||||||
|
root-certificates:
|
||||||
|
- "classpath:AppleRootCA-G2.cer"
|
||||||
|
- "classpath:AppleRootCA-G3.cer"
|
||||||
Reference in New Issue
Block a user