feat(core): 新增向量数据库与Apple支付支持
This commit is contained in:
45
pom.xml
45
pom.xml
@@ -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>-->
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -36,7 +36,9 @@ public class SaTokenConfigure implements WebMvcConfigurer {
|
||||
"/demo/test",
|
||||
"/error",
|
||||
"/demo/talk",
|
||||
"/user/appleLogin"
|
||||
"/user/appleLogin",
|
||||
"/demo/embed",
|
||||
"/demo/testSaveEmbed"
|
||||
};
|
||||
}
|
||||
@Bean
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
/**
|
||||
* 用户前端控制器
|
||||
*
|
||||
|
||||
12
src/main/java/com/yolo/keyborad/model/dto/AppleLoginReq.java
Normal file
12
src/main/java/com/yolo/keyborad/model/dto/AppleLoginReq.java
Normal 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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.yolo.keyborad.service.impl;
|
||||
package com.yolo.keyborad.service;
|
||||
|
||||
/**
|
||||
* Apple相关API
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
|
||||
}
|
||||
}
|
||||
85
src/main/java/com/yolo/keyborad/utils/ApplePayUtil.java
Normal file
85
src/main/java/com/yolo/keyborad/utils/ApplePayUtil.java
Normal 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();
|
||||
}
|
||||
|
||||
}
|
||||
32
src/main/java/com/yolo/keyborad/utils/ProtoUtils.java
Normal file
32
src/main/java/com/yolo/keyborad/utils/ProtoUtils.java
Normal 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();
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ spring:
|
||||
username: root
|
||||
password: 123asd
|
||||
|
||||
|
||||
knife4j:
|
||||
enable: true
|
||||
openapi:
|
||||
|
||||
Reference in New Issue
Block a user