diff --git a/web-api/ruoyi-admin/src/main/java/com/ruoyi/api/ByteApiController.java b/web-api/ruoyi-admin/src/main/java/com/ruoyi/api/ByteApiController.java index 3885ab7..9274a82 100644 --- a/web-api/ruoyi-admin/src/main/java/com/ruoyi/api/ByteApiController.java +++ b/web-api/ruoyi-admin/src/main/java/com/ruoyi/api/ByteApiController.java @@ -4,6 +4,7 @@ import com.ruoyi.ai.domain.*; import com.ruoyi.ai.service.IAiManagerService; import com.ruoyi.ai.service.IAiOrderService; import com.ruoyi.ai.service.IAiTagService; +import com.ruoyi.ai.service.IByteDeptApiKeyService; import com.ruoyi.ai.service.IByteService; import com.ruoyi.api.request.ByteApiRequest; import com.ruoyi.common.annotation.Anonymous; @@ -40,12 +41,10 @@ public class ByteApiController extends BaseController { private final IAiOrderService aiOrderService; private final IAiManagerService managerService; private final IAiTagService aiTagService; + private final IByteDeptApiKeyService byteDeptApiKeyService; @Value("${byteapi.callBackUrl}") private String url; - // 火山引擎配置 - @Value("${volcengine.ark.apiKey}") - private String volcApiKey; @Value("${volcengine.ark.baseUrl}") private String volcBaseUrl; @Value("${volcengine.ark.callbackUrl}") @@ -105,7 +104,8 @@ public class ByteApiController extends BaseController { byteBodyReq.setSize("2K"); byteBodyReq.setStream(false); byteBodyReq.setWatermark(false); - ByteBodyRes byteBodyRes = byteService.promptToImg(byteBodyReq); + String arkApiKey = byteDeptApiKeyService.resolveVolcApiKey(SecurityUtils.getAiUserId()); + ByteBodyRes byteBodyRes = byteService.promptToImg(byteBodyReq, arkApiKey); List data = byteBodyRes.getData(); ByteDataRes byteDataRes = data.get(0); String url = byteDataRes.getUrl(); @@ -184,7 +184,8 @@ public class ByteApiController extends BaseController { byteBodyReq.setSize("2K"); byteBodyReq.setStream(false); byteBodyReq.setWatermark(false); - ByteBodyRes byteBodyRes = byteService.promptToImg(byteBodyReq); + String arkApiKey = byteDeptApiKeyService.resolveVolcApiKey(SecurityUtils.getAiUserId()); + ByteBodyRes byteBodyRes = byteService.promptToImg(byteBodyReq, arkApiKey); List data = byteBodyRes.getData(); ByteDataRes byteDataRes = data.get(0); String url = byteDataRes.getUrl(); @@ -295,7 +296,8 @@ public class ByteApiController extends BaseController { byteBodyReq.setResolution("720p"); byteBodyReq.setRatio("3:4"); - ByteBodyRes byteBodyRes = byteService.imgToVideo(byteBodyReq); + String arkApiKey = byteDeptApiKeyService.resolveVolcApiKey(SecurityUtils.getAiUserId()); + ByteBodyRes byteBodyRes = byteService.imgToVideo(byteBodyReq, arkApiKey); String id = byteBodyRes.getId(); if (id == null) { aiOrderService.orderFailure(aiOrder); @@ -313,7 +315,8 @@ public class ByteApiController extends BaseController { @GetMapping(value = "/{id}") @ApiOperation("视频下载") public AjaxResult getInfo(@PathVariable("id") String id) throws Exception { - ByteBodyRes byteBodyRes = byteService.uploadVideo(id); + String arkApiKey = byteDeptApiKeyService.resolveVolcApiKey(SecurityUtils.getAiUserId()); + ByteBodyRes byteBodyRes = byteService.uploadVideo(id, arkApiKey); if ("succeeded".equals(byteBodyRes.getStatus())) { content content = byteBodyRes.getContent(); String videoUrl = content.getVideo_url(); @@ -356,7 +359,8 @@ public class ByteApiController extends BaseController { @PostMapping(value = "/{id}/cancel") @ApiOperation("取消视频生成任务") public AjaxResult cancelTask(@PathVariable("id") String id) throws Exception { - return byteService.cancelVideoTask(id); + String arkApiKey = byteDeptApiKeyService.resolveVolcApiKey(SecurityUtils.getAiUserId()); + return byteService.cancelVideoTask(id, arkApiKey); } } diff --git a/web-api/ruoyi-admin/src/main/java/com/ruoyi/api/PortalVideoController.java b/web-api/ruoyi-admin/src/main/java/com/ruoyi/api/PortalVideoController.java index 7795619..994bb34 100644 --- a/web-api/ruoyi-admin/src/main/java/com/ruoyi/api/PortalVideoController.java +++ b/web-api/ruoyi-admin/src/main/java/com/ruoyi/api/PortalVideoController.java @@ -25,6 +25,7 @@ import com.ruoyi.common.utils.TencentCosUtil; import com.ruoyi.config.PortalVideoProperties; import io.swagger.annotations.Api; import io.swagger.annotations.ApiOperation; +import lombok.extern.slf4j.Slf4j; import lombok.RequiredArgsConstructor; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; @@ -44,6 +45,7 @@ import java.util.stream.Collectors; @RestController @RequestMapping("/api/portal/video") @RequiredArgsConstructor(onConstructor_ = @Autowired) +@Slf4j public class PortalVideoController extends BaseController { private final IByteService byteService; @@ -61,6 +63,13 @@ public class PortalVideoController extends BaseController { return byteDeptApiKeyService.resolveVolcApiKey(SecurityUtils.getAiUserId()); } + private static String maskKey(String k) { + if (StringUtils.isEmpty(k)) return ""; + String t = k.trim(); + if (t.length() <= 8) return "***"; + return t.substring(0, 4) + "***" + t.substring(t.length() - 4); + } + /** 与 ai_manager.type、portal.video.function-type 对齐,用于扣费 */ private String resolveFunctionType(PortalVideoGenRequest req) { if (StringUtils.isNotEmpty(req.getFunctionType())) { @@ -201,6 +210,18 @@ public class PortalVideoController extends BaseController { aiOrderService.orderSuccess(aiOrder); return AjaxResult.success(byteBodyRes); } catch (Exception e) { + try { + log.error( + "portal video submit failed: endpoint=/api/portal/video/{}, mode={}, req={}, byteBodyReq={}, resolvedKeyMasked={}", + mode, + mode, + OM.writeValueAsString(req), + OM.writeValueAsString(byteBodyReq), + maskKey(apiKey()) + ); + } catch (Exception ignored) { + log.error("portal video submit failed: endpoint=/api/portal/video/{}, mode={}", mode, mode); + } aiOrderService.orderFailure(aiOrder); throw new RuntimeException(e); } diff --git a/web-api/ruoyi-system/src/main/java/com/ruoyi/ai/service/IByteService.java b/web-api/ruoyi-system/src/main/java/com/ruoyi/ai/service/IByteService.java index ef42e36..251ff27 100644 --- a/web-api/ruoyi-system/src/main/java/com/ruoyi/ai/service/IByteService.java +++ b/web-api/ruoyi-system/src/main/java/com/ruoyi/ai/service/IByteService.java @@ -10,11 +10,13 @@ public interface IByteService { * 文生图 */ ByteBodyRes promptToImg(ByteBodyReq req) throws Exception; + ByteBodyRes promptToImg(ByteBodyReq req, String arkApiKey) throws Exception; /** * 图生图 */ ByteBodyRes imgToImg(ByteBodyReq req) throws Exception; + ByteBodyRes imgToImg(ByteBodyReq req, String arkApiKey) throws Exception; /** * 首尾帧图生视频(使用全局配置的 Ark API Key) diff --git a/web-api/ruoyi-system/src/main/java/com/ruoyi/ai/service/impl/ByteDeptApiKeyServiceImpl.java b/web-api/ruoyi-system/src/main/java/com/ruoyi/ai/service/impl/ByteDeptApiKeyServiceImpl.java index 5c03a99..9ddbcbe 100644 --- a/web-api/ruoyi-system/src/main/java/com/ruoyi/ai/service/impl/ByteDeptApiKeyServiceImpl.java +++ b/web-api/ruoyi-system/src/main/java/com/ruoyi/ai/service/impl/ByteDeptApiKeyServiceImpl.java @@ -8,12 +8,14 @@ import com.ruoyi.common.core.redis.RedisCache; import com.ruoyi.common.exception.ServiceException; import com.ruoyi.common.utils.StringUtils; import com.ruoyi.system.service.ISysDeptService; +import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import java.util.concurrent.TimeUnit; @Service +@Slf4j public class ByteDeptApiKeyServiceImpl implements IByteDeptApiKeyService { private static final String NO_DEPT_MSG = "用户未分配部门:请在后台为门户用户设置 ai_user.dept_id(关联 sys_dept.dept_id)"; @@ -49,12 +51,14 @@ public class ByteDeptApiKeyServiceImpl implements IByteDeptApiKeyService { throw new ServiceException(NO_DEPT_ROW_MSG); } // 优先使用用户直接归属部门的 Key;多数业务把用户挂在分公司(如 101)并在该节点配 byte_api_key + Long keyDeptId = userDept.getDeptId(); String apiKey = trimKey(userDept.getByteApiKey()); if (StringUtils.isEmpty(apiKey)) { Long fallbackDeptId = resolveSecondLevelDeptId(userDept); if (!fallbackDeptId.equals(userDept.getDeptId())) { SysDept keyDept = sysDeptService.selectDeptById(fallbackDeptId); if (keyDept != null) { + keyDeptId = keyDept.getDeptId(); apiKey = trimKey(keyDept.getByteApiKey()); } } @@ -62,12 +66,48 @@ public class ByteDeptApiKeyServiceImpl implements IByteDeptApiKeyService { if (StringUtils.isEmpty(apiKey)) { throw new ServiceException(NO_API_KEY_MSG); } + log.info( + "resolveVolcApiKey ok: aiUserId={}, userDeptId={}, keyDeptId={}, keyLen={}, keyMasked={}, keyFingerprint={}", + aiUserId, + aiUser.getDeptId(), + keyDeptId, + apiKey.length(), + maskKey(apiKey), + fingerprint(apiKey) + ); redisCache.setCacheObject(cacheKey, apiKey, CACHE_HOURS, TimeUnit.HOURS); return apiKey; } + private static String maskKey(String key) { + if (StringUtils.isEmpty(key)) { + return ""; + } + if (key.length() <= 8) { + return "***"; + } + return key.substring(0, 4) + "***" + key.substring(key.length() - 4); + } + + private static String fingerprint(String key) { + if (StringUtils.isEmpty(key)) { + return ""; + } + return Integer.toHexString(key.hashCode()); + } + private static String trimKey(String raw) { - return raw == null ? null : raw.trim(); + if (raw == null) { + return null; + } + String k = raw.trim(); + if (k.regionMatches(true, 0, "Bearer ", 0, 7)) { + k = k.substring(7).trim(); + } + if ((k.startsWith("\"") && k.endsWith("\"")) || (k.startsWith("'") && k.endsWith("'"))) { + k = k.substring(1, k.length() - 1).trim(); + } + return k; } /** diff --git a/web-api/ruoyi-system/src/main/java/com/ruoyi/ai/service/impl/ByteService.java b/web-api/ruoyi-system/src/main/java/com/ruoyi/ai/service/impl/ByteService.java index 0059f44..8cc6c27 100644 --- a/web-api/ruoyi-system/src/main/java/com/ruoyi/ai/service/impl/ByteService.java +++ b/web-api/ruoyi-system/src/main/java/com/ruoyi/ai/service/impl/ByteService.java @@ -9,13 +9,42 @@ 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 lombok.extern.slf4j.Slf4j; import okhttp3.HttpUrl; import okhttp3.*; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; @Service +@Slf4j public class ByteService implements IByteService { + private static String normalizeArkApiKey(String rawKey) { + if (StringUtils.isBlank(rawKey)) { + return rawKey; + } + String k = rawKey.trim(); + if (k.regionMatches(true, 0, "Bearer ", 0, 7)) { + k = k.substring(7).trim(); + } + if ((k.startsWith("\"") && k.endsWith("\"")) || (k.startsWith("'") && k.endsWith("'"))) { + k = k.substring(1, k.length() - 1).trim(); + } + return k; + } + + private static String maskBearer(String authHeader) { + if (StringUtils.isBlank(authHeader)) { + return authHeader; + } + String prefix = "Bearer "; + String token = authHeader.startsWith(prefix) ? authHeader.substring(prefix.length()) : authHeader; + token = token == null ? "" : token.trim(); + if (token.length() <= 8) { + return prefix + "***"; + } + return prefix + token.substring(0, 4) + "***" + token.substring(token.length() - 4); + } + // private final OkHttpClient okHttpClient = OkHttpUtils.createOkHttpClient(); @@ -29,28 +58,35 @@ public class ByteService implements IByteService { @Value("${byteapi.url}") private String API_URL; - @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); + throw new Exception("promptToImg error:apiKey is required"); + } + + @Override + public ByteBodyRes promptToImg(ByteBodyReq req, String arkApiKey) throws Exception { + return this.imgToImg(req, arkApiKey); } @Override public ByteBodyRes imgToImg(ByteBodyReq req) throws Exception { + throw new Exception("imgToImg error:apiKey is required"); + } + + @Override + public ByteBodyRes imgToImg(ByteBodyReq req, String arkApiKey) throws Exception { // 1. 验证请求参数(可选,根据业务需求) if (StringUtils.isBlank(req.getPrompt())) { throw new Exception("imgToImg error:prompt is null"); } + arkApiKey = normalizeArkApiKey(arkApiKey); + if (StringUtils.isBlank(arkApiKey)) { + throw new Exception("imgToImg error:apiKey is null"); + } // 2. 构建请求体JSON(基于ByteBodyReq的字段) // 注意:ByteBodyReq需包含与API参数对应的字段(model、prompt等) @@ -59,7 +95,7 @@ public class ByteService implements IByteService { Request request = new Request.Builder() .url(API_URL + "/images/generations") .header("Content-Type", "application/json") - .header("Authorization", "Bearer " + apiKey) + .header("Authorization", "Bearer " + arkApiKey) // .proxy(new Proxy(Proxy.Type.HTTP, new InetSocketAddress("192.168.1.1", 8080))) .post(RequestBody.create( MediaType.parse("application/json"), @@ -84,7 +120,7 @@ public class ByteService implements IByteService { @Override public ByteBodyRes imgToVideo(ByteBodyReq req) throws Exception { - return imgToVideo(req, volcApiKey); + throw new Exception("imgToVideo error:apiKey is required"); } @Override @@ -92,40 +128,52 @@ public class ByteService implements IByteService { if (req == null) { throw new Exception("imgToVideo error:req is null"); } + arkApiKey = normalizeArkApiKey(arkApiKey); if (StringUtils.isBlank(arkApiKey)) { throw new Exception("imgToVideo error:apiKey is null"); } String jsonBody = objectMapper.writeValueAsString(req); + String requestUrl = volcBaseUrl + "/api/v1/proxy/ark/contents/generations/tasks"; + String authHeader = "Bearer " + arkApiKey; Request request = new Request.Builder() - .url(volcBaseUrl + "/api/v1/proxy/ark/contents/generations/tasks") + .url(requestUrl) .header("Content-Type", "application/json") - .header("Authorization", "Bearer " + arkApiKey) + .header("Authorization", authHeader) .post(RequestBody.create( MediaType.parse("application/json; charset=utf-8"), jsonBody )) .build(); - Response response = OkHttpUtils.newCall(request).execute(); + log.error( + "imgToVideo request: url={}, method=POST, headers={{Content-Type=application/json, Authorization={}}}, body={}", + requestUrl, + maskBearer(authHeader), + jsonBody + ); - if (!response.isSuccessful()) { - String errorMsg = response.body() != null ? response.body().string() : "imgToVideo error"; - throw new Exception("imgToVideo error:" + errorMsg); + Response response = OkHttpUtils.newCall(request).execute(); + int code = response.code(); + String responseBody = response.body() != null ? response.body().string() : ""; + + log.error("imgToVideo response: code={}, body={}", code, responseBody); + + if (code < 200 || code >= 300) { + throw new Exception("imgToVideo error:" + (StringUtils.isNotEmpty(responseBody) ? responseBody : "imgToVideo error")); } - if (response.body() == null) { + if (StringUtils.isEmpty(responseBody)) { 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 { - return uploadVideo(id, volcApiKey); + throw new Exception("uploadVideo error:apiKey is required"); } @Override @@ -133,6 +181,7 @@ public class ByteService implements IByteService { if (StringUtils.isBlank(id)) { throw new Exception("uploadVideo error:id is null"); } + arkApiKey = normalizeArkApiKey(arkApiKey); if (StringUtils.isBlank(arkApiKey)) { throw new Exception("uploadVideo error:apiKey is null"); } @@ -161,7 +210,7 @@ public class ByteService implements IByteService { @Override public AjaxResult cancelVideoTask(String id) throws Exception { - return cancelVideoTask(id, volcApiKey); + return AjaxResult.error("API Key 无效"); } @Override @@ -169,6 +218,7 @@ public class ByteService implements IByteService { if (StringUtils.isBlank(id)) { return AjaxResult.error("任务ID不能为空"); } + arkApiKey = normalizeArkApiKey(arkApiKey); if (StringUtils.isBlank(arkApiKey)) { return AjaxResult.error("API Key 无效"); } @@ -196,6 +246,7 @@ public class ByteService implements IByteService { @Override public String listVideoGenerationTasks(int pageNum, int pageSize, String arkApiKey) throws Exception { + arkApiKey = normalizeArkApiKey(arkApiKey); if (StringUtils.isBlank(arkApiKey)) { throw new Exception("listVideoGenerationTasks error:apiKey is null"); }