Compare commits

..

2 Commits

6 changed files with 282 additions and 109 deletions

View File

@ -9,7 +9,6 @@ spring:
username: root username: root
password: mkMReisAKl6I7rVqEY90 password: mkMReisAKl6I7rVqEY90
driverClassName: com.mysql.cj.jdbc.Driver driverClassName: com.mysql.cj.jdbc.Driver
# 初始连接数 # 初始连接数
initialSize: 5 initialSize: 5
# 最小连接池数量 # 最小连接池数量
@ -52,6 +51,4 @@ spring:
merge-sql: true merge-sql: true
wall: wall:
config: config:
multi-statement-allow: true multi-statement-allow: true

View File

@ -224,16 +224,25 @@ tencentCos:
# domain: https://images.iqyjsnwv.com/ # domain: https://images.iqyjsnwv.com/
byteapi: byteapi:
url: url:
apiKey: apiKey:
callBackUrl: callBackUrl:
# 火山引擎 Ark API (Seedance 2.0)
volcengine: volcengine:
region: cn-beijing
# 素材库等 ark 控制面 OpenAPI与 byteapi 数据面推理地址无关)
# URL 请加引号;代码会去掉 https:// 再交给 SDKSDK 自己会拼 https://,避免双 scheme
openApiEndpoint: "https://ark.cn-beijing.volces.com"
ark: ark:
baseUrl: baseUrl: "https://ark.cn-beijing.volces.com"
apiKey: openApiVersion: "2024-01-01"
callbackUrl: apiKey: 3e33e034-7e25-4228-8864-b51b2a7a8f97
callbackUrl: http://47.86.170.114:8011/api/ai/volcCallback
ak: AKLTNmYyN2VhZTcyMDcxNDNlNzg3OGVlMDVmZjRhNWQwY2M
sk: Tm1ZeU1UTmlORFk1WmpKa05HUmpaRGcxTWpjMFpqUmpOVE01TUdJME5URQ==
url: https://ark.ap-southeast.bytepluses.com/api/v3
apiKey: 3e33e034-7e25-4228-8864-b51b2a7a8f97
callBackUrl: http://47.86.170.114:5173/
# 门户视频生成页:模型 / 比例 / 时长 / 分辨率均由此处维护,前后端不写死业务枚举 # 门户视频生成页:模型 / 比例 / 时长 / 分辨率均由此处维护,前后端不写死业务枚举
portal: portal:

View File

@ -1,104 +1,269 @@
package com.ruoyi.ai.service.impl; package com.ruoyi.ai.service.impl;
import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.*;
import com.fasterxml.jackson.databind.JsonNode; import com.ruoyi.ai.mapper.AiUserMapper;
import com.fasterxml.jackson.databind.MapperFeature; import com.ruoyi.common.core.domain.entity.AiUser;
import com.fasterxml.jackson.databind.ObjectMapper; import com.ruoyi.common.core.domain.entity.SysDept;
import com.fasterxml.jackson.databind.PropertyNamingStrategies;
import com.ruoyi.ai.service.IByteDeptApiKeyService;
import com.ruoyi.common.utils.SecurityUtils; import com.ruoyi.common.utils.SecurityUtils;
import com.ruoyi.common.utils.http.OkHttpUtils; import com.ruoyi.common.utils.http.OkHttpUtils;
import okhttp3.MediaType; import com.ruoyi.system.mapper.SysDeptMapper;
import okhttp3.Request; import lombok.extern.slf4j.Slf4j;
import okhttp3.RequestBody; import okhttp3.*;
import okhttp3.Response;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.io.IOException; import java.io.IOException;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
import java.security.MessageDigest;
import java.time.ZoneOffset;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
@Slf4j
@Service @Service
public class BaseByteApiService { public class BaseByteApiService {
protected final ObjectMapper objectMapper = new ObjectMapper() @Resource
private AiUserMapper userMapper;
@Resource
private SysDeptMapper deptMapper;
protected String DEPT_ANCESTORS_SPLIT = ",";
@Value("${volcengine.ak}")
protected String accessKeyId;
@Value("${volcengine.sk}")
protected String secretAccessKey;
@Value("${volcengine.region}")
protected String region;
private static final String CONTENT_TYPE_JSON = "application/json; charset=utf-8";
private static final MediaType JSON_MEDIA = MediaType.parse(CONTENT_TYPE_JSON);
private static final String SERVICE_ARK = "ark";
private static final String VERSION = "2024-01-01";
private static final String METHOD_POST = "POST";
private static final String PATH_ROOT = "/";
private static final String SIGNED_HEADERS = "content-type;host;x-content-sha256;x-date";
private OkHttpClient httpClient = OkHttpUtils.createOkHttpClient();
private String controlApiHost;
@PostConstruct
public void init() {
this.controlApiHost = "ark." + this.region + ".volcengineapi.com";
this.httpClient = OkHttpUtils.createOkHttpClient();
}
private final ObjectMapper jsonMapper = new ObjectMapper()
.setSerializationInclusion(JsonInclude.Include.NON_NULL) .setSerializationInclusion(JsonInclude.Include.NON_NULL)
.setPropertyNamingStrategy(PropertyNamingStrategies.UPPER_CAMEL_CASE) .setPropertyNamingStrategy(PropertyNamingStrategies.UPPER_CAMEL_CASE)
.configure(MapperFeature.ACCEPT_CASE_INSENSITIVE_PROPERTIES, true) .configure(MapperFeature.ACCEPT_CASE_INSENSITIVE_PROPERTIES, true)
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
.configure(DeserializationFeature.ACCEPT_EMPTY_STRING_AS_NULL_OBJECT, true);
// API地址可配置在配置文件中
@Value("${byteapi.url}")
protected String API_URL;
@Autowired
protected IByteDeptApiKeyService byteDeptApiKeyService;
/**
* POST JSON 调用方舟 OpenAPI若响应含 {@code Result} 节点则解析为业务对象 api.docx 返回示例一致
*/
protected <T> T httpExecute(String path, Object request, Class<T> clz) throws IOException {
String jsonBody = objectMapper.writeValueAsString(request);
RequestBody body = RequestBody.create(
MediaType.parse("application/json; charset=utf-8"),
jsonBody);
Request httpRequest = new Request.Builder()
.url(API_URL + path)
.header("Content-Type", "application/json")
.header("Authorization", "Bearer " + resolveCurrentAiUserApiKey())
.post(body)
.build();
try (Response response = OkHttpUtils.newCall(httpRequest).execute()) {
if (!response.isSuccessful()) {
String errorMsg = response.body() != null ? response.body().string() : "execute error";
throw new RuntimeException("execute error" + errorMsg);
}
if (response.body() == null) {
throw new RuntimeException("response body null");
}
String responseBody = response.body().string();
JsonNode root = objectMapper.readTree(responseBody);
JsonNode result = root.get("Result");
if (result != null && !result.isNull()) {
return objectMapper.treeToValue(result, clz);
}
return objectMapper.readValue(responseBody, clz);
}
}
/**
* 无业务体返回的 OpenAPI 调用 DeleteAsset
*/
protected void httpExecuteNoContent(String path, Object request) throws IOException {
String jsonBody = objectMapper.writeValueAsString(request);
RequestBody body = RequestBody.create(
MediaType.parse("application/json; charset=utf-8"),
jsonBody);
Request httpRequest = new Request.Builder()
.url(API_URL + path)
.header("Content-Type", "application/json")
.header("Authorization", "Bearer " + resolveCurrentAiUserApiKey())
.post(body)
.build();
try (Response response = OkHttpUtils.newCall(httpRequest).execute()) {
if (!response.isSuccessful()) {
String errorMsg = response.body() != null ? response.body().string() : "execute error";
throw new RuntimeException("execute error" + errorMsg);
}
}
}
/** /**
* 根据用户找到对应的project * 根据用户找到对应的project
*/ */
protected String getUserProject() { protected String getUserProject() {
// TODO Long userId = SecurityUtils.getAiUserId();
return "default"; if (userId == null) {
return null;
}
AiUser user = userMapper.selectAiUserById(userId);
if (user == null) {
return null;
}
// 第二层部门IDAPI_KEY放在这里
Long secondLvDeptId = getSecondLevelDept(user.getDeptId());
if (secondLvDeptId == null) {
return null;
}
SysDept secondDept = deptMapper.selectDeptById(secondLvDeptId);
return secondDept.getProject();
} }
protected String resolveCurrentAiUserApiKey() { /**
return byteDeptApiKeyService.resolveVolcApiKey(SecurityUtils.getAiUserId()); * 找到当前部门所属第二部门ID
*
* @param deptId 当前部门
*/
protected Long getSecondLevelDept(long deptId) {
SysDept dept = deptMapper.selectDeptById(deptId);
String ancestors = dept.getAncestors();
// 判断是第几层
if (ancestors == null || ancestors.isEmpty() || "0".equals(ancestors)) {
// 第一层
return null;
}
String[] parentDeptArray = ancestors.split(DEPT_ANCESTORS_SPLIT);
int length = parentDeptArray.length;
if (length == 2) {
// 只有一个上级所以当前节点是第二层直接返回
return deptId;
}
// 大于二级
return Long.parseLong(parentDeptArray[2]);
}
public <R> R callApi(String action, Object request, Class<R> responseClass) throws IOException {
String body = jsonMapper.writeValueAsString(request);
SignedRequest signed = sign(action, body);
RequestBody requestBody = RequestBody.create(JSON_MEDIA, body.getBytes(StandardCharsets.UTF_8));
Request okRequest = new Request.Builder()
.url(signed.url)
.header("X-Content-Sha256", signed.xContentSha256)
.header("X-Date", signed.xDate)
.header("Content-Type", CONTENT_TYPE_JSON)
.header("Authorization", signed.authorization)
.post(requestBody)
.build();
try (Response response = httpClient.newCall(okRequest).execute()) {
if (responseClass == null) {
return null;
}
String respBody = response.body() != null ? response.body().string() : "";
if (!response.isSuccessful()) {
throw new IOException("HTTP " + response.code() + ": " + respBody);
}
return parseResult(respBody, responseClass);
}
}
private <T> T parseResult(String body, Class<T> resultType) throws IOException {
JsonNode root = jsonMapper.readTree(body);
JsonNode result = root.get("Result");
if (result != null && !result.isNull()) {
return jsonMapper.treeToValue(result, resultType);
}
return jsonMapper.readValue(body, resultType);
}
private SignedRequest sign(String action, String body) throws IOException {
ZonedDateTime nowUtc = ZonedDateTime.now(ZoneOffset.UTC);
String xDate = nowUtc.format(DateTimeFormatter.ofPattern("yyyyMMdd'T'HHmmss'Z'"));
String shortDate = xDate.substring(0, 8);
String xContentSha256 = sha256Hex(body == null ? "" : body);
Map<String, Object> query = new TreeMap<>();
query.put("Action", action);
query.put("Version", VERSION);
String canonicalQuery = canonicalQueryString(query);
String canonicalHeaders = String.join("\n",
"content-type:" + CONTENT_TYPE_JSON,
"host:" + controlApiHost,
"x-content-sha256:" + xContentSha256,
"x-date:" + xDate);
String canonicalRequest = String.join("\n",
METHOD_POST.toUpperCase(),
PATH_ROOT,
canonicalQuery,
canonicalHeaders,
"",
SIGNED_HEADERS,
xContentSha256);
String hashedCanonical = sha256Hex(canonicalRequest);
String credentialScope = shortDate + "/" + region + "/" + SERVICE_ARK + "/request";
String stringToSign = String.join("\n", "HMAC-SHA256", xDate, credentialScope, hashedCanonical);
byte[] kDate = hmac256(secretAccessKey.getBytes(StandardCharsets.UTF_8), shortDate);
byte[] kRegion = hmac256(kDate, region);
byte[] kService = hmac256(kRegion, SERVICE_ARK);
byte[] kSigning = hmac256(kService, "request");
String signature = toHex(hmac256(kSigning, stringToSign));
String authorization = "HMAC-SHA256 Credential="
+ accessKeyId + "/" + credentialScope
+ ", SignedHeaders=" + SIGNED_HEADERS
+ ", Signature=" + signature;
HttpUrl url = HttpUrl.parse("https://" + controlApiHost + "/?" + canonicalQuery);
if (url == null) {
throw new IOException("invalid control API URL");
}
return new SignedRequest(url, xContentSha256, xDate, authorization);
}
private static String esc(String s) {
if (s == null) {
return "";
}
return s.replace("\\", "\\\\").replace("\"", "\\\"");
}
private static String canonicalQueryString(Map<String, Object> params) throws IOException {
StringBuilder sb = new StringBuilder();
for (String key : new TreeMap<>(params).keySet()) {
Object val = params.get(key);
if (val instanceof List) {
@SuppressWarnings("unchecked")
List<Object> list = (List<Object>) val;
for (Object item : list) {
sb.append(encode(key)).append("=").append(encode(String.valueOf(item))).append("&");
}
} else {
sb.append(encode(key)).append("=").append(encode(String.valueOf(val))).append("&");
}
}
if (sb.length() == 0) {
return "";
}
return sb.substring(0, sb.length() - 1).replace("+", "%20");
}
private static String encode(String raw) throws IOException {
return URLEncoder.encode(raw, StandardCharsets.UTF_8.name()).replace("+", "%20");
}
private static String sha256Hex(String content) throws IOException {
try {
MessageDigest md = MessageDigest.getInstance("SHA-256");
return toHex(md.digest(content.getBytes(StandardCharsets.UTF_8)));
} catch (Exception e) {
throw new IOException(e);
}
}
private static byte[] hmac256(byte[] key, String data) throws IOException {
try {
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(key, "HmacSHA256"));
return mac.doFinal(data.getBytes(StandardCharsets.UTF_8));
} catch (Exception e) {
throw new IOException(e);
}
}
private static String toHex(byte[] bytes) {
StringBuilder sb = new StringBuilder(bytes.length * 2);
for (byte b : bytes) {
sb.append(String.format("%02x", b));
}
return sb.toString();
}
private static final class SignedRequest {
final HttpUrl url;
final String xContentSha256;
final String xDate;
final String authorization;
SignedRequest(HttpUrl url, String xContentSha256, String xDate, String authorization) {
this.url = url;
this.xContentSha256 = xContentSha256;
this.xDate = xDate;
this.authorization = authorization;
}
} }
} }

View File

@ -11,20 +11,21 @@ import com.ruoyi.common.core.response.asset.ListAssetsGroupResponse;
import com.ruoyi.common.core.response.asset.UpdateAssetGroupResponse; import com.ruoyi.common.core.response.asset.UpdateAssetGroupResponse;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.io.IOException; import java.io.IOException;
@Service @Service
public class ByteAssetGroupService extends BaseByteApiService implements IByteAssetGroupService { public class ByteAssetGroupService extends BaseByteApiService implements IByteAssetGroupService {
private static final String LIST_ASSET_GROUPS_URL = "/open/ListAssetGroups"; private static final String ACTION_LIST_ASSET_GROUPS = "ListAssetGroups";
private static final String CREATE_ASSET_GROUP_URL = "/open/CreateAssetGroup"; private static final String ACTION_CREATE_ASSET_GROUP = "CreateAssetGroup";
private static final String GET_ASSET_GROUP_URL = "/open/GetAssetGroup"; private static final String ACTION_GET_ASSET_GROUP = "GetAssetGroup";
private static final String UPDATE_ASSET_GROUP_URL = "/open/UpdateAssetGroup"; private static final String ACTION_UPDATE_ASSET_GROUP = "UpdateAssetGroup";
@Override @Override
public ListAssetsGroupResponse listAssetGroups(ListAssetsGroupRequest request) throws IOException { public ListAssetsGroupResponse listAssetGroups(ListAssetsGroupRequest request) throws IOException {
request.setProjectName(getUserProject()); request.setProjectName(getUserProject());
return httpExecute(LIST_ASSET_GROUPS_URL, request, ListAssetsGroupResponse.class); return callApi(ACTION_LIST_ASSET_GROUPS, request, ListAssetsGroupResponse.class);
} }
@Override @Override
@ -32,18 +33,18 @@ public class ByteAssetGroupService extends BaseByteApiService implements IByteAs
// 固定值 // 固定值
request.setGroupType("AIGC"); request.setGroupType("AIGC");
request.setProjectName(getUserProject()); request.setProjectName(getUserProject());
return httpExecute(CREATE_ASSET_GROUP_URL, request, CreateAssetGroupResponse.class); return callApi(ACTION_CREATE_ASSET_GROUP, request, CreateAssetGroupResponse.class);
} }
@Override @Override
public GetAssetGroupResponse getAssetGroup(GetAssetGroupRequest request) throws IOException { public GetAssetGroupResponse getAssetGroup(GetAssetGroupRequest request) throws IOException {
request.setProjectName(getUserProject()); request.setProjectName(getUserProject());
return httpExecute(GET_ASSET_GROUP_URL, request, GetAssetGroupResponse.class); return callApi(ACTION_GET_ASSET_GROUP, request, GetAssetGroupResponse.class);
} }
@Override @Override
public UpdateAssetGroupResponse updateAssetGroup(UpdateAssetGroupRequest request) throws IOException { public UpdateAssetGroupResponse updateAssetGroup(UpdateAssetGroupRequest request) throws IOException {
request.setProjectName(getUserProject()); request.setProjectName(getUserProject());
return httpExecute(UPDATE_ASSET_GROUP_URL, request, UpdateAssetGroupResponse.class); return callApi(ACTION_UPDATE_ASSET_GROUP, request, UpdateAssetGroupResponse.class);
} }
} }

View File

@ -16,6 +16,7 @@ import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartFile;
import javax.annotation.Resource;
import java.io.IOException; import java.io.IOException;
@Service @Service
@ -24,11 +25,11 @@ public class ByteAssetService extends BaseByteApiService implements IByteAssetSe
private final TencentCosUtil tencentCosUtil; private final TencentCosUtil tencentCosUtil;
private static final String CREATE_ASSET_URL = "/open/CreateAsset"; private static final String ACTION_CREATE_ASSET = "CreateAsset";
private static final String LIST_ASSETS_URL = "/open/ListAssets"; private static final String ACTION_LIST_ASSETS = "ListAssets";
private static final String GET_ASSET_URL = "/open/GetAsset"; private static final String ACTION_GET_ASSET = "GetAsset";
private static final String UPDATE_ASSET_URL = "/open/UpdateAsset"; private static final String ACTION_UPDATE_ASSET = "UpdateAsset";
private static final String DELETE_ASSET_URL = "/open/DeleteAsset"; private static final String ACTION_DELETE_ASSET = "DeleteAsset";
@Override @Override
public CreateAssetResponse createAsset(MultipartFile file, String groupId, String assetType, String name, String projectName) throws Exception { public CreateAssetResponse createAsset(MultipartFile file, String groupId, String assetType, String name, String projectName) throws Exception {
@ -45,30 +46,30 @@ public class ByteAssetService extends BaseByteApiService implements IByteAssetSe
request.setName(name); request.setName(name);
request.setAssetType(assetType); request.setAssetType(assetType);
request.setProjectName(getUserProject()); request.setProjectName(getUserProject());
return httpExecute(CREATE_ASSET_URL, request, CreateAssetResponse.class); return callApi(ACTION_CREATE_ASSET, request, CreateAssetResponse.class);
} }
@Override @Override
public ListAssetsResponse listAssets(ListAssetsRequest request) throws IOException { public ListAssetsResponse listAssets(ListAssetsRequest request) throws IOException {
request.setProjectName(getUserProject()); request.setProjectName(getUserProject());
return httpExecute(LIST_ASSETS_URL, request, ListAssetsResponse.class); return callApi(ACTION_LIST_ASSETS, request, ListAssetsResponse.class);
} }
@Override @Override
public GetAssetResponse getAsset(GetAssetRequest request) throws IOException { public GetAssetResponse getAsset(GetAssetRequest request) throws IOException {
request.setProjectName(getUserProject()); request.setProjectName(getUserProject());
return httpExecute(GET_ASSET_URL, request, GetAssetResponse.class); return callApi(ACTION_GET_ASSET, request, GetAssetResponse.class);
} }
@Override @Override
public UpdateAssetResponse updateAsset(UpdateAssetRequest request) throws IOException { public UpdateAssetResponse updateAsset(UpdateAssetRequest request) throws IOException {
request.setProjectName(getUserProject()); request.setProjectName(getUserProject());
return httpExecute(UPDATE_ASSET_URL, request, UpdateAssetResponse.class); return callApi(ACTION_UPDATE_ASSET, request, UpdateAssetResponse.class);
} }
@Override @Override
public void deleteAsset(DeleteAssetRequest request) throws IOException { public void deleteAsset(DeleteAssetRequest request) throws IOException {
request.setProjectName(getUserProject()); request.setProjectName(getUserProject());
httpExecuteNoContent(DELETE_ASSET_URL, request); callApi(ACTION_DELETE_ASSET, request, null);
} }
} }

View File

@ -60,7 +60,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
</select> </select>
<select id="selectDeptById" parameterType="Long" resultMap="SysDeptResult"> <select id="selectDeptById" parameterType="Long" resultMap="SysDeptResult">
select d.dept_id, d.parent_id, d.ancestors, d.dept_name, d.order_num, d.leader, d.phone, d.email, d.byte_api_key, d.status, select d.dept_id, d.parent_id, d.ancestors, d.dept_name, d.order_num, d.leader, d.phone, d.email, d.byte_api_key, d.status, d.project,
(select dept_name from sys_dept where dept_id = d.parent_id) parent_name (select dept_name from sys_dept where dept_id = d.parent_id) parent_name
from sys_dept d from sys_dept d
where d.dept_id = #{deptId} where d.dept_id = #{deptId}