feat: 第三方素材API配置和流水线任务素材传递
This commit is contained in:
parent
72310e82b8
commit
f7518f1265
@ -0,0 +1,22 @@
|
||||
package com.designhub.config;
|
||||
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.http.client.SimpleClientHttpRequestFactory;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
|
||||
/**
|
||||
* RestTemplate配置类
|
||||
*/
|
||||
@Configuration
|
||||
public class RestTemplateConfig {
|
||||
|
||||
@Bean
|
||||
public RestTemplate restTemplate() {
|
||||
SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory();
|
||||
factory.setConnectTimeout(10000); // 连接超时10秒
|
||||
factory.setReadTimeout(30000); // 读取超时30秒
|
||||
return new RestTemplate(factory);
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,32 @@
|
||||
package com.designhub.controller;
|
||||
|
||||
import com.designhub.common.Result;
|
||||
import com.designhub.service.ExternalAssetService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
/**
|
||||
* 第三方素材API控制器
|
||||
*/
|
||||
@Tag(name = "第三方素材API", description = "第三方素材API相关接口")
|
||||
@RestController
|
||||
@RequestMapping("/api/external-asset")
|
||||
@RequiredArgsConstructor
|
||||
public class ExternalAssetController {
|
||||
|
||||
private final ExternalAssetService externalAssetService;
|
||||
|
||||
@Operation(summary = "测试API连接")
|
||||
@PostMapping("/test")
|
||||
public Result<Boolean> testConnection() {
|
||||
boolean success = externalAssetService.testConnection();
|
||||
if (success) {
|
||||
return Result.success(true);
|
||||
} else {
|
||||
return Result.error("连接测试失败,请检查API地址和密钥");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,55 @@
|
||||
package com.designhub.dto;
|
||||
|
||||
import lombok.Data;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 第三方素材API响应DTO
|
||||
*/
|
||||
@Data
|
||||
public class ExternalAssetDTO {
|
||||
|
||||
private boolean success;
|
||||
private AssetData data;
|
||||
private ErrorInfo error;
|
||||
|
||||
@Data
|
||||
public static class AssetData {
|
||||
private String taskNo;
|
||||
private String productModel;
|
||||
private String categoryName;
|
||||
private String sceneName;
|
||||
private String status;
|
||||
private String createdAt;
|
||||
private String updatedAt;
|
||||
private Map<String, List<AssetItem>> assets;
|
||||
private DownloadUrls downloadUrls;
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class AssetItem {
|
||||
private String id;
|
||||
private String url;
|
||||
private String downloadUrl;
|
||||
private String fileName;
|
||||
private Long fileSize;
|
||||
private Integer sortOrder;
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class DownloadUrls {
|
||||
private String all;
|
||||
private String main;
|
||||
private String sku;
|
||||
private String detail;
|
||||
private String psd;
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class ErrorInfo {
|
||||
private String code;
|
||||
private String message;
|
||||
}
|
||||
}
|
||||
|
||||
@ -96,6 +96,16 @@ public class TaskItem implements Serializable {
|
||||
*/
|
||||
private String deliveryPath;
|
||||
|
||||
/**
|
||||
* 素材下载链接(从第三方API获取)
|
||||
*/
|
||||
private String downloadUrl;
|
||||
|
||||
/**
|
||||
* 前置子任务的素材下载链接(流水线任务中,前一环节完成后传递)
|
||||
*/
|
||||
private String prevDownloadUrl;
|
||||
|
||||
/**
|
||||
* 提交时间
|
||||
*/
|
||||
|
||||
@ -0,0 +1,163 @@
|
||||
package com.designhub.service;
|
||||
|
||||
import com.designhub.dto.ExternalAssetDTO;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.http.*;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
|
||||
/**
|
||||
* 第三方素材API服务
|
||||
* 用于查询外部素材系统的任务完成状态和下载链接
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class ExternalAssetService {
|
||||
|
||||
private final SystemConfigService systemConfigService;
|
||||
private final RestTemplate restTemplate;
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
/**
|
||||
* 查询任务素材
|
||||
* @param taskNo 任务编号
|
||||
* @return 素材信息,包含下载链接
|
||||
*/
|
||||
public ExternalAssetDTO queryAssets(String taskNo) {
|
||||
String apiUrl = systemConfigService.getConfigValue("external_asset_api_url");
|
||||
String apiKey = systemConfigService.getConfigValue("external_asset_api_key");
|
||||
|
||||
if (apiUrl == null || apiUrl.isEmpty()) {
|
||||
log.warn("第三方素材API地址未配置");
|
||||
ExternalAssetDTO result = new ExternalAssetDTO();
|
||||
result.setSuccess(false);
|
||||
ExternalAssetDTO.ErrorInfo error = new ExternalAssetDTO.ErrorInfo();
|
||||
error.setCode("CONFIG_ERROR");
|
||||
error.setMessage("第三方素材API未配置");
|
||||
result.setError(error);
|
||||
return result;
|
||||
}
|
||||
|
||||
if (apiKey == null || apiKey.isEmpty()) {
|
||||
log.warn("第三方素材API Key未配置");
|
||||
ExternalAssetDTO result = new ExternalAssetDTO();
|
||||
result.setSuccess(false);
|
||||
ExternalAssetDTO.ErrorInfo error = new ExternalAssetDTO.ErrorInfo();
|
||||
error.setCode("CONFIG_ERROR");
|
||||
error.setMessage("第三方素材API Key未配置");
|
||||
result.setError(error);
|
||||
return result;
|
||||
}
|
||||
|
||||
try {
|
||||
// 构建请求URL
|
||||
String url = apiUrl.endsWith("/") ? apiUrl : apiUrl + "/";
|
||||
url = url + "product-assets?taskNo=" + taskNo;
|
||||
|
||||
log.info("查询第三方素材API: {}", url);
|
||||
|
||||
// 构建请求头
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.set("X-API-Key", apiKey);
|
||||
headers.setContentType(MediaType.APPLICATION_JSON);
|
||||
|
||||
HttpEntity<String> entity = new HttpEntity<>(headers);
|
||||
|
||||
// 发送请求
|
||||
ResponseEntity<String> response = restTemplate.exchange(
|
||||
url,
|
||||
HttpMethod.GET,
|
||||
entity,
|
||||
String.class
|
||||
);
|
||||
|
||||
log.info("第三方素材API响应状态: {}", response.getStatusCode());
|
||||
log.debug("第三方素材API响应内容: {}", response.getBody());
|
||||
|
||||
if (response.getStatusCode() == HttpStatus.OK && response.getBody() != null) {
|
||||
return objectMapper.readValue(response.getBody(), ExternalAssetDTO.class);
|
||||
} else {
|
||||
ExternalAssetDTO result = new ExternalAssetDTO();
|
||||
result.setSuccess(false);
|
||||
ExternalAssetDTO.ErrorInfo error = new ExternalAssetDTO.ErrorInfo();
|
||||
error.setCode("REQUEST_FAILED");
|
||||
error.setMessage("请求失败: " + response.getStatusCode());
|
||||
result.setError(error);
|
||||
return result;
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("调用第三方素材API失败", e);
|
||||
ExternalAssetDTO result = new ExternalAssetDTO();
|
||||
result.setSuccess(false);
|
||||
ExternalAssetDTO.ErrorInfo error = new ExternalAssetDTO.ErrorInfo();
|
||||
error.setCode("EXCEPTION");
|
||||
error.setMessage("调用API失败: " + e.getMessage());
|
||||
result.setError(error);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查任务素材是否完成
|
||||
* @param taskNo 任务编号
|
||||
* @return 下载链接(如果完成),否则返回null
|
||||
*/
|
||||
public String checkTaskCompletion(String taskNo) {
|
||||
ExternalAssetDTO result = queryAssets(taskNo);
|
||||
|
||||
if (result.isSuccess() && result.getData() != null) {
|
||||
ExternalAssetDTO.DownloadUrls urls = result.getData().getDownloadUrls();
|
||||
if (urls != null && urls.getAll() != null && !urls.getAll().isEmpty()) {
|
||||
log.info("任务 {} 素材已完成,下载链接: {}", taskNo, urls.getAll());
|
||||
return urls.getAll();
|
||||
}
|
||||
}
|
||||
|
||||
log.info("任务 {} 素材未完成或查询失败", taskNo);
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 测试API连接
|
||||
* @return 测试结果
|
||||
*/
|
||||
public boolean testConnection() {
|
||||
String apiUrl = systemConfigService.getConfigValue("external_asset_api_url");
|
||||
String apiKey = systemConfigService.getConfigValue("external_asset_api_key");
|
||||
|
||||
if (apiUrl == null || apiUrl.isEmpty() || apiKey == null || apiKey.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
// 测试连接(使用一个测试任务号)
|
||||
String url = apiUrl.endsWith("/") ? apiUrl : apiUrl + "/";
|
||||
url = url + "product-assets?taskNo=TEST000000000000";
|
||||
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.set("X-API-Key", apiKey);
|
||||
|
||||
HttpEntity<String> entity = new HttpEntity<>(headers);
|
||||
|
||||
ResponseEntity<String> response = restTemplate.exchange(
|
||||
url,
|
||||
HttpMethod.GET,
|
||||
entity,
|
||||
String.class
|
||||
);
|
||||
|
||||
// 只要能收到响应(无论是404还是200),说明连接正常
|
||||
return response.getStatusCode().is2xxSuccessful() ||
|
||||
response.getStatusCode() == HttpStatus.NOT_FOUND;
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("测试第三方素材API连接失败", e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -35,6 +35,8 @@ public class TaskItemService {
|
||||
private final NotificationService notificationService;
|
||||
private final DingTalkService dingTalkService;
|
||||
private final ConfigService configService;
|
||||
private final ExternalAssetService externalAssetService;
|
||||
private final SystemConfigService systemConfigService;
|
||||
|
||||
/**
|
||||
* 获取可接单的子任务列表
|
||||
@ -262,6 +264,9 @@ public class TaskItemService {
|
||||
|
||||
/**
|
||||
* 完结子任务(关键方法:包含自动解锁逻辑)
|
||||
* @param itemId 子任务ID
|
||||
* @param userId 用户ID
|
||||
* @param deliveryPath 交付路径(可选,如果启用第三方API则自动获取)
|
||||
*/
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public Map<String, Object> completeItem(Long itemId, Long userId, String deliveryPath) {
|
||||
@ -283,10 +288,40 @@ public class TaskItemService {
|
||||
throw new BusinessException(ResultCode.TASK_STATUS_ERROR, "只能完结进行中的子任务");
|
||||
}
|
||||
|
||||
// 4. 更新子任务状态
|
||||
// 4. 检查是否启用第三方素材API验证
|
||||
String downloadUrl = null;
|
||||
boolean externalApiEnabled = "true".equalsIgnoreCase(
|
||||
systemConfigService.getConfigValue("external_asset_api_enabled", "false"));
|
||||
|
||||
if (externalApiEnabled) {
|
||||
// 调用第三方素材API验证任务完成状态
|
||||
String taskNo = item.getItemNo(); // 使用子任务编号作为任务号
|
||||
|
||||
try {
|
||||
downloadUrl = externalAssetService.checkTaskCompletion(taskNo);
|
||||
} catch (Exception e) {
|
||||
log.warn("调用第三方素材API失败", e);
|
||||
}
|
||||
|
||||
if (downloadUrl == null) {
|
||||
// 如果第三方API未返回下载链接,提示用户去上传素材
|
||||
throw new BusinessException(ResultCode.ERROR,
|
||||
"素材未提交,请先前往 3D.YYDD.AI 提交任务素材后再完结任务");
|
||||
}
|
||||
|
||||
log.info("第三方素材API返回下载链接: {}", downloadUrl);
|
||||
} else {
|
||||
// 未启用第三方API,使用传统方式(需要传入deliveryPath)
|
||||
if (deliveryPath == null || deliveryPath.trim().isEmpty()) {
|
||||
throw new BusinessException(ResultCode.ERROR, "交付路径不能为空");
|
||||
}
|
||||
}
|
||||
|
||||
// 5. 更新子任务状态
|
||||
item.setStatus("completed");
|
||||
item.setProgress(100);
|
||||
item.setDeliveryPath(deliveryPath);
|
||||
item.setDeliveryPath(downloadUrl != null ? downloadUrl : deliveryPath);
|
||||
item.setDownloadUrl(downloadUrl);
|
||||
item.setCompleteTime(LocalDateTime.now());
|
||||
item.setUpdateTime(LocalDateTime.now());
|
||||
taskItemMapper.updateById(item);
|
||||
@ -321,6 +356,26 @@ public class TaskItemService {
|
||||
if (allGroupCompleted) {
|
||||
log.info("✅ 第 {} 组所有子任务已完成,准备解锁下一组", currentGroupNo);
|
||||
|
||||
// 🔥 收集当前组所有子任务的下载链接,传递给下一组
|
||||
StringBuilder prevDownloadUrls = new StringBuilder();
|
||||
for (TaskItem groupItem : currentGroupItems) {
|
||||
if (groupItem.getDownloadUrl() != null && !groupItem.getDownloadUrl().isEmpty()) {
|
||||
if (prevDownloadUrls.length() > 0) {
|
||||
prevDownloadUrls.append(",");
|
||||
}
|
||||
prevDownloadUrls.append(groupItem.getDownloadUrl());
|
||||
}
|
||||
// 同时继承前面环节的下载链接
|
||||
if (groupItem.getPrevDownloadUrl() != null && !groupItem.getPrevDownloadUrl().isEmpty()) {
|
||||
if (prevDownloadUrls.length() > 0) {
|
||||
prevDownloadUrls.append(",");
|
||||
}
|
||||
prevDownloadUrls.append(groupItem.getPrevDownloadUrl());
|
||||
}
|
||||
}
|
||||
String prevDownloadUrlStr = prevDownloadUrls.toString();
|
||||
log.info("前置环节素材下载链接: {}", prevDownloadUrlStr);
|
||||
|
||||
// 解锁下一组的所有子任务
|
||||
Integer nextGroupNo = currentGroupNo + 1;
|
||||
List<TaskItem> nextGroupItems = taskItemMapper.selectList(
|
||||
@ -331,14 +386,15 @@ public class TaskItemService {
|
||||
);
|
||||
|
||||
if (!nextGroupItems.isEmpty()) {
|
||||
// 批量解锁下一组的所有子任务,状态设为待审核
|
||||
// 批量解锁下一组的所有子任务,状态设为待审核,并传递前置环节的下载链接
|
||||
for (TaskItem nextItem : nextGroupItems) {
|
||||
nextItem.setStatus("pending_approval"); // 待接单部门负责人审核
|
||||
nextItem.setIsLocked(0);
|
||||
nextItem.setPrevDownloadUrl(prevDownloadUrlStr); // 🔥 传递前置环节的下载链接
|
||||
nextItem.setUpdateTime(LocalDateTime.now());
|
||||
taskItemMapper.updateById(nextItem);
|
||||
|
||||
log.info("✅ 解锁子任务: {} (组号: {}, ID: {}),待接单部门负责人审核",
|
||||
log.info("✅ 解锁子任务: {} (组号: {}, ID: {}),待接单部门负责人审核,已传递前置素材链接",
|
||||
nextItem.getItemNo(), nextGroupNo, nextItem.getId());
|
||||
}
|
||||
|
||||
@ -380,10 +436,40 @@ public class TaskItemService {
|
||||
int totalCount = taskItemMapper.countTotalItems(item.getTaskId());
|
||||
|
||||
if (completedCount == totalCount) {
|
||||
// 🔥 获取最后一组子任务的下载链接,保存到主任务
|
||||
Integer maxGroupNo = taskItemMapper.selectList(
|
||||
new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<TaskItem>()
|
||||
.eq(TaskItem::getTaskId, item.getTaskId())
|
||||
.orderByDesc(TaskItem::getGroupNo)
|
||||
.last("LIMIT 1")
|
||||
).stream().findFirst().map(TaskItem::getGroupNo).orElse(1);
|
||||
|
||||
List<TaskItem> lastGroupItems = taskItemMapper.selectList(
|
||||
new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<TaskItem>()
|
||||
.eq(TaskItem::getTaskId, item.getTaskId())
|
||||
.eq(TaskItem::getGroupNo, maxGroupNo)
|
||||
);
|
||||
|
||||
// 收集最后一组的下载链接
|
||||
StringBuilder finalDownloadUrl = new StringBuilder();
|
||||
for (TaskItem lastItem : lastGroupItems) {
|
||||
if (lastItem.getDownloadUrl() != null && !lastItem.getDownloadUrl().isEmpty()) {
|
||||
if (finalDownloadUrl.length() > 0) {
|
||||
finalDownloadUrl.append(",");
|
||||
}
|
||||
finalDownloadUrl.append(lastItem.getDownloadUrl());
|
||||
}
|
||||
}
|
||||
|
||||
// 所有子任务完成,更新主任务状态
|
||||
mainTask.setStatus("completed");
|
||||
mainTask.setCompleteTime(LocalDateTime.now());
|
||||
mainTask.setUpdateTime(LocalDateTime.now());
|
||||
// 🔥 保存最后一组的下载链接到主任务
|
||||
if (finalDownloadUrl.length() > 0) {
|
||||
mainTask.setDeliveryPath(finalDownloadUrl.toString());
|
||||
log.info("主任务最终素材下载链接: {}", finalDownloadUrl);
|
||||
}
|
||||
taskMapper.updateById(mainTask);
|
||||
|
||||
log.info("🎉 主任务 {} 所有子任务已完成,主任务标记为完成", mainTask.getId());
|
||||
|
||||
@ -0,0 +1,15 @@
|
||||
-- 添加第三方素材API相关配置
|
||||
-- V9: 第三方素材API配置
|
||||
|
||||
-- 在task_item表添加下载链接字段
|
||||
ALTER TABLE task_item ADD COLUMN IF NOT EXISTS download_url VARCHAR(1000) COMMENT '素材下载链接(从第三方API获取)';
|
||||
ALTER TABLE task_item ADD COLUMN IF NOT EXISTS prev_download_url VARCHAR(2000) COMMENT '前置子任务的素材下载链接(流水线任务中传递)';
|
||||
|
||||
-- 添加系统配置:第三方素材API
|
||||
INSERT INTO sys_config (config_key, config_value, config_group, config_name, description, value_type, built_in, sort_order, create_time, update_time, deleted)
|
||||
VALUES
|
||||
('external_asset_api_url', '', 'external_api', '第三方素材API地址', '第三方素材系统的API地址,例如:https://your-domain.com/api/external/', 'string', 1, 1, NOW(), NOW(), 0),
|
||||
('external_asset_api_key', '', 'external_api', '第三方素材API密钥', '用于调用第三方素材API的密钥', 'string', 1, 2, NOW(), NOW(), 0),
|
||||
('external_asset_api_enabled', 'false', 'external_api', '启用第三方素材API', '是否启用第三方素材API验证任务完结', 'boolean', 1, 3, NOW(), NOW(), 0)
|
||||
ON DUPLICATE KEY UPDATE update_time = NOW();
|
||||
|
||||
@ -70,6 +70,8 @@ export interface Task {
|
||||
allDeliveryPaths?: string[]
|
||||
// 预览图
|
||||
previewImage?: string
|
||||
// 素材下载链接
|
||||
downloadUrl?: string
|
||||
}
|
||||
|
||||
// 任务子项(流水线任务)
|
||||
@ -107,6 +109,9 @@ export interface TaskItem {
|
||||
spuInfo?: string
|
||||
taskSubType?: string
|
||||
attachmentUrl?: string
|
||||
previewImage?: string
|
||||
downloadUrl?: string
|
||||
prevDownloadUrl?: string // 前置子任务的素材下载链接
|
||||
}
|
||||
|
||||
// 创建流水线任务请求
|
||||
|
||||
@ -1188,6 +1188,73 @@
|
||||
</div>
|
||||
</a-tab-pane>
|
||||
|
||||
<!-- 第三方素材API配置 -->
|
||||
<a-tab-pane key="external-asset-api" tab="素材API配置">
|
||||
<div class="settings-section">
|
||||
<div class="section-header">
|
||||
<h2 class="section-title">第三方素材API配置</h2>
|
||||
</div>
|
||||
|
||||
<a-alert
|
||||
message="使用说明"
|
||||
description="配置第三方素材API后,设计师完结任务时系统会自动调用API验证素材是否已提交。如果素材未提交,系统会提示设计师前往素材平台提交。完结成功后,需求方可以直接下载素材。"
|
||||
type="info"
|
||||
show-icon
|
||||
style="margin-bottom: 24px;"
|
||||
/>
|
||||
|
||||
<a-form :model="externalAssetApiConfig" layout="vertical" class="external-api-form">
|
||||
<a-form-item label="启用第三方素材API">
|
||||
<a-switch
|
||||
v-model:checked="externalAssetApiConfig.enabled"
|
||||
checked-children="已启用"
|
||||
un-checked-children="未启用"
|
||||
/>
|
||||
<div style="margin-top: 8px; font-size: 12px; color: #888;">
|
||||
启用后,设计师完结任务时将验证素材是否已在第三方平台提交
|
||||
</div>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="API地址" :required="externalAssetApiConfig.enabled">
|
||||
<a-input
|
||||
v-model:value="externalAssetApiConfig.apiUrl"
|
||||
placeholder="https://your-domain.com/api/external/"
|
||||
size="large"
|
||||
:disabled="!externalAssetApiConfig.enabled"
|
||||
/>
|
||||
<div style="margin-top: 8px; font-size: 12px; color: #888;">
|
||||
第三方素材系统的API地址,需以/结尾
|
||||
</div>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="API密钥" :required="externalAssetApiConfig.enabled">
|
||||
<a-input-password
|
||||
v-model:value="externalAssetApiConfig.apiKey"
|
||||
placeholder="请输入API密钥"
|
||||
size="large"
|
||||
:disabled="!externalAssetApiConfig.enabled"
|
||||
/>
|
||||
<div style="margin-top: 8px; font-size: 12px; color: #888;">
|
||||
用于调用第三方素材API的密钥(X-API-Key)
|
||||
</div>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item>
|
||||
<a-space>
|
||||
<a-button type="primary" size="large" @click="handleSaveExternalAssetApiConfig" :loading="savingExternalAssetApi">
|
||||
<template #icon><Icon name="check" size="sm" /></template>
|
||||
保存配置
|
||||
</a-button>
|
||||
<a-button size="large" @click="handleTestExternalAssetApi" :loading="testingExternalAssetApi" :disabled="!externalAssetApiConfig.apiUrl || !externalAssetApiConfig.apiKey">
|
||||
<template #icon><Icon name="refresh" size="sm" /></template>
|
||||
测试连接
|
||||
</a-button>
|
||||
</a-space>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</div>
|
||||
</a-tab-pane>
|
||||
|
||||
<!-- 第三方数据库配置 -->
|
||||
<a-tab-pane key="third-party-db" tab="第三方数据库">
|
||||
<div class="settings-section">
|
||||
@ -3506,6 +3573,98 @@ const delayPenaltyConfig = ref({
|
||||
|
||||
const savingDelayPenaltyConfig = ref(false)
|
||||
|
||||
// ==================== 第三方素材API配置 ====================
|
||||
const externalAssetApiConfig = reactive({
|
||||
enabled: false,
|
||||
apiUrl: '',
|
||||
apiKey: ''
|
||||
})
|
||||
const savingExternalAssetApi = ref(false)
|
||||
const testingExternalAssetApi = ref(false)
|
||||
|
||||
// 加载第三方素材API配置
|
||||
const loadExternalAssetApiConfig = async () => {
|
||||
try {
|
||||
const response = await getSystemConfigsByGroup('external_api')
|
||||
const configs = response.data || []
|
||||
|
||||
const urlConfig = configs.find((c: any) => c.configKey === 'external_asset_api_url')
|
||||
const keyConfig = configs.find((c: any) => c.configKey === 'external_asset_api_key')
|
||||
const enabledConfig = configs.find((c: any) => c.configKey === 'external_asset_api_enabled')
|
||||
|
||||
externalAssetApiConfig.apiUrl = urlConfig?.configValue || ''
|
||||
externalAssetApiConfig.apiKey = keyConfig?.configValue || ''
|
||||
externalAssetApiConfig.enabled = enabledConfig?.configValue === 'true'
|
||||
} catch (error) {
|
||||
console.error('加载第三方素材API配置失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 保存第三方素材API配置
|
||||
const handleSaveExternalAssetApiConfig = async () => {
|
||||
if (externalAssetApiConfig.enabled) {
|
||||
if (!externalAssetApiConfig.apiUrl) {
|
||||
message.error('请输入API地址')
|
||||
return
|
||||
}
|
||||
if (!externalAssetApiConfig.apiKey) {
|
||||
message.error('请输入API密钥')
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
savingExternalAssetApi.value = true
|
||||
try {
|
||||
await batchUpdateSystemConfigs({
|
||||
'external_asset_api_url': externalAssetApiConfig.apiUrl,
|
||||
'external_asset_api_key': externalAssetApiConfig.apiKey,
|
||||
'external_asset_api_enabled': externalAssetApiConfig.enabled.toString()
|
||||
})
|
||||
message.success('第三方素材API配置保存成功')
|
||||
} catch (error: any) {
|
||||
message.error(error.message || '保存失败')
|
||||
} finally {
|
||||
savingExternalAssetApi.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 测试第三方素材API连接
|
||||
const handleTestExternalAssetApi = async () => {
|
||||
if (!externalAssetApiConfig.apiUrl || !externalAssetApiConfig.apiKey) {
|
||||
message.error('请先填写API地址和密钥')
|
||||
return
|
||||
}
|
||||
|
||||
testingExternalAssetApi.value = true
|
||||
try {
|
||||
// 先保存配置
|
||||
await batchUpdateSystemConfigs({
|
||||
'external_asset_api_url': externalAssetApiConfig.apiUrl,
|
||||
'external_asset_api_key': externalAssetApiConfig.apiKey
|
||||
})
|
||||
|
||||
// 调用测试接口
|
||||
const response = await fetch('/api/external-asset/test', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${localStorage.getItem('token')}`
|
||||
}
|
||||
})
|
||||
const result = await response.json()
|
||||
|
||||
if (result.code === 200 && result.data === true) {
|
||||
message.success('连接测试成功')
|
||||
} else {
|
||||
message.error(result.message || '连接测试失败')
|
||||
}
|
||||
} catch (error: any) {
|
||||
message.error(error.message || '连接测试失败')
|
||||
} finally {
|
||||
testingExternalAssetApi.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 加载延误扣分配置
|
||||
const loadDelayPenaltyConfig = async () => {
|
||||
try {
|
||||
@ -3771,6 +3930,8 @@ onMounted(() => {
|
||||
loadTaskReleaseConfig()
|
||||
// 加载延误扣分配置
|
||||
loadDelayPenaltyConfig()
|
||||
// 加载第三方素材API配置
|
||||
loadExternalAssetApiConfig()
|
||||
})
|
||||
|
||||
// S3配置相关方法
|
||||
|
||||
@ -945,6 +945,17 @@
|
||||
<!-- 操作按钮 -->
|
||||
<div class="task-actions">
|
||||
<a-button size="small" @click="handleViewTaskDetail(task)">查看详情</a-button>
|
||||
<!-- 上一环节素材下载(流水线任务) -->
|
||||
<a-button
|
||||
v-if="task.prevDownloadUrl"
|
||||
type="default"
|
||||
size="small"
|
||||
@click.stop="handleDownloadPrevAssets(task)"
|
||||
class="prev-assets-btn"
|
||||
>
|
||||
<Icon name="download" size="sm" style="margin-right: 4px;" />
|
||||
上一环节素材
|
||||
</a-button>
|
||||
<a-button v-if="canUpdateProgress(task)" type="primary" size="small" @click="handleUpdateProgress(task)">
|
||||
更新进度
|
||||
</a-button>
|
||||
@ -1636,6 +1647,28 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 下载素材按钮(已完成任务) -->
|
||||
<div v-if="task.status === 'completed'" class="task-actions" style="margin-top: 16px; padding-top: 16px; border-top: 1px solid rgba(49, 240, 139, 0.2);">
|
||||
<a-button
|
||||
v-if="task.downloadUrl || task.deliveryPath"
|
||||
type="primary"
|
||||
size="small"
|
||||
@click.stop="handleDownloadAssets(task)"
|
||||
style="width: 100%;">
|
||||
<Icon name="download" size="sm" style="margin-right: 4px;" />
|
||||
下载素材
|
||||
</a-button>
|
||||
<a-button
|
||||
v-else
|
||||
type="default"
|
||||
size="small"
|
||||
disabled
|
||||
style="width: 100%; opacity: 0.5;">
|
||||
<Icon name="download" size="sm" style="margin-right: 4px;" />
|
||||
暂无素材
|
||||
</a-button>
|
||||
</div>
|
||||
|
||||
<!-- 被驳回或草稿任务的操作按钮 -->
|
||||
<div v-if="['rejected', 'draft'].includes(task.status) || task.approvalStatus?.includes('rejected')" class="task-actions" style="margin-top: 16px; padding-top: 16px; border-top: 1px solid rgba(49, 240, 139, 0.2);">
|
||||
<a-button
|
||||
@ -2311,11 +2344,13 @@ import dayjs from 'dayjs'
|
||||
interface TaskOrSubTask extends Task {
|
||||
isSubTask?: boolean // 是否为子任务
|
||||
itemId?: number // 子任务ID
|
||||
itemNo?: string // 子任务编号
|
||||
subTaskNo?: string // 子任务编号
|
||||
groupNo?: number // 子任务组号
|
||||
taskDescription?: string // 主任务描述
|
||||
submitTime?: string // 提交时间
|
||||
taskTypeName?: string // 任务类型名称(子任务直接返回)
|
||||
prevDownloadUrl?: string // 前置子任务的素材下载链接
|
||||
}
|
||||
|
||||
const router = useRouter()
|
||||
@ -3686,6 +3721,67 @@ const handleUpdateProgress = (task: TaskOrSubTask) => {
|
||||
|
||||
// 完结任务(支持普通任务和子任务)
|
||||
// 编辑被驳回的任务
|
||||
// 下载素材
|
||||
const handleDownloadAssets = (task: Task) => {
|
||||
const downloadUrl = task.downloadUrl || task.deliveryPath
|
||||
if (downloadUrl) {
|
||||
// 如果是http链接,直接打开
|
||||
if (downloadUrl.startsWith('http')) {
|
||||
window.open(downloadUrl, '_blank')
|
||||
} else {
|
||||
// 复制到剪贴板
|
||||
navigator.clipboard.writeText(downloadUrl).then(() => {
|
||||
message.success('素材路径已复制到剪贴板')
|
||||
}).catch(() => {
|
||||
message.info(`素材路径: ${downloadUrl}`)
|
||||
})
|
||||
}
|
||||
} else {
|
||||
message.warning('暂无可下载的素材')
|
||||
}
|
||||
}
|
||||
|
||||
// 下载上一环节素材(流水线任务)
|
||||
const handleDownloadPrevAssets = (task: TaskOrSubTask) => {
|
||||
const prevDownloadUrl = task.prevDownloadUrl
|
||||
if (prevDownloadUrl) {
|
||||
// 如果包含多个链接(逗号分隔),显示弹窗让用户选择
|
||||
const urls = prevDownloadUrl.split(',').filter(url => url.trim())
|
||||
if (urls.length === 1) {
|
||||
// 只有一个链接,直接打开
|
||||
if (urls[0].startsWith('http')) {
|
||||
window.open(urls[0], '_blank')
|
||||
} else {
|
||||
navigator.clipboard.writeText(urls[0]).then(() => {
|
||||
message.success('素材路径已复制到剪贴板')
|
||||
}).catch(() => {
|
||||
message.info(`素材路径: ${urls[0]}`)
|
||||
})
|
||||
}
|
||||
} else {
|
||||
// 多个链接,显示列表让用户选择
|
||||
Modal.info({
|
||||
title: '上一环节素材下载',
|
||||
content: h('div', [
|
||||
h('p', { style: 'margin-bottom: 12px;' }, `共 ${urls.length} 个素材文件:`),
|
||||
...urls.map((url, index) =>
|
||||
h('div', { style: 'margin-bottom: 8px;' }, [
|
||||
h('a', {
|
||||
href: url,
|
||||
target: '_blank',
|
||||
style: 'color: #1890ff; word-break: break-all;'
|
||||
}, `素材 ${index + 1}: ${url.length > 50 ? url.substring(0, 50) + '...' : url}`)
|
||||
])
|
||||
)
|
||||
]),
|
||||
width: 600
|
||||
})
|
||||
}
|
||||
} else {
|
||||
message.warning('暂无上一环节的素材')
|
||||
}
|
||||
}
|
||||
|
||||
const handleEditRejectedTask = (task: Task) => {
|
||||
// 跳转到创建任务页面,并传入任务ID作为编辑标识
|
||||
router.push({
|
||||
@ -3698,43 +3794,57 @@ const handleEditRejectedTask = (task: Task) => {
|
||||
}
|
||||
|
||||
const handleCompleteTask = (task: TaskOrSubTask) => {
|
||||
let deliveryPath = ''
|
||||
const isSubTask = task.isSubTask
|
||||
const taskType = isSubTask ? '子任务' : '任务'
|
||||
const taskNo = isSubTask ? (task.itemNo || task.taskNo) : task.taskNo
|
||||
|
||||
Modal.confirm({
|
||||
title: `完结${taskType}`,
|
||||
content: h('div', [
|
||||
h('div', { style: 'margin-bottom: 12px;' }, `确认完结${taskType}:${task.title}`),
|
||||
h('div', { style: 'margin-bottom: 12px;' }, '交付路径:'),
|
||||
h(Textarea, {
|
||||
rows: 4,
|
||||
placeholder: '请输入交付路径或说明',
|
||||
style: 'width: 100%',
|
||||
onChange: (e: any) => { deliveryPath = e.target.value }
|
||||
})
|
||||
h('div', { style: 'color: #666; font-size: 13px;' }, [
|
||||
h('p', { style: 'margin-bottom: 8px;' }, '系统将自动从素材平台获取您提交的素材。'),
|
||||
h('p', { style: 'margin: 0;' }, `任务编号:${taskNo || '--'}`)
|
||||
])
|
||||
]),
|
||||
okText: '确认完结',
|
||||
cancelText: '取消',
|
||||
onOk: async () => {
|
||||
if (!deliveryPath.trim()) {
|
||||
message.error('请输入交付路径')
|
||||
return Promise.reject()
|
||||
}
|
||||
|
||||
try {
|
||||
if (isSubTask) {
|
||||
// 完结子任务
|
||||
await taskApi.completeTaskItem(task.itemId || task.id!, { deliveryPath })
|
||||
message.success('子任务已完成,等待下一个子任务解锁')
|
||||
// 完结子任务(后端会自动调用第三方API验证)
|
||||
await taskApi.completeTaskItem(task.itemId || task.id!, { deliveryPath: '' })
|
||||
message.success('子任务已完成')
|
||||
} else {
|
||||
// 完结普通任务
|
||||
await taskApi.completeTask(task.id!, { deliveryPath })
|
||||
await taskApi.completeTask(task.id!, { deliveryPath: '' })
|
||||
message.success('任务已提交审核')
|
||||
}
|
||||
loadInProgressTasks()
|
||||
loadAllTabCounts()
|
||||
loadDashboardStats()
|
||||
} catch (error: any) {
|
||||
message.error(error.message || '任务完结失败')
|
||||
// 如果是素材未提交的错误,显示特定提示
|
||||
const errorMsg = error.message || error.msg || '任务完结失败'
|
||||
if (errorMsg.includes('素材未提交') || errorMsg.includes('3D.YYDD.AI')) {
|
||||
Modal.warning({
|
||||
title: '素材未提交',
|
||||
content: h('div', [
|
||||
h('p', '请先前往素材平台提交任务素材后再完结任务。'),
|
||||
h('p', { style: 'margin-top: 8px;' }, [
|
||||
'任务编号:',
|
||||
h('strong', taskNo || '--')
|
||||
]),
|
||||
h('a', {
|
||||
href: 'https://3d.yydd.ai',
|
||||
target: '_blank',
|
||||
style: 'color: #1890ff; margin-top: 12px; display: inline-block;'
|
||||
}, '前往素材平台 →')
|
||||
])
|
||||
})
|
||||
} else {
|
||||
message.error(errorMsg)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
@ -5124,6 +5234,17 @@ onUnmounted(() => {
|
||||
gap: $space-sm;
|
||||
padding-top: $space-md;
|
||||
border-top: 1px solid $border-primary;
|
||||
flex-wrap: wrap;
|
||||
|
||||
.prev-assets-btn {
|
||||
background: rgba(24, 144, 255, 0.1);
|
||||
border-color: #1890ff;
|
||||
color: #1890ff;
|
||||
|
||||
&:hover {
|
||||
background: rgba(24, 144, 255, 0.2);
|
||||
}
|
||||
}
|
||||
margin-top: $space-md;
|
||||
|
||||
button {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user