Compare commits

..

2 Commits

Author SHA1 Message Date
old burden 8a4c0f73c6 fix: 新需求 对接火山seedance 2026-03-27 15:27:19 +08:00
old burden 3a05e41f79 fix: 新需求 2026-03-27 10:51:32 +08:00
11 changed files with 414 additions and 130 deletions

View File

@ -10,10 +10,10 @@ import com.ruoyi.common.annotation.Anonymous;
import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.core.domain.model.LoginAiUser;
import com.ruoyi.common.utils.AwsS3Util;
import com.ruoyi.common.utils.RandomStringUtil;
import com.ruoyi.common.utils.SecurityUtils;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.common.utils.TencentCosUtil;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.RequiredArgsConstructor;
@ -36,13 +36,21 @@ import java.util.regex.Pattern;
public class ByteApiController extends BaseController {
private final IByteService byteService;
private final AwsS3Util awsS3Util;
private final TencentCosUtil tencentCosUtil;
private final IAiOrderService aiOrderService;
private final IAiManagerService managerService;
private final IAiTagService aiTagService;
@Value("${byteapi.callBackUrl}")
private String url;
// 火山引擎配置
@Value("${volcengine.ark.apiKey}")
private String volcApiKey;
@Value("${volcengine.ark.baseUrl}")
private String volcBaseUrl;
@Value("${volcengine.ark.callbackUrl}")
private String volcCallbackUrl;
@PostMapping("/promptToImg")
@ApiOperation("文生图")
public AjaxResult promptToImg(@RequestBody ByteApiRequest request) {
@ -51,9 +59,12 @@ public class ByteApiController extends BaseController {
return AjaxResult.error("functionType is null");
}
String mode = request.getMode() != null ? request.getMode() : "image-to-video";
AiManager aiManager = managerService.selectAiManagerByType(functionType);
String tags = request.getTags();
String text = "";
String text = request.getText();
// 如果使用标签系统生成prompt
if (StringUtils.isNotEmpty(tags)) {
List<AiTag> aiTags = aiTagService.selectAiTagListByIds(request.getTags(), aiManager.getParentIdSort());
List<String> tagPrompts = new ArrayList<>();
@ -70,22 +81,29 @@ public class ByteApiController extends BaseController {
tagPrompts.add(p);
}
text = StringUtils.replacePlaceholders(aiManager.getPrompt(), tagPrompts);
} else {
text = aiManager.getPrompt();
}
if (StringUtils.isEmpty(text)) {
return AjaxResult.error("text is null");
}
AiOrder aiOrder = aiOrderService.getAiOrder(functionType);
try {
if (aiOrder == null) {
return AjaxResult.error(-1, "You have a low balance, please recharge");
}
aiOrder.setText(text);
aiOrder.setFunctionType(mode); // 记录生成模式
// 文生视频模式下不设置图片
if ("image-to-video".equals(mode) && firstUrl != null) {
aiOrder.setImg1(firstUrl.toString());
}
ByteBodyReq byteBodyReq = new ByteBodyReq();
byteBodyReq.setModel("ep-20251104104536-2gpgz");
// model由前端传入默认为Seedance 2.0
byteBodyReq.setModel(StringUtils.isNotEmpty(request.getModel()) ?
request.getModel() : "ep-20260326165811-dlkth");
byteBodyReq.setPrompt(text);
byteBodyReq.setSequential_image_generation("disabled");
byteBodyReq.setResponse_format("url");
@ -96,7 +114,7 @@ public class ByteApiController extends BaseController {
List<ByteDataRes> data = byteBodyRes.getData();
ByteDataRes byteDataRes = data.get(0);
String url = byteDataRes.getUrl();
url = awsS3Util.uploadFileByUrl(url);
url = tencentCosUtil.uploadFileByUrl(url);
if (url == null) {
// 判断生成失败退回金额逻辑
aiOrderService.orderFailure(aiOrder);
@ -175,7 +193,7 @@ public class ByteApiController extends BaseController {
List<ByteDataRes> data = byteBodyRes.getData();
ByteDataRes byteDataRes = data.get(0);
String url = byteDataRes.getUrl();
url = awsS3Util.uploadFileByUrl(url);
url = tencentCosUtil.uploadFileByUrl(url);
if (url == null) {
// 判断生成失败退回金额逻辑
aiOrderService.orderFailure(aiOrder);
@ -191,7 +209,7 @@ public class ByteApiController extends BaseController {
}
@PostMapping("/imgToVideo")
@ApiOperation("图生视频")
@ApiOperation("图生视频 (Seedance 2.0)")
public AjaxResult imgToVideo(@RequestBody ByteApiRequest request) throws Exception {
String functionType = request.getFunctionType();
if (null == functionType) {
@ -235,51 +253,56 @@ public class ByteApiController extends BaseController {
return AjaxResult.error(-1, "You have a low balance, please recharge");
}
try {
// String text = request.getText();
// if (StringUtils.isBlank(text)) {
// return AjaxResult.error("text is null");
// }
// String tags = request.getTags();
// if (StringUtils.isNotBlank(tags)) {
// text = "(优先考虑以下关键词:" + tags + ")";
// }
aiOrder.setText(text);
aiOrder.setImg1(firstUrl.toString());
Integer duration = request.getDuration();
Integer duration = request.getDuration() != null ? request.getDuration() : 4;
ByteBodyReq byteBodyReq = new ByteBodyReq();
byteBodyReq.setModel("ep-20251113072240-cfxlz");
byteBodyReq.setCallback_url(url + "/api/ai/callBack");
List<ContentItem> content = new ArrayList<>();
ContentItem contentItem = new ContentItem();
contentItem.setType("text");
contentItem.setText(text + " --dur " + duration + " --fps 24 --rs 720p --wm false --cf false");
content.add(contentItem);
// model由前端传入默认为Seedance2.0
byteBodyReq.setModel(StringUtils.isNotEmpty(request.getModel()) ?
request.getModel() : "ep-20260326165811-dlkth");
byteBodyReq.setCallback_url(volcCallbackUrl);
ContentItem contentItem1 = new ContentItem();
contentItem1.setType("image_url");
contentItem1.setRole("first_frame");
ImageUrl imageUrl1 = new ImageUrl();
imageUrl1.setUrl(firstUrl.toString());
contentItem1.setImageUrl(imageUrl1);
content.add(contentItem1);
// 构建符合火山引擎格式的content
List<ContentItem> contentList = new ArrayList<>();
// 文本提示词
ContentItem textItem = new ContentItem();
textItem.setType("text");
textItem.setText(text);
contentList.add(textItem);
// 首帧图片
ContentItem firstFrameItem = new ContentItem();
firstFrameItem.setType("image_url");
firstFrameItem.setRole("first_frame");
ImageUrl firstImageUrl = new ImageUrl();
firstImageUrl.setUrl(firstUrl.toString());
firstFrameItem.setImageUrl(firstImageUrl);
contentList.add(firstFrameItem);
// 如果有尾帧
String lastUrl = request.getLastUrl();
if (StringUtils.isNotBlank(lastUrl)) {
ContentItem contentItem2 = new ContentItem();
contentItem2.setType("image_url");
contentItem2.setRole("last_frame");
ImageUrl imageUrl2 = new ImageUrl();
imageUrl2.setUrl(lastUrl);
contentItem2.setImageUrl(imageUrl2);
content.add(contentItem2);
ContentItem lastFrameItem = new ContentItem();
lastFrameItem.setType("image_url");
lastFrameItem.setRole("last_frame");
ImageUrl lastImageUrl = new ImageUrl();
lastImageUrl.setUrl(lastUrl);
lastFrameItem.setImageUrl(lastImageUrl);
contentList.add(lastFrameItem);
aiOrder.setImg2(lastUrl);
}
byteBodyReq.setContent(content);
byteBodyReq.setContent(contentList);
byteBodyReq.setDuration(duration);
byteBodyReq.setResolution("720p");
byteBodyReq.setRatio("3:4");
ByteBodyRes byteBodyRes = byteService.imgToVideo(byteBodyReq);
String id = byteBodyRes.getId();
if (id == null) {
// 判断生成失败退回金额逻辑
aiOrderService.orderFailure(aiOrder);
return AjaxResult.error(-2, "generation failed, balance has been refunded");
}
@ -299,7 +322,7 @@ public class ByteApiController extends BaseController {
if ("succeeded".equals(byteBodyRes.getStatus())) {
content content = byteBodyRes.getContent();
String videoUrl = content.getVideo_url();
videoUrl = awsS3Util.uploadFileByUrl(videoUrl);
videoUrl = tencentCosUtil.uploadFileByUrl(videoUrl);
content.setVideo_url(videoUrl);
AiOrder aiOrderByResult = aiOrderService.getAiOrderByResult(id);
AiOrder aiOrder = new AiOrder();
@ -311,24 +334,34 @@ public class ByteApiController extends BaseController {
return AjaxResult.success(byteBodyRes);
}
@GetMapping(value = "/callBack")
@ApiOperation("视频下载回调")
@GetMapping(value = "/volcCallback")
@ApiOperation("火山引擎视频回调")
@Anonymous
public AjaxResult callBack(@PathVariable("id") ByteBodyRes byteBodyRes) throws Exception {
public AjaxResult volcCallback(@RequestBody ByteBodyRes byteBodyRes) throws Exception {
if ("succeeded".equals(byteBodyRes.getStatus())) {
String id = byteBodyRes.getId();
content content = byteBodyRes.getContent();
String videoUrl = content.getVideo_url();
videoUrl = awsS3Util.uploadFileByUrl(videoUrl);
content.setVideo_url(videoUrl);
content contentObj = byteBodyRes.getContent();
if (contentObj != null && StringUtils.isNotEmpty(contentObj.getVideo_url())) {
String videoUrl = contentObj.getVideo_url();
videoUrl = tencentCosUtil.uploadFileByUrl(videoUrl);
contentObj.setVideo_url(videoUrl);
AiOrder aiOrderByResult = aiOrderService.getAiOrderByResult(id);
if (aiOrderByResult != null) {
AiOrder aiOrder = new AiOrder();
aiOrder.setId(aiOrderByResult.getId());
aiOrder.setResult(videoUrl);
// aiOrder.setUpdateBy(SecurityUtils.getLoginAiUser().getUsername());
aiOrderService.updateAiOrder(aiOrder);
}
return AjaxResult.success(byteBodyRes);
}
}
return AjaxResult.success("callback success");
}
@PostMapping(value = "/{id}/cancel")
@ApiOperation("取消视频生成任务")
public AjaxResult cancelTask(@PathVariable("id") String id) throws Exception {
return byteService.cancelVideoTask(id);
}
}

View File

@ -1,7 +1,7 @@
package com.ruoyi.api;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.utils.AwsS3Util;
import com.ruoyi.common.utils.TencentCosUtil;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;
@ -18,7 +18,7 @@ import org.springframework.web.multipart.MultipartFile;
@Api(tags = "文件上传")
@RequiredArgsConstructor(onConstructor_ = @Autowired)
public class FileController {
private final AwsS3Util awsS3Util;
private final TencentCosUtil tencentCosUtil;
/**
* 文件上传
@ -30,7 +30,7 @@ public class FileController {
@ApiParam(name = "file", value = "文件", required = true)
@RequestParam("file") MultipartFile file) throws Exception {
AjaxResult ajax = AjaxResult.success();
String uploadUrl = awsS3Util.uploadMultipartFile(file, true);
String uploadUrl = tencentCosUtil.uploadMultipartFile(file, true);
ajax.put("url", uploadUrl);
return ajax;
}

View File

@ -31,6 +31,11 @@ public class ByteApiRequest {
@ApiModelProperty(name = "标签字符串")
private String tags;
@ApiModelProperty(name = "使用的模型")
private String model;
@ApiModelProperty(name = "生成模式text-to-video 或 image-to-video")
private String mode = "text-to-video";
}

View File

@ -4,7 +4,6 @@ import com.ruoyi.common.annotation.Anonymous;
import com.ruoyi.common.config.RuoYiConfig;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.exception.base.BaseException;
import com.ruoyi.common.utils.AwsS3Util;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.common.utils.TencentCosUtil;
import com.ruoyi.common.utils.file.FileUploadUtils;
@ -36,7 +35,6 @@ public class CommonController {
private static final Logger log = LoggerFactory.getLogger(CommonController.class);
private final ServerConfig serverConfig;
private final TencentCosUtil tencentCosUtil;
private final AwsS3Util awsS3Util;
/**
* 通用下载请求
@ -158,7 +156,7 @@ public class CommonController {
}
/**
* AWS上传请求单个
* 腾讯云COS上传请求单个
*/
@ApiOperation("图片上传接口")
@PostMapping("/aws/upload")
@ -166,7 +164,7 @@ public class CommonController {
AjaxResult ajax = AjaxResult.success();
String uploadUrl;
try {
uploadUrl = awsS3Util.uploadMultipartFile(file, true);
uploadUrl = tencentCosUtil.uploadMultipartFile(file, true);
} catch (Exception e) {
return AjaxResult.error(e.getMessage());
}

View File

@ -209,24 +209,32 @@ google:
redirect-uri:
tencentCos:
accessKey:
secretKey:
endpoint:
bucketName:
domain:
accessKey: AKIDBE3dzBdLsHYfZLwKVSFArLchZDerrfHf
secretKey: EDyUmsnX2IJ5f0oRn1QdeQ0TmrtqgQ1c
endpoint: ap-guangzhou
bucketName: seedance-1331490964
domain: https://seedance-1331490964.cos.ap-guangzhou.myqcloud.com
aws:
accessKey: AKIAYVMHEVDDZQGE3HVX
secretKey: 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
endpoint: ap-southeast-1
bucketName: di-image
domain: https://images.iqyjsnwv.com/
# aws配置已替换为腾讯云COS请在环境变量或配置文件中设置腾讯云凭证
# aws:
# accessKey: AKIAYVMHEVDDZQGE3HVX
# secretKey: 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
# endpoint: ap-southeast-1
# bucketName: di-image
# domain: https://images.iqyjsnwv.com/
byteapi:
url: https://ark.ap-southeast.bytepluses.com/api/v3
apiKey: 327d2815-2516-44c2-9e32-2dc50bf7afd7
apiKey: 3e33e034-7e25-4228-8864-b51b2a7a8f97
callBackUrl: https://undressing.top
# 火山引擎 Ark API (Seedance 2.0)
volcengine:
ark:
baseUrl: https://ark.cn-beijing.volces.com
apiKey: 3e33e034-7e25-4228-8864-b51b2a7a8f97
callbackUrl: https://undressing.top/api/ai/volcCallback
jinsha:
url: https://api.jinshapay.xyz
appId: 1763617360

View File

@ -13,7 +13,13 @@ import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.UUID;
@Component
@ -33,22 +39,32 @@ public class TencentCosUtil {
private String domain;
//文件上传
public String upload(MultipartFile file) {
/**
* 上传MultipartFile到腾讯云COS返回文件访问地址
* 与AwsS3Util.uploadMultipartFile方法接口兼容
*/
public String upload(MultipartFile file) throws Exception {
return uploadMultipartFile(file, true);
}
/**
* 上传MultipartFile到腾讯云COS返回文件访问地址
*
* @param file 前端上传的MultipartFile
* @param isPublic 是否公开访问当前实现中忽略使用domain配置
* @return 文件访问地址URL字符串
*/
public String uploadMultipartFile(MultipartFile file, boolean isPublic) throws Exception {
if (file.isEmpty()) {
throw new IllegalArgumentException("上传文件不能为空");
}
// 3 生成 cos 客户端
COSClient cosClient = createCosClient();
// 存储桶的命名格式为 BucketName-APPID此处填写的存储桶名称必须为此格式
// 对象键(Key)是对象在存储桶中的唯一标识 998u-09iu-09i-333
//在文件名称前面添加uuid值
String key = UUID.randomUUID().toString().replaceAll("-", "").substring(0, 8) + "_"
+ file.getOriginalFilename();
//对上传文件分组根据当前日期 /2022/11/11
String dateTime = new DateTime().toString("yyyy/MM/dd");
key = dateTime + "/" + key;
// 生成唯一文件键格式与AWS一致yyyy/MM/dd/uuid_filename
String key = generateCosKey(file.getOriginalFilename());
try {
//获取上传文件输入流
InputStream inputStream = file.getInputStream();
ObjectMetadata objectMetadata = new ObjectMetadata();
PutObjectRequest putObjectRequest = new PutObjectRequest(
@ -56,17 +72,41 @@ public class TencentCosUtil {
key,
inputStream,
objectMetadata);
// 高级接口会返回一个异步结果Upload
PutObjectResult putObjectResult = cosClient.putObject(putObjectRequest);
//返回上传文件路径
//https://ggkt-atguigu-1310644373.cos.ap-beijing.myqcloud.com/01.jpg
String url = domain + "/" + key;
// 返回COS文件访问地址
String url = domain + (domain.endsWith("/") ? "" : "/") + key;
return url;
} catch (Exception e) {
e.printStackTrace();
throw new RuntimeException("上传文件到COS失败: " + e.getMessage(), e);
} finally {
cosClient.shutdown();
}
}
/**
* 通过URL下载文件并上传到COS
* 与AwsS3Util.uploadFileByUrl方法接口兼容
*/
public String uploadFileByUrl(String fileUrl) throws Exception {
return uploadFileByUrl(fileUrl, true);
}
public String uploadFileByUrl(String fileUrl, boolean isPublic) throws Exception {
if (fileUrl == null || fileUrl.trim().isEmpty()) {
throw new IllegalArgumentException("文件下载链接不能为空");
}
Path tempPath = downloadFileToTemp(fileUrl);
try {
// 使用临时文件上传
MultipartFile multipartFile = createMultipartFileFromPath(tempPath, extractFileNameFromUrl(fileUrl));
return uploadMultipartFile(multipartFile, isPublic);
} finally {
Files.deleteIfExists(tempPath);
}
return null;
}
@ -78,7 +118,107 @@ public class TencentCosUtil {
ClientConfig clientConfig = new ClientConfig(region);
// 这里建议设置使用 https 协议
clientConfig.setHttpProtocol(HttpProtocol.https);
clientConfig.setConnectionTimeout(30 * 1000); // 连接超时30秒
clientConfig.setSocketTimeout(60 * 1000); // 读取超时60秒
//1.3 生成cos客户端
return new COSClient(credentials, clientConfig);
}
/**
* 生成COS文件键与AWS保持一致的命名格式
*/
private String generateCosKey(String originalFileName) {
String uuid = UUID.randomUUID().toString().replaceAll("-", "").substring(0, 8);
String dateTime = new DateTime().toString("yyyy/MM/dd");
return dateTime + "/" + uuid + "_" + originalFileName;
}
/**
* 下载文件到临时路径
*/
private Path downloadFileToTemp(String fileUrl) throws Exception {
String suffix = getFileSuffixFromUrl(fileUrl);
Path tempPath = Files.createTempFile("url-upload-", suffix);
HttpURLConnection connection = null;
InputStream in = null;
OutputStream out = null;
try {
URL url = new URL(fileUrl);
connection = (HttpURLConnection) url.openConnection();
connection.setRequestMethod("GET");
connection.setConnectTimeout(5000);
connection.setReadTimeout(10000);
int responseCode = connection.getResponseCode();
if (responseCode < 200 || responseCode >= 300) {
throw new RuntimeException("文件下载失败,状态码:" + responseCode);
}
in = connection.getInputStream();
out = Files.newOutputStream(tempPath);
byte[] buffer = new byte[4096];
int bytesRead;
while ((bytesRead = in.read(buffer)) != -1) {
out.write(buffer, 0, bytesRead);
}
} finally {
if (out != null) {
try { out.close(); } catch (IOException e) { e.printStackTrace(); }
}
if (in != null) {
try { in.close(); } catch (IOException e) { e.printStackTrace(); }
}
if (connection != null) {
connection.disconnect();
}
}
return tempPath;
}
private String extractFileNameFromUrl(String fileUrl) {
String fileName = fileUrl.substring(fileUrl.lastIndexOf("/") + 1);
if (fileName.contains("?")) {
fileName = fileName.split("\\?")[0];
}
return fileName.isEmpty() ? "default_file" : fileName;
}
private String getFileSuffixFromUrl(String fileUrl) {
String fileName = extractFileNameFromUrl(fileUrl);
if (fileName.contains(".")) {
return fileName.substring(fileName.lastIndexOf("."));
}
return ".tmp";
}
/**
* 将Path转换为MultipartFile简单实现用于uploadFileByUrl
*/
private MultipartFile createMultipartFileFromPath(Path path, String originalFilename) throws IOException {
byte[] bytes = Files.readAllBytes(path);
return new MultipartFile() {
@Override
public String getName() { return "file"; }
@Override
public String getOriginalFilename() { return originalFilename; }
@Override
public String getContentType() { return null; }
@Override
public boolean isEmpty() { return bytes.length == 0; }
@Override
public long getSize() { return bytes.length; }
@Override
public byte[] getBytes() throws IOException { return bytes; }
@Override
public InputStream getInputStream() throws IOException { return Files.newInputStream(path); }
@Override
public void transferTo(java.io.File dest) throws IOException, IllegalStateException {
Files.copy(path, dest.toPath());
}
};
}
}

View File

@ -68,6 +68,22 @@ public class AiOrder extends BaseEntity {
@Excel(name = "是否置顶N-否 Y-是")
private String isTop;
/** 生成模式text-to-video 或 image-to-video */
@Excel(name = "生成模式")
private String mode;
/** 视频时长(秒) */
@Excel(name = "视频时长")
private Integer duration;
/** 分辨率(如 720p, 1080p */
@Excel(name = "分辨率")
private String resolution;
/** 宽高比(如 16:9, 9:16 */
@Excel(name = "宽高比")
private String ratio;
/** 首帧图片 */
private String img1;

View File

@ -30,5 +30,10 @@ public class ByteBodyReq {
@JsonProperty("content")
private List<ContentItem> content;
private Integer duration;
private String resolution;
private String ratio;
private Integer seed;
}

View File

@ -2,6 +2,7 @@ package com.ruoyi.ai.service;
import com.ruoyi.ai.domain.ByteBodyReq;
import com.ruoyi.ai.domain.ByteBodyRes;
import com.ruoyi.common.core.domain.AjaxResult;
public interface IByteService {
@ -24,4 +25,9 @@ public interface IByteService {
* 下载视频
*/
ByteBodyRes uploadVideo(String id) throws Exception;
/**
* 取消视频生成任务
*/
AjaxResult cancelVideoTask(String id) throws Exception;
}

View File

@ -6,6 +6,7 @@ import com.fasterxml.jackson.databind.ObjectMapper;
import com.ruoyi.ai.domain.ByteBodyReq;
import com.ruoyi.ai.domain.ByteBodyRes;
import com.ruoyi.ai.service.IByteService;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.common.utils.http.OkHttpUtils;
import okhttp3.*;
@ -30,6 +31,13 @@ public class ByteService implements IByteService {
@Value("${byteapi.apiKey}")
private String apiKey;
// 火山引擎配置
@Value("${volcengine.ark.baseUrl:https://ark.cn-beijing.volces.com}")
private String volcBaseUrl;
@Value("${volcengine.ark.apiKey}")
private String volcApiKey;
@Override
public ByteBodyRes promptToImg(ByteBodyReq req) throws Exception {
return this.imgToImg(req);
@ -75,71 +83,92 @@ public class ByteService implements IByteService {
@Override
public ByteBodyRes imgToVideo(ByteBodyReq req) throws Exception {
if (req == null) {
throw new Exception("imgToVideo errorreq is null");
}
// 1. 验证请求参数可选根据业务需求
// if (StringUtils.isBlank(req.getPrompt())) {
// throw new Exception("imgToVideo errorprompt is null");
// }
// 2. 构建请求体JSON基于ByteBodyReq的字段
// 注意ByteBodyReq需包含与API参数对应的字段modelprompt等
// 使用火山引擎配置
String jsonBody = objectMapper.writeValueAsString(req);
// 3. 构建请求
Request request = new Request.Builder()
.url(API_URL + "/contents/generations/tasks")
.url(volcBaseUrl + "/api/v3/contents/generations/tasks")
.header("Content-Type", "application/json")
.header("Authorization", "Bearer " + apiKey)
.header("Authorization", "Bearer " + volcApiKey)
.post(RequestBody.create(
MediaType.parse("application/json; charset=utf-8"),
jsonBody
))
.build();
// 4. 发送同步请求因方法需要返回值使用execute而非enqueue
Response response = OkHttpUtils.newCall(request).execute();
// 5. 处理响应
if (!response.isSuccessful()) {
// 非200状态返回错误信息假设ByteBodyRes有error字段
String errorMsg = response.body() != null ? response.body().string() : "imgToVideo error";
throw new Exception("imgToVideo error" + errorMsg);
}
// 6. 解析成功响应为ByteBodyRes
if (response.body() == null) {
throw new Exception("imgToVideo response null");
}
String responseBody = response.body().string();
return objectMapper.readValue(responseBody, ByteBodyRes.class);
}
@Override
public ByteBodyRes uploadVideo(String id) throws Exception {
// 1. 验证请求参数可选根据业务需求
if (StringUtils.isBlank(id)) {
throw new Exception("uploadVideo errorid is null");
}
// 2. 构建请求体JSON基于ByteBodyReq的字段
// 注意ByteBodyReq需包含与API参数对应的字段modelprompt等
//String jsonBody = objectMapper.writeValueAsString(req);
// 3. 构建请求
// 使用火山引擎配置查询任务状态
Request request = new Request.Builder()
.url(API_URL + "/contents/generations/tasks/" + id)
.url(volcBaseUrl + "/api/v3/contents/generations/tasks/" + id)
.header("Content-Type", "application/json")
.header("Authorization", "Bearer " + apiKey)
.header("Authorization", "Bearer " + volcApiKey)
.get()
.build();
// 4. 发送同步请求因方法需要返回值使用execute而非enqueue
Response response = OkHttpUtils.newCall(request).execute();
// 5. 处理响应
if (!response.isSuccessful()) {
// 非200状态返回错误信息假设ByteBodyRes有error字段
String errorMsg = response.body() != null ? response.body().string() : "uploadVideo error";
throw new Exception("uploadVideo error" + errorMsg);
}
// 6. 解析成功响应为ByteBodyRes
if (response.body() == null) {
throw new Exception("uploadVideo response null");
}
String responseBody = response.body().string();
return objectMapper.readValue(responseBody, ByteBodyRes.class);
}
@Override
public AjaxResult cancelVideoTask(String id) throws Exception {
if (StringUtils.isBlank(id)) {
return AjaxResult.error("任务ID不能为空");
}
try {
// 向火山引擎发送 DELETE 请求取消任务
Request request = new Request.Builder()
.url(volcBaseUrl + "/api/v3/contents/generations/tasks/" + id)
.header("Content-Type", "application/json")
.header("Authorization", "Bearer " + volcApiKey)
.delete()
.build();
Response response = OkHttpUtils.newCall(request).execute();
if (!response.isSuccessful()) {
String errorMsg = response.body() != null ? response.body().string() : "cancel failed";
return AjaxResult.error("取消任务失败:" + errorMsg);
}
return AjaxResult.success("任务已取消,余额已退回");
} catch (Exception e) {
return AjaxResult.error("取消任务异常:" + e.getMessage());
}
}
}

View File

@ -23,6 +23,7 @@ SET FOREIGN_KEY_CHECKS = 0;
DROP TABLE IF EXISTS `ai_balance_change_record`;
CREATE TABLE `ai_balance_change_record` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`order_no` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '关联订单号',
`del_flag` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '0' COMMENT '删除标志0代表存在 2代表删除',
`create_by` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '' COMMENT '创建者',
`create_time` datetime NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
@ -33,7 +34,8 @@ CREATE TABLE `ai_balance_change_record` (
`type` tinyint(1) NULL DEFAULT NULL COMMENT '操作类型',
`change_amount` decimal(10, 2) NULL DEFAULT NULL COMMENT '变更金额',
`result_amount` decimal(10, 2) NULL DEFAULT NULL COMMENT '变更后金额',
PRIMARY KEY (`id`) USING BTREE
PRIMARY KEY (`id`) USING BTREE,
KEY `idx_order_no` (`order_no`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1159 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '余额使用记录' ROW_FORMAT = DYNAMIC;
-- ----------------------------
@ -1449,7 +1451,7 @@ CREATE TABLE `ai_manager` (
`create_by` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '' COMMENT '创建者',
`create_time` datetime NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_by` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '' COMMENT '更新者',
`update_time` datetime NULL DEFAULT CURRENT_TIMESTAMP COMMENT '更新时间',
`update_time` datetime NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`remark` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '备注',
`title` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT 'AI标题',
`price` decimal(10, 2) NULL DEFAULT 0.00 COMMENT '价格',
@ -1464,12 +1466,12 @@ CREATE TABLE `ai_manager` (
-- ----------------------------
-- Records of ai_manager
-- ----------------------------
INSERT INTO `ai_manager` VALUES (1, '0', 'admin', '2025-11-13 20:05:27', 'admin', '2025-12-23 14:30:07', NULL, '图生图1', 10.00, 0, '1girl, solo, exact same character as reference image, identical face eye color hairstyle accessories expression pose background lighting, (completely nude:1.2), naked, no clothes, breasts fully exposed, {胸部大小}, nipples perfectly matching skin tone, {动作}, pose that naturally conceals lower body from front view, legs positioned to avoid frontal genital exposure, side profile or back view emphasis, subtle natural body contours without explicit details, perfect anatomy, exactly two arms and two legs only, perfect hands with exactly 5 clearly separated fingers each no fusion no extra fingers, smooth natural skin texture, realistic proportions, masterpiece, best quality, ultra detailed, 8k, soft lighting, depth of field, high resolution, intricate details, cinematic composition, (if multiple characters: all characters following same rules no frontal exposure:1.1)(negative: clothes, bra, panties, underwear, bikini, swimsuit, any fabric even one pixel, censored, mosaic, bar censor, any censorship, pasties, nipple covers, frontal genital exposure, visible slit, visible pussy, exposed crotch, swollen labia, puffy labia, deformed genital area, dark mismatched crotch skin, any pubic details, extra arms, extra legs, extra hands, extra fingers, third arm, third leg, mutated limbs, more than two arms, more than two legs, fused fingers, deformed hands, bad hands, unnatural poses, violating human anatomy, loli, child, old, realistic photo, lowres:1.8, blurry, artifacts, overexposed, underexposed, pixelated, jpeg artifacts, watermark, text, signature, ugly, deformed, mutated, extra limbs, poorly drawn face, poorly drawn hands, missing limbs, floating limbs, disconnected limbs)', '11', '1,17');
INSERT INTO `ai_manager` VALUES (2, '0', 'admin', '2025-11-21 22:24:28', 'admin', '2025-12-14 20:20:56', NULL, '图生图2', 10.00, 0, '生成{主题},{风格}包含{细节},分辨率{技术参数}的图像', '12', NULL);
INSERT INTO `ai_manager` VALUES (3, '0', 'admin', '2025-11-13 20:06:02', 'admin', '2025-12-24 15:03:00', NULL, '一键换脸', 10.00, 1, '保持参考图1的内容风格不变用参考图2的脸部对参考图1的脸部进行替换', '13', '');
INSERT INTO `ai_manager` VALUES (4, '0', 'admin', '2025-11-13 20:07:33', 'admin', '2025-12-23 14:47:10', NULL, '快捷生图', 8.00, 0, '1girl, solo, detailed face with {发型} {眼睛颜色} {配饰} {表情} {姿势} in {背景}, {服装描述} {胸部大小} no pubic hair at all, no body hair anywhere from neck to toes, pose that naturally conceals lower body from front view, legs positioned to avoid frontal genital exposure, side profile or back view emphasis, subtle natural body contours without explicit details, perfect anatomy, exactly two arms and two legs only, perfect hands with exactly 5 clearly separated fingers each no fusion no extra fingers, smooth natural skin texture, realistic proportions, masterpiece, best quality, ultra detailed, 8k, soft lighting, depth of field, high resolution, intricate details, cinematic composition, (if multiple characters: {多人描述}, all characters following same rules no frontal exposure:1.1)(negative: everyday clothes, casual outfit, school uniform, regular dress, any non-specified clothing, clothing glitch, fabric clipping, pubic hair, body hair, happy trail, hair on abdomen, hair on stomach, hair on torso, hair around navel, any hair below neck except head hair, frontal genital exposure, visible slit, visible pussy, exposed crotch, swollen labia, puffy labia, deformed genital area, dark mismatched crotch skin, any pubic details, extra arms, extra legs, extra hands, extra fingers, third arm, third leg, mutated limbs, more than two arms, more than two legs, fused fingers, deformed hands, bad hands, unnatural poses, violating human anatomy, bad anatomy, loli, child, old, realistic photo, lowres:1.9, blurry, artifacts, overexposed, underexposed, pixelated, jpeg artifacts, watermark, text, signature, ugly, deformed, mutated, extra limbs, poorly drawn face, poorly drawn hands, missing limbs, floating limbs, disconnected limbs, clothes if nude mode, bra if nude mode, panties if nude mode, underwear if nude mode, bikini if nude mode, swimsuit if nude mode, any fabric even one pixel if nude mode, censored if nude mode, mosaic if nude mode, bar censor if nude mode, any censorship if nude mode, pasties if nude mode, nipple covers if nude mode)\n', '1', '8,5,11,64,66,68,70,72,74');
INSERT INTO `ai_manager` VALUES (5, '0', 'admin', '2025-11-13 20:07:47', 'admin', '2026-01-08 14:58:37', '', '快捷生视频', 35.00, 0, '跳舞', '21', '');
INSERT INTO `ai_manager` VALUES (7, '0', 'admin', '2025-11-25 19:41:23', 'admin', '2025-11-25 19:41:23', NULL, '视频换脸', 35.00, 1, NULL, '22', NULL);
INSERT INTO `ai_manager` VALUES (1, '0', 'admin', '2025-11-13 20:05:27', 'admin', '2025-12-23 14:30:07', NULL, '图生图', 10.00, 0, '1girl, solo, exact same character as reference image, (completely nude:1.2), naked, no clothes, breasts fully exposed, {胸部大小}, perfect anatomy, masterpiece, best quality', '11', NULL);
INSERT INTO `ai_manager` VALUES (2, '0', 'admin', '2025-11-21 22:24:28', 'admin', '2025-12-14 20:20:56', NULL, '图生图-高级', 12.00, 0, '生成{主题},{风格}包含{细节},分辨率{技术参数}的图像', '12', NULL);
INSERT INTO `ai_manager` VALUES (3, '0', 'admin', '2025-11-13 20:06:02', 'admin', '2025-12-24 15:03:00', NULL, '一键换脸', 10.00, 1, '保持参考图1的内容风格不变用参考图2的脸部对参考图1的脸部进行替换', '13', NULL);
INSERT INTO `ai_manager` VALUES (4, '0', 'admin', '2025-11-13 20:07:33', 'admin', '2025-12-23 14:47:10', NULL, '快捷生图', 8.00, 0, '1girl, solo, detailed face with {发型} {眼睛颜色} {配饰} {表情} {姿势} in {背景}, {服装描述} {胸部大小}, perfect anatomy, masterpiece, best quality', '11', NULL);
INSERT INTO `ai_manager` VALUES (5, '0', 'admin', '2025-11-13 20:07:47', 'admin', '2026-01-08 14:58:37', NULL, '快捷生视频', 35.00, 0, '跳舞', '21', NULL);
INSERT INTO `ai_manager` VALUES (7, '0', 'admin', '2025-11-25 19:41:23', 'admin', '2025-11-25 19:41:23', NULL, '视频换脸', 35.00, 1, '视频换脸功能', '22', NULL);
-- ----------------------------
-- Table structure for ai_order
@ -1494,7 +1496,13 @@ CREATE TABLE `ai_order` (
`is_top` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT 'N' COMMENT '是否置顶N-否 Y-是',
`img1` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '首帧图片',
`img2` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '第二张图片',
PRIMARY KEY (`id`) USING BTREE
`mode` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '生成模式text-to-video 或 image-to-video',
`duration` int NULL DEFAULT 5 COMMENT '视频时长(秒)',
`resolution` varchar(16) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '720p' COMMENT '分辨率',
`ratio` varchar(16) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '9:16' COMMENT '宽高比',
PRIMARY KEY (`id`) USING BTREE,
KEY `idx_order_num` (`order_num`) USING BTREE,
KEY `idx_user_id` (`user_id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1310 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'API订单记录' ROW_FORMAT = DYNAMIC;
-- ----------------------------
@ -2792,6 +2800,7 @@ CREATE TABLE `ai_pay_setting` (
DROP TABLE IF EXISTS `ai_rebate_record`;
CREATE TABLE `ai_rebate_record` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`order_no` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '关联订单号',
`del_flag` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '0' COMMENT '删除标志0代表存在 2代表删除',
`create_by` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '' COMMENT '创建者',
`create_time` datetime NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
@ -2801,7 +2810,8 @@ CREATE TABLE `ai_rebate_record` (
`superior_id` bigint NULL DEFAULT NULL COMMENT '上级ID',
`subordinate_id` bigint NULL DEFAULT NULL COMMENT '下级ID',
`amount` decimal(10, 2) NULL DEFAULT NULL COMMENT '返佣金额',
PRIMARY KEY (`id`) USING BTREE
PRIMARY KEY (`id`) USING BTREE,
KEY `idx_order_no` (`order_no`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 37 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '返佣记录' ROW_FORMAT = DYNAMIC;
-- ----------------------------
@ -3103,6 +3113,7 @@ INSERT INTO `ai_sample_amount` VALUES (2, '0', 'admin', '2025-11-14 22:37:01', '
DROP TABLE IF EXISTS `ai_sample_amount_record`;
CREATE TABLE `ai_sample_amount_record` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`order_no` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '关联订单号',
`del_flag` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '0' COMMENT '删除标志0代表存在 2代表删除',
`create_by` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '' COMMENT '创建者',
`create_time` datetime NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
@ -3113,7 +3124,8 @@ CREATE TABLE `ai_sample_amount_record` (
`sample_amount` decimal(10, 2) NULL DEFAULT NULL COMMENT '体验金额',
`recycle_time` datetime NULL DEFAULT NULL COMMENT '回收时间',
`status` tinyint NULL DEFAULT 0 COMMENT '回收状态0-已发放 1-已回收',
PRIMARY KEY (`id`) USING BTREE
PRIMARY KEY (`id`) USING BTREE,
KEY `idx_order_no` (`order_no`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 34 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '体验金领取记录' ROW_FORMAT = DYNAMIC;
-- ----------------------------
@ -5813,4 +5825,36 @@ INSERT INTO `user_message` VALUES (2, 2);
INSERT INTO `user_message` VALUES (2, 3);
INSERT INTO `user_message` VALUES (2, 4);
-- ----------------------------
-- Table structure for ai_template
-- ----------------------------
DROP TABLE IF EXISTS `ai_template`;
CREATE TABLE `ai_template` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '自增ID',
`name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '模版名称',
`chinese_content` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL COMMENT '模版中文内容',
`english_content` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL COMMENT '模版英文内容',
`image_url` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '模版图片URL',
`ai_id` bigint NULL DEFAULT NULL COMMENT '关联AI类型ID',
`status` tinyint(1) NULL DEFAULT 1 COMMENT '状态0禁用 1启用',
`remark` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '备注',
`create_by` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '' COMMENT '创建者',
`create_time` datetime NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_by` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '' COMMENT '更新者',
`update_time` datetime NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`del_flag` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '0' COMMENT '删除标志0代表存在 1代表删除',
PRIMARY KEY (`id`) USING BTREE,
KEY `idx_ai_id` (`ai_id`) USING BTREE,
KEY `idx_status` (`status`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1001 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'AI模板表' ROW_FORMAT = DYNAMIC;
-- ----------------------------
-- Records of ai_template
-- ----------------------------
INSERT INTO `ai_template` (`name`, `chinese_content`, `english_content`, `image_url`, `ai_id`, `status`, `remark`) VALUES
('默认写真模板', '一个穿着衣服的年轻女性,微笑面对镜头,高清写真风格', 'A young woman wearing clothes, smiling at the camera, high-definition portrait style', 'https://seedance-1331490964.cos.ap-guangzhou.myqcloud.com/ai/default-template.jpg', 11, 1, '默认写真模板'),
('艺术裸体模板', '一个优雅的艺术裸体女性,柔和光线,专业摄影风格', 'An elegant artistic nude female with soft lighting, professional photography style', 'https://seedance-1331490964.cos.ap-guangzhou.myqcloud.com/ai/nude-art.jpg', 11, 1, '艺术裸体模板'),
('时尚都市模板', '时尚都市年轻女性写真,现代潮流风格', 'Fashionable urban young woman portrait, modern trendy style', 'https://seedance-1331490964.cos.ap-guangzhou.myqcloud.com/ai/fashion.jpg', 21, 1, '时尚写真模板'),
('性感写真模板', '性感迷人女性写真,专业灯光和构图', 'Sexy and charming female portrait with professional lighting and composition', 'https://seedance-1331490964.cos.ap-guangzhou.myqcloud.com/ai/sexy.jpg', 11, 1, '性感写真模板');
SET FOREIGN_KEY_CHECKS = 1;