修复图片下载问题
This commit is contained in:
@@ -36,7 +36,8 @@ public class PermissionMenuSchemaInitializer {
|
|||||||
new DefaultAdminMenu("查询ASIN", "admin_query_asin", "query-asin", 65),
|
new DefaultAdminMenu("查询ASIN", "admin_query_asin", "query-asin", 65),
|
||||||
new DefaultAdminMenu("商品类目", "admin_product_categories", "product-categories", 66),
|
new DefaultAdminMenu("商品类目", "admin_product_categories", "product-categories", 66),
|
||||||
new DefaultAdminMenu("查看生成记录", "admin_history", "history", 70),
|
new DefaultAdminMenu("查看生成记录", "admin_history", "history", 70),
|
||||||
new DefaultAdminMenu("版本管理", "admin_version", "version", 80)
|
new DefaultAdminMenu("软件版本管理", "admin_version", "version", 80),
|
||||||
|
new DefaultAdminMenu("数字人版本管理", "digital_human_version", "digital-human-version", 81)
|
||||||
);
|
);
|
||||||
|
|
||||||
@EventListener(ApplicationReadyEvent.class)
|
@EventListener(ApplicationReadyEvent.class)
|
||||||
|
|||||||
@@ -78,7 +78,7 @@ public class PriceTrackTaskService {
|
|||||||
private FileTaskEntity loadTaskForExecution(Long taskId) {
|
private FileTaskEntity loadTaskForExecution(Long taskId) {
|
||||||
Map<Long, FileTaskEntity> cachedTasks = priceTrackTaskCacheService.getTaskCacheBatch(List.of(taskId));
|
Map<Long, FileTaskEntity> cachedTasks = priceTrackTaskCacheService.getTaskCacheBatch(List.of(taskId));
|
||||||
FileTaskEntity cached = cachedTasks.get(taskId);
|
FileTaskEntity cached = cachedTasks.get(taskId);
|
||||||
if (cached != null) {
|
if (cached != null && hasText(cached.getRequestJson())) {
|
||||||
return cached;
|
return cached;
|
||||||
}
|
}
|
||||||
FileTaskEntity dbTask = fileTaskMapper.selectById(taskId);
|
FileTaskEntity dbTask = fileTaskMapper.selectById(taskId);
|
||||||
@@ -396,7 +396,6 @@ public class PriceTrackTaskService {
|
|||||||
task.setCreatedAt(LocalDateTime.now());
|
task.setCreatedAt(LocalDateTime.now());
|
||||||
task.setUpdatedAt(LocalDateTime.now());
|
task.setUpdatedAt(LocalDateTime.now());
|
||||||
fileTaskMapper.insert(task);
|
fileTaskMapper.insert(task);
|
||||||
priceTrackTaskCacheService.saveTaskCache(task);
|
|
||||||
priceTrackTaskCacheService.touchTaskHeartbeat(task.getId());
|
priceTrackTaskCacheService.touchTaskHeartbeat(task.getId());
|
||||||
if (request.getLoopRunId() != null) {
|
if (request.getLoopRunId() != null) {
|
||||||
priceTrackLoopRunService.bindChildTask(request.getLoopRunId(), task.getId(), request.getRoundIndex(), request.getShopIndex());
|
priceTrackLoopRunService.bindChildTask(request.getLoopRunId(), task.getId(), request.getRoundIndex(), request.getShopIndex());
|
||||||
@@ -447,6 +446,7 @@ public class PriceTrackTaskService {
|
|||||||
throw new BusinessException("序列化任务数据失败");
|
throw new BusinessException("序列化任务数据失败");
|
||||||
}
|
}
|
||||||
fileTaskMapper.updateById(task);
|
fileTaskMapper.updateById(task);
|
||||||
|
priceTrackTaskCacheService.saveTaskCache(task);
|
||||||
|
|
||||||
// TODO: Python 侧轮询 RUNNING 任务,pickup 后开始处理
|
// TODO: Python 侧轮询 RUNNING 任务,pickup 后开始处理
|
||||||
// Java 侧通过 submitResult 接口接收 Python 回传结果
|
// Java 侧通过 submitResult 接口接收 Python 回传结果
|
||||||
@@ -1548,6 +1548,10 @@ public class PriceTrackTaskService {
|
|||||||
return taskDistributedLockService.acquire(MODULE_TYPE, taskId);
|
return taskDistributedLockService.acquire(MODULE_TYPE, taskId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static boolean hasText(String value) {
|
||||||
|
return value != null && !value.isBlank();
|
||||||
|
}
|
||||||
|
|
||||||
private void cleanupTaskCacheIfTerminal(Long taskId, String taskStatus) {
|
private void cleanupTaskCacheIfTerminal(Long taskId, String taskStatus) {
|
||||||
if (!"SUCCESS".equals(taskStatus) && !"FAILED".equals(taskStatus) && !"DELETE_EMPTY".equals(taskStatus)) {
|
if (!"SUCCESS".equals(taskStatus) && !"FAILED".equals(taskStatus) && !"DELETE_EMPTY".equals(taskStatus)) {
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ public class ProductRiskExcelAssemblyService {
|
|||||||
createTextCell(row, 0, firstNonBlank(dto == null ? null : dto.getShopName(), shopDisplayName));
|
createTextCell(row, 0, firstNonBlank(dto == null ? null : dto.getShopName(), shopDisplayName));
|
||||||
createTextCell(row, 1, dto == null ? null : dto.getProductAsinSku());
|
createTextCell(row, 1, dto == null ? null : dto.getProductAsinSku());
|
||||||
createTextCell(row, 2, dto == null ? null : dto.getStatus());
|
createTextCell(row, 2, dto == null ? null : dto.getStatus());
|
||||||
createTextCell(row, 3, hasBusinessData(dto) ? "是" : "否");
|
createTextCell(row, 3, completionDisplayValue(dto));
|
||||||
createTextCell(row, 4, dto == null ? null : dto.getRemoveAsin());
|
createTextCell(row, 4, dto == null ? null : dto.getRemoveAsin());
|
||||||
createTextCell(row, 5, dto == null ? null : dto.getRemoveStatus());
|
createTextCell(row, 5, dto == null ? null : dto.getRemoveStatus());
|
||||||
}
|
}
|
||||||
@@ -119,6 +119,32 @@ public class ProductRiskExcelAssemblyService {
|
|||||||
|| hasText(dto.getRemoveStatus()));
|
|| hasText(dto.getRemoveStatus()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static String completionDisplayValue(ProductRiskRowDto dto) {
|
||||||
|
if (dto == null || !hasBusinessData(dto)) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
return isCompleted(dto) ? "是" : "否";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean isCompleted(ProductRiskRowDto dto) {
|
||||||
|
return Boolean.TRUE.equals(dto.getDone())
|
||||||
|
|| indicatesCompleted(dto.getStatus())
|
||||||
|
|| indicatesCompleted(dto.getRemoveStatus());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean indicatesCompleted(String value) {
|
||||||
|
if (!hasText(value)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
String normalized = value.trim().toUpperCase(java.util.Locale.ROOT);
|
||||||
|
return normalized.contains("完成")
|
||||||
|
|| normalized.contains("成功")
|
||||||
|
|| normalized.contains("已处理")
|
||||||
|
|| normalized.contains("DONE")
|
||||||
|
|| normalized.contains("SUCCESS")
|
||||||
|
|| normalized.contains("COMPLETED");
|
||||||
|
}
|
||||||
|
|
||||||
private static boolean hasText(String value) {
|
private static boolean hasText(String value) {
|
||||||
return value != null && !value.isBlank();
|
return value != null && !value.isBlank();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ import java.util.ArrayList;
|
|||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.Iterator;
|
import java.util.Iterator;
|
||||||
|
import java.util.LinkedHashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
@@ -345,13 +346,20 @@ public class SimilarAsinImageEmbedder {
|
|||||||
|
|
||||||
private byte[] downloadWithRetry(String url) throws IOException, TimeoutException {
|
private byte[] downloadWithRetry(String url) throws IOException, TimeoutException {
|
||||||
validateHttpsUrl(url);
|
validateHttpsUrl(url);
|
||||||
|
List<String> candidates = downloadCandidates(url);
|
||||||
IOException last = null;
|
IOException last = null;
|
||||||
TimeoutException lastTimeout = null;
|
TimeoutException lastTimeout = null;
|
||||||
long waitSeconds = downloadTimeoutSeconds * 2L;
|
long waitSeconds = downloadTimeoutSeconds * 2L;
|
||||||
for (int attempt = 0; attempt <= DOWNLOAD_MAX_RETRY; attempt++) {
|
for (int attempt = 0; attempt <= DOWNLOAD_MAX_RETRY; attempt++) {
|
||||||
Future<byte[]> future = downloadPool.submit(() -> doFetch(url));
|
String attemptUrl = candidates.get(Math.min(attempt, candidates.size() - 1));
|
||||||
|
Future<byte[]> future = downloadPool.submit(() -> doFetch(attemptUrl));
|
||||||
try {
|
try {
|
||||||
return future.get(waitSeconds, TimeUnit.SECONDS);
|
byte[] bytes = future.get(waitSeconds, TimeUnit.SECONDS);
|
||||||
|
if (!attemptUrl.equals(url)) {
|
||||||
|
log.info("[similar-asin][image] download-fallback-success originalUrl={} usedUrl={} attempt={}/{}",
|
||||||
|
url, attemptUrl, attempt + 1, DOWNLOAD_MAX_RETRY + 1);
|
||||||
|
}
|
||||||
|
return bytes;
|
||||||
} catch (java.util.concurrent.ExecutionException ee) {
|
} catch (java.util.concurrent.ExecutionException ee) {
|
||||||
Throwable cause = ee.getCause();
|
Throwable cause = ee.getCause();
|
||||||
if (cause instanceof DownloadOversizeException doe) {
|
if (cause instanceof DownloadOversizeException doe) {
|
||||||
@@ -360,7 +368,7 @@ public class SimilarAsinImageEmbedder {
|
|||||||
if (cause instanceof IOException io) {
|
if (cause instanceof IOException io) {
|
||||||
last = io;
|
last = io;
|
||||||
lastTimeout = null;
|
lastTimeout = null;
|
||||||
logRetry(url, attempt, io);
|
logRetry(attemptUrl, attempt, io);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (cause instanceof RuntimeException re) {
|
if (cause instanceof RuntimeException re) {
|
||||||
@@ -371,11 +379,12 @@ public class SimilarAsinImageEmbedder {
|
|||||||
future.cancel(true);
|
future.cancel(true);
|
||||||
TimeoutException wrapped = new TimeoutException("image download timeout after attempt "
|
TimeoutException wrapped = new TimeoutException("image download timeout after attempt "
|
||||||
+ (attempt + 1) + "/" + (DOWNLOAD_MAX_RETRY + 1)
|
+ (attempt + 1) + "/" + (DOWNLOAD_MAX_RETRY + 1)
|
||||||
+ ", waitSeconds=" + waitSeconds);
|
+ ", waitSeconds=" + waitSeconds
|
||||||
|
+ ", url=" + attemptUrl);
|
||||||
wrapped.initCause(te);
|
wrapped.initCause(te);
|
||||||
lastTimeout = wrapped;
|
lastTimeout = wrapped;
|
||||||
last = null;
|
last = null;
|
||||||
logRetry(url, attempt, wrapped);
|
logRetry(attemptUrl, attempt, wrapped);
|
||||||
} catch (InterruptedException ie) {
|
} catch (InterruptedException ie) {
|
||||||
Thread.currentThread().interrupt();
|
Thread.currentThread().interrupt();
|
||||||
throw new IOException("image download interrupted", ie);
|
throw new IOException("image download interrupted", ie);
|
||||||
@@ -387,6 +396,50 @@ public class SimilarAsinImageEmbedder {
|
|||||||
throw last != null ? last : new IOException("image download failed without cause");
|
throw last != null ? last : new IOException("image download failed without cause");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static List<String> downloadCandidates(String url) {
|
||||||
|
List<String> single = List.of(url);
|
||||||
|
URI uri;
|
||||||
|
try {
|
||||||
|
uri = new URI(url);
|
||||||
|
} catch (URISyntaxException ex) {
|
||||||
|
return single;
|
||||||
|
}
|
||||||
|
String host = uri.getHost();
|
||||||
|
String rawPath = uri.getRawPath();
|
||||||
|
if (host == null || rawPath == null
|
||||||
|
|| !host.toLowerCase(Locale.ROOT).endsWith("media-amazon.com")
|
||||||
|
|| !rawPath.startsWith("/images/I/")) {
|
||||||
|
return single;
|
||||||
|
}
|
||||||
|
int slash = rawPath.lastIndexOf('/');
|
||||||
|
String dir = rawPath.substring(0, slash + 1);
|
||||||
|
String file = rawPath.substring(slash + 1);
|
||||||
|
int markerStart = file.lastIndexOf("._");
|
||||||
|
int markerEnd = markerStart < 0 ? -1 : file.indexOf("_.", markerStart + 2);
|
||||||
|
if (markerStart < 0 || markerEnd < 0) {
|
||||||
|
return single;
|
||||||
|
}
|
||||||
|
String prefix = file.substring(0, markerStart);
|
||||||
|
String ext = file.substring(markerEnd + 1);
|
||||||
|
LinkedHashSet<String> candidates = new LinkedHashSet<>();
|
||||||
|
candidates.add(url);
|
||||||
|
candidates.add(buildUrl(uri, dir + prefix + "._AC_SY450_" + ext));
|
||||||
|
candidates.add(buildUrl(uri, dir + prefix + "._AC_SX425_" + ext));
|
||||||
|
return new ArrayList<>(candidates);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String buildUrl(URI uri, String rawPath) {
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
sb.append(uri.getScheme()).append("://").append(uri.getRawAuthority()).append(rawPath);
|
||||||
|
if (uri.getRawQuery() != null) {
|
||||||
|
sb.append('?').append(uri.getRawQuery());
|
||||||
|
}
|
||||||
|
if (uri.getRawFragment() != null) {
|
||||||
|
sb.append('#').append(uri.getRawFragment());
|
||||||
|
}
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
|
||||||
private void logRetry(String url, int attempt, Exception ex) {
|
private void logRetry(String url, int attempt, Exception ex) {
|
||||||
if (attempt < DOWNLOAD_MAX_RETRY) {
|
if (attempt < DOWNLOAD_MAX_RETRY) {
|
||||||
log.debug("[similar-asin][image] download-retry url={} nextAttempt={}/{} err={}",
|
log.debug("[similar-asin][image] download-retry url={} nextAttempt={}/{} err={}",
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
INSERT INTO `columns` (`name`, `column_key`, `menu_type`, `route_path`, `sort_order`)
|
||||||
|
SELECT '软件版本管理', 'admin_version', 'admin', 'version', 80
|
||||||
|
WHERE NOT EXISTS (
|
||||||
|
SELECT 1 FROM columns WHERE column_key = 'admin_version'
|
||||||
|
);
|
||||||
|
|
||||||
|
INSERT INTO `columns` (`name`, `column_key`, `menu_type`, `route_path`, `sort_order`)
|
||||||
|
SELECT '数字人版本管理', 'digital_human_version', 'admin', 'digital-human-version', 81
|
||||||
|
WHERE NOT EXISTS (
|
||||||
|
SELECT 1 FROM columns WHERE column_key = 'digital_human_version'
|
||||||
|
);
|
||||||
|
|
||||||
|
UPDATE `columns`
|
||||||
|
SET `name` = '软件版本管理',
|
||||||
|
`route_path` = 'version',
|
||||||
|
`menu_type` = 'admin',
|
||||||
|
`sort_order` = 80
|
||||||
|
WHERE `column_key` = 'admin_version';
|
||||||
|
|
||||||
|
UPDATE `columns`
|
||||||
|
SET `name` = '数字人版本管理',
|
||||||
|
`route_path` = 'digital-human-version',
|
||||||
|
`menu_type` = 'admin',
|
||||||
|
`sort_order` = 81
|
||||||
|
WHERE `column_key` = 'digital_human_version';
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
package com.nanri.aiimage.modules.productrisk.service;
|
||||||
|
|
||||||
|
import com.nanri.aiimage.modules.productrisk.model.dto.ProductRiskRowDto;
|
||||||
|
import com.nanri.aiimage.modules.productrisk.model.enums.ProductRiskCountryCode;
|
||||||
|
import org.apache.poi.ss.usermodel.Sheet;
|
||||||
|
import org.apache.poi.ss.usermodel.Workbook;
|
||||||
|
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertNotEquals;
|
||||||
|
|
||||||
|
class ProductRiskExcelAssemblyServiceTest {
|
||||||
|
|
||||||
|
private final ProductRiskExcelAssemblyService service = new ProductRiskExcelAssemblyService();
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void completionColumnUsesChineseValueAndCompletedStatusWinsOverFalseDone() throws Exception {
|
||||||
|
Path xlsx = Files.createTempFile("product-risk-result-", ".xlsx");
|
||||||
|
try {
|
||||||
|
ProductRiskRowDto completed = row("SKU-1", "处理完成", Boolean.FALSE);
|
||||||
|
ProductRiskRowDto running = row("SKU-2", "处理中", Boolean.FALSE);
|
||||||
|
ProductRiskRowDto done = row("SKU-3", null, Boolean.TRUE);
|
||||||
|
|
||||||
|
service.writeWorkbook(
|
||||||
|
xlsx.toFile(),
|
||||||
|
"店铺A",
|
||||||
|
Map.of(ProductRiskCountryCode.DE.getSheetName(), List.of(completed, running, done)));
|
||||||
|
|
||||||
|
try (InputStream in = Files.newInputStream(xlsx);
|
||||||
|
Workbook workbook = new XSSFWorkbook(in)) {
|
||||||
|
Sheet sheet = workbook.getSheet(ProductRiskCountryCode.DE.getSheetName());
|
||||||
|
assertEquals("完成", sheet.getRow(0).getCell(3).getStringCellValue());
|
||||||
|
assertEquals("是", sheet.getRow(1).getCell(3).getStringCellValue());
|
||||||
|
assertNotEquals("false", sheet.getRow(1).getCell(3).getStringCellValue());
|
||||||
|
assertEquals("否", sheet.getRow(2).getCell(3).getStringCellValue());
|
||||||
|
assertEquals("是", sheet.getRow(3).getCell(3).getStringCellValue());
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
Files.deleteIfExists(xlsx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ProductRiskRowDto row(String productAsinSku, String status, Boolean done) {
|
||||||
|
ProductRiskRowDto row = new ProductRiskRowDto();
|
||||||
|
row.setShopName("店铺A");
|
||||||
|
row.setProductAsinSku(productAsinSku);
|
||||||
|
row.setStatus(status);
|
||||||
|
row.setDone(done);
|
||||||
|
return row;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -177,6 +177,25 @@ class SimilarAsinImageEmbedderTest {
|
|||||||
assertNotNull(req.header("Accept-Language"));
|
assertNotNull(req.header("Accept-Language"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void amazonImageDownloadCandidatesTrySmallerVariantsWithinRetryBudget() {
|
||||||
|
List<String> candidates = SimilarAsinImageEmbedder.downloadCandidates(
|
||||||
|
"https://m.media-amazon.com/images/I/71AGS2r4JML._AC_SX679_.jpg");
|
||||||
|
|
||||||
|
assertEquals(List.of(
|
||||||
|
"https://m.media-amazon.com/images/I/71AGS2r4JML._AC_SX679_.jpg",
|
||||||
|
"https://m.media-amazon.com/images/I/71AGS2r4JML._AC_SY450_.jpg",
|
||||||
|
"https://m.media-amazon.com/images/I/71AGS2r4JML._AC_SX425_.jpg"), candidates);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void downloadCandidatesLeaveNonAmazonAndUnsignedUrlsUntouched() {
|
||||||
|
String coze = "https://lf9-bot-platform-tos-sign.coze.cn/bot-studio-bot-platform/bot_files/1/image/jpeg/2/merged_image.jpg?x-expires=1&x-signature=a";
|
||||||
|
assertEquals(List.of(coze), SimilarAsinImageEmbedder.downloadCandidates(coze));
|
||||||
|
assertEquals(List.of("https://cbu01.alicdn.com/O1CN01x.jpg"),
|
||||||
|
SimilarAsinImageEmbedder.downloadCandidates("https://cbu01.alicdn.com/O1CN01x.jpg"));
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void buildImageRequestUsesDownloadHeadersForCozeSignedImages() {
|
void buildImageRequestUsesDownloadHeadersForCozeSignedImages() {
|
||||||
var req = SimilarAsinImageEmbedder.buildImageRequest(
|
var req = SimilarAsinImageEmbedder.buildImageRequest(
|
||||||
|
|||||||
@@ -111,6 +111,11 @@ ADMIN_MENU_ACCESS_CONFIG.update({
|
|||||||
'route_path': 'version',
|
'route_path': 'version',
|
||||||
'error': '无权访问版本管理模块',
|
'error': '无权访问版本管理模块',
|
||||||
},
|
},
|
||||||
|
'digital-human-version': {
|
||||||
|
'column_key': 'digital_human_version',
|
||||||
|
'route_path': 'digital-human-version',
|
||||||
|
'error': '无权访问数字人版本管理模块',
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
@@ -1436,7 +1441,7 @@ def list_versions():
|
|||||||
@admin_required
|
@admin_required
|
||||||
def list_digital_human_versions():
|
def list_digital_human_versions():
|
||||||
"""代理:查询数字人版本列表"""
|
"""代理:查询数字人版本列表"""
|
||||||
_, _, denied = _ensure_admin_menu_access('version')
|
_, _, denied = _ensure_admin_menu_access('digital-human-version')
|
||||||
if denied:
|
if denied:
|
||||||
return denied
|
return denied
|
||||||
page = request.args.get('page', '1')
|
page = request.args.get('page', '1')
|
||||||
@@ -1460,6 +1465,9 @@ def list_digital_human_versions():
|
|||||||
@admin_required
|
@admin_required
|
||||||
def get_latest_digital_human_version():
|
def get_latest_digital_human_version():
|
||||||
"""代理:获取最新数字人版本"""
|
"""代理:获取最新数字人版本"""
|
||||||
|
_, _, denied = _ensure_admin_menu_access('digital-human-version')
|
||||||
|
if denied:
|
||||||
|
return denied
|
||||||
result, error_response, status_code = _proxy_backend_java(
|
result, error_response, status_code = _proxy_backend_java(
|
||||||
'GET',
|
'GET',
|
||||||
'/api/digital-human/versions/latest',
|
'/api/digital-human/versions/latest',
|
||||||
@@ -1474,7 +1482,7 @@ def get_latest_digital_human_version():
|
|||||||
@admin_required
|
@admin_required
|
||||||
def upload_digital_human_version():
|
def upload_digital_human_version():
|
||||||
"""代理:上传数字人版本"""
|
"""代理:上传数字人版本"""
|
||||||
_, _, denied = _ensure_admin_menu_access('version')
|
_, _, denied = _ensure_admin_menu_access('digital-human-version')
|
||||||
if denied:
|
if denied:
|
||||||
return denied
|
return denied
|
||||||
|
|
||||||
@@ -1511,7 +1519,7 @@ def upload_digital_human_version():
|
|||||||
@admin_required
|
@admin_required
|
||||||
def release_digital_human_version(version):
|
def release_digital_human_version(version):
|
||||||
"""代理:发布数字人版本"""
|
"""代理:发布数字人版本"""
|
||||||
_, _, denied = _ensure_admin_menu_access('version')
|
_, _, denied = _ensure_admin_menu_access('digital-human-version')
|
||||||
if denied:
|
if denied:
|
||||||
return denied
|
return denied
|
||||||
result, error_response, status_code = _proxy_backend_java(
|
result, error_response, status_code = _proxy_backend_java(
|
||||||
@@ -1528,7 +1536,7 @@ def release_digital_human_version(version):
|
|||||||
@admin_required
|
@admin_required
|
||||||
def set_latest_digital_human_version(version):
|
def set_latest_digital_human_version(version):
|
||||||
"""代理:设为最新数字人版本"""
|
"""代理:设为最新数字人版本"""
|
||||||
_, _, denied = _ensure_admin_menu_access('version')
|
_, _, denied = _ensure_admin_menu_access('digital-human-version')
|
||||||
if denied:
|
if denied:
|
||||||
return denied
|
return denied
|
||||||
result, error_response, status_code = _proxy_backend_java(
|
result, error_response, status_code = _proxy_backend_java(
|
||||||
@@ -1545,7 +1553,7 @@ def set_latest_digital_human_version(version):
|
|||||||
@admin_required
|
@admin_required
|
||||||
def delete_digital_human_version(version):
|
def delete_digital_human_version(version):
|
||||||
"""代理:删除数字人版本"""
|
"""代理:删除数字人版本"""
|
||||||
_, _, denied = _ensure_admin_menu_access('version')
|
_, _, denied = _ensure_admin_menu_access('digital-human-version')
|
||||||
if denied:
|
if denied:
|
||||||
return denied
|
return denied
|
||||||
result, error_response, status_code = _proxy_backend_java(
|
result, error_response, status_code = _proxy_backend_java(
|
||||||
|
|||||||
@@ -59,8 +59,8 @@ file_url_pre = f"https://{bucket}.oss-cn-hangzhou.aliyuncs.com/"
|
|||||||
backend_java_base_url, backend_java_base_url_source = _get_env(
|
backend_java_base_url, backend_java_base_url_source = _get_env(
|
||||||
"java_api_base",
|
"java_api_base",
|
||||||
"BACKEND_JAVA_BASE_URL",
|
"BACKEND_JAVA_BASE_URL",
|
||||||
# default="http://api.aishufu.top:18080/",
|
default="http://api.aishufu.top:18080/",
|
||||||
default="http://127.0.0.1:18080/",
|
# default="http://127.0.0.1:18080/",
|
||||||
)
|
)
|
||||||
backend_java_base_url = backend_java_base_url.rstrip("/")
|
backend_java_base_url = backend_java_base_url.rstrip("/")
|
||||||
os.environ["OSS_ACCESS_KEY_ID"] = accessKeyId
|
os.environ["OSS_ACCESS_KEY_ID"] = accessKeyId
|
||||||
|
|||||||
@@ -108,7 +108,8 @@
|
|||||||
'query-asin': 'panel-query-asin',
|
'query-asin': 'panel-query-asin',
|
||||||
'product-categories': 'panel-product-categories',
|
'product-categories': 'panel-product-categories',
|
||||||
'history': 'panel-history',
|
'history': 'panel-history',
|
||||||
'version': 'panel-version'
|
'version': 'panel-version',
|
||||||
|
'digital-human-version': 'panel-digital-human-version'
|
||||||
};
|
};
|
||||||
function runTabLoader(tabName) {
|
function runTabLoader(tabName) {
|
||||||
if (tabName === 'users') { loadUsers(1); loadColumnsForPermission(); }
|
if (tabName === 'users') { loadUsers(1); loadColumnsForPermission(); }
|
||||||
@@ -121,7 +122,8 @@
|
|||||||
else if (tabName === 'query-asin') loadQueryAsin(1);
|
else if (tabName === 'query-asin') loadQueryAsin(1);
|
||||||
else if (tabName === 'product-categories') loadProductCategories();
|
else if (tabName === 'product-categories') loadProductCategories();
|
||||||
else if (tabName === 'history') loadHistory(1);
|
else if (tabName === 'history') loadHistory(1);
|
||||||
else if (tabName === 'version') loadVersions();
|
else if (tabName === 'version') loadSoftwareVersions();
|
||||||
|
else if (tabName === 'digital-human-version') loadDigitalHumanVersions();
|
||||||
}
|
}
|
||||||
function hideAllAdminPanels() {
|
function hideAllAdminPanels() {
|
||||||
document.querySelectorAll('.tab-panel').forEach(function (panel) {
|
document.querySelectorAll('.tab-panel').forEach(function (panel) {
|
||||||
@@ -3519,12 +3521,38 @@
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
function loadVersions(page) {
|
function loadSoftwareVersions() {
|
||||||
|
fetch('/api/admin/versions')
|
||||||
|
.then(function (r) { return r.json(); })
|
||||||
|
.then(function (res) {
|
||||||
|
var tbody = document.getElementById('versionListBody');
|
||||||
|
var pagination = document.getElementById('versionPagination');
|
||||||
|
if (pagination) pagination.innerHTML = '';
|
||||||
|
if (!res.success) {
|
||||||
|
tbody.innerHTML = '<tr><td colspan="4" class="empty-tip">加载失败: ' + (res.error || '') + '</td></tr>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var items = res.items || [];
|
||||||
|
if (!items.length) {
|
||||||
|
tbody.innerHTML = '<tr><td colspan="4" class="empty-tip">暂无版本记录</td></tr>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
tbody.innerHTML = items.map(function (v) {
|
||||||
|
var url = v.file_url || '';
|
||||||
|
return '<tr><td>' + (v.version || '') + '</td><td><a href="' + url + '" target="_blank" rel="noopener">' + url + '</a></td><td>' + (v.created_at || '') + '</td><td><a href="' + url + '" download class="btn btn-sm">下载</a></td></tr>';
|
||||||
|
}).join('');
|
||||||
|
})
|
||||||
|
.catch(function () {
|
||||||
|
document.getElementById('versionListBody').innerHTML = '<tr><td colspan="4" class="empty-tip">请求失败</td></tr>';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadDigitalHumanVersions(page) {
|
||||||
page = page || 1;
|
page = page || 1;
|
||||||
fetch('/api/admin/digital-human-versions?page=' + page + '&pageSize=20')
|
fetch('/api/admin/digital-human-versions?page=' + page + '&pageSize=20')
|
||||||
.then(function (r) { return r.json(); })
|
.then(function (r) { return r.json(); })
|
||||||
.then(function (res) {
|
.then(function (res) {
|
||||||
var tbody = document.getElementById('versionListBody');
|
var tbody = document.getElementById('digitalHumanVersionListBody');
|
||||||
if (!res.success) {
|
if (!res.success) {
|
||||||
tbody.innerHTML = '<tr><td colspan="8" class="empty-tip">加载失败: ' + (res.message || res.error || '') + '</td></tr>';
|
tbody.innerHTML = '<tr><td colspan="8" class="empty-tip">加载失败: ' + (res.message || res.error || '') + '</td></tr>';
|
||||||
return;
|
return;
|
||||||
@@ -3585,12 +3613,12 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 分页
|
// 分页
|
||||||
var pagination = document.getElementById('versionPagination');
|
var pagination = document.getElementById('digitalHumanVersionPagination');
|
||||||
if (pages > 1) {
|
if (pages > 1) {
|
||||||
var html = '';
|
var html = '';
|
||||||
for (var i = 1; i <= pages; i++) {
|
for (var i = 1; i <= pages; i++) {
|
||||||
var active = i === current ? ' active' : '';
|
var active = i === current ? ' active' : '';
|
||||||
html += '<span class="page-item' + active + '" onclick="loadVersions(' + i + ')">' + i + '</span>';
|
html += '<span class="page-item' + active + '" onclick="loadDigitalHumanVersions(' + i + ')">' + i + '</span>';
|
||||||
}
|
}
|
||||||
pagination.innerHTML = html;
|
pagination.innerHTML = html;
|
||||||
} else {
|
} else {
|
||||||
@@ -3598,14 +3626,13 @@
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(function (err) {
|
.catch(function (err) {
|
||||||
document.getElementById('versionListBody').innerHTML = '<tr><td colspan="8" class="empty-tip">请求失败: ' + (err.message || '') + '</td></tr>';
|
document.getElementById('digitalHumanVersionListBody').innerHTML = '<tr><td colspan="8" class="empty-tip">请求失败: ' + (err.message || '') + '</td></tr>';
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
window.loadDigitalHumanVersions = loadDigitalHumanVersions;
|
||||||
|
|
||||||
document.getElementById('btnUploadVersion').onclick = function () {
|
document.getElementById('btnUploadVersion').onclick = function () {
|
||||||
var version = (document.getElementById('versionNumber').value || '').trim();
|
var version = (document.getElementById('versionNumber').value || '').trim();
|
||||||
var minClientVersion = (document.getElementById('versionMinClientVersion').value || '').trim();
|
|
||||||
var changelog = (document.getElementById('versionChangelog').value || '').trim();
|
|
||||||
var fileInput = document.getElementById('versionZip');
|
var fileInput = document.getElementById('versionZip');
|
||||||
var msgEl = document.getElementById('msgVersion');
|
var msgEl = document.getElementById('msgVersion');
|
||||||
msgEl.textContent = '';
|
msgEl.textContent = '';
|
||||||
@@ -3628,6 +3655,62 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var formData = new FormData();
|
||||||
|
formData.append('file', file);
|
||||||
|
formData.append('version', version);
|
||||||
|
|
||||||
|
msgEl.textContent = '上传中,请稍候...';
|
||||||
|
msgEl.classList.remove('err', 'ok');
|
||||||
|
|
||||||
|
fetch('/api/admin/version', {
|
||||||
|
method: 'POST',
|
||||||
|
body: formData
|
||||||
|
})
|
||||||
|
.then(function (r) { return r.json(); })
|
||||||
|
.then(function (res) {
|
||||||
|
if (res.success) {
|
||||||
|
msgEl.textContent = '发布成功。版本:' + res.version + ',链接:' + (res.file_url || '');
|
||||||
|
msgEl.classList.add('ok');
|
||||||
|
document.getElementById('versionNumber').value = '';
|
||||||
|
fileInput.value = '';
|
||||||
|
loadSoftwareVersions();
|
||||||
|
} else {
|
||||||
|
msgEl.textContent = res.error || res.message || '上传失败';
|
||||||
|
msgEl.classList.add('err');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(function (err) {
|
||||||
|
msgEl.textContent = '请求失败: ' + (err.message || '');
|
||||||
|
msgEl.classList.add('err');
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
document.getElementById('btnUploadDigitalHumanVersion').onclick = function () {
|
||||||
|
var version = (document.getElementById('digitalHumanVersionNumber').value || '').trim();
|
||||||
|
var minClientVersion = (document.getElementById('digitalHumanMinClientVersion').value || '').trim();
|
||||||
|
var changelog = (document.getElementById('digitalHumanVersionChangelog').value || '').trim();
|
||||||
|
var fileInput = document.getElementById('digitalHumanVersionZip');
|
||||||
|
var msgEl = document.getElementById('msgDigitalHumanVersion');
|
||||||
|
msgEl.textContent = '';
|
||||||
|
msgEl.className = 'msg';
|
||||||
|
|
||||||
|
if (!version) {
|
||||||
|
msgEl.textContent = '请填写版本号';
|
||||||
|
msgEl.classList.add('err');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!fileInput.files || fileInput.files.length === 0) {
|
||||||
|
msgEl.textContent = '请选择 zip 压缩包';
|
||||||
|
msgEl.classList.add('err');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var file = fileInput.files[0];
|
||||||
|
if (!(file.name || '').toLowerCase().endsWith('.zip')) {
|
||||||
|
msgEl.textContent = '仅支持 .zip 格式';
|
||||||
|
msgEl.classList.add('err');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
var formData = new FormData();
|
var formData = new FormData();
|
||||||
formData.append('file', file);
|
formData.append('file', file);
|
||||||
formData.append('version', version);
|
formData.append('version', version);
|
||||||
@@ -3646,11 +3729,11 @@
|
|||||||
if (res.success && res.data) {
|
if (res.success && res.data) {
|
||||||
msgEl.textContent = '上传成功!版本:' + res.data.version + '(状态:草稿,请在列表中点击"发布"按钮)';
|
msgEl.textContent = '上传成功!版本:' + res.data.version + '(状态:草稿,请在列表中点击"发布"按钮)';
|
||||||
msgEl.classList.add('ok');
|
msgEl.classList.add('ok');
|
||||||
document.getElementById('versionNumber').value = '';
|
document.getElementById('digitalHumanVersionNumber').value = '';
|
||||||
document.getElementById('versionMinClientVersion').value = '';
|
document.getElementById('digitalHumanMinClientVersion').value = '';
|
||||||
document.getElementById('versionChangelog').value = '';
|
document.getElementById('digitalHumanVersionChangelog').value = '';
|
||||||
fileInput.value = '';
|
fileInput.value = '';
|
||||||
loadVersions();
|
loadDigitalHumanVersions();
|
||||||
} else {
|
} else {
|
||||||
msgEl.textContent = res.message || res.error || '上传失败';
|
msgEl.textContent = res.message || res.error || '上传失败';
|
||||||
msgEl.classList.add('err');
|
msgEl.classList.add('err');
|
||||||
@@ -3671,7 +3754,7 @@
|
|||||||
.then(function (res) {
|
.then(function (res) {
|
||||||
if (res.success) {
|
if (res.success) {
|
||||||
alert('发布成功!');
|
alert('发布成功!');
|
||||||
loadVersions();
|
loadDigitalHumanVersions();
|
||||||
} else {
|
} else {
|
||||||
alert('发布失败:' + (res.message || res.error || ''));
|
alert('发布失败:' + (res.message || res.error || ''));
|
||||||
}
|
}
|
||||||
@@ -3690,7 +3773,7 @@
|
|||||||
.then(function (res) {
|
.then(function (res) {
|
||||||
if (res.success) {
|
if (res.success) {
|
||||||
alert('设置成功!版本 ' + version + ' 已标记为最新。');
|
alert('设置成功!版本 ' + version + ' 已标记为最新。');
|
||||||
loadVersions();
|
loadDigitalHumanVersions();
|
||||||
} else {
|
} else {
|
||||||
alert('设置失败:' + (res.message || res.error || ''));
|
alert('设置失败:' + (res.message || res.error || ''));
|
||||||
}
|
}
|
||||||
@@ -3709,7 +3792,7 @@
|
|||||||
.then(function (res) {
|
.then(function (res) {
|
||||||
if (res.success) {
|
if (res.success) {
|
||||||
alert('删除成功!');
|
alert('删除成功!');
|
||||||
loadVersions();
|
loadDigitalHumanVersions();
|
||||||
} else {
|
} else {
|
||||||
alert('删除失败:' + (res.message || res.error || ''));
|
alert('删除失败:' + (res.message || res.error || ''));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -793,7 +793,8 @@
|
|||||||
<div class="tab" data-tab="query-asin">查询 ASIN</div>
|
<div class="tab" data-tab="query-asin">查询 ASIN</div>
|
||||||
<div class="tab" data-tab="product-categories">商品类目</div>
|
<div class="tab" data-tab="product-categories">商品类目</div>
|
||||||
<div class="tab" data-tab="history">查看生成记录</div>
|
<div class="tab" data-tab="history">查看生成记录</div>
|
||||||
<div class="tab" data-tab="version">版本管理</div>
|
<div class="tab" data-tab="version">软件版本管理</div>
|
||||||
|
<div class="tab" data-tab="digital-human-version">数字人版本管理</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 用户管理 -->
|
<!-- 用户管理 -->
|
||||||
@@ -916,8 +917,46 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 数字人版本管理 -->
|
<!-- 软件版本管理 -->
|
||||||
<div id="panel-version" class="tab-panel">
|
<div id="panel-version" class="tab-panel">
|
||||||
|
<div class="form-box">
|
||||||
|
<h3 style="margin-bottom:16px;font-size:15px;">上传新版本</h3>
|
||||||
|
<p style="margin-bottom:16px;font-size:13px;color:#666;">
|
||||||
|
上传软件 ZIP 包,系统会自动计算 MD5 并存储到 OSS。
|
||||||
|
</p>
|
||||||
|
<div class="form-row">
|
||||||
|
<div class="form-group" style="min-width:200px;">
|
||||||
|
<label>版本号 *</label>
|
||||||
|
<input type="text" id="versionNumber" placeholder="例如:1.0.0">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>ZIP 压缩包 *</label>
|
||||||
|
<input type="file" id="versionZip" accept=".zip">
|
||||||
|
<p style="font-size:12px;color:#999;margin-top:4px;">仅支持 .zip 格式</p>
|
||||||
|
</div>
|
||||||
|
<button class="btn" id="btnUploadVersion">上传软件版本</button>
|
||||||
|
<p class="msg" id="msgVersion"></p>
|
||||||
|
</div>
|
||||||
|
<div class="panel-box">
|
||||||
|
<h3 style="margin-bottom:16px;font-size:15px;">版本列表</h3>
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th style="width:120px;">版本号</th>
|
||||||
|
<th>下载链接</th>
|
||||||
|
<th style="width:160px;">创建时间</th>
|
||||||
|
<th style="width:120px;">操作</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="versionListBody"></tbody>
|
||||||
|
</table>
|
||||||
|
<div class="pagination" id="versionPagination"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 数字人版本管理 -->
|
||||||
|
<div id="panel-digital-human-version" class="tab-panel">
|
||||||
<div class="form-box">
|
<div class="form-box">
|
||||||
<h3 style="margin-bottom:16px;font-size:15px;">上传新版本</h3>
|
<h3 style="margin-bottom:16px;font-size:15px;">上传新版本</h3>
|
||||||
<p style="margin-bottom:16px;font-size:13px;color:#666;">
|
<p style="margin-bottom:16px;font-size:13px;color:#666;">
|
||||||
@@ -926,24 +965,23 @@
|
|||||||
<div class="form-row">
|
<div class="form-row">
|
||||||
<div class="form-group" style="min-width:200px;">
|
<div class="form-group" style="min-width:200px;">
|
||||||
<label>版本号 *</label>
|
<label>版本号 *</label>
|
||||||
<input type="text" id="versionNumber" placeholder="例如:1.0.0">
|
<input type="text" id="digitalHumanVersionNumber" placeholder="例如:1.0.0">
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group" style="min-width:200px;">
|
<div class="form-group" style="min-width:200px;">
|
||||||
<label>最低客户端版本</label>
|
<label>最低客户端版本</label>
|
||||||
<input type="text" id="versionMinClientVersion" placeholder="例如:2.0.0(可选)">
|
<input type="text" id="digitalHumanMinClientVersion" placeholder="例如:2.0.0(可选)">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>更新日志</label>
|
<label>更新日志</label>
|
||||||
<textarea id="versionChangelog" rows="3" placeholder="例如:修复数字人口型同步问题,优化启动速度" style="width:100%;padding:10px;border:1px solid #ddd;border-radius:6px;resize:vertical;"></textarea>
|
<textarea id="digitalHumanVersionChangelog" rows="3" placeholder="例如:修复数字人口型同步问题,优化启动速度" style="width:100%;padding:10px;border:1px solid #ddd;border-radius:6px;resize:vertical;"></textarea>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>ZIP 压缩包 *</label>
|
<label>ZIP 压缩包 *</label>
|
||||||
<input type="file" id="versionZip" accept=".zip">
|
<input type="file" id="digitalHumanVersionZip" accept=".zip">
|
||||||
<p style="font-size:12px;color:#999;margin-top:4px;">仅支持 .zip 格式,文件名:ShuFuDigitalHuman.zip</p>
|
|
||||||
</div>
|
</div>
|
||||||
<button class="btn" id="btnUploadVersion">上传(草稿状态)</button>
|
<button class="btn" id="btnUploadDigitalHumanVersion">上传(草稿状态)</button>
|
||||||
<p class="msg" id="msgVersion"></p>
|
<p class="msg" id="msgDigitalHumanVersion"></p>
|
||||||
</div>
|
</div>
|
||||||
<div class="panel-box">
|
<div class="panel-box">
|
||||||
<h3 style="margin-bottom:16px;font-size:15px;">版本列表</h3>
|
<h3 style="margin-bottom:16px;font-size:15px;">版本列表</h3>
|
||||||
@@ -960,9 +998,9 @@
|
|||||||
<th style="width:240px;">操作</th>
|
<th style="width:240px;">操作</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody id="versionListBody"></tbody>
|
<tbody id="digitalHumanVersionListBody"></tbody>
|
||||||
</table>
|
</table>
|
||||||
<div class="pagination" id="versionPagination"></div>
|
<div class="pagination" id="digitalHumanVersionPagination"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user