feat(core): 新增向量数据库与Apple支付支持

This commit is contained in:
2025-11-13 22:02:47 +08:00
parent 38ce370cb0
commit 9170f93d67
14 changed files with 346 additions and 13 deletions

45
pom.xml
View File

@@ -55,7 +55,50 @@
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- <dependency>-->
<!-- qdrant向量数据库 sdk -->
<dependency>
<groupId>io.qdrant</groupId>
<artifactId>client</artifactId>
<version>1.15.0</version>
<exclusions>
<exclusion>
<artifactId>guava</artifactId>
<groupId>com.google.guava</groupId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>33.2.0-jre</version>
</dependency>
<dependency>
<groupId>com.google.protobuf</groupId>
<artifactId>protobuf-java</artifactId>
<version>4.28.2</version>
</dependency>
<!-- gRPC API包含 ManagedChannel-->
<dependency>
<groupId>io.grpc</groupId>
<artifactId>grpc-api</artifactId>
<version>1.65.1</version>
</dependency>
<!-- gRPC Stub一般也会用到间接依赖 grpc-api-->
<dependency>
<groupId>io.grpc</groupId>
<artifactId>grpc-stub</artifactId>
<version>1.65.1</version>
</dependency>
<!-- gRPC Netty 传输实现QdrantGrpcClient 底层需要)-->
<dependency>
<groupId>io.grpc</groupId>
<artifactId>grpc-netty-shaded</artifactId>
<version>1.65.1</version>
</dependency>
<!-- <dependency>-->
<!-- <groupId>com.alibaba.cloud.ai</groupId>-->
<!-- <artifactId>spring-ai-alibaba-starter-dashscope</artifactId>-->
<!-- <version>1.0.0.4</version>-->

View File

@@ -3,12 +3,17 @@ package com.yolo.keyborad.config;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.model.ChatModel;
import org.springframework.ai.document.MetadataMode;
import org.springframework.ai.openai.OpenAiChatOptions;
import org.springframework.ai.openai.OpenAiEmbeddingModel;
import org.springframework.ai.openai.OpenAiEmbeddingOptions;
import org.springframework.ai.openai.api.OpenAiApi;
import org.springframework.ai.retry.RetryUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/*
* @author: ziin
* @date: 2025/11/11 20:37
@@ -39,4 +44,19 @@ public class LLMConfig {
.build())
.build();
}
@Bean
public OpenAiEmbeddingModel embeddingModel() {
return new OpenAiEmbeddingModel(
this.openAiApi(),
MetadataMode.EMBED,
OpenAiEmbeddingOptions.builder()
.model("qwen/qwen3-embedding-8b")
.user("user-6")
.build(),
RetryUtils.DEFAULT_RETRY_TEMPLATE);
}
}

View File

@@ -0,0 +1,33 @@
package com.yolo.keyborad.config;
import io.qdrant.client.QdrantClient;
import io.qdrant.client.QdrantGrpcClient;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/*
* @author: ziin
* @date: 2025/11/13 20:28
*/
@Configuration
public class QdrantClientConfig {
private final String qdrantHost = "b0c7f1ee-0eb9-469e-83e0-654249d9bd04.us-east4-0.gcp.cloud.qdrant.io";
private final Integer qdrantPort = 6334;
private final String apiKey = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhY2Nlc3MiOiJtIn0.HX_GxjXCrnhw2DQbMnMFzvDeaHbmNpI2tj2hoUjkvVU";
@Bean
public QdrantClient qdrantClient() {
return new QdrantClient(
QdrantGrpcClient.newBuilder(
qdrantHost,
qdrantPort,
true
)
.withApiKey(apiKey)
.build()
);
}
}

View File

@@ -36,7 +36,9 @@ public class SaTokenConfigure implements WebMvcConfigurer {
"/demo/test",
"/error",
"/demo/talk",
"/user/appleLogin"
"/user/appleLogin",
"/demo/embed",
"/demo/testSaveEmbed"
};
}
@Bean

View File

@@ -1,23 +1,26 @@
package com.yolo.keyborad.controller;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.toolkit.StringUtils;
import com.yolo.keyborad.common.BaseResponse;
import com.yolo.keyborad.common.ErrorCode;
import com.yolo.keyborad.common.ResultUtils;
import com.yolo.keyborad.exception.BusinessException;
import com.yolo.keyborad.model.dto.IosPayVerifyReq;
import com.yolo.keyborad.service.impl.QdrantVectorService;
import io.qdrant.client.QdrantClient;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.embedding.Embedding;
import org.springframework.ai.embedding.EmbeddingResponse;
import org.springframework.ai.openai.OpenAiEmbeddingModel;
import org.springframework.boot.context.properties.bind.DefaultValue;
import org.springframework.web.bind.annotation.*;
import reactor.core.publisher.Flux;
import java.util.List;
/*
* @author: ziin
* @date: 2025/10/28 20:42
@@ -32,6 +35,14 @@ public class DemoController {
@Resource
private ChatClient client;
@Resource
private OpenAiEmbeddingModel embeddingModel;
@Resource
private QdrantVectorService qdrantVectorService;
@GetMapping("/test")
@Operation(summary = "测试接口", description = "测试接口")
public BaseResponse<String> testDemo(){
@@ -53,9 +64,26 @@ public class DemoController {
.content();
}
@PostMapping("/embed")
@Operation(summary = "测试向量接口", description = "测试向量接口")
@Parameter(name = "userInput",required = true,description = "测试向量接口",example = "you are so cute!")
public BaseResponse<Embedding> testEmbed(@DefaultValue("you are so cute!") @RequestBody List<String> userInput){
EmbeddingResponse response = embeddingModel.embedForResponse(userInput);
return ResultUtils.success(response.getResult());
}
@Operation(summary = "IOS内购凭证校验", description = "IOS内购凭证校验")
public BaseResponse<String> iosPay(@RequestBody IosPayVerifyReq req) {
return null;
}
@PostMapping("/testSaveEmbed")
@Operation(summary = "测试存储向量接口", description = "测试存储向量接口")
@Parameter(name = "userInput",required = true,description = "测试存储向量接口")
public BaseResponse<Boolean> testSaveEmbed( @RequestBody List<Float> userInput) throws Exception {
qdrantVectorService.upsertPoint(1L, userInput, null);
return ResultUtils.success(true);
}
}

View File

@@ -1,22 +1,17 @@
package com.yolo.keyborad.controller;
import com.alibaba.fastjson.JSON;
import com.yolo.keyborad.common.BaseResponse;
import com.yolo.keyborad.common.ResultUtils;
import com.yolo.keyborad.model.dto.AppleLoginReq;
import com.yolo.keyborad.service.impl.IAppleService;
import com.yolo.keyborad.service.IAppleService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeanUtils;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.Objects;
/**
* 用户前端控制器
*

View File

@@ -0,0 +1,12 @@
package com.yolo.keyborad.model.dto;
import lombok.Data;
/*
* @author: ziin
* @date: 2025/11/13 16:15
*/
@Data
public class AppleLoginReq {
private String identityToken;
}

View File

@@ -0,0 +1,21 @@
package com.yolo.keyborad.model.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@Data
public class IosPayVerifyReq {
@Schema(description = "商家订单id")
private String orderId;
@Schema(description = "用户id")
private String userId;
@Schema(description = "验证凭据")
private String receiptDate;
@Schema(description = "ios选项值")
private String productId;
}

View File

@@ -1,4 +1,4 @@
package com.yolo.keyborad.service.impl;
package com.yolo.keyborad.service;
/**
* Apple相关API

View File

@@ -3,6 +3,7 @@ package com.yolo.keyborad.service.impl;
import cn.hutool.http.HttpUtil;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.yolo.keyborad.service.IAppleService;
import io.jsonwebtoken.*;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;

View File

@@ -0,0 +1,60 @@
package com.yolo.keyborad.service.impl;
import com.yolo.keyborad.utils.ProtoUtils;
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 org.springframework.stereotype.Service;
import java.util.List;
import java.util.Map;
import static io.qdrant.client.PointIdFactory.id;
import static io.qdrant.client.ValueFactory.value;
import static io.qdrant.client.VectorsFactory.vectors;
@Service
public class QdrantVectorService {
@Resource
private QdrantClient qdrantClient;
private static final String COLLECTION_NAME = "test_document";
/**
* 插入/更新一条向量数据
*
* @param id 向量ID可以是 Long / String自行约定
* @param vector 向量(和 collection 中定义的 size 一致)
* @param payload 额外信息例如原文、标题、userId 等
*/
public void upsertPoint(long id, List<Float> vector, Map<String, JsonWithInt.Value> payload) throws Exception {
// 1. 确保 collection 存在(没有就创建一次即可)
qdrantClient.createCollectionAsync(
COLLECTION_NAME,
Collections.VectorParams.newBuilder()
.setSize(vector.size()) // 向量维度
.setDistance(Collections.Distance.Cosine) // 相似度度量
.build()
).get(); // 简单起见直接 get(),生产建议在启动时提前创建好
qdrantClient.upsertAsync(
COLLECTION_NAME,
List.of(
Points.PointStruct.newBuilder()
.setId(id(id))
.setVectors(vectors(vector))
.putAllPayload(Map.of("payload",value("testInfo")))
.build()
)
).get();
}
}

View File

@@ -0,0 +1,85 @@
package com.yolo.keyborad.utils;
import javax.net.ssl.*;
import java.io.BufferedOutputStream;
import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.URL;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.Locale;
public class ApplePayUtil {
private static class TrustAnyTrustManager implements X509TrustManager {
@Override
public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {
}
@Override
public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {
}
@Override
public X509Certificate[] getAcceptedIssuers() {
return new X509Certificate[]{};
}
}
private static class TrustAnyHostnameVerifier implements HostnameVerifier {
@Override
public boolean verify(String hostname, SSLSession session) {
return true;
}
}
private static final String url_sandbox = "https://sandbox.itunes.apple.com/verifyReceipt";
private static final String url_verify = "https://buy.itunes.apple.com/verifyReceipt";
/**
* 苹果服务器验证
*
* @param receipt 账单
* @return null 或返回结果 沙盒 https://sandbox.itunes.apple.com/verifyReceipt
* @url 要验证的地址
*/
public static String buyAppVerify(String receipt, int type) throws Exception {
//环境判断 线上/开发环境用不同的请求链接
String url = "";
if (type == 0) {
url = url_sandbox; //沙盒测试
} else {
url = url_verify; //线上测试
}
SSLContext sc = SSLContext.getInstance("SSL");
sc.init(null, new TrustManager[]{new TrustAnyTrustManager()}, new java.security.SecureRandom());
URL console = new URL(url);
HttpsURLConnection conn = (HttpsURLConnection) console.openConnection();
conn.setSSLSocketFactory(sc.getSocketFactory());
conn.setHostnameVerifier(new TrustAnyHostnameVerifier());
conn.setRequestMethod("POST");
conn.setRequestProperty("content-type", "text/json");
conn.setRequestProperty("Proxy-Connection", "Keep-Alive");
conn.setDoInput(true);
conn.setDoOutput(true);
BufferedOutputStream hurlBufOus = new BufferedOutputStream(conn.getOutputStream());
//拼成固定的格式传给平台
String str = String.format(Locale.CHINA, "{\"receipt-data\":\"" + receipt + "\"}");
hurlBufOus.write(str.getBytes());
hurlBufOus.flush();
InputStream is = conn.getInputStream();
BufferedReader reader = new BufferedReader(new InputStreamReader(is));
String line = null;
StringBuffer sb = new StringBuffer();
while ((line = reader.readLine()) != null) {
sb.append(line);
}
return sb.toString();
}
}

View File

@@ -0,0 +1,32 @@
package com.yolo.keyborad.utils;
import com.google.protobuf.Struct;
import com.google.protobuf.Value;
import java.util.Map;
public class ProtoUtils {
public static Struct mapToStruct(Map<String, Object> map) {
Struct.Builder structBuilder = Struct.newBuilder();
map.forEach((key, value) -> structBuilder.putFields(key, toValue(value)));
return structBuilder.build();
}
private static Value toValue(Object obj) {
Value.Builder valueBuilder = Value.newBuilder();
if (obj instanceof String) {
valueBuilder.setStringValue((String) obj);
} else if (obj instanceof Number) {
valueBuilder.setNumberValue(((Number) obj).doubleValue());
} else if (obj instanceof Boolean) {
valueBuilder.setBoolValue((Boolean) obj);
} else {
// 复杂类型你自己扩展,也可以转 string 存
valueBuilder.setStringValue(obj.toString());
}
return valueBuilder.build();
}
}

View File

@@ -5,6 +5,7 @@ spring:
username: root
password: 123asd
knife4j:
enable: true
openapi: