Compare commits

..

No commits in common. "8faff7562d859a5e2b26f06677c895fce615fbbf" and "f075e6b2b124ca52943cf0642783928fa930c3d8" have entirely different histories.

6 changed files with 109 additions and 282 deletions

View File

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

View File

@ -228,21 +228,12 @@ byteapi:
apiKey:
callBackUrl:
# 火山引擎 Ark API (Seedance 2.0)
volcengine:
region: cn-beijing
# 素材库等 ark 控制面 OpenAPI与 byteapi 数据面推理地址无关)
# URL 请加引号;代码会去掉 https:// 再交给 SDKSDK 自己会拼 https://,避免双 scheme
openApiEndpoint: "https://ark.cn-beijing.volces.com"
ark:
baseUrl: "https://ark.cn-beijing.volces.com"
openApiVersion: "2024-01-01"
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/
baseUrl:
apiKey:
callbackUrl:
# 门户视频生成页:模型 / 比例 / 时长 / 分辨率均由此处维护,前后端不写死业务枚举
portal:

View File

@ -1,269 +1,104 @@
package com.ruoyi.ai.service.impl;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.databind.*;
import com.ruoyi.ai.mapper.AiUserMapper;
import com.ruoyi.common.core.domain.entity.AiUser;
import com.ruoyi.common.core.domain.entity.SysDept;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.MapperFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.PropertyNamingStrategies;
import com.ruoyi.ai.service.IByteDeptApiKeyService;
import com.ruoyi.common.utils.SecurityUtils;
import com.ruoyi.common.utils.http.OkHttpUtils;
import com.ruoyi.system.mapper.SysDeptMapper;
import lombok.extern.slf4j.Slf4j;
import okhttp3.*;
import okhttp3.MediaType;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
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.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
public class BaseByteApiService {
@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()
protected final ObjectMapper objectMapper = new ObjectMapper()
.setSerializationInclusion(JsonInclude.Include.NON_NULL)
.setPropertyNamingStrategy(PropertyNamingStrategies.UPPER_CAMEL_CASE)
.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
*/
protected String getUserProject() {
Long userId = SecurityUtils.getAiUserId();
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();
// TODO
return "default";
}
/**
* 找到当前部门所属第二部门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;
}
protected String resolveCurrentAiUserApiKey() {
return byteDeptApiKeyService.resolveVolcApiKey(SecurityUtils.getAiUserId());
}
}

View File

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

View File

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

View File

@ -60,7 +60,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
</select>
<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, d.project,
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 dept_name from sys_dept where dept_id = d.parent_id) parent_name
from sys_dept d
where d.dept_id = #{deptId}