SpringBoot对接TonaAI OpenAPI
Maven依赖(pom.xml)
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.fastjson2</groupId>
<artifactId>fastjson2</artifactId>
<version>2.0.53</version>
</dependency>
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId>
<version>4.12.0</version>
</dependency>
属性实体(TonaaiProperties.java)
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
@ConfigurationProperties(prefix = "tonaai")
@Component
@Data
public class TonaaiProperties {
String baseUrl;
String apiKey;
String tonaaiapiPublicKey;
String downstreamPrivateKey;
}
配置文件(application.yml)
tonaai:
base-url: https://openapi.tonaai.cn
api-key: 您的ApiKey,你可以在https://api.tonaai.cn管理面板获取
tonaaiapi-public-key: TonaAI生成的公钥,用于对TonaAI返回的信息进行签名校验,你可以在https://api.tonaai.cn管理面板获取私钥
downstream-private-key: 您的私钥,用于在提交任务时进行签名,你可以用支付宝开放平台秘钥工具生成秘钥,并在https://api.tonaai.cn管理面板设置公钥,同时将私钥填写在这一行
工具类(TonaAiApiUtil.java)
import com.alibaba.fastjson2.JSONArray;
import com.alibaba.fastjson2.JSONObject;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import okhttp3.*;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.math.BigDecimal;
import java.security.KeyFactory;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.Signature;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.Base64;
import java.util.List;
import java.util.concurrent.TimeUnit;
@AllArgsConstructor
@Slf4j
public class TonaAiApiUtil {
private TonaaiProperties tonaaiProperties;
private final MediaType MEDIA_TYPE_JSON = MediaType.get("application/json; charset=utf-8");
private final String appId="1001";
public String upload(MultipartFile file,String fileParamName) throws Exception {
OkHttpClient client = new OkHttpClient.Builder()
.connectTimeout(30, TimeUnit.SECONDS)
.writeTimeout(120, TimeUnit.SECONDS) // 文件上传可能需要较长时间
.readTimeout(30, TimeUnit.SECONDS)
.build();
try {
// 1. 检查文件是否为空
if (file == null || file.isEmpty()) {
System.err.println("提供的 MultipartFile 为空或未选择文件。");
return null;
}
// 2. 获取文件的原始字节数组
// 注意: getBytes() 会将整个文件加载到内存中。对于大文件,考虑使用 InputStream 和自定义 RequestBody
byte[] fileBytes = file.getBytes();
// 获取原始文件名
String originalFilename = file.getOriginalFilename();
// 获取文件的 MIME 类型
String contentType = file.getContentType();
// 3. 创建文件的 RequestBody
// 使用 MultipartFile 的 contentType,如果为空则使用通用的二进制流类型
MediaType mediaType = contentType != null ? MediaType.parse(contentType) : MediaType.parse("application/octet-stream");
RequestBody fileBody = RequestBody.create(fileBytes, mediaType);
// 4. 构建 MultipartBody
MultipartBody.Builder multipartBuilder = new MultipartBody.Builder()
.setType(MultipartBody.FORM); // 设置为 multipart/form-data
// 5. 添加文件部分
// 注意: originalFilename 可能为 null,需要处理
multipartBuilder.addFormDataPart(
fileParamName,
originalFilename != null ? originalFilename : "unknown_file",
fileBody
);
RequestBody requestBody = multipartBuilder.build();
// 6. 构建 Request
Request request = new Request.Builder()
.url(tonaaiProperties.getBaseUrl() + "/v1/file/upload")
.post(requestBody) // POST 请求
.addHeader("ApiKey", tonaaiProperties.getApiKey())
.build();
// 7. 执行请求
try (Response response = client.newCall(request).execute()) {
if (response.isSuccessful() && response.body() != null) {
String res=response.body().string();
JSONObject resJson = JSONObject.parseObject(res);
if(resJson.getInteger("code")==0){
JSONObject dataJson = resJson.getJSONObject("data");
return dataJson.getString("fileId");
}else{
log.error("提交任务失败,原因{}", resJson.getString("message"));
return null;
}
}else{
log.error("请求失败,状态码: {}", response.code());
return null;
}
}
} catch (IOException e) {
e.printStackTrace();
log.error("处理 MultipartFile 或执行网络请求时发生错误: {}" ,e.getMessage());
return null;
}
}
public String postDeaiTaskText(String origin,String lang,String mode) {
return postDeaiTask(origin,"text",lang,mode);//0-文本,1-文档
}
public String postDeaiTaskFile(String url,String lang,String mode) {
MultipartFile file = DownloadUtil.urlToFile(url);
String fileParamName = "file";
try {
String fileId = upload(file, fileParamName);
if(fileId != null){
return postDeaiTask(fileId,"file",lang,mode);
}else{
return null;
}
} catch (Exception e) {
log.error("上传文件到TonaAiAPI服务器失败");
return null;
}
}
public String postDeaiTask(String origin,String type,String lang,String mode) {
OkHttpClient client = new OkHttpClient.Builder()
.connectTimeout(10, java.util.concurrent.TimeUnit.SECONDS)
.build();
JSONObject data=new JSONObject();
data.put("origin", origin);
data.put("type", type);
data.put("lang",lang);
data.put("mode",mode);
String jsonBody = signature(data.toJSONString());
if(jsonBody==null){
throw new RuntimeException("提交任务失败");
}
RequestBody body = RequestBody.create(jsonBody, MEDIA_TYPE_JSON);
Request request = new Request.Builder()
.url(tonaaiProperties.getBaseUrl() + "/v1/task/reduce_ai_rate")
.post(body) // ← 指定为 POST 请求
.addHeader("ApiKey", tonaaiProperties.getApiKey())
.build();
try (Response response = client.newCall(request).execute()) {
if (response.isSuccessful() && response.body() != null) {
String res=response.body().string();
JSONObject resJson = JSONObject.parseObject(res);
if(resJson.getInteger("code")==0){
JSONObject dataJson=resJson.getJSONObject("data");
String taskId=dataJson.getString("taskId");
return taskId;
}else{
throw new RuntimeException("提交任务失败,原因"+resJson.getString("message"));
}
}else{
throw new RuntimeException("请求失败,状态码: " + response.code());
}
} catch (Exception e) {
e.printStackTrace();
throw new RuntimeException("提交任务失败");
}
}
public String postRewriteTaskText(String origin,String lang,String mode) {
return postRewriteTask(origin,"text",lang,mode);//0-文本,1-文档
}
public String postRewriteTaskFile(String url,String lang,String mode) {
MultipartFile file = DownloadUtil.urlToFile(url);
String fileParamName = "file";
try {
String fileId = upload(file, fileParamName);
if(fileId != null){
return postRewriteTask(fileId,"file",lang,mode);
}else{
return null;
}
} catch (Exception e) {
log.error("上传文件到TonaAiAPI服务器失败");
return null;
}
}
public String postRewriteTask(String origin,String type,String lang,String mode) {
OkHttpClient client = new OkHttpClient.Builder()
.connectTimeout(10, java.util.concurrent.TimeUnit.SECONDS)
.build();
JSONObject data=new JSONObject();
data.put("origin", origin);
data.put("type", type);
data.put("lang",lang);
data.put("mode",mode);
String jsonBody = signature(data.toJSONString());
if(jsonBody==null){
throw new RuntimeException("提交任务失败");
}
RequestBody body = RequestBody.create(jsonBody, MEDIA_TYPE_JSON);
Request request = new Request.Builder()
.url(tonaaiProperties.getBaseUrl() + "/v1/task/reduce_rp_rate")
.post(body) // ← 指定为 POST 请求
.addHeader("ApiKey", tonaaiProperties.getApiKey())
.build();
try (Response response = client.newCall(request).execute()) {
if (response.isSuccessful() && response.body() != null) {
String res=response.body().string();
JSONObject resJson = JSONObject.parseObject(res);
if(resJson.getInteger("code")==0){
JSONObject dataJson=resJson.getJSONObject("data");
String taskId=dataJson.getString("taskId");
return taskId;
}else{
throw new RuntimeException("提交任务失败,原因"+resJson.getString("message"));
}
}else{
throw new RuntimeException("请求失败,状态码: " + response.code());
}
} catch (Exception e) {
e.printStackTrace();
throw new RuntimeException("提交任务失败");
}
}
public String postAigcCheckTask(String title, String author, String url) {
MultipartFile file = DownloadUtil.urlToFile(url);
String fileName = "file";
try {
String fileIdStr = upload(file, fileName);
Long fileId = Long.parseLong(fileIdStr);
if(fileId != null){
return postAigcCheckTask(title,author,fileId);
}else{
return null;
}
} catch (Exception e) {
log.error("上传文件到TonaAiAPI服务器失败");
return null;
}
}
public String postAigcCheckTask(String title, String author, Long fileId) {
OkHttpClient client = new OkHttpClient.Builder()
.connectTimeout(10, java.util.concurrent.TimeUnit.SECONDS)
.build();
JSONObject data=new JSONObject();
data.put("title", title);
data.put("author", author);
data.put("fileId", fileId);
String jsonBody = signature(data.toJSONString());
if(jsonBody==null){
throw new RuntimeException("提交任务失败");
}
RequestBody body = RequestBody.create(jsonBody, MEDIA_TYPE_JSON);
Request request = new Request.Builder()
.url(tonaaiProperties.getBaseUrl() + "/v1/task/aigc_check")
.post(body) // ← 指定为 POST 请求
.addHeader("ApiKey", tonaaiProperties.getApiKey())
.build();
try (Response response = client.newCall(request).execute()) {
if (response.isSuccessful() && response.body() != null) {
String res=response.body().string();
JSONObject resJson = JSONObject.parseObject(res);
if(resJson.getInteger("code")==0){
JSONObject dataJson=resJson.getJSONObject("data");
String taskId=dataJson.getString("taskId");
return taskId;
}else{
throw new RuntimeException("提交任务失败,原因:"+resJson.getString("message"));
}
}else{
throw new RuntimeException("请求失败,状态码: " + response.code());
}
} catch (Exception e) {
e.printStackTrace();
throw new RuntimeException("提交任务失败");
}
}
public JSONArray getTaskResult(List<Long> taskIds) {
OkHttpClient client = new OkHttpClient.Builder()
.connectTimeout(10, java.util.concurrent.TimeUnit.SECONDS)
.build();
JSONArray json=new JSONArray(taskIds);
RequestBody body = RequestBody.create(json.toJSONString(), MEDIA_TYPE_JSON);
Request request = new Request.Builder()
.url(tonaaiProperties.getBaseUrl() + "/v1/use_record/batch_record")
.post(body) // ← 指定为 POST 请求
.addHeader("ApiKey", tonaaiProperties.getApiKey())
.build();
try (Response response = client.newCall(request).execute()) {
if (response.isSuccessful() && response.body() != null) {
String res=response.body().string();
JSONObject resJson = JSONObject.parseObject(res);
if(resJson.getInteger("code")==0){
JSONArray dataJson=resJson.getJSONArray("data");
return dataJson;
}else{
throw new RuntimeException("获取任务结果失败,原因"+resJson.getString("message"));
}
}else{
throw new RuntimeException("请求失败,状态码: " + response.code());
}
} catch (Exception e) {
e.printStackTrace();
throw new RuntimeException("获取任务结果失败");
}
}
public BigDecimal getBalance() {
OkHttpClient client = new OkHttpClient.Builder()
.connectTimeout(10, java.util.concurrent.TimeUnit.SECONDS)
.build();
Request request = new Request.Builder()
.url(tonaaiProperties.getBaseUrl() + "/v1/user/balance")
.addHeader("ApiKey", tonaaiProperties.getApiKey())
.build();
try (Response response = client.newCall(request).execute()) {
if (response.isSuccessful() && response.body() != null) {
String res=response.body().string();
JSONObject resJson = JSONObject.parseObject(res);
if(resJson.getInteger("code")==0){
return resJson.getBigDecimal("data");
}else{
throw new RuntimeException("获取余额失败,原因"+resJson.getString("message"));
}
}else{
throw new RuntimeException("请求失败,状态码: " + response.code());
}
} catch (Exception e) {
e.printStackTrace();
throw new RuntimeException("获取余额失败");
}
}
private String signature(String jsonData){
String timestamp = java.time.ZonedDateTime.now().toString(); // ISO 8601
String nonceStr = java.util.UUID.randomUUID().toString().replace("-", "").substring(0, 16);
// 2. 拼接待签名字符串
String signContent = buildSignContent(appId, timestamp, nonceStr, jsonData);
log.info("signContent:{}" ,signContent);
// 3. 签名
try {
String sign = sign(signContent, loadPrivateKeyFromPem(tonaaiProperties.getDownstreamPrivateKey()));
log.info("sign:{}", sign);
// 4. 构造请求体 JSON
return String.format(
"""
{
"app_id": "%s",
"timestamp": "%s",
"nonce_str": "%s",
"data": %s,
"sign": "%s"
}
""",
appId, timestamp, nonceStr, jsonData, sign
);
} catch (Exception e) {
log.error("签名失败");
return null;
}
}
public static PrivateKey loadPrivateKeyFromPem(String pemContent) throws Exception {
String privateKeyPEM = pemContent
.replace("-----BEGIN PRIVATE KEY-----", "")
.replace("-----END PRIVATE KEY-----", "")
.replaceAll("\\s", "");
byte[] decoded = Base64.getDecoder().decode(privateKeyPEM);
PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(decoded);
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
return keyFactory.generatePrivate(keySpec);
}
public static PublicKey loadPublicKeyFromPem(String pemContent) throws Exception {
String publicKeyPEM = pemContent
.replace("-----BEGIN PUBLIC KEY-----", "")
.replace("-----END PUBLIC KEY-----", "")
.replaceAll("\\s", "");
byte[] decoded = Base64.getDecoder().decode(publicKeyPEM);
X509EncodedKeySpec keySpec = new X509EncodedKeySpec(decoded);
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
return keyFactory.generatePublic(keySpec);
}
/**
* 使用私钥对内容进行 SHA256withRSA 签名
*/
public static String sign(String content, PrivateKey privateKey) throws Exception {
Signature signature = Signature.getInstance("SHA256withRSA");
signature.initSign(privateKey);
signature.update(content.getBytes("UTF-8"));
byte[] signedBytes = signature.sign();
return Base64.getEncoder().encodeToString(signedBytes);
}
/**
* 验证签名是否有效
* @param content 待签名的原始字符串(拼接后的)
* @param sign Base64 编码的签名值
* @param publicKey 你的公钥
* @return true if valid
*/
public static boolean verifySign(String content, String sign, PublicKey publicKey) throws Exception {
Signature signature = Signature.getInstance("SHA256withRSA");
signature.initVerify(publicKey);
signature.update(content.getBytes("UTF-8"));
byte[] signBytes = java.util.Base64.getDecoder().decode(sign);
return signature.verify(signBytes);
}
/**
* 拼接待验签字符串(必须和上游一致!)
* 注意:实际应按字段名字典序排序
*/
public static String buildSignContent(String appId, String timestamp, String nonceStr, String jsonData) {
return "app_id=" + appId +
"&data=" + jsonData +
"&nonce_str=" + nonceStr +
"×tamp=" + timestamp;
}
}
结果通知(NotifyController.java)
import cn.tonaai.tonaaibackend.pojo.properties.TonaaiProperties;
import com.alibaba.fastjson2.JSONObject;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.transaction.annotation.Transactional;
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.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
@RestController
@RequestMapping("/notify")
@Slf4j
public class NotifyController {
@Autowired
private TonaaiProperties tonaaiProperties;
@PostMapping("/tonaai_result")
public String test(@RequestBody String json) {
JSONObject request = JSONObject.parseObject(json);
log.info("上游回调,消息接收 {}", request);
try {
if (request == null || request.getString("data") == null || request.getString("sign") == null) {
return "false";
}
String signContent = TonaAiApiUtil.buildSignContent(
request.getString("app_id"),
request.getString("timestamp"),
request.getString("nonce_str"),
request.getString("data")
);
boolean isValid = TonaAiApiUtil.verifySign(signContent, request.getString("sign"), TonaAiApiUtil.loadPublicKeyFromPem(tonaaiProperties.getTonaaiapiPublicKey()));
if (!isValid) {
return "false";
}
log.info("验签成功");
JSONObject data = JSONObject.parseObject(request.getString("data"));
log.info("任务id: {}", data.getLong("id"));
//TODO:再此处进行业务处理,更新结果到数据库
log.info("回调任务处理完成,ID:{}",task.getId());
return "success";
} catch (Exception e) {
return "false";
}
}
}
调用说明
提交任务:
注:以下方法提交后都会返回一个taskId,代表该任务在TonaAI系统中的唯一标识符
String postAigcCheckTask(String title, String author, String url);//-->提交AIGC检测任务
String postDeaiTaskText(String origin,String lang,String mode);//-->文本方式提交降AI率
String postDeaiTaskFile(String url,String lang,String mode);//-->文件方式提交降AI率
String postRewriteTaskText(String origin,String lang,String mode);//-->文本方式提交降重复率
String postRewriteTaskFile(String url,String lang,String mode);//-->文件方式提交降重复率
/*
参数:
title: 文章标题,仅在AIGC检测需要
author:文章作者,仅在AIGC检测需要
origin:原文文本内容
url:原文件链接
lang:语言,cn-中文,en-英文,目前仅支持中文
mode:模式,normal-正常,powerful-强力,目前降AI仅支持normal,降重支持normal和powerful
*/
查询数据
JSONArray getTaskResult(List<Long> taskIds);//-->taskIds是之前提交的任务的taskId放一起形成的列表,批量查询效率更高,以免触发接口限流
BigDecimal getBalance();//-->获取您的账户余额
消息通知
考虑到通过getTaskResult方法定时轮询效率太低,推荐使用类似于支付宝支付回调的方式来接收消息,你可以在https://api.tonaai.cn控制台配置回调地址及您的公钥(配置文件中的私钥downstream-private-key对应的公钥,这个公钥是您通过支付宝开放平台秘钥工具生成的公钥),配置完毕后,如果系统中有处理好的任务,TonaAI会通过您提供的接口主动告知您处理结果,具体参见上面的结果通知(NotifyController.java)
约定常量(提交任务时的错误码信息)
public class ApiConstant {
public static final Integer CODE_SUCCESS=0;//账户欠费
public static final Integer CODE_SIGNATURE_ERROR=1000;//签名验证错误
public static final Integer CODE_ARREARS=1001;//账户欠费
public static final Integer CODE_NO_HIT_ARREARS_CACHE=1002;//没有在Redis中找到是否欠费的标记
public static final Integer CODE_NEED_ORIGIN=1003;//缺少原文内容或文件ID
public static final Integer CODE_ORIGIN_TOO_LONG=1004;//原文内容过长
public static final Integer CODE_FILE_ID_NO_EXIST=1005;//文件ID不存在
public static final Integer CODE_PARAMS_ERROR=1006;//参数错误,标题,作者,文件ID
public static final Integer CODE_TITLE_TOO_LONG=1007;//标题过长
public static final Integer CODE_AUTHOR_TOO_LONG=1008;//作者信息过长
public static final Integer CODE_NULL_FILE=1009;//文件为空
public static final Integer CODE_FILE_PARSING_FAILED=1010;//文件解析失败
public static final Integer CODE_TASK_ID_LIST_NULL=1011;//查询结果的任务id列表不能为空
public static final Integer CODE_TASK_ID_LIST_TOO_LONG=1012;//查询结果的任务id列表太长
public static final Integer CODE_FILE_TYPE_ERROR=1013;//任务不支持的文件类型
public static final Integer CODE_ORIGIN_TYPE_ERROR=1014;//源信息错误
public static final Integer CODE_NO_SUPPORT_LANG=1015;//不支持的语言
public static final Integer CODE_NO_SUPPORT_MODE=1016;//不支持的优化模式
}
任务状态
任务状态属性由status表示,您可以在getTaskResult()查询到结果或者消息通知得到结果后获取status属性,status的是字符串类型
数值 | 含义 |
---|---|
-1 | 失败 |
0 | 待处理 |
1 | 处理中 |
2 | 处理成功 |
温馨提示
代码中的DownloadUtil.urlToFile()
方法是自己实现的下载文件(从文件url得到MultipartFile文件),您可以自己实现,也可以直接把MultipartFile传进去
public class DownloadUtil {
public static MultipartFile urlToFile(String fileUrl) {
URL url = null;
URLConnection connection;
String fileName;
String contentType;
try {
url = new URL(fileUrl);
connection = url.openConnection();
fileName = getFileName(url);
contentType = connection.getContentType();
} catch (IOException e) {
log.error("文件下载失败");
return null;
}
try (InputStream inputStream = connection.getInputStream()) {
byte[] bytes = inputStream.readAllBytes();
return new MockMultipartFile(fileName, fileName, contentType, bytes);
} catch (IOException e) {
log.error("文件下载失败");
return null;
}
}
private static String getFileName(URL url) {
String[] pathSegments = url.getPath().split("/");
return pathSegments[pathSegments.length - 1];
}
}