Skip to content

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 +
                "&timestamp=" + 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];
    }

}